diff options
Diffstat (limited to 'libs')
491 files changed, 26293 insertions, 19790 deletions
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/common/CommonFoldingFeature.java b/libs/WindowManager/Jetpack/src/androidx/window/common/CommonFoldingFeature.java index 921552b6cfbb..68ff806c6765 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/common/CommonFoldingFeature.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/common/CommonFoldingFeature.java @@ -199,7 +199,7 @@ public final class CommonFoldingFeature { throw new IllegalArgumentException( "Display feature rectangle cannot have zero width and height simultaneously."); } - this.mRect = rect; + this.mRect = new Rect(rect); } /** Returns the type of the feature. */ @@ -217,7 +217,7 @@ public final class CommonFoldingFeature { /** Returns the bounds of the feature. */ @NonNull public Rect getRect() { - return mRect; + return new Rect(mRect); } @Override diff --git a/libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java b/libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java index fdcb7be597d5..cc2bb63ca8e1 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java @@ -22,7 +22,6 @@ import static androidx.window.common.CommonFoldingFeature.COMMON_STATE_UNKNOWN; import static androidx.window.common.CommonFoldingFeature.parseListFromString; import android.annotation.NonNull; -import android.annotation.Nullable; import android.content.Context; import android.hardware.devicestate.DeviceStateManager; import android.hardware.devicestate.DeviceStateManager.DeviceStateCallback; @@ -30,22 +29,25 @@ 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.util.DataProducer; import com.android.internal.R; +import java.util.ArrayList; import java.util.List; +import java.util.Objects; import java.util.Optional; import java.util.Set; +import java.util.function.Consumer; /** - * An implementation of {@link androidx.window.util.DataProducer} 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}. + * An implementation of {@link androidx.window.util.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}. */ -public final class DeviceStateManagerFoldingFeatureProducer extends - BaseDataProducer<List<CommonFoldingFeature>> { +public final class DeviceStateManagerFoldingFeatureProducer + extends BaseDataProducer<List<CommonFoldingFeature>> { private static final String TAG = DeviceStateManagerFoldingFeatureProducer.class.getSimpleName(); private static final boolean DEBUG = false; @@ -54,15 +56,11 @@ public final class DeviceStateManagerFoldingFeatureProducer extends private int mCurrentDeviceState = INVALID_DEVICE_STATE; - private final DeviceStateCallback mDeviceStateCallback = (state) -> { - mCurrentDeviceState = state; - notifyDataChanged(); - }; @NonNull - private final DataProducer<String> mRawFoldSupplier; + private final BaseDataProducer<String> mRawFoldSupplier; public DeviceStateManagerFoldingFeatureProducer(@NonNull Context context, - @NonNull DataProducer<String> rawFoldSupplier) { + @NonNull BaseDataProducer<String> rawFoldSupplier) { mRawFoldSupplier = rawFoldSupplier; String[] deviceStatePosturePairs = context.getResources() .getStringArray(R.array.config_device_state_postures); @@ -70,7 +68,8 @@ public final class DeviceStateManagerFoldingFeatureProducer extends String[] deviceStatePostureMapping = deviceStatePosturePair.split(":"); if (deviceStatePostureMapping.length != 2) { if (DEBUG) { - Log.e(TAG, "Malformed device state posture pair: " + deviceStatePosturePair); + Log.e(TAG, "Malformed device state posture pair: " + + deviceStatePosturePair); } continue; } @@ -82,7 +81,8 @@ public final class DeviceStateManagerFoldingFeatureProducer extends posture = Integer.parseInt(deviceStatePostureMapping[1]); } catch (NumberFormatException e) { if (DEBUG) { - Log.e(TAG, "Failed to parse device state or posture: " + deviceStatePosturePair, + Log.e(TAG, "Failed to parse device state or posture: " + + deviceStatePosturePair, e); } continue; @@ -92,32 +92,95 @@ public final class DeviceStateManagerFoldingFeatureProducer extends } if (mDeviceStateToPostureMap.size() > 0) { - context.getSystemService(DeviceStateManager.class) - .registerCallback(context.getMainExecutor(), mDeviceStateCallback); + DeviceStateCallback deviceStateCallback = (state) -> { + mCurrentDeviceState = state; + mRawFoldSupplier.getData(this::notifyFoldingFeatureChange); + }; + Objects.requireNonNull(context.getSystemService(DeviceStateManager.class)) + .registerCallback(context.getMainExecutor(), deviceStateCallback); } } - @Override - @Nullable - public Optional<List<CommonFoldingFeature>> getData() { - final int globalHingeState = globalHingeState(); - Optional<String> displayFeaturesString = mRawFoldSupplier.getData(); - if (displayFeaturesString.isEmpty() || TextUtils.isEmpty(displayFeaturesString.get())) { - return Optional.empty(); + /** + * Add a callback to mCallbacks if there is no device state. This callback will be run + * once a device state is set. Otherwise,run the callback immediately. + */ + private void runCallbackWhenValidState(@NonNull Consumer<List<CommonFoldingFeature>> callback, + String displayFeaturesString) { + if (isCurrentStateValid()) { + callback.accept(calculateFoldingFeature(displayFeaturesString)); + } else { + // This callback will be added to mCallbacks and removed once it runs once. + AcceptOnceConsumer<List<CommonFoldingFeature>> singleRunCallback = + new AcceptOnceConsumer<>(this, callback); + addDataChangedCallback(singleRunCallback); } - return Optional.of(parseListFromString(displayFeaturesString.get(), globalHingeState)); + } + + /** + * Checks to find {@link DeviceStateManagerFoldingFeatureProducer#mCurrentDeviceState} in the + * {@link DeviceStateManagerFoldingFeatureProducer#mDeviceStateToPostureMap} which was + * initialized in the constructor of {@link DeviceStateManagerFoldingFeatureProducer}. + * Returns a boolean value of whether the device state is valid. + */ + private boolean isCurrentStateValid() { + // If the device state is not found in the map, indexOfKey returns a negative number. + return mDeviceStateToPostureMap.indexOfKey(mCurrentDeviceState) >= 0; } @Override - protected void onListenersChanged(Set<Runnable> callbacks) { + protected void onListenersChanged( + @NonNull Set<Consumer<List<CommonFoldingFeature>>> callbacks) { super.onListenersChanged(callbacks); if (callbacks.isEmpty()) { - mRawFoldSupplier.removeDataChangedCallback(this::notifyDataChanged); + mCurrentDeviceState = INVALID_DEVICE_STATE; + mRawFoldSupplier.removeDataChangedCallback(this::notifyFoldingFeatureChange); + } else { + mRawFoldSupplier.addDataChangedCallback(this::notifyFoldingFeatureChange); + } + } + + @NonNull + @Override + public Optional<List<CommonFoldingFeature>> getCurrentData() { + Optional<String> displayFeaturesString = mRawFoldSupplier.getCurrentData(); + if (!isCurrentStateValid()) { + return Optional.empty(); + } else { + return displayFeaturesString.map(this::calculateFoldingFeature); + } + } + + /** + * Adds the data to the storeFeaturesConsumer when the data is ready. + * @param storeFeaturesConsumer a consumer to collect the data when it is first available. + */ + public void getData(Consumer<List<CommonFoldingFeature>> storeFeaturesConsumer) { + mRawFoldSupplier.getData((String displayFeaturesString) -> { + if (TextUtils.isEmpty(displayFeaturesString)) { + storeFeaturesConsumer.accept(new ArrayList<>()); + } else { + runCallbackWhenValidState(storeFeaturesConsumer, displayFeaturesString); + } + }); + } + + private void notifyFoldingFeatureChange(String displayFeaturesString) { + if (!isCurrentStateValid()) { + return; + } + if (TextUtils.isEmpty(displayFeaturesString)) { + notifyDataChanged(new ArrayList<>()); } else { - mRawFoldSupplier.addDataChangedCallback(this::notifyDataChanged); + notifyDataChanged(calculateFoldingFeature(displayFeaturesString)); } } + private List<CommonFoldingFeature> calculateFoldingFeature(String displayFeaturesString) { + final int globalHingeState = globalHingeState(); + return parseListFromString(displayFeaturesString, globalHingeState); + } + private int globalHingeState() { return mDeviceStateToPostureMap.get(mCurrentDeviceState, COMMON_STATE_UNKNOWN); } diff --git a/libs/WindowManager/Jetpack/src/androidx/window/common/RawFoldingFeatureProducer.java b/libs/WindowManager/Jetpack/src/androidx/window/common/RawFoldingFeatureProducer.java index 69ad1badce60..7906342d445d 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/common/RawFoldingFeatureProducer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/common/RawFoldingFeatureProducer.java @@ -32,6 +32,7 @@ import com.android.internal.R; import java.util.Optional; import java.util.Set; +import java.util.function.Consumer; /** * Implementation of {@link androidx.window.util.DataProducer} that produces a @@ -40,7 +41,7 @@ import java.util.Set; * settings where the {@link String} property is saved with the key * {@link RawFoldingFeatureProducer#DISPLAY_FEATURES}. If this value is null or empty then the * value in {@link android.content.res.Resources} is used. If both are empty then - * {@link RawFoldingFeatureProducer#getData()} returns an empty object. + * {@link RawFoldingFeatureProducer#getData} returns an empty object. * {@link RawFoldingFeatureProducer} listens to changes in the setting so that it can override * the system {@link CommonFoldingFeature} data. */ @@ -63,12 +64,13 @@ public final class RawFoldingFeatureProducer extends BaseDataProducer<String> { @Override @NonNull - public Optional<String> getData() { + public void getData(Consumer<String> dataConsumer) { String displayFeaturesString = getFeatureString(); if (displayFeaturesString == null) { - return Optional.empty(); + dataConsumer.accept(""); + } else { + dataConsumer.accept(displayFeaturesString); } - return Optional.of(displayFeaturesString); } /** @@ -84,7 +86,7 @@ public final class RawFoldingFeatureProducer extends BaseDataProducer<String> { } @Override - protected void onListenersChanged(Set<Runnable> callbacks) { + protected void onListenersChanged(Set<Consumer<String>> callbacks) { if (callbacks.isEmpty()) { unregisterObserversIfNeeded(); } else { @@ -92,6 +94,12 @@ public final class RawFoldingFeatureProducer extends BaseDataProducer<String> { } } + @NonNull + @Override + public Optional<String> getCurrentData() { + return Optional.of(getFeatureString()); + } + /** * Registers settings observers, if needed. When settings observers are registered for this * producer callbacks for changes in data will be triggered. @@ -125,8 +133,8 @@ public final class RawFoldingFeatureProducer extends BaseDataProducer<String> { @Override public void onChange(boolean selfChange, Uri uri) { if (mDisplayFeaturesUri.equals(uri)) { - notifyDataChanged(); + notifyDataChanged(getFeatureString()); } } } -} +}
\ No newline at end of file diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java index bdf703c9bd38..fb0a9db6a20b 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java @@ -20,11 +20,14 @@ import android.app.ActivityThread; import android.content.Context; import androidx.annotation.NonNull; +import androidx.window.extensions.area.WindowAreaComponent; +import androidx.window.extensions.area.WindowAreaComponentImpl; import androidx.window.extensions.embedding.ActivityEmbeddingComponent; import androidx.window.extensions.embedding.SplitController; import androidx.window.extensions.layout.WindowLayoutComponent; import androidx.window.extensions.layout.WindowLayoutComponentImpl; + /** * The reference implementation of {@link WindowExtensions} that implements the initial API version. */ @@ -33,7 +36,9 @@ public class WindowExtensionsImpl implements WindowExtensions { private final Object mLock = new Object(); private volatile WindowLayoutComponent mWindowLayoutComponent; private volatile SplitController mSplitController; + private volatile WindowAreaComponent mWindowAreaComponent; + // TODO(b/241126279) Introduce constants to better version functionality @Override public int getVendorApiLevel() { return 1; @@ -75,4 +80,23 @@ public class WindowExtensionsImpl implements WindowExtensions { } return mSplitController; } + + /** + * Returns a reference implementation of {@link WindowAreaComponent} if available, + * {@code null} otherwise. The implementation must match the API level reported in + * {@link WindowExtensions#getWindowAreaComponent()}. + * @return {@link WindowAreaComponent} OEM implementation. + */ + public WindowAreaComponent getWindowAreaComponent() { + if (mWindowAreaComponent == null) { + synchronized (mLock) { + if (mWindowAreaComponent == null) { + Context context = ActivityThread.currentApplication(); + mWindowAreaComponent = + new WindowAreaComponentImpl(context); + } + } + } + return mWindowAreaComponent; + } } diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/WindowAreaComponentImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/WindowAreaComponentImpl.java new file mode 100644 index 000000000000..3adae7006369 --- /dev/null +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/WindowAreaComponentImpl.java @@ -0,0 +1,255 @@ +/* + * 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 androidx.window.extensions.area; + +import static android.hardware.devicestate.DeviceStateManager.INVALID_DEVICE_STATE; + +import android.app.Activity; +import android.content.Context; +import android.hardware.devicestate.DeviceStateManager; +import android.hardware.devicestate.DeviceStateRequest; +import android.util.ArraySet; + +import androidx.annotation.NonNull; + +import com.android.internal.R; +import com.android.internal.annotations.GuardedBy; + +import java.util.concurrent.Executor; +import java.util.function.Consumer; + +/** + * Reference implementation of androidx.window.extensions.area OEM interface for use with + * WindowManager Jetpack. + * + * This component currently supports Rear Display mode with the ability to add and remove + * status listeners for this mode. + * + * The public methods in this class are thread-safe. + **/ +public class WindowAreaComponentImpl implements WindowAreaComponent, + DeviceStateManager.DeviceStateCallback { + + private final Object mLock = new Object(); + + private final DeviceStateManager mDeviceStateManager; + private final Executor mExecutor; + + @GuardedBy("mLock") + private final ArraySet<Consumer<Integer>> mRearDisplayStatusListeners = new ArraySet<>(); + private final int mRearDisplayState; + @WindowAreaSessionState + private int mRearDisplaySessionStatus = WindowAreaComponent.SESSION_STATE_INACTIVE; + + @GuardedBy("mLock") + private int mCurrentDeviceState = INVALID_DEVICE_STATE; + @GuardedBy("mLock") + private int mCurrentDeviceBaseState = INVALID_DEVICE_STATE; + @GuardedBy("mLock") + private DeviceStateRequest mDeviceStateRequest; + + public WindowAreaComponentImpl(@NonNull Context context) { + mDeviceStateManager = context.getSystemService(DeviceStateManager.class); + mExecutor = context.getMainExecutor(); + + // TODO(b/236022708) Move rear display state to device state config file + mRearDisplayState = context.getResources().getInteger( + R.integer.config_deviceStateRearDisplay); + + mDeviceStateManager.registerCallback(mExecutor, this); + } + + /** + * Adds a listener interested in receiving updates on the RearDisplayStatus + * of the device. Because this is being called from the OEM provided + * extensions, we will post the result of the listener on the executor + * provided by the developer at the initial call site. + * + * Depending on the initial state of the device, we will return either + * {@link WindowAreaComponent#STATUS_AVAILABLE} or + * {@link WindowAreaComponent#STATUS_UNAVAILABLE} if the feature is supported or not in that + * state respectively. When the rear display feature is triggered, we update the status to be + * {@link WindowAreaComponent#STATUS_UNAVAILABLE}. TODO(b/240727590) Prefix with AREA_ + * + * TODO(b/239833099) Add a STATUS_ACTIVE option to let apps know if a feature is currently + * enabled. + * + * @param consumer {@link Consumer} interested in receiving updates to the status of + * rear display mode. + */ + public void addRearDisplayStatusListener( + @NonNull Consumer<@WindowAreaStatus Integer> consumer) { + synchronized (mLock) { + mRearDisplayStatusListeners.add(consumer); + + // If current device state is still invalid, we haven't gotten our initial value yet + if (mCurrentDeviceState == INVALID_DEVICE_STATE) { + return; + } + consumer.accept(getCurrentStatus()); + } + } + + /** + * Removes a listener no longer interested in receiving updates. + * @param consumer no longer interested in receiving updates to RearDisplayStatus + */ + public void removeRearDisplayStatusListener( + @NonNull Consumer<@WindowAreaStatus Integer> consumer) { + synchronized (mLock) { + mRearDisplayStatusListeners.remove(consumer); + } + } + + /** + * Creates and starts a rear display session and provides updates to the + * callback provided. Because this is being called from the OEM provided + * extensions, we will post the result of the listener on the executor + * provided by the developer at the initial call site. + * + * When we enable rear display mode, we submit a request to {@link DeviceStateManager} + * to override the device state to the state that corresponds to RearDisplay + * mode. When the {@link DeviceStateRequest} is activated, we let the + * consumer know that the session is active by sending + * {@link WindowAreaComponent#SESSION_STATE_ACTIVE}. + * + * @param activity to provide updates to the client on + * the status of the Session + * @param rearDisplaySessionCallback to provide updates to the client on + * the status of the Session + */ + public void startRearDisplaySession(@NonNull Activity activity, + @NonNull Consumer<@WindowAreaSessionState Integer> rearDisplaySessionCallback) { + synchronized (mLock) { + if (mDeviceStateRequest != null) { + // Rear display session is already active + throw new IllegalStateException( + "Unable to start new rear display session as one is already active"); + } + mDeviceStateRequest = DeviceStateRequest.newBuilder(mRearDisplayState).build(); + mDeviceStateManager.requestState( + mDeviceStateRequest, + mExecutor, + new DeviceStateRequestCallbackAdapter(rearDisplaySessionCallback) + ); + } + } + + /** + * Ends the current rear display session and provides updates to the + * callback provided. Because this is being called from the OEM provided + * extensions, we will post the result of the listener on the executor + * provided by the developer. + */ + public void endRearDisplaySession() { + synchronized (mLock) { + if (mDeviceStateRequest != null || isRearDisplayActive()) { + mDeviceStateRequest = null; + mDeviceStateManager.cancelStateRequest(); + } else { + throw new IllegalStateException( + "Unable to cancel a rear display session as there is no active session"); + } + } + } + + @Override + public void onBaseStateChanged(int state) { + synchronized (mLock) { + mCurrentDeviceBaseState = state; + if (state == mCurrentDeviceState) { + updateStatusConsumers(getCurrentStatus()); + } + } + } + + @Override + public void onStateChanged(int state) { + synchronized (mLock) { + mCurrentDeviceState = state; + updateStatusConsumers(getCurrentStatus()); + } + } + + @GuardedBy("mLock") + private int getCurrentStatus() { + if (mRearDisplaySessionStatus == WindowAreaComponent.SESSION_STATE_ACTIVE + || isRearDisplayActive()) { + return WindowAreaComponent.STATUS_UNAVAILABLE; + } + return WindowAreaComponent.STATUS_AVAILABLE; + } + + /** + * Helper method to determine if a rear display session is currently active by checking + * if the current device configuration matches that of rear display. This would be true + * if there is a device override currently active (base state != current state) and the current + * state is that which corresponds to {@code mRearDisplayState} + * @return {@code true} if the device is in rear display mode and {@code false} if not + */ + @GuardedBy("mLock") + private boolean isRearDisplayActive() { + return (mCurrentDeviceState != mCurrentDeviceBaseState) && (mCurrentDeviceState + == mRearDisplayState); + } + + @GuardedBy("mLock") + private void updateStatusConsumers(@WindowAreaStatus int windowAreaStatus) { + synchronized (mLock) { + for (int i = 0; i < mRearDisplayStatusListeners.size(); i++) { + mRearDisplayStatusListeners.valueAt(i).accept(windowAreaStatus); + } + } + } + + /** + * Callback for the {@link DeviceStateRequest} to be notified of when the request has been + * activated or cancelled. This callback provides information to the client library + * on the status of the RearDisplay session through {@code mRearDisplaySessionCallback} + */ + private class DeviceStateRequestCallbackAdapter implements DeviceStateRequest.Callback { + + private final Consumer<Integer> mRearDisplaySessionCallback; + + DeviceStateRequestCallbackAdapter(@NonNull Consumer<Integer> callback) { + mRearDisplaySessionCallback = callback; + } + + @Override + public void onRequestActivated(@NonNull DeviceStateRequest request) { + synchronized (mLock) { + if (request.equals(mDeviceStateRequest)) { + mRearDisplaySessionStatus = WindowAreaComponent.SESSION_STATE_ACTIVE; + mRearDisplaySessionCallback.accept(mRearDisplaySessionStatus); + updateStatusConsumers(getCurrentStatus()); + } + } + } + + @Override + public void onRequestCanceled(DeviceStateRequest request) { + synchronized (mLock) { + if (request.equals(mDeviceStateRequest)) { + mDeviceStateRequest = null; + } + mRearDisplaySessionStatus = WindowAreaComponent.SESSION_STATE_INACTIVE; + mRearDisplaySessionCallback.accept(mRearDisplaySessionStatus); + updateStatusConsumers(getCurrentStatus()); + } + } + } +} 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 3ff531573f1f..74303e2fab7c 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java @@ -21,7 +21,6 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import android.app.Activity; import android.app.WindowConfiguration.WindowingMode; import android.content.Intent; -import android.content.res.Configuration; import android.graphics.Rect; import android.os.Bundle; import android.os.IBinder; @@ -29,8 +28,10 @@ import android.util.ArrayMap; import android.window.TaskFragmentCreationParams; import android.window.TaskFragmentInfo; import android.window.TaskFragmentOrganizer; +import android.window.TaskFragmentTransaction; import android.window.WindowContainerTransaction; +import androidx.annotation.GuardedBy; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -51,34 +52,26 @@ class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer { @VisibleForTesting final Map<IBinder, TaskFragmentInfo> mFragmentInfos = new ArrayMap<>(); - /** - * Mapping from the client assigned unique token to the TaskFragment parent - * {@link Configuration}. - */ - final Map<IBinder, Configuration> mFragmentParentConfigs = new ArrayMap<>(); - + @NonNull private final TaskFragmentCallback mCallback; + @VisibleForTesting + @Nullable TaskFragmentAnimationController mAnimationController; /** * Callback that notifies the controller about changes to task fragments. */ interface TaskFragmentCallback { - void onTaskFragmentAppeared(@NonNull TaskFragmentInfo taskFragmentInfo); - void onTaskFragmentInfoChanged(@NonNull TaskFragmentInfo taskFragmentInfo); - void onTaskFragmentVanished(@NonNull TaskFragmentInfo taskFragmentInfo); - void onTaskFragmentParentInfoChanged(@NonNull IBinder fragmentToken, - @NonNull Configuration parentConfig); - void onActivityReparentToTask(int taskId, @NonNull Intent activityIntent, - @NonNull IBinder activityToken); + void onTransactionReady(@NonNull TaskFragmentTransaction transaction); } /** * @param executor callbacks from WM Core are posted on this executor. It should be tied to the * UI thread that all other calls into methods of this class are also on. */ - JetpackTaskFragmentOrganizer(@NonNull Executor executor, TaskFragmentCallback callback) { + JetpackTaskFragmentOrganizer(@NonNull Executor executor, + @NonNull TaskFragmentCallback callback) { super(executor); mCallback = callback; } @@ -101,6 +94,7 @@ class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer { } /** No longer overrides the animation if the transition is on the given Task. */ + @GuardedBy("mLock") void stopOverrideSplitAnimation(int taskId) { if (mAnimationController != null) { mAnimationController.unregisterRemoteAnimations(taskId); @@ -153,41 +147,31 @@ class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer { * @param wct WindowContainerTransaction in which the task fragment should be resized. * @param fragmentToken token of an existing TaskFragment. */ - void expandTaskFragment(WindowContainerTransaction wct, IBinder fragmentToken) { + void expandTaskFragment(@NonNull WindowContainerTransaction wct, + @NonNull IBinder fragmentToken) { resizeTaskFragment(wct, fragmentToken, new Rect()); setAdjacentTaskFragments(wct, fragmentToken, null /* secondary */, null /* splitRule */); updateWindowingMode(wct, fragmentToken, WINDOWING_MODE_UNDEFINED); } /** - * Expands an existing TaskFragment to fill parent. - * @param fragmentToken token of an existing TaskFragment. - */ - void expandTaskFragment(IBinder fragmentToken) { - WindowContainerTransaction wct = new WindowContainerTransaction(); - expandTaskFragment(wct, fragmentToken); - applyTransaction(wct); - } - - /** * Expands an Activity to fill parent by moving it to a new TaskFragment. * @param fragmentToken token to create new TaskFragment with. * @param activity activity to move to the fill-parent TaskFragment. */ - void expandActivity(IBinder fragmentToken, Activity activity) { - final WindowContainerTransaction wct = new WindowContainerTransaction(); + void expandActivity(@NonNull WindowContainerTransaction wct, @NonNull IBinder fragmentToken, + @NonNull Activity activity) { createTaskFragmentAndReparentActivity( wct, fragmentToken, activity.getActivityToken(), new Rect(), WINDOWING_MODE_UNDEFINED, activity); - applyTransaction(wct); } /** * @param ownerToken The token of the activity that creates this task fragment. It does not * have to be a child of this task fragment, but must belong to the same task. */ - void createTaskFragment(WindowContainerTransaction wct, IBinder fragmentToken, - IBinder ownerToken, @NonNull Rect bounds, @WindowingMode int windowingMode) { + void createTaskFragment(@NonNull WindowContainerTransaction wct, @NonNull IBinder fragmentToken, + @NonNull IBinder ownerToken, @NonNull Rect bounds, @WindowingMode int windowingMode) { final TaskFragmentCreationParams fragmentOptions = createFragmentOptions(fragmentToken, ownerToken, bounds, windowingMode); wct.createTaskFragment(fragmentOptions); @@ -197,9 +181,9 @@ class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer { * @param ownerToken The token of the activity that creates this task fragment. It does not * have to be a child of this task fragment, but must belong to the same task. */ - private void createTaskFragmentAndReparentActivity( - WindowContainerTransaction wct, IBinder fragmentToken, IBinder ownerToken, - @NonNull Rect bounds, @WindowingMode int windowingMode, Activity activity) { + private void createTaskFragmentAndReparentActivity(@NonNull WindowContainerTransaction wct, + @NonNull IBinder fragmentToken, @NonNull IBinder ownerToken, @NonNull Rect bounds, + @WindowingMode int windowingMode, @NonNull Activity activity) { createTaskFragment(wct, fragmentToken, ownerToken, bounds, windowingMode); wct.reparentActivityToTaskFragment(fragmentToken, activity.getActivityToken()); } @@ -208,9 +192,9 @@ class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer { * @param ownerToken The token of the activity that creates this task fragment. It does not * have to be a child of this task fragment, but must belong to the same task. */ - private void createTaskFragmentAndStartActivity( - WindowContainerTransaction wct, IBinder fragmentToken, IBinder ownerToken, - @NonNull Rect bounds, @WindowingMode int windowingMode, Intent activityIntent, + private void createTaskFragmentAndStartActivity(@NonNull WindowContainerTransaction wct, + @NonNull IBinder fragmentToken, @NonNull IBinder ownerToken, @NonNull Rect bounds, + @WindowingMode int windowingMode, @NonNull Intent activityIntent, @Nullable Bundle activityOptions) { createTaskFragment(wct, fragmentToken, ownerToken, bounds, windowingMode); wct.startActivityInTaskFragment(fragmentToken, ownerToken, activityIntent, activityOptions); @@ -231,8 +215,8 @@ class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer { wct.setAdjacentTaskFragments(primary, secondary, adjacentParams); } - TaskFragmentCreationParams createFragmentOptions(IBinder fragmentToken, IBinder ownerToken, - Rect bounds, @WindowingMode int windowingMode) { + TaskFragmentCreationParams createFragmentOptions(@NonNull IBinder fragmentToken, + @NonNull IBinder ownerToken, @NonNull Rect bounds, @WindowingMode int windowingMode) { if (mFragmentInfos.containsKey(fragmentToken)) { throw new IllegalArgumentException( "There is an existing TaskFragment with fragmentToken=" + fragmentToken); @@ -247,7 +231,7 @@ class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer { .build(); } - void resizeTaskFragment(WindowContainerTransaction wct, IBinder fragmentToken, + void resizeTaskFragment(@NonNull WindowContainerTransaction wct, @NonNull IBinder fragmentToken, @Nullable Rect bounds) { if (!mFragmentInfos.containsKey(fragmentToken)) { throw new IllegalArgumentException( @@ -259,8 +243,8 @@ class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer { wct.setBounds(mFragmentInfos.get(fragmentToken).getToken(), bounds); } - void updateWindowingMode(WindowContainerTransaction wct, IBinder fragmentToken, - @WindowingMode int windowingMode) { + void updateWindowingMode(@NonNull WindowContainerTransaction wct, + @NonNull IBinder fragmentToken, @WindowingMode int windowingMode) { if (!mFragmentInfos.containsKey(fragmentToken)) { throw new IllegalArgumentException( "Can't find an existing TaskFragment with fragmentToken=" + fragmentToken); @@ -268,7 +252,8 @@ class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer { wct.setWindowingMode(mFragmentInfos.get(fragmentToken).getToken(), windowingMode); } - void deleteTaskFragment(WindowContainerTransaction wct, IBinder fragmentToken) { + void deleteTaskFragment(@NonNull WindowContainerTransaction wct, + @NonNull IBinder fragmentToken) { if (!mFragmentInfos.containsKey(fragmentToken)) { throw new IllegalArgumentException( "Can't find an existing TaskFragment with fragmentToken=" + fragmentToken); @@ -276,51 +261,16 @@ class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer { wct.deleteTaskFragment(mFragmentInfos.get(fragmentToken).getToken()); } - @Override - public void onTaskFragmentAppeared(@NonNull TaskFragmentInfo taskFragmentInfo) { - final IBinder fragmentToken = taskFragmentInfo.getFragmentToken(); - mFragmentInfos.put(fragmentToken, taskFragmentInfo); - - if (mCallback != null) { - mCallback.onTaskFragmentAppeared(taskFragmentInfo); - } - } - - @Override - public void onTaskFragmentInfoChanged(@NonNull TaskFragmentInfo taskFragmentInfo) { - final IBinder fragmentToken = taskFragmentInfo.getFragmentToken(); - mFragmentInfos.put(fragmentToken, taskFragmentInfo); - - if (mCallback != null) { - mCallback.onTaskFragmentInfoChanged(taskFragmentInfo); - } + void updateTaskFragmentInfo(@NonNull TaskFragmentInfo taskFragmentInfo) { + mFragmentInfos.put(taskFragmentInfo.getFragmentToken(), taskFragmentInfo); } - @Override - public void onTaskFragmentVanished(@NonNull TaskFragmentInfo taskFragmentInfo) { + void removeTaskFragmentInfo(@NonNull TaskFragmentInfo taskFragmentInfo) { mFragmentInfos.remove(taskFragmentInfo.getFragmentToken()); - mFragmentParentConfigs.remove(taskFragmentInfo.getFragmentToken()); - - if (mCallback != null) { - mCallback.onTaskFragmentVanished(taskFragmentInfo); - } } @Override - public void onTaskFragmentParentInfoChanged( - @NonNull IBinder fragmentToken, @NonNull Configuration parentConfig) { - mFragmentParentConfigs.put(fragmentToken, parentConfig); - - if (mCallback != null) { - mCallback.onTaskFragmentParentInfoChanged(fragmentToken, parentConfig); - } - } - - @Override - public void onActivityReparentToTask(int taskId, @NonNull Intent activityIntent, - @NonNull IBinder activityToken) { - if (mCallback != null) { - mCallback.onActivityReparentToTask(taskId, activityIntent, activityToken); - } + public void onTransactionReady(@NonNull TaskFragmentTransaction transaction) { + mCallback.onTransactionReady(transaction); } } 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 f09a91018bf0..00be5a6e3416 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java @@ -16,26 +16,36 @@ package androidx.window.extensions.embedding; -import android.annotation.NonNull; import android.app.Activity; +import android.content.res.Configuration; import android.util.Pair; import android.util.Size; +import android.window.WindowContainerTransaction; + +import androidx.annotation.NonNull; /** * Client-side descriptor of a split that holds two containers. */ class SplitContainer { + @NonNull private final TaskFragmentContainer mPrimaryContainer; + @NonNull private final TaskFragmentContainer mSecondaryContainer; + @NonNull private final SplitRule mSplitRule; + @NonNull + private SplitAttributes mSplitAttributes; SplitContainer(@NonNull TaskFragmentContainer primaryContainer, @NonNull Activity primaryActivity, @NonNull TaskFragmentContainer secondaryContainer, - @NonNull SplitRule splitRule) { + @NonNull SplitRule splitRule, + @NonNull SplitAttributes splitAttributes) { mPrimaryContainer = primaryContainer; mSecondaryContainer = secondaryContainer; mSplitRule = splitRule; + mSplitAttributes = splitAttributes; if (shouldFinishPrimaryWithSecondary(splitRule)) { if (mPrimaryContainer.getRunningActivityCount() == 1 @@ -68,6 +78,26 @@ class SplitContainer { return mSplitRule; } + @NonNull + SplitAttributes getSplitAttributes() { + return mSplitAttributes; + } + + /** + * Updates the {@link SplitAttributes} to this container. + * It is usually used when there's a folding state change or + * {@link SplitController#onTaskFragmentParentInfoChanged(WindowContainerTransaction, int, + * Configuration)}. + */ + void setSplitAttributes(@NonNull SplitAttributes splitAttributes) { + mSplitAttributes = splitAttributes; + } + + @NonNull + TaskContainer getTaskContainer() { + return getPrimaryContainer().getTaskContainer(); + } + /** Returns the minimum dimension pair of primary container and secondary container. */ @NonNull Pair<Size, Size> getMinDimensionsPair() { @@ -137,6 +167,7 @@ class SplitContainer { + " primaryContainer=" + mPrimaryContainer + " secondaryContainer=" + mSecondaryContainer + " splitRule=" + mSplitRule + + " splitAttributes" + mSplitAttributes + "}"; } } 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 c9a0d7d99cc6..203ece091e46 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java @@ -16,8 +16,22 @@ package androidx.window.extensions.embedding; +import static android.app.ActivityManager.START_SUCCESS; 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.window.TaskFragmentOrganizer.KEY_ERROR_CALLBACK_OP_TYPE; +import static android.window.TaskFragmentOrganizer.KEY_ERROR_CALLBACK_TASK_FRAGMENT_INFO; +import static android.window.TaskFragmentOrganizer.KEY_ERROR_CALLBACK_THROWABLE; +import static android.window.TaskFragmentOrganizer.getTransitionType; +import static android.window.TaskFragmentTransaction.TYPE_ACTIVITY_REPARENTED_TO_TASK; +import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_APPEARED; +import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_ERROR; +import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_INFO_CHANGED; +import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_PARENT_INFO_CHANGED; +import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_VANISHED; +import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REPARENT_ACTIVITY_TO_TASK_FRAGMENT; +import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT; import static androidx.window.extensions.embedding.SplitContainer.getFinishPrimaryWithSecondaryBehavior; import static androidx.window.extensions.embedding.SplitContainer.getFinishSecondaryWithPrimaryBehavior; @@ -27,12 +41,13 @@ import static androidx.window.extensions.embedding.SplitContainer.shouldFinishAs import static androidx.window.extensions.embedding.SplitPresenter.RESULT_EXPAND_FAILED_NO_TF_INFO; import static androidx.window.extensions.embedding.SplitPresenter.getActivityIntentMinDimensionsPair; import static androidx.window.extensions.embedding.SplitPresenter.getNonEmbeddedActivityBounds; -import static androidx.window.extensions.embedding.SplitPresenter.shouldShowSideBySide; +import static androidx.window.extensions.embedding.SplitPresenter.shouldShowSplit; import android.app.Activity; import android.app.ActivityClient; import android.app.ActivityOptions; import android.app.ActivityThread; +import android.app.Application; import android.app.Instrumentation; import android.content.ComponentName; import android.content.Context; @@ -43,23 +58,31 @@ import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.Looper; +import android.os.SystemProperties; import android.util.ArraySet; import android.util.Log; import android.util.Pair; import android.util.Size; import android.util.SparseArray; +import android.view.WindowMetrics; import android.window.TaskFragmentInfo; +import android.window.TaskFragmentParentInfo; +import android.window.TaskFragmentTransaction; 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.EmptyLifecycleCallbacksAdapter; +import androidx.window.extensions.WindowExtensionsProvider; +import androidx.window.extensions.layout.WindowLayoutComponentImpl; import com.android.internal.annotations.VisibleForTesting; import java.util.ArrayList; import java.util.List; +import java.util.Objects; import java.util.Set; import java.util.concurrent.Executor; import java.util.function.Consumer; @@ -70,6 +93,8 @@ import java.util.function.Consumer; public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmentCallback, ActivityEmbeddingComponent { static final String TAG = "SplitController"; + static final boolean ENABLE_SHELL_TRANSITIONS = + SystemProperties.getBoolean("persist.wm.debug.shell_transit", false); @VisibleForTesting @GuardedBy("mLock") @@ -79,6 +104,23 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen @GuardedBy("mLock") private final List<EmbeddingRule> mSplitRules = new ArrayList<>(); /** + * A developer-defined {@link SplitAttributes} calculator to compute the current + * {@link SplitAttributes} with the current device and window states. + * It is registered via {@link #setSplitAttributesCalculator(SplitAttributesCalculator)} + * and unregistered via {@link #clearSplitAttributesCalculator()}. + * This is called when: + * <ul> + * <li>{@link SplitPresenter#updateSplitContainer(SplitContainer, TaskFragmentContainer, + * WindowContainerTransaction)}</li> + * <li>There's a started Activity which matches {@link SplitPairRule} </li> + * <li>Checking whether the place holder should be launched if there's a Activity matches + * {@link SplitPlaceholderRule} </li> + * </ul> + */ + @GuardedBy("mLock") + @Nullable + private SplitAttributesCalculator mSplitAttributesCalculator; + /** * Map from Task id to {@link TaskContainer} which contains all TaskFragment and split pair info * below it. * When the app is host of multiple Tasks, there can be multiple splits controlled by the same @@ -88,24 +130,65 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen @GuardedBy("mLock") final SparseArray<TaskContainer> mTaskContainers = new SparseArray<>(); - // Callback to Jetpack to notify about changes to split states. - @NonNull + /** Callback to Jetpack to notify about changes to split states. */ + @Nullable private Consumer<List<SplitInfo>> mEmbeddingCallback; private final List<SplitInfo> mLastReportedSplitStates = new ArrayList<>(); private final Handler mHandler; - private final Object mLock = new Object(); + final Object mLock = new Object(); + private final ActivityStartMonitor mActivityStartMonitor; + @NonNull + final WindowLayoutComponentImpl mWindowLayoutComponent; public SplitController() { + this((WindowLayoutComponentImpl) Objects.requireNonNull(WindowExtensionsProvider + .getWindowExtensions().getWindowLayoutComponent())); + } + + @VisibleForTesting + SplitController(@NonNull WindowLayoutComponentImpl windowLayoutComponent) { final MainThreadExecutor executor = new MainThreadExecutor(); mHandler = executor.mHandler; mPresenter = new SplitPresenter(executor, this); - ActivityThread activityThread = ActivityThread.currentActivityThread(); + final ActivityThread activityThread = ActivityThread.currentActivityThread(); + final Application application = activityThread.getApplication(); // Register a callback to be notified about activities being created. - activityThread.getApplication().registerActivityLifecycleCallbacks( - new LifecycleCallbacks()); + application.registerActivityLifecycleCallbacks(new LifecycleCallbacks()); // Intercept activity starts to route activities to new containers if necessary. Instrumentation instrumentation = activityThread.getInstrumentation(); - instrumentation.addMonitor(new ActivityStartMonitor()); + + mActivityStartMonitor = new ActivityStartMonitor(); + instrumentation.addMonitor(mActivityStartMonitor); + mWindowLayoutComponent = windowLayoutComponent; + mWindowLayoutComponent.addFoldingStateChangedCallback(new FoldingFeatureListener()); + } + + private class FoldingFeatureListener implements Consumer<List<CommonFoldingFeature>> { + @Override + public void accept(List<CommonFoldingFeature> foldingFeatures) { + synchronized (mLock) { + final WindowContainerTransaction wct = new WindowContainerTransaction(); + for (int i = 0; i < mTaskContainers.size(); i++) { + final TaskContainer taskContainer = mTaskContainers.valueAt(i); + if (!taskContainer.isVisible()) { + continue; + } + if (taskContainer.getDisplayId() != DEFAULT_DISPLAY) { + continue; + } + // TODO(b/238948678): Support reporting display features in all windowing modes. + if (taskContainer.isInMultiWindow()) { + continue; + } + if (taskContainer.isEmpty()) { + continue; + } + updateContainersInTask(wct, taskContainer); + updateAnimationOverride(taskContainer); + } + mPresenter.applyTransaction(wct); + } + } } /** Updates the embedding rules applied to future activity launches. */ @@ -120,6 +203,26 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen } } + @Override + public void setSplitAttributesCalculator(@NonNull SplitAttributesCalculator calculator) { + synchronized (mLock) { + mSplitAttributesCalculator = calculator; + } + } + + @Override + public void clearSplitAttributesCalculator() { + synchronized (mLock) { + mSplitAttributesCalculator = null; + } + } + + @GuardedBy("mLock") + @Nullable + SplitAttributesCalculator getSplitAttributesCalculator() { + return mSplitAttributesCalculator; + } + @NonNull List<EmbeddingRule> getSplitRules() { return mSplitRules; @@ -136,161 +239,340 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen } } + /** + * Called when the transaction is ready so that the organizer can update the TaskFragments based + * on the changes in transaction. + */ @Override - public void onTaskFragmentAppeared(@NonNull TaskFragmentInfo taskFragmentInfo) { + public void onTransactionReady(@NonNull TaskFragmentTransaction transaction) { synchronized (mLock) { - TaskFragmentContainer container = getContainer(taskFragmentInfo.getFragmentToken()); - if (container == null) { - return; + final WindowContainerTransaction wct = new WindowContainerTransaction(); + final List<TaskFragmentTransaction.Change> changes = transaction.getChanges(); + for (TaskFragmentTransaction.Change change : changes) { + final int taskId = change.getTaskId(); + final TaskFragmentInfo info = change.getTaskFragmentInfo(); + switch (change.getType()) { + case TYPE_TASK_FRAGMENT_APPEARED: + mPresenter.updateTaskFragmentInfo(info); + onTaskFragmentAppeared(wct, info); + break; + case TYPE_TASK_FRAGMENT_INFO_CHANGED: + mPresenter.updateTaskFragmentInfo(info); + onTaskFragmentInfoChanged(wct, info); + break; + case TYPE_TASK_FRAGMENT_VANISHED: + mPresenter.removeTaskFragmentInfo(info); + onTaskFragmentVanished(wct, info); + break; + case TYPE_TASK_FRAGMENT_PARENT_INFO_CHANGED: + onTaskFragmentParentInfoChanged(wct, taskId, + change.getTaskFragmentParentInfo()); + break; + case TYPE_TASK_FRAGMENT_ERROR: + final Bundle errorBundle = change.getErrorBundle(); + final IBinder errorToken = change.getErrorCallbackToken(); + final TaskFragmentInfo errorTaskFragmentInfo = errorBundle.getParcelable( + KEY_ERROR_CALLBACK_TASK_FRAGMENT_INFO, TaskFragmentInfo.class); + final int opType = errorBundle.getInt(KEY_ERROR_CALLBACK_OP_TYPE); + final Throwable exception = errorBundle.getSerializable( + KEY_ERROR_CALLBACK_THROWABLE, Throwable.class); + if (errorTaskFragmentInfo != null) { + mPresenter.updateTaskFragmentInfo(errorTaskFragmentInfo); + } + onTaskFragmentError(wct, errorToken, errorTaskFragmentInfo, opType, + exception); + break; + case TYPE_ACTIVITY_REPARENTED_TO_TASK: + onActivityReparentedToTask( + wct, + taskId, + change.getActivityIntent(), + change.getActivityToken()); + break; + default: + throw new IllegalArgumentException( + "Unknown TaskFragmentEvent=" + change.getType()); + } } - container.setInfo(taskFragmentInfo); - if (container.isFinished()) { - mPresenter.cleanupContainer(container, false /* shouldFinishDependent */); - } + // Notify the server, and the server should apply and merge the + // WindowContainerTransaction to the active sync to finish the TaskFragmentTransaction. + mPresenter.onTransactionHandled(transaction.getTransactionToken(), wct, + getTransitionType(wct), false /* shouldApplyIndependently */); updateCallbackIfNecessary(); } } - @Override - public void onTaskFragmentInfoChanged(@NonNull TaskFragmentInfo taskFragmentInfo) { - synchronized (mLock) { - TaskFragmentContainer container = getContainer(taskFragmentInfo.getFragmentToken()); - if (container == null) { - return; - } + /** + * Called when a TaskFragment is created and organized by this organizer. + * + * @param wct The {@link WindowContainerTransaction} to make any changes with if needed. + * @param taskFragmentInfo Info of the TaskFragment that is created. + */ + // Suppress GuardedBy warning because lint ask to mark this method as + // @GuardedBy(container.mController.mLock), which is mLock itself + @SuppressWarnings("GuardedBy") + @VisibleForTesting + @GuardedBy("mLock") + void onTaskFragmentAppeared(@NonNull WindowContainerTransaction wct, + @NonNull TaskFragmentInfo taskFragmentInfo) { + final TaskFragmentContainer container = getContainer(taskFragmentInfo.getFragmentToken()); + if (container == null) { + return; + } - final WindowContainerTransaction wct = new WindowContainerTransaction(); - final boolean wasInPip = isInPictureInPicture(container); - container.setInfo(taskFragmentInfo); - final boolean isInPip = isInPictureInPicture(container); - // Check if there are no running activities - consider the container empty if there are - // no non-finishing activities left. - if (!taskFragmentInfo.hasRunningActivity()) { - if (taskFragmentInfo.isTaskFragmentClearedForPip()) { - // Do not finish the dependents if the last activity is reparented to PiP. - // Instead, the original split should be cleanup, and the dependent may be - // expanded to fullscreen. - cleanupForEnterPip(wct, container); - mPresenter.cleanupContainer(container, false /* shouldFinishDependent */, wct); - } else if (taskFragmentInfo.isTaskClearedForReuse()) { - // Do not finish the dependents if this TaskFragment was cleared due to - // launching activity in the Task. - mPresenter.cleanupContainer(container, false /* shouldFinishDependent */, wct); - } else if (!container.isWaitingActivityAppear()) { - // Do not finish the container before the expected activity appear until - // timeout. - mPresenter.cleanupContainer(container, true /* shouldFinishDependent */, wct); - } - } else if (wasInPip && isInPip) { - // No update until exit PIP. - return; - } else if (isInPip) { - // Enter PIP. - // All overrides will be cleanup. - container.setLastRequestedBounds(null /* bounds */); - container.setLastRequestedWindowingMode(WINDOWING_MODE_UNDEFINED); - cleanupForEnterPip(wct, container); - } else if (wasInPip) { - // Exit PIP. - // Updates the presentation of the container. Expand or launch placeholder if - // needed. - updateContainer(wct, container); - } - mPresenter.applyTransaction(wct); - updateCallbackIfNecessary(); + container.setInfo(wct, taskFragmentInfo); + if (container.isFinished()) { + mPresenter.cleanupContainer(wct, container, false /* shouldFinishDependent */); + } else { + // Update with the latest Task configuration. + updateContainer(wct, container); } } - @Override - public void onTaskFragmentVanished(@NonNull TaskFragmentInfo taskFragmentInfo) { - synchronized (mLock) { - final TaskFragmentContainer container = getContainer( - taskFragmentInfo.getFragmentToken()); - if (container != null) { - // Cleanup if the TaskFragment vanished is not requested by the organizer. - removeContainer(container); - // Make sure the top container is updated. - final TaskFragmentContainer newTopContainer = getTopActiveContainer( - container.getTaskId()); - if (newTopContainer != null) { - final WindowContainerTransaction wct = new WindowContainerTransaction(); - updateContainer(wct, newTopContainer); - mPresenter.applyTransaction(wct); - } - updateCallbackIfNecessary(); + /** + * Called when the status of an organized TaskFragment is changed. + * + * @param wct The {@link WindowContainerTransaction} to make any changes with if needed. + * @param taskFragmentInfo Info of the TaskFragment that is changed. + */ + // Suppress GuardedBy warning because lint ask to mark this method as + // @GuardedBy(container.mController.mLock), which is mLock itself + @SuppressWarnings("GuardedBy") + @VisibleForTesting + @GuardedBy("mLock") + void onTaskFragmentInfoChanged(@NonNull WindowContainerTransaction wct, + @NonNull TaskFragmentInfo taskFragmentInfo) { + final TaskFragmentContainer container = getContainer(taskFragmentInfo.getFragmentToken()); + if (container == null) { + return; + } + + final boolean wasInPip = isInPictureInPicture(container); + container.setInfo(wct, taskFragmentInfo); + final boolean isInPip = isInPictureInPicture(container); + // Check if there are no running activities - consider the container empty if there are + // no non-finishing activities left. + if (!taskFragmentInfo.hasRunningActivity()) { + if (taskFragmentInfo.isTaskFragmentClearedForPip()) { + // Do not finish the dependents if the last activity is reparented to PiP. + // Instead, the original split should be cleanup, and the dependent may be + // expanded to fullscreen. + cleanupForEnterPip(wct, container); + mPresenter.cleanupContainer(wct, container, false /* shouldFinishDependent */); + } else if (taskFragmentInfo.isTaskClearedForReuse()) { + // Do not finish the dependents if this TaskFragment was cleared due to + // launching activity in the Task. + mPresenter.cleanupContainer(wct, container, false /* shouldFinishDependent */); + } else if (!container.isWaitingActivityAppear()) { + // Do not finish the container before the expected activity appear until + // timeout. + mPresenter.cleanupContainer(wct, container, true /* shouldFinishDependent */); } - cleanupTaskFragment(taskFragmentInfo.getFragmentToken()); + } else if (wasInPip && isInPip) { + // No update until exit PIP. + return; + } else if (isInPip) { + // Enter PIP. + // All overrides will be cleanup. + container.setLastRequestedBounds(null /* bounds */); + container.setLastRequestedWindowingMode(WINDOWING_MODE_UNDEFINED); + cleanupForEnterPip(wct, container); + } else if (wasInPip) { + // Exit PIP. + // Updates the presentation of the container. Expand or launch placeholder if + // needed. + updateContainer(wct, container); } } - @Override - public void onTaskFragmentParentInfoChanged(@NonNull IBinder fragmentToken, - @NonNull Configuration parentConfig) { - synchronized (mLock) { - final TaskFragmentContainer container = getContainer(fragmentToken); - if (container != null) { - onTaskConfigurationChanged(container.getTaskId(), parentConfig); - if (isInPictureInPicture(parentConfig)) { - // No need to update presentation in PIP until the Task exit PIP. - return; - } - mPresenter.updateContainer(container); - updateCallbackIfNecessary(); + /** + * Called when an organized TaskFragment is removed. + * + * @param wct The {@link WindowContainerTransaction} to make any changes with if needed. + * @param taskFragmentInfo Info of the TaskFragment that is removed. + */ + @VisibleForTesting + @GuardedBy("mLock") + void onTaskFragmentVanished(@NonNull WindowContainerTransaction wct, + @NonNull TaskFragmentInfo taskFragmentInfo) { + final TaskFragmentContainer container = getContainer(taskFragmentInfo.getFragmentToken()); + if (container != null) { + // Cleanup if the TaskFragment vanished is not requested by the organizer. + removeContainer(container); + // Make sure the top container is updated. + final TaskFragmentContainer newTopContainer = getTopActiveContainer( + container.getTaskId()); + if (newTopContainer != null) { + updateContainer(wct, newTopContainer); } } + cleanupTaskFragment(taskFragmentInfo.getFragmentToken()); } - @Override - public void onActivityReparentToTask(int taskId, @NonNull Intent activityIntent, - @NonNull IBinder activityToken) { - synchronized (mLock) { - // If the activity belongs to the current app process, we treat it as a new activity - // launch. - final Activity activity = getActivity(activityToken); - if (activity != null) { - // We don't allow split as primary for new launch because we currently only support - // launching to top. We allow split as primary for activity reparent because the - // activity may be split as primary before it is reparented out. In that case, we - // want to show it as primary again when it is reparented back. - if (!resolveActivityToContainer(activity, true /* isOnReparent */)) { - // When there is no embedding rule matched, try to place it in the top container - // like a normal launch. - placeActivityInTopContainer(activity); - } - updateCallbackIfNecessary(); - return; - } + /** + * Called when the parent leaf Task of organized TaskFragments is changed. + * When the leaf Task is changed, the organizer may want to update the TaskFragments in one + * transaction. + * + * For case like screen size change, it will trigger {@link #onTaskFragmentParentInfoChanged} + * with new Task bounds, but may not trigger {@link #onTaskFragmentInfoChanged} because there + * can be an override bounds. + * + * @param wct The {@link WindowContainerTransaction} to make any changes with if needed. + * @param taskId Id of the parent Task that is changed. + * @param parentInfo {@link TaskFragmentParentInfo} of the parent Task. + */ + @VisibleForTesting + @GuardedBy("mLock") + void onTaskFragmentParentInfoChanged(@NonNull WindowContainerTransaction wct, + int taskId, @NonNull TaskFragmentParentInfo parentInfo) { + final TaskContainer taskContainer = getTaskContainer(taskId); + if (taskContainer == null || taskContainer.isEmpty()) { + Log.e(TAG, "onTaskFragmentParentInfoChanged on empty Task id=" + taskId); + return; + } + taskContainer.updateTaskFragmentParentInfo(parentInfo); + if (!taskContainer.isVisible()) { + // Don't update containers if the task is not visible. We only update containers when + // parentInfo#isVisibleRequested is true. + return; + } + onTaskContainerInfoChanged(taskContainer, parentInfo.getConfiguration()); + if (isInPictureInPicture(parentInfo.getConfiguration())) { + // No need to update presentation in PIP until the Task exit PIP. + return; + } + updateContainersInTask(wct, taskContainer); + } - final TaskContainer taskContainer = getTaskContainer(taskId); - if (taskContainer == null || taskContainer.isInPictureInPicture()) { - // We don't embed activity when it is in PIP. - return; + private void updateContainersInTask(@NonNull WindowContainerTransaction wct, + @NonNull TaskContainer taskContainer) { + // Update all TaskFragments in the Task. Make a copy of the list since some may be + // removed on updating. + final List<TaskFragmentContainer> containers = + new ArrayList<>(taskContainer.mContainers); + for (int i = containers.size() - 1; i >= 0; i--) { + final TaskFragmentContainer container = containers.get(i); + // Wait until onTaskFragmentAppeared to update new container. + if (!container.isFinished() && !container.isWaitingActivityAppear()) { + updateContainer(wct, container); } + } + } - // If the activity belongs to a different app process, we treat it as starting new - // intent, since both actions might result in a new activity that should appear in an - // organized TaskFragment. - final WindowContainerTransaction wct = new WindowContainerTransaction(); - TaskFragmentContainer targetContainer = resolveStartActivityIntent(wct, taskId, - activityIntent, null /* launchingActivity */); - if (targetContainer == null) { + /** + * Called when an Activity is reparented to the Task with organized TaskFragment. For example, + * when an Activity enters and then exits Picture-in-picture, it will be reparented back to its + * original Task. In this case, we need to notify the organizer so that it can check if the + * Activity matches any split rule. + * + * @param wct The {@link WindowContainerTransaction} to make any changes with if needed. + * @param taskId The Task that the activity is reparented to. + * @param activityIntent The intent that the activity is original launched with. + * @param activityToken If the activity belongs to the same process as the organizer, this + * will be the actual activity token; if the activity belongs to a + * different process, the server will generate a temporary token that + * the organizer can use to reparent the activity through + * {@link WindowContainerTransaction} if needed. + */ + @VisibleForTesting + @GuardedBy("mLock") + void onActivityReparentedToTask(@NonNull WindowContainerTransaction wct, + int taskId, @NonNull Intent activityIntent, + @NonNull IBinder activityToken) { + // If the activity belongs to the current app process, we treat it as a new activity + // launch. + final Activity activity = getActivity(activityToken); + if (activity != null) { + // We don't allow split as primary for new launch because we currently only support + // launching to top. We allow split as primary for activity reparent because the + // activity may be split as primary before it is reparented out. In that case, we + // want to show it as primary again when it is reparented back. + if (!resolveActivityToContainer(wct, activity, true /* isOnReparent */)) { // When there is no embedding rule matched, try to place it in the top container // like a normal launch. - targetContainer = taskContainer.getTopTaskFragmentContainer(); + placeActivityInTopContainer(wct, activity); } - if (targetContainer == null) { - return; + return; + } + + final TaskContainer taskContainer = getTaskContainer(taskId); + if (taskContainer == null || taskContainer.isInPictureInPicture()) { + // We don't embed activity when it is in PIP. + return; + } + + // If the activity belongs to a different app process, we treat it as starting new + // intent, since both actions might result in a new activity that should appear in an + // organized TaskFragment. + TaskFragmentContainer targetContainer = resolveStartActivityIntent(wct, taskId, + activityIntent, null /* launchingActivity */); + if (targetContainer == null) { + // When there is no embedding rule matched, try to place it in the top container + // like a normal launch. + targetContainer = taskContainer.getTopTaskFragmentContainer(); + } + if (targetContainer == null) { + return; + } + wct.reparentActivityToTaskFragment(targetContainer.getTaskFragmentToken(), + activityToken); + // Because the activity does not belong to the organizer process, we wait until + // onTaskFragmentAppeared to trigger updateCallbackIfNecessary(). + } + + /** + * Called when the {@link WindowContainerTransaction} created with + * {@link WindowContainerTransaction#setErrorCallbackToken(IBinder)} failed on the server side. + * + * @param wct The {@link WindowContainerTransaction} to make any changes with if needed. + * @param errorCallbackToken token set in + * {@link WindowContainerTransaction#setErrorCallbackToken(IBinder)} + * @param taskFragmentInfo The {@link TaskFragmentInfo}. This could be {@code null} if no + * TaskFragment created. + * @param opType The {@link WindowContainerTransaction.HierarchyOp} of the failed + * transaction operation. + * @param exception exception from the server side. + */ + // Suppress GuardedBy warning because lint ask to mark this method as + // @GuardedBy(container.mController.mLock), which is mLock itself + @SuppressWarnings("GuardedBy") + @VisibleForTesting + @GuardedBy("mLock") + void onTaskFragmentError(@NonNull WindowContainerTransaction wct, + @Nullable IBinder errorCallbackToken, @Nullable TaskFragmentInfo taskFragmentInfo, + int opType, @NonNull Throwable exception) { + Log.e(TAG, "onTaskFragmentError=" + exception.getMessage()); + switch (opType) { + case HIERARCHY_OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT: + case HIERARCHY_OP_TYPE_REPARENT_ACTIVITY_TO_TASK_FRAGMENT: { + final TaskFragmentContainer container; + if (taskFragmentInfo != null) { + container = getContainer(taskFragmentInfo.getFragmentToken()); + } else { + container = null; + } + if (container == null) { + break; + } + + // Update the latest taskFragmentInfo and perform necessary clean-up + container.setInfo(wct, taskFragmentInfo); + container.clearPendingAppearedActivities(); + if (container.isEmpty()) { + mPresenter.cleanupContainer(wct, container, false /* shouldFinishDependent */); + } + break; } - wct.reparentActivityToTaskFragment(targetContainer.getTaskFragmentToken(), - activityToken); - mPresenter.applyTransaction(wct); - // Because the activity does not belong to the organizer process, we wait until - // onTaskFragmentAppeared to trigger updateCallbackIfNecessary(). + default: + Log.e(TAG, "onTaskFragmentError: taskFragmentInfo = " + taskFragmentInfo + + ", opType = " + opType); } } - /** Called on receiving {@link #onTaskFragmentVanished(TaskFragmentInfo)} for cleanup. */ + /** Called on receiving {@link #onTaskFragmentVanished} for cleanup. */ + @GuardedBy("mLock") private void cleanupTaskFragment(@NonNull IBinder taskFragmentToken) { for (int i = mTaskContainers.size() - 1; i >= 0; i--) { final TaskContainer taskContainer = mTaskContainers.valueAt(i); @@ -306,14 +588,11 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen } } - private void onTaskConfigurationChanged(int taskId, @NonNull Configuration config) { - final TaskContainer taskContainer = mTaskContainers.get(taskId); - if (taskContainer == null) { - return; - } + @GuardedBy("mLock") + private void onTaskContainerInfoChanged(@NonNull TaskContainer taskContainer, + @NonNull Configuration config) { final boolean wasInPip = taskContainer.isInPictureInPicture(); final boolean isInPIp = isInPictureInPicture(config); - taskContainer.setWindowingMode(config.windowConfiguration.getWindowingMode()); // We need to check the animation override when enter/exit PIP or has bounds changed. boolean shouldUpdateAnimationOverride = wasInPip != isInPIp; @@ -331,32 +610,49 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen * Updates if we should override transition animation. We only want to override if the Task * bounds is large enough for at least one split rule. */ + @GuardedBy("mLock") private void updateAnimationOverride(@NonNull TaskContainer taskContainer) { - if (!taskContainer.isTaskBoundsInitialized() - || !taskContainer.isWindowingModeInitialized()) { + if (ENABLE_SHELL_TRANSITIONS) { + // TODO(b/207070762): cleanup with legacy app transition + // Animation will be handled by WM Shell with Shell transition enabled. + return; + } + if (!taskContainer.isTaskBoundsInitialized()) { // We don't know about the Task bounds/windowingMode yet. return; } - // We only want to override if it supports split. - if (supportSplit(taskContainer)) { + // We only want to override if the TaskContainer may show split. + if (mayShowSplit(taskContainer)) { mPresenter.startOverrideSplitAnimation(taskContainer.getTaskId()); } else { mPresenter.stopOverrideSplitAnimation(taskContainer.getTaskId()); } } - private boolean supportSplit(@NonNull TaskContainer taskContainer) { + /** Returns whether the given {@link TaskContainer} may show in split. */ + // Suppress GuardedBy warning because lint asks to mark this method as + // @GuardedBy(mPresenter.mController.mLock), which is mLock itself + @SuppressWarnings("GuardedBy") + @GuardedBy("mLock") + private boolean mayShowSplit(@NonNull TaskContainer taskContainer) { // No split inside PIP. if (taskContainer.isInPictureInPicture()) { return false; } + // Always assume the TaskContainer if SplitAttributesCalculator is set + if (mSplitAttributesCalculator != null) { + return true; + } // Check if the parent container bounds can support any split rule. for (EmbeddingRule rule : mSplitRules) { if (!(rule instanceof SplitRule)) { continue; } - if (shouldShowSideBySide(taskContainer.getTaskBounds(), (SplitRule) rule)) { + final SplitRule splitRule = (SplitRule) rule; + final SplitAttributes splitAttributes = mPresenter.computeSplitAttributes( + taskContainer.getTaskProperties(), splitRule, null /* minDimensionsPair */); + if (shouldShowSplit(splitAttributes)) { return true; } } @@ -364,10 +660,12 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen } @VisibleForTesting - void onActivityCreated(@NonNull Activity launchedActivity) { + @GuardedBy("mLock") + void onActivityCreated(@NonNull WindowContainerTransaction wct, + @NonNull Activity launchedActivity) { // TODO(b/229680885): we don't support launching into primary yet because we want to always // launch the new activity on top. - resolveActivityToContainer(launchedActivity, false /* isOnReparent */); + resolveActivityToContainer(wct, launchedActivity, false /* isOnReparent */); updateCallbackIfNecessary(); } @@ -381,7 +679,9 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen * in a state that the caller shouldn't handle. */ @VisibleForTesting - boolean resolveActivityToContainer(@NonNull Activity activity, boolean isOnReparent) { + @GuardedBy("mLock") + boolean resolveActivityToContainer(@NonNull WindowContainerTransaction wct, + @NonNull Activity activity, boolean isOnReparent) { if (isInPictureInPicture(activity) || activity.isFinishing()) { // We don't embed activity when it is in PIP, or finishing. Return true since we don't // want any extra handling. @@ -389,7 +689,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen } if (!isOnReparent && getContainerWithActivity(activity) == null - && getInitialTaskFragmentToken(activity) != null) { + && getTaskFragmentTokenFromActivityClientRecord(activity) != null) { // We can't find the new launched activity in any recorded container, but it is // currently placed in an embedded TaskFragment. This can happen in two cases: // 1. the activity is embedded in another app. @@ -413,12 +713,12 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen // 1. Whether the new launched activity should always expand. if (shouldExpand(activity, null /* intent */)) { - expandActivity(activity); + expandActivity(wct, activity); return true; } // 2. Whether the new launched activity should launch a placeholder. - if (launchPlaceholderIfNecessary(activity, !isOnReparent)) { + if (launchPlaceholderIfNecessary(wct, activity, !isOnReparent)) { return true; } @@ -433,11 +733,11 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen // Can't find any activity below. return false; } - if (putActivitiesIntoSplitIfNecessary(activityBelow, activity)) { + if (putActivitiesIntoSplitIfNecessary(wct, activityBelow, activity)) { // Have split rule of [ activityBelow | launchedActivity ]. return true; } - if (isOnReparent && putActivitiesIntoSplitIfNecessary(activity, activityBelow)) { + if (isOnReparent && putActivitiesIntoSplitIfNecessary(wct, activity, activityBelow)) { // Have split rule of [ launchedActivity | activityBelow]. return true; } @@ -460,19 +760,20 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen // Can't find the top activity on the other split TaskFragment. return false; } - if (putActivitiesIntoSplitIfNecessary(otherTopActivity, activity)) { + if (putActivitiesIntoSplitIfNecessary(wct, otherTopActivity, activity)) { // Have split rule of [ otherTopActivity | launchedActivity ]. return true; } // Have split rule of [ launchedActivity | otherTopActivity]. - return isOnReparent && putActivitiesIntoSplitIfNecessary(activity, otherTopActivity); + return isOnReparent && putActivitiesIntoSplitIfNecessary(wct, activity, otherTopActivity); } /** * Places the given activity to the top most TaskFragment in the task if there is any. */ @VisibleForTesting - void placeActivityInTopContainer(@NonNull Activity activity) { + void placeActivityInTopContainer(@NonNull WindowContainerTransaction wct, + @NonNull Activity activity) { if (getContainerWithActivity(activity) != null) { // The activity has already been put in a TaskFragment. This is likely to be done by // the server when the activity is started. @@ -488,21 +789,25 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen return; } targetContainer.addPendingAppearedActivity(activity); - final WindowContainerTransaction wct = new WindowContainerTransaction(); wct.reparentActivityToTaskFragment(targetContainer.getTaskFragmentToken(), activity.getActivityToken()); - mPresenter.applyTransaction(wct); } /** * Starts an activity to side of the launchingActivity with the provided split config. */ - private void startActivityToSide(@NonNull Activity launchingActivity, @NonNull Intent intent, + // Suppress GuardedBy warning because lint ask to mark this method as + // @GuardedBy(container.mController.mLock), which is mLock itself + @SuppressWarnings("GuardedBy") + @GuardedBy("mLock") + private void startActivityToSide(@NonNull WindowContainerTransaction wct, + @NonNull Activity launchingActivity, @NonNull Intent intent, @Nullable Bundle options, @NonNull SplitRule sideRule, - @Nullable Consumer<Exception> failureCallback, boolean isPlaceholder) { + @NonNull SplitAttributes splitAttributes, @Nullable Consumer<Exception> failureCallback, + boolean isPlaceholder) { try { - mPresenter.startActivityToSide(launchingActivity, intent, options, sideRule, - isPlaceholder); + mPresenter.startActivityToSide(wct, launchingActivity, intent, options, sideRule, + splitAttributes, isPlaceholder); } catch (Exception e) { if (failureCallback != null) { failureCallback.accept(e); @@ -514,19 +819,25 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen * Expands the given activity by either expanding the TaskFragment it is currently in or putting * it into a new expanded TaskFragment. */ - private void expandActivity(@NonNull Activity activity) { + @GuardedBy("mLock") + private void expandActivity(@NonNull WindowContainerTransaction wct, + @NonNull Activity activity) { final TaskFragmentContainer container = getContainerWithActivity(activity); if (shouldContainerBeExpanded(container)) { // Make sure that the existing container is expanded. - mPresenter.expandTaskFragment(container.getTaskFragmentToken()); + mPresenter.expandTaskFragment(wct, container.getTaskFragmentToken()); } else { // Put activity into a new expanded container. final TaskFragmentContainer newContainer = newContainer(activity, getTaskId(activity)); - mPresenter.expandActivity(newContainer.getTaskFragmentToken(), activity); + mPresenter.expandActivity(wct, newContainer.getTaskFragmentToken(), activity); } } /** Whether the given new launched activity is in a split with a rule matched. */ + // Suppress GuardedBy warning because lint asks to mark this method as + // @GuardedBy(mPresenter.mController.mLock), which is mLock itself + @SuppressWarnings("GuardedBy") + @GuardedBy("mLock") private boolean isNewActivityInSplitWithRuleMatched(@NonNull Activity launchedActivity) { final TaskFragmentContainer container = getContainerWithActivity(launchedActivity); final SplitContainer splitContainer = getActiveSplitForContainer(container); @@ -607,8 +918,12 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen * Checks if there is a rule to split the two activities. If there is one, puts them into split * and returns {@code true}. Otherwise, returns {@code false}. */ - private boolean putActivitiesIntoSplitIfNecessary(@NonNull Activity primaryActivity, - @NonNull Activity secondaryActivity) { + // Suppress GuardedBy warning because lint ask to mark this method as + // @GuardedBy(mPresenter.mController.mLock), which is mLock itself + @SuppressWarnings("GuardedBy") + @GuardedBy("mLock") + private boolean putActivitiesIntoSplitIfNecessary(@NonNull WindowContainerTransaction wct, + @NonNull Activity primaryActivity, @NonNull Activity secondaryActivity) { final SplitPairRule splitRule = getSplitRule(primaryActivity, secondaryActivity); if (splitRule == null) { return false; @@ -616,8 +931,9 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen final TaskFragmentContainer primaryContainer = getContainerWithActivity( primaryActivity); final SplitContainer splitContainer = getActiveSplitForContainer(primaryContainer); + final WindowMetrics taskWindowMetrics = mPresenter.getTaskWindowMetrics(primaryActivity); if (splitContainer != null && primaryContainer == splitContainer.getPrimaryContainer() - && canReuseContainer(splitRule, splitContainer.getSplitRule())) { + && canReuseContainer(splitRule, splitContainer.getSplitRule(), taskWindowMetrics)) { // Can launch in the existing secondary container if the rules share the same // presentation. final TaskFragmentContainer secondaryContainer = splitContainer.getSecondaryContainer(); @@ -626,23 +942,23 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen return true; } secondaryContainer.addPendingAppearedActivity(secondaryActivity); - final WindowContainerTransaction wct = new WindowContainerTransaction(); if (mPresenter.expandSplitContainerIfNeeded(wct, splitContainer, primaryActivity, secondaryActivity, null /* secondaryIntent */) != RESULT_EXPAND_FAILED_NO_TF_INFO) { wct.reparentActivityToTaskFragment( secondaryContainer.getTaskFragmentToken(), secondaryActivity.getActivityToken()); - mPresenter.applyTransaction(wct); return true; } } // Create new split pair. - mPresenter.createNewSplitContainer(primaryActivity, secondaryActivity, splitRule); + mPresenter.createNewSplitContainer(wct, primaryActivity, secondaryActivity, splitRule); return true; } - private void onActivityConfigurationChanged(@NonNull Activity activity) { + @GuardedBy("mLock") + private void onActivityConfigurationChanged(@NonNull WindowContainerTransaction wct, + @NonNull Activity activity) { if (activity.isFinishing()) { // Do nothing if the activity is currently finishing. return; @@ -661,15 +977,16 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen } // Check if activity requires a placeholder - launchPlaceholderIfNecessary(activity, false /* isOnCreated */); + launchPlaceholderIfNecessary(wct, activity, false /* isOnCreated */); } @VisibleForTesting + @GuardedBy("mLock") void onActivityDestroyed(@NonNull Activity activity) { // Remove any pending appeared activity, as the server won't send finished activity to the // organizer. for (int i = mTaskContainers.size() - 1; i >= 0; i--) { - mTaskContainers.valueAt(i).cleanupPendingAppearedActivity(activity); + mTaskContainers.valueAt(i).onActivityDestroyed(activity); } // We didn't trigger the callback if there were any pending appeared activities, so check // again after the pending is removed. @@ -680,8 +997,53 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen * Called when we have been waiting too long for the TaskFragment to become non-empty after * creation. */ + @GuardedBy("mLock") void onTaskFragmentAppearEmptyTimeout(@NonNull TaskFragmentContainer container) { - mPresenter.cleanupContainer(container, false /* shouldFinishDependent */); + final WindowContainerTransaction wct = new WindowContainerTransaction(); + onTaskFragmentAppearEmptyTimeout(wct, container); + // Can be applied independently as a timeout callback. + mPresenter.applyTransaction(wct, getTransitionType(wct), + true /* shouldApplyIndependently */); + } + + /** + * Called when we have been waiting too long for the TaskFragment to become non-empty after + * creation. + */ + @GuardedBy("mLock") + void onTaskFragmentAppearEmptyTimeout(@NonNull WindowContainerTransaction wct, + @NonNull TaskFragmentContainer container) { + mPresenter.cleanupContainer(wct, container, false /* shouldFinishDependent */); + } + + @Nullable + @GuardedBy("mLock") + private TaskFragmentContainer resolveStartActivityIntentFromNonActivityContext( + @NonNull WindowContainerTransaction wct, @NonNull Intent intent) { + final int taskCount = mTaskContainers.size(); + if (taskCount == 0) { + // We don't have other Activity to check split with. + return null; + } + if (taskCount > 1) { + Log.w(TAG, "App is calling startActivity from a non-Activity context when it has" + + " more than one Task. If the new launch Activity is in a different process," + + " and it is expected to be embedded, please start it from an Activity" + + " instead."); + return null; + } + + // Check whether the Intent should be embedded in the known Task. + final TaskContainer taskContainer = mTaskContainers.valueAt(0); + if (taskContainer.isInPictureInPicture() + || taskContainer.getTopNonFinishingActivity() == null) { + // We don't embed activity when it is in PIP, or if we can't find any other owner + // activity in the Task. + return null; + } + + return resolveStartActivityIntent(wct, taskContainer.getTaskId(), intent, + null /* launchingActivity */); } /** @@ -701,6 +1063,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen */ @VisibleForTesting @Nullable + @GuardedBy("mLock") TaskFragmentContainer resolveStartActivityIntent(@NonNull WindowContainerTransaction wct, int taskId, @NonNull Intent intent, @Nullable Activity launchingActivity) { /* @@ -763,6 +1126,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen /** * Returns an empty expanded {@link TaskFragmentContainer} that we can launch an activity into. */ + @GuardedBy("mLock") @Nullable private TaskFragmentContainer createEmptyExpandedContainer( @NonNull WindowContainerTransaction wct, @NonNull Intent intent, int taskId, @@ -793,6 +1157,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen * Returns a container for the new activity intent to launch into as splitting with the primary * activity. */ + @GuardedBy("mLock") @Nullable private TaskFragmentContainer getSecondaryContainerForSplitIfAny( @NonNull WindowContainerTransaction wct, @NonNull Activity primaryActivity, @@ -803,8 +1168,9 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen } final TaskFragmentContainer existingContainer = getContainerWithActivity(primaryActivity); final SplitContainer splitContainer = getActiveSplitForContainer(existingContainer); + final WindowMetrics taskWindowMetrics = mPresenter.getTaskWindowMetrics(primaryActivity); if (splitContainer != null && existingContainer == splitContainer.getPrimaryContainer() - && (canReuseContainer(splitRule, splitContainer.getSplitRule()) + && (canReuseContainer(splitRule, splitContainer.getSplitRule(), taskWindowMetrics) // TODO(b/231845476) we should always respect clearTop. || !respectClearTop) && mPresenter.expandSplitContainerIfNeeded(wct, splitContainer, primaryActivity, @@ -843,12 +1209,14 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen return newContainer(pendingAppearedActivity, pendingAppearedActivity, taskId); } + @GuardedBy("mLock") TaskFragmentContainer newContainer(@NonNull Activity pendingAppearedActivity, @NonNull Activity activityInTask, int taskId) { return newContainer(pendingAppearedActivity, null /* pendingAppearedIntent */, activityInTask, taskId); } + @GuardedBy("mLock") TaskFragmentContainer newContainer(@NonNull Intent pendingAppearedIntent, @NonNull Activity activityInTask, int taskId) { return newContainer(null /* pendingAppearedActivity */, pendingAppearedIntent, @@ -865,13 +1233,14 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen * if needed. * @param taskId parent Task of the new TaskFragment. */ + @GuardedBy("mLock") TaskFragmentContainer newContainer(@Nullable Activity pendingAppearedActivity, @Nullable Intent pendingAppearedIntent, @NonNull Activity activityInTask, int taskId) { if (activityInTask == null) { throw new IllegalArgumentException("activityInTask must not be null,"); } if (!mTaskContainers.contains(taskId)) { - mTaskContainers.put(taskId, new TaskContainer(taskId)); + mTaskContainers.put(taskId, new TaskContainer(taskId, activityInTask)); } final TaskContainer taskContainer = mTaskContainers.get(taskId); final TaskFragmentContainer container = new TaskFragmentContainer(pendingAppearedActivity, @@ -883,10 +1252,6 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen Log.w(TAG, "Can't find bounds from activity=" + activityInTask); } } - if (!taskContainer.isWindowingModeInitialized()) { - taskContainer.setWindowingMode(activityInTask.getResources().getConfiguration() - .windowConfiguration.getWindowingMode()); - } updateAnimationOverride(taskContainer); return container; } @@ -895,12 +1260,16 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen * Creates and registers a new split with the provided containers and configuration. Finishes * existing secondary containers if found for the given primary container. */ + // Suppress GuardedBy warning because lint ask to mark this method as + // @GuardedBy(mPresenter.mController.mLock), which is mLock itself + @SuppressWarnings("GuardedBy") + @GuardedBy("mLock") void registerSplit(@NonNull WindowContainerTransaction wct, @NonNull TaskFragmentContainer primaryContainer, @NonNull Activity primaryActivity, @NonNull TaskFragmentContainer secondaryContainer, - @NonNull SplitRule splitRule) { + @NonNull SplitRule splitRule, @NonNull SplitAttributes splitAttributes) { final SplitContainer splitContainer = new SplitContainer(primaryContainer, primaryActivity, - secondaryContainer, splitRule); + secondaryContainer, splitRule, splitAttributes); // Remove container later to prevent pinning escaping toast showing in lock task mode. if (splitRule instanceof SplitPairRule && ((SplitPairRule) splitRule).shouldClearTop()) { removeExistingSecondaryContainers(wct, primaryContainer); @@ -909,6 +1278,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen } /** Cleanups all the dependencies when the TaskFragment is entering PIP. */ + @GuardedBy("mLock") private void cleanupForEnterPip(@NonNull WindowContainerTransaction wct, @NonNull TaskFragmentContainer container) { final TaskContainer taskContainer = container.getTaskContainer(); @@ -1022,9 +1392,10 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen * Updates the presentation of the container. If the container is part of the split or should * have a placeholder, it will also update the other part of the split. */ + @GuardedBy("mLock") void updateContainer(@NonNull WindowContainerTransaction wct, @NonNull TaskFragmentContainer container) { - if (launchPlaceholderIfNecessary(container)) { + if (launchPlaceholderIfNecessary(wct, container)) { // Placeholder was launched, the positions will be updated when the activity is added // to the secondary container. return; @@ -1049,7 +1420,13 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen // Skip position update - one or both containers are finished. return; } - if (dismissPlaceholderIfNecessary(splitContainer)) { + final TaskContainer taskContainer = splitContainer.getTaskContainer(); + final SplitRule splitRule = splitContainer.getSplitRule(); + final Pair<Size, Size> minDimensionsPair = splitContainer.getMinDimensionsPair(); + final SplitAttributes splitAttributes = mPresenter.computeSplitAttributes( + taskContainer.getTaskProperties(), splitRule, minDimensionsPair); + splitContainer.setSplitAttributes(splitAttributes); + if (dismissPlaceholderIfNecessary(wct, splitContainer)) { // Placeholder was finished, the positions will be updated when its container is emptied return; } @@ -1111,16 +1488,23 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen /** * Checks if the container requires a placeholder and launches it if necessary. */ - private boolean launchPlaceholderIfNecessary(@NonNull TaskFragmentContainer container) { + @GuardedBy("mLock") + private boolean launchPlaceholderIfNecessary(@NonNull WindowContainerTransaction wct, + @NonNull TaskFragmentContainer container) { final Activity topActivity = container.getTopNonFinishingActivity(); if (topActivity == null) { return false; } - return launchPlaceholderIfNecessary(topActivity, false /* isOnCreated */); + return launchPlaceholderIfNecessary(wct, topActivity, false /* isOnCreated */); } - boolean launchPlaceholderIfNecessary(@NonNull Activity activity, boolean isOnCreated) { + // Suppress GuardedBy warning because lint ask to mark this method as + // @GuardedBy(mPresenter.mController.mLock), which is mLock itself + @SuppressWarnings("GuardedBy") + @GuardedBy("mLock") + boolean launchPlaceholderIfNecessary(@NonNull WindowContainerTransaction wct, + @NonNull Activity activity, boolean isOnCreated) { if (activity.isFinishing()) { return false; } @@ -1144,18 +1528,20 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen return false; } + final TaskContainer.TaskProperties taskProperties = mPresenter.getTaskProperties(activity); final Pair<Size, Size> minDimensionsPair = getActivityIntentMinDimensionsPair(activity, placeholderRule.getPlaceholderIntent()); - if (!shouldShowSideBySide( - mPresenter.getParentContainerBounds(activity), placeholderRule, - minDimensionsPair)) { + final SplitAttributes splitAttributes = mPresenter.computeSplitAttributes(taskProperties, + placeholderRule, minDimensionsPair); + if (!SplitPresenter.shouldShowSplit(splitAttributes)) { return false; } // TODO(b/190433398): Handle failed request final Bundle options = getPlaceholderOptions(activity, isOnCreated); - startActivityToSide(activity, placeholderRule.getPlaceholderIntent(), options, - placeholderRule, null /* failureCallback */, true /* isPlaceholder */); + startActivityToSide(wct, activity, placeholderRule.getPlaceholderIntent(), options, + placeholderRule, splitAttributes, null /* failureCallback */, + true /* isPlaceholder */); return true; } @@ -1180,8 +1566,13 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen return options.toBundle(); } + // Suppress GuardedBy warning because lint ask to mark this method as + // @GuardedBy(mPresenter.mController.mLock), which is mLock itself + @SuppressWarnings("GuardedBy") @VisibleForTesting - boolean dismissPlaceholderIfNecessary(@NonNull SplitContainer splitContainer) { + @GuardedBy("mLock") + boolean dismissPlaceholderIfNecessary(@NonNull WindowContainerTransaction wct, + @NonNull SplitContainer splitContainer) { if (!splitContainer.isPlaceholderContainer()) { return false; } @@ -1190,12 +1581,11 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen // The placeholder should remain after it was first shown. return false; } - - if (shouldShowSideBySide(splitContainer)) { + final SplitAttributes splitAttributes = splitContainer.getSplitAttributes(); + if (SplitPresenter.shouldShowSplit(splitAttributes)) { return false; } - - mPresenter.cleanupContainer(splitContainer.getSecondaryContainer(), + mPresenter.cleanupContainer(wct, splitContainer.getSecondaryContainer(), false /* shouldFinishDependent */); return true; } @@ -1204,6 +1594,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen * Returns the rule to launch a placeholder for the activity with the provided component name * if it is configured in the split config. */ + @GuardedBy("mLock") private SplitPlaceholderRule getPlaceholderRule(@NonNull Activity activity) { for (EmbeddingRule rule : mSplitRules) { if (!(rule instanceof SplitPlaceholderRule)) { @@ -1220,6 +1611,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen /** * Notifies listeners about changes to split states if necessary. */ + @GuardedBy("mLock") private void updateCallbackIfNecessary() { if (mEmbeddingCallback == null) { return; @@ -1241,6 +1633,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen * null, that indicates that the active split states are in an intermediate state and should * not be reported. */ + @GuardedBy("mLock") @Nullable private List<SplitInfo> getActiveSplitStates() { List<SplitInfo> splitStates = new ArrayList<>(); @@ -1260,12 +1653,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen final ActivityStack secondaryContainer = container.getSecondaryContainer() .toActivityStack(); final SplitInfo splitState = new SplitInfo(primaryContainer, secondaryContainer, - // Splits that are not showing side-by-side are reported as having 0 split - // ratio, since by definition in the API the primary container occupies no - // width of the split when covered by the secondary. - shouldShowSideBySide(container) - ? container.getSplitRule().getSplitRatio() - : 0.0f); + container.getSplitAttributes()); splitStates.add(splitState); } } @@ -1303,6 +1691,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen * Returns a split rule for the provided pair of primary activity and secondary activity intent * if available. */ + @GuardedBy("mLock") @Nullable private SplitPairRule getSplitRule(@NonNull Activity primaryActivity, @NonNull Intent secondaryActivityIntent) { @@ -1321,6 +1710,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen /** * Returns a split rule for the provided pair of primary and secondary activities if available. */ + @GuardedBy("mLock") @Nullable private SplitPairRule getSplitRule(@NonNull Activity primaryActivity, @NonNull Activity secondaryActivity) { @@ -1373,22 +1763,29 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen return ActivityThread.currentActivityThread().getActivity(activityToken); } + @VisibleForTesting + ActivityStartMonitor getActivityStartMonitor() { + return mActivityStartMonitor; + } + /** - * Gets the token of the initial TaskFragment that embedded this activity. Do not rely on it - * after creation because the activity could be reparented. + * Gets the token of the TaskFragment that embedded this activity. It is available as soon as + * the activity is created and attached, so it can be used during {@link #onActivityCreated} + * before the server notifies the organizer to avoid racing condition. */ @VisibleForTesting @Nullable - IBinder getInitialTaskFragmentToken(@NonNull Activity activity) { + IBinder getTaskFragmentTokenFromActivityClientRecord(@NonNull Activity activity) { final ActivityThread.ActivityClientRecord record = ActivityThread.currentActivityThread() .getActivityClient(activity.getActivityToken()); - return record != null ? record.mInitialTaskFragmentToken : null; + return record != null ? record.mTaskFragmentToken : null; } /** * Returns {@code true} if an Activity with the provided component name should always be * expanded to occupy full task bounds. Such activity must not be put in a split. */ + @GuardedBy("mLock") private boolean shouldExpand(@Nullable Activity activity, @Nullable Intent intent) { for (EmbeddingRule rule : mSplitRules) { if (!(rule instanceof ActivityRule)) { @@ -1414,6 +1811,10 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen * 'sticky' and the placeholder was finished when fully overlapping the primary container. * @return {@code true} if the associated container should be retained (and not be finished). */ + // Suppress GuardedBy warning because lint ask to mark this method as + // @GuardedBy(mPresenter.mController.mLock), which is mLock itself + @SuppressWarnings("GuardedBy") + @GuardedBy("mLock") boolean shouldRetainAssociatedContainer(@NonNull TaskFragmentContainer finishingContainer, @NonNull TaskFragmentContainer associatedContainer) { SplitContainer splitContainer = getActiveSplitForContainers(associatedContainer, @@ -1432,7 +1833,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen } // Decide whether the associated container should be retained based on the current // presentation mode. - if (shouldShowSideBySide(splitContainer)) { + if (shouldShowSplit(splitContainer)) { return !shouldFinishAssociatedContainerWhenAdjacent(finishBehavior); } else { return !shouldFinishAssociatedContainerWhenStacked(finishBehavior); @@ -1456,10 +1857,12 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen private final class LifecycleCallbacks extends EmptyLifecycleCallbacksAdapter { @Override - public void onActivityPreCreated(Activity activity, Bundle savedInstanceState) { + public void onActivityPreCreated(@NonNull Activity activity, + @Nullable Bundle savedInstanceState) { synchronized (mLock) { final IBinder activityToken = activity.getActivityToken(); - final IBinder initialTaskFragmentToken = getInitialTaskFragmentToken(activity); + final IBinder initialTaskFragmentToken = + getTaskFragmentTokenFromActivityClientRecord(activity); // If the activity is not embedded, then it will not have an initial task fragment // token so no further action is needed. if (initialTaskFragmentToken == null) { @@ -1485,25 +1888,35 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen } @Override - public void onActivityPostCreated(Activity activity, Bundle savedInstanceState) { + public void onActivityPostCreated(@NonNull Activity activity, + @Nullable Bundle savedInstanceState) { // Calling after Activity#onCreate is complete to allow the app launch something // first. In case of a configured placeholder activity we want to make sure // that we don't launch it if an activity itself already requested something to be // launched to side. synchronized (mLock) { - SplitController.this.onActivityCreated(activity); + final WindowContainerTransaction wct = new WindowContainerTransaction(); + SplitController.this.onActivityCreated(wct, activity); + // The WCT should be applied and merged to the activity launch transition. + mPresenter.applyTransaction(wct, getTransitionType(wct), + false /* shouldApplyIndependently */); } } @Override - public void onActivityConfigurationChanged(Activity activity) { + public void onActivityConfigurationChanged(@NonNull Activity activity) { synchronized (mLock) { - SplitController.this.onActivityConfigurationChanged(activity); + final WindowContainerTransaction wct = new WindowContainerTransaction(); + SplitController.this.onActivityConfigurationChanged(wct, activity); + // The WCT should be applied and merged to the Task change transition so that the + // placeholder is launched in the same transition. + mPresenter.applyTransaction(wct, getTransitionType(wct), + false /* shouldApplyIndependently */); } } @Override - public void onActivityPostDestroyed(Activity activity) { + public void onActivityPostDestroyed(@NonNull Activity activity) { synchronized (mLock) { SplitController.this.onActivityDestroyed(activity); } @@ -1515,7 +1928,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen private final Handler mHandler = new Handler(Looper.getMainLooper()); @Override - public void execute(Runnable r) { + public void execute(@NonNull Runnable r) { mHandler.post(r); } } @@ -1524,39 +1937,78 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen * A monitor that intercepts all activity start requests originating in the client process and * can amend them to target a specific task fragment to form a split. */ - private class ActivityStartMonitor extends Instrumentation.ActivityMonitor { + @VisibleForTesting + class ActivityStartMonitor extends Instrumentation.ActivityMonitor { + @VisibleForTesting + Intent mCurrentIntent; @Override public Instrumentation.ActivityResult onStartActivity(@NonNull Context who, @NonNull Intent intent, @NonNull Bundle options) { - // TODO(b/190433398): Check if the activity is configured to always be expanded. - - // Check if activity should be put in a split with the activity that launched it. - if (!(who instanceof Activity)) { - return super.onStartActivity(who, intent, options); - } - final Activity launchingActivity = (Activity) who; - if (isInPictureInPicture(launchingActivity)) { - // We don't embed activity when it is in PIP. - return super.onStartActivity(who, intent, options); + // TODO(b/232042367): Consolidate the activity create handling so that we can handle + // cross-process the same as normal. + + final Activity launchingActivity; + if (who instanceof Activity) { + // We will check if the new activity should be split with the activity that launched + // it. + launchingActivity = (Activity) who; + if (isInPictureInPicture(launchingActivity)) { + // We don't embed activity when it is in PIP. + return super.onStartActivity(who, intent, options); + } + } else { + // When the context to start activity is not an Activity context, we will check if + // the new activity should be embedded in the known Task belonging to the organizer + // process. @see #resolveStartActivityIntentFromNonActivityContext + // It is a current security limitation that we can't access the activity info of + // other process even if it is in the same Task. + launchingActivity = null; } synchronized (mLock) { - final int taskId = getTaskId(launchingActivity); final WindowContainerTransaction wct = new WindowContainerTransaction(); - final TaskFragmentContainer launchedInTaskFragment = resolveStartActivityIntent(wct, - taskId, intent, launchingActivity); + final TaskFragmentContainer launchedInTaskFragment; + if (launchingActivity != null) { + final int taskId = getTaskId(launchingActivity); + launchedInTaskFragment = resolveStartActivityIntent(wct, taskId, intent, + launchingActivity); + } else { + launchedInTaskFragment = resolveStartActivityIntentFromNonActivityContext(wct, + intent); + } if (launchedInTaskFragment != null) { - mPresenter.applyTransaction(wct); + // Make sure the WCT is applied immediately instead of being queued so that the + // TaskFragment will be ready before activity attachment. + mPresenter.applyTransaction(wct, getTransitionType(wct), + false /* shouldApplyIndependently */); // Amend the request to let the WM know that the activity should be placed in // the dedicated container. options.putBinder(ActivityOptions.KEY_LAUNCH_TASK_FRAGMENT_TOKEN, launchedInTaskFragment.getTaskFragmentToken()); + mCurrentIntent = intent; } } return super.onStartActivity(who, intent, options); } + + @Override + public void onStartActivityResult(int result, @NonNull Bundle bOptions) { + super.onStartActivityResult(result, bOptions); + if (mCurrentIntent != null && result != START_SUCCESS) { + // Clear the pending appeared intent if the activity was not started successfully. + final IBinder token = bOptions.getBinder( + ActivityOptions.KEY_LAUNCH_TASK_FRAGMENT_TOKEN); + if (token != null) { + final TaskFragmentContainer container = getContainer(token); + if (container != null) { + container.clearPendingAppearedIntentIfNeeded(mCurrentIntent); + } + } + } + mCurrentIntent = null; + } } /** @@ -1574,29 +2026,40 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen * If the two rules have the same presentation, we can reuse the same {@link SplitContainer} if * there is any. */ - private static boolean canReuseContainer(SplitRule rule1, SplitRule rule2) { + private static boolean canReuseContainer(@NonNull SplitRule rule1, @NonNull SplitRule rule2, + @NonNull WindowMetrics parentWindowMetrics) { if (!isContainerReusableRule(rule1) || !isContainerReusableRule(rule2)) { return false; } - return haveSamePresentation((SplitPairRule) rule1, (SplitPairRule) rule2); + return haveSamePresentation((SplitPairRule) rule1, (SplitPairRule) rule2, + parentWindowMetrics); } /** Whether the two rules have the same presentation. */ - private static boolean haveSamePresentation(SplitPairRule rule1, SplitPairRule rule2) { + @VisibleForTesting + static boolean haveSamePresentation(@NonNull SplitPairRule rule1, + @NonNull SplitPairRule rule2, @NonNull WindowMetrics parentWindowMetrics) { + if (rule1.getTag() != null || rule2.getTag() != null) { + // Tag must be unique if it is set. We don't want to reuse the container if the rules + // have different tags because they can have different SplitAttributes later through + // SplitAttributesCalculator. + return Objects.equals(rule1.getTag(), rule2.getTag()); + } + // If both rules don't have tag, compare all SplitRules' properties that may affect their + // SplitAttributes. // TODO(b/231655482): add util method to do the comparison in SplitPairRule. - return rule1.getSplitRatio() == rule2.getSplitRatio() - && rule1.getLayoutDirection() == rule2.getLayoutDirection() - && rule1.getFinishPrimaryWithSecondary() - == rule2.getFinishPrimaryWithSecondary() - && rule1.getFinishSecondaryWithPrimary() - == rule2.getFinishSecondaryWithPrimary(); + return rule1.getDefaultSplitAttributes().equals(rule2.getDefaultSplitAttributes()) + && rule1.checkParentMetrics(parentWindowMetrics) + == rule2.checkParentMetrics(parentWindowMetrics) + && rule1.getFinishPrimaryWithSecondary() == rule2.getFinishPrimaryWithSecondary() + && rule1.getFinishSecondaryWithPrimary() == rule2.getFinishSecondaryWithPrimary(); } /** * Whether it is ok for other rule to reuse the {@link TaskFragmentContainer} of the given * rule. */ - private static boolean isContainerReusableRule(SplitRule rule) { + private static boolean isContainerReusableRule(@NonNull SplitRule rule) { // We don't expect to reuse the placeholder rule. if (!(rule instanceof SplitPairRule)) { return false; 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 a89847a30d20..79603233ae14 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java @@ -22,11 +22,11 @@ import android.app.Activity; import android.app.ActivityThread; import android.app.WindowConfiguration; import android.app.WindowConfiguration.WindowingMode; -import android.content.Context; import android.content.Intent; import android.content.pm.ActivityInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; +import android.content.res.Configuration; import android.graphics.Rect; import android.os.Bundle; import android.os.IBinder; @@ -38,12 +38,25 @@ import android.view.WindowInsets; import android.view.WindowMetrics; import android.window.WindowContainerTransaction; +import androidx.annotation.GuardedBy; import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.window.extensions.embedding.SplitAttributes.SplitType; +import androidx.window.extensions.embedding.SplitAttributes.SplitType.ExpandContainersSplitType; +import androidx.window.extensions.embedding.SplitAttributes.SplitType.HingeSplitType; +import androidx.window.extensions.embedding.SplitAttributes.SplitType.RatioSplitType; +import androidx.window.extensions.embedding.SplitAttributesCalculator.SplitAttributesCalculatorParams; +import androidx.window.extensions.embedding.TaskContainer.TaskProperties; +import androidx.window.extensions.layout.DisplayFeature; +import androidx.window.extensions.layout.FoldingFeature; +import androidx.window.extensions.layout.WindowLayoutInfo; import com.android.internal.annotations.VisibleForTesting; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; import java.util.concurrent.Executor; /** @@ -65,11 +78,25 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { }) private @interface Position {} + private static final int CONTAINER_POSITION_LEFT = 0; + private static final int CONTAINER_POSITION_TOP = 1; + private static final int CONTAINER_POSITION_RIGHT = 2; + private static final int CONTAINER_POSITION_BOTTOM = 3; + + @IntDef(value = { + CONTAINER_POSITION_LEFT, + CONTAINER_POSITION_TOP, + CONTAINER_POSITION_RIGHT, + CONTAINER_POSITION_BOTTOM, + }) + private @interface ContainerPosition {} + /** * Result of {@link #expandSplitContainerIfNeeded(WindowContainerTransaction, SplitContainer, * Activity, Activity, Intent)}. * No need to expand the splitContainer because screen is big enough to - * {@link #shouldShowSideBySide(Rect, SplitRule, Pair)} and minimum dimensions is satisfied. + * {@link #shouldShowSplit(SplitAttributes)} and minimum dimensions is + * satisfied. */ static final int RESULT_NOT_EXPANDED = 0; /** @@ -77,7 +104,7 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { * Activity, Activity, Intent)}. * The splitContainer should be expanded. It is usually because minimum dimensions is not * satisfied. - * @see #shouldShowSideBySide(Rect, SplitRule, Pair) + * @see #shouldShowSplit(SplitAttributes) */ static final int RESULT_EXPANDED = 1; /** @@ -100,39 +127,26 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { }) private @interface ResultCode {} + @VisibleForTesting + static final SplitAttributes EXPAND_CONTAINERS_ATTRIBUTES = + new SplitAttributes.Builder() + .setSplitType(new ExpandContainersSplitType()) + .build(); + private final SplitController mController; - SplitPresenter(@NonNull Executor executor, SplitController controller) { + SplitPresenter(@NonNull Executor executor, @NonNull SplitController controller) { super(executor, controller); mController = controller; registerOrganizer(); } /** - * Updates the presentation of the provided container. - */ - void updateContainer(@NonNull TaskFragmentContainer container) { - final WindowContainerTransaction wct = new WindowContainerTransaction(); - mController.updateContainer(wct, container); - applyTransaction(wct); - } - - /** * Deletes the specified container and all other associated and dependent containers in the same * transaction. */ - void cleanupContainer(@NonNull TaskFragmentContainer container, boolean shouldFinishDependent) { - final WindowContainerTransaction wct = new WindowContainerTransaction(); - cleanupContainer(container, shouldFinishDependent, wct); - applyTransaction(wct); - } - - /** - * Deletes the specified container and all other associated and dependent containers in the same - * transaction. - */ - void cleanupContainer(@NonNull TaskFragmentContainer container, boolean shouldFinishDependent, - @NonNull WindowContainerTransaction wct) { + void cleanupContainer(@NonNull WindowContainerTransaction wct, + @NonNull TaskFragmentContainer container, boolean shouldFinishDependent) { container.finish(shouldFinishDependent, this, wct, mController); final TaskFragmentContainer newTopContainer = mController.getTopActiveContainer( @@ -147,14 +161,17 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { * @return The newly created secondary container. */ @NonNull + @GuardedBy("mController.mLock") TaskFragmentContainer createNewSplitWithEmptySideContainer( @NonNull WindowContainerTransaction wct, @NonNull Activity primaryActivity, @NonNull Intent secondaryIntent, @NonNull SplitPairRule rule) { - final Rect parentBounds = getParentContainerBounds(primaryActivity); + final TaskProperties taskProperties = getTaskProperties(primaryActivity); final Pair<Size, Size> minDimensionsPair = getActivityIntentMinDimensionsPair( primaryActivity, secondaryIntent); - final Rect primaryRectBounds = getBoundsForPosition(POSITION_START, parentBounds, rule, - primaryActivity, minDimensionsPair); + final SplitAttributes splitAttributes = computeSplitAttributes(taskProperties, rule, + minDimensionsPair); + final Rect primaryRectBounds = getBoundsForPosition(POSITION_START, taskProperties, + splitAttributes); final TaskFragmentContainer primaryContainer = prepareContainerForActivity(wct, primaryActivity, primaryRectBounds, null); @@ -162,8 +179,8 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { final int taskId = primaryContainer.getTaskId(); final TaskFragmentContainer secondaryContainer = mController.newContainer( secondaryIntent, primaryActivity, taskId); - final Rect secondaryRectBounds = getBoundsForPosition(POSITION_END, parentBounds, - rule, primaryActivity, minDimensionsPair); + final Rect secondaryRectBounds = getBoundsForPosition(POSITION_END, taskProperties, + splitAttributes); final int windowingMode = mController.getTaskContainer(taskId) .getWindowingModeForSplitTaskFragment(secondaryRectBounds); createTaskFragment(wct, secondaryContainer.getTaskFragmentToken(), @@ -172,9 +189,10 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { // Set adjacent to each other so that the containers below will be invisible. setAdjacentTaskFragments(wct, primaryContainer, secondaryContainer, rule, - minDimensionsPair); + splitAttributes); - mController.registerSplit(wct, primaryContainer, primaryActivity, secondaryContainer, rule); + mController.registerSplit(wct, primaryContainer, primaryActivity, secondaryContainer, rule, + splitAttributes); return secondaryContainer; } @@ -190,25 +208,29 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { * created and the activity will be re-parented to it. * @param rule The split rule to be applied to the container. */ - void createNewSplitContainer(@NonNull Activity primaryActivity, - @NonNull Activity secondaryActivity, @NonNull SplitPairRule rule) { - final WindowContainerTransaction wct = new WindowContainerTransaction(); - - final Rect parentBounds = getParentContainerBounds(primaryActivity); + @GuardedBy("mController.mLock") + void createNewSplitContainer(@NonNull WindowContainerTransaction wct, + @NonNull Activity primaryActivity, @NonNull Activity secondaryActivity, + @NonNull SplitPairRule rule) { + final TaskProperties taskProperties = getTaskProperties(primaryActivity); final Pair<Size, Size> minDimensionsPair = getActivitiesMinDimensionsPair(primaryActivity, secondaryActivity); - final Rect primaryRectBounds = getBoundsForPosition(POSITION_START, parentBounds, rule, - primaryActivity, minDimensionsPair); + final SplitAttributes splitAttributes = computeSplitAttributes(taskProperties, rule, + minDimensionsPair); + final Rect primaryRectBounds = getBoundsForPosition(POSITION_START, taskProperties, + splitAttributes); final TaskFragmentContainer primaryContainer = prepareContainerForActivity(wct, primaryActivity, primaryRectBounds, null); - final Rect secondaryRectBounds = getBoundsForPosition(POSITION_END, parentBounds, rule, - primaryActivity, minDimensionsPair); + final Rect secondaryRectBounds = getBoundsForPosition(POSITION_END, taskProperties, + splitAttributes); final TaskFragmentContainer curSecondaryContainer = mController.getContainerWithActivity( secondaryActivity); TaskFragmentContainer containerToAvoid = primaryContainer; - if (rule.shouldClearTop() && curSecondaryContainer != null) { - // Do not reuse the current TaskFragment if the rule is to clear top. + if (curSecondaryContainer != null + && (rule.shouldClearTop() || primaryContainer.isAbove(curSecondaryContainer))) { + // Do not reuse the current TaskFragment if the rule is to clear top, or if it is below + // the primary TaskFragment. containerToAvoid = curSecondaryContainer; } final TaskFragmentContainer secondaryContainer = prepareContainerForActivity(wct, @@ -216,11 +238,10 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { // Set adjacent to each other so that the containers below will be invisible. setAdjacentTaskFragments(wct, primaryContainer, secondaryContainer, rule, - minDimensionsPair); - - mController.registerSplit(wct, primaryContainer, primaryActivity, secondaryContainer, rule); + splitAttributes); - applyTransaction(wct); + mController.registerSplit(wct, primaryContainer, primaryActivity, secondaryContainer, rule, + splitAttributes); } /** @@ -262,15 +283,16 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { * @param rule The split rule to be applied to the container. * @param isPlaceholder Whether the launch is a placeholder. */ - void startActivityToSide(@NonNull Activity launchingActivity, @NonNull Intent activityIntent, - @Nullable Bundle activityOptions, @NonNull SplitRule rule, boolean isPlaceholder) { - final Rect parentBounds = getParentContainerBounds(launchingActivity); - final Pair<Size, Size> minDimensionsPair = getActivityIntentMinDimensionsPair( - launchingActivity, activityIntent); - final Rect primaryRectBounds = getBoundsForPosition(POSITION_START, parentBounds, rule, - launchingActivity, minDimensionsPair); - final Rect secondaryRectBounds = getBoundsForPosition(POSITION_END, parentBounds, rule, - launchingActivity, minDimensionsPair); + @GuardedBy("mController.mLock") + void startActivityToSide(@NonNull WindowContainerTransaction wct, + @NonNull Activity launchingActivity, @NonNull Intent activityIntent, + @Nullable Bundle activityOptions, @NonNull SplitRule rule, + @NonNull SplitAttributes splitAttributes, boolean isPlaceholder) { + final TaskProperties taskProperties = getTaskProperties(launchingActivity); + final Rect primaryRectBounds = getBoundsForPosition(POSITION_START, taskProperties, + splitAttributes); + final Rect secondaryRectBounds = getBoundsForPosition(POSITION_END, taskProperties, + splitAttributes); TaskFragmentContainer primaryContainer = mController.getContainerWithActivity( launchingActivity); @@ -284,9 +306,8 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { launchingActivity, taskId); final int windowingMode = mController.getTaskContainer(taskId) .getWindowingModeForSplitTaskFragment(primaryRectBounds); - final WindowContainerTransaction wct = new WindowContainerTransaction(); mController.registerSplit(wct, primaryContainer, launchingActivity, secondaryContainer, - rule); + rule, splitAttributes); startActivityToSide(wct, primaryContainer.getTaskFragmentToken(), primaryRectBounds, launchingActivity, secondaryContainer.getTaskFragmentToken(), secondaryRectBounds, activityIntent, activityOptions, rule, windowingMode); @@ -294,7 +315,6 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { // When placeholder is launched in split, we should keep the focus on the primary. wct.requestFocusOnTaskFragment(primaryContainer.getTaskFragmentToken()); } - applyTransaction(wct); } /** @@ -303,22 +323,24 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { * @param updatedContainer The task fragment that was updated and caused this split update. * @param wct WindowContainerTransaction that this update should be performed with. */ + @GuardedBy("mController.mLock") void updateSplitContainer(@NonNull SplitContainer splitContainer, @NonNull TaskFragmentContainer updatedContainer, @NonNull WindowContainerTransaction wct) { - // Getting the parent bounds using the updated container - it will have the recent value. - final Rect parentBounds = getParentContainerBounds(updatedContainer); + // Getting the parent configuration using the updated container - it will have the recent + // value. final SplitRule rule = splitContainer.getSplitRule(); final TaskFragmentContainer primaryContainer = splitContainer.getPrimaryContainer(); final Activity activity = primaryContainer.getTopNonFinishingActivity(); if (activity == null) { return; } - final Pair<Size, Size> minDimensionsPair = splitContainer.getMinDimensionsPair(); - final Rect primaryRectBounds = getBoundsForPosition(POSITION_START, parentBounds, rule, - activity, minDimensionsPair); - final Rect secondaryRectBounds = getBoundsForPosition(POSITION_END, parentBounds, rule, - activity, minDimensionsPair); + final TaskProperties taskProperties = getTaskProperties(updatedContainer); + final SplitAttributes splitAttributes = splitContainer.getSplitAttributes(); + final Rect primaryRectBounds = getBoundsForPosition(POSITION_START, taskProperties, + splitAttributes); + final Rect secondaryRectBounds = getBoundsForPosition(POSITION_END, taskProperties, + splitAttributes); final TaskFragmentContainer secondaryContainer = splitContainer.getSecondaryContainer(); // Whether the placeholder is becoming side-by-side with the primary from fullscreen. final boolean isPlaceholderBecomingSplit = splitContainer.isPlaceholderContainer() @@ -330,7 +352,7 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { resizeTaskFragmentIfRegistered(wct, primaryContainer, primaryRectBounds); resizeTaskFragmentIfRegistered(wct, secondaryContainer, secondaryRectBounds); setAdjacentTaskFragments(wct, primaryContainer, secondaryContainer, rule, - minDimensionsPair); + splitAttributes); if (isPlaceholderBecomingSplit) { // When placeholder is shown in split, we should keep the focus on the primary. wct.requestFocusOnTaskFragment(primaryContainer.getTaskFragmentToken()); @@ -342,14 +364,14 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { updateTaskFragmentWindowingModeIfRegistered(wct, secondaryContainer, windowingMode); } + @GuardedBy("mController.mLock") private void setAdjacentTaskFragments(@NonNull WindowContainerTransaction wct, @NonNull TaskFragmentContainer primaryContainer, @NonNull TaskFragmentContainer secondaryContainer, @NonNull SplitRule splitRule, - @NonNull Pair<Size, Size> minDimensionsPair) { - final Rect parentBounds = getParentContainerBounds(primaryContainer); + @NonNull SplitAttributes splitAttributes) { // Clear adjacent TaskFragments if the container is shown in fullscreen, or the // secondaryContainer could not be finished. - if (!shouldShowSideBySide(parentBounds, splitRule, minDimensionsPair)) { + if (!shouldShowSplit(splitAttributes)) { setAdjacentTaskFragments(wct, primaryContainer.getTaskFragmentToken(), null /* secondary */, null /* splitRule */); } else { @@ -435,8 +457,9 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { * Expands the split container if the current split bounds are smaller than the Activity or * Intent that is added to the container. * - * @return the {@link ResultCode} based on {@link #shouldShowSideBySide(Rect, SplitRule, Pair)} - * and if {@link android.window.TaskFragmentInfo} has reported to the client side. + * @return the {@link ResultCode} based on + * {@link #shouldShowSplit(SplitAttributes)} and if + * {@link android.window.TaskFragmentInfo} has reported to the client side. */ @ResultCode int expandSplitContainerIfNeeded(@NonNull WindowContainerTransaction wct, @@ -446,7 +469,6 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { throw new IllegalArgumentException("Either secondaryActivity or secondaryIntent must be" + " non-null."); } - final Rect taskBounds = getParentContainerBounds(primaryActivity); final Pair<Size, Size> minDimensionsPair; if (secondaryActivity != null) { minDimensionsPair = getActivitiesMinDimensionsPair(primaryActivity, secondaryActivity); @@ -455,7 +477,12 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { secondaryIntent); } // Expand the splitContainer if minimum dimensions are not satisfied. - if (!shouldShowSideBySide(taskBounds, splitContainer.getSplitRule(), minDimensionsPair)) { + final TaskContainer taskContainer = splitContainer.getTaskContainer(); + final SplitAttributes splitAttributes = sanitizeSplitAttributes( + taskContainer.getTaskProperties(), splitContainer.getSplitAttributes(), + minDimensionsPair); + splitContainer.setSplitAttributes(splitAttributes); + if (!shouldShowSplit(splitAttributes)) { // If the client side hasn't received TaskFragmentInfo yet, we can't change TaskFragment // bounds. Return failure to create a new SplitContainer which fills task bounds. if (splitContainer.getPrimaryContainer().getInfo() == null @@ -469,47 +496,74 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { return RESULT_NOT_EXPANDED; } - static boolean shouldShowSideBySide(@NonNull Rect parentBounds, @NonNull SplitRule rule) { - return shouldShowSideBySide(parentBounds, rule, null /* minimumDimensionPair */); + static boolean shouldShowSplit(@NonNull SplitContainer splitContainer) { + return shouldShowSplit(splitContainer.getSplitAttributes()); } - static boolean shouldShowSideBySide(@NonNull SplitContainer splitContainer) { - final Rect parentBounds = getParentContainerBounds(splitContainer.getPrimaryContainer()); - - return shouldShowSideBySide(parentBounds, splitContainer.getSplitRule(), - splitContainer.getMinDimensionsPair()); + static boolean shouldShowSplit(@NonNull SplitAttributes splitAttributes) { + return !(splitAttributes.getSplitType() instanceof ExpandContainersSplitType); } - static boolean shouldShowSideBySide(@NonNull Rect parentBounds, @NonNull SplitRule rule, - @Nullable Pair<Size, Size> minDimensionsPair) { - // TODO(b/190433398): Supply correct insets. - final WindowMetrics parentMetrics = new WindowMetrics(parentBounds, - new WindowInsets(new Rect())); - // Don't show side by side if bounds is not qualified. - if (!rule.checkParentMetrics(parentMetrics)) { - return false; + @GuardedBy("mController.mLock") + @NonNull + SplitAttributes computeSplitAttributes(@NonNull TaskProperties taskProperties, + @NonNull SplitRule rule, @Nullable Pair<Size, Size> minDimensionsPair) { + final Configuration taskConfiguration = taskProperties.getConfiguration(); + final WindowMetrics taskWindowMetrics = getTaskWindowMetrics(taskConfiguration); + final SplitAttributesCalculator calculator = mController.getSplitAttributesCalculator(); + final SplitAttributes defaultSplitAttributes = rule.getDefaultSplitAttributes(); + final boolean isDefaultMinSizeSatisfied = rule.checkParentMetrics(taskWindowMetrics); + if (calculator == null) { + if (!isDefaultMinSizeSatisfied) { + return EXPAND_CONTAINERS_ATTRIBUTES; + } + return sanitizeSplitAttributes(taskProperties, defaultSplitAttributes, + minDimensionsPair); } - final float splitRatio = rule.getSplitRatio(); - // We only care the size of the bounds regardless of its position. - final Rect primaryBounds = getPrimaryBounds(parentBounds, splitRatio, true /* isLtr */); - final Rect secondaryBounds = getSecondaryBounds(parentBounds, splitRatio, true /* isLtr */); + final WindowLayoutInfo windowLayoutInfo = mController.mWindowLayoutComponent + .getCurrentWindowLayoutInfo(taskProperties.getDisplayId(), + taskConfiguration.windowConfiguration); + final SplitAttributesCalculatorParams params = new SplitAttributesCalculatorParams( + taskWindowMetrics, taskConfiguration, defaultSplitAttributes, + isDefaultMinSizeSatisfied, windowLayoutInfo, rule.getTag()); + final SplitAttributes splitAttributes = calculator.computeSplitAttributesForParams(params); + return sanitizeSplitAttributes(taskProperties, splitAttributes, minDimensionsPair); + } + /** + * Returns {@link #EXPAND_CONTAINERS_ATTRIBUTES} if the passed {@link SplitAttributes} doesn't + * meet the minimum dimensions set in {@link ActivityInfo.WindowLayout}. Otherwise, returns + * the passed {@link SplitAttributes}. + */ + @NonNull + private SplitAttributes sanitizeSplitAttributes(@NonNull TaskProperties taskProperties, + @NonNull SplitAttributes splitAttributes, + @Nullable Pair<Size, Size> minDimensionsPair) { if (minDimensionsPair == null) { - return true; + return splitAttributes; } - return !boundsSmallerThanMinDimensions(primaryBounds, minDimensionsPair.first) - && !boundsSmallerThanMinDimensions(secondaryBounds, minDimensionsPair.second); + final FoldingFeature foldingFeature = getFoldingFeature(taskProperties); + final Configuration taskConfiguration = taskProperties.getConfiguration(); + final Rect primaryBounds = getPrimaryBounds(taskConfiguration, splitAttributes, + foldingFeature); + final Rect secondaryBounds = getSecondaryBounds(taskConfiguration, splitAttributes, + foldingFeature); + if (boundsSmallerThanMinDimensions(primaryBounds, minDimensionsPair.first) + || boundsSmallerThanMinDimensions(secondaryBounds, minDimensionsPair.second)) { + return EXPAND_CONTAINERS_ATTRIBUTES; + } + return splitAttributes; } @NonNull - static Pair<Size, Size> getActivitiesMinDimensionsPair(Activity primaryActivity, - Activity secondaryActivity) { + static Pair<Size, Size> getActivitiesMinDimensionsPair(@NonNull Activity primaryActivity, + @NonNull Activity secondaryActivity) { return new Pair<>(getMinDimensions(primaryActivity), getMinDimensions(secondaryActivity)); } @NonNull - static Pair<Size, Size> getActivityIntentMinDimensionsPair(Activity primaryActivity, - Intent secondaryIntent) { + static Pair<Size, Size> getActivityIntentMinDimensionsPair(@NonNull Activity primaryActivity, + @NonNull Intent secondaryIntent) { return new Pair<>(getMinDimensions(primaryActivity), getMinDimensions(secondaryIntent)); } @@ -560,20 +614,25 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { @VisibleForTesting @NonNull - static Rect getBoundsForPosition(@Position int position, @NonNull Rect parentBounds, - @NonNull SplitRule rule, @NonNull Activity primaryActivity, - @Nullable Pair<Size, Size> minDimensionsPair) { - if (!shouldShowSideBySide(parentBounds, rule, minDimensionsPair)) { + Rect getBoundsForPosition(@Position int position, @NonNull TaskProperties taskProperties, + @NonNull SplitAttributes splitAttributes) { + final Configuration taskConfiguration = taskProperties.getConfiguration(); + final FoldingFeature foldingFeature = getFoldingFeature(taskProperties); + final SplitType splitType = computeSplitType(splitAttributes, taskConfiguration, + foldingFeature); + final SplitAttributes computedSplitAttributes = new SplitAttributes.Builder() + .setSplitType(splitType) + .setLayoutDirection(splitAttributes.getLayoutDirection()) + .build(); + if (!shouldShowSplit(computedSplitAttributes)) { return new Rect(); } - final boolean isLtr = isLtr(primaryActivity, rule); - final float splitRatio = rule.getSplitRatio(); - switch (position) { case POSITION_START: - return getPrimaryBounds(parentBounds, splitRatio, isLtr); + return getPrimaryBounds(taskConfiguration, computedSplitAttributes, foldingFeature); case POSITION_END: - return getSecondaryBounds(parentBounds, splitRatio, isLtr); + return getSecondaryBounds(taskConfiguration, computedSplitAttributes, + foldingFeature); case POSITION_FILL: default: return new Rect(); @@ -581,74 +640,303 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { } @NonNull - private static Rect getPrimaryBounds(@NonNull Rect parentBounds, float splitRatio, - boolean isLtr) { - return isLtr ? getLeftContainerBounds(parentBounds, splitRatio) - : getRightContainerBounds(parentBounds, 1 - splitRatio); + private Rect getPrimaryBounds(@NonNull Configuration taskConfiguration, + @NonNull SplitAttributes splitAttributes, @Nullable FoldingFeature foldingFeature) { + if (!shouldShowSplit(splitAttributes)) { + return new Rect(); + } + switch (splitAttributes.getLayoutDirection()) { + case SplitAttributes.LayoutDirection.LEFT_TO_RIGHT: { + return getLeftContainerBounds(taskConfiguration, splitAttributes, foldingFeature); + } + case SplitAttributes.LayoutDirection.RIGHT_TO_LEFT: { + return getRightContainerBounds(taskConfiguration, splitAttributes, foldingFeature); + } + case SplitAttributes.LayoutDirection.LOCALE: { + final boolean isLtr = taskConfiguration.getLayoutDirection() + == View.LAYOUT_DIRECTION_LTR; + return isLtr + ? getLeftContainerBounds(taskConfiguration, splitAttributes, foldingFeature) + : getRightContainerBounds(taskConfiguration, splitAttributes, + foldingFeature); + } + case SplitAttributes.LayoutDirection.TOP_TO_BOTTOM: { + return getTopContainerBounds(taskConfiguration, splitAttributes, foldingFeature); + } + case SplitAttributes.LayoutDirection.BOTTOM_TO_TOP: { + return getBottomContainerBounds(taskConfiguration, splitAttributes, foldingFeature); + } + default: + throw new IllegalArgumentException("Unknown layout direction:" + + splitAttributes.getLayoutDirection()); + } + } + + @NonNull + private Rect getSecondaryBounds(@NonNull Configuration taskConfiguration, + @NonNull SplitAttributes splitAttributes, @Nullable FoldingFeature foldingFeature) { + if (!shouldShowSplit(splitAttributes)) { + return new Rect(); + } + switch (splitAttributes.getLayoutDirection()) { + case SplitAttributes.LayoutDirection.LEFT_TO_RIGHT: { + return getRightContainerBounds(taskConfiguration, splitAttributes, foldingFeature); + } + case SplitAttributes.LayoutDirection.RIGHT_TO_LEFT: { + return getLeftContainerBounds(taskConfiguration, splitAttributes, foldingFeature); + } + case SplitAttributes.LayoutDirection.LOCALE: { + final boolean isLtr = taskConfiguration.getLayoutDirection() + == View.LAYOUT_DIRECTION_LTR; + return isLtr + ? getRightContainerBounds(taskConfiguration, splitAttributes, + foldingFeature) + : getLeftContainerBounds(taskConfiguration, splitAttributes, + foldingFeature); + } + case SplitAttributes.LayoutDirection.TOP_TO_BOTTOM: { + return getBottomContainerBounds(taskConfiguration, splitAttributes, foldingFeature); + } + case SplitAttributes.LayoutDirection.BOTTOM_TO_TOP: { + return getTopContainerBounds(taskConfiguration, splitAttributes, foldingFeature); + } + default: + throw new IllegalArgumentException("Unknown layout direction:" + + splitAttributes.getLayoutDirection()); + } + } + + @NonNull + private Rect getLeftContainerBounds(@NonNull Configuration taskConfiguration, + @NonNull SplitAttributes splitAttributes, @Nullable FoldingFeature foldingFeature) { + final int right = computeBoundaryBetweenContainers(taskConfiguration, splitAttributes, + CONTAINER_POSITION_LEFT, foldingFeature); + final Rect taskBounds = taskConfiguration.windowConfiguration.getBounds(); + return new Rect(taskBounds.left, taskBounds.top, right, taskBounds.bottom); } @NonNull - private static Rect getSecondaryBounds(@NonNull Rect parentBounds, float splitRatio, - boolean isLtr) { - return isLtr ? getRightContainerBounds(parentBounds, splitRatio) - : getLeftContainerBounds(parentBounds, 1 - splitRatio); + private Rect getRightContainerBounds(@NonNull Configuration taskConfiguration, + @NonNull SplitAttributes splitAttributes, @Nullable FoldingFeature foldingFeature) { + final int left = computeBoundaryBetweenContainers(taskConfiguration, splitAttributes, + CONTAINER_POSITION_RIGHT, foldingFeature); + final Rect parentBounds = taskConfiguration.windowConfiguration.getBounds(); + return new Rect(left, parentBounds.top, parentBounds.right, parentBounds.bottom); } - private static Rect getLeftContainerBounds(@NonNull Rect parentBounds, float splitRatio) { - return new Rect( - parentBounds.left, - parentBounds.top, - (int) (parentBounds.left + parentBounds.width() * splitRatio), - parentBounds.bottom); + @NonNull + private Rect getTopContainerBounds(@NonNull Configuration taskConfiguration, + @NonNull SplitAttributes splitAttributes, @Nullable FoldingFeature foldingFeature) { + final int bottom = computeBoundaryBetweenContainers(taskConfiguration, splitAttributes, + CONTAINER_POSITION_TOP, foldingFeature); + final Rect parentBounds = taskConfiguration.windowConfiguration.getBounds(); + return new Rect(parentBounds.left, parentBounds.top, parentBounds.right, bottom); } - private static Rect getRightContainerBounds(@NonNull Rect parentBounds, float splitRatio) { - return new Rect( - (int) (parentBounds.left + parentBounds.width() * splitRatio), - parentBounds.top, - parentBounds.right, - parentBounds.bottom); + @NonNull + private Rect getBottomContainerBounds(@NonNull Configuration taskConfiguration, + @NonNull SplitAttributes splitAttributes, @Nullable FoldingFeature foldingFeature) { + final int top = computeBoundaryBetweenContainers(taskConfiguration, splitAttributes, + CONTAINER_POSITION_BOTTOM, foldingFeature); + final Rect parentBounds = taskConfiguration.windowConfiguration.getBounds(); + return new Rect(parentBounds.left, top, parentBounds.right, parentBounds.bottom); } /** - * Checks if a split with the provided rule should be displays in left-to-right layout - * direction, either always or with the current configuration. + * Computes the boundary position between the primary and the secondary containers for the given + * {@link ContainerPosition} with {@link SplitAttributes}, current window and device states. + * <ol> + * <li>For {@link #CONTAINER_POSITION_TOP}, it computes the boundary with the bottom + * container, which is {@link Rect#bottom} of the top container bounds.</li> + * <li>For {@link #CONTAINER_POSITION_BOTTOM}, it computes the boundary with the top + * container, which is {@link Rect#top} of the bottom container bounds.</li> + * <li>For {@link #CONTAINER_POSITION_LEFT}, it computes the boundary with the right + * container, which is {@link Rect#right} of the left container bounds.</li> + * <li>For {@link #CONTAINER_POSITION_RIGHT}, it computes the boundary with the bottom + * container, which is {@link Rect#left} of the right container bounds.</li> + * </ol> + * + * @see #getTopContainerBounds(Configuration, SplitAttributes, FoldingFeature) + * @see #getBottomContainerBounds(Configuration, SplitAttributes, FoldingFeature) + * @see #getLeftContainerBounds(Configuration, SplitAttributes, FoldingFeature) + * @see #getRightContainerBounds(Configuration, SplitAttributes, FoldingFeature) */ - private static boolean isLtr(@NonNull Context context, @NonNull SplitRule rule) { - switch (rule.getLayoutDirection()) { - case LayoutDirection.LOCALE: - return context.getResources().getConfiguration().getLayoutDirection() - == View.LAYOUT_DIRECTION_LTR; - case LayoutDirection.RTL: - return false; - case LayoutDirection.LTR: + private int computeBoundaryBetweenContainers(@NonNull Configuration taskConfiguration, + @NonNull SplitAttributes splitAttributes, @ContainerPosition int position, + @Nullable FoldingFeature foldingFeature) { + final Rect parentBounds = taskConfiguration.windowConfiguration.getBounds(); + final int startPoint = shouldSplitHorizontally(splitAttributes) + ? parentBounds.top + : parentBounds.left; + final int dimen = shouldSplitHorizontally(splitAttributes) + ? parentBounds.height() + : parentBounds.width(); + final SplitType splitType = splitAttributes.getSplitType(); + if (splitType instanceof RatioSplitType) { + final RatioSplitType splitRatio = (RatioSplitType) splitType; + return (int) (startPoint + dimen * splitRatio.getRatio()); + } + // At this point, SplitType must be a HingeSplitType and foldingFeature must be + // non-null. RatioSplitType and ExpandContainerSplitType have been handled earlier. + Objects.requireNonNull(foldingFeature); + if (!(splitType instanceof HingeSplitType)) { + throw new IllegalArgumentException("Unknown splitType:" + splitType); + } + final Rect hingeArea = foldingFeature.getBounds(); + switch (position) { + case CONTAINER_POSITION_LEFT: + return hingeArea.left; + case CONTAINER_POSITION_TOP: + return hingeArea.top; + case CONTAINER_POSITION_RIGHT: + return hingeArea.right; + case CONTAINER_POSITION_BOTTOM: + return hingeArea.bottom; default: + throw new IllegalArgumentException("Unknown position:" + position); + } + } + + @Nullable + private FoldingFeature getFoldingFeature(@NonNull TaskProperties taskProperties) { + final int displayId = taskProperties.getDisplayId(); + final WindowConfiguration windowConfiguration = taskProperties.getConfiguration() + .windowConfiguration; + final WindowLayoutInfo info = mController.mWindowLayoutComponent + .getCurrentWindowLayoutInfo(displayId, windowConfiguration); + final List<DisplayFeature> displayFeatures = info.getDisplayFeatures(); + if (displayFeatures.isEmpty()) { + return null; + } + final List<FoldingFeature> foldingFeatures = new ArrayList<>(); + for (DisplayFeature displayFeature : displayFeatures) { + if (displayFeature instanceof FoldingFeature) { + foldingFeatures.add((FoldingFeature) displayFeature); + } + } + // TODO(b/240219484): Support device with multiple hinges. + if (foldingFeatures.size() != 1) { + return null; + } + return foldingFeatures.get(0); + } + + /** + * Indicates that this {@link SplitAttributes} splits the task horizontally. Returns + * {@code false} if this {@link SplitAttributes} splits the task vertically. + */ + private static boolean shouldSplitHorizontally(SplitAttributes splitAttributes) { + switch (splitAttributes.getLayoutDirection()) { + case SplitAttributes.LayoutDirection.TOP_TO_BOTTOM: + case SplitAttributes.LayoutDirection.BOTTOM_TO_TOP: return true; + default: + return false; + } + } + + /** + * Computes the {@link SplitType} with the {@link SplitAttributes} and the current device and + * window state. + * If passed {@link SplitAttributes#getSplitType} is a {@link RatioSplitType}. It reversed + * the ratio if the computed {@link SplitAttributes#getLayoutDirection} is + * {@link SplitAttributes.LayoutDirection.LEFT_TO_RIGHT} or + * {@link SplitAttributes.LayoutDirection.BOTTOM_TO_TOP} to make the bounds calculation easier. + * If passed {@link SplitAttributes#getSplitType} is a {@link HingeSplitType}, it checks + * the current device and window states to determine whether the split container should split + * by hinge or use {@link HingeSplitType#getFallbackSplitType}. + */ + private SplitType computeSplitType(@NonNull SplitAttributes splitAttributes, + @NonNull Configuration taskConfiguration, @Nullable FoldingFeature foldingFeature) { + final int layoutDirection = splitAttributes.getLayoutDirection(); + final SplitType splitType = splitAttributes.getSplitType(); + if (splitType instanceof ExpandContainersSplitType) { + return splitType; + } else if (splitType instanceof RatioSplitType) { + final RatioSplitType splitRatio = (RatioSplitType) splitType; + // Reverse the ratio for RIGHT_TO_LEFT and BOTTOM_TO_TOP to make the boundary + // computation have the same direction, which is from (top, left) to (bottom, right). + final SplitType reversedSplitType = new RatioSplitType(1 - splitRatio.getRatio()); + switch (layoutDirection) { + case SplitAttributes.LayoutDirection.LEFT_TO_RIGHT: + case SplitAttributes.LayoutDirection.TOP_TO_BOTTOM: + return splitType; + case SplitAttributes.LayoutDirection.RIGHT_TO_LEFT: + case SplitAttributes.LayoutDirection.BOTTOM_TO_TOP: + return reversedSplitType; + case LayoutDirection.LOCALE: { + boolean isLtr = taskConfiguration.getLayoutDirection() + == View.LAYOUT_DIRECTION_LTR; + return isLtr ? splitType : reversedSplitType; + } + } + } else if (splitType instanceof HingeSplitType) { + final HingeSplitType hinge = (HingeSplitType) splitType; + @WindowingMode + final int windowingMode = taskConfiguration.windowConfiguration.getWindowingMode(); + return shouldSplitByHinge(splitAttributes, foldingFeature, windowingMode) + ? hinge : hinge.getFallbackSplitType(); + } + throw new IllegalArgumentException("Unknown SplitType:" + splitType); + } + + private static boolean shouldSplitByHinge(@NonNull SplitAttributes splitAttributes, + @Nullable FoldingFeature foldingFeature, @WindowingMode int taskWindowingMode) { + // Only HingeSplitType may split the task bounds by hinge. + if (!(splitAttributes.getSplitType() instanceof HingeSplitType)) { + return false; + } + // Device is not foldable, so there's no hinge to match. + if (foldingFeature == null) { + return false; + } + // The task is in multi-window mode. Match hinge doesn't make sense because current task + // bounds may not fit display bounds. + if (WindowConfiguration.inMultiWindowMode(taskWindowingMode)) { + return false; } + // Return true if how the split attributes split the task bounds matches the orientation of + // folding area orientation. + return shouldSplitHorizontally(splitAttributes) == isFoldingAreaHorizontal(foldingFeature); + } + + private static boolean isFoldingAreaHorizontal(@NonNull FoldingFeature foldingFeature) { + final Rect bounds = foldingFeature.getBounds(); + return bounds.width() > bounds.height(); } @NonNull - static Rect getParentContainerBounds(@NonNull TaskFragmentContainer container) { - return container.getTaskContainer().getTaskBounds(); + static TaskProperties getTaskProperties(@NonNull TaskFragmentContainer container) { + return container.getTaskContainer().getTaskProperties(); } @NonNull - Rect getParentContainerBounds(@NonNull Activity activity) { - final TaskFragmentContainer container = mController.getContainerWithActivity(activity); - if (container != null) { - return getParentContainerBounds(container); + TaskProperties getTaskProperties(@NonNull Activity activity) { + final TaskContainer taskContainer = mController.getTaskContainer( + mController.getTaskId(activity)); + if (taskContainer != null) { + return taskContainer.getTaskProperties(); } - // Obtain bounds from Activity instead because the Activity hasn't been embedded yet. - return getNonEmbeddedActivityBounds(activity); + // Use a copy of configuration because activity's configuration may be updated later, + // or we may get unexpected TaskContainer's configuration if Activity's configuration is + // updated. An example is Activity is going to be in split. + return new TaskProperties(activity.getDisplayId(), + new Configuration(activity.getResources().getConfiguration())); } - /** - * Obtains the bounds from a non-embedded Activity. - * <p> - * Note that callers should use {@link #getParentContainerBounds(Activity)} instead for most - * cases unless we want to obtain task bounds before - * {@link TaskContainer#isTaskBoundsInitialized()}. - */ + @NonNull + WindowMetrics getTaskWindowMetrics(@NonNull Activity activity) { + return getTaskWindowMetrics(getTaskProperties(activity).getConfiguration()); + } + + @NonNull + private static WindowMetrics getTaskWindowMetrics(@NonNull Configuration taskConfiguration) { + final Rect taskBounds = taskConfiguration.windowConfiguration.getBounds(); + // TODO(b/190433398): Supply correct insets. + return new WindowMetrics(taskBounds, WindowInsets.CONSUMED); + } + + /** Obtains the bounds from a non-embedded Activity. */ @NonNull static Rect getNonEmbeddedActivityBounds(@NonNull Activity activity) { final WindowConfiguration windowConfiguration = 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 0ea5603b1f3d..91573ffef568 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java @@ -21,15 +21,19 @@ 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 android.annotation.NonNull; -import android.annotation.Nullable; import android.app.Activity; import android.app.WindowConfiguration; import android.app.WindowConfiguration.WindowingMode; +import android.content.res.Configuration; import android.graphics.Rect; import android.os.IBinder; import android.util.ArraySet; import android.window.TaskFragmentInfo; +import android.window.TaskFragmentParentInfo; +import android.window.WindowContainerTransaction; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import java.util.ArrayList; import java.util.List; @@ -41,13 +45,10 @@ class TaskContainer { /** The unique task id. */ private final int mTaskId; + // TODO(b/240219484): consolidate to mConfiguration /** Available window bounds of this Task. */ private final Rect mTaskBounds = new Rect(); - /** Windowing mode of this Task. */ - @WindowingMode - private int mWindowingMode = WINDOWING_MODE_UNDEFINED; - /** Active TaskFragments in this Task. */ @NonNull final List<TaskFragmentContainer> mContainers = new ArrayList<>(); @@ -56,24 +57,56 @@ class TaskContainer { @NonNull final List<SplitContainer> mSplitContainers = new ArrayList<>(); + @NonNull + private final Configuration mConfiguration; + + private int mDisplayId; + + private boolean mIsVisible; + /** * TaskFragments that the organizer has requested to be closed. They should be removed when - * the organizer receives {@link SplitController#onTaskFragmentVanished(TaskFragmentInfo)} event - * for them. + * the organizer receives + * {@link SplitController#onTaskFragmentVanished(WindowContainerTransaction, TaskFragmentInfo)} + * event for them. */ final Set<IBinder> mFinishedContainer = new ArraySet<>(); - TaskContainer(int taskId) { + /** + * 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. + * + */ + TaskContainer(int taskId, @NonNull Activity activityInTask) { if (taskId == INVALID_TASK_ID) { throw new IllegalArgumentException("Invalid Task id"); } mTaskId = taskId; + // Make a copy in case the activity's config is updated, and updates the TaskContainer's + // config unexpectedly. + mConfiguration = new Configuration(activityInTask.getResources().getConfiguration()); + mDisplayId = activityInTask.getDisplayId(); + // Note that it is always called when there's a new Activity is started, which implies + // the host task is visible. + mIsVisible = true; } int getTaskId() { return mTaskId; } + int getDisplayId() { + return mDisplayId; + } + + boolean isVisible() { + return mIsVisible; + } + @NonNull Rect getTaskBounds() { return mTaskBounds; @@ -93,13 +126,21 @@ class TaskContainer { return !mTaskBounds.isEmpty(); } - void setWindowingMode(int windowingMode) { - mWindowingMode = windowingMode; + @NonNull + Configuration getConfiguration() { + // Make a copy in case the config is updated unexpectedly. + return new Configuration(mConfiguration); } - /** Whether the Task windowing mode has been initialized. */ - boolean isWindowingModeInitialized() { - return mWindowingMode != WINDOWING_MODE_UNDEFINED; + @NonNull + TaskProperties getTaskProperties() { + return new TaskProperties(mDisplayId, mConfiguration); + } + + void updateTaskFragmentParentInfo(@NonNull TaskFragmentParentInfo info) { + mConfiguration.setTo(info.getConfiguration()); + mDisplayId = info.getDisplayId(); + mIsVisible = info.isVisibleRequested(); } /** @@ -122,13 +163,20 @@ class TaskContainer { // DecorCaptionView won't work correctly. As a result, have the TaskFragment to be in the // Task windowing mode if the Task is in multi window. // TODO we won't need this anymore after we migrate Freeform caption to WM Shell. - return WindowConfiguration.inMultiWindowMode(mWindowingMode) - ? mWindowingMode - : WINDOWING_MODE_MULTI_WINDOW; + return isInMultiWindow() ? getWindowingMode() : WINDOWING_MODE_MULTI_WINDOW; } boolean isInPictureInPicture() { - return mWindowingMode == WINDOWING_MODE_PINNED; + return getWindowingMode() == WINDOWING_MODE_PINNED; + } + + boolean isInMultiWindow() { + return WindowConfiguration.inMultiWindowMode(getWindowingMode()); + } + + @WindowingMode + private int getWindowingMode() { + return getConfiguration().windowConfiguration.getWindowingMode(); } /** Whether there is any {@link TaskFragmentContainer} below this Task. */ @@ -136,6 +184,13 @@ class TaskContainer { return mContainers.isEmpty() && mFinishedContainer.isEmpty(); } + /** Called when the activity is destroyed. */ + void onActivityDestroyed(@NonNull Activity activity) { + for (TaskFragmentContainer container : mContainers) { + container.onActivityDestroyed(activity); + } + } + /** Removes the pending appeared activity from all TaskFragments in this Task. */ void cleanupPendingAppearedActivity(@NonNull Activity pendingAppearedActivity) { for (TaskFragmentContainer container : mContainers) { @@ -161,4 +216,32 @@ class TaskContainer { } return null; } + + int indexOf(@NonNull TaskFragmentContainer child) { + return mContainers.indexOf(child); + } + + /** + * A wrapper class which contains the display ID and {@link Configuration} of a + * {@link TaskContainer} + */ + static final class TaskProperties { + private final int mDisplayId; + @NonNull + private final Configuration mConfiguration; + + TaskProperties(int displayId, @NonNull Configuration configuration) { + mDisplayId = displayId; + mConfiguration = configuration; + } + + int getDisplayId() { + return mDisplayId; + } + + @NonNull + Configuration getConfiguration() { + return mConfiguration; + } + } } diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationAdapter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationAdapter.java index cdee9e386b33..af5d8c561874 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationAdapter.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationAdapter.java @@ -16,7 +16,6 @@ package androidx.window.extensions.embedding; -import static android.graphics.Matrix.MSCALE_X; import static android.graphics.Matrix.MTRANS_X; import static android.graphics.Matrix.MTRANS_Y; @@ -41,30 +40,44 @@ class TaskFragmentAnimationAdapter { */ private static final int LAYER_NO_OVERRIDE = -1; + @NonNull final Animation mAnimation; + @NonNull final RemoteAnimationTarget mTarget; + @NonNull final SurfaceControl mLeash; + /** Area in absolute coordinate that the animation surface shouldn't go beyond. */ + @NonNull + private final Rect mWholeAnimationBounds = new Rect(); + @NonNull final Transformation mTransformation = new Transformation(); + @NonNull final float[] mMatrix = new float[9]; + @NonNull final float[] mVecs = new float[4]; + @NonNull final Rect mRect = new Rect(); private boolean mIsFirstFrame = true; private int mOverrideLayer = LAYER_NO_OVERRIDE; TaskFragmentAnimationAdapter(@NonNull Animation animation, @NonNull RemoteAnimationTarget target) { - this(animation, target, target.leash); + this(animation, target, target.leash, target.screenSpaceBounds); } /** * @param leash the surface to animate. + * @param wholeAnimationBounds area in absolute coordinate that the animation surface shouldn't + * go beyond. */ TaskFragmentAnimationAdapter(@NonNull Animation animation, - @NonNull RemoteAnimationTarget target, @NonNull SurfaceControl leash) { + @NonNull RemoteAnimationTarget target, @NonNull SurfaceControl leash, + @NonNull Rect wholeAnimationBounds) { mAnimation = animation; mTarget = target; mLeash = leash; + mWholeAnimationBounds.set(wholeAnimationBounds); } /** @@ -94,23 +107,32 @@ class TaskFragmentAnimationAdapter { /** To be overridden by subclasses to adjust the animation surface change. */ void onAnimationUpdateInner(@NonNull SurfaceControl.Transaction t) { + // Update the surface position and alpha. mTransformation.getMatrix().postTranslate( mTarget.localBounds.left, mTarget.localBounds.top); t.setMatrix(mLeash, mTransformation.getMatrix(), mMatrix); t.setAlpha(mLeash, mTransformation.getAlpha()); - // Get current animation position. + + // Get current surface bounds in absolute coordinate. + // positionX/Y are in local coordinate, so minus the local offset to get the slide amount. final int positionX = Math.round(mMatrix[MTRANS_X]); final int positionY = Math.round(mMatrix[MTRANS_Y]); - // The exiting surface starts at position: mTarget.localBounds and moves with - // positionX varying. Offset our crop region by the amount we have slided so crop - // regions stays exactly on the original container in split. - final int cropOffsetX = mTarget.localBounds.left - positionX; - final int cropOffsetY = mTarget.localBounds.top - positionY; - final Rect cropRect = new Rect(); - cropRect.set(mTarget.localBounds); - // Because window crop uses absolute position. - cropRect.offsetTo(0, 0); - cropRect.offset(cropOffsetX, cropOffsetY); + final Rect cropRect = new Rect(mTarget.screenSpaceBounds); + final Rect localBounds = mTarget.localBounds; + cropRect.offset(positionX - localBounds.left, positionY - localBounds.top); + + // Store the current offset of the surface top left from (0,0) in absolute coordinate. + final int offsetX = cropRect.left; + final int offsetY = cropRect.top; + + // Intersect to make sure the animation happens within the whole animation bounds. + if (!cropRect.intersect(mWholeAnimationBounds)) { + // Hide the surface when it is outside of the animation area. + t.setAlpha(mLeash, 0); + } + + // cropRect is in absolute coordinate, so we need to translate it to surface top left. + cropRect.offset(-offsetX, -offsetY); t.setCrop(mLeash, cropRect); } @@ -124,52 +146,6 @@ class TaskFragmentAnimationAdapter { } /** - * Should be used when the {@link RemoteAnimationTarget} is in split with others, and want to - * animate together as one. This adapter will offset the animation leash to make the animate of - * two windows look like a single window. - */ - static class SplitAdapter extends TaskFragmentAnimationAdapter { - private final boolean mIsLeftHalf; - private final int mWholeAnimationWidth; - - /** - * @param isLeftHalf whether this is the left half of the animation. - * @param wholeAnimationWidth the whole animation windows width. - */ - SplitAdapter(@NonNull Animation animation, @NonNull RemoteAnimationTarget target, - boolean isLeftHalf, int wholeAnimationWidth) { - super(animation, target); - mIsLeftHalf = isLeftHalf; - mWholeAnimationWidth = wholeAnimationWidth; - if (wholeAnimationWidth == 0) { - throw new IllegalArgumentException("SplitAdapter must provide wholeAnimationWidth"); - } - } - - @Override - void onAnimationUpdateInner(@NonNull SurfaceControl.Transaction t) { - float posX = mTarget.localBounds.left; - final float posY = mTarget.localBounds.top; - // This window is half of the whole animation window. Offset left/right to make it - // look as one with the other half. - mTransformation.getMatrix().getValues(mMatrix); - final int targetWidth = mTarget.localBounds.width(); - final float scaleX = mMatrix[MSCALE_X]; - final float totalOffset = mWholeAnimationWidth * (1 - scaleX) / 2; - final float curOffset = targetWidth * (1 - scaleX) / 2; - final float offsetDiff = totalOffset - curOffset; - if (mIsLeftHalf) { - posX += offsetDiff; - } else { - posX -= offsetDiff; - } - mTransformation.getMatrix().postTranslate(posX, posY); - t.setMatrix(mLeash, mTransformation.getMatrix(), mMatrix); - t.setAlpha(mLeash, mTransformation.getAlpha()); - } - } - - /** * Should be used for the animation of the snapshot of a {@link RemoteAnimationTarget} that has * size change. */ @@ -177,7 +153,7 @@ class TaskFragmentAnimationAdapter { SnapshotAdapter(@NonNull Animation animation, @NonNull RemoteAnimationTarget target) { // Start leash is the snapshot of the starting surface. - super(animation, target, target.startLeash); + super(animation, target, target.startLeash, target.screenSpaceBounds); } @Override diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationController.java index f721341a3647..ee2e139bb0b2 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationController.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationController.java @@ -30,6 +30,8 @@ import android.view.RemoteAnimationAdapter; import android.view.RemoteAnimationDefinition; import android.window.TaskFragmentOrganizer; +import androidx.annotation.NonNull; + import com.android.internal.annotations.VisibleForTesting; /** Controls the TaskFragment remote animations. */ @@ -45,7 +47,7 @@ class TaskFragmentAnimationController { /** Task Ids that we have registered for remote animation. */ private final ArraySet<Integer> mRegisterTasks = new ArraySet<>(); - TaskFragmentAnimationController(TaskFragmentOrganizer organizer) { + TaskFragmentAnimationController(@NonNull TaskFragmentOrganizer organizer) { mOrganizer = organizer; mDefinition = new RemoteAnimationDefinition(); final RemoteAnimationAdapter animationAdapter = diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationRunner.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationRunner.java index c4f37091a491..8c416e881059 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationRunner.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationRunner.java @@ -112,6 +112,7 @@ class TaskFragmentAnimationRunner extends IRemoteAnimationRunner.Stub { } /** Creates the animator given the transition type and windows. */ + @NonNull private Animator createAnimator(@WindowManager.TransitionOldType int transit, @NonNull RemoteAnimationTarget[] targets, @NonNull IRemoteAnimationFinishedCallback finishedCallback) { @@ -161,6 +162,7 @@ class TaskFragmentAnimationRunner extends IRemoteAnimationRunner.Stub { } /** List of {@link TaskFragmentAnimationAdapter} to handle animations on all window targets. */ + @NonNull private List<TaskFragmentAnimationAdapter> createAnimationAdapters( @WindowManager.TransitionOldType int transit, @NonNull RemoteAnimationTarget[] targets) { @@ -180,12 +182,14 @@ class TaskFragmentAnimationRunner extends IRemoteAnimationRunner.Stub { } } + @NonNull private List<TaskFragmentAnimationAdapter> createOpenAnimationAdapters( @NonNull RemoteAnimationTarget[] targets) { return createOpenCloseAnimationAdapters(targets, true /* isOpening */, mAnimationSpec::loadOpenAnimation); } + @NonNull private List<TaskFragmentAnimationAdapter> createCloseAnimationAdapters( @NonNull RemoteAnimationTarget[] targets) { return createOpenCloseAnimationAdapters(targets, false /* isOpening */, @@ -196,6 +200,7 @@ class TaskFragmentAnimationRunner extends IRemoteAnimationRunner.Stub { * Creates {@link TaskFragmentAnimationAdapter} for OPEN and CLOSE types of transition. * @param isOpening {@code true} for OPEN type, {@code false} for CLOSE type. */ + @NonNull private List<TaskFragmentAnimationAdapter> createOpenCloseAnimationAdapters( @NonNull RemoteAnimationTarget[] targets, boolean isOpening, @NonNull BiFunction<RemoteAnimationTarget, Rect, Animation> animationProvider) { @@ -208,10 +213,10 @@ class TaskFragmentAnimationRunner extends IRemoteAnimationRunner.Stub { for (RemoteAnimationTarget target : targets) { if (target.mode != MODE_CLOSING) { openingTargets.add(target); - openingWholeScreenBounds.union(target.localBounds); + openingWholeScreenBounds.union(target.screenSpaceBounds); } else { closingTargets.add(target); - closingWholeScreenBounds.union(target.localBounds); + closingWholeScreenBounds.union(target.screenSpaceBounds); } } @@ -238,27 +243,17 @@ class TaskFragmentAnimationRunner extends IRemoteAnimationRunner.Stub { return adapters; } + @NonNull private TaskFragmentAnimationAdapter createOpenCloseAnimationAdapter( @NonNull RemoteAnimationTarget target, @NonNull BiFunction<RemoteAnimationTarget, Rect, Animation> animationProvider, @NonNull Rect wholeAnimationBounds) { final Animation animation = animationProvider.apply(target, wholeAnimationBounds); - final Rect targetBounds = target.localBounds; - if (targetBounds.left == wholeAnimationBounds.left - && targetBounds.right != wholeAnimationBounds.right) { - // This is the left split of the whole animation window. - return new TaskFragmentAnimationAdapter.SplitAdapter(animation, target, - true /* isLeftHalf */, wholeAnimationBounds.width()); - } else if (targetBounds.left != wholeAnimationBounds.left - && targetBounds.right == wholeAnimationBounds.right) { - // This is the right split of the whole animation window. - return new TaskFragmentAnimationAdapter.SplitAdapter(animation, target, - false /* isLeftHalf */, wholeAnimationBounds.width()); - } - // Open/close window that fills the whole animation. - return new TaskFragmentAnimationAdapter(animation, target); + return new TaskFragmentAnimationAdapter(animation, target, target.leash, + wholeAnimationBounds); } + @NonNull private List<TaskFragmentAnimationAdapter> createChangeAnimationAdapters( @NonNull RemoteAnimationTarget[] targets) { final List<TaskFragmentAnimationAdapter> adapters = new ArrayList<>(); diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationSpec.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationSpec.java index 586ac1f212a1..ef5ea563de12 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationSpec.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationSpec.java @@ -26,6 +26,7 @@ import android.graphics.Rect; import android.os.Handler; import android.provider.Settings; import android.view.RemoteAnimationTarget; +import android.view.WindowManager; import android.view.animation.AlphaAnimation; import android.view.animation.Animation; import android.view.animation.AnimationSet; @@ -68,16 +69,14 @@ class TaskFragmentAnimationSpec { // The transition animation should be adjusted based on the developer option. final ContentResolver resolver = mContext.getContentResolver(); - mTransitionAnimationScaleSetting = Settings.Global.getFloat(resolver, - Settings.Global.TRANSITION_ANIMATION_SCALE, - mContext.getResources().getFloat( - R.dimen.config_appTransitionAnimationDurationScaleDefault)); + mTransitionAnimationScaleSetting = getTransitionAnimationScaleSetting(); resolver.registerContentObserver( Settings.Global.getUriFor(Settings.Global.TRANSITION_ANIMATION_SCALE), false, new SettingsObserver(handler)); } /** For target that doesn't need to be animated. */ + @NonNull static Animation createNoopAnimation(@NonNull RemoteAnimationTarget target) { // Noop but just keep the target showing/hiding. final float alpha = target.mode == MODE_CLOSING ? 0f : 1f; @@ -85,6 +84,7 @@ class TaskFragmentAnimationSpec { } /** Animation for target that is opening in a change transition. */ + @NonNull Animation createChangeBoundsOpenAnimation(@NonNull RemoteAnimationTarget target) { final Rect bounds = target.localBounds; // The target will be animated in from left or right depends on its position. @@ -101,6 +101,7 @@ class TaskFragmentAnimationSpec { } /** Animation for target that is closing in a change transition. */ + @NonNull Animation createChangeBoundsCloseAnimation(@NonNull RemoteAnimationTarget target) { final Rect bounds = target.localBounds; // The target will be animated out to left or right depends on its position. @@ -121,6 +122,7 @@ class TaskFragmentAnimationSpec { * @return the return array always has two elements. The first one is for the start leash, and * the second one is for the end leash. */ + @NonNull Animation[] createChangeBoundsChangeAnimations(@NonNull RemoteAnimationTarget target) { // Both start bounds and end bounds are in screen coordinates. We will post translate // to the local coordinates in TaskFragmentAnimationAdapter#onAnimationUpdate @@ -177,30 +179,60 @@ class TaskFragmentAnimationSpec { return new Animation[]{startSet, endSet}; } + @NonNull Animation loadOpenAnimation(@NonNull RemoteAnimationTarget target, @NonNull Rect wholeAnimationBounds) { final boolean isEnter = target.mode != MODE_CLOSING; - final Animation animation = mTransitionAnimation.loadDefaultAnimationRes(isEnter - ? com.android.internal.R.anim.task_fragment_open_enter - : com.android.internal.R.anim.task_fragment_open_exit); - animation.initialize(target.localBounds.width(), target.localBounds.height(), + final Animation animation; + // Background color on TaskDisplayArea has already been set earlier in + // WindowContainer#getAnimationAdapter. + if (target.showBackdrop) { + animation = mTransitionAnimation.loadDefaultAnimationRes(isEnter + ? com.android.internal.R.anim.task_fragment_clear_top_open_enter + : com.android.internal.R.anim.task_fragment_clear_top_open_exit); + } else { + animation = mTransitionAnimation.loadDefaultAnimationRes(isEnter + ? com.android.internal.R.anim.task_fragment_open_enter + : com.android.internal.R.anim.task_fragment_open_exit); + } + // Use the whole animation bounds instead of the change bounds, so that when multiple change + // targets are opening at the same time, the animation applied to each will be the same. + // Otherwise, we may see gap between the activities that are launching together. + animation.initialize(wholeAnimationBounds.width(), wholeAnimationBounds.height(), wholeAnimationBounds.width(), wholeAnimationBounds.height()); animation.scaleCurrentDuration(mTransitionAnimationScaleSetting); return animation; } + @NonNull Animation loadCloseAnimation(@NonNull RemoteAnimationTarget target, @NonNull Rect wholeAnimationBounds) { final boolean isEnter = target.mode != MODE_CLOSING; - final Animation animation = mTransitionAnimation.loadDefaultAnimationRes(isEnter - ? com.android.internal.R.anim.task_fragment_close_enter - : com.android.internal.R.anim.task_fragment_close_exit); - animation.initialize(target.localBounds.width(), target.localBounds.height(), + final Animation animation; + if (target.showBackdrop) { + animation = mTransitionAnimation.loadDefaultAnimationRes(isEnter + ? com.android.internal.R.anim.task_fragment_clear_top_close_enter + : com.android.internal.R.anim.task_fragment_clear_top_close_exit); + } else { + animation = mTransitionAnimation.loadDefaultAnimationRes(isEnter + ? com.android.internal.R.anim.task_fragment_close_enter + : com.android.internal.R.anim.task_fragment_close_exit); + } + // Use the whole animation bounds instead of the change bounds, so that when multiple change + // targets are closing at the same time, the animation applied to each will be the same. + // Otherwise, we may see gap between the activities that are finishing together. + animation.initialize(wholeAnimationBounds.width(), wholeAnimationBounds.height(), wholeAnimationBounds.width(), wholeAnimationBounds.height()); animation.scaleCurrentDuration(mTransitionAnimationScaleSetting); return animation; } + private float getTransitionAnimationScaleSetting() { + return WindowManager.fixScale(Settings.Global.getFloat(mContext.getContentResolver(), + Settings.Global.TRANSITION_ANIMATION_SCALE, mContext.getResources().getFloat( + R.dimen.config_appTransitionAnimationDurationScaleDefault))); + } + private class SettingsObserver extends ContentObserver { SettingsObserver(@NonNull Handler handler) { super(handler); @@ -208,9 +240,7 @@ class TaskFragmentAnimationSpec { @Override public void onChange(boolean selfChange) { - mTransitionAnimationScaleSetting = Settings.Global.getFloat( - mContext.getContentResolver(), Settings.Global.TRANSITION_ANIMATION_SCALE, - mTransitionAnimationScaleSetting); + mTransitionAnimationScaleSetting = getTransitionAnimationScaleSetting(); } } } 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 abf32a26efa2..18712aed1be6 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java @@ -18,9 +18,8 @@ package androidx.window.extensions.embedding; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; -import android.annotation.NonNull; -import android.annotation.Nullable; import android.app.Activity; +import android.app.ActivityThread; import android.app.WindowConfiguration.WindowingMode; import android.content.Intent; import android.graphics.Rect; @@ -30,6 +29,10 @@ import android.util.Size; import android.window.TaskFragmentInfo; import android.window.WindowContainerTransaction; +import androidx.annotation.GuardedBy; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + import com.android.internal.annotations.VisibleForTesting; import java.util.ArrayList; @@ -175,6 +178,7 @@ class TaskFragmentContainer { && mInfo.getActivities().size() == collectNonFinishingActivities().size(); } + @NonNull ActivityStack toActivityStack() { return new ActivityStack(collectNonFinishingActivities(), isEmpty()); } @@ -187,17 +191,72 @@ class TaskFragmentContainer { // Remove the pending activity from other TaskFragments. mTaskContainer.cleanupPendingAppearedActivity(pendingAppearedActivity); mPendingAppearedActivities.add(pendingAppearedActivity); + updateActivityClientRecordTaskFragmentToken(pendingAppearedActivity); + } + + /** + * Updates the {@link ActivityThread.ActivityClientRecord#mTaskFragmentToken} for the + * activity. This makes sure the token is up-to-date if the activity is relaunched later. + */ + private void updateActivityClientRecordTaskFragmentToken(@NonNull Activity activity) { + final ActivityThread.ActivityClientRecord record = ActivityThread + .currentActivityThread().getActivityClient(activity.getActivityToken()); + if (record != null) { + record.mTaskFragmentToken = mToken; + } } void removePendingAppearedActivity(@NonNull Activity pendingAppearedActivity) { mPendingAppearedActivities.remove(pendingAppearedActivity); } + void clearPendingAppearedActivities() { + final List<Activity> cleanupActivities = new ArrayList<>(mPendingAppearedActivities); + // Clear mPendingAppearedActivities so that #getContainerWithActivity won't return the + // current TaskFragment. + mPendingAppearedActivities.clear(); + mPendingAppearedIntent = null; + + // For removed pending activities, we need to update the them to their previous containers. + for (Activity activity : cleanupActivities) { + final TaskFragmentContainer curContainer = mController.getContainerWithActivity( + activity); + if (curContainer != null) { + curContainer.updateActivityClientRecordTaskFragmentToken(activity); + } + } + } + + /** Called when the activity is destroyed. */ + void onActivityDestroyed(@NonNull Activity activity) { + removePendingAppearedActivity(activity); + if (mInfo != null) { + // Remove the activity now because there can be a delay before the server callback. + mInfo.getActivities().remove(activity.getActivityToken()); + } + } + @Nullable Intent getPendingAppearedIntent() { return mPendingAppearedIntent; } + void setPendingAppearedIntent(@Nullable Intent intent) { + mPendingAppearedIntent = intent; + } + + /** + * Clears the pending appeared Intent if it is the same as given Intent. Otherwise, the + * pending appeared Intent is cleared when TaskFragmentInfo is set and is not empty (has + * running activities). + */ + void clearPendingAppearedIntentIfNeeded(@NonNull Intent intent) { + if (mPendingAppearedIntent == null || mPendingAppearedIntent != intent) { + return; + } + mPendingAppearedIntent = null; + } + boolean hasActivity(@NonNull IBinder token) { if (mInfo != null && mInfo.getActivities().contains(token)) { return true; @@ -228,15 +287,26 @@ class TaskFragmentContainer { return mInfo; } - void setInfo(@NonNull TaskFragmentInfo info) { + @GuardedBy("mController.mLock") + void setInfo(@NonNull WindowContainerTransaction wct, @NonNull TaskFragmentInfo info) { if (!mIsFinished && mInfo == null && info.isEmpty()) { - // onTaskFragmentAppeared with empty info. We will remove the TaskFragment if it is - // still empty after timeout. - mAppearEmptyTimeout = () -> { + // onTaskFragmentAppeared with empty info. We will remove the TaskFragment if no + // pending appeared intent/activities. Otherwise, wait and removing the TaskFragment if + // it is still empty after timeout. + if (mPendingAppearedIntent != null || !mPendingAppearedActivities.isEmpty()) { + mAppearEmptyTimeout = () -> { + synchronized (mController.mLock) { + mAppearEmptyTimeout = null; + // Call without the pass-in wct when timeout. We need to applyWct directly + // in this case. + mController.onTaskFragmentAppearEmptyTimeout(this); + } + }; + mController.getHandler().postDelayed(mAppearEmptyTimeout, APPEAR_EMPTY_TIMEOUT_MS); + } else { mAppearEmptyTimeout = null; - mController.onTaskFragmentAppearEmptyTimeout(this); - }; - mController.getHandler().postDelayed(mAppearEmptyTimeout, APPEAR_EMPTY_TIMEOUT_MS); + mController.onTaskFragmentAppearEmptyTimeout(wct, this); + } } else if (mAppearEmptyTimeout != null && !info.isEmpty()) { mController.getHandler().removeCallbacks(mAppearEmptyTimeout); mAppearEmptyTimeout = null; @@ -362,7 +432,7 @@ class TaskFragmentContainer { // In case we have requested to reparent the activity to another container (as // pendingAppeared), we don't want to finish it with this container. && mController.getContainerWithActivity(activity) == this) { - activity.finish(); + wct.finishActivity(activity.getActivityToken()); } } @@ -387,7 +457,7 @@ class TaskFragmentContainer { || controller.shouldRetainAssociatedActivity(this, activity)) { continue; } - activity.finish(); + wct.finishActivity(activity.getActivityToken()); } mActivitiesToFinishOnExit.clear(); } @@ -470,6 +540,18 @@ class TaskFragmentContainer { return new Size(maxMinWidth, maxMinHeight); } + /** Whether the current TaskFragment is above the {@code other} TaskFragment. */ + boolean isAbove(@NonNull TaskFragmentContainer other) { + if (mTaskContainer != other.mTaskContainer) { + throw new IllegalArgumentException( + "Trying to compare two TaskFragments in different Task."); + } + if (this == other) { + throw new IllegalArgumentException("Trying to compare a TaskFragment with itself."); + } + return mTaskContainer.indexOf(this) > mTaskContainer.indexOf(other); + } + @Override public String toString() { return toString(true /* includeContainersToFinishOnExit */); 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 c1d1c8e8d4e0..c76f568e117f 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java @@ -20,23 +20,26 @@ import static android.view.Display.DEFAULT_DISPLAY; import static androidx.window.common.CommonFoldingFeature.COMMON_STATE_FLAT; import static androidx.window.common.CommonFoldingFeature.COMMON_STATE_HALF_OPENED; +import static androidx.window.util.ExtensionHelper.isZero; import static androidx.window.util.ExtensionHelper.rotateRectToDisplayRotation; import static androidx.window.util.ExtensionHelper.transformToWindowSpaceRect; -import android.annotation.Nullable; import android.app.Activity; -import android.app.ActivityManager; -import android.app.ActivityManager.AppTask; +import android.app.ActivityClient; import android.app.Application; import android.app.WindowConfiguration; +import android.content.ComponentCallbacks; import android.content.Context; +import android.content.res.Configuration; import android.graphics.Rect; import android.os.Bundle; import android.os.IBinder; import android.util.ArrayMap; -import android.util.Log; +import android.window.WindowContext; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.UiContext; import androidx.window.common.CommonFoldingFeature; import androidx.window.common.DeviceStateManagerFoldingFeatureProducer; import androidx.window.common.EmptyLifecycleCallbacksAdapter; @@ -44,9 +47,9 @@ import androidx.window.common.RawFoldingFeatureProducer; import androidx.window.util.DataProducer; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.Set; import java.util.function.Consumer; @@ -61,12 +64,17 @@ import java.util.function.Consumer; public class WindowLayoutComponentImpl implements WindowLayoutComponent { private static final String TAG = "SampleExtension"; - private final Map<Activity, Consumer<WindowLayoutInfo>> mWindowLayoutChangeListeners = + private final Map<Context, Consumer<WindowLayoutInfo>> mWindowLayoutChangeListeners = new ArrayMap<>(); private final DataProducer<List<CommonFoldingFeature>> mFoldingFeatureProducer; - public WindowLayoutComponentImpl(Context context) { + private final List<CommonFoldingFeature> mLastReportedFoldingFeatures = new ArrayList<>(); + + private final Map<IBinder, WindowContextConfigListener> mWindowContextConfigListeners = + new ArrayMap<>(); + + public WindowLayoutComponentImpl(@NonNull Context context) { ((Application) context.getApplicationContext()) .registerActivityLifecycleCallbacks(new NotifyOnConfigurationChanged()); RawFoldingFeatureProducer foldingFeatureProducer = new RawFoldingFeatureProducer(context); @@ -75,46 +83,84 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent { mFoldingFeatureProducer.addDataChangedCallback(this::onDisplayFeaturesChanged); } + /** Registers to listen to {@link CommonFoldingFeature} changes */ + public void addFoldingStateChangedCallback(Consumer<List<CommonFoldingFeature>> consumer) { + mFoldingFeatureProducer.addDataChangedCallback(consumer); + } + /** * Adds a listener interested in receiving updates to {@link WindowLayoutInfo} * * @param activity hosting a {@link android.view.Window} * @param consumer interested in receiving updates to {@link WindowLayoutInfo} */ + @Override public void addWindowLayoutInfoListener(@NonNull Activity activity, @NonNull Consumer<WindowLayoutInfo> consumer) { - mWindowLayoutChangeListeners.put(activity, consumer); - onDisplayFeaturesChanged(); + addWindowLayoutInfoListener((Context) activity, consumer); } /** - * Removes a listener no longer interested in receiving updates. - * - * @param consumer no longer interested in receiving updates to {@link WindowLayoutInfo} + * Similar to {@link #addWindowLayoutInfoListener(Activity, Consumer)}, but takes a UI Context + * as a parameter. */ - public void removeWindowLayoutInfoListener( + // TODO(b/204073440): Add @Override to hook the API in WM extensions library. + public void addWindowLayoutInfoListener(@NonNull @UiContext Context context, @NonNull Consumer<WindowLayoutInfo> consumer) { - mWindowLayoutChangeListeners.values().remove(consumer); - onDisplayFeaturesChanged(); + if (mWindowLayoutChangeListeners.containsKey(context) + || mWindowLayoutChangeListeners.containsValue(consumer)) { + // Early return if the listener or consumer has been registered. + return; + } + if (!context.isUiContext()) { + throw new IllegalArgumentException("Context must be a UI Context, which should be" + + " an Activity or a WindowContext"); + } + mFoldingFeatureProducer.getData((features) -> { + // Get the WindowLayoutInfo from the activity and pass the value to the layoutConsumer. + WindowLayoutInfo newWindowLayout = getWindowLayoutInfo(context, features); + consumer.accept(newWindowLayout); + }); + mWindowLayoutChangeListeners.put(context, consumer); + + if (context instanceof WindowContext) { + final IBinder windowContextToken = context.getWindowContextToken(); + final WindowContextConfigListener listener = + new WindowContextConfigListener(windowContextToken); + context.registerComponentCallbacks(listener); + mWindowContextConfigListeners.put(windowContextToken, listener); + } } - void updateWindowLayout(@NonNull Activity activity, - @NonNull WindowLayoutInfo newLayout) { - Consumer<WindowLayoutInfo> consumer = mWindowLayoutChangeListeners.get(activity); - if (consumer != null) { - consumer.accept(newLayout); + /** + * Removes a listener no longer interested in receiving updates. + * + * @param consumer no longer interested in receiving updates to {@link WindowLayoutInfo} + */ + @Override + public void removeWindowLayoutInfoListener(@NonNull Consumer<WindowLayoutInfo> consumer) { + for (Context context : mWindowLayoutChangeListeners.keySet()) { + if (!mWindowLayoutChangeListeners.get(context).equals(consumer)) { + continue; + } + if (context instanceof WindowContext) { + final IBinder token = context.getWindowContextToken(); + context.unregisterComponentCallbacks(mWindowContextConfigListeners.get(token)); + mWindowContextConfigListeners.remove(token); + } + break; } + mWindowLayoutChangeListeners.values().remove(consumer); } @NonNull - Set<Activity> getActivitiesListeningForLayoutChanges() { + Set<Context> getContextsListeningForLayoutChanges() { return mWindowLayoutChangeListeners.keySet(); } - @NonNull private boolean isListeningForLayoutChanges(IBinder token) { - for (Activity activity: getActivitiesListeningForLayoutChanges()) { - if (token.equals(activity.getWindow().getAttributes().token)) { + for (Context context: getContextsListeningForLayoutChanges()) { + if (token.equals(Context.getToken(context))) { return true; } } @@ -128,12 +174,12 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent { /** * A convenience method to translate from the common feature state to the extensions feature * state. More specifically, translates from {@link CommonFoldingFeature.State} to - * {@link FoldingFeature.STATE_FLAT} or {@link FoldingFeature.STATE_HALF_OPENED}. If it is not + * {@link FoldingFeature#STATE_FLAT} or {@link FoldingFeature#STATE_HALF_OPENED}. If it is not * possible to translate, then we will return a {@code null} value. * * @param state if it matches a value in {@link CommonFoldingFeature.State}, {@code null} - * otherwise. @return a {@link FoldingFeature.STATE_FLAT} or - * {@link FoldingFeature.STATE_HALF_OPENED} if the given state matches a value in + * otherwise. @return a {@link FoldingFeature#STATE_FLAT} or + * {@link FoldingFeature#STATE_HALF_OPENED} if the given state matches a value in * {@link CommonFoldingFeature.State} and {@code null} otherwise. */ @Nullable @@ -147,17 +193,48 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent { } } - private void onDisplayFeaturesChanged() { - for (Activity activity : getActivitiesListeningForLayoutChanges()) { - WindowLayoutInfo newLayout = getWindowLayoutInfo(activity); - updateWindowLayout(activity, newLayout); + private void onDisplayFeaturesChanged(List<CommonFoldingFeature> storedFeatures) { + mLastReportedFoldingFeatures.clear(); + mLastReportedFoldingFeatures.addAll(storedFeatures); + for (Context context : getContextsListeningForLayoutChanges()) { + // Get the WindowLayoutInfo from the activity and pass the value to the layoutConsumer. + Consumer<WindowLayoutInfo> layoutConsumer = mWindowLayoutChangeListeners.get(context); + WindowLayoutInfo newWindowLayout = getWindowLayoutInfo(context, storedFeatures); + layoutConsumer.accept(newWindowLayout); } } + /** + * Translates the {@link DisplayFeature} into a {@link WindowLayoutInfo} when a + * valid state is found. + * @param context a proxy for the {@link android.view.Window} that contains the + * {@link DisplayFeature}. + */ + private WindowLayoutInfo getWindowLayoutInfo(@NonNull @UiContext Context context, + List<CommonFoldingFeature> storedFeatures) { + List<DisplayFeature> displayFeatureList = getDisplayFeatures(context, storedFeatures); + return new WindowLayoutInfo(displayFeatureList); + } + + /** + * Gets the current {@link WindowLayoutInfo} computed with passed {@link WindowConfiguration}. + * + * @return current {@link WindowLayoutInfo} on the default display. Returns + * empty {@link WindowLayoutInfo} on secondary displays. + */ @NonNull - private WindowLayoutInfo getWindowLayoutInfo(@NonNull Activity activity) { - List<DisplayFeature> displayFeatures = getDisplayFeatures(activity); - return new WindowLayoutInfo(displayFeatures); + public WindowLayoutInfo getCurrentWindowLayoutInfo(int displayId, + @NonNull WindowConfiguration windowConfiguration) { + return getWindowLayoutInfo(displayId, windowConfiguration, mLastReportedFoldingFeatures); + } + + /** @see #getWindowLayoutInfo(Context, List) */ + private WindowLayoutInfo getWindowLayoutInfo(int displayId, + @NonNull WindowConfiguration windowConfiguration, + List<CommonFoldingFeature> storedFeatures) { + List<DisplayFeature> displayFeatureList = getDisplayFeatures(displayId, windowConfiguration, + storedFeatures); + return new WindowLayoutInfo(displayFeatureList); } /** @@ -173,97 +250,111 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent { * bounds are not valid, constructing a {@link FoldingFeature} will throw an * {@link IllegalArgumentException} since this can cause negative UI effects down stream. * - * @param activity a proxy for the {@link android.view.Window} that contains the + * @param context a proxy for the {@link android.view.Window} that contains the * {@link DisplayFeature}. - * @return a {@link List} of valid {@link DisplayFeature} that * are within the {@link android.view.Window} of the {@link Activity} */ - private List<DisplayFeature> getDisplayFeatures(@NonNull Activity activity) { + private List<DisplayFeature> getDisplayFeatures( + @NonNull @UiContext Context context, List<CommonFoldingFeature> storedFeatures) { + if (!shouldReportDisplayFeatures(context)) { + return Collections.emptyList(); + } + return getDisplayFeatures(context.getDisplayId(), + context.getResources().getConfiguration().windowConfiguration, + storedFeatures); + } + + /** @see #getDisplayFeatures(Context, List) */ + private List<DisplayFeature> getDisplayFeatures(int displayId, + @NonNull WindowConfiguration windowConfiguration, + List<CommonFoldingFeature> storedFeatures) { List<DisplayFeature> features = new ArrayList<>(); - int displayId = activity.getDisplay().getDisplayId(); if (displayId != DEFAULT_DISPLAY) { - Log.w(TAG, "This sample doesn't support display features on secondary displays"); return features; } - if (isTaskInMultiWindowMode(activity)) { - // It is recommended not to report any display features in multi-window mode, since it - // won't be possible to synchronize the display feature positions with window movement. - return features; - } + for (CommonFoldingFeature baseFeature : storedFeatures) { + Integer state = convertToExtensionState(baseFeature.getState()); + if (state == null) { + continue; + } + Rect featureRect = baseFeature.getRect(); + rotateRectToDisplayRotation(displayId, featureRect); + transformToWindowSpaceRect(windowConfiguration, featureRect); - Optional<List<CommonFoldingFeature>> storedFeatures = mFoldingFeatureProducer.getData(); - if (storedFeatures.isPresent()) { - for (CommonFoldingFeature baseFeature : storedFeatures.get()) { - Integer state = convertToExtensionState(baseFeature.getState()); - if (state == null) { - continue; - } - Rect featureRect = baseFeature.getRect(); - rotateRectToDisplayRotation(displayId, featureRect); - transformToWindowSpaceRect(activity, featureRect); - - if (!isRectZero(featureRect)) { - // TODO(b/228641877) Remove guarding if when fixed. - features.add(new FoldingFeature(featureRect, baseFeature.getType(), state)); - } + if (!isZero(featureRect)) { + // TODO(b/228641877): Remove guarding when fixed. + features.add(new FoldingFeature(featureRect, baseFeature.getType(), state)); } } return features; } /** - * Checks whether the task associated with the activity is in multi-window. If task info is not - * available it defaults to {@code true}. + * Checks whether display features should be reported for the activity. + * TODO(b/238948678): Support reporting display features in all windowing modes. */ - private boolean isTaskInMultiWindowMode(@NonNull Activity activity) { - final ActivityManager am = activity.getSystemService(ActivityManager.class); - if (am == null) { - return true; + private boolean shouldReportDisplayFeatures(@NonNull @UiContext Context context) { + int displayId = context.getDisplay().getDisplayId(); + if (displayId != DEFAULT_DISPLAY) { + // Display features are not supported on secondary displays. + return false; } - - final List<AppTask> appTasks = am.getAppTasks(); - final int taskId = activity.getTaskId(); - AppTask task = null; - for (AppTask t : appTasks) { - if (t.getTaskInfo().taskId == taskId) { - task = t; - break; - } + final int windowingMode; + if (context instanceof Activity) { + windowingMode = ActivityClient.getInstance().getTaskWindowingMode( + context.getActivityToken()); + } else { + // TODO(b/242674941): use task windowing mode for window context that associates with + // activity. + windowingMode = context.getResources().getConfiguration().windowConfiguration + .getWindowingMode(); } - if (task == null) { - // The task might be removed on the server already. - return true; + if (windowingMode == -1) { + // If we cannot determine the task windowing mode for any reason, it is likely that we + // won't be able to determine its position correctly as well. DisplayFeatures' bounds + // in this case can't be computed correctly, so we should skip. + return false; } - return WindowConfiguration.inMultiWindowMode(task.getTaskInfo().getWindowingMode()); + // It is recommended not to report any display features in multi-window mode, since it + // won't be possible to synchronize the display feature positions with window movement. + return !WindowConfiguration.inMultiWindowMode(windowingMode); } - /** - * Returns {@link true} if a {@link Rect} has zero width and zero height, - * {@code false} otherwise. - */ - private boolean isRectZero(Rect rect) { - return rect.width() == 0 && rect.height() == 0; + private void onDisplayFeaturesChangedIfListening(@NonNull IBinder token) { + if (isListeningForLayoutChanges(token)) { + mFoldingFeatureProducer.getData( + WindowLayoutComponentImpl.this::onDisplayFeaturesChanged); + } } private final class NotifyOnConfigurationChanged extends EmptyLifecycleCallbacksAdapter { @Override public void onActivityCreated(Activity activity, Bundle savedInstanceState) { super.onActivityCreated(activity, savedInstanceState); - onDisplayFeaturesChangedIfListening(activity); + onDisplayFeaturesChangedIfListening(activity.getActivityToken()); } @Override public void onActivityConfigurationChanged(Activity activity) { super.onActivityConfigurationChanged(activity); - onDisplayFeaturesChangedIfListening(activity); + onDisplayFeaturesChangedIfListening(activity.getActivityToken()); } + } - private void onDisplayFeaturesChangedIfListening(Activity activity) { - IBinder token = activity.getWindow().getAttributes().token; - if (token == null || isListeningForLayoutChanges(token)) { - onDisplayFeaturesChanged(); - } + private final class WindowContextConfigListener implements ComponentCallbacks { + final IBinder mToken; + + WindowContextConfigListener(IBinder token) { + mToken = token; + } + + @Override + public void onConfigurationChanged(@NonNull Configuration newConfig) { + onDisplayFeaturesChangedIfListening(mToken); } + + @Override + public void onLowMemory() {} } } diff --git a/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SampleSidecarImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SampleSidecarImpl.java index 970f0a2af632..5bfb0ebdcaa8 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SampleSidecarImpl.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SampleSidecarImpl.java @@ -28,41 +28,42 @@ import android.content.Context; import android.graphics.Rect; import android.os.Bundle; import android.os.IBinder; -import android.util.Log; import androidx.annotation.NonNull; import androidx.window.common.CommonFoldingFeature; import androidx.window.common.DeviceStateManagerFoldingFeatureProducer; import androidx.window.common.EmptyLifecycleCallbacksAdapter; import androidx.window.common.RawFoldingFeatureProducer; -import androidx.window.util.DataProducer; +import androidx.window.util.BaseDataProducer; import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.Optional; /** * Reference implementation of androidx.window.sidecar OEM interface for use with * WindowManager Jetpack. */ class SampleSidecarImpl extends StubSidecar { - private static final String TAG = "SampleSidecar"; - - private final DataProducer<List<CommonFoldingFeature>> mFoldingFeatureProducer; - + private List<CommonFoldingFeature> mStoredFeatures = new ArrayList<>(); SampleSidecarImpl(Context context) { ((Application) context.getApplicationContext()) .registerActivityLifecycleCallbacks(new NotifyOnConfigurationChanged()); - DataProducer<String> settingsFeatureProducer = new RawFoldingFeatureProducer(context); - mFoldingFeatureProducer = new DeviceStateManagerFoldingFeatureProducer(context, - settingsFeatureProducer); + BaseDataProducer<String> settingsFeatureProducer = new RawFoldingFeatureProducer(context); + BaseDataProducer<List<CommonFoldingFeature>> foldingFeatureProducer = + new DeviceStateManagerFoldingFeatureProducer(context, + settingsFeatureProducer); - mFoldingFeatureProducer.addDataChangedCallback(this::onDisplayFeaturesChanged); + foldingFeatureProducer.addDataChangedCallback(this::onDisplayFeaturesChanged); } - private void onDisplayFeaturesChanged() { + private void setStoredFeatures(List<CommonFoldingFeature> storedFeatures) { + mStoredFeatures = storedFeatures; + } + + private void onDisplayFeaturesChanged(List<CommonFoldingFeature> storedFeatures) { + setStoredFeatures(storedFeatures); updateDeviceState(getDeviceState()); for (IBinder windowToken : getWindowsListeningForLayoutChanges()) { SidecarWindowLayoutInfo newLayout = getWindowLayoutInfo(windowToken); @@ -79,16 +80,16 @@ class SampleSidecarImpl extends StubSidecar { } private int deviceStateFromFeature() { - List<CommonFoldingFeature> storedFeatures = mFoldingFeatureProducer.getData() - .orElse(Collections.emptyList()); - for (int i = 0; i < storedFeatures.size(); i++) { - CommonFoldingFeature feature = storedFeatures.get(i); + for (int i = 0; i < mStoredFeatures.size(); i++) { + CommonFoldingFeature feature = mStoredFeatures.get(i); final int state = feature.getState(); switch (state) { case CommonFoldingFeature.COMMON_STATE_FLAT: return SidecarDeviceState.POSTURE_OPENED; case CommonFoldingFeature.COMMON_STATE_HALF_OPENED: return SidecarDeviceState.POSTURE_HALF_OPENED; + case CommonFoldingFeature.COMMON_STATE_UNKNOWN: + return SidecarDeviceState.POSTURE_UNKNOWN; } } return SidecarDeviceState.POSTURE_UNKNOWN; @@ -109,7 +110,6 @@ class SampleSidecarImpl extends StubSidecar { private List<SidecarDisplayFeature> getDisplayFeatures(@NonNull Activity activity) { int displayId = activity.getDisplay().getDisplayId(); if (displayId != DEFAULT_DISPLAY) { - Log.w(TAG, "This sample doesn't support display features on secondary displays"); return Collections.emptyList(); } @@ -119,18 +119,15 @@ class SampleSidecarImpl extends StubSidecar { return Collections.emptyList(); } - Optional<List<CommonFoldingFeature>> storedFeatures = mFoldingFeatureProducer.getData(); List<SidecarDisplayFeature> features = new ArrayList<>(); - if (storedFeatures.isPresent()) { - for (CommonFoldingFeature baseFeature : storedFeatures.get()) { - SidecarDisplayFeature feature = new SidecarDisplayFeature(); - Rect featureRect = baseFeature.getRect(); - rotateRectToDisplayRotation(displayId, featureRect); - transformToWindowSpaceRect(activity, featureRect); - feature.setRect(featureRect); - feature.setType(baseFeature.getType()); - features.add(feature); - } + for (CommonFoldingFeature baseFeature : mStoredFeatures) { + SidecarDisplayFeature feature = new SidecarDisplayFeature(); + Rect featureRect = baseFeature.getRect(); + rotateRectToDisplayRotation(displayId, featureRect); + transformToWindowSpaceRect(activity, featureRect); + feature.setRect(featureRect); + feature.setType(baseFeature.getType()); + features.add(feature); } return Collections.unmodifiableList(features); } @@ -138,7 +135,7 @@ class SampleSidecarImpl extends StubSidecar { @Override protected void onListenersChanged() { if (hasListeners()) { - onDisplayFeaturesChanged(); + onDisplayFeaturesChanged(mStoredFeatures); } } @@ -158,7 +155,7 @@ class SampleSidecarImpl extends StubSidecar { private void onDisplayFeaturesChangedForActivity(@NonNull Activity activity) { IBinder token = activity.getWindow().getAttributes().token; if (token == null || mWindowLayoutChangeListenerTokens.contains(token)) { - onDisplayFeaturesChanged(); + onDisplayFeaturesChanged(mStoredFeatures); } } } diff --git a/libs/WindowManager/Jetpack/src/androidx/window/util/AcceptOnceConsumer.java b/libs/WindowManager/Jetpack/src/androidx/window/util/AcceptOnceConsumer.java new file mode 100644 index 000000000000..7624b693ac43 --- /dev/null +++ b/libs/WindowManager/Jetpack/src/androidx/window/util/AcceptOnceConsumer.java @@ -0,0 +1,42 @@ +/* + * 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 androidx.window.util; + +import android.annotation.NonNull; + +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}. + */ +public class AcceptOnceConsumer<T> implements Consumer<T> { + private final Consumer<T> mCallback; + private final DataProducer<T> mProducer; + + public AcceptOnceConsumer(@NonNull DataProducer<T> producer, @NonNull Consumer<T> callback) { + mProducer = producer; + mCallback = callback; + } + + @Override + public void accept(@NonNull T t) { + mCallback.accept(t); + mProducer.removeDataChangedCallback(this); + } +} diff --git a/libs/WindowManager/Jetpack/src/androidx/window/util/BaseDataProducer.java b/libs/WindowManager/Jetpack/src/androidx/window/util/BaseDataProducer.java index 930db3b701b7..cbaa27712015 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/util/BaseDataProducer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/util/BaseDataProducer.java @@ -16,41 +16,75 @@ package androidx.window.util; +import androidx.annotation.GuardedBy; import androidx.annotation.NonNull; import java.util.LinkedHashSet; +import java.util.Optional; import java.util.Set; +import java.util.function.Consumer; /** * Base class that provides the implementation for the callback mechanism of the - * {@link DataProducer} API. + * {@link DataProducer} API. This class is thread safe for adding, removing, and notifying + * consumers. * - * @param <T> The type of data this producer returns through {@link #getData()}. + * @param <T> The type of data this producer returns through {@link DataProducer#getData}. */ public abstract class BaseDataProducer<T> implements DataProducer<T> { - private final Set<Runnable> mCallbacks = new LinkedHashSet<>(); + private final Object mLock = new Object(); + @GuardedBy("mLock") + private final Set<Consumer<T>> mCallbacks = new LinkedHashSet<>(); + + /** + * Adds a callback to the set of callbacks listening for data. Data is delivered through + * {@link BaseDataProducer#notifyDataChanged(Object)}. This method is thread safe. Callers + * should ensure that callbacks are thread safe. + * @param callback that will receive data from the producer. + */ @Override - public final void addDataChangedCallback(@NonNull Runnable callback) { - mCallbacks.add(callback); - onListenersChanged(mCallbacks); + public final void addDataChangedCallback(@NonNull Consumer<T> callback) { + synchronized (mLock) { + mCallbacks.add(callback); + Optional<T> currentData = getCurrentData(); + currentData.ifPresent(callback); + onListenersChanged(mCallbacks); + } } + /** + * Removes a callback to the set of callbacks listening for data. This method is thread safe + * for adding. + * @param callback that was registered in + * {@link BaseDataProducer#addDataChangedCallback(Consumer)}. + */ @Override - public final void removeDataChangedCallback(@NonNull Runnable callback) { - mCallbacks.remove(callback); - onListenersChanged(mCallbacks); + public final void removeDataChangedCallback(@NonNull Consumer<T> callback) { + synchronized (mLock) { + mCallbacks.remove(callback); + onListenersChanged(mCallbacks); + } } - protected void onListenersChanged(Set<Runnable> callbacks) {} + protected void onListenersChanged(Set<Consumer<T>> callbacks) {} + + /** + * @return the current data if available and {@code Optional.empty()} otherwise. + */ + @NonNull + public abstract Optional<T> getCurrentData(); /** - * Called to notify all registered callbacks that the data provided by {@link #getData()} has - * changed. + * 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. */ - protected void notifyDataChanged() { - for (Runnable callback : mCallbacks) { - callback.run(); + protected void notifyDataChanged(T value) { + synchronized (mLock) { + for (Consumer<T> callback : mCallbacks) { + callback.accept(value); + } } } -} +}
\ No newline at end of file diff --git a/libs/WindowManager/Jetpack/src/androidx/window/util/DataProducer.java b/libs/WindowManager/Jetpack/src/androidx/window/util/DataProducer.java index d4d1a23b756b..ec301dc34aaa 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/util/DataProducer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/util/DataProducer.java @@ -18,26 +18,27 @@ package androidx.window.util; import android.annotation.NonNull; -import java.util.Optional; +import java.util.function.Consumer; /** - * Produces data through {@link #getData()} and provides a mechanism for receiving a callback when - * the data managed by the produces has changed. + * 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 #getData()}. + * @param <T> The type of data this producer returns through {@link DataProducer#getData}. */ public interface DataProducer<T> { /** - * Returns the data currently stored in the provider, or {@link Optional#empty()} if the - * provider has no data. + * Emits the first available data at that point in time. + * @param dataConsumer a {@link Consumer} that will receive one value. */ - Optional<T> getData(); + void getData(@NonNull Consumer<T> dataConsumer); /** - * Adds a callback to be notified when the data returned from {@link #getData()} has changed. + * Adds a callback to be notified when the data returned + * from {@link DataProducer#getData} has changed. */ - void addDataChangedCallback(@NonNull Runnable callback); + void addDataChangedCallback(@NonNull Consumer<T> callback); - /** Removes a callback previously added with {@link #addDataChangedCallback(Runnable)}. */ - void removeDataChangedCallback(@NonNull Runnable callback); + /** Removes a callback previously added with {@link #addDataChangedCallback(Consumer)}. */ + void removeDataChangedCallback(@NonNull Consumer<T> callback); } diff --git a/libs/WindowManager/Jetpack/src/androidx/window/util/ExtensionHelper.java b/libs/WindowManager/Jetpack/src/androidx/window/util/ExtensionHelper.java index 2a593f15a9de..9e2611f392a3 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/util/ExtensionHelper.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/util/ExtensionHelper.java @@ -21,14 +21,16 @@ import static android.view.Surface.ROTATION_180; import static android.view.Surface.ROTATION_270; import static android.view.Surface.ROTATION_90; -import android.app.Activity; +import android.app.WindowConfiguration; +import android.content.Context; import android.graphics.Rect; import android.hardware.display.DisplayManagerGlobal; import android.view.DisplayInfo; import android.view.Surface; +import android.view.WindowManager; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; +import androidx.annotation.UiContext; /** * Util class for both Sidecar and Extensions. @@ -86,26 +88,31 @@ public final class ExtensionHelper { } /** Transforms rectangle from absolute coordinate space to the window coordinate space. */ - public static void transformToWindowSpaceRect(Activity activity, Rect inOutRect) { - Rect windowRect = getWindowBounds(activity); - if (windowRect == null) { - inOutRect.setEmpty(); - return; - } - if (!Rect.intersects(inOutRect, windowRect)) { + public static void transformToWindowSpaceRect(@NonNull @UiContext Context context, + Rect inOutRect) { + transformToWindowSpaceRect(getWindowBounds(context), inOutRect); + } + + /** @see ExtensionHelper#transformToWindowSpaceRect(Context, Rect) */ + public static void transformToWindowSpaceRect(@NonNull WindowConfiguration windowConfiguration, + Rect inOutRect) { + transformToWindowSpaceRect(windowConfiguration.getBounds(), inOutRect); + } + + private static void transformToWindowSpaceRect(@NonNull Rect bounds, @NonNull Rect inOutRect) { + if (!inOutRect.intersect(bounds)) { inOutRect.setEmpty(); return; } - inOutRect.intersect(windowRect); - inOutRect.offset(-windowRect.left, -windowRect.top); + inOutRect.offset(-bounds.left, -bounds.top); } /** * Gets the current window bounds in absolute coordinates. */ - @Nullable - private static Rect getWindowBounds(@NonNull Activity activity) { - return activity.getWindowManager().getCurrentWindowMetrics().getBounds(); + @NonNull + private static Rect getWindowBounds(@NonNull @UiContext Context context) { + return context.getSystemService(WindowManager.class).getCurrentWindowMetrics().getBounds(); } /** diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/EmbeddingTestUtils.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/EmbeddingTestUtils.java index effc1a3ef3ea..40f7a273980a 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 @@ -16,9 +16,12 @@ package androidx.window.extensions.embedding; +import static android.view.Display.DEFAULT_DISPLAY; + import static androidx.window.extensions.embedding.SplitRule.FINISH_ALWAYS; import static androidx.window.extensions.embedding.SplitRule.FINISH_NEVER; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import android.annotation.NonNull; @@ -26,32 +29,68 @@ import android.app.Activity; import android.content.Intent; import android.content.pm.ActivityInfo; import android.content.res.Configuration; +import android.content.res.Resources; import android.graphics.Point; import android.graphics.Rect; import android.util.Pair; import android.window.TaskFragmentInfo; import android.window.WindowContainerToken; +import androidx.window.extensions.embedding.SplitAttributes.SplitType; +import androidx.window.extensions.layout.DisplayFeature; +import androidx.window.extensions.layout.FoldingFeature; +import androidx.window.extensions.layout.WindowLayoutInfo; + +import java.util.ArrayList; import java.util.Collections; +import java.util.List; public class EmbeddingTestUtils { static final Rect TASK_BOUNDS = new Rect(0, 0, 600, 1200); static final int TASK_ID = 10; - static final float SPLIT_RATIO = 0.5f; + static final SplitType SPLIT_TYPE = SplitType.RatioSplitType.splitEqually(); + static final SplitAttributes SPLIT_ATTRIBUTES = new SplitAttributes.Builder().build(); + static final String TEST_TAG = "test"; /** Default finish behavior in Jetpack. */ static final int DEFAULT_FINISH_PRIMARY_WITH_SECONDARY = FINISH_NEVER; static final int DEFAULT_FINISH_SECONDARY_WITH_PRIMARY = FINISH_ALWAYS; + private static final float SPLIT_RATIO = 0.5f; private EmbeddingTestUtils() {} /** Gets the bounds of a TaskFragment that is in split. */ static Rect getSplitBounds(boolean isPrimary) { - final int width = (int) (TASK_BOUNDS.width() * SPLIT_RATIO); + return getSplitBounds(isPrimary, false /* shouldSplitHorizontally */); + } + + /** Gets the bounds of a TaskFragment that is in split. */ + static Rect getSplitBounds(boolean isPrimary, boolean shouldSplitHorizontally) { + final int dimension = (int) ( + (shouldSplitHorizontally ? TASK_BOUNDS.height() : TASK_BOUNDS.width()) + * SPLIT_RATIO); + if (shouldSplitHorizontally) { + return isPrimary + ? new Rect( + TASK_BOUNDS.left, + TASK_BOUNDS.top, + TASK_BOUNDS.right, + TASK_BOUNDS.top + dimension) + : new Rect( + TASK_BOUNDS.left, + TASK_BOUNDS.top + dimension, + TASK_BOUNDS.right, + TASK_BOUNDS.bottom); + } return isPrimary - ? new Rect(TASK_BOUNDS.left, TASK_BOUNDS.top, TASK_BOUNDS.left + width, - TASK_BOUNDS.bottom) + ? new Rect( + TASK_BOUNDS.left, + TASK_BOUNDS.top, + TASK_BOUNDS.left + dimension, + TASK_BOUNDS.bottom) : new Rect( - TASK_BOUNDS.left + width, TASK_BOUNDS.top, TASK_BOUNDS.right, + TASK_BOUNDS.left + dimension, + TASK_BOUNDS.top, + TASK_BOUNDS.right, TASK_BOUNDS.bottom); } @@ -69,10 +108,15 @@ public class EmbeddingTestUtils { activityPair -> false, targetPair::equals, w -> true) - .setSplitRatio(SPLIT_RATIO) + .setDefaultSplitAttributes( + new SplitAttributes.Builder() + .setSplitType(SPLIT_TYPE) + .build() + ) .setShouldClearTop(clearTop) .setFinishPrimaryWithSecondary(DEFAULT_FINISH_PRIMARY_WITH_SECONDARY) .setFinishSecondaryWithPrimary(DEFAULT_FINISH_SECONDARY_WITH_PRIMARY) + .setTag(TEST_TAG) .build(); } @@ -101,10 +145,15 @@ public class EmbeddingTestUtils { targetPair::equals, activityIntentPair -> false, w -> true) - .setSplitRatio(SPLIT_RATIO) + .setDefaultSplitAttributes( + new SplitAttributes.Builder() + .setSplitType(SPLIT_TYPE) + .build() + ) .setFinishPrimaryWithSecondary(finishPrimaryWithSecondary) .setFinishSecondaryWithPrimary(finishSecondaryWithPrimary) .setShouldClearTop(clearTop) + .setTag(TEST_TAG) .build(); } @@ -130,4 +179,29 @@ public class EmbeddingTestUtils { primaryBounds.width() + 1, primaryBounds.height() + 1); return aInfo; } + + static TaskContainer createTestTaskContainer() { + Resources resources = mock(Resources.class); + doReturn(new Configuration()).when(resources).getConfiguration(); + Activity activity = mock(Activity.class); + doReturn(resources).when(activity).getResources(); + doReturn(DEFAULT_DISPLAY).when(activity).getDisplayId(); + + return new TaskContainer(TASK_ID, activity); + } + + static WindowLayoutInfo createWindowLayoutInfo() { + final FoldingFeature foldingFeature = new FoldingFeature( + new Rect( + TASK_BOUNDS.left, + TASK_BOUNDS.top + TASK_BOUNDS.height() / 2 - 5, + TASK_BOUNDS.right, + TASK_BOUNDS.top + TASK_BOUNDS.height() / 2 + 5 + ), + FoldingFeature.TYPE_HINGE, + FoldingFeature.STATE_HALF_OPENED); + final List<DisplayFeature> displayFeatures = new ArrayList<>(); + displayFeatures.add(foldingFeature); + return new WindowLayoutInfo(displayFeatures); + } } 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 4d2595275f20..957a24873998 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 @@ -19,14 +19,15 @@ package androidx.window.extensions.embedding; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_ID; +import static androidx.window.extensions.embedding.EmbeddingTestUtils.createTestTaskContainer; -import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -36,6 +37,7 @@ import android.graphics.Point; import android.os.Handler; import android.platform.test.annotations.Presubmit; import android.window.TaskFragmentInfo; +import android.window.TaskFragmentTransaction; import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; @@ -56,6 +58,8 @@ import java.util.ArrayList; * Build/Install/Run: * atest WMJetpackUnitTests:JetpackTaskFragmentOrganizerTest */ +// Suppress GuardedBy warning on unit tests +@SuppressWarnings("GuardedBy") @Presubmit @SmallTest @RunWith(AndroidJUnit4.class) @@ -114,12 +118,12 @@ public class JetpackTaskFragmentOrganizerTest { @Test public void testExpandTaskFragment() { - final TaskContainer taskContainer = new TaskContainer(TASK_ID); + final TaskContainer taskContainer = createTestTaskContainer(); final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */, new Intent(), taskContainer, mSplitController); final TaskFragmentInfo info = createMockInfo(container); mOrganizer.mFragmentInfos.put(container.getTaskFragmentToken(), info); - container.setInfo(info); + container.setInfo(mTransaction, info); mOrganizer.expandTaskFragment(mTransaction, container.getTaskFragmentToken()); @@ -127,6 +131,14 @@ public class JetpackTaskFragmentOrganizerTest { WINDOWING_MODE_UNDEFINED); } + @Test + public void testOnTransactionReady() { + final TaskFragmentTransaction transaction = new TaskFragmentTransaction(); + mOrganizer.onTransactionReady(transaction); + + verify(mCallback).onTransactionReady(transaction); + } + private TaskFragmentInfo createMockInfo(TaskFragmentContainer container) { return new TaskFragmentInfo(container.getTaskFragmentToken(), mock(WindowContainerToken.class), new Configuration(), 0 /* runningActivityCount */, 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 ad496a906a33..25d034756265 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 @@ -16,18 +16,32 @@ package androidx.window.extensions.embedding; +import static android.app.ActivityManager.START_CANCELED; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; - -import static androidx.window.extensions.embedding.EmbeddingTestUtils.SPLIT_RATIO; +import static android.view.Display.DEFAULT_DISPLAY; +import static android.window.TaskFragmentTransaction.TYPE_ACTIVITY_REPARENTED_TO_TASK; +import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_APPEARED; +import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_ERROR; +import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_INFO_CHANGED; +import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_PARENT_INFO_CHANGED; +import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_VANISHED; +import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_CREATE_TASK_FRAGMENT; + +import static androidx.window.extensions.embedding.EmbeddingTestUtils.DEFAULT_FINISH_PRIMARY_WITH_SECONDARY; +import static androidx.window.extensions.embedding.EmbeddingTestUtils.DEFAULT_FINISH_SECONDARY_WITH_PRIMARY; +import static androidx.window.extensions.embedding.EmbeddingTestUtils.SPLIT_ATTRIBUTES; import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_BOUNDS; import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_ID; +import static androidx.window.extensions.embedding.EmbeddingTestUtils.TEST_TAG; import static androidx.window.extensions.embedding.EmbeddingTestUtils.createActivityInfoWithMinDimensions; import static androidx.window.extensions.embedding.EmbeddingTestUtils.createMockTaskFragmentInfo; import static androidx.window.extensions.embedding.EmbeddingTestUtils.createSplitRule; +import static androidx.window.extensions.embedding.EmbeddingTestUtils.createTestTaskContainer; import static androidx.window.extensions.embedding.EmbeddingTestUtils.getSplitBounds; import static androidx.window.extensions.embedding.SplitRule.FINISH_ALWAYS; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doCallRealMethod; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; @@ -65,12 +79,19 @@ import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.platform.test.annotations.Presubmit; +import android.view.WindowInsets; +import android.view.WindowMetrics; import android.window.TaskFragmentInfo; +import android.window.TaskFragmentOrganizer; +import android.window.TaskFragmentParentInfo; +import android.window.TaskFragmentTransaction; import android.window.WindowContainerTransaction; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; +import androidx.window.extensions.layout.WindowLayoutComponentImpl; +import androidx.window.extensions.layout.WindowLayoutInfo; import org.junit.Before; import org.junit.Test; @@ -88,6 +109,8 @@ import java.util.List; * Build/Install/Run: * atest WMJetpackUnitTests:SplitControllerTest */ +// Suppress GuardedBy warning on unit tests +@SuppressWarnings("GuardedBy") @Presubmit @SmallTest @RunWith(AndroidJUnit4.class) @@ -104,6 +127,8 @@ public class SplitControllerTest { private WindowContainerTransaction mTransaction; @Mock private Handler mHandler; + @Mock + private WindowLayoutComponentImpl mWindowLayoutComponent; private SplitController mSplitController; private SplitPresenter mSplitPresenter; @@ -111,11 +136,13 @@ public class SplitControllerTest { @Before public void setUp() { MockitoAnnotations.initMocks(this); - mSplitController = new SplitController(); + doReturn(new WindowLayoutInfo(new ArrayList<>())).when(mWindowLayoutComponent) + .getCurrentWindowLayoutInfo(anyInt(), any()); + mSplitController = new SplitController(mWindowLayoutComponent); mSplitPresenter = mSplitController.mPresenter; spyOn(mSplitController); spyOn(mSplitPresenter); - doNothing().when(mSplitPresenter).applyTransaction(any()); + doNothing().when(mSplitPresenter).applyTransaction(any(), anyInt(), anyBoolean()); final Configuration activityConfig = new Configuration(); activityConfig.windowConfiguration.setBounds(TASK_BOUNDS); activityConfig.windowConfiguration.setMaxBounds(TASK_BOUNDS); @@ -126,7 +153,7 @@ public class SplitControllerTest { @Test public void testGetTopActiveContainer() { - final TaskContainer taskContainer = new TaskContainer(TASK_ID); + final TaskContainer taskContainer = createTestTaskContainer(); // tf1 has no running activity so is not active. final TaskFragmentContainer tf1 = new TaskFragmentContainer(null /* activity */, new Intent(), taskContainer, mSplitController); @@ -157,14 +184,14 @@ public class SplitControllerTest { final TaskFragmentInfo info = mock(TaskFragmentInfo.class); doReturn(new ArrayList<>()).when(info).getActivities(); doReturn(true).when(info).isEmpty(); - tf1.setInfo(info); + tf1.setInfo(mTransaction, info); assertWithMessage("Must return tf because we are waiting for tf1 to become non-empty after" + " creation.") .that(mSplitController.getTopActiveContainer(TASK_ID)).isEqualTo(tf1); doReturn(false).when(info).isEmpty(); - tf1.setInfo(info); + tf1.setInfo(mTransaction, info); assertWithMessage("Must return null because tf1 becomes empty.") .that(mSplitController.getTopActiveContainer(TASK_ID)).isNull(); @@ -176,19 +203,21 @@ public class SplitControllerTest { doReturn(tf.getTaskFragmentToken()).when(mInfo).getFragmentToken(); // The TaskFragment has been removed in the server, we only need to cleanup the reference. - mSplitController.onTaskFragmentVanished(mInfo); + mSplitController.onTaskFragmentVanished(mTransaction, mInfo); verify(mSplitPresenter, never()).deleteTaskFragment(any(), any()); verify(mSplitController).removeContainer(tf); - verify(mActivity, never()).finish(); + verify(mTransaction, never()).finishActivity(any()); } @Test public void testOnTaskFragmentAppearEmptyTimeout() { final TaskFragmentContainer tf = mSplitController.newContainer(mActivity, TASK_ID); - mSplitController.onTaskFragmentAppearEmptyTimeout(tf); + doCallRealMethod().when(mSplitController).onTaskFragmentAppearEmptyTimeout(any(), any()); + mSplitController.onTaskFragmentAppearEmptyTimeout(mTransaction, tf); - verify(mSplitPresenter).cleanupContainer(tf, false /* shouldFinishDependent */); + verify(mSplitPresenter).cleanupContainer(mTransaction, tf, + false /* shouldFinishDependent */); } @Test @@ -228,8 +257,8 @@ public class SplitControllerTest { spyOn(tf); doReturn(mActivity).when(tf).getTopNonFinishingActivity(); doReturn(true).when(tf).isEmpty(); - doReturn(true).when(mSplitController).launchPlaceholderIfNecessary(mActivity, - false /* isOnCreated */); + doReturn(true).when(mSplitController).launchPlaceholderIfNecessary(mTransaction, + mActivity, false /* isOnCreated */); doNothing().when(mSplitPresenter).updateSplitContainer(any(), any(), any()); mSplitController.updateContainer(mTransaction, tf); @@ -249,12 +278,14 @@ public class SplitControllerTest { mSplitController.updateContainer(mTransaction, tf); - verify(mSplitController, never()).dismissPlaceholderIfNecessary(any()); + verify(mSplitController, never()).dismissPlaceholderIfNecessary(any(), any()); // Verify if tf is not in the top splitContainer, final SplitContainer splitContainer = mock(SplitContainer.class); doReturn(tf).when(splitContainer).getPrimaryContainer(); doReturn(tf).when(splitContainer).getSecondaryContainer(); + doReturn(createTestTaskContainer()).when(splitContainer).getTaskContainer(); + doReturn(createSplitRule(mActivity, mActivity)).when(splitContainer).getSplitRule(); final List<SplitContainer> splitContainers = mSplitController.getTaskContainer(TASK_ID).mSplitContainers; splitContainers.add(splitContainer); @@ -263,7 +294,7 @@ public class SplitControllerTest { mSplitController.updateContainer(mTransaction, tf); - verify(mSplitController, never()).dismissPlaceholderIfNecessary(any()); + verify(mSplitController, never()).dismissPlaceholderIfNecessary(any(), any()); // Verify if one or both containers in the top SplitContainer are finished, // dismissPlaceholder() won't be called. @@ -272,12 +303,12 @@ public class SplitControllerTest { mSplitController.updateContainer(mTransaction, tf); - verify(mSplitController, never()).dismissPlaceholderIfNecessary(any()); + verify(mSplitController, never()).dismissPlaceholderIfNecessary(any(), any()); // Verify if placeholder should be dismissed, updateSplitContainer() won't be called. doReturn(false).when(tf).isFinished(); doReturn(true).when(mSplitController) - .dismissPlaceholderIfNecessary(splitContainer); + .dismissPlaceholderIfNecessary(mTransaction, splitContainer); mSplitController.updateContainer(mTransaction, tf); @@ -285,7 +316,7 @@ public class SplitControllerTest { // Verify if the top active split is updated if both of its containers are not finished. doReturn(false).when(mSplitController) - .dismissPlaceholderIfNecessary(splitContainer); + .dismissPlaceholderIfNecessary(mTransaction, splitContainer); mSplitController.updateContainer(mTransaction, tf); @@ -293,35 +324,57 @@ public class SplitControllerTest { } @Test + public void testOnStartActivityResultError() { + final Intent intent = new Intent(); + final TaskContainer taskContainer = createTestTaskContainer(); + final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */, + intent, taskContainer, mSplitController); + final SplitController.ActivityStartMonitor monitor = + mSplitController.getActivityStartMonitor(); + + container.setPendingAppearedIntent(intent); + final Bundle bundle = new Bundle(); + bundle.putBinder(ActivityOptions.KEY_LAUNCH_TASK_FRAGMENT_TOKEN, + container.getTaskFragmentToken()); + monitor.mCurrentIntent = intent; + doReturn(container).when(mSplitController).getContainer(any()); + + monitor.onStartActivityResult(START_CANCELED, bundle); + assertNull(container.getPendingAppearedIntent()); + } + + @Test public void testOnActivityCreated() { - mSplitController.onActivityCreated(mActivity); + mSplitController.onActivityCreated(mTransaction, mActivity); // Disallow to split as primary because we want the new launch to be always on top. - verify(mSplitController).resolveActivityToContainer(mActivity, false /* isOnReparent */); + verify(mSplitController).resolveActivityToContainer(mTransaction, mActivity, + false /* isOnReparent */); } @Test - public void testOnActivityReparentToTask_sameProcess() { - mSplitController.onActivityReparentToTask(TASK_ID, new Intent(), + public void testOnActivityReparentedToTask_sameProcess() { + mSplitController.onActivityReparentedToTask(mTransaction, TASK_ID, new Intent(), mActivity.getActivityToken()); // Treated as on activity created, but allow to split as primary. - verify(mSplitController).resolveActivityToContainer(mActivity, true /* isOnReparent */); + verify(mSplitController).resolveActivityToContainer(mTransaction, + mActivity, true /* isOnReparent */); // Try to place the activity to the top TaskFragment when there is no matched rule. - verify(mSplitController).placeActivityInTopContainer(mActivity); + verify(mSplitController).placeActivityInTopContainer(mTransaction, mActivity); } @Test - public void testOnActivityReparentToTask_diffProcess() { + public void testOnActivityReparentedToTask_diffProcess() { // Create an empty TaskFragment to initialize for the Task. mSplitController.newContainer(new Intent(), mActivity, TASK_ID); final IBinder activityToken = new Binder(); final Intent intent = new Intent(); - mSplitController.onActivityReparentToTask(TASK_ID, intent, activityToken); + mSplitController.onActivityReparentedToTask(mTransaction, TASK_ID, intent, activityToken); // Treated as starting new intent - verify(mSplitController, never()).resolveActivityToContainer(any(), anyBoolean()); + verify(mSplitController, never()).resolveActivityToContainer(any(), any(), anyBoolean()); verify(mSplitController).resolveStartActivityIntent(any(), eq(TASK_ID), eq(intent), isNull()); } @@ -483,26 +536,29 @@ public class SplitControllerTest { @Test public void testPlaceActivityInTopContainer() { - mSplitController.placeActivityInTopContainer(mActivity); + mSplitController.placeActivityInTopContainer(mTransaction, mActivity); - verify(mSplitPresenter, never()).applyTransaction(any()); + verify(mTransaction, never()).reparentActivityToTaskFragment(any(), any()); - mSplitController.newContainer(new Intent(), mActivity, TASK_ID); - mSplitController.placeActivityInTopContainer(mActivity); + // Place in the top container if there is no other rule matched. + final TaskFragmentContainer topContainer = mSplitController + .newContainer(new Intent(), mActivity, TASK_ID); + mSplitController.placeActivityInTopContainer(mTransaction, mActivity); - verify(mSplitPresenter).applyTransaction(any()); + verify(mTransaction).reparentActivityToTaskFragment(topContainer.getTaskFragmentToken(), + mActivity.getActivityToken()); // Not reparent if activity is in a TaskFragment. - clearInvocations(mSplitPresenter); + clearInvocations(mTransaction); mSplitController.newContainer(mActivity, TASK_ID); - mSplitController.placeActivityInTopContainer(mActivity); + mSplitController.placeActivityInTopContainer(mTransaction, mActivity); - verify(mSplitPresenter, never()).applyTransaction(any()); + verify(mTransaction, never()).reparentActivityToTaskFragment(any(), any()); } @Test public void testResolveActivityToContainer_noRuleMatched() { - final boolean result = mSplitController.resolveActivityToContainer(mActivity, + final boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity, false /* isOnReparent */); assertFalse(result); @@ -514,7 +570,7 @@ public class SplitControllerTest { setupExpandRule(mActivity); // When the activity is not in any TaskFragment, create a new expanded TaskFragment for it. - final boolean result = mSplitController.resolveActivityToContainer(mActivity, + final boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity, false /* isOnReparent */); final TaskFragmentContainer container = mSplitController.getContainerWithActivity( mActivity); @@ -522,7 +578,8 @@ public class SplitControllerTest { assertTrue(result); assertNotNull(container); verify(mSplitController).newContainer(mActivity, TASK_ID); - verify(mSplitPresenter).expandActivity(container.getTaskFragmentToken(), mActivity); + verify(mSplitPresenter).expandActivity(mTransaction, container.getTaskFragmentToken(), + mActivity); } @Test @@ -531,11 +588,11 @@ public class SplitControllerTest { // When the activity is not in any TaskFragment, create a new expanded TaskFragment for it. final TaskFragmentContainer container = mSplitController.newContainer(mActivity, TASK_ID); - final boolean result = mSplitController.resolveActivityToContainer(mActivity, + final boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity, false /* isOnReparent */); assertTrue(result); - verify(mSplitPresenter).expandTaskFragment(container.getTaskFragmentToken()); + verify(mSplitPresenter).expandTaskFragment(mTransaction, container.getTaskFragmentToken()); } @Test @@ -545,14 +602,15 @@ public class SplitControllerTest { // When the activity is not in any TaskFragment, create a new expanded TaskFragment for it. final Activity activity = createMockActivity(); addSplitTaskFragments(activity, mActivity); - final boolean result = mSplitController.resolveActivityToContainer(mActivity, + final boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity, false /* isOnReparent */); final TaskFragmentContainer container = mSplitController.getContainerWithActivity( mActivity); assertTrue(result); assertNotNull(container); - verify(mSplitPresenter).expandActivity(container.getTaskFragmentToken(), mActivity); + verify(mSplitPresenter).expandActivity(mTransaction, container.getTaskFragmentToken(), + mActivity); } @Test @@ -562,13 +620,13 @@ public class SplitControllerTest { (SplitPlaceholderRule) mSplitController.getSplitRules().get(0); // Launch placeholder if the activity is not in any TaskFragment. - final boolean result = mSplitController.resolveActivityToContainer(mActivity, + final boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity, false /* isOnReparent */); assertTrue(result); - verify(mSplitPresenter).startActivityToSide(mActivity, PLACEHOLDER_INTENT, + verify(mSplitPresenter).startActivityToSide(mTransaction, mActivity, PLACEHOLDER_INTENT, mSplitController.getPlaceholderOptions(mActivity, true /* isOnCreated */), - placeholderRule, true /* isPlaceholder */); + placeholderRule, SPLIT_ATTRIBUTES, true /* isPlaceholder */); } @Test @@ -579,12 +637,12 @@ public class SplitControllerTest { final Activity activity = createMockActivity(); mSplitController.newContainer(mActivity, TASK_ID); mSplitController.newContainer(activity, TASK_ID); - final boolean result = mSplitController.resolveActivityToContainer(mActivity, + final boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity, false /* isOnReparent */); assertFalse(result); - verify(mSplitPresenter, never()).startActivityToSide(any(), any(), any(), any(), - anyBoolean()); + verify(mSplitPresenter, never()).startActivityToSide(any(), any(), any(), any(), any(), + any(), anyBoolean()); } @Test @@ -595,13 +653,13 @@ public class SplitControllerTest { // Launch placeholder if the activity is in the topmost expanded TaskFragment. mSplitController.newContainer(mActivity, TASK_ID); - final boolean result = mSplitController.resolveActivityToContainer(mActivity, + final boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity, false /* isOnReparent */); assertTrue(result); - verify(mSplitPresenter).startActivityToSide(mActivity, PLACEHOLDER_INTENT, + verify(mSplitPresenter).startActivityToSide(mTransaction, mActivity, PLACEHOLDER_INTENT, mSplitController.getPlaceholderOptions(mActivity, true /* isOnCreated */), - placeholderRule, true /* isPlaceholder */); + placeholderRule, SPLIT_ATTRIBUTES, true /* isPlaceholder */); } @Test @@ -611,12 +669,12 @@ public class SplitControllerTest { // Don't launch placeholder if the activity is in primary split. final Activity secondaryActivity = createMockActivity(); addSplitTaskFragments(mActivity, secondaryActivity); - final boolean result = mSplitController.resolveActivityToContainer(mActivity, + final boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity, false /* isOnReparent */); assertFalse(result); - verify(mSplitPresenter, never()).startActivityToSide(any(), any(), any(), any(), - anyBoolean()); + verify(mSplitPresenter, never()).startActivityToSide(any(), any(), any(), any(), any(), + any(), anyBoolean()); } @Test @@ -628,13 +686,13 @@ public class SplitControllerTest { // Launch placeholder if the activity is in secondary split. final Activity primaryActivity = createMockActivity(); addSplitTaskFragments(primaryActivity, mActivity); - final boolean result = mSplitController.resolveActivityToContainer(mActivity, + final boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity, false /* isOnReparent */); assertTrue(result); - verify(mSplitPresenter).startActivityToSide(mActivity, PLACEHOLDER_INTENT, + verify(mSplitPresenter).startActivityToSide(mTransaction, mActivity, PLACEHOLDER_INTENT, mSplitController.getPlaceholderOptions(mActivity, true /* isOnCreated */), - placeholderRule, true /* isPlaceholder */); + placeholderRule, SPLIT_ATTRIBUTES, true /* isPlaceholder */); } @Test @@ -653,14 +711,15 @@ public class SplitControllerTest { primaryContainer, mActivity, secondaryContainer, - splitRule); + splitRule, + SPLIT_ATTRIBUTES); clearInvocations(mSplitController); - final boolean result = mSplitController.resolveActivityToContainer(mActivity, + final boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity, false /* isOnReparent */); assertTrue(result); verify(mSplitController, never()).newContainer(any(), any(), any(), anyInt()); - verify(mSplitController, never()).registerSplit(any(), any(), any(), any(), any()); + verify(mSplitController, never()).registerSplit(any(), any(), any(), any(), any(), any()); } @Test @@ -680,11 +739,12 @@ public class SplitControllerTest { primaryContainer, mActivity, secondaryContainer, - splitRule); + splitRule, + SPLIT_ATTRIBUTES); final Activity launchedActivity = createMockActivity(); primaryContainer.addPendingAppearedActivity(launchedActivity); - assertFalse(mSplitController.resolveActivityToContainer(launchedActivity, + assertFalse(mSplitController.resolveActivityToContainer(mTransaction, launchedActivity, false /* isOnReparent */)); } @@ -696,12 +756,12 @@ public class SplitControllerTest { // Activity is already in secondary split, no need to create new split. addSplitTaskFragments(primaryActivity, mActivity); clearInvocations(mSplitController); - final boolean result = mSplitController.resolveActivityToContainer(mActivity, + final boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity, false /* isOnReparent */); assertTrue(result); verify(mSplitController, never()).newContainer(any(), any(), any(), anyInt()); - verify(mSplitController, never()).registerSplit(any(), any(), any(), any(), any()); + verify(mSplitController, never()).registerSplit(any(), any(), any(), any(), any(), any()); } @Test @@ -714,7 +774,7 @@ public class SplitControllerTest { addSplitTaskFragments(primaryActivity, secondaryActivity); mSplitController.getContainerWithActivity(secondaryActivity) .addPendingAppearedActivity(mActivity); - final boolean result = mSplitController.resolveActivityToContainer(mActivity, + final boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity, false /* isOnReparent */); assertFalse(result); @@ -738,8 +798,9 @@ public class SplitControllerTest { primaryContainer, mActivity, secondaryContainer, - placeholderRule); - final boolean result = mSplitController.resolveActivityToContainer(mActivity, + placeholderRule, + SPLIT_ATTRIBUTES); + final boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity, false /* isOnReparent */); assertTrue(result); @@ -753,7 +814,7 @@ public class SplitControllerTest { final TaskFragmentContainer container = mSplitController.newContainer(activityBelow, TASK_ID); container.addPendingAppearedActivity(mActivity); - final boolean result = mSplitController.resolveActivityToContainer(mActivity, + final boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity, false /* isOnReparent */); assertTrue(result); @@ -769,14 +830,15 @@ public class SplitControllerTest { final TaskFragmentContainer container = mSplitController.newContainer(activityBelow, TASK_ID); container.addPendingAppearedActivity(mActivity); - boolean result = mSplitController.resolveActivityToContainer(mActivity, + boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity, false /* isOnReparent */); assertFalse(result); assertEquals(container, mSplitController.getContainerWithActivity(mActivity)); // Allow to split as primary. - result = mSplitController.resolveActivityToContainer(mActivity, true /* isOnReparent */); + result = mSplitController.resolveActivityToContainer(mTransaction, mActivity, + true /* isOnReparent */); assertTrue(result); assertSplitPair(mActivity, activityBelow); @@ -794,7 +856,7 @@ public class SplitControllerTest { final TaskFragmentContainer secondaryContainer = mSplitController.getContainerWithActivity( activityBelow); secondaryContainer.addPendingAppearedActivity(mActivity); - final boolean result = mSplitController.resolveActivityToContainer(mActivity, + final boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity, false /* isOnReparent */); final TaskFragmentContainer container = mSplitController.getContainerWithActivity( mActivity); @@ -815,14 +877,15 @@ public class SplitControllerTest { final TaskFragmentContainer primaryContainer = mSplitController.getContainerWithActivity( primaryActivity); primaryContainer.addPendingAppearedActivity(mActivity); - boolean result = mSplitController.resolveActivityToContainer(mActivity, + boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity, false /* isOnReparent */); assertFalse(result); assertEquals(primaryContainer, mSplitController.getContainerWithActivity(mActivity)); - result = mSplitController.resolveActivityToContainer(mActivity, true /* isOnReparent */); + result = mSplitController.resolveActivityToContainer(mTransaction, mActivity, + true /* isOnReparent */); assertTrue(result); assertSplitPair(mActivity, primaryActivity); @@ -840,7 +903,7 @@ public class SplitControllerTest { container.addPendingAppearedActivity(mActivity); // Allow to split as primary. - boolean result = mSplitController.resolveActivityToContainer(mActivity, + boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity, true /* isOnReparent */); assertTrue(result); @@ -858,7 +921,7 @@ public class SplitControllerTest { TASK_ID); container.addPendingAppearedActivity(mActivity); - boolean result = mSplitController.resolveActivityToContainer(mActivity, + boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity, false /* isOnReparent */); assertTrue(result); @@ -876,22 +939,23 @@ public class SplitControllerTest { doReturn(secondaryActivity).when(mSplitController).findActivityBelow(eq(mActivity)); clearInvocations(mSplitPresenter); - boolean result = mSplitController.resolveActivityToContainer(mActivity, + boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity, false /* isOnReparent */); assertTrue(result); assertSplitPair(primaryActivity, mActivity, true /* matchParentBounds */); assertEquals(mSplitController.getContainerWithActivity(secondaryActivity), mSplitController.getContainerWithActivity(mActivity)); - verify(mSplitPresenter, never()).createNewSplitContainer(any(), any(), any()); + verify(mSplitPresenter, never()).createNewSplitContainer(any(), any(), any(), any()); } @Test public void testResolveActivityToContainer_inUnknownTaskFragment() { - doReturn(new Binder()).when(mSplitController).getInitialTaskFragmentToken(mActivity); + doReturn(new Binder()).when(mSplitController) + .getTaskFragmentTokenFromActivityClientRecord(mActivity); // No need to handle when the new launched activity is in an unknown TaskFragment. - assertTrue(mSplitController.resolveActivityToContainer(mActivity, + assertTrue(mSplitController.resolveActivityToContainer(mTransaction, mActivity, false /* isOnReparent */)); } @@ -940,13 +1004,153 @@ public class SplitControllerTest { assertTrue(primaryContainer.isFinished()); assertTrue(secondaryContainer0.isFinished()); assertTrue(secondaryContainer1.isFinished()); - verify(mActivity).finish(); - verify(secondaryActivity0).finish(); - verify(secondaryActivity1).finish(); + verify(mTransaction).finishActivity(mActivity.getActivityToken()); + verify(mTransaction).finishActivity(secondaryActivity0.getActivityToken()); + verify(mTransaction).finishActivity(secondaryActivity1.getActivityToken()); assertTrue(taskContainer.mContainers.isEmpty()); assertTrue(taskContainer.mSplitContainers.isEmpty()); } + @Test + public void testOnTransactionReady_taskFragmentAppeared() { + final TaskFragmentTransaction transaction = new TaskFragmentTransaction(); + final TaskFragmentInfo info = mock(TaskFragmentInfo.class); + transaction.addChange(new TaskFragmentTransaction.Change(TYPE_TASK_FRAGMENT_APPEARED) + .setTaskId(TASK_ID) + .setTaskFragmentToken(new Binder()) + .setTaskFragmentInfo(info)); + mSplitController.onTransactionReady(transaction); + + verify(mSplitController).onTaskFragmentAppeared(any(), eq(info)); + verify(mSplitPresenter).onTransactionHandled(eq(transaction.getTransactionToken()), any(), + anyInt(), anyBoolean()); + } + + @Test + public void testOnTransactionReady_taskFragmentInfoChanged() { + final TaskFragmentTransaction transaction = new TaskFragmentTransaction(); + final TaskFragmentInfo info = mock(TaskFragmentInfo.class); + transaction.addChange(new TaskFragmentTransaction.Change(TYPE_TASK_FRAGMENT_INFO_CHANGED) + .setTaskId(TASK_ID) + .setTaskFragmentToken(new Binder()) + .setTaskFragmentInfo(info)); + mSplitController.onTransactionReady(transaction); + + verify(mSplitController).onTaskFragmentInfoChanged(any(), eq(info)); + verify(mSplitPresenter).onTransactionHandled(eq(transaction.getTransactionToken()), any(), + anyInt(), anyBoolean()); + } + + @Test + public void testOnTransactionReady_taskFragmentVanished() { + final TaskFragmentTransaction transaction = new TaskFragmentTransaction(); + final TaskFragmentInfo info = mock(TaskFragmentInfo.class); + transaction.addChange(new TaskFragmentTransaction.Change(TYPE_TASK_FRAGMENT_VANISHED) + .setTaskId(TASK_ID) + .setTaskFragmentToken(new Binder()) + .setTaskFragmentInfo(info)); + mSplitController.onTransactionReady(transaction); + + verify(mSplitController).onTaskFragmentVanished(any(), eq(info)); + verify(mSplitPresenter).onTransactionHandled(eq(transaction.getTransactionToken()), any(), + anyInt(), anyBoolean()); + } + + @Test + public void testOnTransactionReady_taskFragmentParentInfoChanged() { + final TaskFragmentTransaction transaction = new TaskFragmentTransaction(); + final TaskFragmentParentInfo parentInfo = new TaskFragmentParentInfo(Configuration.EMPTY, + DEFAULT_DISPLAY, true); + transaction.addChange(new TaskFragmentTransaction.Change( + TYPE_TASK_FRAGMENT_PARENT_INFO_CHANGED) + .setTaskId(TASK_ID) + .setTaskFragmentParentInfo(parentInfo)); + mSplitController.onTransactionReady(transaction); + + verify(mSplitController).onTaskFragmentParentInfoChanged(any(), eq(TASK_ID), + eq(parentInfo)); + verify(mSplitPresenter).onTransactionHandled(eq(transaction.getTransactionToken()), any(), + anyInt(), anyBoolean()); + } + + @Test + public void testOnTransactionReady_taskFragmentParentError() { + final TaskFragmentTransaction transaction = new TaskFragmentTransaction(); + final IBinder errorToken = new Binder(); + final TaskFragmentInfo info = mock(TaskFragmentInfo.class); + final int opType = HIERARCHY_OP_TYPE_CREATE_TASK_FRAGMENT; + final Exception exception = new SecurityException("test"); + final Bundle errorBundle = TaskFragmentOrganizer.putErrorInfoInBundle(exception, info, + opType); + transaction.addChange(new TaskFragmentTransaction.Change(TYPE_TASK_FRAGMENT_ERROR) + .setErrorCallbackToken(errorToken) + .setErrorBundle(errorBundle)); + mSplitController.onTransactionReady(transaction); + + verify(mSplitController).onTaskFragmentError(any(), eq(errorToken), eq(info), eq(opType), + eq(exception)); + verify(mSplitPresenter).onTransactionHandled(eq(transaction.getTransactionToken()), any(), + anyInt(), anyBoolean()); + } + + @Test + public void testOnTransactionReady_activityReparentedToTask() { + final TaskFragmentTransaction transaction = new TaskFragmentTransaction(); + final Intent intent = mock(Intent.class); + final IBinder activityToken = new Binder(); + transaction.addChange(new TaskFragmentTransaction.Change(TYPE_ACTIVITY_REPARENTED_TO_TASK) + .setTaskId(TASK_ID) + .setActivityIntent(intent) + .setActivityToken(activityToken)); + mSplitController.onTransactionReady(transaction); + + verify(mSplitController).onActivityReparentedToTask(any(), eq(TASK_ID), eq(intent), + eq(activityToken)); + verify(mSplitPresenter).onTransactionHandled(eq(transaction.getTransactionToken()), any(), + anyInt(), anyBoolean()); + } + + @Test + public void testHasSamePresentation() { + SplitPairRule splitRule1 = new SplitPairRule.Builder( + activityPair -> true, + activityIntentPair -> true, + windowMetrics -> true) + .setFinishSecondaryWithPrimary(DEFAULT_FINISH_SECONDARY_WITH_PRIMARY) + .setFinishPrimaryWithSecondary(DEFAULT_FINISH_PRIMARY_WITH_SECONDARY) + .setDefaultSplitAttributes(SPLIT_ATTRIBUTES) + .build(); + SplitPairRule splitRule2 = new SplitPairRule.Builder( + activityPair -> true, + activityIntentPair -> true, + windowMetrics -> true) + .setFinishSecondaryWithPrimary(DEFAULT_FINISH_SECONDARY_WITH_PRIMARY) + .setFinishPrimaryWithSecondary(DEFAULT_FINISH_PRIMARY_WITH_SECONDARY) + .setDefaultSplitAttributes(SPLIT_ATTRIBUTES) + .build(); + + assertTrue("Rules must have same presentation if tags are null and has same properties.", + SplitController.haveSamePresentation(splitRule1, splitRule2, + new WindowMetrics(TASK_BOUNDS, WindowInsets.CONSUMED))); + + splitRule2 = new SplitPairRule.Builder( + activityPair -> true, + activityIntentPair -> true, + windowMetrics -> true) + .setFinishSecondaryWithPrimary(DEFAULT_FINISH_SECONDARY_WITH_PRIMARY) + .setFinishPrimaryWithSecondary(DEFAULT_FINISH_PRIMARY_WITH_SECONDARY) + .setDefaultSplitAttributes(SPLIT_ATTRIBUTES) + .setTag(TEST_TAG) + .build(); + + assertFalse("Rules must have different presentations if tags are not equal regardless" + + "of other properties", + SplitController.haveSamePresentation(splitRule1, splitRule2, + new WindowMetrics(TASK_BOUNDS, WindowInsets.CONSUMED))); + + + } + /** Creates a mock activity in the organizer process. */ private Activity createMockActivity() { final Activity activity = mock(Activity.class); @@ -956,6 +1160,7 @@ public class SplitControllerTest { doReturn(activity).when(mSplitController).getActivity(activityToken); doReturn(TASK_ID).when(activity).getTaskId(); doReturn(new ActivityInfo()).when(activity).getActivityInfo(); + doReturn(DEFAULT_DISPLAY).when(activity).getDisplayId(); return activity; } @@ -970,7 +1175,7 @@ public class SplitControllerTest { private void setupTaskFragmentInfo(@NonNull TaskFragmentContainer container, @NonNull Activity activity) { final TaskFragmentInfo info = createMockTaskFragmentInfo(container, activity); - container.setInfo(info); + container.setInfo(mTransaction, info); mSplitPresenter.mFragmentInfos.put(container.getTaskFragmentToken(), info); } @@ -994,7 +1199,7 @@ public class SplitControllerTest { private void setupPlaceholderRule(@NonNull Activity primaryActivity) { final SplitRule placeholderRule = new SplitPlaceholderRule.Builder(PLACEHOLDER_INTENT, primaryActivity::equals, i -> false, w -> true) - .setSplitRatio(SPLIT_RATIO) + .setDefaultSplitAttributes(SPLIT_ATTRIBUTES) .build(); mSplitController.setEmbeddingRules(Collections.singleton(placeholderRule)); } @@ -1047,7 +1252,8 @@ public class SplitControllerTest { primaryContainer, primaryContainer.getTopNonFinishingActivity(), secondaryContainer, - rule); + rule, + SPLIT_ATTRIBUTES); // We need to set those in case we are not respecting clear top. // TODO(b/231845476) we should always respect clearTop. diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java index d79319666c01..6dae0a1086b3 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java @@ -16,29 +16,35 @@ package androidx.window.extensions.embedding; +import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; +import static android.view.Display.DEFAULT_DISPLAY; +import static androidx.window.extensions.embedding.EmbeddingTestUtils.DEFAULT_FINISH_PRIMARY_WITH_SECONDARY; +import static androidx.window.extensions.embedding.EmbeddingTestUtils.DEFAULT_FINISH_SECONDARY_WITH_PRIMARY; +import static androidx.window.extensions.embedding.EmbeddingTestUtils.SPLIT_ATTRIBUTES; import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_BOUNDS; import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_ID; import static androidx.window.extensions.embedding.EmbeddingTestUtils.createActivityInfoWithMinDimensions; import static androidx.window.extensions.embedding.EmbeddingTestUtils.createMockTaskFragmentInfo; import static androidx.window.extensions.embedding.EmbeddingTestUtils.createSplitRule; +import static androidx.window.extensions.embedding.EmbeddingTestUtils.createWindowLayoutInfo; import static androidx.window.extensions.embedding.EmbeddingTestUtils.getSplitBounds; +import static androidx.window.extensions.embedding.SplitPresenter.EXPAND_CONTAINERS_ATTRIBUTES; import static androidx.window.extensions.embedding.SplitPresenter.POSITION_END; import static androidx.window.extensions.embedding.SplitPresenter.POSITION_FILL; import static androidx.window.extensions.embedding.SplitPresenter.POSITION_START; import static androidx.window.extensions.embedding.SplitPresenter.RESULT_EXPANDED; import static androidx.window.extensions.embedding.SplitPresenter.RESULT_EXPAND_FAILED_NO_TF_INFO; import static androidx.window.extensions.embedding.SplitPresenter.RESULT_NOT_EXPANDED; -import static androidx.window.extensions.embedding.SplitPresenter.getBoundsForPosition; import static androidx.window.extensions.embedding.SplitPresenter.getMinDimensions; -import static androidx.window.extensions.embedding.SplitPresenter.shouldShowSideBySide; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; @@ -65,6 +71,8 @@ import android.window.WindowContainerTransaction; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; +import androidx.window.extensions.layout.WindowLayoutComponentImpl; +import androidx.window.extensions.layout.WindowLayoutInfo; import org.junit.Before; import org.junit.Test; @@ -72,12 +80,16 @@ import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import java.util.ArrayList; + /** * Test class for {@link SplitPresenter}. * * Build/Install/Run: * atest WMJetpackUnitTests:SplitPresenterTest */ +// Suppress GuardedBy warning on unit tests +@SuppressWarnings("GuardedBy") @Presubmit @SmallTest @RunWith(AndroidJUnit4.class) @@ -91,13 +103,17 @@ public class SplitPresenterTest { private TaskFragmentInfo mTaskFragmentInfo; @Mock private WindowContainerTransaction mTransaction; + @Mock + private WindowLayoutComponentImpl mWindowLayoutComponent; private SplitController mController; private SplitPresenter mPresenter; @Before public void setUp() { MockitoAnnotations.initMocks(this); - mController = new SplitController(); + doReturn(new WindowLayoutInfo(new ArrayList<>())).when(mWindowLayoutComponent) + .getCurrentWindowLayoutInfo(anyInt(), any()); + mController = new SplitController(mWindowLayoutComponent); mPresenter = mController.mPresenter; spyOn(mController); spyOn(mPresenter); @@ -159,59 +175,263 @@ public class SplitPresenterTest { @Test public void testShouldShowSideBySide() { - Activity secondaryActivity = createMockActivity(); - final SplitRule splitRule = createSplitRule(mActivity, secondaryActivity); + assertTrue(SplitPresenter.shouldShowSplit(SPLIT_ATTRIBUTES)); - assertTrue(shouldShowSideBySide(TASK_BOUNDS, splitRule)); + final SplitAttributes expandContainers = new SplitAttributes.Builder() + .setSplitType(new SplitAttributes.SplitType.ExpandContainersSplitType()) + .build(); - // Set minDimensions of primary container to larger than primary bounds. - final Rect primaryBounds = getSplitBounds(true /* isPrimary */); - Pair<Size, Size> minDimensionsPair = new Pair<>( - new Size(primaryBounds.width() + 1, primaryBounds.height() + 1), null); + assertFalse(SplitPresenter.shouldShowSplit(expandContainers)); + } - assertFalse(shouldShowSideBySide(TASK_BOUNDS, splitRule, minDimensionsPair)); + @Test + public void testGetBoundsForPosition_expandContainers() { + final TaskContainer.TaskProperties taskProperties = getTaskProperty(); + final SplitAttributes splitAttributes = new SplitAttributes.Builder() + .setSplitType(new SplitAttributes.SplitType.ExpandContainersSplitType()) + .build(); + + assertEquals("Task bounds must be reported.", + new Rect(), + mPresenter.getBoundsForPosition(POSITION_START, taskProperties, splitAttributes)); + + assertEquals("Task bounds must be reported.", + new Rect(), + mPresenter.getBoundsForPosition(POSITION_END, taskProperties, splitAttributes)); + assertEquals("Task bounds must be reported.", + new Rect(), + mPresenter.getBoundsForPosition(POSITION_FILL, taskProperties, splitAttributes)); } @Test - public void testGetBoundsForPosition() { - Activity secondaryActivity = createMockActivity(); - final SplitRule splitRule = createSplitRule(mActivity, secondaryActivity); - final Rect primaryBounds = getSplitBounds(true /* isPrimary */); - final Rect secondaryBounds = getSplitBounds(false /* isPrimary */); + public void testGetBoundsForPosition_splitVertically() { + final Rect primaryBounds = getSplitBounds(true /* isPrimary */, + false /* splitHorizontally */); + final Rect secondaryBounds = getSplitBounds(false /* isPrimary */, + false /* splitHorizontally */); + final TaskContainer.TaskProperties taskProperties = getTaskProperty(); + SplitAttributes splitAttributes = new SplitAttributes.Builder() + .setSplitType(SplitAttributes.SplitType.RatioSplitType.splitEqually()) + .setLayoutDirection(SplitAttributes.LayoutDirection.LEFT_TO_RIGHT) + .build(); assertEquals("Primary bounds must be reported.", primaryBounds, - getBoundsForPosition(POSITION_START, TASK_BOUNDS, splitRule, - mActivity, null /* miniDimensionsPair */)); + mPresenter.getBoundsForPosition(POSITION_START, taskProperties, splitAttributes)); + + assertEquals("Secondary bounds must be reported.", + secondaryBounds, + mPresenter.getBoundsForPosition(POSITION_END, taskProperties, splitAttributes)); + assertEquals("Task bounds must be reported.", + new Rect(), + mPresenter.getBoundsForPosition(POSITION_FILL, taskProperties, splitAttributes)); + + splitAttributes = new SplitAttributes.Builder() + .setSplitType(SplitAttributes.SplitType.RatioSplitType.splitEqually()) + .setLayoutDirection(SplitAttributes.LayoutDirection.RIGHT_TO_LEFT) + .build(); + + assertEquals("Secondary bounds must be reported.", + secondaryBounds, + mPresenter.getBoundsForPosition(POSITION_START, taskProperties, splitAttributes)); + + assertEquals("Primary bounds must be reported.", + primaryBounds, + mPresenter.getBoundsForPosition(POSITION_END, taskProperties, splitAttributes)); + assertEquals("Task bounds must be reported.", + new Rect(), + mPresenter.getBoundsForPosition(POSITION_FILL, taskProperties, splitAttributes)); + + splitAttributes = new SplitAttributes.Builder() + .setSplitType(SplitAttributes.SplitType.RatioSplitType.splitEqually()) + .setLayoutDirection(SplitAttributes.LayoutDirection.LOCALE) + .build(); + // Layout direction should follow screen layout for SplitAttributes.LayoutDirection.LOCALE. + taskProperties.getConfiguration().screenLayout |= Configuration.SCREENLAYOUT_LAYOUTDIR_RTL; + + assertEquals("Secondary bounds must be reported.", + secondaryBounds, + mPresenter.getBoundsForPosition(POSITION_START, taskProperties, splitAttributes)); + + assertEquals("Primary bounds must be reported.", + primaryBounds, + mPresenter.getBoundsForPosition(POSITION_END, taskProperties, splitAttributes)); + assertEquals("Task bounds must be reported.", + new Rect(), + mPresenter.getBoundsForPosition(POSITION_FILL, taskProperties, splitAttributes)); + } + + @Test + public void testGetBoundsForPosition_splitHorizontally() { + final Rect primaryBounds = getSplitBounds(true /* isPrimary */, + true /* splitHorizontally */); + final Rect secondaryBounds = getSplitBounds(false /* isPrimary */, + true /* splitHorizontally */); + final TaskContainer.TaskProperties taskProperties = getTaskProperty(); + SplitAttributes splitAttributes = new SplitAttributes.Builder() + .setSplitType(SplitAttributes.SplitType.RatioSplitType.splitEqually()) + .setLayoutDirection(SplitAttributes.LayoutDirection.TOP_TO_BOTTOM) + .build(); + + assertEquals("Primary bounds must be reported.", + primaryBounds, + mPresenter.getBoundsForPosition(POSITION_START, taskProperties, splitAttributes)); + + assertEquals("Secondary bounds must be reported.", + secondaryBounds, + mPresenter.getBoundsForPosition(POSITION_END, taskProperties, splitAttributes)); + assertEquals("Task bounds must be reported.", + new Rect(), + mPresenter.getBoundsForPosition(POSITION_FILL, taskProperties, splitAttributes)); + + splitAttributes = new SplitAttributes.Builder() + .setSplitType(SplitAttributes.SplitType.RatioSplitType.splitEqually()) + .setLayoutDirection(SplitAttributes.LayoutDirection.BOTTOM_TO_TOP) + .build(); assertEquals("Secondary bounds must be reported.", secondaryBounds, - getBoundsForPosition(POSITION_END, TASK_BOUNDS, splitRule, - mActivity, null /* miniDimensionsPair */)); + mPresenter.getBoundsForPosition(POSITION_START, taskProperties, splitAttributes)); + + assertEquals("Primary bounds must be reported.", + primaryBounds, + mPresenter.getBoundsForPosition(POSITION_END, taskProperties, splitAttributes)); assertEquals("Task bounds must be reported.", new Rect(), - getBoundsForPosition(POSITION_FILL, TASK_BOUNDS, splitRule, - mActivity, null /* miniDimensionsPair */)); + mPresenter.getBoundsForPosition(POSITION_FILL, taskProperties, splitAttributes)); + } - Pair<Size, Size> minDimensionsPair = new Pair<>( - new Size(primaryBounds.width() + 1, primaryBounds.height() + 1), null); + @Test + public void testGetBoundsForPosition_useHingeFallback() { + final Rect primaryBounds = getSplitBounds(true /* isPrimary */, + false /* splitHorizontally */); + final Rect secondaryBounds = getSplitBounds(false /* isPrimary */, + false /* splitHorizontally */); + final TaskContainer.TaskProperties taskProperties = getTaskProperty(); + final SplitAttributes splitAttributes = new SplitAttributes.Builder() + .setSplitType(new SplitAttributes.SplitType.HingeSplitType( + SplitAttributes.SplitType.RatioSplitType.splitEqually() + )).setLayoutDirection(SplitAttributes.LayoutDirection.LEFT_TO_RIGHT) + .build(); + + // There's no hinge on the device. Use fallback SplitType. + doReturn(new WindowLayoutInfo(new ArrayList<>())).when(mWindowLayoutComponent) + .getCurrentWindowLayoutInfo(anyInt(), any()); + + assertEquals("PrimaryBounds must be reported.", + primaryBounds, + mPresenter.getBoundsForPosition(POSITION_START, taskProperties, splitAttributes)); - assertEquals("Fullscreen bounds must be reported because of min dimensions.", + assertEquals("SecondaryBounds must be reported.", + secondaryBounds, + mPresenter.getBoundsForPosition(POSITION_END, taskProperties, splitAttributes)); + assertEquals("Task bounds must be reported.", new Rect(), - getBoundsForPosition(POSITION_START, TASK_BOUNDS, - splitRule, mActivity, minDimensionsPair)); + mPresenter.getBoundsForPosition(POSITION_FILL, taskProperties, splitAttributes)); + + // Hinge is reported, but the host task is in multi-window mode. Still use fallback + // splitType. + doReturn(createWindowLayoutInfo()).when(mWindowLayoutComponent) + .getCurrentWindowLayoutInfo(anyInt(), any()); + taskProperties.getConfiguration().windowConfiguration + .setWindowingMode(WINDOWING_MODE_MULTI_WINDOW); + + assertEquals("PrimaryBounds must be reported.", + primaryBounds, + mPresenter.getBoundsForPosition(POSITION_START, taskProperties, splitAttributes)); + + assertEquals("SecondaryBounds must be reported.", + secondaryBounds, + mPresenter.getBoundsForPosition(POSITION_END, taskProperties, splitAttributes)); + assertEquals("Task bounds must be reported.", + new Rect(), + mPresenter.getBoundsForPosition(POSITION_FILL, taskProperties, splitAttributes)); + + // Hinge is reported, and the host task is in fullscreen, but layout direction doesn't match + // folding area orientation. Still use fallback splitType. + doReturn(createWindowLayoutInfo()).when(mWindowLayoutComponent) + .getCurrentWindowLayoutInfo(anyInt(), any()); + taskProperties.getConfiguration().windowConfiguration + .setWindowingMode(WINDOWING_MODE_FULLSCREEN); + + assertEquals("PrimaryBounds must be reported.", + primaryBounds, + mPresenter.getBoundsForPosition(POSITION_START, taskProperties, splitAttributes)); + + assertEquals("SecondaryBounds must be reported.", + secondaryBounds, + mPresenter.getBoundsForPosition(POSITION_END, taskProperties, splitAttributes)); + assertEquals("Task bounds must be reported.", + new Rect(), + mPresenter.getBoundsForPosition(POSITION_FILL, taskProperties, splitAttributes)); + } + + @Test + public void testGetBoundsForPosition_fallbackToExpandContainers() { + final TaskContainer.TaskProperties taskProperties = getTaskProperty(); + final SplitAttributes splitAttributes = new SplitAttributes.Builder() + .setSplitType(new SplitAttributes.SplitType.HingeSplitType( + new SplitAttributes.SplitType.ExpandContainersSplitType() + )).setLayoutDirection(SplitAttributes.LayoutDirection.LEFT_TO_RIGHT) + .build(); + + assertEquals("Task bounds must be reported.", + new Rect(), + mPresenter.getBoundsForPosition(POSITION_START, taskProperties, splitAttributes)); + + assertEquals("Task bounds must be reported.", + new Rect(), + mPresenter.getBoundsForPosition(POSITION_END, taskProperties, splitAttributes)); + assertEquals("Task bounds must be reported.", + new Rect(), + mPresenter.getBoundsForPosition(POSITION_FILL, taskProperties, splitAttributes)); + } + + @Test + public void testGetBoundsForPosition_useHingeSplitType() { + final TaskContainer.TaskProperties taskProperties = getTaskProperty(); + final SplitAttributes splitAttributes = new SplitAttributes.Builder() + .setSplitType(new SplitAttributes.SplitType.HingeSplitType( + new SplitAttributes.SplitType.ExpandContainersSplitType() + )).setLayoutDirection(SplitAttributes.LayoutDirection.TOP_TO_BOTTOM) + .build(); + final WindowLayoutInfo windowLayoutInfo = createWindowLayoutInfo(); + doReturn(windowLayoutInfo).when(mWindowLayoutComponent) + .getCurrentWindowLayoutInfo(anyInt(), any()); + final Rect hingeBounds = windowLayoutInfo.getDisplayFeatures().get(0).getBounds(); + final Rect primaryBounds = new Rect( + TASK_BOUNDS.left, + TASK_BOUNDS.top, + TASK_BOUNDS.right, + hingeBounds.top + ); + final Rect secondaryBounds = new Rect( + TASK_BOUNDS.left, + hingeBounds.bottom, + TASK_BOUNDS.right, + TASK_BOUNDS.bottom + ); + + assertEquals("PrimaryBounds must be reported.", + primaryBounds, + mPresenter.getBoundsForPosition(POSITION_START, taskProperties, splitAttributes)); + + assertEquals("SecondaryBounds must be reported.", + secondaryBounds, + mPresenter.getBoundsForPosition(POSITION_END, taskProperties, splitAttributes)); + assertEquals("Task bounds must be reported.", + new Rect(), + mPresenter.getBoundsForPosition(POSITION_FILL, taskProperties, splitAttributes)); } @Test public void testExpandSplitContainerIfNeeded() { - SplitContainer splitContainer = mock(SplitContainer.class); Activity secondaryActivity = createMockActivity(); SplitRule splitRule = createSplitRule(mActivity, secondaryActivity); TaskFragmentContainer primaryTf = mController.newContainer(mActivity, TASK_ID); TaskFragmentContainer secondaryTf = mController.newContainer(secondaryActivity, TASK_ID); - doReturn(splitRule).when(splitContainer).getSplitRule(); - doReturn(primaryTf).when(splitContainer).getPrimaryContainer(); - doReturn(secondaryTf).when(splitContainer).getSecondaryContainer(); + SplitContainer splitContainer = new SplitContainer(primaryTf, secondaryActivity, + secondaryTf, splitRule, SPLIT_ATTRIBUTES); assertThrows(IllegalArgumentException.class, () -> mPresenter.expandSplitContainerIfNeeded(mTransaction, splitContainer, mActivity, @@ -221,31 +441,95 @@ public class SplitPresenterTest { splitContainer, mActivity, secondaryActivity, null /* secondaryIntent */)); verify(mPresenter, never()).expandTaskFragment(any(), any()); + splitContainer.setSplitAttributes(SPLIT_ATTRIBUTES); doReturn(createActivityInfoWithMinDimensions()).when(secondaryActivity).getActivityInfo(); assertEquals(RESULT_EXPAND_FAILED_NO_TF_INFO, mPresenter.expandSplitContainerIfNeeded( mTransaction, splitContainer, mActivity, secondaryActivity, null /* secondaryIntent */)); - primaryTf.setInfo(createMockTaskFragmentInfo(primaryTf, mActivity)); - secondaryTf.setInfo(createMockTaskFragmentInfo(secondaryTf, secondaryActivity)); + splitContainer.setSplitAttributes(SPLIT_ATTRIBUTES); + primaryTf.setInfo(mTransaction, createMockTaskFragmentInfo(primaryTf, mActivity)); + secondaryTf.setInfo(mTransaction, + createMockTaskFragmentInfo(secondaryTf, secondaryActivity)); assertEquals(RESULT_EXPANDED, mPresenter.expandSplitContainerIfNeeded(mTransaction, splitContainer, mActivity, secondaryActivity, null /* secondaryIntent */)); - verify(mPresenter).expandTaskFragment(eq(mTransaction), - eq(primaryTf.getTaskFragmentToken())); - verify(mPresenter).expandTaskFragment(eq(mTransaction), - eq(secondaryTf.getTaskFragmentToken())); + verify(mPresenter).expandTaskFragment(mTransaction, primaryTf.getTaskFragmentToken()); + verify(mPresenter).expandTaskFragment(mTransaction, secondaryTf.getTaskFragmentToken()); + splitContainer.setSplitAttributes(SPLIT_ATTRIBUTES); clearInvocations(mPresenter); assertEquals(RESULT_EXPANDED, mPresenter.expandSplitContainerIfNeeded(mTransaction, splitContainer, mActivity, null /* secondaryActivity */, new Intent(ApplicationProvider.getApplicationContext(), MinimumDimensionActivity.class))); - verify(mPresenter).expandTaskFragment(eq(mTransaction), - eq(primaryTf.getTaskFragmentToken())); - verify(mPresenter).expandTaskFragment(eq(mTransaction), - eq(secondaryTf.getTaskFragmentToken())); + verify(mPresenter).expandTaskFragment(mTransaction, primaryTf.getTaskFragmentToken()); + verify(mPresenter).expandTaskFragment(mTransaction, secondaryTf.getTaskFragmentToken()); + } + + @Test + public void testCreateNewSplitContainer_secondaryAbovePrimary() { + final Activity secondaryActivity = createMockActivity(); + final TaskFragmentContainer bottomTf = mController.newContainer(secondaryActivity, TASK_ID); + final TaskFragmentContainer primaryTf = mController.newContainer(mActivity, TASK_ID); + final SplitPairRule rule = new SplitPairRule.Builder(pair -> + pair.first == mActivity && pair.second == secondaryActivity, pair -> false, + metrics -> true) + .setDefaultSplitAttributes(SPLIT_ATTRIBUTES) + .setShouldClearTop(false) + .build(); + + mPresenter.createNewSplitContainer(mTransaction, mActivity, secondaryActivity, rule); + + assertEquals(primaryTf, mController.getContainerWithActivity(mActivity)); + final TaskFragmentContainer secondaryTf = mController.getContainerWithActivity( + secondaryActivity); + assertNotEquals(bottomTf, secondaryTf); + assertTrue(secondaryTf.isAbove(primaryTf)); + } + + @Test + public void testComputeSplitAttributes() { + final SplitPairRule splitPairRule = new SplitPairRule.Builder( + activityPair -> true, + activityIntentPair -> true, + windowMetrics -> windowMetrics.getBounds().equals(TASK_BOUNDS)) + .setFinishSecondaryWithPrimary(DEFAULT_FINISH_SECONDARY_WITH_PRIMARY) + .setFinishPrimaryWithSecondary(DEFAULT_FINISH_PRIMARY_WITH_SECONDARY) + .setDefaultSplitAttributes(SPLIT_ATTRIBUTES) + .build(); + final TaskContainer.TaskProperties taskProperties = getTaskProperty(); + + assertEquals(SPLIT_ATTRIBUTES, mPresenter.computeSplitAttributes(taskProperties, + splitPairRule, null /* minDimensionsPair */)); + + final Pair<Size, Size> minDimensionsPair = new Pair<>( + new Size(TASK_BOUNDS.width(), TASK_BOUNDS.height()), null); + + assertEquals(EXPAND_CONTAINERS_ATTRIBUTES, mPresenter.computeSplitAttributes(taskProperties, + splitPairRule, minDimensionsPair)); + + taskProperties.getConfiguration().windowConfiguration.setBounds(new Rect( + TASK_BOUNDS.left + 1, TASK_BOUNDS.top + 1, TASK_BOUNDS.right + 1, + TASK_BOUNDS.bottom + 1)); + + assertEquals(EXPAND_CONTAINERS_ATTRIBUTES, mPresenter.computeSplitAttributes(taskProperties, + splitPairRule, null /* minDimensionsPair */)); + + final SplitAttributes splitAttributes = new SplitAttributes.Builder() + .setSplitType( + new SplitAttributes.SplitType.HingeSplitType( + SplitAttributes.SplitType.RatioSplitType.splitEqually() + ) + ).build(); + + mController.setSplitAttributesCalculator(params -> { + return splitAttributes; + }); + + assertEquals(splitAttributes, mPresenter.computeSplitAttributes(taskProperties, + splitPairRule, null /* minDimensionsPair */)); } private Activity createMockActivity() { @@ -259,4 +543,10 @@ public class SplitPresenterTest { doReturn(mock(IBinder.class)).when(activity).getActivityToken(); return activity; } + + private static TaskContainer.TaskProperties getTaskProperty() { + final Configuration configuration = new Configuration(); + configuration.windowConfiguration.setBounds(TASK_BOUNDS); + return new TaskContainer.TaskProperties(DEFAULT_DISPLAY, configuration); + } } 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 dd67e48ef353..af9c6ba5c162 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 @@ -21,9 +21,10 @@ 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.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; +import static android.view.Display.DEFAULT_DISPLAY; import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_BOUNDS; -import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_ID; +import static androidx.window.extensions.embedding.EmbeddingTestUtils.createTestTaskContainer; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -34,8 +35,10 @@ import static org.mockito.Mockito.mock; import android.app.Activity; import android.content.Intent; +import android.content.res.Configuration; import android.graphics.Rect; import android.platform.test.annotations.Presubmit; +import android.window.TaskFragmentParentInfo; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; @@ -66,7 +69,7 @@ public class TaskContainerTest { @Test public void testIsTaskBoundsInitialized() { - final TaskContainer taskContainer = new TaskContainer(TASK_ID); + final TaskContainer taskContainer = createTestTaskContainer(); assertFalse(taskContainer.isTaskBoundsInitialized()); @@ -77,7 +80,7 @@ public class TaskContainerTest { @Test public void testSetTaskBounds() { - final TaskContainer taskContainer = new TaskContainer(TASK_ID); + final TaskContainer taskContainer = createTestTaskContainer(); assertFalse(taskContainer.setTaskBounds(new Rect())); @@ -87,30 +90,24 @@ public class TaskContainerTest { } @Test - public void testIsWindowingModeInitialized() { - final TaskContainer taskContainer = new TaskContainer(TASK_ID); - - assertFalse(taskContainer.isWindowingModeInitialized()); - - taskContainer.setWindowingMode(WINDOWING_MODE_FULLSCREEN); - - assertTrue(taskContainer.isWindowingModeInitialized()); - } - - @Test public void testGetWindowingModeForSplitTaskFragment() { - final TaskContainer taskContainer = new TaskContainer(TASK_ID); + final TaskContainer taskContainer = createTestTaskContainer(); final Rect splitBounds = new Rect(0, 0, 500, 1000); + final Configuration configuration = new Configuration(); assertEquals(WINDOWING_MODE_MULTI_WINDOW, taskContainer.getWindowingModeForSplitTaskFragment(splitBounds)); - taskContainer.setWindowingMode(WINDOWING_MODE_FULLSCREEN); + configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN); + taskContainer.updateTaskFragmentParentInfo(new TaskFragmentParentInfo(configuration, + DEFAULT_DISPLAY, true /* visible */)); assertEquals(WINDOWING_MODE_MULTI_WINDOW, taskContainer.getWindowingModeForSplitTaskFragment(splitBounds)); - taskContainer.setWindowingMode(WINDOWING_MODE_FREEFORM); + configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM); + taskContainer.updateTaskFragmentParentInfo(new TaskFragmentParentInfo(configuration, + DEFAULT_DISPLAY, true /* visible */)); assertEquals(WINDOWING_MODE_FREEFORM, taskContainer.getWindowingModeForSplitTaskFragment(splitBounds)); @@ -123,22 +120,27 @@ public class TaskContainerTest { @Test public void testIsInPictureInPicture() { - final TaskContainer taskContainer = new TaskContainer(TASK_ID); + final TaskContainer taskContainer = createTestTaskContainer(); + final Configuration configuration = new Configuration(); assertFalse(taskContainer.isInPictureInPicture()); - taskContainer.setWindowingMode(WINDOWING_MODE_FULLSCREEN); + configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN); + taskContainer.updateTaskFragmentParentInfo(new TaskFragmentParentInfo(configuration, + DEFAULT_DISPLAY, true /* visible */)); assertFalse(taskContainer.isInPictureInPicture()); - taskContainer.setWindowingMode(WINDOWING_MODE_PINNED); + configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_PINNED); + taskContainer.updateTaskFragmentParentInfo(new TaskFragmentParentInfo(configuration, + DEFAULT_DISPLAY, true /* visible */)); assertTrue(taskContainer.isInPictureInPicture()); } @Test public void testIsEmpty() { - final TaskContainer taskContainer = new TaskContainer(TASK_ID); + final TaskContainer taskContainer = createTestTaskContainer(); assertTrue(taskContainer.isEmpty()); @@ -155,7 +157,7 @@ public class TaskContainerTest { @Test public void testGetTopTaskFragmentContainer() { - final TaskContainer taskContainer = new TaskContainer(TASK_ID); + final TaskContainer taskContainer = createTestTaskContainer(); assertNull(taskContainer.getTopTaskFragmentContainer()); final TaskFragmentContainer tf0 = new TaskFragmentContainer(null /* activity */, @@ -169,7 +171,7 @@ public class TaskContainerTest { @Test public void testGetTopNonFinishingActivity() { - final TaskContainer taskContainer = new TaskContainer(TASK_ID); + final TaskContainer taskContainer = createTestTaskContainer(); assertNull(taskContainer.getTopNonFinishingActivity()); final TaskFragmentContainer tf0 = mock(TaskFragmentContainer.class); diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java index 28c2773e25cb..35415d816d8b 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java @@ -16,9 +16,11 @@ package androidx.window.extensions.embedding; -import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_ID; import static androidx.window.extensions.embedding.EmbeddingTestUtils.createMockTaskFragmentInfo; +import static androidx.window.extensions.embedding.EmbeddingTestUtils.createTestTaskContainer; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; import static org.junit.Assert.assertEquals; @@ -36,7 +38,6 @@ import static org.mockito.Mockito.never; import android.app.Activity; import android.content.Intent; import android.os.Binder; -import android.os.Handler; import android.os.IBinder; import android.platform.test.annotations.Presubmit; import android.window.TaskFragmentInfo; @@ -62,32 +63,34 @@ import java.util.List; * Build/Install/Run: * atest WMJetpackUnitTests:TaskFragmentContainerTest */ +// Suppress GuardedBy warning on unit tests +@SuppressWarnings("GuardedBy") @Presubmit @SmallTest @RunWith(AndroidJUnit4.class) public class TaskFragmentContainerTest { @Mock private SplitPresenter mPresenter; - @Mock private SplitController mController; @Mock private TaskFragmentInfo mInfo; @Mock - private Handler mHandler; + private WindowContainerTransaction mTransaction; private Activity mActivity; private Intent mIntent; @Before public void setup() { MockitoAnnotations.initMocks(this); - doReturn(mHandler).when(mController).getHandler(); + mController = new SplitController(); + spyOn(mController); mActivity = createMockActivity(); mIntent = new Intent(); } @Test public void testNewContainer() { - final TaskContainer taskContainer = new TaskContainer(TASK_ID); + final TaskContainer taskContainer = createTestTaskContainer(); // One of the activity and the intent must be non-null assertThrows(IllegalArgumentException.class, @@ -100,44 +103,43 @@ public class TaskFragmentContainerTest { @Test public void testFinish() { - final TaskContainer taskContainer = new TaskContainer(TASK_ID); + final TaskContainer taskContainer = createTestTaskContainer(); final TaskFragmentContainer container = new TaskFragmentContainer(mActivity, null /* pendingAppearedIntent */, taskContainer, mController); doReturn(container).when(mController).getContainerWithActivity(mActivity); - final WindowContainerTransaction wct = new WindowContainerTransaction(); // Only remove the activity, but not clear the reference until appeared. - container.finish(true /* shouldFinishDependent */, mPresenter, wct, mController); + container.finish(true /* shouldFinishDependent */, mPresenter, mTransaction, mController); - verify(mActivity).finish(); + verify(mTransaction).finishActivity(mActivity.getActivityToken()); verify(mPresenter, never()).deleteTaskFragment(any(), any()); verify(mController, never()).removeContainer(any()); // Calling twice should not finish activity again. - clearInvocations(mActivity); - container.finish(true /* shouldFinishDependent */, mPresenter, wct, mController); + clearInvocations(mTransaction); + container.finish(true /* shouldFinishDependent */, mPresenter, mTransaction, mController); - verify(mActivity, never()).finish(); + verify(mTransaction, never()).finishActivity(any()); verify(mPresenter, never()).deleteTaskFragment(any(), any()); verify(mController, never()).removeContainer(any()); // Remove all references after the container has appeared in server. doReturn(new ArrayList<>()).when(mInfo).getActivities(); - container.setInfo(mInfo); - container.finish(true /* shouldFinishDependent */, mPresenter, wct, mController); + container.setInfo(mTransaction, mInfo); + container.finish(true /* shouldFinishDependent */, mPresenter, mTransaction, mController); - verify(mActivity, never()).finish(); - verify(mPresenter).deleteTaskFragment(wct, container.getTaskFragmentToken()); + verify(mTransaction, never()).finishActivity(any()); + verify(mPresenter).deleteTaskFragment(mTransaction, container.getTaskFragmentToken()); verify(mController).removeContainer(container); } @Test public void testFinish_notFinishActivityThatIsReparenting() { - final TaskContainer taskContainer = new TaskContainer(TASK_ID); + final TaskContainer taskContainer = createTestTaskContainer(); final TaskFragmentContainer container0 = new TaskFragmentContainer(mActivity, null /* pendingAppearedIntent */, taskContainer, mController); final TaskFragmentInfo info = createMockTaskFragmentInfo(container0, mActivity); - container0.setInfo(info); + container0.setInfo(mTransaction, info); // Request to reparent the activity to a new TaskFragment. final TaskFragmentContainer container1 = new TaskFragmentContainer(mActivity, null /* pendingAppearedIntent */, taskContainer, mController); @@ -147,14 +149,14 @@ public class TaskFragmentContainerTest { // The activity is requested to be reparented, so don't finish it. container0.finish(true /* shouldFinishDependent */, mPresenter, wct, mController); - verify(mActivity, never()).finish(); + verify(mTransaction, never()).finishActivity(any()); verify(mPresenter).deleteTaskFragment(wct, container0.getTaskFragmentToken()); verify(mController).removeContainer(container0); } @Test public void testSetInfo() { - final TaskContainer taskContainer = new TaskContainer(TASK_ID); + final TaskContainer taskContainer = createTestTaskContainer(); // Pending activity should be cleared when it has appeared on server side. final TaskFragmentContainer pendingActivityContainer = new TaskFragmentContainer(mActivity, null /* pendingAppearedIntent */, taskContainer, mController); @@ -163,7 +165,7 @@ public class TaskFragmentContainerTest { final TaskFragmentInfo info0 = createMockTaskFragmentInfo(pendingActivityContainer, mActivity); - pendingActivityContainer.setInfo(info0); + pendingActivityContainer.setInfo(mTransaction, info0); assertTrue(pendingActivityContainer.mPendingAppearedActivities.isEmpty()); @@ -175,14 +177,14 @@ public class TaskFragmentContainerTest { final TaskFragmentInfo info1 = createMockTaskFragmentInfo(pendingIntentContainer, mActivity); - pendingIntentContainer.setInfo(info1); + pendingIntentContainer.setInfo(mTransaction, info1); assertNull(pendingIntentContainer.getPendingAppearedIntent()); } @Test public void testIsWaitingActivityAppear() { - final TaskContainer taskContainer = new TaskContainer(TASK_ID); + final TaskContainer taskContainer = createTestTaskContainer(); final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */, mIntent, taskContainer, mController); @@ -191,49 +193,51 @@ public class TaskFragmentContainerTest { final TaskFragmentInfo info = mock(TaskFragmentInfo.class); doReturn(new ArrayList<>()).when(info).getActivities(); doReturn(true).when(info).isEmpty(); - container.setInfo(info); + container.setInfo(mTransaction, info); assertTrue(container.isWaitingActivityAppear()); doReturn(false).when(info).isEmpty(); - container.setInfo(info); + container.setInfo(mTransaction, info); assertFalse(container.isWaitingActivityAppear()); } @Test public void testAppearEmptyTimeout() { - final TaskContainer taskContainer = new TaskContainer(TASK_ID); + doNothing().when(mController).onTaskFragmentAppearEmptyTimeout(any(), any()); + final TaskContainer taskContainer = createTestTaskContainer(); final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */, mIntent, taskContainer, mController); assertNull(container.mAppearEmptyTimeout); - // Not set if it is not appeared empty. - final TaskFragmentInfo info = mock(TaskFragmentInfo.class); - doReturn(new ArrayList<>()).when(info).getActivities(); - doReturn(false).when(info).isEmpty(); - container.setInfo(info); - - assertNull(container.mAppearEmptyTimeout); - // Set timeout if the first info set is empty. + final TaskFragmentInfo info = mock(TaskFragmentInfo.class); container.mInfo = null; doReturn(true).when(info).isEmpty(); - container.setInfo(info); + container.setInfo(mTransaction, info); assertNotNull(container.mAppearEmptyTimeout); + // Not set if it is not appeared empty. + doReturn(new ArrayList<>()).when(info).getActivities(); + doReturn(false).when(info).isEmpty(); + container.setInfo(mTransaction, info); + + assertNull(container.mAppearEmptyTimeout); + // Remove timeout after the container becomes non-empty. doReturn(false).when(info).isEmpty(); - container.setInfo(info); + container.setInfo(mTransaction, info); assertNull(container.mAppearEmptyTimeout); // Running the timeout will call into SplitController.onTaskFragmentAppearEmptyTimeout. container.mInfo = null; + container.setPendingAppearedIntent(mIntent); doReturn(true).when(info).isEmpty(); - container.setInfo(info); + container.setInfo(mTransaction, info); container.mAppearEmptyTimeout.run(); assertNull(container.mAppearEmptyTimeout); @@ -242,7 +246,7 @@ public class TaskFragmentContainerTest { @Test public void testCollectNonFinishingActivities() { - final TaskContainer taskContainer = new TaskContainer(TASK_ID); + final TaskContainer taskContainer = createTestTaskContainer(); final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */, mIntent, taskContainer, mController); List<Activity> activities = container.collectNonFinishingActivities(); @@ -259,7 +263,7 @@ public class TaskFragmentContainerTest { final List<IBinder> runningActivities = Lists.newArrayList(activity0.getActivityToken(), activity1.getActivityToken()); doReturn(runningActivities).when(mInfo).getActivities(); - container.setInfo(mInfo); + container.setInfo(mTransaction, mInfo); activities = container.collectNonFinishingActivities(); assertEquals(3, activities.size()); @@ -270,7 +274,7 @@ public class TaskFragmentContainerTest { @Test public void testAddPendingActivity() { - final TaskContainer taskContainer = new TaskContainer(TASK_ID); + final TaskContainer taskContainer = createTestTaskContainer(); final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */, mIntent, taskContainer, mController); container.addPendingAppearedActivity(mActivity); @@ -283,8 +287,20 @@ public class TaskFragmentContainerTest { } @Test + public void testIsAbove() { + final TaskContainer taskContainer = createTestTaskContainer(); + final TaskFragmentContainer container0 = new TaskFragmentContainer(null /* activity */, + mIntent, taskContainer, mController); + final TaskFragmentContainer container1 = new TaskFragmentContainer(null /* activity */, + mIntent, taskContainer, mController); + + assertTrue(container1.isAbove(container0)); + assertFalse(container0.isAbove(container1)); + } + + @Test public void testGetBottomMostActivity() { - final TaskContainer taskContainer = new TaskContainer(TASK_ID); + final TaskContainer taskContainer = createTestTaskContainer(); final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */, mIntent, taskContainer, mController); container.addPendingAppearedActivity(mActivity); @@ -294,11 +310,30 @@ public class TaskFragmentContainerTest { final Activity activity = createMockActivity(); final List<IBinder> runningActivities = Lists.newArrayList(activity.getActivityToken()); doReturn(runningActivities).when(mInfo).getActivities(); - container.setInfo(mInfo); + container.setInfo(mTransaction, mInfo); assertEquals(activity, container.getBottomMostActivity()); } + @Test + public void testOnActivityDestroyed() { + final TaskContainer taskContainer = createTestTaskContainer(); + final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */, + mIntent, taskContainer, mController); + container.addPendingAppearedActivity(mActivity); + final List<IBinder> activities = new ArrayList<>(); + activities.add(mActivity.getActivityToken()); + doReturn(activities).when(mInfo).getActivities(); + container.setInfo(mTransaction, mInfo); + + assertTrue(container.hasActivity(mActivity.getActivityToken())); + + taskContainer.onActivityDestroyed(mActivity); + + // It should not contain the destroyed Activity. + assertFalse(container.hasActivity(mActivity.getActivityToken())); + } + /** Creates a mock activity in the organizer process. */ private Activity createMockActivity() { final Activity activity = mock(Activity.class); diff --git a/libs/WindowManager/Jetpack/window-extensions-release.aar b/libs/WindowManager/Jetpack/window-extensions-release.aar Binary files differindex f54ab08d8a8a..2c766d81d611 100644 --- a/libs/WindowManager/Jetpack/window-extensions-release.aar +++ b/libs/WindowManager/Jetpack/window-extensions-release.aar diff --git a/libs/WindowManager/Shell/res/color/decor_button_dark_color.xml b/libs/WindowManager/Shell/res/color/decor_button_dark_color.xml new file mode 100644 index 000000000000..bf325bd84c00 --- /dev/null +++ b/libs/WindowManager/Shell/res/color/decor_button_dark_color.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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. + --> +<selector xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto"> + <item app:state_task_focused="true" android:color="#FF000000" /> + <item android:color="#33000000" /> +</selector> diff --git a/libs/WindowManager/Shell/res/color/decor_button_light_color.xml b/libs/WindowManager/Shell/res/color/decor_button_light_color.xml new file mode 100644 index 000000000000..2e48bca7786a --- /dev/null +++ b/libs/WindowManager/Shell/res/color/decor_button_light_color.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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. + --> +<selector xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto"> + <item app:state_task_focused="true" android:color="#FFFFFFFF" /> + <item android:color="#33FFFFFF" /> +</selector>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/color/decor_caption_title_color.xml b/libs/WindowManager/Shell/res/color/decor_caption_title_color.xml new file mode 100644 index 000000000000..1ecc13e4da38 --- /dev/null +++ b/libs/WindowManager/Shell/res/color/decor_caption_title_color.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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. + --> +<selector xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto"> + <!-- Fading the to 85% blackness --> + <item app:state_task_focused="true" android:color="#D8D8D8" /> + <!-- Fading the to 95% blackness --> + <item android:color="#F2F2F2" /> +</selector> diff --git a/libs/WindowManager/Shell/res/color/taskbar_background.xml b/libs/WindowManager/Shell/res/color/taskbar_background.xml index 329e5b9b31a0..b3d260299106 100644 --- a/libs/WindowManager/Shell/res/color/taskbar_background.xml +++ b/libs/WindowManager/Shell/res/color/taskbar_background.xml @@ -14,6 +14,7 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License. --> +<!-- Should be the same as in packages/apps/Launcher3/res/color-v31/taskbar_background.xml --> <selector xmlns:android="http://schemas.android.com/apk/res/android"> - <item android:color="@android:color/system_neutral1_500" android:lStar="35" /> + <item android:color="@android:color/system_neutral1_500" android:lStar="15" /> </selector>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/drawable/decor_caption_title.xml b/libs/WindowManager/Shell/res/drawable/decor_caption_title.xml new file mode 100644 index 000000000000..8207365a737d --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/decor_caption_title.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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. + --> +<shape android:shape="rectangle" + android:tintMode="multiply" + android:tint="@color/decor_caption_title_color" + xmlns:android="http://schemas.android.com/apk/res/android"> + <solid android:color="?android:attr/colorPrimary" /> +</shape> diff --git a/libs/WindowManager/Shell/res/drawable/decor_close_button_dark.xml b/libs/WindowManager/Shell/res/drawable/decor_close_button_dark.xml new file mode 100644 index 000000000000..f2f1a1d55dee --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/decor_close_button_dark.xml @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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. + --> +<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" + > + <group android:scaleX="0.5" + android:scaleY="0.5" + android:translateX="8.0" + android:translateY="8.0" > + <path + android:fillColor="@android:color/white" + android:pathData="M6.9,4.0l-2.9,2.9 9.1,9.1 -9.1,9.200001 2.9,2.799999 9.1,-9.1 9.1,9.1 2.9,-2.799999 -9.1,-9.200001 9.1,-9.1 -2.9,-2.9 -9.1,9.2z"/> + </group> +</vector> diff --git a/libs/WindowManager/Shell/res/drawable/decor_maximize_button_dark.xml b/libs/WindowManager/Shell/res/drawable/decor_maximize_button_dark.xml new file mode 100644 index 000000000000..ab4e29ac97e5 --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/decor_maximize_button_dark.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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. + --> +<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"> + <group android:scaleX="0.5" + android:scaleY="0.5" + android:translateX="8.0" + android:translateY="8.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"/> + </group> +</vector> + + diff --git a/libs/WindowManager/Shell/res/drawable/decor_minimize_button_dark.xml b/libs/WindowManager/Shell/res/drawable/decor_minimize_button_dark.xml new file mode 100644 index 000000000000..0bcaa530dc80 --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/decor_minimize_button_dark.xml @@ -0,0 +1,24 @@ +<!-- + ~ 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. + --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24" + android:tint="?attr/colorControlNormal"> + <path + android:fillColor="@android:color/white" android:pathData="M6,21V19H18V21Z"/> +</vector> diff --git a/libs/WindowManager/Shell/res/drawable/letterbox_education_ic_letterboxed_app.xml b/libs/WindowManager/Shell/res/drawable/letterbox_education_ic_light_bulb.xml index 6fcd1de892a3..ddfb5c27e701 100644 --- a/libs/WindowManager/Shell/res/drawable/letterbox_education_ic_letterboxed_app.xml +++ b/libs/WindowManager/Shell/res/drawable/letterbox_education_ic_light_bulb.xml @@ -15,15 +15,12 @@ ~ limitations under the License. --> <vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="@dimen/letterbox_education_dialog_icon_size" - android:height="@dimen/letterbox_education_dialog_icon_size" - android:viewportWidth="48" - android:viewportHeight="48"> + 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="@color/letterbox_education_accent_primary" - android:fillType="evenOdd" - android:pathData="M2 8C0.895431 8 0 8.89543 0 10V38C0 39.1046 0.895431 40 2 40H46C47.1046 40 48 39.1046 48 38V10C48 8.89543 47.1046 8 46 8H2ZM44 12H4V36H44V12Z" /> - <path - android:fillColor="@color/letterbox_education_accent_primary" - android:pathData="M 17 14 L 31 14 Q 32 14 32 15 L 32 33 Q 32 34 31 34 L 17 34 Q 16 34 16 33 L 16 15 Q 16 14 17 14 Z" /> + 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" /> </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 cbfcfd06e3b7..22a8f39ca687 100644 --- a/libs/WindowManager/Shell/res/drawable/letterbox_education_ic_reposition.xml +++ b/libs/WindowManager/Shell/res/drawable/letterbox_education_ic_reposition.xml @@ -15,18 +15,16 @@ ~ limitations under the License. --> <vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="@dimen/letterbox_education_dialog_icon_size" - android:height="@dimen/letterbox_education_dialog_icon_size" - android:viewportWidth="48" - android:viewportHeight="48"> + android:width="@dimen/letterbox_education_dialog_icon_width" + android:height="@dimen/letterbox_education_dialog_icon_height" + android:viewportWidth="40" + android:viewportHeight="32"> + <path android:fillColor="@color/letterbox_education_text_secondary" android:fillType="evenOdd" - android:pathData="M2 8C0.895431 8 0 8.89543 0 10V38C0 39.1046 0.895431 40 2 40H46C47.1046 40 48 39.1046 48 38V10C48 8.89543 47.1046 8 46 8H2ZM44 12H4V36H44V12Z" /> + 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" /> <path android:fillColor="@color/letterbox_education_text_secondary" - android:pathData="M 14 22 H 30 V 26 H 14 V 22 Z" /> - <path - android:fillColor="@color/letterbox_education_text_secondary" - android:pathData="M26 16L34 24L26 32V16Z" /> + android:pathData="M19.98 8L17.16 10.82L20.32 14L12 14V18H20.32L17.14 21.18L19.98 24L28 16.02L19.98 8Z" /> </vector> diff --git a/libs/WindowManager/Shell/res/drawable/letterbox_education_ic_screen_rotation.xml b/libs/WindowManager/Shell/res/drawable/letterbox_education_ic_screen_rotation.xml deleted file mode 100644 index 469eb1e14849..000000000000 --- a/libs/WindowManager/Shell/res/drawable/letterbox_education_ic_screen_rotation.xml +++ /dev/null @@ -1,26 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - ~ 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. - --> -<vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="@dimen/letterbox_education_dialog_icon_size" - android:height="@dimen/letterbox_education_dialog_icon_size" - android:viewportWidth="48" - android:viewportHeight="48"> - <path - android:fillColor="@color/letterbox_education_text_secondary" - android:fillType="evenOdd" - android:pathData="M22.56 2H26C37.02 2 46 10.98 46 22H42C42 14.44 36.74 8.1 29.7 6.42L31.74 10L28.26 12L22.56 2ZM22 46H25.44L19.74 36L16.26 38L18.3 41.58C11.26 39.9 6 33.56 6 26H2C2 37.02 10.98 46 22 46ZM20.46 12L36 27.52L27.54 36L12 20.48L20.46 12ZM17.64 9.18C18.42 8.4 19.44 8 20.46 8C21.5 8 22.52 8.4 23.3 9.16L38.84 24.7C40.4 26.26 40.4 28.78 38.84 30.34L30.36 38.82C29.58 39.6 28.56 40 27.54 40C26.52 40 25.5 39.6 24.72 38.82L9.18 23.28C7.62 21.72 7.62 19.2 9.18 17.64L17.64 9.18Z" /> -</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 dcb8aed05c9c..15e65f716b20 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 @@ -15,18 +15,12 @@ ~ limitations under the License. --> <vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="@dimen/letterbox_education_dialog_icon_size" - android:height="@dimen/letterbox_education_dialog_icon_size" - android:viewportWidth="48" - android:viewportHeight="48"> + android:width="@dimen/letterbox_education_dialog_icon_width" + android:height="@dimen/letterbox_education_dialog_icon_height" + android:viewportWidth="40" + android:viewportHeight="32"> + <path android:fillColor="@color/letterbox_education_text_secondary" - android:fillType="evenOdd" - android:pathData="M2 8C0.895431 8 0 8.89543 0 10V38C0 39.1046 0.895431 40 2 40H46C47.1046 40 48 39.1046 48 38V10C48 8.89543 47.1046 8 46 8H2ZM44 12H4V36H44V12Z" /> - <path - android:fillColor="@color/letterbox_education_text_secondary" - android:pathData="M6 16C6 14.8954 6.89543 14 8 14H21C22.1046 14 23 14.8954 23 16V32C23 33.1046 22.1046 34 21 34H8C6.89543 34 6 33.1046 6 32V16Z" /> - <path - android:fillColor="@color/letterbox_education_text_secondary" - android:pathData="M25 16C25 14.8954 25.8954 14 27 14H40C41.1046 14 42 14.8954 42 16V32C42 33.1046 41.1046 34 40 34H27C25.8954 34 25 33.1046 25 32V16Z" /> + 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" /> </vector>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/layout/bubble_overflow_container.xml b/libs/WindowManager/Shell/res/layout/bubble_overflow_container.xml index cb516cdbe49b..df5985c605d1 100644 --- a/libs/WindowManager/Shell/res/layout/bubble_overflow_container.xml +++ b/libs/WindowManager/Shell/res/layout/bubble_overflow_container.xml @@ -30,7 +30,8 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:layout_gravity="center_horizontal" - android:gravity="center"/> + android:gravity="center" + android:clipChildren="false"/> <LinearLayout android:id="@+id/bubble_overflow_empty_state" diff --git a/libs/WindowManager/Shell/res/layout/caption_window_decoration.xml b/libs/WindowManager/Shell/res/layout/caption_window_decoration.xml new file mode 100644 index 000000000000..d183e42c173b --- /dev/null +++ b/libs/WindowManager/Shell/res/layout/caption_window_decoration.xml @@ -0,0 +1,55 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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. + --> +<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/decor_caption_title"> + <Button + android:id="@+id/minimize_window" + android:visibility="gone" + android:layout_width="32dp" + android:layout_height="32dp" + android:layout_margin="5dp" + android:padding="4dp" + android:layout_gravity="top|end" + android:contentDescription="@string/maximize_button_text" + android:background="@drawable/decor_minimize_button_dark" + android:duplicateParentState="true"/> + <Button + android:id="@+id/maximize_window" + android:layout_width="32dp" + android:layout_height="32dp" + android:layout_margin="5dp" + android:padding="4dp" + android:layout_gravity="center_vertical|end" + android:contentDescription="@string/maximize_button_text" + android:background="@drawable/decor_maximize_button_dark" + android:duplicateParentState="true"/> + <Button + android:id="@+id/close_window" + android:layout_width="32dp" + android:layout_height="32dp" + android:layout_margin="5dp" + android:padding="4dp" + android:layout_gravity="center_vertical|end" + android:contentDescription="@string/close_button_text" + android:background="@drawable/decor_close_button_dark" + android:duplicateParentState="true"/> +</com.android.wm.shell.windowdecor.WindowDecorLinearLayout> 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 cd1d99ae58b0..c65f24d84e37 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 @@ -26,7 +26,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" - android:layout_marginBottom="12dp"/> + android:layout_marginBottom="20dp"/> <TextView android:id="@+id/letterbox_education_dialog_action_text" 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 95923763d889..3a44eb9089dd 100644 --- a/libs/WindowManager/Shell/res/layout/letterbox_education_dialog_layout.xml +++ b/libs/WindowManager/Shell/res/layout/letterbox_education_dialog_layout.xml @@ -50,13 +50,16 @@ android:layout_height="wrap_content" android:gravity="center_horizontal" android:orientation="vertical" - android:padding="24dp"> + android:paddingTop="32dp" + android:paddingBottom="32dp" + android:paddingLeft="56dp" + android:paddingRight="56dp"> <ImageView - android:layout_width="@dimen/letterbox_education_dialog_icon_size" - android:layout_height="@dimen/letterbox_education_dialog_icon_size" - android:layout_marginBottom="12dp" - android:src="@drawable/letterbox_education_ic_letterboxed_app"/> + 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:src="@drawable/letterbox_education_ic_light_bulb"/> <TextView android:id="@+id/letterbox_education_dialog_title" @@ -68,16 +71,6 @@ android:textColor="@color/compat_controls_text" android:textSize="24sp"/> - <TextView - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_marginTop="8dp" - android:lineSpacingExtra="4sp" - android:text="@string/letterbox_education_dialog_subtext" - android:textAlignment="center" - android:textColor="@color/letterbox_education_text_secondary" - android:textSize="14sp"/> - <LinearLayout android:layout_width="wrap_content" android:layout_height="wrap_content" @@ -88,16 +81,16 @@ <com.android.wm.shell.compatui.letterboxedu.LetterboxEduDialogActionLayout android:layout_width="wrap_content" android:layout_height="wrap_content" - app:icon="@drawable/letterbox_education_ic_screen_rotation" - app:text="@string/letterbox_education_screen_rotation_text"/> + app:icon="@drawable/letterbox_education_ic_reposition" + app:text="@string/letterbox_education_reposition_text"/> <com.android.wm.shell.compatui.letterboxedu.LetterboxEduDialogActionLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart= "@dimen/letterbox_education_dialog_space_between_actions" - app:icon="@drawable/letterbox_education_ic_reposition" - app:text="@string/letterbox_education_reposition_text"/> + app:icon="@drawable/letterbox_education_ic_split_screen" + app:text="@string/letterbox_education_split_screen_text"/> </LinearLayout> @@ -105,7 +98,7 @@ android:id="@+id/letterbox_education_dialog_dismiss_button" android:layout_width="match_parent" android:layout_height="56dp" - android:layout_marginTop="48dp" + android:layout_marginTop="40dp" android:background= "@drawable/letterbox_education_dismiss_button_background_ripple" android:text="@string/letterbox_education_got_it" diff --git a/libs/WindowManager/Shell/res/values-af/strings_tv.xml b/libs/WindowManager/Shell/res/values-af/strings_tv.xml index 6187ea46769c..c87bec093cca 100644 --- a/libs/WindowManager/Shell/res/values-af/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-af/strings_tv.xml @@ -19,16 +19,10 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Beeld-in-beeld"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Titellose program)"</string> - <string name="pip_close" msgid="2955969519031223530">"Maak toe"</string> + <string name="pip_close" msgid="9135220303720555525">"Maak PIP toe"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Volskerm"</string> - <string name="pip_move" msgid="158770205886688553">"Skuif"</string> - <string name="pip_expand" msgid="1051966011679297308">"Vou uit"</string> - <string name="pip_collapse" msgid="3903295106641385962">"Vou in"</string> + <string name="pip_move" msgid="1544227837964635439">"Skuif PIP"</string> + <string name="pip_expand" msgid="7605396312689038178">"Vou PIP uit"</string> + <string name="pip_collapse" msgid="5732233773786896094">"Vou PIP in"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Dubbeldruk "<annotation icon="home_icon">" TUIS "</annotation>" vir kontroles"</string> - <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Prent-in-prent-kieslys"</string> - <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Skuif links"</string> - <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Skuif regs"</string> - <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Skuif op"</string> - <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Skuif af"</string> - <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Klaar"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-am/strings_tv.xml b/libs/WindowManager/Shell/res/values-am/strings_tv.xml index 74ce49ef078e..d23353858de6 100644 --- a/libs/WindowManager/Shell/res/values-am/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-am/strings_tv.xml @@ -19,16 +19,10 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"ስዕል-ላይ-ስዕል"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(ርዕስ የሌለው ፕሮግራም)"</string> - <string name="pip_close" msgid="2955969519031223530">"ዝጋ"</string> + <string name="pip_close" msgid="9135220303720555525">"PIPን ዝጋ"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"ሙሉ ማያ ገጽ"</string> - <string name="pip_move" msgid="158770205886688553">"ውሰድ"</string> - <string name="pip_expand" msgid="1051966011679297308">"ዘርጋ"</string> - <string name="pip_collapse" msgid="3903295106641385962">"ሰብስብ"</string> + <string name="pip_move" msgid="1544227837964635439">"ፒአይፒ ውሰድ"</string> + <string name="pip_expand" msgid="7605396312689038178">"ፒአይፒን ዘርጋ"</string> + <string name="pip_collapse" msgid="5732233773786896094">"ፒአይፒን ሰብስብ"</string> <string name="pip_edu_text" msgid="3672999496647508701">" ለመቆጣጠሪያዎች "<annotation icon="home_icon">"መነሻ"</annotation>"ን ሁለቴ ይጫኑ"</string> - <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"የስዕል-ላይ-ስዕል ምናሌ።"</string> - <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"ወደ ግራ ውሰድ"</string> - <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"ወደ ቀኝ ውሰድ"</string> - <string name="a11y_action_pip_move_up" msgid="98502616918621959">"ወደ ላይ ውሰድ"</string> - <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"ወደ ታች ውሰድ"</string> - <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"ተጠናቅቋል"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ar/strings_tv.xml b/libs/WindowManager/Shell/res/values-ar/strings_tv.xml index 9c195a7386a9..a1ceda5fc987 100644 --- a/libs/WindowManager/Shell/res/values-ar/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-ar/strings_tv.xml @@ -19,16 +19,10 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"نافذة ضمن النافذة"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(ليس هناك عنوان للبرنامج)"</string> - <string name="pip_close" msgid="2955969519031223530">"إغلاق"</string> + <string name="pip_close" msgid="9135220303720555525">"إغلاق PIP"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"ملء الشاشة"</string> - <string name="pip_move" msgid="158770205886688553">"نقل"</string> - <string name="pip_expand" msgid="1051966011679297308">"توسيع"</string> - <string name="pip_collapse" msgid="3903295106641385962">"تصغير"</string> + <string name="pip_move" msgid="1544227837964635439">"نقل نافذة داخل النافذة (PIP)"</string> + <string name="pip_expand" msgid="7605396312689038178">"توسيع نافذة داخل النافذة (PIP)"</string> + <string name="pip_collapse" msgid="5732233773786896094">"تصغير نافذة داخل النافذة (PIP)"</string> <string name="pip_edu_text" msgid="3672999496647508701">" انقر مرتين على "<annotation icon="home_icon">" الصفحة الرئيسية "</annotation>" للوصول لعناصر التحكم."</string> - <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"قائمة نافذة ضمن النافذة"</string> - <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"نقل لليسار"</string> - <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"نقل لليمين"</string> - <string name="a11y_action_pip_move_up" msgid="98502616918621959">"نقل للأعلى"</string> - <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"نقل للأسفل"</string> - <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"تمّ"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-as/strings_tv.xml b/libs/WindowManager/Shell/res/values-as/strings_tv.xml index 816b5b1c79dc..8d7bd9f6a27e 100644 --- a/libs/WindowManager/Shell/res/values-as/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-as/strings_tv.xml @@ -19,16 +19,10 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"চিত্ৰৰ ভিতৰত চিত্ৰ"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(শিৰোনামবিহীন কাৰ্যক্ৰম)"</string> - <string name="pip_close" msgid="2955969519031223530">"বন্ধ কৰক"</string> + <string name="pip_close" msgid="9135220303720555525">"পিপ বন্ধ কৰক"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"সম্পূৰ্ণ স্ক্ৰীন"</string> - <string name="pip_move" msgid="158770205886688553">"স্থানান্তৰ কৰক"</string> - <string name="pip_expand" msgid="1051966011679297308">"বিস্তাৰ কৰক"</string> - <string name="pip_collapse" msgid="3903295106641385962">"সংকোচন কৰক"</string> + <string name="pip_move" msgid="1544227837964635439">"পিপ স্থানান্তৰ কৰক"</string> + <string name="pip_expand" msgid="7605396312689038178">"পিপ বিস্তাৰ কৰক"</string> + <string name="pip_collapse" msgid="5732233773786896094">"পিপ সংকোচন কৰক"</string> <string name="pip_edu_text" msgid="3672999496647508701">" নিয়ন্ত্ৰণৰ বাবে "<annotation icon="home_icon">" গৃহপৃষ্ঠা "</annotation>" বুটামত দুবাৰ হেঁচক"</string> - <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"চিত্ৰৰ ভিতৰৰ চিত্ৰ মেনু।"</string> - <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"বাওঁফাললৈ নিয়ক"</string> - <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"সোঁফাললৈ নিয়ক"</string> - <string name="a11y_action_pip_move_up" msgid="98502616918621959">"ওপৰলৈ নিয়ক"</string> - <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"তললৈ নিয়ক"</string> - <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"হ’ল"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-az/strings_tv.xml b/libs/WindowManager/Shell/res/values-az/strings_tv.xml index ccb7a7069ad8..87c46fa41a01 100644 --- a/libs/WindowManager/Shell/res/values-az/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-az/strings_tv.xml @@ -19,16 +19,10 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Şəkil-içində-Şəkil"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Başlıqsız proqram)"</string> - <string name="pip_close" msgid="2955969519031223530">"Bağlayın"</string> + <string name="pip_close" msgid="9135220303720555525">"PIP bağlayın"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Tam ekran"</string> - <string name="pip_move" msgid="158770205886688553">"Köçürün"</string> - <string name="pip_expand" msgid="1051966011679297308">"Genişləndirin"</string> - <string name="pip_collapse" msgid="3903295106641385962">"Yığcamlaşdırın"</string> + <string name="pip_move" msgid="1544227837964635439">"PIP tətbiq edin"</string> + <string name="pip_expand" msgid="7605396312689038178">"PIP-ni genişləndirin"</string> + <string name="pip_collapse" msgid="5732233773786896094">"PIP-ni yığcamlaşdırın"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Nizamlayıcılar üçün "<annotation icon="home_icon">" ƏSAS SƏHİFƏ "</annotation>" süçimini iki dəfə basın"</string> - <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Şəkildə şəkil menyusu."</string> - <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Sola köçürün"</string> - <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Sağa köçürün"</string> - <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Yuxarı köçürün"</string> - <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Aşağı köçürün"</string> - <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Hazırdır"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-b+sr+Latn/strings_tv.xml b/libs/WindowManager/Shell/res/values-b+sr+Latn/strings_tv.xml index 51a1262b1de7..c87f30611a07 100644 --- a/libs/WindowManager/Shell/res/values-b+sr+Latn/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-b+sr+Latn/strings_tv.xml @@ -19,16 +19,10 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Slika u slici"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Program bez naslova)"</string> - <string name="pip_close" msgid="2955969519031223530">"Zatvori"</string> + <string name="pip_close" msgid="9135220303720555525">"Zatvori PIP"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Ceo ekran"</string> - <string name="pip_move" msgid="158770205886688553">"Premesti"</string> - <string name="pip_expand" msgid="1051966011679297308">"Proširi"</string> - <string name="pip_collapse" msgid="3903295106641385962">"Skupi"</string> + <string name="pip_move" msgid="1544227837964635439">"Premesti sliku u slici"</string> + <string name="pip_expand" msgid="7605396312689038178">"Proširi sliku u slici"</string> + <string name="pip_collapse" msgid="5732233773786896094">"Skupi sliku u slici"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Dvaput pritisnite "<annotation icon="home_icon">" HOME "</annotation>" za kontrole"</string> - <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Meni Slika u slici."</string> - <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Pomerite nalevo"</string> - <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Pomerite nadesno"</string> - <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Pomerite nagore"</string> - <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Pomerite nadole"</string> - <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Gotovo"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-be/strings_tv.xml b/libs/WindowManager/Shell/res/values-be/strings_tv.xml index 15a353c649d6..3566bc372820 100644 --- a/libs/WindowManager/Shell/res/values-be/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-be/strings_tv.xml @@ -19,16 +19,10 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Відарыс у відарысе"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Праграма без назвы)"</string> - <string name="pip_close" msgid="2955969519031223530">"Закрыць"</string> + <string name="pip_close" msgid="9135220303720555525">"Закрыць PIP"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Поўнаэкранны рэжым"</string> - <string name="pip_move" msgid="158770205886688553">"Перамясціць"</string> - <string name="pip_expand" msgid="1051966011679297308">"Разгарнуць"</string> - <string name="pip_collapse" msgid="3903295106641385962">"Згарнуць"</string> + <string name="pip_move" msgid="1544227837964635439">"Перамясціць PIP"</string> + <string name="pip_expand" msgid="7605396312689038178">"Разгарнуць відарыс у відарысе"</string> + <string name="pip_collapse" msgid="5732233773786896094">"Згарнуць відарыс у відарысе"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Двойчы націсніце "<annotation icon="home_icon">" ГАЛОЎНЫ ЭКРАН "</annotation>" для пераходу ў налады"</string> - <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Меню рэжыму \"Відарыс у відарысе\"."</string> - <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Перамясціць улева"</string> - <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Перамясціць управа"</string> - <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Перамясціць уверх"</string> - <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Перамясціць уніз"</string> - <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Гатова"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-bg/strings_tv.xml b/libs/WindowManager/Shell/res/values-bg/strings_tv.xml index 2b27a6927077..91049fd2cf02 100644 --- a/libs/WindowManager/Shell/res/values-bg/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-bg/strings_tv.xml @@ -19,16 +19,10 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Картина в картината"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Програма без заглавие)"</string> - <string name="pip_close" msgid="2955969519031223530">"Затваряне"</string> + <string name="pip_close" msgid="9135220303720555525">"Затваряне на PIP"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Цял екран"</string> - <string name="pip_move" msgid="158770205886688553">"Преместване"</string> - <string name="pip_expand" msgid="1051966011679297308">"Разгъване"</string> - <string name="pip_collapse" msgid="3903295106641385962">"Свиване"</string> + <string name="pip_move" msgid="1544227837964635439">"„Картина в картина“: Преместв."</string> + <string name="pip_expand" msgid="7605396312689038178">"Разгъване на прозореца за PIP"</string> + <string name="pip_collapse" msgid="5732233773786896094">"Свиване на прозореца за PIP"</string> <string name="pip_edu_text" msgid="3672999496647508701">" За достъп до контролите натиснете 2 пъти "<annotation icon="home_icon">"НАЧАЛО"</annotation></string> - <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Меню за функцията „Картина в картината“."</string> - <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Преместване наляво"</string> - <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Преместване надясно"</string> - <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Преместване нагоре"</string> - <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Преместване надолу"</string> - <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Готово"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-bn/strings_tv.xml b/libs/WindowManager/Shell/res/values-bn/strings_tv.xml index 23c8ffabeede..792708d128a5 100644 --- a/libs/WindowManager/Shell/res/values-bn/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-bn/strings_tv.xml @@ -19,16 +19,10 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"ছবির-মধ্যে-ছবি"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(শিরোনামহীন প্রোগ্রাম)"</string> - <string name="pip_close" msgid="2955969519031223530">"বন্ধ করুন"</string> + <string name="pip_close" msgid="9135220303720555525">"PIP বন্ধ করুন"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"পূর্ণ স্ক্রিন"</string> - <string name="pip_move" msgid="158770205886688553">"সরান"</string> - <string name="pip_expand" msgid="1051966011679297308">"বড় করুন"</string> - <string name="pip_collapse" msgid="3903295106641385962">"আড়াল করুন"</string> + <string name="pip_move" msgid="1544227837964635439">"PIP সরান"</string> + <string name="pip_expand" msgid="7605396312689038178">"PIP বড় করুন"</string> + <string name="pip_collapse" msgid="5732233773786896094">"PIP আড়াল করুন"</string> <string name="pip_edu_text" msgid="3672999496647508701">" কন্ট্রোলের জন্য "<annotation icon="home_icon">" হোম "</annotation>" বোতামে ডবল প্রেস করুন"</string> - <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"ছবির-মধ্যে-ছবি মেনু।"</string> - <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"বাঁদিকে সরান"</string> - <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"ডানদিকে সরান"</string> - <string name="a11y_action_pip_move_up" msgid="98502616918621959">"উপরে তুলুন"</string> - <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"নিচে নামান"</string> - <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"হয়ে গেছে"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-bs/strings.xml b/libs/WindowManager/Shell/res/values-bs/strings.xml index f10b62e65e97..ae01c641cc43 100644 --- a/libs/WindowManager/Shell/res/values-bs/strings.xml +++ b/libs/WindowManager/Shell/res/values-bs/strings.xml @@ -35,7 +35,7 @@ <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Aplikacija ne podržava dijeljenje ekrana."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Aplikacija možda neće raditi na sekundarnom ekranu."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Aplikacija ne podržava pokretanje na sekundarnim ekranima."</string> - <string name="accessibility_divider" msgid="703810061635792791">"Razdjelnik podijeljenog ekrana"</string> + <string name="accessibility_divider" msgid="703810061635792791">"Razdjelnik ekrana"</string> <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Lijevo cijeli ekran"</string> <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Lijevo 70%"</string> <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Lijevo 50%"</string> diff --git a/libs/WindowManager/Shell/res/values-bs/strings_tv.xml b/libs/WindowManager/Shell/res/values-bs/strings_tv.xml index 443fd620fd65..b7f0dca1b5a5 100644 --- a/libs/WindowManager/Shell/res/values-bs/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-bs/strings_tv.xml @@ -19,16 +19,10 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Slika u slici"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Program bez naslova)"</string> - <string name="pip_close" msgid="2955969519031223530">"Zatvori"</string> + <string name="pip_close" msgid="9135220303720555525">"Zatvori sliku u slici"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Cijeli ekran"</string> - <string name="pip_move" msgid="158770205886688553">"Premjesti"</string> - <string name="pip_expand" msgid="1051966011679297308">"Proširi"</string> - <string name="pip_collapse" msgid="3903295106641385962">"Suzi"</string> + <string name="pip_move" msgid="1544227837964635439">"Pokreni sliku u slici"</string> + <string name="pip_expand" msgid="7605396312689038178">"Proširi sliku u slici"</string> + <string name="pip_collapse" msgid="5732233773786896094">"Suzi sliku u slici"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Dvaput pritisnite "<annotation icon="home_icon">" POČETNI EKRAN "</annotation>" za kontrole"</string> - <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Meni za način rada slika u slici."</string> - <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Pomjeranje ulijevo"</string> - <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Pomjeranje udesno"</string> - <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Pomjeranje nagore"</string> - <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Pomjeranje nadolje"</string> - <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Gotovo"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ca/strings_tv.xml b/libs/WindowManager/Shell/res/values-ca/strings_tv.xml index 94ba0db7e978..1c560c7afa06 100644 --- a/libs/WindowManager/Shell/res/values-ca/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-ca/strings_tv.xml @@ -19,16 +19,10 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Pantalla en pantalla"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Programa sense títol)"</string> - <string name="pip_close" msgid="2955969519031223530">"Tanca"</string> + <string name="pip_close" msgid="9135220303720555525">"Tanca PIP"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Pantalla completa"</string> - <string name="pip_move" msgid="158770205886688553">"Mou"</string> - <string name="pip_expand" msgid="1051966011679297308">"Desplega"</string> - <string name="pip_collapse" msgid="3903295106641385962">"Replega"</string> + <string name="pip_move" msgid="1544227837964635439">"Mou pantalla en pantalla"</string> + <string name="pip_expand" msgid="7605396312689038178">"Desplega pantalla en pantalla"</string> + <string name="pip_collapse" msgid="5732233773786896094">"Replega pantalla en pantalla"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Prem dos cops "<annotation icon="home_icon">" INICI "</annotation>" per accedir als controls"</string> - <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Menú de pantalla en pantalla."</string> - <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Mou cap a l\'esquerra"</string> - <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Mou cap a la dreta"</string> - <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Mou cap amunt"</string> - <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Mou cap avall"</string> - <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Fet"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-cs/strings_tv.xml b/libs/WindowManager/Shell/res/values-cs/strings_tv.xml index 3ed85dce0433..9a8cc2b4d70e 100644 --- a/libs/WindowManager/Shell/res/values-cs/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-cs/strings_tv.xml @@ -19,16 +19,10 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Obraz v obraze"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Bez názvu)"</string> - <string name="pip_close" msgid="2955969519031223530">"Zavřít"</string> + <string name="pip_close" msgid="9135220303720555525">"Ukončit obraz v obraze (PIP)"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Celá obrazovka"</string> - <string name="pip_move" msgid="158770205886688553">"Přesunout"</string> - <string name="pip_expand" msgid="1051966011679297308">"Rozbalit"</string> - <string name="pip_collapse" msgid="3903295106641385962">"Sbalit"</string> + <string name="pip_move" msgid="1544227837964635439">"Přesunout PIP"</string> + <string name="pip_expand" msgid="7605396312689038178">"Rozbalit PIP"</string> + <string name="pip_collapse" msgid="5732233773786896094">"Sbalit PIP"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Ovládací prvky zobrazíte dvojitým stisknutím "<annotation icon="home_icon">"tlačítka plochy"</annotation></string> - <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Nabídka režimu obrazu v obraze"</string> - <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Přesunout doleva"</string> - <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Přesunout doprava"</string> - <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Přesunout nahoru"</string> - <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Přesunout dolů"</string> - <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Hotovo"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-da/strings_tv.xml b/libs/WindowManager/Shell/res/values-da/strings_tv.xml index 09024428a825..cba660ac723c 100644 --- a/libs/WindowManager/Shell/res/values-da/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-da/strings_tv.xml @@ -19,16 +19,10 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Integreret billede"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Program uden titel)"</string> - <string name="pip_close" msgid="2955969519031223530">"Luk"</string> + <string name="pip_close" msgid="9135220303720555525">"Luk integreret billede"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Fuld skærm"</string> - <string name="pip_move" msgid="158770205886688553">"Flyt"</string> - <string name="pip_expand" msgid="1051966011679297308">"Udvid"</string> - <string name="pip_collapse" msgid="3903295106641385962">"Skjul"</string> + <string name="pip_move" msgid="1544227837964635439">"Flyt PIP"</string> + <string name="pip_expand" msgid="7605396312689038178">"Udvid PIP"</string> + <string name="pip_collapse" msgid="5732233773786896094">"Skjul PIP"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Tryk to gange på "<annotation icon="home_icon">" HJEM "</annotation>" for at se indstillinger"</string> - <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Menu for integreret billede."</string> - <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Flyt til venstre"</string> - <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Flyt til højre"</string> - <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Flyt op"</string> - <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Flyt ned"</string> - <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Udfør"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-de/strings_tv.xml b/libs/WindowManager/Shell/res/values-de/strings_tv.xml index 18535c9d9338..02a1b66eb63f 100644 --- a/libs/WindowManager/Shell/res/values-de/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-de/strings_tv.xml @@ -19,16 +19,10 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Bild im Bild"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Kein Sendungsname gefunden)"</string> - <string name="pip_close" msgid="2955969519031223530">"Schließen"</string> + <string name="pip_close" msgid="9135220303720555525">"PIP schließen"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Vollbild"</string> - <string name="pip_move" msgid="158770205886688553">"Bewegen"</string> - <string name="pip_expand" msgid="1051966011679297308">"Maximieren"</string> - <string name="pip_collapse" msgid="3903295106641385962">"Minimieren"</string> + <string name="pip_move" msgid="1544227837964635439">"BiB verschieben"</string> + <string name="pip_expand" msgid="7605396312689038178">"BiB maximieren"</string> + <string name="pip_collapse" msgid="5732233773786896094">"BiB minimieren"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Für Steuerelemente zweimal "<annotation icon="home_icon">"STARTBILDSCHIRMTASTE"</annotation>" drücken"</string> - <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Menü „Bild im Bild“."</string> - <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Nach links bewegen"</string> - <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Nach rechts bewegen"</string> - <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Nach oben bewegen"</string> - <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Nach unten bewegen"</string> - <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Fertig"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-el/strings_tv.xml b/libs/WindowManager/Shell/res/values-el/strings_tv.xml index 5f8a004b0a1f..24cd030cd754 100644 --- a/libs/WindowManager/Shell/res/values-el/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-el/strings_tv.xml @@ -19,16 +19,10 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Picture-in-Picture"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Δεν υπάρχει τίτλος προγράμματος)"</string> - <string name="pip_close" msgid="2955969519031223530">"Κλείσιμο"</string> + <string name="pip_close" msgid="9135220303720555525">"Κλείσιμο PIP"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Πλήρης οθόνη"</string> - <string name="pip_move" msgid="158770205886688553">"Μετακίνηση"</string> - <string name="pip_expand" msgid="1051966011679297308">"Ανάπτυξη"</string> - <string name="pip_collapse" msgid="3903295106641385962">"Σύμπτυξη"</string> + <string name="pip_move" msgid="1544227837964635439">"Μετακίνηση PIP"</string> + <string name="pip_expand" msgid="7605396312689038178">"Ανάπτυξη PIP"</string> + <string name="pip_collapse" msgid="5732233773786896094">"Σύμπτυξη PIP"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Πατήστε δύο φορές "<annotation icon="home_icon">" ΑΡΧΙΚΗ ΟΘΟΝΗ "</annotation>" για στοιχεία ελέγχου"</string> - <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Μενού λειτουργίας Picture-in-Picture."</string> - <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Μετακίνηση αριστερά"</string> - <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Μετακίνηση δεξιά"</string> - <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Μετακίνηση επάνω"</string> - <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Μετακίνηση κάτω"</string> - <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Τέλος"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-en-rAU/strings_tv.xml b/libs/WindowManager/Shell/res/values-en-rAU/strings_tv.xml index 839789b22a1c..82257b42814d 100644 --- a/libs/WindowManager/Shell/res/values-en-rAU/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-en-rAU/strings_tv.xml @@ -19,16 +19,10 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Picture-in-picture"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(No title program)"</string> - <string name="pip_close" msgid="2955969519031223530">"Close"</string> + <string name="pip_close" msgid="9135220303720555525">"Close PIP"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Full screen"</string> - <string name="pip_move" msgid="158770205886688553">"Move"</string> - <string name="pip_expand" msgid="1051966011679297308">"Expand"</string> - <string name="pip_collapse" msgid="3903295106641385962">"Collapse"</string> + <string name="pip_move" msgid="1544227837964635439">"Move PIP"</string> + <string name="pip_expand" msgid="7605396312689038178">"Expand PIP"</string> + <string name="pip_collapse" msgid="5732233773786896094">"Collapse PIP"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Double-press "<annotation icon="home_icon">" HOME "</annotation>" for controls"</string> - <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Picture-in-picture menu"</string> - <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Move left"</string> - <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Move right"</string> - <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Move up"</string> - <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Move down"</string> - <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Done"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-en-rCA/strings_tv.xml b/libs/WindowManager/Shell/res/values-en-rCA/strings_tv.xml index 839789b22a1c..82257b42814d 100644 --- a/libs/WindowManager/Shell/res/values-en-rCA/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-en-rCA/strings_tv.xml @@ -19,16 +19,10 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Picture-in-picture"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(No title program)"</string> - <string name="pip_close" msgid="2955969519031223530">"Close"</string> + <string name="pip_close" msgid="9135220303720555525">"Close PIP"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Full screen"</string> - <string name="pip_move" msgid="158770205886688553">"Move"</string> - <string name="pip_expand" msgid="1051966011679297308">"Expand"</string> - <string name="pip_collapse" msgid="3903295106641385962">"Collapse"</string> + <string name="pip_move" msgid="1544227837964635439">"Move PIP"</string> + <string name="pip_expand" msgid="7605396312689038178">"Expand PIP"</string> + <string name="pip_collapse" msgid="5732233773786896094">"Collapse PIP"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Double-press "<annotation icon="home_icon">" HOME "</annotation>" for controls"</string> - <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Picture-in-picture menu"</string> - <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Move left"</string> - <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Move right"</string> - <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Move up"</string> - <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Move down"</string> - <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Done"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-en-rGB/strings_tv.xml b/libs/WindowManager/Shell/res/values-en-rGB/strings_tv.xml index 839789b22a1c..82257b42814d 100644 --- a/libs/WindowManager/Shell/res/values-en-rGB/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-en-rGB/strings_tv.xml @@ -19,16 +19,10 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Picture-in-picture"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(No title program)"</string> - <string name="pip_close" msgid="2955969519031223530">"Close"</string> + <string name="pip_close" msgid="9135220303720555525">"Close PIP"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Full screen"</string> - <string name="pip_move" msgid="158770205886688553">"Move"</string> - <string name="pip_expand" msgid="1051966011679297308">"Expand"</string> - <string name="pip_collapse" msgid="3903295106641385962">"Collapse"</string> + <string name="pip_move" msgid="1544227837964635439">"Move PIP"</string> + <string name="pip_expand" msgid="7605396312689038178">"Expand PIP"</string> + <string name="pip_collapse" msgid="5732233773786896094">"Collapse PIP"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Double-press "<annotation icon="home_icon">" HOME "</annotation>" for controls"</string> - <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Picture-in-picture menu"</string> - <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Move left"</string> - <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Move right"</string> - <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Move up"</string> - <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Move down"</string> - <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Done"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-en-rIN/strings_tv.xml b/libs/WindowManager/Shell/res/values-en-rIN/strings_tv.xml index 839789b22a1c..82257b42814d 100644 --- a/libs/WindowManager/Shell/res/values-en-rIN/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-en-rIN/strings_tv.xml @@ -19,16 +19,10 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Picture-in-picture"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(No title program)"</string> - <string name="pip_close" msgid="2955969519031223530">"Close"</string> + <string name="pip_close" msgid="9135220303720555525">"Close PIP"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Full screen"</string> - <string name="pip_move" msgid="158770205886688553">"Move"</string> - <string name="pip_expand" msgid="1051966011679297308">"Expand"</string> - <string name="pip_collapse" msgid="3903295106641385962">"Collapse"</string> + <string name="pip_move" msgid="1544227837964635439">"Move PIP"</string> + <string name="pip_expand" msgid="7605396312689038178">"Expand PIP"</string> + <string name="pip_collapse" msgid="5732233773786896094">"Collapse PIP"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Double-press "<annotation icon="home_icon">" HOME "</annotation>" for controls"</string> - <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Picture-in-picture menu"</string> - <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Move left"</string> - <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Move right"</string> - <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Move up"</string> - <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Move down"</string> - <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Done"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-en-rXC/strings_tv.xml b/libs/WindowManager/Shell/res/values-en-rXC/strings_tv.xml index 507e066e3812..a6e494cfed3c 100644 --- a/libs/WindowManager/Shell/res/values-en-rXC/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-en-rXC/strings_tv.xml @@ -19,16 +19,10 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Picture-in-Picture"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(No title program)"</string> - <string name="pip_close" msgid="2955969519031223530">"Close"</string> + <string name="pip_close" msgid="9135220303720555525">"Close PIP"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Full screen"</string> - <string name="pip_move" msgid="158770205886688553">"Move"</string> - <string name="pip_expand" msgid="1051966011679297308">"Expand"</string> - <string name="pip_collapse" msgid="3903295106641385962">"Collapse"</string> + <string name="pip_move" msgid="1544227837964635439">"Move PIP"</string> + <string name="pip_expand" msgid="7605396312689038178">"Expand PIP"</string> + <string name="pip_collapse" msgid="5732233773786896094">"Collapse PIP"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Double press "<annotation icon="home_icon">" HOME "</annotation>" for controls"</string> - <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Picture-in-Picture menu."</string> - <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Move left"</string> - <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Move right"</string> - <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Move up"</string> - <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Move down"</string> - <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Done"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-es-rUS/strings_tv.xml b/libs/WindowManager/Shell/res/values-es-rUS/strings_tv.xml index a2c27b79e04c..458f6b15b857 100644 --- a/libs/WindowManager/Shell/res/values-es-rUS/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-es-rUS/strings_tv.xml @@ -19,16 +19,10 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Pantalla en pantalla"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Sin título de programa)"</string> - <string name="pip_close" msgid="2955969519031223530">"Cerrar"</string> + <string name="pip_close" msgid="9135220303720555525">"Cerrar PIP"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Pantalla completa"</string> - <string name="pip_move" msgid="158770205886688553">"Mover"</string> - <string name="pip_expand" msgid="1051966011679297308">"Expandir"</string> - <string name="pip_collapse" msgid="3903295106641385962">"Contraer"</string> + <string name="pip_move" msgid="1544227837964635439">"Mover PIP"</string> + <string name="pip_expand" msgid="7605396312689038178">"Maximizar PIP"</string> + <string name="pip_collapse" msgid="5732233773786896094">"Minimizar PIP"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Presiona dos veces "<annotation icon="home_icon">"INICIO"</annotation>" para ver los controles"</string> - <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Menú de pantalla en pantalla"</string> - <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Mover hacia la izquierda"</string> - <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Mover hacia la derecha"</string> - <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Mover hacia arriba"</string> - <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Mover hacia abajo"</string> - <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Listo"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-es/strings.xml b/libs/WindowManager/Shell/res/values-es/strings.xml index 39990dc8cb0c..974960708190 100644 --- a/libs/WindowManager/Shell/res/values-es/strings.xml +++ b/libs/WindowManager/Shell/res/values-es/strings.xml @@ -46,10 +46,10 @@ <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Superior 50%"</string> <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Superior 30%"</string> <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Pantalla inferior completa"</string> - <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Usar modo Una mano"</string> + <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Usar Modo una mano"</string> <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Para salir, desliza el dedo hacia arriba desde la parte inferior de la pantalla o toca cualquier zona que haya encima de la aplicación"</string> - <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Iniciar modo Una mano"</string> - <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"Salir del modo Una mano"</string> + <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Iniciar Modo una mano"</string> + <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"Salir del Modo una mano"</string> <string name="bubbles_settings_button_description" msgid="1301286017420516912">"Ajustes de las burbujas de <xliff:g id="APP_NAME">%1$s</xliff:g>"</string> <string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"Menú adicional"</string> <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"Volver a añadir a la pila"</string> @@ -63,7 +63,7 @@ <string name="bubble_dismiss_text" msgid="8816558050659478158">"Cerrar burbuja"</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> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"Las conversaciones nuevas aparecen como iconos flotantes llamadas \"burbujas\". Toca una burbuja para abrirla. Arrástrala para moverla."</string> <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"Controla las burbujas"</string> <string name="bubbles_user_education_manage" msgid="3460756219946517198">"Toca Gestionar para desactivar las burbujas de esta aplicación"</string> <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"Entendido"</string> diff --git a/libs/WindowManager/Shell/res/values-es/strings_tv.xml b/libs/WindowManager/Shell/res/values-es/strings_tv.xml index 75db421ec405..0a690984dac5 100644 --- a/libs/WindowManager/Shell/res/values-es/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-es/strings_tv.xml @@ -19,16 +19,10 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Imagen en imagen"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Programa sin título)"</string> - <string name="pip_close" msgid="2955969519031223530">"Cerrar"</string> + <string name="pip_close" msgid="9135220303720555525">"Cerrar PIP"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Pantalla completa"</string> - <string name="pip_move" msgid="158770205886688553">"Mover"</string> - <string name="pip_expand" msgid="1051966011679297308">"Mostrar"</string> - <string name="pip_collapse" msgid="3903295106641385962">"Contraer"</string> + <string name="pip_move" msgid="1544227837964635439">"Mover imagen en imagen"</string> + <string name="pip_expand" msgid="7605396312689038178">"Mostrar imagen en imagen"</string> + <string name="pip_collapse" msgid="5732233773786896094">"Ocultar imagen en imagen"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Pulsa dos veces "<annotation icon="home_icon">"INICIO"</annotation>" para ver los controles"</string> - <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Menú de imagen en imagen."</string> - <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Mover hacia la izquierda"</string> - <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Mover hacia la derecha"</string> - <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Mover hacia arriba"</string> - <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Mover hacia abajo"</string> - <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Hecho"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-et/strings_tv.xml b/libs/WindowManager/Shell/res/values-et/strings_tv.xml index e8fcb180c0c4..dc0232303a70 100644 --- a/libs/WindowManager/Shell/res/values-et/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-et/strings_tv.xml @@ -19,16 +19,10 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Pilt pildis"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Programmi pealkiri puudub)"</string> - <string name="pip_close" msgid="2955969519031223530">"Sule"</string> + <string name="pip_close" msgid="9135220303720555525">"Sule PIP"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Täisekraan"</string> - <string name="pip_move" msgid="158770205886688553">"Teisalda"</string> - <string name="pip_expand" msgid="1051966011679297308">"Laienda"</string> - <string name="pip_collapse" msgid="3903295106641385962">"Ahenda"</string> + <string name="pip_move" msgid="1544227837964635439">"Teisalda PIP-režiimi"</string> + <string name="pip_expand" msgid="7605396312689038178">"Laienda PIP-akent"</string> + <string name="pip_collapse" msgid="5732233773786896094">"Ahenda PIP-aken"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Nuppude nägemiseks vajutage 2 korda nuppu "<annotation icon="home_icon">"AVAKUVA"</annotation></string> - <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Menüü Pilt pildis."</string> - <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Teisalda vasakule"</string> - <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Teisalda paremale"</string> - <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Teisalda üles"</string> - <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Teisalda alla"</string> - <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Valmis"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-eu/strings.xml b/libs/WindowManager/Shell/res/values-eu/strings.xml index 67b9a433dc03..caa335a96222 100644 --- a/libs/WindowManager/Shell/res/values-eu/strings.xml +++ b/libs/WindowManager/Shell/res/values-eu/strings.xml @@ -23,7 +23,7 @@ <string name="pip_phone_enter_split" msgid="7042877263880641911">"Sartu pantaila zatituan"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Menua"</string> <string name="pip_notification_title" msgid="1347104727641353453">"Pantaila txiki gainjarrian dago <xliff:g id="NAME">%s</xliff:g>"</string> - <string name="pip_notification_message" msgid="8854051911700302620">"<xliff:g id="NAME">%s</xliff:g> zerbitzuak eginbide hori erabiltzea nahi ez baduzu, sakatu hau ezarpenak ireki eta aukera desaktibatzeko."</string> + <string name="pip_notification_message" msgid="8854051911700302620">"Ez baduzu nahi <xliff:g id="NAME">%s</xliff:g> zerbitzuak eginbide hori erabiltzea, sakatu hau ezarpenak ireki eta aukera desaktibatzeko."</string> <string name="pip_play" msgid="3496151081459417097">"Erreproduzitu"</string> <string name="pip_pause" msgid="690688849510295232">"Pausatu"</string> <string name="pip_skip_to_next" msgid="8403429188794867653">"Joan hurrengora"</string> diff --git a/libs/WindowManager/Shell/res/values-eu/strings_tv.xml b/libs/WindowManager/Shell/res/values-eu/strings_tv.xml index 07d75d2de9cd..bce06da2c66f 100644 --- a/libs/WindowManager/Shell/res/values-eu/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-eu/strings_tv.xml @@ -19,16 +19,10 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Pantaila txiki gainjarria"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Programa izengabea)"</string> - <string name="pip_close" msgid="2955969519031223530">"Itxi"</string> + <string name="pip_close" msgid="9135220303720555525">"Itxi PIPa"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Pantaila osoa"</string> - <string name="pip_move" msgid="158770205886688553">"Mugitu"</string> - <string name="pip_expand" msgid="1051966011679297308">"Zabaldu"</string> - <string name="pip_collapse" msgid="3903295106641385962">"Tolestu"</string> + <string name="pip_move" msgid="1544227837964635439">"Mugitu pantaila txiki gainjarria"</string> + <string name="pip_expand" msgid="7605396312689038178">"Zabaldu pantaila txiki gainjarria"</string> + <string name="pip_collapse" msgid="5732233773786896094">"Tolestu pantaila txiki gainjarria"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Kontrolatzeko aukerak atzitzeko, sakatu birritan "<annotation icon="home_icon">" HASIERA "</annotation></string> - <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Pantaila txiki gainjarriaren menua."</string> - <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Eraman ezkerrera"</string> - <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Eraman eskuinera"</string> - <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Eraman gora"</string> - <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Eraman behera"</string> - <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Eginda"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-fa/strings_tv.xml b/libs/WindowManager/Shell/res/values-fa/strings_tv.xml index 03f51d01a3a8..ff9a03c6cefb 100644 --- a/libs/WindowManager/Shell/res/values-fa/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-fa/strings_tv.xml @@ -19,16 +19,10 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"تصویر در تصویر"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(برنامه بدون عنوان)"</string> - <string name="pip_close" msgid="2955969519031223530">"بستن"</string> + <string name="pip_close" msgid="9135220303720555525">"بستن PIP"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"تمام صفحه"</string> - <string name="pip_move" msgid="158770205886688553">"انتقال"</string> - <string name="pip_expand" msgid="1051966011679297308">"گسترده کردن"</string> - <string name="pip_collapse" msgid="3903295106641385962">"جمع کردن"</string> + <string name="pip_move" msgid="1544227837964635439">"انتقال PIP (تصویر در تصویر)"</string> + <string name="pip_expand" msgid="7605396312689038178">"گسترده کردن «تصویر در تصویر»"</string> + <string name="pip_collapse" msgid="5732233773786896094">"جمع کردن «تصویر در تصویر»"</string> <string name="pip_edu_text" msgid="3672999496647508701">" برای کنترلها، دکمه "<annotation icon="home_icon">"صفحه اصلی"</annotation>" را دوبار فشار دهید"</string> - <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"منوی تصویر در تصویر."</string> - <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"انتقال بهچپ"</string> - <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"انتقال بهراست"</string> - <string name="a11y_action_pip_move_up" msgid="98502616918621959">"انتقال بهبالا"</string> - <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"انتقال بهپایین"</string> - <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"تمام"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-fi/strings_tv.xml b/libs/WindowManager/Shell/res/values-fi/strings_tv.xml index 24ab7d99e180..3e8bf9032780 100644 --- a/libs/WindowManager/Shell/res/values-fi/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-fi/strings_tv.xml @@ -19,16 +19,10 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Kuva kuvassa"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Nimetön)"</string> - <string name="pip_close" msgid="2955969519031223530">"Sulje"</string> + <string name="pip_close" msgid="9135220303720555525">"Sulje PIP"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Koko näyttö"</string> - <string name="pip_move" msgid="158770205886688553">"Siirrä"</string> - <string name="pip_expand" msgid="1051966011679297308">"Laajenna"</string> - <string name="pip_collapse" msgid="3903295106641385962">"Tiivistä"</string> + <string name="pip_move" msgid="1544227837964635439">"Siirrä PIP"</string> + <string name="pip_expand" msgid="7605396312689038178">"Laajenna PIP"</string> + <string name="pip_collapse" msgid="5732233773786896094">"Tiivistä PIP"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Asetukset: paina "<annotation icon="home_icon">"ALOITUSNÄYTTÖPAINIKETTA"</annotation>" kahdesti"</string> - <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Kuva kuvassa ‑valikko."</string> - <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Siirrä vasemmalle"</string> - <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Siirrä oikealle"</string> - <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Siirrä ylös"</string> - <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Siirrä alas"</string> - <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Valmis"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-fr-rCA/strings_tv.xml b/libs/WindowManager/Shell/res/values-fr-rCA/strings_tv.xml index 87651ec711d9..66e13b89c64b 100644 --- a/libs/WindowManager/Shell/res/values-fr-rCA/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-fr-rCA/strings_tv.xml @@ -19,16 +19,10 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Incrustation d\'image"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Aucun programme de titre)"</string> - <string name="pip_close" msgid="2955969519031223530">"Fermer"</string> + <string name="pip_close" msgid="9135220303720555525">"Fermer mode IDI"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Plein écran"</string> - <string name="pip_move" msgid="158770205886688553">"Déplacer"</string> - <string name="pip_expand" msgid="1051966011679297308">"Développer"</string> - <string name="pip_collapse" msgid="3903295106641385962">"Réduire"</string> + <string name="pip_move" msgid="1544227837964635439">"Déplacer l\'image incrustée"</string> + <string name="pip_expand" msgid="7605396312689038178">"Développer l\'image incrustée"</string> + <string name="pip_collapse" msgid="5732233773786896094">"Réduire l\'image incrustée"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Appuyez deux fois sur "<annotation icon="home_icon">" ACCUEIL "</annotation>" pour les commandes"</string> - <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Menu d\'incrustation d\'image."</string> - <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Déplacer vers la gauche"</string> - <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Déplacer vers la droite"</string> - <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Déplacer vers le haut"</string> - <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Déplacer vers le bas"</string> - <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"OK"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-fr/strings.xml b/libs/WindowManager/Shell/res/values-fr/strings.xml index 07475055f03e..b3e22af0a3e3 100644 --- a/libs/WindowManager/Shell/res/values-fr/strings.xml +++ b/libs/WindowManager/Shell/res/values-fr/strings.xml @@ -63,7 +63,7 @@ <string name="bubble_dismiss_text" msgid="8816558050659478158">"Fermer la bulle"</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> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"Les nouvelles conversations s\'affichent sous forme d\'icônes flottantes ou bulles. Appuyez sur la bulle pour l\'ouvrir. Faites-la glisser pour la déplacer."</string> <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"Contrôlez les bulles à tout moment"</string> <string name="bubbles_user_education_manage" msgid="3460756219946517198">"Appuyez sur \"Gérer\" pour désactiver les bulles de cette application"</string> <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"OK"</string> diff --git a/libs/WindowManager/Shell/res/values-fr/strings_tv.xml b/libs/WindowManager/Shell/res/values-fr/strings_tv.xml index 37863fb82295..ed9baf5b6215 100644 --- a/libs/WindowManager/Shell/res/values-fr/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-fr/strings_tv.xml @@ -19,16 +19,10 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Picture-in-picture"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Programme sans titre)"</string> - <string name="pip_close" msgid="2955969519031223530">"Fermer"</string> + <string name="pip_close" msgid="9135220303720555525">"Fermer mode PIP"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Plein écran"</string> - <string name="pip_move" msgid="158770205886688553">"Déplacer"</string> - <string name="pip_expand" msgid="1051966011679297308">"Développer"</string> - <string name="pip_collapse" msgid="3903295106641385962">"Réduire"</string> + <string name="pip_move" msgid="1544227837964635439">"Déplacer le PIP"</string> + <string name="pip_expand" msgid="7605396312689038178">"Développer la fenêtre PIP"</string> + <string name="pip_collapse" msgid="5732233773786896094">"Réduire la fenêtre PIP"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Menu de commandes : appuyez deux fois sur "<annotation icon="home_icon">"ACCUEIL"</annotation></string> - <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Menu \"Picture-in-picture\"."</string> - <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Déplacer vers la gauche"</string> - <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Déplacer vers la droite"</string> - <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Déplacer vers le haut"</string> - <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Déplacer vers le bas"</string> - <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"OK"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-gl/strings_tv.xml b/libs/WindowManager/Shell/res/values-gl/strings_tv.xml index 5d6de76c4deb..a057434d7853 100644 --- a/libs/WindowManager/Shell/res/values-gl/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-gl/strings_tv.xml @@ -19,16 +19,10 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Pantalla superposta"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Programa sen título)"</string> - <string name="pip_close" msgid="2955969519031223530">"Pechar"</string> + <string name="pip_close" msgid="9135220303720555525">"Pechar PIP"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Pantalla completa"</string> - <string name="pip_move" msgid="158770205886688553">"Mover"</string> - <string name="pip_expand" msgid="1051966011679297308">"Despregar"</string> - <string name="pip_collapse" msgid="3903295106641385962">"Contraer"</string> + <string name="pip_move" msgid="1544227837964635439">"Mover pantalla superposta"</string> + <string name="pip_expand" msgid="7605396312689038178">"Despregar pantalla superposta"</string> + <string name="pip_collapse" msgid="5732233773786896094">"Contraer pantalla superposta"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Preme "<annotation icon="home_icon">"INICIO"</annotation>" dúas veces para acceder aos controis"</string> - <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Menú de pantalla superposta."</string> - <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Mover cara á esquerda"</string> - <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Mover cara á dereita"</string> - <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Mover cara arriba"</string> - <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Mover cara abaixo"</string> - <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Feito"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-gu/strings_tv.xml b/libs/WindowManager/Shell/res/values-gu/strings_tv.xml index 6c1b9db73582..d9525910e4c6 100644 --- a/libs/WindowManager/Shell/res/values-gu/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-gu/strings_tv.xml @@ -19,16 +19,10 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"ચિત્રમાં-ચિત્ર"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(કોઈ ટાઇટલ પ્રોગ્રામ નથી)"</string> - <string name="pip_close" msgid="2955969519031223530">"બંધ કરો"</string> + <string name="pip_close" msgid="9135220303720555525">"PIP બંધ કરો"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"પૂર્ણ સ્ક્રીન"</string> - <string name="pip_move" msgid="158770205886688553">"ખસેડો"</string> - <string name="pip_expand" msgid="1051966011679297308">"મોટું કરો"</string> - <string name="pip_collapse" msgid="3903295106641385962">"નાનું કરો"</string> + <string name="pip_move" msgid="1544227837964635439">"PIP ખસેડો"</string> + <string name="pip_expand" msgid="7605396312689038178">"PIP મોટી કરો"</string> + <string name="pip_collapse" msgid="5732233773786896094">"PIP નાની કરો"</string> <string name="pip_edu_text" msgid="3672999496647508701">" નિયંત્રણો માટે "<annotation icon="home_icon">" હોમ "</annotation>" બટન પર બે વાર દબાવો"</string> - <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"ચિત્રમાં ચિત્ર મેનૂ."</string> - <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"ડાબે ખસેડો"</string> - <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"જમણે ખસેડો"</string> - <string name="a11y_action_pip_move_up" msgid="98502616918621959">"ઉપર ખસેડો"</string> - <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"નીચે ખસેડો"</string> - <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"થઈ ગયું"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-hi/strings.xml b/libs/WindowManager/Shell/res/values-hi/strings.xml index a5fcb97d1418..36b11514c7e5 100644 --- a/libs/WindowManager/Shell/res/values-hi/strings.xml +++ b/libs/WindowManager/Shell/res/values-hi/strings.xml @@ -65,9 +65,9 @@ <string name="bubbles_user_education_title" msgid="2112319053732691899">"बबल्स का इस्तेमाल करके चैट करें"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"नई बातचीत फ़्लोटिंग आइकॉन या बबल्स की तरह दिखेंगी. बबल को खोलने के लिए टैप करें. इसे एक जगह से दूसरी जगह ले जाने के लिए खींचें और छोड़ें."</string> <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"जब चाहें, बबल्स को कंट्रोल करें"</string> - <string name="bubbles_user_education_manage" msgid="3460756219946517198">"इस ऐप्लिकेशन पर बबल्स को बंद करने के लिए \'मैनेज करें\' पर टैप करें"</string> + <string name="bubbles_user_education_manage" msgid="3460756219946517198">"इस ऐप्लिकेशन पर बबल्स को बंद करने के लिए \'प्रबंधित करें\' पर टैप करें"</string> <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"ठीक है"</string> - <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"हाल ही के कोई बबल्स नहीं हैं"</string> + <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"हाल ही के बबल्स मौजूद नहीं हैं"</string> <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"हाल ही के बबल्स और हटाए गए बबल्स यहां दिखेंगे"</string> <string name="notification_bubble_title" msgid="6082910224488253378">"बबल"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"मैनेज करें"</string> diff --git a/libs/WindowManager/Shell/res/values-hi/strings_tv.xml b/libs/WindowManager/Shell/res/values-hi/strings_tv.xml index e0227253b2dc..d897ac73f80d 100644 --- a/libs/WindowManager/Shell/res/values-hi/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-hi/strings_tv.xml @@ -19,16 +19,10 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"पिक्चर में पिक्चर"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(कोई शीर्षक कार्यक्रम नहीं)"</string> - <string name="pip_close" msgid="2955969519031223530">"बंद करें"</string> + <string name="pip_close" msgid="9135220303720555525">"PIP बंद करें"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"फ़ुल स्क्रीन"</string> - <string name="pip_move" msgid="158770205886688553">"ले जाएं"</string> - <string name="pip_expand" msgid="1051966011679297308">"बड़ा करें"</string> - <string name="pip_collapse" msgid="3903295106641385962">"छोटा करें"</string> + <string name="pip_move" msgid="1544227837964635439">"पीआईपी को दूसरी जगह लेकर जाएं"</string> + <string name="pip_expand" msgid="7605396312689038178">"पीआईपी विंडो को बड़ा करें"</string> + <string name="pip_collapse" msgid="5732233773786896094">"पीआईपी विंडो को छोटा करें"</string> <string name="pip_edu_text" msgid="3672999496647508701">" कंट्रोल मेन्यू पर जाने के लिए, "<annotation icon="home_icon">" होम बटन"</annotation>" दो बार दबाएं"</string> - <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"पिक्चर में पिक्चर मेन्यू."</string> - <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"बाईं ओर ले जाएं"</string> - <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"दाईं ओर ले जाएं"</string> - <string name="a11y_action_pip_move_up" msgid="98502616918621959">"ऊपर ले जाएं"</string> - <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"नीचे ले जाएं"</string> - <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"हो गया"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-hr/strings_tv.xml b/libs/WindowManager/Shell/res/values-hr/strings_tv.xml index a09e6e805f63..8f5f3164c4d7 100644 --- a/libs/WindowManager/Shell/res/values-hr/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-hr/strings_tv.xml @@ -19,16 +19,10 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Slika u slici"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Program bez naslova)"</string> - <string name="pip_close" msgid="2955969519031223530">"Zatvori"</string> + <string name="pip_close" msgid="9135220303720555525">"Zatvori PIP"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Cijeli zaslon"</string> - <string name="pip_move" msgid="158770205886688553">"Premjesti"</string> - <string name="pip_expand" msgid="1051966011679297308">"Proširi"</string> - <string name="pip_collapse" msgid="3903295106641385962">"Sažmi"</string> + <string name="pip_move" msgid="1544227837964635439">"Premjesti PIP"</string> + <string name="pip_expand" msgid="7605396312689038178">"Proširi PIP"</string> + <string name="pip_collapse" msgid="5732233773786896094">"Sažmi PIP"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Dvaput pritisnite "<annotation icon="home_icon">"POČETNI ZASLON"</annotation>" za kontrole"</string> - <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Izbornik slike u slici."</string> - <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Pomaknite ulijevo"</string> - <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Pomaknite udesno"</string> - <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Pomaknite prema gore"</string> - <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Pomaknite prema dolje"</string> - <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Gotovo"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-hu/strings_tv.xml b/libs/WindowManager/Shell/res/values-hu/strings_tv.xml index 5e065c2ad4e7..fc8d79589121 100644 --- a/libs/WindowManager/Shell/res/values-hu/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-hu/strings_tv.xml @@ -19,16 +19,10 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Kép a képben"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Cím nélküli program)"</string> - <string name="pip_close" msgid="2955969519031223530">"Bezárás"</string> + <string name="pip_close" msgid="9135220303720555525">"PIP bezárása"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Teljes képernyő"</string> - <string name="pip_move" msgid="158770205886688553">"Áthelyezés"</string> - <string name="pip_expand" msgid="1051966011679297308">"Kibontás"</string> - <string name="pip_collapse" msgid="3903295106641385962">"Összecsukás"</string> + <string name="pip_move" msgid="1544227837964635439">"PIP áthelyezése"</string> + <string name="pip_expand" msgid="7605396312689038178">"Kép a képben kibontása"</string> + <string name="pip_collapse" msgid="5732233773786896094">"Kép a képben összecsukása"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Vezérlők: "<annotation icon="home_icon">" KEZDŐKÉPERNYŐ "</annotation>" gomb kétszer megnyomva"</string> - <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Kép a képben menü."</string> - <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Mozgatás balra"</string> - <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Mozgatás jobbra"</string> - <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Mozgatás felfelé"</string> - <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Mozgatás lefelé"</string> - <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Kész"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-hy/strings_tv.xml b/libs/WindowManager/Shell/res/values-hy/strings_tv.xml index 7963abf8972b..f5665b8dd166 100644 --- a/libs/WindowManager/Shell/res/values-hy/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-hy/strings_tv.xml @@ -19,16 +19,10 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Նկար նկարի մեջ"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Առանց վերնագրի ծրագիր)"</string> - <string name="pip_close" msgid="2955969519031223530">"Փակել"</string> + <string name="pip_close" msgid="9135220303720555525">"Փակել PIP-ն"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Լիէկրան"</string> - <string name="pip_move" msgid="158770205886688553">"Տեղափոխել"</string> - <string name="pip_expand" msgid="1051966011679297308">"Ծավալել"</string> - <string name="pip_collapse" msgid="3903295106641385962">"Ծալել"</string> + <string name="pip_move" msgid="1544227837964635439">"Տեղափոխել PIP-ը"</string> + <string name="pip_expand" msgid="7605396312689038178">"Ծավալել PIP-ը"</string> + <string name="pip_collapse" msgid="5732233773786896094">"Ծալել PIP-ը"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Կարգավորումների համար կրկնակի սեղմեք "<annotation icon="home_icon">"ԳԼԽԱՎՈՐ ԷԿՐԱՆ"</annotation></string> - <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"«Նկար նկարի մեջ» ռեժիմի ընտրացանկ։"</string> - <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Տեղափոխել ձախ"</string> - <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Տեղափոխել աջ"</string> - <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Տեղափոխել վերև"</string> - <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Տեղափոխել ներքև"</string> - <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Պատրաստ է"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-in/strings_tv.xml b/libs/WindowManager/Shell/res/values-in/strings_tv.xml index 7d37154bb86c..a1535653f679 100644 --- a/libs/WindowManager/Shell/res/values-in/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-in/strings_tv.xml @@ -19,16 +19,10 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Picture-in-Picture"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Program tanpa judul)"</string> - <string name="pip_close" msgid="2955969519031223530">"Tutup"</string> + <string name="pip_close" msgid="9135220303720555525">"Tutup PIP"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Layar penuh"</string> - <string name="pip_move" msgid="158770205886688553">"Pindahkan"</string> - <string name="pip_expand" msgid="1051966011679297308">"Luaskan"</string> - <string name="pip_collapse" msgid="3903295106641385962">"Ciutkan"</string> + <string name="pip_move" msgid="1544227837964635439">"Pindahkan PIP"</string> + <string name="pip_expand" msgid="7605396312689038178">"Luaskan PIP"</string> + <string name="pip_collapse" msgid="5732233773786896094">"Ciutkan PIP"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Tekan dua kali "<annotation icon="home_icon">" HOME "</annotation>" untuk membuka kontrol"</string> - <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Menu Picture-in-Picture."</string> - <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Pindahkan ke kiri"</string> - <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Pindahkan ke kanan"</string> - <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Pindahkan ke atas"</string> - <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Pindahkan ke bawah"</string> - <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Selesai"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-is/strings_tv.xml b/libs/WindowManager/Shell/res/values-is/strings_tv.xml index 1490cb98e034..70ca1afe3aea 100644 --- a/libs/WindowManager/Shell/res/values-is/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-is/strings_tv.xml @@ -19,16 +19,10 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Mynd í mynd"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Efni án titils)"</string> - <string name="pip_close" msgid="2955969519031223530">"Loka"</string> + <string name="pip_close" msgid="9135220303720555525">"Loka mynd í mynd"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Allur skjárinn"</string> - <string name="pip_move" msgid="158770205886688553">"Færa"</string> - <string name="pip_expand" msgid="1051966011679297308">"Stækka"</string> - <string name="pip_collapse" msgid="3903295106641385962">"Minnka"</string> + <string name="pip_move" msgid="1544227837964635439">"Færa innfellda mynd"</string> + <string name="pip_expand" msgid="7605396312689038178">"Stækka innfellda mynd"</string> + <string name="pip_collapse" msgid="5732233773786896094">"Minnka innfellda mynd"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Ýttu tvisvar á "<annotation icon="home_icon">" HEIM "</annotation>" til að opna stillingar"</string> - <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Valmynd fyrir mynd í mynd."</string> - <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Færa til vinstri"</string> - <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Færa til hægri"</string> - <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Færa upp"</string> - <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Færa niður"</string> - <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Lokið"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-it/strings_tv.xml b/libs/WindowManager/Shell/res/values-it/strings_tv.xml index a48516f2588e..cda627517872 100644 --- a/libs/WindowManager/Shell/res/values-it/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-it/strings_tv.xml @@ -19,16 +19,10 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Picture in picture"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Programma senza titolo)"</string> - <string name="pip_close" msgid="2955969519031223530">"Chiudi"</string> + <string name="pip_close" msgid="9135220303720555525">"Chiudi PIP"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Schermo intero"</string> - <string name="pip_move" msgid="158770205886688553">"Sposta"</string> - <string name="pip_expand" msgid="1051966011679297308">"Espandi"</string> - <string name="pip_collapse" msgid="3903295106641385962">"Comprimi"</string> + <string name="pip_move" msgid="1544227837964635439">"Sposta PIP"</string> + <string name="pip_expand" msgid="7605396312689038178">"Espandi PIP"</string> + <string name="pip_collapse" msgid="5732233773786896094">"Comprimi PIP"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Premi due volte "<annotation icon="home_icon">" HOME "</annotation>" per aprire i controlli"</string> - <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Menu Picture in picture."</string> - <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Sposta a sinistra"</string> - <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Sposta a destra"</string> - <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Sposta su"</string> - <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Sposta giù"</string> - <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Fine"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-iw/strings_tv.xml b/libs/WindowManager/Shell/res/values-iw/strings_tv.xml index 2af1896d3c67..30ce97b998ca 100644 --- a/libs/WindowManager/Shell/res/values-iw/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-iw/strings_tv.xml @@ -19,16 +19,10 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"תמונה בתוך תמונה"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(תוכנית ללא כותרת)"</string> - <string name="pip_close" msgid="2955969519031223530">"סגירה"</string> + <string name="pip_close" msgid="9135220303720555525">"סגירת PIP"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"מסך מלא"</string> - <string name="pip_move" msgid="158770205886688553">"העברה"</string> - <string name="pip_expand" msgid="1051966011679297308">"הרחבה"</string> - <string name="pip_collapse" msgid="3903295106641385962">"כיווץ"</string> + <string name="pip_move" msgid="1544227837964635439">"העברת תמונה בתוך תמונה (PIP)"</string> + <string name="pip_expand" msgid="7605396312689038178">"הרחבת חלון תמונה-בתוך-תמונה"</string> + <string name="pip_collapse" msgid="5732233773786896094">"כיווץ של חלון תמונה-בתוך-תמונה"</string> <string name="pip_edu_text" msgid="3672999496647508701">" לחיצה כפולה על "<annotation icon="home_icon">" הלחצן הראשי "</annotation>" תציג את אמצעי הבקרה"</string> - <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"תפריט \'תמונה בתוך תמונה\'."</string> - <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"הזזה שמאלה"</string> - <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"הזזה ימינה"</string> - <string name="a11y_action_pip_move_up" msgid="98502616918621959">"הזזה למעלה"</string> - <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"הזזה למטה"</string> - <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"סיום"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ja/strings_tv.xml b/libs/WindowManager/Shell/res/values-ja/strings_tv.xml index bc7dcb7aa029..e58e7bf6fabc 100644 --- a/libs/WindowManager/Shell/res/values-ja/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-ja/strings_tv.xml @@ -19,16 +19,10 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"ピクチャー イン ピクチャー"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(無題の番組)"</string> - <string name="pip_close" msgid="2955969519031223530">"閉じる"</string> + <string name="pip_close" msgid="9135220303720555525">"PIP を閉じる"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"全画面表示"</string> - <string name="pip_move" msgid="158770205886688553">"移動"</string> - <string name="pip_expand" msgid="1051966011679297308">"開く"</string> - <string name="pip_collapse" msgid="3903295106641385962">"閉じる"</string> + <string name="pip_move" msgid="1544227837964635439">"PIP を移動"</string> + <string name="pip_expand" msgid="7605396312689038178">"PIP を開く"</string> + <string name="pip_collapse" msgid="5732233773786896094">"PIP を閉じる"</string> <string name="pip_edu_text" msgid="3672999496647508701">" コントロールにアクセス: "<annotation icon="home_icon">" ホーム "</annotation>" を 2 回押します"</string> - <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"ピクチャー イン ピクチャーのメニューです。"</string> - <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"左に移動"</string> - <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"右に移動"</string> - <string name="a11y_action_pip_move_up" msgid="98502616918621959">"上に移動"</string> - <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"下に移動"</string> - <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"完了"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ka/strings_tv.xml b/libs/WindowManager/Shell/res/values-ka/strings_tv.xml index 898dac2aca88..b09686646c8b 100644 --- a/libs/WindowManager/Shell/res/values-ka/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-ka/strings_tv.xml @@ -19,16 +19,10 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"ეკრანი ეკრანში"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(პროგრამის სათაურის გარეშე)"</string> - <string name="pip_close" msgid="2955969519031223530">"დახურვა"</string> + <string name="pip_close" msgid="9135220303720555525">"PIP-ის დახურვა"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"სრულ ეკრანზე"</string> - <string name="pip_move" msgid="158770205886688553">"გადაადგილება"</string> - <string name="pip_expand" msgid="1051966011679297308">"გაშლა"</string> - <string name="pip_collapse" msgid="3903295106641385962">"ჩაკეცვა"</string> + <string name="pip_move" msgid="1544227837964635439">"PIP გადატანა"</string> + <string name="pip_expand" msgid="7605396312689038178">"PIP-ის გაშლა"</string> + <string name="pip_collapse" msgid="5732233773786896094">"PIP-ის ჩაკეცვა"</string> <string name="pip_edu_text" msgid="3672999496647508701">" მართვის საშუალებებზე წვდომისთვის ორმაგად დააჭირეთ "<annotation icon="home_icon">" მთავარ ღილაკს "</annotation></string> - <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"მენიუ „ეკრანი ეკრანში“."</string> - <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"მარცხნივ გადატანა"</string> - <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"მარჯვნივ გადატანა"</string> - <string name="a11y_action_pip_move_up" msgid="98502616918621959">"ზემოთ გადატანა"</string> - <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"ქვემოთ გადატანა"</string> - <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"მზადაა"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-kk/strings_tv.xml b/libs/WindowManager/Shell/res/values-kk/strings_tv.xml index cdf564fb4ca0..7bade0dff0d9 100644 --- a/libs/WindowManager/Shell/res/values-kk/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-kk/strings_tv.xml @@ -19,16 +19,10 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Суреттегі сурет"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Атаусыз бағдарлама)"</string> - <string name="pip_close" msgid="2955969519031223530">"Жабу"</string> + <string name="pip_close" msgid="9135220303720555525">"PIP жабу"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Толық экран"</string> - <string name="pip_move" msgid="158770205886688553">"Жылжыту"</string> - <string name="pip_expand" msgid="1051966011679297308">"Жаю"</string> - <string name="pip_collapse" msgid="3903295106641385962">"Жию"</string> + <string name="pip_move" msgid="1544227837964635439">"PIP клипін жылжыту"</string> + <string name="pip_expand" msgid="7605396312689038178">"PIP терезесін жаю"</string> + <string name="pip_collapse" msgid="5732233773786896094">"PIP терезесін жию"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Басқару элементтері: "<annotation icon="home_icon">" НЕГІЗГІ ЭКРАН "</annotation>" түймесін екі рет басыңыз."</string> - <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"\"Сурет ішіндегі сурет\" мәзірі."</string> - <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Солға жылжыту"</string> - <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Оңға жылжыту"</string> - <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Жоғары жылжыту"</string> - <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Төмен жылжыту"</string> - <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Дайын"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-km/strings_tv.xml b/libs/WindowManager/Shell/res/values-km/strings_tv.xml index 1a7ae813c1d3..721be1fc1650 100644 --- a/libs/WindowManager/Shell/res/values-km/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-km/strings_tv.xml @@ -19,16 +19,10 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"រូបក្នុងរូប"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(កម្មវិធីគ្មានចំណងជើង)"</string> - <string name="pip_close" msgid="2955969519031223530">"បិទ"</string> + <string name="pip_close" msgid="9135220303720555525">"បិទ PIP"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"ពេញអេក្រង់"</string> - <string name="pip_move" msgid="158770205886688553">"ផ្លាស់ទី"</string> - <string name="pip_expand" msgid="1051966011679297308">"ពង្រីក"</string> - <string name="pip_collapse" msgid="3903295106641385962">"បង្រួម"</string> + <string name="pip_move" msgid="1544227837964635439">"ផ្លាស់ទី PIP"</string> + <string name="pip_expand" msgid="7605396312689038178">"ពង្រីក PIP"</string> + <string name="pip_collapse" msgid="5732233773786896094">"បង្រួម PIP"</string> <string name="pip_edu_text" msgid="3672999496647508701">" ចុចពីរដងលើ"<annotation icon="home_icon">"ប៊ូតុងដើម"</annotation>" ដើម្បីបើកផ្ទាំងគ្រប់គ្រង"</string> - <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"ម៉ឺនុយរូបក្នុងរូប"</string> - <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"ផ្លាស់ទីទៅឆ្វេង"</string> - <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"ផ្លាស់ទីទៅស្តាំ"</string> - <string name="a11y_action_pip_move_up" msgid="98502616918621959">"ផ្លាស់ទីឡើងលើ"</string> - <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"ផ្លាស់ទីចុះក្រោម"</string> - <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"រួចរាល់"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-kn/strings_tv.xml b/libs/WindowManager/Shell/res/values-kn/strings_tv.xml index 45de068c80a0..8310c8a1169c 100644 --- a/libs/WindowManager/Shell/res/values-kn/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-kn/strings_tv.xml @@ -19,16 +19,10 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"ಚಿತ್ರದಲ್ಲಿ ಚಿತ್ರ"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(ಶೀರ್ಷಿಕೆ ರಹಿತ ಕಾರ್ಯಕ್ರಮ)"</string> - <string name="pip_close" msgid="2955969519031223530">"ಮುಚ್ಚಿರಿ"</string> + <string name="pip_close" msgid="9135220303720555525">"PIP ಮುಚ್ಚಿ"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"ಪೂರ್ಣ ಪರದೆ"</string> - <string name="pip_move" msgid="158770205886688553">"ಸರಿಸಿ"</string> - <string name="pip_expand" msgid="1051966011679297308">"ವಿಸ್ತೃತಗೊಳಿಸಿ"</string> - <string name="pip_collapse" msgid="3903295106641385962">"ಕುಗ್ಗಿಸಿ"</string> + <string name="pip_move" msgid="1544227837964635439">"PIP ಅನ್ನು ಸರಿಸಿ"</string> + <string name="pip_expand" msgid="7605396312689038178">"ಚಿತ್ರದಲ್ಲಿ ಚಿತ್ರವನ್ನು ವಿಸ್ತರಿಸಿ"</string> + <string name="pip_collapse" msgid="5732233773786896094">"ಚಿತ್ರದಲ್ಲಿ ಚಿತ್ರವನ್ನು ಕುಗ್ಗಿಸಿ"</string> <string name="pip_edu_text" msgid="3672999496647508701">" ಕಂಟ್ರೋಲ್ಗಳಿಗಾಗಿ "<annotation icon="home_icon">" ಹೋಮ್ "</annotation>" ಅನ್ನು ಎರಡು ಬಾರಿ ಒತ್ತಿ"</string> - <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"ಚಿತ್ರದಲ್ಲಿ ಚಿತ್ರ ಮೆನು."</string> - <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"ಎಡಕ್ಕೆ ಸರಿಸಿ"</string> - <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"ಬಲಕ್ಕೆ ಸರಿಸಿ"</string> - <string name="a11y_action_pip_move_up" msgid="98502616918621959">"ಮೇಲಕ್ಕೆ ಸರಿಸಿ"</string> - <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"ಕೆಳಗೆ ಸರಿಸಿ"</string> - <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"ಮುಗಿದಿದೆ"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ko/strings_tv.xml b/libs/WindowManager/Shell/res/values-ko/strings_tv.xml index 9e8f1f1258a5..a3e055a515a1 100644 --- a/libs/WindowManager/Shell/res/values-ko/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-ko/strings_tv.xml @@ -19,16 +19,10 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"PIP 모드"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(제목 없는 프로그램)"</string> - <string name="pip_close" msgid="2955969519031223530">"닫기"</string> + <string name="pip_close" msgid="9135220303720555525">"PIP 닫기"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"전체화면"</string> - <string name="pip_move" msgid="158770205886688553">"이동"</string> - <string name="pip_expand" msgid="1051966011679297308">"펼치기"</string> - <string name="pip_collapse" msgid="3903295106641385962">"접기"</string> + <string name="pip_move" msgid="1544227837964635439">"PIP 이동"</string> + <string name="pip_expand" msgid="7605396312689038178">"PIP 펼치기"</string> + <string name="pip_collapse" msgid="5732233773786896094">"PIP 접기"</string> <string name="pip_edu_text" msgid="3672999496647508701">" 제어 메뉴에 액세스하려면 "<annotation icon="home_icon">" 홈 "</annotation>"을 두 번 누르세요."</string> - <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"PIP 모드 메뉴입니다."</string> - <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"왼쪽으로 이동"</string> - <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"오른쪽으로 이동"</string> - <string name="a11y_action_pip_move_up" msgid="98502616918621959">"위로 이동"</string> - <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"아래로 이동"</string> - <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"완료"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ky/strings_tv.xml b/libs/WindowManager/Shell/res/values-ky/strings_tv.xml index 19fac5876bb0..887ac52c8e43 100644 --- a/libs/WindowManager/Shell/res/values-ky/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-ky/strings_tv.xml @@ -19,16 +19,10 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Сүрөттөгү сүрөт"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Аталышы жок программа)"</string> - <string name="pip_close" msgid="2955969519031223530">"Жабуу"</string> + <string name="pip_close" msgid="9135220303720555525">"PIP\'ти жабуу"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Толук экран"</string> - <string name="pip_move" msgid="158770205886688553">"Жылдыруу"</string> - <string name="pip_expand" msgid="1051966011679297308">"Жайып көрсөтүү"</string> - <string name="pip_collapse" msgid="3903295106641385962">"Жыйыштыруу"</string> + <string name="pip_move" msgid="1544227837964635439">"PIP\'ти жылдыруу"</string> + <string name="pip_expand" msgid="7605396312689038178">"PIP\'ти жайып көрсөтүү"</string> + <string name="pip_collapse" msgid="5732233773786896094">"PIP\'ти жыйыштыруу"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Башкаруу элементтерин ачуу үчүн "<annotation icon="home_icon">" БАШКЫ БЕТ "</annotation>" баскычын эки жолу басыңыз"</string> - <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Сүрөт ичиндеги сүрөт менюсу."</string> - <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Солго жылдыруу"</string> - <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Оңго жылдыруу"</string> - <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Жогору жылдыруу"</string> - <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Төмөн жылдыруу"</string> - <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Бүттү"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-lo/strings_tv.xml b/libs/WindowManager/Shell/res/values-lo/strings_tv.xml index 6cd0f37c516c..91c4a033356d 100644 --- a/libs/WindowManager/Shell/res/values-lo/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-lo/strings_tv.xml @@ -19,16 +19,10 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"ການສະແດງຜົນຊ້ອນກັນ"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(ໂປຣແກຣມບໍ່ມີຊື່)"</string> - <string name="pip_close" msgid="2955969519031223530">"ປິດ"</string> + <string name="pip_close" msgid="9135220303720555525">"ປິດ PIP"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"ເຕັມໜ້າຈໍ"</string> - <string name="pip_move" msgid="158770205886688553">"ຍ້າຍ"</string> - <string name="pip_expand" msgid="1051966011679297308">"ຂະຫຍາຍ"</string> - <string name="pip_collapse" msgid="3903295106641385962">"ຫຍໍ້"</string> + <string name="pip_move" msgid="1544227837964635439">"ຍ້າຍ PIP"</string> + <string name="pip_expand" msgid="7605396312689038178">"ຂະຫຍາຍ PIP"</string> + <string name="pip_collapse" msgid="5732233773786896094">"ຫຍໍ້ PIP ລົງ"</string> <string name="pip_edu_text" msgid="3672999496647508701">" ກົດ "<annotation icon="home_icon">" HOME "</annotation>" ສອງເທື່ອສຳລັບການຄວບຄຸມ"</string> - <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"ເມນູການສະແດງຜົນຊ້ອນກັນ."</string> - <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"ຍ້າຍໄປຊ້າຍ"</string> - <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"ຍ້າຍໄປຂວາ"</string> - <string name="a11y_action_pip_move_up" msgid="98502616918621959">"ຍ້າຍຂຶ້ນ"</string> - <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"ຍ້າຍລົງ"</string> - <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"ແລ້ວໆ"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-lt/strings_tv.xml b/libs/WindowManager/Shell/res/values-lt/strings_tv.xml index 52017dca2b94..04265ca01b48 100644 --- a/libs/WindowManager/Shell/res/values-lt/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-lt/strings_tv.xml @@ -19,16 +19,10 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Vaizdas vaizde"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Programa be pavadinimo)"</string> - <string name="pip_close" msgid="2955969519031223530">"Uždaryti"</string> + <string name="pip_close" msgid="9135220303720555525">"Uždaryti PIP"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Visas ekranas"</string> - <string name="pip_move" msgid="158770205886688553">"Perkelti"</string> - <string name="pip_expand" msgid="1051966011679297308">"Išskleisti"</string> - <string name="pip_collapse" msgid="3903295106641385962">"Sutraukti"</string> + <string name="pip_move" msgid="1544227837964635439">"Perkelti PIP"</string> + <string name="pip_expand" msgid="7605396312689038178">"Iškleisti PIP"</string> + <string name="pip_collapse" msgid="5732233773786896094">"Sutraukti PIP"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Jei reikia valdiklių, dukart paspauskite "<annotation icon="home_icon">"PAGRINDINIS"</annotation></string> - <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Vaizdo vaizde meniu."</string> - <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Perkelti kairėn"</string> - <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Perkelti dešinėn"</string> - <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Perkelti aukštyn"</string> - <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Perkelti žemyn"</string> - <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Atlikta"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-lv/strings_tv.xml b/libs/WindowManager/Shell/res/values-lv/strings_tv.xml index 11abac6f6197..8c6191e00833 100644 --- a/libs/WindowManager/Shell/res/values-lv/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-lv/strings_tv.xml @@ -19,16 +19,10 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Attēls attēlā"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Programma bez nosaukuma)"</string> - <string name="pip_close" msgid="2955969519031223530">"Aizvērt"</string> + <string name="pip_close" msgid="9135220303720555525">"Aizvērt PIP"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Pilnekrāna režīms"</string> - <string name="pip_move" msgid="158770205886688553">"Pārvietot"</string> - <string name="pip_expand" msgid="1051966011679297308">"Izvērst"</string> - <string name="pip_collapse" msgid="3903295106641385962">"Sakļaut"</string> + <string name="pip_move" msgid="1544227837964635439">"Pārvietot attēlu attēlā"</string> + <string name="pip_expand" msgid="7605396312689038178">"Izvērst “Attēls attēlā” logu"</string> + <string name="pip_collapse" msgid="5732233773786896094">"Sakļaut “Attēls attēlā” logu"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Atvērt vadīklas: divreiz nospiediet pogu "<annotation icon="home_icon">"SĀKUMS"</annotation></string> - <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Izvēlne attēlam attēlā."</string> - <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Pārvietot pa kreisi"</string> - <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Pārvietot pa labi"</string> - <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Pārvietot augšup"</string> - <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Pārvietot lejup"</string> - <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Gatavs"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-mk/strings_tv.xml b/libs/WindowManager/Shell/res/values-mk/strings_tv.xml index 21293223b882..beef1fef862b 100644 --- a/libs/WindowManager/Shell/res/values-mk/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-mk/strings_tv.xml @@ -19,16 +19,10 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Слика во слика"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Програма без наслов)"</string> - <string name="pip_close" msgid="2955969519031223530">"Затвори"</string> + <string name="pip_close" msgid="9135220303720555525">"Затвори PIP"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Цел екран"</string> - <string name="pip_move" msgid="158770205886688553">"Премести"</string> - <string name="pip_expand" msgid="1051966011679297308">"Прошири"</string> - <string name="pip_collapse" msgid="3903295106641385962">"Собери"</string> + <string name="pip_move" msgid="1544227837964635439">"Премести PIP"</string> + <string name="pip_expand" msgid="7605396312689038178">"Прошири ја сликата во слика"</string> + <string name="pip_collapse" msgid="5732233773786896094">"Собери ја сликата во слика"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Притиснете двапати на "<annotation icon="home_icon">" HOME "</annotation>" за контроли"</string> - <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Мени за „Слика во слика“."</string> - <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Премести налево"</string> - <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Премести надесно"</string> - <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Премести нагоре"</string> - <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Премести надолу"</string> - <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Готово"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ml/strings_tv.xml b/libs/WindowManager/Shell/res/values-ml/strings_tv.xml index 549e39b21101..c2a532d09647 100644 --- a/libs/WindowManager/Shell/res/values-ml/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-ml/strings_tv.xml @@ -19,16 +19,10 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"ചിത്രത്തിനുള്ളിൽ ചിത്രം"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(പേരില്ലാത്ത പ്രോഗ്രാം)"</string> - <string name="pip_close" msgid="2955969519031223530">"അടയ്ക്കുക"</string> + <string name="pip_close" msgid="9135220303720555525">"PIP അടയ്ക്കുക"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"പൂര്ണ്ണ സ്ക്രീന്"</string> - <string name="pip_move" msgid="158770205886688553">"നീക്കുക"</string> - <string name="pip_expand" msgid="1051966011679297308">"വികസിപ്പിക്കുക"</string> - <string name="pip_collapse" msgid="3903295106641385962">"ചുരുക്കുക"</string> + <string name="pip_move" msgid="1544227837964635439">"PIP നീക്കുക"</string> + <string name="pip_expand" msgid="7605396312689038178">"PIP വികസിപ്പിക്കുക"</string> + <string name="pip_collapse" msgid="5732233773786896094">"PIP ചുരുക്കുക"</string> <string name="pip_edu_text" msgid="3672999496647508701">" നിയന്ത്രണങ്ങൾക്കായി "<annotation icon="home_icon">" ഹോം "</annotation>" രണ്ട് തവണ അമർത്തുക"</string> - <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"ചിത്രത്തിനുള്ളിൽ ചിത്രം മെനു."</string> - <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"ഇടത്തേക്ക് നീക്കുക"</string> - <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"വലത്തേക്ക് നീക്കുക"</string> - <string name="a11y_action_pip_move_up" msgid="98502616918621959">"മുകളിലേക്ക് നീക്കുക"</string> - <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"താഴേക്ക് നീക്കുക"</string> - <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"പൂർത്തിയായി"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-mn/strings_tv.xml b/libs/WindowManager/Shell/res/values-mn/strings_tv.xml index 9a85d96ca602..bf8c59b57359 100644 --- a/libs/WindowManager/Shell/res/values-mn/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-mn/strings_tv.xml @@ -19,16 +19,10 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Дэлгэц доторх дэлгэц"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Гарчиггүй хөтөлбөр)"</string> - <string name="pip_close" msgid="2955969519031223530">"Хаах"</string> + <string name="pip_close" msgid="9135220303720555525">"PIP-г хаах"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Бүтэн дэлгэц"</string> - <string name="pip_move" msgid="158770205886688553">"Зөөх"</string> - <string name="pip_expand" msgid="1051966011679297308">"Дэлгэх"</string> - <string name="pip_collapse" msgid="3903295106641385962">"Хураах"</string> + <string name="pip_move" msgid="1544227837964635439">"PIP-г зөөх"</string> + <string name="pip_expand" msgid="7605396312689038178">"PIP-г дэлгэх"</string> + <string name="pip_collapse" msgid="5732233773786896094">"PIP-г хураах"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Хяналтад хандах бол "<annotation icon="home_icon">" HOME "</annotation>" дээр хоёр дарна уу"</string> - <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Дэлгэцэн доторх дэлгэцийн цэс."</string> - <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Зүүн тийш зөөх"</string> - <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Баруун тийш зөөх"</string> - <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Дээш зөөх"</string> - <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Доош зөөх"</string> - <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Болсон"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-mr/strings_tv.xml b/libs/WindowManager/Shell/res/values-mr/strings_tv.xml index a9779b3a3e89..5d519b7afe9a 100644 --- a/libs/WindowManager/Shell/res/values-mr/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-mr/strings_tv.xml @@ -19,16 +19,10 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"चित्रात-चित्र"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(शीर्षक नसलेला कार्यक्रम)"</string> - <string name="pip_close" msgid="2955969519031223530">"बंद करा"</string> + <string name="pip_close" msgid="9135220303720555525">"PIP बंद करा"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"फुल स्क्रीन"</string> - <string name="pip_move" msgid="158770205886688553">"हलवा"</string> - <string name="pip_expand" msgid="1051966011679297308">"विस्तार करा"</string> - <string name="pip_collapse" msgid="3903295106641385962">"कोलॅप्स करा"</string> + <string name="pip_move" msgid="1544227837964635439">"PIP हलवा"</string> + <string name="pip_expand" msgid="7605396312689038178">"PIP चा विस्तार करा"</string> + <string name="pip_collapse" msgid="5732233773786896094">"PIP कोलॅप्स करा"</string> <string name="pip_edu_text" msgid="3672999496647508701">" नियंत्रणांसाठी "<annotation icon="home_icon">" होम "</annotation>" दोनदा दाबा"</string> - <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"चित्रात-चित्र मेनू."</string> - <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"डावीकडे हलवा"</string> - <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"उजवीकडे हलवा"</string> - <string name="a11y_action_pip_move_up" msgid="98502616918621959">"वर हलवा"</string> - <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"खाली हलवा"</string> - <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"पूर्ण झाले"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ms/strings_tv.xml b/libs/WindowManager/Shell/res/values-ms/strings_tv.xml index 8fe992d9f3b9..08642c47c91a 100644 --- a/libs/WindowManager/Shell/res/values-ms/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-ms/strings_tv.xml @@ -19,16 +19,10 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Gambar dalam Gambar"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Program tiada tajuk)"</string> - <string name="pip_close" msgid="2955969519031223530">"Tutup"</string> + <string name="pip_close" msgid="9135220303720555525">"Tutup PIP"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Skrin penuh"</string> - <string name="pip_move" msgid="158770205886688553">"Alih"</string> - <string name="pip_expand" msgid="1051966011679297308">"Kembangkan"</string> - <string name="pip_collapse" msgid="3903295106641385962">"Kuncupkan"</string> + <string name="pip_move" msgid="1544227837964635439">"Alihkan PIP"</string> + <string name="pip_expand" msgid="7605396312689038178">"Kembangkan PIP"</string> + <string name="pip_collapse" msgid="5732233773786896094">"Kuncupkan PIP"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Tekan dua kali "<annotation icon="home_icon">" LAMAN UTAMA "</annotation>" untuk mengakses kawalan"</string> - <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Menu Gambar dalam Gambar."</string> - <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Alih ke kiri"</string> - <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Alih ke kanan"</string> - <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Alih ke atas"</string> - <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Alih ke bawah"</string> - <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Selesai"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-my/strings_tv.xml b/libs/WindowManager/Shell/res/values-my/strings_tv.xml index 105628d8149e..e01daee115ca 100644 --- a/libs/WindowManager/Shell/res/values-my/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-my/strings_tv.xml @@ -19,16 +19,10 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"နှစ်ခုထပ်၍ကြည့်ခြင်း"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(ခေါင်းစဉ်မဲ့ အစီအစဉ်)"</string> - <string name="pip_close" msgid="2955969519031223530">"ပိတ်ရန်"</string> + <string name="pip_close" msgid="9135220303720555525">"PIP ကိုပိတ်ပါ"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"မျက်နှာပြင် အပြည့်"</string> - <string name="pip_move" msgid="158770205886688553">"ရွှေ့ရန်"</string> - <string name="pip_expand" msgid="1051966011679297308">"ချဲ့ရန်"</string> - <string name="pip_collapse" msgid="3903295106641385962">"လျှော့ပြရန်"</string> + <string name="pip_move" msgid="1544227837964635439">"PIP ရွှေ့ရန်"</string> + <string name="pip_expand" msgid="7605396312689038178">"PIP ကို ချဲ့ရန်"</string> + <string name="pip_collapse" msgid="5732233773786896094">"PIP ကို လျှော့ပြပါ"</string> <string name="pip_edu_text" msgid="3672999496647508701">" ထိန်းချုပ်မှုအတွက် "<annotation icon="home_icon">" ပင်မခလုတ် "</annotation>" နှစ်ချက်နှိပ်ပါ"</string> - <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"နှစ်ခုထပ်၍ ကြည့်ခြင်းမီနူး။"</string> - <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"ဘယ်သို့ရွှေ့ရန်"</string> - <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"ညာသို့ရွှေ့ရန်"</string> - <string name="a11y_action_pip_move_up" msgid="98502616918621959">"အပေါ်သို့ရွှေ့ရန်"</string> - <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"အောက်သို့ရွှေ့ရန်"</string> - <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"ပြီးပြီ"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-nb/strings.xml b/libs/WindowManager/Shell/res/values-nb/strings.xml index 2f2fea6eb833..9fd42b2f129c 100644 --- a/libs/WindowManager/Shell/res/values-nb/strings.xml +++ b/libs/WindowManager/Shell/res/values-nb/strings.xml @@ -63,7 +63,7 @@ <string name="bubble_dismiss_text" msgid="8816558050659478158">"Lukk boblen"</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> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"Nye samtaler vises som flytende ikoner eller bobler. Trykk for å åpne bobler. Dra for å flytte dem."</string> <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"Kontrollér bobler når som helst"</string> <string name="bubbles_user_education_manage" msgid="3460756219946517198">"Trykk på Administrer for å slå av bobler for denne appen"</string> <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"Greit"</string> diff --git a/libs/WindowManager/Shell/res/values-nb/strings_tv.xml b/libs/WindowManager/Shell/res/values-nb/strings_tv.xml index ca63518df7a5..65ed0b7f5bff 100644 --- a/libs/WindowManager/Shell/res/values-nb/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-nb/strings_tv.xml @@ -19,16 +19,10 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Bilde-i-bilde"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Program uten tittel)"</string> - <string name="pip_close" msgid="2955969519031223530">"Lukk"</string> + <string name="pip_close" msgid="9135220303720555525">"Lukk PIP"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Fullskjerm"</string> - <string name="pip_move" msgid="158770205886688553">"Flytt"</string> - <string name="pip_expand" msgid="1051966011679297308">"Vis"</string> - <string name="pip_collapse" msgid="3903295106641385962">"Skjul"</string> + <string name="pip_move" msgid="1544227837964635439">"Flytt BIB"</string> + <string name="pip_expand" msgid="7605396312689038178">"Vis BIB"</string> + <string name="pip_collapse" msgid="5732233773786896094">"Skjul BIB"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Dobbelttrykk på "<annotation icon="home_icon">"HJEM"</annotation>" for å åpne kontroller"</string> - <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Bilde-i-bilde-meny."</string> - <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Flytt til venstre"</string> - <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Flytt til høyre"</string> - <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Flytt opp"</string> - <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Flytt ned"</string> - <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Ferdig"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ne/strings_tv.xml b/libs/WindowManager/Shell/res/values-ne/strings_tv.xml index 7cbf9e294e7b..d33fed67efb6 100644 --- a/libs/WindowManager/Shell/res/values-ne/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-ne/strings_tv.xml @@ -19,16 +19,10 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Picture-in-Picture"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(शीर्षकविहीन कार्यक्रम)"</string> - <string name="pip_close" msgid="2955969519031223530">"बन्द गर्नुहोस्"</string> + <string name="pip_close" msgid="9135220303720555525">"PIP लाई बन्द गर्नुहोस्"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"फुल स्क्रिन"</string> - <string name="pip_move" msgid="158770205886688553">"सार्नुहोस्"</string> - <string name="pip_expand" msgid="1051966011679297308">"एक्स्पान्ड गर्नुहोस्"</string> - <string name="pip_collapse" msgid="3903295106641385962">"कोल्याप्स गर्नुहोस्"</string> + <string name="pip_move" msgid="1544227837964635439">"PIP सार्नुहोस्"</string> + <string name="pip_expand" msgid="7605396312689038178">"PIP विन्डो एक्स्पान्ड गर्नु…"</string> + <string name="pip_collapse" msgid="5732233773786896094">"PIP विन्डो कोल्याप्स गर्नुहोस्"</string> <string name="pip_edu_text" msgid="3672999496647508701">" कन्ट्रोल मेनु खोल्न "<annotation icon="home_icon">" होम "</annotation>" बटन दुई पटक थिच्नुहोस्"</string> - <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"\"picture-in-picture\" मेनु।"</string> - <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"बायाँतिर सार्नुहोस्"</string> - <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"दायाँतिर सार्नुहोस्"</string> - <string name="a11y_action_pip_move_up" msgid="98502616918621959">"माथितिर सार्नुहोस्"</string> - <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"तलतिर सार्नुहोस्"</string> - <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"सम्पन्न भयो"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-nl/strings_tv.xml b/libs/WindowManager/Shell/res/values-nl/strings_tv.xml index 2deaeddc4080..9763c5665ab2 100644 --- a/libs/WindowManager/Shell/res/values-nl/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-nl/strings_tv.xml @@ -19,16 +19,10 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Scherm-in-scherm"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Naamloos programma)"</string> - <string name="pip_close" msgid="2955969519031223530">"Sluiten"</string> + <string name="pip_close" msgid="9135220303720555525">"PIP sluiten"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Volledig scherm"</string> - <string name="pip_move" msgid="158770205886688553">"Verplaatsen"</string> - <string name="pip_expand" msgid="1051966011679297308">"Uitvouwen"</string> - <string name="pip_collapse" msgid="3903295106641385962">"Samenvouwen"</string> + <string name="pip_move" msgid="1544227837964635439">"SIS verplaatsen"</string> + <string name="pip_expand" msgid="7605396312689038178">"SIS uitvouwen"</string> + <string name="pip_collapse" msgid="5732233773786896094">"SIS samenvouwen"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Druk twee keer op "<annotation icon="home_icon">" HOME "</annotation>" voor bedieningselementen"</string> - <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Scherm-in-scherm-menu."</string> - <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Naar links verplaatsen"</string> - <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Naar rechts verplaatsen"</string> - <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Omhoog verplaatsen"</string> - <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Omlaag verplaatsen"</string> - <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Klaar"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-or/strings_tv.xml b/libs/WindowManager/Shell/res/values-or/strings_tv.xml index 0c1d99e4ca71..e0344855bd1f 100644 --- a/libs/WindowManager/Shell/res/values-or/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-or/strings_tv.xml @@ -19,16 +19,10 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"ପିକଚର୍-ଇନ୍-ପିକଚର୍"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(କୌଣସି ଟାଇଟଲ୍ ପ୍ରୋଗ୍ରାମ୍ ନାହିଁ)"</string> - <string name="pip_close" msgid="2955969519031223530">"ବନ୍ଦ କରନ୍ତୁ"</string> + <string name="pip_close" msgid="9135220303720555525">"PIP ବନ୍ଦ କରନ୍ତୁ"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"ପୂର୍ଣ୍ଣ ସ୍କ୍ରୀନ୍"</string> - <string name="pip_move" msgid="158770205886688553">"ମୁଭ କରନ୍ତୁ"</string> - <string name="pip_expand" msgid="1051966011679297308">"ବିସ୍ତାର କରନ୍ତୁ"</string> - <string name="pip_collapse" msgid="3903295106641385962">"ସଙ୍କୁଚିତ କରନ୍ତୁ"</string> + <string name="pip_move" msgid="1544227837964635439">"PIPକୁ ମୁଭ କରନ୍ତୁ"</string> + <string name="pip_expand" msgid="7605396312689038178">"PIPକୁ ବିସ୍ତାର କରନ୍ତୁ"</string> + <string name="pip_collapse" msgid="5732233773786896094">"PIPକୁ ସଙ୍କୁଚିତ କରନ୍ତୁ"</string> <string name="pip_edu_text" msgid="3672999496647508701">" ନିୟନ୍ତ୍ରଣଗୁଡ଼ିକ ପାଇଁ "<annotation icon="home_icon">" ହୋମ ବଟନ "</annotation>"କୁ ଦୁଇଥର ଦବାନ୍ତୁ"</string> - <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"ପିକଚର-ଇନ-ପିକଚର ମେନୁ।"</string> - <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"ବାମକୁ ମୁଭ କରନ୍ତୁ"</string> - <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"ଡାହାଣକୁ ମୁଭ କରନ୍ତୁ"</string> - <string name="a11y_action_pip_move_up" msgid="98502616918621959">"ଉପରକୁ ମୁଭ କରନ୍ତୁ"</string> - <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"ତଳକୁ ମୁଭ କରନ୍ତୁ"</string> - <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"ହୋଇଗଲା"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-pa/strings_tv.xml b/libs/WindowManager/Shell/res/values-pa/strings_tv.xml index a1edde738775..9c01ac3f3cc0 100644 --- a/libs/WindowManager/Shell/res/values-pa/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-pa/strings_tv.xml @@ -19,16 +19,10 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"ਤਸਵੀਰ-ਵਿੱਚ-ਤਸਵੀਰ"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(ਸਿਰਲੇਖ-ਰਹਿਤ ਪ੍ਰੋਗਰਾਮ)"</string> - <string name="pip_close" msgid="2955969519031223530">"ਬੰਦ ਕਰੋ"</string> + <string name="pip_close" msgid="9135220303720555525">"PIP ਬੰਦ ਕਰੋ"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"ਪੂਰੀ ਸਕ੍ਰੀਨ"</string> - <string name="pip_move" msgid="158770205886688553">"ਲਿਜਾਓ"</string> - <string name="pip_expand" msgid="1051966011679297308">"ਵਿਸਤਾਰ ਕਰੋ"</string> - <string name="pip_collapse" msgid="3903295106641385962">"ਸਮੇਟੋ"</string> + <string name="pip_move" msgid="1544227837964635439">"PIP ਨੂੰ ਲਿਜਾਓ"</string> + <string name="pip_expand" msgid="7605396312689038178">"PIP ਦਾ ਵਿਸਤਾਰ ਕਰੋ"</string> + <string name="pip_collapse" msgid="5732233773786896094">"PIP ਨੂੰ ਸਮੇਟੋ"</string> <string name="pip_edu_text" msgid="3672999496647508701">" ਕੰਟਰੋਲਾਂ ਲਈ "<annotation icon="home_icon">" ਹੋਮ ਬਟਨ "</annotation>" ਨੂੰ ਦੋ ਵਾਰ ਦਬਾਓ"</string> - <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"ਤਸਵੀਰ-ਵਿੱਚ-ਤਸਵੀਰ ਮੀਨੂ।"</string> - <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"ਖੱਬੇ ਲਿਜਾਓ"</string> - <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"ਸੱਜੇ ਲਿਜਾਓ"</string> - <string name="a11y_action_pip_move_up" msgid="98502616918621959">"ਉੱਪਰ ਲਿਜਾਓ"</string> - <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"ਹੇਠਾਂ ਲਿਜਾਓ"</string> - <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"ਹੋ ਗਿਆ"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-pl/strings_tv.xml b/libs/WindowManager/Shell/res/values-pl/strings_tv.xml index 2bb90addc241..b922e2d5a6ba 100644 --- a/libs/WindowManager/Shell/res/values-pl/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-pl/strings_tv.xml @@ -19,16 +19,10 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Obraz w obrazie"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Program bez tytułu)"</string> - <string name="pip_close" msgid="2955969519031223530">"Zamknij"</string> + <string name="pip_close" msgid="9135220303720555525">"Zamknij PIP"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Pełny ekran"</string> - <string name="pip_move" msgid="158770205886688553">"Przenieś"</string> - <string name="pip_expand" msgid="1051966011679297308">"Rozwiń"</string> - <string name="pip_collapse" msgid="3903295106641385962">"Zwiń"</string> + <string name="pip_move" msgid="1544227837964635439">"Przenieś PIP"</string> + <string name="pip_expand" msgid="7605396312689038178">"Rozwiń PIP"</string> + <string name="pip_collapse" msgid="5732233773786896094">"Zwiń PIP"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Naciśnij dwukrotnie "<annotation icon="home_icon">"EKRAN GŁÓWNY"</annotation>", aby wyświetlić ustawienia"</string> - <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Menu funkcji Obraz w obrazie."</string> - <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Przenieś w lewo"</string> - <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Przenieś w prawo"</string> - <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Przenieś w górę"</string> - <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Przenieś w dół"</string> - <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Gotowe"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-pt-rBR/strings_tv.xml b/libs/WindowManager/Shell/res/values-pt-rBR/strings_tv.xml index 14d1c34fd3e8..cc4eb3c32c1f 100644 --- a/libs/WindowManager/Shell/res/values-pt-rBR/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-pt-rBR/strings_tv.xml @@ -19,16 +19,10 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Picture-in-picture"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(programa sem título)"</string> - <string name="pip_close" msgid="2955969519031223530">"Fechar"</string> + <string name="pip_close" msgid="9135220303720555525">"Fechar PIP"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Tela cheia"</string> - <string name="pip_move" msgid="158770205886688553">"Mover"</string> - <string name="pip_expand" msgid="1051966011679297308">"Abrir"</string> - <string name="pip_collapse" msgid="3903295106641385962">"Fechar"</string> + <string name="pip_move" msgid="1544227837964635439">"Mover picture-in-picture"</string> + <string name="pip_expand" msgid="7605396312689038178">"Abrir picture-in-picture"</string> + <string name="pip_collapse" msgid="5732233773786896094">"Fechar picture-in-picture"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Pressione o botão "<annotation icon="home_icon">"home"</annotation>" duas vezes para acessar os controles"</string> - <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Menu do picture-in-picture"</string> - <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Mover para a esquerda"</string> - <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Mover para a direita"</string> - <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Mover para cima"</string> - <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Mover para baixo"</string> - <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Concluído"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-pt-rPT/strings_tv.xml b/libs/WindowManager/Shell/res/values-pt-rPT/strings_tv.xml index 1ada4508714a..c4ae78d89ba8 100644 --- a/libs/WindowManager/Shell/res/values-pt-rPT/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-pt-rPT/strings_tv.xml @@ -19,16 +19,10 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Ecrã no ecrã"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Sem título do programa)"</string> - <string name="pip_close" msgid="2955969519031223530">"Fechar"</string> + <string name="pip_close" msgid="9135220303720555525">"Fechar PIP"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Ecrã inteiro"</string> - <string name="pip_move" msgid="158770205886688553">"Mover"</string> - <string name="pip_expand" msgid="1051966011679297308">"Expandir"</string> - <string name="pip_collapse" msgid="3903295106641385962">"Reduzir"</string> + <string name="pip_move" msgid="1544227837964635439">"Mover Ecrã no ecrã"</string> + <string name="pip_expand" msgid="7605396312689038178">"Expandir Ecrã no ecrã"</string> + <string name="pip_collapse" msgid="5732233773786896094">"Reduzir Ecrã no ecrã"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Prima duas vezes "<annotation icon="home_icon">" PÁGINA INICIAL "</annotation>" para controlos"</string> - <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Menu de ecrã no ecrã."</string> - <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Mover para a esquerda"</string> - <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Mover para a direita"</string> - <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Mover para cima"</string> - <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Mover para baixo"</string> - <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Concluído"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-pt/strings_tv.xml b/libs/WindowManager/Shell/res/values-pt/strings_tv.xml index 14d1c34fd3e8..cc4eb3c32c1f 100644 --- a/libs/WindowManager/Shell/res/values-pt/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-pt/strings_tv.xml @@ -19,16 +19,10 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Picture-in-picture"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(programa sem título)"</string> - <string name="pip_close" msgid="2955969519031223530">"Fechar"</string> + <string name="pip_close" msgid="9135220303720555525">"Fechar PIP"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Tela cheia"</string> - <string name="pip_move" msgid="158770205886688553">"Mover"</string> - <string name="pip_expand" msgid="1051966011679297308">"Abrir"</string> - <string name="pip_collapse" msgid="3903295106641385962">"Fechar"</string> + <string name="pip_move" msgid="1544227837964635439">"Mover picture-in-picture"</string> + <string name="pip_expand" msgid="7605396312689038178">"Abrir picture-in-picture"</string> + <string name="pip_collapse" msgid="5732233773786896094">"Fechar picture-in-picture"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Pressione o botão "<annotation icon="home_icon">"home"</annotation>" duas vezes para acessar os controles"</string> - <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Menu do picture-in-picture"</string> - <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Mover para a esquerda"</string> - <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Mover para a direita"</string> - <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Mover para cima"</string> - <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Mover para baixo"</string> - <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Concluído"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ro/strings.xml b/libs/WindowManager/Shell/res/values-ro/strings.xml index ba3701dc734c..804d34f980ff 100644 --- a/libs/WindowManager/Shell/res/values-ro/strings.xml +++ b/libs/WindowManager/Shell/res/values-ro/strings.xml @@ -17,20 +17,20 @@ <resources xmlns:android="http://schemas.android.com/apk/res/android" xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> - <string name="pip_phone_close" msgid="5783752637260411309">"Închide"</string> - <string name="pip_phone_expand" msgid="2579292903468287504">"Extinde"</string> + <string name="pip_phone_close" msgid="5783752637260411309">"Închideți"</string> + <string name="pip_phone_expand" msgid="2579292903468287504">"Extindeți"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Setări"</string> - <string name="pip_phone_enter_split" msgid="7042877263880641911">"Accesează ecranul împărțit"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Accesați ecranul împărțit"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Meniu"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> este în modul picture-in-picture"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Dacă nu doriți ca <xliff:g id="NAME">%s</xliff:g> să utilizeze această funcție, atingeți pentru a deschide setările și dezactivați-o."</string> - <string name="pip_play" msgid="3496151081459417097">"Redă"</string> - <string name="pip_pause" msgid="690688849510295232">"Întrerupe"</string> + <string name="pip_play" msgid="3496151081459417097">"Redați"</string> + <string name="pip_pause" msgid="690688849510295232">"Întrerupeți"</string> <string name="pip_skip_to_next" msgid="8403429188794867653">"Treceți la următorul"</string> <string name="pip_skip_to_prev" msgid="7172158111196394092">"Treceți la cel anterior"</string> <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Redimensionați"</string> <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Stocați"</string> - <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Anulează stocarea"</string> + <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Anulați stocarea"</string> <string name="dock_forced_resizable" msgid="1749750436092293116">"Este posibil ca aplicația să nu funcționeze cu ecranul împărțit."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Aplicația nu acceptă ecranul împărțit."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Este posibil ca aplicația să nu funcționeze pe un ecran secundar."</string> @@ -48,19 +48,19 @@ <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Partea de jos pe ecran complet"</string> <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Folosirea modului cu o mână"</string> <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Pentru a ieși, glisați în sus din partea de jos a ecranului sau atingeți oriunde deasupra ferestrei aplicației"</string> - <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Activează modul cu o mână"</string> + <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Activați modul cu o mână"</string> <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"Părăsiți modul cu o mână"</string> <string name="bubbles_settings_button_description" msgid="1301286017420516912">"Setări pentru baloanele <xliff:g id="APP_NAME">%1$s</xliff:g>"</string> <string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"Suplimentar"</string> - <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"Adaugă înapoi în stivă"</string> + <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"Adăugați înapoi în stivă"</string> <string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> de la <xliff:g id="APP_NAME">%2$s</xliff:g>"</string> <string name="bubble_content_description_stack" msgid="8071515017164630429">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> de la <xliff:g id="APP_NAME">%2$s</xliff:g> și încă <xliff:g id="BUBBLE_COUNT">%3$d</xliff:g>"</string> - <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"Mută în stânga sus"</string> - <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_move_top_left" msgid="2644118920500782758">"Mutați în stânga sus"</string> + <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Mutați în dreapta sus"</string> + <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Mutați în stânga jos"</string> + <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Mutați în dreapta jos"</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_dismiss_text" msgid="8816558050659478158">"Închideți balonul"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Nu afișați 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ți pentru a deschide balonul. Trageți pentru a-l muta."</string> @@ -70,15 +70,15 @@ <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"Nu există baloane recente"</string> <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"Baloanele recente și baloanele respinse vor apărea aici"</string> <string name="notification_bubble_title" msgid="6082910224488253378">"Balon"</string> - <string name="manage_bubbles_text" msgid="7730624269650594419">"Gestionează"</string> + <string name="manage_bubbles_text" msgid="7730624269650594419">"Gestionați"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Balonul a fost respins."</string> <string name="restart_button_description" msgid="5887656107651190519">"Atingeți ca să reporniți aplicația și să treceți în modul ecran complet."</string> <string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"Aveți probleme cu camera foto?\nAtingeți pentru a reîncadra"</string> <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Nu ați remediat problema?\nAtingeți pentru a reveni"</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Nu aveți probleme cu camera foto? Atingeți pentru a închide."</string> <string name="letterbox_education_dialog_title" msgid="6688664582871779215">"Unele aplicații funcționează cel mai bine în orientarea portret"</string> - <string name="letterbox_education_dialog_subtext" msgid="4853542518367719562">"Încearcă una dintre aceste opțiuni pentru a profita din plin de spațiu"</string> - <string name="letterbox_education_screen_rotation_text" msgid="5085786687366339027">"Rotește dispozitivul pentru a trece în modul ecran complet"</string> - <string name="letterbox_education_reposition_text" msgid="1068293354123934727">"Atinge de două ori lângă o aplicație pentru a o repoziționa"</string> + <string name="letterbox_education_dialog_subtext" msgid="4853542518367719562">"Încercați una dintre aceste opțiuni pentru a profita din plin de spațiu"</string> + <string name="letterbox_education_screen_rotation_text" msgid="5085786687366339027">"Rotiți dispozitivul pentru a trece în modul ecran complet"</string> + <string name="letterbox_education_reposition_text" msgid="1068293354123934727">"Atingeți de două ori lângă o aplicație pentru a o repoziționa"</string> <string name="letterbox_education_got_it" msgid="4057634570866051177">"OK"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ro/strings_tv.xml b/libs/WindowManager/Shell/res/values-ro/strings_tv.xml index 36df2864a752..86a30f49df15 100644 --- a/libs/WindowManager/Shell/res/values-ro/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-ro/strings_tv.xml @@ -19,16 +19,10 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Picture-in-picture"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Program fără titlu)"</string> - <string name="pip_close" msgid="2955969519031223530">"Închide"</string> + <string name="pip_close" msgid="9135220303720555525">"Închideți PIP"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Ecran complet"</string> - <string name="pip_move" msgid="158770205886688553">"Mută"</string> - <string name="pip_expand" msgid="1051966011679297308">"Extinde"</string> - <string name="pip_collapse" msgid="3903295106641385962">"Restrânge"</string> - <string name="pip_edu_text" msgid="3672999496647508701">" Apasă de două ori "<annotation icon="home_icon">"butonul ecran de pornire"</annotation>" pentru comenzi"</string> - <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Meniu picture-in-picture."</string> - <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Mută spre stânga"</string> - <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Mută spre dreapta"</string> - <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Mută în sus"</string> - <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Mută în jos"</string> - <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Gata"</string> + <string name="pip_move" msgid="1544227837964635439">"Mutați fereastra PIP"</string> + <string name="pip_expand" msgid="7605396312689038178">"Extindeți fereastra PIP"</string> + <string name="pip_collapse" msgid="5732233773786896094">"Restrângeți fereastra PIP"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Apăsați de două ori "<annotation icon="home_icon">"butonul ecran de pornire"</annotation>" pentru comenzi"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ru/strings_tv.xml b/libs/WindowManager/Shell/res/values-ru/strings_tv.xml index e7f55ec1bc57..08623e1e69c5 100644 --- a/libs/WindowManager/Shell/res/values-ru/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-ru/strings_tv.xml @@ -19,16 +19,10 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Картинка в картинке"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Без названия)"</string> - <string name="pip_close" msgid="2955969519031223530">"Закрыть"</string> + <string name="pip_close" msgid="9135220303720555525">"\"Кадр в кадре\" – выйти"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Во весь экран"</string> - <string name="pip_move" msgid="158770205886688553">"Переместить"</string> - <string name="pip_expand" msgid="1051966011679297308">"Развернуть"</string> - <string name="pip_collapse" msgid="3903295106641385962">"Свернуть"</string> + <string name="pip_move" msgid="1544227837964635439">"Переместить PIP"</string> + <string name="pip_expand" msgid="7605396312689038178">"Развернуть PIP"</string> + <string name="pip_collapse" msgid="5732233773786896094">"Свернуть PIP"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Элементы управления: дважды нажмите "<annotation icon="home_icon">" кнопку главного экрана "</annotation></string> - <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Меню \"Картинка в картинке\"."</string> - <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Переместить влево"</string> - <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Переместить вправо"</string> - <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Переместить вверх"</string> - <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Переместить вниз"</string> - <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Готово"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-si/strings_tv.xml b/libs/WindowManager/Shell/res/values-si/strings_tv.xml index 5478ce5d3d40..fbb0ebba0623 100644 --- a/libs/WindowManager/Shell/res/values-si/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-si/strings_tv.xml @@ -19,16 +19,10 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"පින්තූරය-තුළ-පින්තූරය"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(මාතෘකාවක් නැති වැඩසටහන)"</string> - <string name="pip_close" msgid="2955969519031223530">"වසන්න"</string> + <string name="pip_close" msgid="9135220303720555525">"PIP වසන්න"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"සම්පූර්ණ තිරය"</string> - <string name="pip_move" msgid="158770205886688553">"ගෙන යන්න"</string> - <string name="pip_expand" msgid="1051966011679297308">"දිග හරින්න"</string> - <string name="pip_collapse" msgid="3903295106641385962">"හකුළන්න"</string> + <string name="pip_move" msgid="1544227837964635439">"PIP ගෙන යන්න"</string> + <string name="pip_expand" msgid="7605396312689038178">"PIP දිග හරින්න"</string> + <string name="pip_collapse" msgid="5732233773786896094">"PIP හකුළන්න"</string> <string name="pip_edu_text" msgid="3672999496647508701">" පාලන සඳහා "<annotation icon="home_icon">" මුල් පිටුව "</annotation>" දෙවරක් ඔබන්න"</string> - <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"පින්තූරය තුළ පින්තූරය මෙනුව"</string> - <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"වමට ගෙන යන්න"</string> - <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"දකුණට ගෙන යන්න"</string> - <string name="a11y_action_pip_move_up" msgid="98502616918621959">"ඉහළට ගෙන යන්න"</string> - <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"පහළට ගෙන යන්න"</string> - <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"නිමයි"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-sk/strings_tv.xml b/libs/WindowManager/Shell/res/values-sk/strings_tv.xml index 1df43afca2da..81cb0eafc759 100644 --- a/libs/WindowManager/Shell/res/values-sk/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-sk/strings_tv.xml @@ -19,16 +19,10 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Obraz v obraze"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Program bez názvu)"</string> - <string name="pip_close" msgid="2955969519031223530">"Zavrieť"</string> + <string name="pip_close" msgid="9135220303720555525">"Zavrieť režim PIP"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Celá obrazovka"</string> - <string name="pip_move" msgid="158770205886688553">"Presunúť"</string> - <string name="pip_expand" msgid="1051966011679297308">"Rozbaliť"</string> - <string name="pip_collapse" msgid="3903295106641385962">"Zbaliť"</string> + <string name="pip_move" msgid="1544227837964635439">"Presunúť obraz v obraze"</string> + <string name="pip_expand" msgid="7605396312689038178">"Rozbaliť obraz v obraze"</string> + <string name="pip_collapse" msgid="5732233773786896094">"Zbaliť obraz v obraze"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Ovládanie zobraz. dvoj. stlač. "<annotation icon="home_icon">" TLAČIDLA PLOCHY "</annotation></string> - <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Ponuka obrazu v obraze."</string> - <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Posunúť doľava"</string> - <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Posunúť doprava"</string> - <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Posunúť nahor"</string> - <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Posunúť nadol"</string> - <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Hotovo"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-sl/strings_tv.xml b/libs/WindowManager/Shell/res/values-sl/strings_tv.xml index 88fc8325aa01..060aaa0ce647 100644 --- a/libs/WindowManager/Shell/res/values-sl/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-sl/strings_tv.xml @@ -19,16 +19,10 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Slika v sliki"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Program brez naslova)"</string> - <string name="pip_close" msgid="2955969519031223530">"Zapri"</string> + <string name="pip_close" msgid="9135220303720555525">"Zapri način PIP"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Celozaslonsko"</string> - <string name="pip_move" msgid="158770205886688553">"Premakni"</string> - <string name="pip_expand" msgid="1051966011679297308">"Razširi"</string> - <string name="pip_collapse" msgid="3903295106641385962">"Strni"</string> + <string name="pip_move" msgid="1544227837964635439">"Premakni sliko v sliki"</string> + <string name="pip_expand" msgid="7605396312689038178">"Razširi sliko v sliki"</string> + <string name="pip_collapse" msgid="5732233773786896094">"Strni sliko v sliki"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Za kontrolnike dvakrat pritisnite gumb za "<annotation icon="home_icon">" ZAČETNI ZASLON "</annotation></string> - <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Meni za sliko v sliki"</string> - <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Premakni levo"</string> - <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Premakni desno"</string> - <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Premakni navzgor"</string> - <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Premakni navzdol"</string> - <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Končano"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-sq/strings_tv.xml b/libs/WindowManager/Shell/res/values-sq/strings_tv.xml index 58687e5867fe..9bfdb6a3edd8 100644 --- a/libs/WindowManager/Shell/res/values-sq/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-sq/strings_tv.xml @@ -19,16 +19,10 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Figurë brenda figurës"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Program pa titull)"</string> - <string name="pip_close" msgid="2955969519031223530">"Mbyll"</string> + <string name="pip_close" msgid="9135220303720555525">"Mbyll PIP"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Ekrani i plotë"</string> - <string name="pip_move" msgid="158770205886688553">"Lëviz"</string> - <string name="pip_expand" msgid="1051966011679297308">"Zgjero"</string> - <string name="pip_collapse" msgid="3903295106641385962">"Palos"</string> + <string name="pip_move" msgid="1544227837964635439">"Zhvendos PIP"</string> + <string name="pip_expand" msgid="7605396312689038178">"Zgjero PIP"</string> + <string name="pip_collapse" msgid="5732233773786896094">"Palos PIP"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Trokit dy herë "<annotation icon="home_icon">" KREU "</annotation>" për kontrollet"</string> - <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Menyja e \"Figurës brenda figurës\"."</string> - <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Lëviz majtas"</string> - <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Lëviz djathtas"</string> - <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Lëviz lart"</string> - <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Lëviz poshtë"</string> - <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"U krye"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-sr/strings_tv.xml b/libs/WindowManager/Shell/res/values-sr/strings_tv.xml index e850979174a3..6bc5c87bab48 100644 --- a/libs/WindowManager/Shell/res/values-sr/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-sr/strings_tv.xml @@ -19,16 +19,10 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Слика у слици"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Програм без наслова)"</string> - <string name="pip_close" msgid="2955969519031223530">"Затвори"</string> + <string name="pip_close" msgid="9135220303720555525">"Затвори PIP"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Цео екран"</string> - <string name="pip_move" msgid="158770205886688553">"Премести"</string> - <string name="pip_expand" msgid="1051966011679297308">"Прошири"</string> - <string name="pip_collapse" msgid="3903295106641385962">"Скупи"</string> + <string name="pip_move" msgid="1544227837964635439">"Премести слику у слици"</string> + <string name="pip_expand" msgid="7605396312689038178">"Прошири слику у слици"</string> + <string name="pip_collapse" msgid="5732233773786896094">"Скупи слику у слици"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Двапут притисните "<annotation icon="home_icon">" HOME "</annotation>" за контроле"</string> - <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Мени Слика у слици."</string> - <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Померите налево"</string> - <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Померите надесно"</string> - <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Померите нагоре"</string> - <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Померите надоле"</string> - <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Готово"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-sv/strings_tv.xml b/libs/WindowManager/Shell/res/values-sv/strings_tv.xml index d3a9c3de66db..b3465ab1db85 100644 --- a/libs/WindowManager/Shell/res/values-sv/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-sv/strings_tv.xml @@ -19,16 +19,10 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Bild-i-bild"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Namnlöst program)"</string> - <string name="pip_close" msgid="2955969519031223530">"Stäng"</string> + <string name="pip_close" msgid="9135220303720555525">"Stäng PIP"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Helskärm"</string> - <string name="pip_move" msgid="158770205886688553">"Flytta"</string> - <string name="pip_expand" msgid="1051966011679297308">"Utöka"</string> - <string name="pip_collapse" msgid="3903295106641385962">"Komprimera"</string> + <string name="pip_move" msgid="1544227837964635439">"Flytta BIB"</string> + <string name="pip_expand" msgid="7605396312689038178">"Utöka bild-i-bild"</string> + <string name="pip_collapse" msgid="5732233773786896094">"Komprimera bild-i-bild"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Tryck snabbt två gånger på "<annotation icon="home_icon">" HEM "</annotation>" för kontroller"</string> - <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Bild-i-bild-meny."</string> - <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Flytta åt vänster"</string> - <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Flytta åt höger"</string> - <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Flytta uppåt"</string> - <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Flytta nedåt"</string> - <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Klar"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-sw/strings_tv.xml b/libs/WindowManager/Shell/res/values-sw/strings_tv.xml index 7b9a310ff0b6..baff49ed821a 100644 --- a/libs/WindowManager/Shell/res/values-sw/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-sw/strings_tv.xml @@ -19,16 +19,10 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Pachika Picha Ndani ya Picha Nyingine"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Programu isiyo na jina)"</string> - <string name="pip_close" msgid="2955969519031223530">"Funga"</string> + <string name="pip_close" msgid="9135220303720555525">"Funga PIP"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Skrini nzima"</string> - <string name="pip_move" msgid="158770205886688553">"Hamisha"</string> - <string name="pip_expand" msgid="1051966011679297308">"Panua"</string> - <string name="pip_collapse" msgid="3903295106641385962">"Kunja"</string> + <string name="pip_move" msgid="1544227837964635439">"Kuhamisha PIP"</string> + <string name="pip_expand" msgid="7605396312689038178">"Panua PIP"</string> + <string name="pip_collapse" msgid="5732233773786896094">"Kunja PIP"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Bonyeza mara mbili kitufe cha "<annotation icon="home_icon">" UKURASA WA KWANZA "</annotation>" kupata vidhibiti"</string> - <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Menyu ya kipengele cha kupachika picha ndani ya picha nyingine."</string> - <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Sogeza kushoto"</string> - <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Sogeza kulia"</string> - <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Sogeza juu"</string> - <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Sogeza chini"</string> - <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Imemaliza"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ta/strings_tv.xml b/libs/WindowManager/Shell/res/values-ta/strings_tv.xml index e201401e2e35..4439e299c919 100644 --- a/libs/WindowManager/Shell/res/values-ta/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-ta/strings_tv.xml @@ -19,16 +19,10 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"பிக்ச்சர்-இன்-பிக்ச்சர்"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(தலைப்பு இல்லை)"</string> - <string name="pip_close" msgid="2955969519031223530">"மூடுக"</string> + <string name="pip_close" msgid="9135220303720555525">"PIPஐ மூடு"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"முழுத்திரை"</string> - <string name="pip_move" msgid="158770205886688553">"நகர்த்து"</string> - <string name="pip_expand" msgid="1051966011679297308">"விரி"</string> - <string name="pip_collapse" msgid="3903295106641385962">"சுருக்கு"</string> + <string name="pip_move" msgid="1544227837964635439">"PIPபை நகர்த்து"</string> + <string name="pip_expand" msgid="7605396312689038178">"PIPபை விரிவாக்கு"</string> + <string name="pip_collapse" msgid="5732233773786896094">"PIPபைச் சுருக்கு"</string> <string name="pip_edu_text" msgid="3672999496647508701">" கட்டுப்பாடுகள்: "<annotation icon="home_icon">" முகப்பு "</annotation>" பட்டனை இருமுறை அழுத்துக"</string> - <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"பிக்ச்சர்-இன்-பிக்ச்சர் மெனு."</string> - <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"இடப்புறம் நகர்த்து"</string> - <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"வலப்புறம் நகர்த்து"</string> - <string name="a11y_action_pip_move_up" msgid="98502616918621959">"மேலே நகர்த்து"</string> - <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"கீழே நகர்த்து"</string> - <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"முடிந்தது"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-te/strings_tv.xml b/libs/WindowManager/Shell/res/values-te/strings_tv.xml index 6284d90cb11f..35579346615f 100644 --- a/libs/WindowManager/Shell/res/values-te/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-te/strings_tv.xml @@ -19,16 +19,10 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"పిక్చర్-ఇన్-పిక్చర్"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(శీర్షిక లేని ప్రోగ్రామ్)"</string> - <string name="pip_close" msgid="2955969519031223530">"మూసివేయండి"</string> + <string name="pip_close" msgid="9135220303720555525">"PIPని మూసివేయి"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"ఫుల్-స్క్రీన్"</string> - <string name="pip_move" msgid="158770205886688553">"తరలించండి"</string> - <string name="pip_expand" msgid="1051966011679297308">"విస్తరించండి"</string> - <string name="pip_collapse" msgid="3903295106641385962">"కుదించండి"</string> + <string name="pip_move" msgid="1544227837964635439">"PIPను తరలించండి"</string> + <string name="pip_expand" msgid="7605396312689038178">"PIPని విస్తరించండి"</string> + <string name="pip_collapse" msgid="5732233773786896094">"PIPని కుదించండి"</string> <string name="pip_edu_text" msgid="3672999496647508701">" కంట్రోల్స్ కోసం "<annotation icon="home_icon">" HOME "</annotation>" బటన్ రెండుసార్లు నొక్కండి"</string> - <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"పిక్చర్-ఇన్-పిక్చర్ మెనూ."</string> - <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"ఎడమ వైపుగా జరపండి"</string> - <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"కుడి వైపుగా జరపండి"</string> - <string name="a11y_action_pip_move_up" msgid="98502616918621959">"పైకి జరపండి"</string> - <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"కిందికి జరపండి"</string> - <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"పూర్తయింది"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-television/config.xml b/libs/WindowManager/Shell/res/values-television/config.xml index 86ca65526336..cc0333efd82b 100644 --- a/libs/WindowManager/Shell/res/values-television/config.xml +++ b/libs/WindowManager/Shell/res/values-television/config.xml @@ -43,4 +43,13 @@ <!-- Time (duration in milliseconds) that the shell waits for an app to close the PiP by itself if a custom action is present before closing it. --> <integer name="config_pipForceCloseDelay">5000</integer> + + <!-- Animation duration when exit starting window: fade out icon --> + <integer name="starting_window_app_reveal_icon_fade_out_duration">0</integer> + + <!-- Animation duration when exit starting window: reveal app --> + <integer name="starting_window_app_reveal_anim_delay">0</integer> + + <!-- Animation duration when exit starting window: reveal app --> + <integer name="starting_window_app_reveal_anim_duration">0</integer> </resources> diff --git a/libs/WindowManager/Shell/res/values-th/strings_tv.xml b/libs/WindowManager/Shell/res/values-th/strings_tv.xml index 27cf56c6e154..0a07d157ec6f 100644 --- a/libs/WindowManager/Shell/res/values-th/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-th/strings_tv.xml @@ -19,16 +19,10 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"การแสดงภาพซ้อนภาพ"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(ไม่มีชื่อรายการ)"</string> - <string name="pip_close" msgid="2955969519031223530">"ปิด"</string> + <string name="pip_close" msgid="9135220303720555525">"ปิด PIP"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"เต็มหน้าจอ"</string> - <string name="pip_move" msgid="158770205886688553">"ย้าย"</string> - <string name="pip_expand" msgid="1051966011679297308">"ขยาย"</string> - <string name="pip_collapse" msgid="3903295106641385962">"ยุบ"</string> + <string name="pip_move" msgid="1544227837964635439">"ย้าย PIP"</string> + <string name="pip_expand" msgid="7605396312689038178">"ขยาย PIP"</string> + <string name="pip_collapse" msgid="5732233773786896094">"ยุบ PIP"</string> <string name="pip_edu_text" msgid="3672999496647508701">" กดปุ่ม "<annotation icon="home_icon">" หน้าแรก "</annotation>" สองครั้งเพื่อเปิดการควบคุม"</string> - <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"เมนูการแสดงภาพซ้อนภาพ"</string> - <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"ย้ายไปทางซ้าย"</string> - <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"ย้ายไปทางขวา"</string> - <string name="a11y_action_pip_move_up" msgid="98502616918621959">"ย้ายขึ้น"</string> - <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"ย้ายลง"</string> - <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"เสร็จ"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-tl/strings_tv.xml b/libs/WindowManager/Shell/res/values-tl/strings_tv.xml index 4cc050bebe5b..9a11a38fa492 100644 --- a/libs/WindowManager/Shell/res/values-tl/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-tl/strings_tv.xml @@ -19,16 +19,10 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Picture-in-Picture"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Walang pamagat na programa)"</string> - <string name="pip_close" msgid="2955969519031223530">"Isara"</string> + <string name="pip_close" msgid="9135220303720555525">"Isara ang PIP"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Full screen"</string> - <string name="pip_move" msgid="158770205886688553">"Ilipat"</string> - <string name="pip_expand" msgid="1051966011679297308">"I-expand"</string> - <string name="pip_collapse" msgid="3903295106641385962">"I-collapse"</string> + <string name="pip_move" msgid="1544227837964635439">"Ilipat ang PIP"</string> + <string name="pip_expand" msgid="7605396312689038178">"I-expand ang PIP"</string> + <string name="pip_collapse" msgid="5732233773786896094">"I-collapse ang PIP"</string> <string name="pip_edu_text" msgid="3672999496647508701">" I-double press ang "<annotation icon="home_icon">" HOME "</annotation>" para sa mga kontrol"</string> - <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Menu ng Picture-in-Picture."</string> - <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Ilipat pakaliwa"</string> - <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Ilipat pakanan"</string> - <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Itaas"</string> - <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Ibaba"</string> - <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Tapos na"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-tr/strings_tv.xml b/libs/WindowManager/Shell/res/values-tr/strings_tv.xml index 69bb608061e4..bf4bc6f1fff7 100644 --- a/libs/WindowManager/Shell/res/values-tr/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-tr/strings_tv.xml @@ -19,16 +19,10 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Pencere İçinde Pencere"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Başlıksız program)"</string> - <string name="pip_close" msgid="2955969519031223530">"Kapat"</string> + <string name="pip_close" msgid="9135220303720555525">"PIP\'yi kapat"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Tam ekran"</string> - <string name="pip_move" msgid="158770205886688553">"Taşı"</string> - <string name="pip_expand" msgid="1051966011679297308">"Genişlet"</string> - <string name="pip_collapse" msgid="3903295106641385962">"Daralt"</string> - <string name="pip_edu_text" msgid="3672999496647508701">" Kontroller için "<annotation icon="home_icon">" ANA SAYFA "</annotation>" düğmesine iki kez basın"</string> - <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Pencere içinde pencere menüsü."</string> - <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Sola taşı"</string> - <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Sağa taşı"</string> - <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Yukarı taşı"</string> - <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Aşağı taşı"</string> - <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Bitti"</string> + <string name="pip_move" msgid="1544227837964635439">"PIP\'yi taşı"</string> + <string name="pip_expand" msgid="7605396312689038178">"PIP penceresini genişlet"</string> + <string name="pip_collapse" msgid="5732233773786896094">"PIP penceresini daralt"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Kontroller için "<annotation icon="home_icon">" ANA SAYFA "</annotation>"\'ya iki kez basın"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-uk/strings_tv.xml b/libs/WindowManager/Shell/res/values-uk/strings_tv.xml index 81a8285c58cf..7e9f54e68f54 100644 --- a/libs/WindowManager/Shell/res/values-uk/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-uk/strings_tv.xml @@ -19,16 +19,10 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Картинка в картинці"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Програма без назви)"</string> - <string name="pip_close" msgid="2955969519031223530">"Закрити"</string> + <string name="pip_close" msgid="9135220303720555525">"Закрити PIP"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"На весь екран"</string> - <string name="pip_move" msgid="158770205886688553">"Перемістити"</string> - <string name="pip_expand" msgid="1051966011679297308">"Розгорнути"</string> - <string name="pip_collapse" msgid="3903295106641385962">"Згорнути"</string> + <string name="pip_move" msgid="1544227837964635439">"Перемістити картинку в картинці"</string> + <string name="pip_expand" msgid="7605396312689038178">"Розгорнути картинку в картинці"</string> + <string name="pip_collapse" msgid="5732233773786896094">"Згорнути картинку в картинці"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Відкрити елементи керування: двічі натисніть "<annotation icon="home_icon">"HOME"</annotation></string> - <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Меню \"картинка в картинці\""</string> - <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Перемістити ліворуч"</string> - <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Перемістити праворуч"</string> - <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Перемістити вгору"</string> - <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Перемістити вниз"</string> - <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Готово"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ur/strings_tv.xml b/libs/WindowManager/Shell/res/values-ur/strings_tv.xml index e83885772f2d..c2ef69ff1488 100644 --- a/libs/WindowManager/Shell/res/values-ur/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-ur/strings_tv.xml @@ -19,16 +19,10 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"تصویر میں تصویر"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(بلا عنوان پروگرام)"</string> - <string name="pip_close" msgid="2955969519031223530">"بند کریں"</string> + <string name="pip_close" msgid="9135220303720555525">"PIP بند کریں"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"فُل اسکرین"</string> - <string name="pip_move" msgid="158770205886688553">"منتقل کریں"</string> - <string name="pip_expand" msgid="1051966011679297308">"پھیلائیں"</string> - <string name="pip_collapse" msgid="3903295106641385962">"سکیڑیں"</string> + <string name="pip_move" msgid="1544227837964635439">"PIP کو منتقل کریں"</string> + <string name="pip_expand" msgid="7605396312689038178">"PIP کو پھیلائیں"</string> + <string name="pip_collapse" msgid="5732233773786896094">"PIP کو سکیڑیں"</string> <string name="pip_edu_text" msgid="3672999496647508701">" کنٹرولز کے لیے "<annotation icon="home_icon">"ہوم "</annotation>" بٹن کو دو بار دبائیں"</string> - <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"تصویر میں تصویر کا مینو۔"</string> - <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"دائیں منتقل کریں"</string> - <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"بائیں منتقل کریں"</string> - <string name="a11y_action_pip_move_up" msgid="98502616918621959">"اوپر منتقل کریں"</string> - <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"نیچے منتقل کریں"</string> - <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"ہو گیا"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-uz/strings_tv.xml b/libs/WindowManager/Shell/res/values-uz/strings_tv.xml index da953356628c..9ab95c80aa25 100644 --- a/libs/WindowManager/Shell/res/values-uz/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-uz/strings_tv.xml @@ -19,16 +19,10 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Tasvir ustida tasvir"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Nomsiz)"</string> - <string name="pip_close" msgid="2955969519031223530">"Yopish"</string> + <string name="pip_close" msgid="9135220303720555525">"Kadr ichida kadr – chiqish"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Butun ekran"</string> - <string name="pip_move" msgid="158770205886688553">"Boshqa joyga olish"</string> - <string name="pip_expand" msgid="1051966011679297308">"Yoyish"</string> - <string name="pip_collapse" msgid="3903295106641385962">"Yopish"</string> + <string name="pip_move" msgid="1544227837964635439">"PIPni siljitish"</string> + <string name="pip_expand" msgid="7605396312689038178">"PIP funksiyasini yoyish"</string> + <string name="pip_collapse" msgid="5732233773786896094">"PIP funksiyasini yopish"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Boshqaruv uchun "<annotation icon="home_icon">"ASOSIY"</annotation>" tugmani ikki marta bosing"</string> - <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Tasvir ustida tasvir menyusi."</string> - <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Chapga olish"</string> - <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Oʻngga olish"</string> - <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Tepaga olish"</string> - <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Pastga olish"</string> - <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Tayyor"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-vi/strings_tv.xml b/libs/WindowManager/Shell/res/values-vi/strings_tv.xml index 1f9260fdcff0..146376d3cab6 100644 --- a/libs/WindowManager/Shell/res/values-vi/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-vi/strings_tv.xml @@ -19,16 +19,10 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Hình trong hình"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Không có chương trình tiêu đề)"</string> - <string name="pip_close" msgid="2955969519031223530">"Đóng"</string> + <string name="pip_close" msgid="9135220303720555525">"Đóng PIP"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Toàn màn hình"</string> - <string name="pip_move" msgid="158770205886688553">"Di chuyển"</string> - <string name="pip_expand" msgid="1051966011679297308">"Mở rộng"</string> - <string name="pip_collapse" msgid="3903295106641385962">"Thu gọn"</string> + <string name="pip_move" msgid="1544227837964635439">"Di chuyển PIP (Ảnh trong ảnh)"</string> + <string name="pip_expand" msgid="7605396312689038178">"Mở rộng PIP (Ảnh trong ảnh)"</string> + <string name="pip_collapse" msgid="5732233773786896094">"Thu gọn PIP (Ảnh trong ảnh)"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Nhấn đúp vào nút "<annotation icon="home_icon">" MÀN HÌNH CHÍNH "</annotation>" để mở trình đơn điều khiển"</string> - <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Trình đơn hình trong hình."</string> - <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Di chuyển sang trái"</string> - <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Di chuyển sang phải"</string> - <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Di chuyển lên"</string> - <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Di chuyển xuống"</string> - <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Xong"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-zh-rCN/strings_tv.xml b/libs/WindowManager/Shell/res/values-zh-rCN/strings_tv.xml index 399d639fe70f..55407d2c699d 100644 --- a/libs/WindowManager/Shell/res/values-zh-rCN/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-zh-rCN/strings_tv.xml @@ -19,16 +19,10 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"画中画"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(节目没有标题)"</string> - <string name="pip_close" msgid="2955969519031223530">"关闭"</string> + <string name="pip_close" msgid="9135220303720555525">"关闭画中画"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"全屏"</string> - <string name="pip_move" msgid="158770205886688553">"移动"</string> - <string name="pip_expand" msgid="1051966011679297308">"展开"</string> - <string name="pip_collapse" msgid="3903295106641385962">"收起"</string> + <string name="pip_move" msgid="1544227837964635439">"移动画中画窗口"</string> + <string name="pip_expand" msgid="7605396312689038178">"展开 PIP"</string> + <string name="pip_collapse" msgid="5732233773786896094">"收起 PIP"</string> <string name="pip_edu_text" msgid="3672999496647508701">" 按两次"<annotation icon="home_icon">"主屏幕"</annotation>"按钮可查看相关控件"</string> - <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"画中画菜单。"</string> - <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"左移"</string> - <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"右移"</string> - <string name="a11y_action_pip_move_up" msgid="98502616918621959">"上移"</string> - <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"下移"</string> - <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"完成"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-zh-rHK/strings_tv.xml b/libs/WindowManager/Shell/res/values-zh-rHK/strings_tv.xml index acbc26d033cd..15e278d8ecc2 100644 --- a/libs/WindowManager/Shell/res/values-zh-rHK/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-zh-rHK/strings_tv.xml @@ -19,16 +19,10 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"畫中畫"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(沒有標題的節目)"</string> - <string name="pip_close" msgid="2955969519031223530">"關閉"</string> + <string name="pip_close" msgid="9135220303720555525">"關閉 PIP"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"全螢幕"</string> - <string name="pip_move" msgid="158770205886688553">"移動"</string> - <string name="pip_expand" msgid="1051966011679297308">"展開"</string> - <string name="pip_collapse" msgid="3903295106641385962">"收合"</string> + <string name="pip_move" msgid="1544227837964635439">"移動畫中畫"</string> + <string name="pip_expand" msgid="7605396312689038178">"展開畫中畫"</string> + <string name="pip_collapse" msgid="5732233773786896094">"收合畫中畫"</string> <string name="pip_edu_text" msgid="3672999496647508701">" 按兩下"<annotation icon="home_icon">" 主畫面按鈕"</annotation>"即可顯示控制項"</string> - <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"畫中畫選單。"</string> - <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"向左移"</string> - <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"向右移"</string> - <string name="a11y_action_pip_move_up" msgid="98502616918621959">"向上移"</string> - <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"向下移"</string> - <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"完成"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-zh-rTW/strings_tv.xml b/libs/WindowManager/Shell/res/values-zh-rTW/strings_tv.xml index f8c683ec3a60..0b17b31d23d0 100644 --- a/libs/WindowManager/Shell/res/values-zh-rTW/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-zh-rTW/strings_tv.xml @@ -19,16 +19,10 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"子母畫面"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(無標題的節目)"</string> - <string name="pip_close" msgid="2955969519031223530">"關閉"</string> + <string name="pip_close" msgid="9135220303720555525">"關閉子母畫面"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"全螢幕"</string> - <string name="pip_move" msgid="158770205886688553">"移動"</string> - <string name="pip_expand" msgid="1051966011679297308">"展開"</string> - <string name="pip_collapse" msgid="3903295106641385962">"收合"</string> + <string name="pip_move" msgid="1544227837964635439">"移動子母畫面"</string> + <string name="pip_expand" msgid="7605396312689038178">"展開子母畫面"</string> + <string name="pip_collapse" msgid="5732233773786896094">"收合子母畫面"</string> <string name="pip_edu_text" msgid="3672999496647508701">" 按兩下"<annotation icon="home_icon">"主畫面按鈕"</annotation>"即可顯示控制選項"</string> - <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"子母畫面選單。"</string> - <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"向左移動"</string> - <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"向右移動"</string> - <string name="a11y_action_pip_move_up" msgid="98502616918621959">"向上移動"</string> - <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"向下移動"</string> - <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"完成"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-zu/strings_tv.xml b/libs/WindowManager/Shell/res/values-zu/strings_tv.xml index 20243a9dfc9c..dad8c8128222 100644 --- a/libs/WindowManager/Shell/res/values-zu/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-zu/strings_tv.xml @@ -19,16 +19,10 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Isithombe-esithombeni"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Alukho uhlelo lwesihloko)"</string> - <string name="pip_close" msgid="2955969519031223530">"Vala"</string> + <string name="pip_close" msgid="9135220303720555525">"Vala i-PIP"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Iskrini esigcwele"</string> - <string name="pip_move" msgid="158770205886688553">"Hambisa"</string> - <string name="pip_expand" msgid="1051966011679297308">"Nweba"</string> - <string name="pip_collapse" msgid="3903295106641385962">"Goqa"</string> + <string name="pip_move" msgid="1544227837964635439">"Hambisa i-PIP"</string> + <string name="pip_expand" msgid="7605396312689038178">"Nweba i-PIP"</string> + <string name="pip_collapse" msgid="5732233773786896094">"Goqa i-PIP"</string> <string name="pip_edu_text" msgid="3672999496647508701">" Chofoza kabili "<annotation icon="home_icon">" IKHAYA"</annotation>" mayelana nezilawuli"</string> - <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Imenyu yesithombe-esithombeni"</string> - <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Yisa kwesokunxele"</string> - <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Yisa kwesokudla"</string> - <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Khuphula"</string> - <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Yehlisa"</string> - <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Kwenziwe"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values/attrs.xml b/libs/WindowManager/Shell/res/values/attrs.xml index 4aaeef8afcb0..2aad4c1c1805 100644 --- a/libs/WindowManager/Shell/res/values/attrs.xml +++ b/libs/WindowManager/Shell/res/values/attrs.xml @@ -19,4 +19,8 @@ <attr name="icon" format="reference" /> <attr name="text" format="string" /> </declare-styleable> + + <declare-styleable name="MessageState"> + <attr name="state_task_focused" format="boolean"/> + </declare-styleable> </resources> diff --git a/libs/WindowManager/Shell/res/values/config.xml b/libs/WindowManager/Shell/res/values/config.xml index f03b7f66cdc8..30c3d50ed8ad 100644 --- a/libs/WindowManager/Shell/res/values/config.xml +++ b/libs/WindowManager/Shell/res/values/config.xml @@ -19,6 +19,10 @@ by the resources of the app using the Shell library. --> <bool name="config_enableShellMainThread">false</bool> + <!-- Determines whether to register the shell task organizer on init. + TODO(b/238217847): This config is temporary until we refactor the base WMComponent. --> + <bool name="config_registerShellTaskOrganizerOnInit">true</bool> + <!-- Animation duration for PIP when entering. --> <integer name="config_pipEnterAnimationDuration">425</integer> diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml index 1dac9caba01e..0bc70857a113 100644 --- a/libs/WindowManager/Shell/res/values/dimen.xml +++ b/libs/WindowManager/Shell/res/values/dimen.xml @@ -81,6 +81,9 @@ <!-- The width and height of the background for custom action in PiP menu. --> <dimen name="pip_custom_close_bg_size">32dp</dimen> + <!-- Extra padding between picture-in-picture windows and any registered keep clear areas. --> + <dimen name="pip_keep_clear_areas_padding">16dp</dimen> + <dimen name="dismiss_target_x_size">24dp</dimen> <dimen name="floating_dismiss_bottom_margin">50dp</dimen> @@ -246,8 +249,17 @@ <!-- The corner radius of the letterbox education dialog. --> <dimen name="letterbox_education_dialog_corner_radius">28dp</dimen> - <!-- The size of an icon in the letterbox education dialog. --> - <dimen name="letterbox_education_dialog_icon_size">48dp</dimen> + <!-- The width of the top icon in the letterbox education dialog. --> + <dimen name="letterbox_education_dialog_title_icon_width">45dp</dimen> + + <!-- The height of the top icon in the letterbox education dialog. --> + <dimen name="letterbox_education_dialog_title_icon_height">44dp</dimen> + + <!-- The width of an icon in the letterbox education dialog. --> + <dimen name="letterbox_education_dialog_icon_width">40dp</dimen> + + <!-- The height of an icon in the letterbox education dialog. --> + <dimen name="letterbox_education_dialog_icon_height">32dp</dimen> <!-- The fixed width of the dialog if there is enough space in the parent. --> <dimen name="letterbox_education_dialog_width">472dp</dimen> @@ -285,4 +297,28 @@ when the pinned stack size is overridden by app via minWidth/minHeight. --> <dimen name="overridable_minimal_size_pip_resizable_task">48dp</dimen> + + <!-- The size of the drag handle / menu shown along with a floating task. --> + <dimen name="floating_task_menu_size">32dp</dimen> + + <!-- The size of menu items in the floating task menu. --> + <dimen name="floating_task_menu_item_size">24dp</dimen> + + <!-- The horizontal margin of menu items in the floating task menu. --> + <dimen name="floating_task_menu_item_padding">5dp</dimen> + + <!-- The width of visible floating view region when stashed. --> + <dimen name="floating_task_stash_offset">32dp</dimen> + + <!-- The amount of elevation for a floating task. --> + <dimen name="floating_task_elevation">8dp</dimen> + + <!-- The amount of padding around the bottom and top of the task. --> + <dimen name="floating_task_vertical_padding">8dp</dimen> + + <!-- The normal size of the dismiss target. --> + <dimen name="floating_task_dismiss_circle_size">150dp</dimen> + + <!-- The smaller size of the dismiss target (shrinks when something is in the target). --> + <dimen name="floating_dismiss_circle_small">120dp</dimen> </resources> diff --git a/libs/WindowManager/Shell/res/values/strings.xml b/libs/WindowManager/Shell/res/values/strings.xml index a24311fb1f21..1d1162daf249 100644 --- a/libs/WindowManager/Shell/res/values/strings.xml +++ b/libs/WindowManager/Shell/res/values/strings.xml @@ -157,7 +157,7 @@ <string name="accessibility_bubble_dismissed">Bubble dismissed.</string> <!-- Description of the restart button in the hint of size compatibility mode. [CHAR LIMIT=NONE] --> - <string name="restart_button_description">Tap to restart this app and go full screen.</string> + <string name="restart_button_description">Tap to restart this app for a better view.</string> <!-- Description of the camera compat button for applying stretched issues treatment in the hint for compatibility control. [CHAR LIMIT=NONE] --> @@ -172,18 +172,25 @@ <string name="camera_compat_dismiss_button_description">No camera issues? Tap to dismiss.</string> <!-- The title of the letterbox education dialog. [CHAR LIMIT=NONE] --> - <string name="letterbox_education_dialog_title">Some apps work best in portrait</string> + <string name="letterbox_education_dialog_title">See and do more</string> - <!-- The subtext of the letterbox education dialog. [CHAR LIMIT=NONE] --> - <string name="letterbox_education_dialog_subtext">Try one of these options to make the most of your space</string> - - <!-- Description of the rotate screen action. [CHAR LIMIT=NONE] --> - <string name="letterbox_education_screen_rotation_text">Rotate your device to go full screen</string> + <!-- Description of the split screen action. [CHAR LIMIT=NONE] --> + <string name="letterbox_education_split_screen_text">Drag in another app for split-screen</string> <!-- Description of the reposition app action. [CHAR LIMIT=NONE] --> - <string name="letterbox_education_reposition_text">Double-tap next to an app to reposition it</string> + <string name="letterbox_education_reposition_text">Double-tap outside an app to reposition it</string> <!-- Button text for dismissing the letterbox education dialog. [CHAR LIMIT=20] --> <string name="letterbox_education_got_it">Got it</string> + <!-- Accessibility description of the letterbox education toast expand to dialog button. [CHAR LIMIT=NONE] --> + <string name="letterbox_education_expand_button_description">Expand for more information.</string> + + <!-- Freeform window caption strings --> + <!-- Accessibility text for the maximize window button [CHAR LIMIT=NONE] --> + <string name="maximize_button_text">Maximize</string> + <!-- Accessibility text for the minimize window button [CHAR LIMIT=NONE] --> + <string name="minimize_button_text">Minimize</string> + <!-- Accessibility text for the close window button [CHAR LIMIT=NONE] --> + <string name="close_button_text">Close</string> </resources> diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/ProtoLogController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/ProtoLogController.java new file mode 100644 index 000000000000..d2760022a015 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/ProtoLogController.java @@ -0,0 +1,112 @@ +/* + * 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; + +import com.android.wm.shell.protolog.ShellProtoLogImpl; +import com.android.wm.shell.sysui.ShellCommandHandler; +import com.android.wm.shell.sysui.ShellInit; + +import java.io.PrintWriter; +import java.util.Arrays; + +/** + * Controls the {@link ShellProtoLogImpl} in WMShell via adb shell commands. + * + * Use with {@code adb shell dumpsys activity service SystemUIService WMShell protolog ...}. + */ +public class ProtoLogController implements ShellCommandHandler.ShellCommandActionHandler { + private final ShellCommandHandler mShellCommandHandler; + private final ShellProtoLogImpl mShellProtoLog; + + public ProtoLogController(ShellInit shellInit, + ShellCommandHandler shellCommandHandler) { + shellInit.addInitCallback(this::onInit, this); + mShellCommandHandler = shellCommandHandler; + mShellProtoLog = ShellProtoLogImpl.getSingleInstance(); + } + + void onInit() { + mShellCommandHandler.addCommandCallback("protolog", this, this); + } + + @Override + public boolean onShellCommand(String[] args, PrintWriter pw) { + switch (args[0]) { + case "status": { + pw.println(mShellProtoLog.getStatus()); + return true; + } + case "start": { + mShellProtoLog.startProtoLog(pw); + return true; + } + case "stop": { + mShellProtoLog.stopProtoLog(pw, true /* writeToFile */); + return true; + } + case "enable-text": { + String[] groups = Arrays.copyOfRange(args, 1, args.length); + int result = mShellProtoLog.startTextLogging(groups, pw); + if (result == 0) { + pw.println("Starting logging on groups: " + Arrays.toString(groups)); + return true; + } + return false; + } + case "disable-text": { + String[] groups = Arrays.copyOfRange(args, 1, args.length); + int result = mShellProtoLog.stopTextLogging(groups, pw); + if (result == 0) { + pw.println("Stopping logging on groups: " + Arrays.toString(groups)); + return true; + } + return false; + } + case "enable": { + String[] groups = Arrays.copyOfRange(args, 1, args.length); + return mShellProtoLog.startTextLogging(groups, pw) == 0; + } + case "disable": { + String[] groups = Arrays.copyOfRange(args, 1, args.length); + return mShellProtoLog.stopTextLogging(groups, pw) == 0; + } + default: { + pw.println("Invalid command: " + args[0]); + printShellCommandHelp(pw, ""); + return false; + } + } + } + + @Override + public void printShellCommandHelp(PrintWriter pw, String prefix) { + pw.println(prefix + "status"); + pw.println(prefix + " Get current ProtoLog status."); + pw.println(prefix + "start"); + pw.println(prefix + " Start proto logging."); + pw.println(prefix + "stop"); + pw.println(prefix + " Stop proto logging and flush to file."); + pw.println(prefix + "enable [group...]"); + pw.println(prefix + " Enable proto logging for given groups."); + pw.println(prefix + "disable [group...]"); + pw.println(prefix + " Disable proto logging for given groups."); + pw.println(prefix + "enable-text [group...]"); + pw.println(prefix + " Enable logcat logging for given groups."); + pw.println(prefix + "disable-text [group...]"); + pw.println(prefix + " Disable logcat logging for given groups."); + } +} 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 14ba9df93f24..b085b73d78ce 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/RootDisplayAreaOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/RootDisplayAreaOrganizer.java @@ -16,14 +16,20 @@ package com.android.wm.shell; +import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE; + +import android.app.WindowConfiguration; import android.util.SparseArray; import android.view.SurfaceControl; import android.window.DisplayAreaAppearedInfo; import android.window.DisplayAreaInfo; import android.window.DisplayAreaOrganizer; +import android.window.WindowContainerTransaction; import androidx.annotation.NonNull; +import com.android.internal.protolog.common.ProtoLog; + import java.io.PrintWriter; import java.util.List; import java.util.concurrent.Executor; @@ -85,6 +91,8 @@ public class RootDisplayAreaOrganizer extends DisplayAreaOrganizer { } mDisplayAreasInfo.remove(displayId); + mLeashes.get(displayId).release(); + mLeashes.remove(displayId); } @Override @@ -100,10 +108,44 @@ public class RootDisplayAreaOrganizer extends DisplayAreaOrganizer { mDisplayAreasInfo.put(displayId, displayAreaInfo); } + /** + * Create a {@link WindowContainerTransaction} to update display windowing mode. + * + * @param displayId display id to update windowing mode for + * @param windowingMode target {@link WindowConfiguration.WindowingMode} + * @return {@link WindowContainerTransaction} with pending operation to set windowing mode + */ + public WindowContainerTransaction prepareWindowingModeChange(int displayId, + @WindowConfiguration.WindowingMode int windowingMode) { + WindowContainerTransaction wct = new WindowContainerTransaction(); + DisplayAreaInfo displayAreaInfo = mDisplayAreasInfo.get(displayId); + if (displayAreaInfo == null) { + ProtoLog.e(WM_SHELL_DESKTOP_MODE, + "unable to update windowing mode for display %d display not found", displayId); + return wct; + } + + ProtoLog.d(WM_SHELL_DESKTOP_MODE, + "setWindowingMode: displayId=%d current wmMode=%d new wmMode=%d", displayId, + displayAreaInfo.configuration.windowConfiguration.getWindowingMode(), + windowingMode); + + wct.setWindowingMode(displayAreaInfo.token, windowingMode); + return wct; + } + public void dump(@NonNull PrintWriter pw, String prefix) { final String innerPrefix = prefix + " "; final String childPrefix = innerPrefix + " "; pw.println(prefix + this); + + for (int i = 0; i < mDisplayAreasInfo.size(); i++) { + int displayId = mDisplayAreasInfo.keyAt(i); + DisplayAreaInfo displayAreaInfo = mDisplayAreasInfo.get(displayId); + int windowingMode = + displayAreaInfo.configuration.windowConfiguration.getWindowingMode(); + pw.println(innerPrefix + "# displayId=" + displayId + " wmMode=" + windowingMode); + } } @Override 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 9230c22c5d95..ca977ed2cb94 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/RootTaskDisplayAreaOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/RootTaskDisplayAreaOrganizer.java @@ -179,6 +179,14 @@ public class RootTaskDisplayAreaOrganizer extends DisplayAreaOrganizer { } /** + * Returns the {@link DisplayAreaInfo} of the {@link DisplayAreaInfo#displayId}. + */ + @Nullable + public DisplayAreaInfo getDisplayAreaInfo(int displayId) { + return mDisplayAreasInfo.get(displayId); + } + + /** * Applies the {@link DisplayAreaInfo} to the {@link DisplayAreaContext} specified by * {@link DisplayAreaInfo#displayId}. */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellCommandHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellCommandHandler.java deleted file mode 100644 index 73fd6931066d..000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellCommandHandler.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (C) 2019 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.wm.shell; - -import com.android.wm.shell.common.annotations.ExternalThread; - -import java.io.PrintWriter; - -/** - * An entry point into the shell for dumping shell internal state and running adb commands. - * - * Use with {@code adb shell dumpsys activity service SystemUIService WMShell ...}. - */ -@ExternalThread -public interface ShellCommandHandler { - /** - * Dumps the shell state. - */ - void dump(PrintWriter pw); - - /** - * Handles a shell command. - */ - boolean handleCommand(final String[] args, PrintWriter pw); -} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellCommandHandlerImpl.java b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellCommandHandlerImpl.java deleted file mode 100644 index 06f4367752fb..000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellCommandHandlerImpl.java +++ /dev/null @@ -1,230 +0,0 @@ -/* - * Copyright (C) 2019 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.wm.shell; - -import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; - -import com.android.wm.shell.apppairs.AppPairsController; -import com.android.wm.shell.common.ShellExecutor; -import com.android.wm.shell.hidedisplaycutout.HideDisplayCutoutController; -import com.android.wm.shell.kidsmode.KidsModeTaskOrganizer; -import com.android.wm.shell.legacysplitscreen.LegacySplitScreenController; -import com.android.wm.shell.onehanded.OneHandedController; -import com.android.wm.shell.pip.Pip; -import com.android.wm.shell.recents.RecentTasksController; -import com.android.wm.shell.splitscreen.SplitScreenController; - -import java.io.PrintWriter; -import java.util.Optional; - -/** - * An entry point into the shell for dumping shell internal state and running adb commands. - * - * Use with {@code adb shell dumpsys activity service SystemUIService WMShell ...}. - */ -public final class ShellCommandHandlerImpl { - private static final String TAG = ShellCommandHandlerImpl.class.getSimpleName(); - - private final Optional<LegacySplitScreenController> mLegacySplitScreenOptional; - private final Optional<SplitScreenController> mSplitScreenOptional; - private final Optional<Pip> mPipOptional; - private final Optional<OneHandedController> mOneHandedOptional; - private final Optional<HideDisplayCutoutController> mHideDisplayCutout; - private final Optional<AppPairsController> mAppPairsOptional; - private final Optional<RecentTasksController> mRecentTasks; - private final ShellTaskOrganizer mShellTaskOrganizer; - private final KidsModeTaskOrganizer mKidsModeTaskOrganizer; - private final ShellExecutor mMainExecutor; - private final HandlerImpl mImpl = new HandlerImpl(); - - public ShellCommandHandlerImpl( - ShellTaskOrganizer shellTaskOrganizer, - KidsModeTaskOrganizer kidsModeTaskOrganizer, - Optional<LegacySplitScreenController> legacySplitScreenOptional, - Optional<SplitScreenController> splitScreenOptional, - Optional<Pip> pipOptional, - Optional<OneHandedController> oneHandedOptional, - Optional<HideDisplayCutoutController> hideDisplayCutout, - Optional<AppPairsController> appPairsOptional, - Optional<RecentTasksController> recentTasks, - ShellExecutor mainExecutor) { - mShellTaskOrganizer = shellTaskOrganizer; - mKidsModeTaskOrganizer = kidsModeTaskOrganizer; - mRecentTasks = recentTasks; - mLegacySplitScreenOptional = legacySplitScreenOptional; - mSplitScreenOptional = splitScreenOptional; - mPipOptional = pipOptional; - mOneHandedOptional = oneHandedOptional; - mHideDisplayCutout = hideDisplayCutout; - mAppPairsOptional = appPairsOptional; - mMainExecutor = mainExecutor; - } - - public ShellCommandHandler asShellCommandHandler() { - return mImpl; - } - - /** Dumps WM Shell internal state. */ - private void dump(PrintWriter pw) { - mShellTaskOrganizer.dump(pw, ""); - pw.println(); - pw.println(); - mPipOptional.ifPresent(pip -> pip.dump(pw)); - mLegacySplitScreenOptional.ifPresent(splitScreen -> splitScreen.dump(pw)); - mOneHandedOptional.ifPresent(oneHanded -> oneHanded.dump(pw)); - mHideDisplayCutout.ifPresent(hideDisplayCutout -> hideDisplayCutout.dump(pw)); - pw.println(); - pw.println(); - mAppPairsOptional.ifPresent(appPairs -> appPairs.dump(pw, "")); - pw.println(); - pw.println(); - mSplitScreenOptional.ifPresent(splitScreen -> splitScreen.dump(pw, "")); - pw.println(); - pw.println(); - mRecentTasks.ifPresent(recentTasks -> recentTasks.dump(pw, "")); - pw.println(); - pw.println(); - mKidsModeTaskOrganizer.dump(pw, ""); - } - - - /** Returns {@code true} if command was found and executed. */ - private boolean handleCommand(final String[] args, PrintWriter pw) { - if (args.length < 2) { - // Argument at position 0 is "WMShell". - return false; - } - switch (args[1]) { - case "pair": - return runPair(args, pw); - case "unpair": - return runUnpair(args, pw); - case "moveToSideStage": - return runMoveToSideStage(args, pw); - case "removeFromSideStage": - return runRemoveFromSideStage(args, pw); - case "setSideStagePosition": - return runSetSideStagePosition(args, pw); - case "help": - return runHelp(pw); - default: - return false; - } - } - - private boolean runPair(String[] args, PrintWriter pw) { - if (args.length < 4) { - // First two arguments are "WMShell" and command name. - pw.println("Error: two task ids should be provided as arguments"); - return false; - } - final int taskId1 = new Integer(args[2]); - final int taskId2 = new Integer(args[3]); - mAppPairsOptional.ifPresent(appPairs -> appPairs.pair(taskId1, taskId2)); - return true; - } - - private boolean runUnpair(String[] args, PrintWriter pw) { - if (args.length < 3) { - // First two arguments are "WMShell" and command name. - pw.println("Error: task id should be provided as an argument"); - return false; - } - final int taskId = new Integer(args[2]); - mAppPairsOptional.ifPresent(appPairs -> appPairs.unpair(taskId)); - return true; - } - - private boolean runMoveToSideStage(String[] args, PrintWriter pw) { - if (args.length < 3) { - // First arguments are "WMShell" and command name. - pw.println("Error: task id should be provided as arguments"); - return false; - } - final int taskId = new Integer(args[2]); - final int sideStagePosition = args.length > 3 - ? new Integer(args[3]) : SPLIT_POSITION_BOTTOM_OR_RIGHT; - mSplitScreenOptional.ifPresent(split -> split.moveToSideStage(taskId, sideStagePosition)); - return true; - } - - private boolean runRemoveFromSideStage(String[] args, PrintWriter pw) { - if (args.length < 3) { - // First arguments are "WMShell" and command name. - pw.println("Error: task id should be provided as arguments"); - return false; - } - final int taskId = new Integer(args[2]); - mSplitScreenOptional.ifPresent(split -> split.removeFromSideStage(taskId)); - return true; - } - - private boolean runSetSideStagePosition(String[] args, PrintWriter pw) { - if (args.length < 3) { - // First arguments are "WMShell" and command name. - pw.println("Error: side stage position should be provided as arguments"); - return false; - } - final int position = new Integer(args[2]); - mSplitScreenOptional.ifPresent(split -> split.setSideStagePosition(position)); - return true; - } - - private boolean runHelp(PrintWriter pw) { - pw.println("Window Manager Shell commands:"); - pw.println(" help"); - pw.println(" Print this help text."); - pw.println(" <no arguments provided>"); - pw.println(" Dump Window Manager Shell internal state"); - pw.println(" pair <taskId1> <taskId2>"); - pw.println(" unpair <taskId>"); - pw.println(" Pairs/unpairs tasks with given ids."); - pw.println(" moveToSideStage <taskId> <SideStagePosition>"); - pw.println(" Move a task with given id in split-screen mode."); - pw.println(" removeFromSideStage <taskId>"); - pw.println(" Remove a task with given id in split-screen mode."); - pw.println(" setSideStageOutline <true/false>"); - pw.println(" Enable/Disable outline on the side-stage."); - pw.println(" setSideStagePosition <SideStagePosition>"); - pw.println(" Sets the position of the side-stage."); - return true; - } - - private class HandlerImpl implements ShellCommandHandler { - @Override - public void dump(PrintWriter pw) { - try { - mMainExecutor.executeBlocking(() -> ShellCommandHandlerImpl.this.dump(pw)); - } catch (InterruptedException e) { - throw new RuntimeException("Failed to dump the Shell in 2s", e); - } - } - - @Override - public boolean handleCommand(String[] args, PrintWriter pw) { - try { - boolean[] result = new boolean[1]; - mMainExecutor.executeBlocking(() -> { - result[0] = ShellCommandHandlerImpl.this.handleCommand(args, pw); - }); - return result[0]; - } catch (InterruptedException e) { - throw new RuntimeException("Failed to handle Shell command in 2s", e); - } - } - } -} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellInitImpl.java b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellInitImpl.java deleted file mode 100644 index 62fb840d29d1..000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellInitImpl.java +++ /dev/null @@ -1,164 +0,0 @@ -/* - * Copyright (C) 2019 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.wm.shell; - -import static com.android.wm.shell.ShellTaskOrganizer.TASK_LISTENER_TYPE_FULLSCREEN; - -import com.android.wm.shell.apppairs.AppPairsController; -import com.android.wm.shell.bubbles.BubbleController; -import com.android.wm.shell.common.DisplayController; -import com.android.wm.shell.common.DisplayImeController; -import com.android.wm.shell.common.DisplayInsetsController; -import com.android.wm.shell.common.ShellExecutor; -import com.android.wm.shell.common.annotations.ExternalThread; -import com.android.wm.shell.draganddrop.DragAndDropController; -import com.android.wm.shell.freeform.FreeformTaskListener; -import com.android.wm.shell.fullscreen.FullscreenTaskListener; -import com.android.wm.shell.fullscreen.FullscreenUnfoldController; -import com.android.wm.shell.kidsmode.KidsModeTaskOrganizer; -import com.android.wm.shell.pip.phone.PipTouchHandler; -import com.android.wm.shell.recents.RecentTasksController; -import com.android.wm.shell.splitscreen.SplitScreenController; -import com.android.wm.shell.startingsurface.StartingWindowController; -import com.android.wm.shell.transition.Transitions; -import com.android.wm.shell.unfold.UnfoldTransitionHandler; - -import java.util.Optional; - -/** - * The entry point implementation into the shell for initializing shell internal state. - */ -public class ShellInitImpl { - private static final String TAG = ShellInitImpl.class.getSimpleName(); - - private final DisplayController mDisplayController; - private final DisplayImeController mDisplayImeController; - private final DisplayInsetsController mDisplayInsetsController; - private final DragAndDropController mDragAndDropController; - private final ShellTaskOrganizer mShellTaskOrganizer; - private final KidsModeTaskOrganizer mKidsModeTaskOrganizer; - private final Optional<BubbleController> mBubblesOptional; - private final Optional<SplitScreenController> mSplitScreenOptional; - private final Optional<AppPairsController> mAppPairsOptional; - private final Optional<PipTouchHandler> mPipTouchHandlerOptional; - private final FullscreenTaskListener mFullscreenTaskListener; - private final Optional<FullscreenUnfoldController> mFullscreenUnfoldController; - private final Optional<UnfoldTransitionHandler> mUnfoldTransitionHandler; - private final Optional<FreeformTaskListener> mFreeformTaskListenerOptional; - private final ShellExecutor mMainExecutor; - private final Transitions mTransitions; - private final StartingWindowController mStartingWindow; - private final Optional<RecentTasksController> mRecentTasks; - - private final InitImpl mImpl = new InitImpl(); - - public ShellInitImpl( - DisplayController displayController, - DisplayImeController displayImeController, - DisplayInsetsController displayInsetsController, - DragAndDropController dragAndDropController, - ShellTaskOrganizer shellTaskOrganizer, - KidsModeTaskOrganizer kidsModeTaskOrganizer, - Optional<BubbleController> bubblesOptional, - Optional<SplitScreenController> splitScreenOptional, - Optional<AppPairsController> appPairsOptional, - Optional<PipTouchHandler> pipTouchHandlerOptional, - FullscreenTaskListener fullscreenTaskListener, - Optional<FullscreenUnfoldController> fullscreenUnfoldTransitionController, - Optional<UnfoldTransitionHandler> unfoldTransitionHandler, - Optional<FreeformTaskListener> freeformTaskListenerOptional, - Optional<RecentTasksController> recentTasks, - Transitions transitions, - StartingWindowController startingWindow, - ShellExecutor mainExecutor) { - mDisplayController = displayController; - mDisplayImeController = displayImeController; - mDisplayInsetsController = displayInsetsController; - mDragAndDropController = dragAndDropController; - mShellTaskOrganizer = shellTaskOrganizer; - mKidsModeTaskOrganizer = kidsModeTaskOrganizer; - mBubblesOptional = bubblesOptional; - mSplitScreenOptional = splitScreenOptional; - mAppPairsOptional = appPairsOptional; - mFullscreenTaskListener = fullscreenTaskListener; - mPipTouchHandlerOptional = pipTouchHandlerOptional; - mFullscreenUnfoldController = fullscreenUnfoldTransitionController; - mUnfoldTransitionHandler = unfoldTransitionHandler; - mFreeformTaskListenerOptional = freeformTaskListenerOptional; - mRecentTasks = recentTasks; - mTransitions = transitions; - mMainExecutor = mainExecutor; - mStartingWindow = startingWindow; - } - - public ShellInit asShellInit() { - return mImpl; - } - - private void init() { - // Start listening for display and insets changes - mDisplayController.initialize(); - mDisplayInsetsController.initialize(); - mDisplayImeController.startMonitorDisplays(); - - // Setup the shell organizer - mShellTaskOrganizer.addListenerForType( - mFullscreenTaskListener, TASK_LISTENER_TYPE_FULLSCREEN); - mShellTaskOrganizer.initStartingWindow(mStartingWindow); - mShellTaskOrganizer.registerOrganizer(); - - mAppPairsOptional.ifPresent(AppPairsController::onOrganizerRegistered); - mSplitScreenOptional.ifPresent(SplitScreenController::onOrganizerRegistered); - mBubblesOptional.ifPresent(BubbleController::initialize); - - // Bind the splitscreen impl to the drag drop controller - mDragAndDropController.initialize(mSplitScreenOptional); - - if (Transitions.ENABLE_SHELL_TRANSITIONS) { - mTransitions.register(mShellTaskOrganizer); - mUnfoldTransitionHandler.ifPresent(UnfoldTransitionHandler::init); - } - - // TODO(b/181599115): This should really be the pip controller, but until we can provide the - // controller instead of the feature interface, can just initialize the touch handler if - // needed - mPipTouchHandlerOptional.ifPresent((handler) -> handler.init()); - - // Initialize optional freeform - mFreeformTaskListenerOptional.ifPresent(f -> - mShellTaskOrganizer.addListenerForType( - f, ShellTaskOrganizer.TASK_LISTENER_TYPE_FREEFORM)); - - mFullscreenUnfoldController.ifPresent(FullscreenUnfoldController::init); - mRecentTasks.ifPresent(RecentTasksController::init); - - // Initialize kids mode task organizer - mKidsModeTaskOrganizer.initialize(mStartingWindow); - } - - @ExternalThread - private class InitImpl implements ShellInit { - @Override - public void init() { - try { - mMainExecutor.executeBlocking(() -> ShellInitImpl.this.init()); - } catch (InterruptedException e) { - throw new RuntimeException("Failed to initialize the Shell in 2s", e); - } - } - } -} 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 31f0ef0192ae..43679364b443 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java @@ -16,13 +16,16 @@ package com.android.wm.shell; +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_MULTI_WINDOW; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; +import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE; import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_TASK_ORG; +import static com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS; import android.annotation.IntDef; import android.annotation.NonNull; @@ -30,7 +33,6 @@ import android.annotation.Nullable; import android.app.ActivityManager.RunningTaskInfo; import android.app.TaskInfo; import android.app.WindowConfiguration; -import android.content.Context; import android.content.LocusId; import android.content.pm.ActivityInfo; import android.graphics.Rect; @@ -46,6 +48,7 @@ import android.window.StartingWindowInfo; import android.window.StartingWindowRemovalInfo; import android.window.TaskAppearedInfo; import android.window.TaskOrganizer; +import android.window.WindowContainerTransaction; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.protolog.common.ProtoLog; @@ -55,6 +58,9 @@ import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.compatui.CompatUIController; import com.android.wm.shell.recents.RecentTasksController; import com.android.wm.shell.startingsurface.StartingWindowController; +import com.android.wm.shell.sysui.ShellCommandHandler; +import com.android.wm.shell.sysui.ShellInit; +import com.android.wm.shell.unfold.UnfoldAnimationController; import java.io.PrintWriter; import java.util.ArrayList; @@ -70,6 +76,7 @@ import java.util.function.Consumer; */ public class ShellTaskOrganizer extends TaskOrganizer implements CompatUIController.CompatUICallback { + private static final String TAG = "ShellTaskOrganizer"; // Intentionally using negative numbers here so the positive numbers can be used // for task id specific listeners that will be added later. @@ -88,8 +95,6 @@ public class ShellTaskOrganizer extends TaskOrganizer implements }) public @interface TaskListenerType {} - private static final String TAG = "ShellTaskOrganizer"; - /** * Callbacks for when the tasks change in the system. */ @@ -175,42 +180,62 @@ public class ShellTaskOrganizer extends TaskOrganizer implements @Nullable private final CompatUIController mCompatUI; + @NonNull + private final ShellCommandHandler mShellCommandHandler; + @Nullable private final Optional<RecentTasksController> mRecentTasks; @Nullable - private RunningTaskInfo mLastFocusedTaskInfo; + private final UnfoldAnimationController mUnfoldAnimationController; - public ShellTaskOrganizer(ShellExecutor mainExecutor, Context context) { - this(null /* taskOrganizerController */, mainExecutor, context, null /* compatUI */, - Optional.empty() /* recentTasksController */); - } + @Nullable + private RunningTaskInfo mLastFocusedTaskInfo; - public ShellTaskOrganizer(ShellExecutor mainExecutor, Context context, @Nullable - CompatUIController compatUI) { - this(null /* taskOrganizerController */, mainExecutor, context, compatUI, - Optional.empty() /* recentTasksController */); + public ShellTaskOrganizer(ShellExecutor mainExecutor) { + this(null /* shellInit */, null /* shellCommandHandler */, + null /* taskOrganizerController */, null /* compatUI */, + Optional.empty() /* unfoldAnimationController */, + Optional.empty() /* recentTasksController */, + mainExecutor); } - public ShellTaskOrganizer(ShellExecutor mainExecutor, Context context, @Nullable - CompatUIController compatUI, - Optional<RecentTasksController> recentTasks) { - this(null /* taskOrganizerController */, mainExecutor, context, compatUI, - recentTasks); + public ShellTaskOrganizer(ShellInit shellInit, + ShellCommandHandler shellCommandHandler, + @Nullable CompatUIController compatUI, + Optional<UnfoldAnimationController> unfoldAnimationController, + Optional<RecentTasksController> recentTasks, + ShellExecutor mainExecutor) { + this(shellInit, shellCommandHandler, null /* taskOrganizerController */, compatUI, + unfoldAnimationController, recentTasks, mainExecutor); } @VisibleForTesting - protected ShellTaskOrganizer(ITaskOrganizerController taskOrganizerController, - ShellExecutor mainExecutor, Context context, @Nullable CompatUIController compatUI, - Optional<RecentTasksController> recentTasks) { + protected ShellTaskOrganizer(ShellInit shellInit, + ShellCommandHandler shellCommandHandler, + ITaskOrganizerController taskOrganizerController, + @Nullable CompatUIController compatUI, + Optional<UnfoldAnimationController> unfoldAnimationController, + Optional<RecentTasksController> recentTasks, + ShellExecutor mainExecutor) { super(taskOrganizerController, mainExecutor); + mShellCommandHandler = shellCommandHandler; mCompatUI = compatUI; mRecentTasks = recentTasks; - if (compatUI != null) { - compatUI.setCompatUICallback(this); + mUnfoldAnimationController = unfoldAnimationController.orElse(null); + if (shellInit != null) { + shellInit.addInitCallback(this::onInit, this); } } + private void onInit() { + mShellCommandHandler.addDumpCallback(this::dump, this); + if (mCompatUI != null) { + mCompatUI.setCompatUICallback(this); + } + registerOrganizer(); + } + @Override public List<TaskAppearedInfo> registerOrganizer() { synchronized (mLock) { @@ -437,8 +462,12 @@ public class ShellTaskOrganizer extends TaskOrganizer implements if (listener != null) { listener.onTaskAppeared(info.getTaskInfo(), info.getLeash()); } + if (mUnfoldAnimationController != null) { + mUnfoldAnimationController.onTaskAppeared(info.getTaskInfo(), info.getLeash()); + } notifyLocusVisibilityIfNeeded(info.getTaskInfo()); notifyCompatUI(info.getTaskInfo(), listener); + mRecentTasks.ifPresent(recentTasks -> recentTasks.onTaskAdded(info.getTaskInfo())); } /** @@ -458,6 +487,11 @@ public class ShellTaskOrganizer extends TaskOrganizer implements public void onTaskInfoChanged(RunningTaskInfo taskInfo) { synchronized (mLock) { ProtoLog.v(WM_SHELL_TASK_ORG, "Task info changed taskId=%d", taskInfo.taskId); + + if (mUnfoldAnimationController != null) { + mUnfoldAnimationController.onTaskInfoChanged(taskInfo); + } + final TaskAppearedInfo data = mTasks.get(taskInfo.taskId); final TaskListener oldListener = getTaskListener(data.getTaskInfo()); final TaskListener newListener = getTaskListener(taskInfo); @@ -482,7 +516,9 @@ public class ShellTaskOrganizer extends TaskOrganizer implements || (taskInfo.topActivityType == WindowConfiguration.ACTIVITY_TYPE_HOME && taskInfo.isVisible); final boolean focusTaskChanged = (mLastFocusedTaskInfo == null - || mLastFocusedTaskInfo.taskId != taskInfo.taskId) && isFocusedOrHome; + || mLastFocusedTaskInfo.taskId != taskInfo.taskId + || mLastFocusedTaskInfo.getWindowingMode() != taskInfo.getWindowingMode()) + && isFocusedOrHome; if (focusTaskChanged) { for (int i = 0; i < mFocusListeners.size(); i++) { mFocusListeners.valueAt(i).onFocusTaskChanged(taskInfo); @@ -507,8 +543,13 @@ public class ShellTaskOrganizer extends TaskOrganizer implements public void onTaskVanished(RunningTaskInfo taskInfo) { synchronized (mLock) { ProtoLog.v(WM_SHELL_TASK_ORG, "Task vanished taskId=%d", taskInfo.taskId); + if (mUnfoldAnimationController != null) { + mUnfoldAnimationController.onTaskVanished(taskInfo); + } + final int taskId = taskInfo.taskId; - final TaskListener listener = getTaskListener(mTasks.get(taskId).getTaskInfo()); + final TaskAppearedInfo appearedInfo = mTasks.get(taskId); + final TaskListener listener = getTaskListener(appearedInfo.getTaskInfo()); mTasks.remove(taskId); if (listener != null) { listener.onTaskVanished(taskInfo); @@ -518,6 +559,11 @@ public class ShellTaskOrganizer extends TaskOrganizer implements notifyCompatUI(taskInfo, null /* taskListener */); // Notify the recent tasks that a task has been removed mRecentTasks.ifPresent(recentTasks -> recentTasks.onTaskRemoved(taskInfo)); + + if (!ENABLE_SHELL_TRANSITIONS && (appearedInfo.getLeash() != null)) { + // Preemptively clean up the leash only if shell transitions are not enabled + appearedInfo.getLeash().release(); + } } } @@ -647,6 +693,57 @@ public class ShellTaskOrganizer extends TaskOrganizer implements taskListener.reparentChildSurfaceToTask(taskId, sc, t); } + /** + * Create a {@link WindowContainerTransaction} to clear task bounds. + * + * Only affects tasks that have {@link RunningTaskInfo#getActivityType()} set to + * {@link WindowConfiguration#ACTIVITY_TYPE_STANDARD}. + * + * @param displayId display id for tasks that will have bounds cleared + * @return {@link WindowContainerTransaction} with pending operations to clear bounds + */ + public WindowContainerTransaction prepareClearBoundsForStandardTasks(int displayId) { + ProtoLog.d(WM_SHELL_DESKTOP_MODE, "prepareClearBoundsForTasks: displayId=%d", displayId); + WindowContainerTransaction wct = new WindowContainerTransaction(); + for (int i = 0; i < mTasks.size(); i++) { + RunningTaskInfo taskInfo = mTasks.valueAt(i).getTaskInfo(); + if ((taskInfo.displayId == displayId) && (taskInfo.getActivityType() + == WindowConfiguration.ACTIVITY_TYPE_STANDARD)) { + ProtoLog.d(WM_SHELL_DESKTOP_MODE, "clearing bounds for token=%s taskInfo=%s", + taskInfo.token, taskInfo); + wct.setBounds(taskInfo.token, null); + } + } + return wct; + } + + /** + * Create a {@link WindowContainerTransaction} to clear task level freeform setting. + * + * Only affects tasks that have {@link RunningTaskInfo#getActivityType()} set to + * {@link WindowConfiguration#ACTIVITY_TYPE_STANDARD}. + * + * @param displayId display id for tasks that will have windowing mode reset to {@link + * WindowConfiguration#WINDOWING_MODE_UNDEFINED} + * @return {@link WindowContainerTransaction} with pending operations to clear windowing mode + */ + public WindowContainerTransaction prepareClearFreeformForStandardTasks(int displayId) { + ProtoLog.d(WM_SHELL_DESKTOP_MODE, "prepareClearFreeformForTasks: displayId=%d", displayId); + WindowContainerTransaction wct = new WindowContainerTransaction(); + for (int i = 0; i < mTasks.size(); i++) { + RunningTaskInfo taskInfo = mTasks.valueAt(i).getTaskInfo(); + if (taskInfo.displayId == displayId + && taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM + && taskInfo.getActivityType() == ACTIVITY_TYPE_STANDARD) { + ProtoLog.d(WM_SHELL_DESKTOP_MODE, + "clearing windowing mode for token=%s taskInfo=%s", taskInfo.token, + taskInfo); + wct.setWindowingMode(taskInfo.token, WINDOWING_MODE_UNDEFINED); + } + } + return wct; + } + private void logSizeCompatRestartButtonEventReported(@NonNull TaskAppearedInfo info, int event) { ActivityInfo topActivityInfo = info.getTaskInfo().topActivityInfo; @@ -773,7 +870,18 @@ public class ShellTaskOrganizer extends TaskOrganizer implements final int key = mTasks.keyAt(i); final TaskAppearedInfo info = mTasks.valueAt(i); final TaskListener listener = getTaskListener(info.getTaskInfo()); - pw.println(innerPrefix + "#" + i + " task=" + key + " listener=" + listener); + final int windowingMode = info.getTaskInfo().getWindowingMode(); + String pkg = ""; + if (info.getTaskInfo().baseActivity != null) { + pkg = info.getTaskInfo().baseActivity.getPackageName(); + } + Rect bounds = info.getTaskInfo().getConfiguration().windowConfiguration.getBounds(); + boolean running = info.getTaskInfo().isRunning; + boolean visible = info.getTaskInfo().isVisible; + boolean focused = info.getTaskInfo().isFocused; + pw.println(innerPrefix + "#" + i + " task=" + key + " listener=" + listener + + " wmMode=" + windowingMode + " pkg=" + pkg + " bounds=" + bounds + + " running=" + running + " visible=" + visible + " focused=" + focused); } pw.println(); @@ -783,6 +891,7 @@ public class ShellTaskOrganizer extends TaskOrganizer implements final TaskListener listener = mLaunchCookieToListener.valueAt(i); pw.println(innerPrefix + "#" + i + " cookie=" + key + " listener=" + listener); } + } } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/TaskView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/TaskView.java index d28a68a42b2b..d76ad3d27c70 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/TaskView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/TaskView.java @@ -54,7 +54,10 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback, /** Callback for listening task state. */ public interface Listener { - /** Called when the container is ready for launching activities. */ + /** + * Only called once when the surface has been created & the container is ready for + * launching activities. + */ default void onInitialized() {} /** Called when the container can no longer launch activities. */ @@ -80,12 +83,13 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback, private final SyncTransactionQueue mSyncQueue; private final TaskViewTransitions mTaskViewTransitions; - private ActivityManager.RunningTaskInfo mTaskInfo; + protected ActivityManager.RunningTaskInfo mTaskInfo; private WindowContainerToken mTaskToken; private SurfaceControl mTaskLeash; private final SurfaceControl.Transaction mTransaction = new SurfaceControl.Transaction(); private boolean mSurfaceCreated; private boolean mIsInitialized; + private boolean mNotifiedForInitialized; private Listener mListener; private Executor mListenerExecutor; private Region mObscuredTouchRegion; @@ -110,6 +114,13 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback, mGuard.open("release"); } + /** + * @return {@code True} when the TaskView's surface has been created, {@code False} otherwise. + */ + public boolean isInitialized() { + return mIsInitialized; + } + /** Until all users are converted, we may have mixed-use (eg. Car). */ private boolean isUsingShellTransitions() { return mTaskViewTransitions != null && Transitions.ENABLE_SHELL_TRANSITIONS; @@ -269,11 +280,17 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback, resetTaskInfo(); }); mGuard.close(); - if (mListener != null && mIsInitialized) { + mIsInitialized = false; + notifyReleased(); + } + + /** Called when the {@link TaskView} has been released. */ + protected void notifyReleased() { + if (mListener != null && mNotifiedForInitialized) { mListenerExecutor.execute(() -> { mListener.onReleased(); }); - mIsInitialized = false; + mNotifiedForInitialized = false; } } @@ -407,12 +424,8 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback, @Override public void surfaceCreated(SurfaceHolder holder) { mSurfaceCreated = true; - if (mListener != null && !mIsInitialized) { - mIsInitialized = true; - mListenerExecutor.execute(() -> { - mListener.onInitialized(); - }); - } + mIsInitialized = true; + notifyInitialized(); mShellExecutor.execute(() -> { if (mTaskToken == null) { // Nothing to update, task is not yet available @@ -430,6 +443,16 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback, }); } + /** Called when the {@link TaskView} is initialized. */ + protected void notifyInitialized() { + if (mListener != null && !mNotifiedForInitialized) { + mNotifiedForInitialized = true; + mListenerExecutor.execute(() -> { + mListener.onInitialized(); + }); + } + } + @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { if (mTaskToken == null) { @@ -494,7 +517,9 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback, getViewTreeObserver().removeOnComputeInternalInsetsListener(this); } - ActivityManager.RunningTaskInfo getTaskInfo() { + /** Returns the task info for the task in the TaskView. */ + @Nullable + public ActivityManager.RunningTaskInfo getTaskInfo() { return mTaskInfo; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java new file mode 100644 index 000000000000..591e3476ecd9 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java @@ -0,0 +1,210 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.activityembedding; + +import static android.graphics.Matrix.MTRANS_X; +import static android.graphics.Matrix.MTRANS_Y; + +import android.annotation.CallSuper; +import android.graphics.Point; +import android.graphics.Rect; +import android.view.Choreographer; +import android.view.SurfaceControl; +import android.view.animation.Animation; +import android.view.animation.Transformation; +import android.window.TransitionInfo; + +import androidx.annotation.NonNull; + +/** + * Wrapper to handle the ActivityEmbedding animation update in one + * {@link SurfaceControl.Transaction}. + */ +class ActivityEmbeddingAnimationAdapter { + + /** + * If {@link #mOverrideLayer} is set to this value, we don't want to override the surface layer. + */ + private static final int LAYER_NO_OVERRIDE = -1; + + @NonNull + final Animation mAnimation; + @NonNull + final TransitionInfo.Change mChange; + @NonNull + final SurfaceControl mLeash; + /** Area in absolute coordinate that the animation surface shouldn't go beyond. */ + @NonNull + private final Rect mWholeAnimationBounds = new Rect(); + + @NonNull + final Transformation mTransformation = new Transformation(); + @NonNull + final float[] mMatrix = new float[9]; + @NonNull + final float[] mVecs = new float[4]; + @NonNull + final Rect mRect = new Rect(); + private boolean mIsFirstFrame = true; + private int mOverrideLayer = LAYER_NO_OVERRIDE; + + ActivityEmbeddingAnimationAdapter(@NonNull Animation animation, + @NonNull TransitionInfo.Change change) { + this(animation, change, change.getLeash(), change.getEndAbsBounds()); + } + + /** + * @param leash the surface to animate, which is not necessary the same as + * {@link TransitionInfo.Change#getLeash()}, it can be a screenshot for example. + * @param wholeAnimationBounds area in absolute coordinate that the animation surface shouldn't + * go beyond. + */ + ActivityEmbeddingAnimationAdapter(@NonNull Animation animation, + @NonNull TransitionInfo.Change change, @NonNull SurfaceControl leash, + @NonNull Rect wholeAnimationBounds) { + mAnimation = animation; + mChange = change; + mLeash = leash; + mWholeAnimationBounds.set(wholeAnimationBounds); + } + + /** + * Surface layer to be set at the first frame of the animation. We will not set the layer if it + * is set to {@link #LAYER_NO_OVERRIDE}. + */ + final void overrideLayer(int layer) { + mOverrideLayer = layer; + } + + /** Called on frame update. */ + final void onAnimationUpdate(@NonNull SurfaceControl.Transaction t, long currentPlayTime) { + if (mIsFirstFrame) { + t.show(mLeash); + if (mOverrideLayer != LAYER_NO_OVERRIDE) { + t.setLayer(mLeash, mOverrideLayer); + } + mIsFirstFrame = false; + } + + // Extract the transformation to the current time. + mAnimation.getTransformation(Math.min(currentPlayTime, mAnimation.getDuration()), + mTransformation); + t.setFrameTimelineVsync(Choreographer.getInstance().getVsyncId()); + onAnimationUpdateInner(t); + } + + /** To be overridden by subclasses to adjust the animation surface change. */ + void onAnimationUpdateInner(@NonNull SurfaceControl.Transaction t) { + // Update the surface position and alpha. + final Point offset = mChange.getEndRelOffset(); + mTransformation.getMatrix().postTranslate(offset.x, offset.y); + t.setMatrix(mLeash, mTransformation.getMatrix(), mMatrix); + t.setAlpha(mLeash, mTransformation.getAlpha()); + + // Get current surface bounds in absolute coordinate. + // positionX/Y are in local coordinate, so minus the local offset to get the slide amount. + final int positionX = Math.round(mMatrix[MTRANS_X]); + final int positionY = Math.round(mMatrix[MTRANS_Y]); + final Rect cropRect = new Rect(mChange.getEndAbsBounds()); + cropRect.offset(positionX - offset.x, positionY - offset.y); + + // Store the current offset of the surface top left from (0,0) in absolute coordinate. + final int offsetX = cropRect.left; + final int offsetY = cropRect.top; + + // Intersect to make sure the animation happens within the whole animation bounds. + if (!cropRect.intersect(mWholeAnimationBounds)) { + // Hide the surface when it is outside of the animation area. + t.setAlpha(mLeash, 0); + } + + // cropRect is in absolute coordinate, so we need to translate it to surface top left. + cropRect.offset(-offsetX, -offsetY); + t.setCrop(mLeash, cropRect); + } + + /** Called after animation finished. */ + @CallSuper + void onAnimationEnd(@NonNull SurfaceControl.Transaction t) { + onAnimationUpdate(t, mAnimation.getDuration()); + } + + final long getDurationHint() { + return mAnimation.computeDurationHint(); + } + + /** + * Should be used for the animation of the snapshot of a {@link TransitionInfo.Change} that has + * size change. + */ + static class SnapshotAdapter extends ActivityEmbeddingAnimationAdapter { + + SnapshotAdapter(@NonNull Animation animation, @NonNull TransitionInfo.Change change, + @NonNull SurfaceControl snapshotLeash) { + super(animation, change, snapshotLeash, change.getEndAbsBounds()); + } + + @Override + void onAnimationUpdateInner(@NonNull SurfaceControl.Transaction t) { + // Snapshot should always be placed at the top left of the animation leash. + mTransformation.getMatrix().postTranslate(0, 0); + t.setMatrix(mLeash, mTransformation.getMatrix(), mMatrix); + t.setAlpha(mLeash, mTransformation.getAlpha()); + } + + @Override + void onAnimationEnd(@NonNull SurfaceControl.Transaction t) { + super.onAnimationEnd(t); + // Remove the screenshot leash after animation is finished. + if (mLeash.isValid()) { + t.remove(mLeash); + } + } + } + + /** + * Should be used for the animation of the {@link TransitionInfo.Change} that has size change. + */ + static class BoundsChangeAdapter extends ActivityEmbeddingAnimationAdapter { + + BoundsChangeAdapter(@NonNull Animation animation, @NonNull TransitionInfo.Change change) { + super(animation, change); + } + + @Override + void onAnimationUpdateInner(@NonNull SurfaceControl.Transaction t) { + final Point offset = mChange.getEndRelOffset(); + mTransformation.getMatrix().postTranslate(offset.x, offset.y); + t.setMatrix(mLeash, mTransformation.getMatrix(), mMatrix); + t.setAlpha(mLeash, mTransformation.getAlpha()); + + // The following applies an inverse scale to the clip-rect so that it crops "after" the + // scale instead of before. + mVecs[1] = mVecs[2] = 0; + mVecs[0] = mVecs[3] = 1; + mTransformation.getMatrix().mapVectors(mVecs); + mVecs[0] = 1.f / mVecs[0]; + mVecs[3] = 1.f / mVecs[3]; + final Rect clipRect = mTransformation.getClipRect(); + mRect.left = (int) (clipRect.left * mVecs[0] + 0.5f); + mRect.right = (int) (clipRect.right * mVecs[0] + 0.5f); + mRect.top = (int) (clipRect.top * mVecs[3] + 0.5f); + mRect.bottom = (int) (clipRect.bottom * mVecs[3] + 0.5f); + t.setCrop(mLeash, mRect); + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java new file mode 100644 index 000000000000..756d80204833 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java @@ -0,0 +1,347 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.activityembedding; + +import static android.view.WindowManager.TRANSIT_CHANGE; +import static android.view.WindowManagerPolicyConstants.TYPE_LAYER_OFFSET; +import static android.window.TransitionInfo.FLAG_IS_BEHIND_STARTING_WINDOW; + +import static com.android.wm.shell.transition.TransitionAnimationHelper.addBackgroundToTransition; +import static com.android.wm.shell.transition.TransitionAnimationHelper.getTransitionBackgroundColorIfSet; + +import android.animation.Animator; +import android.animation.ValueAnimator; +import android.content.Context; +import android.graphics.Rect; +import android.os.IBinder; +import android.util.ArraySet; +import android.util.Log; +import android.view.SurfaceControl; +import android.view.animation.Animation; +import android.window.TransitionInfo; +import android.window.WindowContainerToken; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.wm.shell.common.ScreenshotUtils; +import com.android.wm.shell.transition.Transitions; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +/** To run the ActivityEmbedding animations. */ +class ActivityEmbeddingAnimationRunner { + + private static final String TAG = "ActivityEmbeddingAnimR"; + + private final ActivityEmbeddingController mController; + @VisibleForTesting + final ActivityEmbeddingAnimationSpec mAnimationSpec; + + ActivityEmbeddingAnimationRunner(@NonNull Context context, + @NonNull ActivityEmbeddingController controller) { + mController = controller; + mAnimationSpec = new ActivityEmbeddingAnimationSpec(context); + } + + /** Creates and starts animation for ActivityEmbedding transition. */ + void startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction) { + final Animator animator = createAnimator(info, startTransaction, finishTransaction, + () -> mController.onAnimationFinished(transition)); + startTransaction.apply(); + animator.start(); + } + + /** + * Sets transition animation scale settings value. + * @param scale The setting value of transition animation scale. + */ + void setAnimScaleSetting(float scale) { + mAnimationSpec.setAnimScaleSetting(scale); + } + + /** Creates the animator for the given {@link TransitionInfo}. */ + @VisibleForTesting + @NonNull + Animator createAnimator(@NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, + @NonNull Runnable animationFinishCallback) { + final List<ActivityEmbeddingAnimationAdapter> adapters = + createAnimationAdapters(info, startTransaction, finishTransaction); + long duration = 0; + for (ActivityEmbeddingAnimationAdapter adapter : adapters) { + duration = Math.max(duration, adapter.getDurationHint()); + } + final ValueAnimator animator = ValueAnimator.ofFloat(0, 1); + animator.setDuration(duration); + animator.addUpdateListener((anim) -> { + // Update all adapters in the same transaction. + final SurfaceControl.Transaction t = new SurfaceControl.Transaction(); + for (ActivityEmbeddingAnimationAdapter adapter : adapters) { + adapter.onAnimationUpdate(t, animator.getCurrentPlayTime()); + } + t.apply(); + }); + animator.addListener(new Animator.AnimatorListener() { + @Override + public void onAnimationStart(Animator animation) {} + + @Override + public void onAnimationEnd(Animator animation) { + final SurfaceControl.Transaction t = new SurfaceControl.Transaction(); + for (ActivityEmbeddingAnimationAdapter adapter : adapters) { + adapter.onAnimationEnd(t); + } + t.apply(); + animationFinishCallback.run(); + } + + @Override + public void onAnimationCancel(Animator animation) {} + + @Override + public void onAnimationRepeat(Animator animation) {} + }); + return animator; + } + + /** + * Creates list of {@link ActivityEmbeddingAnimationAdapter} to handle animations on all window + * changes. + */ + @NonNull + private List<ActivityEmbeddingAnimationAdapter> createAnimationAdapters( + @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction) { + boolean isChangeTransition = false; + for (TransitionInfo.Change change : info.getChanges()) { + if (change.hasFlags(FLAG_IS_BEHIND_STARTING_WINDOW)) { + // Skip the animation if the windows are behind an app starting window. + return new ArrayList<>(); + } + if (!isChangeTransition && change.getMode() == TRANSIT_CHANGE + && !change.getStartAbsBounds().equals(change.getEndAbsBounds())) { + isChangeTransition = true; + } + } + if (isChangeTransition) { + return createChangeAnimationAdapters(info, startTransaction); + } + if (Transitions.isClosingType(info.getType())) { + return createCloseAnimationAdapters(info, startTransaction, finishTransaction); + } + return createOpenAnimationAdapters(info, startTransaction, finishTransaction); + } + + @NonNull + private List<ActivityEmbeddingAnimationAdapter> createOpenAnimationAdapters( + @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction) { + return createOpenCloseAnimationAdapters(info, startTransaction, finishTransaction, + true /* isOpening */, mAnimationSpec::loadOpenAnimation); + } + + @NonNull + private List<ActivityEmbeddingAnimationAdapter> createCloseAnimationAdapters( + @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction) { + return createOpenCloseAnimationAdapters(info, startTransaction, finishTransaction, + false /* isOpening */, mAnimationSpec::loadCloseAnimation); + } + + /** + * Creates {@link ActivityEmbeddingAnimationAdapter} for OPEN and CLOSE types of transition. + * @param isOpening {@code true} for OPEN type, {@code false} for CLOSE type. + */ + @NonNull + private List<ActivityEmbeddingAnimationAdapter> createOpenCloseAnimationAdapters( + @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, boolean isOpening, + @NonNull AnimationProvider animationProvider) { + // We need to know if the change window is only a partial of the whole animation screen. + // If so, we will need to adjust it to make the whole animation screen looks like one. + final List<TransitionInfo.Change> openingChanges = new ArrayList<>(); + final List<TransitionInfo.Change> closingChanges = new ArrayList<>(); + final Rect openingWholeScreenBounds = new Rect(); + final Rect closingWholeScreenBounds = new Rect(); + for (TransitionInfo.Change change : info.getChanges()) { + if (Transitions.isOpeningType(change.getMode())) { + openingChanges.add(change); + openingWholeScreenBounds.union(change.getEndAbsBounds()); + } else { + closingChanges.add(change); + closingWholeScreenBounds.union(change.getEndAbsBounds()); + } + } + + // For OPEN transition, open windows should be above close windows. + // For CLOSE transition, open windows should be below close windows. + int offsetLayer = TYPE_LAYER_OFFSET; + final List<ActivityEmbeddingAnimationAdapter> adapters = new ArrayList<>(); + for (TransitionInfo.Change change : openingChanges) { + final ActivityEmbeddingAnimationAdapter adapter = createOpenCloseAnimationAdapter( + info, change, startTransaction, finishTransaction, animationProvider, + openingWholeScreenBounds); + if (isOpening) { + adapter.overrideLayer(offsetLayer++); + } + adapters.add(adapter); + } + for (TransitionInfo.Change change : closingChanges) { + final ActivityEmbeddingAnimationAdapter adapter = createOpenCloseAnimationAdapter( + info, change, startTransaction, finishTransaction, animationProvider, + closingWholeScreenBounds); + if (!isOpening) { + adapter.overrideLayer(offsetLayer++); + } + adapters.add(adapter); + } + return adapters; + } + + @NonNull + private ActivityEmbeddingAnimationAdapter createOpenCloseAnimationAdapter( + @NonNull TransitionInfo info, @NonNull TransitionInfo.Change change, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, + @NonNull AnimationProvider animationProvider, @NonNull Rect wholeAnimationBounds) { + final Animation animation = animationProvider.get(info, change, wholeAnimationBounds); + // We may want to show a background color for open/close transition. + final int backgroundColor = getTransitionBackgroundColorIfSet(info, change, animation, + 0 /* defaultColor */); + if (backgroundColor != 0) { + addBackgroundToTransition(info.getRootLeash(), backgroundColor, startTransaction, + finishTransaction); + } + return new ActivityEmbeddingAnimationAdapter(animation, change, change.getLeash(), + wholeAnimationBounds); + } + + @NonNull + private List<ActivityEmbeddingAnimationAdapter> createChangeAnimationAdapters( + @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction) { + final List<ActivityEmbeddingAnimationAdapter> adapters = new ArrayList<>(); + final Set<TransitionInfo.Change> handledChanges = new ArraySet<>(); + + // For the first iteration, we prepare the animation for the change type windows. This is + // needed because there may be window that is reparented while resizing. In such case, we + // will do the following: + // 1. Capture a screenshot from the Activity surface. + // 2. Attach the screenshot surface to the top of TaskFragment (Activity's parent) surface. + // 3. Animate the TaskFragment using Activity Change info (start/end bounds). + // This is because the TaskFragment surface/change won't contain the Activity's before its + // reparent. + for (TransitionInfo.Change change : info.getChanges()) { + if (change.getMode() != TRANSIT_CHANGE + || change.getStartAbsBounds().equals(change.getEndAbsBounds())) { + continue; + } + + // This is the window with bounds change. + handledChanges.add(change); + final WindowContainerToken parentToken = change.getParent(); + TransitionInfo.Change boundsAnimationChange = change; + if (parentToken != null) { + // When the parent window is also included in the transition as an opening window, + // we would like to animate the parent window instead. + final TransitionInfo.Change parentChange = info.getChange(parentToken); + if (parentChange != null && Transitions.isOpeningType(parentChange.getMode())) { + // We won't create a separate animation for the parent, but to animate the + // parent for the child resizing. + handledChanges.add(parentChange); + boundsAnimationChange = parentChange; + } + } + + final Animation[] animations = mAnimationSpec.createChangeBoundsChangeAnimations(change, + boundsAnimationChange.getEndAbsBounds()); + + // Create a screenshot based on change, but attach it to the top of the + // boundsAnimationChange. + final SurfaceControl screenshotLeash = getOrCreateScreenshot(change, + boundsAnimationChange, startTransaction); + if (screenshotLeash != null) { + // Adapter for the starting screenshot leash. + // The screenshot leash will be removed in SnapshotAdapter#onAnimationEnd + adapters.add(new ActivityEmbeddingAnimationAdapter.SnapshotAdapter( + animations[0], change, screenshotLeash)); + } else { + Log.e(TAG, "Failed to take screenshot for change=" + change); + } + // Adapter for the ending bounds changed leash. + adapters.add(new ActivityEmbeddingAnimationAdapter.BoundsChangeAdapter( + animations[1], boundsAnimationChange)); + } + + // Handle the other windows that don't have bounds change in the same transition. + for (TransitionInfo.Change change : info.getChanges()) { + if (handledChanges.contains(change)) { + // Skip windows that we have already handled in the previous iteration. + continue; + } + + final Animation animation; + if (change.getParent() != null + && handledChanges.contains(info.getChange(change.getParent()))) { + // No-op if it will be covered by the changing parent window. + animation = ActivityEmbeddingAnimationSpec.createNoopAnimation(change); + } else if (Transitions.isClosingType(change.getMode())) { + animation = mAnimationSpec.createChangeBoundsCloseAnimation(change); + } else { + animation = mAnimationSpec.createChangeBoundsOpenAnimation(change); + } + adapters.add(new ActivityEmbeddingAnimationAdapter(animation, change)); + } + return adapters; + } + + /** + * Takes a screenshot of the given {@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. + */ + @Nullable + private SurfaceControl getOrCreateScreenshot(@NonNull TransitionInfo.Change screenshotChange, + @NonNull TransitionInfo.Change animationChange, + @NonNull SurfaceControl.Transaction t) { + final SurfaceControl screenshotLeash = screenshotChange.getSnapshot(); + if (screenshotLeash != null) { + // If WM Core has already taken a screenshot, make sure it is reparented to the + // animation leash. + t.reparent(screenshotLeash, animationChange.getLeash()); + return screenshotLeash; + } + + // If WM Core hasn't taken a screenshot, take a screenshot now. + final Rect cropBounds = new Rect(screenshotChange.getStartAbsBounds()); + cropBounds.offsetTo(0, 0); + return ScreenshotUtils.takeScreenshot(t, screenshotChange.getLeash(), + animationChange.getLeash(), cropBounds, Integer.MAX_VALUE); + } + + /** To provide an {@link Animation} based on the transition infos. */ + private interface AnimationProvider { + Animation get(@NonNull TransitionInfo info, @NonNull TransitionInfo.Change change, + @NonNull Rect animationBounds); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java new file mode 100644 index 000000000000..eb6ac7615266 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java @@ -0,0 +1,233 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.activityembedding; + + +import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_NONE; +import static com.android.wm.shell.transition.TransitionAnimationHelper.loadAttributeAnimation; + +import android.content.Context; +import android.graphics.Point; +import android.graphics.Rect; +import android.view.animation.AlphaAnimation; +import android.view.animation.Animation; +import android.view.animation.AnimationSet; +import android.view.animation.AnimationUtils; +import android.view.animation.ClipRectAnimation; +import android.view.animation.Interpolator; +import android.view.animation.LinearInterpolator; +import android.view.animation.ScaleAnimation; +import android.view.animation.TranslateAnimation; +import android.window.TransitionInfo; + +import androidx.annotation.NonNull; + +import com.android.internal.policy.TransitionAnimation; +import com.android.wm.shell.transition.Transitions; + +/** Animation spec for ActivityEmbedding transition. */ +// TODO(b/206557124): provide an easier way to customize animation +class ActivityEmbeddingAnimationSpec { + + private static final String TAG = "ActivityEmbeddingAnimSpec"; + private static final int CHANGE_ANIMATION_DURATION = 517; + private static final int CHANGE_ANIMATION_FADE_DURATION = 80; + private static final int CHANGE_ANIMATION_FADE_OFFSET = 30; + + private final Context mContext; + private final TransitionAnimation mTransitionAnimation; + private final Interpolator mFastOutExtraSlowInInterpolator; + private final LinearInterpolator mLinearInterpolator; + private float mTransitionAnimationScaleSetting; + + ActivityEmbeddingAnimationSpec(@NonNull Context context) { + mContext = context; + mTransitionAnimation = new TransitionAnimation(mContext, false /* debug */, TAG); + mFastOutExtraSlowInInterpolator = AnimationUtils.loadInterpolator( + mContext, android.R.interpolator.fast_out_extra_slow_in); + mLinearInterpolator = new LinearInterpolator(); + } + + /** + * Sets transition animation scale settings value. + * @param scale The setting value of transition animation scale. + */ + void setAnimScaleSetting(float scale) { + mTransitionAnimationScaleSetting = scale; + } + + /** For window that doesn't need to be animated. */ + @NonNull + static Animation createNoopAnimation(@NonNull TransitionInfo.Change change) { + // Noop but just keep the window showing/hiding. + final float alpha = Transitions.isClosingType(change.getMode()) ? 0f : 1f; + return new AlphaAnimation(alpha, alpha); + } + + /** Animation for window that is opening in a change transition. */ + @NonNull + Animation createChangeBoundsOpenAnimation(@NonNull TransitionInfo.Change change) { + final Rect bounds = change.getEndAbsBounds(); + final Point offset = change.getEndRelOffset(); + // The window will be animated in from left or right depends on its position. + final int startLeft = offset.x == 0 ? -bounds.width() : bounds.width(); + + // The position should be 0-based as we will post translate in + // ActivityEmbeddingAnimationAdapter#onAnimationUpdate + final Animation animation = new TranslateAnimation(startLeft, 0, 0, 0); + animation.setInterpolator(mFastOutExtraSlowInInterpolator); + animation.setDuration(CHANGE_ANIMATION_DURATION); + animation.initialize(bounds.width(), bounds.height(), bounds.width(), bounds.height()); + animation.scaleCurrentDuration(mTransitionAnimationScaleSetting); + return animation; + } + + /** Animation for window that is closing in a change transition. */ + @NonNull + Animation createChangeBoundsCloseAnimation(@NonNull TransitionInfo.Change change) { + final Rect bounds = change.getEndAbsBounds(); + final Point offset = change.getEndRelOffset(); + // The window will be animated out to left or right depends on its position. + final int endLeft = offset.x == 0 ? -bounds.width() : bounds.width(); + + // The position should be 0-based as we will post translate in + // ActivityEmbeddingAnimationAdapter#onAnimationUpdate + final Animation animation = new TranslateAnimation(0, endLeft, 0, 0); + animation.setInterpolator(mFastOutExtraSlowInInterpolator); + animation.setDuration(CHANGE_ANIMATION_DURATION); + animation.initialize(bounds.width(), bounds.height(), bounds.width(), bounds.height()); + animation.scaleCurrentDuration(mTransitionAnimationScaleSetting); + return animation; + } + + /** + * Animation for window that is changing (bounds change) in a change transition. + * @return the return array always has two elements. The first one is for the start leash, and + * the second one is for the end leash. + */ + @NonNull + Animation[] createChangeBoundsChangeAnimations(@NonNull TransitionInfo.Change change, + @NonNull Rect parentBounds) { + // Both start bounds and end bounds are in screen coordinates. We will post translate + // to the local coordinates in ActivityEmbeddingAnimationAdapter#onAnimationUpdate + final Rect startBounds = change.getStartAbsBounds(); + final Rect endBounds = change.getEndAbsBounds(); + float scaleX = ((float) startBounds.width()) / endBounds.width(); + float scaleY = ((float) startBounds.height()) / endBounds.height(); + // Start leash is a child of the end leash. Reverse the scale so that the start leash won't + // be scaled up with its parent. + float startScaleX = 1.f / scaleX; + float startScaleY = 1.f / scaleY; + + // The start leash will be fade out. + final AnimationSet startSet = new AnimationSet(false /* shareInterpolator */); + final Animation startAlpha = new AlphaAnimation(1f, 0f); + startAlpha.setInterpolator(mLinearInterpolator); + startAlpha.setDuration(CHANGE_ANIMATION_FADE_DURATION); + startAlpha.setStartOffset(CHANGE_ANIMATION_FADE_OFFSET); + startSet.addAnimation(startAlpha); + final Animation startScale = new ScaleAnimation(startScaleX, startScaleX, startScaleY, + startScaleY); + startScale.setInterpolator(mFastOutExtraSlowInInterpolator); + startScale.setDuration(CHANGE_ANIMATION_DURATION); + startSet.addAnimation(startScale); + startSet.initialize(startBounds.width(), startBounds.height(), endBounds.width(), + endBounds.height()); + startSet.scaleCurrentDuration(mTransitionAnimationScaleSetting); + + // The end leash will be moved into the end position while scaling. + final AnimationSet endSet = new AnimationSet(true /* shareInterpolator */); + endSet.setInterpolator(mFastOutExtraSlowInInterpolator); + final Animation endScale = new ScaleAnimation(scaleX, 1, scaleY, 1); + endScale.setDuration(CHANGE_ANIMATION_DURATION); + endSet.addAnimation(endScale); + // The position should be 0-based as we will post translate in + // ActivityEmbeddingAnimationAdapter#onAnimationUpdate + final Animation endTranslate = new TranslateAnimation(startBounds.left - endBounds.left, 0, + 0, 0); + endTranslate.setDuration(CHANGE_ANIMATION_DURATION); + endSet.addAnimation(endTranslate); + // The end leash is resizing, we should update the window crop based on the clip rect. + final Rect startClip = new Rect(startBounds); + final Rect endClip = new Rect(endBounds); + startClip.offsetTo(0, 0); + endClip.offsetTo(0, 0); + final Animation clipAnim = new ClipRectAnimation(startClip, endClip); + clipAnim.setDuration(CHANGE_ANIMATION_DURATION); + endSet.addAnimation(clipAnim); + endSet.initialize(startBounds.width(), startBounds.height(), parentBounds.width(), + parentBounds.height()); + endSet.scaleCurrentDuration(mTransitionAnimationScaleSetting); + + return new Animation[]{startSet, endSet}; + } + + @NonNull + Animation loadOpenAnimation(@NonNull TransitionInfo info, + @NonNull TransitionInfo.Change change, @NonNull Rect wholeAnimationBounds) { + final boolean isEnter = Transitions.isOpeningType(change.getMode()); + final Animation animation; + // TODO(b/207070762): Implement edgeExtension version + if (shouldShowBackdrop(info, change)) { + animation = mTransitionAnimation.loadDefaultAnimationRes(isEnter + ? com.android.internal.R.anim.task_fragment_clear_top_open_enter + : com.android.internal.R.anim.task_fragment_clear_top_open_exit); + } else { + animation = mTransitionAnimation.loadDefaultAnimationRes(isEnter + ? com.android.internal.R.anim.task_fragment_open_enter + : com.android.internal.R.anim.task_fragment_open_exit); + } + // Use the whole animation bounds instead of the change bounds, so that when multiple change + // targets are opening at the same time, the animation applied to each will be the same. + // Otherwise, we may see gap between the activities that are launching together. + animation.initialize(wholeAnimationBounds.width(), wholeAnimationBounds.height(), + wholeAnimationBounds.width(), wholeAnimationBounds.height()); + animation.scaleCurrentDuration(mTransitionAnimationScaleSetting); + return animation; + } + + @NonNull + Animation loadCloseAnimation(@NonNull TransitionInfo info, + @NonNull TransitionInfo.Change change, @NonNull Rect wholeAnimationBounds) { + final boolean isEnter = Transitions.isOpeningType(change.getMode()); + final Animation animation; + // TODO(b/207070762): Implement edgeExtension version + if (shouldShowBackdrop(info, change)) { + animation = mTransitionAnimation.loadDefaultAnimationRes(isEnter + ? com.android.internal.R.anim.task_fragment_clear_top_close_enter + : com.android.internal.R.anim.task_fragment_clear_top_close_exit); + } else { + animation = mTransitionAnimation.loadDefaultAnimationRes(isEnter + ? com.android.internal.R.anim.task_fragment_close_enter + : com.android.internal.R.anim.task_fragment_close_exit); + } + // Use the whole animation bounds instead of the change bounds, so that when multiple change + // targets are closing at the same time, the animation applied to each will be the same. + // Otherwise, we may see gap between the activities that are finishing together. + animation.initialize(wholeAnimationBounds.width(), wholeAnimationBounds.height(), + wholeAnimationBounds.width(), wholeAnimationBounds.height()); + animation.scaleCurrentDuration(mTransitionAnimationScaleSetting); + return animation; + } + + private boolean shouldShowBackdrop(@NonNull TransitionInfo info, + @NonNull TransitionInfo.Change change) { + final Animation a = loadAttributeAnimation(info, change, WALLPAPER_TRANSITION_NONE, + mTransitionAnimation); + return a != null && a.getShowBackdrop(); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingController.java new file mode 100644 index 000000000000..521a65cc4df6 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingController.java @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.activityembedding; + +import static android.window.TransitionInfo.FLAG_FILLS_TASK; +import static android.window.TransitionInfo.FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY; + +import static java.util.Objects.requireNonNull; + +import android.content.Context; +import android.os.IBinder; +import android.util.ArrayMap; +import android.view.SurfaceControl; +import android.window.TransitionInfo; +import android.window.TransitionRequestInfo; +import android.window.WindowContainerTransaction; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.wm.shell.sysui.ShellInit; +import com.android.wm.shell.transition.Transitions; + +/** + * Responsible for handling ActivityEmbedding related transitions. + */ +public class ActivityEmbeddingController implements Transitions.TransitionHandler { + + private final Context mContext; + @VisibleForTesting + final Transitions mTransitions; + @VisibleForTesting + final ActivityEmbeddingAnimationRunner mAnimationRunner; + + /** + * Keeps track of the currently-running transition callback associated with each transition + * token. + */ + private final ArrayMap<IBinder, Transitions.TransitionFinishCallback> mTransitionCallbacks = + new ArrayMap<>(); + + private ActivityEmbeddingController(@NonNull Context context, @NonNull ShellInit shellInit, + @NonNull Transitions transitions) { + mContext = requireNonNull(context); + mTransitions = requireNonNull(transitions); + mAnimationRunner = new ActivityEmbeddingAnimationRunner(context, this); + + shellInit.addInitCallback(this::onInit, this); + } + + /** + * Creates {@link ActivityEmbeddingController}, returns {@code null} if the feature is not + * supported. + */ + @Nullable + public static ActivityEmbeddingController create(@NonNull Context context, + @NonNull ShellInit shellInit, @NonNull Transitions transitions) { + return Transitions.ENABLE_SHELL_TRANSITIONS + ? new ActivityEmbeddingController(context, shellInit, transitions) + : null; + } + + /** Registers to handle transitions. */ + public void onInit() { + mTransitions.addHandler(this); + } + + @Override + public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, + @NonNull Transitions.TransitionFinishCallback finishCallback) { + boolean containsEmbeddingSplit = false; + for (TransitionInfo.Change change : info.getChanges()) { + if (!change.hasFlags(FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY)) { + // Only animate the transition if all changes are in a Task with ActivityEmbedding. + return false; + } + if (!containsEmbeddingSplit && !change.hasFlags(FLAG_FILLS_TASK)) { + // Whether the Task contains any ActivityEmbedding split before or after the + // transition. + containsEmbeddingSplit = true; + } + } + if (!containsEmbeddingSplit) { + // Let the system to play the default animation if there is no ActivityEmbedding split + // window. This allows to play the app customized animation when there is no embedding, + // such as the device is in a folded state. + return false; + } + + // Start ActivityEmbedding animation. + mTransitionCallbacks.put(transition, finishCallback); + mAnimationRunner.startAnimation(transition, info, startTransaction, finishTransaction); + return true; + } + + @Nullable + @Override + public WindowContainerTransaction handleRequest(@NonNull IBinder transition, + @NonNull TransitionRequestInfo request) { + return null; + } + + @Override + public void setAnimScaleSetting(float scale) { + mAnimationRunner.setAnimScaleSetting(scale); + } + + /** Called when the animation is finished. */ + void onAnimationFinished(@NonNull IBinder transition) { + final Transitions.TransitionFinishCallback callback = + mTransitionCallbacks.remove(transition); + if (callback == null) { + throw new IllegalStateException("No finish callback found"); + } + callback.onTransitionFinished(null /* wct */, null /* wctCB */); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/animation/Interpolators.java b/libs/WindowManager/Shell/src/com/android/wm/shell/animation/Interpolators.java index 2aead9392e59..a0dde6ad168d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/animation/Interpolators.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/animation/Interpolators.java @@ -53,6 +53,19 @@ public class Interpolators { public static final Interpolator LINEAR_OUT_SLOW_IN = new PathInterpolator(0f, 0f, 0.2f, 1f); /** + * The accelerated emphasized interpolator. Used for hero / emphasized movement of content that + * is disappearing e.g. when moving off screen. + */ + public static final Interpolator EMPHASIZED_ACCELERATE = new PathInterpolator( + 0.3f, 0f, 0.8f, 0.15f); + + /** + * The decelerating emphasized interpolator. Used for hero / emphasized movement of content that + * is appearing e.g. when coming from off screen + */ + public static final Interpolator EMPHASIZED_DECELERATE = new PathInterpolator( + 0.05f, 0.7f, 0.1f, 1f); + /** * Interpolator to be used when animating a move based on a click. Pair with enough duration. */ public static final Interpolator TOUCH_RESPONSE = new PathInterpolator(0.3f, 0f, 0.1f, 1f); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/animation/PhysicsAnimator.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/animation/PhysicsAnimator.kt index b483fe03e80f..ee8c41417458 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/animation/PhysicsAnimator.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/animation/PhysicsAnimator.kt @@ -22,7 +22,6 @@ import android.view.View import androidx.dynamicanimation.animation.DynamicAnimation import androidx.dynamicanimation.animation.FlingAnimation import androidx.dynamicanimation.animation.FloatPropertyCompat -import androidx.dynamicanimation.animation.FrameCallbackScheduler import androidx.dynamicanimation.animation.SpringAnimation import androidx.dynamicanimation.animation.SpringForce @@ -125,12 +124,6 @@ class PhysicsAnimator<T> private constructor (target: T) { private var defaultFling: FlingConfig = globalDefaultFling /** - * FrameCallbackScheduler to use if it need custom FrameCallbackScheduler, if this is null, - * it will use the default FrameCallbackScheduler in the DynamicAnimation. - */ - private var customScheduler: FrameCallbackScheduler? = null - - /** * Internal listeners that respond to DynamicAnimations updating and ending, and dispatch to * the listeners provided via [addUpdateListener] and [addEndListener]. This allows us to add * just one permanent update and end listener to the DynamicAnimations. @@ -454,14 +447,6 @@ class PhysicsAnimator<T> private constructor (target: T) { this.defaultFling = defaultFling } - /** - * Set the custom FrameCallbackScheduler for all aniatmion in this animator. Set this with null for - * restoring to default FrameCallbackScheduler. - */ - fun setCustomScheduler(scheduler: FrameCallbackScheduler) { - this.customScheduler = scheduler - } - /** Starts the animations! */ fun start() { startAction() @@ -511,12 +496,9 @@ class PhysicsAnimator<T> private constructor (target: T) { // springs) on this property before flinging. cancel(animatedProperty) - // Apply the custom animation scheduler if it not null - val flingAnim = getFlingAnimation(animatedProperty, target) - flingAnim.scheduler = customScheduler ?: flingAnim.scheduler - // Apply the configuration and start the animation. - flingAnim.also { flingConfig.applyToAnimation(it) }.start() + getFlingAnimation(animatedProperty, target) + .also { flingConfig.applyToAnimation(it) }.start() } } @@ -529,18 +511,6 @@ class PhysicsAnimator<T> private constructor (target: T) { // Apply the configuration and start the animation. val springAnim = getSpringAnimation(animatedProperty, target) - // If customScheduler is exist and has not been set to the animation, - // it should set here. - if (customScheduler != null && - springAnim.scheduler != customScheduler) { - // Cancel the animation before set animation handler - if (springAnim.isRunning) { - cancel(animatedProperty) - } - // Apply the custom scheduler handler if it not null - springAnim.scheduler = customScheduler ?: springAnim.scheduler - } - // Apply the configuration and start the animation. springConfig.applyToAnimation(springAnim) animationStartActions.add(springAnim::start) @@ -596,12 +566,9 @@ class PhysicsAnimator<T> private constructor (target: T) { } } - // Apply the custom animation scheduler if it not null - val springAnim = getSpringAnimation(animatedProperty, target) - springAnim.scheduler = customScheduler ?: springAnim.scheduler - // Apply the configuration and start the spring animation. - springAnim.also { springConfig.applyToAnimation(it) }.start() + getSpringAnimation(animatedProperty, target) + .also { springConfig.applyToAnimation(it) }.start() } } }) @@ -829,8 +796,12 @@ class PhysicsAnimator<T> private constructor (target: T) { /** Cancels all in progress animations on all properties. */ fun cancel() { - cancelAction(flingAnimations.keys) - cancelAction(springAnimations.keys) + if (flingAnimations.size > 0) { + cancelAction(flingAnimations.keys) + } + if (springAnimations.size > 0) { + cancelAction(springAnimations.keys) + } } /** Cancels in progress animations on the provided properties only. */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPair.java b/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPair.java deleted file mode 100644 index 3f0b01bef0ce..000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPair.java +++ /dev/null @@ -1,357 +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.apppairs; - -import static android.app.ActivityTaskManager.INVALID_TASK_ID; -import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; -import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; - -import static 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.protolog.ShellProtoLogGroup.WM_SHELL_TASK_ORG; - -import android.app.ActivityManager; -import android.view.SurfaceControl; -import android.view.SurfaceSession; -import android.window.WindowContainerToken; -import android.window.WindowContainerTransaction; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.android.internal.protolog.common.ProtoLog; -import com.android.wm.shell.ShellTaskOrganizer; -import com.android.wm.shell.common.DisplayController; -import com.android.wm.shell.common.DisplayImeController; -import com.android.wm.shell.common.DisplayInsetsController; -import com.android.wm.shell.common.SurfaceUtils; -import com.android.wm.shell.common.SyncTransactionQueue; -import com.android.wm.shell.common.split.SplitLayout; -import com.android.wm.shell.common.split.SplitWindowManager; - -import java.io.PrintWriter; - -/** - * An app-pairs consisting of {@link #mRootTaskInfo} that acts as the hierarchy parent of - * {@link #mTaskInfo1} and {@link #mTaskInfo2} in the pair. - * Also includes all UI for managing the pair like the divider. - */ -class AppPair implements ShellTaskOrganizer.TaskListener, SplitLayout.SplitLayoutHandler { - private static final String TAG = AppPair.class.getSimpleName(); - - private ActivityManager.RunningTaskInfo mRootTaskInfo; - private SurfaceControl mRootTaskLeash; - private ActivityManager.RunningTaskInfo mTaskInfo1; - private SurfaceControl mTaskLeash1; - private ActivityManager.RunningTaskInfo mTaskInfo2; - private SurfaceControl mTaskLeash2; - private SurfaceControl mDimLayer1; - private SurfaceControl mDimLayer2; - private final SurfaceSession mSurfaceSession = new SurfaceSession(); - - private final AppPairsController mController; - private final SyncTransactionQueue mSyncQueue; - private final DisplayController mDisplayController; - private final DisplayImeController mDisplayImeController; - private final DisplayInsetsController mDisplayInsetsController; - private SplitLayout mSplitLayout; - - private final SplitWindowManager.ParentContainerCallbacks mParentContainerCallbacks = - new SplitWindowManager.ParentContainerCallbacks() { - @Override - public void attachToParentSurface(SurfaceControl.Builder b) { - b.setParent(mRootTaskLeash); - } - - @Override - public void onLeashReady(SurfaceControl leash) { - mSyncQueue.runInSync(t -> t - .show(leash) - .setLayer(leash, Integer.MAX_VALUE) - .setPosition(leash, - mSplitLayout.getDividerBounds().left, - mSplitLayout.getDividerBounds().top)); - } - }; - - AppPair(AppPairsController controller) { - mController = controller; - mSyncQueue = controller.getSyncTransactionQueue(); - mDisplayController = controller.getDisplayController(); - mDisplayImeController = controller.getDisplayImeController(); - mDisplayInsetsController = controller.getDisplayInsetsController(); - } - - int getRootTaskId() { - return mRootTaskInfo != null ? mRootTaskInfo.taskId : INVALID_TASK_ID; - } - - private int getTaskId1() { - return mTaskInfo1 != null ? mTaskInfo1.taskId : INVALID_TASK_ID; - } - - private int getTaskId2() { - return mTaskInfo2 != null ? mTaskInfo2.taskId : INVALID_TASK_ID; - } - - boolean contains(int taskId) { - return taskId == getRootTaskId() || taskId == getTaskId1() || taskId == getTaskId2(); - } - - boolean pair(ActivityManager.RunningTaskInfo task1, ActivityManager.RunningTaskInfo task2) { - ProtoLog.v(WM_SHELL_TASK_ORG, "pair task1=%d task2=%d in AppPair=%s", - task1.taskId, task2.taskId, this); - - if (!task1.supportsMultiWindow || !task2.supportsMultiWindow) { - ProtoLog.e(WM_SHELL_TASK_ORG, - "Can't pair tasks that doesn't support multi window, " - + "task1.supportsMultiWindow=%b, task2.supportsMultiWindow=%b", - task1.supportsMultiWindow, task2.supportsMultiWindow); - return false; - } - - mTaskInfo1 = task1; - mTaskInfo2 = task2; - mSplitLayout = new SplitLayout(TAG + "SplitDivider", - mDisplayController.getDisplayContext(mRootTaskInfo.displayId), - mRootTaskInfo.configuration, this /* layoutChangeListener */, - mParentContainerCallbacks, mDisplayImeController, mController.getTaskOrganizer(), - SplitLayout.PARALLAX_DISMISSING); - mDisplayInsetsController.addInsetsChangedListener(mRootTaskInfo.displayId, mSplitLayout); - - final WindowContainerToken token1 = task1.token; - final WindowContainerToken token2 = task2.token; - final WindowContainerTransaction wct = new WindowContainerTransaction(); - - wct.setHidden(mRootTaskInfo.token, false) - .reparent(token1, mRootTaskInfo.token, true /* onTop */) - .reparent(token2, mRootTaskInfo.token, true /* onTop */) - .setWindowingMode(token1, WINDOWING_MODE_MULTI_WINDOW) - .setWindowingMode(token2, WINDOWING_MODE_MULTI_WINDOW) - .setBounds(token1, mSplitLayout.getBounds1()) - .setBounds(token2, mSplitLayout.getBounds2()) - // Moving the root task to top after the child tasks were repareted , or the root - // task cannot be visible and focused. - .reorder(mRootTaskInfo.token, true); - mController.getTaskOrganizer().applyTransaction(wct); - return true; - } - - void unpair() { - unpair(null /* toTopToken */); - } - - private void unpair(@Nullable WindowContainerToken toTopToken) { - final WindowContainerToken token1 = mTaskInfo1.token; - final WindowContainerToken token2 = mTaskInfo2.token; - final WindowContainerTransaction wct = new WindowContainerTransaction(); - - // Reparent out of this container and reset windowing mode. - wct.setHidden(mRootTaskInfo.token, true) - .reorder(mRootTaskInfo.token, false) - .reparent(token1, null, token1 == toTopToken /* onTop */) - .reparent(token2, null, token2 == toTopToken /* onTop */) - .setWindowingMode(token1, WINDOWING_MODE_UNDEFINED) - .setWindowingMode(token2, WINDOWING_MODE_UNDEFINED); - mController.getTaskOrganizer().applyTransaction(wct); - - mTaskInfo1 = null; - mTaskInfo2 = null; - mSplitLayout.release(); - mSplitLayout = null; - } - - @Override - public void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash) { - if (mRootTaskInfo == null || taskInfo.taskId == mRootTaskInfo.taskId) { - mRootTaskInfo = taskInfo; - mRootTaskLeash = leash; - } else if (taskInfo.taskId == getTaskId1()) { - mTaskInfo1 = taskInfo; - mTaskLeash1 = leash; - mSyncQueue.runInSync(t -> mDimLayer1 = - SurfaceUtils.makeDimLayer(t, mTaskLeash1, "Dim layer", mSurfaceSession)); - } else if (taskInfo.taskId == getTaskId2()) { - mTaskInfo2 = taskInfo; - mTaskLeash2 = leash; - mSyncQueue.runInSync(t -> mDimLayer2 = - SurfaceUtils.makeDimLayer(t, mTaskLeash2, "Dim layer", mSurfaceSession)); - } else { - throw new IllegalStateException("Unknown task=" + taskInfo.taskId); - } - - if (mTaskLeash1 == null || mTaskLeash2 == null) return; - - mSplitLayout.init(); - - mSyncQueue.runInSync(t -> t - .show(mRootTaskLeash) - .show(mTaskLeash1) - .show(mTaskLeash2) - .setPosition(mTaskLeash1, - mTaskInfo1.positionInParent.x, - mTaskInfo1.positionInParent.y) - .setPosition(mTaskLeash2, - mTaskInfo2.positionInParent.x, - mTaskInfo2.positionInParent.y)); - } - - @Override - public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) { - if (!taskInfo.supportsMultiWindow) { - // Dismiss AppPair if the task no longer supports multi window. - mController.unpair(mRootTaskInfo.taskId); - return; - } - if (taskInfo.taskId == getRootTaskId()) { - if (mRootTaskInfo.isVisible != taskInfo.isVisible) { - mSyncQueue.runInSync(t -> { - if (taskInfo.isVisible) { - t.show(mRootTaskLeash); - } else { - t.hide(mRootTaskLeash); - } - }); - } - mRootTaskInfo = taskInfo; - - if (mSplitLayout != null - && mSplitLayout.updateConfiguration(mRootTaskInfo.configuration)) { - onLayoutSizeChanged(mSplitLayout); - } - } else if (taskInfo.taskId == getTaskId1()) { - mTaskInfo1 = taskInfo; - } else if (taskInfo.taskId == getTaskId2()) { - mTaskInfo2 = taskInfo; - } else { - throw new IllegalStateException("Unknown task=" + taskInfo.taskId); - } - } - - @Override - public int getSplitItemPosition(WindowContainerToken token) { - if (token == null) { - return SPLIT_POSITION_UNDEFINED; - } - - if (token.equals(mTaskInfo1.getToken())) { - return SPLIT_POSITION_TOP_OR_LEFT; - } else if (token.equals(mTaskInfo2.getToken())) { - return SPLIT_POSITION_BOTTOM_OR_RIGHT; - } - - return SPLIT_POSITION_UNDEFINED; - } - - @Override - public void onTaskVanished(ActivityManager.RunningTaskInfo taskInfo) { - if (taskInfo.taskId == getRootTaskId()) { - // We don't want to release this object back to the pool since the root task went away. - mController.unpair(mRootTaskInfo.taskId, false /* releaseToPool */); - } else if (taskInfo.taskId == getTaskId1()) { - mController.unpair(mRootTaskInfo.taskId); - mSyncQueue.runInSync(t -> t.remove(mDimLayer1)); - } else if (taskInfo.taskId == getTaskId2()) { - mController.unpair(mRootTaskInfo.taskId); - mSyncQueue.runInSync(t -> t.remove(mDimLayer2)); - } - } - - @Override - public void attachChildSurfaceToTask(int taskId, SurfaceControl.Builder b) { - b.setParent(findTaskSurface(taskId)); - } - - @Override - public void reparentChildSurfaceToTask(int taskId, SurfaceControl sc, - SurfaceControl.Transaction t) { - t.reparent(sc, findTaskSurface(taskId)); - } - - private SurfaceControl findTaskSurface(int taskId) { - if (getRootTaskId() == taskId) { - return mRootTaskLeash; - } else if (getTaskId1() == taskId) { - return mTaskLeash1; - } else if (getTaskId2() == taskId) { - return mTaskLeash2; - } else { - throw new IllegalArgumentException("There is no surface for taskId=" + taskId); - } - } - - @Override - public void dump(@NonNull PrintWriter pw, String prefix) { - final String innerPrefix = prefix + " "; - final String childPrefix = innerPrefix + " "; - pw.println(prefix + this); - if (mRootTaskInfo != null) { - pw.println(innerPrefix + "Root taskId=" + mRootTaskInfo.taskId - + " winMode=" + mRootTaskInfo.getWindowingMode()); - } - if (mTaskInfo1 != null) { - pw.println(innerPrefix + "1 taskId=" + mTaskInfo1.taskId - + " winMode=" + mTaskInfo1.getWindowingMode()); - } - if (mTaskInfo2 != null) { - pw.println(innerPrefix + "2 taskId=" + mTaskInfo2.taskId - + " winMode=" + mTaskInfo2.getWindowingMode()); - } - } - - @Override - public String toString() { - return TAG + "#" + getRootTaskId(); - } - - @Override - public void onSnappedToDismiss(boolean snappedToEnd) { - unpair(snappedToEnd ? mTaskInfo1.token : mTaskInfo2.token /* toTopToken */); - } - - @Override - public void onLayoutPositionChanging(SplitLayout layout) { - mSyncQueue.runInSync(t -> - layout.applySurfaceChanges(t, mTaskLeash1, mTaskLeash2, mDimLayer1, mDimLayer2, - true /* applyResizingOffset */)); - } - - @Override - public void onLayoutSizeChanging(SplitLayout layout) { - mSyncQueue.runInSync(t -> - layout.applySurfaceChanges(t, mTaskLeash1, mTaskLeash2, mDimLayer1, mDimLayer2, - true /* applyResizingOffset */)); - } - - @Override - public void onLayoutSizeChanged(SplitLayout layout) { - final WindowContainerTransaction wct = new WindowContainerTransaction(); - layout.applyTaskChanges(wct, mTaskInfo1, mTaskInfo2); - mSyncQueue.queue(wct); - mSyncQueue.runInSync(t -> - layout.applySurfaceChanges(t, mTaskLeash1, mTaskLeash2, mDimLayer1, mDimLayer2, - false /* applyResizingOffset */)); - } - - @Override - public void setLayoutOffsetTarget(int offsetX, int offsetY, SplitLayout layout) { - final WindowContainerTransaction wct = new WindowContainerTransaction(); - layout.applyLayoutOffsetTarget(wct, offsetX, offsetY, mTaskInfo1, mTaskInfo2); - mController.getTaskOrganizer().applyTransaction(wct); - } -} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPairs.java b/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPairs.java deleted file mode 100644 index a9b1dbc3c23b..000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPairs.java +++ /dev/null @@ -1,38 +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.apppairs; - -import android.app.ActivityManager; - -import androidx.annotation.NonNull; - -import com.android.wm.shell.common.annotations.ExternalThread; - -import java.io.PrintWriter; - -/** - * Interface to engage app pairs feature. - */ -@ExternalThread -public interface AppPairs { - /** Pairs indicated tasks. */ - boolean pair(int task1, int task2); - /** Pairs indicated tasks. */ - boolean pair(ActivityManager.RunningTaskInfo task1, ActivityManager.RunningTaskInfo task2); - /** Unpairs any app-pair containing this task id. */ - void unpair(int taskId); -} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPairsController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPairsController.java deleted file mode 100644 index 53234ab971d6..000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPairsController.java +++ /dev/null @@ -1,213 +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.apppairs; - -import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_TASK_ORG; - -import android.app.ActivityManager; -import android.util.Slog; -import android.util.SparseArray; - -import androidx.annotation.NonNull; - -import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.protolog.common.ProtoLog; -import com.android.wm.shell.ShellTaskOrganizer; -import com.android.wm.shell.common.DisplayController; -import com.android.wm.shell.common.DisplayImeController; -import com.android.wm.shell.common.DisplayInsetsController; -import com.android.wm.shell.common.ShellExecutor; -import com.android.wm.shell.common.SyncTransactionQueue; - -import java.io.PrintWriter; - -/** - * Class manages app-pairs multitasking mode and implements the main interface {@link AppPairs}. - */ -public class AppPairsController { - private static final String TAG = AppPairsController.class.getSimpleName(); - - private final ShellTaskOrganizer mTaskOrganizer; - private final SyncTransactionQueue mSyncQueue; - private final ShellExecutor mMainExecutor; - private final AppPairsImpl mImpl = new AppPairsImpl(); - - private AppPairsPool mPairsPool; - // Active app-pairs mapped by root task id key. - private final SparseArray<AppPair> mActiveAppPairs = new SparseArray<>(); - private final DisplayController mDisplayController; - private final DisplayImeController mDisplayImeController; - private final DisplayInsetsController mDisplayInsetsController; - - public AppPairsController(ShellTaskOrganizer organizer, SyncTransactionQueue syncQueue, - DisplayController displayController, ShellExecutor mainExecutor, - DisplayImeController displayImeController, - DisplayInsetsController displayInsetsController) { - mTaskOrganizer = organizer; - mSyncQueue = syncQueue; - mDisplayController = displayController; - mDisplayImeController = displayImeController; - mDisplayInsetsController = displayInsetsController; - mMainExecutor = mainExecutor; - } - - public AppPairs asAppPairs() { - return mImpl; - } - - public void onOrganizerRegistered() { - if (mPairsPool == null) { - setPairsPool(new AppPairsPool(this)); - } - } - - @VisibleForTesting - public void setPairsPool(AppPairsPool pool) { - mPairsPool = pool; - } - - public boolean pair(int taskId1, int taskId2) { - final ActivityManager.RunningTaskInfo task1 = mTaskOrganizer.getRunningTaskInfo(taskId1); - final ActivityManager.RunningTaskInfo task2 = mTaskOrganizer.getRunningTaskInfo(taskId2); - if (task1 == null || task2 == null) { - return false; - } - return pair(task1, task2); - } - - public boolean pair(ActivityManager.RunningTaskInfo task1, - ActivityManager.RunningTaskInfo task2) { - return pairInner(task1, task2) != null; - } - - @VisibleForTesting - public AppPair pairInner( - @NonNull ActivityManager.RunningTaskInfo task1, - @NonNull ActivityManager.RunningTaskInfo task2) { - final AppPair pair = mPairsPool.acquire(); - if (!pair.pair(task1, task2)) { - mPairsPool.release(pair); - return null; - } - - mActiveAppPairs.put(pair.getRootTaskId(), pair); - return pair; - } - - public void unpair(int taskId) { - unpair(taskId, true /* releaseToPool */); - } - - public void unpair(int taskId, boolean releaseToPool) { - AppPair pair = mActiveAppPairs.get(taskId); - if (pair == null) { - for (int i = mActiveAppPairs.size() - 1; i >= 0; --i) { - final AppPair candidate = mActiveAppPairs.valueAt(i); - if (candidate.contains(taskId)) { - pair = candidate; - break; - } - } - } - if (pair == null) { - ProtoLog.v(WM_SHELL_TASK_ORG, "taskId %d isn't isn't in an app-pair.", taskId); - return; - } - - ProtoLog.v(WM_SHELL_TASK_ORG, "unpair taskId=%d pair=%s", taskId, pair); - mActiveAppPairs.remove(pair.getRootTaskId()); - pair.unpair(); - if (releaseToPool) { - mPairsPool.release(pair); - } - } - - ShellTaskOrganizer getTaskOrganizer() { - return mTaskOrganizer; - } - - SyncTransactionQueue getSyncTransactionQueue() { - return mSyncQueue; - } - - DisplayController getDisplayController() { - return mDisplayController; - } - - DisplayImeController getDisplayImeController() { - return mDisplayImeController; - } - - DisplayInsetsController getDisplayInsetsController() { - return mDisplayInsetsController; - } - - public void dump(@NonNull PrintWriter pw, String prefix) { - final String innerPrefix = prefix + " "; - final String childPrefix = innerPrefix + " "; - pw.println(prefix + this); - - for (int i = mActiveAppPairs.size() - 1; i >= 0; --i) { - mActiveAppPairs.valueAt(i).dump(pw, childPrefix); - } - - if (mPairsPool != null) { - mPairsPool.dump(pw, prefix); - } - } - - @Override - public String toString() { - return TAG + "#" + mActiveAppPairs.size(); - } - - private class AppPairsImpl implements AppPairs { - @Override - public boolean pair(int task1, int task2) { - boolean[] result = new boolean[1]; - try { - mMainExecutor.executeBlocking(() -> { - result[0] = AppPairsController.this.pair(task1, task2); - }); - } catch (InterruptedException e) { - Slog.e(TAG, "Failed to pair tasks: " + task1 + ", " + task2); - } - return result[0]; - } - - @Override - public boolean pair(ActivityManager.RunningTaskInfo task1, - ActivityManager.RunningTaskInfo task2) { - boolean[] result = new boolean[1]; - try { - mMainExecutor.executeBlocking(() -> { - result[0] = AppPairsController.this.pair(task1, task2); - }); - } catch (InterruptedException e) { - Slog.e(TAG, "Failed to pair tasks: " + task1 + ", " + task2); - } - return result[0]; - } - - @Override - public void unpair(int taskId) { - mMainExecutor.execute(() -> { - AppPairsController.this.unpair(taskId); - }); - } - } -} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPairsPool.java b/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPairsPool.java deleted file mode 100644 index 5c6037ea0702..000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPairsPool.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.apppairs; - -import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; -import static android.view.Display.DEFAULT_DISPLAY; - -import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_TASK_ORG; - -import androidx.annotation.NonNull; - -import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.protolog.common.ProtoLog; - -import java.io.PrintWriter; -import java.util.ArrayList; - -/** - * Class that manager pool of {@link AppPair} objects. Helps reduce the need to call system_server - * to create a root task for the app-pair when needed since we always have one ready to go. - */ -class AppPairsPool { - private static final String TAG = AppPairsPool.class.getSimpleName(); - - @VisibleForTesting - final AppPairsController mController; - // The pool - private final ArrayList<AppPair> mPool = new ArrayList(); - - AppPairsPool(AppPairsController controller) { - mController = controller; - incrementPool(); - } - - AppPair acquire() { - final AppPair entry = mPool.remove(mPool.size() - 1); - ProtoLog.v(WM_SHELL_TASK_ORG, "acquire entry.taskId=%s listener=%s size=%d", - entry.getRootTaskId(), entry, mPool.size()); - if (mPool.size() == 0) { - incrementPool(); - } - return entry; - } - - void release(AppPair entry) { - mPool.add(entry); - ProtoLog.v(WM_SHELL_TASK_ORG, "release entry.taskId=%s listener=%s size=%d", - entry.getRootTaskId(), entry, mPool.size()); - } - - @VisibleForTesting - void incrementPool() { - ProtoLog.v(WM_SHELL_TASK_ORG, "incrementPool size=%d", mPool.size()); - final AppPair entry = new AppPair(mController); - // TODO: multi-display... - mController.getTaskOrganizer().createRootTask( - DEFAULT_DISPLAY, WINDOWING_MODE_FULLSCREEN, entry); - mPool.add(entry); - } - - @VisibleForTesting - int poolSize() { - return mPool.size(); - } - - public void dump(@NonNull PrintWriter pw, String prefix) { - final String innerPrefix = prefix + " "; - final String childPrefix = innerPrefix + " "; - pw.println(prefix + this); - for (int i = mPool.size() - 1; i >= 0; --i) { - mPool.get(i).dump(pw, childPrefix); - } - } - - @Override - public String toString() { - return TAG + "#" + mPool.size(); - } -} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/OWNERS b/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/OWNERS deleted file mode 100644 index 4d9b520e3f0e..000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/OWNERS +++ /dev/null @@ -1,2 +0,0 @@ -# WM shell sub-modules apppair owner -chenghsiuchang@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 8c0affb0a432..86f9d5b534f4 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 @@ -29,6 +29,13 @@ import com.android.wm.shell.common.annotations.ExternalThread; public interface BackAnimation { /** + * Returns a binder that can be passed to an external process to update back animations. + */ + default IBackAnimation createExternalInterface() { + return null; + } + + /** * Called when a {@link MotionEvent} is generated by a back gesture. * * @param touchX the X touch position of the {@link MotionEvent}. @@ -47,13 +54,6 @@ public interface BackAnimation { void setTriggerBack(boolean triggerBack); /** - * Returns a binder that can be passed to an external process to update back animations. - */ - default IBackAnimation createExternalInterface() { - return null; - } - - /** * Sets the threshold values that defining edge swipe behavior. * @param triggerThreshold the min threshold to trigger back. * @param progressThreshold the max threshold to keep progressing back animation. 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 0cf2b28921e1..33ecdd88fad3 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,6 +16,9 @@ package com.android.wm.shell.back; +import static android.view.RemoteAnimationTarget.MODE_CLOSING; +import static android.view.RemoteAnimationTarget.MODE_OPENING; + import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission; import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BACK_PREVIEW; @@ -27,21 +30,29 @@ import android.app.WindowConfiguration; import android.content.ContentResolver; import android.content.Context; import android.database.ContentObserver; -import android.graphics.Point; -import android.graphics.PointF; import android.hardware.HardwareBuffer; +import android.hardware.input.InputManager; import android.net.Uri; import android.os.Handler; +import android.os.IBinder; import android.os.RemoteException; +import android.os.SystemClock; import android.os.SystemProperties; import android.os.UserHandle; import android.provider.Settings.Global; import android.util.Log; +import android.view.IWindowFocusObserver; +import android.view.InputDevice; +import android.view.KeyCharacterMap; +import android.view.KeyEvent; import android.view.MotionEvent; import android.view.RemoteAnimationTarget; import android.view.SurfaceControl; +import android.window.BackAnimationAdaptor; import android.window.BackEvent; import android.window.BackNavigationInfo; +import android.window.IBackAnimationRunner; +import android.window.IBackNaviAnimationController; import android.window.IOnBackInvokedCallback; import com.android.internal.annotations.VisibleForTesting; @@ -50,6 +61,7 @@ import com.android.wm.shell.common.RemoteCallable; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.annotations.ShellBackgroundThread; import com.android.wm.shell.common.annotations.ShellMainThread; +import com.android.wm.shell.sysui.ShellInit; import java.util.concurrent.atomic.AtomicBoolean; @@ -68,28 +80,22 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont private static final int PROGRESS_THRESHOLD = SystemProperties .getInt(PREDICTIVE_BACK_PROGRESS_THRESHOLD_PROP, -1); private final AtomicBoolean mEnableAnimations = new AtomicBoolean(false); + // TODO (b/241808055) Find a appropriate time to remove during refactor + private static final boolean USE_TRANSITION = + SystemProperties.getInt("persist.wm.debug.predictive_back_ani_trans", 1) != 0; /** * Max duration to wait for a transition to finish before accepting another gesture start * request. */ private static final long MAX_TRANSITION_DURATION = 2000; - /** - * Location of the initial touch event of the back gesture. - */ - private final PointF mInitTouchLocation = new PointF(); - - /** - * Raw delta between {@link #mInitTouchLocation} and the last touch location. - */ - private final Point mTouchEventDelta = new Point(); - private final ShellExecutor mShellExecutor; - /** True when a back gesture is ongoing */ private boolean mBackGestureStarted = false; /** Tracks if an uninterruptible transition is in progress */ private boolean mTransitionInProgress = false; + /** Tracks if we should start the back gesture on the next motion move event */ + private boolean mShouldStartOnNextMoveEvent = false; /** @see #setTriggerBack(boolean) */ private boolean mTriggerBack; @@ -98,6 +104,9 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont private final SurfaceControl.Transaction mTransaction; private final IActivityTaskManager mActivityTaskManager; private final Context mContext; + private final ContentResolver mContentResolver; + private final ShellExecutor mShellExecutor; + private final Handler mBgHandler; @Nullable private IOnBackInvokedCallback mBackToLauncherCallback; private float mTriggerThreshold; @@ -107,17 +116,133 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont mTransitionInProgress = false; }; + private RemoteAnimationTarget mAnimationTarget; + IBackAnimationRunner mIBackAnimationRunner; + private IBackNaviAnimationController mBackAnimationController; + private BackAnimationAdaptor mBackAnimationAdaptor; + + private boolean mWaitingAnimationStart; + private final TouchTracker mTouchTracker = new TouchTracker(); + private final CachingBackDispatcher mCachingBackDispatcher = new CachingBackDispatcher(); + + @VisibleForTesting + final IWindowFocusObserver mFocusObserver = new IWindowFocusObserver.Stub() { + @Override + public void focusGained(IBinder inputToken) { } + @Override + public void focusLost(IBinder inputToken) { + mShellExecutor.execute(() -> { + if (!mBackGestureStarted || mTransitionInProgress) { + // If an uninterruptible transition is already in progress, we should ignore + // this due to the transition may cause focus lost. (alpha = 0) + return; + } + setTriggerBack(false); + onGestureFinished(false); + }); + } + }; + + /** + * Helper class to record the touch location for gesture start and latest. + */ + private static class TouchTracker { + /** + * Location of the latest touch event + */ + private float mLatestTouchX; + private float mLatestTouchY; + private int mSwipeEdge; + + /** + * Location of the initial touch event of the back gesture. + */ + private float mInitTouchX; + private float mInitTouchY; + + void update(float touchX, float touchY, int swipeEdge) { + mLatestTouchX = touchX; + mLatestTouchY = touchY; + mSwipeEdge = swipeEdge; + } + + void setGestureStartLocation(float touchX, float touchY) { + mInitTouchX = touchX; + mInitTouchY = touchY; + } + + int getDeltaFromGestureStart(float touchX) { + return Math.round(touchX - mInitTouchX); + } + + void reset() { + mInitTouchX = 0; + mInitTouchY = 0; + } + } + + /** + * Cache the temporary callback and trigger result if gesture was finish before received + * BackAnimationRunner#onAnimationStart/cancel, so there can continue play the animation. + */ + private class CachingBackDispatcher { + private IOnBackInvokedCallback mOnBackCallback; + private boolean mTriggerBack; + // Whether we are waiting to receive onAnimationStart + private boolean mWaitingAnimation; + + void startWaitingAnimation() { + mWaitingAnimation = true; + } + + boolean set(IOnBackInvokedCallback callback, boolean triggerBack) { + if (mWaitingAnimation) { + mOnBackCallback = callback; + mTriggerBack = triggerBack; + return true; + } + return false; + } + + boolean consume() { + boolean consumed = false; + if (mWaitingAnimation && mOnBackCallback != null) { + if (mTriggerBack) { + final BackEvent backFinish = new BackEvent( + mTouchTracker.mLatestTouchX, mTouchTracker.mLatestTouchY, 1, + mTouchTracker.mSwipeEdge, mAnimationTarget); + dispatchOnBackProgressed(mBackToLauncherCallback, backFinish); + dispatchOnBackInvoked(mOnBackCallback); + } else { + final BackEvent backFinish = new BackEvent( + mTouchTracker.mLatestTouchX, mTouchTracker.mLatestTouchY, 0, + mTouchTracker.mSwipeEdge, mAnimationTarget); + dispatchOnBackProgressed(mBackToLauncherCallback, backFinish); + dispatchOnBackCancelled(mOnBackCallback); + } + startTransition(); + consumed = true; + } + mOnBackCallback = null; + mWaitingAnimation = false; + return consumed; + } + } + public BackAnimationController( + @NonNull ShellInit shellInit, @NonNull @ShellMainThread ShellExecutor shellExecutor, @NonNull @ShellBackgroundThread Handler backgroundHandler, Context context) { - this(shellExecutor, backgroundHandler, new SurfaceControl.Transaction(), + this(shellInit, shellExecutor, backgroundHandler, new SurfaceControl.Transaction(), ActivityTaskManager.getService(), context, context.getContentResolver()); } @VisibleForTesting - BackAnimationController(@NonNull @ShellMainThread ShellExecutor shellExecutor, - @NonNull @ShellBackgroundThread Handler handler, + BackAnimationController( + @NonNull ShellInit shellInit, + @NonNull @ShellMainThread ShellExecutor shellExecutor, + @NonNull @ShellBackgroundThread Handler bgHandler, @NonNull SurfaceControl.Transaction transaction, @NonNull IActivityTaskManager activityTaskManager, Context context, ContentResolver contentResolver) { @@ -125,7 +250,13 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont mTransaction = transaction; mActivityTaskManager = activityTaskManager; mContext = context; - setupAnimationDeveloperSettingsObserver(contentResolver, handler); + mContentResolver = contentResolver; + mBgHandler = bgHandler; + shellInit.addInitCallback(this::onInit, this); + } + + private void onInit() { + setupAnimationDeveloperSettingsObserver(mContentResolver, mBgHandler); } private void setupAnimationDeveloperSettingsObserver( @@ -233,6 +364,9 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont @VisibleForTesting void setBackToLauncherCallback(IOnBackInvokedCallback callback) { mBackToLauncherCallback = callback; + if (USE_TRANSITION) { + createAdaptor(); + } } private void clearBackToLauncherCallback() { @@ -241,15 +375,18 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont @VisibleForTesting void onBackToLauncherAnimationFinished() { - if (mBackNavigationInfo != null) { - IOnBackInvokedCallback callback = mBackNavigationInfo.getOnBackInvokedCallback(); - if (mTriggerBack) { + final boolean triggerBack = mTriggerBack; + IOnBackInvokedCallback callback = mBackNavigationInfo != null + ? mBackNavigationInfo.getOnBackInvokedCallback() : null; + // Make sure the notification sequence should be controller > client. + finishAnimation(); + if (callback != null) { + if (triggerBack) { dispatchOnBackInvoked(callback); } else { dispatchOnBackCancelled(callback); } } - finishAnimation(); } /** @@ -261,34 +398,45 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont if (mTransitionInProgress) { return; } - if (keyAction == MotionEvent.ACTION_MOVE) { + + mTouchTracker.update(touchX, touchY, swipeEdge); + if (keyAction == MotionEvent.ACTION_DOWN) { if (!mBackGestureStarted) { + mShouldStartOnNextMoveEvent = true; + } + } else if (keyAction == MotionEvent.ACTION_MOVE) { + if (!mBackGestureStarted && mShouldStartOnNextMoveEvent) { // Let the animation initialized here to make sure the onPointerDownOutsideFocus // could be happened when ACTION_DOWN, it may change the current focus that we // would access it when startBackNavigation. - initAnimation(touchX, touchY); + onGestureStarted(touchX, touchY); + mShouldStartOnNextMoveEvent = false; } onMove(touchX, touchY, swipeEdge); } else if (keyAction == MotionEvent.ACTION_UP || keyAction == MotionEvent.ACTION_CANCEL) { ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Finishing gesture with event action: %d", keyAction); - onGestureFinished(); + if (keyAction == MotionEvent.ACTION_CANCEL) { + mTriggerBack = false; + } + onGestureFinished(true); } } - private void initAnimation(float touchX, float touchY) { + private void onGestureStarted(float touchX, float touchY) { ProtoLog.d(WM_SHELL_BACK_PREVIEW, "initAnimation mMotionStarted=%b", mBackGestureStarted); if (mBackGestureStarted || mBackNavigationInfo != null) { Log.e(TAG, "Animation is being initialized but is already started."); finishAnimation(); } - mInitTouchLocation.set(touchX, touchY); + mTouchTracker.setGestureStartLocation(touchX, touchY); mBackGestureStarted = true; try { boolean requestAnimation = mEnableAnimations.get(); - mBackNavigationInfo = mActivityTaskManager.startBackNavigation(requestAnimation); + mBackNavigationInfo = mActivityTaskManager.startBackNavigation(requestAnimation, + mFocusObserver, mBackAnimationAdaptor); onBackNavigationInfoReceived(mBackNavigationInfo); } catch (RemoteException remoteException) { Log.e(TAG, "Failed to initAnimation", remoteException); @@ -300,11 +448,11 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Received backNavigationInfo:%s", backNavigationInfo); if (backNavigationInfo == null) { Log.e(TAG, "Received BackNavigationInfo is null."); - finishAnimation(); return; } int backType = backNavigationInfo.getType(); IOnBackInvokedCallback targetCallback = null; + final boolean dispatchToLauncher = shouldDispatchToLauncher(backType); if (backType == BackNavigationInfo.TYPE_CROSS_ACTIVITY) { HardwareBuffer hardwareBuffer = backNavigationInfo.getScreenshotHardwareBuffer(); if (hardwareBuffer != null) { @@ -312,12 +460,17 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont backNavigationInfo.getTaskWindowConfiguration()); } mTransaction.apply(); - } else if (shouldDispatchToLauncher(backType)) { + } else if (dispatchToLauncher) { targetCallback = mBackToLauncherCallback; + if (USE_TRANSITION) { + mCachingBackDispatcher.startWaitingAnimation(); + } } else if (backType == BackNavigationInfo.TYPE_CALLBACK) { targetCallback = mBackNavigationInfo.getOnBackInvokedCallback(); } - dispatchOnBackStarted(targetCallback); + if (!USE_TRANSITION || !dispatchToLauncher) { + dispatchOnBackStarted(targetCallback); + } } /** @@ -356,36 +509,89 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont if (!mBackGestureStarted || mBackNavigationInfo == null) { return; } - int deltaX = Math.round(touchX - mInitTouchLocation.x); + int deltaX = mTouchTracker.getDeltaFromGestureStart(touchX); float progressThreshold = PROGRESS_THRESHOLD >= 0 ? PROGRESS_THRESHOLD : mProgressThreshold; float progress = Math.min(Math.max(Math.abs(deltaX) / progressThreshold, 0), 1); - int backType = mBackNavigationInfo.getType(); - RemoteAnimationTarget animationTarget = mBackNavigationInfo.getDepartingAnimationTarget(); + if (USE_TRANSITION) { + if (mBackAnimationController != null && mAnimationTarget != null) { + final BackEvent backEvent = new BackEvent( + touchX, touchY, progress, swipeEdge, mAnimationTarget); + dispatchOnBackProgressed(mBackToLauncherCallback, backEvent); + } + } else { + int backType = mBackNavigationInfo.getType(); + RemoteAnimationTarget animationTarget = + mBackNavigationInfo.getDepartingAnimationTarget(); + + BackEvent backEvent = new BackEvent( + touchX, touchY, progress, swipeEdge, animationTarget); + IOnBackInvokedCallback targetCallback = null; + if (shouldDispatchToLauncher(backType)) { + targetCallback = mBackToLauncherCallback; + } else if (backType == BackNavigationInfo.TYPE_CROSS_TASK + || backType == BackNavigationInfo.TYPE_CROSS_ACTIVITY) { + // TODO(208427216) Run the actual animation + } else if (backType == BackNavigationInfo.TYPE_CALLBACK) { + targetCallback = mBackNavigationInfo.getOnBackInvokedCallback(); + } + dispatchOnBackProgressed(targetCallback, backEvent); + } + } - BackEvent backEvent = new BackEvent( - touchX, touchY, progress, swipeEdge, animationTarget); - IOnBackInvokedCallback targetCallback = null; - if (shouldDispatchToLauncher(backType)) { - targetCallback = mBackToLauncherCallback; - } else if (backType == BackNavigationInfo.TYPE_CROSS_TASK - || backType == BackNavigationInfo.TYPE_CROSS_ACTIVITY) { - // TODO(208427216) Run the actual animation - } else if (backType == BackNavigationInfo.TYPE_CALLBACK) { - targetCallback = mBackNavigationInfo.getOnBackInvokedCallback(); + private void injectBackKey() { + sendBackEvent(KeyEvent.ACTION_DOWN); + sendBackEvent(KeyEvent.ACTION_UP); + } + + private void sendBackEvent(int action) { + final long when = SystemClock.uptimeMillis(); + final KeyEvent ev = new KeyEvent(when, when, action, KeyEvent.KEYCODE_BACK, 0 /* repeat */, + 0 /* metaState */, KeyCharacterMap.VIRTUAL_KEYBOARD, 0 /* scancode */, + KeyEvent.FLAG_FROM_SYSTEM | KeyEvent.FLAG_VIRTUAL_HARD_KEY, + InputDevice.SOURCE_KEYBOARD); + + ev.setDisplayId(mContext.getDisplay().getDisplayId()); + if (!InputManager.getInstance() + .injectInputEvent(ev, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC)) { + Log.e(TAG, "Inject input event fail"); } - dispatchOnBackProgressed(targetCallback, backEvent); } - private void onGestureFinished() { + private void onGestureFinished(boolean fromTouch) { ProtoLog.d(WM_SHELL_BACK_PREVIEW, "onGestureFinished() mTriggerBack == %s", mTriggerBack); - if (!mBackGestureStarted || mBackNavigationInfo == null) { + if (!mBackGestureStarted) { + finishAnimation(); return; } + + if (fromTouch) { + // Let touch reset the flag otherwise it will start a new back navigation and refresh + // the info when received a new move event. + mBackGestureStarted = false; + } + + if (mTransitionInProgress) { + return; + } + + if (mBackNavigationInfo == null) { + // No focus window found or core are running recents animation, inject back key as + // legacy behavior. + if (mTriggerBack) { + injectBackKey(); + } + finishAnimation(); + return; + } + int backType = mBackNavigationInfo.getType(); boolean shouldDispatchToLauncher = shouldDispatchToLauncher(backType); IOnBackInvokedCallback targetCallback = shouldDispatchToLauncher ? mBackToLauncherCallback : mBackNavigationInfo.getOnBackInvokedCallback(); + if (mCachingBackDispatcher.set(targetCallback, mTriggerBack)) { + return; + } if (shouldDispatchToLauncher) { startTransition(); } @@ -403,7 +609,10 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont private boolean shouldDispatchToLauncher(int backType) { return backType == BackNavigationInfo.TYPE_RETURN_TO_HOME && mBackToLauncherCallback != null - && mEnableAnimations.get(); + && mEnableAnimations.get() + && mBackNavigationInfo != null + && ((USE_TRANSITION && mBackNavigationInfo.isPrepareRemoteAnimation()) + || mBackNavigationInfo.getDepartingAnimationTarget() != null); } private static void dispatchOnBackStarted(IOnBackInvokedCallback callback) { @@ -468,29 +677,43 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont private void finishAnimation() { ProtoLog.d(WM_SHELL_BACK_PREVIEW, "BackAnimationController: finishAnimation()"); - mBackGestureStarted = false; - mTouchEventDelta.set(0, 0); - mInitTouchLocation.set(0, 0); + mTouchTracker.reset(); BackNavigationInfo backNavigationInfo = mBackNavigationInfo; boolean triggerBack = mTriggerBack; mBackNavigationInfo = null; mTriggerBack = false; + mShouldStartOnNextMoveEvent = false; if (backNavigationInfo == null) { return; } - RemoteAnimationTarget animationTarget = backNavigationInfo.getDepartingAnimationTarget(); - if (animationTarget != null) { - if (animationTarget.leash != null && animationTarget.leash.isValid()) { - mTransaction.remove(animationTarget.leash); + + if (!USE_TRANSITION) { + RemoteAnimationTarget animationTarget = backNavigationInfo + .getDepartingAnimationTarget(); + if (animationTarget != null) { + if (animationTarget.leash != null && animationTarget.leash.isValid()) { + mTransaction.remove(animationTarget.leash); + } } + SurfaceControl screenshotSurface = backNavigationInfo.getScreenshotSurface(); + if (screenshotSurface != null && screenshotSurface.isValid()) { + mTransaction.remove(screenshotSurface); + } + mTransaction.apply(); } - SurfaceControl screenshotSurface = backNavigationInfo.getScreenshotSurface(); - if (screenshotSurface != null && screenshotSurface.isValid()) { - mTransaction.remove(screenshotSurface); - } - mTransaction.apply(); stopTransition(); backNavigationInfo.onBackNavigationFinished(triggerBack); + if (USE_TRANSITION) { + final IBackNaviAnimationController controller = mBackAnimationController; + if (controller != null) { + try { + controller.finish(triggerBack); + } catch (RemoteException r) { + // Oh no! + } + } + mBackAnimationController = null; + } } private void startTransition() { @@ -508,4 +731,50 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont mShellExecutor.removeCallbacks(mResetTransitionRunnable); mTransitionInProgress = false; } + + private void createAdaptor() { + mIBackAnimationRunner = new IBackAnimationRunner.Stub() { + @Override + public void onAnimationCancelled() { + // no op for now + } + @Override // Binder interface + public void onAnimationStart(IBackNaviAnimationController controller, int type, + RemoteAnimationTarget[] apps, RemoteAnimationTarget[] wallpapers, + RemoteAnimationTarget[] nonApps) { + mShellExecutor.execute(() -> { + mBackAnimationController = controller; + for (int i = 0; i < apps.length; i++) { + final RemoteAnimationTarget target = apps[i]; + if (MODE_CLOSING == target.mode) { + mAnimationTarget = target; + } else if (MODE_OPENING == target.mode) { + // TODO Home activity should handle the visibility for itself + // once it finish relayout for orientation change + SurfaceControl.Transaction tx = + new SurfaceControl.Transaction(); + tx.setAlpha(target.leash, 1); + tx.apply(); + } + } + // TODO animation target should be passed at onBackStarted + dispatchOnBackStarted(mBackToLauncherCallback); + // TODO This is Workaround for LauncherBackAnimationController, there will need + // to dispatch onBackProgressed twice(startBack & updateBackProgress) to + // initialize the animation data, for now that would happen when onMove + // called, but there will no expected animation if the down -> up gesture + // happen very fast which ACTION_MOVE only happen once. + final BackEvent backInit = new BackEvent( + mTouchTracker.mLatestTouchX, mTouchTracker.mLatestTouchY, 0, + mTouchTracker.mSwipeEdge, mAnimationTarget); + dispatchOnBackProgressed(mBackToLauncherCallback, backInit); + if (!mCachingBackDispatcher.consume()) { + dispatchOnBackProgressed(mBackToLauncherCallback, backInit); + } + }); + } + }; + mBackAnimationAdaptor = new BackAnimationAdaptor(mIBackAnimationRunner, + BackNavigationInfo.TYPE_RETURN_TO_HOME); + } } 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 31fc6a5be589..922472a26113 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 @@ -59,6 +59,8 @@ import java.util.concurrent.Executor; public class Bubble implements BubbleViewProvider { private static final String TAG = "Bubble"; + public static final String KEY_APP_BUBBLE = "key_app_bubble"; + private final String mKey; @Nullable private final String mGroupKey; @@ -164,13 +166,22 @@ public class Bubble implements BubbleViewProvider { private PendingIntent mDeleteIntent; /** + * Used only for a special bubble in the stack that has the key {@link #KEY_APP_BUBBLE}. + * There can only be one of these bubbles in the stack and this intent will be populated for + * that bubble. + */ + @Nullable + private Intent mAppIntent; + + /** * Create a bubble with limited information based on given {@link ShortcutInfo}. * Note: Currently this is only being used when the bubble is persisted to disk. */ @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, Executor mainExecutor) { + int taskId, @Nullable final String locus, Executor mainExecutor, + final Bubbles.BubbleMetadataFlagListener listener) { Objects.requireNonNull(key); Objects.requireNonNull(shortcutInfo); mMetadataShortcutId = shortcutInfo.getId(); @@ -188,11 +199,28 @@ public class Bubble implements BubbleViewProvider { mShowBubbleUpdateDot = false; mMainExecutor = mainExecutor; mTaskId = taskId; + mBubbleMetadataFlagListener = listener; + } + + public Bubble(Intent intent, + UserHandle user, + Executor mainExecutor) { + mKey = KEY_APP_BUBBLE; + mGroupKey = null; + mLocusId = null; + mFlags = 0; + mUser = user; + mShowBubbleUpdateDot = false; + mMainExecutor = mainExecutor; + mTaskId = INVALID_TASK_ID; + mAppIntent = intent; + mDesiredHeight = Integer.MAX_VALUE; + mPackageName = intent.getPackage(); } @VisibleForTesting(visibility = PRIVATE) public Bubble(@NonNull final BubbleEntry entry, - @Nullable final Bubbles.BubbleMetadataFlagListener listener, + final Bubbles.BubbleMetadataFlagListener listener, final Bubbles.PendingIntentCanceledListener intentCancelListener, Executor mainExecutor) { mKey = entry.getKey(); @@ -415,6 +443,9 @@ public class Bubble implements BubbleViewProvider { mShortcutInfo = info.shortcutInfo; mAppName = info.appName; + if (mTitle == null) { + mTitle = mAppName; + } mFlyoutMessage = info.flyoutMessage; mBadgeBitmap = info.badgeBitmap; @@ -452,6 +483,7 @@ public class Bubble implements BubbleViewProvider { */ void setEntry(@NonNull final BubbleEntry entry) { Objects.requireNonNull(entry); + boolean showingDotPreviously = showDot(); mLastUpdated = entry.getStatusBarNotification().getPostTime(); mIsBubble = entry.getStatusBarNotification().getNotification().isBubbleNotification(); mPackageName = entry.getStatusBarNotification().getPackageName(); @@ -498,6 +530,10 @@ public class Bubble implements BubbleViewProvider { mShouldSuppressNotificationDot = entry.shouldSuppressNotificationDot(); mShouldSuppressNotificationList = entry.shouldSuppressNotificationList(); mShouldSuppressPeek = entry.shouldSuppressPeek(); + if (showingDotPreviously != showDot()) { + // This will update the UI if needed + setShowDot(showDot()); + } } @Nullable @@ -513,7 +549,7 @@ public class Bubble implements BubbleViewProvider { * @return the last time this bubble was updated or accessed, whichever is most recent. */ long getLastActivity() { - return Math.max(mLastUpdated, mLastAccessed); + return isAppBubble() ? Long.MAX_VALUE : Math.max(mLastUpdated, mLastAccessed); } /** @@ -712,6 +748,15 @@ public class Bubble implements BubbleViewProvider { return mDeleteIntent; } + @Nullable + Intent getAppBubbleIntent() { + return mAppIntent; + } + + boolean isAppBubble() { + return KEY_APP_BUBBLE.equals(mKey); + } + Intent getSettingsIntent(final Context context) { final Intent intent = new Intent(Settings.ACTION_APP_NOTIFICATION_BUBBLE_SETTINGS); intent.putExtra(Settings.EXTRA_APP_PACKAGE, getPackageName()); @@ -816,7 +861,7 @@ public class Bubble implements BubbleViewProvider { /** * Description of current bubble state. */ - public void dump(@NonNull PrintWriter pw, @NonNull String[] args) { + public void dump(@NonNull PrintWriter pw) { pw.print("key: "); pw.println(mKey); pw.print(" showInShade: "); pw.println(showInShade()); pw.print(" showDot: "); pw.println(showDot()); @@ -825,8 +870,9 @@ public class Bubble implements BubbleViewProvider { pw.print(" desiredHeight: "); pw.println(getDesiredHeightString()); pw.print(" suppressNotif: "); pw.println(shouldSuppressNotification()); pw.print(" autoExpand: "); pw.println(shouldAutoExpand()); + pw.print(" bubbleMetadataFlagListener null: " + (mBubbleMetadataFlagListener == null)); if (mExpandedView != null) { - mExpandedView.dump(pw, args); + mExpandedView.dump(pw); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleBadgeIconFactory.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleBadgeIconFactory.java index 4eeb20769e09..d6803e8052c6 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleBadgeIconFactory.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleBadgeIconFactory.java @@ -19,14 +19,14 @@ package com.android.wm.shell.bubbles; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Canvas; -import android.graphics.Paint; +import android.graphics.Color; import android.graphics.Path; +import android.graphics.Rect; import android.graphics.drawable.AdaptiveIconDrawable; import android.graphics.drawable.Drawable; import com.android.launcher3.icons.BaseIconFactory; import com.android.launcher3.icons.BitmapInfo; -import com.android.launcher3.icons.ShadowGenerator; import com.android.wm.shell.R; /** @@ -44,78 +44,77 @@ public class BubbleBadgeIconFactory extends BaseIconFactory { * will include the workprofile indicator on the badge if appropriate. */ BitmapInfo getBadgeBitmap(Drawable userBadgedAppIcon, boolean isImportantConversation) { - ShadowGenerator shadowGenerator = new ShadowGenerator(mIconBitmapSize); - Bitmap userBadgedBitmap = createIconBitmap(userBadgedAppIcon, 1f, mIconBitmapSize); - if (userBadgedAppIcon instanceof AdaptiveIconDrawable) { - userBadgedBitmap = Bitmap.createScaledBitmap( - getCircleBitmap((AdaptiveIconDrawable) userBadgedAppIcon, /* size */ - userBadgedAppIcon.getIntrinsicWidth()), - mIconBitmapSize, mIconBitmapSize, /* filter */ true); + AdaptiveIconDrawable ad = (AdaptiveIconDrawable) userBadgedAppIcon; + userBadgedAppIcon = new CircularAdaptiveIcon(ad.getBackground(), ad.getForeground()); } - if (isImportantConversation) { - final float ringStrokeWidth = mContext.getResources().getDimensionPixelSize( - com.android.internal.R.dimen.importance_ring_stroke_width); - final int importantConversationColor = mContext.getResources().getColor( + userBadgedAppIcon = new CircularRingDrawable(userBadgedAppIcon); + } + Bitmap userBadgedBitmap = createIconBitmap( + userBadgedAppIcon, 1, BITMAP_GENERATION_MODE_WITH_SHADOW); + return createIconBitmap(userBadgedBitmap); + } + + private class CircularRingDrawable extends CircularAdaptiveIcon { + + final int mImportantConversationColor; + final Rect mTempBounds = new Rect(); + + final Drawable mDr; + + CircularRingDrawable(Drawable dr) { + super(null, null); + mDr = dr; + mImportantConversationColor = mContext.getResources().getColor( R.color.important_conversation, null); - Bitmap badgeAndRing = Bitmap.createBitmap(userBadgedBitmap.getWidth(), - userBadgedBitmap.getHeight(), userBadgedBitmap.getConfig()); - Canvas c = new Canvas(badgeAndRing); - - Paint ringPaint = new Paint(); - ringPaint.setStyle(Paint.Style.FILL); - ringPaint.setColor(importantConversationColor); - ringPaint.setAntiAlias(true); - c.drawCircle(c.getWidth() / 2, c.getHeight() / 2, c.getWidth() / 2, ringPaint); - - final int bitmapTop = (int) ringStrokeWidth; - final int bitmapLeft = (int) ringStrokeWidth; - final int bitmapWidth = c.getWidth() - 2 * (int) ringStrokeWidth; - final int bitmapHeight = c.getHeight() - 2 * (int) ringStrokeWidth; - - Bitmap scaledBitmap = Bitmap.createScaledBitmap(userBadgedBitmap, bitmapWidth, - bitmapHeight, /* filter */ true); - c.drawBitmap(scaledBitmap, bitmapTop, bitmapLeft, /* paint */null); - - shadowGenerator.recreateIcon(Bitmap.createBitmap(badgeAndRing), c); - return createIconBitmap(badgeAndRing); - } else { - Canvas c = new Canvas(); - c.setBitmap(userBadgedBitmap); - shadowGenerator.recreateIcon(Bitmap.createBitmap(userBadgedBitmap), c); - return createIconBitmap(userBadgedBitmap); + } + + @Override + public void draw(Canvas canvas) { + int save = canvas.save(); + canvas.clipPath(getIconMask()); + canvas.drawColor(mImportantConversationColor); + int ringStrokeWidth = mContext.getResources().getDimensionPixelSize( + com.android.internal.R.dimen.importance_ring_stroke_width); + mTempBounds.set(getBounds()); + mTempBounds.inset(ringStrokeWidth, ringStrokeWidth); + mDr.setBounds(mTempBounds); + mDr.draw(canvas); + canvas.restoreToCount(save); } } - private Bitmap getCircleBitmap(AdaptiveIconDrawable icon, int size) { - Drawable foreground = icon.getForeground(); - Drawable background = icon.getBackground(); - Bitmap bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888); - Canvas canvas = new Canvas(); - canvas.setBitmap(bitmap); - - // Clip canvas to circle. - Path circlePath = new Path(); - circlePath.addCircle(/* x */ size / 2f, - /* y */ size / 2f, - /* radius */ size / 2f, - Path.Direction.CW); - canvas.clipPath(circlePath); - - // Draw background. - background.setBounds(0, 0, size, size); - background.draw(canvas); - - // Draw foreground. The foreground and background drawables are derived from adaptive icons - // Some icon shapes fill more space than others, so adaptive icons are normalized to about - // the same size. This size is smaller than the original bounds, so we estimate - // the difference in this offset. - int offset = size / 5; - foreground.setBounds(-offset, -offset, size + offset, size + offset); - foreground.draw(canvas); - - canvas.setBitmap(null); - return bitmap; + private static class CircularAdaptiveIcon extends AdaptiveIconDrawable { + + final Path mPath = new Path(); + + CircularAdaptiveIcon(Drawable bg, Drawable fg) { + super(bg, fg); + } + + @Override + public Path getIconMask() { + mPath.reset(); + Rect bounds = getBounds(); + mPath.addOval(bounds.left, bounds.top, bounds.right, bounds.bottom, Path.Direction.CW); + return mPath; + } + + @Override + public void draw(Canvas canvas) { + int save = canvas.save(); + canvas.clipPath(getIconMask()); + + canvas.drawColor(Color.BLACK); + Drawable d; + if ((d = getBackground()) != null) { + d.draw(canvas); + } + if ((d = getForeground()) != null) { + d.draw(canvas); + } + canvas.restoreToCount(save); + } } } 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 f427a2c4bc95..93413dbe7e5f 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 @@ -25,6 +25,7 @@ import static android.view.View.VISIBLE; import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; import static com.android.wm.shell.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_CONTROLLER; +import static com.android.wm.shell.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_GESTURE; 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.TASKBAR_POSITION_BOTTOM; @@ -69,28 +70,22 @@ import android.os.UserHandle; import android.os.UserManager; import android.service.notification.NotificationListenerService; import android.service.notification.NotificationListenerService.RankingMap; -import android.util.ArraySet; import android.util.Log; import android.util.Pair; -import android.util.Slog; import android.util.SparseArray; -import android.util.SparseSetArray; import android.view.View; import android.view.ViewGroup; import android.view.WindowInsets; import android.view.WindowManager; -import android.window.WindowContainerTransaction; import androidx.annotation.MainThread; import androidx.annotation.Nullable; import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.logging.UiEventLogger; import com.android.internal.statusbar.IStatusBarService; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.TaskViewTransitions; import com.android.wm.shell.WindowManagerShellWrapper; -import com.android.wm.shell.common.DisplayChangeController; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.FloatingContentCoordinator; import com.android.wm.shell.common.ShellExecutor; @@ -103,14 +98,20 @@ 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.sysui.ConfigurationChangeListener; +import com.android.wm.shell.sysui.ShellCommandHandler; +import com.android.wm.shell.sysui.ShellController; +import com.android.wm.shell.sysui.ShellInit; import java.io.PrintWriter; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.Set; import java.util.concurrent.Executor; import java.util.function.Consumer; import java.util.function.IntConsumer; @@ -121,7 +122,7 @@ import java.util.function.IntConsumer; * * The controller manages addition, removal, and visible state of bubbles on screen. */ -public class BubbleController { +public class BubbleController implements ConfigurationChangeListener { private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleController" : TAG_BUBBLES; @@ -157,6 +158,8 @@ public class BubbleController { private final DisplayController mDisplayController; private final TaskViewTransitions mTaskViewTransitions; private final SyncTransactionQueue mSyncQueue; + private final ShellController mShellController; + private final ShellCommandHandler mShellCommandHandler; // Used to post to main UI thread private final ShellExecutor mMainExecutor; @@ -176,8 +179,8 @@ public class BubbleController { private int mCurrentUserId; // Current profiles of the user (e.g. user with a workprofile) private SparseArray<UserInfo> mCurrentProfiles; - // Saves notification keys of active bubbles when users are switched. - private final SparseSetArray<String> mSavedBubbleKeysPerUser; + // Saves data about active bubbles when users are switched. + private final SparseArray<UserBubbleData> mSavedUserBubbleData; // Used when ranking updates occur and we check if things should bubble / unbubble private NotificationListenerService.Ranking mTmpRanking; @@ -224,44 +227,11 @@ public class BubbleController { /** Drag and drop controller to register listener for onDragStarted. */ private DragAndDropController mDragAndDropController; - /** - * Creates an instance of the BubbleController. - */ - public static BubbleController create(Context context, - @Nullable BubbleStackView.SurfaceSynchronizer synchronizer, - FloatingContentCoordinator floatingContentCoordinator, - @Nullable IStatusBarService statusBarService, - WindowManager windowManager, - WindowManagerShellWrapper windowManagerShellWrapper, - UserManager userManager, - LauncherApps launcherApps, - TaskStackListenerImpl taskStackListener, - UiEventLogger uiEventLogger, - ShellTaskOrganizer organizer, - DisplayController displayController, - Optional<OneHandedController> oneHandedOptional, - DragAndDropController dragAndDropController, - @ShellMainThread ShellExecutor mainExecutor, - @ShellMainThread Handler mainHandler, - @ShellBackgroundThread ShellExecutor bgExecutor, - TaskViewTransitions taskViewTransitions, - SyncTransactionQueue syncQueue) { - BubbleLogger logger = new BubbleLogger(uiEventLogger); - BubblePositioner positioner = new BubblePositioner(context, windowManager); - BubbleData data = new BubbleData(context, logger, positioner, mainExecutor); - return new BubbleController(context, data, synchronizer, floatingContentCoordinator, - new BubbleDataRepository(context, launcherApps, mainExecutor), - statusBarService, windowManager, windowManagerShellWrapper, userManager, - launcherApps, logger, taskStackListener, organizer, positioner, displayController, - oneHandedOptional, dragAndDropController, mainExecutor, mainHandler, bgExecutor, - taskViewTransitions, syncQueue); - } - - /** - * Testing constructor. - */ - @VisibleForTesting - protected BubbleController(Context context, + + public BubbleController(Context context, + ShellInit shellInit, + ShellCommandHandler shellCommandHandler, + ShellController shellController, BubbleData data, @Nullable BubbleStackView.SurfaceSynchronizer synchronizer, FloatingContentCoordinator floatingContentCoordinator, @@ -284,6 +254,8 @@ public class BubbleController { TaskViewTransitions taskViewTransitions, SyncTransactionQueue syncQueue) { mContext = context; + mShellCommandHandler = shellCommandHandler; + mShellController = shellController; mLauncherApps = launcherApps; mBarService = statusBarService == null ? IStatusBarService.Stub.asInterface( @@ -304,7 +276,7 @@ public class BubbleController { mCurrentUserId = ActivityManager.getCurrentUser(); mBubblePositioner = positioner; mBubbleData = data; - mSavedBubbleKeysPerUser = new SparseSetArray<>(); + mSavedUserBubbleData = new SparseArray<>(); mBubbleIconFactory = new BubbleIconFactory(context); mBubbleBadgeIconFactory = new BubbleBadgeIconFactory(context); mDisplayController = displayController; @@ -312,6 +284,7 @@ public class BubbleController { mOneHandedOptional = oneHandedOptional; mDragAndDropController = dragAndDropController; mSyncQueue = syncQueue; + shellInit.addInitCallback(this::onInit, this); } private void registerOneHandedState(OneHandedController oneHanded) { @@ -333,9 +306,10 @@ public class BubbleController { }); } - public void initialize() { + protected void onInit() { mBubbleData.setListener(mBubbleDataListener); mBubbleData.setSuppressionChangedListener(this::onBubbleMetadataFlagChanged); + mDataRepository.setSuppressionChangedListener(this::onBubbleMetadataFlagChanged); mBubbleData.setPendingIntentCancelledListener(bubble -> { if (bubble.getBubbleIntent() == null) { @@ -435,17 +409,13 @@ public class BubbleController { }); mDisplayController.addDisplayChangingController( - new DisplayChangeController.OnDisplayChangingListener() { - @Override - public void onRotateDisplay(int displayId, int fromRotation, int toRotation, - WindowContainerTransaction t) { - // This is triggered right before the rotation is applied - if (fromRotation != toRotation) { - if (mStackView != null) { - // Layout listener set on stackView will update the positioner - // once the rotation is applied - mStackView.onOrientationChanged(); - } + (displayId, fromRotation, toRotation, newDisplayAreaInfo, t) -> { + // This is triggered right before the rotation is applied + if (fromRotation != toRotation) { + if (mStackView != null) { + // Layout listener set on stackView will update the positioner + // once the rotation is applied + mStackView.onOrientationChanged(); } } }); @@ -456,6 +426,16 @@ public class BubbleController { // Clear out any persisted bubbles on disk that no longer have a valid user. List<UserInfo> users = mUserManager.getAliveUsers(); mDataRepository.sanitizeBubbles(users); + + // Init profiles + SparseArray<UserInfo> userProfiles = new SparseArray<>(); + for (UserInfo user : mUserManager.getProfiles(mCurrentUserId)) { + userProfiles.put(user.id, user); + } + mCurrentProfiles = userProfiles; + + mShellController.addConfigurationChangeListener(this); + mShellCommandHandler.addDumpCallback(this::dump, this); } @VisibleForTesting @@ -563,7 +543,6 @@ public class BubbleController { if (mNotifEntryToExpandOnShadeUnlock != null) { expandStackAndSelectBubble(mNotifEntryToExpandOnShadeUnlock); - mNotifEntryToExpandOnShadeUnlock = null; } updateStack(); @@ -809,11 +788,13 @@ public class BubbleController { */ private void saveBubbles(@UserIdInt int userId) { // First clear any existing keys that might be stored. - mSavedBubbleKeysPerUser.remove(userId); + mSavedUserBubbleData.remove(userId); + UserBubbleData userBubbleData = new UserBubbleData(); // Add in all active bubbles for the current user. for (Bubble bubble : mBubbleData.getBubbles()) { - mSavedBubbleKeysPerUser.add(userId, bubble.getKey()); + userBubbleData.add(bubble.getKey(), bubble.showInShade()); } + mSavedUserBubbleData.put(userId, userBubbleData); } /** @@ -822,25 +803,27 @@ public class BubbleController { * @param userId the id of the user */ private void restoreBubbles(@UserIdInt int userId) { - ArraySet<String> savedBubbleKeys = mSavedBubbleKeysPerUser.get(userId); - if (savedBubbleKeys == null) { + UserBubbleData savedBubbleData = mSavedUserBubbleData.get(userId); + if (savedBubbleData == null) { // There were no bubbles saved for this used. return; } - mSysuiProxy.getShouldRestoredEntries(savedBubbleKeys, (entries) -> { + mSysuiProxy.getShouldRestoredEntries(savedBubbleData.getKeys(), (entries) -> { mMainExecutor.execute(() -> { for (BubbleEntry e : entries) { if (canLaunchInTaskView(mContext, e)) { - updateBubble(e, true /* suppressFlyout */, false /* showInShade */); + boolean showInShade = savedBubbleData.isShownInShade(e.getKey()); + updateBubble(e, true /* suppressFlyout */, showInShade); } } }); }); // Finally, remove the entries for this user now that bubbles are restored. - mSavedBubbleKeysPerUser.remove(userId); + mSavedUserBubbleData.remove(userId); } - private void updateForThemeChanges() { + @Override + public void onThemeChanged() { if (mStackView != null) { mStackView.onThemeChanged(); } @@ -860,7 +843,8 @@ public class BubbleController { } } - private void onConfigChanged(Configuration newConfig) { + @Override + public void onConfigurationChanged(Configuration newConfig) { if (mBubblePositioner != null) { mBubblePositioner.update(); } @@ -885,6 +869,19 @@ public class BubbleController { } } + private void onNotificationPanelExpandedChanged(boolean expanded) { + if (DEBUG_BUBBLE_GESTURE) { + Log.d(TAG, "onNotificationPanelExpandedChanged: expanded=" + expanded); + } + if (mStackView != null && mStackView.isExpanded()) { + if (expanded) { + mStackView.stopMonitoringSwipeUpGesture(); + } else { + mStackView.startMonitoringSwipeUpGesture(); + } + } + } + private void setSysuiProxy(Bubbles.SysuiProxy proxy) { mSysuiProxy = proxy; } @@ -932,15 +929,6 @@ public class BubbleController { return (isSummary && isSuppressedSummary) || isSuppressedBubble; } - private void removeSuppressedSummaryIfNecessary(String groupKey, Consumer<String> callback) { - if (mBubbleData.isSummarySuppressed(groupKey)) { - mBubbleData.removeSuppressedSummary(groupKey); - if (callback != null) { - callback.accept(mBubbleData.getSummaryKey(groupKey)); - } - } - } - /** Promote the provided bubble from the overflow view. */ public void promoteBubbleFromOverflow(Bubble bubble) { mLogger.log(bubble, BubbleLogger.Event.BUBBLE_OVERFLOW_REMOVE_BACK_TO_STACK); @@ -1013,7 +1001,33 @@ public class BubbleController { */ @VisibleForTesting public void updateBubble(BubbleEntry notif) { - updateBubble(notif, false /* suppressFlyout */, true /* showInShade */); + int bubbleUserId = notif.getStatusBarNotification().getUserId(); + if (isCurrentProfile(bubbleUserId)) { + updateBubble(notif, false /* suppressFlyout */, true /* showInShade */); + } else { + // Skip update, but store it in user bubbles so it gets restored after user switch + mSavedUserBubbleData.get(bubbleUserId, new UserBubbleData()).add(notif.getKey(), + true /* shownInShade */); + if (DEBUG_BUBBLE_CONTROLLER) { + Log.d(TAG, + "Ignore update to bubble for not active user. Bubble userId=" + bubbleUserId + + " current userId=" + mCurrentUserId); + } + } + } + + /** + * Adds a bubble for a specific intent. These bubbles are <b>not</b> backed by a notification + * and remain until the user dismisses the bubble or bubble stack. Only one intent bubble + * is supported at a time. + * + * @param intent the intent to display in the bubble expanded view. + */ + public void addAppBubble(Intent intent) { + if (intent == null || intent.getPackage() == null) return; + Bubble b = new Bubble(intent, UserHandle.of(mCurrentUserId), mMainExecutor); + b.setShouldAutoExpand(true); + inflateAndAdd(b, /* suppressFlyout= */ true, /* showInShade= */ false); } /** @@ -1050,18 +1064,28 @@ public class BubbleController { public void updateBubble(BubbleEntry notif, boolean suppressFlyout, boolean showInShade) { // If this is an interruptive notif, mark that it's interrupted mSysuiProxy.setNotificationInterruption(notif.getKey()); - if (!notif.getRanking().isTextChanged() + boolean isNonInterruptiveNotExpanding = !notif.getRanking().isTextChanged() && (notif.getBubbleMetadata() != null - && !notif.getBubbleMetadata().getAutoExpandBubble()) + && !notif.getBubbleMetadata().getAutoExpandBubble()); + if (isNonInterruptiveNotExpanding && mBubbleData.hasOverflowBubbleWithKey(notif.getKey())) { // Update the bubble but don't promote it out of overflow Bubble b = mBubbleData.getOverflowBubbleWithKey(notif.getKey()); - b.setEntry(notif); + if (notif.isBubble()) { + notif.setFlagBubble(false); + } + updateNotNotifyingEntry(b, notif, showInShade); + } else if (mBubbleData.hasAnyBubbleWithKey(notif.getKey()) + && isNonInterruptiveNotExpanding) { + Bubble b = mBubbleData.getAnyBubbleWithkey(notif.getKey()); + if (b != null) { + updateNotNotifyingEntry(b, notif, showInShade); + } } else if (mBubbleData.isSuppressedWithLocusId(notif.getLocusId())) { // Update the bubble but don't promote it out of overflow Bubble b = mBubbleData.getSuppressedBubbleWithKey(notif.getKey()); if (b != null) { - b.setEntry(notif); + updateNotNotifyingEntry(b, notif, showInShade); } } else { Bubble bubble = mBubbleData.getOrCreateBubble(notif, null /* persistedBubble */); @@ -1071,13 +1095,28 @@ public class BubbleController { if (bubble.shouldAutoExpand()) { bubble.setShouldAutoExpand(false); } + mImpl.mCachedState.updateBubbleSuppressedState(bubble); } else { inflateAndAdd(bubble, suppressFlyout, showInShade); } } } - void inflateAndAdd(Bubble bubble, boolean suppressFlyout, boolean showInShade) { + void updateNotNotifyingEntry(Bubble b, BubbleEntry entry, boolean showInShade) { + boolean showInShadeBefore = b.showInShade(); + boolean isBubbleSelected = Objects.equals(b, mBubbleData.getSelectedBubble()); + boolean isBubbleExpandedAndSelected = isStackExpanded() && isBubbleSelected; + b.setEntry(entry); + boolean suppress = isBubbleExpandedAndSelected || !showInShade || !b.showInShade(); + b.setSuppressNotification(suppress); + b.setShowDot(!isBubbleExpandedAndSelected); + if (showInShadeBefore != b.showInShade()) { + mImpl.mCachedState.updateBubbleSuppressedState(b); + } + } + + @VisibleForTesting + public void inflateAndAdd(Bubble bubble, boolean suppressFlyout, boolean showInShade) { // Lazy init stack view when a bubble is created ensureStackViewCreated(); bubble.setInflateSynchronously(mInflateSynchronously); @@ -1106,7 +1145,10 @@ public class BubbleController { } @VisibleForTesting - public void onEntryUpdated(BubbleEntry entry, boolean shouldBubbleUp) { + public void onEntryUpdated(BubbleEntry entry, boolean shouldBubbleUp, boolean fromSystem) { + if (!fromSystem) { + return; + } // shouldBubbleUp checks canBubble & for bubble metadata boolean shouldBubble = shouldBubbleUp && canLaunchInTaskView(mContext, entry); if (!shouldBubble && mBubbleData.hasAnyBubbleWithKey(entry.getKey())) { @@ -1167,9 +1209,9 @@ public class BubbleController { // notification, so that the bubble will be re-created if shouldBubbleUp returns // true. mBubbleData.dismissBubbleWithKey(key, DISMISS_NO_BUBBLE_UP); - } else if (entry != null && mTmpRanking.isBubble() && !isActive) { + } else if (entry != null && mTmpRanking.isBubble() && !isActiveOrInOverflow) { entry.setFlagBubble(true); - onEntryUpdated(entry, shouldBubbleUp); + onEntryUpdated(entry, shouldBubbleUp, /* fromSystem= */ true); } } } @@ -1309,19 +1351,7 @@ public class BubbleController { } mSysuiProxy.updateNotificationBubbleButton(bubble.getKey()); } - } - mSysuiProxy.getPendingOrActiveEntry(bubble.getKey(), (entry) -> { - mMainExecutor.execute(() -> { - if (entry != null) { - final String groupKey = entry.getStatusBarNotification().getGroupKey(); - if (getBubblesInGroup(groupKey).isEmpty()) { - // Time to potentially remove the summary - mSysuiProxy.notifyMaybeCancelSummary(bubble.getKey()); - } - } - }); - }); } mDataRepository.removeBubbles(mCurrentUserId, bubblesToBeRemovedFromRepository); @@ -1342,23 +1372,24 @@ public class BubbleController { mStackView.setBubbleSuppressed(update.unsuppressedBubble, false); } + boolean collapseStack = update.expandedChanged && !update.expanded; + // At this point, the correct bubbles are inflated in the stack. // Make sure the order in bubble data is reflected in bubble row. if (update.orderChanged && mStackView != null) { mDataRepository.addBubbles(mCurrentUserId, update.bubbles); - mStackView.updateBubbleOrder(update.bubbles); + // if the stack is going to be collapsed, do not update pointer position + // after reordering + mStackView.updateBubbleOrder(update.bubbles, !collapseStack); } - if (update.expandedChanged && !update.expanded) { + if (collapseStack) { mStackView.setExpanded(false); mSysuiProxy.requestNotificationShadeTopUi(false, TAG); } if (update.selectionChanged && mStackView != null) { mStackView.setSelectedBubble(update.selectedBubble); - if (update.selectedBubble != null) { - mSysuiProxy.updateNotificationSuppression(update.selectedBubble.getKey()); - } } // Expanding? Apply this last. @@ -1417,7 +1448,6 @@ public class BubbleController { // in the shade, it is essentially removed. Bubble bubbleChild = mBubbleData.getAnyBubbleWithkey(child.getKey()); if (bubbleChild != null) { - mSysuiProxy.removeNotificationEntry(bubbleChild.getKey()); bubbleChild.setSuppressNotification(true); bubbleChild.setShowDot(false /* show */); } @@ -1468,16 +1498,29 @@ public class BubbleController { } /** + * Check if notification panel is in an expanded state. + * Makes a call to System UI process and delivers the result via {@code callback} on the + * WM Shell main thread. + * + * @param callback callback that has the result of notification panel expanded state + */ + public void isNotificationPanelExpanded(Consumer<Boolean> callback) { + mSysuiProxy.isNotificationPanelExpand(expanded -> + mMainExecutor.execute(() -> callback.accept(expanded))); + } + + /** * Description of current bubble state. */ - private void dump(PrintWriter pw, String[] args) { + private void dump(PrintWriter pw, String prefix) { pw.println("BubbleController state:"); - mBubbleData.dump(pw, args); + mBubbleData.dump(pw); pw.println(); if (mStackView != null) { - mStackView.dump(pw, args); + mStackView.dump(pw); } pw.println(); + mImpl.mCachedState.dump(pw); } /** @@ -1546,7 +1589,7 @@ public class BubbleController { public void onImeVisibilityChanged(boolean imeVisible, int imeHeight) { mBubblePositioner.setImeVisible(imeVisible, imeHeight); if (mStackView != null) { - mStackView.animateForIme(imeVisible); + mStackView.setImeVisible(imeVisible); } } } @@ -1662,28 +1705,12 @@ public class BubbleController { } @Override - public boolean isStackExpanded() { - return mCachedState.isStackExpanded(); - } - - @Override @Nullable public Bubble getBubbleWithShortcutId(String shortcutId) { return mCachedState.getBubbleWithShortcutId(shortcutId); } @Override - public void removeSuppressedSummaryIfNecessary(String groupKey, Consumer<String> callback, - Executor callbackExecutor) { - mMainExecutor.execute(() -> { - Consumer<String> cb = callback != null - ? (key) -> callbackExecutor.execute(() -> callback.accept(key)) - : null; - BubbleController.this.removeSuppressedSummaryIfNecessary(groupKey, cb); - }); - } - - @Override public void collapseStack() { mMainExecutor.execute(() -> { BubbleController.this.collapseStack(); @@ -1691,13 +1718,6 @@ public class BubbleController { } @Override - public void updateForThemeChanges() { - mMainExecutor.execute(() -> { - BubbleController.this.updateForThemeChanges(); - }); - } - - @Override public void expandStackAndSelectBubble(BubbleEntry entry) { mMainExecutor.execute(() -> { BubbleController.this.expandStackAndSelectBubble(entry); @@ -1719,13 +1739,6 @@ public class BubbleController { } @Override - public void openBubbleOverflow() { - mMainExecutor.execute(() -> { - BubbleController.this.openBubbleOverflow(); - }); - } - - @Override public boolean handleDismissalInterception(BubbleEntry entry, @Nullable List<BubbleEntry> children, IntConsumer removeCallback, Executor callbackExecutor) { @@ -1759,9 +1772,9 @@ public class BubbleController { } @Override - public void onEntryUpdated(BubbleEntry entry, boolean shouldBubbleUp) { + public void onEntryUpdated(BubbleEntry entry, boolean shouldBubbleUp, boolean fromSystem) { mMainExecutor.execute(() -> { - BubbleController.this.onEntryUpdated(entry, shouldBubbleUp); + BubbleController.this.onEntryUpdated(entry, shouldBubbleUp, fromSystem); }); } @@ -1836,22 +1849,38 @@ public class BubbleController { } @Override - public void onConfigChanged(Configuration newConfig) { - mMainExecutor.execute(() -> { - BubbleController.this.onConfigChanged(newConfig); - }); + public void onNotificationPanelExpandedChanged(boolean expanded) { + mMainExecutor.execute( + () -> BubbleController.this.onNotificationPanelExpandedChanged(expanded)); } + } - @Override - public void dump(PrintWriter pw, String[] args) { - try { - mMainExecutor.executeBlocking(() -> { - BubbleController.this.dump(pw, args); - mCachedState.dump(pw); - }); - } catch (InterruptedException e) { - Slog.e(TAG, "Failed to dump BubbleController in 2s"); - } + /** + * Bubble data that is stored per user. + * Used to store and restore active bubbles during user switching. + */ + private static class UserBubbleData { + private final Map<String, Boolean> mKeyToShownInShadeMap = new HashMap<>(); + + /** + * Add bubble key and whether it should be shown in notification shade + */ + void add(String key, boolean shownInShade) { + mKeyToShownInShadeMap.put(key, shownInShade); + } + + /** + * Get all bubble keys stored for this user + */ + Set<String> getKeys() { + return mKeyToShownInShadeMap.keySet(); + } + + /** + * Check if this bubble with the given key should be shown in the notification shade + */ + boolean isShownInShade(String key) { + return mKeyToShownInShadeMap.get(key); } } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java index fa86c8436647..af31391fec96 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 @@ -158,7 +158,6 @@ public class BubbleData { @Nullable private Listener mListener; - @Nullable private Bubbles.BubbleMetadataFlagListener mBubbleMetadataFlagListener; private Bubbles.PendingIntentCanceledListener mCancelledListener; @@ -1136,7 +1135,7 @@ public class BubbleData { /** * Description of current bubble data state. */ - public void dump(PrintWriter pw, String[] args) { + public void dump(PrintWriter pw) { pw.print("selected: "); pw.println(mSelectedBubble != null ? mSelectedBubble.getKey() @@ -1147,13 +1146,13 @@ public class BubbleData { pw.print("stack bubble count: "); pw.println(mBubbles.size()); for (Bubble bubble : mBubbles) { - bubble.dump(pw, args); + bubble.dump(pw); } pw.print("overflow bubble count: "); pw.println(mOverflowBubbles.size()); for (Bubble bubble : mOverflowBubbles) { - bubble.dump(pw, args); + bubble.dump(pw); } pw.print("summaryKeys: "); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleDataRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleDataRepository.kt index 97560f44fb06..3a5961462c87 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 @@ -25,6 +25,7 @@ import android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_PINNED_BY_ANY_LA import android.content.pm.UserInfo import android.os.UserHandle import android.util.Log +import com.android.wm.shell.bubbles.Bubbles.BubbleMetadataFlagListener import com.android.wm.shell.bubbles.storage.BubbleEntity import com.android.wm.shell.bubbles.storage.BubblePersistentRepository import com.android.wm.shell.bubbles.storage.BubbleVolatileRepository @@ -47,6 +48,13 @@ internal class BubbleDataRepository( private val ioScope = CoroutineScope(Dispatchers.IO) private var job: Job? = null + // For use in Bubble construction. + private lateinit var bubbleMetadataFlagListener: BubbleMetadataFlagListener + + fun setSuppressionChangedListener(listener: BubbleMetadataFlagListener) { + bubbleMetadataFlagListener = listener + } + /** * Adds the bubble in memory, then persists the snapshot after adding the bubble to disk * asynchronously. @@ -197,7 +205,8 @@ internal class BubbleDataRepository( entity.title, entity.taskId, entity.locus, - mainExecutor + mainExecutor, + bubbleMetadataFlagListener ) } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleDebugConfig.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleDebugConfig.java index dc2ace949f0c..dce6b56261ff 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleDebugConfig.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleDebugConfig.java @@ -46,6 +46,9 @@ public class BubbleDebugConfig { static final boolean DEBUG_OVERFLOW = false; static final boolean DEBUG_USER_EDUCATION = false; static final boolean DEBUG_POSITIONER = false; + public static final boolean DEBUG_COLLAPSE_ANIMATOR = false; + static final boolean DEBUG_BUBBLE_GESTURE = false; + public static boolean DEBUG_EXPANDED_VIEW_DRAGGING = false; private static final boolean FORCE_SHOW_USER_EDUCATION = false; private static final String FORCE_SHOW_USER_EDUCATION_SETTING = 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 b8bf1a8e497e..5ea370b65407 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 @@ -43,10 +43,13 @@ import android.graphics.CornerPathEffect; import android.graphics.Outline; import android.graphics.Paint; import android.graphics.Picture; +import android.graphics.PointF; import android.graphics.Rect; import android.graphics.drawable.ShapeDrawable; import android.os.RemoteException; import android.util.AttributeSet; +import android.util.FloatProperty; +import android.util.IntProperty; import android.util.Log; import android.util.TypedValue; import android.view.LayoutInflater; @@ -75,6 +78,62 @@ import java.io.PrintWriter; public class BubbleExpandedView extends LinearLayout { private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleExpandedView" : TAG_BUBBLES; + /** {@link IntProperty} for updating bottom clip */ + public static final IntProperty<BubbleExpandedView> BOTTOM_CLIP_PROPERTY = + new IntProperty<BubbleExpandedView>("bottomClip") { + @Override + public void setValue(BubbleExpandedView expandedView, int value) { + expandedView.setBottomClip(value); + } + + @Override + public Integer get(BubbleExpandedView expandedView) { + return expandedView.mBottomClip; + } + }; + + /** {@link FloatProperty} for updating taskView or overflow alpha */ + public static final FloatProperty<BubbleExpandedView> CONTENT_ALPHA = + new FloatProperty<BubbleExpandedView>("contentAlpha") { + @Override + public void setValue(BubbleExpandedView expandedView, float value) { + expandedView.setContentAlpha(value); + } + + @Override + public Float get(BubbleExpandedView expandedView) { + return expandedView.getContentAlpha(); + } + }; + + /** {@link FloatProperty} for updating background and pointer alpha */ + public static final FloatProperty<BubbleExpandedView> BACKGROUND_ALPHA = + new FloatProperty<BubbleExpandedView>("backgroundAlpha") { + @Override + public void setValue(BubbleExpandedView expandedView, float value) { + expandedView.setBackgroundAlpha(value); + } + + @Override + public Float get(BubbleExpandedView expandedView) { + return expandedView.getAlpha(); + } + }; + + /** {@link FloatProperty} for updating manage button alpha */ + public static final FloatProperty<BubbleExpandedView> MANAGE_BUTTON_ALPHA = + new FloatProperty<BubbleExpandedView>("manageButtonAlpha") { + @Override + public void setValue(BubbleExpandedView expandedView, float value) { + expandedView.mManageButton.setAlpha(value); + } + + @Override + public Float get(BubbleExpandedView expandedView) { + return expandedView.mManageButton.getAlpha(); + } + }; + // The triangle pointing to the expanded view private View mPointerView; @Nullable private int[] mExpandedViewContainerLocation; @@ -90,7 +149,7 @@ public class BubbleExpandedView extends LinearLayout { /** * Whether we want the {@code TaskView}'s content to be visible (alpha = 1f). If - * {@link #mIsAlphaAnimating} is true, this may not reflect the {@code TaskView}'s actual alpha + * {@link #mIsAnimating} is true, this may not reflect the {@code TaskView}'s actual alpha * value until the animation ends. */ private boolean mIsContentVisible = false; @@ -99,12 +158,13 @@ public class BubbleExpandedView extends LinearLayout { * Whether we're animating the {@code TaskView}'s alpha value. If so, we will hold off on * applying alpha changes from {@link #setContentVisibility} until the animation ends. */ - private boolean mIsAlphaAnimating = false; + private boolean mIsAnimating = false; private int mPointerWidth; private int mPointerHeight; private float mPointerRadius; private float mPointerOverlap; + private final PointF mPointerPos = new PointF(); private CornerPathEffect mPointerEffect; private ShapeDrawable mCurrentPointer; private ShapeDrawable mTopPointer; @@ -113,11 +173,13 @@ public class BubbleExpandedView extends LinearLayout { private float mCornerRadius = 0f; private int mBackgroundColorFloating; private boolean mUsingMaxHeight; - + private int mTopClip = 0; + private int mBottomClip = 0; @Nullable private Bubble mBubble; private PendingIntent mPendingIntent; // TODO(b/170891664): Don't use a flag, set the BubbleOverflow object instead private boolean mIsOverflow; + private boolean mIsClipping; private BubbleController mController; private BubbleStackView mStackView; @@ -162,15 +224,23 @@ public class BubbleExpandedView extends LinearLayout { try { options.setTaskAlwaysOnTop(true); options.setLaunchedFromBubble(true); - if (!mIsOverflow && mBubble.hasMetadataShortcutId()) { + + Intent fillInIntent = new Intent(); + // Apply flags to make behaviour match documentLaunchMode=always. + fillInIntent.addFlags(FLAG_ACTIVITY_NEW_DOCUMENT); + fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK); + + if (mBubble.isAppBubble()) { + PendingIntent pi = PendingIntent.getActivity(mContext, 0, + mBubble.getAppBubbleIntent(), + PendingIntent.FLAG_MUTABLE, + null); + mTaskView.startActivity(pi, fillInIntent, options, launchBounds); + } else if (!mIsOverflow && mBubble.hasMetadataShortcutId()) { options.setApplyActivityFlagsForBubbles(true); mTaskView.startShortcutActivity(mBubble.getShortcutInfo(), options, launchBounds); } else { - Intent fillInIntent = new Intent(); - // Apply flags to make behaviour match documentLaunchMode=always. - fillInIntent.addFlags(FLAG_ACTIVITY_NEW_DOCUMENT); - fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK); if (mBubble != null) { mBubble.setIntentActive(); } @@ -268,7 +338,8 @@ public class BubbleExpandedView extends LinearLayout { mExpandedViewContainer.setOutlineProvider(new ViewOutlineProvider() { @Override public void getOutline(View view, Outline outline) { - outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), mCornerRadius); + Rect clip = new Rect(0, mTopClip, view.getWidth(), view.getHeight() - mBottomClip); + outline.setRoundRect(clip, mCornerRadius); } }); mExpandedViewContainer.setClipToOutline(true); @@ -300,9 +371,9 @@ public class BubbleExpandedView extends LinearLayout { // they should not collapse the stack (which all other touches on areas around the AV // would do). if (motionEvent.getRawY() >= avBounds.top - && motionEvent.getRawY() <= avBounds.bottom - && (motionEvent.getRawX() < avBounds.left - || motionEvent.getRawX() > avBounds.right)) { + && motionEvent.getRawY() <= avBounds.bottom + && (motionEvent.getRawX() < avBounds.left + || motionEvent.getRawX() > avBounds.right)) { return true; } @@ -384,7 +455,7 @@ public class BubbleExpandedView extends LinearLayout { } void applyThemeAttrs() { - final TypedArray ta = mContext.obtainStyledAttributes(new int[] { + final TypedArray ta = mContext.obtainStyledAttributes(new int[]{ android.R.attr.dialogCornerRadius, android.R.attr.colorBackgroundFloating}); boolean supportsRoundedCorners = ScreenDecorationsUtils.supportsRoundedCornersOnWindows( @@ -429,7 +500,7 @@ public class BubbleExpandedView extends LinearLayout { * ordering surfaces during animations. When content is drawn on top of the app (e.g. bubble * being dragged out, the manage menu) this is set to false, otherwise it should be true. */ - void setSurfaceZOrderedOnTop(boolean onTop) { + public void setSurfaceZOrderedOnTop(boolean onTop) { if (mTaskView == null) { return; } @@ -510,12 +581,12 @@ public class BubbleExpandedView extends LinearLayout { } /** - * Whether we are currently animating the {@code TaskView}'s alpha value. If this is set to + * Whether we are currently animating the {@code TaskView}. If this is set to * true, calls to {@link #setContentVisibility} will not be applied until this is set to false * again. */ - void setAlphaAnimating(boolean animating) { - mIsAlphaAnimating = animating; + public void setAnimating(boolean animating) { + mIsAnimating = animating; // If we're done animating, apply the correct if (!animating) { @@ -524,18 +595,139 @@ public class BubbleExpandedView extends LinearLayout { } /** - * Sets the alpha of the underlying {@code TaskView}, since changing the expanded view's alpha - * does not affect the {@code TaskView} since it uses a Surface. + * Get alpha from underlying {@code TaskView} if this view is for a bubble. + * Or get alpha for the overflow view if this view is for overflow. + * + * @return alpha for the content being shown */ - void setTaskViewAlpha(float alpha) { + public float getContentAlpha() { + if (mIsOverflow) { + return mOverflowView.getAlpha(); + } if (mTaskView != null) { + return mTaskView.getAlpha(); + } + return 1f; + } + + /** + * Set alpha of the underlying {@code TaskView} if this view is for a bubble. + * Or set alpha for the overflow view if this view is for overflow. + * + * Changing expanded view's alpha does not affect the {@code TaskView} since it uses a Surface. + */ + public void setContentAlpha(float alpha) { + if (mIsOverflow) { + mOverflowView.setAlpha(alpha); + } else if (mTaskView != null) { mTaskView.setAlpha(alpha); } + } + + /** + * Sets the alpha of the background and the pointer view. + */ + public void setBackgroundAlpha(float alpha) { mPointerView.setAlpha(alpha); setAlpha(alpha); } /** + * Set translation Y for the expanded view content. + * Excludes manage button and pointer. + */ + public void setContentTranslationY(float translationY) { + mExpandedViewContainer.setTranslationY(translationY); + + // Left or right pointer can become detached when moving the view up + if (translationY <= 0 && (isShowingLeftPointer() || isShowingRightPointer())) { + // Y coordinate where the pointer would start to get detached from the expanded view. + // Takes into account bottom clipping and rounded corners + float detachPoint = + mExpandedViewContainer.getBottom() - mBottomClip - mCornerRadius + translationY; + float pointerBottom = mPointerPos.y + mPointerHeight; + // If pointer bottom is past detach point, move it in by that many pixels + float horizontalShift = 0; + if (pointerBottom > detachPoint) { + horizontalShift = pointerBottom - detachPoint; + } + if (isShowingLeftPointer()) { + // Move left pointer right + movePointerBy(horizontalShift, 0); + } else { + // Move right pointer left + movePointerBy(-horizontalShift, 0); + } + // Hide pointer if it is moved by entire width + mPointerView.setVisibility( + horizontalShift > mPointerWidth ? View.INVISIBLE : View.VISIBLE); + } + } + + /** + * Update alpha value for the manage button + */ + public void setManageButtonAlpha(float alpha) { + mManageButton.setAlpha(alpha); + } + + /** + * Set {@link #setTranslationY(float) translationY} for the manage button + */ + public void setManageButtonTranslationY(float translationY) { + mManageButton.setTranslationY(translationY); + } + + /** + * Set top clipping for the view + */ + public void setTopClip(int clip) { + mTopClip = clip; + onContainerClipUpdate(); + } + + /** + * Set bottom clipping for the view + */ + public void setBottomClip(int clip) { + mBottomClip = clip; + onContainerClipUpdate(); + } + + private void onContainerClipUpdate() { + if (mTopClip == 0 && mBottomClip == 0) { + if (mIsClipping) { + mIsClipping = false; + if (mTaskView != null) { + mTaskView.setClipBounds(null); + mTaskView.setEnableSurfaceClipping(false); + } + mExpandedViewContainer.invalidateOutline(); + } + } else { + if (!mIsClipping) { + mIsClipping = true; + if (mTaskView != null) { + mTaskView.setEnableSurfaceClipping(true); + } + } + mExpandedViewContainer.invalidateOutline(); + if (mTaskView != null) { + mTaskView.setClipBounds(new Rect(0, mTopClip, mTaskView.getWidth(), + mTaskView.getHeight() - mBottomClip)); + } + } + } + + /** + * Move pointer from base position + */ + public void movePointerBy(float x, float y) { + mPointerView.setTranslationX(mPointerPos.x + x); + mPointerView.setTranslationY(mPointerPos.y + y); + } + + /** * Set visibility of contents in the expanded state. * * @param visibility {@code true} if the contents should be visible on the screen. @@ -543,13 +735,13 @@ public class BubbleExpandedView extends LinearLayout { * Note that this contents visibility doesn't affect visibility at {@link android.view.View}, * and setting {@code false} actually means rendering the contents in transparent. */ - void setContentVisibility(boolean visibility) { + public void setContentVisibility(boolean visibility) { if (DEBUG_BUBBLE_EXPANDED_VIEW) { Log.d(TAG, "setContentVisibility: visibility=" + visibility + " bubble=" + getBubbleKey()); } mIsContentVisible = visibility; - if (mTaskView != null && !mIsAlphaAnimating) { + if (mTaskView != null && !mIsAnimating) { mTaskView.setAlpha(visibility ? 1f : 0f); mPointerView.setAlpha(visibility ? 1f : 0f); } @@ -560,6 +752,44 @@ public class BubbleExpandedView extends LinearLayout { return mTaskView; } + @VisibleForTesting + public BubbleOverflowContainerView getOverflow() { + return mOverflowView; + } + + + /** + * Return content height: taskView or overflow. + * Takes into account clippings set by {@link #setTopClip(int)} and {@link #setBottomClip(int)} + * + * @return if bubble is for overflow, return overflow height, otherwise return taskView height + */ + public int getContentHeight() { + if (mIsOverflow) { + return mOverflowView.getHeight() - mTopClip - mBottomClip; + } + if (mTaskView != null) { + return mTaskView.getHeight() - mTopClip - mBottomClip; + } + return 0; + } + + /** + * Return bottom position of the content on screen + * + * @return if bubble is for overflow, return value for overflow, otherwise taskView + */ + public int getContentBottomOnScreen() { + Rect out = new Rect(); + if (mIsOverflow) { + mOverflowView.getBoundsOnScreen(out); + } + if (mTaskView != null) { + mTaskView.getBoundsOnScreen(out); + } + return out.bottom; + } + int getTaskId() { return mTaskId; } @@ -687,7 +917,9 @@ public class BubbleExpandedView extends LinearLayout { mTaskView.onLocationChanged(); } if (mIsOverflow) { - mOverflowView.show(); + post(() -> { + mOverflowView.show(); + }); } } @@ -730,38 +962,59 @@ public class BubbleExpandedView extends LinearLayout { post(() -> { mCurrentPointer = showVertically ? onLeft ? mLeftPointer : mRightPointer : mTopPointer; updatePointerView(); - float pointerY; - float pointerX; if (showVertically) { - pointerY = bubbleCenter - (mPointerWidth / 2f); + mPointerPos.y = bubbleCenter - (mPointerWidth / 2f); if (!isRtl) { - pointerX = onLeft + mPointerPos.x = onLeft ? -mPointerHeight + mPointerOverlap : getWidth() - mPaddingRight - mPointerOverlap; } else { - pointerX = onLeft + mPointerPos.x = onLeft ? -(getWidth() - mPaddingLeft - mPointerOverlap) : mPointerHeight - mPointerOverlap; } } else { - pointerY = mPointerOverlap; + mPointerPos.y = mPointerOverlap; if (!isRtl) { - pointerX = bubbleCenter - (mPointerWidth / 2f); + mPointerPos.x = bubbleCenter - (mPointerWidth / 2f); } else { - pointerX = -(getWidth() - mPaddingLeft - bubbleCenter) + (mPointerWidth / 2f); + mPointerPos.x = -(getWidth() - mPaddingLeft - bubbleCenter) + + (mPointerWidth / 2f); } } if (animate) { - mPointerView.animate().translationX(pointerX).translationY(pointerY).start(); + mPointerView.animate().translationX(mPointerPos.x).translationY( + mPointerPos.y).start(); } else { - mPointerView.setTranslationY(pointerY); - mPointerView.setTranslationX(pointerX); + mPointerView.setTranslationY(mPointerPos.y); + mPointerView.setTranslationX(mPointerPos.x); mPointerView.setVisibility(VISIBLE); } }); } /** + * Return true if pointer is shown on the left + */ + public boolean isShowingLeftPointer() { + return mCurrentPointer == mLeftPointer; + } + + /** + * Return true if pointer is shown on the right + */ + public boolean isShowingRightPointer() { + return mCurrentPointer == mRightPointer; + } + + /** + * Return width of the current pointer + */ + public int getPointerWidth() { + return mPointerWidth; + } + + /** * Position of the manage button displayed in the expanded view. Used for placing user * education about the manage button. */ @@ -799,7 +1052,7 @@ public class BubbleExpandedView extends LinearLayout { /** * Description of current expanded view state. */ - public void dump(@NonNull PrintWriter pw, @NonNull String[] args) { + public void dump(@NonNull PrintWriter pw) { pw.print("BubbleExpandedView"); pw.print(" taskId: "); pw.println(mTaskId); pw.print(" stackView: "); pw.println(mStackView); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleIconFactory.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleIconFactory.java index 9d3bf34895d3..5dab8a071f76 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleIconFactory.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleIconFactory.java @@ -21,6 +21,7 @@ import android.content.Context; import android.content.Intent; import android.content.pm.LauncherApps; import android.content.pm.ShortcutInfo; +import android.graphics.Bitmap; import android.graphics.drawable.Drawable; import android.graphics.drawable.Icon; @@ -65,4 +66,19 @@ public class BubbleIconFactory extends BaseIconFactory { return null; } } + + /** + * Creates the bitmap for the provided drawable and returns the scale used for + * drawing the actual drawable. + */ + public Bitmap createIconBitmap(@NonNull Drawable icon, float[] outScale) { + if (outScale == null) { + outScale = new float[1]; + } + icon = normalizeAndWrapToAdaptiveIcon(icon, + true /* shrinkNonAdaptiveIcons */, + null /* outscale */, + outScale); + return createIconBitmap(icon, outScale[0], BITMAP_GENERATION_MODE_WITH_SHADOW); + } } 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 fcd0ed7308ef..9aa285fff19c 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 @@ -167,7 +167,10 @@ public class BubbleOverflowContainerView extends LinearLayout { void updateOverflow() { Resources res = getResources(); - final int columns = res.getInteger(R.integer.bubbles_overflow_columns); + int columns = (int) Math.round(getWidth() + / (res.getDimension(R.dimen.bubble_name_width))); + columns = columns > 0 ? columns : res.getInteger(R.integer.bubbles_overflow_columns); + mRecyclerView.setLayoutManager( new OverflowGridLayoutManager(getContext(), columns)); if (mRecyclerView.getItemDecorationCount() == 0) { 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 e9729e45731b..dbad5df9cf56 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 @@ -16,6 +16,8 @@ package com.android.wm.shell.bubbles; +import static android.view.View.LAYOUT_DIRECTION_RTL; + import static java.lang.annotation.RetentionPolicy.SOURCE; import android.annotation.IntDef; @@ -28,7 +30,6 @@ import android.graphics.Rect; import android.graphics.RectF; import android.util.Log; import android.view.Surface; -import android.view.View; import android.view.WindowInsets; import android.view.WindowManager; import android.view.WindowMetrics; @@ -366,6 +367,14 @@ public class BubblePositioner { return mImeVisible ? mImeHeight : 0; } + /** Return top position of the IME if it's visible */ + public int getImeTop() { + if (mImeVisible) { + return getScreenRect().bottom - getImeHeight() - getInsets().bottom; + } + return 0; + } + /** Sets whether the IME is visible. **/ public void setImeVisible(boolean visible, int height) { mImeVisible = visible; @@ -557,16 +566,30 @@ public class BubblePositioner { * @return the position of the bubble on-screen when the stack is expanded. */ public PointF getExpandedBubbleXY(int index, BubbleStackView.StackViewState state) { - final float positionInRow = index * (mBubbleSize + mSpacingBetweenBubbles); + boolean showBubblesVertically = showBubblesVertically(); + boolean isRtl = mContext.getResources().getConfiguration().getLayoutDirection() + == LAYOUT_DIRECTION_RTL; + + int onScreenIndex; + if (showBubblesVertically || !isRtl) { + onScreenIndex = index; + } else { + // If bubbles are shown horizontally, check if RTL language is used. + // If RTL is active, position first bubble on the right and last on the left. + // Last bubble has screen index 0 and first bubble has max screen index value. + onScreenIndex = state.numberOfBubbles - 1 - index; + } + + final float positionInRow = onScreenIndex * (mBubbleSize + mSpacingBetweenBubbles); final float expandedStackSize = getExpandedStackSize(state.numberOfBubbles); - final float centerPosition = showBubblesVertically() + final float centerPosition = showBubblesVertically ? mPositionRect.centerY() : mPositionRect.centerX(); // alignment - centered on the edge final float rowStart = centerPosition - (expandedStackSize / 2f); float x; float y; - if (showBubblesVertically()) { + if (showBubblesVertically) { int inset = mExpandedViewLargeScreenInsetClosestEdge; y = rowStart + positionInRow; int left = mIsLargeScreen @@ -583,8 +606,8 @@ public class BubblePositioner { x = rowStart + positionInRow; } - if (showBubblesVertically() && mImeVisible) { - return new PointF(x, getExpandedBubbleYForIme(index, state)); + if (showBubblesVertically && mImeVisible) { + return new PointF(x, getExpandedBubbleYForIme(onScreenIndex, state)); } return new PointF(x, y); } @@ -693,7 +716,7 @@ public class BubblePositioner { // Start on the left if we're in LTR, right otherwise. final boolean startOnLeft = mContext.getResources().getConfiguration().getLayoutDirection() - != View.LAYOUT_DIRECTION_RTL; + != LAYOUT_DIRECTION_RTL; final float startingVerticalOffset = mContext.getResources().getDimensionPixelOffset( R.dimen.bubble_stack_starting_offset_y); // TODO: placement bug here because mPositionRect doesn't handle the overhanging edge @@ -749,4 +772,21 @@ public class BubblePositioner { public void setPinnedLocation(PointF point) { mPinLocation = point; } + + /** + * Navigation bar has an area where system gestures can be started from. + * + * @return {@link Rect} for system navigation bar gesture zone + */ + public Rect getNavBarGestureZone() { + // Gesture zone height from the bottom + int gestureZoneHeight = mContext.getResources().getDimensionPixelSize( + com.android.internal.R.dimen.navigation_bar_gesture_height); + Rect screen = getScreenRect(); + return new Rect( + screen.left, + screen.bottom - gestureZoneHeight, + screen.right, + screen.bottom); + } } 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 0a334140d616..be100bb1dd34 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 @@ -21,6 +21,7 @@ import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; import static com.android.wm.shell.animation.Interpolators.ALPHA_IN; import static com.android.wm.shell.animation.Interpolators.ALPHA_OUT; +import static com.android.wm.shell.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_GESTURE; import static com.android.wm.shell.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_STACK_VIEW; import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_BUBBLES; import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME; @@ -44,6 +45,7 @@ import android.graphics.Rect; import android.graphics.RectF; import android.graphics.drawable.ColorDrawable; import android.os.Bundle; +import android.os.SystemProperties; import android.provider.Settings; import android.util.Log; import android.view.Choreographer; @@ -56,6 +58,7 @@ import android.view.View; import android.view.ViewGroup; import android.view.ViewOutlineProvider; import android.view.ViewTreeObserver; +import android.view.WindowManagerPolicyConstants; import android.view.accessibility.AccessibilityNodeInfo; import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; import android.widget.FrameLayout; @@ -75,8 +78,12 @@ import com.android.internal.util.FrameworkStatsLog; import com.android.wm.shell.R; import com.android.wm.shell.animation.Interpolators; import com.android.wm.shell.animation.PhysicsAnimator; +import com.android.wm.shell.bubbles.BubblesNavBarMotionEventHandler.MotionEventListener; import com.android.wm.shell.bubbles.animation.AnimatableScaleMatrix; import com.android.wm.shell.bubbles.animation.ExpandedAnimationController; +import com.android.wm.shell.bubbles.animation.ExpandedViewAnimationController; +import com.android.wm.shell.bubbles.animation.ExpandedViewAnimationControllerImpl; +import com.android.wm.shell.bubbles.animation.ExpandedViewAnimationControllerStub; import com.android.wm.shell.bubbles.animation.PhysicsAnimationLayout; import com.android.wm.shell.bubbles.animation.StackAnimationController; import com.android.wm.shell.common.FloatingContentCoordinator; @@ -89,6 +96,7 @@ import java.math.RoundingMode; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Objects; import java.util.function.Consumer; import java.util.stream.Collectors; @@ -97,6 +105,15 @@ import java.util.stream.Collectors; */ public class BubbleStackView extends FrameLayout implements ViewTreeObserver.OnComputeInternalInsetsListener { + /** + * Set to {@code true} to enable home gesture handling in bubbles + */ + public static final boolean HOME_GESTURE_ENABLED = + SystemProperties.getBoolean("persist.wm.debug.bubbles_home_gesture", true); + + public static final boolean ENABLE_FLING_TO_DISMISS_BUBBLE = + SystemProperties.getBoolean("persist.wm.debug.fling_to_dismiss_bubble", true); + private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleStackView" : TAG_BUBBLES; /** How far the flyout needs to be dragged before it's dismissed regardless of velocity. */ @@ -123,6 +140,9 @@ public class BubbleStackView extends FrameLayout private static final float SCRIM_ALPHA = 0.6f; + /** Minimum alpha value for scrim when alpha is being changed via drag */ + private static final float MIN_SCRIM_ALPHA_FOR_DRAG = 0.2f; + /** * How long to wait to animate the stack temporarily invisible after a drag/flyout hide * animation ends, if we are in fact temporarily invisible. @@ -148,7 +168,7 @@ public class BubbleStackView extends FrameLayout * Handler to use for all delayed animations - this way, we can easily cancel them before * starting a new animation. */ - private final ShellExecutor mDelayedAnimationExecutor; + private final ShellExecutor mMainExecutor; private Runnable mDelayedAnimation; /** @@ -197,8 +217,10 @@ public class BubbleStackView extends FrameLayout private PhysicsAnimationLayout mBubbleContainer; private StackAnimationController mStackAnimationController; private ExpandedAnimationController mExpandedAnimationController; + private ExpandedViewAnimationController mExpandedViewAnimationController; private View mScrim; + private boolean mScrimAnimating; private View mManageMenuScrim; private FrameLayout mExpandedViewContainer; @@ -276,8 +298,11 @@ public class BubbleStackView extends FrameLayout */ private int mPointerIndexDown = -1; + @Nullable + private BubblesNavBarGestureTracker mBubblesNavBarGestureTracker; + /** Description of current animation controller state. */ - public void dump(PrintWriter pw, String[] args) { + public void dump(PrintWriter pw) { pw.println("Stack view state:"); String bubblesOnScreen = BubbleDebugConfig.formatBubblesString( @@ -291,8 +316,8 @@ public class BubbleStackView extends FrameLayout pw.print(" expandedContainerMatrix: "); pw.println(mExpandedViewContainer.getAnimationMatrix()); - mStackAnimationController.dump(pw, args); - mExpandedAnimationController.dump(pw, args); + mStackAnimationController.dump(pw); + mExpandedAnimationController.dump(pw); if (mExpandedBubble != null) { pw.println("Expanded bubble state:"); @@ -693,6 +718,90 @@ public class BubbleStackView extends FrameLayout } }; + /** Touch listener set on the whole view that forwards event to the swipe up listener. */ + private final RelativeTouchListener mContainerSwipeListener = new RelativeTouchListener() { + @Override + public boolean onDown(@NonNull View v, @NonNull MotionEvent ev) { + // Pass move event on to swipe listener + mSwipeUpListener.onDown(ev.getX(), ev.getY()); + return true; + } + + @Override + public void onMove(@NonNull View v, @NonNull MotionEvent ev, float viewInitialX, + float viewInitialY, float dx, float dy) { + // Pass move event on to swipe listener + mSwipeUpListener.onMove(dx, dy); + } + + @Override + public void onUp(@NonNull View v, @NonNull MotionEvent ev, float viewInitialX, + float viewInitialY, float dx, float dy, float velX, float velY) { + // Pass up even on to swipe listener + mSwipeUpListener.onUp(velX, velY); + } + }; + + /** MotionEventListener that listens from home gesture swipe event. */ + private final MotionEventListener mSwipeUpListener = new MotionEventListener() { + @Override + public void onDown(float x, float y) {} + + @Override + public void onMove(float dx, float dy) { + if ((mManageEduView != null && mManageEduView.getVisibility() == VISIBLE) + || isStackEduShowing()) { + return; + } + + if (mShowingManage) { + showManageMenu(false /* show */); + } + // Only allow up, normalize for up direction + float collapsed = -Math.min(dy, 0); + mExpandedViewAnimationController.updateDrag((int) collapsed); + + // Update scrim + if (!mScrimAnimating) { + mScrim.setAlpha(getScrimAlphaForDrag(collapsed)); + } + } + + @Override + public void onCancel() { + mExpandedViewAnimationController.animateBackToExpanded(); + } + + @Override + public void onUp(float velX, float velY) { + mExpandedViewAnimationController.setSwipeVelocity(velY); + if (mExpandedViewAnimationController.shouldCollapse()) { + // Update data first and start the animation when we are processing change + mBubbleData.setExpanded(false); + } else { + mExpandedViewAnimationController.animateBackToExpanded(); + + // Update scrim + if (!mScrimAnimating) { + showScrim(true); + } + } + } + + private float getScrimAlphaForDrag(float dragAmount) { + // dragAmount should be negative as we allow scroll up only + if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) { + float alphaRange = SCRIM_ALPHA - MIN_SCRIM_ALPHA_FOR_DRAG; + + int dragMax = mExpandedBubble.getExpandedView().getContentHeight(); + float dragFraction = dragAmount / dragMax; + + return Math.max(SCRIM_ALPHA - alphaRange * dragFraction, MIN_SCRIM_ALPHA_FOR_DRAG); + } + return SCRIM_ALPHA; + } + }; + /** Click listener set on the flyout, which expands the stack when the flyout is tapped. */ private OnClickListener mFlyoutClickListener = new OnClickListener() { @Override @@ -766,7 +875,7 @@ public class BubbleStackView extends FrameLayout ShellExecutor mainExecutor) { super(context); - mDelayedAnimationExecutor = mainExecutor; + mMainExecutor = mainExecutor; mBubbleController = bubbleController; mBubbleData = data; @@ -796,6 +905,14 @@ public class BubbleStackView extends FrameLayout mExpandedAnimationController = new ExpandedAnimationController(mPositioner, onBubbleAnimatedOut, this); + + if (HOME_GESTURE_ENABLED) { + mExpandedViewAnimationController = + new ExpandedViewAnimationControllerImpl(context, mPositioner); + } else { + mExpandedViewAnimationController = new ExpandedViewAnimationControllerStub(); + } + mSurfaceSynchronizer = synchronizer != null ? synchronizer : DEFAULT_SURFACE_SYNCHRONIZER; // Force LTR by default since most of the Bubbles UI is positioned manually by the user, or @@ -971,7 +1088,7 @@ public class BubbleStackView extends FrameLayout if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) { // We need to be Z ordered on top in order for alpha animations to work. mExpandedBubble.getExpandedView().setSurfaceZOrderedOnTop(true); - mExpandedBubble.getExpandedView().setAlphaAnimating(true); + mExpandedBubble.getExpandedView().setAnimating(true); } } @@ -985,14 +1102,15 @@ public class BubbleStackView extends FrameLayout // = 0f remains in effect. && !mExpandedViewTemporarilyHidden) { mExpandedBubble.getExpandedView().setSurfaceZOrderedOnTop(false); - mExpandedBubble.getExpandedView().setAlphaAnimating(false); + mExpandedBubble.getExpandedView().setAnimating(false); } } }); mExpandedViewAlphaAnimator.addUpdateListener(valueAnimator -> { if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) { - mExpandedBubble.getExpandedView().setTaskViewAlpha( - (float) valueAnimator.getAnimatedValue()); + float alpha = (float) valueAnimator.getAnimatedValue(); + mExpandedBubble.getExpandedView().setContentAlpha(alpha); + mExpandedBubble.getExpandedView().setBackgroundAlpha(alpha); } }); @@ -1152,7 +1270,7 @@ public class BubbleStackView extends FrameLayout } final boolean seen = getPrefBoolean(ManageEducationViewKt.PREF_MANAGED_EDUCATION); final boolean shouldShow = (!seen || BubbleDebugConfig.forceShowUserEducation(mContext)) - && mExpandedBubble != null; + && mExpandedBubble != null && mExpandedBubble.getExpandedView() != null; if (BubbleDebugConfig.DEBUG_USER_EDUCATION) { Log.d(TAG, "Show manage edu: " + shouldShow); } @@ -1708,7 +1826,7 @@ public class BubbleStackView extends FrameLayout /** * Update bubble order and pointer position. */ - public void updateBubbleOrder(List<Bubble> bubbles) { + public void updateBubbleOrder(List<Bubble> bubbles, boolean updatePointerPositoion) { final Runnable reorder = () -> { for (int i = 0; i < bubbles.size(); i++) { Bubble bubble = bubbles.get(i); @@ -1724,7 +1842,10 @@ public class BubbleStackView extends FrameLayout .map(b -> b.getIconView()).collect(Collectors.toList()); mStackAnimationController.animateReorder(bubbleViews, reorder); } - updatePointerPosition(false /* forIme */); + + if (updatePointerPositoion) { + updatePointerPosition(false /* forIme */); + } } /** @@ -1795,6 +1916,7 @@ public class BubbleStackView extends FrameLayout private void showNewlySelectedBubble(BubbleViewProvider bubbleToSelect) { final BubbleViewProvider previouslySelected = mExpandedBubble; mExpandedBubble = bubbleToSelect; + mExpandedViewAnimationController.setExpandedView(mExpandedBubble.getExpandedView()); if (mIsExpanded) { hideCurrentInputMethod(); @@ -1843,12 +1965,19 @@ public class BubbleStackView extends FrameLayout return; } + boolean wasExpanded = mIsExpanded; + hideCurrentInputMethod(); mBubbleController.getSysuiProxy().onStackExpandChanged(shouldExpand); - if (mIsExpanded) { - animateCollapse(); + if (wasExpanded) { + stopMonitoringSwipeUpGesture(); + if (HOME_GESTURE_ENABLED) { + animateCollapse(); + } else { + animateCollapseWithScale(); + } logBubbleEvent(mExpandedBubble, FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__COLLAPSED); } else { animateExpansion(); @@ -1856,11 +1985,58 @@ public class BubbleStackView extends FrameLayout logBubbleEvent(mExpandedBubble, FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__EXPANDED); logBubbleEvent(mExpandedBubble, FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__STACK_EXPANDED); + if (HOME_GESTURE_ENABLED) { + mBubbleController.isNotificationPanelExpanded(notifPanelExpanded -> { + if (!notifPanelExpanded && mIsExpanded) { + startMonitoringSwipeUpGesture(); + } + }); + } } notifyExpansionChanged(mExpandedBubble, mIsExpanded); } /** + * Monitor for swipe up gesture that is used to collapse expanded view + */ + void startMonitoringSwipeUpGesture() { + if (DEBUG_BUBBLE_GESTURE) { + Log.d(TAG, "startMonitoringSwipeUpGesture"); + } + stopMonitoringSwipeUpGestureInternal(); + + if (isGestureNavEnabled()) { + mBubblesNavBarGestureTracker = new BubblesNavBarGestureTracker(mContext, mPositioner); + mBubblesNavBarGestureTracker.start(mSwipeUpListener); + setOnTouchListener(mContainerSwipeListener); + } + } + + private boolean isGestureNavEnabled() { + return mContext.getResources().getInteger( + com.android.internal.R.integer.config_navBarInteractionMode) + == WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL; + } + + /** + * Stop monitoring for swipe up gesture + */ + void stopMonitoringSwipeUpGesture() { + if (DEBUG_BUBBLE_GESTURE) { + Log.d(TAG, "stopMonitoringSwipeUpGesture"); + } + stopMonitoringSwipeUpGestureInternal(); + } + + private void stopMonitoringSwipeUpGestureInternal() { + if (mBubblesNavBarGestureTracker != null) { + mBubblesNavBarGestureTracker.stop(); + mBubblesNavBarGestureTracker = null; + setOnTouchListener(null); + } + } + + /** * Called when back press occurs while bubbles are expanded. */ public void onBackPressed() { @@ -1982,15 +2158,28 @@ public class BubbleStackView extends FrameLayout } private void showScrim(boolean show) { + AnimatorListenerAdapter listener = new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + mScrimAnimating = true; + } + + @Override + public void onAnimationEnd(Animator animation) { + mScrimAnimating = false; + } + }; if (show) { mScrim.animate() .setInterpolator(ALPHA_IN) .alpha(SCRIM_ALPHA) + .setListener(listener) .start(); } else { mScrim.animate() .alpha(0f) .setInterpolator(ALPHA_OUT) + .setListener(listener) .start(); } } @@ -2072,11 +2261,12 @@ public class BubbleStackView extends FrameLayout mExpandedViewContainer.setAnimationMatrix(mExpandedViewContainerMatrix); if (mExpandedBubble.getExpandedView() != null) { - mExpandedBubble.getExpandedView().setTaskViewAlpha(0f); + mExpandedBubble.getExpandedView().setContentAlpha(0f); + mExpandedBubble.getExpandedView().setBackgroundAlpha(0f); // We'll be starting the alpha animation after a slight delay, so set this flag early // here. - mExpandedBubble.getExpandedView().setAlphaAnimating(true); + mExpandedBubble.getExpandedView().setAnimating(true); } mDelayedAnimation = () -> { @@ -2114,10 +2304,10 @@ public class BubbleStackView extends FrameLayout }) .start(); }; - mDelayedAnimationExecutor.executeDelayed(mDelayedAnimation, startDelay); + mMainExecutor.executeDelayed(mDelayedAnimation, startDelay); } - private void animateCollapse() { + private void animateCollapseWithScale() { cancelDelayedExpandCollapseSwitchAnimations(); if (mManageEduView != null && mManageEduView.getVisibility() == VISIBLE) { @@ -2217,6 +2407,68 @@ public class BubbleStackView extends FrameLayout .start(); } + private void animateCollapse() { + cancelDelayedExpandCollapseSwitchAnimations(); + + if (mManageEduView != null && mManageEduView.getVisibility() == VISIBLE) { + mManageEduView.hide(); + } + + mIsExpanded = false; + mIsExpansionAnimating = true; + + showScrim(false); + + mBubbleContainer.cancelAllAnimations(); + + // If we were in the middle of swapping, the animating-out surface would have been scaling + // to zero - finish it off. + PhysicsAnimator.getInstance(mAnimatingOutSurfaceContainer).cancel(); + mAnimatingOutSurfaceContainer.setScaleX(0f); + mAnimatingOutSurfaceContainer.setScaleY(0f); + + // Let the expanded animation controller know that it shouldn't animate child adds/reorders + // since we're about to animate collapsed. + mExpandedAnimationController.notifyPreparingToCollapse(); + + final Runnable collapseBackToStack = () -> mExpandedAnimationController.collapseBackToStack( + mStackAnimationController + .getStackPositionAlongNearestHorizontalEdge() + /* collapseTo */, + () -> mBubbleContainer.setActiveController(mStackAnimationController)); + + final Runnable after = () -> { + final BubbleViewProvider previouslySelected = mExpandedBubble; + // TODO(b/231350255): investigate why this call is needed here + beforeExpandedViewAnimation(); + if (mManageEduView != null) { + mManageEduView.hide(); + } + + if (DEBUG_BUBBLE_STACK_VIEW) { + Log.d(TAG, "animateCollapse"); + Log.d(TAG, BubbleDebugConfig.formatBubblesString(getBubblesOnScreen(), + mExpandedBubble)); + } + updateOverflowVisibility(); + updateZOrder(); + updateBadges(true /* setBadgeForCollapsedStack */); + afterExpandedViewAnimation(); + if (previouslySelected != null) { + previouslySelected.setTaskViewVisibility(false); + } + mExpandedViewAnimationController.reset(); + }; + mExpandedViewAnimationController.animateCollapse(collapseBackToStack, after); + if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) { + // When the animation completes, we should no longer be showing the content. + // This won't actually update content visibility immediately, if we are currently + // animating. But updates the internal state for the content to be hidden after + // animation completes. + mExpandedBubble.getExpandedView().setContentVisibility(false); + } + } + private void animateSwitchBubbles() { // If we're no longer expanded, this is meaningless. if (!mIsExpanded) { @@ -2277,7 +2529,7 @@ public class BubbleStackView extends FrameLayout mExpandedViewContainer.setAnimationMatrix(mExpandedViewContainerMatrix); - mDelayedAnimationExecutor.executeDelayed(() -> { + mMainExecutor.executeDelayed(() -> { if (!mIsExpanded) { mIsBubbleSwitchAnimating = false; return; @@ -2308,7 +2560,7 @@ public class BubbleStackView extends FrameLayout * animating flags for those animations. */ private void cancelDelayedExpandCollapseSwitchAnimations() { - mDelayedAnimationExecutor.removeCallbacks(mDelayedAnimation); + mMainExecutor.removeCallbacks(mDelayedAnimation); mIsExpansionAnimating = false; mIsBubbleSwitchAnimating = false; @@ -2332,9 +2584,18 @@ public class BubbleStackView extends FrameLayout /** * Updates the stack based for IME changes. When collapsed it'll move the stack if it * overlaps where they IME would be. When expanded it'll shift the expanded bubbles - * if they might overlap with the IME (this only happens for large screens). + * if they might overlap with the IME (this only happens for large screens) + * and clip the expanded view. */ - public void animateForIme(boolean visible) { + public void setImeVisible(boolean visible) { + if (HOME_GESTURE_ENABLED) { + setImeVisibleInternal(visible); + } else { + setImeVisibleWithoutClipping(visible); + } + } + + private void setImeVisibleWithoutClipping(boolean visible) { if ((mIsExpansionAnimating || mIsBubbleSwitchAnimating) && mIsExpanded) { // This will update the animation so the bubbles move to position for the IME mExpandedAnimationController.expandFromStack(() -> { @@ -2385,6 +2646,62 @@ public class BubbleStackView extends FrameLayout } } + private void setImeVisibleInternal(boolean visible) { + if ((mIsExpansionAnimating || mIsBubbleSwitchAnimating) && mIsExpanded) { + // This will update the animation so the bubbles move to position for the IME + mExpandedAnimationController.expandFromStack(() -> { + updatePointerPosition(false /* forIme */); + afterExpandedViewAnimation(); + mExpandedViewAnimationController.animateForImeVisibilityChange(visible); + } /* after */); + return; + } + + if (!mIsExpanded && getBubbleCount() > 0) { + final float stackDestinationY = + mStackAnimationController.animateForImeVisibility(visible); + + // How far the stack is animating due to IME, we'll just animate the flyout by that + // much too. + final float stackDy = + stackDestinationY - mStackAnimationController.getStackPosition().y; + + // If the flyout is visible, translate it along with the bubble stack. + if (mFlyout.getVisibility() == VISIBLE) { + PhysicsAnimator.getInstance(mFlyout) + .spring(DynamicAnimation.TRANSLATION_Y, + mFlyout.getTranslationY() + stackDy, + FLYOUT_IME_ANIMATION_SPRING_CONFIG) + .start(); + } + } + + if (mIsExpanded) { + mExpandedViewAnimationController.animateForImeVisibilityChange(visible); + if (mPositioner.showBubblesVertically() + && mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) { + float selectedY = mPositioner.getExpandedBubbleXY(getState().selectedIndex, + getState()).y; + float newExpandedViewTop = mPositioner.getExpandedViewY(mExpandedBubble, selectedY); + mExpandedBubble.getExpandedView().setImeVisible(visible); + if (!mExpandedBubble.getExpandedView().isUsingMaxHeight()) { + mExpandedViewContainer.animate().translationY(newExpandedViewTop); + } + List<Animator> animList = new ArrayList(); + for (int i = 0; i < mBubbleContainer.getChildCount(); i++) { + View child = mBubbleContainer.getChildAt(i); + float transY = mPositioner.getExpandedBubbleXY(i, getState()).y; + ObjectAnimator anim = ObjectAnimator.ofFloat(child, TRANSLATION_Y, transY); + animList.add(anim); + } + updatePointerPosition(true /* forIme */); + AnimatorSet set = new AnimatorSet(); + set.playTogether(animList); + set.start(); + } + } + } + @Override public boolean dispatchTouchEvent(MotionEvent ev) { if (ev.getAction() != MotionEvent.ACTION_DOWN && ev.getActionIndex() != mPointerIndexDown) { @@ -2473,8 +2790,10 @@ public class BubbleStackView extends FrameLayout private void dismissBubbleIfExists(@Nullable BubbleViewProvider bubble) { if (bubble != null && mBubbleData.hasBubbleInStackWithKey(bubble.getKey())) { - if (mIsExpanded && mBubbleData.getBubbles().size() > 1) { - // If we have more than 1 bubble we will perform the switch animation + if (mIsExpanded && mBubbleData.getBubbles().size() > 1 + && Objects.equals(bubble, mExpandedBubble)) { + // If we have more than 1 bubble and it's the current bubble being dismissed, + // we will perform the switch animation mIsBubbleSwitchAnimating = true; } mBubbleData.dismissBubbleWithKey(bubble.getKey(), Bubbles.DISMISS_USER_GESTURE); @@ -2820,7 +3139,7 @@ public class BubbleStackView extends FrameLayout && mExpandedBubble.getExpandedView() != null) { BubbleExpandedView bev = mExpandedBubble.getExpandedView(); bev.setContentVisibility(false); - bev.setAlphaAnimating(!mIsExpansionAnimating); + bev.setAnimating(!mIsExpansionAnimating); mExpandedViewContainerMatrix.setScaleX(0f); mExpandedViewContainerMatrix.setScaleY(0f); mExpandedViewContainerMatrix.setTranslate(0f, 0f); 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 69762c9bc06a..f437553337ef 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 @@ -195,15 +195,18 @@ public class BubbleViewInfoTask extends AsyncTask<Void, Void, BubbleViewInfoTask b.isImportantConversation()); info.badgeBitmap = badgeBitmapInfo.icon; // Raw badge bitmap never includes the important conversation ring - info.mRawBadgeBitmap = badgeIconFactory.getBadgeBitmap(badgedIcon, false).icon; - info.bubbleBitmap = iconFactory.createBadgedIconBitmap(bubbleDrawable).icon; + info.mRawBadgeBitmap = b.isImportantConversation() + ? badgeIconFactory.getBadgeBitmap(badgedIcon, false).icon + : badgeBitmapInfo.icon; + + float[] bubbleBitmapScale = new float[1]; + info.bubbleBitmap = iconFactory.createIconBitmap(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 = iconFactory.getNormalizer().getScale(bubbleDrawable, - null /* outBounds */, null /* path */, null /* outMaskShape */); + float scale = bubbleBitmapScale[0]; float radius = DEFAULT_PATH_SIZE / 2f; matrix.setScale(scale /* x scale */, scale /* y scale */, radius /* pivot x */, radius /* pivot y */); 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 8a0db0a12711..b3104b518440 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,12 +23,10 @@ import static java.lang.annotation.RetentionPolicy.SOURCE; import android.app.NotificationChannel; import android.content.pm.UserInfo; -import android.content.res.Configuration; import android.os.Bundle; import android.os.UserHandle; import android.service.notification.NotificationListenerService; import android.service.notification.NotificationListenerService.RankingMap; -import android.util.ArraySet; import android.util.Pair; import android.util.SparseArray; @@ -37,11 +35,11 @@ import androidx.annotation.Nullable; import com.android.wm.shell.common.annotations.ExternalThread; -import java.io.PrintWriter; import java.lang.annotation.Retention; import java.lang.annotation.Target; import java.util.HashMap; import java.util.List; +import java.util.Set; import java.util.concurrent.Executor; import java.util.function.Consumer; import java.util.function.IntConsumer; @@ -92,24 +90,9 @@ public interface Bubbles { */ boolean isBubbleExpanded(String key); - /** @return {@code true} if stack of bubbles is expanded or not. */ - boolean isStackExpanded(); - - /** - * Removes a group key indicating that the summary for this group should no longer be - * suppressed. - * - * @param callback If removed, this callback will be called with the summary key of the group - */ - void removeSuppressedSummaryIfNecessary(String groupKey, Consumer<String> callback, - Executor callbackExecutor); - /** Tell the stack of bubbles to collapse. */ void collapseStack(); - /** Tell the controller need update its UI to fit theme. */ - void updateForThemeChanges(); - /** * Request the stack expand if needed, then select the specified Bubble as current. * If no bubble exists for this entry, one is created. @@ -134,9 +117,6 @@ public interface Bubbles { /** Called for any taskbar changes. */ void onTaskbarChanged(Bundle b); - /** Open the overflow view. */ - void openBubbleOverflow(); - /** * We intercept notification entries (including group summaries) dismissed by the user when * there is an active bubble associated with it. We do this so that developers can still @@ -175,8 +155,9 @@ public interface Bubbles { * * @param entry the {@link BubbleEntry} by the notification. * @param shouldBubbleUp {@code true} if this notification should bubble up. + * @param fromSystem {@code true} if this update is from NotificationManagerService. */ - void onEntryUpdated(BubbleEntry entry, boolean shouldBubbleUp); + void onEntryUpdated(BubbleEntry entry, boolean shouldBubbleUp, boolean fromSystem); /** * Called when new notification entry removed. @@ -214,6 +195,11 @@ public interface Bubbles { int modificationType); /** + * Called when notification panel is expanded or collapsed + */ + void onNotificationPanelExpandedChanged(boolean expanded); + + /** * Called when the status bar has become visible or invisible (either permanently or * temporarily). */ @@ -250,16 +236,6 @@ public interface Bubbles { */ void onUserRemoved(int removedUserId); - /** - * Called when config changed. - * - * @param newConfig the new config. - */ - void onConfigChanged(Configuration newConfig); - - /** Description of current bubble state. */ - void dump(PrintWriter pw, String[] args); - /** Listener to find out about stack expansion / collapse events. */ interface BubbleExpandListener { /** @@ -285,11 +261,11 @@ public interface Bubbles { /** Callback to tell SysUi components execute some methods. */ interface SysuiProxy { - void isNotificationShadeExpand(Consumer<Boolean> callback); + void isNotificationPanelExpand(Consumer<Boolean> callback); void getPendingOrActiveEntry(String key, Consumer<BubbleEntry> callback); - void getShouldRestoredEntries(ArraySet<String> savedBubbleKeys, + void getShouldRestoredEntries(Set<String> savedBubbleKeys, Consumer<List<BubbleEntry>> callback); void setNotificationInterruption(String key); @@ -300,14 +276,8 @@ public interface Bubbles { void notifyInvalidateNotifications(String reason); - void notifyMaybeCancelSummary(String key); - - void removeNotificationEntry(String key); - void updateNotificationBubbleButton(String key); - void updateNotificationSuppression(String key); - void onStackExpandChanged(boolean shouldExpand); void onManageMenuExpandChanged(boolean menuExpanded); 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 new file mode 100644 index 000000000000..e7beeeb06534 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblesNavBarGestureTracker.java @@ -0,0 +1,104 @@ +/* + * 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.bubbles; + +import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_BUBBLES; +import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME; + +import android.content.Context; +import android.hardware.input.InputManager; +import android.util.Log; +import android.view.Choreographer; +import android.view.InputChannel; +import android.view.InputEventReceiver; +import android.view.InputMonitor; + +import androidx.annotation.Nullable; + +import com.android.wm.shell.bubbles.BubblesNavBarMotionEventHandler.MotionEventListener; + +/** + * Set up tracking bubbles gestures that begin in navigation bar + */ +class BubblesNavBarGestureTracker { + + private static final String TAG = TAG_WITH_CLASS_NAME ? "BubblesGestureTracker" : TAG_BUBBLES; + + private static final String GESTURE_MONITOR = "bubbles-gesture"; + private final Context mContext; + private final BubblePositioner mPositioner; + + @Nullable + private InputMonitor mInputMonitor; + @Nullable + private InputEventReceiver mInputEventReceiver; + + BubblesNavBarGestureTracker(Context context, BubblePositioner positioner) { + mContext = context; + mPositioner = positioner; + } + + /** + * Start tracking gestures + * + * @param listener listener that is notified of touch events + */ + void start(MotionEventListener listener) { + if (BubbleDebugConfig.DEBUG_BUBBLE_GESTURE) { + Log.d(TAG, "start monitoring bubbles swipe up gesture"); + } + + stopInternal(); + + mInputMonitor = InputManager.getInstance().monitorGestureInput(GESTURE_MONITOR, + mContext.getDisplayId()); + InputChannel inputChannel = mInputMonitor.getInputChannel(); + + BubblesNavBarMotionEventHandler motionEventHandler = + new BubblesNavBarMotionEventHandler(mContext, mPositioner, + this::onInterceptTouch, listener); + mInputEventReceiver = new BubblesNavBarInputEventReceiver(inputChannel, + Choreographer.getInstance(), motionEventHandler); + } + + void stop() { + if (BubbleDebugConfig.DEBUG_BUBBLE_GESTURE) { + Log.d(TAG, "stop monitoring bubbles swipe up gesture"); + } + stopInternal(); + } + + private void stopInternal() { + if (mInputEventReceiver != null) { + mInputEventReceiver.dispose(); + mInputEventReceiver = null; + } + if (mInputMonitor != null) { + mInputMonitor.dispose(); + mInputMonitor = null; + } + } + + private void onInterceptTouch() { + if (BubbleDebugConfig.DEBUG_BUBBLE_GESTURE) { + Log.d(TAG, "intercept touch event"); + } + if (mInputMonitor != null) { + mInputMonitor.pilferPointers(); + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblesNavBarInputEventReceiver.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblesNavBarInputEventReceiver.java new file mode 100644 index 000000000000..45037b87830f --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblesNavBarInputEventReceiver.java @@ -0,0 +1,51 @@ +/* + * 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.bubbles; + +import android.os.Looper; +import android.view.BatchedInputEventReceiver; +import android.view.Choreographer; +import android.view.InputChannel; +import android.view.InputEvent; +import android.view.MotionEvent; + +/** + * Bubbles {@link BatchedInputEventReceiver} for monitoring touches from navbar gesture area + */ +class BubblesNavBarInputEventReceiver extends BatchedInputEventReceiver { + + private final BubblesNavBarMotionEventHandler mMotionEventHandler; + + BubblesNavBarInputEventReceiver(InputChannel inputChannel, + Choreographer choreographer, BubblesNavBarMotionEventHandler motionEventHandler) { + super(inputChannel, Looper.myLooper(), choreographer); + mMotionEventHandler = motionEventHandler; + } + + @Override + public void onInputEvent(InputEvent event) { + boolean handled = false; + try { + if (!(event instanceof MotionEvent)) { + return; + } + handled = mMotionEventHandler.onMotionEvent((MotionEvent) event); + } finally { + finishInputEvent(event, handled); + } + } +} 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 new file mode 100644 index 000000000000..844526ca0f35 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblesNavBarMotionEventHandler.java @@ -0,0 +1,175 @@ +/* + * 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.bubbles; + +import static com.android.wm.shell.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_GESTURE; +import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_BUBBLES; +import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME; + +import android.content.Context; +import android.graphics.PointF; +import android.util.Log; +import android.view.MotionEvent; +import android.view.VelocityTracker; +import android.view.ViewConfiguration; + +import androidx.annotation.Nullable; + +/** + * Handles {@link MotionEvent}s for bubbles that begin in the nav bar area + */ +class BubblesNavBarMotionEventHandler { + private static final String TAG = + TAG_WITH_CLASS_NAME ? "BubblesNavBarMotionEventHandler" : TAG_BUBBLES; + private static final int VELOCITY_UNITS = 1000; + + private final Runnable mOnInterceptTouch; + private final MotionEventListener mMotionEventListener; + private final int mTouchSlop; + private final BubblePositioner mPositioner; + private final PointF mTouchDown = new PointF(); + private boolean mTrackingTouches; + private boolean mInterceptingTouches; + @Nullable + private VelocityTracker mVelocityTracker; + + BubblesNavBarMotionEventHandler(Context context, BubblePositioner positioner, + Runnable onInterceptTouch, MotionEventListener motionEventListener) { + mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); + mPositioner = positioner; + mOnInterceptTouch = onInterceptTouch; + mMotionEventListener = motionEventListener; + } + + /** + * Handle {@link MotionEvent} and forward it to {@code motionEventListener} defined in + * constructor + * + * @return {@code true} if this {@link MotionEvent} is handled (it started in the gesture area) + */ + public boolean onMotionEvent(MotionEvent motionEvent) { + float dx = motionEvent.getX() - mTouchDown.x; + float dy = motionEvent.getY() - mTouchDown.y; + + switch (motionEvent.getAction()) { + case MotionEvent.ACTION_DOWN: + if (isInGestureRegion(motionEvent)) { + mTouchDown.set(motionEvent.getX(), motionEvent.getY()); + mMotionEventListener.onDown(motionEvent.getX(), motionEvent.getY()); + mTrackingTouches = true; + return true; + } + break; + case MotionEvent.ACTION_MOVE: + if (mTrackingTouches) { + if (!mInterceptingTouches && Math.hypot(dx, dy) > mTouchSlop) { + mInterceptingTouches = true; + mOnInterceptTouch.run(); + } + if (mInterceptingTouches) { + getVelocityTracker().addMovement(motionEvent); + mMotionEventListener.onMove(dx, dy); + } + return true; + } + break; + case MotionEvent.ACTION_CANCEL: + if (mTrackingTouches) { + mMotionEventListener.onCancel(); + finishTracking(); + return true; + } + break; + case MotionEvent.ACTION_UP: + if (mTrackingTouches) { + if (mInterceptingTouches) { + getVelocityTracker().computeCurrentVelocity(VELOCITY_UNITS); + mMotionEventListener.onUp(getVelocityTracker().getXVelocity(), + getVelocityTracker().getYVelocity()); + } + finishTracking(); + return true; + } + break; + } + return false; + } + + private boolean isInGestureRegion(MotionEvent ev) { + // Only handles touch events beginning in navigation bar system gesture zone + if (mPositioner.getNavBarGestureZone().contains((int) ev.getX(), (int) ev.getY())) { + if (DEBUG_BUBBLE_GESTURE) { + Log.d(TAG, "handling touch y=" + ev.getY() + + " navBarGestureZone=" + mPositioner.getNavBarGestureZone()); + } + return true; + } + return false; + } + + private VelocityTracker getVelocityTracker() { + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } + return mVelocityTracker; + } + + private void finishTracking() { + mTouchDown.set(0, 0); + mTrackingTouches = false; + mInterceptingTouches = false; + if (mVelocityTracker != null) { + mVelocityTracker.recycle(); + mVelocityTracker = null; + } + } + + /** + * Callback for receiving {@link MotionEvent} updates + */ + interface MotionEventListener { + /** + * Touch down action. + * + * @param x x coordinate + * @param y y coordinate + */ + void onDown(float x, float y); + + /** + * Move action. + * Reports distance from point reported in {@link #onDown(float, float)} + * + * @param dx distance moved on x-axis from starting point, in pixels + * @param dy distance moved on y-axis from starting point, in pixels + */ + void onMove(float dx, float dy); + + /** + * Touch up action. + * + * @param velX velocity of the move action on x axis + * @param velY velocity of the move actin on y axis + */ + void onUp(float velX, float velY); + + /** + * Motion action was cancelled. + */ + void onCancel(); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/DismissView.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/DismissView.kt index 063dac3d4109..ab194dfb3ce9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/DismissView.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/DismissView.kt @@ -24,8 +24,8 @@ import android.util.IntProperty import android.view.Gravity import android.view.View import android.view.ViewGroup -import android.view.WindowManager import android.view.WindowInsets +import android.view.WindowManager import android.widget.FrameLayout import androidx.dynamicanimation.animation.DynamicAnimation import androidx.dynamicanimation.animation.SpringForce.DAMPING_RATIO_LOW_BOUNCY @@ -41,6 +41,7 @@ class DismissView(context: Context) : FrameLayout(context) { var circle = DismissCircleView(context) var isShowing = false + var targetSizeResId: Int private val animator = PhysicsAnimator.getInstance(circle) private val spring = PhysicsAnimator.SpringConfig(STIFFNESS_LOW, DAMPING_RATIO_LOW_BOUNCY) @@ -70,7 +71,8 @@ class DismissView(context: Context) : FrameLayout(context) { setVisibility(View.INVISIBLE) setBackgroundDrawable(gradientDrawable) - val targetSize: Int = resources.getDimensionPixelSize(R.dimen.dismiss_circle_size) + targetSizeResId = R.dimen.dismiss_circle_size + val targetSize: Int = resources.getDimensionPixelSize(targetSizeResId) addView(circle, LayoutParams(targetSize, targetSize, Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL)) // start with circle offscreen so it's animated up @@ -126,7 +128,7 @@ class DismissView(context: Context) : FrameLayout(context) { layoutParams.height = resources.getDimensionPixelSize( R.dimen.floating_dismiss_gradient_height) - val targetSize: Int = resources.getDimensionPixelSize(R.dimen.dismiss_circle_size) + val targetSize = resources.getDimensionPixelSize(targetSizeResId) circle.layoutParams.width = targetSize circle.layoutParams.height = targetSize circle.requestLayout() @@ -153,4 +155,4 @@ class DismissView(context: Context) : FrameLayout(context) { setPadding(0, 0, 0, navInset.bottom + resources.getDimensionPixelSize(R.dimen.floating_dismiss_bottom_margin)) } -}
\ No newline at end of file +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/RelativeTouchListener.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/RelativeTouchListener.kt index cf0cefec401a..ea9d065d5f53 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/RelativeTouchListener.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/RelativeTouchListener.kt @@ -17,8 +17,6 @@ package com.android.wm.shell.bubbles import android.graphics.PointF -import android.os.Handler -import android.os.Looper import android.view.MotionEvent import android.view.VelocityTracker import android.view.View @@ -146,6 +144,12 @@ abstract class RelativeTouchListener : View.OnTouchListener { velocityTracker.clear() movedEnough = false } + + MotionEvent.ACTION_CANCEL -> { + v.handler.removeCallbacksAndMessages(null) + velocityTracker.clear() + movedEnough = false + } } return true 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 573f42468512..b91062f891e8 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationController.java @@ -16,12 +16,17 @@ package com.android.wm.shell.bubbles.animation; +import static android.view.View.LAYOUT_DIRECTION_RTL; + import static com.android.wm.shell.bubbles.BubblePositioner.NUM_VISIBLE_WHEN_RESTING; +import static com.android.wm.shell.bubbles.BubbleStackView.ENABLE_FLING_TO_DISMISS_BUBBLE; +import static com.android.wm.shell.bubbles.BubbleStackView.HOME_GESTURE_ENABLED; import android.content.res.Resources; import android.graphics.Path; import android.graphics.PointF; import android.view.View; +import android.view.animation.Interpolator; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -61,7 +66,10 @@ public class ExpandedAnimationController private static final float DAMPING_RATIO_MEDIUM_LOW_BOUNCY = 0.65f; /** Stiffness for the expand/collapse path-following animation. */ - private static final int EXPAND_COLLAPSE_ANIM_STIFFNESS = 1000; + private static final int EXPAND_COLLAPSE_ANIM_STIFFNESS = 400; + + /** Stiffness for the expand/collapse animation when home gesture handling is off */ + private static final int EXPAND_COLLAPSE_ANIM_STIFFNESS_WITHOUT_HOME_GESTURE = 1000; /** * Velocity required to dismiss an individual bubble without dragging it into the dismiss @@ -73,6 +81,11 @@ public class ExpandedAnimationController new PhysicsAnimator.SpringConfig( EXPAND_COLLAPSE_ANIM_STIFFNESS, SpringForce.DAMPING_RATIO_NO_BOUNCY); + private final PhysicsAnimator.SpringConfig mAnimateOutSpringConfigWithoutHomeGesture = + new PhysicsAnimator.SpringConfig( + EXPAND_COLLAPSE_ANIM_STIFFNESS_WITHOUT_HOME_GESTURE, + SpringForce.DAMPING_RATIO_NO_BOUNCY); + /** Horizontal offset between bubbles, which we need to know to re-stack them. */ private float mStackOffsetPx; /** Size of each bubble. */ @@ -233,6 +246,11 @@ public class ExpandedAnimationController }; } + boolean showBubblesVertically = mPositioner.showBubblesVertically(); + final boolean isRtl = + mLayout.getContext().getResources().getConfiguration().getLayoutDirection() + == LAYOUT_DIRECTION_RTL; + // Animate each bubble individually, since each path will end in a different spot. animationsForChildrenFromIndex(0, (index, animation) -> { final View bubble = mLayout.getChildAt(index); @@ -267,9 +285,20 @@ public class ExpandedAnimationController // right side, the first bubble is traveling to the top left, so it leads. During // collapse to the left, the first bubble has the shortest travel time back to the stack // position, so it leads (and vice versa). - final boolean firstBubbleLeads = - (expanding && !mLayout.isFirstChildXLeftOfCenter(bubble.getTranslationX())) + final boolean firstBubbleLeads; + if (showBubblesVertically || !isRtl) { + firstBubbleLeads = + (expanding && !mLayout.isFirstChildXLeftOfCenter(bubble.getTranslationX())) || (!expanding && mLayout.isFirstChildXLeftOfCenter(mCollapsePoint.x)); + } else { + // For RTL languages, when showing bubbles horizontally, it is reversed. The bubbles + // are positioned right to left. This means that when expanding from left, the top + // bubble will lead as it will be positioned on the right. And when expanding from + // right, the top bubble will have the least travel distance. + firstBubbleLeads = + (expanding && mLayout.isFirstChildXLeftOfCenter(bubble.getTranslationX())) + || (!expanding && !mLayout.isFirstChildXLeftOfCenter(mCollapsePoint.x)); + } final int startDelay = firstBubbleLeads ? (index * 10) : ((mLayout.getChildCount() - index) * 10); @@ -278,11 +307,20 @@ public class ExpandedAnimationController (firstBubbleLeads && index == 0) || (!firstBubbleLeads && index == mLayout.getChildCount() - 1); + Interpolator interpolator; + if (HOME_GESTURE_ENABLED) { + // When home gesture is enabled, we use a different animation timing for collapse + interpolator = expanding + ? Interpolators.EMPHASIZED_ACCELERATE : Interpolators.EMPHASIZED_DECELERATE; + } else { + interpolator = Interpolators.LINEAR; + } + animation .followAnimatedTargetAlongPath( path, EXPAND_COLLAPSE_TARGET_ANIM_DURATION /* targetAnimDuration */, - Interpolators.LINEAR /* targetAnimInterpolator */, + interpolator /* targetAnimInterpolator */, isLeadBubble ? mLeadBubbleEndAction : null /* endAction */, () -> mLeadBubbleEndAction = null /* endAction */) .withStartDelay(startDelay) @@ -329,6 +367,7 @@ public class ExpandedAnimationController mMagnetizedBubbleDraggingOut.setMagnetListener(listener); mMagnetizedBubbleDraggingOut.setHapticsEnabled(true); mMagnetizedBubbleDraggingOut.setFlingToTargetMinVelocity(FLING_TO_DISMISS_MIN_VELOCITY); + mMagnetizedBubbleDraggingOut.setFlingToTargetEnabled(ENABLE_FLING_TO_DISMISS_BUBBLE); } private void springBubbleTo(View bubble, float x, float y) { @@ -431,7 +470,7 @@ public class ExpandedAnimationController } /** Description of current animation controller state. */ - public void dump(PrintWriter pw, String[] args) { + public void dump(PrintWriter pw) { pw.println("ExpandedAnimationController state:"); pw.print(" isActive: "); pw.println(isActiveController()); pw.print(" animatingExpand: "); pw.println(mAnimatingExpand); @@ -525,10 +564,16 @@ public class ExpandedAnimationController finishRemoval.run(); mOnBubbleAnimatedOutAction.run(); } else { + PhysicsAnimator.SpringConfig springConfig; + if (HOME_GESTURE_ENABLED) { + springConfig = mAnimateOutSpringConfig; + } else { + springConfig = mAnimateOutSpringConfigWithoutHomeGesture; + } PhysicsAnimator.getInstance(child) .spring(DynamicAnimation.ALPHA, 0f) - .spring(DynamicAnimation.SCALE_X, 0f, mAnimateOutSpringConfig) - .spring(DynamicAnimation.SCALE_Y, 0f, mAnimateOutSpringConfig) + .spring(DynamicAnimation.SCALE_X, 0f, springConfig) + .spring(DynamicAnimation.SCALE_Y, 0f, springConfig) .withEndActions(finishRemoval, mOnBubbleAnimatedOutAction) .start(); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedViewAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedViewAnimationController.java new file mode 100644 index 000000000000..8a33780bc8d5 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedViewAnimationController.java @@ -0,0 +1,75 @@ +/* + * 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.bubbles.animation; + +import com.android.wm.shell.bubbles.BubbleExpandedView; + +/** + * Animation controller for bubble expanded view collapsing + */ +public interface ExpandedViewAnimationController { + /** + * Set expanded view that this controller is working with. + */ + void setExpandedView(BubbleExpandedView expandedView); + + /** + * Set current collapse value, in pixels. + * + * @param distance pixels that user dragged the view by + */ + void updateDrag(float distance); + + /** + * Set current swipe velocity. + * Velocity is directional: + * <ul> + * <li>velocity < 0 means swipe direction is up</li> + * <li>velocity > 0 means swipe direction is down</li> + * </ul> + */ + void setSwipeVelocity(float velocity); + + /** + * Check if view is dragged past collapse threshold or swipe up velocity exceeds min velocity + * required to collapse the view + */ + boolean shouldCollapse(); + + /** + * Animate view to collapsed state + * + * @param startStackCollapse runnable that is triggered when bubbles can start moving back to + * their collapsed location + * @param after runnable to run after animation is complete + */ + void animateCollapse(Runnable startStackCollapse, Runnable after); + + /** + * Animate the view back to fully expanded state. + */ + void animateBackToExpanded(); + + /** + * Animate view for IME visibility change + */ + void animateForImeVisibilityChange(boolean visible); + + /** + * Reset the view to fully expanded state + */ + void reset(); +} 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 new file mode 100644 index 000000000000..845dca34b41f --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedViewAnimationControllerImpl.java @@ -0,0 +1,431 @@ +/* + * 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.bubbles.animation; + +import static com.android.wm.shell.bubbles.BubbleDebugConfig.DEBUG_COLLAPSE_ANIMATOR; +import static com.android.wm.shell.bubbles.BubbleDebugConfig.DEBUG_EXPANDED_VIEW_DRAGGING; +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.BubbleExpandedView.BACKGROUND_ALPHA; +import static com.android.wm.shell.bubbles.BubbleExpandedView.BOTTOM_CLIP_PROPERTY; +import static com.android.wm.shell.bubbles.BubbleExpandedView.CONTENT_ALPHA; +import static com.android.wm.shell.bubbles.BubbleExpandedView.MANAGE_BUTTON_ALPHA; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.animation.ValueAnimator; +import android.annotation.SuppressLint; +import android.content.Context; +import android.util.Log; +import android.view.HapticFeedbackConstants; +import android.view.ViewConfiguration; + +import androidx.annotation.Nullable; +import androidx.dynamicanimation.animation.DynamicAnimation; +import androidx.dynamicanimation.animation.FloatPropertyCompat; +import androidx.dynamicanimation.animation.SpringAnimation; +import androidx.dynamicanimation.animation.SpringForce; + +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 java.util.ArrayList; +import java.util.List; + +/** + * Implementation of {@link ExpandedViewAnimationController} that uses a collapse animation to + * hide the {@link BubbleExpandedView} + */ +public class ExpandedViewAnimationControllerImpl implements ExpandedViewAnimationController { + + private static final String TAG = TAG_WITH_CLASS_NAME ? "ExpandedViewAnimCtrl" : TAG_BUBBLES; + + private static final float COLLAPSE_THRESHOLD = 0.02f; + + private static final int COLLAPSE_DURATION_MS = 250; + + private static final int MANAGE_BUTTON_ANIM_DURATION_MS = 78; + + private static final int CONTENT_OPACITY_ANIM_DELAY_MS = 93; + private static final int CONTENT_OPACITY_ANIM_DURATION_MS = 78; + + private static final int BACKGROUND_OPACITY_ANIM_DELAY_MS = 172; + private static final int BACKGROUND_OPACITY_ANIM_DURATION_MS = 78; + + /** Animation fraction threshold for content alpha animation when stack collapse should begin */ + private static final float STACK_COLLAPSE_THRESHOLD = 0.5f; + + private static final FloatPropertyCompat<ExpandedViewAnimationControllerImpl> + COLLAPSE_HEIGHT_PROPERTY = + new FloatPropertyCompat<ExpandedViewAnimationControllerImpl>("CollapseSpring") { + @Override + public float getValue(ExpandedViewAnimationControllerImpl controller) { + return controller.getCollapsedAmount(); + } + + @Override + public void setValue(ExpandedViewAnimationControllerImpl controller, + float value) { + controller.setCollapsedAmount(value); + } + }; + + private final int mMinFlingVelocity; + private float mSwipeUpVelocity; + private float mSwipeDownVelocity; + private final BubblePositioner mPositioner; + private final FlingAnimationUtils mFlingAnimationUtils; + private int mDraggedAmount; + private float mCollapsedAmount; + @Nullable + private BubbleExpandedView mExpandedView; + @Nullable + private AnimatorSet mCollapseAnimation; + private boolean mNotifiedAboutThreshold; + private SpringAnimation mBackToExpandedAnimation; + @Nullable + private ObjectAnimator mBottomClipAnim; + + public ExpandedViewAnimationControllerImpl(Context context, BubblePositioner positioner) { + mFlingAnimationUtils = new FlingAnimationUtils(context.getResources().getDisplayMetrics(), + COLLAPSE_DURATION_MS / 1000f); + mMinFlingVelocity = ViewConfiguration.get(context).getScaledMinimumFlingVelocity(); + mPositioner = positioner; + } + + private static void adjustAnimatorSetDuration(AnimatorSet animatorSet, + float durationAdjustment) { + for (Animator animator : animatorSet.getChildAnimations()) { + animator.setStartDelay((long) (animator.getStartDelay() * durationAdjustment)); + animator.setDuration((long) (animator.getDuration() * durationAdjustment)); + } + } + + @Override + public void setExpandedView(BubbleExpandedView expandedView) { + if (mExpandedView != null) { + if (DEBUG_COLLAPSE_ANIMATOR) { + Log.d(TAG, "updating expandedView, resetting previous"); + } + if (mCollapseAnimation != null) { + mCollapseAnimation.cancel(); + } + if (mBackToExpandedAnimation != null) { + mBackToExpandedAnimation.cancel(); + } + reset(); + } + mExpandedView = expandedView; + } + + @Override + public void updateDrag(float distance) { + if (mExpandedView != null) { + mDraggedAmount = OverScroll.dampedScroll(distance, mExpandedView.getContentHeight()); + + if (DEBUG_COLLAPSE_ANIMATOR && DEBUG_EXPANDED_VIEW_DRAGGING) { + Log.d(TAG, "updateDrag: distance=" + distance + " dragged=" + mDraggedAmount); + } + + setCollapsedAmount(mDraggedAmount); + + if (!mNotifiedAboutThreshold && isPastCollapseThreshold()) { + mNotifiedAboutThreshold = true; + if (DEBUG_COLLAPSE_ANIMATOR) { + Log.d(TAG, "notifying over collapse threshold"); + } + vibrateIfEnabled(); + } + } + } + + @Override + public void setSwipeVelocity(float velocity) { + if (velocity < 0) { + mSwipeUpVelocity = Math.abs(velocity); + mSwipeDownVelocity = 0; + } else { + mSwipeUpVelocity = 0; + mSwipeDownVelocity = velocity; + } + } + + @Override + public boolean shouldCollapse() { + if (mSwipeDownVelocity > mMinFlingVelocity) { + // Swipe velocity is positive and over fling velocity. + // This is a swipe down, always reset to expanded state, regardless of dragged amount. + if (DEBUG_COLLAPSE_ANIMATOR) { + Log.d(TAG, + "not collapsing expanded view, swipe down velocity: " + mSwipeDownVelocity + + " minV: " + mMinFlingVelocity); + } + return false; + } + + if (mSwipeUpVelocity > mMinFlingVelocity) { + // Swiping up and over fling velocity, collapse the view. + if (DEBUG_COLLAPSE_ANIMATOR) { + Log.d(TAG, + "collapse expanded view, swipe up velocity: " + mSwipeUpVelocity + " minV: " + + mMinFlingVelocity); + } + return true; + } + + if (isPastCollapseThreshold()) { + if (DEBUG_COLLAPSE_ANIMATOR) { + Log.d(TAG, "collapse expanded view, past threshold, dragged: " + mDraggedAmount); + } + return true; + } + + if (DEBUG_COLLAPSE_ANIMATOR) { + Log.d(TAG, "not collapsing expanded view"); + } + + return false; + } + + @Override + public void animateCollapse(Runnable startStackCollapse, Runnable after) { + if (DEBUG_COLLAPSE_ANIMATOR) { + Log.d(TAG, + "expandedView animate collapse swipeVel=" + mSwipeUpVelocity + " minFlingVel=" + + mMinFlingVelocity); + } + if (mExpandedView != null) { + // Mark it as animating immediately to avoid updates to the view before animation starts + mExpandedView.setAnimating(true); + + if (mCollapseAnimation != null) { + mCollapseAnimation.cancel(); + } + mCollapseAnimation = createCollapseAnimation(mExpandedView, startStackCollapse, after); + + if (mSwipeUpVelocity >= mMinFlingVelocity) { + int contentHeight = mExpandedView.getContentHeight(); + + // Use a temp animator to get adjusted duration value for swipe. + // This new value will be used to adjust animation times proportionally in the + // animator set. If we adjust animator set duration directly, all child animations + // will get the same animation time. + ValueAnimator tempAnimator = new ValueAnimator(); + mFlingAnimationUtils.applyDismissing(tempAnimator, mCollapsedAmount, contentHeight, + mSwipeUpVelocity, (contentHeight - mCollapsedAmount)); + + float durationAdjustment = + (float) tempAnimator.getDuration() / COLLAPSE_DURATION_MS; + + adjustAnimatorSetDuration(mCollapseAnimation, durationAdjustment); + mCollapseAnimation.setInterpolator(tempAnimator.getInterpolator()); + } + mCollapseAnimation.start(); + } + } + + @Override + public void animateBackToExpanded() { + if (DEBUG_COLLAPSE_ANIMATOR) { + Log.d(TAG, "expandedView animate back to expanded"); + } + BubbleExpandedView expandedView = mExpandedView; + if (expandedView == null) { + return; + } + + expandedView.setAnimating(true); + + mBackToExpandedAnimation = new SpringAnimation(this, COLLAPSE_HEIGHT_PROPERTY); + mBackToExpandedAnimation.setSpring(new SpringForce() + .setStiffness(SpringForce.STIFFNESS_LOW) + .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY) + ); + mBackToExpandedAnimation.addEndListener(new OneTimeEndListener() { + @Override + public void onAnimationEnd(DynamicAnimation animation, boolean canceled, float value, + float velocity) { + super.onAnimationEnd(animation, canceled, value, velocity); + mNotifiedAboutThreshold = false; + mBackToExpandedAnimation = null; + expandedView.setAnimating(false); + } + }); + mBackToExpandedAnimation.setStartValue(mCollapsedAmount); + mBackToExpandedAnimation.animateToFinalPosition(0); + } + + @Override + public void animateForImeVisibilityChange(boolean visible) { + if (mExpandedView != null) { + if (mBottomClipAnim != null) { + mBottomClipAnim.cancel(); + } + int clip = 0; + if (visible) { + // Clip the expanded view at the top of the IME view + clip = mExpandedView.getContentBottomOnScreen() - mPositioner.getImeTop(); + // Don't allow negative clip value + clip = Math.max(clip, 0); + } + mBottomClipAnim = ObjectAnimator.ofInt(mExpandedView, BOTTOM_CLIP_PROPERTY, clip); + mBottomClipAnim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mBottomClipAnim = null; + } + }); + mBottomClipAnim.start(); + } + } + + @Override + public void reset() { + if (DEBUG_COLLAPSE_ANIMATOR) { + Log.d(TAG, "reset expandedView collapsed state"); + } + if (mExpandedView == null) { + return; + } + mExpandedView.setAnimating(false); + + if (mCollapseAnimation != null) { + mCollapseAnimation.cancel(); + } + if (mBackToExpandedAnimation != null) { + mBackToExpandedAnimation.cancel(); + } + mExpandedView.setContentAlpha(1); + mExpandedView.setBackgroundAlpha(1); + mExpandedView.setManageButtonAlpha(1); + setCollapsedAmount(0); + mExpandedView.setBottomClip(0); + mExpandedView.movePointerBy(0, 0); + mCollapsedAmount = 0; + mDraggedAmount = 0; + mSwipeUpVelocity = 0; + mSwipeDownVelocity = 0; + mNotifiedAboutThreshold = false; + } + + private float getCollapsedAmount() { + return mCollapsedAmount; + } + + private void setCollapsedAmount(float collapsed) { + if (mCollapsedAmount != collapsed) { + float previous = mCollapsedAmount; + mCollapsedAmount = collapsed; + + if (mExpandedView != null) { + if (previous == 0) { + // View was not collapsed before. Apply z order change + mExpandedView.setSurfaceZOrderedOnTop(true); + mExpandedView.setAnimating(true); + } + + mExpandedView.setTopClip((int) mCollapsedAmount); + // Move up with translationY. Use negative collapsed value + mExpandedView.setContentTranslationY(-mCollapsedAmount); + mExpandedView.setManageButtonTranslationY(-mCollapsedAmount); + + if (mCollapsedAmount == 0) { + // View is no longer collapsed. Revert z order change + mExpandedView.setSurfaceZOrderedOnTop(false); + mExpandedView.setAnimating(false); + } + } + } + } + + private boolean isPastCollapseThreshold() { + if (mExpandedView != null) { + return mDraggedAmount > mExpandedView.getContentHeight() * COLLAPSE_THRESHOLD; + } + return false; + } + + private AnimatorSet createCollapseAnimation(BubbleExpandedView expandedView, + Runnable startStackCollapse, Runnable after) { + List<Animator> animatorList = new ArrayList<>(); + animatorList.add(createHeightAnimation(expandedView)); + animatorList.add(createManageButtonAnimation()); + ObjectAnimator contentAlphaAnimation = createContentAlphaAnimation(); + final boolean[] notified = {false}; + contentAlphaAnimation.addUpdateListener(animation -> { + if (!notified[0] && animation.getAnimatedFraction() > STACK_COLLAPSE_THRESHOLD) { + notified[0] = true; + // Notify bubbles that they can start moving back to the collapsed position + startStackCollapse.run(); + } + }); + animatorList.add(contentAlphaAnimation); + animatorList.add(createBackgroundAlphaAnimation()); + + AnimatorSet animatorSet = new AnimatorSet(); + animatorSet.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + after.run(); + } + }); + animatorSet.playTogether(animatorList); + return animatorSet; + } + + private ValueAnimator createHeightAnimation(BubbleExpandedView expandedView) { + ValueAnimator animator = ValueAnimator.ofInt((int) mCollapsedAmount, + expandedView.getContentHeight()); + animator.setInterpolator(Interpolators.EMPHASIZED_ACCELERATE); + animator.setDuration(COLLAPSE_DURATION_MS); + animator.addUpdateListener(anim -> setCollapsedAmount((int) anim.getAnimatedValue())); + return animator; + } + + private ObjectAnimator createManageButtonAnimation() { + ObjectAnimator animator = ObjectAnimator.ofFloat(mExpandedView, MANAGE_BUTTON_ALPHA, 0f); + animator.setDuration(MANAGE_BUTTON_ANIM_DURATION_MS); + animator.setInterpolator(Interpolators.LINEAR); + return animator; + } + + private ObjectAnimator createContentAlphaAnimation() { + ObjectAnimator animator = ObjectAnimator.ofFloat(mExpandedView, CONTENT_ALPHA, 0f); + animator.setDuration(CONTENT_OPACITY_ANIM_DURATION_MS); + animator.setInterpolator(Interpolators.LINEAR); + animator.setStartDelay(CONTENT_OPACITY_ANIM_DELAY_MS); + return animator; + } + + private ObjectAnimator createBackgroundAlphaAnimation() { + ObjectAnimator animator = ObjectAnimator.ofFloat(mExpandedView, BACKGROUND_ALPHA, 0f); + animator.setDuration(BACKGROUND_OPACITY_ANIM_DURATION_MS); + animator.setInterpolator(Interpolators.LINEAR); + animator.setStartDelay(BACKGROUND_OPACITY_ANIM_DELAY_MS); + return animator; + } + + @SuppressLint("MissingPermission") + private void vibrateIfEnabled() { + if (mExpandedView != null) { + mExpandedView.performHapticFeedback(HapticFeedbackConstants.DRAG_CROSSING); + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedViewAnimationControllerStub.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedViewAnimationControllerStub.java new file mode 100644 index 000000000000..bb8a3aaaf551 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedViewAnimationControllerStub.java @@ -0,0 +1,57 @@ +/* + * 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.bubbles.animation; + +import com.android.wm.shell.bubbles.BubbleExpandedView; + +/** + * Stub implementation {@link ExpandedViewAnimationController} that does not animate the + * {@link BubbleExpandedView} + */ +public class ExpandedViewAnimationControllerStub implements ExpandedViewAnimationController { + @Override + public void setExpandedView(BubbleExpandedView expandedView) { + } + + @Override + public void updateDrag(float distance) { + } + + @Override + public void setSwipeVelocity(float velocity) { + } + + @Override + public boolean shouldCollapse() { + return false; + } + + @Override + public void animateCollapse(Runnable startStackCollapse, Runnable after) { + } + + @Override + public void animateBackToExpanded() { + } + + @Override + public void animateForImeVisibilityChange(boolean visible) { + } + + @Override + public void reset() { + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/OverScroll.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/OverScroll.java new file mode 100644 index 000000000000..d4e76ed0282e --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/OverScroll.java @@ -0,0 +1,57 @@ +/* + * 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.bubbles.animation; + +/** + * Utility methods for overscroll damping and related effect. + * + * Copied from packages/apps/Launcher3/src/com/android/launcher3/touch/OverScroll.java + */ +public class OverScroll { + + private static final float OVERSCROLL_DAMP_FACTOR = 0.07f; + + /** + * This curve determines how the effect of scrolling over the limits of the page diminishes + * as the user pulls further and further from the bounds + * + * @param f The percentage of how much the user has overscrolled. + * @return A transformed percentage based on the influence curve. + */ + private static float overScrollInfluenceCurve(float f) { + f -= 1.0f; + return f * f * f + 1.0f; + } + + /** + * @param amount The original amount overscrolled. + * @param max The maximum amount that the View can overscroll. + * @return The dampened overscroll amount. + */ + public static int dampedScroll(float amount, int max) { + if (Float.compare(amount, 0) == 0) return 0; + + float f = amount / max; + f = f / (Math.abs(f)) * (overScrollInfluenceCurve(Math.abs(f))); + + // Clamp this factor, f, to -1 < f < 1 + if (Math.abs(f) >= 1) { + f /= Math.abs(f); + } + + return Math.round(OVERSCROLL_DAMP_FACTOR * f * max); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java index 0a1b4d70fb2b..961722ba9bc0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java @@ -17,6 +17,7 @@ package com.android.wm.shell.bubbles.animation; import static com.android.wm.shell.bubbles.BubblePositioner.NUM_VISIBLE_WHEN_RESTING; +import static com.android.wm.shell.bubbles.BubbleStackView.ENABLE_FLING_TO_DISMISS_BUBBLE; import android.content.ContentResolver; import android.content.res.Resources; @@ -431,7 +432,7 @@ public class StackAnimationController extends } /** Description of current animation controller state. */ - public void dump(PrintWriter pw, String[] args) { + public void dump(PrintWriter pw) { pw.println("StackAnimationController state:"); pw.print(" isActive: "); pw.println(isActiveController()); pw.print(" restingStackPos: "); @@ -1028,6 +1029,7 @@ public class StackAnimationController extends }; mMagnetizedStack.setHapticsEnabled(true); mMagnetizedStack.setFlingToTargetMinVelocity(FLING_TO_DISMISS_MIN_VELOCITY); + mMagnetizedStack.setFlingToTargetEnabled(ENABLE_FLING_TO_DISMISS_BUBBLE); } final ContentResolver contentResolver = mLayout.getContext().getContentResolver(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DismissCircleView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DismissCircleView.java index 976fba52b9e2..e0c782d1675b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DismissCircleView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DismissCircleView.java @@ -49,6 +49,8 @@ public class DismissCircleView extends FrameLayout { @Override protected void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); + final Resources res = getResources(); + setBackground(res.getDrawable(R.drawable.dismiss_circle_background)); setViewSizes(); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayChangeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayChangeController.java index c32733d4f73c..ae1f43320c8b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayChangeController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayChangeController.java @@ -16,16 +16,19 @@ package com.android.wm.shell.common; +import android.annotation.Nullable; import android.os.RemoteException; import android.util.Slog; -import android.view.IDisplayWindowRotationCallback; -import android.view.IDisplayWindowRotationController; +import android.view.IDisplayChangeWindowCallback; +import android.view.IDisplayChangeWindowController; import android.view.IWindowManager; +import android.window.DisplayAreaInfo; import android.window.WindowContainerTransaction; import androidx.annotation.BinderThread; import com.android.wm.shell.common.annotations.ShellMainThread; +import com.android.wm.shell.sysui.ShellInit; import java.util.concurrent.CopyOnWriteArrayList; @@ -40,17 +43,22 @@ public class DisplayChangeController { private final ShellExecutor mMainExecutor; private final IWindowManager mWmService; - private final IDisplayWindowRotationController mControllerImpl; + private final IDisplayChangeWindowController mControllerImpl; - private final CopyOnWriteArrayList<OnDisplayChangingListener> mRotationListener = + private final CopyOnWriteArrayList<OnDisplayChangingListener> mDisplayChangeListener = new CopyOnWriteArrayList<>(); - public DisplayChangeController(IWindowManager wmService, ShellExecutor mainExecutor) { + public DisplayChangeController(IWindowManager wmService, ShellInit shellInit, + ShellExecutor mainExecutor) { mMainExecutor = mainExecutor; mWmService = wmService; - mControllerImpl = new DisplayWindowRotationControllerImpl(); + mControllerImpl = new DisplayChangeWindowControllerImpl(); + shellInit.addInitCallback(this::onInit, this); + } + + private void onInit() { try { - mWmService.setDisplayWindowRotationController(mControllerImpl); + mWmService.setDisplayChangeWindowController(mControllerImpl); } catch (RemoteException e) { throw new RuntimeException("Unable to register rotation controller"); } @@ -59,63 +67,64 @@ public class DisplayChangeController { /** * Adds a display rotation controller. */ - public void addRotationListener(OnDisplayChangingListener listener) { - mRotationListener.add(listener); + public void addDisplayChangeListener(OnDisplayChangingListener listener) { + mDisplayChangeListener.add(listener); } /** * Removes a display rotation controller. */ - public void removeRotationListener(OnDisplayChangingListener listener) { - mRotationListener.remove(listener); + public void removeDisplayChangeListener(OnDisplayChangingListener listener) { + mDisplayChangeListener.remove(listener); } - /** Query all listeners for changes that should happen on rotation. */ - public void dispatchOnRotateDisplay(WindowContainerTransaction outWct, int displayId, - final int fromRotation, final int toRotation) { - for (OnDisplayChangingListener c : mRotationListener) { - c.onRotateDisplay(displayId, fromRotation, toRotation, outWct); + /** Query all listeners for changes that should happen on display change. */ + public void dispatchOnDisplayChange(WindowContainerTransaction outWct, int displayId, + int fromRotation, int toRotation, DisplayAreaInfo newDisplayAreaInfo) { + for (OnDisplayChangingListener c : mDisplayChangeListener) { + c.onDisplayChange(displayId, fromRotation, toRotation, newDisplayAreaInfo, outWct); } } - private void onRotateDisplay(int displayId, final int fromRotation, final int toRotation, - IDisplayWindowRotationCallback callback) { + private void onDisplayChange(int displayId, int fromRotation, int toRotation, + DisplayAreaInfo newDisplayAreaInfo, IDisplayChangeWindowCallback callback) { WindowContainerTransaction t = new WindowContainerTransaction(); - dispatchOnRotateDisplay(t, displayId, fromRotation, toRotation); + dispatchOnDisplayChange(t, displayId, fromRotation, toRotation, newDisplayAreaInfo); try { - callback.continueRotateDisplay(toRotation, t); + callback.continueDisplayChange(t); } catch (RemoteException e) { - Slog.e(TAG, "Failed to continue rotation", e); + Slog.e(TAG, "Failed to continue handling display change", e); } } @BinderThread - private class DisplayWindowRotationControllerImpl - extends IDisplayWindowRotationController.Stub { + private class DisplayChangeWindowControllerImpl + extends IDisplayChangeWindowController.Stub { @Override - public void onRotateDisplay(int displayId, final int fromRotation, - final int toRotation, IDisplayWindowRotationCallback callback) { - mMainExecutor.execute(() -> { - DisplayChangeController.this.onRotateDisplay(displayId, fromRotation, toRotation, - callback); - }); + public void onDisplayChange(int displayId, int fromRotation, int toRotation, + DisplayAreaInfo newDisplayAreaInfo, IDisplayChangeWindowCallback callback) { + mMainExecutor.execute(() -> DisplayChangeController.this + .onDisplayChange(displayId, fromRotation, toRotation, + newDisplayAreaInfo, callback)); } } /** * Give a listener a chance to queue up configuration changes to execute as part of a - * display rotation. The contents of {@link #onRotateDisplay} must run synchronously. + * display rotation. The contents of {@link #onDisplayChange} must run synchronously. */ @ShellMainThread public interface OnDisplayChangingListener { /** - * Called before the display is rotated. Contents of this method must run synchronously. - * @param displayId Id of display that is rotating. - * @param fromRotation starting rotation of the display. - * @param toRotation target rotation of the display (after rotating). + * Called before the display size has changed. + * Contents of this method must run synchronously. + * @param displayId display id of the display that is under the change + * @param fromRotation rotation before the change + * @param toRotation rotation after the change + * @param newDisplayAreaInfo display area info after applying the update * @param t A task transaction to populate. */ - void onRotateDisplay(int displayId, int fromRotation, int toRotation, - WindowContainerTransaction t); + void onDisplayChange(int displayId, int fromRotation, int toRotation, + @Nullable DisplayAreaInfo newDisplayAreaInfo, WindowContainerTransaction t); } } 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 4ba32e93fb3d..f07ea751b044 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 @@ -34,6 +34,7 @@ import androidx.annotation.BinderThread; import com.android.wm.shell.common.DisplayChangeController.OnDisplayChangingListener; import com.android.wm.shell.common.annotations.ShellMainThread; +import com.android.wm.shell.sysui.ShellInit; import java.util.ArrayList; import java.util.List; @@ -57,19 +58,23 @@ public class DisplayController { private final SparseArray<DisplayRecord> mDisplays = new SparseArray<>(); private final ArrayList<OnDisplaysChangedListener> mDisplayChangedListeners = new ArrayList<>(); - public DisplayController(Context context, IWindowManager wmService, + public DisplayController(Context context, IWindowManager wmService, ShellInit shellInit, ShellExecutor mainExecutor) { mMainExecutor = mainExecutor; mContext = context; mWmService = wmService; - mChangeController = new DisplayChangeController(mWmService, mainExecutor); + // TODO: Inject this instead + mChangeController = new DisplayChangeController(mWmService, shellInit, mainExecutor); mDisplayContainerListener = new DisplayWindowListenerImpl(); + // Note, add this after DisplaceChangeController is constructed to ensure that is + // initialized first + shellInit.addInitCallback(this::onInit, this); } /** * Initializes the window listener. */ - public void initialize() { + public void onInit() { try { int[] displayIds = mWmService.registerDisplayWindowListener(mDisplayContainerListener); for (int i = 0; i < displayIds.length; i++) { @@ -156,14 +161,14 @@ public class DisplayController { * Adds a display rotation controller. */ public void addDisplayChangingController(OnDisplayChangingListener controller) { - mChangeController.addRotationListener(controller); + mChangeController.addDisplayChangeListener(controller); } /** * Removes a display rotation controller. */ public void removeDisplayChangingController(OnDisplayChangingListener controller) { - mChangeController.removeRotationListener(controller); + mChangeController.removeDisplayChangeListener(controller); } private void onDisplayAdded(int displayId) { 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 6a2acf438302..266cf294a950 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 @@ -20,6 +20,7 @@ import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; import android.annotation.IntDef; +import android.content.ComponentName; import android.content.Context; import android.content.res.Configuration; import android.graphics.Point; @@ -43,6 +44,7 @@ import android.view.animation.PathInterpolator; import androidx.annotation.VisibleForTesting; import com.android.internal.view.IInputMethodManager; +import com.android.wm.shell.sysui.ShellInit; import java.util.ArrayList; import java.util.concurrent.Executor; @@ -73,18 +75,24 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged private final ArrayList<ImePositionProcessor> mPositionProcessors = new ArrayList<>(); - public DisplayImeController(IWindowManager wmService, DisplayController displayController, + public DisplayImeController(IWindowManager wmService, + ShellInit shellInit, + DisplayController displayController, DisplayInsetsController displayInsetsController, - Executor mainExecutor, TransactionPool transactionPool) { + TransactionPool transactionPool, + Executor mainExecutor) { mWmService = wmService; mDisplayController = displayController; mDisplayInsetsController = displayInsetsController; mMainExecutor = mainExecutor; mTransactionPool = transactionPool; + shellInit.addInitCallback(this::onInit, this); } - /** Starts monitor displays changes and set insets controller for each displays. */ - public void startMonitorDisplays() { + /** + * Starts monitor displays changes and set insets controller for each displays. + */ + public void onInit() { mDisplayController.addDisplayWindowListener(this); } @@ -324,7 +332,7 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged } @Override - public void topFocusedWindowChanged(String packageName, + public void topFocusedWindowChanged(ComponentName component, InsetsVisibilities requestedVisibilities) { // Do nothing } 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 b6705446674a..90a01f8c5295 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 @@ -16,6 +16,7 @@ package com.android.wm.shell.common; +import android.content.ComponentName; import android.os.RemoteException; import android.util.Slog; import android.util.SparseArray; @@ -28,6 +29,7 @@ import android.view.InsetsVisibilities; import androidx.annotation.BinderThread; import com.android.wm.shell.common.annotations.ShellMainThread; +import com.android.wm.shell.sysui.ShellInit; import java.util.concurrent.CopyOnWriteArrayList; @@ -44,17 +46,20 @@ public class DisplayInsetsController implements DisplayController.OnDisplaysChan private final SparseArray<CopyOnWriteArrayList<OnInsetsChangedListener>> mListeners = new SparseArray<>(); - public DisplayInsetsController(IWindowManager wmService, DisplayController displayController, + public DisplayInsetsController(IWindowManager wmService, + ShellInit shellInit, + DisplayController displayController, ShellExecutor mainExecutor) { mWmService = wmService; mDisplayController = displayController; mMainExecutor = mainExecutor; + shellInit.addInitCallback(this::onInit, this); } /** * Starts listening for insets for each display. **/ - public void initialize() { + public void onInit() { mDisplayController.addDisplayWindowListener(this); } @@ -171,14 +176,14 @@ public class DisplayInsetsController implements DisplayController.OnDisplaysChan } } - private void topFocusedWindowChanged(String packageName, + private void topFocusedWindowChanged(ComponentName component, InsetsVisibilities requestedVisibilities) { CopyOnWriteArrayList<OnInsetsChangedListener> listeners = mListeners.get(mDisplayId); if (listeners == null) { return; } for (OnInsetsChangedListener listener : listeners) { - listener.topFocusedWindowChanged(packageName, requestedVisibilities); + listener.topFocusedWindowChanged(component, requestedVisibilities); } } @@ -186,10 +191,10 @@ public class DisplayInsetsController implements DisplayController.OnDisplaysChan private class DisplayWindowInsetsControllerImpl extends IDisplayWindowInsetsController.Stub { @Override - public void topFocusedWindowChanged(String packageName, + public void topFocusedWindowChanged(ComponentName component, InsetsVisibilities requestedVisibilities) throws RemoteException { mMainExecutor.execute(() -> { - PerDisplay.this.topFocusedWindowChanged(packageName, requestedVisibilities); + PerDisplay.this.topFocusedWindowChanged(component, requestedVisibilities); }); } @@ -234,10 +239,10 @@ public class DisplayInsetsController implements DisplayController.OnDisplaysChan /** * Called when top focused window changes to determine whether or not to take over insets * control. Won't be called if config_remoteInsetsControllerControlsSystemBars is false. - * @param packageName The name of the package that is open in the top focussed window. + * @param component The application component that is open in the top focussed window. * @param requestedVisibilities The insets visibilities requested by the focussed window. */ - default void topFocusedWindowChanged(String packageName, + default void topFocusedWindowChanged(ComponentName component, InsetsVisibilities requestedVisibilities) {} /** 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 47f1e2e18255..96efeeb0c5eb 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 @@ -96,7 +96,8 @@ public class DisplayLayout { /** * Different from {@link #equals(Object)}, this method compares the basic geometry properties - * of two {@link DisplayLayout} objects including width, height, rotation, density and cutout. + * of two {@link DisplayLayout} objects including width, height, rotation, density, cutout and + * insets. * @return {@code true} if the given {@link DisplayLayout} is identical geometry wise. */ public boolean isSameGeometry(@NonNull DisplayLayout other) { @@ -104,7 +105,8 @@ public class DisplayLayout { && mHeight == other.mHeight && mRotation == other.mRotation && mDensityDpi == other.mDensityDpi - && Objects.equals(mCutout, other.mCutout); + && Objects.equals(mCutout, other.mCutout) + && Objects.equals(mStableInsets, other.mStableInsets); } @Override 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 index fd3aa05cfc06..ec344d345139 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/InteractionJankMonitorUtils.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/InteractionJankMonitorUtils.java @@ -18,7 +18,9 @@ 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.InteractionJankMonitor; @@ -44,6 +46,24 @@ public class InteractionJankMonitorUtils { } /** + * Begin a trace session. + * + * @param cujType the specific {@link InteractionJankMonitor.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(@InteractionJankMonitor.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 InteractionJankMonitor.CujType}. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/ScreenshotUtils.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/ScreenshotUtils.java index c4bd73ba1b4a..2a1bf0ee42ba 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/ScreenshotUtils.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/ScreenshotUtils.java @@ -28,7 +28,7 @@ import java.util.function.Consumer; public class ScreenshotUtils { /** - * Take a screenshot of the specified SurfaceControl. + * Takes a screenshot of the specified SurfaceControl. * * @param sc the SurfaceControl to take a screenshot of * @param crop the crop to use when capturing the screenshot @@ -49,11 +49,14 @@ public class ScreenshotUtils { SurfaceControl mScreenshot = null; SurfaceControl.Transaction mTransaction; SurfaceControl mSurfaceControl; + SurfaceControl mParentSurfaceControl; int mLayer; - BufferConsumer(SurfaceControl.Transaction t, SurfaceControl sc, int layer) { + BufferConsumer(SurfaceControl.Transaction t, SurfaceControl sc, SurfaceControl parentSc, + int layer) { mTransaction = t; mSurfaceControl = sc; + mParentSurfaceControl = parentSc; mLayer = layer; } @@ -72,7 +75,7 @@ public class ScreenshotUtils { mTransaction.setBuffer(mScreenshot, buffer.getHardwareBuffer()); mTransaction.setColorSpace(mScreenshot, buffer.getColorSpace()); - mTransaction.reparent(mScreenshot, mSurfaceControl); + mTransaction.reparent(mScreenshot, mParentSurfaceControl); mTransaction.setLayer(mScreenshot, mLayer); mTransaction.show(mScreenshot); mTransaction.apply(); @@ -80,7 +83,7 @@ public class ScreenshotUtils { } /** - * Take a screenshot of the specified SurfaceControl. + * Takes a screenshot of the specified SurfaceControl. * * @param t the transaction used to set changes on the resulting screenshot. * @param sc the SurfaceControl to take a screenshot of @@ -91,7 +94,23 @@ public class ScreenshotUtils { */ public static SurfaceControl takeScreenshot(SurfaceControl.Transaction t, SurfaceControl sc, Rect crop, int layer) { - BufferConsumer consumer = new BufferConsumer(t, sc, layer); + return takeScreenshot(t, sc, sc /* parentSc */, crop, layer); + } + + /** + * Takes a screenshot of the specified SurfaceControl. + * + * @param t the transaction used to set changes on the resulting screenshot. + * @param sc the SurfaceControl to take a screenshot of + * @param parentSc the SurfaceControl to attach the screenshot to. + * @param crop the crop to use when capturing the screenshot + * @param layer the layer to place the screenshot + * + * @return A SurfaceControl where the screenshot will be attached, or null if failed. + */ + public static SurfaceControl takeScreenshot(SurfaceControl.Transaction t, SurfaceControl sc, + SurfaceControl parentSc, Rect crop, int layer) { + BufferConsumer consumer = new BufferConsumer(t, sc, parentSc, layer); captureLayer(sc, crop, consumer); return consumer.mScreenshot; } 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 d5875c03ccd2..e270edb800bd 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 @@ -221,8 +221,7 @@ public class SystemWindows { } final Display display = mDisplayController.getDisplay(mDisplayId); SurfaceControlViewHost viewRoot = - new SurfaceControlViewHost( - view.getContext(), display, wwm, true /* useSfChoreographer */); + new SurfaceControlViewHost(view.getContext(), display, wwm); attrs.flags |= FLAG_HARDWARE_ACCELERATED; viewRoot.setView(view, attrs); mViewRoots.put(view, viewRoot); 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 6305959bb6ac..8bc16bcc9d9d 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 @@ -206,12 +206,12 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { mSplitLayout = layout; mSplitWindowManager = splitWindowManager; mViewHost = viewHost; - mDividerBounds.set(layout.getDividerBounds()); + layout.getDividerBounds(mDividerBounds); onInsetsChanged(insetsState, false /* animate */); } void onInsetsChanged(InsetsState insetsState, boolean animate) { - mTempRect.set(mSplitLayout.getDividerBounds()); + mSplitLayout.getDividerBounds(mTempRect); final InsetsSource taskBarInsetsSource = insetsState.getSource(InsetsState.ITYPE_EXTRA_NAVIGATION_BAR); // Only insets the divider bar with task bar when it's expanded so that the rounded corners @@ -286,6 +286,7 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { setTouching(); mStartPos = touchPos; mMoving = false; + mSplitLayout.onStartDragging(); break; case MotionEvent.ACTION_MOVE: mVelocityTracker.addMovement(event); @@ -301,7 +302,10 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: releaseTouching(); - if (!mMoving) break; + if (!mMoving) { + mSplitLayout.onDraggingCancelled(); + break; + } mVelocityTracker.addMovement(event); mVelocityTracker.computeCurrentVelocity(1000 /* units */); 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 484294ab295b..74f8bf9ac863 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 @@ -50,12 +50,15 @@ import com.android.launcher3.icons.IconProvider; import com.android.wm.shell.R; import com.android.wm.shell.common.SurfaceUtils; +import java.util.function.Consumer; + /** * Handles split decor like showing resizing hint for a specific split. */ public class SplitDecorManager extends WindowlessWindowManager { private static final String TAG = SplitDecorManager.class.getSimpleName(); private static final String RESIZING_BACKGROUND_SURFACE_NAME = "ResizingBackground"; + private static final String GAP_BACKGROUND_SURFACE_NAME = "GapBackground"; private static final long FADE_DURATION = 133; private final IconProvider mIconProvider; @@ -67,6 +70,7 @@ public class SplitDecorManager extends WindowlessWindowManager { private SurfaceControl mHostLeash; private SurfaceControl mIconLeash; private SurfaceControl mBackgroundLeash; + private SurfaceControl mGapBackgroundLeash; private boolean mShown; private boolean mIsResizing; @@ -141,6 +145,10 @@ public class SplitDecorManager extends WindowlessWindowManager { t.remove(mBackgroundLeash); mBackgroundLeash = null; } + if (mGapBackgroundLeash != null) { + t.remove(mGapBackgroundLeash); + mGapBackgroundLeash = null; + } mHostLeash = null; mIcon = null; mResizingIconView = null; @@ -150,7 +158,7 @@ public class SplitDecorManager extends WindowlessWindowManager { /** Showing resizing hint. */ public void onResizing(ActivityManager.RunningTaskInfo resizingTask, Rect newBounds, - SurfaceControl.Transaction t) { + Rect sideBounds, SurfaceControl.Transaction t) { if (mResizingIconView == null) { return; } @@ -176,6 +184,19 @@ public class SplitDecorManager extends WindowlessWindowManager { .setLayer(mBackgroundLeash, Integer.MAX_VALUE - 1); } + if (mGapBackgroundLeash == null) { + final boolean isLandscape = newBounds.height() == sideBounds.height(); + final int left = isLandscape ? mBounds.width() : 0; + final int top = isLandscape ? 0 : mBounds.height(); + mGapBackgroundLeash = SurfaceUtils.makeColorLayer(mHostLeash, + GAP_BACKGROUND_SURFACE_NAME, mSurfaceSession); + // Fill up another side bounds area. + t.setColor(mGapBackgroundLeash, getResizingBackgroundColor(resizingTask)) + .setLayer(mGapBackgroundLeash, Integer.MAX_VALUE - 2) + .setPosition(mGapBackgroundLeash, left, top) + .setWindowCrop(mGapBackgroundLeash, sideBounds.width(), sideBounds.height()); + } + if (mIcon == null && resizingTask.topActivityInfo != null) { mIcon = mIconProvider.getIcon(resizingTask.topActivityInfo); mResizingIconView.setImageDrawable(mIcon); @@ -193,7 +214,7 @@ public class SplitDecorManager extends WindowlessWindowManager { newBounds.height() / 2 - mIconSize / 2); if (animate) { - startFadeAnimation(show, false /* isResized */); + startFadeAnimation(show, null /* finishedConsumer */); mShown = show; } } @@ -224,14 +245,29 @@ public class SplitDecorManager extends WindowlessWindowManager { mFadeAnimator.cancel(); } if (mShown) { - startFadeAnimation(false /* show */, true /* isResized */); + fadeOutDecor(null /* finishedCallback */); } else { // Decor surface is hidden so release it directly. releaseDecor(t); } } - private void startFadeAnimation(boolean show, boolean isResized) { + /** Fade-out decor surface with animation end callback, if decor is hidden, run the callback + * directly. */ + public void fadeOutDecor(Runnable finishedCallback) { + if (mShown) { + startFadeAnimation(false /* show */, transaction -> { + releaseDecor(transaction); + if (finishedCallback != null) finishedCallback.run(); + }); + mShown = false; + } else { + if (finishedCallback != null) finishedCallback.run(); + } + } + + private void startFadeAnimation(boolean show, + Consumer<SurfaceControl.Transaction> finishedConsumer) { final SurfaceControl.Transaction animT = new SurfaceControl.Transaction(); mFadeAnimator = ValueAnimator.ofFloat(0f, 1f); mFadeAnimator.setDuration(FADE_DURATION); @@ -249,7 +285,9 @@ public class SplitDecorManager extends WindowlessWindowManager { @Override public void onAnimationStart(@NonNull Animator animation) { if (show) { - animT.show(mBackgroundLeash).show(mIconLeash).apply(); + animT.show(mBackgroundLeash).show(mIconLeash).show(mGapBackgroundLeash).apply(); + } else { + animT.hide(mGapBackgroundLeash).apply(); } } @@ -263,8 +301,8 @@ public class SplitDecorManager extends WindowlessWindowManager { animT.hide(mIconLeash); } } - if (isResized) { - releaseDecor(animT); + if (finishedConsumer != null) { + finishedConsumer.accept(animT); } animT.apply(); animT.close(); @@ -280,6 +318,11 @@ public class SplitDecorManager extends WindowlessWindowManager { mBackgroundLeash = null; } + if (mGapBackgroundLeash != null) { + t.remove(mGapBackgroundLeash); + mGapBackgroundLeash = null; + } + if (mIcon != null) { mResizingIconView.setVisibility(View.GONE); mResizingIconView.setImageDrawable(null); 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 c94455d9151a..419e62daf586 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 @@ -24,6 +24,7 @@ import static android.view.WindowManager.DOCKED_LEFT; import static android.view.WindowManager.DOCKED_RIGHT; import static android.view.WindowManager.DOCKED_TOP; +import static com.android.internal.jank.InteractionJankMonitor.CUJ_SPLIT_SCREEN_RESIZE; import static com.android.internal.policy.DividerSnapAlgorithm.SnapTarget.FLAG_DISMISS_END; import static com.android.internal.policy.DividerSnapAlgorithm.SnapTarget.FLAG_DISMISS_START; import static com.android.wm.shell.animation.Interpolators.DIM_INTERPOLATOR; @@ -31,9 +32,11 @@ import static com.android.wm.shell.animation.Interpolators.SLOWDOWN_INTERPOLATOR 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.splitscreen.SplitScreenController.EXIT_REASON_DRAG_DIVIDER; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; import android.animation.ValueAnimator; import android.annotation.NonNull; import android.app.ActivityManager; @@ -55,7 +58,6 @@ import android.window.WindowContainerTransaction; import androidx.annotation.Nullable; import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.policy.DividerSnapAlgorithm; import com.android.internal.policy.DockedDividerUtils; import com.android.wm.shell.R; @@ -78,6 +80,11 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange public static final int PARALLAX_DISMISSING = 1; public static final int PARALLAX_ALIGN_CENTER = 2; + private static final int FLING_RESIZE_DURATION = 250; + private static final int FLING_SWITCH_DURATION = 350; + private static final int FLING_ENTER_DURATION = 350; + private static final int FLING_EXIT_DURATION = 350; + private final int mDividerWindowWidth; private final int mDividerInsets; private final int mDividerSize; @@ -85,8 +92,13 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange private final Rect mTempRect = new Rect(); private final Rect mRootBounds = new Rect(); private final Rect mDividerBounds = new Rect(); + // Bounds1 final position should be always at top or left private final Rect mBounds1 = new Rect(); + // Bounds2 final position should be always at bottom or right private final Rect mBounds2 = new Rect(); + // The temp bounds outside of display bounds for side stage when split screen inactive to avoid + // flicker next time active split screen. + private final Rect mInvisibleBounds = new Rect(); private final Rect mWinBounds1 = new Rect(); private final Rect mWinBounds2 = new Rect(); private final SplitLayoutHandler mSplitLayoutHandler; @@ -135,6 +147,8 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange resetDividerPosition(); mDimNonImeSide = resources.getBoolean(R.bool.config_dimNonImeAttachedSide); + + updateInvisibleRect(); } private int getDividerInsets(Resources resources, Display display) { @@ -178,6 +192,11 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange return outBounds; } + /** Gets root bounds of the whole split layout */ + public Rect getRootBounds() { + return new Rect(mRootBounds); + } + /** Gets bounds of divider window with screen based coordinate. */ public Rect getDividerBounds() { return new Rect(mDividerBounds); @@ -190,6 +209,50 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange return outBounds; } + /** Gets bounds of the primary split with screen based coordinate on the param Rect. */ + public void getBounds1(Rect rect) { + rect.set(mBounds1); + } + + /** Gets bounds of the primary split with parent based coordinate on the param Rect. */ + public void getRefBounds1(Rect rect) { + getBounds1(rect); + rect.offset(-mRootBounds.left, -mRootBounds.top); + } + + /** Gets bounds of the secondary split with screen based coordinate on the param Rect. */ + public void getBounds2(Rect rect) { + rect.set(mBounds2); + } + + /** Gets bounds of the secondary split with parent based coordinate on the param Rect. */ + public void getRefBounds2(Rect rect) { + getBounds2(rect); + rect.offset(-mRootBounds.left, -mRootBounds.top); + } + + /** Gets root bounds of the whole split layout on the param Rect. */ + public void getRootBounds(Rect rect) { + rect.set(mRootBounds); + } + + /** Gets bounds of divider window with screen based coordinate on the param Rect. */ + public void getDividerBounds(Rect rect) { + rect.set(mDividerBounds); + } + + /** Gets bounds of divider window with parent based coordinate on the param Rect. */ + public void getRefDividerBounds(Rect rect) { + getDividerBounds(rect); + rect.offset(-mRootBounds.left, -mRootBounds.top); + } + + /** Gets bounds size equal to root bounds but outside of screen, used for position side stage + * when split inactive to avoid flicker when next time active. */ + public void getInvisibleBounds(Rect rect) { + rect.set(mInvisibleBounds); + } + /** Returns leash of the current divider bar. */ @Nullable public SurfaceControl getDividerLeash() { @@ -209,31 +272,40 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange : (float) ((mBounds1.bottom + mBounds2.top) / 2f) / mBounds2.bottom)); } + private void updateInvisibleRect() { + mInvisibleBounds.set(mRootBounds.left, mRootBounds.top, + isLandscape() ? mRootBounds.right / 2 : mRootBounds.right, + isLandscape() ? mRootBounds.bottom : mRootBounds.bottom / 2); + mInvisibleBounds.offset(isLandscape() ? mRootBounds.right : 0, + isLandscape() ? 0 : mRootBounds.bottom); + } + /** Applies new configuration, returns {@code false} if there's no effect to the layout. */ public boolean updateConfiguration(Configuration configuration) { - // Always update configuration after orientation changed to make sure to render divider bar - // with proper resources that matching screen orientation. - final int orientation = configuration.orientation; - if (mOrientation != orientation) { - mContext = mContext.createConfigurationContext(configuration); - mSplitWindowManager.setConfiguration(configuration); - mOrientation = orientation; - } - // Update the split bounds when necessary. Besides root bounds changed, split bounds need to // be updated when the rotation changed to cover the case that users rotated the screen 180 // degrees. + // Make sure to render the divider bar with proper resources that matching the screen + // orientation. final int rotation = configuration.windowConfiguration.getRotation(); final Rect rootBounds = configuration.windowConfiguration.getBounds(); - if (mRotation == rotation && mRootBounds.equals(rootBounds)) { + final int orientation = configuration.orientation; + + if (mOrientation == orientation + && mRotation == rotation + && mRootBounds.equals(rootBounds)) { return false; } + mContext = mContext.createConfigurationContext(configuration); + mSplitWindowManager.setConfiguration(configuration); + mOrientation = orientation; mTempRect.set(mRootBounds); mRootBounds.set(rootBounds); mRotation = rotation; mDividerSnapAlgorithm = getSnapAlgorithm(mContext, mRootBounds, null); initDividerPosition(mTempRect); + updateInvisibleRect(); return true; } @@ -270,28 +342,35 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange updateBounds(mDividePosition); } - /** Updates recording bounds of divider window and both of the splits. */ private void updateBounds(int position) { - mDividerBounds.set(mRootBounds); - mBounds1.set(mRootBounds); - mBounds2.set(mRootBounds); + updateBounds(position, mBounds1, mBounds2, mDividerBounds, true /* setEffectBounds */); + } + + /** Updates recording bounds of divider window and both of the splits. */ + private void updateBounds(int position, Rect bounds1, Rect bounds2, Rect dividerBounds, + boolean setEffectBounds) { + dividerBounds.set(mRootBounds); + bounds1.set(mRootBounds); + bounds2.set(mRootBounds); final boolean isLandscape = isLandscape(mRootBounds); if (isLandscape) { position += mRootBounds.left; - mDividerBounds.left = position - mDividerInsets; - mDividerBounds.right = mDividerBounds.left + mDividerWindowWidth; - mBounds1.right = position; - mBounds2.left = mBounds1.right + mDividerSize; + dividerBounds.left = position - mDividerInsets; + dividerBounds.right = dividerBounds.left + mDividerWindowWidth; + bounds1.right = position; + bounds2.left = bounds1.right + mDividerSize; } else { position += mRootBounds.top; - mDividerBounds.top = position - mDividerInsets; - mDividerBounds.bottom = mDividerBounds.top + mDividerWindowWidth; - mBounds1.bottom = position; - mBounds2.top = mBounds1.bottom + mDividerSize; + dividerBounds.top = position - mDividerInsets; + dividerBounds.bottom = dividerBounds.top + mDividerWindowWidth; + bounds1.bottom = position; + bounds2.top = bounds1.bottom + mDividerSize; + } + DockedDividerUtils.sanitizeStackBounds(bounds1, true /** topLeft */); + DockedDividerUtils.sanitizeStackBounds(bounds2, false /** topLeft */); + if (setEffectBounds) { + mSurfaceEffectPolicy.applyDividerPosition(position, isLandscape); } - DockedDividerUtils.sanitizeStackBounds(mBounds1, true /** topLeft */); - DockedDividerUtils.sanitizeStackBounds(mBounds2, false /** topLeft */); - mSurfaceEffectPolicy.applyDividerPosition(position, isLandscape); } /** Inflates {@link DividerView} on the root surface. */ @@ -349,6 +428,13 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange mFreezeDividerWindow = freezeDividerWindow; } + /** Update current layout as divider put on start or end position. */ + public void setDividerAtBorder(boolean start) { + final int pos = start ? mDividerSnapAlgorithm.getDismissStartTarget().position + : mDividerSnapAlgorithm.getDismissEndTarget().position; + setDividePosition(pos, false /* applyLayoutChange */); + } + /** * Updates bounds with the passing position. Usually used to update recording bounds while * performing animation or dragging divider bar to resize the splits. @@ -393,20 +479,31 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange public void snapToTarget(int currentPosition, DividerSnapAlgorithm.SnapTarget snapTarget) { switch (snapTarget.flag) { case FLAG_DISMISS_START: - flingDividePosition(currentPosition, snapTarget.position, - () -> mSplitLayoutHandler.onSnappedToDismiss(false /* bottomOrRight */)); + flingDividePosition(currentPosition, snapTarget.position, FLING_RESIZE_DURATION, + () -> mSplitLayoutHandler.onSnappedToDismiss(false /* bottomOrRight */, + EXIT_REASON_DRAG_DIVIDER)); break; case FLAG_DISMISS_END: - flingDividePosition(currentPosition, snapTarget.position, - () -> mSplitLayoutHandler.onSnappedToDismiss(true /* bottomOrRight */)); + flingDividePosition(currentPosition, snapTarget.position, FLING_RESIZE_DURATION, + () -> mSplitLayoutHandler.onSnappedToDismiss(true /* bottomOrRight */, + EXIT_REASON_DRAG_DIVIDER)); break; default: - flingDividePosition(currentPosition, snapTarget.position, + flingDividePosition(currentPosition, snapTarget.position, FLING_RESIZE_DURATION, () -> setDividePosition(snapTarget.position, true /* applyLayoutChange */)); break; } } + void onStartDragging() { + InteractionJankMonitorUtils.beginTracing(CUJ_SPLIT_SCREEN_RESIZE, mContext, + getDividerLeash(), null /* tag */); + } + + void onDraggingCancelled() { + InteractionJankMonitorUtils.cancelTracing(CUJ_SPLIT_SCREEN_RESIZE); + } + void onDoubleTappedDivider() { mSplitLayoutHandler.onDoubleTappedDivider(); } @@ -423,28 +520,58 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange private DividerSnapAlgorithm getSnapAlgorithm(Context context, Rect rootBounds, @Nullable Rect stableInsets) { final boolean isLandscape = isLandscape(rootBounds); + final Rect insets = stableInsets != null ? stableInsets : getDisplayInsets(context); + + // Make split axis insets value same as the larger one to avoid bounds1 and bounds2 + // have difference for avoiding size-compat mode when switching unresizable apps in + // landscape while they are letterboxed. + if (!isLandscape) { + final int largerInsets = Math.max(insets.top, insets.bottom); + insets.set(insets.left, largerInsets, insets.right, largerInsets); + } + return new DividerSnapAlgorithm( context.getResources(), rootBounds.width(), rootBounds.height(), mDividerSize, !isLandscape, - stableInsets != null ? stableInsets : getDisplayInsets(context), + insets, isLandscape ? DOCKED_LEFT : DOCKED_TOP /* dockSide */); } + /** Fling divider from current position to end or start position then exit */ + public void flingDividerToDismiss(boolean toEnd, int reason) { + final int target = toEnd ? mDividerSnapAlgorithm.getDismissEndTarget().position + : mDividerSnapAlgorithm.getDismissStartTarget().position; + flingDividePosition(getDividePosition(), target, FLING_EXIT_DURATION, + () -> mSplitLayoutHandler.onSnappedToDismiss(toEnd, reason)); + } + + /** Fling divider from current position to center position. */ + public void flingDividerToCenter() { + final int pos = mDividerSnapAlgorithm.getMiddleTarget().position; + flingDividePosition(getDividePosition(), pos, FLING_ENTER_DURATION, + () -> setDividePosition(pos, true /* applyLayoutChange */)); + } + @VisibleForTesting - void flingDividePosition(int from, int to, @Nullable Runnable flingFinishedCallback) { + void flingDividePosition(int from, int to, int duration, + @Nullable Runnable flingFinishedCallback) { if (from == to) { // No animation run, still callback to stop resizing. mSplitLayoutHandler.onLayoutSizeChanged(this); + + if (flingFinishedCallback != null) { + flingFinishedCallback.run(); + } + InteractionJankMonitorUtils.endTracing( + CUJ_SPLIT_SCREEN_RESIZE); return; } - InteractionJankMonitorUtils.beginTracing(InteractionJankMonitor.CUJ_SPLIT_SCREEN_RESIZE, - mSplitWindowManager.getDividerView(), "Divider fling"); ValueAnimator animator = ValueAnimator .ofInt(from, to) - .setDuration(250); + .setDuration(duration); animator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN); animator.addUpdateListener( animation -> updateDivideBounds((int) animation.getAnimatedValue())); @@ -455,7 +582,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange flingFinishedCallback.run(); } InteractionJankMonitorUtils.endTracing( - InteractionJankMonitor.CUJ_SPLIT_SCREEN_RESIZE); + CUJ_SPLIT_SCREEN_RESIZE); } @Override @@ -466,6 +593,86 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange animator.start(); } + /** Swich both surface position with animation. */ + public void splitSwitching(SurfaceControl.Transaction t, SurfaceControl leash1, + SurfaceControl leash2, Runnable finishCallback) { + final boolean isLandscape = isLandscape(); + final Rect insets = getDisplayInsets(mContext); + insets.set(isLandscape ? insets.left : 0, isLandscape ? 0 : insets.top, + isLandscape ? insets.right : 0, isLandscape ? 0 : insets.bottom); + + final int dividerPos = mDividerSnapAlgorithm.calculateNonDismissingSnapTarget( + isLandscape ? 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, + 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); + // DO NOT move to insets area for smooth animation. + distBounds1.set(distBounds1.left, distBounds1.top, + distBounds1.right - insets.right, distBounds1.bottom - insets.bottom); + distBounds2.set(distBounds2.left + insets.left, distBounds2.top + insets.top, + distBounds2.right, distBounds2.bottom); + + ValueAnimator animator1 = moveSurface(t, leash1, getRefBounds1(), distBounds1, + false /* alignStart */); + ValueAnimator animator2 = moveSurface(t, leash2, getRefBounds2(), distBounds2, + true /* alignStart */); + ValueAnimator animator3 = moveSurface(t, getDividerLeash(), getRefDividerBounds(), + distDividerBounds, true /* alignStart */); + + AnimatorSet set = new AnimatorSet(); + set.playTogether(animator1, animator2, animator3); + set.setDuration(FLING_SWITCH_DURATION); + set.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mDividePosition = dividerPos; + updateBounds(mDividePosition); + finishCallback.run(); + } + }); + set.start(); + } + + private ValueAnimator moveSurface(SurfaceControl.Transaction t, SurfaceControl leash, + Rect start, Rect end, boolean alignStart) { + 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(); + ValueAnimator animator = ValueAnimator.ofFloat(0, 1); + animator.addUpdateListener(animation -> { + if (leash == null) return; + + 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 (alignStart) { + t.setPosition(leash, distX, distY); + t.setWindowCrop(leash, width, height); + } else { + final int offsetX = width - tempStart.width(); + final int offsetY = height - tempStart.height(); + t.setPosition(leash, distX + offsetX, distY + offsetY); + mTempRect.set(0, 0, width, height); + mTempRect.offsetTo(-offsetX, -offsetY); + t.setCrop(leash, mTempRect); + } + t.apply(); + }); + return animator; + } + private static Rect getDisplayInsets(Context context) { return context.getSystemService(WindowManager.class) .getMaximumWindowMetrics() @@ -504,15 +711,15 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange boolean applyResizingOffset) { final SurfaceControl dividerLeash = getDividerLeash(); if (dividerLeash != null) { - mTempRect.set(getRefDividerBounds()); + getRefDividerBounds(mTempRect); t.setPosition(dividerLeash, mTempRect.left, mTempRect.top); // Resets layer of divider bar to make sure it is always on top. t.setLayer(dividerLeash, Integer.MAX_VALUE); } - mTempRect.set(getRefBounds1()); + getRefBounds1(mTempRect); t.setPosition(leash1, mTempRect.left, mTempRect.top) .setWindowCrop(leash1, mTempRect.width(), mTempRect.height()); - mTempRect.set(getRefBounds2()); + getRefBounds2(mTempRect); t.setPosition(leash2, mTempRect.left, mTempRect.top) .setWindowCrop(leash2, mTempRect.width(), mTempRect.height()); @@ -560,31 +767,23 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange ActivityManager.RunningTaskInfo taskInfo1, ActivityManager.RunningTaskInfo taskInfo2) { if (offsetX == 0 && offsetY == 0) { wct.setBounds(taskInfo1.token, mBounds1); - wct.setAppBounds(taskInfo1.token, null); wct.setScreenSizeDp(taskInfo1.token, SCREEN_WIDTH_DP_UNDEFINED, SCREEN_HEIGHT_DP_UNDEFINED); wct.setBounds(taskInfo2.token, mBounds2); - wct.setAppBounds(taskInfo2.token, null); wct.setScreenSizeDp(taskInfo2.token, SCREEN_WIDTH_DP_UNDEFINED, SCREEN_HEIGHT_DP_UNDEFINED); } else { - mTempRect.set(taskInfo1.configuration.windowConfiguration.getBounds()); + getBounds1(mTempRect); mTempRect.offset(offsetX, offsetY); wct.setBounds(taskInfo1.token, mTempRect); - mTempRect.set(taskInfo1.configuration.windowConfiguration.getAppBounds()); - mTempRect.offset(offsetX, offsetY); - wct.setAppBounds(taskInfo1.token, mTempRect); wct.setScreenSizeDp(taskInfo1.token, taskInfo1.configuration.screenWidthDp, taskInfo1.configuration.screenHeightDp); - mTempRect.set(taskInfo2.configuration.windowConfiguration.getBounds()); + getBounds2(mTempRect); mTempRect.offset(offsetX, offsetY); wct.setBounds(taskInfo2.token, mTempRect); - mTempRect.set(taskInfo2.configuration.windowConfiguration.getAppBounds()); - mTempRect.offset(offsetX, offsetY); - wct.setAppBounds(taskInfo2.token, mTempRect); wct.setScreenSizeDp(taskInfo2.token, taskInfo2.configuration.screenWidthDp, taskInfo2.configuration.screenHeightDp); @@ -602,7 +801,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange public interface SplitLayoutHandler { /** Calls when dismissing split. */ - void onSnappedToDismiss(boolean snappedToEnd); + void onSnappedToDismiss(boolean snappedToEnd, int reason); /** * Calls when resizing the split bounds. @@ -971,16 +1170,16 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange boolean adjusted = false; if (mYOffsetForIme != 0) { if (dividerLeash != null) { - mTempRect.set(mDividerBounds); + getRefDividerBounds(mTempRect); mTempRect.offset(0, mYOffsetForIme); t.setPosition(dividerLeash, mTempRect.left, mTempRect.top); } - mTempRect.set(mBounds1); + getRefBounds1(mTempRect); mTempRect.offset(0, mYOffsetForIme); t.setPosition(leash1, mTempRect.left, mTempRect.top); - mTempRect.set(mBounds2); + getRefBounds2(mTempRect); mTempRect.offset(0, mYOffsetForIme); t.setPosition(leash2, mTempRect.left, mTempRect.top); adjusted = true; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenConstants.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenConstants.java index 9b614875119b..b8204d013105 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenConstants.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenConstants.java @@ -15,6 +15,12 @@ */ package com.android.wm.shell.common.split; +import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; +import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; +import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; +import static android.window.TransitionInfo.FLAG_FIRST_CUSTOM; + import android.annotation.IntDef; /** Helper utility class of methods and constants that are available to be imported in Launcher. */ @@ -44,4 +50,13 @@ public class SplitScreenConstants { }) public @interface SplitPosition { } + + public static final int[] CONTROLLED_ACTIVITY_TYPES = {ACTIVITY_TYPE_STANDARD}; + public static final int[] CONTROLLED_WINDOWING_MODES = + {WINDOWING_MODE_FULLSCREEN, WINDOWING_MODE_UNDEFINED}; + public static final int[] CONTROLLED_WINDOWING_MODES_WHEN_ACTIVE = + {WINDOWING_MODE_FULLSCREEN, WINDOWING_MODE_UNDEFINED, WINDOWING_MODE_MULTI_WINDOW}; + + /** Flag applied to a transition change to identify it as a divider bar for animation. */ + public static final int FLAG_IS_DIVIDER_BAR = FLAG_FIRST_CUSTOM; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUI.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUI.java deleted file mode 100644 index b87cf47dd93f..000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUI.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright (C) 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.wm.shell.compatui; - -import com.android.wm.shell.common.annotations.ExternalThread; - -/** - * Interface to engage compat UI. - */ -@ExternalThread -public interface CompatUI { - /** - * Called when the keyguard showing state changes. Removes all compat UIs if the - * keyguard is now showing. - * - * <p>Note that if the keyguard is occluded it will also be considered showing. - * - * @param showing indicates if the keyguard is now showing. - */ - void onKeyguardShowingChanged(boolean showing); -} 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 99b32a677abe..235fd9c469ea 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 @@ -39,9 +39,11 @@ import com.android.wm.shell.common.DisplayInsetsController.OnInsetsChangedListen 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.common.annotations.ExternalThread; import com.android.wm.shell.compatui.CompatUIWindowManager.CompatUIHintsState; import com.android.wm.shell.compatui.letterboxedu.LetterboxEduWindowManager; +import com.android.wm.shell.sysui.KeyguardChangeListener; +import com.android.wm.shell.sysui.ShellController; +import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.transition.Transitions; import java.lang.ref.WeakReference; @@ -58,7 +60,7 @@ import dagger.Lazy; * activities are in compatibility mode. */ public class CompatUIController implements OnDisplaysChangedListener, - DisplayImeController.ImePositionProcessor { + DisplayImeController.ImePositionProcessor, KeyguardChangeListener { /** Callback for compat UI interaction. */ public interface CompatUICallback { @@ -100,13 +102,13 @@ public class CompatUIController implements OnDisplaysChangedListener, private final SparseArray<WeakReference<Context>> mDisplayContextCache = new SparseArray<>(0); private final Context mContext; + private final ShellController mShellController; private final DisplayController mDisplayController; private final DisplayInsetsController mDisplayInsetsController; private final DisplayImeController mImeController; private final SyncTransactionQueue mSyncQueue; private final ShellExecutor mMainExecutor; private final Lazy<Transitions> mTransitionsLazy; - private final CompatUIImpl mImpl = new CompatUIImpl(); private CompatUICallback mCallback; @@ -118,6 +120,8 @@ public class CompatUIController implements OnDisplaysChangedListener, private boolean mKeyguardShowing; public CompatUIController(Context context, + ShellInit shellInit, + ShellController shellController, DisplayController displayController, DisplayInsetsController displayInsetsController, DisplayImeController imeController, @@ -125,20 +129,21 @@ public class CompatUIController implements OnDisplaysChangedListener, ShellExecutor mainExecutor, Lazy<Transitions> transitionsLazy) { mContext = context; + mShellController = shellController; mDisplayController = displayController; mDisplayInsetsController = displayInsetsController; mImeController = imeController; mSyncQueue = syncQueue; mMainExecutor = mainExecutor; mTransitionsLazy = transitionsLazy; - mDisplayController.addDisplayWindowListener(this); - mImeController.addPositionProcessor(this); mCompatUIHintsState = new CompatUIHintsState(); + shellInit.addInitCallback(this::onInit, this); } - /** Returns implementation of {@link CompatUI}. */ - public CompatUI asCompatUI() { - return mImpl; + private void onInit() { + mShellController.addKeyguardChangeListener(this); + mDisplayController.addDisplayWindowListener(this); + mImeController.addPositionProcessor(this); } /** Sets the callback for UI interactions. */ @@ -223,9 +228,10 @@ public class CompatUIController implements OnDisplaysChangedListener, layout -> layout.updateVisibility(showOnDisplay(displayId))); } - @VisibleForTesting - void onKeyguardShowingChanged(boolean showing) { - mKeyguardShowing = showing; + @Override + public void onKeyguardVisibilityChanged(boolean visible, boolean occluded, + boolean animatingDismiss) { + mKeyguardShowing = visible; // Hide the compat UIs when keyguard is showing. forAllLayouts(layout -> layout.updateVisibility(showOnDisplay(layout.getDisplayId()))); } @@ -373,19 +379,6 @@ public class CompatUIController implements OnDisplaysChangedListener, } } - /** - * The interface for calls from outside the Shell, within the host process. - */ - @ExternalThread - private class CompatUIImpl implements CompatUI { - @Override - public void onKeyguardShowingChanged(boolean showing) { - mMainExecutor.execute(() -> { - CompatUIController.this.onKeyguardShowingChanged(showing); - }); - } - } - /** An implementation of {@link OnInsetsChangedListener} for a given display id. */ private class PerDisplayOnInsetsChangedListener implements OnInsetsChangedListener { final int mDisplayId; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/DynamicOverride.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/DynamicOverride.java index 806f795d1015..10b121bbc32c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/DynamicOverride.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/DynamicOverride.java @@ -92,6 +92,8 @@ import javax.inject.Qualifier; * * For example, this uses the same setup as above, but the interface provided (if bound) is used * otherwise the default is created: + * + * BaseModule: * @BindsOptionalOf * @DynamicOverride * abstract Interface dynamicInterface(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/README.txt b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/README.txt deleted file mode 100644 index 1cd69edf7cd2..000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/README.txt +++ /dev/null @@ -1,13 +0,0 @@ -The dagger modules in this directory can be included by the host SysUI using the Shell library for -explicity injection of Shell components. Apps using this library are not required to use these -dagger modules for setup, but it is recommended for them to include them as needed. - -The modules are currently inherited as such: - -+- WMShellBaseModule (common shell features across SysUI) - | - +- WMShellModule (handheld) - | - +- TvPipModule (tv pip) - | - +- TvWMShellModule (tv)
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/ShellCreateTrigger.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/ShellCreateTrigger.java new file mode 100644 index 000000000000..482b19983850 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/ShellCreateTrigger.java @@ -0,0 +1,38 @@ +/* + * 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.dagger; + +import java.lang.annotation.Documented; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import javax.inject.Qualifier; + +/** + * An annotation to specifically mark the provider that is triggering the creation of independent + * shell components that are not created as a part of the dependencies for interfaces passed to + * SysUI. + * + * TODO: This will be removed once we have a more explicit method for specifying components to start + * with SysUI + */ +@Documented +@Inherited +@Qualifier +@Retention(RetentionPolicy.RUNTIME) +public @interface ShellCreateTrigger {} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/ShellCreateTriggerOverride.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/ShellCreateTriggerOverride.java new file mode 100644 index 000000000000..31c678968a25 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/ShellCreateTriggerOverride.java @@ -0,0 +1,38 @@ +/* + * 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.dagger; + +import java.lang.annotation.Documented; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import javax.inject.Qualifier; + +/** + * An annotation for non-base modules to specifically mark the provider that is triggering the + * creation of independent shell components that are not created as a part of the dependencies for + * interfaces passed to SysUI. + * + * TODO: This will be removed once we have a more explicit method for specifying components to start + * with SysUI + */ +@Documented +@Inherited +@Qualifier +@Retention(RetentionPolicy.RUNTIME) +public @interface ShellCreateTriggerOverride {} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvPipModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvPipModule.java index 1ea5e21a2c1e..8022e9b1cd81 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvPipModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvPipModule.java @@ -48,6 +48,8 @@ import com.android.wm.shell.pip.tv.TvPipNotificationController; import com.android.wm.shell.pip.tv.TvPipTaskOrganizer; import com.android.wm.shell.pip.tv.TvPipTransition; import com.android.wm.shell.splitscreen.SplitScreenController; +import com.android.wm.shell.sysui.ShellController; +import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.transition.Transitions; import java.util.Optional; @@ -64,6 +66,8 @@ public abstract class TvPipModule { @Provides static Optional<Pip> providePip( Context context, + ShellInit shellInit, + ShellController shellController, TvPipBoundsState tvPipBoundsState, TvPipBoundsAlgorithm tvPipBoundsAlgorithm, TvPipBoundsController tvPipBoundsController, @@ -81,6 +85,8 @@ public abstract class TvPipModule { return Optional.of( TvPipController.create( context, + shellInit, + shellController, tvPipBoundsState, tvPipBoundsAlgorithm, tvPipBoundsController, @@ -135,12 +141,14 @@ public abstract class TvPipModule { @WMSingleton @Provides static PipTransitionController provideTvPipTransition( - Transitions transitions, ShellTaskOrganizer shellTaskOrganizer, + ShellInit shellInit, + ShellTaskOrganizer shellTaskOrganizer, + Transitions transitions, PipAnimationController pipAnimationController, TvPipBoundsAlgorithm tvPipBoundsAlgorithm, TvPipBoundsState tvPipBoundsState, TvPipMenuController pipMenuController) { - return new TvPipTransition(tvPipBoundsState, pipMenuController, - tvPipBoundsAlgorithm, pipAnimationController, transitions, shellTaskOrganizer); + return new TvPipTransition(shellInit, shellTaskOrganizer, transitions, tvPipBoundsState, + pipMenuController, tvPipBoundsAlgorithm, pipAnimationController); } @WMSingleton 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 db6131a17114..c25bbbf06dda 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 @@ -24,22 +24,20 @@ import android.content.pm.PackageManager; import android.os.Handler; import android.os.SystemProperties; import android.view.IWindowManager; +import android.view.WindowManager; import com.android.internal.logging.UiEventLogger; import com.android.launcher3.icons.IconProvider; +import com.android.wm.shell.ProtoLogController; +import com.android.wm.shell.R; import com.android.wm.shell.RootDisplayAreaOrganizer; import com.android.wm.shell.RootTaskDisplayAreaOrganizer; -import com.android.wm.shell.ShellCommandHandler; -import com.android.wm.shell.ShellCommandHandlerImpl; -import com.android.wm.shell.ShellInit; -import com.android.wm.shell.ShellInitImpl; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.TaskViewFactory; import com.android.wm.shell.TaskViewFactoryController; import com.android.wm.shell.TaskViewTransitions; import com.android.wm.shell.WindowManagerShellWrapper; -import com.android.wm.shell.apppairs.AppPairs; -import com.android.wm.shell.apppairs.AppPairsController; +import com.android.wm.shell.activityembedding.ActivityEmbeddingController; import com.android.wm.shell.back.BackAnimation; import com.android.wm.shell.back.BackAnimationController; import com.android.wm.shell.bubbles.BubbleController; @@ -58,20 +56,20 @@ import com.android.wm.shell.common.annotations.ShellAnimationThread; import com.android.wm.shell.common.annotations.ShellBackgroundThread; import com.android.wm.shell.common.annotations.ShellMainThread; import com.android.wm.shell.common.annotations.ShellSplashscreenThread; -import com.android.wm.shell.compatui.CompatUI; import com.android.wm.shell.compatui.CompatUIController; +import com.android.wm.shell.desktopmode.DesktopMode; +import com.android.wm.shell.desktopmode.DesktopModeController; +import com.android.wm.shell.desktopmode.DesktopModeStatus; +import com.android.wm.shell.desktopmode.DesktopModeTaskRepository; import com.android.wm.shell.displayareahelper.DisplayAreaHelper; import com.android.wm.shell.displayareahelper.DisplayAreaHelperController; -import com.android.wm.shell.draganddrop.DragAndDrop; import com.android.wm.shell.draganddrop.DragAndDropController; -import com.android.wm.shell.freeform.FreeformTaskListener; +import com.android.wm.shell.floating.FloatingTasks; +import com.android.wm.shell.floating.FloatingTasksController; +import com.android.wm.shell.freeform.FreeformComponents; import com.android.wm.shell.fullscreen.FullscreenTaskListener; -import com.android.wm.shell.fullscreen.FullscreenUnfoldController; -import com.android.wm.shell.hidedisplaycutout.HideDisplayCutout; import com.android.wm.shell.hidedisplaycutout.HideDisplayCutoutController; import com.android.wm.shell.kidsmode.KidsModeTaskOrganizer; -import com.android.wm.shell.legacysplitscreen.LegacySplitScreen; -import com.android.wm.shell.legacysplitscreen.LegacySplitScreenController; import com.android.wm.shell.onehanded.OneHanded; import com.android.wm.shell.onehanded.OneHandedController; import com.android.wm.shell.pip.Pip; @@ -87,12 +85,16 @@ import com.android.wm.shell.startingsurface.StartingSurface; import com.android.wm.shell.startingsurface.StartingWindowController; import com.android.wm.shell.startingsurface.StartingWindowTypeAlgorithm; import com.android.wm.shell.startingsurface.phone.PhoneStartingWindowTypeAlgorithm; -import com.android.wm.shell.tasksurfacehelper.TaskSurfaceHelper; -import com.android.wm.shell.tasksurfacehelper.TaskSurfaceHelperController; +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.ShellInterface; import com.android.wm.shell.transition.ShellTransitions; import com.android.wm.shell.transition.Transitions; import com.android.wm.shell.unfold.ShellUnfoldProgressProvider; +import com.android.wm.shell.unfold.UnfoldAnimationController; import com.android.wm.shell.unfold.UnfoldTransitionHandler; +import com.android.wm.shell.windowdecor.WindowDecorViewModel; import java.util.Optional; @@ -120,38 +122,34 @@ public abstract class WMShellBaseModule { @WMSingleton @Provides static DisplayController provideDisplayController(Context context, - IWindowManager wmService, @ShellMainThread ShellExecutor mainExecutor) { - return new DisplayController(context, wmService, mainExecutor); + IWindowManager wmService, + ShellInit shellInit, + @ShellMainThread ShellExecutor mainExecutor) { + return new DisplayController(context, wmService, shellInit, mainExecutor); } @WMSingleton @Provides - static DisplayInsetsController provideDisplayInsetsController( IWindowManager wmService, + static DisplayInsetsController provideDisplayInsetsController(IWindowManager wmService, + ShellInit shellInit, DisplayController displayController, @ShellMainThread ShellExecutor mainExecutor) { - return new DisplayInsetsController(wmService, displayController, mainExecutor); + return new DisplayInsetsController(wmService, shellInit, displayController, + mainExecutor); } - // Workaround for dynamic overriding with a default implementation, see {@link DynamicOverride} - @BindsOptionalOf - @DynamicOverride - abstract DisplayImeController optionalDisplayImeController(); - @WMSingleton @Provides static DisplayImeController provideDisplayImeController( - @DynamicOverride Optional<DisplayImeController> overrideDisplayImeController, IWindowManager wmService, + ShellInit shellInit, DisplayController displayController, DisplayInsetsController displayInsetsController, - @ShellMainThread ShellExecutor mainExecutor, - TransactionPool transactionPool + TransactionPool transactionPool, + @ShellMainThread ShellExecutor mainExecutor ) { - if (overrideDisplayImeController.isPresent()) { - return overrideDisplayImeController.get(); - } - return new DisplayImeController(wmService, displayController, displayInsetsController, - mainExecutor, transactionPool); + return new DisplayImeController(wmService, shellInit, displayController, + displayInsetsController, transactionPool, mainExecutor); } @WMSingleton @@ -163,56 +161,64 @@ public abstract class WMShellBaseModule { @WMSingleton @Provides static DragAndDropController provideDragAndDropController(Context context, - DisplayController displayController, UiEventLogger uiEventLogger, - IconProvider iconProvider, @ShellMainThread ShellExecutor mainExecutor) { - return new DragAndDropController(context, displayController, uiEventLogger, iconProvider, - mainExecutor); - } - - @WMSingleton - @Provides - static Optional<DragAndDrop> provideDragAndDrop(DragAndDropController dragAndDropController) { - return Optional.of(dragAndDropController.asDragAndDrop()); + ShellInit shellInit, + ShellController shellController, + DisplayController displayController, + UiEventLogger uiEventLogger, + IconProvider iconProvider, + @ShellMainThread ShellExecutor mainExecutor) { + return new DragAndDropController(context, shellInit, shellController, displayController, + uiEventLogger, iconProvider, mainExecutor); } @WMSingleton @Provides - static ShellTaskOrganizer provideShellTaskOrganizer(@ShellMainThread ShellExecutor mainExecutor, + static ShellTaskOrganizer provideShellTaskOrganizer( Context context, + ShellInit shellInit, + ShellCommandHandler shellCommandHandler, CompatUIController compatUI, - Optional<RecentTasksController> recentTasksOptional + Optional<UnfoldAnimationController> unfoldAnimationController, + Optional<RecentTasksController> recentTasksOptional, + @ShellMainThread ShellExecutor mainExecutor ) { - return new ShellTaskOrganizer(mainExecutor, context, compatUI, recentTasksOptional); + if (!context.getResources().getBoolean(R.bool.config_registerShellTaskOrganizerOnInit)) { + // TODO(b/238217847): Force override shell init if registration is disabled + shellInit = new ShellInit(mainExecutor); + } + return new ShellTaskOrganizer(shellInit, shellCommandHandler, compatUI, + unfoldAnimationController, recentTasksOptional, mainExecutor); } @WMSingleton @Provides static KidsModeTaskOrganizer provideKidsModeTaskOrganizer( - @ShellMainThread ShellExecutor mainExecutor, - @ShellMainThread Handler mainHandler, Context context, + ShellInit shellInit, + ShellCommandHandler shellCommandHandler, SyncTransactionQueue syncTransactionQueue, DisplayController displayController, DisplayInsetsController displayInsetsController, - Optional<RecentTasksController> recentTasksOptional + Optional<UnfoldAnimationController> unfoldAnimationController, + Optional<RecentTasksController> recentTasksOptional, + @ShellMainThread ShellExecutor mainExecutor, + @ShellMainThread Handler mainHandler ) { - return new KidsModeTaskOrganizer(mainExecutor, mainHandler, context, syncTransactionQueue, - displayController, displayInsetsController, recentTasksOptional); - } - - @WMSingleton - @Provides static Optional<CompatUI> provideCompatUI(CompatUIController compatUIController) { - return Optional.of(compatUIController.asCompatUI()); + return new KidsModeTaskOrganizer(context, shellInit, shellCommandHandler, + syncTransactionQueue, displayController, displayInsetsController, + unfoldAnimationController, recentTasksOptional, mainExecutor, mainHandler); } @WMSingleton @Provides static CompatUIController provideCompatUIController(Context context, + ShellInit shellInit, + ShellController shellController, DisplayController displayController, DisplayInsetsController displayInsetsController, DisplayImeController imeController, SyncTransactionQueue syncQueue, @ShellMainThread ShellExecutor mainExecutor, Lazy<Transitions> transitionsLazy) { - return new CompatUIController(context, displayController, displayInsetsController, - imeController, syncQueue, mainExecutor, transitionsLazy); + return new CompatUIController(context, shellInit, shellController, displayController, + displayInsetsController, imeController, syncQueue, mainExecutor, transitionsLazy); } @WMSingleton @@ -267,6 +273,22 @@ public abstract class WMShellBaseModule { return backAnimationController.map(BackAnimationController::getBackAnimationImpl); } + @WMSingleton + @Provides + static Optional<BackAnimationController> provideBackAnimationController( + Context context, + ShellInit shellInit, + @ShellMainThread ShellExecutor shellExecutor, + @ShellBackgroundThread Handler backgroundHandler + ) { + if (BackAnimationController.IS_ENABLED) { + return Optional.of( + new BackAnimationController(shellInit, shellExecutor, backgroundHandler, + context)); + } + return Optional.empty(); + } + // // Bubbles (optional feature) // @@ -287,24 +309,33 @@ public abstract class WMShellBaseModule { // Workaround for dynamic overriding with a default implementation, see {@link DynamicOverride} @BindsOptionalOf @DynamicOverride - abstract FullscreenTaskListener optionalFullscreenTaskListener(); + abstract FullscreenTaskListener<?> optionalFullscreenTaskListener(); @WMSingleton @Provides - static FullscreenTaskListener provideFullscreenTaskListener( - @DynamicOverride Optional<FullscreenTaskListener> fullscreenTaskListener, + static FullscreenTaskListener<?> provideFullscreenTaskListener( + @DynamicOverride Optional<FullscreenTaskListener<?>> fullscreenTaskListener, + ShellInit shellInit, + ShellTaskOrganizer shellTaskOrganizer, SyncTransactionQueue syncQueue, - Optional<FullscreenUnfoldController> optionalFullscreenUnfoldController, - Optional<RecentTasksController> recentTasksOptional) { + Optional<RecentTasksController> recentTasksOptional, + Optional<WindowDecorViewModel<?>> windowDecorViewModelOptional) { if (fullscreenTaskListener.isPresent()) { return fullscreenTaskListener.get(); } else { - return new FullscreenTaskListener(syncQueue, optionalFullscreenUnfoldController, - recentTasksOptional); + return new FullscreenTaskListener(shellInit, shellTaskOrganizer, syncQueue, + recentTasksOptional, windowDecorViewModelOptional); } } // + // Window Decoration + // + + @BindsOptionalOf + abstract WindowDecorViewModel<?> optionalWindowDecorViewModel(); + + // // Unfold transition // @@ -314,31 +345,33 @@ public abstract class WMShellBaseModule { // Workaround for dynamic overriding with a default implementation, see {@link DynamicOverride} @BindsOptionalOf @DynamicOverride - abstract FullscreenUnfoldController optionalFullscreenUnfoldController(); + abstract UnfoldAnimationController optionalUnfoldController(); @WMSingleton @Provides - static Optional<FullscreenUnfoldController> provideFullscreenUnfoldController( - @DynamicOverride Optional<FullscreenUnfoldController> fullscreenUnfoldController, + static Optional<UnfoldAnimationController> provideUnfoldController( + @DynamicOverride Lazy<Optional<UnfoldAnimationController>> + fullscreenUnfoldController, Optional<ShellUnfoldProgressProvider> progressProvider) { if (progressProvider.isPresent() && progressProvider.get() != ShellUnfoldProgressProvider.NO_PROVIDER) { - return fullscreenUnfoldController; + return fullscreenUnfoldController.get(); } return Optional.empty(); } + @BindsOptionalOf + @DynamicOverride + abstract UnfoldTransitionHandler optionalUnfoldTransitionHandler(); + @WMSingleton @Provides static Optional<UnfoldTransitionHandler> provideUnfoldTransitionHandler( Optional<ShellUnfoldProgressProvider> progressProvider, - TransactionPool transactionPool, - Transitions transitions, - @ShellMainThread ShellExecutor executor) { - if (progressProvider.isPresent()) { - return Optional.of( - new UnfoldTransitionHandler(progressProvider.get(), transactionPool, executor, - transitions)); + @DynamicOverride Lazy<Optional<UnfoldTransitionHandler>> handler) { + if (progressProvider.isPresent() + && progressProvider.get() != ShellUnfoldProgressProvider.NO_PROVIDER) { + return handler.get(); } return Optional.empty(); } @@ -350,15 +383,15 @@ public abstract class WMShellBaseModule { // Workaround for dynamic overriding with a default implementation, see {@link DynamicOverride} @BindsOptionalOf @DynamicOverride - abstract FreeformTaskListener optionalFreeformTaskListener(); + abstract FreeformComponents optionalFreeformComponents(); @WMSingleton @Provides - static Optional<FreeformTaskListener> provideFreeformTaskListener( - @DynamicOverride Optional<FreeformTaskListener> freeformTaskListener, + static Optional<FreeformComponents> provideFreeformComponents( + @DynamicOverride Optional<FreeformComponents> freeformComponents, Context context) { - if (FreeformTaskListener.isFreeformEnabled(context)) { - return freeformTaskListener; + if (FreeformComponents.isFreeformEnabled(context)) { + return freeformComponents; } return Optional.empty(); } @@ -369,17 +402,15 @@ public abstract class WMShellBaseModule { @WMSingleton @Provides - static Optional<HideDisplayCutout> provideHideDisplayCutout( - Optional<HideDisplayCutoutController> hideDisplayCutoutController) { - return hideDisplayCutoutController.map((controller) -> controller.asHideDisplayCutout()); - } - - @WMSingleton - @Provides static Optional<HideDisplayCutoutController> provideHideDisplayCutoutController(Context context, - DisplayController displayController, @ShellMainThread ShellExecutor mainExecutor) { + ShellInit shellInit, + ShellCommandHandler shellCommandHandler, + ShellController shellController, + DisplayController displayController, + @ShellMainThread ShellExecutor mainExecutor) { return Optional.ofNullable( - HideDisplayCutoutController.create(context, displayController, mainExecutor)); + HideDisplayCutoutController.create(context, shellInit, shellCommandHandler, + shellController, displayController, mainExecutor)); } // @@ -408,23 +439,6 @@ public abstract class WMShellBaseModule { } // - // Task to Surface communication - // - - @WMSingleton - @Provides - static Optional<TaskSurfaceHelper> provideTaskSurfaceHelper( - Optional<TaskSurfaceHelperController> taskSurfaceController) { - return taskSurfaceController.map((controller) -> controller.asTaskSurfaceHelper()); - } - - @Provides - static Optional<TaskSurfaceHelperController> provideTaskSurfaceHelperController( - ShellTaskOrganizer taskOrganizer, @ShellMainThread ShellExecutor mainExecutor) { - return Optional.ofNullable(new TaskSurfaceHelperController(taskOrganizer, mainExecutor)); - } - - // // Pip (optional feature) // @@ -444,8 +458,8 @@ public abstract class WMShellBaseModule { @WMSingleton @Provides - static PipSurfaceTransactionHelper providePipSurfaceTransactionHelper() { - return new PipSurfaceTransactionHelper(); + static PipSurfaceTransactionHelper providePipSurfaceTransactionHelper(Context context) { + return new PipSurfaceTransactionHelper(context); } @WMSingleton @@ -473,11 +487,17 @@ public abstract class WMShellBaseModule { @Provides static Optional<RecentTasksController> provideRecentTasksController( Context context, + ShellInit shellInit, + ShellCommandHandler shellCommandHandler, TaskStackListenerImpl taskStackListener, + ActivityTaskManager activityTaskManager, + Optional<DesktopModeTaskRepository> desktopModeTaskRepository, @ShellMainThread ShellExecutor mainExecutor ) { return Optional.ofNullable( - RecentTasksController.create(context, taskStackListener, mainExecutor)); + RecentTasksController.create(context, shellInit, shellCommandHandler, + taskStackListener, activityTaskManager, desktopModeTaskRepository, + mainExecutor)); } // @@ -492,12 +512,15 @@ public abstract class WMShellBaseModule { @WMSingleton @Provides - static Transitions provideTransitions(ShellTaskOrganizer organizer, TransactionPool pool, - DisplayController displayController, Context context, + static Transitions provideTransitions(Context context, + ShellInit shellInit, + ShellTaskOrganizer organizer, + TransactionPool pool, + DisplayController displayController, @ShellMainThread ShellExecutor mainExecutor, @ShellMainThread Handler mainHandler, @ShellAnimationThread ShellExecutor animExecutor) { - return new Transitions(organizer, pool, displayController, context, mainExecutor, + return new Transitions(context, shellInit, organizer, pool, displayController, mainExecutor, mainHandler, animExecutor); } @@ -561,29 +584,47 @@ public abstract class WMShellBaseModule { return Optional.empty(); } - // Legacy split (optional feature) + // + // Floating tasks + // @WMSingleton @Provides - static Optional<LegacySplitScreen> provideLegacySplitScreen( - Optional<LegacySplitScreenController> splitScreenController) { - return splitScreenController.map((controller) -> controller.asLegacySplitScreen()); + static Optional<FloatingTasks> provideFloatingTasks( + Optional<FloatingTasksController> floatingTaskController) { + return floatingTaskController.map((controller) -> controller.asFloatingTasks()); } - @BindsOptionalOf - abstract LegacySplitScreenController optionalLegacySplitScreenController(); - - // App Pairs (optional feature) - @WMSingleton @Provides - static Optional<AppPairs> provideAppPairs(Optional<AppPairsController> appPairsController) { - return appPairsController.map((controller) -> controller.asAppPairs()); + static Optional<FloatingTasksController> provideFloatingTasksController(Context context, + ShellInit shellInit, + ShellController shellController, + ShellCommandHandler shellCommandHandler, + Optional<BubbleController> bubbleController, + WindowManager windowManager, + ShellTaskOrganizer organizer, + TaskViewTransitions taskViewTransitions, + @ShellMainThread ShellExecutor mainExecutor, + @ShellBackgroundThread ShellExecutor bgExecutor, + SyncTransactionQueue syncQueue) { + if (FloatingTasksController.FLOATING_TASKS_ENABLED) { + return Optional.of(new FloatingTasksController(context, + shellInit, + shellController, + shellCommandHandler, + bubbleController, + windowManager, + organizer, + taskViewTransitions, + mainExecutor, + bgExecutor, + syncQueue)); + } else { + return Optional.empty(); + } } - @BindsOptionalOf - abstract AppPairsController optionalAppPairs(); - // // Starting window // @@ -598,11 +639,13 @@ public abstract class WMShellBaseModule { @WMSingleton @Provides static StartingWindowController provideStartingWindowController(Context context, + ShellInit shellInit, + ShellTaskOrganizer shellTaskOrganizer, @ShellSplashscreenThread ShellExecutor splashScreenExecutor, StartingWindowTypeAlgorithm startingWindowTypeAlgorithm, IconProvider iconProvider, TransactionPool pool) { - return new StartingWindowController(context, splashScreenExecutor, - startingWindowTypeAlgorithm, iconProvider, pool); + return new StartingWindowController(context, shellInit, shellTaskOrganizer, + splashScreenExecutor, startingWindowTypeAlgorithm, iconProvider, pool); } // Workaround for dynamic overriding with a default implementation, see {@link DynamicOverride} @@ -644,19 +687,96 @@ public abstract class WMShellBaseModule { taskViewTransitions); } + // - // Misc + // ActivityEmbedding + // + + @WMSingleton + @Provides + static Optional<ActivityEmbeddingController> provideActivityEmbeddingController( + Context context, + ShellInit shellInit, + Transitions transitions) { + return Optional.ofNullable( + ActivityEmbeddingController.create(context, shellInit, transitions)); + } + + // + // SysUI -> Shell interface // @WMSingleton @Provides - static ShellInit provideShellInit(ShellInitImpl impl) { - return impl.asShellInit(); + static ShellInterface provideShellSysuiCallbacks( + @ShellCreateTrigger Object createTrigger, + ShellController shellController) { + return shellController.asShell(); } @WMSingleton @Provides - static ShellInitImpl provideShellInitImpl(DisplayController displayController, + static ShellController provideShellController(ShellInit shellInit, + ShellCommandHandler shellCommandHandler, + @ShellMainThread ShellExecutor mainExecutor) { + return new ShellController(shellInit, shellCommandHandler, mainExecutor); + } + + // + // Desktop mode (optional feature) + // + + @WMSingleton + @Provides + static Optional<DesktopMode> provideDesktopMode( + Optional<DesktopModeController> desktopModeController) { + return desktopModeController.map(DesktopModeController::asDesktopMode); + } + + @BindsOptionalOf + @DynamicOverride + abstract DesktopModeController optionalDesktopModeController(); + + @WMSingleton + @Provides + static Optional<DesktopModeController> providesDesktopModeController( + @DynamicOverride Optional<DesktopModeController> desktopModeController) { + if (DesktopModeStatus.IS_SUPPORTED) { + return desktopModeController; + } + return Optional.empty(); + } + + @BindsOptionalOf + @DynamicOverride + abstract DesktopModeTaskRepository optionalDesktopModeTaskRepository(); + + @WMSingleton + @Provides + static Optional<DesktopModeTaskRepository> providesDesktopTaskRepository( + @DynamicOverride Optional<DesktopModeTaskRepository> desktopModeTaskRepository) { + if (DesktopModeStatus.IS_SUPPORTED) { + return desktopModeTaskRepository; + } + return Optional.empty(); + } + + // + // Misc + // + + // Workaround for dynamic overriding with a default implementation, see {@link DynamicOverride} + @BindsOptionalOf + @ShellCreateTriggerOverride + abstract Object provideIndependentShellComponentsToCreateOverride(); + + // TODO: Temporarily move dependencies to this instead of ShellInit since that is needed to add + // the callback. We will be moving to a different explicit startup mechanism in a follow- up CL. + @WMSingleton + @ShellCreateTrigger + @Provides + static Object provideIndependentShellComponentsToCreate( + DisplayController displayController, DisplayImeController displayImeController, DisplayInsetsController displayInsetsController, DragAndDropController dragAndDropController, @@ -664,75 +784,40 @@ public abstract class WMShellBaseModule { KidsModeTaskOrganizer kidsModeTaskOrganizer, Optional<BubbleController> bubblesOptional, Optional<SplitScreenController> splitScreenOptional, - Optional<AppPairsController> appPairsOptional, + Optional<Pip> pipOptional, Optional<PipTouchHandler> pipTouchHandlerOptional, - FullscreenTaskListener fullscreenTaskListener, - Optional<FullscreenUnfoldController> appUnfoldTransitionController, + FullscreenTaskListener<?> fullscreenTaskListener, + Optional<UnfoldAnimationController> unfoldAnimationController, Optional<UnfoldTransitionHandler> unfoldTransitionHandler, - Optional<FreeformTaskListener> freeformTaskListener, + Optional<FreeformComponents> freeformComponents, Optional<RecentTasksController> recentTasksOptional, + Optional<OneHandedController> oneHandedControllerOptional, + Optional<HideDisplayCutoutController> hideDisplayCutoutControllerOptional, + Optional<ActivityEmbeddingController> activityEmbeddingOptional, Transitions transitions, StartingWindowController startingWindow, - @ShellMainThread ShellExecutor mainExecutor) { - return new ShellInitImpl(displayController, - displayImeController, - displayInsetsController, - dragAndDropController, - shellTaskOrganizer, - kidsModeTaskOrganizer, - bubblesOptional, - splitScreenOptional, - appPairsOptional, - pipTouchHandlerOptional, - fullscreenTaskListener, - appUnfoldTransitionController, - unfoldTransitionHandler, - freeformTaskListener, - recentTasksOptional, - transitions, - startingWindow, - mainExecutor); + ProtoLogController protoLogController, + @ShellCreateTriggerOverride Optional<Object> overriddenCreateTrigger) { + return new Object(); } - /** - * Note, this is only optional because we currently pass this to the SysUI component scope and - * for non-primary users, we may inject a null-optional for that dependency. - */ @WMSingleton @Provides - static Optional<ShellCommandHandler> provideShellCommandHandler(ShellCommandHandlerImpl impl) { - return Optional.of(impl.asShellCommandHandler()); + static ShellInit provideShellInit(@ShellMainThread ShellExecutor mainExecutor) { + return new ShellInit(mainExecutor); } @WMSingleton @Provides - static ShellCommandHandlerImpl provideShellCommandHandlerImpl( - ShellTaskOrganizer shellTaskOrganizer, - KidsModeTaskOrganizer kidsModeTaskOrganizer, - Optional<LegacySplitScreenController> legacySplitScreenOptional, - Optional<SplitScreenController> splitScreenOptional, - Optional<Pip> pipOptional, - Optional<OneHandedController> oneHandedOptional, - Optional<HideDisplayCutoutController> hideDisplayCutout, - Optional<AppPairsController> appPairsOptional, - Optional<RecentTasksController> recentTasksOptional, - @ShellMainThread ShellExecutor mainExecutor) { - return new ShellCommandHandlerImpl(shellTaskOrganizer, kidsModeTaskOrganizer, - legacySplitScreenOptional, splitScreenOptional, pipOptional, oneHandedOptional, - hideDisplayCutout, appPairsOptional, recentTasksOptional, mainExecutor); + static ShellCommandHandler provideShellCommandHandler() { + return new ShellCommandHandler(); } @WMSingleton @Provides - static Optional<BackAnimationController> provideBackAnimationController( - Context context, - @ShellMainThread ShellExecutor shellExecutor, - @ShellBackgroundThread Handler backgroundHandler - ) { - if (BackAnimationController.IS_ENABLED) { - return Optional.of( - new BackAnimationController(shellExecutor, backgroundHandler, context)); - } - return Optional.empty(); + static ProtoLogController provideProtoLogController( + ShellInit shellInit, + ShellCommandHandler shellCommandHandler) { + return new ProtoLogController(shellInit, shellCommandHandler); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellConcurrencyModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellConcurrencyModule.java index cc741d3896a2..0cc545a7724a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellConcurrencyModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellConcurrencyModule.java @@ -20,21 +20,19 @@ import static android.os.Process.THREAD_PRIORITY_BACKGROUND; import static android.os.Process.THREAD_PRIORITY_DISPLAY; import static android.os.Process.THREAD_PRIORITY_TOP_APP_BOOST; -import android.animation.AnimationHandler; import android.content.Context; import android.os.Build; import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; import android.os.Trace; +import android.view.Choreographer; import androidx.annotation.Nullable; -import com.android.internal.graphics.SfVsyncFrameCallbackProvider; import com.android.wm.shell.R; import com.android.wm.shell.common.HandlerExecutor; import com.android.wm.shell.common.ShellExecutor; -import com.android.wm.shell.common.annotations.ChoreographerSfVsync; import com.android.wm.shell.common.annotations.ExternalMainThread; import com.android.wm.shell.common.annotations.ShellAnimationThread; import com.android.wm.shell.common.annotations.ShellBackgroundThread; @@ -144,6 +142,25 @@ public abstract class WMShellConcurrencyModule { } /** + * Provide a Shell main-thread {@link Choreographer} with the app vsync. + * + * @param executor the executor of the shell main thread + */ + @WMSingleton + @Provides + @ShellMainThread + public static Choreographer provideShellMainChoreographer( + @ShellMainThread ShellExecutor executor) { + try { + final Choreographer[] choreographer = new Choreographer[1]; + executor.executeBlocking(() -> choreographer[0] = Choreographer.getInstance()); + return choreographer[0]; + } catch (InterruptedException e) { + throw new RuntimeException("Failed to obtain main Choreographer.", e); + } + } + + /** * Provide a Shell animation-thread Executor. */ @WMSingleton @@ -175,30 +192,6 @@ public abstract class WMShellConcurrencyModule { } /** - * Provide a Shell main-thread AnimationHandler. The AnimationHandler can be set on - * {@link android.animation.ValueAnimator}s and will ensure that the animation will run on - * the Shell main-thread with the SF vsync. - */ - @WMSingleton - @Provides - @ChoreographerSfVsync - public static AnimationHandler provideShellMainExecutorSfVsyncAnimationHandler( - @ShellMainThread ShellExecutor mainExecutor) { - try { - AnimationHandler handler = new AnimationHandler(); - mainExecutor.executeBlocking(() -> { - // This is called on the animation thread since it calls - // Choreographer.getSfInstance() which returns a thread-local Choreographer instance - // that uses the SF vsync - handler.setProvider(new SfVsyncFrameCallbackProvider()); - }); - return handler; - } catch (InterruptedException e) { - throw new RuntimeException("Failed to initialize SfVsync animation handler in 1s", e); - } - } - - /** * Provides a Shell background thread Handler for low priority background tasks. */ @WMSingleton 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 b3799e2cf8d9..37a50b611039 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,11 +16,11 @@ package com.android.wm.shell.dagger; -import android.animation.AnimationHandler; import android.content.Context; import android.content.pm.LauncherApps; import android.os.Handler; import android.os.UserManager; +import android.view.Choreographer; import android.view.WindowManager; import com.android.internal.jank.InteractionJankMonitor; @@ -31,8 +31,11 @@ import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.TaskViewTransitions; import com.android.wm.shell.WindowManagerShellWrapper; -import com.android.wm.shell.apppairs.AppPairsController; import com.android.wm.shell.bubbles.BubbleController; +import com.android.wm.shell.bubbles.BubbleData; +import com.android.wm.shell.bubbles.BubbleDataRepository; +import com.android.wm.shell.bubbles.BubbleLogger; +import com.android.wm.shell.bubbles.BubblePositioner; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayImeController; import com.android.wm.shell.common.DisplayInsetsController; @@ -43,13 +46,16 @@ import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.common.SystemWindows; import com.android.wm.shell.common.TaskStackListenerImpl; import com.android.wm.shell.common.TransactionPool; -import com.android.wm.shell.common.annotations.ChoreographerSfVsync; import com.android.wm.shell.common.annotations.ShellBackgroundThread; import com.android.wm.shell.common.annotations.ShellMainThread; +import com.android.wm.shell.desktopmode.DesktopModeController; +import com.android.wm.shell.desktopmode.DesktopModeTaskRepository; import com.android.wm.shell.draganddrop.DragAndDropController; +import com.android.wm.shell.freeform.FreeformComponents; import com.android.wm.shell.freeform.FreeformTaskListener; -import com.android.wm.shell.fullscreen.FullscreenUnfoldController; -import com.android.wm.shell.legacysplitscreen.LegacySplitScreenController; +import com.android.wm.shell.freeform.FreeformTaskTransitionHandler; +import com.android.wm.shell.freeform.FreeformTaskTransitionObserver; +import com.android.wm.shell.fullscreen.FullscreenTaskListener; import com.android.wm.shell.onehanded.OneHandedController; import com.android.wm.shell.pip.Pip; import com.android.wm.shell.pip.PipAnimationController; @@ -65,21 +71,35 @@ import com.android.wm.shell.pip.PipTransition; import com.android.wm.shell.pip.PipTransitionController; import com.android.wm.shell.pip.PipTransitionState; import com.android.wm.shell.pip.PipUiEventLogger; +import com.android.wm.shell.pip.phone.PhonePipKeepClearAlgorithm; import com.android.wm.shell.pip.phone.PhonePipMenuController; import com.android.wm.shell.pip.phone.PipController; import com.android.wm.shell.pip.phone.PipMotionHelper; import com.android.wm.shell.pip.phone.PipTouchHandler; import com.android.wm.shell.recents.RecentTasksController; import com.android.wm.shell.splitscreen.SplitScreenController; -import com.android.wm.shell.splitscreen.StageTaskUnfoldController; +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.DefaultMixedHandler; import com.android.wm.shell.transition.Transitions; import com.android.wm.shell.unfold.ShellUnfoldProgressProvider; +import com.android.wm.shell.unfold.UnfoldAnimationController; import com.android.wm.shell.unfold.UnfoldBackgroundController; - +import com.android.wm.shell.unfold.UnfoldTransitionHandler; +import com.android.wm.shell.unfold.animation.FullscreenUnfoldTaskAnimator; +import com.android.wm.shell.unfold.animation.SplitTaskUnfoldAnimator; +import com.android.wm.shell.unfold.animation.UnfoldTaskAnimator; +import com.android.wm.shell.unfold.qualifier.UnfoldShellTransition; +import com.android.wm.shell.unfold.qualifier.UnfoldTransition; +import com.android.wm.shell.windowdecor.CaptionWindowDecorViewModel; +import com.android.wm.shell.windowdecor.WindowDecorViewModel; + +import java.util.ArrayList; +import java.util.List; import java.util.Optional; -import javax.inject.Provider; - +import dagger.Binds; import dagger.Lazy; import dagger.Module; import dagger.Provides; @@ -93,16 +113,42 @@ import dagger.Provides; * dependencies should go into {@link WMShellBaseModule}. */ @Module(includes = WMShellBaseModule.class) -public class WMShellModule { +public abstract class WMShellModule { // // Bubbles // + @WMSingleton + @Provides + static BubbleLogger provideBubbleLogger(UiEventLogger uiEventLogger) { + return new BubbleLogger(uiEventLogger); + } + + @WMSingleton + @Provides + static BubblePositioner provideBubblePositioner(Context context, + WindowManager windowManager) { + return new BubblePositioner(context, windowManager); + } + + @WMSingleton + @Provides + static BubbleData provideBubbleData(Context context, + BubbleLogger logger, + BubblePositioner positioner, + @ShellMainThread ShellExecutor mainExecutor) { + return new BubbleData(context, logger, positioner, mainExecutor); + } + // Note: Handler needed for LauncherApps.register @WMSingleton @Provides static BubbleController provideBubbleController(Context context, + ShellInit shellInit, + ShellCommandHandler shellCommandHandler, + ShellController shellController, + BubbleData data, FloatingContentCoordinator floatingContentCoordinator, IStatusBarService statusBarService, WindowManager windowManager, @@ -110,8 +156,9 @@ public class WMShellModule { UserManager userManager, LauncherApps launcherApps, TaskStackListenerImpl taskStackListener, - UiEventLogger uiEventLogger, + BubbleLogger logger, ShellTaskOrganizer organizer, + BubblePositioner positioner, DisplayController displayController, @DynamicOverride Optional<OneHandedController> oneHandedOptional, DragAndDropController dragAndDropController, @@ -120,24 +167,90 @@ public class WMShellModule { @ShellBackgroundThread ShellExecutor bgExecutor, TaskViewTransitions taskViewTransitions, SyncTransactionQueue syncQueue) { - return BubbleController.create(context, null /* synchronizer */, - floatingContentCoordinator, statusBarService, windowManager, - windowManagerShellWrapper, userManager, launcherApps, taskStackListener, - uiEventLogger, organizer, displayController, oneHandedOptional, - dragAndDropController, mainExecutor, mainHandler, bgExecutor, + return new BubbleController(context, shellInit, shellCommandHandler, shellController, data, + null /* synchronizer */, floatingContentCoordinator, + new BubbleDataRepository(context, launcherApps, mainExecutor), + statusBarService, windowManager, windowManagerShellWrapper, userManager, + launcherApps, logger, taskStackListener, organizer, positioner, displayController, + oneHandedOptional, dragAndDropController, mainExecutor, mainHandler, bgExecutor, taskViewTransitions, syncQueue); } // + // Window decoration + // + + @WMSingleton + @Provides + static WindowDecorViewModel<?> provideWindowDecorViewModel( + Context context, + @ShellMainThread Handler mainHandler, + @ShellMainThread Choreographer mainChoreographer, + ShellTaskOrganizer taskOrganizer, + DisplayController displayController, + SyncTransactionQueue syncQueue, + @DynamicOverride DesktopModeController desktopModeController) { + return new CaptionWindowDecorViewModel( + context, + mainHandler, + mainChoreographer, + taskOrganizer, + displayController, + syncQueue, + desktopModeController); + } + + // // Freeform // @WMSingleton @Provides @DynamicOverride - static FreeformTaskListener provideFreeformTaskListener( - SyncTransactionQueue syncQueue) { - return new FreeformTaskListener(syncQueue); + static FreeformComponents provideFreeformComponents( + FreeformTaskListener<?> taskListener, + FreeformTaskTransitionHandler transitionHandler, + FreeformTaskTransitionObserver transitionObserver) { + return new FreeformComponents( + taskListener, Optional.of(transitionHandler), Optional.of(transitionObserver)); + } + + @WMSingleton + @Provides + static FreeformTaskListener<?> provideFreeformTaskListener( + Context context, + ShellInit shellInit, + ShellTaskOrganizer shellTaskOrganizer, + Optional<DesktopModeTaskRepository> desktopModeTaskRepository, + WindowDecorViewModel<?> windowDecorViewModel) { + // TODO(b/238217847): Temporarily add this check here until we can remove the dynamic + // override for this controller from the base module + ShellInit init = FreeformComponents.isFreeformEnabled(context) + ? shellInit + : null; + return new FreeformTaskListener<>(init, shellTaskOrganizer, desktopModeTaskRepository, + windowDecorViewModel); + } + + @WMSingleton + @Provides + static FreeformTaskTransitionHandler provideFreeformTaskTransitionHandler( + ShellInit shellInit, + Transitions transitions, + WindowDecorViewModel<?> windowDecorViewModel) { + return new FreeformTaskTransitionHandler(shellInit, transitions, windowDecorViewModel); + } + + @WMSingleton + @Provides + static FreeformTaskTransitionObserver provideFreeformTaskTransitionObserver( + Context context, + ShellInit shellInit, + Transitions transitions, + FullscreenTaskListener<?> fullscreenTaskListener, + FreeformTaskListener<?> freeformTaskListener) { + return new FreeformTaskTransitionObserver( + context, shellInit, transitions, fullscreenTaskListener, freeformTaskListener); } // @@ -150,12 +263,20 @@ public class WMShellModule { @Provides @DynamicOverride static OneHandedController provideOneHandedController(Context context, - WindowManager windowManager, DisplayController displayController, - DisplayLayout displayLayout, TaskStackListenerImpl taskStackListener, - UiEventLogger uiEventLogger, InteractionJankMonitor jankMonitor, - @ShellMainThread ShellExecutor mainExecutor, @ShellMainThread Handler mainHandler) { - return OneHandedController.create(context, windowManager, displayController, displayLayout, - taskStackListener, jankMonitor, uiEventLogger, mainExecutor, mainHandler); + ShellInit shellInit, + ShellCommandHandler shellCommandHandler, + ShellController shellController, + WindowManager windowManager, + DisplayController displayController, + DisplayLayout displayLayout, + TaskStackListenerImpl taskStackListener, + UiEventLogger uiEventLogger, + InteractionJankMonitor jankMonitor, + @ShellMainThread ShellExecutor mainExecutor, + @ShellMainThread Handler mainHandler) { + return OneHandedController.create(context, shellInit, shellCommandHandler, shellController, + windowManager, displayController, displayLayout, taskStackListener, jankMonitor, + uiEventLogger, mainExecutor, mainHandler); } // @@ -166,45 +287,26 @@ public class WMShellModule { @Provides @DynamicOverride static SplitScreenController provideSplitScreenController( + Context context, + ShellInit shellInit, + ShellCommandHandler shellCommandHandler, + ShellController shellController, ShellTaskOrganizer shellTaskOrganizer, - SyncTransactionQueue syncQueue, Context context, + SyncTransactionQueue syncQueue, RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, - @ShellMainThread ShellExecutor mainExecutor, DisplayController displayController, DisplayImeController displayImeController, - DisplayInsetsController displayInsetsController, Transitions transitions, - TransactionPool transactionPool, IconProvider iconProvider, + DisplayInsetsController displayInsetsController, + DragAndDropController dragAndDropController, + Transitions transitions, + TransactionPool transactionPool, + IconProvider iconProvider, Optional<RecentTasksController> recentTasks, - Provider<Optional<StageTaskUnfoldController>> stageTaskUnfoldControllerProvider) { - return new SplitScreenController(shellTaskOrganizer, syncQueue, context, - rootTaskDisplayAreaOrganizer, mainExecutor, displayController, displayImeController, - displayInsetsController, transitions, transactionPool, iconProvider, - recentTasks, stageTaskUnfoldControllerProvider); - } - - @WMSingleton - @Provides - static LegacySplitScreenController provideLegacySplitScreen(Context context, - DisplayController displayController, SystemWindows systemWindows, - DisplayImeController displayImeController, TransactionPool transactionPool, - ShellTaskOrganizer shellTaskOrganizer, SyncTransactionQueue syncQueue, - TaskStackListenerImpl taskStackListener, Transitions transitions, - @ShellMainThread ShellExecutor mainExecutor, - @ChoreographerSfVsync AnimationHandler sfVsyncAnimationHandler) { - return new LegacySplitScreenController(context, displayController, systemWindows, - displayImeController, transactionPool, shellTaskOrganizer, syncQueue, - taskStackListener, transitions, mainExecutor, sfVsyncAnimationHandler); - } - - @WMSingleton - @Provides - static AppPairsController provideAppPairs(ShellTaskOrganizer shellTaskOrganizer, - SyncTransactionQueue syncQueue, DisplayController displayController, - @ShellMainThread ShellExecutor mainExecutor, - DisplayImeController displayImeController, - DisplayInsetsController displayInsetsController) { - return new AppPairsController(shellTaskOrganizer, syncQueue, displayController, - mainExecutor, displayImeController, displayInsetsController); + @ShellMainThread ShellExecutor mainExecutor) { + return new SplitScreenController(context, shellInit, shellCommandHandler, shellController, + shellTaskOrganizer, syncQueue, rootTaskDisplayAreaOrganizer, displayController, + displayImeController, displayInsetsController, dragAndDropController, transitions, + transactionPool, iconProvider, recentTasks, mainExecutor); } // @@ -213,21 +315,37 @@ public class WMShellModule { @WMSingleton @Provides - static Optional<Pip> providePip(Context context, DisplayController displayController, - PipAppOpsListener pipAppOpsListener, PipBoundsAlgorithm pipBoundsAlgorithm, - PipBoundsState pipBoundsState, PipMediaController pipMediaController, - PhonePipMenuController phonePipMenuController, PipTaskOrganizer pipTaskOrganizer, - PipTouchHandler pipTouchHandler, PipTransitionController pipTransitionController, + static Optional<Pip> providePip(Context context, + ShellInit shellInit, + ShellCommandHandler shellCommandHandler, + ShellController shellController, + DisplayController displayController, + PipAnimationController pipAnimationController, + PipAppOpsListener pipAppOpsListener, + PipBoundsAlgorithm pipBoundsAlgorithm, + PhonePipKeepClearAlgorithm pipKeepClearAlgorithm, + PipBoundsState pipBoundsState, + PipMotionHelper pipMotionHelper, + PipMediaController pipMediaController, + PhonePipMenuController phonePipMenuController, + PipTaskOrganizer pipTaskOrganizer, + PipTransitionState pipTransitionState, + PipTouchHandler pipTouchHandler, + PipTransitionController pipTransitionController, WindowManagerShellWrapper windowManagerShellWrapper, TaskStackListenerImpl taskStackListener, PipParamsChangedForwarder pipParamsChangedForwarder, + DisplayInsetsController displayInsetsController, Optional<OneHandedController> oneHandedController, @ShellMainThread ShellExecutor mainExecutor) { - return Optional.ofNullable(PipController.create(context, displayController, - pipAppOpsListener, pipBoundsAlgorithm, pipBoundsState, - pipMediaController, phonePipMenuController, pipTaskOrganizer, - pipTouchHandler, pipTransitionController, windowManagerShellWrapper, - taskStackListener, pipParamsChangedForwarder, oneHandedController, mainExecutor)); + return Optional.ofNullable(PipController.create( + context, shellInit, shellCommandHandler, shellController, + displayController, pipAnimationController, pipAppOpsListener, pipBoundsAlgorithm, + pipKeepClearAlgorithm, pipBoundsState, pipMotionHelper, pipMediaController, + phonePipMenuController, pipTaskOrganizer, pipTransitionState, pipTouchHandler, + pipTransitionController, windowManagerShellWrapper, taskStackListener, + pipParamsChangedForwarder, displayInsetsController, oneHandedController, + mainExecutor)); } @WMSingleton @@ -244,9 +362,17 @@ public class WMShellModule { @WMSingleton @Provides + static PhonePipKeepClearAlgorithm providePhonePipKeepClearAlgorithm(Context context) { + return new PhonePipKeepClearAlgorithm(context); + } + + @WMSingleton + @Provides static PipBoundsAlgorithm providesPipBoundsAlgorithm(Context context, - PipBoundsState pipBoundsState, PipSnapAlgorithm pipSnapAlgorithm) { - return new PipBoundsAlgorithm(context, pipBoundsState, pipSnapAlgorithm); + PipBoundsState pipBoundsState, PipSnapAlgorithm pipSnapAlgorithm, + PhonePipKeepClearAlgorithm pipKeepClearAlgorithm) { + return new PipBoundsAlgorithm(context, pipBoundsState, pipSnapAlgorithm, + pipKeepClearAlgorithm); } // Handler is used by Icon.loadDrawableAsync @@ -266,14 +392,16 @@ public class WMShellModule { @WMSingleton @Provides static PipTouchHandler providePipTouchHandler(Context context, - PhonePipMenuController menuPhoneController, PipBoundsAlgorithm pipBoundsAlgorithm, + ShellInit shellInit, + PhonePipMenuController menuPhoneController, + PipBoundsAlgorithm pipBoundsAlgorithm, PipBoundsState pipBoundsState, PipTaskOrganizer pipTaskOrganizer, PipMotionHelper pipMotionHelper, FloatingContentCoordinator floatingContentCoordinator, PipUiEventLogger pipUiEventLogger, @ShellMainThread ShellExecutor mainExecutor) { - return new PipTouchHandler(context, menuPhoneController, pipBoundsAlgorithm, + return new PipTouchHandler(context, shellInit, menuPhoneController, pipBoundsAlgorithm, pipBoundsState, pipTaskOrganizer, pipMotionHelper, floatingContentCoordinator, pipUiEventLogger, mainExecutor); } @@ -317,15 +445,15 @@ public class WMShellModule { @WMSingleton @Provides static PipTransitionController providePipTransitionController(Context context, - Transitions transitions, ShellTaskOrganizer shellTaskOrganizer, + ShellInit shellInit, ShellTaskOrganizer shellTaskOrganizer, Transitions transitions, PipAnimationController pipAnimationController, PipBoundsAlgorithm pipBoundsAlgorithm, PipBoundsState pipBoundsState, PipTransitionState pipTransitionState, PhonePipMenuController pipMenuController, PipSurfaceTransactionHelper pipSurfaceTransactionHelper, Optional<SplitScreenController> splitScreenOptional) { - return new PipTransition(context, pipBoundsState, pipTransitionState, pipMenuController, - pipBoundsAlgorithm, pipAnimationController, transitions, shellTaskOrganizer, - pipSurfaceTransactionHelper, splitScreenOptional); + return new PipTransition(context, shellInit, shellTaskOrganizer, transitions, + pipBoundsState, pipTransitionState, pipMenuController, pipBoundsAlgorithm, + pipAnimationController, pipSurfaceTransactionHelper, splitScreenOptional); } @WMSingleton @@ -348,6 +476,27 @@ public class WMShellModule { floatingContentCoordinator); } + @WMSingleton + @Provides + static PipParamsChangedForwarder providePipParamsChangedForwarder() { + return new PipParamsChangedForwarder(); + } + + // + // Transitions + // + + @WMSingleton + @Provides + static DefaultMixedHandler provideDefaultMixedHandler( + ShellInit shellInit, + Optional<SplitScreenController> splitScreenOptional, + Optional<PipTouchHandler> pipTouchHandlerOptional, + Transitions transitions) { + return new DefaultMixedHandler(shellInit, transitions, splitScreenOptional, + pipTouchHandlerOptional); + } + // // Unfold transition // @@ -355,36 +504,80 @@ public class WMShellModule { @WMSingleton @Provides @DynamicOverride - static FullscreenUnfoldController provideFullscreenUnfoldController( - Context context, + static UnfoldAnimationController provideUnfoldAnimationController( Optional<ShellUnfoldProgressProvider> progressProvider, - Lazy<UnfoldBackgroundController> unfoldBackgroundController, - DisplayInsetsController displayInsetsController, + TransactionPool transactionPool, + @UnfoldTransition SplitTaskUnfoldAnimator splitAnimator, + FullscreenUnfoldTaskAnimator fullscreenAnimator, + Lazy<Optional<UnfoldTransitionHandler>> unfoldTransitionHandler, + ShellInit shellInit, @ShellMainThread ShellExecutor mainExecutor ) { - return new FullscreenUnfoldController(context, mainExecutor, - unfoldBackgroundController.get(), progressProvider.get(), + final List<UnfoldTaskAnimator> animators = new ArrayList<>(); + animators.add(splitAnimator); + animators.add(fullscreenAnimator); + + return new UnfoldAnimationController( + shellInit, + transactionPool, + progressProvider.get(), + animators, + unfoldTransitionHandler, + mainExecutor + ); + } + + @Provides + static FullscreenUnfoldTaskAnimator provideFullscreenUnfoldTaskAnimator( + Context context, + UnfoldBackgroundController unfoldBackgroundController, + DisplayInsetsController displayInsetsController + ) { + return new FullscreenUnfoldTaskAnimator(context, unfoldBackgroundController, displayInsetsController); } @Provides - static Optional<StageTaskUnfoldController> provideStageTaskUnfoldController( - Optional<ShellUnfoldProgressProvider> progressProvider, + static SplitTaskUnfoldAnimator provideSplitTaskUnfoldAnimatorBase( Context context, - TransactionPool transactionPool, - Lazy<UnfoldBackgroundController> unfoldBackgroundController, - DisplayInsetsController displayInsetsController, - @ShellMainThread ShellExecutor mainExecutor + UnfoldBackgroundController backgroundController, + @ShellMainThread ShellExecutor executor, + Lazy<Optional<SplitScreenController>> splitScreenOptional, + DisplayInsetsController displayInsetsController ) { - return progressProvider.map(shellUnfoldTransitionProgressProvider -> - new StageTaskUnfoldController( - context, - transactionPool, - shellUnfoldTransitionProgressProvider, - displayInsetsController, - unfoldBackgroundController.get(), - mainExecutor - )); + // TODO(b/238217847): The lazy reference here causes some dependency issues since it + // immediately registers a listener on that controller on init. We should reference the + // controller directly once we refactor ShellTaskOrganizer to not depend on the unfold + // animation controller directly. + return new SplitTaskUnfoldAnimator(context, executor, splitScreenOptional, + backgroundController, displayInsetsController); + } + + @WMSingleton + @UnfoldShellTransition + @Binds + abstract SplitTaskUnfoldAnimator provideShellSplitTaskUnfoldAnimator( + SplitTaskUnfoldAnimator splitTaskUnfoldAnimator); + + @WMSingleton + @UnfoldTransition + @Binds + abstract SplitTaskUnfoldAnimator provideSplitTaskUnfoldAnimator( + SplitTaskUnfoldAnimator splitTaskUnfoldAnimator); + + @WMSingleton + @Provides + @DynamicOverride + static UnfoldTransitionHandler provideUnfoldTransitionHandler( + Optional<ShellUnfoldProgressProvider> progressProvider, + FullscreenUnfoldTaskAnimator animator, + @UnfoldShellTransition SplitTaskUnfoldAnimator unfoldAnimator, + TransactionPool transactionPool, + Transitions transitions, + @ShellMainThread ShellExecutor executor, + ShellInit shellInit) { + return new UnfoldTransitionHandler(shellInit, progressProvider.get(), animator, + unfoldAnimator, transactionPool, executor, transitions); } @WMSingleton @@ -399,9 +592,45 @@ public class WMShellModule { ); } + // + // Desktop mode (optional feature) + // + @WMSingleton @Provides - static PipParamsChangedForwarder providePipParamsChangedForwarder() { - return new PipParamsChangedForwarder(); + @DynamicOverride + static DesktopModeController provideDesktopModeController(Context context, ShellInit shellInit, + ShellTaskOrganizer shellTaskOrganizer, + RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, + Transitions transitions, + @DynamicOverride DesktopModeTaskRepository desktopModeTaskRepository, + @ShellMainThread Handler mainHandler, + @ShellMainThread ShellExecutor mainExecutor + ) { + return new DesktopModeController(context, shellInit, shellTaskOrganizer, + rootTaskDisplayAreaOrganizer, transitions, desktopModeTaskRepository, mainHandler, + mainExecutor); + } + + @WMSingleton + @Provides + @DynamicOverride + static DesktopModeTaskRepository provideDesktopModeTaskRepository() { + return new DesktopModeTaskRepository(); + } + + // + // Misc + // + + // TODO: Temporarily move dependencies to this instead of ShellInit since that is needed to add + // the callback. We will be moving to a different explicit startup mechanism in a follow- up CL. + @WMSingleton + @ShellCreateTriggerOverride + @Provides + static Object provideIndependentShellComponentsToCreate( + DefaultMixedHandler defaultMixedHandler, + Optional<DesktopModeController> desktopModeController) { + return new Object(); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellInit.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java index d7010b174744..ff3be38d09e1 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellInit.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 The Android Open Source Project + * 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. @@ -14,17 +14,18 @@ * limitations under the License. */ -package com.android.wm.shell; +package com.android.wm.shell.desktopmode; import com.android.wm.shell.common.annotations.ExternalThread; /** - * An entry point into the shell for initializing shell internal state. + * Interface to interact with desktop mode feature in shell. */ @ExternalThread -public interface ShellInit { - /** - * Initializes the shell state. - */ - void init(); +public interface DesktopMode { + + /** Returns a binder that can be passed to an external process to manipulate DesktopMode. */ + default IDesktopMode createExternalInterface() { + return null; + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java new file mode 100644 index 000000000000..99739c457aa6 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java @@ -0,0 +1,275 @@ +/* + * 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.desktopmode; + +import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; +import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static android.view.WindowManager.TRANSIT_CHANGE; + +import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission; +import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE; + +import android.app.ActivityManager.RunningTaskInfo; +import android.app.WindowConfiguration; +import android.content.Context; +import android.database.ContentObserver; +import android.net.Uri; +import android.os.Handler; +import android.os.UserHandle; +import android.provider.Settings; +import android.util.ArraySet; +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.wm.shell.RootTaskDisplayAreaOrganizer; +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.common.RemoteCallable; +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.annotations.ExternalThread; +import com.android.wm.shell.common.annotations.ShellMainThread; +import com.android.wm.shell.sysui.ShellInit; +import com.android.wm.shell.transition.Transitions; + +import java.util.ArrayList; +import java.util.Comparator; + +/** + * Handles windowing changes when desktop mode system setting changes + */ +public class DesktopModeController implements RemoteCallable<DesktopModeController> { + + private final Context mContext; + private final ShellTaskOrganizer mShellTaskOrganizer; + private final RootTaskDisplayAreaOrganizer mRootTaskDisplayAreaOrganizer; + private final Transitions mTransitions; + private final DesktopModeTaskRepository mDesktopModeTaskRepository; + private final ShellExecutor mMainExecutor; + private final DesktopMode mDesktopModeImpl = new DesktopModeImpl(); + private final SettingsObserver mSettingsObserver; + + public DesktopModeController(Context context, ShellInit shellInit, + ShellTaskOrganizer shellTaskOrganizer, + RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, + Transitions transitions, + DesktopModeTaskRepository desktopModeTaskRepository, + @ShellMainThread Handler mainHandler, + @ShellMainThread ShellExecutor mainExecutor) { + mContext = context; + mShellTaskOrganizer = shellTaskOrganizer; + mRootTaskDisplayAreaOrganizer = rootTaskDisplayAreaOrganizer; + mTransitions = transitions; + mDesktopModeTaskRepository = desktopModeTaskRepository; + mMainExecutor = mainExecutor; + mSettingsObserver = new SettingsObserver(mContext, mainHandler); + shellInit.addInitCallback(this::onInit, this); + } + + private void onInit() { + ProtoLog.d(WM_SHELL_DESKTOP_MODE, "Initialize DesktopModeController"); + mSettingsObserver.observe(); + if (DesktopModeStatus.isActive(mContext)) { + updateDesktopModeActive(true); + } + } + + @Override + public Context getContext() { + return mContext; + } + + @Override + public ShellExecutor getRemoteCallExecutor() { + return mMainExecutor; + } + + /** + * Get connection interface between sysui and shell + */ + public DesktopMode asDesktopMode() { + return mDesktopModeImpl; + } + + @VisibleForTesting + void updateDesktopModeActive(boolean active) { + ProtoLog.d(WM_SHELL_DESKTOP_MODE, "updateDesktopModeActive: active=%s", active); + + int displayId = mContext.getDisplayId(); + + WindowContainerTransaction wct = new WindowContainerTransaction(); + // Reset freeform windowing mode that is set per task level (tasks should inherit + // container value) + wct.merge(mShellTaskOrganizer.prepareClearFreeformForStandardTasks(displayId), + true /* transfer */); + int targetWindowingMode; + if (active) { + targetWindowingMode = WINDOWING_MODE_FREEFORM; + } else { + targetWindowingMode = WINDOWING_MODE_FULLSCREEN; + // Clear any resized bounds + wct.merge(mShellTaskOrganizer.prepareClearBoundsForStandardTasks(displayId), + true /* transfer */); + } + prepareWindowingModeChange(wct, displayId, targetWindowingMode); + if (Transitions.ENABLE_SHELL_TRANSITIONS) { + mTransitions.startTransition(TRANSIT_CHANGE, wct, null); + } else { + mRootTaskDisplayAreaOrganizer.applyTransaction(wct); + } + } + + private void prepareWindowingModeChange(WindowContainerTransaction wct, + int displayId, @WindowConfiguration.WindowingMode int windowingMode) { + DisplayAreaInfo displayAreaInfo = mRootTaskDisplayAreaOrganizer + .getDisplayAreaInfo(displayId); + if (displayAreaInfo == null) { + ProtoLog.e(WM_SHELL_DESKTOP_MODE, + "unable to update windowing mode for display %d display not found", displayId); + return; + } + + ProtoLog.d(WM_SHELL_DESKTOP_MODE, + "setWindowingMode: displayId=%d current wmMode=%d new wmMode=%d", displayId, + displayAreaInfo.configuration.windowConfiguration.getWindowingMode(), + windowingMode); + + wct.setWindowingMode(displayAreaInfo.token, windowingMode); + } + + /** + * Show apps on desktop + */ + public void showDesktopApps() { + ArraySet<Integer> activeTasks = mDesktopModeTaskRepository.getActiveTasks(); + ProtoLog.d(WM_SHELL_DESKTOP_MODE, "bringDesktopAppsToFront: tasks=%s", activeTasks.size()); + ArrayList<RunningTaskInfo> taskInfos = new ArrayList<>(); + for (Integer taskId : activeTasks) { + RunningTaskInfo taskInfo = mShellTaskOrganizer.getRunningTaskInfo(taskId); + if (taskInfo != null) { + taskInfos.add(taskInfo); + } + } + // Order by lastActiveTime, descending + taskInfos.sort(Comparator.comparingLong(task -> -task.lastActiveTime)); + WindowContainerTransaction wct = new WindowContainerTransaction(); + for (RunningTaskInfo task : taskInfos) { + wct.reorder(task.token, true); + } + mShellTaskOrganizer.applyTransaction(wct); + } + + /** + * Turn desktop mode on or off + * @param active the desired state for desktop mode setting + */ + public void setDesktopModeActive(boolean active) { + int value = active ? 1 : 0; + Settings.System.putInt(mContext.getContentResolver(), Settings.System.DESKTOP_MODE, value); + } + + /** + * Returns the windowing mode of the display area with the specified displayId. + * @param displayId + * @return + */ + public int getDisplayAreaWindowingMode(int displayId) { + return mRootTaskDisplayAreaOrganizer.getDisplayAreaInfo(displayId) + .configuration.windowConfiguration.getWindowingMode(); + } + + /** + * A {@link ContentObserver} for listening to changes to {@link Settings.System#DESKTOP_MODE} + */ + private final class SettingsObserver extends ContentObserver { + + private final Uri mDesktopModeSetting = Settings.System.getUriFor( + Settings.System.DESKTOP_MODE); + + private final Context mContext; + + SettingsObserver(Context context, Handler handler) { + super(handler); + mContext = context; + } + + public void observe() { + // TODO(b/242867463): listen for setting change for all users + mContext.getContentResolver().registerContentObserver(mDesktopModeSetting, + false /* notifyForDescendants */, this /* observer */, UserHandle.USER_CURRENT); + } + + @Override + public void onChange(boolean selfChange, @Nullable Uri uri) { + if (mDesktopModeSetting.equals(uri)) { + ProtoLog.d(WM_SHELL_DESKTOP_MODE, "Received update for desktop mode setting"); + desktopModeSettingChanged(); + } + } + + private void desktopModeSettingChanged() { + boolean enabled = DesktopModeStatus.isActive(mContext); + updateDesktopModeActive(enabled); + } + } + + /** + * The interface for calls from outside the shell, within the host process. + */ + @ExternalThread + private final class DesktopModeImpl implements DesktopMode { + + private IDesktopModeImpl mIDesktopMode; + + @Override + public IDesktopMode createExternalInterface() { + if (mIDesktopMode != null) { + mIDesktopMode.invalidate(); + } + mIDesktopMode = new IDesktopModeImpl(DesktopModeController.this); + return mIDesktopMode; + } + } + + /** + * The interface for calls from outside the host process. + */ + @BinderThread + private static class IDesktopModeImpl extends IDesktopMode.Stub { + + private DesktopModeController mController; + + IDesktopModeImpl(DesktopModeController controller) { + mController = controller; + } + + /** + * Invalidates this instance, preventing future calls from updating the controller. + */ + void invalidate() { + mController = null; + } + + public void showDesktopApps() { + executeRemoteCallWithTaskPermission(mController, "showDesktopApps", + DesktopModeController::showDesktopApps); + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java new file mode 100644 index 000000000000..195ff502e7dc --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java @@ -0,0 +1,58 @@ +/* + * 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.desktopmode; + +import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE; + +import android.content.Context; +import android.os.SystemProperties; +import android.os.UserHandle; +import android.provider.Settings; + +import com.android.internal.protolog.common.ProtoLog; + +/** + * Constants for desktop mode feature + */ +public class DesktopModeStatus { + + /** + * Flag to indicate whether desktop mode is available on the device + */ + public static final boolean IS_SUPPORTED = SystemProperties.getBoolean( + "persist.wm.debug.desktop_mode", false); + + /** + * Check if desktop mode is active + * + * @return {@code true} if active + */ + public static boolean isActive(Context context) { + if (!IS_SUPPORTED) { + return false; + } + try { + int result = Settings.System.getIntForUser(context.getContentResolver(), + Settings.System.DESKTOP_MODE, UserHandle.USER_CURRENT); + ProtoLog.d(WM_SHELL_DESKTOP_MODE, "isDesktopModeEnabled=%s", result); + return result != 0; + } catch (Exception e) { + ProtoLog.e(WM_SHELL_DESKTOP_MODE, "Failed to read DESKTOP_MODE setting %s", e); + return false; + } + } +} 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 new file mode 100644 index 000000000000..988601c0e8a8 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt @@ -0,0 +1,89 @@ +/* + * 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.desktopmode + +import android.util.ArraySet + +/** + * Keeps track of task data related to desktop mode. + */ +class DesktopModeTaskRepository { + + /** + * 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. + */ + private val activeTasks = ArraySet<Int>() + private val listeners = ArraySet<Listener>() + + /** + * Add a [Listener] to be notified of updates to the repository. + */ + fun addListener(listener: Listener) { + listeners.add(listener) + } + + /** + * Remove a previously registered [Listener] + */ + fun removeListener(listener: Listener) { + listeners.remove(listener) + } + + /** + * Mark a task with given [taskId] as active. + */ + fun addActiveTask(taskId: Int) { + val added = activeTasks.add(taskId) + if (added) { + listeners.onEach { it.onActiveTasksChanged() } + } + } + + /** + * Remove task with given [taskId] from active tasks. + */ + fun removeActiveTask(taskId: Int) { + val removed = activeTasks.remove(taskId) + if (removed) { + listeners.onEach { it.onActiveTasksChanged() } + } + } + + /** + * Check if a task with the given [taskId] was marked as an active task + */ + fun isActiveTask(taskId: Int): Boolean { + return activeTasks.contains(taskId) + } + + /** + * Get a set of the active tasks + */ + fun getActiveTasks(): ArraySet<Int> { + return ArraySet(activeTasks) + } + + /** + * Defines interface for classes that can listen to changes in repository state. + */ + interface Listener { + fun onActiveTasksChanged() + } +} 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 new file mode 100644 index 000000000000..5042bd6f2d65 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl @@ -0,0 +1,26 @@ +/* + * 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.desktopmode; + +/** + * Interface that is exposed to remote callers to manipulate desktop mode features. + */ +interface IDesktopMode { + + /** Show apps on the desktop */ + void showDesktopApps(); +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/README.md b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/README.md new file mode 100644 index 000000000000..73a7348d5aca --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/README.md @@ -0,0 +1,18 @@ +# Window Manager Shell Readme + +The following docs present more detail about the implementation of the WMShell library (in no +particular order): + +1) [What is the Shell](overview.md) +2) [Integration with SystemUI & Launcher](sysui.md) +3) [Usage of Dagger](dagger.md) +4) [Threading model in the Shell](threading.md) +5) [Making changes in the Shell](changes.md) +6) [Extending the Shell for Products/OEMs](extending.md) +7) [Debugging in the Shell](debugging.md) +8) [Testing in the Shell](testing.md) + +Todo +- Per-feature docs +- Feature flagging +- Best practices
\ 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 new file mode 100644 index 000000000000..2aa933d641fa --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/changes.md @@ -0,0 +1,87 @@ +# Making changes in the Shell + +--- + +## Code reviews + +In addition to the individual reviewers who are most familiar with the changes you are making, +please also add [wm-code-reviewers@google.com](http://g/wm-code-reviewers) to keep other WM folks +in the loop. + +## Adding new code + +### Internal Shell utility classes +If the new component is used only within the WMShell library, then there are no special +considerations, go ahead and add it (in the `com.android.wm.shell.common` package for example) +and make sure the appropriate [unit tests](testing.md) are added. + +### Internal Shell components +If the new component is to be used by other components/features within the Shell library, then +you can create an appropriate package for this component to add your new code. The current +pattern is to have a single `<Component name>Controller` that handles the initialization of the +component. + +As mentioned in the [Dagger usage](dagger.md) docs, you need to determine whether it should go into: +- `WMShellBaseModule` for components that other base & product components will depend on +- or `WMShellModule`, `TvWmShellModule`, etc. for product specific components that no base + components depend on + +### SysUI accessible components +In addition to doing the above, you will also need to provide an interface for calling to SysUI +from the Shell and vice versa. The current pattern is to have a parallel `Optional<Component name>` +interface that the `<Component name>Controller` implements and handles on the main Shell thread. + +In addition, because components accessible to SysUI injection are explicitly listed, you'll have to +add an appropriate method in `WMComponent` to get the interface and update the `Builder` in +`SysUIComponent` to take the interface so it can be injected in SysUI code. The binding between +the two is done in `SystemUIFactory#init()` which will need to be updated as well. + +### Launcher accessible components +Because Launcher is not a part of SystemUI and is a separate process, exposing controllers to +Launcher requires a new AIDL interface to be created and implemented by the controller. The +implementation of the stub interface in the controller otherwise behaves similar to the interface +to SysUI where it posts the work to the main Shell thread. + +### Component initialization +To initialize the component: +- On the Shell side, you potentially need to do two things to initialize the component: + - Inject `ShellInit` into your component and add an init callback + - Ensure that your component is a part of the dagger dependency graph, either by: + - Making this component a dependency of an existing component already exposed to SystemUI + - Explicitly add this component to the WMShellBaseModule @ShellCreateTrigger provider or + the @ShellCreateTriggerOverride provider for your product module to expose it explicitly + if it is a completely independent component +- On the SysUI side, update `WMShell` to setup any bindings for the component that depend on + SysUI code + +To verify that your component is being initialized at startup, you can enable the `WM_SHELL_INIT` +protolog group and restart the SysUI process: +```shell +adb shell wm logging enable-text WM_SHELL_INIT +adb shell kill `pid com.android.systemui` +adb logcat *:S WindowManagerShell +``` + +### General Do's & Dont's +Do: +- Do add unit tests for all new components +- Do keep controllers simple and break them down as needed + +Don't: +- **Don't** do initialization in the constructor, only do initialization in the init callbacks. + Otherwise it complicates the building of the dependency graph. +- **Don't** create dependencies from base-module components on specific features (the base module + is intended for use with all products) + - Try adding a mechanism to register and listen for changes from the base module component instead +- **Don't** add blocking synchronous calls in the SysUI interface between Shell & SysUI + - Try adding a push-mechanism to share data, or an async callback to request data + +### 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 +Launcher uses. + +If the new code doesn't fall into those categories, they can be added explicitly in the Shell's +[Android.bp](frameworks/base/libs/WindowManager/Shell/Android.bp) file under the +`wm_shell_util-sources` filegroup.
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/dagger.md b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/dagger.md new file mode 100644 index 000000000000..6c01d962adc9 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/dagger.md @@ -0,0 +1,50 @@ +# Usage of Dagger in the Shell library + +--- + +## Dependencies + +Dagger is not required to use the Shell library, but it has a lot of obvious benefits: + +- Not having to worry about how to instantiate all the dependencies of a class, especially as + dependencies evolve (ie. product controller depends on base controller) +- Can create boundaries within the same app to encourage better code modularity + +As such, the Shell also tries to provide some reasonable out-of-the-box modules for use with Dagger. + +## Modules + +All the Dagger related code in the Shell can be found in the `com.android.wm.shell.dagger` package, +this is intentional as it keeps the "magic" in a single location. The explicit nature of how +components in the shell are provided is as a result a bit more verbose, but it makes it easy for +developers to jump into a few select files and understand how different components are provided +(especially as products override components). + +The module dependency tree looks a bit like: +- [WMShellConcurrencyModule](frameworks/base/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellConcurrencyModule.java) + (provides threading-related components) + - [WMShellBaseModule](frameworks/base/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java) + (provides components that are likely common to all products, ie. DisplayController, + Transactions, etc.) + - [WMShellModule](frameworks/base/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java) + (phone/tablet specific components only) + - [TvPipModule](frameworks/base/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvPipModule.java) + (PIP specific components for TV) + - [TvWMShellModule](frameworks/base/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvWMShellModule.java) + (TV specific components only) + - etc. + +Ideally features could be abstracted out into their own modules and included as needed by each +product. + +## Overriding base components + +In some rare cases, there are base components that can change behavior depending on which +product it runs on. If there are hooks that can be added to the component, that is the +preferable approach. + +The alternative is to use the [@DynamicOverride](frameworks/base/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/DynamicOverride.java) +annotation to allow the product module to provide an implementation that the base module can +reference. This is most useful if the existence of the entire component is controlled by the +product and the override implementation is optional (there is a default implementation). More +details can be found in the class's javadoc.
\ 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 new file mode 100644 index 000000000000..99922fbc2d95 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/debugging.md @@ -0,0 +1,69 @@ +# Debugging in the Shell + +--- + +## Logging & ProtoLogs + +The interactions in the Shell can be pretty complicated, so having good logging is crucial to +debugging problems that arise (especially in dogfood). The Shell uses the same efficient Protolog +mechanism as WM Core, which can be enabled at runtime on debug devices. + +**TLDR** Don’t use Logs or Slogs except for error cases, Protologs are much more flexible, +easy to add and easy to use + +### Adding a new ProtoLog +Update `ShellProtoLogGroup` to include a new log group (ie. NEW_FEATURE) for the content you want to +log. ProtoLog log calls mirror Log.v/d/e(), and take a format message and arguments: +```java +ProtoLog.v(NEW_FEATURE, "Test log w/ params: %d %s", 1, “a”) +``` +This code itself will not compile by itself, but the `protologtool` will preprocess the file when +building to check the log state (is enabled) before printing the print format style log. + +**Notes** +- ProtoLogs currently only work from soong builds (ie. via make/mp). We need to reimplement the + tool for use with SysUI-studio +- Non-text ProtoLogs are not currently supported with the Shell library (you can't view them with + traces in Winscope) + +### Enabling ProtoLog command line logging +Run these commands to enable protologs for both WM Core and WM Shell to print to logcat. +```shell +adb shell wm logging enable-text NEW_FEATURE +adb shell wm logging disable-text NEW_FEATURE +``` + +## Winscope Tracing + +The Winscope tool is extremely useful in determining what is happening on-screen in both +WindowManager and SurfaceFlinger. Follow [go/winscope](http://go/winscope-help) to learn how to +use the tool. + +In addition, there is limited preliminary support for Winscope tracing componetns in the Shell, +which involves adding trace fields to [wm_shell_trace.proto](frameworks/base/libs/WindowManager/Shell/proto/wm_shell_trace.proto) +file and ensure it is updated as a part of `WMShell#writeToProto`. + +Tracing can be started via the shell command (to be added to the Winscope tool as needed): +```shell +adb shell cmd statusbar tracing start +adb shell cmd statusbar tracing stop +``` + +## Dumps + +Because the Shell library is built as a part of SystemUI, dumping the state is currently done as a +part of dumping the SystemUI service. Dumping the Shell specific data can be done by specifying the +WMShell SysUI service: + +```shell +adb shell dumpsys activity service SystemUIService WMShell +``` + +If information should be added to the dump, either: +- Update `WMShell` if you are dumping SysUI state +- Inject `ShellCommandHandler` into your Shell class, and add a dump callback + +## Debugging in Android Studio + +If you are using the [go/sysui-studio](http://go/sysui-studio) project, then you can debug Shell +code directly from Android Studio like any other app. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/extending.md b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/extending.md new file mode 100644 index 000000000000..061ae00e2b25 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/extending.md @@ -0,0 +1,13 @@ +# Extending the Shell for Products/OEMs + +--- + +## General Do's & Dont's + +Do: +- + +Don't +- **Don't** override classes provided by WMShellBaseModule, it makes it difficult to make + simple changes to the Shell library base modules which are shared by all products + - If possible add mechanisms to modify the base class behavior
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/overview.md b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/overview.md new file mode 100644 index 000000000000..a88ef6aea2ec --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/overview.md @@ -0,0 +1,58 @@ +# What is the WindowManager Shell + +--- + +## Motivation + +The primary motivation for the WindowManager Shell (WMShell) library is to effectively scale +WindowManager by making it easy™ and safe to create windowing features to fit the needs of +various Android products and form factors. + +To achieve this, WindowManager separates the policy of managing windows (WMCore) from the +presentation of surfaces (WMShell) and provides a minimal interface boundary for the two to +communicate. + +## Who is using the library? + +Currently, the WMShell library is used to drive the windowing experience on handheld +(phones & tablets), TV, Auto, Arc++, and Wear to varying degrees. + +## Where does the code live + +The core WMShell library code is currently located in the [frameworks/base/libs/WindowManager/Shell](frameworks/base/libs/WindowManager/Shell) +directory and is included as a part dependency of the host SystemUI apk. + +## How do I build the Shell library + +The library can be built directly by running (using [go/makepush](http://go/makepush)): +```shell +mp :WindowManager-Shell +``` +But this is mainly useful for inspecting the contents of the library or verifying it builds. The +various targets can be found in the Shell library's [Android.bp](frameworks/base/libs/WindowManager/Shell/Android.bp) +file. + +Normally, you would build it as a part of the host SystemUI, for example via commandline: +```shell +# Phone SystemUI variant +mp sysuig +# Building Shell & SysUI changes along w/ framework changes +mp core services sysuig +``` + +Or preferably, if you are making WMShell/SysUI only changes (no other framework changes), then +building via [go/sysui-studio](http://go/sysui-studio) allows for very quick iteration (one click +build and push of SysUI in < 30s). + +If you are making framework changes and are using `aidegen` to set up your platform IDE, make sure +to include the appropriate directories to build, for example: +```shell +# frameworks/base will include base/libs/WindowManager/Shell and base/packages/SystemUI +aidegen frameworks/base \ + vendor/<oem>/packages/SystemUI \ + ... +``` + +## Other useful links +- [go/o-o-summit-20](go/o-o-summit-20) (Video presentations from the WM team) +- [go/o-o-summit-21](go/o-o-summit-21)
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/sysui.md b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/sysui.md new file mode 100644 index 000000000000..d6302e640ba7 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/sysui.md @@ -0,0 +1,83 @@ +# Shell & SystemUI + +--- + +## Setup + +The SystemUI of various products depend on and build against the WM Shell library. To ensure +that we don't inadvertently build dependencies between the Shell library and one particular +product (ie. handheld SysUI), we deliberately separate the initialization of the WM Shell +component from the SysUI component when set up through Dagger. + +**TLDR** Initialize everything as needed in the WM component scope and export only well +defined interfaces to SysUI. + +## Initialization + +There are more details in the Dagger docs, but the general overview of the SysUI/Shell +initialization flow is such: + +1) SysUI Global scope is initialize (see `GlobalModule` and its included modules) +2) WM Shell scope is initialized, for example + 1) On phones: `WMComponent` includes `WMShellModule` which includes `WMShellBaseModule` + (common to all SysUI) + 2) On TVs: `TvWMComponent` includes `TvWMShellModule` which includes `WMShellBaseModule` + 3) etc. +3) SysUI explicitly passes interfaces provided from the `WMComponent` to `SysUIComponent` via + the `SysUIComponent#Builder`, then builds the SysUI scoped components +4) `WMShell` is the SystemUI “service” (in the SysUI scope) that initializes with the app after the +SystemUI part of the dependency graph has been created. It contains the binding code between the +interfaces provided by the Shell and the rest of SystemUI. +5) SysUI can inject the interfaces into its own components + +More detail can be found in [go/wm-sysui-dagger](http://go/wm-sysui-dagger). + +## Interfaces from SysUI to Shell components + +Within the same process, the WM Shell components can be running on a different thread than the main +SysUI thread (disabled on certain products). This introduces challenges where we have to be +careful about how SysUI calls into the Shell and vice versa. + +As a result, we enforce explicit interfaces between SysUI and Shell components, and the +implementations of the interfaces on each side need to post to the right thread before it calls +into other code. + +For example, you might have: +1) (Shell) ShellFeature interface to be used from SysUI +2) (Shell) ShellFeatureController handles logic, implements ShellFeature interface and posts to + main Shell thread +3) SysUI application init injects Optional<ShellFeature> as an interface to SysUI to call +4) (SysUI) SysUIFeature depends on ShellFeature interface +5) (SysUI) SysUIFeature injects Optional<ShellFeature>, and sets up a callback for the Shell to + call, and the callback posts to the main SysUI thread + +Adding an interface to a Shell component may seem like a lot of boiler plate, but is currently +necessary to maintain proper threading and logic isolation. + +## Listening for Configuration changes & other SysUI events + +Aside from direct calls into Shell controllers for exposed features, the Shell also receives +common event callbacks from SysUI via the `ShellController`. This includes things like: + +- Configuration changes +- Keyguard events +- Shell init +- Shell dumps & commands + +For other events which are specific to the Shell feature, then you can add callback methods on +the Shell feature interface. Any such calls should <u>**never**</u> be synchronous calls as +they will need to post to the Shell main thread to run. + +## Shell commands & Dumps + +Since the Shell library is a part of the SysUI process, it relies on SysUI to trigger commands +on individual Shell components, or to dump individual shell components. + +```shell +# Dump everything +adb shell dumpsys activity service SystemUIService WMShell + +# Run a specific command +adb shell dumpsys activity service SystemUIService WMShell help +adb shell dumpsys activity service SystemUIService WMShell <cmd> <args> ... +```
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/testing.md b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/testing.md new file mode 100644 index 000000000000..8a80333facc4 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/testing.md @@ -0,0 +1,49 @@ +# Testing + +--- + +## Unit tests + +New WM Shell unit tests can be added to the +[Shell/tests/unittest](frameworks/base/libs/WindowManager/Shell/tests/unittest) directory, and can +be run via command line using `atest`: +```shell +atest WMShellUnitTests +``` + +If you use the SysUI Studio project, you can run and debug tests directly in the source files +(click on the little arrows next to the test class or test method). + +These unit tests are run as a part of WindowManager presubmit, and the dashboards for these unit +tests tests can be found at [go/wm-tests](http://go/wm-tests). + +This [GCL file](http://go/wm-unit-tests-gcl) configures the tests being run on the server. + +## Flicker tests + +Flicker tests are tests that perform actions and make assertions on the state in Window Manager +and SurfaceFlinger traces captured during the run. + +New WM Shell Flicker tests can be added to the +[Shell/tests/flicker](frameworks/base/libs/WindowManager/Shell/tests/flicker) directory, and can +be run via command line using `atest`: +```shell +atest WMShellFlickerTests +``` + +**Note**: Currently Flicker tests can only be run from the commandline and not via SysUI Studio + +A subset of the flicker tests tests are run as a part of WindowManager presubmit, and the +dashboards for these tests tests can be found at [go/wm-tests-flicker](http://go/wm-tests-flicker). + +## CTS tests + +Some windowing features also have CTS tests to ensure consistent behavior across OEMs. For example: +- Picture-in-Picture: + [PinnedStackTests](cts/tests/framework/base/windowmanager/src/android/server/wm/PinnedStackTests.java) +- etc. + +These can also be run via commandline only using `atest`, for example: +```shell +atest PinnedStackTests +```
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/threading.md b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/threading.md new file mode 100644 index 000000000000..eac748894432 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/threading.md @@ -0,0 +1,83 @@ +# Threading + +--- + +## Boundaries + +```text + Thread boundary + | + WM Shell | SystemUI + | + | +FeatureController <-> FeatureInterface <--|--> WMShell <-> SysUI + | (^post to shell thread) | (^post to main thread) + ... | + | | + OtherControllers | +``` + +## Threads + +We currently have multiple threads in use in the Shell library depending on the configuration by +the product. +- SysUI main thread (standard main thread) +- `ShellMainThread` (only used if the resource `config_enableShellMainThread` is set true + (ie. phones)) + - This falls back to the SysUI main thread otherwise + - **Note**: + - This thread runs with `THREAD_PRIORITY_DISPLAY` priority since so many windowing-critical + components depend on it + - This is also the UI thread for almost all UI created by the Shell + - The Shell main thread Handler (and the Executor that wraps it) is async, so + messages/runnables used via this Handler are handled immediately if there is no sync + messages prior to it in the queue. +- `ShellBackgroundThread` (for longer running tasks where we don't want to block the shell main + thread) + - This is always another thread even if config_enableShellMainThread is not set true + - **Note**: + - This thread runs with `THREAD_PRIORITY_BACKGROUND` priority +- `ShellAnimationThread` (currently only used for Transitions and Splitscreen, but potentially all + animations could be offloaded here) +- `ShellSplashScreenThread` (only for use with splashscreens) + +## Dagger setup + +The threading-related components are provided by the [WMShellConcurrencyModule](frameworks/base/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellConcurrencyModule.java), +for example, the Executors and Handlers for the various threads that are used. You can request +an executor of the necessary type by using the appropriate annotation for each of the threads (ie. +`@ShellMainThread Executor`) when injecting into your Shell component. + +To get the SysUI main thread, you can use the `@Main` annotation. + +## Best practices + +### Components +- Don't do initialization in the Shell component constructors + - If the host SysUI is not careful, it may construct the WMComponent dependencies on the main + thread, and this reduces the likelihood that components will intiailize on the wrong thread + in such cases +- Be careful of using CountDownLatch and other blocking synchronization mechanisms in Shell code + - If the Shell main thread is not a separate thread, this will cause a deadlock +- Callbacks, Observers, Listeners to any non-shell component should post onto main Shell thread + - This includes Binder calls, SysUI calls, BroadcastReceivers, etc. Basically any API that + takes a runnable should either be registered with the right Executor/Handler or posted to + the main Shell thread manually +- Since everything in the Shell runs on the main Shell thread, you do **not** need to explicitly + `synchronize` your code (unless you are trying to prevent reentrantcy, but that can also be + done in other ways) + +### Handlers/Executors +- You generally **never** need to create Handlers explicitly, instead inject `@ShellMainThread + ShellExecutor` instead + - This is a common pattern to defer logic in UI code, but the Handler created wraps the Looper + that is currently running, which can be wrong (see above for initialization vs construction) +- That said, sometimes Handlers are necessary because Framework API only takes Handlers or you + want to dedupe multiple messages + - In such cases inject `@ShellMainThread Handler` or use view.getHandler() which should be OK + assuming that the view root was initialized on the main Shell thread +- **Never use Looper.getMainLooper()** + - It's likely going to be wrong, you can inject `@Main ShellExecutor` to get the SysUI main thread + +### Testing +- You can use a `TestShellExecutor` to control the processing of messages
\ No newline at end of file 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 95de2dc61a43..b59fe1818780 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 @@ -60,29 +60,30 @@ import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.splitscreen.SplitScreenController; +import com.android.wm.shell.sysui.ConfigurationChangeListener; +import com.android.wm.shell.sysui.ShellController; +import com.android.wm.shell.sysui.ShellInit; import java.util.ArrayList; -import java.util.Optional; /** * Handles the global drag and drop handling for the Shell. */ public class DragAndDropController implements DisplayController.OnDisplaysChangedListener, - View.OnDragListener { + View.OnDragListener, ConfigurationChangeListener { private static final String TAG = DragAndDropController.class.getSimpleName(); private final Context mContext; + private final ShellController mShellController; private final DisplayController mDisplayController; private final DragAndDropEventLogger mLogger; private final IconProvider mIconProvider; private SplitScreenController mSplitScreen; private ShellExecutor mMainExecutor; - private DragAndDropImpl mImpl; private ArrayList<DragAndDropListener> mListeners = new ArrayList<>(); private final SparseArray<PerDisplay> mDisplayDropTargets = new SparseArray<>(); - private final SurfaceControl.Transaction mTransaction = new SurfaceControl.Transaction(); /** * Listener called during drag events, currently just onDragStarted. @@ -92,23 +93,40 @@ public class DragAndDropController implements DisplayController.OnDisplaysChange void onDragStarted(); } - public DragAndDropController(Context context, DisplayController displayController, - UiEventLogger uiEventLogger, IconProvider iconProvider, ShellExecutor mainExecutor) { + public DragAndDropController(Context context, + ShellInit shellInit, + ShellController shellController, + DisplayController displayController, + UiEventLogger uiEventLogger, + IconProvider iconProvider, + ShellExecutor mainExecutor) { mContext = context; + mShellController = shellController; mDisplayController = displayController; mLogger = new DragAndDropEventLogger(uiEventLogger); mIconProvider = iconProvider; mMainExecutor = mainExecutor; - mImpl = new DragAndDropImpl(); + shellInit.addInitCallback(this::onInit, this); } - public DragAndDrop asDragAndDrop() { - return mImpl; + /** + * Called when the controller is initialized. + */ + public void onInit() { + // TODO(b/238217847): The dependency from SplitscreenController on DragAndDropController is + // inverted, which leads to SplitscreenController not setting its instance until after + // onDisplayAdded. We can remove this post once we fix that dependency. + mMainExecutor.executeDelayed(() -> { + mDisplayController.addDisplayWindowListener(this); + }, 0); + mShellController.addConfigurationChangeListener(this); } - public void initialize(Optional<SplitScreenController> splitscreen) { - mSplitScreen = splitscreen.orElse(null); - mDisplayController.addDisplayWindowListener(this); + /** + * Sets the splitscreen controller to use if the feature is available. + */ + public void setSplitScreenController(SplitScreenController splitscreen) { + mSplitScreen = splitscreen; } /** Adds a listener to be notified of drag and drop events. */ @@ -240,12 +258,12 @@ public class DragAndDropController implements DisplayController.OnDisplaysChange break; case ACTION_DRAG_ENTERED: pd.dragLayout.show(); - pd.dragLayout.update(event); break; case ACTION_DRAG_LOCATION: pd.dragLayout.update(event); break; case ACTION_DROP: { + pd.dragLayout.update(event); return handleDrop(event, pd); } case ACTION_DRAG_EXITED: { @@ -310,13 +328,15 @@ public class DragAndDropController implements DisplayController.OnDisplaysChange return mimeTypes; } - private void onThemeChange() { + @Override + public void onThemeChanged() { for (int i = 0; i < mDisplayDropTargets.size(); i++) { mDisplayDropTargets.get(i).dragLayout.onThemeChange(); } } - private void onConfigChanged(Configuration newConfig) { + @Override + public void onConfigurationChanged(Configuration newConfig) { for (int i = 0; i < mDisplayDropTargets.size(); i++) { mDisplayDropTargets.get(i).dragLayout.onConfigChanged(newConfig); } @@ -342,21 +362,4 @@ public class DragAndDropController implements DisplayController.OnDisplaysChange dragLayout = dl; } } - - private class DragAndDropImpl implements DragAndDrop { - - @Override - public void onThemeChanged() { - mMainExecutor.execute(() -> { - DragAndDropController.this.onThemeChange(); - }); - } - - @Override - public void onConfigChanged(Configuration newConfig) { - mMainExecutor.execute(() -> { - DragAndDropController.this.onConfigChanged(newConfig); - }); - } - } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java index 756831007c35..62bf5172e106 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java @@ -17,6 +17,8 @@ package com.android.wm.shell.draganddrop; import static android.app.ActivityTaskManager.INVALID_TASK_ID; +import static android.app.ComponentOptions.KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED; +import static android.app.ComponentOptions.KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED_BY_PERMISSION; import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; @@ -45,12 +47,10 @@ import android.app.WindowConfiguration; import android.content.ActivityNotFoundException; import android.content.ClipData; import android.content.ClipDescription; -import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.pm.ActivityInfo; import android.content.pm.LauncherApps; -import android.content.pm.ResolveInfo; import android.graphics.Insets; import android.graphics.Rect; import android.os.Bundle; @@ -64,11 +64,9 @@ import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import com.android.internal.logging.InstanceId; -import com.android.internal.protolog.common.ProtoLog; import com.android.wm.shell.R; import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.split.SplitScreenConstants.SplitPosition; -import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.splitscreen.SplitScreenController; import java.lang.annotation.Retention; @@ -267,50 +265,14 @@ public class DragAndDropPolicy { mStarter.startShortcut(packageName, id, position, opts, user); } else { final PendingIntent launchIntent = intent.getParcelableExtra(EXTRA_PENDING_INTENT); - mStarter.startIntent(launchIntent, getStartIntentFillInIntent(launchIntent, position), - position, opts); + // Put BAL flags to avoid activity start aborted. + opts.putBoolean(KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED, true); + opts.putBoolean(KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED_BY_PERMISSION, true); + mStarter.startIntent(launchIntent, null /* fillIntent */, position, opts); } } /** - * Returns the fill-in intent to use when starting an app from a drop. - */ - @VisibleForTesting - Intent getStartIntentFillInIntent(PendingIntent launchIntent, @SplitPosition int position) { - // Get the drag app - final List<ResolveInfo> infos = launchIntent.queryIntentComponents(0 /* flags */); - final ComponentName dragIntentActivity = !infos.isEmpty() - ? infos.get(0).activityInfo.getComponentName() - : null; - - // Get the current app (either fullscreen or the remaining app post-drop if in splitscreen) - final boolean inSplitScreen = mSplitScreen != null - && mSplitScreen.isSplitScreenVisible(); - final ComponentName currentActivity; - if (!inSplitScreen) { - currentActivity = mSession.runningTaskInfo != null - ? mSession.runningTaskInfo.baseActivity - : null; - } else { - final int nonReplacedSplitPosition = position == SPLIT_POSITION_TOP_OR_LEFT - ? SPLIT_POSITION_BOTTOM_OR_RIGHT - : SPLIT_POSITION_TOP_OR_LEFT; - ActivityManager.RunningTaskInfo nonReplacedTaskInfo = - mSplitScreen.getTaskInfo(nonReplacedSplitPosition); - currentActivity = nonReplacedTaskInfo.baseActivity; - } - - if (currentActivity.equals(dragIntentActivity)) { - // Only apply MULTIPLE_TASK if we are dragging the same activity - final Intent fillInIntent = new Intent(); - fillInIntent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK); - ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, "Adding MULTIPLE_TASK"); - return fillInIntent; - } - return null; - } - - /** * Per-drag session data. */ private static class DragSession { 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 ff3c0834cf62..497a6f696df8 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 @@ -105,6 +105,10 @@ public class DragLayout extends LinearLayout { MATCH_PARENT)); ((LayoutParams) mDropZoneView1.getLayoutParams()).weight = 1; ((LayoutParams) mDropZoneView2.getLayoutParams()).weight = 1; + int orientation = getResources().getConfiguration().orientation; + setOrientation(orientation == Configuration.ORIENTATION_LANDSCAPE + ? LinearLayout.HORIZONTAL + : LinearLayout.VERTICAL); updateContainerMargins(getResources().getConfiguration().orientation); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/floating/FloatingDismissController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/floating/FloatingDismissController.java new file mode 100644 index 000000000000..83a1734dc71a --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/floating/FloatingDismissController.java @@ -0,0 +1,259 @@ +/* + * 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.floating; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; +import android.content.Context; +import android.content.res.Resources; +import android.view.MotionEvent; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.dynamicanimation.animation.DynamicAnimation; + +import com.android.wm.shell.R; +import com.android.wm.shell.bubbles.DismissView; +import com.android.wm.shell.common.magnetictarget.MagnetizedObject; +import com.android.wm.shell.floating.views.FloatingTaskLayer; +import com.android.wm.shell.floating.views.FloatingTaskView; + +import java.util.Objects; + +/** + * Controls a floating dismiss circle that has a 'magnetic' field around it, causing views moved + * close to the target to be stuck to it unless moved out again. + */ +public class FloatingDismissController { + + /** Velocity required to dismiss the view without dragging it into the dismiss target. */ + private static final float FLING_TO_DISMISS_MIN_VELOCITY = 4000f; + /** + * Max velocity that the view can be moving through the target with to stick (i.e. if it's + * more than this velocity, it will pass through the target. + */ + private static final float STICK_TO_TARGET_MAX_X_VELOCITY = 2000f; + /** + * Percentage of the target width to use to determine if an object flung towards the target + * should dismiss (e.g. if target is 100px and this is set ot 2f, anything flung within a + * 200px-wide area around the target will be considered 'near' enough get dismissed). + */ + private static final float FLING_TO_TARGET_WIDTH_PERCENT = 2f; + /** Minimum alpha to apply to the view being dismissed when it is in the target. */ + private static final float DISMISS_VIEW_MIN_ALPHA = 0.6f; + /** Amount to scale down the view being dismissed when it is in the target. */ + private static final float DISMISS_VIEW_SCALE_DOWN_PERCENT = 0.15f; + + private Context mContext; + private FloatingTasksController mController; + private FloatingTaskLayer mParent; + + private DismissView mDismissView; + private ValueAnimator mDismissAnimator; + private View mViewBeingDismissed; + private float mDismissSizePercent; + private float mDismissSize; + + /** + * The currently magnetized object, which is being dragged and will be attracted to the magnetic + * dismiss target. + */ + private MagnetizedObject<View> mMagnetizedObject; + /** + * The MagneticTarget instance for our circular dismiss view. This is added to the + * MagnetizedObject instances for the view being dragged. + */ + private MagnetizedObject.MagneticTarget mMagneticTarget; + /** Magnet listener that handles animating and dismissing the view. */ + private MagnetizedObject.MagnetListener mFloatingViewMagnetListener; + + public FloatingDismissController(Context context, FloatingTasksController controller, + FloatingTaskLayer parent) { + mContext = context; + mController = controller; + mParent = parent; + updateSizes(); + createAndAddDismissView(); + + mDismissAnimator = ValueAnimator.ofFloat(1f, 0f); + mDismissAnimator.addUpdateListener(animation -> { + final float value = (float) animation.getAnimatedValue(); + if (mDismissView != null) { + mDismissView.setPivotX((mDismissView.getRight() - mDismissView.getLeft()) / 2f); + mDismissView.setPivotY((mDismissView.getBottom() - mDismissView.getTop()) / 2f); + final float scaleValue = Math.max(value, mDismissSizePercent); + mDismissView.getCircle().setScaleX(scaleValue); + mDismissView.getCircle().setScaleY(scaleValue); + } + if (mViewBeingDismissed != null) { + // TODO: alpha doesn't actually apply to taskView currently. + mViewBeingDismissed.setAlpha(Math.max(value, DISMISS_VIEW_MIN_ALPHA)); + mViewBeingDismissed.setScaleX(Math.max(value, DISMISS_VIEW_SCALE_DOWN_PERCENT)); + mViewBeingDismissed.setScaleY(Math.max(value, DISMISS_VIEW_SCALE_DOWN_PERCENT)); + } + }); + + mFloatingViewMagnetListener = new MagnetizedObject.MagnetListener() { + @Override + public void onStuckToTarget( + @NonNull MagnetizedObject.MagneticTarget target) { + animateDismissing(/* dismissing= */ true); + } + + @Override + public void onUnstuckFromTarget(@NonNull MagnetizedObject.MagneticTarget target, + float velX, float velY, boolean wasFlungOut) { + animateDismissing(/* dismissing= */ false); + mParent.onUnstuckFromTarget((FloatingTaskView) mViewBeingDismissed, velX, velY, + wasFlungOut); + } + + @Override + public void onReleasedInTarget(@NonNull MagnetizedObject.MagneticTarget target) { + doDismiss(); + } + }; + } + + /** Updates all the sizes used and applies them to the {@link DismissView}. */ + public void updateSizes() { + Resources res = mContext.getResources(); + mDismissSize = res.getDimensionPixelSize( + R.dimen.floating_task_dismiss_circle_size); + final float minDismissSize = res.getDimensionPixelSize( + R.dimen.floating_dismiss_circle_small); + mDismissSizePercent = minDismissSize / mDismissSize; + + if (mDismissView != null) { + mDismissView.updateResources(); + } + } + + /** Prepares the view being dragged to be magnetic. */ + public void setUpMagneticObject(View viewBeingDragged) { + mViewBeingDismissed = viewBeingDragged; + mMagnetizedObject = getMagnetizedView(viewBeingDragged); + mMagnetizedObject.clearAllTargets(); + mMagnetizedObject.addTarget(mMagneticTarget); + mMagnetizedObject.setMagnetListener(mFloatingViewMagnetListener); + } + + /** Shows or hides the dismiss target. */ + public void showDismiss(boolean show) { + if (show) { + mDismissView.show(); + } else { + mDismissView.hide(); + } + } + + /** Passes the MotionEvent to the magnetized object and returns true if it was consumed. */ + public boolean passEventToMagnetizedObject(MotionEvent event) { + return mMagnetizedObject != null && mMagnetizedObject.maybeConsumeMotionEvent(event); + } + + private void createAndAddDismissView() { + if (mDismissView != null) { + mParent.removeView(mDismissView); + } + mDismissView = new DismissView(mContext); + mDismissView.setTargetSizeResId(R.dimen.floating_task_dismiss_circle_size); + mDismissView.updateResources(); + mParent.addView(mDismissView); + + final float dismissRadius = mDismissSize; + // Save the MagneticTarget instance for the newly set up view - we'll add this to the + // MagnetizedObjects when the dismiss view gets shown. + mMagneticTarget = new MagnetizedObject.MagneticTarget( + mDismissView.getCircle(), (int) dismissRadius); + } + + private MagnetizedObject<View> getMagnetizedView(View v) { + if (mMagnetizedObject != null + && Objects.equals(mMagnetizedObject.getUnderlyingObject(), v)) { + // Same view being dragged, we can reuse the magnetic object. + return mMagnetizedObject; + } + MagnetizedObject<View> magnetizedView = new MagnetizedObject<View>( + mContext, + v, + DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y + ) { + @Override + public float getWidth(@NonNull View underlyingObject) { + return underlyingObject.getWidth(); + } + + @Override + public float getHeight(@NonNull View underlyingObject) { + return underlyingObject.getHeight(); + } + + @Override + public void getLocationOnScreen(@NonNull View underlyingObject, + @NonNull int[] loc) { + loc[0] = (int) underlyingObject.getTranslationX(); + loc[1] = (int) underlyingObject.getTranslationY(); + } + }; + magnetizedView.setHapticsEnabled(true); + magnetizedView.setFlingToTargetMinVelocity(FLING_TO_DISMISS_MIN_VELOCITY); + magnetizedView.setStickToTargetMaxXVelocity(STICK_TO_TARGET_MAX_X_VELOCITY); + magnetizedView.setFlingToTargetWidthPercent(FLING_TO_TARGET_WIDTH_PERCENT); + return magnetizedView; + } + + /** Animates the dismiss treatment on the view being dismissed. */ + private void animateDismissing(boolean shouldDismiss) { + if (mViewBeingDismissed == null) { + return; + } + if (shouldDismiss) { + mDismissAnimator.removeAllListeners(); + mDismissAnimator.start(); + } else { + mDismissAnimator.removeAllListeners(); + mDismissAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + resetDismissAnimator(); + } + }); + mDismissAnimator.reverse(); + } + } + + /** Actually dismisses the view. */ + private void doDismiss() { + mDismissView.hide(); + mController.removeTask(); + resetDismissAnimator(); + mViewBeingDismissed = null; + } + + private void resetDismissAnimator() { + mDismissAnimator.removeAllListeners(); + mDismissAnimator.cancel(); + if (mDismissView != null) { + mDismissView.cancelAnimators(); + mDismissView.getCircle().setScaleX(1f); + mDismissView.getCircle().setScaleY(1f); + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/floating/FloatingTasks.java b/libs/WindowManager/Shell/src/com/android/wm/shell/floating/FloatingTasks.java new file mode 100644 index 000000000000..935666026bf4 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/floating/FloatingTasks.java @@ -0,0 +1,41 @@ +/* + * 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.floating; + +import android.content.Intent; + +import com.android.wm.shell.common.annotations.ExternalThread; + +/** + * Interface to interact with floating tasks. + */ +@ExternalThread +public interface FloatingTasks { + + /** + * Shows, stashes, or un-stashes the floating task depending on state: + * - If there is no floating task for this intent, it shows the task for the provided intent. + * - If there is a floating task for this intent, but it's stashed, this un-stashes it. + * - If there is a floating task for this intent, and it's not stashed, this stashes it. + */ + void showOrSetStashed(Intent intent); + + /** Returns a binder that can be passed to an external process to manipulate FloatingTasks. */ + default IFloatingTasks createExternalInterface() { + return null; + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/floating/FloatingTasksController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/floating/FloatingTasksController.java new file mode 100644 index 000000000000..67552991869b --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/floating/FloatingTasksController.java @@ -0,0 +1,455 @@ +/* + * 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.floating; + +import static android.app.ActivityTaskManager.INVALID_TASK_ID; +import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; + +import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission; +import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_FLOATING_APPS; + +import android.annotation.Nullable; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ShortcutInfo; +import android.content.res.Configuration; +import android.graphics.PixelFormat; +import android.graphics.Point; +import android.os.SystemProperties; +import android.view.ViewGroup; +import android.view.WindowManager; + +import androidx.annotation.BinderThread; +import androidx.annotation.VisibleForTesting; + +import com.android.internal.protolog.common.ProtoLog; +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.TaskViewTransitions; +import com.android.wm.shell.bubbles.BubbleController; +import com.android.wm.shell.common.RemoteCallable; +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.SyncTransactionQueue; +import com.android.wm.shell.common.annotations.ExternalThread; +import com.android.wm.shell.common.annotations.ShellBackgroundThread; +import com.android.wm.shell.common.annotations.ShellMainThread; +import com.android.wm.shell.floating.views.FloatingTaskLayer; +import com.android.wm.shell.floating.views.FloatingTaskView; +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 java.io.PrintWriter; +import java.util.Objects; +import java.util.Optional; + +/** + * Entry point for creating and managing floating tasks. + * + * A single window layer is added and the task(s) are displayed using a {@link FloatingTaskView} + * within that window. + * + * Currently optimized for a single task. Multiple tasks are not supported. + */ +public class FloatingTasksController implements RemoteCallable<FloatingTasksController>, + ConfigurationChangeListener { + + private static final String TAG = FloatingTasksController.class.getSimpleName(); + + public static final boolean FLOATING_TASKS_ENABLED = + SystemProperties.getBoolean("persist.wm.debug.floating_tasks", false); + public static final boolean SHOW_FLOATING_TASKS_AS_BUBBLES = + SystemProperties.getBoolean("persist.wm.debug.floating_tasks_as_bubbles", false); + + @VisibleForTesting + static final int SMALLEST_SCREEN_WIDTH_DP_TO_BE_TABLET = 600; + + // Only used for testing + private Configuration mConfig; + private boolean mFloatingTasksEnabledForTests; + + private FloatingTaskImpl mImpl = new FloatingTaskImpl(); + private Context mContext; + private ShellController mShellController; + private ShellCommandHandler mShellCommandHandler; + private @Nullable BubbleController mBubbleController; + private WindowManager mWindowManager; + private ShellTaskOrganizer mTaskOrganizer; + private TaskViewTransitions mTaskViewTransitions; + private @ShellMainThread ShellExecutor mMainExecutor; + // TODO: mBackgroundThread is not used but we'll probs need it eventually? + private @ShellBackgroundThread ShellExecutor mBackgroundThread; + private SyncTransactionQueue mSyncQueue; + + private boolean mIsFloatingLayerAdded; + private FloatingTaskLayer mFloatingTaskLayer; + private final Point mLastPosition = new Point(-1, -1); + + private Task mTask; + + // Simple class to hold onto info for intent or shortcut based tasks. + public static class Task { + public int taskId = INVALID_TASK_ID; + @Nullable + public Intent intent; + @Nullable + public ShortcutInfo info; + @Nullable + public FloatingTaskView floatingView; + } + + public FloatingTasksController(Context context, + ShellInit shellInit, + ShellController shellController, + ShellCommandHandler shellCommandHandler, + Optional<BubbleController> bubbleController, + WindowManager windowManager, + ShellTaskOrganizer organizer, + TaskViewTransitions transitions, + @ShellMainThread ShellExecutor mainExecutor, + @ShellBackgroundThread ShellExecutor bgExceutor, + SyncTransactionQueue syncTransactionQueue) { + mContext = context; + mShellController = shellController; + mShellCommandHandler = shellCommandHandler; + mBubbleController = bubbleController.get(); + mWindowManager = windowManager; + mTaskOrganizer = organizer; + mTaskViewTransitions = transitions; + mMainExecutor = mainExecutor; + mBackgroundThread = bgExceutor; + mSyncQueue = syncTransactionQueue; + if (isFloatingTasksEnabled()) { + shellInit.addInitCallback(this::onInit, this); + } + mShellCommandHandler.addDumpCallback(this::dump, this); + } + + protected void onInit() { + mShellController.addConfigurationChangeListener(this); + } + + /** Only used for testing. */ + @VisibleForTesting + void setConfig(Configuration config) { + mConfig = config; + } + + /** Only used for testing. */ + @VisibleForTesting + void setFloatingTasksEnabled(boolean enabled) { + mFloatingTasksEnabledForTests = enabled; + } + + /** Whether the floating layer is available. */ + boolean isFloatingLayerAvailable() { + Configuration config = mConfig == null + ? mContext.getResources().getConfiguration() + : mConfig; + return config.smallestScreenWidthDp >= SMALLEST_SCREEN_WIDTH_DP_TO_BE_TABLET; + } + + /** Whether floating tasks are enabled. */ + boolean isFloatingTasksEnabled() { + return FLOATING_TASKS_ENABLED || mFloatingTasksEnabledForTests; + } + + @Override + public void onThemeChanged() { + if (mIsFloatingLayerAdded) { + mFloatingTaskLayer.updateSizes(); + } + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + // TODO: probably other stuff here to do (e.g. handle rotation) + if (mIsFloatingLayerAdded) { + mFloatingTaskLayer.updateSizes(); + } + } + + /** Returns false if the task shouldn't be shown. */ + private boolean canShowTask(Intent intent) { + ProtoLog.d(WM_SHELL_FLOATING_APPS, "canShowTask -- %s", intent); + if (!isFloatingTasksEnabled() || !isFloatingLayerAvailable()) return false; + if (intent == null) { + ProtoLog.e(WM_SHELL_FLOATING_APPS, "canShowTask given null intent, doing nothing"); + return false; + } + return true; + } + + /** Returns true if the task was or should be shown as a bubble. */ + private boolean maybeShowTaskAsBubble(Intent intent) { + if (SHOW_FLOATING_TASKS_AS_BUBBLES && mBubbleController != null) { + removeFloatingLayer(); + if (intent.getPackage() != null) { + mBubbleController.addAppBubble(intent); + ProtoLog.d(WM_SHELL_FLOATING_APPS, "showing floating task as bubble: %s", intent); + } else { + ProtoLog.d(WM_SHELL_FLOATING_APPS, + "failed to show floating task as bubble: %s; unknown package", intent); + } + return true; + } + return false; + } + + /** + * Shows, stashes, or un-stashes the floating task depending on state: + * - If there is no floating task for this intent, it shows this the provided task. + * - If there is a floating task for this intent, but it's stashed, this un-stashes it. + * - If there is a floating task for this intent, and it's not stashed, this stashes it. + */ + public void showOrSetStashed(Intent intent) { + if (!canShowTask(intent)) return; + if (maybeShowTaskAsBubble(intent)) return; + + addFloatingLayer(); + + if (isTaskAttached(mTask) && intent.filterEquals(mTask.intent)) { + // The task is already added, toggle the stash state. + mFloatingTaskLayer.setStashed(mTask, !mTask.floatingView.isStashed()); + return; + } + + // If we're here it's either a new or different task + showNewTask(intent); + } + + /** + * Shows a floating task with the provided intent. + * If the same task is present it will un-stash it or do nothing if it is already un-stashed. + * Removes any other floating tasks that might exist. + */ + public void showTask(Intent intent) { + if (!canShowTask(intent)) return; + if (maybeShowTaskAsBubble(intent)) return; + + addFloatingLayer(); + + if (isTaskAttached(mTask) && intent.filterEquals(mTask.intent)) { + // The task is already added, show it if it's stashed. + if (mTask.floatingView.isStashed()) { + mFloatingTaskLayer.setStashed(mTask, false); + } + return; + } + showNewTask(intent); + } + + private void showNewTask(Intent intent) { + if (mTask != null && !intent.filterEquals(mTask.intent)) { + mFloatingTaskLayer.removeAllTaskViews(); + mTask.floatingView.cleanUpTaskView(); + mTask = null; + } + + FloatingTaskView ftv = new FloatingTaskView(mContext, this); + ftv.createTaskView(mContext, mTaskOrganizer, mTaskViewTransitions, mSyncQueue); + + mTask = new Task(); + mTask.floatingView = ftv; + mTask.intent = intent; + + // Add & start the task. + mFloatingTaskLayer.addTask(mTask); + ProtoLog.d(WM_SHELL_FLOATING_APPS, "showNewTask, startingIntent: %s", intent); + mTask.floatingView.startTask(mMainExecutor, mTask); + } + + /** + * Removes the task and cleans up the view. + */ + public void removeTask() { + if (mTask != null) { + ProtoLog.d(WM_SHELL_FLOATING_APPS, "Removing task with id=%d", mTask.taskId); + + if (mTask.floatingView != null) { + // TODO: animate it + mFloatingTaskLayer.removeView(mTask.floatingView); + mTask.floatingView.cleanUpTaskView(); + } + removeFloatingLayer(); + } + } + + /** + * Whether there is a floating task and if it is stashed. + */ + public boolean isStashed() { + return isTaskAttached(mTask) && mTask.floatingView.isStashed(); + } + + /** + * If a floating task exists, this sets whether it is stashed and animates if needed. + */ + public void setStashed(boolean shouldStash) { + if (mTask != null && mTask.floatingView != null && mIsFloatingLayerAdded) { + mFloatingTaskLayer.setStashed(mTask, shouldStash); + } + } + + /** + * Saves the last position the floating task was in so that it can be put there again. + */ + public void setLastPosition(int x, int y) { + mLastPosition.set(x, y); + } + + /** + * Returns the last position the floating task was in. + */ + public Point getLastPosition() { + return mLastPosition; + } + + /** + * Whether the provided task has a view that's attached to the floating layer. + */ + private boolean isTaskAttached(Task t) { + return t != null && t.floatingView != null + && mIsFloatingLayerAdded + && mFloatingTaskLayer.getTaskViewCount() > 0 + && Objects.equals(mFloatingTaskLayer.getFirstTaskView(), t.floatingView); + } + + // TODO: when this is added, if there are bubbles, they get hidden? Is only one layer of this + // type allowed? Bubbles & floating tasks should probably be in the same layer to reduce + // # of windows. + private void addFloatingLayer() { + if (mIsFloatingLayerAdded) { + return; + } + + mFloatingTaskLayer = new FloatingTaskLayer(mContext, this, mWindowManager); + + WindowManager.LayoutParams params = new WindowManager.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT, + WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY, + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE + | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL + | WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED, + PixelFormat.TRANSLUCENT + ); + params.setTrustedOverlay(); + params.setFitInsetsTypes(0); + params.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE; + params.setTitle("FloatingTaskLayer"); + params.packageName = mContext.getPackageName(); + params.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; + params.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS; + + try { + mIsFloatingLayerAdded = true; + mWindowManager.addView(mFloatingTaskLayer, params); + } catch (IllegalStateException e) { + // This means the floating layer has already been added which shouldn't happen. + e.printStackTrace(); + } + } + + private void removeFloatingLayer() { + if (!mIsFloatingLayerAdded) { + return; + } + try { + mIsFloatingLayerAdded = false; + if (mFloatingTaskLayer != null) { + mWindowManager.removeView(mFloatingTaskLayer); + } + } catch (IllegalArgumentException e) { + // This means the floating layer has already been removed which shouldn't happen. + e.printStackTrace(); + } + } + + /** + * Description of current floating task state. + */ + private void dump(PrintWriter pw, String prefix) { + pw.println("FloatingTaskController state:"); + pw.print(" isFloatingLayerAvailable= "); pw.println(isFloatingLayerAvailable()); + pw.print(" isFloatingTasksEnabled= "); pw.println(isFloatingTasksEnabled()); + pw.print(" mIsFloatingLayerAdded= "); pw.println(mIsFloatingLayerAdded); + pw.print(" mLastPosition= "); pw.println(mLastPosition); + pw.println(); + } + + /** Returns the {@link FloatingTasks} implementation. */ + public FloatingTasks asFloatingTasks() { + return mImpl; + } + + @Override + public Context getContext() { + return mContext; + } + + @Override + public ShellExecutor getRemoteCallExecutor() { + return mMainExecutor; + } + + /** + * The interface for calls from outside the shell, within the host process. + */ + @ExternalThread + private class FloatingTaskImpl implements FloatingTasks { + private IFloatingTasksImpl mIFloatingTasks; + + @Override + public void showOrSetStashed(Intent intent) { + mMainExecutor.execute(() -> FloatingTasksController.this.showOrSetStashed(intent)); + } + + @Override + public IFloatingTasks createExternalInterface() { + if (mIFloatingTasks != null) { + mIFloatingTasks.invalidate(); + } + mIFloatingTasks = new IFloatingTasksImpl(FloatingTasksController.this); + return mIFloatingTasks; + } + } + + /** + * The interface for calls from outside the host process. + */ + @BinderThread + private static class IFloatingTasksImpl extends IFloatingTasks.Stub { + private FloatingTasksController mController; + + IFloatingTasksImpl(FloatingTasksController controller) { + mController = controller; + } + + /** + * Invalidates this instance, preventing future calls from updating the controller. + */ + void invalidate() { + mController = null; + } + + public void showTask(Intent intent) { + executeRemoteCallWithTaskPermission(mController, "showTask", + (controller) -> controller.showTask(intent)); + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/DividerState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/floating/IFloatingTasks.aidl index af2ab158ab46..f79ca1039865 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/DividerState.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/floating/IFloatingTasks.aidl @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016 The Android Open Source Project + * 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. @@ -14,12 +14,15 @@ * limitations under the License. */ -package com.android.wm.shell.legacysplitscreen; +package com.android.wm.shell.floating; + +import android.content.Intent; /** - * Class to hold state of divider that needs to persist across configuration changes. + * Interface that is exposed to remote callers to manipulate floating task features. */ -final class DividerState { - public boolean animateAfterRecentsDrawn; - public float mRatioPositionBeforeMinimized; +interface IFloatingTasks { + + void showTask(in Intent intent) = 1; + } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/floating/views/FloatingMenuView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/floating/views/FloatingMenuView.java new file mode 100644 index 000000000000..c922109751ba --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/floating/views/FloatingMenuView.java @@ -0,0 +1,73 @@ +/* + * 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.floating.views; + +import android.annotation.Nullable; +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.LinearLayout; + +import com.android.wm.shell.R; + +/** + * Displays the menu items for a floating task view (e.g. close). + */ +public class FloatingMenuView extends LinearLayout { + + private int mItemSize; + private int mItemMargin; + + public FloatingMenuView(Context context) { + super(context); + setOrientation(LinearLayout.HORIZONTAL); + setGravity(Gravity.CENTER); + + mItemSize = context.getResources().getDimensionPixelSize( + R.dimen.floating_task_menu_item_size); + mItemMargin = context.getResources().getDimensionPixelSize( + R.dimen.floating_task_menu_item_padding); + } + + /** Adds a clickable item to the menu bar. Items are ordered as added. */ + public void addMenuItem(@Nullable Drawable drawable, View.OnClickListener listener) { + ImageView itemView = new ImageView(getContext()); + itemView.setScaleType(ImageView.ScaleType.CENTER); + if (drawable != null) { + itemView.setImageDrawable(drawable); + } + LinearLayout.LayoutParams lp = new LayoutParams(mItemSize, + ViewGroup.LayoutParams.MATCH_PARENT); + lp.setMarginStart(mItemMargin); + lp.setMarginEnd(mItemMargin); + addView(itemView, lp); + + itemView.setOnClickListener(listener); + } + + /** + * The menu extends past the top of the TaskView because of the rounded corners. This means + * to center content in the menu we must subtract the radius (i.e. the amount of space covered + * by TaskView). + */ + public void setCornerRadius(float radius) { + setPadding(getPaddingLeft(), getPaddingTop(), getPaddingRight(), (int) radius); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/floating/views/FloatingTaskLayer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/floating/views/FloatingTaskLayer.java new file mode 100644 index 000000000000..16dab2415bf2 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/floating/views/FloatingTaskLayer.java @@ -0,0 +1,687 @@ +/* + * 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.floating.views; + +import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_FLOATING_APPS; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.annotation.Nullable; +import android.content.Context; +import android.graphics.Color; +import android.graphics.Insets; +import android.graphics.Point; +import android.graphics.Rect; +import android.graphics.Region; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewPropertyAnimator; +import android.view.ViewTreeObserver; +import android.view.WindowInsets; +import android.view.WindowManager; +import android.view.WindowMetrics; +import android.widget.FrameLayout; + +import androidx.annotation.NonNull; +import androidx.dynamicanimation.animation.DynamicAnimation; +import androidx.dynamicanimation.animation.FlingAnimation; + +import com.android.internal.protolog.common.ProtoLog; +import com.android.wm.shell.R; +import com.android.wm.shell.floating.FloatingDismissController; +import com.android.wm.shell.floating.FloatingTasksController; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * This is the layout that {@link FloatingTaskView}s are contained in. It handles input and + * movement of the task views. + */ +public class FloatingTaskLayer extends FrameLayout + implements ViewTreeObserver.OnComputeInternalInsetsListener { + + private static final String TAG = FloatingTaskLayer.class.getSimpleName(); + + /** How big to make the task view based on screen width of the largest size. */ + private static final float START_SIZE_WIDTH_PERCENT = 0.33f; + /** Min fling velocity required to move the view from one side of the screen to the other. */ + private static final float ESCAPE_VELOCITY = 750f; + /** Amount of friction to apply to fling animations. */ + private static final float FLING_FRICTION = 1.9f; + + private final FloatingTasksController mController; + private final FloatingDismissController mDismissController; + private final WindowManager mWindowManager; + private final TouchHandlerImpl mTouchHandler; + + private final Region mTouchableRegion = new Region(); + private final Rect mPositionRect = new Rect(); + private final Point mDefaultStartPosition = new Point(); + private final Point mTaskViewSize = new Point(); + private WindowInsets mWindowInsets; + private int mVerticalPadding; + private int mOverhangWhenStashed; + + private final List<Rect> mSystemGestureExclusionRects = Collections.singletonList(new Rect()); + private ViewTreeObserver.OnDrawListener mSystemGestureExclusionListener = + this::updateSystemGestureExclusion; + + /** Interface allowing something to handle the touch events going to a task. */ + interface FloatingTaskTouchHandler { + void onDown(@NonNull FloatingTaskView v, @NonNull MotionEvent ev, + float viewInitialX, float viewInitialY); + + void onMove(@NonNull FloatingTaskView v, @NonNull MotionEvent ev, + float dx, float dy); + + void onUp(@NonNull FloatingTaskView v, @NonNull MotionEvent ev, + float dx, float dy, float velX, float velY); + + void onClick(@NonNull FloatingTaskView v); + } + + public FloatingTaskLayer(Context context, + FloatingTasksController controller, + WindowManager windowManager) { + super(context); + // TODO: Why is this necessary? Without it FloatingTaskView does not render correctly. + setBackgroundColor(Color.argb(0, 0, 0, 0)); + + mController = controller; + mWindowManager = windowManager; + updateSizes(); + + // TODO: Might make sense to put dismiss controller in the touch handler since that's the + // main user of dismiss controller. + mDismissController = new FloatingDismissController(context, mController, this); + mTouchHandler = new TouchHandlerImpl(); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + getViewTreeObserver().addOnComputeInternalInsetsListener(this); + getViewTreeObserver().addOnDrawListener(mSystemGestureExclusionListener); + setOnApplyWindowInsetsListener((view, windowInsets) -> { + if (!windowInsets.equals(mWindowInsets)) { + mWindowInsets = windowInsets; + updateSizes(); + } + return windowInsets; + }); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + getViewTreeObserver().removeOnComputeInternalInsetsListener(this); + getViewTreeObserver().removeOnDrawListener(mSystemGestureExclusionListener); + } + + @Override + public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo) { + inoutInfo.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION); + mTouchableRegion.setEmpty(); + getTouchableRegion(mTouchableRegion); + inoutInfo.touchableRegion.set(mTouchableRegion); + } + + /** Adds a floating task to the layout. */ + public void addTask(FloatingTasksController.Task task) { + if (task.floatingView == null) return; + + task.floatingView.setTouchHandler(mTouchHandler); + addView(task.floatingView, new LayoutParams(mTaskViewSize.x, mTaskViewSize.y)); + updateTaskViewPosition(task.floatingView); + } + + /** Animates the stashed state of the provided task, if it's part of the floating layer. */ + public void setStashed(FloatingTasksController.Task task, boolean shouldStash) { + if (task.floatingView != null && task.floatingView.getParent() == this) { + mTouchHandler.stashTaskView(task.floatingView, shouldStash); + } + } + + /** Removes all {@link FloatingTaskView} from the layout. */ + public void removeAllTaskViews() { + int childCount = getChildCount(); + ArrayList<View> viewsToRemove = new ArrayList<>(); + for (int i = 0; i < childCount; i++) { + if (getChildAt(i) instanceof FloatingTaskView) { + viewsToRemove.add(getChildAt(i)); + } + } + for (View v : viewsToRemove) { + removeView(v); + } + } + + /** Returns the number of task views in the layout. */ + public int getTaskViewCount() { + int taskViewCount = 0; + int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + if (getChildAt(i) instanceof FloatingTaskView) { + taskViewCount++; + } + } + return taskViewCount; + } + + /** + * Called when the task view is un-stuck from the dismiss target. + * @param v the task view being moved. + * @param velX the x velocity of the motion event. + * @param velY the y velocity of the motion event. + * @param wasFlungOut true if the user flung the task view out of the dismiss target (i.e. there + * was an 'up' event), otherwise the user is still dragging. + */ + public void onUnstuckFromTarget(FloatingTaskView v, float velX, float velY, + boolean wasFlungOut) { + mTouchHandler.onUnstuckFromTarget(v, velX, velY, wasFlungOut); + } + + /** + * Updates dimensions and applies them to any task views. + */ + public void updateSizes() { + if (mDismissController != null) { + mDismissController.updateSizes(); + } + + mOverhangWhenStashed = getResources().getDimensionPixelSize( + R.dimen.floating_task_stash_offset); + mVerticalPadding = getResources().getDimensionPixelSize( + R.dimen.floating_task_vertical_padding); + + WindowMetrics windowMetrics = mWindowManager.getCurrentWindowMetrics(); + WindowInsets windowInsets = windowMetrics.getWindowInsets(); + Insets insets = windowInsets.getInsetsIgnoringVisibility(WindowInsets.Type.navigationBars() + | WindowInsets.Type.statusBars() + | WindowInsets.Type.displayCutout()); + Rect bounds = windowMetrics.getBounds(); + mPositionRect.set(bounds.left + insets.left, + bounds.top + insets.top + mVerticalPadding, + bounds.right - insets.right, + bounds.bottom - insets.bottom - mVerticalPadding); + + int taskViewWidth = Math.max(bounds.height(), bounds.width()); + int taskViewHeight = Math.min(bounds.height(), bounds.width()); + taskViewHeight = taskViewHeight - (insets.top + insets.bottom + (mVerticalPadding * 2)); + mTaskViewSize.set((int) (taskViewWidth * START_SIZE_WIDTH_PERCENT), taskViewHeight); + mDefaultStartPosition.set(mPositionRect.left, mPositionRect.top); + + // Update existing views + int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + if (getChildAt(i) instanceof FloatingTaskView) { + FloatingTaskView child = (FloatingTaskView) getChildAt(i); + LayoutParams lp = (LayoutParams) child.getLayoutParams(); + lp.width = mTaskViewSize.x; + lp.height = mTaskViewSize.y; + child.setLayoutParams(lp); + updateTaskViewPosition(child); + } + } + } + + /** Returns the first floating task view in the layout. (Currently only ever 1 view). */ + @Nullable + public FloatingTaskView getFirstTaskView() { + int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + View child = getChildAt(i); + if (child instanceof FloatingTaskView) { + return (FloatingTaskView) child; + } + } + return null; + } + + private void updateTaskViewPosition(FloatingTaskView floatingView) { + Point lastPosition = mController.getLastPosition(); + if (lastPosition.x == -1 && lastPosition.y == -1) { + floatingView.setX(mDefaultStartPosition.x); + floatingView.setY(mDefaultStartPosition.y); + } else { + floatingView.setX(lastPosition.x); + floatingView.setY(lastPosition.y); + } + if (mTouchHandler.isStashedPosition(floatingView)) { + floatingView.setStashed(true); + } + floatingView.updateLocation(); + } + + /** + * Updates the area of the screen that shouldn't allow the back gesture due to the placement + * of task view (i.e. when task view is stashed on an edge, tapping or swiping that edge would + * un-stash the task view instead of performing the back gesture). + */ + private void updateSystemGestureExclusion() { + Rect excludeZone = mSystemGestureExclusionRects.get(0); + FloatingTaskView floatingTaskView = getFirstTaskView(); + if (floatingTaskView != null && floatingTaskView.isStashed()) { + excludeZone.set(floatingTaskView.getLeft(), + floatingTaskView.getTop(), + floatingTaskView.getRight(), + floatingTaskView.getBottom()); + excludeZone.offset((int) (floatingTaskView.getTranslationX()), + (int) (floatingTaskView.getTranslationY())); + setSystemGestureExclusionRects(mSystemGestureExclusionRects); + } else { + excludeZone.setEmpty(); + setSystemGestureExclusionRects(Collections.emptyList()); + } + } + + /** + * Fills in the touchable region for floating windows. This is used by WindowManager to + * decide which touch events go to the floating windows. + */ + private void getTouchableRegion(Region outRegion) { + int childCount = getChildCount(); + Rect temp = new Rect(); + for (int i = 0; i < childCount; i++) { + View child = getChildAt(i); + if (child instanceof FloatingTaskView) { + child.getBoundsOnScreen(temp); + outRegion.op(temp, Region.Op.UNION); + } + } + } + + /** + * Implementation of the touch handler. Animates the task view based on touch events. + */ + private class TouchHandlerImpl implements FloatingTaskTouchHandler { + /** + * The view can be stashed by swiping it towards the current edge or moving it there. If + * the view gets moved in a way that is not one of these gestures, this is flipped to false. + */ + private boolean mCanStash = true; + /** + * This is used to indicate that the view has been un-stuck from the dismiss target and + * needs to spring to the current touch location. + */ + // TODO: implement this behavior + private boolean mSpringToTouchOnNextMotionEvent = false; + + private ArrayList<FlingAnimation> mFlingAnimations; + private ViewPropertyAnimator mViewPropertyAnimation; + + private float mViewInitialX; + private float mViewInitialY; + + private float[] mMinMax = new float[2]; + + @Override + public void onDown(@NonNull FloatingTaskView v, @NonNull MotionEvent ev, float viewInitialX, + float viewInitialY) { + mCanStash = true; + mViewInitialX = viewInitialX; + mViewInitialY = viewInitialY; + mDismissController.setUpMagneticObject(v); + mDismissController.passEventToMagnetizedObject(ev); + } + + @Override + public void onMove(@NonNull FloatingTaskView v, @NonNull MotionEvent ev, + float dx, float dy) { + // Shows the magnetic dismiss target if needed. + mDismissController.showDismiss(/* show= */ true); + + // Send it to magnetic target first. + if (mDismissController.passEventToMagnetizedObject(ev)) { + v.setStashed(false); + mCanStash = true; + + return; + } + + // If we're here magnetic target didn't want it so move as per normal. + + v.setTranslationX(capX(v, mViewInitialX + dx, /* isMoving= */ true)); + v.setTranslationY(capY(v, mViewInitialY + dy)); + if (v.isStashed()) { + // Check if we've moved far enough to be not stashed. + final float centerX = mPositionRect.centerX() - (v.getWidth() / 2f); + final boolean viewInitiallyOnLeftSide = mViewInitialX < centerX; + if (viewInitiallyOnLeftSide) { + if (v.getTranslationX() > mPositionRect.left) { + v.setStashed(false); + mCanStash = true; + } + } else if (v.getTranslationX() + v.getWidth() < mPositionRect.right) { + v.setStashed(false); + mCanStash = true; + } + } + } + + // Reference for math / values: StackAnimationController#flingStackThenSpringToEdge. + // TODO clean up the code here, pretty hard to comprehend + // TODO code here doesn't work the best when in portrait (e.g. can't fling up/down on edges) + @Override + public void onUp(@NonNull FloatingTaskView v, @NonNull MotionEvent ev, + float dx, float dy, float velX, float velY) { + + // Send it to magnetic target first. + if (mDismissController.passEventToMagnetizedObject(ev)) { + v.setStashed(false); + return; + } + mDismissController.showDismiss(/* show= */ false); + + // If we're here magnetic target didn't want it so handle up as per normal. + + final float x = capX(v, mViewInitialX + dx, /* isMoving= */ false); + final float centerX = mPositionRect.centerX(); + final boolean viewInitiallyOnLeftSide = mViewInitialX + v.getWidth() < centerX; + final boolean viewOnLeftSide = x + v.getWidth() < centerX; + final boolean isFling = Math.abs(velX) > ESCAPE_VELOCITY; + final boolean isFlingLeft = isFling && velX < ESCAPE_VELOCITY; + // TODO: check velX here sometimes it doesn't stash on move when I think it should + final boolean shouldStashFromMove = + (velX < 0 && v.getTranslationX() < mPositionRect.left) + || (velX > 0 + && v.getTranslationX() + v.getWidth() > mPositionRect.right); + final boolean shouldStashFromFling = viewInitiallyOnLeftSide == viewOnLeftSide + && isFling + && ((viewOnLeftSide && velX < ESCAPE_VELOCITY) + || (!viewOnLeftSide && velX > ESCAPE_VELOCITY)); + final boolean shouldStash = mCanStash && (shouldStashFromFling || shouldStashFromMove); + + ProtoLog.d(WM_SHELL_FLOATING_APPS, + "shouldStash=%s shouldStashFromFling=%s shouldStashFromMove=%s" + + " viewInitiallyOnLeftSide=%s viewOnLeftSide=%s isFling=%s velX=%f" + + " isStashed=%s", shouldStash, shouldStashFromFling, shouldStashFromMove, + viewInitiallyOnLeftSide, viewOnLeftSide, isFling, velX, v.isStashed()); + + if (v.isStashed()) { + mMinMax[0] = viewOnLeftSide + ? mPositionRect.left - v.getWidth() + mOverhangWhenStashed + : mPositionRect.right - v.getWidth(); + mMinMax[1] = viewOnLeftSide + ? mPositionRect.left + : mPositionRect.right - mOverhangWhenStashed; + } else { + populateMinMax(v, viewOnLeftSide, shouldStash, mMinMax); + } + + boolean movingLeft = isFling ? isFlingLeft : viewOnLeftSide; + float destinationRelativeX = movingLeft + ? mMinMax[0] + : mMinMax[1]; + + // TODO: why is this necessary / when does this happen? + if (mMinMax[1] < v.getTranslationX()) { + mMinMax[1] = v.getTranslationX(); + } + if (v.getTranslationX() < mMinMax[0]) { + mMinMax[0] = v.getTranslationX(); + } + + // Use the touch event's velocity if it's sufficient, otherwise use the minimum velocity + // so that it'll make it all the way to the side of the screen. + final float minimumVelocityToReachEdge = + getMinimumVelocityToReachEdge(v, destinationRelativeX); + final float startXVelocity = movingLeft + ? Math.min(minimumVelocityToReachEdge, velX) + : Math.max(minimumVelocityToReachEdge, velX); + + cancelAnyAnimations(v); + + mFlingAnimations = getAnimationForUpEvent(v, shouldStash, + startXVelocity, mMinMax[0], mMinMax[1], destinationRelativeX); + for (int i = 0; i < mFlingAnimations.size(); i++) { + mFlingAnimations.get(i).start(); + } + } + + @Override + public void onClick(@NonNull FloatingTaskView v) { + if (v.isStashed()) { + final float centerX = mPositionRect.centerX() - (v.getWidth() / 2f); + final boolean viewOnLeftSide = v.getTranslationX() < centerX; + final float destinationRelativeX = viewOnLeftSide + ? mPositionRect.left + : mPositionRect.right - v.getWidth(); + final float minimumVelocityToReachEdge = + getMinimumVelocityToReachEdge(v, destinationRelativeX); + populateMinMax(v, viewOnLeftSide, /* stashed= */ true, mMinMax); + + cancelAnyAnimations(v); + + FlingAnimation flingAnimation = new FlingAnimation(v, + DynamicAnimation.TRANSLATION_X); + flingAnimation.setFriction(FLING_FRICTION) + .setStartVelocity(minimumVelocityToReachEdge) + .setMinValue(mMinMax[0]) + .setMaxValue(mMinMax[1]) + .addEndListener((animation, canceled, value, velocity) -> { + if (canceled) return; + mController.setLastPosition((int) v.getTranslationX(), + (int) v.getTranslationY()); + v.setStashed(false); + v.updateLocation(); + }); + mFlingAnimations = new ArrayList<>(); + mFlingAnimations.add(flingAnimation); + flingAnimation.start(); + } + } + + public void onUnstuckFromTarget(FloatingTaskView v, float velX, float velY, + boolean wasFlungOut) { + if (wasFlungOut) { + snapTaskViewToEdge(v, velX, /* shouldStash= */ false); + } else { + // TODO: use this for something / to spring the view to the touch location + mSpringToTouchOnNextMotionEvent = true; + } + } + + public void stashTaskView(FloatingTaskView v, boolean shouldStash) { + if (v.isStashed() == shouldStash) { + return; + } + final float centerX = mPositionRect.centerX() - (v.getWidth() / 2f); + final boolean viewOnLeftSide = v.getTranslationX() < centerX; + snapTaskViewToEdge(v, viewOnLeftSide ? -ESCAPE_VELOCITY : ESCAPE_VELOCITY, shouldStash); + } + + public boolean isStashedPosition(View v) { + return v.getTranslationX() < mPositionRect.left + || v.getTranslationX() + v.getWidth() > mPositionRect.right; + } + + // TODO: a lot of this is duplicated in onUp -- can it be unified? + private void snapTaskViewToEdge(FloatingTaskView v, float velX, boolean shouldStash) { + final boolean movingLeft = velX < ESCAPE_VELOCITY; + populateMinMax(v, movingLeft, shouldStash, mMinMax); + float destinationRelativeX = movingLeft + ? mMinMax[0] + : mMinMax[1]; + + // TODO: why is this necessary / when does this happen? + if (mMinMax[1] < v.getTranslationX()) { + mMinMax[1] = v.getTranslationX(); + } + if (v.getTranslationX() < mMinMax[0]) { + mMinMax[0] = v.getTranslationX(); + } + + // Use the touch event's velocity if it's sufficient, otherwise use the minimum velocity + // so that it'll make it all the way to the side of the screen. + final float minimumVelocityToReachEdge = + getMinimumVelocityToReachEdge(v, destinationRelativeX); + final float startXVelocity = movingLeft + ? Math.min(minimumVelocityToReachEdge, velX) + : Math.max(minimumVelocityToReachEdge, velX); + + cancelAnyAnimations(v); + + mFlingAnimations = getAnimationForUpEvent(v, + shouldStash, startXVelocity, mMinMax[0], mMinMax[1], + destinationRelativeX); + for (int i = 0; i < mFlingAnimations.size(); i++) { + mFlingAnimations.get(i).start(); + } + } + + private void cancelAnyAnimations(FloatingTaskView v) { + if (mFlingAnimations != null) { + for (int i = 0; i < mFlingAnimations.size(); i++) { + if (mFlingAnimations.get(i).isRunning()) { + mFlingAnimations.get(i).cancel(); + } + } + } + if (mViewPropertyAnimation != null) { + mViewPropertyAnimation.cancel(); + mViewPropertyAnimation = null; + } + } + + private ArrayList<FlingAnimation> getAnimationForUpEvent(FloatingTaskView v, + boolean shouldStash, float startVelX, float minValue, float maxValue, + float destinationRelativeX) { + final float ty = v.getTranslationY(); + final ArrayList<FlingAnimation> animations = new ArrayList<>(); + if (ty != capY(v, ty)) { + // The view was being dismissed so the Y is out of bounds, need to animate that. + FlingAnimation yFlingAnimation = new FlingAnimation(v, + DynamicAnimation.TRANSLATION_Y); + yFlingAnimation.setFriction(FLING_FRICTION) + .setStartVelocity(startVelX) + .setMinValue(mPositionRect.top) + .setMaxValue(mPositionRect.bottom - mTaskViewSize.y); + animations.add(yFlingAnimation); + } + FlingAnimation flingAnimation = new FlingAnimation(v, DynamicAnimation.TRANSLATION_X); + flingAnimation.setFriction(FLING_FRICTION) + .setStartVelocity(startVelX) + .setMinValue(minValue) + .setMaxValue(maxValue) + .addEndListener((animation, canceled, value, velocity) -> { + if (canceled) return; + Runnable endAction = () -> { + v.setStashed(shouldStash); + v.updateLocation(); + if (!v.isStashed()) { + mController.setLastPosition((int) v.getTranslationX(), + (int) v.getTranslationY()); + } + }; + if (!shouldStash) { + final int xTranslation = (int) v.getTranslationX(); + if (xTranslation != destinationRelativeX) { + // TODO: this animation doesn't feel great, should figure out + // a better way to do this or remove the need for it all together. + mViewPropertyAnimation = v.animate() + .translationX(destinationRelativeX) + .setListener(getAnimationListener(endAction)); + mViewPropertyAnimation.start(); + } else { + endAction.run(); + } + } else { + endAction.run(); + } + }); + animations.add(flingAnimation); + return animations; + } + + private AnimatorListenerAdapter getAnimationListener(Runnable endAction) { + return new AnimatorListenerAdapter() { + boolean translationCanceled = false; + @Override + public void onAnimationCancel(Animator animation) { + super.onAnimationCancel(animation); + translationCanceled = true; + } + + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + if (!translationCanceled) { + endAction.run(); + } + } + }; + } + + private void populateMinMax(FloatingTaskView v, boolean onLeft, boolean shouldStash, + float[] out) { + if (shouldStash) { + out[0] = onLeft + ? mPositionRect.left - v.getWidth() + mOverhangWhenStashed + : mPositionRect.right - v.getWidth(); + out[1] = onLeft + ? mPositionRect.left + : mPositionRect.right - mOverhangWhenStashed; + } else { + out[0] = mPositionRect.left; + out[1] = mPositionRect.right - mTaskViewSize.x; + } + } + + private float getMinimumVelocityToReachEdge(FloatingTaskView v, + float destinationRelativeX) { + // Minimum velocity required for the view to make it to the targeted side of the screen, + // taking friction into account (4.2f is the number that friction scalars are multiplied + // by in DynamicAnimation.DragForce). This is an estimate and could be slightly off, the + // animation at the end will ensure that it reaches the destination X regardless. + return (destinationRelativeX - v.getTranslationX()) * (FLING_FRICTION * 4.2f); + } + + private float capX(FloatingTaskView v, float x, boolean isMoving) { + final int width = v.getWidth(); + if (v.isStashed() || isMoving) { + if (x < mPositionRect.left - v.getWidth() + mOverhangWhenStashed) { + return mPositionRect.left - v.getWidth() + mOverhangWhenStashed; + } + if (x > mPositionRect.right - mOverhangWhenStashed) { + return mPositionRect.right - mOverhangWhenStashed; + } + } else { + if (x < mPositionRect.left) { + return mPositionRect.left; + } + if (x > mPositionRect.right - width) { + return mPositionRect.right - width; + } + } + return x; + } + + private float capY(FloatingTaskView v, float y) { + final int height = v.getHeight(); + if (y < mPositionRect.top) { + return mPositionRect.top; + } + if (y > mPositionRect.bottom - height) { + return mPositionRect.bottom - height; + } + return y; + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/floating/views/FloatingTaskView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/floating/views/FloatingTaskView.java new file mode 100644 index 000000000000..581204a82ec7 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/floating/views/FloatingTaskView.java @@ -0,0 +1,385 @@ +/* + * 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.floating.views; + +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; + +import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_FLOATING_APPS; + +import android.app.ActivityManager; +import android.app.ActivityOptions; +import android.app.ActivityTaskManager; +import android.app.PendingIntent; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.res.TypedArray; +import android.graphics.Color; +import android.graphics.Outline; +import android.graphics.Rect; +import android.os.RemoteException; +import android.util.Log; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewOutlineProvider; +import android.widget.FrameLayout; + +import androidx.annotation.NonNull; + +import com.android.internal.policy.ScreenDecorationsUtils; +import com.android.internal.protolog.common.ProtoLog; +import com.android.wm.shell.R; +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.TaskView; +import com.android.wm.shell.TaskViewTransitions; +import com.android.wm.shell.bubbles.RelativeTouchListener; +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.SyncTransactionQueue; +import com.android.wm.shell.common.annotations.ShellMainThread; +import com.android.wm.shell.floating.FloatingTasksController; + +/** + * A view that holds a floating task using {@link TaskView} along with additional UI to manage + * the task. + */ +public class FloatingTaskView extends FrameLayout { + + private static final String TAG = FloatingTaskView.class.getSimpleName(); + + private FloatingTasksController mController; + + private FloatingMenuView mMenuView; + private int mMenuHeight; + private TaskView mTaskView; + + private float mCornerRadius = 0f; + private int mBackgroundColor; + + private FloatingTasksController.Task mTask; + + private boolean mIsStashed; + + /** + * Creates a floating task view. + * + * @param context the context to use. + * @param controller the controller to notify about changes in the floating task (e.g. removal). + */ + public FloatingTaskView(Context context, FloatingTasksController controller) { + super(context); + mController = controller; + setElevation(getResources().getDimensionPixelSize(R.dimen.floating_task_elevation)); + mMenuHeight = context.getResources().getDimensionPixelSize(R.dimen.floating_task_menu_size); + mMenuView = new FloatingMenuView(context); + addView(mMenuView); + + applyThemeAttrs(); + + setClipToOutline(true); + setOutlineProvider(new ViewOutlineProvider() { + @Override + public void getOutline(View view, Outline outline) { + outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), mCornerRadius); + } + }); + } + + // TODO: call this when theme/config changes + void applyThemeAttrs() { + boolean supportsRoundedCorners = ScreenDecorationsUtils.supportsRoundedCornersOnWindows( + mContext.getResources()); + final TypedArray ta = mContext.obtainStyledAttributes(new int[] { + android.R.attr.dialogCornerRadius, + android.R.attr.colorBackgroundFloating}); + mCornerRadius = supportsRoundedCorners ? ta.getDimensionPixelSize(0, 0) : 0; + mCornerRadius = mCornerRadius / 2f; + mBackgroundColor = ta.getColor(1, Color.WHITE); + + ta.recycle(); + + mMenuView.setCornerRadius(mCornerRadius); + mMenuHeight = getResources().getDimensionPixelSize( + R.dimen.floating_task_menu_size); + + if (mTaskView != null) { + mTaskView.setCornerRadius(mCornerRadius); + } + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + int height = MeasureSpec.getSize(heightMeasureSpec); + + // Add corner radius here so that the menu extends behind the rounded corners of TaskView. + int menuViewHeight = Math.min((int) (mMenuHeight + mCornerRadius), height); + measureChild(mMenuView, widthMeasureSpec, MeasureSpec.makeMeasureSpec(menuViewHeight, + MeasureSpec.getMode(heightMeasureSpec))); + + if (mTaskView != null) { + int taskViewHeight = height - menuViewHeight; + measureChild(mTaskView, widthMeasureSpec, MeasureSpec.makeMeasureSpec(taskViewHeight, + MeasureSpec.getMode(heightMeasureSpec))); + } + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + // Drag handle above + final int dragHandleBottom = t + mMenuView.getMeasuredHeight(); + mMenuView.layout(l, t, r, dragHandleBottom); + if (mTaskView != null) { + // Subtract radius so that the menu extends behind the rounded corners of TaskView. + mTaskView.layout(l, (int) (dragHandleBottom - mCornerRadius), r, + dragHandleBottom + mTaskView.getMeasuredHeight()); + } + } + + /** + * Constructs the TaskView to display the task. Must be called for {@link #startTask} to work. + */ + public void createTaskView(Context context, ShellTaskOrganizer organizer, + TaskViewTransitions transitions, SyncTransactionQueue syncQueue) { + mTaskView = new TaskView(context, organizer, transitions, syncQueue); + addView(mTaskView); + mTaskView.setEnableSurfaceClipping(true); + mTaskView.setCornerRadius(mCornerRadius); + } + + /** + * Starts the provided task in the TaskView, if the TaskView exists. This should be called after + * {@link #createTaskView}. + */ + public void startTask(@ShellMainThread ShellExecutor executor, + FloatingTasksController.Task task) { + if (mTaskView == null) { + Log.e(TAG, "starting task before creating the view!"); + return; + } + mTask = task; + mTaskView.setListener(executor, mTaskViewListener); + } + + /** + * Sets the touch handler for the view. + * + * @param handler the touch handler for the view. + */ + public void setTouchHandler(FloatingTaskLayer.FloatingTaskTouchHandler handler) { + setOnTouchListener(new RelativeTouchListener() { + @Override + public boolean onDown(@NonNull View v, @NonNull MotionEvent ev) { + handler.onDown(FloatingTaskView.this, ev, v.getTranslationX(), v.getTranslationY()); + return true; + } + + @Override + public void onMove(@NonNull View v, @NonNull MotionEvent ev, float viewInitialX, + float viewInitialY, float dx, float dy) { + handler.onMove(FloatingTaskView.this, ev, dx, dy); + } + + @Override + public void onUp(@NonNull View v, @NonNull MotionEvent ev, float viewInitialX, + float viewInitialY, float dx, float dy, float velX, float velY) { + handler.onUp(FloatingTaskView.this, ev, dx, dy, velX, velY); + } + }); + setOnClickListener(view -> { + handler.onClick(FloatingTaskView.this); + }); + + mMenuView.addMenuItem(null, view -> { + if (mIsStashed) { + // If we're stashed all clicks un-stash. + handler.onClick(FloatingTaskView.this); + } + }); + } + + private void setContentVisibility(boolean visible) { + if (mTaskView == null) return; + mTaskView.setAlpha(visible ? 1f : 0f); + } + + /** + * Sets the alpha of both this view and the TaskView. + */ + public void setTaskViewAlpha(float alpha) { + if (mTaskView != null) { + mTaskView.setAlpha(alpha); + } + setAlpha(alpha); + } + + /** + * Call when the location or size of the view has changed to update TaskView. + */ + public void updateLocation() { + if (mTaskView == null) return; + mTaskView.onLocationChanged(); + } + + private void updateMenuColor() { + ActivityManager.RunningTaskInfo info = mTaskView.getTaskInfo(); + int color = info != null ? info.taskDescription.getBackgroundColor() : -1; + if (color != -1) { + mMenuView.setBackgroundColor(color); + } else { + mMenuView.setBackgroundColor(mBackgroundColor); + } + } + + /** + * Sets whether the view is stashed or not. + * + * Also updates the touchable area based on this. If the view is stashed we don't direct taps + * on the activity to the activity, instead a tap will un-stash the view. + */ + public void setStashed(boolean isStashed) { + if (mIsStashed != isStashed) { + mIsStashed = isStashed; + if (mTaskView == null) { + return; + } + updateObscuredTouchRect(); + } + } + + /** Whether the view is stashed at the edge of the screen or not. **/ + public boolean isStashed() { + return mIsStashed; + } + + private void updateObscuredTouchRect() { + if (mIsStashed) { + Rect tmpRect = new Rect(); + getBoundsOnScreen(tmpRect); + mTaskView.setObscuredTouchRect(tmpRect); + } else { + mTaskView.setObscuredTouchRect(null); + } + } + + /** + * Whether the task needs to be restarted, this can happen when {@link #cleanUpTaskView()} has + * been called on this view or if + * {@link #startTask(ShellExecutor, FloatingTasksController.Task)} was never called. + */ + public boolean needsTaskStarted() { + // If the task needs to be restarted then TaskView would have been cleaned up. + return mTaskView == null; + } + + /** Call this when the floating task activity is no longer in use. */ + public void cleanUpTaskView() { + if (mTask != null && mTask.taskId != INVALID_TASK_ID) { + try { + ActivityTaskManager.getService().removeTask(mTask.taskId); + } catch (RemoteException e) { + Log.e(TAG, e.getMessage()); + } + } + if (mTaskView != null) { + mTaskView.release(); + removeView(mTaskView); + mTaskView = null; + } + } + + // TODO: use task background colour / how to get the taskInfo ? + private static int getDragBarColor(ActivityManager.RunningTaskInfo taskInfo) { + final int taskBgColor = taskInfo.taskDescription.getStatusBarColor(); + return Color.valueOf(taskBgColor == -1 ? Color.WHITE : taskBgColor).toArgb(); + } + + private final TaskView.Listener mTaskViewListener = new TaskView.Listener() { + private boolean mInitialized = false; + private boolean mDestroyed = false; + + @Override + public void onInitialized() { + if (mDestroyed || mInitialized) { + return; + } + // Custom options so there is no activity transition animation + ActivityOptions options = ActivityOptions.makeCustomAnimation(getContext(), + /* enterResId= */ 0, /* exitResId= */ 0); + + Rect launchBounds = new Rect(); + mTaskView.getBoundsOnScreen(launchBounds); + + try { + options.setTaskAlwaysOnTop(true); + if (mTask.intent != null) { + Intent fillInIntent = new Intent(); + // Apply flags to make behaviour match documentLaunchMode=always. + fillInIntent.addFlags(FLAG_ACTIVITY_NEW_DOCUMENT); + fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK); + + PendingIntent pi = PendingIntent.getActivity(mContext, 0, mTask.intent, + PendingIntent.FLAG_MUTABLE, + null); + mTaskView.startActivity(pi, fillInIntent, options, launchBounds); + } else { + ProtoLog.e(WM_SHELL_FLOATING_APPS, "Tried to start a task with null intent"); + } + } catch (RuntimeException e) { + ProtoLog.e(WM_SHELL_FLOATING_APPS, "Exception while starting task: %s", + e.getMessage()); + mController.removeTask(); + } + mInitialized = true; + } + + @Override + public void onReleased() { + mDestroyed = true; + } + + @Override + public void onTaskCreated(int taskId, ComponentName name) { + mTask.taskId = taskId; + updateMenuColor(); + setContentVisibility(true); + } + + @Override + public void onTaskVisibilityChanged(int taskId, boolean visible) { + setContentVisibility(visible); + } + + @Override + public void onTaskRemovalStarted(int taskId) { + // Must post because this is called from a binder thread. + post(() -> { + mController.removeTask(); + cleanUpTaskView(); + }); + } + + @Override + public void onBackPressedOnTaskRoot(int taskId) { + if (mTask.taskId == taskId && !mIsStashed) { + // TODO: is removing the window the desired behavior? + post(() -> mController.removeTask()); + } + } + }; +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformComponents.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformComponents.java new file mode 100644 index 000000000000..eee5aaee3ec3 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformComponents.java @@ -0,0 +1,59 @@ +/* + * 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.freeform; + +import static android.content.pm.PackageManager.FEATURE_FREEFORM_WINDOW_MANAGEMENT; +import static android.provider.Settings.Global.DEVELOPMENT_ENABLE_FREEFORM_WINDOWS_SUPPORT; + +import android.content.Context; +import android.provider.Settings; + +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.transition.Transitions; + +import java.util.Optional; + +/** + * Class that holds freeform related classes. It serves as the single injection point of + * all freeform classes to avoid leaking implementation details to the base Dagger module. + */ +public class FreeformComponents { + public final ShellTaskOrganizer.TaskListener mTaskListener; + public final Optional<Transitions.TransitionHandler> mTransitionHandler; + public final Optional<Transitions.TransitionObserver> mTransitionObserver; + + /** + * Creates an instance with the given components. + */ + public FreeformComponents( + ShellTaskOrganizer.TaskListener taskListener, + Optional<Transitions.TransitionHandler> transitionHandler, + Optional<Transitions.TransitionObserver> transitionObserver) { + mTaskListener = taskListener; + mTransitionHandler = transitionHandler; + mTransitionObserver = transitionObserver; + } + + /** + * Returns if this device supports freeform. + */ + public static boolean isFreeformEnabled(Context context) { + return context.getPackageManager().hasSystemFeature(FEATURE_FREEFORM_WINDOW_MANAGEMENT) + || Settings.Global.getInt(context.getContentResolver(), + DEVELOPMENT_ENABLE_FREEFORM_WINDOWS_SUPPORT, 0) != 0; + } +} 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 fef9be36a35f..e2d5a499d1e1 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 @@ -16,97 +16,156 @@ package com.android.wm.shell.freeform; -import static android.content.pm.PackageManager.FEATURE_FREEFORM_WINDOW_MANAGEMENT; -import static android.provider.Settings.Global.DEVELOPMENT_ENABLE_FREEFORM_WINDOWS_SUPPORT; +import static com.android.wm.shell.ShellTaskOrganizer.TASK_LISTENER_TYPE_FREEFORM; import android.app.ActivityManager.RunningTaskInfo; -import android.content.Context; -import android.graphics.Point; -import android.graphics.Rect; -import android.provider.Settings; -import android.util.Slog; +import android.util.Log; import android.util.SparseArray; import android.view.SurfaceControl; +import android.window.TransitionInfo; + +import androidx.annotation.Nullable; import com.android.internal.protolog.common.ProtoLog; import com.android.wm.shell.ShellTaskOrganizer; -import com.android.wm.shell.common.SyncTransactionQueue; +import com.android.wm.shell.desktopmode.DesktopModeStatus; +import com.android.wm.shell.desktopmode.DesktopModeTaskRepository; import com.android.wm.shell.protolog.ShellProtoLogGroup; +import com.android.wm.shell.sysui.ShellInit; +import com.android.wm.shell.transition.Transitions; +import com.android.wm.shell.windowdecor.WindowDecorViewModel; import java.io.PrintWriter; +import java.util.Optional; /** * {@link ShellTaskOrganizer.TaskListener} for {@link * ShellTaskOrganizer#TASK_LISTENER_TYPE_FREEFORM}. + * + * @param <T> the type of window decoration instance */ -public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener { +public class FreeformTaskListener<T extends AutoCloseable> + implements ShellTaskOrganizer.TaskListener { private static final String TAG = "FreeformTaskListener"; - private final SyncTransactionQueue mSyncQueue; + private final ShellTaskOrganizer mShellTaskOrganizer; + private final Optional<DesktopModeTaskRepository> mDesktopModeTaskRepository; + private final WindowDecorViewModel<T> mWindowDecorationViewModel; - private final SparseArray<State> mTasks = new SparseArray<>(); + private final SparseArray<State<T>> mTasks = new SparseArray<>(); + private final SparseArray<T> mWindowDecorOfVanishedTasks = new SparseArray<>(); - private static class State { + private static class State<T extends AutoCloseable> { RunningTaskInfo mTaskInfo; SurfaceControl mLeash; + T mWindowDecoration; + } + + public FreeformTaskListener( + ShellInit shellInit, + ShellTaskOrganizer shellTaskOrganizer, + Optional<DesktopModeTaskRepository> desktopModeTaskRepository, + WindowDecorViewModel<T> windowDecorationViewModel) { + mShellTaskOrganizer = shellTaskOrganizer; + mWindowDecorationViewModel = windowDecorationViewModel; + mDesktopModeTaskRepository = desktopModeTaskRepository; + if (shellInit != null) { + shellInit.addInitCallback(this::onInit, this); + } } - public FreeformTaskListener(SyncTransactionQueue syncQueue) { - mSyncQueue = syncQueue; + private void onInit() { + mShellTaskOrganizer.addListenerForType(this, TASK_LISTENER_TYPE_FREEFORM); } @Override public void onTaskAppeared(RunningTaskInfo taskInfo, SurfaceControl leash) { - if (mTasks.get(taskInfo.taskId) != null) { - throw new RuntimeException("Task appeared more than once: #" + taskInfo.taskId); - } ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TASK_ORG, "Freeform Task Appeared: #%d", taskInfo.taskId); - final State state = new State(); + final State<T> state = createOrUpdateTaskState(taskInfo, leash); + if (!Transitions.ENABLE_SHELL_TRANSITIONS) { + SurfaceControl.Transaction t = new SurfaceControl.Transaction(); + state.mWindowDecoration = + mWindowDecorationViewModel.createWindowDecoration(taskInfo, leash, t, t); + t.apply(); + } + + if (DesktopModeStatus.IS_SUPPORTED && taskInfo.isVisible) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE, + "Adding active freeform task: #%d", taskInfo.taskId); + mDesktopModeTaskRepository.ifPresent(it -> it.addActiveTask(taskInfo.taskId)); + } + } + + private State<T> createOrUpdateTaskState(RunningTaskInfo taskInfo, SurfaceControl leash) { + State<T> state = mTasks.get(taskInfo.taskId); + if (state != null) { + updateTaskInfo(taskInfo); + return state; + } + + state = new State<>(); state.mTaskInfo = taskInfo; state.mLeash = leash; mTasks.put(taskInfo.taskId, state); - final Rect taskBounds = taskInfo.configuration.windowConfiguration.getBounds(); - mSyncQueue.runInSync(t -> { - Point taskPosition = taskInfo.positionInParent; - t.setPosition(leash, taskPosition.x, taskPosition.y) - .setWindowCrop(leash, taskBounds.width(), taskBounds.height()) - .show(leash); - }); + return state; } @Override public void onTaskVanished(RunningTaskInfo taskInfo) { - State state = mTasks.get(taskInfo.taskId); + final State<T> state = mTasks.get(taskInfo.taskId); if (state == null) { - Slog.e(TAG, "Task already vanished: #" + taskInfo.taskId); + // This is possible if the transition happens before this method. return; } ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TASK_ORG, "Freeform Task Vanished: #%d", taskInfo.taskId); mTasks.remove(taskInfo.taskId); + + if (DesktopModeStatus.IS_SUPPORTED) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE, + "Removing active freeform task: #%d", taskInfo.taskId); + mDesktopModeTaskRepository.ifPresent(it -> it.removeActiveTask(taskInfo.taskId)); + } + + if (Transitions.ENABLE_SHELL_TRANSITIONS) { + // Save window decorations of closing tasks so that we can hand them over to the + // transition system if this method happens before the transition. In case where the + // transition didn't happen, it'd be cleared when the next transition finished. + if (state.mWindowDecoration != null) { + mWindowDecorOfVanishedTasks.put(taskInfo.taskId, state.mWindowDecoration); + } + return; + } + releaseWindowDecor(state.mWindowDecoration); } @Override public void onTaskInfoChanged(RunningTaskInfo taskInfo) { - State state = mTasks.get(taskInfo.taskId); - if (state == null) { - throw new RuntimeException( - "Task info changed before appearing: #" + taskInfo.taskId); - } + final State<T> state = updateTaskInfo(taskInfo); ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TASK_ORG, "Freeform Task Info Changed: #%d", taskInfo.taskId); - state.mTaskInfo = taskInfo; + if (state.mWindowDecoration != null) { + mWindowDecorationViewModel.onTaskInfoChanged(state.mTaskInfo, state.mWindowDecoration); + } - final Rect taskBounds = taskInfo.configuration.windowConfiguration.getBounds(); - final SurfaceControl leash = state.mLeash; - mSyncQueue.runInSync(t -> { - Point taskPosition = taskInfo.positionInParent; - t.setPosition(leash, taskPosition.x, taskPosition.y) - .setWindowCrop(leash, taskBounds.width(), taskBounds.height()) - .show(leash); - }); + if (DesktopModeStatus.IS_SUPPORTED) { + if (taskInfo.isVisible) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE, + "Adding active freeform task: #%d", taskInfo.taskId); + mDesktopModeTaskRepository.ifPresent(it -> it.addActiveTask(taskInfo.taskId)); + } + } + } + + private State<T> updateTaskInfo(RunningTaskInfo taskInfo) { + final State<T> state = mTasks.get(taskInfo.taskId); + if (state == null) { + throw new RuntimeException("Task info changed before appearing: #" + taskInfo.taskId); + } + state.mTaskInfo = taskInfo; + return state; } @Override @@ -127,6 +186,103 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener { return mTasks.get(taskId).mLeash; } + /** + * Creates a window decoration for a transition. + * + * @param change the change of this task transition that needs to have the task layer as the + * leash + * @return {@code true} if it creates the window decoration; {@code false} otherwise + */ + boolean createWindowDecoration( + TransitionInfo.Change change, + SurfaceControl.Transaction startT, + SurfaceControl.Transaction finishT) { + final State<T> state = createOrUpdateTaskState(change.getTaskInfo(), change.getLeash()); + if (state.mWindowDecoration != null) { + return false; + } + state.mWindowDecoration = mWindowDecorationViewModel.createWindowDecoration( + state.mTaskInfo, state.mLeash, startT, finishT); + return true; + } + + /** + * Gives out the ownership of the task's window decoration. The given task is leaving (of has + * left) this task listener. This is the transition system asking for the ownership. + * + * @param taskInfo the maximizing task + * @return the window decor of the maximizing task if any + */ + T giveWindowDecoration( + RunningTaskInfo taskInfo, + SurfaceControl.Transaction startT, + SurfaceControl.Transaction finishT) { + T windowDecor; + final State<T> state = mTasks.get(taskInfo.taskId); + if (state != null) { + windowDecor = state.mWindowDecoration; + state.mWindowDecoration = null; + } else { + windowDecor = + mWindowDecorOfVanishedTasks.removeReturnOld(taskInfo.taskId); + } + if (windowDecor == null) { + return null; + } + mWindowDecorationViewModel.setupWindowDecorationForTransition( + taskInfo, startT, finishT, windowDecor); + return windowDecor; + } + + /** + * Adopt the incoming window decoration and lets the window decoration prepare for a transition. + * + * @param change the change of this task transition that needs to have the task layer as the + * leash + * @param startT the start transaction of this transition + * @param finishT the finish transaction of this transition + * @param windowDecor the window decoration to adopt + * @return {@code true} if it adopts the window decoration; {@code false} otherwise + */ + boolean adoptWindowDecoration( + TransitionInfo.Change change, + SurfaceControl.Transaction startT, + SurfaceControl.Transaction finishT, + @Nullable AutoCloseable windowDecor) { + final State<T> state = createOrUpdateTaskState(change.getTaskInfo(), change.getLeash()); + state.mWindowDecoration = mWindowDecorationViewModel.adoptWindowDecoration(windowDecor); + if (state.mWindowDecoration != null) { + mWindowDecorationViewModel.setupWindowDecorationForTransition( + state.mTaskInfo, startT, finishT, state.mWindowDecoration); + return true; + } else { + state.mWindowDecoration = mWindowDecorationViewModel.createWindowDecoration( + state.mTaskInfo, state.mLeash, startT, finishT); + return false; + } + } + + void onTaskTransitionFinished() { + if (mWindowDecorOfVanishedTasks.size() == 0) { + return; + } + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, + "Clearing window decors of vanished tasks. There could be visual defects " + + "if any of them is used later in transitions."); + for (int i = 0; i < mWindowDecorOfVanishedTasks.size(); ++i) { + releaseWindowDecor(mWindowDecorOfVanishedTasks.valueAt(i)); + } + mWindowDecorOfVanishedTasks.clear(); + } + + private void releaseWindowDecor(T windowDecor) { + try { + windowDecor.close(); + } catch (Exception e) { + Log.e(TAG, "Failed to release window decoration.", e); + } + } + @Override public void dump(PrintWriter pw, String prefix) { final String innerPrefix = prefix + " "; @@ -138,16 +294,4 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener { public String toString() { return TAG; } - - /** - * Checks if freeform support is enabled in system. - * - * @param context context used to check settings and package manager. - * @return {@code true} if freeform is enabled, {@code false} if not. - */ - public static boolean isFreeformEnabled(Context context) { - return context.getPackageManager().hasSystemFeature(FEATURE_FREEFORM_WINDOW_MANAGEMENT) - || Settings.Global.getInt(context.getContentResolver(), - DEVELOPMENT_ENABLE_FREEFORM_WINDOWS_SUPPORT, 0) != 0; - } } 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 new file mode 100644 index 000000000000..fd4c85fad77f --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionHandler.java @@ -0,0 +1,174 @@ +/* + * 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.freeform; + +import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; +import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; + +import android.app.ActivityManager; +import android.app.WindowConfiguration; +import android.os.IBinder; +import android.view.SurfaceControl; +import android.view.WindowManager; +import android.window.TransitionInfo; +import android.window.TransitionRequestInfo; +import android.window.WindowContainerTransaction; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.wm.shell.sysui.ShellInit; +import com.android.wm.shell.transition.Transitions; +import com.android.wm.shell.windowdecor.WindowDecorViewModel; + +import java.util.ArrayList; +import java.util.List; + +/** + * The {@link Transitions.TransitionHandler} that handles freeform task maximizing and restoring + * transitions. + */ +public class FreeformTaskTransitionHandler + implements Transitions.TransitionHandler, FreeformTaskTransitionStarter { + + private final Transitions mTransitions; + private final WindowDecorViewModel<?> mWindowDecorViewModel; + + private final List<IBinder> mPendingTransitionTokens = new ArrayList<>(); + + public FreeformTaskTransitionHandler( + ShellInit shellInit, + Transitions transitions, + WindowDecorViewModel<?> windowDecorViewModel) { + mTransitions = transitions; + mWindowDecorViewModel = windowDecorViewModel; + if (Transitions.ENABLE_SHELL_TRANSITIONS) { + shellInit.addInitCallback(this::onInit, this); + } + } + + private void onInit() { + mWindowDecorViewModel.setFreeformTaskTransitionStarter(this); + } + + @Override + public void startWindowingModeTransition( + int targetWindowingMode, WindowContainerTransaction wct) { + final int type; + switch (targetWindowingMode) { + case WINDOWING_MODE_FULLSCREEN: + type = Transitions.TRANSIT_MAXIMIZE; + break; + case WINDOWING_MODE_FREEFORM: + type = Transitions.TRANSIT_RESTORE_FROM_MAXIMIZE; + break; + default: + throw new IllegalArgumentException("Unexpected target windowing mode " + + WindowConfiguration.windowingModeToString(targetWindowingMode)); + } + final IBinder token = mTransitions.startTransition(type, wct, this); + mPendingTransitionTokens.add(token); + } + + @Override + public void startMinimizedModeTransition(WindowContainerTransaction wct) { + final int type = WindowManager.TRANSIT_TO_BACK; + mPendingTransitionTokens.add(mTransitions.startTransition(type, wct, this)); + } + + + @Override + public void startRemoveTransition(WindowContainerTransaction wct) { + final int type = WindowManager.TRANSIT_CLOSE; + mPendingTransitionTokens.add(mTransitions.startTransition(type, wct, this)); + } + + @Override + public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction startT, + @NonNull SurfaceControl.Transaction finishT, + @NonNull Transitions.TransitionFinishCallback finishCallback) { + boolean transitionHandled = false; + for (TransitionInfo.Change change : info.getChanges()) { + if ((change.getFlags() & TransitionInfo.FLAG_IS_WALLPAPER) != 0) { + continue; + } + + final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo(); + if (taskInfo == null || taskInfo.taskId == -1) { + continue; + } + + switch (change.getMode()) { + case WindowManager.TRANSIT_CHANGE: + transitionHandled |= startChangeTransition( + transition, info.getType(), change); + break; + case WindowManager.TRANSIT_TO_BACK: + transitionHandled |= startMinimizeTransition(transition); + break; + } + } + + mPendingTransitionTokens.remove(transition); + + if (!transitionHandled) { + return false; + } + + startT.apply(); + mTransitions.getMainExecutor().execute( + () -> finishCallback.onTransitionFinished(null, null)); + return true; + } + + private boolean startChangeTransition( + IBinder transition, + int type, + TransitionInfo.Change change) { + if (!mPendingTransitionTokens.contains(transition)) { + return false; + } + + boolean handled = false; + final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo(); + if (type == Transitions.TRANSIT_MAXIMIZE + && taskInfo.getWindowingMode() == WINDOWING_MODE_FULLSCREEN) { + // TODO: Add maximize animations + handled = true; + } + + if (type == Transitions.TRANSIT_RESTORE_FROM_MAXIMIZE + && taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM) { + // TODO: Add restore animations + handled = true; + } + + return handled; + } + + private boolean startMinimizeTransition(IBinder transition) { + return mPendingTransitionTokens.contains(transition); + } + + @Nullable + @Override + public WindowContainerTransaction handleRequest(@NonNull IBinder transition, + @NonNull TransitionRequestInfo request) { + return null; + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java new file mode 100644 index 000000000000..17d60671e964 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java @@ -0,0 +1,245 @@ +/* + * 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.freeform; + +import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; +import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; + +import android.app.ActivityManager; +import android.content.Context; +import android.os.IBinder; +import android.util.Log; +import android.view.SurfaceControl; +import android.view.WindowManager; +import android.window.TransitionInfo; +import android.window.WindowContainerToken; + +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; + +import com.android.wm.shell.fullscreen.FullscreenTaskListener; +import com.android.wm.shell.sysui.ShellInit; +import com.android.wm.shell.transition.Transitions; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * The {@link Transitions.TransitionHandler} that handles freeform task launches, closes, + * maximizing and restoring transitions. It also reports transitions so that window decorations can + * be a part of transitions. + */ +public class FreeformTaskTransitionObserver implements Transitions.TransitionObserver { + private static final String TAG = "FreeformTO"; + + private final Transitions mTransitions; + private final FreeformTaskListener<?> mFreeformTaskListener; + private final FullscreenTaskListener<?> mFullscreenTaskListener; + + private final Map<IBinder, List<AutoCloseable>> mTransitionToWindowDecors = new HashMap<>(); + + public FreeformTaskTransitionObserver( + Context context, + ShellInit shellInit, + Transitions transitions, + FullscreenTaskListener<?> fullscreenTaskListener, + FreeformTaskListener<?> freeformTaskListener) { + mTransitions = transitions; + mFreeformTaskListener = freeformTaskListener; + mFullscreenTaskListener = fullscreenTaskListener; + if (Transitions.ENABLE_SHELL_TRANSITIONS && FreeformComponents.isFreeformEnabled(context)) { + shellInit.addInitCallback(this::onInit, this); + } + } + + @VisibleForTesting + void onInit() { + mTransitions.registerObserver(this); + } + + @Override + public void onTransitionReady( + @NonNull IBinder transition, + @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction startT, + @NonNull SurfaceControl.Transaction finishT) { + final ArrayList<AutoCloseable> windowDecors = new ArrayList<>(); + final ArrayList<WindowContainerToken> taskParents = new ArrayList<>(); + for (TransitionInfo.Change change : info.getChanges()) { + if ((change.getFlags() & TransitionInfo.FLAG_IS_WALLPAPER) != 0) { + continue; + } + + final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo(); + if (taskInfo == null || taskInfo.taskId == -1) { + continue; + } + // Filter out non-leaf tasks. Freeform/fullscreen don't nest tasks, but split-screen + // does, so this prevents adding duplicate captions in that scenario. + if (change.getParent() != null + && info.getChange(change.getParent()).getTaskInfo() != null) { + // This logic relies on 2 assumptions: 1 is that child tasks will be visited before + // parents (due to how z-order works). 2 is that no non-tasks are interleaved + // between tasks (hierarchically). + taskParents.add(change.getContainer()); + } + if (taskParents.contains(change.getContainer())) { + continue; + } + + switch (change.getMode()) { + case WindowManager.TRANSIT_OPEN: + case WindowManager.TRANSIT_TO_FRONT: + onOpenTransitionReady(change, startT, finishT); + break; + case WindowManager.TRANSIT_CLOSE: { + onCloseTransitionReady(change, windowDecors, startT, finishT); + break; + } + case WindowManager.TRANSIT_CHANGE: + onChangeTransitionReady(info.getType(), change, startT, finishT); + break; + } + } + if (!windowDecors.isEmpty()) { + mTransitionToWindowDecors.put(transition, windowDecors); + } + } + + private void onOpenTransitionReady( + TransitionInfo.Change change, + SurfaceControl.Transaction startT, + SurfaceControl.Transaction finishT) { + switch (change.getTaskInfo().getWindowingMode()){ + case WINDOWING_MODE_FREEFORM: + mFreeformTaskListener.createWindowDecoration(change, startT, finishT); + break; + case WINDOWING_MODE_FULLSCREEN: + mFullscreenTaskListener.createWindowDecoration(change, startT, finishT); + break; + } + } + + private void onCloseTransitionReady( + TransitionInfo.Change change, + ArrayList<AutoCloseable> windowDecors, + SurfaceControl.Transaction startT, + SurfaceControl.Transaction finishT) { + final AutoCloseable windowDecor; + switch (change.getTaskInfo().getWindowingMode()) { + case WINDOWING_MODE_FREEFORM: + windowDecor = mFreeformTaskListener.giveWindowDecoration(change.getTaskInfo(), + startT, finishT); + break; + case WINDOWING_MODE_FULLSCREEN: + windowDecor = mFullscreenTaskListener.giveWindowDecoration(change.getTaskInfo(), + startT, finishT); + break; + default: + windowDecor = null; + } + if (windowDecor != null) { + windowDecors.add(windowDecor); + } + } + + private void onChangeTransitionReady( + int type, + TransitionInfo.Change change, + SurfaceControl.Transaction startT, + SurfaceControl.Transaction finishT) { + AutoCloseable windowDecor = null; + + boolean adopted = false; + final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo(); + if (taskInfo.getWindowingMode() == WINDOWING_MODE_FULLSCREEN) { + windowDecor = mFreeformTaskListener.giveWindowDecoration( + change.getTaskInfo(), startT, finishT); + if (windowDecor != null) { + adopted = mFullscreenTaskListener.adoptWindowDecoration( + change, startT, finishT, windowDecor); + } else { + // will return false if it already has the window decor. + adopted = mFullscreenTaskListener.createWindowDecoration(change, startT, finishT); + } + } + + if (taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM) { + windowDecor = mFullscreenTaskListener.giveWindowDecoration( + change.getTaskInfo(), startT, finishT); + if (windowDecor != null) { + adopted = mFreeformTaskListener.adoptWindowDecoration( + change, startT, finishT, windowDecor); + } else { + // will return false if it already has the window decor. + adopted = mFreeformTaskListener.createWindowDecoration(change, startT, finishT); + } + } + + if (!adopted) { + releaseWindowDecor(windowDecor); + } + } + + @Override + public void onTransitionStarting(@NonNull IBinder transition) {} + + @Override + public void onTransitionMerged(@NonNull IBinder merged, @NonNull IBinder playing) { + final List<AutoCloseable> windowDecorsOfMerged = mTransitionToWindowDecors.get(merged); + if (windowDecorsOfMerged == null) { + // We are adding window decorations of the merged transition to them of the playing + // transition so if there is none of them there is nothing to do. + return; + } + mTransitionToWindowDecors.remove(merged); + + final List<AutoCloseable> windowDecorsOfPlaying = mTransitionToWindowDecors.get(playing); + if (windowDecorsOfPlaying != null) { + windowDecorsOfPlaying.addAll(windowDecorsOfMerged); + } else { + mTransitionToWindowDecors.put(playing, windowDecorsOfMerged); + } + } + + @Override + public void onTransitionFinished(@NonNull IBinder transition, boolean aborted) { + final List<AutoCloseable> windowDecors = mTransitionToWindowDecors.getOrDefault( + transition, Collections.emptyList()); + mTransitionToWindowDecors.remove(transition); + + for (AutoCloseable windowDecor : windowDecors) { + releaseWindowDecor(windowDecor); + } + mFullscreenTaskListener.onTaskTransitionFinished(); + mFreeformTaskListener.onTaskTransitionFinished(); + } + + private static void releaseWindowDecor(AutoCloseable windowDecor) { + if (windowDecor == null) { + return; + } + try { + windowDecor.close(); + } catch (Exception e) { + Log.e(TAG, "Failed to release window decoration.", e); + } + } +} 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 new file mode 100644 index 000000000000..8da4c6ab4b36 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionStarter.java @@ -0,0 +1,51 @@ +/* + * 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.freeform; + +import android.window.WindowContainerTransaction; + +/** + * The interface around {@link FreeformTaskTransitionHandler} for task listeners to start freeform + * task transitions. + */ +public interface FreeformTaskTransitionStarter { + + /** + * Starts a windowing mode transition. + * + * @param targetWindowingMode the target windowing mode + * @param wct the {@link WindowContainerTransaction} that changes the windowing mode + * + */ + void startWindowingModeTransition(int targetWindowingMode, WindowContainerTransaction wct); + + /** + * Starts window minimization transition + * + * @param wct the {@link WindowContainerTransaction} that changes the windowing mode + * + */ + void startMinimizedModeTransition(WindowContainerTransaction wct); + + /** + * Starts close window transition + * + * @param wct the {@link WindowContainerTransaction} that closes the task + * + */ + void startRemoveTransition(WindowContainerTransaction wct); +}
\ No newline at end of file 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 73e6cba43ec0..76e296bb8c61 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenTaskListener.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenTaskListener.java @@ -16,110 +16,292 @@ package com.android.wm.shell.fullscreen; -import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; - import static com.android.wm.shell.ShellTaskOrganizer.TASK_LISTENER_TYPE_FULLSCREEN; import static com.android.wm.shell.ShellTaskOrganizer.taskListenerTypeToString; +import android.app.ActivityManager; import android.app.ActivityManager.RunningTaskInfo; -import android.app.TaskInfo; import android.graphics.Point; -import android.util.Slog; +import android.util.Log; import android.util.SparseArray; -import android.util.SparseBooleanArray; import android.view.SurfaceControl; +import android.window.TransitionInfo; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import com.android.internal.protolog.common.ProtoLog; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.recents.RecentTasksController; +import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.transition.Transitions; +import com.android.wm.shell.windowdecor.WindowDecorViewModel; import java.io.PrintWriter; import java.util.Optional; /** * Organizes tasks presented in {@link android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN}. + * @param <T> the type of window decoration instance */ -public class FullscreenTaskListener implements ShellTaskOrganizer.TaskListener { +public class FullscreenTaskListener<T extends AutoCloseable> + implements ShellTaskOrganizer.TaskListener { private static final String TAG = "FullscreenTaskListener"; - private final SyncTransactionQueue mSyncQueue; - private final FullscreenUnfoldController mFullscreenUnfoldController; - private final Optional<RecentTasksController> mRecentTasksOptional; + private final ShellTaskOrganizer mShellTaskOrganizer; - private final SparseArray<TaskData> mDataByTaskId = new SparseArray<>(); - private final AnimatableTasksListener mAnimatableTasksListener = new AnimatableTasksListener(); + private final SparseArray<State<T>> mTasks = new SparseArray<>(); + private final SparseArray<T> mWindowDecorOfVanishedTasks = new SparseArray<>(); - public FullscreenTaskListener(SyncTransactionQueue syncQueue, - Optional<FullscreenUnfoldController> unfoldController) { - this(syncQueue, unfoldController, Optional.empty()); + private static class State<T extends AutoCloseable> { + RunningTaskInfo mTaskInfo; + SurfaceControl mLeash; + T mWindowDecoration; + } + private final SyncTransactionQueue mSyncQueue; + private final Optional<RecentTasksController> mRecentTasksOptional; + private final Optional<WindowDecorViewModel<T>> mWindowDecorViewModelOptional; + /** + * This constructor is used by downstream products. + */ + public FullscreenTaskListener(SyncTransactionQueue syncQueue) { + this(null /* shellInit */, null /* shellTaskOrganizer */, syncQueue, Optional.empty(), + Optional.empty()); } - public FullscreenTaskListener(SyncTransactionQueue syncQueue, - Optional<FullscreenUnfoldController> unfoldController, - Optional<RecentTasksController> recentTasks) { + public FullscreenTaskListener(ShellInit shellInit, + ShellTaskOrganizer shellTaskOrganizer, + SyncTransactionQueue syncQueue, + Optional<RecentTasksController> recentTasksOptional, + Optional<WindowDecorViewModel<T>> windowDecorViewModelOptional) { + mShellTaskOrganizer = shellTaskOrganizer; mSyncQueue = syncQueue; - mFullscreenUnfoldController = unfoldController.orElse(null); - mRecentTasksOptional = recentTasks; + mRecentTasksOptional = recentTasksOptional; + mWindowDecorViewModelOptional = windowDecorViewModelOptional; + // Note: Some derivative FullscreenTaskListener implementations do not use ShellInit + if (shellInit != null) { + shellInit.addInitCallback(this::onInit, this); + } + } + + private void onInit() { + mShellTaskOrganizer.addListenerForType(this, TASK_LISTENER_TYPE_FULLSCREEN); } @Override public void onTaskAppeared(RunningTaskInfo taskInfo, SurfaceControl leash) { - if (mDataByTaskId.get(taskInfo.taskId) != null) { + if (mTasks.get(taskInfo.taskId) != null) { throw new IllegalStateException("Task appeared more than once: #" + taskInfo.taskId); } ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TASK_ORG, "Fullscreen Task Appeared: #%d", taskInfo.taskId); final Point positionInParent = taskInfo.positionInParent; - mDataByTaskId.put(taskInfo.taskId, new TaskData(leash, positionInParent)); - if (Transitions.ENABLE_SHELL_TRANSITIONS) return; - mSyncQueue.runInSync(t -> { - // Reset several properties back to fullscreen (PiP, for example, leaves all these - // properties in a bad state). - t.setWindowCrop(leash, null); - t.setPosition(leash, positionInParent.x, positionInParent.y); - t.setAlpha(leash, 1f); - t.setMatrix(leash, 1, 0, 0, 1); - t.show(leash); - }); + final State<T> state = new State(); + state.mLeash = leash; + state.mTaskInfo = taskInfo; + mTasks.put(taskInfo.taskId, state); - mAnimatableTasksListener.onTaskAppeared(taskInfo); + if (Transitions.ENABLE_SHELL_TRANSITIONS) return; updateRecentsForVisibleFullscreenTask(taskInfo); + if (mWindowDecorViewModelOptional.isPresent()) { + SurfaceControl.Transaction t = new SurfaceControl.Transaction(); + state.mWindowDecoration = + mWindowDecorViewModelOptional.get().createWindowDecoration(taskInfo, + leash, t, t); + t.apply(); + } + if (state.mWindowDecoration == null) { + mSyncQueue.runInSync(t -> { + // Reset several properties back to fullscreen (PiP, for example, leaves all these + // properties in a bad state). + t.setWindowCrop(leash, null); + t.setPosition(leash, positionInParent.x, positionInParent.y); + t.setAlpha(leash, 1f); + t.setMatrix(leash, 1, 0, 0, 1); + t.show(leash); + }); + } } @Override public void onTaskInfoChanged(RunningTaskInfo taskInfo) { + final State<T> state = mTasks.get(taskInfo.taskId); + final Point oldPositionInParent = state.mTaskInfo.positionInParent; + state.mTaskInfo = taskInfo; + if (state.mWindowDecoration != null) { + mWindowDecorViewModelOptional.get().onTaskInfoChanged( + state.mTaskInfo, state.mWindowDecoration); + } if (Transitions.ENABLE_SHELL_TRANSITIONS) return; - - mAnimatableTasksListener.onTaskInfoChanged(taskInfo); updateRecentsForVisibleFullscreenTask(taskInfo); - final TaskData data = mDataByTaskId.get(taskInfo.taskId); - final Point positionInParent = taskInfo.positionInParent; - if (!positionInParent.equals(data.positionInParent)) { - data.positionInParent.set(positionInParent.x, positionInParent.y); + final Point positionInParent = state.mTaskInfo.positionInParent; + if (!oldPositionInParent.equals(state.mTaskInfo.positionInParent)) { mSyncQueue.runInSync(t -> { - t.setPosition(data.surface, positionInParent.x, positionInParent.y); + t.setPosition(state.mLeash, positionInParent.x, positionInParent.y); }); } } @Override - public void onTaskVanished(RunningTaskInfo taskInfo) { - if (mDataByTaskId.get(taskInfo.taskId) == null) { - Slog.e(TAG, "Task already vanished: #" + taskInfo.taskId); + public void onTaskVanished(ActivityManager.RunningTaskInfo taskInfo) { + final State<T> state = mTasks.get(taskInfo.taskId); + if (state == null) { + // This is possible if the transition happens before this method. return; } - - mAnimatableTasksListener.onTaskVanished(taskInfo); - mDataByTaskId.remove(taskInfo.taskId); - ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TASK_ORG, "Fullscreen Task Vanished: #%d", taskInfo.taskId); + mTasks.remove(taskInfo.taskId); + + if (Transitions.ENABLE_SHELL_TRANSITIONS) { + // Save window decorations of closing tasks so that we can hand them over to the + // transition system if this method happens before the transition. In case where the + // transition didn't happen, it'd be cleared when the next transition finished. + if (state.mWindowDecoration != null) { + mWindowDecorOfVanishedTasks.put(taskInfo.taskId, state.mWindowDecoration); + } + return; + } + releaseWindowDecor(state.mWindowDecoration); + } + + /** + * Creates a window decoration for a transition. + * + * @param change the change of this task transition that needs to have the task layer as the + * leash + * @return {@code true} if a decoration was actually created. + */ + public boolean createWindowDecoration(TransitionInfo.Change change, + SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT) { + final State<T> state = createOrUpdateTaskState(change.getTaskInfo(), change.getLeash()); + if (!mWindowDecorViewModelOptional.isPresent()) return false; + if (state.mWindowDecoration != null) { + // Already has a decoration. + return false; + } + T newWindowDecor = mWindowDecorViewModelOptional.get().createWindowDecoration( + state.mTaskInfo, state.mLeash, startT, finishT); + if (newWindowDecor != null) { + state.mWindowDecoration = newWindowDecor; + return true; + } + return false; + } + + /** + * Adopt the incoming window decoration and lets the window decoration prepare for a transition. + * + * @param change the change of this task transition that needs to have the task layer as the + * leash + * @param startT the start transaction of this transition + * @param finishT the finish transaction of this transition + * @param windowDecor the window decoration to adopt + * @return {@code true} if it adopts the window decoration; {@code false} otherwise + */ + public boolean adoptWindowDecoration( + TransitionInfo.Change change, + SurfaceControl.Transaction startT, + SurfaceControl.Transaction finishT, + @Nullable AutoCloseable windowDecor) { + if (!mWindowDecorViewModelOptional.isPresent()) { + return false; + } + final State<T> state = createOrUpdateTaskState(change.getTaskInfo(), change.getLeash()); + state.mWindowDecoration = mWindowDecorViewModelOptional.get().adoptWindowDecoration( + windowDecor); + if (state.mWindowDecoration != null) { + mWindowDecorViewModelOptional.get().setupWindowDecorationForTransition( + state.mTaskInfo, startT, finishT, state.mWindowDecoration); + return true; + } else { + T newWindowDecor = mWindowDecorViewModelOptional.get().createWindowDecoration( + state.mTaskInfo, state.mLeash, startT, finishT); + if (newWindowDecor != null) { + state.mWindowDecoration = newWindowDecor; + } + return false; + } + } + + /** + * Clear window decors of vanished tasks. + */ + public void onTaskTransitionFinished() { + if (mWindowDecorOfVanishedTasks.size() == 0) { + return; + } + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, + "Clearing window decors of vanished tasks. There could be visual defects " + + "if any of them is used later in transitions."); + for (int i = 0; i < mWindowDecorOfVanishedTasks.size(); ++i) { + releaseWindowDecor(mWindowDecorOfVanishedTasks.valueAt(i)); + } + mWindowDecorOfVanishedTasks.clear(); + } + + /** + * Gives out the ownership of the task's window decoration. The given task is leaving (of has + * left) this task listener. This is the transition system asking for the ownership. + * + * @param taskInfo the maximizing task + * @return the window decor of the maximizing task if any + */ + public T giveWindowDecoration( + ActivityManager.RunningTaskInfo taskInfo, + SurfaceControl.Transaction startT, + SurfaceControl.Transaction finishT) { + T windowDecor; + final State<T> state = mTasks.get(taskInfo.taskId); + if (state != null) { + windowDecor = state.mWindowDecoration; + state.mWindowDecoration = null; + } else { + windowDecor = + mWindowDecorOfVanishedTasks.removeReturnOld(taskInfo.taskId); + } + if (mWindowDecorViewModelOptional.isPresent() && windowDecor != null) { + mWindowDecorViewModelOptional.get().setupWindowDecorationForTransition( + taskInfo, startT, finishT, windowDecor); + } + + return windowDecor; + } + + private State<T> createOrUpdateTaskState(ActivityManager.RunningTaskInfo taskInfo, + SurfaceControl leash) { + State<T> state = mTasks.get(taskInfo.taskId); + if (state != null) { + updateTaskInfo(taskInfo); + return state; + } + + state = new State<T>(); + state.mTaskInfo = taskInfo; + state.mLeash = leash; + mTasks.put(taskInfo.taskId, state); + + return state; + } + + private State<T> updateTaskInfo(ActivityManager.RunningTaskInfo taskInfo) { + final State<T> state = mTasks.get(taskInfo.taskId); + state.mTaskInfo = taskInfo; + return state; + } + + private void releaseWindowDecor(T windowDecor) { + if (windowDecor == null) { + return; + } + try { + windowDecor.close(); + } catch (Exception e) { + Log.e(TAG, "Failed to release window decoration.", e); + } } private void updateRecentsForVisibleFullscreenTask(RunningTaskInfo taskInfo) { @@ -143,17 +325,17 @@ public class FullscreenTaskListener implements ShellTaskOrganizer.TaskListener { } private SurfaceControl findTaskSurface(int taskId) { - if (!mDataByTaskId.contains(taskId)) { + if (!mTasks.contains(taskId)) { throw new IllegalArgumentException("There is no surface for taskId=" + taskId); } - return mDataByTaskId.get(taskId).surface; + return mTasks.get(taskId).mLeash; } @Override public void dump(@NonNull PrintWriter pw, String prefix) { final String innerPrefix = prefix + " "; pw.println(prefix + this); - pw.println(innerPrefix + mDataByTaskId.size() + " Tasks"); + pw.println(innerPrefix + mTasks.size() + " Tasks"); } @Override @@ -161,77 +343,5 @@ public class FullscreenTaskListener implements ShellTaskOrganizer.TaskListener { return TAG + ":" + taskListenerTypeToString(TASK_LISTENER_TYPE_FULLSCREEN); } - /** - * Per-task data for each managed task. - */ - private static class TaskData { - public final SurfaceControl surface; - public final Point positionInParent; - - public TaskData(SurfaceControl surface, Point positionInParent) { - this.surface = surface; - this.positionInParent = positionInParent; - } - } - - class AnimatableTasksListener { - private final SparseBooleanArray mTaskIds = new SparseBooleanArray(); - - public void onTaskAppeared(RunningTaskInfo taskInfo) { - final boolean isApplicable = isAnimatable(taskInfo); - if (isApplicable) { - mTaskIds.put(taskInfo.taskId, true); - - if (mFullscreenUnfoldController != null) { - SurfaceControl leash = mDataByTaskId.get(taskInfo.taskId).surface; - mFullscreenUnfoldController.onTaskAppeared(taskInfo, leash); - } - } - } - - public void onTaskInfoChanged(RunningTaskInfo taskInfo) { - final boolean isCurrentlyApplicable = mTaskIds.get(taskInfo.taskId); - final boolean isApplicable = isAnimatable(taskInfo); - - if (isCurrentlyApplicable) { - if (isApplicable) { - // Still applicable, send update - if (mFullscreenUnfoldController != null) { - mFullscreenUnfoldController.onTaskInfoChanged(taskInfo); - } - } else { - // Became inapplicable - if (mFullscreenUnfoldController != null) { - mFullscreenUnfoldController.onTaskVanished(taskInfo); - } - mTaskIds.put(taskInfo.taskId, false); - } - } else { - if (isApplicable) { - // Became applicable - mTaskIds.put(taskInfo.taskId, true); - - if (mFullscreenUnfoldController != null) { - SurfaceControl leash = mDataByTaskId.get(taskInfo.taskId).surface; - mFullscreenUnfoldController.onTaskAppeared(taskInfo, leash); - } - } - } - } - public void onTaskVanished(RunningTaskInfo taskInfo) { - final boolean isCurrentlyApplicable = mTaskIds.get(taskInfo.taskId); - if (isCurrentlyApplicable && mFullscreenUnfoldController != null) { - mFullscreenUnfoldController.onTaskVanished(taskInfo); - } - mTaskIds.put(taskInfo.taskId, false); - } - - private boolean isAnimatable(TaskInfo taskInfo) { - // Filter all visible tasks that are not launcher tasks - // We do not animate launcher as it handles the animation by itself - return taskInfo != null && taskInfo.isVisible && taskInfo.getConfiguration() - .windowConfiguration.getActivityType() != ACTIVITY_TYPE_HOME; - } - } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutoutController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutoutController.java index 23f76ca5f6ae..32125fa44148 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutoutController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutoutController.java @@ -19,7 +19,6 @@ package com.android.wm.shell.hidedisplaycutout; import android.content.Context; import android.content.res.Configuration; import android.os.SystemProperties; -import android.util.Slog; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -27,20 +26,23 @@ import androidx.annotation.VisibleForTesting; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.ShellExecutor; +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 java.io.PrintWriter; -import java.util.concurrent.TimeUnit; /** * Manages the hide display cutout status. */ -public class HideDisplayCutoutController { +public class HideDisplayCutoutController implements ConfigurationChangeListener { private static final String TAG = "HideDisplayCutoutController"; private final Context mContext; + private final ShellCommandHandler mShellCommandHandler; + private final ShellController mShellController; private final HideDisplayCutoutOrganizer mOrganizer; - private final ShellExecutor mMainExecutor; - private final HideDisplayCutoutImpl mImpl = new HideDisplayCutoutImpl(); @VisibleForTesting boolean mEnabled; @@ -49,8 +51,12 @@ public class HideDisplayCutoutController { * supported. */ @Nullable - public static HideDisplayCutoutController create( - Context context, DisplayController displayController, ShellExecutor mainExecutor) { + public static HideDisplayCutoutController create(Context context, + ShellInit shellInit, + ShellCommandHandler shellCommandHandler, + ShellController shellController, + DisplayController displayController, + ShellExecutor mainExecutor) { // The SystemProperty is set for devices that support this feature and is used to control // whether to create the HideDisplayCutout instance. // It's defined in the device.mk (e.g. device/google/crosshatch/device.mk). @@ -60,19 +66,26 @@ public class HideDisplayCutoutController { HideDisplayCutoutOrganizer organizer = new HideDisplayCutoutOrganizer(context, displayController, mainExecutor); - return new HideDisplayCutoutController(context, organizer, mainExecutor); + return new HideDisplayCutoutController(context, shellInit, shellCommandHandler, + shellController, organizer); } - HideDisplayCutoutController(Context context, HideDisplayCutoutOrganizer organizer, - ShellExecutor mainExecutor) { + HideDisplayCutoutController(Context context, + ShellInit shellInit, + ShellCommandHandler shellCommandHandler, + ShellController shellController, + HideDisplayCutoutOrganizer organizer) { mContext = context; + mShellCommandHandler = shellCommandHandler; + mShellController = shellController; mOrganizer = organizer; - mMainExecutor = mainExecutor; - updateStatus(); + shellInit.addInitCallback(this::onInit, this); } - public HideDisplayCutout asHideDisplayCutout() { - return mImpl; + private void onInit() { + mShellCommandHandler.addDumpCallback(this::dump, this); + updateStatus(); + mShellController.addConfigurationChangeListener(this); } @VisibleForTesting @@ -94,26 +107,18 @@ public class HideDisplayCutoutController { } } - private void onConfigurationChanged(Configuration newConfig) { + @Override + public void onConfigurationChanged(Configuration newConfig) { updateStatus(); } - public void dump(@NonNull PrintWriter pw) { - final String prefix = " "; + public void dump(@NonNull PrintWriter pw, String prefix) { + final String innerPrefix = " "; pw.print(TAG); pw.println(" states: "); - pw.print(prefix); + pw.print(innerPrefix); pw.print("mEnabled="); pw.println(mEnabled); mOrganizer.dump(pw); } - - private class HideDisplayCutoutImpl implements HideDisplayCutout { - @Override - public void onConfigurationChanged(Configuration newConfig) { - mMainExecutor.execute(() -> { - HideDisplayCutoutController.this.onConfigurationChanged(newConfig); - }); - } - } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutoutOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutoutOrganizer.java index 3f7d78dda037..f376e1fd6174 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutoutOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutoutOrganizer.java @@ -64,8 +64,8 @@ class HideDisplayCutoutOrganizer extends DisplayAreaOrganizer { @VisibleForTesting final Rect mCurrentDisplayBounds = new Rect(); // The default display cutout in natural orientation. - private Insets mDefaultCutoutInsets; - private Insets mCurrentCutoutInsets; + private Insets mDefaultCutoutInsets = Insets.NONE; + private Insets mCurrentCutoutInsets = Insets.NONE; private boolean mIsDefaultPortrait; private int mStatusBarHeight; @VisibleForTesting @@ -78,27 +78,35 @@ class HideDisplayCutoutOrganizer extends DisplayAreaOrganizer { private final DisplayController.OnDisplaysChangedListener mListener = new DisplayController.OnDisplaysChangedListener() { @Override + public void onDisplayAdded(int displayId) { + onDisplayChanged(displayId); + } + + @Override public void onDisplayConfigurationChanged(int displayId, Configuration newConfig) { - if (displayId != DEFAULT_DISPLAY) { - return; - } - DisplayLayout displayLayout = - mDisplayController.getDisplayLayout(DEFAULT_DISPLAY); - if (displayLayout == null) { - return; - } - final boolean rotationChanged = mRotation != displayLayout.rotation(); - mRotation = displayLayout.rotation(); - if (rotationChanged || isDisplayBoundsChanged()) { - updateBoundsAndOffsets(true /* enabled */); - final WindowContainerTransaction wct = new WindowContainerTransaction(); - final SurfaceControl.Transaction t = new SurfaceControl.Transaction(); - applyAllBoundsAndOffsets(wct, t); - applyTransaction(wct, t); - } + onDisplayChanged(displayId); } }; + private void onDisplayChanged(int displayId) { + if (displayId != DEFAULT_DISPLAY) { + return; + } + final DisplayLayout displayLayout = mDisplayController.getDisplayLayout(DEFAULT_DISPLAY); + if (displayLayout == null) { + return; + } + final boolean rotationChanged = mRotation != displayLayout.rotation(); + mRotation = displayLayout.rotation(); + if (rotationChanged || isDisplayBoundsChanged()) { + updateBoundsAndOffsets(true /* enabled */); + final WindowContainerTransaction wct = new WindowContainerTransaction(); + final SurfaceControl.Transaction t = new SurfaceControl.Transaction(); + applyAllBoundsAndOffsets(wct, t); + applyTransaction(wct, t); + } + } + HideDisplayCutoutOrganizer(Context context, DisplayController displayController, ShellExecutor mainExecutor) { super(mainExecutor); @@ -128,9 +136,10 @@ class HideDisplayCutoutOrganizer extends DisplayAreaOrganizer { final WindowContainerTransaction wct = new WindowContainerTransaction(); final SurfaceControl.Transaction t = new SurfaceControl.Transaction(); - applyBoundsAndOffsets( - displayAreaInfo.token, mDisplayAreaMap.get(displayAreaInfo.token), wct, t); + final SurfaceControl leash = mDisplayAreaMap.get(displayAreaInfo.token); + applyBoundsAndOffsets(displayAreaInfo.token, leash, wct, t); applyTransaction(wct, t); + leash.release(); mDisplayAreaMap.remove(displayAreaInfo.token); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/kidsmode/KidsModeTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/kidsmode/KidsModeTaskOrganizer.java index b4c87b6cbf95..e91987dab972 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/kidsmode/KidsModeTaskOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/kidsmode/KidsModeTaskOrganizer.java @@ -50,7 +50,9 @@ 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.recents.RecentTasksController; -import com.android.wm.shell.startingsurface.StartingWindowController; +import com.android.wm.shell.sysui.ShellCommandHandler; +import com.android.wm.shell.sysui.ShellInit; +import com.android.wm.shell.unfold.UnfoldAnimationController; import java.io.PrintWriter; import java.util.List; @@ -72,6 +74,7 @@ public class KidsModeTaskOrganizer extends ShellTaskOrganizer { private final Handler mMainHandler; private final Context mContext; + private final ShellCommandHandler mShellCommandHandler; private final SyncTransactionQueue mSyncQueue; private final DisplayController mDisplayController; private final DisplayInsetsController mDisplayInsetsController; @@ -139,45 +142,61 @@ public class KidsModeTaskOrganizer extends ShellTaskOrganizer { @VisibleForTesting KidsModeTaskOrganizer( - ITaskOrganizerController taskOrganizerController, - ShellExecutor mainExecutor, - Handler mainHandler, Context context, + ShellInit shellInit, + ShellCommandHandler shellCommandHandler, + ITaskOrganizerController taskOrganizerController, SyncTransactionQueue syncTransactionQueue, DisplayController displayController, DisplayInsetsController displayInsetsController, + Optional<UnfoldAnimationController> unfoldAnimationController, Optional<RecentTasksController> recentTasks, - KidsModeSettingsObserver kidsModeSettingsObserver) { - super(taskOrganizerController, mainExecutor, context, /* compatUI= */ null, recentTasks); + KidsModeSettingsObserver kidsModeSettingsObserver, + ShellExecutor mainExecutor, + Handler mainHandler) { + // Note: we don't call super with the shell init because we will be initializing manually + super(/* shellInit= */ null, /* shellCommandHandler= */ null, taskOrganizerController, + /* compatUI= */ null, unfoldAnimationController, recentTasks, mainExecutor); mContext = context; + mShellCommandHandler = shellCommandHandler; mMainHandler = mainHandler; mSyncQueue = syncTransactionQueue; mDisplayController = displayController; mDisplayInsetsController = displayInsetsController; mKidsModeSettingsObserver = kidsModeSettingsObserver; + shellInit.addInitCallback(this::onInit, this); } public KidsModeTaskOrganizer( - ShellExecutor mainExecutor, - Handler mainHandler, Context context, + ShellInit shellInit, + ShellCommandHandler shellCommandHandler, SyncTransactionQueue syncTransactionQueue, DisplayController displayController, DisplayInsetsController displayInsetsController, - Optional<RecentTasksController> recentTasks) { - super(mainExecutor, context, /* compatUI= */ null, recentTasks); + Optional<UnfoldAnimationController> unfoldAnimationController, + Optional<RecentTasksController> recentTasks, + ShellExecutor mainExecutor, + Handler mainHandler) { + // Note: we don't call super with the shell init because we will be initializing manually + super(/* shellInit= */ null, /* taskOrganizerController= */ null, /* compatUI= */ null, + unfoldAnimationController, recentTasks, mainExecutor); mContext = context; + mShellCommandHandler = shellCommandHandler; mMainHandler = mainHandler; mSyncQueue = syncTransactionQueue; mDisplayController = displayController; mDisplayInsetsController = displayInsetsController; + shellInit.addInitCallback(this::onInit, this); } /** * Initializes kids mode status. */ - public void initialize(StartingWindowController startingWindowController) { - initStartingWindow(startingWindowController); + public void onInit() { + if (mShellCommandHandler != null) { + mShellCommandHandler.addDumpCallback(this::dump, this); + } if (mKidsModeSettingsObserver == null) { mKidsModeSettingsObserver = new KidsModeSettingsObserver(mMainHandler, mContext); } @@ -297,11 +316,13 @@ public class KidsModeTaskOrganizer extends ShellTaskOrganizer { true /* onTop */); wct.reorder(rootToken, mEnabled /* onTop */); mSyncQueue.queue(wct); - final SurfaceControl rootLeash = mLaunchRootLeash; - mSyncQueue.runInSync(t -> { - t.setPosition(rootLeash, taskBounds.left, taskBounds.top); - t.setWindowCrop(rootLeash, taskBounds.width(), taskBounds.height()); - }); + if (mEnabled) { + final SurfaceControl rootLeash = mLaunchRootLeash; + mSyncQueue.runInSync(t -> { + t.setPosition(rootLeash, taskBounds.left, taskBounds.top); + t.setWindowCrop(rootLeash, taskBounds.width(), taskBounds.height()); + }); + } } private Rect calculateBounds() { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/DividerImeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/DividerImeController.java deleted file mode 100644 index aced072c8c71..000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/DividerImeController.java +++ /dev/null @@ -1,418 +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.legacysplitscreen; - -import static android.content.res.Configuration.SCREEN_HEIGHT_DP_UNDEFINED; -import static android.content.res.Configuration.SCREEN_WIDTH_DP_UNDEFINED; - -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.animation.ValueAnimator; -import android.annotation.Nullable; -import android.graphics.Rect; -import android.util.Slog; -import android.view.Choreographer; -import android.view.SurfaceControl; -import android.window.TaskOrganizer; -import android.window.WindowContainerToken; -import android.window.WindowContainerTransaction; - -import com.android.wm.shell.common.DisplayImeController; -import com.android.wm.shell.common.ShellExecutor; -import com.android.wm.shell.common.TransactionPool; - -class DividerImeController implements DisplayImeController.ImePositionProcessor { - private static final String TAG = "DividerImeController"; - private static final boolean DEBUG = LegacySplitScreenController.DEBUG; - - private static final float ADJUSTED_NONFOCUS_DIM = 0.3f; - - private final LegacySplitScreenTaskListener mSplits; - private final TransactionPool mTransactionPool; - private final ShellExecutor mMainExecutor; - private final TaskOrganizer mTaskOrganizer; - - /** - * These are the y positions of the top of the IME surface when it is hidden and when it is - * shown respectively. These are NOT necessarily the top of the visible IME itself. - */ - private int mHiddenTop = 0; - private int mShownTop = 0; - - // The following are target states (what we are curretly animating towards). - /** - * {@code true} if, at the end of the animation, the split task positions should be - * adjusted by height of the IME. This happens when the secondary split is the IME target. - */ - private boolean mTargetAdjusted = false; - /** - * {@code true} if, at the end of the animation, the IME should be shown/visible - * regardless of what has focus. - */ - private boolean mTargetShown = false; - private float mTargetPrimaryDim = 0.f; - private float mTargetSecondaryDim = 0.f; - - // The following are the current (most recent) states set during animation - /** {@code true} if the secondary split has IME focus. */ - private boolean mSecondaryHasFocus = false; - /** The dimming currently applied to the primary/secondary splits. */ - private float mLastPrimaryDim = 0.f; - private float mLastSecondaryDim = 0.f; - /** The most recent y position of the top of the IME surface */ - private int mLastAdjustTop = -1; - - // The following are states reached last time an animation fully completed. - /** {@code true} if the IME was shown/visible by the last-completed animation. */ - private boolean mImeWasShown = false; - /** {@code true} if the split positions were adjusted by the last-completed animation. */ - private boolean mAdjusted = false; - - /** - * When some aspect of split-screen needs to animate independent from the IME, - * this will be non-null and control split animation. - */ - @Nullable - private ValueAnimator mAnimation = null; - - private boolean mPaused = true; - private boolean mPausedTargetAdjusted = false; - - DividerImeController(LegacySplitScreenTaskListener splits, TransactionPool pool, - ShellExecutor mainExecutor, TaskOrganizer taskOrganizer) { - mSplits = splits; - mTransactionPool = pool; - mMainExecutor = mainExecutor; - mTaskOrganizer = taskOrganizer; - } - - private DividerView getView() { - return mSplits.mSplitScreenController.getDividerView(); - } - - private LegacySplitDisplayLayout getLayout() { - return mSplits.mSplitScreenController.getSplitLayout(); - } - - private boolean isDividerHidden() { - final DividerView view = mSplits.mSplitScreenController.getDividerView(); - return view == null || view.isHidden(); - } - - private boolean getSecondaryHasFocus(int displayId) { - WindowContainerToken imeSplit = mTaskOrganizer.getImeTarget(displayId); - return imeSplit != null - && (imeSplit.asBinder() == mSplits.mSecondary.token.asBinder()); - } - - void reset() { - mPaused = true; - mPausedTargetAdjusted = false; - mAnimation = null; - mAdjusted = mTargetAdjusted = false; - mImeWasShown = mTargetShown = false; - mTargetPrimaryDim = mTargetSecondaryDim = mLastPrimaryDim = mLastSecondaryDim = 0.f; - mSecondaryHasFocus = false; - mLastAdjustTop = -1; - } - - private void updateDimTargets() { - final boolean splitIsVisible = !getView().isHidden(); - mTargetPrimaryDim = (mSecondaryHasFocus && mTargetShown && splitIsVisible) - ? ADJUSTED_NONFOCUS_DIM : 0.f; - mTargetSecondaryDim = (!mSecondaryHasFocus && mTargetShown && splitIsVisible) - ? ADJUSTED_NONFOCUS_DIM : 0.f; - } - - - @Override - public void onImeControlTargetChanged(int displayId, boolean controlling) { - // Restore the split layout when wm-shell is not controlling IME insets anymore. - if (!controlling && mTargetShown) { - mPaused = false; - mTargetAdjusted = mTargetShown = false; - mTargetPrimaryDim = mTargetSecondaryDim = 0.f; - updateImeAdjustState(true /* force */); - startAsyncAnimation(); - } - } - - @Override - @ImeAnimationFlags - public int onImeStartPositioning(int displayId, int hiddenTop, int shownTop, - boolean imeShouldShow, boolean imeIsFloating, SurfaceControl.Transaction t) { - if (isDividerHidden()) { - return 0; - } - mHiddenTop = hiddenTop; - mShownTop = shownTop; - mTargetShown = imeShouldShow; - mSecondaryHasFocus = getSecondaryHasFocus(displayId); - final boolean targetAdjusted = imeShouldShow && mSecondaryHasFocus - && !imeIsFloating && !getLayout().mDisplayLayout.isLandscape() - && !mSplits.mSplitScreenController.isMinimized(); - if (mLastAdjustTop < 0) { - mLastAdjustTop = imeShouldShow ? hiddenTop : shownTop; - } else if (mLastAdjustTop != (imeShouldShow ? mShownTop : mHiddenTop)) { - if (mTargetAdjusted != targetAdjusted && targetAdjusted == mAdjusted) { - // Check for an "interruption" of an existing animation. In this case, we - // need to fake-flip the last-known state direction so that the animation - // completes in the other direction. - mAdjusted = mTargetAdjusted; - } else if (targetAdjusted && mTargetAdjusted && mAdjusted) { - // Already fully adjusted for IME, but IME height has changed; so, force-start - // an async animation to the new IME height. - mAdjusted = false; - } - } - if (mPaused) { - mPausedTargetAdjusted = targetAdjusted; - if (DEBUG) Slog.d(TAG, " ime starting but paused " + dumpState()); - return (targetAdjusted || mAdjusted) ? IME_ANIMATION_NO_ALPHA : 0; - } - mTargetAdjusted = targetAdjusted; - updateDimTargets(); - if (DEBUG) Slog.d(TAG, " ime starting. " + dumpState()); - if (mAnimation != null || (mImeWasShown && imeShouldShow - && mTargetAdjusted != mAdjusted)) { - // We need to animate adjustment independently of the IME position, so - // start our own animation to drive adjustment. This happens when a - // different split's editor has gained focus while the IME is still visible. - startAsyncAnimation(); - } - updateImeAdjustState(); - - return (mTargetAdjusted || mAdjusted) ? IME_ANIMATION_NO_ALPHA : 0; - } - - private void updateImeAdjustState() { - updateImeAdjustState(false /* force */); - } - - private void updateImeAdjustState(boolean force) { - if (mAdjusted != mTargetAdjusted || force) { - // Reposition the server's secondary split position so that it evaluates - // insets properly. - WindowContainerTransaction wct = new WindowContainerTransaction(); - final LegacySplitDisplayLayout splitLayout = getLayout(); - if (mTargetAdjusted) { - splitLayout.updateAdjustedBounds(mShownTop, mHiddenTop, mShownTop); - wct.setBounds(mSplits.mSecondary.token, splitLayout.mAdjustedSecondary); - // "Freeze" the configuration size so that the app doesn't get a config - // or relaunch. This is required because normally nav-bar contributes - // to configuration bounds (via nondecorframe). - Rect adjustAppBounds = new Rect(mSplits.mSecondary.configuration - .windowConfiguration.getAppBounds()); - adjustAppBounds.offset(0, splitLayout.mAdjustedSecondary.top - - splitLayout.mSecondary.top); - wct.setAppBounds(mSplits.mSecondary.token, adjustAppBounds); - wct.setScreenSizeDp(mSplits.mSecondary.token, - mSplits.mSecondary.configuration.screenWidthDp, - mSplits.mSecondary.configuration.screenHeightDp); - - wct.setBounds(mSplits.mPrimary.token, splitLayout.mAdjustedPrimary); - adjustAppBounds = new Rect(mSplits.mPrimary.configuration - .windowConfiguration.getAppBounds()); - adjustAppBounds.offset(0, splitLayout.mAdjustedPrimary.top - - splitLayout.mPrimary.top); - wct.setAppBounds(mSplits.mPrimary.token, adjustAppBounds); - wct.setScreenSizeDp(mSplits.mPrimary.token, - mSplits.mPrimary.configuration.screenWidthDp, - mSplits.mPrimary.configuration.screenHeightDp); - } else { - wct.setBounds(mSplits.mSecondary.token, splitLayout.mSecondary); - wct.setAppBounds(mSplits.mSecondary.token, null); - wct.setScreenSizeDp(mSplits.mSecondary.token, - SCREEN_WIDTH_DP_UNDEFINED, SCREEN_HEIGHT_DP_UNDEFINED); - wct.setBounds(mSplits.mPrimary.token, splitLayout.mPrimary); - wct.setAppBounds(mSplits.mPrimary.token, null); - wct.setScreenSizeDp(mSplits.mPrimary.token, - SCREEN_WIDTH_DP_UNDEFINED, SCREEN_HEIGHT_DP_UNDEFINED); - } - - if (!mSplits.mSplitScreenController.getWmProxy().queueSyncTransactionIfWaiting(wct)) { - mTaskOrganizer.applyTransaction(wct); - } - } - - // Update all the adjusted-for-ime states - if (!mPaused) { - final DividerView view = getView(); - if (view != null) { - view.setAdjustedForIme(mTargetShown, mTargetShown - ? DisplayImeController.ANIMATION_DURATION_SHOW_MS - : DisplayImeController.ANIMATION_DURATION_HIDE_MS); - } - } - mSplits.mSplitScreenController.setAdjustedForIme(mTargetShown && !mPaused); - } - - @Override - public void onImePositionChanged(int displayId, int imeTop, - SurfaceControl.Transaction t) { - if (mAnimation != null || isDividerHidden() || mPaused) { - // Not synchronized with IME anymore, so return. - return; - } - final float fraction = ((float) imeTop - mHiddenTop) / (mShownTop - mHiddenTop); - final float progress = mTargetShown ? fraction : 1.f - fraction; - onProgress(progress, t); - } - - @Override - public void onImeEndPositioning(int displayId, boolean cancelled, - SurfaceControl.Transaction t) { - if (mAnimation != null || isDividerHidden() || mPaused) { - // Not synchronized with IME anymore, so return. - return; - } - onEnd(cancelled, t); - } - - private void onProgress(float progress, SurfaceControl.Transaction t) { - final DividerView view = getView(); - if (mTargetAdjusted != mAdjusted && !mPaused) { - final LegacySplitDisplayLayout splitLayout = getLayout(); - final float fraction = mTargetAdjusted ? progress : 1.f - progress; - mLastAdjustTop = (int) (fraction * mShownTop + (1.f - fraction) * mHiddenTop); - splitLayout.updateAdjustedBounds(mLastAdjustTop, mHiddenTop, mShownTop); - view.resizeSplitSurfaces(t, splitLayout.mAdjustedPrimary, - splitLayout.mAdjustedSecondary); - } - final float invProg = 1.f - progress; - view.setResizeDimLayer(t, true /* primary */, - mLastPrimaryDim * invProg + progress * mTargetPrimaryDim); - view.setResizeDimLayer(t, false /* primary */, - mLastSecondaryDim * invProg + progress * mTargetSecondaryDim); - } - - void setDimsHidden(SurfaceControl.Transaction t, boolean hidden) { - final DividerView view = getView(); - if (hidden) { - view.setResizeDimLayer(t, true /* primary */, 0.f /* alpha */); - view.setResizeDimLayer(t, false /* primary */, 0.f /* alpha */); - } else { - updateDimTargets(); - view.setResizeDimLayer(t, true /* primary */, mTargetPrimaryDim); - view.setResizeDimLayer(t, false /* primary */, mTargetSecondaryDim); - } - } - - private void onEnd(boolean cancelled, SurfaceControl.Transaction t) { - if (!cancelled) { - onProgress(1.f, t); - mAdjusted = mTargetAdjusted; - mImeWasShown = mTargetShown; - mLastAdjustTop = mAdjusted ? mShownTop : mHiddenTop; - mLastPrimaryDim = mTargetPrimaryDim; - mLastSecondaryDim = mTargetSecondaryDim; - } - } - - private void startAsyncAnimation() { - if (mAnimation != null) { - mAnimation.cancel(); - } - mAnimation = ValueAnimator.ofFloat(0.f, 1.f); - mAnimation.setDuration(DisplayImeController.ANIMATION_DURATION_SHOW_MS); - if (mTargetAdjusted != mAdjusted) { - final float fraction = - ((float) mLastAdjustTop - mHiddenTop) / (mShownTop - mHiddenTop); - final float progress = mTargetAdjusted ? fraction : 1.f - fraction; - mAnimation.setCurrentFraction(progress); - } - - mAnimation.addUpdateListener(animation -> { - SurfaceControl.Transaction t = mTransactionPool.acquire(); - float value = (float) animation.getAnimatedValue(); - onProgress(value, t); - t.setFrameTimelineVsync(Choreographer.getSfInstance().getVsyncId()); - t.apply(); - mTransactionPool.release(t); - }); - mAnimation.setInterpolator(DisplayImeController.INTERPOLATOR); - mAnimation.addListener(new AnimatorListenerAdapter() { - private boolean mCancel = false; - - @Override - public void onAnimationCancel(Animator animation) { - mCancel = true; - } - - @Override - public void onAnimationEnd(Animator animation) { - SurfaceControl.Transaction t = mTransactionPool.acquire(); - onEnd(mCancel, t); - t.apply(); - mTransactionPool.release(t); - mAnimation = null; - } - }); - mAnimation.start(); - } - - private String dumpState() { - return "top:" + mHiddenTop + "->" + mShownTop - + " adj:" + mAdjusted + "->" + mTargetAdjusted + "(" + mLastAdjustTop + ")" - + " shw:" + mImeWasShown + "->" + mTargetShown - + " dims:" + mLastPrimaryDim + "," + mLastSecondaryDim - + "->" + mTargetPrimaryDim + "," + mTargetSecondaryDim - + " shf:" + mSecondaryHasFocus + " desync:" + (mAnimation != null) - + " paus:" + mPaused + "[" + mPausedTargetAdjusted + "]"; - } - - /** Completely aborts/resets adjustment state */ - public void pause(int displayId) { - if (DEBUG) Slog.d(TAG, "ime pause posting " + dumpState()); - mMainExecutor.execute(() -> { - if (DEBUG) Slog.d(TAG, "ime pause run posted " + dumpState()); - if (mPaused) { - return; - } - mPaused = true; - mPausedTargetAdjusted = mTargetAdjusted; - mTargetAdjusted = false; - mTargetPrimaryDim = mTargetSecondaryDim = 0.f; - updateImeAdjustState(); - startAsyncAnimation(); - if (mAnimation != null) { - mAnimation.end(); - } - }); - } - - public void resume(int displayId) { - if (DEBUG) Slog.d(TAG, "ime resume posting " + dumpState()); - mMainExecutor.execute(() -> { - if (DEBUG) Slog.d(TAG, "ime resume run posted " + dumpState()); - if (!mPaused) { - return; - } - mPaused = false; - mTargetAdjusted = mPausedTargetAdjusted; - updateDimTargets(); - final DividerView view = getView(); - if ((mTargetAdjusted != mAdjusted) && !mSplits.mSplitScreenController.isMinimized() - && view != null) { - // End unminimize animations since they conflict with adjustment animations. - view.finishAnimations(); - } - updateImeAdjustState(); - startAsyncAnimation(); - }); - } -} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/DividerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/DividerView.java deleted file mode 100644 index 73be2835d2cd..000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/DividerView.java +++ /dev/null @@ -1,1314 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES 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.legacysplitscreen; - -import static android.view.PointerIcon.TYPE_HORIZONTAL_DOUBLE_ARROW; -import static android.view.PointerIcon.TYPE_VERTICAL_DOUBLE_ARROW; -import static android.view.WindowManager.DOCKED_RIGHT; - -import static com.android.wm.shell.animation.Interpolators.DIM_INTERPOLATOR; -import static com.android.wm.shell.animation.Interpolators.SLOWDOWN_INTERPOLATOR; -import static com.android.wm.shell.common.split.DividerView.TOUCH_ANIMATION_DURATION; -import static com.android.wm.shell.common.split.DividerView.TOUCH_RELEASE_ANIMATION_DURATION; - -import android.animation.AnimationHandler; -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.animation.ValueAnimator; -import android.annotation.Nullable; -import android.content.Context; -import android.content.res.Configuration; -import android.graphics.Matrix; -import android.graphics.Rect; -import android.graphics.Region; -import android.graphics.Region.Op; -import android.hardware.display.DisplayManager; -import android.os.Bundle; -import android.util.AttributeSet; -import android.util.Slog; -import android.view.Choreographer; -import android.view.Display; -import android.view.MotionEvent; -import android.view.PointerIcon; -import android.view.SurfaceControl; -import android.view.SurfaceControl.Transaction; -import android.view.VelocityTracker; -import android.view.View; -import android.view.View.OnTouchListener; -import android.view.ViewConfiguration; -import android.view.ViewTreeObserver.InternalInsetsInfo; -import android.view.ViewTreeObserver.OnComputeInternalInsetsListener; -import android.view.WindowManager; -import android.view.accessibility.AccessibilityNodeInfo; -import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; -import android.view.animation.Interpolator; -import android.view.animation.PathInterpolator; -import android.widget.FrameLayout; - -import com.android.internal.logging.MetricsLogger; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; -import com.android.internal.policy.DividerSnapAlgorithm; -import com.android.internal.policy.DividerSnapAlgorithm.SnapTarget; -import com.android.internal.policy.DockedDividerUtils; -import com.android.wm.shell.R; -import com.android.wm.shell.animation.FlingAnimationUtils; -import com.android.wm.shell.animation.Interpolators; -import com.android.wm.shell.common.split.DividerHandleView; - -import java.util.function.Consumer; - -/** - * Docked stack divider. - */ -public class DividerView extends FrameLayout implements OnTouchListener, - OnComputeInternalInsetsListener { - private static final String TAG = "DividerView"; - private static final boolean DEBUG = LegacySplitScreenController.DEBUG; - - interface DividerCallbacks { - void onDraggingStart(); - void onDraggingEnd(); - } - - public static final int INVALID_RECENTS_GROW_TARGET = -1; - - private static final int LOG_VALUE_RESIZE_50_50 = 0; - private static final int LOG_VALUE_RESIZE_DOCKED_SMALLER = 1; - private static final int LOG_VALUE_RESIZE_DOCKED_LARGER = 2; - - private static final int LOG_VALUE_UNDOCK_MAX_DOCKED = 0; - private static final int LOG_VALUE_UNDOCK_MAX_OTHER = 1; - - private static final int TASK_POSITION_SAME = Integer.MAX_VALUE; - - /** - * How much the background gets scaled when we are in the minimized dock state. - */ - private static final float MINIMIZE_DOCK_SCALE = 0f; - private static final float ADJUSTED_FOR_IME_SCALE = 0.5f; - - private static final Interpolator IME_ADJUST_INTERPOLATOR = - new PathInterpolator(0.2f, 0f, 0.1f, 1f); - - private DividerHandleView mHandle; - private View mBackground; - private MinimizedDockShadow mMinimizedShadow; - private int mStartX; - private int mStartY; - private int mStartPosition; - private int mDockSide; - private boolean mMoving; - private int mTouchSlop; - private boolean mBackgroundLifted; - private boolean mIsInMinimizeInteraction; - SnapTarget mSnapTargetBeforeMinimized; - - private int mDividerInsets; - private final Display mDefaultDisplay; - - private int mDividerSize; - private int mTouchElevation; - private int mLongPressEntraceAnimDuration; - - private final Rect mDockedRect = new Rect(); - private final Rect mDockedTaskRect = new Rect(); - private final Rect mOtherTaskRect = new Rect(); - private final Rect mOtherRect = new Rect(); - private final Rect mDockedInsetRect = new Rect(); - private final Rect mOtherInsetRect = new Rect(); - private final Rect mLastResizeRect = new Rect(); - private final Rect mTmpRect = new Rect(); - private LegacySplitScreenController mSplitScreenController; - private WindowManagerProxy mWindowManagerProxy; - private DividerWindowManager mWindowManager; - private VelocityTracker mVelocityTracker; - private FlingAnimationUtils mFlingAnimationUtils; - private LegacySplitDisplayLayout mSplitLayout; - private DividerImeController mImeController; - private DividerCallbacks mCallback; - - private AnimationHandler mSfVsyncAnimationHandler; - private ValueAnimator mCurrentAnimator; - private boolean mEntranceAnimationRunning; - private boolean mExitAnimationRunning; - private int mExitStartPosition; - private boolean mDockedStackMinimized; - private boolean mHomeStackResizable; - private boolean mAdjustedForIme; - private DividerState mState; - - private LegacySplitScreenTaskListener mTiles; - boolean mFirstLayout = true; - int mDividerPositionX; - int mDividerPositionY; - - private final Matrix mTmpMatrix = new Matrix(); - private final float[] mTmpValues = new float[9]; - - // The view is removed or in the process of been removed from the system. - private boolean mRemoved; - - // Whether the surface for this view has been hidden regardless of actual visibility. This is - // used interact with keyguard. - private boolean mSurfaceHidden = false; - - private final AccessibilityDelegate mHandleDelegate = new AccessibilityDelegate() { - @Override - public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) { - super.onInitializeAccessibilityNodeInfo(host, info); - final DividerSnapAlgorithm snapAlgorithm = getSnapAlgorithm(); - if (isHorizontalDivision()) { - info.addAction(new AccessibilityAction(R.id.action_move_tl_full, - mContext.getString(R.string.accessibility_action_divider_top_full))); - if (snapAlgorithm.isFirstSplitTargetAvailable()) { - info.addAction(new AccessibilityAction(R.id.action_move_tl_70, - mContext.getString(R.string.accessibility_action_divider_top_70))); - } - if (snapAlgorithm.showMiddleSplitTargetForAccessibility()) { - // Only show the middle target if there are more than 1 split target - info.addAction(new AccessibilityAction(R.id.action_move_tl_50, - mContext.getString(R.string.accessibility_action_divider_top_50))); - } - if (snapAlgorithm.isLastSplitTargetAvailable()) { - info.addAction(new AccessibilityAction(R.id.action_move_tl_30, - mContext.getString(R.string.accessibility_action_divider_top_30))); - } - info.addAction(new AccessibilityAction(R.id.action_move_rb_full, - mContext.getString(R.string.accessibility_action_divider_bottom_full))); - } else { - info.addAction(new AccessibilityAction(R.id.action_move_tl_full, - mContext.getString(R.string.accessibility_action_divider_left_full))); - if (snapAlgorithm.isFirstSplitTargetAvailable()) { - info.addAction(new AccessibilityAction(R.id.action_move_tl_70, - mContext.getString(R.string.accessibility_action_divider_left_70))); - } - if (snapAlgorithm.showMiddleSplitTargetForAccessibility()) { - // Only show the middle target if there are more than 1 split target - info.addAction(new AccessibilityAction(R.id.action_move_tl_50, - mContext.getString(R.string.accessibility_action_divider_left_50))); - } - if (snapAlgorithm.isLastSplitTargetAvailable()) { - info.addAction(new AccessibilityAction(R.id.action_move_tl_30, - mContext.getString(R.string.accessibility_action_divider_left_30))); - } - info.addAction(new AccessibilityAction(R.id.action_move_rb_full, - mContext.getString(R.string.accessibility_action_divider_right_full))); - } - } - - @Override - public boolean performAccessibilityAction(View host, int action, Bundle args) { - int currentPosition = getCurrentPosition(); - SnapTarget nextTarget = null; - DividerSnapAlgorithm snapAlgorithm = mSplitLayout.getSnapAlgorithm(); - if (action == R.id.action_move_tl_full) { - nextTarget = snapAlgorithm.getDismissEndTarget(); - } else if (action == R.id.action_move_tl_70) { - nextTarget = snapAlgorithm.getLastSplitTarget(); - } else if (action == R.id.action_move_tl_50) { - nextTarget = snapAlgorithm.getMiddleTarget(); - } else if (action == R.id.action_move_tl_30) { - nextTarget = snapAlgorithm.getFirstSplitTarget(); - } else if (action == R.id.action_move_rb_full) { - nextTarget = snapAlgorithm.getDismissStartTarget(); - } - if (nextTarget != null) { - startDragging(true /* animate */, false /* touching */); - stopDragging(currentPosition, nextTarget, 250, Interpolators.FAST_OUT_SLOW_IN); - return true; - } - return super.performAccessibilityAction(host, action, args); - } - }; - - private final Runnable mResetBackgroundRunnable = new Runnable() { - @Override - public void run() { - resetBackground(); - } - }; - - public DividerView(Context context) { - this(context, null); - } - - public DividerView(Context context, @Nullable AttributeSet attrs) { - this(context, attrs, 0); - } - - public DividerView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { - this(context, attrs, defStyleAttr, 0); - } - - public DividerView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, - int defStyleRes) { - super(context, attrs, defStyleAttr, defStyleRes); - final DisplayManager displayManager = - (DisplayManager) mContext.getSystemService(Context.DISPLAY_SERVICE); - mDefaultDisplay = displayManager.getDisplay(Display.DEFAULT_DISPLAY); - } - - public void setAnimationHandler(AnimationHandler sfVsyncAnimationHandler) { - mSfVsyncAnimationHandler = sfVsyncAnimationHandler; - } - - @Override - protected void onFinishInflate() { - super.onFinishInflate(); - mHandle = findViewById(R.id.docked_divider_handle); - mBackground = findViewById(R.id.docked_divider_background); - mMinimizedShadow = findViewById(R.id.minimized_dock_shadow); - mHandle.setOnTouchListener(this); - final int dividerWindowWidth = getResources().getDimensionPixelSize( - com.android.internal.R.dimen.docked_stack_divider_thickness); - mDividerInsets = getResources().getDimensionPixelSize( - com.android.internal.R.dimen.docked_stack_divider_insets); - mDividerSize = dividerWindowWidth - 2 * mDividerInsets; - mTouchElevation = getResources().getDimensionPixelSize( - R.dimen.docked_stack_divider_lift_elevation); - mLongPressEntraceAnimDuration = getResources().getInteger( - R.integer.long_press_dock_anim_duration); - mTouchSlop = ViewConfiguration.get(mContext).getScaledTouchSlop(); - mFlingAnimationUtils = new FlingAnimationUtils(getResources().getDisplayMetrics(), 0.3f); - boolean landscape = getResources().getConfiguration().orientation - == Configuration.ORIENTATION_LANDSCAPE; - mHandle.setPointerIcon(PointerIcon.getSystemIcon(getContext(), - landscape ? TYPE_HORIZONTAL_DOUBLE_ARROW : TYPE_VERTICAL_DOUBLE_ARROW)); - getViewTreeObserver().addOnComputeInternalInsetsListener(this); - mHandle.setAccessibilityDelegate(mHandleDelegate); - } - - @Override - protected void onAttachedToWindow() { - super.onAttachedToWindow(); - - // Save the current target if not minimized once attached to window - if (mDockSide != WindowManager.DOCKED_INVALID && !mIsInMinimizeInteraction) { - saveSnapTargetBeforeMinimized(mSnapTargetBeforeMinimized); - } - mFirstLayout = true; - } - - void onDividerRemoved() { - mRemoved = true; - mCallback = null; - } - - @Override - protected void onLayout(boolean changed, int left, int top, int right, int bottom) { - super.onLayout(changed, left, top, right, bottom); - if (mFirstLayout) { - // Wait for first layout so that the ViewRootImpl surface has been created. - initializeSurfaceState(); - mFirstLayout = false; - } - int minimizeLeft = 0; - int minimizeTop = 0; - if (mDockSide == WindowManager.DOCKED_TOP) { - minimizeTop = mBackground.getTop(); - } else if (mDockSide == WindowManager.DOCKED_LEFT) { - minimizeLeft = mBackground.getLeft(); - } else if (mDockSide == WindowManager.DOCKED_RIGHT) { - minimizeLeft = mBackground.getRight() - mMinimizedShadow.getWidth(); - } - mMinimizedShadow.layout(minimizeLeft, minimizeTop, - minimizeLeft + mMinimizedShadow.getMeasuredWidth(), - minimizeTop + mMinimizedShadow.getMeasuredHeight()); - if (changed) { - notifySplitScreenBoundsChanged(); - } - } - - void injectDependencies(LegacySplitScreenController splitScreenController, - DividerWindowManager windowManager, DividerState dividerState, - DividerCallbacks callback, LegacySplitScreenTaskListener tiles, - LegacySplitDisplayLayout sdl, DividerImeController imeController, - WindowManagerProxy wmProxy) { - mSplitScreenController = splitScreenController; - mWindowManager = windowManager; - mState = dividerState; - mCallback = callback; - mTiles = tiles; - mSplitLayout = sdl; - mImeController = imeController; - mWindowManagerProxy = wmProxy; - - if (mState.mRatioPositionBeforeMinimized == 0) { - // Set the middle target as the initial state - mSnapTargetBeforeMinimized = mSplitLayout.getSnapAlgorithm().getMiddleTarget(); - } else { - repositionSnapTargetBeforeMinimized(); - } - } - - /** Gets non-minimized secondary bounds of split screen. */ - public Rect getNonMinimizedSplitScreenSecondaryBounds() { - mOtherTaskRect.set(mSplitLayout.mSecondary); - return mOtherTaskRect; - } - - private boolean inSplitMode() { - return getVisibility() == VISIBLE; - } - - /** Unlike setVisible, this directly hides the surface without changing view visibility. */ - void setHidden(boolean hidden) { - if (mSurfaceHidden == hidden) { - return; - } - mSurfaceHidden = hidden; - post(() -> { - final SurfaceControl sc = getWindowSurfaceControl(); - if (sc == null) { - return; - } - Transaction t = mTiles.getTransaction(); - if (hidden) { - t.hide(sc); - } else { - t.show(sc); - } - mImeController.setDimsHidden(t, hidden); - t.apply(); - mTiles.releaseTransaction(t); - }); - } - - boolean isHidden() { - return getVisibility() != View.VISIBLE || mSurfaceHidden; - } - - /** Starts dragging the divider bar. */ - public boolean startDragging(boolean animate, boolean touching) { - cancelFlingAnimation(); - if (touching) { - mHandle.setTouching(true, animate); - } - mDockSide = mSplitLayout.getPrimarySplitSide(); - - mWindowManagerProxy.setResizing(true); - if (touching) { - mWindowManager.setSlippery(false); - liftBackground(); - } - if (mCallback != null) { - mCallback.onDraggingStart(); - } - return inSplitMode(); - } - - /** Stops dragging the divider bar. */ - public void stopDragging(int position, float velocity, boolean avoidDismissStart, - boolean logMetrics) { - mHandle.setTouching(false, true /* animate */); - fling(position, velocity, avoidDismissStart, logMetrics); - mWindowManager.setSlippery(true); - releaseBackground(); - } - - private void stopDragging(int position, SnapTarget target, long duration, - Interpolator interpolator) { - stopDragging(position, target, duration, 0 /* startDelay*/, 0 /* endDelay */, interpolator); - } - - private void stopDragging(int position, SnapTarget target, long duration, - Interpolator interpolator, long endDelay) { - stopDragging(position, target, duration, 0 /* startDelay*/, endDelay, interpolator); - } - - private void stopDragging(int position, SnapTarget target, long duration, long startDelay, - long endDelay, Interpolator interpolator) { - mHandle.setTouching(false, true /* animate */); - flingTo(position, target, duration, startDelay, endDelay, interpolator); - mWindowManager.setSlippery(true); - releaseBackground(); - } - - private void stopDragging() { - mHandle.setTouching(false, true /* animate */); - mWindowManager.setSlippery(true); - mWindowManagerProxy.setResizing(false); - releaseBackground(); - } - - private void updateDockSide() { - mDockSide = mSplitLayout.getPrimarySplitSide(); - mMinimizedShadow.setDockSide(mDockSide); - } - - public DividerSnapAlgorithm getSnapAlgorithm() { - return mDockedStackMinimized ? mSplitLayout.getMinimizedSnapAlgorithm(mHomeStackResizable) - : mSplitLayout.getSnapAlgorithm(); - } - - public int getCurrentPosition() { - return isHorizontalDivision() ? mDividerPositionY : mDividerPositionX; - } - - public boolean isMinimized() { - return mDockedStackMinimized; - } - - @Override - public boolean onTouch(View v, MotionEvent event) { - convertToScreenCoordinates(event); - final int action = event.getAction() & MotionEvent.ACTION_MASK; - switch (action) { - case MotionEvent.ACTION_DOWN: - mVelocityTracker = VelocityTracker.obtain(); - mVelocityTracker.addMovement(event); - mStartX = (int) event.getX(); - mStartY = (int) event.getY(); - boolean result = startDragging(true /* animate */, true /* touching */); - if (!result) { - - // Weren't able to start dragging successfully, so cancel it again. - stopDragging(); - } - mStartPosition = getCurrentPosition(); - mMoving = false; - return result; - case MotionEvent.ACTION_MOVE: - mVelocityTracker.addMovement(event); - int x = (int) event.getX(); - int y = (int) event.getY(); - boolean exceededTouchSlop = - isHorizontalDivision() && Math.abs(y - mStartY) > mTouchSlop - || (!isHorizontalDivision() && Math.abs(x - mStartX) > mTouchSlop); - if (!mMoving && exceededTouchSlop) { - mStartX = x; - mStartY = y; - mMoving = true; - } - if (mMoving && mDockSide != WindowManager.DOCKED_INVALID) { - SnapTarget snapTarget = getSnapAlgorithm().calculateSnapTarget( - mStartPosition, 0 /* velocity */, false /* hardDismiss */); - resizeStackSurfaces(calculatePosition(x, y), mStartPosition, snapTarget, - null /* transaction */); - } - break; - case MotionEvent.ACTION_UP: - case MotionEvent.ACTION_CANCEL: - if (!mMoving) { - stopDragging(); - break; - } - - x = (int) event.getRawX(); - y = (int) event.getRawY(); - mVelocityTracker.addMovement(event); - mVelocityTracker.computeCurrentVelocity(1000); - int position = calculatePosition(x, y); - stopDragging(position, isHorizontalDivision() ? mVelocityTracker.getYVelocity() - : mVelocityTracker.getXVelocity(), false /* avoidDismissStart */, - true /* log */); - mMoving = false; - break; - } - return true; - } - - private void logResizeEvent(SnapTarget snapTarget) { - if (snapTarget == mSplitLayout.getSnapAlgorithm().getDismissStartTarget()) { - MetricsLogger.action( - mContext, MetricsEvent.ACTION_WINDOW_UNDOCK_MAX, dockSideTopLeft(mDockSide) - ? LOG_VALUE_UNDOCK_MAX_OTHER - : LOG_VALUE_UNDOCK_MAX_DOCKED); - } else if (snapTarget == mSplitLayout.getSnapAlgorithm().getDismissEndTarget()) { - MetricsLogger.action( - mContext, MetricsEvent.ACTION_WINDOW_UNDOCK_MAX, dockSideBottomRight(mDockSide) - ? LOG_VALUE_UNDOCK_MAX_OTHER - : LOG_VALUE_UNDOCK_MAX_DOCKED); - } else if (snapTarget == mSplitLayout.getSnapAlgorithm().getMiddleTarget()) { - MetricsLogger.action(mContext, MetricsEvent.ACTION_WINDOW_DOCK_RESIZE, - LOG_VALUE_RESIZE_50_50); - } else if (snapTarget == mSplitLayout.getSnapAlgorithm().getFirstSplitTarget()) { - MetricsLogger.action(mContext, MetricsEvent.ACTION_WINDOW_DOCK_RESIZE, - dockSideTopLeft(mDockSide) - ? LOG_VALUE_RESIZE_DOCKED_SMALLER - : LOG_VALUE_RESIZE_DOCKED_LARGER); - } else if (snapTarget == mSplitLayout.getSnapAlgorithm().getLastSplitTarget()) { - MetricsLogger.action(mContext, MetricsEvent.ACTION_WINDOW_DOCK_RESIZE, - dockSideTopLeft(mDockSide) - ? LOG_VALUE_RESIZE_DOCKED_LARGER - : LOG_VALUE_RESIZE_DOCKED_SMALLER); - } - } - - private void convertToScreenCoordinates(MotionEvent event) { - event.setLocation(event.getRawX(), event.getRawY()); - } - - private void fling(int position, float velocity, boolean avoidDismissStart, - boolean logMetrics) { - DividerSnapAlgorithm currentSnapAlgorithm = getSnapAlgorithm(); - SnapTarget snapTarget = currentSnapAlgorithm.calculateSnapTarget(position, velocity); - if (avoidDismissStart && snapTarget == currentSnapAlgorithm.getDismissStartTarget()) { - snapTarget = currentSnapAlgorithm.getFirstSplitTarget(); - } - if (logMetrics) { - logResizeEvent(snapTarget); - } - ValueAnimator anim = getFlingAnimator(position, snapTarget, 0 /* endDelay */); - mFlingAnimationUtils.apply(anim, position, snapTarget.position, velocity); - anim.start(); - } - - private void flingTo(int position, SnapTarget target, long duration, long startDelay, - long endDelay, Interpolator interpolator) { - ValueAnimator anim = getFlingAnimator(position, target, endDelay); - anim.setDuration(duration); - anim.setStartDelay(startDelay); - anim.setInterpolator(interpolator); - anim.start(); - } - - private ValueAnimator getFlingAnimator(int position, final SnapTarget snapTarget, - final long endDelay) { - if (mCurrentAnimator != null) { - cancelFlingAnimation(); - updateDockSide(); - } - if (DEBUG) Slog.d(TAG, "Getting fling " + position + "->" + snapTarget.position); - final boolean taskPositionSameAtEnd = snapTarget.flag == SnapTarget.FLAG_NONE; - ValueAnimator anim = ValueAnimator.ofInt(position, snapTarget.position); - anim.addUpdateListener(animation -> resizeStackSurfaces((int) animation.getAnimatedValue(), - taskPositionSameAtEnd && animation.getAnimatedFraction() == 1f - ? TASK_POSITION_SAME - : snapTarget.taskPosition, - snapTarget, null /* transaction */)); - Consumer<Boolean> endAction = cancelled -> { - if (DEBUG) Slog.d(TAG, "End Fling " + cancelled + " min:" + mIsInMinimizeInteraction); - final boolean wasMinimizeInteraction = mIsInMinimizeInteraction; - // Reset minimized divider position after unminimized state animation finishes. - if (!cancelled && !mDockedStackMinimized && mIsInMinimizeInteraction) { - mIsInMinimizeInteraction = false; - } - boolean dismissed = commitSnapFlags(snapTarget); - mWindowManagerProxy.setResizing(false); - updateDockSide(); - mCurrentAnimator = null; - mEntranceAnimationRunning = false; - mExitAnimationRunning = false; - if (!dismissed && !wasMinimizeInteraction) { - mWindowManagerProxy.applyResizeSplits(snapTarget.position, mSplitLayout); - } - if (mCallback != null) { - mCallback.onDraggingEnd(); - } - - // Record last snap target the divider moved to - if (!mIsInMinimizeInteraction) { - // The last snapTarget position can be negative when the last divider position was - // offscreen. In that case, save the middle (default) SnapTarget so calculating next - // position isn't negative. - final SnapTarget saveTarget; - if (snapTarget.position < 0) { - saveTarget = mSplitLayout.getSnapAlgorithm().getMiddleTarget(); - } else { - saveTarget = snapTarget; - } - final DividerSnapAlgorithm snapAlgo = mSplitLayout.getSnapAlgorithm(); - if (saveTarget.position != snapAlgo.getDismissEndTarget().position - && saveTarget.position != snapAlgo.getDismissStartTarget().position) { - saveSnapTargetBeforeMinimized(saveTarget); - } - } - notifySplitScreenBoundsChanged(); - }; - anim.addListener(new AnimatorListenerAdapter() { - - private boolean mCancelled; - - @Override - public void onAnimationCancel(Animator animation) { - mCancelled = true; - } - - @Override - public void onAnimationEnd(Animator animation) { - long delay = 0; - if (endDelay != 0) { - delay = endDelay; - } else if (mCancelled) { - delay = 0; - } - if (delay == 0) { - endAction.accept(mCancelled); - } else { - final Boolean cancelled = mCancelled; - if (DEBUG) Slog.d(TAG, "Posting endFling " + cancelled + " d:" + delay + "ms"); - getHandler().postDelayed(() -> endAction.accept(cancelled), delay); - } - } - }); - mCurrentAnimator = anim; - mCurrentAnimator.setAnimationHandler(mSfVsyncAnimationHandler); - return anim; - } - - private void notifySplitScreenBoundsChanged() { - if (mSplitLayout.mPrimary == null || mSplitLayout.mSecondary == null) { - return; - } - mOtherTaskRect.set(mSplitLayout.mSecondary); - - mTmpRect.set(mHandle.getLeft(), mHandle.getTop(), mHandle.getRight(), mHandle.getBottom()); - if (isHorizontalDivision()) { - mTmpRect.offsetTo(mHandle.getLeft(), mDividerPositionY); - } else { - mTmpRect.offsetTo(mDividerPositionX, mHandle.getTop()); - } - mWindowManagerProxy.setTouchRegion(mTmpRect); - - mTmpRect.set(mSplitLayout.mDisplayLayout.stableInsets()); - switch (mSplitLayout.getPrimarySplitSide()) { - case WindowManager.DOCKED_LEFT: - mTmpRect.left = 0; - break; - case WindowManager.DOCKED_RIGHT: - mTmpRect.right = 0; - break; - case WindowManager.DOCKED_TOP: - mTmpRect.top = 0; - break; - } - mSplitScreenController.notifyBoundsChanged(mOtherTaskRect, mTmpRect); - } - - private void cancelFlingAnimation() { - if (mCurrentAnimator != null) { - mCurrentAnimator.cancel(); - } - } - - private boolean commitSnapFlags(SnapTarget target) { - if (target.flag == SnapTarget.FLAG_NONE) { - return false; - } - final boolean dismissOrMaximize; - if (target.flag == SnapTarget.FLAG_DISMISS_START) { - dismissOrMaximize = mDockSide == WindowManager.DOCKED_LEFT - || mDockSide == WindowManager.DOCKED_TOP; - } else { - dismissOrMaximize = mDockSide == WindowManager.DOCKED_RIGHT - || mDockSide == WindowManager.DOCKED_BOTTOM; - } - mWindowManagerProxy.dismissOrMaximizeDocked(mTiles, mSplitLayout, dismissOrMaximize); - Transaction t = mTiles.getTransaction(); - setResizeDimLayer(t, true /* primary */, 0f); - setResizeDimLayer(t, false /* primary */, 0f); - t.apply(); - mTiles.releaseTransaction(t); - return true; - } - - private void liftBackground() { - if (mBackgroundLifted) { - return; - } - if (isHorizontalDivision()) { - mBackground.animate().scaleY(1.4f); - } else { - mBackground.animate().scaleX(1.4f); - } - mBackground.animate() - .setInterpolator(Interpolators.TOUCH_RESPONSE) - .setDuration(TOUCH_ANIMATION_DURATION) - .translationZ(mTouchElevation) - .start(); - - // Lift handle as well so it doesn't get behind the background, even though it doesn't - // cast shadow. - mHandle.animate() - .setInterpolator(Interpolators.TOUCH_RESPONSE) - .setDuration(TOUCH_ANIMATION_DURATION) - .translationZ(mTouchElevation) - .start(); - mBackgroundLifted = true; - } - - private void releaseBackground() { - if (!mBackgroundLifted) { - return; - } - mBackground.animate() - .setInterpolator(Interpolators.FAST_OUT_SLOW_IN) - .setDuration(TOUCH_RELEASE_ANIMATION_DURATION) - .translationZ(0) - .scaleX(1f) - .scaleY(1f) - .start(); - mHandle.animate() - .setInterpolator(Interpolators.FAST_OUT_SLOW_IN) - .setDuration(TOUCH_RELEASE_ANIMATION_DURATION) - .translationZ(0) - .start(); - mBackgroundLifted = false; - } - - private void initializeSurfaceState() { - int midPos = mSplitLayout.getSnapAlgorithm().getMiddleTarget().position; - // Recalculate the split-layout's internal tile bounds - mSplitLayout.resizeSplits(midPos); - Transaction t = mTiles.getTransaction(); - if (mDockedStackMinimized) { - int position = mSplitLayout.getMinimizedSnapAlgorithm(mHomeStackResizable) - .getMiddleTarget().position; - calculateBoundsForPosition(position, mDockSide, mDockedRect); - calculateBoundsForPosition(position, DockedDividerUtils.invertDockSide(mDockSide), - mOtherRect); - mDividerPositionX = mDividerPositionY = position; - resizeSplitSurfaces(t, mDockedRect, mSplitLayout.mPrimary, - mOtherRect, mSplitLayout.mSecondary); - } else { - resizeSplitSurfaces(t, mSplitLayout.mPrimary, null, - mSplitLayout.mSecondary, null); - } - setResizeDimLayer(t, true /* primary */, 0.f /* alpha */); - setResizeDimLayer(t, false /* secondary */, 0.f /* alpha */); - t.apply(); - mTiles.releaseTransaction(t); - - // Get the actually-visible bar dimensions (relative to full window). This is a thin - // bar going through the center. - final Rect dividerBar = isHorizontalDivision() - ? new Rect(0, mDividerInsets, mSplitLayout.mDisplayLayout.width(), - mDividerInsets + mDividerSize) - : new Rect(mDividerInsets, 0, mDividerInsets + mDividerSize, - mSplitLayout.mDisplayLayout.height()); - final Region touchRegion = new Region(dividerBar); - // Add in the "draggable" portion. While not visible, this is an expanded area that the - // user can interact with. - touchRegion.union(new Rect(mHandle.getLeft(), mHandle.getTop(), - mHandle.getRight(), mHandle.getBottom())); - mWindowManager.setTouchRegion(touchRegion); - } - - void setMinimizedDockStack(boolean minimized, boolean isHomeStackResizable, - Transaction t) { - mHomeStackResizable = isHomeStackResizable; - updateDockSide(); - if (!minimized) { - resetBackground(); - } - mMinimizedShadow.setAlpha(minimized ? 1f : 0f); - if (mDockedStackMinimized != minimized) { - mDockedStackMinimized = minimized; - if (mSplitLayout.mDisplayLayout.rotation() != mDefaultDisplay.getRotation()) { - // Splitscreen to minimize is about to starts after rotating landscape to seascape, - // update display info and snap algorithm targets - repositionSnapTargetBeforeMinimized(); - } - if (mIsInMinimizeInteraction != minimized || mCurrentAnimator != null) { - cancelFlingAnimation(); - if (minimized) { - // Relayout to recalculate the divider shadow when minimizing - requestLayout(); - mIsInMinimizeInteraction = true; - resizeStackSurfaces(mSplitLayout.getMinimizedSnapAlgorithm(mHomeStackResizable) - .getMiddleTarget(), t); - } else { - resizeStackSurfaces(mSnapTargetBeforeMinimized, t); - mIsInMinimizeInteraction = false; - } - } - } - } - - void enterSplitMode(boolean isHomeStackResizable) { - setHidden(false); - - SnapTarget miniMid = - mSplitLayout.getMinimizedSnapAlgorithm(isHomeStackResizable).getMiddleTarget(); - if (mDockedStackMinimized) { - mDividerPositionY = mDividerPositionX = miniMid.position; - } - } - - /** - * Tries to grab a surface control from ViewRootImpl. If this isn't available for some reason - * (ie. the window isn't ready yet), it will get the surfacecontrol that the WindowlessWM has - * assigned to it. - */ - private SurfaceControl getWindowSurfaceControl() { - return mWindowManager.mSystemWindows.getViewSurface(this); - } - - void exitSplitMode() { - // The view is going to be removed right after this function involved, updates the surface - // in the current thread instead of posting it to the view's UI thread. - final SurfaceControl sc = getWindowSurfaceControl(); - if (sc == null) { - return; - } - Transaction t = mTiles.getTransaction(); - t.hide(sc); - mImeController.setDimsHidden(t, true); - t.apply(); - mTiles.releaseTransaction(t); - - // Reset tile bounds - int midPos = mSplitLayout.getSnapAlgorithm().getMiddleTarget().position; - mWindowManagerProxy.applyResizeSplits(midPos, mSplitLayout); - } - - void setMinimizedDockStack(boolean minimized, long animDuration, - boolean isHomeStackResizable) { - if (DEBUG) Slog.d(TAG, "setMinDock: " + mDockedStackMinimized + "->" + minimized); - mHomeStackResizable = isHomeStackResizable; - updateDockSide(); - if (mDockedStackMinimized != minimized) { - mIsInMinimizeInteraction = true; - mDockedStackMinimized = minimized; - stopDragging(minimized - ? mSnapTargetBeforeMinimized.position - : getCurrentPosition(), - minimized - ? mSplitLayout.getMinimizedSnapAlgorithm(mHomeStackResizable) - .getMiddleTarget() - : mSnapTargetBeforeMinimized, - animDuration, Interpolators.FAST_OUT_SLOW_IN, 0); - setAdjustedForIme(false, animDuration); - } - if (!minimized) { - mBackground.animate().withEndAction(mResetBackgroundRunnable); - } - mBackground.animate() - .setInterpolator(Interpolators.FAST_OUT_SLOW_IN) - .setDuration(animDuration) - .start(); - } - - // Needed to end any currently playing animations when they might compete with other anims - // (specifically, IME adjust animation immediately after leaving minimized). Someday maybe - // these can be unified, but not today. - void finishAnimations() { - if (mCurrentAnimator != null) { - mCurrentAnimator.end(); - } - } - - void setAdjustedForIme(boolean adjustedForIme, long animDuration) { - if (mAdjustedForIme == adjustedForIme) { - return; - } - updateDockSide(); - mHandle.animate() - .setInterpolator(IME_ADJUST_INTERPOLATOR) - .setDuration(animDuration) - .alpha(adjustedForIme ? 0f : 1f) - .start(); - if (mDockSide == WindowManager.DOCKED_TOP) { - mBackground.setPivotY(0); - mBackground.animate() - .scaleY(adjustedForIme ? ADJUSTED_FOR_IME_SCALE : 1f); - } - if (!adjustedForIme) { - mBackground.animate().withEndAction(mResetBackgroundRunnable); - } - mBackground.animate() - .setInterpolator(IME_ADJUST_INTERPOLATOR) - .setDuration(animDuration) - .start(); - mAdjustedForIme = adjustedForIme; - } - - private void saveSnapTargetBeforeMinimized(SnapTarget target) { - mSnapTargetBeforeMinimized = target; - mState.mRatioPositionBeforeMinimized = (float) target.position - / (isHorizontalDivision() ? mSplitLayout.mDisplayLayout.height() - : mSplitLayout.mDisplayLayout.width()); - } - - private void resetBackground() { - mBackground.setPivotX(mBackground.getWidth() / 2); - mBackground.setPivotY(mBackground.getHeight() / 2); - mBackground.setScaleX(1f); - mBackground.setScaleY(1f); - mMinimizedShadow.setAlpha(0f); - } - - @Override - protected void onConfigurationChanged(Configuration newConfig) { - super.onConfigurationChanged(newConfig); - } - - private void repositionSnapTargetBeforeMinimized() { - int position = (int) (mState.mRatioPositionBeforeMinimized - * (isHorizontalDivision() ? mSplitLayout.mDisplayLayout.height() - : mSplitLayout.mDisplayLayout.width())); - - // Set the snap target before minimized but do not save until divider is attached and not - // minimized because it does not know its minimized state yet. - mSnapTargetBeforeMinimized = - mSplitLayout.getSnapAlgorithm().calculateNonDismissingSnapTarget(position); - } - - private int calculatePosition(int touchX, int touchY) { - return isHorizontalDivision() ? calculateYPosition(touchY) : calculateXPosition(touchX); - } - - public boolean isHorizontalDivision() { - return getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT; - } - - private int calculateXPosition(int touchX) { - return mStartPosition + touchX - mStartX; - } - - private int calculateYPosition(int touchY) { - return mStartPosition + touchY - mStartY; - } - - private void alignTopLeft(Rect containingRect, Rect rect) { - int width = rect.width(); - int height = rect.height(); - rect.set(containingRect.left, containingRect.top, - containingRect.left + width, containingRect.top + height); - } - - private void alignBottomRight(Rect containingRect, Rect rect) { - int width = rect.width(); - int height = rect.height(); - rect.set(containingRect.right - width, containingRect.bottom - height, - containingRect.right, containingRect.bottom); - } - - private void calculateBoundsForPosition(int position, int dockSide, Rect outRect) { - DockedDividerUtils.calculateBoundsForPosition(position, dockSide, outRect, - mSplitLayout.mDisplayLayout.width(), mSplitLayout.mDisplayLayout.height(), - mDividerSize); - } - - private void resizeStackSurfaces(SnapTarget taskSnapTarget, Transaction t) { - resizeStackSurfaces(taskSnapTarget.position, taskSnapTarget.position, taskSnapTarget, t); - } - - void resizeSplitSurfaces(Transaction t, Rect dockedRect, Rect otherRect) { - resizeSplitSurfaces(t, dockedRect, null, otherRect, null); - } - - private void resizeSplitSurfaces(Transaction t, Rect dockedRect, Rect dockedTaskRect, - Rect otherRect, Rect otherTaskRect) { - dockedTaskRect = dockedTaskRect == null ? dockedRect : dockedTaskRect; - otherTaskRect = otherTaskRect == null ? otherRect : otherTaskRect; - - mDividerPositionX = mSplitLayout.getPrimarySplitSide() == DOCKED_RIGHT - ? otherRect.right : dockedRect.right; - mDividerPositionY = dockedRect.bottom; - - if (DEBUG) { - Slog.d(TAG, "Resizing split surfaces: " + dockedRect + " " + dockedTaskRect - + " " + otherRect + " " + otherTaskRect); - } - - t.setPosition(mTiles.mPrimarySurface, dockedTaskRect.left, dockedTaskRect.top); - Rect crop = new Rect(dockedRect); - crop.offsetTo(-Math.min(dockedTaskRect.left - dockedRect.left, 0), - -Math.min(dockedTaskRect.top - dockedRect.top, 0)); - t.setWindowCrop(mTiles.mPrimarySurface, crop); - t.setPosition(mTiles.mSecondarySurface, otherTaskRect.left, otherTaskRect.top); - crop.set(otherRect); - crop.offsetTo(-(otherTaskRect.left - otherRect.left), - -(otherTaskRect.top - otherRect.top)); - t.setWindowCrop(mTiles.mSecondarySurface, crop); - final SurfaceControl dividerCtrl = getWindowSurfaceControl(); - if (dividerCtrl != null) { - if (isHorizontalDivision()) { - t.setPosition(dividerCtrl, 0, mDividerPositionY - mDividerInsets); - } else { - t.setPosition(dividerCtrl, mDividerPositionX - mDividerInsets, 0); - } - } - } - - void setResizeDimLayer(Transaction t, boolean primary, float alpha) { - SurfaceControl dim = primary ? mTiles.mPrimaryDim : mTiles.mSecondaryDim; - if (alpha <= 0.001f) { - t.hide(dim); - } else { - t.setAlpha(dim, alpha); - t.show(dim); - } - } - - void resizeStackSurfaces(int position, int taskPosition, SnapTarget taskSnapTarget, - Transaction transaction) { - if (mRemoved) { - // This divider view has been removed so shouldn't have any additional influence. - return; - } - calculateBoundsForPosition(position, mDockSide, mDockedRect); - calculateBoundsForPosition(position, DockedDividerUtils.invertDockSide(mDockSide), - mOtherRect); - - if (mDockedRect.equals(mLastResizeRect) && !mEntranceAnimationRunning) { - return; - } - - // Make sure shadows are updated - if (mBackground.getZ() > 0f) { - mBackground.invalidate(); - } - - final boolean ownTransaction = transaction == null; - final Transaction t = ownTransaction ? mTiles.getTransaction() : transaction; - mLastResizeRect.set(mDockedRect); - if (mIsInMinimizeInteraction) { - calculateBoundsForPosition(mSnapTargetBeforeMinimized.position, mDockSide, - mDockedTaskRect); - calculateBoundsForPosition(mSnapTargetBeforeMinimized.position, - DockedDividerUtils.invertDockSide(mDockSide), mOtherTaskRect); - - // Move a right-docked-app to line up with the divider while dragging it - if (mDockSide == DOCKED_RIGHT) { - mDockedTaskRect.offset(Math.max(position, -mDividerSize) - - mDockedTaskRect.left + mDividerSize, 0); - } - resizeSplitSurfaces(t, mDockedRect, mDockedTaskRect, mOtherRect, mOtherTaskRect); - if (ownTransaction) { - t.setFrameTimelineVsync(Choreographer.getSfInstance().getVsyncId()); - t.apply(); - mTiles.releaseTransaction(t); - } - return; - } - - if (mEntranceAnimationRunning && taskPosition != TASK_POSITION_SAME) { - calculateBoundsForPosition(taskPosition, mDockSide, mDockedTaskRect); - - // Move a docked app if from the right in position with the divider up to insets - if (mDockSide == DOCKED_RIGHT) { - mDockedTaskRect.offset(Math.max(position, -mDividerSize) - - mDockedTaskRect.left + mDividerSize, 0); - } - calculateBoundsForPosition(taskPosition, DockedDividerUtils.invertDockSide(mDockSide), - mOtherTaskRect); - resizeSplitSurfaces(t, mDockedRect, mDockedTaskRect, mOtherRect, mOtherTaskRect); - } else if (mExitAnimationRunning && taskPosition != TASK_POSITION_SAME) { - calculateBoundsForPosition(taskPosition, mDockSide, mDockedTaskRect); - mDockedInsetRect.set(mDockedTaskRect); - calculateBoundsForPosition(mExitStartPosition, - DockedDividerUtils.invertDockSide(mDockSide), mOtherTaskRect); - mOtherInsetRect.set(mOtherTaskRect); - applyExitAnimationParallax(mOtherTaskRect, position); - - // Move a right-docked-app to line up with the divider while dragging it - if (mDockSide == DOCKED_RIGHT) { - mDockedTaskRect.offset(position + mDividerSize, 0); - } - resizeSplitSurfaces(t, mDockedRect, mDockedTaskRect, mOtherRect, mOtherTaskRect); - } else if (taskPosition != TASK_POSITION_SAME) { - calculateBoundsForPosition(position, DockedDividerUtils.invertDockSide(mDockSide), - mOtherRect); - int dockSideInverted = DockedDividerUtils.invertDockSide(mDockSide); - int taskPositionDocked = - restrictDismissingTaskPosition(taskPosition, mDockSide, taskSnapTarget); - int taskPositionOther = - restrictDismissingTaskPosition(taskPosition, dockSideInverted, taskSnapTarget); - calculateBoundsForPosition(taskPositionDocked, mDockSide, mDockedTaskRect); - calculateBoundsForPosition(taskPositionOther, dockSideInverted, mOtherTaskRect); - mTmpRect.set(0, 0, mSplitLayout.mDisplayLayout.width(), - mSplitLayout.mDisplayLayout.height()); - alignTopLeft(mDockedRect, mDockedTaskRect); - alignTopLeft(mOtherRect, mOtherTaskRect); - mDockedInsetRect.set(mDockedTaskRect); - mOtherInsetRect.set(mOtherTaskRect); - if (dockSideTopLeft(mDockSide)) { - alignTopLeft(mTmpRect, mDockedInsetRect); - alignBottomRight(mTmpRect, mOtherInsetRect); - } else { - alignBottomRight(mTmpRect, mDockedInsetRect); - alignTopLeft(mTmpRect, mOtherInsetRect); - } - applyDismissingParallax(mDockedTaskRect, mDockSide, taskSnapTarget, position, - taskPositionDocked); - applyDismissingParallax(mOtherTaskRect, dockSideInverted, taskSnapTarget, position, - taskPositionOther); - resizeSplitSurfaces(t, mDockedRect, mDockedTaskRect, mOtherRect, mOtherTaskRect); - } else { - resizeSplitSurfaces(t, mDockedRect, null, mOtherRect, null); - } - SnapTarget closestDismissTarget = getSnapAlgorithm().getClosestDismissTarget(position); - float dimFraction = getDimFraction(position, closestDismissTarget); - setResizeDimLayer(t, isDismissTargetPrimary(closestDismissTarget), dimFraction); - if (ownTransaction) { - t.apply(); - mTiles.releaseTransaction(t); - } - } - - private void applyExitAnimationParallax(Rect taskRect, int position) { - if (mDockSide == WindowManager.DOCKED_TOP) { - taskRect.offset(0, (int) ((position - mExitStartPosition) * 0.25f)); - } else if (mDockSide == WindowManager.DOCKED_LEFT) { - taskRect.offset((int) ((position - mExitStartPosition) * 0.25f), 0); - } else if (mDockSide == WindowManager.DOCKED_RIGHT) { - taskRect.offset((int) ((mExitStartPosition - position) * 0.25f), 0); - } - } - - private float getDimFraction(int position, SnapTarget dismissTarget) { - if (mEntranceAnimationRunning) { - return 0f; - } - float fraction = getSnapAlgorithm().calculateDismissingFraction(position); - fraction = Math.max(0, Math.min(fraction, 1f)); - fraction = DIM_INTERPOLATOR.getInterpolation(fraction); - return fraction; - } - - /** - * When the snap target is dismissing one side, make sure that the dismissing side doesn't get - * 0 size. - */ - private int restrictDismissingTaskPosition(int taskPosition, int dockSide, - SnapTarget snapTarget) { - if (snapTarget.flag == SnapTarget.FLAG_DISMISS_START && dockSideTopLeft(dockSide)) { - return Math.max(mSplitLayout.getSnapAlgorithm().getFirstSplitTarget().position, - mStartPosition); - } else if (snapTarget.flag == SnapTarget.FLAG_DISMISS_END - && dockSideBottomRight(dockSide)) { - return Math.min(mSplitLayout.getSnapAlgorithm().getLastSplitTarget().position, - mStartPosition); - } else { - return taskPosition; - } - } - - /** - * Applies a parallax to the task when dismissing. - */ - private void applyDismissingParallax(Rect taskRect, int dockSide, SnapTarget snapTarget, - int position, int taskPosition) { - float fraction = Math.min(1, Math.max(0, - mSplitLayout.getSnapAlgorithm().calculateDismissingFraction(position))); - SnapTarget dismissTarget = null; - SnapTarget splitTarget = null; - int start = 0; - if (position <= mSplitLayout.getSnapAlgorithm().getLastSplitTarget().position - && dockSideTopLeft(dockSide)) { - dismissTarget = mSplitLayout.getSnapAlgorithm().getDismissStartTarget(); - splitTarget = mSplitLayout.getSnapAlgorithm().getFirstSplitTarget(); - start = taskPosition; - } else if (position >= mSplitLayout.getSnapAlgorithm().getLastSplitTarget().position - && dockSideBottomRight(dockSide)) { - dismissTarget = mSplitLayout.getSnapAlgorithm().getDismissEndTarget(); - splitTarget = mSplitLayout.getSnapAlgorithm().getLastSplitTarget(); - start = splitTarget.position; - } - if (dismissTarget != null && fraction > 0f - && isDismissing(splitTarget, position, dockSide)) { - fraction = calculateParallaxDismissingFraction(fraction, dockSide); - int offsetPosition = (int) (start + fraction - * (dismissTarget.position - splitTarget.position)); - int width = taskRect.width(); - int height = taskRect.height(); - switch (dockSide) { - case WindowManager.DOCKED_LEFT: - taskRect.left = offsetPosition - width; - taskRect.right = offsetPosition; - break; - case WindowManager.DOCKED_RIGHT: - taskRect.left = offsetPosition + mDividerSize; - taskRect.right = offsetPosition + width + mDividerSize; - break; - case WindowManager.DOCKED_TOP: - taskRect.top = offsetPosition - height; - taskRect.bottom = offsetPosition; - break; - case WindowManager.DOCKED_BOTTOM: - taskRect.top = offsetPosition + mDividerSize; - taskRect.bottom = offsetPosition + height + mDividerSize; - break; - } - } - } - - /** - * @return for a specified {@code fraction}, this returns an adjusted value that simulates a - * slowing down parallax effect - */ - private static float calculateParallaxDismissingFraction(float fraction, int dockSide) { - float result = SLOWDOWN_INTERPOLATOR.getInterpolation(fraction) / 3.5f; - - // Less parallax at the top, just because. - if (dockSide == WindowManager.DOCKED_TOP) { - result /= 2f; - } - return result; - } - - private static boolean isDismissing(SnapTarget snapTarget, int position, int dockSide) { - if (dockSide == WindowManager.DOCKED_TOP || dockSide == WindowManager.DOCKED_LEFT) { - return position < snapTarget.position; - } else { - return position > snapTarget.position; - } - } - - private boolean isDismissTargetPrimary(SnapTarget dismissTarget) { - return (dismissTarget.flag == SnapTarget.FLAG_DISMISS_START && dockSideTopLeft(mDockSide)) - || (dismissTarget.flag == SnapTarget.FLAG_DISMISS_END - && dockSideBottomRight(mDockSide)); - } - - /** - * @return true if and only if {@code dockSide} is top or left - */ - private static boolean dockSideTopLeft(int dockSide) { - return dockSide == WindowManager.DOCKED_TOP || dockSide == WindowManager.DOCKED_LEFT; - } - - /** - * @return true if and only if {@code dockSide} is bottom or right - */ - private static boolean dockSideBottomRight(int dockSide) { - return dockSide == WindowManager.DOCKED_BOTTOM || dockSide == WindowManager.DOCKED_RIGHT; - } - - @Override - public void onComputeInternalInsets(InternalInsetsInfo inoutInfo) { - inoutInfo.setTouchableInsets(InternalInsetsInfo.TOUCHABLE_INSETS_REGION); - inoutInfo.touchableRegion.set(mHandle.getLeft(), mHandle.getTop(), mHandle.getRight(), - mHandle.getBottom()); - inoutInfo.touchableRegion.op(mBackground.getLeft(), mBackground.getTop(), - mBackground.getRight(), mBackground.getBottom(), Op.UNION); - } - - void onUndockingTask() { - int dockSide = mSplitLayout.getPrimarySplitSide(); - if (inSplitMode()) { - startDragging(false /* animate */, false /* touching */); - SnapTarget target = dockSideTopLeft(dockSide) - ? mSplitLayout.getSnapAlgorithm().getDismissEndTarget() - : mSplitLayout.getSnapAlgorithm().getDismissStartTarget(); - - // Don't start immediately - give a little bit time to settle the drag resize change. - mExitAnimationRunning = true; - mExitStartPosition = getCurrentPosition(); - stopDragging(mExitStartPosition, target, 336 /* duration */, 100 /* startDelay */, - 0 /* endDelay */, Interpolators.FAST_OUT_SLOW_IN); - } - } - - private int calculatePositionForInsetBounds() { - mSplitLayout.mDisplayLayout.getStableBounds(mTmpRect); - return DockedDividerUtils.calculatePositionForBounds(mTmpRect, mDockSide, mDividerSize); - } -} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/DividerWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/DividerWindowManager.java deleted file mode 100644 index 2c3ae68e4749..000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/DividerWindowManager.java +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES 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.legacysplitscreen; - -import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; -import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE; -import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL; -import static android.view.WindowManager.LayoutParams.FLAG_SLIPPERY; -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.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; -import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION; -import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY; -import static android.view.WindowManager.LayoutParams.TYPE_DOCK_DIVIDER; -import static android.view.WindowManager.SHELL_ROOT_LAYER_DIVIDER; - -import android.graphics.PixelFormat; -import android.graphics.Region; -import android.os.Binder; -import android.view.View; -import android.view.WindowManager; - -import com.android.wm.shell.common.SystemWindows; - -/** - * Manages the window parameters of the docked stack divider. - */ -final class DividerWindowManager { - - private static final String WINDOW_TITLE = "DockedStackDivider"; - - final SystemWindows mSystemWindows; - private WindowManager.LayoutParams mLp; - private View mView; - - DividerWindowManager(SystemWindows systemWindows) { - mSystemWindows = systemWindows; - } - - /** Add a divider view */ - void add(View view, int width, int height, int displayId) { - mLp = new WindowManager.LayoutParams( - width, height, TYPE_DOCK_DIVIDER, - FLAG_NOT_FOCUSABLE | FLAG_NOT_TOUCH_MODAL - | FLAG_WATCH_OUTSIDE_TOUCH | FLAG_SPLIT_TOUCH | FLAG_SLIPPERY, - PixelFormat.TRANSLUCENT); - mLp.token = new Binder(); - mLp.setTitle(WINDOW_TITLE); - mLp.privateFlags |= PRIVATE_FLAG_NO_MOVE_ANIMATION | PRIVATE_FLAG_TRUSTED_OVERLAY; - mLp.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; - view.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN - | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION - | View.SYSTEM_UI_FLAG_LAYOUT_STABLE); - mSystemWindows.addView(view, mLp, displayId, SHELL_ROOT_LAYER_DIVIDER); - mView = view; - } - - void remove() { - if (mView != null) { - mSystemWindows.removeView(mView); - } - mView = null; - } - - void setSlippery(boolean slippery) { - boolean changed = false; - if (slippery && (mLp.flags & FLAG_SLIPPERY) == 0) { - mLp.flags |= FLAG_SLIPPERY; - changed = true; - } else if (!slippery && (mLp.flags & FLAG_SLIPPERY) != 0) { - mLp.flags &= ~FLAG_SLIPPERY; - changed = true; - } - if (changed) { - mSystemWindows.updateViewLayout(mView, mLp); - } - } - - void setTouchable(boolean touchable) { - if (mView == null) { - return; - } - boolean changed = false; - if (!touchable && (mLp.flags & FLAG_NOT_TOUCHABLE) == 0) { - mLp.flags |= FLAG_NOT_TOUCHABLE; - changed = true; - } else if (touchable && (mLp.flags & FLAG_NOT_TOUCHABLE) != 0) { - mLp.flags &= ~FLAG_NOT_TOUCHABLE; - changed = true; - } - if (changed) { - mSystemWindows.updateViewLayout(mView, mLp); - } - } - - /** Sets the touch region to `touchRegion`. Use null to unset.*/ - void setTouchRegion(Region touchRegion) { - if (mView == null) { - return; - } - mSystemWindows.setTouchableRegion(mView, touchRegion); - } -} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/ForcedResizableInfoActivity.java b/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/ForcedResizableInfoActivity.java deleted file mode 100644 index 4fe28e630114..000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/ForcedResizableInfoActivity.java +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES 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.legacysplitscreen; - -import static android.app.ITaskStackListener.FORCED_RESIZEABLE_REASON_SECONDARY_DISPLAY; -import static android.app.ITaskStackListener.FORCED_RESIZEABLE_REASON_SPLIT_SCREEN; - -import android.annotation.Nullable; -import android.app.Activity; -import android.app.ActivityManager; -import android.os.Bundle; -import android.view.KeyEvent; -import android.view.MotionEvent; -import android.view.View; -import android.view.View.OnTouchListener; -import android.widget.TextView; - -import com.android.wm.shell.R; - -/** - * Translucent activity that gets started on top of a task in multi-window to inform the user that - * we forced the activity below to be resizable. - * - * Note: This activity runs on the main thread of the process hosting the Shell lib. - */ -public class ForcedResizableInfoActivity extends Activity implements OnTouchListener { - - public static final String EXTRA_FORCED_RESIZEABLE_REASON = "extra_forced_resizeable_reason"; - - private static final long DISMISS_DELAY = 2500; - - private final Runnable mFinishRunnable = new Runnable() { - @Override - public void run() { - finish(); - } - }; - - @Override - protected void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.forced_resizable_activity); - TextView tv = findViewById(com.android.internal.R.id.message); - int reason = getIntent().getIntExtra(EXTRA_FORCED_RESIZEABLE_REASON, -1); - String text; - switch (reason) { - case FORCED_RESIZEABLE_REASON_SPLIT_SCREEN: - text = getString(R.string.dock_forced_resizable); - break; - case FORCED_RESIZEABLE_REASON_SECONDARY_DISPLAY: - text = getString(R.string.forced_resizable_secondary_display); - break; - default: - throw new IllegalArgumentException("Unexpected forced resizeable reason: " - + reason); - } - tv.setText(text); - getWindow().setTitle(text); - getWindow().getDecorView().setOnTouchListener(this); - } - - @Override - protected void onStart() { - super.onStart(); - getWindow().getDecorView().postDelayed(mFinishRunnable, DISMISS_DELAY); - } - - @Override - protected void onStop() { - super.onStop(); - finish(); - } - - @Override - public boolean onTouch(View v, MotionEvent event) { - finish(); - return true; - } - - @Override - public boolean onKeyDown(int keyCode, KeyEvent event) { - finish(); - return true; - } - - @Override - public void finish() { - super.finish(); - overridePendingTransition(0, R.anim.forced_resizable_exit); - } - - @Override - public void setTaskDescription(ActivityManager.TaskDescription taskDescription) { - // Do nothing - } -} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/ForcedResizableInfoActivityController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/ForcedResizableInfoActivityController.java deleted file mode 100644 index 139544f951ce..000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/ForcedResizableInfoActivityController.java +++ /dev/null @@ -1,150 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES 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.legacysplitscreen; - - -import static com.android.wm.shell.legacysplitscreen.ForcedResizableInfoActivity.EXTRA_FORCED_RESIZEABLE_REASON; - -import android.app.ActivityOptions; -import android.content.Context; -import android.content.Intent; -import android.os.UserHandle; -import android.util.ArraySet; -import android.widget.Toast; - -import com.android.wm.shell.R; -import com.android.wm.shell.common.ShellExecutor; - -import java.util.function.Consumer; - -/** - * Controller that decides when to show the {@link ForcedResizableInfoActivity}. - */ -final class ForcedResizableInfoActivityController implements DividerView.DividerCallbacks { - - private static final String SELF_PACKAGE_NAME = "com.android.systemui"; - - private static final int TIMEOUT = 1000; - private final Context mContext; - private final ShellExecutor mMainExecutor; - private final ArraySet<PendingTaskRecord> mPendingTasks = new ArraySet<>(); - private final ArraySet<String> mPackagesShownInSession = new ArraySet<>(); - private boolean mDividerDragging; - - private final Runnable mTimeoutRunnable = this::showPending; - - private final Consumer<Boolean> mDockedStackExistsListener = exists -> { - if (!exists) { - mPackagesShownInSession.clear(); - } - }; - - /** Record of force resized task that's pending to be handled. */ - private class PendingTaskRecord { - int mTaskId; - /** - * {@link android.app.ITaskStackListener#FORCED_RESIZEABLE_REASON_SPLIT_SCREEN} or - * {@link android.app.ITaskStackListener#FORCED_RESIZEABLE_REASON_SECONDARY_DISPLAY} - */ - int mReason; - - PendingTaskRecord(int taskId, int reason) { - this.mTaskId = taskId; - this.mReason = reason; - } - } - - ForcedResizableInfoActivityController(Context context, - LegacySplitScreenController splitScreenController, - ShellExecutor mainExecutor) { - mContext = context; - mMainExecutor = mainExecutor; - splitScreenController.registerInSplitScreenListener(mDockedStackExistsListener); - } - - @Override - public void onDraggingStart() { - mDividerDragging = true; - mMainExecutor.removeCallbacks(mTimeoutRunnable); - } - - @Override - public void onDraggingEnd() { - mDividerDragging = false; - showPending(); - } - - void onAppTransitionFinished() { - if (!mDividerDragging) { - showPending(); - } - } - - void activityForcedResizable(String packageName, int taskId, int reason) { - if (debounce(packageName)) { - return; - } - mPendingTasks.add(new PendingTaskRecord(taskId, reason)); - postTimeout(); - } - - void activityDismissingSplitScreen() { - Toast.makeText(mContext, R.string.dock_non_resizeble_failed_to_dock_text, - Toast.LENGTH_SHORT).show(); - } - - void activityLaunchOnSecondaryDisplayFailed() { - Toast.makeText(mContext, R.string.activity_launch_on_secondary_display_failed_text, - Toast.LENGTH_SHORT).show(); - } - - private void showPending() { - mMainExecutor.removeCallbacks(mTimeoutRunnable); - for (int i = mPendingTasks.size() - 1; i >= 0; i--) { - PendingTaskRecord pendingRecord = mPendingTasks.valueAt(i); - Intent intent = new Intent(mContext, ForcedResizableInfoActivity.class); - ActivityOptions options = ActivityOptions.makeBasic(); - options.setLaunchTaskId(pendingRecord.mTaskId); - // Set as task overlay and allow to resume, so that when an app enters split-screen and - // becomes paused, the overlay will still be shown. - options.setTaskOverlay(true, true /* canResume */); - intent.putExtra(EXTRA_FORCED_RESIZEABLE_REASON, pendingRecord.mReason); - mContext.startActivityAsUser(intent, options.toBundle(), UserHandle.CURRENT); - } - mPendingTasks.clear(); - } - - private void postTimeout() { - mMainExecutor.removeCallbacks(mTimeoutRunnable); - mMainExecutor.executeDelayed(mTimeoutRunnable, TIMEOUT); - } - - private boolean debounce(String packageName) { - if (packageName == null) { - return false; - } - - // We launch ForcedResizableInfoActivity into a task that was forced resizable, so that - // triggers another notification. So ignore our own activity. - if (SELF_PACKAGE_NAME.equals(packageName)) { - return true; - } - boolean debounce = mPackagesShownInSession.contains(packageName); - mPackagesShownInSession.add(packageName); - return debounce; - } -} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitDisplayLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitDisplayLayout.java deleted file mode 100644 index f201634d3d4a..000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitDisplayLayout.java +++ /dev/null @@ -1,326 +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.legacysplitscreen; - -import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; -import static android.content.res.Configuration.ORIENTATION_PORTRAIT; -import static android.util.RotationUtils.rotateBounds; -import static android.view.WindowManager.DOCKED_BOTTOM; -import static android.view.WindowManager.DOCKED_INVALID; -import static android.view.WindowManager.DOCKED_LEFT; -import static android.view.WindowManager.DOCKED_RIGHT; -import static android.view.WindowManager.DOCKED_TOP; - -import android.annotation.NonNull; -import android.content.Context; -import android.content.res.Configuration; -import android.content.res.Resources; -import android.graphics.Rect; -import android.util.TypedValue; -import android.window.WindowContainerTransaction; - -import com.android.internal.policy.DividerSnapAlgorithm; -import com.android.internal.policy.DockedDividerUtils; -import com.android.wm.shell.common.DisplayLayout; - -/** - * Handles split-screen related internal display layout. In general, this represents the - * WM-facing understanding of the splits. - */ -public class LegacySplitDisplayLayout { - /** Minimum size of an adjusted stack bounds relative to original stack bounds. Used to - * restrict IME adjustment so that a min portion of top stack remains visible.*/ - private static final float ADJUSTED_STACK_FRACTION_MIN = 0.3f; - - private static final int DIVIDER_WIDTH_INACTIVE_DP = 4; - - LegacySplitScreenTaskListener mTiles; - DisplayLayout mDisplayLayout; - Context mContext; - - // Lazy stuff - boolean mResourcesValid = false; - int mDividerSize; - int mDividerSizeInactive; - private DividerSnapAlgorithm mSnapAlgorithm = null; - private DividerSnapAlgorithm mMinimizedSnapAlgorithm = null; - Rect mPrimary = null; - Rect mSecondary = null; - Rect mAdjustedPrimary = null; - Rect mAdjustedSecondary = null; - final Rect mTmpBounds = new Rect(); - - public LegacySplitDisplayLayout(Context ctx, DisplayLayout dl, - LegacySplitScreenTaskListener taskTiles) { - mTiles = taskTiles; - mDisplayLayout = dl; - mContext = ctx; - } - - void rotateTo(int newRotation) { - mDisplayLayout.rotateTo(mContext.getResources(), newRotation); - final Configuration config = new Configuration(); - config.unset(); - config.orientation = mDisplayLayout.getOrientation(); - Rect tmpRect = new Rect(0, 0, mDisplayLayout.width(), mDisplayLayout.height()); - tmpRect.inset(mDisplayLayout.nonDecorInsets()); - config.windowConfiguration.setAppBounds(tmpRect); - tmpRect.set(0, 0, mDisplayLayout.width(), mDisplayLayout.height()); - tmpRect.inset(mDisplayLayout.stableInsets()); - config.screenWidthDp = (int) (tmpRect.width() / mDisplayLayout.density()); - config.screenHeightDp = (int) (tmpRect.height() / mDisplayLayout.density()); - mContext = mContext.createConfigurationContext(config); - mSnapAlgorithm = null; - mMinimizedSnapAlgorithm = null; - mResourcesValid = false; - } - - private void updateResources() { - if (mResourcesValid) { - return; - } - mResourcesValid = true; - Resources res = mContext.getResources(); - mDividerSize = DockedDividerUtils.getDividerSize(res, - DockedDividerUtils.getDividerInsets(res)); - mDividerSizeInactive = (int) TypedValue.applyDimension( - TypedValue.COMPLEX_UNIT_DIP, DIVIDER_WIDTH_INACTIVE_DP, res.getDisplayMetrics()); - } - - int getPrimarySplitSide() { - switch (mDisplayLayout.getNavigationBarPosition(mContext.getResources())) { - case DisplayLayout.NAV_BAR_BOTTOM: - return mDisplayLayout.isLandscape() ? DOCKED_LEFT : DOCKED_TOP; - case DisplayLayout.NAV_BAR_LEFT: - return DOCKED_RIGHT; - case DisplayLayout.NAV_BAR_RIGHT: - return DOCKED_LEFT; - default: - return DOCKED_INVALID; - } - } - - DividerSnapAlgorithm getSnapAlgorithm() { - if (mSnapAlgorithm == null) { - updateResources(); - boolean isHorizontalDivision = !mDisplayLayout.isLandscape(); - mSnapAlgorithm = new DividerSnapAlgorithm(mContext.getResources(), - mDisplayLayout.width(), mDisplayLayout.height(), mDividerSize, - isHorizontalDivision, mDisplayLayout.stableInsets(), getPrimarySplitSide()); - } - return mSnapAlgorithm; - } - - DividerSnapAlgorithm getMinimizedSnapAlgorithm(boolean homeStackResizable) { - if (mMinimizedSnapAlgorithm == null) { - updateResources(); - boolean isHorizontalDivision = !mDisplayLayout.isLandscape(); - mMinimizedSnapAlgorithm = new DividerSnapAlgorithm(mContext.getResources(), - mDisplayLayout.width(), mDisplayLayout.height(), mDividerSize, - isHorizontalDivision, mDisplayLayout.stableInsets(), getPrimarySplitSide(), - true /* isMinimized */, homeStackResizable); - } - return mMinimizedSnapAlgorithm; - } - - /** - * Resize primary bounds and secondary bounds by divider position. - * - * @param position divider position. - * @return true if calculated bounds changed. - */ - boolean resizeSplits(int position) { - mPrimary = mPrimary == null ? new Rect() : mPrimary; - mSecondary = mSecondary == null ? new Rect() : mSecondary; - int dockSide = getPrimarySplitSide(); - boolean boundsChanged; - - mTmpBounds.set(mPrimary); - DockedDividerUtils.calculateBoundsForPosition(position, dockSide, mPrimary, - mDisplayLayout.width(), mDisplayLayout.height(), mDividerSize); - boundsChanged = !mPrimary.equals(mTmpBounds); - - mTmpBounds.set(mSecondary); - DockedDividerUtils.calculateBoundsForPosition(position, - DockedDividerUtils.invertDockSide(dockSide), mSecondary, mDisplayLayout.width(), - mDisplayLayout.height(), mDividerSize); - boundsChanged |= !mSecondary.equals(mTmpBounds); - return boundsChanged; - } - - void resizeSplits(int position, WindowContainerTransaction t) { - if (resizeSplits(position)) { - t.setBounds(mTiles.mPrimary.token, mPrimary); - t.setBounds(mTiles.mSecondary.token, mSecondary); - - t.setSmallestScreenWidthDp(mTiles.mPrimary.token, - getSmallestWidthDpForBounds(mContext, mDisplayLayout, mPrimary)); - t.setSmallestScreenWidthDp(mTiles.mSecondary.token, - getSmallestWidthDpForBounds(mContext, mDisplayLayout, mSecondary)); - } - } - - Rect calcResizableMinimizedHomeStackBounds() { - DividerSnapAlgorithm.SnapTarget miniMid = - getMinimizedSnapAlgorithm(true /* resizable */).getMiddleTarget(); - Rect homeBounds = new Rect(); - DockedDividerUtils.calculateBoundsForPosition(miniMid.position, - DockedDividerUtils.invertDockSide(getPrimarySplitSide()), homeBounds, - mDisplayLayout.width(), mDisplayLayout.height(), mDividerSize); - return homeBounds; - } - - /** - * Updates the adjustment depending on it's current state. - */ - void updateAdjustedBounds(int currImeTop, int hiddenTop, int shownTop) { - adjustForIME(mDisplayLayout, currImeTop, hiddenTop, shownTop, mDividerSize, - mDividerSizeInactive, mPrimary, mSecondary); - } - - /** Assumes top/bottom split. Splits are not adjusted for left/right splits. */ - private void adjustForIME(DisplayLayout dl, int currImeTop, int hiddenTop, int shownTop, - int dividerWidth, int dividerWidthInactive, Rect primaryBounds, Rect secondaryBounds) { - if (mAdjustedPrimary == null) { - mAdjustedPrimary = new Rect(); - mAdjustedSecondary = new Rect(); - } - - final Rect displayStableRect = new Rect(); - dl.getStableBounds(displayStableRect); - - final float shownFraction = ((float) (currImeTop - hiddenTop)) / (shownTop - hiddenTop); - final int currDividerWidth = - (int) (dividerWidthInactive * shownFraction + dividerWidth * (1.f - shownFraction)); - - // Calculate the highest we can move the bottom of the top stack to keep 30% visible. - final int minTopStackBottom = displayStableRect.top - + (int) ((mPrimary.bottom - displayStableRect.top) * ADJUSTED_STACK_FRACTION_MIN); - // Based on that, calculate the maximum amount we'll allow the ime to shift things. - final int maxOffset = mPrimary.bottom - minTopStackBottom; - // Calculate how much we would shift things without limits (basically the height of ime). - final int desiredOffset = hiddenTop - shownTop; - // Calculate an "adjustedTop" which is the currImeTop but restricted by our constraints. - // We want an effect where the adjustment only occurs during the "highest" portion of the - // ime animation. This is done by shifting the adjustment values by the difference in - // offsets (effectively playing the whole adjustment animation some fixed amount of pixels - // below the ime top). - final int topCorrection = Math.max(0, desiredOffset - maxOffset); - final int adjustedTop = currImeTop + topCorrection; - // The actual yOffset is the distance between adjustedTop and the bottom of the display. - // Since our adjustedTop values are playing "below" the ime, we clamp at 0 so we only - // see adjustment upward. - final int yOffset = Math.max(0, dl.height() - adjustedTop); - - // TOP - // Reduce the offset by an additional small amount to squish the divider bar. - mAdjustedPrimary.set(primaryBounds); - mAdjustedPrimary.offset(0, -yOffset + (dividerWidth - currDividerWidth)); - - // BOTTOM - mAdjustedSecondary.set(secondaryBounds); - mAdjustedSecondary.offset(0, -yOffset); - } - - static int getSmallestWidthDpForBounds(@NonNull Context context, DisplayLayout dl, - Rect bounds) { - int dividerSize = DockedDividerUtils.getDividerSize(context.getResources(), - DockedDividerUtils.getDividerInsets(context.getResources())); - - int minWidth = Integer.MAX_VALUE; - - // Go through all screen orientations and find the orientation in which the task has the - // smallest width. - Rect tmpRect = new Rect(); - Rect rotatedDisplayRect = new Rect(); - Rect displayRect = new Rect(0, 0, dl.width(), dl.height()); - - DisplayLayout tmpDL = new DisplayLayout(); - for (int rotation = 0; rotation < 4; rotation++) { - tmpDL.set(dl); - tmpDL.rotateTo(context.getResources(), rotation); - DividerSnapAlgorithm snap = initSnapAlgorithmForRotation(context, tmpDL, dividerSize); - - tmpRect.set(bounds); - rotateBounds(tmpRect, displayRect, dl.rotation(), rotation); - rotatedDisplayRect.set(0, 0, tmpDL.width(), tmpDL.height()); - final int dockSide = getPrimarySplitSide(tmpRect, rotatedDisplayRect, - tmpDL.getOrientation()); - final int position = DockedDividerUtils.calculatePositionForBounds(tmpRect, dockSide, - dividerSize); - - final int snappedPosition = - snap.calculateNonDismissingSnapTarget(position).position; - DockedDividerUtils.calculateBoundsForPosition(snappedPosition, dockSide, tmpRect, - tmpDL.width(), tmpDL.height(), dividerSize); - Rect insettedDisplay = new Rect(rotatedDisplayRect); - insettedDisplay.inset(tmpDL.stableInsets()); - tmpRect.intersect(insettedDisplay); - minWidth = Math.min(tmpRect.width(), minWidth); - } - return (int) (minWidth / dl.density()); - } - - static DividerSnapAlgorithm initSnapAlgorithmForRotation(Context context, DisplayLayout dl, - int dividerSize) { - final Configuration config = new Configuration(); - config.unset(); - config.orientation = dl.getOrientation(); - Rect tmpRect = new Rect(0, 0, dl.width(), dl.height()); - tmpRect.inset(dl.nonDecorInsets()); - config.windowConfiguration.setAppBounds(tmpRect); - tmpRect.set(0, 0, dl.width(), dl.height()); - tmpRect.inset(dl.stableInsets()); - config.screenWidthDp = (int) (tmpRect.width() / dl.density()); - config.screenHeightDp = (int) (tmpRect.height() / dl.density()); - final Context rotationContext = context.createConfigurationContext(config); - return new DividerSnapAlgorithm( - rotationContext.getResources(), dl.width(), dl.height(), dividerSize, - config.orientation == ORIENTATION_PORTRAIT, dl.stableInsets()); - } - - /** - * Get the current primary-split side. Determined by its location of {@param bounds} within - * {@param displayRect} but if both are the same, it will try to dock to each side and determine - * if allowed in its respected {@param orientation}. - * - * @param bounds bounds of the primary split task to get which side is docked - * @param displayRect bounds of the display that contains the primary split task - * @param orientation the origination of device - * @return current primary-split side - */ - static int getPrimarySplitSide(Rect bounds, Rect displayRect, int orientation) { - if (orientation == ORIENTATION_PORTRAIT) { - // Portrait mode, docked either at the top or the bottom. - final int diff = (displayRect.bottom - bounds.bottom) - (bounds.top - displayRect.top); - if (diff < 0) { - return DOCKED_BOTTOM; - } else { - // Top is default - return DOCKED_TOP; - } - } else if (orientation == ORIENTATION_LANDSCAPE) { - // Landscape mode, docked either on the left or on the right. - final int diff = (displayRect.right - bounds.right) - (bounds.left - displayRect.left); - if (diff < 0) { - return DOCKED_RIGHT; - } - return DOCKED_LEFT; - } - return DOCKED_INVALID; - } -} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitScreen.java b/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitScreen.java deleted file mode 100644 index 499a9c5fa631..000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitScreen.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.legacysplitscreen; - -import android.graphics.Rect; -import android.window.WindowContainerToken; - -import com.android.wm.shell.common.annotations.ExternalThread; - -import java.io.PrintWriter; -import java.util.function.BiConsumer; -import java.util.function.Consumer; - -/** - * Interface to engage split screen feature. - */ -@ExternalThread -public interface LegacySplitScreen { - /** Called when keyguard showing state changed. */ - void onKeyguardVisibilityChanged(boolean isShowing); - - /** Returns {@link DividerView}. */ - DividerView getDividerView(); - - /** Returns {@code true} if one of the split screen is in minimized mode. */ - boolean isMinimized(); - - /** Returns {@code true} if the home stack is resizable. */ - boolean isHomeStackResizable(); - - /** Returns {@code true} if the divider is visible. */ - boolean isDividerVisible(); - - /** Switch to minimized state if appropriate. */ - void setMinimized(boolean minimized); - - /** Called when there's a task undocking. */ - void onUndockingTask(); - - /** Called when app transition finished. */ - void onAppTransitionFinished(); - - /** Dumps current status of Split Screen. */ - void dump(PrintWriter pw); - - /** Registers listener that gets called whenever the existence of the divider changes. */ - void registerInSplitScreenListener(Consumer<Boolean> listener); - - /** Unregisters listener that gets called whenever the existence of the divider changes. */ - void unregisterInSplitScreenListener(Consumer<Boolean> listener); - - /** Registers listener that gets called whenever the split screen bounds changes. */ - void registerBoundsChangeListener(BiConsumer<Rect, Rect> listener); - - /** @return the container token for the secondary split root task. */ - WindowContainerToken getSecondaryRoot(); - - /** - * Splits the primary task if feasible, this is to preserve legacy way to toggle split screen. - * Like triggering split screen through long pressing recents app button or through - * {@link android.accessibilityservice.AccessibilityService#GLOBAL_ACTION_TOGGLE_SPLIT_SCREEN}. - * - * @return {@code true} if it successes to split the primary task. - */ - boolean splitPrimaryTask(); - - /** - * Exits the split to make the primary task fullscreen. - */ - void dismissSplitToPrimaryTask(); -} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitScreenController.java deleted file mode 100644 index 67e487de0993..000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitScreenController.java +++ /dev/null @@ -1,762 +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.legacysplitscreen; - -import static android.app.ActivityManager.LOCK_TASK_MODE_PINNED; -import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; -import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS; -import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED; -import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY; -import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; -import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; -import static android.view.Display.DEFAULT_DISPLAY; - -import android.animation.AnimationHandler; -import android.app.ActivityManager; -import android.app.ActivityManager.RunningTaskInfo; -import android.app.ActivityTaskManager; -import android.content.Context; -import android.content.res.Configuration; -import android.graphics.Rect; -import android.os.RemoteException; -import android.provider.Settings; -import android.util.Slog; -import android.view.LayoutInflater; -import android.view.View; -import android.widget.Toast; -import android.window.TaskOrganizer; -import android.window.WindowContainerToken; -import android.window.WindowContainerTransaction; - -import com.android.internal.policy.DividerSnapAlgorithm; -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.DisplayImeController; -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.common.SystemWindows; -import com.android.wm.shell.common.TaskStackListenerCallback; -import com.android.wm.shell.common.TaskStackListenerImpl; -import com.android.wm.shell.common.TransactionPool; -import com.android.wm.shell.transition.Transitions; - -import java.io.PrintWriter; -import java.lang.ref.WeakReference; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.function.BiConsumer; -import java.util.function.Consumer; - -/** - * Controls split screen feature. - */ -public class LegacySplitScreenController implements DisplayController.OnDisplaysChangedListener { - static final boolean DEBUG = false; - - private static final String TAG = "SplitScreenCtrl"; - private static final int DEFAULT_APP_TRANSITION_DURATION = 336; - - private final Context mContext; - private final DisplayChangeController.OnDisplayChangingListener mRotationController; - private final DisplayController mDisplayController; - private final DisplayImeController mImeController; - private final DividerImeController mImePositionProcessor; - private final DividerState mDividerState = new DividerState(); - private final ForcedResizableInfoActivityController mForcedResizableController; - private final ShellExecutor mMainExecutor; - private final AnimationHandler mSfVsyncAnimationHandler; - private final LegacySplitScreenTaskListener mSplits; - private final SystemWindows mSystemWindows; - final TransactionPool mTransactionPool; - private final WindowManagerProxy mWindowManagerProxy; - private final TaskOrganizer mTaskOrganizer; - private final SplitScreenImpl mImpl = new SplitScreenImpl(); - - private final CopyOnWriteArrayList<WeakReference<Consumer<Boolean>>> mDockedStackExistsListeners - = new CopyOnWriteArrayList<>(); - private final ArrayList<WeakReference<BiConsumer<Rect, Rect>>> mBoundsChangedListeners = - new ArrayList<>(); - - - private DividerWindowManager mWindowManager; - private DividerView mView; - - // Keeps track of real-time split geometry including snap positions and ime adjustments - private LegacySplitDisplayLayout mSplitLayout; - - // Transient: this contains the layout calculated for a new rotation requested by WM. This is - // kept around so that we can wait for a matching configuration change and then use the exact - // layout that we sent back to WM. - private LegacySplitDisplayLayout mRotateSplitLayout; - - private boolean mIsKeyguardShowing; - private boolean mVisible = false; - private volatile boolean mMinimized = false; - private volatile boolean mAdjustedForIme = false; - private boolean mHomeStackResizable = false; - - public LegacySplitScreenController(Context context, - DisplayController displayController, SystemWindows systemWindows, - DisplayImeController imeController, TransactionPool transactionPool, - ShellTaskOrganizer shellTaskOrganizer, SyncTransactionQueue syncQueue, - TaskStackListenerImpl taskStackListener, Transitions transitions, - ShellExecutor mainExecutor, AnimationHandler sfVsyncAnimationHandler) { - mContext = context; - mDisplayController = displayController; - mSystemWindows = systemWindows; - mImeController = imeController; - mMainExecutor = mainExecutor; - mSfVsyncAnimationHandler = sfVsyncAnimationHandler; - mForcedResizableController = new ForcedResizableInfoActivityController(context, this, - mainExecutor); - mTransactionPool = transactionPool; - mWindowManagerProxy = new WindowManagerProxy(syncQueue, shellTaskOrganizer); - mTaskOrganizer = shellTaskOrganizer; - mSplits = new LegacySplitScreenTaskListener(this, shellTaskOrganizer, transitions, - syncQueue); - mImePositionProcessor = new DividerImeController(mSplits, mTransactionPool, mMainExecutor, - shellTaskOrganizer); - mRotationController = - (display, fromRotation, toRotation, wct) -> { - if (!mSplits.isSplitScreenSupported() || mWindowManagerProxy == null) { - return; - } - WindowContainerTransaction t = new WindowContainerTransaction(); - DisplayLayout displayLayout = - new DisplayLayout(mDisplayController.getDisplayLayout(display)); - LegacySplitDisplayLayout sdl = - new LegacySplitDisplayLayout(mContext, displayLayout, mSplits); - sdl.rotateTo(toRotation); - mRotateSplitLayout = sdl; - // snap resets to middle target when not minimized and rotation changed. - final int position = mMinimized ? mView.mSnapTargetBeforeMinimized.position - : sdl.getSnapAlgorithm().getMiddleTarget().position; - DividerSnapAlgorithm snap = sdl.getSnapAlgorithm(); - final DividerSnapAlgorithm.SnapTarget target = - snap.calculateNonDismissingSnapTarget(position); - sdl.resizeSplits(target.position, t); - - if (isSplitActive() && mHomeStackResizable) { - mWindowManagerProxy - .applyHomeTasksMinimized(sdl, mSplits.mSecondary.token, t); - } - if (mWindowManagerProxy.queueSyncTransactionIfWaiting(t)) { - // Because sync transactions are serialized, its possible for an "older" - // bounds-change to get applied after a screen rotation. In that case, we - // want to actually defer on that rather than apply immediately. Of course, - // this means that the bounds may not change until after the rotation so - // the user might see some artifacts. This should be rare. - Slog.w(TAG, "Screen rotated while other operations were pending, this may" - + " result in some graphical artifacts."); - } else { - wct.merge(t, true /* transfer */); - } - }; - - mWindowManager = new DividerWindowManager(mSystemWindows); - - // No need to listen to display window container or create root tasks if the device is not - // using legacy split screen. - if (!context.getResources().getBoolean(com.android.internal.R.bool.config_useLegacySplit)) { - return; - } - - - mDisplayController.addDisplayWindowListener(this); - // Don't initialize the divider or anything until we get the default display. - - taskStackListener.addListener( - new TaskStackListenerCallback() { - @Override - public void onActivityRestartAttempt(ActivityManager.RunningTaskInfo task, - boolean homeTaskVisible, boolean clearedTask, boolean wasVisible) { - if (!wasVisible || task.getWindowingMode() - != WINDOWING_MODE_SPLIT_SCREEN_PRIMARY - || !mSplits.isSplitScreenSupported()) { - return; - } - - if (isMinimized()) { - onUndockingTask(); - } - } - - @Override - public void onActivityForcedResizable(String packageName, int taskId, - int reason) { - mForcedResizableController.activityForcedResizable(packageName, taskId, - reason); - } - - @Override - public void onActivityDismissingDockedStack() { - mForcedResizableController.activityDismissingSplitScreen(); - } - - @Override - public void onActivityLaunchOnSecondaryDisplayFailed() { - mForcedResizableController.activityLaunchOnSecondaryDisplayFailed(); - } - }); - } - - public LegacySplitScreen asLegacySplitScreen() { - return mImpl; - } - - public void onSplitScreenSupported() { - // Set starting tile bounds based on middle target - final WindowContainerTransaction tct = new WindowContainerTransaction(); - int midPos = mSplitLayout.getSnapAlgorithm().getMiddleTarget().position; - mSplitLayout.resizeSplits(midPos, tct); - mTaskOrganizer.applyTransaction(tct); - } - - public void onKeyguardVisibilityChanged(boolean showing) { - if (!isSplitActive() || mView == null) { - return; - } - mView.setHidden(showing); - mIsKeyguardShowing = showing; - } - - @Override - public void onDisplayAdded(int displayId) { - if (displayId != DEFAULT_DISPLAY) { - return; - } - mSplitLayout = new LegacySplitDisplayLayout(mDisplayController.getDisplayContext(displayId), - mDisplayController.getDisplayLayout(displayId), mSplits); - mImeController.addPositionProcessor(mImePositionProcessor); - mDisplayController.addDisplayChangingController(mRotationController); - if (!ActivityTaskManager.supportsSplitScreenMultiWindow(mContext)) { - removeDivider(); - return; - } - try { - mSplits.init(); - } catch (Exception e) { - Slog.e(TAG, "Failed to register docked stack listener", e); - removeDivider(); - return; - } - } - - @Override - public void onDisplayConfigurationChanged(int displayId, Configuration newConfig) { - if (displayId != DEFAULT_DISPLAY || !mSplits.isSplitScreenSupported()) { - return; - } - mSplitLayout = new LegacySplitDisplayLayout(mDisplayController.getDisplayContext(displayId), - mDisplayController.getDisplayLayout(displayId), mSplits); - if (mRotateSplitLayout == null) { - int midPos = mSplitLayout.getSnapAlgorithm().getMiddleTarget().position; - final WindowContainerTransaction tct = new WindowContainerTransaction(); - mSplitLayout.resizeSplits(midPos, tct); - mTaskOrganizer.applyTransaction(tct); - } else if (mSplitLayout.mDisplayLayout.rotation() - == mRotateSplitLayout.mDisplayLayout.rotation()) { - mSplitLayout.mPrimary = new Rect(mRotateSplitLayout.mPrimary); - mSplitLayout.mSecondary = new Rect(mRotateSplitLayout.mSecondary); - mRotateSplitLayout = null; - } - if (isSplitActive()) { - update(newConfig); - } - } - - public boolean isMinimized() { - return mMinimized; - } - - public boolean isHomeStackResizable() { - return mHomeStackResizable; - } - - public DividerView getDividerView() { - return mView; - } - - public boolean isDividerVisible() { - return mView != null && mView.getVisibility() == View.VISIBLE; - } - - /** - * This indicates that at-least one of the splits has content. This differs from - * isDividerVisible because the divider is only visible once *everything* is in split mode - * while this only cares if some things are (eg. while entering/exiting as well). - */ - public boolean isSplitActive() { - return mSplits.mPrimary != null && mSplits.mSecondary != null - && (mSplits.mPrimary.topActivityType != ACTIVITY_TYPE_UNDEFINED - || mSplits.mSecondary.topActivityType != ACTIVITY_TYPE_UNDEFINED); - } - - public void addDivider(Configuration configuration) { - Context dctx = mDisplayController.getDisplayContext(mContext.getDisplayId()); - mView = (DividerView) - LayoutInflater.from(dctx).inflate(R.layout.docked_stack_divider, null); - mView.setAnimationHandler(mSfVsyncAnimationHandler); - DisplayLayout displayLayout = mDisplayController.getDisplayLayout(mContext.getDisplayId()); - mView.injectDependencies(this, mWindowManager, mDividerState, mForcedResizableController, - mSplits, mSplitLayout, mImePositionProcessor, mWindowManagerProxy); - mView.setVisibility(mVisible ? View.VISIBLE : View.INVISIBLE); - mView.setMinimizedDockStack(mMinimized, mHomeStackResizable, null /* transaction */); - final int size = dctx.getResources().getDimensionPixelSize( - com.android.internal.R.dimen.docked_stack_divider_thickness); - final boolean landscape = configuration.orientation == ORIENTATION_LANDSCAPE; - final int width = landscape ? size : displayLayout.width(); - final int height = landscape ? displayLayout.height() : size; - mWindowManager.add(mView, width, height, mContext.getDisplayId()); - } - - public void removeDivider() { - if (mView != null) { - mView.onDividerRemoved(); - } - mWindowManager.remove(); - } - - public void update(Configuration configuration) { - final boolean isDividerHidden = mView != null && mIsKeyguardShowing; - - removeDivider(); - addDivider(configuration); - - if (mMinimized) { - mView.setMinimizedDockStack(true, mHomeStackResizable, null /* transaction */); - updateTouchable(); - } - mView.setHidden(isDividerHidden); - } - - public void onTaskVanished() { - removeDivider(); - } - - public void updateVisibility(final boolean visible) { - if (DEBUG) Slog.d(TAG, "Updating visibility " + mVisible + "->" + visible); - if (mVisible != visible) { - mVisible = visible; - mView.setVisibility(visible ? View.VISIBLE : View.INVISIBLE); - - if (visible) { - mView.enterSplitMode(mHomeStackResizable); - // Update state because animations won't finish. - mWindowManagerProxy.runInSync( - t -> mView.setMinimizedDockStack(mMinimized, mHomeStackResizable, t)); - - } else { - mView.exitSplitMode(); - mWindowManagerProxy.runInSync( - t -> mView.setMinimizedDockStack(false, mHomeStackResizable, t)); - } - // Notify existence listeners - synchronized (mDockedStackExistsListeners) { - mDockedStackExistsListeners.removeIf(wf -> { - Consumer<Boolean> l = wf.get(); - if (l != null) l.accept(visible); - return l == null; - }); - } - } - } - - public void setMinimized(final boolean minimized) { - if (DEBUG) Slog.d(TAG, "posting ext setMinimized " + minimized + " vis:" + mVisible); - mMainExecutor.execute(() -> { - if (DEBUG) Slog.d(TAG, "run posted ext setMinimized " + minimized + " vis:" + mVisible); - if (!mVisible) { - return; - } - setHomeMinimized(minimized); - }); - } - - public void setHomeMinimized(final boolean minimized) { - if (DEBUG) { - Slog.d(TAG, "setHomeMinimized min:" + mMinimized + "->" + minimized + " hrsz:" - + mHomeStackResizable + " split:" + isDividerVisible()); - } - WindowContainerTransaction wct = new WindowContainerTransaction(); - final boolean minimizedChanged = mMinimized != minimized; - // Update minimized state - if (minimizedChanged) { - mMinimized = minimized; - } - // Always set this because we could be entering split when mMinimized is already true - wct.setFocusable(mSplits.mPrimary.token, !mMinimized); - - // Sync state to DividerView if it exists. - if (mView != null) { - final int displayId = mView.getDisplay() != null - ? mView.getDisplay().getDisplayId() : DEFAULT_DISPLAY; - // pause ime here (before updateMinimizedDockedStack) - if (mMinimized) { - mImePositionProcessor.pause(displayId); - } - if (minimizedChanged) { - // This conflicts with IME adjustment, so only call it when things change. - mView.setMinimizedDockStack(minimized, getAnimDuration(), mHomeStackResizable); - } - if (!mMinimized) { - // afterwards so it can end any animations started in view - mImePositionProcessor.resume(displayId); - } - } - updateTouchable(); - - // If we are only setting focusability, a sync transaction isn't necessary (in fact it - // can interrupt other animations), so see if it can be submitted on pending instead. - if (!mWindowManagerProxy.queueSyncTransactionIfWaiting(wct)) { - mTaskOrganizer.applyTransaction(wct); - } - } - - public void setAdjustedForIme(boolean adjustedForIme) { - if (mAdjustedForIme == adjustedForIme) { - return; - } - mAdjustedForIme = adjustedForIme; - updateTouchable(); - } - - public void updateTouchable() { - mWindowManager.setTouchable(!mAdjustedForIme); - } - - public void onUndockingTask() { - if (mView != null) { - mView.onUndockingTask(); - } - } - - public void onAppTransitionFinished() { - if (mView == null) { - return; - } - mForcedResizableController.onAppTransitionFinished(); - } - - public void dump(PrintWriter pw) { - pw.print(" mVisible="); pw.println(mVisible); - pw.print(" mMinimized="); pw.println(mMinimized); - pw.print(" mAdjustedForIme="); pw.println(mAdjustedForIme); - } - - public long getAnimDuration() { - float transitionScale = Settings.Global.getFloat(mContext.getContentResolver(), - Settings.Global.TRANSITION_ANIMATION_SCALE, - mContext.getResources().getFloat( - com.android.internal.R.dimen - .config_appTransitionAnimationDurationScaleDefault)); - final long transitionDuration = DEFAULT_APP_TRANSITION_DURATION; - return (long) (transitionDuration * transitionScale); - } - - public void registerInSplitScreenListener(Consumer<Boolean> listener) { - listener.accept(isDividerVisible()); - synchronized (mDockedStackExistsListeners) { - mDockedStackExistsListeners.add(new WeakReference<>(listener)); - } - } - - public void unregisterInSplitScreenListener(Consumer<Boolean> listener) { - synchronized (mDockedStackExistsListeners) { - for (int i = mDockedStackExistsListeners.size() - 1; i >= 0; i--) { - if (mDockedStackExistsListeners.get(i) == listener) { - mDockedStackExistsListeners.remove(i); - } - } - } - } - - public void registerBoundsChangeListener(BiConsumer<Rect, Rect> listener) { - synchronized (mBoundsChangedListeners) { - mBoundsChangedListeners.add(new WeakReference<>(listener)); - } - } - - public boolean splitPrimaryTask() { - try { - if (ActivityTaskManager.getService().getLockTaskModeState() == LOCK_TASK_MODE_PINNED) { - return false; - } - } catch (RemoteException e) { - return false; - } - if (isSplitActive() || mSplits.mPrimary == null) { - return false; - } - - // Try fetching the top running task. - final List<RunningTaskInfo> runningTasks = - ActivityTaskManager.getInstance().getTasks(1 /* maxNum */); - if (runningTasks == null || runningTasks.isEmpty()) { - return false; - } - // Note: The set of running tasks from the system is ordered by recency. - final RunningTaskInfo topRunningTask = runningTasks.get(0); - final int activityType = topRunningTask.getActivityType(); - if (activityType == ACTIVITY_TYPE_HOME || activityType == ACTIVITY_TYPE_RECENTS) { - return false; - } - - if (!topRunningTask.supportsSplitScreenMultiWindow) { - Toast.makeText(mContext, R.string.dock_non_resizeble_failed_to_dock_text, - Toast.LENGTH_SHORT).show(); - return false; - } - - final WindowContainerTransaction wct = new WindowContainerTransaction(); - // Clear out current windowing mode before reparenting to split task. - wct.setWindowingMode(topRunningTask.token, WINDOWING_MODE_UNDEFINED); - wct.reparent(topRunningTask.token, mSplits.mPrimary.token, true /* onTop */); - mWindowManagerProxy.applySyncTransaction(wct); - return true; - } - - public void dismissSplitToPrimaryTask() { - startDismissSplit(true /* toPrimaryTask */); - } - - /** Notifies the bounds of split screen changed. */ - public void notifyBoundsChanged(Rect secondaryWindowBounds, Rect secondaryWindowInsets) { - synchronized (mBoundsChangedListeners) { - mBoundsChangedListeners.removeIf(wf -> { - BiConsumer<Rect, Rect> l = wf.get(); - if (l != null) l.accept(secondaryWindowBounds, secondaryWindowInsets); - return l == null; - }); - } - } - - public void startEnterSplit() { - update(mDisplayController.getDisplayContext( - mContext.getDisplayId()).getResources().getConfiguration()); - // Set resizable directly here because applyEnterSplit already resizes home stack. - mHomeStackResizable = mWindowManagerProxy.applyEnterSplit(mSplits, - mRotateSplitLayout != null ? mRotateSplitLayout : mSplitLayout); - } - - public void prepareEnterSplitTransition(WindowContainerTransaction outWct) { - // Set resizable directly here because buildEnterSplit already resizes home stack. - mHomeStackResizable = mWindowManagerProxy.buildEnterSplit(outWct, mSplits, - mRotateSplitLayout != null ? mRotateSplitLayout : mSplitLayout); - } - - public void finishEnterSplitTransition(boolean minimized) { - update(mDisplayController.getDisplayContext( - mContext.getDisplayId()).getResources().getConfiguration()); - if (minimized) { - ensureMinimizedSplit(); - } else { - ensureNormalSplit(); - } - } - - public void startDismissSplit(boolean toPrimaryTask) { - startDismissSplit(toPrimaryTask, false /* snapped */); - } - - public void startDismissSplit(boolean toPrimaryTask, boolean snapped) { - if (Transitions.ENABLE_SHELL_TRANSITIONS) { - mSplits.getSplitTransitions().dismissSplit( - mSplits, mSplitLayout, !toPrimaryTask, snapped); - } else { - mWindowManagerProxy.applyDismissSplit(mSplits, mSplitLayout, !toPrimaryTask); - onDismissSplit(); - } - } - - public void onDismissSplit() { - updateVisibility(false /* visible */); - mMinimized = false; - // Resets divider bar position to undefined, so new divider bar will apply default position - // next time entering split mode. - mDividerState.mRatioPositionBeforeMinimized = 0; - removeDivider(); - mImePositionProcessor.reset(); - } - - public void ensureMinimizedSplit() { - setHomeMinimized(true /* minimized */); - if (mView != null && !isDividerVisible()) { - // Wasn't in split-mode yet, so enter now. - if (DEBUG) { - Slog.d(TAG, " entering split mode with minimized=true"); - } - updateVisibility(true /* visible */); - } - } - - public void ensureNormalSplit() { - setHomeMinimized(false /* minimized */); - if (mView != null && !isDividerVisible()) { - // Wasn't in split-mode, so enter now. - if (DEBUG) { - Slog.d(TAG, " enter split mode unminimized "); - } - updateVisibility(true /* visible */); - } - } - - public LegacySplitDisplayLayout getSplitLayout() { - return mSplitLayout; - } - - public WindowManagerProxy getWmProxy() { - return mWindowManagerProxy; - } - - public WindowContainerToken getSecondaryRoot() { - if (mSplits == null || mSplits.mSecondary == null) { - return null; - } - return mSplits.mSecondary.token; - } - - private class SplitScreenImpl implements LegacySplitScreen { - @Override - public boolean isMinimized() { - return mMinimized; - } - - @Override - public boolean isHomeStackResizable() { - return mHomeStackResizable; - } - - /** - * TODO: Remove usage from outside the shell. - */ - @Override - public DividerView getDividerView() { - return LegacySplitScreenController.this.getDividerView(); - } - - @Override - public boolean isDividerVisible() { - boolean[] result = new boolean[1]; - try { - mMainExecutor.executeBlocking(() -> { - result[0] = LegacySplitScreenController.this.isDividerVisible(); - }); - } catch (InterruptedException e) { - Slog.e(TAG, "Failed to get divider visible"); - } - return result[0]; - } - - @Override - public void onKeyguardVisibilityChanged(boolean isShowing) { - mMainExecutor.execute(() -> { - LegacySplitScreenController.this.onKeyguardVisibilityChanged(isShowing); - }); - } - - @Override - public void setMinimized(boolean minimized) { - mMainExecutor.execute(() -> { - LegacySplitScreenController.this.setMinimized(minimized); - }); - } - - @Override - public void onUndockingTask() { - mMainExecutor.execute(() -> { - LegacySplitScreenController.this.onUndockingTask(); - }); - } - - @Override - public void onAppTransitionFinished() { - mMainExecutor.execute(() -> { - LegacySplitScreenController.this.onAppTransitionFinished(); - }); - } - - @Override - public void registerInSplitScreenListener(Consumer<Boolean> listener) { - mMainExecutor.execute(() -> { - LegacySplitScreenController.this.registerInSplitScreenListener(listener); - }); - } - - @Override - public void unregisterInSplitScreenListener(Consumer<Boolean> listener) { - mMainExecutor.execute(() -> { - LegacySplitScreenController.this.unregisterInSplitScreenListener(listener); - }); - } - - @Override - public void registerBoundsChangeListener(BiConsumer<Rect, Rect> listener) { - mMainExecutor.execute(() -> { - LegacySplitScreenController.this.registerBoundsChangeListener(listener); - }); - } - - @Override - public WindowContainerToken getSecondaryRoot() { - WindowContainerToken[] result = new WindowContainerToken[1]; - try { - mMainExecutor.executeBlocking(() -> { - result[0] = LegacySplitScreenController.this.getSecondaryRoot(); - }); - } catch (InterruptedException e) { - Slog.e(TAG, "Failed to get secondary root"); - } - return result[0]; - } - - @Override - public boolean splitPrimaryTask() { - boolean[] result = new boolean[1]; - try { - mMainExecutor.executeBlocking(() -> { - result[0] = LegacySplitScreenController.this.splitPrimaryTask(); - }); - } catch (InterruptedException e) { - Slog.e(TAG, "Failed to split primary task"); - } - return result[0]; - } - - @Override - public void dismissSplitToPrimaryTask() { - mMainExecutor.execute(() -> { - LegacySplitScreenController.this.dismissSplitToPrimaryTask(); - }); - } - - @Override - public void dump(PrintWriter pw) { - try { - mMainExecutor.executeBlocking(() -> { - LegacySplitScreenController.this.dump(pw); - }); - } catch (InterruptedException e) { - Slog.e(TAG, "Failed to dump LegacySplitScreenController in 2s"); - } - } - } -} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitScreenTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitScreenTaskListener.java deleted file mode 100644 index d2f42c39acd5..000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitScreenTaskListener.java +++ /dev/null @@ -1,376 +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.legacysplitscreen; - -import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; -import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS; -import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED; -import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY; -import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_SECONDARY; -import static android.view.Display.DEFAULT_DISPLAY; - -import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_TASK_ORG; - -import android.app.ActivityManager.RunningTaskInfo; -import android.graphics.Point; -import android.graphics.Rect; -import android.util.Log; -import android.util.SparseArray; -import android.view.SurfaceControl; -import android.view.SurfaceSession; -import android.window.TaskOrganizer; - -import androidx.annotation.NonNull; - -import com.android.internal.protolog.common.ProtoLog; -import com.android.wm.shell.ShellTaskOrganizer; -import com.android.wm.shell.common.SurfaceUtils; -import com.android.wm.shell.common.SyncTransactionQueue; -import com.android.wm.shell.transition.Transitions; - -import java.io.PrintWriter; -import java.util.ArrayList; - -class LegacySplitScreenTaskListener implements ShellTaskOrganizer.TaskListener { - private static final String TAG = LegacySplitScreenTaskListener.class.getSimpleName(); - private static final boolean DEBUG = LegacySplitScreenController.DEBUG; - - private final ShellTaskOrganizer mTaskOrganizer; - private final SyncTransactionQueue mSyncQueue; - private final SparseArray<SurfaceControl> mLeashByTaskId = new SparseArray<>(); - - // TODO(shell-transitions): Remove when switched to shell-transitions. - private final SparseArray<Point> mPositionByTaskId = new SparseArray<>(); - - RunningTaskInfo mPrimary; - RunningTaskInfo mSecondary; - SurfaceControl mPrimarySurface; - SurfaceControl mSecondarySurface; - SurfaceControl mPrimaryDim; - SurfaceControl mSecondaryDim; - Rect mHomeBounds = new Rect(); - final LegacySplitScreenController mSplitScreenController; - private boolean mSplitScreenSupported = false; - - final SurfaceSession mSurfaceSession = new SurfaceSession(); - - private final LegacySplitScreenTransitions mSplitTransitions; - - LegacySplitScreenTaskListener(LegacySplitScreenController splitScreenController, - ShellTaskOrganizer shellTaskOrganizer, - Transitions transitions, - SyncTransactionQueue syncQueue) { - mSplitScreenController = splitScreenController; - mTaskOrganizer = shellTaskOrganizer; - mSplitTransitions = new LegacySplitScreenTransitions(splitScreenController.mTransactionPool, - transitions, mSplitScreenController, this); - transitions.addHandler(mSplitTransitions); - mSyncQueue = syncQueue; - } - - void init() { - synchronized (this) { - try { - mTaskOrganizer.createRootTask( - DEFAULT_DISPLAY, WINDOWING_MODE_SPLIT_SCREEN_PRIMARY, this); - mTaskOrganizer.createRootTask( - DEFAULT_DISPLAY, WINDOWING_MODE_SPLIT_SCREEN_SECONDARY, this); - } catch (Exception e) { - // teardown to prevent callbacks - mTaskOrganizer.removeListener(this); - throw e; - } - } - } - - boolean isSplitScreenSupported() { - return mSplitScreenSupported; - } - - SurfaceControl.Transaction getTransaction() { - return mSplitScreenController.mTransactionPool.acquire(); - } - - void releaseTransaction(SurfaceControl.Transaction t) { - mSplitScreenController.mTransactionPool.release(t); - } - - TaskOrganizer getTaskOrganizer() { - return mTaskOrganizer; - } - - LegacySplitScreenTransitions getSplitTransitions() { - return mSplitTransitions; - } - - @Override - public void onTaskAppeared(RunningTaskInfo taskInfo, SurfaceControl leash) { - synchronized (this) { - if (taskInfo.hasParentTask()) { - handleChildTaskAppeared(taskInfo, leash); - return; - } - - final int winMode = taskInfo.getWindowingMode(); - if (winMode == WINDOWING_MODE_SPLIT_SCREEN_PRIMARY) { - ProtoLog.v(WM_SHELL_TASK_ORG, - "%s onTaskAppeared Primary taskId=%d", TAG, taskInfo.taskId); - mPrimary = taskInfo; - mPrimarySurface = leash; - } else if (winMode == WINDOWING_MODE_SPLIT_SCREEN_SECONDARY) { - ProtoLog.v(WM_SHELL_TASK_ORG, - "%s onTaskAppeared Secondary taskId=%d", TAG, taskInfo.taskId); - mSecondary = taskInfo; - mSecondarySurface = leash; - } else { - ProtoLog.v(WM_SHELL_TASK_ORG, "%s onTaskAppeared unknown taskId=%d winMode=%d", - TAG, taskInfo.taskId, winMode); - } - - if (!mSplitScreenSupported && mPrimarySurface != null && mSecondarySurface != null) { - mSplitScreenSupported = true; - mSplitScreenController.onSplitScreenSupported(); - ProtoLog.v(WM_SHELL_TASK_ORG, "%s onTaskAppeared Supported", TAG); - - // Initialize dim surfaces: - SurfaceControl.Transaction t = getTransaction(); - mPrimaryDim = SurfaceUtils.makeDimLayer( - t, mPrimarySurface, "Primary Divider Dim", mSurfaceSession); - mSecondaryDim = SurfaceUtils.makeDimLayer( - t, mSecondarySurface, "Secondary Divider Dim", mSurfaceSession); - t.apply(); - releaseTransaction(t); - } - } - } - - @Override - public void onTaskVanished(RunningTaskInfo taskInfo) { - synchronized (this) { - mPositionByTaskId.remove(taskInfo.taskId); - if (taskInfo.hasParentTask()) { - mLeashByTaskId.remove(taskInfo.taskId); - return; - } - - final boolean isPrimaryTask = mPrimary != null - && taskInfo.token.equals(mPrimary.token); - final boolean isSecondaryTask = mSecondary != null - && taskInfo.token.equals(mSecondary.token); - - if (mSplitScreenSupported && (isPrimaryTask || isSecondaryTask)) { - mSplitScreenSupported = false; - - SurfaceControl.Transaction t = getTransaction(); - t.remove(mPrimaryDim); - t.remove(mSecondaryDim); - t.remove(mPrimarySurface); - t.remove(mSecondarySurface); - t.apply(); - releaseTransaction(t); - - mSplitScreenController.onTaskVanished(); - } - } - } - - @Override - public void onTaskInfoChanged(RunningTaskInfo taskInfo) { - if (taskInfo.displayId != DEFAULT_DISPLAY) { - return; - } - synchronized (this) { - if (!taskInfo.supportsMultiWindow) { - if (mSplitScreenController.isDividerVisible()) { - // Dismiss the split screen if the task no longer supports multi window. - if (taskInfo.taskId == mPrimary.taskId - || taskInfo.parentTaskId == mPrimary.taskId) { - // If the primary is focused, dismiss to primary. - mSplitScreenController - .startDismissSplit(taskInfo.isFocused /* toPrimaryTask */); - } else { - // If the secondary is not focused, dismiss to primary. - mSplitScreenController - .startDismissSplit(!taskInfo.isFocused /* toPrimaryTask */); - } - } - return; - } - if (taskInfo.hasParentTask()) { - // changed messages are noisy since it reports on every ensureVisibility. This - // conflicts with legacy app-transitions which "swaps" the position to a - // leash. For now, only update when position actually changes to avoid - // poorly-timed duplicate calls. - if (taskInfo.positionInParent.equals(mPositionByTaskId.get(taskInfo.taskId))) { - return; - } - handleChildTaskChanged(taskInfo); - } else { - handleTaskInfoChanged(taskInfo); - } - mPositionByTaskId.put(taskInfo.taskId, new Point(taskInfo.positionInParent)); - } - } - - private void handleChildTaskAppeared(RunningTaskInfo taskInfo, SurfaceControl leash) { - mLeashByTaskId.put(taskInfo.taskId, leash); - mPositionByTaskId.put(taskInfo.taskId, new Point(taskInfo.positionInParent)); - if (Transitions.ENABLE_SHELL_TRANSITIONS) return; - updateChildTaskSurface(taskInfo, leash, true /* firstAppeared */); - } - - private void handleChildTaskChanged(RunningTaskInfo taskInfo) { - if (Transitions.ENABLE_SHELL_TRANSITIONS) return; - final SurfaceControl leash = mLeashByTaskId.get(taskInfo.taskId); - updateChildTaskSurface(taskInfo, leash, false /* firstAppeared */); - } - - private void updateChildTaskSurface( - RunningTaskInfo taskInfo, SurfaceControl leash, boolean firstAppeared) { - final Point taskPositionInParent = taskInfo.positionInParent; - mSyncQueue.runInSync(t -> { - t.setWindowCrop(leash, null); - t.setPosition(leash, taskPositionInParent.x, taskPositionInParent.y); - if (firstAppeared && !Transitions.ENABLE_SHELL_TRANSITIONS) { - t.setAlpha(leash, 1f); - t.setMatrix(leash, 1, 0, 0, 1); - t.show(leash); - } - }); - } - - /** - * This is effectively a finite state machine which moves between the various split-screen - * presentations based on the contents of the split regions. - */ - private void handleTaskInfoChanged(RunningTaskInfo info) { - if (!mSplitScreenSupported) { - // This shouldn't happen; but apparently there is a chance that SysUI crashes without - // system server receiving binder-death (or maybe it receives binder-death too late?). - // In this situation, when sys-ui restarts, the split root-tasks will still exist so - // there is a small window of time during init() where WM might send messages here - // before init() fails. So, avoid a cycle of crashes by returning early. - Log.e(TAG, "Got handleTaskInfoChanged when not initialized: " + info); - return; - } - final boolean secondaryImpliedMinimize = mSecondary.topActivityType == ACTIVITY_TYPE_HOME - || (mSecondary.topActivityType == ACTIVITY_TYPE_RECENTS - && mSplitScreenController.isHomeStackResizable()); - final boolean primaryWasEmpty = mPrimary.topActivityType == ACTIVITY_TYPE_UNDEFINED; - final boolean secondaryWasEmpty = mSecondary.topActivityType == ACTIVITY_TYPE_UNDEFINED; - if (info.token.asBinder() == mPrimary.token.asBinder()) { - mPrimary = info; - } else if (info.token.asBinder() == mSecondary.token.asBinder()) { - mSecondary = info; - } - if (DEBUG) { - Log.d(TAG, "onTaskInfoChanged " + mPrimary + " " + mSecondary); - } - if (Transitions.ENABLE_SHELL_TRANSITIONS) return; - final boolean primaryIsEmpty = mPrimary.topActivityType == ACTIVITY_TYPE_UNDEFINED; - final boolean secondaryIsEmpty = mSecondary.topActivityType == ACTIVITY_TYPE_UNDEFINED; - final boolean secondaryImpliesMinimize = mSecondary.topActivityType == ACTIVITY_TYPE_HOME - || (mSecondary.topActivityType == ACTIVITY_TYPE_RECENTS - && mSplitScreenController.isHomeStackResizable()); - if (primaryIsEmpty == primaryWasEmpty && secondaryWasEmpty == secondaryIsEmpty - && secondaryImpliedMinimize == secondaryImpliesMinimize) { - // No relevant changes - return; - } - if (primaryIsEmpty || secondaryIsEmpty) { - // At-least one of the splits is empty which means we are currently transitioning - // into or out-of split-screen mode. - if (DEBUG) { - Log.d(TAG, " at-least one split empty " + mPrimary.topActivityType - + " " + mSecondary.topActivityType); - } - if (mSplitScreenController.isDividerVisible()) { - // Was in split-mode, which means we are leaving split, so continue that. - // This happens when the stack in the primary-split is dismissed. - if (DEBUG) { - Log.d(TAG, " was in split, so this means leave it " - + mPrimary.topActivityType + " " + mSecondary.topActivityType); - } - mSplitScreenController.startDismissSplit(false /* toPrimaryTask */); - } else if (!primaryIsEmpty && primaryWasEmpty && secondaryWasEmpty) { - // Wasn't in split-mode (both were empty), but now that the primary split is - // populated, we should fully enter split by moving everything else into secondary. - // This just tells window-manager to reparent things, the UI will respond - // when it gets new task info for the secondary split. - if (DEBUG) { - Log.d(TAG, " was not in split, but primary is populated, so enter it"); - } - mSplitScreenController.startEnterSplit(); - } - } else if (secondaryImpliesMinimize) { - // Workaround for b/172686383, we can't rely on the sync bounds change transaction for - // the home task to finish before the last updateChildTaskSurface() call even if it's - // queued on the sync transaction queue, so ensure that the home task surface is updated - // again before we minimize - final ArrayList<RunningTaskInfo> tasks = new ArrayList<>(); - mSplitScreenController.getWmProxy().getHomeAndRecentsTasks(tasks, - mSplitScreenController.getSecondaryRoot()); - for (int i = 0; i < tasks.size(); i++) { - final RunningTaskInfo taskInfo = tasks.get(i); - final SurfaceControl leash = mLeashByTaskId.get(taskInfo.taskId); - if (leash != null) { - updateChildTaskSurface(taskInfo, leash, false /* firstAppeared */); - } - } - - // Both splits are populated but the secondary split has a home/recents stack on top, - // so enter minimized mode. - mSplitScreenController.ensureMinimizedSplit(); - } else { - // Both splits are populated by normal activities, so make sure we aren't minimized. - mSplitScreenController.ensureNormalSplit(); - } - } - - @Override - public void attachChildSurfaceToTask(int taskId, SurfaceControl.Builder b) { - b.setParent(findTaskSurface(taskId)); - } - - @Override - public void reparentChildSurfaceToTask(int taskId, SurfaceControl sc, - SurfaceControl.Transaction t) { - t.reparent(sc, findTaskSurface(taskId)); - } - - private SurfaceControl findTaskSurface(int taskId) { - if (!mLeashByTaskId.contains(taskId)) { - throw new IllegalArgumentException("There is no surface for taskId=" + taskId); - } - return mLeashByTaskId.get(taskId); - } - - @Override - public void dump(@NonNull PrintWriter pw, String prefix) { - final String innerPrefix = prefix + " "; - final String childPrefix = innerPrefix + " "; - pw.println(prefix + this); - pw.println(innerPrefix + "mSplitScreenSupported=" + mSplitScreenSupported); - if (mPrimary != null) pw.println(innerPrefix + "mPrimary.taskId=" + mPrimary.taskId); - if (mSecondary != null) pw.println(innerPrefix + "mSecondary.taskId=" + mSecondary.taskId); - } - - @Override - public String toString() { - return TAG; - } -} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitScreenTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitScreenTransitions.java deleted file mode 100644 index b1fa2ac25fe7..000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitScreenTransitions.java +++ /dev/null @@ -1,348 +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.legacysplitscreen; - -import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; -import static android.view.WindowManager.TRANSIT_CHANGE; -import static android.view.WindowManager.TRANSIT_CLOSE; -import static android.view.WindowManager.TRANSIT_FIRST_CUSTOM; -import static android.view.WindowManager.TRANSIT_OPEN; -import static android.view.WindowManager.TRANSIT_TO_BACK; -import static android.view.WindowManager.TRANSIT_TO_FRONT; - -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.animation.ValueAnimator; -import android.annotation.NonNull; -import android.annotation.Nullable; -import android.app.ActivityManager; -import android.app.WindowConfiguration; -import android.graphics.Rect; -import android.os.IBinder; -import android.view.SurfaceControl; -import android.view.WindowManager; -import android.window.TransitionInfo; -import android.window.TransitionRequestInfo; -import android.window.WindowContainerTransaction; - -import com.android.wm.shell.common.TransactionPool; -import com.android.wm.shell.common.annotations.ExternalThread; -import com.android.wm.shell.transition.Transitions; - -import java.util.ArrayList; - -/** Plays transition animations for split-screen */ -public class LegacySplitScreenTransitions implements Transitions.TransitionHandler { - private static final String TAG = "SplitScreenTransitions"; - - public static final int TRANSIT_SPLIT_DISMISS_SNAP = TRANSIT_FIRST_CUSTOM + 10; - - private final TransactionPool mTransactionPool; - private final Transitions mTransitions; - private final LegacySplitScreenController mSplitScreen; - private final LegacySplitScreenTaskListener mListener; - - private IBinder mPendingDismiss = null; - private boolean mDismissFromSnap = false; - private IBinder mPendingEnter = null; - private IBinder mAnimatingTransition = null; - - /** Keeps track of currently running animations */ - private final ArrayList<Animator> mAnimations = new ArrayList<>(); - - private Transitions.TransitionFinishCallback mFinishCallback = null; - private SurfaceControl.Transaction mFinishTransaction; - - LegacySplitScreenTransitions(@NonNull TransactionPool pool, @NonNull Transitions transitions, - @NonNull LegacySplitScreenController splitScreen, - @NonNull LegacySplitScreenTaskListener listener) { - mTransactionPool = pool; - mTransitions = transitions; - mSplitScreen = splitScreen; - mListener = listener; - } - - @Override - public WindowContainerTransaction handleRequest(@NonNull IBinder transition, - @Nullable TransitionRequestInfo request) { - WindowContainerTransaction out = null; - final ActivityManager.RunningTaskInfo triggerTask = request.getTriggerTask(); - final @WindowManager.TransitionType int type = request.getType(); - if (mSplitScreen.isDividerVisible()) { - // try to handle everything while in split-screen - out = new WindowContainerTransaction(); - if (triggerTask != null) { - final boolean shouldDismiss = - // if we close the primary-docked task, then leave split-screen since there - // is nothing behind it. - ((type == TRANSIT_CLOSE || type == TRANSIT_TO_BACK) - && triggerTask.parentTaskId == mListener.mPrimary.taskId) - // if an activity that is not supported in multi window mode is launched, - // we also need to leave split-screen. - || ((type == TRANSIT_OPEN || type == TRANSIT_TO_FRONT) - && !triggerTask.supportsMultiWindow); - // In both cases, dismiss the primary - if (shouldDismiss) { - WindowManagerProxy.buildDismissSplit(out, mListener, - mSplitScreen.getSplitLayout(), true /* dismiss */); - if (type == TRANSIT_OPEN || type == TRANSIT_TO_FRONT) { - out.reorder(triggerTask.token, true /* onTop */); - } - mPendingDismiss = transition; - } - } - } else if (triggerTask != null) { - // Not in split mode, so look for an open with a trigger task. - if ((type == TRANSIT_OPEN || type == TRANSIT_TO_FRONT) - && triggerTask.configuration.windowConfiguration.getWindowingMode() - == WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY) { - out = new WindowContainerTransaction(); - mSplitScreen.prepareEnterSplitTransition(out); - mPendingEnter = transition; - } - } - return out; - } - - // TODO(shell-transitions): real animations - private void startExampleAnimation(@NonNull SurfaceControl leash, boolean show) { - final float end = show ? 1.f : 0.f; - final float start = 1.f - end; - final SurfaceControl.Transaction transaction = mTransactionPool.acquire(); - final ValueAnimator va = ValueAnimator.ofFloat(start, end); - va.setDuration(500); - va.addUpdateListener(animation -> { - float fraction = animation.getAnimatedFraction(); - transaction.setAlpha(leash, start * (1.f - fraction) + end * fraction); - transaction.apply(); - }); - final Runnable finisher = () -> { - transaction.setAlpha(leash, end); - transaction.apply(); - mTransactionPool.release(transaction); - mTransitions.getMainExecutor().execute(() -> { - mAnimations.remove(va); - onFinish(); - }); - }; - va.addListener(new Animator.AnimatorListener() { - @Override - public void onAnimationStart(Animator animation) { } - - @Override - public void onAnimationEnd(Animator animation) { - finisher.run(); - } - - @Override - public void onAnimationCancel(Animator animation) { - finisher.run(); - } - - @Override - public void onAnimationRepeat(Animator animation) { } - }); - mAnimations.add(va); - mTransitions.getAnimExecutor().execute(va::start); - } - - // TODO(shell-transitions): real animations - private void startExampleResizeAnimation(@NonNull SurfaceControl leash, - @NonNull Rect startBounds, @NonNull Rect endBounds) { - final SurfaceControl.Transaction transaction = mTransactionPool.acquire(); - final ValueAnimator va = ValueAnimator.ofFloat(0.f, 1.f); - va.setDuration(500); - va.addUpdateListener(animation -> { - float fraction = animation.getAnimatedFraction(); - transaction.setWindowCrop(leash, - (int) (startBounds.width() * (1.f - fraction) + endBounds.width() * fraction), - (int) (startBounds.height() * (1.f - fraction) - + endBounds.height() * fraction)); - transaction.setPosition(leash, - startBounds.left * (1.f - fraction) + endBounds.left * fraction, - startBounds.top * (1.f - fraction) + endBounds.top * fraction); - transaction.apply(); - }); - final Runnable finisher = () -> { - transaction.setWindowCrop(leash, 0, 0); - transaction.setPosition(leash, endBounds.left, endBounds.top); - transaction.apply(); - mTransactionPool.release(transaction); - mTransitions.getMainExecutor().execute(() -> { - mAnimations.remove(va); - onFinish(); - }); - }; - va.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - finisher.run(); - } - - @Override - public void onAnimationCancel(Animator animation) { - finisher.run(); - } - }); - mAnimations.add(va); - mTransitions.getAnimExecutor().execute(va::start); - } - - @Override - public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, - @NonNull SurfaceControl.Transaction startTransaction, - @NonNull SurfaceControl.Transaction finishTransaction, - @NonNull Transitions.TransitionFinishCallback finishCallback) { - if (transition != mPendingDismiss && transition != mPendingEnter) { - // If we're not in split-mode, just abort - if (!mSplitScreen.isDividerVisible()) return false; - // Check to see if HOME is involved - for (int i = info.getChanges().size() - 1; i >= 0; --i) { - final TransitionInfo.Change change = info.getChanges().get(i); - if (change.getTaskInfo() == null - || change.getTaskInfo().getActivityType() != ACTIVITY_TYPE_HOME) continue; - if (change.getMode() == TRANSIT_OPEN || change.getMode() == TRANSIT_TO_FRONT) { - mSplitScreen.ensureMinimizedSplit(); - } else if (change.getMode() == TRANSIT_CLOSE - || change.getMode() == TRANSIT_TO_BACK) { - mSplitScreen.ensureNormalSplit(); - } - } - // Use normal animations. - return false; - } - - mFinishCallback = finishCallback; - mFinishTransaction = mTransactionPool.acquire(); - mAnimatingTransition = transition; - - // Play fade animations - for (int i = info.getChanges().size() - 1; i >= 0; --i) { - final TransitionInfo.Change change = info.getChanges().get(i); - final SurfaceControl leash = change.getLeash(); - final int mode = info.getChanges().get(i).getMode(); - - if (mode == TRANSIT_CHANGE) { - if (change.getParent() != null) { - // This is probably reparented, so we want the parent to be immediately visible - final TransitionInfo.Change parentChange = info.getChange(change.getParent()); - startTransaction.show(parentChange.getLeash()); - startTransaction.setAlpha(parentChange.getLeash(), 1.f); - // and then animate this layer outside the parent (since, for example, this is - // the home task animating from fullscreen to part-screen). - startTransaction.reparent(leash, info.getRootLeash()); - startTransaction.setLayer(leash, info.getChanges().size() - i); - // build the finish reparent/reposition - mFinishTransaction.reparent(leash, parentChange.getLeash()); - mFinishTransaction.setPosition(leash, - change.getEndRelOffset().x, change.getEndRelOffset().y); - } - // TODO(shell-transitions): screenshot here - final Rect startBounds = new Rect(change.getStartAbsBounds()); - final boolean isHome = change.getTaskInfo() != null - && change.getTaskInfo().getActivityType() == ACTIVITY_TYPE_HOME; - if (mPendingDismiss == transition && mDismissFromSnap && !isHome) { - // Home is special since it doesn't move during fling. Everything else, though, - // when dismissing from snap, the top/left is at 0,0. - startBounds.offsetTo(0, 0); - } - final Rect endBounds = new Rect(change.getEndAbsBounds()); - startBounds.offset(-info.getRootOffset().x, -info.getRootOffset().y); - endBounds.offset(-info.getRootOffset().x, -info.getRootOffset().y); - startExampleResizeAnimation(leash, startBounds, endBounds); - } - if (change.getParent() != null) { - continue; - } - - if (transition == mPendingEnter - && mListener.mPrimary.token.equals(change.getContainer()) - || mListener.mSecondary.token.equals(change.getContainer())) { - startTransaction.setWindowCrop(leash, change.getStartAbsBounds().width(), - change.getStartAbsBounds().height()); - if (mListener.mPrimary.token.equals(change.getContainer())) { - // Move layer to top since we want it above the oversized home task during - // animation even though home task is on top in hierarchy. - startTransaction.setLayer(leash, info.getChanges().size() + 1); - } - } - boolean isOpening = Transitions.isOpeningType(info.getType()); - if (isOpening && (mode == TRANSIT_OPEN || mode == TRANSIT_TO_FRONT)) { - // fade in - startExampleAnimation(leash, true /* show */); - } else if (!isOpening && (mode == TRANSIT_CLOSE || mode == TRANSIT_TO_BACK)) { - // fade out - if (transition == mPendingDismiss && mDismissFromSnap) { - // Dismissing via snap-to-top/bottom means that the dismissed task is already - // not-visible (usually cropped to oblivion) so immediately set its alpha to 0 - // and don't animate it so it doesn't pop-in when reparented. - startTransaction.setAlpha(leash, 0.f); - } else { - startExampleAnimation(leash, false /* show */); - } - } - } - if (transition == mPendingEnter) { - // If entering, check if we should enter into minimized or normal split - boolean homeIsVisible = false; - for (int i = info.getChanges().size() - 1; i >= 0; --i) { - final TransitionInfo.Change change = info.getChanges().get(i); - if (change.getTaskInfo() == null - || change.getTaskInfo().getActivityType() != ACTIVITY_TYPE_HOME) { - continue; - } - homeIsVisible = change.getMode() == TRANSIT_OPEN - || change.getMode() == TRANSIT_TO_FRONT - || change.getMode() == TRANSIT_CHANGE; - break; - } - mSplitScreen.finishEnterSplitTransition(homeIsVisible); - } - startTransaction.apply(); - onFinish(); - return true; - } - - @ExternalThread - void dismissSplit(LegacySplitScreenTaskListener tiles, LegacySplitDisplayLayout layout, - boolean dismissOrMaximize, boolean snapped) { - final WindowContainerTransaction wct = new WindowContainerTransaction(); - WindowManagerProxy.buildDismissSplit(wct, tiles, layout, dismissOrMaximize); - mTransitions.getMainExecutor().execute(() -> { - mDismissFromSnap = snapped; - mPendingDismiss = mTransitions.startTransition(TRANSIT_SPLIT_DISMISS_SNAP, wct, this); - }); - } - - private void onFinish() { - if (!mAnimations.isEmpty()) return; - mFinishTransaction.apply(); - mTransactionPool.release(mFinishTransaction); - mFinishTransaction = null; - mFinishCallback.onTransitionFinished(null /* wct */, null /* wctCB */); - mFinishCallback = null; - if (mAnimatingTransition == mPendingEnter) { - mPendingEnter = null; - } - if (mAnimatingTransition == mPendingDismiss) { - mSplitScreen.onDismissSplit(); - mPendingDismiss = null; - } - mDismissFromSnap = false; - mAnimatingTransition = null; - } -} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/MinimizedDockShadow.java b/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/MinimizedDockShadow.java deleted file mode 100644 index 1e9223cbe3e2..000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/MinimizedDockShadow.java +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES 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.legacysplitscreen; - -import android.annotation.Nullable; -import android.content.Context; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.LinearGradient; -import android.graphics.Paint; -import android.graphics.Shader; -import android.util.AttributeSet; -import android.view.View; -import android.view.WindowManager; - -import com.android.wm.shell.R; - -/** - * Shadow for the minimized dock state on homescreen. - */ -public class MinimizedDockShadow extends View { - - private final Paint mShadowPaint = new Paint(); - - private int mDockSide = WindowManager.DOCKED_INVALID; - - public MinimizedDockShadow(Context context, @Nullable AttributeSet attrs) { - super(context, attrs); - } - - void setDockSide(int dockSide) { - if (dockSide != mDockSide) { - mDockSide = dockSide; - updatePaint(getLeft(), getTop(), getRight(), getBottom()); - invalidate(); - } - } - - private void updatePaint(int left, int top, int right, int bottom) { - int startColor = mContext.getResources().getColor( - R.color.minimize_dock_shadow_start, null); - int endColor = mContext.getResources().getColor( - R.color.minimize_dock_shadow_end, null); - final int middleColor = Color.argb( - (Color.alpha(startColor) + Color.alpha(endColor)) / 2, 0, 0, 0); - final int quarter = Color.argb( - (int) (Color.alpha(startColor) * 0.25f + Color.alpha(endColor) * 0.75f), - 0, 0, 0); - if (mDockSide == WindowManager.DOCKED_TOP) { - mShadowPaint.setShader(new LinearGradient( - 0, 0, 0, bottom - top, - new int[] { startColor, middleColor, quarter, endColor }, - new float[] { 0f, 0.35f, 0.6f, 1f }, Shader.TileMode.CLAMP)); - } else if (mDockSide == WindowManager.DOCKED_LEFT) { - mShadowPaint.setShader(new LinearGradient( - 0, 0, right - left, 0, - new int[] { startColor, middleColor, quarter, endColor }, - new float[] { 0f, 0.35f, 0.6f, 1f }, Shader.TileMode.CLAMP)); - } else if (mDockSide == WindowManager.DOCKED_RIGHT) { - mShadowPaint.setShader(new LinearGradient( - right - left, 0, 0, 0, - new int[] { startColor, middleColor, quarter, endColor }, - new float[] { 0f, 0.35f, 0.6f, 1f }, Shader.TileMode.CLAMP)); - } - } - - @Override - protected void onLayout(boolean changed, int left, int top, int right, int bottom) { - super.onLayout(changed, left, top, right, bottom); - if (changed) { - updatePaint(left, top, right, bottom); - invalidate(); - } - } - - @Override - protected void onDraw(Canvas canvas) { - canvas.drawRect(0, 0, getWidth(), getHeight(), mShadowPaint); - } - - @Override - public boolean hasOverlappingRendering() { - return false; - } -} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/WindowManagerProxy.java b/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/WindowManagerProxy.java deleted file mode 100644 index e42e43bbc2be..000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/WindowManagerProxy.java +++ /dev/null @@ -1,386 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES 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.legacysplitscreen; - -import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; -import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS; -import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; -import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED; -import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; -import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_SECONDARY; -import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; -import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; -import static android.content.res.Configuration.ORIENTATION_UNDEFINED; -import static android.view.Display.DEFAULT_DISPLAY; - -import android.annotation.NonNull; -import android.app.ActivityManager; -import android.app.ActivityTaskManager; -import android.graphics.Rect; -import android.os.RemoteException; -import android.util.Log; -import android.view.Display; -import android.view.SurfaceControl; -import android.view.WindowManagerGlobal; -import android.window.TaskOrganizer; -import android.window.WindowContainerToken; -import android.window.WindowContainerTransaction; - -import com.android.internal.annotations.GuardedBy; -import com.android.internal.util.ArrayUtils; -import com.android.wm.shell.common.SyncTransactionQueue; -import com.android.wm.shell.transition.Transitions; - -import java.util.ArrayList; -import java.util.List; - -/** - * Proxy to simplify calls into window manager/activity manager - */ -class WindowManagerProxy { - - private static final String TAG = "WindowManagerProxy"; - private static final int[] HOME_AND_RECENTS = {ACTIVITY_TYPE_HOME, ACTIVITY_TYPE_RECENTS}; - private static final int[] CONTROLLED_ACTIVITY_TYPES = { - ACTIVITY_TYPE_STANDARD, - ACTIVITY_TYPE_HOME, - ACTIVITY_TYPE_RECENTS, - ACTIVITY_TYPE_UNDEFINED - }; - private static final int[] CONTROLLED_WINDOWING_MODES = { - WINDOWING_MODE_FULLSCREEN, - WINDOWING_MODE_SPLIT_SCREEN_SECONDARY, - WINDOWING_MODE_UNDEFINED - }; - - @GuardedBy("mDockedRect") - private final Rect mDockedRect = new Rect(); - - private final Rect mTmpRect1 = new Rect(); - - @GuardedBy("mDockedRect") - private final Rect mTouchableRegion = new Rect(); - - private final SyncTransactionQueue mSyncTransactionQueue; - private final TaskOrganizer mTaskOrganizer; - - WindowManagerProxy(SyncTransactionQueue syncQueue, TaskOrganizer taskOrganizer) { - mSyncTransactionQueue = syncQueue; - mTaskOrganizer = taskOrganizer; - } - - void dismissOrMaximizeDocked(final LegacySplitScreenTaskListener tiles, - LegacySplitDisplayLayout layout, final boolean dismissOrMaximize) { - if (Transitions.ENABLE_SHELL_TRANSITIONS) { - tiles.mSplitScreenController.startDismissSplit(!dismissOrMaximize, true /* snapped */); - } else { - applyDismissSplit(tiles, layout, dismissOrMaximize); - } - } - - public void setResizing(final boolean resizing) { - try { - ActivityTaskManager.getService().setSplitScreenResizing(resizing); - } catch (RemoteException e) { - Log.w(TAG, "Error calling setDockedStackResizing: " + e); - } - } - - /** Sets a touch region */ - public void setTouchRegion(Rect region) { - try { - synchronized (mDockedRect) { - mTouchableRegion.set(region); - } - WindowManagerGlobal.getWindowManagerService().setDockedTaskDividerTouchRegion( - mTouchableRegion); - } catch (RemoteException e) { - Log.w(TAG, "Failed to set touchable region: " + e); - } - } - - void applyResizeSplits(int position, LegacySplitDisplayLayout splitLayout) { - WindowContainerTransaction t = new WindowContainerTransaction(); - splitLayout.resizeSplits(position, t); - applySyncTransaction(t); - } - - boolean getHomeAndRecentsTasks(List<ActivityManager.RunningTaskInfo> out, - WindowContainerToken parent) { - boolean resizable = false; - List<ActivityManager.RunningTaskInfo> rootTasks = parent == null - ? mTaskOrganizer.getRootTasks(Display.DEFAULT_DISPLAY, HOME_AND_RECENTS) - : mTaskOrganizer.getChildTasks(parent, HOME_AND_RECENTS); - for (int i = 0, n = rootTasks.size(); i < n; ++i) { - final ActivityManager.RunningTaskInfo ti = rootTasks.get(i); - out.add(ti); - if (ti.topActivityType == ACTIVITY_TYPE_HOME) { - resizable = ti.isResizeable; - } - } - return resizable; - } - - /** - * Assign a fixed override-bounds to home tasks that reflect their geometry while the primary - * split is minimized. This actually "sticks out" of the secondary split area, but when in - * minimized mode, the secondary split gets a 'negative' crop to expose it. - */ - boolean applyHomeTasksMinimized(LegacySplitDisplayLayout layout, WindowContainerToken parent, - @NonNull WindowContainerTransaction wct) { - // Resize the home/recents stacks to the larger minimized-state size - final Rect homeBounds; - final ArrayList<ActivityManager.RunningTaskInfo> homeStacks = new ArrayList<>(); - boolean isHomeResizable = getHomeAndRecentsTasks(homeStacks, parent); - if (isHomeResizable) { - homeBounds = layout.calcResizableMinimizedHomeStackBounds(); - } else { - // home is not resizable, so lock it to its inherent orientation size. - homeBounds = new Rect(0, 0, 0, 0); - for (int i = homeStacks.size() - 1; i >= 0; --i) { - if (homeStacks.get(i).topActivityType == ACTIVITY_TYPE_HOME) { - final int orient = homeStacks.get(i).configuration.orientation; - final boolean displayLandscape = layout.mDisplayLayout.isLandscape(); - final boolean isLandscape = orient == ORIENTATION_LANDSCAPE - || (orient == ORIENTATION_UNDEFINED && displayLandscape); - homeBounds.right = isLandscape == displayLandscape - ? layout.mDisplayLayout.width() : layout.mDisplayLayout.height(); - homeBounds.bottom = isLandscape == displayLandscape - ? layout.mDisplayLayout.height() : layout.mDisplayLayout.width(); - break; - } - } - } - for (int i = homeStacks.size() - 1; i >= 0; --i) { - // For non-resizable homes, the minimized size is actually the fullscreen-size. As a - // result, we don't minimize for recents since it only shows half-size screenshots. - if (!isHomeResizable) { - if (homeStacks.get(i).topActivityType == ACTIVITY_TYPE_RECENTS) { - continue; - } - wct.setWindowingMode(homeStacks.get(i).token, WINDOWING_MODE_FULLSCREEN); - } - wct.setBounds(homeStacks.get(i).token, homeBounds); - } - layout.mTiles.mHomeBounds.set(homeBounds); - return isHomeResizable; - } - - /** @see #buildEnterSplit */ - boolean applyEnterSplit(LegacySplitScreenTaskListener tiles, LegacySplitDisplayLayout layout) { - // Set launchtile first so that any stack created after - // getAllRootTaskInfos and before reparent (even if unlikely) are placed - // correctly. - WindowContainerTransaction wct = new WindowContainerTransaction(); - wct.setLaunchRoot(tiles.mSecondary.token, CONTROLLED_WINDOWING_MODES, - CONTROLLED_ACTIVITY_TYPES); - final boolean isHomeResizable = buildEnterSplit(wct, tiles, layout); - applySyncTransaction(wct); - return isHomeResizable; - } - - /** - * Finishes entering split-screen by reparenting all FULLSCREEN tasks into the secondary split. - * This assumes there is already something in the primary split since that is usually what - * triggers a call to this. In the same transaction, this overrides the home task bounds via - * {@link #applyHomeTasksMinimized}. - * - * @return whether the home stack is resizable - */ - boolean buildEnterSplit(WindowContainerTransaction outWct, LegacySplitScreenTaskListener tiles, - LegacySplitDisplayLayout layout) { - List<ActivityManager.RunningTaskInfo> rootTasks = - mTaskOrganizer.getRootTasks(DEFAULT_DISPLAY, null /* activityTypes */); - if (rootTasks.isEmpty()) { - return false; - } - ActivityManager.RunningTaskInfo topHomeTask = null; - for (int i = rootTasks.size() - 1; i >= 0; --i) { - final ActivityManager.RunningTaskInfo rootTask = rootTasks.get(i); - // Check whether the task can be moved to split secondary. - if (!rootTask.supportsMultiWindow && rootTask.topActivityType != ACTIVITY_TYPE_HOME) { - continue; - } - // Only move split controlling tasks to split secondary. - final int windowingMode = rootTask.getWindowingMode(); - if (!ArrayUtils.contains(CONTROLLED_WINDOWING_MODES, windowingMode) - || !ArrayUtils.contains(CONTROLLED_ACTIVITY_TYPES, rootTask.getActivityType()) - // Excludes split screen secondary due to it's the root we're reparenting to. - || windowingMode == WINDOWING_MODE_SPLIT_SCREEN_SECONDARY) { - continue; - } - // Since this iterates from bottom to top, update topHomeTask for every fullscreen task - // so it will be left with the status of the top one. - topHomeTask = isHomeOrRecentTask(rootTask) ? rootTask : null; - outWct.reparent(rootTask.token, tiles.mSecondary.token, true /* onTop */); - } - // Move the secondary split-forward. - outWct.reorder(tiles.mSecondary.token, true /* onTop */); - boolean isHomeResizable = applyHomeTasksMinimized(layout, null /* parent */, - outWct); - if (topHomeTask != null && !Transitions.ENABLE_SHELL_TRANSITIONS) { - // Translate/update-crop of secondary out-of-band with sync transaction -- Until BALST - // is enabled, this temporarily syncs the home surface position with offset until - // sync transaction finishes. - outWct.setBoundsChangeTransaction(topHomeTask.token, tiles.mHomeBounds); - } - return isHomeResizable; - } - - static boolean isHomeOrRecentTask(ActivityManager.RunningTaskInfo ti) { - final int atype = ti.getActivityType(); - return atype == ACTIVITY_TYPE_HOME || atype == ACTIVITY_TYPE_RECENTS; - } - - /** @see #buildDismissSplit */ - void applyDismissSplit(LegacySplitScreenTaskListener tiles, LegacySplitDisplayLayout layout, - boolean dismissOrMaximize) { - // TODO(task-org): Once task-org is more complete, consider using Appeared/Vanished - // plus specific APIs to clean this up. - final WindowContainerTransaction wct = new WindowContainerTransaction(); - // Set launch root first so that any task created after getChildContainers and - // before reparent (pretty unlikely) are put into fullscreen. - wct.setLaunchRoot(tiles.mSecondary.token, null, null); - buildDismissSplit(wct, tiles, layout, dismissOrMaximize); - applySyncTransaction(wct); - } - - /** - * Reparents all tile members back to their display and resets home task override bounds. - * @param dismissOrMaximize When {@code true} this resolves the split by closing the primary - * split (thus resulting in the top of the secondary split becoming - * fullscreen. {@code false} resolves the other way. - */ - static void buildDismissSplit(WindowContainerTransaction outWct, - LegacySplitScreenTaskListener tiles, LegacySplitDisplayLayout layout, - boolean dismissOrMaximize) { - // TODO(task-org): Once task-org is more complete, consider using Appeared/Vanished - // plus specific APIs to clean this up. - final TaskOrganizer taskOrg = tiles.getTaskOrganizer(); - List<ActivityManager.RunningTaskInfo> primaryChildren = - taskOrg.getChildTasks(tiles.mPrimary.token, null /* activityTypes */); - List<ActivityManager.RunningTaskInfo> secondaryChildren = - taskOrg.getChildTasks(tiles.mSecondary.token, null /* activityTypes */); - // In some cases (eg. non-resizable is launched), system-server will leave split-screen. - // as a result, the above will not capture any tasks; yet, we need to clean-up the - // home task bounds. - List<ActivityManager.RunningTaskInfo> freeHomeAndRecents = - taskOrg.getRootTasks(DEFAULT_DISPLAY, HOME_AND_RECENTS); - // Filter out the root split tasks - freeHomeAndRecents.removeIf(p -> p.token.equals(tiles.mSecondary.token) - || p.token.equals(tiles.mPrimary.token)); - - if (primaryChildren.isEmpty() && secondaryChildren.isEmpty() - && freeHomeAndRecents.isEmpty()) { - return; - } - if (dismissOrMaximize) { - // Dismissing, so move all primary split tasks first - for (int i = primaryChildren.size() - 1; i >= 0; --i) { - outWct.reparent(primaryChildren.get(i).token, null /* parent */, - true /* onTop */); - } - boolean homeOnTop = false; - // Don't need to worry about home tasks because they are already in the "proper" - // order within the secondary split. - for (int i = secondaryChildren.size() - 1; i >= 0; --i) { - final ActivityManager.RunningTaskInfo ti = secondaryChildren.get(i); - outWct.reparent(ti.token, null /* parent */, true /* onTop */); - if (isHomeOrRecentTask(ti)) { - outWct.setBounds(ti.token, null); - outWct.setWindowingMode(ti.token, WINDOWING_MODE_UNDEFINED); - if (i == 0) { - homeOnTop = true; - } - } - } - if (homeOnTop && !Transitions.ENABLE_SHELL_TRANSITIONS) { - // Translate/update-crop of secondary out-of-band with sync transaction -- instead - // play this in sync with new home-app frame because until BALST is enabled this - // shows up on screen before the syncTransaction returns. - // We only have access to the secondary root surface, though, so in order to - // position things properly, we have to take into account the existing negative - // offset/crop of the minimized-home task. - final boolean landscape = layout.mDisplayLayout.isLandscape(); - final int posX = landscape ? layout.mSecondary.left - tiles.mHomeBounds.left - : layout.mSecondary.left; - final int posY = landscape ? layout.mSecondary.top - : layout.mSecondary.top - tiles.mHomeBounds.top; - final SurfaceControl.Transaction sft = new SurfaceControl.Transaction(); - sft.setPosition(tiles.mSecondarySurface, posX, posY); - final Rect crop = new Rect(0, 0, layout.mDisplayLayout.width(), - layout.mDisplayLayout.height()); - crop.offset(-posX, -posY); - sft.setWindowCrop(tiles.mSecondarySurface, crop); - outWct.setBoundsChangeTransaction(tiles.mSecondary.token, sft); - } - } else { - // Maximize, so move non-home secondary split first - for (int i = secondaryChildren.size() - 1; i >= 0; --i) { - if (isHomeOrRecentTask(secondaryChildren.get(i))) { - continue; - } - outWct.reparent(secondaryChildren.get(i).token, null /* parent */, - true /* onTop */); - } - // Find and place home tasks in-between. This simulates the fact that there was - // nothing behind the primary split's tasks. - for (int i = secondaryChildren.size() - 1; i >= 0; --i) { - final ActivityManager.RunningTaskInfo ti = secondaryChildren.get(i); - if (isHomeOrRecentTask(ti)) { - outWct.reparent(ti.token, null /* parent */, true /* onTop */); - // reset bounds and mode too - outWct.setBounds(ti.token, null); - outWct.setWindowingMode(ti.token, WINDOWING_MODE_UNDEFINED); - } - } - for (int i = primaryChildren.size() - 1; i >= 0; --i) { - outWct.reparent(primaryChildren.get(i).token, null /* parent */, - true /* onTop */); - } - } - for (int i = freeHomeAndRecents.size() - 1; i >= 0; --i) { - outWct.setBounds(freeHomeAndRecents.get(i).token, null); - outWct.setWindowingMode(freeHomeAndRecents.get(i).token, WINDOWING_MODE_UNDEFINED); - } - // Reset focusable to true - outWct.setFocusable(tiles.mPrimary.token, true /* focusable */); - } - - /** - * Utility to apply a sync transaction serially with other sync transactions. - * - * @see SyncTransactionQueue#queue - */ - void applySyncTransaction(WindowContainerTransaction wct) { - mSyncTransactionQueue.queue(wct); - } - - /** - * @see SyncTransactionQueue#queueIfWaiting - */ - boolean queueSyncTransactionIfWaiting(WindowContainerTransaction wct) { - return mSyncTransactionQueue.queueIfWaiting(wct); - } - - /** - * @see SyncTransactionQueue#runInSync - */ - void runInSync(SyncTransactionQueue.TransactionRunnable runnable) { - mSyncTransactionQueue.runInSync(runnable); - } -} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHanded.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHanded.java index b00182f36cc8..7129165a78dc 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHanded.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHanded.java @@ -16,7 +16,6 @@ package com.android.wm.shell.onehanded; -import android.content.res.Configuration; import android.os.SystemProperties; import com.android.wm.shell.common.annotations.ExternalThread; @@ -38,16 +37,6 @@ public interface OneHanded { } /** - * Return one handed settings enabled or not. - */ - boolean isOneHandedEnabled(); - - /** - * Return swipe to notification settings enabled or not. - */ - boolean isSwipeToNotificationEnabled(); - - /** * Enters one handed mode. */ void startOneHanded(); @@ -81,19 +70,4 @@ public interface OneHanded { * transition start or finish */ void registerTransitionCallback(OneHandedTransitionCallback callback); - - /** - * Receive onConfigurationChanged() events - */ - void onConfigChanged(Configuration newConfig); - - /** - * Notifies when user switch complete - */ - void onUserSwitch(int userId); - - /** - * Notifies when keyguard visibility changed - */ - void onKeyguardVisibilityChanged(boolean showing); } 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 179b725ab210..e0c4fe8c4fba 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java @@ -28,17 +28,16 @@ import static com.android.wm.shell.onehanded.OneHandedState.STATE_NONE; import android.annotation.BinderThread; import android.content.ComponentName; import android.content.Context; -import android.content.om.IOverlayManager; import android.content.res.Configuration; import android.database.ContentObserver; import android.graphics.Rect; import android.os.Handler; -import android.os.ServiceManager; import android.os.SystemProperties; import android.provider.Settings; import android.util.Slog; import android.view.WindowManager; import android.view.accessibility.AccessibilityManager; +import android.window.DisplayAreaInfo; import android.window.WindowContainerTransaction; import androidx.annotation.NonNull; @@ -55,6 +54,12 @@ 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.common.annotations.ExternalThread; +import com.android.wm.shell.sysui.ConfigurationChangeListener; +import com.android.wm.shell.sysui.KeyguardChangeListener; +import com.android.wm.shell.sysui.ShellCommandHandler; +import com.android.wm.shell.sysui.ShellController; +import com.android.wm.shell.sysui.ShellInit; +import com.android.wm.shell.sysui.UserChangeListener; import java.io.PrintWriter; @@ -62,7 +67,8 @@ import java.io.PrintWriter; * Manages and manipulates the one handed states, transitions, and gesture for phones. */ public class OneHandedController implements RemoteCallable<OneHandedController>, - DisplayChangeController.OnDisplayChangingListener { + DisplayChangeController.OnDisplayChangingListener, ConfigurationChangeListener, + KeyguardChangeListener, UserChangeListener { private static final String TAG = "OneHandedController"; private static final String ONE_HANDED_MODE_OFFSET_PERCENTAGE = @@ -71,8 +77,8 @@ public class OneHandedController implements RemoteCallable<OneHandedController>, public static final String SUPPORT_ONE_HANDED_MODE = "ro.support_one_handed_mode"; - private volatile boolean mIsOneHandedEnabled; - private volatile boolean mIsSwipeToNotificationEnabled; + private boolean mIsOneHandedEnabled; + private boolean mIsSwipeToNotificationEnabled; private boolean mIsShortcutEnabled; private boolean mTaskChangeToExit; private boolean mLockedDisabled; @@ -82,6 +88,8 @@ public class OneHandedController implements RemoteCallable<OneHandedController>, private Context mContext; + private final ShellCommandHandler mShellCommandHandler; + private final ShellController mShellController; private final AccessibilityManager mAccessibilityManager; private final DisplayController mDisplayController; private final OneHandedSettingsUtil mOneHandedSettingsUtil; @@ -91,7 +99,6 @@ public class OneHandedController implements RemoteCallable<OneHandedController>, private final OneHandedState mState; private final OneHandedTutorialHandler mTutorialHandler; private final TaskStackListenerImpl mTaskStackListener; - private final IOverlayManager mOverlayManager; private final ShellExecutor mMainExecutor; private final Handler mMainHandler; private final OneHandedImpl mImpl = new OneHandedImpl(); @@ -189,9 +196,11 @@ public class OneHandedController implements RemoteCallable<OneHandedController>, /** * Creates {@link OneHandedController}, returns {@code null} if the feature is not supported. */ - public static OneHandedController create( - Context context, WindowManager windowManager, DisplayController displayController, - DisplayLayout displayLayout, TaskStackListenerImpl taskStackListener, + public static OneHandedController create(Context context, + ShellInit shellInit, ShellCommandHandler shellCommandHandler, + ShellController shellController, WindowManager windowManager, + DisplayController displayController, DisplayLayout displayLayout, + TaskStackListenerImpl taskStackListener, InteractionJankMonitor jankMonitor, UiEventLogger uiEventLogger, ShellExecutor mainExecutor, Handler mainHandler) { OneHandedSettingsUtil settingsUtil = new OneHandedSettingsUtil(); @@ -209,16 +218,17 @@ public class OneHandedController implements RemoteCallable<OneHandedController>, context, displayLayout, settingsUtil, animationController, tutorialHandler, jankMonitor, mainExecutor); OneHandedUiEventLogger oneHandedUiEventsLogger = new OneHandedUiEventLogger(uiEventLogger); - IOverlayManager overlayManager = IOverlayManager.Stub.asInterface( - ServiceManager.getService(Context.OVERLAY_SERVICE)); - return new OneHandedController(context, displayController, organizer, touchHandler, - tutorialHandler, settingsUtil, accessibilityUtil, timeoutHandler, oneHandedState, - jankMonitor, oneHandedUiEventsLogger, overlayManager, taskStackListener, - mainExecutor, mainHandler); + return new OneHandedController(context, shellInit, shellCommandHandler, shellController, + displayController, organizer, touchHandler, tutorialHandler, settingsUtil, + accessibilityUtil, timeoutHandler, oneHandedState, oneHandedUiEventsLogger, + taskStackListener, mainExecutor, mainHandler); } @VisibleForTesting OneHandedController(Context context, + ShellInit shellInit, + ShellCommandHandler shellCommandHandler, + ShellController shellController, DisplayController displayController, OneHandedDisplayAreaOrganizer displayAreaOrganizer, OneHandedTouchHandler touchHandler, @@ -227,13 +237,13 @@ public class OneHandedController implements RemoteCallable<OneHandedController>, OneHandedAccessibilityUtil oneHandedAccessibilityUtil, OneHandedTimeoutHandler timeoutHandler, OneHandedState state, - InteractionJankMonitor jankMonitor, OneHandedUiEventLogger uiEventsLogger, - IOverlayManager overlayManager, TaskStackListenerImpl taskStackListener, ShellExecutor mainExecutor, Handler mainHandler) { mContext = context; + mShellCommandHandler = shellCommandHandler; + mShellController = shellController; mOneHandedSettingsUtil = settingsUtil; mOneHandedAccessibilityUtil = oneHandedAccessibilityUtil; mDisplayAreaOrganizer = displayAreaOrganizer; @@ -241,13 +251,12 @@ public class OneHandedController implements RemoteCallable<OneHandedController>, mTouchHandler = touchHandler; mState = state; mTutorialHandler = tutorialHandler; - mOverlayManager = overlayManager; mMainExecutor = mainExecutor; mMainHandler = mainHandler; mOneHandedUiEventLogger = uiEventsLogger; mTaskStackListener = taskStackListener; + mAccessibilityManager = AccessibilityManager.getInstance(mContext); - mDisplayController.addDisplayWindowListener(mDisplaysChangedListener); final float offsetPercentageConfig = context.getResources().getFraction( R.fraction.config_one_handed_offset, 1, 1); final int sysPropPercentageConfig = SystemProperties.getInt( @@ -267,6 +276,12 @@ public class OneHandedController implements RemoteCallable<OneHandedController>, getObserver(this::onSwipeToNotificationEnabledChanged); mShortcutEnabledObserver = getObserver(this::onShortcutEnabledChanged); + shellInit.addInitCallback(this::onInit, this); + } + + private void onInit() { + mShellCommandHandler.addDumpCallback(this::dump, this); + mDisplayController.addDisplayWindowListener(mDisplaysChangedListener); mDisplayController.addDisplayChangingController(this); setupCallback(); registerSettingObservers(mUserId); @@ -274,11 +289,13 @@ public class OneHandedController implements RemoteCallable<OneHandedController>, updateSettings(); updateDisplayLayout(mContext.getDisplayId()); - mAccessibilityManager = AccessibilityManager.getInstance(context); mAccessibilityManager.addAccessibilityStateChangeListener( mAccessibilityStateChangeListener); mState.addSListeners(mTutorialHandler); + mShellController.addConfigurationChangeListener(this); + mShellController.addKeyguardChangeListener(this); + mShellController.addUserChangeListener(this); } public OneHanded asOneHanded() { @@ -594,7 +611,8 @@ public class OneHandedController implements RemoteCallable<OneHandedController>, mLockedDisabled = locked && !enabled; } - private void onConfigChanged(Configuration newConfig) { + @Override + public void onConfigurationChanged(Configuration newConfig) { if (mTutorialHandler == null) { return; } @@ -604,11 +622,15 @@ public class OneHandedController implements RemoteCallable<OneHandedController>, mTutorialHandler.onConfigurationChanged(); } - private void onKeyguardVisibilityChanged(boolean showing) { - mKeyguardShowing = showing; + @Override + public void onKeyguardVisibilityChanged(boolean visible, boolean occluded, + boolean animatingDismiss) { + mKeyguardShowing = visible; + stopOneHanded(); } - private void onUserSwitch(int newUserId) { + @Override + public void onUserChanged(int newUserId, @NonNull Context userContext) { unregisterSettingObservers(); mUserId = newUserId; registerSettingObservers(newUserId); @@ -616,7 +638,7 @@ public class OneHandedController implements RemoteCallable<OneHandedController>, updateOneHandedEnabled(); } - public void dump(@NonNull PrintWriter pw) { + public void dump(@NonNull PrintWriter pw, String prefix) { final String innerPrefix = " "; pw.println(); pw.println(TAG); @@ -659,11 +681,11 @@ public class OneHandedController implements RemoteCallable<OneHandedController>, } /** - * Handles rotation based on OnDisplayChangingListener callback + * Handles display change based on OnDisplayChangingListener callback */ @Override - public void onRotateDisplay(int displayId, int fromRotation, int toRotation, - WindowContainerTransaction wct) { + public void onDisplayChange(int displayId, int fromRotation, int toRotation, + DisplayAreaInfo newDisplayAreaInfo, WindowContainerTransaction wct) { if (!isInitialized()) { return; } @@ -674,9 +696,12 @@ public class OneHandedController implements RemoteCallable<OneHandedController>, return; } + if (mState.getState() == STATE_ACTIVE) { + mOneHandedUiEventLogger.writeEvent( + OneHandedUiEventLogger.EVENT_ONE_HANDED_TRIGGER_ROTATION_OUT); + } + mDisplayAreaOrganizer.onRotateDisplay(mContext, toRotation, wct); - mOneHandedUiEventLogger.writeEvent( - OneHandedUiEventLogger.EVENT_ONE_HANDED_TRIGGER_ROTATION_OUT); } /** @@ -696,18 +721,6 @@ public class OneHandedController implements RemoteCallable<OneHandedController>, } @Override - public boolean isOneHandedEnabled() { - // This is volatile so return directly - return mIsOneHandedEnabled; - } - - @Override - public boolean isSwipeToNotificationEnabled() { - // This is volatile so return directly - return mIsSwipeToNotificationEnabled; - } - - @Override public void startOneHanded() { mMainExecutor.execute(() -> { OneHandedController.this.startOneHanded(); @@ -748,27 +761,6 @@ public class OneHandedController implements RemoteCallable<OneHandedController>, OneHandedController.this.registerTransitionCallback(callback); }); } - - @Override - public void onConfigChanged(Configuration newConfig) { - mMainExecutor.execute(() -> { - OneHandedController.this.onConfigChanged(newConfig); - }); - } - - @Override - public void onUserSwitch(int userId) { - mMainExecutor.execute(() -> { - OneHandedController.this.onUserSwitch(userId); - }); - } - - @Override - public void onKeyguardVisibilityChanged(boolean showing) { - mMainExecutor.execute(() -> { - OneHandedController.this.onKeyguardVisibilityChanged(showing); - }); - } } /** 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 f61d1b95bd85..451afa08040c 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 @@ -159,6 +159,10 @@ public class OneHandedDisplayAreaOrganizer extends DisplayAreaOrganizer { @Override public void onDisplayAreaVanished(@NonNull DisplayAreaInfo displayAreaInfo) { + final SurfaceControl leash = mDisplayAreaTokenMap.get(displayAreaInfo.token); + if (leash != null) { + leash.release(); + } mDisplayAreaTokenMap.remove(displayAreaInfo.token); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/IPip.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/IPip.aidl index e03421dd58ac..4def15db2f52 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/IPip.aidl +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/IPip.aidl @@ -37,12 +37,13 @@ interface IPip { * @param activityInfo ActivityInfo tied to the Activity * @param pictureInPictureParams PictureInPictureParams tied to the Activity * @param launcherRotation Launcher rotation to calculate the PiP destination bounds - * @param shelfHeight Shelf height of launcher to calculate the PiP destination bounds + * @param hotseatKeepClearArea Bounds of Hotseat to avoid used to calculate PiP destination + bounds * @return destination bounds the PiP window should land into */ Rect startSwipePipToHome(in ComponentName componentName, in ActivityInfo activityInfo, in PictureInPictureParams pictureInPictureParams, - int launcherRotation, int shelfHeight) = 1; + int launcherRotation, in Rect hotseatKeepClearArea) = 1; /** * Notifies the swiping Activity to PiP onto home transition is finished 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 3b3091a9caf3..c06881ae6ad7 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java @@ -16,12 +16,10 @@ package com.android.wm.shell.pip; -import android.content.res.Configuration; import android.graphics.Rect; import com.android.wm.shell.common.annotations.ExternalThread; -import java.io.PrintWriter; import java.util.function.Consumer; /** @@ -44,24 +42,6 @@ public interface Pip { } /** - * Called when configuration is changed. - */ - default void onConfigurationChanged(Configuration newConfig) { - } - - /** - * Called when display size or font size of settings changed - */ - default void onDensityOrFontScaleChanged() { - } - - /** - * Called when overlay package change invoked. - */ - default void onOverlayChanged() { - } - - /** * Called when SysUI state changed. * * @param isSysUiStateValid Is SysUI state valid or not. @@ -71,12 +51,6 @@ public interface Pip { } /** - * Registers the session listener for the current user. - */ - default void registerSessionListenerForCurrentUser() { - } - - /** * Sets both shelf visibility and its height. * * @param visible visibility of shelf. @@ -86,12 +60,12 @@ public interface Pip { } /** - * Registers the pinned stack animation listener. + * Set the callback when {@link PipTaskOrganizer#isInPip()} state is changed. * - * @param callback The callback of pinned stack animation. + * @param callback The callback accepts the result of {@link PipTaskOrganizer#isInPip()} + * when it's changed. */ - default void setPinnedStackAnimationListener(Consumer<Boolean> callback) { - } + default void setOnIsInPipStateChangedListener(Consumer<Boolean> callback) {} /** * Set the pinned stack with {@link PipAnimationController.AnimationType} @@ -118,29 +92,4 @@ public interface Pip { * view hierarchy or destroyed. */ default void removePipExclusionBoundsChangeListener(Consumer<Rect> listener) { } - - /** - * Called when the visibility of keyguard is changed. - * @param showing {@code true} if keyguard is now showing, {@code false} otherwise. - * @param animating {@code true} if system is animating between keyguard and surface behind, - * this only makes sense when showing is {@code false}. - */ - default void onKeyguardVisibilityChanged(boolean showing, boolean animating) { } - - /** - * Called when the dismissing animation keyguard and surfaces behind is finished. - * See also {@link #onKeyguardVisibilityChanged(boolean, boolean)}. - * - * TODO(b/206741900) deprecate this path once we're able to animate the PiP window as part of - * keyguard dismiss animation. - */ - default void onKeyguardDismissAnimationFinished() { } - - /** - * Dump the current state and information if need. - * - * @param pw The stream to dump information to. - */ - default void dump(PrintWriter pw) { - } } 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 4eba1697b595..6728c00af51b 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 @@ -29,7 +29,6 @@ import android.annotation.NonNull; import android.app.TaskInfo; import android.content.Context; import android.graphics.Rect; -import android.view.Choreographer; import android.view.Surface; import android.view.SurfaceControl; import android.window.TaskSnapshot; @@ -183,7 +182,7 @@ public class PipAnimationController { return mCurrentAnimator; } - PipTransitionAnimator getCurrentAnimator() { + public PipTransitionAnimator getCurrentAnimator() { return mCurrentAnimator; } @@ -196,6 +195,17 @@ public class PipAnimationController { } /** + * Returns true if the PiP window is currently being animated. + */ + public boolean isAnimating() { + PipAnimationController.PipTransitionAnimator animator = getCurrentAnimator(); + if (animator != null && animator.isRunning()) { + return true; + } + return false; + } + + /** * Quietly cancel the animator by removing the listeners first. */ static void quietCancel(@NonNull ValueAnimator animator) { @@ -279,14 +289,15 @@ public class PipAnimationController { mEndValue = endValue; addListener(this); addUpdateListener(this); - mSurfaceControlTransactionFactory = SurfaceControl.Transaction::new; + mSurfaceControlTransactionFactory = + new PipSurfaceTransactionHelper.VsyncSurfaceControlTransactionFactory(); mTransitionDirection = TRANSITION_DIRECTION_NONE; } @Override public void onAnimationStart(Animator animation) { mCurrentValue = mStartValue; - onStartTransaction(mLeash, newSurfaceControlTransaction()); + onStartTransaction(mLeash, mSurfaceControlTransactionFactory.getTransaction()); if (mPipAnimationCallback != null) { mPipAnimationCallback.onPipAnimationStart(mTaskInfo, this); } @@ -294,14 +305,16 @@ public class PipAnimationController { @Override public void onAnimationUpdate(ValueAnimator animation) { - applySurfaceControlTransaction(mLeash, newSurfaceControlTransaction(), + applySurfaceControlTransaction(mLeash, + mSurfaceControlTransactionFactory.getTransaction(), animation.getAnimatedFraction()); } @Override public void onAnimationEnd(Animator animation) { mCurrentValue = mEndValue; - final SurfaceControl.Transaction tx = newSurfaceControlTransaction(); + final SurfaceControl.Transaction tx = + mSurfaceControlTransactionFactory.getTransaction(); onEndTransaction(mLeash, tx, mTransitionDirection); if (mPipAnimationCallback != null) { mPipAnimationCallback.onPipAnimationEnd(mTaskInfo, tx, this); @@ -348,7 +361,8 @@ public class PipAnimationController { } void setColorContentOverlay(Context context) { - final SurfaceControl.Transaction tx = newSurfaceControlTransaction(); + final SurfaceControl.Transaction tx = + mSurfaceControlTransactionFactory.getTransaction(); if (mContentOverlay != null) { mContentOverlay.detach(tx); } @@ -357,7 +371,8 @@ public class PipAnimationController { } void setSnapshotContentOverlay(TaskSnapshot snapshot, Rect sourceRectHint) { - final SurfaceControl.Transaction tx = newSurfaceControlTransaction(); + final SurfaceControl.Transaction tx = + mSurfaceControlTransactionFactory.getTransaction(); if (mContentOverlay != null) { mContentOverlay.detach(tx); } @@ -406,7 +421,7 @@ public class PipAnimationController { void setDestinationBounds(Rect destinationBounds) { mDestinationBounds.set(destinationBounds); if (mAnimationType == ANIM_TYPE_ALPHA) { - onStartTransaction(mLeash, newSurfaceControlTransaction()); + onStartTransaction(mLeash, mSurfaceControlTransactionFactory.getTransaction()); } } @@ -441,16 +456,6 @@ public class PipAnimationController { mEndValue = endValue; } - /** - * @return {@link SurfaceControl.Transaction} instance with vsync-id. - */ - protected SurfaceControl.Transaction newSurfaceControlTransaction() { - final SurfaceControl.Transaction tx = - mSurfaceControlTransactionFactory.getTransaction(); - tx.setFrameTimelineVsync(Choreographer.getSfInstance().getVsyncId()); - return tx; - } - @VisibleForTesting public void setSurfaceControlTransactionFactory( PipSurfaceTransactionHelper.SurfaceControlTransactionFactory factory) { @@ -591,7 +596,7 @@ public class PipAnimationController { final Rect insets = computeInsets(fraction); getSurfaceTransactionHelper().scaleAndCrop(tx, leash, sourceHintRect, initialSourceValue, bounds, insets, - isInPipDirection); + isInPipDirection, fraction); if (shouldApplyCornerRadius()) { final Rect sourceBounds = new Rect(initialContainerRect); sourceBounds.inset(insets); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsAlgorithm.java index 7397e5273753..cd61dbb5b7d1 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsAlgorithm.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsAlgorithm.java @@ -47,6 +47,7 @@ public class PipBoundsAlgorithm { private final @NonNull PipBoundsState mPipBoundsState; private final PipSnapAlgorithm mSnapAlgorithm; + private final PipKeepClearAlgorithm mPipKeepClearAlgorithm; private float mDefaultSizePercent; private float mMinAspectRatioForMinSize; @@ -60,9 +61,11 @@ public class PipBoundsAlgorithm { protected Point mScreenEdgeInsets; public PipBoundsAlgorithm(Context context, @NonNull PipBoundsState pipBoundsState, - @NonNull PipSnapAlgorithm pipSnapAlgorithm) { + @NonNull PipSnapAlgorithm pipSnapAlgorithm, + @NonNull PipKeepClearAlgorithm pipKeepClearAlgorithm) { mPipBoundsState = pipBoundsState; mSnapAlgorithm = pipSnapAlgorithm; + mPipKeepClearAlgorithm = pipKeepClearAlgorithm; reloadResources(context); // Initialize the aspect ratio to the default aspect ratio. Don't do this in reload // resources as it would clobber mAspectRatio when entering PiP from fullscreen which @@ -129,8 +132,21 @@ public class PipBoundsAlgorithm { return getDefaultBounds(INVALID_SNAP_FRACTION, null /* size */); } - /** Returns the destination bounds to place the PIP window on entry. */ + /** + * Returns the destination bounds to place the PIP window on entry. + * If there are any keep clear areas registered, the position will try to avoid occluding them. + */ public Rect getEntryDestinationBounds() { + Rect entryBounds = getEntryDestinationBoundsIgnoringKeepClearAreas(); + Rect insets = new Rect(); + getInsetBounds(insets); + return mPipKeepClearAlgorithm.findUnoccludedPosition(entryBounds, + mPipBoundsState.getRestrictedKeepClearAreas(), + mPipBoundsState.getUnrestrictedKeepClearAreas(), insets); + } + + /** Returns the destination bounds to place the PIP window on entry. */ + public Rect getEntryDestinationBoundsIgnoringKeepClearAreas() { final PipBoundsState.PipReentryState reentryState = mPipBoundsState.getReentryState(); final Rect destinationBounds = reentryState != null @@ -138,9 +154,10 @@ public class PipBoundsAlgorithm { : getDefaultBounds(); final boolean useCurrentSize = reentryState != null && reentryState.getSize() != null; - return transformBoundsToAspectRatioIfValid(destinationBounds, + Rect aspectRatioBounds = transformBoundsToAspectRatioIfValid(destinationBounds, mPipBoundsState.getAspectRatio(), false /* useCurrentMinEdgeSize */, useCurrentSize); + return aspectRatioBounds; } /** Returns the current bounds adjusted to the new aspect ratio, if valid. */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipContentOverlay.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipContentOverlay.java index 0e32663955d3..7096a645ef85 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipContentOverlay.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipContentOverlay.java @@ -111,9 +111,6 @@ public abstract class PipContentOverlay { private final TaskSnapshot mSnapshot; private final Rect mSourceRectHint; - private float mTaskSnapshotScaleX; - private float mTaskSnapshotScaleY; - public PipSnapshotOverlay(TaskSnapshot snapshot, Rect sourceRectHint) { mSnapshot = snapshot; mSourceRectHint = new Rect(sourceRectHint); @@ -125,16 +122,16 @@ public abstract class PipContentOverlay { @Override public void attach(SurfaceControl.Transaction tx, SurfaceControl parentLeash) { - mTaskSnapshotScaleX = (float) mSnapshot.getTaskSize().x + final float taskSnapshotScaleX = (float) mSnapshot.getTaskSize().x / mSnapshot.getHardwareBuffer().getWidth(); - mTaskSnapshotScaleY = (float) mSnapshot.getTaskSize().y + final float taskSnapshotScaleY = (float) mSnapshot.getTaskSize().y / mSnapshot.getHardwareBuffer().getHeight(); tx.show(mLeash); tx.setLayer(mLeash, Integer.MAX_VALUE); tx.setBuffer(mLeash, mSnapshot.getHardwareBuffer()); // Relocate the content to parentLeash's coordinates. tx.setPosition(mLeash, -mSourceRectHint.left, -mSourceRectHint.top); - tx.setScale(mLeash, mTaskSnapshotScaleX, mTaskSnapshotScaleY); + tx.setScale(mLeash, taskSnapshotScaleX, taskSnapshotScaleY); tx.reparent(mLeash, parentLeash); tx.apply(); } @@ -146,20 +143,6 @@ public abstract class PipContentOverlay { @Override public void onAnimationEnd(SurfaceControl.Transaction atomicTx, Rect destinationBounds) { - // Work around to make sure the snapshot overlay is aligned with PiP window before - // the atomicTx is committed along with the final WindowContainerTransaction. - final SurfaceControl.Transaction nonAtomicTx = new SurfaceControl.Transaction(); - final float scaleX = (float) destinationBounds.width() - / mSourceRectHint.width(); - final float scaleY = (float) destinationBounds.height() - / mSourceRectHint.height(); - final float scale = Math.max( - scaleX * mTaskSnapshotScaleX, scaleY * mTaskSnapshotScaleY); - nonAtomicTx.setScale(mLeash, scale, scale); - nonAtomicTx.setPosition(mLeash, - -scale * mSourceRectHint.left / mTaskSnapshotScaleX, - -scale * mSourceRectHint.top / mTaskSnapshotScaleY); - nonAtomicTx.apply(); atomicTx.remove(mLeash); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipKeepClearAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipKeepClearAlgorithm.java new file mode 100644 index 000000000000..e3495e100c62 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipKeepClearAlgorithm.java @@ -0,0 +1,53 @@ +/* + * 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.pip; + +import android.graphics.Rect; + +import java.util.Set; + +/** + * Interface for interacting with keep clear algorithm used to move PiP window out of the way of + * keep clear areas. + */ +public interface PipKeepClearAlgorithm { + + /** + * Adjust the position of picture in picture window based on the registered keep clear areas. + * @param pipBoundsState state of the PiP to use for the calculations + * @param pipBoundsAlgorithm algorithm implementation used to get the entry destination bounds + * @return + */ + default Rect adjust(PipBoundsState pipBoundsState, PipBoundsAlgorithm pipBoundsAlgorithm) { + return pipBoundsState.getBounds(); + } + + /** + * Calculate the bounds so that none of the keep clear areas are occluded, while the bounds stay + * within the allowed bounds. If such position is not feasible, return original bounds. + * @param defaultBounds initial bounds used in the calculation + * @param restrictedKeepClearAreas registered restricted keep clear areas + * @param unrestrictedKeepClearAreas registered unrestricted keep clear areas + * @param allowedBounds bounds that define the allowed space for the output, result will always + * be inside those bounds + * @return bounds that don't cover any of the keep clear areas and are within allowed bounds + */ + default Rect findUnoccludedPosition(Rect defaultBounds, Set<Rect> restrictedKeepClearAreas, + Set<Rect> unrestrictedKeepClearAreas, Rect allowedBounds) { + return defaultBounds; + } +} 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 a017a2674359..b9746e338ced 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 @@ -20,6 +20,7 @@ import android.content.Context; import android.graphics.Matrix; import android.graphics.Rect; import android.graphics.RectF; +import android.view.Choreographer; import android.view.SurfaceControl; import com.android.wm.shell.R; @@ -39,6 +40,10 @@ public class PipSurfaceTransactionHelper { private int mCornerRadius; private int mShadowRadius; + public PipSurfaceTransactionHelper(Context context) { + onDensityOrFontScaleChanged(context); + } + /** * Called when display size or font size of settings changed * @@ -104,7 +109,7 @@ public class PipSurfaceTransactionHelper { public PipSurfaceTransactionHelper scaleAndCrop(SurfaceControl.Transaction tx, SurfaceControl leash, Rect sourceRectHint, Rect sourceBounds, Rect destinationBounds, Rect insets, - boolean isInPipDirection) { + boolean isInPipDirection, float fraction) { mTmpDestinationRect.set(sourceBounds); // Similar to {@link #scale}, we want to position the surface relative to the screen // coordinates so offset the bounds to 0,0 @@ -116,9 +121,13 @@ public class PipSurfaceTransactionHelper { if (isInPipDirection && sourceRectHint != null && sourceRectHint.width() < sourceBounds.width()) { // scale by sourceRectHint if it's not edge-to-edge, for entering PiP transition only. - scale = sourceBounds.width() <= sourceBounds.height() + final float endScale = sourceBounds.width() <= sourceBounds.height() ? (float) destinationBounds.width() / sourceRectHint.width() : (float) destinationBounds.height() / sourceRectHint.height(); + final float startScale = sourceBounds.width() <= sourceBounds.height() + ? (float) destinationBounds.width() / sourceBounds.width() + : (float) destinationBounds.height() / sourceBounds.height(); + scale = (1 - fraction) * startScale + fraction * endScale; } else { scale = sourceBounds.width() <= sourceBounds.height() ? (float) destinationBounds.width() / sourceBounds.width() @@ -226,4 +235,18 @@ public class PipSurfaceTransactionHelper { public interface SurfaceControlTransactionFactory { SurfaceControl.Transaction getTransaction(); } + + /** + * Implementation of {@link SurfaceControlTransactionFactory} that returns + * {@link SurfaceControl.Transaction} with VsyncId being set. + */ + public static class VsyncSurfaceControlTransactionFactory + implements SurfaceControlTransactionFactory { + @Override + public SurfaceControl.Transaction getTransaction() { + final SurfaceControl.Transaction tx = new SurfaceControl.Transaction(); + tx.setFrameTimelineVsync(Choreographer.getInstance().getVsyncId()); + return tx; + } + } } 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 e624de661737..f170e774739f 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 @@ -62,6 +62,7 @@ import android.content.res.Configuration; import android.graphics.Rect; import android.os.RemoteException; import android.os.SystemClock; +import android.util.Log; import android.view.Display; import android.view.Surface; import android.view.SurfaceControl; @@ -126,7 +127,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, private final PipBoundsAlgorithm mPipBoundsAlgorithm; private final @NonNull PipMenuController mPipMenuController; private final PipAnimationController mPipAnimationController; - private final PipTransitionController mPipTransitionController; + protected final PipTransitionController mPipTransitionController; protected final PipParamsChangedForwarder mPipParamsChangedForwarder; private final PipUiEventLogger mPipUiEventLoggerLogger; private final int mEnterAnimationDuration; @@ -195,6 +196,26 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, } }; + @VisibleForTesting + final PipTransitionController.PipTransitionCallback mPipTransitionCallback = + new PipTransitionController.PipTransitionCallback() { + @Override + public void onPipTransitionStarted(int direction, Rect pipBounds) {} + + @Override + public void onPipTransitionFinished(int direction) { + // Apply the deferred RunningTaskInfo if applicable after all proper callbacks + // are sent. + if (direction == TRANSITION_DIRECTION_TO_PIP && mDeferredTaskInfo != null) { + onTaskInfoChanged(mDeferredTaskInfo); + mDeferredTaskInfo = null; + } + } + + @Override + public void onPipTransitionCanceled(int direction) {} + }; + private final PipAnimationController.PipTransactionHandler mPipTransactionHandler = new PipAnimationController.PipTransactionHandler() { @Override @@ -215,7 +236,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, private ActivityManager.RunningTaskInfo mDeferredTaskInfo; private WindowContainerToken mToken; private SurfaceControl mLeash; - private PipTransitionState mPipTransitionState; + protected PipTransitionState mPipTransitionState; private @PipAnimationController.AnimationType int mOneShotAnimationType = ANIM_TYPE_BOUNDS; private long mLastOneShotAlphaAnimationTime; private PipSurfaceTransactionHelper.SurfaceControlTransactionFactory @@ -283,7 +304,8 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, mSurfaceTransactionHelper = surfaceTransactionHelper; mPipAnimationController = pipAnimationController; mPipUiEventLoggerLogger = pipUiEventLogger; - mSurfaceControlTransactionFactory = SurfaceControl.Transaction::new; + mSurfaceControlTransactionFactory = + new PipSurfaceTransactionHelper.VsyncSurfaceControlTransactionFactory(); mSplitScreenOptional = splitScreenOptional; mTaskOrganizer = shellTaskOrganizer; mMainExecutor = mainExecutor; @@ -295,6 +317,11 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, mTaskOrganizer.addFocusListener(this); mPipTransitionController.setPipOrganizer(this); displayController.addDisplayWindowListener(this); + pipTransitionController.registerPipTransitionCallback(mPipTransitionCallback); + } + + public PipTransitionController getTransitionController() { + return mPipTransitionController; } public Rect getCurrentOrAnimatingBounds() { @@ -440,7 +467,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, // When exit to fullscreen with Shell transition enabled, we update the Task windowing // mode directly so that it can also trigger display rotation and visibility update in // the same transition if there will be any. - wct.setWindowingMode(mToken, WINDOWING_MODE_UNDEFINED); + wct.setWindowingMode(mToken, getOutPipWindowingMode()); // We can inherit the parent bounds as it is going to be fullscreen. The // destinationBounds calculated above will be incorrect if this is with rotation. wct.setBounds(mToken, null); @@ -458,7 +485,8 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, } // Cancel the existing animator if there is any. - cancelCurrentAnimator(); + // TODO(b/232439933): this is disabled temporarily to unblock b/234502692. + // cancelCurrentAnimator(); // Set the exiting state first so if there is fixed rotation later, the running animation // won't be interrupted by alpha animation for existing PiP. @@ -538,7 +566,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, if (Transitions.ENABLE_SHELL_TRANSITIONS) { final WindowContainerTransaction wct = new WindowContainerTransaction(); wct.setBounds(mToken, null); - wct.setWindowingMode(mToken, WINDOWING_MODE_UNDEFINED); + wct.setWindowingMode(mToken, getOutPipWindowingMode()); wct.reorder(mToken, false); mPipTransitionController.startExitTransition(TRANSIT_REMOVE_PIP, wct, null /* destinationBounds */); @@ -767,11 +795,6 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, mPipTransitionState.setTransitionState(PipTransitionState.ENTERED_PIP); } mPipTransitionController.sendOnPipTransitionFinished(direction); - // Apply the deferred RunningTaskInfo if applicable after all proper callbacks are sent. - if (direction == TRANSITION_DIRECTION_TO_PIP && mDeferredTaskInfo != null) { - onTaskInfoChanged(mDeferredTaskInfo); - mDeferredTaskInfo = null; - } } private void sendOnPipTransitionCancelled( @@ -925,6 +948,12 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, /** Called when exiting PIP transition is finished to do the state cleanup. */ void onExitPipFinished(TaskInfo info) { + if (mLeash == null) { + // TODO(239461594): Remove once the double call to onExitPipFinished() is fixed + Log.w(TAG, "Warning, onExitPipFinished() called multiple times in the same sessino"); + return; + } + clearWaitForFixedRotation(); if (mSwipePipToHomeOverlay != null) { removeContentOverlay(mSwipePipToHomeOverlay, null /* callback */); @@ -938,6 +967,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, mPipBoundsState.setBounds(new Rect()); mPipUiEventLoggerLogger.setTaskInfo(null); mPipMenuController.detach(); + mLeash = null; if (info.displayId != Display.DEFAULT_DISPLAY && mOnDisplayIdChangeCallback != null) { mOnDisplayIdChangeCallback.accept(Display.DEFAULT_DISPLAY); @@ -1276,6 +1306,12 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, return; } + if (mLeash == null || !mLeash.isValid()) { + Log.e(TAG, String.format("scheduleFinishResizePip with null leash! mState=%d", + mPipTransitionState.getTransitionState())); + return; + } + finishResize(createFinishResizeSurfaceTransaction(destinationBounds), destinationBounds, direction, -1); if (updateBoundsCallback != null) { @@ -1467,6 +1503,11 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, "%s: Abort animation, invalid leash", TAG); return null; } + if (isInPipDirection(direction) + && !isSourceRectHintValidForEnterPip(sourceHintRect, destinationBounds)) { + // The given source rect hint is too small for enter PiP animation, reset it to null. + sourceHintRect = null; + } final int rotationDelta = mWaitForFixedRotation ? deltaRotation(mCurrentRotation, mNextRotation) : Surface.ROTATION_0; @@ -1541,6 +1582,20 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, } /** + * This is a situation in which the source rect hint on at least one axis is smaller + * than the destination bounds, which represents a problem because we would have to scale + * up that axis to fit the bounds. So instead, just fallback to the non-source hint + * animation in this case. + * + * @return {@code false} if the given source is too small to use for the entering animation. + */ + private boolean isSourceRectHintValidForEnterPip(Rect sourceRectHint, Rect destinationBounds) { + return sourceRectHint != null + && sourceRectHint.width() > destinationBounds.width() + && sourceRectHint.height() > destinationBounds.height(); + } + + /** * Sync with {@link SplitScreenController} on destination bounds if PiP is going to * split screen. * 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 36e712459863..33761d23379d 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 @@ -28,7 +28,6 @@ import static android.view.WindowManager.TRANSIT_OPEN; import static android.view.WindowManager.TRANSIT_PIP; import static android.view.WindowManager.transitTypeToString; import static android.window.TransitionInfo.FLAG_IS_DISPLAY; -import static android.window.TransitionInfo.FLAG_IS_WALLPAPER; import static com.android.wm.shell.pip.PipAnimationController.ANIM_TYPE_ALPHA; import static com.android.wm.shell.pip.PipAnimationController.ANIM_TYPE_BOUNDS; @@ -43,6 +42,7 @@ import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP_TO_SP import static com.android.wm.shell.transition.Transitions.TRANSIT_REMOVE_PIP; import static com.android.wm.shell.transition.Transitions.isOpeningType; +import android.animation.Animator; import android.app.ActivityManager; import android.app.TaskInfo; import android.content.Context; @@ -52,6 +52,7 @@ import android.graphics.Rect; import android.os.IBinder; import android.view.Surface; import android.view.SurfaceControl; +import android.view.WindowManager; import android.window.TransitionInfo; import android.window.TransitionRequestInfo; import android.window.WindowContainerToken; @@ -65,6 +66,7 @@ import com.android.wm.shell.R; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.splitscreen.SplitScreenController; +import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.transition.CounterRotatorHelper; import com.android.wm.shell.transition.Transitions; @@ -106,17 +108,18 @@ public class PipTransition extends PipTransitionController { private boolean mHasFadeOut; public PipTransition(Context context, + @NonNull ShellInit shellInit, + @NonNull ShellTaskOrganizer shellTaskOrganizer, + @NonNull Transitions transitions, PipBoundsState pipBoundsState, PipTransitionState pipTransitionState, PipMenuController pipMenuController, PipBoundsAlgorithm pipBoundsAlgorithm, PipAnimationController pipAnimationController, - Transitions transitions, - @NonNull ShellTaskOrganizer shellTaskOrganizer, PipSurfaceTransactionHelper pipSurfaceTransactionHelper, Optional<SplitScreenController> splitScreenOptional) { - super(pipBoundsState, pipMenuController, pipBoundsAlgorithm, - pipAnimationController, transitions, shellTaskOrganizer); + super(shellInit, shellTaskOrganizer, transitions, pipBoundsState, pipMenuController, + pipBoundsAlgorithm, pipAnimationController); mContext = context; mPipTransitionState = pipTransitionState; mEnterExitAnimationDuration = context.getResources() @@ -145,6 +148,11 @@ public class PipTransition extends PipTransitionController { if (destinationBounds != null) { mExitDestinationBounds.set(destinationBounds); } + final PipAnimationController.PipTransitionAnimator animator = + mPipAnimationController.getCurrentAnimator(); + if (animator != null && animator.isRunning()) { + animator.cancel(); + } mExitTransition = mTransitions.startTransition(type, out, this); } @@ -217,13 +225,20 @@ public class PipTransition extends PipTransitionController { } // Entering PIP. - if (isEnteringPip(info, mCurrentPipTaskToken)) { - return startEnterAnimation(info, startTransaction, finishTransaction, finishCallback); + if (isEnteringPip(info)) { + startEnterAnimation(info, startTransaction, finishTransaction, finishCallback); + return true; } // For transition that we don't animate, but contains the PIP leash, we need to update the // PIP surface, otherwise it will be reset after the transition. if (currentPipTaskChange != null) { + // Set the "end" bounds of pip. The default setup uses the start bounds. Since this is + // changing the *finish*Transaction, we need to use the end bounds. This will also + // make sure that the fade-in animation (below) uses the end bounds as well. + if (!currentPipTaskChange.getEndAbsBounds().isEmpty()) { + mPipBoundsState.setBounds(currentPipTaskChange.getEndAbsBounds()); + } updatePipForUnhandledTransition(currentPipTaskChange, startTransaction, finishTransaction); } @@ -236,6 +251,13 @@ public class PipTransition extends PipTransitionController { return false; } + @Override + public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget, + @NonNull Transitions.TransitionFinishCallback finishCallback) { + end(); + } + /** Helper to identify whether this handler is currently the one playing an animation */ private boolean isAnimatingLocally() { return mFinishTransaction != null; @@ -245,16 +267,9 @@ public class PipTransition extends PipTransitionController { @Override public WindowContainerTransaction handleRequest(@NonNull IBinder transition, @NonNull TransitionRequestInfo request) { - if (request.getType() == TRANSIT_PIP) { + if (requestHasPipEnter(request)) { WindowContainerTransaction wct = new WindowContainerTransaction(); - if (mOneShotAnimationType == ANIM_TYPE_ALPHA) { - mRequestedEnterTransition = transition; - mRequestedEnterTask = request.getTriggerTask().token; - wct.setActivityWindowingMode(request.getTriggerTask().token, - WINDOWING_MODE_UNDEFINED); - final Rect destinationBounds = mPipBoundsAlgorithm.getEntryDestinationBounds(); - wct.setBounds(request.getTriggerTask().token, destinationBounds); - } + augmentRequest(transition, request, wct); return wct; } else { return null; @@ -262,6 +277,29 @@ public class PipTransition extends PipTransitionController { } @Override + public void augmentRequest(@NonNull IBinder transition, + @NonNull TransitionRequestInfo request, @NonNull WindowContainerTransaction outWCT) { + if (!requestHasPipEnter(request)) { + throw new IllegalStateException("Called PiP augmentRequest when request has no PiP"); + } + if (mOneShotAnimationType == ANIM_TYPE_ALPHA) { + mRequestedEnterTransition = transition; + mRequestedEnterTask = request.getTriggerTask().token; + outWCT.setActivityWindowingMode(request.getTriggerTask().token, + WINDOWING_MODE_UNDEFINED); + final Rect destinationBounds = mPipBoundsAlgorithm.getEntryDestinationBounds(); + outWCT.setBounds(request.getTriggerTask().token, destinationBounds); + } + } + + @Override + public void end() { + Animator animator = mPipAnimationController.getCurrentAnimator(); + if (animator == null) return; + animator.end(); + } + + @Override public boolean handleRotateDisplay(int startRotation, int endRotation, WindowContainerTransaction wct) { if (mRequestedEnterTransition != null && mOneShotAnimationType == ANIM_TYPE_ALPHA) { @@ -279,7 +317,8 @@ public class PipTransition extends PipTransitionController { } @Override - public void onTransitionMerged(@NonNull IBinder transition) { + public void onTransitionConsumed(@NonNull IBinder transition, boolean aborted, + @Nullable SurfaceControl.Transaction finishT) { if (transition != mExitTransition) { return; } @@ -292,7 +331,7 @@ public class PipTransition extends PipTransitionController { } // Unset exitTransition AFTER cancel so that finishResize knows we are merging. mExitTransition = null; - if (!cancelled) return; + if (!cancelled || aborted) return; final ActivityManager.RunningTaskInfo taskInfo = mPipOrganizer.getTaskInfo(); if (taskInfo != null) { startExpandAnimation(taskInfo, mPipOrganizer.getSurfaceControl(), @@ -315,11 +354,27 @@ public class PipTransition extends PipTransitionController { // (likely a remote like launcher), so don't fire the finish-callback here -- wait until // the exit transition is merged. if ((mExitTransition == null || isAnimatingLocally()) && mFinishCallback != null) { - WindowContainerTransaction wct = new WindowContainerTransaction(); - prepareFinishResizeTransaction(taskInfo, destinationBounds, - direction, wct); - if (tx != null) { - wct.setBoundsChangeTransaction(taskInfo.token, tx); + WindowContainerTransaction wct = null; + if (isOutPipDirection(direction)) { + // Only need to reset surface properties. The server-side operations were already + // done at the start. + if (tx != null) { + mFinishTransaction.merge(tx); + } + } else { + wct = new WindowContainerTransaction(); + if (isInPipDirection(direction)) { + // If we are animating from fullscreen using a bounds animation, then reset the + // activity windowing mode, and set the task bounds to the final bounds + wct.setActivityWindowingMode(taskInfo.token, WINDOWING_MODE_UNDEFINED); + wct.scheduleFinishEnterPip(taskInfo.token, destinationBounds); + wct.setBounds(taskInfo.token, destinationBounds); + } else { + wct.setBounds(taskInfo.token, null /* bounds */); + } + if (tx != null) { + wct.setBoundsChangeTransaction(taskInfo.token, tx); + } } final SurfaceControl leash = mPipOrganizer.getSurfaceControl(); final int displayRotation = taskInfo.getConfiguration().windowConfiguration @@ -559,92 +614,94 @@ public class PipTransition extends PipTransitionController { } /** Whether we should handle the given {@link TransitionInfo} animation as entering PIP. */ - private static boolean isEnteringPip(@NonNull TransitionInfo info, - @Nullable WindowContainerToken currentPipTaskToken) { + private boolean isEnteringPip(@NonNull TransitionInfo info) { for (int i = info.getChanges().size() - 1; i >= 0; --i) { final TransitionInfo.Change change = info.getChanges().get(i); - if (change.getTaskInfo() != null - && change.getTaskInfo().getWindowingMode() == WINDOWING_MODE_PINNED - && !change.getContainer().equals(currentPipTaskToken)) { - // We support TRANSIT_PIP type (from RootWindowContainer) or TRANSIT_OPEN (from apps - // that enter PiP instantly on opening, mostly from CTS/Flicker tests) - if (info.getType() == TRANSIT_PIP || info.getType() == TRANSIT_OPEN) { - return true; - } - // This can happen if the request to enter PIP happens when we are collecting for - // another transition, such as TRANSIT_CHANGE (display rotation). - if (info.getType() == TRANSIT_CHANGE) { - return true; - } + if (isEnteringPip(change, info.getType())) return true; + } + return false; + } - // Please file a bug to handle the unexpected transition type. - throw new IllegalStateException("Entering PIP with unexpected transition type=" - + transitTypeToString(info.getType())); + /** Whether a particular change is a window that is entering pip. */ + @Override + public boolean isEnteringPip(@NonNull TransitionInfo.Change change, + @WindowManager.TransitionType int transitType) { + if (change.getTaskInfo() != null + && change.getTaskInfo().getWindowingMode() == WINDOWING_MODE_PINNED + && !change.getContainer().equals(mCurrentPipTaskToken)) { + // We support TRANSIT_PIP type (from RootWindowContainer) or TRANSIT_OPEN (from apps + // that enter PiP instantly on opening, mostly from CTS/Flicker tests) + if (transitType == TRANSIT_PIP || transitType == TRANSIT_OPEN) { + return true; } + // This can happen if the request to enter PIP happens when we are collecting for + // another transition, such as TRANSIT_CHANGE (display rotation). + if (transitType == TRANSIT_CHANGE) { + return true; + } + + // Please file a bug to handle the unexpected transition type. + throw new IllegalStateException("Entering PIP with unexpected transition type=" + + transitTypeToString(transitType)); } return false; } - private boolean startEnterAnimation(@NonNull TransitionInfo info, + private void startEnterAnimation(@NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction, @NonNull SurfaceControl.Transaction finishTransaction, @NonNull Transitions.TransitionFinishCallback finishCallback) { - // Search for an Enter PiP transition (along with a show wallpaper one) + // Search for an Enter PiP transition TransitionInfo.Change enterPip = null; - TransitionInfo.Change wallpaper = null; for (int i = info.getChanges().size() - 1; i >= 0; --i) { final TransitionInfo.Change change = info.getChanges().get(i); if (change.getTaskInfo() != null && change.getTaskInfo().getWindowingMode() == WINDOWING_MODE_PINNED) { enterPip = change; - } else if ((change.getFlags() & FLAG_IS_WALLPAPER) != 0) { - wallpaper = change; } } if (enterPip == null) { - return false; + throw new IllegalStateException("Trying to start PiP animation without a pip" + + "participant"); } - // Keep track of the PIP task. - mCurrentPipTaskToken = enterPip.getContainer(); - mHasFadeOut = false; - if (mFinishCallback != null) { - callFinishCallback(null /* wct */); - mFinishTransaction = null; - throw new RuntimeException("Previous callback not called, aborting entering PIP."); - } - - // Show the wallpaper if there is a wallpaper change. - if (wallpaper != null) { - startTransaction.show(wallpaper.getLeash()); - startTransaction.setAlpha(wallpaper.getLeash(), 1.f); - } // Make sure other open changes are visible as entering PIP. Some may be hidden in // Transitions#setupStartState because the transition type is OPEN (such as auto-enter). for (int i = info.getChanges().size() - 1; i >= 0; --i) { final TransitionInfo.Change change = info.getChanges().get(i); - if (change == enterPip || change == wallpaper) { - continue; - } + if (change == enterPip) continue; if (isOpeningType(change.getMode())) { final SurfaceControl leash = change.getLeash(); startTransaction.show(leash).setAlpha(leash, 1.f); } } + startEnterAnimation(enterPip, startTransaction, finishTransaction, finishCallback); + } + + @Override + public void startEnterAnimation(@NonNull final TransitionInfo.Change pipChange, + @NonNull final SurfaceControl.Transaction startTransaction, + @NonNull final SurfaceControl.Transaction finishTransaction, + @NonNull final Transitions.TransitionFinishCallback finishCallback) { + if (mFinishCallback != null) { + callFinishCallback(null /* wct */); + mFinishTransaction = null; + throw new RuntimeException("Previous callback not called, aborting entering PIP."); + } + + // Keep track of the PIP task and animation. + mCurrentPipTaskToken = pipChange.getContainer(); + mHasFadeOut = false; mPipTransitionState.setTransitionState(PipTransitionState.ENTERING_PIP); mFinishCallback = finishCallback; mFinishTransaction = finishTransaction; - final int endRotation = mInFixedRotation ? mEndFixedRotation : enterPip.getEndRotation(); - return startEnterAnimation(enterPip.getTaskInfo(), enterPip.getLeash(), - startTransaction, finishTransaction, enterPip.getStartRotation(), - endRotation); - } - private boolean startEnterAnimation(final TaskInfo taskInfo, final SurfaceControl leash, - final SurfaceControl.Transaction startTransaction, - final SurfaceControl.Transaction finishTransaction, - final int startRotation, final int endRotation) { + final ActivityManager.RunningTaskInfo taskInfo = pipChange.getTaskInfo(); + final SurfaceControl leash = pipChange.getLeash(); + final int startRotation = pipChange.getStartRotation(); + final int endRotation = mInFixedRotation ? mEndFixedRotation : pipChange.getEndRotation(); + setBoundsStateForEntry(taskInfo.topActivity, taskInfo.pictureInPictureParams, taskInfo.topActivityInfo); final Rect destinationBounds = mPipBoundsAlgorithm.getEntryDestinationBounds(); @@ -657,12 +714,11 @@ public class PipTransition extends PipTransitionController { computeEnterPipRotatedBounds(rotationDelta, startRotation, endRotation, taskInfo, destinationBounds, sourceHintRect); } - PipAnimationController.PipTransitionAnimator animator; // Set corner radius for entering pip. mSurfaceTransactionHelper .crop(finishTransaction, leash, destinationBounds) .round(finishTransaction, leash, true /* applyCornerRadius */); - mPipMenuController.attach(leash); + mTransitions.getMainExecutor().executeDelayed(() -> mPipMenuController.attach(leash), 0); if (taskInfo.pictureInPictureParams != null && taskInfo.pictureInPictureParams.isAutoEnterEnabled() @@ -694,7 +750,7 @@ public class PipTransition extends PipTransitionController { null /* callback */, false /* withStartDelay */); } mPipTransitionState.setInSwipePipToHomeTransition(false); - return true; + return; } if (rotationDelta != Surface.ROTATION_0) { @@ -702,6 +758,12 @@ public class PipTransition extends PipTransitionController { tmpTransform.postRotate(rotationDelta); startTransaction.setMatrix(leash, tmpTransform, new float[9]); } + if (mOneShotAnimationType == ANIM_TYPE_ALPHA) { + startTransaction.setAlpha(leash, 0f); + } + startTransaction.apply(); + + PipAnimationController.PipTransitionAnimator animator; if (mOneShotAnimationType == ANIM_TYPE_BOUNDS) { animator = mPipAnimationController.getAnimator(taskInfo, leash, currentBounds, currentBounds, destinationBounds, sourceHintRect, TRANSITION_DIRECTION_TO_PIP, @@ -712,7 +774,6 @@ public class PipTransition extends PipTransitionController { animator.setColorContentOverlay(mContext); } } else if (mOneShotAnimationType == ANIM_TYPE_ALPHA) { - startTransaction.setAlpha(leash, 0f); animator = mPipAnimationController.getAnimator(taskInfo, leash, destinationBounds, 0f, 1f); mOneShotAnimationType = ANIM_TYPE_BOUNDS; @@ -720,7 +781,6 @@ public class PipTransition extends PipTransitionController { throw new RuntimeException("Unrecognized animation type: " + mOneShotAnimationType); } - startTransaction.apply(); animator.setTransitionDirection(TRANSITION_DIRECTION_TO_PIP) .setPipAnimationCallback(mPipAnimationCallback) .setDuration(mEnterExitAnimationDuration); @@ -731,8 +791,6 @@ public class PipTransition extends PipTransitionController { animator.setDestinationBounds(mPipBoundsAlgorithm.getEntryDestinationBounds()); } animator.start(); - - return true; } /** Computes destination bounds in old rotation and updates source hint rect if available. */ @@ -852,27 +910,4 @@ public class PipTransition extends PipTransitionController { mPipMenuController.movePipMenu(null, null, destinationBounds); mPipMenuController.updateMenuBounds(destinationBounds); } - - private void prepareFinishResizeTransaction(TaskInfo taskInfo, Rect destinationBounds, - @PipAnimationController.TransitionDirection int direction, - WindowContainerTransaction wct) { - Rect taskBounds = null; - if (isInPipDirection(direction)) { - // If we are animating from fullscreen using a bounds animation, then reset the - // activity windowing mode set by WM, and set the task bounds to the final bounds - taskBounds = destinationBounds; - wct.setActivityWindowingMode(taskInfo.token, WINDOWING_MODE_UNDEFINED); - wct.scheduleFinishEnterPip(taskInfo.token, destinationBounds); - } else if (isOutPipDirection(direction)) { - // If we are animating to fullscreen, then we need to reset the override bounds - // on the task to ensure that the task "matches" the parent's bounds. - taskBounds = (direction == TRANSITION_DIRECTION_LEAVE_PIP) - ? null : destinationBounds; - wct.setWindowingMode(taskInfo.token, getOutPipWindowingMode()); - // Simply reset the activity mode set prior to the animation running. - wct.setActivityWindowingMode(taskInfo.token, WINDOWING_MODE_UNDEFINED); - } - - wct.setBounds(taskInfo.token, taskBounds); - } } 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 54f46e0c9938..f51e247fe112 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 @@ -17,6 +17,7 @@ package com.android.wm.shell.pip; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; +import static android.view.WindowManager.TRANSIT_PIP; import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_REMOVE_STACK; import static com.android.wm.shell.pip.PipAnimationController.isInPipDirection; @@ -27,12 +28,17 @@ import android.app.TaskInfo; import android.content.ComponentName; import android.content.pm.ActivityInfo; import android.graphics.Rect; -import android.os.Handler; -import android.os.Looper; +import android.os.IBinder; import android.view.SurfaceControl; +import android.view.WindowManager; +import android.window.TransitionInfo; +import android.window.TransitionRequestInfo; import android.window.WindowContainerTransaction; +import androidx.annotation.NonNull; + import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.transition.Transitions; import java.util.ArrayList; @@ -49,7 +55,6 @@ public abstract class PipTransitionController implements Transitions.TransitionH protected final ShellTaskOrganizer mShellTaskOrganizer; protected final PipMenuController mPipMenuController; protected final Transitions mTransitions; - private final Handler mMainHandler; private final List<PipTransitionCallback> mPipTransitionCallbacks = new ArrayList<>(); protected PipTaskOrganizer mPipOrganizer; @@ -127,22 +132,28 @@ public abstract class PipTransitionController implements Transitions.TransitionH public void onFixedRotationStarted() { } - public PipTransitionController(PipBoundsState pipBoundsState, + public PipTransitionController( + @NonNull ShellInit shellInit, + @NonNull ShellTaskOrganizer shellTaskOrganizer, + @NonNull Transitions transitions, + PipBoundsState pipBoundsState, PipMenuController pipMenuController, PipBoundsAlgorithm pipBoundsAlgorithm, - PipAnimationController pipAnimationController, Transitions transitions, - @android.annotation.NonNull ShellTaskOrganizer shellTaskOrganizer) { + PipAnimationController pipAnimationController) { mPipBoundsState = pipBoundsState; mPipMenuController = pipMenuController; mShellTaskOrganizer = shellTaskOrganizer; mPipBoundsAlgorithm = pipBoundsAlgorithm; mPipAnimationController = pipAnimationController; mTransitions = transitions; - mMainHandler = new Handler(Looper.getMainLooper()); if (Transitions.ENABLE_SHELL_TRANSITIONS) { - transitions.addHandler(this); + shellInit.addInitCallback(this::onInit, this); } } + private void onInit() { + mTransitions.addHandler(this); + } + void setPipOrganizer(PipTaskOrganizer pto) { mPipOrganizer = pto; } @@ -206,6 +217,34 @@ public abstract class PipTransitionController implements Transitions.TransitionH return false; } + /** @return whether the transition-request represents a pip-entry. */ + public boolean requestHasPipEnter(@NonNull TransitionRequestInfo request) { + return request.getType() == TRANSIT_PIP; + } + + /** Whether a particular change is a window that is entering pip. */ + public boolean isEnteringPip(@NonNull TransitionInfo.Change change, + @WindowManager.TransitionType int transitType) { + return false; + } + + /** Add PiP-related changes to `outWCT` for the given request. */ + public void augmentRequest(@NonNull IBinder transition, + @NonNull TransitionRequestInfo request, @NonNull WindowContainerTransaction outWCT) { + throw new IllegalStateException("Request isn't entering PiP"); + } + + /** Play a transition animation for entering PiP on a specific PiP change. */ + public void startEnterAnimation(@NonNull final TransitionInfo.Change pipChange, + @NonNull final SurfaceControl.Transaction startTransaction, + @NonNull final SurfaceControl.Transaction finishTransaction, + @NonNull final Transitions.TransitionFinishCallback finishCallback) { + } + + /** End the currently-playing PiP animation. */ + public void end() { + } + /** * Callback interface for PiP transitions (both from and to PiP mode) */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionState.java index 85e56b7dd99f..c6b5ce93fd35 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionState.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionState.java @@ -17,12 +17,15 @@ package com.android.wm.shell.pip; import android.annotation.IntDef; +import android.annotation.NonNull; import android.app.PictureInPictureParams; import android.content.ComponentName; import android.content.pm.ActivityInfo; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.List; /** * Used to keep track of PiP leash state as it appears and animates by {@link PipTaskOrganizer} and @@ -37,6 +40,9 @@ public class PipTransitionState { public static final int ENTERED_PIP = 4; public static final int EXITING_PIP = 5; + private final List<OnPipTransitionStateChangedListener> mOnPipTransitionStateChangedListeners = + new ArrayList<>(); + /** * If set to {@code true}, no entering PiP transition would be kicked off and most likely * it's due to the fact that Launcher is handling the transition directly when swiping @@ -65,7 +71,13 @@ public class PipTransitionState { } public void setTransitionState(@TransitionState int state) { - mState = state; + if (mState != state) { + for (int i = 0; i < mOnPipTransitionStateChangedListeners.size(); i++) { + mOnPipTransitionStateChangedListeners.get(i).onPipTransitionStateChanged( + mState, state); + } + mState = state; + } } public @TransitionState int getTransitionState() { @@ -73,8 +85,12 @@ public class PipTransitionState { } public boolean isInPip() { - return mState >= TASK_APPEARED - && mState != EXITING_PIP; + return isInPip(mState); + } + + /** Returns true if activity has fully entered PiP mode. */ + public boolean hasEnteredPip() { + return hasEnteredPip(mState); } public void setInSwipePipToHomeTransition(boolean inSwipePipToHomeTransition) { @@ -94,4 +110,28 @@ public class PipTransitionState { return mState < ENTERING_PIP || mState == EXITING_PIP; } + + public void addOnPipTransitionStateChangedListener( + @NonNull OnPipTransitionStateChangedListener listener) { + mOnPipTransitionStateChangedListeners.add(listener); + } + + public void removeOnPipTransitionStateChangedListener( + @NonNull OnPipTransitionStateChangedListener listener) { + mOnPipTransitionStateChangedListeners.remove(listener); + } + + public static boolean isInPip(@TransitionState int state) { + return state >= TASK_APPEARED && state != EXITING_PIP; + } + + /** Returns true if activity has fully entered PiP mode. */ + public static boolean hasEnteredPip(@TransitionState int state) { + return state == ENTERED_PIP; + } + + public interface OnPipTransitionStateChangedListener { + void onPipTransitionStateChanged(@TransitionState int oldState, + @TransitionState int newState); + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipUtils.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipUtils.java index dc60bcf742ce..fa0061982c45 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipUtils.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipUtils.java @@ -83,7 +83,9 @@ public class PipUtils { public static boolean remoteActionsMatch(RemoteAction action1, RemoteAction action2) { if (action1 == action2) return true; if (action1 == null || action2 == null) return false; - return Objects.equals(action1.getTitle(), action2.getTitle()) + return action1.isEnabled() == action2.isEnabled() + && action1.shouldShowIcon() == action2.shouldShowIcon() + && Objects.equals(action1.getTitle(), action2.getTitle()) && Objects.equals(action1.getContentDescription(), action2.getContentDescription()) && Objects.equals(action1.getActionIntent(), action2.getActionIntent()); } @@ -116,7 +118,7 @@ public class PipUtils { if (taskId <= 0) return null; try { return ActivityTaskManager.getService().getTaskSnapshot( - taskId, isLowResolution); + taskId, isLowResolution, false /* takeSnapshotIfNeeded */); } catch (RemoteException e) { Log.e(TAG, "Failed to get task snapshot, taskId=" + taskId, e); return null; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipKeepClearAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipKeepClearAlgorithm.java new file mode 100644 index 000000000000..84071e08d472 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipKeepClearAlgorithm.java @@ -0,0 +1,142 @@ +/* + * 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.pip.phone; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Rect; +import android.util.ArraySet; +import android.view.Gravity; + +import com.android.wm.shell.R; +import com.android.wm.shell.pip.PipBoundsAlgorithm; +import com.android.wm.shell.pip.PipBoundsState; +import com.android.wm.shell.pip.PipKeepClearAlgorithm; + +import java.util.Set; + +/** + * Calculates the adjusted position that does not occlude keep clear areas. + */ +public class PhonePipKeepClearAlgorithm implements PipKeepClearAlgorithm { + + protected int mKeepClearAreasPadding; + + public PhonePipKeepClearAlgorithm(Context context) { + reloadResources(context); + } + + private void reloadResources(Context context) { + final Resources res = context.getResources(); + mKeepClearAreasPadding = res.getDimensionPixelSize(R.dimen.pip_keep_clear_areas_padding); + } + + /** + * Adjusts the current position of PiP to avoid occluding keep clear areas. This will push PiP + * towards the closest edge and then apply calculations to avoid occluding keep clear areas. + */ + public Rect adjust(PipBoundsState pipBoundsState, PipBoundsAlgorithm pipBoundsAlgorithm) { + Rect startingBounds = pipBoundsState.getBounds().isEmpty() + ? pipBoundsAlgorithm.getEntryDestinationBoundsIgnoringKeepClearAreas() + : pipBoundsState.getBounds(); + float snapFraction = pipBoundsAlgorithm.getSnapFraction(startingBounds); + int verticalGravity = Gravity.BOTTOM; + int horizontalGravity; + if (snapFraction >= 0.5f && snapFraction < 2.5f) { + horizontalGravity = Gravity.RIGHT; + } else { + horizontalGravity = Gravity.LEFT; + } + // push the bounds based on the gravity + Rect insets = new Rect(); + pipBoundsAlgorithm.getInsetBounds(insets); + if (pipBoundsState.isImeShowing()) { + insets.bottom -= pipBoundsState.getImeHeight(); + } + Rect pushedBounds = new Rect(startingBounds); + if (verticalGravity == Gravity.BOTTOM) { + pushedBounds.offsetTo(pushedBounds.left, + insets.bottom - pushedBounds.height()); + } + if (horizontalGravity == Gravity.RIGHT) { + pushedBounds.offsetTo(insets.right - pushedBounds.width(), pushedBounds.top); + } else { + pushedBounds.offsetTo(insets.left, pushedBounds.top); + } + return findUnoccludedPosition(pushedBounds, pipBoundsState.getRestrictedKeepClearAreas(), + pipBoundsState.getUnrestrictedKeepClearAreas(), insets); + } + + /** Returns a new {@code Rect} that does not occlude the provided keep clear areas. */ + public Rect findUnoccludedPosition(Rect defaultBounds, Set<Rect> restrictedKeepClearAreas, + Set<Rect> unrestrictedKeepClearAreas, Rect allowedBounds) { + if (restrictedKeepClearAreas.isEmpty() && unrestrictedKeepClearAreas.isEmpty()) { + return defaultBounds; + } + Set<Rect> keepClearAreas = new ArraySet<>(); + if (!restrictedKeepClearAreas.isEmpty()) { + keepClearAreas.addAll(restrictedKeepClearAreas); + } + if (!unrestrictedKeepClearAreas.isEmpty()) { + keepClearAreas.addAll(unrestrictedKeepClearAreas); + } + Rect outBounds = new Rect(defaultBounds); + for (Rect r : keepClearAreas) { + Rect tmpRect = new Rect(r); + // add extra padding to the keep clear area + tmpRect.inset(-mKeepClearAreasPadding, -mKeepClearAreasPadding); + if (Rect.intersects(r, outBounds)) { + if (tryOffsetUp(outBounds, tmpRect, allowedBounds)) continue; + if (tryOffsetLeft(outBounds, tmpRect, allowedBounds)) continue; + if (tryOffsetDown(outBounds, tmpRect, allowedBounds)) continue; + if (tryOffsetRight(outBounds, tmpRect, allowedBounds)) continue; + } + } + return outBounds; + } + + private static boolean tryOffsetLeft(Rect rectToMove, Rect rectToAvoid, Rect allowedBounds) { + return tryOffset(rectToMove, rectToAvoid, allowedBounds, + rectToAvoid.left - rectToMove.right, 0); + } + + private static boolean tryOffsetRight(Rect rectToMove, Rect rectToAvoid, Rect allowedBounds) { + return tryOffset(rectToMove, rectToAvoid, allowedBounds, + rectToAvoid.right - rectToMove.left, 0); + } + + private static boolean tryOffsetUp(Rect rectToMove, Rect rectToAvoid, Rect allowedBounds) { + return tryOffset(rectToMove, rectToAvoid, allowedBounds, + 0, rectToAvoid.top - rectToMove.bottom); + } + + private static boolean tryOffsetDown(Rect rectToMove, Rect rectToAvoid, Rect allowedBounds) { + return tryOffset(rectToMove, rectToAvoid, allowedBounds, + 0, rectToAvoid.bottom - rectToMove.top); + } + + private static boolean tryOffset(Rect rectToMove, Rect rectToAvoid, Rect allowedBounds, + int dx, int dy) { + Rect tmp = new Rect(rectToMove); + tmp.offset(dx, dy); + if (!Rect.intersects(rectToAvoid, tmp) && allowedBounds.contains(tmp)) { + rectToMove.offsetTo(tmp.left, tmp.top); + return true; + } + return false; + } +} 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 4942987742a0..281ea530e9e1 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 @@ -31,8 +31,6 @@ import android.os.RemoteException; import android.util.Size; import android.view.MotionEvent; import android.view.SurfaceControl; -import android.view.SyncRtSurfaceTransactionApplier; -import android.view.SyncRtSurfaceTransactionApplier.SurfaceParams; import android.view.WindowManagerGlobal; import com.android.internal.protolog.common.ProtoLog; @@ -42,6 +40,7 @@ import com.android.wm.shell.pip.PipBoundsState; import com.android.wm.shell.pip.PipMediaController; import com.android.wm.shell.pip.PipMediaController.ActionListener; import com.android.wm.shell.pip.PipMenuController; +import com.android.wm.shell.pip.PipSurfaceTransactionHelper; import com.android.wm.shell.pip.PipUiEventLogger; import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.splitscreen.SplitScreenController; @@ -115,6 +114,10 @@ public class PhonePipMenuController implements PipMenuController { private final ShellExecutor mMainExecutor; private final Handler mMainHandler; + private final PipSurfaceTransactionHelper.SurfaceControlTransactionFactory + mSurfaceControlTransactionFactory; + private final float[] mTmpTransform = new float[9]; + private final ArrayList<Listener> mListeners = new ArrayList<>(); private final SystemWindows mSystemWindows; private final Optional<SplitScreenController> mSplitScreenController; @@ -124,7 +127,6 @@ public class PhonePipMenuController implements PipMenuController { private RemoteAction mCloseAction; private List<RemoteAction> mMediaActions; - private SyncRtSurfaceTransactionApplier mApplier; private int mMenuState; private PipMenuView mPipMenuView; @@ -150,6 +152,9 @@ public class PhonePipMenuController implements PipMenuController { mMainHandler = mainHandler; mSplitScreenController = splitScreenOptional; mPipUiEventLogger = pipUiEventLogger; + + mSurfaceControlTransactionFactory = + new PipSurfaceTransactionHelper.VsyncSurfaceControlTransactionFactory(); } public boolean isMenuVisible() { @@ -194,7 +199,6 @@ public class PhonePipMenuController implements PipMenuController { return; } - mApplier = null; mSystemWindows.removeView(mPipMenuView); mPipMenuView = null; } @@ -289,7 +293,7 @@ public class PhonePipMenuController implements PipMenuController { willResizeMenu, withDelay, showResizeHandle, Debug.getCallers(5, " ")); } - if (!maybeCreateSyncApplier()) { + if (!checkPipMenuState()) { return; } @@ -312,7 +316,7 @@ public class PhonePipMenuController implements PipMenuController { return; } - if (!maybeCreateSyncApplier()) { + if (!checkPipMenuState()) { return; } @@ -328,18 +332,15 @@ public class PhonePipMenuController implements PipMenuController { mTmpSourceRectF.set(mTmpSourceBounds); mTmpDestinationRectF.set(destinationBounds); mMoveTransform.setRectToRect(mTmpSourceRectF, mTmpDestinationRectF, Matrix.ScaleToFit.FILL); - SurfaceControl surfaceControl = getSurfaceControl(); - SurfaceParams params = new SurfaceParams.Builder(surfaceControl) - .withMatrix(mMoveTransform) - .build(); + final SurfaceControl surfaceControl = getSurfaceControl(); + final SurfaceControl.Transaction menuTx = + mSurfaceControlTransactionFactory.getTransaction(); + menuTx.setMatrix(surfaceControl, mMoveTransform, mTmpTransform); if (pipLeash != null && t != null) { - SurfaceParams pipParams = new SurfaceParams.Builder(pipLeash) - .withMergeTransaction(t) - .build(); - mApplier.scheduleApply(params, pipParams); - } else { - mApplier.scheduleApply(params); + // Merge the two transactions, vsyncId has been set on menuTx. + menuTx.merge(t); } + menuTx.apply(); } /** @@ -353,36 +354,29 @@ public class PhonePipMenuController implements PipMenuController { return; } - if (!maybeCreateSyncApplier()) { + if (!checkPipMenuState()) { return; } - SurfaceControl surfaceControl = getSurfaceControl(); - SurfaceParams params = new SurfaceParams.Builder(surfaceControl) - .withWindowCrop(destinationBounds) - .build(); + final SurfaceControl surfaceControl = getSurfaceControl(); + final SurfaceControl.Transaction menuTx = + mSurfaceControlTransactionFactory.getTransaction(); + menuTx.setCrop(surfaceControl, destinationBounds); if (pipLeash != null && t != null) { - SurfaceParams pipParams = new SurfaceParams.Builder(pipLeash) - .withMergeTransaction(t) - .build(); - mApplier.scheduleApply(params, pipParams); - } else { - mApplier.scheduleApply(params); + // Merge the two transactions, vsyncId has been set on menuTx. + menuTx.merge(t); } + menuTx.apply(); } - private boolean maybeCreateSyncApplier() { + private boolean checkPipMenuState() { if (mPipMenuView == null || mPipMenuView.getViewRootImpl() == null) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: Not going to move PiP, either menu or its parent is not created.", TAG); return false; } - if (mApplier == null) { - mApplier = new SyncRtSurfaceTransactionApplier(mPipMenuView); - } - - return mApplier != null; + return true; } /** 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 dad261ad9580..af47666efa5a 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 @@ -43,11 +43,13 @@ import android.content.pm.ActivityInfo; import android.content.res.Configuration; import android.graphics.Rect; import android.os.RemoteException; +import android.os.SystemProperties; import android.os.UserHandle; import android.os.UserManager; import android.util.Pair; import android.util.Size; import android.view.DisplayInfo; +import android.view.InsetsState; import android.view.SurfaceControl; import android.view.WindowManagerGlobal; import android.window.WindowContainerTransaction; @@ -63,6 +65,7 @@ import com.android.wm.shell.R; import com.android.wm.shell.WindowManagerShellWrapper; 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.RemoteCallable; import com.android.wm.shell.common.ShellExecutor; @@ -79,13 +82,21 @@ import com.android.wm.shell.pip.PipAnimationController; import com.android.wm.shell.pip.PipAppOpsListener; import com.android.wm.shell.pip.PipBoundsAlgorithm; import com.android.wm.shell.pip.PipBoundsState; +import com.android.wm.shell.pip.PipKeepClearAlgorithm; import com.android.wm.shell.pip.PipMediaController; import com.android.wm.shell.pip.PipParamsChangedForwarder; import com.android.wm.shell.pip.PipSnapAlgorithm; import com.android.wm.shell.pip.PipTaskOrganizer; import com.android.wm.shell.pip.PipTransitionController; +import com.android.wm.shell.pip.PipTransitionState; import com.android.wm.shell.pip.PipUtils; import com.android.wm.shell.protolog.ShellProtoLogGroup; +import com.android.wm.shell.sysui.ConfigurationChangeListener; +import com.android.wm.shell.sysui.KeyguardChangeListener; +import com.android.wm.shell.sysui.ShellCommandHandler; +import com.android.wm.shell.sysui.ShellController; +import com.android.wm.shell.sysui.ShellInit; +import com.android.wm.shell.sysui.UserChangeListener; import com.android.wm.shell.transition.Transitions; import java.io.PrintWriter; @@ -99,38 +110,96 @@ import java.util.function.Consumer; * Manages the picture-in-picture (PIP) UI and states for Phones. */ public class PipController implements PipTransitionController.PipTransitionCallback, - RemoteCallable<PipController> { + RemoteCallable<PipController>, ConfigurationChangeListener, KeyguardChangeListener, + UserChangeListener { private static final String TAG = "PipController"; + private static final long PIP_KEEP_CLEAR_AREAS_DELAY = + SystemProperties.getLong("persist.wm.debug.pip_keep_clear_areas_delay", 200); + + private boolean mEnablePipKeepClearAlgorithm = + SystemProperties.getBoolean("persist.wm.debug.enable_pip_keep_clear_algorithm", false); + + @VisibleForTesting + void setEnablePipKeepClearAlgorithm(boolean value) { + mEnablePipKeepClearAlgorithm = value; + } + private Context mContext; protected ShellExecutor mMainExecutor; private DisplayController mDisplayController; private PipInputConsumer mPipInputConsumer; private WindowManagerShellWrapper mWindowManagerShellWrapper; + private PipAnimationController mPipAnimationController; private PipAppOpsListener mAppOpsListener; private PipMediaController mMediaController; private PipBoundsAlgorithm mPipBoundsAlgorithm; + private PipKeepClearAlgorithm mPipKeepClearAlgorithm; private PipBoundsState mPipBoundsState; + private PipMotionHelper mPipMotionHelper; private PipTouchHandler mTouchHandler; private PipTransitionController mPipTransitionController; private TaskStackListenerImpl mTaskStackListener; private PipParamsChangedForwarder mPipParamsChangedForwarder; + private DisplayInsetsController mDisplayInsetsController; private Optional<OneHandedController> mOneHandedController; + private final ShellCommandHandler mShellCommandHandler; + private final ShellController mShellController; protected final PipImpl mImpl; private final Rect mTmpInsetBounds = new Rect(); private final int mEnterAnimationDuration; + private final Runnable mMovePipInResponseToKeepClearAreasChangeCallback = + this::onKeepClearAreasChangedCallback; + + private void onKeepClearAreasChangedCallback() { + if (!mEnablePipKeepClearAlgorithm) { + // early bail out if the keep clear areas feature is disabled + return; + } + // if there is another animation ongoing, wait for it to finish and try again + if (mPipAnimationController.isAnimating()) { + mMainExecutor.removeCallbacks( + mMovePipInResponseToKeepClearAreasChangeCallback); + mMainExecutor.executeDelayed( + mMovePipInResponseToKeepClearAreasChangeCallback, + PIP_KEEP_CLEAR_AREAS_DELAY); + return; + } + updatePipPositionForKeepClearAreas(); + } + + private void updatePipPositionForKeepClearAreas() { + if (!mEnablePipKeepClearAlgorithm) { + // early bail out if the keep clear areas feature is disabled + return; + } + // only move if already in pip, other transitions account for keep clear areas + if (mPipTransitionState.hasEnteredPip()) { + Rect destBounds = mPipKeepClearAlgorithm.adjust(mPipBoundsState, + mPipBoundsAlgorithm); + // only move if the bounds are actually different + if (destBounds != mPipBoundsState.getBounds()) { + mPipTaskOrganizer.scheduleAnimateResizePip(destBounds, + mEnterAnimationDuration, null); + } + } + } + private boolean mIsInFixedRotation; private PipAnimationListener mPinnedStackAnimationRecentsCallback; protected PhonePipMenuController mMenuController; protected PipTaskOrganizer mPipTaskOrganizer; + private PipTransitionState mPipTransitionState; protected PinnedStackListenerForwarder.PinnedTaskListener mPinnedTaskListener = new PipControllerPinnedTaskListener(); private boolean mIsKeyguardShowingOrAnimating; + private Consumer<Boolean> mOnIsInPipStateChangedListener; + private interface PipAnimationListener { /** * Notifies the listener that the Pip animation is started. @@ -156,7 +225,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb * Handler for display rotation changes. */ private final DisplayChangeController.OnDisplayChangingListener mRotationController = ( - int displayId, int fromRotation, int toRotation, WindowContainerTransaction t) -> { + displayId, fromRotation, toRotation, newDisplayAreaInfo, t) -> { if (mPipTransitionController.handleRotateDisplay(fromRotation, toRotation, t)) { return; } @@ -247,7 +316,15 @@ public class PipController implements PipTransitionController.PipTransitionCallb public void onKeepClearAreasChanged(int displayId, Set<Rect> restricted, Set<Rect> unrestricted) { if (mPipBoundsState.getDisplayId() == displayId) { - mPipBoundsState.setKeepClearAreas(restricted, unrestricted); + if (mEnablePipKeepClearAlgorithm) { + mPipBoundsState.setKeepClearAreas(restricted, unrestricted); + + mMainExecutor.removeCallbacks( + mMovePipInResponseToKeepClearAreasChangeCallback); + mMainExecutor.executeDelayed( + mMovePipInResponseToKeepClearAreasChangeCallback, + PIP_KEEP_CLEAR_AREAS_DELAY); + } } } }; @@ -261,6 +338,9 @@ public class PipController implements PipTransitionController.PipTransitionCallb public void onImeVisibilityChanged(boolean imeVisible, int imeHeight) { mPipBoundsState.setImeVisibility(imeVisible, imeHeight); mTouchHandler.onImeVisibilityChanged(imeVisible, imeHeight); + if (imeVisible) { + updatePipPositionForKeepClearAreas(); + } } @Override @@ -284,14 +364,27 @@ public class PipController implements PipTransitionController.PipTransitionCallb * Instantiates {@link PipController}, returns {@code null} if the feature not supported. */ @Nullable - public static Pip create(Context context, DisplayController displayController, - PipAppOpsListener pipAppOpsListener, PipBoundsAlgorithm pipBoundsAlgorithm, - PipBoundsState pipBoundsState, PipMediaController pipMediaController, - PhonePipMenuController phonePipMenuController, PipTaskOrganizer pipTaskOrganizer, - PipTouchHandler pipTouchHandler, PipTransitionController pipTransitionController, + public static Pip create(Context context, + ShellInit shellInit, + ShellCommandHandler shellCommandHandler, + ShellController shellController, + DisplayController displayController, + PipAnimationController pipAnimationController, + PipAppOpsListener pipAppOpsListener, + PipBoundsAlgorithm pipBoundsAlgorithm, + PipKeepClearAlgorithm pipKeepClearAlgorithm, + PipBoundsState pipBoundsState, + PipMotionHelper pipMotionHelper, + PipMediaController pipMediaController, + PhonePipMenuController phonePipMenuController, + PipTaskOrganizer pipTaskOrganizer, + PipTransitionState pipTransitionState, + PipTouchHandler pipTouchHandler, + PipTransitionController pipTransitionController, WindowManagerShellWrapper windowManagerShellWrapper, TaskStackListenerImpl taskStackListener, PipParamsChangedForwarder pipParamsChangedForwarder, + DisplayInsetsController displayInsetsController, Optional<OneHandedController> oneHandedController, ShellExecutor mainExecutor) { if (!context.getPackageManager().hasSystemFeature(FEATURE_PICTURE_IN_PICTURE)) { @@ -300,27 +393,37 @@ public class PipController implements PipTransitionController.PipTransitionCallb return null; } - return new PipController(context, displayController, pipAppOpsListener, pipBoundsAlgorithm, - pipBoundsState, pipMediaController, - phonePipMenuController, pipTaskOrganizer, pipTouchHandler, pipTransitionController, - windowManagerShellWrapper, taskStackListener, pipParamsChangedForwarder, + return new PipController(context, shellInit, shellCommandHandler, shellController, + displayController, pipAnimationController, pipAppOpsListener, + pipBoundsAlgorithm, pipKeepClearAlgorithm, pipBoundsState, pipMotionHelper, + pipMediaController, phonePipMenuController, pipTaskOrganizer, pipTransitionState, + pipTouchHandler, pipTransitionController, windowManagerShellWrapper, + taskStackListener, pipParamsChangedForwarder, displayInsetsController, oneHandedController, mainExecutor) .mImpl; } protected PipController(Context context, + ShellInit shellInit, + ShellCommandHandler shellCommandHandler, + ShellController shellController, DisplayController displayController, + PipAnimationController pipAnimationController, PipAppOpsListener pipAppOpsListener, PipBoundsAlgorithm pipBoundsAlgorithm, + PipKeepClearAlgorithm pipKeepClearAlgorithm, @NonNull PipBoundsState pipBoundsState, + PipMotionHelper pipMotionHelper, PipMediaController pipMediaController, PhonePipMenuController phonePipMenuController, PipTaskOrganizer pipTaskOrganizer, + PipTransitionState pipTransitionState, PipTouchHandler pipTouchHandler, PipTransitionController pipTransitionController, WindowManagerShellWrapper windowManagerShellWrapper, TaskStackListenerImpl taskStackListener, PipParamsChangedForwarder pipParamsChangedForwarder, + DisplayInsetsController displayInsetsController, Optional<OneHandedController> oneHandedController, ShellExecutor mainExecutor ) { @@ -331,16 +434,22 @@ public class PipController implements PipTransitionController.PipTransitionCallb } mContext = context; + mShellCommandHandler = shellCommandHandler; + mShellController = shellController; mImpl = new PipImpl(); mWindowManagerShellWrapper = windowManagerShellWrapper; mDisplayController = displayController; mPipBoundsAlgorithm = pipBoundsAlgorithm; + mPipKeepClearAlgorithm = pipKeepClearAlgorithm; mPipBoundsState = pipBoundsState; + mPipMotionHelper = pipMotionHelper; mPipTaskOrganizer = pipTaskOrganizer; + mPipTransitionState = pipTransitionState; mMainExecutor = mainExecutor; mMediaController = pipMediaController; mMenuController = phonePipMenuController; mTouchHandler = pipTouchHandler; + mPipAnimationController = pipAnimationController; mAppOpsListener = pipAppOpsListener; mOneHandedController = oneHandedController; mPipTransitionController = pipTransitionController; @@ -349,12 +458,13 @@ public class PipController implements PipTransitionController.PipTransitionCallb mEnterAnimationDuration = mContext.getResources() .getInteger(R.integer.config_pipEnterAnimationDuration); mPipParamsChangedForwarder = pipParamsChangedForwarder; + mDisplayInsetsController = displayInsetsController; - //TODO: move this to ShellInit when PipController can be injected - mMainExecutor.execute(this::init); + shellInit.addInitCallback(this::onInit, this); } - public void init() { + private void onInit() { + mShellCommandHandler.addDumpCallback(this::dump, this); mPipInputConsumer = new PipInputConsumer(WindowManagerGlobal.getWindowManagerService(), INPUT_CONSUMER_PIP, mMainExecutor); mPipTransitionController.registerPipTransitionCallback(this); @@ -363,6 +473,15 @@ public class PipController implements PipTransitionController.PipTransitionCallb onDisplayChanged(mDisplayController.getDisplayLayout(displayId), false /* saveRestoreSnapFraction */); }); + mPipTransitionState.addOnPipTransitionStateChangedListener((oldState, newState) -> { + if (mOnIsInPipStateChangedListener != null) { + final boolean wasInPip = PipTransitionState.isInPip(oldState); + final boolean nowInPip = PipTransitionState.isInPip(newState); + if (nowInPip != wasInPip) { + mOnIsInPipStateChangedListener.accept(nowInPip); + } + } + }); mPipBoundsState.setOnMinimalSizeChangeCallback( () -> { // The minimal size drives the normal bounds, so they need to be recalculated. @@ -458,14 +577,21 @@ public class PipController implements PipTransitionController.PipTransitionCallb mPipBoundsState.getBounds(), mPipBoundsState.getAspectRatio()); Objects.requireNonNull(destinationBounds, "Missing destination bounds"); - mPipTaskOrganizer.scheduleAnimateResizePip(destinationBounds, - mEnterAnimationDuration, - null /* updateBoundsCallback */); - - mTouchHandler.onAspectRatioChanged(); - updateMovementBounds(null /* toBounds */, false /* fromRotation */, - false /* fromImeAdjustment */, false /* fromShelfAdjustment */, - null /* windowContainerTransaction */); + if (!destinationBounds.equals(mPipBoundsState.getBounds())) { + mPipTaskOrganizer.scheduleAnimateResizePip(destinationBounds, + mEnterAnimationDuration, + null /* updateBoundsCallback */); + mTouchHandler.onAspectRatioChanged(); + updateMovementBounds(null /* toBounds */, false /* fromRotation */, + false /* fromImeAdjustment */, false /* fromShelfAdjustment */, + null /* windowContainerTransaction */); + } else { + // when we enter pip for the first time, the destination bounds and pip + // bounds will already match, since they are calculated prior to + // starting the animation, so we only need to update the min/max size + // that is used for e.g. double tap to maximized state + mTouchHandler.updateMinMaxSize(ratio); + } } @Override @@ -475,8 +601,18 @@ public class PipController implements PipTransitionController.PipTransitionCallb } }); + mDisplayInsetsController.addInsetsChangedListener(mPipBoundsState.getDisplayId(), + new DisplayInsetsController.OnInsetsChangedListener() { + @Override + public void insetsChanged(InsetsState insetsState) { + onDisplayChanged( + mDisplayController.getDisplayLayout(mPipBoundsState.getDisplayId()), + false /* saveRestoreSnapFraction */); + } + }); + mOneHandedController.ifPresent(controller -> { - controller.asOneHanded().registerTransitionCallback( + controller.registerTransitionCallback( new OneHandedTransitionCallback() { @Override public void onStartFinished(Rect bounds) { @@ -489,6 +625,12 @@ public class PipController implements PipTransitionController.PipTransitionCallb } }); }); + + mMediaController.registerSessionListenerForCurrentUser(); + + mShellController.addConfigurationChangeListener(this); + mShellController.addKeyguardChangeListener(this); + mShellController.addUserChangeListener(this); } @Override @@ -501,18 +643,27 @@ public class PipController implements PipTransitionController.PipTransitionCallb return mMainExecutor; } - private void onConfigurationChanged(Configuration newConfig) { + @Override + public void onUserChanged(int newUserId, @NonNull Context userContext) { + // Re-register the media session listener when switching users + mMediaController.registerSessionListenerForCurrentUser(); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { mPipBoundsAlgorithm.onConfigurationChanged(mContext); mTouchHandler.onConfigurationChanged(); mPipBoundsState.onConfigurationChanged(); } - private void onDensityOrFontScaleChanged() { + @Override + public void onDensityOrFontScaleChanged() { mPipTaskOrganizer.onDensityOrFontScaleChanged(mContext); onPipResourceDimensionsChanged(); } - private void onOverlayChanged() { + @Override + public void onThemeChanged() { mTouchHandler.onOverlayChanged(); onDisplayChanged(new DisplayLayout(mContext, mContext.getDisplay()), false /* saveRestoreSnapFraction */); @@ -542,33 +693,50 @@ public class PipController implements PipTransitionController.PipTransitionCallb mMenuController.attachPipMenuView(); // Calculate the snap fraction of the current stack along the old movement bounds final PipSnapAlgorithm pipSnapAlgorithm = mPipBoundsAlgorithm.getSnapAlgorithm(); - final Rect postChangeStackBounds = new Rect(mPipBoundsState.getBounds()); - final float snapFraction = pipSnapAlgorithm.getSnapFraction(postChangeStackBounds, - mPipBoundsAlgorithm.getMovementBounds(postChangeStackBounds), + final Rect postChangeBounds = new Rect(mPipBoundsState.getBounds()); + final float snapFraction = pipSnapAlgorithm.getSnapFraction(postChangeBounds, + mPipBoundsAlgorithm.getMovementBounds(postChangeBounds), mPipBoundsState.getStashedState()); + // Scale PiP on density dpi change, so it appears to be the same size physically. + final boolean densityDpiChanged = mPipBoundsState.getDisplayLayout().densityDpi() != 0 + && (mPipBoundsState.getDisplayLayout().densityDpi() != layout.densityDpi()); + if (densityDpiChanged) { + final float scale = (float) layout.densityDpi() + / mPipBoundsState.getDisplayLayout().densityDpi(); + postChangeBounds.set(0, 0, + (int) (postChangeBounds.width() * scale), + (int) (postChangeBounds.height() * scale)); + } + updateDisplayLayout.run(); - // Calculate the stack bounds in the new orientation based on same fraction along the + // Calculate the PiP bounds in the new orientation based on same fraction along the // rotated movement bounds. final Rect postChangeMovementBounds = mPipBoundsAlgorithm.getMovementBounds( - postChangeStackBounds, false /* adjustForIme */); - pipSnapAlgorithm.applySnapFraction(postChangeStackBounds, postChangeMovementBounds, + postChangeBounds, false /* adjustForIme */); + pipSnapAlgorithm.applySnapFraction(postChangeBounds, postChangeMovementBounds, snapFraction, mPipBoundsState.getStashedState(), mPipBoundsState.getStashOffset(), mPipBoundsState.getDisplayBounds(), mPipBoundsState.getDisplayLayout().stableInsets()); - mTouchHandler.getMotionHelper().movePip(postChangeStackBounds); + if (densityDpiChanged) { + // Using PipMotionHelper#movePip directly here may cause race condition since + // the app content in PiP mode may or may not be updated for the new density dpi. + final int duration = mContext.getResources().getInteger( + R.integer.config_pipEnterAnimationDuration); + mPipTaskOrganizer.scheduleAnimateResizePip( + postChangeBounds, duration, null /* updateBoundsCallback */); + } else { + // Directly move PiP to its final destination bounds without animation. + mPipTaskOrganizer.scheduleFinishResizePip(postChangeBounds); + } } else { updateDisplayLayout.run(); } } - private void registerSessionListenerForCurrentUser() { - mMediaController.registerSessionListenerForCurrentUser(); - } - private void onSystemUiStateChanged(boolean isValidState, int flag) { mTouchHandler.onSystemUiStateChanged(isValidState); } @@ -600,21 +768,24 @@ public class PipController implements PipTransitionController.PipTransitionCallb * finished first to reset the visibility of PiP window. * See also {@link #onKeyguardDismissAnimationFinished()} */ - private void onKeyguardVisibilityChanged(boolean keyguardShowing, boolean animating) { + @Override + public void onKeyguardVisibilityChanged(boolean visible, boolean occluded, + boolean animatingDismiss) { if (!mPipTaskOrganizer.isInPip()) { return; } - if (keyguardShowing) { + if (visible) { mIsKeyguardShowingOrAnimating = true; hidePipMenu(null /* onStartCallback */, null /* onEndCallback */); mPipTaskOrganizer.setPipVisibility(false); - } else if (!animating) { + } else if (!animatingDismiss) { mIsKeyguardShowingOrAnimating = false; mPipTaskOrganizer.setPipVisibility(true); } } - private void onKeyguardDismissAnimationFinished() { + @Override + public void onKeyguardDismissAnimationFinished() { if (mPipTaskOrganizer.isInPip()) { mIsKeyguardShowingOrAnimating = false; mPipTaskOrganizer.setPipVisibility(true); @@ -637,6 +808,13 @@ public class PipController implements PipTransitionController.PipTransitionCallb } } + private void setOnIsInPipStateChangedListener(Consumer<Boolean> callback) { + mOnIsInPipStateChangedListener = callback; + if (mOnIsInPipStateChangedListener != null) { + callback.accept(mPipTransitionState.isInPip()); + } + } + private void setShelfHeightLocked(boolean visible, int height) { final int shelfHeight = visible ? height : 0; mPipBoundsState.setShelfVisibility(visible, shelfHeight); @@ -663,8 +841,16 @@ public class PipController implements PipTransitionController.PipTransitionCallb private Rect startSwipePipToHome(ComponentName componentName, ActivityInfo activityInfo, PictureInPictureParams pictureInPictureParams, - int launcherRotation, int shelfHeight) { - setShelfHeightLocked(shelfHeight > 0 /* visible */, shelfHeight); + int launcherRotation, Rect hotseatKeepClearArea) { + + if (mEnablePipKeepClearAlgorithm) { + // pre-emptively add the keep clear area for Hotseat, so that it is taken into account + // when calculating the entry destination bounds of PiP window + mPipBoundsState.getRestrictedKeepClearAreas().add(hotseatKeepClearArea); + } else { + int shelfHeight = hotseatKeepClearArea.height(); + setShelfHeightLocked(shelfHeight > 0 /* visible */, shelfHeight); + } onDisplayRotationChangedNotInPip(mContext, launcherRotation); final Rect entryBounds = mPipTaskOrganizer.startSwipePipToHome(componentName, activityInfo, pictureInPictureParams); @@ -838,7 +1024,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb return true; } - private void dump(PrintWriter pw) { + private void dump(PrintWriter pw, String prefix) { final String innerPrefix = " "; pw.println(TAG); mMenuController.dump(pw, innerPrefix); @@ -872,27 +1058,6 @@ public class PipController implements PipTransitionController.PipTransitionCallb } @Override - public void onConfigurationChanged(Configuration newConfig) { - mMainExecutor.execute(() -> { - PipController.this.onConfigurationChanged(newConfig); - }); - } - - @Override - public void onDensityOrFontScaleChanged() { - mMainExecutor.execute(() -> { - PipController.this.onDensityOrFontScaleChanged(); - }); - } - - @Override - public void onOverlayChanged() { - mMainExecutor.execute(() -> { - PipController.this.onOverlayChanged(); - }); - } - - @Override public void onSystemUiStateChanged(boolean isSysUiStateValid, int flag) { mMainExecutor.execute(() -> { PipController.this.onSystemUiStateChanged(isSysUiStateValid, flag); @@ -900,16 +1065,16 @@ public class PipController implements PipTransitionController.PipTransitionCallb } @Override - public void registerSessionListenerForCurrentUser() { + public void setShelfHeight(boolean visible, int height) { mMainExecutor.execute(() -> { - PipController.this.registerSessionListenerForCurrentUser(); + PipController.this.setShelfHeight(visible, height); }); } @Override - public void setShelfHeight(boolean visible, int height) { + public void setOnIsInPipStateChangedListener(Consumer<Boolean> callback) { mMainExecutor.execute(() -> { - PipController.this.setShelfHeight(visible, height); + PipController.this.setOnIsInPipStateChangedListener(callback); }); } @@ -940,30 +1105,6 @@ public class PipController implements PipTransitionController.PipTransitionCallb PipController.this.showPictureInPictureMenu(); }); } - - @Override - public void onKeyguardVisibilityChanged(boolean showing, boolean animating) { - mMainExecutor.execute(() -> { - PipController.this.onKeyguardVisibilityChanged(showing, animating); - }); - } - - @Override - public void onKeyguardDismissAnimationFinished() { - mMainExecutor.execute(PipController.this::onKeyguardDismissAnimationFinished); - } - - @Override - public void dump(PrintWriter pw) { - try { - mMainExecutor.executeBlocking(() -> { - PipController.this.dump(pw); - }); - } catch (InterruptedException e) { - ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: Failed to dump PipController in 2s", TAG); - } - } } /** @@ -1008,12 +1149,12 @@ public class PipController implements PipTransitionController.PipTransitionCallb @Override public Rect startSwipePipToHome(ComponentName componentName, ActivityInfo activityInfo, PictureInPictureParams pictureInPictureParams, int launcherRotation, - int shelfHeight) { + Rect keepClearArea) { Rect[] result = new Rect[1]; executeRemoteCallWithTaskPermission(mController, "startSwipePipToHome", (controller) -> { result[0] = controller.startSwipePipToHome(componentName, activityInfo, - pictureInPictureParams, launcherRotation, shelfHeight); + pictureInPictureParams, launcherRotation, keepClearArea); }, true /* blocking */); return result[0]; } 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 a0e22011b5d0..7619646804ad 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 @@ -288,8 +288,10 @@ public class PipDismissTargetHandler implements ViewTreeObserver.OnPreDrawListen if (mTargetViewContainer.getVisibility() != View.VISIBLE) { mTargetViewContainer.getViewTreeObserver().addOnPreDrawListener(this); - mTargetViewContainer.show(); } + // always invoke show, since the target might still be VISIBLE while playing hide animation, + // so we want to ensure it will show back again + mTargetViewContainer.show(); } /** Animates the magnetic dismiss target out and then sets it to GONE. */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipDoubleTapHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipDoubleTapHelper.java new file mode 100644 index 000000000000..acc0caf95e35 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipDoubleTapHelper.java @@ -0,0 +1,123 @@ +/* + * 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.pip.phone; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.graphics.Rect; + +import com.android.wm.shell.pip.PipBoundsState; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Static utilities to get appropriate {@link PipDoubleTapHelper.PipSizeSpec} on a double tap. + */ +public class PipDoubleTapHelper { + + /** + * Should not be instantiated as a stateless class. + */ + private PipDoubleTapHelper() {} + + /** + * A constant that represents a pip screen size. + * + * <p>CUSTOM - user resized screen size (by pinching in/out)</p> + * <p>DEFAULT - normal screen size used as default when entering pip mode</p> + * <p>MAX - maximum allowed screen size</p> + */ + @IntDef(value = { + SIZE_SPEC_CUSTOM, + SIZE_SPEC_DEFAULT, + SIZE_SPEC_MAX + }) + @Retention(RetentionPolicy.SOURCE) + @interface PipSizeSpec {} + + static final int SIZE_SPEC_CUSTOM = 2; + static final int SIZE_SPEC_DEFAULT = 0; + static final int SIZE_SPEC_MAX = 1; + + /** + * Returns MAX or DEFAULT {@link PipSizeSpec} to toggle to/from. + * + * <p>Each double tap toggles back and forth between {@code PipSizeSpec.CUSTOM} and + * either {@code PipSizeSpec.MAX} or {@code PipSizeSpec.DEFAULT}. The choice between + * the latter two sizes is determined based on the current state of the pip screen.</p> + * + * @param mPipBoundsState current state of the pip screen + */ + @PipSizeSpec + private static int getMaxOrDefaultPipSizeSpec(@NonNull PipBoundsState mPipBoundsState) { + // determine the average pip screen width + int averageWidth = (mPipBoundsState.getMaxSize().x + + mPipBoundsState.getMinSize().x) / 2; + + // If pip screen width is above average, DEFAULT is the size spec we need to + // toggle to. Otherwise, we choose MAX. + return (mPipBoundsState.getBounds().width() > averageWidth) + ? SIZE_SPEC_DEFAULT + : SIZE_SPEC_MAX; + } + + /** + * Determines the {@link PipSizeSpec} to toggle to on double tap. + * + * @param mPipBoundsState current state of the pip screen + * @param userResizeBounds latest user resized bounds (by pinching in/out) + * @return pip screen size to switch to + */ + @PipSizeSpec + static int nextSizeSpec(@NonNull PipBoundsState mPipBoundsState, + @NonNull Rect userResizeBounds) { + // is pip screen at its maximum + boolean isScreenMax = mPipBoundsState.getBounds().width() + == mPipBoundsState.getMaxSize().x; + + // is pip screen at its normal default size + boolean isScreenDefault = (mPipBoundsState.getBounds().width() + == mPipBoundsState.getNormalBounds().width()) + && (mPipBoundsState.getBounds().height() + == mPipBoundsState.getNormalBounds().height()); + + // edge case 1 + // if user hasn't resized screen yet, i.e. CUSTOM size does not exist yet + // or if user has resized exactly to DEFAULT, then we just want to maximize + if (isScreenDefault + && userResizeBounds.width() == mPipBoundsState.getNormalBounds().width()) { + return SIZE_SPEC_MAX; + } + + // edge case 2 + // if user has maximized, then we want to toggle to DEFAULT + if (isScreenMax + && userResizeBounds.width() == mPipBoundsState.getMaxSize().x) { + return SIZE_SPEC_DEFAULT; + } + + // otherwise in general we want to toggle back to user's CUSTOM size + if (isScreenDefault || isScreenMax) { + return SIZE_SPEC_CUSTOM; + } + + // if we are currently in user resized CUSTOM size state + // then we toggle either to MAX or DEFAULT depending on the current pip screen state + return getMaxOrDefaultPipSizeSpec(mPipBoundsState); + } +} 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 0f3ff36601fb..8e3376f163c1 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 @@ -146,11 +146,8 @@ public class PipInputConsumer { "%s: Failed to create input consumer, %s", TAG, e); } mMainExecutor.execute(() -> { - // Choreographer.getSfInstance() must be called on the thread that the input event - // receiver should be receiving events - // TODO(b/222697646): remove getSfInstance usage and use vsyncId for transactions mInputEventReceiver = new InputEventReceiver(inputChannel, - Looper.myLooper(), Choreographer.getSfInstance()); + Looper.myLooper(), Choreographer.getInstance()); if (mRegistrationListener != null) { mRegistrationListener.onRegistrationChanged(true /* isRegistered */); } 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 6390c8984dac..979b7c7dc31f 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 @@ -207,6 +207,9 @@ public class PipMenuView extends FrameLayout { } }); + // this disables the ripples + mEnterSplitButton.setEnabled(false); + findViewById(R.id.resize_handle).setAlpha(0); mActionsGroup = findViewById(R.id.actions_group); @@ -282,7 +285,7 @@ public class PipMenuView extends FrameLayout { && mSplitScreenControllerOptional.get().isTaskInSplitScreen(taskInfo.taskId); mFocusedTaskAllowSplitScreen = isSplitScreen || (taskInfo.getWindowingMode() == WINDOWING_MODE_FULLSCREEN - && taskInfo.supportsSplitScreenMultiWindow + && taskInfo.supportsMultiWindow && taskInfo.topActivityType != WindowConfiguration.ACTIVITY_TYPE_HOME); } 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 5a21e0734277..afb64c9eec41 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java @@ -33,10 +33,7 @@ import android.content.Context; import android.graphics.PointF; import android.graphics.Rect; import android.os.Debug; -import android.os.Looper; -import android.view.Choreographer; - -import androidx.dynamicanimation.animation.FrameCallbackScheduler; +import android.os.SystemProperties; import com.android.internal.protolog.common.ProtoLog; import com.android.wm.shell.R; @@ -62,6 +59,8 @@ import kotlin.jvm.functions.Function0; public class PipMotionHelper implements PipAppOpsListener.Callback, FloatingContentCoordinator.FloatingContent { + public static final boolean ENABLE_FLING_TO_DISMISS_PIP = + SystemProperties.getBoolean("persist.wm.debug.fling_to_dismiss_pip", true); private static final String TAG = "PipMotionHelper"; private static final boolean DEBUG = false; @@ -89,25 +88,6 @@ public class PipMotionHelper implements PipAppOpsListener.Callback, /** Coordinator instance for resolving conflicts with other floating content. */ private FloatingContentCoordinator mFloatingContentCoordinator; - private ThreadLocal<FrameCallbackScheduler> mSfSchedulerThreadLocal = - ThreadLocal.withInitial(() -> { - final Looper initialLooper = Looper.myLooper(); - final FrameCallbackScheduler scheduler = new FrameCallbackScheduler() { - @Override - public void postFrameCallback(@androidx.annotation.NonNull Runnable runnable) { - // TODO(b/222697646): remove getSfInstance usage and use vsyncId for - // transactions - Choreographer.getSfInstance().postFrameCallback(t -> runnable.run()); - } - - @Override - public boolean isCurrentThread() { - return Looper.myLooper() == initialLooper; - } - }; - return scheduler; - }); - /** * PhysicsAnimator instance for animating {@link PipBoundsState#getMotionBoundsState()} * using physics animations. @@ -210,10 +190,8 @@ public class PipMotionHelper implements PipAppOpsListener.Callback, } public void init() { - // Note: Needs to get the shell main thread sf vsync animation handler mTemporaryBoundsPhysicsAnimator = PhysicsAnimator.getInstance( mPipBoundsState.getMotionBoundsState().getBoundsInMotion()); - mTemporaryBoundsPhysicsAnimator.setCustomScheduler(mSfSchedulerThreadLocal.get()); } @NonNull @@ -729,6 +707,7 @@ public class PipMotionHelper implements PipAppOpsListener.Callback, loc[1] = animatedPipBounds.top; } }; + mMagnetizedPip.setFlingToTargetEnabled(ENABLE_FLING_TO_DISMISS_PIP); } return mMagnetizedPip; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipResizeGestureHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipResizeGestureHandler.java index abf1a9500e6d..89d85e4b292d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipResizeGestureHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipResizeGestureHandler.java @@ -625,8 +625,7 @@ public class PipResizeGestureHandler { class PipResizeInputEventReceiver extends BatchedInputEventReceiver { PipResizeInputEventReceiver(InputChannel channel, Looper looper) { - // TODO(b/222697646): remove getSfInstance usage and use vsyncId for transactions - super(channel, looper, Choreographer.getSfInstance()); + super(channel, looper, Choreographer.getInstance()); } public void onInputEvent(InputEvent event) { 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 ac7b9033b2b9..1f3f31e025a0 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 @@ -34,6 +34,7 @@ import android.content.res.Resources; import android.graphics.Point; import android.graphics.PointF; import android.graphics.Rect; +import android.os.SystemProperties; import android.provider.DeviceConfig; import android.util.Size; import android.view.DisplayCutout; @@ -54,8 +55,10 @@ import com.android.wm.shell.pip.PipAnimationController; import com.android.wm.shell.pip.PipBoundsAlgorithm; import com.android.wm.shell.pip.PipBoundsState; import com.android.wm.shell.pip.PipTaskOrganizer; +import com.android.wm.shell.pip.PipTransitionController; import com.android.wm.shell.pip.PipUiEventLogger; import com.android.wm.shell.protolog.ShellProtoLogGroup; +import com.android.wm.shell.sysui.ShellInit; import java.io.PrintWriter; @@ -68,6 +71,9 @@ public class PipTouchHandler { private static final String TAG = "PipTouchHandler"; private static final float DEFAULT_STASH_VELOCITY_THRESHOLD = 18000.f; + private static final boolean ENABLE_PIP_KEEP_CLEAR_ALGORITHM = + SystemProperties.getBoolean("persist.wm.debug.enable_pip_keep_clear_algorithm", false); + // Allow PIP to resize to a slightly bigger state upon touch private boolean mEnableResize; private final Context mContext; @@ -164,6 +170,7 @@ public class PipTouchHandler { @SuppressLint("InflateParams") public PipTouchHandler(Context context, + ShellInit shellInit, PhonePipMenuController menuController, PipBoundsAlgorithm pipBoundsAlgorithm, @NonNull PipBoundsState pipBoundsState, @@ -172,7 +179,6 @@ public class PipTouchHandler { FloatingContentCoordinator floatingContentCoordinator, PipUiEventLogger pipUiEventLogger, ShellExecutor mainExecutor) { - // Initialize the Pip input consumer mContext = context; mMainExecutor = mainExecutor; mAccessibilityManager = context.getSystemService(AccessibilityManager.class); @@ -212,9 +218,17 @@ public class PipTouchHandler { mMotionHelper, pipTaskOrganizer, mPipBoundsAlgorithm.getSnapAlgorithm(), this::onAccessibilityShowMenu, this::updateMovementBounds, this::animateToUnStashedState, mainExecutor); + + // TODO(b/181599115): This should really be initializes as part of the pip controller, but + // until all PIP implementations derive from the controller, just initialize the touch handler + // if it is needed + shellInit.addInitCallback(this::onInit, this); } - public void init() { + /** + * Called when the touch handler is initialized. + */ + public void onInit() { Resources res = mContext.getResources(); mEnableResize = res.getBoolean(R.bool.config_pipEnableResizeForMenu); reloadResources(); @@ -250,6 +264,10 @@ public class PipTouchHandler { }); } + public PipTransitionController getTransitionHandler() { + return mPipTaskOrganizer.getTransitionController(); + } + private void reloadResources() { final Resources res = mContext.getResources(); mBottomOffsetBufferPx = res.getDimensionPixelSize(R.dimen.pip_bottom_offset_buffer); @@ -398,13 +416,7 @@ public class PipTouchHandler { mPipBoundsState.getExpandedBounds(), insetBounds, expandedMovementBounds, bottomOffset); - if (mPipResizeGestureHandler.isUsingPinchToZoom()) { - updatePinchResizeSizeConstraints(insetBounds, normalBounds, aspectRatio); - } else { - mPipResizeGestureHandler.updateMinSize(normalBounds.width(), normalBounds.height()); - mPipResizeGestureHandler.updateMaxSize(mPipBoundsState.getExpandedBounds().width(), - mPipBoundsState.getExpandedBounds().height()); - } + updatePipSizeConstraints(insetBounds, normalBounds, aspectRatio); // The extra offset does not really affect the movement bounds, but are applied based on the // current state (ime showing, or shelf offset) when we need to actually shift @@ -418,6 +430,9 @@ public class PipTouchHandler { if (mTouchState.isUserInteracting()) { // Defer the update of the current movement bounds until after the user finishes // touching the screen + } else if (ENABLE_PIP_KEEP_CLEAR_ALGORITHM) { + // Ignore moving PiP if keep clear algorithm is enabled, since IME and shelf height + // now are accounted for in the keep clear algorithm calculations } else { final boolean isExpanded = mMenuState == MENU_STATE_FULL && willResizeMenu(); final Rect toMovementBounds = new Rect(); @@ -473,6 +488,27 @@ public class PipTouchHandler { } } + /** + * Update the values for min/max allowed size of picture in picture window based on the aspect + * ratio. + * @param aspectRatio aspect ratio to use for the calculation of min/max size + */ + public void updateMinMaxSize(float aspectRatio) { + updatePipSizeConstraints(mInsetBounds, mPipBoundsState.getNormalBounds(), + aspectRatio); + } + + private void updatePipSizeConstraints(Rect insetBounds, Rect normalBounds, + float aspectRatio) { + if (mPipResizeGestureHandler.isUsingPinchToZoom()) { + updatePinchResizeSizeConstraints(insetBounds, normalBounds, aspectRatio); + } else { + mPipResizeGestureHandler.updateMinSize(normalBounds.width(), normalBounds.height()); + mPipResizeGestureHandler.updateMaxSize(mPipBoundsState.getExpandedBounds().width(), + mPipBoundsState.getExpandedBounds().height()); + } + } + private void updatePinchResizeSizeConstraints(Rect insetBounds, Rect normalBounds, float aspectRatio) { final int shorterLength = Math.min(mPipBoundsState.getDisplayBounds().width(), @@ -900,9 +936,18 @@ public class PipTouchHandler { if (mMenuController.isMenuVisible()) { mMenuController.hideMenu(ANIM_TYPE_NONE, false /* resize */); } - if (toExpand) { + + // the size to toggle to after a double tap + int nextSize = PipDoubleTapHelper + .nextSizeSpec(mPipBoundsState, getUserResizeBounds()); + + // actually toggle to the size chosen + if (nextSize == PipDoubleTapHelper.SIZE_SPEC_MAX) { mPipResizeGestureHandler.setUserResizeBounds(mPipBoundsState.getBounds()); animateToMaximizedState(null); + } else if (nextSize == PipDoubleTapHelper.SIZE_SPEC_DEFAULT) { + mPipResizeGestureHandler.setUserResizeBounds(mPipBoundsState.getBounds()); + animateToNormalSize(null); } else { animateToUnexpandedState(getUserResizeBounds()); } 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 a2eadcdf6210..ce34d2f9547d 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 @@ -39,6 +39,7 @@ import com.android.internal.protolog.common.ProtoLog; import com.android.wm.shell.R; import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.pip.PipBoundsAlgorithm; +import com.android.wm.shell.pip.PipKeepClearAlgorithm; import com.android.wm.shell.pip.PipSnapAlgorithm; import com.android.wm.shell.pip.tv.TvPipKeepClearAlgorithm.Placement; import com.android.wm.shell.protolog.ShellProtoLogGroup; @@ -63,7 +64,8 @@ public class TvPipBoundsAlgorithm extends PipBoundsAlgorithm { public TvPipBoundsAlgorithm(Context context, @NonNull TvPipBoundsState tvPipBoundsState, @NonNull PipSnapAlgorithm pipSnapAlgorithm) { - super(context, tvPipBoundsState, pipSnapAlgorithm); + super(context, tvPipBoundsState, pipSnapAlgorithm, + new PipKeepClearAlgorithm() {}); this.mTvPipBoundsState = tvPipBoundsState; this.mKeepClearAlgorithm = new TvPipKeepClearAlgorithm(); reloadResources(context); 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 fa48def9c7d7..4e1b0469eb96 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java @@ -32,6 +32,8 @@ import android.graphics.Rect; import android.os.RemoteException; import android.view.Gravity; +import androidx.annotation.NonNull; + import com.android.internal.protolog.common.ProtoLog; import com.android.wm.shell.R; import com.android.wm.shell.WindowManagerShellWrapper; @@ -49,6 +51,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.protolog.ShellProtoLogGroup; +import com.android.wm.shell.sysui.ConfigurationChangeListener; +import com.android.wm.shell.sysui.ShellController; +import com.android.wm.shell.sysui.ShellInit; +import com.android.wm.shell.sysui.UserChangeListener; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -61,7 +67,8 @@ import java.util.Set; */ public class TvPipController implements PipTransitionController.PipTransitionCallback, TvPipBoundsController.PipBoundsListener, TvPipMenuController.Delegate, - TvPipNotificationController.Delegate, DisplayController.OnDisplaysChangedListener { + TvPipNotificationController.Delegate, DisplayController.OnDisplaysChangedListener, + ConfigurationChangeListener, UserChangeListener { private static final String TAG = "TvPipController"; static final boolean DEBUG = false; @@ -93,6 +100,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal private final Context mContext; + private final ShellController mShellController; private final TvPipBoundsState mTvPipBoundsState; private final TvPipBoundsAlgorithm mTvPipBoundsAlgorithm; private final TvPipBoundsController mTvPipBoundsController; @@ -101,6 +109,11 @@ public class TvPipController implements PipTransitionController.PipTransitionCal private final PipMediaController mPipMediaController; private final TvPipNotificationController mPipNotificationController; private final TvPipMenuController mTvPipMenuController; + private final PipTransitionController mPipTransitionController; + private final TaskStackListenerImpl mTaskStackListener; + private final PipParamsChangedForwarder mPipParamsChangedForwarder; + private final DisplayController mDisplayController; + private final WindowManagerShellWrapper mWmShellWrapper; private final ShellExecutor mMainExecutor; private final TvPipImpl mImpl = new TvPipImpl(); @@ -117,6 +130,8 @@ public class TvPipController implements PipTransitionController.PipTransitionCal public static Pip create( Context context, + ShellInit shellInit, + ShellController shellController, TvPipBoundsState tvPipBoundsState, TvPipBoundsAlgorithm tvPipBoundsAlgorithm, TvPipBoundsController tvPipBoundsController, @@ -133,6 +148,8 @@ public class TvPipController implements PipTransitionController.PipTransitionCal ShellExecutor mainExecutor) { return new TvPipController( context, + shellInit, + shellController, tvPipBoundsState, tvPipBoundsAlgorithm, tvPipBoundsController, @@ -151,6 +168,8 @@ public class TvPipController implements PipTransitionController.PipTransitionCal private TvPipController( Context context, + ShellInit shellInit, + ShellController shellController, TvPipBoundsState tvPipBoundsState, TvPipBoundsAlgorithm tvPipBoundsAlgorithm, TvPipBoundsController tvPipBoundsController, @@ -163,10 +182,12 @@ public class TvPipController implements PipTransitionController.PipTransitionCal TaskStackListenerImpl taskStackListener, PipParamsChangedForwarder pipParamsChangedForwarder, DisplayController displayController, - WindowManagerShellWrapper wmShell, + WindowManagerShellWrapper wmShellWrapper, ShellExecutor mainExecutor) { mContext = context; mMainExecutor = mainExecutor; + mShellController = shellController; + mDisplayController = displayController; mTvPipBoundsState = tvPipBoundsState; mTvPipBoundsState.setDisplayId(context.getDisplayId()); @@ -185,17 +206,36 @@ public class TvPipController implements PipTransitionController.PipTransitionCal mAppOpsListener = pipAppOpsListener; mPipTaskOrganizer = pipTaskOrganizer; - pipTransitionController.registerPipTransitionCallback(this); + mPipTransitionController = pipTransitionController; + mPipParamsChangedForwarder = pipParamsChangedForwarder; + mTaskStackListener = taskStackListener; + mWmShellWrapper = wmShellWrapper; + shellInit.addInitCallback(this::onInit, this); + } + + private void onInit() { + mPipTransitionController.registerPipTransitionCallback(this); loadConfigurations(); - registerPipParamsChangedListener(pipParamsChangedForwarder); - registerTaskStackListenerCallback(taskStackListener); - registerWmShellPinnedStackListener(wmShell); - displayController.addDisplayWindowListener(this); + registerPipParamsChangedListener(mPipParamsChangedForwarder); + registerTaskStackListenerCallback(mTaskStackListener); + registerWmShellPinnedStackListener(mWmShellWrapper); + registerSessionListenerForCurrentUser(); + mDisplayController.addDisplayWindowListener(this); + + mShellController.addConfigurationChangeListener(this); + mShellController.addUserChangeListener(this); + } + + @Override + public void onUserChanged(int newUserId, @NonNull Context userContext) { + // Re-register the media session listener when switching users + registerSessionListenerForCurrentUser(); } - private void onConfigurationChanged(Configuration newConfig) { + @Override + public void onConfigurationChanged(Configuration newConfig) { if (DEBUG) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: onConfigurationChanged(), state=%s", TAG, stateToName(mState)); @@ -668,18 +708,6 @@ public class TvPipController implements PipTransitionController.PipTransitionCal } private class TvPipImpl implements Pip { - @Override - public void onConfigurationChanged(Configuration newConfig) { - mMainExecutor.execute(() -> { - TvPipController.this.onConfigurationChanged(newConfig); - }); - } - - @Override - public void registerSessionListenerForCurrentUser() { - mMainExecutor.execute(() -> { - TvPipController.this.registerSessionListenerForCurrentUser(); - }); - } + // Not used } } 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 5062cc436461..8ebcf63f36e9 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 @@ -32,6 +32,7 @@ import com.android.wm.shell.pip.PipAnimationController; import com.android.wm.shell.pip.PipBoundsState; import com.android.wm.shell.pip.PipMenuController; import com.android.wm.shell.pip.PipTransitionController; +import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.transition.Transitions; /** @@ -39,14 +40,16 @@ import com.android.wm.shell.transition.Transitions; * TODO: Implement animation once TV is using Transitions. */ public class TvPipTransition extends PipTransitionController { - public TvPipTransition(PipBoundsState pipBoundsState, + public TvPipTransition( + @NonNull ShellInit shellInit, + @NonNull ShellTaskOrganizer shellTaskOrganizer, + @NonNull Transitions transitions, + PipBoundsState pipBoundsState, PipMenuController pipMenuController, TvPipBoundsAlgorithm tvPipBoundsAlgorithm, - PipAnimationController pipAnimationController, - Transitions transitions, - @NonNull ShellTaskOrganizer shellTaskOrganizer) { - super(pipBoundsState, pipMenuController, tvPipBoundsAlgorithm, pipAnimationController, - transitions, shellTaskOrganizer); + PipAnimationController pipAnimationController) { + super(shellInit, shellTaskOrganizer, transitions, pipBoundsState, pipMenuController, + tvPipBoundsAlgorithm, pipAnimationController); } @Override 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 d04c34916256..c52ed249c2ca 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 @@ -26,11 +26,13 @@ import com.android.internal.protolog.common.IProtoLogGroup; public enum ShellProtoLogGroup implements IProtoLogGroup { // NOTE: Since we enable these from the same WM ShellCommand, these names should not conflict // with those in the framework ProtoLogGroup + WM_SHELL_INIT(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, true, + Consts.TAG_WM_SHELL), WM_SHELL_TASK_ORG(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false, Consts.TAG_WM_SHELL), WM_SHELL_TRANSITIONS(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, true, Consts.TAG_WM_SHELL), - WM_SHELL_DRAG_AND_DROP(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false, + WM_SHELL_DRAG_AND_DROP(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, true, Consts.TAG_WM_SHELL), WM_SHELL_STARTING_WINDOW(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false, Consts.TAG_WM_STARTING_WINDOW), @@ -38,10 +40,16 @@ public enum ShellProtoLogGroup implements IProtoLogGroup { "ShellBackPreview"), WM_SHELL_RECENT_TASKS(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false, Consts.TAG_WM_SHELL), - WM_SHELL_PICTURE_IN_PICTURE(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, - false, Consts.TAG_WM_SHELL), + WM_SHELL_PICTURE_IN_PICTURE(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false, + Consts.TAG_WM_SHELL), WM_SHELL_SPLIT_SCREEN(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false, Consts.TAG_WM_SHELL), + WM_SHELL_SYSUI_EVENTS(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false, + Consts.TAG_WM_SHELL), + WM_SHELL_DESKTOP_MODE(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false, + Consts.TAG_WM_SHELL), + WM_SHELL_FLOATING_APPS(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false, + Consts.TAG_WM_SHELL), TEST_GROUP(true, true, false, "WindowManagerShellProtoLogTest"); private final boolean mEnabled; 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 6e78fcba4a00..b71cc32a0347 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 @@ -16,6 +16,8 @@ package com.android.wm.shell.recents; +import android.app.ActivityManager; + import com.android.wm.shell.recents.IRecentTasksListener; import com.android.wm.shell.util.GroupedRecentTaskInfo; @@ -38,4 +40,9 @@ interface IRecentTasks { * Gets the set of recent tasks. */ GroupedRecentTaskInfo[] getRecentTasks(int maxNum, int flags, int userId) = 3; + + /** + * Gets the set of running tasks. + */ + ActivityManager.RunningTaskInfo[] getRunningTasks(int maxNum) = 4; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasksListener.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasksListener.aidl index 8efa42830d80..59f72335678e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasksListener.aidl +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasksListener.aidl @@ -16,6 +16,8 @@ package com.android.wm.shell.recents; +import android.app.ActivityManager; + /** * Listener interface that Launcher attaches to SystemUI to get split-screen callbacks. */ @@ -25,4 +27,14 @@ oneway interface IRecentTasksListener { * Called when the set of recent tasks change. */ void onRecentTasksChanged(); + + /** + * Called when a running task appears. + */ + void onRunningTaskAppeared(in ActivityManager.RunningTaskInfo taskInfo); + + /** + * Called when a running task vanishes. + */ + void onRunningTaskVanished(in ActivityManager.RunningTaskInfo taskInfo); }
\ No newline at end of file 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 a5748f69388f..2a625524b48b 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 @@ -17,6 +17,11 @@ package com.android.wm.shell.recents; import com.android.wm.shell.common.annotations.ExternalThread; +import com.android.wm.shell.util.GroupedRecentTaskInfo; + +import java.util.List; +import java.util.concurrent.Executor; +import java.util.function.Consumer; /** * Interface for interacting with the recent tasks. @@ -29,4 +34,11 @@ public interface RecentTasks { default IRecentTasks createExternalInterface() { return null; } + + /** + * Gets the set of recent tasks. + */ + default void getRecentTasks(int maxNum, int flags, int userId, Executor callbackExecutor, + Consumer<List<GroupedRecentTaskInfo>> callback) { + } } 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 c166178e9bbd..02b5a35f653b 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 @@ -17,12 +17,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.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission; import android.app.ActivityManager; import android.app.ActivityTaskManager; import android.app.TaskInfo; +import android.content.ComponentName; import android.content.Context; import android.os.RemoteException; import android.util.Slog; @@ -42,39 +44,50 @@ import com.android.wm.shell.common.TaskStackListenerCallback; import com.android.wm.shell.common.TaskStackListenerImpl; import com.android.wm.shell.common.annotations.ExternalThread; import com.android.wm.shell.common.annotations.ShellMainThread; +import com.android.wm.shell.desktopmode.DesktopModeStatus; +import com.android.wm.shell.desktopmode.DesktopModeTaskRepository; import com.android.wm.shell.protolog.ShellProtoLogGroup; +import com.android.wm.shell.sysui.ShellCommandHandler; +import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.util.GroupedRecentTaskInfo; -import com.android.wm.shell.util.StagedSplitBounds; +import com.android.wm.shell.util.SplitBounds; import java.io.PrintWriter; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; +import java.util.concurrent.Executor; +import java.util.function.Consumer; /** * Manages the recent task list from the system, caching it as necessary. */ public class RecentTasksController implements TaskStackListenerCallback, - RemoteCallable<RecentTasksController> { + RemoteCallable<RecentTasksController>, DesktopModeTaskRepository.Listener { private static final String TAG = RecentTasksController.class.getSimpleName(); private final Context mContext; + private final ShellCommandHandler mShellCommandHandler; + private final Optional<DesktopModeTaskRepository> mDesktopModeTaskRepository; private final ShellExecutor mMainExecutor; private final TaskStackListenerImpl mTaskStackListener; private final RecentTasks mImpl = new RecentTasksImpl(); + private final ActivityTaskManager mActivityTaskManager; + private IRecentTasksListener mListener; + private final boolean mIsDesktopMode; - private final ArrayList<Runnable> mCallbacks = new ArrayList<>(); // Mapping of split task ids, mappings are symmetrical (ie. if t1 is the taskid of a task in a // pair, then mSplitTasks[t1] = t2, and mSplitTasks[t2] = t1) private final SparseIntArray mSplitTasks = new SparseIntArray(); /** - * Maps taskId to {@link StagedSplitBounds} for both taskIDs. + * Maps taskId to {@link SplitBounds} for both taskIDs. * Meaning there will be two taskId integers mapping to the same object. * If there's any ordering to the pairing than we can probably just get away with only one * taskID mapping to it, leaving both for consistency with {@link #mSplitTasks} for now. */ - private final Map<Integer, StagedSplitBounds> mTaskSplitBoundsMap = new HashMap<>(); + private final Map<Integer, SplitBounds> mTaskSplitBoundsMap = new HashMap<>(); /** * Creates {@link RecentTasksController}, returns {@code null} if the feature is not @@ -83,34 +96,51 @@ public class RecentTasksController implements TaskStackListenerCallback, @Nullable public static RecentTasksController create( Context context, + ShellInit shellInit, + ShellCommandHandler shellCommandHandler, TaskStackListenerImpl taskStackListener, + ActivityTaskManager activityTaskManager, + Optional<DesktopModeTaskRepository> desktopModeTaskRepository, @ShellMainThread ShellExecutor mainExecutor ) { if (!context.getResources().getBoolean(com.android.internal.R.bool.config_hasRecents)) { return null; } - return new RecentTasksController(context, taskStackListener, mainExecutor); + return new RecentTasksController(context, shellInit, shellCommandHandler, taskStackListener, + activityTaskManager, desktopModeTaskRepository, mainExecutor); } - RecentTasksController(Context context, TaskStackListenerImpl taskStackListener, + RecentTasksController(Context context, + ShellInit shellInit, + ShellCommandHandler shellCommandHandler, + TaskStackListenerImpl taskStackListener, + ActivityTaskManager activityTaskManager, + Optional<DesktopModeTaskRepository> desktopModeTaskRepository, ShellExecutor mainExecutor) { mContext = context; + mShellCommandHandler = shellCommandHandler; + mActivityTaskManager = activityTaskManager; + mIsDesktopMode = mContext.getPackageManager().hasSystemFeature(FEATURE_PC); mTaskStackListener = taskStackListener; + mDesktopModeTaskRepository = desktopModeTaskRepository; mMainExecutor = mainExecutor; + shellInit.addInitCallback(this::onInit, this); } public RecentTasks asRecentTasks() { return mImpl; } - public void init() { + private void onInit() { + mShellCommandHandler.addDumpCallback(this::dump, this); mTaskStackListener.addListener(this); + mDesktopModeTaskRepository.ifPresent(it -> it.addListener(this)); } /** * Adds a split pair. This call does not validate the taskIds, only that they are not the same. */ - public void addSplitPair(int taskId1, int taskId2, StagedSplitBounds splitBounds) { + public void addSplitPair(int taskId1, int taskId2, SplitBounds splitBounds) { if (taskId1 == taskId2) { return; } @@ -176,44 +206,80 @@ public class RecentTasksController implements TaskStackListenerCallback, notifyRecentTasksChanged(); } - public void onTaskRemoved(TaskInfo taskInfo) { + public void onTaskAdded(ActivityManager.RunningTaskInfo taskInfo) { + notifyRunningTaskAppeared(taskInfo); + } + + public void onTaskRemoved(ActivityManager.RunningTaskInfo taskInfo) { // Remove any split pairs associated with this task removeSplitPair(taskInfo.taskId); notifyRecentTasksChanged(); + notifyRunningTaskVanished(taskInfo); } public void onTaskWindowingModeChanged(TaskInfo taskInfo) { notifyRecentTasksChanged(); } + @Override + public void onActiveTasksChanged() { + notifyRecentTasksChanged(); + } + @VisibleForTesting void notifyRecentTasksChanged() { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENT_TASKS, "Notify recent tasks changed"); - for (int i = 0; i < mCallbacks.size(); i++) { - mCallbacks.get(i).run(); + if (mListener == null) { + return; + } + try { + mListener.onRecentTasksChanged(); + } catch (RemoteException e) { + Slog.w(TAG, "Failed call notifyRecentTasksChanged", e); + } + } + + /** + * Notify the running task listener that a task appeared on desktop environment. + */ + private void notifyRunningTaskAppeared(ActivityManager.RunningTaskInfo taskInfo) { + if (mListener == null || !mIsDesktopMode || taskInfo.realActivity == null) { + return; + } + try { + mListener.onRunningTaskAppeared(taskInfo); + } catch (RemoteException e) { + Slog.w(TAG, "Failed call onRunningTaskAppeared", e); } } - private void registerRecentTasksListener(Runnable listener) { - if (!mCallbacks.contains(listener)) { - mCallbacks.add(listener); + /** + * Notify the running task listener that a task was removed on desktop environment. + */ + private void notifyRunningTaskVanished(ActivityManager.RunningTaskInfo taskInfo) { + if (mListener == null || !mIsDesktopMode || taskInfo.realActivity == null) { + return; + } + try { + mListener.onRunningTaskVanished(taskInfo); + } catch (RemoteException e) { + Slog.w(TAG, "Failed call onRunningTaskVanished", e); } } - private void unregisterRecentTasksListener(Runnable listener) { - mCallbacks.remove(listener); + private void registerRecentTasksListener(IRecentTasksListener listener) { + mListener = listener; } - @VisibleForTesting - List<ActivityManager.RecentTaskInfo> getRawRecentTasks(int maxNum, int flags, int userId) { - return ActivityTaskManager.getInstance().getRecentTasks(maxNum, flags, userId); + private void unregisterRecentTasksListener() { + mListener = null; } @VisibleForTesting ArrayList<GroupedRecentTaskInfo> getRecentTasks(int maxNum, int flags, int userId) { // Note: the returned task list is from the most-recent to least-recent order - final List<ActivityManager.RecentTaskInfo> rawList = getRawRecentTasks(maxNum, flags, - userId); + final List<ActivityManager.RecentTaskInfo> rawList = mActivityTaskManager.getRecentTasks( + maxNum, flags, userId); // Make a mapping of task id -> task info final SparseArray<ActivityManager.RecentTaskInfo> rawMapping = new SparseArray<>(); @@ -222,6 +288,9 @@ public class RecentTasksController implements TaskStackListenerCallback, rawMapping.put(taskInfo.taskId, taskInfo); } + boolean desktopModeActive = DesktopModeStatus.isActive(mContext); + ArrayList<ActivityManager.RecentTaskInfo> freeformTasks = new ArrayList<>(); + // Pull out the pairs as we iterate back in the list ArrayList<GroupedRecentTaskInfo> recentTasks = new ArrayList<>(); for (int i = 0; i < rawList.size(); i++) { @@ -231,19 +300,57 @@ public class RecentTasksController implements TaskStackListenerCallback, continue; } + if (desktopModeActive && mDesktopModeTaskRepository.isPresent() + && mDesktopModeTaskRepository.get().isActiveTask(taskInfo.taskId)) { + // Freeform tasks will be added as a separate entry + freeformTasks.add(taskInfo); + continue; + } + final int pairedTaskId = mSplitTasks.get(taskInfo.taskId); - if (pairedTaskId != INVALID_TASK_ID && rawMapping.contains(pairedTaskId)) { + if (!desktopModeActive && pairedTaskId != INVALID_TASK_ID && rawMapping.contains( + pairedTaskId)) { final ActivityManager.RecentTaskInfo pairedTaskInfo = rawMapping.get(pairedTaskId); rawMapping.remove(pairedTaskId); - recentTasks.add(new GroupedRecentTaskInfo(taskInfo, pairedTaskInfo, + recentTasks.add(GroupedRecentTaskInfo.forSplitTasks(taskInfo, pairedTaskInfo, mTaskSplitBoundsMap.get(pairedTaskId))); } else { - recentTasks.add(new GroupedRecentTaskInfo(taskInfo)); + recentTasks.add(GroupedRecentTaskInfo.forSingleTask(taskInfo)); } } + + // Add a special entry for freeform tasks + if (!freeformTasks.isEmpty()) { + recentTasks.add(0, GroupedRecentTaskInfo.forFreeformTasks( + freeformTasks.toArray(new ActivityManager.RecentTaskInfo[0]))); + } + return recentTasks; } + /** + * Find the background task that match the given component. + */ + @Nullable + public ActivityManager.RecentTaskInfo findTaskInBackground(ComponentName componentName) { + if (componentName == null) { + return null; + } + List<ActivityManager.RecentTaskInfo> tasks = mActivityTaskManager.getRecentTasks( + Integer.MAX_VALUE, ActivityManager.RECENT_IGNORE_UNAVAILABLE, + ActivityManager.getCurrentUser()); + for (int i = 0; i < tasks.size(); i++) { + final ActivityManager.RecentTaskInfo task = tasks.get(i); + if (task.isVisible) { + continue; + } + if (componentName.equals(task.baseIntent.getComponent())) { + return task; + } + } + return null; + } + public void dump(@NonNull PrintWriter pw, String prefix) { final String innerPrefix = prefix + " "; pw.println(prefix + TAG); @@ -269,6 +376,16 @@ public class RecentTasksController implements TaskStackListenerCallback, mIRecentTasks = new IRecentTasksImpl(RecentTasksController.this); return mIRecentTasks; } + + @Override + public void getRecentTasks(int maxNum, int flags, int userId, Executor executor, + Consumer<List<GroupedRecentTaskInfo>> callback) { + mMainExecutor.execute(() -> { + List<GroupedRecentTaskInfo> tasks = + RecentTasksController.this.getRecentTasks(maxNum, flags, userId); + executor.execute(() -> callback.accept(tasks)); + }); + } } @@ -280,19 +397,28 @@ public class RecentTasksController implements TaskStackListenerCallback, private RecentTasksController mController; private final SingleInstanceRemoteListener<RecentTasksController, IRecentTasksListener> mListener; - private final Runnable mRecentTasksListener = - new Runnable() { - @Override - public void run() { - mListener.call(l -> l.onRecentTasksChanged()); - } - }; + private final IRecentTasksListener mRecentTasksListener = new IRecentTasksListener.Stub() { + @Override + public void onRecentTasksChanged() throws RemoteException { + mListener.call(l -> l.onRecentTasksChanged()); + } + + @Override + public void onRunningTaskAppeared(ActivityManager.RunningTaskInfo taskInfo) { + mListener.call(l -> l.onRunningTaskAppeared(taskInfo)); + } + + @Override + public void onRunningTaskVanished(ActivityManager.RunningTaskInfo taskInfo) { + mListener.call(l -> l.onRunningTaskVanished(taskInfo)); + } + }; public IRecentTasksImpl(RecentTasksController controller) { mController = controller; mListener = new SingleInstanceRemoteListener<>(controller, c -> c.registerRecentTasksListener(mRecentTasksListener), - c -> c.unregisterRecentTasksListener(mRecentTasksListener)); + c -> c.unregisterRecentTasksListener()); } /** @@ -331,5 +457,16 @@ public class RecentTasksController implements TaskStackListenerCallback, true /* blocking */); return out[0]; } + + @Override + public ActivityManager.RunningTaskInfo[] getRunningTasks(int maxNum) { + final ActivityManager.RunningTaskInfo[][] tasks = + new ActivityManager.RunningTaskInfo[][] {null}; + executeRemoteCallWithTaskPermission(mController, "getRunningTasks", + (controller) -> tasks[0] = ActivityTaskManager.getInstance().getTasks(maxNum) + .toArray(new ActivityManager.RunningTaskInfo[0]), + true /* blocking */); + return tasks[0]; + } } }
\ No newline at end of file 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 51921e747f1a..ecdafa9a63f4 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 @@ -18,8 +18,10 @@ package com.android.wm.shell.splitscreen; import android.app.PendingIntent; import android.content.Intent; +import android.content.pm.ShortcutInfo; import android.os.Bundle; import android.os.UserHandle; +import com.android.internal.logging.InstanceId; import android.view.RemoteAnimationAdapter; import android.view.RemoteAnimationTarget; import android.window.RemoteTransition; @@ -66,34 +68,35 @@ interface ISplitScreen { * Starts a shortcut in a stage. */ oneway void startShortcut(String packageName, String shortcutId, int position, - in Bundle options, in UserHandle user) = 8; + in Bundle options, in UserHandle user, in InstanceId instanceId) = 8; /** * Starts an activity in a stage. */ oneway void startIntent(in PendingIntent intent, in Intent fillInIntent, int position, - in Bundle options) = 9; + in Bundle options, in InstanceId instanceId) = 9; /** * Starts tasks simultaneously in one transition. */ oneway void startTasks(int mainTaskId, in Bundle mainOptions, int sideTaskId, in Bundle sideOptions, int sidePosition, float splitRatio, - in RemoteTransition remoteTransition) = 10; + in RemoteTransition remoteTransition, in InstanceId instanceId) = 10; /** * Version of startTasks using legacy transition system. */ oneway void startTasksWithLegacyTransition(int mainTaskId, in Bundle mainOptions, int sideTaskId, in Bundle sideOptions, int sidePosition, - float splitRatio, in RemoteAnimationAdapter adapter) = 11; + float splitRatio, in RemoteAnimationAdapter adapter, in InstanceId instanceId) = 11; /** - * Start a pair of intent and task using legacy transition system. + * Starts a pair of intent and task using legacy transition system. */ oneway void startIntentAndTaskWithLegacyTransition(in PendingIntent pendingIntent, in Intent fillInIntent, int taskId, in Bundle mainOptions,in Bundle sideOptions, - int sidePosition, float splitRatio, in RemoteAnimationAdapter adapter) = 12; + int sidePosition, float splitRatio, in RemoteAnimationAdapter adapter, + in InstanceId instanceId) = 12; /** * Blocking call that notifies and gets additional split-screen targets when entering @@ -108,4 +111,11 @@ interface ISplitScreen { * does not expect split to currently be running. */ RemoteAnimationTarget[] onStartingSplitLegacy(in RemoteAnimationTarget[] appTargets) = 14; + + /** + * Starts a pair of shortcut and task using legacy transition system. + */ + oneway void startShortcutAndTaskWithLegacyTransition(in ShortcutInfo shortcutInfo, int taskId, + in Bundle mainOptions, in Bundle sideOptions, int sidePosition, float splitRatio, + in RemoteAnimationAdapter adapter, in InstanceId instanceId) = 15; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/MainStage.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/MainStage.java index ae5e075c4d3f..e7ec15e70c11 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/MainStage.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/MainStage.java @@ -16,7 +16,9 @@ package com.android.wm.shell.splitscreen; -import android.annotation.Nullable; +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 android.content.Context; import android.view.SurfaceSession; import android.window.WindowContainerToken; @@ -38,10 +40,9 @@ class MainStage extends StageTaskListener { MainStage(Context context, ShellTaskOrganizer taskOrganizer, int displayId, StageListenerCallbacks callbacks, SyncTransactionQueue syncQueue, - SurfaceSession surfaceSession, IconProvider iconProvider, - @Nullable StageTaskUnfoldController stageTaskUnfoldController) { - super(context, taskOrganizer, displayId, callbacks, syncQueue, surfaceSession, iconProvider, - stageTaskUnfoldController); + SurfaceSession surfaceSession, IconProvider iconProvider) { + super(context, taskOrganizer, displayId, callbacks, syncQueue, surfaceSession, + iconProvider); } boolean isActive() { @@ -76,10 +77,10 @@ class MainStage extends StageTaskListener { if (mRootTaskInfo == null) return; final WindowContainerToken rootToken = mRootTaskInfo.token; wct.reparentTasks( - rootToken, - null /* newParent */, - CONTROLLED_WINDOWING_MODES_WHEN_ACTIVE, - CONTROLLED_ACTIVITY_TYPES, - toTop); + 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 index d55619f5e5ed..8639b36faf4c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SideStage.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SideStage.java @@ -16,7 +16,6 @@ package com.android.wm.shell.splitscreen; -import android.annotation.Nullable; import android.app.ActivityManager; import android.content.Context; import android.view.SurfaceSession; @@ -38,10 +37,9 @@ class SideStage extends StageTaskListener { SideStage(Context context, ShellTaskOrganizer taskOrganizer, int displayId, StageListenerCallbacks callbacks, SyncTransactionQueue syncQueue, - SurfaceSession surfaceSession, IconProvider iconProvider, - @Nullable StageTaskUnfoldController stageTaskUnfoldController) { - super(context, taskOrganizer, displayId, callbacks, syncQueue, surfaceSession, iconProvider, - stageTaskUnfoldController); + SurfaceSession surfaceSession, IconProvider iconProvider) { + super(context, taskOrganizer, displayId, callbacks, syncQueue, surfaceSession, + iconProvider); } boolean removeAllTasks(WindowContainerTransaction wct, boolean toTop) { @@ -49,8 +47,8 @@ class SideStage extends StageTaskListener { wct.reparentTasks( mRootTaskInfo.token, null /* newParent */, - CONTROLLED_WINDOWING_MODES_WHEN_ACTIVE, - CONTROLLED_ACTIVITY_TYPES, + null /* windowingModes */, + null /* activityTypes */, toTop); 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 448773ae9ea2..e73b799b7a3d 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 @@ -18,6 +18,7 @@ package com.android.wm.shell.splitscreen; import android.annotation.IntDef; import android.annotation.NonNull; +import android.graphics.Rect; import com.android.wm.shell.common.annotations.ExternalThread; import com.android.wm.shell.common.split.SplitScreenConstants.SplitPosition; @@ -58,6 +59,7 @@ public interface SplitScreen { interface SplitScreenListener { default void onStagePositionChanged(@StageType int stage, @SplitPosition int position) {} default void onTaskStageChanged(int taskId, @StageType int stage, boolean visible) {} + default void onSplitBoundsChanged(Rect rootBounds, Rect mainBounds, Rect sideBounds) {} default void onSplitVisibilityChanged(boolean visible) {} } @@ -75,12 +77,6 @@ public interface SplitScreen { return null; } - /** - * Called when the visibility of the keyguard changes. - * @param showing Indicates if the keyguard is now visible. - */ - void onKeyguardVisibilityChanged(boolean showing); - /** Called when device waking up finished. */ void onFinishedWakingUp(); 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 31b510c38457..07a6895e2720 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 @@ -18,6 +18,7 @@ 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.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; @@ -31,6 +32,7 @@ import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_UNDEFINED; import static com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS; import android.app.ActivityManager; +import android.app.ActivityOptions; import android.app.ActivityTaskManager; import android.app.PendingIntent; import android.content.ActivityNotFoundException; @@ -38,6 +40,7 @@ import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.pm.LauncherApps; +import android.content.pm.ShortcutInfo; import android.graphics.Rect; import android.os.Bundle; import android.os.RemoteException; @@ -45,6 +48,7 @@ import android.os.UserHandle; import android.util.ArrayMap; import android.util.Slog; import android.view.IRemoteAnimationFinishedCallback; +import android.view.IRemoteAnimationRunner; import android.view.RemoteAnimationAdapter; import android.view.RemoteAnimationTarget; import android.view.SurfaceControl; @@ -58,7 +62,9 @@ import androidx.annotation.IntDef; import androidx.annotation.NonNull; 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.launcher3.icons.IconProvider; import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.ShellTaskOrganizer; @@ -73,21 +79,24 @@ import com.android.wm.shell.common.TransactionPool; import com.android.wm.shell.common.annotations.ExternalThread; import com.android.wm.shell.common.split.SplitLayout; import com.android.wm.shell.common.split.SplitScreenConstants.SplitPosition; +import com.android.wm.shell.draganddrop.DragAndDropController; import com.android.wm.shell.draganddrop.DragAndDropPolicy; +import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.recents.RecentTasksController; import com.android.wm.shell.splitscreen.SplitScreen.StageType; -import com.android.wm.shell.transition.LegacyTransitions; +import com.android.wm.shell.sysui.KeyguardChangeListener; +import com.android.wm.shell.sysui.ShellCommandHandler; +import com.android.wm.shell.sysui.ShellController; +import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.transition.Transitions; import java.io.PrintWriter; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; -import java.util.Arrays; +import java.util.Objects; import java.util.Optional; import java.util.concurrent.Executor; -import javax.inject.Provider; - /** * Class manages split-screen multitasking mode and implements the main interface * {@link SplitScreen}. @@ -96,19 +105,19 @@ import javax.inject.Provider; */ // TODO(b/198577848): Implement split screen flicker test to consolidate CUJ of split screen. public class SplitScreenController implements DragAndDropPolicy.Starter, - RemoteCallable<SplitScreenController> { + RemoteCallable<SplitScreenController>, KeyguardChangeListener { private static final String TAG = SplitScreenController.class.getSimpleName(); - static final int EXIT_REASON_UNKNOWN = 0; - static final int EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW = 1; - static final int EXIT_REASON_APP_FINISHED = 2; - static final int EXIT_REASON_DEVICE_FOLDED = 3; - static final int EXIT_REASON_DRAG_DIVIDER = 4; - static final int EXIT_REASON_RETURN_HOME = 5; - static final int EXIT_REASON_ROOT_TASK_VANISHED = 6; - static final int EXIT_REASON_SCREEN_LOCKED = 7; - static final int EXIT_REASON_SCREEN_LOCKED_SHOW_ON_TOP = 8; - static final int EXIT_REASON_CHILD_TASK_ENTER_PIP = 9; + public static final int EXIT_REASON_UNKNOWN = 0; + public static final int EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW = 1; + public static final int EXIT_REASON_APP_FINISHED = 2; + public static final int EXIT_REASON_DEVICE_FOLDED = 3; + public static final int EXIT_REASON_DRAG_DIVIDER = 4; + public static final int EXIT_REASON_RETURN_HOME = 5; + public static final int EXIT_REASON_ROOT_TASK_VANISHED = 6; + public static final int EXIT_REASON_SCREEN_LOCKED = 7; + public static final int EXIT_REASON_SCREEN_LOCKED_SHOW_ON_TOP = 8; + public static final int EXIT_REASON_CHILD_TASK_ENTER_PIP = 9; @IntDef(value = { EXIT_REASON_UNKNOWN, EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW, @@ -124,6 +133,22 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, @Retention(RetentionPolicy.SOURCE) @interface ExitReason{} + public static final int ENTER_REASON_UNKNOWN = 0; + public static final int ENTER_REASON_MULTI_INSTANCE = 1; + public static final int ENTER_REASON_DRAG = 2; + public static final int ENTER_REASON_LAUNCHER = 3; + /** Acts as a mapping to the actual EnterReasons as defined in the logging proto */ + @IntDef(value = { + ENTER_REASON_MULTI_INSTANCE, + ENTER_REASON_DRAG, + ENTER_REASON_LAUNCHER, + ENTER_REASON_UNKNOWN + }) + public @interface SplitEnterReason { + } + + private final ShellCommandHandler mShellCommandHandler; + private final ShellController mShellController; private final ShellTaskOrganizer mTaskOrganizer; private final SyncTransactionQueue mSyncQueue; private final Context mContext; @@ -133,27 +158,37 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, private final DisplayController mDisplayController; private final DisplayImeController mDisplayImeController; private final DisplayInsetsController mDisplayInsetsController; + private final DragAndDropController mDragAndDropController; private final Transitions mTransitions; private final TransactionPool mTransactionPool; - private final SplitscreenEventLogger mLogger; private final IconProvider mIconProvider; private final Optional<RecentTasksController> mRecentTasksOptional; - private final Provider<Optional<StageTaskUnfoldController>> mUnfoldControllerProvider; + private final SplitScreenShellCommandHandler mSplitScreenShellCommandHandler; private 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 mSplitTasksContainerLayer; - - public SplitScreenController(ShellTaskOrganizer shellTaskOrganizer, - SyncTransactionQueue syncQueue, Context context, + private SurfaceControl mGoingToRecentsTasksLayer; + private SurfaceControl mStartingSplitTasksLayer; + + public SplitScreenController(Context context, + ShellInit shellInit, + ShellCommandHandler shellCommandHandler, + ShellController shellController, + ShellTaskOrganizer shellTaskOrganizer, + SyncTransactionQueue syncQueue, RootTaskDisplayAreaOrganizer rootTDAOrganizer, - ShellExecutor mainExecutor, DisplayController displayController, + DisplayController displayController, DisplayImeController displayImeController, DisplayInsetsController displayInsetsController, - Transitions transitions, TransactionPool transactionPool, IconProvider iconProvider, + DragAndDropController dragAndDropController, + Transitions transitions, + TransactionPool transactionPool, + IconProvider iconProvider, Optional<RecentTasksController> recentTasks, - Provider<Optional<StageTaskUnfoldController>> unfoldControllerProvider) { + ShellExecutor mainExecutor) { + mShellCommandHandler = shellCommandHandler; + mShellController = shellController; mTaskOrganizer = shellTaskOrganizer; mSyncQueue = syncQueue; mContext = context; @@ -162,18 +197,47 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, mDisplayController = displayController; mDisplayImeController = displayImeController; mDisplayInsetsController = displayInsetsController; + mDragAndDropController = dragAndDropController; mTransitions = transitions; mTransactionPool = transactionPool; - mUnfoldControllerProvider = unfoldControllerProvider; - mLogger = new SplitscreenEventLogger(); mIconProvider = iconProvider; mRecentTasksOptional = recentTasks; + mSplitScreenShellCommandHandler = new SplitScreenShellCommandHandler(this); + // TODO(b/238217847): Temporarily add this check here until we can remove the dynamic + // override for this controller from the base module + if (ActivityTaskManager.supportsSplitScreenMultiWindow(context)) { + shellInit.addInitCallback(this::onInit, this); + } } public SplitScreen asSplitScreen() { return mImpl; } + /** + * This will be called after ShellTaskOrganizer has initialized/registered because of the + * dependency order. + */ + @VisibleForTesting + void onInit() { + mShellCommandHandler.addDumpCallback(this::dump, this); + mShellCommandHandler.addCommandCallback("splitscreen", mSplitScreenShellCommandHandler, + this); + mShellController.addKeyguardChangeListener(this); + if (mStageCoordinator == null) { + // TODO: Multi-display + mStageCoordinator = createStageCoordinator(); + } + mDragAndDropController.setSplitScreenController(this); + } + + protected StageCoordinator createStageCoordinator() { + return new StageCoordinator(mContext, DEFAULT_DISPLAY, mSyncQueue, + mTaskOrganizer, mDisplayController, mDisplayImeController, + mDisplayInsetsController, mTransitions, mTransactionPool, + mIconProvider, mMainExecutor, mRecentTasksOptional); + } + @Override public Context getContext() { return mContext; @@ -184,20 +248,22 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, return mMainExecutor; } - public void onOrganizerRegistered() { - if (mStageCoordinator == null) { - // TODO: Multi-display - mStageCoordinator = new StageCoordinator(mContext, DEFAULT_DISPLAY, mSyncQueue, - mTaskOrganizer, mDisplayController, mDisplayImeController, - mDisplayInsetsController, mTransitions, mTransactionPool, mLogger, - mIconProvider, mMainExecutor, mRecentTasksOptional, mUnfoldControllerProvider); - } - } - public boolean isSplitScreenVisible() { return mStageCoordinator.isSplitScreenVisible(); } + public StageCoordinator getTransitionHandler() { + return mStageCoordinator; + } + + public ActivityManager.RunningTaskInfo getFocusingTaskInfo() { + return mStageCoordinator.getFocusingTaskInfo(); + } + + public boolean isValidToEnterSplitScreen(@NonNull ActivityManager.RunningTaskInfo taskInfo) { + return mStageCoordinator.isValidToEnterSplitScreen(taskInfo); + } + @Nullable public ActivityManager.RunningTaskInfo getTaskInfo(@SplitPosition int splitPosition) { if (!isSplitScreenVisible() || splitPosition == SPLIT_POSITION_UNDEFINED) { @@ -222,6 +288,14 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, new WindowContainerTransaction()); } + /** + * Update surfaces of the split screen layout based on the current state + * @param transaction to write the updates to + */ + public void updateSplitScreenSurfaces(SurfaceControl.Transaction transaction) { + mStageCoordinator.updateSurfaces(transaction); + } + private boolean moveToStage(int taskId, @StageType int stageType, @SplitPosition int stagePosition, WindowContainerTransaction wct) { final ActivityManager.RunningTaskInfo task = mTaskOrganizer.getRunningTaskInfo(taskId); @@ -263,8 +337,10 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, mStageCoordinator.exitSplitScreen(toTopTaskId, exitReason); } - public void onKeyguardVisibilityChanged(boolean showing) { - mStageCoordinator.onKeyguardVisibilityChanged(showing); + @Override + public void onKeyguardVisibilityChanged(boolean visible, boolean occluded, + boolean animatingDismiss) { + mStageCoordinator.onKeyguardVisibilityChanged(visible); } public void onFinishedWakingUp() { @@ -288,179 +364,250 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, } public void startTask(int taskId, @SplitPosition int position, @Nullable Bundle options) { + final int[] result = new int[1]; + IRemoteAnimationRunner wrapper = new IRemoteAnimationRunner.Stub() { + @Override + public void onAnimationStart(@WindowManager.TransitionOldType int transit, + RemoteAnimationTarget[] apps, + RemoteAnimationTarget[] wallpapers, + RemoteAnimationTarget[] nonApps, + final IRemoteAnimationFinishedCallback finishedCallback) { + try { + finishedCallback.onAnimationFinished(); + } catch (RemoteException e) { + Slog.e(TAG, "Failed to invoke onAnimationFinished", e); + } + if (result[0] == START_SUCCESS || result[0] == START_TASK_TO_FRONT) { + final WindowContainerTransaction evictWct = new WindowContainerTransaction(); + mStageCoordinator.prepareEvictNonOpeningChildTasks(position, apps, evictWct); + mSyncQueue.queue(evictWct); + } + } + @Override + public void onAnimationCancelled(boolean isKeyguardOccluded) { + final WindowContainerTransaction evictWct = new WindowContainerTransaction(); + mStageCoordinator.prepareEvictInvisibleChildTasks(evictWct); + mSyncQueue.queue(evictWct); + } + }; options = mStageCoordinator.resolveStartStage(STAGE_TYPE_UNDEFINED, position, options, null /* wct */); + RemoteAnimationAdapter wrappedAdapter = new RemoteAnimationAdapter(wrapper, + 0 /* duration */, 0 /* statusBarTransitionDelay */); + ActivityOptions activityOptions = ActivityOptions.fromBundle(options); + activityOptions.update(ActivityOptions.makeRemoteAnimation(wrappedAdapter)); try { - final WindowContainerTransaction evictWct = new WindowContainerTransaction(); - mStageCoordinator.prepareEvictChildTasks(position, evictWct); - final int result = - ActivityTaskManager.getService().startActivityFromRecents(taskId, options); - if (result == START_SUCCESS || result == START_TASK_TO_FRONT) { - mSyncQueue.queue(evictWct); - } + result[0] = ActivityTaskManager.getService().startActivityFromRecents(taskId, + activityOptions.toBundle()); } catch (RemoteException e) { Slog.e(TAG, "Failed to launch task", e); } } + /** + * See {@link #startShortcut(String, String, int, Bundle, UserHandle)} + * @param instanceId to be used by {@link SplitscreenEventLogger} + */ + public void startShortcut(String packageName, String shortcutId, @SplitPosition int position, + @Nullable Bundle options, UserHandle user, @NonNull InstanceId instanceId) { + mStageCoordinator.getLogger().enterRequested(instanceId, ENTER_REASON_LAUNCHER); + startShortcut(packageName, shortcutId, position, options, user); + } + + @Override public void startShortcut(String packageName, String shortcutId, @SplitPosition int position, @Nullable Bundle options, UserHandle user) { + IRemoteAnimationRunner wrapper = new IRemoteAnimationRunner.Stub() { + @Override + public void onAnimationStart(@WindowManager.TransitionOldType int transit, + RemoteAnimationTarget[] apps, + RemoteAnimationTarget[] wallpapers, + RemoteAnimationTarget[] nonApps, + final IRemoteAnimationFinishedCallback finishedCallback) { + try { + finishedCallback.onAnimationFinished(); + } catch (RemoteException e) { + Slog.e(TAG, "Failed to invoke onAnimationFinished", e); + } + final WindowContainerTransaction evictWct = new WindowContainerTransaction(); + mStageCoordinator.prepareEvictNonOpeningChildTasks(position, apps, evictWct); + mSyncQueue.queue(evictWct); + } + @Override + public void onAnimationCancelled(boolean isKeyguardOccluded) { + } + }; options = mStageCoordinator.resolveStartStage(STAGE_TYPE_UNDEFINED, position, options, null /* wct */); - final WindowContainerTransaction evictWct = new WindowContainerTransaction(); - mStageCoordinator.prepareEvictChildTasks(position, evictWct); - + RemoteAnimationAdapter wrappedAdapter = new RemoteAnimationAdapter(wrapper, + 0 /* duration */, 0 /* statusBarTransitionDelay */); + ActivityOptions activityOptions = ActivityOptions.fromBundle(options); + activityOptions.update(ActivityOptions.makeRemoteAnimation(wrappedAdapter)); try { - LauncherApps launcherApps = - mContext.getSystemService(LauncherApps.class); + LauncherApps launcherApps = mContext.getSystemService(LauncherApps.class); launcherApps.startShortcut(packageName, shortcutId, null /* sourceBounds */, - options, user); - mSyncQueue.queue(evictWct); + activityOptions.toBundle(), user); } catch (ActivityNotFoundException e) { Slog.e(TAG, "Failed to launch shortcut", e); } } + /** + * See {@link #startIntent(PendingIntent, Intent, int, Bundle)} + * @param instanceId to be used by {@link SplitscreenEventLogger} + */ + public void startIntent(PendingIntent intent, @Nullable Intent fillInIntent, + @SplitPosition int position, @Nullable Bundle options, @NonNull InstanceId instanceId) { + mStageCoordinator.getLogger().enterRequested(instanceId, ENTER_REASON_LAUNCHER); + startIntent(intent, fillInIntent, position, options); + } + + @Override public void startIntent(PendingIntent intent, @Nullable Intent fillInIntent, @SplitPosition int position, @Nullable Bundle options) { - if (!ENABLE_SHELL_TRANSITIONS) { - startIntentLegacy(intent, fillInIntent, position, options); - return; + if (fillInIntent == null) { + fillInIntent = new Intent(); } + // Flag this as a no-user-action launch to prevent sending user leaving event to the + // current top activity since it's going to be put into another side of the split. This + // prevents the current top activity from going into pip mode due to user leaving event. + fillInIntent.addFlags(FLAG_ACTIVITY_NO_USER_ACTION); - try { - options = mStageCoordinator.resolveStartStage(STAGE_TYPE_UNDEFINED, position, options, - null /* wct */); - - // Flag this as a no-user-action launch to prevent sending user leaving event to the - // current top activity since it's going to be put into another side of the split. This - // prevents the current top activity from going into pip mode due to user leaving event. - if (fillInIntent == null) { - fillInIntent = new Intent(); + // Flag with MULTIPLE_TASK if this is launching the same activity into both sides of the + // split and there is no reusable background task. + if (shouldAddMultipleTaskFlag(intent.getIntent(), position)) { + final ActivityManager.RecentTaskInfo taskInfo = mRecentTasksOptional.isPresent() + ? mRecentTasksOptional.get().findTaskInBackground( + intent.getIntent().getComponent()) + : null; + if (taskInfo != null) { + startTask(taskInfo.taskId, position, options); + return; } - fillInIntent.addFlags(FLAG_ACTIVITY_NO_USER_ACTION); + fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK); + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Adding MULTIPLE_TASK"); + } - intent.send(mContext, 0, fillInIntent, null /* onFinished */, null /* handler */, - null /* requiredPermission */, options); - } catch (PendingIntent.CanceledException e) { - Slog.e(TAG, "Failed to launch task", e); + if (!ENABLE_SHELL_TRANSITIONS) { + mStageCoordinator.startIntentLegacy(intent, fillInIntent, position, options); + return; } + mStageCoordinator.startIntent(intent, fillInIntent, position, options); } - private void startIntentLegacy(PendingIntent intent, @Nullable Intent fillInIntent, - @SplitPosition int position, @Nullable Bundle options) { - final WindowContainerTransaction evictWct = new WindowContainerTransaction(); - mStageCoordinator.prepareEvictChildTasks(position, evictWct); - - LegacyTransitions.ILegacyTransition transition = new LegacyTransitions.ILegacyTransition() { - @Override - public void onAnimationStart(int transit, RemoteAnimationTarget[] apps, - RemoteAnimationTarget[] wallpapers, RemoteAnimationTarget[] nonApps, - IRemoteAnimationFinishedCallback finishedCallback, - SurfaceControl.Transaction t) { - if (apps == null || apps.length == 0) { - final ActivityManager.RunningTaskInfo pairedTaskInfo = - getTaskInfo(SplitLayout.reversePosition(position)); - final ComponentName pairedActivity = - pairedTaskInfo != null ? pairedTaskInfo.baseActivity : null; - final ComponentName intentActivity = - intent.getIntent() != null ? intent.getIntent().getComponent() : null; - if (pairedActivity != null && pairedActivity.equals(intentActivity)) { - // Switch split position if dragging the same activity to another side. - setSideStagePosition(SplitLayout.reversePosition( - mStageCoordinator.getSideStagePosition())); - } - - // Do nothing when the animation was cancelled. - t.apply(); - return; - } - - mStageCoordinator.updateSurfaceBounds(null /* layout */, t, - false /* applyResizingOffset */); - for (int i = 0; i < apps.length; ++i) { - if (apps[i].mode == MODE_OPENING) { - t.show(apps[i].leash); - } - } - t.apply(); + /** Returns {@code true} if it's launching the same component on both sides of the split. */ + @VisibleForTesting + boolean shouldAddMultipleTaskFlag(@Nullable Intent startIntent, @SplitPosition int position) { + if (startIntent == null) { + return false; + } - if (finishedCallback != null) { - try { - finishedCallback.onAnimationFinished(); - } catch (RemoteException e) { - Slog.e(TAG, "Error finishing legacy transition: ", e); - } - } + final ComponentName launchingActivity = startIntent.getComponent(); + if (launchingActivity == null) { + return false; + } - mSyncQueue.queue(evictWct); + if (isSplitScreenVisible()) { + // To prevent users from constantly dropping the same app to the same side resulting in + // a large number of instances in the background. + final ActivityManager.RunningTaskInfo targetTaskInfo = getTaskInfo(position); + final ComponentName targetActivity = targetTaskInfo != null + ? targetTaskInfo.baseIntent.getComponent() : null; + if (Objects.equals(launchingActivity, targetActivity)) { + return false; } - }; - final WindowContainerTransaction wct = new WindowContainerTransaction(); - options = mStageCoordinator.resolveStartStage(STAGE_TYPE_UNDEFINED, position, options, wct); + // Allow users to start a new instance the same to adjacent side. + final ActivityManager.RunningTaskInfo pairedTaskInfo = + getTaskInfo(SplitLayout.reversePosition(position)); + final ComponentName pairedActivity = pairedTaskInfo != null + ? pairedTaskInfo.baseIntent.getComponent() : null; + return Objects.equals(launchingActivity, pairedActivity); + } - // Flag this as a no-user-action launch to prevent sending user leaving event to the current - // top activity since it's going to be put into another side of the split. This prevents the - // current top activity from going into pip mode due to user leaving event. - if (fillInIntent == null) { - fillInIntent = new Intent(); + final ActivityManager.RunningTaskInfo taskInfo = getFocusingTaskInfo(); + if (taskInfo != null && isValidToEnterSplitScreen(taskInfo)) { + return Objects.equals(taskInfo.baseIntent.getComponent(), launchingActivity); } - fillInIntent.addFlags(FLAG_ACTIVITY_NO_USER_ACTION); - wct.sendPendingIntent(intent, fillInIntent, options); - mSyncQueue.queue(transition, WindowManager.TRANSIT_OPEN, wct); + return false; } 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; } - return reparentSplitTasksForAnimation(apps, true /*splitExpectedToBeVisible*/); - } - RemoteAnimationTarget[] onStartingSplitLegacy(RemoteAnimationTarget[] apps) { - return reparentSplitTasksForAnimation(apps, false /*splitExpectedToBeVisible*/); + 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()}; } - private RemoteAnimationTarget[] reparentSplitTasksForAnimation(RemoteAnimationTarget[] apps, - boolean splitExpectedToBeVisible) { + RemoteAnimationTarget[] onStartingSplitLegacy(RemoteAnimationTarget[] apps) { if (ENABLE_SHELL_TRANSITIONS) return null; - // TODO(b/206487881): Integrate this with shell transition. - if (splitExpectedToBeVisible && !isSplitScreenVisible()) return null; - // Split not visible, but not enough apps to have split, also return null - if (!splitExpectedToBeVisible && apps.length < 2) return null; - SurfaceControl.Transaction transaction = new SurfaceControl.Transaction(); - if (mSplitTasksContainerLayer != null) { - // Remove the previous layer before recreating - transaction.remove(mSplitTasksContainerLayer); + 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()) .setContainerLayer() .setName("RecentsAnimationSplitTasks") .setHidden(false) - .setCallsite("SplitScreenController#onGoingtoRecentsLegacy"); + .setCallsite(callsite); mRootTDAOrganizer.attachToDisplayArea(DEFAULT_DISPLAY, builder); - mSplitTasksContainerLayer = builder.build(); - - // Ensure that we order these in the parent in the right z-order as their previous order - Arrays.sort(apps, (a1, a2) -> a1.prefixOrderIndex - a2.prefixOrderIndex); - int layer = 1; - for (RemoteAnimationTarget appTarget : apps) { - transaction.reparent(appTarget.leash, mSplitTasksContainerLayer); - transaction.setPosition(appTarget.leash, appTarget.screenSpaceBounds.left, + final SurfaceControl splitTasksLayer = builder.build(); + + for (int i = 0; i < apps.length; ++i) { + final RemoteAnimationTarget appTarget = apps[i]; + t.reparent(appTarget.leash, splitTasksLayer); + t.setPosition(appTarget.leash, appTarget.screenSpaceBounds.left, appTarget.screenSpaceBounds.top); - transaction.setLayer(appTarget.leash, layer++); } - transaction.apply(); - transaction.close(); - return new RemoteAnimationTarget[]{mStageCoordinator.getDividerBarLegacyTarget()}; + return splitTasksLayer; } /** * Sets drag info to be logged when splitscreen is entered. @@ -535,6 +682,17 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, } @Override + public void onSplitBoundsChanged(Rect rootBounds, Rect mainBounds, Rect sideBounds) { + for (int i = 0; i < mExecutors.size(); i++) { + final int index = i; + mExecutors.valueAt(index).execute(() -> { + mExecutors.keyAt(index).onSplitBoundsChanged(rootBounds, mainBounds, + sideBounds); + }); + } + } + + @Override public void onSplitVisibilityChanged(boolean visible) { for (int i = 0; i < mExecutors.size(); i++) { final int index = i; @@ -583,13 +741,6 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, } @Override - public void onKeyguardVisibilityChanged(boolean showing) { - mMainExecutor.execute(() -> { - SplitScreenController.this.onKeyguardVisibilityChanged(showing); - }); - } - - @Override public void onFinishedWakingUp() { mMainExecutor.execute(() -> { SplitScreenController.this.onFinishedWakingUp(); @@ -679,49 +830,64 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, @Override public void startTasksWithLegacyTransition(int mainTaskId, @Nullable Bundle mainOptions, int sideTaskId, @Nullable Bundle sideOptions, @SplitPosition int sidePosition, - float splitRatio, RemoteAnimationAdapter adapter) { + float splitRatio, RemoteAnimationAdapter adapter, InstanceId instanceId) { executeRemoteCallWithTaskPermission(mController, "startTasks", (controller) -> controller.mStageCoordinator.startTasksWithLegacyTransition( mainTaskId, mainOptions, sideTaskId, sideOptions, sidePosition, - splitRatio, adapter)); + splitRatio, adapter, instanceId)); } @Override public void startIntentAndTaskWithLegacyTransition(PendingIntent pendingIntent, Intent fillInIntent, int taskId, Bundle mainOptions, Bundle sideOptions, - int sidePosition, float splitRatio, RemoteAnimationAdapter adapter) { + int sidePosition, float splitRatio, RemoteAnimationAdapter adapter, + InstanceId instanceId) { executeRemoteCallWithTaskPermission(mController, "startIntentAndTaskWithLegacyTransition", (controller) -> controller.mStageCoordinator.startIntentAndTaskWithLegacyTransition( pendingIntent, fillInIntent, taskId, mainOptions, sideOptions, - sidePosition, splitRatio, adapter)); + sidePosition, splitRatio, adapter, instanceId)); + } + + @Override + public void startShortcutAndTaskWithLegacyTransition(ShortcutInfo shortcutInfo, + int taskId, @Nullable Bundle mainOptions, @Nullable Bundle sideOptions, + @SplitPosition int sidePosition, float splitRatio, RemoteAnimationAdapter adapter, + InstanceId instanceId) { + executeRemoteCallWithTaskPermission(mController, + "startShortcutAndTaskWithLegacyTransition", (controller) -> + controller.mStageCoordinator.startShortcutAndTaskWithLegacyTransition( + shortcutInfo, taskId, mainOptions, sideOptions, sidePosition, + splitRatio, adapter, instanceId)); } @Override public void startTasks(int mainTaskId, @Nullable Bundle mainOptions, int sideTaskId, @Nullable Bundle sideOptions, @SplitPosition int sidePosition, float splitRatio, - @Nullable RemoteTransition remoteTransition) { + @Nullable RemoteTransition remoteTransition, InstanceId instanceId) { executeRemoteCallWithTaskPermission(mController, "startTasks", (controller) -> controller.mStageCoordinator.startTasks(mainTaskId, mainOptions, - sideTaskId, sideOptions, sidePosition, splitRatio, remoteTransition)); + sideTaskId, sideOptions, sidePosition, splitRatio, remoteTransition, + instanceId)); } @Override public void startShortcut(String packageName, String shortcutId, int position, - @Nullable Bundle options, UserHandle user) { + @Nullable Bundle options, UserHandle user, InstanceId instanceId) { executeRemoteCallWithTaskPermission(mController, "startShortcut", (controller) -> { - controller.startShortcut(packageName, shortcutId, position, options, user); + controller.startShortcut(packageName, shortcutId, position, options, user, + instanceId); }); } @Override public void startIntent(PendingIntent intent, Intent fillInIntent, int position, - @Nullable Bundle options) { + @Nullable Bundle options, InstanceId instanceId) { executeRemoteCallWithTaskPermission(mController, "startIntent", (controller) -> { - controller.startIntent(intent, fillInIntent, position, options); + controller.startIntent(intent, fillInIntent, position, options, instanceId); }); } 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 new file mode 100644 index 000000000000..7fd03a9a306b --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenShellCommandHandler.java @@ -0,0 +1,96 @@ +/* + * 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.splitscreen; + +import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; + +import com.android.wm.shell.sysui.ShellCommandHandler; + +import java.io.PrintWriter; + +/** + * Handles the shell commands for the SplitscreenController. + */ +public class SplitScreenShellCommandHandler implements + ShellCommandHandler.ShellCommandActionHandler { + + private final SplitScreenController mController; + + public SplitScreenShellCommandHandler(SplitScreenController controller) { + mController = controller; + } + + @Override + public boolean onShellCommand(String[] args, PrintWriter pw) { + switch (args[0]) { + case "moveToSideStage": + return runMoveToSideStage(args, pw); + case "removeFromSideStage": + return runRemoveFromSideStage(args, pw); + case "setSideStagePosition": + return runSetSideStagePosition(args, pw); + default: + pw.println("Invalid command: " + args[0]); + return false; + } + } + + private boolean runMoveToSideStage(String[] args, PrintWriter pw) { + if (args.length < 3) { + // First argument is the action name. + pw.println("Error: task id should be provided as arguments"); + return false; + } + final int taskId = new Integer(args[1]); + final int sideStagePosition = args.length > 2 + ? new Integer(args[2]) : SPLIT_POSITION_BOTTOM_OR_RIGHT; + mController.moveToSideStage(taskId, sideStagePosition); + return true; + } + + private boolean runRemoveFromSideStage(String[] args, PrintWriter pw) { + if (args.length < 2) { + // First argument is the action name. + pw.println("Error: task id should be provided as arguments"); + return false; + } + final int taskId = new Integer(args[1]); + mController.removeFromSideStage(taskId); + return true; + } + + private boolean runSetSideStagePosition(String[] args, PrintWriter pw) { + if (args.length < 2) { + // First argument is the action name. + pw.println("Error: side stage position should be provided as arguments"); + return false; + } + final int position = new Integer(args[1]); + mController.setSideStagePosition(position); + return true; + } + + @Override + public void printShellCommandHelp(PrintWriter pw, String prefix) { + pw.println(prefix + "moveToSideStage <taskId> <SideStagePosition>"); + pw.println(prefix + " Move a task with given id in split-screen mode."); + pw.println(prefix + "removeFromSideStage <taskId>"); + pw.println(prefix + " Remove a task with given id in split-screen mode."); + pw.println(prefix + "setSideStagePosition <SideStagePosition>"); + pw.println(prefix + " Sets the position of the side-stage."); + } +} 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 cd121ed41fdd..d7ca791e3863 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,7 +21,6 @@ import static android.view.WindowManager.TRANSIT_CLOSE; import static android.view.WindowManager.TRANSIT_OPEN; import static android.view.WindowManager.TRANSIT_TO_BACK; import static android.view.WindowManager.TRANSIT_TO_FRONT; -import static android.window.TransitionInfo.FLAG_FIRST_CUSTOM; import static com.android.wm.shell.splitscreen.SplitScreen.stageTypeToString; import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_DRAG_DIVIDER; @@ -58,19 +57,16 @@ import java.util.ArrayList; class SplitScreenTransitions { private static final String TAG = "SplitScreenTransitions"; - /** Flag applied to a transition change to identify it as a divider bar for animation. */ - public static final int FLAG_IS_DIVIDER_BAR = FLAG_FIRST_CUSTOM; - private final TransactionPool mTransactionPool; private final Transitions mTransitions; private final Runnable mOnFinish; DismissTransition mPendingDismiss = null; - IBinder mPendingEnter = null; - IBinder mPendingRecent = null; + TransitSession mPendingEnter = null; + TransitSession mPendingRecent = null; private IBinder mAnimatingTransition = null; - private OneShotRemoteHandler mPendingRemoteHandler = null; + OneShotRemoteHandler mPendingRemoteHandler = null; private OneShotRemoteHandler mActiveRemoteHandler = null; private final Transitions.TransitionFinishCallback mRemoteFinishCB = this::onFinish; @@ -94,9 +90,11 @@ class SplitScreenTransitions { @NonNull SurfaceControl.Transaction startTransaction, @NonNull SurfaceControl.Transaction finishTransaction, @NonNull Transitions.TransitionFinishCallback finishCallback, - @NonNull WindowContainerToken mainRoot, @NonNull WindowContainerToken sideRoot) { + @NonNull WindowContainerToken mainRoot, @NonNull WindowContainerToken sideRoot, + @NonNull WindowContainerToken topRoot) { mFinishCallback = finishCallback; mAnimatingTransition = transition; + mFinishTransaction = finishTransaction; if (mPendingRemoteHandler != null) { mPendingRemoteHandler.startAnimation(transition, info, startTransaction, finishTransaction, mRemoteFinishCB); @@ -104,14 +102,12 @@ class SplitScreenTransitions { mPendingRemoteHandler = null; return; } - playInternalAnimation(transition, info, startTransaction, mainRoot, sideRoot); + playInternalAnimation(transition, info, startTransaction, mainRoot, sideRoot, topRoot); } private void playInternalAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction t, @NonNull WindowContainerToken mainRoot, - @NonNull WindowContainerToken sideRoot) { - mFinishTransaction = mTransactionPool.acquire(); - + @NonNull WindowContainerToken sideRoot, @NonNull WindowContainerToken topRoot) { // Play some place-holder fade animations for (int i = info.getChanges().size() - 1; i >= 0; --i) { final TransitionInfo.Change change = info.getChanges().get(i); @@ -140,11 +136,14 @@ class SplitScreenTransitions { endBounds.offset(-info.getRootOffset().x, -info.getRootOffset().y); startExampleResizeAnimation(leash, startBounds, endBounds); } - if (change.getParent() != null) { + boolean isRootOrSplitSideRoot = change.getParent() == null + || topRoot.equals(change.getParent()); + // For enter or exit, we only want to animate the side roots but not the top-root. + if (!isRootOrSplitSideRoot || topRoot.equals(change.getContainer())) { continue; } - if (transition == mPendingEnter && (mainRoot.equals(change.getContainer()) + if (isPendingEnter(transition) && (mainRoot.equals(change.getContainer()) || sideRoot.equals(change.getContainer()))) { t.setPosition(leash, change.getEndAbsBounds().left, change.getEndAbsBounds().top); t.setWindowCrop(leash, change.getEndAbsBounds().width(), @@ -170,12 +169,40 @@ class SplitScreenTransitions { onFinish(null /* wct */, null /* wctCB */); } + boolean isPendingTransition(IBinder transition) { + return isPendingEnter(transition) + || isPendingDismiss(transition) + || isPendingRecent(transition); + } + + boolean isPendingEnter(IBinder transition) { + return mPendingEnter != null && mPendingEnter.mTransition == transition; + } + + boolean isPendingRecent(IBinder transition) { + return mPendingRecent != null && mPendingRecent.mTransition == transition; + } + + boolean isPendingDismiss(IBinder transition) { + return mPendingDismiss != null && mPendingDismiss.mTransition == transition; + } + /** Starts a transition to enter split with a remote transition animator. */ - IBinder startEnterTransition(@WindowManager.TransitionType int transitType, - @NonNull WindowContainerTransaction wct, @Nullable RemoteTransition remoteTransition, - @NonNull Transitions.TransitionHandler handler) { + IBinder startEnterTransition( + @WindowManager.TransitionType int transitType, + WindowContainerTransaction wct, + @Nullable RemoteTransition remoteTransition, + Transitions.TransitionHandler handler, + @Nullable TransitionCallback callback) { final IBinder transition = mTransitions.startTransition(transitType, wct, handler); - mPendingEnter = transition; + setEnterTransition(transition, remoteTransition, callback); + return transition; + } + + /** Sets a transition to enter split. */ + void setEnterTransition(@NonNull IBinder transition, + @Nullable RemoteTransition remoteTransition, @Nullable TransitionCallback callback) { + mPendingEnter = new TransitSession(transition, callback); if (remoteTransition != null) { // Wrapping it for ease-of-use (OneShot handles all the binder linking/death stuff) @@ -183,32 +210,35 @@ class SplitScreenTransitions { mTransitions.getMainExecutor(), remoteTransition); mPendingRemoteHandler.setTransition(transition); } - return transition; + + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " splitTransition " + + " deduced Enter split screen"); } /** Starts a transition to dismiss split. */ - IBinder startDismissTransition(@Nullable IBinder transition, WindowContainerTransaction wct, + IBinder startDismissTransition(WindowContainerTransaction wct, Transitions.TransitionHandler handler, @SplitScreen.StageType int dismissTop, @SplitScreenController.ExitReason int reason) { final int type = reason == EXIT_REASON_DRAG_DIVIDER ? TRANSIT_SPLIT_DISMISS_SNAP : TRANSIT_SPLIT_DISMISS; - if (transition == null) { - transition = mTransitions.startTransition(type, wct, handler); - } + IBinder transition = mTransitions.startTransition(type, wct, handler); + setDismissTransition(transition, dismissTop, reason); + return transition; + } + + /** Sets a transition to dismiss split. */ + void setDismissTransition(@NonNull IBinder transition, @SplitScreen.StageType int dismissTop, + @SplitScreenController.ExitReason int reason) { mPendingDismiss = new DismissTransition(transition, reason, dismissTop); ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " splitTransition " + " deduced Dismiss due to %s. toTop=%s", exitReasonToString(reason), stageTypeToString(dismissTop)); - return transition; } - IBinder startRecentTransition(@Nullable IBinder transition, WindowContainerTransaction wct, - Transitions.TransitionHandler handler, @Nullable RemoteTransition remoteTransition) { - if (transition == null) { - transition = mTransitions.startTransition(TRANSIT_OPEN, wct, handler); - } - mPendingRecent = transition; + void setRecentTransition(@NonNull IBinder transition, + @Nullable RemoteTransition remoteTransition, @Nullable TransitionCallback callback) { + mPendingRecent = new TransitSession(transition, callback); if (remoteTransition != null) { // Wrapping it for ease-of-use (OneShot handles all the binder linking/death stuff) @@ -219,44 +249,93 @@ class SplitScreenTransitions { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " splitTransition " + " deduced Enter recent panel"); - return transition; } void mergeAnimation(IBinder transition, TransitionInfo info, SurfaceControl.Transaction t, IBinder mergeTarget, Transitions.TransitionFinishCallback finishCallback) { - if (mergeTarget == mAnimatingTransition && mActiveRemoteHandler != null) { + if (mergeTarget != mAnimatingTransition) return; + + if (isPendingEnter(transition) && isPendingRecent(mergeTarget)) { + mPendingRecent.mCallback = new TransitionCallback() { + @Override + public void onTransitionFinished(WindowContainerTransaction finishWct, + SurfaceControl.Transaction finishT) { + // Since there's an entering transition merged, recent transition no longer + // need to handle entering split screen after the transition finished. + } + }; + } + + if (mActiveRemoteHandler != null) { mActiveRemoteHandler.mergeAnimation(transition, info, t, mergeTarget, finishCallback); + } else { + for (int i = mAnimations.size() - 1; i >= 0; --i) { + final Animator anim = mAnimations.get(i); + mTransitions.getAnimExecutor().execute(anim::end); + } + } + } + + boolean end() { + // If its remote, there's nothing we can do right now. + if (mActiveRemoteHandler != null) return false; + for (int i = mAnimations.size() - 1; i >= 0; --i) { + final Animator anim = mAnimations.get(i); + mTransitions.getAnimExecutor().execute(anim::end); + } + return true; + } + + void onTransitionConsumed(@NonNull IBinder transition, boolean aborted, + @Nullable SurfaceControl.Transaction finishT) { + if (isPendingEnter(transition)) { + if (!aborted) { + // An enter transition got merged, appends the rest operations to finish entering + // split screen. + mStageCoordinator.finishEnterSplitScreen(finishT); + mPendingRemoteHandler = null; + } + + mPendingEnter.mCallback.onTransitionConsumed(aborted); + mPendingEnter = null; + mPendingRemoteHandler = null; + } else if (isPendingDismiss(transition)) { + mPendingDismiss.mCallback.onTransitionConsumed(aborted); + mPendingDismiss = null; + } else if (isPendingRecent(transition)) { + mPendingRecent.mCallback.onTransitionConsumed(aborted); + mPendingRecent = null; + mPendingRemoteHandler = null; } } void onFinish(WindowContainerTransaction wct, WindowContainerTransactionCallback wctCB) { if (!mAnimations.isEmpty()) return; - if (mAnimatingTransition == mPendingEnter) { + + TransitionCallback callback = null; + if (isPendingEnter(mAnimatingTransition)) { + callback = mPendingEnter.mCallback; mPendingEnter = null; } - if (mPendingDismiss != null && mPendingDismiss.mTransition == mAnimatingTransition) { + if (isPendingDismiss(mAnimatingTransition)) { + callback = mPendingDismiss.mCallback; mPendingDismiss = null; } - if (mAnimatingTransition == mPendingRecent) { - // If the clean-up wct is null when finishing recent transition, it indicates it's - // returning to home and thus no need to reorder tasks. - final boolean returnToHome = wct == null; - if (returnToHome) { - wct = new WindowContainerTransaction(); - } - mStageCoordinator.onRecentTransitionFinished(returnToHome, wct, mFinishTransaction); + if (isPendingRecent(mAnimatingTransition)) { + callback = mPendingRecent.mCallback; mPendingRecent = null; } + + if (callback != null) { + if (wct == null) wct = new WindowContainerTransaction(); + callback.onTransitionFinished(wct, mFinishTransaction); + } + mPendingRemoteHandler = null; mActiveRemoteHandler = null; mAnimatingTransition = null; mOnFinish.run(); - if (mFinishTransaction != null) { - mFinishTransaction.apply(); - mTransactionPool.release(mFinishTransaction); - mFinishTransaction = null; - } if (mFinishCallback != null) { mFinishCallback.onTransitionFinished(wct /* wct */, wctCB /* wctCB */); mFinishCallback = null; @@ -353,17 +432,34 @@ class SplitScreenTransitions { || info.getType() == TRANSIT_SPLIT_SCREEN_PAIR_OPEN; } - /** Bundled information of dismiss transition. */ - static class DismissTransition { - IBinder mTransition; + /** Clean-up callbacks for transition. */ + interface TransitionCallback { + /** Calls when the transition got consumed. */ + default void onTransitionConsumed(boolean aborted) {} - int mReason; + /** Calls when the transition finished. */ + default void onTransitionFinished(WindowContainerTransaction finishWct, + SurfaceControl.Transaction finishT) {} + } + + /** Session for a transition and its clean-up callback. */ + static class TransitSession { + final IBinder mTransition; + TransitionCallback mCallback; + + TransitSession(IBinder transition, @Nullable TransitionCallback callback) { + mTransition = transition; + mCallback = callback != null ? callback : new TransitionCallback() {}; + } + } - @SplitScreen.StageType - int mDismissTop; + /** Bundled information of dismiss transition. */ + static class DismissTransition extends TransitSession { + final int mReason; + final @SplitScreen.StageType int mDismissTop; DismissTransition(IBinder transition, int reason, int dismissTop) { - this.mTransition = transition; + super(transition, null /* callback */); this.mReason = reason; this.mDismissTop = dismissTop; } 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 3e7a1004ed7a..2dc4a0441b06 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 @@ -16,7 +16,9 @@ package com.android.wm.shell.splitscreen; -import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__ENTER_REASON__OVERVIEW; +import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__ENTER_REASON__LAUNCHER; +import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__ENTER_REASON__MULTI_INSTANCE; +import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__ENTER_REASON__UNKNOWN_ENTER; import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__APP_DOES_NOT_SUPPORT_MULTIWINDOW; import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__APP_FINISHED; import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__DEVICE_FOLDED; @@ -28,6 +30,10 @@ import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED_ 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.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; +import static com.android.wm.shell.splitscreen.SplitScreenController.ENTER_REASON_UNKNOWN; import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW; import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_APP_FINISHED; import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_DEVICE_FOLDED; @@ -38,6 +44,7 @@ import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON 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 android.annotation.Nullable; import android.util.Slog; import com.android.internal.logging.InstanceId; @@ -59,7 +66,7 @@ public class SplitscreenEventLogger { // Drag info private @SplitPosition int mDragEnterPosition; - private InstanceId mDragEnterSessionId; + private @Nullable InstanceId mEnterSessionId; // For deduping async events private int mLastMainStagePosition = -1; @@ -67,6 +74,7 @@ public class SplitscreenEventLogger { private int mLastSideStagePosition = -1; private int mLastSideStageUid = -1; private float mLastSplitRatio = -1f; + private @SplitScreenController.SplitEnterReason int mEnterReason = ENTER_REASON_UNKNOWN; public SplitscreenEventLogger() { mIdSequence = new InstanceIdSequence(Integer.MAX_VALUE); @@ -79,12 +87,35 @@ public class SplitscreenEventLogger { return mLoggerSessionId != null; } + public boolean isEnterRequestedByDrag() { + return mEnterReason == ENTER_REASON_DRAG; + } + /** * May be called before logEnter() to indicate that the session was started from a drag. */ - public void enterRequestedByDrag(@SplitPosition int position, InstanceId dragSessionId) { + public void enterRequestedByDrag(@SplitPosition int position, InstanceId enterSessionId) { mDragEnterPosition = position; - mDragEnterSessionId = dragSessionId; + enterRequested(enterSessionId, ENTER_REASON_DRAG); + } + + /** + * May be called before logEnter() to indicate that the session was started from launcher. + * This specifically is for all the scenarios where split started without a drag interaction + */ + public void enterRequested(@Nullable InstanceId enterSessionId, + @SplitScreenController.SplitEnterReason int enterReason) { + mEnterSessionId = enterSessionId; + mEnterReason = enterReason; + } + + /** + * @return if an enterSessionId has been set via either + * {@link #enterRequested(InstanceId, int)} or + * {@link #enterRequestedByDrag(int, InstanceId)} + */ + public boolean hasValidEnterSessionId() { + return mEnterSessionId != null; } /** @@ -95,9 +126,7 @@ public class SplitscreenEventLogger { @SplitPosition int sideStagePosition, int sideStageUid, boolean isLandscape) { mLoggerSessionId = mIdSequence.newInstanceId(); - int enterReason = mDragEnterPosition != SPLIT_POSITION_UNDEFINED - ? getDragEnterReasonFromSplitPosition(mDragEnterPosition, isLandscape) - : SPLITSCREEN_UICHANGED__ENTER_REASON__OVERVIEW; + int enterReason = getLoggerEnterReason(isLandscape); updateMainStageState(getMainStagePositionFromSplitPosition(mainStagePosition, isLandscape), mainStageUid); updateSideStageState(getSideStagePositionFromSplitPosition(sideStagePosition, isLandscape), @@ -112,10 +141,24 @@ public class SplitscreenEventLogger { mLastMainStageUid, mLastSideStagePosition, mLastSideStageUid, - mDragEnterSessionId != null ? mDragEnterSessionId.getId() : 0, + mEnterSessionId != null ? mEnterSessionId.getId() : 0, mLoggerSessionId.getId()); } + private int getLoggerEnterReason(boolean isLandscape) { + switch (mEnterReason) { + case ENTER_REASON_MULTI_INSTANCE: + return SPLITSCREEN_UICHANGED__ENTER_REASON__MULTI_INSTANCE; + case ENTER_REASON_LAUNCHER: + return SPLITSCREEN_UICHANGED__ENTER_REASON__LAUNCHER; + case ENTER_REASON_DRAG: + return getDragEnterReasonFromSplitPosition(mDragEnterPosition, isLandscape); + case ENTER_REASON_UNKNOWN: + default: + return SPLITSCREEN_UICHANGED__ENTER_REASON__UNKNOWN_ENTER; + } + } + /** * Returns the framework logging constant given a splitscreen exit reason. */ @@ -176,11 +219,12 @@ public class SplitscreenEventLogger { // Reset states mLoggerSessionId = null; mDragEnterPosition = SPLIT_POSITION_UNDEFINED; - mDragEnterSessionId = null; + mEnterSessionId = null; mLastMainStagePosition = -1; mLastMainStageUid = -1; mLastSideStagePosition = -1; mLastSideStageUid = -1; + mEnterReason = ENTER_REASON_UNKNOWN; } /** 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 30f316efb2b3..c17f8226c925 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java @@ -18,25 +18,36 @@ package com.android.wm.shell.splitscreen; import static android.app.ActivityOptions.KEY_LAUNCH_ROOT_TASK_TOKEN; import static android.app.ActivityTaskManager.INVALID_TASK_ID; +import static android.app.ComponentOptions.KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED; +import static android.app.ComponentOptions.KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED_BY_PERMISSION; import static android.app.WindowConfiguration.ACTIVITY_TYPE_ASSISTANT; 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.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK; +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_TO_BACK; import static android.view.WindowManager.TRANSIT_TO_FRONT; import static android.view.WindowManager.transitTypeToString; 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.CONTROLLED_ACTIVITY_TYPES; +import static com.android.wm.shell.common.split.SplitScreenConstants.CONTROLLED_WINDOWING_MODES; +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.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.ENTER_REASON_LAUNCHER; +import static com.android.wm.shell.splitscreen.SplitScreenController.ENTER_REASON_MULTI_INSTANCE; import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW; import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_APP_FINISHED; import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_CHILD_TASK_ENTER_PIP; @@ -47,7 +58,6 @@ import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON 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.splitscreen.SplitScreenTransitions.FLAG_IS_DIVIDER_BAR; import static com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS; import static com.android.wm.shell.transition.Transitions.TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE; import static com.android.wm.shell.transition.Transitions.TRANSIT_SPLIT_SCREEN_PAIR_OPEN; @@ -62,11 +72,12 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.app.ActivityManager; import android.app.ActivityOptions; -import android.app.ActivityTaskManager; +import android.app.IActivityTaskManager; import android.app.PendingIntent; import android.app.WindowConfiguration; import android.content.Context; import android.content.Intent; +import android.content.pm.ShortcutInfo; import android.content.res.Configuration; import android.graphics.Rect; import android.hardware.devicestate.DeviceStateManager; @@ -74,6 +85,7 @@ import android.os.Bundle; import android.os.Debug; import android.os.IBinder; import android.os.RemoteException; +import android.os.ServiceManager; import android.util.Log; import android.util.Slog; import android.view.Choreographer; @@ -84,6 +96,8 @@ import android.view.RemoteAnimationTarget; import android.view.SurfaceControl; import android.view.SurfaceSession; import android.view.WindowManager; +import android.widget.Toast; +import android.window.DisplayAreaInfo; import android.window.RemoteTransition; import android.window.TransitionInfo; import android.window.TransitionRequestInfo; @@ -93,7 +107,9 @@ 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.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.DisplayImeController; @@ -109,16 +125,16 @@ import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.recents.RecentTasksController; import com.android.wm.shell.splitscreen.SplitScreen.StageType; import com.android.wm.shell.splitscreen.SplitScreenController.ExitReason; +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.StagedSplitBounds; +import com.android.wm.shell.util.SplitBounds; import java.io.PrintWriter; import java.util.ArrayList; import java.util.List; import java.util.Optional; -import javax.inject.Provider; - /** * Coordinates the staging (visibility, sizing, ...) of the split-screen {@link MainStage} and * {@link SideStage} stages. @@ -132,9 +148,9 @@ import javax.inject.Provider; * This rules are mostly implemented in {@link #onStageVisibilityChanged(StageListenerImpl)} and * {@link #onStageHasChildrenChanged(StageListenerImpl).} */ -class StageCoordinator implements SplitLayout.SplitLayoutHandler, +public class StageCoordinator implements SplitLayout.SplitLayoutHandler, DisplayController.OnDisplaysChangedListener, Transitions.TransitionHandler, - ShellTaskOrganizer.TaskListener { + ShellTaskOrganizer.TaskListener, ShellTaskOrganizer.FocusListener { private static final String TAG = StageCoordinator.class.getSimpleName(); @@ -142,10 +158,8 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, private final MainStage mMainStage; private final StageListenerImpl mMainStageListener = new StageListenerImpl(); - private final StageTaskUnfoldController mMainUnfoldController; private final SideStage mSideStage; private final StageListenerImpl mSideStageListener = new StageListenerImpl(); - private final StageTaskUnfoldController mSideUnfoldController; private final DisplayLayout mDisplayLayout; @SplitPosition private int mSideStagePosition = SPLIT_POSITION_BOTTOM_OR_RIGHT; @@ -168,6 +182,11 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, private final ShellExecutor mMainExecutor; private final Optional<RecentTasksController> mRecentTasks; + private final Rect mTempRect1 = new Rect(); + private final Rect mTempRect2 = new Rect(); + + private ActivityManager.RunningTaskInfo mFocusingTaskInfo; + /** * A single-top root task which the split divider attached to. */ @@ -181,12 +200,14 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, private boolean mShouldUpdateRecents; private boolean mExitSplitScreenOnHide; private boolean mIsDividerRemoteAnimating; - private boolean mResizingSplits; + private boolean mIsExiting; /** The target stage to dismiss to when unlock after folded. */ @StageType private int mTopStageAfterFoldDismiss = STAGE_TYPE_UNDEFINED; + private DefaultMixedHandler mMixedHandler; + private final SplitWindowManager.ParentContainerCallbacks mParentContainerCallbacks = new SplitWindowManager.ParentContainerCallbacks() { @Override @@ -196,27 +217,57 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, @Override public void onLeashReady(SurfaceControl leash) { - mSyncQueue.runInSync(t -> applyDividerVisibility(t)); + // This is for avoiding divider invisible due to delay of creating so only need + // to do when divider should visible case. + if (mDividerVisible) { + mSyncQueue.runInSync(t -> applyDividerVisibility(t)); + } } }; - StageCoordinator(Context context, int displayId, SyncTransactionQueue syncQueue, + private final SplitScreenTransitions.TransitionCallback mRecentTransitionCallback = + new SplitScreenTransitions.TransitionCallback() { + @Override + public void onTransitionFinished(WindowContainerTransaction finishWct, + SurfaceControl.Transaction finishT) { + // Check if the recent transition is finished by returning to the current split, so we + // can restore the divider bar. + for (int i = 0; i < finishWct.getHierarchyOps().size(); ++i) { + final WindowContainerTransaction.HierarchyOp op = + finishWct.getHierarchyOps().get(i); + final IBinder container = op.getContainer(); + if (op.getType() == HIERARCHY_OP_TYPE_REORDER && op.getToTop() + && (mMainStage.containsContainer(container) + || mSideStage.containsContainer(container))) { + updateSurfaceBounds(mSplitLayout, finishT, false /* applyResizingOffset */); + setDividerVisibility(true, finishT); + return; + } + } + + // Dismiss the split screen if it's not returning to split. + prepareExitSplitScreen(STAGE_TYPE_UNDEFINED, finishWct); + setSplitsVisible(false); + setDividerVisibility(false, finishT); + logExit(EXIT_REASON_UNKNOWN); + } + }; + + protected StageCoordinator(Context context, int displayId, SyncTransactionQueue syncQueue, ShellTaskOrganizer taskOrganizer, DisplayController displayController, DisplayImeController displayImeController, DisplayInsetsController displayInsetsController, Transitions transitions, - TransactionPool transactionPool, SplitscreenEventLogger logger, + TransactionPool transactionPool, IconProvider iconProvider, ShellExecutor mainExecutor, - Optional<RecentTasksController> recentTasks, - Provider<Optional<StageTaskUnfoldController>> unfoldControllerProvider) { + Optional<RecentTasksController> recentTasks) { mContext = context; mDisplayId = displayId; mSyncQueue = syncQueue; mTaskOrganizer = taskOrganizer; - mLogger = logger; + mLogger = new SplitscreenEventLogger(); mMainExecutor = mainExecutor; mRecentTasks = recentTasks; - mMainUnfoldController = unfoldControllerProvider.get().orElse(null); - mSideUnfoldController = unfoldControllerProvider.get().orElse(null); + taskOrganizer.createRootTask(displayId, WINDOWING_MODE_FULLSCREEN, this /* listener */); mMainStage = new MainStage( @@ -226,8 +277,7 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, mMainStageListener, mSyncQueue, mSurfaceSession, - iconProvider, - mMainUnfoldController); + iconProvider); mSideStage = new SideStage( mContext, mTaskOrganizer, @@ -235,8 +285,7 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, mSideStageListener, mSyncQueue, mSurfaceSession, - iconProvider, - mSideUnfoldController); + iconProvider); mDisplayController = displayController; mDisplayImeController = displayImeController; mDisplayInsetsController = displayInsetsController; @@ -250,6 +299,7 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, mDisplayController.addDisplayWindowListener(this); mDisplayLayout = new DisplayLayout(displayController.getDisplayLayout(displayId)); transitions.addHandler(this); + mTaskOrganizer.addFocusListener(this); } @VisibleForTesting @@ -258,9 +308,8 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, DisplayController displayController, DisplayImeController displayImeController, DisplayInsetsController displayInsetsController, SplitLayout splitLayout, Transitions transitions, TransactionPool transactionPool, - SplitscreenEventLogger logger, ShellExecutor mainExecutor, - Optional<RecentTasksController> recentTasks, - Provider<Optional<StageTaskUnfoldController>> unfoldControllerProvider) { + ShellExecutor mainExecutor, + Optional<RecentTasksController> recentTasks) { mContext = context; mDisplayId = displayId; mSyncQueue = syncQueue; @@ -274,9 +323,7 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, mSplitLayout = splitLayout; mSplitTransitions = new SplitScreenTransitions(transactionPool, transitions, this::onTransitionAnimationComplete, this); - mMainUnfoldController = unfoldControllerProvider.get().orElse(null); - mSideUnfoldController = unfoldControllerProvider.get().orElse(null); - mLogger = logger; + mLogger = new SplitscreenEventLogger(); mMainExecutor = mainExecutor; mRecentTasks = recentTasks; mDisplayController.addDisplayWindowListener(this); @@ -284,6 +331,10 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, transitions.addHandler(this); } + public void setMixedHandler(DefaultMixedHandler mixedHandler) { + mMixedHandler = mixedHandler; + } + @VisibleForTesting SplitScreenTransitions getSplitTransitions() { return mSplitTransitions; @@ -329,15 +380,23 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, final WindowContainerTransaction evictWct = new WindowContainerTransaction(); targetStage.evictAllChildren(evictWct); targetStage.addTask(task, wct); - if (!evictWct.isEmpty()) { - wct.merge(evictWct, true /* transfer */); - } if (ENABLE_SHELL_TRANSITIONS) { prepareEnterSplitScreen(wct); - mSplitTransitions.startEnterTransition(TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE, - wct, null, this); + mSplitTransitions.startEnterTransition(TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE, wct, + null, this, new SplitScreenTransitions.TransitionCallback() { + @Override + public void onTransitionFinished(WindowContainerTransaction finishWct, + SurfaceControl.Transaction finishT) { + if (!evictWct.isEmpty()) { + finishWct.merge(evictWct, true); + } + } + }); } else { + if (!evictWct.isEmpty()) { + wct.merge(evictWct, true /* transfer */); + } mTaskOrganizer.applyTransaction(wct); } return true; @@ -357,21 +416,126 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, return result; } + SplitscreenEventLogger getLogger() { + return mLogger; + } + + /** Launches an activity into split. */ + void startIntent(PendingIntent intent, Intent fillInIntent, @SplitPosition int position, + @Nullable Bundle options) { + final WindowContainerTransaction wct = new WindowContainerTransaction(); + final WindowContainerTransaction evictWct = new WindowContainerTransaction(); + prepareEvictChildTasks(position, evictWct); + + options = resolveStartStage(STAGE_TYPE_UNDEFINED, position, options, null /* wct */); + wct.sendPendingIntent(intent, fillInIntent, options); + prepareEnterSplitScreen(wct, null /* taskInfo */, position); + + mSplitTransitions.startEnterTransition(TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE, wct, null, this, + new SplitScreenTransitions.TransitionCallback() { + @Override + public void onTransitionConsumed(boolean aborted) { + // Switch the split position if launching as MULTIPLE_TASK failed. + if (aborted + && (fillInIntent.getFlags() & FLAG_ACTIVITY_MULTIPLE_TASK) != 0) { + setSideStagePositionAnimated( + SplitLayout.reversePosition(mSideStagePosition)); + } + } + + @Override + public void onTransitionFinished(WindowContainerTransaction finishWct, + SurfaceControl.Transaction finishT) { + if (!evictWct.isEmpty()) { + finishWct.merge(evictWct, true); + } + } + }); + } + + /** Launches an activity into split by legacy transition. */ + void startIntentLegacy(PendingIntent intent, Intent fillInIntent, + @SplitPosition int position, @androidx.annotation.Nullable Bundle options) { + final WindowContainerTransaction evictWct = new WindowContainerTransaction(); + prepareEvictChildTasks(position, evictWct); + + LegacyTransitions.ILegacyTransition transition = new LegacyTransitions.ILegacyTransition() { + @Override + public void onAnimationStart(int transit, RemoteAnimationTarget[] apps, + RemoteAnimationTarget[] wallpapers, RemoteAnimationTarget[] nonApps, + IRemoteAnimationFinishedCallback finishedCallback, + SurfaceControl.Transaction t) { + if (apps == null || apps.length == 0) { + if (mMainStage.getChildCount() == 0 || mSideStage.getChildCount() == 0) { + mMainExecutor.execute(() -> + exitSplitScreen(mMainStage.getChildCount() == 0 + ? mSideStage : mMainStage, EXIT_REASON_UNKNOWN)); + } else { + // Switch the split position if launching as MULTIPLE_TASK failed. + if ((fillInIntent.getFlags() & FLAG_ACTIVITY_MULTIPLE_TASK) != 0) { + setSideStagePosition(SplitLayout.reversePosition( + getSideStagePosition()), null); + } + } + + // Do nothing when the animation was cancelled. + t.apply(); + return; + } + + 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); + } + } + + 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 (!mMainStage.isActive() && 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 mainTaskId, @Nullable Bundle mainOptions, int sideTaskId, @Nullable Bundle sideOptions, @SplitPosition int sidePosition, float splitRatio, - @Nullable RemoteTransition remoteTransition) { + @Nullable RemoteTransition remoteTransition, InstanceId instanceId) { final WindowContainerTransaction wct = new WindowContainerTransaction(); mainOptions = mainOptions != null ? mainOptions : new Bundle(); sideOptions = sideOptions != null ? sideOptions : new Bundle(); setSideStagePosition(sidePosition, wct); + if (mMainStage.isActive()) { + mMainStage.evictAllChildren(wct); + mSideStage.evictAllChildren(wct); + } else { + // 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 */); + } mSplitLayout.setDivideRatio(splitRatio); - // 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 */); updateWindowBounds(mSplitLayout, wct); wct.reorder(mRootTaskInfo.token, true); + wct.setForceTranslucent(mRootTaskInfo.token, false); // Make sure the launch options will put tasks in the corresponding split roots addActivityOptions(mainOptions, mMainStage); @@ -382,42 +546,70 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, wct.startTask(sideTaskId, sideOptions); mSplitTransitions.startEnterTransition( - TRANSIT_SPLIT_SCREEN_PAIR_OPEN, wct, remoteTransition, this); + TRANSIT_SPLIT_SCREEN_PAIR_OPEN, wct, remoteTransition, this, null); + setEnterInstanceId(instanceId); } /** Starts 2 tasks in one legacy transition. */ void startTasksWithLegacyTransition(int mainTaskId, @Nullable Bundle mainOptions, int sideTaskId, @Nullable Bundle sideOptions, @SplitPosition int sidePosition, - float splitRatio, RemoteAnimationAdapter adapter) { - startWithLegacyTransition(mainTaskId, sideTaskId, null /* pendingIntent */, - null /* fillInIntent */, mainOptions, sideOptions, sidePosition, splitRatio, - adapter); + float splitRatio, RemoteAnimationAdapter adapter, + InstanceId instanceId) { + final WindowContainerTransaction wct = new WindowContainerTransaction(); + if (sideOptions == null) sideOptions = new Bundle(); + addActivityOptions(sideOptions, mSideStage); + wct.startTask(sideTaskId, sideOptions); + + startWithLegacyTransition(wct, mainTaskId, mainOptions, sidePosition, splitRatio, adapter, + instanceId); } /** Start an intent and a task ordered by {@code intentFirst}. */ void startIntentAndTaskWithLegacyTransition(PendingIntent pendingIntent, Intent fillInIntent, int taskId, @Nullable Bundle mainOptions, @Nullable Bundle sideOptions, - @SplitPosition int sidePosition, float splitRatio, RemoteAnimationAdapter adapter) { - startWithLegacyTransition(taskId, INVALID_TASK_ID, pendingIntent, fillInIntent, - mainOptions, sideOptions, sidePosition, splitRatio, adapter); + @SplitPosition int sidePosition, float splitRatio, RemoteAnimationAdapter adapter, + InstanceId instanceId) { + final WindowContainerTransaction wct = new WindowContainerTransaction(); + if (sideOptions == null) sideOptions = new Bundle(); + addActivityOptions(sideOptions, mSideStage); + wct.sendPendingIntent(pendingIntent, fillInIntent, sideOptions); + + startWithLegacyTransition(wct, taskId, mainOptions, sidePosition, splitRatio, adapter, + instanceId); } - private void startWithLegacyTransition(int mainTaskId, int sideTaskId, - @Nullable PendingIntent pendingIntent, @Nullable Intent fillInIntent, - @Nullable Bundle mainOptions, @Nullable Bundle sideOptions, - @SplitPosition int sidePosition, float splitRatio, RemoteAnimationAdapter adapter) { - final boolean withIntent = pendingIntent != null && fillInIntent != null; + void startShortcutAndTaskWithLegacyTransition(ShortcutInfo shortcutInfo, + int taskId, @Nullable Bundle mainOptions, @Nullable Bundle sideOptions, + @SplitPosition int sidePosition, float splitRatio, RemoteAnimationAdapter adapter, + InstanceId instanceId) { + final WindowContainerTransaction wct = new WindowContainerTransaction(); + if (sideOptions == null) sideOptions = new Bundle(); + addActivityOptions(sideOptions, mSideStage); + wct.startShortcut(mContext.getPackageName(), shortcutInfo, sideOptions); + + startWithLegacyTransition(wct, taskId, mainOptions, sidePosition, splitRatio, adapter, + instanceId); + } + + /** + * @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 Bundle mainOptions, @SplitPosition int sidePosition, float splitRatio, + RemoteAnimationAdapter adapter, InstanceId instanceId) { // Init divider first to make divider leash for remote animation target. mSplitLayout.init(); + mSplitLayout.setDivideRatio(splitRatio); + // Set false to avoid record new bounds with old task still on top; mShouldUpdateRecents = false; mIsDividerRemoteAnimating = true; - final WindowContainerTransaction wct = new WindowContainerTransaction(); + final WindowContainerTransaction evictWct = new WindowContainerTransaction(); prepareEvictChildTasks(SPLIT_POSITION_TOP_OR_LEFT, evictWct); prepareEvictChildTasks(SPLIT_POSITION_BOTTOM_OR_RIGHT, evictWct); - // Need to add another wrapper here in shell so that we can inject the divider bar - // and also manage the process elevation via setRunningRemote + IRemoteAnimationRunner wrapper = new IRemoteAnimationRunner.Stub() { @Override public void onAnimationStart(@WindowManager.TransitionOldType int transit, @@ -425,31 +617,19 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, RemoteAnimationTarget[] wallpapers, RemoteAnimationTarget[] nonApps, final IRemoteAnimationFinishedCallback finishedCallback) { - RemoteAnimationTarget[] augmentedNonApps = - new RemoteAnimationTarget[nonApps.length + 1]; - for (int i = 0; i < nonApps.length; ++i) { - augmentedNonApps[i] = nonApps[i]; - } - augmentedNonApps[augmentedNonApps.length - 1] = getDividerBarLegacyTarget(); - IRemoteAnimationFinishedCallback wrapCallback = new IRemoteAnimationFinishedCallback.Stub() { @Override public void onAnimationFinished() throws RemoteException { - onRemoteAnimationFinishedOrCancelled(evictWct); + onRemoteAnimationFinishedOrCancelled(false /* cancel */, evictWct); finishedCallback.onAnimationFinished(); } }; + Transitions.setRunningRemoteTransitionDelegate(adapter.getCallingApplication()); try { - try { - ActivityTaskManager.getService().setRunningRemoteTransitionDelegate( - adapter.getCallingApplication()); - } catch (SecurityException e) { - Slog.e(TAG, "Unable to boost animation thread. This should only happen" - + " during unit tests"); - } adapter.getRunner().onAnimationStart(transit, apps, wallpapers, - augmentedNonApps, wrapCallback); + ArrayUtils.appendElement(RemoteAnimationTarget.class, nonApps, + getDividerBarLegacyTarget()), wrapCallback); } catch (RemoteException e) { Slog.e(TAG, "Error starting remote animation", e); } @@ -457,7 +637,7 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, @Override public void onAnimationCancelled(boolean isKeyguardOccluded) { - onRemoteAnimationFinishedOrCancelled(evictWct); + onRemoteAnimationFinishedOrCancelled(true /* cancel */, evictWct); try { adapter.getRunner().onAnimationCancelled(isKeyguardOccluded); } catch (RemoteException e) { @@ -476,47 +656,44 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, mainOptions = mainActivityOptions.toBundle(); } - sideOptions = sideOptions != null ? sideOptions : new Bundle(); setSideStagePosition(sidePosition, wct); - - mSplitLayout.setDivideRatio(splitRatio); if (!mMainStage.isActive()) { - // 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 */); } - updateWindowBounds(mSplitLayout, wct); - wct.reorder(mRootTaskInfo.token, true); - // Make sure the launch options will put tasks in the corresponding split roots + if (mainOptions == null) mainOptions = new Bundle(); addActivityOptions(mainOptions, mMainStage); - addActivityOptions(sideOptions, mSideStage); - - // Add task launch requests + updateWindowBounds(mSplitLayout, wct); wct.startTask(mainTaskId, mainOptions); - if (withIntent) { - wct.sendPendingIntent(pendingIntent, fillInIntent, sideOptions); - } else { - wct.startTask(sideTaskId, sideOptions); - } - // Using legacy transitions, so we can't use blast sync since it conflicts. - mTaskOrganizer.applyTransaction(wct); + wct.reorder(mRootTaskInfo.token, true); + wct.setForceTranslucent(mRootTaskInfo.token, false); + + mSyncQueue.queue(wct); mSyncQueue.runInSync(t -> { setDividerVisibility(true, t); updateSurfaceBounds(mSplitLayout, t, false /* applyResizingOffset */); }); + + setEnterInstanceId(instanceId); + } + + private void setEnterInstanceId(InstanceId instanceId) { + if (instanceId != null) { + mLogger.enterRequested(instanceId, ENTER_REASON_LAUNCHER); + } } - private void onRemoteAnimationFinishedOrCancelled(WindowContainerTransaction evictWct) { + private void onRemoteAnimationFinishedOrCancelled(boolean cancel, + WindowContainerTransaction evictWct) { mIsDividerRemoteAnimating = false; mShouldUpdateRecents = true; // 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-instagce, we should exit split and expand that app as full screen. - if (mMainStage.getChildCount() == 0 || mSideStage.getChildCount() == 0) { + // multi-instance, we should exit split and expand that app as full screen. + if (!cancel && (mMainStage.getChildCount() == 0 || mSideStage.getChildCount() == 0)) { mMainExecutor.execute(() -> exitSplitScreen(mMainStage.getChildCount() == 0 - ? mSideStage : mMainStage, EXIT_REASON_UNKNOWN)); + ? mSideStage : mMainStage, EXIT_REASON_UNKNOWN)); } else { mSyncQueue.queue(evictWct); } @@ -534,6 +711,15 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, } } + void prepareEvictNonOpeningChildTasks(@SplitPosition int position, RemoteAnimationTarget[] apps, + WindowContainerTransaction wct) { + if (position == mSideStagePosition) { + mSideStage.evictNonOpeningChildren(apps, wct); + } else { + mMainStage.evictNonOpeningChildren(apps, wct); + } + } + void prepareEvictInvisibleChildTasks(WindowContainerTransaction wct) { mMainStage.evictInvisibleChildren(wct); mSideStage.evictInvisibleChildren(wct); @@ -603,11 +789,28 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, } int getTaskId(@SplitPosition int splitPosition) { - if (mSideStagePosition == splitPosition) { - return mSideStage.getTopVisibleChildTaskId(); - } else { - return mMainStage.getTopVisibleChildTaskId(); + if (splitPosition == SPLIT_POSITION_UNDEFINED) { + return INVALID_TASK_ID; } + + return mSideStagePosition == splitPosition + ? mSideStage.getTopVisibleChildTaskId() + : mMainStage.getTopVisibleChildTaskId(); + } + + void setSideStagePositionAnimated(@SplitPosition int sideStagePosition) { + if (mSideStagePosition == sideStagePosition) return; + SurfaceControl.Transaction t = mTransactionPool.acquire(); + final StageTaskListener topLeftStage = + mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mSideStage : mMainStage; + final StageTaskListener bottomRightStage = + mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mMainStage : mSideStage; + mSplitLayout.splitSwitching(t, topLeftStage.mRootLeash, bottomRightStage.mRootLeash, + () -> { + setSideStagePosition(SplitLayout.reversePosition(mSideStagePosition), + null /* wct */); + mTransactionPool.release(t); + }); } void setSideStagePosition(@SplitPosition int sideStagePosition, @@ -627,7 +830,7 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, onLayoutSizeChanged(mSplitLayout); } else { updateWindowBounds(mSplitLayout, wct); - updateUnfoldBounds(); + sendOnBoundsChanged(); } } } @@ -642,7 +845,7 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, if (ENABLE_SHELL_TRANSITIONS) { final WindowContainerTransaction wct = new WindowContainerTransaction(); prepareExitSplitScreen(mTopStageAfterFoldDismiss, wct); - mSplitTransitions.startDismissTransition(null /* transition */, wct, this, + mSplitTransitions.startDismissTransition(wct, this, mTopStageAfterFoldDismiss, EXIT_REASON_DEVICE_FOLDED); } else { exitSplitScreen( @@ -675,8 +878,8 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, final int dismissTop = mainStageVisible ? STAGE_TYPE_MAIN : STAGE_TYPE_SIDE; final WindowContainerTransaction wct = new WindowContainerTransaction(); prepareExitSplitScreen(dismissTop, wct); - mSplitTransitions.startDismissTransition(null /* transition */, wct, this, - dismissTop, EXIT_REASON_SCREEN_LOCKED_SHOW_ON_TOP); + mSplitTransitions.startDismissTransition(wct, this, dismissTop, + EXIT_REASON_SCREEN_LOCKED_SHOW_ON_TOP); } } } @@ -712,7 +915,9 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, private void applyExitSplitScreen(@Nullable StageTaskListener childrenToTop, WindowContainerTransaction wct, @ExitReason int exitReason) { - if (!mMainStage.isActive()) return; + if (!mMainStage.isActive() || mIsExiting) return; + + onSplitScreenExit(); mRecentTasks.ifPresent(recentTasks -> { // Notify recents if we are exiting in a way that breaks the pair, and disable further @@ -723,26 +928,54 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, } }); mShouldUpdateRecents = false; + mIsDividerRemoteAnimating = false; - // When the exit split-screen is caused by one of the task enters auto pip, - // we want the tasks to be put to bottom instead of top, otherwise it will end up - // a fullscreen plus a pinned task instead of pinned only at the end of the transition. - final boolean fromEnteringPip = exitReason == EXIT_REASON_CHILD_TASK_ENTER_PIP; - mSideStage.removeAllTasks(wct, !fromEnteringPip && mSideStage == childrenToTop); - mMainStage.deactivate(wct, !fromEnteringPip && mMainStage == childrenToTop); - wct.reorder(mRootTaskInfo.token, false /* onTop */); - mTaskOrganizer.applyTransaction(wct); + mSplitLayout.getInvisibleBounds(mTempRect1); + if (childrenToTop == null) { + mSideStage.removeAllTasks(wct, false /* toTop */); + mMainStage.deactivate(wct, false /* toTop */); + wct.reorder(mRootTaskInfo.token, false /* onTop */); + wct.setForceTranslucent(mRootTaskInfo.token, true); + wct.setBounds(mSideStage.mRootTaskInfo.token, mTempRect1); + onTransitionAnimationComplete(); + } else { + // Expand to top side split as full screen for fading out decor animation and dismiss + // another side split(Moving its children to bottom). + mIsExiting = true; + childrenToTop.resetBounds(wct); + wct.reorder(childrenToTop.mRootTaskInfo.token, true); + wct.setSmallestScreenWidthDp(childrenToTop.mRootTaskInfo.token, + SMALLEST_SCREEN_WIDTH_DP_UNDEFINED); + } + mSyncQueue.queue(wct); mSyncQueue.runInSync(t -> { - setResizingSplits(false /* resizing */); t.setWindowCrop(mMainStage.mRootLeash, null) .setWindowCrop(mSideStage.mRootLeash, null); + t.hide(mMainStage.mDimLayer).hide(mSideStage.mDimLayer); setDividerVisibility(false, t); + + if (childrenToTop == null) { + t.setPosition(mSideStage.mRootLeash, mTempRect1.left, mTempRect1.right); + } else { + // In this case, exit still under progress, fade out the split decor after first WCT + // done and do remaining WCT after animation finished. + childrenToTop.fadeOutDecor(() -> { + WindowContainerTransaction finishedWCT = new WindowContainerTransaction(); + mIsExiting = false; + mMainStage.deactivate(finishedWCT, childrenToTop == mMainStage /* toTop */); + mSideStage.removeAllTasks(finishedWCT, childrenToTop == mSideStage /* toTop */); + finishedWCT.reorder(mRootTaskInfo.token, false /* toTop */); + finishedWCT.setForceTranslucent(mRootTaskInfo.token, true); + finishedWCT.setBounds(mSideStage.mRootTaskInfo.token, mTempRect1); + mSyncQueue.queue(finishedWCT); + mSyncQueue.runInSync(at -> { + at.setPosition(mSideStage.mRootLeash, mTempRect1.left, mTempRect1.right); + }); + onTransitionAnimationComplete(); + }); + } }); - // Hide divider and reset its position. - mSplitLayout.resetDividerPosition(); - mSplitLayout.release(); - mTopStageAfterFoldDismiss = STAGE_TYPE_UNDEFINED; Slog.i(TAG, "applyExitSplitScreen, reason = " + exitReasonToString(exitReason)); // Log the exit if (childrenToTop != null) { @@ -753,6 +986,47 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, } /** + * Overridden by child classes. + */ + protected void onSplitScreenEnter() { + } + + /** + * Overridden by child classes. + */ + protected void onSplitScreenExit() { + } + + /** + * Exits the split screen by finishing one of the tasks. + */ + protected void exitStage(@SplitPosition int stageToClose) { + if (ENABLE_SHELL_TRANSITIONS) { + StageTaskListener stageToTop = mSideStagePosition == stageToClose + ? mMainStage + : mSideStage; + exitSplitScreen(stageToTop, EXIT_REASON_APP_FINISHED); + } else { + boolean toEnd = stageToClose == SPLIT_POSITION_BOTTOM_OR_RIGHT; + mSplitLayout.flingDividerToDismiss(toEnd, EXIT_REASON_APP_FINISHED); + } + } + + /** + * Grants focus to the main or the side stages. + */ + protected void grantFocusToStage(@SplitPosition int stageToFocus) { + IActivityTaskManager activityTaskManagerService = IActivityTaskManager.Stub.asInterface( + ServiceManager.getService(Context.ACTIVITY_TASK_SERVICE)); + try { + activityTaskManagerService.setFocusedTask(getTaskId(stageToFocus)); + } catch (RemoteException | NullPointerException e) { + ProtoLog.e(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, + "%s: Unable to update focus on the chosen stage, %s", TAG, e); + } + } + + /** * Returns whether the split pair in the recent tasks list should be broken. */ private boolean shouldBreakPairedTaskInRecents(@ExitReason int exitReason) { @@ -799,6 +1073,7 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, @Nullable ActivityManager.RunningTaskInfo taskInfo, @SplitPosition int startPosition) { if (mMainStage.isActive()) return; + onSplitScreenEnter(); if (taskInfo != null) { setSideStagePosition(startPosition, wct); mSideStage.addTask(taskInfo, wct); @@ -806,12 +1081,14 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, mMainStage.activate(wct, true /* includingTopTask */); updateWindowBounds(mSplitLayout, wct); wct.reorder(mRootTaskInfo.token, true); + wct.setForceTranslucent(mRootTaskInfo.token, false); } void finishEnterSplitScreen(SurfaceControl.Transaction t) { mSplitLayout.init(); setDividerVisibility(true, t); updateSurfaceBounds(mSplitLayout, t, false /* applyResizingOffset */); + t.show(mRootTaskLeash); setSplitsVisible(true); mShouldUpdateRecents = true; updateRecentTasksSplitPair(); @@ -840,6 +1117,10 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, private void addActivityOptions(Bundle opts, StageTaskListener stage) { opts.putParcelable(KEY_LAUNCH_ROOT_TASK_TOKEN, stage.mRootTaskInfo.token); + // Put BAL flags to avoid activity start aborted. Otherwise, flows like shortcut to split + // will be canceled. + opts.putBoolean(KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED, true); + opts.putBoolean(KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED_BY_PERMISSION, true); } void updateActivityOptions(Bundle opts, @SplitPosition int position) { @@ -860,6 +1141,10 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, listener.onStagePositionChanged(STAGE_TYPE_MAIN, getMainStagePosition()); listener.onStagePositionChanged(STAGE_TYPE_SIDE, getSideStagePosition()); listener.onSplitVisibilityChanged(isSplitScreenVisible()); + if (mSplitLayout != null) { + listener.onSplitBoundsChanged(mSplitLayout.getRootBounds(), getMainStageBounds(), + getSideStageBounds()); + } mSideStage.onSplitScreenListenerRegistered(listener, STAGE_TYPE_SIDE); mMainStage.onSplitScreenListenerRegistered(listener, STAGE_TYPE_MAIN); } @@ -872,6 +1157,14 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, } } + private void sendOnBoundsChanged() { + if (mSplitLayout == null) return; + for (int i = mListeners.size() - 1; i >= 0; --i) { + mListeners.get(i).onSplitBoundsChanged(mSplitLayout.getRootBounds(), + getMainStageBounds(), getSideStageBounds()); + } + } + private void onStageChildTaskStatusChanged(StageListenerImpl stageListener, int taskId, boolean present, boolean visible) { int stage; @@ -897,9 +1190,11 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, } } - private void onStageChildTaskEnterPip(StageListenerImpl stageListener, int taskId) { - exitSplitScreen(stageListener == mMainStageListener ? mMainStage : mSideStage, - EXIT_REASON_CHILD_TASK_ENTER_PIP); + private void onStageChildTaskEnterPip() { + // When the exit split-screen is caused by one of the task enters auto pip, + // we want both tasks to be put to bottom instead of top, otherwise it will end up + // a fullscreen plus a pinned task instead of pinned only at the end of the transition. + exitSplitScreen(null, EXIT_REASON_CHILD_TASK_ENTER_PIP); } private void updateRecentTasksSplitPair() { @@ -921,7 +1216,7 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, leftTopTaskId = mainStageTopTaskId; rightBottomTaskId = sideStageTopTaskId; } - StagedSplitBounds splitBounds = new StagedSplitBounds(topLeftBounds, bottomRightBounds, + SplitBounds splitBounds = new SplitBounds(topLeftBounds, bottomRightBounds, leftTopTaskId, rightBottomTaskId); if (mainStageTopTaskId != INVALID_TASK_ID && sideStageTopTaskId != INVALID_TASK_ID) { // Update the pair for the top tasks @@ -935,12 +1230,7 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, final SplitScreen.SplitScreenListener l = mListeners.get(i); l.onSplitVisibilityChanged(mDividerVisible); } - - if (mMainUnfoldController != null && mSideUnfoldController != null) { - mMainUnfoldController.onSplitVisibilityChanged(mDividerVisible); - mSideUnfoldController.onSplitVisibilityChanged(mDividerVisible); - updateUnfoldBounds(); - } + sendOnBoundsChanged(); } @Override @@ -961,11 +1251,6 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, mDisplayInsetsController.addInsetsChangedListener(mDisplayId, mSplitLayout); } - if (mMainUnfoldController != null && mSideUnfoldController != null) { - mMainUnfoldController.init(); - mSideUnfoldController.init(); - } - onRootTaskAppeared(); } @@ -979,13 +1264,8 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, mRootTaskInfo = taskInfo; if (mSplitLayout != null && mSplitLayout.updateConfiguration(mRootTaskInfo.configuration) - && mMainStage.isActive()) { - // TODO(b/204925795): With Shell transition, We are handling split bounds rotation at - // onRotateDisplay. But still need to handle unfold case. - if (ENABLE_SHELL_TRANSITIONS) { - updateUnfoldBounds(); - return; - } + && mMainStage.isActive() + && !ENABLE_SHELL_TRANSITIONS) { // Clear the divider remote animating flag as the divider will be re-rendered to apply // the new rotation config. mIsDividerRemoteAnimating = false; @@ -1009,6 +1289,7 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, } mRootTaskInfo = null; + mRootTaskLeash = null; } @@ -1025,10 +1306,15 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, wct.reparent(mMainStage.mRootTaskInfo.token, mRootTaskInfo.token, true); wct.reparent(mSideStage.mRootTaskInfo.token, mRootTaskInfo.token, true); // Make the stages adjacent to each other so they occlude what's behind them. - wct.setAdjacentRoots(mMainStage.mRootTaskInfo.token, mSideStage.mRootTaskInfo.token, - true /* moveTogether */); + wct.setAdjacentRoots(mMainStage.mRootTaskInfo.token, mSideStage.mRootTaskInfo.token); wct.setLaunchAdjacentFlagRoot(mSideStage.mRootTaskInfo.token); - mTaskOrganizer.applyTransaction(wct); + wct.setForceTranslucent(mRootTaskInfo.token, true); + mSplitLayout.getInvisibleBounds(mTempRect1); + wct.setBounds(mSideStage.mRootTaskInfo.token, mTempRect1); + mSyncQueue.queue(wct); + mSyncQueue.runInSync(t -> { + t.setPosition(mSideStage.mRootLeash, mTempRect1.left, mTempRect1.top); + }); } private void onRootTaskVanished() { @@ -1136,12 +1422,11 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, mDividerFadeInAnimator.cancel(); return; } + mSplitLayout.getRefDividerBounds(mTempRect1); transaction.show(dividerLeash); transaction.setAlpha(dividerLeash, 0); transaction.setLayer(dividerLeash, Integer.MAX_VALUE); - transaction.setPosition(dividerLeash, - mSplitLayout.getRefDividerBounds().left, - mSplitLayout.getRefDividerBounds().top); + transaction.setPosition(dividerLeash, mTempRect1.left, mTempRect1.top); transaction.apply(); } @@ -1161,27 +1446,58 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, private void onStageHasChildrenChanged(StageListenerImpl stageListener) { final boolean hasChildren = stageListener.mHasChildren; final boolean isSideStage = stageListener == mSideStageListener; - if (!hasChildren) { + if (!hasChildren && !mIsExiting && mMainStage.isActive()) { if (isSideStage && mMainStageListener.mVisible) { // Exit to main stage if side stage no longer has children. - exitSplitScreen(mMainStage, EXIT_REASON_APP_FINISHED); + if (ENABLE_SHELL_TRANSITIONS) { + exitSplitScreen(mMainStage, EXIT_REASON_APP_FINISHED); + } else { + mSplitLayout.flingDividerToDismiss( + mSideStagePosition == SPLIT_POSITION_BOTTOM_OR_RIGHT, + EXIT_REASON_APP_FINISHED); + } } else if (!isSideStage && mSideStageListener.mVisible) { // Exit to side stage if main stage no longer has children. - exitSplitScreen(mSideStage, EXIT_REASON_APP_FINISHED); + if (ENABLE_SHELL_TRANSITIONS) { + exitSplitScreen(mSideStage, EXIT_REASON_APP_FINISHED); + } else { + mSplitLayout.flingDividerToDismiss( + mSideStagePosition != SPLIT_POSITION_BOTTOM_OR_RIGHT, + EXIT_REASON_APP_FINISHED); + } } - } else if (isSideStage && !mMainStage.isActive()) { + } else if (isSideStage && hasChildren && !mMainStage.isActive()) { final WindowContainerTransaction wct = new WindowContainerTransaction(); mSplitLayout.init(); - prepareEnterSplitScreen(wct); + if (mLogger.isEnterRequestedByDrag()) { + prepareEnterSplitScreen(wct); + } else { + // TODO (b/238697912) : Add the validation to prevent entering non-recovered status + onSplitScreenEnter(); + mSplitLayout.setDividerAtBorder(mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT); + mMainStage.activate(wct, true /* includingTopTask */); + updateWindowBounds(mSplitLayout, wct); + wct.reorder(mRootTaskInfo.token, true); + wct.setForceTranslucent(mRootTaskInfo.token, false); + } + mSyncQueue.queue(wct); - mSyncQueue.runInSync(t -> - updateSurfaceBounds(mSplitLayout, t, false /* applyResizingOffset */)); + mSyncQueue.runInSync(t -> { + if (mLogger.isEnterRequestedByDrag()) { + updateSurfaceBounds(mSplitLayout, t, false /* applyResizingOffset */); + } else { + mSplitLayout.flingDividerToCenter(); + } + }); } if (mMainStageListener.mHasChildren && mSideStageListener.mHasChildren) { mShouldUpdateRecents = true; 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(), @@ -1190,27 +1506,43 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, } } + boolean isValidToEnterSplitScreen(@NonNull ActivityManager.RunningTaskInfo taskInfo) { + return taskInfo.supportsMultiWindow + && ArrayUtils.contains(CONTROLLED_ACTIVITY_TYPES, taskInfo.getActivityType()) + && ArrayUtils.contains(CONTROLLED_WINDOWING_MODES, taskInfo.getWindowingMode()); + } + + ActivityManager.RunningTaskInfo getFocusingTaskInfo() { + return mFocusingTaskInfo; + } + + @Override + public void onFocusTaskChanged(ActivityManager.RunningTaskInfo taskInfo) { + mFocusingTaskInfo = taskInfo; + } + @Override - public void onSnappedToDismiss(boolean bottomOrRight) { + public void onSnappedToDismiss(boolean bottomOrRight, int reason) { final boolean mainStageToTop = bottomOrRight ? mSideStagePosition == SPLIT_POSITION_BOTTOM_OR_RIGHT : mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT; if (!ENABLE_SHELL_TRANSITIONS) { - exitSplitScreen(mainStageToTop ? mMainStage : mSideStage, EXIT_REASON_DRAG_DIVIDER); + exitSplitScreen(mainStageToTop ? mMainStage : mSideStage, reason); return; } - setResizingSplits(false /* resizing */); final int dismissTop = mainStageToTop ? STAGE_TYPE_MAIN : STAGE_TYPE_SIDE; final WindowContainerTransaction wct = new WindowContainerTransaction(); prepareExitSplitScreen(dismissTop, wct); - mSplitTransitions.startDismissTransition( - null /* transition */, wct, this, dismissTop, EXIT_REASON_DRAG_DIVIDER); + if (mRootTaskInfo != null) { + wct.setDoNotPip(mRootTaskInfo.token); + } + mSplitTransitions.startDismissTransition(wct, this, dismissTop, EXIT_REASON_DRAG_DIVIDER); } @Override public void onDoubleTappedDivider() { - setSideStagePosition(SplitLayout.reversePosition(mSideStagePosition), null /* wct */); + setSideStagePositionAnimated(SplitLayout.reversePosition(mSideStagePosition)); mLogger.logSwap(getMainStagePosition(), mMainStage.getTopChildTaskUid(), getSideStagePosition(), mSideStage.getTopChildTaskUid(), mSplitLayout.isLandscape()); @@ -1229,10 +1561,11 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, public void onLayoutSizeChanging(SplitLayout layout) { final SurfaceControl.Transaction t = mTransactionPool.acquire(); t.setFrameTimelineVsync(Choreographer.getInstance().getVsyncId()); - setResizingSplits(true /* resizing */); updateSurfaceBounds(layout, t, true /* applyResizingOffset */); - mMainStage.onResizing(getMainStageBounds(), t); - mSideStage.onResizing(getSideStageBounds(), t); + getMainStageBounds(mTempRect1); + getSideStageBounds(mTempRect2); + mMainStage.onResizing(mTempRect1, mTempRect2, t); + mSideStage.onResizing(mTempRect2, mTempRect1, t); t.apply(); mTransactionPool.release(t); } @@ -1241,10 +1574,9 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, public void onLayoutSizeChanged(SplitLayout layout) { final WindowContainerTransaction wct = new WindowContainerTransaction(); updateWindowBounds(layout, wct); - updateUnfoldBounds(); + sendOnBoundsChanged(); mSyncQueue.queue(wct); mSyncQueue.runInSync(t -> { - setResizingSplits(false /* resizing */); updateSurfaceBounds(layout, t, false /* applyResizingOffset */); mMainStage.onResized(t); mSideStage.onResized(t); @@ -1252,15 +1584,6 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, mLogger.logResize(mSplitLayout.getDividerPositionAsFraction()); } - private void updateUnfoldBounds() { - if (mMainUnfoldController != null && mSideUnfoldController != null) { - mMainUnfoldController.onLayoutChanged(getMainStageBounds(), getMainStagePosition(), - isLandscape()); - mSideUnfoldController.onLayoutChanged(getSideStageBounds(), getSideStagePosition(), - isLandscape()); - } - } - private boolean isLandscape() { return mSplitLayout.isLandscape(); } @@ -1288,16 +1611,6 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, applyResizingOffset); } - void setResizingSplits(boolean resizing) { - if (resizing == mResizingSplits) return; - try { - ActivityTaskManager.getService().setSplitScreenResizing(resizing); - mResizingSplits = resizing; - } catch (RemoteException e) { - Slog.w(TAG, "Error calling setSplitScreenResizing", e); - } - } - @Override public int getSplitItemPosition(WindowContainerToken token) { if (token == null) { @@ -1329,7 +1642,7 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, if (displayId != DEFAULT_DISPLAY) { return; } - mDisplayController.addDisplayChangingController(this::onRotateDisplay); + mDisplayController.addDisplayChangingController(this::onDisplayChange); } @Override @@ -1340,16 +1653,22 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, mDisplayLayout.set(mDisplayController.getDisplayLayout(displayId)); } - private void onRotateDisplay(int displayId, int fromRotation, int toRotation, - WindowContainerTransaction wct) { + void updateSurfaces(SurfaceControl.Transaction transaction) { + updateSurfaceBounds(mSplitLayout, transaction, /* applyResizingOffset */ false); + mSplitLayout.update(transaction); + } + + private void onDisplayChange(int displayId, int fromRotation, int toRotation, + @Nullable DisplayAreaInfo newDisplayAreaInfo, WindowContainerTransaction wct) { if (!mMainStage.isActive()) return; - // Only do this when shell transition - if (!ENABLE_SHELL_TRANSITIONS) return; mDisplayLayout.rotateTo(mContext.getResources(), toRotation); mSplitLayout.rotateTo(toRotation, mDisplayLayout.stableInsets()); + if (newDisplayAreaInfo != null) { + mSplitLayout.updateConfiguration(newDisplayAreaInfo.configuration); + } updateWindowBounds(mSplitLayout, wct); - updateUnfoldBounds(); + sendOnBoundsChanged(); } private void onFoldedStateChanged(boolean folded) { @@ -1373,6 +1692,22 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, ? mSplitLayout.getBounds2() : mSplitLayout.getBounds1(); } + private void getSideStageBounds(Rect rect) { + if (mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT) { + mSplitLayout.getBounds1(rect); + } else { + mSplitLayout.getBounds2(rect); + } + } + + private void getMainStageBounds(Rect rect) { + if (mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT) { + mSplitLayout.getBounds2(rect); + } else { + mSplitLayout.getBounds1(rect); + } + } + /** * Get the stage that should contain this `taskInfo`. The stage doesn't necessarily contain * this task (yet) so this can also be used to identify which stage to put a task into. @@ -1400,7 +1735,8 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, @Nullable TransitionRequestInfo request) { final ActivityManager.RunningTaskInfo triggerTask = request.getTriggerTask(); if (triggerTask == null) { - if (mMainStage.isActive()) { + if (isSplitActive()) { + // Check if the display is rotating. final TransitionRequestInfo.DisplayChange displayChange = request.getDisplayChange(); if (request.getType() == TRANSIT_CHANGE && displayChange != null @@ -1427,7 +1763,7 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, mRecentTasks.ifPresent(recentTasks -> recentTasks.removeSplitPair(triggerTask.taskId)); } - if (mMainStage.isActive()) { + if (isSplitActive()) { // Try to handle everything while in split-screen, so return a WCT even if it's empty. ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " split is active so using split" + "Transition to handle request. triggerTask=%d type=%s mainChildren=%d" @@ -1442,7 +1778,7 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, int dismissTop = getStageType(stage) == STAGE_TYPE_MAIN ? STAGE_TYPE_SIDE : STAGE_TYPE_MAIN; prepareExitSplitScreen(dismissTop, out); - mSplitTransitions.startDismissTransition(transition, out, this, dismissTop, + mSplitTransitions.setDismissTransition(transition, dismissTop, EXIT_REASON_APP_FINISHED); } } else if (isOpening && inFullscreen) { @@ -1452,13 +1788,14 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, } else if (activityType == ACTIVITY_TYPE_HOME || activityType == ACTIVITY_TYPE_RECENTS) { // Enter overview panel, so start recent transition. - mSplitTransitions.startRecentTransition(transition, out, this, - request.getRemoteTransition()); - } else { - // Occluded by the other fullscreen task, so dismiss both. + mSplitTransitions.setRecentTransition(transition, request.getRemoteTransition(), + mRecentTransitionCallback); + } else if (mSplitTransitions.mPendingRecent == null) { + // If split-task is not controlled by recents animation + // and occluded by the other fullscreen task, dismiss both. prepareExitSplitScreen(STAGE_TYPE_UNDEFINED, out); - mSplitTransitions.startDismissTransition(transition, out, this, - STAGE_TYPE_UNDEFINED, EXIT_REASON_UNKNOWN); + mSplitTransitions.setDismissTransition( + transition, STAGE_TYPE_UNDEFINED, EXIT_REASON_UNKNOWN); } } } else { @@ -1466,12 +1803,40 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, // One task is appearing into split, prepare to enter split screen. out = new WindowContainerTransaction(); prepareEnterSplitScreen(out); - mSplitTransitions.mPendingEnter = transition; + mSplitTransitions.setEnterTransition( + transition, request.getRemoteTransition(), null /* callback */); } } return out; } + /** + * This is used for mixed scenarios. For such scenarios, just make sure to include exiting + * split or entering split when appropriate. + */ + public void addEnterOrExitIfNeeded(@Nullable TransitionRequestInfo request, + @NonNull WindowContainerTransaction outWCT) { + final ActivityManager.RunningTaskInfo triggerTask = request.getTriggerTask(); + if (triggerTask != null && triggerTask.displayId != mDisplayId) { + // Skip handling task on the other display. + return; + } + final @WindowManager.TransitionType int type = request.getType(); + if (isSplitActive() && !isOpeningType(type) + && (mMainStage.getChildCount() == 0 || mSideStage.getChildCount() == 0)) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " One of the splits became " + + "empty during a mixed transition (one not handled by split)," + + " so make sure split-screen state is cleaned-up. " + + "mainStageCount=%d sideStageCount=%d", mMainStage.getChildCount(), + mSideStage.getChildCount()); + prepareExitSplitScreen(STAGE_TYPE_UNDEFINED, outWCT); + } + } + + public boolean isSplitActive() { + return mMainStage.isActive(); + } + @Override public void mergeAnimation(IBinder transition, TransitionInfo info, SurfaceControl.Transaction t, IBinder mergeTarget, @@ -1479,17 +1844,15 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, mSplitTransitions.mergeAnimation(transition, info, t, mergeTarget, finishCallback); } + /** Jump the current transition animation to the end. */ + public boolean end() { + return mSplitTransitions.end(); + } + @Override - public void onTransitionMerged(@NonNull IBinder transition) { - // Once the pending enter transition got merged, make sure to bring divider bar visible and - // clear the pending transition from cache to prevent mess-up the following state. - if (transition == mSplitTransitions.mPendingEnter) { - final SurfaceControl.Transaction t = mTransactionPool.acquire(); - finishEnterSplitScreen(t); - mSplitTransitions.mPendingEnter = null; - t.apply(); - mTransactionPool.release(t); - } + public void onTransitionConsumed(@NonNull IBinder transition, boolean aborted, + @Nullable SurfaceControl.Transaction finishT) { + mSplitTransitions.onTransitionConsumed(transition, aborted, finishT); } @Override @@ -1498,10 +1861,7 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, @NonNull SurfaceControl.Transaction startTransaction, @NonNull SurfaceControl.Transaction finishTransaction, @NonNull Transitions.TransitionFinishCallback finishCallback) { - if (transition != mSplitTransitions.mPendingEnter - && transition != mSplitTransitions.mPendingRecent - && (mSplitTransitions.mPendingDismiss == null - || mSplitTransitions.mPendingDismiss.mTransition != transition)) { + if (!mSplitTransitions.isPendingTransition(transition)) { // 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. @@ -1545,28 +1905,56 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, // Use normal animations. return false; + } else if (mMixedHandler != null && hasDisplayChange(info)) { + // A display-change has been un-expectedly inserted into the transition. Redirect + // handling to the mixed-handler to deal with splitting it up. + if (mMixedHandler.animatePendingSplitWithDisplayChange(transition, info, + startTransaction, finishTransaction, finishCallback)) { + return true; + } } + return startPendingAnimation(transition, info, startTransaction, finishTransaction, + finishCallback); + } + + /** Starts the pending transition animation. */ + public boolean startPendingAnimation(@NonNull IBinder transition, + @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, + @NonNull Transitions.TransitionFinishCallback finishCallback) { boolean shouldAnimate = true; - if (mSplitTransitions.mPendingEnter == transition) { - shouldAnimate = startPendingEnterAnimation(transition, info, startTransaction); - } else if (mSplitTransitions.mPendingRecent == transition) { + if (mSplitTransitions.isPendingEnter(transition)) { + shouldAnimate = startPendingEnterAnimation( + transition, info, startTransaction, finishTransaction); + } else if (mSplitTransitions.isPendingRecent(transition)) { shouldAnimate = startPendingRecentAnimation(transition, info, startTransaction); - } else if (mSplitTransitions.mPendingDismiss != null - && mSplitTransitions.mPendingDismiss.mTransition == transition) { + } else if (mSplitTransitions.isPendingDismiss(transition)) { shouldAnimate = startPendingDismissAnimation( mSplitTransitions.mPendingDismiss, info, startTransaction, finishTransaction); } if (!shouldAnimate) return false; mSplitTransitions.playAnimation(transition, info, startTransaction, finishTransaction, - finishCallback, mMainStage.mRootTaskInfo.token, mSideStage.mRootTaskInfo.token); + finishCallback, mMainStage.mRootTaskInfo.token, mSideStage.mRootTaskInfo.token, + mRootTaskInfo.token); return true; } - void onTransitionAnimationComplete() { + private boolean hasDisplayChange(TransitionInfo info) { + boolean has = false; + for (int iC = 0; iC < info.getChanges().size() && !has; ++iC) { + final TransitionInfo.Change change = info.getChanges().get(iC); + has = change.getMode() == TRANSIT_CHANGE && (change.getFlags() & FLAG_IS_DISPLAY) != 0; + } + return has; + } + + /** Called to clean-up state and do house-keeping after the animation is done. */ + public void onTransitionAnimationComplete() { // If still playing, let it finish. - if (!mMainStage.isActive()) { + if (!mMainStage.isActive() && !mIsExiting) { // Update divider state after animation so that it is still around and positioned // properly for the animation itself. mSplitLayout.release(); @@ -1576,7 +1964,8 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, } private boolean startPendingEnterAnimation(@NonNull IBinder transition, - @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction t) { + @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction t, + @NonNull SurfaceControl.Transaction finishT) { // First, verify that we actually have opened apps in both splits. TransitionInfo.Change mainChild = null; TransitionInfo.Change sideChild = null; @@ -1623,13 +2012,13 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, + " before startAnimation()."); } - finishEnterSplitScreen(t); - addDividerBarToTransition(info, t, true /* show */); + finishEnterSplitScreen(finishT); + addDividerBarToTransition(info, finishT, true /* show */); return true; } - private boolean startPendingDismissAnimation( - @NonNull SplitScreenTransitions.DismissTransition dismissTransition, + /** Synchronize split-screen state with transition and make appropriate preparations. */ + public void prepareDismissAnimation(@StageType int toStage, @ExitReason int dismissReason, @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction t, @NonNull SurfaceControl.Transaction finishT) { // Make some noise if things aren't totally expected. These states shouldn't effect @@ -1662,7 +2051,7 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, mRecentTasks.ifPresent(recentTasks -> { // Notify recents if we are exiting in a way that breaks the pair, and disable further // updates to splits in the recents until we enter split again - if (shouldBreakPairedTaskInRecents(dismissTransition.mReason) && mShouldUpdateRecents) { + if (shouldBreakPairedTaskInRecents(dismissReason) && mShouldUpdateRecents) { for (TransitionInfo.Change change : info.getChanges()) { final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo(); if (taskInfo != null @@ -1679,30 +2068,37 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, // Wait until after animation to update divider // Reset crops so they don't interfere with subsequent launches - t.setWindowCrop(mMainStage.mRootLeash, null); - t.setWindowCrop(mSideStage.mRootLeash, null); + t.setCrop(mMainStage.mRootLeash, null); + t.setCrop(mSideStage.mRootLeash, null); + + if (toStage == STAGE_TYPE_UNDEFINED) { + logExit(dismissReason); + } else { + logExitToStage(dismissReason, toStage == STAGE_TYPE_MAIN); + } + + // Hide divider and dim layer on transition finished. + setDividerVisibility(false, finishT); + finishT.hide(mMainStage.mDimLayer); + finishT.hide(mSideStage.mDimLayer); + } + private boolean startPendingDismissAnimation( + @NonNull SplitScreenTransitions.DismissTransition dismissTransition, + @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction t, + @NonNull SurfaceControl.Transaction finishT) { + prepareDismissAnimation(dismissTransition.mDismissTop, dismissTransition.mReason, info, + t, finishT); if (dismissTransition.mDismissTop == STAGE_TYPE_UNDEFINED) { - logExit(dismissTransition.mReason); // TODO: Have a proper remote for this. Until then, though, reset state and use the // normal animation stuff (which falls back to the normal launcher remote). + t.hide(mSplitLayout.getDividerLeash()); mSplitLayout.release(t); mSplitTransitions.mPendingDismiss = null; return false; - } else { - logExitToStage(dismissTransition.mReason, - dismissTransition.mDismissTop == STAGE_TYPE_MAIN); } - addDividerBarToTransition(info, t, false /* show */); - // We're dismissing split by moving the other one to fullscreen. - // Since we don't have any animations for this yet, just use the internal example - // animations. - - // Hide divider and dim layer on transition finished. - setDividerVisibility(false, finishT); - finishT.hide(mMainStage.mDimLayer); - finishT.hide(mSideStage.mDimLayer); + addDividerBarToTransition(info, finishT, false /* show */); return true; } @@ -1712,46 +2108,26 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, return true; } - void onRecentTransitionFinished(boolean returnToHome, WindowContainerTransaction wct, - SurfaceControl.Transaction finishT) { - // Exclude the case that the split screen has been dismissed already. - if (!mMainStage.isActive()) { - // The latest split dismissing transition might be a no-op transition and thus won't - // callback startAnimation, update split visibility here to cover this kind of no-op - // transition case. - setSplitsVisible(false); - return; - } - - if (returnToHome) { - // When returning to home from recent apps, the splitting tasks are already hidden, so - // append the reset of dismissing operations into the clean-up wct. - prepareExitSplitScreen(STAGE_TYPE_UNDEFINED, wct); - setSplitsVisible(false); - logExit(EXIT_REASON_RETURN_HOME); - } else { - setDividerVisibility(true, finishT); - } - } - private void addDividerBarToTransition(@NonNull TransitionInfo info, - @NonNull SurfaceControl.Transaction t, boolean show) { + @NonNull SurfaceControl.Transaction finishT, boolean show) { final SurfaceControl leash = mSplitLayout.getDividerLeash(); final TransitionInfo.Change barChange = new TransitionInfo.Change(null /* token */, leash); - final Rect bounds = mSplitLayout.getDividerBounds(); - barChange.setStartAbsBounds(bounds); - barChange.setEndAbsBounds(bounds); + mSplitLayout.getRefDividerBounds(mTempRect1); + barChange.setStartAbsBounds(mTempRect1); + barChange.setEndAbsBounds(mTempRect1); barChange.setMode(show ? TRANSIT_TO_FRONT : TRANSIT_TO_BACK); barChange.setFlags(FLAG_IS_DIVIDER_BAR); // Technically this should be order-0, but this is running after layer assignment // and it's a special case, so just add to end. info.addChange(barChange); - // Be default, make it visible. The remote animator can adjust alpha if it plans to animate. + if (show) { - t.setAlpha(leash, 1.f); - t.setLayer(leash, Integer.MAX_VALUE); - t.setPosition(leash, bounds.left, bounds.top); - t.show(leash); + finishT.setLayer(leash, Integer.MAX_VALUE); + finishT.setPosition(leash, mTempRect1.left, mTempRect1.top); + finishT.show(leash); + // Ensure divider surface are re-parented back into the hierarchy at the end of the + // transition. See Transition#buildFinishTransaction for more detail. + finishT.reparent(leash, mRootTaskLeash); } } @@ -1855,8 +2231,8 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, } @Override - public void onChildTaskEnterPip(int taskId) { - StageCoordinator.this.onStageChildTaskEnterPip(this, taskId); + public void onChildTaskEnterPip() { + StageCoordinator.this.onStageChildTaskEnterPip(); } @Override @@ -1868,19 +2244,22 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, @Override public void onNoLongerSupportMultiWindow() { if (mMainStage.isActive()) { + final Toast splitUnsupportedToast = Toast.makeText(mContext, + R.string.dock_non_resizeble_failed_to_dock_text, Toast.LENGTH_SHORT); final boolean isMainStage = mMainStageListener == this; if (!ENABLE_SHELL_TRANSITIONS) { StageCoordinator.this.exitSplitScreen(isMainStage ? mMainStage : mSideStage, EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW); + splitUnsupportedToast.show(); return; } final int stageType = isMainStage ? STAGE_TYPE_MAIN : STAGE_TYPE_SIDE; final WindowContainerTransaction wct = new WindowContainerTransaction(); prepareExitSplitScreen(stageType, wct); - mSplitTransitions.startDismissTransition(null /* transition */, wct, - StageCoordinator.this, stageType, + mSplitTransitions.startDismissTransition(wct,StageCoordinator.this, stageType, EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW); + splitUnsupportedToast.show(); } } 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 949bf5f55808..6b90eabe3bd2 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 @@ -17,12 +17,13 @@ package com.android.wm.shell.splitscreen; import static android.app.ActivityTaskManager.INVALID_TASK_ID; -import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; -import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.app.WindowConfiguration.WINDOWING_MODE_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_WHEN_ACTIVE; import static com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS; import android.annotation.CallSuper; @@ -31,7 +32,10 @@ import android.app.ActivityManager; import android.content.Context; import android.graphics.Point; import android.graphics.Rect; +import android.os.IBinder; +import android.util.Slog; import android.util.SparseArray; +import android.view.RemoteAnimationTarget; import android.view.SurfaceControl; import android.view.SurfaceSession; import android.window.WindowContainerToken; @@ -39,6 +43,7 @@ import android.window.WindowContainerTransaction; import androidx.annotation.NonNull; +import com.android.internal.util.ArrayUtils; import com.android.launcher3.icons.IconProvider; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.SurfaceUtils; @@ -47,6 +52,7 @@ import com.android.wm.shell.common.split.SplitDecorManager; import com.android.wm.shell.splitscreen.SplitScreen.StageType; import java.io.PrintWriter; +import java.util.function.Predicate; /** * Base class that handle common task org. related for split-screen stages. @@ -60,12 +66,6 @@ import java.io.PrintWriter; class StageTaskListener implements ShellTaskOrganizer.TaskListener { private static final String TAG = StageTaskListener.class.getSimpleName(); - protected static final int[] CONTROLLED_ACTIVITY_TYPES = {ACTIVITY_TYPE_STANDARD}; - protected static final int[] CONTROLLED_WINDOWING_MODES = - {WINDOWING_MODE_FULLSCREEN, WINDOWING_MODE_UNDEFINED}; - protected static final int[] CONTROLLED_WINDOWING_MODES_WHEN_ACTIVE = - {WINDOWING_MODE_FULLSCREEN, WINDOWING_MODE_UNDEFINED, WINDOWING_MODE_MULTI_WINDOW}; - /** Callback interface for listening to changes in a split-screen stage. */ public interface StageListenerCallbacks { void onRootTaskAppeared(); @@ -74,7 +74,7 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener { void onChildTaskStatusChanged(int taskId, boolean present, boolean visible); - void onChildTaskEnterPip(int taskId); + void onChildTaskEnterPip(); void onRootTaskVanished(); @@ -95,18 +95,14 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener { // TODO(b/204308910): Extracts SplitDecorManager related code to common package. private SplitDecorManager mSplitDecorManager; - private final StageTaskUnfoldController mStageTaskUnfoldController; - StageTaskListener(Context context, ShellTaskOrganizer taskOrganizer, int displayId, StageListenerCallbacks callbacks, SyncTransactionQueue syncQueue, - SurfaceSession surfaceSession, IconProvider iconProvider, - @Nullable StageTaskUnfoldController stageTaskUnfoldController) { + SurfaceSession surfaceSession, IconProvider iconProvider) { mContext = context; mCallbacks = callbacks; mSyncQueue = syncQueue; mSurfaceSession = surfaceSession; mIconProvider = iconProvider; - mStageTaskUnfoldController = stageTaskUnfoldController; taskOrganizer.createRootTask(displayId, WINDOWING_MODE_MULTI_WINDOW, this); } @@ -119,63 +115,53 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener { } boolean containsToken(WindowContainerToken token) { - if (token.equals(mRootTaskInfo.token)) { - return true; - } - - for (int i = mChildrenTaskInfo.size() - 1; i >= 0; --i) { - if (token.equals(mChildrenTaskInfo.valueAt(i).token)) { - return true; - } - } + return contains(t -> t.token.equals(token)); + } - return false; + boolean containsContainer(IBinder binder) { + return contains(t -> t.token.asBinder() == binder); } /** * Returns the top visible child task's id. */ int getTopVisibleChildTaskId() { - for (int i = mChildrenTaskInfo.size() - 1; i >= 0; --i) { - final ActivityManager.RunningTaskInfo info = mChildrenTaskInfo.valueAt(i); - if (info.isVisible) { - return info.taskId; - } - } - return INVALID_TASK_ID; + final ActivityManager.RunningTaskInfo taskInfo = getChildTaskInfo(t -> t.isVisible); + return taskInfo != null ? taskInfo.taskId : INVALID_TASK_ID; } /** * Returns the top activity uid for the top child task. */ int getTopChildTaskUid() { - for (int i = mChildrenTaskInfo.size() - 1; i >= 0; --i) { - final ActivityManager.RunningTaskInfo info = mChildrenTaskInfo.valueAt(i); - if (info.topActivityInfo == null) { - continue; - } - return info.topActivityInfo.applicationInfo.uid; - } - return 0; + final ActivityManager.RunningTaskInfo taskInfo = + getChildTaskInfo(t -> t.topActivityInfo != null); + return taskInfo != null ? taskInfo.topActivityInfo.applicationInfo.uid : 0; } /** @return {@code true} if this listener contains the currently focused task. */ boolean isFocused() { - if (mRootTaskInfo == null) { - return false; - } + return contains(t -> t.isFocused); + } - if (mRootTaskInfo.isFocused) { + private boolean contains(Predicate<ActivityManager.RunningTaskInfo> predicate) { + if (mRootTaskInfo != null && predicate.test(mRootTaskInfo)) { return true; } + return getChildTaskInfo(predicate) != null; + } + + @Nullable + private ActivityManager.RunningTaskInfo getChildTaskInfo( + Predicate<ActivityManager.RunningTaskInfo> predicate) { for (int i = mChildrenTaskInfo.size() - 1; i >= 0; --i) { - if (mChildrenTaskInfo.valueAt(i).isFocused) { - return true; + final ActivityManager.RunningTaskInfo taskInfo = mChildrenTaskInfo.valueAt(i); + if (predicate.test(taskInfo)) { + return taskInfo; } } - - return false; + return null; } @Override @@ -207,20 +193,11 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener { throw new IllegalArgumentException(this + "\n Unknown task: " + taskInfo + "\n mRootTaskInfo: " + mRootTaskInfo); } - - if (mStageTaskUnfoldController != null) { - mStageTaskUnfoldController.onTaskAppeared(taskInfo, leash); - } } @Override @CallSuper public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) { - if (!taskInfo.supportsMultiWindow) { - // Leave split screen if the task no longer supports multi window. - mCallbacks.onNoLongerSupportMultiWindow(); - return; - } if (mRootTaskInfo.taskId == taskInfo.taskId) { // Inflates split decor view only when the root task is visible. if (mRootTaskInfo.isVisible != taskInfo.isVisible) { @@ -233,6 +210,15 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener { } mRootTaskInfo = taskInfo; } else if (taskInfo.parentTaskId == mRootTaskInfo.taskId) { + if (!taskInfo.supportsMultiWindow + || !ArrayUtils.contains(CONTROLLED_ACTIVITY_TYPES, taskInfo.getActivityType()) + || !ArrayUtils.contains(CONTROLLED_WINDOWING_MODES_WHEN_ACTIVE, + taskInfo.getWindowingMode())) { + // Leave split screen if the task no longer supports multi window or have + // uncontrolled task. + mCallbacks.onNoLongerSupportMultiWindow(); + return; + } mChildrenTaskInfo.put(taskInfo.taskId, taskInfo); mCallbacks.onChildTaskStatusChanged(taskInfo.taskId, true /* present */, taskInfo.isVisible); @@ -258,6 +244,7 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener { if (mRootTaskInfo.taskId == taskId) { mCallbacks.onRootTaskVanished(); mRootTaskInfo = null; + mRootLeash = null; mSyncQueue.runInSync(t -> { t.remove(mDimLayer); mSplitDecorManager.release(t); @@ -266,22 +253,18 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener { mChildrenTaskInfo.remove(taskId); mChildrenLeashes.remove(taskId); mCallbacks.onChildTaskStatusChanged(taskId, false /* present */, taskInfo.isVisible); - if (taskInfo.getWindowingMode() == WINDOWING_MODE_PINNED) { - mCallbacks.onChildTaskEnterPip(taskId); - } if (ENABLE_SHELL_TRANSITIONS) { // Status is managed/synchronized by the transition lifecycle. return; } + if (taskInfo.getWindowingMode() == WINDOWING_MODE_PINNED) { + mCallbacks.onChildTaskEnterPip(); + } sendStatusChanged(); } else { throw new IllegalArgumentException(this + "\n Unknown task: " + taskInfo + "\n mRootTaskInfo: " + mRootTaskInfo); } - - if (mStageTaskUnfoldController != null) { - mStageTaskUnfoldController.onTaskVanished(taskInfo); - } } @Override @@ -305,9 +288,9 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener { } } - void onResizing(Rect newBounds, SurfaceControl.Transaction t) { + void onResizing(Rect newBounds, Rect sideBounds, SurfaceControl.Transaction t) { if (mSplitDecorManager != null && mRootTaskInfo != null) { - mSplitDecorManager.onResizing(mRootTaskInfo, newBounds, t); + mSplitDecorManager.onResizing(mRootTaskInfo, newBounds, sideBounds, t); } } @@ -317,6 +300,14 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener { } } + void fadeOutDecor(Runnable finishedCallback) { + if (mSplitDecorManager != null) { + mSplitDecorManager.fadeOutDecor(finishedCallback); + } else { + finishedCallback.run(); + } + } + void addTask(ActivityManager.RunningTaskInfo task, WindowContainerTransaction wct) { // Clear overridden bounds and windowing mode to make sure the child task can inherit // windowing mode and bounds from split root. @@ -341,6 +332,19 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener { } } + void evictNonOpeningChildren(RemoteAnimationTarget[] apps, WindowContainerTransaction wct) { + final SparseArray<ActivityManager.RunningTaskInfo> toBeEvict = mChildrenTaskInfo.clone(); + for (int i = 0; i < apps.length; i++) { + if (apps[i].mode == MODE_OPENING) { + toBeEvict.remove(apps[i].taskId); + } + } + for (int i = toBeEvict.size() - 1; i >= 0; i--) { + final ActivityManager.RunningTaskInfo taskInfo = toBeEvict.valueAt(i); + wct.reparent(taskInfo.token, null /* parent */, false /* onTop */); + } + } + void evictInvisibleChildren(WindowContainerTransaction wct) { for (int i = mChildrenTaskInfo.size() - 1; i >= 0; i--) { final ActivityManager.RunningTaskInfo taskInfo = mChildrenTaskInfo.valueAt(i); @@ -350,6 +354,11 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener { } } + void resetBounds(WindowContainerTransaction wct) { + wct.setBounds(mRootTaskInfo.token, null); + wct.setAppBounds(mRootTaskInfo.token, null); + } + void onSplitScreenListenerRegistered(SplitScreen.SplitScreenListener listener, @StageType int stage) { for (int i = mChildrenTaskInfo.size() - 1; i >= 0; --i) { @@ -363,7 +372,13 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener { SurfaceControl leash, boolean firstAppeared) { final Point taskPositionInParent = taskInfo.positionInParent; mSyncQueue.runInSync(t -> { - t.setWindowCrop(leash, null); + // The task surface might be released before running in the sync queue for the case like + // trampoline launch, so check if the surface is valid before processing it. + if (!leash.isValid()) { + Slog.w(TAG, "Skip updating invalid child task surface of task#" + taskInfo.taskId); + return; + } + t.setCrop(leash, null); t.setPosition(leash, taskPositionInParent.x, taskPositionInParent.y); if (firstAppeared && !ENABLE_SHELL_TRANSITIONS) { t.setAlpha(leash, 1f); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/ISplitScreen.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/ISplitScreen.aidl deleted file mode 100644 index 45f6d3c8b154..000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/ISplitScreen.aidl +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright (C) 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.wm.shell.stagesplit; - -import android.app.PendingIntent; -import android.content.Intent; -import android.os.Bundle; -import android.os.UserHandle; -import android.view.RemoteAnimationAdapter; -import android.view.RemoteAnimationTarget; -import android.window.RemoteTransition; - -import com.android.wm.shell.stagesplit.ISplitScreenListener; - -/** - * Interface that is exposed to remote callers to manipulate the splitscreen feature. - */ -interface ISplitScreen { - - /** - * Registers a split screen listener. - */ - oneway void registerSplitScreenListener(in ISplitScreenListener listener) = 1; - - /** - * Unregisters a split screen listener. - */ - oneway void unregisterSplitScreenListener(in ISplitScreenListener listener) = 2; - - /** - * Hides the side-stage if it is currently visible. - */ - oneway void setSideStageVisibility(boolean visible) = 3; - - /** - * Removes a task from the side stage. - */ - oneway void removeFromSideStage(int taskId) = 4; - - /** - * Removes the split-screen stages and leaving indicated task to top. Passing INVALID_TASK_ID - * to indicate leaving no top task after leaving split-screen. - */ - oneway void exitSplitScreen(int toTopTaskId) = 5; - - /** - * @param exitSplitScreenOnHide if to exit split-screen if both stages are not visible. - */ - oneway void exitSplitScreenOnHide(boolean exitSplitScreenOnHide) = 6; - - /** - * Starts a task in a stage. - */ - oneway void startTask(int taskId, int stage, int position, in Bundle options) = 7; - - /** - * Starts a shortcut in a stage. - */ - oneway void startShortcut(String packageName, String shortcutId, int stage, int position, - in Bundle options, in UserHandle user) = 8; - - /** - * Starts an activity in a stage. - */ - oneway void startIntent(in PendingIntent intent, in Intent fillInIntent, int stage, - int position, in Bundle options) = 9; - - /** - * Starts tasks simultaneously in one transition. - */ - oneway void startTasks(int mainTaskId, in Bundle mainOptions, int sideTaskId, - in Bundle sideOptions, int sidePosition, in RemoteTransition remoteTransition) = 10; - - /** - * Version of startTasks using legacy transition system. - */ - oneway void startTasksWithLegacyTransition(int mainTaskId, in Bundle mainOptions, - int sideTaskId, in Bundle sideOptions, int sidePosition, - in RemoteAnimationAdapter adapter) = 11; - - /** - * Blocking call that notifies and gets additional split-screen targets when entering - * recents (for example: the dividerBar). - * @param cancel is true if leaving recents back to split (eg. the gesture was cancelled). - * @param appTargets apps that will be re-parented to display area - */ - RemoteAnimationTarget[] onGoingToRecentsLegacy(boolean cancel, - in RemoteAnimationTarget[] appTargets) = 12; -} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/MainStage.java b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/MainStage.java deleted file mode 100644 index 83855be91e04..000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/MainStage.java +++ /dev/null @@ -1,104 +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.stagesplit; - -import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; - -import android.annotation.Nullable; -import android.graphics.Rect; -import android.view.SurfaceSession; -import android.window.WindowContainerToken; -import android.window.WindowContainerTransaction; - -import com.android.wm.shell.ShellTaskOrganizer; -import com.android.wm.shell.common.SyncTransactionQueue; - -/** - * Main stage for split-screen mode. When split-screen is active all standard activity types launch - * on the main stage, except for task that are explicitly pinned to the {@link SideStage}. - * @see StageCoordinator - */ -class MainStage extends StageTaskListener { - private static final String TAG = MainStage.class.getSimpleName(); - - private boolean mIsActive = false; - - MainStage(ShellTaskOrganizer taskOrganizer, int displayId, - StageListenerCallbacks callbacks, SyncTransactionQueue syncQueue, - SurfaceSession surfaceSession, - @Nullable StageTaskUnfoldController stageTaskUnfoldController) { - super(taskOrganizer, displayId, callbacks, syncQueue, surfaceSession, - stageTaskUnfoldController); - } - - boolean isActive() { - return mIsActive; - } - - void activate(Rect rootBounds, WindowContainerTransaction wct) { - if (mIsActive) return; - - final WindowContainerToken rootToken = mRootTaskInfo.token; - wct.setBounds(rootToken, rootBounds) - .setWindowingMode(rootToken, WINDOWING_MODE_MULTI_WINDOW) - .setLaunchRoot( - rootToken, - CONTROLLED_WINDOWING_MODES, - CONTROLLED_ACTIVITY_TYPES) - .reparentTasks( - null /* currentParent */, - rootToken, - CONTROLLED_WINDOWING_MODES, - CONTROLLED_ACTIVITY_TYPES, - true /* onTop */) - // Moving the root task to top after the child tasks were re-parented , or the root - // task cannot be visible and focused. - .reorder(rootToken, true /* onTop */); - - mIsActive = true; - } - - void deactivate(WindowContainerTransaction wct) { - deactivate(wct, false /* toTop */); - } - - void deactivate(WindowContainerTransaction wct, boolean toTop) { - if (!mIsActive) return; - mIsActive = false; - - if (mRootTaskInfo == null) return; - final WindowContainerToken rootToken = mRootTaskInfo.token; - wct.setLaunchRoot( - rootToken, - null, - null) - .reparentTasks( - rootToken, - null /* newParent */, - CONTROLLED_WINDOWING_MODES_WHEN_ACTIVE, - CONTROLLED_ACTIVITY_TYPES, - toTop) - // We want this re-order to the bottom regardless since we are re-parenting - // all its tasks. - .reorder(rootToken, false /* onTop */); - } - - void updateConfiguration(int windowingMode, Rect bounds, WindowContainerTransaction wct) { - wct.setBounds(mRootTaskInfo.token, bounds) - .setWindowingMode(mRootTaskInfo.token, windowingMode); - } -} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/OWNERS b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/OWNERS deleted file mode 100644 index 264e88f32bff..000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/OWNERS +++ /dev/null @@ -1,2 +0,0 @@ -# WM shell sub-modules stagesplit owner -chenghsiuchang@google.com diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/OutlineManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/OutlineManager.java deleted file mode 100644 index 8fbad52c630f..000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/OutlineManager.java +++ /dev/null @@ -1,181 +0,0 @@ -/* - * Copyright (C) 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.wm.shell.stagesplit; - -import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; -import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE; -import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION; -import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY; -import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; - -import android.annotation.Nullable; -import android.content.Context; -import android.content.res.Configuration; -import android.graphics.PixelFormat; -import android.graphics.Rect; -import android.os.Binder; -import android.view.IWindow; -import android.view.InsetsSource; -import android.view.InsetsState; -import android.view.LayoutInflater; -import android.view.SurfaceControl; -import android.view.SurfaceControlViewHost; -import android.view.View; -import android.view.ViewGroup; -import android.view.WindowManager; -import android.view.WindowlessWindowManager; -import android.widget.FrameLayout; - -import com.android.wm.shell.R; - -/** - * Handles drawing outline of the bounds of provided root surface. The outline will be drown with - * the consideration of display insets like status bar, navigation bar and display cutout. - */ -class OutlineManager extends WindowlessWindowManager { - private static final String WINDOW_NAME = "SplitOutlineLayer"; - private final Context mContext; - private final Rect mRootBounds = new Rect(); - private final Rect mTempRect = new Rect(); - private final Rect mLastOutlineBounds = new Rect(); - private final InsetsState mInsetsState = new InsetsState(); - private final int mExpandedTaskBarHeight; - private OutlineView mOutlineView; - private SurfaceControlViewHost mViewHost; - private SurfaceControl mHostLeash; - private SurfaceControl mLeash; - - OutlineManager(Context context, Configuration configuration) { - super(configuration, null /* rootSurface */, null /* hostInputToken */); - mContext = context.createWindowContext(context.getDisplay(), TYPE_APPLICATION_OVERLAY, - null /* options */); - mExpandedTaskBarHeight = mContext.getResources().getDimensionPixelSize( - com.android.internal.R.dimen.taskbar_frame_height); - } - - @Override - protected void attachToParentSurface(IWindow window, SurfaceControl.Builder b) { - b.setParent(mHostLeash); - } - - void inflate(SurfaceControl rootLeash, Rect rootBounds) { - if (mLeash != null || mViewHost != null) return; - - mHostLeash = rootLeash; - mRootBounds.set(rootBounds); - mViewHost = new SurfaceControlViewHost(mContext, mContext.getDisplay(), this); - - final FrameLayout rootLayout = (FrameLayout) LayoutInflater.from(mContext) - .inflate(R.layout.split_outline, null); - mOutlineView = rootLayout.findViewById(R.id.split_outline); - - final WindowManager.LayoutParams lp = new WindowManager.LayoutParams( - 0 /* width */, 0 /* height */, TYPE_APPLICATION_OVERLAY, - FLAG_NOT_FOCUSABLE | FLAG_NOT_TOUCHABLE, PixelFormat.TRANSLUCENT); - lp.width = mRootBounds.width(); - lp.height = mRootBounds.height(); - lp.token = new Binder(); - lp.setTitle(WINDOW_NAME); - lp.privateFlags |= PRIVATE_FLAG_NO_MOVE_ANIMATION | PRIVATE_FLAG_TRUSTED_OVERLAY; - // TODO(b/189839391): Set INPUT_FEATURE_NO_INPUT_CHANNEL after WM supports - // TRUSTED_OVERLAY for windowless window without input channel. - mViewHost.setView(rootLayout, lp); - mLeash = getSurfaceControl(mViewHost.getWindowToken()); - - drawOutline(); - } - - void release() { - if (mViewHost != null) { - mViewHost.release(); - mViewHost = null; - } - mRootBounds.setEmpty(); - mLastOutlineBounds.setEmpty(); - mOutlineView = null; - mHostLeash = null; - mLeash = null; - } - - @Nullable - SurfaceControl getOutlineLeash() { - return mLeash; - } - - void setVisibility(boolean visible) { - if (mOutlineView != null) { - mOutlineView.setVisibility(visible ? View.VISIBLE : View.INVISIBLE); - } - } - - void setRootBounds(Rect rootBounds) { - if (mViewHost == null || mViewHost.getView() == null) { - return; - } - - if (!mRootBounds.equals(rootBounds)) { - WindowManager.LayoutParams lp = - (WindowManager.LayoutParams) mViewHost.getView().getLayoutParams(); - lp.width = rootBounds.width(); - lp.height = rootBounds.height(); - mViewHost.relayout(lp); - mRootBounds.set(rootBounds); - drawOutline(); - } - } - - void onInsetsChanged(InsetsState insetsState) { - if (!mInsetsState.equals(insetsState)) { - mInsetsState.set(insetsState); - drawOutline(); - } - } - - private void computeOutlineBounds(Rect rootBounds, InsetsState insetsState, Rect outBounds) { - outBounds.set(rootBounds); - final InsetsSource taskBarInsetsSource = - insetsState.getSource(InsetsState.ITYPE_EXTRA_NAVIGATION_BAR); - // Only insets the divider bar with task bar when it's expanded so that the rounded corners - // will be drawn against task bar. - if (taskBarInsetsSource.getFrame().height() >= mExpandedTaskBarHeight) { - outBounds.inset(taskBarInsetsSource.calculateVisibleInsets(outBounds)); - } - - // Offset the coordinate from screen based to surface based. - outBounds.offset(-rootBounds.left, -rootBounds.top); - } - - void drawOutline() { - if (mOutlineView == null) { - return; - } - - computeOutlineBounds(mRootBounds, mInsetsState, mTempRect); - if (mTempRect.equals(mLastOutlineBounds)) { - return; - } - - ViewGroup.MarginLayoutParams lp = - (ViewGroup.MarginLayoutParams) mOutlineView.getLayoutParams(); - lp.leftMargin = mTempRect.left; - lp.topMargin = mTempRect.top; - lp.width = mTempRect.width(); - lp.height = mTempRect.height(); - mOutlineView.setLayoutParams(lp); - mLastOutlineBounds.set(mTempRect); - } -} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/OutlineView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/OutlineView.java deleted file mode 100644 index 92b1381fc808..000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/OutlineView.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright (C) 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.wm.shell.stagesplit; - -import static android.view.RoundedCorner.POSITION_BOTTOM_LEFT; -import static android.view.RoundedCorner.POSITION_BOTTOM_RIGHT; -import static android.view.RoundedCorner.POSITION_TOP_LEFT; -import static android.view.RoundedCorner.POSITION_TOP_RIGHT; - -import android.content.Context; -import android.graphics.Canvas; -import android.graphics.Paint; -import android.graphics.Path; -import android.util.AttributeSet; -import android.view.RoundedCorner; -import android.view.View; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.android.internal.R; - -/** View for drawing split outline. */ -public class OutlineView extends View { - private final Paint mPaint = new Paint(); - private final Path mPath = new Path(); - private final float[] mRadii = new float[8]; - - public OutlineView(@NonNull Context context, @Nullable AttributeSet attrs) { - super(context, attrs); - mPaint.setStyle(Paint.Style.STROKE); - mPaint.setStrokeWidth( - getResources().getDimension(R.dimen.accessibility_focus_highlight_stroke_width)); - mPaint.setColor(getResources().getColor(R.color.system_accent1_100, null)); - } - - @Override - protected void onAttachedToWindow() { - // TODO(b/200850654): match the screen corners with the actual display decor. - mRadii[0] = mRadii[1] = getCornerRadius(POSITION_TOP_LEFT); - mRadii[2] = mRadii[3] = getCornerRadius(POSITION_TOP_RIGHT); - mRadii[4] = mRadii[5] = getCornerRadius(POSITION_BOTTOM_RIGHT); - mRadii[6] = mRadii[7] = getCornerRadius(POSITION_BOTTOM_LEFT); - } - - private int getCornerRadius(@RoundedCorner.Position int position) { - final RoundedCorner roundedCorner = getDisplay().getRoundedCorner(position); - return roundedCorner == null ? 0 : roundedCorner.getRadius(); - } - - @Override - protected void onLayout(boolean changed, int left, int top, int right, int bottom) { - if (changed) { - mPath.reset(); - mPath.addRoundRect(0, 0, getWidth(), getHeight(), mRadii, Path.Direction.CW); - } - } - - @Override - protected void onDraw(Canvas canvas) { - canvas.drawPath(mPath, mPaint); - } - - @Override - public boolean hasOverlappingRendering() { - return false; - } -} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SideStage.java b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SideStage.java deleted file mode 100644 index 55c4f3aea19a..000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SideStage.java +++ /dev/null @@ -1,144 +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.stagesplit; - -import android.annotation.CallSuper; -import android.annotation.Nullable; -import android.app.ActivityManager; -import android.content.Context; -import android.graphics.Rect; -import android.view.InsetsSourceControl; -import android.view.InsetsState; -import android.view.SurfaceControl; -import android.view.SurfaceSession; -import android.window.WindowContainerToken; -import android.window.WindowContainerTransaction; - -import com.android.wm.shell.ShellTaskOrganizer; -import com.android.wm.shell.common.DisplayInsetsController; -import com.android.wm.shell.common.SyncTransactionQueue; - -/** - * Side stage for split-screen mode. Only tasks that are explicitly pinned to this stage show up - * here. All other task are launch in the {@link MainStage}. - * - * @see StageCoordinator - */ -class SideStage extends StageTaskListener implements - DisplayInsetsController.OnInsetsChangedListener { - private static final String TAG = SideStage.class.getSimpleName(); - private final Context mContext; - private OutlineManager mOutlineManager; - - SideStage(Context context, ShellTaskOrganizer taskOrganizer, int displayId, - StageListenerCallbacks callbacks, SyncTransactionQueue syncQueue, - SurfaceSession surfaceSession, - @Nullable StageTaskUnfoldController stageTaskUnfoldController) { - super(taskOrganizer, displayId, callbacks, syncQueue, surfaceSession, - stageTaskUnfoldController); - mContext = context; - } - - void addTask(ActivityManager.RunningTaskInfo task, Rect rootBounds, - WindowContainerTransaction wct) { - final WindowContainerToken rootToken = mRootTaskInfo.token; - wct.setBounds(rootToken, rootBounds) - .reparent(task.token, rootToken, true /* onTop*/) - // Moving the root task to top after the child tasks were reparented , or the root - // task cannot be visible and focused. - .reorder(rootToken, true /* onTop */); - } - - boolean removeAllTasks(WindowContainerTransaction wct, boolean toTop) { - // No matter if the root task is empty or not, moving the root to bottom because it no - // longer preserves visible child task. - wct.reorder(mRootTaskInfo.token, false /* onTop */); - if (mChildrenTaskInfo.size() == 0) return false; - wct.reparentTasks( - mRootTaskInfo.token, - null /* newParent */, - CONTROLLED_WINDOWING_MODES_WHEN_ACTIVE, - CONTROLLED_ACTIVITY_TYPES, - toTop); - return true; - } - - boolean removeTask(int taskId, WindowContainerToken newParent, WindowContainerTransaction wct) { - final ActivityManager.RunningTaskInfo task = mChildrenTaskInfo.get(taskId); - if (task == null) return false; - wct.reparent(task.token, newParent, false /* onTop */); - return true; - } - - @Nullable - public SurfaceControl getOutlineLeash() { - return mOutlineManager.getOutlineLeash(); - } - - @Override - @CallSuper - public void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash) { - super.onTaskAppeared(taskInfo, leash); - if (isRootTask(taskInfo)) { - mOutlineManager = new OutlineManager(mContext, taskInfo.configuration); - enableOutline(true); - } - } - - @Override - @CallSuper - public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) { - super.onTaskInfoChanged(taskInfo); - if (isRootTask(taskInfo)) { - mOutlineManager.setRootBounds(taskInfo.configuration.windowConfiguration.getBounds()); - } - } - - private boolean isRootTask(ActivityManager.RunningTaskInfo taskInfo) { - return mRootTaskInfo != null && mRootTaskInfo.taskId == taskInfo.taskId; - } - - void enableOutline(boolean enable) { - if (mOutlineManager == null) { - return; - } - - if (enable) { - if (mRootTaskInfo != null) { - mOutlineManager.inflate(mRootLeash, - mRootTaskInfo.configuration.windowConfiguration.getBounds()); - } - } else { - mOutlineManager.release(); - } - } - - void setOutlineVisibility(boolean visible) { - mOutlineManager.setVisibility(visible); - } - - @Override - public void insetsChanged(InsetsState insetsState) { - mOutlineManager.onInsetsChanged(insetsState); - } - - @Override - public void insetsControlChanged(InsetsState insetsState, - InsetsSourceControl[] activeControls) { - insetsChanged(insetsState); - } -} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SplitScreen.java b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SplitScreen.java deleted file mode 100644 index c5d231262cd2..000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SplitScreen.java +++ /dev/null @@ -1,99 +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.stagesplit; - -import android.annotation.IntDef; -import android.annotation.NonNull; - -import com.android.wm.shell.common.annotations.ExternalThread; -import com.android.wm.shell.common.split.SplitScreenConstants.SplitPosition; - -import java.util.concurrent.Executor; - -/** - * Interface to engage split-screen feature. - * TODO: Figure out which of these are actually needed outside of the Shell - */ -@ExternalThread -public interface SplitScreen { - /** - * Stage type isn't specified normally meaning to use what ever the default is. - * E.g. exit split-screen and launch the app in fullscreen. - */ - int STAGE_TYPE_UNDEFINED = -1; - /** - * The main stage type. - * @see MainStage - */ - int STAGE_TYPE_MAIN = 0; - - /** - * The side stage type. - * @see SideStage - */ - int STAGE_TYPE_SIDE = 1; - - @IntDef(prefix = { "STAGE_TYPE_" }, value = { - STAGE_TYPE_UNDEFINED, - STAGE_TYPE_MAIN, - STAGE_TYPE_SIDE - }) - @interface StageType {} - - /** Callback interface for listening to changes in a split-screen stage. */ - interface SplitScreenListener { - default void onStagePositionChanged(@StageType int stage, @SplitPosition int position) {} - default void onTaskStageChanged(int taskId, @StageType int stage, boolean visible) {} - default void onSplitVisibilityChanged(boolean visible) {} - } - - /** Registers listener that gets split screen callback. */ - void registerSplitScreenListener(@NonNull SplitScreenListener listener, - @NonNull Executor executor); - - /** Unregisters listener that gets split screen callback. */ - void unregisterSplitScreenListener(@NonNull SplitScreenListener listener); - - /** - * Returns a binder that can be passed to an external process to manipulate SplitScreen. - */ - default ISplitScreen createExternalInterface() { - return null; - } - - /** - * Called when the keyguard occluded state changes. - * @param occluded Indicates if the keyguard is now occluded. - */ - void onKeyguardOccludedChanged(boolean occluded); - - /** - * Called when the visibility of the keyguard changes. - * @param showing Indicates if the keyguard is now visible. - */ - void onKeyguardVisibilityChanged(boolean showing); - - /** Get a string representation of a stage type */ - static String stageTypeToString(@StageType int stage) { - switch (stage) { - case STAGE_TYPE_UNDEFINED: return "UNDEFINED"; - case STAGE_TYPE_MAIN: return "MAIN"; - case STAGE_TYPE_SIDE: return "SIDE"; - default: return "UNKNOWN(" + stage + ")"; - } - } -} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SplitScreenController.java deleted file mode 100644 index 07174051a344..000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SplitScreenController.java +++ /dev/null @@ -1,595 +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.stagesplit; - -import static android.view.Display.DEFAULT_DISPLAY; -import static android.view.RemoteAnimationTarget.MODE_OPENING; - -import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission; -import static com.android.wm.shell.common.split.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.splitscreen.SplitScreen.STAGE_TYPE_UNDEFINED; - -import android.app.ActivityManager; -import android.app.ActivityTaskManager; -import android.app.PendingIntent; -import android.content.ActivityNotFoundException; -import android.content.Context; -import android.content.Intent; -import android.content.pm.LauncherApps; -import android.graphics.Rect; -import android.os.Bundle; -import android.os.IBinder; -import android.os.RemoteException; -import android.os.UserHandle; -import android.util.ArrayMap; -import android.util.Slog; -import android.view.IRemoteAnimationFinishedCallback; -import android.view.RemoteAnimationAdapter; -import android.view.RemoteAnimationTarget; -import android.view.SurfaceControl; -import android.view.SurfaceSession; -import android.view.WindowManager; -import android.window.RemoteTransition; -import android.window.WindowContainerTransaction; - -import androidx.annotation.BinderThread; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.android.internal.logging.InstanceId; -import com.android.internal.util.FrameworkStatsLog; -import com.android.wm.shell.RootTaskDisplayAreaOrganizer; -import com.android.wm.shell.ShellTaskOrganizer; -import com.android.wm.shell.common.DisplayImeController; -import com.android.wm.shell.common.DisplayInsetsController; -import com.android.wm.shell.common.RemoteCallable; -import com.android.wm.shell.common.ShellExecutor; -import com.android.wm.shell.common.SyncTransactionQueue; -import com.android.wm.shell.common.TransactionPool; -import com.android.wm.shell.common.annotations.ExternalThread; -import com.android.wm.shell.common.split.SplitScreenConstants.SplitPosition; -import com.android.wm.shell.draganddrop.DragAndDropPolicy; -import com.android.wm.shell.transition.LegacyTransitions; -import com.android.wm.shell.transition.Transitions; - -import java.io.PrintWriter; -import java.util.Arrays; -import java.util.Optional; -import java.util.concurrent.Executor; - -import javax.inject.Provider; - -/** - * Class manages split-screen multitasking mode and implements the main interface - * {@link SplitScreen}. - * @see StageCoordinator - */ -// TODO(b/198577848): Implement split screen flicker test to consolidate CUJ of split screen. -public class SplitScreenController implements DragAndDropPolicy.Starter, - RemoteCallable<SplitScreenController> { - private static final String TAG = SplitScreenController.class.getSimpleName(); - - private final ShellTaskOrganizer mTaskOrganizer; - private final SyncTransactionQueue mSyncQueue; - private final Context mContext; - private final RootTaskDisplayAreaOrganizer mRootTDAOrganizer; - private final ShellExecutor mMainExecutor; - private final SplitScreenImpl mImpl = new SplitScreenImpl(); - private final DisplayImeController mDisplayImeController; - private final DisplayInsetsController mDisplayInsetsController; - private final Transitions mTransitions; - private final TransactionPool mTransactionPool; - private final SplitscreenEventLogger mLogger; - private final Provider<Optional<StageTaskUnfoldController>> mUnfoldControllerProvider; - - private StageCoordinator mStageCoordinator; - - public SplitScreenController(ShellTaskOrganizer shellTaskOrganizer, - SyncTransactionQueue syncQueue, Context context, - RootTaskDisplayAreaOrganizer rootTDAOrganizer, - ShellExecutor mainExecutor, DisplayImeController displayImeController, - DisplayInsetsController displayInsetsController, - Transitions transitions, TransactionPool transactionPool, - Provider<Optional<StageTaskUnfoldController>> unfoldControllerProvider) { - mTaskOrganizer = shellTaskOrganizer; - mSyncQueue = syncQueue; - mContext = context; - mRootTDAOrganizer = rootTDAOrganizer; - mMainExecutor = mainExecutor; - mDisplayImeController = displayImeController; - mDisplayInsetsController = displayInsetsController; - mTransitions = transitions; - mTransactionPool = transactionPool; - mUnfoldControllerProvider = unfoldControllerProvider; - mLogger = new SplitscreenEventLogger(); - } - - public SplitScreen asSplitScreen() { - return mImpl; - } - - @Override - public Context getContext() { - return mContext; - } - - @Override - public ShellExecutor getRemoteCallExecutor() { - return mMainExecutor; - } - - public void onOrganizerRegistered() { - if (mStageCoordinator == null) { - // TODO: Multi-display - mStageCoordinator = new StageCoordinator(mContext, DEFAULT_DISPLAY, mSyncQueue, - mRootTDAOrganizer, mTaskOrganizer, mDisplayImeController, - mDisplayInsetsController, mTransitions, mTransactionPool, mLogger, - mUnfoldControllerProvider); - } - } - - public boolean isSplitScreenVisible() { - return mStageCoordinator.isSplitScreenVisible(); - } - - public boolean moveToSideStage(int taskId, @SplitPosition int sideStagePosition) { - final ActivityManager.RunningTaskInfo task = mTaskOrganizer.getRunningTaskInfo(taskId); - if (task == null) { - throw new IllegalArgumentException("Unknown taskId" + taskId); - } - return moveToSideStage(task, sideStagePosition); - } - - public boolean moveToSideStage(ActivityManager.RunningTaskInfo task, - @SplitPosition int sideStagePosition) { - return mStageCoordinator.moveToSideStage(task, sideStagePosition); - } - - public boolean removeFromSideStage(int taskId) { - return mStageCoordinator.removeFromSideStage(taskId); - } - - public void setSideStageOutline(boolean enable) { - mStageCoordinator.setSideStageOutline(enable); - } - - public void setSideStagePosition(@SplitPosition int sideStagePosition) { - mStageCoordinator.setSideStagePosition(sideStagePosition, null /* wct */); - } - - public void setSideStageVisibility(boolean visible) { - mStageCoordinator.setSideStageVisibility(visible); - } - - public void enterSplitScreen(int taskId, boolean leftOrTop) { - moveToSideStage(taskId, - leftOrTop ? SPLIT_POSITION_TOP_OR_LEFT : SPLIT_POSITION_BOTTOM_OR_RIGHT); - } - - public void exitSplitScreen(int toTopTaskId, int exitReason) { - mStageCoordinator.exitSplitScreen(toTopTaskId, exitReason); - } - - public void onKeyguardOccludedChanged(boolean occluded) { - mStageCoordinator.onKeyguardOccludedChanged(occluded); - } - - public void onKeyguardVisibilityChanged(boolean showing) { - mStageCoordinator.onKeyguardVisibilityChanged(showing); - } - - public void exitSplitScreenOnHide(boolean exitSplitScreenOnHide) { - mStageCoordinator.exitSplitScreenOnHide(exitSplitScreenOnHide); - } - - public void getStageBounds(Rect outTopOrLeftBounds, Rect outBottomOrRightBounds) { - mStageCoordinator.getStageBounds(outTopOrLeftBounds, outBottomOrRightBounds); - } - - public void registerSplitScreenListener(SplitScreen.SplitScreenListener listener) { - mStageCoordinator.registerSplitScreenListener(listener); - } - - public void unregisterSplitScreenListener(SplitScreen.SplitScreenListener listener) { - mStageCoordinator.unregisterSplitScreenListener(listener); - } - - public void startTask(int taskId, @SplitPosition int position, @Nullable Bundle options) { - options = mStageCoordinator.resolveStartStage(STAGE_TYPE_UNDEFINED, position, options, - null /* wct */); - - try { - ActivityTaskManager.getService().startActivityFromRecents(taskId, options); - } catch (RemoteException e) { - Slog.e(TAG, "Failed to launch task", e); - } - } - - public void startShortcut(String packageName, String shortcutId, @SplitPosition int position, - @Nullable Bundle options, UserHandle user) { - options = mStageCoordinator.resolveStartStage(STAGE_TYPE_UNDEFINED, position, options, - null /* wct */); - - try { - LauncherApps launcherApps = - mContext.getSystemService(LauncherApps.class); - launcherApps.startShortcut(packageName, shortcutId, null /* sourceBounds */, - options, user); - } catch (ActivityNotFoundException e) { - Slog.e(TAG, "Failed to launch shortcut", e); - } - } - - public void startIntent(PendingIntent intent, Intent fillInIntent, @SplitPosition int position, - @Nullable Bundle options) { - if (!Transitions.ENABLE_SHELL_TRANSITIONS) { - startIntentLegacy(intent, fillInIntent, position, options); - return; - } - mStageCoordinator.startIntent(intent, fillInIntent, STAGE_TYPE_UNDEFINED, position, options, - null /* remote */); - } - - private void startIntentLegacy(PendingIntent intent, Intent fillInIntent, - @SplitPosition int position, @Nullable Bundle options) { - LegacyTransitions.ILegacyTransition transition = new LegacyTransitions.ILegacyTransition() { - @Override - public void onAnimationStart(int transit, RemoteAnimationTarget[] apps, - RemoteAnimationTarget[] wallpapers, RemoteAnimationTarget[] nonApps, - IRemoteAnimationFinishedCallback finishedCallback, - SurfaceControl.Transaction t) { - mStageCoordinator.updateSurfaceBounds(null /* layout */, t, - false /* applyResizingOffset */); - - if (apps != null) { - for (int i = 0; i < apps.length; ++i) { - if (apps[i].mode == MODE_OPENING) { - t.show(apps[i].leash); - } - } - } - - t.apply(); - if (finishedCallback != null) { - try { - finishedCallback.onAnimationFinished(); - } catch (RemoteException e) { - Slog.e(TAG, "Error finishing legacy transition: ", e); - } - } - } - }; - WindowContainerTransaction wct = new WindowContainerTransaction(); - options = mStageCoordinator.resolveStartStage(STAGE_TYPE_UNDEFINED, position, options, wct); - wct.sendPendingIntent(intent, fillInIntent, options); - mSyncQueue.queue(transition, WindowManager.TRANSIT_OPEN, wct); - } - - RemoteAnimationTarget[] onGoingToRecentsLegacy(boolean cancel, RemoteAnimationTarget[] apps) { - if (!isSplitScreenVisible()) return null; - final SurfaceControl.Builder builder = new SurfaceControl.Builder(new SurfaceSession()) - .setContainerLayer() - .setName("RecentsAnimationSplitTasks") - .setHidden(false) - .setCallsite("SplitScreenController#onGoingtoRecentsLegacy"); - mRootTDAOrganizer.attachToDisplayArea(DEFAULT_DISPLAY, builder); - SurfaceControl sc = builder.build(); - SurfaceControl.Transaction transaction = new SurfaceControl.Transaction(); - - // Ensure that we order these in the parent in the right z-order as their previous order - Arrays.sort(apps, (a1, a2) -> a1.prefixOrderIndex - a2.prefixOrderIndex); - int layer = 1; - for (RemoteAnimationTarget appTarget : apps) { - transaction.reparent(appTarget.leash, sc); - transaction.setPosition(appTarget.leash, appTarget.screenSpaceBounds.left, - appTarget.screenSpaceBounds.top); - transaction.setLayer(appTarget.leash, layer++); - } - transaction.apply(); - transaction.close(); - return new RemoteAnimationTarget[]{ - mStageCoordinator.getDividerBarLegacyTarget(), - mStageCoordinator.getOutlineLegacyTarget()}; - } - - /** - * Sets drag info to be logged when splitscreen is entered. - */ - public void logOnDroppedToSplit(@SplitPosition int position, InstanceId dragSessionId) { - mStageCoordinator.logOnDroppedToSplit(position, dragSessionId); - } - - public void dump(@NonNull PrintWriter pw, String prefix) { - pw.println(prefix + TAG); - if (mStageCoordinator != null) { - mStageCoordinator.dump(pw, prefix); - } - } - - /** - * The interface for calls from outside the Shell, within the host process. - */ - @ExternalThread - private class SplitScreenImpl implements SplitScreen { - private ISplitScreenImpl mISplitScreen; - private final ArrayMap<SplitScreenListener, Executor> mExecutors = new ArrayMap<>(); - private final SplitScreenListener mListener = new SplitScreenListener() { - @Override - public void onStagePositionChanged(int stage, int position) { - for (int i = 0; i < mExecutors.size(); i++) { - final int index = i; - mExecutors.valueAt(index).execute(() -> { - mExecutors.keyAt(index).onStagePositionChanged(stage, position); - }); - } - } - - @Override - public void onTaskStageChanged(int taskId, int stage, boolean visible) { - for (int i = 0; i < mExecutors.size(); i++) { - final int index = i; - mExecutors.valueAt(index).execute(() -> { - mExecutors.keyAt(index).onTaskStageChanged(taskId, stage, visible); - }); - } - } - - @Override - public void onSplitVisibilityChanged(boolean visible) { - for (int i = 0; i < mExecutors.size(); i++) { - final int index = i; - mExecutors.valueAt(index).execute(() -> { - mExecutors.keyAt(index).onSplitVisibilityChanged(visible); - }); - } - } - }; - - @Override - public ISplitScreen createExternalInterface() { - if (mISplitScreen != null) { - mISplitScreen.invalidate(); - } - mISplitScreen = new ISplitScreenImpl(SplitScreenController.this); - return mISplitScreen; - } - - @Override - public void onKeyguardOccludedChanged(boolean occluded) { - mMainExecutor.execute(() -> { - SplitScreenController.this.onKeyguardOccludedChanged(occluded); - }); - } - - @Override - public void registerSplitScreenListener(SplitScreenListener listener, Executor executor) { - if (mExecutors.containsKey(listener)) return; - - mMainExecutor.execute(() -> { - if (mExecutors.size() == 0) { - SplitScreenController.this.registerSplitScreenListener(mListener); - } - - mExecutors.put(listener, executor); - }); - - executor.execute(() -> { - mStageCoordinator.sendStatusToListener(listener); - }); - } - - @Override - public void unregisterSplitScreenListener(SplitScreenListener listener) { - mMainExecutor.execute(() -> { - mExecutors.remove(listener); - - if (mExecutors.size() == 0) { - SplitScreenController.this.unregisterSplitScreenListener(mListener); - } - }); - } - - @Override - public void onKeyguardVisibilityChanged(boolean showing) { - mMainExecutor.execute(() -> { - SplitScreenController.this.onKeyguardVisibilityChanged(showing); - }); - } - } - - /** - * The interface for calls from outside the host process. - */ - @BinderThread - private static class ISplitScreenImpl extends ISplitScreen.Stub { - private SplitScreenController mController; - private ISplitScreenListener mListener; - private final SplitScreen.SplitScreenListener mSplitScreenListener = - new SplitScreen.SplitScreenListener() { - @Override - public void onStagePositionChanged(int stage, int position) { - try { - if (mListener != null) { - mListener.onStagePositionChanged(stage, position); - } - } catch (RemoteException e) { - Slog.e(TAG, "onStagePositionChanged", e); - } - } - - @Override - public void onTaskStageChanged(int taskId, int stage, boolean visible) { - try { - if (mListener != null) { - mListener.onTaskStageChanged(taskId, stage, visible); - } - } catch (RemoteException e) { - Slog.e(TAG, "onTaskStageChanged", e); - } - } - }; - private final IBinder.DeathRecipient mListenerDeathRecipient = - new IBinder.DeathRecipient() { - @Override - @BinderThread - public void binderDied() { - final SplitScreenController controller = mController; - controller.getRemoteCallExecutor().execute(() -> { - mListener = null; - controller.unregisterSplitScreenListener(mSplitScreenListener); - }); - } - }; - - public ISplitScreenImpl(SplitScreenController controller) { - mController = controller; - } - - /** - * Invalidates this instance, preventing future calls from updating the controller. - */ - void invalidate() { - mController = null; - } - - @Override - public void registerSplitScreenListener(ISplitScreenListener listener) { - executeRemoteCallWithTaskPermission(mController, "registerSplitScreenListener", - (controller) -> { - if (mListener != null) { - mListener.asBinder().unlinkToDeath(mListenerDeathRecipient, - 0 /* flags */); - } - if (listener != null) { - try { - listener.asBinder().linkToDeath(mListenerDeathRecipient, - 0 /* flags */); - } catch (RemoteException e) { - Slog.e(TAG, "Failed to link to death"); - return; - } - } - mListener = listener; - controller.registerSplitScreenListener(mSplitScreenListener); - }); - } - - @Override - public void unregisterSplitScreenListener(ISplitScreenListener listener) { - executeRemoteCallWithTaskPermission(mController, "unregisterSplitScreenListener", - (controller) -> { - if (mListener != null) { - mListener.asBinder().unlinkToDeath(mListenerDeathRecipient, - 0 /* flags */); - } - mListener = null; - controller.unregisterSplitScreenListener(mSplitScreenListener); - }); - } - - @Override - public void exitSplitScreen(int toTopTaskId) { - executeRemoteCallWithTaskPermission(mController, "exitSplitScreen", - (controller) -> { - controller.exitSplitScreen(toTopTaskId, - FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__UNKNOWN_EXIT); - }); - } - - @Override - public void exitSplitScreenOnHide(boolean exitSplitScreenOnHide) { - executeRemoteCallWithTaskPermission(mController, "exitSplitScreenOnHide", - (controller) -> { - controller.exitSplitScreenOnHide(exitSplitScreenOnHide); - }); - } - - @Override - public void setSideStageVisibility(boolean visible) { - executeRemoteCallWithTaskPermission(mController, "setSideStageVisibility", - (controller) -> { - controller.setSideStageVisibility(visible); - }); - } - - @Override - public void removeFromSideStage(int taskId) { - executeRemoteCallWithTaskPermission(mController, "removeFromSideStage", - (controller) -> { - controller.removeFromSideStage(taskId); - }); - } - - @Override - public void startTask(int taskId, int stage, int position, @Nullable Bundle options) { - executeRemoteCallWithTaskPermission(mController, "startTask", - (controller) -> { - controller.startTask(taskId, position, options); - }); - } - - @Override - public void startTasksWithLegacyTransition(int mainTaskId, @Nullable Bundle mainOptions, - int sideTaskId, @Nullable Bundle sideOptions, @SplitPosition int sidePosition, - RemoteAnimationAdapter adapter) { - executeRemoteCallWithTaskPermission(mController, "startTasks", - (controller) -> controller.mStageCoordinator.startTasksWithLegacyTransition( - mainTaskId, mainOptions, sideTaskId, sideOptions, sidePosition, - adapter)); - } - - @Override - public void startTasks(int mainTaskId, @Nullable Bundle mainOptions, - int sideTaskId, @Nullable Bundle sideOptions, - @SplitPosition int sidePosition, - @Nullable RemoteTransition remoteTransition) { - executeRemoteCallWithTaskPermission(mController, "startTasks", - (controller) -> controller.mStageCoordinator.startTasks(mainTaskId, mainOptions, - sideTaskId, sideOptions, sidePosition, remoteTransition)); - } - - @Override - public void startShortcut(String packageName, String shortcutId, int stage, int position, - @Nullable Bundle options, UserHandle user) { - executeRemoteCallWithTaskPermission(mController, "startShortcut", - (controller) -> { - controller.startShortcut(packageName, shortcutId, position, - options, user); - }); - } - - @Override - public void startIntent(PendingIntent intent, Intent fillInIntent, int stage, int position, - @Nullable Bundle options) { - executeRemoteCallWithTaskPermission(mController, "startIntent", - (controller) -> { - controller.startIntent(intent, fillInIntent, position, options); - }); - } - - @Override - public RemoteAnimationTarget[] onGoingToRecentsLegacy(boolean cancel, - RemoteAnimationTarget[] apps) { - final RemoteAnimationTarget[][] out = new RemoteAnimationTarget[][]{null}; - executeRemoteCallWithTaskPermission(mController, "onGoingToRecentsLegacy", - (controller) -> out[0] = controller.onGoingToRecentsLegacy(cancel, apps), - true /* blocking */); - return out[0]; - } - } -} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SplitScreenTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SplitScreenTransitions.java deleted file mode 100644 index 018365420177..000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SplitScreenTransitions.java +++ /dev/null @@ -1,292 +0,0 @@ -/* - * Copyright (C) 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.wm.shell.stagesplit; - -import static android.view.WindowManager.TRANSIT_CHANGE; -import static android.view.WindowManager.TRANSIT_CLOSE; -import static android.view.WindowManager.TRANSIT_OPEN; -import static android.view.WindowManager.TRANSIT_TO_BACK; -import static android.view.WindowManager.TRANSIT_TO_FRONT; -import static android.window.TransitionInfo.FLAG_FIRST_CUSTOM; - -import static com.android.wm.shell.transition.Transitions.TRANSIT_SPLIT_DISMISS_SNAP; -import static com.android.wm.shell.transition.Transitions.isOpeningType; - -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.animation.ValueAnimator; -import android.annotation.NonNull; -import android.annotation.Nullable; -import android.graphics.Rect; -import android.os.IBinder; -import android.view.SurfaceControl; -import android.view.WindowManager; -import android.window.RemoteTransition; -import android.window.TransitionInfo; -import android.window.WindowContainerToken; -import android.window.WindowContainerTransaction; - -import com.android.wm.shell.common.TransactionPool; -import com.android.wm.shell.transition.OneShotRemoteHandler; -import com.android.wm.shell.transition.Transitions; - -import java.util.ArrayList; - -/** Manages transition animations for split-screen. */ -class SplitScreenTransitions { - private static final String TAG = "SplitScreenTransitions"; - - /** Flag applied to a transition change to identify it as a divider bar for animation. */ - public static final int FLAG_IS_DIVIDER_BAR = FLAG_FIRST_CUSTOM; - - private final TransactionPool mTransactionPool; - private final Transitions mTransitions; - private final Runnable mOnFinish; - - IBinder mPendingDismiss = null; - IBinder mPendingEnter = null; - - private IBinder mAnimatingTransition = null; - private OneShotRemoteHandler mRemoteHandler = null; - - private Transitions.TransitionFinishCallback mRemoteFinishCB = (wct, wctCB) -> { - if (wct != null || wctCB != null) { - throw new UnsupportedOperationException("finish transactions not supported yet."); - } - onFinish(); - }; - - /** Keeps track of currently running animations */ - private final ArrayList<Animator> mAnimations = new ArrayList<>(); - - private Transitions.TransitionFinishCallback mFinishCallback = null; - private SurfaceControl.Transaction mFinishTransaction; - - SplitScreenTransitions(@NonNull TransactionPool pool, @NonNull Transitions transitions, - @NonNull Runnable onFinishCallback) { - mTransactionPool = pool; - mTransitions = transitions; - mOnFinish = onFinishCallback; - } - - void playAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, - @NonNull SurfaceControl.Transaction startTransaction, - @NonNull SurfaceControl.Transaction finishTransaction, - @NonNull Transitions.TransitionFinishCallback finishCallback, - @NonNull WindowContainerToken mainRoot, @NonNull WindowContainerToken sideRoot) { - mFinishCallback = finishCallback; - mAnimatingTransition = transition; - if (mRemoteHandler != null) { - mRemoteHandler.startAnimation(transition, info, startTransaction, finishTransaction, - mRemoteFinishCB); - mRemoteHandler = null; - return; - } - playInternalAnimation(transition, info, startTransaction, mainRoot, sideRoot); - } - - private void playInternalAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, - @NonNull SurfaceControl.Transaction t, @NonNull WindowContainerToken mainRoot, - @NonNull WindowContainerToken sideRoot) { - mFinishTransaction = mTransactionPool.acquire(); - - // Play some place-holder fade animations - for (int i = info.getChanges().size() - 1; i >= 0; --i) { - final TransitionInfo.Change change = info.getChanges().get(i); - final SurfaceControl leash = change.getLeash(); - final int mode = info.getChanges().get(i).getMode(); - - if (mode == TRANSIT_CHANGE) { - if (change.getParent() != null) { - // This is probably reparented, so we want the parent to be immediately visible - final TransitionInfo.Change parentChange = info.getChange(change.getParent()); - t.show(parentChange.getLeash()); - t.setAlpha(parentChange.getLeash(), 1.f); - // and then animate this layer outside the parent (since, for example, this is - // the home task animating from fullscreen to part-screen). - t.reparent(leash, info.getRootLeash()); - t.setLayer(leash, info.getChanges().size() - i); - // build the finish reparent/reposition - mFinishTransaction.reparent(leash, parentChange.getLeash()); - mFinishTransaction.setPosition(leash, - change.getEndRelOffset().x, change.getEndRelOffset().y); - } - // TODO(shell-transitions): screenshot here - final Rect startBounds = new Rect(change.getStartAbsBounds()); - final Rect endBounds = new Rect(change.getEndAbsBounds()); - startBounds.offset(-info.getRootOffset().x, -info.getRootOffset().y); - endBounds.offset(-info.getRootOffset().x, -info.getRootOffset().y); - startExampleResizeAnimation(leash, startBounds, endBounds); - } - if (change.getParent() != null) { - continue; - } - - if (transition == mPendingEnter && (mainRoot.equals(change.getContainer()) - || sideRoot.equals(change.getContainer()))) { - t.setWindowCrop(leash, change.getStartAbsBounds().width(), - change.getStartAbsBounds().height()); - } - boolean isOpening = isOpeningType(info.getType()); - if (isOpening && (mode == TRANSIT_OPEN || mode == TRANSIT_TO_FRONT)) { - // fade in - startExampleAnimation(leash, true /* show */); - } else if (!isOpening && (mode == TRANSIT_CLOSE || mode == TRANSIT_TO_BACK)) { - // fade out - if (info.getType() == TRANSIT_SPLIT_DISMISS_SNAP) { - // Dismissing via snap-to-top/bottom means that the dismissed task is already - // not-visible (usually cropped to oblivion) so immediately set its alpha to 0 - // and don't animate it so it doesn't pop-in when reparented. - t.setAlpha(leash, 0.f); - } else { - startExampleAnimation(leash, false /* show */); - } - } - } - t.apply(); - onFinish(); - } - - /** Starts a transition to enter split with a remote transition animator. */ - IBinder startEnterTransition(@WindowManager.TransitionType int transitType, - @NonNull WindowContainerTransaction wct, @Nullable RemoteTransition remoteTransition, - @NonNull Transitions.TransitionHandler handler) { - if (remoteTransition != null) { - // Wrapping it for ease-of-use (OneShot handles all the binder linking/death stuff) - mRemoteHandler = new OneShotRemoteHandler( - mTransitions.getMainExecutor(), remoteTransition); - } - final IBinder transition = mTransitions.startTransition(transitType, wct, handler); - mPendingEnter = transition; - if (mRemoteHandler != null) { - mRemoteHandler.setTransition(transition); - } - return transition; - } - - /** Starts a transition for dismissing split after dragging the divider to a screen edge */ - IBinder startSnapToDismiss(@NonNull WindowContainerTransaction wct, - @NonNull Transitions.TransitionHandler handler) { - final IBinder transition = mTransitions.startTransition( - TRANSIT_SPLIT_DISMISS_SNAP, wct, handler); - mPendingDismiss = transition; - return transition; - } - - void onFinish() { - if (!mAnimations.isEmpty()) return; - mOnFinish.run(); - if (mFinishTransaction != null) { - mFinishTransaction.apply(); - mTransactionPool.release(mFinishTransaction); - mFinishTransaction = null; - } - mFinishCallback.onTransitionFinished(null /* wct */, null /* wctCB */); - mFinishCallback = null; - if (mAnimatingTransition == mPendingEnter) { - mPendingEnter = null; - } - if (mAnimatingTransition == mPendingDismiss) { - mPendingDismiss = null; - } - mAnimatingTransition = null; - } - - // TODO(shell-transitions): real animations - private void startExampleAnimation(@NonNull SurfaceControl leash, boolean show) { - final float end = show ? 1.f : 0.f; - final float start = 1.f - end; - final SurfaceControl.Transaction transaction = mTransactionPool.acquire(); - final ValueAnimator va = ValueAnimator.ofFloat(start, end); - va.setDuration(500); - va.addUpdateListener(animation -> { - float fraction = animation.getAnimatedFraction(); - transaction.setAlpha(leash, start * (1.f - fraction) + end * fraction); - transaction.apply(); - }); - final Runnable finisher = () -> { - transaction.setAlpha(leash, end); - transaction.apply(); - mTransactionPool.release(transaction); - mTransitions.getMainExecutor().execute(() -> { - mAnimations.remove(va); - onFinish(); - }); - }; - va.addListener(new Animator.AnimatorListener() { - @Override - public void onAnimationStart(Animator animation) { } - - @Override - public void onAnimationEnd(Animator animation) { - finisher.run(); - } - - @Override - public void onAnimationCancel(Animator animation) { - finisher.run(); - } - - @Override - public void onAnimationRepeat(Animator animation) { } - }); - mAnimations.add(va); - mTransitions.getAnimExecutor().execute(va::start); - } - - // TODO(shell-transitions): real animations - private void startExampleResizeAnimation(@NonNull SurfaceControl leash, - @NonNull Rect startBounds, @NonNull Rect endBounds) { - final SurfaceControl.Transaction transaction = mTransactionPool.acquire(); - final ValueAnimator va = ValueAnimator.ofFloat(0.f, 1.f); - va.setDuration(500); - va.addUpdateListener(animation -> { - float fraction = animation.getAnimatedFraction(); - transaction.setWindowCrop(leash, - (int) (startBounds.width() * (1.f - fraction) + endBounds.width() * fraction), - (int) (startBounds.height() * (1.f - fraction) - + endBounds.height() * fraction)); - transaction.setPosition(leash, - startBounds.left * (1.f - fraction) + endBounds.left * fraction, - startBounds.top * (1.f - fraction) + endBounds.top * fraction); - transaction.apply(); - }); - final Runnable finisher = () -> { - transaction.setWindowCrop(leash, 0, 0); - transaction.setPosition(leash, endBounds.left, endBounds.top); - transaction.apply(); - mTransactionPool.release(transaction); - mTransitions.getMainExecutor().execute(() -> { - mAnimations.remove(va); - onFinish(); - }); - }; - va.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - finisher.run(); - } - - @Override - public void onAnimationCancel(Animator animation) { - finisher.run(); - } - }); - mAnimations.add(va); - mTransitions.getAnimExecutor().execute(va::start); - } -} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SplitscreenEventLogger.java b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SplitscreenEventLogger.java deleted file mode 100644 index e1850396a5c0..000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SplitscreenEventLogger.java +++ /dev/null @@ -1,324 +0,0 @@ -/* - * Copyright (C) 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.wm.shell.stagesplit; - -import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__ENTER_REASON__OVERVIEW; -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 com.android.internal.logging.InstanceId; -import com.android.internal.logging.InstanceIdSequence; -import com.android.internal.util.FrameworkStatsLog; -import com.android.wm.shell.common.split.SplitScreenConstants.SplitPosition; - -/** - * Helper class that to log Drag & Drop UIEvents for a single session, see also go/uievent - */ -public class SplitscreenEventLogger { - - // Used to generate instance ids for this drag if one is not provided - private final InstanceIdSequence mIdSequence; - - // The instance id for the current splitscreen session (from start to end) - private InstanceId mLoggerSessionId; - - // Drag info - private @SplitPosition int mDragEnterPosition; - private InstanceId mDragEnterSessionId; - - // For deduping async events - private int mLastMainStagePosition = -1; - private int mLastMainStageUid = -1; - private int mLastSideStagePosition = -1; - private int mLastSideStageUid = -1; - private float mLastSplitRatio = -1f; - - public SplitscreenEventLogger() { - mIdSequence = new InstanceIdSequence(Integer.MAX_VALUE); - } - - /** - * Return whether a splitscreen session has started. - */ - public boolean hasStartedSession() { - return mLoggerSessionId != null; - } - - /** - * May be called before logEnter() to indicate that the session was started from a drag. - */ - public void enterRequestedByDrag(@SplitPosition int position, InstanceId dragSessionId) { - mDragEnterPosition = position; - mDragEnterSessionId = dragSessionId; - } - - /** - * Logs when the user enters splitscreen. - */ - public void logEnter(float splitRatio, - @SplitPosition int mainStagePosition, int mainStageUid, - @SplitPosition int sideStagePosition, int sideStageUid, - boolean isLandscape) { - mLoggerSessionId = mIdSequence.newInstanceId(); - int enterReason = mDragEnterPosition != SPLIT_POSITION_UNDEFINED - ? getDragEnterReasonFromSplitPosition(mDragEnterPosition, isLandscape) - : SPLITSCREEN_UICHANGED__ENTER_REASON__OVERVIEW; - updateMainStageState(getMainStagePositionFromSplitPosition(mainStagePosition, isLandscape), - mainStageUid); - updateSideStageState(getSideStagePositionFromSplitPosition(sideStagePosition, isLandscape), - sideStageUid); - updateSplitRatioState(splitRatio); - FrameworkStatsLog.write(FrameworkStatsLog.SPLITSCREEN_UI_CHANGED, - FrameworkStatsLog.SPLITSCREEN_UICHANGED__ACTION__ENTER, - enterReason, - 0 /* exitReason */, - splitRatio, - mLastMainStagePosition, - mLastMainStageUid, - mLastSideStagePosition, - mLastSideStageUid, - mDragEnterSessionId != null ? mDragEnterSessionId.getId() : 0, - mLoggerSessionId.getId()); - } - - /** - * Logs when the user exits splitscreen. Only one of the main or side stages should be - * specified to indicate which position was focused as a part of exiting (both can be unset). - */ - public void logExit(int exitReason, @SplitPosition int mainStagePosition, int mainStageUid, - @SplitPosition int sideStagePosition, int sideStageUid, boolean isLandscape) { - if (mLoggerSessionId == null) { - // Ignore changes until we've started logging the session - return; - } - if ((mainStagePosition != SPLIT_POSITION_UNDEFINED - && sideStagePosition != SPLIT_POSITION_UNDEFINED) - || (mainStageUid != 0 && sideStageUid != 0)) { - throw new IllegalArgumentException("Only main or side stage should be set"); - } - FrameworkStatsLog.write(FrameworkStatsLog.SPLITSCREEN_UI_CHANGED, - FrameworkStatsLog.SPLITSCREEN_UICHANGED__ACTION__EXIT, - 0 /* enterReason */, - exitReason, - 0f /* splitRatio */, - getMainStagePositionFromSplitPosition(mainStagePosition, isLandscape), - mainStageUid, - getSideStagePositionFromSplitPosition(sideStagePosition, isLandscape), - sideStageUid, - 0 /* dragInstanceId */, - mLoggerSessionId.getId()); - - // Reset states - mLoggerSessionId = null; - mDragEnterPosition = SPLIT_POSITION_UNDEFINED; - mDragEnterSessionId = null; - mLastMainStagePosition = -1; - mLastMainStageUid = -1; - mLastSideStagePosition = -1; - mLastSideStageUid = -1; - } - - /** - * Logs when an app in the main stage changes. - */ - public void logMainStageAppChange(@SplitPosition int mainStagePosition, int mainStageUid, - boolean isLandscape) { - if (mLoggerSessionId == null) { - // Ignore changes until we've started logging the session - return; - } - if (!updateMainStageState(getMainStagePositionFromSplitPosition(mainStagePosition, - isLandscape), mainStageUid)) { - // Ignore if there are no user perceived changes - return; - } - - FrameworkStatsLog.write(FrameworkStatsLog.SPLITSCREEN_UI_CHANGED, - FrameworkStatsLog.SPLITSCREEN_UICHANGED__ACTION__APP_CHANGE, - 0 /* enterReason */, - 0 /* exitReason */, - 0f /* splitRatio */, - mLastMainStagePosition, - mLastMainStageUid, - 0 /* sideStagePosition */, - 0 /* sideStageUid */, - 0 /* dragInstanceId */, - mLoggerSessionId.getId()); - } - - /** - * Logs when an app in the side stage changes. - */ - public void logSideStageAppChange(@SplitPosition int sideStagePosition, int sideStageUid, - boolean isLandscape) { - if (mLoggerSessionId == null) { - // Ignore changes until we've started logging the session - return; - } - if (!updateSideStageState(getSideStagePositionFromSplitPosition(sideStagePosition, - isLandscape), sideStageUid)) { - // Ignore if there are no user perceived changes - return; - } - - FrameworkStatsLog.write(FrameworkStatsLog.SPLITSCREEN_UI_CHANGED, - FrameworkStatsLog.SPLITSCREEN_UICHANGED__ACTION__APP_CHANGE, - 0 /* enterReason */, - 0 /* exitReason */, - 0f /* splitRatio */, - 0 /* mainStagePosition */, - 0 /* mainStageUid */, - mLastSideStagePosition, - mLastSideStageUid, - 0 /* dragInstanceId */, - mLoggerSessionId.getId()); - } - - /** - * Logs when the splitscreen ratio changes. - */ - public void logResize(float splitRatio) { - if (mLoggerSessionId == null) { - // Ignore changes until we've started logging the session - return; - } - if (splitRatio <= 0f || splitRatio >= 1f) { - // Don't bother reporting resizes that end up dismissing the split, that will be logged - // via the exit event - return; - } - if (!updateSplitRatioState(splitRatio)) { - // Ignore if there are no user perceived changes - return; - } - FrameworkStatsLog.write(FrameworkStatsLog.SPLITSCREEN_UI_CHANGED, - FrameworkStatsLog.SPLITSCREEN_UICHANGED__ACTION__RESIZE, - 0 /* enterReason */, - 0 /* exitReason */, - mLastSplitRatio, - 0 /* mainStagePosition */, 0 /* mainStageUid */, - 0 /* sideStagePosition */, 0 /* sideStageUid */, - 0 /* dragInstanceId */, - mLoggerSessionId.getId()); - } - - /** - * Logs when the apps in splitscreen are swapped. - */ - public void logSwap(@SplitPosition int mainStagePosition, int mainStageUid, - @SplitPosition int sideStagePosition, int sideStageUid, boolean isLandscape) { - if (mLoggerSessionId == null) { - // Ignore changes until we've started logging the session - return; - } - - updateMainStageState(getMainStagePositionFromSplitPosition(mainStagePosition, isLandscape), - mainStageUid); - updateSideStageState(getSideStagePositionFromSplitPosition(sideStagePosition, isLandscape), - sideStageUid); - FrameworkStatsLog.write(FrameworkStatsLog.SPLITSCREEN_UI_CHANGED, - FrameworkStatsLog.SPLITSCREEN_UICHANGED__ACTION__SWAP, - 0 /* enterReason */, - 0 /* exitReason */, - 0f /* splitRatio */, - mLastMainStagePosition, - mLastMainStageUid, - mLastSideStagePosition, - mLastSideStageUid, - 0 /* dragInstanceId */, - mLoggerSessionId.getId()); - } - - private boolean updateMainStageState(int mainStagePosition, int mainStageUid) { - boolean changed = (mLastMainStagePosition != mainStagePosition) - || (mLastMainStageUid != mainStageUid); - if (!changed) { - return false; - } - - mLastMainStagePosition = mainStagePosition; - mLastMainStageUid = mainStageUid; - return true; - } - - private boolean updateSideStageState(int sideStagePosition, int sideStageUid) { - boolean changed = (mLastSideStagePosition != sideStagePosition) - || (mLastSideStageUid != sideStageUid); - if (!changed) { - return false; - } - - mLastSideStagePosition = sideStagePosition; - mLastSideStageUid = sideStageUid; - return true; - } - - private boolean updateSplitRatioState(float splitRatio) { - boolean changed = Float.compare(mLastSplitRatio, splitRatio) != 0; - if (!changed) { - return false; - } - - mLastSplitRatio = splitRatio; - return true; - } - - public int getDragEnterReasonFromSplitPosition(@SplitPosition int position, - boolean isLandscape) { - if (isLandscape) { - return position == SPLIT_POSITION_TOP_OR_LEFT - ? FrameworkStatsLog.SPLITSCREEN_UICHANGED__ENTER_REASON__DRAG_LEFT - : FrameworkStatsLog.SPLITSCREEN_UICHANGED__ENTER_REASON__DRAG_RIGHT; - } else { - return position == SPLIT_POSITION_TOP_OR_LEFT - ? FrameworkStatsLog.SPLITSCREEN_UICHANGED__ENTER_REASON__DRAG_TOP - : FrameworkStatsLog.SPLITSCREEN_UICHANGED__ENTER_REASON__DRAG_BOTTOM; - } - } - - private int getMainStagePositionFromSplitPosition(@SplitPosition int position, - boolean isLandscape) { - if (position == SPLIT_POSITION_UNDEFINED) { - return 0; - } - if (isLandscape) { - return position == SPLIT_POSITION_TOP_OR_LEFT - ? FrameworkStatsLog.SPLITSCREEN_UICHANGED__MAIN_STAGE_POSITION__LEFT - : FrameworkStatsLog.SPLITSCREEN_UICHANGED__MAIN_STAGE_POSITION__RIGHT; - } else { - return position == SPLIT_POSITION_TOP_OR_LEFT - ? FrameworkStatsLog.SPLITSCREEN_UICHANGED__MAIN_STAGE_POSITION__TOP - : FrameworkStatsLog.SPLITSCREEN_UICHANGED__MAIN_STAGE_POSITION__BOTTOM; - } - } - - private int getSideStagePositionFromSplitPosition(@SplitPosition int position, - boolean isLandscape) { - if (position == SPLIT_POSITION_UNDEFINED) { - return 0; - } - if (isLandscape) { - return position == SPLIT_POSITION_TOP_OR_LEFT - ? FrameworkStatsLog.SPLITSCREEN_UICHANGED__SIDE_STAGE_POSITION__LEFT - : FrameworkStatsLog.SPLITSCREEN_UICHANGED__SIDE_STAGE_POSITION__RIGHT; - } else { - return position == SPLIT_POSITION_TOP_OR_LEFT - ? FrameworkStatsLog.SPLITSCREEN_UICHANGED__SIDE_STAGE_POSITION__TOP - : FrameworkStatsLog.SPLITSCREEN_UICHANGED__SIDE_STAGE_POSITION__BOTTOM; - } - } -} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/StageCoordinator.java deleted file mode 100644 index de0feeecad4b..000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/StageCoordinator.java +++ /dev/null @@ -1,1333 +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.stagesplit; - -import static android.app.ActivityOptions.KEY_LAUNCH_ROOT_TASK_TOKEN; -import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; -import static android.view.WindowManager.LayoutParams.TYPE_DOCK_DIVIDER; -import static android.view.WindowManager.TRANSIT_OPEN; -import static android.view.WindowManager.TRANSIT_TO_BACK; -import static android.view.WindowManager.TRANSIT_TO_FRONT; -import static android.view.WindowManager.transitTypeToString; - -import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__APP_DOES_NOT_SUPPORT_MULTIWINDOW; -import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__APP_FINISHED; -import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__DEVICE_FOLDED; -import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__DRAG_DIVIDER; -import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__RETURN_HOME; -import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__SCREEN_LOCKED_SHOW_ON_TOP; -import static com.android.wm.shell.common.split.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.stagesplit.SplitScreen.STAGE_TYPE_MAIN; -import static com.android.wm.shell.stagesplit.SplitScreen.STAGE_TYPE_SIDE; -import static com.android.wm.shell.stagesplit.SplitScreen.STAGE_TYPE_UNDEFINED; -import static com.android.wm.shell.stagesplit.SplitScreen.stageTypeToString; -import static com.android.wm.shell.stagesplit.SplitScreenTransitions.FLAG_IS_DIVIDER_BAR; -import static com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS; -import static com.android.wm.shell.transition.Transitions.TRANSIT_SPLIT_DISMISS_SNAP; -import static com.android.wm.shell.transition.Transitions.TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE; -import static com.android.wm.shell.transition.Transitions.TRANSIT_SPLIT_SCREEN_PAIR_OPEN; -import static com.android.wm.shell.transition.Transitions.isClosingType; -import static com.android.wm.shell.transition.Transitions.isOpeningType; - -import android.annotation.NonNull; -import android.annotation.Nullable; -import android.app.ActivityManager; -import android.app.ActivityOptions; -import android.app.ActivityTaskManager; -import android.app.PendingIntent; -import android.app.WindowConfiguration; -import android.content.Context; -import android.content.Intent; -import android.graphics.Rect; -import android.hardware.devicestate.DeviceStateManager; -import android.os.Bundle; -import android.os.IBinder; -import android.os.RemoteException; -import android.util.Log; -import android.util.Slog; -import android.view.IRemoteAnimationFinishedCallback; -import android.view.IRemoteAnimationRunner; -import android.view.RemoteAnimationAdapter; -import android.view.RemoteAnimationTarget; -import android.view.SurfaceControl; -import android.view.SurfaceSession; -import android.view.WindowManager; -import android.window.DisplayAreaInfo; -import android.window.RemoteTransition; -import android.window.TransitionInfo; -import android.window.TransitionRequestInfo; -import android.window.WindowContainerToken; -import android.window.WindowContainerTransaction; - -import com.android.internal.R; -import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.logging.InstanceId; -import com.android.internal.protolog.common.ProtoLog; -import com.android.wm.shell.RootTaskDisplayAreaOrganizer; -import com.android.wm.shell.ShellTaskOrganizer; -import com.android.wm.shell.common.DisplayImeController; -import com.android.wm.shell.common.DisplayInsetsController; -import com.android.wm.shell.common.SyncTransactionQueue; -import com.android.wm.shell.common.TransactionPool; -import com.android.wm.shell.common.split.SplitLayout; -import com.android.wm.shell.common.split.SplitScreenConstants.SplitPosition; -import com.android.wm.shell.common.split.SplitWindowManager; -import com.android.wm.shell.protolog.ShellProtoLogGroup; -import com.android.wm.shell.transition.Transitions; - -import java.io.PrintWriter; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; - -import javax.inject.Provider; - -/** - * Coordinates the staging (visibility, sizing, ...) of the split-screen {@link MainStage} and - * {@link SideStage} stages. - * Some high-level rules: - * - The {@link StageCoordinator} is only considered active if the {@link SideStage} contains at - * least one child task. - * - The {@link MainStage} should only have children if the coordinator is active. - * - The {@link SplitLayout} divider is only visible if both the {@link MainStage} - * and {@link SideStage} are visible. - * - The {@link MainStage} configuration is fullscreen when the {@link SideStage} isn't visible. - * This rules are mostly implemented in {@link #onStageVisibilityChanged(StageListenerImpl)} and - * {@link #onStageHasChildrenChanged(StageListenerImpl).} - */ -class StageCoordinator implements SplitLayout.SplitLayoutHandler, - RootTaskDisplayAreaOrganizer.RootTaskDisplayAreaListener, Transitions.TransitionHandler { - - private static final String TAG = StageCoordinator.class.getSimpleName(); - - /** internal value for mDismissTop that represents no dismiss */ - private static final int NO_DISMISS = -2; - - private final SurfaceSession mSurfaceSession = new SurfaceSession(); - - private final MainStage mMainStage; - private final StageListenerImpl mMainStageListener = new StageListenerImpl(); - private final StageTaskUnfoldController mMainUnfoldController; - private final SideStage mSideStage; - private final StageListenerImpl mSideStageListener = new StageListenerImpl(); - private final StageTaskUnfoldController mSideUnfoldController; - @SplitPosition - private int mSideStagePosition = SPLIT_POSITION_BOTTOM_OR_RIGHT; - - private final int mDisplayId; - private SplitLayout mSplitLayout; - private boolean mDividerVisible; - private final SyncTransactionQueue mSyncQueue; - private final RootTaskDisplayAreaOrganizer mRootTDAOrganizer; - private final ShellTaskOrganizer mTaskOrganizer; - private DisplayAreaInfo mDisplayAreaInfo; - private final Context mContext; - private final List<SplitScreen.SplitScreenListener> mListeners = new ArrayList<>(); - private final DisplayImeController mDisplayImeController; - private final DisplayInsetsController mDisplayInsetsController; - private final SplitScreenTransitions mSplitTransitions; - private final SplitscreenEventLogger mLogger; - private boolean mExitSplitScreenOnHide; - private boolean mKeyguardOccluded; - - // TODO(b/187041611): remove this flag after totally deprecated legacy split - /** Whether the device is supporting legacy split or not. */ - private boolean mUseLegacySplit; - - @SplitScreen.StageType private int mDismissTop = NO_DISMISS; - - /** The target stage to dismiss to when unlock after folded. */ - @SplitScreen.StageType private int mTopStageAfterFoldDismiss = STAGE_TYPE_UNDEFINED; - - private final Runnable mOnTransitionAnimationComplete = () -> { - // If still playing, let it finish. - if (!isSplitScreenVisible()) { - // Update divider state after animation so that it is still around and positioned - // properly for the animation itself. - setDividerVisibility(false); - mSplitLayout.resetDividerPosition(); - } - mDismissTop = NO_DISMISS; - }; - - private final SplitWindowManager.ParentContainerCallbacks mParentContainerCallbacks = - new SplitWindowManager.ParentContainerCallbacks() { - @Override - public void attachToParentSurface(SurfaceControl.Builder b) { - mRootTDAOrganizer.attachToDisplayArea(mDisplayId, b); - } - - @Override - public void onLeashReady(SurfaceControl leash) { - mSyncQueue.runInSync(t -> applyDividerVisibility(t)); - } - }; - - StageCoordinator(Context context, int displayId, SyncTransactionQueue syncQueue, - RootTaskDisplayAreaOrganizer rootTDAOrganizer, ShellTaskOrganizer taskOrganizer, - DisplayImeController displayImeController, - DisplayInsetsController displayInsetsController, Transitions transitions, - TransactionPool transactionPool, SplitscreenEventLogger logger, - Provider<Optional<StageTaskUnfoldController>> unfoldControllerProvider) { - mContext = context; - mDisplayId = displayId; - mSyncQueue = syncQueue; - mRootTDAOrganizer = rootTDAOrganizer; - mTaskOrganizer = taskOrganizer; - mLogger = logger; - mMainUnfoldController = unfoldControllerProvider.get().orElse(null); - mSideUnfoldController = unfoldControllerProvider.get().orElse(null); - - mMainStage = new MainStage( - mTaskOrganizer, - mDisplayId, - mMainStageListener, - mSyncQueue, - mSurfaceSession, - mMainUnfoldController); - mSideStage = new SideStage( - mContext, - mTaskOrganizer, - mDisplayId, - mSideStageListener, - mSyncQueue, - mSurfaceSession, - mSideUnfoldController); - mDisplayImeController = displayImeController; - mDisplayInsetsController = displayInsetsController; - mDisplayInsetsController.addInsetsChangedListener(mDisplayId, mSideStage); - mRootTDAOrganizer.registerListener(displayId, this); - final DeviceStateManager deviceStateManager = - mContext.getSystemService(DeviceStateManager.class); - deviceStateManager.registerCallback(taskOrganizer.getExecutor(), - new DeviceStateManager.FoldStateListener(mContext, this::onFoldedStateChanged)); - mSplitTransitions = new SplitScreenTransitions(transactionPool, transitions, - mOnTransitionAnimationComplete); - transitions.addHandler(this); - } - - @VisibleForTesting - StageCoordinator(Context context, int displayId, SyncTransactionQueue syncQueue, - RootTaskDisplayAreaOrganizer rootTDAOrganizer, ShellTaskOrganizer taskOrganizer, - MainStage mainStage, SideStage sideStage, DisplayImeController displayImeController, - DisplayInsetsController displayInsetsController, SplitLayout splitLayout, - Transitions transitions, TransactionPool transactionPool, - SplitscreenEventLogger logger, - Provider<Optional<StageTaskUnfoldController>> unfoldControllerProvider) { - mContext = context; - mDisplayId = displayId; - mSyncQueue = syncQueue; - mRootTDAOrganizer = rootTDAOrganizer; - mTaskOrganizer = taskOrganizer; - mMainStage = mainStage; - mSideStage = sideStage; - mDisplayImeController = displayImeController; - mDisplayInsetsController = displayInsetsController; - mRootTDAOrganizer.registerListener(displayId, this); - mSplitLayout = splitLayout; - mSplitTransitions = new SplitScreenTransitions(transactionPool, transitions, - mOnTransitionAnimationComplete); - mMainUnfoldController = unfoldControllerProvider.get().orElse(null); - mSideUnfoldController = unfoldControllerProvider.get().orElse(null); - mLogger = logger; - transitions.addHandler(this); - } - - @VisibleForTesting - SplitScreenTransitions getSplitTransitions() { - return mSplitTransitions; - } - - boolean isSplitScreenVisible() { - return mSideStageListener.mVisible && mMainStageListener.mVisible; - } - - boolean moveToSideStage(ActivityManager.RunningTaskInfo task, - @SplitPosition int sideStagePosition) { - final WindowContainerTransaction wct = new WindowContainerTransaction(); - setSideStagePosition(sideStagePosition, wct); - mMainStage.activate(getMainStageBounds(), wct); - mSideStage.addTask(task, getSideStageBounds(), wct); - mSyncQueue.queue(wct); - mSyncQueue.runInSync( - t -> updateSurfaceBounds(null /* layout */, t, false /* applyResizingOffset */)); - return true; - } - - boolean removeFromSideStage(int taskId) { - final WindowContainerTransaction wct = new WindowContainerTransaction(); - - /** - * {@link MainStage} will be deactivated in {@link #onStageHasChildrenChanged} if the - * {@link SideStage} no longer has children. - */ - final boolean result = mSideStage.removeTask(taskId, - mMainStage.isActive() ? mMainStage.mRootTaskInfo.token : null, - wct); - mTaskOrganizer.applyTransaction(wct); - return result; - } - - void setSideStageOutline(boolean enable) { - mSideStage.enableOutline(enable); - } - - /** Starts 2 tasks in one transition. */ - void startTasks(int mainTaskId, @Nullable Bundle mainOptions, int sideTaskId, - @Nullable Bundle sideOptions, @SplitPosition int sidePosition, - @Nullable RemoteTransition remoteTransition) { - final WindowContainerTransaction wct = new WindowContainerTransaction(); - mainOptions = mainOptions != null ? mainOptions : new Bundle(); - sideOptions = sideOptions != null ? sideOptions : new Bundle(); - setSideStagePosition(sidePosition, wct); - - // Build a request WCT that will launch both apps such that task 0 is on the main stage - // while task 1 is on the side stage. - mMainStage.activate(getMainStageBounds(), wct); - mSideStage.setBounds(getSideStageBounds(), wct); - - // Make sure the launch options will put tasks in the corresponding split roots - addActivityOptions(mainOptions, mMainStage); - addActivityOptions(sideOptions, mSideStage); - - // Add task launch requests - wct.startTask(mainTaskId, mainOptions); - wct.startTask(sideTaskId, sideOptions); - - mSplitTransitions.startEnterTransition( - TRANSIT_SPLIT_SCREEN_PAIR_OPEN, wct, remoteTransition, this); - } - - /** Starts 2 tasks in one legacy transition. */ - void startTasksWithLegacyTransition(int mainTaskId, @Nullable Bundle mainOptions, - int sideTaskId, @Nullable Bundle sideOptions, @SplitPosition int sidePosition, - RemoteAnimationAdapter adapter) { - final WindowContainerTransaction wct = new WindowContainerTransaction(); - // Need to add another wrapper here in shell so that we can inject the divider bar - // and also manage the process elevation via setRunningRemote - IRemoteAnimationRunner wrapper = new IRemoteAnimationRunner.Stub() { - @Override - public void onAnimationStart(@WindowManager.TransitionOldType int transit, - RemoteAnimationTarget[] apps, - RemoteAnimationTarget[] wallpapers, - RemoteAnimationTarget[] nonApps, - final IRemoteAnimationFinishedCallback finishedCallback) { - RemoteAnimationTarget[] augmentedNonApps = - new RemoteAnimationTarget[nonApps.length + 1]; - for (int i = 0; i < nonApps.length; ++i) { - augmentedNonApps[i] = nonApps[i]; - } - augmentedNonApps[augmentedNonApps.length - 1] = getDividerBarLegacyTarget(); - try { - ActivityTaskManager.getService().setRunningRemoteTransitionDelegate( - adapter.getCallingApplication()); - adapter.getRunner().onAnimationStart(transit, apps, wallpapers, nonApps, - finishedCallback); - } catch (RemoteException e) { - Slog.e(TAG, "Error starting remote animation", e); - } - } - - @Override - public void onAnimationCancelled(boolean isKeyguardOccluded) { - try { - adapter.getRunner().onAnimationCancelled(isKeyguardOccluded); - } catch (RemoteException e) { - Slog.e(TAG, "Error starting remote animation", e); - } - } - }; - RemoteAnimationAdapter wrappedAdapter = new RemoteAnimationAdapter( - wrapper, adapter.getDuration(), adapter.getStatusBarTransitionDelay()); - - if (mainOptions == null) { - mainOptions = ActivityOptions.makeRemoteAnimation(wrappedAdapter).toBundle(); - } else { - ActivityOptions mainActivityOptions = ActivityOptions.fromBundle(mainOptions); - mainActivityOptions.update(ActivityOptions.makeRemoteAnimation(wrappedAdapter)); - } - - sideOptions = sideOptions != null ? sideOptions : new Bundle(); - setSideStagePosition(sidePosition, wct); - - // Build a request WCT that will launch both apps such that task 0 is on the main stage - // while task 1 is on the side stage. - mMainStage.activate(getMainStageBounds(), wct); - mSideStage.setBounds(getSideStageBounds(), wct); - - // Make sure the launch options will put tasks in the corresponding split roots - addActivityOptions(mainOptions, mMainStage); - addActivityOptions(sideOptions, mSideStage); - - // Add task launch requests - wct.startTask(mainTaskId, mainOptions); - wct.startTask(sideTaskId, sideOptions); - - // Using legacy transitions, so we can't use blast sync since it conflicts. - mTaskOrganizer.applyTransaction(wct); - } - - public void startIntent(PendingIntent intent, Intent fillInIntent, - @SplitScreen.StageType int stage, @SplitPosition int position, - @androidx.annotation.Nullable Bundle options, - @Nullable RemoteTransition remoteTransition) { - final WindowContainerTransaction wct = new WindowContainerTransaction(); - options = resolveStartStage(stage, position, options, wct); - wct.sendPendingIntent(intent, fillInIntent, options); - mSplitTransitions.startEnterTransition( - TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE, wct, remoteTransition, this); - } - - Bundle resolveStartStage(@SplitScreen.StageType int stage, - @SplitPosition int position, @androidx.annotation.Nullable Bundle options, - @androidx.annotation.Nullable WindowContainerTransaction wct) { - switch (stage) { - case STAGE_TYPE_UNDEFINED: { - // Use the stage of the specified position is valid. - if (position != SPLIT_POSITION_UNDEFINED) { - if (position == getSideStagePosition()) { - options = resolveStartStage(STAGE_TYPE_SIDE, position, options, wct); - } else { - options = resolveStartStage(STAGE_TYPE_MAIN, position, options, wct); - } - } else { - // Exit split-screen and launch fullscreen since stage wasn't specified. - prepareExitSplitScreen(STAGE_TYPE_UNDEFINED, wct); - } - break; - } - case STAGE_TYPE_SIDE: { - if (position != SPLIT_POSITION_UNDEFINED) { - setSideStagePosition(position, wct); - } else { - position = getSideStagePosition(); - } - if (options == null) { - options = new Bundle(); - } - updateActivityOptions(options, position); - break; - } - case STAGE_TYPE_MAIN: { - if (position != SPLIT_POSITION_UNDEFINED) { - // Set the side stage opposite of what we want to the main stage. - final int sideStagePosition = position == SPLIT_POSITION_TOP_OR_LEFT - ? SPLIT_POSITION_BOTTOM_OR_RIGHT : SPLIT_POSITION_TOP_OR_LEFT; - setSideStagePosition(sideStagePosition, wct); - } else { - position = getMainStagePosition(); - } - if (options == null) { - options = new Bundle(); - } - updateActivityOptions(options, position); - break; - } - default: - throw new IllegalArgumentException("Unknown stage=" + stage); - } - - return options; - } - - @SplitPosition - int getSideStagePosition() { - return mSideStagePosition; - } - - @SplitPosition - int getMainStagePosition() { - return mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT - ? SPLIT_POSITION_BOTTOM_OR_RIGHT : SPLIT_POSITION_TOP_OR_LEFT; - } - - void setSideStagePosition(@SplitPosition int sideStagePosition, - @Nullable WindowContainerTransaction wct) { - setSideStagePosition(sideStagePosition, true /* updateBounds */, wct); - } - - private void setSideStagePosition(@SplitPosition int sideStagePosition, boolean updateBounds, - @Nullable WindowContainerTransaction wct) { - if (mSideStagePosition == sideStagePosition) return; - mSideStagePosition = sideStagePosition; - sendOnStagePositionChanged(); - - if (mSideStageListener.mVisible && updateBounds) { - if (wct == null) { - // onLayoutSizeChanged builds/applies a wct with the contents of updateWindowBounds. - onLayoutSizeChanged(mSplitLayout); - } else { - updateWindowBounds(mSplitLayout, wct); - updateUnfoldBounds(); - } - } - } - - void setSideStageVisibility(boolean visible) { - if (mSideStageListener.mVisible == visible) return; - - final WindowContainerTransaction wct = new WindowContainerTransaction(); - mSideStage.setVisibility(visible, wct); - mTaskOrganizer.applyTransaction(wct); - } - - void onKeyguardOccludedChanged(boolean occluded) { - // Do not exit split directly, because it needs to wait for task info update to determine - // which task should remain on top after split dismissed. - mKeyguardOccluded = occluded; - } - - void onKeyguardVisibilityChanged(boolean showing) { - if (!showing && mMainStage.isActive() - && mTopStageAfterFoldDismiss != STAGE_TYPE_UNDEFINED) { - exitSplitScreen(mTopStageAfterFoldDismiss == STAGE_TYPE_MAIN ? mMainStage : mSideStage, - SPLITSCREEN_UICHANGED__EXIT_REASON__DEVICE_FOLDED); - } - } - - void exitSplitScreenOnHide(boolean exitSplitScreenOnHide) { - mExitSplitScreenOnHide = exitSplitScreenOnHide; - } - - void exitSplitScreen(int toTopTaskId, int exitReason) { - StageTaskListener childrenToTop = null; - if (mMainStage.containsTask(toTopTaskId)) { - childrenToTop = mMainStage; - } else if (mSideStage.containsTask(toTopTaskId)) { - childrenToTop = mSideStage; - } - - final WindowContainerTransaction wct = new WindowContainerTransaction(); - if (childrenToTop != null) { - childrenToTop.reorderChild(toTopTaskId, true /* onTop */, wct); - } - applyExitSplitScreen(childrenToTop, wct, exitReason); - } - - private void exitSplitScreen(StageTaskListener childrenToTop, int exitReason) { - final WindowContainerTransaction wct = new WindowContainerTransaction(); - applyExitSplitScreen(childrenToTop, wct, exitReason); - } - - private void applyExitSplitScreen( - StageTaskListener childrenToTop, - WindowContainerTransaction wct, int exitReason) { - mSideStage.removeAllTasks(wct, childrenToTop == mSideStage); - mMainStage.deactivate(wct, childrenToTop == mMainStage); - mTaskOrganizer.applyTransaction(wct); - mSyncQueue.runInSync(t -> t - .setWindowCrop(mMainStage.mRootLeash, null) - .setWindowCrop(mSideStage.mRootLeash, null)); - // Hide divider and reset its position. - setDividerVisibility(false); - mSplitLayout.resetDividerPosition(); - mTopStageAfterFoldDismiss = STAGE_TYPE_UNDEFINED; - if (childrenToTop != null) { - logExitToStage(exitReason, childrenToTop == mMainStage); - } else { - logExit(exitReason); - } - } - - /** - * Unlike exitSplitScreen, this takes a stagetype vs an actual stage-reference and populates - * an existing WindowContainerTransaction (rather than applying immediately). This is intended - * to be used when exiting split might be bundled with other window operations. - */ - void prepareExitSplitScreen(@SplitScreen.StageType int stageToTop, - @NonNull WindowContainerTransaction wct) { - mSideStage.removeAllTasks(wct, stageToTop == STAGE_TYPE_SIDE); - mMainStage.deactivate(wct, stageToTop == STAGE_TYPE_MAIN); - } - - void getStageBounds(Rect outTopOrLeftBounds, Rect outBottomOrRightBounds) { - outTopOrLeftBounds.set(mSplitLayout.getBounds1()); - outBottomOrRightBounds.set(mSplitLayout.getBounds2()); - } - - private void addActivityOptions(Bundle opts, StageTaskListener stage) { - opts.putParcelable(KEY_LAUNCH_ROOT_TASK_TOKEN, stage.mRootTaskInfo.token); - } - - void updateActivityOptions(Bundle opts, @SplitPosition int position) { - addActivityOptions(opts, position == mSideStagePosition ? mSideStage : mMainStage); - } - - void registerSplitScreenListener(SplitScreen.SplitScreenListener listener) { - if (mListeners.contains(listener)) return; - mListeners.add(listener); - sendStatusToListener(listener); - } - - void unregisterSplitScreenListener(SplitScreen.SplitScreenListener listener) { - mListeners.remove(listener); - } - - void sendStatusToListener(SplitScreen.SplitScreenListener listener) { - listener.onStagePositionChanged(STAGE_TYPE_MAIN, getMainStagePosition()); - listener.onStagePositionChanged(STAGE_TYPE_SIDE, getSideStagePosition()); - listener.onSplitVisibilityChanged(isSplitScreenVisible()); - mSideStage.onSplitScreenListenerRegistered(listener, STAGE_TYPE_SIDE); - mMainStage.onSplitScreenListenerRegistered(listener, STAGE_TYPE_MAIN); - } - - private void sendOnStagePositionChanged() { - for (int i = mListeners.size() - 1; i >= 0; --i) { - final SplitScreen.SplitScreenListener l = mListeners.get(i); - l.onStagePositionChanged(STAGE_TYPE_MAIN, getMainStagePosition()); - l.onStagePositionChanged(STAGE_TYPE_SIDE, getSideStagePosition()); - } - } - - private void onStageChildTaskStatusChanged(StageListenerImpl stageListener, int taskId, - boolean present, boolean visible) { - int stage; - if (present) { - stage = stageListener == mSideStageListener ? STAGE_TYPE_SIDE : STAGE_TYPE_MAIN; - } else { - // No longer on any stage - stage = STAGE_TYPE_UNDEFINED; - } - if (stage == STAGE_TYPE_MAIN) { - mLogger.logMainStageAppChange(getMainStagePosition(), mMainStage.getTopChildTaskUid(), - mSplitLayout.isLandscape()); - } else { - mLogger.logSideStageAppChange(getSideStagePosition(), mSideStage.getTopChildTaskUid(), - mSplitLayout.isLandscape()); - } - - for (int i = mListeners.size() - 1; i >= 0; --i) { - mListeners.get(i).onTaskStageChanged(taskId, stage, visible); - } - } - - private void sendSplitVisibilityChanged() { - for (int i = mListeners.size() - 1; i >= 0; --i) { - final SplitScreen.SplitScreenListener l = mListeners.get(i); - l.onSplitVisibilityChanged(mDividerVisible); - } - - if (mMainUnfoldController != null && mSideUnfoldController != null) { - mMainUnfoldController.onSplitVisibilityChanged(mDividerVisible); - mSideUnfoldController.onSplitVisibilityChanged(mDividerVisible); - } - } - - private void onStageRootTaskAppeared(StageListenerImpl stageListener) { - if (mMainStageListener.mHasRootTask && mSideStageListener.mHasRootTask) { - mUseLegacySplit = mContext.getResources().getBoolean(R.bool.config_useLegacySplit); - final WindowContainerTransaction wct = new WindowContainerTransaction(); - // Make the stages adjacent to each other so they occlude what's behind them. - wct.setAdjacentRoots(mMainStage.mRootTaskInfo.token, mSideStage.mRootTaskInfo.token, - true /* moveTogether */); - - // Only sets side stage as launch-adjacent-flag-root when the device is not using legacy - // split to prevent new split behavior confusing users. - if (!mUseLegacySplit) { - wct.setLaunchAdjacentFlagRoot(mSideStage.mRootTaskInfo.token); - } - - mTaskOrganizer.applyTransaction(wct); - } - } - - private void onStageRootTaskVanished(StageListenerImpl stageListener) { - if (stageListener == mMainStageListener || stageListener == mSideStageListener) { - final WindowContainerTransaction wct = new WindowContainerTransaction(); - // Deactivate the main stage if it no longer has a root task. - mMainStage.deactivate(wct); - - if (!mUseLegacySplit) { - wct.clearLaunchAdjacentFlagRoot(mSideStage.mRootTaskInfo.token); - } - - mTaskOrganizer.applyTransaction(wct); - } - } - - private void setDividerVisibility(boolean visible) { - if (mDividerVisible == visible) return; - mDividerVisible = visible; - if (visible) { - mSplitLayout.init(); - updateUnfoldBounds(); - } else { - mSplitLayout.release(); - } - sendSplitVisibilityChanged(); - } - - private void onStageVisibilityChanged(StageListenerImpl stageListener) { - final boolean sideStageVisible = mSideStageListener.mVisible; - final boolean mainStageVisible = mMainStageListener.mVisible; - final boolean bothStageVisible = sideStageVisible && mainStageVisible; - final boolean bothStageInvisible = !sideStageVisible && !mainStageVisible; - final boolean sameVisibility = sideStageVisible == mainStageVisible; - // Only add or remove divider when both visible or both invisible to avoid sometimes we only - // got one stage visibility changed for a moment and it will cause flicker. - if (sameVisibility) { - setDividerVisibility(bothStageVisible); - } - - if (bothStageInvisible) { - if (mExitSplitScreenOnHide - // Don't dismiss staged split when both stages are not visible due to sleeping display, - // like the cases keyguard showing or screen off. - || (!mMainStage.mRootTaskInfo.isSleeping && !mSideStage.mRootTaskInfo.isSleeping)) { - exitSplitScreen(null /* childrenToTop */, - SPLITSCREEN_UICHANGED__EXIT_REASON__RETURN_HOME); - } - } else if (mKeyguardOccluded) { - // At least one of the stages is visible while keyguard occluded. Dismiss split because - // there's show-when-locked activity showing on top of keyguard. Also make sure the - // task contains show-when-locked activity remains on top after split dismissed. - final StageTaskListener toTop = - mainStageVisible ? mMainStage : (sideStageVisible ? mSideStage : null); - exitSplitScreen(toTop, SPLITSCREEN_UICHANGED__EXIT_REASON__SCREEN_LOCKED_SHOW_ON_TOP); - } - - mSyncQueue.runInSync(t -> { - // Same above, we only set root tasks and divider leash visibility when both stage - // change to visible or invisible to avoid flicker. - if (sameVisibility) { - t.setVisibility(mSideStage.mRootLeash, bothStageVisible) - .setVisibility(mMainStage.mRootLeash, bothStageVisible); - applyDividerVisibility(t); - applyOutlineVisibility(t); - } - }); - } - - private void applyDividerVisibility(SurfaceControl.Transaction t) { - final SurfaceControl dividerLeash = mSplitLayout.getDividerLeash(); - if (dividerLeash == null) { - return; - } - - if (mDividerVisible) { - t.show(dividerLeash) - .setLayer(dividerLeash, Integer.MAX_VALUE) - .setPosition(dividerLeash, - mSplitLayout.getDividerBounds().left, - mSplitLayout.getDividerBounds().top); - } else { - t.hide(dividerLeash); - } - } - - private void applyOutlineVisibility(SurfaceControl.Transaction t) { - final SurfaceControl outlineLeash = mSideStage.getOutlineLeash(); - if (outlineLeash == null) { - return; - } - - if (mDividerVisible) { - t.show(outlineLeash).setLayer(outlineLeash, Integer.MAX_VALUE); - } else { - t.hide(outlineLeash); - } - } - - private void onStageHasChildrenChanged(StageListenerImpl stageListener) { - final boolean hasChildren = stageListener.mHasChildren; - final boolean isSideStage = stageListener == mSideStageListener; - if (!hasChildren) { - if (isSideStage && mMainStageListener.mVisible) { - // Exit to main stage if side stage no longer has children. - exitSplitScreen(mMainStage, SPLITSCREEN_UICHANGED__EXIT_REASON__APP_FINISHED); - } else if (!isSideStage && mSideStageListener.mVisible) { - // Exit to side stage if main stage no longer has children. - exitSplitScreen(mSideStage, SPLITSCREEN_UICHANGED__EXIT_REASON__APP_FINISHED); - } - } else if (isSideStage) { - final WindowContainerTransaction wct = new WindowContainerTransaction(); - // Make sure the main stage is active. - mMainStage.activate(getMainStageBounds(), wct); - mSideStage.setBounds(getSideStageBounds(), wct); - mTaskOrganizer.applyTransaction(wct); - } - if (!mLogger.hasStartedSession() && mMainStageListener.mHasChildren - && mSideStageListener.mHasChildren) { - mLogger.logEnter(mSplitLayout.getDividerPositionAsFraction(), - getMainStagePosition(), mMainStage.getTopChildTaskUid(), - getSideStagePosition(), mSideStage.getTopChildTaskUid(), - mSplitLayout.isLandscape()); - } - } - - @VisibleForTesting - IBinder onSnappedToDismissTransition(boolean mainStageToTop) { - final WindowContainerTransaction wct = new WindowContainerTransaction(); - prepareExitSplitScreen(mainStageToTop ? STAGE_TYPE_MAIN : STAGE_TYPE_SIDE, wct); - return mSplitTransitions.startSnapToDismiss(wct, this); - } - - @Override - public void onSnappedToDismiss(boolean bottomOrRight) { - final boolean mainStageToTop = - bottomOrRight ? mSideStagePosition == SPLIT_POSITION_BOTTOM_OR_RIGHT - : mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT; - if (ENABLE_SHELL_TRANSITIONS) { - onSnappedToDismissTransition(mainStageToTop); - return; - } - exitSplitScreen(mainStageToTop ? mMainStage : mSideStage, - SPLITSCREEN_UICHANGED__EXIT_REASON__DRAG_DIVIDER); - } - - @Override - public void onDoubleTappedDivider() { - setSideStagePosition(mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT - ? SPLIT_POSITION_BOTTOM_OR_RIGHT : SPLIT_POSITION_TOP_OR_LEFT, null /* wct */); - mLogger.logSwap(getMainStagePosition(), mMainStage.getTopChildTaskUid(), - getSideStagePosition(), mSideStage.getTopChildTaskUid(), - mSplitLayout.isLandscape()); - } - - @Override - public void onLayoutPositionChanging(SplitLayout layout) { - mSyncQueue.runInSync(t -> updateSurfaceBounds(layout, t, true /* applyResizingOffset */)); - } - - @Override - public void onLayoutSizeChanging(SplitLayout layout) { - mSyncQueue.runInSync(t -> updateSurfaceBounds(layout, t, true /* applyResizingOffset */)); - mSideStage.setOutlineVisibility(false); - } - - @Override - public void onLayoutSizeChanged(SplitLayout layout) { - final WindowContainerTransaction wct = new WindowContainerTransaction(); - updateWindowBounds(layout, wct); - updateUnfoldBounds(); - mSyncQueue.queue(wct); - mSyncQueue.runInSync(t -> updateSurfaceBounds(layout, t, false /* applyResizingOffset */)); - mSideStage.setOutlineVisibility(true); - mLogger.logResize(mSplitLayout.getDividerPositionAsFraction()); - } - - private void updateUnfoldBounds() { - if (mMainUnfoldController != null && mSideUnfoldController != null) { - mMainUnfoldController.onLayoutChanged(getMainStageBounds()); - mSideUnfoldController.onLayoutChanged(getSideStageBounds()); - } - } - - /** - * Populates `wct` with operations that match the split windows to the current layout. - * To match relevant surfaces, make sure to call updateSurfaceBounds after `wct` is applied - */ - private void updateWindowBounds(SplitLayout layout, WindowContainerTransaction wct) { - final StageTaskListener topLeftStage = - mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mSideStage : mMainStage; - final StageTaskListener bottomRightStage = - mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mMainStage : mSideStage; - layout.applyTaskChanges(wct, topLeftStage.mRootTaskInfo, bottomRightStage.mRootTaskInfo); - } - - void updateSurfaceBounds(@Nullable SplitLayout layout, @NonNull SurfaceControl.Transaction t, - boolean applyResizingOffset) { - final StageTaskListener topLeftStage = - mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mSideStage : mMainStage; - final StageTaskListener bottomRightStage = - mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mMainStage : mSideStage; - (layout != null ? layout : mSplitLayout).applySurfaceChanges(t, topLeftStage.mRootLeash, - bottomRightStage.mRootLeash, topLeftStage.mDimLayer, bottomRightStage.mDimLayer, - applyResizingOffset); - } - - @Override - public int getSplitItemPosition(WindowContainerToken token) { - if (token == null) { - return SPLIT_POSITION_UNDEFINED; - } - - if (token.equals(mMainStage.mRootTaskInfo.getToken())) { - return getMainStagePosition(); - } else if (token.equals(mSideStage.mRootTaskInfo.getToken())) { - return getSideStagePosition(); - } - - return SPLIT_POSITION_UNDEFINED; - } - - @Override - public void setLayoutOffsetTarget(int offsetX, int offsetY, SplitLayout layout) { - final StageTaskListener topLeftStage = - mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mSideStage : mMainStage; - final StageTaskListener bottomRightStage = - mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mMainStage : mSideStage; - final WindowContainerTransaction wct = new WindowContainerTransaction(); - layout.applyLayoutOffsetTarget(wct, offsetX, offsetY, topLeftStage.mRootTaskInfo, - bottomRightStage.mRootTaskInfo); - mTaskOrganizer.applyTransaction(wct); - } - - @Override - public void onDisplayAreaAppeared(DisplayAreaInfo displayAreaInfo) { - mDisplayAreaInfo = displayAreaInfo; - if (mSplitLayout == null) { - mSplitLayout = new SplitLayout(TAG + "SplitDivider", mContext, - mDisplayAreaInfo.configuration, this, mParentContainerCallbacks, - mDisplayImeController, mTaskOrganizer, SplitLayout.PARALLAX_DISMISSING); - mDisplayInsetsController.addInsetsChangedListener(mDisplayId, mSplitLayout); - - if (mMainUnfoldController != null && mSideUnfoldController != null) { - mMainUnfoldController.init(); - mSideUnfoldController.init(); - } - } - } - - @Override - public void onDisplayAreaVanished(DisplayAreaInfo displayAreaInfo) { - throw new IllegalStateException("Well that was unexpected..."); - } - - @Override - public void onDisplayAreaInfoChanged(DisplayAreaInfo displayAreaInfo) { - mDisplayAreaInfo = displayAreaInfo; - if (mSplitLayout != null - && mSplitLayout.updateConfiguration(mDisplayAreaInfo.configuration) - && mMainStage.isActive()) { - onLayoutSizeChanged(mSplitLayout); - } - } - - private void onFoldedStateChanged(boolean folded) { - mTopStageAfterFoldDismiss = STAGE_TYPE_UNDEFINED; - if (!folded) return; - - if (mMainStage.isFocused()) { - mTopStageAfterFoldDismiss = STAGE_TYPE_MAIN; - } else if (mSideStage.isFocused()) { - mTopStageAfterFoldDismiss = STAGE_TYPE_SIDE; - } - } - - private Rect getSideStageBounds() { - return mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT - ? mSplitLayout.getBounds1() : mSplitLayout.getBounds2(); - } - - private Rect getMainStageBounds() { - return mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT - ? mSplitLayout.getBounds2() : mSplitLayout.getBounds1(); - } - - /** - * Get the stage that should contain this `taskInfo`. The stage doesn't necessarily contain - * this task (yet) so this can also be used to identify which stage to put a task into. - */ - private StageTaskListener getStageOfTask(ActivityManager.RunningTaskInfo taskInfo) { - // TODO(b/184679596): Find a way to either include task-org information in the transition, - // or synchronize task-org callbacks so we can use stage.containsTask - if (mMainStage.mRootTaskInfo != null - && taskInfo.parentTaskId == mMainStage.mRootTaskInfo.taskId) { - return mMainStage; - } else if (mSideStage.mRootTaskInfo != null - && taskInfo.parentTaskId == mSideStage.mRootTaskInfo.taskId) { - return mSideStage; - } - return null; - } - - @SplitScreen.StageType - private int getStageType(StageTaskListener stage) { - return stage == mMainStage ? STAGE_TYPE_MAIN : STAGE_TYPE_SIDE; - } - - @Override - public WindowContainerTransaction handleRequest(@NonNull IBinder transition, - @Nullable TransitionRequestInfo request) { - final ActivityManager.RunningTaskInfo triggerTask = request.getTriggerTask(); - if (triggerTask == null) { - // still want to monitor everything while in split-screen, so return non-null. - return isSplitScreenVisible() ? new WindowContainerTransaction() : null; - } - - WindowContainerTransaction out = null; - final @WindowManager.TransitionType int type = request.getType(); - if (isSplitScreenVisible()) { - // try to handle everything while in split-screen, so return a WCT even if it's empty. - ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " split is active so using split" - + "Transition to handle request. triggerTask=%d type=%s mainChildren=%d" - + " sideChildren=%d", triggerTask.taskId, transitTypeToString(type), - mMainStage.getChildCount(), mSideStage.getChildCount()); - out = new WindowContainerTransaction(); - final StageTaskListener stage = getStageOfTask(triggerTask); - if (stage != null) { - // dismiss split if the last task in one of the stages is going away - if (isClosingType(type) && stage.getChildCount() == 1) { - // The top should be the opposite side that is closing: - mDismissTop = getStageType(stage) == STAGE_TYPE_MAIN - ? STAGE_TYPE_SIDE : STAGE_TYPE_MAIN; - } - } else { - if (triggerTask.getActivityType() == ACTIVITY_TYPE_HOME && isOpeningType(type)) { - // Going home so dismiss both. - mDismissTop = STAGE_TYPE_UNDEFINED; - } - } - if (mDismissTop != NO_DISMISS) { - ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " splitTransition " - + " deduced Dismiss from request. toTop=%s", - stageTypeToString(mDismissTop)); - prepareExitSplitScreen(mDismissTop, out); - mSplitTransitions.mPendingDismiss = transition; - } - } else { - // Not in split mode, so look for an open into a split stage just so we can whine and - // complain about how this isn't a supported operation. - if ((type == TRANSIT_OPEN || type == TRANSIT_TO_FRONT)) { - if (getStageOfTask(triggerTask) != null) { - throw new IllegalStateException("Entering split implicitly with only one task" - + " isn't supported."); - } - } - } - return out; - } - - @Override - public boolean startAnimation(@NonNull IBinder transition, - @NonNull TransitionInfo info, - @NonNull SurfaceControl.Transaction startTransaction, - @NonNull SurfaceControl.Transaction finishTransaction, - @NonNull Transitions.TransitionFinishCallback finishCallback) { - if (transition != mSplitTransitions.mPendingDismiss - && transition != mSplitTransitions.mPendingEnter) { - // Not entering or exiting, so just do some house-keeping and validation. - - // If we're not in split-mode, just abort so something else can handle it. - if (!isSplitScreenVisible()) return false; - - for (int iC = 0; iC < info.getChanges().size(); ++iC) { - final TransitionInfo.Change change = info.getChanges().get(iC); - final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo(); - if (taskInfo == null || !taskInfo.hasParentTask()) continue; - final StageTaskListener stage = getStageOfTask(taskInfo); - if (stage == null) continue; - if (isOpeningType(change.getMode())) { - if (!stage.containsTask(taskInfo.taskId)) { - Log.w(TAG, "Expected onTaskAppeared on " + stage + " to have been called" - + " with " + taskInfo.taskId + " before startAnimation()."); - } - } else if (isClosingType(change.getMode())) { - if (stage.containsTask(taskInfo.taskId)) { - Log.w(TAG, "Expected onTaskVanished on " + stage + " to have been called" - + " with " + taskInfo.taskId + " before startAnimation()."); - } - } - } - if (mMainStage.getChildCount() == 0 || mSideStage.getChildCount() == 0) { - // TODO(shell-transitions): Implement a fallback behavior for now. - throw new IllegalStateException("Somehow removed the last task in a stage" - + " outside of a proper transition"); - // This can happen in some pathological cases. For example: - // 1. main has 2 tasks [Task A (Single-task), Task B], side has one task [Task C] - // 2. Task B closes itself and starts Task A in LAUNCH_ADJACENT at the same time - // In this case, the result *should* be that we leave split. - // TODO(b/184679596): Find a way to either include task-org information in - // the transition, or synchronize task-org callbacks. - } - - // Use normal animations. - return false; - } - - boolean shouldAnimate = true; - if (mSplitTransitions.mPendingEnter == transition) { - shouldAnimate = startPendingEnterAnimation(transition, info, startTransaction); - } else if (mSplitTransitions.mPendingDismiss == transition) { - shouldAnimate = startPendingDismissAnimation(transition, info, startTransaction); - } - if (!shouldAnimate) return false; - - mSplitTransitions.playAnimation(transition, info, startTransaction, finishTransaction, - finishCallback, mMainStage.mRootTaskInfo.token, mSideStage.mRootTaskInfo.token); - return true; - } - - private boolean startPendingEnterAnimation(@NonNull IBinder transition, - @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction t) { - if (info.getType() == TRANSIT_SPLIT_SCREEN_PAIR_OPEN) { - // First, verify that we actually have opened 2 apps in split. - TransitionInfo.Change mainChild = null; - TransitionInfo.Change sideChild = null; - for (int iC = 0; iC < info.getChanges().size(); ++iC) { - final TransitionInfo.Change change = info.getChanges().get(iC); - final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo(); - if (taskInfo == null || !taskInfo.hasParentTask()) continue; - final @SplitScreen.StageType int stageType = getStageType(getStageOfTask(taskInfo)); - if (stageType == STAGE_TYPE_MAIN) { - mainChild = change; - } else if (stageType == STAGE_TYPE_SIDE) { - sideChild = change; - } - } - if (mainChild == null || sideChild == null) { - throw new IllegalStateException("Launched 2 tasks in split, but didn't receive" - + " 2 tasks in transition. Possibly one of them failed to launch"); - // TODO: fallback logic. Probably start a new transition to exit split before - // applying anything here. Ideally consolidate with transition-merging. - } - - // Update local states (before animating). - setDividerVisibility(true); - setSideStagePosition(SPLIT_POSITION_BOTTOM_OR_RIGHT, false /* updateBounds */, - null /* wct */); - setSplitsVisible(true); - - addDividerBarToTransition(info, t, true /* show */); - - // Make some noise if things aren't totally expected. These states shouldn't effect - // transitions locally, but remotes (like Launcher) may get confused if they were - // depending on listener callbacks. This can happen because task-organizer callbacks - // aren't serialized with transition callbacks. - // TODO(b/184679596): Find a way to either include task-org information in - // the transition, or synchronize task-org callbacks. - if (!mMainStage.containsTask(mainChild.getTaskInfo().taskId)) { - Log.w(TAG, "Expected onTaskAppeared on " + mMainStage - + " to have been called with " + mainChild.getTaskInfo().taskId - + " before startAnimation()."); - } - if (!mSideStage.containsTask(sideChild.getTaskInfo().taskId)) { - Log.w(TAG, "Expected onTaskAppeared on " + mSideStage - + " to have been called with " + sideChild.getTaskInfo().taskId - + " before startAnimation()."); - } - return true; - } else { - // TODO: other entry method animations - throw new RuntimeException("Unsupported split-entry"); - } - } - - private boolean startPendingDismissAnimation(@NonNull IBinder transition, - @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction t) { - // Make some noise if things aren't totally expected. These states shouldn't effect - // transitions locally, but remotes (like Launcher) may get confused if they were - // depending on listener callbacks. This can happen because task-organizer callbacks - // aren't serialized with transition callbacks. - // TODO(b/184679596): Find a way to either include task-org information in - // the transition, or synchronize task-org callbacks. - if (mMainStage.getChildCount() != 0) { - final StringBuilder tasksLeft = new StringBuilder(); - for (int i = 0; i < mMainStage.getChildCount(); ++i) { - tasksLeft.append(i != 0 ? ", " : ""); - tasksLeft.append(mMainStage.mChildrenTaskInfo.keyAt(i)); - } - Log.w(TAG, "Expected onTaskVanished on " + mMainStage - + " to have been called with [" + tasksLeft.toString() - + "] before startAnimation()."); - } - if (mSideStage.getChildCount() != 0) { - final StringBuilder tasksLeft = new StringBuilder(); - for (int i = 0; i < mSideStage.getChildCount(); ++i) { - tasksLeft.append(i != 0 ? ", " : ""); - tasksLeft.append(mSideStage.mChildrenTaskInfo.keyAt(i)); - } - Log.w(TAG, "Expected onTaskVanished on " + mSideStage - + " to have been called with [" + tasksLeft.toString() - + "] before startAnimation()."); - } - - // Update local states. - setSplitsVisible(false); - // Wait until after animation to update divider - - if (info.getType() == TRANSIT_SPLIT_DISMISS_SNAP) { - // Reset crops so they don't interfere with subsequent launches - t.setWindowCrop(mMainStage.mRootLeash, null); - t.setWindowCrop(mSideStage.mRootLeash, null); - } - - if (mDismissTop == STAGE_TYPE_UNDEFINED) { - // Going home (dismissing both splits) - - // TODO: Have a proper remote for this. Until then, though, reset state and use the - // normal animation stuff (which falls back to the normal launcher remote). - t.hide(mSplitLayout.getDividerLeash()); - setDividerVisibility(false); - mSplitTransitions.mPendingDismiss = null; - return false; - } - - addDividerBarToTransition(info, t, false /* show */); - // We're dismissing split by moving the other one to fullscreen. - // Since we don't have any animations for this yet, just use the internal example - // animations. - return true; - } - - private void addDividerBarToTransition(@NonNull TransitionInfo info, - @NonNull SurfaceControl.Transaction t, boolean show) { - final SurfaceControl leash = mSplitLayout.getDividerLeash(); - final TransitionInfo.Change barChange = new TransitionInfo.Change(null /* token */, leash); - final Rect bounds = mSplitLayout.getDividerBounds(); - barChange.setStartAbsBounds(bounds); - barChange.setEndAbsBounds(bounds); - barChange.setMode(show ? TRANSIT_TO_FRONT : TRANSIT_TO_BACK); - barChange.setFlags(FLAG_IS_DIVIDER_BAR); - // Technically this should be order-0, but this is running after layer assignment - // and it's a special case, so just add to end. - info.addChange(barChange); - // Be default, make it visible. The remote animator can adjust alpha if it plans to animate. - if (show) { - t.setAlpha(leash, 1.f); - t.setLayer(leash, Integer.MAX_VALUE); - t.setPosition(leash, bounds.left, bounds.top); - t.show(leash); - } - } - - RemoteAnimationTarget getDividerBarLegacyTarget() { - final Rect bounds = mSplitLayout.getDividerBounds(); - return new RemoteAnimationTarget(-1 /* taskId */, -1 /* mode */, - mSplitLayout.getDividerLeash(), false /* isTranslucent */, null /* clipRect */, - null /* contentInsets */, Integer.MAX_VALUE /* prefixOrderIndex */, - new android.graphics.Point(0, 0) /* position */, bounds, bounds, - new WindowConfiguration(), true, null /* startLeash */, null /* startBounds */, - null /* taskInfo */, false /* allowEnterPip */, TYPE_DOCK_DIVIDER); - } - - RemoteAnimationTarget getOutlineLegacyTarget() { - final Rect bounds = mSideStage.mRootTaskInfo.configuration.windowConfiguration.getBounds(); - // Leverage TYPE_DOCK_DIVIDER type when wrapping outline remote animation target in order to - // distinguish as a split auxiliary target in Launcher. - return new RemoteAnimationTarget(-1 /* taskId */, -1 /* mode */, - mSideStage.getOutlineLeash(), false /* isTranslucent */, null /* clipRect */, - null /* contentInsets */, Integer.MAX_VALUE /* prefixOrderIndex */, - new android.graphics.Point(0, 0) /* position */, bounds, bounds, - new WindowConfiguration(), true, null /* startLeash */, null /* startBounds */, - null /* taskInfo */, false /* allowEnterPip */, TYPE_DOCK_DIVIDER); - } - - @Override - public void dump(@NonNull PrintWriter pw, String prefix) { - final String innerPrefix = prefix + " "; - final String childPrefix = innerPrefix + " "; - pw.println(prefix + TAG + " mDisplayId=" + mDisplayId); - pw.println(innerPrefix + "mDividerVisible=" + mDividerVisible); - pw.println(innerPrefix + "MainStage"); - pw.println(childPrefix + "isActive=" + mMainStage.isActive()); - mMainStageListener.dump(pw, childPrefix); - pw.println(innerPrefix + "SideStage"); - mSideStageListener.dump(pw, childPrefix); - pw.println(innerPrefix + "mSplitLayout=" + mSplitLayout); - } - - /** - * Directly set the visibility of both splits. This assumes hasChildren matches visibility. - * This is intended for batch use, so it assumes other state management logic is already - * handled. - */ - private void setSplitsVisible(boolean visible) { - mMainStageListener.mVisible = mSideStageListener.mVisible = visible; - mMainStageListener.mHasChildren = mSideStageListener.mHasChildren = visible; - } - - /** - * Sets drag info to be logged when splitscreen is next entered. - */ - public void logOnDroppedToSplit(@SplitPosition int position, InstanceId dragSessionId) { - mLogger.enterRequestedByDrag(position, dragSessionId); - } - - /** - * Logs the exit of splitscreen. - */ - private void logExit(int exitReason) { - mLogger.logExit(exitReason, - SPLIT_POSITION_UNDEFINED, 0 /* mainStageUid */, - SPLIT_POSITION_UNDEFINED, 0 /* sideStageUid */, - mSplitLayout.isLandscape()); - } - - /** - * Logs the exit of splitscreen to a specific stage. This must be called before the exit is - * executed. - */ - private void logExitToStage(int exitReason, boolean toMainStage) { - mLogger.logExit(exitReason, - toMainStage ? getMainStagePosition() : SPLIT_POSITION_UNDEFINED, - toMainStage ? mMainStage.getTopChildTaskUid() : 0 /* mainStageUid */, - !toMainStage ? getSideStagePosition() : SPLIT_POSITION_UNDEFINED, - !toMainStage ? mSideStage.getTopChildTaskUid() : 0 /* sideStageUid */, - mSplitLayout.isLandscape()); - } - - class StageListenerImpl implements StageTaskListener.StageListenerCallbacks { - boolean mHasRootTask = false; - boolean mVisible = false; - boolean mHasChildren = false; - - @Override - public void onRootTaskAppeared() { - mHasRootTask = true; - StageCoordinator.this.onStageRootTaskAppeared(this); - } - - @Override - public void onStatusChanged(boolean visible, boolean hasChildren) { - if (!mHasRootTask) return; - - if (mHasChildren != hasChildren) { - mHasChildren = hasChildren; - StageCoordinator.this.onStageHasChildrenChanged(this); - } - if (mVisible != visible) { - mVisible = visible; - StageCoordinator.this.onStageVisibilityChanged(this); - } - } - - @Override - public void onChildTaskStatusChanged(int taskId, boolean present, boolean visible) { - StageCoordinator.this.onStageChildTaskStatusChanged(this, taskId, present, visible); - } - - @Override - public void onRootTaskVanished() { - reset(); - StageCoordinator.this.onStageRootTaskVanished(this); - } - - @Override - public void onNoLongerSupportMultiWindow() { - if (mMainStage.isActive()) { - StageCoordinator.this.exitSplitScreen(null /* childrenToTop */, - SPLITSCREEN_UICHANGED__EXIT_REASON__APP_DOES_NOT_SUPPORT_MULTIWINDOW); - } - } - - private void reset() { - mHasRootTask = false; - mVisible = false; - mHasChildren = false; - } - - public void dump(@NonNull PrintWriter pw, String prefix) { - pw.println(prefix + "mHasRootTask=" + mHasRootTask); - pw.println(prefix + "mVisible=" + mVisible); - pw.println(prefix + "mHasChildren=" + mHasChildren); - } - } -} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/StageTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/StageTaskListener.java deleted file mode 100644 index 7b679580fa87..000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/StageTaskListener.java +++ /dev/null @@ -1,298 +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.stagesplit; - -import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; -import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; -import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; -import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; - -import static com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS; - -import android.annotation.CallSuper; -import android.annotation.Nullable; -import android.app.ActivityManager; -import android.graphics.Point; -import android.graphics.Rect; -import android.util.SparseArray; -import android.view.SurfaceControl; -import android.view.SurfaceSession; -import android.window.WindowContainerTransaction; - -import androidx.annotation.NonNull; - -import com.android.wm.shell.ShellTaskOrganizer; -import com.android.wm.shell.common.SurfaceUtils; -import com.android.wm.shell.common.SyncTransactionQueue; - -import java.io.PrintWriter; - -/** - * Base class that handle common task org. related for split-screen stages. - * Note that this class and its sub-class do not directly perform hierarchy operations. - * They only serve to hold a collection of tasks and provide APIs like - * {@link #setBounds(Rect, WindowContainerTransaction)} for the centralized {@link StageCoordinator} - * to perform operations in-sync with other containers. - * - * @see StageCoordinator - */ -class StageTaskListener implements ShellTaskOrganizer.TaskListener { - private static final String TAG = StageTaskListener.class.getSimpleName(); - - protected static final int[] CONTROLLED_ACTIVITY_TYPES = {ACTIVITY_TYPE_STANDARD}; - protected static final int[] CONTROLLED_WINDOWING_MODES = - {WINDOWING_MODE_FULLSCREEN, WINDOWING_MODE_UNDEFINED}; - protected static final int[] CONTROLLED_WINDOWING_MODES_WHEN_ACTIVE = - {WINDOWING_MODE_FULLSCREEN, WINDOWING_MODE_UNDEFINED, WINDOWING_MODE_MULTI_WINDOW}; - - /** Callback interface for listening to changes in a split-screen stage. */ - public interface StageListenerCallbacks { - void onRootTaskAppeared(); - - void onStatusChanged(boolean visible, boolean hasChildren); - - void onChildTaskStatusChanged(int taskId, boolean present, boolean visible); - - void onRootTaskVanished(); - void onNoLongerSupportMultiWindow(); - } - - private final StageListenerCallbacks mCallbacks; - private final SurfaceSession mSurfaceSession; - protected final SyncTransactionQueue mSyncQueue; - - protected ActivityManager.RunningTaskInfo mRootTaskInfo; - protected SurfaceControl mRootLeash; - protected SurfaceControl mDimLayer; - protected SparseArray<ActivityManager.RunningTaskInfo> mChildrenTaskInfo = new SparseArray<>(); - private final SparseArray<SurfaceControl> mChildrenLeashes = new SparseArray<>(); - - private final StageTaskUnfoldController mStageTaskUnfoldController; - - StageTaskListener(ShellTaskOrganizer taskOrganizer, int displayId, - StageListenerCallbacks callbacks, SyncTransactionQueue syncQueue, - SurfaceSession surfaceSession, - @Nullable StageTaskUnfoldController stageTaskUnfoldController) { - mCallbacks = callbacks; - mSyncQueue = syncQueue; - mSurfaceSession = surfaceSession; - mStageTaskUnfoldController = stageTaskUnfoldController; - taskOrganizer.createRootTask(displayId, WINDOWING_MODE_MULTI_WINDOW, this); - } - - int getChildCount() { - return mChildrenTaskInfo.size(); - } - - boolean containsTask(int taskId) { - return mChildrenTaskInfo.contains(taskId); - } - - /** - * Returns the top activity uid for the top child task. - */ - int getTopChildTaskUid() { - for (int i = mChildrenTaskInfo.size() - 1; i >= 0; --i) { - final ActivityManager.RunningTaskInfo info = mChildrenTaskInfo.valueAt(i); - if (info.topActivityInfo == null) { - continue; - } - return info.topActivityInfo.applicationInfo.uid; - } - return 0; - } - - /** @return {@code true} if this listener contains the currently focused task. */ - boolean isFocused() { - if (mRootTaskInfo == null) { - return false; - } - - if (mRootTaskInfo.isFocused) { - return true; - } - - for (int i = mChildrenTaskInfo.size() - 1; i >= 0; --i) { - if (mChildrenTaskInfo.valueAt(i).isFocused) { - return true; - } - } - - return false; - } - - @Override - @CallSuper - public void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash) { - if (mRootTaskInfo == null && !taskInfo.hasParentTask()) { - mRootLeash = leash; - mRootTaskInfo = taskInfo; - mCallbacks.onRootTaskAppeared(); - sendStatusChanged(); - mSyncQueue.runInSync(t -> { - t.hide(mRootLeash); - mDimLayer = - SurfaceUtils.makeDimLayer(t, mRootLeash, "Dim layer", mSurfaceSession); - }); - } else if (taskInfo.parentTaskId == mRootTaskInfo.taskId) { - final int taskId = taskInfo.taskId; - mChildrenLeashes.put(taskId, leash); - mChildrenTaskInfo.put(taskId, taskInfo); - updateChildTaskSurface(taskInfo, leash, true /* firstAppeared */); - mCallbacks.onChildTaskStatusChanged(taskId, true /* present */, taskInfo.isVisible); - if (ENABLE_SHELL_TRANSITIONS) { - // Status is managed/synchronized by the transition lifecycle. - return; - } - sendStatusChanged(); - } else { - throw new IllegalArgumentException(this + "\n Unknown task: " + taskInfo - + "\n mRootTaskInfo: " + mRootTaskInfo); - } - - if (mStageTaskUnfoldController != null) { - mStageTaskUnfoldController.onTaskAppeared(taskInfo, leash); - } - } - - @Override - @CallSuper - public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) { - if (!taskInfo.supportsMultiWindow) { - // Leave split screen if the task no longer supports multi window. - mCallbacks.onNoLongerSupportMultiWindow(); - return; - } - if (mRootTaskInfo.taskId == taskInfo.taskId) { - mRootTaskInfo = taskInfo; - } else if (taskInfo.parentTaskId == mRootTaskInfo.taskId) { - mChildrenTaskInfo.put(taskInfo.taskId, taskInfo); - mCallbacks.onChildTaskStatusChanged(taskInfo.taskId, true /* present */, - taskInfo.isVisible); - if (!ENABLE_SHELL_TRANSITIONS) { - updateChildTaskSurface( - taskInfo, mChildrenLeashes.get(taskInfo.taskId), false /* firstAppeared */); - } - } else { - throw new IllegalArgumentException(this + "\n Unknown task: " + taskInfo - + "\n mRootTaskInfo: " + mRootTaskInfo); - } - if (ENABLE_SHELL_TRANSITIONS) { - // Status is managed/synchronized by the transition lifecycle. - return; - } - sendStatusChanged(); - } - - @Override - @CallSuper - public void onTaskVanished(ActivityManager.RunningTaskInfo taskInfo) { - final int taskId = taskInfo.taskId; - if (mRootTaskInfo.taskId == taskId) { - mCallbacks.onRootTaskVanished(); - mSyncQueue.runInSync(t -> t.remove(mDimLayer)); - mRootTaskInfo = null; - } else if (mChildrenTaskInfo.contains(taskId)) { - mChildrenTaskInfo.remove(taskId); - mChildrenLeashes.remove(taskId); - mCallbacks.onChildTaskStatusChanged(taskId, false /* present */, taskInfo.isVisible); - if (ENABLE_SHELL_TRANSITIONS) { - // Status is managed/synchronized by the transition lifecycle. - return; - } - sendStatusChanged(); - } else { - throw new IllegalArgumentException(this + "\n Unknown task: " + taskInfo - + "\n mRootTaskInfo: " + mRootTaskInfo); - } - - if (mStageTaskUnfoldController != null) { - mStageTaskUnfoldController.onTaskVanished(taskInfo); - } - } - - @Override - public void attachChildSurfaceToTask(int taskId, SurfaceControl.Builder b) { - b.setParent(findTaskSurface(taskId)); - } - - @Override - public void reparentChildSurfaceToTask(int taskId, SurfaceControl sc, - SurfaceControl.Transaction t) { - t.reparent(sc, findTaskSurface(taskId)); - } - - private SurfaceControl findTaskSurface(int taskId) { - if (mRootTaskInfo.taskId == taskId) { - return mRootLeash; - } else if (mChildrenLeashes.contains(taskId)) { - return mChildrenLeashes.get(taskId); - } else { - throw new IllegalArgumentException("There is no surface for taskId=" + taskId); - } - } - - void setBounds(Rect bounds, WindowContainerTransaction wct) { - wct.setBounds(mRootTaskInfo.token, bounds); - } - - void reorderChild(int taskId, boolean onTop, WindowContainerTransaction wct) { - if (!containsTask(taskId)) { - return; - } - wct.reorder(mChildrenTaskInfo.get(taskId).token, onTop /* onTop */); - } - - void setVisibility(boolean visible, WindowContainerTransaction wct) { - wct.reorder(mRootTaskInfo.token, visible /* onTop */); - } - - void onSplitScreenListenerRegistered(SplitScreen.SplitScreenListener listener, - @SplitScreen.StageType int stage) { - for (int i = mChildrenTaskInfo.size() - 1; i >= 0; --i) { - int taskId = mChildrenTaskInfo.keyAt(i); - listener.onTaskStageChanged(taskId, stage, - mChildrenTaskInfo.get(taskId).isVisible); - } - } - - private void updateChildTaskSurface(ActivityManager.RunningTaskInfo taskInfo, - SurfaceControl leash, boolean firstAppeared) { - final Point taskPositionInParent = taskInfo.positionInParent; - mSyncQueue.runInSync(t -> { - t.setWindowCrop(leash, null); - t.setPosition(leash, taskPositionInParent.x, taskPositionInParent.y); - if (firstAppeared && !ENABLE_SHELL_TRANSITIONS) { - t.setAlpha(leash, 1f); - t.setMatrix(leash, 1, 0, 0, 1); - t.show(leash); - } - }); - } - - private void sendStatusChanged() { - mCallbacks.onStatusChanged(mRootTaskInfo.isVisible, mChildrenTaskInfo.size() > 0); - } - - @Override - @CallSuper - public void dump(@NonNull PrintWriter pw, String prefix) { - final String innerPrefix = prefix + " "; - final String childPrefix = innerPrefix + " "; - pw.println(prefix + this); - } -} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/StageTaskUnfoldController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/StageTaskUnfoldController.java deleted file mode 100644 index 62b9da6d4715..000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/StageTaskUnfoldController.java +++ /dev/null @@ -1,224 +0,0 @@ -/* - * Copyright (C) 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.wm.shell.stagesplit; - -import static android.view.Display.DEFAULT_DISPLAY; - -import android.animation.RectEvaluator; -import android.animation.TypeEvaluator; -import android.annotation.NonNull; -import android.app.ActivityManager; -import android.content.Context; -import android.graphics.Rect; -import android.util.SparseArray; -import android.view.InsetsSource; -import android.view.InsetsState; -import android.view.SurfaceControl; - -import com.android.internal.policy.ScreenDecorationsUtils; -import com.android.wm.shell.common.DisplayInsetsController; -import com.android.wm.shell.common.DisplayInsetsController.OnInsetsChangedListener; -import com.android.wm.shell.common.TransactionPool; -import com.android.wm.shell.unfold.ShellUnfoldProgressProvider; -import com.android.wm.shell.unfold.ShellUnfoldProgressProvider.UnfoldListener; -import com.android.wm.shell.unfold.UnfoldBackgroundController; - -import java.util.concurrent.Executor; - -/** - * Controls transformations of the split screen task surfaces in response - * to the unfolding/folding action on foldable devices - */ -public class StageTaskUnfoldController implements UnfoldListener, OnInsetsChangedListener { - - private static final TypeEvaluator<Rect> RECT_EVALUATOR = new RectEvaluator(new Rect()); - private static final float CROPPING_START_MARGIN_FRACTION = 0.05f; - - private final SparseArray<AnimationContext> mAnimationContextByTaskId = new SparseArray<>(); - private final ShellUnfoldProgressProvider mUnfoldProgressProvider; - private final DisplayInsetsController mDisplayInsetsController; - private final UnfoldBackgroundController mBackgroundController; - private final Executor mExecutor; - private final int mExpandedTaskBarHeight; - private final float mWindowCornerRadiusPx; - private final Rect mStageBounds = new Rect(); - private final TransactionPool mTransactionPool; - - private InsetsSource mTaskbarInsetsSource; - private boolean mBothStagesVisible; - - public StageTaskUnfoldController(@NonNull Context context, - @NonNull TransactionPool transactionPool, - @NonNull ShellUnfoldProgressProvider unfoldProgressProvider, - @NonNull DisplayInsetsController displayInsetsController, - @NonNull UnfoldBackgroundController backgroundController, - @NonNull Executor executor) { - mUnfoldProgressProvider = unfoldProgressProvider; - mTransactionPool = transactionPool; - mExecutor = executor; - mBackgroundController = backgroundController; - mDisplayInsetsController = displayInsetsController; - mWindowCornerRadiusPx = ScreenDecorationsUtils.getWindowCornerRadius(context); - mExpandedTaskBarHeight = context.getResources().getDimensionPixelSize( - com.android.internal.R.dimen.taskbar_frame_height); - } - - /** - * Initializes the controller, starts listening for the external events - */ - public void init() { - mUnfoldProgressProvider.addListener(mExecutor, this); - mDisplayInsetsController.addInsetsChangedListener(DEFAULT_DISPLAY, this); - } - - @Override - public void insetsChanged(InsetsState insetsState) { - mTaskbarInsetsSource = insetsState.getSource(InsetsState.ITYPE_EXTRA_NAVIGATION_BAR); - for (int i = mAnimationContextByTaskId.size() - 1; i >= 0; i--) { - AnimationContext context = mAnimationContextByTaskId.valueAt(i); - context.update(); - } - } - - /** - * Called when split screen task appeared - * @param taskInfo info for the appeared task - * @param leash surface leash for the appeared task - */ - public void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash) { - AnimationContext context = new AnimationContext(leash); - mAnimationContextByTaskId.put(taskInfo.taskId, context); - } - - /** - * Called when a split screen task vanished - * @param taskInfo info for the vanished task - */ - public void onTaskVanished(ActivityManager.RunningTaskInfo taskInfo) { - AnimationContext context = mAnimationContextByTaskId.get(taskInfo.taskId); - if (context != null) { - final SurfaceControl.Transaction transaction = mTransactionPool.acquire(); - resetSurface(transaction, context); - transaction.apply(); - mTransactionPool.release(transaction); - } - mAnimationContextByTaskId.remove(taskInfo.taskId); - } - - @Override - public void onStateChangeProgress(float progress) { - if (mAnimationContextByTaskId.size() == 0 || !mBothStagesVisible) return; - - final SurfaceControl.Transaction transaction = mTransactionPool.acquire(); - mBackgroundController.ensureBackground(transaction); - - for (int i = mAnimationContextByTaskId.size() - 1; i >= 0; i--) { - AnimationContext context = mAnimationContextByTaskId.valueAt(i); - - context.mCurrentCropRect.set(RECT_EVALUATOR - .evaluate(progress, context.mStartCropRect, context.mEndCropRect)); - - transaction.setWindowCrop(context.mLeash, context.mCurrentCropRect) - .setCornerRadius(context.mLeash, mWindowCornerRadiusPx); - } - - transaction.apply(); - - mTransactionPool.release(transaction); - } - - @Override - public void onStateChangeFinished() { - resetTransformations(); - } - - /** - * Called when split screen visibility changes - * @param bothStagesVisible true if both stages of the split screen are visible - */ - public void onSplitVisibilityChanged(boolean bothStagesVisible) { - mBothStagesVisible = bothStagesVisible; - if (!bothStagesVisible) { - resetTransformations(); - } - } - - /** - * Called when split screen stage bounds changed - * @param bounds new bounds for this stage - */ - public void onLayoutChanged(Rect bounds) { - mStageBounds.set(bounds); - - for (int i = mAnimationContextByTaskId.size() - 1; i >= 0; i--) { - final AnimationContext context = mAnimationContextByTaskId.valueAt(i); - context.update(); - } - } - - private void resetTransformations() { - final SurfaceControl.Transaction transaction = mTransactionPool.acquire(); - - for (int i = mAnimationContextByTaskId.size() - 1; i >= 0; i--) { - final AnimationContext context = mAnimationContextByTaskId.valueAt(i); - resetSurface(transaction, context); - } - mBackgroundController.removeBackground(transaction); - transaction.apply(); - - mTransactionPool.release(transaction); - } - - private void resetSurface(SurfaceControl.Transaction transaction, AnimationContext context) { - transaction - .setWindowCrop(context.mLeash, null) - .setCornerRadius(context.mLeash, 0.0F); - } - - private class AnimationContext { - final SurfaceControl mLeash; - final Rect mStartCropRect = new Rect(); - final Rect mEndCropRect = new Rect(); - final Rect mCurrentCropRect = new Rect(); - - private AnimationContext(SurfaceControl leash) { - this.mLeash = leash; - update(); - } - - private void update() { - mStartCropRect.set(mStageBounds); - - if (mTaskbarInsetsSource != null) { - // Only insets the cropping window with taskbar when taskbar is expanded - if (mTaskbarInsetsSource.getFrame().height() >= mExpandedTaskBarHeight) { - mStartCropRect.inset(mTaskbarInsetsSource - .calculateVisibleInsets(mStartCropRect)); - } - } - - // Offset to surface coordinates as layout bounds are in screen coordinates - mStartCropRect.offsetTo(0, 0); - - mEndCropRect.set(mStartCropRect); - - int maxSize = Math.max(mEndCropRect.width(), mEndCropRect.height()); - int margin = (int) (maxSize * CROPPING_START_MARGIN_FRACTION); - mStartCropRect.inset(margin, margin, margin, margin); - } - } -} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawer.java index 54d62edf2570..a0e176c7ea68 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 @@ -261,7 +261,8 @@ public class StartingSurfaceDrawer { WindowManager.LayoutParams.TYPE_APPLICATION_STARTING); params.setFitInsetsSides(0); params.setFitInsetsTypes(0); - params.format = PixelFormat.TRANSLUCENT; + params.format = suggestType == STARTING_WINDOW_TYPE_LEGACY_SPLASH_SCREEN + ? PixelFormat.OPAQUE : PixelFormat.TRANSLUCENT; int windowFlags = WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR; 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 fbc992378e50..379af21ac956 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 @@ -42,10 +42,12 @@ import androidx.annotation.BinderThread; import com.android.internal.annotations.GuardedBy; import com.android.internal.util.function.TriConsumer; import com.android.launcher3.icons.IconProvider; +import com.android.wm.shell.ShellTaskOrganizer; 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.sysui.ShellInit; /** * Implementation to draw the starting window to an application, and remove the starting window @@ -74,6 +76,7 @@ public class StartingWindowController implements RemoteCallable<StartingWindowCo private TriConsumer<Integer, Integer, Integer> mTaskLaunchingCallback; private final StartingSurfaceImpl mImpl = new StartingSurfaceImpl(); private final Context mContext; + private final ShellTaskOrganizer mShellTaskOrganizer; private final ShellExecutor mSplashScreenExecutor; /** * Need guarded because it has exposed to StartingSurface @@ -81,14 +84,20 @@ public class StartingWindowController implements RemoteCallable<StartingWindowCo @GuardedBy("mTaskBackgroundColors") private final SparseIntArray mTaskBackgroundColors = new SparseIntArray(); - public StartingWindowController(Context context, ShellExecutor splashScreenExecutor, - StartingWindowTypeAlgorithm startingWindowTypeAlgorithm, IconProvider iconProvider, + public StartingWindowController(Context context, + ShellInit shellInit, + ShellTaskOrganizer shellTaskOrganizer, + ShellExecutor splashScreenExecutor, + StartingWindowTypeAlgorithm startingWindowTypeAlgorithm, + IconProvider iconProvider, TransactionPool pool) { mContext = context; + mShellTaskOrganizer = shellTaskOrganizer; mStartingSurfaceDrawer = new StartingSurfaceDrawer(context, splashScreenExecutor, iconProvider, pool); mStartingWindowTypeAlgorithm = startingWindowTypeAlgorithm; mSplashScreenExecutor = splashScreenExecutor; + shellInit.addInitCallback(this::onInit, this); } /** @@ -98,6 +107,10 @@ public class StartingWindowController implements RemoteCallable<StartingWindowCo return mImpl; } + private void onInit() { + mShellTaskOrganizer.initStartingWindow(this); + } + @Override public Context getContext() { return mContext; 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 95bc579a4a51..7b498e4f54ec 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 @@ -20,10 +20,8 @@ import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; import static android.graphics.Color.WHITE; import static android.graphics.Color.alpha; import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER; -import static android.view.ViewRootImpl.LOCAL_LAYOUT; import static android.view.WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS; import static android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS; -import static android.view.WindowLayout.UNSPECIFIED_LENGTH; import static android.view.WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; import static android.view.WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED; import static android.view.WindowManager.LayoutParams.FLAG_IGNORE_CHEEK_PRESSES; @@ -53,7 +51,6 @@ import android.annotation.Nullable; import android.app.ActivityManager; import android.app.ActivityManager.TaskDescription; import android.app.ActivityThread; -import android.app.WindowConfiguration; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; @@ -80,7 +77,6 @@ import android.view.SurfaceSession; import android.view.View; import android.view.ViewGroup; import android.view.WindowInsets; -import android.view.WindowLayout; import android.view.WindowManager; import android.view.WindowManagerGlobal; import android.window.ClientWindowFrames; @@ -212,8 +208,6 @@ public class TaskSnapshotWindow { final IWindowSession session = WindowManagerGlobal.getWindowSession(); final SurfaceControl surfaceControl = new SurfaceControl(); final ClientWindowFrames tmpFrames = new ClientWindowFrames(); - final WindowLayout windowLayout = new WindowLayout(); - final Rect displayCutoutSafe = new Rect(); final InsetsSourceControl[] tmpControls = new InsetsSourceControl[0]; final MergedConfiguration tmpMergedConfiguration = new MergedConfiguration(); @@ -234,11 +228,13 @@ public class TaskSnapshotWindow { final InsetsState tmpInsetsState = new InsetsState(); final InputChannel tmpInputChannel = new InputChannel(); + final float[] sizeCompatScale = { 1f }; try { Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "TaskSnapshot#addToDisplay"); final int res = session.addToDisplay(window, layoutParams, View.GONE, displayId, - info.requestedVisibilities, tmpInputChannel, tmpInsetsState, tmpControls); + info.requestedVisibilities, tmpInputChannel, tmpInsetsState, tmpControls, + new Rect(), sizeCompatScale); Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); if (res < 0) { Slog.w(TAG, "Failed to add snapshot starting window res=" + res); @@ -250,25 +246,9 @@ public class TaskSnapshotWindow { window.setOuter(snapshotSurface); try { Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "TaskSnapshot#relayout"); - if (LOCAL_LAYOUT) { - if (!surfaceControl.isValid()) { - session.updateVisibility(window, layoutParams, View.VISIBLE, - tmpMergedConfiguration, surfaceControl, tmpInsetsState, tmpControls); - } - tmpInsetsState.getDisplayCutoutSafe(displayCutoutSafe); - final WindowConfiguration winConfig = - tmpMergedConfiguration.getMergedConfiguration().windowConfiguration; - windowLayout.computeFrames(layoutParams, tmpInsetsState, displayCutoutSafe, - winConfig.getBounds(), winConfig.getWindowingMode(), UNSPECIFIED_LENGTH, - UNSPECIFIED_LENGTH, info.requestedVisibilities, - null /* attachedWindowFrame */, 1f /* compatScale */, tmpFrames); - session.updateLayout(window, layoutParams, 0 /* flags */, tmpFrames, - UNSPECIFIED_LENGTH, UNSPECIFIED_LENGTH); - } else { - session.relayout(window, layoutParams, -1, -1, View.VISIBLE, 0, - tmpFrames, tmpMergedConfiguration, surfaceControl, tmpInsetsState, - tmpControls, new Bundle()); - } + session.relayout(window, layoutParams, -1, -1, View.VISIBLE, 0, 0, 0, + tmpFrames, tmpMergedConfiguration, surfaceControl, tmpInsetsState, + tmpControls, new Bundle()); Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); } catch (RemoteException e) { snapshotSurface.clearWindowSynced(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ConfigurationChangeListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ConfigurationChangeListener.java new file mode 100644 index 000000000000..2fca8f0ecc76 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ConfigurationChangeListener.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.sysui; + +import android.content.res.Configuration; + +/** + * Callbacks for when the configuration changes. + */ +public interface ConfigurationChangeListener { + + /** + * Called when a configuration changes. This precedes all the following callbacks. + */ + default void onConfigurationChanged(Configuration newConfiguration) {} + + /** + * Convenience method to the above, called when the density or font scale changes. + */ + default void onDensityOrFontScaleChanged() {} + + /** + * Convenience method to the above, called when the smallest screen width changes. + */ + default void onSmallestScreenWidthChanged() {} + + /** + * Convenience method to the above, called when the system theme changes, including dark/light + * UI_MODE changes. + */ + default void onThemeChanged() {} + + /** + * Convenience method to the above, called when the local list or layout direction changes. + */ + default void onLocaleOrLayoutDirectionChanged() {} +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/KeyguardChangeListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/KeyguardChangeListener.java new file mode 100644 index 000000000000..9df863163b50 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/KeyguardChangeListener.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.sysui; + +/** + * Callbacks for when the keyguard changes. + */ +public interface KeyguardChangeListener { + /** + * Called when the keyguard is showing (and if so, whether it is occluded). + */ + default void onKeyguardVisibilityChanged(boolean visible, boolean occluded, + boolean animatingDismiss) {} + + /** + * Called when the keyguard dismiss animation has finished. + * + * TODO(b/206741900) deprecate this path once we're able to animate the PiP window as part of + * keyguard dismiss animation. + */ + default void onKeyguardDismissAnimationFinished() {} +} 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 new file mode 100644 index 000000000000..2e6ddc363906 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellCommandHandler.java @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.sysui; + +import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_INIT; + +import com.android.internal.protolog.common.ProtoLog; + +import java.io.PrintWriter; +import java.util.Arrays; +import java.util.TreeMap; +import java.util.function.BiConsumer; + +/** + * An entry point into the shell for dumping shell internal state and running adb commands. + * + * Use with {@code adb shell dumpsys activity service SystemUIService WMShell ...}. + */ +public final class ShellCommandHandler { + private static final String TAG = ShellCommandHandler.class.getSimpleName(); + + // We're using a TreeMap to keep them sorted by command name + private final TreeMap<String, BiConsumer<PrintWriter, String>> mDumpables = new TreeMap<>(); + private final TreeMap<String, ShellCommandActionHandler> mCommands = new TreeMap<>(); + + public interface ShellCommandActionHandler { + /** + * Handles the given command. + * + * @param args the arguments starting with the action name, then the action arguments + * @param pw the write to print output to + */ + boolean onShellCommand(String[] args, PrintWriter pw); + + /** + * Prints the help for this class of commands. Implementations do not need to print the + * command class. + */ + void printShellCommandHelp(PrintWriter pw, String prefix); + } + + + /** + * Adds a callback to run when the Shell is being dumped. + * + * @param callback the callback to be made when Shell is dumped, takes the print writer and + * a prefix + * @param instance used for debugging only + */ + public <T> void addDumpCallback(BiConsumer<PrintWriter, String> callback, T instance) { + mDumpables.put(instance.getClass().getSimpleName(), callback); + ProtoLog.v(WM_SHELL_INIT, "Adding dump callback for %s", + instance.getClass().getSimpleName()); + } + + /** + * Adds an action callback to be invoked when the user runs that particular command from adb. + * + * @param commandClass the top level class of command to invoke + * @param actions the interface to callback when an action of this class is invoked + * @param instance used for debugging only + */ + public <T> void addCommandCallback(String commandClass, ShellCommandActionHandler actions, + T instance) { + mCommands.put(commandClass, actions); + ProtoLog.v(WM_SHELL_INIT, "Adding command class %s for %s", commandClass, + instance.getClass().getSimpleName()); + } + + /** Dumps WM Shell internal state. */ + public void dump(PrintWriter pw) { + for (String key : mDumpables.keySet()) { + final BiConsumer<PrintWriter, String> r = mDumpables.get(key); + r.accept(pw, ""); + pw.println(); + } + } + + + /** Returns {@code true} if command was found and executed. */ + public boolean handleCommand(final String[] args, PrintWriter pw) { + if (args.length < 2) { + // Argument at position 0 is "WMShell". + return false; + } + + final String cmdClass = args[1]; + if (cmdClass.toLowerCase().equals("help")) { + return runHelp(pw); + } + if (!mCommands.containsKey(cmdClass)) { + return false; + } + + // Only pass the actions onwards as arguments to the callback + final ShellCommandActionHandler actions = mCommands.get(args[1]); + final String[] cmdClassArgs = Arrays.copyOfRange(args, 2, args.length); + actions.onShellCommand(cmdClassArgs, pw); + return true; + } + + private boolean runHelp(PrintWriter pw) { + pw.println("Window Manager Shell commands:"); + for (String commandClass : mCommands.keySet()) { + pw.println(" " + commandClass); + mCommands.get(commandClass).printShellCommandHelp(pw, " "); + } + pw.println(" help"); + pw.println(" Print this help text."); + pw.println(" <no arguments provided>"); + pw.println(" Dump all Window Manager Shell internal state"); + return true; + } +} 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 new file mode 100644 index 000000000000..57993948886b --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellController.java @@ -0,0 +1,278 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.sysui; + +import static android.content.pm.ActivityInfo.CONFIG_ASSETS_PATHS; +import static android.content.pm.ActivityInfo.CONFIG_FONT_SCALE; +import static android.content.pm.ActivityInfo.CONFIG_LAYOUT_DIRECTION; +import static android.content.pm.ActivityInfo.CONFIG_LOCALE; +import static android.content.pm.ActivityInfo.CONFIG_SMALLEST_SCREEN_SIZE; +import static android.content.pm.ActivityInfo.CONFIG_UI_MODE; + +import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_SYSUI_EVENTS; + +import android.content.Context; +import android.content.pm.ActivityInfo; +import android.content.pm.UserInfo; +import android.content.res.Configuration; + +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; + +import com.android.internal.protolog.common.ProtoLog; +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.annotations.ExternalThread; + +import java.io.PrintWriter; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * Handles event callbacks from SysUI that can be used within the Shell. + */ +public class ShellController { + private static final String TAG = ShellController.class.getSimpleName(); + + private final ShellInit mShellInit; + private final ShellCommandHandler mShellCommandHandler; + private final ShellExecutor mMainExecutor; + private final ShellInterfaceImpl mImpl = new ShellInterfaceImpl(); + + private final CopyOnWriteArrayList<ConfigurationChangeListener> mConfigChangeListeners = + new CopyOnWriteArrayList<>(); + private final CopyOnWriteArrayList<KeyguardChangeListener> mKeyguardChangeListeners = + new CopyOnWriteArrayList<>(); + private final CopyOnWriteArrayList<UserChangeListener> mUserChangeListeners = + new CopyOnWriteArrayList<>(); + + private Configuration mLastConfiguration; + + + public ShellController(ShellInit shellInit, ShellCommandHandler shellCommandHandler, + ShellExecutor mainExecutor) { + mShellInit = shellInit; + mShellCommandHandler = shellCommandHandler; + mMainExecutor = mainExecutor; + } + + /** + * Returns the external interface to this controller. + */ + public ShellInterface asShell() { + return mImpl; + } + + /** + * Adds a new configuration listener. The configuration change callbacks are not made in any + * particular order. + */ + public void addConfigurationChangeListener(ConfigurationChangeListener listener) { + mConfigChangeListeners.remove(listener); + mConfigChangeListeners.add(listener); + } + + /** + * Removes an existing configuration listener. + */ + public void removeConfigurationChangeListener(ConfigurationChangeListener listener) { + mConfigChangeListeners.remove(listener); + } + + /** + * Adds a new Keyguard listener. The Keyguard change callbacks are not made in any + * particular order. + */ + public void addKeyguardChangeListener(KeyguardChangeListener listener) { + mKeyguardChangeListeners.remove(listener); + mKeyguardChangeListeners.add(listener); + } + + /** + * Removes an existing Keyguard listener. + */ + public void removeKeyguardChangeListener(KeyguardChangeListener listener) { + mKeyguardChangeListeners.remove(listener); + } + + /** + * Adds a new user-change listener. The user change callbacks are not made in any + * particular order. + */ + public void addUserChangeListener(UserChangeListener listener) { + mUserChangeListeners.remove(listener); + mUserChangeListeners.add(listener); + } + + /** + * Removes an existing user-change listener. + */ + public void removeUserChangeListener(UserChangeListener listener) { + mUserChangeListeners.remove(listener); + } + + @VisibleForTesting + void onConfigurationChanged(Configuration newConfig) { + // The initial config is send on startup and doesn't trigger listener callbacks + if (mLastConfiguration == null) { + mLastConfiguration = new Configuration(newConfig); + ProtoLog.v(WM_SHELL_SYSUI_EVENTS, "Initial Configuration: %s", newConfig); + return; + } + + final int diff = newConfig.diff(mLastConfiguration); + ProtoLog.v(WM_SHELL_SYSUI_EVENTS, "New configuration change: %s", newConfig); + ProtoLog.v(WM_SHELL_SYSUI_EVENTS, "\tchanges=%s", + Configuration.configurationDiffToString(diff)); + final boolean densityFontScaleChanged = (diff & CONFIG_FONT_SCALE) != 0 + || (diff & ActivityInfo.CONFIG_DENSITY) != 0; + final boolean smallestScreenWidthChanged = (diff & CONFIG_SMALLEST_SCREEN_SIZE) != 0; + final boolean themeChanged = (diff & CONFIG_ASSETS_PATHS) != 0 + || (diff & CONFIG_UI_MODE) != 0; + final boolean localOrLayoutDirectionChanged = (diff & CONFIG_LOCALE) != 0 + || (diff & CONFIG_LAYOUT_DIRECTION) != 0; + + // Update the last configuration and call listeners + mLastConfiguration.updateFrom(newConfig); + for (ConfigurationChangeListener listener : mConfigChangeListeners) { + listener.onConfigurationChanged(newConfig); + if (densityFontScaleChanged) { + listener.onDensityOrFontScaleChanged(); + } + if (smallestScreenWidthChanged) { + listener.onSmallestScreenWidthChanged(); + } + if (themeChanged) { + listener.onThemeChanged(); + } + if (localOrLayoutDirectionChanged) { + listener.onLocaleOrLayoutDirectionChanged(); + } + } + } + + @VisibleForTesting + void onKeyguardVisibilityChanged(boolean visible, boolean occluded, boolean animatingDismiss) { + ProtoLog.v(WM_SHELL_SYSUI_EVENTS, "Keyguard visibility changed: visible=%b " + + "occluded=%b animatingDismiss=%b", visible, occluded, animatingDismiss); + for (KeyguardChangeListener listener : mKeyguardChangeListeners) { + listener.onKeyguardVisibilityChanged(visible, occluded, animatingDismiss); + } + } + + @VisibleForTesting + void onKeyguardDismissAnimationFinished() { + ProtoLog.v(WM_SHELL_SYSUI_EVENTS, "Keyguard dismiss animation finished"); + for (KeyguardChangeListener listener : mKeyguardChangeListeners) { + listener.onKeyguardDismissAnimationFinished(); + } + } + + @VisibleForTesting + void onUserChanged(int newUserId, @NonNull Context userContext) { + ProtoLog.v(WM_SHELL_SYSUI_EVENTS, "User changed: id=%d", newUserId); + for (UserChangeListener listener : mUserChangeListeners) { + listener.onUserChanged(newUserId, userContext); + } + } + + @VisibleForTesting + void onUserProfilesChanged(@NonNull List<UserInfo> profiles) { + ProtoLog.v(WM_SHELL_SYSUI_EVENTS, "User profiles changed"); + for (UserChangeListener listener : mUserChangeListeners) { + listener.onUserProfilesChanged(profiles); + } + } + + public void dump(@NonNull PrintWriter pw, String prefix) { + final String innerPrefix = prefix + " "; + pw.println(prefix + TAG); + pw.println(innerPrefix + "mConfigChangeListeners=" + mConfigChangeListeners.size()); + pw.println(innerPrefix + "mLastConfiguration=" + mLastConfiguration); + pw.println(innerPrefix + "mKeyguardChangeListeners=" + mKeyguardChangeListeners.size()); + pw.println(innerPrefix + "mUserChangeListeners=" + mUserChangeListeners.size()); + } + + /** + * The interface for calls from outside the Shell, within the host process. + */ + @ExternalThread + private class ShellInterfaceImpl implements ShellInterface { + + @Override + public void onInit() { + try { + mMainExecutor.executeBlocking(() -> mShellInit.init()); + } catch (InterruptedException e) { + throw new RuntimeException("Failed to initialize the Shell in 2s", e); + } + } + + @Override + public void dump(PrintWriter pw) { + try { + mMainExecutor.executeBlocking(() -> mShellCommandHandler.dump(pw)); + } catch (InterruptedException e) { + throw new RuntimeException("Failed to dump the Shell in 2s", e); + } + } + + @Override + public boolean handleCommand(String[] args, PrintWriter pw) { + try { + boolean[] result = new boolean[1]; + mMainExecutor.executeBlocking(() -> { + result[0] = mShellCommandHandler.handleCommand(args, pw); + }); + return result[0]; + } catch (InterruptedException e) { + throw new RuntimeException("Failed to handle Shell command in 2s", e); + } + } + + @Override + public void onConfigurationChanged(Configuration newConfiguration) { + mMainExecutor.execute(() -> + ShellController.this.onConfigurationChanged(newConfiguration)); + } + + @Override + public void onKeyguardVisibilityChanged(boolean visible, boolean occluded, + boolean animatingDismiss) { + mMainExecutor.execute(() -> + ShellController.this.onKeyguardVisibilityChanged(visible, occluded, + animatingDismiss)); + } + + @Override + public void onKeyguardDismissAnimationFinished() { + mMainExecutor.execute(() -> + ShellController.this.onKeyguardDismissAnimationFinished()); + } + + @Override + public void onUserChanged(int newUserId, @NonNull Context userContext) { + mMainExecutor.execute(() -> + ShellController.this.onUserChanged(newUserId, userContext)); + } + + @Override + public void onUserProfilesChanged(@NonNull List<UserInfo> profiles) { + mMainExecutor.execute(() -> + ShellController.this.onUserProfilesChanged(profiles)); + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellInit.java b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellInit.java new file mode 100644 index 000000000000..ac52235375c4 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellInit.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.sysui; + +import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_INIT; + +import android.os.Build; +import android.os.SystemClock; +import android.util.Pair; + +import androidx.annotation.VisibleForTesting; + +import com.android.internal.protolog.common.ProtoLog; +import com.android.wm.shell.common.ShellExecutor; + +import java.util.ArrayList; + +/** + * The entry point implementation into the shell for initializing shell internal state. Classes + * which need to initialize on start of the host SysUI should inject an instance of this class and + * add an init callback. + */ +public class ShellInit { + private static final String TAG = ShellInit.class.getSimpleName(); + + private final ShellExecutor mMainExecutor; + + // An ordered list of init callbacks to be made once shell is first started + private final ArrayList<Pair<String, Runnable>> mInitCallbacks = new ArrayList<>(); + private boolean mHasInitialized; + + + public ShellInit(ShellExecutor mainExecutor) { + mMainExecutor = mainExecutor; + } + + /** + * Adds a callback to the ordered list of callbacks be made when Shell is first started. This + * can be used in class constructors when dagger is used to ensure that the initialization order + * matches the dependency order. + * + * @param r the callback to be made when Shell is initialized + * @param instance used for debugging only + */ + public <T extends Object> void addInitCallback(Runnable r, T instance) { + if (mHasInitialized) { + if (Build.isDebuggable()) { + // All callbacks must be added prior to the Shell being initialized + throw new IllegalArgumentException("Can not add callback after init"); + } + return; + } + final String className = instance.getClass().getSimpleName(); + mInitCallbacks.add(new Pair<>(className, r)); + ProtoLog.v(WM_SHELL_INIT, "Adding init callback for %s", className); + } + + /** + * Calls all the init callbacks when the Shell is first starting. + */ + @VisibleForTesting + public void init() { + ProtoLog.v(WM_SHELL_INIT, "Initializing Shell Components: %d", mInitCallbacks.size()); + // Init in order of registration + for (int i = 0; i < mInitCallbacks.size(); i++) { + final Pair<String, Runnable> info = mInitCallbacks.get(i); + final long t1 = SystemClock.uptimeMillis(); + info.second.run(); + final long t2 = SystemClock.uptimeMillis(); + ProtoLog.v(WM_SHELL_INIT, "\t%s init took %dms", info.first, (t2 - t1)); + } + mInitCallbacks.clear(); + mHasInitialized = true; + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellInterface.java b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellInterface.java new file mode 100644 index 000000000000..2108c824ac6f --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellInterface.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.sysui; + +import android.content.Context; +import android.content.pm.UserInfo; +import android.content.res.Configuration; + +import androidx.annotation.NonNull; + +import java.io.PrintWriter; +import java.util.List; + +/** + * General interface for notifying the Shell of common SysUI events like configuration or keyguard + * changes. + */ +public interface ShellInterface { + + /** + * Initializes the shell state. + */ + default void onInit() {} + + /** + * Dumps the shell state. + */ + default void dump(PrintWriter pw) {} + + /** + * Handles a shell command. + */ + default boolean handleCommand(final String[] args, PrintWriter pw) { + return false; + } + + /** + * Notifies the Shell that the configuration has changed. + */ + default void onConfigurationChanged(Configuration newConfiguration) {} + + /** + * Notifies the Shell that the keyguard is showing (and if so, whether it is occluded) or not + * showing, and whether it is animating a dismiss. + */ + default void onKeyguardVisibilityChanged(boolean visible, boolean occluded, + boolean animatingDismiss) {} + + /** + * Notifies the Shell when the keyguard dismiss animation has finished. + */ + default void onKeyguardDismissAnimationFinished() {} + + /** + * Notifies the Shell when the user changes. + */ + default void onUserChanged(int newUserId, @NonNull Context userContext) {} + + /** + * Notifies the Shell when a profile belonging to the user changes. + */ + default void onUserProfilesChanged(@NonNull List<UserInfo> profiles) {} +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/UserChangeListener.java index 60123ab97fd7..3d0909f6128d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutout.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/UserChangeListener.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 The Android Open Source Project + * 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. @@ -14,23 +14,26 @@ * limitations under the License. */ -package com.android.wm.shell.hidedisplaycutout; +package com.android.wm.shell.sysui; -import android.content.res.Configuration; +import android.content.Context; +import android.content.pm.UserInfo; import androidx.annotation.NonNull; -import com.android.wm.shell.common.annotations.ExternalThread; - -import java.io.PrintWriter; +import java.util.List; /** - * Interface to engage hide display cutout feature. + * Callbacks for when the user or user's profiles changes. */ -@ExternalThread -public interface HideDisplayCutout { +public interface UserChangeListener { + /** + * Called when the current (parent) user changes. + */ + default void onUserChanged(int newUserId, @NonNull Context userContext) {} + /** - * Notifies {@link Configuration} changed. + * Called when a profile belonging to the user changes. */ - void onConfigurationChanged(Configuration newConfig); + default void onUserProfilesChanged(@NonNull List<UserInfo> profiles) {} } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/tasksurfacehelper/TaskSurfaceHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/tasksurfacehelper/TaskSurfaceHelper.java deleted file mode 100644 index ad9dda619370..000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/tasksurfacehelper/TaskSurfaceHelper.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright (C) 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.wm.shell.tasksurfacehelper; - -import android.app.ActivityManager.RunningTaskInfo; -import android.graphics.Rect; -import android.view.SurfaceControl; - -import java.util.concurrent.Executor; -import java.util.function.Consumer; - -/** - * Interface to communicate with a Task's SurfaceControl. - */ -public interface TaskSurfaceHelper { - - /** Sets the METADATA_GAME_MODE for the layer corresponding to the task **/ - default void setGameModeForTask(int taskId, int gameMode) {} - - /** Takes a screenshot for a task **/ - default void screenshotTask(RunningTaskInfo taskInfo, Rect crop, Executor executor, - Consumer<SurfaceControl.ScreenshotHardwareBuffer> consumer) {} -} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/tasksurfacehelper/TaskSurfaceHelperController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/tasksurfacehelper/TaskSurfaceHelperController.java deleted file mode 100644 index 064d9d1231c1..000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/tasksurfacehelper/TaskSurfaceHelperController.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright (C) 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.wm.shell.tasksurfacehelper; - -import android.app.ActivityManager.RunningTaskInfo; -import android.graphics.Rect; -import android.view.SurfaceControl; - -import com.android.wm.shell.ShellTaskOrganizer; -import com.android.wm.shell.common.ShellExecutor; - -import java.util.concurrent.Executor; -import java.util.function.Consumer; - -/** - * Intermediary controller that communicates with {@link ShellTaskOrganizer} to send commands - * to SurfaceControl. - */ -public class TaskSurfaceHelperController { - - private final ShellTaskOrganizer mTaskOrganizer; - private final ShellExecutor mMainExecutor; - private final TaskSurfaceHelperImpl mImpl = new TaskSurfaceHelperImpl(); - - public TaskSurfaceHelperController(ShellTaskOrganizer taskOrganizer, - ShellExecutor mainExecutor) { - mTaskOrganizer = taskOrganizer; - mMainExecutor = mainExecutor; - } - - public TaskSurfaceHelper asTaskSurfaceHelper() { - return mImpl; - } - - /** - * Sends a Transaction to set the game mode metadata on the - * corresponding SurfaceControl - */ - public void setGameModeForTask(int taskId, int gameMode) { - mTaskOrganizer.setSurfaceMetadata(taskId, SurfaceControl.METADATA_GAME_MODE, gameMode); - } - - /** - * Take screenshot of the specified task. - */ - public void screenshotTask(RunningTaskInfo taskInfo, Rect crop, - Consumer<SurfaceControl.ScreenshotHardwareBuffer> consumer) { - mTaskOrganizer.screenshotTask(taskInfo, crop, consumer); - } - - private class TaskSurfaceHelperImpl implements TaskSurfaceHelper { - @Override - public void setGameModeForTask(int taskId, int gameMode) { - mMainExecutor.execute(() -> { - TaskSurfaceHelperController.this.setGameModeForTask(taskId, gameMode); - }); - } - - @Override - public void screenshotTask(RunningTaskInfo taskInfo, Rect crop, Executor executor, - Consumer<SurfaceControl.ScreenshotHardwareBuffer> consumer) { - mMainExecutor.execute(() -> { - TaskSurfaceHelperController.this.screenshotTask(taskInfo, crop, - (t) -> executor.execute(() -> consumer.accept(t))); - }); - } - } -} 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 new file mode 100644 index 000000000000..3cba92956f95 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java @@ -0,0 +1,398 @@ +/* + * 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.transition; + +import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; +import static android.view.WindowManager.TRANSIT_CHANGE; +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.splitscreen.SplitScreen.STAGE_TYPE_UNDEFINED; +import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_CHILD_TASK_ENTER_PIP; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.IBinder; +import android.view.SurfaceControl; +import android.view.WindowManager; +import android.window.TransitionInfo; +import android.window.TransitionRequestInfo; +import android.window.WindowContainerTransaction; + +import com.android.internal.protolog.common.ProtoLog; +import com.android.wm.shell.pip.PipTransitionController; +import com.android.wm.shell.pip.phone.PipTouchHandler; +import com.android.wm.shell.protolog.ShellProtoLogGroup; +import com.android.wm.shell.splitscreen.SplitScreenController; +import com.android.wm.shell.splitscreen.StageCoordinator; +import com.android.wm.shell.sysui.ShellInit; + +import java.util.ArrayList; +import java.util.Optional; + +/** + * A handler for dealing with transitions involving multiple other handlers. For example: an + * activity in split-screen going into PiP. + */ +public class DefaultMixedHandler implements Transitions.TransitionHandler { + + private final Transitions mPlayer; + private PipTransitionController mPipHandler; + private StageCoordinator mSplitHandler; + + private static class MixedTransition { + static final int TYPE_ENTER_PIP_FROM_SPLIT = 1; + + /** Both the display and split-state (enter/exit) is changing */ + static final int TYPE_DISPLAY_AND_SPLIT_CHANGE = 2; + + /** The default animation for this mixed transition. */ + static final int ANIM_TYPE_DEFAULT = 0; + + /** For ENTER_PIP_FROM_SPLIT, indicates that this is a to-home animation. */ + static final int ANIM_TYPE_GOING_HOME = 1; + + final int mType; + int mAnimType = 0; + final IBinder mTransition; + + Transitions.TransitionFinishCallback mFinishCallback = null; + Transitions.TransitionHandler mLeftoversHandler = null; + WindowContainerTransaction mFinishWCT = null; + + /** + * Mixed transitions are made up of multiple "parts". This keeps track of how many + * parts are currently animating. + */ + int mInFlightSubAnimations = 0; + + MixedTransition(int type, IBinder transition) { + mType = type; + mTransition = transition; + } + } + + private final ArrayList<MixedTransition> mActiveTransitions = new ArrayList<>(); + + public DefaultMixedHandler(@NonNull ShellInit shellInit, @NonNull Transitions player, + Optional<SplitScreenController> splitScreenControllerOptional, + Optional<PipTouchHandler> pipTouchHandlerOptional) { + mPlayer = player; + if (Transitions.ENABLE_SHELL_TRANSITIONS && pipTouchHandlerOptional.isPresent() + && splitScreenControllerOptional.isPresent()) { + // Add after dependencies because it is higher priority + shellInit.addInitCallback(() -> { + mPipHandler = pipTouchHandlerOptional.get().getTransitionHandler(); + mSplitHandler = splitScreenControllerOptional.get().getTransitionHandler(); + mPlayer.addHandler(this); + if (mSplitHandler != null) { + mSplitHandler.setMixedHandler(this); + } + }, this); + } + } + + @Nullable + @Override + public WindowContainerTransaction handleRequest(@NonNull IBinder transition, + @NonNull TransitionRequestInfo request) { + if (mPipHandler.requestHasPipEnter(request) && mSplitHandler.isSplitActive()) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Got a PiP-enter request while " + + "Split-Screen is active, so treat it as Mixed."); + if (request.getRemoteTransition() != null) { + throw new IllegalStateException("Unexpected remote transition in" + + "pip-enter-from-split request"); + } + mActiveTransitions.add(new MixedTransition(MixedTransition.TYPE_ENTER_PIP_FROM_SPLIT, + transition)); + + WindowContainerTransaction out = new WindowContainerTransaction(); + mPipHandler.augmentRequest(transition, request, out); + mSplitHandler.addEnterOrExitIfNeeded(request, out); + return out; + } + return null; + } + + private TransitionInfo subCopy(@NonNull TransitionInfo info, + @WindowManager.TransitionType int newType, boolean withChanges) { + final TransitionInfo out = new TransitionInfo(newType, withChanges ? info.getFlags() : 0); + if (withChanges) { + for (int i = 0; i < info.getChanges().size(); ++i) { + out.getChanges().add(info.getChanges().get(i)); + } + } + out.setRootLeash(info.getRootLeash(), info.getRootOffset().x, info.getRootOffset().y); + out.setAnimationOptions(info.getAnimationOptions()); + return out; + } + + private boolean isHomeOpening(@NonNull TransitionInfo.Change change) { + return change.getTaskInfo() != null + && change.getTaskInfo().getActivityType() != ACTIVITY_TYPE_HOME; + } + + private boolean isWallpaper(@NonNull TransitionInfo.Change change) { + return (change.getFlags() & FLAG_IS_WALLPAPER) != 0; + } + + @Override + public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, + @NonNull Transitions.TransitionFinishCallback finishCallback) { + MixedTransition mixed = null; + for (int i = mActiveTransitions.size() - 1; i >= 0; --i) { + if (mActiveTransitions.get(i).mTransition != transition) continue; + mixed = mActiveTransitions.get(i); + break; + } + if (mixed == null) return false; + + if (mixed.mType == MixedTransition.TYPE_ENTER_PIP_FROM_SPLIT) { + return animateEnterPipFromSplit(mixed, info, startTransaction, finishTransaction, + finishCallback); + } else if (mixed.mType == MixedTransition.TYPE_DISPLAY_AND_SPLIT_CHANGE) { + return false; + } else { + mActiveTransitions.remove(mixed); + throw new IllegalStateException("Starting mixed animation without a known mixed type? " + + mixed.mType); + } + } + + private boolean animateEnterPipFromSplit(@NonNull final MixedTransition mixed, + @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, + @NonNull Transitions.TransitionFinishCallback finishCallback) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Animating a mixed transition for " + + "entering PIP while Split-Screen is active."); + TransitionInfo.Change pipChange = null; + TransitionInfo.Change wallpaper = null; + final TransitionInfo everythingElse = subCopy(info, TRANSIT_TO_BACK, true /* changes */); + boolean homeIsOpening = false; + for (int i = info.getChanges().size() - 1; i >= 0; --i) { + TransitionInfo.Change change = info.getChanges().get(i); + if (mPipHandler.isEnteringPip(change, info.getType())) { + if (pipChange != null) { + throw new IllegalStateException("More than 1 pip-entering changes in one" + + " transition? " + info); + } + pipChange = change; + // going backwards, so remove-by-index is fine. + everythingElse.getChanges().remove(i); + } else if (isHomeOpening(change)) { + homeIsOpening = true; + } else if (isWallpaper(change)) { + wallpaper = change; + } + } + if (pipChange == null) { + // um, something probably went wrong. + return false; + } + final boolean isGoingHome = homeIsOpening; + mixed.mFinishCallback = finishCallback; + Transitions.TransitionFinishCallback finishCB = (wct, wctCB) -> { + --mixed.mInFlightSubAnimations; + if (mixed.mInFlightSubAnimations > 0) return; + mActiveTransitions.remove(mixed); + if (isGoingHome) { + mSplitHandler.onTransitionAnimationComplete(); + } + mixed.mFinishCallback.onTransitionFinished(wct, wctCB); + }; + if (isGoingHome) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Animation is actually mixed " + + "since entering-PiP caused us to leave split and return home."); + // We need to split the transition into 2 parts: the pip part (animated by pip) + // and the dismiss-part (animated by launcher). + mixed.mInFlightSubAnimations = 2; + // immediately make the wallpaper visible (so that we don't see it pop-in during + // the time it takes to start recents animation (which is remote). + if (wallpaper != null) { + startTransaction.show(wallpaper.getLeash()).setAlpha(wallpaper.getLeash(), 1.f); + } + // make a new startTransaction because pip's startEnterAnimation "consumes" it so + // we need a separate one to send over to launcher. + SurfaceControl.Transaction otherStartT = new SurfaceControl.Transaction(); + // Let split update internal state for dismiss. + mSplitHandler.prepareDismissAnimation(STAGE_TYPE_UNDEFINED, + EXIT_REASON_CHILD_TASK_ENTER_PIP, everythingElse, otherStartT, + finishTransaction); + + // We are trying to accommodate launcher's close animation which can't handle the + // divider-bar, so if split-handler is closing the divider-bar, just hide it and remove + // from transition info. + for (int i = everythingElse.getChanges().size() - 1; i >= 0; --i) { + if ((everythingElse.getChanges().get(i).getFlags() & FLAG_IS_DIVIDER_BAR) != 0) { + everythingElse.getChanges().remove(i); + break; + } + } + + mPipHandler.startEnterAnimation(pipChange, startTransaction, finishTransaction, + finishCB); + // Dispatch the rest of the transition normally. This will most-likely be taken by + // recents or default handler. + mixed.mLeftoversHandler = mPlayer.dispatchTransition(mixed.mTransition, everythingElse, + otherStartT, finishTransaction, finishCB, this); + } else { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Not leaving split, so just " + + "forward animation to Pip-Handler."); + // This happens if the pip-ing activity is in a multi-activity task (and thus a + // new pip task is spawned). In this case, we don't actually exit split so we can + // just let pip transition handle the animation verbatim. + mixed.mInFlightSubAnimations = 1; + mPipHandler.startAnimation(mixed.mTransition, info, startTransaction, finishTransaction, + finishCB); + } + return true; + } + + private void unlinkMissingParents(TransitionInfo from) { + for (int i = 0; i < from.getChanges().size(); ++i) { + final TransitionInfo.Change chg = from.getChanges().get(i); + if (chg.getParent() == null) continue; + if (from.getChange(chg.getParent()) == null) { + from.getChanges().get(i).setParent(null); + } + } + } + + private boolean isWithinTask(TransitionInfo info, TransitionInfo.Change chg) { + TransitionInfo.Change curr = chg; + while (curr != null) { + if (curr.getTaskInfo() != null) return true; + if (curr.getParent() == null) break; + curr = info.getChange(curr.getParent()); + } + return false; + } + + /** + * This is intended to be called by SplitCoordinator as a helper to mix an already-pending + * split transition with a display-change. The use-case for this is when a display + * change/rotation gets collected into a split-screen enter/exit transition which has already + * been claimed by StageCoordinator.handleRequest . This happens during launcher tests. + */ + public boolean animatePendingSplitWithDisplayChange(@NonNull IBinder transition, + @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startT, + @NonNull SurfaceControl.Transaction finishT, + @NonNull Transitions.TransitionFinishCallback finishCallback) { + final TransitionInfo everythingElse = subCopy(info, info.getType(), true /* withChanges */); + final TransitionInfo displayPart = subCopy(info, TRANSIT_CHANGE, false /* withChanges */); + for (int i = info.getChanges().size() - 1; i >= 0; --i) { + TransitionInfo.Change change = info.getChanges().get(i); + if (isWithinTask(info, change)) continue; + displayPart.addChange(change); + everythingElse.getChanges().remove(i); + } + if (displayPart.getChanges().isEmpty()) return false; + unlinkMissingParents(everythingElse); + final MixedTransition mixed = new MixedTransition( + MixedTransition.TYPE_DISPLAY_AND_SPLIT_CHANGE, transition); + mixed.mFinishCallback = finishCallback; + mActiveTransitions.add(mixed); + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Animation is a mix of display change " + + "and split change."); + // We need to split the transition into 2 parts: the split part and the display part. + mixed.mInFlightSubAnimations = 2; + + Transitions.TransitionFinishCallback finishCB = (wct, wctCB) -> { + --mixed.mInFlightSubAnimations; + if (wctCB != null) { + throw new IllegalArgumentException("Can't mix transitions that require finish" + + " sync callback"); + } + if (wct != null) { + if (mixed.mFinishWCT == null) { + mixed.mFinishWCT = wct; + } else { + mixed.mFinishWCT.merge(wct, true /* transfer */); + } + } + if (mixed.mInFlightSubAnimations > 0) return; + mActiveTransitions.remove(mixed); + mixed.mFinishCallback.onTransitionFinished(mixed.mFinishWCT, null /* wctCB */); + }; + + // Dispatch the display change. This will most-likely be taken by the default handler. + // Do this first since the first handler used will apply the startT; the display change + // needs to take a screenshot before that happens so we need it to be the first handler. + mixed.mLeftoversHandler = mPlayer.dispatchTransition(mixed.mTransition, displayPart, + startT, finishT, finishCB, mSplitHandler); + + // Note: at this point, startT has probably already been applied, so we are basically + // giving splitHandler an empty startT. This is currently OK because display-change will + // grab a screenshot and paste it on top anyways. + mSplitHandler.startPendingAnimation( + transition, everythingElse, startT, finishT, finishCB); + return true; + } + + @Override + public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget, + @NonNull Transitions.TransitionFinishCallback finishCallback) { + for (int i = 0; i < mActiveTransitions.size(); ++i) { + if (mActiveTransitions.get(i) != mergeTarget) continue; + MixedTransition mixed = mActiveTransitions.get(i); + if (mixed.mInFlightSubAnimations <= 0) { + // Already done, so no need to end it. + return; + } + if (mixed.mType == MixedTransition.TYPE_ENTER_PIP_FROM_SPLIT) { + if (mixed.mAnimType == MixedTransition.ANIM_TYPE_GOING_HOME) { + boolean ended = mSplitHandler.end(); + // If split couldn't end (because it is remote), then don't end everything else + // since we have to play out the animation anyways. + if (!ended) return; + mPipHandler.end(); + if (mixed.mLeftoversHandler != null) { + mixed.mLeftoversHandler.mergeAnimation(transition, info, t, mergeTarget, + finishCallback); + } + } else { + mPipHandler.end(); + } + } else if (mixed.mType == MixedTransition.TYPE_DISPLAY_AND_SPLIT_CHANGE) { + // queue + } else { + throw new IllegalStateException("Playing a mixed transition with unknown type? " + + mixed.mType); + } + } + } + + @Override + public void onTransitionConsumed(@NonNull IBinder transition, boolean aborted, + @Nullable SurfaceControl.Transaction finishT) { + MixedTransition mixed = null; + for (int i = mActiveTransitions.size() - 1; i >= 0; --i) { + if (mActiveTransitions.get(i).mTransition != transition) continue; + mixed = mActiveTransitions.remove(i); + break; + } + if (mixed == null) return; + if (mixed.mType == MixedTransition.TYPE_ENTER_PIP_FROM_SPLIT) { + mPipHandler.onTransitionConsumed(transition, aborted, finishT); + } + } +} 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 9154226b7b22..dbb2948de5db 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,7 +18,6 @@ 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; @@ -42,7 +41,11 @@ import static android.view.WindowManager.TRANSIT_OPEN; import static android.view.WindowManager.TRANSIT_RELAUNCH; import static android.view.WindowManager.TRANSIT_TO_BACK; import static android.view.WindowManager.TRANSIT_TO_FRONT; +import static android.window.TransitionInfo.FLAG_CROSS_PROFILE_OWNER_THUMBNAIL; +import static android.window.TransitionInfo.FLAG_CROSS_PROFILE_WORK_THUMBNAIL; import static android.window.TransitionInfo.FLAG_DISPLAY_HAS_ALERT_WINDOWS; +import static android.window.TransitionInfo.FLAG_FILLS_TASK; +import static android.window.TransitionInfo.FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY; import static android.window.TransitionInfo.FLAG_IS_DISPLAY; import static android.window.TransitionInfo.FLAG_IS_VOICE_INTERACTION; import static android.window.TransitionInfo.FLAG_IS_WALLPAPER; @@ -55,6 +58,10 @@ import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITI import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_INTRA_OPEN; import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_NONE; import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_OPEN; +import static com.android.wm.shell.transition.TransitionAnimationHelper.addBackgroundToTransition; +import static com.android.wm.shell.transition.TransitionAnimationHelper.getTransitionBackgroundColorIfSet; +import static com.android.wm.shell.transition.TransitionAnimationHelper.loadAttributeAnimation; +import static com.android.wm.shell.transition.TransitionAnimationHelper.sDisableCustomTaskAnimationProperty; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; @@ -62,6 +69,7 @@ import android.animation.ValueAnimator; import android.annotation.ColorInt; import android.annotation.NonNull; import android.annotation.Nullable; +import android.app.ActivityManager; import android.app.ActivityThread; import android.app.admin.DevicePolicyManager; import android.content.BroadcastReceiver; @@ -69,7 +77,6 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.graphics.Canvas; -import android.graphics.Color; import android.graphics.Insets; import android.graphics.Paint; import android.graphics.PixelFormat; @@ -79,7 +86,6 @@ import android.graphics.drawable.Drawable; import android.hardware.HardwareBuffer; import android.os.Handler; import android.os.IBinder; -import android.os.SystemProperties; import android.os.UserHandle; import android.util.ArrayMap; import android.view.Choreographer; @@ -107,6 +113,7 @@ 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.sysui.ShellInit; import java.util.ArrayList; import java.util.List; @@ -116,24 +123,10 @@ import java.util.function.Consumer; public class DefaultTransitionHandler implements Transitions.TransitionHandler { private static final int MAX_ANIMATION_DURATION = 3000; - /** - * Restrict ability of activities overriding transition animation in a way such that - * an activity can do it only when the transition happens within a same task. - * - * @see android.app.Activity#overridePendingTransition(int, int) - */ - private static final String DISABLE_CUSTOM_TASK_ANIMATION_PROPERTY = - "persist.wm.disable_custom_task_animation"; - - /** - * @see #DISABLE_CUSTOM_TASK_ANIMATION_PROPERTY - */ - static boolean sDisableCustomTaskAnimationProperty = - SystemProperties.getBoolean(DISABLE_CUSTOM_TASK_ANIMATION_PROPERTY, true); - private final TransactionPool mTransactionPool; private final DisplayController mDisplayController; private final Context mContext; + private final Handler mMainHandler; private final ShellExecutor mMainExecutor; private final ShellExecutor mAnimExecutor; private final TransitionAnimation mTransitionAnimation; @@ -150,8 +143,6 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { private final int mCurrentUserId; - private ScreenRotationAnimation mRotationAnimation; - private Drawable mEnterpriseThumbnailDrawable; private BroadcastReceiver mEnterpriseResourceUpdatedReceiver = new BroadcastReceiver() { @@ -165,27 +156,33 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { } }; - DefaultTransitionHandler(@NonNull DisplayController displayController, - @NonNull TransactionPool transactionPool, Context context, + DefaultTransitionHandler(@NonNull Context context, + @NonNull ShellInit shellInit, + @NonNull DisplayController displayController, + @NonNull TransactionPool transactionPool, @NonNull ShellExecutor mainExecutor, @NonNull Handler mainHandler, @NonNull ShellExecutor animExecutor) { mDisplayController = displayController; mTransactionPool = transactionPool; mContext = context; + mMainHandler = mainHandler; mMainExecutor = mainExecutor; mAnimExecutor = animExecutor; mTransitionAnimation = new TransitionAnimation(context, false /* debug */, Transitions.TAG); mCurrentUserId = UserHandle.myUserId(); + mDevicePolicyManager = mContext.getSystemService(DevicePolicyManager.class); + shellInit.addInitCallback(this::onInit, this); + } - mDevicePolicyManager = context.getSystemService(DevicePolicyManager.class); + private void onInit() { updateEnterpriseThumbnailDrawable(); mContext.registerReceiver( mEnterpriseResourceUpdatedReceiver, new IntentFilter(ACTION_DEVICE_POLICY_RESOURCE_UPDATED), /* broadcastPermission = */ null, - mainHandler); + mMainHandler); - AttributeCache.init(context); + AttributeCache.init(mContext); } private void updateEnterpriseThumbnailDrawable() { @@ -195,14 +192,24 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { } @VisibleForTesting - static boolean isRotationSeamless(@NonNull TransitionInfo info, - DisplayController displayController) { + static int getRotationAnimationHint(@NonNull TransitionInfo.Change displayChange, + @NonNull TransitionInfo info, @NonNull DisplayController displayController) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, - "Display is changing, check if it should be seamless."); - boolean checkedDisplayLayout = false; - boolean hasTask = false; - boolean displayExplicitSeamless = false; - for (int i = info.getChanges().size() - 1; i >= 0; --i) { + "Display is changing, resolve the animation hint."); + // The explicit request of display has the highest priority. + if (displayChange.getRotationAnimation() == ROTATION_ANIMATION_SEAMLESS) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, + " display requests explicit seamless"); + return ROTATION_ANIMATION_SEAMLESS; + } + + boolean allTasksSeamless = false; + boolean rejectSeamless = false; + ActivityManager.RunningTaskInfo topTaskInfo = null; + int animationHint = ROTATION_ANIMATION_ROTATE; + // Traverse in top-to-bottom order so that the first task is top-most. + final int size = info.getChanges().size(); + for (int i = 0; i < size; ++i) { final TransitionInfo.Change change = info.getChanges().get(i); // Only look at changing things. showing/hiding don't need to rotate. @@ -215,95 +222,69 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { if ((change.getFlags() & FLAG_DISPLAY_HAS_ALERT_WINDOWS) != 0) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " display has system alert windows, so not seamless."); - return false; + rejectSeamless = true; } - displayExplicitSeamless = - change.getRotationAnimation() == ROTATION_ANIMATION_SEAMLESS; } else if ((change.getFlags() & FLAG_IS_WALLPAPER) != 0) { if (change.getRotationAnimation() != ROTATION_ANIMATION_SEAMLESS) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " wallpaper is participating but isn't seamless."); - return false; + rejectSeamless = true; } } else if (change.getTaskInfo() != null) { - hasTask = true; + final int anim = change.getRotationAnimation(); + final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo(); + final boolean isTopTask = topTaskInfo == null; + if (isTopTask) { + topTaskInfo = taskInfo; + if (anim != ROTATION_ANIMATION_UNSPECIFIED + && anim != ROTATION_ANIMATION_SEAMLESS) { + animationHint = anim; + } + } // We only enable seamless rotation if all the visible task windows requested it. - if (change.getRotationAnimation() != ROTATION_ANIMATION_SEAMLESS) { + if (anim != ROTATION_ANIMATION_SEAMLESS) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " task %s isn't requesting seamless, so not seamless.", - change.getTaskInfo().taskId); - return false; - } - - // This is the only way to get display-id currently, so we will check display - // capabilities here - if (!checkedDisplayLayout) { - // only need to check display once. - checkedDisplayLayout = true; - final DisplayLayout displayLayout = displayController.getDisplayLayout( - change.getTaskInfo().displayId); - // For the upside down rotation we don't rotate seamlessly as the navigation - // bar moves position. Note most apps (using orientation:sensor or user as - // opposed to fullSensor) will not enter the reverse portrait orientation, so - // actually the orientation won't change at all. - int upsideDownRotation = displayLayout.getUpsideDownRotation(); - if (change.getStartRotation() == upsideDownRotation - || change.getEndRotation() == upsideDownRotation) { - ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, - " rotation involves upside-down portrait, so not seamless."); - return false; - } - - // If the navigation bar can't change sides, then it will jump when we change - // orientations and we don't rotate seamlessly - unless that is allowed, eg. - // with gesture navigation where the navbar is low-profile enough that this - // isn't very noticeable. - if (!displayLayout.allowSeamlessRotationDespiteNavBarMoving() - && (!(displayLayout.navigationBarCanMove() - && (change.getStartAbsBounds().width() - != change.getStartAbsBounds().height())))) { - ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, - " nav bar changes sides, so not seamless."); - return false; - } + taskInfo.taskId); + allTasksSeamless = false; + } else if (isTopTask) { + allTasksSeamless = true; } } } - // ROTATION_ANIMATION_SEAMLESS can only be requested by task or display. - if (hasTask || displayExplicitSeamless) { - ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Rotation IS seamless."); - return true; + if (!allTasksSeamless || rejectSeamless) { + return animationHint; } - return false; - } - /** - * Gets the rotation animation for the topmost task. Assumes that seamless is checked - * elsewhere, so it will default SEAMLESS to ROTATE. - */ - private int getRotationAnimation(@NonNull TransitionInfo info) { - // Traverse in top-to-bottom order so that the first task is top-most - for (int i = 0; i < info.getChanges().size(); ++i) { - final TransitionInfo.Change change = info.getChanges().get(i); - - // Only look at changing things. showing/hiding don't need to rotate. - if (change.getMode() != TRANSIT_CHANGE) continue; - - // This container isn't rotating, so we can ignore it. - if (change.getEndRotation() == change.getStartRotation()) continue; + // This is the only way to get display-id currently, so check display capabilities here. + final DisplayLayout displayLayout = displayController.getDisplayLayout( + topTaskInfo.displayId); + // For the upside down rotation we don't rotate seamlessly as the navigation bar moves + // position. Note most apps (using orientation:sensor or user as opposed to fullSensor) + // will not enter the reverse portrait orientation, so actually the orientation won't + // change at all. + final int upsideDownRotation = displayLayout.getUpsideDownRotation(); + if (displayChange.getStartRotation() == upsideDownRotation + || displayChange.getEndRotation() == upsideDownRotation) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, + " rotation involves upside-down portrait, so not seamless."); + return animationHint; + } - if (change.getTaskInfo() != null) { - final int anim = change.getRotationAnimation(); - if (anim == ROTATION_ANIMATION_UNSPECIFIED - // Fallback animation for seamless should also be default. - || anim == ROTATION_ANIMATION_SEAMLESS) { - return ROTATION_ANIMATION_ROTATE; - } - return anim; - } + // If the navigation bar can't change sides, then it will jump when we change orientations + // and we don't rotate seamlessly - unless that is allowed, e.g. with gesture navigation + // where the navbar is low-profile enough that this isn't very noticeable. + if (!displayLayout.allowSeamlessRotationDespiteNavBarMoving() + && (!(displayLayout.navigationBarCanMove() + && (displayChange.getStartAbsBounds().width() + != displayChange.getStartAbsBounds().height())))) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, + " nav bar changes sides, so not seamless."); + return animationHint; } - return ROTATION_ANIMATION_ROTATE; + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Rotation IS seamless."); + return ROTATION_ANIMATION_SEAMLESS; } @Override @@ -330,12 +311,6 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { final Runnable onAnimFinish = () -> { if (!animations.isEmpty()) return; - - if (mRotationAnimation != null) { - mRotationAnimation.kill(); - mRotationAnimation = null; - } - mAnimations.remove(transition); finishCallback.onTransitionFinished(null /* wct */, null /* wctCB */); }; @@ -352,14 +327,11 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { if (change.getMode() == TRANSIT_CHANGE && (change.getFlags() & FLAG_IS_DISPLAY) != 0) { if (info.getType() == TRANSIT_CHANGE) { - isSeamlessDisplayChange = isRotationSeamless(info, mDisplayController); - final int anim = getRotationAnimation(info); + final int anim = getRotationAnimationHint(change, info, mDisplayController); + isSeamlessDisplayChange = anim == ROTATION_ANIMATION_SEAMLESS; if (!(isSeamlessDisplayChange || anim == ROTATION_ANIMATION_JUMPCUT)) { - mRotationAnimation = new ScreenRotationAnimation(mContext, mSurfaceSession, - mTransactionPool, startTransaction, change, info.getRootLeash(), - anim); - mRotationAnimation.startAnimation(animations, onAnimFinish, - mTransitionAnimationScaleSetting, mMainExecutor, mAnimExecutor); + startRotationAnimation(startTransaction, change, info, anim, animations, + onAnimFinish); continue; } } else { @@ -398,11 +370,20 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { change.getEndAbsBounds().top - info.getRootOffset().y); // Seamless display transition doesn't need to animate. if (isSeamlessDisplayChange) continue; - if (isTask) { - // Skip non-tasks since those usually have null bounds. + if (isTask || (change.hasFlags(FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY) + && !change.hasFlags(FLAG_FILLS_TASK))) { + // Update Task and embedded split window crop bounds, otherwise we may see crop + // on previous bounds during the rotation animation. startTransaction.setWindowCrop(change.getLeash(), change.getEndAbsBounds().width(), change.getEndAbsBounds().height()); } + // Rotation change of independent non display window container. + if (change.getParent() == null + && change.getStartRotation() != change.getEndRotation()) { + startRotationAnimation(startTransaction, change, info, + ROTATION_ANIMATION_ROTATE, animations, onAnimFinish); + continue; + } } // Don't animate anything that isn't independent. @@ -438,22 +419,8 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { cornerRadius = 0; } - if (a.getShowBackdrop()) { - if (info.getAnimationOptions().getBackgroundColor() != 0) { - // If available use the background color provided through AnimationOptions - backgroundColorForTransition = - info.getAnimationOptions().getBackgroundColor(); - } else if (a.getBackdropColor() != 0) { - // Otherwise fallback on the background color provided through the animation - // definition. - backgroundColorForTransition = a.getBackdropColor(); - } else if (change.getBackgroundColor() != 0) { - // Otherwise default to the window's background color if provided through - // the theme as the background color for the animation - the top most window - // with a valid background color and showBackground set takes precedence. - backgroundColorForTransition = change.getBackgroundColor(); - } - } + backgroundColorForTransition = getTransitionBackgroundColorIfSet(info, change, a, + backgroundColorForTransition); boolean delayedEdgeExtension = false; if (!isTask && a.hasExtension()) { @@ -470,8 +437,9 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { } final Rect clipRect = Transitions.isClosingType(change.getMode()) - ? mRotator.getEndBoundsInStartRotation(change) - : change.getEndAbsBounds(); + ? new Rect(mRotator.getEndBoundsInStartRotation(change)) + : new Rect(change.getEndAbsBounds()); + clipRect.offsetTo(0, 0); if (delayedEdgeExtension) { // If the edge extension needs to happen after the startTransition has been @@ -480,11 +448,11 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { postStartTransactionCallbacks.add(t -> startSurfaceAnimation(animations, a, change.getLeash(), onAnimFinish, mTransactionPool, mMainExecutor, mAnimExecutor, - null /* position */, cornerRadius, clipRect)); + change.getEndRelOffset(), cornerRadius, clipRect)); } else { startSurfaceAnimation(animations, a, change.getLeash(), onAnimFinish, - mTransactionPool, mMainExecutor, mAnimExecutor, null /* position */, - cornerRadius, clipRect); + mTransactionPool, mMainExecutor, mAnimExecutor, + change.getEndRelOffset(), cornerRadius, clipRect); } if (info.getAnimationOptions() != null) { @@ -520,9 +488,52 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { return true; } + @Override + public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget, + @NonNull Transitions.TransitionFinishCallback finishCallback) { + ArrayList<Animator> anims = mAnimations.get(mergeTarget); + if (anims == null) return; + for (int i = anims.size() - 1; i >= 0; --i) { + final Animator anim = anims.get(i); + mAnimExecutor.execute(anim::end); + } + } + + private void startRotationAnimation(SurfaceControl.Transaction startTransaction, + TransitionInfo.Change change, TransitionInfo info, int animHint, + ArrayList<Animator> animations, Runnable onAnimFinish) { + final ScreenRotationAnimation anim = new ScreenRotationAnimation(mContext, mSurfaceSession, + mTransactionPool, startTransaction, change, info.getRootLeash(), animHint); + // The rotation animation may consist of 3 animations: fade-out screenshot, fade-in real + // content, and background color. The item of "animGroup" will be removed if the sub + // animation is finished. Then if the list becomes empty, the rotation animation is done. + final ArrayList<Animator> animGroup = new ArrayList<>(3); + final ArrayList<Animator> animGroupStore = new ArrayList<>(3); + final Runnable finishCallback = () -> { + if (!animGroup.isEmpty()) return; + anim.kill(); + animations.removeAll(animGroupStore); + onAnimFinish.run(); + }; + anim.startAnimation(animGroup, finishCallback, mTransitionAnimationScaleSetting, + mMainExecutor, mAnimExecutor); + for (int i = animGroup.size() - 1; i >= 0; i--) { + final Animator animator = animGroup.get(i); + animGroupStore.add(animator); + animations.add(animator); + } + } + private void edgeExtendWindow(TransitionInfo.Change change, Animation a, SurfaceControl.Transaction startTransaction, SurfaceControl.Transaction finishTransaction) { + // Do not create edge extension surface for transfer starting window change. + // The app surface could be empty thus nothing can draw on the hardware renderer, which will + // block this thread when calling Surface#unlockCanvasAndPost. + if ((change.getFlags() & FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT) != 0) { + return; + } final Transformation transformationAtStart = new Transformation(); a.getTransformationAt(0, transformationAtStart); final Transformation transformationAtEnd = new Transformation(); @@ -631,29 +642,6 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { return edgeExtensionLayer; } - private void addBackgroundToTransition( - @NonNull SurfaceControl rootLeash, - @ColorInt int color, - @NonNull SurfaceControl.Transaction startTransaction, - @NonNull SurfaceControl.Transaction finishTransaction - ) { - final Color bgColor = Color.valueOf(color); - final float[] colorArray = new float[] { bgColor.red(), bgColor.green(), bgColor.blue() }; - - final SurfaceControl animationBackgroundSurface = new SurfaceControl.Builder() - .setName("Animation Background") - .setParent(rootLeash) - .setColorLayer() - .setOpaque(true) - .build(); - - startTransaction - .setLayer(animationBackgroundSurface, Integer.MIN_VALUE) - .setColor(animationBackgroundSurface, colorArray) - .show(animationBackgroundSurface); - finishTransaction.remove(animationBackgroundSurface); - } - @Nullable @Override public WindowContainerTransaction handleRequest(@NonNull IBinder transition, @@ -667,9 +655,9 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { } @Nullable - private Animation loadAnimation(TransitionInfo info, TransitionInfo.Change change, - int wallpaperTransit) { - Animation a = null; + private Animation loadAnimation(@NonNull TransitionInfo info, + @NonNull TransitionInfo.Change change, int wallpaperTransit) { + Animation a; final int type = info.getType(); final int flags = info.getFlags(); @@ -680,7 +668,7 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { final boolean isTask = change.getTaskInfo() != null; final TransitionInfo.AnimationOptions options = info.getAnimationOptions(); final int overrideType = options != null ? options.getType() : ANIM_NONE; - final boolean canCustomContainer = isTask ? !sDisableCustomTaskAnimationProperty : true; + final boolean canCustomContainer = !isTask || !sDisableCustomTaskAnimationProperty; final Rect endBounds = Transitions.isClosingType(changeMode) ? mRotator.getEndBoundsInStartRotation(change) : change.getEndAbsBounds(); @@ -724,72 +712,7 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { // This received a transferred starting window, so don't animate return null; } else { - int animAttr = 0; - boolean translucent = false; - if (wallpaperTransit == WALLPAPER_TRANSITION_INTRA_OPEN) { - animAttr = enter - ? R.styleable.WindowAnimation_wallpaperIntraOpenEnterAnimation - : R.styleable.WindowAnimation_wallpaperIntraOpenExitAnimation; - } else if (wallpaperTransit == WALLPAPER_TRANSITION_INTRA_CLOSE) { - animAttr = enter - ? R.styleable.WindowAnimation_wallpaperIntraCloseEnterAnimation - : R.styleable.WindowAnimation_wallpaperIntraCloseExitAnimation; - } else if (wallpaperTransit == WALLPAPER_TRANSITION_OPEN) { - animAttr = enter - ? R.styleable.WindowAnimation_wallpaperOpenEnterAnimation - : R.styleable.WindowAnimation_wallpaperOpenExitAnimation; - } else if (wallpaperTransit == WALLPAPER_TRANSITION_CLOSE) { - animAttr = enter - ? R.styleable.WindowAnimation_wallpaperCloseEnterAnimation - : R.styleable.WindowAnimation_wallpaperCloseExitAnimation; - } else if (type == TRANSIT_OPEN) { - // We will translucent open animation for translucent activities and tasks. Choose - // WindowAnimation_activityOpenEnterAnimation and set translucent here, then - // TransitionAnimation loads appropriate animation later. - if ((changeFlags & FLAG_TRANSLUCENT) != 0 && enter) { - translucent = true; - } - if (isTask && !translucent) { - animAttr = enter - ? R.styleable.WindowAnimation_taskOpenEnterAnimation - : R.styleable.WindowAnimation_taskOpenExitAnimation; - } else { - animAttr = enter - ? R.styleable.WindowAnimation_activityOpenEnterAnimation - : R.styleable.WindowAnimation_activityOpenExitAnimation; - } - } else if (type == TRANSIT_TO_FRONT) { - animAttr = enter - ? R.styleable.WindowAnimation_taskToFrontEnterAnimation - : R.styleable.WindowAnimation_taskToFrontExitAnimation; - } else if (type == TRANSIT_CLOSE) { - if (isTask) { - animAttr = enter - ? R.styleable.WindowAnimation_taskCloseEnterAnimation - : R.styleable.WindowAnimation_taskCloseExitAnimation; - } else { - if ((changeFlags & FLAG_TRANSLUCENT) != 0 && !enter) { - translucent = true; - } - animAttr = enter - ? R.styleable.WindowAnimation_activityCloseEnterAnimation - : R.styleable.WindowAnimation_activityCloseExitAnimation; - } - } else if (type == TRANSIT_TO_BACK) { - animAttr = enter - ? R.styleable.WindowAnimation_taskToBackEnterAnimation - : R.styleable.WindowAnimation_taskToBackExitAnimation; - } - - if (animAttr != 0) { - if (overrideType == ANIM_FROM_STYLE && canCustomContainer) { - a = mTransitionAnimation - .loadAnimationAttr(options.getPackageName(), options.getAnimations(), - animAttr, translucent); - } else { - a = mTransitionAnimation.loadDefaultAnimationAttr(animAttr, translucent); - } - } + a = loadAttributeAnimation(info, change, wallpaperTransit, mTransitionAnimation); } if (a != null) { @@ -834,13 +757,19 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { }); }; va.addListener(new AnimatorListenerAdapter() { + private boolean mFinished = false; + @Override public void onAnimationEnd(Animator animation) { + if (mFinished) return; + mFinished = true; finisher.run(); } @Override public void onAnimationCancel(Animator animation) { + if (mFinished) return; + mFinished = true; finisher.run(); } }); @@ -851,11 +780,10 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { private void attachThumbnail(@NonNull ArrayList<Animator> animations, @NonNull Runnable finishCallback, TransitionInfo.Change change, TransitionInfo.AnimationOptions options, float cornerRadius) { - final boolean isTask = change.getTaskInfo() != null; final boolean isOpen = Transitions.isOpeningType(change.getMode()); final boolean isClose = Transitions.isClosingType(change.getMode()); if (isOpen) { - if (options.getType() == ANIM_OPEN_CROSS_PROFILE_APPS && isTask) { + if (options.getType() == ANIM_OPEN_CROSS_PROFILE_APPS) { attachCrossProfileThumbnailAnimation(animations, finishCallback, change, cornerRadius); } else if (options.getType() == ANIM_THUMBNAIL_SCALE_UP) { @@ -870,8 +798,13 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { @NonNull Runnable finishCallback, TransitionInfo.Change change, float cornerRadius) { final Rect bounds = change.getEndAbsBounds(); // Show the right drawable depending on the user we're transitioning to. - final Drawable thumbnailDrawable = change.getTaskInfo().userId == mCurrentUserId - ? mContext.getDrawable(R.drawable.ic_account_circle) : mEnterpriseThumbnailDrawable; + final Drawable thumbnailDrawable = change.hasFlags(FLAG_CROSS_PROFILE_OWNER_THUMBNAIL) + ? mContext.getDrawable(R.drawable.ic_account_circle) + : change.hasFlags(FLAG_CROSS_PROFILE_WORK_THUMBNAIL) + ? mEnterpriseThumbnailDrawable : null; + if (thumbnailDrawable == null) { + return; + } final HardwareBuffer thumbnail = mTransitionAnimation.createCrossProfileAppsThumbnail( thumbnailDrawable, bounds); if (thumbnail == null) { @@ -896,7 +829,7 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { a.restrictDuration(MAX_ANIMATION_DURATION); a.scaleCurrentDuration(mTransitionAnimationScaleSetting); startSurfaceAnimation(animations, a, wt.getSurface(), finisher, mTransactionPool, - mMainExecutor, mAnimExecutor, new Point(bounds.left, bounds.top), + mMainExecutor, mAnimExecutor, change.getEndRelOffset(), cornerRadius, change.getEndAbsBounds()); } @@ -921,7 +854,7 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { a.restrictDuration(MAX_ANIMATION_DURATION); a.scaleCurrentDuration(mTransitionAnimationScaleSetting); startSurfaceAnimation(animations, a, wt.getSurface(), finisher, mTransactionPool, - mMainExecutor, mAnimExecutor, null /* position */, + mMainExecutor, mAnimExecutor, change.getEndRelOffset(), cornerRadius, change.getEndAbsBounds()); } @@ -954,7 +887,7 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { private static void applyTransformation(long time, SurfaceControl.Transaction t, SurfaceControl leash, Animation anim, Transformation transformation, float[] matrix, - Point position, float cornerRadius, @Nullable Rect clipRect) { + Point position, float cornerRadius, @Nullable Rect immutableClipRect) { anim.getTransformation(time, transformation); if (position != null) { transformation.getMatrix().postTranslate(position.x, position.y); @@ -962,6 +895,7 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { t.setMatrix(leash, transformation.getMatrix(), matrix); t.setAlpha(leash, transformation.getAlpha()); + final Rect clipRect = immutableClipRect == null ? null : new Rect(immutableClipRect); Insets extensionInsets = Insets.min(transformation.getInsets(), Insets.NONE); if (!extensionInsets.equals(Insets.NONE) && clipRect != null && !clipRect.isEmpty()) { // Clip out any overflowing edge extension 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 3e2a0e635a75..4e1fa290270d 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 @@ -18,11 +18,9 @@ package com.android.wm.shell.transition; import android.annotation.NonNull; import android.annotation.Nullable; -import android.app.ActivityTaskManager; import android.os.IBinder; import android.os.RemoteException; import android.util.Log; -import android.util.Slog; import android.view.SurfaceControl; import android.window.IRemoteTransition; import android.window.IRemoteTransitionFinishedCallback; @@ -87,18 +85,14 @@ public class OneShotRemoteHandler implements Transitions.TransitionHandler { }); } }; + Transitions.setRunningRemoteTransitionDelegate(mRemote.getAppThread()); try { if (mRemote.asBinder() != null) { mRemote.asBinder().linkToDeath(remoteDied, 0 /* flags */); } - try { - ActivityTaskManager.getService().setRunningRemoteTransitionDelegate( - mRemote.getAppThread()); - } catch (SecurityException e) { - Slog.e(Transitions.TAG, "Unable to boost animation thread. This should only happen" - + " during unit tests"); - } mRemote.getRemoteTransition().startAnimation(transition, info, startTransaction, cb); + // assume that remote will apply the start transaction. + startTransaction.clear(); } catch (RemoteException e) { Log.e(Transitions.TAG, "Error running remote transition.", e); if (mRemote.asBinder() != null) { @@ -120,6 +114,11 @@ public class OneShotRemoteHandler implements Transitions.TransitionHandler { @Override public void onTransitionFinished(WindowContainerTransaction wct, SurfaceControl.Transaction sct) { + // We have merged, since we sent the transaction over binder, the one in this + // process won't be cleared if the remote applied it. We don't actually know if the + // remote applied the transaction, but applying twice will break surfaceflinger + // so just assume the worst-case and clear the local transaction. + t.clear(); mMainExecutor.execute( () -> finishCallback.onTransitionFinished(wct, null /* wctCB */)); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RemoteTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RemoteTransitionHandler.java index ece9f47e8788..9469529de8f1 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 @@ -18,7 +18,6 @@ package com.android.wm.shell.transition; import android.annotation.NonNull; import android.annotation.Nullable; -import android.app.ActivityTaskManager; import android.os.IBinder; import android.os.RemoteException; import android.util.ArrayMap; @@ -83,7 +82,8 @@ public class RemoteTransitionHandler implements Transitions.TransitionHandler { } @Override - public void onTransitionMerged(@NonNull IBinder transition) { + public void onTransitionConsumed(@NonNull IBinder transition, boolean aborted, + @Nullable SurfaceControl.Transaction finishT) { mRequestedRemotes.remove(transition); } @@ -129,16 +129,12 @@ public class RemoteTransitionHandler implements Transitions.TransitionHandler { }); } }; + Transitions.setRunningRemoteTransitionDelegate(remote.getAppThread()); try { handleDeath(remote.asBinder(), finishCallback); - try { - ActivityTaskManager.getService().setRunningRemoteTransitionDelegate( - remote.getAppThread()); - } catch (SecurityException e) { - Log.e(Transitions.TAG, "Unable to boost animation thread. This should only happen" - + " during unit tests"); - } remote.getRemoteTransition().startAnimation(transition, info, startTransaction, cb); + // assume that remote will apply the start transaction. + startTransaction.clear(); } catch (RemoteException e) { Log.e(Transitions.TAG, "Error running remote transition.", e); unhandleDeath(remote.asBinder(), finishCallback); @@ -162,6 +158,11 @@ public class RemoteTransitionHandler implements Transitions.TransitionHandler { @Override public void onTransitionFinished(WindowContainerTransaction wct, SurfaceControl.Transaction sct) { + // We have merged, since we sent the transaction over binder, the one in this + // process won't be cleared if the remote applied it. We don't actually know if the + // remote applied the transaction, but applying twice will break surfaceflinger + // so just assume the worst-case and clear the local transaction. + t.clear(); mMainExecutor.execute(() -> { if (!mRequestedRemotes.containsKey(mergeTarget)) { Log.e(TAG, "Merged transition finished after it's mergeTarget (the " 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 46f73fda37a1..b647f43da522 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 @@ -34,7 +34,6 @@ import android.annotation.NonNull; import android.content.Context; import android.graphics.Color; import android.graphics.ColorSpace; -import android.graphics.GraphicBuffer; import android.graphics.Matrix; import android.graphics.Rect; import android.hardware.HardwareBuffer; @@ -85,12 +84,8 @@ class ScreenRotationAnimation { private final Context mContext; private final TransactionPool mTransactionPool; private final float[] mTmpFloats = new float[9]; - // Complete transformations being applied. - private final Matrix mSnapshotInitialMatrix = new Matrix(); - /** The leash of display. */ + /** The leash of the changing window container. */ private final SurfaceControl mSurfaceControl; - private final Rect mStartBounds = new Rect(); - private final Rect mEndBounds = new Rect(); private final int mAnimHint; private final int mStartWidth; @@ -108,8 +103,7 @@ class ScreenRotationAnimation { */ private SurfaceControl mBackColorSurface; /** The leash using to animate screenshot layer. */ - private SurfaceControl mAnimLeash; - private Transaction mTransaction; + private final SurfaceControl mAnimLeash; // The current active animation to move from the old to the new rotated // state. Which animation is run here will depend on the old and new @@ -137,9 +131,6 @@ class ScreenRotationAnimation { mStartRotation = change.getStartRotation(); mEndRotation = change.getEndRotation(); - mStartBounds.set(change.getStartAbsBounds()); - mEndBounds.set(change.getEndAbsBounds()); - mAnimLeash = new SurfaceControl.Builder(session) .setParent(rootLeash) .setEffectLayer() @@ -148,53 +139,59 @@ class ScreenRotationAnimation { .build(); try { - SurfaceControl.LayerCaptureArgs args = - new SurfaceControl.LayerCaptureArgs.Builder(mSurfaceControl) - .setCaptureSecureLayers(true) - .setAllowProtected(true) - .setSourceCrop(new Rect(0, 0, mStartWidth, mStartHeight)) - .build(); - SurfaceControl.ScreenshotHardwareBuffer screenshotBuffer = - SurfaceControl.captureLayers(args); - if (screenshotBuffer == null) { - Slog.w(TAG, "Unable to take screenshot of display"); - return; - } - - mScreenshotLayer = new SurfaceControl.Builder(session) - .setParent(mAnimLeash) - .setBLASTLayer() - .setSecure(screenshotBuffer.containsSecureLayers()) - .setCallsite("ShellRotationAnimation") - .setName("RotationLayer") - .build(); + if (change.getSnapshot() != null) { + mScreenshotLayer = change.getSnapshot(); + t.reparent(mScreenshotLayer, mAnimLeash); + mStartLuma = change.getSnapshotLuma(); + } else { + SurfaceControl.LayerCaptureArgs args = + new SurfaceControl.LayerCaptureArgs.Builder(mSurfaceControl) + .setCaptureSecureLayers(true) + .setAllowProtected(true) + .setSourceCrop(new Rect(0, 0, mStartWidth, mStartHeight)) + .build(); + SurfaceControl.ScreenshotHardwareBuffer screenshotBuffer = + SurfaceControl.captureLayers(args); + if (screenshotBuffer == null) { + Slog.w(TAG, "Unable to take screenshot of display"); + return; + } + + mScreenshotLayer = new SurfaceControl.Builder(session) + .setParent(mAnimLeash) + .setBLASTLayer() + .setSecure(screenshotBuffer.containsSecureLayers()) + .setOpaque(true) + .setCallsite("ShellRotationAnimation") + .setName("RotationLayer") + .build(); - GraphicBuffer buffer = GraphicBuffer.createFromHardwareBuffer( - screenshotBuffer.getHardwareBuffer()); + final ColorSpace colorSpace = screenshotBuffer.getColorSpace(); + final HardwareBuffer hardwareBuffer = screenshotBuffer.getHardwareBuffer(); + t.setDataSpace(mScreenshotLayer, colorSpace.getDataSpace()); + t.setBuffer(mScreenshotLayer, hardwareBuffer); + t.show(mScreenshotLayer); + if (!isCustomRotate()) { + mStartLuma = getMedianBorderLuma(hardwareBuffer, colorSpace); + } + } t.setLayer(mAnimLeash, SCREEN_FREEZE_LAYER_BASE); - t.setPosition(mAnimLeash, 0, 0); - t.setAlpha(mAnimLeash, 1); t.show(mAnimLeash); - - t.setBuffer(mScreenshotLayer, buffer); - t.setColorSpace(mScreenshotLayer, screenshotBuffer.getColorSpace()); - t.show(mScreenshotLayer); + // Crop the real content in case it contains a larger child layer, e.g. wallpaper. + t.setCrop(mSurfaceControl, new Rect(0, 0, mEndWidth, mEndHeight)); if (!isCustomRotate()) { mBackColorSurface = new SurfaceControl.Builder(session) .setParent(rootLeash) .setColorLayer() + .setOpaque(true) .setCallsite("ShellRotationAnimation") .setName("BackColorSurface") .build(); - HardwareBuffer hardwareBuffer = screenshotBuffer.getHardwareBuffer(); - mStartLuma = getMedianBorderLuma(hardwareBuffer, screenshotBuffer.getColorSpace()); - t.setLayer(mBackColorSurface, -1); t.setColor(mBackColorSurface, new float[]{mStartLuma, mStartLuma, mStartLuma}); - t.setAlpha(mBackColorSurface, 1); t.show(mBackColorSurface); } @@ -202,7 +199,7 @@ class ScreenRotationAnimation { Slog.w(TAG, "Unable to allocate freeze surface", e); } - setRotation(t); + setScreenshotTransform(t); t.apply(); } @@ -210,19 +207,36 @@ class ScreenRotationAnimation { return mAnimHint == ROTATION_ANIMATION_CROSSFADE || mAnimHint == ROTATION_ANIMATION_JUMPCUT; } - private void setRotation(SurfaceControl.Transaction t) { - // Compute the transformation matrix that must be applied - // to the snapshot to make it stay in the same original position - // with the current screen rotation. - int delta = deltaRotation(mEndRotation, mStartRotation); - createRotationMatrix(delta, mStartWidth, mStartHeight, mSnapshotInitialMatrix); - setRotationTransform(t, mSnapshotInitialMatrix); - } - - private void setRotationTransform(SurfaceControl.Transaction t, Matrix matrix) { + private void setScreenshotTransform(SurfaceControl.Transaction t) { if (mScreenshotLayer == null) { return; } + final Matrix matrix = new Matrix(); + final int delta = deltaRotation(mEndRotation, mStartRotation); + if (delta != 0) { + // Compute the transformation matrix that must be applied to the snapshot to make it + // stay in the same original position with the current screen rotation. + switch (delta) { + case Surface.ROTATION_90: + matrix.setRotate(90, 0, 0); + matrix.postTranslate(mStartHeight, 0); + break; + case Surface.ROTATION_180: + matrix.setRotate(180, 0, 0); + matrix.postTranslate(mStartWidth, mStartHeight); + break; + case Surface.ROTATION_270: + matrix.setRotate(270, 0, 0); + matrix.postTranslate(0, mStartWidth); + break; + } + } else if ((mEndWidth > mStartWidth) == (mEndHeight > mStartHeight) + && (mEndWidth != mStartWidth || mEndHeight != mStartHeight)) { + // Display resizes without rotation change. + final float scale = Math.max((float) mEndWidth / mStartHeight, + (float) mEndHeight / mStartHeight); + matrix.setScale(scale, scale); + } matrix.getValues(mTmpFloats); float x = mTmpFloats[Matrix.MTRANS_X]; float y = mTmpFloats[Matrix.MTRANS_Y]; @@ -230,9 +244,6 @@ class ScreenRotationAnimation { t.setMatrix(mScreenshotLayer, mTmpFloats[Matrix.MSCALE_X], mTmpFloats[Matrix.MSKEW_Y], mTmpFloats[Matrix.MSKEW_X], mTmpFloats[Matrix.MSCALE_Y]); - - t.setAlpha(mScreenshotLayer, (float) 1.0); - t.show(mScreenshotLayer); } /** @@ -298,7 +309,6 @@ class ScreenRotationAnimation { mRotateEnterAnimation.restrictDuration(MAX_ANIMATION_DURATION); mRotateEnterAnimation.scaleCurrentDuration(animationScale); - mTransaction = mTransactionPool.acquire(); if (customRotate) { mRotateAlphaAnimation.initialize(mEndWidth, mEndHeight, mStartWidth, mStartHeight); mRotateAlphaAnimation.restrictDuration(MAX_ANIMATION_DURATION); @@ -378,22 +388,16 @@ class ScreenRotationAnimation { } public void kill() { - Transaction t = mTransaction != null ? mTransaction : mTransactionPool.acquire(); + final Transaction t = mTransactionPool.acquire(); if (mAnimLeash.isValid()) { t.remove(mAnimLeash); } - if (mScreenshotLayer != null) { - if (mScreenshotLayer.isValid()) { - t.remove(mScreenshotLayer); - } - mScreenshotLayer = null; + if (mScreenshotLayer != null && mScreenshotLayer.isValid()) { + t.remove(mScreenshotLayer); } - if (mBackColorSurface != null) { - if (mBackColorSurface.isValid()) { - t.remove(mBackColorSurface); - } - mBackColorSurface = null; + if (mBackColorSurface != null && mBackColorSurface.isValid()) { + t.remove(mBackColorSurface); } t.apply(); mTransactionPool.release(t); @@ -486,27 +490,6 @@ class ScreenRotationAnimation { return getMedianBorderLuma(buffer.getHardwareBuffer(), buffer.getColorSpace()); } - private static void createRotationMatrix(int rotation, int width, int height, - Matrix outMatrix) { - switch (rotation) { - case Surface.ROTATION_0: - outMatrix.reset(); - break; - case Surface.ROTATION_90: - outMatrix.setRotate(90, 0, 0); - outMatrix.postTranslate(height, 0); - break; - case Surface.ROTATION_180: - outMatrix.setRotate(180, 0, 0); - outMatrix.postTranslate(width, height); - break; - case Surface.ROTATION_270: - outMatrix.setRotate(270, 0, 0); - outMatrix.postTranslate(0, width); - break; - } - } - private static void applyColor(int startColor, int endColor, float[] rgbFloat, float fraction, SurfaceControl surface, SurfaceControl.Transaction t) { final int color = (Integer) ArgbEvaluator.getInstance().evaluate(fraction, startColor, 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 new file mode 100644 index 000000000000..efee6f40b53e --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java @@ -0,0 +1,220 @@ +/* + * 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.transition; + +import static android.app.ActivityOptions.ANIM_FROM_STYLE; +import static android.app.ActivityOptions.ANIM_NONE; +import static android.app.WindowConfiguration.ACTIVITY_TYPE_DREAM; +import static android.view.WindowManager.TRANSIT_CLOSE; +import static android.view.WindowManager.TRANSIT_OPEN; +import static android.view.WindowManager.TRANSIT_TO_BACK; +import static android.view.WindowManager.TRANSIT_TO_FRONT; +import static android.view.WindowManager.transitTypeToString; +import static android.window.TransitionInfo.FLAG_TRANSLUCENT; + +import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_CLOSE; +import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_INTRA_CLOSE; +import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_INTRA_OPEN; +import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_OPEN; + +import android.annotation.ColorInt; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.graphics.Color; +import android.os.SystemProperties; +import android.view.SurfaceControl; +import android.view.animation.Animation; +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.wm.shell.protolog.ShellProtoLogGroup; + +/** The helper class that provides methods for adding styles to transition animations. */ +public class TransitionAnimationHelper { + + /** + * Restrict ability of activities overriding transition animation in a way such that + * an activity can do it only when the transition happens within a same task. + * + * @see android.app.Activity#overridePendingTransition(int, int) + */ + private static final String DISABLE_CUSTOM_TASK_ANIMATION_PROPERTY = + "persist.wm.disable_custom_task_animation"; + + /** + * @see #DISABLE_CUSTOM_TASK_ANIMATION_PROPERTY + */ + static final boolean sDisableCustomTaskAnimationProperty = + SystemProperties.getBoolean(DISABLE_CUSTOM_TASK_ANIMATION_PROPERTY, true); + + /** Loads the animation that is defined through attribute id for the given transition. */ + @Nullable + public static Animation loadAttributeAnimation(@NonNull TransitionInfo info, + @NonNull TransitionInfo.Change change, int wallpaperTransit, + @NonNull TransitionAnimation transitionAnimation) { + final int type = info.getType(); + final int changeMode = change.getMode(); + final int changeFlags = change.getFlags(); + final boolean enter = Transitions.isOpeningType(changeMode); + final boolean isTask = change.getTaskInfo() != null; + final TransitionInfo.AnimationOptions options = info.getAnimationOptions(); + final int overrideType = options != null ? options.getType() : ANIM_NONE; + final boolean canCustomContainer = !isTask || !sDisableCustomTaskAnimationProperty; + final boolean isDream = + isTask && change.getTaskInfo().topActivityType == ACTIVITY_TYPE_DREAM; + int animAttr = 0; + boolean translucent = false; + if (isDream) { + if (type == TRANSIT_OPEN) { + animAttr = enter + ? R.styleable.WindowAnimation_dreamActivityOpenEnterAnimation + : R.styleable.WindowAnimation_dreamActivityOpenExitAnimation; + } else if (type == TRANSIT_CLOSE) { + animAttr = enter + ? 0 + : R.styleable.WindowAnimation_dreamActivityCloseExitAnimation; + } + } else if (wallpaperTransit == WALLPAPER_TRANSITION_INTRA_OPEN) { + animAttr = enter + ? R.styleable.WindowAnimation_wallpaperIntraOpenEnterAnimation + : R.styleable.WindowAnimation_wallpaperIntraOpenExitAnimation; + } else if (wallpaperTransit == WALLPAPER_TRANSITION_INTRA_CLOSE) { + animAttr = enter + ? R.styleable.WindowAnimation_wallpaperIntraCloseEnterAnimation + : R.styleable.WindowAnimation_wallpaperIntraCloseExitAnimation; + } else if (wallpaperTransit == WALLPAPER_TRANSITION_OPEN) { + animAttr = enter + ? R.styleable.WindowAnimation_wallpaperOpenEnterAnimation + : R.styleable.WindowAnimation_wallpaperOpenExitAnimation; + } else if (wallpaperTransit == WALLPAPER_TRANSITION_CLOSE) { + animAttr = enter + ? R.styleable.WindowAnimation_wallpaperCloseEnterAnimation + : R.styleable.WindowAnimation_wallpaperCloseExitAnimation; + } else if (type == TRANSIT_OPEN) { + // We will translucent open animation for translucent activities and tasks. Choose + // WindowAnimation_activityOpenEnterAnimation and set translucent here, then + // TransitionAnimation loads appropriate animation later. + if ((changeFlags & FLAG_TRANSLUCENT) != 0 && enter) { + translucent = true; + } + if (isTask && !translucent) { + animAttr = enter + ? R.styleable.WindowAnimation_taskOpenEnterAnimation + : R.styleable.WindowAnimation_taskOpenExitAnimation; + } else { + animAttr = enter + ? R.styleable.WindowAnimation_activityOpenEnterAnimation + : R.styleable.WindowAnimation_activityOpenExitAnimation; + } + } else if (type == TRANSIT_TO_FRONT) { + animAttr = enter + ? R.styleable.WindowAnimation_taskToFrontEnterAnimation + : R.styleable.WindowAnimation_taskToFrontExitAnimation; + } else if (type == TRANSIT_CLOSE) { + if (isTask) { + animAttr = enter + ? R.styleable.WindowAnimation_taskCloseEnterAnimation + : R.styleable.WindowAnimation_taskCloseExitAnimation; + } else { + if ((changeFlags & FLAG_TRANSLUCENT) != 0 && !enter) { + translucent = true; + } + animAttr = enter + ? R.styleable.WindowAnimation_activityCloseEnterAnimation + : R.styleable.WindowAnimation_activityCloseExitAnimation; + } + } else if (type == TRANSIT_TO_BACK) { + animAttr = enter + ? R.styleable.WindowAnimation_taskToBackEnterAnimation + : R.styleable.WindowAnimation_taskToBackExitAnimation; + } + + Animation a = null; + if (animAttr != 0) { + if (overrideType == ANIM_FROM_STYLE && canCustomContainer) { + a = transitionAnimation + .loadAnimationAttr(options.getPackageName(), options.getAnimations(), + animAttr, translucent); + } else { + a = transitionAnimation.loadDefaultAnimationAttr(animAttr, translucent); + } + } + + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, + "loadAnimation: anim=%s animAttr=0x%x type=%s isEntrance=%b", a, animAttr, + transitTypeToString(type), + enter); + return a; + } + + /** + * Gets the background {@link ColorInt} for the given transition animation if it is set. + * + * @param defaultColor {@link ColorInt} to return if there is no background color specified by + * the given transition animation. + */ + @ColorInt + public static int getTransitionBackgroundColorIfSet(@NonNull TransitionInfo info, + @NonNull TransitionInfo.Change change, @NonNull Animation a, + @ColorInt int defaultColor) { + if (!a.getShowBackdrop()) { + return defaultColor; + } + if (info.getAnimationOptions() != null + && info.getAnimationOptions().getBackgroundColor() != 0) { + // If available use the background color provided through AnimationOptions + return info.getAnimationOptions().getBackgroundColor(); + } else if (a.getBackdropColor() != 0) { + // Otherwise fallback on the background color provided through the animation + // definition. + return a.getBackdropColor(); + } else if (change.getBackgroundColor() != 0) { + // Otherwise default to the window's background color if provided through + // the theme as the background color for the animation - the top most window + // with a valid background color and showBackground set takes precedence. + return change.getBackgroundColor(); + } + return defaultColor; + } + + /** + * Adds the given {@code backgroundColor} as the background color to the transition animation. + */ + public static void addBackgroundToTransition(@NonNull SurfaceControl rootLeash, + @ColorInt int backgroundColor, @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction) { + if (backgroundColor == 0) { + // No background color. + return; + } + final Color bgColor = Color.valueOf(backgroundColor); + final float[] colorArray = new float[] { bgColor.red(), bgColor.green(), bgColor.blue() }; + final SurfaceControl animationBackgroundSurface = new SurfaceControl.Builder() + .setName("Animation Background") + .setParent(rootLeash) + .setColorLayer() + .setOpaque(true) + .build(); + startTransaction + .setLayer(animationBackgroundSurface, Integer.MIN_VALUE) + .setColor(animationBackgroundSurface, colorArray) + .show(animationBackgroundSurface); + finishTransaction.remove(animationBackgroundSurface); + } +} 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 435d67087f34..29d25bc39223 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 @@ -23,6 +23,8 @@ import static android.view.WindowManager.TRANSIT_KEYGUARD_GOING_AWAY; import static android.view.WindowManager.TRANSIT_OPEN; import static android.view.WindowManager.TRANSIT_TO_BACK; import static android.view.WindowManager.TRANSIT_TO_FRONT; +import static android.view.WindowManager.fixScale; +import static android.window.TransitionInfo.FLAG_IS_INPUT_METHOD; import static android.window.TransitionInfo.FLAG_IS_WALLPAPER; import static android.window.TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT; @@ -30,6 +32,8 @@ import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTas import android.annotation.NonNull; import android.annotation.Nullable; +import android.app.ActivityTaskManager; +import android.app.IApplicationThread; import android.content.ContentResolver; import android.content.Context; import android.database.ContentObserver; @@ -56,13 +60,13 @@ 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.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.RemoteCallable; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.TransactionPool; import com.android.wm.shell.common.annotations.ExternalThread; import com.android.wm.shell.protolog.ShellProtoLogGroup; +import com.android.wm.shell.sysui.ShellInit; import java.util.ArrayList; import java.util.Arrays; @@ -97,11 +101,18 @@ public class Transitions implements RemoteCallable<Transitions> { /** Transition type for dismissing split-screen. */ public static final int TRANSIT_SPLIT_DISMISS = TRANSIT_FIRST_CUSTOM + 7; + /** Transition type for freeform to maximize transition. */ + public static final int TRANSIT_MAXIMIZE = WindowManager.TRANSIT_FIRST_CUSTOM + 8; + + /** Transition type for maximize to freeform transition. */ + public static final int TRANSIT_RESTORE_FROM_MAXIMIZE = WindowManager.TRANSIT_FIRST_CUSTOM + 9; + private final WindowOrganizer mOrganizer; private final Context mContext; private final ShellExecutor mMainExecutor; private final ShellExecutor mAnimExecutor; private final TransitionPlayerImpl mPlayerImpl; + private final DefaultTransitionHandler mDefaultTransitionHandler; private final RemoteTransitionHandler mRemoteTransitionHandler; private final DisplayController mDisplayController; private final ShellTransitionImpl mImpl = new ShellTransitionImpl(); @@ -109,6 +120,8 @@ public class Transitions implements RemoteCallable<Transitions> { /** List of possible handlers. Ordered by specificity (eg. tapped back to front). */ private final ArrayList<TransitionHandler> mHandlers = new ArrayList<>(); + private final ArrayList<TransitionObserver> mObservers = new ArrayList<>(); + /** List of {@link Runnable} instances to run when the last active transition has finished. */ private final ArrayList<Runnable> mRunWhenIdleQueue = new ArrayList<>(); @@ -127,8 +140,11 @@ public class Transitions implements RemoteCallable<Transitions> { /** Keeps track of currently playing transitions in the order of receipt. */ private final ArrayList<ActiveTransition> mActiveTransitions = new ArrayList<>(); - public Transitions(@NonNull WindowOrganizer organizer, @NonNull TransactionPool pool, - @NonNull DisplayController displayController, @NonNull Context context, + public Transitions(@NonNull Context context, + @NonNull ShellInit shellInit, + @NonNull WindowOrganizer organizer, + @NonNull TransactionPool pool, + @NonNull DisplayController displayController, @NonNull ShellExecutor mainExecutor, @NonNull Handler mainHandler, @NonNull ShellExecutor animExecutor) { mOrganizer = organizer; @@ -137,33 +153,40 @@ public class Transitions implements RemoteCallable<Transitions> { mAnimExecutor = animExecutor; mDisplayController = displayController; mPlayerImpl = new TransitionPlayerImpl(); + mDefaultTransitionHandler = new DefaultTransitionHandler(context, shellInit, + displayController, pool, mainExecutor, mainHandler, animExecutor); + mRemoteTransitionHandler = new RemoteTransitionHandler(mMainExecutor); + shellInit.addInitCallback(this::onInit, this); + } + + private void onInit() { // The very last handler (0 in the list) should be the default one. - mHandlers.add(new DefaultTransitionHandler(displayController, pool, context, mainExecutor, - mainHandler, animExecutor)); + mHandlers.add(mDefaultTransitionHandler); + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "addHandler: Default"); // Next lowest priority is remote transitions. - mRemoteTransitionHandler = new RemoteTransitionHandler(mainExecutor); mHandlers.add(mRemoteTransitionHandler); + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "addHandler: Remote"); - ContentResolver resolver = context.getContentResolver(); - mTransitionAnimationScaleSetting = Settings.Global.getFloat(resolver, - Settings.Global.TRANSITION_ANIMATION_SCALE, - context.getResources().getFloat( - R.dimen.config_appTransitionAnimationDurationScaleDefault)); + ContentResolver resolver = mContext.getContentResolver(); + mTransitionAnimationScaleSetting = getTransitionAnimationScaleSetting(); dispatchAnimScaleSetting(mTransitionAnimationScaleSetting); resolver.registerContentObserver( Settings.Global.getUriFor(Settings.Global.TRANSITION_ANIMATION_SCALE), false, new SettingsObserver()); + + if (Transitions.ENABLE_SHELL_TRANSITIONS) { + // Register this transition handler with Core + mOrganizer.registerTransitionPlayer(mPlayerImpl); + // Pre-load the instance. + TransitionMetrics.getInstance(); + } } - private Transitions() { - mOrganizer = null; - mContext = null; - mMainExecutor = null; - mAnimExecutor = null; - mDisplayController = null; - mPlayerImpl = null; - mRemoteTransitionHandler = null; + private float getTransitionAnimationScaleSetting() { + return fixScale(Settings.Global.getFloat(mContext.getContentResolver(), + Settings.Global.TRANSITION_ANIMATION_SCALE, mContext.getResources().getFloat( + R.dimen.config_appTransitionAnimationDurationScaleDefault))); } public ShellTransitions asRemoteTransitions() { @@ -186,20 +209,20 @@ public class Transitions implements RemoteCallable<Transitions> { } } - /** Register this transition handler with Core */ - public void register(ShellTaskOrganizer taskOrganizer) { - if (mPlayerImpl == null) return; - taskOrganizer.registerTransitionPlayer(mPlayerImpl); - // Pre-load the instance. - TransitionMetrics.getInstance(); - } - /** * Adds a handler candidate. * @see TransitionHandler */ public void addHandler(@NonNull TransitionHandler handler) { + if (mHandlers.isEmpty()) { + throw new RuntimeException("Unexpected handler added prior to initialization, please " + + "use ShellInit callbacks to ensure proper ordering"); + } mHandlers.add(handler); + // Set initial scale settings. + handler.setAnimScaleSetting(mTransitionAnimationScaleSetting); + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "addHandler: %s", + handler.getClass().getSimpleName()); } public ShellExecutor getMainExecutor() { @@ -227,6 +250,29 @@ public class Transitions implements RemoteCallable<Transitions> { mRemoteTransitionHandler.removeFiltered(remoteTransition); } + /** Registers an observer on the lifecycle of transitions. */ + public void registerObserver(@NonNull TransitionObserver observer) { + mObservers.add(observer); + } + + /** Unregisters the observer. */ + public void unregisterObserver(@NonNull TransitionObserver observer) { + mObservers.remove(observer); + } + + /** Boosts the process priority of remote animation player. */ + public static void setRunningRemoteTransitionDelegate(IApplicationThread appThread) { + if (appThread == null) return; + try { + ActivityTaskManager.getService().setRunningRemoteTransitionDelegate(appThread); + } catch (SecurityException e) { + Log.e(TAG, "Unable to boost animation process. This should only happen" + + " during unit tests"); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + } + /** * Runs the given {@code runnable} when the last active transition has finished, or immediately * if there are currently no active transitions. @@ -287,12 +333,14 @@ public class Transitions implements RemoteCallable<Transitions> { finishT.setAlpha(leash, 1.f); } } else if (mode == TRANSIT_CLOSE || mode == TRANSIT_TO_BACK) { - // Wallpaper is a bit of an anomaly: it's visibility is tied to other WindowStates. - // As a result, we actually can't hide it's WindowToken because there may not be a - // transition associated with it becoming visible again. Fortunately, since it is - // always z-ordered to the back, we don't have to worry about it flickering to the - // front during reparenting, so the hide here isn't necessary for it. - if ((change.getFlags() & FLAG_IS_WALLPAPER) == 0) { + // Wallpaper/IME are anomalies: their visibility is tied to other WindowStates. + // As a result, we actually can't hide their WindowTokens because there may not be a + // transition associated with them becoming visible again. Fortunately, since + // wallpapers are always z-ordered to the back, we don't have to worry about it + // flickering to the front during reparenting. Similarly, the IME is reparented to + // the associated app, so its visibility is coupled. So, an explicit hide is not + // needed visually anyways. + if ((change.getFlags() & (FLAG_IS_WALLPAPER | FLAG_IS_INPUT_METHOD)) == 0) { finishT.hide(leash); } } @@ -309,13 +357,14 @@ public class Transitions implements RemoteCallable<Transitions> { if (info.getRootLeash().isValid()) { t.show(info.getRootLeash()); } + final int numChanges = info.getChanges().size(); // Put animating stuff above this line and put static stuff below it. - int zSplitLine = info.getChanges().size(); + final int zSplitLine = numChanges + 1; // changes should be ordered top-to-bottom in z - for (int i = info.getChanges().size() - 1; i >= 0; --i) { + for (int i = numChanges - 1; i >= 0; --i) { final TransitionInfo.Change change = info.getChanges().get(i); final SurfaceControl leash = change.getLeash(); - final int mode = info.getChanges().get(i).getMode(); + final int mode = change.getMode(); // Don't reparent anything that isn't independent within its parents if (!TransitionInfo.isIndependent(change, info)) { @@ -329,26 +378,31 @@ public class Transitions implements RemoteCallable<Transitions> { t.setPosition(leash, change.getStartAbsBounds().left - info.getRootOffset().x, change.getStartAbsBounds().top - info.getRootOffset().y); } + final int layer; // Put all the OPEN/SHOW on top - if (mode == TRANSIT_OPEN || mode == TRANSIT_TO_FRONT) { + if ((change.getFlags() & FLAG_IS_WALLPAPER) != 0) { + // Wallpaper is always at the bottom. + layer = -zSplitLine; + } else if (mode == TRANSIT_OPEN || mode == TRANSIT_TO_FRONT) { if (isOpening) { // put on top - t.setLayer(leash, zSplitLine + info.getChanges().size() - i); + layer = zSplitLine + numChanges - i; } else { // put on bottom - t.setLayer(leash, zSplitLine - i); + layer = zSplitLine - i; } } else if (mode == TRANSIT_CLOSE || mode == TRANSIT_TO_BACK) { if (isOpening) { // put on bottom and leave visible - t.setLayer(leash, zSplitLine - i); + layer = zSplitLine - i; } else { // put on top - t.setLayer(leash, zSplitLine + info.getChanges().size() - i); + layer = zSplitLine + numChanges - i; } } else { // CHANGE or other - t.setLayer(leash, zSplitLine + info.getChanges().size() - i); + layer = zSplitLine + numChanges - i; } + t.setLayer(leash, layer); } } @@ -371,12 +425,18 @@ public class Transitions implements RemoteCallable<Transitions> { + Arrays.toString(mActiveTransitions.stream().map( activeTransition -> activeTransition.mToken).toArray())); } + + for (int i = 0; i < mObservers.size(); ++i) { + mObservers.get(i).onTransitionReady(transitionToken, info, t, finishT); + } + if (!info.getRootLeash().isValid()) { // Invalid root-leash implies that the transition is empty/no-op, so just do // housekeeping and return. ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Invalid root leash (%s): %s", transitionToken, info); t.apply(); + finishT.apply(); onAbort(transitionToken); return; } @@ -400,6 +460,7 @@ public class Transitions implements RemoteCallable<Transitions> { } if (nonTaskChange && transferStartingWindow) { t.apply(); + finishT.apply(); // Treat this as an abort since we are bypassing any merge logic and effectively // finishing immediately. onAbort(transitionToken); @@ -435,33 +496,46 @@ public class Transitions implements RemoteCallable<Transitions> { playing.mToken, (wct, cb) -> onFinish(merging.mToken, wct, cb)); } - boolean startAnimation(@NonNull ActiveTransition active, TransitionHandler handler) { - return handler.startAnimation(active.mToken, active.mInfo, active.mStartT, active.mFinishT, - (wct, cb) -> onFinish(active.mToken, wct, cb)); - } + private void playTransition(@NonNull ActiveTransition active) { + for (int i = 0; i < mObservers.size(); ++i) { + mObservers.get(i).onTransitionStarting(active.mToken); + } - void playTransition(@NonNull ActiveTransition active) { setupAnimHierarchy(active.mInfo, active.mStartT, active.mFinishT); // If a handler already chose to run this animation, try delegating to it first. if (active.mHandler != null) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " try firstHandler %s", active.mHandler); - if (startAnimation(active, active.mHandler)) { + boolean consumed = active.mHandler.startAnimation(active.mToken, active.mInfo, + active.mStartT, active.mFinishT, (wct, cb) -> onFinish(active.mToken, wct, cb)); + if (consumed) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " animated by firstHandler"); return; } } - // Otherwise give every other handler a chance (in order) + // Otherwise give every other handler a chance + active.mHandler = dispatchTransition(active.mToken, active.mInfo, active.mStartT, + active.mFinishT, (wct, cb) -> onFinish(active.mToken, wct, cb), active.mHandler); + } + + /** + * Gives every handler (in order) a chance to animate until one consumes the transition. + * @return the handler which consumed the transition. + */ + TransitionHandler dispatchTransition(@NonNull IBinder transition, @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction startT, @NonNull SurfaceControl.Transaction finishT, + @NonNull TransitionFinishCallback finishCB, @Nullable TransitionHandler skip) { for (int i = mHandlers.size() - 1; i >= 0; --i) { - if (mHandlers.get(i) == active.mHandler) continue; + if (mHandlers.get(i) == skip) continue; ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " try handler %s", mHandlers.get(i)); - if (startAnimation(active, mHandlers.get(i))) { + boolean consumed = mHandlers.get(i).startAnimation(transition, info, startT, finishT, + finishCB); + if (consumed) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " animated by %s", mHandlers.get(i)); - active.mHandler = mHandlers.get(i); - return; + return mHandlers.get(i); } } throw new IllegalStateException( @@ -496,15 +570,29 @@ public class Transitions implements RemoteCallable<Transitions> { active.mMerged = true; active.mAborted = abort; if (active.mHandler != null) { - active.mHandler.onTransitionMerged(active.mToken); + active.mHandler.onTransitionConsumed( + active.mToken, abort, abort ? null : active.mFinishT); + } + for (int i = 0; i < mObservers.size(); ++i) { + mObservers.get(i).onTransitionMerged( + active.mToken, mActiveTransitions.get(0).mToken); } return; } - mActiveTransitions.get(activeIdx).mAborted = abort; + final ActiveTransition active = mActiveTransitions.get(activeIdx); + active.mAborted = abort; + if (active.mAborted && active.mHandler != null) { + // Notifies to clean-up the aborted transition. + active.mHandler.onTransitionConsumed( + transition, true /* aborted */, null /* finishTransaction */); + } + for (int i = 0; i < mObservers.size(); ++i) { + mObservers.get(i).onTransitionFinished(active.mToken, active.mAborted); + } ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Transition animation finished (abort=%b), notifying core %s", abort, transition); // Merge all relevant transactions together - SurfaceControl.Transaction fullFinish = mActiveTransitions.get(activeIdx).mFinishT; + SurfaceControl.Transaction fullFinish = active.mFinishT; for (int iA = activeIdx + 1; iA < mActiveTransitions.size(); ++iA) { final ActiveTransition toMerge = mActiveTransitions.get(iA); if (!toMerge.mMerged) break; @@ -533,7 +621,15 @@ public class Transitions implements RemoteCallable<Transitions> { while (mActiveTransitions.size() > activeIdx && mActiveTransitions.get(activeIdx).mAborted) { ActiveTransition aborted = mActiveTransitions.remove(activeIdx); + // Notifies to clean-up the aborted transition. + if (aborted.mHandler != null) { + aborted.mHandler.onTransitionConsumed( + transition, true /* aborted */, null /* finishTransaction */); + } mOrganizer.finishTransition(aborted.mToken, null /* wct */, null /* wctCB */); + for (int i = 0; i < mObservers.size(); ++i) { + mObservers.get(i).onTransitionFinished(active.mToken, true); + } } if (mActiveTransitions.size() <= activeIdx) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "All active transition animations " @@ -615,8 +711,9 @@ public class Transitions implements RemoteCallable<Transitions> { if (wct == null) { wct = new WindowContainerTransaction(); } - mDisplayController.getChangeController().dispatchOnRotateDisplay(wct, - change.getDisplayId(), change.getStartRotation(), change.getEndRotation()); + mDisplayController.getChangeController().dispatchOnDisplayChange(wct, + change.getDisplayId(), change.getStartRotation(), change.getEndRotation(), + null /* newDisplayAreaInfo */); } } active.mToken = mOrganizer.startTransition( @@ -714,9 +811,15 @@ public class Transitions implements RemoteCallable<Transitions> { /** * Called when a transition which was already "claimed" by this handler has been merged - * into another animation. Gives this handler a chance to clean-up any expectations. + * into another animation or has been aborted. Gives this handler a chance to clean-up any + * expectations. + * + * @param transition The transition been consumed. + * @param aborted Whether the transition is aborted or not. + * @param finishTransaction The transaction to be applied after the transition animated. */ - default void onTransitionMerged(@NonNull IBinder transition) { } + default void onTransitionConsumed(@NonNull IBinder transition, boolean aborted, + @Nullable SurfaceControl.Transaction finishTransaction) { } /** * Sets transition animation scale settings value to handler. @@ -726,6 +829,52 @@ public class Transitions implements RemoteCallable<Transitions> { default void setAnimScaleSetting(float scale) {} } + /** + * Interface for something that needs to know the lifecycle of some transitions, but never + * handles any transition by itself. + */ + public interface TransitionObserver { + /** + * Called when the transition is ready to play. It may later be merged into other + * transitions. Note this doesn't mean this transition will be played anytime soon. + * + * @param transition the unique token of this transition + * @param startTransaction the transaction given to the handler to be applied before the + * transition animation. This will be applied when the transition + * handler that handles this transition starts the transition. + * @param finishTransaction the transaction given to the handler to be applied after the + * transition animation. The Transition system will apply it when + * finishCallback is called by the transition handler. + */ + void onTransitionReady(@NonNull IBinder transition, @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction startTransaction, + @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); + + /** + * Called when a transition is merged into another transition. There won't be any following + * lifecycle calls for the merged transition. + * + * @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); + + /** + * Called when the transition is finished. This isn't called for merged 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); + } + @BinderThread private class TransitionPlayerImpl extends ITransitionPlayer.Stub { @Override @@ -820,9 +969,7 @@ public class Transitions implements RemoteCallable<Transitions> { @Override public void onChange(boolean selfChange) { super.onChange(selfChange); - mTransitionAnimationScaleSetting = Settings.Global.getFloat( - mContext.getContentResolver(), Settings.Global.TRANSITION_ANIMATION_SCALE, - mTransitionAnimationScaleSetting); + mTransitionAnimationScaleSetting = getTransitionAnimationScaleSetting(); mMainExecutor.execute(() -> dispatchAnimScaleSetting(mTransitionAnimationScaleSetting)); } 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 new file mode 100644 index 000000000000..6b59e313b01b --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldAnimationController.java @@ -0,0 +1,234 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.unfold; + +import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; + +import android.annotation.NonNull; +import android.app.ActivityManager.RunningTaskInfo; +import android.app.TaskInfo; +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.sysui.ShellInit; +import com.android.wm.shell.unfold.ShellUnfoldProgressProvider.UnfoldListener; +import com.android.wm.shell.unfold.animation.UnfoldTaskAnimator; + +import java.util.List; +import java.util.Optional; + +import dagger.Lazy; + +/** + * Manages fold/unfold animations of tasks on foldable devices. + * When folding or unfolding a foldable device we play animations that + * transform task cropping/scaling/rounded corners. + * + * This controller manages: + * 1) Folding/unfolding when Shell transitions disabled + * 2) Folding when Shell transitions enabled, unfolding is managed by + * {@link com.android.wm.shell.unfold.UnfoldTransitionHandler} + */ +public class UnfoldAnimationController implements UnfoldListener { + + private final ShellUnfoldProgressProvider mUnfoldProgressProvider; + private final ShellExecutor mExecutor; + private final TransactionPool mTransactionPool; + private final List<UnfoldTaskAnimator> mAnimators; + private final Lazy<Optional<UnfoldTransitionHandler>> mUnfoldTransitionHandler; + + private final SparseArray<SurfaceControl> mTaskSurfaces = new SparseArray<>(); + private final SparseArray<UnfoldTaskAnimator> mAnimatorsByTaskId = new SparseArray<>(); + + public UnfoldAnimationController( + @NonNull ShellInit shellInit, + @NonNull TransactionPool transactionPool, + @NonNull ShellUnfoldProgressProvider unfoldProgressProvider, + @NonNull List<UnfoldTaskAnimator> animators, + @NonNull Lazy<Optional<UnfoldTransitionHandler>> unfoldTransitionHandler, + @NonNull ShellExecutor executor) { + mUnfoldProgressProvider = unfoldProgressProvider; + mUnfoldTransitionHandler = unfoldTransitionHandler; + mTransactionPool = transactionPool; + mExecutor = executor; + mAnimators = animators; + // TODO(b/238217847): Temporarily add this check here until we can remove the dynamic + // override for this controller from the base module + if (unfoldProgressProvider != ShellUnfoldProgressProvider.NO_PROVIDER) { + shellInit.addInitCallback(this::onInit, this); + } + } + + /** + * Initializes the controller, starts listening for the external events + */ + public void onInit() { + mUnfoldProgressProvider.addListener(mExecutor, this); + + for (int i = 0; i < mAnimators.size(); i++) { + final UnfoldTaskAnimator animator = mAnimators.get(i); + animator.init(); + // TODO(b/238217847): See #provideSplitTaskUnfoldAnimatorBase + mExecutor.executeDelayed(animator::start, 0); + } + } + + /** + * Called when a task appeared + * @param taskInfo info for the appeared task + * @param leash surface leash for the appeared task + */ + public void onTaskAppeared(RunningTaskInfo taskInfo, SurfaceControl leash) { + mTaskSurfaces.put(taskInfo.taskId, leash); + + // Find the first matching animator + for (int i = 0; i < mAnimators.size(); i++) { + final UnfoldTaskAnimator animator = mAnimators.get(i); + if (animator.isApplicableTask(taskInfo)) { + mAnimatorsByTaskId.put(taskInfo.taskId, animator); + animator.onTaskAppeared(taskInfo, leash); + break; + } + } + } + + /** + * Called when task info changed + * @param taskInfo info for the changed task + */ + public void onTaskInfoChanged(RunningTaskInfo taskInfo) { + final UnfoldTaskAnimator animator = mAnimatorsByTaskId.get(taskInfo.taskId); + final boolean isCurrentlyApplicable = animator != null; + + if (isCurrentlyApplicable) { + final boolean isApplicable = animator.isApplicableTask(taskInfo); + if (isApplicable) { + // Still applicable, send update + animator.onTaskChanged(taskInfo); + } else { + // Became inapplicable + resetTask(animator, taskInfo); + animator.onTaskVanished(taskInfo); + mAnimatorsByTaskId.remove(taskInfo.taskId); + } + } else { + // Find the first matching animator + for (int i = 0; i < mAnimators.size(); i++) { + final UnfoldTaskAnimator currentAnimator = mAnimators.get(i); + if (currentAnimator.isApplicableTask(taskInfo)) { + // Became applicable + mAnimatorsByTaskId.put(taskInfo.taskId, currentAnimator); + + SurfaceControl leash = mTaskSurfaces.get(taskInfo.taskId); + currentAnimator.onTaskAppeared(taskInfo, leash); + break; + } + } + } + } + + /** + * Called when a task vanished + * @param taskInfo info for the vanished task + */ + public void onTaskVanished(RunningTaskInfo taskInfo) { + mTaskSurfaces.remove(taskInfo.taskId); + + final UnfoldTaskAnimator animator = mAnimatorsByTaskId.get(taskInfo.taskId); + final boolean isCurrentlyApplicable = animator != null; + + if (isCurrentlyApplicable) { + resetTask(animator, taskInfo); + animator.onTaskVanished(taskInfo); + mAnimatorsByTaskId.remove(taskInfo.taskId); + } + } + + @Override + public void onStateChangeStarted() { + if (mUnfoldTransitionHandler.get().get().willHandleTransition()) { + return; + } + + SurfaceControl.Transaction transaction = null; + for (int i = 0; i < mAnimators.size(); i++) { + final UnfoldTaskAnimator animator = mAnimators.get(i); + if (animator.hasActiveTasks()) { + if (transaction == null) transaction = mTransactionPool.acquire(); + animator.prepareStartTransaction(transaction); + } + } + + if (transaction != null) { + transaction.apply(); + mTransactionPool.release(transaction); + } + } + + @Override + public void onStateChangeProgress(float progress) { + if (mUnfoldTransitionHandler.get().get().willHandleTransition()) { + return; + } + + SurfaceControl.Transaction transaction = null; + for (int i = 0; i < mAnimators.size(); i++) { + final UnfoldTaskAnimator animator = mAnimators.get(i); + if (animator.hasActiveTasks()) { + if (transaction == null) transaction = mTransactionPool.acquire(); + animator.applyAnimationProgress(progress, transaction); + } + } + + if (transaction != null) { + transaction.apply(); + mTransactionPool.release(transaction); + } + } + + @Override + public void onStateChangeFinished() { + if (mUnfoldTransitionHandler.get().get().willHandleTransition()) { + return; + } + + final SurfaceControl.Transaction transaction = mTransactionPool.acquire(); + + for (int i = 0; i < mAnimators.size(); i++) { + final UnfoldTaskAnimator animator = mAnimators.get(i); + animator.resetAllSurfaces(transaction); + animator.prepareFinishTransaction(transaction); + } + + transaction.apply(); + + mTransactionPool.release(transaction); + } + + private void resetTask(UnfoldTaskAnimator animator, TaskInfo taskInfo) { + if (taskInfo.getWindowingMode() == WINDOWING_MODE_PINNED) { + // PiP task has its own cleanup path, ignore surface reset to avoid conflict. + return; + } + final SurfaceControl.Transaction transaction = mTransactionPool.acquire(); + animator.resetSurface(taskInfo, transaction); + transaction.apply(); + mTransactionPool.release(transaction); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldBackgroundController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldBackgroundController.java index 9faf454261d3..86ca292399cb 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldBackgroundController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldBackgroundController.java @@ -79,7 +79,7 @@ public class UnfoldBackgroundController { } private float[] getBackgroundColor(Context context) { - int colorInt = context.getResources().getColor(R.color.unfold_transition_background); + int colorInt = context.getResources().getColor(R.color.taskbar_background); return new float[]{ (float) red(colorInt) / 255.0F, (float) green(colorInt) / 255.0F, 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 639603941c18..5d7b62905d3b 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 @@ -16,8 +16,6 @@ package com.android.wm.shell.unfold; -import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; -import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.view.WindowManager.TRANSIT_CHANGE; import android.os.IBinder; @@ -30,15 +28,24 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.wm.shell.common.TransactionPool; +import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.transition.Transitions; import com.android.wm.shell.transition.Transitions.TransitionFinishCallback; import com.android.wm.shell.transition.Transitions.TransitionHandler; import com.android.wm.shell.unfold.ShellUnfoldProgressProvider.UnfoldListener; +import com.android.wm.shell.unfold.animation.FullscreenUnfoldTaskAnimator; +import com.android.wm.shell.unfold.animation.SplitTaskUnfoldAnimator; +import com.android.wm.shell.unfold.animation.UnfoldTaskAnimator; import java.util.ArrayList; import java.util.List; import java.util.concurrent.Executor; +/** + * Transition handler that is responsible for animating app surfaces when unfolding of foldable + * devices. It does not handle the folding animation, which is done in + * {@link UnfoldAnimationController}. + */ public class UnfoldTransitionHandler implements TransitionHandler, UnfoldListener { private final ShellUnfoldProgressProvider mUnfoldProgressProvider; @@ -51,17 +58,37 @@ public class UnfoldTransitionHandler implements TransitionHandler, UnfoldListene @Nullable private IBinder mTransition; - private final List<TransitionInfo.Change> mAnimatedFullscreenTasks = new ArrayList<>(); + private final List<UnfoldTaskAnimator> mAnimators = new ArrayList<>(); - public UnfoldTransitionHandler(ShellUnfoldProgressProvider unfoldProgressProvider, - TransactionPool transactionPool, Executor executor, Transitions transitions) { + public UnfoldTransitionHandler(ShellInit shellInit, + ShellUnfoldProgressProvider unfoldProgressProvider, + FullscreenUnfoldTaskAnimator fullscreenUnfoldAnimator, + SplitTaskUnfoldAnimator splitUnfoldTaskAnimator, + TransactionPool transactionPool, + Executor executor, + Transitions transitions) { mUnfoldProgressProvider = unfoldProgressProvider; mTransactionPool = transactionPool; mExecutor = executor; mTransitions = transitions; + + mAnimators.add(splitUnfoldTaskAnimator); + mAnimators.add(fullscreenUnfoldAnimator); + // TODO(b/238217847): Temporarily add this check here until we can remove the dynamic + // override for this controller from the base module + if (unfoldProgressProvider != ShellUnfoldProgressProvider.NO_PROVIDER + && Transitions.ENABLE_SHELL_TRANSITIONS) { + shellInit.addInitCallback(this::onInit, this); + } } - public void init() { + /** + * Called when the transition handler is initialized. + */ + public void onInit() { + for (int i = 0; i < mAnimators.size(); i++) { + mAnimators.get(i).init(); + } mTransitions.addHandler(this); mUnfoldProgressProvider.addListener(mExecutor, this); } @@ -71,49 +98,69 @@ public class UnfoldTransitionHandler implements TransitionHandler, UnfoldListene @NonNull SurfaceControl.Transaction startTransaction, @NonNull SurfaceControl.Transaction finishTransaction, @NonNull TransitionFinishCallback finishCallback) { - if (transition != mTransition) return false; - startTransaction.apply(); - - mAnimatedFullscreenTasks.clear(); - info.getChanges().forEach(change -> { - final boolean allowedToAnimate = change.getTaskInfo() != null - && change.getTaskInfo().getWindowingMode() == WINDOWING_MODE_FULLSCREEN - && change.getTaskInfo().getActivityType() != ACTIVITY_TYPE_HOME - && change.getMode() == TRANSIT_CHANGE; - - if (allowedToAnimate) { - mAnimatedFullscreenTasks.add(change); + for (int i = 0; i < mAnimators.size(); i++) { + final UnfoldTaskAnimator animator = mAnimators.get(i); + animator.clearTasks(); + + info.getChanges().forEach(change -> { + if (change.getTaskInfo() != null + && change.getMode() == TRANSIT_CHANGE + && animator.isApplicableTask(change.getTaskInfo())) { + animator.onTaskAppeared(change.getTaskInfo(), change.getLeash()); + } + }); + + if (animator.hasActiveTasks()) { + animator.prepareStartTransaction(startTransaction); + animator.prepareFinishTransaction(finishTransaction); + animator.start(); } - }); + } + startTransaction.apply(); mFinishCallback = finishCallback; - mTransition = null; return true; } @Override public void onStateChangeProgress(float progress) { - mAnimatedFullscreenTasks.forEach(change -> { - final SurfaceControl.Transaction transaction = mTransactionPool.acquire(); + if (mTransition == null) return; - // TODO: this is a placeholder animation, replace with a spec version in the next CLs - final float testScale = 0.8f + 0.2f * progress; - transaction.setScale(change.getLeash(), testScale, testScale); + SurfaceControl.Transaction transaction = null; + for (int i = 0; i < mAnimators.size(); i++) { + final UnfoldTaskAnimator animator = mAnimators.get(i); + + if (animator.hasActiveTasks()) { + if (transaction == null) { + transaction = mTransactionPool.acquire(); + } + + animator.applyAnimationProgress(progress, transaction); + } + } + + if (transaction != null) { transaction.apply(); mTransactionPool.release(transaction); - }); + } } @Override public void onStateChangeFinished() { - if (mFinishCallback != null) { - mFinishCallback.onTransitionFinished(null, null); - mFinishCallback = null; - mAnimatedFullscreenTasks.clear(); + if (mFinishCallback == null) return; + + for (int i = 0; i < mAnimators.size(); i++) { + final UnfoldTaskAnimator animator = mAnimators.get(i); + animator.clearTasks(); + animator.stop(); } + + mFinishCallback.onTransitionFinished(null, null); + mFinishCallback = null; + mTransition = null; } @Nullable @@ -127,4 +174,8 @@ public class UnfoldTransitionHandler implements TransitionHandler, UnfoldListene } return null; } + + public boolean willHandleTransition() { + return mTransition != null; + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenUnfoldController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/animation/FullscreenUnfoldTaskAnimator.java index aa3868cfca84..eab82f00e962 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenUnfoldController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/animation/FullscreenUnfoldTaskAnimator.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021 The Android Open Source Project + * 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. @@ -14,16 +14,16 @@ * limitations under the License. */ -package com.android.wm.shell.fullscreen; +package com.android.wm.shell.unfold.animation; -import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; +import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; +import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.util.MathUtils.lerp; import static android.view.Display.DEFAULT_DISPLAY; import android.animation.RectEvaluator; import android.animation.TypeEvaluator; import android.annotation.NonNull; -import android.app.ActivityManager; import android.app.TaskInfo; import android.content.Context; import android.graphics.Matrix; @@ -32,22 +32,26 @@ import android.util.SparseArray; import android.view.InsetsSource; import android.view.InsetsState; import android.view.SurfaceControl; +import android.view.SurfaceControl.Transaction; import com.android.internal.policy.ScreenDecorationsUtils; import com.android.wm.shell.common.DisplayInsetsController; -import com.android.wm.shell.common.DisplayInsetsController.OnInsetsChangedListener; -import com.android.wm.shell.unfold.ShellUnfoldProgressProvider; -import com.android.wm.shell.unfold.ShellUnfoldProgressProvider.UnfoldListener; +import com.android.wm.shell.unfold.UnfoldAnimationController; import com.android.wm.shell.unfold.UnfoldBackgroundController; -import java.util.concurrent.Executor; - /** - * Controls full screen app unfold transition: animating cropping window and scaling when - * folding or unfolding a foldable device. + * This helper class contains logic that calculates scaling and cropping parameters + * for the folding/unfolding animation. As an input it receives TaskInfo objects and + * surfaces leashes and as an output it could fill surface transactions with required + * transformations. + * + * This class is used by + * {@link com.android.wm.shell.unfold.UnfoldTransitionHandler} and + * {@link UnfoldAnimationController}. They use independent + * instances of FullscreenUnfoldTaskAnimator. */ -public final class FullscreenUnfoldController implements UnfoldListener, - OnInsetsChangedListener { +public class FullscreenUnfoldTaskAnimator implements UnfoldTaskAnimator, + DisplayInsetsController.OnInsetsChangedListener { private static final float[] FLOAT_9 = new float[9]; private static final TypeEvaluator<Rect> RECT_EVALUATOR = new RectEvaluator(new Rect()); @@ -57,49 +61,77 @@ public final class FullscreenUnfoldController implements UnfoldListener, private static final float END_SCALE = 1f; private static final float START_SCALE = END_SCALE - VERTICAL_START_MARGIN * 2; - private final Executor mExecutor; - private final ShellUnfoldProgressProvider mProgressProvider; - private final DisplayInsetsController mDisplayInsetsController; - private final SparseArray<AnimationContext> mAnimationContextByTaskId = new SparseArray<>(); + private final int mExpandedTaskBarHeight; + private final float mWindowCornerRadiusPx; + private final DisplayInsetsController mDisplayInsetsController; private final UnfoldBackgroundController mBackgroundController; private InsetsSource mTaskbarInsetsSource; - private final float mWindowCornerRadiusPx; - private final float mExpandedTaskBarHeight; - - private final SurfaceControl.Transaction mTransaction = new SurfaceControl.Transaction(); - - public FullscreenUnfoldController( - @NonNull Context context, - @NonNull Executor executor, + public FullscreenUnfoldTaskAnimator(Context context, @NonNull UnfoldBackgroundController backgroundController, - @NonNull ShellUnfoldProgressProvider progressProvider, - @NonNull DisplayInsetsController displayInsetsController - ) { - mExecutor = executor; - mProgressProvider = progressProvider; + DisplayInsetsController displayInsetsController) { mDisplayInsetsController = displayInsetsController; - mWindowCornerRadiusPx = ScreenDecorationsUtils.getWindowCornerRadius(context); + mBackgroundController = backgroundController; mExpandedTaskBarHeight = context.getResources().getDimensionPixelSize( com.android.internal.R.dimen.taskbar_frame_height); - mBackgroundController = backgroundController; + mWindowCornerRadiusPx = ScreenDecorationsUtils.getWindowCornerRadius(context); } - /** - * Initializes the controller - */ public void init() { - mProgressProvider.addListener(mExecutor, this); mDisplayInsetsController.addInsetsChangedListener(DEFAULT_DISPLAY, this); } @Override - public void onStateChangeProgress(float progress) { - if (mAnimationContextByTaskId.size() == 0) return; + public void insetsChanged(InsetsState insetsState) { + mTaskbarInsetsSource = insetsState.getSource(InsetsState.ITYPE_EXTRA_NAVIGATION_BAR); + for (int i = mAnimationContextByTaskId.size() - 1; i >= 0; i--) { + AnimationContext context = mAnimationContextByTaskId.valueAt(i); + context.update(mTaskbarInsetsSource, context.mTaskInfo); + } + } + + public boolean hasActiveTasks() { + return mAnimationContextByTaskId.size() > 0; + } + + @Override + public void onTaskAppeared(TaskInfo taskInfo, SurfaceControl leash) { + AnimationContext animationContext = new AnimationContext(leash, mTaskbarInsetsSource, + taskInfo); + mAnimationContextByTaskId.put(taskInfo.taskId, animationContext); + } + + @Override + public void onTaskChanged(TaskInfo taskInfo) { + AnimationContext animationContext = mAnimationContextByTaskId.get(taskInfo.taskId); + if (animationContext != null) { + animationContext.update(mTaskbarInsetsSource, taskInfo); + } + } + + @Override + public void onTaskVanished(TaskInfo taskInfo) { + mAnimationContextByTaskId.remove(taskInfo.taskId); + } - mBackgroundController.ensureBackground(mTransaction); + @Override + public void clearTasks() { + mAnimationContextByTaskId.clear(); + } + + @Override + public void resetSurface(TaskInfo taskInfo, Transaction transaction) { + final AnimationContext context = mAnimationContextByTaskId.get(taskInfo.taskId); + if (context != null) { + resetSurface(context, transaction); + } + } + + @Override + public void applyAnimationProgress(float progress, Transaction transaction) { + if (mAnimationContextByTaskId.size() == 0) return; for (int i = mAnimationContextByTaskId.size() - 1; i >= 0; i--) { final AnimationContext context = mAnimationContextByTaskId.valueAt(i); @@ -111,75 +143,41 @@ public final class FullscreenUnfoldController implements UnfoldListener, context.mMatrix.setScale(scale, scale, context.mCurrentCropRect.exactCenterX(), context.mCurrentCropRect.exactCenterY()); - mTransaction.setWindowCrop(context.mLeash, context.mCurrentCropRect) + transaction.setWindowCrop(context.mLeash, context.mCurrentCropRect) .setMatrix(context.mLeash, context.mMatrix, FLOAT_9) - .setCornerRadius(context.mLeash, mWindowCornerRadiusPx); + .setCornerRadius(context.mLeash, mWindowCornerRadiusPx) + .show(context.mLeash); } - - mTransaction.apply(); } @Override - public void onStateChangeFinished() { - for (int i = mAnimationContextByTaskId.size() - 1; i >= 0; i--) { - final AnimationContext context = mAnimationContextByTaskId.valueAt(i); - resetSurface(context); - } - - mBackgroundController.removeBackground(mTransaction); - mTransaction.apply(); + public void prepareStartTransaction(Transaction transaction) { + mBackgroundController.ensureBackground(transaction); } @Override - public void insetsChanged(InsetsState insetsState) { - mTaskbarInsetsSource = insetsState.getSource(InsetsState.ITYPE_EXTRA_NAVIGATION_BAR); - for (int i = mAnimationContextByTaskId.size() - 1; i >= 0; i--) { - AnimationContext context = mAnimationContextByTaskId.valueAt(i); - context.update(mTaskbarInsetsSource, context.mTaskInfo); - } + public void prepareFinishTransaction(Transaction transaction) { + mBackgroundController.removeBackground(transaction); } - /** - * Called when a new matching task appeared - */ - public void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash) { - AnimationContext animationContext = new AnimationContext(leash, mTaskbarInsetsSource, - taskInfo); - mAnimationContextByTaskId.put(taskInfo.taskId, animationContext); - } - - /** - * Called when matching task changed - */ - public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) { - AnimationContext animationContext = mAnimationContextByTaskId.get(taskInfo.taskId); - if (animationContext != null) { - animationContext.update(mTaskbarInsetsSource, taskInfo); - } + @Override + public boolean isApplicableTask(TaskInfo taskInfo) { + return taskInfo != null && taskInfo.isVisible() + && taskInfo.realActivity != null // to filter out parents created by organizer + && taskInfo.getWindowingMode() == WINDOWING_MODE_FULLSCREEN + && taskInfo.getActivityType() != ACTIVITY_TYPE_HOME; } - /** - * Called when matching task vanished - */ - public void onTaskVanished(ActivityManager.RunningTaskInfo taskInfo) { - AnimationContext animationContext = mAnimationContextByTaskId.get(taskInfo.taskId); - if (animationContext != null) { - // PiP task has its own cleanup path, ignore surface reset to avoid conflict. - if (taskInfo.getWindowingMode() != WINDOWING_MODE_PINNED) { - resetSurface(animationContext); - } - mAnimationContextByTaskId.remove(taskInfo.taskId); - } - - if (mAnimationContextByTaskId.size() == 0) { - mBackgroundController.removeBackground(mTransaction); + @Override + public void resetAllSurfaces(Transaction transaction) { + for (int i = mAnimationContextByTaskId.size() - 1; i >= 0; i--) { + final AnimationContext context = mAnimationContextByTaskId.valueAt(i); + resetSurface(context, transaction); } - - mTransaction.apply(); } - private void resetSurface(AnimationContext context) { - mTransaction + private void resetSurface(AnimationContext context, Transaction transaction) { + transaction .setWindowCrop(context.mLeash, null) .setCornerRadius(context.mLeash, 0.0F) .setMatrix(context.mLeash, 1.0F, 0.0F, 0.0F, 1.0F) @@ -197,10 +195,9 @@ public final class FullscreenUnfoldController implements UnfoldListener, TaskInfo mTaskInfo; - private AnimationContext(SurfaceControl leash, - InsetsSource taskBarInsetsSource, - TaskInfo taskInfo) { - this.mLeash = leash; + private AnimationContext(SurfaceControl leash, InsetsSource taskBarInsetsSource, + TaskInfo taskInfo) { + mLeash = leash; update(taskBarInsetsSource, taskInfo); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskUnfoldController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/animation/SplitTaskUnfoldAnimator.java index 59eecb5db136..6e10ebe94c5d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskUnfoldController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/animation/SplitTaskUnfoldAnimator.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021 The Android Open Source Project + * 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. @@ -14,17 +14,19 @@ * limitations under the License. */ -package com.android.wm.shell.splitscreen; +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.splitscreen.SplitScreen.STAGE_TYPE_MAIN; +import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_UNDEFINED; import android.animation.RectEvaluator; import android.animation.TypeEvaluator; -import android.annotation.NonNull; -import android.app.ActivityManager; +import android.app.TaskInfo; import android.content.Context; import android.graphics.Insets; import android.graphics.Rect; @@ -32,67 +34,130 @@ import android.util.SparseArray; import android.view.InsetsSource; import android.view.InsetsState; import android.view.SurfaceControl; +import android.view.SurfaceControl.Transaction; import com.android.internal.policy.ScreenDecorationsUtils; import com.android.wm.shell.common.DisplayInsetsController; -import com.android.wm.shell.common.DisplayInsetsController.OnInsetsChangedListener; -import com.android.wm.shell.common.TransactionPool; import com.android.wm.shell.common.split.SplitScreenConstants.SplitPosition; -import com.android.wm.shell.unfold.ShellUnfoldProgressProvider; -import com.android.wm.shell.unfold.ShellUnfoldProgressProvider.UnfoldListener; +import com.android.wm.shell.splitscreen.SplitScreen; +import com.android.wm.shell.splitscreen.SplitScreen.SplitScreenListener; +import com.android.wm.shell.splitscreen.SplitScreenController; +import com.android.wm.shell.unfold.UnfoldAnimationController; import com.android.wm.shell.unfold.UnfoldBackgroundController; +import java.util.Optional; import java.util.concurrent.Executor; +import dagger.Lazy; + /** - * Controls transformations of the split screen task surfaces in response - * to the unfolding/folding action on foldable devices + * This helper class contains logic that calculates scaling and cropping parameters + * for the folding/unfolding animation. As an input it receives TaskInfo objects and + * surfaces leashes and as an output it could fill surface transactions with required + * transformations. + * + * This class is used by + * {@link com.android.wm.shell.unfold.UnfoldTransitionHandler} and + * {@link UnfoldAnimationController}. + * They use independent instances of SplitTaskUnfoldAnimator. */ -public class StageTaskUnfoldController implements UnfoldListener, OnInsetsChangedListener { +public class SplitTaskUnfoldAnimator implements UnfoldTaskAnimator, + DisplayInsetsController.OnInsetsChangedListener, SplitScreenListener { private static final TypeEvaluator<Rect> RECT_EVALUATOR = new RectEvaluator(new Rect()); private static final float CROPPING_START_MARGIN_FRACTION = 0.05f; - private final SparseArray<AnimationContext> mAnimationContextByTaskId = new SparseArray<>(); - private final ShellUnfoldProgressProvider mUnfoldProgressProvider; - private final DisplayInsetsController mDisplayInsetsController; - private final UnfoldBackgroundController mBackgroundController; private final Executor mExecutor; + private final DisplayInsetsController mDisplayInsetsController; + private final SparseArray<AnimationContext> mAnimationContextByTaskId = new SparseArray<>(); private final int mExpandedTaskBarHeight; private final float mWindowCornerRadiusPx; - private final Rect mStageBounds = new Rect(); - private final TransactionPool mTransactionPool; + private final Lazy<Optional<SplitScreenController>> mSplitScreenController; + private final UnfoldBackgroundController mUnfoldBackgroundController; + + private final Rect mMainStageBounds = new Rect(); + private final Rect mSideStageBounds = new Rect(); + private final Rect mRootStageBounds = new Rect(); private InsetsSource mTaskbarInsetsSource; - private boolean mBothStagesVisible; - - public StageTaskUnfoldController(@NonNull Context context, - @NonNull TransactionPool transactionPool, - @NonNull ShellUnfoldProgressProvider unfoldProgressProvider, - @NonNull DisplayInsetsController displayInsetsController, - @NonNull UnfoldBackgroundController backgroundController, - @NonNull Executor executor) { - mUnfoldProgressProvider = unfoldProgressProvider; - mTransactionPool = transactionPool; - mExecutor = executor; - mBackgroundController = backgroundController; + + @SplitPosition + private int mMainStagePosition = SPLIT_POSITION_UNDEFINED; + @SplitPosition + private int mSideStagePosition = SPLIT_POSITION_UNDEFINED; + + public SplitTaskUnfoldAnimator(Context context, Executor executor, + Lazy<Optional<SplitScreenController>> splitScreenController, + UnfoldBackgroundController unfoldBackgroundController, + DisplayInsetsController displayInsetsController) { mDisplayInsetsController = displayInsetsController; - mWindowCornerRadiusPx = ScreenDecorationsUtils.getWindowCornerRadius(context); + mExecutor = executor; + mUnfoldBackgroundController = unfoldBackgroundController; + mSplitScreenController = splitScreenController; mExpandedTaskBarHeight = context.getResources().getDimensionPixelSize( com.android.internal.R.dimen.taskbar_frame_height); + mWindowCornerRadiusPx = ScreenDecorationsUtils.getWindowCornerRadius(context); } - /** - * Initializes the controller, starts listening for the external events - */ + /** Initializes the animator, this should be called only once */ + @Override public void init() { - mUnfoldProgressProvider.addListener(mExecutor, this); mDisplayInsetsController.addInsetsChangedListener(DEFAULT_DISPLAY, this); } + /** + * Starts listening for split-screen changes and gets initial split-screen + * layout information through the listener + */ + @Override + public void start() { + mSplitScreenController.get().get().asSplitScreen() + .registerSplitScreenListener(this, mExecutor); + } + + /** + * Stops listening for the split-screen layout changes + */ + @Override + public void stop() { + mSplitScreenController.get().get().asSplitScreen() + .unregisterSplitScreenListener(this); + } + @Override public void insetsChanged(InsetsState insetsState) { mTaskbarInsetsSource = insetsState.getSource(InsetsState.ITYPE_EXTRA_NAVIGATION_BAR); + updateContexts(); + } + + @Override + public void onTaskStageChanged(int taskId, int stage, boolean visible) { + final AnimationContext context = mAnimationContextByTaskId.get(taskId); + if (context != null) { + context.mStageType = stage; + context.update(); + } + } + + @Override + public void onStagePositionChanged(int stage, int position) { + if (stage == STAGE_TYPE_MAIN) { + mMainStagePosition = position; + } else { + mSideStagePosition = position; + } + updateContexts(); + } + + @Override + public void onSplitBoundsChanged(Rect rootBounds, Rect mainBounds, Rect sideBounds) { + mRootStageBounds.set(rootBounds); + mMainStageBounds.set(mainBounds); + mSideStageBounds.set(sideBounds); + updateContexts(); + } + + private void updateContexts() { for (int i = mAnimationContextByTaskId.size() - 1; i >= 0; i--) { AnimationContext context = mAnimationContextByTaskId.valueAt(i); context.update(); @@ -100,44 +165,73 @@ public class StageTaskUnfoldController implements UnfoldListener, OnInsetsChange } /** - * Called when split screen task appeared - * @param taskInfo info for the appeared task - * @param leash surface leash for the appeared task + * Register a split task in the animator + * @param taskInfo info of the task + * @param leash the surface of the task */ - public void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash) { - // Only handle child task surface here. - if (!taskInfo.hasParentTask()) return; - + @Override + public void onTaskAppeared(TaskInfo taskInfo, SurfaceControl leash) { AnimationContext context = new AnimationContext(leash); mAnimationContextByTaskId.put(taskInfo.taskId, context); } /** - * Called when a split screen task vanished - * @param taskInfo info for the vanished task + * Unregister the task from the unfold animation + * @param taskInfo info of the task + */ + @Override + public void onTaskVanished(TaskInfo taskInfo) { + mAnimationContextByTaskId.remove(taskInfo.taskId); + } + + @Override + public boolean isApplicableTask(TaskInfo taskInfo) { + return taskInfo.hasParentTask() + && taskInfo.isVisible + && taskInfo.realActivity != null // to filter out parents created by organizer + && taskInfo.getWindowingMode() == WINDOWING_MODE_MULTI_WINDOW; + } + + /** + * Clear all registered tasks */ - public void onTaskVanished(ActivityManager.RunningTaskInfo taskInfo) { - if (!taskInfo.hasParentTask()) return; + @Override + public void clearTasks() { + mAnimationContextByTaskId.clear(); + } + /** + * Reset transformations of the task that could have been applied by the animator + * @param taskInfo task to reset + * @param transaction a transaction to write the changes to + */ + @Override + public void resetSurface(TaskInfo taskInfo, Transaction transaction) { AnimationContext context = mAnimationContextByTaskId.get(taskInfo.taskId); if (context != null) { - final SurfaceControl.Transaction transaction = mTransactionPool.acquire(); resetSurface(transaction, context); - transaction.apply(); - mTransactionPool.release(transaction); } - mAnimationContextByTaskId.remove(taskInfo.taskId); } + /** + * Reset all surface transformation that could have been introduced by the animator + * @param transaction to write changes to + */ @Override - public void onStateChangeProgress(float progress) { - if (mAnimationContextByTaskId.size() == 0 || !mBothStagesVisible) return; - - final SurfaceControl.Transaction transaction = mTransactionPool.acquire(); - mBackgroundController.ensureBackground(transaction); + public void resetAllSurfaces(Transaction transaction) { + for (int i = mAnimationContextByTaskId.size() - 1; i >= 0; i--) { + final AnimationContext context = mAnimationContextByTaskId.valueAt(i); + resetSurface(transaction, context); + } + } + @Override + public void applyAnimationProgress(float progress, Transaction transaction) { for (int i = mAnimationContextByTaskId.size() - 1; i >= 0; i--) { AnimationContext context = mAnimationContextByTaskId.valueAt(i); + if (context.mStageType == STAGE_TYPE_UNDEFINED) { + continue; + } context.mCurrentCropRect.set(RECT_EVALUATOR .evaluate(progress, context.mStartCropRect, context.mEndCropRect)); @@ -145,53 +239,25 @@ public class StageTaskUnfoldController implements UnfoldListener, OnInsetsChange transaction.setWindowCrop(context.mLeash, context.mCurrentCropRect) .setCornerRadius(context.mLeash, mWindowCornerRadiusPx); } - - transaction.apply(); - - mTransactionPool.release(transaction); } @Override - public void onStateChangeFinished() { - resetTransformations(); + public void prepareStartTransaction(Transaction transaction) { + mUnfoldBackgroundController.ensureBackground(transaction); + mSplitScreenController.get().get().updateSplitScreenSurfaces(transaction); } - /** - * Called when split screen visibility changes - * @param bothStagesVisible true if both stages of the split screen are visible - */ - public void onSplitVisibilityChanged(boolean bothStagesVisible) { - mBothStagesVisible = bothStagesVisible; - if (!bothStagesVisible) { - resetTransformations(); - } + @Override + public void prepareFinishTransaction(Transaction transaction) { + mUnfoldBackgroundController.removeBackground(transaction); } /** - * Called when split screen stage bounds changed - * @param bounds new bounds for this stage + * @return true if there are tasks to animate */ - public void onLayoutChanged(Rect bounds, @SplitPosition int splitPosition, - boolean isLandscape) { - mStageBounds.set(bounds); - - for (int i = mAnimationContextByTaskId.size() - 1; i >= 0; i--) { - final AnimationContext context = mAnimationContextByTaskId.valueAt(i); - context.update(splitPosition, isLandscape); - } - } - - private void resetTransformations() { - final SurfaceControl.Transaction transaction = mTransactionPool.acquire(); - - for (int i = mAnimationContextByTaskId.size() - 1; i >= 0; i--) { - final AnimationContext context = mAnimationContextByTaskId.valueAt(i); - resetSurface(transaction, context); - } - mBackgroundController.removeBackground(transaction); - transaction.apply(); - - mTransactionPool.release(transaction); + @Override + public boolean hasActiveTasks() { + return mAnimationContextByTaskId.size() > 0; } private void resetSurface(SurfaceControl.Transaction transaction, AnimationContext context) { @@ -202,26 +268,24 @@ public class StageTaskUnfoldController implements UnfoldListener, OnInsetsChange private class AnimationContext { final SurfaceControl mLeash; + final Rect mStartCropRect = new Rect(); final Rect mEndCropRect = new Rect(); final Rect mCurrentCropRect = new Rect(); - private @SplitPosition int mSplitPosition = SPLIT_POSITION_UNDEFINED; - private boolean mIsLandscape = false; + @SplitScreen.StageType + int mStageType = STAGE_TYPE_UNDEFINED; private AnimationContext(SurfaceControl leash) { - this.mLeash = leash; - update(); - } - - private void update(@SplitPosition int splitPosition, boolean isLandscape) { - this.mSplitPosition = splitPosition; - this.mIsLandscape = isLandscape; + mLeash = leash; update(); } private void update() { - mStartCropRect.set(mStageBounds); + final Rect stageBounds = mStageType == STAGE_TYPE_MAIN + ? mMainStageBounds : mSideStageBounds; + + mStartCropRect.set(stageBounds); boolean taskbarExpanded = isTaskbarExpanded(); if (taskbarExpanded) { @@ -239,7 +303,8 @@ public class StageTaskUnfoldController implements UnfoldListener, OnInsetsChange // Sides adjacent to split bar or task bar are not be animated. Insets margins; - if (mIsLandscape) { // Left and right splits. + final boolean isLandscape = mRootStageBounds.width() > mRootStageBounds.height(); + if (isLandscape) { // Left and right splits. margins = getLandscapeMargins(margin, taskbarExpanded); } else { // Top and bottom splits. margins = getPortraitMargins(margin, taskbarExpanded); @@ -251,7 +316,9 @@ public class StageTaskUnfoldController implements UnfoldListener, OnInsetsChange int left = margin; int right = margin; int bottom = taskbarExpanded ? 0 : margin; // Taskbar margin. - if (mSplitPosition == SPLIT_POSITION_TOP_OR_LEFT) { + final int splitPosition = mStageType == STAGE_TYPE_MAIN + ? mMainStagePosition : mSideStagePosition; + if (splitPosition == SPLIT_POSITION_TOP_OR_LEFT) { right = 0; // Divider margin. } else { left = 0; // Divider margin. @@ -262,7 +329,9 @@ public class StageTaskUnfoldController implements UnfoldListener, OnInsetsChange private Insets getPortraitMargins(int margin, boolean taskbarExpanded) { int bottom = margin; int top = margin; - if (mSplitPosition == SPLIT_POSITION_TOP_OR_LEFT) { + final int splitPosition = mStageType == STAGE_TYPE_MAIN + ? mMainStagePosition : mSideStagePosition; + if (splitPosition == SPLIT_POSITION_TOP_OR_LEFT) { bottom = 0; // Divider margin. } else { // Bottom split. top = 0; // Divider margin. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/animation/UnfoldTaskAnimator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/animation/UnfoldTaskAnimator.java new file mode 100644 index 000000000000..e1e366301b46 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/animation/UnfoldTaskAnimator.java @@ -0,0 +1,117 @@ +/* + * 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.unfold.animation; + +import android.app.TaskInfo; +import android.view.SurfaceControl; +import android.view.SurfaceControl.Transaction; + +/** + * Interface for classes that handle animations of tasks when folding or unfolding + * foldable devices. + */ +public interface UnfoldTaskAnimator { + /** + * Initializes the animator, this should be called once in the lifetime of the animator + */ + default void init() {} + + /** + * Starts the animator, it might start listening for some events from the system. + * Applying animation should be done only when animator is started. + * Animator could be started/stopped several times. + */ + default void start() {} + + /** + * Stops the animator, it could unsubscribe from system events. + */ + default void stop() {} + + /** + * If this method returns true then task updates will be propagated to + * the animator using the onTaskAppeared/Changed/Vanished callbacks. + * @return true if this task should be animated by this animator + */ + default boolean isApplicableTask(TaskInfo taskInfo) { + return false; + } + + /** + * Called whenever a task applicable to this animator appeared + * (isApplicableTask returns true for this task) + * + * @param taskInfo info of the appeared task + * @param leash surface of the task + */ + default void onTaskAppeared(TaskInfo taskInfo, SurfaceControl leash) {} + + /** + * Called whenever a task applicable to this animator changed + * @param taskInfo info of the changed task + */ + default void onTaskChanged(TaskInfo taskInfo) {} + + /** + * Called whenever a task applicable to this animator vanished + * @param taskInfo info of the vanished task + */ + default void onTaskVanished(TaskInfo taskInfo) {} + + /** + * @return true if there tasks that could be potentially animated + */ + default boolean hasActiveTasks() { + return false; + } + + /** + * Clears all registered tasks in the animator + */ + default void clearTasks() {} + + /** + * Apply task surfaces transformations based on the current unfold progress + * @param progress unfold transition progress + * @param transaction to write changes to + */ + default void applyAnimationProgress(float progress, Transaction transaction) {} + + /** + * Apply task surfaces transformations that should be set before starting the animation + * @param transaction to write changes to + */ + default void prepareStartTransaction(Transaction transaction) {} + + /** + * Apply task surfaces transformations that should be set after finishing the animation + * @param transaction to write changes to + */ + default void prepareFinishTransaction(Transaction transaction) {} + + /** + * Resets task surface to its initial transformation + * @param transaction to write changes to + */ + default void resetSurface(TaskInfo taskInfo, Transaction transaction) {} + + /** + * Resets all task surfaces to their initial transformations + * @param transaction to write changes to + */ + default void resetAllSurfaces(Transaction transaction) {} +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDrop.java b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/qualifier/UnfoldShellTransition.java index edeff6e37182..4c868305dcdb 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDrop.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/qualifier/UnfoldShellTransition.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 The Android Open Source Project + * 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. @@ -14,21 +14,16 @@ * limitations under the License. */ -package com.android.wm.shell.draganddrop; +package com.android.wm.shell.unfold.qualifier; -import android.content.res.Configuration; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; -import com.android.wm.shell.common.annotations.ExternalThread; +import javax.inject.Qualifier; /** - * Interface for telling DragAndDrop stuff. + * Indicates that this class is used for the shell unfold transition */ -@ExternalThread -public interface DragAndDrop { - - /** Called when the theme changes. */ - void onThemeChanged(); - - /** Called when the configuration changes. */ - void onConfigChanged(Configuration newConfig); -} +@Qualifier +@Retention(RetentionPolicy.RUNTIME) +public @interface UnfoldShellTransition {} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/ISplitScreenListener.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/qualifier/UnfoldTransition.java index 46e4299f99fa..4d2b3e6f899b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/ISplitScreenListener.aidl +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/qualifier/UnfoldTransition.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021 The Android Open Source Project + * 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. @@ -14,20 +14,17 @@ * limitations under the License. */ -package com.android.wm.shell.stagesplit; +package com.android.wm.shell.unfold.qualifier; -/** - * Listener interface that Launcher attaches to SystemUI to get split-screen callbacks. - */ -oneway interface ISplitScreenListener { +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; - /** - * Called when the stage position changes. - */ - void onStagePositionChanged(int stage, int position); +import javax.inject.Qualifier; - /** - * Called when a task changes stages. - */ - void onTaskStageChanged(int taskId, int stage, boolean visible); -}
\ No newline at end of file +/** + * Indicates that this class is used for unfold transition implemented + * without using Shell transitions + */ +@Qualifier +@Retention(RetentionPolicy.RUNTIME) +public @interface UnfoldTransition {} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/util/GroupedRecentTaskInfo.java b/libs/WindowManager/Shell/src/com/android/wm/shell/util/GroupedRecentTaskInfo.java index 603d05d78fc0..c045cebdf4e0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/util/GroupedRecentTaskInfo.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/util/GroupedRecentTaskInfo.java @@ -16,6 +16,7 @@ package com.android.wm.shell.util; +import android.annotation.IntDef; import android.app.ActivityManager; import android.app.WindowConfiguration; import android.os.Parcel; @@ -24,40 +25,143 @@ import android.os.Parcelable; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import java.util.Arrays; +import java.util.List; + /** * Simple container for recent tasks. May contain either a single or pair of tasks. */ public class GroupedRecentTaskInfo implements Parcelable { - public @NonNull ActivityManager.RecentTaskInfo mTaskInfo1; - public @Nullable ActivityManager.RecentTaskInfo mTaskInfo2; - public @Nullable StagedSplitBounds mStagedSplitBounds; - public GroupedRecentTaskInfo(@NonNull ActivityManager.RecentTaskInfo task1) { - this(task1, null, null); + public static final int TYPE_SINGLE = 1; + public static final int TYPE_SPLIT = 2; + public static final int TYPE_FREEFORM = 3; + + @IntDef(prefix = {"TYPE_"}, value = { + TYPE_SINGLE, + TYPE_SPLIT, + TYPE_FREEFORM + }) + public @interface GroupType {} + + @NonNull + private final ActivityManager.RecentTaskInfo[] mTasks; + @Nullable + private final SplitBounds mSplitBounds; + @GroupType + private final int mType; + + /** + * Create new for a single task + */ + public static GroupedRecentTaskInfo forSingleTask( + @NonNull ActivityManager.RecentTaskInfo task) { + return new GroupedRecentTaskInfo(new ActivityManager.RecentTaskInfo[]{task}, null, + TYPE_SINGLE); + } + + /** + * Create new for a pair of tasks in split screen + */ + 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); } - public GroupedRecentTaskInfo(@NonNull ActivityManager.RecentTaskInfo task1, - @Nullable ActivityManager.RecentTaskInfo task2, - @Nullable StagedSplitBounds stagedSplitBounds) { - mTaskInfo1 = task1; - mTaskInfo2 = task2; - mStagedSplitBounds = stagedSplitBounds; + /** + * Create new for a group of freeform tasks + */ + public static GroupedRecentTaskInfo forFreeformTasks( + @NonNull ActivityManager.RecentTaskInfo... tasks) { + return new GroupedRecentTaskInfo(tasks, null, TYPE_FREEFORM); + } + + private GroupedRecentTaskInfo(@NonNull ActivityManager.RecentTaskInfo[] tasks, + @Nullable SplitBounds splitBounds, @GroupType int type) { + mTasks = tasks; + mSplitBounds = splitBounds; + mType = type; } GroupedRecentTaskInfo(Parcel parcel) { - mTaskInfo1 = parcel.readTypedObject(ActivityManager.RecentTaskInfo.CREATOR); - mTaskInfo2 = parcel.readTypedObject(ActivityManager.RecentTaskInfo.CREATOR); - mStagedSplitBounds = parcel.readTypedObject(StagedSplitBounds.CREATOR); + mTasks = parcel.createTypedArray(ActivityManager.RecentTaskInfo.CREATOR); + mSplitBounds = parcel.readTypedObject(SplitBounds.CREATOR); + mType = parcel.readInt(); + } + + /** + * Get primary {@link ActivityManager.RecentTaskInfo} + */ + @NonNull + public ActivityManager.RecentTaskInfo getTaskInfo1() { + return mTasks[0]; + } + + /** + * Get secondary {@link ActivityManager.RecentTaskInfo}. + * + * Used in split screen. + */ + @Nullable + public ActivityManager.RecentTaskInfo getTaskInfo2() { + if (mTasks.length > 1) { + return mTasks[1]; + } + return null; + } + + /** + * Get all {@link ActivityManager.RecentTaskInfo}s grouped together. + */ + @NonNull + public List<ActivityManager.RecentTaskInfo> getTaskInfoList() { + return Arrays.asList(mTasks); + } + + /** + * Return {@link SplitBounds} if this is a split screen entry or {@code null} + */ + @Nullable + public SplitBounds getSplitBounds() { + return mSplitBounds; + } + + /** + * Get type of this recents entry. One of {@link GroupType} + */ + @GroupType + public int getType() { + return mType; } @Override public String toString() { - String taskString = "Task1: " + getTaskInfo(mTaskInfo1) - + ", Task2: " + getTaskInfo(mTaskInfo2); - if (mStagedSplitBounds != null) { - taskString += ", SplitBounds: " + mStagedSplitBounds.toString(); + StringBuilder taskString = new StringBuilder(); + for (int i = 0; i < mTasks.length; i++) { + if (i == 0) { + taskString.append("Task"); + } else { + taskString.append(", Task"); + } + taskString.append(i + 1).append(": ").append(getTaskInfo(mTasks[i])); + } + if (mSplitBounds != null) { + taskString.append(", SplitBounds: ").append(mSplitBounds); + } + taskString.append(", Type="); + switch (mType) { + case TYPE_SINGLE: + taskString.append("TYPE_SINGLE"); + break; + case TYPE_SPLIT: + taskString.append("TYPE_SPLIT"); + break; + case TYPE_FREEFORM: + taskString.append("TYPE_FREEFORM"); + break; } - return taskString; + return taskString.toString(); } private String getTaskInfo(ActivityManager.RecentTaskInfo taskInfo) { @@ -74,9 +178,9 @@ public class GroupedRecentTaskInfo implements Parcelable { @Override public void writeToParcel(Parcel parcel, int flags) { - parcel.writeTypedObject(mTaskInfo1, flags); - parcel.writeTypedObject(mTaskInfo2, flags); - parcel.writeTypedObject(mStagedSplitBounds, flags); + parcel.writeTypedArray(mTasks, flags); + parcel.writeTypedObject(mSplitBounds, flags); + parcel.writeInt(mType); } @Override diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/util/StagedSplitBounds.java b/libs/WindowManager/Shell/src/com/android/wm/shell/util/SplitBounds.java index a0c84cc33ebd..e90389764af3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/util/StagedSplitBounds.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/util/SplitBounds.java @@ -25,7 +25,7 @@ import java.util.Objects; * Container of various information needed to display split screen * tasks/leashes/etc in Launcher */ -public class StagedSplitBounds implements Parcelable { +public class SplitBounds implements Parcelable { public final Rect leftTopBounds; public final Rect rightBottomBounds; /** This rect represents the actual gap between the two apps */ @@ -43,7 +43,7 @@ public class StagedSplitBounds implements Parcelable { public final int leftTopTaskId; public final int rightBottomTaskId; - public StagedSplitBounds(Rect leftTopBounds, Rect rightBottomBounds, + public SplitBounds(Rect leftTopBounds, Rect rightBottomBounds, int leftTopTaskId, int rightBottomTaskId) { this.leftTopBounds = leftTopBounds; this.rightBottomBounds = rightBottomBounds; @@ -66,7 +66,7 @@ public class StagedSplitBounds implements Parcelable { topTaskPercent = this.leftTopBounds.height() / (float) rightBottomBounds.bottom; } - public StagedSplitBounds(Parcel parcel) { + public SplitBounds(Parcel parcel) { leftTopBounds = parcel.readTypedObject(Rect.CREATOR); rightBottomBounds = parcel.readTypedObject(Rect.CREATOR); visualDividerBounds = parcel.readTypedObject(Rect.CREATOR); @@ -96,11 +96,11 @@ public class StagedSplitBounds implements Parcelable { @Override public boolean equals(Object obj) { - if (!(obj instanceof StagedSplitBounds)) { + if (!(obj instanceof SplitBounds)) { return false; } // Only need to check the base fields (the other fields are derived from these) - final StagedSplitBounds other = (StagedSplitBounds) obj; + final SplitBounds other = (SplitBounds) obj; return Objects.equals(leftTopBounds, other.leftTopBounds) && Objects.equals(rightBottomBounds, other.rightBottomBounds) && leftTopTaskId == other.leftTopTaskId @@ -120,15 +120,15 @@ public class StagedSplitBounds implements Parcelable { + "AppsVertical? " + appsStackedVertically; } - public static final Creator<StagedSplitBounds> CREATOR = new Creator<StagedSplitBounds>() { + public static final Creator<SplitBounds> CREATOR = new Creator<SplitBounds>() { @Override - public StagedSplitBounds createFromParcel(Parcel in) { - return new StagedSplitBounds(in); + public SplitBounds createFromParcel(Parcel in) { + return new SplitBounds(in); } @Override - public StagedSplitBounds[] newArray(int size) { - return new StagedSplitBounds[size]; + public SplitBounds[] newArray(int size) { + return new SplitBounds[size]; } }; } 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 new file mode 100644 index 000000000000..9e49b51e1504 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java @@ -0,0 +1,260 @@ +/* + * 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.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_UNDEFINED; + +import android.app.ActivityManager; +import android.app.ActivityManager.RunningTaskInfo; +import android.app.ActivityTaskManager; +import android.content.Context; +import android.os.Handler; +import android.view.Choreographer; +import android.view.MotionEvent; +import android.view.SurfaceControl; +import android.view.View; +import android.window.WindowContainerToken; +import android.window.WindowContainerTransaction; + +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.SyncTransactionQueue; +import com.android.wm.shell.desktopmode.DesktopModeController; +import com.android.wm.shell.desktopmode.DesktopModeStatus; +import com.android.wm.shell.freeform.FreeformTaskTransitionStarter; +import com.android.wm.shell.transition.Transitions; + +/** + * View model for the window decoration with a caption and shadows. Works with + * {@link CaptionWindowDecoration}. + */ +public class CaptionWindowDecorViewModel implements WindowDecorViewModel<CaptionWindowDecoration> { + private final ActivityTaskManager mActivityTaskManager; + private final ShellTaskOrganizer mTaskOrganizer; + private final Context mContext; + private final Handler mMainHandler; + private final Choreographer mMainChoreographer; + private final DisplayController mDisplayController; + private final SyncTransactionQueue mSyncQueue; + private FreeformTaskTransitionStarter mTransitionStarter; + private DesktopModeController mDesktopModeController; + + public CaptionWindowDecorViewModel( + Context context, + Handler mainHandler, + Choreographer mainChoreographer, + ShellTaskOrganizer taskOrganizer, + DisplayController displayController, + SyncTransactionQueue syncQueue, + DesktopModeController desktopModeController) { + mContext = context; + mMainHandler = mainHandler; + mMainChoreographer = mainChoreographer; + mActivityTaskManager = mContext.getSystemService(ActivityTaskManager.class); + mTaskOrganizer = taskOrganizer; + mDisplayController = displayController; + mSyncQueue = syncQueue; + mDesktopModeController = desktopModeController; + } + + @Override + public void setFreeformTaskTransitionStarter(FreeformTaskTransitionStarter transitionStarter) { + mTransitionStarter = transitionStarter; + } + + @Override + public CaptionWindowDecoration createWindowDecoration( + ActivityManager.RunningTaskInfo taskInfo, + SurfaceControl taskSurface, + SurfaceControl.Transaction startT, + SurfaceControl.Transaction finishT) { + if (!shouldShowWindowDecor(taskInfo)) return null; + final CaptionWindowDecoration windowDecoration = new CaptionWindowDecoration( + mContext, + mDisplayController, + mTaskOrganizer, + taskInfo, + taskSurface, + mMainHandler, + mMainChoreographer, + mSyncQueue); + TaskPositioner taskPositioner = new TaskPositioner(mTaskOrganizer, windowDecoration); + CaptionTouchEventListener touchEventListener = + new CaptionTouchEventListener(taskInfo, taskPositioner); + windowDecoration.setCaptionListeners(touchEventListener, touchEventListener); + windowDecoration.setDragResizeCallback(taskPositioner); + setupWindowDecorationForTransition(taskInfo, startT, finishT, windowDecoration); + setupCaptionColor(taskInfo, windowDecoration); + return windowDecoration; + } + + @Override + public CaptionWindowDecoration adoptWindowDecoration(AutoCloseable windowDecor) { + if (!(windowDecor instanceof CaptionWindowDecoration)) return null; + final CaptionWindowDecoration captionWindowDecor = (CaptionWindowDecoration) windowDecor; + if (!shouldShowWindowDecor(captionWindowDecor.mTaskInfo)) { + return null; + } + return captionWindowDecor; + } + + @Override + public void onTaskInfoChanged(RunningTaskInfo taskInfo, CaptionWindowDecoration decoration) { + decoration.relayout(taskInfo); + + setupCaptionColor(taskInfo, decoration); + } + + private void setupCaptionColor(RunningTaskInfo taskInfo, CaptionWindowDecoration decoration) { + int statusBarColor = taskInfo.taskDescription.getStatusBarColor(); + decoration.setCaptionColor(statusBarColor); + } + + @Override + public void setupWindowDecorationForTransition( + RunningTaskInfo taskInfo, + SurfaceControl.Transaction startT, + SurfaceControl.Transaction finishT, + CaptionWindowDecoration decoration) { + decoration.relayout(taskInfo, startT, finishT); + } + + private class CaptionTouchEventListener implements + View.OnClickListener, View.OnTouchListener { + + private final int mTaskId; + private final WindowContainerToken mTaskToken; + private final DragResizeCallback mDragResizeCallback; + + private int mDragPointerId = -1; + + private CaptionTouchEventListener( + RunningTaskInfo taskInfo, + DragResizeCallback dragResizeCallback) { + mTaskId = taskInfo.taskId; + mTaskToken = taskInfo.token; + mDragResizeCallback = dragResizeCallback; + } + + @Override + public void onClick(View v) { + final int id = v.getId(); + if (id == R.id.close_window) { + WindowContainerTransaction wct = new WindowContainerTransaction(); + wct.removeTask(mTaskToken); + if (Transitions.ENABLE_SHELL_TRANSITIONS) { + mTransitionStarter.startRemoveTransition(wct); + } else { + mSyncQueue.queue(wct); + } + } else if (id == R.id.maximize_window) { + WindowContainerTransaction wct = new WindowContainerTransaction(); + RunningTaskInfo taskInfo = mTaskOrganizer.getRunningTaskInfo(mTaskId); + int targetWindowingMode = taskInfo.getWindowingMode() != WINDOWING_MODE_FULLSCREEN + ? WINDOWING_MODE_FULLSCREEN : WINDOWING_MODE_FREEFORM; + int displayWindowingMode = + taskInfo.configuration.windowConfiguration.getDisplayWindowingMode(); + wct.setWindowingMode(mTaskToken, + targetWindowingMode == displayWindowingMode + ? WINDOWING_MODE_UNDEFINED : targetWindowingMode); + if (targetWindowingMode == WINDOWING_MODE_FULLSCREEN) { + wct.setBounds(mTaskToken, null); + } + if (Transitions.ENABLE_SHELL_TRANSITIONS) { + mTransitionStarter.startWindowingModeTransition(targetWindowingMode, wct); + } else { + mSyncQueue.queue(wct); + } + } else if (id == R.id.minimize_window) { + WindowContainerTransaction wct = new WindowContainerTransaction(); + wct.reorder(mTaskToken, false); + if (Transitions.ENABLE_SHELL_TRANSITIONS) { + mTransitionStarter.startMinimizedModeTransition(wct); + } else { + mSyncQueue.queue(wct); + } + } + } + + @Override + public boolean onTouch(View v, MotionEvent e) { + if (v.getId() != R.id.caption) { + return false; + } + handleEventForMove(e); + + if (e.getAction() != MotionEvent.ACTION_DOWN) { + return false; + } + RunningTaskInfo taskInfo = mTaskOrganizer.getRunningTaskInfo(mTaskId); + if (taskInfo.isFocused) { + return false; + } + WindowContainerTransaction wct = new WindowContainerTransaction(); + wct.reorder(mTaskToken, true /* onTop */); + mSyncQueue.queue(wct); + return true; + } + + private void handleEventForMove(MotionEvent e) { + RunningTaskInfo taskInfo = mTaskOrganizer.getRunningTaskInfo(mTaskId); + int windowingMode = mDesktopModeController + .getDisplayAreaWindowingMode(taskInfo.displayId); + if (windowingMode == WINDOWING_MODE_FULLSCREEN) { + return; + } + switch (e.getActionMasked()) { + case MotionEvent.ACTION_DOWN: + mDragPointerId = e.getPointerId(0); + mDragResizeCallback.onDragResizeStart( + 0 /* ctrlType */, e.getRawX(0), e.getRawY(0)); + break; + case MotionEvent.ACTION_MOVE: { + int dragPointerIdx = e.findPointerIndex(mDragPointerId); + mDragResizeCallback.onDragResizeMove( + e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx)); + break; + } + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: { + int dragPointerIdx = e.findPointerIndex(mDragPointerId); + int statusBarHeight = mDisplayController.getDisplayLayout(taskInfo.displayId) + .stableInsets().top; + mDragResizeCallback.onDragResizeEnd( + e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx)); + if (e.getRawY(dragPointerIdx) <= statusBarHeight + && windowingMode == WINDOWING_MODE_FREEFORM) { + mDesktopModeController.setDesktopModeActive(false); + } + break; + } + } + } + } + + private boolean shouldShowWindowDecor(RunningTaskInfo taskInfo) { + if (taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM) return true; + return DesktopModeStatus.IS_SUPPORTED + && taskInfo.getActivityType() == ACTIVITY_TYPE_STANDARD + && mDisplayController.getDisplayContext(taskInfo.displayId) + .getResources().getConfiguration().smallestScreenWidthDp >= 600; + } +} 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 new file mode 100644 index 000000000000..733f6b7d5dbf --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java @@ -0,0 +1,217 @@ +/* + * 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.windowdecor; + +import android.app.ActivityManager; +import android.app.WindowConfiguration; +import android.content.Context; +import android.content.res.ColorStateList; +import android.graphics.Color; +import android.graphics.Rect; +import android.graphics.drawable.GradientDrawable; +import android.graphics.drawable.VectorDrawable; +import android.os.Handler; +import android.view.Choreographer; +import android.view.SurfaceControl; +import android.view.View; +import android.window.WindowContainerTransaction; + +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.SyncTransactionQueue; +import com.android.wm.shell.desktopmode.DesktopModeStatus; + +/** + * Defines visuals and behaviors of a window decoration of a caption bar and shadows. It works with + * {@link CaptionWindowDecorViewModel}. The caption bar contains maximize and close buttons. + * + * {@link CaptionWindowDecorViewModel} can change the color of the caption bar based on the foremost + * app's request through {@link #setCaptionColor(int)}, in which it changes the foreground color of + * caption buttons according to the luminance of the background. + * + * The shadow's thickness is 20dp when the window is in focus and 5dp when the window isn't. + */ +public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearLayout> { + // The thickness of shadows of a window that has focus in DIP. + private static final int DECOR_SHADOW_FOCUSED_THICKNESS_IN_DIP = 20; + // The thickness of shadows of a window that doesn't have focus in DIP. + private static final int DECOR_SHADOW_UNFOCUSED_THICKNESS_IN_DIP = 5; + + // Height of button (32dp) + 2 * margin (5dp each) + private static final int DECOR_CAPTION_HEIGHT_IN_DIP = 42; + private static final int RESIZE_HANDLE_IN_DIP = 30; + + private static final Rect EMPTY_OUTSET = new Rect(); + private static final Rect RESIZE_HANDLE_OUTSET = new Rect( + RESIZE_HANDLE_IN_DIP, RESIZE_HANDLE_IN_DIP, RESIZE_HANDLE_IN_DIP, RESIZE_HANDLE_IN_DIP); + + private final Handler mHandler; + private final Choreographer mChoreographer; + private final SyncTransactionQueue mSyncQueue; + + private View.OnClickListener mOnCaptionButtonClickListener; + private View.OnTouchListener mOnCaptionTouchListener; + private DragResizeCallback mDragResizeCallback; + + private DragResizeInputListener mDragResizeListener; + + private final WindowDecoration.RelayoutResult<WindowDecorLinearLayout> mResult = + new WindowDecoration.RelayoutResult<>(); + + CaptionWindowDecoration( + Context context, + DisplayController displayController, + ShellTaskOrganizer taskOrganizer, + ActivityManager.RunningTaskInfo taskInfo, + SurfaceControl taskSurface, + Handler handler, + Choreographer choreographer, + SyncTransactionQueue syncQueue) { + super(context, displayController, taskOrganizer, taskInfo, taskSurface); + + mHandler = handler; + mChoreographer = choreographer; + mSyncQueue = syncQueue; + } + + void setCaptionListeners( + View.OnClickListener onCaptionButtonClickListener, + View.OnTouchListener onCaptionTouchListener) { + mOnCaptionButtonClickListener = onCaptionButtonClickListener; + mOnCaptionTouchListener = onCaptionTouchListener; + } + + void setDragResizeCallback(DragResizeCallback dragResizeCallback) { + mDragResizeCallback = dragResizeCallback; + } + + @Override + void relayout(ActivityManager.RunningTaskInfo taskInfo) { + final SurfaceControl.Transaction t = new SurfaceControl.Transaction(); + relayout(taskInfo, t, t); + mSyncQueue.runInSync(transaction -> { + transaction.merge(t); + t.close(); + }); + } + + void relayout(ActivityManager.RunningTaskInfo taskInfo, + SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT) { + final int shadowRadiusDp = taskInfo.isFocused + ? DECOR_SHADOW_FOCUSED_THICKNESS_IN_DIP : DECOR_SHADOW_UNFOCUSED_THICKNESS_IN_DIP; + final boolean isFreeform = mTaskInfo.configuration.windowConfiguration.getWindowingMode() + == WindowConfiguration.WINDOWING_MODE_FREEFORM; + final boolean isDragResizeable = isFreeform && mTaskInfo.isResizeable; + final Rect outset = isDragResizeable ? RESIZE_HANDLE_OUTSET : EMPTY_OUTSET; + + WindowDecorLinearLayout oldRootView = mResult.mRootView; + final SurfaceControl oldDecorationSurface = mDecorationContainerSurface; + final WindowContainerTransaction wct = new WindowContainerTransaction(); + relayout(taskInfo, R.layout.caption_window_decoration, oldRootView, + DECOR_CAPTION_HEIGHT_IN_DIP, outset, shadowRadiusDp, startT, finishT, wct, mResult); + taskInfo = null; // Clear it just in case we use it accidentally + + mTaskOrganizer.applyTransaction(wct); + + 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; + } + if (oldRootView != mResult.mRootView) { + setupRootView(); + } + + if (!isDragResizeable) { + closeDragResizeListener(); + return; + } + + if (oldDecorationSurface != mDecorationContainerSurface || mDragResizeListener == null) { + closeDragResizeListener(); + mDragResizeListener = new DragResizeInputListener( + mContext, + mHandler, + mChoreographer, + mDisplay.getDisplayId(), + mDecorationContainerSurface, + mDragResizeCallback); + } + + mDragResizeListener.setGeometry( + mResult.mWidth, mResult.mHeight, (int) (mResult.mDensity * RESIZE_HANDLE_IN_DIP)); + } + + /** + * Sets up listeners when a new root view is created. + */ + private void setupRootView() { + View caption = mResult.mRootView.findViewById(R.id.caption); + caption.setOnTouchListener(mOnCaptionTouchListener); + View maximize = caption.findViewById(R.id.maximize_window); + if (DesktopModeStatus.IS_SUPPORTED) { + // Hide maximize button when desktop mode is available + maximize.setVisibility(View.GONE); + } else { + maximize.setVisibility(View.VISIBLE); + maximize.setOnClickListener(mOnCaptionButtonClickListener); + } + View close = caption.findViewById(R.id.close_window); + close.setOnClickListener(mOnCaptionButtonClickListener); + View minimize = caption.findViewById(R.id.minimize_window); + minimize.setOnClickListener(mOnCaptionButtonClickListener); + } + + void setCaptionColor(int captionColor) { + if (mResult.mRootView == null) { + return; + } + + View caption = mResult.mRootView.findViewById(R.id.caption); + GradientDrawable captionDrawable = (GradientDrawable) caption.getBackground(); + captionDrawable.setColor(captionColor); + + int buttonTintColorRes = + Color.valueOf(captionColor).luminance() < 0.5 + ? R.color.decor_button_light_color + : R.color.decor_button_dark_color; + ColorStateList buttonTintColor = + caption.getResources().getColorStateList(buttonTintColorRes, null /* theme */); + View maximize = caption.findViewById(R.id.maximize_window); + VectorDrawable maximizeBackground = (VectorDrawable) maximize.getBackground(); + maximizeBackground.setTintList(buttonTintColor); + + View close = caption.findViewById(R.id.close_window); + VectorDrawable closeBackground = (VectorDrawable) close.getBackground(); + closeBackground.setTintList(buttonTintColor); + } + + private void closeDragResizeListener() { + if (mDragResizeListener == null) { + return; + } + mDragResizeListener.close(); + mDragResizeListener = null; + } + + @Override + public void close() { + closeDragResizeListener(); + super.close(); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeCallback.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeCallback.java new file mode 100644 index 000000000000..ee160a15df19 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeCallback.java @@ -0,0 +1,46 @@ +/* + * 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.windowdecor; + +/** + * Callback called when receiving drag-resize or drag-move related input events. + */ +public interface DragResizeCallback { + /** + * Called when a drag resize starts. + * + * @param ctrlType {@link TaskPositioner.CtrlType} indicating the direction of resizing, use + * {@code 0} to indicate it's a move + * @param x x coordinate in window decoration coordinate system where the drag resize starts + * @param y y coordinate in window decoration coordinate system where the drag resize starts + */ + void onDragResizeStart(@TaskPositioner.CtrlType int ctrlType, float x, float y); + + /** + * Called when the pointer moves during a drag resize. + * @param x x coordinate in window decoration coordinate system of the new pointer location + * @param y y coordinate in window decoration coordinate system of the new pointer location + */ + void onDragResizeMove(float x, float y); + + /** + * Called when a drag resize stops. + * @param x x coordinate in window decoration coordinate system where the drag resize stops + * @param y y coordinate in window decoration coordinate system where the drag resize stops + */ + void onDragResizeEnd(float x, float y); +} 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 new file mode 100644 index 000000000000..3d014959a952 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java @@ -0,0 +1,295 @@ +/* + * 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.windowdecor; + +import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; +import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY; +import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION; + +import android.content.Context; +import android.graphics.Rect; +import android.graphics.Region; +import android.hardware.input.InputManager; +import android.os.Binder; +import android.os.Handler; +import android.os.IBinder; +import android.os.RemoteException; +import android.view.Choreographer; +import android.view.IWindowSession; +import android.view.InputChannel; +import android.view.InputEvent; +import android.view.InputEventReceiver; +import android.view.MotionEvent; +import android.view.PointerIcon; +import android.view.SurfaceControl; +import android.view.WindowManagerGlobal; + +import com.android.internal.view.BaseIWindow; + +/** + * An input event listener registered to InputDispatcher to receive input events on task edges and + * convert them to drag resize requests. + */ +class DragResizeInputListener implements AutoCloseable { + private static final String TAG = "DragResizeInputListener"; + + private final IWindowSession mWindowSession = WindowManagerGlobal.getWindowSession(); + private final Handler mHandler; + private final Choreographer mChoreographer; + private final InputManager mInputManager; + + private final int mDisplayId; + private final BaseIWindow mFakeWindow; + private final IBinder mFocusGrantToken; + private final SurfaceControl mDecorationSurface; + private final InputChannel mInputChannel; + private final TaskResizeInputEventReceiver mInputEventReceiver; + private final com.android.wm.shell.windowdecor.DragResizeCallback mCallback; + + private int mWidth; + private int mHeight; + private int mResizeHandleThickness; + + private int mDragPointerId = -1; + + DragResizeInputListener( + Context context, + Handler handler, + Choreographer choreographer, + int displayId, + SurfaceControl decorationSurface, + DragResizeCallback callback) { + mInputManager = context.getSystemService(InputManager.class); + mHandler = handler; + mChoreographer = choreographer; + mDisplayId = displayId; + mDecorationSurface = decorationSurface; + // Use a fake window as the backing surface is a container layer and we don't want to create + // a buffer layer for it so we can't use ViewRootImpl. + mFakeWindow = new BaseIWindow(); + mFakeWindow.setSession(mWindowSession); + mFocusGrantToken = new Binder(); + mInputChannel = new InputChannel(); + try { + mWindowSession.grantInputChannel( + mDisplayId, + mDecorationSurface, + mFakeWindow, + null /* hostInputToken */, + FLAG_NOT_FOCUSABLE, + PRIVATE_FLAG_TRUSTED_OVERLAY, + TYPE_APPLICATION, + mFocusGrantToken, + TAG + " of " + decorationSurface.toString(), + mInputChannel); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + + mInputEventReceiver = new TaskResizeInputEventReceiver( + mInputChannel, mHandler, mChoreographer); + mCallback = callback; + } + + /** + * Updates geometry of this drag resize handler. Needs to be called every time there is a size + * change to notify the input event receiver it's ready to take the next input event. Otherwise + * it'll keep batching move events and the drag resize process is stalled. + * + * This is also used to update the touch regions of this handler every event dispatched here is + * a potential resize request. + * + * @param width The width of the drag resize handler in pixels, including resize handle + * thickness. That is task width + 2 * resize handle thickness. + * @param height The height of the drag resize handler in pixels, including resize handle + * thickness. That is task height + 2 * resize handle thickness. + * @param resizeHandleThickness The thickness of the resize handle in pixels. + */ + void setGeometry(int width, int height, int resizeHandleThickness) { + if (mWidth == width && mHeight == height + && mResizeHandleThickness == resizeHandleThickness) { + return; + } + + mWidth = width; + mHeight = height; + mResizeHandleThickness = resizeHandleThickness; + + Region touchRegion = new Region(); + final Rect topInputBounds = new Rect(0, 0, mWidth, mResizeHandleThickness); + touchRegion.union(topInputBounds); + + final Rect leftInputBounds = new Rect(0, mResizeHandleThickness, + mResizeHandleThickness, mHeight - mResizeHandleThickness); + touchRegion.union(leftInputBounds); + + final Rect rightInputBounds = new Rect( + mWidth - mResizeHandleThickness, mResizeHandleThickness, + mWidth, mHeight - mResizeHandleThickness); + touchRegion.union(rightInputBounds); + + final Rect bottomInputBounds = new Rect(0, mHeight - mResizeHandleThickness, + mWidth, mHeight); + touchRegion.union(bottomInputBounds); + + try { + mWindowSession.updateInputChannel( + mInputChannel.getToken(), + mDisplayId, + mDecorationSurface, + FLAG_NOT_FOCUSABLE, + PRIVATE_FLAG_TRUSTED_OVERLAY, + touchRegion); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + } + + @Override + public void close() { + mInputChannel.dispose(); + try { + mWindowSession.remove(mFakeWindow); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + } + + private class TaskResizeInputEventReceiver extends InputEventReceiver { + private final Choreographer mChoreographer; + private final Runnable mConsumeBatchEventRunnable; + private boolean mConsumeBatchEventScheduled; + + private TaskResizeInputEventReceiver( + InputChannel inputChannel, Handler handler, Choreographer choreographer) { + super(inputChannel, handler.getLooper()); + mChoreographer = choreographer; + + mConsumeBatchEventRunnable = () -> { + mConsumeBatchEventScheduled = false; + if (consumeBatchedInputEvents(mChoreographer.getFrameTimeNanos())) { + // If we consumed a batch here, we want to go ahead and schedule the + // consumption of batched input events on the next frame. Otherwise, we would + // wait until we have more input events pending and might get starved by other + // things occurring in the process. + scheduleConsumeBatchEvent(); + } + }; + } + + @Override + public void onBatchedInputEventPending(int source) { + scheduleConsumeBatchEvent(); + } + + private void scheduleConsumeBatchEvent() { + if (mConsumeBatchEventScheduled) { + return; + } + mChoreographer.postCallback( + Choreographer.CALLBACK_INPUT, mConsumeBatchEventRunnable, null); + mConsumeBatchEventScheduled = true; + } + + @Override + public void onInputEvent(InputEvent inputEvent) { + finishInputEvent(inputEvent, handleInputEvent(inputEvent)); + } + + private boolean handleInputEvent(InputEvent inputEvent) { + if (!(inputEvent instanceof MotionEvent)) { + return false; + } + + MotionEvent e = (MotionEvent) inputEvent; + switch (e.getActionMasked()) { + case MotionEvent.ACTION_DOWN: { + mDragPointerId = e.getPointerId(0); + mCallback.onDragResizeStart( + calculateCtrlType(e.getX(0), e.getY(0)), e.getRawX(0), e.getRawY(0)); + break; + } + case MotionEvent.ACTION_MOVE: { + int dragPointerIndex = e.findPointerIndex(mDragPointerId); + mCallback.onDragResizeMove( + e.getRawX(dragPointerIndex), e.getRawY(dragPointerIndex)); + break; + } + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: { + int dragPointerIndex = e.findPointerIndex(mDragPointerId); + mCallback.onDragResizeEnd( + e.getRawX(dragPointerIndex), e.getRawY(dragPointerIndex)); + mDragPointerId = -1; + break; + } + case MotionEvent.ACTION_HOVER_ENTER: + case MotionEvent.ACTION_HOVER_MOVE: { + updateCursorType(e.getXCursorPosition(), e.getYCursorPosition()); + break; + } + case MotionEvent.ACTION_HOVER_EXIT: + mInputManager.setPointerIconType(PointerIcon.TYPE_DEFAULT); + break; + } + return true; + } + + @TaskPositioner.CtrlType + private int calculateCtrlType(float x, float y) { + int ctrlType = 0; + if (x < mResizeHandleThickness) { + ctrlType |= TaskPositioner.CTRL_TYPE_LEFT; + } + if (x > mWidth - mResizeHandleThickness) { + ctrlType |= TaskPositioner.CTRL_TYPE_RIGHT; + } + if (y < mResizeHandleThickness) { + ctrlType |= TaskPositioner.CTRL_TYPE_TOP; + } + if (y > mHeight - mResizeHandleThickness) { + ctrlType |= TaskPositioner.CTRL_TYPE_BOTTOM; + } + return ctrlType; + } + + private void updateCursorType(float x, float y) { + @TaskPositioner.CtrlType int ctrlType = calculateCtrlType(x, y); + + int cursorType = PointerIcon.TYPE_DEFAULT; + switch (ctrlType) { + case TaskPositioner.CTRL_TYPE_LEFT: + case TaskPositioner.CTRL_TYPE_RIGHT: + cursorType = PointerIcon.TYPE_HORIZONTAL_DOUBLE_ARROW; + break; + case TaskPositioner.CTRL_TYPE_TOP: + case TaskPositioner.CTRL_TYPE_BOTTOM: + cursorType = PointerIcon.TYPE_VERTICAL_DOUBLE_ARROW; + break; + case TaskPositioner.CTRL_TYPE_LEFT | TaskPositioner.CTRL_TYPE_TOP: + case TaskPositioner.CTRL_TYPE_RIGHT | TaskPositioner.CTRL_TYPE_BOTTOM: + cursorType = PointerIcon.TYPE_TOP_LEFT_DIAGONAL_DOUBLE_ARROW; + break; + case TaskPositioner.CTRL_TYPE_LEFT | TaskPositioner.CTRL_TYPE_BOTTOM: + case TaskPositioner.CTRL_TYPE_RIGHT | TaskPositioner.CTRL_TYPE_TOP: + cursorType = PointerIcon.TYPE_TOP_RIGHT_DIAGONAL_DOUBLE_ARROW; + break; + } + mInputManager.setPointerIconType(cursorType); + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskFocusStateConsumer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskFocusStateConsumer.java new file mode 100644 index 000000000000..1c61802bbd5c --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskFocusStateConsumer.java @@ -0,0 +1,21 @@ +/* + * 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.windowdecor; + +interface TaskFocusStateConsumer { + void setTaskFocusState(boolean focused); +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskPositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskPositioner.java new file mode 100644 index 000000000000..280569b05d87 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskPositioner.java @@ -0,0 +1,99 @@ +/* + * 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.windowdecor; + +import android.annotation.IntDef; +import android.graphics.PointF; +import android.graphics.Rect; +import android.window.WindowContainerTransaction; + +import com.android.wm.shell.ShellTaskOrganizer; + +class TaskPositioner implements DragResizeCallback { + + @IntDef({CTRL_TYPE_LEFT, CTRL_TYPE_RIGHT, CTRL_TYPE_TOP, CTRL_TYPE_BOTTOM}) + @interface CtrlType {} + + static final int CTRL_TYPE_LEFT = 1; + static final int CTRL_TYPE_RIGHT = 2; + static final int CTRL_TYPE_TOP = 4; + static final int CTRL_TYPE_BOTTOM = 8; + + private final ShellTaskOrganizer mTaskOrganizer; + private final WindowDecoration mWindowDecoration; + + private final Rect mTaskBoundsAtDragStart = new Rect(); + private final PointF mResizeStartPoint = new PointF(); + private final Rect mResizeTaskBounds = new Rect(); + + private int mCtrlType; + + TaskPositioner(ShellTaskOrganizer taskOrganizer, WindowDecoration windowDecoration) { + mTaskOrganizer = taskOrganizer; + mWindowDecoration = windowDecoration; + } + + @Override + public void onDragResizeStart(int ctrlType, float x, float y) { + mCtrlType = ctrlType; + + mTaskBoundsAtDragStart.set( + mWindowDecoration.mTaskInfo.configuration.windowConfiguration.getBounds()); + mResizeStartPoint.set(x, y); + } + + @Override + public void onDragResizeMove(float x, float y) { + changeBounds(x, y); + } + + @Override + public void onDragResizeEnd(float x, float y) { + changeBounds(x, y); + + mCtrlType = 0; + mTaskBoundsAtDragStart.setEmpty(); + mResizeStartPoint.set(0, 0); + } + + private void changeBounds(float x, float y) { + float deltaX = x - mResizeStartPoint.x; + mResizeTaskBounds.set(mTaskBoundsAtDragStart); + if ((mCtrlType & CTRL_TYPE_LEFT) != 0) { + mResizeTaskBounds.left += deltaX; + } + if ((mCtrlType & CTRL_TYPE_RIGHT) != 0) { + mResizeTaskBounds.right += deltaX; + } + float deltaY = y - mResizeStartPoint.y; + if ((mCtrlType & CTRL_TYPE_TOP) != 0) { + mResizeTaskBounds.top += deltaY; + } + if ((mCtrlType & CTRL_TYPE_BOTTOM) != 0) { + mResizeTaskBounds.bottom += deltaY; + } + if (mCtrlType == 0) { + mResizeTaskBounds.offset((int) deltaX, (int) deltaY); + } + + if (!mResizeTaskBounds.isEmpty()) { + final WindowContainerTransaction wct = new WindowContainerTransaction(); + wct.setBounds(mWindowDecoration.mTaskInfo.token, mResizeTaskBounds); + mTaskOrganizer.applyTransaction(wct); + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecorLinearLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecorLinearLayout.java new file mode 100644 index 000000000000..6d8001a2f92b --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecorLinearLayout.java @@ -0,0 +1,72 @@ +/* + * 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.windowdecor; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.LinearLayout; + +import androidx.annotation.Nullable; + +import com.android.wm.shell.R; + +/** + * A {@link LinearLayout} that takes an additional task focused drawable state. The new state is + * used to select the correct background color for views in the window decoration. + */ +public class WindowDecorLinearLayout extends LinearLayout implements TaskFocusStateConsumer { + private static final int[] TASK_FOCUSED_STATE = { R.attr.state_task_focused }; + + private boolean mIsTaskFocused; + + public WindowDecorLinearLayout(Context context) { + super(context); + } + + public WindowDecorLinearLayout(Context context, + @Nullable AttributeSet attrs) { + super(context, attrs); + } + + public WindowDecorLinearLayout(Context context, @Nullable AttributeSet attrs, + int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public WindowDecorLinearLayout(Context context, AttributeSet attrs, int defStyleAttr, + int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + @Override + public void setTaskFocusState(boolean focused) { + mIsTaskFocused = focused; + + refreshDrawableState(); + } + + @Override + protected int[] onCreateDrawableState(int extraSpace) { + if (!mIsTaskFocused) { + return super.onCreateDrawableState(extraSpace); + } + + final int[] states = super.onCreateDrawableState(extraSpace + 1); + mergeDrawableStates(states, TASK_FOCUSED_STATE); + return states; + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecorViewModel.java new file mode 100644 index 000000000000..d9697d288ab6 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecorViewModel.java @@ -0,0 +1,90 @@ +/* + * 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.windowdecor; + +import android.app.ActivityManager; +import android.view.SurfaceControl; + +import androidx.annotation.Nullable; + +import com.android.wm.shell.freeform.FreeformTaskTransitionStarter; + +/** + * The interface used by some {@link com.android.wm.shell.ShellTaskOrganizer.TaskListener} to help + * customize {@link WindowDecoration}. Its implementations are responsible to interpret user's + * interactions with UI widgets in window decorations and send corresponding requests to system + * servers. + * + * @param <T> The actual decoration type + */ +public interface WindowDecorViewModel<T extends AutoCloseable> { + + /** + * Sets the transition starter that starts freeform task transitions. + * + * @param transitionStarter the transition starter that starts freeform task transitions + */ + void setFreeformTaskTransitionStarter(FreeformTaskTransitionStarter transitionStarter); + + /** + * Creates a window decoration for the given task. + * Can be {@code null} for Fullscreen tasks but not Freeform ones. + * + * @param taskInfo the initial task info of the task + * @param taskSurface the surface of the task + * @param startT the start transaction to be applied before the transition + * @param finishT the finish transaction to restore states after the transition + * @return the window decoration object + */ + @Nullable T createWindowDecoration( + ActivityManager.RunningTaskInfo taskInfo, + SurfaceControl taskSurface, + SurfaceControl.Transaction startT, + SurfaceControl.Transaction finishT); + + /** + * Adopts the window decoration if possible. + * May be {@code null} if a window decor is not needed or the given one is incompatible. + * + * @param windowDecor the potential window decoration to adopt + * @return the window decoration if it can be adopted, or {@code null} otherwise. + */ + @Nullable T adoptWindowDecoration(@Nullable AutoCloseable windowDecor); + + /** + * Notifies a task info update on the given task, with the window decoration created previously + * for this task by {@link #createWindowDecoration}. + * + * @param taskInfo the new task info of the task + * @param windowDecoration the window decoration created for the task + */ + void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo, T windowDecoration); + + /** + * Notifies a transition is about to start about the given task to give the window decoration a + * chance to prepare for this transition. + * + * @param startT the start transaction to be applied before the transition + * @param finishT the finish transaction to restore states after the transition + * @param windowDecoration the window decoration created for the task + */ + void setupWindowDecorationForTransition( + ActivityManager.RunningTaskInfo taskInfo, + SurfaceControl.Transaction startT, + SurfaceControl.Transaction finishT, + T windowDecoration); +} 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 new file mode 100644 index 000000000000..3e3a864f48c7 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java @@ -0,0 +1,391 @@ +/* + * 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.windowdecor; + +import android.app.ActivityManager.RunningTaskInfo; +import android.content.Context; +import android.content.res.Configuration; +import android.graphics.Color; +import android.graphics.PixelFormat; +import android.graphics.Point; +import android.graphics.Rect; +import android.util.DisplayMetrics; +import android.view.Display; +import android.view.InsetsState; +import android.view.LayoutInflater; +import android.view.SurfaceControl; +import android.view.SurfaceControlViewHost; +import android.view.View; +import android.view.ViewRootImpl; +import android.view.WindowManager; +import android.view.WindowlessWindowManager; +import android.window.WindowContainerTransaction; + +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.common.DisplayController; + +import java.util.function.Supplier; + +/** + * Manages a container surface and a windowless window to show window decoration. Responsible to + * update window decoration window state and layout parameters on task info changes and so that + * window decoration is in correct state and bounds. + * + * The container surface is a child of the task display area in the same display, so that window + * decorations can be drawn out of the task bounds and receive input events from out of the task + * bounds to support drag resizing. + * + * The windowless window that hosts window decoration is positioned in front of all activities, to + * allow the foreground activity to draw its own background behind window decorations, such as + * the window captions. + * + * @param <T> The type of the root view + */ +public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> + implements AutoCloseable { + private static final int[] CAPTION_INSETS_TYPES = { InsetsState.ITYPE_CAPTION_BAR }; + + /** + * System-wide context. Only used to create context with overridden configurations. + */ + final Context mContext; + final DisplayController mDisplayController; + final ShellTaskOrganizer mTaskOrganizer; + final Supplier<SurfaceControl.Builder> mSurfaceControlBuilderSupplier; + final Supplier<SurfaceControl.Transaction> mSurfaceControlTransactionSupplier; + final Supplier<WindowContainerTransaction> mWindowContainerTransactionSupplier; + final SurfaceControlViewHostFactory mSurfaceControlViewHostFactory; + private final DisplayController.OnDisplaysChangedListener mOnDisplaysChangedListener = + new DisplayController.OnDisplaysChangedListener() { + @Override + public void onDisplayAdded(int displayId) { + if (mTaskInfo.displayId != displayId) { + return; + } + + mDisplayController.removeDisplayWindowListener(this); + relayout(mTaskInfo); + } + }; + + RunningTaskInfo mTaskInfo; + final SurfaceControl mTaskSurface; + + Display mDisplay; + Context mDecorWindowContext; + SurfaceControl mDecorationContainerSurface; + SurfaceControl mTaskBackgroundSurface; + + SurfaceControl mCaptionContainerSurface; + private CaptionWindowManager mCaptionWindowManager; + private SurfaceControlViewHost mViewHost; + + private final Rect mCaptionInsetsRect = new Rect(); + private final Rect mTaskSurfaceCrop = new Rect(); + private final float[] mTmpColor = new float[3]; + + WindowDecoration( + Context context, + DisplayController displayController, + ShellTaskOrganizer taskOrganizer, + RunningTaskInfo taskInfo, + SurfaceControl taskSurface) { + this(context, displayController, taskOrganizer, taskInfo, taskSurface, + SurfaceControl.Builder::new, SurfaceControl.Transaction::new, + WindowContainerTransaction::new, new SurfaceControlViewHostFactory() {}); + } + + WindowDecoration( + Context context, + DisplayController displayController, + ShellTaskOrganizer taskOrganizer, + RunningTaskInfo taskInfo, + SurfaceControl taskSurface, + Supplier<SurfaceControl.Builder> surfaceControlBuilderSupplier, + Supplier<SurfaceControl.Transaction> surfaceControlTransactionSupplier, + Supplier<WindowContainerTransaction> windowContainerTransactionSupplier, + SurfaceControlViewHostFactory surfaceControlViewHostFactory) { + mContext = context; + mDisplayController = displayController; + mTaskOrganizer = taskOrganizer; + mTaskInfo = taskInfo; + mTaskSurface = taskSurface; + mSurfaceControlBuilderSupplier = surfaceControlBuilderSupplier; + mSurfaceControlTransactionSupplier = surfaceControlTransactionSupplier; + mWindowContainerTransactionSupplier = windowContainerTransactionSupplier; + mSurfaceControlViewHostFactory = surfaceControlViewHostFactory; + + mDisplay = mDisplayController.getDisplay(mTaskInfo.displayId); + mDecorWindowContext = mContext.createConfigurationContext(mTaskInfo.getConfiguration()); + } + + /** + * Used by {@link WindowDecoration} to trigger a new relayout because the requirements for a + * relayout weren't satisfied are satisfied now. + * + * @param taskInfo The previous {@link RunningTaskInfo} passed into {@link #relayout} or the + * constructor. + */ + abstract void relayout(RunningTaskInfo taskInfo); + + void relayout(RunningTaskInfo taskInfo, int layoutResId, T rootView, float captionHeightDp, + Rect outsetsDp, float shadowRadiusDp, SurfaceControl.Transaction startT, + SurfaceControl.Transaction finishT, WindowContainerTransaction wct, + RelayoutResult<T> outResult) { + outResult.reset(); + + final Configuration oldTaskConfig = mTaskInfo.getConfiguration(); + if (taskInfo != null) { + mTaskInfo = taskInfo; + } + + if (!mTaskInfo.isVisible) { + releaseViews(); + finishT.hide(mTaskSurface); + return; + } + + if (rootView == null && layoutResId == 0) { + throw new IllegalArgumentException("layoutResId and rootView can't both be invalid."); + } + + outResult.mRootView = rootView; + rootView = null; // Clear it just in case we use it accidentally + final Configuration taskConfig = mTaskInfo.getConfiguration(); + if (oldTaskConfig.densityDpi != taskConfig.densityDpi + || mDisplay == null + || mDisplay.getDisplayId() != mTaskInfo.displayId) { + releaseViews(); + + if (!obtainDisplayOrRegisterListener()) { + outResult.mRootView = null; + return; + } + mDecorWindowContext = mContext.createConfigurationContext(taskConfig); + if (layoutResId != 0) { + outResult.mRootView = + (T) LayoutInflater.from(mDecorWindowContext).inflate(layoutResId, null); + } + } + + if (outResult.mRootView == null) { + outResult.mRootView = + (T) LayoutInflater.from(mDecorWindowContext).inflate(layoutResId, null); + } + + // DecorationContainerSurface + if (mDecorationContainerSurface == null) { + final SurfaceControl.Builder builder = mSurfaceControlBuilderSupplier.get(); + mDecorationContainerSurface = builder + .setName("Decor container of Task=" + mTaskInfo.taskId) + .setContainerLayer() + .setParent(mTaskSurface) + .build(); + + startT.setTrustedOverlay(mDecorationContainerSurface, true); + } + + final Rect taskBounds = taskConfig.windowConfiguration.getBounds(); + outResult.mDensity = taskConfig.densityDpi * DisplayMetrics.DENSITY_DEFAULT_SCALE; + final int decorContainerOffsetX = -(int) (outsetsDp.left * outResult.mDensity); + final int decorContainerOffsetY = -(int) (outsetsDp.top * outResult.mDensity); + outResult.mWidth = taskBounds.width() + + (int) (outsetsDp.right * outResult.mDensity) + - decorContainerOffsetX; + outResult.mHeight = taskBounds.height() + + (int) (outsetsDp.bottom * outResult.mDensity) + - decorContainerOffsetY; + startT.setPosition( + mDecorationContainerSurface, decorContainerOffsetX, decorContainerOffsetY) + .setWindowCrop(mDecorationContainerSurface, outResult.mWidth, outResult.mHeight) + // TODO(b/244455401): Change the z-order when it's better organized + .setLayer(mDecorationContainerSurface, mTaskInfo.numActivities + 1) + .show(mDecorationContainerSurface); + + // TaskBackgroundSurface + if (mTaskBackgroundSurface == null) { + final SurfaceControl.Builder builder = mSurfaceControlBuilderSupplier.get(); + mTaskBackgroundSurface = builder + .setName("Background of Task=" + mTaskInfo.taskId) + .setEffectLayer() + .setParent(mTaskSurface) + .build(); + } + + float shadowRadius = outResult.mDensity * shadowRadiusDp; + int backgroundColorInt = mTaskInfo.taskDescription.getBackgroundColor(); + mTmpColor[0] = (float) Color.red(backgroundColorInt) / 255.f; + mTmpColor[1] = (float) Color.green(backgroundColorInt) / 255.f; + mTmpColor[2] = (float) Color.blue(backgroundColorInt) / 255.f; + startT.setWindowCrop(mTaskBackgroundSurface, taskBounds.width(), taskBounds.height()) + .setShadowRadius(mTaskBackgroundSurface, shadowRadius) + .setColor(mTaskBackgroundSurface, mTmpColor) + // TODO(b/244455401): Change the z-order when it's better organized + .setLayer(mTaskBackgroundSurface, -1) + .show(mTaskBackgroundSurface); + + // CaptionContainerSurface, CaptionWindowManager + if (mCaptionContainerSurface == null) { + final SurfaceControl.Builder builder = mSurfaceControlBuilderSupplier.get(); + mCaptionContainerSurface = builder + .setName("Caption container of Task=" + mTaskInfo.taskId) + .setContainerLayer() + .setParent(mDecorationContainerSurface) + .build(); + } + + final int captionHeight = (int) Math.ceil(captionHeightDp * outResult.mDensity); + startT.setPosition( + mCaptionContainerSurface, -decorContainerOffsetX, -decorContainerOffsetY) + .setWindowCrop(mCaptionContainerSurface, taskBounds.width(), captionHeight) + .show(mCaptionContainerSurface); + + 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 CaptionWindowManager( + mTaskInfo.getConfiguration(), mCaptionContainerSurface); + } + + // Caption view + mCaptionWindowManager.setConfiguration(taskConfig); + final WindowManager.LayoutParams lp = + new WindowManager.LayoutParams(taskBounds.width(), captionHeight, + WindowManager.LayoutParams.TYPE_APPLICATION, + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, PixelFormat.TRANSPARENT); + lp.setTitle("Caption of Task=" + mTaskInfo.taskId); + lp.setTrustedOverlay(); + if (mViewHost == null) { + mViewHost = mSurfaceControlViewHostFactory.create(mDecorWindowContext, mDisplay, + mCaptionWindowManager); + mViewHost.setView(outResult.mRootView, lp); + } else { + mViewHost.relayout(lp); + } + + if (ViewRootImpl.CAPTION_ON_SHELL) { + outResult.mRootView.setTaskFocusState(mTaskInfo.isFocused); + + // Caption insets + mCaptionInsetsRect.set(taskBounds); + mCaptionInsetsRect.bottom = mCaptionInsetsRect.top + captionHeight; + wct.addRectInsetsProvider(mTaskInfo.token, mCaptionInsetsRect, CAPTION_INSETS_TYPES); + } else { + startT.hide(mCaptionContainerSurface); + } + + // Task surface itself + Point taskPosition = mTaskInfo.positionInParent; + mTaskSurfaceCrop.set( + decorContainerOffsetX, + decorContainerOffsetY, + outResult.mWidth + decorContainerOffsetX, + outResult.mHeight + decorContainerOffsetY); + startT.show(mTaskSurface); + finishT.setPosition(mTaskSurface, taskPosition.x, taskPosition.y) + .setCrop(mTaskSurface, mTaskSurfaceCrop); + } + + /** + * Obtains the {@link Display} instance for the display ID in {@link #mTaskInfo} if it exists or + * registers {@link #mOnDisplaysChangedListener} if it doesn't. + * + * @return {@code true} if the {@link Display} instance exists; or {@code false} otherwise + */ + private boolean obtainDisplayOrRegisterListener() { + mDisplay = mDisplayController.getDisplay(mTaskInfo.displayId); + if (mDisplay == null) { + mDisplayController.addDisplayWindowListener(mOnDisplaysChangedListener); + return false; + } + return true; + } + + private void releaseViews() { + 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; + released = true; + } + + if (mDecorationContainerSurface != null) { + t.remove(mDecorationContainerSurface); + mDecorationContainerSurface = null; + released = true; + } + + if (mTaskBackgroundSurface != null) { + t.remove(mTaskBackgroundSurface); + mTaskBackgroundSurface = null; + released = true; + } + + if (released) { + t.apply(); + } + + final WindowContainerTransaction wct = mWindowContainerTransactionSupplier.get(); + wct.removeInsetsProvider(mTaskInfo.token, CAPTION_INSETS_TYPES); + mTaskOrganizer.applyTransaction(wct); + } + + @Override + public void close() { + mDisplayController.removeDisplayWindowListener(mOnDisplaysChangedListener); + releaseViews(); + } + + static class RelayoutResult<T extends View & TaskFocusStateConsumer> { + int mWidth; + int mHeight; + float mDensity; + T mRootView; + + void reset() { + mWidth = 0; + mHeight = 0; + mDensity = 0; + mRootView = null; + } + } + + private static class CaptionWindowManager extends WindowlessWindowManager { + CaptionWindowManager(Configuration config, SurfaceControl rootSurface) { + super(config, rootSurface, null /* hostInputToken */); + } + + @Override + public void setConfiguration(Configuration configuration) { + super.setConfiguration(configuration); + } + } + + interface SurfaceControlViewHostFactory { + default SurfaceControlViewHost create(Context c, Display d, WindowlessWindowManager wmm) { + return new SurfaceControlViewHost(c, d, wmm); + } + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonAssertions.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonAssertions.kt index cb478c84c2b7..cba396a82a87 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonAssertions.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonAssertions.kt @@ -43,6 +43,84 @@ fun FlickerTestParameter.appPairsDividerBecomesVisible() { } } +fun FlickerTestParameter.splitScreenDividerBecomesVisible() { + layerBecomesVisible(SPLIT_SCREEN_DIVIDER_COMPONENT) +} + +fun FlickerTestParameter.layerBecomesVisible( + component: FlickerComponentName +) { + assertLayers { + this.isInvisible(component) + .then() + .isVisible(component) + } +} + +fun FlickerTestParameter.layerIsVisibleAtEnd( + component: FlickerComponentName +) { + assertLayersEnd { + this.isVisible(component) + } +} + +fun FlickerTestParameter.splitAppLayerBoundsBecomesVisible( + rotation: Int, + component: FlickerComponentName, + splitLeftTop: Boolean +) { + assertLayers { + val dividerRegion = this.last().layer(SPLIT_SCREEN_DIVIDER_COMPONENT).visibleRegion.region + this.isInvisible(component) + .then() + .invoke("splitAppLayerBoundsBecomesVisible") { + it.visibleRegion(component).overlaps( + if (splitLeftTop) { + getSplitLeftTopRegion(dividerRegion, rotation) + } else { + getSplitRightBottomRegion(dividerRegion, rotation) + } + ) + } + } +} + +fun FlickerTestParameter.splitAppLayerBoundsIsVisibleAtEnd( + rotation: Int, + component: FlickerComponentName, + splitLeftTop: Boolean +) { + assertLayersEnd { + val dividerRegion = layer(SPLIT_SCREEN_DIVIDER_COMPONENT).visibleRegion.region + visibleRegion(component).overlaps( + if (splitLeftTop) { + getSplitLeftTopRegion(dividerRegion, rotation) + } else { + getSplitRightBottomRegion(dividerRegion, rotation) + } + ) + } +} + +fun FlickerTestParameter.appWindowBecomesVisible( + component: FlickerComponentName +) { + assertWm { + this.isAppWindowInvisible(component) + .then() + .isAppWindowVisible(component) + } +} + +fun FlickerTestParameter.appWindowIsVisibleAtEnd( + component: FlickerComponentName +) { + assertWmEnd { + this.isAppWindowVisible(component) + } +} + fun FlickerTestParameter.dockedStackDividerIsVisibleAtEnd() { assertLayersEnd { this.isVisible(DOCKED_STACK_DIVIDER_COMPONENT) @@ -118,21 +196,53 @@ fun FlickerTestParameter.dockedStackSecondaryBoundsIsVisibleAtEnd( fun getPrimaryRegion(dividerRegion: Region, rotation: Int): Region { val displayBounds = WindowUtils.getDisplayBounds(rotation) return if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180) { - Region.from(0, 0, displayBounds.bounds.right, - dividerRegion.bounds.top + WindowUtils.dockedStackDividerInset) + Region.from( + 0, 0, displayBounds.bounds.right, + dividerRegion.bounds.top + WindowUtils.dockedStackDividerInset + ) } else { - Region.from(0, 0, dividerRegion.bounds.left + WindowUtils.dockedStackDividerInset, - displayBounds.bounds.bottom) + Region.from( + 0, 0, dividerRegion.bounds.left + WindowUtils.dockedStackDividerInset, + displayBounds.bounds.bottom + ) } } fun getSecondaryRegion(dividerRegion: Region, rotation: Int): Region { val displayBounds = WindowUtils.getDisplayBounds(rotation) return if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180) { - Region.from(0, dividerRegion.bounds.bottom - WindowUtils.dockedStackDividerInset, - displayBounds.bounds.right, displayBounds.bounds.bottom) + Region.from( + 0, dividerRegion.bounds.bottom - WindowUtils.dockedStackDividerInset, + displayBounds.bounds.right, displayBounds.bounds.bottom + ) } else { - Region.from(dividerRegion.bounds.right - WindowUtils.dockedStackDividerInset, 0, - displayBounds.bounds.right, displayBounds.bounds.bottom) + Region.from( + dividerRegion.bounds.right - WindowUtils.dockedStackDividerInset, 0, + displayBounds.bounds.right, displayBounds.bounds.bottom + ) } -}
\ No newline at end of file +} + +fun getSplitLeftTopRegion(dividerRegion: Region, rotation: Int): Region { + val displayBounds = WindowUtils.getDisplayBounds(rotation) + return if (displayBounds.width > displayBounds.height) { + Region.from(0, 0, dividerRegion.bounds.left, displayBounds.bounds.bottom) + } else { + Region.from(0, 0, displayBounds.bounds.right, dividerRegion.bounds.top) + } +} + +fun getSplitRightBottomRegion(dividerRegion: Region, rotation: Int): Region { + val displayBounds = WindowUtils.getDisplayBounds(rotation) + return if (displayBounds.width > displayBounds.height) { + Region.from( + dividerRegion.bounds.right, 0, displayBounds.bounds.right, + displayBounds.bounds.bottom + ) + } else { + Region.from( + 0, dividerRegion.bounds.bottom, displayBounds.bounds.right, + displayBounds.bounds.bottom + ) + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonConstants.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonConstants.kt index 40891f36a5da..f56eb6e783aa 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonConstants.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonConstants.kt @@ -21,4 +21,5 @@ import com.android.server.wm.traces.common.FlickerComponentName const val SYSTEM_UI_PACKAGE_NAME = "com.android.systemui" val APP_PAIR_SPLIT_DIVIDER_COMPONENT = FlickerComponentName("", "AppPairSplitDivider#") -val DOCKED_STACK_DIVIDER_COMPONENT = FlickerComponentName("", "DockedStackDivider#")
\ No newline at end of file +val DOCKED_STACK_DIVIDER_COMPONENT = FlickerComponentName("", "DockedStackDivider#") +val SPLIT_SCREEN_DIVIDER_COMPONENT = FlickerComponentName("", "StageCoordinatorSplitDivider#") diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestCannotPairNonResizeableApps.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestCannotPairNonResizeableApps.kt deleted file mode 100644 index c9cab39b7d8b..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestCannotPairNonResizeableApps.kt +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Copyright (C) 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.wm.shell.flicker.apppairs - -import androidx.test.filters.RequiresDevice -import com.android.server.wm.flicker.FlickerParametersRunnerFactory -import com.android.server.wm.flicker.FlickerTestParameter -import com.android.server.wm.flicker.FlickerTestParameterFactory -import com.android.server.wm.flicker.annotation.Group1 -import com.android.server.wm.flicker.dsl.FlickerBuilder -import com.android.wm.shell.flicker.appPairsDividerIsInvisibleAtEnd -import com.android.wm.shell.flicker.helpers.AppPairsHelper -import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.resetMultiWindowConfig -import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.setSupportsNonResizableMultiWindow -import org.junit.After -import org.junit.Before -import org.junit.FixMethodOrder -import org.junit.Ignore -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.MethodSorters -import org.junit.runners.Parameterized - -/** - * Test cold launch app from launcher. When the device doesn't support non-resizable in multi window - * {@link Settings.Global.DEVELOPMENT_ENABLE_NON_RESIZABLE_MULTI_WINDOW}, app pairs should not pair - * non-resizable apps. - * - * To run this test: `atest WMShellFlickerTests:AppPairsTestCannotPairNonResizeableApps` - */ -@RequiresDevice -@RunWith(Parameterized::class) -@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -@Group1 -class AppPairsTestCannotPairNonResizeableApps( - testSpec: FlickerTestParameter -) : AppPairsTransition(testSpec) { - - override val transition: FlickerBuilder.() -> Unit - get() = { - super.transition(this) - transitions { - nonResizeableApp?.launchViaIntent(wmHelper) - // TODO pair apps through normal UX flow - executeShellCommand( - composePairsCommand(primaryTaskId, nonResizeableTaskId, pair = true)) - nonResizeableApp?.run { wmHelper.waitForFullScreenApp(nonResizeableApp.component) } - } - } - - @Before - override fun setup() { - super.setup() - setSupportsNonResizableMultiWindow(instrumentation, -1) - } - - @After - override fun teardown() { - super.teardown() - resetMultiWindowConfig(instrumentation) - } - - @Ignore - @Test - override fun navBarLayerRotatesAndScales() = super.navBarLayerRotatesAndScales() - - @Ignore - @Test - override fun statusBarLayerRotatesScales() = super.statusBarLayerRotatesScales() - - @Ignore - @Test - override fun navBarLayerIsVisible() = super.navBarLayerIsVisible() - - @Ignore - @Test - fun appPairsDividerIsInvisibleAtEnd() = testSpec.appPairsDividerIsInvisibleAtEnd() - - @Ignore - @Test - fun onlyResizeableAppWindowVisible() { - val nonResizeableApp = nonResizeableApp - require(nonResizeableApp != null) { - "Non resizeable app not initialized" - } - testSpec.assertWmEnd { - isAppWindowVisible(nonResizeableApp.component) - isAppWindowInvisible(primaryApp.component) - } - } - - companion object { - @Parameterized.Parameters(name = "{0}") - @JvmStatic - fun getParams(): List<FlickerTestParameter> { - return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests( - repetitions = AppPairsHelper.TEST_REPETITIONS) - } - } -}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestPairPrimaryAndSecondaryApps.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestPairPrimaryAndSecondaryApps.kt deleted file mode 100644 index 60c32c99d1ff..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestPairPrimaryAndSecondaryApps.kt +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright (C) 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.wm.shell.flicker.apppairs - -import androidx.test.filters.RequiresDevice -import com.android.server.wm.flicker.FlickerParametersRunnerFactory -import com.android.server.wm.flicker.FlickerTestParameter -import com.android.server.wm.flicker.FlickerTestParameterFactory -import com.android.server.wm.flicker.annotation.Group1 -import com.android.server.wm.flicker.dsl.FlickerBuilder -import com.android.wm.shell.flicker.APP_PAIR_SPLIT_DIVIDER_COMPONENT -import com.android.wm.shell.flicker.appPairsDividerIsVisibleAtEnd -import com.android.wm.shell.flicker.helpers.AppPairsHelper -import com.android.wm.shell.flicker.helpers.AppPairsHelper.Companion.waitAppsShown -import org.junit.FixMethodOrder -import org.junit.Ignore -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.MethodSorters -import org.junit.runners.Parameterized - -/** - * Test cold launch app from launcher. - * To run this test: `atest WMShellFlickerTests:AppPairsTestPairPrimaryAndSecondaryApps` - */ -@RequiresDevice -@RunWith(Parameterized::class) -@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -@Group1 -class AppPairsTestPairPrimaryAndSecondaryApps( - testSpec: FlickerTestParameter -) : AppPairsTransition(testSpec) { - override val transition: FlickerBuilder.() -> Unit - get() = { - super.transition(this) - transitions { - // TODO pair apps through normal UX flow - executeShellCommand( - composePairsCommand(primaryTaskId, secondaryTaskId, pair = true)) - waitAppsShown(primaryApp, secondaryApp) - } - } - - @Ignore - @Test - override fun navBarLayerIsVisible() = super.navBarLayerIsVisible() - - @Ignore - @Test - override fun navBarLayerRotatesAndScales() = super.navBarLayerRotatesAndScales() - - @Ignore - @Test - override fun statusBarLayerRotatesScales() = super.statusBarLayerRotatesScales() - - @Ignore - @Test - fun appPairsDividerIsVisibleAtEnd() = testSpec.appPairsDividerIsVisibleAtEnd() - - @Ignore - @Test - fun bothAppWindowsVisible() { - testSpec.assertWmEnd { - isAppWindowVisible(primaryApp.component) - isAppWindowVisible(secondaryApp.component) - } - } - - @Ignore - @Test - fun appsEndingBounds() { - testSpec.assertLayersEnd { - val dividerRegion = layer(APP_PAIR_SPLIT_DIVIDER_COMPONENT).visibleRegion.region - visibleRegion(primaryApp.component) - .coversExactly(appPairsHelper.getPrimaryBounds(dividerRegion)) - visibleRegion(secondaryApp.component) - .coversExactly(appPairsHelper.getSecondaryBounds(dividerRegion)) - } - } - - companion object { - @Parameterized.Parameters(name = "{0}") - @JvmStatic - fun getParams(): List<FlickerTestParameter> { - return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests( - repetitions = AppPairsHelper.TEST_REPETITIONS) - } - } -} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestSupportPairNonResizeableApps.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestSupportPairNonResizeableApps.kt deleted file mode 100644 index 24869a802167..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestSupportPairNonResizeableApps.kt +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Copyright (C) 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.wm.shell.flicker.apppairs - -import android.view.Display -import androidx.test.filters.RequiresDevice -import com.android.server.wm.flicker.FlickerParametersRunnerFactory -import com.android.server.wm.flicker.FlickerTestParameter -import com.android.server.wm.flicker.FlickerTestParameterFactory -import com.android.server.wm.flicker.annotation.Group1 -import com.android.server.wm.flicker.dsl.FlickerBuilder -import com.android.server.wm.traces.common.WindowManagerConditionsFactory -import com.android.wm.shell.flicker.appPairsDividerIsVisibleAtEnd -import com.android.wm.shell.flicker.helpers.AppPairsHelper -import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.resetMultiWindowConfig -import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.setSupportsNonResizableMultiWindow -import org.junit.After -import org.junit.Before -import org.junit.FixMethodOrder -import org.junit.Ignore -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.MethodSorters -import org.junit.runners.Parameterized - -/** - * Test cold launch app from launcher. When the device supports non-resizable in multi window - * {@link Settings.Global.DEVELOPMENT_ENABLE_NON_RESIZABLE_MULTI_WINDOW}, app pairs can pair - * non-resizable apps. - * - * To run this test: `atest WMShellFlickerTests:AppPairsTestSupportPairNonResizeableApps` - */ -@RequiresDevice -@RunWith(Parameterized::class) -@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -@Group1 -class AppPairsTestSupportPairNonResizeableApps( - testSpec: FlickerTestParameter -) : AppPairsTransition(testSpec) { - - override val transition: FlickerBuilder.() -> Unit - get() = { - super.transition(this) - transitions { - nonResizeableApp?.launchViaIntent(wmHelper) - // TODO pair apps through normal UX flow - executeShellCommand( - composePairsCommand(primaryTaskId, nonResizeableTaskId, pair = true)) - val waitConditions = mutableListOf( - WindowManagerConditionsFactory.isWindowVisible(primaryApp.component), - WindowManagerConditionsFactory.isLayerVisible(primaryApp.component), - WindowManagerConditionsFactory.isAppTransitionIdle(Display.DEFAULT_DISPLAY)) - - nonResizeableApp?.let { - waitConditions.add( - WindowManagerConditionsFactory.isWindowVisible(nonResizeableApp.component)) - waitConditions.add( - WindowManagerConditionsFactory.isLayerVisible(nonResizeableApp.component)) - } - wmHelper.waitFor(*waitConditions.toTypedArray()) - } - } - - @Before - override fun setup() { - super.setup() - setSupportsNonResizableMultiWindow(instrumentation, 1) - } - - @After - override fun teardown() { - super.teardown() - resetMultiWindowConfig(instrumentation) - } - - @Ignore - @Test - override fun navBarLayerIsVisible() = super.navBarLayerIsVisible() - - @Ignore - @Test - override fun navBarLayerRotatesAndScales() = super.navBarLayerRotatesAndScales() - - @Ignore - @Test - override fun statusBarLayerRotatesScales() = super.statusBarLayerRotatesScales() - - @Ignore - @Test - fun appPairsDividerIsVisibleAtEnd() = testSpec.appPairsDividerIsVisibleAtEnd() - - @Ignore - @Test - fun bothAppWindowVisible() { - val nonResizeableApp = nonResizeableApp - require(nonResizeableApp != null) { - "Non resizeable app not initialized" - } - testSpec.assertWmEnd { - isAppWindowVisible(nonResizeableApp.component) - isAppWindowVisible(primaryApp.component) - } - } - - companion object { - @Parameterized.Parameters(name = "{0}") - @JvmStatic - fun getParams(): List<FlickerTestParameter> { - return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests( - repetitions = AppPairsHelper.TEST_REPETITIONS) - } - } -}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestUnpairPrimaryAndSecondaryApps.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestUnpairPrimaryAndSecondaryApps.kt deleted file mode 100644 index 007415d19860..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestUnpairPrimaryAndSecondaryApps.kt +++ /dev/null @@ -1,121 +0,0 @@ -/* - * Copyright (C) 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.wm.shell.flicker.apppairs - -import android.os.SystemClock -import androidx.test.filters.RequiresDevice -import com.android.server.wm.flicker.FlickerParametersRunnerFactory -import com.android.server.wm.flicker.FlickerTestParameter -import com.android.server.wm.flicker.FlickerTestParameterFactory -import com.android.server.wm.flicker.annotation.Group1 -import com.android.server.wm.flicker.dsl.FlickerBuilder -import com.android.wm.shell.flicker.APP_PAIR_SPLIT_DIVIDER_COMPONENT -import com.android.wm.shell.flicker.appPairsDividerIsInvisibleAtEnd -import com.android.wm.shell.flicker.helpers.AppPairsHelper -import com.android.wm.shell.flicker.helpers.AppPairsHelper.Companion.waitAppsShown -import org.junit.FixMethodOrder -import org.junit.Ignore -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.MethodSorters -import org.junit.runners.Parameterized - -/** - * Test cold launch app from launcher. - * To run this test: `atest WMShellFlickerTests:AppPairsTestUnpairPrimaryAndSecondaryApps` - */ -@RequiresDevice -@RunWith(Parameterized::class) -@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -@Group1 -class AppPairsTestUnpairPrimaryAndSecondaryApps( - testSpec: FlickerTestParameter -) : AppPairsTransition(testSpec) { - override val transition: FlickerBuilder.() -> Unit - get() = { - super.transition(this) - setup { - eachRun { - executeShellCommand( - composePairsCommand(primaryTaskId, secondaryTaskId, pair = true)) - waitAppsShown(primaryApp, secondaryApp) - } - } - transitions { - // TODO pair apps through normal UX flow - executeShellCommand( - composePairsCommand(primaryTaskId, secondaryTaskId, pair = false)) - SystemClock.sleep(AppPairsHelper.TIMEOUT_MS) - } - } - - @Ignore - @Test - override fun navBarLayerRotatesAndScales() = super.navBarLayerRotatesAndScales() - - @Ignore - @Test - override fun statusBarLayerRotatesScales() = super.statusBarLayerRotatesScales() - - @Ignore - @Test - fun appPairsDividerIsInvisibleAtEnd() = testSpec.appPairsDividerIsInvisibleAtEnd() - - @Ignore - @Test - fun bothAppWindowsInvisible() { - testSpec.assertWmEnd { - isAppWindowInvisible(primaryApp.component) - isAppWindowInvisible(secondaryApp.component) - } - } - - @Ignore - @Test - fun appsStartingBounds() { - testSpec.assertLayersStart { - val dividerRegion = layer(APP_PAIR_SPLIT_DIVIDER_COMPONENT).visibleRegion.region - visibleRegion(primaryApp.component) - .coversExactly(appPairsHelper.getPrimaryBounds(dividerRegion)) - visibleRegion(secondaryApp.component) - .coversExactly(appPairsHelper.getSecondaryBounds(dividerRegion)) - } - } - - @Ignore - @Test - fun appsEndingBounds() { - testSpec.assertLayersEnd { - notContains(primaryApp.component) - notContains(secondaryApp.component) - } - } - - @Ignore - @Test - override fun navBarLayerIsVisible() = super.navBarLayerIsVisible() - - companion object { - @Parameterized.Parameters(name = "{0}") - @JvmStatic - fun getParams(): List<FlickerTestParameter> { - return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests( - repetitions = AppPairsHelper.TEST_REPETITIONS) - } - } -} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTransition.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTransition.kt deleted file mode 100644 index 3e17948b4a84..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTransition.kt +++ /dev/null @@ -1,178 +0,0 @@ -/* - * Copyright (C) 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.wm.shell.flicker.apppairs - -import android.app.Instrumentation -import android.content.Context -import android.system.helpers.ActivityHelper -import androidx.test.platform.app.InstrumentationRegistry -import com.android.server.wm.flicker.FlickerBuilderProvider -import com.android.server.wm.flicker.FlickerTestParameter -import com.android.server.wm.flicker.dsl.FlickerBuilder -import com.android.server.wm.flicker.helpers.setRotation -import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen -import com.android.server.wm.flicker.navBarLayerIsVisible -import com.android.server.wm.flicker.navBarLayerRotatesAndScales -import com.android.server.wm.flicker.navBarWindowIsVisible -import com.android.server.wm.flicker.statusBarLayerIsVisible -import com.android.server.wm.flicker.statusBarLayerRotatesScales -import com.android.server.wm.flicker.statusBarWindowIsVisible -import com.android.server.wm.traces.parser.toFlickerComponent -import com.android.wm.shell.flicker.helpers.AppPairsHelper -import com.android.wm.shell.flicker.helpers.BaseAppHelper -import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.getDevEnableNonResizableMultiWindow -import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.setDevEnableNonResizableMultiWindow -import com.android.wm.shell.flicker.helpers.SplitScreenHelper -import com.android.wm.shell.flicker.testapp.Components -import org.junit.After -import org.junit.Before -import org.junit.Ignore -import org.junit.Test - -abstract class AppPairsTransition(protected val testSpec: FlickerTestParameter) { - protected val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation() - protected val context: Context = instrumentation.context - protected val activityHelper = ActivityHelper.getInstance() - protected val appPairsHelper = AppPairsHelper(instrumentation, - Components.SplitScreenActivity.LABEL, - Components.SplitScreenActivity.COMPONENT.toFlickerComponent()) - - protected val primaryApp = SplitScreenHelper.getPrimary(instrumentation) - protected val secondaryApp = SplitScreenHelper.getSecondary(instrumentation) - protected open val nonResizeableApp: SplitScreenHelper? = - SplitScreenHelper.getNonResizeable(instrumentation) - protected var primaryTaskId = "" - protected var secondaryTaskId = "" - protected var nonResizeableTaskId = "" - private var prevDevEnableNonResizableMultiWindow = 0 - - @Before - open fun setup() { - prevDevEnableNonResizableMultiWindow = getDevEnableNonResizableMultiWindow(context) - if (prevDevEnableNonResizableMultiWindow != 0) { - // Turn off the development option - setDevEnableNonResizableMultiWindow(context, 0) - } - } - - @After - open fun teardown() { - setDevEnableNonResizableMultiWindow(context, prevDevEnableNonResizableMultiWindow) - } - - @FlickerBuilderProvider - fun buildFlicker(): FlickerBuilder { - return FlickerBuilder(instrumentation).apply { - transition(this) - } - } - - internal open val transition: FlickerBuilder.() -> Unit - get() = { - setup { - test { - device.wakeUpAndGoToHomeScreen() - } - eachRun { - this.setRotation(testSpec.startRotation) - primaryApp.launchViaIntent(wmHelper) - secondaryApp.launchViaIntent(wmHelper) - nonResizeableApp?.launchViaIntent(wmHelper) - updateTasksId() - } - } - teardown { - eachRun { - executeShellCommand(composePairsCommand( - primaryTaskId, secondaryTaskId, pair = false)) - executeShellCommand(composePairsCommand( - primaryTaskId, nonResizeableTaskId, pair = false)) - primaryApp.exit(wmHelper) - secondaryApp.exit(wmHelper) - nonResizeableApp?.exit(wmHelper) - } - } - } - - protected fun updateTasksId() { - primaryTaskId = getTaskIdForActivity( - primaryApp.component.packageName, primaryApp.component.className).toString() - secondaryTaskId = getTaskIdForActivity( - secondaryApp.component.packageName, secondaryApp.component.className).toString() - val nonResizeableApp = nonResizeableApp - if (nonResizeableApp != null) { - nonResizeableTaskId = getTaskIdForActivity( - nonResizeableApp.component.packageName, - nonResizeableApp.component.className).toString() - } - } - - private fun getTaskIdForActivity(pkgName: String, activityName: String): Int { - return activityHelper.getTaskIdForActivity(pkgName, activityName) - } - - internal fun executeShellCommand(cmd: String) { - BaseAppHelper.executeShellCommand(instrumentation, cmd) - } - - internal fun composePairsCommand( - primaryApp: String, - secondaryApp: String, - pair: Boolean - ): String = buildString { - // dumpsys activity service SystemUIService WMShell {pair|unpair} ${TASK_ID_1} ${TASK_ID_2} - append("dumpsys activity service SystemUIService WMShell ") - if (pair) { - append("pair ") - } else { - append("unpair ") - } - append("$primaryApp $secondaryApp") - } - - @Ignore - @Test - open fun navBarLayerIsVisible() { - testSpec.navBarLayerIsVisible() - } - - @Ignore - @Test - open fun statusBarLayerIsVisible() { - testSpec.statusBarLayerIsVisible() - } - - @Ignore - @Test - open fun navBarWindowIsVisible() { - testSpec.navBarWindowIsVisible() - } - - @Ignore - @Test - open fun statusBarWindowIsVisible() { - testSpec.statusBarWindowIsVisible() - } - - @Ignore - @Test - open fun navBarLayerRotatesAndScales() = testSpec.navBarLayerRotatesAndScales() - - @Ignore - @Test - open fun statusBarLayerRotatesScales() = testSpec.statusBarLayerRotatesScales() -}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/OWNERS b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/OWNERS deleted file mode 100644 index 8446b37dbf06..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/OWNERS +++ /dev/null @@ -1,2 +0,0 @@ -# window manager > wm shell > Split Screen -# Bug component: 928697 diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/RotateTwoLaunchedAppsInAppPairsMode.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/RotateTwoLaunchedAppsInAppPairsMode.kt deleted file mode 100644 index b0c3ba20d948..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/RotateTwoLaunchedAppsInAppPairsMode.kt +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Copyright (C) 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.wm.shell.flicker.apppairs - -import android.view.Surface -import androidx.test.filters.RequiresDevice -import com.android.server.wm.flicker.FlickerParametersRunnerFactory -import com.android.server.wm.flicker.FlickerTestParameter -import com.android.server.wm.flicker.FlickerTestParameterFactory -import com.android.server.wm.flicker.annotation.Group1 -import com.android.server.wm.flicker.dsl.FlickerBuilder -import com.android.server.wm.flicker.helpers.setRotation -import com.android.wm.shell.flicker.appPairsDividerIsVisibleAtEnd -import com.android.wm.shell.flicker.appPairsPrimaryBoundsIsVisibleAtEnd -import com.android.wm.shell.flicker.appPairsSecondaryBoundsIsVisibleAtEnd -import com.android.wm.shell.flicker.helpers.AppPairsHelper.Companion.waitAppsShown -import com.android.wm.shell.flicker.helpers.SplitScreenHelper -import org.junit.FixMethodOrder -import org.junit.Ignore -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.MethodSorters -import org.junit.runners.Parameterized - -/** - * Test open apps to app pairs and rotate. - * To run this test: `atest WMShellFlickerTests:RotateTwoLaunchedAppsInAppPairsMode` - */ -@RequiresDevice -@RunWith(Parameterized::class) -@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -@Group1 -class RotateTwoLaunchedAppsInAppPairsMode( - testSpec: FlickerTestParameter -) : RotateTwoLaunchedAppsTransition(testSpec) { - override val transition: FlickerBuilder.() -> Unit - get() = { - super.transition(this) - transitions { - executeShellCommand(composePairsCommand( - primaryTaskId, secondaryTaskId, true /* pair */)) - waitAppsShown(primaryApp, secondaryApp) - setRotation(testSpec.endRotation) - } - } - - @Ignore - @Test - override fun navBarLayerIsVisible() = super.navBarLayerIsVisible() - - @Ignore - @Test - override fun statusBarLayerIsVisible() = super.statusBarLayerIsVisible() - - @Ignore - @Test - fun bothAppWindowsVisible() { - testSpec.assertWmEnd { - isAppWindowVisible(primaryApp.component) - isAppWindowVisible(secondaryApp.component) - } - } - - @Ignore - @Test - fun appPairsDividerIsVisibleAtEnd() = testSpec.appPairsDividerIsVisibleAtEnd() - - @Ignore - @Test - fun appPairsPrimaryBoundsIsVisibleAtEnd() = - testSpec.appPairsPrimaryBoundsIsVisibleAtEnd(testSpec.endRotation, - primaryApp.component) - - @Ignore - @Test - fun appPairsSecondaryBoundsIsVisibleAtEnd() = - testSpec.appPairsSecondaryBoundsIsVisibleAtEnd(testSpec.endRotation, - secondaryApp.component) - - @Ignore - @Test - override fun statusBarLayerRotatesScales() = super.statusBarLayerRotatesScales() - - companion object { - @Parameterized.Parameters(name = "{0}") - @JvmStatic - fun getParams(): Collection<FlickerTestParameter> { - return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests( - repetitions = SplitScreenHelper.TEST_REPETITIONS, - supportedRotations = listOf(Surface.ROTATION_90, Surface.ROTATION_270) - ) - } - } -}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/RotateTwoLaunchedAppsRotateAndEnterAppPairsMode.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/RotateTwoLaunchedAppsRotateAndEnterAppPairsMode.kt deleted file mode 100644 index ae56c7732a4d..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/RotateTwoLaunchedAppsRotateAndEnterAppPairsMode.kt +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Copyright (C) 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.wm.shell.flicker.apppairs - -import android.view.Surface -import androidx.test.filters.RequiresDevice -import com.android.server.wm.flicker.FlickerParametersRunnerFactory -import com.android.server.wm.flicker.FlickerTestParameter -import com.android.server.wm.flicker.FlickerTestParameterFactory -import com.android.server.wm.flicker.annotation.Group1 -import com.android.server.wm.flicker.dsl.FlickerBuilder -import com.android.server.wm.flicker.helpers.setRotation -import com.android.wm.shell.flicker.appPairsDividerIsVisibleAtEnd -import com.android.wm.shell.flicker.appPairsPrimaryBoundsIsVisibleAtEnd -import com.android.wm.shell.flicker.appPairsSecondaryBoundsIsVisibleAtEnd -import com.android.wm.shell.flicker.helpers.AppPairsHelper.Companion.waitAppsShown -import com.android.wm.shell.flicker.helpers.SplitScreenHelper -import org.junit.FixMethodOrder -import org.junit.Ignore -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.MethodSorters -import org.junit.runners.Parameterized - -/** - * Test open apps to app pairs and rotate. - * To run this test: `atest WMShellFlickerTests:RotateTwoLaunchedAppsRotateAndEnterAppPairsMode` - */ -@RequiresDevice -@RunWith(Parameterized::class) -@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -@Group1 -class RotateTwoLaunchedAppsRotateAndEnterAppPairsMode( - testSpec: FlickerTestParameter -) : RotateTwoLaunchedAppsTransition(testSpec) { - override val transition: FlickerBuilder.() -> Unit - get() = { - super.transition(this) - transitions { - this.setRotation(testSpec.endRotation) - executeShellCommand( - composePairsCommand(primaryTaskId, secondaryTaskId, pair = true)) - waitAppsShown(primaryApp, secondaryApp) - } - } - - @Ignore - @Test - fun appPairsDividerIsVisibleAtEnd() = testSpec.appPairsDividerIsVisibleAtEnd() - - @Ignore - @Test - override fun navBarWindowIsVisible() = super.navBarWindowIsVisible() - - @Ignore - @Test - override fun navBarLayerIsVisible() = super.navBarLayerIsVisible() - - @Ignore - @Test - override fun statusBarWindowIsVisible() = super.statusBarWindowIsVisible() - - @Ignore - @Test - override fun statusBarLayerIsVisible() = super.statusBarLayerIsVisible() - - @Ignore - @Test - override fun statusBarLayerRotatesScales() = super.statusBarLayerRotatesScales() - - @Ignore - @Test - fun bothAppWindowsVisible() { - testSpec.assertWmEnd { - isAppWindowVisible(primaryApp.component) - isAppWindowVisible(secondaryApp.component) - } - } - - @Ignore - @Test - fun appPairsPrimaryBoundsIsVisibleAtEnd() = - testSpec.appPairsPrimaryBoundsIsVisibleAtEnd(testSpec.endRotation, - primaryApp.component) - - @Ignore - @Test - fun appPairsSecondaryBoundsIsVisibleAtEnd() = - testSpec.appPairsSecondaryBoundsIsVisibleAtEnd(testSpec.endRotation, - secondaryApp.component) - - companion object { - @Parameterized.Parameters(name = "{0}") - @JvmStatic - fun getParams(): Collection<FlickerTestParameter> { - return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests( - repetitions = SplitScreenHelper.TEST_REPETITIONS, - supportedRotations = listOf(Surface.ROTATION_90, Surface.ROTATION_270) - ) - } - } -} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/RotateTwoLaunchedAppsTransition.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/RotateTwoLaunchedAppsTransition.kt deleted file mode 100644 index b1f1c9e539df..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/RotateTwoLaunchedAppsTransition.kt +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright (C) 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.wm.shell.flicker.apppairs - -import android.view.Surface -import com.android.server.wm.flicker.FlickerTestParameter -import com.android.server.wm.flicker.dsl.FlickerBuilder -import com.android.server.wm.flicker.helpers.setRotation -import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen -import com.android.wm.shell.flicker.helpers.BaseAppHelper.Companion.isShellTransitionsEnabled -import com.android.wm.shell.flicker.helpers.SplitScreenHelper -import org.junit.Assume.assumeFalse -import org.junit.Before -import org.junit.Ignore -import org.junit.Test - -abstract class RotateTwoLaunchedAppsTransition( - testSpec: FlickerTestParameter -) : AppPairsTransition(testSpec) { - override val nonResizeableApp: SplitScreenHelper? - get() = null - - override val transition: FlickerBuilder.() -> Unit - get() = { - setup { - test { - device.wakeUpAndGoToHomeScreen() - this.setRotation(Surface.ROTATION_0) - primaryApp.launchViaIntent(wmHelper) - secondaryApp.launchViaIntent(wmHelper) - updateTasksId() - } - } - teardown { - eachRun { - executeShellCommand(composePairsCommand( - primaryTaskId, secondaryTaskId, pair = false)) - primaryApp.exit(wmHelper) - secondaryApp.exit(wmHelper) - } - } - } - - @Before - override fun setup() { - // AppPairs hasn't been updated to Shell Transition. There will be conflict on rotation. - assumeFalse(isShellTransitionsEnabled()) - super.setup() - } - - @Ignore - @Test - override fun navBarLayerIsVisible() { - super.navBarLayerIsVisible() - } - - @Ignore - @Test - override fun navBarLayerRotatesAndScales() { - super.navBarLayerRotatesAndScales() - } -}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/PipAppHelper.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/PipAppHelper.kt index e9d438a569d5..8157a4e453af 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/PipAppHelper.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/PipAppHelper.kt @@ -39,7 +39,7 @@ class PipAppHelper(instrumentation: Instrumentation) : BaseAppHelper( ) { private val mediaSessionManager: MediaSessionManager get() = context.getSystemService(MediaSessionManager::class.java) - ?: error("Could not get MediaSessionManager") + ?: error("Could not get MediaSessionManager") private val mediaController: MediaController? get() = mediaSessionManager.getActiveSessions(null).firstOrNull { @@ -69,8 +69,10 @@ class PipAppHelper(instrumentation: Instrumentation) : BaseAppHelper( action: String? = null, stringExtras: Map<String, String> ) { - launchViaIntentAndWaitShown(wmHelper, expectedWindowName, action, stringExtras, - waitConditions = arrayOf(WindowManagerStateHelper.pipShownCondition)) + launchViaIntentAndWaitShown( + wmHelper, expectedWindowName, action, stringExtras, + waitConditions = arrayOf(WindowManagerStateHelper.pipShownCondition) + ) } /** @@ -85,7 +87,7 @@ class PipAppHelper(instrumentation: Instrumentation) : BaseAppHelper( // from "the bottom". repeat(FOCUS_ATTEMPTS) { uiDevice.findObject(selector)?.apply { if (isFocusedOrHasFocusedChild) return true } - ?: error("The object we try to focus on is gone.") + ?: error("The object we try to focus on is gone.") uiDevice.pressDPadDown() uiDevice.waitForIdle() @@ -100,29 +102,39 @@ class PipAppHelper(instrumentation: Instrumentation) : BaseAppHelper( // Wait on WMHelper or simply wait for 3 seconds wmHelper?.waitPipShown() ?: SystemClock.sleep(3_000) // when entering pip, the dismiss button is visible at the start. to ensure the pip - // animation is complete, wait until the pip dismiss button is no longer visible. + // animation is complete, wait until the pip dismiss button is no longer visible. // b/176822698: dismiss-only state will be removed in the future uiDevice.wait(Until.gone(By.res(SYSTEMUI_PACKAGE, "dismiss")), FIND_TIMEOUT) } + fun enableEnterPipOnUserLeaveHint() { + clickObject(ENTER_PIP_ON_USER_LEAVE_HINT) + } + + fun enableAutoEnterForPipActivity() { + clickObject(ENTER_PIP_AUTOENTER) + } + fun clickStartMediaSessionButton() { clickObject(MEDIA_SESSION_START_RADIO_BUTTON_ID) } fun checkWithCustomActionsCheckbox() = uiDevice - .findObject(By.res(component.packageName, WITH_CUSTOM_ACTIONS_BUTTON_ID)) - ?.takeIf { it.isCheckable } - ?.apply { if (!isChecked) clickObject(WITH_CUSTOM_ACTIONS_BUTTON_ID) } - ?: error("'With custom actions' checkbox not found") + .findObject(By.res(component.packageName, WITH_CUSTOM_ACTIONS_BUTTON_ID)) + ?.takeIf { it.isCheckable } + ?.apply { if (!isChecked) clickObject(WITH_CUSTOM_ACTIONS_BUTTON_ID) } + ?: error("'With custom actions' checkbox not found") fun pauseMedia() = mediaController?.transportControls?.pause() - ?: error("No active media session found") + ?: error("No active media session found") fun stopMedia() = mediaController?.transportControls?.stop() - ?: error("No active media session found") + ?: error("No active media session found") - @Deprecated("Use PipAppHelper.closePipWindow(wmHelper) instead", - ReplaceWith("closePipWindow(wmHelper)")) + @Deprecated( + "Use PipAppHelper.closePipWindow(wmHelper) instead", + ReplaceWith("closePipWindow(wmHelper)") + ) fun closePipWindow() { if (isTelevision) { uiDevice.closeTvPipWindow() @@ -152,7 +164,7 @@ class PipAppHelper(instrumentation: Instrumentation) : BaseAppHelper( val dismissSelector = By.res(SYSTEMUI_PACKAGE, "dismiss") uiDevice.wait(Until.hasObject(dismissSelector), FIND_TIMEOUT) val dismissPipObject = uiDevice.findObject(dismissSelector) - ?: error("PIP window dismiss button not found") + ?: error("PIP window dismiss button not found") val dismissButtonBounds = dismissPipObject.visibleBounds uiDevice.click(dismissButtonBounds.centerX(), dismissButtonBounds.centerY()) } @@ -172,7 +184,7 @@ class PipAppHelper(instrumentation: Instrumentation) : BaseAppHelper( val expandSelector = By.res(SYSTEMUI_PACKAGE, "expand_button") uiDevice.wait(Until.hasObject(expandSelector), FIND_TIMEOUT) val expandPipObject = uiDevice.findObject(expandSelector) - ?: error("PIP window expand button not found") + ?: error("PIP window expand button not found") val expandButtonBounds = expandPipObject.visibleBounds uiDevice.click(expandButtonBounds.centerX(), expandButtonBounds.centerY()) wmHelper.waitPipGone() @@ -194,5 +206,7 @@ class PipAppHelper(instrumentation: Instrumentation) : BaseAppHelper( private const val ENTER_PIP_BUTTON_ID = "enter_pip" private const val WITH_CUSTOM_ACTIONS_BUTTON_ID = "with_custom_actions" private const val MEDIA_SESSION_START_RADIO_BUTTON_ID = "media_session_start" + private const val ENTER_PIP_ON_USER_LEAVE_HINT = "enter_pip_on_leave_manual" + private const val ENTER_PIP_AUTOENTER = "enter_pip_on_leave_autoenter" } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/SplitScreenHelper.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/SplitScreenHelper.kt index 0ec9b2d869a8..49eca63a23ec 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/SplitScreenHelper.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/SplitScreenHelper.kt @@ -17,10 +17,21 @@ package com.android.wm.shell.flicker.helpers import android.app.Instrumentation -import android.content.res.Resources +import android.graphics.Point +import android.os.SystemClock +import android.view.InputDevice +import android.view.MotionEvent +import androidx.test.uiautomator.By +import androidx.test.uiautomator.BySelector +import androidx.test.uiautomator.UiDevice +import androidx.test.uiautomator.Until +import com.android.launcher3.tapl.LauncherInstrumentation import com.android.server.wm.traces.common.FlickerComponentName import com.android.server.wm.traces.parser.toFlickerComponent +import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper +import com.android.wm.shell.flicker.SYSTEM_UI_PACKAGE_NAME import com.android.wm.shell.flicker.testapp.Components +import org.junit.Assert class SplitScreenHelper( instrumentation: Instrumentation, @@ -31,25 +42,170 @@ class SplitScreenHelper( companion object { const val TEST_REPETITIONS = 1 const val TIMEOUT_MS = 3_000L + const val DRAG_DURATION_MS = 1_000L + const val NOTIFICATION_SCROLLER = "notification_stack_scroller" + const val GESTURE_STEP_MS = 16L - // TODO: remove all legacy split screen flicker tests when legacy split screen is fully - // deprecated. - fun isUsingLegacySplit(): Boolean = - Resources.getSystem().getBoolean(com.android.internal.R.bool.config_useLegacySplit) + private val notificationScrollerSelector: BySelector + get() = By.res(SYSTEM_UI_PACKAGE_NAME, NOTIFICATION_SCROLLER) + private val notificationContentSelector: BySelector + get() = By.text("Notification content") fun getPrimary(instrumentation: Instrumentation): SplitScreenHelper = - SplitScreenHelper(instrumentation, + SplitScreenHelper( + instrumentation, Components.SplitScreenActivity.LABEL, - Components.SplitScreenActivity.COMPONENT.toFlickerComponent()) + Components.SplitScreenActivity.COMPONENT.toFlickerComponent() + ) fun getSecondary(instrumentation: Instrumentation): SplitScreenHelper = - SplitScreenHelper(instrumentation, + SplitScreenHelper( + instrumentation, Components.SplitScreenSecondaryActivity.LABEL, - Components.SplitScreenSecondaryActivity.COMPONENT.toFlickerComponent()) + Components.SplitScreenSecondaryActivity.COMPONENT.toFlickerComponent() + ) fun getNonResizeable(instrumentation: Instrumentation): SplitScreenHelper = - SplitScreenHelper(instrumentation, + SplitScreenHelper( + instrumentation, Components.NonResizeableActivity.LABEL, - Components.NonResizeableActivity.COMPONENT.toFlickerComponent()) + Components.NonResizeableActivity.COMPONENT.toFlickerComponent() + ) + + fun getSendNotification(instrumentation: Instrumentation): SplitScreenHelper = + SplitScreenHelper( + instrumentation, + Components.SendNotificationActivity.LABEL, + Components.SendNotificationActivity.COMPONENT.toFlickerComponent() + ) + + fun dragFromNotificationToSplit( + instrumentation: Instrumentation, + device: UiDevice, + wmHelper: WindowManagerStateHelper + ) { + val displayBounds = wmHelper.currentState.layerState + .displays.firstOrNull { !it.isVirtual } + ?.layerStackSpace + ?: error("Display not found") + + // Pull down the notifications + device.swipe( + displayBounds.centerX(), 5, + displayBounds.centerX(), displayBounds.bottom, 20 /* steps */ + ) + SystemClock.sleep(TIMEOUT_MS) + + // Find the target notification + val notificationScroller = device.wait( + Until.findObject(notificationScrollerSelector), TIMEOUT_MS + ) + var notificationContent = notificationScroller.findObject(notificationContentSelector) + + while (notificationContent == null) { + device.swipe( + displayBounds.centerX(), displayBounds.centerY(), + displayBounds.centerX(), displayBounds.centerY() - 150, 20 /* steps */ + ) + notificationContent = notificationScroller.findObject(notificationContentSelector) + } + + // Drag to split + var dragStart = notificationContent.visibleCenter + var dragMiddle = Point(dragStart.x + 50, dragStart.y) + var dragEnd = Point(displayBounds.width / 4, displayBounds.width / 4) + val downTime = SystemClock.uptimeMillis() + + touch( + instrumentation, MotionEvent.ACTION_DOWN, downTime, downTime, + TIMEOUT_MS, dragStart + ) + // It needs a horizontal movement to trigger the drag + touchMove( + instrumentation, downTime, SystemClock.uptimeMillis(), + DRAG_DURATION_MS, dragStart, dragMiddle + ) + touchMove( + instrumentation, downTime, SystemClock.uptimeMillis(), + DRAG_DURATION_MS, dragMiddle, dragEnd + ) + // Wait for a while to start splitting + SystemClock.sleep(TIMEOUT_MS) + touch( + instrumentation, MotionEvent.ACTION_UP, downTime, SystemClock.uptimeMillis(), + GESTURE_STEP_MS, dragEnd + ) + SystemClock.sleep(TIMEOUT_MS) + } + + fun touch( + instrumentation: Instrumentation, + action: Int, + downTime: Long, + eventTime: Long, + duration: Long, + point: Point + ) { + val motionEvent = MotionEvent.obtain( + downTime, eventTime, action, point.x.toFloat(), point.y.toFloat(), 0 + ) + motionEvent.source = InputDevice.SOURCE_TOUCHSCREEN + instrumentation.uiAutomation.injectInputEvent(motionEvent, true) + motionEvent.recycle() + SystemClock.sleep(duration) + } + + fun touchMove( + instrumentation: Instrumentation, + downTime: Long, + eventTime: Long, + duration: Long, + from: Point, + to: Point + ) { + val steps: Long = duration / GESTURE_STEP_MS + var currentTime = eventTime + var currentX = from.x.toFloat() + var currentY = from.y.toFloat() + val stepX = (to.x.toFloat() - from.x.toFloat()) / steps.toFloat() + val stepY = (to.y.toFloat() - from.y.toFloat()) / steps.toFloat() + + for (i in 1..steps) { + val motionMove = MotionEvent.obtain( + downTime, currentTime, MotionEvent.ACTION_MOVE, currentX, currentY, 0 + ) + motionMove.source = InputDevice.SOURCE_TOUCHSCREEN + instrumentation.uiAutomation.injectInputEvent(motionMove, true) + motionMove.recycle() + + currentTime += GESTURE_STEP_MS + if (i == steps - 1) { + currentX = to.x.toFloat() + currentY = to.y.toFloat() + } else { + currentX += stepX + currentY += stepY + } + SystemClock.sleep(GESTURE_STEP_MS) + } + } + + fun createShortcutOnHotseatIfNotExist( + taplInstrumentation: LauncherInstrumentation, + appName: String + ) { + taplInstrumentation.workspace + .deleteAppIcon(taplInstrumentation.workspace.getHotseatAppIcon(0)) + val allApps = taplInstrumentation.workspace.switchToAllApps() + allApps.freeze() + try { + val appIconSrc = allApps.getAppIcon(appName) + Assert.assertNotNull("Unable to find app icon", appIconSrc) + val appIconDest = appIconSrc.dragToHotseat(0) + Assert.assertNotNull("Unable to drag app icon on hotseat", appIconDest) + } finally { + allApps.unfreeze() + } + } } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenDockActivity.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenDockActivity.kt deleted file mode 100644 index c86a1229d8d8..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenDockActivity.kt +++ /dev/null @@ -1,116 +0,0 @@ -/* - * Copyright (C) 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.wm.shell.flicker.legacysplitscreen - -import android.platform.test.annotations.Presubmit -import android.view.Surface -import android.view.WindowManagerPolicyConstants -import androidx.test.filters.FlakyTest -import androidx.test.filters.RequiresDevice -import com.android.server.wm.flicker.FlickerParametersRunnerFactory -import com.android.server.wm.flicker.FlickerTestParameter -import com.android.server.wm.flicker.FlickerTestParameterFactory -import com.android.server.wm.flicker.annotation.Group4 -import com.android.server.wm.flicker.dsl.FlickerBuilder -import com.android.server.wm.flicker.helpers.launchSplitScreen -import com.android.server.wm.flicker.navBarWindowIsVisible -import com.android.server.wm.flicker.statusBarWindowIsVisible -import com.android.server.wm.traces.common.FlickerComponentName -import com.android.wm.shell.flicker.dockedStackDividerBecomesVisible -import com.android.wm.shell.flicker.dockedStackPrimaryBoundsIsVisibleAtEnd -import com.android.wm.shell.flicker.helpers.SplitScreenHelper -import org.junit.FixMethodOrder -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.MethodSorters -import org.junit.runners.Parameterized - -/** - * Test open activity and dock to primary split screen - * To run this test: `atest WMShellFlickerTests:EnterSplitScreenDockActivity` - */ -@Presubmit -@RequiresDevice -@RunWith(Parameterized::class) -@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -@Group4 -class EnterSplitScreenDockActivity( - testSpec: FlickerTestParameter -) : LegacySplitScreenTransition(testSpec) { - override val transition: FlickerBuilder.() -> Unit - get() = { - super.transition(this) - transitions { - device.launchSplitScreen(wmHelper) - } - } - - override val ignoredWindows: List<FlickerComponentName> - get() = listOf(LAUNCHER_COMPONENT, LIVE_WALLPAPER_COMPONENT, - splitScreenApp.component, FlickerComponentName.SPLASH_SCREEN, - FlickerComponentName.SNAPSHOT, LAUNCHER_COMPONENT) - - @Presubmit - @Test - fun dockedStackPrimaryBoundsIsVisibleAtEnd() = - testSpec.dockedStackPrimaryBoundsIsVisibleAtEnd(testSpec.startRotation, - splitScreenApp.component) - - @Presubmit - @Test - fun dockedStackDividerBecomesVisible() = testSpec.dockedStackDividerBecomesVisible() - - @Presubmit - @Test - fun navBarWindowIsVisible() = testSpec.navBarWindowIsVisible() - - @Presubmit - @Test - fun statusBarWindowIsVisible() = testSpec.statusBarWindowIsVisible() - - @Presubmit - @Test - fun appWindowIsVisible() { - testSpec.assertWmEnd { - isAppWindowVisible(splitScreenApp.component) - } - } - - @FlakyTest - @Test - override fun visibleLayersShownMoreThanOneConsecutiveEntry() = - super.visibleLayersShownMoreThanOneConsecutiveEntry() - - @Presubmit - @Test - override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = - super.visibleWindowsShownMoreThanOneConsecutiveEntry() - - companion object { - @Parameterized.Parameters(name = "{0}") - @JvmStatic - fun getParams(): Collection<FlickerTestParameter> { - return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests( - repetitions = SplitScreenHelper.TEST_REPETITIONS, - supportedRotations = listOf(Surface.ROTATION_0), // bugId = 179116910 - supportedNavigationModes = listOf( - WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL_OVERLAY) - ) - } - } -} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenFromDetachedRecentTask.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenFromDetachedRecentTask.kt deleted file mode 100644 index 2f9244be9c18..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenFromDetachedRecentTask.kt +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright (C) 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.wm.shell.flicker.legacysplitscreen - -import android.platform.test.annotations.Presubmit -import android.view.Surface -import androidx.test.filters.RequiresDevice -import com.android.server.wm.flicker.FlickerParametersRunnerFactory -import com.android.server.wm.flicker.FlickerTestParameter -import com.android.server.wm.flicker.FlickerTestParameterFactory -import com.android.server.wm.flicker.annotation.Group4 -import com.android.server.wm.flicker.dsl.FlickerBuilder -import com.android.server.wm.flicker.helpers.launchSplitScreen -import com.android.server.wm.traces.common.FlickerComponentName -import com.android.wm.shell.flicker.dockedStackDividerIsVisibleAtEnd -import com.android.wm.shell.flicker.helpers.SplitScreenHelper -import org.junit.FixMethodOrder -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.MethodSorters -import org.junit.runners.Parameterized - -/** - * Test enter split screen from a detached recent task - * - * To run this test: `atest WMShellFlickerTests:EnterSplitScreenFromDetachedRecentTask` - */ -@RequiresDevice -@RunWith(Parameterized::class) -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) -@Group4 -class EnterSplitScreenFromDetachedRecentTask( - testSpec: FlickerTestParameter -) : LegacySplitScreenTransition(testSpec) { - - override val transition: FlickerBuilder.() -> Unit - get() = { - cleanSetup(this) - setup { - eachRun { - splitScreenApp.launchViaIntent(wmHelper) - // Press back to remove the task, but it should still be shown in recent. - device.pressBack() - } - } - transitions { - device.launchSplitScreen(wmHelper) - } - } - - override val ignoredWindows: List<FlickerComponentName> - get() = listOf(LAUNCHER_COMPONENT, - FlickerComponentName.SPLASH_SCREEN, - FlickerComponentName.SNAPSHOT, - splitScreenApp.component) - - @Presubmit - @Test - fun dockedStackDividerIsVisibleAtEnd() = testSpec.dockedStackDividerIsVisibleAtEnd() - - @Presubmit - @Test - fun appWindowIsVisible() { - testSpec.assertWmEnd { - isAppWindowVisible(splitScreenApp.component) - } - } - - @Presubmit - @Test - override fun visibleLayersShownMoreThanOneConsecutiveEntry() = - super.visibleLayersShownMoreThanOneConsecutiveEntry() - - @Presubmit - @Test - override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = - super.visibleWindowsShownMoreThanOneConsecutiveEntry() - - companion object { - @Parameterized.Parameters(name = "{0}") - @JvmStatic - fun getParams(): Collection<FlickerTestParameter> { - return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests( - repetitions = SplitScreenHelper.TEST_REPETITIONS, - supportedRotations = listOf(Surface.ROTATION_0) // bugId = 179116910 - ) - } - } -}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenLaunchToSide.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenLaunchToSide.kt deleted file mode 100644 index 1740c3ec24ca..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenLaunchToSide.kt +++ /dev/null @@ -1,126 +0,0 @@ -/* - * Copyright (C) 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.wm.shell.flicker.legacysplitscreen - -import android.platform.test.annotations.Presubmit -import android.view.Surface -import androidx.test.filters.RequiresDevice -import com.android.server.wm.flicker.FlickerParametersRunnerFactory -import com.android.server.wm.flicker.FlickerTestParameter -import com.android.server.wm.flicker.FlickerTestParameterFactory -import com.android.server.wm.flicker.annotation.Group4 -import com.android.server.wm.flicker.dsl.FlickerBuilder -import com.android.server.wm.flicker.helpers.launchSplitScreen -import com.android.server.wm.flicker.helpers.reopenAppFromOverview -import com.android.server.wm.flicker.navBarWindowIsVisible -import com.android.server.wm.flicker.statusBarWindowIsVisible -import com.android.server.wm.traces.common.FlickerComponentName -import com.android.wm.shell.flicker.dockedStackDividerBecomesVisible -import com.android.wm.shell.flicker.dockedStackPrimaryBoundsIsVisibleAtEnd -import com.android.wm.shell.flicker.dockedStackSecondaryBoundsIsVisibleAtEnd -import com.android.wm.shell.flicker.helpers.SplitScreenHelper -import org.junit.FixMethodOrder -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.MethodSorters -import org.junit.runners.Parameterized - -/** - * Test open activity to primary split screen and dock secondary activity to side - * To run this test: `atest WMShellFlickerTests:EnterSplitScreenLaunchToSide` - */ -@RequiresDevice -@RunWith(Parameterized::class) -@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -@Group4 -class EnterSplitScreenLaunchToSide( - testSpec: FlickerTestParameter -) : LegacySplitScreenTransition(testSpec) { - override val transition: FlickerBuilder.() -> Unit - get() = { - super.transition(this) - transitions { - device.launchSplitScreen(wmHelper) - device.reopenAppFromOverview(wmHelper) - } - } - - override val ignoredWindows: List<FlickerComponentName> - get() = listOf(LAUNCHER_COMPONENT, splitScreenApp.component, - secondaryApp.component, FlickerComponentName.SPLASH_SCREEN, - FlickerComponentName.SNAPSHOT) - - @Presubmit - @Test - fun dockedStackPrimaryBoundsIsVisibleAtEnd() = - testSpec.dockedStackPrimaryBoundsIsVisibleAtEnd(testSpec.startRotation, - splitScreenApp.component) - - @Presubmit - @Test - fun dockedStackSecondaryBoundsIsVisibleAtEnd() = - testSpec.dockedStackSecondaryBoundsIsVisibleAtEnd(testSpec.startRotation, - secondaryApp.component) - - @Presubmit - @Test - fun dockedStackDividerBecomesVisible() = testSpec.dockedStackDividerBecomesVisible() - - @Presubmit - @Test - fun appWindowBecomesVisible() { - testSpec.assertWm { - // when the app is launched, first the activity becomes visible, then the - // SnapshotStartingWindow appears and then the app window becomes visible. - // Because we log WM once per frame, sometimes the activity and the window - // become visible in the same entry, sometimes not, thus it is not possible to - // assert the visibility of the activity here - this.isAppWindowInvisible(secondaryApp.component) - .then() - // during re-parenting, the window may disappear and reappear from the - // trace, this occurs because we log only 1x per frame - .notContains(secondaryApp.component, isOptional = true) - .then() - .isAppWindowVisible(secondaryApp.component) - } - } - - @Presubmit - @Test - fun navBarWindowIsVisible() = testSpec.navBarWindowIsVisible() - - @Presubmit - @Test - fun statusBarWindowIsVisible() = testSpec.statusBarWindowIsVisible() - - @Presubmit - @Test - override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = - super.visibleWindowsShownMoreThanOneConsecutiveEntry() - - companion object { - @Parameterized.Parameters(name = "{0}") - @JvmStatic - fun getParams(): Collection<FlickerTestParameter> { - return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests( - repetitions = SplitScreenHelper.TEST_REPETITIONS, - supportedRotations = listOf(Surface.ROTATION_0) // bugId = 175687842 - ) - } - } -} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenNotSupportNonResizable.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenNotSupportNonResizable.kt deleted file mode 100644 index 4c063b918e96..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenNotSupportNonResizable.kt +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright (C) 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.wm.shell.flicker.legacysplitscreen - -import android.platform.test.annotations.Presubmit -import android.view.Surface -import androidx.test.filters.RequiresDevice -import com.android.server.wm.flicker.FlickerParametersRunnerFactory -import com.android.server.wm.flicker.FlickerTestParameter -import com.android.server.wm.flicker.FlickerTestParameterFactory -import com.android.server.wm.flicker.annotation.Group4 -import com.android.server.wm.flicker.dsl.FlickerBuilder -import com.android.server.wm.flicker.helpers.canSplitScreen -import com.android.server.wm.traces.common.FlickerComponentName -import com.android.wm.shell.flicker.dockedStackDividerNotExistsAtEnd -import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.resetMultiWindowConfig -import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.setSupportsNonResizableMultiWindow -import com.android.wm.shell.flicker.helpers.SplitScreenHelper -import org.junit.After -import org.junit.Assert -import org.junit.Before -import org.junit.FixMethodOrder -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.MethodSorters -import org.junit.runners.Parameterized - -/** - * Test enter split screen from non-resizable activity. When the device doesn't support - * non-resizable in multi window, there should be no button to enter split screen for non-resizable - * activity. - * - * To run this test: `atest WMShellFlickerTests:EnterSplitScreenNotSupportNonResizable` - */ -@RequiresDevice -@RunWith(Parameterized::class) -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) -@Group4 -class EnterSplitScreenNotSupportNonResizable( - testSpec: FlickerTestParameter -) : LegacySplitScreenTransition(testSpec) { - - override val transition: FlickerBuilder.() -> Unit - get() = { - cleanSetup(this) - setup { - eachRun { - nonResizeableApp.launchViaIntent(wmHelper) - } - } - transitions { - if (device.canSplitScreen(wmHelper)) { - Assert.fail("Non-resizeable app should not enter split screen") - } - } - } - - override val ignoredWindows: List<FlickerComponentName> - get() = listOf(LAUNCHER_COMPONENT, - FlickerComponentName.SPLASH_SCREEN, - FlickerComponentName.SNAPSHOT, - nonResizeableApp.component, - splitScreenApp.component) - - @Before - override fun setup() { - super.setup() - setSupportsNonResizableMultiWindow(instrumentation, -1) - } - - @After - override fun teardown() { - super.teardown() - resetMultiWindowConfig(instrumentation) - } - - @Presubmit - @Test - fun dockedStackDividerNotExistsAtEnd() = testSpec.dockedStackDividerNotExistsAtEnd() - - @Presubmit - @Test - override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = - super.visibleWindowsShownMoreThanOneConsecutiveEntry() - - companion object { - @Parameterized.Parameters(name = "{0}") - @JvmStatic - fun getParams(): Collection<FlickerTestParameter> { - return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests( - repetitions = SplitScreenHelper.TEST_REPETITIONS, - supportedRotations = listOf(Surface.ROTATION_0)) // b/178685668 - } - } -}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenSupportNonResizable.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenSupportNonResizable.kt deleted file mode 100644 index f75dee619564..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenSupportNonResizable.kt +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Copyright (C) 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.wm.shell.flicker.legacysplitscreen - -import android.platform.test.annotations.Presubmit -import android.view.Surface -import androidx.test.filters.RequiresDevice -import com.android.server.wm.flicker.FlickerParametersRunnerFactory -import com.android.server.wm.flicker.FlickerTestParameter -import com.android.server.wm.flicker.FlickerTestParameterFactory -import com.android.server.wm.flicker.annotation.Group2 -import com.android.server.wm.flicker.dsl.FlickerBuilder -import com.android.server.wm.flicker.helpers.launchSplitScreen -import com.android.server.wm.traces.common.FlickerComponentName -import com.android.wm.shell.flicker.dockedStackDividerIsVisibleAtEnd -import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.resetMultiWindowConfig -import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.setSupportsNonResizableMultiWindow -import com.android.wm.shell.flicker.helpers.SplitScreenHelper -import org.junit.After -import org.junit.Before -import org.junit.FixMethodOrder -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.MethodSorters -import org.junit.runners.Parameterized - -/** - * Test enter split screen from non-resizable activity. When the device supports - * non-resizable in multi window, there should be a button to enter split screen for non-resizable - * activity. - * - * To run this test: `atest WMShellFlickerTests:EnterSplitScreenSupportNonResizable` - */ -@RequiresDevice -@RunWith(Parameterized::class) -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) -@Group2 -class EnterSplitScreenSupportNonResizable( - testSpec: FlickerTestParameter -) : LegacySplitScreenTransition(testSpec) { - - override val transition: FlickerBuilder.() -> Unit - get() = { - cleanSetup(this) - setup { - eachRun { - nonResizeableApp.launchViaIntent(wmHelper) - } - } - transitions { - device.launchSplitScreen(wmHelper) - } - } - - override val ignoredWindows: List<FlickerComponentName> - get() = listOf(LAUNCHER_COMPONENT, - FlickerComponentName.SPLASH_SCREEN, - FlickerComponentName.SNAPSHOT, - nonResizeableApp.component, - splitScreenApp.component) - - @Before - override fun setup() { - super.setup() - setSupportsNonResizableMultiWindow(instrumentation, 1) - } - - @After - override fun teardown() { - super.teardown() - resetMultiWindowConfig(instrumentation) - } - - @Presubmit - @Test - fun dockedStackDividerIsVisibleAtEnd() = testSpec.dockedStackDividerIsVisibleAtEnd() - - @Presubmit - @Test - fun appWindowIsVisible() { - testSpec.assertWmEnd { - isAppWindowVisible(nonResizeableApp.component) - } - } - - @Presubmit - @Test - override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = - super.visibleWindowsShownMoreThanOneConsecutiveEntry() - - companion object { - @Parameterized.Parameters(name = "{0}") - @JvmStatic - fun getParams(): Collection<FlickerTestParameter> { - return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests( - repetitions = SplitScreenHelper.TEST_REPETITIONS, - supportedRotations = listOf(Surface.ROTATION_0)) // b/178685668 - } - } -}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ExitLegacySplitScreenFromBottom.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ExitLegacySplitScreenFromBottom.kt deleted file mode 100644 index ef7d65e8a732..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ExitLegacySplitScreenFromBottom.kt +++ /dev/null @@ -1,124 +0,0 @@ -/* - * Copyright (C) 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.wm.shell.flicker.legacysplitscreen - -import android.view.Surface -import androidx.test.filters.FlakyTest -import androidx.test.filters.RequiresDevice -import com.android.server.wm.flicker.FlickerParametersRunnerFactory -import com.android.server.wm.flicker.FlickerTestParameter -import com.android.server.wm.flicker.FlickerTestParameterFactory -import com.android.server.wm.flicker.annotation.Group2 -import com.android.server.wm.flicker.dsl.FlickerBuilder -import com.android.server.wm.flicker.helpers.exitSplitScreenFromBottom -import com.android.server.wm.flicker.helpers.launchSplitScreen -import com.android.server.wm.flicker.navBarWindowIsVisible -import com.android.server.wm.flicker.statusBarWindowIsVisible -import com.android.server.wm.traces.common.FlickerComponentName -import com.android.wm.shell.flicker.DOCKED_STACK_DIVIDER_COMPONENT -import com.android.wm.shell.flicker.helpers.SplitScreenHelper -import org.junit.FixMethodOrder -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.MethodSorters -import org.junit.runners.Parameterized - -/** - * Test open resizeable activity split in primary, and drag divider to bottom exit split screen - * To run this test: `atest WMShellFlickerTests:ExitLegacySplitScreenFromBottom` - */ -@RequiresDevice -@RunWith(Parameterized::class) -@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -@Group2 -class ExitLegacySplitScreenFromBottom( - testSpec: FlickerTestParameter -) : LegacySplitScreenTransition(testSpec) { - override val transition: FlickerBuilder.() -> Unit - get() = { - super.transition(this) - setup { - eachRun { - splitScreenApp.launchViaIntent(wmHelper) - device.launchSplitScreen(wmHelper) - } - } - teardown { - eachRun { - splitScreenApp.exit(wmHelper) - } - } - transitions { - device.exitSplitScreenFromBottom(wmHelper) - } - } - - override val ignoredWindows: List<FlickerComponentName> - get() = listOf(LAUNCHER_COMPONENT, FlickerComponentName.SPLASH_SCREEN, - splitScreenApp.component, secondaryApp.component, - FlickerComponentName.SNAPSHOT) - - @FlakyTest - @Test - fun layerBecomesInvisible() { - testSpec.assertLayers { - this.isVisible(DOCKED_STACK_DIVIDER_COMPONENT) - .then() - .isInvisible(DOCKED_STACK_DIVIDER_COMPONENT) - } - } - - @FlakyTest - @Test - fun appWindowBecomesInVisible() { - testSpec.assertWm { - this.isAppWindowVisible(secondaryApp.component) - .then() - .isAppWindowInvisible(secondaryApp.component) - } - } - - @FlakyTest - @Test - fun navBarWindowIsVisible() = testSpec.navBarWindowIsVisible() - - @FlakyTest - @Test - fun statusBarWindowIsVisible() = testSpec.statusBarWindowIsVisible() - - @FlakyTest - @Test - override fun visibleLayersShownMoreThanOneConsecutiveEntry() = - super.visibleLayersShownMoreThanOneConsecutiveEntry() - - @FlakyTest - @Test - override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = - super.visibleWindowsShownMoreThanOneConsecutiveEntry() - - companion object { - @Parameterized.Parameters(name = "{0}") - @JvmStatic - fun getParams(): Collection<FlickerTestParameter> { - return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests( - repetitions = SplitScreenHelper.TEST_REPETITIONS, - supportedRotations = listOf(Surface.ROTATION_0) // b/175687842 - ) - } - } -}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ExitPrimarySplitScreenShowSecondaryFullscreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ExitPrimarySplitScreenShowSecondaryFullscreen.kt deleted file mode 100644 index d913a6d85d3d..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ExitPrimarySplitScreenShowSecondaryFullscreen.kt +++ /dev/null @@ -1,129 +0,0 @@ -/* - * Copyright (C) 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.wm.shell.flicker.legacysplitscreen - -import android.platform.test.annotations.Presubmit -import android.view.Surface -import androidx.test.filters.FlakyTest -import androidx.test.filters.RequiresDevice -import com.android.server.wm.flicker.FlickerParametersRunnerFactory -import com.android.server.wm.flicker.FlickerTestParameter -import com.android.server.wm.flicker.FlickerTestParameterFactory -import com.android.server.wm.flicker.annotation.Group2 -import com.android.server.wm.flicker.dsl.FlickerBuilder -import com.android.server.wm.flicker.helpers.launchSplitScreen -import com.android.server.wm.flicker.helpers.reopenAppFromOverview -import com.android.server.wm.flicker.navBarWindowIsVisible -import com.android.server.wm.flicker.statusBarWindowIsVisible -import com.android.server.wm.traces.common.FlickerComponentName -import com.android.wm.shell.flicker.dockedStackDividerNotExistsAtEnd -import com.android.wm.shell.flicker.helpers.SplitScreenHelper -import org.junit.FixMethodOrder -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.MethodSorters -import org.junit.runners.Parameterized - -/** - * Test dock activity to primary split screen, and open secondary to side, exit primary split - * and test secondary activity become full screen. - * To run this test: `atest WMShellFlickerTests:ExitPrimarySplitScreenShowSecondaryFullscreen` - */ -@RequiresDevice -@RunWith(Parameterized::class) -@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -@Group2 -class ExitPrimarySplitScreenShowSecondaryFullscreen( - testSpec: FlickerTestParameter -) : LegacySplitScreenTransition(testSpec) { - override val transition: FlickerBuilder.() -> Unit - get() = { - super.transition(this) - teardown { - eachRun { - secondaryApp.exit(wmHelper) - } - } - transitions { - splitScreenApp.launchViaIntent(wmHelper) - secondaryApp.launchViaIntent(wmHelper) - device.launchSplitScreen(wmHelper) - device.reopenAppFromOverview(wmHelper) - // TODO(b/175687842) Can not find Split screen divider, use exit() instead - splitScreenApp.exit(wmHelper) - } - } - - override val ignoredWindows: List<FlickerComponentName> - get() = listOf(LAUNCHER_COMPONENT, FlickerComponentName.SPLASH_SCREEN, - splitScreenApp.component, secondaryApp.component, - FlickerComponentName.SNAPSHOT) - - @Presubmit - @Test - fun dockedStackDividerNotExistsAtEnd() = testSpec.dockedStackDividerNotExistsAtEnd() - - @FlakyTest - @Test - fun layerBecomesInvisible() { - testSpec.assertLayers { - this.isVisible(splitScreenApp.component) - .then() - .isInvisible(splitScreenApp.component) - } - } - - @FlakyTest - @Test - fun appWindowBecomesInVisible() { - testSpec.assertWm { - this.isAppWindowVisible(splitScreenApp.component) - .then() - .isAppWindowInvisible(splitScreenApp.component) - } - } - - @Presubmit - @Test - fun navBarWindowIsVisible() = testSpec.navBarWindowIsVisible() - - @Presubmit - @Test - fun statusBarWindowIsVisible() = testSpec.statusBarWindowIsVisible() - - @Presubmit - @Test - override fun visibleLayersShownMoreThanOneConsecutiveEntry() = - super.visibleLayersShownMoreThanOneConsecutiveEntry() - - @Presubmit - @Test - override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = - super.visibleWindowsShownMoreThanOneConsecutiveEntry() - - companion object { - @Parameterized.Parameters(name = "{0}") - @JvmStatic - fun getParams(): Collection<FlickerTestParameter> { - return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests( - repetitions = SplitScreenHelper.TEST_REPETITIONS, - supportedRotations = listOf(Surface.ROTATION_0) // bugId = 179116910 - ) - } - } -} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenFromIntentNotSupportNonResizable.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenFromIntentNotSupportNonResizable.kt deleted file mode 100644 index f3ff7b156aaf..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenFromIntentNotSupportNonResizable.kt +++ /dev/null @@ -1,196 +0,0 @@ -/* - * Copyright (C) 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.wm.shell.flicker.legacysplitscreen - -import android.platform.test.annotations.Presubmit -import android.view.Surface -import androidx.test.filters.RequiresDevice -import com.android.server.wm.flicker.FlickerParametersRunnerFactory -import com.android.server.wm.flicker.FlickerTestParameter -import com.android.server.wm.flicker.FlickerTestParameterFactory -import com.android.server.wm.flicker.annotation.Group2 -import com.android.server.wm.flicker.dsl.FlickerBuilder -import com.android.server.wm.flicker.helpers.launchSplitScreen -import com.android.server.wm.traces.common.FlickerComponentName -import com.android.wm.shell.flicker.DOCKED_STACK_DIVIDER_COMPONENT -import com.android.wm.shell.flicker.dockedStackDividerNotExistsAtEnd -import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.resetMultiWindowConfig -import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.setSupportsNonResizableMultiWindow -import com.android.wm.shell.flicker.helpers.SplitScreenHelper -import org.junit.After -import org.junit.Before -import org.junit.FixMethodOrder -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.MethodSorters -import org.junit.runners.Parameterized - -/** - * Test launch non-resizable activity via intent in split screen mode. When the device does not - * support non-resizable in multi window, it should trigger exit split screen. - * To run this test: `atest WMShellFlickerTests:LegacySplitScreenFromIntentNotSupportNonResizable` - */ -@RequiresDevice -@RunWith(Parameterized::class) -@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -@Group2 -class LegacySplitScreenFromIntentNotSupportNonResizable( - testSpec: FlickerTestParameter -) : LegacySplitScreenTransition(testSpec) { - - override val transition: FlickerBuilder.() -> Unit - get() = { - cleanSetup(this) - setup { - eachRun { - splitScreenApp.launchViaIntent(wmHelper) - device.launchSplitScreen(wmHelper) - } - } - transitions { - nonResizeableApp.launchViaIntent(wmHelper) - wmHelper.waitForAppTransitionIdle() - } - } - - override val ignoredWindows: List<FlickerComponentName> - get() = listOf(DOCKED_STACK_DIVIDER_COMPONENT, LAUNCHER_COMPONENT, LETTERBOX_COMPONENT, - nonResizeableApp.component, splitScreenApp.component, - FlickerComponentName.SPLASH_SCREEN, - FlickerComponentName.SNAPSHOT) - - @Before - override fun setup() { - super.setup() - setSupportsNonResizableMultiWindow(instrumentation, -1) - } - - @After - override fun teardown() { - super.teardown() - resetMultiWindowConfig(instrumentation) - } - - @Presubmit - @Test - fun resizableAppLayerBecomesInvisible() { - testSpec.assertLayers { - this.isVisible(splitScreenApp.component) - .then() - .isInvisible(splitScreenApp.component) - } - } - - @Presubmit - @Test - fun nonResizableAppLayerBecomesVisible() { - testSpec.assertLayers { - this.notContains(nonResizeableApp.component) - .then() - .isInvisible(nonResizeableApp.component) - .then() - .isVisible(nonResizeableApp.component) - } - } - - /** - * Assets that [splitScreenApp] exists at the start of the trace and, once it becomes - * invisible, it remains invisible until the end of the trace. - */ - @Presubmit - @Test - fun resizableAppWindowBecomesInvisible() { - testSpec.assertWm { - // when the activity gets PAUSED the window may still be marked as visible - // it will be updated in the next log entry. This occurs because we record 1x - // per frame, thus ignore activity check here - this.isAppWindowVisible(splitScreenApp.component) - .then() - // immediately after the window (after onResume and before perform relayout) - // the activity is invisible. This may or not be logged, since we record 1x - // per frame, thus ignore activity check here - .isAppWindowInvisible(splitScreenApp.component) - } - } - - /** - * Assets that [nonResizeableApp] doesn't exist at the start of the trace, then - * [nonResizeableApp] is created (visible or not) and, once [nonResizeableApp] becomes - * visible, it remains visible until the end of the trace. - */ - @Presubmit - @Test - fun nonResizableAppWindowBecomesVisible() { - testSpec.assertWm { - this.notContains(nonResizeableApp.component) - .then() - // we log once per frame, upon logging, window may be visible or not depending - // on what was processed until that moment. Both behaviors are correct - .isAppWindowInvisible(nonResizeableApp.component, isOptional = true) - .then() - // immediately after the window (after onResume and before perform relayout) - // the activity is invisible. This may or not be logged, since we record 1x - // per frame, thus ignore activity check here - .isAppWindowVisible(nonResizeableApp.component) - } - } - - /** - * Asserts that both the app window and the activity are visible at the end of the trace - */ - @Presubmit - @Test - fun nonResizableAppWindowBecomesVisibleAtEnd() { - testSpec.assertWmEnd { - isAppWindowVisible(nonResizeableApp.component) - } - } - - @Presubmit - @Test - fun dockedStackDividerNotExistsAtEnd() = testSpec.dockedStackDividerNotExistsAtEnd() - - @Presubmit - @Test - fun onlyNonResizableAppWindowIsVisibleAtEnd() { - testSpec.assertWmEnd { - isAppWindowInvisible(splitScreenApp.component) - isAppWindowVisible(nonResizeableApp.component) - } - } - - @Presubmit - @Test - override fun visibleLayersShownMoreThanOneConsecutiveEntry() = - super.visibleLayersShownMoreThanOneConsecutiveEntry() - - @Presubmit - @Test - override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = - super.visibleWindowsShownMoreThanOneConsecutiveEntry() - - companion object { - @Parameterized.Parameters(name = "{0}") - @JvmStatic - fun getParams(): Collection<FlickerTestParameter> { - return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests( - repetitions = SplitScreenHelper.TEST_REPETITIONS, - supportedRotations = listOf(Surface.ROTATION_0)) // b/178685668 - } - } -} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenFromIntentSupportNonResizable.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenFromIntentSupportNonResizable.kt deleted file mode 100644 index 42e707ab0850..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenFromIntentSupportNonResizable.kt +++ /dev/null @@ -1,153 +0,0 @@ -/* - * Copyright (C) 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.wm.shell.flicker.legacysplitscreen - -import android.platform.test.annotations.Presubmit -import android.view.Surface -import androidx.test.filters.RequiresDevice -import com.android.server.wm.flicker.FlickerParametersRunnerFactory -import com.android.server.wm.flicker.FlickerTestParameter -import com.android.server.wm.flicker.FlickerTestParameterFactory -import com.android.server.wm.flicker.annotation.Group2 -import com.android.server.wm.flicker.dsl.FlickerBuilder -import com.android.server.wm.flicker.helpers.launchSplitScreen -import com.android.server.wm.traces.common.FlickerComponentName -import com.android.wm.shell.flicker.DOCKED_STACK_DIVIDER_COMPONENT -import com.android.wm.shell.flicker.dockedStackDividerIsVisibleAtEnd -import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.resetMultiWindowConfig -import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.setSupportsNonResizableMultiWindow -import com.android.wm.shell.flicker.helpers.SplitScreenHelper -import org.junit.After -import org.junit.Before -import org.junit.FixMethodOrder -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.MethodSorters -import org.junit.runners.Parameterized - -/** - * Test launch non-resizable activity via intent in split screen mode. When the device supports - * non-resizable in multi window, it should show the non-resizable app in split screen. - * To run this test: `atest WMShellFlickerTests:LegacySplitScreenFromIntentSupportNonResizable` - */ -@RequiresDevice -@RunWith(Parameterized::class) -@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -@Group2 -class LegacySplitScreenFromIntentSupportNonResizable( - testSpec: FlickerTestParameter -) : LegacySplitScreenTransition(testSpec) { - - override val transition: FlickerBuilder.() -> Unit - get() = { - cleanSetup(this) - setup { - eachRun { - splitScreenApp.launchViaIntent(wmHelper) - device.launchSplitScreen(wmHelper) - } - } - transitions { - nonResizeableApp.launchViaIntent(wmHelper) - wmHelper.waitForAppTransitionIdle() - } - } - - override val ignoredWindows: List<FlickerComponentName> - get() = listOf(DOCKED_STACK_DIVIDER_COMPONENT, LAUNCHER_COMPONENT, LETTERBOX_COMPONENT, - nonResizeableApp.component, splitScreenApp.component, - FlickerComponentName.SPLASH_SCREEN, - FlickerComponentName.SNAPSHOT) - - @Before - override fun setup() { - super.setup() - setSupportsNonResizableMultiWindow(instrumentation, 1) - } - - @After - override fun teardown() { - super.teardown() - resetMultiWindowConfig(instrumentation) - } - - @Presubmit - @Test - fun nonResizableAppLayerBecomesVisible() { - testSpec.assertLayers { - this.isInvisible(nonResizeableApp.component) - .then() - .isVisible(nonResizeableApp.component) - } - } - - /** - * Assets that [nonResizeableApp] doesn't exist at the start of the trace, then - * [nonResizeableApp] is created (visible or not) and, once [nonResizeableApp] becomes - * visible, it remains visible until the end of the trace. - */ - @Presubmit - @Test - fun nonResizableAppWindowBecomesVisible() { - testSpec.assertWm { - this.notContains(nonResizeableApp.component) - .then() - // we log once per frame, upon logging, window may be visible or not depending - // on what was processed until that moment. Both behaviors are correct - .isAppWindowInvisible(nonResizeableApp.component, isOptional = true) - .then() - // immediately after the window (after onResume and before perform relayout) - // the activity is invisible. This may or not be logged, since we record 1x - // per frame, thus ignore activity check here - .isAppWindowVisible(nonResizeableApp.component) - } - } - - @Presubmit - @Test - fun dockedStackDividerIsVisibleAtEnd() = testSpec.dockedStackDividerIsVisibleAtEnd() - - @Presubmit - @Test - fun bothAppsWindowsAreVisibleAtEnd() { - testSpec.assertWmEnd { - isAppWindowVisible(splitScreenApp.component) - isAppWindowVisible(nonResizeableApp.component) - } - } - - @Presubmit - @Test - override fun visibleLayersShownMoreThanOneConsecutiveEntry() = - super.visibleLayersShownMoreThanOneConsecutiveEntry() - - @Presubmit - @Test - override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = - super.visibleWindowsShownMoreThanOneConsecutiveEntry() - - companion object { - @Parameterized.Parameters(name = "{0}") - @JvmStatic - fun getParams(): Collection<FlickerTestParameter> { - return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests( - repetitions = SplitScreenHelper.TEST_REPETITIONS, - supportedRotations = listOf(Surface.ROTATION_0)) // b/178685668 - } - } -} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenFromRecentNotSupportNonResizable.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenFromRecentNotSupportNonResizable.kt deleted file mode 100644 index c1fba7d1530c..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenFromRecentNotSupportNonResizable.kt +++ /dev/null @@ -1,169 +0,0 @@ -/* - * Copyright (C) 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.wm.shell.flicker.legacysplitscreen - -import android.platform.test.annotations.Presubmit -import android.view.Surface -import androidx.test.filters.FlakyTest -import androidx.test.filters.RequiresDevice -import com.android.server.wm.flicker.FlickerParametersRunnerFactory -import com.android.server.wm.flicker.FlickerTestParameter -import com.android.server.wm.flicker.FlickerTestParameterFactory -import com.android.server.wm.flicker.annotation.Group2 -import com.android.server.wm.flicker.dsl.FlickerBuilder -import com.android.server.wm.flicker.helpers.launchSplitScreen -import com.android.server.wm.flicker.helpers.reopenAppFromOverview -import com.android.server.wm.traces.common.FlickerComponentName -import com.android.wm.shell.flicker.DOCKED_STACK_DIVIDER_COMPONENT -import com.android.wm.shell.flicker.dockedStackDividerNotExistsAtEnd -import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.resetMultiWindowConfig -import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.setSupportsNonResizableMultiWindow -import com.android.wm.shell.flicker.helpers.SplitScreenHelper -import org.junit.After -import org.junit.Before -import org.junit.FixMethodOrder -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.MethodSorters -import org.junit.runners.Parameterized - -/** - * Test launch non-resizable activity via recent overview in split screen mode. When the device does - * not support non-resizable in multi window, it should trigger exit split screen. - * To run this test: `atest WMShellFlickerTests:LegacySplitScreenFromRecentNotSupportNonResizable` - */ -@RequiresDevice -@RunWith(Parameterized::class) -@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -@Group2 -class LegacySplitScreenFromRecentNotSupportNonResizable( - testSpec: FlickerTestParameter -) : LegacySplitScreenTransition(testSpec) { - - override val transition: FlickerBuilder.() -> Unit - get() = { - cleanSetup(this) - setup { - eachRun { - nonResizeableApp.launchViaIntent(wmHelper) - splitScreenApp.launchViaIntent(wmHelper) - device.launchSplitScreen(wmHelper) - } - } - transitions { - device.reopenAppFromOverview(wmHelper) - } - } - - override val ignoredWindows: List<FlickerComponentName> - get() = listOf(DOCKED_STACK_DIVIDER_COMPONENT, LAUNCHER_COMPONENT, LETTERBOX_COMPONENT, - TOAST_COMPONENT, splitScreenApp.component, nonResizeableApp.component, - FlickerComponentName.SPLASH_SCREEN, - FlickerComponentName.SNAPSHOT) - - @Before - override fun setup() { - super.setup() - setSupportsNonResizableMultiWindow(instrumentation, -1) - } - - @After - override fun teardown() { - super.teardown() - resetMultiWindowConfig(instrumentation) - } - - @Presubmit - @Test - fun resizableAppLayerBecomesInvisible() { - testSpec.assertLayers { - this.isVisible(splitScreenApp.component) - .then() - .isInvisible(splitScreenApp.component) - } - } - - @Presubmit - @Test - fun nonResizableAppLayerBecomesVisible() { - testSpec.assertLayers { - this.isInvisible(nonResizeableApp.component) - .then() - .isVisible(nonResizeableApp.component) - } - } - - @Presubmit - @Test - fun resizableAppWindowBecomesInvisible() { - testSpec.assertWm { - // when the activity gets PAUSED the window may still be marked as visible - // it will be updated in the next log entry. This occurs because we record 1x - // per frame, thus ignore activity check here - this.isAppWindowVisible(splitScreenApp.component) - .then() - // immediately after the window (after onResume and before perform relayout) - // the activity is invisible. This may or not be logged, since we record 1x - // per frame, thus ignore activity check here - .isAppWindowInvisible(splitScreenApp.component) - } - } - - @FlakyTest - @Test - fun nonResizableAppWindowBecomesVisible() { - testSpec.assertWm { - this.isAppWindowInvisible(nonResizeableApp.component) - .then() - .isAppWindowVisible(nonResizeableApp.component) - } - } - - @Presubmit - @Test - fun dockedStackDividerNotExistsAtEnd() = testSpec.dockedStackDividerNotExistsAtEnd() - - @Presubmit - @Test - fun onlyNonResizableAppWindowIsVisibleAtEnd() { - testSpec.assertWmEnd { - isAppWindowInvisible(splitScreenApp.component) - isAppWindowVisible(nonResizeableApp.component) - } - } - - @Presubmit - @Test - override fun visibleLayersShownMoreThanOneConsecutiveEntry() = - super.visibleLayersShownMoreThanOneConsecutiveEntry() - - @Presubmit - @Test - override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = - super.visibleWindowsShownMoreThanOneConsecutiveEntry() - - companion object { - @Parameterized.Parameters(name = "{0}") - @JvmStatic - fun getParams(): Collection<FlickerTestParameter> { - return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests( - repetitions = SplitScreenHelper.TEST_REPETITIONS, - supportedRotations = listOf(Surface.ROTATION_0)) // b/178685668 - } - } -} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenFromRecentSupportNonResizable.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenFromRecentSupportNonResizable.kt deleted file mode 100644 index 6ac8683ac054..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenFromRecentSupportNonResizable.kt +++ /dev/null @@ -1,155 +0,0 @@ -/* - * Copyright (C) 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.wm.shell.flicker.legacysplitscreen - -import android.platform.test.annotations.Presubmit -import android.view.Surface -import androidx.test.filters.RequiresDevice -import com.android.server.wm.flicker.FlickerParametersRunnerFactory -import com.android.server.wm.flicker.FlickerTestParameter -import com.android.server.wm.flicker.FlickerTestParameterFactory -import com.android.server.wm.flicker.annotation.Group2 -import com.android.server.wm.flicker.dsl.FlickerBuilder -import com.android.server.wm.flicker.helpers.launchSplitScreen -import com.android.server.wm.flicker.helpers.reopenAppFromOverview -import com.android.server.wm.traces.common.FlickerComponentName -import com.android.wm.shell.flicker.DOCKED_STACK_DIVIDER_COMPONENT -import com.android.wm.shell.flicker.dockedStackDividerIsVisibleAtEnd -import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.resetMultiWindowConfig -import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.setSupportsNonResizableMultiWindow -import com.android.wm.shell.flicker.helpers.SplitScreenHelper -import org.junit.After -import org.junit.Before -import org.junit.FixMethodOrder -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.MethodSorters -import org.junit.runners.Parameterized - -/** - * Test launch non-resizable activity via recent overview in split screen mode. When the device - * supports non-resizable in multi window, it should show the non-resizable app in split screen. - * To run this test: `atest WMShellFlickerTests:LegacySplitScreenFromRecentSupportNonResizable` - */ -@RequiresDevice -@RunWith(Parameterized::class) -@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -@Group2 -class LegacySplitScreenFromRecentSupportNonResizable( - testSpec: FlickerTestParameter -) : LegacySplitScreenTransition(testSpec) { - - override val transition: FlickerBuilder.() -> Unit - get() = { - cleanSetup(this) - setup { - eachRun { - nonResizeableApp.launchViaIntent(wmHelper) - splitScreenApp.launchViaIntent(wmHelper) - device.launchSplitScreen(wmHelper) - } - } - transitions { - device.reopenAppFromOverview(wmHelper) - } - } - - override val ignoredWindows: List<FlickerComponentName> - get() = listOf(DOCKED_STACK_DIVIDER_COMPONENT, LAUNCHER_COMPONENT, LETTERBOX_COMPONENT, - TOAST_COMPONENT, splitScreenApp.component, nonResizeableApp.component, - FlickerComponentName.SPLASH_SCREEN, - FlickerComponentName.SNAPSHOT) - - @Before - override fun setup() { - super.setup() - setSupportsNonResizableMultiWindow(instrumentation, 1) - } - - @After - override fun teardown() { - super.teardown() - resetMultiWindowConfig(instrumentation) - } - - @Presubmit - @Test - fun nonResizableAppLayerBecomesVisible() { - testSpec.assertLayers { - this.isInvisible(nonResizeableApp.component) - .then() - .isVisible(nonResizeableApp.component) - } - } - - @Presubmit - @Test - fun nonResizableAppWindowBecomesVisible() { - testSpec.assertWm { - // when the app is launched, first the activity becomes visible, then the - // SnapshotStartingWindow appears and then the app window becomes visible. - // Because we log WM once per frame, sometimes the activity and the window - // become visible in the same entry, sometimes not, thus it is not possible to - // assert the visibility of the activity here - this.isAppWindowInvisible(nonResizeableApp.component) - .then() - // during re-parenting, the window may disappear and reappear from the - // trace, this occurs because we log only 1x per frame - .notContains(nonResizeableApp.component, isOptional = true) - .then() - // if the window reappears after re-parenting it will most likely not - // be visible in the first log entry (because we log only 1x per frame) - .isAppWindowInvisible(nonResizeableApp.component, isOptional = true) - .then() - .isAppWindowVisible(nonResizeableApp.component) - } - } - - @Presubmit - @Test - fun dockedStackDividerIsVisibleAtEnd() = testSpec.dockedStackDividerIsVisibleAtEnd() - - @Presubmit - @Test - fun bothAppsWindowsAreVisibleAtEnd() { - testSpec.assertWmEnd { - isAppWindowVisible(splitScreenApp.component) - isAppWindowVisible(nonResizeableApp.component) - } - } - - @Presubmit - @Test - override fun visibleLayersShownMoreThanOneConsecutiveEntry() = - super.visibleLayersShownMoreThanOneConsecutiveEntry() - - @Presubmit - @Test - override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = - super.visibleWindowsShownMoreThanOneConsecutiveEntry() - - companion object { - @Parameterized.Parameters(name = "{0}") - @JvmStatic - fun getParams(): Collection<FlickerTestParameter> { - return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests( - repetitions = SplitScreenHelper.TEST_REPETITIONS, - supportedRotations = listOf(Surface.ROTATION_0)) // b/178685668 - } - } -} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenRotateTransition.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenRotateTransition.kt deleted file mode 100644 index b01f41c9e2ec..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenRotateTransition.kt +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright (C) 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.wm.shell.flicker.legacysplitscreen - -import android.view.Surface -import com.android.server.wm.flicker.FlickerTestParameter -import com.android.server.wm.flicker.dsl.FlickerBuilder -import com.android.server.wm.flicker.helpers.openQuickStepAndClearRecentAppsFromOverview -import com.android.server.wm.flicker.helpers.setRotation -import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen - -abstract class LegacySplitScreenRotateTransition( - testSpec: FlickerTestParameter -) : LegacySplitScreenTransition(testSpec) { - override val transition: FlickerBuilder.() -> Unit - get() = { - setup { - eachRun { - device.wakeUpAndGoToHomeScreen() - device.openQuickStepAndClearRecentAppsFromOverview(wmHelper) - secondaryApp.launchViaIntent(wmHelper) - splitScreenApp.launchViaIntent(wmHelper) - } - } - teardown { - eachRun { - splitScreenApp.exit(wmHelper) - secondaryApp.exit(wmHelper) - this.setRotation(Surface.ROTATION_0) - } - } - } -} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenToLauncher.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenToLauncher.kt deleted file mode 100644 index fb1004bda0cb..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenToLauncher.kt +++ /dev/null @@ -1,160 +0,0 @@ -/* - * Copyright (C) 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.wm.shell.flicker.legacysplitscreen - -import android.platform.test.annotations.Presubmit -import android.view.Surface -import androidx.test.filters.FlakyTest -import androidx.test.filters.RequiresDevice -import com.android.server.wm.flicker.FlickerParametersRunnerFactory -import com.android.server.wm.flicker.FlickerTestParameter -import com.android.server.wm.flicker.FlickerTestParameterFactory -import com.android.server.wm.flicker.annotation.Group2 -import com.android.server.wm.flicker.dsl.FlickerBuilder -import com.android.server.wm.flicker.entireScreenCovered -import com.android.server.wm.flicker.helpers.exitSplitScreen -import com.android.server.wm.flicker.helpers.launchSplitScreen -import com.android.server.wm.flicker.helpers.openQuickStepAndClearRecentAppsFromOverview -import com.android.server.wm.flicker.helpers.setRotation -import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen -import com.android.server.wm.flicker.navBarLayerIsVisible -import com.android.server.wm.flicker.navBarLayerRotatesAndScales -import com.android.server.wm.flicker.navBarWindowIsVisible -import com.android.server.wm.flicker.statusBarLayerIsVisible -import com.android.server.wm.flicker.statusBarLayerRotatesScales -import com.android.server.wm.flicker.statusBarWindowIsVisible -import com.android.server.wm.traces.common.FlickerComponentName -import com.android.wm.shell.flicker.dockedStackDividerBecomesInvisible -import com.android.wm.shell.flicker.helpers.SimpleAppHelper -import org.junit.FixMethodOrder -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.MethodSorters -import org.junit.runners.Parameterized - -/** - * Test open app to split screen. - * To run this test: `atest WMShellFlickerTests:LegacySplitScreenToLauncher` - */ -@RequiresDevice -@RunWith(Parameterized::class) -@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -@Group2 -class LegacySplitScreenToLauncher( - testSpec: FlickerTestParameter -) : LegacySplitScreenTransition(testSpec) { - private val testApp = SimpleAppHelper(instrumentation) - - override val transition: FlickerBuilder.() -> Unit - get() = { - setup { - test { - device.wakeUpAndGoToHomeScreen() - device.openQuickStepAndClearRecentAppsFromOverview(wmHelper) - } - eachRun { - testApp.launchViaIntent(wmHelper) - this.setRotation(testSpec.endRotation) - device.launchSplitScreen(wmHelper) - device.waitForIdle() - } - } - teardown { - eachRun { - testApp.exit(wmHelper) - } - } - transitions { - device.exitSplitScreen() - } - } - - override val ignoredWindows: List<FlickerComponentName> - get() = listOf(LAUNCHER_COMPONENT, FlickerComponentName.SPLASH_SCREEN, - FlickerComponentName.SNAPSHOT) - - @Presubmit - @Test - fun navBarWindowIsVisible() = testSpec.navBarWindowIsVisible() - - @Presubmit - @Test - fun statusBarWindowIsVisible() = testSpec.statusBarWindowIsVisible() - - @Presubmit - @Test - fun navBarLayerIsVisible() = testSpec.navBarLayerIsVisible() - - @Presubmit - @Test - fun entireScreenCovered() = testSpec.entireScreenCovered() - - @Presubmit - @Test - fun navBarLayerRotatesAndScales() = testSpec.navBarLayerRotatesAndScales() - - @FlakyTest(bugId = 206753786) - @Test - fun statusBarLayerRotatesScales() = testSpec.statusBarLayerRotatesScales() - - @Presubmit - @Test - fun statusBarLayerIsVisible() = testSpec.statusBarLayerIsVisible() - - @FlakyTest - @Test - fun dockedStackDividerBecomesInvisible() = testSpec.dockedStackDividerBecomesInvisible() - - @FlakyTest - @Test - fun layerBecomesInvisible() { - testSpec.assertLayers { - this.isVisible(testApp.component) - .then() - .isInvisible(testApp.component) - } - } - - @FlakyTest - @Test - fun focusDoesNotChange() { - testSpec.assertEventLog { - this.focusDoesNotChange() - } - } - - @Presubmit - @Test - override fun visibleLayersShownMoreThanOneConsecutiveEntry() = - super.visibleLayersShownMoreThanOneConsecutiveEntry() - - @Presubmit - @Test - override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = - super.visibleWindowsShownMoreThanOneConsecutiveEntry() - - companion object { - @Parameterized.Parameters(name = "{0}") - @JvmStatic - fun getParams(): Collection<FlickerTestParameter> { - // b/161435597 causes the test not to work on 90 degrees - return FlickerTestParameterFactory.getInstance() - .getConfigNonRotationTests(supportedRotations = listOf(Surface.ROTATION_0)) - } - } -} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenTransition.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenTransition.kt deleted file mode 100644 index a4a1f617e497..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenTransition.kt +++ /dev/null @@ -1,149 +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/LICENSE2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES 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.legacysplitscreen - -import android.app.Instrumentation -import android.content.Context -import android.support.test.launcherhelper.LauncherStrategyFactory -import android.view.Surface -import androidx.test.filters.FlakyTest -import androidx.test.platform.app.InstrumentationRegistry -import com.android.server.wm.flicker.FlickerBuilderProvider -import com.android.server.wm.flicker.FlickerTestParameter -import com.android.server.wm.flicker.dsl.FlickerBuilder -import com.android.server.wm.flicker.helpers.openQuickStepAndClearRecentAppsFromOverview -import com.android.server.wm.flicker.helpers.setRotation -import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen -import com.android.server.wm.traces.common.FlickerComponentName -import com.android.wm.shell.flicker.helpers.BaseAppHelper.Companion.isShellTransitionsEnabled -import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.getDevEnableNonResizableMultiWindow -import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.setDevEnableNonResizableMultiWindow -import com.android.wm.shell.flicker.helpers.SplitScreenHelper -import org.junit.After -import org.junit.Assume.assumeFalse -import org.junit.Assume.assumeTrue -import org.junit.Before -import org.junit.Test - -abstract class LegacySplitScreenTransition(protected val testSpec: FlickerTestParameter) { - protected val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation() - protected val context: Context = instrumentation.context - protected val splitScreenApp = SplitScreenHelper.getPrimary(instrumentation) - protected val secondaryApp = SplitScreenHelper.getSecondary(instrumentation) - protected val nonResizeableApp = SplitScreenHelper.getNonResizeable(instrumentation) - protected val LAUNCHER_COMPONENT = FlickerComponentName("", - LauncherStrategyFactory.getInstance(instrumentation) - .launcherStrategy.supportedLauncherPackage) - private var prevDevEnableNonResizableMultiWindow = 0 - - @Before - open fun setup() { - // Only run legacy split tests when the system is using legacy split screen. - assumeTrue(SplitScreenHelper.isUsingLegacySplit()) - // Legacy split is having some issue with Shell transition, and will be deprecated soon. - assumeFalse(isShellTransitionsEnabled()) - prevDevEnableNonResizableMultiWindow = getDevEnableNonResizableMultiWindow(context) - if (prevDevEnableNonResizableMultiWindow != 0) { - // Turn off the development option - setDevEnableNonResizableMultiWindow(context, 0) - } - } - - @After - open fun teardown() { - setDevEnableNonResizableMultiWindow(context, prevDevEnableNonResizableMultiWindow) - } - - /** - * List of windows that are ignored when verifying that visible elements appear on 2 - * consecutive entries in the trace. - * - * b/182720234 - */ - open val ignoredWindows: List<FlickerComponentName> = listOf( - FlickerComponentName.SPLASH_SCREEN, - FlickerComponentName.SNAPSHOT) - - protected open val transition: FlickerBuilder.() -> Unit - get() = { - setup { - eachRun { - device.wakeUpAndGoToHomeScreen() - device.openQuickStepAndClearRecentAppsFromOverview(wmHelper) - secondaryApp.launchViaIntent(wmHelper) - splitScreenApp.launchViaIntent(wmHelper) - this.setRotation(testSpec.startRotation) - } - } - teardown { - eachRun { - secondaryApp.exit(wmHelper) - splitScreenApp.exit(wmHelper) - this.setRotation(Surface.ROTATION_0) - } - } - } - - @FlickerBuilderProvider - fun buildFlicker(): FlickerBuilder { - return FlickerBuilder(instrumentation).apply { - transition(this) - } - } - - internal open val cleanSetup: FlickerBuilder.() -> Unit - get() = { - setup { - eachRun { - device.wakeUpAndGoToHomeScreen() - device.openQuickStepAndClearRecentAppsFromOverview(wmHelper) - this.setRotation(testSpec.startRotation) - } - } - teardown { - eachRun { - nonResizeableApp.exit(wmHelper) - splitScreenApp.exit(wmHelper) - device.pressHome() - this.setRotation(Surface.ROTATION_0) - } - } - } - - @FlakyTest(bugId = 178447631) - @Test - open fun visibleWindowsShownMoreThanOneConsecutiveEntry() { - testSpec.assertWm { - this.visibleWindowsShownMoreThanOneConsecutiveEntry(ignoredWindows) - } - } - - @FlakyTest(bugId = 178447631) - @Test - open fun visibleLayersShownMoreThanOneConsecutiveEntry() { - testSpec.assertLayers { - this.visibleLayersShownMoreThanOneConsecutiveEntry(ignoredWindows) - } - } - - companion object { - internal val LIVE_WALLPAPER_COMPONENT = FlickerComponentName("", - "com.breel.wallpapers18.soundviz.wallpaper.variations.SoundVizWallpaperV2") - internal val LETTERBOX_COMPONENT = FlickerComponentName("", "Letterbox") - internal val TOAST_COMPONENT = FlickerComponentName("", "Toast") - } -} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/OWNERS b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/OWNERS deleted file mode 100644 index 8446b37dbf06..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/OWNERS +++ /dev/null @@ -1,2 +0,0 @@ -# window manager > wm shell > Split Screen -# Bug component: 928697 diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/OpenAppToLegacySplitScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/OpenAppToLegacySplitScreen.kt deleted file mode 100644 index 087b21c544c5..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/OpenAppToLegacySplitScreen.kt +++ /dev/null @@ -1,122 +0,0 @@ -/* - * Copyright (C) 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.wm.shell.flicker.legacysplitscreen - -import android.platform.test.annotations.Presubmit -import android.view.Surface -import androidx.test.filters.FlakyTest -import androidx.test.filters.RequiresDevice -import com.android.server.wm.flicker.FlickerParametersRunnerFactory -import com.android.server.wm.flicker.FlickerTestParameter -import com.android.server.wm.flicker.FlickerTestParameterFactory -import com.android.server.wm.flicker.annotation.Group2 -import com.android.server.wm.flicker.dsl.FlickerBuilder -import com.android.server.wm.flicker.entireScreenCovered -import com.android.server.wm.flicker.helpers.launchSplitScreen -import com.android.server.wm.flicker.statusBarLayerIsVisible -import com.android.server.wm.traces.common.FlickerComponentName -import com.android.wm.shell.flicker.appPairsDividerBecomesVisible -import com.android.wm.shell.flicker.helpers.SplitScreenHelper -import org.junit.FixMethodOrder -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.MethodSorters -import org.junit.runners.Parameterized - -/** - * Test open app to split screen. - * To run this test: `atest WMShellFlickerTests:OpenAppToLegacySplitScreen` - */ -@RequiresDevice -@RunWith(Parameterized::class) -@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -@Group2 -class OpenAppToLegacySplitScreen( - testSpec: FlickerTestParameter -) : LegacySplitScreenTransition(testSpec) { - override val transition: FlickerBuilder.() -> Unit - get() = { - super.transition(this) - transitions { - device.launchSplitScreen(wmHelper) - wmHelper.waitForAppTransitionIdle() - } - } - - override val ignoredWindows: List<FlickerComponentName> - get() = listOf(LAUNCHER_COMPONENT, splitScreenApp.component, - FlickerComponentName.SPLASH_SCREEN, - FlickerComponentName.SNAPSHOT) - - @FlakyTest - @Test - fun appWindowBecomesVisible() { - testSpec.assertWm { - this.isAppWindowInvisible(splitScreenApp.component) - .then() - .isAppWindowVisible(splitScreenApp.component) - } - } - - @Presubmit - @Test - fun entireScreenCovered() = testSpec.entireScreenCovered() - - @Presubmit - @Test - fun statusBarLayerIsVisible() = testSpec.statusBarLayerIsVisible() - - @Presubmit - @Test - fun appPairsDividerBecomesVisible() = testSpec.appPairsDividerBecomesVisible() - - @FlakyTest - @Test - fun layerBecomesVisible() { - testSpec.assertLayers { - this.isInvisible(splitScreenApp.component) - .then() - .isVisible(splitScreenApp.component) - } - } - - @Presubmit - @Test - fun focusChanges() { - testSpec.assertEventLog { - this.focusChanges(splitScreenApp.`package`, - "recents_animation_input_consumer", "NexusLauncherActivity") - } - } - - @Presubmit - @Test - override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = - super.visibleWindowsShownMoreThanOneConsecutiveEntry() - - companion object { - @Parameterized.Parameters(name = "{0}") - @JvmStatic - fun getParams(): Collection<FlickerTestParameter> { - return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests( - repetitions = SplitScreenHelper.TEST_REPETITIONS, - supportedRotations = listOf(Surface.ROTATION_0) // bugId = 179116910 - ) - } - } -} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ResizeLegacySplitScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ResizeLegacySplitScreen.kt deleted file mode 100644 index e2da1a4565c0..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ResizeLegacySplitScreen.kt +++ /dev/null @@ -1,228 +0,0 @@ -/* - * Copyright (C) 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.wm.shell.flicker.legacysplitscreen - -import android.util.Rational -import android.view.Surface -import androidx.test.filters.FlakyTest -import androidx.test.filters.RequiresDevice -import androidx.test.uiautomator.By -import com.android.server.wm.flicker.FlickerParametersRunnerFactory -import com.android.server.wm.flicker.FlickerTestParameter -import com.android.server.wm.flicker.FlickerTestParameterFactory -import com.android.server.wm.flicker.annotation.Group2 -import com.android.server.wm.flicker.dsl.FlickerBuilder -import com.android.server.wm.flicker.entireScreenCovered -import com.android.server.wm.flicker.helpers.ImeAppHelper -import com.android.server.wm.flicker.helpers.WindowUtils -import com.android.server.wm.flicker.helpers.launchSplitScreen -import com.android.server.wm.flicker.helpers.resizeSplitScreen -import com.android.server.wm.flicker.helpers.setRotation -import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen -import com.android.server.wm.flicker.navBarLayerIsVisible -import com.android.server.wm.flicker.navBarLayerRotatesAndScales -import com.android.server.wm.flicker.navBarWindowIsVisible -import com.android.server.wm.flicker.statusBarLayerIsVisible -import com.android.server.wm.flicker.statusBarLayerRotatesScales -import com.android.server.wm.flicker.statusBarWindowIsVisible -import com.android.server.wm.traces.common.region.Region -import com.android.server.wm.traces.parser.toFlickerComponent -import com.android.wm.shell.flicker.DOCKED_STACK_DIVIDER_COMPONENT -import com.android.wm.shell.flicker.helpers.SimpleAppHelper -import com.android.wm.shell.flicker.testapp.Components -import org.junit.FixMethodOrder -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.MethodSorters -import org.junit.runners.Parameterized - -/** - * Test split screen resizing window transitions. - * To run this test: `atest WMShellFlickerTests:ResizeLegacySplitScreen` - * - * Currently it runs only in 0 degrees because of b/156100803 - */ -@RequiresDevice -@RunWith(Parameterized::class) -@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -@FlakyTest(bugId = 159096424) -@Group2 -class ResizeLegacySplitScreen( - testSpec: FlickerTestParameter -) : LegacySplitScreenTransition(testSpec) { - private val testAppTop = SimpleAppHelper(instrumentation) - private val testAppBottom = ImeAppHelper(instrumentation) - - override val transition: FlickerBuilder.() -> Unit - get() = { - setup { - eachRun { - device.wakeUpAndGoToHomeScreen() - this.setRotation(testSpec.startRotation) - this.launcherStrategy.clearRecentAppsFromOverview() - testAppBottom.launchViaIntent(wmHelper) - device.pressHome() - testAppTop.launchViaIntent(wmHelper) - device.waitForIdle() - device.launchSplitScreen(wmHelper) - val snapshot = - device.findObject(By.res(device.launcherPackageName, "snapshot")) - snapshot.click() - testAppBottom.openIME(device) - device.pressBack() - device.resizeSplitScreen(startRatio) - } - } - teardown { - eachRun { - testAppTop.exit(wmHelper) - testAppBottom.exit(wmHelper) - } - } - transitions { - device.resizeSplitScreen(stopRatio) - } - } - - @Test - fun navBarWindowIsVisible() = testSpec.navBarWindowIsVisible() - - @Test - fun statusBarWindowIsVisible() = testSpec.statusBarWindowIsVisible() - - @FlakyTest(bugId = 156223549) - @Test - fun topAppWindowIsAlwaysVisible() { - testSpec.assertWm { - this.isAppWindowVisible(Components.SimpleActivity.COMPONENT.toFlickerComponent()) - } - } - - @FlakyTest(bugId = 156223549) - @Test - fun bottomAppWindowIsAlwaysVisible() { - testSpec.assertWm { - this.isAppWindowVisible(Components.ImeActivity.COMPONENT.toFlickerComponent()) - } - } - - @Test - fun navBarLayerIsVisible() = testSpec.navBarLayerIsVisible() - - @Test - fun statusBarLayerIsVisible() = testSpec.statusBarLayerIsVisible() - - @Test - fun entireScreenCovered() = testSpec.entireScreenCovered() - - @Test - fun navBarLayerRotatesAndScales() = testSpec.navBarLayerRotatesAndScales() - - @FlakyTest(bugId = 206753786) - @Test - fun statusBarLayerRotatesScales() = testSpec.statusBarLayerRotatesScales() - - @Test - fun topAppLayerIsAlwaysVisible() { - testSpec.assertLayers { - this.isVisible(Components.SimpleActivity.COMPONENT.toFlickerComponent()) - } - } - - @Test - fun bottomAppLayerIsAlwaysVisible() { - testSpec.assertLayers { - this.isVisible(Components.ImeActivity.COMPONENT.toFlickerComponent()) - } - } - - @Test - fun dividerLayerIsAlwaysVisible() { - testSpec.assertLayers { - this.isVisible(DOCKED_STACK_DIVIDER_COMPONENT) - } - } - - @FlakyTest - @Test - fun appsStartingBounds() { - testSpec.assertLayersStart { - val displayBounds = WindowUtils.displayBounds - val dividerBounds = - layer(DOCKED_STACK_DIVIDER_COMPONENT).visibleRegion.region.bounds - - val topAppBounds = Region.from(0, 0, dividerBounds.right, - dividerBounds.top + WindowUtils.dockedStackDividerInset) - val bottomAppBounds = Region.from(0, - dividerBounds.bottom - WindowUtils.dockedStackDividerInset, - displayBounds.right, - displayBounds.bottom - WindowUtils.navigationBarFrameHeight) - visibleRegion(Components.SimpleActivity.COMPONENT.toFlickerComponent()) - .coversExactly(topAppBounds) - visibleRegion(Components.ImeActivity.COMPONENT.toFlickerComponent()) - .coversExactly(bottomAppBounds) - } - } - - @FlakyTest - @Test - fun appsEndingBounds() { - testSpec.assertLayersStart { - val displayBounds = WindowUtils.displayBounds - val dividerBounds = - layer(DOCKED_STACK_DIVIDER_COMPONENT).visibleRegion.region.bounds - - val topAppBounds = Region.from(0, 0, dividerBounds.right, - dividerBounds.top + WindowUtils.dockedStackDividerInset) - val bottomAppBounds = Region.from(0, - dividerBounds.bottom - WindowUtils.dockedStackDividerInset, - displayBounds.right, - displayBounds.bottom - WindowUtils.navigationBarFrameHeight) - - visibleRegion(Components.SimpleActivity.COMPONENT.toFlickerComponent()) - .coversExactly(topAppBounds) - visibleRegion(Components.ImeActivity.COMPONENT.toFlickerComponent()) - .coversExactly(bottomAppBounds) - } - } - - @Test - fun focusDoesNotChange() { - testSpec.assertEventLog { - focusDoesNotChange() - } - } - - companion object { - private val startRatio = Rational(1, 3) - private val stopRatio = Rational(2, 3) - - @Parameterized.Parameters(name = "{0}") - @JvmStatic - fun getParams(): Collection<FlickerTestParameter> { - return FlickerTestParameterFactory.getInstance() - .getConfigNonRotationTests(supportedRotations = listOf(Surface.ROTATION_0)) - .map { - val description = (startRatio.toString().replace("/", "-") + "_to_" + - stopRatio.toString().replace("/", "-")) - val newName = "${FlickerTestParameter.defaultName(it)}_$description" - FlickerTestParameter(it.config, nameOverride = newName) - } - } - } -} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateOneLaunchedAppAndEnterSplitScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateOneLaunchedAppAndEnterSplitScreen.kt deleted file mode 100644 index d703ea082c87..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateOneLaunchedAppAndEnterSplitScreen.kt +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright (C) 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.wm.shell.flicker.legacysplitscreen - -import android.platform.test.annotations.Presubmit -import android.view.Surface -import androidx.test.filters.FlakyTest -import androidx.test.filters.RequiresDevice -import com.android.server.wm.flicker.FlickerParametersRunnerFactory -import com.android.server.wm.flicker.FlickerTestParameter -import com.android.server.wm.flicker.FlickerTestParameterFactory -import com.android.server.wm.flicker.annotation.Group2 -import com.android.server.wm.flicker.dsl.FlickerBuilder -import com.android.server.wm.flicker.helpers.launchSplitScreen -import com.android.server.wm.flicker.helpers.setRotation -import com.android.server.wm.flicker.navBarLayerRotatesAndScales -import com.android.server.wm.flicker.navBarWindowIsVisible -import com.android.server.wm.flicker.statusBarLayerRotatesScales -import com.android.server.wm.flicker.statusBarWindowIsVisible -import com.android.wm.shell.flicker.dockedStackDividerIsVisibleAtEnd -import com.android.wm.shell.flicker.dockedStackPrimaryBoundsIsVisibleAtEnd -import com.android.wm.shell.flicker.helpers.SplitScreenHelper -import org.junit.FixMethodOrder -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.MethodSorters -import org.junit.runners.Parameterized - -/** - * Test dock activity to primary split screen and rotate - * To run this test: `atest WMShellFlickerTests:RotateOneLaunchedAppAndEnterSplitScreen` - */ -@RequiresDevice -@RunWith(Parameterized::class) -@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -@Group2 -class RotateOneLaunchedAppAndEnterSplitScreen( - testSpec: FlickerTestParameter -) : LegacySplitScreenRotateTransition(testSpec) { - override val transition: FlickerBuilder.() -> Unit - get() = { - super.transition(this) - transitions { - device.launchSplitScreen(wmHelper) - this.setRotation(testSpec.startRotation) - } - } - - @Presubmit - @Test - fun dockedStackDividerIsVisibleAtEnd() = testSpec.dockedStackDividerIsVisibleAtEnd() - - @Presubmit - @Test - fun dockedStackPrimaryBoundsIsVisibleAtEnd() = - testSpec.dockedStackPrimaryBoundsIsVisibleAtEnd(testSpec.startRotation, - splitScreenApp.component) - - @Presubmit - @Test - fun navBarLayerRotatesAndScales() = testSpec.navBarLayerRotatesAndScales() - - @FlakyTest(bugId = 206753786) - @Test - fun statusBarLayerRotatesScales() = testSpec.statusBarLayerRotatesScales() - - @Presubmit - @Test - fun navBarWindowIsVisible() = testSpec.navBarWindowIsVisible() - - @Presubmit - @Test - fun statusBarWindowIsVisible() = testSpec.statusBarWindowIsVisible() - - @FlakyTest - @Test - fun appWindowBecomesVisible() { - testSpec.assertWm { - this.isAppWindowInvisible(splitScreenApp.component) - .then() - .isAppWindowVisible(splitScreenApp.component) - } - } - - @Presubmit - @Test - override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = - super.visibleWindowsShownMoreThanOneConsecutiveEntry() - - companion object { - @Parameterized.Parameters(name = "{0}") - @JvmStatic - fun getParams(): Collection<FlickerTestParameter> { - return FlickerTestParameterFactory.getInstance() - .getConfigNonRotationTests(repetitions = SplitScreenHelper.TEST_REPETITIONS, - supportedRotations = listOf(Surface.ROTATION_0)) // b/178685668 - } - } -} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateOneLaunchedAppInSplitScreenMode.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateOneLaunchedAppInSplitScreenMode.kt deleted file mode 100644 index 6b1883914e59..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateOneLaunchedAppInSplitScreenMode.kt +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright (C) 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.wm.shell.flicker.legacysplitscreen - -import android.platform.test.annotations.Presubmit -import android.view.Surface -import androidx.test.filters.FlakyTest -import androidx.test.filters.RequiresDevice -import com.android.server.wm.flicker.FlickerParametersRunnerFactory -import com.android.server.wm.flicker.FlickerTestParameter -import com.android.server.wm.flicker.FlickerTestParameterFactory -import com.android.server.wm.flicker.annotation.Group2 -import com.android.server.wm.flicker.dsl.FlickerBuilder -import com.android.server.wm.flicker.helpers.launchSplitScreen -import com.android.server.wm.flicker.helpers.setRotation -import com.android.server.wm.flicker.navBarLayerRotatesAndScales -import com.android.server.wm.flicker.navBarWindowIsVisible -import com.android.server.wm.flicker.statusBarLayerRotatesScales -import com.android.server.wm.flicker.statusBarWindowIsVisible -import com.android.wm.shell.flicker.dockedStackDividerIsVisibleAtEnd -import com.android.wm.shell.flicker.dockedStackPrimaryBoundsIsVisibleAtEnd -import com.android.wm.shell.flicker.helpers.SplitScreenHelper -import org.junit.FixMethodOrder -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.MethodSorters -import org.junit.runners.Parameterized - -/** - * Rotate - * To run this test: `atest WMShellFlickerTests:RotateOneLaunchedAppInSplitScreenMode` - */ -@RequiresDevice -@RunWith(Parameterized::class) -@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -@Group2 -class RotateOneLaunchedAppInSplitScreenMode( - testSpec: FlickerTestParameter -) : LegacySplitScreenRotateTransition(testSpec) { - override val transition: FlickerBuilder.() -> Unit - get() = { - super.transition(this) - transitions { - this.setRotation(testSpec.startRotation) - device.launchSplitScreen(wmHelper) - } - } - - @Presubmit - @Test - fun dockedStackDividerIsVisibleAtEnd() = testSpec.dockedStackDividerIsVisibleAtEnd() - - @Presubmit - @Test - fun dockedStackPrimaryBoundsIsVisibleAtEnd() = testSpec.dockedStackPrimaryBoundsIsVisibleAtEnd( - testSpec.startRotation, splitScreenApp.component) - - @Presubmit - @Test - fun navBarLayerRotatesAndScales() = testSpec.navBarLayerRotatesAndScales() - - @FlakyTest(bugId = 206753786) - @Test - fun statusBarLayerRotatesScales() = testSpec.statusBarLayerRotatesScales() - - @Presubmit - @Test - fun navBarWindowIsVisible() = testSpec.navBarWindowIsVisible() - - @Presubmit - @Test - fun statusBarWindowIsVisible() = testSpec.statusBarWindowIsVisible() - - @FlakyTest - @Test - fun appWindowBecomesVisible() { - testSpec.assertWm { - this.isAppWindowInvisible(splitScreenApp.component) - .then() - .isAppWindowVisible(splitScreenApp.component) - } - } - - @Presubmit - @Test - override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = - super.visibleWindowsShownMoreThanOneConsecutiveEntry() - - companion object { - @Parameterized.Parameters(name = "{0}") - @JvmStatic - fun getParams(): Collection<FlickerTestParameter> { - return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests( - repetitions = SplitScreenHelper.TEST_REPETITIONS, - supportedRotations = listOf(Surface.ROTATION_0)) // b/178685668 - } - } -} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateTwoLaunchedAppAndEnterSplitScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateTwoLaunchedAppAndEnterSplitScreen.kt deleted file mode 100644 index acd658b5ba56..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateTwoLaunchedAppAndEnterSplitScreen.kt +++ /dev/null @@ -1,136 +0,0 @@ -/* - * Copyright (C) 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.wm.shell.flicker.legacysplitscreen - -import android.platform.test.annotations.Presubmit -import android.view.Surface -import androidx.test.filters.FlakyTest -import androidx.test.filters.RequiresDevice -import com.android.server.wm.flicker.FlickerParametersRunnerFactory -import com.android.server.wm.flicker.FlickerTestParameter -import com.android.server.wm.flicker.FlickerTestParameterFactory -import com.android.server.wm.flicker.annotation.Group2 -import com.android.server.wm.flicker.dsl.FlickerBuilder -import com.android.server.wm.flicker.helpers.launchSplitScreen -import com.android.server.wm.flicker.helpers.reopenAppFromOverview -import com.android.server.wm.flicker.helpers.setRotation -import com.android.server.wm.flicker.navBarLayerRotatesAndScales -import com.android.server.wm.flicker.navBarWindowIsVisible -import com.android.server.wm.flicker.statusBarLayerRotatesScales -import com.android.server.wm.flicker.statusBarWindowIsVisible -import com.android.wm.shell.flicker.dockedStackDividerIsVisibleAtEnd -import com.android.wm.shell.flicker.dockedStackPrimaryBoundsIsVisibleAtEnd -import com.android.wm.shell.flicker.dockedStackSecondaryBoundsIsVisibleAtEnd -import com.android.wm.shell.flicker.helpers.SplitScreenHelper -import org.junit.FixMethodOrder -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.MethodSorters -import org.junit.runners.Parameterized - -/** - * Test open app to split screen. - * To run this test: `atest WMShellFlickerTests:RotateTwoLaunchedAppAndEnterSplitScreen` - */ -@RequiresDevice -@RunWith(Parameterized::class) -@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -@Group2 -class RotateTwoLaunchedAppAndEnterSplitScreen( - testSpec: FlickerTestParameter -) : LegacySplitScreenRotateTransition(testSpec) { - override val transition: FlickerBuilder.() -> Unit - get() = { - super.transition(this) - transitions { - this.setRotation(testSpec.startRotation) - device.launchSplitScreen(wmHelper) - device.reopenAppFromOverview(wmHelper) - } - } - - @Presubmit - @Test - fun dockedStackDividerIsVisibleAtEnd() = testSpec.dockedStackDividerIsVisibleAtEnd() - - @Presubmit - @Test - fun dockedStackPrimaryBoundsIsVisibleAtEnd() = - testSpec.dockedStackPrimaryBoundsIsVisibleAtEnd(testSpec.startRotation, - splitScreenApp.component) - - @Presubmit - @Test - fun dockedStackSecondaryBoundsIsVisibleAtEnd() = - testSpec.dockedStackSecondaryBoundsIsVisibleAtEnd(testSpec.startRotation, - secondaryApp.component) - - @Presubmit - @Test - fun navBarLayerRotatesAndScales() = testSpec.navBarLayerRotatesAndScales() - - @FlakyTest(bugId = 206753786) - @Test - fun statusBarLayerRotatesScales() = testSpec.statusBarLayerRotatesScales() - - @Presubmit - @Test - fun appWindowBecomesVisible() { - testSpec.assertWm { - // when the app is launched, first the activity becomes visible, then the - // SnapshotStartingWindow appears and then the app window becomes visible. - // Because we log WM once per frame, sometimes the activity and the window - // become visible in the same entry, sometimes not, thus it is not possible to - // assert the visibility of the activity here - this.isAppWindowInvisible(secondaryApp.component) - .then() - // during re-parenting, the window may disappear and reappear from the - // trace, this occurs because we log only 1x per frame - .notContains(secondaryApp.component, isOptional = true) - .then() - // if the window reappears after re-parenting it will most likely not - // be visible in the first log entry (because we log only 1x per frame) - .isAppWindowInvisible(secondaryApp.component, isOptional = true) - .then() - .isAppWindowVisible(secondaryApp.component) - } - } - - @Presubmit - @Test - fun navBarWindowIsVisible() = testSpec.navBarWindowIsVisible() - - @Presubmit - @Test - fun statusBarWindowIsVisible() = testSpec.statusBarWindowIsVisible() - - @Presubmit - @Test - override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = - super.visibleWindowsShownMoreThanOneConsecutiveEntry() - - companion object { - @Parameterized.Parameters(name = "{0}") - @JvmStatic - fun getParams(): Collection<FlickerTestParameter> { - return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests( - repetitions = SplitScreenHelper.TEST_REPETITIONS, - supportedRotations = listOf(Surface.ROTATION_0)) // b/178685668 - } - } -} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateTwoLaunchedAppInSplitScreenMode.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateTwoLaunchedAppInSplitScreenMode.kt deleted file mode 100644 index b40be8b5f401..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateTwoLaunchedAppInSplitScreenMode.kt +++ /dev/null @@ -1,133 +0,0 @@ -/* - * Copyright (C) 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.wm.shell.flicker.legacysplitscreen - -import android.platform.test.annotations.Presubmit -import android.view.Surface -import androidx.test.filters.FlakyTest -import androidx.test.filters.RequiresDevice -import com.android.server.wm.flicker.FlickerParametersRunnerFactory -import com.android.server.wm.flicker.FlickerTestParameter -import com.android.server.wm.flicker.FlickerTestParameterFactory -import com.android.server.wm.flicker.annotation.Group2 -import com.android.server.wm.flicker.dsl.FlickerBuilder -import com.android.server.wm.flicker.helpers.launchSplitScreen -import com.android.server.wm.flicker.helpers.reopenAppFromOverview -import com.android.server.wm.flicker.helpers.setRotation -import com.android.server.wm.flicker.navBarLayerRotatesAndScales -import com.android.server.wm.flicker.navBarWindowIsVisible -import com.android.server.wm.flicker.statusBarLayerRotatesScales -import com.android.server.wm.flicker.statusBarWindowIsVisible -import com.android.wm.shell.flicker.dockedStackDividerIsVisibleAtEnd -import com.android.wm.shell.flicker.dockedStackPrimaryBoundsIsVisibleAtEnd -import com.android.wm.shell.flicker.dockedStackSecondaryBoundsIsVisibleAtEnd -import com.android.wm.shell.flicker.helpers.SplitScreenHelper -import org.junit.FixMethodOrder -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.MethodSorters -import org.junit.runners.Parameterized - -/** - * Test open app to split screen. - * To run this test: `atest WMShellFlickerTests:RotateTwoLaunchedAppInSplitScreenMode` - */ -@RequiresDevice -@RunWith(Parameterized::class) -@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -@Group2 -class RotateTwoLaunchedAppInSplitScreenMode( - testSpec: FlickerTestParameter -) : LegacySplitScreenRotateTransition(testSpec) { - override val transition: FlickerBuilder.() -> Unit - get() = { - super.transition(this) - setup { - eachRun { - device.launchSplitScreen(wmHelper) - device.reopenAppFromOverview(wmHelper) - this.setRotation(testSpec.startRotation) - } - } - transitions { - this.setRotation(testSpec.startRotation) - } - } - - @Presubmit - @Test - fun dockedStackDividerIsVisibleAtEnd() = testSpec.dockedStackDividerIsVisibleAtEnd() - - @Presubmit - @Test - fun dockedStackPrimaryBoundsIsVisibleAtEnd() = - testSpec.dockedStackPrimaryBoundsIsVisibleAtEnd(testSpec.startRotation, - splitScreenApp.component) - - @Presubmit - @Test - fun dockedStackSecondaryBoundsIsVisibleAtEnd() = - testSpec.dockedStackSecondaryBoundsIsVisibleAtEnd(testSpec.startRotation, - secondaryApp.component) - - @Presubmit - @Test - fun navBarLayerRotatesAndScales() = testSpec.navBarLayerRotatesAndScales() - - @FlakyTest(bugId = 206753786) - @Test - fun statusBarLayerRotatesScales() = testSpec.statusBarLayerRotatesScales() - - @FlakyTest - @Test - fun appWindowBecomesVisible() { - testSpec.assertWm { - this.isAppWindowInvisible(secondaryApp.component) - .then() - .isAppWindowVisible(secondaryApp.component) - } - } - - @Presubmit - @Test - fun navBarWindowIsVisible() = testSpec.navBarWindowIsVisible() - - @Presubmit - @Test - fun statusBarWindowIsVisible() = testSpec.statusBarWindowIsVisible() - - @Presubmit - @Test - override fun visibleLayersShownMoreThanOneConsecutiveEntry() = - super.visibleLayersShownMoreThanOneConsecutiveEntry() - - @Presubmit - @Test - override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = - super.visibleWindowsShownMoreThanOneConsecutiveEntry() - - companion object { - @Parameterized.Parameters(name = "{0}") - @JvmStatic - fun getParams(): Collection<FlickerTestParameter> { - return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests( - repetitions = SplitScreenHelper.TEST_REPETITIONS, - supportedRotations = listOf(Surface.ROTATION_0)) // b/178685668 - } - } -} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/AutoEnterPipOnGoToHomeTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/AutoEnterPipOnGoToHomeTest.kt new file mode 100644 index 000000000000..ce624f2b5bbe --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/AutoEnterPipOnGoToHomeTest.kt @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.pip + +import android.platform.test.annotations.FlakyTest +import androidx.test.filters.RequiresDevice +import com.android.launcher3.tapl.LauncherInstrumentation +import com.android.server.wm.flicker.FlickerParametersRunnerFactory +import com.android.server.wm.flicker.FlickerTestParameter +import com.android.server.wm.flicker.annotation.Group3 +import com.android.server.wm.flicker.dsl.FlickerBuilder +import org.junit.Assume +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** + * Test entering pip from an app via auto-enter property when navigating to home. + * + * To run this test: `atest WMShellFlickerTests:AutoEnterPipOnGoToHomeTest` + * + * Actions: + * Launch an app in full screen + * Select "Auto-enter PiP" radio button + * Press Home button or swipe up to go Home and put [pipApp] in pip mode + * + * Notes: + * 1. All assertions are inherited from [EnterPipTest] + * 2. Part of the test setup occurs automatically via + * [com.android.server.wm.flicker.TransitionRunnerWithRules], + * including configuring navigation mode, initial orientation and ensuring no + * apps are running before setup + */ +@RequiresDevice +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +@FlakyTest(bugId = 238367575) +@Group3 +class AutoEnterPipOnGoToHomeTest(testSpec: FlickerTestParameter) : EnterPipTest(testSpec) { + protected val taplInstrumentation = LauncherInstrumentation() + /** + * Defines the transition used to run the test + */ + override val transition: FlickerBuilder.() -> Unit + get() = { + setupAndTeardown(this) + setup { + eachRun { + pipApp.launchViaIntent(wmHelper) + pipApp.enableAutoEnterForPipActivity() + } + } + teardown { + eachRun { + // close gracefully so that onActivityUnpinned() can be called before force exit + pipApp.closePipWindow(wmHelper) + pipApp.exit(wmHelper) + } + } + transitions { + taplInstrumentation.goHome() + } + } + + override fun pipLayerReduces() { + val layerName = pipApp.component.toLayerName() + testSpec.assertLayers { + val pipLayerList = this.layers { it.name.contains(layerName) && it.isVisible } + pipLayerList.zipWithNext { previous, current -> + current.visibleRegion.notBiggerThan(previous.visibleRegion.region) + } + } + } + + /** + * Checks that [pipApp] window is animated towards default position in right bottom corner + */ + @Test + fun pipLayerMovesTowardsRightBottomCorner() { + // in gestural nav the swipe makes PiP first go upwards + Assume.assumeFalse(testSpec.isGesturalNavigation) + val layerName = pipApp.component.toLayerName() + testSpec.assertLayers { + val pipLayerList = this.layers { it.name.contains(layerName) && it.isVisible } + // Pip animates towards the right bottom corner, but because it is being resized at the + // same time, it is possible it shrinks first quickly below the default position and get + // moved up after that in just few last frames + pipLayerList.zipWithNext { previous, current -> + current.visibleRegion.isToTheRightBottom(previous.visibleRegion.region, 3) + } + } + } + + override fun focusChanges() { + // in gestural nav the focus goes to different activity on swipe up + Assume.assumeFalse(testSpec.isGesturalNavigation) + super.focusChanges() + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipOnUserLeaveHintTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipOnUserLeaveHintTest.kt new file mode 100644 index 000000000000..953f59a1f70b --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipOnUserLeaveHintTest.kt @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.pip + +import androidx.test.filters.RequiresDevice +import com.android.launcher3.tapl.LauncherInstrumentation +import com.android.server.wm.flicker.FlickerParametersRunnerFactory +import com.android.server.wm.flicker.FlickerTestParameter +import com.android.server.wm.flicker.annotation.Group3 +import com.android.server.wm.flicker.dsl.FlickerBuilder +import org.junit.Assume +import org.junit.FixMethodOrder +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** + * Test entering pip from an app via [onUserLeaveHint] and by navigating to home. + * + * To run this test: `atest WMShellFlickerTests:EnterPipOnUserLeaveHintTest` + * + * Actions: + * Launch an app in full screen + * Select "Via code behind" radio button + * Press Home button or swipe up to go Home and put [pipApp] in pip mode + * + * Notes: + * 1. All assertions are inherited from [EnterPipTest] + * 2. Part of the test setup occurs automatically via + * [com.android.server.wm.flicker.TransitionRunnerWithRules], + * including configuring navigation mode, initial orientation and ensuring no + * apps are running before setup + */ +@RequiresDevice +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +@Group3 +class EnterPipOnUserLeaveHintTest(testSpec: FlickerTestParameter) : EnterPipTest(testSpec) { + protected val taplInstrumentation = LauncherInstrumentation() + /** + * Defines the transition used to run the test + */ + override val transition: FlickerBuilder.() -> Unit + get() = { + setupAndTeardown(this) + setup { + eachRun { + pipApp.launchViaIntent(wmHelper) + pipApp.enableEnterPipOnUserLeaveHint() + } + } + teardown { + eachRun { + pipApp.exit(wmHelper) + } + } + transitions { + taplInstrumentation.goHome() + } + } + + override fun pipAppLayerAlwaysVisible() { + if (!testSpec.isGesturalNavigation) super.pipAppLayerAlwaysVisible() else { + // pip layer in gesture nav will disappear during transition + testSpec.assertLayers { + this.isVisible(pipApp.component) + .then().isInvisible(pipApp.component) + .then().isVisible(pipApp.component) + } + } + } + + override fun pipLayerReduces() { + // in gestural nav the pip enters through alpha animation + Assume.assumeFalse(testSpec.isGesturalNavigation) + super.pipLayerReduces() + } + + override fun focusChanges() { + // in gestural nav the focus goes to different activity on swipe up + Assume.assumeFalse(testSpec.isGesturalNavigation) + super.focusChanges() + } + + override fun pipLayerRemainInsideVisibleBounds() { + if (!testSpec.isGesturalNavigation) super.pipLayerRemainInsideVisibleBounds() else { + // pip layer in gesture nav will disappear during transition + testSpec.assertLayersStart { + this.visibleRegion(pipApp.component).coversAtMost(displayBounds) + } + testSpec.assertLayersEnd { + this.visibleRegion(pipApp.component).coversAtMost(displayBounds) + } + } + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipTest.kt index 0640ac526bd0..61ac49835185 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipTest.kt @@ -18,7 +18,6 @@ package com.android.wm.shell.flicker.pip import android.platform.test.annotations.Presubmit import android.view.Surface -import androidx.test.filters.FlakyTest import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.FlickerParametersRunnerFactory import com.android.server.wm.flicker.FlickerTestParameter @@ -43,7 +42,7 @@ import org.junit.runners.Parameterized * * Notes: * 1. Some default assertions (e.g., nav bar, status bar and screen covered) - * are inherited [PipTransition] + * are inherited from [PipTransition] * 2. Part of the test setup occurs automatically via * [com.android.server.wm.flicker.TransitionRunnerWithRules], * including configuring navigation mode, initial orientation and ensuring no @@ -54,7 +53,7 @@ import org.junit.runners.Parameterized @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) @Group3 -class EnterPipTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) { +open class EnterPipTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) { /** * Defines the transition used to run the test @@ -77,11 +76,6 @@ class EnterPipTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) { } } - /** {@inheritDoc} */ - @FlakyTest(bugId = 206753786) - @Test - override fun statusBarLayerRotatesScales() = super.statusBarLayerRotatesScales() - /** * Checks [pipApp] window remains visible throughout the animation */ @@ -98,7 +92,7 @@ class EnterPipTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) { */ @Presubmit @Test - fun pipAppLayerAlwaysVisible() { + open fun pipAppLayerAlwaysVisible() { testSpec.assertLayers { this.isVisible(pipApp.component) } @@ -122,7 +116,7 @@ class EnterPipTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) { */ @Presubmit @Test - fun pipLayerRemainInsideVisibleBounds() { + open fun pipLayerRemainInsideVisibleBounds() { testSpec.assertLayersVisibleRegion(pipApp.component) { coversAtMost(displayBounds) } @@ -133,12 +127,12 @@ class EnterPipTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) { */ @Presubmit @Test - fun pipLayerReduces() { + open fun pipLayerReduces() { val layerName = pipApp.component.toLayerName() testSpec.assertLayers { val pipLayerList = this.layers { it.name.contains(layerName) && it.isVisible } pipLayerList.zipWithNext { previous, current -> - current.visibleRegion.coversAtMost(previous.visibleRegion.region) + current.visibleRegion.notBiggerThan(previous.visibleRegion.region) } } } @@ -175,7 +169,7 @@ class EnterPipTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) { */ @Presubmit @Test - fun focusChanges() { + open fun focusChanges() { testSpec.assertEventLog { this.focusChanges(pipApp.`package`, "NexusLauncherActivity") } @@ -192,8 +186,10 @@ class EnterPipTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) { @JvmStatic fun getParams(): List<FlickerTestParameter> { return FlickerTestParameterFactory.getInstance() - .getConfigNonRotationTests(supportedRotations = listOf(Surface.ROTATION_0), - repetitions = 3) + .getConfigNonRotationTests( + supportedRotations = listOf(Surface.ROTATION_0), + repetitions = 3 + ) } } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientationTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientationTest.kt index accb524d3de1..7680f4dfa1d5 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientationTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientationTest.kt @@ -28,13 +28,12 @@ import com.android.server.wm.flicker.dsl.FlickerBuilder import com.android.server.wm.flicker.entireScreenCovered import com.android.server.wm.flicker.helpers.WindowUtils import com.android.server.wm.flicker.navBarLayerRotatesAndScales -import com.android.server.wm.flicker.statusBarLayerRotatesScales import com.android.server.wm.traces.common.FlickerComponentName import com.android.wm.shell.flicker.helpers.FixedAppHelper import com.android.wm.shell.flicker.pip.PipTransition.BroadcastActionTrigger.Companion.ORIENTATION_LANDSCAPE import com.android.wm.shell.flicker.pip.PipTransition.BroadcastActionTrigger.Companion.ORIENTATION_PORTRAIT -import com.android.wm.shell.flicker.testapp.Components.PipActivity.ACTION_ENTER_PIP import com.android.wm.shell.flicker.testapp.Components.FixedActivity.EXTRA_FIXED_ORIENTATION +import com.android.wm.shell.flicker.testapp.Components.PipActivity.ACTION_ENTER_PIP import org.junit.FixMethodOrder import org.junit.Test import org.junit.runner.RunWith @@ -44,7 +43,7 @@ import org.junit.runners.Parameterized /** * Test entering pip while changing orientation (from app in landscape to pip window in portrait) * - * To run this test: `atest EnterPipToOtherOrientationTest:EnterPipToOtherOrientationTest` + * To run this test: `atest WMShellFlickerTests:EnterPipToOtherOrientationTest` * * Actions: * Launch [testApp] on a fixed portrait orientation @@ -114,14 +113,6 @@ class EnterPipToOtherOrientationTest( override fun navBarLayerRotatesAndScales() = testSpec.navBarLayerRotatesAndScales() /** - * Checks that the [FlickerComponentName.STATUS_BAR] has the correct position at - * the start and end of the transition - */ - @FlakyTest(bugId = 206753786) - @Test - override fun statusBarLayerRotatesScales() = testSpec.statusBarLayerRotatesScales() - - /** * 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 diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaExpandButtonClickTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaExpandButtonClickTest.kt index a3ed79bf0409..0768e82e491c 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaExpandButtonClickTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaExpandButtonClickTest.kt @@ -79,11 +79,6 @@ class ExitPipViaExpandButtonClickTest( } /** {@inheritDoc} */ - @FlakyTest(bugId = 206753786) - @Test - override fun statusBarLayerRotatesScales() = super.statusBarLayerRotatesScales() - - /** {@inheritDoc} */ @FlakyTest(bugId = 197726610) @Test override fun pipLayerExpands() = super.pipLayerExpands() diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaIntentTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaIntentTest.kt index 37e9344348d9..c6a705dacb8d 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaIntentTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaIntentTest.kt @@ -80,7 +80,17 @@ class ExitPipViaIntentTest(testSpec: FlickerTestParameter) : ExitPipToAppTransit /** {@inheritDoc} */ @FlakyTest(bugId = 206753786) @Test - override fun statusBarLayerRotatesScales() = super.statusBarLayerRotatesScales() + override fun statusBarLayerRotatesScales() { + Assume.assumeFalse(isShellTransitionsEnabled) + super.statusBarLayerRotatesScales() + } + + @Presubmit + @Test + fun statusBarLayerRotatesScales_ShellTransit() { + Assume.assumeTrue(isShellTransitionsEnabled) + super.statusBarLayerRotatesScales() + } /** {@inheritDoc} */ @FlakyTest(bugId = 197726610) diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipWithSwipeDownTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipWithSwipeDownTest.kt index ab07ede5bb32..128703ad332c 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipWithSwipeDownTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipWithSwipeDownTest.kt @@ -25,7 +25,6 @@ import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.FlickerTestParameterFactory import com.android.server.wm.flicker.annotation.Group3 import com.android.server.wm.flicker.dsl.FlickerBuilder -import com.android.server.wm.flicker.statusBarLayerRotatesScales import org.junit.FixMethodOrder import org.junit.Test import org.junit.runner.RunWith @@ -78,10 +77,6 @@ class ExitPipWithSwipeDownTest(testSpec: FlickerTestParameter) : ExitPipTransiti @Test override fun pipLayerBecomesInvisible() = super.pipLayerBecomesInvisible() - @FlakyTest(bugId = 206753786) - @Test - override fun statusBarLayerRotatesScales() = testSpec.statusBarLayerRotatesScales() - /** * Checks that the focus doesn't change between windows during the transition */ @@ -108,4 +103,4 @@ class ExitPipWithSwipeDownTest(testSpec: FlickerTestParameter) : ExitPipTransiti repetitions = 3) } } -}
\ No newline at end of file +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipKeyboardTestShellTransit.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipKeyboardTestShellTransit.kt index 1a21d32f568c..fe51228230cb 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipKeyboardTestShellTransit.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipKeyboardTestShellTransit.kt @@ -16,7 +16,7 @@ package com.android.wm.shell.flicker.pip -import androidx.test.filters.FlakyTest +import android.platform.test.annotations.Presubmit import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.FlickerParametersRunnerFactory import com.android.server.wm.flicker.FlickerTestParameter @@ -35,7 +35,6 @@ import org.junit.runners.Parameterized @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) @Group4 -@FlakyTest(bugId = 217777115) class PipKeyboardTestShellTransit(testSpec: FlickerTestParameter) : PipKeyboardTest(testSpec) { @Before @@ -43,7 +42,7 @@ class PipKeyboardTestShellTransit(testSpec: FlickerTestParameter) : PipKeyboardT Assume.assumeTrue(isShellTransitionsEnabled) } - @FlakyTest(bugId = 214452854) + @Presubmit @Test override fun statusBarLayerRotatesScales() = super.statusBarLayerRotatesScales() }
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipLegacySplitScreenTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipLegacySplitScreenTest.kt deleted file mode 100644 index 21175a0767a5..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipLegacySplitScreenTest.kt +++ /dev/null @@ -1,148 +0,0 @@ -/* - * Copyright (C) 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.wm.shell.flicker.pip - -import android.platform.test.annotations.Presubmit -import android.view.Surface -import androidx.test.filters.FlakyTest -import androidx.test.filters.RequiresDevice -import com.android.server.wm.flicker.FlickerParametersRunnerFactory -import com.android.server.wm.flicker.FlickerTestParameter -import com.android.server.wm.flicker.FlickerTestParameterFactory -import com.android.server.wm.flicker.annotation.Group4 -import com.android.server.wm.flicker.dsl.FlickerBuilder -import com.android.server.wm.flicker.helpers.launchSplitScreen -import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen -import com.android.server.wm.flicker.rules.RemoveAllTasksButHomeRule.Companion.removeAllTasksButHome -import com.android.wm.shell.flicker.helpers.BaseAppHelper.Companion.isShellTransitionsEnabled -import com.android.wm.shell.flicker.helpers.FixedAppHelper -import com.android.wm.shell.flicker.helpers.ImeAppHelper -import com.android.wm.shell.flicker.helpers.SplitScreenHelper -import com.android.wm.shell.flicker.testapp.Components.PipActivity.EXTRA_ENTER_PIP -import org.junit.Assume.assumeFalse -import org.junit.Assume.assumeTrue -import org.junit.Before -import org.junit.FixMethodOrder -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.MethodSorters -import org.junit.runners.Parameterized - -/** - * Test Pip with split-screen. - * To run this test: `atest WMShellFlickerTests:PipLegacySplitScreenTest` - */ -@RequiresDevice -@RunWith(Parameterized::class) -@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -@Group4 -class PipLegacySplitScreenTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) { - private val imeApp = ImeAppHelper(instrumentation) - private val testApp = FixedAppHelper(instrumentation) - - @Before - open fun setup() { - // Only run legacy split tests when the system is using legacy split screen. - assumeTrue(SplitScreenHelper.isUsingLegacySplit()) - // Legacy split is having some issue with Shell transition, and will be deprecated soon. - assumeFalse(isShellTransitionsEnabled()) - } - - override val transition: FlickerBuilder.() -> Unit - get() = { - setup { - test { - removeAllTasksButHome() - device.wakeUpAndGoToHomeScreen() - pipApp.launchViaIntent(stringExtras = mapOf(EXTRA_ENTER_PIP to "true"), - wmHelper = wmHelper) - } - } - transitions { - testApp.launchViaIntent(wmHelper) - device.launchSplitScreen(wmHelper) - imeApp.launchViaIntent(wmHelper) - } - teardown { - eachRun { - imeApp.exit(wmHelper) - testApp.exit(wmHelper) - } - test { - removeAllTasksButHome() - } - } - } - - /** {@inheritDoc} */ - @FlakyTest(bugId = 206753786) - @Test - override fun statusBarLayerRotatesScales() = super.statusBarLayerRotatesScales() - - @FlakyTest(bugId = 161435597) - @Test - fun pipWindowInsideDisplayBounds() { - testSpec.assertWmVisibleRegion(pipApp.component) { - coversAtMost(displayBounds) - } - } - - @Presubmit - @Test - fun bothAppWindowsVisible() { - testSpec.assertWmEnd { - isAppWindowVisible(testApp.component) - isAppWindowVisible(imeApp.component) - doNotOverlap(testApp.component, imeApp.component) - } - } - - @FlakyTest(bugId = 161435597) - @Test - fun pipLayerInsideDisplayBounds() { - testSpec.assertLayersVisibleRegion(pipApp.component) { - coversAtMost(displayBounds) - } - } - - @Presubmit - @Test - fun bothAppLayersVisible() { - testSpec.assertLayersEnd { - visibleRegion(testApp.component).coversAtMost(displayBounds) - visibleRegion(imeApp.component).coversAtMost(displayBounds) - } - } - - @FlakyTest(bugId = 161435597) - @Test - override fun entireScreenCovered() = super.entireScreenCovered() - - companion object { - const val TEST_REPETITIONS = 2 - - @Parameterized.Parameters(name = "{0}") - @JvmStatic - fun getParams(): Collection<FlickerTestParameter> { - return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests( - supportedRotations = listOf(Surface.ROTATION_0), - repetitions = TEST_REPETITIONS - ) - } - } -} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipRotationTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipRotationTest.kt index c1ee1a7cbb35..9fad4997e63a 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipRotationTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipRotationTest.kt @@ -27,12 +27,9 @@ import com.android.server.wm.flicker.annotation.Group4 import com.android.server.wm.flicker.dsl.FlickerBuilder import com.android.server.wm.flicker.entireScreenCovered import com.android.server.wm.flicker.helpers.WindowUtils -import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled import com.android.server.wm.flicker.helpers.setRotation import com.android.server.wm.flicker.navBarLayerRotatesAndScales -import com.android.server.wm.flicker.statusBarLayerRotatesScales import com.android.wm.shell.flicker.helpers.FixedAppHelper -import org.junit.Assume import org.junit.FixMethodOrder import org.junit.Test import org.junit.runner.RunWith @@ -98,13 +95,6 @@ open class PipRotationTest(testSpec: FlickerTestParameter) : PipTransition(testS override fun navBarLayerRotatesAndScales() = testSpec.navBarLayerRotatesAndScales() /** - * Checks the position of the status bar at the start and end of the transition - */ - @FlakyTest(bugId = 206753786) - @Test - override fun statusBarLayerRotatesScales() = testSpec.statusBarLayerRotatesScales() - - /** * Checks that [fixedApp] layer is within [screenBoundsStart] at the start of the transition */ @Presubmit @@ -141,14 +131,6 @@ open class PipRotationTest(testSpec: FlickerTestParameter) : PipTransition(testS @Presubmit @Test fun pipLayerRotates_StartingBounds() { - Assume.assumeFalse(isShellTransitionsEnabled) - pipLayerRotates_StartingBounds_internal() - } - - @FlakyTest(bugId = 228024285) - @Test - fun pipLayerRotates_StartingBounds_ShellTransit() { - Assume.assumeTrue(isShellTransitionsEnabled) pipLayerRotates_StartingBounds_internal() } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/SetRequestedOrientationWhilePinnedTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/SetRequestedOrientationWhilePinnedTest.kt index e40f2bc1ed5a..51339a1deb4b 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/SetRequestedOrientationWhilePinnedTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/SetRequestedOrientationWhilePinnedTest.kt @@ -25,9 +25,9 @@ import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.FlickerTestParameterFactory import com.android.server.wm.flicker.annotation.Group4 import com.android.server.wm.flicker.dsl.FlickerBuilder +import com.android.server.wm.flicker.helpers.WindowUtils import com.android.server.wm.flicker.helpers.setRotation import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen -import com.android.server.wm.flicker.helpers.WindowUtils import com.android.server.wm.flicker.rules.RemoveAllTasksButHomeRule.Companion.removeAllTasksButHome import com.android.wm.shell.flicker.pip.PipTransition.BroadcastActionTrigger.Companion.ORIENTATION_LANDSCAPE import com.android.wm.shell.flicker.testapp.Components @@ -113,10 +113,6 @@ open class SetRequestedOrientationWhilePinnedTest( @Test override fun navBarLayerRotatesAndScales() = super.navBarLayerRotatesAndScales() - @FlakyTest(bugId = 206753786) - @Test - override fun statusBarLayerRotatesScales() = super.statusBarLayerRotatesScales() - @Presubmit @Test fun pipWindowInsideDisplay() { diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromAllApps.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromAllApps.kt new file mode 100644 index 000000000000..702710caded7 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromAllApps.kt @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.splitscreen + +import android.platform.test.annotations.Presubmit +import android.view.WindowManagerPolicyConstants +import androidx.test.filters.RequiresDevice +import com.android.server.wm.flicker.FlickerParametersRunnerFactory +import com.android.server.wm.flicker.FlickerTestParameter +import com.android.server.wm.flicker.FlickerTestParameterFactory +import com.android.server.wm.flicker.annotation.Group1 +import com.android.server.wm.flicker.dsl.FlickerBuilder +import com.android.wm.shell.flicker.appWindowBecomesVisible +import com.android.wm.shell.flicker.appWindowIsVisibleAtEnd +import com.android.wm.shell.flicker.helpers.SplitScreenHelper +import com.android.wm.shell.flicker.layerBecomesVisible +import com.android.wm.shell.flicker.layerIsVisibleAtEnd +import com.android.wm.shell.flicker.splitAppLayerBoundsBecomesVisible +import com.android.wm.shell.flicker.splitAppLayerBoundsIsVisibleAtEnd +import com.android.wm.shell.flicker.splitScreenDividerBecomesVisible +import org.junit.Assume +import org.junit.Before +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** + * Test enter split screen by dragging app icon from all apps. + * This test is only for large screen devices. + * + * To run this test: `atest WMShellFlickerTests:EnterSplitScreenByDragFromAllApps` + */ +@RequiresDevice +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +@Group1 +class EnterSplitScreenByDragFromAllApps( + testSpec: FlickerTestParameter +) : SplitScreenBase(testSpec) { + + @Before + open fun before() { + Assume.assumeTrue(taplInstrumentation.isTablet) + } + + override val transition: FlickerBuilder.() -> Unit + get() = { + super.transition(this) + setup { + eachRun { + taplInstrumentation.goHome() + primaryApp.launchViaIntent(wmHelper) + } + } + transitions { + taplInstrumentation.launchedAppState.taskbar + .openAllApps() + .getAppIcon(secondaryApp.appName) + .dragToSplitscreen(secondaryApp.component.packageName, + primaryApp.component.packageName) + } + } + + @Presubmit + @Test + fun dividerBecomesVisible() = testSpec.splitScreenDividerBecomesVisible() + + @Presubmit + @Test + fun primaryAppLayerIsVisibleAtEnd() = testSpec.layerIsVisibleAtEnd(primaryApp.component) + + @Presubmit + @Test + fun secondaryAppLayerBecomesVisible() = testSpec.layerBecomesVisible(secondaryApp.component) + + @Presubmit + @Test + fun primaryAppBoundsIsVisibleAtEnd() = testSpec.splitAppLayerBoundsIsVisibleAtEnd( + testSpec.endRotation, primaryApp.component, false /* splitLeftTop */) + + @Presubmit + @Test + fun secondaryAppBoundsBecomesVisible() = testSpec.splitAppLayerBoundsBecomesVisible( + testSpec.endRotation, secondaryApp.component, true /* splitLeftTop */) + + @Presubmit + @Test + fun primaryAppWindowIsVisibleAtEnd() = testSpec.appWindowIsVisibleAtEnd(primaryApp.component) + + @Presubmit + @Test + fun secondaryAppWindowBecomesVisible() = + testSpec.appWindowBecomesVisible(secondaryApp.component) + + companion object { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): List<FlickerTestParameter> { + return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests( + repetitions = SplitScreenHelper.TEST_REPETITIONS, + // TODO(b/176061063):The 3 buttons of nav bar do not exist in the hierarchy. + supportedNavigationModes = + listOf(WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL_OVERLAY)) + } + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromNotification.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromNotification.kt new file mode 100644 index 000000000000..7323d992ecd4 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromNotification.kt @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.splitscreen + +import android.platform.test.annotations.Presubmit +import android.view.WindowManagerPolicyConstants +import androidx.test.filters.RequiresDevice +import androidx.test.uiautomator.By +import androidx.test.uiautomator.Until +import com.android.server.wm.flicker.FlickerParametersRunnerFactory +import com.android.server.wm.flicker.FlickerTestParameter +import com.android.server.wm.flicker.FlickerTestParameterFactory +import com.android.server.wm.flicker.annotation.Group1 +import com.android.server.wm.flicker.dsl.FlickerBuilder +import com.android.wm.shell.flicker.appWindowIsVisibleAtEnd +import com.android.wm.shell.flicker.helpers.SplitScreenHelper +import com.android.wm.shell.flicker.layerBecomesVisible +import com.android.wm.shell.flicker.layerIsVisibleAtEnd +import com.android.wm.shell.flicker.splitAppLayerBoundsBecomesVisible +import com.android.wm.shell.flicker.splitAppLayerBoundsIsVisibleAtEnd +import com.android.wm.shell.flicker.splitScreenDividerBecomesVisible +import org.junit.Assume +import org.junit.Before +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** + * Test enter split screen by dragging app icon from notification. + * This test is only for large screen devices. + * + * To run this test: `atest WMShellFlickerTests:EnterSplitScreenByDragFromNotification` + */ +@RequiresDevice +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +@Group1 +class EnterSplitScreenByDragFromNotification( + testSpec: FlickerTestParameter +) : SplitScreenBase(testSpec) { + + private val sendNotificationApp = SplitScreenHelper.getSendNotification(instrumentation) + + @Before + fun before() { + Assume.assumeTrue(taplInstrumentation.isTablet) + } + + override val transition: FlickerBuilder.() -> Unit + get() = { + super.transition(this) + setup { + eachRun { + // Send a notification + sendNotificationApp.launchViaIntent(wmHelper) + val sendNotification = device.wait( + Until.findObject(By.text("Send Notification")), + SplitScreenHelper.TIMEOUT_MS + ) + sendNotification?.click() ?: error("Send notification button not found") + + taplInstrumentation.goHome() + primaryApp.launchViaIntent(wmHelper) + } + } + transitions { + SplitScreenHelper.dragFromNotificationToSplit(instrumentation, device, wmHelper) + } + teardown { + eachRun { + sendNotificationApp.exit(wmHelper) + } + } + } + + @Presubmit + @Test + fun dividerBecomesVisible() = testSpec.splitScreenDividerBecomesVisible() + + @Presubmit + @Test + fun primaryAppLayerIsVisibleAtEnd() = testSpec.layerIsVisibleAtEnd(primaryApp.component) + + @Presubmit + @Test + fun secondaryAppLayerBecomesVisible() = + testSpec.layerBecomesVisible(sendNotificationApp.component) + + @Presubmit + @Test + fun primaryAppBoundsIsVisibleAtEnd() = testSpec.splitAppLayerBoundsIsVisibleAtEnd( + testSpec.endRotation, primaryApp.component, false /* splitLeftTop */ + ) + + @Presubmit + @Test + fun secondaryAppBoundsBecomesVisible() = testSpec.splitAppLayerBoundsBecomesVisible( + testSpec.endRotation, sendNotificationApp.component, true /* splitLeftTop */ + ) + + @Presubmit + @Test + fun primaryAppWindowIsVisibleAtEnd() = testSpec.appWindowIsVisibleAtEnd(primaryApp.component) + + @Presubmit + @Test + fun secondaryAppWindowIsVisibleAtEnd() = + testSpec.appWindowIsVisibleAtEnd(sendNotificationApp.component) + + companion object { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): List<FlickerTestParameter> { + return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests( + repetitions = SplitScreenHelper.TEST_REPETITIONS, + // TODO(b/176061063):The 3 buttons of nav bar do not exist in the hierarchy. + supportedNavigationModes = + listOf(WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL_OVERLAY) + ) + } + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromTaskbar.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromTaskbar.kt new file mode 100644 index 000000000000..05c6e24ee89d --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromTaskbar.kt @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.splitscreen + +import android.platform.test.annotations.Presubmit +import android.view.WindowManagerPolicyConstants +import androidx.test.filters.RequiresDevice +import com.android.server.wm.flicker.FlickerParametersRunnerFactory +import com.android.server.wm.flicker.FlickerTestParameter +import com.android.server.wm.flicker.FlickerTestParameterFactory +import com.android.server.wm.flicker.annotation.Group1 +import com.android.server.wm.flicker.dsl.FlickerBuilder +import com.android.wm.shell.flicker.appWindowBecomesVisible +import com.android.wm.shell.flicker.appWindowIsVisibleAtEnd +import com.android.wm.shell.flicker.helpers.SplitScreenHelper +import com.android.wm.shell.flicker.layerBecomesVisible +import com.android.wm.shell.flicker.layerIsVisibleAtEnd +import com.android.wm.shell.flicker.splitAppLayerBoundsBecomesVisible +import com.android.wm.shell.flicker.splitAppLayerBoundsIsVisibleAtEnd +import com.android.wm.shell.flicker.splitScreenDividerBecomesVisible +import org.junit.Assume +import org.junit.Before +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** + * Test enter split screen by dragging app icon from taskbar. + * This test is only for large screen devices. + * + * To run this test: `atest WMShellFlickerTests:EnterSplitScreenByDragFromTaskbar` + */ +@RequiresDevice +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +@Group1 +class EnterSplitScreenByDragFromTaskbar( + testSpec: FlickerTestParameter +) : SplitScreenBase(testSpec) { + + @Before + fun before() { + Assume.assumeTrue(taplInstrumentation.isTablet) + } + + override val transition: FlickerBuilder.() -> Unit + get() = { + super.transition(this) + setup { + eachRun { + taplInstrumentation.goHome() + SplitScreenHelper.createShortcutOnHotseatIfNotExist( + taplInstrumentation, secondaryApp.appName + ) + primaryApp.launchViaIntent(wmHelper) + } + } + transitions { + taplInstrumentation.launchedAppState.taskbar + .getAppIcon(secondaryApp.appName) + .dragToSplitscreen( + secondaryApp.component.packageName, + primaryApp.component.packageName + ) + } + } + + @Presubmit + @Test + fun dividerBecomesVisible() = testSpec.splitScreenDividerBecomesVisible() + + @Presubmit + @Test + fun primaryAppLayerIsVisibleAtEnd() = testSpec.layerIsVisibleAtEnd(primaryApp.component) + + @Presubmit + @Test + fun secondaryAppLayerBecomesVisible() = testSpec.layerBecomesVisible(secondaryApp.component) + + @Presubmit + @Test + fun primaryAppBoundsIsVisibleAtEnd() = testSpec.splitAppLayerBoundsIsVisibleAtEnd( + testSpec.endRotation, primaryApp.component, false /* splitLeftTop */ + ) + + @Presubmit + @Test + fun secondaryAppBoundsBecomesVisible() = testSpec.splitAppLayerBoundsBecomesVisible( + testSpec.endRotation, secondaryApp.component, true /* splitLeftTop */ + ) + + @Presubmit + @Test + fun primaryAppWindowIsVisibleAtEnd() = testSpec.appWindowIsVisibleAtEnd(primaryApp.component) + + @Presubmit + @Test + fun secondaryAppWindowBecomesVisible() = + testSpec.appWindowBecomesVisible(secondaryApp.component) + + companion object { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): List<FlickerTestParameter> { + return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests( + repetitions = SplitScreenHelper.TEST_REPETITIONS, + supportedNavigationModes = + listOf(WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL_OVERLAY) + ) + } + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SplitScreenBase.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SplitScreenBase.kt new file mode 100644 index 000000000000..52c2daf96a3c --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SplitScreenBase.kt @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.splitscreen + +import android.app.Instrumentation +import android.content.Context +import androidx.test.platform.app.InstrumentationRegistry +import com.android.launcher3.tapl.LauncherInstrumentation +import com.android.server.wm.flicker.FlickerBuilderProvider +import com.android.server.wm.flicker.FlickerTestParameter +import com.android.server.wm.flicker.dsl.FlickerBuilder +import com.android.server.wm.flicker.helpers.setRotation +import com.android.wm.shell.flicker.helpers.SplitScreenHelper + +abstract class SplitScreenBase(protected val testSpec: FlickerTestParameter) { + protected val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation() + protected val taplInstrumentation = LauncherInstrumentation() + protected val context: Context = instrumentation.context + protected val primaryApp = SplitScreenHelper.getPrimary(instrumentation) + protected val secondaryApp = SplitScreenHelper.getSecondary(instrumentation) + + @FlickerBuilderProvider + fun buildFlicker(): FlickerBuilder { + return FlickerBuilder(instrumentation).apply { + transition(this) + } + } + + protected open val transition: FlickerBuilder.() -> Unit + get() = { + setup { + test { + taplInstrumentation.setEnableRotation(true) + setRotation(testSpec.startRotation) + taplInstrumentation.setExpectedRotation(testSpec.startRotation) + } + } + teardown { + eachRun { + primaryApp.exit(wmHelper) + secondaryApp.exit(wmHelper) + } + } + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/AndroidManifest.xml b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/AndroidManifest.xml index bd98585b67ec..bc0b0b6292b4 100644 --- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/AndroidManifest.xml +++ b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/AndroidManifest.xml @@ -92,6 +92,17 @@ </intent-filter> </activity> + <activity android:name=".SendNotificationActivity" + android:taskAffinity="com.android.wm.shell.flicker.testapp.SendNotificationActivity" + android:theme="@style/CutoutShortEdges" + android:label="SendNotificationApp" + android:exported="true"> + <intent-filter> + <action android:name="android.intent.action.MAIN"/> + <category android:name="android.intent.category.LAUNCHER"/> + </intent-filter> + </activity> + <activity android:name=".NonResizeableActivity" android:resizeableActivity="false" android:taskAffinity="com.android.wm.shell.flicker.testapp.NonResizeableActivity" diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_notification.xml b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_notification.xml new file mode 100644 index 000000000000..8d59b567e59b --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_notification.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright 2021 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical" + android:background="@android:color/black"> + + <Button + android:id="@+id/button_send_notification" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_centerHorizontal="true" + android:layout_centerVertical="true" + android:text="Send Notification" /> +</RelativeLayout> diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_pip.xml b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_pip.xml index 909b77c87894..229098313afa 100644 --- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_pip.xml +++ b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_pip.xml @@ -44,6 +44,39 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" + android:checkedButton="@id/enter_pip_on_leave_disabled"> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="Enter PiP on home press"/> + + <RadioButton + android:id="@+id/enter_pip_on_leave_disabled" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="Disabled" + android:onClick="onAutoPipSelected"/> + + <RadioButton + android:id="@+id/enter_pip_on_leave_manual" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="Via code behind" + android:onClick="onAutoPipSelected"/> + + <RadioButton + android:id="@+id/enter_pip_on_leave_autoenter" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="Auto-enter PiP" + android:onClick="onAutoPipSelected"/> + </RadioGroup> + + <RadioGroup + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" android:checkedButton="@id/ratio_default"> <TextView diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/Components.java b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/Components.java index 0ed59bdafd1d..a2b580da5898 100644 --- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/Components.java +++ b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/Components.java @@ -88,6 +88,12 @@ public class Components { PACKAGE_NAME + ".SplitScreenSecondaryActivity"); } + public static class SendNotificationActivity { + public static final String LABEL = "SendNotificationApp"; + public static final ComponentName COMPONENT = new ComponentName(PACKAGE_NAME, + PACKAGE_NAME + ".SendNotificationActivity"); + } + public static class LaunchBubbleActivity { public static final String LABEL = "LaunchBubbleApp"; public static final ComponentName COMPONENT = new ComponentName(PACKAGE_NAME, diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/PipActivity.java b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/PipActivity.java index a6ba7823e22d..615b1730579c 100644 --- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/PipActivity.java +++ b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/PipActivity.java @@ -48,6 +48,7 @@ import android.view.View; import android.view.Window; import android.view.WindowManager; import android.widget.CheckBox; +import android.widget.RadioButton; import java.util.ArrayList; import java.util.Arrays; @@ -201,6 +202,17 @@ public class PipActivity extends Activity { super.onDestroy(); } + @Override + protected void onUserLeaveHint() { + // Only used when auto PiP is disabled. This is to simulate the behavior that an app + // supports regular PiP but not auto PiP. + final boolean manuallyEnterPip = + ((RadioButton) findViewById(R.id.enter_pip_on_leave_manual)).isChecked(); + if (manuallyEnterPip) { + enterPictureInPictureMode(); + } + } + private RemoteAction buildRemoteAction(Icon icon, String label, String action) { final Intent intent = new Intent(action); final PendingIntent pendingIntent = @@ -216,6 +228,21 @@ public class PipActivity extends Activity { enterPictureInPictureMode(mPipParamsBuilder.build()); } + public void onAutoPipSelected(View v) { + switch (v.getId()) { + case R.id.enter_pip_on_leave_manual: + // disable auto enter PiP + case R.id.enter_pip_on_leave_disabled: + mPipParamsBuilder.setAutoEnterEnabled(false); + setPictureInPictureParams(mPipParamsBuilder.build()); + break; + case R.id.enter_pip_on_leave_autoenter: + mPipParamsBuilder.setAutoEnterEnabled(true); + setPictureInPictureParams(mPipParamsBuilder.build()); + break; + } + } + public void onRatioSelected(View v) { switch (v.getId()) { case R.id.ratio_default: diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/SendNotificationActivity.java b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/SendNotificationActivity.java new file mode 100644 index 000000000000..8020ef2270a0 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/SendNotificationActivity.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.testapp; + +import android.app.Activity; +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Intent; +import android.os.Bundle; +import android.view.View; + +public class SendNotificationActivity extends Activity { + private NotificationManager mNotificationManager; + private String mChannelId = "Channel id"; + private String mChannelName = "Channel name"; + private NotificationChannel mChannel; + private int mNotifyId = 0; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_notification); + findViewById(R.id.button_send_notification).setOnClickListener(this::sendNotification); + + mChannel = new NotificationChannel(mChannelId, mChannelName, + NotificationManager.IMPORTANCE_DEFAULT); + mNotificationManager = getSystemService(NotificationManager.class); + mNotificationManager.createNotificationChannel(mChannel); + } + + private void sendNotification(View v) { + PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, + new Intent(this, SendNotificationActivity.class), + PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE); + Notification notification = new Notification.Builder(this, mChannelId) + .setContentTitle("Notification App") + .setContentText("Notification content") + .setWhen(System.currentTimeMillis()) + .setSmallIcon(R.drawable.ic_message) + .setContentIntent(pendingIntent) + .build(); + + mNotificationManager.notify(mNotifyId, notification); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/Android.bp b/libs/WindowManager/Shell/tests/unittest/Android.bp index ea10be564351..1a8b9540cbd0 100644 --- a/libs/WindowManager/Shell/tests/unittest/Android.bp +++ b/libs/WindowManager/Shell/tests/unittest/Android.bp @@ -28,6 +28,9 @@ android_test { "**/*.java", "**/*.kt", ], + resource_dirs: [ + "res", + ], static_libs: [ "WindowManager-Shell", @@ -65,4 +68,9 @@ android_test { optimize: { enabled: false, }, + + aaptflags: [ + "--extra-packages", + "com.android.wm.shell.tests", + ], } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/MockSurfaceControlHelper.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/MockSurfaceControlHelper.java new file mode 100644 index 000000000000..f8b3fb3def62 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/MockSurfaceControlHelper.java @@ -0,0 +1,55 @@ +/* + * 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; + +import static org.mockito.Mockito.RETURNS_SELF; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; + +import android.view.SurfaceControl; + +/** + * Helper class to provide mocks for {@link SurfaceControl.Builder} and + * {@link SurfaceControl.Transaction} with method chaining support. + */ +public class MockSurfaceControlHelper { + private MockSurfaceControlHelper() {} + + /** + * Creates a mock {@link SurfaceControl.Builder} that supports method chaining and return the + * given {@link SurfaceControl} when calling {@link SurfaceControl.Builder#build()}. + * + * @param mockSurfaceControl the first {@link SurfaceControl} to return + * @return the mock of {@link SurfaceControl.Builder} + */ + public static SurfaceControl.Builder createMockSurfaceControlBuilder( + SurfaceControl mockSurfaceControl) { + final SurfaceControl.Builder mockBuilder = mock(SurfaceControl.Builder.class, RETURNS_SELF); + doReturn(mockSurfaceControl) + .when(mockBuilder) + .build(); + return mockBuilder; + } + + /** + * Creates a mock {@link SurfaceControl.Transaction} that supports method chaining. + * @return the mock of {@link SurfaceControl.Transaction} + */ + public static SurfaceControl.Transaction createMockSurfaceControlTransaction() { + return mock(SurfaceControl.Transaction.class, RETURNS_SELF); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellInitTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellInitTest.java new file mode 100644 index 000000000000..4bcdcaae230b --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellInitTest.java @@ -0,0 +1,93 @@ +/* + * 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; + +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; + +import androidx.test.filters.SmallTest; + +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.sysui.ShellInit; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.ArrayList; + +@SmallTest +@RunWith(AndroidTestingRunner.class) +@TestableLooper.RunWithLooper(setAsMainLooper = true) +public class ShellInitTest extends ShellTestCase { + + @Mock private ShellExecutor mMainExecutor; + + private ShellInit mImpl; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mImpl = new ShellInit(mMainExecutor); + } + + @Test + public void testAddInitCallbacks_expectCalledInOrder() { + ArrayList<Integer> results = new ArrayList<>(); + mImpl.addInitCallback(() -> { + results.add(1); + }, new Object()); + mImpl.addInitCallback(() -> { + results.add(2); + }, new Object()); + mImpl.addInitCallback(() -> { + results.add(3); + }, new Object()); + mImpl.init(); + assertTrue(results.get(0) == 1); + assertTrue(results.get(1) == 2); + assertTrue(results.get(2) == 3); + } + + @Test + public void testNoInitCallbacksAfterInit_expectException() { + mImpl.init(); + try { + mImpl.addInitCallback(() -> {}, new Object()); + fail("Expected exception when adding callback after init"); + } catch (IllegalArgumentException e) { + // Expected + } + } + + @Test + public void testDoubleInit_expectNoOp() { + ArrayList<Integer> results = new ArrayList<>(); + mImpl.addInitCallback(() -> { + results.add(1); + }, new Object()); + mImpl.init(); + assertTrue(results.size() == 1); + mImpl.init(); + assertTrue(results.size() == 1); + } +} 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 a6caefe6d3e7..7cbace5af48f 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,9 +16,13 @@ package com.android.wm.shell; +import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; +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_MULTI_WINDOW; 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 com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; @@ -26,19 +30,25 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.spy; import static com.android.wm.shell.ShellTaskOrganizer.TASK_LISTENER_TYPE_FULLSCREEN; import static com.android.wm.shell.ShellTaskOrganizer.TASK_LISTENER_TYPE_MULTI_WINDOW; import static com.android.wm.shell.ShellTaskOrganizer.TASK_LISTENER_TYPE_PIP; +import static com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import static org.junit.Assume.assumeFalse; import static org.mockito.ArgumentMatchers.any; 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.when; import android.app.ActivityManager.RunningTaskInfo; import android.app.TaskInfo; -import android.content.Context; +import android.app.WindowConfiguration; import android.content.LocusId; import android.content.pm.ParceledListSlice; import android.os.Binder; @@ -46,18 +56,21 @@ 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; import android.window.WindowContainerToken; +import android.window.WindowContainerTransaction; +import android.window.WindowContainerTransaction.Change; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; 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.compatui.CompatUIController; +import com.android.wm.shell.sysui.ShellCommandHandler; +import com.android.wm.shell.sysui.ShellInit; import org.junit.Before; import org.junit.Test; @@ -76,19 +89,19 @@ import java.util.Optional; */ @SmallTest @RunWith(AndroidJUnit4.class) -public class ShellTaskOrganizerTests { +public class ShellTaskOrganizerTests extends ShellTestCase { @Mock private ITaskOrganizerController mTaskOrganizerController; @Mock - private Context mContext; - @Mock private CompatUIController mCompatUI; + @Mock + private ShellExecutor mTestExecutor; + @Mock + private ShellCommandHandler mShellCommandHandler; - ShellTaskOrganizer mOrganizer; - private final SyncTransactionQueue mSyncTransactionQueue = mock(SyncTransactionQueue.class); - private final TransactionPool mTransactionPool = mock(TransactionPool.class); - private final ShellExecutor mTestExecutor = mock(ShellExecutor.class); + private ShellTaskOrganizer mOrganizer; + private ShellInit mShellInit; private class TrackingTaskListener implements ShellTaskOrganizer.TaskListener { final ArrayList<RunningTaskInfo> appeared = new ArrayList<>(); @@ -132,18 +145,42 @@ public class ShellTaskOrganizerTests { doReturn(ParceledListSlice.<TaskAppearedInfo>emptyList()) .when(mTaskOrganizerController).registerTaskOrganizer(any()); } catch (RemoteException e) {} - mOrganizer = spy(new ShellTaskOrganizer(mTaskOrganizerController, mTestExecutor, mContext, - mCompatUI, Optional.empty())); + mShellInit = spy(new ShellInit(mTestExecutor)); + mOrganizer = spy(new ShellTaskOrganizer(mShellInit, mShellCommandHandler, + mTaskOrganizerController, mCompatUI, Optional.empty(), Optional.empty(), + mTestExecutor)); + mShellInit.init(); } @Test - public void registerOrganizer_sendRegisterTaskOrganizer() throws RemoteException { - mOrganizer.registerOrganizer(); + public void instantiate_addInitCallback() { + verify(mShellInit, times(1)).addInitCallback(any(), any()); + } + + @Test + public void instantiate_addDumpCallback() { + verify(mShellCommandHandler, times(1)).addDumpCallback(any(), any()); + } + @Test + public void testInit_sendRegisterTaskOrganizer() throws RemoteException { verify(mTaskOrganizerController).registerTaskOrganizer(any(ITaskOrganizer.class)); } @Test + public void testTaskLeashReleasedAfterVanished() throws RemoteException { + assumeFalse(ENABLE_SHELL_TRANSITIONS); + RunningTaskInfo taskInfo = createTaskInfo(1, WINDOWING_MODE_MULTI_WINDOW); + SurfaceControl taskLeash = new SurfaceControl.Builder(new SurfaceSession()) + .setName("task").build(); + mOrganizer.registerOrganizer(); + mOrganizer.onTaskAppeared(taskInfo, taskLeash); + assertTrue(taskLeash.isValid()); + mOrganizer.onTaskVanished(taskInfo); + assertTrue(!taskLeash.isValid()); + } + + @Test public void testOneListenerPerType() { mOrganizer.addListenerForType(new TrackingTaskListener(), TASK_LISTENER_TYPE_MULTI_WINDOW); try { @@ -601,6 +638,99 @@ public class ShellTaskOrganizerTests { verify(mTaskOrganizerController).restartTaskTopActivityProcessIfVisible(task1.token); } + @Test + public void testPrepareClearBoundsForStandardTasks() { + MockToken token1 = new MockToken(); + RunningTaskInfo task1 = createTaskInfo(1, WINDOWING_MODE_UNDEFINED, token1); + mOrganizer.onTaskAppeared(task1, null); + + MockToken token2 = new MockToken(); + RunningTaskInfo task2 = createTaskInfo(2, WINDOWING_MODE_UNDEFINED, token2); + mOrganizer.onTaskAppeared(task2, null); + + MockToken otherDisplayToken = new MockToken(); + RunningTaskInfo otherDisplayTask = createTaskInfo(3, WINDOWING_MODE_UNDEFINED, + otherDisplayToken); + otherDisplayTask.displayId = 2; + mOrganizer.onTaskAppeared(otherDisplayTask, null); + + WindowContainerTransaction wct = mOrganizer.prepareClearBoundsForStandardTasks(1); + + assertEquals(wct.getChanges().size(), 2); + Change boundsChange1 = wct.getChanges().get(token1.binder()); + assertNotNull(boundsChange1); + assertNotEquals( + (boundsChange1.getWindowSetMask() & WindowConfiguration.WINDOW_CONFIG_BOUNDS), 0); + assertTrue(boundsChange1.getConfiguration().windowConfiguration.getBounds().isEmpty()); + + Change boundsChange2 = wct.getChanges().get(token2.binder()); + assertNotNull(boundsChange2); + assertNotEquals( + (boundsChange2.getWindowSetMask() & WindowConfiguration.WINDOW_CONFIG_BOUNDS), 0); + assertTrue(boundsChange2.getConfiguration().windowConfiguration.getBounds().isEmpty()); + } + + @Test + public void testPrepareClearBoundsForStandardTasks_onlyClearActivityTypeStandard() { + MockToken token1 = new MockToken(); + RunningTaskInfo task1 = createTaskInfo(1, WINDOWING_MODE_UNDEFINED, token1); + mOrganizer.onTaskAppeared(task1, null); + + MockToken token2 = new MockToken(); + RunningTaskInfo task2 = createTaskInfo(2, WINDOWING_MODE_UNDEFINED, token2); + task2.configuration.windowConfiguration.setActivityType(ACTIVITY_TYPE_HOME); + mOrganizer.onTaskAppeared(task2, null); + + WindowContainerTransaction wct = mOrganizer.prepareClearBoundsForStandardTasks(1); + + // Only clear bounds for task1 + assertEquals(1, wct.getChanges().size()); + assertNotNull(wct.getChanges().get(token1.binder())); + } + + @Test + public void testPrepareClearFreeformForStandardTasks() { + MockToken token1 = new MockToken(); + RunningTaskInfo task1 = createTaskInfo(1, WINDOWING_MODE_FREEFORM, token1); + mOrganizer.onTaskAppeared(task1, null); + + MockToken token2 = new MockToken(); + RunningTaskInfo task2 = createTaskInfo(2, WINDOWING_MODE_MULTI_WINDOW, token2); + mOrganizer.onTaskAppeared(task2, null); + + MockToken otherDisplayToken = new MockToken(); + RunningTaskInfo otherDisplayTask = createTaskInfo(3, WINDOWING_MODE_FREEFORM, + otherDisplayToken); + otherDisplayTask.displayId = 2; + mOrganizer.onTaskAppeared(otherDisplayTask, null); + + WindowContainerTransaction wct = mOrganizer.prepareClearFreeformForStandardTasks(1); + + // Only task with freeform windowing mode and the right display should be updated + assertEquals(wct.getChanges().size(), 1); + Change wmModeChange1 = wct.getChanges().get(token1.binder()); + assertNotNull(wmModeChange1); + assertEquals(wmModeChange1.getWindowingMode(), WINDOWING_MODE_UNDEFINED); + } + + @Test + public void testPrepareClearFreeformForStandardTasks_onlyClearActivityTypeStandard() { + MockToken token1 = new MockToken(); + RunningTaskInfo task1 = createTaskInfo(1, WINDOWING_MODE_FREEFORM, token1); + mOrganizer.onTaskAppeared(task1, null); + + MockToken token2 = new MockToken(); + RunningTaskInfo task2 = createTaskInfo(2, WINDOWING_MODE_FREEFORM, token2); + task2.configuration.windowConfiguration.setActivityType(ACTIVITY_TYPE_HOME); + mOrganizer.onTaskAppeared(task2, null); + + WindowContainerTransaction wct = mOrganizer.prepareClearFreeformForStandardTasks(1); + + // Only clear freeform for task1 + assertEquals(1, wct.getChanges().size()); + assertNotNull(wct.getChanges().get(token1.binder())); + } + private static RunningTaskInfo createTaskInfo(int taskId, int windowingMode) { RunningTaskInfo taskInfo = new RunningTaskInfo(); taskInfo.taskId = taskId; @@ -608,4 +738,30 @@ public class ShellTaskOrganizerTests { return taskInfo; } + private static RunningTaskInfo createTaskInfo(int taskId, int windowingMode, MockToken token) { + RunningTaskInfo taskInfo = createTaskInfo(taskId, windowingMode); + taskInfo.displayId = 1; + taskInfo.token = token.token(); + taskInfo.configuration.windowConfiguration.setActivityType(ACTIVITY_TYPE_STANDARD); + return taskInfo; + } + + private static class MockToken { + private final WindowContainerToken mToken; + private final IBinder mBinder; + + MockToken() { + mToken = mock(WindowContainerToken.class); + mBinder = mock(IBinder.class); + when(mToken.asBinder()).thenReturn(mBinder); + } + + WindowContainerToken token() { + return mToken; + } + + IBinder binder() { + return mBinder; + } + } } 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 403dbf9d9554..b5ee037892ba 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 @@ -24,6 +24,8 @@ import android.testing.TestableContext; import androidx.test.platform.app.InstrumentationRegistry; +import com.android.internal.protolog.common.ProtoLog; + import org.junit.After; import org.junit.Before; import org.mockito.MockitoAnnotations; @@ -37,6 +39,9 @@ public abstract class ShellTestCase { @Before public void shellSetup() { + // Disable protolog tool when running the tests from studio + ProtoLog.REQUIRE_PROTOLOGTOOL = false; + MockitoAnnotations.initMocks(this); final Context context = InstrumentationRegistry.getInstrumentation().getTargetContext(); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TaskViewTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TaskViewTest.java index 32f1587752cb..ff1d2990a82a 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TaskViewTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TaskViewTest.java @@ -169,6 +169,7 @@ public class TaskViewTest extends ShellTestCase { mTaskView.onTaskAppeared(mTaskInfo, mLeash); verify(mViewListener).onTaskCreated(eq(mTaskInfo.taskId), any()); + assertThat(mTaskView.isInitialized()).isTrue(); verify(mViewListener, never()).onTaskVisibilityChanged(anyInt(), anyBoolean()); } @@ -178,6 +179,7 @@ public class TaskViewTest extends ShellTestCase { mTaskView.surfaceCreated(mock(SurfaceHolder.class)); verify(mViewListener).onInitialized(); + assertThat(mTaskView.isInitialized()).isTrue(); // No task, no visibility change verify(mViewListener, never()).onTaskVisibilityChanged(anyInt(), anyBoolean()); } @@ -189,6 +191,7 @@ public class TaskViewTest extends ShellTestCase { mTaskView.surfaceCreated(mock(SurfaceHolder.class)); verify(mViewListener).onInitialized(); + assertThat(mTaskView.isInitialized()).isTrue(); verify(mViewListener).onTaskVisibilityChanged(eq(mTaskInfo.taskId), eq(true)); } @@ -223,6 +226,7 @@ public class TaskViewTest extends ShellTestCase { verify(mOrganizer).removeListener(eq(mTaskView)); verify(mViewListener).onReleased(); + assertThat(mTaskView.isInitialized()).isFalse(); } @Test @@ -270,6 +274,7 @@ public class TaskViewTest extends ShellTestCase { verify(mViewListener).onTaskCreated(eq(mTaskInfo.taskId), any()); verify(mViewListener, never()).onInitialized(); + assertThat(mTaskView.isInitialized()).isFalse(); // If there's no surface the task should be made invisible verify(mViewListener).onTaskVisibilityChanged(eq(mTaskInfo.taskId), eq(false)); } @@ -281,6 +286,7 @@ public class TaskViewTest extends ShellTestCase { verify(mTaskViewTransitions, never()).setTaskViewVisible(any(), anyBoolean()); verify(mViewListener).onInitialized(); + assertThat(mTaskView.isInitialized()).isTrue(); // No task, no visibility change verify(mViewListener, never()).onTaskVisibilityChanged(anyInt(), anyBoolean()); } @@ -353,6 +359,7 @@ public class TaskViewTest extends ShellTestCase { verify(mOrganizer).removeListener(eq(mTaskView)); verify(mViewListener).onReleased(); + assertThat(mTaskView.isInitialized()).isFalse(); verify(mTaskViewTransitions).removeTaskView(eq(mTaskView)); } 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 51eec27cfc0e..3672ae386dc4 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 @@ -25,8 +25,10 @@ import static org.mockito.Mockito.mock; import android.app.ActivityManager; import android.app.WindowConfiguration; +import android.graphics.Point; import android.graphics.Rect; import android.os.IBinder; +import android.view.Display; import android.window.IWindowContainerToken; import android.window.WindowContainerToken; @@ -38,6 +40,11 @@ public final class TestRunningTaskInfoBuilder { private int mParentTaskId = INVALID_TASK_ID; private @WindowConfiguration.ActivityType int mActivityType = ACTIVITY_TYPE_STANDARD; private @WindowConfiguration.WindowingMode int mWindowingMode = WINDOWING_MODE_UNDEFINED; + private int mDisplayId = Display.DEFAULT_DISPLAY; + private ActivityManager.TaskDescription.Builder mTaskDescriptionBuilder = null; + private final Point mPositionInParent = new Point(); + private boolean mIsVisible = false; + private long mLastActiveTime; public static WindowContainerToken createMockWCToken() { final IWindowContainerToken itoken = mock(IWindowContainerToken.class); @@ -46,6 +53,11 @@ public final class TestRunningTaskInfoBuilder { return new WindowContainerToken(itoken); } + public TestRunningTaskInfoBuilder setToken(WindowContainerToken token) { + mToken = token; + return this; + } + public TestRunningTaskInfoBuilder setBounds(Rect bounds) { mBounds.set(bounds); return this; @@ -68,17 +80,48 @@ public final class TestRunningTaskInfoBuilder { return this; } + public TestRunningTaskInfoBuilder setDisplayId(int displayId) { + mDisplayId = displayId; + return this; + } + + public TestRunningTaskInfoBuilder setTaskDescriptionBuilder( + ActivityManager.TaskDescription.Builder builder) { + mTaskDescriptionBuilder = builder; + return this; + } + + public TestRunningTaskInfoBuilder setPositionInParent(int x, int y) { + mPositionInParent.set(x, y); + return this; + } + + public TestRunningTaskInfoBuilder setVisible(boolean isVisible) { + mIsVisible = isVisible; + return this; + } + + public TestRunningTaskInfoBuilder setLastActiveTime(long lastActiveTime) { + mLastActiveTime = lastActiveTime; + return this; + } + public ActivityManager.RunningTaskInfo build() { final ActivityManager.RunningTaskInfo info = new ActivityManager.RunningTaskInfo(); - info.parentTaskId = INVALID_TASK_ID; info.taskId = sNextTaskId++; info.parentTaskId = mParentTaskId; + info.displayId = mDisplayId; info.configuration.windowConfiguration.setBounds(mBounds); info.configuration.windowConfiguration.setActivityType(mActivityType); info.configuration.windowConfiguration.setWindowingMode(mWindowingMode); info.token = mToken; info.isResizeable = true; info.supportsMultiWindow = true; + info.taskDescription = + mTaskDescriptionBuilder != null ? mTaskDescriptionBuilder.build() : null; + info.positionInParent = mPositionInParent; + info.isVisible = mIsVisible; + 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 da95c77d2b89..fe8b305093d7 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 @@ -48,9 +48,10 @@ public class TestShellExecutor implements ShellExecutor { } public void flushAll() { - for (Runnable r : mRunnables) { + final ArrayList<Runnable> tmpRunnable = new ArrayList<>(mRunnables); + mRunnables.clear(); + for (Runnable r : tmpRunnable) { r.run(); } - mRunnables.clear(); } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunnerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunnerTests.java new file mode 100644 index 000000000000..98b59126227c --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunnerTests.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.activityembedding; + +import static android.view.WindowManager.TRANSIT_OPEN; +import static android.window.TransitionInfo.FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY; +import static android.window.TransitionInfo.FLAG_IS_BEHIND_STARTING_WINDOW; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +import android.animation.Animator; +import android.window.TransitionInfo; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; + +/** + * Tests for {@link ActivityEmbeddingAnimationRunner}. + * + * Build/Install/Run: + * atest WMShellUnitTests:ActivityEmbeddingAnimationRunnerTests + */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class ActivityEmbeddingAnimationRunnerTests extends ActivityEmbeddingAnimationTestBase { + + @Before + public void setup() { + super.setUp(); + doNothing().when(mController).onAnimationFinished(any()); + } + + @Test + public void testStartAnimation() { + final TransitionInfo info = new TransitionInfo(TRANSIT_OPEN, 0); + final TransitionInfo.Change embeddingChange = createChange(); + embeddingChange.setFlags(FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY); + info.addChange(embeddingChange); + doReturn(mAnimator).when(mAnimRunner).createAnimator(any(), any(), any(), any()); + + mAnimRunner.startAnimation(mTransition, info, mStartTransaction, mFinishTransaction); + + final ArgumentCaptor<Runnable> finishCallback = ArgumentCaptor.forClass(Runnable.class); + verify(mAnimRunner).createAnimator(eq(info), eq(mStartTransaction), eq(mFinishTransaction), + finishCallback.capture()); + verify(mStartTransaction).apply(); + verify(mAnimator).start(); + verifyNoMoreInteractions(mFinishTransaction); + verify(mController, never()).onAnimationFinished(any()); + + // Call onAnimationFinished() when the animation is finished. + finishCallback.getValue().run(); + + verify(mController).onAnimationFinished(mTransition); + } + + @Test + public void testChangesBehindStartingWindow() { + final TransitionInfo info = new TransitionInfo(TRANSIT_OPEN, 0); + final TransitionInfo.Change embeddingChange = createChange(); + embeddingChange.setFlags(FLAG_IS_BEHIND_STARTING_WINDOW); + info.addChange(embeddingChange); + final Animator animator = mAnimRunner.createAnimator( + info, mStartTransaction, mFinishTransaction, + () -> mFinishCallback.onTransitionFinished(null /* wct */, null /* wctCB */)); + + // The animation should be empty when it is behind starting window. + assertEquals(0, animator.getDuration()); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationTestBase.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationTestBase.java new file mode 100644 index 000000000000..3792e8361284 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationTestBase.java @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.activityembedding; + +import static android.window.TransitionInfo.FLAG_FILLS_TASK; +import static android.window.TransitionInfo.FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assume.assumeTrue; +import static org.mockito.Mockito.mock; + +import android.animation.Animator; +import android.annotation.CallSuper; +import android.annotation.NonNull; +import android.graphics.Rect; +import android.os.IBinder; +import android.view.SurfaceControl; +import android.window.TransitionInfo; +import android.window.WindowContainerToken; + +import com.android.wm.shell.ShellTestCase; +import com.android.wm.shell.sysui.ShellInit; +import com.android.wm.shell.transition.Transitions; + +import org.junit.Before; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** TestBase for ActivityEmbedding animation. */ +abstract class ActivityEmbeddingAnimationTestBase extends ShellTestCase { + + @Mock + ShellInit mShellInit; + @Mock + Transitions mTransitions; + @Mock + IBinder mTransition; + @Mock + SurfaceControl.Transaction mStartTransaction; + @Mock + SurfaceControl.Transaction mFinishTransaction; + @Mock + Transitions.TransitionFinishCallback mFinishCallback; + @Mock + Animator mAnimator; + + ActivityEmbeddingController mController; + ActivityEmbeddingAnimationRunner mAnimRunner; + ActivityEmbeddingAnimationSpec mAnimSpec; + + @CallSuper + @Before + public void setUp() { + assumeTrue(Transitions.ENABLE_SHELL_TRANSITIONS); + MockitoAnnotations.initMocks(this); + mController = ActivityEmbeddingController.create(mContext, mShellInit, mTransitions); + assertNotNull(mController); + mAnimRunner = mController.mAnimationRunner; + assertNotNull(mAnimRunner); + mAnimSpec = mAnimRunner.mAnimationSpec; + assertNotNull(mAnimSpec); + spyOn(mController); + spyOn(mAnimRunner); + spyOn(mAnimSpec); + } + + /** Creates a mock {@link TransitionInfo.Change}. */ + static TransitionInfo.Change createChange() { + return new TransitionInfo.Change(mock(WindowContainerToken.class), + mock(SurfaceControl.class)); + } + + /** + * Creates a mock {@link TransitionInfo.Change} with + * {@link TransitionInfo#FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY} flag. + */ + static TransitionInfo.Change createEmbeddedChange(@NonNull Rect startBounds, + @NonNull Rect endBounds, @NonNull Rect taskBounds) { + final TransitionInfo.Change change = createChange(); + change.setFlags(FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY); + change.setStartAbsBounds(startBounds); + change.setEndAbsBounds(endBounds); + if (taskBounds.width() == startBounds.width() + && taskBounds.height() == startBounds.height() + && taskBounds.width() == endBounds.width() + && taskBounds.height() == endBounds.height()) { + change.setFlags(FLAG_FILLS_TASK); + } + return change; + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingControllerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingControllerTests.java new file mode 100644 index 000000000000..baecf6fe6673 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingControllerTests.java @@ -0,0 +1,188 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.activityembedding; + +import static android.view.WindowManager.TRANSIT_OPEN; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.never; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +import android.graphics.Rect; +import android.window.TransitionInfo; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Tests for {@link ActivityEmbeddingController}. + * + * Build/Install/Run: + * atest WMShellUnitTests:ActivityEmbeddingControllerTests + */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class ActivityEmbeddingControllerTests extends ActivityEmbeddingAnimationTestBase { + + private static final Rect TASK_BOUNDS = new Rect(0, 0, 1000, 500); + private static final Rect EMBEDDED_LEFT_BOUNDS = new Rect(0, 0, 500, 500); + private static final Rect EMBEDDED_RIGHT_BOUNDS = new Rect(500, 0, 1000, 500); + + @Before + public void setup() { + super.setUp(); + doReturn(mAnimator).when(mAnimRunner).createAnimator(any(), any(), any(), any()); + } + + @Test + public void testInstantiate() { + verify(mShellInit).addInitCallback(any(), any()); + } + + @Test + public void testOnInit() { + mController.onInit(); + + verify(mTransitions).addHandler(mController); + } + + @Test + public void testSetAnimScaleSetting() { + mController.setAnimScaleSetting(1.0f); + + verify(mAnimRunner).setAnimScaleSetting(1.0f); + verify(mAnimSpec).setAnimScaleSetting(1.0f); + } + + @Test + public void testStartAnimation_containsNonActivityEmbeddingChange() { + final TransitionInfo info = new TransitionInfo(TRANSIT_OPEN, 0); + final TransitionInfo.Change embeddingChange = createEmbeddedChange(EMBEDDED_LEFT_BOUNDS, + EMBEDDED_LEFT_BOUNDS, TASK_BOUNDS); + final TransitionInfo.Change nonEmbeddingChange = createChange(); + info.addChange(embeddingChange); + info.addChange(nonEmbeddingChange); + + // No-op because it contains non-embedded change. + assertFalse(mController.startAnimation(mTransition, info, mStartTransaction, + mFinishTransaction, mFinishCallback)); + verify(mAnimRunner, never()).startAnimation(any(), any(), any(), any()); + verifyNoMoreInteractions(mStartTransaction); + verifyNoMoreInteractions(mFinishTransaction); + verifyNoMoreInteractions(mFinishCallback); + } + + @Test + public void testStartAnimation_containsOnlyFillTaskActivityEmbeddingChange() { + final TransitionInfo info = new TransitionInfo(TRANSIT_OPEN, 0); + final TransitionInfo.Change embeddingChange = createEmbeddedChange(TASK_BOUNDS, TASK_BOUNDS, + TASK_BOUNDS); + info.addChange(embeddingChange); + + // No-op because it only contains embedded change that fills the Task. We will let the + // default handler to animate such transition. + assertFalse(mController.startAnimation(mTransition, info, mStartTransaction, + mFinishTransaction, mFinishCallback)); + verify(mAnimRunner, never()).startAnimation(any(), any(), any(), any()); + verifyNoMoreInteractions(mStartTransaction); + verifyNoMoreInteractions(mFinishTransaction); + verifyNoMoreInteractions(mFinishCallback); + } + + @Test + public void testStartAnimation_containsActivityEmbeddingSplitChange() { + // Change that occupies only part of the Task. + final TransitionInfo info = new TransitionInfo(TRANSIT_OPEN, 0); + final TransitionInfo.Change embeddingChange = createEmbeddedChange(EMBEDDED_LEFT_BOUNDS, + EMBEDDED_LEFT_BOUNDS, TASK_BOUNDS); + info.addChange(embeddingChange); + + // ActivityEmbeddingController will handle such transition. + assertTrue(mController.startAnimation(mTransition, info, mStartTransaction, + mFinishTransaction, mFinishCallback)); + verify(mAnimRunner).startAnimation(mTransition, info, mStartTransaction, + mFinishTransaction); + verify(mStartTransaction).apply(); + verifyNoMoreInteractions(mFinishTransaction); + } + + @Test + public void testStartAnimation_containsChangeEnterActivityEmbeddingSplit() { + // Change that is entering ActivityEmbedding split. + final TransitionInfo info = new TransitionInfo(TRANSIT_OPEN, 0); + final TransitionInfo.Change embeddingChange = createEmbeddedChange(TASK_BOUNDS, + EMBEDDED_LEFT_BOUNDS, TASK_BOUNDS); + info.addChange(embeddingChange); + + // ActivityEmbeddingController will handle such transition. + assertTrue(mController.startAnimation(mTransition, info, mStartTransaction, + mFinishTransaction, mFinishCallback)); + verify(mAnimRunner).startAnimation(mTransition, info, mStartTransaction, + mFinishTransaction); + verify(mStartTransaction).apply(); + verifyNoMoreInteractions(mFinishTransaction); + } + + @Test + public void testStartAnimation_containsChangeExitActivityEmbeddingSplit() { + // Change that is exiting ActivityEmbedding split. + final TransitionInfo info = new TransitionInfo(TRANSIT_OPEN, 0); + final TransitionInfo.Change embeddingChange = createEmbeddedChange(EMBEDDED_RIGHT_BOUNDS, + TASK_BOUNDS, TASK_BOUNDS); + info.addChange(embeddingChange); + + // ActivityEmbeddingController will handle such transition. + assertTrue(mController.startAnimation(mTransition, info, mStartTransaction, + mFinishTransaction, mFinishCallback)); + verify(mAnimRunner).startAnimation(mTransition, info, mStartTransaction, + mFinishTransaction); + verify(mStartTransaction).apply(); + verifyNoMoreInteractions(mFinishTransaction); + } + + @Test + public void testOnAnimationFinished() { + // Should not call finish when there is no transition. + assertThrows(IllegalStateException.class, + () -> mController.onAnimationFinished(mTransition)); + + final TransitionInfo info = new TransitionInfo(TRANSIT_OPEN, 0); + final TransitionInfo.Change embeddingChange = createEmbeddedChange(EMBEDDED_LEFT_BOUNDS, + EMBEDDED_LEFT_BOUNDS, TASK_BOUNDS); + info.addChange(embeddingChange); + mController.startAnimation(mTransition, info, mStartTransaction, + mFinishTransaction, mFinishCallback); + + verify(mFinishCallback, never()).onTransitionFinished(any(), any()); + mController.onAnimationFinished(mTransition); + verify(mFinishCallback).onTransitionFinished(any(), any()); + + // Should not call finish when the finish has already been called. + assertThrows(IllegalStateException.class, + () -> mController.onAnimationFinished(mTransition)); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/AppPairTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/AppPairTests.java deleted file mode 100644 index e73d9aaf190a..000000000000 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/AppPairTests.java +++ /dev/null @@ -1,123 +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.apppairs; - -import static android.view.Display.DEFAULT_DISPLAY; - -import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; - -import static com.google.common.truth.Truth.assertThat; - -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import android.app.ActivityManager; -import android.hardware.display.DisplayManager; - -import androidx.test.annotation.UiThreadTest; -import androidx.test.ext.junit.runners.AndroidJUnit4; -import androidx.test.filters.SmallTest; - -import com.android.wm.shell.ShellTaskOrganizer; -import com.android.wm.shell.ShellTestCase; -import com.android.wm.shell.TestRunningTaskInfoBuilder; -import com.android.wm.shell.common.DisplayController; -import com.android.wm.shell.common.SyncTransactionQueue; - -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; - -/** - * Tests for {@link AppPair} - * Build/Install/Run: - * atest WMShellUnitTests:AppPairTests - */ -@SmallTest -@RunWith(AndroidJUnit4.class) -public class AppPairTests extends ShellTestCase { - - private AppPairsController mController; - @Mock private SyncTransactionQueue mSyncQueue; - @Mock private ShellTaskOrganizer mTaskOrganizer; - @Mock private DisplayController mDisplayController; - - @Before - public void setUp() { - MockitoAnnotations.initMocks(this); - when(mDisplayController.getDisplayContext(anyInt())).thenReturn(mContext); - when(mDisplayController.getDisplay(anyInt())).thenReturn( - mContext.getSystemService(DisplayManager.class).getDisplay(DEFAULT_DISPLAY)); - mController = new TestAppPairsController( - mTaskOrganizer, - mSyncQueue, - mDisplayController); - spyOn(mController); - } - - @After - public void tearDown() {} - - @Test - @UiThreadTest - public void testContains() { - final ActivityManager.RunningTaskInfo task1 = new TestRunningTaskInfoBuilder().build(); - final ActivityManager.RunningTaskInfo task2 = new TestRunningTaskInfoBuilder().build(); - - final AppPair pair = mController.pairInner(task1, task2); - assertThat(pair.contains(task1.taskId)).isTrue(); - assertThat(pair.contains(task2.taskId)).isTrue(); - - pair.unpair(); - assertThat(pair.contains(task1.taskId)).isFalse(); - assertThat(pair.contains(task2.taskId)).isFalse(); - } - - @Test - @UiThreadTest - public void testVanishUnpairs() { - final ActivityManager.RunningTaskInfo task1 = new TestRunningTaskInfoBuilder().build(); - final ActivityManager.RunningTaskInfo task2 = new TestRunningTaskInfoBuilder().build(); - - final AppPair pair = mController.pairInner(task1, task2); - assertThat(pair.contains(task1.taskId)).isTrue(); - assertThat(pair.contains(task2.taskId)).isTrue(); - - pair.onTaskVanished(task1); - assertThat(pair.contains(task1.taskId)).isFalse(); - assertThat(pair.contains(task2.taskId)).isFalse(); - } - - @Test - @UiThreadTest - public void testOnTaskInfoChanged_notSupportsMultiWindow() { - final ActivityManager.RunningTaskInfo task1 = new TestRunningTaskInfoBuilder().build(); - final ActivityManager.RunningTaskInfo task2 = new TestRunningTaskInfoBuilder().build(); - - final AppPair pair = mController.pairInner(task1, task2); - assertThat(pair.contains(task1.taskId)).isTrue(); - assertThat(pair.contains(task2.taskId)).isTrue(); - - task1.supportsMultiWindow = false; - pair.onTaskInfoChanged(task1); - verify(mController).unpair(pair.getRootTaskId()); - } -} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/AppPairsControllerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/AppPairsControllerTests.java deleted file mode 100644 index 505c153eff9c..000000000000 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/AppPairsControllerTests.java +++ /dev/null @@ -1,104 +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.apppairs; - -import static android.view.Display.DEFAULT_DISPLAY; - -import static com.google.common.truth.Truth.assertThat; - -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.Mockito.when; - -import android.app.ActivityManager; -import android.hardware.display.DisplayManager; - -import androidx.test.annotation.UiThreadTest; -import androidx.test.ext.junit.runners.AndroidJUnit4; -import androidx.test.filters.SmallTest; - -import com.android.wm.shell.ShellTaskOrganizer; -import com.android.wm.shell.ShellTestCase; -import com.android.wm.shell.TestRunningTaskInfoBuilder; -import com.android.wm.shell.common.DisplayController; -import com.android.wm.shell.common.SyncTransactionQueue; - -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; - -/** Tests for {@link AppPairsController} */ -@SmallTest -@RunWith(AndroidJUnit4.class) -public class AppPairsControllerTests extends ShellTestCase { - private TestAppPairsController mController; - private TestAppPairsPool mPool; - @Mock private SyncTransactionQueue mSyncQueue; - @Mock private ShellTaskOrganizer mTaskOrganizer; - @Mock private DisplayController mDisplayController; - - @Before - public void setUp() { - MockitoAnnotations.initMocks(this); - when(mDisplayController.getDisplayContext(anyInt())).thenReturn(mContext); - when(mDisplayController.getDisplay(anyInt())).thenReturn( - mContext.getSystemService(DisplayManager.class).getDisplay(DEFAULT_DISPLAY)); - mController = new TestAppPairsController( - mTaskOrganizer, - mSyncQueue, - mDisplayController); - mPool = mController.getPool(); - } - - @After - public void tearDown() {} - - @Test - @UiThreadTest - public void testPairUnpair() { - final ActivityManager.RunningTaskInfo task1 = new TestRunningTaskInfoBuilder().build(); - final ActivityManager.RunningTaskInfo task2 = new TestRunningTaskInfoBuilder().build(); - - final AppPair pair = mController.pairInner(task1, task2); - assertThat(pair.contains(task1.taskId)).isTrue(); - assertThat(pair.contains(task2.taskId)).isTrue(); - assertThat(mPool.poolSize()).isGreaterThan(0); - - mController.unpair(task2.taskId); - assertThat(pair.contains(task1.taskId)).isFalse(); - assertThat(pair.contains(task2.taskId)).isFalse(); - assertThat(mPool.poolSize()).isGreaterThan(1); - } - - @Test - @UiThreadTest - public void testUnpair_DontReleaseToPool() { - final ActivityManager.RunningTaskInfo task1 = new TestRunningTaskInfoBuilder().build(); - final ActivityManager.RunningTaskInfo task2 = new TestRunningTaskInfoBuilder().build(); - - final AppPair pair = mController.pairInner(task1, task2); - assertThat(pair.contains(task1.taskId)).isTrue(); - assertThat(pair.contains(task2.taskId)).isTrue(); - - mController.unpair(task2.taskId, false /* releaseToPool */); - assertThat(pair.contains(task1.taskId)).isFalse(); - assertThat(pair.contains(task2.taskId)).isFalse(); - assertThat(mPool.poolSize()).isEqualTo(1); - } -} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/AppPairsPoolTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/AppPairsPoolTests.java deleted file mode 100644 index a3f134ee97ed..000000000000 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/AppPairsPoolTests.java +++ /dev/null @@ -1,77 +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.apppairs; - -import static com.google.common.truth.Truth.assertThat; - -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.Mockito.when; - -import androidx.test.ext.junit.runners.AndroidJUnit4; -import androidx.test.filters.SmallTest; - -import com.android.wm.shell.ShellTaskOrganizer; -import com.android.wm.shell.ShellTestCase; -import com.android.wm.shell.common.DisplayController; -import com.android.wm.shell.common.SyncTransactionQueue; - -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; - -/** Tests for {@link AppPairsPool} */ -@SmallTest -@RunWith(AndroidJUnit4.class) -public class AppPairsPoolTests extends ShellTestCase { - private TestAppPairsController mController; - private TestAppPairsPool mPool; - @Mock private SyncTransactionQueue mSyncQueue; - @Mock private ShellTaskOrganizer mTaskOrganizer; - @Mock private DisplayController mDisplayController; - - @Before - public void setUp() { - MockitoAnnotations.initMocks(this); - when(mDisplayController.getDisplayContext(anyInt())).thenReturn(mContext); - mController = new TestAppPairsController( - mTaskOrganizer, - mSyncQueue, - mDisplayController); - mPool = mController.getPool(); - } - - @After - public void tearDown() {} - - @Test - public void testInitialState() { - // Pool should always start off with at least 1 entry. - assertThat(mPool.poolSize()).isGreaterThan(0); - } - - @Test - public void testAcquireRelease() { - assertThat(mPool.poolSize()).isGreaterThan(0); - final AppPair appPair = mPool.acquire(); - assertThat(mPool.poolSize()).isGreaterThan(0); - mPool.release(appPair); - assertThat(mPool.poolSize()).isGreaterThan(1); - } -} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/TestAppPairsController.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/TestAppPairsController.java deleted file mode 100644 index 294bc1276291..000000000000 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/TestAppPairsController.java +++ /dev/null @@ -1,42 +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.apppairs; - -import static org.mockito.Mockito.mock; - -import com.android.wm.shell.ShellTaskOrganizer; -import com.android.wm.shell.common.DisplayController; -import com.android.wm.shell.common.DisplayImeController; -import com.android.wm.shell.common.DisplayInsetsController; -import com.android.wm.shell.common.ShellExecutor; -import com.android.wm.shell.common.SyncTransactionQueue; - -public class TestAppPairsController extends AppPairsController { - private TestAppPairsPool mPool; - - public TestAppPairsController(ShellTaskOrganizer organizer, SyncTransactionQueue syncQueue, - DisplayController displayController) { - super(organizer, syncQueue, displayController, mock(ShellExecutor.class), - mock(DisplayImeController.class), mock(DisplayInsetsController.class)); - mPool = new TestAppPairsPool(this); - setPairsPool(mPool); - } - - TestAppPairsPool getPool() { - return mPool; - } -} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/TestAppPairsPool.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/TestAppPairsPool.java deleted file mode 100644 index 1ee7fff44892..000000000000 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/TestAppPairsPool.java +++ /dev/null @@ -1,36 +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.apppairs; - -import android.app.ActivityManager; - -import com.android.wm.shell.TestRunningTaskInfoBuilder; - -public class TestAppPairsPool extends AppPairsPool{ - TestAppPairsPool(AppPairsController controller) { - super(controller); - } - - @Override - void incrementPool() { - final AppPair entry = new AppPair(mController); - final ActivityManager.RunningTaskInfo info = - new TestRunningTaskInfoBuilder().build(); - entry.onTaskAppeared(info, null /* leash */); - release(entry); - } -} 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 e7c5cb2183db..90a377309edd 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 @@ -20,6 +20,7 @@ import static android.window.BackNavigationInfo.KEY_TRIGGER_BACK; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; @@ -28,6 +29,7 @@ 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; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; @@ -39,6 +41,7 @@ import android.graphics.Point; import android.graphics.Rect; import android.hardware.HardwareBuffer; import android.os.Handler; +import android.os.IBinder; import android.os.RemoteCallback; import android.os.RemoteException; import android.provider.Settings; @@ -51,13 +54,16 @@ import android.view.RemoteAnimationTarget; import android.view.SurfaceControl; import android.window.BackEvent; import android.window.BackNavigationInfo; +import android.window.IBackNaviAnimationController; import android.window.IOnBackInvokedCallback; import androidx.test.filters.SmallTest; import androidx.test.platform.app.InstrumentationRegistry; import com.android.internal.util.test.FakeSettingsProvider; +import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.TestShellExecutor; +import com.android.wm.shell.sysui.ShellInit; import org.junit.Before; import org.junit.Ignore; @@ -74,10 +80,11 @@ import org.mockito.MockitoAnnotations; @TestableLooper.RunWithLooper @SmallTest @RunWith(AndroidTestingRunner.class) -public class BackAnimationControllerTest { +public class BackAnimationControllerTest extends ShellTestCase { private static final String ANIMATION_ENABLED = "1"; private final TestShellExecutor mShellExecutor = new TestShellExecutor(); + private ShellInit mShellInit; @Rule public TestableContext mContext = @@ -92,6 +99,9 @@ public class BackAnimationControllerTest { @Mock private IOnBackInvokedCallback mIOnBackInvokedCallback; + @Mock + private IBackNaviAnimationController mIBackNaviAnimationController; + private BackAnimationController mController; private int mEventTime = 0; @@ -107,10 +117,12 @@ public class BackAnimationControllerTest { Settings.Global.putString(mContentResolver, Settings.Global.ENABLE_BACK_ANIMATION, ANIMATION_ENABLED); mTestableLooper = TestableLooper.get(this); - mController = new BackAnimationController( + mShellInit = spy(new ShellInit(mShellExecutor)); + mController = new BackAnimationController(mShellInit, mShellExecutor, new Handler(mTestableLooper.getLooper()), mTransaction, mActivityTaskManager, mContext, mContentResolver); + mShellInit.init(); mEventTime = 0; mShellExecutor.flushAll(); } @@ -119,25 +131,24 @@ public class BackAnimationControllerTest { SurfaceControl screenshotSurface, HardwareBuffer hardwareBuffer, int backType, - IOnBackInvokedCallback onBackInvokedCallback) { - BackNavigationInfo navigationInfo = new BackNavigationInfo( - backType, - topAnimationTarget, - screenshotSurface, - hardwareBuffer, - new WindowConfiguration(), - new RemoteCallback((bundle) -> {}), - onBackInvokedCallback); - try { - doReturn(navigationInfo).when(mActivityTaskManager).startBackNavigation(anyBoolean()); - } catch (RemoteException ex) { - ex.rethrowFromSystemServer(); - } + IOnBackInvokedCallback onBackInvokedCallback, boolean prepareAnimation) { + BackNavigationInfo.Builder builder = new BackNavigationInfo.Builder() + .setType(backType) + .setDepartingAnimationTarget(topAnimationTarget) + .setScreenshotSurface(screenshotSurface) + .setScreenshotBuffer(hardwareBuffer) + .setTaskWindowConfiguration(new WindowConfiguration()) + .setOnBackNavigationDone(new RemoteCallback((bundle) -> {})) + .setOnBackInvokedCallback(onBackInvokedCallback) + .setPrepareAnimation(prepareAnimation); + + createNavigationInfo(builder); } private void createNavigationInfo(BackNavigationInfo.Builder builder) { try { - doReturn(builder.build()).when(mActivityTaskManager).startBackNavigation(anyBoolean()); + doReturn(builder.build()).when(mActivityTaskManager) + .startBackNavigation(anyBoolean(), any(), any()); } catch (RemoteException ex) { ex.rethrowFromSystemServer(); } @@ -159,12 +170,17 @@ public class BackAnimationControllerTest { } @Test + public void instantiateController_addInitCallback() { + verify(mShellInit, times(1)).addInitCallback(any(), any()); + } + + @Test @Ignore("b/207481538") public void crossActivity_screenshotAttachedAndVisible() { SurfaceControl screenshotSurface = new SurfaceControl(); HardwareBuffer hardwareBuffer = mock(HardwareBuffer.class); createNavigationInfo(createAnimationTarget(), screenshotSurface, hardwareBuffer, - BackNavigationInfo.TYPE_CROSS_ACTIVITY, null); + BackNavigationInfo.TYPE_CROSS_ACTIVITY, null, true); doMotionEvent(MotionEvent.ACTION_DOWN, 0); verify(mTransaction).setBuffer(screenshotSurface, hardwareBuffer); verify(mTransaction).setVisibility(screenshotSurface, true); @@ -177,7 +193,7 @@ public class BackAnimationControllerTest { HardwareBuffer hardwareBuffer = mock(HardwareBuffer.class); RemoteAnimationTarget animationTarget = createAnimationTarget(); createNavigationInfo(animationTarget, screenshotSurface, hardwareBuffer, - BackNavigationInfo.TYPE_CROSS_ACTIVITY, null); + BackNavigationInfo.TYPE_CROSS_ACTIVITY, null, true); doMotionEvent(MotionEvent.ACTION_DOWN, 0); doMotionEvent(MotionEvent.ACTION_MOVE, 100); // b/207481538, we check that the surface is not moved for now, we can re-enable this once @@ -211,15 +227,16 @@ public class BackAnimationControllerTest { mController.setBackToLauncherCallback(mIOnBackInvokedCallback); RemoteAnimationTarget animationTarget = createAnimationTarget(); createNavigationInfo(animationTarget, null, null, - BackNavigationInfo.TYPE_RETURN_TO_HOME, null); + BackNavigationInfo.TYPE_RETURN_TO_HOME, null, true); doMotionEvent(MotionEvent.ACTION_DOWN, 0); // Check that back start and progress is dispatched when first move. doMotionEvent(MotionEvent.ACTION_MOVE, 100); + simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME, animationTarget); verify(mIOnBackInvokedCallback).onBackStarted(); ArgumentCaptor<BackEvent> backEventCaptor = ArgumentCaptor.forClass(BackEvent.class); - verify(mIOnBackInvokedCallback).onBackProgressed(backEventCaptor.capture()); + verify(mIOnBackInvokedCallback, atLeastOnce()).onBackProgressed(backEventCaptor.capture()); assertEquals(animationTarget, backEventCaptor.getValue().getDepartingAnimationTarget()); // Check that back invocation is dispatched. @@ -232,17 +249,19 @@ public class BackAnimationControllerTest { public void animationDisabledFromSettings() throws RemoteException { // Toggle the setting off Settings.Global.putString(mContentResolver, Settings.Global.ENABLE_BACK_ANIMATION, "0"); - mController = new BackAnimationController( + ShellInit shellInit = new ShellInit(mShellExecutor); + mController = new BackAnimationController(shellInit, mShellExecutor, new Handler(mTestableLooper.getLooper()), mTransaction, mActivityTaskManager, mContext, mContentResolver); + shellInit.init(); mController.setBackToLauncherCallback(mIOnBackInvokedCallback); RemoteAnimationTarget animationTarget = createAnimationTarget(); IOnBackInvokedCallback appCallback = mock(IOnBackInvokedCallback.class); ArgumentCaptor<BackEvent> backEventCaptor = ArgumentCaptor.forClass(BackEvent.class); createNavigationInfo(animationTarget, null, null, - BackNavigationInfo.TYPE_RETURN_TO_HOME, appCallback); + BackNavigationInfo.TYPE_RETURN_TO_HOME, appCallback, false); triggerBackGesture(); @@ -260,9 +279,10 @@ public class BackAnimationControllerTest { mController.setBackToLauncherCallback(mIOnBackInvokedCallback); RemoteAnimationTarget animationTarget = createAnimationTarget(); createNavigationInfo(animationTarget, null, null, - BackNavigationInfo.TYPE_RETURN_TO_HOME, null); + BackNavigationInfo.TYPE_RETURN_TO_HOME, null, true); triggerBackGesture(); + simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME, animationTarget); // Check that back invocation is dispatched. verify(mIOnBackInvokedCallback).onBackInvoked(); @@ -271,11 +291,17 @@ public class BackAnimationControllerTest { // the previous transition is finished. doMotionEvent(MotionEvent.ACTION_DOWN, 0); verifyNoMoreInteractions(mIOnBackInvokedCallback); + mController.onBackToLauncherAnimationFinished(); + + // Verify that more events from a rejected swipe cannot start animation. + doMotionEvent(MotionEvent.ACTION_MOVE, 100); + doMotionEvent(MotionEvent.ACTION_UP, 0); + verifyNoMoreInteractions(mIOnBackInvokedCallback); // Verify that we start accepting gestures again once transition finishes. - mController.onBackToLauncherAnimationFinished(); doMotionEvent(MotionEvent.ACTION_DOWN, 0); doMotionEvent(MotionEvent.ACTION_MOVE, 100); + simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME, animationTarget); verify(mIOnBackInvokedCallback).onBackStarted(); } @@ -284,18 +310,49 @@ public class BackAnimationControllerTest { mController.setBackToLauncherCallback(mIOnBackInvokedCallback); RemoteAnimationTarget animationTarget = createAnimationTarget(); createNavigationInfo(animationTarget, null, null, - BackNavigationInfo.TYPE_RETURN_TO_HOME, null); + BackNavigationInfo.TYPE_RETURN_TO_HOME, null, true); triggerBackGesture(); + simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME, animationTarget); reset(mIOnBackInvokedCallback); // Simulate transition timeout. mShellExecutor.flushAll(); doMotionEvent(MotionEvent.ACTION_DOWN, 0); doMotionEvent(MotionEvent.ACTION_MOVE, 100); + simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME, animationTarget); verify(mIOnBackInvokedCallback).onBackStarted(); } + + @Test + public void cancelBackInvokeWhenLostFocus() throws RemoteException { + mController.setBackToLauncherCallback(mIOnBackInvokedCallback); + RemoteAnimationTarget animationTarget = createAnimationTarget(); + + createNavigationInfo(animationTarget, null, null, + BackNavigationInfo.TYPE_RETURN_TO_HOME, null, true); + + doMotionEvent(MotionEvent.ACTION_DOWN, 0); + // Check that back start and progress is dispatched when first move. + doMotionEvent(MotionEvent.ACTION_MOVE, 100); + simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME, animationTarget); + verify(mIOnBackInvokedCallback).onBackStarted(); + + // Check that back invocation is dispatched. + mController.setTriggerBack(true); // Fake trigger back + + // In case the focus has been changed. + IBinder token = mock(IBinder.class); + mController.mFocusObserver.focusLost(token); + mShellExecutor.flushAll(); + verify(mIOnBackInvokedCallback).onBackCancelled(); + + // No more back invoke. + doMotionEvent(MotionEvent.ACTION_UP, 0); + verify(mIOnBackInvokedCallback, never()).onBackInvoked(); + } + private void doMotionEvent(int actionDown, int coordinate) { mController.onMotionEvent( coordinate, coordinate, @@ -303,4 +360,14 @@ public class BackAnimationControllerTest { BackEvent.EDGE_LEFT); mEventTime += 10; } + + private void simulateRemoteAnimationStart(int type, RemoteAnimationTarget animationTarget) + throws RemoteException { + if (mController.mIBackAnimationRunner != null) { + final RemoteAnimationTarget[] targets = new RemoteAnimationTarget[]{animationTarget}; + mController.mIBackAnimationRunner.onAnimationStart(mIBackNaviAnimationController, type, + targets, null, null); + mShellExecutor.flushAll(); + } + } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubblesNavBarMotionEventHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubblesNavBarMotionEventHandlerTest.java new file mode 100644 index 000000000000..44ff35466ae2 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubblesNavBarMotionEventHandlerTest.java @@ -0,0 +1,142 @@ +/* + * 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.bubbles; + +import static android.view.MotionEvent.ACTION_CANCEL; +import static android.view.MotionEvent.ACTION_DOWN; +import static android.view.MotionEvent.ACTION_MOVE; +import static android.view.MotionEvent.ACTION_UP; + +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.floatThat; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.verifyZeroInteractions; + +import android.os.SystemClock; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; +import android.view.MotionEvent; +import android.view.WindowManager; + +import androidx.test.filters.SmallTest; + +import com.android.wm.shell.ShellTestCase; +import com.android.wm.shell.bubbles.BubblesNavBarMotionEventHandler.MotionEventListener; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** + * Test {@link MotionEvent} handling in {@link BubblesNavBarMotionEventHandler}. + * Verifies that swipe events + */ +@SmallTest +@TestableLooper.RunWithLooper(setAsMainLooper = true) +@RunWith(AndroidTestingRunner.class) +public class BubblesNavBarMotionEventHandlerTest extends ShellTestCase { + + private BubblesNavBarMotionEventHandler mMotionEventHandler; + @Mock + private WindowManager mWindowManager; + @Mock + private Runnable mInterceptTouchRunnable; + @Mock + private MotionEventListener mMotionEventListener; + private long mMotionEventTime; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + TestableBubblePositioner positioner = new TestableBubblePositioner(getContext(), + mWindowManager); + mMotionEventHandler = new BubblesNavBarMotionEventHandler(getContext(), positioner, + mInterceptTouchRunnable, mMotionEventListener); + mMotionEventTime = SystemClock.uptimeMillis(); + } + + @Test + public void testMotionEvent_swipeUpInGestureZone_handled() { + mMotionEventHandler.onMotionEvent(newEvent(ACTION_DOWN, 0, 990)); + mMotionEventHandler.onMotionEvent(newEvent(ACTION_MOVE, 0, 690)); + mMotionEventHandler.onMotionEvent(newEvent(ACTION_MOVE, 0, 490)); + mMotionEventHandler.onMotionEvent(newEvent(ACTION_MOVE, 0, 390)); + mMotionEventHandler.onMotionEvent(newEvent(ACTION_UP, 0, 390)); + + verify(mMotionEventListener).onDown(0, 990); + verify(mMotionEventListener).onMove(0, -300); + verify(mMotionEventListener).onMove(0, -500); + verify(mMotionEventListener).onMove(0, -600); + // Check that velocity up is about 5000 + verify(mMotionEventListener).onUp(eq(0f), floatThat(f -> Math.round(f) == -5000)); + verifyZeroInteractions(mMotionEventListener); + verify(mInterceptTouchRunnable).run(); + } + + @Test + public void testMotionEvent_swipeUpOutsideGestureZone_ignored() { + mMotionEventHandler.onMotionEvent(newEvent(ACTION_DOWN, 0, 500)); + mMotionEventHandler.onMotionEvent(newEvent(ACTION_MOVE, 0, 100)); + mMotionEventHandler.onMotionEvent(newEvent(ACTION_UP, 0, 100)); + + verifyZeroInteractions(mMotionEventListener); + verifyZeroInteractions(mInterceptTouchRunnable); + } + + @Test + public void testMotionEvent_horizontalMoveMoreThanTouchSlop_handled() { + mMotionEventHandler.onMotionEvent(newEvent(ACTION_DOWN, 0, 990)); + mMotionEventHandler.onMotionEvent(newEvent(ACTION_MOVE, 100, 990)); + mMotionEventHandler.onMotionEvent(newEvent(ACTION_UP, 100, 990)); + + verify(mMotionEventListener).onDown(0, 990); + verify(mMotionEventListener).onMove(100, 0); + verify(mMotionEventListener).onUp(0, 0); + verifyZeroInteractions(mMotionEventListener); + verify(mInterceptTouchRunnable).run(); + } + + @Test + public void testMotionEvent_moveLessThanTouchSlop_ignored() { + mMotionEventHandler.onMotionEvent(newEvent(ACTION_DOWN, 0, 990)); + mMotionEventHandler.onMotionEvent(newEvent(ACTION_MOVE, 0, 989)); + mMotionEventHandler.onMotionEvent(newEvent(ACTION_UP, 0, 989)); + + verify(mMotionEventListener).onDown(0, 990); + verifyNoMoreInteractions(mMotionEventListener); + verifyZeroInteractions(mInterceptTouchRunnable); + } + + @Test + public void testMotionEvent_actionCancel_listenerNotified() { + mMotionEventHandler.onMotionEvent(newEvent(ACTION_DOWN, 0, 990)); + mMotionEventHandler.onMotionEvent(newEvent(ACTION_CANCEL, 0, 990)); + verify(mMotionEventListener).onDown(0, 990); + verify(mMotionEventListener).onCancel(); + verifyNoMoreInteractions(mMotionEventListener); + verifyZeroInteractions(mInterceptTouchRunnable); + } + + private MotionEvent newEvent(int actionDown, float x, float y) { + MotionEvent event = MotionEvent.obtain(0L, mMotionEventTime, actionDown, x, y, 0); + mMotionEventTime += 10; + return event; + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubblesTestActivity.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubblesTestActivity.java index d5fbe556045a..0537d0ea2404 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubblesTestActivity.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubblesTestActivity.java @@ -20,7 +20,7 @@ import android.app.Activity; import android.content.Intent; import android.os.Bundle; -import com.android.wm.shell.R; +import com.android.wm.shell.tests.R; /** * Referenced by NotificationTestHelper#makeBubbleMetadata diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/ExpandedViewAnimationControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/ExpandedViewAnimationControllerTest.java new file mode 100644 index 000000000000..991913afbb90 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/ExpandedViewAnimationControllerTest.java @@ -0,0 +1,181 @@ +/* + * 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.bubbles.animation; + +import static com.google.common.truth.Truth.assertThat; + +import static org.junit.Assume.assumeTrue; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; +import android.view.ViewConfiguration; +import android.view.WindowManager; + +import androidx.test.filters.SmallTest; + +import com.android.wm.shell.ShellTestCase; +import com.android.wm.shell.bubbles.BubbleExpandedView; +import com.android.wm.shell.bubbles.TestableBubblePositioner; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@SmallTest +@RunWith(AndroidTestingRunner.class) +@TestableLooper.RunWithLooper(setAsMainLooper = true) +public class ExpandedViewAnimationControllerTest extends ShellTestCase { + + private ExpandedViewAnimationController mController; + + @Mock + private WindowManager mWindowManager; + + @Mock + private BubbleExpandedView mMockExpandedView; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + TestableBubblePositioner positioner = new TestableBubblePositioner(getContext(), + mWindowManager); + mController = new ExpandedViewAnimationControllerImpl(getContext(), positioner); + + mController.setExpandedView(mMockExpandedView); + when(mMockExpandedView.getContentHeight()).thenReturn(1000); + } + + @Test + public void testUpdateDrag_expandedViewMovesUpAndClipped() { + // Drag by 50 pixels which corresponds to 10 pixels with overscroll + int dragDistance = 50; + int dampenedDistance = 10; + + mController.updateDrag(dragDistance); + + verify(mMockExpandedView).setTopClip(dampenedDistance); + verify(mMockExpandedView).setContentTranslationY(-dampenedDistance); + verify(mMockExpandedView).setManageButtonTranslationY(-dampenedDistance); + } + + @Test + public void testUpdateDrag_zOrderUpdates() { + mController.updateDrag(10); + mController.updateDrag(20); + + verify(mMockExpandedView, times(1)).setSurfaceZOrderedOnTop(true); + verify(mMockExpandedView, times(1)).setAnimating(true); + } + + @Test + public void testUpdateDrag_moveBackToZero_zOrderRestored() { + mController.updateDrag(50); + reset(mMockExpandedView); + mController.updateDrag(0); + mController.updateDrag(0); + + verify(mMockExpandedView, times(1)).setSurfaceZOrderedOnTop(false); + verify(mMockExpandedView, times(1)).setAnimating(false); + } + + @Test + public void testUpdateDrag_hapticFeedbackOnlyOnce() { + // Drag by 10 which is below the collapse threshold - no feedback + mController.updateDrag(10); + verify(mMockExpandedView, times(0)).performHapticFeedback(anyInt()); + // 150 takes it over the threshold - perform feedback + mController.updateDrag(150); + verify(mMockExpandedView, times(1)).performHapticFeedback(anyInt()); + // Continue dragging, no more feedback + mController.updateDrag(200); + verify(mMockExpandedView, times(1)).performHapticFeedback(anyInt()); + // Drag below threshold and over again - no more feedback + mController.updateDrag(10); + mController.updateDrag(150); + verify(mMockExpandedView, times(1)).performHapticFeedback(anyInt()); + } + + @Test + public void testShouldCollapse_doNotCollapseIfNotDragged() { + assertThat(mController.shouldCollapse()).isFalse(); + } + + @Test + public void testShouldCollapse_doNotCollapseIfVelocityDown() { + assumeTrue("Min fling velocity should be > 1 for this test", getMinFlingVelocity() > 1); + mController.setSwipeVelocity(getVelocityAboveMinFling()); + assertThat(mController.shouldCollapse()).isFalse(); + } + + @Test + public void tesShouldCollapse_doNotCollapseIfVelocityUpIsSmall() { + assumeTrue("Min fling velocity should be > 1 for this test", getMinFlingVelocity() > 1); + mController.setSwipeVelocity(-getVelocityBelowMinFling()); + assertThat(mController.shouldCollapse()).isFalse(); + } + + @Test + public void testShouldCollapse_collapseIfVelocityUpIsLarge() { + assumeTrue("Min fling velocity should be > 1 for this test", getMinFlingVelocity() > 1); + mController.setSwipeVelocity(-getVelocityAboveMinFling()); + assertThat(mController.shouldCollapse()).isTrue(); + } + + @Test + public void testShouldCollapse_collapseIfPastThreshold() { + mController.updateDrag(500); + assertThat(mController.shouldCollapse()).isTrue(); + } + + @Test + public void testReset() { + mController.updateDrag(100); + reset(mMockExpandedView); + mController.reset(); + verify(mMockExpandedView, atLeastOnce()).setAnimating(false); + verify(mMockExpandedView).setContentAlpha(1); + verify(mMockExpandedView).setBackgroundAlpha(1); + verify(mMockExpandedView).setManageButtonAlpha(1); + verify(mMockExpandedView).setManageButtonAlpha(1); + verify(mMockExpandedView).setTopClip(0); + verify(mMockExpandedView).setContentTranslationY(-0f); + verify(mMockExpandedView).setManageButtonTranslationY(-0f); + verify(mMockExpandedView).setBottomClip(0); + verify(mMockExpandedView).movePointerBy(0, 0); + assertThat(mController.shouldCollapse()).isFalse(); + } + + private int getVelocityBelowMinFling() { + return getMinFlingVelocity() - 1; + } + + private int getVelocityAboveMinFling() { + return getMinFlingVelocity() + 1; + } + + private int getMinFlingVelocity() { + return ViewConfiguration.get(getContext()).getScaledMinimumFlingVelocity(); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/storage/BubblePersistentRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/storage/BubblePersistentRepositoryTest.kt index 0972cf2c032f..1636c5f73133 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/storage/BubblePersistentRepositoryTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/storage/BubblePersistentRepositoryTest.kt @@ -25,6 +25,7 @@ import com.android.wm.shell.bubbles.storage.BubbleXmlHelperTest.Companion.sparse import junit.framework.Assert.assertEquals import junit.framework.Assert.assertNotNull import junit.framework.Assert.assertTrue +import org.junit.After import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -61,6 +62,12 @@ class BubblePersistentRepositoryTest : ShellTestCase() { bubbles.put(1, user1Bubbles) } + @After + fun teardown() { + // Clean up the any persisted bubbles for the next run + repository.persistsToDisk(SparseArray()) + } + @Test fun testReadWriteOperation() { // Verify read before write doesn't cause FileNotFoundException @@ -71,4 +78,4 @@ class BubblePersistentRepositoryTest : ShellTestCase() { repository.persistsToDisk(bubbles) assertTrue(sparseArraysEqual(bubbles, repository.readFromDisk())) } -}
\ No newline at end of file +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayChangeControllerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayChangeControllerTests.java new file mode 100644 index 000000000000..b8aa8e7cbc48 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayChangeControllerTests.java @@ -0,0 +1,64 @@ +/* + * 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 static com.android.dx.mockito.inline.extended.ExtendedMockito.spy; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import android.view.IWindowManager; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import com.android.wm.shell.ShellTestCase; +import com.android.wm.shell.sysui.ShellInit; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** + * Tests for the display change controller. + * + * Build/Install/Run: + * atest WMShellUnitTests:DisplayChangeControllerTests + */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class DisplayChangeControllerTests extends ShellTestCase { + + private @Mock IWindowManager mWM; + private @Mock ShellInit mShellInit; + private @Mock ShellExecutor mMainExecutor; + private DisplayChangeController mController; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mController = spy(new DisplayChangeController(mWM, mShellInit, mMainExecutor)); + } + + @Test + public void instantiate_addInitCallback() { + verify(mShellInit, times(1)).addInitCallback(any(), any()); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayControllerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayControllerTests.java new file mode 100644 index 000000000000..1e5e153fdfe1 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayControllerTests.java @@ -0,0 +1,65 @@ +/* + * 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 static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import android.content.Context; +import android.view.IWindowManager; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import com.android.wm.shell.ShellTestCase; +import com.android.wm.shell.sysui.ShellInit; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** + * Tests for the display controller. + * + * Build/Install/Run: + * atest WMShellUnitTests:DisplayControllerTests + */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class DisplayControllerTests extends ShellTestCase { + + private @Mock Context mContext; + private @Mock IWindowManager mWM; + private @Mock ShellInit mShellInit; + private @Mock ShellExecutor mMainExecutor; + private DisplayController mController; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mController = new DisplayController(mContext, mWM, mShellInit, mMainExecutor); + } + + @Test + public void instantiateController_addInitCallback() { + verify(mShellInit, times(1)).addInitCallback(any(), eq(mController)); + } +} 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 b88845044263..9967e5f47752 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 @@ -26,6 +26,7 @@ import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyZeroInteractions; @@ -39,26 +40,33 @@ import android.view.SurfaceControl; import androidx.test.filters.SmallTest; import com.android.internal.view.IInputMethodManager; +import com.android.wm.shell.ShellTestCase; +import com.android.wm.shell.sysui.ShellInit; import org.junit.Before; import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; import java.util.concurrent.Executor; @SmallTest -public class DisplayImeControllerTest { +public class DisplayImeControllerTest extends ShellTestCase { + @Mock private SurfaceControl.Transaction mT; - private DisplayImeController.PerDisplay mPerDisplay; + @Mock private IInputMethodManager mMock; + @Mock + private ShellInit mShellInit; + private DisplayImeController.PerDisplay mPerDisplay; private Executor mExecutor; @Before public void setUp() throws Exception { - mT = mock(SurfaceControl.Transaction.class); - mMock = mock(IInputMethodManager.class); + MockitoAnnotations.initMocks(this); mExecutor = spy(Runnable::run); - mPerDisplay = new DisplayImeController(null, null, null, mExecutor, new TransactionPool() { + mPerDisplay = new DisplayImeController(null, mShellInit, null, null, new TransactionPool() { @Override public SurfaceControl.Transaction acquire() { return mT; @@ -67,7 +75,7 @@ public class DisplayImeControllerTest { @Override public void release(SurfaceControl.Transaction t) { } - }) { + }, mExecutor) { @Override public IInputMethodManager getImms() { return mMock; @@ -78,6 +86,11 @@ public class DisplayImeControllerTest { } @Test + public void instantiateController_addInitCallback() { + verify(mShellInit, times(1)).addInitCallback(any(), any()); + } + + @Test public void insetsControlChanged_schedulesNoWorkOnExecutor() { mPerDisplay.insetsControlChanged(insetsStateWithIme(false), insetsSourceControl()); verifyZeroInteractions(mExecutor); @@ -121,7 +134,7 @@ public class DisplayImeControllerTest { private InsetsSourceControl[] insetsSourceControl() { return new InsetsSourceControl[]{ new InsetsSourceControl( - ITYPE_IME, mock(SurfaceControl.class), new Point(0, 0), Insets.NONE) + ITYPE_IME, mock(SurfaceControl.class), false, new Point(0, 0), Insets.NONE) }; } 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 3bf06cc0ede3..5f5a3c584ee0 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 @@ -19,11 +19,13 @@ package com.android.wm.shell.common; import static android.view.Display.DEFAULT_DISPLAY; import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.notNull; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import android.content.ComponentName; import android.os.RemoteException; import android.util.SparseArray; import android.view.IDisplayWindowInsetsController; @@ -34,7 +36,9 @@ import android.view.InsetsVisibilities; import androidx.test.filters.SmallTest; +import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.TestShellExecutor; +import com.android.wm.shell.sysui.ShellInit; import org.junit.Before; import org.junit.Test; @@ -45,7 +49,7 @@ import org.mockito.MockitoAnnotations; import java.util.List; @SmallTest -public class DisplayInsetsControllerTest { +public class DisplayInsetsControllerTest extends ShellTestCase { private static final int SECOND_DISPLAY = DEFAULT_DISPLAY + 10; @@ -53,6 +57,8 @@ public class DisplayInsetsControllerTest { private IWindowManager mWm; @Mock private DisplayController mDisplayController; + @Mock + private ShellInit mShellInit; private DisplayInsetsController mController; private SparseArray<IDisplayWindowInsetsController> mInsetsControllersByDisplayId; private TestShellExecutor mExecutor; @@ -67,11 +73,16 @@ public class DisplayInsetsControllerTest { mInsetsControllersByDisplayId = new SparseArray<>(); mDisplayIdCaptor = ArgumentCaptor.forClass(Integer.class); mInsetsControllerCaptor = ArgumentCaptor.forClass(IDisplayWindowInsetsController.class); - mController = new DisplayInsetsController(mWm, mDisplayController, mExecutor); + mController = new DisplayInsetsController(mWm, mShellInit, mDisplayController, mExecutor); addDisplay(DEFAULT_DISPLAY); } @Test + public void instantiateController_addInitCallback() { + verify(mShellInit, times(1)).addInitCallback(any(), any()); + } + + @Test public void testOnDisplayAdded_setsDisplayWindowInsetsControllerOnWMService() throws RemoteException { addDisplay(SECOND_DISPLAY); @@ -164,7 +175,7 @@ public class DisplayInsetsControllerTest { int hideInsetsCount = 0; @Override - public void topFocusedWindowChanged(String packageName, + public void topFocusedWindowChanged(ComponentName component, InsetsVisibilities requestedVisibilities) { topFocusedWindowChangedCount++; } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayLayoutTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayLayoutTest.java index 0ffa5b35331d..d467b399ebbb 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayLayoutTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayLayoutTest.java @@ -41,11 +41,13 @@ import androidx.test.filters.SmallTest; import com.android.internal.R; import com.android.internal.policy.SystemBarUtils; +import com.android.wm.shell.ShellTestCase; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.mockito.MockitoSession; +import org.mockito.quality.Strictness; /** * Tests for {@link DisplayLayout}. @@ -54,13 +56,14 @@ import org.mockito.MockitoSession; * atest WMShellUnitTests:DisplayLayoutTest */ @SmallTest -public class DisplayLayoutTest { +public class DisplayLayoutTest extends ShellTestCase { private MockitoSession mMockitoSession; @Before public void setup() { mMockitoSession = mockitoSession() .initMocks(this) + .strictness(Strictness.WARN) .mockStatic(SystemBarUtils.class) .startMocking(); } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/TaskStackListenerImplTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/TaskStackListenerImplTest.java index 96938ebc27df..1347e061eb45 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/TaskStackListenerImplTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/TaskStackListenerImplTest.java @@ -35,6 +35,8 @@ import android.window.TaskSnapshot; import androidx.test.filters.SmallTest; +import com.android.wm.shell.ShellTestCase; + import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -47,7 +49,7 @@ import org.mockito.MockitoAnnotations; @RunWith(AndroidTestingRunner.class) @TestableLooper.RunWithLooper @SmallTest -public class TaskStackListenerImplTest { +public class TaskStackListenerImplTest extends ShellTestCase { @Mock private IActivityTaskManager mActivityTaskManager; 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 f1e602fcf778..695550dd8fa5 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 @@ -133,7 +133,7 @@ public class SplitLayoutTests extends ShellTestCase { mSplitLayout.snapToTarget(0 /* currentPosition */, snapTarget); waitDividerFlingFinished(); - verify(mSplitLayoutHandler).onSnappedToDismiss(eq(false)); + verify(mSplitLayoutHandler).onSnappedToDismiss(eq(false), anyInt()); } @Test @@ -145,7 +145,7 @@ public class SplitLayoutTests extends ShellTestCase { mSplitLayout.snapToTarget(0 /* currentPosition */, snapTarget); waitDividerFlingFinished(); - verify(mSplitLayoutHandler).onSnappedToDismiss(eq(true)); + verify(mSplitLayoutHandler).onSnappedToDismiss(eq(true), anyInt()); } @Test @@ -159,7 +159,8 @@ public class SplitLayoutTests extends ShellTestCase { } private void waitDividerFlingFinished() { - verify(mSplitLayout).flingDividePosition(anyInt(), anyInt(), mRunnableCaptor.capture()); + verify(mSplitLayout).flingDividePosition(anyInt(), anyInt(), anyInt(), + mRunnableCaptor.capture()); mRunnableCaptor.getValue().run(); } 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 596100dcdead..6292130ddec9 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 @@ -29,6 +29,7 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -53,6 +54,8 @@ 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.compatui.letterboxedu.LetterboxEduWindowManager; +import com.android.wm.shell.sysui.ShellController; +import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.transition.Transitions; import org.junit.Before; @@ -78,6 +81,8 @@ public class CompatUIControllerTest extends ShellTestCase { private static final int TASK_ID = 12; private CompatUIController mController; + private ShellInit mShellInit; + private @Mock ShellController mMockShellController; private @Mock DisplayController mMockDisplayController; private @Mock DisplayInsetsController mMockDisplayInsetsController; private @Mock DisplayLayout mMockDisplayLayout; @@ -105,9 +110,10 @@ public class CompatUIControllerTest extends ShellTestCase { doReturn(TASK_ID).when(mMockLetterboxEduLayout).getTaskId(); doReturn(true).when(mMockLetterboxEduLayout).createLayout(anyBoolean()); doReturn(true).when(mMockLetterboxEduLayout).updateCompatInfo(any(), any(), anyBoolean()); - mController = new CompatUIController(mContext, mMockDisplayController, - mMockDisplayInsetsController, mMockImeController, mMockSyncQueue, mMockExecutor, - mMockTransitionsLazy) { + mShellInit = spy(new ShellInit(mMockExecutor)); + mController = new CompatUIController(mContext, mShellInit, mMockShellController, + mMockDisplayController, mMockDisplayInsetsController, mMockImeController, + mMockSyncQueue, mMockExecutor, mMockTransitionsLazy) { @Override CompatUIWindowManager createCompatUiWindowManager(Context context, TaskInfo taskInfo, ShellTaskOrganizer.TaskListener taskListener) { @@ -120,10 +126,21 @@ public class CompatUIControllerTest extends ShellTestCase { return mMockLetterboxEduLayout; } }; + mShellInit.init(); spyOn(mController); } @Test + public void instantiateController_addInitCallback() { + verify(mShellInit, times(1)).addInitCallback(any(), any()); + } + + @Test + public void instantiateController_registerKeyguardChangeListener() { + verify(mMockShellController, times(1)).addKeyguardChangeListener(any()); + } + + @Test public void testListenerRegistered() { verify(mMockDisplayController).addDisplayWindowListener(mController); verify(mMockImeController).addPositionProcessor(mController); @@ -324,7 +341,7 @@ public class CompatUIControllerTest extends ShellTestCase { /* hasSizeCompat= */ true, CAMERA_COMPAT_CONTROL_HIDDEN), mMockTaskListener); // Verify that the restart button is hidden after keyguard becomes showing. - mController.onKeyguardShowingChanged(true); + mController.onKeyguardVisibilityChanged(true, false, false); verify(mMockCompatLayout).updateVisibility(false); verify(mMockLetterboxEduLayout).updateVisibility(false); @@ -340,7 +357,7 @@ public class CompatUIControllerTest extends ShellTestCase { false); // Verify button is shown after keyguard becomes not showing. - mController.onKeyguardShowingChanged(false); + mController.onKeyguardVisibilityChanged(false, false, false); verify(mMockCompatLayout).updateVisibility(true); verify(mMockLetterboxEduLayout).updateVisibility(true); @@ -352,7 +369,7 @@ public class CompatUIControllerTest extends ShellTestCase { /* hasSizeCompat= */ true, CAMERA_COMPAT_CONTROL_HIDDEN), mMockTaskListener); mController.onImeVisibilityChanged(DISPLAY_ID, /* isShowing= */ true); - mController.onKeyguardShowingChanged(true); + mController.onKeyguardVisibilityChanged(true, false, false); verify(mMockCompatLayout, times(2)).updateVisibility(false); verify(mMockLetterboxEduLayout, times(2)).updateVisibility(false); @@ -360,7 +377,7 @@ public class CompatUIControllerTest extends ShellTestCase { clearInvocations(mMockCompatLayout, mMockLetterboxEduLayout); // Verify button remains hidden after keyguard becomes not showing since IME is showing. - mController.onKeyguardShowingChanged(false); + mController.onKeyguardVisibilityChanged(false, false, false); verify(mMockCompatLayout).updateVisibility(false); verify(mMockLetterboxEduLayout).updateVisibility(false); @@ -378,7 +395,7 @@ public class CompatUIControllerTest extends ShellTestCase { /* hasSizeCompat= */ true, CAMERA_COMPAT_CONTROL_HIDDEN), mMockTaskListener); mController.onImeVisibilityChanged(DISPLAY_ID, /* isShowing= */ true); - mController.onKeyguardShowingChanged(true); + mController.onKeyguardVisibilityChanged(true, false, false); verify(mMockCompatLayout, times(2)).updateVisibility(false); verify(mMockLetterboxEduLayout, times(2)).updateVisibility(false); @@ -392,7 +409,7 @@ public class CompatUIControllerTest extends ShellTestCase { verify(mMockLetterboxEduLayout).updateVisibility(false); // Verify button is shown after keyguard becomes not showing. - mController.onKeyguardShowingChanged(false); + mController.onKeyguardVisibilityChanged(false, false, false); verify(mMockCompatLayout).updateVisibility(true); verify(mMockLetterboxEduLayout).updateVisibility(true); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java new file mode 100644 index 000000000000..dd23d97d9199 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java @@ -0,0 +1,261 @@ +/* + * 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.desktopmode; + +import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; +import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; +import static android.app.WindowConfiguration.WINDOW_CONFIG_BOUNDS; +import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REORDER; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +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.when; + +import android.app.ActivityManager; +import android.os.Handler; +import android.os.IBinder; +import android.testing.AndroidTestingRunner; +import android.window.DisplayAreaInfo; +import android.window.WindowContainerToken; +import android.window.WindowContainerTransaction; +import android.window.WindowContainerTransaction.Change; + +import androidx.test.filters.SmallTest; + +import com.android.dx.mockito.inline.extended.StaticMockitoSession; +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.common.ShellExecutor; +import com.android.wm.shell.sysui.ShellInit; +import com.android.wm.shell.transition.Transitions; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.Mockito; + +@SmallTest +@RunWith(AndroidTestingRunner.class) +public class DesktopModeControllerTest extends ShellTestCase { + + @Mock + private ShellTaskOrganizer mShellTaskOrganizer; + @Mock + private RootTaskDisplayAreaOrganizer mRootTaskDisplayAreaOrganizer; + @Mock + private ShellExecutor mTestExecutor; + @Mock + private Handler mMockHandler; + @Mock + private Transitions mMockTransitions; + private TestShellExecutor mExecutor; + + private DesktopModeController mController; + private DesktopModeTaskRepository mDesktopModeTaskRepository; + private ShellInit mShellInit; + private StaticMockitoSession mMockitoSession; + + @Before + public void setUp() { + mMockitoSession = mockitoSession().mockStatic(DesktopModeStatus.class).startMocking(); + when(DesktopModeStatus.isActive(any())).thenReturn(true); + + mShellInit = Mockito.spy(new ShellInit(mTestExecutor)); + mExecutor = new TestShellExecutor(); + + mDesktopModeTaskRepository = new DesktopModeTaskRepository(); + + mController = new DesktopModeController(mContext, mShellInit, mShellTaskOrganizer, + mRootTaskDisplayAreaOrganizer, mMockTransitions, + mDesktopModeTaskRepository, mMockHandler, mExecutor); + + when(mShellTaskOrganizer.prepareClearFreeformForStandardTasks(anyInt())).thenReturn( + new WindowContainerTransaction()); + + mShellInit.init(); + clearInvocations(mShellTaskOrganizer); + clearInvocations(mRootTaskDisplayAreaOrganizer); + } + + @After + public void tearDown() { + mMockitoSession.finishMocking(); + } + + @Test + public void instantiate_addInitCallback() { + verify(mShellInit, times(1)).addInitCallback(any(), any()); + } + + @Test + public void testDesktopModeEnabled_taskWmClearedDisplaySetToFreeform() { + // Create a fake WCT to simulate setting task windowing mode to undefined + WindowContainerTransaction taskWct = new WindowContainerTransaction(); + MockToken taskMockToken = new MockToken(); + taskWct.setWindowingMode(taskMockToken.token(), WINDOWING_MODE_UNDEFINED); + when(mShellTaskOrganizer.prepareClearFreeformForStandardTasks( + mContext.getDisplayId())).thenReturn(taskWct); + + // Create a fake DisplayAreaInfo to check if windowing mode change is set correctly + MockToken displayMockToken = new MockToken(); + DisplayAreaInfo displayAreaInfo = new DisplayAreaInfo(displayMockToken.mToken, + mContext.getDisplayId(), 0); + when(mRootTaskDisplayAreaOrganizer.getDisplayAreaInfo(mContext.getDisplayId())) + .thenReturn(displayAreaInfo); + + // The test + mController.updateDesktopModeActive(true); + + ArgumentCaptor<WindowContainerTransaction> arg = ArgumentCaptor.forClass( + WindowContainerTransaction.class); + verify(mRootTaskDisplayAreaOrganizer).applyTransaction(arg.capture()); + + // WCT should have 2 changes - clear task wm mode and set display wm mode + WindowContainerTransaction wct = arg.getValue(); + assertThat(wct.getChanges()).hasSize(2); + + // Verify executed WCT has a change for setting task windowing mode to undefined + Change taskWmModeChange = wct.getChanges().get(taskMockToken.binder()); + assertThat(taskWmModeChange).isNotNull(); + assertThat(taskWmModeChange.getWindowingMode()).isEqualTo(WINDOWING_MODE_UNDEFINED); + + // Verify executed WCT has a change for setting display windowing mode to freeform + Change displayWmModeChange = wct.getChanges().get(displayAreaInfo.token.asBinder()); + assertThat(displayWmModeChange).isNotNull(); + assertThat(displayWmModeChange.getWindowingMode()).isEqualTo(WINDOWING_MODE_FREEFORM); + } + + @Test + public void testDesktopModeDisabled_taskWmAndBoundsClearedDisplaySetToFullscreen() { + // Create a fake WCT to simulate setting task windowing mode to undefined + WindowContainerTransaction taskWmWct = new WindowContainerTransaction(); + MockToken taskWmMockToken = new MockToken(); + taskWmWct.setWindowingMode(taskWmMockToken.token(), WINDOWING_MODE_UNDEFINED); + when(mShellTaskOrganizer.prepareClearFreeformForStandardTasks( + mContext.getDisplayId())).thenReturn(taskWmWct); + + // Create a fake WCT to simulate clearing task bounds + WindowContainerTransaction taskBoundsWct = new WindowContainerTransaction(); + MockToken taskBoundsMockToken = new MockToken(); + taskBoundsWct.setBounds(taskBoundsMockToken.token(), null); + when(mShellTaskOrganizer.prepareClearBoundsForStandardTasks( + mContext.getDisplayId())).thenReturn(taskBoundsWct); + + // Create a fake DisplayAreaInfo to check if windowing mode change is set correctly + MockToken displayMockToken = new MockToken(); + DisplayAreaInfo displayAreaInfo = new DisplayAreaInfo(displayMockToken.mToken, + mContext.getDisplayId(), 0); + when(mRootTaskDisplayAreaOrganizer.getDisplayAreaInfo(mContext.getDisplayId())) + .thenReturn(displayAreaInfo); + + // The test + mController.updateDesktopModeActive(false); + + ArgumentCaptor<WindowContainerTransaction> arg = ArgumentCaptor.forClass( + WindowContainerTransaction.class); + verify(mRootTaskDisplayAreaOrganizer).applyTransaction(arg.capture()); + + // WCT should have 3 changes - clear task wm mode and bounds and set display wm mode + WindowContainerTransaction wct = arg.getValue(); + assertThat(wct.getChanges()).hasSize(3); + + // Verify executed WCT has a change for setting task windowing mode to undefined + Change taskWmMode = wct.getChanges().get(taskWmMockToken.binder()); + assertThat(taskWmMode).isNotNull(); + assertThat(taskWmMode.getWindowingMode()).isEqualTo(WINDOWING_MODE_UNDEFINED); + + // Verify executed WCT has a change for clearing task bounds + Change bounds = wct.getChanges().get(taskBoundsMockToken.binder()); + assertThat(bounds).isNotNull(); + assertThat(bounds.getWindowSetMask() & WINDOW_CONFIG_BOUNDS).isNotEqualTo(0); + assertThat(bounds.getConfiguration().windowConfiguration.getBounds().isEmpty()).isTrue(); + + // Verify executed WCT has a change for setting display windowing mode to fullscreen + Change displayWmModeChange = wct.getChanges().get(displayAreaInfo.token.asBinder()); + assertThat(displayWmModeChange).isNotNull(); + assertThat(displayWmModeChange.getWindowingMode()).isEqualTo(WINDOWING_MODE_FULLSCREEN); + } + + @Test + public void testShowDesktopApps() { + // Set up two active tasks on desktop + mDesktopModeTaskRepository.addActiveTask(1); + mDesktopModeTaskRepository.addActiveTask(2); + MockToken token1 = new MockToken(); + MockToken token2 = new MockToken(); + ActivityManager.RunningTaskInfo taskInfo1 = new TestRunningTaskInfoBuilder().setToken( + token1.token()).setLastActiveTime(100).build(); + ActivityManager.RunningTaskInfo taskInfo2 = new TestRunningTaskInfoBuilder().setToken( + token2.token()).setLastActiveTime(200).build(); + when(mShellTaskOrganizer.getRunningTaskInfo(1)).thenReturn(taskInfo1); + when(mShellTaskOrganizer.getRunningTaskInfo(2)).thenReturn(taskInfo2); + + // Run show desktop apps logic + mController.showDesktopApps(); + ArgumentCaptor<WindowContainerTransaction> wctCaptor = ArgumentCaptor.forClass( + WindowContainerTransaction.class); + verify(mShellTaskOrganizer).applyTransaction(wctCaptor.capture()); + WindowContainerTransaction wct = wctCaptor.getValue(); + + // Check wct has reorder calls + assertThat(wct.getHierarchyOps()).hasSize(2); + + // Task 2 has activity later, must be first + WindowContainerTransaction.HierarchyOp op1 = wct.getHierarchyOps().get(0); + assertThat(op1.getType()).isEqualTo(HIERARCHY_OP_TYPE_REORDER); + assertThat(op1.getContainer()).isEqualTo(token2.binder()); + + // Task 1 should be second + WindowContainerTransaction.HierarchyOp op2 = wct.getHierarchyOps().get(0); + assertThat(op2.getType()).isEqualTo(HIERARCHY_OP_TYPE_REORDER); + assertThat(op2.getContainer()).isEqualTo(token2.binder()); + } + + private static class MockToken { + private final WindowContainerToken mToken; + private final IBinder mBinder; + + MockToken() { + mToken = mock(WindowContainerToken.class); + mBinder = mock(IBinder.class); + when(mToken.asBinder()).thenReturn(mBinder); + } + + WindowContainerToken token() { + return mToken; + } + + IBinder binder() { + return mBinder; + } + } +} 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 new file mode 100644 index 000000000000..9b28d11f6a9d --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt @@ -0,0 +1,99 @@ +/* + * 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.desktopmode + +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.android.wm.shell.ShellTestCase +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 DesktopModeTaskRepositoryTest : ShellTestCase() { + + private lateinit var repo: DesktopModeTaskRepository + + @Before + fun setUp() { + repo = DesktopModeTaskRepository() + } + + @Test + fun addActiveTask_listenerNotifiedAndTaskIsActive() { + val listener = TestListener() + repo.addListener(listener) + + repo.addActiveTask(1) + assertThat(listener.activeTaskChangedCalls).isEqualTo(1) + assertThat(repo.isActiveTask(1)).isTrue() + } + + @Test + fun addActiveTask_sameTaskDoesNotNotify() { + val listener = TestListener() + repo.addListener(listener) + + repo.addActiveTask(1) + repo.addActiveTask(1) + assertThat(listener.activeTaskChangedCalls).isEqualTo(1) + } + + @Test + fun addActiveTask_multipleTasksAddedNotifiesForEach() { + val listener = TestListener() + repo.addListener(listener) + + repo.addActiveTask(1) + repo.addActiveTask(2) + assertThat(listener.activeTaskChangedCalls).isEqualTo(2) + } + + @Test + fun removeActiveTask_listenerNotifiedAndTaskNotActive() { + val listener = TestListener() + repo.addListener(listener) + + repo.addActiveTask(1) + repo.removeActiveTask(1) + // Notify once for add and once for remove + assertThat(listener.activeTaskChangedCalls).isEqualTo(2) + assertThat(repo.isActiveTask(1)).isFalse() + } + + @Test + fun removeActiveTask_removeNotExistingTaskDoesNotNotify() { + val listener = TestListener() + repo.addListener(listener) + repo.removeActiveTask(99) + assertThat(listener.activeTaskChangedCalls).isEqualTo(0) + } + + @Test + fun isActiveTask_notExistingTaskReturnsFalse() { + assertThat(repo.isActiveTask(99)).isFalse() + } + + class TestListener : DesktopModeTaskRepository.Listener { + var activeTaskChangedCalls = 0 + override fun onActiveTasksChanged() { + activeTaskChangedCalls++ + } + } +} 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 aaeebef03d0f..b6dbcf204364 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 @@ -21,6 +21,7 @@ import static android.view.Display.DEFAULT_DISPLAY; import static android.view.DragEvent.ACTION_DRAG_STARTED; import static org.junit.Assert.assertFalse; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -44,9 +45,12 @@ import androidx.test.filters.SmallTest; import com.android.internal.logging.UiEventLogger; import com.android.launcher3.icons.IconProvider; +import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.splitscreen.SplitScreenController; +import com.android.wm.shell.sysui.ShellController; +import com.android.wm.shell.sysui.ShellInit; import org.junit.Before; import org.junit.Test; @@ -54,35 +58,50 @@ import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; -import java.util.Optional; - /** * Tests for the drag and drop controller. */ @SmallTest @RunWith(AndroidJUnit4.class) -public class DragAndDropControllerTest { +public class DragAndDropControllerTest extends ShellTestCase { @Mock private Context mContext; - + @Mock + private ShellInit mShellInit; + @Mock + private ShellController mShellController; @Mock private DisplayController mDisplayController; - @Mock private UiEventLogger mUiEventLogger; - @Mock private DragAndDropController.DragAndDropListener mDragAndDropListener; + @Mock + private IconProvider mIconProvider; + @Mock + private ShellExecutor mMainExecutor; + @Mock + private SplitScreenController mSplitScreenController; private DragAndDropController mController; @Before public void setUp() throws RemoteException { MockitoAnnotations.initMocks(this); - mController = new DragAndDropController(mContext, mDisplayController, mUiEventLogger, - mock(IconProvider.class), mock(ShellExecutor.class)); - mController.initialize(Optional.of(mock(SplitScreenController.class))); + mController = new DragAndDropController(mContext, mShellInit, mShellController, + mDisplayController, mUiEventLogger, mIconProvider, mMainExecutor); + mController.onInit(); + } + + @Test + public void instantiateController_addInitCallback() { + verify(mShellInit, times(1)).addInitCallback(any(), any()); + } + + @Test + public void instantiateController_registerConfigChangeListener() { + verify(mShellController, times(1)).addConfigurationChangeListener(any()); } @Test diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropPolicyTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropPolicyTest.java index bb6026c36c97..9e988e8e8726 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropPolicyTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropPolicyTest.java @@ -34,7 +34,6 @@ import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_SPL 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 junit.framework.Assert.assertNull; import static junit.framework.Assert.assertTrue; import static junit.framework.Assert.fail; @@ -57,7 +56,6 @@ import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.pm.ActivityInfo; -import android.content.pm.ResolveInfo; import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Insets; @@ -68,6 +66,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; 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.splitscreen.SplitScreenController; @@ -87,7 +86,7 @@ import java.util.HashSet; */ @SmallTest @RunWith(AndroidJUnit4.class) -public class DragAndDropPolicyTest { +public class DragAndDropPolicyTest extends ShellTestCase { @Mock private Context mContext; @@ -182,8 +181,10 @@ public class DragAndDropPolicyTest { info.configuration.windowConfiguration.setActivityType(actType); info.configuration.windowConfiguration.setWindowingMode(winMode); info.isResizeable = true; - info.baseActivity = new ComponentName(getInstrumentation().getContext().getPackageName(), + info.baseActivity = new ComponentName(getInstrumentation().getContext(), ".ActivityWithMode" + winMode); + info.baseIntent = new Intent(); + info.baseIntent.setComponent(info.baseActivity); ActivityInfo activityInfo = new ActivityInfo(); activityInfo.packageName = info.baseActivity.getPackageName(); activityInfo.name = info.baseActivity.getClassName(); @@ -263,62 +264,6 @@ public class DragAndDropPolicyTest { } } - @Test - public void testLaunchMultipleTask_differentActivity() { - setRunningTask(mFullscreenAppTask); - mPolicy.start(mLandscapeDisplayLayout, mActivityClipData, mLoggerSessionId); - Intent fillInIntent = mPolicy.getStartIntentFillInIntent(mock(PendingIntent.class), 0); - assertNull(fillInIntent); - } - - @Test - public void testLaunchMultipleTask_differentActivity_inSplitscreen() { - setRunningTask(mFullscreenAppTask); - doReturn(true).when(mSplitScreenStarter).isSplitScreenVisible(); - doReturn(mFullscreenAppTask).when(mSplitScreenStarter).getTaskInfo(anyInt()); - mPolicy.start(mLandscapeDisplayLayout, mActivityClipData, mLoggerSessionId); - Intent fillInIntent = mPolicy.getStartIntentFillInIntent(mock(PendingIntent.class), 0); - assertNull(fillInIntent); - } - - @Test - public void testLaunchMultipleTask_sameActivity() { - setRunningTask(mFullscreenAppTask); - - // Replace the mocked drag pending intent and ensure it resolves to the same activity - PendingIntent launchIntent = mock(PendingIntent.class); - ResolveInfo launchInfo = new ResolveInfo(); - launchInfo.activityInfo = mFullscreenAppTask.topActivityInfo; - doReturn(Collections.singletonList(launchInfo)) - .when(launchIntent).queryIntentComponents(anyInt()); - mActivityClipData.getItemAt(0).getIntent().putExtra(ClipDescription.EXTRA_PENDING_INTENT, - launchIntent); - - mPolicy.start(mLandscapeDisplayLayout, mActivityClipData, mLoggerSessionId); - Intent fillInIntent = mPolicy.getStartIntentFillInIntent(launchIntent, 0); - assertTrue((fillInIntent.getFlags() & Intent.FLAG_ACTIVITY_MULTIPLE_TASK) != 0); - } - - @Test - public void testLaunchMultipleTask_sameActivity_inSplitScreen() { - setRunningTask(mFullscreenAppTask); - - // Replace the mocked drag pending intent and ensure it resolves to the same activity - PendingIntent launchIntent = mock(PendingIntent.class); - ResolveInfo launchInfo = new ResolveInfo(); - launchInfo.activityInfo = mFullscreenAppTask.topActivityInfo; - doReturn(Collections.singletonList(launchInfo)) - .when(launchIntent).queryIntentComponents(anyInt()); - mActivityClipData.getItemAt(0).getIntent().putExtra(ClipDescription.EXTRA_PENDING_INTENT, - launchIntent); - - doReturn(true).when(mSplitScreenStarter).isSplitScreenVisible(); - doReturn(mFullscreenAppTask).when(mSplitScreenStarter).getTaskInfo(anyInt()); - mPolicy.start(mLandscapeDisplayLayout, mActivityClipData, mLoggerSessionId); - Intent fillInIntent = mPolicy.getStartIntentFillInIntent(launchIntent, 0); - assertTrue((fillInIntent.getFlags() & Intent.FLAG_ACTIVITY_MULTIPLE_TASK) != 0); - } - private Target filterTargetByType(ArrayList<Target> targets, int type) { for (Target t : targets) { if (type == t.type) { diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/floating/FloatingTasksControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/floating/FloatingTasksControllerTest.java new file mode 100644 index 000000000000..a88c83779f25 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/floating/FloatingTasksControllerTest.java @@ -0,0 +1,248 @@ +/* + * 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.floating; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; +import static com.android.wm.shell.floating.FloatingTasksController.SMALLEST_SCREEN_WIDTH_DP_TO_BE_TABLET; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +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 static org.mockito.Mockito.when; + +import android.content.Intent; +import android.content.res.Configuration; +import android.graphics.Insets; +import android.graphics.Rect; +import android.os.RemoteException; +import android.os.SystemProperties; +import android.view.WindowInsets; +import android.view.WindowManager; +import android.view.WindowMetrics; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.ShellTestCase; +import com.android.wm.shell.TaskViewTransitions; +import com.android.wm.shell.TestShellExecutor; +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.SyncTransactionQueue; +import com.android.wm.shell.floating.views.FloatingTaskLayer; +import com.android.wm.shell.sysui.ShellCommandHandler; +import com.android.wm.shell.sysui.ShellController; +import com.android.wm.shell.sysui.ShellInit; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.Optional; + +/** + * Tests for the floating tasks controller. + */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class FloatingTasksControllerTest extends ShellTestCase { + // Some behavior in the controller constructor is dependent on this so we can only + // validate if it's working for the real value for those things. + private static final boolean FLOATING_TASKS_ACTUALLY_ENABLED = + SystemProperties.getBoolean("persist.wm.debug.floating_tasks", false); + + @Mock private ShellInit mShellInit; + @Mock private ShellController mShellController; + @Mock private WindowManager mWindowManager; + @Mock private ShellTaskOrganizer mTaskOrganizer; + @Captor private ArgumentCaptor<FloatingTaskLayer> mFloatingTaskLayerCaptor; + + private FloatingTasksController mController; + + @Before + public void setUp() throws RemoteException { + MockitoAnnotations.initMocks(this); + + WindowMetrics windowMetrics = mock(WindowMetrics.class); + WindowInsets windowInsets = mock(WindowInsets.class); + Insets insets = Insets.of(0, 0, 0, 0); + when(mWindowManager.getCurrentWindowMetrics()).thenReturn(windowMetrics); + when(windowMetrics.getWindowInsets()).thenReturn(windowInsets); + when(windowMetrics.getBounds()).thenReturn(new Rect(0, 0, 1000, 1000)); + when(windowInsets.getInsetsIgnoringVisibility(anyInt())).thenReturn(insets); + + // For the purposes of this test, just run everything synchronously + ShellExecutor shellExecutor = new TestShellExecutor(); + when(mTaskOrganizer.getExecutor()).thenReturn(shellExecutor); + } + + @After + public void tearDown() { + if (mController != null) { + mController.removeTask(); + mController = null; + } + } + + private void setUpTabletConfig() { + Configuration config = mock(Configuration.class); + config.smallestScreenWidthDp = SMALLEST_SCREEN_WIDTH_DP_TO_BE_TABLET; + mController.setConfig(config); + } + + private void setUpPhoneConfig() { + Configuration config = mock(Configuration.class); + config.smallestScreenWidthDp = SMALLEST_SCREEN_WIDTH_DP_TO_BE_TABLET - 1; + mController.setConfig(config); + } + + private void createController() { + mController = new FloatingTasksController(mContext, + mShellInit, + mShellController, + mock(ShellCommandHandler.class), + Optional.empty(), + mWindowManager, + mTaskOrganizer, + mock(TaskViewTransitions.class), + mock(ShellExecutor.class), + mock(ShellExecutor.class), + mock(SyncTransactionQueue.class)); + spyOn(mController); + } + + // + // Shell specific + // + @Test + public void instantiateController_addInitCallback() { + if (FLOATING_TASKS_ACTUALLY_ENABLED) { + createController(); + setUpTabletConfig(); + + verify(mShellInit, times(1)).addInitCallback(any(), any()); + } + } + + @Test + public void instantiateController_doesntAddInitCallback() { + if (!FLOATING_TASKS_ACTUALLY_ENABLED) { + createController(); + + verify(mShellInit, never()).addInitCallback(any(), any()); + } + } + + @Test + public void onInit_registerConfigChangeListener() { + if (FLOATING_TASKS_ACTUALLY_ENABLED) { + createController(); + setUpTabletConfig(); + mController.onInit(); + + verify(mShellController, times(1)).addConfigurationChangeListener(any()); + } + } + + // + // Tests for floating layer, which is only available for tablets. + // + + @Test + public void testIsFloatingLayerAvailable_true() { + createController(); + setUpTabletConfig(); + assertThat(mController.isFloatingLayerAvailable()).isTrue(); + } + + @Test + public void testIsFloatingLayerAvailable_false() { + createController(); + setUpPhoneConfig(); + assertThat(mController.isFloatingLayerAvailable()).isFalse(); + } + + // + // Tests for floating tasks being enabled, guarded by sysprop flag. + // + + @Test + public void testIsFloatingTasksEnabled_true() { + createController(); + mController.setFloatingTasksEnabled(true); + setUpTabletConfig(); + assertThat(mController.isFloatingTasksEnabled()).isTrue(); + } + + @Test + public void testIsFloatingTasksEnabled_false() { + createController(); + mController.setFloatingTasksEnabled(false); + setUpTabletConfig(); + assertThat(mController.isFloatingTasksEnabled()).isFalse(); + } + + // + // Tests for behavior depending on flags + // + + @Test + public void testShowTaskIntent_enabled() { + createController(); + mController.setFloatingTasksEnabled(true); + setUpTabletConfig(); + + mController.showTask(mock(Intent.class)); + verify(mWindowManager).addView(mFloatingTaskLayerCaptor.capture(), any()); + assertThat(mFloatingTaskLayerCaptor.getValue().getTaskViewCount()).isEqualTo(1); + } + + @Test + public void testShowTaskIntent_notEnabled() { + createController(); + mController.setFloatingTasksEnabled(false); + setUpTabletConfig(); + + mController.showTask(mock(Intent.class)); + verify(mWindowManager, never()).addView(any(), any()); + } + + @Test + public void testRemoveTask() { + createController(); + mController.setFloatingTasksEnabled(true); + setUpTabletConfig(); + + mController.showTask(mock(Intent.class)); + verify(mWindowManager).addView(mFloatingTaskLayerCaptor.capture(), any()); + assertThat(mFloatingTaskLayerCaptor.getValue().getTaskViewCount()).isEqualTo(1); + + mController.removeTask(); + verify(mWindowManager).removeView(mFloatingTaskLayerCaptor.capture()); + assertThat(mFloatingTaskLayerCaptor.getValue().getTaskViewCount()).isEqualTo(0); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserverTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserverTest.java new file mode 100644 index 000000000000..0fd5cb081ea9 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserverTest.java @@ -0,0 +1,302 @@ +/* + * 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.freeform; + +import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; +import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static android.view.WindowManager.TRANSIT_CHANGE; +import static android.view.WindowManager.TRANSIT_CLOSE; +import static android.view.WindowManager.TRANSIT_OPEN; + +import static com.android.wm.shell.transition.Transitions.TRANSIT_MAXIMIZE; +import static com.android.wm.shell.transition.Transitions.TRANSIT_RESTORE_FROM_MAXIMIZE; + +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.same; +import static org.mockito.Mockito.verify; + +import android.app.ActivityManager; +import android.content.Context; +import android.content.pm.PackageManager; +import android.os.IBinder; +import android.view.SurfaceControl; +import android.window.IWindowContainerToken; +import android.window.TransitionInfo; +import android.window.WindowContainerToken; + +import androidx.test.filters.SmallTest; + +import com.android.wm.shell.fullscreen.FullscreenTaskListener; +import com.android.wm.shell.sysui.ShellInit; +import com.android.wm.shell.transition.Transitions; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** + * Tests of {@link FreeformTaskTransitionObserver} + */ +@SmallTest +public class FreeformTaskTransitionObserverTest { + + @Mock + private ShellInit mShellInit; + @Mock + private Transitions mTransitions; + @Mock + private FullscreenTaskListener<?> mFullscreenTaskListener; + @Mock + private FreeformTaskListener<?> mFreeformTaskListener; + + private FreeformTaskTransitionObserver mTransitionObserver; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + PackageManager pm = mock(PackageManager.class); + doReturn(true).when(pm).hasSystemFeature( + PackageManager.FEATURE_FREEFORM_WINDOW_MANAGEMENT); + final Context context = mock(Context.class); + doReturn(pm).when(context).getPackageManager(); + + mTransitionObserver = new FreeformTaskTransitionObserver( + context, mShellInit, mTransitions, mFullscreenTaskListener, mFreeformTaskListener); + if (Transitions.ENABLE_SHELL_TRANSITIONS) { + final ArgumentCaptor<Runnable> initRunnableCaptor = ArgumentCaptor.forClass( + Runnable.class); + verify(mShellInit).addInitCallback(initRunnableCaptor.capture(), + same(mTransitionObserver)); + initRunnableCaptor.getValue().run(); + } else { + mTransitionObserver.onInit(); + } + } + + @Test + public void testRegistersObserverAtInit() { + verify(mTransitions).registerObserver(same(mTransitionObserver)); + } + + @Test + public void testCreatesWindowDecorOnOpenTransition_freeform() { + final TransitionInfo.Change change = + createChange(TRANSIT_OPEN, 1, WINDOWING_MODE_FREEFORM); + final TransitionInfo info = new TransitionInfo(TRANSIT_OPEN, 0); + info.addChange(change); + + final IBinder transition = mock(IBinder.class); + final SurfaceControl.Transaction startT = mock(SurfaceControl.Transaction.class); + final SurfaceControl.Transaction finishT = mock(SurfaceControl.Transaction.class); + mTransitionObserver.onTransitionReady(transition, info, startT, finishT); + mTransitionObserver.onTransitionStarting(transition); + + verify(mFreeformTaskListener).createWindowDecoration(change, startT, finishT); + } + + @Test + public void testObtainsWindowDecorOnCloseTransition_freeform() { + final TransitionInfo.Change change = + createChange(TRANSIT_CLOSE, 1, WINDOWING_MODE_FREEFORM); + final TransitionInfo info = new TransitionInfo(TRANSIT_CLOSE, 0); + info.addChange(change); + + final IBinder transition = mock(IBinder.class); + final SurfaceControl.Transaction startT = mock(SurfaceControl.Transaction.class); + final SurfaceControl.Transaction finishT = mock(SurfaceControl.Transaction.class); + mTransitionObserver.onTransitionReady(transition, info, startT, finishT); + mTransitionObserver.onTransitionStarting(transition); + + verify(mFreeformTaskListener).giveWindowDecoration(change.getTaskInfo(), startT, finishT); + } + + @Test + public void testDoesntCloseWindowDecorDuringCloseTransition() throws Exception { + final TransitionInfo.Change change = + createChange(TRANSIT_CLOSE, 1, WINDOWING_MODE_FREEFORM); + final TransitionInfo info = new TransitionInfo(TRANSIT_CLOSE, 0); + info.addChange(change); + + final AutoCloseable windowDecor = mock(AutoCloseable.class); + doReturn(windowDecor).when(mFreeformTaskListener).giveWindowDecoration( + eq(change.getTaskInfo()), any(), any()); + + final IBinder transition = mock(IBinder.class); + final SurfaceControl.Transaction startT = mock(SurfaceControl.Transaction.class); + final SurfaceControl.Transaction finishT = mock(SurfaceControl.Transaction.class); + mTransitionObserver.onTransitionReady(transition, info, startT, finishT); + mTransitionObserver.onTransitionStarting(transition); + + verify(windowDecor, never()).close(); + } + + @Test + public void testClosesWindowDecorAfterCloseTransition() throws Exception { + final TransitionInfo.Change change = + createChange(TRANSIT_CLOSE, 1, WINDOWING_MODE_FREEFORM); + final TransitionInfo info = new TransitionInfo(TRANSIT_CLOSE, 0); + info.addChange(change); + + final AutoCloseable windowDecor = mock(AutoCloseable.class); + doReturn(windowDecor).when(mFreeformTaskListener).giveWindowDecoration( + eq(change.getTaskInfo()), any(), any()); + + final IBinder transition = mock(IBinder.class); + final SurfaceControl.Transaction startT = mock(SurfaceControl.Transaction.class); + final SurfaceControl.Transaction finishT = mock(SurfaceControl.Transaction.class); + mTransitionObserver.onTransitionReady(transition, info, startT, finishT); + mTransitionObserver.onTransitionStarting(transition); + mTransitionObserver.onTransitionFinished(transition, false); + + verify(windowDecor).close(); + } + + @Test + public void testClosesMergedWindowDecorationAfterTransitionFinishes() throws Exception { + // The playing transition + final TransitionInfo.Change change1 = + createChange(TRANSIT_OPEN, 1, WINDOWING_MODE_FREEFORM); + final TransitionInfo info1 = new TransitionInfo(TRANSIT_OPEN, 0); + info1.addChange(change1); + + final IBinder transition1 = mock(IBinder.class); + final SurfaceControl.Transaction startT1 = mock(SurfaceControl.Transaction.class); + final SurfaceControl.Transaction finishT1 = mock(SurfaceControl.Transaction.class); + mTransitionObserver.onTransitionReady(transition1, info1, startT1, finishT1); + mTransitionObserver.onTransitionStarting(transition1); + + // The merged transition + final TransitionInfo.Change change2 = + createChange(TRANSIT_CLOSE, 2, WINDOWING_MODE_FREEFORM); + final TransitionInfo info2 = new TransitionInfo(TRANSIT_CLOSE, 0); + info2.addChange(change2); + + final AutoCloseable windowDecor2 = mock(AutoCloseable.class); + doReturn(windowDecor2).when(mFreeformTaskListener).giveWindowDecoration( + eq(change2.getTaskInfo()), any(), any()); + + final IBinder transition2 = mock(IBinder.class); + final SurfaceControl.Transaction startT2 = mock(SurfaceControl.Transaction.class); + final SurfaceControl.Transaction finishT2 = mock(SurfaceControl.Transaction.class); + mTransitionObserver.onTransitionReady(transition2, info2, startT2, finishT2); + mTransitionObserver.onTransitionMerged(transition2, transition1); + + mTransitionObserver.onTransitionFinished(transition1, false); + + verify(windowDecor2).close(); + } + + @Test + public void testClosesAllWindowDecorsOnTransitionMergeAfterCloseTransitions() throws Exception { + // The playing transition + final TransitionInfo.Change change1 = + createChange(TRANSIT_CLOSE, 1, WINDOWING_MODE_FREEFORM); + final TransitionInfo info1 = new TransitionInfo(TRANSIT_CLOSE, 0); + info1.addChange(change1); + + final AutoCloseable windowDecor1 = mock(AutoCloseable.class); + doReturn(windowDecor1).when(mFreeformTaskListener).giveWindowDecoration( + eq(change1.getTaskInfo()), any(), any()); + + final IBinder transition1 = mock(IBinder.class); + final SurfaceControl.Transaction startT1 = mock(SurfaceControl.Transaction.class); + final SurfaceControl.Transaction finishT1 = mock(SurfaceControl.Transaction.class); + mTransitionObserver.onTransitionReady(transition1, info1, startT1, finishT1); + mTransitionObserver.onTransitionStarting(transition1); + + // The merged transition + final TransitionInfo.Change change2 = + createChange(TRANSIT_CLOSE, 2, WINDOWING_MODE_FREEFORM); + final TransitionInfo info2 = new TransitionInfo(TRANSIT_CLOSE, 0); + info2.addChange(change2); + + final AutoCloseable windowDecor2 = mock(AutoCloseable.class); + doReturn(windowDecor2).when(mFreeformTaskListener).giveWindowDecoration( + eq(change2.getTaskInfo()), any(), any()); + + final IBinder transition2 = mock(IBinder.class); + final SurfaceControl.Transaction startT2 = mock(SurfaceControl.Transaction.class); + final SurfaceControl.Transaction finishT2 = mock(SurfaceControl.Transaction.class); + mTransitionObserver.onTransitionReady(transition2, info2, startT2, finishT2); + mTransitionObserver.onTransitionMerged(transition2, transition1); + + mTransitionObserver.onTransitionFinished(transition1, false); + + verify(windowDecor1).close(); + verify(windowDecor2).close(); + } + + @Test + public void testTransfersWindowDecorOnMaximize() { + final TransitionInfo.Change change = + createChange(TRANSIT_CHANGE, 1, WINDOWING_MODE_FULLSCREEN); + final TransitionInfo info = new TransitionInfo(TRANSIT_MAXIMIZE, 0); + info.addChange(change); + + final AutoCloseable windowDecor = mock(AutoCloseable.class); + doReturn(windowDecor).when(mFreeformTaskListener).giveWindowDecoration( + eq(change.getTaskInfo()), any(), any()); + + final IBinder transition = mock(IBinder.class); + final SurfaceControl.Transaction startT = mock(SurfaceControl.Transaction.class); + final SurfaceControl.Transaction finishT = mock(SurfaceControl.Transaction.class); + mTransitionObserver.onTransitionReady(transition, info, startT, finishT); + mTransitionObserver.onTransitionStarting(transition); + + verify(mFreeformTaskListener).giveWindowDecoration(change.getTaskInfo(), startT, finishT); + verify(mFullscreenTaskListener).adoptWindowDecoration( + eq(change), same(startT), same(finishT), any()); + } + + @Test + public void testTransfersWindowDecorOnRestoreFromMaximize() { + final TransitionInfo.Change change = + createChange(TRANSIT_CHANGE, 1, WINDOWING_MODE_FREEFORM); + final TransitionInfo info = new TransitionInfo(TRANSIT_RESTORE_FROM_MAXIMIZE, 0); + info.addChange(change); + + final IBinder transition = mock(IBinder.class); + final SurfaceControl.Transaction startT = mock(SurfaceControl.Transaction.class); + final SurfaceControl.Transaction finishT = mock(SurfaceControl.Transaction.class); + mTransitionObserver.onTransitionReady(transition, info, startT, finishT); + mTransitionObserver.onTransitionStarting(transition); + + verify(mFullscreenTaskListener).giveWindowDecoration(change.getTaskInfo(), startT, finishT); + verify(mFreeformTaskListener).adoptWindowDecoration( + eq(change), same(startT), same(finishT), any()); + } + + private static TransitionInfo.Change createChange(int mode, int taskId, int windowingMode) { + final ActivityManager.RunningTaskInfo taskInfo = new ActivityManager.RunningTaskInfo(); + taskInfo.taskId = taskId; + taskInfo.configuration.windowConfiguration.setWindowingMode(windowingMode); + + final TransitionInfo.Change change = new TransitionInfo.Change( + new WindowContainerToken(mock(IWindowContainerToken.class)), + mock(SurfaceControl.class)); + change.setMode(mode); + change.setTaskInfo(taskInfo); + return change; + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/fullscreen/FullscreenTaskListenerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/fullscreen/FullscreenTaskListenerTest.java deleted file mode 100644 index 4523e2c9cba5..000000000000 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/fullscreen/FullscreenTaskListenerTest.java +++ /dev/null @@ -1,166 +0,0 @@ -/* - * Copyright (C) 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.wm.shell.fullscreen; - -import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; - -import static org.junit.Assume.assumeFalse; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.inOrder; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; -import static org.mockito.Mockito.when; - -import android.app.ActivityManager.RunningTaskInfo; -import android.app.WindowConfiguration; -import android.content.res.Configuration; -import android.graphics.Point; -import android.os.SystemProperties; -import android.view.SurfaceControl; - -import androidx.test.filters.SmallTest; - -import com.android.wm.shell.common.SyncTransactionQueue; -import com.android.wm.shell.recents.RecentTasksController; - -import org.junit.Before; -import org.junit.Test; -import org.mockito.InOrder; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -import java.util.Optional; - -@SmallTest -public class FullscreenTaskListenerTest { - private static final boolean ENABLE_SHELL_TRANSITIONS = - SystemProperties.getBoolean("persist.wm.debug.shell_transit", false); - - @Mock - private SyncTransactionQueue mSyncQueue; - @Mock - private FullscreenUnfoldController mUnfoldController; - @Mock - private RecentTasksController mRecentTasksController; - @Mock - private SurfaceControl mSurfaceControl; - - private Optional<FullscreenUnfoldController> mFullscreenUnfoldController; - - private FullscreenTaskListener mListener; - - @Before - public void setup() { - MockitoAnnotations.initMocks(this); - mFullscreenUnfoldController = Optional.of(mUnfoldController); - mListener = new FullscreenTaskListener(mSyncQueue, mFullscreenUnfoldController, - Optional.empty()); - } - - @Test - public void testAnimatableTaskAppeared_notifiesUnfoldController() { - assumeFalse(ENABLE_SHELL_TRANSITIONS); - RunningTaskInfo info = createTaskInfo(/* visible */ true, /* taskId */ 0); - - mListener.onTaskAppeared(info, mSurfaceControl); - - verify(mUnfoldController).onTaskAppeared(eq(info), any()); - } - - @Test - public void testMultipleAnimatableTasksAppeared_notifiesUnfoldController() { - assumeFalse(ENABLE_SHELL_TRANSITIONS); - RunningTaskInfo animatable1 = createTaskInfo(/* visible */ true, /* taskId */ 0); - RunningTaskInfo animatable2 = createTaskInfo(/* visible */ true, /* taskId */ 1); - - mListener.onTaskAppeared(animatable1, mSurfaceControl); - mListener.onTaskAppeared(animatable2, mSurfaceControl); - - InOrder order = inOrder(mUnfoldController); - order.verify(mUnfoldController).onTaskAppeared(eq(animatable1), any()); - order.verify(mUnfoldController).onTaskAppeared(eq(animatable2), any()); - } - - @Test - public void testNonAnimatableTaskAppeared_doesNotNotifyUnfoldController() { - assumeFalse(ENABLE_SHELL_TRANSITIONS); - RunningTaskInfo info = createTaskInfo(/* visible */ false, /* taskId */ 0); - - mListener.onTaskAppeared(info, mSurfaceControl); - - verifyNoMoreInteractions(mUnfoldController); - } - - @Test - public void testNonAnimatableTaskChanged_doesNotNotifyUnfoldController() { - assumeFalse(ENABLE_SHELL_TRANSITIONS); - RunningTaskInfo info = createTaskInfo(/* visible */ false, /* taskId */ 0); - mListener.onTaskAppeared(info, mSurfaceControl); - - mListener.onTaskInfoChanged(info); - - verifyNoMoreInteractions(mUnfoldController); - } - - @Test - public void testNonAnimatableTaskVanished_doesNotNotifyUnfoldController() { - assumeFalse(ENABLE_SHELL_TRANSITIONS); - RunningTaskInfo info = createTaskInfo(/* visible */ false, /* taskId */ 0); - mListener.onTaskAppeared(info, mSurfaceControl); - - mListener.onTaskVanished(info); - - verifyNoMoreInteractions(mUnfoldController); - } - - @Test - public void testAnimatableTaskBecameInactive_notifiesUnfoldController() { - assumeFalse(ENABLE_SHELL_TRANSITIONS); - RunningTaskInfo animatableTask = createTaskInfo(/* visible */ true, /* taskId */ 0); - mListener.onTaskAppeared(animatableTask, mSurfaceControl); - RunningTaskInfo notAnimatableTask = createTaskInfo(/* visible */ false, /* taskId */ 0); - - mListener.onTaskInfoChanged(notAnimatableTask); - - verify(mUnfoldController).onTaskVanished(eq(notAnimatableTask)); - } - - @Test - public void testAnimatableTaskVanished_notifiesUnfoldController() { - assumeFalse(ENABLE_SHELL_TRANSITIONS); - RunningTaskInfo taskInfo = createTaskInfo(/* visible */ true, /* taskId */ 0); - mListener.onTaskAppeared(taskInfo, mSurfaceControl); - - mListener.onTaskVanished(taskInfo); - - verify(mUnfoldController).onTaskVanished(eq(taskInfo)); - } - - private RunningTaskInfo createTaskInfo(boolean visible, int taskId) { - final RunningTaskInfo info = spy(new RunningTaskInfo()); - info.isVisible = visible; - info.positionInParent = new Point(); - when(info.getWindowingMode()).thenReturn(WindowConfiguration.WINDOWING_MODE_FULLSCREEN); - final Configuration configuration = new Configuration(); - configuration.windowConfiguration.setActivityType(ACTIVITY_TYPE_STANDARD); - when(info.getConfiguration()).thenReturn(configuration); - info.taskId = taskId; - return info; - } -} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutoutControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutoutControllerTest.java index b976c1287aca..6c301bbbc7f1 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutoutControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutoutControllerTest.java @@ -16,10 +16,13 @@ package com.android.wm.shell.hidedisplaycutout; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -import android.platform.test.annotations.Presubmit; import android.testing.AndroidTestingRunner; import android.testing.TestableContext; import android.testing.TestableLooper; @@ -27,7 +30,11 @@ import android.testing.TestableLooper; import androidx.test.filters.SmallTest; import androidx.test.platform.app.InstrumentationRegistry; +import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.sysui.ShellCommandHandler; +import com.android.wm.shell.sysui.ShellController; +import com.android.wm.shell.sysui.ShellInit; import org.junit.Before; import org.junit.Test; @@ -35,25 +42,45 @@ import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; -@Presubmit @SmallTest @RunWith(AndroidTestingRunner.class) @TestableLooper.RunWithLooper -public class HideDisplayCutoutControllerTest { +public class HideDisplayCutoutControllerTest extends ShellTestCase { private TestableContext mContext = new TestableContext( InstrumentationRegistry.getInstrumentation().getTargetContext(), null); - private HideDisplayCutoutController mHideDisplayCutoutController; @Mock - private HideDisplayCutoutOrganizer mMockDisplayAreaOrganizer; + private ShellCommandHandler mShellCommandHandler; @Mock - private ShellExecutor mMockMainExecutor; + private ShellController mShellController; + @Mock + private HideDisplayCutoutOrganizer mMockDisplayAreaOrganizer; + + private HideDisplayCutoutController mHideDisplayCutoutController; + private ShellInit mShellInit; @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); - mHideDisplayCutoutController = new HideDisplayCutoutController( - mContext, mMockDisplayAreaOrganizer, mMockMainExecutor); + mShellInit = spy(new ShellInit(mock(ShellExecutor.class))); + mHideDisplayCutoutController = new HideDisplayCutoutController(mContext, mShellInit, + mShellCommandHandler, mShellController, mMockDisplayAreaOrganizer); + mShellInit.init(); + } + + @Test + public void instantiateController_addInitCallback() { + verify(mShellInit, times(1)).addInitCallback(any(), any()); + } + + @Test + public void instantiateController_registerDumpCallback() { + verify(mShellCommandHandler, times(1)).addDumpCallback(any(), any()); + } + + @Test + public void instantiateController_registerConfigChangeListener() { + verify(mShellController, times(1)).addConfigurationChangeListener(any()); } @Test diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutoutOrganizerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutoutOrganizerTest.java index 16e92395c85e..49521cfbb34d 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutoutOrganizerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutoutOrganizerTest.java @@ -32,7 +32,6 @@ import android.content.res.Configuration; import android.graphics.Insets; import android.graphics.Rect; import android.os.Binder; -import android.platform.test.annotations.Presubmit; import android.testing.AndroidTestingRunner; import android.testing.TestableContext; import android.testing.TestableLooper; @@ -48,6 +47,7 @@ import android.window.WindowContainerToken; import androidx.test.filters.SmallTest; import androidx.test.platform.app.InstrumentationRegistry; +import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.ShellExecutor; @@ -61,11 +61,10 @@ import org.mockito.MockitoAnnotations; import java.util.ArrayList; -@Presubmit @SmallTest @RunWith(AndroidTestingRunner.class) @TestableLooper.RunWithLooper -public class HideDisplayCutoutOrganizerTest { +public class HideDisplayCutoutOrganizerTest extends ShellTestCase { private TestableContext mContext = new TestableContext( InstrumentationRegistry.getInstrumentation().getTargetContext(), null); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/kidsmode/KidsModeTaskOrganizerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/kidsmode/KidsModeTaskOrganizerTest.java index 440a6f8fb59a..ecfb427dbced 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/kidsmode/KidsModeTaskOrganizerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/kidsmode/KidsModeTaskOrganizerTest.java @@ -44,11 +44,13 @@ import android.window.WindowContainerTransaction; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; +import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayInsetsController; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; -import com.android.wm.shell.startingsurface.StartingWindowController; +import com.android.wm.shell.sysui.ShellCommandHandler; +import com.android.wm.shell.sysui.ShellInit; import org.junit.Before; import org.junit.Test; @@ -61,7 +63,7 @@ import java.util.Optional; @SmallTest @RunWith(AndroidJUnit4.class) -public class KidsModeTaskOrganizerTest { +public class KidsModeTaskOrganizerTest extends ShellTestCase { @Mock private ITaskOrganizerController mTaskOrganizerController; @Mock private Context mContext; @Mock private Handler mHandler; @@ -72,7 +74,8 @@ public class KidsModeTaskOrganizerTest { @Mock private WindowContainerToken mToken; @Mock private WindowContainerTransaction mTransaction; @Mock private KidsModeSettingsObserver mObserver; - @Mock private StartingWindowController mStartingWindowController; + @Mock private ShellInit mShellInit; + @Mock private ShellCommandHandler mShellCommandHandler; @Mock private DisplayInsetsController mDisplayInsetsController; KidsModeTaskOrganizer mOrganizer; @@ -86,15 +89,20 @@ public class KidsModeTaskOrganizerTest { } catch (RemoteException e) { } // NOTE: KidsModeTaskOrganizer should have a null CompatUIController. - mOrganizer = spy(new KidsModeTaskOrganizer(mTaskOrganizerController, mTestExecutor, - mHandler, mContext, mSyncTransactionQueue, mDisplayController, - mDisplayInsetsController, Optional.empty(), mObserver)); - mOrganizer.initialize(mStartingWindowController); + mOrganizer = spy(new KidsModeTaskOrganizer(mContext, mShellInit, mShellCommandHandler, + mTaskOrganizerController, mSyncTransactionQueue, mDisplayController, + mDisplayInsetsController, Optional.empty(), Optional.empty(), mObserver, + mTestExecutor, mHandler)); doReturn(mTransaction).when(mOrganizer).getWindowContainerTransaction(); doReturn(new InsetsState()).when(mDisplayController).getInsetsState(DEFAULT_DISPLAY); } @Test + public void instantiateController_addInitCallback() { + verify(mShellInit, times(1)).addInitCallback(any(), any()); + } + + @Test public void testKidsModeOn() { doReturn(true).when(mObserver).isEnabled(); 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 ecf1c5d41864..cf8297eec061 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedControllerTest.java @@ -30,27 +30,27 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import android.content.om.IOverlayManager; import android.graphics.Rect; import android.os.Handler; -import android.os.UserHandle; import android.testing.AndroidTestingRunner; import android.util.ArrayMap; import android.view.Display; import android.view.Surface; -import android.view.SurfaceControl; import android.window.WindowContainerTransaction; import androidx.test.filters.SmallTest; -import com.android.internal.jank.InteractionJankMonitor; 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.sysui.ShellCommandHandler; +import com.android.wm.shell.sysui.ShellController; +import com.android.wm.shell.sysui.ShellInit; import org.junit.Before; import org.junit.Test; @@ -62,16 +62,20 @@ import org.mockito.MockitoAnnotations; @SmallTest @RunWith(AndroidTestingRunner.class) public class OneHandedControllerTest extends OneHandedTestCase { - private int mCurrentUser = UserHandle.myUserId(); Display mDisplay; OneHandedAccessibilityUtil mOneHandedAccessibilityUtil; OneHandedController mSpiedOneHandedController; OneHandedTimeoutHandler mSpiedTimeoutHandler; OneHandedState mSpiedTransitionState; + ShellInit mShellInit; @Mock - DisplayLayout mDisplayLayout; + ShellCommandHandler mMockShellCommandHandler; + @Mock + ShellController mMockShellController; + @Mock + DisplayLayout mMockDisplayLayout; @Mock DisplayController mMockDisplayController; @Mock @@ -87,16 +91,10 @@ public class OneHandedControllerTest extends OneHandedTestCase { @Mock OneHandedUiEventLogger mMockUiEventLogger; @Mock - InteractionJankMonitor mMockJankMonitor; - @Mock - IOverlayManager mMockOverlayManager; - @Mock TaskStackListenerImpl mMockTaskStackListener; @Mock ShellExecutor mMockShellMainExecutor; @Mock - SurfaceControl mMockLeash; - @Mock Handler mMockShellMainHandler; final boolean mDefaultEnabled = true; @@ -107,7 +105,7 @@ public class OneHandedControllerTest extends OneHandedTestCase { public void setUp() { MockitoAnnotations.initMocks(this); mDisplay = mContext.getDisplay(); - mDisplayLayout = Mockito.mock(DisplayLayout.class); + mMockDisplayLayout = Mockito.mock(DisplayLayout.class); mSpiedTimeoutHandler = spy(new OneHandedTimeoutHandler(mMockShellMainExecutor)); mSpiedTransitionState = spy(new OneHandedState()); @@ -127,11 +125,15 @@ public class OneHandedControllerTest extends OneHandedTestCase { when(mMockDisplayAreaOrganizer.getLastDisplayBounds()).thenReturn( new Rect(0, 0, 1080, 2400)); - when(mMockDisplayAreaOrganizer.getDisplayLayout()).thenReturn(mDisplayLayout); + when(mMockDisplayAreaOrganizer.getDisplayLayout()).thenReturn(mMockDisplayLayout); + mShellInit = spy(new ShellInit(mMockShellMainExecutor)); mOneHandedAccessibilityUtil = new OneHandedAccessibilityUtil(mContext); mSpiedOneHandedController = spy(new OneHandedController( mContext, + mShellInit, + mMockShellCommandHandler, + mMockShellController, mMockDisplayController, mMockDisplayAreaOrganizer, mMockTouchHandler, @@ -140,13 +142,37 @@ public class OneHandedControllerTest extends OneHandedTestCase { mOneHandedAccessibilityUtil, mSpiedTimeoutHandler, mSpiedTransitionState, - mMockJankMonitor, mMockUiEventLogger, - mMockOverlayManager, mMockTaskStackListener, mMockShellMainExecutor, mMockShellMainHandler) ); + mShellInit.init(); + } + + @Test + public void instantiateController_addInitCallback() { + verify(mShellInit, times(1)).addInitCallback(any(), any()); + } + + @Test + public void instantiateController_registerDumpCallback() { + verify(mMockShellCommandHandler, times(1)).addDumpCallback(any(), any()); + } + + @Test + public void testControllerRegistersConfigChangeListener() { + verify(mMockShellController, times(1)).addConfigurationChangeListener(any()); + } + + @Test + public void testControllerRegistersKeyguardChangeListener() { + verify(mMockShellController, times(1)).addKeyguardChangeListener(any()); + } + + @Test + public void testControllerRegistersUserChangeListener() { + verify(mMockShellController, times(1)).addUserChangeListener(any()); } @Test @@ -304,9 +330,9 @@ public class OneHandedControllerTest extends OneHandedTestCase { @Test public void testRotation90CanNotStartOneHanded() { - mDisplayLayout.rotateTo(mContext.getResources(), Surface.ROTATION_90); + mMockDisplayLayout.rotateTo(mContext.getResources(), Surface.ROTATION_90); mSpiedTransitionState.setState(STATE_NONE); - when(mDisplayLayout.isLandscape()).thenReturn(true); + when(mMockDisplayLayout.isLandscape()).thenReturn(true); mSpiedOneHandedController.setOneHandedEnabled(true); mSpiedOneHandedController.setLockedDisabled(false /* locked */, false /* enabled */); mSpiedOneHandedController.startOneHanded(); @@ -316,10 +342,10 @@ public class OneHandedControllerTest extends OneHandedTestCase { @Test public void testRotation180CanStartOneHanded() { - mDisplayLayout.rotateTo(mContext.getResources(), Surface.ROTATION_180); + mMockDisplayLayout.rotateTo(mContext.getResources(), Surface.ROTATION_180); mSpiedTransitionState.setState(STATE_NONE); when(mMockDisplayAreaOrganizer.isReady()).thenReturn(true); - when(mDisplayLayout.isLandscape()).thenReturn(false); + when(mMockDisplayLayout.isLandscape()).thenReturn(false); mSpiedOneHandedController.setOneHandedEnabled(true); mSpiedOneHandedController.setLockedDisabled(false /* locked */, false /* enabled */); mSpiedOneHandedController.startOneHanded(); @@ -329,9 +355,9 @@ public class OneHandedControllerTest extends OneHandedTestCase { @Test public void testRotation270CanNotStartOneHanded() { - mDisplayLayout.rotateTo(mContext.getResources(), Surface.ROTATION_270); + mMockDisplayLayout.rotateTo(mContext.getResources(), Surface.ROTATION_270); mSpiedTransitionState.setState(STATE_NONE); - when(mDisplayLayout.isLandscape()).thenReturn(true); + when(mMockDisplayLayout.isLandscape()).thenReturn(true); mSpiedOneHandedController.setOneHandedEnabled(true); mSpiedOneHandedController.setLockedDisabled(false /* locked */, false /* enabled */); mSpiedOneHandedController.startOneHanded(); @@ -345,8 +371,8 @@ public class OneHandedControllerTest extends OneHandedTestCase { when(mMockSettingsUitl.getSettingsSwipeToNotificationEnabled(any(), anyInt())).thenReturn( false); final WindowContainerTransaction handlerWCT = new WindowContainerTransaction(); - mSpiedOneHandedController.onRotateDisplay(mDisplay.getDisplayId(), Surface.ROTATION_0, - Surface.ROTATION_90, handlerWCT); + mSpiedOneHandedController.onDisplayChange(mDisplay.getDisplayId(), Surface.ROTATION_0, + Surface.ROTATION_90, null /* newDisplayAreaInfo */, handlerWCT); verify(mMockDisplayAreaOrganizer, atLeastOnce()).onRotateDisplay(eq(mContext), eq(Surface.ROTATION_90), any(WindowContainerTransaction.class)); @@ -358,8 +384,8 @@ public class OneHandedControllerTest extends OneHandedTestCase { when(mMockSettingsUitl.getSettingsSwipeToNotificationEnabled(any(), anyInt())).thenReturn( false); final WindowContainerTransaction handlerWCT = new WindowContainerTransaction(); - mSpiedOneHandedController.onRotateDisplay(mDisplay.getDisplayId(), Surface.ROTATION_0, - Surface.ROTATION_90, handlerWCT); + mSpiedOneHandedController.onDisplayChange(mDisplay.getDisplayId(), Surface.ROTATION_0, + Surface.ROTATION_90, null /* newDisplayAreaInfo */, handlerWCT); verify(mMockDisplayAreaOrganizer, never()).onRotateDisplay(eq(mContext), eq(Surface.ROTATION_90), any(WindowContainerTransaction.class)); @@ -371,8 +397,8 @@ public class OneHandedControllerTest extends OneHandedTestCase { when(mMockSettingsUitl.getSettingsSwipeToNotificationEnabled(any(), anyInt())).thenReturn( true); final WindowContainerTransaction handlerWCT = new WindowContainerTransaction(); - mSpiedOneHandedController.onRotateDisplay(mDisplay.getDisplayId(), Surface.ROTATION_0, - Surface.ROTATION_90, handlerWCT); + mSpiedOneHandedController.onDisplayChange(mDisplay.getDisplayId(), Surface.ROTATION_0, + Surface.ROTATION_90, null /* newDisplayAreaInfo */, handlerWCT); verify(mMockDisplayAreaOrganizer, never()).onRotateDisplay(eq(mContext), eq(Surface.ROTATION_90), any(WindowContainerTransaction.class)); @@ -384,8 +410,8 @@ public class OneHandedControllerTest extends OneHandedTestCase { when(mMockSettingsUitl.getSettingsSwipeToNotificationEnabled(any(), anyInt())).thenReturn( false); final WindowContainerTransaction handlerWCT = new WindowContainerTransaction(); - mSpiedOneHandedController.onRotateDisplay(mDisplay.getDisplayId(), Surface.ROTATION_0, - Surface.ROTATION_90, handlerWCT); + mSpiedOneHandedController.onDisplayChange(mDisplay.getDisplayId(), Surface.ROTATION_0, + Surface.ROTATION_90, null /* newDisplayAreaInfo */, handlerWCT); verify(mMockDisplayAreaOrganizer, atLeastOnce()).onRotateDisplay(eq(mContext), eq(Surface.ROTATION_90), any(WindowContainerTransaction.class)); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedStateTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedStateTest.java index dba1b8b86261..a39bdf04bf56 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedStateTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedStateTest.java @@ -29,22 +29,21 @@ import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import android.content.om.IOverlayManager; import android.graphics.Rect; import android.os.Handler; -import android.os.UserHandle; import android.testing.AndroidTestingRunner; import android.util.ArrayMap; import android.view.Display; -import android.view.SurfaceControl; import androidx.test.filters.SmallTest; -import com.android.internal.jank.InteractionJankMonitor; 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.sysui.ShellCommandHandler; +import com.android.wm.shell.sysui.ShellController; +import com.android.wm.shell.sysui.ShellInit; import org.junit.Before; import org.junit.Test; @@ -55,7 +54,6 @@ import org.mockito.MockitoAnnotations; @SmallTest @RunWith(AndroidTestingRunner.class) public class OneHandedStateTest extends OneHandedTestCase { - private int mCurrentUser = UserHandle.myUserId(); Display mDisplay; DisplayLayout mDisplayLayout; @@ -65,6 +63,12 @@ public class OneHandedStateTest extends OneHandedTestCase { OneHandedState mSpiedState; @Mock + ShellInit mMockShellInit; + @Mock + ShellCommandHandler mMockShellCommandHandler; + @Mock + ShellController mMockShellController; + @Mock DisplayController mMockDisplayController; @Mock OneHandedDisplayAreaOrganizer mMockDisplayAreaOrganizer; @@ -77,16 +81,10 @@ public class OneHandedStateTest extends OneHandedTestCase { @Mock OneHandedUiEventLogger mMockUiEventLogger; @Mock - InteractionJankMonitor mMockJankMonitor; - @Mock - IOverlayManager mMockOverlayManager; - @Mock TaskStackListenerImpl mMockTaskStackListener; @Mock ShellExecutor mMockShellMainExecutor; @Mock - SurfaceControl mMockLeash; - @Mock Handler mMockShellMainHandler; final boolean mDefaultEnabled = true; @@ -119,6 +117,9 @@ public class OneHandedStateTest extends OneHandedTestCase { mOneHandedAccessibilityUtil = new OneHandedAccessibilityUtil(mContext); mSpiedOneHandedController = spy(new OneHandedController( mContext, + mMockShellInit, + mMockShellCommandHandler, + mMockShellController, mMockDisplayController, mMockDisplayAreaOrganizer, mMockTouchHandler, @@ -127,9 +128,7 @@ public class OneHandedStateTest extends OneHandedTestCase { mOneHandedAccessibilityUtil, mSpiedTimeoutHandler, mSpiedState, - mMockJankMonitor, mMockUiEventLogger, - mMockOverlayManager, mMockTaskStackListener, mMockShellMainExecutor, mMockShellMainHandler) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedTestCase.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedTestCase.java index 8b03dc58c3bf..808ab2167dc7 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedTestCase.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedTestCase.java @@ -30,6 +30,8 @@ import android.view.WindowManager; import androidx.test.platform.app.InstrumentationRegistry; +import com.android.wm.shell.ShellTestCase; + import org.junit.Before; import org.junit.Rule; import org.mockito.Answers; @@ -38,7 +40,7 @@ import org.mockito.Mock; /** * Base class that does One Handed specific setup. */ -public abstract class OneHandedTestCase { +public abstract class OneHandedTestCase extends ShellTestCase { @Mock(answer = Answers.RETURNS_DEEP_STUBS) protected Context mContext; 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 c685fdc1f09c..5880ffb0dce2 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 @@ -21,6 +21,7 @@ import static android.view.Surface.ROTATION_0; import static android.view.Surface.ROTATION_270; import static android.view.Surface.ROTATION_90; +import static com.android.wm.shell.MockSurfaceControlHelper.createMockSurfaceControlTransaction; import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_LEAVE_PIP; import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_TO_PIP; @@ -36,7 +37,9 @@ import android.testing.TestableLooper; import android.view.SurfaceControl; import androidx.test.filters.SmallTest; +import androidx.test.platform.app.InstrumentationRegistry; +import com.android.wm.shell.MockSurfaceControlHelper; import com.android.wm.shell.ShellTestCase; import org.junit.Before; @@ -60,19 +63,18 @@ public class PipAnimationControllerTest extends ShellTestCase { @Mock private TaskInfo mTaskInfo; - @Mock private PipAnimationController.PipAnimationCallback mPipAnimationCallback; @Before public void setUp() throws Exception { - mPipAnimationController = new PipAnimationController( - new PipSurfaceTransactionHelper()); + MockitoAnnotations.initMocks(this); + mPipAnimationController = new PipAnimationController(new PipSurfaceTransactionHelper( + InstrumentationRegistry.getInstrumentation().getTargetContext())); mLeash = new SurfaceControl.Builder() .setContainerLayer() .setName("FakeLeash") .build(); - MockitoAnnotations.initMocks(this); } @Test @@ -103,7 +105,8 @@ public class PipAnimationControllerTest extends ShellTestCase { final PipAnimationController.PipTransitionAnimator oldAnimator = mPipAnimationController .getAnimator(mTaskInfo, mLeash, baseValue, startValue, endValue1, null, TRANSITION_DIRECTION_TO_PIP, 0, ROTATION_0); - oldAnimator.setSurfaceControlTransactionFactory(PipDummySurfaceControlTx::new); + oldAnimator.setSurfaceControlTransactionFactory( + MockSurfaceControlHelper::createMockSurfaceControlTransaction); oldAnimator.start(); final PipAnimationController.PipTransitionAnimator newAnimator = mPipAnimationController @@ -133,7 +136,7 @@ public class PipAnimationControllerTest extends ShellTestCase { @Test public void pipTransitionAnimator_rotatedEndValue() { - final PipDummySurfaceControlTx tx = new PipDummySurfaceControlTx(); + final SurfaceControl.Transaction tx = createMockSurfaceControlTransaction(); final Rect startBounds = new Rect(200, 700, 400, 800); final Rect endBounds = new Rect(0, 0, 500, 1000); // Fullscreen to PiP. @@ -183,7 +186,8 @@ public class PipAnimationControllerTest extends ShellTestCase { final PipAnimationController.PipTransitionAnimator animator = mPipAnimationController .getAnimator(mTaskInfo, mLeash, baseValue, startValue, endValue, null, TRANSITION_DIRECTION_TO_PIP, 0, ROTATION_0); - animator.setSurfaceControlTransactionFactory(PipDummySurfaceControlTx::new); + animator.setSurfaceControlTransactionFactory( + MockSurfaceControlHelper::createMockSurfaceControlTransaction); animator.setPipAnimationCallback(mPipAnimationCallback); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsAlgorithmTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsAlgorithmTest.java index 0059846c6055..262e4290ef44 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsAlgorithmTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsAlgorithmTest.java @@ -64,7 +64,7 @@ public class PipBoundsAlgorithmTest extends ShellTestCase { initializeMockResources(); mPipBoundsState = new PipBoundsState(mContext); mPipBoundsAlgorithm = new PipBoundsAlgorithm(mContext, mPipBoundsState, - new PipSnapAlgorithm()); + new PipSnapAlgorithm(), new PipKeepClearAlgorithm() {}); mPipBoundsState.setDisplayLayout( new DisplayLayout(mDefaultDisplayInfo, mContext.getResources(), true, true)); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipDummySurfaceControlTx.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipDummySurfaceControlTx.java deleted file mode 100644 index ccf8f6e03844..000000000000 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipDummySurfaceControlTx.java +++ /dev/null @@ -1,66 +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.pip; - -import android.graphics.Matrix; -import android.view.SurfaceControl; - -/** - * A dummy {@link SurfaceControl.Transaction} class for testing purpose and supports - * method chaining. - */ -public class PipDummySurfaceControlTx extends SurfaceControl.Transaction { - @Override - public SurfaceControl.Transaction setAlpha(SurfaceControl leash, float alpha) { - return this; - } - - @Override - public SurfaceControl.Transaction setPosition(SurfaceControl leash, float x, float y) { - return this; - } - - @Override - public SurfaceControl.Transaction setWindowCrop(SurfaceControl leash, int w, int h) { - return this; - } - - @Override - public SurfaceControl.Transaction setCornerRadius(SurfaceControl leash, float radius) { - return this; - } - - @Override - public SurfaceControl.Transaction setShadowRadius(SurfaceControl leash, float radius) { - return this; - } - - @Override - public SurfaceControl.Transaction setMatrix(SurfaceControl leash, Matrix matrix, - float[] float9) { - return this; - } - - @Override - public SurfaceControl.Transaction setFrameTimelineVsync(long frameTimelineVsyncId) { - return this; - } - - @Override - public void apply() {} -} - diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java index e8e6254697c2..90880772b25d 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java @@ -21,13 +21,13 @@ import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTI import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyFloat; import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.ArgumentMatchers.anyLong; 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.verify; import android.app.ActivityManager; @@ -42,8 +42,10 @@ import android.testing.TestableLooper; import android.util.Rational; import android.util.Size; import android.view.DisplayInfo; +import android.view.SurfaceControl; import android.window.WindowContainerToken; +import com.android.wm.shell.MockSurfaceControlHelper; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.TestShellExecutor; @@ -68,7 +70,7 @@ import java.util.Optional; @RunWith(AndroidTestingRunner.class) @TestableLooper.RunWithLooper public class PipTaskOrganizerTest extends ShellTestCase { - private PipTaskOrganizer mSpiedPipTaskOrganizer; + private PipTaskOrganizer mPipTaskOrganizer; @Mock private DisplayController mMockDisplayController; @Mock private SyncTransactionQueue mMockSyncTransactionQueue; @@ -96,16 +98,17 @@ public class PipTaskOrganizerTest extends ShellTestCase { mPipBoundsState = new PipBoundsState(mContext); mPipTransitionState = new PipTransitionState(); mPipBoundsAlgorithm = new PipBoundsAlgorithm(mContext, mPipBoundsState, - new PipSnapAlgorithm()); + new PipSnapAlgorithm(), new PipKeepClearAlgorithm() {}); mMainExecutor = new TestShellExecutor(); - mSpiedPipTaskOrganizer = spy(new PipTaskOrganizer(mContext, + mPipTaskOrganizer = new PipTaskOrganizer(mContext, mMockSyncTransactionQueue, mPipTransitionState, mPipBoundsState, mPipBoundsAlgorithm, mMockPhonePipMenuController, mMockPipAnimationController, mMockPipSurfaceTransactionHelper, mMockPipTransitionController, mMockPipParamsChangedForwarder, mMockOptionalSplitScreen, mMockDisplayController, - mMockPipUiEventLogger, mMockShellTaskOrganizer, mMainExecutor)); + mMockPipUiEventLogger, mMockShellTaskOrganizer, mMainExecutor); mMainExecutor.flushAll(); preparePipTaskOrg(); + preparePipSurfaceTransactionHelper(); } @Test @@ -122,14 +125,14 @@ public class PipTaskOrganizerTest extends ShellTestCase { public void startSwipePipToHome_updatesAspectRatio() { final Rational aspectRatio = new Rational(2, 1); - mSpiedPipTaskOrganizer.startSwipePipToHome(mComponent1, null, createPipParams(aspectRatio)); + mPipTaskOrganizer.startSwipePipToHome(mComponent1, null, createPipParams(aspectRatio)); assertEquals(aspectRatio.floatValue(), mPipBoundsState.getAspectRatio(), 0.01f); } @Test public void startSwipePipToHome_updatesLastPipComponentName() { - mSpiedPipTaskOrganizer.startSwipePipToHome(mComponent1, null, createPipParams(null)); + mPipTaskOrganizer.startSwipePipToHome(mComponent1, null, createPipParams(null)); assertEquals(mComponent1, mPipBoundsState.getLastPipComponentName()); } @@ -138,7 +141,7 @@ public class PipTaskOrganizerTest extends ShellTestCase { public void startSwipePipToHome_updatesOverrideMinSize() { final Size minSize = new Size(400, 320); - mSpiedPipTaskOrganizer.startSwipePipToHome(mComponent1, createActivityInfo(minSize), + mPipTaskOrganizer.startSwipePipToHome(mComponent1, createActivityInfo(minSize), createPipParams(null)); assertEquals(minSize, mPipBoundsState.getOverrideMinSize()); @@ -148,16 +151,16 @@ public class PipTaskOrganizerTest extends ShellTestCase { public void onTaskAppeared_updatesAspectRatio() { final Rational aspectRatio = new Rational(2, 1); - mSpiedPipTaskOrganizer.onTaskAppeared(createTaskInfo(mComponent1, - createPipParams(aspectRatio)), null /* leash */); + mPipTaskOrganizer.onTaskAppeared(createTaskInfo(mComponent1, + createPipParams(aspectRatio)), mock(SurfaceControl.class)); assertEquals(aspectRatio.floatValue(), mPipBoundsState.getAspectRatio(), 0.01f); } @Test public void onTaskAppeared_updatesLastPipComponentName() { - mSpiedPipTaskOrganizer.onTaskAppeared(createTaskInfo(mComponent1, createPipParams(null)), - null /* leash */); + mPipTaskOrganizer.onTaskAppeared(createTaskInfo(mComponent1, createPipParams(null)), + mock(SurfaceControl.class)); assertEquals(mComponent1, mPipBoundsState.getLastPipComponentName()); } @@ -166,9 +169,9 @@ public class PipTaskOrganizerTest extends ShellTestCase { public void onTaskAppeared_updatesOverrideMinSize() { final Size minSize = new Size(400, 320); - mSpiedPipTaskOrganizer.onTaskAppeared( + mPipTaskOrganizer.onTaskAppeared( createTaskInfo(mComponent1, createPipParams(null), minSize), - null /* leash */); + mock(SurfaceControl.class)); assertEquals(minSize, mPipBoundsState.getOverrideMinSize()); } @@ -177,16 +180,16 @@ public class PipTaskOrganizerTest extends ShellTestCase { public void onTaskInfoChanged_notInPip_deferUpdatesAspectRatio() { final Rational startAspectRatio = new Rational(2, 1); final Rational newAspectRatio = new Rational(1, 2); - mSpiedPipTaskOrganizer.onTaskAppeared(createTaskInfo(mComponent1, - createPipParams(startAspectRatio)), null /* leash */); + mPipTaskOrganizer.onTaskAppeared(createTaskInfo(mComponent1, + createPipParams(startAspectRatio)), mock(SurfaceControl.class)); // It is in entering transition, should defer onTaskInfoChanged callback in this case. - mSpiedPipTaskOrganizer.onTaskInfoChanged(createTaskInfo(mComponent1, + mPipTaskOrganizer.onTaskInfoChanged(createTaskInfo(mComponent1, createPipParams(newAspectRatio))); verify(mMockPipParamsChangedForwarder, never()).notifyAspectRatioChanged(anyFloat()); // Once the entering transition finishes, the new aspect ratio applies in a deferred manner - mSpiedPipTaskOrganizer.sendOnPipTransitionFinished(TRANSITION_DIRECTION_TO_PIP); + sendOnPipTransitionFinished(TRANSITION_DIRECTION_TO_PIP); verify(mMockPipParamsChangedForwarder) .notifyAspectRatioChanged(newAspectRatio.floatValue()); } @@ -195,11 +198,11 @@ public class PipTaskOrganizerTest extends ShellTestCase { public void onTaskInfoChanged_inPip_updatesAspectRatioIfChanged() { final Rational startAspectRatio = new Rational(2, 1); final Rational newAspectRatio = new Rational(1, 2); - mSpiedPipTaskOrganizer.onTaskAppeared(createTaskInfo(mComponent1, - createPipParams(startAspectRatio)), null /* leash */); - mSpiedPipTaskOrganizer.sendOnPipTransitionFinished(TRANSITION_DIRECTION_TO_PIP); + mPipTaskOrganizer.onTaskAppeared(createTaskInfo(mComponent1, + createPipParams(startAspectRatio)), mock(SurfaceControl.class)); + sendOnPipTransitionFinished(TRANSITION_DIRECTION_TO_PIP); - mSpiedPipTaskOrganizer.onTaskInfoChanged(createTaskInfo(mComponent1, + mPipTaskOrganizer.onTaskInfoChanged(createTaskInfo(mComponent1, createPipParams(newAspectRatio))); verify(mMockPipParamsChangedForwarder) @@ -208,11 +211,11 @@ public class PipTaskOrganizerTest extends ShellTestCase { @Test public void onTaskInfoChanged_inPip_updatesLastPipComponentName() { - mSpiedPipTaskOrganizer.onTaskAppeared(createTaskInfo(mComponent1, - createPipParams(null)), null /* leash */); - mSpiedPipTaskOrganizer.sendOnPipTransitionFinished(TRANSITION_DIRECTION_TO_PIP); + mPipTaskOrganizer.onTaskAppeared(createTaskInfo(mComponent1, + createPipParams(null)), mock(SurfaceControl.class)); + sendOnPipTransitionFinished(TRANSITION_DIRECTION_TO_PIP); - mSpiedPipTaskOrganizer.onTaskInfoChanged(createTaskInfo(mComponent2, + mPipTaskOrganizer.onTaskInfoChanged(createTaskInfo(mComponent2, createPipParams(null))); assertEquals(mComponent2, mPipBoundsState.getLastPipComponentName()); @@ -220,12 +223,12 @@ public class PipTaskOrganizerTest extends ShellTestCase { @Test public void onTaskInfoChanged_inPip_updatesOverrideMinSize() { - mSpiedPipTaskOrganizer.onTaskAppeared(createTaskInfo(mComponent1, - createPipParams(null)), null /* leash */); - mSpiedPipTaskOrganizer.sendOnPipTransitionFinished(TRANSITION_DIRECTION_TO_PIP); + mPipTaskOrganizer.onTaskAppeared(createTaskInfo(mComponent1, + createPipParams(null)), mock(SurfaceControl.class)); + sendOnPipTransitionFinished(TRANSITION_DIRECTION_TO_PIP); final Size minSize = new Size(400, 320); - mSpiedPipTaskOrganizer.onTaskInfoChanged(createTaskInfo(mComponent2, + mPipTaskOrganizer.onTaskInfoChanged(createTaskInfo(mComponent2, createPipParams(null), minSize)); assertEquals(minSize, mPipBoundsState.getOverrideMinSize()); @@ -233,22 +236,42 @@ public class PipTaskOrganizerTest extends ShellTestCase { @Test public void onTaskVanished_clearsPipBounds() { - mSpiedPipTaskOrganizer.onTaskAppeared(createTaskInfo(mComponent1, - createPipParams(null)), null /* leash */); + mPipTaskOrganizer.onTaskAppeared(createTaskInfo(mComponent1, + createPipParams(null)), mock(SurfaceControl.class)); mPipBoundsState.setBounds(new Rect(100, 100, 200, 150)); - mSpiedPipTaskOrganizer.onTaskVanished(createTaskInfo(mComponent1, createPipParams(null))); + mPipTaskOrganizer.onTaskVanished(createTaskInfo(mComponent1, createPipParams(null))); assertTrue(mPipBoundsState.getBounds().isEmpty()); } + private void sendOnPipTransitionFinished( + @PipAnimationController.TransitionDirection int direction) { + mPipTaskOrganizer.sendOnPipTransitionFinished(direction); + // PipTransitionController will call back into PipTaskOrganizer. + mPipTaskOrganizer.mPipTransitionCallback.onPipTransitionFinished(direction); + } + private void preparePipTaskOrg() { final DisplayInfo info = new DisplayInfo(); mPipBoundsState.setDisplayLayout(new DisplayLayout(info, mContext.getResources(), true, true)); - mSpiedPipTaskOrganizer.setOneShotAnimationType(PipAnimationController.ANIM_TYPE_ALPHA); - mSpiedPipTaskOrganizer.setSurfaceControlTransactionFactory(PipDummySurfaceControlTx::new); - doNothing().when(mSpiedPipTaskOrganizer).enterPipWithAlphaAnimation(any(), anyLong()); - doNothing().when(mSpiedPipTaskOrganizer).scheduleAnimateResizePip(any(), anyInt(), any()); + mPipTaskOrganizer.setOneShotAnimationType(PipAnimationController.ANIM_TYPE_ALPHA); + mPipTaskOrganizer.setSurfaceControlTransactionFactory( + MockSurfaceControlHelper::createMockSurfaceControlTransaction); + } + + private void preparePipSurfaceTransactionHelper() { + doReturn(mMockPipSurfaceTransactionHelper).when(mMockPipSurfaceTransactionHelper) + .crop(any(), any(), any()); + doReturn(mMockPipSurfaceTransactionHelper).when(mMockPipSurfaceTransactionHelper) + .resetScale(any(), any(), any()); + doReturn(mMockPipSurfaceTransactionHelper).when(mMockPipSurfaceTransactionHelper) + .round(any(), any(), anyBoolean()); + doReturn(mMockPipSurfaceTransactionHelper).when(mMockPipSurfaceTransactionHelper) + .scale(any(), any(), any(), any(), anyFloat()); + doReturn(mMockPipSurfaceTransactionHelper).when(mMockPipSurfaceTransactionHelper) + .alpha(any(), any(), anyFloat()); + doNothing().when(mMockPipSurfaceTransactionHelper).onDensityOrFontScaleChanged(any()); } private static ActivityManager.RunningTaskInfo createTaskInfo( 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 new file mode 100644 index 000000000000..4d7e9e450ceb --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PhonePipKeepClearAlgorithmTest.java @@ -0,0 +1,96 @@ +/* + * 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.pip.phone; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + +import android.graphics.Rect; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; + +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.Set; + +/** + * Unit tests against {@link PhonePipKeepClearAlgorithm}. + */ +@RunWith(AndroidTestingRunner.class) +@SmallTest +@TestableLooper.RunWithLooper(setAsMainLooper = true) +public class PhonePipKeepClearAlgorithmTest extends ShellTestCase { + + private PhonePipKeepClearAlgorithm mPipKeepClearAlgorithm; + private static final Rect DISPLAY_BOUNDS = new Rect(0, 0, 1000, 1000); + + @Before + public void setUp() throws Exception { + mPipKeepClearAlgorithm = new PhonePipKeepClearAlgorithm(mContext); + } + + @Test + public void findUnoccludedPosition_withCollidingRestrictedKeepClearArea_movesBounds() { + final Rect inBounds = new Rect(0, 0, 100, 100); + final Rect keepClearRect = new Rect(50, 50, 150, 150); + + final Rect outBounds = mPipKeepClearAlgorithm.findUnoccludedPosition(inBounds, + Set.of(keepClearRect), Set.of(), DISPLAY_BOUNDS); + + assertFalse(outBounds.contains(keepClearRect)); + } + + @Test + public void findUnoccludedPosition_withNonCollidingRestrictedKeepClearArea_boundsUnchanged() { + final Rect inBounds = new Rect(0, 0, 100, 100); + final Rect keepClearRect = new Rect(100, 100, 150, 150); + + final Rect outBounds = mPipKeepClearAlgorithm.findUnoccludedPosition(inBounds, + Set.of(keepClearRect), Set.of(), DISPLAY_BOUNDS); + + assertEquals(inBounds, outBounds); + } + + @Test + public void findUnoccludedPosition_withCollidingUnrestrictedKeepClearArea_moveBounds() { + // TODO(b/183746978): update this test to accommodate for the updated algorithm + final Rect inBounds = new Rect(0, 0, 100, 100); + final Rect keepClearRect = new Rect(50, 50, 150, 150); + + final Rect outBounds = mPipKeepClearAlgorithm.findUnoccludedPosition(inBounds, Set.of(), + Set.of(keepClearRect), DISPLAY_BOUNDS); + + assertFalse(outBounds.contains(keepClearRect)); + } + + @Test + public void findUnoccludedPosition_withNonCollidingUnrestrictedKeepClearArea_boundsUnchanged() { + final Rect inBounds = new Rect(0, 0, 100, 100); + final Rect keepClearRect = new Rect(100, 100, 150, 150); + + final Rect outBounds = mPipKeepClearAlgorithm.findUnoccludedPosition(inBounds, Set.of(), + Set.of(keepClearRect), DISPLAY_BOUNDS); + + assertEquals(inBounds, 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 df18133adcfb..1e08f1e55797 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java @@ -23,7 +23,9 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -41,10 +43,12 @@ import android.util.Size; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.WindowManagerShellWrapper; 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.ShellExecutor; import com.android.wm.shell.common.TaskStackListenerImpl; import com.android.wm.shell.onehanded.OneHandedController; +import com.android.wm.shell.pip.PipAnimationController; import com.android.wm.shell.pip.PipAppOpsListener; import com.android.wm.shell.pip.PipBoundsAlgorithm; import com.android.wm.shell.pip.PipBoundsState; @@ -53,11 +57,16 @@ import com.android.wm.shell.pip.PipParamsChangedForwarder; import com.android.wm.shell.pip.PipSnapAlgorithm; import com.android.wm.shell.pip.PipTaskOrganizer; import com.android.wm.shell.pip.PipTransitionController; +import com.android.wm.shell.pip.PipTransitionState; +import com.android.wm.shell.sysui.ShellCommandHandler; +import com.android.wm.shell.sysui.ShellController; +import com.android.wm.shell.sysui.ShellInit; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import java.util.Optional; @@ -71,14 +80,20 @@ import java.util.Set; @TestableLooper.RunWithLooper public class PipControllerTest extends ShellTestCase { private PipController mPipController; + private ShellInit mShellInit; + private ShellController mShellController; + @Mock private ShellCommandHandler mMockShellCommandHandler; @Mock private DisplayController mMockDisplayController; @Mock private PhonePipMenuController mMockPhonePipMenuController; + @Mock private PipAnimationController mMockPipAnimationController; @Mock private PipAppOpsListener mMockPipAppOpsListener; @Mock private PipBoundsAlgorithm mMockPipBoundsAlgorithm; + @Mock private PhonePipKeepClearAlgorithm mMockPipKeepClearAlgorithm; @Mock private PipSnapAlgorithm mMockPipSnapAlgorithm; @Mock private PipMediaController mMockPipMediaController; @Mock private PipTaskOrganizer mMockPipTaskOrganizer; + @Mock private PipTransitionState mMockPipTransitionState; @Mock private PipTransitionController mMockPipTransitionController; @Mock private PipTouchHandler mMockPipTouchHandler; @Mock private PipMotionHelper mMockPipMotionHelper; @@ -87,7 +102,8 @@ public class PipControllerTest extends ShellTestCase { @Mock private TaskStackListenerImpl mMockTaskStackListener; @Mock private ShellExecutor mMockExecutor; @Mock private Optional<OneHandedController> mMockOneHandedController; - @Mock private PipParamsChangedForwarder mPipParamsChangedForwarder; + @Mock private PipParamsChangedForwarder mMockPipParamsChangedForwarder; + @Mock private DisplayInsetsController mMockDisplayInsetsController; @Mock private DisplayLayout mMockDisplayLayout1; @Mock private DisplayLayout mMockDisplayLayout2; @@ -99,18 +115,53 @@ public class PipControllerTest extends ShellTestCase { ((Runnable) invocation.getArgument(0)).run(); return null; }).when(mMockExecutor).execute(any()); - mPipController = new PipController(mContext, mMockDisplayController, - mMockPipAppOpsListener, mMockPipBoundsAlgorithm, - mMockPipBoundsState, mMockPipMediaController, - mMockPhonePipMenuController, mMockPipTaskOrganizer, mMockPipTouchHandler, - mMockPipTransitionController, mMockWindowManagerShellWrapper, - mMockTaskStackListener, mPipParamsChangedForwarder, - mMockOneHandedController, mMockExecutor); + mShellInit = spy(new ShellInit(mMockExecutor)); + mShellController = spy(new ShellController(mShellInit, mMockShellCommandHandler, + mMockExecutor)); + mPipController = new PipController(mContext, mShellInit, mMockShellCommandHandler, + mShellController, mMockDisplayController, mMockPipAnimationController, + mMockPipAppOpsListener, mMockPipBoundsAlgorithm, mMockPipKeepClearAlgorithm, + mMockPipBoundsState, mMockPipMotionHelper, mMockPipMediaController, + mMockPhonePipMenuController, mMockPipTaskOrganizer, mMockPipTransitionState, + mMockPipTouchHandler, mMockPipTransitionController, mMockWindowManagerShellWrapper, + mMockTaskStackListener, mMockPipParamsChangedForwarder, + mMockDisplayInsetsController, mMockOneHandedController, mMockExecutor); + mShellInit.init(); when(mMockPipBoundsAlgorithm.getSnapAlgorithm()).thenReturn(mMockPipSnapAlgorithm); when(mMockPipTouchHandler.getMotionHelper()).thenReturn(mMockPipMotionHelper); } @Test + public void instantiatePipController_addInitCallback() { + verify(mShellInit, times(1)).addInitCallback(any(), any()); + } + + @Test + public void instantiateController_registerDumpCallback() { + verify(mMockShellCommandHandler, times(1)).addDumpCallback(any(), any()); + } + + @Test + public void instantiatePipController_registerConfigChangeListener() { + verify(mShellController, times(1)).addConfigurationChangeListener(any()); + } + + @Test + public void instantiatePipController_registerKeyguardChangeListener() { + verify(mShellController, times(1)).addKeyguardChangeListener(any()); + } + + @Test + public void instantiatePipController_registerUserChangeListener() { + verify(mShellController, times(1)).addUserChangeListener(any()); + } + + @Test + public void instantiatePipController_registerMediaListener() { + verify(mMockPipMediaController, times(1)).registerSessionListenerForCurrentUser(); + } + + @Test public void instantiatePipController_registersPipTransitionCallback() { verify(mMockPipTransitionController).registerPipTransitionCallback(any()); } @@ -132,13 +183,15 @@ public class PipControllerTest extends ShellTestCase { when(mockPackageManager.hasSystemFeature(FEATURE_PICTURE_IN_PICTURE)).thenReturn(false); when(spyContext.getPackageManager()).thenReturn(mockPackageManager); - assertNull(PipController.create(spyContext, mMockDisplayController, - mMockPipAppOpsListener, mMockPipBoundsAlgorithm, - mMockPipBoundsState, mMockPipMediaController, - mMockPhonePipMenuController, mMockPipTaskOrganizer, mMockPipTouchHandler, - mMockPipTransitionController, mMockWindowManagerShellWrapper, - mMockTaskStackListener, mPipParamsChangedForwarder, - mMockOneHandedController, mMockExecutor)); + ShellInit shellInit = new ShellInit(mMockExecutor); + assertNull(PipController.create(spyContext, shellInit, mMockShellCommandHandler, + mShellController, mMockDisplayController, mMockPipAnimationController, + mMockPipAppOpsListener, mMockPipBoundsAlgorithm, mMockPipKeepClearAlgorithm, + mMockPipBoundsState, mMockPipMotionHelper, mMockPipMediaController, + mMockPhonePipMenuController, mMockPipTaskOrganizer, mMockPipTransitionState, + mMockPipTouchHandler, mMockPipTransitionController, mMockWindowManagerShellWrapper, + mMockTaskStackListener, mMockPipParamsChangedForwarder, + mMockDisplayInsetsController, mMockOneHandedController, mMockExecutor)); } @Test @@ -199,7 +252,7 @@ public class PipControllerTest extends ShellTestCase { mPipController.mDisplaysChangedListener.onDisplayConfigurationChanged( displayId, new Configuration()); - verify(mMockPipMotionHelper).movePip(any(Rect.class)); + verify(mMockPipTaskOrganizer).scheduleFinishResizePip(any(Rect.class)); } @Test @@ -215,11 +268,24 @@ public class PipControllerTest extends ShellTestCase { mPipController.mDisplaysChangedListener.onDisplayConfigurationChanged( displayId, new Configuration()); - verify(mMockPipMotionHelper, never()).movePip(any(Rect.class)); + verify(mMockPipTaskOrganizer, never()).scheduleFinishResizePip(any(Rect.class)); } @Test - public void onKeepClearAreasChanged_updatesPipBoundsState() { + public void onKeepClearAreasChanged_featureDisabled_pipBoundsStateDoesntChange() { + final int displayId = 1; + final Rect keepClearArea = new Rect(0, 0, 10, 10); + when(mMockPipBoundsState.getDisplayId()).thenReturn(displayId); + + mPipController.mDisplaysChangedListener.onKeepClearAreasChanged( + displayId, Set.of(keepClearArea), Set.of()); + + verify(mMockPipBoundsState, never()).setKeepClearAreas(Mockito.anySet(), Mockito.anySet()); + } + + @Test + public void onKeepClearAreasChanged_featureEnabled_updatesPipBoundsState() { + mPipController.setEnablePipKeepClearAlgorithm(true); final int displayId = 1; final Rect keepClearArea = new Rect(0, 0, 10, 10); when(mMockPipBoundsState.getDisplayId()).thenReturn(displayId); @@ -229,4 +295,11 @@ public class PipControllerTest extends ShellTestCase { verify(mMockPipBoundsState).setKeepClearAreas(Set.of(keepClearArea), Set.of()); } + + @Test + public void onUserChangeRegisterMediaListener() { + reset(mMockPipMediaController); + mShellController.asShell().onUserChanged(100, mContext); + verify(mMockPipMediaController, times(1)).registerSessionListenerForCurrentUser(); + } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipDoubleTapHelperTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipDoubleTapHelperTest.java new file mode 100644 index 000000000000..8ce3ca4bdc00 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipDoubleTapHelperTest.java @@ -0,0 +1,172 @@ +/* + * 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.pip.phone; + +import static com.android.wm.shell.pip.phone.PipDoubleTapHelper.SIZE_SPEC_CUSTOM; +import static com.android.wm.shell.pip.phone.PipDoubleTapHelper.SIZE_SPEC_DEFAULT; +import static com.android.wm.shell.pip.phone.PipDoubleTapHelper.SIZE_SPEC_MAX; +import static com.android.wm.shell.pip.phone.PipDoubleTapHelper.nextSizeSpec; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import android.graphics.Point; +import android.graphics.Rect; +import android.testing.AndroidTestingRunner; + +import com.android.wm.shell.ShellTestCase; +import com.android.wm.shell.pip.PipBoundsState; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; + +/** + * Unit test against {@link PipDoubleTapHelper}. + */ +@RunWith(AndroidTestingRunner.class) +public class PipDoubleTapHelperTest extends ShellTestCase { + // represents the current pip window state and has information on current + // max, min, and normal sizes + @Mock private PipBoundsState mBoundStateMock; + // tied to boundsStateMock.getBounds() in setUp() + @Mock private Rect mBoundsMock; + + // represents the most recent manually resized bounds + // i.e. dimensions from the most recent pinch in/out + @Mock private Rect mUserResizeBoundsMock; + + // actual dimensions of the pip screen bounds + private static final int MAX_WIDTH = 100; + private static final int DEFAULT_WIDTH = 40; + private static final int MIN_WIDTH = 10; + + private static final int AVERAGE_WIDTH = (MAX_WIDTH + MIN_WIDTH) / 2; + + /** + * Initializes mocks and assigns values for different pip screen bounds. + */ + @Before + public void setUp() { + // define pip bounds + when(mBoundStateMock.getMaxSize()).thenReturn(new Point(MAX_WIDTH, 20)); + when(mBoundStateMock.getMinSize()).thenReturn(new Point(MIN_WIDTH, 2)); + + Rect rectMock = mock(Rect.class); + when(rectMock.width()).thenReturn(DEFAULT_WIDTH); + when(mBoundStateMock.getNormalBounds()).thenReturn(rectMock); + + when(mBoundsMock.width()).thenReturn(DEFAULT_WIDTH); + when(mBoundStateMock.getBounds()).thenReturn(mBoundsMock); + } + + /** + * Tests {@link PipDoubleTapHelper#nextSizeSpec(PipBoundsState, Rect)}. + * + * <p>when the user resizes the screen to a larger than the average but not the maximum width, + * then we toggle between {@code PipSizeSpec.CUSTOM} and {@code PipSizeSpec.DEFAULT} + */ + @Test + public void testNextScreenSize_resizedWiderThanAverage_returnDefaultThenCustom() { + // make the user resize width in between MAX and average + when(mUserResizeBoundsMock.width()).thenReturn((MAX_WIDTH + AVERAGE_WIDTH) / 2); + // make current bounds same as resized bound since no double tap yet + when(mBoundsMock.width()).thenReturn((MAX_WIDTH + AVERAGE_WIDTH) / 2); + + // then nextScreenSize() i.e. double tapping should + // toggle to DEFAULT state + Assert.assertSame(nextSizeSpec(mBoundStateMock, mUserResizeBoundsMock), + SIZE_SPEC_DEFAULT); + + // once we toggle to DEFAULT our screen size gets updated + // but not the user resize bounds + when(mBoundsMock.width()).thenReturn(DEFAULT_WIDTH); + + // then nextScreenSize() i.e. double tapping should + // toggle to CUSTOM state + Assert.assertSame(nextSizeSpec(mBoundStateMock, mUserResizeBoundsMock), + SIZE_SPEC_CUSTOM); + } + + /** + * Tests {@link PipDoubleTapHelper#nextSizeSpec(PipBoundsState, Rect)}. + * + * <p>when the user resizes the screen to a smaller than the average but not the default width, + * then we toggle between {@code PipSizeSpec.CUSTOM} and {@code PipSizeSpec.MAX} + */ + @Test + public void testNextScreenSize_resizedNarrowerThanAverage_returnMaxThenCustom() { + // make the user resize width in between MIN and average + when(mUserResizeBoundsMock.width()).thenReturn((MIN_WIDTH + AVERAGE_WIDTH) / 2); + // make current bounds same as resized bound since no double tap yet + when(mBoundsMock.width()).thenReturn((MIN_WIDTH + AVERAGE_WIDTH) / 2); + + // then nextScreenSize() i.e. double tapping should + // toggle to MAX state + Assert.assertSame(nextSizeSpec(mBoundStateMock, mUserResizeBoundsMock), + SIZE_SPEC_MAX); + + // once we toggle to MAX our screen size gets updated + // but not the user resize bounds + when(mBoundsMock.width()).thenReturn(MAX_WIDTH); + + // then nextScreenSize() i.e. double tapping should + // toggle to CUSTOM state + Assert.assertSame(nextSizeSpec(mBoundStateMock, mUserResizeBoundsMock), + SIZE_SPEC_CUSTOM); + } + + /** + * Tests {@link PipDoubleTapHelper#nextSizeSpec(PipBoundsState, Rect)}. + * + * <p>when the user resizes the screen to exactly the maximum width + * then we toggle to {@code PipSizeSpec.DEFAULT} + */ + @Test + public void testNextScreenSize_resizedToMax_returnDefault() { + // the resized width is the same as MAX_WIDTH + when(mUserResizeBoundsMock.width()).thenReturn(MAX_WIDTH); + // the current bounds are also at MAX_WIDTH + when(mBoundsMock.width()).thenReturn(MAX_WIDTH); + + // then nextScreenSize() i.e. double tapping should + // toggle to DEFAULT state + Assert.assertSame(nextSizeSpec(mBoundStateMock, mUserResizeBoundsMock), + SIZE_SPEC_DEFAULT); + } + + /** + * Tests {@link PipDoubleTapHelper#nextSizeSpec(PipBoundsState, Rect)}. + * + * <p>when the user resizes the screen to exactly the default width + * then we toggle to {@code PipSizeSpec.MAX} + */ + @Test + public void testNextScreenSize_resizedToDefault_returnMax() { + // the resized width is the same as DEFAULT_WIDTH + when(mUserResizeBoundsMock.width()).thenReturn(DEFAULT_WIDTH); + // the current bounds are also at DEFAULT_WIDTH + when(mBoundsMock.width()).thenReturn(DEFAULT_WIDTH); + + // then nextScreenSize() i.e. double tapping should + // toggle to MAX state + Assert.assertSame(nextSizeSpec(mBoundStateMock, mUserResizeBoundsMock), + SIZE_SPEC_MAX); + } +} 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 dd10aa7752f5..dba037db72eb 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 @@ -36,6 +36,7 @@ import com.android.wm.shell.common.FloatingContentCoordinator; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.pip.PipBoundsAlgorithm; import com.android.wm.shell.pip.PipBoundsState; +import com.android.wm.shell.pip.PipKeepClearAlgorithm; import com.android.wm.shell.pip.PipSnapAlgorithm; import com.android.wm.shell.pip.PipTaskOrganizer; import com.android.wm.shell.pip.PipTransitionController; @@ -87,8 +88,10 @@ public class PipResizeGestureHandlerTest extends ShellTestCase { MockitoAnnotations.initMocks(this); mPipBoundsState = new PipBoundsState(mContext); final PipSnapAlgorithm pipSnapAlgorithm = new PipSnapAlgorithm(); + final PipKeepClearAlgorithm pipKeepClearAlgorithm = + new PipKeepClearAlgorithm() {}; final PipBoundsAlgorithm pipBoundsAlgorithm = new PipBoundsAlgorithm(mContext, - mPipBoundsState, pipSnapAlgorithm); + mPipBoundsState, pipSnapAlgorithm, pipKeepClearAlgorithm); final PipMotionHelper motionHelper = new PipMotionHelper(mContext, mPipBoundsState, mPipTaskOrganizer, mPhonePipMenuController, pipSnapAlgorithm, mMockPipTransitionController, mFloatingContentCoordinator); 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 74519eaf3ebf..474d6aaf4623 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 @@ -34,10 +34,12 @@ import com.android.wm.shell.common.FloatingContentCoordinator; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.pip.PipBoundsAlgorithm; import com.android.wm.shell.pip.PipBoundsState; +import com.android.wm.shell.pip.PipKeepClearAlgorithm; import com.android.wm.shell.pip.PipSnapAlgorithm; import com.android.wm.shell.pip.PipTaskOrganizer; import com.android.wm.shell.pip.PipTransitionController; import com.android.wm.shell.pip.PipUiEventLogger; +import com.android.wm.shell.sysui.ShellInit; import org.junit.Before; import org.junit.Test; @@ -78,6 +80,9 @@ public class PipTouchHandlerTest extends ShellTestCase { private PipUiEventLogger mPipUiEventLogger; @Mock + private ShellInit mShellInit; + + @Mock private ShellExecutor mMainExecutor; private PipBoundsState mPipBoundsState; @@ -100,15 +105,16 @@ public class PipTouchHandlerTest extends ShellTestCase { MockitoAnnotations.initMocks(this); mPipBoundsState = new PipBoundsState(mContext); mPipSnapAlgorithm = new PipSnapAlgorithm(); - mPipBoundsAlgorithm = new PipBoundsAlgorithm(mContext, mPipBoundsState, mPipSnapAlgorithm); + mPipBoundsAlgorithm = new PipBoundsAlgorithm(mContext, mPipBoundsState, mPipSnapAlgorithm, + new PipKeepClearAlgorithm() {}); PipMotionHelper pipMotionHelper = new PipMotionHelper(mContext, mPipBoundsState, mPipTaskOrganizer, mPhonePipMenuController, mPipSnapAlgorithm, mMockPipTransitionController, mFloatingContentCoordinator); - mPipTouchHandler = new PipTouchHandler(mContext, mPhonePipMenuController, - mPipBoundsAlgorithm, mPipBoundsState, mPipTaskOrganizer, - pipMotionHelper, mFloatingContentCoordinator, mPipUiEventLogger, - mMainExecutor); - mPipTouchHandler.init(); + mPipTouchHandler = new PipTouchHandler(mContext, mShellInit, mPhonePipMenuController, + mPipBoundsAlgorithm, mPipBoundsState, mPipTaskOrganizer, pipMotionHelper, + mFloatingContentCoordinator, mPipUiEventLogger, mMainExecutor); + // We aren't actually using ShellInit, so just call init directly + mPipTouchHandler.onInit(); mMotionHelper = Mockito.spy(mPipTouchHandler.getMotionHelper()); mPipResizeGestureHandler = Mockito.spy(mPipTouchHandler.getPipResizeGestureHandler()); mPipTouchHandler.setPipMotionHelper(mMotionHelper); @@ -133,6 +139,11 @@ public class PipTouchHandlerTest extends ShellTestCase { } @Test + public void instantiate_addInitCallback() { + verify(mShellInit, times(1)).addInitCallback(any(), any()); + } + + @Test public void updateMovementBounds_minMaxBounds() { final int shorterLength = Math.min(mPipBoundsState.getDisplayBounds().width(), mPipBoundsState.getDisplayBounds().height()); 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 new file mode 100644 index 000000000000..baa06f2f0c45 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/GroupedRecentTaskInfoTest.kt @@ -0,0 +1,169 @@ +/* + * 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.recents + +import android.app.ActivityManager +import android.graphics.Rect +import android.os.Parcel +import android.testing.AndroidTestingRunner +import android.window.IWindowContainerToken +import android.window.WindowContainerToken +import androidx.test.filters.SmallTest +import com.android.wm.shell.ShellTestCase +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.google.common.truth.Correspondence +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.mock + +/** + * Tests for [GroupedRecentTaskInfo] + */ +@SmallTest +@RunWith(AndroidTestingRunner::class) +class GroupedRecentTaskInfoTest : ShellTestCase() { + + @Test + fun testSingleTask_hasCorrectType() { + assertThat(singleTaskGroupInfo().type).isEqualTo(TYPE_SINGLE) + } + + @Test + fun testSingleTask_task1Set_task2Null() { + val group = singleTaskGroupInfo() + assertThat(group.taskInfo1.taskId).isEqualTo(1) + assertThat(group.taskInfo2).isNull() + } + + @Test + fun testSingleTask_taskInfoList_hasOneTask() { + val list = singleTaskGroupInfo().taskInfoList + assertThat(list).hasSize(1) + assertThat(list[0].taskId).isEqualTo(1) + } + + @Test + fun testSplitTasks_hasCorrectType() { + assertThat(splitTasksGroupInfo().type).isEqualTo(TYPE_SPLIT) + } + + @Test + fun testSplitTasks_task1Set_task2Set_boundsSet() { + val group = splitTasksGroupInfo() + assertThat(group.taskInfo1.taskId).isEqualTo(1) + assertThat(group.taskInfo2?.taskId).isEqualTo(2) + assertThat(group.splitBounds).isNotNull() + } + + @Test + fun testSplitTasks_taskInfoList_hasTwoTasks() { + val list = splitTasksGroupInfo().taskInfoList + assertThat(list).hasSize(2) + assertThat(list[0].taskId).isEqualTo(1) + assertThat(list[1].taskId).isEqualTo(2) + } + + @Test + fun testFreeformTasks_hasCorrectType() { + assertThat(freeformTasksGroupInfo().type).isEqualTo(TYPE_FREEFORM) + } + + @Test + fun testSplitTasks_taskInfoList_hasThreeTasks() { + val list = freeformTasksGroupInfo().taskInfoList + assertThat(list).hasSize(3) + assertThat(list[0].taskId).isEqualTo(1) + assertThat(list[1].taskId).isEqualTo(2) + assertThat(list[2].taskId).isEqualTo(3) + } + + @Test + fun testParcelling_singleTask() { + val recentTaskInfo = singleTaskGroupInfo() + 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_SINGLE) + assertThat(recentTaskInfoParcel.taskInfo1.taskId).isEqualTo(1) + assertThat(recentTaskInfoParcel.taskInfo2).isNull() + } + + @Test + fun testParcelling_splitTasks() { + val recentTaskInfo = splitTasksGroupInfo() + 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_SPLIT) + assertThat(recentTaskInfoParcel.taskInfo1.taskId).isEqualTo(1) + assertThat(recentTaskInfoParcel.taskInfo2).isNotNull() + assertThat(recentTaskInfoParcel.taskInfo2!!.taskId).isEqualTo(2) + assertThat(recentTaskInfoParcel.splitBounds).isNotNull() + } + + @Test + fun testParcelling_freeformTasks() { + val recentTaskInfo = freeformTasksGroupInfo() + 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.taskInfoList).hasSize(3) + // Only compare task ids + val taskIdComparator = Correspondence.transforming<ActivityManager.RecentTaskInfo, Int>( + { it?.taskId }, "has taskId of" + ) + assertThat(recentTaskInfoParcel.taskInfoList).comparingElementsUsing(taskIdComparator) + .containsExactly(1, 2, 3) + } + + private fun createTaskInfo(id: Int) = ActivityManager.RecentTaskInfo().apply { + taskId = id + token = WindowContainerToken(mock(IWindowContainerToken::class.java)) + } + + private fun singleTaskGroupInfo(): GroupedRecentTaskInfo { + val task = createTaskInfo(id = 1) + return GroupedRecentTaskInfo.forSingleTask(task) + } + + private fun splitTasksGroupInfo(): GroupedRecentTaskInfo { + val task1 = createTaskInfo(id = 1) + val task2 = createTaskInfo(id = 2) + val splitBounds = SplitBounds(Rect(), Rect(), 1, 2) + 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) + } +} 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 50f6bd7b4927..b8aaaa76e3c7 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 @@ -20,34 +20,46 @@ import static android.app.ActivityManager.RECENT_IGNORE_UNAVAILABLE; 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 org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.isA; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import static java.lang.Integer.MAX_VALUE; import android.app.ActivityManager; +import android.app.ActivityTaskManager; import android.content.Context; +import android.content.pm.PackageManager; import android.graphics.Rect; import android.view.SurfaceControl; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; +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.ShellExecutor; import com.android.wm.shell.common.TaskStackListenerImpl; +import com.android.wm.shell.desktopmode.DesktopModeStatus; +import com.android.wm.shell.desktopmode.DesktopModeTaskRepository; +import com.android.wm.shell.sysui.ShellCommandHandler; +import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.util.GroupedRecentTaskInfo; -import com.android.wm.shell.util.StagedSplitBounds; +import com.android.wm.shell.util.SplitBounds; import org.junit.Before; import org.junit.Test; @@ -56,7 +68,9 @@ import org.mockito.Mock; import java.util.ArrayList; import java.util.Arrays; +import java.util.List; import java.util.Optional; +import java.util.function.Consumer; /** * Tests for {@link RecentTasksController}. @@ -69,18 +83,41 @@ public class RecentTasksControllerTest extends ShellTestCase { private Context mContext; @Mock private TaskStackListenerImpl mTaskStackListener; + @Mock + private ShellCommandHandler mShellCommandHandler; + @Mock + private DesktopModeTaskRepository mDesktopModeTaskRepository; + @Mock + private ActivityTaskManager mActivityTaskManager; private ShellTaskOrganizer mShellTaskOrganizer; private RecentTasksController mRecentTasksController; - private ShellExecutor mMainExecutor; + private ShellInit mShellInit; + private TestShellExecutor mMainExecutor; @Before public void setUp() { mMainExecutor = new TestShellExecutor(); - mRecentTasksController = spy(new RecentTasksController(mContext, mTaskStackListener, - mMainExecutor)); - mShellTaskOrganizer = new ShellTaskOrganizer(mMainExecutor, mContext, - null /* sizeCompatUI */, Optional.of(mRecentTasksController)); + when(mContext.getPackageManager()).thenReturn(mock(PackageManager.class)); + mShellInit = spy(new ShellInit(mMainExecutor)); + mRecentTasksController = spy(new RecentTasksController(mContext, mShellInit, + mShellCommandHandler, mTaskStackListener, mActivityTaskManager, + Optional.of(mDesktopModeTaskRepository), mMainExecutor)); + mShellTaskOrganizer = new ShellTaskOrganizer(mShellInit, mShellCommandHandler, + null /* sizeCompatUI */, Optional.empty(), Optional.of(mRecentTasksController), + mMainExecutor); + mShellInit.init(); + } + + @Test + public void instantiateController_addInitCallback() { + verify(mShellInit, times(1)).addInitCallback(any(), isA(RecentTasksController.class)); + } + + @Test + public void instantiateController_addDumpCallback() { + verify(mShellCommandHandler, times(1)).addDumpCallback(any(), + isA(RecentTasksController.class)); } @Test @@ -89,7 +126,7 @@ public class RecentTasksControllerTest extends ShellTestCase { ActivityManager.RecentTaskInfo t2 = makeTaskInfo(2); setRawList(t1, t2); - mRecentTasksController.addSplitPair(t1.taskId, t2.taskId, mock(StagedSplitBounds.class)); + mRecentTasksController.addSplitPair(t1.taskId, t2.taskId, mock(SplitBounds.class)); verify(mRecentTasksController).notifyRecentTasksChanged(); reset(mRecentTasksController); @@ -104,10 +141,10 @@ public class RecentTasksControllerTest extends ShellTestCase { setRawList(t1, t2); // Verify only one update if the split info is the same - StagedSplitBounds bounds1 = new StagedSplitBounds(new Rect(0, 0, 50, 50), + SplitBounds bounds1 = new SplitBounds(new Rect(0, 0, 50, 50), new Rect(50, 50, 100, 100), t1.taskId, t2.taskId); mRecentTasksController.addSplitPair(t1.taskId, t2.taskId, bounds1); - StagedSplitBounds bounds2 = new StagedSplitBounds(new Rect(0, 0, 50, 50), + SplitBounds bounds2 = new SplitBounds(new Rect(0, 0, 50, 50), new Rect(50, 50, 100, 100), t1.taskId, t2.taskId); mRecentTasksController.addSplitPair(t1.taskId, t2.taskId, bounds2); verify(mRecentTasksController, times(1)).notifyRecentTasksChanged(); @@ -139,8 +176,8 @@ public class RecentTasksControllerTest extends ShellTestCase { setRawList(t1, t2, t3, t4, t5, t6); // Mark a couple pairs [t2, t4], [t3, t5] - StagedSplitBounds pair1Bounds = new StagedSplitBounds(new Rect(), new Rect(), 2, 4); - StagedSplitBounds pair2Bounds = new StagedSplitBounds(new Rect(), new Rect(), 3, 5); + SplitBounds pair1Bounds = new SplitBounds(new Rect(), new Rect(), 2, 4); + SplitBounds pair2Bounds = new SplitBounds(new Rect(), new Rect(), 3, 5); mRecentTasksController.addSplitPair(t2.taskId, t4.taskId, pair1Bounds); mRecentTasksController.addSplitPair(t3.taskId, t5.taskId, pair2Bounds); @@ -155,6 +192,77 @@ public class RecentTasksControllerTest extends ShellTestCase { } @Test + public void testGetRecentTasks_ReturnsRecentTasksAsynchronously() { + @SuppressWarnings("unchecked") + final List<GroupedRecentTaskInfo>[] recentTasks = new List[1]; + Consumer<List<GroupedRecentTaskInfo>> consumer = argument -> recentTasks[0] = argument; + ActivityManager.RecentTaskInfo t1 = makeTaskInfo(1); + ActivityManager.RecentTaskInfo t2 = makeTaskInfo(2); + ActivityManager.RecentTaskInfo t3 = makeTaskInfo(3); + ActivityManager.RecentTaskInfo t4 = makeTaskInfo(4); + ActivityManager.RecentTaskInfo t5 = makeTaskInfo(5); + ActivityManager.RecentTaskInfo t6 = makeTaskInfo(6); + setRawList(t1, t2, t3, t4, t5, t6); + + // Mark a couple pairs [t2, t4], [t3, t5] + SplitBounds pair1Bounds = new SplitBounds(new Rect(), new Rect(), 2, 4); + SplitBounds pair2Bounds = new SplitBounds(new Rect(), new Rect(), 3, 5); + + mRecentTasksController.addSplitPair(t2.taskId, t4.taskId, pair1Bounds); + mRecentTasksController.addSplitPair(t3.taskId, t5.taskId, pair2Bounds); + + mRecentTasksController.asRecentTasks() + .getRecentTasks(MAX_VALUE, RECENT_IGNORE_UNAVAILABLE, 0, Runnable::run, consumer); + mMainExecutor.flushAll(); + + assertGroupedTasksListEquals(recentTasks[0], + t1.taskId, -1, + t2.taskId, t4.taskId, + t3.taskId, t5.taskId, + t6.taskId, -1); + } + + @Test + public void testGetRecentTasks_groupActiveFreeformTasks() { + StaticMockitoSession mockitoSession = mockitoSession().mockStatic( + DesktopModeStatus.class).startMocking(); + when(DesktopModeStatus.isActive(any())).thenReturn(true); + + ActivityManager.RecentTaskInfo t1 = makeTaskInfo(1); + ActivityManager.RecentTaskInfo t2 = makeTaskInfo(2); + ActivityManager.RecentTaskInfo t3 = makeTaskInfo(3); + ActivityManager.RecentTaskInfo t4 = makeTaskInfo(4); + setRawList(t1, t2, t3, t4); + + when(mDesktopModeTaskRepository.isActiveTask(1)).thenReturn(true); + when(mDesktopModeTaskRepository.isActiveTask(3)).thenReturn(true); + + ArrayList<GroupedRecentTaskInfo> recentTasks = mRecentTasksController.getRecentTasks( + MAX_VALUE, RECENT_IGNORE_UNAVAILABLE, 0); + + // 2 freeform tasks should be grouped into one, 3 total recents entries + assertEquals(3, recentTasks.size()); + GroupedRecentTaskInfo freeformGroup = recentTasks.get(0); + GroupedRecentTaskInfo singleGroup1 = recentTasks.get(1); + GroupedRecentTaskInfo singleGroup2 = recentTasks.get(2); + + // Check that groups have expected types + assertEquals(GroupedRecentTaskInfo.TYPE_FREEFORM, freeformGroup.getType()); + assertEquals(GroupedRecentTaskInfo.TYPE_SINGLE, singleGroup1.getType()); + assertEquals(GroupedRecentTaskInfo.TYPE_SINGLE, singleGroup2.getType()); + + // Check freeform group entries + assertEquals(t1, freeformGroup.getTaskInfoList().get(0)); + assertEquals(t3, freeformGroup.getTaskInfoList().get(1)); + + // Check single entries + assertEquals(t2, singleGroup1.getTaskInfo1()); + assertEquals(t4, singleGroup2.getTaskInfo1()); + + mockitoSession.finishMocking(); + } + + @Test public void testRemovedTaskRemovesSplit() { ActivityManager.RecentTaskInfo t1 = makeTaskInfo(1); ActivityManager.RecentTaskInfo t2 = makeTaskInfo(2); @@ -162,7 +270,7 @@ public class RecentTasksControllerTest extends ShellTestCase { setRawList(t1, t2, t3); // Add a pair - StagedSplitBounds pair1Bounds = new StagedSplitBounds(new Rect(), new Rect(), 2, 3); + SplitBounds pair1Bounds = new SplitBounds(new Rect(), new Rect(), 2, 3); mRecentTasksController.addSplitPair(t2.taskId, t3.taskId, pair1Bounds); reset(mRecentTasksController); @@ -223,37 +331,39 @@ public class RecentTasksControllerTest extends ShellTestCase { for (ActivityManager.RecentTaskInfo task : tasks) { rawList.add(task); } - doReturn(rawList).when(mRecentTasksController).getRawRecentTasks(anyInt(), anyInt(), + doReturn(rawList).when(mActivityTaskManager).getRecentTasks(anyInt(), anyInt(), anyInt()); return rawList; } /** * Asserts that the recent tasks matches the given task ids. + * * @param expectedTaskIds list of task ids that map to the flattened task ids of the tasks in * the grouped task list */ - private void assertGroupedTasksListEquals(ArrayList<GroupedRecentTaskInfo> recentTasks, + private void assertGroupedTasksListEquals(List<GroupedRecentTaskInfo> recentTasks, int... expectedTaskIds) { int[] flattenedTaskIds = new int[recentTasks.size() * 2]; for (int i = 0; i < recentTasks.size(); i++) { GroupedRecentTaskInfo pair = recentTasks.get(i); - int taskId1 = pair.mTaskInfo1.taskId; + int taskId1 = pair.getTaskInfo1().taskId; flattenedTaskIds[2 * i] = taskId1; - flattenedTaskIds[2 * i + 1] = pair.mTaskInfo2 != null - ? pair.mTaskInfo2.taskId + flattenedTaskIds[2 * i + 1] = pair.getTaskInfo2() != null + ? pair.getTaskInfo2().taskId : -1; - if (pair.mTaskInfo2 != null) { - assertNotNull(pair.mStagedSplitBounds); - int leftTopTaskId = pair.mStagedSplitBounds.leftTopTaskId; - int bottomRightTaskId = pair.mStagedSplitBounds.rightBottomTaskId; + if (pair.getTaskInfo2() != null) { + assertNotNull(pair.getSplitBounds()); + int leftTopTaskId = pair.getSplitBounds().leftTopTaskId; + int bottomRightTaskId = pair.getSplitBounds().rightBottomTaskId; // Unclear if pairs are ordered by split position, most likely not. - assertTrue(leftTopTaskId == taskId1 || leftTopTaskId == pair.mTaskInfo2.taskId); + assertTrue(leftTopTaskId == taskId1 + || leftTopTaskId == pair.getTaskInfo2().taskId); assertTrue(bottomRightTaskId == taskId1 - || bottomRightTaskId == pair.mTaskInfo2.taskId); + || bottomRightTaskId == pair.getTaskInfo2().taskId); } else { - assertNull(pair.mStagedSplitBounds); + assertNull(pair.getSplitBounds()); } } assertTrue("Expected: " + Arrays.toString(expectedTaskIds) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/StagedSplitBoundsTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/SplitBoundsTest.java index ad73c56950bd..50d02ae0dccd 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/StagedSplitBoundsTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/SplitBoundsTest.java @@ -9,7 +9,8 @@ import android.graphics.Rect; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; -import com.android.wm.shell.util.StagedSplitBounds; +import com.android.wm.shell.ShellTestCase; +import com.android.wm.shell.util.SplitBounds; import org.junit.Before; import org.junit.Test; @@ -17,7 +18,7 @@ import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) @SmallTest -public class StagedSplitBoundsTest { +public class SplitBoundsTest extends ShellTestCase { private static final int DEVICE_WIDTH = 100; private static final int DEVICE_LENGTH = 200; private static final int DIVIDER_SIZE = 20; @@ -42,21 +43,21 @@ public class StagedSplitBoundsTest { @Test public void testVerticalStacked() { - StagedSplitBounds ssb = new StagedSplitBounds(mTopRect, mBottomRect, + SplitBounds ssb = new SplitBounds(mTopRect, mBottomRect, TASK_ID_1, TASK_ID_2); assertTrue(ssb.appsStackedVertically); } @Test public void testHorizontalStacked() { - StagedSplitBounds ssb = new StagedSplitBounds(mLeftRect, mRightRect, + SplitBounds ssb = new SplitBounds(mLeftRect, mRightRect, TASK_ID_1, TASK_ID_2); assertFalse(ssb.appsStackedVertically); } @Test public void testHorizontalDividerBounds() { - StagedSplitBounds ssb = new StagedSplitBounds(mTopRect, mBottomRect, + SplitBounds ssb = new SplitBounds(mTopRect, mBottomRect, TASK_ID_1, TASK_ID_2); Rect dividerBounds = ssb.visualDividerBounds; assertEquals(0, dividerBounds.left); @@ -67,7 +68,7 @@ public class StagedSplitBoundsTest { @Test public void testVerticalDividerBounds() { - StagedSplitBounds ssb = new StagedSplitBounds(mLeftRect, mRightRect, + SplitBounds ssb = new SplitBounds(mLeftRect, mRightRect, TASK_ID_1, TASK_ID_2); Rect dividerBounds = ssb.visualDividerBounds; assertEquals(DEVICE_WIDTH / 2 - DIVIDER_SIZE / 2, dividerBounds.left); @@ -78,7 +79,7 @@ public class StagedSplitBoundsTest { @Test public void testEqualVerticalTaskPercent() { - StagedSplitBounds ssb = new StagedSplitBounds(mTopRect, mBottomRect, + SplitBounds ssb = new SplitBounds(mTopRect, mBottomRect, TASK_ID_1, TASK_ID_2); float topPercentSpaceTaken = (float) (DEVICE_LENGTH / 2 - DIVIDER_SIZE / 2) / DEVICE_LENGTH; assertEquals(topPercentSpaceTaken, ssb.topTaskPercent, 0.01); @@ -86,7 +87,7 @@ public class StagedSplitBoundsTest { @Test public void testEqualHorizontalTaskPercent() { - StagedSplitBounds ssb = new StagedSplitBounds(mLeftRect, mRightRect, + SplitBounds ssb = new SplitBounds(mLeftRect, mRightRect, TASK_ID_1, TASK_ID_2); float leftPercentSpaceTaken = (float) (DEVICE_WIDTH / 2 - DIVIDER_SIZE / 2) / DEVICE_WIDTH; assertEquals(leftPercentSpaceTaken, ssb.leftTaskPercent, 0.01); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/MainStageTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/MainStageTests.java index 0639ad5d0a62..68cb57c14d8c 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/MainStageTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/MainStageTests.java @@ -61,7 +61,7 @@ public class MainStageTests extends ShellTestCase { MockitoAnnotations.initMocks(this); mRootTaskInfo = new TestRunningTaskInfoBuilder().build(); mMainStage = new MainStage(mContext, mTaskOrganizer, DEFAULT_DISPLAY, mCallbacks, - mSyncQueue, mSurfaceSession, mIconProvider, null); + mSyncQueue, mSurfaceSession, mIconProvider); mMainStage.onTaskAppeared(mRootTaskInfo, mRootLeash); } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SideStageTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SideStageTests.java index a31aa58bdc26..3b42a48b5a40 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SideStageTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SideStageTests.java @@ -66,7 +66,7 @@ public class SideStageTests extends ShellTestCase { MockitoAnnotations.initMocks(this); mRootTask = new TestRunningTaskInfoBuilder().build(); mSideStage = new SideStage(mContext, mTaskOrganizer, DEFAULT_DISPLAY, mCallbacks, - mSyncQueue, mSurfaceSession, mIconProvider, null); + mSyncQueue, mSurfaceSession, mIconProvider); mSideStage.onTaskAppeared(mRootTask, mRootLeash); } 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 new file mode 100644 index 000000000000..5a68361c595c --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java @@ -0,0 +1,210 @@ +/* + * 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.splitscreen; + +import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; +import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; + +import static 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 org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.ActivityManager; +import android.content.ComponentName; +import android.content.Intent; +import android.content.pm.ActivityInfo; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import com.android.launcher3.icons.IconProvider; +import com.android.wm.shell.RootTaskDisplayAreaOrganizer; +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.ShellTestCase; +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.ShellExecutor; +import com.android.wm.shell.common.SyncTransactionQueue; +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.sysui.ShellCommandHandler; +import com.android.wm.shell.sysui.ShellController; +import com.android.wm.shell.sysui.ShellInit; +import com.android.wm.shell.transition.Transitions; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.Optional; + +/** + * Tests for {@link SplitScreenController} + */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class SplitScreenControllerTests extends ShellTestCase { + + @Mock ShellInit mShellInit; + @Mock ShellController mShellController; + @Mock ShellCommandHandler mShellCommandHandler; + @Mock ShellTaskOrganizer mTaskOrganizer; + @Mock SyncTransactionQueue mSyncQueue; + @Mock RootTaskDisplayAreaOrganizer mRootTDAOrganizer; + @Mock ShellExecutor mMainExecutor; + @Mock DisplayController mDisplayController; + @Mock DisplayImeController mDisplayImeController; + @Mock DisplayInsetsController mDisplayInsetsController; + @Mock DragAndDropController mDragAndDropController; + @Mock Transitions mTransitions; + @Mock TransactionPool mTransactionPool; + @Mock IconProvider mIconProvider; + @Mock Optional<RecentTasksController> mRecentTasks; + + private SplitScreenController mSplitScreenController; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + mSplitScreenController = spy(new SplitScreenController(mContext, mShellInit, + mShellCommandHandler, mShellController, mTaskOrganizer, mSyncQueue, + mRootTDAOrganizer, mDisplayController, mDisplayImeController, + mDisplayInsetsController, mDragAndDropController, mTransitions, mTransactionPool, + mIconProvider, mRecentTasks, mMainExecutor)); + } + + @Test + public void instantiateController_addInitCallback() { + verify(mShellInit, times(1)).addInitCallback(any(), any()); + } + + @Test + public void instantiateController_registerDumpCallback() { + doReturn(mMainExecutor).when(mTaskOrganizer).getExecutor(); + when(mDisplayController.getDisplayLayout(anyInt())).thenReturn(new DisplayLayout()); + mSplitScreenController.onInit(); + verify(mShellCommandHandler, times(1)).addDumpCallback(any(), any()); + } + + @Test + public void instantiateController_registerCommandCallback() { + doReturn(mMainExecutor).when(mTaskOrganizer).getExecutor(); + when(mDisplayController.getDisplayLayout(anyInt())).thenReturn(new DisplayLayout()); + mSplitScreenController.onInit(); + verify(mShellCommandHandler, times(1)).addCommandCallback(eq("splitscreen"), any(), any()); + } + + @Test + public void testControllerRegistersKeyguardChangeListener() { + doReturn(mMainExecutor).when(mTaskOrganizer).getExecutor(); + when(mDisplayController.getDisplayLayout(anyInt())).thenReturn(new DisplayLayout()); + mSplitScreenController.onInit(); + verify(mShellController, times(1)).addKeyguardChangeListener(any()); + } + + @Test + public void testShouldAddMultipleTaskFlag_notInSplitScreen() { + doReturn(false).when(mSplitScreenController).isSplitScreenVisible(); + doReturn(true).when(mSplitScreenController).isValidToEnterSplitScreen(any()); + + // Verify launching the same activity returns true. + Intent startIntent = createStartIntent("startActivity"); + ActivityManager.RunningTaskInfo focusTaskInfo = + createTaskInfo(WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, startIntent); + doReturn(focusTaskInfo).when(mSplitScreenController).getFocusingTaskInfo(); + assertTrue(mSplitScreenController.shouldAddMultipleTaskFlag( + startIntent, SPLIT_POSITION_TOP_OR_LEFT)); + + // Verify launching different activity returns false. + Intent diffIntent = createStartIntent("diffActivity"); + focusTaskInfo = + createTaskInfo(WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, diffIntent); + doReturn(focusTaskInfo).when(mSplitScreenController).getFocusingTaskInfo(); + assertFalse(mSplitScreenController.shouldAddMultipleTaskFlag( + startIntent, SPLIT_POSITION_TOP_OR_LEFT)); + } + + @Test + public void testShouldAddMultipleTaskFlag_inSplitScreen() { + doReturn(true).when(mSplitScreenController).isSplitScreenVisible(); + Intent startIntent = createStartIntent("startActivity"); + ActivityManager.RunningTaskInfo sameTaskInfo = + createTaskInfo(WINDOWING_MODE_MULTI_WINDOW, ACTIVITY_TYPE_STANDARD, startIntent); + Intent diffIntent = createStartIntent("diffActivity"); + ActivityManager.RunningTaskInfo differentTaskInfo = + createTaskInfo(WINDOWING_MODE_MULTI_WINDOW, ACTIVITY_TYPE_STANDARD, diffIntent); + + // Verify launching the same activity return false. + doReturn(sameTaskInfo).when(mSplitScreenController) + .getTaskInfo(SPLIT_POSITION_TOP_OR_LEFT); + assertFalse(mSplitScreenController.shouldAddMultipleTaskFlag( + startIntent, SPLIT_POSITION_TOP_OR_LEFT)); + + // Verify launching the same activity as adjacent returns true. + doReturn(differentTaskInfo).when(mSplitScreenController) + .getTaskInfo(SPLIT_POSITION_TOP_OR_LEFT); + doReturn(sameTaskInfo).when(mSplitScreenController) + .getTaskInfo(SPLIT_POSITION_BOTTOM_OR_RIGHT); + assertTrue(mSplitScreenController.shouldAddMultipleTaskFlag( + startIntent, SPLIT_POSITION_TOP_OR_LEFT)); + + // Verify launching different activity from adjacent returns false. + doReturn(differentTaskInfo).when(mSplitScreenController) + .getTaskInfo(SPLIT_POSITION_TOP_OR_LEFT); + doReturn(differentTaskInfo).when(mSplitScreenController) + .getTaskInfo(SPLIT_POSITION_BOTTOM_OR_RIGHT); + assertFalse(mSplitScreenController.shouldAddMultipleTaskFlag( + startIntent, SPLIT_POSITION_TOP_OR_LEFT)); + } + + private Intent createStartIntent(String activityName) { + Intent intent = new Intent(); + intent.setComponent(new ComponentName(mContext, activityName)); + return intent; + } + + private ActivityManager.RunningTaskInfo createTaskInfo(int winMode, int actType, + Intent strIntent) { + ActivityManager.RunningTaskInfo info = new ActivityManager.RunningTaskInfo(); + info.configuration.windowConfiguration.setActivityType(actType); + info.configuration.windowConfiguration.setWindowingMode(winMode); + info.supportsMultiWindow = true; + info.baseIntent = strIntent; + info.baseActivity = strIntent.getComponent(); + ActivityInfo activityInfo = new ActivityInfo(); + activityInfo.packageName = info.baseActivity.getPackageName(); + activityInfo.name = info.baseActivity.getClassName(); + info.topActivityInfo = activityInfo; + return info; + } +} 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 eb9d3a11d285..ae69b3ddd042 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 @@ -40,8 +40,6 @@ import com.android.wm.shell.transition.Transitions; import java.util.Optional; -import javax.inject.Provider; - public class SplitTestUtils { static SplitLayout createMockSplitLayout() { @@ -73,13 +71,11 @@ public class SplitTestUtils { DisplayController displayController, DisplayImeController imeController, DisplayInsetsController insetsController, SplitLayout splitLayout, Transitions transitions, TransactionPool transactionPool, - SplitscreenEventLogger logger, ShellExecutor mainExecutor, - Optional<RecentTasksController> recentTasks, - Provider<Optional<StageTaskUnfoldController>> unfoldController) { + ShellExecutor mainExecutor, + Optional<RecentTasksController> recentTasks) { super(context, displayId, syncQueue, taskOrganizer, mainStage, sideStage, displayController, imeController, insetsController, splitLayout, - transitions, transactionPool, logger, mainExecutor, recentTasks, - unfoldController); + transitions, transactionPool, mainExecutor, recentTasks); // Prepare root task for testing. mRootTask = new TestRunningTaskInfoBuilder().build(); 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 ffaab652aa99..ea0033ba4bbb 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 @@ -95,7 +95,6 @@ public class SplitTransitionTests extends ShellTestCase { @Mock private TransactionPool mTransactionPool; @Mock private Transitions mTransitions; @Mock private SurfaceSession mSurfaceSession; - @Mock private SplitscreenEventLogger mLogger; @Mock private IconProvider mIconProvider; @Mock private ShellExecutor mMainExecutor; private SplitLayout mSplitLayout; @@ -118,16 +117,16 @@ public class SplitTransitionTests extends ShellTestCase { mSplitLayout = SplitTestUtils.createMockSplitLayout(); mMainStage = new MainStage(mContext, mTaskOrganizer, DEFAULT_DISPLAY, mock( StageTaskListener.StageListenerCallbacks.class), mSyncQueue, mSurfaceSession, - mIconProvider, null); + mIconProvider); mMainStage.onTaskAppeared(new TestRunningTaskInfoBuilder().build(), createMockSurface()); mSideStage = new SideStage(mContext, mTaskOrganizer, DEFAULT_DISPLAY, mock( StageTaskListener.StageListenerCallbacks.class), mSyncQueue, mSurfaceSession, - mIconProvider, null); + mIconProvider); mSideStage.onTaskAppeared(new TestRunningTaskInfoBuilder().build(), createMockSurface()); mStageCoordinator = new SplitTestUtils.TestStageCoordinator(mContext, DEFAULT_DISPLAY, mSyncQueue, mTaskOrganizer, mMainStage, mSideStage, mDisplayController, mDisplayImeController, mDisplayInsetsController, mSplitLayout, mTransitions, - mTransactionPool, mLogger, mMainExecutor, Optional.empty(), Optional::empty); + mTransactionPool, mMainExecutor, Optional.empty()); mSplitScreenTransitions = mStageCoordinator.getSplitTransitions(); doAnswer((Answer<IBinder>) invocation -> mock(IBinder.class)) .when(mTransitions).startTransition(anyInt(), any(), any()); @@ -182,7 +181,7 @@ public class SplitTransitionTests extends ShellTestCase { IBinder transition = mSplitScreenTransitions.startEnterTransition( TRANSIT_SPLIT_SCREEN_PAIR_OPEN, new WindowContainerTransaction(), - new RemoteTransition(testRemote), mStageCoordinator); + new RemoteTransition(testRemote), mStageCoordinator, null); mMainStage.onTaskAppeared(mMainChild, createMockSurface()); mSideStage.onTaskAppeared(mSideChild, createMockSurface()); boolean accepted = mStageCoordinator.startAnimation(transition, info, @@ -340,7 +339,7 @@ public class SplitTransitionTests extends ShellTestCase { TransitionInfo info = new TransitionInfo(TRANSIT_TO_BACK, 0); info.addChange(mainChange); info.addChange(sideChange); - IBinder transition = mSplitScreenTransitions.startDismissTransition(null, + IBinder transition = mSplitScreenTransitions.startDismissTransition( new WindowContainerTransaction(), mStageCoordinator, EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW, STAGE_TYPE_SIDE); boolean accepted = mStageCoordinator.startAnimation(transition, info, @@ -363,7 +362,7 @@ public class SplitTransitionTests extends ShellTestCase { TransitionInfo info = new TransitionInfo(TRANSIT_TO_BACK, 0); info.addChange(mainChange); info.addChange(sideChange); - IBinder transition = mSplitScreenTransitions.startDismissTransition(null, + IBinder transition = mSplitScreenTransitions.startDismissTransition( new WindowContainerTransaction(), mStageCoordinator, EXIT_REASON_DRAG_DIVIDER, STAGE_TYPE_SIDE); mMainStage.onTaskVanished(mMainChild); @@ -422,7 +421,7 @@ public class SplitTransitionTests extends ShellTestCase { TransitionInfo enterInfo = createEnterPairInfo(); IBinder enterTransit = mSplitScreenTransitions.startEnterTransition( TRANSIT_SPLIT_SCREEN_PAIR_OPEN, new WindowContainerTransaction(), - new RemoteTransition(new TestRemoteTransition()), mStageCoordinator); + new RemoteTransition(new TestRemoteTransition()), mStageCoordinator, null); mMainStage.onTaskAppeared(mMainChild, createMockSurface()); mSideStage.onTaskAppeared(mSideChild, createMockSurface()); mStageCoordinator.startAnimation(enterTransit, enterInfo, 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 42d998f6b0ee..835087007b30 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java @@ -16,7 +16,10 @@ package com.android.wm.shell.splitscreen; +import static android.app.ActivityOptions.KEY_LAUNCH_ROOT_TASK_TOKEN; import static android.app.ActivityTaskManager.INVALID_TASK_ID; +import static android.app.ComponentOptions.KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED; +import static android.app.ComponentOptions.KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED_BY_PERMISSION; import static android.view.Display.DEFAULT_DISPLAY; import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; @@ -28,12 +31,12 @@ 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 org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.clearInvocations; -import static org.mockito.Mockito.doNothing; 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; @@ -41,8 +44,10 @@ import static org.mockito.Mockito.when; import android.app.ActivityManager; import android.content.res.Configuration; import android.graphics.Rect; +import android.os.Bundle; import android.view.SurfaceControl; import android.view.SurfaceSession; +import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -58,6 +63,7 @@ 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.splitscreen.SplitScreen.SplitScreenListener; import com.android.wm.shell.transition.Transitions; import org.junit.Before; @@ -68,8 +74,6 @@ import org.mockito.MockitoAnnotations; import java.util.Optional; -import javax.inject.Provider; - /** * Tests for {@link StageCoordinator} */ @@ -85,10 +89,6 @@ public class StageCoordinatorTests extends ShellTestCase { @Mock private SideStage mSideStage; @Mock - private StageTaskUnfoldController mMainUnfoldController; - @Mock - private StageTaskUnfoldController mSideUnfoldController; - @Mock private SplitLayout mSplitLayout; @Mock private DisplayController mDisplayController; @@ -101,12 +101,11 @@ public class StageCoordinatorTests extends ShellTestCase { @Mock private TransactionPool mTransactionPool; @Mock - private SplitscreenEventLogger mLogger; - @Mock private ShellExecutor mMainExecutor; private final Rect mBounds1 = new Rect(10, 20, 30, 40); 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; @@ -118,17 +117,20 @@ public class StageCoordinatorTests extends ShellTestCase { MockitoAnnotations.initMocks(this); mStageCoordinator = spy(new StageCoordinator(mContext, DEFAULT_DISPLAY, mSyncQueue, mTaskOrganizer, mMainStage, mSideStage, mDisplayController, mDisplayImeController, - mDisplayInsetsController, mSplitLayout, mTransitions, mTransactionPool, mLogger, - mMainExecutor, Optional.empty(), new UnfoldControllerProvider())); - doNothing().when(mStageCoordinator).updateActivityOptions(any(), anyInt()); + mDisplayInsetsController, mSplitLayout, mTransitions, mTransactionPool, + mMainExecutor, Optional.empty())); when(mSplitLayout.getBounds1()).thenReturn(mBounds1); when(mSplitLayout.getBounds2()).thenReturn(mBounds2); + when(mSplitLayout.getRootBounds()).thenReturn(mRootBounds); when(mSplitLayout.isLandscape()).thenReturn(false); mRootTask = new TestRunningTaskInfoBuilder().build(); mRootLeash = new SurfaceControl.Builder(mSurfaceSession).setName("test").build(); mStageCoordinator.onTaskAppeared(mRootTask, mRootLeash); + + mSideStage.mRootTaskInfo = new TestRunningTaskInfoBuilder().build(); + mMainStage.mRootTaskInfo = new TestRunningTaskInfoBuilder().build(); } @Test @@ -168,13 +170,6 @@ public class StageCoordinatorTests extends ShellTestCase { } @Test - public void testRootTaskAppeared_initializesUnfoldControllers() { - verify(mMainUnfoldController).init(); - verify(mSideUnfoldController).init(); - verify(mStageCoordinator).onRootTaskAppeared(); - } - - @Test public void testRootTaskInfoChanged_updatesSplitLayout() { mStageCoordinator.onTaskInfoChanged(mRootTask); @@ -184,26 +179,25 @@ public class StageCoordinatorTests extends ShellTestCase { @Test public void testLayoutChanged_topLeftSplitPosition_updatesUnfoldStageBounds() { mStageCoordinator.setSideStagePosition(SPLIT_POSITION_TOP_OR_LEFT, null); - clearInvocations(mMainUnfoldController, mSideUnfoldController); + final SplitScreenListener listener = mock(SplitScreenListener.class); + mStageCoordinator.registerSplitScreenListener(listener); + clearInvocations(listener); mStageCoordinator.onLayoutSizeChanged(mSplitLayout); - verify(mMainUnfoldController).onLayoutChanged(mBounds2, SPLIT_POSITION_BOTTOM_OR_RIGHT, - false); - verify(mSideUnfoldController).onLayoutChanged(mBounds1, SPLIT_POSITION_TOP_OR_LEFT, false); + verify(listener).onSplitBoundsChanged(mRootBounds, mBounds2, mBounds1); } @Test public void testLayoutChanged_bottomRightSplitPosition_updatesUnfoldStageBounds() { mStageCoordinator.setSideStagePosition(SPLIT_POSITION_BOTTOM_OR_RIGHT, null); - clearInvocations(mMainUnfoldController, mSideUnfoldController); + final SplitScreenListener listener = mock(SplitScreenListener.class); + mStageCoordinator.registerSplitScreenListener(listener); + clearInvocations(listener); mStageCoordinator.onLayoutSizeChanged(mSplitLayout); - verify(mMainUnfoldController).onLayoutChanged(mBounds1, SPLIT_POSITION_TOP_OR_LEFT, - false); - verify(mSideUnfoldController).onLayoutChanged(mBounds2, SPLIT_POSITION_BOTTOM_OR_RIGHT, - false); + verify(listener).onSplitBoundsChanged(mRootBounds, mBounds1, mBounds2); } @Test @@ -234,8 +228,7 @@ public class StageCoordinatorTests extends ShellTestCase { mStageCoordinator.exitSplitScreen(testTaskId, EXIT_REASON_RETURN_HOME); verify(mMainStage).reorderChild(eq(testTaskId), eq(true), any(WindowContainerTransaction.class)); - verify(mSideStage).removeAllTasks(any(WindowContainerTransaction.class), eq(false)); - verify(mMainStage).deactivate(any(WindowContainerTransaction.class), eq(true)); + verify(mMainStage).resetBounds(any(WindowContainerTransaction.class)); } @Test @@ -247,8 +240,7 @@ public class StageCoordinatorTests extends ShellTestCase { mStageCoordinator.exitSplitScreen(testTaskId, EXIT_REASON_RETURN_HOME); verify(mSideStage).reorderChild(eq(testTaskId), eq(true), any(WindowContainerTransaction.class)); - verify(mSideStage).removeAllTasks(any(WindowContainerTransaction.class), eq(true)); - verify(mMainStage).deactivate(any(WindowContainerTransaction.class), eq(false)); + verify(mSideStage).resetBounds(any(WindowContainerTransaction.class)); } @Test @@ -315,19 +307,15 @@ public class StageCoordinatorTests extends ShellTestCase { verify(mSplitLayout).applySurfaceChanges(any(), any(), any(), any(), any(), eq(false)); } - private class UnfoldControllerProvider implements - Provider<Optional<StageTaskUnfoldController>> { - - private boolean isMain = true; - - @Override - public Optional<StageTaskUnfoldController> get() { - if (isMain) { - isMain = false; - return Optional.of(mMainUnfoldController); - } else { - return Optional.of(mSideUnfoldController); - } - } + @Test + public void testAddActivityOptions_addsBackgroundActivitiesFlags() { + Bundle options = mStageCoordinator.resolveStartStage(STAGE_TYPE_MAIN, + SPLIT_POSITION_UNDEFINED, null /* options */, null /* wct */); + + assertEquals(options.getParcelable(KEY_LAUNCH_ROOT_TASK_TOKEN, WindowContainerToken.class), + mMainStage.mRootTaskInfo.token); + assertTrue(options.getBoolean(KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED)); + assertTrue(options.getBoolean( + KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED_BY_PERMISSION)); } } 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 157c30bcb6c7..5ee8bf3006a3 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,7 +25,6 @@ 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.Mockito.clearInvocations; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -72,8 +71,6 @@ public final class StageTaskListenerTests extends ShellTestCase { private SyncTransactionQueue mSyncQueue; @Mock private IconProvider mIconProvider; - @Mock - private StageTaskUnfoldController mStageTaskUnfoldController; @Captor private ArgumentCaptor<SyncTransactionQueue.TransactionRunnable> mRunnableCaptor; private SurfaceSession mSurfaceSession = new SurfaceSession(); @@ -92,8 +89,7 @@ public final class StageTaskListenerTests extends ShellTestCase { mCallbacks, mSyncQueue, mSurfaceSession, - mIconProvider, - mStageTaskUnfoldController); + mIconProvider); mRootTask = new TestRunningTaskInfoBuilder().build(); mRootTask.parentTaskId = INVALID_TASK_ID; mSurfaceControl = new SurfaceControl.Builder(mSurfaceSession).setName("test").build(); @@ -130,30 +126,6 @@ public final class StageTaskListenerTests extends ShellTestCase { verify(mCallbacks).onStatusChanged(eq(mRootTask.isVisible), eq(true)); } - @Test - public void testTaskAppeared_notifiesUnfoldListener() { - assumeFalse(ENABLE_SHELL_TRANSITIONS); - final ActivityManager.RunningTaskInfo task = - new TestRunningTaskInfoBuilder().setParentTaskId(mRootTask.taskId).build(); - - mStageTaskListener.onTaskAppeared(task, mSurfaceControl); - - verify(mStageTaskUnfoldController).onTaskAppeared(eq(task), eq(mSurfaceControl)); - } - - @Test - public void testTaskVanished_notifiesUnfoldListener() { - assumeFalse(ENABLE_SHELL_TRANSITIONS); - final ActivityManager.RunningTaskInfo task = - new TestRunningTaskInfoBuilder().setParentTaskId(mRootTask.taskId).build(); - mStageTaskListener.onTaskAppeared(task, mSurfaceControl); - clearInvocations(mStageTaskUnfoldController); - - mStageTaskListener.onTaskVanished(task); - - verify(mStageTaskUnfoldController).onTaskVanished(eq(task)); - } - @Test(expected = IllegalArgumentException.class) public void testUnknownTaskVanished() { final ActivityManager.RunningTaskInfo task = new TestRunningTaskInfoBuilder().build(); 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 630d0d2c827c..e5ae2962e6e4 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 @@ -73,6 +73,7 @@ import androidx.test.filters.SmallTest; import androidx.test.platform.app.InstrumentationRegistry; 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; @@ -91,7 +92,7 @@ import java.util.function.IntSupplier; */ @SmallTest @RunWith(AndroidJUnit4.class) -public class StartingSurfaceDrawerTests { +public class StartingSurfaceDrawerTests extends ShellTestCase { @Mock private IBinder mBinder; @Mock @@ -249,7 +250,8 @@ public class StartingSurfaceDrawerTests { any() /* window */, any() /* attrs */, anyInt() /* viewVisibility */, anyInt() /* displayId */, any() /* requestedVisibility */, any() /* outInputChannel */, - any() /* outInsetsState */, any() /* outActiveControls */); + any() /* outInsetsState */, any() /* outActiveControls */, + any() /* outAttachedFrame */, any() /* outSizeCompatScale */); TaskSnapshotWindow mockSnapshotWindow = TaskSnapshotWindow.create(windowInfo, mBinder, snapshot, mTestExecutor, () -> { 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 new file mode 100644 index 000000000000..35515e3bb6e8 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingWindowControllerTests.java @@ -0,0 +1,80 @@ +/* + * 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.startingsurface; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import android.content.Context; +import android.hardware.display.DisplayManager; +import android.view.Display; + +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.common.ShellExecutor; +import com.android.wm.shell.common.TransactionPool; +import com.android.wm.shell.sysui.ShellInit; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** + * Tests for the starting window controller. + * + * Build/Install/Run: + * atest WMShellUnitTests:StartingWindowControllerTests + */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class StartingWindowControllerTests extends ShellTestCase { + + private @Mock Context mContext; + private @Mock DisplayManager mDisplayManager; + private @Mock ShellInit mShellInit; + private @Mock ShellTaskOrganizer mTaskOrganizer; + private @Mock ShellExecutor mMainExecutor; + private @Mock StartingWindowTypeAlgorithm mTypeAlgorithm; + private @Mock IconProvider mIconProvider; + private @Mock TransactionPool mTransactionPool; + private StartingWindowController mController; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + doReturn(mock(Display.class)).when(mDisplayManager).getDisplay(anyInt()); + doReturn(mDisplayManager).when(mContext).getSystemService(eq(DisplayManager.class)); + mController = new StartingWindowController(mContext, mShellInit, mTaskOrganizer, + mMainExecutor, mTypeAlgorithm, mIconProvider, mTransactionPool); + } + + @Test + public void instantiate_addInitCallback() { + verify(mShellInit, times(1)).addInitCallback(any(), any()); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/TaskSnapshotWindowTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/TaskSnapshotWindowTest.java index 78e27c956807..3de50bb60470 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/TaskSnapshotWindowTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/TaskSnapshotWindowTest.java @@ -47,6 +47,7 @@ import android.window.TaskSnapshot; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; +import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.TestShellExecutor; import org.junit.Test; @@ -58,7 +59,7 @@ import org.junit.runner.RunWith; */ @SmallTest @RunWith(AndroidJUnit4.class) -public class TaskSnapshotWindowTest { +public class TaskSnapshotWindowTest extends ShellTestCase { private TaskSnapshotWindow mWindow; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sysui/ShellControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sysui/ShellControllerTest.java new file mode 100644 index 000000000000..d6ddba9e927d --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sysui/ShellControllerTest.java @@ -0,0 +1,411 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.sysui; + +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; + +import android.content.Context; +import android.content.pm.UserInfo; +import android.content.res.Configuration; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; + +import androidx.annotation.NonNull; +import androidx.test.filters.SmallTest; +import androidx.test.platform.app.InstrumentationRegistry; + +import com.android.wm.shell.ShellTestCase; +import com.android.wm.shell.common.ShellExecutor; + +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 java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +@SmallTest +@RunWith(AndroidTestingRunner.class) +@TestableLooper.RunWithLooper(setAsMainLooper = true) +public class ShellControllerTest extends ShellTestCase { + + private static final int TEST_USER_ID = 100; + + @Mock + private ShellInit mShellInit; + @Mock + private ShellCommandHandler mShellCommandHandler; + @Mock + private ShellExecutor mExecutor; + @Mock + private Context mTestUserContext; + + private ShellController mController; + private TestConfigurationChangeListener mConfigChangeListener; + private TestKeyguardChangeListener mKeyguardChangeListener; + private TestUserChangeListener mUserChangeListener; + + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mKeyguardChangeListener = new TestKeyguardChangeListener(); + mConfigChangeListener = new TestConfigurationChangeListener(); + mUserChangeListener = new TestUserChangeListener(); + mController = new ShellController(mShellInit, mShellCommandHandler, mExecutor); + mController.onConfigurationChanged(getConfigurationCopy()); + } + + @After + public void tearDown() { + // Do nothing + } + + @Test + public void testAddUserChangeListener_ensureCallback() { + mController.addUserChangeListener(mUserChangeListener); + + mController.onUserChanged(TEST_USER_ID, mTestUserContext); + assertTrue(mUserChangeListener.userChanged == 1); + assertTrue(mUserChangeListener.lastUserContext == mTestUserContext); + } + + @Test + public void testDoubleAddUserChangeListener_ensureSingleCallback() { + mController.addUserChangeListener(mUserChangeListener); + mController.addUserChangeListener(mUserChangeListener); + + mController.onUserChanged(TEST_USER_ID, mTestUserContext); + assertTrue(mUserChangeListener.userChanged == 1); + assertTrue(mUserChangeListener.lastUserContext == mTestUserContext); + } + + @Test + public void testAddRemoveUserChangeListener_ensureNoCallback() { + mController.addUserChangeListener(mUserChangeListener); + mController.removeUserChangeListener(mUserChangeListener); + + mController.onUserChanged(TEST_USER_ID, mTestUserContext); + assertTrue(mUserChangeListener.userChanged == 0); + assertTrue(mUserChangeListener.lastUserContext == null); + } + + @Test + public void testUserProfilesChanged() { + mController.addUserChangeListener(mUserChangeListener); + + ArrayList<UserInfo> profiles = new ArrayList<>(); + profiles.add(mock(UserInfo.class)); + profiles.add(mock(UserInfo.class)); + mController.onUserProfilesChanged(profiles); + assertTrue(mUserChangeListener.lastUserProfiles.equals(profiles)); + } + + @Test + public void testAddKeyguardChangeListener_ensureCallback() { + mController.addKeyguardChangeListener(mKeyguardChangeListener); + + mController.onKeyguardVisibilityChanged(true, false, false); + assertTrue(mKeyguardChangeListener.visibilityChanged == 1); + assertTrue(mKeyguardChangeListener.dismissAnimationFinished == 0); + } + + @Test + public void testDoubleAddKeyguardChangeListener_ensureSingleCallback() { + mController.addKeyguardChangeListener(mKeyguardChangeListener); + mController.addKeyguardChangeListener(mKeyguardChangeListener); + + mController.onKeyguardVisibilityChanged(true, false, false); + assertTrue(mKeyguardChangeListener.visibilityChanged == 1); + assertTrue(mKeyguardChangeListener.dismissAnimationFinished == 0); + } + + @Test + public void testAddRemoveKeyguardChangeListener_ensureNoCallback() { + mController.addKeyguardChangeListener(mKeyguardChangeListener); + mController.removeKeyguardChangeListener(mKeyguardChangeListener); + + mController.onKeyguardVisibilityChanged(true, false, false); + assertTrue(mKeyguardChangeListener.visibilityChanged == 0); + assertTrue(mKeyguardChangeListener.dismissAnimationFinished == 0); + } + + @Test + public void testKeyguardVisibilityChanged() { + mController.addKeyguardChangeListener(mKeyguardChangeListener); + + mController.onKeyguardVisibilityChanged(true, true, true); + assertTrue(mKeyguardChangeListener.visibilityChanged == 1); + assertTrue(mKeyguardChangeListener.lastAnimatingDismiss); + assertTrue(mKeyguardChangeListener.lastOccluded); + assertTrue(mKeyguardChangeListener.lastAnimatingDismiss); + assertTrue(mKeyguardChangeListener.dismissAnimationFinished == 0); + } + + @Test + public void testKeyguardDismissAnimationFinished() { + mController.addKeyguardChangeListener(mKeyguardChangeListener); + + mController.onKeyguardDismissAnimationFinished(); + assertTrue(mKeyguardChangeListener.visibilityChanged == 0); + assertTrue(mKeyguardChangeListener.dismissAnimationFinished == 1); + } + + @Test + public void testAddConfigurationChangeListener_ensureCallback() { + mController.addConfigurationChangeListener(mConfigChangeListener); + + Configuration newConfig = getConfigurationCopy(); + newConfig.densityDpi = 200; + mController.onConfigurationChanged(newConfig); + assertTrue(mConfigChangeListener.configChanges == 1); + } + + @Test + public void testDoubleAddConfigurationChangeListener_ensureSingleCallback() { + mController.addConfigurationChangeListener(mConfigChangeListener); + mController.addConfigurationChangeListener(mConfigChangeListener); + + Configuration newConfig = getConfigurationCopy(); + newConfig.densityDpi = 200; + mController.onConfigurationChanged(newConfig); + assertTrue(mConfigChangeListener.configChanges == 1); + } + + @Test + public void testAddRemoveConfigurationChangeListener_ensureNoCallback() { + mController.addConfigurationChangeListener(mConfigChangeListener); + mController.removeConfigurationChangeListener(mConfigChangeListener); + + Configuration newConfig = getConfigurationCopy(); + newConfig.densityDpi = 200; + mController.onConfigurationChanged(newConfig); + assertTrue(mConfigChangeListener.configChanges == 0); + } + + @Test + public void testMultipleConfigurationChangeListeners() { + TestConfigurationChangeListener listener2 = new TestConfigurationChangeListener(); + mController.addConfigurationChangeListener(mConfigChangeListener); + mController.addConfigurationChangeListener(listener2); + + Configuration newConfig = getConfigurationCopy(); + newConfig.densityDpi = 200; + mController.onConfigurationChanged(newConfig); + assertTrue(mConfigChangeListener.configChanges == 1); + assertTrue(listener2.configChanges == 1); + } + + @Test + public void testRemoveListenerDuringCallback() { + TestConfigurationChangeListener badListener = new TestConfigurationChangeListener() { + @Override + public void onConfigurationChanged(Configuration newConfiguration) { + mController.removeConfigurationChangeListener(this); + } + }; + mController.addConfigurationChangeListener(badListener); + mController.addConfigurationChangeListener(mConfigChangeListener); + + // Ensure we don't fail just because a listener was removed mid-callback + Configuration newConfig = getConfigurationCopy(); + newConfig.densityDpi = 200; + mController.onConfigurationChanged(newConfig); + } + + @Test + public void testDensityChangeCallback() { + mController.addConfigurationChangeListener(mConfigChangeListener); + + Configuration newConfig = getConfigurationCopy(); + newConfig.densityDpi = 200; + mController.onConfigurationChanged(newConfig); + assertTrue(mConfigChangeListener.configChanges == 1); + assertTrue(mConfigChangeListener.densityChanges == 1); + assertTrue(mConfigChangeListener.smallestWidthChanges == 0); + assertTrue(mConfigChangeListener.themeChanges == 0); + assertTrue(mConfigChangeListener.localeChanges == 0); + } + + @Test + public void testFontScaleChangeCallback() { + mController.addConfigurationChangeListener(mConfigChangeListener); + + Configuration newConfig = getConfigurationCopy(); + newConfig.fontScale = 2; + mController.onConfigurationChanged(newConfig); + assertTrue(mConfigChangeListener.configChanges == 1); + assertTrue(mConfigChangeListener.densityChanges == 1); + assertTrue(mConfigChangeListener.smallestWidthChanges == 0); + assertTrue(mConfigChangeListener.themeChanges == 0); + assertTrue(mConfigChangeListener.localeChanges == 0); + } + + @Test + public void testSmallestWidthChangeCallback() { + mController.addConfigurationChangeListener(mConfigChangeListener); + + Configuration newConfig = getConfigurationCopy(); + newConfig.smallestScreenWidthDp = 100; + mController.onConfigurationChanged(newConfig); + assertTrue(mConfigChangeListener.configChanges == 1); + assertTrue(mConfigChangeListener.densityChanges == 0); + assertTrue(mConfigChangeListener.smallestWidthChanges == 1); + assertTrue(mConfigChangeListener.themeChanges == 0); + assertTrue(mConfigChangeListener.localeChanges == 0); + } + + @Test + public void testThemeChangeCallback() { + mController.addConfigurationChangeListener(mConfigChangeListener); + + Configuration newConfig = getConfigurationCopy(); + newConfig.assetsSeq++; + mController.onConfigurationChanged(newConfig); + assertTrue(mConfigChangeListener.configChanges == 1); + assertTrue(mConfigChangeListener.densityChanges == 0); + assertTrue(mConfigChangeListener.smallestWidthChanges == 0); + assertTrue(mConfigChangeListener.themeChanges == 1); + assertTrue(mConfigChangeListener.localeChanges == 0); + } + + @Test + public void testNightModeChangeCallback() { + mController.addConfigurationChangeListener(mConfigChangeListener); + + Configuration newConfig = getConfigurationCopy(); + newConfig.uiMode = Configuration.UI_MODE_NIGHT_YES; + mController.onConfigurationChanged(newConfig); + assertTrue(mConfigChangeListener.configChanges == 1); + assertTrue(mConfigChangeListener.densityChanges == 0); + assertTrue(mConfigChangeListener.smallestWidthChanges == 0); + assertTrue(mConfigChangeListener.themeChanges == 1); + assertTrue(mConfigChangeListener.localeChanges == 0); + } + + @Test + public void testLocaleChangeCallback() { + mController.addConfigurationChangeListener(mConfigChangeListener); + + Configuration newConfig = getConfigurationCopy(); + // Just change the locales to be different + if (newConfig.locale == Locale.CANADA) { + newConfig.locale = Locale.US; + } else { + newConfig.locale = Locale.CANADA; + } + mController.onConfigurationChanged(newConfig); + assertTrue(mConfigChangeListener.configChanges == 1); + assertTrue(mConfigChangeListener.densityChanges == 0); + assertTrue(mConfigChangeListener.smallestWidthChanges == 0); + assertTrue(mConfigChangeListener.themeChanges == 0); + assertTrue(mConfigChangeListener.localeChanges == 1); + } + + private Configuration getConfigurationCopy() { + final Configuration c = new Configuration(InstrumentationRegistry.getInstrumentation() + .getTargetContext().getResources().getConfiguration()); + // In tests this might be undefined so make sure it's valid + c.assetsSeq = 1; + return c; + } + + private class TestConfigurationChangeListener implements ConfigurationChangeListener { + // Counts of number of times each of the callbacks are called + public int configChanges; + public int densityChanges; + public int smallestWidthChanges; + public int themeChanges; + public int localeChanges; + + @Override + public void onConfigurationChanged(Configuration newConfiguration) { + configChanges++; + } + + @Override + public void onDensityOrFontScaleChanged() { + densityChanges++; + } + + @Override + public void onSmallestScreenWidthChanged() { + smallestWidthChanges++; + } + + @Override + public void onThemeChanged() { + themeChanges++; + } + + @Override + public void onLocaleOrLayoutDirectionChanged() { + localeChanges++; + } + } + + private class TestKeyguardChangeListener implements KeyguardChangeListener { + // Counts of number of times each of the callbacks are called + public int visibilityChanged; + public boolean lastVisibility; + public boolean lastOccluded; + public boolean lastAnimatingDismiss; + public int dismissAnimationFinished; + + @Override + public void onKeyguardVisibilityChanged(boolean visible, boolean occluded, + boolean animatingDismiss) { + lastVisibility = visible; + lastOccluded = occluded; + lastAnimatingDismiss = animatingDismiss; + visibilityChanged++; + } + + @Override + public void onKeyguardDismissAnimationFinished() { + dismissAnimationFinished++; + } + } + + private class TestUserChangeListener implements UserChangeListener { + // Counts of number of times each of the callbacks are called + public int userChanged; + public int lastUserId; + public Context lastUserContext; + public int userProfilesChanged; + public List<? extends UserInfo> lastUserProfiles; + + + @Override + public void onUserChanged(int newUserId, @NonNull Context userContext) { + userChanged++; + lastUserId = newUserId; + lastUserContext = userContext; + } + + @Override + public void onUserProfilesChanged(@NonNull List<UserInfo> profiles) { + userProfilesChanged++; + lastUserProfiles = profiles; + } + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/tasksurfacehelper/TaskSurfaceHelperControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/tasksurfacehelper/TaskSurfaceHelperControllerTest.java deleted file mode 100644 index d6142753b48a..000000000000 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/tasksurfacehelper/TaskSurfaceHelperControllerTest.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright (C) 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.wm.shell.tasksurfacehelper; - -import static org.mockito.Mockito.verify; - -import android.platform.test.annotations.Presubmit; -import android.testing.AndroidTestingRunner; -import android.view.SurfaceControl; - -import androidx.test.filters.SmallTest; - -import com.android.wm.shell.ShellTaskOrganizer; -import com.android.wm.shell.common.ShellExecutor; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -@Presubmit -@RunWith(AndroidTestingRunner.class) -@SmallTest -public class TaskSurfaceHelperControllerTest { - private TaskSurfaceHelperController mTaskSurfaceHelperController; - @Mock - private ShellTaskOrganizer mMockTaskOrganizer; - @Mock - private ShellExecutor mMockShellExecutor; - - @Before - public void setUp() throws Exception { - MockitoAnnotations.initMocks(this); - mTaskSurfaceHelperController = new TaskSurfaceHelperController( - mMockTaskOrganizer, mMockShellExecutor); - } - - @Test - public void testSetGameModeForTask() { - mTaskSurfaceHelperController.setGameModeForTask(/*taskId*/1, /*gameMode*/3); - verify(mMockTaskOrganizer).setSurfaceMetadata(1, SurfaceControl.METADATA_GAME_MODE, 3); - } -} 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 a0b12976b467..c6492bee040e 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 @@ -22,6 +22,7 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.view.Display.DEFAULT_DISPLAY; +import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_ROTATE; import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_SEAMLESS; import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_UNSPECIFIED; import static android.view.WindowManager.TRANSIT_CHANGE; @@ -43,11 +44,13 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -59,8 +62,6 @@ import android.os.Handler; import android.os.IBinder; import android.os.Looper; import android.os.RemoteException; -import android.view.IDisplayWindowListener; -import android.view.IWindowManager; import android.view.Surface; import android.view.SurfaceControl; import android.view.WindowManager; @@ -79,14 +80,18 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; import androidx.test.platform.app.InstrumentationRegistry; +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.DisplayLayout; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.TransactionPool; +import com.android.wm.shell.sysui.ShellInit; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.InOrder; import java.util.ArrayList; @@ -98,7 +103,7 @@ import java.util.ArrayList; */ @SmallTest @RunWith(AndroidJUnit4.class) -public class ShellTransitionTests { +public class ShellTransitionTests extends ShellTestCase { private final WindowOrganizer mOrganizer = mock(WindowOrganizer.class); private final TransactionPool mTransactionPool = mock(TransactionPool.class); @@ -116,6 +121,14 @@ public class ShellTransitionTests { } @Test + public void instantiate_addInitCallback() { + ShellInit shellInit = mock(ShellInit.class); + final Transitions t = new Transitions(mContext, shellInit, mOrganizer, mTransactionPool, + createTestDisplayController(), mMainExecutor, mMainHandler, mAnimExecutor); + verify(shellInit, times(1)).addInitCallback(any(), eq(t)); + } + + @Test public void testBasicTransitionFlow() { Transitions transitions = createTestTransitions(); transitions.replaceDefaultHandlerForTest(mDefaultHandler); @@ -541,64 +554,77 @@ public class ShellTransitionTests { final @Surface.Rotation int upsideDown = displays .getDisplayLayout(DEFAULT_DISPLAY).getUpsideDownRotation(); + TransitionInfo.Change displayChange = new ChangeBuilder(TRANSIT_CHANGE) + .setFlags(FLAG_IS_DISPLAY).setRotate().build(); + // Set non-square display so nav bar won't be allowed to move. + displayChange.getStartAbsBounds().set(0, 0, 1000, 2000); final TransitionInfo normalDispRotate = new TransitionInfoBuilder(TRANSIT_CHANGE) - .addChange(new ChangeBuilder(TRANSIT_CHANGE).setFlags(FLAG_IS_DISPLAY).setRotate() - .build()) + .addChange(displayChange) .addChange(new ChangeBuilder(TRANSIT_CHANGE).setTask(taskInfo).setRotate().build()) .build(); - assertFalse(DefaultTransitionHandler.isRotationSeamless(normalDispRotate, displays)); + assertEquals(ROTATION_ANIMATION_ROTATE, DefaultTransitionHandler.getRotationAnimationHint( + displayChange, normalDispRotate, displays)); // Seamless if all tasks are seamless final TransitionInfo rotateSeamless = new TransitionInfoBuilder(TRANSIT_CHANGE) - .addChange(new ChangeBuilder(TRANSIT_CHANGE).setFlags(FLAG_IS_DISPLAY).setRotate() - .build()) + .addChange(displayChange) .addChange(new ChangeBuilder(TRANSIT_CHANGE).setTask(taskInfo) .setRotate(ROTATION_ANIMATION_SEAMLESS).build()) .build(); - assertTrue(DefaultTransitionHandler.isRotationSeamless(rotateSeamless, displays)); + assertEquals(ROTATION_ANIMATION_SEAMLESS, DefaultTransitionHandler.getRotationAnimationHint( + displayChange, rotateSeamless, displays)); // Not seamless if there is PiP (or any other non-seamless task) final TransitionInfo pipDispRotate = new TransitionInfoBuilder(TRANSIT_CHANGE) - .addChange(new ChangeBuilder(TRANSIT_CHANGE).setFlags(FLAG_IS_DISPLAY).setRotate() - .build()) + .addChange(displayChange) .addChange(new ChangeBuilder(TRANSIT_CHANGE).setTask(taskInfo) .setRotate(ROTATION_ANIMATION_SEAMLESS).build()) .addChange(new ChangeBuilder(TRANSIT_CHANGE).setTask(taskInfoPip) .setRotate().build()) .build(); - assertFalse(DefaultTransitionHandler.isRotationSeamless(pipDispRotate, displays)); + assertEquals(ROTATION_ANIMATION_ROTATE, DefaultTransitionHandler.getRotationAnimationHint( + displayChange, pipDispRotate, displays)); + + // Not seamless if there is no changed task. + final TransitionInfo noTask = new TransitionInfoBuilder(TRANSIT_CHANGE) + .addChange(displayChange) + .build(); + assertEquals(ROTATION_ANIMATION_ROTATE, DefaultTransitionHandler.getRotationAnimationHint( + displayChange, noTask, displays)); // Not seamless if one of rotations is upside-down + displayChange = new ChangeBuilder(TRANSIT_CHANGE).setFlags(FLAG_IS_DISPLAY) + .setRotate(upsideDown, ROTATION_ANIMATION_UNSPECIFIED).build(); final TransitionInfo seamlessUpsideDown = new TransitionInfoBuilder(TRANSIT_CHANGE) - .addChange(new ChangeBuilder(TRANSIT_CHANGE).setFlags(FLAG_IS_DISPLAY) - .setRotate(upsideDown, ROTATION_ANIMATION_UNSPECIFIED).build()) + .addChange(displayChange) .addChange(new ChangeBuilder(TRANSIT_CHANGE).setTask(taskInfo) .setRotate(upsideDown, ROTATION_ANIMATION_SEAMLESS).build()) .build(); - assertFalse(DefaultTransitionHandler.isRotationSeamless(seamlessUpsideDown, displays)); + assertEquals(ROTATION_ANIMATION_ROTATE, DefaultTransitionHandler.getRotationAnimationHint( + displayChange, seamlessUpsideDown, displays)); // Not seamless if system alert windows + displayChange = new ChangeBuilder(TRANSIT_CHANGE) + .setFlags(FLAG_IS_DISPLAY | FLAG_DISPLAY_HAS_ALERT_WINDOWS).setRotate().build(); final TransitionInfo seamlessButAlert = new TransitionInfoBuilder(TRANSIT_CHANGE) - .addChange(new ChangeBuilder(TRANSIT_CHANGE).setFlags( - FLAG_IS_DISPLAY | FLAG_DISPLAY_HAS_ALERT_WINDOWS).setRotate().build()) + .addChange(displayChange) .addChange(new ChangeBuilder(TRANSIT_CHANGE).setTask(taskInfo) .setRotate(ROTATION_ANIMATION_SEAMLESS).build()) .build(); - assertFalse(DefaultTransitionHandler.isRotationSeamless(seamlessButAlert, displays)); - - // Not seamless if there is no changed task. - final TransitionInfo noTask = new TransitionInfoBuilder(TRANSIT_CHANGE) - .addChange(new ChangeBuilder(TRANSIT_CHANGE).setFlags(FLAG_IS_DISPLAY) - .setRotate().build()) - .build(); - assertFalse(DefaultTransitionHandler.isRotationSeamless(noTask, displays)); + assertEquals(ROTATION_ANIMATION_ROTATE, DefaultTransitionHandler.getRotationAnimationHint( + displayChange, seamlessButAlert, displays)); // Seamless if display is explicitly seamless. + displayChange = new ChangeBuilder(TRANSIT_CHANGE).setFlags(FLAG_IS_DISPLAY) + .setRotate(ROTATION_ANIMATION_SEAMLESS).build(); final TransitionInfo seamlessDisplay = new TransitionInfoBuilder(TRANSIT_CHANGE) - .addChange(new ChangeBuilder(TRANSIT_CHANGE).setFlags(FLAG_IS_DISPLAY) - .setRotate(ROTATION_ANIMATION_SEAMLESS).build()) + .addChange(displayChange) + // The animation hint of task will be ignored. + .addChange(new ChangeBuilder(TRANSIT_CHANGE).setTask(taskInfo) + .setRotate(ROTATION_ANIMATION_ROTATE).build()) .build(); - assertTrue(DefaultTransitionHandler.isRotationSeamless(seamlessDisplay, displays)); + assertEquals(ROTATION_ANIMATION_SEAMLESS, DefaultTransitionHandler.getRotationAnimationHint( + displayChange, seamlessDisplay, displays)); } @Test @@ -678,6 +704,204 @@ public class ShellTransitionTests { verify(runnable4, times(1)).run(); } + @Test + public void testObserverLifecycle_basicTransitionFlow() { + Transitions transitions = createTestTransitions(); + Transitions.TransitionObserver observer = mock(Transitions.TransitionObserver.class); + transitions.registerObserver(observer); + transitions.replaceDefaultHandlerForTest(mDefaultHandler); + + IBinder transitToken = new Binder(); + transitions.requestStartTransition(transitToken, + new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */, null /* remote */)); + TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN) + .addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build(); + SurfaceControl.Transaction startT = mock(SurfaceControl.Transaction.class); + SurfaceControl.Transaction finishT = mock(SurfaceControl.Transaction.class); + transitions.onTransitionReady(transitToken, info, startT, finishT); + + InOrder observerOrder = inOrder(observer); + observerOrder.verify(observer).onTransitionReady(transitToken, info, startT, finishT); + observerOrder.verify(observer).onTransitionStarting(transitToken); + verify(observer, times(0)).onTransitionFinished(eq(transitToken), anyBoolean()); + mDefaultHandler.finishAll(); + mMainExecutor.flushAll(); + verify(observer).onTransitionFinished(transitToken, false); + } + + @Test + public void testObserverLifecycle_queueing() { + Transitions transitions = createTestTransitions(); + Transitions.TransitionObserver observer = mock(Transitions.TransitionObserver.class); + transitions.registerObserver(observer); + transitions.replaceDefaultHandlerForTest(mDefaultHandler); + + IBinder transitToken1 = new Binder(); + transitions.requestStartTransition(transitToken1, + new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */, null /* remote */)); + TransitionInfo info1 = new TransitionInfoBuilder(TRANSIT_OPEN) + .addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build(); + SurfaceControl.Transaction startT1 = mock(SurfaceControl.Transaction.class); + SurfaceControl.Transaction finishT1 = mock(SurfaceControl.Transaction.class); + transitions.onTransitionReady(transitToken1, info1, startT1, finishT1); + verify(observer).onTransitionReady(transitToken1, info1, startT1, finishT1); + + IBinder transitToken2 = new Binder(); + transitions.requestStartTransition(transitToken2, + new TransitionRequestInfo(TRANSIT_CLOSE, null /* trigger */, null /* remote */)); + TransitionInfo info2 = new TransitionInfoBuilder(TRANSIT_CLOSE) + .addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build(); + SurfaceControl.Transaction startT2 = mock(SurfaceControl.Transaction.class); + SurfaceControl.Transaction finishT2 = mock(SurfaceControl.Transaction.class); + transitions.onTransitionReady(transitToken2, info2, startT2, finishT2); + verify(observer, times(1)).onTransitionReady(transitToken2, info2, startT2, finishT2); + verify(observer, times(0)).onTransitionStarting(transitToken2); + verify(observer, times(0)).onTransitionFinished(eq(transitToken1), anyBoolean()); + verify(observer, times(0)).onTransitionFinished(eq(transitToken2), anyBoolean()); + + mDefaultHandler.finishAll(); + mMainExecutor.flushAll(); + // first transition finished + verify(observer, times(1)).onTransitionFinished(transitToken1, false); + verify(observer, times(1)).onTransitionStarting(transitToken2); + verify(observer, times(0)).onTransitionFinished(eq(transitToken2), anyBoolean()); + + mDefaultHandler.finishAll(); + mMainExecutor.flushAll(); + verify(observer, times(1)).onTransitionFinished(transitToken2, false); + } + + + @Test + public void testObserverLifecycle_merging() { + Transitions transitions = createTestTransitions(); + Transitions.TransitionObserver observer = mock(Transitions.TransitionObserver.class); + transitions.registerObserver(observer); + mDefaultHandler.setSimulateMerge(true); + transitions.replaceDefaultHandlerForTest(mDefaultHandler); + + IBinder transitToken1 = new Binder(); + transitions.requestStartTransition(transitToken1, + new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */, null /* remote */)); + TransitionInfo info1 = new TransitionInfoBuilder(TRANSIT_OPEN) + .addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build(); + SurfaceControl.Transaction startT1 = mock(SurfaceControl.Transaction.class); + SurfaceControl.Transaction finishT1 = mock(SurfaceControl.Transaction.class); + transitions.onTransitionReady(transitToken1, info1, startT1, finishT1); + + IBinder transitToken2 = new Binder(); + transitions.requestStartTransition(transitToken2, + new TransitionRequestInfo(TRANSIT_CLOSE, null /* trigger */, null /* remote */)); + TransitionInfo info2 = new TransitionInfoBuilder(TRANSIT_CLOSE) + .addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build(); + SurfaceControl.Transaction startT2 = mock(SurfaceControl.Transaction.class); + SurfaceControl.Transaction finishT2 = mock(SurfaceControl.Transaction.class); + transitions.onTransitionReady(transitToken2, info2, startT2, finishT2); + + InOrder observerOrder = inOrder(observer); + observerOrder.verify(observer).onTransitionReady(transitToken2, info2, startT2, finishT2); + observerOrder.verify(observer).onTransitionMerged(transitToken2, transitToken1); + verify(observer, times(0)).onTransitionFinished(eq(transitToken1), anyBoolean()); + + mDefaultHandler.finishAll(); + mMainExecutor.flushAll(); + // transition + merged all finished. + verify(observer, times(1)).onTransitionFinished(transitToken1, false); + // Merged transition won't receive any lifecycle calls beyond ready + verify(observer, times(0)).onTransitionStarting(transitToken2); + verify(observer, times(0)).onTransitionFinished(eq(transitToken2), anyBoolean()); + } + + @Test + public void testObserverLifecycle_mergingAfterQueueing() { + Transitions transitions = createTestTransitions(); + Transitions.TransitionObserver observer = mock(Transitions.TransitionObserver.class); + transitions.registerObserver(observer); + mDefaultHandler.setSimulateMerge(true); + transitions.replaceDefaultHandlerForTest(mDefaultHandler); + + // Make a test handler that only responds to multi-window triggers AND only animates + // Change transitions. + final WindowContainerTransaction handlerWCT = new WindowContainerTransaction(); + TestTransitionHandler testHandler = new TestTransitionHandler() { + @Override + public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, + @NonNull Transitions.TransitionFinishCallback finishCallback) { + for (TransitionInfo.Change chg : info.getChanges()) { + if (chg.getMode() == TRANSIT_CHANGE) { + return super.startAnimation(transition, info, startTransaction, + finishTransaction, finishCallback); + } + } + return false; + } + + @Nullable + @Override + public WindowContainerTransaction handleRequest(@NonNull IBinder transition, + @NonNull TransitionRequestInfo request) { + final RunningTaskInfo task = request.getTriggerTask(); + return (task != null && task.getWindowingMode() == WINDOWING_MODE_MULTI_WINDOW) + ? handlerWCT : null; + } + }; + transitions.addHandler(testHandler); + + // Use test handler to play an animation + IBinder transitToken1 = new Binder(); + RunningTaskInfo mwTaskInfo = + createTaskInfo(1, WINDOWING_MODE_MULTI_WINDOW, ACTIVITY_TYPE_STANDARD); + transitions.requestStartTransition(transitToken1, + new TransitionRequestInfo(TRANSIT_CHANGE, mwTaskInfo, null /* remote */)); + TransitionInfo change = new TransitionInfoBuilder(TRANSIT_CHANGE) + .addChange(TRANSIT_CHANGE).build(); + SurfaceControl.Transaction startT1 = mock(SurfaceControl.Transaction.class); + SurfaceControl.Transaction finishT1 = mock(SurfaceControl.Transaction.class); + transitions.onTransitionReady(transitToken1, change, startT1, finishT1); + + // Request the second transition that should be handled by the default handler + IBinder transitToken2 = new Binder(); + TransitionInfo open = new TransitionInfoBuilder(TRANSIT_OPEN) + .addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build(); + transitions.requestStartTransition(transitToken2, + new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */, null /* remote */)); + SurfaceControl.Transaction startT2 = mock(SurfaceControl.Transaction.class); + SurfaceControl.Transaction finishT2 = mock(SurfaceControl.Transaction.class); + transitions.onTransitionReady(transitToken2, open, startT2, finishT2); + verify(observer).onTransitionReady(transitToken2, open, startT2, finishT2); + verify(observer, times(0)).onTransitionStarting(transitToken2); + + // Request the third transition that should be merged into the second one + IBinder transitToken3 = new Binder(); + transitions.requestStartTransition(transitToken3, + new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */, null /* remote */)); + SurfaceControl.Transaction startT3 = mock(SurfaceControl.Transaction.class); + SurfaceControl.Transaction finishT3 = mock(SurfaceControl.Transaction.class); + transitions.onTransitionReady(transitToken3, open, startT3, finishT3); + verify(observer, times(0)).onTransitionStarting(transitToken2); + verify(observer).onTransitionReady(transitToken3, open, startT3, finishT3); + verify(observer, times(0)).onTransitionStarting(transitToken3); + + testHandler.finishAll(); + mMainExecutor.flushAll(); + + verify(observer).onTransitionFinished(transitToken1, false); + + mDefaultHandler.finishAll(); + mMainExecutor.flushAll(); + + InOrder observerOrder = inOrder(observer); + observerOrder.verify(observer).onTransitionStarting(transitToken2); + observerOrder.verify(observer).onTransitionMerged(transitToken3, transitToken2); + observerOrder.verify(observer).onTransitionFinished(transitToken2, false); + + // Merged transition won't receive any lifecycle calls beyond ready + verify(observer, times(0)).onTransitionStarting(transitToken3); + verify(observer, times(0)).onTransitionFinished(eq(transitToken3), anyBoolean()); + } + class TransitionInfoBuilder { final TransitionInfo mInfo; @@ -824,33 +1048,21 @@ public class ShellTransitionTests { } private DisplayController createTestDisplayController() { - IWindowManager mockWM = mock(IWindowManager.class); - final IDisplayWindowListener[] displayListener = new IDisplayWindowListener[1]; - try { - doReturn(new int[]{DEFAULT_DISPLAY}).when(mockWM).registerDisplayWindowListener(any()); - } catch (RemoteException e) { - // No remote stuff happening, so this can't be hit - } - DisplayController out = new DisplayController(mContext, mockWM, mMainExecutor); - out.initialize(); + DisplayLayout displayLayout = mock(DisplayLayout.class); + doReturn(Surface.ROTATION_180).when(displayLayout).getUpsideDownRotation(); + // By default we ignore nav bar in deciding if a seamless rotation is allowed. + doReturn(true).when(displayLayout).allowSeamlessRotationDespiteNavBarMoving(); + + DisplayController out = mock(DisplayController.class); + doReturn(displayLayout).when(out).getDisplayLayout(DEFAULT_DISPLAY); return out; } private Transitions createTestTransitions() { - return new Transitions(mOrganizer, mTransactionPool, createTestDisplayController(), - mContext, mMainExecutor, mMainHandler, mAnimExecutor); + ShellInit shellInit = new ShellInit(mMainExecutor); + final Transitions t = new Transitions(mContext, shellInit, mOrganizer, mTransactionPool, + createTestDisplayController(), mMainExecutor, mMainHandler, mAnimExecutor); + shellInit.init(); + return t; } -// -// private class TestDisplayController extends DisplayController { -// private final DisplayLayout mTestDisplayLayout; -// TestDisplayController() { -// super(mContext, mock(IWindowManager.class), mMainExecutor); -// mTestDisplayLayout = new DisplayLayout(); -// mTestDisplayLayout. -// } -// -// @Override -// DisplayLayout -// } - } diff --git a/libs/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 new file mode 100644 index 000000000000..81eefe25704e --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/unfold/UnfoldAnimationControllerTest.java @@ -0,0 +1,367 @@ +/* + * 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.unfold; + +import static com.android.wm.shell.unfold.UnfoldAnimationControllerTest.TestUnfoldTaskAnimator.UNSET; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.ActivityManager.RunningTaskInfo; +import android.app.TaskInfo; +import android.testing.AndroidTestingRunner; +import android.view.SurfaceControl; +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.sysui.ShellInit; +import com.android.wm.shell.unfold.animation.UnfoldTaskAnimator; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +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.Predicate; + +/** + * Tests for {@link UnfoldAnimationController}. + * + * Build/Install/Run: + * atest WMShellUnitTests:UnfoldAnimationControllerTest + */ +@RunWith(AndroidTestingRunner.class) +public class UnfoldAnimationControllerTest extends ShellTestCase { + + @Mock + private TransactionPool mTransactionPool; + @Mock + private UnfoldTransitionHandler mUnfoldTransitionHandler; + @Mock + private ShellInit mShellInit; + @Mock + private SurfaceControl mLeash; + + private UnfoldAnimationController mUnfoldAnimationController; + + private final TestShellUnfoldProgressProvider mProgressProvider = + new TestShellUnfoldProgressProvider(); + private final TestShellExecutor mShellExecutor = new TestShellExecutor(); + + private final TestUnfoldTaskAnimator mTaskAnimator1 = new TestUnfoldTaskAnimator(); + private final TestUnfoldTaskAnimator mTaskAnimator2 = new TestUnfoldTaskAnimator(); + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + when(mTransactionPool.acquire()).thenReturn(mock(SurfaceControl.Transaction.class)); + + final List<UnfoldTaskAnimator> animators = new ArrayList<>(); + animators.add(mTaskAnimator1); + animators.add(mTaskAnimator2); + mUnfoldAnimationController = new UnfoldAnimationController( + mShellInit, + mTransactionPool, + mProgressProvider, + animators, + () -> Optional.of(mUnfoldTransitionHandler), + mShellExecutor + ); + } + + @Test + public void instantiateController_addInitCallback() { + verify(mShellInit, times(1)).addInitCallback(any(), any()); + } + + @Test + public void testAppearedMatchingTask_appliesUnfoldProgress() { + mTaskAnimator1.setTaskMatcher((info) -> info.getWindowingMode() == 2); + RunningTaskInfo taskInfo = new TestRunningTaskInfoBuilder() + .setWindowingMode(2).build(); + + mUnfoldAnimationController.onTaskAppeared(taskInfo, mLeash); + + mUnfoldAnimationController.onStateChangeProgress(0.5f); + assertThat(mTaskAnimator1.mLastAppliedProgress).isEqualTo(0.5f); + } + + @Test + public void testAppearedMatchingTaskTwoDifferentAnimators_appliesUnfoldProgressToBoth() { + mTaskAnimator1.setTaskMatcher((info) -> info.getWindowingMode() == 1); + mTaskAnimator2.setTaskMatcher((info) -> info.getWindowingMode() == 2); + RunningTaskInfo taskInfo1 = new TestRunningTaskInfoBuilder() + .setWindowingMode(1).build(); + RunningTaskInfo taskInfo2 = new TestRunningTaskInfoBuilder() + .setWindowingMode(2).build(); + + mUnfoldAnimationController.onTaskAppeared(taskInfo1, mLeash); + mUnfoldAnimationController.onTaskAppeared(taskInfo2, mLeash); + + mUnfoldAnimationController.onStateChangeProgress(0.5f); + assertThat(mTaskAnimator1.mLastAppliedProgress).isEqualTo(0.5f); + assertThat(mTaskAnimator2.mLastAppliedProgress).isEqualTo(0.5f); + } + + @Test + public void testAppearedNonMatchingTask_doesNotApplyUnfoldProgress() { + mTaskAnimator1.setTaskMatcher((info) -> info.getWindowingMode() == 2); + RunningTaskInfo taskInfo = new TestRunningTaskInfoBuilder() + .setWindowingMode(0).build(); + + mUnfoldAnimationController.onTaskAppeared(taskInfo, mLeash); + + mUnfoldAnimationController.onStateChangeProgress(0.5f); + assertThat(mTaskAnimator1.mLastAppliedProgress).isEqualTo(UNSET); + } + + @Test + public void testAppearedAndChangedToNonMatchingTask_doesNotApplyUnfoldProgress() { + mTaskAnimator1.setTaskMatcher((info) -> info.getWindowingMode() == 2); + RunningTaskInfo taskInfo = new TestRunningTaskInfoBuilder() + .setWindowingMode(2).build(); + + mUnfoldAnimationController.onTaskAppeared(taskInfo, mLeash); + taskInfo.configuration.windowConfiguration.setWindowingMode(0); + mUnfoldAnimationController.onTaskInfoChanged(taskInfo); + + mUnfoldAnimationController.onStateChangeProgress(0.5f); + assertThat(mTaskAnimator1.mLastAppliedProgress).isEqualTo(UNSET); + } + + @Test + public void testAppearedAndChangedToNonMatchingTaskAndBack_appliesUnfoldProgress() { + mTaskAnimator1.setTaskMatcher((info) -> info.getWindowingMode() == 2); + RunningTaskInfo taskInfo = new TestRunningTaskInfoBuilder() + .setWindowingMode(2).build(); + + mUnfoldAnimationController.onTaskAppeared(taskInfo, mLeash); + taskInfo.configuration.windowConfiguration.setWindowingMode(0); + mUnfoldAnimationController.onTaskInfoChanged(taskInfo); + taskInfo.configuration.windowConfiguration.setWindowingMode(2); + mUnfoldAnimationController.onTaskInfoChanged(taskInfo); + + mUnfoldAnimationController.onStateChangeProgress(0.5f); + assertThat(mTaskAnimator1.mLastAppliedProgress).isEqualTo(0.5f); + } + + @Test + public void testAppearedNonMatchingTaskAndChangedToMatching_appliesUnfoldProgress() { + mTaskAnimator1.setTaskMatcher((info) -> info.getWindowingMode() == 2); + RunningTaskInfo taskInfo = new TestRunningTaskInfoBuilder() + .setWindowingMode(0).build(); + + mUnfoldAnimationController.onTaskAppeared(taskInfo, mLeash); + taskInfo.configuration.windowConfiguration.setWindowingMode(2); + mUnfoldAnimationController.onTaskInfoChanged(taskInfo); + + mUnfoldAnimationController.onStateChangeProgress(0.5f); + assertThat(mTaskAnimator1.mLastAppliedProgress).isEqualTo(0.5f); + } + + @Test + public void testAppearedMatchingTaskAndChanged_appliesUnfoldProgress() { + mTaskAnimator1.setTaskMatcher((info) -> info.getWindowingMode() == 2); + RunningTaskInfo taskInfo = new TestRunningTaskInfoBuilder() + .setWindowingMode(2).build(); + + mUnfoldAnimationController.onTaskAppeared(taskInfo, mLeash); + mUnfoldAnimationController.onTaskInfoChanged(taskInfo); + + mUnfoldAnimationController.onStateChangeProgress(0.5f); + assertThat(mTaskAnimator1.mLastAppliedProgress).isEqualTo(0.5f); + } + + @Test + public void testShellTransitionRunning_doesNotApplyUnfoldProgress() { + when(mUnfoldTransitionHandler.willHandleTransition()).thenReturn(true); + mTaskAnimator1.setTaskMatcher((info) -> info.getWindowingMode() == 2); + RunningTaskInfo taskInfo = new TestRunningTaskInfoBuilder() + .setWindowingMode(2).build(); + + mUnfoldAnimationController.onTaskAppeared(taskInfo, mLeash); + + mUnfoldAnimationController.onStateChangeProgress(0.5f); + assertThat(mTaskAnimator1.mLastAppliedProgress).isEqualTo(UNSET); + } + + @Test + public void testApplicableTaskDisappeared_resetsSurface() { + mTaskAnimator1.setTaskMatcher((info) -> info.getWindowingMode() == 0); + RunningTaskInfo taskInfo = new TestRunningTaskInfoBuilder() + .setWindowingMode(0).build(); + mUnfoldAnimationController.onTaskAppeared(taskInfo, mLeash); + assertThat(mTaskAnimator1.mResetTasks).doesNotContain(taskInfo.taskId); + + mUnfoldAnimationController.onTaskVanished(taskInfo); + + assertThat(mTaskAnimator1.mResetTasks).contains(taskInfo.taskId); + } + + @Test + public void testApplicablePinnedTaskDisappeared_doesNotResetSurface() { + mTaskAnimator1.setTaskMatcher((info) -> info.getWindowingMode() == 2); + RunningTaskInfo taskInfo = new TestRunningTaskInfoBuilder() + .setWindowingMode(2).build(); + mUnfoldAnimationController.onTaskAppeared(taskInfo, mLeash); + assertThat(mTaskAnimator1.mResetTasks).doesNotContain(taskInfo.taskId); + + mUnfoldAnimationController.onTaskVanished(taskInfo); + + assertThat(mTaskAnimator1.mResetTasks).doesNotContain(taskInfo.taskId); + } + + @Test + public void testNonApplicableTaskAppearedDisappeared_doesNotResetSurface() { + mTaskAnimator1.setTaskMatcher((info) -> info.getWindowingMode() == 2); + RunningTaskInfo taskInfo = new TestRunningTaskInfoBuilder() + .setWindowingMode(0).build(); + + mUnfoldAnimationController.onTaskAppeared(taskInfo, mLeash); + mUnfoldAnimationController.onTaskVanished(taskInfo); + + assertThat(mTaskAnimator1.mResetTasks).doesNotContain(taskInfo.taskId); + } + + @Test + public void testInit_initsAndStartsAnimators() { + mUnfoldAnimationController.onInit(); + mShellExecutor.flushAll(); + + assertThat(mTaskAnimator1.mInitialized).isTrue(); + assertThat(mTaskAnimator1.mStarted).isTrue(); + } + + private static class TestShellUnfoldProgressProvider implements ShellUnfoldProgressProvider, + ShellUnfoldProgressProvider.UnfoldListener { + + private final List<UnfoldListener> mListeners = new ArrayList<>(); + + @Override + public void addListener(Executor executor, UnfoldListener listener) { + mListeners.add(listener); + } + + @Override + public void onStateChangeStarted() { + mListeners.forEach(UnfoldListener::onStateChangeStarted); + } + + @Override + public void onStateChangeProgress(float progress) { + mListeners.forEach(unfoldListener -> unfoldListener.onStateChangeProgress(progress)); + } + + @Override + public void onStateChangeFinished() { + mListeners.forEach(UnfoldListener::onStateChangeFinished); + } + } + + public static class TestUnfoldTaskAnimator implements UnfoldTaskAnimator { + + public static final float UNSET = -1f; + private Predicate<TaskInfo> mTaskMatcher = (info) -> false; + + Map<Integer, TaskInfo> mTasksMap = new HashMap<>(); + Set<Integer> mResetTasks = new HashSet<>(); + + boolean mInitialized = false; + boolean mStarted = false; + float mLastAppliedProgress = UNSET; + + @Override + public void init() { + mInitialized = true; + } + + @Override + public void start() { + mStarted = true; + } + + @Override + public void stop() { + mStarted = false; + } + + @Override + public boolean isApplicableTask(TaskInfo taskInfo) { + return mTaskMatcher.test(taskInfo); + } + + @Override + public void applyAnimationProgress(float progress, Transaction transaction) { + mLastAppliedProgress = progress; + } + + public void setTaskMatcher(Predicate<TaskInfo> taskMatcher) { + mTaskMatcher = taskMatcher; + } + + @Override + public void onTaskAppeared(TaskInfo taskInfo, SurfaceControl leash) { + mTasksMap.put(taskInfo.taskId, taskInfo); + } + + @Override + public void onTaskVanished(TaskInfo taskInfo) { + mTasksMap.remove(taskInfo.taskId); + } + + @Override + public void onTaskChanged(TaskInfo taskInfo) { + mTasksMap.put(taskInfo.taskId, taskInfo); + } + + @Override + public void resetSurface(TaskInfo taskInfo, Transaction transaction) { + mResetTasks.add(taskInfo.taskId); + } + + @Override + public void resetAllSurfaces(Transaction transaction) { + mTasksMap.values().forEach((t) -> mResetTasks.add(t.taskId)); + } + + @Override + public boolean hasActiveTasks() { + return mTasksMap.size() > 0; + } + + public List<TaskInfo> getCurrentTasks() { + return new ArrayList<>(mTasksMap.values()); + } + } +} 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 new file mode 100644 index 000000000000..ab6ac949d4a3 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java @@ -0,0 +1,417 @@ +/* + * 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.windowdecor; + +import static com.android.wm.shell.MockSurfaceControlHelper.createMockSurfaceControlBuilder; +import static com.android.wm.shell.MockSurfaceControlHelper.createMockSurfaceControlTransaction; + +import static com.google.common.truth.Truth.assertThat; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.argThat; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.same; +import static org.mockito.Mockito.verify; + +import android.app.ActivityManager; +import android.content.Context; +import android.graphics.Color; +import android.graphics.Point; +import android.graphics.Rect; +import android.testing.AndroidTestingRunner; +import android.util.DisplayMetrics; +import android.view.Display; +import android.view.InsetsState; +import android.view.SurfaceControl; +import android.view.SurfaceControlViewHost; +import android.view.View; +import android.view.ViewRootImpl; +import android.view.WindowManager.LayoutParams; +import android.window.WindowContainerTransaction; + +import androidx.test.filters.SmallTest; + +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.ShellTestCase; +import com.android.wm.shell.TestRunningTaskInfoBuilder; +import com.android.wm.shell.common.DisplayController; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InOrder; +import org.mockito.Mock; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Supplier; + +/** + * Tests for {@link WindowDecoration}. + * + * Build/Install/Run: + * atest WMShellUnitTests:WindowDecorationTests + */ +@SmallTest +@RunWith(AndroidTestingRunner.class) +public class WindowDecorationTests extends ShellTestCase { + private static final int CAPTION_HEIGHT_DP = 32; + private static final int SHADOW_RADIUS_DP = 5; + private static final Rect TASK_BOUNDS = new Rect(100, 300, 400, 400); + private static final Point TASK_POSITION_IN_PARENT = new Point(40, 60); + + private final Rect mOutsetsDp = new Rect(); + private final WindowDecoration.RelayoutResult<TestView> mRelayoutResult = + new WindowDecoration.RelayoutResult<>(); + + @Mock + private DisplayController mMockDisplayController; + @Mock + private ShellTaskOrganizer mMockShellTaskOrganizer; + @Mock + private WindowDecoration.SurfaceControlViewHostFactory mMockSurfaceControlViewHostFactory; + @Mock + private SurfaceControlViewHost mMockSurfaceControlViewHost; + @Mock + private TestView mMockView; + @Mock + private WindowContainerTransaction mMockWindowContainerTransaction; + + private final List<SurfaceControl.Transaction> mMockSurfaceControlTransactions = + new ArrayList<>(); + private final List<SurfaceControl.Builder> mMockSurfaceControlBuilders = new ArrayList<>(); + private SurfaceControl.Transaction mMockSurfaceControlStartT; + private SurfaceControl.Transaction mMockSurfaceControlFinishT; + + @Before + public void setUp() { + mMockSurfaceControlStartT = createMockSurfaceControlTransaction(); + mMockSurfaceControlFinishT = createMockSurfaceControlTransaction(); + + doReturn(mMockSurfaceControlViewHost).when(mMockSurfaceControlViewHostFactory) + .create(any(), any(), any()); + } + + @Test + public void testLayoutResultCalculation_invisibleTask() { + final Display defaultDisplay = mock(Display.class); + doReturn(defaultDisplay).when(mMockDisplayController) + .getDisplay(Display.DEFAULT_DISPLAY); + + final SurfaceControl decorContainerSurface = mock(SurfaceControl.class); + final SurfaceControl.Builder decorContainerSurfaceBuilder = + createMockSurfaceControlBuilder(decorContainerSurface); + mMockSurfaceControlBuilders.add(decorContainerSurfaceBuilder); + final SurfaceControl taskBackgroundSurface = mock(SurfaceControl.class); + final SurfaceControl.Builder taskBackgroundSurfaceBuilder = + createMockSurfaceControlBuilder(taskBackgroundSurface); + mMockSurfaceControlBuilders.add(taskBackgroundSurfaceBuilder); + final SurfaceControl captionContainerSurface = mock(SurfaceControl.class); + final SurfaceControl.Builder captionContainerSurfaceBuilder = + createMockSurfaceControlBuilder(captionContainerSurface); + mMockSurfaceControlBuilders.add(captionContainerSurfaceBuilder); + + final ActivityManager.TaskDescription.Builder taskDescriptionBuilder = + new ActivityManager.TaskDescription.Builder() + .setBackgroundColor(Color.YELLOW); + final ActivityManager.RunningTaskInfo taskInfo = new TestRunningTaskInfoBuilder() + .setDisplayId(Display.DEFAULT_DISPLAY) + .setTaskDescriptionBuilder(taskDescriptionBuilder) + .setBounds(TASK_BOUNDS) + .setPositionInParent(TASK_POSITION_IN_PARENT.x, TASK_POSITION_IN_PARENT.y) + .setVisible(false) + .build(); + taskInfo.isFocused = false; + // Density is 2. Outsets are (20, 40, 60, 80) px. Shadow radius is 10px. Caption height is + // 64px. + taskInfo.configuration.densityDpi = DisplayMetrics.DENSITY_DEFAULT * 2; + mOutsetsDp.set(10, 20, 30, 40); + + final SurfaceControl taskSurface = mock(SurfaceControl.class); + final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo, taskSurface); + + windowDecor.relayout(taskInfo); + + verify(decorContainerSurfaceBuilder, never()).build(); + verify(taskBackgroundSurfaceBuilder, never()).build(); + verify(captionContainerSurfaceBuilder, never()).build(); + verify(mMockSurfaceControlViewHostFactory, never()).create(any(), any(), any()); + + verify(mMockSurfaceControlFinishT).hide(taskSurface); + + assertNull(mRelayoutResult.mRootView); + } + + @Test + public void testLayoutResultCalculation_visibleFocusedTask() { + final Display defaultDisplay = mock(Display.class); + doReturn(defaultDisplay).when(mMockDisplayController) + .getDisplay(Display.DEFAULT_DISPLAY); + + final SurfaceControl decorContainerSurface = mock(SurfaceControl.class); + final SurfaceControl.Builder decorContainerSurfaceBuilder = + createMockSurfaceControlBuilder(decorContainerSurface); + mMockSurfaceControlBuilders.add(decorContainerSurfaceBuilder); + final SurfaceControl taskBackgroundSurface = mock(SurfaceControl.class); + final SurfaceControl.Builder taskBackgroundSurfaceBuilder = + createMockSurfaceControlBuilder(taskBackgroundSurface); + mMockSurfaceControlBuilders.add(taskBackgroundSurfaceBuilder); + final SurfaceControl captionContainerSurface = mock(SurfaceControl.class); + final SurfaceControl.Builder captionContainerSurfaceBuilder = + createMockSurfaceControlBuilder(captionContainerSurface); + mMockSurfaceControlBuilders.add(captionContainerSurfaceBuilder); + + final ActivityManager.TaskDescription.Builder taskDescriptionBuilder = + new ActivityManager.TaskDescription.Builder() + .setBackgroundColor(Color.YELLOW); + final ActivityManager.RunningTaskInfo taskInfo = new TestRunningTaskInfoBuilder() + .setDisplayId(Display.DEFAULT_DISPLAY) + .setTaskDescriptionBuilder(taskDescriptionBuilder) + .setBounds(TASK_BOUNDS) + .setPositionInParent(TASK_POSITION_IN_PARENT.x, TASK_POSITION_IN_PARENT.y) + .setVisible(true) + .build(); + taskInfo.isFocused = true; + // Density is 2. Outsets are (20, 40, 60, 80) px. Shadow radius is 10px. Caption height is + // 64px. + taskInfo.configuration.densityDpi = DisplayMetrics.DENSITY_DEFAULT * 2; + mOutsetsDp.set(10, 20, 30, 40); + + final SurfaceControl taskSurface = mock(SurfaceControl.class); + final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo, taskSurface); + + windowDecor.relayout(taskInfo); + + verify(decorContainerSurfaceBuilder).setParent(taskSurface); + verify(decorContainerSurfaceBuilder).setContainerLayer(); + verify(mMockSurfaceControlStartT).setTrustedOverlay(decorContainerSurface, true); + verify(mMockSurfaceControlStartT).setPosition(decorContainerSurface, -20, -40); + verify(mMockSurfaceControlStartT).setWindowCrop(decorContainerSurface, 380, 220); + + verify(taskBackgroundSurfaceBuilder).setParent(taskSurface); + verify(taskBackgroundSurfaceBuilder).setEffectLayer(); + verify(mMockSurfaceControlStartT).setWindowCrop(taskBackgroundSurface, 300, 100); + verify(mMockSurfaceControlStartT) + .setColor(taskBackgroundSurface, new float[] {1.f, 1.f, 0.f}); + verify(mMockSurfaceControlStartT).setShadowRadius(taskBackgroundSurface, 10); + verify(mMockSurfaceControlStartT).setLayer(taskBackgroundSurface, -1); + verify(mMockSurfaceControlStartT).show(taskBackgroundSurface); + + verify(captionContainerSurfaceBuilder).setParent(decorContainerSurface); + verify(captionContainerSurfaceBuilder).setContainerLayer(); + verify(mMockSurfaceControlStartT).setPosition(captionContainerSurface, 20, 40); + 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)); + if (ViewRootImpl.CAPTION_ON_SHELL) { + verify(mMockView).setTaskFocusState(true); + verify(mMockWindowContainerTransaction) + .addRectInsetsProvider(taskInfo.token, + new Rect(100, 300, 400, 364), + new int[] { InsetsState.ITYPE_CAPTION_BAR }); + } + + verify(mMockSurfaceControlFinishT) + .setPosition(taskSurface, TASK_POSITION_IN_PARENT.x, TASK_POSITION_IN_PARENT.y); + verify(mMockSurfaceControlFinishT) + .setCrop(taskSurface, new Rect(-20, -40, 360, 180)); + verify(mMockSurfaceControlStartT) + .show(taskSurface); + + assertEquals(380, mRelayoutResult.mWidth); + assertEquals(220, mRelayoutResult.mHeight); + assertEquals(2, mRelayoutResult.mDensity, 0.f); + } + + @Test + public void testLayoutResultCalculation_visibleFocusedTaskToInvisible() { + final Display defaultDisplay = mock(Display.class); + doReturn(defaultDisplay).when(mMockDisplayController) + .getDisplay(Display.DEFAULT_DISPLAY); + + final SurfaceControl decorContainerSurface = mock(SurfaceControl.class); + final SurfaceControl.Builder decorContainerSurfaceBuilder = + createMockSurfaceControlBuilder(decorContainerSurface); + mMockSurfaceControlBuilders.add(decorContainerSurfaceBuilder); + final SurfaceControl taskBackgroundSurface = mock(SurfaceControl.class); + final SurfaceControl.Builder taskBackgroundSurfaceBuilder = + createMockSurfaceControlBuilder(taskBackgroundSurface); + mMockSurfaceControlBuilders.add(taskBackgroundSurfaceBuilder); + 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); + + final ActivityManager.TaskDescription.Builder taskDescriptionBuilder = + new ActivityManager.TaskDescription.Builder() + .setBackgroundColor(Color.YELLOW); + final ActivityManager.RunningTaskInfo taskInfo = new TestRunningTaskInfoBuilder() + .setDisplayId(Display.DEFAULT_DISPLAY) + .setTaskDescriptionBuilder(taskDescriptionBuilder) + .setBounds(TASK_BOUNDS) + .setPositionInParent(TASK_POSITION_IN_PARENT.x, TASK_POSITION_IN_PARENT.y) + .setVisible(true) + .build(); + taskInfo.isFocused = true; + // Density is 2. Outsets are (20, 40, 60, 80) px. Shadow radius is 10px. Caption height is + // 64px. + taskInfo.configuration.densityDpi = DisplayMetrics.DENSITY_DEFAULT * 2; + mOutsetsDp.set(10, 20, 30, 40); + + final SurfaceControl taskSurface = mock(SurfaceControl.class); + final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo, taskSurface); + + windowDecor.relayout(taskInfo); + + verify(mMockSurfaceControlViewHost, never()).release(); + verify(t, never()).apply(); + verify(mMockWindowContainerTransaction, never()) + .removeInsetsProvider(eq(taskInfo.token), any()); + + taskInfo.isVisible = false; + windowDecor.relayout(taskInfo); + + final InOrder releaseOrder = inOrder(t, mMockSurfaceControlViewHost); + releaseOrder.verify(mMockSurfaceControlViewHost).release(); + releaseOrder.verify(t).remove(captionContainerSurface); + releaseOrder.verify(t).remove(decorContainerSurface); + releaseOrder.verify(t).remove(taskBackgroundSurface); + releaseOrder.verify(t).apply(); + verify(mMockWindowContainerTransaction).removeInsetsProvider(eq(taskInfo.token), any()); + } + + @Test + public void testNotCrashWhenDisplayAppearsAfterTask() { + doReturn(mock(Display.class)).when(mMockDisplayController) + .getDisplay(Display.DEFAULT_DISPLAY); + + final int displayId = Display.DEFAULT_DISPLAY + 1; + final ActivityManager.TaskDescription.Builder taskDescriptionBuilder = + new ActivityManager.TaskDescription.Builder() + .setBackgroundColor(Color.BLACK); + final ActivityManager.RunningTaskInfo taskInfo = new TestRunningTaskInfoBuilder() + .setDisplayId(displayId) + .setTaskDescriptionBuilder(taskDescriptionBuilder) + .setVisible(true) + .build(); + + final TestWindowDecoration windowDecor = + createWindowDecoration(taskInfo, new SurfaceControl()); + windowDecor.relayout(taskInfo); + + // It shouldn't show the window decoration when it can't obtain the display instance. + assertThat(mRelayoutResult.mRootView).isNull(); + + final ArgumentCaptor<DisplayController.OnDisplaysChangedListener> listenerArgumentCaptor = + ArgumentCaptor.forClass(DisplayController.OnDisplaysChangedListener.class); + verify(mMockDisplayController).addDisplayWindowListener(listenerArgumentCaptor.capture()); + final DisplayController.OnDisplaysChangedListener listener = + listenerArgumentCaptor.getValue(); + + // Adding an irrelevant display shouldn't change the result. + listener.onDisplayAdded(Display.DEFAULT_DISPLAY); + assertThat(mRelayoutResult.mRootView).isNull(); + + final Display mockDisplay = mock(Display.class); + doReturn(mockDisplay).when(mMockDisplayController).getDisplay(displayId); + + listener.onDisplayAdded(displayId); + + // The listener should be removed when the display shows up. + verify(mMockDisplayController).removeDisplayWindowListener(same(listener)); + + assertThat(mRelayoutResult.mRootView).isSameInstanceAs(mMockView); + verify(mMockSurfaceControlViewHostFactory).create(any(), eq(mockDisplay), any()); + verify(mMockSurfaceControlViewHost).setView(same(mMockView), any()); + } + + private TestWindowDecoration createWindowDecoration( + ActivityManager.RunningTaskInfo taskInfo, SurfaceControl testSurface) { + return new TestWindowDecoration(mContext, mMockDisplayController, mMockShellTaskOrganizer, + taskInfo, testSurface, + new MockObjectSupplier<>(mMockSurfaceControlBuilders, + () -> createMockSurfaceControlBuilder(mock(SurfaceControl.class))), + new MockObjectSupplier<>(mMockSurfaceControlTransactions, + () -> mock(SurfaceControl.Transaction.class)), + () -> mMockWindowContainerTransaction, mMockSurfaceControlViewHostFactory); + } + + private class MockObjectSupplier<T> implements Supplier<T> { + private final List<T> mObjects; + private final Supplier<T> mDefaultSupplier; + private int mNumOfCalls = 0; + + private MockObjectSupplier(List<T> objects, Supplier<T> defaultSupplier) { + mObjects = objects; + mDefaultSupplier = defaultSupplier; + } + + @Override + public T get() { + final T mock = mNumOfCalls < mObjects.size() + ? mObjects.get(mNumOfCalls) : mDefaultSupplier.get(); + ++mNumOfCalls; + return mock; + } + } + + private static class TestView extends View implements TaskFocusStateConsumer { + private TestView(Context context) { + super(context); + } + + @Override + public void setTaskFocusState(boolean focused) {} + } + + private class TestWindowDecoration extends WindowDecoration<TestView> { + TestWindowDecoration(Context context, DisplayController displayController, + ShellTaskOrganizer taskOrganizer, ActivityManager.RunningTaskInfo taskInfo, + SurfaceControl taskSurface, + Supplier<SurfaceControl.Builder> surfaceControlBuilderSupplier, + Supplier<SurfaceControl.Transaction> surfaceControlTransactionSupplier, + Supplier<WindowContainerTransaction> windowContainerTransactionSupplier, + SurfaceControlViewHostFactory surfaceControlViewHostFactory) { + super(context, displayController, taskOrganizer, taskInfo, taskSurface, + surfaceControlBuilderSupplier, surfaceControlTransactionSupplier, + windowContainerTransactionSupplier, surfaceControlViewHostFactory); + } + + @Override + void relayout(ActivityManager.RunningTaskInfo taskInfo) { + relayout(null /* taskInfo */, 0 /* layoutResId */, mMockView, CAPTION_HEIGHT_DP, + mOutsetsDp, SHADOW_RADIUS_DP, mMockSurfaceControlStartT, + mMockSurfaceControlFinishT, mMockWindowContainerTransaction, mRelayoutResult); + } + } +} diff --git a/libs/dream/lowlight/Android.bp b/libs/dream/lowlight/Android.bp new file mode 100644 index 000000000000..5b5b0f07cabd --- /dev/null +++ b/libs/dream/lowlight/Android.bp @@ -0,0 +1,47 @@ +// 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 { + // 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: "low_light_dream_lib-sources", + srcs: [ + "src/**/*.java", + ], + path: "src", +} + +android_library { + name: "LowLightDreamLib", + srcs: [ + ":low_light_dream_lib-sources", + ], + resource_dirs: [ + "res", + ], + static_libs: [ + "androidx.arch.core_core-runtime", + "dagger2", + "jsr330", + ], + manifest: "AndroidManifest.xml", + plugins: ["dagger2-compiler"], +} diff --git a/libs/WindowManager/Shell/res/color/unfold_transition_background.xml b/libs/dream/lowlight/AndroidManifest.xml index 63289a3f75d9..a8d952699943 100644 --- a/libs/WindowManager/Shell/res/color/unfold_transition_background.xml +++ b/libs/dream/lowlight/AndroidManifest.xml @@ -1,5 +1,6 @@ <?xml version="1.0" encoding="utf-8"?> -<!-- Copyright (C) 2021 The Android Open Source Project +<!-- + 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. @@ -13,7 +14,5 @@ See the License for the specific language governing permissions and limitations under the License. --> -<selector xmlns:android="http://schemas.android.com/apk/res/android"> - <!-- Matches taskbar color --> - <item android:color="@android:color/system_neutral2_500" android:lStar="35" /> -</selector> + +<manifest package="com.android.dream.lowlight" /> diff --git a/libs/dream/lowlight/res/values/config.xml b/libs/dream/lowlight/res/values/config.xml new file mode 100644 index 000000000000..70fe0738a6f4 --- /dev/null +++ b/libs/dream/lowlight/res/values/config.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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. + --> +<resources> + <!-- The dream component used when the device is low light environment. --> + <string translatable="false" name="config_lowLightDreamComponent"/> +</resources> diff --git a/libs/dream/lowlight/src/com/android/dream/lowlight/LowLightDreamManager.java b/libs/dream/lowlight/src/com/android/dream/lowlight/LowLightDreamManager.java new file mode 100644 index 000000000000..5ecec4ddd1ad --- /dev/null +++ b/libs/dream/lowlight/src/com/android/dream/lowlight/LowLightDreamManager.java @@ -0,0 +1,117 @@ +/* + * 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.dream.lowlight; + +import static com.android.dream.lowlight.dagger.LowLightDreamModule.LOW_LIGHT_DREAM_COMPONENT; + +import android.annotation.IntDef; +import android.annotation.RequiresPermission; +import android.app.DreamManager; +import android.content.ComponentName; +import android.util.Log; + +import androidx.annotation.Nullable; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import javax.inject.Inject; +import javax.inject.Named; + +/** + * Maintains the ambient light mode of the environment the device is in, and sets a low light dream + * component, if present, as the system dream when the ambient light mode is low light. + * + * @hide + */ +public final class LowLightDreamManager { + private static final String TAG = "LowLightDreamManager"; + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + + /** + * @hide + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(prefix = { "AMBIENT_LIGHT_MODE_" }, value = { + AMBIENT_LIGHT_MODE_UNKNOWN, + AMBIENT_LIGHT_MODE_REGULAR, + AMBIENT_LIGHT_MODE_LOW_LIGHT + }) + public @interface AmbientLightMode {} + + /** + * Constant for ambient light mode being unknown. + * @hide + */ + public static final int AMBIENT_LIGHT_MODE_UNKNOWN = 0; + + /** + * Constant for ambient light mode being regular / bright. + * @hide + */ + public static final int AMBIENT_LIGHT_MODE_REGULAR = 1; + + /** + * Constant for ambient light mode being low light / dim. + * @hide + */ + public static final int AMBIENT_LIGHT_MODE_LOW_LIGHT = 2; + + private final DreamManager mDreamManager; + + @Nullable + private final ComponentName mLowLightDreamComponent; + + private int mAmbientLightMode = AMBIENT_LIGHT_MODE_UNKNOWN; + + @Inject + public LowLightDreamManager( + DreamManager dreamManager, + @Named(LOW_LIGHT_DREAM_COMPONENT) @Nullable ComponentName lowLightDreamComponent) { + mDreamManager = dreamManager; + mLowLightDreamComponent = lowLightDreamComponent; + } + + /** + * Sets the current ambient light mode. + * @hide + */ + @RequiresPermission(android.Manifest.permission.WRITE_DREAM_STATE) + public void setAmbientLightMode(@AmbientLightMode int ambientLightMode) { + if (mLowLightDreamComponent == null) { + if (DEBUG) { + Log.d(TAG, "ignore ambient light mode change because low light dream component " + + "is empty"); + } + return; + } + + if (mAmbientLightMode == ambientLightMode) { + return; + } + + if (DEBUG) { + Log.d(TAG, "ambient light mode changed from " + mAmbientLightMode + " to " + + ambientLightMode); + } + + mAmbientLightMode = ambientLightMode; + + mDreamManager.setSystemDreamComponent(mAmbientLightMode == AMBIENT_LIGHT_MODE_LOW_LIGHT + ? mLowLightDreamComponent : null); + } +} diff --git a/libs/dream/lowlight/src/com/android/dream/lowlight/dagger/LowLightDreamModule.java b/libs/dream/lowlight/src/com/android/dream/lowlight/dagger/LowLightDreamModule.java new file mode 100644 index 000000000000..c183a04cb2f9 --- /dev/null +++ b/libs/dream/lowlight/src/com/android/dream/lowlight/dagger/LowLightDreamModule.java @@ -0,0 +1,61 @@ +/* + * 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.dream.lowlight.dagger; + +import android.app.DreamManager; +import android.content.ComponentName; +import android.content.Context; + +import androidx.annotation.Nullable; + +import com.android.dream.lowlight.R; + +import javax.inject.Named; + +import dagger.Module; +import dagger.Provides; + +/** + * Dagger module for low light dream. + * + * @hide + */ +@Module +public interface LowLightDreamModule { + String LOW_LIGHT_DREAM_COMPONENT = "low_light_dream_component"; + + /** + * Provides dream manager. + */ + @Provides + static DreamManager providesDreamManager(Context context) { + return context.getSystemService(DreamManager.class); + } + + /** + * Provides the component name of the low light dream, or null if not configured. + */ + @Provides + @Named(LOW_LIGHT_DREAM_COMPONENT) + @Nullable + static ComponentName providesLowLightDreamComponent(Context context) { + final String lowLightDreamComponent = context.getResources().getString( + R.string.config_lowLightDreamComponent); + return lowLightDreamComponent.isEmpty() ? null + : ComponentName.unflattenFromString(lowLightDreamComponent); + } +} diff --git a/libs/dream/lowlight/tests/Android.bp b/libs/dream/lowlight/tests/Android.bp new file mode 100644 index 000000000000..bd6f05eabac5 --- /dev/null +++ b/libs/dream/lowlight/tests/Android.bp @@ -0,0 +1,45 @@ +// 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 { + default_applicable_licenses: ["frameworks_base_license"], +} + +android_test { + name: "LowLightDreamTests", + srcs: [ + "**/*.java", + ], + static_libs: [ + "LowLightDreamLib", + "androidx.test.runner", + "androidx.test.rules", + "androidx.test.ext.junit", + "frameworks-base-testutils", + "junit", + "mockito-target-extended-minus-junit4", + "platform-test-annotations", + "testables", + "truth-prebuilt", + ], + libs: [ + "android.test.mock", + "android.test.base", + "android.test.runner", + ], + jni_libs: [ + "libdexmakerjvmtiagent", + "libstaticjvmtiagent", + ], +} diff --git a/libs/dream/lowlight/tests/AndroidManifest.xml b/libs/dream/lowlight/tests/AndroidManifest.xml new file mode 100644 index 000000000000..abb71fb53b49 --- /dev/null +++ b/libs/dream/lowlight/tests/AndroidManifest.xml @@ -0,0 +1,33 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> + +<manifest 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" + package="com.android.dream.lowlight.tests"> + + <application android:debuggable="true" android:largeHeap="true"> + <uses-library android:name="android.test.mock" /> + <uses-library android:name="android.test.runner" /> + </application> + + <instrumentation + android:name="androidx.test.runner.AndroidJUnitRunner" + android:label="Tests for LowLightDreamLib" + android:targetPackage="com.android.dream.lowlight.tests"> + </instrumentation> + +</manifest> diff --git a/libs/dream/lowlight/tests/AndroidTest.xml b/libs/dream/lowlight/tests/AndroidTest.xml new file mode 100644 index 000000000000..10800333add2 --- /dev/null +++ b/libs/dream/lowlight/tests/AndroidTest.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> +<configuration description="Runs Tests for LowLightDreamLib"> + <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="LowLightDreamTests.apk" /> + </target_preparer> + + <option name="test-suite-tag" value="apct" /> + <option name="test-suite-tag" value="framework-base-presubmit" /> + <option name="test-tag" value="LowLightDreamLibTests" /> + <test class="com.android.tradefed.testtype.AndroidJUnitTest" > + <option name="package" value="com.android.dream.lowlight.tests" /> + <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" /> + <option name="hidden-api-checks" value="false"/> + </test> +</configuration> diff --git a/libs/dream/lowlight/tests/src/com.android.dream.lowlight/LowLightDreamManagerTest.java b/libs/dream/lowlight/tests/src/com.android.dream.lowlight/LowLightDreamManagerTest.java new file mode 100644 index 000000000000..91a170f7ae14 --- /dev/null +++ b/libs/dream/lowlight/tests/src/com.android.dream.lowlight/LowLightDreamManagerTest.java @@ -0,0 +1,98 @@ +/* + * 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.dream.lowlight; + +import static com.android.dream.lowlight.LowLightDreamManager.AMBIENT_LIGHT_MODE_LOW_LIGHT; +import static com.android.dream.lowlight.LowLightDreamManager.AMBIENT_LIGHT_MODE_REGULAR; +import static com.android.dream.lowlight.LowLightDreamManager.AMBIENT_LIGHT_MODE_UNKNOWN; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +import android.app.DreamManager; +import android.content.ComponentName; +import android.testing.AndroidTestingRunner; + +import androidx.test.filters.SmallTest; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@SmallTest +@RunWith(AndroidTestingRunner.class) +public class LowLightDreamManagerTest { + @Mock + private DreamManager mDreamManager; + + @Mock + private ComponentName mDreamComponent; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + } + + @Test + public void setAmbientLightMode_lowLight_setSystemDream() { + final LowLightDreamManager lowLightDreamManager = new LowLightDreamManager(mDreamManager, + mDreamComponent); + + lowLightDreamManager.setAmbientLightMode(AMBIENT_LIGHT_MODE_LOW_LIGHT); + + verify(mDreamManager).setSystemDreamComponent(mDreamComponent); + } + + @Test + public void setAmbientLightMode_regularLight_clearSystemDream() { + final LowLightDreamManager lowLightDreamManager = new LowLightDreamManager(mDreamManager, + mDreamComponent); + + lowLightDreamManager.setAmbientLightMode(AMBIENT_LIGHT_MODE_REGULAR); + + verify(mDreamManager).setSystemDreamComponent(null); + } + + @Test + public void setAmbientLightMode_defaultUnknownMode_clearSystemDream() { + final LowLightDreamManager lowLightDreamManager = new LowLightDreamManager(mDreamManager, + mDreamComponent); + + // Set to low light first. + lowLightDreamManager.setAmbientLightMode(AMBIENT_LIGHT_MODE_LOW_LIGHT); + clearInvocations(mDreamManager); + + // Return to default unknown mode. + lowLightDreamManager.setAmbientLightMode(AMBIENT_LIGHT_MODE_UNKNOWN); + + verify(mDreamManager).setSystemDreamComponent(null); + } + + @Test + public void setAmbientLightMode_dreamComponentNotSet_doNothing() { + final LowLightDreamManager lowLightDreamManager = new LowLightDreamManager(mDreamManager, + null /*dream component*/); + + lowLightDreamManager.setAmbientLightMode(AMBIENT_LIGHT_MODE_LOW_LIGHT); + + verify(mDreamManager, never()).setSystemDreamComponent(any()); + } +} diff --git a/libs/hwui/FrameInfoVisualizer.cpp b/libs/hwui/FrameInfoVisualizer.cpp index 3a8e559f6d7e..687e4dd324d3 100644 --- a/libs/hwui/FrameInfoVisualizer.cpp +++ b/libs/hwui/FrameInfoVisualizer.cpp @@ -179,7 +179,7 @@ void FrameInfoVisualizer::initializeRects(const int baseline, const int width) { void FrameInfoVisualizer::nextBarSegment(FrameInfoIndex start, FrameInfoIndex end) { int fast_i = (mNumFastRects - 1) * 4; int janky_i = (mNumJankyRects - 1) * 4; - ; + for (size_t fi = 0; fi < mFrameSource.size(); fi++) { if (mFrameSource[fi][FrameInfoIndex::Flags] & FrameInfoFlags::SkippedFrame) { continue; diff --git a/libs/hwui/JankTracker.cpp b/libs/hwui/JankTracker.cpp index 1e5be6c3eed7..4b0ddd2fa2ef 100644 --- a/libs/hwui/JankTracker.cpp +++ b/libs/hwui/JankTracker.cpp @@ -201,8 +201,9 @@ void JankTracker::finishFrame(FrameInfo& frame, std::unique_ptr<FrameMetricsRepo // If we are in triple buffering, we have enough buffers in queue to sustain a single frame // drop without jank, so adjust the frame interval to the deadline. if (isTripleBuffered) { - deadline += frameInterval; - frame.set(FrameInfoIndex::FrameDeadline) += frameInterval; + int64_t originalDeadlineDuration = deadline - frame[FrameInfoIndex::IntendedVsync]; + deadline = mNextFrameStartUnstuffed + originalDeadlineDuration; + frame.set(FrameInfoIndex::FrameDeadline) = deadline; } // If we hit the deadline, cool! diff --git a/libs/hwui/jni/android_graphics_Canvas.cpp b/libs/hwui/jni/android_graphics_Canvas.cpp index 0ef80ee10708..132234b38003 100644 --- a/libs/hwui/jni/android_graphics_Canvas.cpp +++ b/libs/hwui/jni/android_graphics_Canvas.cpp @@ -407,14 +407,28 @@ static void drawVertices(JNIEnv* env, jobject, jlong canvasHandle, indices = (const uint16_t*)(indexA.ptr() + indexIndex); } - SkVertices::VertexMode mode = static_cast<SkVertices::VertexMode>(modeHandle); + SkVertices::VertexMode vertexMode = static_cast<SkVertices::VertexMode>(modeHandle); const Paint* paint = reinterpret_cast<Paint*>(paintHandle); - get_canvas(canvasHandle)->drawVertices(SkVertices::MakeCopy(mode, vertexCount, - reinterpret_cast<const SkPoint*>(verts), - reinterpret_cast<const SkPoint*>(texs), - reinterpret_cast<const SkColor*>(colors), - indexCount, indices).get(), - SkBlendMode::kModulate, *paint); + + // Preserve legacy Skia behavior: ignore the shader if there are no texs set. + Paint noShaderPaint; + if (jtexs == NULL) { + noShaderPaint = Paint(*paint); + noShaderPaint.setShader(nullptr); + paint = &noShaderPaint; + } + // Since https://skia-review.googlesource.com/c/skia/+/473676, Skia will blend paint and vertex + // colors when no shader is provided. This ternary uses kDst to mimic the old behavior of + // ignoring the paint and using the vertex colors directly when no shader is provided. + SkBlendMode blendMode = paint->getShader() ? SkBlendMode::kModulate : SkBlendMode::kDst; + + get_canvas(canvasHandle) + ->drawVertices(SkVertices::MakeCopy( + vertexMode, vertexCount, reinterpret_cast<const SkPoint*>(verts), + reinterpret_cast<const SkPoint*>(texs), + reinterpret_cast<const SkColor*>(colors), indexCount, indices) + .get(), + blendMode, *paint); } static void drawNinePatch(JNIEnv* env, jobject, jlong canvasHandle, jlong bitmapHandle, diff --git a/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp b/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp index 2aca41e41905..8e350d5012a5 100644 --- a/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp +++ b/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp @@ -53,8 +53,12 @@ SkiaOpenGLPipeline::~SkiaOpenGLPipeline() { } MakeCurrentResult SkiaOpenGLPipeline::makeCurrent() { - // TODO: Figure out why this workaround is needed, see b/13913604 - // In the meantime this matches the behavior of GLRenderer, so it is not a regression + // In case the surface was destroyed (e.g. a previous trimMemory call) we + // need to recreate it here. + if (!isSurfaceReady() && mNativeWindow) { + setSurface(mNativeWindow.get(), mSwapBehavior); + } + EGLint error = 0; if (!mEglManager.makeCurrent(mEglSurface, &error)) { return MakeCurrentResult::AlreadyCurrent; @@ -166,6 +170,9 @@ void SkiaOpenGLPipeline::onStop() { } bool SkiaOpenGLPipeline::setSurface(ANativeWindow* surface, SwapBehavior swapBehavior) { + mNativeWindow = surface; + mSwapBehavior = swapBehavior; + if (mEglSurface != EGL_NO_SURFACE) { mEglManager.destroySurface(mEglSurface); mEglSurface = EGL_NO_SURFACE; @@ -182,7 +189,8 @@ bool SkiaOpenGLPipeline::setSurface(ANativeWindow* surface, SwapBehavior swapBeh if (mEglSurface != EGL_NO_SURFACE) { const bool preserveBuffer = (swapBehavior != SwapBehavior::kSwap_discardBuffer); - mBufferPreserved = mEglManager.setPreserveBuffer(mEglSurface, preserveBuffer); + const bool isPreserved = mEglManager.setPreserveBuffer(mEglSurface, preserveBuffer); + ALOGE_IF(preserveBuffer != isPreserved, "Unable to match the desired swap behavior."); return true; } diff --git a/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.h b/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.h index 186998a01745..a80c613697f2 100644 --- a/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.h +++ b/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.h @@ -61,7 +61,8 @@ protected: private: renderthread::EglManager& mEglManager; EGLSurface mEglSurface = EGL_NO_SURFACE; - bool mBufferPreserved = false; + sp<ANativeWindow> mNativeWindow; + renderthread::SwapBehavior mSwapBehavior = renderthread::SwapBehavior::kSwap_discardBuffer; }; } /* namespace skiapipeline */ diff --git a/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp b/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp index 905d46e58014..cc2565d88d5e 100644 --- a/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp +++ b/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp @@ -55,7 +55,12 @@ VulkanManager& SkiaVulkanPipeline::vulkanManager() { } MakeCurrentResult SkiaVulkanPipeline::makeCurrent() { - return MakeCurrentResult::AlreadyCurrent; + // In case the surface was destroyed (e.g. a previous trimMemory call) we + // need to recreate it here. + if (!isSurfaceReady() && mNativeWindow) { + setSurface(mNativeWindow.get(), SwapBehavior::kSwap_default); + } + return isContextReady() ? MakeCurrentResult::AlreadyCurrent : MakeCurrentResult::Failed; } Frame SkiaVulkanPipeline::getFrame() { @@ -130,7 +135,11 @@ DeferredLayerUpdater* SkiaVulkanPipeline::createTextureLayer() { void SkiaVulkanPipeline::onStop() {} -bool SkiaVulkanPipeline::setSurface(ANativeWindow* surface, SwapBehavior swapBehavior) { +// We can safely ignore the swap behavior because VkManager will always operate +// in a mode equivalent to EGLManager::SwapBehavior::kBufferAge +bool SkiaVulkanPipeline::setSurface(ANativeWindow* surface, SwapBehavior /*swapBehavior*/) { + mNativeWindow = surface; + if (mVkSurface) { vulkanManager().destroySurface(mVkSurface); mVkSurface = nullptr; diff --git a/libs/hwui/pipeline/skia/SkiaVulkanPipeline.h b/libs/hwui/pipeline/skia/SkiaVulkanPipeline.h index ada6af67d4a0..a6e685d08aeb 100644 --- a/libs/hwui/pipeline/skia/SkiaVulkanPipeline.h +++ b/libs/hwui/pipeline/skia/SkiaVulkanPipeline.h @@ -61,6 +61,7 @@ private: renderthread::VulkanManager& vulkanManager(); renderthread::VulkanSurface* mVkSurface = nullptr; + sp<ANativeWindow> mNativeWindow; }; } /* namespace skiapipeline */ diff --git a/libs/hwui/renderthread/CanvasContext.cpp b/libs/hwui/renderthread/CanvasContext.cpp index 976117b9bbd4..75d3ff7753cb 100644 --- a/libs/hwui/renderthread/CanvasContext.cpp +++ b/libs/hwui/renderthread/CanvasContext.cpp @@ -512,9 +512,19 @@ nsecs_t CanvasContext::draw() { ATRACE_FORMAT("Drawing " RECT_STRING, SK_RECT_ARGS(dirty)); - const auto drawResult = mRenderPipeline->draw(frame, windowDirty, dirty, mLightGeometry, - &mLayerUpdateQueue, mContentDrawBounds, mOpaque, - mLightInfo, mRenderNodes, &(profiler())); + IRenderPipeline::DrawResult drawResult; + { + // FrameInfoVisualizer accesses the frame events, which cannot be mutated mid-draw + // or it can lead to memory corruption. + // This lock is overly broad, but it's the quickest fix since this mutex is otherwise + // not visible to IRenderPipeline much less FrameInfoVisualizer. And since this is + // the thread we're primarily concerned about being responsive, this being too broad + // shouldn't pose a performance issue. + std::scoped_lock lock(mFrameMetricsReporterMutex); + drawResult = mRenderPipeline->draw(frame, windowDirty, dirty, mLightGeometry, + &mLayerUpdateQueue, mContentDrawBounds, mOpaque, + mLightInfo, mRenderNodes, &(profiler())); + } uint64_t frameCompleteNr = getFrameNumber(); @@ -754,11 +764,11 @@ void CanvasContext::onSurfaceStatsAvailable(void* context, int32_t surfaceContro FrameInfo* frameInfo = instance->getFrameInfoFromLast4(frameNumber, surfaceControlId); if (frameInfo != nullptr) { + std::scoped_lock lock(instance->mFrameMetricsReporterMutex); frameInfo->set(FrameInfoIndex::FrameCompleted) = std::max(gpuCompleteTime, frameInfo->get(FrameInfoIndex::SwapBuffersCompleted)); frameInfo->set(FrameInfoIndex::GpuCompleted) = std::max( gpuCompleteTime, frameInfo->get(FrameInfoIndex::CommandSubmissionCompleted)); - std::scoped_lock lock(instance->mFrameMetricsReporterMutex); instance->mJankTracker.finishFrame(*frameInfo, instance->mFrameMetricsReporter, frameNumber, surfaceControlId); } diff --git a/libs/hwui/tests/unit/JankTrackerTests.cpp b/libs/hwui/tests/unit/JankTrackerTests.cpp index 5b397de36a86..b67e419e7d4a 100644 --- a/libs/hwui/tests/unit/JankTrackerTests.cpp +++ b/libs/hwui/tests/unit/JankTrackerTests.cpp @@ -195,3 +195,68 @@ TEST(JankTracker, doubleStuffedThenPauseThenJank) { ASSERT_EQ(3, container.get()->totalFrameCount()); ASSERT_EQ(2, container.get()->jankFrameCount()); } + +TEST(JankTracker, doubleStuffedTwoIntervalBehind) { + std::mutex mutex; + ProfileDataContainer container(mutex); + JankTracker jankTracker(&container); + std::unique_ptr<FrameMetricsReporter> reporter = std::make_unique<FrameMetricsReporter>(); + + uint64_t frameNumber = 0; + uint32_t surfaceId = 0; + + // First frame janks + FrameInfo* info = jankTracker.startFrame(); + info->set(FrameInfoIndex::IntendedVsync) = 100_ms; + info->set(FrameInfoIndex::Vsync) = 101_ms; + info->set(FrameInfoIndex::SwapBuffersCompleted) = 107_ms; + info->set(FrameInfoIndex::GpuCompleted) = 117_ms; + info->set(FrameInfoIndex::FrameCompleted) = 117_ms; + info->set(FrameInfoIndex::FrameInterval) = 16_ms; + info->set(FrameInfoIndex::FrameDeadline) = 116_ms; + jankTracker.finishFrame(*info, reporter, frameNumber, surfaceId); + + ASSERT_EQ(1, container.get()->jankFrameCount()); + + // Second frame is long, but doesn't jank because double-stuffed. + // Second frame duration is between 1*interval ~ 2*interval + info = jankTracker.startFrame(); + info->set(FrameInfoIndex::IntendedVsync) = 116_ms; + info->set(FrameInfoIndex::Vsync) = 116_ms; + info->set(FrameInfoIndex::SwapBuffersCompleted) = 129_ms; + info->set(FrameInfoIndex::GpuCompleted) = 133_ms; + info->set(FrameInfoIndex::FrameCompleted) = 133_ms; + info->set(FrameInfoIndex::FrameInterval) = 16_ms; + info->set(FrameInfoIndex::FrameDeadline) = 132_ms; + jankTracker.finishFrame(*info, reporter, frameNumber, surfaceId); + + ASSERT_EQ(1, container.get()->jankFrameCount()); + + // Third frame is even longer, cause a jank + // Third frame duration is between 2*interval ~ 3*interval + info = jankTracker.startFrame(); + info->set(FrameInfoIndex::IntendedVsync) = 132_ms; + info->set(FrameInfoIndex::Vsync) = 132_ms; + info->set(FrameInfoIndex::SwapBuffersCompleted) = 160_ms; + info->set(FrameInfoIndex::GpuCompleted) = 165_ms; + info->set(FrameInfoIndex::FrameCompleted) = 165_ms; + info->set(FrameInfoIndex::FrameInterval) = 16_ms; + info->set(FrameInfoIndex::FrameDeadline) = 148_ms; + jankTracker.finishFrame(*info, reporter, frameNumber, surfaceId); + + ASSERT_EQ(2, container.get()->jankFrameCount()); + + // 4th frame is double-stuffed with a 2 * interval latency + // 4th frame duration is between 2*interval ~ 3*interval + info = jankTracker.startFrame(); + info->set(FrameInfoIndex::IntendedVsync) = 148_ms; + info->set(FrameInfoIndex::Vsync) = 148_ms; + info->set(FrameInfoIndex::SwapBuffersCompleted) = 170_ms; + info->set(FrameInfoIndex::GpuCompleted) = 181_ms; + info->set(FrameInfoIndex::FrameCompleted) = 181_ms; + info->set(FrameInfoIndex::FrameInterval) = 16_ms; + info->set(FrameInfoIndex::FrameDeadline) = 164_ms; + jankTracker.finishFrame(*info, reporter, frameNumber, surfaceId); + + ASSERT_EQ(2, container.get()->jankFrameCount()); +} diff --git a/libs/hwui/tests/unit/SkiaPipelineTests.cpp b/libs/hwui/tests/unit/SkiaPipelineTests.cpp index 60ae6044cd5b..7419f8fd89f1 100644 --- a/libs/hwui/tests/unit/SkiaPipelineTests.cpp +++ b/libs/hwui/tests/unit/SkiaPipelineTests.cpp @@ -404,7 +404,9 @@ RENDERTHREAD_SKIA_PIPELINE_TEST(SkiaPipeline, context_lost) { EXPECT_TRUE(pipeline->isSurfaceReady()); renderThread.destroyRenderingContext(); EXPECT_FALSE(pipeline->isSurfaceReady()); - LOG_ALWAYS_FATAL_IF(pipeline->isSurfaceReady()); + + pipeline->makeCurrent(); + EXPECT_TRUE(pipeline->isSurfaceReady()); } RENDERTHREAD_SKIA_PIPELINE_TEST(SkiaPipeline, pictureCallback) { |