diff options
Diffstat (limited to 'libs')
754 files changed, 31992 insertions, 22221 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..7e9c4189dabb 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,10 +36,12 @@ 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; + return 2; } /** @@ -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..febd7917dff9 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,6 +28,7 @@ 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.NonNull; @@ -51,34 +51,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; } @@ -153,41 +145,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 +179,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 +190,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 +213,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 +229,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 +241,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 +250,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 +259,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..c8ac0fc73ff9 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java @@ -16,17 +16,21 @@ package androidx.window.extensions.embedding; -import android.annotation.NonNull; import android.app.Activity; import android.util.Pair; import android.util.Size; +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; SplitContainer(@NonNull TaskFragmentContainer primaryContainer, 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..126f8350839c 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,21 @@ 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.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; @@ -43,12 +56,14 @@ 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.window.TaskFragmentInfo; +import android.window.TaskFragmentTransaction; import android.window.WindowContainerTransaction; import androidx.annotation.GuardedBy; @@ -70,6 +85,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") @@ -93,7 +110,8 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen 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; public SplitController() { final MainThreadExecutor executor = new MainThreadExecutor(); @@ -105,7 +123,8 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen 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); } /** Updates the embedding rules applied to future activity launches. */ @@ -120,6 +139,16 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen } } + @Override + public void setSplitAttributesCalculator(@NonNull SplitAttributesCalculator calculator) { + // TODO: Implement this method + } + + @Override + public void clearSplitAttributesCalculator() { + // TODO: Implement this method + } + @NonNull List<EmbeddingRule> getSplitRules() { return mSplitRules; @@ -136,161 +165,327 @@ 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.getTaskConfiguration()); + 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); + container.setInfo(wct, taskFragmentInfo); + if (container.isFinished()) { + mPresenter.cleanupContainer(wct, container, false /* shouldFinishDependent */); + } else { + // Update with the latest Task configuration. + updateContainer(wct, container); + } + } + + /** + * 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); - } else if (wasInPip) { - // Exit PIP. - // Updates the presentation of the container. Expand or launch placeholder if - // needed. - updateContainer(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 */); } - mPresenter.applyTransaction(wct); - updateCallbackIfNecessary(); + } 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 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 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()); } + cleanupTaskFragment(taskFragmentInfo.getFragmentToken()); } - @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 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 parentConfig Config of the parent Task. + */ + @VisibleForTesting + @GuardedBy("mLock") + void onTaskFragmentParentInfoChanged(@NonNull WindowContainerTransaction wct, + int taskId, @NonNull Configuration parentConfig) { + onTaskConfigurationChanged(taskId, parentConfig); + if (isInPictureInPicture(parentConfig)) { + // No need to update presentation in PIP until the Task exit PIP. + return; + } + final TaskContainer taskContainer = getTaskContainer(taskId); + if (taskContainer == null || taskContainer.isEmpty()) { + Log.e(TAG, "onTaskFragmentParentInfoChanged on empty Task id=" + taskId); + return; + } + // 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); } } } - @Override - public void onActivityReparentToTask(int taskId, @NonNull Intent activityIntent, + /** + * 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) { - 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; - } - - 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. - final WindowContainerTransaction wct = new WindowContainerTransaction(); - TaskFragmentContainer targetContainer = resolveStartActivityIntent(wct, taskId, - activityIntent, null /* launchingActivity */); - if (targetContainer == null) { + // 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. */ private void cleanupTaskFragment(@NonNull IBinder taskFragmentToken) { for (int i = mTaskContainers.size() - 1; i >= 0; i--) { final TaskContainer taskContainer = mTaskContainers.valueAt(i); @@ -332,6 +527,11 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen * bounds is large enough for at least one split rule. */ private void updateAnimationOverride(@NonNull TaskContainer taskContainer) { + 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() || !taskContainer.isWindowingModeInitialized()) { // We don't know about the Task bounds/windowingMode yet. @@ -364,10 +564,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 +583,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 +593,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 +617,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 +637,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 +664,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,20 +693,20 @@ 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, + @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) { try { - mPresenter.startActivityToSide(launchingActivity, intent, options, sideRule, + mPresenter.startActivityToSide(wct, launchingActivity, intent, options, sideRule, isPlaceholder); } catch (Exception e) { if (failureCallback != null) { @@ -514,15 +719,17 @@ 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); } } @@ -607,8 +814,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; @@ -626,23 +837,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 +872,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 +892,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 */); } /** @@ -793,6 +1050,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, @@ -865,6 +1123,7 @@ 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) { @@ -909,6 +1168,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 +1282,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 +1310,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen // Skip position update - one or both containers are finished. return; } - if (dismissPlaceholderIfNecessary(splitContainer)) { + if (dismissPlaceholderIfNecessary(wct, splitContainer)) { // Placeholder was finished, the positions will be updated when its container is emptied return; } @@ -1111,16 +1372,20 @@ 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) { + @GuardedBy("mLock") + boolean launchPlaceholderIfNecessary(@NonNull WindowContainerTransaction wct, + @NonNull Activity activity, boolean isOnCreated) { if (activity.isFinishing()) { return false; } @@ -1154,7 +1419,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen // TODO(b/190433398): Handle failed request final Bundle options = getPlaceholderOptions(activity, isOnCreated); - startActivityToSide(activity, placeholderRule.getPlaceholderIntent(), options, + startActivityToSide(wct, activity, placeholderRule.getPlaceholderIntent(), options, placeholderRule, null /* failureCallback */, true /* isPlaceholder */); return true; } @@ -1181,7 +1446,9 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen } @VisibleForTesting - boolean dismissPlaceholderIfNecessary(@NonNull SplitContainer splitContainer) { + @GuardedBy("mLock") + boolean dismissPlaceholderIfNecessary(@NonNull WindowContainerTransaction wct, + @NonNull SplitContainer splitContainer) { if (!splitContainer.isPlaceholderContainer()) { return false; } @@ -1195,7 +1462,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen return false; } - mPresenter.cleanupContainer(splitContainer.getSecondaryContainer(), + mPresenter.cleanupContainer(wct, splitContainer.getSecondaryContainer(), false /* shouldFinishDependent */); return true; } @@ -1259,13 +1526,20 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen .toActivityStack(); final ActivityStack secondaryContainer = container.getSecondaryContainer() .toActivityStack(); + final SplitAttributes.SplitType splitType = shouldShowSideBySide(container) + ? new SplitAttributes.SplitType.RatioSplitType( + container.getSplitRule().getSplitRatio()) + : new SplitAttributes.SplitType.ExpandContainersSplitType(); 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); + // TODO(b/241042437): use v2 APIs for splitAttributes + new SplitAttributes.Builder() + .setSplitType(splitType) + .setLayoutDirection(container.getSplitRule().getLayoutDirection()) + .build() + ); splitStates.add(splitState); } } @@ -1373,16 +1647,22 @@ 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; } /** @@ -1456,10 +1736,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 +1767,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 +1807,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 +1816,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,7 +1905,7 @@ 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) { if (!isContainerReusableRule(rule1) || !isContainerReusableRule(rule2)) { return false; } @@ -1582,7 +1913,8 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen } /** Whether the two rules have the same presentation. */ - private static boolean haveSamePresentation(SplitPairRule rule1, SplitPairRule rule2) { + private static boolean haveSamePresentation(@NonNull SplitPairRule rule1, + @NonNull SplitPairRule rule2) { // TODO(b/231655482): add util method to do the comparison in SplitPairRule. return rule1.getSplitRatio() == rule2.getSplitRatio() && rule1.getLayoutDirection() == rule2.getLayoutDirection() @@ -1596,7 +1928,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen * 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..2ef8e4c64855 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java @@ -38,6 +38,7 @@ 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; @@ -102,37 +103,18 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { 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( @@ -190,10 +172,10 @@ 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(); - + @GuardedBy("mController.mLock") + void createNewSplitContainer(@NonNull WindowContainerTransaction wct, + @NonNull Activity primaryActivity, @NonNull Activity secondaryActivity, + @NonNull SplitPairRule rule) { final Rect parentBounds = getParentContainerBounds(primaryActivity); final Pair<Size, Size> minDimensionsPair = getActivitiesMinDimensionsPair(primaryActivity, secondaryActivity); @@ -207,8 +189,10 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { 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, @@ -219,8 +203,6 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { minDimensionsPair); mController.registerSplit(wct, primaryContainer, primaryActivity, secondaryContainer, rule); - - applyTransaction(wct); } /** @@ -262,7 +244,8 @@ 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, + void startActivityToSide(@NonNull WindowContainerTransaction wct, + @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( @@ -284,7 +267,6 @@ 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); startActivityToSide(wct, primaryContainer.getTaskFragmentToken(), primaryRectBounds, @@ -294,7 +276,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); } /** @@ -502,14 +483,14 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { } @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)); } 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..b5636777568e 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java @@ -21,8 +21,6 @@ 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; @@ -31,6 +29,9 @@ import android.os.IBinder; import android.util.ArraySet; import android.window.TaskFragmentInfo; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + import java.util.ArrayList; import java.util.List; import java.util.Set; @@ -136,6 +137,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 +169,8 @@ class TaskContainer { } return null; } + + int indexOf(@NonNull TaskFragmentContainer child) { + return mContainers.indexOf(child); + } } 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..626e0d990a6d 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; @@ -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..f24401f0cd53 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; @@ -46,7 +49,6 @@ import androidx.window.util.DataProducer; import java.util.ArrayList; import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.Set; import java.util.function.Consumer; @@ -61,12 +63,15 @@ 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 Map<IBinder, WindowContextConfigListener> mWindowContextConfigListeners = + new ArrayMap<>(); + + public WindowLayoutComponentImpl(@NonNull Context context) { ((Application) context.getApplicationContext()) .registerActivityLifecycleCallbacks(new NotifyOnConfigurationChanged()); RawFoldingFeatureProducer foldingFeatureProducer = new RawFoldingFeatureProducer(context); @@ -81,40 +86,73 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent { * @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 +166,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 +185,25 @@ 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) { + 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); } } - @NonNull - private WindowLayoutInfo getWindowLayoutInfo(@NonNull Activity activity) { - List<DisplayFeature> displayFeatures = getDisplayFeatures(activity); - return new WindowLayoutInfo(displayFeatures); + /** + * 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); } /** @@ -173,97 +219,98 @@ 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) { 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"); + if (!shouldReportDisplayFeatures(context)) { 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; - } + int displayId = context.getDisplay().getDisplayId(); + for (CommonFoldingFeature baseFeature : storedFeatures) { + Integer state = convertToExtensionState(baseFeature.getState()); + if (state == null) { + continue; + } + Rect featureRect = baseFeature.getRect(); + rotateRectToDisplayRotation(displayId, featureRect); + transformToWindowSpaceRect(context, 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 { + 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..31bf96313a95 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/util/ExtensionHelper.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/util/ExtensionHelper.java @@ -21,14 +21,15 @@ 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.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,12 +87,9 @@ 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; - } + public static void transformToWindowSpaceRect(@NonNull @UiContext Context context, + Rect inOutRect) { + Rect windowRect = getWindowBounds(context); if (!Rect.intersects(inOutRect, windowRect)) { inOutRect.setEmpty(); return; @@ -103,9 +101,9 @@ public final class ExtensionHelper { /** * 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/JetpackTaskFragmentOrganizerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java index 4d2595275f20..58a627bafa16 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 @@ -20,7 +20,6 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_ID; -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; @@ -33,9 +32,9 @@ import static org.mockito.Mockito.never; import android.content.Intent; import android.content.res.Configuration; 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 +55,8 @@ import java.util.ArrayList; * Build/Install/Run: * atest WMJetpackUnitTests:JetpackTaskFragmentOrganizerTest */ +// Suppress GuardedBy warning on unit tests +@SuppressWarnings("GuardedBy") @Presubmit @SmallTest @RunWith(AndroidJUnit4.class) @@ -64,10 +65,7 @@ public class JetpackTaskFragmentOrganizerTest { private WindowContainerTransaction mTransaction; @Mock private JetpackTaskFragmentOrganizer.TaskFragmentCallback mCallback; - @Mock private SplitController mSplitController; - @Mock - private Handler mHandler; private JetpackTaskFragmentOrganizer mOrganizer; @Before @@ -75,8 +73,9 @@ public class JetpackTaskFragmentOrganizerTest { MockitoAnnotations.initMocks(this); mOrganizer = new JetpackTaskFragmentOrganizer(Runnable::run, mCallback); mOrganizer.registerOrganizer(); + mSplitController = new SplitController(); spyOn(mOrganizer); - doReturn(mHandler).when(mSplitController).getHandler(); + spyOn(mSplitController); } @Test @@ -119,7 +118,7 @@ public class JetpackTaskFragmentOrganizerTest { 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 +126,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..58870a66feea 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,8 +16,16 @@ 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 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.SPLIT_RATIO; import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_BOUNDS; @@ -66,6 +74,8 @@ import android.os.Handler; import android.os.IBinder; import android.platform.test.annotations.Presubmit; import android.window.TaskFragmentInfo; +import android.window.TaskFragmentOrganizer; +import android.window.TaskFragmentTransaction; import android.window.WindowContainerTransaction; import androidx.test.core.app.ApplicationProvider; @@ -88,6 +98,8 @@ import java.util.List; * Build/Install/Run: * atest WMJetpackUnitTests:SplitControllerTest */ +// Suppress GuardedBy warning on unit tests +@SuppressWarnings("GuardedBy") @Presubmit @SmallTest @RunWith(AndroidJUnit4.class) @@ -115,7 +127,7 @@ public class SplitControllerTest { 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); @@ -157,14 +169,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,7 +188,7 @@ 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); @@ -186,9 +198,10 @@ public class SplitControllerTest { @Test public void testOnTaskFragmentAppearEmptyTimeout() { final TaskFragmentContainer tf = mSplitController.newContainer(mActivity, TASK_ID); - mSplitController.onTaskFragmentAppearEmptyTimeout(tf); + mSplitController.onTaskFragmentAppearEmptyTimeout(mTransaction, tf); - verify(mSplitPresenter).cleanupContainer(tf, false /* shouldFinishDependent */); + verify(mSplitPresenter).cleanupContainer(mTransaction, tf, + false /* shouldFinishDependent */); } @Test @@ -228,8 +241,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,7 +262,7 @@ 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); @@ -263,7 +276,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 +285,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 +298,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 +306,57 @@ public class SplitControllerTest { } @Test + public void testOnStartActivityResultError() { + final Intent intent = new Intent(); + final TaskContainer taskContainer = new TaskContainer(TASK_ID); + 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 +518,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 +552,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 +560,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 +570,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 +584,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,11 +602,11 @@ 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 */); } @@ -579,11 +619,11 @@ 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(), + verify(mSplitPresenter, never()).startActivityToSide(any(), any(), any(), any(), any(), anyBoolean()); } @@ -595,11 +635,11 @@ 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 */); } @@ -611,11 +651,11 @@ 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(), + verify(mSplitPresenter, never()).startActivityToSide(any(), any(), any(), any(), any(), anyBoolean()); } @@ -628,11 +668,11 @@ 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 */); } @@ -655,7 +695,7 @@ public class SplitControllerTest { secondaryContainer, splitRule); clearInvocations(mSplitController); - final boolean result = mSplitController.resolveActivityToContainer(mActivity, + final boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity, false /* isOnReparent */); assertTrue(result); @@ -684,7 +724,7 @@ public class SplitControllerTest { final Activity launchedActivity = createMockActivity(); primaryContainer.addPendingAppearedActivity(launchedActivity); - assertFalse(mSplitController.resolveActivityToContainer(launchedActivity, + assertFalse(mSplitController.resolveActivityToContainer(mTransaction, launchedActivity, false /* isOnReparent */)); } @@ -696,7 +736,7 @@ 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); @@ -714,7 +754,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); @@ -739,7 +779,7 @@ public class SplitControllerTest { mActivity, secondaryContainer, placeholderRule); - final boolean result = mSplitController.resolveActivityToContainer(mActivity, + final boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity, false /* isOnReparent */); assertTrue(result); @@ -753,7 +793,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 +809,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 +835,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 +856,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 +882,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 +900,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 +918,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 */)); } @@ -947,6 +990,104 @@ public class SplitControllerTest { 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 Configuration taskConfig = new Configuration(); + transaction.addChange(new TaskFragmentTransaction.Change( + TYPE_TASK_FRAGMENT_PARENT_INFO_CHANGED) + .setTaskId(TASK_ID) + .setTaskConfiguration(taskConfig)); + mSplitController.onTransactionReady(transaction); + + verify(mSplitController).onTaskFragmentParentInfoChanged(any(), eq(TASK_ID), + eq(taskConfig)); + 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()); + } + /** Creates a mock activity in the organizer process. */ private Activity createMockActivity() { final Activity activity = mock(Activity.class); @@ -970,7 +1111,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); } 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..25f0e25eec75 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 @@ -39,6 +39,7 @@ 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; @@ -78,6 +79,8 @@ import org.mockito.MockitoAnnotations; * Build/Install/Run: * atest WMJetpackUnitTests:SplitPresenterTest */ +// Suppress GuardedBy warning on unit tests +@SuppressWarnings("GuardedBy") @Presubmit @SmallTest @RunWith(AndroidJUnit4.class) @@ -226,15 +229,14 @@ public class SplitPresenterTest { mTransaction, splitContainer, mActivity, secondaryActivity, null /* secondaryIntent */)); - primaryTf.setInfo(createMockTaskFragmentInfo(primaryTf, mActivity)); - secondaryTf.setInfo(createMockTaskFragmentInfo(secondaryTf, secondaryActivity)); + 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()); clearInvocations(mPresenter); @@ -242,10 +244,28 @@ public class SplitPresenterTest { 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) + .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)); } private Activity createMockActivity() { 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..082774e048a9 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 @@ -19,6 +19,8 @@ package androidx.window.extensions.embedding; import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_ID; import static androidx.window.extensions.embedding.EmbeddingTestUtils.createMockTaskFragmentInfo; +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,25 +63,27 @@ 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(); } @@ -123,7 +126,7 @@ public class TaskFragmentContainerTest { // Remove all references after the container has appeared in server. doReturn(new ArrayList<>()).when(mInfo).getActivities(); - container.setInfo(mInfo); + container.setInfo(mTransaction, mInfo); container.finish(true /* shouldFinishDependent */, mPresenter, wct, mController); verify(mActivity, never()).finish(); @@ -137,7 +140,7 @@ public class TaskFragmentContainerTest { 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); @@ -163,7 +166,7 @@ public class TaskFragmentContainerTest { final TaskFragmentInfo info0 = createMockTaskFragmentInfo(pendingActivityContainer, mActivity); - pendingActivityContainer.setInfo(info0); + pendingActivityContainer.setInfo(mTransaction, info0); assertTrue(pendingActivityContainer.mPendingAppearedActivities.isEmpty()); @@ -175,7 +178,7 @@ public class TaskFragmentContainerTest { final TaskFragmentInfo info1 = createMockTaskFragmentInfo(pendingIntentContainer, mActivity); - pendingIntentContainer.setInfo(info1); + pendingIntentContainer.setInfo(mTransaction, info1); assertNull(pendingIntentContainer.getPendingAppearedIntent()); } @@ -191,49 +194,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() { + doNothing().when(mController).onTaskFragmentAppearEmptyTimeout(any(), any()); final TaskContainer taskContainer = new TaskContainer(TASK_ID); 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); @@ -259,7 +264,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()); @@ -283,6 +288,18 @@ public class TaskFragmentContainerTest { } @Test + public void testIsAbove() { + final TaskContainer taskContainer = new TaskContainer(TASK_ID); + 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 TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */, @@ -294,11 +311,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 = new TaskContainer(TASK_ID); + 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/OWNERS b/libs/WindowManager/OWNERS index 780e4c1632f7..2c61df96eb03 100644 --- a/libs/WindowManager/OWNERS +++ b/libs/WindowManager/OWNERS @@ -1,6 +1,3 @@ set noparent include /services/core/java/com/android/server/wm/OWNERS - -# Give submodule owners in shell resource approval -per-file Shell/res*/*/*.xml = hwwang@google.com, lbill@google.com, madym@google.com diff --git a/libs/WindowManager/Shell/OWNERS b/libs/WindowManager/Shell/OWNERS new file mode 100644 index 000000000000..4b125904004a --- /dev/null +++ b/libs/WindowManager/Shell/OWNERS @@ -0,0 +1,4 @@ +xutan@google.com + +# Give submodule owners in shell resource approval +per-file res*/*/*.xml = hwwang@google.com, lbill@google.com, madym@google.com diff --git a/libs/WindowManager/Shell/res/animator/tv_pip_menu_action_button_animator.xml b/libs/WindowManager/Shell/res/animator/tv_window_menu_action_button_animator.xml index 7475abac4695..7475abac4695 100644 --- a/libs/WindowManager/Shell/res/animator/tv_pip_menu_action_button_animator.xml +++ b/libs/WindowManager/Shell/res/animator/tv_window_menu_action_button_animator.xml 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/color/tv_pip_menu_close_icon.xml b/libs/WindowManager/Shell/res/color/tv_window_menu_close_icon.xml index ce8640df0093..67467bbc72ae 100644 --- a/libs/WindowManager/Shell/res/color/tv_pip_menu_close_icon.xml +++ b/libs/WindowManager/Shell/res/color/tv_window_menu_close_icon.xml @@ -15,5 +15,5 @@ ~ limitations under the License. --> <selector xmlns:android="http://schemas.android.com/apk/res/android"> - <item android:color="@color/tv_pip_menu_icon_unfocused" /> + <item android:color="@color/tv_window_menu_icon_unfocused" /> </selector>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/color/tv_pip_menu_icon_bg.xml b/libs/WindowManager/Shell/res/color/tv_window_menu_close_icon_bg.xml index 4f5e63dac5c0..4182bfeefa1b 100644 --- a/libs/WindowManager/Shell/res/color/tv_pip_menu_icon_bg.xml +++ b/libs/WindowManager/Shell/res/color/tv_window_menu_close_icon_bg.xml @@ -1,6 +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. @@ -16,6 +16,6 @@ --> <selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:state_focused="true" - android:color="@color/tv_pip_menu_icon_bg_focused" /> - <item android:color="@color/tv_pip_menu_icon_bg_unfocused" /> + android:color="@color/tv_window_menu_close_icon_bg_focused" /> + <item android:color="@color/tv_window_menu_close_icon_bg_unfocused" /> </selector>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/color/tv_pip_menu_icon.xml b/libs/WindowManager/Shell/res/color/tv_window_menu_icon.xml index 275870450493..45205d2a7138 100644 --- a/libs/WindowManager/Shell/res/color/tv_pip_menu_icon.xml +++ b/libs/WindowManager/Shell/res/color/tv_window_menu_icon.xml @@ -1,6 +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. @@ -16,8 +16,8 @@ --> <selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:state_focused="true" - android:color="@color/tv_pip_menu_icon_focused" /> + android:color="@color/tv_window_menu_icon_focused" /> <item android:state_enabled="false" - android:color="@color/tv_pip_menu_icon_disabled" /> - <item android:color="@color/tv_pip_menu_icon_unfocused" /> + android:color="@color/tv_window_menu_icon_disabled" /> + <item android:color="@color/tv_window_menu_icon_unfocused" /> </selector>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/color/tv_pip_menu_close_icon_bg.xml b/libs/WindowManager/Shell/res/color/tv_window_menu_icon_bg.xml index 6cbf66f00df7..1bd26e1d6583 100644 --- a/libs/WindowManager/Shell/res/color/tv_pip_menu_close_icon_bg.xml +++ b/libs/WindowManager/Shell/res/color/tv_window_menu_icon_bg.xml @@ -16,6 +16,6 @@ --> <selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:state_focused="true" - android:color="@color/tv_pip_menu_close_icon_bg_focused" /> - <item android:color="@color/tv_pip_menu_close_icon_bg_unfocused" /> + android:color="@color/tv_window_menu_icon_bg_focused" /> + <item android:color="@color/tv_window_menu_icon_bg_unfocused" /> </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/drawable/tv_pip_button_bg.xml b/libs/WindowManager/Shell/res/drawable/tv_pip_button_bg.xml deleted file mode 100644 index 1938f4562e97..000000000000 --- a/libs/WindowManager/Shell/res/drawable/tv_pip_button_bg.xml +++ /dev/null @@ -1,21 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - 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. ---> -<shape xmlns:android="http://schemas.android.com/apk/res/android" - android:shape="rectangle"> - <corners android:radius="@dimen/pip_menu_button_radius" /> - <solid android:color="@color/tv_pip_menu_icon_bg" /> -</shape>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/drawable/tv_pip_menu_border.xml b/libs/WindowManager/Shell/res/drawable/tv_pip_menu_border.xml index 846fdb3e8a58..7085a2c72c86 100644 --- a/libs/WindowManager/Shell/res/drawable/tv_pip_menu_border.xml +++ b/libs/WindowManager/Shell/res/drawable/tv_pip_menu_border.xml @@ -15,7 +15,7 @@ ~ limitations under the License. --> <selector xmlns:android="http://schemas.android.com/apk/res/android" - android:exitFadeDuration="@integer/pip_menu_fade_animation_duration"> + android:exitFadeDuration="@integer/tv_window_menu_fade_animation_duration"> <item android:state_activated="true"> <shape android:shape="rectangle"> <corners android:radius="@dimen/pip_menu_border_corner_radius" /> diff --git a/libs/WindowManager/Shell/res/drawable/tv_split_menu_ic_focus.xml b/libs/WindowManager/Shell/res/drawable/tv_split_menu_ic_focus.xml new file mode 100644 index 000000000000..a348b148afb4 --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/tv_split_menu_ic_focus.xml @@ -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. + --> + +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="@dimen/tv_window_menu_icon_size" + android:height="@dimen/tv_window_menu_icon_size" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + + <path + android:fillColor="#FFFFFF" + android:pathData="M17,4h3c1.1,0 2,0.9 2,2v2h-2L20,6h-3L17,4zM4,8L4,6h3L7,4L4,4c-1.1,0 -2,0.9 -2,2v2h2zM20,16v2h-3v2h3c1.1,0 2,-0.9 2,-2v-2h-2zM7,18L4,18v-2L2,16v2c0,1.1 0.9,2 2,2h3v-2zM18,8L6,8v8h12L18,8z"/> +</vector> diff --git a/libs/WindowManager/Shell/res/drawable/tv_split_menu_ic_swap.xml b/libs/WindowManager/Shell/res/drawable/tv_split_menu_ic_swap.xml new file mode 100644 index 000000000000..c5d54c5fa4f2 --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/tv_split_menu_ic_swap.xml @@ -0,0 +1,25 @@ +<!-- + ~ 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/tv_window_menu_icon_size" + android:height="@dimen/tv_window_menu_icon_size" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + <path + android:fillColor="#FFFFFF" + android:pathData="M6.99,11L3,15l3.99,4v-3H14v-2H6.99v-3zM21,9l-3.99,-4v3H10v2h7.01v3L21,9z"/> +</vector>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/drawable/tv_window_button_bg.xml b/libs/WindowManager/Shell/res/drawable/tv_window_button_bg.xml new file mode 100644 index 000000000000..2dba37daf059 --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/tv_window_button_bg.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. + --> +<shape xmlns:android="http://schemas.android.com/apk/res/android" + android:shape="rectangle"> + <corners android:radius="@dimen/tv_window_menu_button_radius" /> + <solid android:color="@color/tv_window_menu_icon_bg" /> +</shape>
\ 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/layout/tv_pip_menu.xml b/libs/WindowManager/Shell/res/layout/tv_pip_menu.xml index 7a3ee23d8cdc..afd3aac9b461 100644 --- a/libs/WindowManager/Shell/res/layout/tv_pip_menu.xml +++ b/libs/WindowManager/Shell/res/layout/tv_pip_menu.xml @@ -16,92 +16,98 @@ --> <!-- Layout for TvPipMenuView --> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" - android:id="@+id/tv_pip_menu" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:gravity="center|top"> + android:id="@+id/tv_pip_menu" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:gravity="center|top"> <!-- Matches the PiP app content --> - <View + <FrameLayout android:id="@+id/tv_pip" android:layout_width="0dp" android:layout_height="0dp" - android:alpha="0" - android:background="@color/tv_pip_menu_background" android:layout_marginTop="@dimen/pip_menu_outer_space" android:layout_marginStart="@dimen/pip_menu_outer_space" - android:layout_marginEnd="@dimen/pip_menu_outer_space"/> - - <ScrollView - android:id="@+id/tv_pip_menu_scroll" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:layout_alignTop="@+id/tv_pip" - android:layout_alignStart="@+id/tv_pip" - android:layout_alignEnd="@+id/tv_pip" - android:layout_alignBottom="@+id/tv_pip" - android:scrollbars="none" - android:visibility="gone"/> - - <HorizontalScrollView - android:id="@+id/tv_pip_menu_horizontal_scroll" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:layout_alignTop="@+id/tv_pip" - android:layout_alignStart="@+id/tv_pip" - android:layout_alignEnd="@+id/tv_pip" - android:layout_alignBottom="@+id/tv_pip" - android:scrollbars="none"> - - <LinearLayout - android:id="@+id/tv_pip_menu_action_buttons" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:orientation="horizontal" - android:alpha="0"> + android:layout_marginEnd="@dimen/pip_menu_outer_space"> - <Space - android:layout_width="@dimen/pip_menu_button_wrapper_margin" - android:layout_height="@dimen/pip_menu_button_wrapper_margin"/> - - <com.android.wm.shell.pip.tv.TvPipMenuActionButton - android:id="@+id/tv_pip_menu_fullscreen_button" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:src="@drawable/pip_ic_fullscreen_white" - android:text="@string/pip_fullscreen" /> + <View + android:id="@+id/tv_pip_menu_background" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="@color/tv_pip_menu_background" + android:alpha="0"/> - <com.android.wm.shell.pip.tv.TvPipMenuActionButton - android:id="@+id/tv_pip_menu_close_button" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:src="@drawable/pip_ic_close_white" - android:text="@string/pip_close" /> + <View + android:id="@+id/tv_pip_menu_dim_layer" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="@color/tv_pip_menu_dim_layer" + android:alpha="0"/> - <!-- More TvPipMenuActionButtons may be added here at runtime. --> + <ScrollView + android:id="@+id/tv_pip_menu_scroll" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:scrollbars="none" + android:visibility="gone"/> - <com.android.wm.shell.pip.tv.TvPipMenuActionButton - android:id="@+id/tv_pip_menu_move_button" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:src="@drawable/pip_ic_move_white" - android:text="@string/pip_move" /> + <HorizontalScrollView + android:id="@+id/tv_pip_menu_horizontal_scroll" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:scrollbars="none"> - <com.android.wm.shell.pip.tv.TvPipMenuActionButton - android:id="@+id/tv_pip_menu_expand_button" + <LinearLayout + android:id="@+id/tv_pip_menu_action_buttons" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:src="@drawable/pip_ic_collapse" - android:visibility="gone" - android:text="@string/pip_collapse" /> - - <Space - android:layout_width="@dimen/pip_menu_button_wrapper_margin" - android:layout_height="@dimen/pip_menu_button_wrapper_margin"/> - - </LinearLayout> - </HorizontalScrollView> + android:orientation="horizontal" + android:alpha="0"> + + <Space + android:layout_width="@dimen/pip_menu_button_wrapper_margin" + android:layout_height="@dimen/pip_menu_button_wrapper_margin"/> + + <com.android.wm.shell.common.TvWindowMenuActionButton + android:id="@+id/tv_pip_menu_fullscreen_button" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:src="@drawable/pip_ic_fullscreen_white" + android:text="@string/pip_fullscreen" /> + + <com.android.wm.shell.common.TvWindowMenuActionButton + android:id="@+id/tv_pip_menu_close_button" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:src="@drawable/pip_ic_close_white" + android:text="@string/pip_close" /> + + <!-- More TvWindowMenuActionButtons may be added here at runtime. --> + + <com.android.wm.shell.common.TvWindowMenuActionButton + android:id="@+id/tv_pip_menu_move_button" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:src="@drawable/pip_ic_move_white" + android:text="@string/pip_move" /> + + <com.android.wm.shell.common.TvWindowMenuActionButton + android:id="@+id/tv_pip_menu_expand_button" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:src="@drawable/pip_ic_collapse" + android:visibility="gone" + android:text="@string/pip_collapse" /> + + <Space + android:layout_width="@dimen/pip_menu_button_wrapper_margin" + android:layout_height="@dimen/pip_menu_button_wrapper_margin"/> + + </LinearLayout> + </HorizontalScrollView> + </FrameLayout> + <!-- Frame around the content, just overlapping the corners to make them round --> <View android:id="@+id/tv_pip_border" android:layout_width="0dp" @@ -111,6 +117,7 @@ android:layout_marginEnd="@dimen/pip_menu_outer_space_frame" android:background="@drawable/tv_pip_menu_border"/> + <!-- Temporarily extending the background to show an edu text hint for opening the menu --> <FrameLayout android:id="@+id/tv_pip_menu_edu_text_container" android:layout_width="match_parent" @@ -128,6 +135,7 @@ android:layout_height="@dimen/pip_menu_edu_text_view_height" android:layout_gravity="bottom|center" android:gravity="center" + android:clickable="false" android:paddingBottom="@dimen/pip_menu_border_width" android:text="@string/pip_edu_text" android:singleLine="true" @@ -137,6 +145,7 @@ android:textAppearance="@style/TvPipEduText"/> </FrameLayout> + <!-- Frame around the PiP content + edu text hint - used to highlight open menu --> <View android:id="@+id/tv_pip_menu_frame" android:layout_width="match_parent" @@ -144,6 +153,17 @@ android:layout_margin="@dimen/pip_menu_outer_space_frame" android:background="@drawable/tv_pip_menu_border"/> + <!-- Move menu --> + <com.android.wm.shell.common.TvWindowMenuActionButton + android:id="@+id/tv_pip_menu_done_button" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_centerHorizontal="true" + android:layout_centerVertical="true" + android:src="@drawable/pip_ic_close_white" + android:visibility="gone" + android:text="@string/a11y_action_pip_move_done" /> + <ImageView android:id="@+id/tv_pip_menu_arrow_up" android:layout_width="@dimen/pip_menu_arrow_size" @@ -151,6 +171,7 @@ android:layout_centerHorizontal="true" android:layout_alignParentTop="true" android:alpha="0" + android:contentDescription="@string/a11y_action_pip_move_up" android:elevation="@dimen/pip_menu_arrow_elevation" android:src="@drawable/pip_ic_move_up" /> @@ -161,6 +182,7 @@ android:layout_centerVertical="true" android:layout_alignParentRight="true" android:alpha="0" + android:contentDescription="@string/a11y_action_pip_move_right" android:elevation="@dimen/pip_menu_arrow_elevation" android:src="@drawable/pip_ic_move_right" /> @@ -171,6 +193,7 @@ android:layout_centerHorizontal="true" android:layout_alignParentBottom="true" android:alpha="0" + android:contentDescription="@string/a11y_action_pip_move_down" android:elevation="@dimen/pip_menu_arrow_elevation" android:src="@drawable/pip_ic_move_down" /> @@ -181,6 +204,7 @@ android:layout_centerVertical="true" android:layout_alignParentLeft="true" android:alpha="0" + android:contentDescription="@string/a11y_action_pip_move_left" android:elevation="@dimen/pip_menu_arrow_elevation" android:src="@drawable/pip_ic_move_left" /> </RelativeLayout> diff --git a/libs/WindowManager/Shell/res/layout/tv_pip_menu_action_button.xml b/libs/WindowManager/Shell/res/layout/tv_pip_menu_action_button.xml deleted file mode 100644 index db96d8de4094..000000000000 --- a/libs/WindowManager/Shell/res/layout/tv_pip_menu_action_button.xml +++ /dev/null @@ -1,40 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - 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. ---> -<!-- Layout for TvPipMenuActionButton --> -<FrameLayout - xmlns:android="http://schemas.android.com/apk/res/android" - android:id="@+id/button" - android:layout_width="@dimen/pip_menu_button_size" - android:layout_height="@dimen/pip_menu_button_size" - android:padding="@dimen/pip_menu_button_margin" - android:stateListAnimator="@animator/tv_pip_menu_action_button_animator" - android:focusable="true"> - - <View android:id="@+id/background" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:layout_gravity="center" - android:duplicateParentState="true" - android:background="@drawable/tv_pip_button_bg"/> - - <ImageView android:id="@+id/icon" - android:layout_width="@dimen/pip_menu_icon_size" - android:layout_height="@dimen/pip_menu_icon_size" - android:layout_gravity="center" - android:duplicateParentState="true" - android:tint="@color/tv_pip_menu_icon" /> -</FrameLayout>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/layout/tv_split_menu_view.xml b/libs/WindowManager/Shell/res/layout/tv_split_menu_view.xml new file mode 100644 index 000000000000..e0fa59c9f157 --- /dev/null +++ b/libs/WindowManager/Shell/res/layout/tv_split_menu_view.xml @@ -0,0 +1,125 @@ +<!-- + ~ 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. + --> +<!-- Layout for TvSplitMenuView --> +<com.android.wm.shell.splitscreen.tv.TvSplitMenuView + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_height="match_parent" + android:layout_width="match_parent"> + + <LinearLayout + android:orientation="horizontal" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:gravity="center"> + + <LinearLayout + android:id="@+id/tv_split_main_menu" + android:layout_width="0dp" + android:layout_weight="1" + android:layout_height="match_parent" + android:orientation="vertical" + android:gravity="center"> + + <Space + android:layout_width="match_parent" + android:layout_height="0dp" + android:layout_weight="1" /> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="0dp" + android:layout_weight="1" + android:orientation="vertical" + android:gravity="center"> + + <com.android.wm.shell.common.TvWindowMenuActionButton + android:id="@+id/tv_split_main_menu_focus_button" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:src="@drawable/tv_split_menu_ic_focus" /> + + </LinearLayout> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="0dp" + android:layout_weight="1" + android:orientation="vertical" + android:gravity="center"> + + <com.android.wm.shell.common.TvWindowMenuActionButton + android:id="@+id/tv_split_main_menu_close_button" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:src="@drawable/pip_ic_close_white" /> + + </LinearLayout> + + </LinearLayout> + + <com.android.wm.shell.common.TvWindowMenuActionButton + android:id="@+id/tv_split_menu_swap_stages" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:src="@drawable/tv_split_menu_ic_swap" /> + + <LinearLayout + android:id="@+id/tv_split_side_menu" + android:layout_width="0dp" + android:layout_weight="1" + android:layout_height="match_parent" + android:orientation="vertical" + android:gravity="center"> + + <Space + android:layout_width="match_parent" + android:layout_height="0dp" + android:layout_weight="1" /> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="0dp" + android:layout_weight="1" + android:orientation="vertical" + android:gravity="center"> + + <com.android.wm.shell.common.TvWindowMenuActionButton + android:id="@+id/tv_split_side_menu_focus_button" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:src="@drawable/tv_split_menu_ic_focus" /> + + </LinearLayout> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="0dp" + android:layout_weight="1" + android:orientation="vertical" + android:gravity="center"> + + <com.android.wm.shell.common.TvWindowMenuActionButton + android:id="@+id/tv_split_side_menu_close_button" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:src="@drawable/pip_ic_close_white" /> + + </LinearLayout> + + </LinearLayout> + + </LinearLayout> +</com.android.wm.shell.splitscreen.tv.TvSplitMenuView> diff --git a/libs/WindowManager/Shell/res/layout/tv_window_menu_action_button.xml b/libs/WindowManager/Shell/res/layout/tv_window_menu_action_button.xml new file mode 100644 index 000000000000..c4dbd39c729a --- /dev/null +++ b/libs/WindowManager/Shell/res/layout/tv_window_menu_action_button.xml @@ -0,0 +1,40 @@ +<?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. + --> +<!-- Layout for TvWindowMenuActionButton --> +<FrameLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/button" + android:layout_width="@dimen/tv_window_menu_button_size" + android:layout_height="@dimen/tv_window_menu_button_size" + android:padding="@dimen/tv_window_menu_button_margin" + android:stateListAnimator="@animator/tv_window_menu_action_button_animator" + android:focusable="true"> + + <View android:id="@+id/background" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_gravity="center" + android:duplicateParentState="true" + android:background="@drawable/tv_window_button_bg"/> + + <ImageView android:id="@+id/icon" + android:layout_width="@dimen/tv_window_menu_icon_size" + android:layout_height="@dimen/tv_window_menu_icon_size" + android:layout_gravity="center" + android:duplicateParentState="true" + android:tint="@color/tv_window_menu_icon" /> +</FrameLayout>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/values-af/strings.xml b/libs/WindowManager/Shell/res/values-af/strings.xml index 88382d77ce35..40c4c35773f8 100644 --- a/libs/WindowManager/Shell/res/values-af/strings.xml +++ b/libs/WindowManager/Shell/res/values-af/strings.xml @@ -22,6 +22,7 @@ <string name="pip_phone_settings" msgid="5468987116750491918">"Instellings"</string> <string name="pip_phone_enter_split" msgid="7042877263880641911">"Gaan by verdeelde skerm in"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Kieslys"</string> + <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Prent-in-prent-kieslys"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> is in beeld-in-beeld"</string> <string name="pip_notification_message" msgid="8854051911700302620">"As jy nie wil hê dat <xliff:g id="NAME">%s</xliff:g> hierdie kenmerk moet gebruik nie, tik om instellings oop te maak en skakel dit af."</string> <string name="pip_play" msgid="3496151081459417097">"Speel"</string> @@ -36,6 +37,7 @@ <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Program sal dalk nie op \'n sekondêre skerm werk nie."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Program steun nie begin op sekondêre skerms nie."</string> <string name="accessibility_divider" msgid="703810061635792791">"Skermverdeler"</string> + <string name="divider_title" msgid="5482989479865361192">"Skermverdeler"</string> <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Volskerm links"</string> <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Links 70%"</string> <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Links 50%"</string> @@ -72,17 +74,19 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Borrel"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Bestuur"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Borrel is toegemaak."</string> - <string name="restart_button_description" msgid="5887656107651190519">"Tik om hierdie program te herbegin en maak volskerm oop."</string> + <string name="restart_button_description" msgid="6712141648865547958">"Tik om hierdie program te herbegin vir ’n beter aansig."</string> <string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"Kamerakwessies?\nTik om aan te pas"</string> <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Nie opgelos nie?\nTik om terug te stel"</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Geen kamerakwessies nie? Tik om toe te maak."</string> - <!-- no translation found for letterbox_education_dialog_title (6688664582871779215) --> + <!-- no translation found for letterbox_education_dialog_title (7739895354143295358) --> <skip /> - <!-- no translation found for letterbox_education_dialog_subtext (4853542518367719562) --> + <!-- no translation found for letterbox_education_split_screen_text (6206339484068670830) --> <skip /> - <!-- no translation found for letterbox_education_screen_rotation_text (5085786687366339027) --> - <skip /> - <!-- no translation found for letterbox_education_reposition_text (1068293354123934727) --> + <!-- no translation found for letterbox_education_reposition_text (4589957299813220661) --> <skip /> <string name="letterbox_education_got_it" msgid="4057634570866051177">"Het dit"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Vou uit vir meer inligting."</string> + <string name="maximize_button_text" msgid="1650859196290301963">"Maksimeer"</string> + <string name="minimize_button_text" msgid="271592547935841753">"Maak klein"</string> + <string name="close_button_text" msgid="2913281996024033299">"Maak toe"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-af/strings_tv.xml b/libs/WindowManager/Shell/res/values-af/strings_tv.xml index 1bfe128b0917..6187ea46769c 100644 --- a/libs/WindowManager/Shell/res/values-af/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-af/strings_tv.xml @@ -19,7 +19,16 @@ 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="9135220303720555525">"Maak PIP toe"</string> + <string name="pip_close" msgid="2955969519031223530">"Maak toe"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Volskerm"</string> - <string name="pip_move" msgid="1544227837964635439">"Skuif PIP"</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_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.xml b/libs/WindowManager/Shell/res/values-am/strings.xml index 20d081f2f547..a183a0bf7e85 100644 --- a/libs/WindowManager/Shell/res/values-am/strings.xml +++ b/libs/WindowManager/Shell/res/values-am/strings.xml @@ -22,6 +22,7 @@ <string name="pip_phone_settings" msgid="5468987116750491918">"ቅንብሮች"</string> <string name="pip_phone_enter_split" msgid="7042877263880641911">"የተከፈለ ማያ ገጽን አስገባ"</string> <string name="pip_menu_title" msgid="5393619322111827096">"ምናሌ"</string> + <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"የስዕል-ላይ-ስዕል ምናሌ"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> በስዕል-ላይ-ስዕል ውስጥ ነው"</string> <string name="pip_notification_message" msgid="8854051911700302620">"<xliff:g id="NAME">%s</xliff:g> ይህን ባህሪ እንዲጠቀም ካልፈለጉ ቅንብሮችን ለመክፈት መታ ያድርጉና ያጥፉት።"</string> <string name="pip_play" msgid="3496151081459417097">"አጫውት"</string> @@ -36,6 +37,7 @@ <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"መተግበሪያ በሁለተኛ ማሳያ ላይ ላይሠራ ይችላል።"</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"መተግበሪያ በሁለተኛ ማሳያዎች ላይ ማስጀመርን አይደግፍም።"</string> <string name="accessibility_divider" msgid="703810061635792791">"የተከፈለ የማያ ገጽ ከፋይ"</string> + <string name="divider_title" msgid="5482989479865361192">"የተከፈለ የማያ ገጽ ከፋይ"</string> <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"የግራ ሙሉ ማያ ገጽ"</string> <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"ግራ 70%"</string> <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"ግራ 50%"</string> @@ -72,17 +74,19 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"አረፋ"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"ያቀናብሩ"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"አረፋ ተሰናብቷል።"</string> - <string name="restart_button_description" msgid="5887656107651190519">"ይህን መተግበሪያ ዳግም ለማስነሳት መታ ያድርጉ እና ወደ ሙሉ ማያ ገጽ ይሂዱ።"</string> + <string name="restart_button_description" msgid="6712141648865547958">"ለተሻለ ዕይታ ይህን መተግበሪያ ዳግም ለማስነሳት መታ ያድርጉ።"</string> <string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"የካሜራ ችግሮች አሉ?\nዳግም ለማበጀት መታ ያድርጉ"</string> <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"አልተስተካከለም?\nለማህደር መታ ያድርጉ"</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"ምንም የካሜራ ችግሮች የሉም? ለማሰናበት መታ ያድርጉ።"</string> - <!-- no translation found for letterbox_education_dialog_title (6688664582871779215) --> + <!-- no translation found for letterbox_education_dialog_title (7739895354143295358) --> <skip /> - <!-- no translation found for letterbox_education_dialog_subtext (4853542518367719562) --> + <!-- no translation found for letterbox_education_split_screen_text (6206339484068670830) --> <skip /> - <!-- no translation found for letterbox_education_screen_rotation_text (5085786687366339027) --> - <skip /> - <!-- no translation found for letterbox_education_reposition_text (1068293354123934727) --> + <!-- no translation found for letterbox_education_reposition_text (4589957299813220661) --> <skip /> <string name="letterbox_education_got_it" msgid="4057634570866051177">"ገባኝ"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"ለተጨማሪ መረጃ ይዘርጉ።"</string> + <string name="maximize_button_text" msgid="1650859196290301963">"አስፋ"</string> + <string name="minimize_button_text" msgid="271592547935841753">"አሳንስ"</string> + <string name="close_button_text" msgid="2913281996024033299">"ዝጋ"</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 456b4b83583a..74ce49ef078e 100644 --- a/libs/WindowManager/Shell/res/values-am/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-am/strings_tv.xml @@ -19,7 +19,16 @@ 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="9135220303720555525">"PIPን ዝጋ"</string> + <string name="pip_close" msgid="2955969519031223530">"ዝጋ"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"ሙሉ ማያ ገጽ"</string> - <string name="pip_move" msgid="1544227837964635439">"ፒአይፒ ውሰድ"</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_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.xml b/libs/WindowManager/Shell/res/values-ar/strings.xml index b41e6421ae55..98f11dd99957 100644 --- a/libs/WindowManager/Shell/res/values-ar/strings.xml +++ b/libs/WindowManager/Shell/res/values-ar/strings.xml @@ -22,6 +22,7 @@ <string name="pip_phone_settings" msgid="5468987116750491918">"الإعدادات"</string> <string name="pip_phone_enter_split" msgid="7042877263880641911">"الدخول في وضع تقسيم الشاشة"</string> <string name="pip_menu_title" msgid="5393619322111827096">"القائمة"</string> + <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"قائمة نافذة ضمن النافذة"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> يظهر في صورة داخل صورة"</string> <string name="pip_notification_message" msgid="8854051911700302620">"إذا كنت لا تريد أن يستخدم <xliff:g id="NAME">%s</xliff:g> هذه الميزة، فانقر لفتح الإعدادات، ثم أوقِف تفعيل هذه الميزة."</string> <string name="pip_play" msgid="3496151081459417097">"تشغيل"</string> @@ -36,6 +37,7 @@ <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"قد لا يعمل التطبيق على شاشة عرض ثانوية."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"لا يمكن تشغيل التطبيق على شاشات عرض ثانوية."</string> <string name="accessibility_divider" msgid="703810061635792791">"أداة تقسيم الشاشة"</string> + <string name="divider_title" msgid="5482989479865361192">"أداة تقسيم الشاشة"</string> <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"عرض النافذة اليسرى بملء الشاشة"</string> <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"ضبط حجم النافذة اليسرى ليكون ٧٠%"</string> <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"ضبط حجم النافذة اليسرى ليكون ٥٠%"</string> @@ -72,17 +74,19 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"فقاعة"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"إدارة"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"تم إغلاق الفقاعة."</string> - <string name="restart_button_description" msgid="5887656107651190519">"انقر لإعادة تشغيل هذا التطبيق والانتقال إلى وضع ملء الشاشة."</string> + <string name="restart_button_description" msgid="6712141648865547958">"انقر لإعادة تشغيل هذا التطبيق للحصول على عرض أفضل."</string> <string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"هل هناك مشاكل في الكاميرا؟\nانقر لإعادة الضبط."</string> <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"ألم يتم حل المشكلة؟\nانقر للعودة"</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"أليس هناك مشاكل في الكاميرا؟ انقر للإغلاق."</string> - <!-- no translation found for letterbox_education_dialog_title (6688664582871779215) --> + <!-- no translation found for letterbox_education_dialog_title (7739895354143295358) --> <skip /> - <!-- no translation found for letterbox_education_dialog_subtext (4853542518367719562) --> + <!-- no translation found for letterbox_education_split_screen_text (6206339484068670830) --> <skip /> - <!-- no translation found for letterbox_education_screen_rotation_text (5085786687366339027) --> - <skip /> - <!-- no translation found for letterbox_education_reposition_text (1068293354123934727) --> + <!-- no translation found for letterbox_education_reposition_text (4589957299813220661) --> <skip /> <string name="letterbox_education_got_it" msgid="4057634570866051177">"حسنًا"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"التوسيع للحصول على مزيد من المعلومات"</string> + <string name="maximize_button_text" msgid="1650859196290301963">"تكبير"</string> + <string name="minimize_button_text" msgid="271592547935841753">"تصغير"</string> + <string name="close_button_text" msgid="2913281996024033299">"إغلاق"</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 2546fe96d86a..9c195a7386a9 100644 --- a/libs/WindowManager/Shell/res/values-ar/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-ar/strings_tv.xml @@ -19,7 +19,16 @@ 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="9135220303720555525">"إغلاق PIP"</string> + <string name="pip_close" msgid="2955969519031223530">"إغلاق"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"ملء الشاشة"</string> - <string name="pip_move" msgid="1544227837964635439">"نقل نافذة داخل النافذة (PIP)"</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_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.xml b/libs/WindowManager/Shell/res/values-as/strings.xml index 663691fdc045..18cb3ffc61ab 100644 --- a/libs/WindowManager/Shell/res/values-as/strings.xml +++ b/libs/WindowManager/Shell/res/values-as/strings.xml @@ -22,6 +22,7 @@ <string name="pip_phone_settings" msgid="5468987116750491918">"ছেটিং"</string> <string name="pip_phone_enter_split" msgid="7042877263880641911">"বিভাজিত স্ক্ৰীন ম’ডলৈ যাওক"</string> <string name="pip_menu_title" msgid="5393619322111827096">"মেনু"</string> + <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"চিত্ৰৰ ভিতৰৰ চিত্ৰ মেনু"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> চিত্ৰৰ ভিতৰৰ চিত্ৰত আছে"</string> <string name="pip_notification_message" msgid="8854051911700302620">"আপুনি যদি <xliff:g id="NAME">%s</xliff:g> সুবিধাটো ব্যৱহাৰ কৰিব নোখোজে, তেন্তে ছেটিং খুলিবলৈ টিপক আৰু তালৈ গৈ ইয়াক অফ কৰক।"</string> <string name="pip_play" msgid="3496151081459417097">"প্লে কৰক"</string> @@ -36,6 +37,7 @@ <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"গৌণ ডিছপ্লেত এপে সঠিকভাৱে কাম নকৰিব পাৰে।"</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"গৌণ ডিছপ্লেত এপ্ লঞ্চ কৰিব নোৱাৰি।"</string> <string name="accessibility_divider" msgid="703810061635792791">"স্প্লিট স্ক্ৰীনৰ বিভাজক"</string> + <string name="divider_title" msgid="5482989479865361192">"বিভাজিত স্ক্ৰীনৰ বিভাজক"</string> <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"বাওঁফালৰ স্ক্ৰীনখন সম্পূৰ্ণ স্ক্ৰীন কৰক"</string> <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"বাওঁফালৰ স্ক্ৰীণখন ৭০% কৰক"</string> <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"বাওঁফালৰ স্ক্ৰীণখন ৫০% কৰক"</string> @@ -72,17 +74,19 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"বাবল"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"পৰিচালনা কৰক"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"বাবল অগ্ৰাহ্য কৰা হৈছে"</string> - <string name="restart_button_description" msgid="5887656107651190519">"এপ্টো ৰিষ্টাৰ্ট কৰিবলৈ আৰু পূৰ্ণ স্ক্ৰীন ব্যৱহাৰ কৰিবলৈ টিপক।"</string> + <string name="restart_button_description" msgid="6712141648865547958">"উন্নত ভিউৰ বাবে এপ্টো ৰিষ্টাৰ্ট কৰিবলৈ টিপক।"</string> <string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"কেমেৰাৰ কোনো সমস্যা হৈছে নেকি?\nপুনৰ খাপ খোৱাবলৈ টিপক"</string> <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"এইটো সমাধান কৰা নাই নেকি?\nপূৰ্বাৱস্থালৈ নিবলৈ টিপক"</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"কেমেৰাৰ কোনো সমস্যা নাই নেকি? অগ্ৰাহ্য কৰিবলৈ টিপক।"</string> - <!-- no translation found for letterbox_education_dialog_title (6688664582871779215) --> + <!-- no translation found for letterbox_education_dialog_title (7739895354143295358) --> <skip /> - <!-- no translation found for letterbox_education_dialog_subtext (4853542518367719562) --> + <!-- no translation found for letterbox_education_split_screen_text (6206339484068670830) --> <skip /> - <!-- no translation found for letterbox_education_screen_rotation_text (5085786687366339027) --> - <skip /> - <!-- no translation found for letterbox_education_reposition_text (1068293354123934727) --> + <!-- no translation found for letterbox_education_reposition_text (4589957299813220661) --> <skip /> <string name="letterbox_education_got_it" msgid="4057634570866051177">"বুজি পালোঁ"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"অধিক তথ্যৰ বাবে বিস্তাৰ কৰক।"</string> + <string name="maximize_button_text" msgid="1650859196290301963">"সৰ্বাধিক মাত্ৰালৈ বঢ়াওক"</string> + <string name="minimize_button_text" msgid="271592547935841753">"মিনিমাইজ কৰক"</string> + <string name="close_button_text" msgid="2913281996024033299">"বন্ধ কৰক"</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 d17c1f3023a3..816b5b1c79dc 100644 --- a/libs/WindowManager/Shell/res/values-as/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-as/strings_tv.xml @@ -19,7 +19,16 @@ 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="9135220303720555525">"পিপ বন্ধ কৰক"</string> + <string name="pip_close" msgid="2955969519031223530">"বন্ধ কৰক"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"সম্পূৰ্ণ স্ক্ৰীন"</string> - <string name="pip_move" msgid="1544227837964635439">"পিপ স্থানান্তৰ কৰক"</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_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.xml b/libs/WindowManager/Shell/res/values-az/strings.xml index 646aba89dd64..2040288cd726 100644 --- a/libs/WindowManager/Shell/res/values-az/strings.xml +++ b/libs/WindowManager/Shell/res/values-az/strings.xml @@ -22,6 +22,7 @@ <string name="pip_phone_settings" msgid="5468987116750491918">"Ayarlar"</string> <string name="pip_phone_enter_split" msgid="7042877263880641911">"Bölünmüş ekrana daxil olun"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Menyu"</string> + <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Şəkildə Şəkil Menyusu"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> şəkil içində şəkildədir"</string> <string name="pip_notification_message" msgid="8854051911700302620">"<xliff:g id="NAME">%s</xliff:g> tətbiqinin bu funksiyadan istifadə etməyini istəmirsinizsə, ayarları açmaq və deaktiv etmək üçün klikləyin."</string> <string name="pip_play" msgid="3496151081459417097">"Oxudun"</string> @@ -36,6 +37,7 @@ <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Tətbiq ikinci ekranda işləməyə bilər."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Tətbiq ikinci ekranda başlamağı dəstəkləmir."</string> <string name="accessibility_divider" msgid="703810061635792791">"Bölünmüş ekran ayırıcısı"</string> + <string name="divider_title" msgid="5482989479865361192">"Bölünmüş ekran ayırıcısı"</string> <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Sol tam ekran"</string> <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Sol 70%"</string> <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Sol 50%"</string> @@ -72,17 +74,19 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Qabarcıq"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"İdarə edin"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Qabarcıqdan imtina edilib."</string> - <string name="restart_button_description" msgid="5887656107651190519">"Bu tətbiqi sıfırlayaraq tam ekrana keçmək üçün toxunun."</string> + <string name="restart_button_description" msgid="6712141648865547958">"Toxunaraq bu tətbiqi yenidən başladın ki, daha görüntü əldə edəsiniz."</string> <string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"Kamera problemi var?\nBərpa etmək üçün toxunun"</string> <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Düzəltməmisiniz?\nGeri qaytarmaq üçün toxunun"</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Kamera problemi yoxdur? Qapatmaq üçün toxunun."</string> - <!-- no translation found for letterbox_education_dialog_title (6688664582871779215) --> + <!-- no translation found for letterbox_education_dialog_title (7739895354143295358) --> <skip /> - <!-- no translation found for letterbox_education_dialog_subtext (4853542518367719562) --> + <!-- no translation found for letterbox_education_split_screen_text (6206339484068670830) --> <skip /> - <!-- no translation found for letterbox_education_screen_rotation_text (5085786687366339027) --> - <skip /> - <!-- no translation found for letterbox_education_reposition_text (1068293354123934727) --> + <!-- no translation found for letterbox_education_reposition_text (4589957299813220661) --> <skip /> <string name="letterbox_education_got_it" msgid="4057634570866051177">"Anladım"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Ətraflı məlumat üçün genişləndirin."</string> + <string name="maximize_button_text" msgid="1650859196290301963">"Böyüdün"</string> + <string name="minimize_button_text" msgid="271592547935841753">"Kiçildin"</string> + <string name="close_button_text" msgid="2913281996024033299">"Bağlayın"</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 a5c47924e31a..ccb7a7069ad8 100644 --- a/libs/WindowManager/Shell/res/values-az/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-az/strings_tv.xml @@ -19,7 +19,16 @@ 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="9135220303720555525">"PIP bağlayın"</string> + <string name="pip_close" msgid="2955969519031223530">"Bağlayın"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Tam ekran"</string> - <string name="pip_move" msgid="1544227837964635439">"PIP tətbiq edin"</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_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.xml b/libs/WindowManager/Shell/res/values-b+sr+Latn/strings.xml index 2ebdf926b357..b2b484e9d249 100644 --- a/libs/WindowManager/Shell/res/values-b+sr+Latn/strings.xml +++ b/libs/WindowManager/Shell/res/values-b+sr+Latn/strings.xml @@ -22,6 +22,7 @@ <string name="pip_phone_settings" msgid="5468987116750491918">"Podešavanja"</string> <string name="pip_phone_enter_split" msgid="7042877263880641911">"Uđi na podeljeni ekran"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Meni"</string> + <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Meni slike u slici."</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> je slika u slici"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Ako ne želite da <xliff:g id="NAME">%s</xliff:g> koristi ovu funkciju, dodirnite da biste otvorili podešavanja i isključili je."</string> <string name="pip_play" msgid="3496151081459417097">"Pusti"</string> @@ -36,6 +37,7 @@ <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Aplikacija možda neće funkcionisati 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">"Razdelnik podeljenog ekrana"</string> + <string name="divider_title" msgid="5482989479865361192">"Razdelnik podeljenog ekrana"</string> <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Režim celog ekrana za levi ekran"</string> <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Levi ekran 70%"</string> <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Levi ekran 50%"</string> @@ -72,17 +74,19 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Oblačić"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Upravljajte"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Oblačić je odbačen."</string> - <string name="restart_button_description" msgid="5887656107651190519">"Dodirnite da biste restartovali aplikaciju i prešli u režim celog ekrana."</string> + <string name="restart_button_description" msgid="6712141648865547958">"Dodirnite da biste restartovali ovu aplikaciju radi boljeg prikaza."</string> <string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"Imate problema sa kamerom?\nDodirnite da biste ponovo uklopili"</string> <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Problem nije rešen?\nDodirnite da biste vratili"</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Nemate problema sa kamerom? Dodirnite da biste odbacili."</string> - <!-- no translation found for letterbox_education_dialog_title (6688664582871779215) --> + <!-- no translation found for letterbox_education_dialog_title (7739895354143295358) --> <skip /> - <!-- no translation found for letterbox_education_dialog_subtext (4853542518367719562) --> + <!-- no translation found for letterbox_education_split_screen_text (6206339484068670830) --> <skip /> - <!-- no translation found for letterbox_education_screen_rotation_text (5085786687366339027) --> - <skip /> - <!-- no translation found for letterbox_education_reposition_text (1068293354123934727) --> + <!-- no translation found for letterbox_education_reposition_text (4589957299813220661) --> <skip /> <string name="letterbox_education_got_it" msgid="4057634570866051177">"Važi"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Proširite za još informacija."</string> + <string name="maximize_button_text" msgid="1650859196290301963">"Uvećajte"</string> + <string name="minimize_button_text" msgid="271592547935841753">"Umanjite"</string> + <string name="close_button_text" msgid="2913281996024033299">"Zatvorite"</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 b4d9bd17b5fe..51a1262b1de7 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,7 +19,16 @@ 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="9135220303720555525">"Zatvori PIP"</string> + <string name="pip_close" msgid="2955969519031223530">"Zatvori"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Ceo ekran"</string> - <string name="pip_move" msgid="1544227837964635439">"Premesti sliku u slici"</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_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.xml b/libs/WindowManager/Shell/res/values-be/strings.xml index 157e16895148..59db9923f714 100644 --- a/libs/WindowManager/Shell/res/values-be/strings.xml +++ b/libs/WindowManager/Shell/res/values-be/strings.xml @@ -22,6 +22,7 @@ <string name="pip_phone_settings" msgid="5468987116750491918">"Налады"</string> <string name="pip_phone_enter_split" msgid="7042877263880641911">"Падзяліць экран"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Меню"</string> + <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Меню рэжыму \"Відарыс у відарысе\""</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> з’яўляецца відарысам у відарысе"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Калі вы не хочаце, каб праграма <xliff:g id="NAME">%s</xliff:g> выкарыстоўвала гэту функцыю, дакраніцеся, каб адкрыць налады і адключыць яе."</string> <string name="pip_play" msgid="3496151081459417097">"Прайграць"</string> @@ -36,6 +37,7 @@ <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Праграма можа не працаваць на дадатковых экранах."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Праграма не падтрымлівае запуск на дадатковых экранах."</string> <string name="accessibility_divider" msgid="703810061635792791">"Раздзяляльнік падзеленага экрана"</string> + <string name="divider_title" msgid="5482989479865361192">"Раздзяляльнік падзеленага экрана"</string> <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Левы экран – поўнаэкранны рэжым"</string> <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Левы экран – 70%"</string> <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Левы экран – 50%"</string> @@ -72,17 +74,19 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Усплывальнае апавяшчэнне"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Кіраваць"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Усплывальнае апавяшчэнне адхілена."</string> - <string name="restart_button_description" msgid="5887656107651190519">"Націсніце, каб перазапусціць гэту праграму і перайсці ў поўнаэкранны рэжым."</string> + <string name="restart_button_description" msgid="6712141648865547958">"Націсніце, каб перазапусціць гэту праграму для лепшага прагляду."</string> <string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"Праблемы з камерай?\nНацісніце, каб пераабсталяваць"</string> <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Не ўдалося выправіць?\nНацісніце, каб аднавіць"</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Ніякіх праблем з камерай? Націсніце, каб адхіліць."</string> - <!-- no translation found for letterbox_education_dialog_title (6688664582871779215) --> + <!-- no translation found for letterbox_education_dialog_title (7739895354143295358) --> <skip /> - <!-- no translation found for letterbox_education_dialog_subtext (4853542518367719562) --> + <!-- no translation found for letterbox_education_split_screen_text (6206339484068670830) --> <skip /> - <!-- no translation found for letterbox_education_screen_rotation_text (5085786687366339027) --> - <skip /> - <!-- no translation found for letterbox_education_reposition_text (1068293354123934727) --> + <!-- no translation found for letterbox_education_reposition_text (4589957299813220661) --> <skip /> <string name="letterbox_education_got_it" msgid="4057634570866051177">"Зразумела"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Разгарнуць для дадатковай інфармацыі"</string> + <string name="maximize_button_text" msgid="1650859196290301963">"Разгарнуць"</string> + <string name="minimize_button_text" msgid="271592547935841753">"Згарнуць"</string> + <string name="close_button_text" msgid="2913281996024033299">"Закрыць"</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 514d06b5b179..15a353c649d6 100644 --- a/libs/WindowManager/Shell/res/values-be/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-be/strings_tv.xml @@ -19,7 +19,16 @@ 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="9135220303720555525">"Закрыць PIP"</string> + <string name="pip_close" msgid="2955969519031223530">"Закрыць"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Поўнаэкранны рэжым"</string> - <string name="pip_move" msgid="1544227837964635439">"Перамясціць PIP"</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_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.xml b/libs/WindowManager/Shell/res/values-bg/strings.xml index 4ed8672db152..8b5c4a95b1bc 100644 --- a/libs/WindowManager/Shell/res/values-bg/strings.xml +++ b/libs/WindowManager/Shell/res/values-bg/strings.xml @@ -22,6 +22,7 @@ <string name="pip_phone_settings" msgid="5468987116750491918">"Настройки"</string> <string name="pip_phone_enter_split" msgid="7042877263880641911">"Преминаване към разделен екран"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Меню"</string> + <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Меню за режима „Картина в картината“"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> е в режима „Картина в картината“"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Ако не искате <xliff:g id="NAME">%s</xliff:g> да използва тази функция, докоснете, за да отворите настройките, и я изключете."</string> <string name="pip_play" msgid="3496151081459417097">"Пускане"</string> @@ -36,6 +37,7 @@ <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Възможно е приложението да не работи на алтернативни дисплеи."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Приложението не поддържа използването на алтернативни дисплеи."</string> <string name="accessibility_divider" msgid="703810061635792791">"Разделител в режима за разделен екран"</string> + <string name="divider_title" msgid="5482989479865361192">"Разделител в режима за разделен екран"</string> <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Ляв екран: Показване на цял екран"</string> <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Ляв екран: 70%"</string> <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Ляв екран: 50%"</string> @@ -72,17 +74,19 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Балонче"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Управление"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Балончето е отхвърлено."</string> - <string name="restart_button_description" msgid="5887656107651190519">"Докоснете, за да рестартирате това приложение в режим на цял екран."</string> + <string name="restart_button_description" msgid="6712141648865547958">"Докоснете, за да рестартирате това приложение с цел по-добър изглед."</string> <string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"Имате проблеми с камерата?\nДокоснете за ремонтиране"</string> <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Проблемът не се отстрани?\nДокоснете за връщане в предишното състояние"</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Нямате проблеми с камерата? Докоснете, за да отхвърлите."</string> - <!-- no translation found for letterbox_education_dialog_title (6688664582871779215) --> + <!-- no translation found for letterbox_education_dialog_title (7739895354143295358) --> <skip /> - <!-- no translation found for letterbox_education_dialog_subtext (4853542518367719562) --> + <!-- no translation found for letterbox_education_split_screen_text (6206339484068670830) --> <skip /> - <!-- no translation found for letterbox_education_screen_rotation_text (5085786687366339027) --> - <skip /> - <!-- no translation found for letterbox_education_reposition_text (1068293354123934727) --> + <!-- no translation found for letterbox_education_reposition_text (4589957299813220661) --> <skip /> <string name="letterbox_education_got_it" msgid="4057634570866051177">"Разбрах"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Разгъване за още информация."</string> + <string name="maximize_button_text" msgid="1650859196290301963">"Увеличаване"</string> + <string name="minimize_button_text" msgid="271592547935841753">"Намаляване"</string> + <string name="close_button_text" msgid="2913281996024033299">"Затваряне"</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 19f83e71ea44..2b27a6927077 100644 --- a/libs/WindowManager/Shell/res/values-bg/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-bg/strings_tv.xml @@ -19,7 +19,16 @@ 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="9135220303720555525">"Затваряне на PIP"</string> + <string name="pip_close" msgid="2955969519031223530">"Затваряне"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Цял екран"</string> - <string name="pip_move" msgid="1544227837964635439">"„Картина в картина“: Преместв."</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_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.xml b/libs/WindowManager/Shell/res/values-bn/strings.xml index 7579fac0ff06..d7ff018a2fdf 100644 --- a/libs/WindowManager/Shell/res/values-bn/strings.xml +++ b/libs/WindowManager/Shell/res/values-bn/strings.xml @@ -22,6 +22,7 @@ <string name="pip_phone_settings" msgid="5468987116750491918">"সেটিংস"</string> <string name="pip_phone_enter_split" msgid="7042877263880641911">"\'স্প্লিট স্ক্রিন\' মোড চালু করুন"</string> <string name="pip_menu_title" msgid="5393619322111827096">"মেনু"</string> + <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"ছবির-মধ্যে-ছবি মেনু"</string> <string name="pip_notification_title" msgid="1347104727641353453">"ছবির-মধ্যে-ছবি তে <xliff:g id="NAME">%s</xliff:g> আছেন"</string> <string name="pip_notification_message" msgid="8854051911700302620">"<xliff:g id="NAME">%s</xliff:g> কে এই বৈশিষ্ট্যটি ব্যবহার করতে দিতে না চাইলে ট্যাপ করে সেটিংসে গিয়ে সেটি বন্ধ করে দিন।"</string> <string name="pip_play" msgid="3496151081459417097">"চালান"</string> @@ -36,6 +37,7 @@ <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"সেকেন্ডারি ডিসপ্লেতে অ্যাপটি কাজ নাও করতে পারে।"</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"সেকেন্ডারি ডিসপ্লেতে অ্যাপ লঞ্চ করা যাবে না।"</string> <string name="accessibility_divider" msgid="703810061635792791">"বিভক্ত-স্ক্রিন বিভাজক"</string> + <string name="divider_title" msgid="5482989479865361192">"স্প্লিট স্ক্রিন বিভাজক"</string> <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"বাঁ দিকের অংশ নিয়ে পূর্ণ স্ক্রিন"</string> <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"৭০% বাকি আছে"</string> <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"৫০% বাকি আছে"</string> @@ -72,17 +74,19 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"বাবল"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"ম্যানেজ করুন"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"বাবল বাতিল করা হয়েছে।"</string> - <string name="restart_button_description" msgid="5887656107651190519">"এই অ্যাপ রিস্টার্ট করতে ট্যাপ করুন ও \'ফুল-স্ক্রিন\' মোড ব্যবহার করুন।"</string> + <string name="restart_button_description" msgid="6712141648865547958">"আরও ভাল ভিউয়ের জন্য এই অ্যাপ রিস্টার্ট করতে ট্যাপ করুন।"</string> <string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"ক্যামেরা সংক্রান্ত সমস্যা?\nরিফিট করতে ট্যাপ করুন"</string> <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"এখনও সমাধান হয়নি?\nরিভার্ট করার জন্য ট্যাপ করুন"</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"ক্যামেরা সংক্রান্ত সমস্যা নেই? বাতিল করতে ট্যাপ করুন।"</string> - <!-- no translation found for letterbox_education_dialog_title (6688664582871779215) --> + <!-- no translation found for letterbox_education_dialog_title (7739895354143295358) --> <skip /> - <!-- no translation found for letterbox_education_dialog_subtext (4853542518367719562) --> + <!-- no translation found for letterbox_education_split_screen_text (6206339484068670830) --> <skip /> - <!-- no translation found for letterbox_education_screen_rotation_text (5085786687366339027) --> - <skip /> - <!-- no translation found for letterbox_education_reposition_text (1068293354123934727) --> + <!-- no translation found for letterbox_education_reposition_text (4589957299813220661) --> <skip /> <string name="letterbox_education_got_it" msgid="4057634570866051177">"বুঝেছি"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"আরও তথ্যের জন্য বড় করুন।"</string> + <string name="maximize_button_text" msgid="1650859196290301963">"বড় করুন"</string> + <string name="minimize_button_text" msgid="271592547935841753">"ছোট করুন"</string> + <string name="close_button_text" msgid="2913281996024033299">"বন্ধ করুন"</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 5f90eeb35a3f..23c8ffabeede 100644 --- a/libs/WindowManager/Shell/res/values-bn/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-bn/strings_tv.xml @@ -19,7 +19,16 @@ 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="9135220303720555525">"PIP বন্ধ করুন"</string> + <string name="pip_close" msgid="2955969519031223530">"বন্ধ করুন"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"পূর্ণ স্ক্রিন"</string> - <string name="pip_move" msgid="1544227837964635439">"PIP সরান"</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_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 7b08d035c7f2..f4c456083147 100644 --- a/libs/WindowManager/Shell/res/values-bs/strings.xml +++ b/libs/WindowManager/Shell/res/values-bs/strings.xml @@ -22,6 +22,7 @@ <string name="pip_phone_settings" msgid="5468987116750491918">"Postavke"</string> <string name="pip_phone_enter_split" msgid="7042877263880641911">"Otvori podijeljeni ekran"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Meni"</string> + <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Meni načina rada slike u slici"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> je u načinu priakza Slika u slici"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Ako ne želite da <xliff:g id="NAME">%s</xliff:g> koristi ovu funkciju, dodirnite da otvorite postavke i isključite je."</string> <string name="pip_play" msgid="3496151081459417097">"Reproduciraj"</string> @@ -35,7 +36,8 @@ <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 ekrana"</string> + <string name="accessibility_divider" msgid="703810061635792791">"Razdjelnik podijeljenog ekrana"</string> + <string name="divider_title" msgid="5482989479865361192">"Razdjelnik podijeljenog 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> @@ -72,17 +74,19 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Oblačić"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Upravljaj"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Oblačić je odbačen."</string> - <string name="restart_button_description" msgid="5887656107651190519">"Dodirnite da ponovo pokrenete ovu aplikaciju i aktivirate prikaz preko cijelog ekrana."</string> + <string name="restart_button_description" msgid="6712141648865547958">"Dodirnite da ponovo pokrenete ovu aplikaciju radi boljeg prikaza."</string> <string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"Problemi s kamerom?\nDodirnite da ponovo namjestite"</string> <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Nije popravljeno?\nDodirnite da vratite"</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Nema problema s kamerom? Dodirnite da odbacite."</string> - <!-- no translation found for letterbox_education_dialog_title (6688664582871779215) --> + <!-- no translation found for letterbox_education_dialog_title (7739895354143295358) --> <skip /> - <!-- no translation found for letterbox_education_dialog_subtext (4853542518367719562) --> + <!-- no translation found for letterbox_education_split_screen_text (6206339484068670830) --> <skip /> - <!-- no translation found for letterbox_education_screen_rotation_text (5085786687366339027) --> - <skip /> - <!-- no translation found for letterbox_education_reposition_text (1068293354123934727) --> + <!-- no translation found for letterbox_education_reposition_text (4589957299813220661) --> <skip /> <string name="letterbox_education_got_it" msgid="4057634570866051177">"Razumijem"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Proširite za više informacija."</string> + <string name="maximize_button_text" msgid="1650859196290301963">"Maksimiziranje"</string> + <string name="minimize_button_text" msgid="271592547935841753">"Minimiziranje"</string> + <string name="close_button_text" msgid="2913281996024033299">"Zatvaranje"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-bs/strings_tv.xml b/libs/WindowManager/Shell/res/values-bs/strings_tv.xml index 3f2adf3600d7..443fd620fd65 100644 --- a/libs/WindowManager/Shell/res/values-bs/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-bs/strings_tv.xml @@ -19,7 +19,16 @@ 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="9135220303720555525">"Zatvori sliku u slici"</string> + <string name="pip_close" msgid="2955969519031223530">"Zatvori"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Cijeli ekran"</string> - <string name="pip_move" msgid="1544227837964635439">"Pokreni sliku u slici"</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_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.xml b/libs/WindowManager/Shell/res/values-ca/strings.xml index 44429cc582db..c4e6b0d5c80b 100644 --- a/libs/WindowManager/Shell/res/values-ca/strings.xml +++ b/libs/WindowManager/Shell/res/values-ca/strings.xml @@ -22,12 +22,13 @@ <string name="pip_phone_settings" msgid="5468987116750491918">"Configuració"</string> <string name="pip_phone_enter_split" msgid="7042877263880641911">"Entra al mode de pantalla dividida"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Menú"</string> + <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Menú de pantalla en pantalla"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> està en pantalla en pantalla"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Si no vols que <xliff:g id="NAME">%s</xliff:g> utilitzi aquesta funció, toca per obrir la configuració i desactiva-la."</string> <string name="pip_play" msgid="3496151081459417097">"Reprodueix"</string> <string name="pip_pause" msgid="690688849510295232">"Posa en pausa"</string> <string name="pip_skip_to_next" msgid="8403429188794867653">"Ves al següent"</string> - <string name="pip_skip_to_prev" msgid="7172158111196394092">"Torna a l\'anterior"</string> + <string name="pip_skip_to_prev" msgid="7172158111196394092">"Ves a l\'anterior"</string> <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Canvia la mida"</string> <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Amaga"</string> <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Deixa d\'amagar"</string> @@ -36,6 +37,7 @@ <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"És possible que l\'aplicació no funcioni en una pantalla secundària."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"L\'aplicació no es pot obrir en pantalles secundàries."</string> <string name="accessibility_divider" msgid="703810061635792791">"Divisor de pantalles"</string> + <string name="divider_title" msgid="5482989479865361192">"Separador de pantalla dividida"</string> <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Pantalla esquerra completa"</string> <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Pantalla esquerra al 70%"</string> <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Pantalla esquerra al 50%"</string> @@ -72,17 +74,19 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Bombolla"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Gestiona"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"La bombolla s\'ha ignorat."</string> - <string name="restart_button_description" msgid="5887656107651190519">"Toca per reiniciar aquesta aplicació i passar a pantalla completa."</string> + <string name="restart_button_description" msgid="6712141648865547958">"Toca per reiniciar aquesta aplicació i obtenir una millor visualització."</string> <string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"Tens problemes amb la càmera?\nToca per resoldre\'ls"</string> <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"El problema no s\'ha resolt?\nToca per desfer els canvis"</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"No tens cap problema amb la càmera? Toca per ignorar."</string> - <!-- no translation found for letterbox_education_dialog_title (6688664582871779215) --> + <!-- no translation found for letterbox_education_dialog_title (7739895354143295358) --> <skip /> - <!-- no translation found for letterbox_education_dialog_subtext (4853542518367719562) --> + <!-- no translation found for letterbox_education_split_screen_text (6206339484068670830) --> <skip /> - <!-- no translation found for letterbox_education_screen_rotation_text (5085786687366339027) --> - <skip /> - <!-- no translation found for letterbox_education_reposition_text (1068293354123934727) --> + <!-- no translation found for letterbox_education_reposition_text (4589957299813220661) --> <skip /> <string name="letterbox_education_got_it" msgid="4057634570866051177">"Entesos"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Desplega per obtenir més informació."</string> + <string name="maximize_button_text" msgid="1650859196290301963">"Maximitza"</string> + <string name="minimize_button_text" msgid="271592547935841753">"Minimitza"</string> + <string name="close_button_text" msgid="2913281996024033299">"Tanca"</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 db750c49884e..94ba0db7e978 100644 --- a/libs/WindowManager/Shell/res/values-ca/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-ca/strings_tv.xml @@ -19,7 +19,16 @@ 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="9135220303720555525">"Tanca PIP"</string> + <string name="pip_close" msgid="2955969519031223530">"Tanca"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Pantalla completa"</string> - <string name="pip_move" msgid="1544227837964635439">"Mou pantalla en pantalla"</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_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.xml b/libs/WindowManager/Shell/res/values-cs/strings.xml index d6e7136abb07..8b0d1ff9abde 100644 --- a/libs/WindowManager/Shell/res/values-cs/strings.xml +++ b/libs/WindowManager/Shell/res/values-cs/strings.xml @@ -22,6 +22,7 @@ <string name="pip_phone_settings" msgid="5468987116750491918">"Nastavení"</string> <string name="pip_phone_enter_split" msgid="7042877263880641911">"Aktivovat rozdělenou obrazovku"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Nabídka"</string> + <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Nabídka režimu obrazu v obraze"</string> <string name="pip_notification_title" msgid="1347104727641353453">"Aplikace <xliff:g id="NAME">%s</xliff:g> je v režimu obraz v obraze"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Pokud nechcete, aby aplikace <xliff:g id="NAME">%s</xliff:g> tuto funkci používala, klepnutím otevřete nastavení a funkci vypněte."</string> <string name="pip_play" msgid="3496151081459417097">"Přehrát"</string> @@ -36,6 +37,7 @@ <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Aplikace na sekundárním displeji nemusí fungovat."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Aplikace nepodporuje spuštění na sekundárních displejích."</string> <string name="accessibility_divider" msgid="703810061635792791">"Čára rozdělující obrazovku"</string> + <string name="divider_title" msgid="5482989479865361192">"Čára rozdělující obrazovku"</string> <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Levá část na celou obrazovku"</string> <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"70 % vlevo"</string> <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"50 % vlevo"</string> @@ -72,17 +74,19 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Bublina"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Spravovat"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Bublina byla zavřena."</string> - <string name="restart_button_description" msgid="5887656107651190519">"Klepnutím aplikaci restartujete a přejdete na režim celé obrazovky"</string> + <string name="restart_button_description" msgid="6712141648865547958">"Klepnutím tuto aplikaci kvůli lepšímu zobrazení restartujete."</string> <string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"Problémy s fotoaparátem?\nKlepnutím vyřešíte"</string> <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Nepomohlo to?\nKlepnutím se vrátíte"</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Žádné problémy s fotoaparátem? Klepnutím zavřete."</string> - <!-- no translation found for letterbox_education_dialog_title (6688664582871779215) --> + <!-- no translation found for letterbox_education_dialog_title (7739895354143295358) --> <skip /> - <!-- no translation found for letterbox_education_dialog_subtext (4853542518367719562) --> + <!-- no translation found for letterbox_education_split_screen_text (6206339484068670830) --> <skip /> - <!-- no translation found for letterbox_education_screen_rotation_text (5085786687366339027) --> - <skip /> - <!-- no translation found for letterbox_education_reposition_text (1068293354123934727) --> + <!-- no translation found for letterbox_education_reposition_text (4589957299813220661) --> <skip /> <string name="letterbox_education_got_it" msgid="4057634570866051177">"OK"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Rozbalením zobrazíte další informace."</string> + <string name="maximize_button_text" msgid="1650859196290301963">"Maximalizovat"</string> + <string name="minimize_button_text" msgid="271592547935841753">"Minimalizovat"</string> + <string name="close_button_text" msgid="2913281996024033299">"Zavřít"</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 cef0b9951363..3ed85dce0433 100644 --- a/libs/WindowManager/Shell/res/values-cs/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-cs/strings_tv.xml @@ -19,7 +19,16 @@ 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="9135220303720555525">"Ukončit obraz v obraze (PIP)"</string> + <string name="pip_close" msgid="2955969519031223530">"Zavřít"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Celá obrazovka"</string> - <string name="pip_move" msgid="1544227837964635439">"Přesunout PIP"</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_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.xml b/libs/WindowManager/Shell/res/values-da/strings.xml index e7b8e73b7385..94faf4124b7f 100644 --- a/libs/WindowManager/Shell/res/values-da/strings.xml +++ b/libs/WindowManager/Shell/res/values-da/strings.xml @@ -22,6 +22,7 @@ <string name="pip_phone_settings" msgid="5468987116750491918">"Indstillinger"</string> <string name="pip_phone_enter_split" msgid="7042877263880641911">"Åbn opdelt skærm"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Menu"</string> + <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Menu for integreret billede"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> vises som integreret billede"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Hvis du ikke ønsker, at <xliff:g id="NAME">%s</xliff:g> skal benytte denne funktion, kan du åbne indstillingerne og deaktivere den."</string> <string name="pip_play" msgid="3496151081459417097">"Afspil"</string> @@ -36,6 +37,7 @@ <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Appen fungerer muligvis ikke på sekundære skærme."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Appen kan ikke åbnes på sekundære skærme."</string> <string name="accessibility_divider" msgid="703810061635792791">"Adskiller til opdelt skærm"</string> + <string name="divider_title" msgid="5482989479865361192">"Adskiller til opdelt skærm"</string> <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Vis venstre del i fuld skærm"</string> <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Venstre 70 %"</string> <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Venstre 50 %"</string> @@ -72,17 +74,19 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Boble"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Administrer"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Boblen blev lukket."</string> - <string name="restart_button_description" msgid="5887656107651190519">"Tryk for at genstarte denne app, og gå til fuld skærm."</string> + <string name="restart_button_description" msgid="6712141648865547958">"Tryk for at genstarte denne app, så visningen forbedres."</string> <string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"Har du problemer med dit kamera?\nTryk for at gendanne det oprindelige format"</string> <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Løste det ikke problemet?\nTryk for at fortryde"</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Har du ingen problemer med dit kamera? Tryk for at afvise."</string> - <!-- no translation found for letterbox_education_dialog_title (6688664582871779215) --> + <!-- no translation found for letterbox_education_dialog_title (7739895354143295358) --> <skip /> - <!-- no translation found for letterbox_education_dialog_subtext (4853542518367719562) --> + <!-- no translation found for letterbox_education_split_screen_text (6206339484068670830) --> <skip /> - <!-- no translation found for letterbox_education_screen_rotation_text (5085786687366339027) --> - <skip /> - <!-- no translation found for letterbox_education_reposition_text (1068293354123934727) --> + <!-- no translation found for letterbox_education_reposition_text (4589957299813220661) --> <skip /> <string name="letterbox_education_got_it" msgid="4057634570866051177">"OK"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Udvid for at få flere oplysninger."</string> + <string name="maximize_button_text" msgid="1650859196290301963">"Maksimér"</string> + <string name="minimize_button_text" msgid="271592547935841753">"Minimer"</string> + <string name="close_button_text" msgid="2913281996024033299">"Luk"</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 23305309098d..09024428a825 100644 --- a/libs/WindowManager/Shell/res/values-da/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-da/strings_tv.xml @@ -19,7 +19,16 @@ 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="9135220303720555525">"Luk integreret billede"</string> + <string name="pip_close" msgid="2955969519031223530">"Luk"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Fuld skærm"</string> - <string name="pip_move" msgid="1544227837964635439">"Flyt PIP"</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_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.xml b/libs/WindowManager/Shell/res/values-de/strings.xml index 57af696cacb1..397144594bb9 100644 --- a/libs/WindowManager/Shell/res/values-de/strings.xml +++ b/libs/WindowManager/Shell/res/values-de/strings.xml @@ -20,8 +20,9 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Schließen"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Maximieren"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Einstellungen"</string> - <string name="pip_phone_enter_split" msgid="7042877263880641911">"„Bildschirm teilen“ aktivieren"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"„Geteilter Bildschirm“ aktivieren"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Menü"</string> + <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Menü „Bild im Bild“"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> ist in Bild im Bild"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Wenn du nicht möchtest, dass <xliff:g id="NAME">%s</xliff:g> diese Funktion verwendet, tippe, um die Einstellungen zu öffnen und die Funktion zu deaktivieren."</string> <string name="pip_play" msgid="3496151081459417097">"Wiedergeben"</string> @@ -31,11 +32,12 @@ <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Größe anpassen"</string> <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"In Stash legen"</string> <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Aus Stash entfernen"</string> - <string name="dock_forced_resizable" msgid="1749750436092293116">"Die App funktioniert unter Umständen bei geteiltem Bildschirmmodus nicht."</string> + <string name="dock_forced_resizable" msgid="1749750436092293116">"Die App funktioniert unter Umständen im Modus für geteilten Bildschirm nicht."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Das Teilen des Bildschirms wird in dieser App nicht unterstützt."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Die App funktioniert auf einem sekundären Display möglicherweise nicht."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Die App unterstützt den Start auf sekundären Displays nicht."</string> <string name="accessibility_divider" msgid="703810061635792791">"Bildschirmteiler"</string> + <string name="divider_title" msgid="5482989479865361192">"Bildschirmteiler"</string> <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Vollbild links"</string> <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"70 % links"</string> <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"50 % links"</string> @@ -72,17 +74,19 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Bubble"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Verwalten"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Bubble verworfen."</string> - <string name="restart_button_description" msgid="5887656107651190519">"Tippe, um die App im Vollbildmodus neu zu starten."</string> + <string name="restart_button_description" msgid="6712141648865547958">"Tippe, um diese App neu zu starten und die Ansicht zu verbessern."</string> <string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"Probleme mit der Kamera?\nZum Anpassen tippen."</string> <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Das Problem ist nicht behoben?\nZum Rückgängigmachen tippen."</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Keine Probleme mit der Kamera? Zum Schließen tippen."</string> - <!-- no translation found for letterbox_education_dialog_title (6688664582871779215) --> + <!-- no translation found for letterbox_education_dialog_title (7739895354143295358) --> <skip /> - <!-- no translation found for letterbox_education_dialog_subtext (4853542518367719562) --> + <!-- no translation found for letterbox_education_split_screen_text (6206339484068670830) --> <skip /> - <!-- no translation found for letterbox_education_screen_rotation_text (5085786687366339027) --> - <skip /> - <!-- no translation found for letterbox_education_reposition_text (1068293354123934727) --> + <!-- no translation found for letterbox_education_reposition_text (4589957299813220661) --> <skip /> <string name="letterbox_education_got_it" msgid="4057634570866051177">"Ok"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Für weitere Informationen maximieren."</string> + <string name="maximize_button_text" msgid="1650859196290301963">"Maximieren"</string> + <string name="minimize_button_text" msgid="271592547935841753">"Minimieren"</string> + <string name="close_button_text" msgid="2913281996024033299">"Schließen"</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 8da9110f5e03..18535c9d9338 100644 --- a/libs/WindowManager/Shell/res/values-de/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-de/strings_tv.xml @@ -19,7 +19,16 @@ 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="9135220303720555525">"PIP schließen"</string> + <string name="pip_close" msgid="2955969519031223530">"Schließen"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Vollbild"</string> - <string name="pip_move" msgid="1544227837964635439">"BiB verschieben"</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_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.xml b/libs/WindowManager/Shell/res/values-el/strings.xml index 873b3299dcf1..023c2d68023b 100644 --- a/libs/WindowManager/Shell/res/values-el/strings.xml +++ b/libs/WindowManager/Shell/res/values-el/strings.xml @@ -22,6 +22,7 @@ <string name="pip_phone_settings" msgid="5468987116750491918">"Ρυθμίσεις"</string> <string name="pip_phone_enter_split" msgid="7042877263880641911">"Μετάβαση σε διαχωρισμό οθόνης"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Μενού"</string> + <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Μενού λειτουργίας Picture-in-Picture"</string> <string name="pip_notification_title" msgid="1347104727641353453">"Η λειτουργία picture-in-picture είναι ενεργή σε <xliff:g id="NAME">%s</xliff:g>."</string> <string name="pip_notification_message" msgid="8854051911700302620">"Εάν δεν θέλετε να χρησιμοποιείται αυτή η λειτουργία από την εφαρμογή <xliff:g id="NAME">%s</xliff:g>, πατήστε για να ανοίξετε τις ρυθμίσεις και απενεργοποιήστε την."</string> <string name="pip_play" msgid="3496151081459417097">"Αναπαραγωγή"</string> @@ -36,6 +37,7 @@ <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Η εφαρμογή ίσως να μην λειτουργήσει σε δευτερεύουσα οθόνη."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Η εφαρμογή δεν υποστηρίζει την εκκίνηση σε δευτερεύουσες οθόνες."</string> <string name="accessibility_divider" msgid="703810061635792791">"Διαχωριστικό οθόνης"</string> + <string name="divider_title" msgid="5482989479865361192">"Διαχωριστικό οθόνης"</string> <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Αριστερή πλήρης οθόνη"</string> <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Αριστερή 70%"</string> <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Αριστερή 50%"</string> @@ -72,17 +74,19 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Συννεφάκι"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Διαχείριση"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Το συννεφάκι παραβλέφθηκε."</string> - <string name="restart_button_description" msgid="5887656107651190519">"Πατήστε για επανεκκίνηση αυτής της εφαρμογής και ενεργοποίηση πλήρους οθόνης."</string> + <string name="restart_button_description" msgid="6712141648865547958">"Πατήστε για να επανεκκινήσετε αυτή την εφαρμογή για καλύτερη προβολή."</string> <string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"Προβλήματα με την κάμερα;\nΠατήστε για επιδιόρθωση."</string> <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Δεν διορθώθηκε;\nΠατήστε για επαναφορά."</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Δεν αντιμετωπίζετε προβλήματα με την κάμερα; Πατήστε για παράβλεψη."</string> - <!-- no translation found for letterbox_education_dialog_title (6688664582871779215) --> + <!-- no translation found for letterbox_education_dialog_title (7739895354143295358) --> <skip /> - <!-- no translation found for letterbox_education_dialog_subtext (4853542518367719562) --> + <!-- no translation found for letterbox_education_split_screen_text (6206339484068670830) --> <skip /> - <!-- no translation found for letterbox_education_screen_rotation_text (5085786687366339027) --> - <skip /> - <!-- no translation found for letterbox_education_reposition_text (1068293354123934727) --> + <!-- no translation found for letterbox_education_reposition_text (4589957299813220661) --> <skip /> <string name="letterbox_education_got_it" msgid="4057634570866051177">"Το κατάλαβα"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Ανάπτυξη για περισσότερες πληροφορίες."</string> + <string name="maximize_button_text" msgid="1650859196290301963">"Μεγιστοποίηση"</string> + <string name="minimize_button_text" msgid="271592547935841753">"Ελαχιστοποίηση"</string> + <string name="close_button_text" msgid="2913281996024033299">"Κλείσιμο"</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 df35113a8752..5f8a004b0a1f 100644 --- a/libs/WindowManager/Shell/res/values-el/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-el/strings_tv.xml @@ -19,7 +19,16 @@ 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="9135220303720555525">"Κλείσιμο PIP"</string> + <string name="pip_close" msgid="2955969519031223530">"Κλείσιμο"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Πλήρης οθόνη"</string> - <string name="pip_move" msgid="1544227837964635439">"Μετακίνηση PIP"</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_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.xml b/libs/WindowManager/Shell/res/values-en-rAU/strings.xml index da4933b18a8f..acc75e46316d 100644 --- a/libs/WindowManager/Shell/res/values-en-rAU/strings.xml +++ b/libs/WindowManager/Shell/res/values-en-rAU/strings.xml @@ -22,6 +22,7 @@ <string name="pip_phone_settings" msgid="5468987116750491918">"Settings"</string> <string name="pip_phone_enter_split" msgid="7042877263880641911">"Enter split screen"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Menu"</string> + <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Picture-in-picture menu"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> is in picture-in-picture"</string> <string name="pip_notification_message" msgid="8854051911700302620">"If you don\'t want <xliff:g id="NAME">%s</xliff:g> to use this feature, tap to open settings and turn it off."</string> <string name="pip_play" msgid="3496151081459417097">"Play"</string> @@ -36,6 +37,7 @@ <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"App may not work on a secondary display."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"App does not support launch on secondary displays."</string> <string name="accessibility_divider" msgid="703810061635792791">"Split screen divider"</string> + <string name="divider_title" msgid="5482989479865361192">"Split screen divider"</string> <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Left full screen"</string> <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Left 70%"</string> <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Left 50%"</string> @@ -72,17 +74,16 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Bubble"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Manage"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Bubble dismissed."</string> - <string name="restart_button_description" msgid="5887656107651190519">"Tap to restart this app and go full screen."</string> + <string name="restart_button_description" msgid="6712141648865547958">"Tap to restart this app for a better view."</string> <string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"Camera issues?\nTap to refit"</string> <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Didn’t fix it?\nTap to revert"</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"No camera issues? Tap to dismiss."</string> - <!-- no translation found for letterbox_education_dialog_title (6688664582871779215) --> - <skip /> - <!-- no translation found for letterbox_education_dialog_subtext (4853542518367719562) --> - <skip /> - <!-- no translation found for letterbox_education_screen_rotation_text (5085786687366339027) --> - <skip /> - <!-- no translation found for letterbox_education_reposition_text (1068293354123934727) --> - <skip /> + <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"See and do more"</string> + <string name="letterbox_education_split_screen_text" msgid="6206339484068670830">"Drag in another app for split-screen"</string> + <string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Double-tap outside an app to reposition it"</string> <string name="letterbox_education_got_it" msgid="4057634570866051177">"Got it"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Expand for more information."</string> + <string name="maximize_button_text" msgid="1650859196290301963">"Maximise"</string> + <string name="minimize_button_text" msgid="271592547935841753">"Minimise"</string> + <string name="close_button_text" msgid="2913281996024033299">"Close"</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 1fb319196bef..839789b22a1c 100644 --- a/libs/WindowManager/Shell/res/values-en-rAU/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-en-rAU/strings_tv.xml @@ -19,7 +19,16 @@ 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="9135220303720555525">"Close PIP"</string> + <string name="pip_close" msgid="2955969519031223530">"Close"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Full screen"</string> - <string name="pip_move" msgid="1544227837964635439">"Move PIP"</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_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.xml b/libs/WindowManager/Shell/res/values-en-rCA/strings.xml index da4933b18a8f..acc75e46316d 100644 --- a/libs/WindowManager/Shell/res/values-en-rCA/strings.xml +++ b/libs/WindowManager/Shell/res/values-en-rCA/strings.xml @@ -22,6 +22,7 @@ <string name="pip_phone_settings" msgid="5468987116750491918">"Settings"</string> <string name="pip_phone_enter_split" msgid="7042877263880641911">"Enter split screen"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Menu"</string> + <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Picture-in-picture menu"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> is in picture-in-picture"</string> <string name="pip_notification_message" msgid="8854051911700302620">"If you don\'t want <xliff:g id="NAME">%s</xliff:g> to use this feature, tap to open settings and turn it off."</string> <string name="pip_play" msgid="3496151081459417097">"Play"</string> @@ -36,6 +37,7 @@ <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"App may not work on a secondary display."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"App does not support launch on secondary displays."</string> <string name="accessibility_divider" msgid="703810061635792791">"Split screen divider"</string> + <string name="divider_title" msgid="5482989479865361192">"Split screen divider"</string> <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Left full screen"</string> <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Left 70%"</string> <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Left 50%"</string> @@ -72,17 +74,16 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Bubble"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Manage"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Bubble dismissed."</string> - <string name="restart_button_description" msgid="5887656107651190519">"Tap to restart this app and go full screen."</string> + <string name="restart_button_description" msgid="6712141648865547958">"Tap to restart this app for a better view."</string> <string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"Camera issues?\nTap to refit"</string> <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Didn’t fix it?\nTap to revert"</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"No camera issues? Tap to dismiss."</string> - <!-- no translation found for letterbox_education_dialog_title (6688664582871779215) --> - <skip /> - <!-- no translation found for letterbox_education_dialog_subtext (4853542518367719562) --> - <skip /> - <!-- no translation found for letterbox_education_screen_rotation_text (5085786687366339027) --> - <skip /> - <!-- no translation found for letterbox_education_reposition_text (1068293354123934727) --> - <skip /> + <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"See and do more"</string> + <string name="letterbox_education_split_screen_text" msgid="6206339484068670830">"Drag in another app for split-screen"</string> + <string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Double-tap outside an app to reposition it"</string> <string name="letterbox_education_got_it" msgid="4057634570866051177">"Got it"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Expand for more information."</string> + <string name="maximize_button_text" msgid="1650859196290301963">"Maximise"</string> + <string name="minimize_button_text" msgid="271592547935841753">"Minimise"</string> + <string name="close_button_text" msgid="2913281996024033299">"Close"</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 1fb319196bef..839789b22a1c 100644 --- a/libs/WindowManager/Shell/res/values-en-rCA/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-en-rCA/strings_tv.xml @@ -19,7 +19,16 @@ 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="9135220303720555525">"Close PIP"</string> + <string name="pip_close" msgid="2955969519031223530">"Close"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Full screen"</string> - <string name="pip_move" msgid="1544227837964635439">"Move PIP"</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_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.xml b/libs/WindowManager/Shell/res/values-en-rGB/strings.xml index da4933b18a8f..acc75e46316d 100644 --- a/libs/WindowManager/Shell/res/values-en-rGB/strings.xml +++ b/libs/WindowManager/Shell/res/values-en-rGB/strings.xml @@ -22,6 +22,7 @@ <string name="pip_phone_settings" msgid="5468987116750491918">"Settings"</string> <string name="pip_phone_enter_split" msgid="7042877263880641911">"Enter split screen"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Menu"</string> + <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Picture-in-picture menu"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> is in picture-in-picture"</string> <string name="pip_notification_message" msgid="8854051911700302620">"If you don\'t want <xliff:g id="NAME">%s</xliff:g> to use this feature, tap to open settings and turn it off."</string> <string name="pip_play" msgid="3496151081459417097">"Play"</string> @@ -36,6 +37,7 @@ <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"App may not work on a secondary display."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"App does not support launch on secondary displays."</string> <string name="accessibility_divider" msgid="703810061635792791">"Split screen divider"</string> + <string name="divider_title" msgid="5482989479865361192">"Split screen divider"</string> <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Left full screen"</string> <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Left 70%"</string> <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Left 50%"</string> @@ -72,17 +74,16 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Bubble"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Manage"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Bubble dismissed."</string> - <string name="restart_button_description" msgid="5887656107651190519">"Tap to restart this app and go full screen."</string> + <string name="restart_button_description" msgid="6712141648865547958">"Tap to restart this app for a better view."</string> <string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"Camera issues?\nTap to refit"</string> <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Didn’t fix it?\nTap to revert"</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"No camera issues? Tap to dismiss."</string> - <!-- no translation found for letterbox_education_dialog_title (6688664582871779215) --> - <skip /> - <!-- no translation found for letterbox_education_dialog_subtext (4853542518367719562) --> - <skip /> - <!-- no translation found for letterbox_education_screen_rotation_text (5085786687366339027) --> - <skip /> - <!-- no translation found for letterbox_education_reposition_text (1068293354123934727) --> - <skip /> + <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"See and do more"</string> + <string name="letterbox_education_split_screen_text" msgid="6206339484068670830">"Drag in another app for split-screen"</string> + <string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Double-tap outside an app to reposition it"</string> <string name="letterbox_education_got_it" msgid="4057634570866051177">"Got it"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Expand for more information."</string> + <string name="maximize_button_text" msgid="1650859196290301963">"Maximise"</string> + <string name="minimize_button_text" msgid="271592547935841753">"Minimise"</string> + <string name="close_button_text" msgid="2913281996024033299">"Close"</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 1fb319196bef..839789b22a1c 100644 --- a/libs/WindowManager/Shell/res/values-en-rGB/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-en-rGB/strings_tv.xml @@ -19,7 +19,16 @@ 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="9135220303720555525">"Close PIP"</string> + <string name="pip_close" msgid="2955969519031223530">"Close"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Full screen"</string> - <string name="pip_move" msgid="1544227837964635439">"Move PIP"</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_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.xml b/libs/WindowManager/Shell/res/values-en-rIN/strings.xml index da4933b18a8f..acc75e46316d 100644 --- a/libs/WindowManager/Shell/res/values-en-rIN/strings.xml +++ b/libs/WindowManager/Shell/res/values-en-rIN/strings.xml @@ -22,6 +22,7 @@ <string name="pip_phone_settings" msgid="5468987116750491918">"Settings"</string> <string name="pip_phone_enter_split" msgid="7042877263880641911">"Enter split screen"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Menu"</string> + <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Picture-in-picture menu"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> is in picture-in-picture"</string> <string name="pip_notification_message" msgid="8854051911700302620">"If you don\'t want <xliff:g id="NAME">%s</xliff:g> to use this feature, tap to open settings and turn it off."</string> <string name="pip_play" msgid="3496151081459417097">"Play"</string> @@ -36,6 +37,7 @@ <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"App may not work on a secondary display."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"App does not support launch on secondary displays."</string> <string name="accessibility_divider" msgid="703810061635792791">"Split screen divider"</string> + <string name="divider_title" msgid="5482989479865361192">"Split screen divider"</string> <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Left full screen"</string> <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Left 70%"</string> <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Left 50%"</string> @@ -72,17 +74,16 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Bubble"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Manage"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Bubble dismissed."</string> - <string name="restart_button_description" msgid="5887656107651190519">"Tap to restart this app and go full screen."</string> + <string name="restart_button_description" msgid="6712141648865547958">"Tap to restart this app for a better view."</string> <string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"Camera issues?\nTap to refit"</string> <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Didn’t fix it?\nTap to revert"</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"No camera issues? Tap to dismiss."</string> - <!-- no translation found for letterbox_education_dialog_title (6688664582871779215) --> - <skip /> - <!-- no translation found for letterbox_education_dialog_subtext (4853542518367719562) --> - <skip /> - <!-- no translation found for letterbox_education_screen_rotation_text (5085786687366339027) --> - <skip /> - <!-- no translation found for letterbox_education_reposition_text (1068293354123934727) --> - <skip /> + <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"See and do more"</string> + <string name="letterbox_education_split_screen_text" msgid="6206339484068670830">"Drag in another app for split-screen"</string> + <string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Double-tap outside an app to reposition it"</string> <string name="letterbox_education_got_it" msgid="4057634570866051177">"Got it"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Expand for more information."</string> + <string name="maximize_button_text" msgid="1650859196290301963">"Maximise"</string> + <string name="minimize_button_text" msgid="271592547935841753">"Minimise"</string> + <string name="close_button_text" msgid="2913281996024033299">"Close"</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 1fb319196bef..839789b22a1c 100644 --- a/libs/WindowManager/Shell/res/values-en-rIN/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-en-rIN/strings_tv.xml @@ -19,7 +19,16 @@ 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="9135220303720555525">"Close PIP"</string> + <string name="pip_close" msgid="2955969519031223530">"Close"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Full screen"</string> - <string name="pip_move" msgid="1544227837964635439">"Move PIP"</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_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.xml b/libs/WindowManager/Shell/res/values-en-rXC/strings.xml index 5c3d0f65374a..4e9f13fccf2c 100644 --- a/libs/WindowManager/Shell/res/values-en-rXC/strings.xml +++ b/libs/WindowManager/Shell/res/values-en-rXC/strings.xml @@ -22,6 +22,7 @@ <string name="pip_phone_settings" msgid="5468987116750491918">"Settings"</string> <string name="pip_phone_enter_split" msgid="7042877263880641911">"Enter split screen"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Menu"</string> + <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Picture-in-Picture Menu"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> is in picture-in-picture"</string> <string name="pip_notification_message" msgid="8854051911700302620">"If you don\'t want <xliff:g id="NAME">%s</xliff:g> to use this feature, tap to open settings and turn it off."</string> <string name="pip_play" msgid="3496151081459417097">"Play"</string> @@ -36,6 +37,7 @@ <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"App may not work on a secondary display."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"App does not support launch on secondary displays."</string> <string name="accessibility_divider" msgid="703810061635792791">"Split-screen divider"</string> + <string name="divider_title" msgid="5482989479865361192">"Split-screen divider"</string> <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Left full screen"</string> <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Left 70%"</string> <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Left 50%"</string> @@ -72,13 +74,16 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Bubble"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Manage"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Bubble dismissed."</string> - <string name="restart_button_description" msgid="5887656107651190519">"Tap to restart this app and go full screen."</string> + <string name="restart_button_description" msgid="6712141648865547958">"Tap to restart this app for a better view."</string> <string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"Camera issues?\nTap to refit"</string> <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Didn’t fix it?\nTap to revert"</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"No camera issues? Tap to dismiss."</string> - <string name="letterbox_education_dialog_title" msgid="6688664582871779215">"Some apps work best in portrait"</string> - <string name="letterbox_education_dialog_subtext" msgid="4853542518367719562">"Try one of these options to make the most of your space"</string> - <string name="letterbox_education_screen_rotation_text" msgid="5085786687366339027">"Rotate your device to go full screen"</string> - <string name="letterbox_education_reposition_text" msgid="1068293354123934727">"Double-tap next to an app to reposition it"</string> + <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"See and do more"</string> + <string name="letterbox_education_split_screen_text" msgid="6206339484068670830">"Drag in another app for split-screen"</string> + <string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Double-tap outside an app to reposition it"</string> <string name="letterbox_education_got_it" msgid="4057634570866051177">"Got it"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Expand for more information."</string> + <string name="maximize_button_text" msgid="1650859196290301963">"Maximize"</string> + <string name="minimize_button_text" msgid="271592547935841753">"Minimize"</string> + <string name="close_button_text" msgid="2913281996024033299">"Close"</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 3b12d90f33a2..507e066e3812 100644 --- a/libs/WindowManager/Shell/res/values-en-rXC/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-en-rXC/strings_tv.xml @@ -19,7 +19,16 @@ 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="9135220303720555525">"Close PIP"</string> + <string name="pip_close" msgid="2955969519031223530">"Close"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Full screen"</string> - <string name="pip_move" msgid="1544227837964635439">"Move PIP"</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_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.xml b/libs/WindowManager/Shell/res/values-es-rUS/strings.xml index 154c7abae42d..b5b6670d0990 100644 --- a/libs/WindowManager/Shell/res/values-es-rUS/strings.xml +++ b/libs/WindowManager/Shell/res/values-es-rUS/strings.xml @@ -22,6 +22,7 @@ <string name="pip_phone_settings" msgid="5468987116750491918">"Configuración"</string> <string name="pip_phone_enter_split" msgid="7042877263880641911">"Introducir pantalla dividida"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Menú"</string> + <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Menú de pantalla en pantalla"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> está en modo de Pantalla en pantalla"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Si no quieres que <xliff:g id="NAME">%s</xliff:g> use esta función, presiona para abrir la configuración y desactivarla."</string> <string name="pip_play" msgid="3496151081459417097">"Reproducir"</string> @@ -36,6 +37,7 @@ <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Es posible que la app no funcione en una pantalla secundaria."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"La app no puede iniciarse en pantallas secundarias."</string> <string name="accessibility_divider" msgid="703810061635792791">"Divisor de pantalla dividida"</string> + <string name="divider_title" msgid="5482989479865361192">"Divisor de pantalla dividida"</string> <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Pantalla izquierda completa"</string> <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Izquierda: 70%"</string> <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Izquierda: 50%"</string> @@ -72,17 +74,19 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Cuadro"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Administrar"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Se descartó el cuadro."</string> - <string name="restart_button_description" msgid="5887656107651190519">"Presiona para reiniciar esta app y acceder al modo de pantalla completa."</string> + <string name="restart_button_description" msgid="6712141648865547958">"Presiona para reiniciar esta app y tener una mejor vista."</string> <string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"¿Tienes problemas con la cámara?\nPresiona para reajustarla"</string> <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"¿No se resolvió?\nPresiona para revertir los cambios"</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"¿No tienes problemas con la cámara? Presionar para descartar."</string> - <!-- no translation found for letterbox_education_dialog_title (6688664582871779215) --> + <!-- no translation found for letterbox_education_dialog_title (7739895354143295358) --> <skip /> - <!-- no translation found for letterbox_education_dialog_subtext (4853542518367719562) --> + <!-- no translation found for letterbox_education_split_screen_text (6206339484068670830) --> <skip /> - <!-- no translation found for letterbox_education_screen_rotation_text (5085786687366339027) --> - <skip /> - <!-- no translation found for letterbox_education_reposition_text (1068293354123934727) --> + <!-- no translation found for letterbox_education_reposition_text (4589957299813220661) --> <skip /> <string name="letterbox_education_got_it" msgid="4057634570866051177">"Entendido"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Expande para obtener más información."</string> + <string name="maximize_button_text" msgid="1650859196290301963">"Maximizar"</string> + <string name="minimize_button_text" msgid="271592547935841753">"Minimizar"</string> + <string name="close_button_text" msgid="2913281996024033299">"Cerrar"</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 1beb0b5b6255..a2c27b79e04c 100644 --- a/libs/WindowManager/Shell/res/values-es-rUS/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-es-rUS/strings_tv.xml @@ -19,7 +19,16 @@ 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="9135220303720555525">"Cerrar PIP"</string> + <string name="pip_close" msgid="2955969519031223530">"Cerrar"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Pantalla completa"</string> - <string name="pip_move" msgid="1544227837964635439">"Mover PIP"</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_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 e2fa3a0376e0..e38fc09d74d4 100644 --- a/libs/WindowManager/Shell/res/values-es/strings.xml +++ b/libs/WindowManager/Shell/res/values-es/strings.xml @@ -22,6 +22,7 @@ <string name="pip_phone_settings" msgid="5468987116750491918">"Ajustes"</string> <string name="pip_phone_enter_split" msgid="7042877263880641911">"Introducir pantalla dividida"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Menú"</string> + <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Menú de imagen en imagen"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> está en imagen en imagen"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Si no quieres que <xliff:g id="NAME">%s</xliff:g> utilice esta función, toca la notificación para abrir los ajustes y desactivarla."</string> <string name="pip_play" msgid="3496151081459417097">"Reproducir"</string> @@ -36,6 +37,7 @@ <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Es posible que la aplicación no funcione en una pantalla secundaria."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"La aplicación no se puede abrir en pantallas secundarias."</string> <string name="accessibility_divider" msgid="703810061635792791">"Dividir la pantalla"</string> + <string name="divider_title" msgid="5482989479865361192">"Divisor de pantalla dividida"</string> <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Pantalla izquierda completa"</string> <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Izquierda 70%"</string> <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Izquierda 50%"</string> @@ -46,10 +48,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 +65,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 llamadas \"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 llamados \"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> @@ -72,17 +74,19 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Burbuja"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Gestionar"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Burbuja cerrada."</string> - <string name="restart_button_description" msgid="5887656107651190519">"Toca para reiniciar esta aplicación e ir a la pantalla completa."</string> + <string name="restart_button_description" msgid="6712141648865547958">"Toca para reiniciar esta aplicación y obtener una mejor vista."</string> <string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"¿Problemas con la cámara?\nToca para reajustar"</string> <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"¿No se ha solucionado?\nToca para revertir"</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"¿No hay problemas con la cámara? Toca para cerrar."</string> - <!-- no translation found for letterbox_education_dialog_title (6688664582871779215) --> + <!-- no translation found for letterbox_education_dialog_title (7739895354143295358) --> <skip /> - <!-- no translation found for letterbox_education_dialog_subtext (4853542518367719562) --> + <!-- no translation found for letterbox_education_split_screen_text (6206339484068670830) --> <skip /> - <!-- no translation found for letterbox_education_screen_rotation_text (5085786687366339027) --> - <skip /> - <!-- no translation found for letterbox_education_reposition_text (1068293354123934727) --> + <!-- no translation found for letterbox_education_reposition_text (4589957299813220661) --> <skip /> <string name="letterbox_education_got_it" msgid="4057634570866051177">"Entendido"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Mostrar más información"</string> + <string name="maximize_button_text" msgid="1650859196290301963">"Maximizar"</string> + <string name="minimize_button_text" msgid="271592547935841753">"Minimizar"</string> + <string name="close_button_text" msgid="2913281996024033299">"Cerrar"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-es/strings_tv.xml b/libs/WindowManager/Shell/res/values-es/strings_tv.xml index d042b43c8ce8..75db421ec405 100644 --- a/libs/WindowManager/Shell/res/values-es/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-es/strings_tv.xml @@ -19,7 +19,16 @@ 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="9135220303720555525">"Cerrar PIP"</string> + <string name="pip_close" msgid="2955969519031223530">"Cerrar"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Pantalla completa"</string> - <string name="pip_move" msgid="1544227837964635439">"Mover imagen en imagen"</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_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.xml b/libs/WindowManager/Shell/res/values-et/strings.xml index da33f4d60d2a..f46845265388 100644 --- a/libs/WindowManager/Shell/res/values-et/strings.xml +++ b/libs/WindowManager/Shell/res/values-et/strings.xml @@ -22,6 +22,7 @@ <string name="pip_phone_settings" msgid="5468987116750491918">"Seaded"</string> <string name="pip_phone_enter_split" msgid="7042877263880641911">"Ava jagatud ekraanikuva"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Menüü"</string> + <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Menüü Pilt pildis"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> on režiimis Pilt pildis"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Kui te ei soovi, et rakendus <xliff:g id="NAME">%s</xliff:g> seda funktsiooni kasutaks, puudutage seadete avamiseks ja lülitage see välja."</string> <string name="pip_play" msgid="3496151081459417097">"Esita"</string> @@ -36,6 +37,7 @@ <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Rakendus ei pruugi teisesel ekraanil töötada."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Rakendus ei toeta teisestel ekraanidel käivitamist."</string> <string name="accessibility_divider" msgid="703810061635792791">"Ekraanijagaja"</string> + <string name="divider_title" msgid="5482989479865361192">"Ekraanijagaja"</string> <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Vasak täisekraan"</string> <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Vasak: 70%"</string> <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Vasak: 50%"</string> @@ -72,17 +74,19 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Mull"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Halda"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Mullist loobuti."</string> - <string name="restart_button_description" msgid="5887656107651190519">"Puudutage rakenduse taaskäivitamiseks ja täisekraanrežiimi aktiveerimiseks."</string> + <string name="restart_button_description" msgid="6712141648865547958">"Puudutage, et see rakendus parema vaate jaoks taaskäivitada."</string> <string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"Kas teil on kaameraprobleeme?\nPuudutage ümberpaigutamiseks."</string> <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Kas probleemi ei lahendatud?\nPuudutage ennistamiseks."</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Kas kaameraprobleeme pole? Puudutage loobumiseks."</string> - <!-- no translation found for letterbox_education_dialog_title (6688664582871779215) --> + <!-- no translation found for letterbox_education_dialog_title (7739895354143295358) --> <skip /> - <!-- no translation found for letterbox_education_dialog_subtext (4853542518367719562) --> + <!-- no translation found for letterbox_education_split_screen_text (6206339484068670830) --> <skip /> - <!-- no translation found for letterbox_education_screen_rotation_text (5085786687366339027) --> - <skip /> - <!-- no translation found for letterbox_education_reposition_text (1068293354123934727) --> + <!-- no translation found for letterbox_education_reposition_text (4589957299813220661) --> <skip /> <string name="letterbox_education_got_it" msgid="4057634570866051177">"Selge"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Laiendage lisateabe saamiseks."</string> + <string name="maximize_button_text" msgid="1650859196290301963">"Maksimeeri"</string> + <string name="minimize_button_text" msgid="271592547935841753">"Minimeeri"</string> + <string name="close_button_text" msgid="2913281996024033299">"Sule"</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 3da16db9e196..e8fcb180c0c4 100644 --- a/libs/WindowManager/Shell/res/values-et/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-et/strings_tv.xml @@ -19,7 +19,16 @@ 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="9135220303720555525">"Sule PIP"</string> + <string name="pip_close" msgid="2955969519031223530">"Sule"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Täisekraan"</string> - <string name="pip_move" msgid="1544227837964635439">"Teisalda PIP-režiimi"</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_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 e0dd3ca2c9e3..2c1ee82f34b2 100644 --- a/libs/WindowManager/Shell/res/values-eu/strings.xml +++ b/libs/WindowManager/Shell/res/values-eu/strings.xml @@ -22,8 +22,9 @@ <string name="pip_phone_settings" msgid="5468987116750491918">"Ezarpenak"</string> <string name="pip_phone_enter_split" msgid="7042877263880641911">"Sartu pantaila zatituan"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Menua"</string> + <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Pantaila txiki gainjarriaren 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">"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_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_play" msgid="3496151081459417097">"Erreproduzitu"</string> <string name="pip_pause" msgid="690688849510295232">"Pausatu"</string> <string name="pip_skip_to_next" msgid="8403429188794867653">"Joan hurrengora"</string> @@ -36,6 +37,7 @@ <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Baliteke aplikazioak ez funtzionatzea bigarren mailako pantailetan."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Aplikazioa ezin da abiarazi bigarren mailako pantailatan."</string> <string name="accessibility_divider" msgid="703810061635792791">"Pantaila-zatitzailea"</string> + <string name="divider_title" msgid="5482989479865361192">"Pantaila-zatitzailea"</string> <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Ezarri ezkerraldea pantaila osoan"</string> <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Ezarri ezkerraldea % 70en"</string> <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Ezarri ezkerraldea % 50en"</string> @@ -72,17 +74,19 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Burbuila"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Kudeatu"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Baztertu da globoa."</string> - <string name="restart_button_description" msgid="5887656107651190519">"Saka ezazu aplikazioa berrabiarazteko, eta ezarri pantaila osoko modua."</string> + <string name="restart_button_description" msgid="6712141648865547958">"Hobeto ikusteko, sakatu hau aplikazioa berrabiarazteko."</string> <string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"Arazoak dauzkazu kamerarekin?\nBerriro doitzeko, sakatu hau."</string> <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Ez al da konpondu?\nLeheneratzeko, sakatu hau."</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Ez daukazu arazorik kamerarekin? Baztertzeko, sakatu hau."</string> - <!-- no translation found for letterbox_education_dialog_title (6688664582871779215) --> + <!-- no translation found for letterbox_education_dialog_title (7739895354143295358) --> <skip /> - <!-- no translation found for letterbox_education_dialog_subtext (4853542518367719562) --> + <!-- no translation found for letterbox_education_split_screen_text (6206339484068670830) --> <skip /> - <!-- no translation found for letterbox_education_screen_rotation_text (5085786687366339027) --> - <skip /> - <!-- no translation found for letterbox_education_reposition_text (1068293354123934727) --> + <!-- no translation found for letterbox_education_reposition_text (4589957299813220661) --> <skip /> <string name="letterbox_education_got_it" msgid="4057634570866051177">"Ados"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Informazio gehiago lortzeko, zabaldu hau."</string> + <string name="maximize_button_text" msgid="1650859196290301963">"Maximizatu"</string> + <string name="minimize_button_text" msgid="271592547935841753">"Minimizatu"</string> + <string name="close_button_text" msgid="2913281996024033299">"Itxi"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-eu/strings_tv.xml b/libs/WindowManager/Shell/res/values-eu/strings_tv.xml index e4b57baf1e5f..07d75d2de9cd 100644 --- a/libs/WindowManager/Shell/res/values-eu/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-eu/strings_tv.xml @@ -19,7 +19,16 @@ 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="9135220303720555525">"Itxi PIPa"</string> + <string name="pip_close" msgid="2955969519031223530">"Itxi"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Pantaila osoa"</string> - <string name="pip_move" msgid="1544227837964635439">"Mugitu pantaila txiki gainjarria"</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_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.xml b/libs/WindowManager/Shell/res/values-fa/strings.xml index 6fcb5ee7ad6d..6b7bf4d9048b 100644 --- a/libs/WindowManager/Shell/res/values-fa/strings.xml +++ b/libs/WindowManager/Shell/res/values-fa/strings.xml @@ -22,6 +22,7 @@ <string name="pip_phone_settings" msgid="5468987116750491918">"تنظیمات"</string> <string name="pip_phone_enter_split" msgid="7042877263880641911">"ورود به حالت «صفحهٔ دونیمه»"</string> <string name="pip_menu_title" msgid="5393619322111827096">"منو"</string> + <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"منو تصویر در تصویر"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> درحالت تصویر در تصویر است"</string> <string name="pip_notification_message" msgid="8854051911700302620">"اگر نمیخواهید <xliff:g id="NAME">%s</xliff:g> از این قابلیت استفاده کند، با ضربه زدن، تنظیمات را باز کنید و آن را خاموش کنید."</string> <string name="pip_play" msgid="3496151081459417097">"پخش"</string> @@ -36,6 +37,7 @@ <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"ممکن است برنامه در نمایشگر ثانویه کار نکند."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"برنامه از راهاندازی در نمایشگرهای ثانویه پشتیبانی نمیکند."</string> <string name="accessibility_divider" msgid="703810061635792791">"تقسیمکننده صفحه"</string> + <string name="divider_title" msgid="5482989479865361192">"تقسیمکننده صفحهٔ دونیمه"</string> <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"تمامصفحه چپ"</string> <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"٪۷۰ چپ"</string> <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"٪۵۰ چپ"</string> @@ -48,8 +50,8 @@ <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"تمامصفحه پایین"</string> <string name="one_handed_tutorial_title" msgid="4583241688067426350">"استفاده از حالت یکدستی"</string> <string name="one_handed_tutorial_description" msgid="3486582858591353067">"برای خارج شدن، از پایین صفحهنمایش تند بهطرف بالا بکشید یا در هر جایی از بالای برنامه که میخواهید ضربه بزنید"</string> - <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"آغاز «حالت تک حرکت»"</string> - <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"خروج از «حالت تک حرکت»"</string> + <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"آغاز «حالت یکدستی»"</string> + <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"خروج از «حالت یکدستی»"</string> <string name="bubbles_settings_button_description" msgid="1301286017420516912">"تنظیمات برای حبابکهای <xliff:g id="APP_NAME">%1$s</xliff:g>"</string> <string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"لبریزشده"</string> <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"افزودن برگشت به پشته"</string> @@ -72,17 +74,19 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"حباب"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"مدیریت"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"حبابک رد شد."</string> - <string name="restart_button_description" msgid="5887656107651190519">"برای بازراهاندازی این برنامه و تغییر به حالت تمامصفحه، ضربه بزنید."</string> + <string name="restart_button_description" msgid="6712141648865547958">"برای داشتن نمایی بهتر، ضربه بزنید تا این برنامه بازراهاندازی شود."</string> <string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"دوربین مشکل دارد؟\nبرای تنظیم مجدد اندازه ضربه بزنید"</string> <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"مشکل برطرف نشد؟\nبرای برگرداندن ضربه بزنید"</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"دوربین مشکلی ندارد؟ برای بستن ضربه بزنید."</string> - <!-- no translation found for letterbox_education_dialog_title (6688664582871779215) --> + <!-- no translation found for letterbox_education_dialog_title (7739895354143295358) --> <skip /> - <!-- no translation found for letterbox_education_dialog_subtext (4853542518367719562) --> + <!-- no translation found for letterbox_education_split_screen_text (6206339484068670830) --> <skip /> - <!-- no translation found for letterbox_education_screen_rotation_text (5085786687366339027) --> - <skip /> - <!-- no translation found for letterbox_education_reposition_text (1068293354123934727) --> + <!-- no translation found for letterbox_education_reposition_text (4589957299813220661) --> <skip /> <string name="letterbox_education_got_it" msgid="4057634570866051177">"متوجهام"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"برای اطلاعات بیشتر، گسترده کنید."</string> + <string name="maximize_button_text" msgid="1650859196290301963">"بزرگ کردن"</string> + <string name="minimize_button_text" msgid="271592547935841753">"کوچک کردن"</string> + <string name="close_button_text" msgid="2913281996024033299">"بستن"</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 aaab34f807db..03f51d01a3a8 100644 --- a/libs/WindowManager/Shell/res/values-fa/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-fa/strings_tv.xml @@ -19,7 +19,16 @@ 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="9135220303720555525">"بستن PIP"</string> + <string name="pip_close" msgid="2955969519031223530">"بستن"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"تمام صفحه"</string> - <string name="pip_move" msgid="1544227837964635439">"انتقال PIP (تصویر در تصویر)"</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_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.xml b/libs/WindowManager/Shell/res/values-fi/strings.xml index fc51ad4598b7..b82a7cc4e8f7 100644 --- a/libs/WindowManager/Shell/res/values-fi/strings.xml +++ b/libs/WindowManager/Shell/res/values-fi/strings.xml @@ -22,6 +22,7 @@ <string name="pip_phone_settings" msgid="5468987116750491918">"Asetukset"</string> <string name="pip_phone_enter_split" msgid="7042877263880641911">"Avaa jaettu näyttö"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Valikko"</string> + <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Kuva kuvassa ‑valikko"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> on kuva kuvassa ‑tilassa"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Jos et halua, että <xliff:g id="NAME">%s</xliff:g> voi käyttää tätä ominaisuutta, avaa asetukset napauttamalla ja poista se käytöstä."</string> <string name="pip_play" msgid="3496151081459417097">"Toista"</string> @@ -36,6 +37,7 @@ <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Sovellus ei ehkä toimi toissijaisella näytöllä."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Sovellus ei tue käynnistämistä toissijaisilla näytöillä."</string> <string name="accessibility_divider" msgid="703810061635792791">"Näytön jakaja"</string> + <string name="divider_title" msgid="5482989479865361192">"Näytönjakaja"</string> <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Vasen koko näytölle"</string> <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Vasen 70 %"</string> <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Vasen 50 %"</string> @@ -72,17 +74,19 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Kupla"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Ylläpidä"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Kupla ohitettu."</string> - <string name="restart_button_description" msgid="5887656107651190519">"Napauta, niin sovellus käynnistyy uudelleen ja siirtyy koko näytön tilaan."</string> + <string name="restart_button_description" msgid="6712141648865547958">"Napauta, niin sovellus käynnistyy uudelleen paremmin näytölle sopivana."</string> <string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"Onko kameran kanssa ongelmia?\nKorjaa napauttamalla"</string> <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Eikö ongelma ratkennut?\nKumoa napauttamalla"</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Ei ongelmia kameran kanssa? Hylkää napauttamalla."</string> - <!-- no translation found for letterbox_education_dialog_title (6688664582871779215) --> + <!-- no translation found for letterbox_education_dialog_title (7739895354143295358) --> <skip /> - <!-- no translation found for letterbox_education_dialog_subtext (4853542518367719562) --> + <!-- no translation found for letterbox_education_split_screen_text (6206339484068670830) --> <skip /> - <!-- no translation found for letterbox_education_screen_rotation_text (5085786687366339027) --> - <skip /> - <!-- no translation found for letterbox_education_reposition_text (1068293354123934727) --> + <!-- no translation found for letterbox_education_reposition_text (4589957299813220661) --> <skip /> <string name="letterbox_education_got_it" msgid="4057634570866051177">"OK"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Katso lisätietoja laajentamalla."</string> + <string name="maximize_button_text" msgid="1650859196290301963">"Suurenna"</string> + <string name="minimize_button_text" msgid="271592547935841753">"Pienennä"</string> + <string name="close_button_text" msgid="2913281996024033299">"Sulje"</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 21c64633fac1..24ab7d99e180 100644 --- a/libs/WindowManager/Shell/res/values-fi/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-fi/strings_tv.xml @@ -19,7 +19,16 @@ 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="9135220303720555525">"Sulje PIP"</string> + <string name="pip_close" msgid="2955969519031223530">"Sulje"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Koko näyttö"</string> - <string name="pip_move" msgid="1544227837964635439">"Siirrä PIP"</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_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.xml b/libs/WindowManager/Shell/res/values-fr-rCA/strings.xml index 43fad3a69f4d..449dc27f0d70 100644 --- a/libs/WindowManager/Shell/res/values-fr-rCA/strings.xml +++ b/libs/WindowManager/Shell/res/values-fr-rCA/strings.xml @@ -22,6 +22,7 @@ <string name="pip_phone_settings" msgid="5468987116750491918">"Paramètres"</string> <string name="pip_phone_enter_split" msgid="7042877263880641911">"Entrer dans l\'écran partagé"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Menu"</string> + <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Menu d\'incrustation d\'image"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> est en mode d\'incrustation d\'image"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Si vous ne voulez pas que <xliff:g id="NAME">%s</xliff:g> utilise cette fonctionnalité, touchez l\'écran pour ouvrir les paramètres, puis désactivez-la."</string> <string name="pip_play" msgid="3496151081459417097">"Lire"</string> @@ -36,6 +37,7 @@ <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Il est possible que l\'application ne fonctionne pas sur un écran secondaire."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"L\'application ne peut pas être lancée sur des écrans secondaires."</string> <string name="accessibility_divider" msgid="703810061635792791">"Séparateur d\'écran partagé"</string> + <string name="divider_title" msgid="5482989479865361192">"Séparateur d\'écran partagé"</string> <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Plein écran à la gauche"</string> <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"70 % à la gauche"</string> <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"50 % à la gauche"</string> @@ -72,17 +74,19 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Bulle"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Gérer"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Bulle ignorée."</string> - <string name="restart_button_description" msgid="5887656107651190519">"Touchez pour redémarrer cette application et passer en plein écran."</string> + <string name="restart_button_description" msgid="6712141648865547958">"Touchez pour redémarrer cette application afin d\'obtenir un meilleur affichage."</string> <string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"Problèmes d\'appareil photo?\nTouchez pour réajuster"</string> <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Problème non résolu?\nTouchez pour rétablir"</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Aucun problème d\'appareil photo? Touchez pour ignorer."</string> - <!-- no translation found for letterbox_education_dialog_title (6688664582871779215) --> + <!-- no translation found for letterbox_education_dialog_title (7739895354143295358) --> <skip /> - <!-- no translation found for letterbox_education_dialog_subtext (4853542518367719562) --> + <!-- no translation found for letterbox_education_split_screen_text (6206339484068670830) --> <skip /> - <!-- no translation found for letterbox_education_screen_rotation_text (5085786687366339027) --> - <skip /> - <!-- no translation found for letterbox_education_reposition_text (1068293354123934727) --> + <!-- no translation found for letterbox_education_reposition_text (4589957299813220661) --> <skip /> <string name="letterbox_education_got_it" msgid="4057634570866051177">"OK"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Développer pour en savoir plus."</string> + <string name="maximize_button_text" msgid="1650859196290301963">"Agrandir"</string> + <string name="minimize_button_text" msgid="271592547935841753">"Réduire"</string> + <string name="close_button_text" msgid="2913281996024033299">"Fermer"</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 f4baaad13999..87651ec711d9 100644 --- a/libs/WindowManager/Shell/res/values-fr-rCA/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-fr-rCA/strings_tv.xml @@ -19,7 +19,16 @@ 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="9135220303720555525">"Fermer mode IDI"</string> + <string name="pip_close" msgid="2955969519031223530">"Fermer"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Plein écran"</string> - <string name="pip_move" msgid="1544227837964635439">"Déplacer l\'image incrustée"</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_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 8b8cc090eee5..15148b71e79d 100644 --- a/libs/WindowManager/Shell/res/values-fr/strings.xml +++ b/libs/WindowManager/Shell/res/values-fr/strings.xml @@ -22,6 +22,7 @@ <string name="pip_phone_settings" msgid="5468987116750491918">"Paramètres"</string> <string name="pip_phone_enter_split" msgid="7042877263880641911">"Accéder à l\'écran partagé"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Menu"</string> + <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Menu \"Picture-in-picture\""</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> est en mode Picture-in-picture"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Si vous ne voulez pas que l\'application <xliff:g id="NAME">%s</xliff:g> utilise cette fonctionnalité, appuyez ici pour ouvrir les paramètres et la désactiver."</string> <string name="pip_play" msgid="3496151081459417097">"Lecture"</string> @@ -36,6 +37,7 @@ <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Il est possible que l\'application ne fonctionne pas sur un écran secondaire."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"L\'application ne peut pas être lancée sur des écrans secondaires."</string> <string name="accessibility_divider" msgid="703810061635792791">"Séparateur d\'écran partagé"</string> + <string name="divider_title" msgid="5482989479865361192">"Séparateur d\'écran partagé"</string> <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Écran de gauche en plein écran"</string> <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Écran de gauche à 70 %"</string> <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Écran de gauche à 50 %"</string> @@ -63,7 +65,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 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 de 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> @@ -72,17 +74,19 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Bulle"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Gérer"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Bulle fermée."</string> - <string name="restart_button_description" msgid="5887656107651190519">"Appuyez pour redémarrer cette application et activer le mode plein écran."</string> + <string name="restart_button_description" msgid="6712141648865547958">"Appuyez pour redémarrer cette appli et avoir une meilleure vue."</string> <string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"Problèmes d\'appareil photo ?\nAppuyez pour réajuster"</string> <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Problème non résolu ?\nAppuyez pour rétablir"</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Aucun problème d\'appareil photo ? Appuyez pour ignorer."</string> - <!-- no translation found for letterbox_education_dialog_title (6688664582871779215) --> + <!-- no translation found for letterbox_education_dialog_title (7739895354143295358) --> <skip /> - <!-- no translation found for letterbox_education_dialog_subtext (4853542518367719562) --> + <!-- no translation found for letterbox_education_split_screen_text (6206339484068670830) --> <skip /> - <!-- no translation found for letterbox_education_screen_rotation_text (5085786687366339027) --> - <skip /> - <!-- no translation found for letterbox_education_reposition_text (1068293354123934727) --> + <!-- no translation found for letterbox_education_reposition_text (4589957299813220661) --> <skip /> <string name="letterbox_education_got_it" msgid="4057634570866051177">"OK"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Développez pour obtenir plus d\'informations"</string> + <string name="maximize_button_text" msgid="1650859196290301963">"Agrandir"</string> + <string name="minimize_button_text" msgid="271592547935841753">"Réduire"</string> + <string name="close_button_text" msgid="2913281996024033299">"Fermer"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-fr/strings_tv.xml b/libs/WindowManager/Shell/res/values-fr/strings_tv.xml index 6ad8174db796..37863fb82295 100644 --- a/libs/WindowManager/Shell/res/values-fr/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-fr/strings_tv.xml @@ -19,7 +19,16 @@ 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="9135220303720555525">"Fermer mode PIP"</string> + <string name="pip_close" msgid="2955969519031223530">"Fermer"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Plein écran"</string> - <string name="pip_move" msgid="1544227837964635439">"Déplacer le PIP"</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_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.xml b/libs/WindowManager/Shell/res/values-gl/strings.xml index 9bc9d9338030..b848fd0539e9 100644 --- a/libs/WindowManager/Shell/res/values-gl/strings.xml +++ b/libs/WindowManager/Shell/res/values-gl/strings.xml @@ -22,6 +22,7 @@ <string name="pip_phone_settings" msgid="5468987116750491918">"Configuración"</string> <string name="pip_phone_enter_split" msgid="7042877263880641911">"Inserir pantalla dividida"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Menú"</string> + <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Menú de pantalla superposta"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> está na pantalla superposta"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Se non queres que <xliff:g id="NAME">%s</xliff:g> utilice esta función, toca a configuración para abrir as opcións e desactivar a función."</string> <string name="pip_play" msgid="3496151081459417097">"Reproducir"</string> @@ -36,6 +37,7 @@ <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"É posible que a aplicación non funcione nunha pantalla secundaria."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"A aplicación non se pode iniciar en pantallas secundarias."</string> <string name="accessibility_divider" msgid="703810061635792791">"Divisor de pantalla dividida"</string> + <string name="divider_title" msgid="5482989479865361192">"Divisor de pantalla dividida"</string> <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Pantalla completa á esquerda"</string> <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"70 % á esquerda"</string> <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"50 % á esquerda"</string> @@ -72,17 +74,19 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Burbulla"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Xestionar"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Ignorouse a burbulla."</string> - <string name="restart_button_description" msgid="5887656107651190519">"Toca o botón para reiniciar esta aplicación e abrila en pantalla completa."</string> + <string name="restart_button_description" msgid="6712141648865547958">"Toca o botón para reiniciar esta aplicación e gozar dunha mellor visualización."</string> <string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"Tes problemas coa cámara?\nToca para reaxustala"</string> <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Non se solucionaron os problemas?\nToca para reverter o seu tratamento"</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Non hai problemas coa cámara? Tocar para ignorar."</string> - <!-- no translation found for letterbox_education_dialog_title (6688664582871779215) --> + <!-- no translation found for letterbox_education_dialog_title (7739895354143295358) --> <skip /> - <!-- no translation found for letterbox_education_dialog_subtext (4853542518367719562) --> + <!-- no translation found for letterbox_education_split_screen_text (6206339484068670830) --> <skip /> - <!-- no translation found for letterbox_education_screen_rotation_text (5085786687366339027) --> - <skip /> - <!-- no translation found for letterbox_education_reposition_text (1068293354123934727) --> + <!-- no translation found for letterbox_education_reposition_text (4589957299813220661) --> <skip /> <string name="letterbox_education_got_it" msgid="4057634570866051177">"Entendido"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Despregar para obter máis información."</string> + <string name="maximize_button_text" msgid="1650859196290301963">"Maximizar"</string> + <string name="minimize_button_text" msgid="271592547935841753">"Minimizar"</string> + <string name="close_button_text" msgid="2913281996024033299">"Pechar"</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 dcb8709d010e..5d6de76c4deb 100644 --- a/libs/WindowManager/Shell/res/values-gl/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-gl/strings_tv.xml @@ -19,7 +19,16 @@ 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="9135220303720555525">"Pechar PIP"</string> + <string name="pip_close" msgid="2955969519031223530">"Pechar"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Pantalla completa"</string> - <string name="pip_move" msgid="1544227837964635439">"Mover pantalla superposta"</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_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.xml b/libs/WindowManager/Shell/res/values-gu/strings.xml index 032b591de660..79d27a24d758 100644 --- a/libs/WindowManager/Shell/res/values-gu/strings.xml +++ b/libs/WindowManager/Shell/res/values-gu/strings.xml @@ -22,6 +22,7 @@ <string name="pip_phone_settings" msgid="5468987116750491918">"સેટિંગ"</string> <string name="pip_phone_enter_split" msgid="7042877263880641911">"વિભાજિત સ્ક્રીન મોડમાં દાખલ થાઓ"</string> <string name="pip_menu_title" msgid="5393619322111827096">"મેનૂ"</string> + <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"ચિત્રમાં ચિત્ર મેનૂ"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> ચિત્રમાં-ચિત્રની અંદર છે"</string> <string name="pip_notification_message" msgid="8854051911700302620">"જો તમે નથી ઇચ્છતા કે <xliff:g id="NAME">%s</xliff:g> આ સુવિધાનો ઉપયોગ કરે, તો સેટિંગ ખોલવા માટે ટૅપ કરો અને તેને બંધ કરો."</string> <string name="pip_play" msgid="3496151081459417097">"ચલાવો"</string> @@ -36,6 +37,7 @@ <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"ઍપ્લિકેશન ગૌણ ડિસ્પ્લે પર કદાચ કામ ન કરે."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"ઍપ્લિકેશન ગૌણ ડિસ્પ્લે પર લૉન્ચનું સમર્થન કરતી નથી."</string> <string name="accessibility_divider" msgid="703810061635792791">"સ્પ્લિટ-સ્ક્રીન વિભાજક"</string> + <string name="divider_title" msgid="5482989479865361192">"સ્ક્રીનને વિભાજિત કરતું વિભાજક"</string> <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"ડાબી પૂર્ણ સ્ક્રીન"</string> <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"ડાબે 70%"</string> <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"ડાબે 50%"</string> @@ -72,17 +74,19 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"બબલ"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"મેનેજ કરો"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"બબલ છોડી દેવાયો."</string> - <string name="restart_button_description" msgid="5887656107651190519">"આ ઍપ ફરીથી ચાલુ કરવા માટે ટૅપ કરીને પૂર્ણ સ્ક્રીન કરો."</string> + <string name="restart_button_description" msgid="6712141648865547958">"વધુ સારા વ્યૂ માટે, આ ઍપને ફરી શરૂ કરવા ટૅપ કરો."</string> <string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"કૅમેરામાં સમસ્યાઓ છે?\nફરીથી ફિટ કરવા માટે ટૅપ કરો"</string> <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"સુધારો નથી થયો?\nપહેલાંના પર પાછું ફેરવવા માટે ટૅપ કરો"</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"કૅમેરામાં કોઈ સમસ્યા નથી? છોડી દેવા માટે ટૅપ કરો."</string> - <!-- no translation found for letterbox_education_dialog_title (6688664582871779215) --> + <!-- no translation found for letterbox_education_dialog_title (7739895354143295358) --> <skip /> - <!-- no translation found for letterbox_education_dialog_subtext (4853542518367719562) --> + <!-- no translation found for letterbox_education_split_screen_text (6206339484068670830) --> <skip /> - <!-- no translation found for letterbox_education_screen_rotation_text (5085786687366339027) --> - <skip /> - <!-- no translation found for letterbox_education_reposition_text (1068293354123934727) --> + <!-- no translation found for letterbox_education_reposition_text (4589957299813220661) --> <skip /> <string name="letterbox_education_got_it" msgid="4057634570866051177">"સમજાઈ ગયું"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"વધુ માહિતી માટે મોટું કરો."</string> + <string name="maximize_button_text" msgid="1650859196290301963">"મોટું કરો"</string> + <string name="minimize_button_text" msgid="271592547935841753">"નાનું કરો"</string> + <string name="close_button_text" msgid="2913281996024033299">"બંધ કરો"</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 ed815caaed0f..6c1b9db73582 100644 --- a/libs/WindowManager/Shell/res/values-gu/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-gu/strings_tv.xml @@ -19,7 +19,16 @@ 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="9135220303720555525">"PIP બંધ કરો"</string> + <string name="pip_close" msgid="2955969519031223530">"બંધ કરો"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"પૂર્ણ સ્ક્રીન"</string> - <string name="pip_move" msgid="1544227837964635439">"PIP ખસેડો"</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_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 72fd65ce55fd..e803abe08511 100644 --- a/libs/WindowManager/Shell/res/values-hi/strings.xml +++ b/libs/WindowManager/Shell/res/values-hi/strings.xml @@ -22,6 +22,7 @@ <string name="pip_phone_settings" msgid="5468987116750491918">"सेटिंग"</string> <string name="pip_phone_enter_split" msgid="7042877263880641911">"स्प्लिट स्क्रीन मोड में जाएं"</string> <string name="pip_menu_title" msgid="5393619322111827096">"मेन्यू"</string> + <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"पिक्चर में पिक्चर मेन्यू"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> \"पिक्चर में पिक्चर\" के अंदर है"</string> <string name="pip_notification_message" msgid="8854051911700302620">"अगर आप नहीं चाहते कि <xliff:g id="NAME">%s</xliff:g> इस सुविधा का उपयोग करे, तो सेटिंग खोलने के लिए टैप करें और उसे बंद करें ."</string> <string name="pip_play" msgid="3496151081459417097">"चलाएं"</string> @@ -36,6 +37,7 @@ <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"हो सकता है कि ऐप प्राइमरी (मुख्य) डिस्प्ले के अलावा बाकी दूसरे डिस्प्ले पर काम न करे."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"प्राइमरी (मुख्य) डिस्प्ले के अलावा बाकी दूसरे डिस्प्ले पर ऐप लॉन्च नहीं किया जा सकता."</string> <string name="accessibility_divider" msgid="703810061635792791">"विभाजित स्क्रीन विभाजक"</string> + <string name="divider_title" msgid="5482989479865361192">"स्प्लिट स्क्रीन डिवाइडर"</string> <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"बाईं स्क्रीन को फ़ुल स्क्रीन बनाएं"</string> <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"बाईं स्क्रीन को 70% बनाएं"</string> <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"बाईं स्क्रीन को 50% बनाएं"</string> @@ -65,24 +67,26 @@ <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> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"बबल खारिज किया गया."</string> - <string name="restart_button_description" msgid="5887656107651190519">"इस ऐप्लिकेशन को रीस्टार्ट करने और फ़ुल स्क्रीन पर देखने के लिए टैप करें."</string> + <string name="restart_button_description" msgid="6712141648865547958">"टैप करके ऐप्लिकेशन को रीस्टार्ट करें और बेहतर व्यू पाएं."</string> <string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"क्या कैमरे से जुड़ी कोई समस्या है?\nफिर से फ़िट करने के लिए टैप करें"</string> <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"क्या समस्या ठीक नहीं हुई?\nपहले जैसा करने के लिए टैप करें"</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"क्या कैमरे से जुड़ी कोई समस्या नहीं है? खारिज करने के लिए टैप करें."</string> - <!-- no translation found for letterbox_education_dialog_title (6688664582871779215) --> + <!-- no translation found for letterbox_education_dialog_title (7739895354143295358) --> <skip /> - <!-- no translation found for letterbox_education_dialog_subtext (4853542518367719562) --> + <!-- no translation found for letterbox_education_split_screen_text (6206339484068670830) --> <skip /> - <!-- no translation found for letterbox_education_screen_rotation_text (5085786687366339027) --> - <skip /> - <!-- no translation found for letterbox_education_reposition_text (1068293354123934727) --> + <!-- no translation found for letterbox_education_reposition_text (4589957299813220661) --> <skip /> <string name="letterbox_education_got_it" msgid="4057634570866051177">"ठीक है"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"ज़्यादा जानकारी के लिए बड़ा करें."</string> + <string name="maximize_button_text" msgid="1650859196290301963">"बड़ा करें"</string> + <string name="minimize_button_text" msgid="271592547935841753">"विंडो छोटी करें"</string> + <string name="close_button_text" msgid="2913281996024033299">"बंद करें"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-hi/strings_tv.xml b/libs/WindowManager/Shell/res/values-hi/strings_tv.xml index 8bcc631b39a2..e0227253b2dc 100644 --- a/libs/WindowManager/Shell/res/values-hi/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-hi/strings_tv.xml @@ -19,7 +19,16 @@ 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="9135220303720555525">"PIP बंद करें"</string> + <string name="pip_close" msgid="2955969519031223530">"बंद करें"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"फ़ुल स्क्रीन"</string> - <string name="pip_move" msgid="1544227837964635439">"पीआईपी को दूसरी जगह लेकर जाएं"</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_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.xml b/libs/WindowManager/Shell/res/values-hr/strings.xml index 5315558ea855..07b946d4bd3a 100644 --- a/libs/WindowManager/Shell/res/values-hr/strings.xml +++ b/libs/WindowManager/Shell/res/values-hr/strings.xml @@ -22,6 +22,7 @@ <string name="pip_phone_settings" msgid="5468987116750491918">"Postavke"</string> <string name="pip_phone_enter_split" msgid="7042877263880641911">"Otvorite podijeljeni zaslon"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Izbornik"</string> + <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Izbornik slike u slici"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> jest na slici u slici"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Ako ne želite da aplikacija <xliff:g id="NAME">%s</xliff:g> upotrebljava tu značajku, dodirnite da biste otvorili postavke i isključili je."</string> <string name="pip_play" msgid="3496151081459417097">"Reproduciraj"</string> @@ -36,6 +37,7 @@ <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Aplikacija možda neće funkcionirati na sekundarnom zaslonu."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Aplikacija ne podržava pokretanje na sekundarnim zaslonima."</string> <string name="accessibility_divider" msgid="703810061635792791">"Razdjelnik podijeljenog zaslona"</string> + <string name="divider_title" msgid="5482989479865361192">"Razdjelnik podijeljenog zaslona"</string> <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Lijevi zaslon u cijeli zaslon"</string> <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Lijevi zaslon na 70%"</string> <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Lijevi zaslon na 50%"</string> @@ -72,17 +74,19 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Oblačić"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Upravljanje"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Oblačić odbačen."</string> - <string name="restart_button_description" msgid="5887656107651190519">"Dodirnite da biste ponovo pokrenuli tu aplikaciju i prikazali je na cijelom zaslonu."</string> + <string name="restart_button_description" msgid="6712141648865547958">"Dodirnite da biste ponovo pokrenuli tu aplikaciju kako biste bolje vidjeli."</string> <string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"Problemi s fotoaparatom?\nDodirnite za popravak"</string> <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Problem nije riješen?\nDodirnite za vraćanje"</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Nemate problema s fotoaparatom? Dodirnite za odbacivanje."</string> - <!-- no translation found for letterbox_education_dialog_title (6688664582871779215) --> + <!-- no translation found for letterbox_education_dialog_title (7739895354143295358) --> <skip /> - <!-- no translation found for letterbox_education_dialog_subtext (4853542518367719562) --> + <!-- no translation found for letterbox_education_split_screen_text (6206339484068670830) --> <skip /> - <!-- no translation found for letterbox_education_screen_rotation_text (5085786687366339027) --> - <skip /> - <!-- no translation found for letterbox_education_reposition_text (1068293354123934727) --> + <!-- no translation found for letterbox_education_reposition_text (4589957299813220661) --> <skip /> <string name="letterbox_education_got_it" msgid="4057634570866051177">"Shvaćam"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Proširite da biste saznali više."</string> + <string name="maximize_button_text" msgid="1650859196290301963">"Maksimiziraj"</string> + <string name="minimize_button_text" msgid="271592547935841753">"Minimiziraj"</string> + <string name="close_button_text" msgid="2913281996024033299">"Zatvori"</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 49b7ae0d7681..a09e6e805f63 100644 --- a/libs/WindowManager/Shell/res/values-hr/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-hr/strings_tv.xml @@ -19,7 +19,16 @@ 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="9135220303720555525">"Zatvori PIP"</string> + <string name="pip_close" msgid="2955969519031223530">"Zatvori"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Cijeli zaslon"</string> - <string name="pip_move" msgid="1544227837964635439">"Premjesti PIP"</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_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.xml b/libs/WindowManager/Shell/res/values-hu/strings.xml index 01671c9ba1d5..08b35bfc4a59 100644 --- a/libs/WindowManager/Shell/res/values-hu/strings.xml +++ b/libs/WindowManager/Shell/res/values-hu/strings.xml @@ -22,6 +22,7 @@ <string name="pip_phone_settings" msgid="5468987116750491918">"Beállítások"</string> <string name="pip_phone_enter_split" msgid="7042877263880641911">"Váltás osztott képernyőre"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Menü"</string> + <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Kép a képben menü"</string> <string name="pip_notification_title" msgid="1347104727641353453">"A(z) <xliff:g id="NAME">%s</xliff:g> kép a képben funkciót használ"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Ha nem szeretné, hogy a(z) <xliff:g id="NAME">%s</xliff:g> használja ezt a funkciót, koppintson a beállítások megnyitásához, és kapcsolja ki."</string> <string name="pip_play" msgid="3496151081459417097">"Lejátszás"</string> @@ -36,6 +37,7 @@ <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Előfordulhat, hogy az alkalmazás nem működik másodlagos kijelzőn."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Az alkalmazást nem lehet másodlagos kijelzőn elindítani."</string> <string name="accessibility_divider" msgid="703810061635792791">"Elválasztó az osztott nézetben"</string> + <string name="divider_title" msgid="5482989479865361192">"Elválasztó az osztott nézetben"</string> <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Bal oldali teljes képernyőre"</string> <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Bal oldali 70%-ra"</string> <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Bal oldali 50%-ra"</string> @@ -72,17 +74,19 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Buborék"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Kezelés"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Buborék elvetve."</string> - <string name="restart_button_description" msgid="5887656107651190519">"Koppintson az alkalmazás újraindításához és a teljes képernyős mód elindításához."</string> + <string name="restart_button_description" msgid="6712141648865547958">"A jobb nézet érdekében koppintson az alkalmazás újraindításához."</string> <string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"Kamerával kapcsolatos problémába ütközött?\nKoppintson a megoldáshoz."</string> <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Nem sikerült a hiba kijavítása?\nKoppintson a visszaállításhoz."</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Nincsenek problémái kamerával? Koppintson az elvetéshez."</string> - <!-- no translation found for letterbox_education_dialog_title (6688664582871779215) --> + <!-- no translation found for letterbox_education_dialog_title (7739895354143295358) --> <skip /> - <!-- no translation found for letterbox_education_dialog_subtext (4853542518367719562) --> + <!-- no translation found for letterbox_education_split_screen_text (6206339484068670830) --> <skip /> - <!-- no translation found for letterbox_education_screen_rotation_text (5085786687366339027) --> - <skip /> - <!-- no translation found for letterbox_education_reposition_text (1068293354123934727) --> + <!-- no translation found for letterbox_education_reposition_text (4589957299813220661) --> <skip /> <string name="letterbox_education_got_it" msgid="4057634570866051177">"Értem"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Kibontással további információkhoz juthat."</string> + <string name="maximize_button_text" msgid="1650859196290301963">"Teljes méret"</string> + <string name="minimize_button_text" msgid="271592547935841753">"Kis méret"</string> + <string name="close_button_text" msgid="2913281996024033299">"Bezárás"</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 484db0cac067..5e065c2ad4e7 100644 --- a/libs/WindowManager/Shell/res/values-hu/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-hu/strings_tv.xml @@ -19,7 +19,16 @@ 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="9135220303720555525">"PIP bezárása"</string> + <string name="pip_close" msgid="2955969519031223530">"Bezárás"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Teljes képernyő"</string> - <string name="pip_move" msgid="1544227837964635439">"PIP áthelyezése"</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_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.xml b/libs/WindowManager/Shell/res/values-hy/strings.xml index 459cd0a6403c..9d81e8cb544e 100644 --- a/libs/WindowManager/Shell/res/values-hy/strings.xml +++ b/libs/WindowManager/Shell/res/values-hy/strings.xml @@ -22,6 +22,7 @@ <string name="pip_phone_settings" msgid="5468987116750491918">"Կարգավորումներ"</string> <string name="pip_phone_enter_split" msgid="7042877263880641911">"Մտնել տրոհված էկրանի ռեժիմ"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Ընտրացանկ"</string> + <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"«Նկար նկարի մեջ» ռեժիմի ընտրացանկ"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g>-ը «Նկար նկարի մեջ» ռեժիմում է"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Եթե չեք ցանկանում, որ <xliff:g id="NAME">%s</xliff:g>-ն օգտագործի այս գործառույթը, հպեք՝ կարգավորումները բացելու և այն անջատելու համար։"</string> <string name="pip_play" msgid="3496151081459417097">"Նվագարկել"</string> @@ -36,6 +37,7 @@ <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Հավելվածը կարող է չաշխատել լրացուցիչ էկրանի վրա"</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Հավելվածը չի աջակցում գործարկումը լրացուցիչ էկրանների վրա"</string> <string name="accessibility_divider" msgid="703810061635792791">"Տրոհված էկրանի բաժանիչ"</string> + <string name="divider_title" msgid="5482989479865361192">"Տրոհված էկրանի բաժանիչ"</string> <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Ձախ էկրանը՝ լիաէկրան"</string> <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Ձախ էկրանը՝ 70%"</string> <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Ձախ էկրանը՝ 50%"</string> @@ -72,17 +74,19 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Պղպջակ"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Կառավարել"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Ամպիկը փակվեց։"</string> - <string name="restart_button_description" msgid="5887656107651190519">"Հպեք՝ հավելվածը վերագործարկելու և լիաէկրան ռեժիմին անցնելու համար։"</string> + <string name="restart_button_description" msgid="6712141648865547958">"Հպեք՝ հավելվածը վերագործարկելու և ավելի հարմար տեսք ընտրելու համար։"</string> <string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"Տեսախցիկի հետ կապված խնդիրնե՞ր կան։\nՀպեք՝ վերակարգավորելու համար։"</string> <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Չհաջողվե՞ց շտկել։\nՀպեք՝ փոփոխությունները չեղարկելու համար։"</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Տեսախցիկի հետ կապված խնդիրներ չկա՞ն։ Փակելու համար հպեք։"</string> - <!-- no translation found for letterbox_education_dialog_title (6688664582871779215) --> + <!-- no translation found for letterbox_education_dialog_title (7739895354143295358) --> <skip /> - <!-- no translation found for letterbox_education_dialog_subtext (4853542518367719562) --> + <!-- no translation found for letterbox_education_split_screen_text (6206339484068670830) --> <skip /> - <!-- no translation found for letterbox_education_screen_rotation_text (5085786687366339027) --> - <skip /> - <!-- no translation found for letterbox_education_reposition_text (1068293354123934727) --> + <!-- no translation found for letterbox_education_reposition_text (4589957299813220661) --> <skip /> <string name="letterbox_education_got_it" msgid="4057634570866051177">"Եղավ"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Ծավալեք՝ ավելին իմանալու համար։"</string> + <string name="maximize_button_text" msgid="1650859196290301963">"Ծավալել"</string> + <string name="minimize_button_text" msgid="271592547935841753">"Ծալել"</string> + <string name="close_button_text" msgid="2913281996024033299">"Փակել"</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 e447ffc0d964..7963abf8972b 100644 --- a/libs/WindowManager/Shell/res/values-hy/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-hy/strings_tv.xml @@ -19,7 +19,16 @@ 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="9135220303720555525">"Փակել PIP-ն"</string> + <string name="pip_close" msgid="2955969519031223530">"Փակել"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Լիէկրան"</string> - <string name="pip_move" msgid="1544227837964635439">"Տեղափոխել PIP-ը"</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_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.xml b/libs/WindowManager/Shell/res/values-in/strings.xml index e5b7421bb97a..f74e83f702c9 100644 --- a/libs/WindowManager/Shell/res/values-in/strings.xml +++ b/libs/WindowManager/Shell/res/values-in/strings.xml @@ -22,6 +22,7 @@ <string name="pip_phone_settings" msgid="5468987116750491918">"Setelan"</string> <string name="pip_phone_enter_split" msgid="7042877263880641911">"Masuk ke mode layar terpisah"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Menu"</string> + <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Menu Picture-in-Picture"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> adalah picture-in-picture"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Jika Anda tidak ingin <xliff:g id="NAME">%s</xliff:g> menggunakan fitur ini, ketuk untuk membuka setelan dan menonaktifkannya."</string> <string name="pip_play" msgid="3496151081459417097">"Putar"</string> @@ -36,6 +37,7 @@ <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Aplikasi mungkin tidak berfungsi pada layar sekunder."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Aplikasi tidak mendukung peluncuran pada layar sekunder."</string> <string name="accessibility_divider" msgid="703810061635792791">"Pembagi layar terpisah"</string> + <string name="divider_title" msgid="5482989479865361192">"Pembagi layar terpisah"</string> <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Layar penuh di kiri"</string> <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Kiri 70%"</string> <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Kiri 50%"</string> @@ -72,17 +74,19 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Balon"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Kelola"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Balon ditutup."</string> - <string name="restart_button_description" msgid="5887656107651190519">"Ketuk untuk memulai ulang aplikasi ini dan membuka layar penuh."</string> + <string name="restart_button_description" msgid="6712141648865547958">"Ketuk untuk memulai ulang aplikasi ini agar mendapatkan tampilan yang lebih baik."</string> <string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"Masalah kamera?\nKetuk untuk memperbaiki"</string> <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Tidak dapat diperbaiki?\nKetuk untuk mengembalikan"</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Tidak ada masalah kamera? Ketuk untuk menutup."</string> - <!-- no translation found for letterbox_education_dialog_title (6688664582871779215) --> + <!-- no translation found for letterbox_education_dialog_title (7739895354143295358) --> <skip /> - <!-- no translation found for letterbox_education_dialog_subtext (4853542518367719562) --> + <!-- no translation found for letterbox_education_split_screen_text (6206339484068670830) --> <skip /> - <!-- no translation found for letterbox_education_screen_rotation_text (5085786687366339027) --> - <skip /> - <!-- no translation found for letterbox_education_reposition_text (1068293354123934727) --> + <!-- no translation found for letterbox_education_reposition_text (4589957299813220661) --> <skip /> <string name="letterbox_education_got_it" msgid="4057634570866051177">"Oke"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Luaskan untuk melihat informasi selengkapnya."</string> + <string name="maximize_button_text" msgid="1650859196290301963">"Maksimalkan"</string> + <string name="minimize_button_text" msgid="271592547935841753">"Minimalkan"</string> + <string name="close_button_text" msgid="2913281996024033299">"Tutup"</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 b63170564734..7d37154bb86c 100644 --- a/libs/WindowManager/Shell/res/values-in/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-in/strings_tv.xml @@ -19,7 +19,16 @@ 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="9135220303720555525">"Tutup PIP"</string> + <string name="pip_close" msgid="2955969519031223530">"Tutup"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Layar penuh"</string> - <string name="pip_move" msgid="1544227837964635439">"Pindahkan PIP"</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_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.xml b/libs/WindowManager/Shell/res/values-is/strings.xml index 1bfec2b9840b..71f6ec77ccd2 100644 --- a/libs/WindowManager/Shell/res/values-is/strings.xml +++ b/libs/WindowManager/Shell/res/values-is/strings.xml @@ -22,6 +22,7 @@ <string name="pip_phone_settings" msgid="5468987116750491918">"Stillingar"</string> <string name="pip_phone_enter_split" msgid="7042877263880641911">"Opna skjáskiptingu"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Valmynd"</string> + <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Valmynd fyrir mynd í mynd"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> er með mynd í mynd"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Ef þú vilt ekki að <xliff:g id="NAME">%s</xliff:g> noti þennan eiginleika skaltu ýta til að opna stillingarnar og slökkva á því."</string> <string name="pip_play" msgid="3496151081459417097">"Spila"</string> @@ -36,6 +37,7 @@ <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Hugsanlegt er að forritið virki ekki á öðrum skjá."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Forrit styður ekki opnun á öðrum skjá."</string> <string name="accessibility_divider" msgid="703810061635792791">"Skjáskipting"</string> + <string name="divider_title" msgid="5482989479865361192">"Skjáskipting"</string> <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Vinstri á öllum skjánum"</string> <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Vinstri 70%"</string> <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Vinstri 50%"</string> @@ -72,17 +74,19 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Blaðra"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Stjórna"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Blöðru lokað."</string> - <string name="restart_button_description" msgid="5887656107651190519">"Ýttu til að endurræsa forritið og sýna það á öllum skjánum."</string> + <string name="restart_button_description" msgid="6712141648865547958">"Ýta til að endurræsa forritið og fá betri sýn."</string> <string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"Myndavélavesen?\nÝttu til að breyta stærð"</string> <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Ennþá vesen?\nÝttu til að afturkalla"</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Ekkert myndavélavesen? Ýttu til að hunsa."</string> - <!-- no translation found for letterbox_education_dialog_title (6688664582871779215) --> + <!-- no translation found for letterbox_education_dialog_title (7739895354143295358) --> <skip /> - <!-- no translation found for letterbox_education_dialog_subtext (4853542518367719562) --> + <!-- no translation found for letterbox_education_split_screen_text (6206339484068670830) --> <skip /> - <!-- no translation found for letterbox_education_screen_rotation_text (5085786687366339027) --> - <skip /> - <!-- no translation found for letterbox_education_reposition_text (1068293354123934727) --> + <!-- no translation found for letterbox_education_reposition_text (4589957299813220661) --> <skip /> <string name="letterbox_education_got_it" msgid="4057634570866051177">"Ég skil"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Stækka til að sjá frekari upplýsingar."</string> + <string name="maximize_button_text" msgid="1650859196290301963">"Stækka"</string> + <string name="minimize_button_text" msgid="271592547935841753">"Minnka"</string> + <string name="close_button_text" msgid="2913281996024033299">"Loka"</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 119ecf088c20..1490cb98e034 100644 --- a/libs/WindowManager/Shell/res/values-is/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-is/strings_tv.xml @@ -19,7 +19,16 @@ 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="9135220303720555525">"Loka mynd í mynd"</string> + <string name="pip_close" msgid="2955969519031223530">"Loka"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Allur skjárinn"</string> - <string name="pip_move" msgid="1544227837964635439">"Færa innfellda mynd"</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_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.xml b/libs/WindowManager/Shell/res/values-it/strings.xml index ebdf44b70551..458313567df3 100644 --- a/libs/WindowManager/Shell/res/values-it/strings.xml +++ b/libs/WindowManager/Shell/res/values-it/strings.xml @@ -22,6 +22,7 @@ <string name="pip_phone_settings" msgid="5468987116750491918">"Impostazioni"</string> <string name="pip_phone_enter_split" msgid="7042877263880641911">"Accedi a schermo diviso"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Menu"</string> + <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Menu Picture in picture"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> è in Picture in picture"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Se non desideri che l\'app <xliff:g id="NAME">%s</xliff:g> utilizzi questa funzione, tocca per aprire le impostazioni e disattivarla."</string> <string name="pip_play" msgid="3496151081459417097">"Riproduci"</string> @@ -36,6 +37,7 @@ <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"L\'app potrebbe non funzionare su un display secondario."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"L\'app non supporta l\'avvio su display secondari."</string> <string name="accessibility_divider" msgid="703810061635792791">"Strumento per schermo diviso"</string> + <string name="divider_title" msgid="5482989479865361192">"Strumento per schermo diviso"</string> <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Schermata sinistra a schermo intero"</string> <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Schermata sinistra al 70%"</string> <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Schermata sinistra al 50%"</string> @@ -46,10 +48,10 @@ <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Schermata superiore al 50%"</string> <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Schermata superiore al 30%"</string> <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Schermata inferiore a schermo intero"</string> - <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Usare la modalità one-hand"</string> + <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Usare la modalità a una mano"</string> <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Per uscire, scorri verso l\'alto dalla parte inferiore dello schermo oppure tocca un punto qualsiasi sopra l\'app"</string> - <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Avvia la modalità one-hand"</string> - <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"Esci dalla modalità one-hand"</string> + <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Avvia la modalità a una mano"</string> + <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"Esci dalla modalità a una mano"</string> <string name="bubbles_settings_button_description" msgid="1301286017420516912">"Impostazioni per bolle <xliff:g id="APP_NAME">%1$s</xliff:g>"</string> <string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"Altre"</string> <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"Aggiungi di nuovo all\'elenco"</string> @@ -72,17 +74,19 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Fumetto"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Gestisci"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Fumetto ignorato."</string> - <string name="restart_button_description" msgid="5887656107651190519">"Tocca per riavviare l\'app e passare alla modalità a schermo intero."</string> + <string name="restart_button_description" msgid="6712141648865547958">"Tocca per riavviare quest\'app per una migliore visualizzazione."</string> <string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"Problemi con la fotocamera?\nTocca per risolverli"</string> <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Il problema non si è risolto?\nTocca per ripristinare"</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Nessun problema con la fotocamera? Tocca per ignorare."</string> - <!-- no translation found for letterbox_education_dialog_title (6688664582871779215) --> + <!-- no translation found for letterbox_education_dialog_title (7739895354143295358) --> <skip /> - <!-- no translation found for letterbox_education_dialog_subtext (4853542518367719562) --> + <!-- no translation found for letterbox_education_split_screen_text (6206339484068670830) --> <skip /> - <!-- no translation found for letterbox_education_screen_rotation_text (5085786687366339027) --> - <skip /> - <!-- no translation found for letterbox_education_reposition_text (1068293354123934727) --> + <!-- no translation found for letterbox_education_reposition_text (4589957299813220661) --> <skip /> <string name="letterbox_education_got_it" msgid="4057634570866051177">"OK"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Espandi per avere ulteriori informazioni."</string> + <string name="maximize_button_text" msgid="1650859196290301963">"Ingrandisci"</string> + <string name="minimize_button_text" msgid="271592547935841753">"Riduci a icona"</string> + <string name="close_button_text" msgid="2913281996024033299">"Chiudi"</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 92f015c387a0..a48516f2588e 100644 --- a/libs/WindowManager/Shell/res/values-it/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-it/strings_tv.xml @@ -19,7 +19,16 @@ 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="9135220303720555525">"Chiudi PIP"</string> + <string name="pip_close" msgid="2955969519031223530">"Chiudi"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Schermo intero"</string> - <string name="pip_move" msgid="1544227837964635439">"Sposta PIP"</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_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.xml b/libs/WindowManager/Shell/res/values-iw/strings.xml index 3a0f72b1597c..bd52cd827e83 100644 --- a/libs/WindowManager/Shell/res/values-iw/strings.xml +++ b/libs/WindowManager/Shell/res/values-iw/strings.xml @@ -22,6 +22,7 @@ <string name="pip_phone_settings" msgid="5468987116750491918">"הגדרות"</string> <string name="pip_phone_enter_split" msgid="7042877263880641911">"כניסה למסך המפוצל"</string> <string name="pip_menu_title" msgid="5393619322111827096">"תפריט"</string> + <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"תפריט \'תמונה בתוך תמונה\'"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> במצב תמונה בתוך תמונה"</string> <string name="pip_notification_message" msgid="8854051911700302620">"אם אינך רוצה שהתכונה הזו תשמש את <xliff:g id="NAME">%s</xliff:g>, יש להקיש כדי לפתוח את ההגדרות ולהשבית את התכונה."</string> <string name="pip_play" msgid="3496151081459417097">"הפעלה"</string> @@ -36,6 +37,7 @@ <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"ייתכן שהאפליקציה לא תפעל במסך משני."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"האפליקציה אינה תומכת בהפעלה במסכים משניים."</string> <string name="accessibility_divider" msgid="703810061635792791">"מחלק מסך מפוצל"</string> + <string name="divider_title" msgid="5482989479865361192">"מחלק מסך מפוצל"</string> <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"מסך שמאלי מלא"</string> <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"שמאלה 70%"</string> <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"שמאלה 50%"</string> @@ -72,17 +74,19 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"בועה"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"ניהול"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"הבועה נסגרה."</string> - <string name="restart_button_description" msgid="5887656107651190519">"צריך להקיש כדי להפעיל מחדש את האפליקציה הזו ולעבור למסך מלא."</string> + <string name="restart_button_description" msgid="6712141648865547958">"כדי לראות טוב יותר יש להקיש ולהפעיל את האפליקציה הזו מחדש."</string> <string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"בעיות במצלמה?\nאפשר להקיש כדי לבצע התאמה מחדש"</string> <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"הבעיה לא נפתרה?\nאפשר להקיש כדי לחזור לגרסה הקודמת"</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"אין בעיות במצלמה? אפשר להקיש כדי לסגור."</string> - <!-- no translation found for letterbox_education_dialog_title (6688664582871779215) --> + <!-- no translation found for letterbox_education_dialog_title (7739895354143295358) --> <skip /> - <!-- no translation found for letterbox_education_dialog_subtext (4853542518367719562) --> + <!-- no translation found for letterbox_education_split_screen_text (6206339484068670830) --> <skip /> - <!-- no translation found for letterbox_education_screen_rotation_text (5085786687366339027) --> - <skip /> - <!-- no translation found for letterbox_education_reposition_text (1068293354123934727) --> + <!-- no translation found for letterbox_education_reposition_text (4589957299813220661) --> <skip /> <string name="letterbox_education_got_it" msgid="4057634570866051177">"הבנתי"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"מרחיבים כדי לקבל מידע נוסף."</string> + <string name="maximize_button_text" msgid="1650859196290301963">"הגדלה"</string> + <string name="minimize_button_text" msgid="271592547935841753">"מזעור"</string> + <string name="close_button_text" msgid="2913281996024033299">"סגירה"</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 d09b850d01d8..2af1896d3c67 100644 --- a/libs/WindowManager/Shell/res/values-iw/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-iw/strings_tv.xml @@ -19,7 +19,16 @@ 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="9135220303720555525">"סגירת PIP"</string> + <string name="pip_close" msgid="2955969519031223530">"סגירה"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"מסך מלא"</string> - <string name="pip_move" msgid="1544227837964635439">"העברת תמונה בתוך תמונה (PIP)"</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_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.xml b/libs/WindowManager/Shell/res/values-ja/strings.xml index 7b3ad248362d..98b3ba61dd7f 100644 --- a/libs/WindowManager/Shell/res/values-ja/strings.xml +++ b/libs/WindowManager/Shell/res/values-ja/strings.xml @@ -22,6 +22,7 @@ <string name="pip_phone_settings" msgid="5468987116750491918">"設定"</string> <string name="pip_phone_enter_split" msgid="7042877263880641911">"分割画面に切り替え"</string> <string name="pip_menu_title" msgid="5393619322111827096">"メニュー"</string> + <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"ピクチャー イン ピクチャーのメニュー"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g>はピクチャー イン ピクチャーで表示中です"</string> <string name="pip_notification_message" msgid="8854051911700302620">"<xliff:g id="NAME">%s</xliff:g>でこの機能を使用しない場合は、タップして設定を開いて OFF にしてください。"</string> <string name="pip_play" msgid="3496151081459417097">"再生"</string> @@ -36,6 +37,7 @@ <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"アプリはセカンダリ ディスプレイでは動作しないことがあります。"</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"アプリはセカンダリ ディスプレイでの起動に対応していません。"</string> <string name="accessibility_divider" msgid="703810061635792791">"分割画面の分割線"</string> + <string name="divider_title" msgid="5482989479865361192">"分割画面の分割線"</string> <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"左全画面"</string> <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"左 70%"</string> <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"左 50%"</string> @@ -72,17 +74,19 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"バブル"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"管理"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"ふきだしが非表示になっています。"</string> - <string name="restart_button_description" msgid="5887656107651190519">"タップしてこのアプリを再起動すると、全画面表示になります。"</string> + <string name="restart_button_description" msgid="6712141648865547958">"タップしてこのアプリを再起動すると、表示が適切になります。"</string> <string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"カメラに関する問題の場合は、\nタップすると修正できます"</string> <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"修正されなかった場合は、\nタップすると元に戻ります"</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"カメラに関する問題でない場合は、タップすると閉じます。"</string> - <!-- no translation found for letterbox_education_dialog_title (6688664582871779215) --> + <!-- no translation found for letterbox_education_dialog_title (7739895354143295358) --> <skip /> - <!-- no translation found for letterbox_education_dialog_subtext (4853542518367719562) --> + <!-- no translation found for letterbox_education_split_screen_text (6206339484068670830) --> <skip /> - <!-- no translation found for letterbox_education_screen_rotation_text (5085786687366339027) --> - <skip /> - <!-- no translation found for letterbox_education_reposition_text (1068293354123934727) --> + <!-- no translation found for letterbox_education_reposition_text (4589957299813220661) --> <skip /> <string name="letterbox_education_got_it" msgid="4057634570866051177">"OK"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"開くと詳細が表示されます。"</string> + <string name="maximize_button_text" msgid="1650859196290301963">"最大化"</string> + <string name="minimize_button_text" msgid="271592547935841753">"最小化"</string> + <string name="close_button_text" msgid="2913281996024033299">"閉じる"</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 d6399e537894..bc7dcb7aa029 100644 --- a/libs/WindowManager/Shell/res/values-ja/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-ja/strings_tv.xml @@ -19,7 +19,16 @@ 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="9135220303720555525">"PIP を閉じる"</string> + <string name="pip_close" msgid="2955969519031223530">"閉じる"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"全画面表示"</string> - <string name="pip_move" msgid="1544227837964635439">"PIP を移動"</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_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.xml b/libs/WindowManager/Shell/res/values-ka/strings.xml index 07ee0f9910f7..fbf0b5ec41c4 100644 --- a/libs/WindowManager/Shell/res/values-ka/strings.xml +++ b/libs/WindowManager/Shell/res/values-ka/strings.xml @@ -22,6 +22,7 @@ <string name="pip_phone_settings" msgid="5468987116750491918">"პარამეტრები"</string> <string name="pip_phone_enter_split" msgid="7042877263880641911">"გაყოფილ ეკრანში შესვლა"</string> <string name="pip_menu_title" msgid="5393619322111827096">"მენიუ"</string> + <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"„ეკრანი ეკრანში“ რეჟიმის მენიუ"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> იყენებს რეჟიმს „ეკრანი ეკრანში“"</string> <string name="pip_notification_message" msgid="8854051911700302620">"თუ არ გსურთ, რომ <xliff:g id="NAME">%s</xliff:g> ამ ფუნქციას იყენებდეს, აქ შეხებით შეგიძლიათ გახსნათ პარამეტრები და გამორთოთ ის."</string> <string name="pip_play" msgid="3496151081459417097">"დაკვრა"</string> @@ -36,6 +37,7 @@ <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"აპმა შეიძლება არ იმუშაოს მეორეულ ეკრანზე."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"აპს არ გააჩნია მეორეული ეკრანის მხარდაჭერა."</string> <string name="accessibility_divider" msgid="703810061635792791">"გაყოფილი ეკრანის რეჟიმის გამყოფი"</string> + <string name="divider_title" msgid="5482989479865361192">"ეკრანის გაყოფის გამყოფი"</string> <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"მარცხენა ნაწილის სრულ ეკრანზე გაშლა"</string> <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"მარცხენა ეკრანი — 70%"</string> <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"მარცხენა ეკრანი — 50%"</string> @@ -72,17 +74,19 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"ბუშტი"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"მართვა"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"ბუშტი დაიხურა."</string> - <string name="restart_button_description" msgid="5887656107651190519">"შეეხეთ ამ აპის გადასატვირთად და გადადით სრულ ეკრანზე."</string> + <string name="restart_button_description" msgid="6712141648865547958">"შეეხეთ, რომ გადატვირთოთ ეს აპი უკეთესი ხედისთვის."</string> <string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"კამერად პრობლემები აქვს?\nშეეხეთ გამოსასწორებლად"</string> <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"არ გამოსწორდა?\nშეეხეთ წინა ვერსიის დასაბრუნებლად"</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"კამერას პრობლემები არ აქვს? შეეხეთ უარყოფისთვის."</string> - <!-- no translation found for letterbox_education_dialog_title (6688664582871779215) --> + <!-- no translation found for letterbox_education_dialog_title (7739895354143295358) --> <skip /> - <!-- no translation found for letterbox_education_dialog_subtext (4853542518367719562) --> + <!-- no translation found for letterbox_education_split_screen_text (6206339484068670830) --> <skip /> - <!-- no translation found for letterbox_education_screen_rotation_text (5085786687366339027) --> - <skip /> - <!-- no translation found for letterbox_education_reposition_text (1068293354123934727) --> + <!-- no translation found for letterbox_education_reposition_text (4589957299813220661) --> <skip /> <string name="letterbox_education_got_it" msgid="4057634570866051177">"გასაგებია"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"დამატებითი ინფორმაციისთვის გააფართოეთ."</string> + <string name="maximize_button_text" msgid="1650859196290301963">"მაქსიმალურად გაშლა"</string> + <string name="minimize_button_text" msgid="271592547935841753">"ჩაკეცვა"</string> + <string name="close_button_text" msgid="2913281996024033299">"დახურვა"</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 8d7bee8f1398..898dac2aca88 100644 --- a/libs/WindowManager/Shell/res/values-ka/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-ka/strings_tv.xml @@ -19,7 +19,16 @@ 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="9135220303720555525">"PIP-ის დახურვა"</string> + <string name="pip_close" msgid="2955969519031223530">"დახურვა"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"სრულ ეკრანზე"</string> - <string name="pip_move" msgid="1544227837964635439">"PIP გადატანა"</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_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.xml b/libs/WindowManager/Shell/res/values-kk/strings.xml index bdaa03eb5943..a75dc3c26a20 100644 --- a/libs/WindowManager/Shell/res/values-kk/strings.xml +++ b/libs/WindowManager/Shell/res/values-kk/strings.xml @@ -22,6 +22,7 @@ <string name="pip_phone_settings" msgid="5468987116750491918">"Параметрлер"</string> <string name="pip_phone_enter_split" msgid="7042877263880641911">"Бөлінген экранға кіру"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Mәзір"</string> + <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"\"Сурет ішіндегі сурет\" мәзірі"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> \"суреттегі сурет\" режимінде"</string> <string name="pip_notification_message" msgid="8854051911700302620">"<xliff:g id="NAME">%s</xliff:g> деген пайдаланушының бұл мүмкіндікті пайдалануын қаламасаңыз, параметрлерді түртіп ашыңыз да, оларды өшіріңіз."</string> <string name="pip_play" msgid="3496151081459417097">"Ойнату"</string> @@ -36,6 +37,7 @@ <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Қолданба қосымша дисплейде жұмыс істемеуі мүмкін."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Қолданба қосымша дисплейлерде іске қосуды қолдамайды."</string> <string name="accessibility_divider" msgid="703810061635792791">"Бөлінген экран бөлгіші"</string> + <string name="divider_title" msgid="5482989479865361192">"Бөлінген экран бөлгіші"</string> <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Сол жағын толық экранға шығару"</string> <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"70% сол жақта"</string> <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"50% сол жақта"</string> @@ -72,17 +74,19 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Көпіршік"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Басқару"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Қалқыма хабар жабылды."</string> - <string name="restart_button_description" msgid="5887656107651190519">"Бұл қолданбаны қайта қосып, толық экранға өту үшін түртіңіз."</string> + <string name="restart_button_description" msgid="6712141648865547958">"Ыңғайлы көріністі реттеу үшін қолданбаны түртіп, өшіріп қосыңыз."</string> <string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"Камерада қателер шықты ма?\nЖөндеу үшін түртіңіз."</string> <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Жөнделмеді ме?\nҚайтару үшін түртіңіз."</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Камерада қателер шықпады ма? Жабу үшін түртіңіз."</string> - <!-- no translation found for letterbox_education_dialog_title (6688664582871779215) --> + <!-- no translation found for letterbox_education_dialog_title (7739895354143295358) --> <skip /> - <!-- no translation found for letterbox_education_dialog_subtext (4853542518367719562) --> + <!-- no translation found for letterbox_education_split_screen_text (6206339484068670830) --> <skip /> - <!-- no translation found for letterbox_education_screen_rotation_text (5085786687366339027) --> - <skip /> - <!-- no translation found for letterbox_education_reposition_text (1068293354123934727) --> + <!-- no translation found for letterbox_education_reposition_text (4589957299813220661) --> <skip /> <string name="letterbox_education_got_it" msgid="4057634570866051177">"Түсінікті"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Толығырақ ақпарат алу үшін терезені жайыңыз."</string> + <string name="maximize_button_text" msgid="1650859196290301963">"Жаю"</string> + <string name="minimize_button_text" msgid="271592547935841753">"Кішірейту"</string> + <string name="close_button_text" msgid="2913281996024033299">"Жабу"</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 05bdcc71f293..cdf564fb4ca0 100644 --- a/libs/WindowManager/Shell/res/values-kk/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-kk/strings_tv.xml @@ -19,7 +19,16 @@ 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="9135220303720555525">"PIP жабу"</string> + <string name="pip_close" msgid="2955969519031223530">"Жабу"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Толық экран"</string> - <string name="pip_move" msgid="1544227837964635439">"PIP клипін жылжыту"</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_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.xml b/libs/WindowManager/Shell/res/values-km/strings.xml index 2654765c22d9..6913c0663f9b 100644 --- a/libs/WindowManager/Shell/res/values-km/strings.xml +++ b/libs/WindowManager/Shell/res/values-km/strings.xml @@ -22,6 +22,7 @@ <string name="pip_phone_settings" msgid="5468987116750491918">"ការកំណត់"</string> <string name="pip_phone_enter_split" msgid="7042877263880641911">"ចូលមុខងារបំបែកអេក្រង់"</string> <string name="pip_menu_title" msgid="5393619322111827096">"ម៉ឺនុយ"</string> + <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"ម៉ឺនុយរូបក្នុងរូប"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> ស្ថិតក្នុងមុខងាររូបក្នុងរូប"</string> <string name="pip_notification_message" msgid="8854051911700302620">"ប្រសិនបើអ្នកមិនចង់ឲ្យ <xliff:g id="NAME">%s</xliff:g> ប្រើមុខងារនេះ សូមចុចបើកការកំណត់ រួចបិទវា។"</string> <string name="pip_play" msgid="3496151081459417097">"លេង"</string> @@ -36,6 +37,7 @@ <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"កម្មវិធីនេះប្រហែលជាមិនដំណើរការនៅលើអេក្រង់បន្ទាប់បន្សំទេ។"</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"កម្មវិធីនេះមិនអាចចាប់ផ្តើមនៅលើអេក្រង់បន្ទាប់បន្សំបានទេ។"</string> <string name="accessibility_divider" msgid="703810061635792791">"កម្មវិធីចែកអេក្រង់បំបែក"</string> + <string name="divider_title" msgid="5482989479865361192">"បន្ទាត់ខណ្ឌចែកអេក្រង់បំបែក"</string> <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"អេក្រង់ពេញខាងឆ្វេង"</string> <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"ឆ្វេង 70%"</string> <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"ឆ្វេង 50%"</string> @@ -72,17 +74,19 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"ពពុះ"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"គ្រប់គ្រង"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"បានច្រានចោលសារលេចឡើង។"</string> - <string name="restart_button_description" msgid="5887656107651190519">"ចុចដើម្បីចាប់ផ្ដើមកម្មវិធីនេះឡើងវិញ រួចចូលប្រើពេញអេក្រង់។"</string> + <string name="restart_button_description" msgid="6712141648865547958">"ចុចដើម្បីចាប់ផ្ដើមកម្មវិធីនេះឡើងវិញសម្រាប់ទិដ្ឋភាពកាន់តែប្រសើរ។"</string> <string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"មានបញ្ហាពាក់ព័ន្ធនឹងកាមេរ៉ាឬ?\nចុចដើម្បីដោះស្រាយ"</string> <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"មិនបានដោះស្រាយបញ្ហានេះទេឬ?\nចុចដើម្បីត្រឡប់"</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"មិនមានបញ្ហាពាក់ព័ន្ធនឹងកាមេរ៉ាទេឬ? ចុចដើម្បីច្រានចោល។"</string> - <!-- no translation found for letterbox_education_dialog_title (6688664582871779215) --> + <!-- no translation found for letterbox_education_dialog_title (7739895354143295358) --> <skip /> - <!-- no translation found for letterbox_education_dialog_subtext (4853542518367719562) --> + <!-- no translation found for letterbox_education_split_screen_text (6206339484068670830) --> <skip /> - <!-- no translation found for letterbox_education_screen_rotation_text (5085786687366339027) --> - <skip /> - <!-- no translation found for letterbox_education_reposition_text (1068293354123934727) --> + <!-- no translation found for letterbox_education_reposition_text (4589957299813220661) --> <skip /> <string name="letterbox_education_got_it" msgid="4057634570866051177">"យល់ហើយ"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"ពង្រីកដើម្បីទទួលបានព័ត៌មានបន្ថែម។"</string> + <string name="maximize_button_text" msgid="1650859196290301963">"ពង្រីក"</string> + <string name="minimize_button_text" msgid="271592547935841753">"បង្រួម"</string> + <string name="close_button_text" msgid="2913281996024033299">"បិទ"</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 e8315163ad46..1a7ae813c1d3 100644 --- a/libs/WindowManager/Shell/res/values-km/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-km/strings_tv.xml @@ -19,7 +19,16 @@ 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="9135220303720555525">"បិទ PIP"</string> + <string name="pip_close" msgid="2955969519031223530">"បិទ"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"ពេញអេក្រង់"</string> - <string name="pip_move" msgid="1544227837964635439">"ផ្លាស់ទី PIP"</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_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.xml b/libs/WindowManager/Shell/res/values-kn/strings.xml index 6edbf13d4e2e..1f246d5189fb 100644 --- a/libs/WindowManager/Shell/res/values-kn/strings.xml +++ b/libs/WindowManager/Shell/res/values-kn/strings.xml @@ -22,6 +22,7 @@ <string name="pip_phone_settings" msgid="5468987116750491918">"ಸೆಟ್ಟಿಂಗ್ಗಳು"</string> <string name="pip_phone_enter_split" msgid="7042877263880641911">"ಸ್ಪ್ಲಿಟ್-ಸ್ಕ್ರೀನ್ಗೆ ಪ್ರವೇಶಿಸಿ"</string> <string name="pip_menu_title" msgid="5393619322111827096">"ಮೆನು"</string> + <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"ಚಿತ್ರದಲ್ಲಿ ಚಿತ್ರ ಮೆನು"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> ಚಿತ್ರದಲ್ಲಿ ಚಿತ್ರವಾಗಿದೆ"</string> <string name="pip_notification_message" msgid="8854051911700302620">"<xliff:g id="NAME">%s</xliff:g> ಈ ವೈಶಿಷ್ಟ್ಯ ಬಳಸುವುದನ್ನು ನೀವು ಬಯಸದಿದ್ದರೆ, ಸೆಟ್ಟಿಂಗ್ಗಳನ್ನು ತೆರೆಯಲು ಮತ್ತು ಅದನ್ನು ಆಫ್ ಮಾಡಲು ಟ್ಯಾಪ್ ಮಾಡಿ."</string> <string name="pip_play" msgid="3496151081459417097">"ಪ್ಲೇ"</string> @@ -36,6 +37,7 @@ <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"ಸೆಕೆಂಡರಿ ಡಿಸ್ಪ್ಲೇಗಳಲ್ಲಿ ಅಪ್ಲಿಕೇಶನ್ ಕಾರ್ಯ ನಿರ್ವಹಿಸದೇ ಇರಬಹುದು."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"ಸೆಕೆಂಡರಿ ಡಿಸ್ಪ್ಲೇಗಳಲ್ಲಿ ಪ್ರಾರಂಭಿಸುವಿಕೆಯನ್ನು ಅಪ್ಲಿಕೇಶನ್ ಬೆಂಬಲಿಸುವುದಿಲ್ಲ."</string> <string name="accessibility_divider" msgid="703810061635792791">"ಸ್ಪ್ಲಿಟ್-ಪರದೆ ಡಿವೈಡರ್"</string> + <string name="divider_title" msgid="5482989479865361192">"ಸ್ಪ್ಲಿಟ್-ಪರದೆ ಡಿವೈಡರ್"</string> <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"ಎಡ ಪೂರ್ಣ ಪರದೆ"</string> <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"70% ಎಡಕ್ಕೆ"</string> <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"50% ಎಡಕ್ಕೆ"</string> @@ -72,17 +74,19 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"ಬಬಲ್"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"ನಿರ್ವಹಿಸಿ"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"ಬಬಲ್ ವಜಾಗೊಳಿಸಲಾಗಿದೆ."</string> - <string name="restart_button_description" msgid="5887656107651190519">"ಈ ಆ್ಯಪ್ ಅನ್ನು ಮರುಪ್ರಾರಂಭಿಸಲು ಮತ್ತು ಪೂರ್ಣ ಸ್ಕ್ರೀನ್ನಲ್ಲಿ ನೋಡಲು ಟ್ಯಾಪ್ ಮಾಡಿ."</string> + <string name="restart_button_description" msgid="6712141648865547958">"ಉತ್ತಮ ವೀಕ್ಷಣೆಗಾಗಿ ಈ ಆ್ಯಪ್ ಅನ್ನು ಮರುಪ್ರಾರಂಭಿಸಲು ಟ್ಯಾಪ್ ಮಾಡಿ."</string> <string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"ಕ್ಯಾಮರಾ ಸಮಸ್ಯೆಗಳಿವೆಯೇ?\nಮರುಹೊಂದಿಸಲು ಟ್ಯಾಪ್ ಮಾಡಿ"</string> <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"ಅದನ್ನು ಸರಿಪಡಿಸಲಿಲ್ಲವೇ?\nಹಿಂತಿರುಗಿಸಲು ಟ್ಯಾಪ್ ಮಾಡಿ"</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"ಕ್ಯಾಮರಾ ಸಮಸ್ಯೆಗಳಿಲ್ಲವೇ? ವಜಾಗೊಳಿಸಲು ಟ್ಯಾಪ್ ಮಾಡಿ."</string> - <!-- no translation found for letterbox_education_dialog_title (6688664582871779215) --> + <!-- no translation found for letterbox_education_dialog_title (7739895354143295358) --> <skip /> - <!-- no translation found for letterbox_education_dialog_subtext (4853542518367719562) --> + <!-- no translation found for letterbox_education_split_screen_text (6206339484068670830) --> <skip /> - <!-- no translation found for letterbox_education_screen_rotation_text (5085786687366339027) --> - <skip /> - <!-- no translation found for letterbox_education_reposition_text (1068293354123934727) --> + <!-- no translation found for letterbox_education_reposition_text (4589957299813220661) --> <skip /> <string name="letterbox_education_got_it" msgid="4057634570866051177">"ಸರಿ"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"ಇನ್ನಷ್ಟು ಮಾಹಿತಿಗಾಗಿ ವಿಸ್ತೃತಗೊಳಿಸಿ."</string> + <string name="maximize_button_text" msgid="1650859196290301963">"ಹಿಗ್ಗಿಸಿ"</string> + <string name="minimize_button_text" msgid="271592547935841753">"ಕುಗ್ಗಿಸಿ"</string> + <string name="close_button_text" msgid="2913281996024033299">"ಮುಚ್ಚಿರಿ"</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 305ef668cb58..45de068c80a0 100644 --- a/libs/WindowManager/Shell/res/values-kn/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-kn/strings_tv.xml @@ -19,7 +19,16 @@ 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="9135220303720555525">"PIP ಮುಚ್ಚಿ"</string> + <string name="pip_close" msgid="2955969519031223530">"ಮುಚ್ಚಿರಿ"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"ಪೂರ್ಣ ಪರದೆ"</string> - <string name="pip_move" msgid="1544227837964635439">"PIP ಅನ್ನು ಸರಿಸಿ"</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_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.xml b/libs/WindowManager/Shell/res/values-ko/strings.xml index 1f8d0b0b175d..e878cef5e75a 100644 --- a/libs/WindowManager/Shell/res/values-ko/strings.xml +++ b/libs/WindowManager/Shell/res/values-ko/strings.xml @@ -22,6 +22,7 @@ <string name="pip_phone_settings" msgid="5468987116750491918">"설정"</string> <string name="pip_phone_enter_split" msgid="7042877263880641911">"화면 분할 모드로 전환"</string> <string name="pip_menu_title" msgid="5393619322111827096">"메뉴"</string> + <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"PIP 모드 메뉴"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g>에서 PIP 사용 중"</string> <string name="pip_notification_message" msgid="8854051911700302620">"<xliff:g id="NAME">%s</xliff:g>에서 이 기능이 사용되는 것을 원하지 않는 경우 탭하여 설정을 열고 기능을 사용 중지하세요."</string> <string name="pip_play" msgid="3496151081459417097">"재생"</string> @@ -36,6 +37,7 @@ <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"앱이 보조 디스플레이에서 작동하지 않을 수도 있습니다."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"앱이 보조 디스플레이에서의 실행을 지원하지 않습니다."</string> <string name="accessibility_divider" msgid="703810061635792791">"화면 분할기"</string> + <string name="divider_title" msgid="5482989479865361192">"화면 분할기"</string> <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"왼쪽 화면 전체화면"</string> <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"왼쪽 화면 70%"</string> <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"왼쪽 화면 50%"</string> @@ -72,17 +74,19 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"버블"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"관리"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"대화창을 닫았습니다."</string> - <string name="restart_button_description" msgid="5887656107651190519">"탭하여 이 앱을 다시 시작하고 전체 화면으로 이동합니다."</string> + <string name="restart_button_description" msgid="6712141648865547958">"보기를 개선하려면 탭하여 앱을 다시 시작합니다."</string> <string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"카메라 문제가 있나요?\n해결하려면 탭하세요."</string> <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"해결되지 않았나요?\n되돌리려면 탭하세요."</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"카메라에 문제가 없나요? 닫으려면 탭하세요."</string> - <!-- no translation found for letterbox_education_dialog_title (6688664582871779215) --> + <!-- no translation found for letterbox_education_dialog_title (7739895354143295358) --> <skip /> - <!-- no translation found for letterbox_education_dialog_subtext (4853542518367719562) --> + <!-- no translation found for letterbox_education_split_screen_text (6206339484068670830) --> <skip /> - <!-- no translation found for letterbox_education_screen_rotation_text (5085786687366339027) --> - <skip /> - <!-- no translation found for letterbox_education_reposition_text (1068293354123934727) --> + <!-- no translation found for letterbox_education_reposition_text (4589957299813220661) --> <skip /> <string name="letterbox_education_got_it" msgid="4057634570866051177">"확인"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"추가 정보는 펼쳐서 확인하세요."</string> + <string name="maximize_button_text" msgid="1650859196290301963">"최대화"</string> + <string name="minimize_button_text" msgid="271592547935841753">"최소화"</string> + <string name="close_button_text" msgid="2913281996024033299">"닫기"</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 76b0adfb3d88..9e8f1f1258a5 100644 --- a/libs/WindowManager/Shell/res/values-ko/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-ko/strings_tv.xml @@ -19,7 +19,16 @@ 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="9135220303720555525">"PIP 닫기"</string> + <string name="pip_close" msgid="2955969519031223530">"닫기"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"전체화면"</string> - <string name="pip_move" msgid="1544227837964635439">"PIP 이동"</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_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.xml b/libs/WindowManager/Shell/res/values-ky/strings.xml index 81eb2d70c2de..20c462e6ad05 100644 --- a/libs/WindowManager/Shell/res/values-ky/strings.xml +++ b/libs/WindowManager/Shell/res/values-ky/strings.xml @@ -22,6 +22,7 @@ <string name="pip_phone_settings" msgid="5468987116750491918">"Жөндөөлөр"</string> <string name="pip_phone_enter_split" msgid="7042877263880641911">"Экранды бөлүү режимине өтүү"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Меню"</string> + <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Сүрөт ичиндеги сүрөт менюсу"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> – сүрөт ичиндеги сүрөт"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Эгер <xliff:g id="NAME">%s</xliff:g> колдонмосу бул функцияны пайдаланбасын десеңиз, жөндөөлөрдү ачып туруп, аны өчүрүп коюңуз."</string> <string name="pip_play" msgid="3496151081459417097">"Ойнотуу"</string> @@ -36,6 +37,7 @@ <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Колдонмо кошумча экранда иштебей коюшу мүмкүн."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Колдонмону кошумча экрандарда иштетүүгө болбойт."</string> <string name="accessibility_divider" msgid="703810061635792791">"Экранды бөлгүч"</string> + <string name="divider_title" msgid="5482989479865361192">"Экранды бөлгүч"</string> <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Сол жактагы экранды толук экран режимине өткөрүү"</string> <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Сол жактагы экранды 70%"</string> <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Сол жактагы экранды 50%"</string> @@ -72,17 +74,19 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Көбүк"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Башкаруу"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Калкып чыкма билдирме жабылды."</string> - <string name="restart_button_description" msgid="5887656107651190519">"Бул колдонмону өчүрүп күйгүзүп, толук экранга өтүү үчүн таптап коюңуз."</string> + <string name="restart_button_description" msgid="6712141648865547958">"Жакшыраак көрүү үчүн бул колдонмону өчүрүп күйгүзүңүз. Ал үчүн таптап коюңуз."</string> <string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"Камерада маселелер келип чыктыбы?\nОңдоо үчүн таптаңыз"</string> <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Оңдолгон жокпу?\nАртка кайтаруу үчүн таптаңыз"</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Камерада маселе жокпу? Этибарга албоо үчүн таптаңыз."</string> - <!-- no translation found for letterbox_education_dialog_title (6688664582871779215) --> + <!-- no translation found for letterbox_education_dialog_title (7739895354143295358) --> <skip /> - <!-- no translation found for letterbox_education_dialog_subtext (4853542518367719562) --> + <!-- no translation found for letterbox_education_split_screen_text (6206339484068670830) --> <skip /> - <!-- no translation found for letterbox_education_screen_rotation_text (5085786687366339027) --> - <skip /> - <!-- no translation found for letterbox_education_reposition_text (1068293354123934727) --> + <!-- no translation found for letterbox_education_reposition_text (4589957299813220661) --> <skip /> <string name="letterbox_education_got_it" msgid="4057634570866051177">"Түшүндүм"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Толук маалымат алуу үчүн жайып көрүңүз."</string> + <string name="maximize_button_text" msgid="1650859196290301963">"Чоңойтуу"</string> + <string name="minimize_button_text" msgid="271592547935841753">"Кичирейтүү"</string> + <string name="close_button_text" msgid="2913281996024033299">"Жабуу"</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 57b955a7c5d4..19fac5876bb0 100644 --- a/libs/WindowManager/Shell/res/values-ky/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-ky/strings_tv.xml @@ -19,7 +19,16 @@ 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="9135220303720555525">"PIP\'ти жабуу"</string> + <string name="pip_close" msgid="2955969519031223530">"Жабуу"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Толук экран"</string> - <string name="pip_move" msgid="1544227837964635439">"PIP\'ти жылдыруу"</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_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.xml b/libs/WindowManager/Shell/res/values-lo/strings.xml index 325213020e5c..3f4a881c042b 100644 --- a/libs/WindowManager/Shell/res/values-lo/strings.xml +++ b/libs/WindowManager/Shell/res/values-lo/strings.xml @@ -22,6 +22,7 @@ <string name="pip_phone_settings" msgid="5468987116750491918">"ການຕັ້ງຄ່າ"</string> <string name="pip_phone_enter_split" msgid="7042877263880641911">"ເຂົ້າການແບ່ງໜ້າຈໍ"</string> <string name="pip_menu_title" msgid="5393619322111827096">"ເມນູ"</string> + <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"ເມນູການສະແດງຜົນຊ້ອນກັນ"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> ແມ່ນເປັນການສະແດງຜົນຫຼາຍຢ່າງພ້ອມກັນ"</string> <string name="pip_notification_message" msgid="8854051911700302620">"ຫາກທ່ານບໍ່ຕ້ອງການ <xliff:g id="NAME">%s</xliff:g> ໃຫ້ໃຊ້ຄຸນສົມບັດນີ້, ໃຫ້ແຕະເພື່ອເປີດການຕັ້ງຄ່າ ແລ້ວປິດມັນໄວ້."</string> <string name="pip_play" msgid="3496151081459417097">"ຫຼິ້ນ"</string> @@ -36,6 +37,7 @@ <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"ແອັບອາດບໍ່ສາມາດໃຊ້ໄດ້ໃນໜ້າຈໍທີສອງ."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"ແອັບບໍ່ຮອງຮັບການເປີດໃນໜ້າຈໍທີສອງ."</string> <string name="accessibility_divider" msgid="703810061635792791">"ຕົວຂັ້ນການແບ່ງໜ້າຈໍ"</string> + <string name="divider_title" msgid="5482989479865361192">"ຕົວຂັ້ນການແບ່ງໜ້າຈໍ"</string> <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"ເຕັມໜ້າຈໍຊ້າຍ"</string> <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"ຊ້າຍ 70%"</string> <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"ຊ້າຍ 50%"</string> @@ -72,17 +74,19 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"ຟອງ"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"ຈັດການ"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"ປິດ Bubble ໄສ້ແລ້ວ."</string> - <string name="restart_button_description" msgid="5887656107651190519">"ແຕະເພື່ອຣີສະຕາດແອັບນີ້ ແລະ ໃຊ້ແບບເຕັມຈໍ."</string> + <string name="restart_button_description" msgid="6712141648865547958">"ແຕະເພື່ອຣີສະຕາດແອັບນີ້ເພື່ອມຸມມອງທີ່ດີຂຶ້ນ."</string> <string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"ມີບັນຫາກ້ອງຖ່າຍຮູບບໍ?\nແຕະເພື່ອປັບໃໝ່"</string> <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"ບໍ່ໄດ້ແກ້ໄຂມັນບໍ?\nແຕະເພື່ອແປງກັບຄືນ"</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"ບໍ່ມີບັນຫາກ້ອງຖ່າຍຮູບບໍ? ແຕະເພື່ອປິດໄວ້."</string> - <!-- no translation found for letterbox_education_dialog_title (6688664582871779215) --> + <!-- no translation found for letterbox_education_dialog_title (7739895354143295358) --> <skip /> - <!-- no translation found for letterbox_education_dialog_subtext (4853542518367719562) --> + <!-- no translation found for letterbox_education_split_screen_text (6206339484068670830) --> <skip /> - <!-- no translation found for letterbox_education_screen_rotation_text (5085786687366339027) --> - <skip /> - <!-- no translation found for letterbox_education_reposition_text (1068293354123934727) --> + <!-- no translation found for letterbox_education_reposition_text (4589957299813220661) --> <skip /> <string name="letterbox_education_got_it" msgid="4057634570866051177">"ເຂົ້າໃຈແລ້ວ"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"ຂະຫຍາຍເພື່ອເບິ່ງຂໍ້ມູນເພີ່ມເຕີມ."</string> + <string name="maximize_button_text" msgid="1650859196290301963">"ຂະຫຍາຍໃຫຍ່ສຸດ"</string> + <string name="minimize_button_text" msgid="271592547935841753">"ຫຍໍ້ລົງ"</string> + <string name="close_button_text" msgid="2913281996024033299">"ປິດ"</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 cbea84ea7ea2..6cd0f37c516c 100644 --- a/libs/WindowManager/Shell/res/values-lo/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-lo/strings_tv.xml @@ -19,7 +19,16 @@ 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="9135220303720555525">"ປິດ PIP"</string> + <string name="pip_close" msgid="2955969519031223530">"ປິດ"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"ເຕັມໜ້າຈໍ"</string> - <string name="pip_move" msgid="1544227837964635439">"ຍ້າຍ PIP"</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_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.xml b/libs/WindowManager/Shell/res/values-lt/strings.xml index 70654c767287..515a263a099f 100644 --- a/libs/WindowManager/Shell/res/values-lt/strings.xml +++ b/libs/WindowManager/Shell/res/values-lt/strings.xml @@ -22,6 +22,7 @@ <string name="pip_phone_settings" msgid="5468987116750491918">"Nustatymai"</string> <string name="pip_phone_enter_split" msgid="7042877263880641911">"Įjungti išskaidyto ekrano režimą"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Meniu"</string> + <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Vaizdo vaizde meniu"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> rodom. vaizdo vaizde"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Jei nenorite, kad „<xliff:g id="NAME">%s</xliff:g>“ naudotų šią funkciją, palietę atidarykite nustatymus ir išjunkite ją."</string> <string name="pip_play" msgid="3496151081459417097">"Leisti"</string> @@ -36,6 +37,7 @@ <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Programa gali neveikti antriniame ekrane."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Programa nepalaiko paleisties antriniuose ekranuose."</string> <string name="accessibility_divider" msgid="703810061635792791">"Skaidyto ekrano daliklis"</string> + <string name="divider_title" msgid="5482989479865361192">"Skaidyto ekrano daliklis"</string> <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Kairysis ekranas viso ekrano režimu"</string> <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Kairysis ekranas 70 %"</string> <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Kairysis ekranas 50 %"</string> @@ -72,17 +74,19 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Debesėlis"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Tvarkyti"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Debesėlio atsisakyta."</string> - <string name="restart_button_description" msgid="5887656107651190519">"Palieskite, kad paleistumėte iš naujo šią programą ir įjungtumėte viso ekrano režimą."</string> + <string name="restart_button_description" msgid="6712141648865547958">"Palieskite, kad iš naujo paleistumėte šią programą ir matytumėte aiškiau."</string> <string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"Iškilo problemų dėl kameros?\nPalieskite, kad pritaikytumėte iš naujo"</string> <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Nepavyko pataisyti?\nPalieskite, kad grąžintumėte"</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Nėra jokių problemų dėl kameros? Palieskite, kad atsisakytumėte."</string> - <!-- no translation found for letterbox_education_dialog_title (6688664582871779215) --> + <!-- no translation found for letterbox_education_dialog_title (7739895354143295358) --> <skip /> - <!-- no translation found for letterbox_education_dialog_subtext (4853542518367719562) --> + <!-- no translation found for letterbox_education_split_screen_text (6206339484068670830) --> <skip /> - <!-- no translation found for letterbox_education_screen_rotation_text (5085786687366339027) --> - <skip /> - <!-- no translation found for letterbox_education_reposition_text (1068293354123934727) --> + <!-- no translation found for letterbox_education_reposition_text (4589957299813220661) --> <skip /> <string name="letterbox_education_got_it" msgid="4057634570866051177">"Supratau"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Išskleiskite, jei reikia daugiau informacijos."</string> + <string name="maximize_button_text" msgid="1650859196290301963">"Padidinti"</string> + <string name="minimize_button_text" msgid="271592547935841753">"Sumažinti"</string> + <string name="close_button_text" msgid="2913281996024033299">"Uždaryti"</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 81716a609fc5..52017dca2b94 100644 --- a/libs/WindowManager/Shell/res/values-lt/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-lt/strings_tv.xml @@ -19,7 +19,16 @@ 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="9135220303720555525">"Uždaryti PIP"</string> + <string name="pip_close" msgid="2955969519031223530">"Uždaryti"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Visas ekranas"</string> - <string name="pip_move" msgid="1544227837964635439">"Perkelti PIP"</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_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.xml b/libs/WindowManager/Shell/res/values-lv/strings.xml index 74d1b3f6578c..080c6f4682ff 100644 --- a/libs/WindowManager/Shell/res/values-lv/strings.xml +++ b/libs/WindowManager/Shell/res/values-lv/strings.xml @@ -22,6 +22,7 @@ <string name="pip_phone_settings" msgid="5468987116750491918">"Iestatījumi"</string> <string name="pip_phone_enter_split" msgid="7042877263880641911">"Piekļūt ekrāna sadalīšanas režīmam"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Izvēlne"</string> + <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Izvēlne attēlam attēlā"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> ir attēlā attēlā"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Ja nevēlaties lietotnē <xliff:g id="NAME">%s</xliff:g> izmantot šo funkciju, pieskarieties, lai atvērtu iestatījumus un izslēgtu funkciju."</string> <string name="pip_play" msgid="3496151081459417097">"Atskaņot"</string> @@ -36,6 +37,7 @@ <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Lietotne, iespējams, nedarbosies sekundārajā displejā."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Lietotnē netiek atbalstīta palaišana sekundārajos displejos."</string> <string name="accessibility_divider" msgid="703810061635792791">"Ekrāna sadalītājs"</string> + <string name="divider_title" msgid="5482989479865361192">"Ekrāna sadalītājs"</string> <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Kreisā daļa pa visu ekrānu"</string> <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Pa kreisi 70%"</string> <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Pa kreisi 50%"</string> @@ -72,17 +74,19 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Burbulis"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Pārvaldīt"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Burbulis ir noraidīts."</string> - <string name="restart_button_description" msgid="5887656107651190519">"Pieskarieties, lai restartētu šo lietotni un pārietu pilnekrāna režīmā."</string> + <string name="restart_button_description" msgid="6712141648865547958">"Pieskarieties, lai restartētu šo lietotni un uzlabotu attēlojumu."</string> <string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"Vai ir problēmas ar kameru?\nPieskarieties, lai tās novērstu."</string> <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Vai problēma netika novērsta?\nPieskarieties, lai atjaunotu."</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Vai nav problēmu ar kameru? Pieskarieties, lai nerādītu."</string> - <!-- no translation found for letterbox_education_dialog_title (6688664582871779215) --> + <!-- no translation found for letterbox_education_dialog_title (7739895354143295358) --> <skip /> - <!-- no translation found for letterbox_education_dialog_subtext (4853542518367719562) --> + <!-- no translation found for letterbox_education_split_screen_text (6206339484068670830) --> <skip /> - <!-- no translation found for letterbox_education_screen_rotation_text (5085786687366339027) --> - <skip /> - <!-- no translation found for letterbox_education_reposition_text (1068293354123934727) --> + <!-- no translation found for letterbox_education_reposition_text (4589957299813220661) --> <skip /> <string name="letterbox_education_got_it" msgid="4057634570866051177">"Labi"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Izvērsiet, lai iegūtu plašāku informāciju."</string> + <string name="maximize_button_text" msgid="1650859196290301963">"Maksimizēt"</string> + <string name="minimize_button_text" msgid="271592547935841753">"Minimizēt"</string> + <string name="close_button_text" msgid="2913281996024033299">"Aizvērt"</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 5295cd230591..11abac6f6197 100644 --- a/libs/WindowManager/Shell/res/values-lv/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-lv/strings_tv.xml @@ -19,7 +19,16 @@ 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="9135220303720555525">"Aizvērt PIP"</string> + <string name="pip_close" msgid="2955969519031223530">"Aizvērt"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Pilnekrāna režīms"</string> - <string name="pip_move" msgid="1544227837964635439">"Pārvietot attēlu attēlā"</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_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.xml b/libs/WindowManager/Shell/res/values-mk/strings.xml index be6ed4d25ac1..47ed632fbffe 100644 --- a/libs/WindowManager/Shell/res/values-mk/strings.xml +++ b/libs/WindowManager/Shell/res/values-mk/strings.xml @@ -22,6 +22,7 @@ <string name="pip_phone_settings" msgid="5468987116750491918">"Поставки"</string> <string name="pip_phone_enter_split" msgid="7042877263880641911">"Влези во поделен екран"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Мени"</string> + <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Мени за „Слика во слика“"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> е во слика во слика"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Ако не сакате <xliff:g id="NAME">%s</xliff:g> да ја користи функцијава, допрете за да ги отворите поставките и да ја исклучите."</string> <string name="pip_play" msgid="3496151081459417097">"Пушти"</string> @@ -36,6 +37,7 @@ <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Апликацијата може да не функционира на друг екран."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Апликацијата не поддржува стартување на други екрани."</string> <string name="accessibility_divider" msgid="703810061635792791">"Разделник на поделен екран"</string> + <string name="divider_title" msgid="5482989479865361192">"Разделник на поделен екран"</string> <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Левиот на цел екран"</string> <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Левиот 70%"</string> <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Левиот 50%"</string> @@ -72,17 +74,19 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Балонче"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Управувајте"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Балончето е отфрлено."</string> - <string name="restart_button_description" msgid="5887656107651190519">"Допрете за да ја рестартирате апликацијава и да ја отворите на цел екран."</string> + <string name="restart_button_description" msgid="6712141648865547958">"Допрете за да ја рестартирате апликацијава за подобар приказ."</string> <string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"Проблеми со камерата?\nДопрете за да се совпадне повторно"</string> <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Не се поправи?\nДопрете за враќање"</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Нема проблеми со камерата? Допрете за отфрлање."</string> - <!-- no translation found for letterbox_education_dialog_title (6688664582871779215) --> + <!-- no translation found for letterbox_education_dialog_title (7739895354143295358) --> <skip /> - <!-- no translation found for letterbox_education_dialog_subtext (4853542518367719562) --> + <!-- no translation found for letterbox_education_split_screen_text (6206339484068670830) --> <skip /> - <!-- no translation found for letterbox_education_screen_rotation_text (5085786687366339027) --> - <skip /> - <!-- no translation found for letterbox_education_reposition_text (1068293354123934727) --> + <!-- no translation found for letterbox_education_reposition_text (4589957299813220661) --> <skip /> <string name="letterbox_education_got_it" msgid="4057634570866051177">"Сфатив"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Проширете за повеќе информации."</string> + <string name="maximize_button_text" msgid="1650859196290301963">"Зголеми"</string> + <string name="minimize_button_text" msgid="271592547935841753">"Минимизирај"</string> + <string name="close_button_text" msgid="2913281996024033299">"Затвори"</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 fa48a6cc1846..21293223b882 100644 --- a/libs/WindowManager/Shell/res/values-mk/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-mk/strings_tv.xml @@ -19,7 +19,16 @@ 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="9135220303720555525">"Затвори PIP"</string> + <string name="pip_close" msgid="2955969519031223530">"Затвори"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Цел екран"</string> - <string name="pip_move" msgid="1544227837964635439">"Премести PIP"</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_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.xml b/libs/WindowManager/Shell/res/values-ml/strings.xml index 14a341b49c0e..faa6a30fdf78 100644 --- a/libs/WindowManager/Shell/res/values-ml/strings.xml +++ b/libs/WindowManager/Shell/res/values-ml/strings.xml @@ -22,6 +22,7 @@ <string name="pip_phone_settings" msgid="5468987116750491918">"ക്രമീകരണം"</string> <string name="pip_phone_enter_split" msgid="7042877263880641911">"സ്ക്രീൻ വിഭജന മോഡിൽ പ്രവേശിക്കുക"</string> <string name="pip_menu_title" msgid="5393619322111827096">"മെനു"</string> + <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"ചിത്രത്തിനുള്ളിൽ ചിത്രം മെനു"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> ചിത്രത്തിനുള്ളിൽ ചിത്രം രീതിയിലാണ്"</string> <string name="pip_notification_message" msgid="8854051911700302620">"<xliff:g id="NAME">%s</xliff:g> ഈ ഫീച്ചർ ഉപയോഗിക്കേണ്ടെങ്കിൽ, ടാപ്പ് ചെയ്ത് ക്രമീകരണം തുറന്ന് അത് ഓഫാക്കുക."</string> <string name="pip_play" msgid="3496151081459417097">"പ്ലേ ചെയ്യുക"</string> @@ -36,6 +37,7 @@ <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"രണ്ടാം ഡിസ്പ്ലേയിൽ ആപ്പ് പ്രവർത്തിച്ചേക്കില്ല."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"രണ്ടാം ഡിസ്പ്ലേകളിൽ സമാരംഭിക്കുന്നതിനെ ആപ്പ് അനുവദിക്കുന്നില്ല."</string> <string name="accessibility_divider" msgid="703810061635792791">"സ്പ്ലിറ്റ്-സ്ക്രീൻ ഡിവൈഡർ"</string> + <string name="divider_title" msgid="5482989479865361192">"സ്ക്രീൻ വിഭജന മോഡ് ഡിവൈഡർ"</string> <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"ഇടത് പൂർണ്ണ സ്ക്രീൻ"</string> <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"ഇടത് 70%"</string> <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"ഇടത് 50%"</string> @@ -72,17 +74,19 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"ബബിൾ"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"മാനേജ് ചെയ്യുക"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"ബബിൾ ഡിസ്മിസ് ചെയ്തു."</string> - <string name="restart_button_description" msgid="5887656107651190519">"ഈ ആപ്പ് റീസ്റ്റാർട്ട് ചെയ്ത് പൂർണ്ണ സ്ക്രീനിലേക്ക് മാറാൻ ടാപ്പ് ചെയ്യുക."</string> + <string name="restart_button_description" msgid="6712141648865547958">"മികച്ച കാഴ്ചയ്ക്കായി ഈ ആപ്പ് റീസ്റ്റാർട്ട് ചെയ്യാൻ ടാപ്പ് ചെയ്യുക."</string> <string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"ക്യാമറ പ്രശ്നങ്ങളുണ്ടോ?\nശരിയാക്കാൻ ടാപ്പ് ചെയ്യുക"</string> <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"അത് പരിഹരിച്ചില്ലേ?\nപുനഃസ്ഥാപിക്കാൻ ടാപ്പ് ചെയ്യുക"</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"ക്യാമറാ പ്രശ്നങ്ങളൊന്നുമില്ലേ? നിരസിക്കാൻ ടാപ്പ് ചെയ്യുക."</string> - <!-- no translation found for letterbox_education_dialog_title (6688664582871779215) --> + <!-- no translation found for letterbox_education_dialog_title (7739895354143295358) --> <skip /> - <!-- no translation found for letterbox_education_dialog_subtext (4853542518367719562) --> + <!-- no translation found for letterbox_education_split_screen_text (6206339484068670830) --> <skip /> - <!-- no translation found for letterbox_education_screen_rotation_text (5085786687366339027) --> - <skip /> - <!-- no translation found for letterbox_education_reposition_text (1068293354123934727) --> + <!-- no translation found for letterbox_education_reposition_text (4589957299813220661) --> <skip /> <string name="letterbox_education_got_it" msgid="4057634570866051177">"മനസ്സിലായി"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"കൂടുതൽ വിവരങ്ങൾക്ക് വികസിപ്പിക്കുക."</string> + <string name="maximize_button_text" msgid="1650859196290301963">"വലുതാക്കുക"</string> + <string name="minimize_button_text" msgid="271592547935841753">"ചെറുതാക്കുക"</string> + <string name="close_button_text" msgid="2913281996024033299">"അടയ്ക്കുക"</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 533375751378..549e39b21101 100644 --- a/libs/WindowManager/Shell/res/values-ml/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-ml/strings_tv.xml @@ -19,7 +19,16 @@ 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="9135220303720555525">"PIP അടയ്ക്കുക"</string> + <string name="pip_close" msgid="2955969519031223530">"അടയ്ക്കുക"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"പൂര്ണ്ണ സ്ക്രീന്"</string> - <string name="pip_move" msgid="1544227837964635439">"PIP നീക്കുക"</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_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.xml b/libs/WindowManager/Shell/res/values-mn/strings.xml index b59f2825b3b5..8e8d06db0436 100644 --- a/libs/WindowManager/Shell/res/values-mn/strings.xml +++ b/libs/WindowManager/Shell/res/values-mn/strings.xml @@ -22,6 +22,7 @@ <string name="pip_phone_settings" msgid="5468987116750491918">"Тохиргоо"</string> <string name="pip_phone_enter_split" msgid="7042877263880641911">"Хуваасан дэлгэцийг оруулна уу"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Цэс"</string> + <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Дэлгэц доторх дэлгэцийн цэс"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> дэлгэцэн доторх дэлгэцэд байна"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Та <xliff:g id="NAME">%s</xliff:g>-д энэ онцлогийг ашиглуулахыг хүсэхгүй байвал тохиргоог нээгээд, үүнийг унтраана уу."</string> <string name="pip_play" msgid="3496151081459417097">"Тоглуулах"</string> @@ -36,6 +37,7 @@ <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Апп хоёрдогч дэлгэцэд ажиллахгүй."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Аппыг хоёрдогч дэлгэцэд эхлүүлэх боломжгүй."</string> <string name="accessibility_divider" msgid="703810061635792791">"\"Дэлгэц хуваах\" хуваагч"</string> + <string name="divider_title" msgid="5482989479865361192">"\"Дэлгэцийг хуваах\" хуваагч"</string> <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Зүүн талын бүтэн дэлгэц"</string> <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Зүүн 70%"</string> <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Зүүн 50%"</string> @@ -72,17 +74,19 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Бөмбөлөг"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Удирдах"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Бөмбөлгийг үл хэрэгссэн."</string> - <string name="restart_button_description" msgid="5887656107651190519">"Энэ аппыг дахин эхлүүлж, бүтэн дэлгэцэд орохын тулд товшино уу."</string> + <string name="restart_button_description" msgid="6712141648865547958">"Харагдах байдлыг сайжруулахын тулд энэ аппыг товшиж, дахин эхлүүлнэ үү."</string> <string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"Камерын асуудал гарсан уу?\nДахин тааруулахын тулд товшино уу"</string> <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Үүнийг засаагүй юу?\nБуцаахын тулд товшино уу"</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Камерын асуудал байхгүй юу? Хаахын тулд товшино уу."</string> - <!-- no translation found for letterbox_education_dialog_title (6688664582871779215) --> + <!-- no translation found for letterbox_education_dialog_title (7739895354143295358) --> <skip /> - <!-- no translation found for letterbox_education_dialog_subtext (4853542518367719562) --> + <!-- no translation found for letterbox_education_split_screen_text (6206339484068670830) --> <skip /> - <!-- no translation found for letterbox_education_screen_rotation_text (5085786687366339027) --> - <skip /> - <!-- no translation found for letterbox_education_reposition_text (1068293354123934727) --> + <!-- no translation found for letterbox_education_reposition_text (4589957299813220661) --> <skip /> <string name="letterbox_education_got_it" msgid="4057634570866051177">"Ойлголоо"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Нэмэлт мэдээлэл авах бол дэлгэнэ үү."</string> + <string name="maximize_button_text" msgid="1650859196290301963">"Томруулах"</string> + <string name="minimize_button_text" msgid="271592547935841753">"Багасгах"</string> + <string name="close_button_text" msgid="2913281996024033299">"Хаах"</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 ca1d27f29cdf..9a85d96ca602 100644 --- a/libs/WindowManager/Shell/res/values-mn/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-mn/strings_tv.xml @@ -19,7 +19,16 @@ 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="9135220303720555525">"PIP-г хаах"</string> + <string name="pip_close" msgid="2955969519031223530">"Хаах"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Бүтэн дэлгэц"</string> - <string name="pip_move" msgid="1544227837964635439">"PIP-г зөөх"</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_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.xml b/libs/WindowManager/Shell/res/values-mr/strings.xml index 3d2d6a36530c..4fc03b276889 100644 --- a/libs/WindowManager/Shell/res/values-mr/strings.xml +++ b/libs/WindowManager/Shell/res/values-mr/strings.xml @@ -22,6 +22,7 @@ <string name="pip_phone_settings" msgid="5468987116750491918">"सेटिंग्ज"</string> <string name="pip_phone_enter_split" msgid="7042877263880641911">"स्प्लिट स्क्रीन एंटर करा"</string> <string name="pip_menu_title" msgid="5393619322111827096">"मेनू"</string> + <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"चित्रात-चित्र मेनू"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> चित्रामध्ये चित्र मध्ये आहे"</string> <string name="pip_notification_message" msgid="8854051911700302620">"<xliff:g id="NAME">%s</xliff:g>ने हे वैशिष्ट्य वापरू नये असे तुम्हाला वाटत असल्यास, सेटिंग्ज उघडण्यासाठी टॅप करा आणि ते बंद करा."</string> <string name="pip_play" msgid="3496151081459417097">"प्ले करा"</string> @@ -36,6 +37,7 @@ <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"दुसऱ्या डिस्प्लेवर अॅप कदाचित चालणार नाही."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"दुसऱ्या डिस्प्लेवर अॅप लाँच होणार नाही."</string> <string name="accessibility_divider" msgid="703810061635792791">"विभाजित-स्क्रीन विभाजक"</string> + <string name="divider_title" msgid="5482989479865361192">"स्प्लिट-स्क्रीन विभाजक"</string> <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"डावी फुल स्क्रीन"</string> <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"डावी 70%"</string> <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"डावी 50%"</string> @@ -72,17 +74,19 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"बबल"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"व्यवस्थापित करा"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"बबल डिसमिस केला."</string> - <string name="restart_button_description" msgid="5887656107651190519">"हे अॅप रीस्टार्ट करण्यासाठी आणि फुल स्क्रीन करण्यासाठी टॅप करा."</string> + <string name="restart_button_description" msgid="6712141648865547958">"अधिक चांगल्या व्ह्यूसाठी हे अॅप रीस्टार्ट करण्याकरिता टॅप करा."</string> <string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"कॅमेराशी संबंधित काही समस्या आहेत का?\nपुन्हा फिट करण्यासाठी टॅप करा"</string> <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"निराकरण झाले नाही?\nरिव्हर्ट करण्यासाठी कृपया टॅप करा"</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"कॅमेराशी संबंधित कोणत्याही समस्या नाहीत का? डिसमिस करण्यासाठी टॅप करा."</string> - <!-- no translation found for letterbox_education_dialog_title (6688664582871779215) --> + <!-- no translation found for letterbox_education_dialog_title (7739895354143295358) --> <skip /> - <!-- no translation found for letterbox_education_dialog_subtext (4853542518367719562) --> + <!-- no translation found for letterbox_education_split_screen_text (6206339484068670830) --> <skip /> - <!-- no translation found for letterbox_education_screen_rotation_text (5085786687366339027) --> - <skip /> - <!-- no translation found for letterbox_education_reposition_text (1068293354123934727) --> + <!-- no translation found for letterbox_education_reposition_text (4589957299813220661) --> <skip /> <string name="letterbox_education_got_it" msgid="4057634570866051177">"समजले"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"अधिक माहितीसाठी विस्तार करा."</string> + <string name="maximize_button_text" msgid="1650859196290301963">"मोठे करा"</string> + <string name="minimize_button_text" msgid="271592547935841753">"लहान करा"</string> + <string name="close_button_text" msgid="2913281996024033299">"बंद करा"</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 212bd21db344..a9779b3a3e89 100644 --- a/libs/WindowManager/Shell/res/values-mr/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-mr/strings_tv.xml @@ -19,7 +19,16 @@ 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="9135220303720555525">"PIP बंद करा"</string> + <string name="pip_close" msgid="2955969519031223530">"बंद करा"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"फुल स्क्रीन"</string> - <string name="pip_move" msgid="1544227837964635439">"PIP हलवा"</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_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.xml b/libs/WindowManager/Shell/res/values-ms/strings.xml index 4e9a7e952a09..2db225aba9f4 100644 --- a/libs/WindowManager/Shell/res/values-ms/strings.xml +++ b/libs/WindowManager/Shell/res/values-ms/strings.xml @@ -22,6 +22,7 @@ <string name="pip_phone_settings" msgid="5468987116750491918">"Tetapan"</string> <string name="pip_phone_enter_split" msgid="7042877263880641911">"Masuk skrin pisah"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Menu"</string> + <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Menu Gambar dalam Gambar"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> terdapat dalam gambar dalam gambar"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Jika anda tidak mahu <xliff:g id="NAME">%s</xliff:g> menggunakan ciri ini, ketik untuk membuka tetapan dan matikan ciri."</string> <string name="pip_play" msgid="3496151081459417097">"Main"</string> @@ -36,6 +37,7 @@ <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Apl mungkin tidak berfungsi pada paparan kedua."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Apl tidak menyokong pelancaran pada paparan kedua."</string> <string name="accessibility_divider" msgid="703810061635792791">"Pembahagi skrin pisah"</string> + <string name="divider_title" msgid="5482989479865361192">"Pembahagi skrin pisah"</string> <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Skrin penuh kiri"</string> <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Kiri 70%"</string> <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Kiri 50%"</string> @@ -72,17 +74,19 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Gelembung"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Urus"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Gelembung diketepikan."</string> - <string name="restart_button_description" msgid="5887656107651190519">"Ketik untuk memulakan semula apl ini dan menggunakan skrin penuh."</string> + <string name="restart_button_description" msgid="6712141648865547958">"Ketik untuk memulakan semula apl ini untuk mendapatkan paparan yang lebih baik."</string> <string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"Isu kamera?\nKetik untuk memuatkan semula"</string> <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Isu tidak dibetulkan?\nKetik untuk kembali"</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Tiada isu kamera? Ketik untuk mengetepikan."</string> - <!-- no translation found for letterbox_education_dialog_title (6688664582871779215) --> + <!-- no translation found for letterbox_education_dialog_title (7739895354143295358) --> <skip /> - <!-- no translation found for letterbox_education_dialog_subtext (4853542518367719562) --> + <!-- no translation found for letterbox_education_split_screen_text (6206339484068670830) --> <skip /> - <!-- no translation found for letterbox_education_screen_rotation_text (5085786687366339027) --> - <skip /> - <!-- no translation found for letterbox_education_reposition_text (1068293354123934727) --> + <!-- no translation found for letterbox_education_reposition_text (4589957299813220661) --> <skip /> <string name="letterbox_education_got_it" msgid="4057634570866051177">"OK"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Kembangkan untuk mendapatkan maklumat lanjut."</string> + <string name="maximize_button_text" msgid="1650859196290301963">"Maksimumkan"</string> + <string name="minimize_button_text" msgid="271592547935841753">"Minimumkan"</string> + <string name="close_button_text" msgid="2913281996024033299">"Tutup"</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 ce2912650da7..8fe992d9f3b9 100644 --- a/libs/WindowManager/Shell/res/values-ms/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-ms/strings_tv.xml @@ -19,7 +19,16 @@ 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="9135220303720555525">"Tutup PIP"</string> + <string name="pip_close" msgid="2955969519031223530">"Tutup"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Skrin penuh"</string> - <string name="pip_move" msgid="1544227837964635439">"Alihkan PIP"</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_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.xml b/libs/WindowManager/Shell/res/values-my/strings.xml index 449e50257d42..e8ab4b04b4dc 100644 --- a/libs/WindowManager/Shell/res/values-my/strings.xml +++ b/libs/WindowManager/Shell/res/values-my/strings.xml @@ -22,6 +22,7 @@ <string name="pip_phone_settings" msgid="5468987116750491918">"ဆက်တင်များ"</string> <string name="pip_phone_enter_split" msgid="7042877263880641911">"မျက်နှာပြင် ခွဲ၍ပြသခြင်းသို့ ဝင်ရန်"</string> <string name="pip_menu_title" msgid="5393619322111827096">"မီနူး"</string> + <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"နှစ်ခုထပ်၍ ကြည့်ခြင်းမီနူး"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> သည် နှစ်ခုထပ်၍ကြည့်ခြင်း ဖွင့်ထားသည်"</string> <string name="pip_notification_message" msgid="8854051911700302620">"<xliff:g id="NAME">%s</xliff:g> အား ဤဝန်ဆောင်မှုကို အသုံးမပြုစေလိုလျှင် ဆက်တင်ကိုဖွင့်ရန် တို့ပြီး ၎င်းဝန်ဆောင်မှုကို ပိတ်လိုက်ပါ။"</string> <string name="pip_play" msgid="3496151081459417097">"ဖွင့်ရန်"</string> @@ -36,6 +37,7 @@ <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"ဤအက်ပ်အနေဖြင့် ဒုတိယဖန်သားပြင်ပေါ်တွင် အလုပ်လုပ်မည် မဟုတ်ပါ။"</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"ဤအက်ပ်အနေဖြင့် ဖွင့်ရန်စနစ်ကို ဒုတိယဖန်သားပြင်မှ အသုံးပြုရန် ပံ့ပိုးမထားပါ။"</string> <string name="accessibility_divider" msgid="703810061635792791">"မျက်နှာပြင်ခွဲခြမ်း ပိုင်းခြားပေးသည့်စနစ်"</string> + <string name="divider_title" msgid="5482989479865361192">"မျက်နှာပြင်ခွဲ၍ပြသသည့် စနစ်"</string> <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"ဘယ်ဘက် မျက်နှာပြင်အပြည့်"</string> <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"ဘယ်ဘက်မျက်နှာပြင် ၇၀%"</string> <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"ဘယ်ဘက် မျက်နှာပြင် ၅၀%"</string> @@ -72,17 +74,19 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"ပူဖောင်းဖောက်သံ"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"စီမံရန်"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"ပူဖောင်းကွက် ဖယ်လိုက်သည်။"</string> - <string name="restart_button_description" msgid="5887656107651190519">"ဤအက်ပ်ကို ပြန်စပြီး ဖန်သားပြင်အပြည့်လုပ်ရန် တို့ပါ။"</string> + <string name="restart_button_description" msgid="6712141648865547958">"ပိုကောင်းသောမြင်ကွင်းအတွက် ဤအက်ပ်ပြန်စရန် တို့နိုင်သည်။"</string> <string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"ကင်မရာပြဿနာလား။\nပြင်ဆင်ရန် တို့ပါ"</string> <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"ကောင်းမသွားဘူးလား။\nပြန်ပြောင်းရန် တို့ပါ"</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"ကင်မရာပြဿနာ မရှိဘူးလား။ ပယ်ရန် တို့ပါ။"</string> - <!-- no translation found for letterbox_education_dialog_title (6688664582871779215) --> + <!-- no translation found for letterbox_education_dialog_title (7739895354143295358) --> <skip /> - <!-- no translation found for letterbox_education_dialog_subtext (4853542518367719562) --> + <!-- no translation found for letterbox_education_split_screen_text (6206339484068670830) --> <skip /> - <!-- no translation found for letterbox_education_screen_rotation_text (5085786687366339027) --> - <skip /> - <!-- no translation found for letterbox_education_reposition_text (1068293354123934727) --> + <!-- no translation found for letterbox_education_reposition_text (4589957299813220661) --> <skip /> <string name="letterbox_education_got_it" msgid="4057634570866051177">"ရပြီ"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"နောက်ထပ်အချက်အလက်များအတွက် ချဲ့နိုင်သည်။"</string> + <string name="maximize_button_text" msgid="1650859196290301963">"ချဲ့ရန်"</string> + <string name="minimize_button_text" msgid="271592547935841753">"ချုံ့ရန်"</string> + <string name="close_button_text" msgid="2913281996024033299">"ပိတ်ရန်"</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 4847742130ba..105628d8149e 100644 --- a/libs/WindowManager/Shell/res/values-my/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-my/strings_tv.xml @@ -19,7 +19,16 @@ 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="9135220303720555525">"PIP ကိုပိတ်ပါ"</string> + <string name="pip_close" msgid="2955969519031223530">"ပိတ်ရန်"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"မျက်နှာပြင် အပြည့်"</string> - <string name="pip_move" msgid="1544227837964635439">"PIP ရွှေ့ရန်"</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_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 2172cc5d3815..278de2d8f1b1 100644 --- a/libs/WindowManager/Shell/res/values-nb/strings.xml +++ b/libs/WindowManager/Shell/res/values-nb/strings.xml @@ -22,6 +22,7 @@ <string name="pip_phone_settings" msgid="5468987116750491918">"Innstillinger"</string> <string name="pip_phone_enter_split" msgid="7042877263880641911">"Aktivér delt skjerm"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Meny"</string> + <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Bilde-i-bilde-meny"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> er i bilde-i-bilde"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Hvis du ikke vil at <xliff:g id="NAME">%s</xliff:g> skal bruke denne funksjonen, kan du trykke for å åpne innstillingene og slå den av."</string> <string name="pip_play" msgid="3496151081459417097">"Spill av"</string> @@ -36,6 +37,7 @@ <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Appen fungerer kanskje ikke på en sekundær skjerm."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Appen kan ikke kjøres på sekundære skjermer."</string> <string name="accessibility_divider" msgid="703810061635792791">"Skilleelement for delt skjerm"</string> + <string name="divider_title" msgid="5482989479865361192">"Skilleelement for delt skjerm"</string> <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Utvid den venstre delen av skjermen til hele skjermen"</string> <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Sett størrelsen på den venstre delen av skjermen til 70 %"</string> <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Sett størrelsen på den venstre delen av skjermen til 50 %"</string> @@ -63,7 +65,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 bobler. Dra for å flytte dem."</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_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> @@ -72,17 +74,19 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Boble"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Administrer"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Boblen er avvist."</string> - <string name="restart_button_description" msgid="5887656107651190519">"Trykk for å starte denne appen på nytt og vise den i fullskjerm."</string> + <string name="restart_button_description" msgid="6712141648865547958">"Trykk for å starte denne appen på nytt for bedre visning."</string> <string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"Har du kameraproblemer?\nTrykk for å tilpasse"</string> <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Ble ikke problemet løst?\nTrykk for å gå tilbake"</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Har du ingen kameraproblemer? Trykk for å lukke."</string> - <!-- no translation found for letterbox_education_dialog_title (6688664582871779215) --> + <!-- no translation found for letterbox_education_dialog_title (7739895354143295358) --> <skip /> - <!-- no translation found for letterbox_education_dialog_subtext (4853542518367719562) --> + <!-- no translation found for letterbox_education_split_screen_text (6206339484068670830) --> <skip /> - <!-- no translation found for letterbox_education_screen_rotation_text (5085786687366339027) --> - <skip /> - <!-- no translation found for letterbox_education_reposition_text (1068293354123934727) --> + <!-- no translation found for letterbox_education_reposition_text (4589957299813220661) --> <skip /> <string name="letterbox_education_got_it" msgid="4057634570866051177">"Greit"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Vis for å få mer informasjon."</string> + <string name="maximize_button_text" msgid="1650859196290301963">"Maksimer"</string> + <string name="minimize_button_text" msgid="271592547935841753">"Minimer"</string> + <string name="close_button_text" msgid="2913281996024033299">"Lukk"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-nb/strings_tv.xml b/libs/WindowManager/Shell/res/values-nb/strings_tv.xml index 7cef11c424ce..ca63518df7a5 100644 --- a/libs/WindowManager/Shell/res/values-nb/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-nb/strings_tv.xml @@ -19,7 +19,16 @@ 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="9135220303720555525">"Lukk PIP"</string> + <string name="pip_close" msgid="2955969519031223530">"Lukk"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Fullskjerm"</string> - <string name="pip_move" msgid="1544227837964635439">"Flytt BIB"</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_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.xml b/libs/WindowManager/Shell/res/values-ne/strings.xml index ff01dcd9ff2d..529f4011b565 100644 --- a/libs/WindowManager/Shell/res/values-ne/strings.xml +++ b/libs/WindowManager/Shell/res/values-ne/strings.xml @@ -22,6 +22,7 @@ <string name="pip_phone_settings" msgid="5468987116750491918">"सेटिङहरू"</string> <string name="pip_phone_enter_split" msgid="7042877263880641911">"स्प्लिट स्क्रिन मोड प्रयोग गर्नुहोस्"</string> <string name="pip_menu_title" msgid="5393619322111827096">"मेनु"</string> + <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"\"picture-in-picture\" मेनु"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> Picture-in-picture मा छ"</string> <string name="pip_notification_message" msgid="8854051911700302620">"तपाईं <xliff:g id="NAME">%s</xliff:g> ले सुविधा प्रयोग नगरोस् भन्ने चाहनुहुन्छ भने ट्याप गरेर सेटिङहरू खोल्नुहोस् र यसलाई निष्क्रिय पार्नुहोस्।"</string> <string name="pip_play" msgid="3496151081459417097">"प्ले गर्नुहोस्"</string> @@ -36,6 +37,7 @@ <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"यो एपले सहायक प्रदर्शनमा काम नगर्नसक्छ।"</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"अनुप्रयोगले सहायक प्रदर्शनहरूमा लञ्च सुविधालाई समर्थन गर्दैन।"</string> <string name="accessibility_divider" msgid="703810061635792791">"विभाजित-स्क्रिन छुट्याउने"</string> + <string name="divider_title" msgid="5482989479865361192">"स्प्लिट स्क्रिन डिभाइडर"</string> <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"बायाँ भाग फुल स्क्रिन"</string> <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"बायाँ भाग ७०%"</string> <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"बायाँ भाग ५०%"</string> @@ -72,17 +74,19 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"बबल"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"व्यवस्थापन गर्नुहोस्"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"बबल हटाइयो।"</string> - <string name="restart_button_description" msgid="5887656107651190519">"यो एप रिस्टार्ट गर्न ट्याप गर्नुहोस् र फुल स्क्रिन मोडमा जानुहोस्।"</string> + <string name="restart_button_description" msgid="6712141648865547958">"यो एप अझ राम्रो हेर्न मिल्ने बनाउनका लागि यसलाई रिस्टार्ट गर्न ट्याप गर्नुहोस्।"</string> <string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"क्यामेरासम्बन्धी समस्या देखियो?\nसमस्या हल गर्न ट्याप गर्नुहोस्"</string> <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"समस्या हल भएन?\nपहिलेको जस्तै बनाउन ट्याप गर्नुहोस्"</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"क्यामेरासम्बन्धी कुनै पनि समस्या छैन? खारेज गर्न ट्याप गर्नुहोस्।"</string> - <!-- no translation found for letterbox_education_dialog_title (6688664582871779215) --> + <!-- no translation found for letterbox_education_dialog_title (7739895354143295358) --> <skip /> - <!-- no translation found for letterbox_education_dialog_subtext (4853542518367719562) --> + <!-- no translation found for letterbox_education_split_screen_text (6206339484068670830) --> <skip /> - <!-- no translation found for letterbox_education_screen_rotation_text (5085786687366339027) --> - <skip /> - <!-- no translation found for letterbox_education_reposition_text (1068293354123934727) --> + <!-- no translation found for letterbox_education_reposition_text (4589957299813220661) --> <skip /> <string name="letterbox_education_got_it" msgid="4057634570866051177">"बुझेँ"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"थप जानकारी प्राप्त गर्न चाहनुहुन्छ भने एक्स्पान्ड गर्नुहोस्।"</string> + <string name="maximize_button_text" msgid="1650859196290301963">"ठुलो बनाउनुहोस्"</string> + <string name="minimize_button_text" msgid="271592547935841753">"मिनिमाइज गर्नुहोस्"</string> + <string name="close_button_text" msgid="2913281996024033299">"बन्द गर्नुहोस्"</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 684d11490be3..7cbf9e294e7b 100644 --- a/libs/WindowManager/Shell/res/values-ne/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-ne/strings_tv.xml @@ -19,7 +19,16 @@ 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="9135220303720555525">"PIP लाई बन्द गर्नुहोस्"</string> + <string name="pip_close" msgid="2955969519031223530">"बन्द गर्नुहोस्"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"फुल स्क्रिन"</string> - <string name="pip_move" msgid="1544227837964635439">"PIP सार्नुहोस्"</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_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.xml b/libs/WindowManager/Shell/res/values-nl/strings.xml index 428cb3fb65d7..88c220cd7dfe 100644 --- a/libs/WindowManager/Shell/res/values-nl/strings.xml +++ b/libs/WindowManager/Shell/res/values-nl/strings.xml @@ -22,6 +22,7 @@ <string name="pip_phone_settings" msgid="5468987116750491918">"Instellingen"</string> <string name="pip_phone_enter_split" msgid="7042877263880641911">"Gesplitst scherm openen"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Menu"</string> + <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Scherm-in-scherm-menu"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> is in scherm-in-scherm"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Als je niet wilt dat <xliff:g id="NAME">%s</xliff:g> deze functie gebruikt, tik je om de instellingen te openen en zet je de functie uit."</string> <string name="pip_play" msgid="3496151081459417097">"Afspelen"</string> @@ -36,6 +37,7 @@ <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"App werkt mogelijk niet op een secundair scherm."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"App kan niet op secundaire displays worden gestart."</string> <string name="accessibility_divider" msgid="703810061635792791">"Scheiding voor gesplitst scherm"</string> + <string name="divider_title" msgid="5482989479865361192">"Scheiding voor gesplitst scherm"</string> <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Linkerscherm op volledig scherm"</string> <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Linkerscherm 70%"</string> <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Linkerscherm 50%"</string> @@ -72,17 +74,19 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Bubbel"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Beheren"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Bubbel gesloten."</string> - <string name="restart_button_description" msgid="5887656107651190519">"Tik om deze app opnieuw te starten en te openen op het volledige scherm."</string> + <string name="restart_button_description" msgid="6712141648865547958">"Tik om deze app opnieuw op te starten voor een betere weergave."</string> <string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"Cameraproblemen?\nTik om opnieuw passend te maken."</string> <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Is dit geen oplossing?\nTik om terug te zetten."</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Geen cameraproblemen? Tik om te sluiten."</string> - <!-- no translation found for letterbox_education_dialog_title (6688664582871779215) --> + <!-- no translation found for letterbox_education_dialog_title (7739895354143295358) --> <skip /> - <!-- no translation found for letterbox_education_dialog_subtext (4853542518367719562) --> + <!-- no translation found for letterbox_education_split_screen_text (6206339484068670830) --> <skip /> - <!-- no translation found for letterbox_education_screen_rotation_text (5085786687366339027) --> - <skip /> - <!-- no translation found for letterbox_education_reposition_text (1068293354123934727) --> + <!-- no translation found for letterbox_education_reposition_text (4589957299813220661) --> <skip /> <string name="letterbox_education_got_it" msgid="4057634570866051177">"OK"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Uitvouwen voor meer informatie."</string> + <string name="maximize_button_text" msgid="1650859196290301963">"Maximaliseren"</string> + <string name="minimize_button_text" msgid="271592547935841753">"Minimaliseren"</string> + <string name="close_button_text" msgid="2913281996024033299">"Sluiten"</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 8562517bd58b..2deaeddc4080 100644 --- a/libs/WindowManager/Shell/res/values-nl/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-nl/strings_tv.xml @@ -19,7 +19,16 @@ 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="9135220303720555525">"PIP sluiten"</string> + <string name="pip_close" msgid="2955969519031223530">"Sluiten"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Volledig scherm"</string> - <string name="pip_move" msgid="1544227837964635439">"SIS verplaatsen"</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_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.xml b/libs/WindowManager/Shell/res/values-or/strings.xml index f9668a1112b3..2c82fbcf9ff7 100644 --- a/libs/WindowManager/Shell/res/values-or/strings.xml +++ b/libs/WindowManager/Shell/res/values-or/strings.xml @@ -22,6 +22,7 @@ <string name="pip_phone_settings" msgid="5468987116750491918">"ସେଟିଂସ୍"</string> <string name="pip_phone_enter_split" msgid="7042877263880641911">"ସ୍ପ୍ଲିଟ ସ୍କ୍ରିନ ମୋଡ ବ୍ୟବହାର କରନ୍ତୁ"</string> <string name="pip_menu_title" msgid="5393619322111827096">"ମେନୁ"</string> + <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"ପିକଚର-ଇନ-ପିକଚର ମେନୁ"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> \"ଛବି-ଭିତରେ-ଛବି\"ରେ ଅଛି"</string> <string name="pip_notification_message" msgid="8854051911700302620">"ଏହି ବୈଶିଷ୍ଟ୍ୟ <xliff:g id="NAME">%s</xliff:g> ବ୍ୟବହାର ନକରିବାକୁ ଯଦି ଆପଣ ଚାହାଁନ୍ତି, ସେଟିଙ୍ଗ ଖୋଲିବାକୁ ଟାପ୍ କରନ୍ତୁ ଏବଂ ଏହା ଅଫ୍ କରିଦିଅନ୍ତୁ।"</string> <string name="pip_play" msgid="3496151081459417097">"ପ୍ଲେ କରନ୍ତୁ"</string> @@ -36,6 +37,7 @@ <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"ସେକେଣ୍ଡାରୀ ଡିସପ୍ଲେରେ ଆପ୍ କାମ ନକରିପାରେ।"</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"ସେକେଣ୍ଡାରୀ ଡିସପ୍ଲେରେ ଆପ୍ ଲଞ୍ଚ ସପୋର୍ଟ କରେ ନାହିଁ।"</string> <string name="accessibility_divider" msgid="703810061635792791">"ସ୍ପ୍ଲିଟ୍-ସ୍କ୍ରୀନ ବିଭାଜକ"</string> + <string name="divider_title" msgid="5482989479865361192">"ସ୍ପ୍ଲିଟ-ସ୍କ୍ରିନ ଡିଭାଇଡର"</string> <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"ବାମ ପଟକୁ ପୂର୍ଣ୍ଣ ସ୍କ୍ରୀନ୍ କରନ୍ତୁ"</string> <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"ବାମ ପଟକୁ 70% କରନ୍ତୁ"</string> <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"ବାମ ପଟକୁ 50% କରନ୍ତୁ"</string> @@ -72,17 +74,19 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"ବବଲ୍"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"ପରିଚାଳନା କରନ୍ତୁ"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"ବବଲ୍ ଖାରଜ କରାଯାଇଛି।"</string> - <string name="restart_button_description" msgid="5887656107651190519">"ଏହି ଆପକୁ ରିଷ୍ଟାର୍ଟ କରି ପୂର୍ଣ୍ଣ ସ୍କ୍ରିନ୍ କରିବାକୁ ଟାପ୍ କରନ୍ତୁ।"</string> + <string name="restart_button_description" msgid="6712141648865547958">"ଏକ ଆହୁରି ଭଲ ଭ୍ୟୁ ପାଇଁ ଏହି ଆପ ରିଷ୍ଟାର୍ଟ କରିବାକୁ ଟାପ କରନ୍ତୁ।"</string> <string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"କ୍ୟାମେରାରେ ସମସ୍ୟା ଅଛି?\nପୁଣି ଫିଟ କରିବାକୁ ଟାପ କରନ୍ତୁ"</string> <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"ଏହାର ସମାଧାନ ହୋଇନାହିଁ?\nଫେରିଯିବା ପାଇଁ ଟାପ କରନ୍ତୁ"</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"କ୍ୟାମେରାରେ କିଛି ସମସ୍ୟା ନାହିଁ? ଖାରଜ କରିବାକୁ ଟାପ କରନ୍ତୁ।"</string> - <!-- no translation found for letterbox_education_dialog_title (6688664582871779215) --> + <!-- no translation found for letterbox_education_dialog_title (7739895354143295358) --> <skip /> - <!-- no translation found for letterbox_education_dialog_subtext (4853542518367719562) --> + <!-- no translation found for letterbox_education_split_screen_text (6206339484068670830) --> <skip /> - <!-- no translation found for letterbox_education_screen_rotation_text (5085786687366339027) --> - <skip /> - <!-- no translation found for letterbox_education_reposition_text (1068293354123934727) --> + <!-- no translation found for letterbox_education_reposition_text (4589957299813220661) --> <skip /> <string name="letterbox_education_got_it" msgid="4057634570866051177">"ବୁଝିଗଲି"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"ଅଧିକ ସୂଚନା ପାଇଁ ବିସ୍ତାର କରନ୍ତୁ।"</string> + <string name="maximize_button_text" msgid="1650859196290301963">"ବଡ଼ କରନ୍ତୁ"</string> + <string name="minimize_button_text" msgid="271592547935841753">"ଛୋଟ କରନ୍ତୁ"</string> + <string name="close_button_text" msgid="2913281996024033299">"ବନ୍ଦ କରନ୍ତୁ"</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 f8bc01642d88..0c1d99e4ca71 100644 --- a/libs/WindowManager/Shell/res/values-or/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-or/strings_tv.xml @@ -19,7 +19,16 @@ 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="9135220303720555525">"PIP ବନ୍ଦ କରନ୍ତୁ"</string> + <string name="pip_close" msgid="2955969519031223530">"ବନ୍ଦ କରନ୍ତୁ"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"ପୂର୍ଣ୍ଣ ସ୍କ୍ରୀନ୍"</string> - <string name="pip_move" msgid="1544227837964635439">"PIPକୁ ମୁଭ କରନ୍ତୁ"</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_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.xml b/libs/WindowManager/Shell/res/values-pa/strings.xml index 7132597d5b11..8c7229f58730 100644 --- a/libs/WindowManager/Shell/res/values-pa/strings.xml +++ b/libs/WindowManager/Shell/res/values-pa/strings.xml @@ -22,6 +22,7 @@ <string name="pip_phone_settings" msgid="5468987116750491918">"ਸੈਟਿੰਗਾਂ"</string> <string name="pip_phone_enter_split" msgid="7042877263880641911">"ਸਪਲਿਟ ਸਕ੍ਰੀਨ ਵਿੱਚ ਦਾਖਲ ਹੋਵੋ"</string> <string name="pip_menu_title" msgid="5393619322111827096">"ਮੀਨੂ"</string> + <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"ਤਸਵੀਰ-ਵਿੱਚ-ਤਸਵੀਰ ਮੀਨੂ"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> ਤਸਵੀਰ-ਅੰਦਰ-ਤਸਵੀਰ ਵਿੱਚ ਹੈ"</string> <string name="pip_notification_message" msgid="8854051911700302620">"ਜੇਕਰ ਤੁਸੀਂ ਨਹੀਂ ਚਾਹੁੰਦੇ ਕਿ <xliff:g id="NAME">%s</xliff:g> ਐਪ ਇਸ ਵਿਸ਼ੇਸ਼ਤਾ ਦੀ ਵਰਤੋਂ ਕਰੇ, ਤਾਂ ਸੈਟਿੰਗਾਂ ਖੋਲ੍ਹਣ ਲਈ ਟੈਪ ਕਰੋ ਅਤੇ ਇਸਨੂੰ ਬੰਦ ਕਰੋ।"</string> <string name="pip_play" msgid="3496151081459417097">"ਚਲਾਓ"</string> @@ -36,6 +37,7 @@ <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"ਹੋ ਸਕਦਾ ਹੈ ਕਿ ਐਪ ਸੈਕੰਡਰੀ ਡਿਸਪਲੇ \'ਤੇ ਕੰਮ ਨਾ ਕਰੇ।"</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"ਐਪ ਸੈਕੰਡਰੀ ਡਿਸਪਲੇਆਂ \'ਤੇ ਲਾਂਚ ਕਰਨ ਦਾ ਸਮਰਥਨ ਨਹੀਂ ਕਰਦੀ"</string> <string name="accessibility_divider" msgid="703810061635792791">"ਸਪਲਿਟ-ਸਕ੍ਰੀਨ ਡਿਵਾਈਡਰ"</string> + <string name="divider_title" msgid="5482989479865361192">"ਸਪਲਿਟ-ਸਕ੍ਰੀਨ ਵਿਭਾਜਕ"</string> <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"ਖੱਬੇ ਪੂਰੀ ਸਕ੍ਰੀਨ"</string> <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"ਖੱਬੇ 70%"</string> <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"ਖੱਬੇ 50%"</string> @@ -72,17 +74,19 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"ਬੁਲਬੁਲਾ"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"ਪ੍ਰਬੰਧਨ ਕਰੋ"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"ਬਬਲ ਨੂੰ ਖਾਰਜ ਕੀਤਾ ਗਿਆ।"</string> - <string name="restart_button_description" msgid="5887656107651190519">"ਇਸ ਐਪ ਨੂੰ ਮੁੜ-ਸ਼ੁਰੂ ਕਰਨ ਲਈ ਟੈਪ ਕਰੋ ਅਤੇ ਪੂਰੀ ਸਕ੍ਰੀਨ ਮੋਡ \'ਤੇ ਜਾਓ।"</string> + <string name="restart_button_description" msgid="6712141648865547958">"ਬਿਹਤਰ ਦ੍ਰਿਸ਼ ਲਈ ਇਸ ਐਪ ਨੂੰ ਮੁੜ-ਸ਼ੁਰੂ ਕਰਨ ਲਈ ਟੈਪ ਕਰੋ।"</string> <string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"ਕੀ ਕੈਮਰੇ ਸੰਬੰਧੀ ਸਮੱਸਿਆਵਾਂ ਹਨ?\nਮੁੜ-ਫਿੱਟ ਕਰਨ ਲਈ ਟੈਪ ਕਰੋ"</string> <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"ਕੀ ਇਹ ਠੀਕ ਨਹੀਂ ਹੋਈ?\nਵਾਪਸ ਉਹੀ ਕਰਨ ਲਈ ਟੈਪ ਕਰੋ"</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"ਕੀ ਕੈਮਰੇ ਸੰਬੰਧੀ ਕੋਈ ਸਮੱਸਿਆ ਨਹੀਂ ਹੈ? ਖਾਰਜ ਕਰਨ ਲਈ ਟੈਪ ਕਰੋ।"</string> - <!-- no translation found for letterbox_education_dialog_title (6688664582871779215) --> + <!-- no translation found for letterbox_education_dialog_title (7739895354143295358) --> <skip /> - <!-- no translation found for letterbox_education_dialog_subtext (4853542518367719562) --> + <!-- no translation found for letterbox_education_split_screen_text (6206339484068670830) --> <skip /> - <!-- no translation found for letterbox_education_screen_rotation_text (5085786687366339027) --> - <skip /> - <!-- no translation found for letterbox_education_reposition_text (1068293354123934727) --> + <!-- no translation found for letterbox_education_reposition_text (4589957299813220661) --> <skip /> <string name="letterbox_education_got_it" msgid="4057634570866051177">"ਸਮਝ ਲਿਆ"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"ਹੋਰ ਜਾਣਕਾਰੀ ਲਈ ਵਿਸਤਾਰ ਕਰੋ।"</string> + <string name="maximize_button_text" msgid="1650859196290301963">"ਵੱਡਾ ਕਰੋ"</string> + <string name="minimize_button_text" msgid="271592547935841753">"ਛੋਟਾ ਕਰੋ"</string> + <string name="close_button_text" msgid="2913281996024033299">"ਬੰਦ ਕਰੋ"</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 1667e5fc6eac..a1edde738775 100644 --- a/libs/WindowManager/Shell/res/values-pa/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-pa/strings_tv.xml @@ -19,7 +19,16 @@ 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="9135220303720555525">"PIP ਬੰਦ ਕਰੋ"</string> + <string name="pip_close" msgid="2955969519031223530">"ਬੰਦ ਕਰੋ"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"ਪੂਰੀ ਸਕ੍ਰੀਨ"</string> - <string name="pip_move" msgid="1544227837964635439">"PIP ਨੂੰ ਲਿਜਾਓ"</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_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.xml b/libs/WindowManager/Shell/res/values-pl/strings.xml index f7f97efa1a88..315b95e88265 100644 --- a/libs/WindowManager/Shell/res/values-pl/strings.xml +++ b/libs/WindowManager/Shell/res/values-pl/strings.xml @@ -22,6 +22,7 @@ <string name="pip_phone_settings" msgid="5468987116750491918">"Ustawienia"</string> <string name="pip_phone_enter_split" msgid="7042877263880641911">"Włącz podzielony ekran"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Menu"</string> + <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Menu funkcji Obraz w obrazie."</string> <string name="pip_notification_title" msgid="1347104727641353453">"Aplikacja <xliff:g id="NAME">%s</xliff:g> działa w trybie obraz w obrazie"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Jeśli nie chcesz, by aplikacja <xliff:g id="NAME">%s</xliff:g> korzystała z tej funkcji, otwórz ustawienia i wyłącz ją."</string> <string name="pip_play" msgid="3496151081459417097">"Odtwórz"</string> @@ -36,6 +37,7 @@ <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Aplikacja może nie działać na dodatkowym ekranie."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Aplikacja nie obsługuje uruchamiania na dodatkowych ekranach."</string> <string name="accessibility_divider" msgid="703810061635792791">"Linia dzielenia ekranu"</string> + <string name="divider_title" msgid="5482989479865361192">"Linia dzielenia ekranu"</string> <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Lewa część ekranu na pełnym ekranie"</string> <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"70% lewej części ekranu"</string> <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"50% lewej części ekranu"</string> @@ -72,17 +74,19 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Dymek"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Zarządzaj"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Zamknięto dymek"</string> - <string name="restart_button_description" msgid="5887656107651190519">"Kliknij, by uruchomić tę aplikację ponownie i przejść w tryb pełnoekranowy."</string> + <string name="restart_button_description" msgid="6712141648865547958">"Kliknij, aby zrestartować aplikację i zyskać lepszą widoczność."</string> <string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"Problemy z aparatem?\nKliknij, aby dopasować"</string> <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Naprawa się nie udała?\nKliknij, aby cofnąć"</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Brak problemów z aparatem? Kliknij, aby zamknąć"</string> - <!-- no translation found for letterbox_education_dialog_title (6688664582871779215) --> + <!-- no translation found for letterbox_education_dialog_title (7739895354143295358) --> <skip /> - <!-- no translation found for letterbox_education_dialog_subtext (4853542518367719562) --> + <!-- no translation found for letterbox_education_split_screen_text (6206339484068670830) --> <skip /> - <!-- no translation found for letterbox_education_screen_rotation_text (5085786687366339027) --> - <skip /> - <!-- no translation found for letterbox_education_reposition_text (1068293354123934727) --> + <!-- no translation found for letterbox_education_reposition_text (4589957299813220661) --> <skip /> <string name="letterbox_education_got_it" msgid="4057634570866051177">"OK"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Rozwiń, aby wyświetlić więcej informacji."</string> + <string name="maximize_button_text" msgid="1650859196290301963">"Maksymalizuj"</string> + <string name="minimize_button_text" msgid="271592547935841753">"Minimalizuj"</string> + <string name="close_button_text" msgid="2913281996024033299">"Zamknij"</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 28bf66a7ee1b..2bb90addc241 100644 --- a/libs/WindowManager/Shell/res/values-pl/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-pl/strings_tv.xml @@ -19,7 +19,16 @@ 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="9135220303720555525">"Zamknij PIP"</string> + <string name="pip_close" msgid="2955969519031223530">"Zamknij"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Pełny ekran"</string> - <string name="pip_move" msgid="1544227837964635439">"Przenieś PIP"</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_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.xml b/libs/WindowManager/Shell/res/values-pt-rBR/strings.xml index a3d2ab0feffa..48781ad437bd 100644 --- a/libs/WindowManager/Shell/res/values-pt-rBR/strings.xml +++ b/libs/WindowManager/Shell/res/values-pt-rBR/strings.xml @@ -22,6 +22,7 @@ <string name="pip_phone_settings" msgid="5468987116750491918">"Configurações"</string> <string name="pip_phone_enter_split" msgid="7042877263880641911">"Dividir tela"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Menu"</string> + <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Menu do picture-in-picture"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> está em picture-in-picture"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Se você não quer que o app <xliff:g id="NAME">%s</xliff:g> use este recurso, toque para abrir as configurações e desativá-lo."</string> <string name="pip_play" msgid="3496151081459417097">"Reproduzir"</string> @@ -36,6 +37,7 @@ <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"É possível que o app não funcione em uma tela secundária."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"O app não é compatível com a inicialização em telas secundárias."</string> <string name="accessibility_divider" msgid="703810061635792791">"Divisor de tela"</string> + <string name="divider_title" msgid="5482989479865361192">"Divisor de tela"</string> <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Lado esquerdo em tela cheia"</string> <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Esquerda a 70%"</string> <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Esquerda a 50%"</string> @@ -72,17 +74,19 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Bolha"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Gerenciar"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Balão dispensado."</string> - <string name="restart_button_description" msgid="5887656107651190519">"Toque para reiniciar o app e usar tela cheia."</string> + <string name="restart_button_description" msgid="6712141648865547958">"Toque para reiniciar o app e atualizar a visualização."</string> <string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"Problemas com a câmera?\nToque para ajustar o enquadramento"</string> <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"O problema não foi corrigido?\nToque para reverter"</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Não tem problemas com a câmera? Toque para dispensar."</string> - <!-- no translation found for letterbox_education_dialog_title (6688664582871779215) --> + <!-- no translation found for letterbox_education_dialog_title (7739895354143295358) --> <skip /> - <!-- no translation found for letterbox_education_dialog_subtext (4853542518367719562) --> + <!-- no translation found for letterbox_education_split_screen_text (6206339484068670830) --> <skip /> - <!-- no translation found for letterbox_education_screen_rotation_text (5085786687366339027) --> - <skip /> - <!-- no translation found for letterbox_education_reposition_text (1068293354123934727) --> + <!-- no translation found for letterbox_education_reposition_text (4589957299813220661) --> <skip /> <string name="letterbox_education_got_it" msgid="4057634570866051177">"Entendi"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Abra para ver mais informações."</string> + <string name="maximize_button_text" msgid="1650859196290301963">"Maximizar"</string> + <string name="minimize_button_text" msgid="271592547935841753">"Minimizar"</string> + <string name="close_button_text" msgid="2913281996024033299">"Fechar"</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 27626b8ecfd6..14d1c34fd3e8 100644 --- a/libs/WindowManager/Shell/res/values-pt-rBR/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-pt-rBR/strings_tv.xml @@ -19,7 +19,16 @@ 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="9135220303720555525">"Fechar PIP"</string> + <string name="pip_close" msgid="2955969519031223530">"Fechar"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Tela cheia"</string> - <string name="pip_move" msgid="1544227837964635439">"Mover picture-in-picture"</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_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.xml b/libs/WindowManager/Shell/res/values-pt-rPT/strings.xml index 86872c811857..e2be183e0ada 100644 --- a/libs/WindowManager/Shell/res/values-pt-rPT/strings.xml +++ b/libs/WindowManager/Shell/res/values-pt-rPT/strings.xml @@ -22,6 +22,7 @@ <string name="pip_phone_settings" msgid="5468987116750491918">"Definições"</string> <string name="pip_phone_enter_split" msgid="7042877263880641911">"Aceder ao ecrã dividido"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Menu"</string> + <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Menu de ecrã no ecrã"</string> <string name="pip_notification_title" msgid="1347104727641353453">"A app <xliff:g id="NAME">%s</xliff:g> está no modo de ecrã no ecrã"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Se não pretende que a app <xliff:g id="NAME">%s</xliff:g> utilize esta funcionalidade, toque para abrir as definições e desative-a."</string> <string name="pip_play" msgid="3496151081459417097">"Reproduzir"</string> @@ -36,6 +37,7 @@ <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"A app pode não funcionar num ecrã secundário."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"A app não é compatível com o início em ecrãs secundários."</string> <string name="accessibility_divider" msgid="703810061635792791">"Divisor do ecrã dividido"</string> + <string name="divider_title" msgid="5482989479865361192">"Divisor do ecrã dividido"</string> <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Ecrã esquerdo inteiro"</string> <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"70% no ecrã esquerdo"</string> <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"50% no ecrã esquerdo"</string> @@ -72,17 +74,19 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Balão"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Gerir"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Balão ignorado."</string> - <string name="restart_button_description" msgid="5887656107651190519">"Toque para reiniciar esta app e ficar em ecrã inteiro."</string> + <string name="restart_button_description" msgid="6712141648865547958">"Toque para reiniciar esta app e ficar com uma melhor visão."</string> <string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"Problemas com a câmara?\nToque aqui para reajustar"</string> <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Não foi corrigido?\nToque para reverter"</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Nenhum problema com a câmara? Toque para ignorar."</string> - <!-- no translation found for letterbox_education_dialog_title (6688664582871779215) --> + <!-- no translation found for letterbox_education_dialog_title (7739895354143295358) --> <skip /> - <!-- no translation found for letterbox_education_dialog_subtext (4853542518367719562) --> + <!-- no translation found for letterbox_education_split_screen_text (6206339484068670830) --> <skip /> - <!-- no translation found for letterbox_education_screen_rotation_text (5085786687366339027) --> - <skip /> - <!-- no translation found for letterbox_education_reposition_text (1068293354123934727) --> + <!-- no translation found for letterbox_education_reposition_text (4589957299813220661) --> <skip /> <string name="letterbox_education_got_it" msgid="4057634570866051177">"OK"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Expandir para obter mais informações"</string> + <string name="maximize_button_text" msgid="1650859196290301963">"Maximizar"</string> + <string name="minimize_button_text" msgid="271592547935841753">"Minimizar"</string> + <string name="close_button_text" msgid="2913281996024033299">"Fechar"</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 a2010cee9e03..1ada4508714a 100644 --- a/libs/WindowManager/Shell/res/values-pt-rPT/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-pt-rPT/strings_tv.xml @@ -19,7 +19,16 @@ 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="9135220303720555525">"Fechar PIP"</string> + <string name="pip_close" msgid="2955969519031223530">"Fechar"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Ecrã inteiro"</string> - <string name="pip_move" msgid="1544227837964635439">"Mover Ecrã no ecrã"</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_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.xml b/libs/WindowManager/Shell/res/values-pt/strings.xml index a3d2ab0feffa..48781ad437bd 100644 --- a/libs/WindowManager/Shell/res/values-pt/strings.xml +++ b/libs/WindowManager/Shell/res/values-pt/strings.xml @@ -22,6 +22,7 @@ <string name="pip_phone_settings" msgid="5468987116750491918">"Configurações"</string> <string name="pip_phone_enter_split" msgid="7042877263880641911">"Dividir tela"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Menu"</string> + <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Menu do picture-in-picture"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> está em picture-in-picture"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Se você não quer que o app <xliff:g id="NAME">%s</xliff:g> use este recurso, toque para abrir as configurações e desativá-lo."</string> <string name="pip_play" msgid="3496151081459417097">"Reproduzir"</string> @@ -36,6 +37,7 @@ <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"É possível que o app não funcione em uma tela secundária."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"O app não é compatível com a inicialização em telas secundárias."</string> <string name="accessibility_divider" msgid="703810061635792791">"Divisor de tela"</string> + <string name="divider_title" msgid="5482989479865361192">"Divisor de tela"</string> <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Lado esquerdo em tela cheia"</string> <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Esquerda a 70%"</string> <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Esquerda a 50%"</string> @@ -72,17 +74,19 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Bolha"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Gerenciar"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Balão dispensado."</string> - <string name="restart_button_description" msgid="5887656107651190519">"Toque para reiniciar o app e usar tela cheia."</string> + <string name="restart_button_description" msgid="6712141648865547958">"Toque para reiniciar o app e atualizar a visualização."</string> <string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"Problemas com a câmera?\nToque para ajustar o enquadramento"</string> <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"O problema não foi corrigido?\nToque para reverter"</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Não tem problemas com a câmera? Toque para dispensar."</string> - <!-- no translation found for letterbox_education_dialog_title (6688664582871779215) --> + <!-- no translation found for letterbox_education_dialog_title (7739895354143295358) --> <skip /> - <!-- no translation found for letterbox_education_dialog_subtext (4853542518367719562) --> + <!-- no translation found for letterbox_education_split_screen_text (6206339484068670830) --> <skip /> - <!-- no translation found for letterbox_education_screen_rotation_text (5085786687366339027) --> - <skip /> - <!-- no translation found for letterbox_education_reposition_text (1068293354123934727) --> + <!-- no translation found for letterbox_education_reposition_text (4589957299813220661) --> <skip /> <string name="letterbox_education_got_it" msgid="4057634570866051177">"Entendi"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Abra para ver mais informações."</string> + <string name="maximize_button_text" msgid="1650859196290301963">"Maximizar"</string> + <string name="minimize_button_text" msgid="271592547935841753">"Minimizar"</string> + <string name="close_button_text" msgid="2913281996024033299">"Fechar"</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 27626b8ecfd6..14d1c34fd3e8 100644 --- a/libs/WindowManager/Shell/res/values-pt/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-pt/strings_tv.xml @@ -19,7 +19,16 @@ 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="9135220303720555525">"Fechar PIP"</string> + <string name="pip_close" msgid="2955969519031223530">"Fechar"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Tela cheia"</string> - <string name="pip_move" msgid="1544227837964635439">"Mover picture-in-picture"</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_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 5448e459a268..65b0472805cf 100644 --- a/libs/WindowManager/Shell/res/values-ro/strings.xml +++ b/libs/WindowManager/Shell/res/values-ro/strings.xml @@ -22,6 +22,7 @@ <string name="pip_phone_settings" msgid="5468987116750491918">"Setări"</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_menu_accessibility_title" msgid="8129016817688656249">"Meniu picture-in-picture"</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">"Redați"</string> @@ -36,6 +37,7 @@ <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Este posibil ca aplicația să nu funcționeze pe un ecran secundar."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Aplicația nu acceptă lansare pe ecrane secundare."</string> <string name="accessibility_divider" msgid="703810061635792791">"Separator pentru ecranul împărțit"</string> + <string name="divider_title" msgid="5482989479865361192">"Separator pentru ecranul împărțit"</string> <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Partea stângă pe ecran complet"</string> <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Partea stângă: 70%"</string> <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Partea stângă: 50%"</string> @@ -72,17 +74,19 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Balon"</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="restart_button_description" msgid="6712141648865547958">"Atingeți ca să reporniți aplicația pentru o vizualizare mai bună."</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> - <!-- no translation found for letterbox_education_dialog_title (6688664582871779215) --> + <!-- no translation found for letterbox_education_dialog_title (7739895354143295358) --> <skip /> - <!-- no translation found for letterbox_education_dialog_subtext (4853542518367719562) --> + <!-- no translation found for letterbox_education_split_screen_text (6206339484068670830) --> <skip /> - <!-- no translation found for letterbox_education_screen_rotation_text (5085786687366339027) --> - <skip /> - <!-- no translation found for letterbox_education_reposition_text (1068293354123934727) --> + <!-- no translation found for letterbox_education_reposition_text (4589957299813220661) --> <skip /> <string name="letterbox_education_got_it" msgid="4057634570866051177">"OK"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Extindeți pentru mai multe informații"</string> + <string name="maximize_button_text" msgid="1650859196290301963">"Maximizați"</string> + <string name="minimize_button_text" msgid="271592547935841753">"Minimizează"</string> + <string name="close_button_text" msgid="2913281996024033299">"Închideți"</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 18e29a60191f..56dadb2e5e65 100644 --- a/libs/WindowManager/Shell/res/values-ro/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-ro/strings_tv.xml @@ -19,7 +19,16 @@ 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="9135220303720555525">"Închideți PIP"</string> + <string name="pip_close" msgid="2955969519031223530">"Închideți"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Ecran complet"</string> - <string name="pip_move" msgid="1544227837964635439">"Mutați fereastra PIP"</string> + <string name="pip_move" msgid="158770205886688553">"Mutați"</string> + <string name="pip_expand" msgid="1051966011679297308">"Extindeți"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Restrângeți"</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> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Meniu picture-in-picture."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Mutați spre stânga"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Mutați spre dreapta"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Mutați în sus"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Mutați în jos"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Gata"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ru/strings.xml b/libs/WindowManager/Shell/res/values-ru/strings.xml index 64e74a27d32b..8affb9add8b9 100644 --- a/libs/WindowManager/Shell/res/values-ru/strings.xml +++ b/libs/WindowManager/Shell/res/values-ru/strings.xml @@ -22,6 +22,7 @@ <string name="pip_phone_settings" msgid="5468987116750491918">"Настройки"</string> <string name="pip_phone_enter_split" msgid="7042877263880641911">"Включить разделение экрана"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Меню"</string> + <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Меню \"Картинка в картинке\""</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> находится в режиме \"Картинка в картинке\""</string> <string name="pip_notification_message" msgid="8854051911700302620">"Чтобы отключить эту функцию для приложения \"<xliff:g id="NAME">%s</xliff:g>\", перейдите в настройки."</string> <string name="pip_play" msgid="3496151081459417097">"Воспроизвести"</string> @@ -36,6 +37,7 @@ <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Приложение может не работать на дополнительном экране"</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Приложение не поддерживает запуск на дополнительных экранах"</string> <string name="accessibility_divider" msgid="703810061635792791">"Разделитель экрана"</string> + <string name="divider_title" msgid="5482989479865361192">"Разделитель экрана"</string> <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Левый во весь экран"</string> <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Левый на 70%"</string> <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Левый на 50%"</string> @@ -72,17 +74,19 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Всплывающая подсказка"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Настроить"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Всплывающий чат закрыт."</string> - <string name="restart_button_description" msgid="5887656107651190519">"Нажмите, чтобы перезапустить приложение и перейти в полноэкранный режим."</string> + <string name="restart_button_description" msgid="6712141648865547958">"Нажмите, чтобы перезапустить приложение и настроить удобный для просмотра вид"</string> <string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"Проблемы с камерой?\nНажмите, чтобы исправить."</string> <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Не помогло?\nНажмите, чтобы отменить изменения."</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Нет проблем с камерой? Нажмите, чтобы закрыть."</string> - <!-- no translation found for letterbox_education_dialog_title (6688664582871779215) --> + <!-- no translation found for letterbox_education_dialog_title (7739895354143295358) --> <skip /> - <!-- no translation found for letterbox_education_dialog_subtext (4853542518367719562) --> + <!-- no translation found for letterbox_education_split_screen_text (6206339484068670830) --> <skip /> - <!-- no translation found for letterbox_education_screen_rotation_text (5085786687366339027) --> - <skip /> - <!-- no translation found for letterbox_education_reposition_text (1068293354123934727) --> + <!-- no translation found for letterbox_education_reposition_text (4589957299813220661) --> <skip /> <string name="letterbox_education_got_it" msgid="4057634570866051177">"ОК"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Развернуть, чтобы узнать больше."</string> + <string name="maximize_button_text" msgid="1650859196290301963">"Развернуть"</string> + <string name="minimize_button_text" msgid="271592547935841753">"Свернуть"</string> + <string name="close_button_text" msgid="2913281996024033299">"Закрыть"</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 d119240adc4d..e7f55ec1bc57 100644 --- a/libs/WindowManager/Shell/res/values-ru/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-ru/strings_tv.xml @@ -19,7 +19,16 @@ 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="9135220303720555525">"\"Кадр в кадре\" – выйти"</string> + <string name="pip_close" msgid="2955969519031223530">"Закрыть"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Во весь экран"</string> - <string name="pip_move" msgid="1544227837964635439">"Переместить PIP"</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_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.xml b/libs/WindowManager/Shell/res/values-si/strings.xml index 3cdaa72b0153..c816065b3585 100644 --- a/libs/WindowManager/Shell/res/values-si/strings.xml +++ b/libs/WindowManager/Shell/res/values-si/strings.xml @@ -22,6 +22,7 @@ <string name="pip_phone_settings" msgid="5468987116750491918">"සැකසීම්"</string> <string name="pip_phone_enter_split" msgid="7042877263880641911">"බෙදුම් තිරයට ඇතුළු වන්න"</string> <string name="pip_menu_title" msgid="5393619322111827096">"මෙනුව"</string> + <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"පින්තූරය තුළ පින්තූරය මෙනුව"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> පින්තූරය-තුළ-පින්තූරය තුළ වේ"</string> <string name="pip_notification_message" msgid="8854051911700302620">"ඔබට <xliff:g id="NAME">%s</xliff:g> මෙම විශේෂාංගය භාවිත කිරීමට අවශ්ය නැති නම්, සැකසීම් විවෘත කිරීමට තට්ටු කර එය ක්රියාවිරහිත කරන්න."</string> <string name="pip_play" msgid="3496151081459417097">"ධාවනය කරන්න"</string> @@ -36,6 +37,7 @@ <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"යෙදුම ද්විතියික සංදර්ශකයක ක්රියා නොකළ හැකිය."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"යෙදුම ද්විතීයික සංදර්ශක මත දියත් කිරීම සඳහා සහාය නොදක්වයි."</string> <string name="accessibility_divider" msgid="703810061635792791">"බෙදුම්-තිර වෙන්කරණය"</string> + <string name="divider_title" msgid="5482989479865361192">"බෙදුම්-තිර වෙන්කරණය"</string> <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"වම් පූර්ණ තිරය"</string> <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"වම් 70%"</string> <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"වම් 50%"</string> @@ -72,17 +74,19 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"බුබුළු"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"කළමනා කරන්න"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"බුබුල ඉවත දමා ඇත."</string> - <string name="restart_button_description" msgid="5887656107651190519">"මෙම යෙදුම යළි ඇරඹීමට සහ පූර්ණ තිරයට යාමට තට්ටු කරන්න."</string> + <string name="restart_button_description" msgid="6712141648865547958">"වඩා හොඳ දසුනක් ලබා ගැනීම සඳහා මෙම යෙදුම යළි ඇරඹීමට තට්ටු කරන්න."</string> <string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"කැමරා ගැටලුද?\nයළි සවි කිරීමට තට්ටු කරන්න"</string> <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"එය විසඳුවේ නැතිද?\nප්රතිවර්තනය කිරීමට තට්ටු කරන්න"</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"කැමරා ගැටලු නොමැතිද? ඉවත දැමීමට තට්ටු කරන්න"</string> - <!-- no translation found for letterbox_education_dialog_title (6688664582871779215) --> + <!-- no translation found for letterbox_education_dialog_title (7739895354143295358) --> <skip /> - <!-- no translation found for letterbox_education_dialog_subtext (4853542518367719562) --> + <!-- no translation found for letterbox_education_split_screen_text (6206339484068670830) --> <skip /> - <!-- no translation found for letterbox_education_screen_rotation_text (5085786687366339027) --> - <skip /> - <!-- no translation found for letterbox_education_reposition_text (1068293354123934727) --> + <!-- no translation found for letterbox_education_reposition_text (4589957299813220661) --> <skip /> <string name="letterbox_education_got_it" msgid="4057634570866051177">"තේරුණා"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"වැඩිදුර තොරතුරු සඳහා දිග හරින්න"</string> + <string name="maximize_button_text" msgid="1650859196290301963">"විහිදන්න"</string> + <string name="minimize_button_text" msgid="271592547935841753">"කුඩා කරන්න"</string> + <string name="close_button_text" msgid="2913281996024033299">"වසන්න"</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 86769b6ee849..5478ce5d3d40 100644 --- a/libs/WindowManager/Shell/res/values-si/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-si/strings_tv.xml @@ -19,7 +19,16 @@ 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="9135220303720555525">"PIP වසන්න"</string> + <string name="pip_close" msgid="2955969519031223530">"වසන්න"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"සම්පූර්ණ තිරය"</string> - <string name="pip_move" msgid="1544227837964635439">"PIP ගෙන යන්න"</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_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.xml b/libs/WindowManager/Shell/res/values-sk/strings.xml index daa202175622..9dfbd54942e3 100644 --- a/libs/WindowManager/Shell/res/values-sk/strings.xml +++ b/libs/WindowManager/Shell/res/values-sk/strings.xml @@ -22,6 +22,7 @@ <string name="pip_phone_settings" msgid="5468987116750491918">"Nastavenia"</string> <string name="pip_phone_enter_split" msgid="7042877263880641911">"Prejsť na rozdelenú obrazovku"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Ponuka"</string> + <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Ponuka obrazu v obraze"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> je v režime obraz v obraze"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Ak nechcete, aby aplikácia <xliff:g id="NAME">%s</xliff:g> používala túto funkciu, klepnutím otvorte nastavenia a vypnite ju."</string> <string name="pip_play" msgid="3496151081459417097">"Prehrať"</string> @@ -36,6 +37,7 @@ <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Aplikácia nemusí fungovať na sekundárnej obrazovke."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Aplikácia nepodporuje spúšťanie na sekundárnych obrazovkách."</string> <string name="accessibility_divider" msgid="703810061635792791">"Rozdeľovač obrazovky"</string> + <string name="divider_title" msgid="5482989479865361192">"Rozdeľovač obrazovky"</string> <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Ľavá – na celú obrazovku"</string> <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Ľavá – 70 %"</string> <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Ľavá – 50 %"</string> @@ -72,17 +74,19 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Bublina"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Spravovať"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Bublina bola zavretá."</string> - <string name="restart_button_description" msgid="5887656107651190519">"Klepnutím reštartujete túto aplikáciu a prejdete do režimu celej obrazovky."</string> + <string name="restart_button_description" msgid="6712141648865547958">"Ak chcete zlepšiť zobrazenie, klepnutím túto aplikáciu reštartujte."</string> <string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"Problémy s kamerou?\nKlepnutím znova upravte."</string> <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Nevyriešilo sa to?\nKlepnutím sa vráťte."</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Nemáte problémy s kamerou? Klepnutím zatvoríte."</string> - <!-- no translation found for letterbox_education_dialog_title (6688664582871779215) --> + <!-- no translation found for letterbox_education_dialog_title (7739895354143295358) --> <skip /> - <!-- no translation found for letterbox_education_dialog_subtext (4853542518367719562) --> + <!-- no translation found for letterbox_education_split_screen_text (6206339484068670830) --> <skip /> - <!-- no translation found for letterbox_education_screen_rotation_text (5085786687366339027) --> - <skip /> - <!-- no translation found for letterbox_education_reposition_text (1068293354123934727) --> + <!-- no translation found for letterbox_education_reposition_text (4589957299813220661) --> <skip /> <string name="letterbox_education_got_it" msgid="4057634570866051177">"Dobre"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Po rozbalení sa dozviete viac."</string> + <string name="maximize_button_text" msgid="1650859196290301963">"Maximalizovať"</string> + <string name="minimize_button_text" msgid="271592547935841753">"Minimalizovať"</string> + <string name="close_button_text" msgid="2913281996024033299">"Zavrieť"</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 6f6ccb703cf6..1df43afca2da 100644 --- a/libs/WindowManager/Shell/res/values-sk/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-sk/strings_tv.xml @@ -19,7 +19,16 @@ 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="9135220303720555525">"Zavrieť režim PIP"</string> + <string name="pip_close" msgid="2955969519031223530">"Zavrieť"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Celá obrazovka"</string> - <string name="pip_move" msgid="1544227837964635439">"Presunúť obraz v obraze"</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_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.xml b/libs/WindowManager/Shell/res/values-sl/strings.xml index b4c7b951d14a..5bf943b96383 100644 --- a/libs/WindowManager/Shell/res/values-sl/strings.xml +++ b/libs/WindowManager/Shell/res/values-sl/strings.xml @@ -22,6 +22,7 @@ <string name="pip_phone_settings" msgid="5468987116750491918">"Nastavitve"</string> <string name="pip_phone_enter_split" msgid="7042877263880641911">"Vklopi razdeljen zaslon"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Meni"</string> + <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Meni za sliko v sliki"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> je v načinu slika v sliki"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Če ne želite, da aplikacija <xliff:g id="NAME">%s</xliff:g> uporablja to funkcijo, se dotaknite, da odprete nastavitve, in funkcijo izklopite."</string> <string name="pip_play" msgid="3496151081459417097">"Predvajaj"</string> @@ -36,6 +37,7 @@ <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Aplikacija morda ne bo delovala na sekundarnem zaslonu."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Aplikacija ne podpira zagona na sekundarnih zaslonih."</string> <string name="accessibility_divider" msgid="703810061635792791">"Razdelilnik zaslonov"</string> + <string name="divider_title" msgid="5482989479865361192">"Razdelilnik zaslonov"</string> <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Levi v celozaslonski način"</string> <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Levi 70 %"</string> <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Levi 50 %"</string> @@ -72,17 +74,19 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Mehurček"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Upravljanje"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Oblaček je bil opuščen."</string> - <string name="restart_button_description" msgid="5887656107651190519">"Dotaknite se za vnovični zagon te aplikacije in preklop v celozaslonski način."</string> + <string name="restart_button_description" msgid="6712141648865547958">"Če želite boljši prikaz, se dotaknite za vnovični zagon te aplikacije."</string> <string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"Težave s fotoaparatom?\nDotaknite se za vnovično prilagoditev"</string> <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"To ni odpravilo težave?\nDotaknite se za povrnitev"</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Nimate težav s fotoaparatom? Dotaknite se za opustitev."</string> - <!-- no translation found for letterbox_education_dialog_title (6688664582871779215) --> + <!-- no translation found for letterbox_education_dialog_title (7739895354143295358) --> <skip /> - <!-- no translation found for letterbox_education_dialog_subtext (4853542518367719562) --> + <!-- no translation found for letterbox_education_split_screen_text (6206339484068670830) --> <skip /> - <!-- no translation found for letterbox_education_screen_rotation_text (5085786687366339027) --> - <skip /> - <!-- no translation found for letterbox_education_reposition_text (1068293354123934727) --> + <!-- no translation found for letterbox_education_reposition_text (4589957299813220661) --> <skip /> <string name="letterbox_education_got_it" msgid="4057634570866051177">"V redu"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Razširitev za več informacij"</string> + <string name="maximize_button_text" msgid="1650859196290301963">"Maksimiraj"</string> + <string name="minimize_button_text" msgid="271592547935841753">"Minimiraj"</string> + <string name="close_button_text" msgid="2913281996024033299">"Zapri"</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 837794ad4be7..88fc8325aa01 100644 --- a/libs/WindowManager/Shell/res/values-sl/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-sl/strings_tv.xml @@ -19,7 +19,16 @@ 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="9135220303720555525">"Zapri način PIP"</string> + <string name="pip_close" msgid="2955969519031223530">"Zapri"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Celozaslonsko"</string> - <string name="pip_move" msgid="1544227837964635439">"Premakni sliko v sliki"</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_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.xml b/libs/WindowManager/Shell/res/values-sq/strings.xml index 5051351bf340..09559990dc37 100644 --- a/libs/WindowManager/Shell/res/values-sq/strings.xml +++ b/libs/WindowManager/Shell/res/values-sq/strings.xml @@ -22,6 +22,7 @@ <string name="pip_phone_settings" msgid="5468987116750491918">"Cilësimet"</string> <string name="pip_phone_enter_split" msgid="7042877263880641911">"Hyr në ekranin e ndarë"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Menyja"</string> + <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Menyja e \"Figurës brenda figurës\""</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> është në figurë brenda figurës"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Nëse nuk dëshiron që <xliff:g id="NAME">%s</xliff:g> ta përdorë këtë funksion, trokit për të hapur cilësimet dhe për ta çaktivizuar."</string> <string name="pip_play" msgid="3496151081459417097">"Luaj"</string> @@ -36,6 +37,7 @@ <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Aplikacioni mund të mos funksionojë në një ekran dytësor."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Aplikacioni nuk mbështet nisjen në ekrane dytësore."</string> <string name="accessibility_divider" msgid="703810061635792791">"Ndarësi i ekranit të ndarë"</string> + <string name="divider_title" msgid="5482989479865361192">"Ndarësi i ekranit të ndarë"</string> <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Ekrani i plotë majtas"</string> <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Majtas 70%"</string> <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Majtas 50%"</string> @@ -72,17 +74,19 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Flluskë"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Menaxho"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Flluska u hoq."</string> - <string name="restart_button_description" msgid="5887656107651190519">"Trokit për ta rinisur këtë aplikacion dhe për të kaluar në ekranin e plotë."</string> + <string name="restart_button_description" msgid="6712141648865547958">"Trokit për të rifilluar këtë aplikacion për një pamje më të mirë."</string> <string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"Ka probleme me kamerën?\nTrokit për ta ripërshtatur"</string> <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Nuk u rregullua?\nTrokit për ta rikthyer"</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Nuk ka probleme me kamerën? Trokit për ta shpërfillur."</string> - <!-- no translation found for letterbox_education_dialog_title (6688664582871779215) --> + <!-- no translation found for letterbox_education_dialog_title (7739895354143295358) --> <skip /> - <!-- no translation found for letterbox_education_dialog_subtext (4853542518367719562) --> + <!-- no translation found for letterbox_education_split_screen_text (6206339484068670830) --> <skip /> - <!-- no translation found for letterbox_education_screen_rotation_text (5085786687366339027) --> - <skip /> - <!-- no translation found for letterbox_education_reposition_text (1068293354123934727) --> + <!-- no translation found for letterbox_education_reposition_text (4589957299813220661) --> <skip /> <string name="letterbox_education_got_it" msgid="4057634570866051177">"E kuptova"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Zgjeroje për më shumë informacion."</string> + <string name="maximize_button_text" msgid="1650859196290301963">"Maksimizo"</string> + <string name="minimize_button_text" msgid="271592547935841753">"Minimizo"</string> + <string name="close_button_text" msgid="2913281996024033299">"Mbyll"</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 107870d0489f..58687e5867fe 100644 --- a/libs/WindowManager/Shell/res/values-sq/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-sq/strings_tv.xml @@ -19,7 +19,16 @@ 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="9135220303720555525">"Mbyll PIP"</string> + <string name="pip_close" msgid="2955969519031223530">"Mbyll"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Ekrani i plotë"</string> - <string name="pip_move" msgid="1544227837964635439">"Zhvendos PIP"</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_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.xml b/libs/WindowManager/Shell/res/values-sr/strings.xml index 96bb48a76368..b42d98e422ff 100644 --- a/libs/WindowManager/Shell/res/values-sr/strings.xml +++ b/libs/WindowManager/Shell/res/values-sr/strings.xml @@ -22,6 +22,7 @@ <string name="pip_phone_settings" msgid="5468987116750491918">"Подешавања"</string> <string name="pip_phone_enter_split" msgid="7042877263880641911">"Уђи на подељени екран"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Мени"</string> + <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Мени слике у слици."</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> је слика у слици"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Ако не желите да <xliff:g id="NAME">%s</xliff:g> користи ову функцију, додирните да бисте отворили подешавања и искључили је."</string> <string name="pip_play" msgid="3496151081459417097">"Пусти"</string> @@ -36,6 +37,7 @@ <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Апликација можда неће функционисати на секундарном екрану."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Апликација не подржава покретање на секундарним екранима."</string> <string name="accessibility_divider" msgid="703810061635792791">"Разделник подељеног екрана"</string> + <string name="divider_title" msgid="5482989479865361192">"Разделник подељеног екрана"</string> <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Режим целог екрана за леви екран"</string> <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Леви екран 70%"</string> <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Леви екран 50%"</string> @@ -72,17 +74,19 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Облачић"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Управљајте"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Облачић је одбачен."</string> - <string name="restart_button_description" msgid="5887656107651190519">"Додирните да бисте рестартовали апликацију и прешли у режим целог екрана."</string> + <string name="restart_button_description" msgid="6712141648865547958">"Додирните да бисте рестартовали ову апликацију ради бољег приказа."</string> <string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"Имате проблема са камером?\nДодирните да бисте поново уклопили"</string> <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Проблем није решен?\nДодирните да бисте вратили"</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Немате проблема са камером? Додирните да бисте одбацили."</string> - <!-- no translation found for letterbox_education_dialog_title (6688664582871779215) --> + <!-- no translation found for letterbox_education_dialog_title (7739895354143295358) --> <skip /> - <!-- no translation found for letterbox_education_dialog_subtext (4853542518367719562) --> + <!-- no translation found for letterbox_education_split_screen_text (6206339484068670830) --> <skip /> - <!-- no translation found for letterbox_education_screen_rotation_text (5085786687366339027) --> - <skip /> - <!-- no translation found for letterbox_education_reposition_text (1068293354123934727) --> + <!-- no translation found for letterbox_education_reposition_text (4589957299813220661) --> <skip /> <string name="letterbox_education_got_it" msgid="4057634570866051177">"Важи"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Проширите за још информација."</string> + <string name="maximize_button_text" msgid="1650859196290301963">"Увећајте"</string> + <string name="minimize_button_text" msgid="271592547935841753">"Умањите"</string> + <string name="close_button_text" msgid="2913281996024033299">"Затворите"</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 ee5690ba4a9a..e850979174a3 100644 --- a/libs/WindowManager/Shell/res/values-sr/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-sr/strings_tv.xml @@ -19,7 +19,16 @@ 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="9135220303720555525">"Затвори PIP"</string> + <string name="pip_close" msgid="2955969519031223530">"Затвори"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Цео екран"</string> - <string name="pip_move" msgid="1544227837964635439">"Премести слику у слици"</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_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.xml b/libs/WindowManager/Shell/res/values-sv/strings.xml index 9fa5c19e3dd4..162b57dbd224 100644 --- a/libs/WindowManager/Shell/res/values-sv/strings.xml +++ b/libs/WindowManager/Shell/res/values-sv/strings.xml @@ -22,6 +22,7 @@ <string name="pip_phone_settings" msgid="5468987116750491918">"Inställningar"</string> <string name="pip_phone_enter_split" msgid="7042877263880641911">"Starta delad skärm"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Meny"</string> + <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Bild-i-bild-meny"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> visas i bild-i-bild"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Om du inte vill att den här funktionen används i <xliff:g id="NAME">%s</xliff:g> öppnar du inställningarna genom att trycka. Sedan inaktiverar du funktionen."</string> <string name="pip_play" msgid="3496151081459417097">"Spela upp"</string> @@ -36,6 +37,7 @@ <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Appen kanske inte fungerar på en sekundär skärm."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Appen kan inte köras på en sekundär skärm."</string> <string name="accessibility_divider" msgid="703810061635792791">"Avdelare för delad skärm"</string> + <string name="divider_title" msgid="5482989479865361192">"Avdelare för delad skärm"</string> <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Helskärm på vänster skärm"</string> <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Vänster 70 %"</string> <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Vänster 50 %"</string> @@ -72,17 +74,19 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Bubbla"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Hantera"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Bubblan ignorerades."</string> - <string name="restart_button_description" msgid="5887656107651190519">"Tryck för att starta om appen i helskärmsläge."</string> + <string name="restart_button_description" msgid="6712141648865547958">"Tryck för att starta om appen och få en bättre vy."</string> <string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"Problem med kameran?\nTryck för att anpassa på nytt"</string> <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Löstes inte problemet?\nTryck för att återställa"</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Inga problem med kameran? Tryck för att ignorera."</string> - <!-- no translation found for letterbox_education_dialog_title (6688664582871779215) --> + <!-- no translation found for letterbox_education_dialog_title (7739895354143295358) --> <skip /> - <!-- no translation found for letterbox_education_dialog_subtext (4853542518367719562) --> + <!-- no translation found for letterbox_education_split_screen_text (6206339484068670830) --> <skip /> - <!-- no translation found for letterbox_education_screen_rotation_text (5085786687366339027) --> - <skip /> - <!-- no translation found for letterbox_education_reposition_text (1068293354123934727) --> + <!-- no translation found for letterbox_education_reposition_text (4589957299813220661) --> <skip /> <string name="letterbox_education_got_it" msgid="4057634570866051177">"OK"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Utöka för mer information."</string> + <string name="maximize_button_text" msgid="1650859196290301963">"Utöka"</string> + <string name="minimize_button_text" msgid="271592547935841753">"Minimera"</string> + <string name="close_button_text" msgid="2913281996024033299">"Stäng"</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 7355adf51e97..d3a9c3de66db 100644 --- a/libs/WindowManager/Shell/res/values-sv/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-sv/strings_tv.xml @@ -19,7 +19,16 @@ 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="9135220303720555525">"Stäng PIP"</string> + <string name="pip_close" msgid="2955969519031223530">"Stäng"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Helskärm"</string> - <string name="pip_move" msgid="1544227837964635439">"Flytta BIB"</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_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.xml b/libs/WindowManager/Shell/res/values-sw/strings.xml index 8c026f96392d..3844d01f5221 100644 --- a/libs/WindowManager/Shell/res/values-sw/strings.xml +++ b/libs/WindowManager/Shell/res/values-sw/strings.xml @@ -22,6 +22,7 @@ <string name="pip_phone_settings" msgid="5468987116750491918">"Mipangilio"</string> <string name="pip_phone_enter_split" msgid="7042877263880641911">"Weka skrini iliyogawanywa"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Menyu"</string> + <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Menyu ya kipengele cha Kupachika Picha ndani ya Picha nyingine."</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> iko katika hali ya picha ndani ya picha nyingine"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Ikiwa hutaki <xliff:g id="NAME">%s</xliff:g> itumie kipengele hiki, gusa ili ufungue mipangilio na uizime."</string> <string name="pip_play" msgid="3496151081459417097">"Cheza"</string> @@ -36,6 +37,7 @@ <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Huenda programu isifanye kazi kwenye dirisha lingine."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Programu hii haiwezi kufunguliwa kwenye madirisha mengine."</string> <string name="accessibility_divider" msgid="703810061635792791">"Kitenganishi cha skrini inayogawanywa"</string> + <string name="divider_title" msgid="5482989479865361192">"Kitenganishi cha kugawa skrini"</string> <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Skrini nzima ya kushoto"</string> <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Kushoto 70%"</string> <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Kushoto 50%"</string> @@ -72,17 +74,19 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Kiputo"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Dhibiti"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Umeondoa kiputo."</string> - <string name="restart_button_description" msgid="5887656107651190519">"Gusa ili uzime na uwashe programu hii, kisha nenda kwenye skrini nzima."</string> + <string name="restart_button_description" msgid="6712141648865547958">"Gusa ili uzime kisha uwashe programu hii, ili upate mwonekano bora."</string> <string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"Je, kuna hitilafu za kamera?\nGusa ili urekebishe"</string> <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Umeshindwa kurekebisha?\nGusa ili urejeshe nakala ya awali"</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Je, hakuna hitilafu za kamera? Gusa ili uondoe."</string> - <!-- no translation found for letterbox_education_dialog_title (6688664582871779215) --> + <!-- no translation found for letterbox_education_dialog_title (7739895354143295358) --> <skip /> - <!-- no translation found for letterbox_education_dialog_subtext (4853542518367719562) --> + <!-- no translation found for letterbox_education_split_screen_text (6206339484068670830) --> <skip /> - <!-- no translation found for letterbox_education_screen_rotation_text (5085786687366339027) --> - <skip /> - <!-- no translation found for letterbox_education_reposition_text (1068293354123934727) --> + <!-- no translation found for letterbox_education_reposition_text (4589957299813220661) --> <skip /> <string name="letterbox_education_got_it" msgid="4057634570866051177">"Nimeelewa"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Panua ili upate maelezo zaidi."</string> + <string name="maximize_button_text" msgid="1650859196290301963">"Panua"</string> + <string name="minimize_button_text" msgid="271592547935841753">"Punguza"</string> + <string name="close_button_text" msgid="2913281996024033299">"Funga"</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 0ee28416137a..7b9a310ff0b6 100644 --- a/libs/WindowManager/Shell/res/values-sw/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-sw/strings_tv.xml @@ -19,7 +19,16 @@ 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="9135220303720555525">"Funga PIP"</string> + <string name="pip_close" msgid="2955969519031223530">"Funga"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Skrini nzima"</string> - <string name="pip_move" msgid="1544227837964635439">"Kuhamisha PIP"</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_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.xml b/libs/WindowManager/Shell/res/values-ta/strings.xml index cb3d138035b2..c45409c3713c 100644 --- a/libs/WindowManager/Shell/res/values-ta/strings.xml +++ b/libs/WindowManager/Shell/res/values-ta/strings.xml @@ -22,6 +22,7 @@ <string name="pip_phone_settings" msgid="5468987116750491918">"அமைப்புகள்"</string> <string name="pip_phone_enter_split" msgid="7042877263880641911">"திரைப் பிரிப்பு பயன்முறைக்குச் செல்"</string> <string name="pip_menu_title" msgid="5393619322111827096">"மெனு"</string> + <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"பிக்ச்சர்-இன்-பிக்ச்சர் மெனு"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> தற்போது பிக்ச்சர்-இன்-பிக்ச்சரில் உள்ளது"</string> <string name="pip_notification_message" msgid="8854051911700302620">"<xliff:g id="NAME">%s</xliff:g> இந்த அம்சத்தைப் பயன்படுத்த வேண்டாம் என நினைத்தால் இங்கு தட்டி அமைப்புகளைத் திறந்து இதை முடக்கவும்."</string> <string name="pip_play" msgid="3496151081459417097">"இயக்கு"</string> @@ -36,6 +37,7 @@ <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"இரண்டாம்நிலைத் திரையில் ஆப்ஸ் வேலை செய்யாமல் போகக்கூடும்."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"இரண்டாம்நிலைத் திரைகளில் பயன்பாட்டைத் தொடங்க முடியாது."</string> <string name="accessibility_divider" msgid="703810061635792791">"திரையைப் பிரிக்கும் பிரிப்பான்"</string> + <string name="divider_title" msgid="5482989479865361192">"திரைப் பிரிப்பான்"</string> <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"இடது புறம் முழுத் திரை"</string> <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"இடது புறம் 70%"</string> <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"இடது புறம் 50%"</string> @@ -72,17 +74,19 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"பபிள்"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"நிர்வகி"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"குமிழ் நிராகரிக்கப்பட்டது."</string> - <string name="restart_button_description" msgid="5887656107651190519">"தட்டுவதன் மூலம் இந்த ஆப்ஸை மீண்டும் தொடங்கலாம், முழுத்திரையில் பார்க்கலாம்."</string> + <string name="restart_button_description" msgid="6712141648865547958">"இங்கு தட்டுவதன் மூலம் இந்த ஆப்ஸை மீண்டும் தொடங்கி, ஆப்ஸ் காட்டப்படும் விதத்தை இன்னும் சிறப்பாக்கலாம்."</string> <string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"கேமரா தொடர்பான சிக்கல்களா?\nமீண்டும் பொருத்த தட்டவும்"</string> <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"சிக்கல்கள் சரிசெய்யப்படவில்லையா?\nமாற்றியமைக்க தட்டவும்"</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"கேமரா தொடர்பான சிக்கல்கள் எதுவும் இல்லையா? நிராகரிக்க தட்டவும்."</string> - <!-- no translation found for letterbox_education_dialog_title (6688664582871779215) --> + <!-- no translation found for letterbox_education_dialog_title (7739895354143295358) --> <skip /> - <!-- no translation found for letterbox_education_dialog_subtext (4853542518367719562) --> + <!-- no translation found for letterbox_education_split_screen_text (6206339484068670830) --> <skip /> - <!-- no translation found for letterbox_education_screen_rotation_text (5085786687366339027) --> - <skip /> - <!-- no translation found for letterbox_education_reposition_text (1068293354123934727) --> + <!-- no translation found for letterbox_education_reposition_text (4589957299813220661) --> <skip /> <string name="letterbox_education_got_it" msgid="4057634570866051177">"சரி"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"கூடுதல் தகவல்களுக்கு விரிவாக்கலாம்."</string> + <string name="maximize_button_text" msgid="1650859196290301963">"பெரிதாக்கும்"</string> + <string name="minimize_button_text" msgid="271592547935841753">"சிறிதாக்கும்"</string> + <string name="close_button_text" msgid="2913281996024033299">"மூடும்"</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 8bcc43bea59a..e201401e2e35 100644 --- a/libs/WindowManager/Shell/res/values-ta/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-ta/strings_tv.xml @@ -19,7 +19,16 @@ 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="9135220303720555525">"PIPஐ மூடு"</string> + <string name="pip_close" msgid="2955969519031223530">"மூடுக"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"முழுத்திரை"</string> - <string name="pip_move" msgid="1544227837964635439">"PIPபை நகர்த்து"</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_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.xml b/libs/WindowManager/Shell/res/values-te/strings.xml index 7589e70cc681..0a61f937e3b6 100644 --- a/libs/WindowManager/Shell/res/values-te/strings.xml +++ b/libs/WindowManager/Shell/res/values-te/strings.xml @@ -22,6 +22,7 @@ <string name="pip_phone_settings" msgid="5468987116750491918">"సెట్టింగ్లు"</string> <string name="pip_phone_enter_split" msgid="7042877263880641911">"స్ప్లిట్ స్క్రీన్ను ఎంటర్ చేయండి"</string> <string name="pip_menu_title" msgid="5393619322111827096">"మెనూ"</string> + <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"పిక్చర్-ఇన్-పిక్చర్ మెనూ"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> చిత్రంలో చిత్రం రూపంలో ఉంది"</string> <string name="pip_notification_message" msgid="8854051911700302620">"<xliff:g id="NAME">%s</xliff:g> ఈ లక్షణాన్ని ఉపయోగించకూడదు అని మీరు అనుకుంటే, సెట్టింగ్లను తెరవడానికి ట్యాప్ చేసి, దీన్ని ఆఫ్ చేయండి."</string> <string name="pip_play" msgid="3496151081459417097">"ప్లే చేయి"</string> @@ -36,6 +37,7 @@ <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"ప్రత్యామ్నాయ డిస్ప్లేలో యాప్ పని చేయకపోవచ్చు."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"ప్రత్యామ్నాయ డిస్ప్లేల్లో ప్రారంభానికి యాప్ మద్దతు లేదు."</string> <string name="accessibility_divider" msgid="703810061635792791">"విభజన స్క్రీన్ విభాగిని"</string> + <string name="divider_title" msgid="5482989479865361192">"స్ప్లిట్ స్క్రీన్ డివైడర్"</string> <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"ఎడమవైపు ఫుల్-స్క్రీన్"</string> <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"ఎడమవైపు 70%"</string> <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"ఎడమవైపు 50%"</string> @@ -72,17 +74,19 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"బబుల్"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"మేనేజ్ చేయండి"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"బబుల్ విస్మరించబడింది."</string> - <string name="restart_button_description" msgid="5887656107651190519">"ఈ యాప్ను రీస్టార్ట్ చేయడానికి ట్యాప్ చేసి, ఆపై పూర్తి స్క్రీన్లోకి వెళ్లండి."</string> + <string name="restart_button_description" msgid="6712141648865547958">"మెరుగైన వీక్షణ కోసం ఈ యాప్ను రీస్టార్ట్ చేయడానికి ట్యాప్ చేయండి."</string> <string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"కెమెరా సమస్యలు ఉన్నాయా?\nరీఫిట్ చేయడానికి ట్యాప్ చేయండి"</string> <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"దాని సమస్యను పరిష్కరించలేదా?\nపూర్వస్థితికి మార్చడానికి ట్యాప్ చేయండి"</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"కెమెరా సమస్యలు లేవా? తీసివేయడానికి ట్యాప్ చేయండి."</string> - <!-- no translation found for letterbox_education_dialog_title (6688664582871779215) --> + <!-- no translation found for letterbox_education_dialog_title (7739895354143295358) --> <skip /> - <!-- no translation found for letterbox_education_dialog_subtext (4853542518367719562) --> + <!-- no translation found for letterbox_education_split_screen_text (6206339484068670830) --> <skip /> - <!-- no translation found for letterbox_education_screen_rotation_text (5085786687366339027) --> - <skip /> - <!-- no translation found for letterbox_education_reposition_text (1068293354123934727) --> + <!-- no translation found for letterbox_education_reposition_text (4589957299813220661) --> <skip /> <string name="letterbox_education_got_it" msgid="4057634570866051177">"అర్థమైంది"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"మరింత సమాచారం కోసం విస్తరించండి."</string> + <string name="maximize_button_text" msgid="1650859196290301963">"గరిష్టీకరించండి"</string> + <string name="minimize_button_text" msgid="271592547935841753">"కుదించండి"</string> + <string name="close_button_text" msgid="2913281996024033299">"మూసివేయండి"</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 6e80bd7b3a0c..6284d90cb11f 100644 --- a/libs/WindowManager/Shell/res/values-te/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-te/strings_tv.xml @@ -19,7 +19,16 @@ 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="9135220303720555525">"PIPని మూసివేయి"</string> + <string name="pip_close" msgid="2955969519031223530">"మూసివేయండి"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"ఫుల్-స్క్రీన్"</string> - <string name="pip_move" msgid="1544227837964635439">"PIPను తరలించండి"</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_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/dimen.xml b/libs/WindowManager/Shell/res/values-television/dimen.xml index 14e89f8b08df..376cc4faca6f 100644 --- a/libs/WindowManager/Shell/res/values-television/dimen.xml +++ b/libs/WindowManager/Shell/res/values-television/dimen.xml @@ -21,4 +21,7 @@ <!-- Padding between PIP and keep clear areas that caused it to move. --> <dimen name="pip_keep_clear_area_padding">16dp</dimen> + + <!-- The corner radius for PiP window. --> + <dimen name="pip_corner_radius">0dp</dimen> </resources> diff --git a/libs/WindowManager/Shell/res/values-th/strings.xml b/libs/WindowManager/Shell/res/values-th/strings.xml index d8a33ff4c8e5..0a41d450a0b5 100644 --- a/libs/WindowManager/Shell/res/values-th/strings.xml +++ b/libs/WindowManager/Shell/res/values-th/strings.xml @@ -22,6 +22,7 @@ <string name="pip_phone_settings" msgid="5468987116750491918">"การตั้งค่า"</string> <string name="pip_phone_enter_split" msgid="7042877263880641911">"เข้าสู่โหมดแบ่งหน้าจอ"</string> <string name="pip_menu_title" msgid="5393619322111827096">"เมนู"</string> + <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"เมนูการแสดงภาพซ้อนภาพ"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> ใช้การแสดงภาพซ้อนภาพ"</string> <string name="pip_notification_message" msgid="8854051911700302620">"หากคุณไม่ต้องการให้ <xliff:g id="NAME">%s</xliff:g> ใช้ฟีเจอร์นี้ ให้แตะเพื่อเปิดการตั้งค่าแล้วปิดฟีเจอร์"</string> <string name="pip_play" msgid="3496151081459417097">"เล่น"</string> @@ -36,6 +37,7 @@ <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"แอปอาจไม่ทำงานในจอแสดงผลรอง"</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"แอปไม่รองรับการเรียกใช้ในจอแสดงผลรอง"</string> <string name="accessibility_divider" msgid="703810061635792791">"เส้นแบ่งหน้าจอ"</string> + <string name="divider_title" msgid="5482989479865361192">"เส้นแยกหน้าจอ"</string> <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"เต็มหน้าจอทางซ้าย"</string> <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"ซ้าย 70%"</string> <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"ซ้าย 50%"</string> @@ -72,17 +74,19 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"บับเบิล"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"จัดการ"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"ปิดบับเบิลแล้ว"</string> - <string name="restart_button_description" msgid="5887656107651190519">"แตะเพื่อรีสตาร์ทแอปนี้และแสดงแบบเต็มหน้าจอ"</string> + <string name="restart_button_description" msgid="6712141648865547958">"แตะเพื่อรีสตาร์ทแอปนี้และรับมุมมองที่ดียิ่งขึ้น"</string> <string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"หากพบปัญหากับกล้อง\nแตะเพื่อแก้ไข"</string> <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"หากไม่ได้แก้ไข\nแตะเพื่อเปลี่ยนกลับ"</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"หากไม่พบปัญหากับกล้อง แตะเพื่อปิด"</string> - <!-- no translation found for letterbox_education_dialog_title (6688664582871779215) --> + <!-- no translation found for letterbox_education_dialog_title (7739895354143295358) --> <skip /> - <!-- no translation found for letterbox_education_dialog_subtext (4853542518367719562) --> + <!-- no translation found for letterbox_education_split_screen_text (6206339484068670830) --> <skip /> - <!-- no translation found for letterbox_education_screen_rotation_text (5085786687366339027) --> - <skip /> - <!-- no translation found for letterbox_education_reposition_text (1068293354123934727) --> + <!-- no translation found for letterbox_education_reposition_text (4589957299813220661) --> <skip /> <string name="letterbox_education_got_it" msgid="4057634570866051177">"รับทราบ"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"ขยายเพื่อดูข้อมูลเพิ่มเติม"</string> + <string name="maximize_button_text" msgid="1650859196290301963">"ขยายใหญ่สุด"</string> + <string name="minimize_button_text" msgid="271592547935841753">"ย่อ"</string> + <string name="close_button_text" msgid="2913281996024033299">"ปิด"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-th/strings_tv.xml b/libs/WindowManager/Shell/res/values-th/strings_tv.xml index b6f63699cc00..27cf56c6e154 100644 --- a/libs/WindowManager/Shell/res/values-th/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-th/strings_tv.xml @@ -19,7 +19,16 @@ 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="9135220303720555525">"ปิด PIP"</string> + <string name="pip_close" msgid="2955969519031223530">"ปิด"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"เต็มหน้าจอ"</string> - <string name="pip_move" msgid="1544227837964635439">"ย้าย PIP"</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_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.xml b/libs/WindowManager/Shell/res/values-tl/strings.xml index 35a58b33931d..e2727992777f 100644 --- a/libs/WindowManager/Shell/res/values-tl/strings.xml +++ b/libs/WindowManager/Shell/res/values-tl/strings.xml @@ -22,6 +22,7 @@ <string name="pip_phone_settings" msgid="5468987116750491918">"Mga Setting"</string> <string name="pip_phone_enter_split" msgid="7042877263880641911">"Pumasok sa split screen"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Menu"</string> + <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Menu ng Picture-in-Picture"</string> <string name="pip_notification_title" msgid="1347104727641353453">"Nasa picture-in-picture ang <xliff:g id="NAME">%s</xliff:g>"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Kung ayaw mong magamit ni <xliff:g id="NAME">%s</xliff:g> ang feature na ito, i-tap upang buksan ang mga setting at i-off ito."</string> <string name="pip_play" msgid="3496151081459417097">"I-play"</string> @@ -36,6 +37,7 @@ <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Maaaring hindi gumana ang app sa pangalawang display."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Hindi sinusuportahan ng app ang paglulunsad sa mga pangalawang display."</string> <string name="accessibility_divider" msgid="703810061635792791">"Divider ng split-screen"</string> + <string name="divider_title" msgid="5482989479865361192">"Divider ng split-screen"</string> <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"I-full screen ang nasa kaliwa"</string> <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Gawing 70% ang nasa kaliwa"</string> <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Gawing 50% ang nasa kaliwa"</string> @@ -72,17 +74,19 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Bubble"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Pamahalaan"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Na-dismiss na ang bubble."</string> - <string name="restart_button_description" msgid="5887656107651190519">"I-tap para i-restart ang app na ito at mag-full screen."</string> + <string name="restart_button_description" msgid="6712141648865547958">"I-tap para i-restart ang app na ito para sa mas magandang view."</string> <string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"May mga isyu sa camera?\nI-tap para i-refit"</string> <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Hindi ito naayos?\nI-tap para i-revert"</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Walang isyu sa camera? I-tap para i-dismiss."</string> - <!-- no translation found for letterbox_education_dialog_title (6688664582871779215) --> + <!-- no translation found for letterbox_education_dialog_title (7739895354143295358) --> <skip /> - <!-- no translation found for letterbox_education_dialog_subtext (4853542518367719562) --> + <!-- no translation found for letterbox_education_split_screen_text (6206339484068670830) --> <skip /> - <!-- no translation found for letterbox_education_screen_rotation_text (5085786687366339027) --> - <skip /> - <!-- no translation found for letterbox_education_reposition_text (1068293354123934727) --> + <!-- no translation found for letterbox_education_reposition_text (4589957299813220661) --> <skip /> <string name="letterbox_education_got_it" msgid="4057634570866051177">"OK"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"I-expand para sa higit pang impormasyon."</string> + <string name="maximize_button_text" msgid="1650859196290301963">"I-maximize"</string> + <string name="minimize_button_text" msgid="271592547935841753">"I-minimize"</string> + <string name="close_button_text" msgid="2913281996024033299">"Isara"</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 71ca2306ea03..4cc050bebe5b 100644 --- a/libs/WindowManager/Shell/res/values-tl/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-tl/strings_tv.xml @@ -19,7 +19,16 @@ 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="9135220303720555525">"Isara ang PIP"</string> + <string name="pip_close" msgid="2955969519031223530">"Isara"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Full screen"</string> - <string name="pip_move" msgid="1544227837964635439">"Ilipat ang PIP"</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_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.xml b/libs/WindowManager/Shell/res/values-tr/strings.xml index 8a9fb7546a20..050fa5fee690 100644 --- a/libs/WindowManager/Shell/res/values-tr/strings.xml +++ b/libs/WindowManager/Shell/res/values-tr/strings.xml @@ -22,6 +22,7 @@ <string name="pip_phone_settings" msgid="5468987116750491918">"Ayarlar"</string> <string name="pip_phone_enter_split" msgid="7042877263880641911">"Bölünmüş ekrana geç"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Menü"</string> + <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Pencere içinde pencere menüsü"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g>, pencere içinde pencere özelliğini kullanıyor"</string> <string name="pip_notification_message" msgid="8854051911700302620">"<xliff:g id="NAME">%s</xliff:g> uygulamasının bu özelliği kullanmasını istemiyorsanız dokunarak ayarları açın ve söz konusu özelliği kapatın."</string> <string name="pip_play" msgid="3496151081459417097">"Oynat"</string> @@ -36,6 +37,7 @@ <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Uygulama ikincil ekranda çalışmayabilir."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Uygulama ikincil ekranlarda başlatılmayı desteklemiyor."</string> <string name="accessibility_divider" msgid="703810061635792791">"Bölünmüş ekran ayırıcı"</string> + <string name="divider_title" msgid="5482989479865361192">"Bölünmüş ekran ayırıcı"</string> <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Solda tam ekran"</string> <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Solda %70"</string> <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Solda %50"</string> @@ -72,17 +74,19 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Baloncuk"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Yönet"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Balon kapatıldı."</string> - <string name="restart_button_description" msgid="5887656107651190519">"Bu uygulamayı yeniden başlatmak ve tam ekrana geçmek için dokunun."</string> + <string name="restart_button_description" msgid="6712141648865547958">"Bu uygulamayı yeniden başlatarak daha iyi bir görünüm elde etmek için dokunun."</string> <string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"Kameranızda sorun mu var?\nDüzeltmek için dokunun"</string> <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Bu işlem sorunu düzeltmedi mi?\nİşlemi geri almak için dokunun"</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Kameranızda sorun yok mu? Kapatmak için dokunun."</string> - <!-- no translation found for letterbox_education_dialog_title (6688664582871779215) --> + <!-- no translation found for letterbox_education_dialog_title (7739895354143295358) --> <skip /> - <!-- no translation found for letterbox_education_dialog_subtext (4853542518367719562) --> + <!-- no translation found for letterbox_education_split_screen_text (6206339484068670830) --> <skip /> - <!-- no translation found for letterbox_education_screen_rotation_text (5085786687366339027) --> - <skip /> - <!-- no translation found for letterbox_education_reposition_text (1068293354123934727) --> + <!-- no translation found for letterbox_education_reposition_text (4589957299813220661) --> <skip /> <string name="letterbox_education_got_it" msgid="4057634570866051177">"Anladım"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Daha fazla bilgi için genişletin."</string> + <string name="maximize_button_text" msgid="1650859196290301963">"Ekranı Kapla"</string> + <string name="minimize_button_text" msgid="271592547935841753">"Küçült"</string> + <string name="close_button_text" msgid="2913281996024033299">"Kapat"</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 e6ae7f167758..69bb608061e4 100644 --- a/libs/WindowManager/Shell/res/values-tr/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-tr/strings_tv.xml @@ -19,7 +19,16 @@ 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="9135220303720555525">"PIP\'yi kapat"</string> + <string name="pip_close" msgid="2955969519031223530">"Kapat"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Tam ekran"</string> - <string name="pip_move" msgid="1544227837964635439">"PIP\'yi taşı"</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> </resources> diff --git a/libs/WindowManager/Shell/res/values-tvdpi/dimen.xml b/libs/WindowManager/Shell/res/values-tvdpi/dimen.xml index 02e726fbc3bf..b45b9ec0c457 100644 --- a/libs/WindowManager/Shell/res/values-tvdpi/dimen.xml +++ b/libs/WindowManager/Shell/res/values-tvdpi/dimen.xml @@ -15,20 +15,20 @@ limitations under the License. --> <resources> - <!-- The dimensions to user for picture-in-picture action buttons. --> - <dimen name="pip_menu_button_size">48dp</dimen> - <dimen name="pip_menu_button_radius">20dp</dimen> - <dimen name="pip_menu_icon_size">20dp</dimen> - <dimen name="pip_menu_button_margin">4dp</dimen> - <dimen name="pip_menu_button_wrapper_margin">26dp</dimen> - <dimen name="pip_menu_border_width">4dp</dimen> - <integer name="pip_menu_fade_animation_duration">500</integer> + <!-- The dimensions to use for tv window menu action buttons. --> + <dimen name="tv_window_menu_button_size">48dp</dimen> + <dimen name="tv_window_menu_button_radius">20dp</dimen> + <dimen name="tv_window_menu_icon_size">20dp</dimen> + <dimen name="tv_window_menu_button_margin">4dp</dimen> + <integer name="tv_window_menu_fade_animation_duration">500</integer> <!-- The pip menu front border corner radius is 2dp smaller than the background corner radius to hide the background from showing through. --> <dimen name="pip_menu_border_corner_radius">4dp</dimen> <dimen name="pip_menu_background_corner_radius">6dp</dimen> + <dimen name="pip_menu_border_width">4dp</dimen> <dimen name="pip_menu_outer_space">24dp</dimen> + <dimen name="pip_menu_button_wrapper_margin">26dp</dimen> <!-- outer space minus border width --> <dimen name="pip_menu_outer_space_frame">20dp</dimen> diff --git a/libs/WindowManager/Shell/res/values-uk/strings.xml b/libs/WindowManager/Shell/res/values-uk/strings.xml index aac9031a7ca7..d5f047fcaf1f 100644 --- a/libs/WindowManager/Shell/res/values-uk/strings.xml +++ b/libs/WindowManager/Shell/res/values-uk/strings.xml @@ -22,6 +22,7 @@ <string name="pip_phone_settings" msgid="5468987116750491918">"Налаштування"</string> <string name="pip_phone_enter_split" msgid="7042877263880641911">"Розділити екран"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Меню"</string> + <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Меню \"Картинка в картинці\""</string> <string name="pip_notification_title" msgid="1347104727641353453">"У додатку <xliff:g id="NAME">%s</xliff:g> є функція \"Картинка в картинці\""</string> <string name="pip_notification_message" msgid="8854051911700302620">"Щоб додаток <xliff:g id="NAME">%s</xliff:g> не використовував цю функцію, вимкніть її в налаштуваннях."</string> <string name="pip_play" msgid="3496151081459417097">"Відтворити"</string> @@ -36,6 +37,7 @@ <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Додаток може не працювати на додатковому екрані."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Додаток не підтримує запуск на додаткових екранах."</string> <string name="accessibility_divider" msgid="703810061635792791">"Розділювач екрана"</string> + <string name="divider_title" msgid="5482989479865361192">"Розділювач екрана"</string> <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Ліве вікно на весь екран"</string> <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Ліве вікно на 70%"</string> <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Ліве вікно на 50%"</string> @@ -72,17 +74,19 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Спливаюче сповіщення"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Налаштувати"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Спливаюче сповіщення закрито."</string> - <string name="restart_button_description" msgid="5887656107651190519">"Натисніть, щоб перезапустити додаток і перейти в повноекранний режим."</string> + <string name="restart_button_description" msgid="6712141648865547958">"Натисніть, щоб перезапустити цей додаток для зручнішого перегляду."</string> <string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"Проблеми з камерою?\nНатисніть, щоб пристосувати"</string> <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Проблему не вирішено?\nНатисніть, щоб скасувати зміни"</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Немає проблем із камерою? Торкніться, щоб закрити."</string> - <!-- no translation found for letterbox_education_dialog_title (6688664582871779215) --> + <!-- no translation found for letterbox_education_dialog_title (7739895354143295358) --> <skip /> - <!-- no translation found for letterbox_education_dialog_subtext (4853542518367719562) --> + <!-- no translation found for letterbox_education_split_screen_text (6206339484068670830) --> <skip /> - <!-- no translation found for letterbox_education_screen_rotation_text (5085786687366339027) --> - <skip /> - <!-- no translation found for letterbox_education_reposition_text (1068293354123934727) --> + <!-- no translation found for letterbox_education_reposition_text (4589957299813220661) --> <skip /> <string name="letterbox_education_got_it" msgid="4057634570866051177">"ОK"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Розгорніть, щоб дізнатися більше."</string> + <string name="maximize_button_text" msgid="1650859196290301963">"Збільшити"</string> + <string name="minimize_button_text" msgid="271592547935841753">"Згорнути"</string> + <string name="close_button_text" msgid="2913281996024033299">"Закрити"</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 97e1f09844fa..81a8285c58cf 100644 --- a/libs/WindowManager/Shell/res/values-uk/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-uk/strings_tv.xml @@ -19,7 +19,16 @@ 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="9135220303720555525">"Закрити PIP"</string> + <string name="pip_close" msgid="2955969519031223530">"Закрити"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"На весь екран"</string> - <string name="pip_move" msgid="1544227837964635439">"Перемістити картинку в картинці"</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_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.xml b/libs/WindowManager/Shell/res/values-ur/strings.xml index e3bab32f309d..700ecaa7cfda 100644 --- a/libs/WindowManager/Shell/res/values-ur/strings.xml +++ b/libs/WindowManager/Shell/res/values-ur/strings.xml @@ -22,6 +22,7 @@ <string name="pip_phone_settings" msgid="5468987116750491918">"ترتیبات"</string> <string name="pip_phone_enter_split" msgid="7042877263880641911">"اسپلٹ اسکرین تک رسائی"</string> <string name="pip_menu_title" msgid="5393619322111827096">"مینو"</string> + <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"تصویر میں تصویر کا مینو"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> تصویر میں تصویر میں ہے"</string> <string name="pip_notification_message" msgid="8854051911700302620">"اگر آپ نہیں چاہتے ہیں کہ <xliff:g id="NAME">%s</xliff:g> اس خصوصیت کا استعمال کرے تو ترتیبات کھولنے کے لیے تھپتھپا کر اسے آف کرے۔"</string> <string name="pip_play" msgid="3496151081459417097">"چلائیں"</string> @@ -36,6 +37,7 @@ <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"ممکن ہے ایپ ثانوی ڈسپلے پر کام نہ کرے۔"</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"ایپ ثانوی ڈسپلیز پر شروعات کا تعاون نہیں کرتی۔"</string> <string name="accessibility_divider" msgid="703810061635792791">"سپلٹ اسکرین تقسیم کار"</string> + <string name="divider_title" msgid="5482989479865361192">"اسپلٹ اسکرین ڈیوائیڈر"</string> <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"بائیں فل اسکرین"</string> <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"بائیں %70"</string> <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"بائیں %50"</string> @@ -72,17 +74,19 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"بلبلہ"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"نظم کریں"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"بلبلہ برخاست کر دیا گیا۔"</string> - <string name="restart_button_description" msgid="5887656107651190519">"یہ ایپ دوبارہ شروع کرنے کے لیے تھپتھپائیں اور پوری اسکرین پر جائیں۔"</string> + <string name="restart_button_description" msgid="6712141648865547958">"بہتر منظر کے لیے اس ایپ کو ری اسٹارٹ کرنے کی خاطر تھپتھپائیں۔"</string> <string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"کیمرے کے مسائل؟\nدوبارہ فٹ کرنے کیلئے تھپتھپائیں"</string> <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"یہ حل نہیں ہوا؟\nلوٹانے کیلئے تھپتھپائیں"</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"کوئی کیمرے کا مسئلہ نہیں ہے؟ برخاست کرنے کیلئے تھپتھپائیں۔"</string> - <!-- no translation found for letterbox_education_dialog_title (6688664582871779215) --> + <!-- no translation found for letterbox_education_dialog_title (7739895354143295358) --> <skip /> - <!-- no translation found for letterbox_education_dialog_subtext (4853542518367719562) --> + <!-- no translation found for letterbox_education_split_screen_text (6206339484068670830) --> <skip /> - <!-- no translation found for letterbox_education_screen_rotation_text (5085786687366339027) --> - <skip /> - <!-- no translation found for letterbox_education_reposition_text (1068293354123934727) --> + <!-- no translation found for letterbox_education_reposition_text (4589957299813220661) --> <skip /> <string name="letterbox_education_got_it" msgid="4057634570866051177">"سمجھ آ گئی"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"مزید معلومات کے لیے پھیلائیں۔"</string> + <string name="maximize_button_text" msgid="1650859196290301963">"بڑا کریں"</string> + <string name="minimize_button_text" msgid="271592547935841753">"چھوٹا کریں"</string> + <string name="close_button_text" msgid="2913281996024033299">"بند کریں"</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 1418570f2538..e83885772f2d 100644 --- a/libs/WindowManager/Shell/res/values-ur/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-ur/strings_tv.xml @@ -19,7 +19,16 @@ 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="9135220303720555525">"PIP بند کریں"</string> + <string name="pip_close" msgid="2955969519031223530">"بند کریں"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"فُل اسکرین"</string> - <string name="pip_move" msgid="1544227837964635439">"PIP کو منتقل کریں"</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_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.xml b/libs/WindowManager/Shell/res/values-uz/strings.xml index 54ec89ae6b30..e843b0be5b1e 100644 --- a/libs/WindowManager/Shell/res/values-uz/strings.xml +++ b/libs/WindowManager/Shell/res/values-uz/strings.xml @@ -22,6 +22,7 @@ <string name="pip_phone_settings" msgid="5468987116750491918">"Sozlamalar"</string> <string name="pip_phone_enter_split" msgid="7042877263880641911">"Ajratilgan ekranga kirish"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Menyu"</string> + <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Tasvir ustida tasvir menyusi"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> tasvir ustida tasvir rejimida"</string> <string name="pip_notification_message" msgid="8854051911700302620">"<xliff:g id="NAME">%s</xliff:g> ilovasi uchun bu funksiyani sozlamalar orqali faolsizlantirish mumkin."</string> <string name="pip_play" msgid="3496151081459417097">"Ijro"</string> @@ -36,6 +37,7 @@ <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Bu ilova qo‘shimcha ekranda ishlamasligi mumkin."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Bu ilova qo‘shimcha ekranlarda ishga tushmaydi."</string> <string name="accessibility_divider" msgid="703810061635792791">"Ekranni ikkiga bo‘lish chizig‘i"</string> + <string name="divider_title" msgid="5482989479865361192">"Ekranni ikkiga ajratish chizigʻi"</string> <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Chapda to‘liq ekran"</string> <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Chapda 70%"</string> <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Chapda 50%"</string> @@ -72,17 +74,19 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Pufaklar"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Boshqarish"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Bulutcha yopildi."</string> - <string name="restart_button_description" msgid="5887656107651190519">"Bu ilovani qaytadan ishga tushirish va butun ekranda ochish uchun bosing."</string> + <string name="restart_button_description" msgid="6712141648865547958">"Yaxshiroq koʻrish maqsadida bu ilovani qayta ishga tushirish uchun bosing."</string> <string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"Kamera nosozmi?\nQayta moslash uchun bosing"</string> <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Tuzatilmadimi?\nQaytarish uchun bosing"</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Kamera muammosizmi? Yopish uchun bosing."</string> - <!-- no translation found for letterbox_education_dialog_title (6688664582871779215) --> + <!-- no translation found for letterbox_education_dialog_title (7739895354143295358) --> <skip /> - <!-- no translation found for letterbox_education_dialog_subtext (4853542518367719562) --> + <!-- no translation found for letterbox_education_split_screen_text (6206339484068670830) --> <skip /> - <!-- no translation found for letterbox_education_screen_rotation_text (5085786687366339027) --> - <skip /> - <!-- no translation found for letterbox_education_reposition_text (1068293354123934727) --> + <!-- no translation found for letterbox_education_reposition_text (4589957299813220661) --> <skip /> <string name="letterbox_education_got_it" msgid="4057634570866051177">"OK"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Batafsil axborot olish uchun kengaytiring."</string> + <string name="maximize_button_text" msgid="1650859196290301963">"Yoyish"</string> + <string name="minimize_button_text" msgid="271592547935841753">"Kichraytirish"</string> + <string name="close_button_text" msgid="2913281996024033299">"Yopish"</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 31c762ef5f64..da953356628c 100644 --- a/libs/WindowManager/Shell/res/values-uz/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-uz/strings_tv.xml @@ -19,7 +19,16 @@ 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="9135220303720555525">"Kadr ichida kadr – chiqish"</string> + <string name="pip_close" msgid="2955969519031223530">"Yopish"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Butun ekran"</string> - <string name="pip_move" msgid="1544227837964635439">"PIPni siljitish"</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_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.xml b/libs/WindowManager/Shell/res/values-vi/strings.xml index b6837023ccb8..ec1eadbfa988 100644 --- a/libs/WindowManager/Shell/res/values-vi/strings.xml +++ b/libs/WindowManager/Shell/res/values-vi/strings.xml @@ -22,6 +22,7 @@ <string name="pip_phone_settings" msgid="5468987116750491918">"Cài đặt"</string> <string name="pip_phone_enter_split" msgid="7042877263880641911">"Truy cập chế độ chia đôi màn hình"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Menu"</string> + <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Trình đơn hình trong hình."</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> đang ở chế độ ảnh trong ảnh"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Nếu bạn không muốn <xliff:g id="NAME">%s</xliff:g> sử dụng tính năng này, hãy nhấn để mở cài đặt và tắt tính năng này."</string> <string name="pip_play" msgid="3496151081459417097">"Phát"</string> @@ -36,6 +37,7 @@ <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Ứng dụng có thể không hoạt động trên màn hình phụ."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Ứng dụng không hỗ trợ khởi chạy trên màn hình phụ."</string> <string name="accessibility_divider" msgid="703810061635792791">"Bộ chia chia đôi màn hình"</string> + <string name="divider_title" msgid="5482989479865361192">"Bộ chia màn hình"</string> <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Toàn màn hình bên trái"</string> <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Trái 70%"</string> <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Trái 50%"</string> @@ -72,17 +74,19 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Bong bóng"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Quản lý"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Đã đóng bong bóng."</string> - <string name="restart_button_description" msgid="5887656107651190519">"Nhấn để khởi động lại ứng dụng này và xem ở chế độ toàn màn hình."</string> + <string name="restart_button_description" msgid="6712141648865547958">"Nhấn để khởi động lại ứng dụng này để xem tốt hơn."</string> <string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"Có vấn đề với máy ảnh?\nHãy nhấn để sửa lỗi"</string> <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Bạn chưa khắc phục vấn đề?\nHãy nhấn để hủy bỏ"</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Không có vấn đề với máy ảnh? Hãy nhấn để đóng."</string> - <!-- no translation found for letterbox_education_dialog_title (6688664582871779215) --> + <!-- no translation found for letterbox_education_dialog_title (7739895354143295358) --> <skip /> - <!-- no translation found for letterbox_education_dialog_subtext (4853542518367719562) --> + <!-- no translation found for letterbox_education_split_screen_text (6206339484068670830) --> <skip /> - <!-- no translation found for letterbox_education_screen_rotation_text (5085786687366339027) --> - <skip /> - <!-- no translation found for letterbox_education_reposition_text (1068293354123934727) --> + <!-- no translation found for letterbox_education_reposition_text (4589957299813220661) --> <skip /> <string name="letterbox_education_got_it" msgid="4057634570866051177">"OK"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Mở rộng để xem thêm thông tin."</string> + <string name="maximize_button_text" msgid="1650859196290301963">"Phóng to"</string> + <string name="minimize_button_text" msgid="271592547935841753">"Thu nhỏ"</string> + <string name="close_button_text" msgid="2913281996024033299">"Đóng"</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 b46cd49c1901..1f9260fdcff0 100644 --- a/libs/WindowManager/Shell/res/values-vi/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-vi/strings_tv.xml @@ -19,7 +19,16 @@ 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="9135220303720555525">"Đóng PIP"</string> + <string name="pip_close" msgid="2955969519031223530">"Đóng"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Toàn màn hình"</string> - <string name="pip_move" msgid="1544227837964635439">"Di chuyển PIP (Ảnh trong ả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_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.xml b/libs/WindowManager/Shell/res/values-zh-rCN/strings.xml index 811d8602a499..37d424402f0c 100644 --- a/libs/WindowManager/Shell/res/values-zh-rCN/strings.xml +++ b/libs/WindowManager/Shell/res/values-zh-rCN/strings.xml @@ -22,6 +22,7 @@ <string name="pip_phone_settings" msgid="5468987116750491918">"设置"</string> <string name="pip_phone_enter_split" msgid="7042877263880641911">"进入分屏模式"</string> <string name="pip_menu_title" msgid="5393619322111827096">"菜单"</string> + <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"画中画菜单"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g>目前位于“画中画”中"</string> <string name="pip_notification_message" msgid="8854051911700302620">"如果您不想让“<xliff:g id="NAME">%s</xliff:g>”使用此功能,请点按以打开设置,然后关闭此功能。"</string> <string name="pip_play" msgid="3496151081459417097">"播放"</string> @@ -36,6 +37,7 @@ <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"应用可能无法在辅显示屏上正常运行。"</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"应用不支持在辅显示屏上启动。"</string> <string name="accessibility_divider" msgid="703810061635792791">"分屏分隔线"</string> + <string name="divider_title" msgid="5482989479865361192">"分屏分隔线"</string> <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"左侧全屏"</string> <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"左侧 70%"</string> <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"左侧 50%"</string> @@ -72,17 +74,19 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"气泡"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"管理"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"已关闭对话泡。"</string> - <string name="restart_button_description" msgid="5887656107651190519">"点按即可重启此应用并进入全屏模式。"</string> + <string name="restart_button_description" msgid="6712141648865547958">"点按即可重启此应用,获得更好的视图体验。"</string> <string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"相机有问题?\n点按即可整修"</string> <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"没有解决此问题?\n点按即可恢复"</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"相机没有问题?点按即可忽略。"</string> - <!-- no translation found for letterbox_education_dialog_title (6688664582871779215) --> + <!-- no translation found for letterbox_education_dialog_title (7739895354143295358) --> <skip /> - <!-- no translation found for letterbox_education_dialog_subtext (4853542518367719562) --> + <!-- no translation found for letterbox_education_split_screen_text (6206339484068670830) --> <skip /> - <!-- no translation found for letterbox_education_screen_rotation_text (5085786687366339027) --> - <skip /> - <!-- no translation found for letterbox_education_reposition_text (1068293354123934727) --> + <!-- no translation found for letterbox_education_reposition_text (4589957299813220661) --> <skip /> <string name="letterbox_education_got_it" msgid="4057634570866051177">"知道了"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"展开即可了解详情。"</string> + <string name="maximize_button_text" msgid="1650859196290301963">"最大化"</string> + <string name="minimize_button_text" msgid="271592547935841753">"最小化"</string> + <string name="close_button_text" msgid="2913281996024033299">"关闭"</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 b6fec635a470..399d639fe70f 100644 --- a/libs/WindowManager/Shell/res/values-zh-rCN/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-zh-rCN/strings_tv.xml @@ -19,7 +19,16 @@ 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="9135220303720555525">"关闭画中画"</string> + <string name="pip_close" msgid="2955969519031223530">"关闭"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"全屏"</string> - <string name="pip_move" msgid="1544227837964635439">"移动画中画窗口"</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_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.xml b/libs/WindowManager/Shell/res/values-zh-rHK/strings.xml index 2a017148f7c4..170cd4cae753 100644 --- a/libs/WindowManager/Shell/res/values-zh-rHK/strings.xml +++ b/libs/WindowManager/Shell/res/values-zh-rHK/strings.xml @@ -22,6 +22,7 @@ <string name="pip_phone_settings" msgid="5468987116750491918">"設定"</string> <string name="pip_phone_enter_split" msgid="7042877263880641911">"進入分割螢幕"</string> <string name="pip_menu_title" msgid="5393619322111827096">"選單"</string> + <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"畫中畫選單"</string> <string name="pip_notification_title" msgid="1347104727641353453">"「<xliff:g id="NAME">%s</xliff:g>」目前在畫中畫模式"</string> <string name="pip_notification_message" msgid="8854051911700302620">"如果您不想「<xliff:g id="NAME">%s</xliff:g>」使用此功能,請輕按以開啟設定,然後停用此功能。"</string> <string name="pip_play" msgid="3496151081459417097">"播放"</string> @@ -36,6 +37,7 @@ <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"應用程式可能無法在次要顯示屏上運作。"</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"應用程式無法在次要顯示屏上啟動。"</string> <string name="accessibility_divider" msgid="703810061635792791">"分割畫面分隔線"</string> + <string name="divider_title" msgid="5482989479865361192">"分割螢幕分隔線"</string> <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"左邊全螢幕"</string> <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"左邊 70%"</string> <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"左邊 50%"</string> @@ -72,17 +74,19 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"氣泡"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"管理"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"對話氣泡已關閉。"</string> - <string name="restart_button_description" msgid="5887656107651190519">"輕按即可重新開啟此應用程式並放大至全螢幕。"</string> + <string name="restart_button_description" msgid="6712141648865547958">"輕按並重新啟動此應用程式,以取得更佳的觀看體驗。"</string> <string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"相機有問題?\n輕按即可修正"</string> <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"未能修正問題?\n輕按即可還原"</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"相機冇問題?㩒一下就可以即可閂咗佢。"</string> - <!-- no translation found for letterbox_education_dialog_title (6688664582871779215) --> + <!-- no translation found for letterbox_education_dialog_title (7739895354143295358) --> <skip /> - <!-- no translation found for letterbox_education_dialog_subtext (4853542518367719562) --> + <!-- no translation found for letterbox_education_split_screen_text (6206339484068670830) --> <skip /> - <!-- no translation found for letterbox_education_screen_rotation_text (5085786687366339027) --> - <skip /> - <!-- no translation found for letterbox_education_reposition_text (1068293354123934727) --> + <!-- no translation found for letterbox_education_reposition_text (4589957299813220661) --> <skip /> <string name="letterbox_education_got_it" msgid="4057634570866051177">"知道了"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"展開即可查看詳情。"</string> + <string name="maximize_button_text" msgid="1650859196290301963">"最大化"</string> + <string name="minimize_button_text" msgid="271592547935841753">"最小化"</string> + <string name="close_button_text" msgid="2913281996024033299">"關閉"</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 b5d54cb04354..acbc26d033cd 100644 --- a/libs/WindowManager/Shell/res/values-zh-rHK/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-zh-rHK/strings_tv.xml @@ -19,7 +19,16 @@ 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="9135220303720555525">"關閉 PIP"</string> + <string name="pip_close" msgid="2955969519031223530">"關閉"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"全螢幕"</string> - <string name="pip_move" msgid="1544227837964635439">"移動畫中畫"</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_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.xml b/libs/WindowManager/Shell/res/values-zh-rTW/strings.xml index 292a43912668..83f8520b2be4 100644 --- a/libs/WindowManager/Shell/res/values-zh-rTW/strings.xml +++ b/libs/WindowManager/Shell/res/values-zh-rTW/strings.xml @@ -22,6 +22,7 @@ <string name="pip_phone_settings" msgid="5468987116750491918">"設定"</string> <string name="pip_phone_enter_split" msgid="7042877263880641911">"進入分割畫面"</string> <string name="pip_menu_title" msgid="5393619322111827096">"選單"</string> + <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"子母畫面選單"</string> <string name="pip_notification_title" msgid="1347104727641353453">"「<xliff:g id="NAME">%s</xliff:g>」目前在子母畫面中"</string> <string name="pip_notification_message" msgid="8854051911700302620">"如果你不想讓「<xliff:g id="NAME">%s</xliff:g>」使用這項功能,請輕觸開啟設定頁面,然後停用此功能。"</string> <string name="pip_play" msgid="3496151081459417097">"播放"</string> @@ -36,6 +37,7 @@ <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"應用程式可能無法在次要顯示器上運作。"</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"應用程式無法在次要顯示器上啟動。"</string> <string name="accessibility_divider" msgid="703810061635792791">"分割畫面分隔線"</string> + <string name="divider_title" msgid="5482989479865361192">"分割畫面分隔線"</string> <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"以全螢幕顯示左側畫面"</string> <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"以 70% 的螢幕空間顯示左側畫面"</string> <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"以 50% 的螢幕空間顯示左側畫面"</string> @@ -72,17 +74,19 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"泡泡"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"管理"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"已關閉泡泡。"</string> - <string name="restart_button_description" msgid="5887656107651190519">"輕觸即可重新啟動這個應用程式並進入全螢幕模式。"</string> + <string name="restart_button_description" msgid="6712141648865547958">"請輕觸並重新啟動此應用程式,取得更良好的觀看體驗。"</string> <string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"相機有問題嗎?\n輕觸即可修正"</string> <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"未修正問題嗎?\n輕觸即可還原"</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"相機沒問題嗎?輕觸即可關閉。"</string> - <!-- no translation found for letterbox_education_dialog_title (6688664582871779215) --> + <!-- no translation found for letterbox_education_dialog_title (7739895354143295358) --> <skip /> - <!-- no translation found for letterbox_education_dialog_subtext (4853542518367719562) --> + <!-- no translation found for letterbox_education_split_screen_text (6206339484068670830) --> <skip /> - <!-- no translation found for letterbox_education_screen_rotation_text (5085786687366339027) --> - <skip /> - <!-- no translation found for letterbox_education_reposition_text (1068293354123934727) --> + <!-- no translation found for letterbox_education_reposition_text (4589957299813220661) --> <skip /> <string name="letterbox_education_got_it" msgid="4057634570866051177">"我知道了"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"展開即可查看詳細資訊。"</string> + <string name="maximize_button_text" msgid="1650859196290301963">"最大化"</string> + <string name="minimize_button_text" msgid="271592547935841753">"最小化"</string> + <string name="close_button_text" msgid="2913281996024033299">"關閉"</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 57db7a839ea2..f8c683ec3a60 100644 --- a/libs/WindowManager/Shell/res/values-zh-rTW/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-zh-rTW/strings_tv.xml @@ -19,7 +19,16 @@ 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="9135220303720555525">"關閉子母畫面"</string> + <string name="pip_close" msgid="2955969519031223530">"關閉"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"全螢幕"</string> - <string name="pip_move" msgid="1544227837964635439">"移動子母畫面"</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_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.xml b/libs/WindowManager/Shell/res/values-zu/strings.xml index 389eb08bf154..686310ac9881 100644 --- a/libs/WindowManager/Shell/res/values-zu/strings.xml +++ b/libs/WindowManager/Shell/res/values-zu/strings.xml @@ -22,6 +22,7 @@ <string name="pip_phone_settings" msgid="5468987116750491918">"Izilungiselelo"</string> <string name="pip_phone_enter_split" msgid="7042877263880641911">"Faka ukuhlukanisa isikrini"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Imenyu"</string> + <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Imenyu Yesithombe-Esithombeni"</string> <string name="pip_notification_title" msgid="1347104727641353453">"U-<xliff:g id="NAME">%s</xliff:g> ungaphakathi kwesithombe esiphakathi kwesithombe"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Uma ungafuni i-<xliff:g id="NAME">%s</xliff:g> ukuthi isebenzise lesi sici, thepha ukuze uvule izilungiselelo uphinde uyivale."</string> <string name="pip_play" msgid="3496151081459417097">"Dlala"</string> @@ -36,6 +37,7 @@ <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Uhlelo lokusebenza kungenzeka lungasebenzi kusibonisi sesibili."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Uhlelo lokusebenza alusekeli ukuqalisa kuzibonisi zesibili."</string> <string name="accessibility_divider" msgid="703810061635792791">"Isihlukanisi sokuhlukanisa isikrini"</string> + <string name="divider_title" msgid="5482989479865361192">"Isihlukanisi sokuhlukanisa isikrini"</string> <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Isikrini esigcwele esingakwesokunxele"</string> <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Kwesokunxele ngo-70%"</string> <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Kwesokunxele ngo-50%"</string> @@ -72,17 +74,19 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Ibhamuza"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Phatha"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Ibhamuza licashisiwe."</string> - <string name="restart_button_description" msgid="5887656107651190519">"Thepha ukuze uqale kabusha lolu hlelo lokusebenza uphinde uye kusikrini esigcwele."</string> + <string name="restart_button_description" msgid="6712141648865547958">"Thepha ukuze uqale kabusha le app ukuze ibonakale kangcono."</string> <string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"Izinkinga zekhamera?\nThepha ukuze uyilinganise kabusha"</string> <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Akuyilungisanga?\nThepha ukuze ubuyele"</string> <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Azikho izinkinga zekhamera? Thepha ukuze ucashise."</string> - <!-- no translation found for letterbox_education_dialog_title (6688664582871779215) --> + <!-- no translation found for letterbox_education_dialog_title (7739895354143295358) --> <skip /> - <!-- no translation found for letterbox_education_dialog_subtext (4853542518367719562) --> + <!-- no translation found for letterbox_education_split_screen_text (6206339484068670830) --> <skip /> - <!-- no translation found for letterbox_education_screen_rotation_text (5085786687366339027) --> - <skip /> - <!-- no translation found for letterbox_education_reposition_text (1068293354123934727) --> + <!-- no translation found for letterbox_education_reposition_text (4589957299813220661) --> <skip /> <string name="letterbox_education_got_it" msgid="4057634570866051177">"Ngiyezwa"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Nweba ukuze uthole ulwazi olwengeziwe"</string> + <string name="maximize_button_text" msgid="1650859196290301963">"Khulisa"</string> + <string name="minimize_button_text" msgid="271592547935841753">"Nciphisa"</string> + <string name="close_button_text" msgid="2913281996024033299">"Vala"</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 646a488e4c35..20243a9dfc9c 100644 --- a/libs/WindowManager/Shell/res/values-zu/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-zu/strings_tv.xml @@ -19,7 +19,16 @@ 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="9135220303720555525">"Vala i-PIP"</string> + <string name="pip_close" msgid="2955969519031223530">"Vala"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Iskrini esigcwele"</string> - <string name="pip_move" msgid="1544227837964635439">"Hambisa i-PIP"</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_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/colors_tv.xml b/libs/WindowManager/Shell/res/values/colors_tv.xml index fa90fe36b545..e6933ca3fce6 100644 --- a/libs/WindowManager/Shell/res/values/colors_tv.xml +++ b/libs/WindowManager/Shell/res/values/colors_tv.xml @@ -15,14 +15,17 @@ ~ limitations under the License. --> <resources> - <color name="tv_pip_menu_icon_focused">#0E0E0F</color> - <color name="tv_pip_menu_icon_unfocused">#F8F9FA</color> - <color name="tv_pip_menu_icon_disabled">#80868B</color> - <color name="tv_pip_menu_close_icon_bg_focused">#D93025</color> - <color name="tv_pip_menu_close_icon_bg_unfocused">#D69F261F</color> - <color name="tv_pip_menu_icon_bg_focused">#E8EAED</color> - <color name="tv_pip_menu_icon_bg_unfocused">#990E0E0F</color> + <color name="tv_window_menu_icon_focused">#0E0E0F</color> + <color name="tv_window_menu_icon_unfocused">#F8F9FA</color> + + <color name="tv_window_menu_icon_disabled">#80868B</color> + <color name="tv_window_menu_close_icon_bg_focused">#D93025</color> + <color name="tv_window_menu_close_icon_bg_unfocused">#D69F261F</color> + <color name="tv_window_menu_icon_bg_focused">#E8EAED</color> + <color name="tv_window_menu_icon_bg_unfocused">#990E0E0F</color> + <color name="tv_pip_menu_focus_border">#E8EAED</color> + <color name="tv_pip_menu_dim_layer">#990E0E0F</color> <color name="tv_pip_menu_background">#1E232C</color> <color name="tv_pip_edu_text">#99D2E3FC</color> diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml index 1dac9caba01e..5696b8dfa8e3 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> diff --git a/libs/WindowManager/Shell/res/values/strings.xml b/libs/WindowManager/Shell/res/values/strings.xml index a24311fb1f21..b48a508fddb8 100644 --- a/libs/WindowManager/Shell/res/values/strings.xml +++ b/libs/WindowManager/Shell/res/values/strings.xml @@ -30,6 +30,9 @@ <!-- Title of menu shown over picture-in-picture. Used for accessibility. --> <string name="pip_menu_title">Menu</string> + <!-- accessibility Title of menu shown over picture-in-picture [CHAR LIMIT=NONE] --> + <string name="pip_menu_accessibility_title">Picture-in-Picture Menu</string> + <!-- PiP BTW notification title. [CHAR LIMIT=50] --> <string name="pip_notification_title"><xliff:g id="name" example="Google Maps">%s</xliff:g> is in picture-in-picture</string> @@ -73,8 +76,10 @@ <!-- Warning message when we try to launch a non-resizeable activity on a secondary display and launch it on the primary instead. --> <string name="activity_launch_on_secondary_display_failed_text">App does not support launch on secondary displays.</string> - <!-- Accessibility label for the divider that separates the windows in split-screen mode [CHAR LIMIT=NONE] --> + <!-- Accessibility label and window tile for the divider that separates the windows in split-screen mode [CHAR LIMIT=NONE] --> <string name="accessibility_divider">Split-screen divider</string> + <!-- Accessibility window title for the split-screen divider window [CHAR LIMIT=NONE] --> + <string name="divider_title">Split-screen divider</string> <!-- Accessibility action for moving docked stack divider to make the left screen full screen [CHAR LIMIT=NONE] --> <string name="accessibility_action_divider_left_full">Left full screen</string> @@ -157,7 +162,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 +177,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> - - <!-- 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> + <string name="letterbox_education_dialog_title">See and do more</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/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..dec1e38914e2 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; @@ -42,10 +44,12 @@ import android.util.Log; import android.util.SparseArray; import android.view.SurfaceControl; import android.window.ITaskOrganizerController; +import android.window.ScreenCapture; import android.window.StartingWindowInfo; import android.window.StartingWindowRemovalInfo; import android.window.TaskAppearedInfo; import android.window.TaskOrganizer; +import android.window.WindowContainerTransaction; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.protolog.common.ProtoLog; @@ -55,6 +59,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 +77,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 +96,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 +181,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,15 +463,19 @@ 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())); } /** * Take a screenshot of a task. */ public void screenshotTask(RunningTaskInfo taskInfo, Rect crop, - Consumer<SurfaceControl.ScreenshotHardwareBuffer> consumer) { + Consumer<ScreenCapture.ScreenshotHardwareBuffer> consumer) { final TaskAppearedInfo info = mTasks.get(taskInfo.taskId); if (info == null) { return; @@ -458,6 +488,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 +517,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 +544,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 +560,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 +694,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 +871,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 +892,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/TEST_MAPPING b/libs/WindowManager/Shell/src/com/android/wm/shell/TEST_MAPPING new file mode 100644 index 000000000000..8dd1369ecbb2 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/TEST_MAPPING @@ -0,0 +1,15 @@ +{ + "ironwood-postsubmit": [ + { + "name": "WMShellFlickerTests", + "options": [ + { + "include-annotation": "android.platform.test.annotations.IwTest" + }, + { + "exclude-annotation": "org.junit.Ignore" + } + ] + } + ] +} 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..d88cc007c7b5 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java @@ -0,0 +1,315 @@ +/* + * 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 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; +import java.util.function.BiFunction; + +/** 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); + 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) { + for (TransitionInfo.Change change : info.getChanges()) { + if (change.getMode() == TRANSIT_CHANGE + && !change.getStartAbsBounds().equals(change.getEndAbsBounds())) { + return createChangeAnimationAdapters(info, startTransaction); + } + } + if (Transitions.isClosingType(info.getType())) { + return createCloseAnimationAdapters(info); + } + return createOpenAnimationAdapters(info); + } + + @NonNull + private List<ActivityEmbeddingAnimationAdapter> createOpenAnimationAdapters( + @NonNull TransitionInfo info) { + return createOpenCloseAnimationAdapters(info, true /* isOpening */, + mAnimationSpec::loadOpenAnimation); + } + + @NonNull + private List<ActivityEmbeddingAnimationAdapter> createCloseAnimationAdapters( + @NonNull TransitionInfo info) { + return createOpenCloseAnimationAdapters(info, 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, boolean isOpening, + @NonNull BiFunction<TransitionInfo.Change, Rect, Animation> 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( + change, animationProvider, openingWholeScreenBounds); + if (isOpening) { + adapter.overrideLayer(offsetLayer++); + } + adapters.add(adapter); + } + for (TransitionInfo.Change change : closingChanges) { + final ActivityEmbeddingAnimationAdapter adapter = createOpenCloseAnimationAdapter( + change, animationProvider, closingWholeScreenBounds); + if (!isOpening) { + adapter.overrideLayer(offsetLayer++); + } + adapters.add(adapter); + } + return adapters; + } + + @NonNull + private ActivityEmbeddingAnimationAdapter createOpenCloseAnimationAdapter( + @NonNull TransitionInfo.Change change, + @NonNull BiFunction<TransitionInfo.Change, Rect, Animation> animationProvider, + @NonNull Rect wholeAnimationBounds) { + final Animation animation = animationProvider.apply(change, wholeAnimationBounds); + 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 (!TransitionInfo.isIndependent(change, info)) { + // 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); + } +} 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..ad0dddf77002 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java @@ -0,0 +1,216 @@ +/* + * 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 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.R; +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.Change change, + @NonNull Rect wholeAnimationBounds) { + final boolean isEnter = Transitions.isOpeningType(change.getMode()); + final Animation animation; + // TODO(b/207070762): + // 1. Implement clearTop version: R.anim.task_fragment_clear_top_close_enter/exit + // 2. Implement edgeExtension version + animation = mTransitionAnimation.loadDefaultAnimationRes(isEnter + ? R.anim.task_fragment_open_enter + : 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.Change change, + @NonNull Rect wholeAnimationBounds) { + final boolean isEnter = Transitions.isOpeningType(change.getMode()); + final Animation animation; + // TODO(b/207070762): + // 1. Implement clearTop version: R.anim.task_fragment_clear_top_close_enter/exit + // 2. Implement edgeExtension version + animation = mTransitionAnimation.loadDefaultAnimationRes(isEnter + ? R.anim.task_fragment_close_enter + : 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; + } +} 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..ebf8c0354c1b 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 @@ -30,13 +30,20 @@ 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; @@ -50,6 +57,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; @@ -67,13 +75,14 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont SETTING_VALUE_ON) != SETTING_VALUE_OFF; private static final int PROGRESS_THRESHOLD = SystemProperties .getInt(PREDICTIVE_BACK_PROGRESS_THRESHOLD_PROP, -1); - private final AtomicBoolean mEnableAnimations = new AtomicBoolean(false); /** * Max duration to wait for a transition to finish before accepting another gesture start * request. */ private static final long MAX_TRANSITION_DURATION = 2000; + private final AtomicBoolean mEnableAnimations = new AtomicBoolean(false); + /** * Location of the initial touch event of the back gesture. */ @@ -83,13 +92,14 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont * 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 +108,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; @@ -105,19 +118,42 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont private final Runnable mResetTransitionRunnable = () -> { finishAnimation(); mTransitionInProgress = false; + ProtoLog.w(WM_SHELL_BACK_PREVIEW, "Transition didn't finish in %d ms. Resetting...", + MAX_TRANSITION_DURATION); + }; + + @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); + }); + } }; 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 +161,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( @@ -261,22 +303,30 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont if (mTransitionInProgress) { return; } - if (keyAction == MotionEvent.ACTION_MOVE) { + 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."); @@ -288,7 +338,8 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont try { boolean requestAnimation = mEnableAnimations.get(); - mBackNavigationInfo = mActivityTaskManager.startBackNavigation(requestAnimation); + mBackNavigationInfo = + mActivityTaskManager.startBackNavigation(requestAnimation, mFocusObserver); onBackNavigationInfoReceived(mBackNavigationInfo); } catch (RemoteException remoteException) { Log.e(TAG, "Failed to initAnimation", remoteException); @@ -300,7 +351,6 @@ 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(); @@ -376,11 +426,52 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont dispatchOnBackProgressed(targetCallback, backEvent); } - private void onGestureFinished() { + 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"); + } + } + + 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 @@ -403,7 +494,9 @@ 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 + && mBackNavigationInfo.getDepartingAnimationTarget() != null; } private static void dispatchOnBackStarted(IOnBackInvokedCallback callback) { @@ -468,16 +561,17 @@ 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); 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()) { 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..b5a575499a3a 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 @@ -170,7 +170,8 @@ public class Bubble implements BubbleViewProvider { @VisibleForTesting(visibility = PRIVATE) public Bubble(@NonNull final String key, @NonNull final ShortcutInfo shortcutInfo, final int desiredHeight, final int desiredHeightResId, @Nullable final String title, - int taskId, @Nullable final String locus, 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 +189,12 @@ public class Bubble implements BubbleViewProvider { mShowBubbleUpdateDot = false; mMainExecutor = mainExecutor; mTaskId = taskId; + mBubbleMetadataFlagListener = listener; } @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(); @@ -452,6 +454,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 +501,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 @@ -816,7 +823,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 +832,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..0dfba3464f23 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,19 @@ 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); + } + } } /** @@ -1050,18 +1050,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 +1081,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 +1131,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 +1195,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 +1337,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 +1358,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 +1434,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 +1484,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 +1575,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 +1691,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 +1704,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 +1725,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 +1758,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 +1835,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..54c91dde7668 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,20 +43,23 @@ 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; -import android.view.SurfaceControl; import android.view.View; import android.view.ViewGroup; import android.view.ViewOutlineProvider; import android.view.accessibility.AccessibilityNodeInfo; import android.widget.FrameLayout; import android.widget.LinearLayout; +import android.window.ScreenCapture; import androidx.annotation.Nullable; @@ -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; @@ -268,7 +330,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 +363,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 +447,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 +492,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; } @@ -445,7 +508,7 @@ public class BubbleExpandedView extends LinearLayout { /** Return a GraphicBuffer with the contents of the task view surface. */ @Nullable - SurfaceControl.ScreenshotHardwareBuffer snapshotActivitySurface() { + ScreenCapture.ScreenshotHardwareBuffer snapshotActivitySurface() { if (mIsOverflow) { // For now, just snapshot the view and return it as a hw buffer so that the animation // code for both the tasks and overflow can be the same @@ -454,7 +517,7 @@ public class BubbleExpandedView extends LinearLayout { p.beginRecording(mOverflowView.getWidth(), mOverflowView.getHeight())); p.endRecording(); Bitmap snapshot = Bitmap.createBitmap(p); - return new SurfaceControl.ScreenshotHardwareBuffer( + return new ScreenCapture.ScreenshotHardwareBuffer( snapshot.getHardwareBuffer(), snapshot.getColorSpace(), false /* containsSecureLayers */, @@ -463,7 +526,7 @@ public class BubbleExpandedView extends LinearLayout { if (mTaskView == null || mTaskView.getSurfaceControl() == null) { return null; } - return SurfaceControl.captureLayers( + return ScreenCapture.captureLayers( mTaskView.getSurfaceControl(), new Rect(0, 0, mTaskView.getWidth(), mTaskView.getHeight()), 1 /* scale */); @@ -510,12 +573,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 +587,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 +727,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 +744,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 +909,9 @@ public class BubbleExpandedView extends LinearLayout { mTaskView.onLocationChanged(); } if (mIsOverflow) { - mOverflowView.show(); + post(() -> { + mOverflowView.show(); + }); } } @@ -730,38 +954,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 +1044,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..2d9c2a9145ab 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,23 +45,25 @@ 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; import android.view.LayoutInflater; import android.view.MotionEvent; -import android.view.SurfaceControl; import android.view.SurfaceHolder; import android.view.SurfaceView; 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; import android.widget.ImageView; import android.widget.TextView; +import android.window.ScreenCapture; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -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; @@ -222,7 +244,7 @@ public class BubbleStackView extends FrameLayout * Buffer containing a screenshot of the animating-out bubble. This is drawn into the * SurfaceView during animations. */ - private SurfaceControl.ScreenshotHardwareBuffer mAnimatingOutBubbleBuffer; + private ScreenCapture.ScreenshotHardwareBuffer mAnimatingOutBubbleBuffer; private BubbleFlyoutView mFlyout; /** Runnable that fades out the flyout and then sets it to GONE. */ @@ -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/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..fad3dee1f927 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 @@ -19,6 +19,7 @@ package com.android.wm.shell.common; import android.graphics.PixelFormat; import android.graphics.Rect; import android.view.SurfaceControl; +import android.window.ScreenCapture; import java.util.function.Consumer; @@ -28,16 +29,16 @@ 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 * @param consumer Consumer for the captured buffer */ public static void captureLayer(SurfaceControl sc, Rect crop, - Consumer<SurfaceControl.ScreenshotHardwareBuffer> consumer) { - consumer.accept(SurfaceControl.captureLayers( - new SurfaceControl.LayerCaptureArgs.Builder(sc) + Consumer<ScreenCapture.ScreenshotHardwareBuffer> consumer) { + consumer.accept(ScreenCapture.captureLayers( + new ScreenCapture.LayerCaptureArgs.Builder(sc) .setSourceCrop(crop) .setCaptureSecureLayers(true) .setAllowProtected(true) @@ -45,20 +46,23 @@ public class ScreenshotUtils { } private static class BufferConsumer implements - Consumer<SurfaceControl.ScreenshotHardwareBuffer> { + Consumer<ScreenCapture.ScreenshotHardwareBuffer> { 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; } @Override - public void accept(SurfaceControl.ScreenshotHardwareBuffer buffer) { + public void accept(ScreenCapture.ScreenshotHardwareBuffer buffer) { if (buffer == null || buffer.getHardwareBuffer() == null) { return; } @@ -72,7 +76,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 +84,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 +95,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/pip/tv/TvPipMenuActionButton.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/TvWindowMenuActionButton.java index a09aab666a31..39b0b5500cea 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuActionButton.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/TvWindowMenuActionButton.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,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.pip.tv; +package com.android.wm.shell.common; import android.content.Context; import android.content.res.TypedArray; @@ -28,33 +28,32 @@ import android.widget.RelativeLayout; import com.android.wm.shell.R; /** - * A View that represents Pip Menu action button, such as "Fullscreen" and "Close" as well custom - * (provided by the application in Pip) and media buttons. + * A common action button for TV window menu layouts. */ -public class TvPipMenuActionButton extends RelativeLayout implements View.OnClickListener { +public class TvWindowMenuActionButton extends RelativeLayout implements View.OnClickListener { private final ImageView mIconImageView; private final View mButtonBackgroundView; private final View mButtonView; private OnClickListener mOnClickListener; - public TvPipMenuActionButton(Context context) { + public TvWindowMenuActionButton(Context context) { this(context, null, 0, 0); } - public TvPipMenuActionButton(Context context, AttributeSet attrs) { + public TvWindowMenuActionButton(Context context, AttributeSet attrs) { this(context, attrs, 0, 0); } - public TvPipMenuActionButton(Context context, AttributeSet attrs, int defStyleAttr) { + public TvWindowMenuActionButton(Context context, AttributeSet attrs, int defStyleAttr) { this(context, attrs, defStyleAttr, 0); } - public TvPipMenuActionButton( + public TvWindowMenuActionButton( Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); final LayoutInflater inflater = (LayoutInflater) getContext() .getSystemService(Context.LAYOUT_INFLATER_SERVICE); - inflater.inflate(R.layout.tv_pip_menu_action_button, this); + inflater.inflate(R.layout.tv_window_menu_action_button, this); mIconImageView = findViewById(R.id.icon); mButtonView = findViewById(R.id.button); @@ -74,8 +73,8 @@ public class TvPipMenuActionButton extends RelativeLayout implements View.OnClic @Override public void setOnClickListener(OnClickListener listener) { - // We do not want to set an OnClickListener to the TvPipMenuActionButton itself, but only to - // the ImageView. So let's "cash" the listener we've been passed here and set a "proxy" + // We do not want to set an OnClickListener to the TvWindowMenuActionButton itself, but only + // to the ImageView. So let's "cash" the listener we've been passed here and set a "proxy" // listener to the ImageView. mOnClickListener = listener; mButtonView.setOnClickListener(listener != null ? this : null); @@ -129,20 +128,27 @@ public class TvPipMenuActionButton extends RelativeLayout implements View.OnClic return mButtonView.isEnabled(); } - void setIsCustomCloseAction(boolean isCustomCloseAction) { + /** + * Marks this button as a custom close action button. + * This changes the style of the action button to highlight that this action finishes the + * Picture-in-Picture activity. + * + * @param isCustomCloseAction sets or unsets this button as a custom close action button. + */ + public void setIsCustomCloseAction(boolean isCustomCloseAction) { mIconImageView.setImageTintList( getResources().getColorStateList( - isCustomCloseAction ? R.color.tv_pip_menu_close_icon - : R.color.tv_pip_menu_icon)); + isCustomCloseAction ? R.color.tv_window_menu_close_icon + : R.color.tv_window_menu_icon)); mButtonBackgroundView.setBackgroundTintList(getResources() - .getColorStateList(isCustomCloseAction ? R.color.tv_pip_menu_close_icon_bg - : R.color.tv_pip_menu_icon_bg)); + .getColorStateList(isCustomCloseAction ? R.color.tv_window_menu_close_icon_bg + : R.color.tv_window_menu_icon_bg)); } @Override public String toString() { if (mButtonView.getContentDescription() == null) { - return TvPipMenuActionButton.class.getSimpleName(); + return TvWindowMenuActionButton.class.getSimpleName(); } return mButtonView.getContentDescription().toString(); } 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/common/split/SplitWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitWindowManager.java index 864b9a7528b0..7fea23797e67 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitWindowManager.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitWindowManager.java @@ -126,6 +126,7 @@ public final class SplitWindowManager extends WindowlessWindowManager { lp.token = new Binder(); lp.setTitle(mWindowName); lp.privateFlags |= PRIVATE_FLAG_NO_MOVE_ANIMATION | PRIVATE_FLAG_TRUSTED_OVERLAY; + lp.accessibilityTitle = mContext.getResources().getString(R.string.accessibility_divider); mViewHost.setView(mDividerView, lp); mDividerView.setup(splitLayout, this, mViewHost, insetsState); } 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/TvWMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvWMShellModule.java index 15bfeb297b41..e9957fd4f4f1 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvWMShellModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvWMShellModule.java @@ -16,16 +16,32 @@ package com.android.wm.shell.dagger; -import android.view.IWindowManager; +import android.content.Context; +import android.os.Handler; +import com.android.launcher3.icons.IconProvider; +import com.android.wm.shell.RootTaskDisplayAreaOrganizer; +import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayImeController; import com.android.wm.shell.common.DisplayInsetsController; import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.SyncTransactionQueue; +import com.android.wm.shell.common.SystemWindows; import com.android.wm.shell.common.TransactionPool; import com.android.wm.shell.common.annotations.ShellMainThread; +import com.android.wm.shell.draganddrop.DragAndDropController; +import com.android.wm.shell.recents.RecentTasksController; +import com.android.wm.shell.splitscreen.SplitScreenController; +import com.android.wm.shell.splitscreen.tv.TvSplitScreenController; import com.android.wm.shell.startingsurface.StartingWindowTypeAlgorithm; import com.android.wm.shell.startingsurface.tv.TvStartingWindowTypeAlgorithm; +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.util.Optional; import dagger.Module; import dagger.Provides; @@ -50,5 +66,33 @@ public class TvWMShellModule { @DynamicOverride static StartingWindowTypeAlgorithm provideStartingWindowTypeAlgorithm() { return new TvStartingWindowTypeAlgorithm(); - }; + } + + @WMSingleton + @Provides + @DynamicOverride + static SplitScreenController provideSplitScreenController(Context context, + ShellInit shellInit, + ShellCommandHandler shellCommandHandler, + ShellController shellController, + ShellTaskOrganizer shellTaskOrganizer, + SyncTransactionQueue syncQueue, + RootTaskDisplayAreaOrganizer rootTDAOrganizer, + DisplayController displayController, + DisplayImeController displayImeController, + DisplayInsetsController displayInsetsController, + DragAndDropController dragAndDropController, + Transitions transitions, + TransactionPool transactionPool, + IconProvider iconProvider, + Optional<RecentTasksController> recentTasks, + @ShellMainThread ShellExecutor mainExecutor, + Handler mainHandler, + SystemWindows systemWindows) { + return new TvSplitScreenController(context, shellInit, shellCommandHandler, shellController, + shellTaskOrganizer, syncQueue, rootTDAOrganizer, displayController, + displayImeController, displayInsetsController, dragAndDropController, transitions, + transactionPool, iconProvider, recentTasks, mainExecutor, mainHandler, + systemWindows); + } } 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..7c3c14e2210c 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 @@ -27,19 +27,15 @@ import android.view.IWindowManager; import com.android.internal.logging.UiEventLogger; import com.android.launcher3.icons.IconProvider; +import com.android.wm.shell.ProtoLogController; 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 +54,16 @@ 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.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.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 +79,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 +116,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 +155,59 @@ 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, - Context context, + static ShellTaskOrganizer provideShellTaskOrganizer( + 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); + 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 +262,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 +298,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 +334,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 +372,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 +391,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 +428,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 +447,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 +476,15 @@ public abstract class WMShellBaseModule { @Provides static Optional<RecentTasksController> provideRecentTasksController( Context context, + ShellInit shellInit, + ShellCommandHandler shellCommandHandler, TaskStackListenerImpl taskStackListener, + Optional<DesktopModeTaskRepository> desktopModeTaskRepository, @ShellMainThread ShellExecutor mainExecutor ) { return Optional.ofNullable( - RecentTasksController.create(context, taskStackListener, mainExecutor)); + RecentTasksController.create(context, shellInit, shellCommandHandler, + taskStackListener, desktopModeTaskRepository, mainExecutor)); } // @@ -492,12 +499,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 +571,6 @@ public abstract class WMShellBaseModule { return Optional.empty(); } - // Legacy split (optional feature) - - @WMSingleton - @Provides - static Optional<LegacySplitScreen> provideLegacySplitScreen( - Optional<LegacySplitScreenController> splitScreenController) { - return splitScreenController.map((controller) -> controller.asLegacySplitScreen()); - } - - @BindsOptionalOf - abstract LegacySplitScreenController optionalLegacySplitScreenController(); - - // App Pairs (optional feature) - - @WMSingleton - @Provides - static Optional<AppPairs> provideAppPairs(Optional<AppPairsController> appPairsController) { - return appPairsController.map((controller) -> controller.asAppPairs()); - } - - @BindsOptionalOf - abstract AppPairsController optionalAppPairs(); - // // Starting window // @@ -598,11 +585,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 +633,75 @@ public abstract class WMShellBaseModule { taskViewTransitions); } + + // + // ActivityEmbedding + // + + @WMSingleton + @Provides + static Optional<ActivityEmbeddingController> provideActivityEmbeddingController( + Context context, + ShellInit shellInit, + Transitions transitions) { + return Optional.ofNullable( + ActivityEmbeddingController.create(context, shellInit, transitions)); + } + // - // Misc + // 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) + // + + @BindsOptionalOf + @DynamicOverride + abstract DesktopModeTaskRepository optionalDesktopModeTaskRepository(); + + @WMSingleton + @Provides + static Optional<DesktopModeTaskRepository> providesDesktopModeTaskRepository( + @DynamicOverride Optional<DesktopModeTaskRepository> desktopModeTaskRepository) { + if (DesktopMode.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 +709,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..27d3e35fa07a 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,23 +16,27 @@ 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; import com.android.internal.logging.UiEventLogger; import com.android.internal.statusbar.IStatusBarService; import com.android.launcher3.icons.IconProvider; +import com.android.wm.shell.RootDisplayAreaOrganizer; 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 +47,17 @@ 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.DesktopMode; +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 +73,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 +115,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 +158,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 +169,88 @@ 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) { + return new CaptionWindowDecorViewModel( + context, + mainHandler, + mainChoreographer, + taskOrganizer, + displayController, + syncQueue); + } + + // // 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,35 @@ 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, + 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, pipAppOpsListener, pipBoundsAlgorithm, pipKeepClearAlgorithm, + pipBoundsState, pipMotionHelper, pipMediaController, phonePipMenuController, + pipTaskOrganizer, pipTransitionState, pipTouchHandler, pipTransitionController, + windowManagerShellWrapper, taskStackListener, pipParamsChangedForwarder, + displayInsetsController, oneHandedController, mainExecutor)); } @WMSingleton @@ -244,9 +360,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 +390,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 +443,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 +474,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 +502,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 +590,46 @@ public class WMShellModule { ); } + // + // Desktop mode (optional feature) + // + + @WMSingleton + @Provides + static Optional<DesktopModeController> provideDesktopModeController( + Context context, ShellInit shellInit, + ShellTaskOrganizer shellTaskOrganizer, + RootDisplayAreaOrganizer rootDisplayAreaOrganizer, + @ShellMainThread Handler mainHandler, + Transitions transitions + ) { + if (DesktopMode.IS_SUPPORTED) { + return Optional.of(new DesktopModeController(context, shellInit, shellTaskOrganizer, + rootDisplayAreaOrganizer, mainHandler, transitions)); + } else { + return Optional.empty(); + } + } + @WMSingleton @Provides - static PipParamsChangedForwarder providePipParamsChangedForwarder() { - return new PipParamsChangedForwarder(); + @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/desktopmode/DesktopMode.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java new file mode 100644 index 000000000000..8993d549964c --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.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 DesktopMode { + + /** + * 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/DesktopModeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java new file mode 100644 index 000000000000..c07ce1065302 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java @@ -0,0 +1,138 @@ +/* + * 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.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE; + +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.window.WindowContainerTransaction; + +import androidx.annotation.Nullable; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.protolog.common.ProtoLog; +import com.android.wm.shell.RootDisplayAreaOrganizer; +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.common.annotations.ShellMainThread; +import com.android.wm.shell.sysui.ShellInit; +import com.android.wm.shell.transition.Transitions; + +/** + * Handles windowing changes when desktop mode system setting changes + */ +public class DesktopModeController { + + private final Context mContext; + private final ShellTaskOrganizer mShellTaskOrganizer; + private final RootDisplayAreaOrganizer mRootDisplayAreaOrganizer; + private final SettingsObserver mSettingsObserver; + private final Transitions mTransitions; + + public DesktopModeController(Context context, ShellInit shellInit, + ShellTaskOrganizer shellTaskOrganizer, + RootDisplayAreaOrganizer rootDisplayAreaOrganizer, + @ShellMainThread Handler mainHandler, + Transitions transitions) { + mContext = context; + mShellTaskOrganizer = shellTaskOrganizer; + mRootDisplayAreaOrganizer = rootDisplayAreaOrganizer; + mSettingsObserver = new SettingsObserver(mContext, mainHandler); + mTransitions = transitions; + shellInit.addInitCallback(this::onInit, this); + } + + private void onInit() { + ProtoLog.d(WM_SHELL_DESKTOP_MODE, "Initialize DesktopModeController"); + mSettingsObserver.observe(); + if (DesktopMode.isActive(mContext)) { + updateDesktopModeActive(true); + } + } + + @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 */); + } + wct.merge(mRootDisplayAreaOrganizer.prepareWindowingModeChange(displayId, + targetWindowingMode), true /* transfer */); + if (Transitions.ENABLE_SHELL_TRANSITIONS) { + mTransitions.startTransition(TRANSIT_CHANGE, wct, null); + } else { + mRootDisplayAreaOrganizer.applyTransaction(wct); + } + } + + /** + * 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 = DesktopMode.isActive(mContext); + updateDesktopModeActive(enabled); + } + } +} 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/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/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..ac95d4bc63d1 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.DesktopMode; +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 (DesktopMode.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 (DesktopMode.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 (DesktopMode.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,96 @@ 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 adopts the window decoration; {@code false} otherwise + */ + void createWindowDecoration( + TransitionInfo.Change change, + SurfaceControl.Transaction startT, + SurfaceControl.Transaction finishT) { + final State<T> state = createOrUpdateTaskState(change.getTaskInfo(), change.getLeash()); + state.mWindowDecoration = mWindowDecorationViewModel.createWindowDecoration( + state.mTaskInfo, state.mLeash, startT, finishT); + } + + /** + * 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); + } + 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 +287,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..a780ec102ea9 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java @@ -0,0 +1,222 @@ +/* + * 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 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<>(); + 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_OPEN: + 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 (type == Transitions.TRANSIT_MAXIMIZE + && taskInfo.getWindowingMode() == WINDOWING_MODE_FULLSCREEN) { + windowDecor = mFreeformTaskListener.giveWindowDecoration( + change.getTaskInfo(), startT, finishT); + adopted = mFullscreenTaskListener.adoptWindowDecoration( + change, startT, finishT, windowDecor); + } + + if (type == Transitions.TRANSIT_RESTORE_FROM_MAXIMIZE + && taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM) { + windowDecor = mFullscreenTaskListener.giveWindowDecoration( + change.getTaskInfo(), startT, finishT); + adopted = mFreeformTaskListener.adoptWindowDecoration( + change, startT, finishT, windowDecor); + } + + 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..7d1259a732c9 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,285 @@ 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 + */ + public void createWindowDecoration(TransitionInfo.Change change, + SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT) { + final State<T> state = createOrUpdateTaskState(change.getTaskInfo(), change.getLeash()); + if (!mWindowDecorViewModelOptional.isPresent()) return; + T newWindowDecor = mWindowDecorViewModelOptional.get().createWindowDecoration( + state.mTaskInfo, state.mLeash, startT, finishT); + if (newWindowDecor != null) { + state.mWindowDecoration = newWindowDecor; + } + } + + /** + * 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 +318,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 +336,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..b32c3eed2fb4 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; } @@ -279,14 +278,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 +294,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 +350,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 +360,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 +410,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 +445,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 +585,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/PipMenuController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipMenuController.java index 16f1d1c2944c..000624499f79 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipMenuController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipMenuController.java @@ -26,11 +26,14 @@ import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; import android.annotation.Nullable; import android.app.ActivityManager.RunningTaskInfo; import android.app.RemoteAction; +import android.content.Context; import android.graphics.PixelFormat; import android.graphics.Rect; import android.view.SurfaceControl; import android.view.WindowManager; +import com.android.wm.shell.R; + import java.util.List; /** @@ -97,16 +100,20 @@ public interface PipMenuController { /** * Returns a default LayoutParams for the PIP Menu. + * @param context the context. * @param width the PIP stack width. * @param height the PIP stack height. */ - default WindowManager.LayoutParams getPipMenuLayoutParams(String title, int width, int height) { + default WindowManager.LayoutParams getPipMenuLayoutParams(Context context, String title, + int width, int height) { final WindowManager.LayoutParams lp = new WindowManager.LayoutParams(width, height, TYPE_APPLICATION_OVERLAY, FLAG_WATCH_OUTSIDE_TOUCH | FLAG_SPLIT_TOUCH | FLAG_SLIPPERY | FLAG_NOT_TOUCHABLE, PixelFormat.TRANSLUCENT); lp.privateFlags |= PRIVATE_FLAG_TRUSTED_OVERLAY; lp.setTitle(title); + lp.accessibilityTitle = context.getResources().getString( + R.string.pip_menu_accessibility_title); return lp; } } 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..1a52d8c395ba 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,24 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, mTaskOrganizer.addFocusListener(this); mPipTransitionController.setPipOrganizer(this); displayController.addDisplayWindowListener(this); + pipTransitionController.registerPipTransitionCallback(mPipTransitionCallback); + } + + public PipTransitionController getTransitionController() { + return mPipTransitionController; + } + + /** + * Returns true if the PiP window is currently being animated. + */ + public boolean isAnimating() { + // TODO(b/183746978) move this to PipAnimationController, and inject that in PipController + PipAnimationController.PipTransitionAnimator animator = + mPipAnimationController.getCurrentAnimator(); + if (animator != null && animator.isRunning()) { + return true; + } + return false; } public Rect getCurrentOrAnimatingBounds() { @@ -440,7 +480,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 +498,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 +579,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 +808,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 +961,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 +980,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 +1319,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 +1516,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 +1595,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..29434f73e84b 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 @@ -116,7 +116,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..a0a8f9fb2cde 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() { @@ -181,7 +186,7 @@ public class PhonePipMenuController implements PipMenuController { mPipMenuView = new PipMenuView(mContext, this, mMainExecutor, mMainHandler, mSplitScreenController, mPipUiEventLogger); mSystemWindows.addView(mPipMenuView, - getPipMenuLayoutParams(MENU_WINDOW_TITLE, 0 /* width */, 0 /* height */), + getPipMenuLayoutParams(mContext, MENU_WINDOW_TITLE, 0 /* width */, 0 /* height */), 0, SHELL_ROOT_LAYER_PIP); setShellRootAccessibilityWindow(); @@ -194,7 +199,6 @@ public class PhonePipMenuController implements PipMenuController { return; } - mApplier = null; mSystemWindows.removeView(mPipMenuView); mPipMenuView = null; } @@ -206,7 +210,7 @@ public class PhonePipMenuController implements PipMenuController { @Override public void updateMenuBounds(Rect destinationBounds) { mSystemWindows.updateViewLayout(mPipMenuView, - getPipMenuLayoutParams(MENU_WINDOW_TITLE, destinationBounds.width(), + getPipMenuLayoutParams(mContext, MENU_WINDOW_TITLE, destinationBounds.width(), destinationBounds.height())); updateMenuLayout(destinationBounds); } @@ -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..bc8191d2af46 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,9 +110,21 @@ 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; @@ -110,27 +133,72 @@ public class PipController implements PipTransitionController.PipTransitionCallb 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 (mPipTaskOrganizer.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 +224,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 +315,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 +337,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 +363,26 @@ 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, + 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 +391,35 @@ public class PipController implements PipTransitionController.PipTransitionCallb return null; } - return new PipController(context, displayController, pipAppOpsListener, pipBoundsAlgorithm, - pipBoundsState, pipMediaController, - phonePipMenuController, pipTaskOrganizer, pipTouchHandler, pipTransitionController, + return new PipController(context, shellInit, shellCommandHandler, shellController, + displayController, pipAppOpsListener, pipBoundsAlgorithm, pipKeepClearAlgorithm, + pipBoundsState, pipMotionHelper, pipMediaController, phonePipMenuController, + pipTaskOrganizer, pipTransitionState, pipTouchHandler, pipTransitionController, windowManagerShellWrapper, taskStackListener, pipParamsChangedForwarder, - oneHandedController, mainExecutor) + displayInsetsController, oneHandedController, mainExecutor) .mImpl; } protected PipController(Context context, + ShellInit shellInit, + ShellCommandHandler shellCommandHandler, + ShellController shellController, DisplayController displayController, 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,12 +430,17 @@ 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; @@ -349,12 +453,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 +468,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 +572,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 +596,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 +620,12 @@ public class PipController implements PipTransitionController.PipTransitionCallb } }); }); + + mMediaController.registerSessionListenerForCurrentUser(); + + mShellController.addConfigurationChangeListener(this); + mShellController.addKeyguardChangeListener(this); + mShellController.addUserChangeListener(this); } @Override @@ -501,18 +638,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 +688,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 +763,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 +803,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 +836,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 +1019,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 +1053,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 +1060,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 +1100,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 +1144,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/OWNERS b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/OWNERS index 85441af9a870..5aa3c4e2abef 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/OWNERS +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/OWNERS @@ -1,2 +1,3 @@ # WM shell sub-module TV pip owner galinap@google.com +bronger@google.com 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..a4b70333850c 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 @@ -29,7 +29,6 @@ import android.content.Context; import android.content.res.Resources; import android.graphics.Insets; import android.graphics.Rect; -import android.util.ArraySet; import android.util.Size; import android.view.Gravity; @@ -39,6 +38,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; @@ -49,11 +49,10 @@ import java.util.Set; * Contains pip bounds calculations that are specific to TV. */ public class TvPipBoundsAlgorithm extends PipBoundsAlgorithm { - private static final String TAG = TvPipBoundsAlgorithm.class.getSimpleName(); - private static final boolean DEBUG = TvPipController.DEBUG; - private final @NonNull TvPipBoundsState mTvPipBoundsState; + @NonNull + private final TvPipBoundsState mTvPipBoundsState; private int mFixedExpandedHeightInPx; private int mFixedExpandedWidthInPx; @@ -63,7 +62,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); @@ -90,10 +90,9 @@ public class TvPipBoundsAlgorithm extends PipBoundsAlgorithm { /** Returns the destination bounds to place the PIP window on entry. */ @Override public Rect getEntryDestinationBounds() { - if (DEBUG) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: getEntryDestinationBounds()", TAG); - } + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: getEntryDestinationBounds()", TAG); + updateExpandedPipSize(); final boolean isPipExpanded = mTvPipBoundsState.isTvExpandedPipSupported() && mTvPipBoundsState.getDesiredTvExpandedAspectRatio() != 0 @@ -108,10 +107,8 @@ public class TvPipBoundsAlgorithm extends PipBoundsAlgorithm { /** Returns the current bounds adjusted to the new aspect ratio, if valid. */ @Override public Rect getAdjustedDestinationBounds(Rect currentBounds, float newAspectRatio) { - if (DEBUG) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: getAdjustedDestinationBounds: %f", TAG, newAspectRatio); - } + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: getAdjustedDestinationBounds: %f", TAG, newAspectRatio); return adjustBoundsForTemporaryDecor(getTvPipPlacement().getBounds()); } @@ -139,25 +136,9 @@ public class TvPipBoundsAlgorithm extends PipBoundsAlgorithm { final Rect insetBounds = new Rect(); getInsetBounds(insetBounds); - Set<Rect> restrictedKeepClearAreas = mTvPipBoundsState.getRestrictedKeepClearAreas(); - Set<Rect> unrestrictedKeepClearAreas = mTvPipBoundsState.getUnrestrictedKeepClearAreas(); - - if (mTvPipBoundsState.isImeShowing()) { - if (DEBUG) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: IME showing, height: %d", - TAG, mTvPipBoundsState.getImeHeight()); - } - - final Rect imeBounds = new Rect( - 0, - insetBounds.bottom - mTvPipBoundsState.getImeHeight(), - insetBounds.right, - insetBounds.bottom); - - unrestrictedKeepClearAreas = new ArraySet<>(unrestrictedKeepClearAreas); - unrestrictedKeepClearAreas.add(imeBounds); - } + final Set<Rect> restrictedKeepClearAreas = mTvPipBoundsState.getRestrictedKeepClearAreas(); + final Set<Rect> unrestrictedKeepClearAreas = + mTvPipBoundsState.getUnrestrictedKeepClearAreas(); mKeepClearAlgorithm.setGravity(mTvPipBoundsState.getTvPipGravity()); mKeepClearAlgorithm.setScreenSize(screenSize); @@ -171,24 +152,22 @@ public class TvPipBoundsAlgorithm extends PipBoundsAlgorithm { restrictedKeepClearAreas, unrestrictedKeepClearAreas); - if (DEBUG) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: screenSize: %s", TAG, screenSize); - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: stashOffset: %d", TAG, mTvPipBoundsState.getStashOffset()); - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: insetBounds: %s", TAG, insetBounds.toShortString()); - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: pipSize: %s", TAG, pipSize); - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: gravity: %s", TAG, Gravity.toString(mTvPipBoundsState.getTvPipGravity())); - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: restrictedKeepClearAreas: %s", TAG, restrictedKeepClearAreas); - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: unrestrictedKeepClearAreas: %s", TAG, unrestrictedKeepClearAreas); - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: placement: %s", TAG, placement); - } + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: screenSize: %s", TAG, screenSize); + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: stashOffset: %d", TAG, mTvPipBoundsState.getStashOffset()); + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: insetBounds: %s", TAG, insetBounds.toShortString()); + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: pipSize: %s", TAG, pipSize); + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: gravity: %s", TAG, Gravity.toString(mTvPipBoundsState.getTvPipGravity())); + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: restrictedKeepClearAreas: %s", TAG, restrictedKeepClearAreas); + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: unrestrictedKeepClearAreas: %s", TAG, unrestrictedKeepClearAreas); + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: placement: %s", TAG, placement); return placement; } @@ -197,13 +176,11 @@ public class TvPipBoundsAlgorithm extends PipBoundsAlgorithm { * @return previous gravity if it is to be saved, or {@link Gravity#NO_GRAVITY} if not. */ int updateGravityOnExpandToggled(int previousGravity, boolean expanding) { - if (DEBUG) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: updateGravityOnExpandToggled(), expanding: %b" - + ", mOrientation: %d, previous gravity: %s", - TAG, expanding, mTvPipBoundsState.getTvFixedPipOrientation(), - Gravity.toString(previousGravity)); - } + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: updateGravityOnExpandToggled(), expanding: %b" + + ", mOrientation: %d, previous gravity: %s", + TAG, expanding, mTvPipBoundsState.getTvFixedPipOrientation(), + Gravity.toString(previousGravity)); if (!mTvPipBoundsState.isTvExpandedPipSupported()) { return Gravity.NO_GRAVITY; @@ -254,10 +231,8 @@ public class TvPipBoundsAlgorithm extends PipBoundsAlgorithm { } } mTvPipBoundsState.setTvPipGravity(updatedGravity); - if (DEBUG) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: new gravity: %s", TAG, Gravity.toString(updatedGravity)); - } + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: new gravity: %s", TAG, Gravity.toString(updatedGravity)); return gravityToSave; } @@ -266,10 +241,8 @@ public class TvPipBoundsAlgorithm extends PipBoundsAlgorithm { * @return true if gravity changed */ boolean updateGravity(int keycode) { - if (DEBUG) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: updateGravity, keycode: %d", TAG, keycode); - } + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: updateGravity, keycode: %d", TAG, keycode); // Check if position change is valid if (mTvPipBoundsState.isTvPipExpanded()) { @@ -326,10 +299,8 @@ public class TvPipBoundsAlgorithm extends PipBoundsAlgorithm { if (updatedGravity != currentGravity) { mTvPipBoundsState.setTvPipGravity(updatedGravity); - if (DEBUG) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: new gravity: %s", TAG, Gravity.toString(updatedGravity)); - } + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: new gravity: %s", TAG, Gravity.toString(updatedGravity)); return true; } return false; @@ -360,8 +331,8 @@ public class TvPipBoundsAlgorithm extends PipBoundsAlgorithm { final Size expandedSize; if (expandedRatio == 0) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: updateExpandedPipSize(): Expanded mode aspect ratio" - + " of 0 not supported", TAG); + "%s: updateExpandedPipSize(): Expanded mode aspect ratio" + + " of 0 not supported", TAG); return; } else if (expandedRatio < 1) { // vertical @@ -373,16 +344,12 @@ public class TvPipBoundsAlgorithm extends PipBoundsAlgorithm { float aspectRatioHeight = mFixedExpandedWidthInPx / expandedRatio; if (maxHeight > aspectRatioHeight) { - if (DEBUG) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: Accommodate aspect ratio", TAG); - } + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: Accommodate aspect ratio", TAG); expandedSize = new Size(mFixedExpandedWidthInPx, (int) aspectRatioHeight); } else { - if (DEBUG) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: Aspect ratio is too extreme, use max size", TAG); - } + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: Aspect ratio is too extreme, use max size", TAG); expandedSize = new Size(mFixedExpandedWidthInPx, maxHeight); } } @@ -395,26 +362,20 @@ public class TvPipBoundsAlgorithm extends PipBoundsAlgorithm { - pipDecorations.left - pipDecorations.right; float aspectRatioWidth = mFixedExpandedHeightInPx * expandedRatio; if (maxWidth > aspectRatioWidth) { - if (DEBUG) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: Accommodate aspect ratio", TAG); - } + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: Accommodate aspect ratio", TAG); expandedSize = new Size((int) aspectRatioWidth, mFixedExpandedHeightInPx); } else { - if (DEBUG) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: Aspect ratio is too extreme, use max size", TAG); - } + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: Aspect ratio is too extreme, use max size", TAG); expandedSize = new Size(maxWidth, mFixedExpandedHeightInPx); } } } mTvPipBoundsState.setTvExpandedSize(expandedSize); - if (DEBUG) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: updateExpandedPipSize(): expanded size, width: %d, height: %d", - TAG, expandedSize.getWidth(), expandedSize.getHeight()); - } + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: updateExpandedPipSize(): expanded size, width: %d, height: %d", + TAG, expandedSize.getWidth(), expandedSize.getHeight()); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsController.java index 3a6ce81821ec..b189163a354a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsController.java @@ -39,7 +39,6 @@ import java.util.function.Supplier; * Manages debouncing of PiP movements and scheduling of unstashing. */ public class TvPipBoundsController { - private static final boolean DEBUG = false; private static final String TAG = "TvPipBoundsController"; /** @@ -122,9 +121,9 @@ public class TvPipBoundsController { cancelScheduledPlacement(); applyPlacementBounds(placement.getUnstashedBounds(), animationDuration); } else if (immediate) { + boolean shouldStash = mUnstashRunnable != null || placement.getTriggerStash(); cancelScheduledPlacement(); - applyPlacementBounds(placement.getBounds(), animationDuration); - scheduleUnstashIfNeeded(placement); + applyPlacement(placement, shouldStash, animationDuration); } else { applyPlacementBounds(mCurrentPlacementBounds, animationDuration); schedulePinnedStackPlacement(placement, animationDuration); @@ -133,11 +132,9 @@ public class TvPipBoundsController { private void schedulePinnedStackPlacement(@NonNull final Placement placement, int animationDuration) { - if (DEBUG) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: schedulePinnedStackPlacement() - pip bounds: %s", - TAG, placement.getBounds().toShortString()); - } + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: schedulePinnedStackPlacement() - pip bounds: %s", + TAG, placement.getBounds().toShortString()); if (mPendingPlacement != null && Objects.equals(mPendingPlacement.getBounds(), placement.getBounds())) { @@ -171,27 +168,24 @@ public class TvPipBoundsController { } private void applyPendingPlacement() { - if (DEBUG) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: applyPendingPlacement()", TAG); - } + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: applyPendingPlacement()", TAG); if (mPendingPlacement != null) { - if (mPendingStash) { - mPendingStash = false; - scheduleUnstashIfNeeded(mPendingPlacement); - } - - if (mUnstashRunnable != null) { - // currently stashed, use stashed pos - applyPlacementBounds(mPendingPlacement.getBounds(), - mPendingPlacementAnimationDuration); - } else { - applyPlacementBounds(mPendingPlacement.getUnstashedBounds(), - mPendingPlacementAnimationDuration); - } + applyPlacement(mPendingPlacement, mPendingStash, mPendingPlacementAnimationDuration); + mPendingStash = false; + mPendingPlacement = null; } + } - mPendingPlacement = null; + private void applyPlacement(@NonNull final Placement placement, boolean shouldStash, + int animationDuration) { + if (placement.getStashType() != STASH_TYPE_NONE && shouldStash) { + scheduleUnstashIfNeeded(placement); + } + + Rect bounds = + mUnstashRunnable != null ? placement.getBounds() : placement.getUnstashedBounds(); + applyPlacementBounds(bounds, animationDuration); } void onPipDismissed() { @@ -227,10 +221,8 @@ public class TvPipBoundsController { } mPipTargetBounds = bounds; - if (DEBUG) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: movePipTo() - new pip bounds: %s", TAG, bounds.toShortString()); - } + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: movePipTo() - new pip bounds: %s", TAG, bounds.toShortString()); if (mListener != null) { mListener.onPipTargetBoundsChange(bounds, animationDuration); 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..85a544b2e8de 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,9 +67,9 @@ 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; private static final int NONEXISTENT_TASK_ID = -1; @@ -73,7 +79,8 @@ public class TvPipController implements PipTransitionController.PipTransitionCal STATE_PIP, STATE_PIP_MENU, }) - public @interface State {} + public @interface State { + } /** * State when there is no applications in Pip. @@ -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,10 +109,16 @@ 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(); - private @State int mState = STATE_NO_PIP; + @State + private int mState = STATE_NO_PIP; private int mPreviousGravity = TvPipBoundsState.DEFAULT_TV_GRAVITY; private int mPinnedTaskId = NONEXISTENT_TASK_ID; @@ -117,6 +131,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 +149,8 @@ public class TvPipController implements PipTransitionController.PipTransitionCal ShellExecutor mainExecutor) { return new TvPipController( context, + shellInit, + shellController, tvPipBoundsState, tvPipBoundsAlgorithm, tvPipBoundsController, @@ -151,6 +169,8 @@ public class TvPipController implements PipTransitionController.PipTransitionCal private TvPipController( Context context, + ShellInit shellInit, + ShellController shellController, TvPipBoundsState tvPipBoundsState, TvPipBoundsAlgorithm tvPipBoundsAlgorithm, TvPipBoundsController tvPipBoundsController, @@ -163,10 +183,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,27 +207,42 @@ 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); } - private void onConfigurationChanged(Configuration newConfig) { - if (DEBUG) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: onConfigurationChanged(), state=%s", TAG, stateToName(mState)); - } + @Override + public void onUserChanged(int newUserId, @NonNull Context userContext) { + // Re-register the media session listener when switching users + registerSessionListenerForCurrentUser(); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: onConfigurationChanged(), state=%s", TAG, stateToName(mState)); if (isPipShown()) { - if (DEBUG) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: > closing Pip.", TAG); - } + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: > closing Pip.", TAG); closePip(); } @@ -228,16 +265,12 @@ public class TvPipController implements PipTransitionController.PipTransitionCal */ @Override public void showPictureInPictureMenu() { - if (DEBUG) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: showPictureInPictureMenu(), state=%s", TAG, stateToName(mState)); - } + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: showPictureInPictureMenu(), state=%s", TAG, stateToName(mState)); if (mState == STATE_NO_PIP) { - if (DEBUG) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: > cannot open Menu from the current state.", TAG); - } + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: > cannot open Menu from the current state.", TAG); return; } @@ -248,10 +281,8 @@ public class TvPipController implements PipTransitionController.PipTransitionCal @Override public void onMenuClosed() { - if (DEBUG) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: closeMenu(), state before=%s", TAG, stateToName(mState)); - } + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: closeMenu(), state before=%s", TAG, stateToName(mState)); setState(STATE_PIP); updatePinnedStackBounds(); } @@ -266,10 +297,8 @@ public class TvPipController implements PipTransitionController.PipTransitionCal */ @Override public void movePipToFullscreen() { - if (DEBUG) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: movePipToFullscreen(), state=%s", TAG, stateToName(mState)); - } + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: movePipToFullscreen(), state=%s", TAG, stateToName(mState)); mPipTaskOrganizer.exitPip(mResizeAnimationDuration, false /* requestEnterSplit */); onPipDisappeared(); @@ -277,10 +306,8 @@ public class TvPipController implements PipTransitionController.PipTransitionCal @Override public void togglePipExpansion() { - if (DEBUG) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: togglePipExpansion()", TAG); - } + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: togglePipExpansion()", TAG); boolean expanding = !mTvPipBoundsState.isTvPipExpanded(); int saveGravity = mTvPipBoundsAlgorithm .updateGravityOnExpandToggled(mPreviousGravity, expanding); @@ -307,10 +334,8 @@ public class TvPipController implements PipTransitionController.PipTransitionCal mPreviousGravity = Gravity.NO_GRAVITY; updatePinnedStackBounds(); } else { - if (DEBUG) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: Position hasn't changed", TAG); - } + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: Position hasn't changed", TAG); } } @@ -363,11 +388,9 @@ public class TvPipController implements PipTransitionController.PipTransitionCal */ @Override public void closePip() { - if (DEBUG) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: closePip(), state=%s, loseAction=%s", TAG, stateToName(mState), - mCloseAction); - } + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: closePip(), state=%s, loseAction=%s", TAG, stateToName(mState), + mCloseAction); if (mCloseAction != null) { try { @@ -403,10 +426,8 @@ public class TvPipController implements PipTransitionController.PipTransitionCal private void checkIfPinnedTaskAppeared() { final TaskInfo pinnedTask = getPinnedTaskInfo(); - if (DEBUG) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: checkIfPinnedTaskAppeared(), task=%s", TAG, pinnedTask); - } + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: checkIfPinnedTaskAppeared(), task=%s", TAG, pinnedTask); if (pinnedTask == null || pinnedTask.topActivity == null) return; mPinnedTaskId = pinnedTask.taskId; @@ -415,10 +436,8 @@ public class TvPipController implements PipTransitionController.PipTransitionCal } private void checkIfPinnedTaskIsGone() { - if (DEBUG) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: onTaskStackChanged()", TAG); - } + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: onTaskStackChanged()", TAG); if (isPipShown() && getPinnedTaskInfo() == null) { ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, @@ -428,10 +447,8 @@ public class TvPipController implements PipTransitionController.PipTransitionCal } private void onPipDisappeared() { - if (DEBUG) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: onPipDisappeared() state=%s", TAG, stateToName(mState)); - } + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: onPipDisappeared() state=%s", TAG, stateToName(mState)); mPipNotificationController.dismiss(); mTvPipMenuController.closeMenu(); @@ -443,19 +460,15 @@ public class TvPipController implements PipTransitionController.PipTransitionCal @Override public void onPipTransitionStarted(int direction, Rect pipBounds) { - if (DEBUG) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: onPipTransition_Started(), state=%s", TAG, stateToName(mState)); - } + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: onPipTransition_Started(), state=%s", TAG, stateToName(mState)); mTvPipMenuController.notifyPipAnimating(true); } @Override public void onPipTransitionCanceled(int direction) { - if (DEBUG) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: onPipTransition_Canceled(), state=%s", TAG, stateToName(mState)); - } + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: onPipTransition_Canceled(), state=%s", TAG, stateToName(mState)); mTvPipMenuController.notifyPipAnimating(false); } @@ -464,19 +477,15 @@ public class TvPipController implements PipTransitionController.PipTransitionCal if (PipAnimationController.isInPipDirection(direction) && mState == STATE_NO_PIP) { setState(STATE_PIP); } - if (DEBUG) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: onPipTransition_Finished(), state=%s", TAG, stateToName(mState)); - } + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: onPipTransition_Finished(), state=%s", TAG, stateToName(mState)); mTvPipMenuController.notifyPipAnimating(false); } private void setState(@State int state) { - if (DEBUG) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: setState(), state=%s, prev=%s", - TAG, stateToName(state), stateToName(mState)); - } + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: setState(), state=%s, prev=%s", + TAG, stateToName(state), stateToName(mState)); mState = state; } @@ -510,11 +519,8 @@ public class TvPipController implements PipTransitionController.PipTransitionCal public void onActivityRestartAttempt(ActivityManager.RunningTaskInfo task, boolean homeTaskVisible, boolean clearedTask, boolean wasVisible) { if (task.getWindowingMode() == WINDOWING_MODE_PINNED) { - if (DEBUG) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: onPinnedActivityRestartAttempt()", TAG); - } - + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: onPinnedActivityRestartAttempt()", TAG); // If the "Pip-ed" Activity is launched again by Launcher or intent, make it // fullscreen. movePipToFullscreen(); @@ -529,7 +535,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal public void onActionsChanged(List<RemoteAction> actions, RemoteAction closeAction) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: onActionsChanged()", TAG); + "%s: onActionsChanged()", TAG); mTvPipMenuController.setAppActions(actions, closeAction); mCloseAction = closeAction; @@ -538,7 +544,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal @Override public void onAspectRatioChanged(float ratio) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: onAspectRatioChanged: %f", TAG, ratio); + "%s: onAspectRatioChanged: %f", TAG, ratio); mTvPipBoundsState.setAspectRatio(ratio); if (!mTvPipBoundsState.isTvPipExpanded()) { @@ -549,7 +555,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal @Override public void onExpandedAspectRatioChanged(float ratio) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: onExpandedAspectRatioChanged: %f", TAG, ratio); + "%s: onExpandedAspectRatioChanged: %f", TAG, ratio); mTvPipBoundsState.setDesiredTvExpandedAspectRatio(ratio, false); mTvPipMenuController.updateExpansionState(); @@ -595,11 +601,9 @@ public class TvPipController implements PipTransitionController.PipTransitionCal wmShell.addPinnedStackListener(new PinnedStackListenerForwarder.PinnedTaskListener() { @Override public void onImeVisibilityChanged(boolean imeVisible, int imeHeight) { - if (DEBUG) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: onImeVisibilityChanged(), visible=%b, height=%d", - TAG, imeVisible, imeHeight); - } + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: onImeVisibilityChanged(), visible=%b, height=%d", + TAG, imeVisible, imeHeight); if (imeVisible == mTvPipBoundsState.isImeShowing() && (!imeVisible || imeHeight == mTvPipBoundsState.getImeHeight())) { @@ -621,17 +625,13 @@ public class TvPipController implements PipTransitionController.PipTransitionCal } private static TaskInfo getPinnedTaskInfo() { - if (DEBUG) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: getPinnedTaskInfo()", TAG); - } + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: getPinnedTaskInfo()", TAG); try { final TaskInfo taskInfo = ActivityTaskManager.getService().getRootTaskInfo( WINDOWING_MODE_PINNED, ACTIVITY_TYPE_UNDEFINED); - if (DEBUG) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: taskInfo=%s", TAG, taskInfo); - } + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: taskInfo=%s", TAG, taskInfo); return taskInfo; } catch (RemoteException e) { ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, @@ -641,10 +641,8 @@ public class TvPipController implements PipTransitionController.PipTransitionCal } private static void removeTask(int taskId) { - if (DEBUG) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: removeTask(), taskId=%d", TAG, taskId); - } + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: removeTask(), taskId=%d", TAG, taskId); try { ActivityTaskManager.getService().removeTask(taskId); } catch (Exception e) { @@ -668,18 +666,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/TvPipMenuController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java index 4ce45e142c64..176fdfee2512 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java @@ -54,7 +54,6 @@ import java.util.Objects; */ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Listener { private static final String TAG = "TvPipMenuController"; - private static final boolean DEBUG = TvPipController.DEBUG; private static final String BACKGROUND_WINDOW_TITLE = "PipBackgroundView"; private final Context mContext; @@ -119,10 +118,8 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis } void setDelegate(Delegate delegate) { - if (DEBUG) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: setDelegate(), delegate=%s", TAG, delegate); - } + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: setDelegate(), delegate=%s", TAG, delegate); if (mDelegate != null) { throw new IllegalStateException( "The delegate has already been set and should not change."); @@ -145,10 +142,8 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis } private void attachPipMenu() { - if (DEBUG) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: attachPipMenu()", TAG); - } + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: attachPipMenu()", TAG); if (mPipMenuView != null) { detachPipMenu(); @@ -158,7 +153,7 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis attachPipMenuView(); mTvPipBoundsState.setPipMenuPermanentDecorInsets(Insets.of(-mPipMenuBorderWidth, - -mPipMenuBorderWidth, -mPipMenuBorderWidth, -mPipMenuBorderWidth)); + -mPipMenuBorderWidth, -mPipMenuBorderWidth, -mPipMenuBorderWidth)); mTvPipBoundsState.setPipMenuTemporaryDecorInsets(Insets.of(0, 0, 0, -mPipEduTextHeight)); mMainHandler.postDelayed(mCloseEduTextRunnable, mPipEduTextShowDurationMs); } @@ -180,35 +175,33 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis private void setUpViewSurfaceZOrder(View v, int zOrderRelativeToPip) { v.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() { - @Override - public void onViewAttachedToWindow(View v) { - v.getViewRootImpl().addSurfaceChangedCallback( - new PipMenuSurfaceChangedCallback(v, zOrderRelativeToPip)); - } - - @Override - public void onViewDetachedFromWindow(View v) { - } + @Override + public void onViewAttachedToWindow(View v) { + v.getViewRootImpl().addSurfaceChangedCallback( + new PipMenuSurfaceChangedCallback(v, zOrderRelativeToPip)); + } + + @Override + public void onViewDetachedFromWindow(View v) { + } }); } private void addPipMenuViewToSystemWindows(View v, String title) { - mSystemWindows.addView(v, getPipMenuLayoutParams(title, 0 /* width */, 0 /* height */), - 0 /* displayId */, SHELL_ROOT_LAYER_PIP); + mSystemWindows.addView(v, getPipMenuLayoutParams(mContext, title, 0 /* width */, + 0 /* height */), 0 /* displayId */, SHELL_ROOT_LAYER_PIP); } void notifyPipAnimating(boolean animating) { mPipMenuView.setEduTextActive(!animating); if (!animating) { - mPipMenuView.onPipTransitionFinished(); + mPipMenuView.onPipTransitionFinished(mTvPipBoundsState.isTvPipExpanded()); } } void showMovementMenuOnly() { - if (DEBUG) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: showMovementMenuOnly()", TAG); - } + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: showMovementMenuOnly()", TAG); setInMoveMode(true); mCloseAfterExitMoveMenu = true; showMenuInternal(); @@ -216,9 +209,7 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis @Override public void showMenu() { - if (DEBUG) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: showMenu()", TAG); - } + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: showMenu()", TAG); setInMoveMode(false); mCloseAfterExitMoveMenu = false; showMenuInternal(); @@ -267,7 +258,6 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis void updateExpansionState() { mPipMenuView.setExpandedModeEnabled(mTvPipBoundsState.isTvExpandedPipSupported() && mTvPipBoundsState.getDesiredTvExpandedAspectRatio() != 0); - mPipMenuView.setIsExpanded(mTvPipBoundsState.isTvPipExpanded()); } private Rect calculateMenuSurfaceBounds(Rect pipBounds) { @@ -275,15 +265,12 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis } void closeMenu() { - if (DEBUG) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: closeMenu()", TAG); - } + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: closeMenu()", TAG); if (mPipMenuView == null) { return; } - mPipMenuView.hideAllUserControls(); grantPipMenuFocus(false); mDelegate.onMenuClosed(); @@ -297,7 +284,6 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis if (mInMoveMode == moveMode) { return; } - mInMoveMode = moveMode; if (mDelegate != null) { mDelegate.onInMoveModeChanged(); @@ -306,22 +292,19 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis @Override public void onEnterMoveMode() { - if (DEBUG) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: onEnterMoveMode - %b, close when exiting move menu: %b", TAG, mInMoveMode, - mCloseAfterExitMoveMenu); - } + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: onEnterMoveMode - %b, close when exiting move menu: %b", TAG, mInMoveMode, + mCloseAfterExitMoveMenu); setInMoveMode(true); mPipMenuView.showMoveMenu(mDelegate.getPipGravity()); } @Override public boolean onExitMoveMode() { - if (DEBUG) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: onExitMoveMode - %b, close when exiting move menu: %b", TAG, mInMoveMode, - mCloseAfterExitMoveMenu); - } + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: onExitMoveMode - %b, close when exiting move menu: %b", TAG, mInMoveMode, + mCloseAfterExitMoveMenu); + if (mCloseAfterExitMoveMenu) { setInMoveMode(false); mCloseAfterExitMoveMenu = false; @@ -338,10 +321,8 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis @Override public boolean onPipMovement(int keycode) { - if (DEBUG) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: onPipMovement - %b", TAG, mInMoveMode); - } + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: onPipMovement - %b", TAG, mInMoveMode); if (mInMoveMode) { mDelegate.movePip(keycode); } @@ -358,18 +339,14 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis @Override public void setAppActions(List<RemoteAction> actions, RemoteAction closeAction) { - if (DEBUG) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: setAppActions()", TAG); - } + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: setAppActions()", TAG); updateAdditionalActionsList(mAppActions, actions, closeAction); } private void onMediaActionsChanged(List<RemoteAction> actions) { - if (DEBUG) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: onMediaActionsChanged()", TAG); - } + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: onMediaActionsChanged()", TAG); // Hide disabled actions. List<RemoteAction> enabledActions = new ArrayList<>(); @@ -421,10 +398,8 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis public void resizePipMenu(@Nullable SurfaceControl pipLeash, @Nullable SurfaceControl.Transaction t, Rect destinationBounds) { - if (DEBUG) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: resizePipMenu: %s", TAG, destinationBounds.toShortString()); - } + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: resizePipMenu: %s", TAG, destinationBounds.toShortString()); if (destinationBounds.isEmpty()) { return; } @@ -438,14 +413,14 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis final SurfaceControl frontSurface = getSurfaceControl(mPipMenuView); final SyncRtSurfaceTransactionApplier.SurfaceParams frontParams = new SyncRtSurfaceTransactionApplier.SurfaceParams.Builder(frontSurface) - .withWindowCrop(menuBounds) - .build(); + .withWindowCrop(menuBounds) + .build(); final SurfaceControl backSurface = getSurfaceControl(mPipBackgroundView); final SyncRtSurfaceTransactionApplier.SurfaceParams backParams = new SyncRtSurfaceTransactionApplier.SurfaceParams.Builder(backSurface) - .withWindowCrop(menuBounds) - .build(); + .withWindowCrop(menuBounds) + .build(); // TODO(b/226580399): switch to using SurfaceSyncer (see b/200284684) to synchronize the // animations of the pip surface with the content of the front and back menu surfaces @@ -468,13 +443,11 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis @Override public void movePipMenu(SurfaceControl pipLeash, SurfaceControl.Transaction transaction, Rect pipDestBounds) { - if (DEBUG) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: movePipMenu: %s", TAG, pipDestBounds.toShortString()); - } + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: movePipMenu: %s", TAG, pipDestBounds.toShortString()); if (pipDestBounds.isEmpty()) { - if (transaction == null && DEBUG) { + if (transaction == null) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: no transaction given", TAG); } @@ -490,16 +463,12 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis // resizing and the PiP menu is also resized. We then want to do a scale from the current // new menu bounds. if (pipLeash != null && transaction != null) { - if (DEBUG) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: tmpSourceBounds based on mPipMenuView.getBoundsOnScreen()", TAG); - } + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: tmpSourceBounds based on mPipMenuView.getBoundsOnScreen()", TAG); mPipMenuView.getBoundsOnScreen(tmpSourceBounds); } else { - if (DEBUG) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: tmpSourceBounds based on menu width and height", TAG); - } + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: tmpSourceBounds based on menu width and height", TAG); tmpSourceBounds.set(0, 0, menuDestBounds.width(), menuDestBounds.height()); } @@ -525,8 +494,8 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis if (pipLeash != null && transaction != null) { final SyncRtSurfaceTransactionApplier.SurfaceParams pipParams = new SyncRtSurfaceTransactionApplier.SurfaceParams.Builder(pipLeash) - .withMergeTransaction(transaction) - .build(); + .withMergeTransaction(transaction) + .build(); mApplier.scheduleApply(frontParams, pipParams); } else { mApplier.scheduleApply(frontParams); @@ -568,15 +537,13 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis @Override public void updateMenuBounds(Rect destinationBounds) { final Rect menuBounds = calculateMenuSurfaceBounds(destinationBounds); - if (DEBUG) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: updateMenuBounds: %s", TAG, menuBounds.toShortString()); - } + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: updateMenuBounds: %s", TAG, menuBounds.toShortString()); mSystemWindows.updateViewLayout(mPipBackgroundView, - getPipMenuLayoutParams(BACKGROUND_WINDOW_TITLE, menuBounds.width(), + getPipMenuLayoutParams(mContext, BACKGROUND_WINDOW_TITLE, menuBounds.width(), menuBounds.height())); mSystemWindows.updateViewLayout(mPipMenuView, - getPipMenuLayoutParams(MENU_WINDOW_TITLE, menuBounds.width(), + getPipMenuLayoutParams(mContext, MENU_WINDOW_TITLE, menuBounds.width(), menuBounds.height())); if (mPipMenuView != null) { @@ -630,10 +597,8 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis } private void grantPipMenuFocus(boolean grantFocus) { - if (DEBUG) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: grantWindowFocus(%b)", TAG, grantFocus); - } + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: grantWindowFocus(%b)", TAG, grantFocus); try { WindowManagerGlobal.getWindowSession().grantEmbeddedWindowFocus(null /* window */, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java index 320c05c4a415..9cd05b0eea75 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java @@ -43,6 +43,7 @@ import android.view.SurfaceControl; import android.view.View; import android.view.ViewGroup; import android.view.ViewRootImpl; +import android.view.accessibility.AccessibilityManager; import android.widget.FrameLayout; import android.widget.HorizontalScrollView; import android.widget.ImageView; @@ -55,6 +56,7 @@ import androidx.annotation.Nullable; import com.android.internal.protolog.common.ProtoLog; import com.android.wm.shell.R; +import com.android.wm.shell.common.TvWindowMenuActionButton; import com.android.wm.shell.pip.PipUtils; import com.android.wm.shell.protolog.ShellProtoLogGroup; @@ -65,11 +67,10 @@ import java.util.List; /** * A View that represents Pip Menu on TV. It's responsible for displaying 3 ever-present Pip Menu * actions: Fullscreen, Move and Close, but could also display "additional" actions, that may be set - * via a {@link #setAdditionalActions(List, Handler)} call. + * via a {@link #setAdditionalActions(List, RemoteAction, Handler)} call. */ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { private static final String TAG = "TvPipMenuView"; - private static final boolean DEBUG = TvPipController.DEBUG; private static final int FIRST_CUSTOM_ACTION_POSITION = 3; @@ -78,7 +79,7 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { private final LinearLayout mActionButtonsContainer; private final View mMenuFrameView; - private final List<TvPipMenuActionButton> mAdditionalButtons = new ArrayList<>(); + private final List<TvWindowMenuActionButton> mAdditionalButtons = new ArrayList<>(); private final View mPipFrameView; private final View mPipView; private final TextView mEduTextView; @@ -93,6 +94,10 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { private final ImageView mArrowRight; private final ImageView mArrowDown; private final ImageView mArrowLeft; + private final TvWindowMenuActionButton mA11yDoneButton; + + private final View mPipBackground; + private final View mDimLayer; private final ScrollView mScrollView; private final HorizontalScrollView mHorizontalScrollView; @@ -102,14 +107,16 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { private boolean mMoveMenuIsVisible; private boolean mButtonMenuIsVisible; - private final TvPipMenuActionButton mExpandButton; - private final TvPipMenuActionButton mCloseButton; + private final TvWindowMenuActionButton mExpandButton; + private final TvWindowMenuActionButton mCloseButton; private boolean mSwitchingOrientation; private final int mPipMenuFadeAnimationDuration; private final int mResizeAnimationDuration; + private final AccessibilityManager mA11yManager; + public TvPipMenuView(@NonNull Context context) { this(context, null); } @@ -128,6 +135,8 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { inflate(context, R.layout.tv_pip_menu, this); + mA11yManager = context.getSystemService(AccessibilityManager.class); + mActionButtonsContainer = findViewById(R.id.tv_pip_menu_action_buttons); mActionButtonsContainer.findViewById(R.id.tv_pip_menu_fullscreen_button) .setOnClickListener(this); @@ -141,6 +150,9 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { mExpandButton = findViewById(R.id.tv_pip_menu_expand_button); mExpandButton.setOnClickListener(this); + mPipBackground = findViewById(R.id.tv_pip_menu_background); + mDimLayer = findViewById(R.id.tv_pip_menu_dim_layer); + mScrollView = findViewById(R.id.tv_pip_menu_scroll); mHorizontalScrollView = findViewById(R.id.tv_pip_menu_horizontal_scroll); @@ -152,6 +164,7 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { mArrowRight = findViewById(R.id.tv_pip_menu_arrow_right); mArrowDown = findViewById(R.id.tv_pip_menu_arrow_down); mArrowLeft = findViewById(R.id.tv_pip_menu_arrow_left); + mA11yDoneButton = findViewById(R.id.tv_pip_menu_done_button); mEduTextView = findViewById(R.id.tv_pip_menu_edu_text); mEduTextContainerView = findViewById(R.id.tv_pip_menu_edu_text_container); @@ -159,7 +172,7 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { mResizeAnimationDuration = context.getResources().getInteger( R.integer.config_pipResizeAnimationDuration); mPipMenuFadeAnimationDuration = context.getResources() - .getInteger(R.integer.pip_menu_fade_animation_duration); + .getInteger(R.integer.tv_window_menu_fade_animation_duration); mPipMenuOuterSpace = context.getResources() .getDimensionPixelSize(R.dimen.pip_menu_outer_space); @@ -223,7 +236,7 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { mCurrentPipBounds.width() / (float) mCurrentPipBounds.height(), finishBounds.width() / (float) finishBounds.height()); if (ratioChanged) { - mPipView.animate() + mPipBackground.animate() .alpha(1f) .setInterpolator(TvPipInterpolators.EXIT) .setDuration(mResizeAnimationDuration / 2) @@ -259,17 +272,19 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { } } - void onPipTransitionFinished() { + void onPipTransitionFinished(boolean isTvPipExpanded) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: onPipTransitionFinished()", TAG); // Fade in content by fading out view on top. - mPipView.animate() + mPipBackground.animate() .alpha(0f) .setDuration(mResizeAnimationDuration / 2) .setInterpolator(TvPipInterpolators.ENTER) .start(); + setIsExpanded(isTvPipExpanded); + // Update buttons. if (mSwitchingOrientation) { mActionButtonsContainer.animate() @@ -432,10 +447,8 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { } void setIsExpanded(boolean expanded) { - if (DEBUG) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: setIsExpanded, expanded: %b", TAG, expanded); - } + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: setIsExpanded, expanded: %b", TAG, expanded); mExpandButton.setImageResource( expanded ? R.drawable.pip_ic_collapse : R.drawable.pip_ic_expand); mExpandButton.setTextAndDescription( @@ -446,28 +459,29 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { * @param gravity for the arrow hints */ void showMoveMenu(int gravity) { - if (DEBUG) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: showMoveMenu()", TAG); - } + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: showMoveMenu()", TAG); mButtonMenuIsVisible = false; mMoveMenuIsVisible = true; showButtonsMenu(false); showMovementHints(gravity); setFrameHighlighted(true); + + mHorizontalScrollView.setFocusable(false); + mScrollView.setFocusable(false); } void showButtonsMenu() { - if (DEBUG) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: showButtonsMenu()", TAG); - } - + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: showButtonsMenu()", TAG); mButtonMenuIsVisible = true; mMoveMenuIsVisible = false; showButtonsMenu(true); hideMovementHints(); setFrameHighlighted(true); + mHorizontalScrollView.setFocusable(true); + mScrollView.setFocusable(true); + // Always focus on the first button when opening the menu, except directly after moving. if (mFocusedButton == null) { // Focus on first button (there is a Space at position 0) @@ -531,10 +545,8 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { */ void setAdditionalActions(List<RemoteAction> actions, RemoteAction closeAction, Handler mainHandler) { - if (DEBUG) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: setAdditionalActions()", TAG); - } + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: setAdditionalActions()", TAG); // Replace system close action with custom close action if available if (closeAction != null) { @@ -553,7 +565,7 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { if (actionsNumber > buttonsNumber) { // Add buttons until we have enough to display all the actions. while (actionsNumber > buttonsNumber) { - TvPipMenuActionButton button = new TvPipMenuActionButton(mContext); + TvWindowMenuActionButton button = new TvWindowMenuActionButton(mContext); button.setOnClickListener(this); mActionButtonsContainer.addView(button, @@ -576,7 +588,7 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { // "Assign" actions to the buttons. for (int index = 0; index < actionsNumber; index++) { final RemoteAction action = actions.get(index); - final TvPipMenuActionButton button = mAdditionalButtons.get(index); + final TvWindowMenuActionButton button = mAdditionalButtons.get(index); // Remove action if it matches the custom close action. if (PipUtils.remoteActionsMatch(action, closeAction)) { @@ -592,7 +604,7 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { } } - private void setActionForButton(RemoteAction action, TvPipMenuActionButton button, + private void setActionForButton(RemoteAction action, TvWindowMenuActionButton button, Handler mainHandler) { button.setVisibility(View.VISIBLE); // Ensure the button is visible. if (action.getContentDescription().length() > 0) { @@ -655,10 +667,16 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { mFocusedButton = mActionButtonsContainer.getFocusedChild(); } + if (event.getKeyCode() == KEYCODE_BACK) { + mListener.onBackPress(); + return true; + } + + if (mA11yManager.isEnabled()) { + return super.dispatchKeyEvent(event); + } + switch (event.getKeyCode()) { - case KEYCODE_BACK: - mListener.onBackPress(); - return true; case KEYCODE_DPAD_UP: case KEYCODE_DPAD_DOWN: case KEYCODE_DPAD_LEFT: @@ -679,15 +697,39 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { * Shows user hints for moving the PiP, e.g. arrows. */ public void showMovementHints(int gravity) { - if (DEBUG) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: showMovementHints(), position: %s", TAG, Gravity.toString(gravity)); - } + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: showMovementHints(), position: %s", TAG, Gravity.toString(gravity)); animateAlphaTo(checkGravity(gravity, Gravity.BOTTOM) ? 1f : 0f, mArrowUp); animateAlphaTo(checkGravity(gravity, Gravity.TOP) ? 1f : 0f, mArrowDown); animateAlphaTo(checkGravity(gravity, Gravity.RIGHT) ? 1f : 0f, mArrowLeft); animateAlphaTo(checkGravity(gravity, Gravity.LEFT) ? 1f : 0f, mArrowRight); + + boolean a11yEnabled = mA11yManager.isEnabled(); + setArrowA11yEnabled(mArrowUp, a11yEnabled, KEYCODE_DPAD_UP); + setArrowA11yEnabled(mArrowDown, a11yEnabled, KEYCODE_DPAD_DOWN); + setArrowA11yEnabled(mArrowLeft, a11yEnabled, KEYCODE_DPAD_LEFT); + setArrowA11yEnabled(mArrowRight, a11yEnabled, KEYCODE_DPAD_RIGHT); + + animateAlphaTo(a11yEnabled ? 1f : 0f, mA11yDoneButton); + if (a11yEnabled) { + mA11yDoneButton.setOnClickListener(v -> { + if (mListener != null) { + mListener.onExitMoveMode(); + } + }); + } + } + + private void setArrowA11yEnabled(View arrowView, boolean enabled, int keycode) { + arrowView.setClickable(enabled); + if (enabled) { + arrowView.setOnClickListener(v -> { + if (mListener != null) { + mListener.onPipMovement(keycode); + } + }); + } } private boolean checkGravity(int gravity, int feature) { @@ -698,29 +740,28 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { * Hides user hints for moving the PiP, e.g. arrows. */ public void hideMovementHints() { - if (DEBUG) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: hideMovementHints()", TAG); - } + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: hideMovementHints()", TAG); + animateAlphaTo(0, mArrowUp); animateAlphaTo(0, mArrowRight); animateAlphaTo(0, mArrowDown); animateAlphaTo(0, mArrowLeft); + animateAlphaTo(0, mA11yDoneButton); } /** * Show or hide the pip buttons menu. */ public void showButtonsMenu(boolean show) { - if (DEBUG) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: showUserActions: %b", TAG, show); - } + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: showUserActions: %b", TAG, show); if (show) { mActionButtonsContainer.setVisibility(VISIBLE); refocusPreviousButton(); } animateAlphaTo(show ? 1 : 0, mActionButtonsContainer); + animateAlphaTo(show ? 1 : 0, mDimLayer); } private void setFrameHighlighted(boolean highlighted) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipNotificationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipNotificationController.java index 61a609d9755e..e3308f0763a0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipNotificationController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipNotificationController.java @@ -114,6 +114,7 @@ public class TvPipNotificationController { .setOngoing(true) .setCategory(Notification.CATEGORY_SYSTEM) .setShowWhen(true) + .setOnlyAlertOnce(true) .setSmallIcon(R.drawable.pip_icon) .setAllowSystemGeneratedContextualActions(false) .setContentIntent(createPendingIntent(context, ACTION_FULLSCREEN)) 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..3fef82352728 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,14 @@ 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), 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/RecentTasksController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java index c166178e9bbd..ff4b2edb7310 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,6 +17,7 @@ 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; @@ -42,39 +43,47 @@ 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.DesktopMode; +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; /** * 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 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 +92,48 @@ public class RecentTasksController implements TaskStackListenerCallback, @Nullable public static RecentTasksController create( Context context, + ShellInit shellInit, + ShellCommandHandler shellCommandHandler, TaskStackListenerImpl taskStackListener, + 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, + desktopModeTaskRepository, mainExecutor); } - RecentTasksController(Context context, TaskStackListenerImpl taskStackListener, + RecentTasksController(Context context, + ShellInit shellInit, + ShellCommandHandler shellCommandHandler, + TaskStackListenerImpl taskStackListener, + Optional<DesktopModeTaskRepository> desktopModeTaskRepository, ShellExecutor mainExecutor) { mContext = context; + mShellCommandHandler = shellCommandHandler; + 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,32 +199,73 @@ 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); } } - private void registerRecentTasksListener(Runnable listener) { - if (!mCallbacks.contains(listener)) { - mCallbacks.add(listener); + /** + * 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); + } + } + + /** + * 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 registerRecentTasksListener(IRecentTasksListener listener) { + mListener = listener; } - private void unregisterRecentTasksListener(Runnable listener) { - mCallbacks.remove(listener); + private void unregisterRecentTasksListener() { + mListener = null; } @VisibleForTesting @@ -222,6 +286,9 @@ public class RecentTasksController implements TaskStackListenerCallback, rawMapping.put(taskInfo.taskId, taskInfo); } + boolean desktopModeActive = DesktopMode.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,16 +298,32 @@ 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()) { + // First task is added separately + recentTasks.add(0, GroupedRecentTaskInfo.forFreeformTasks( + freeformTasks.toArray(new ActivityManager.RecentTaskInfo[0]))); + } + return recentTasks; } @@ -280,19 +363,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 +423,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..3758471664cc 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,8 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, @Retention(RetentionPolicy.SOURCE) @interface ExitReason{} + private final ShellCommandHandler mShellCommandHandler; + private final ShellController mShellController; private final ShellTaskOrganizer mTaskOrganizer; private final SyncTransactionQueue mSyncQueue; private final Context mContext; @@ -133,27 +144,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 +183,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 +234,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 +274,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 +323,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 +350,239 @@ 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) { + } + }; 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); + 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); + 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(); - } - fillInIntent.addFlags(FLAG_ACTIVITY_NO_USER_ACTION); + // Flag with MULTIPLE_TASK if this is launching the same activity into both sides of the + // split. + if (shouldAddMultipleTaskFlag(intent.getIntent(), position)) { + 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 +657,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 +716,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 +805,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..626ccb1d2890 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,7 @@ 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__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; @@ -59,7 +59,7 @@ public class SplitscreenEventLogger { // Drag info private @SplitPosition int mDragEnterPosition; - private InstanceId mDragEnterSessionId; + private InstanceId mEnterSessionId; // For deduping async events private int mLastMainStagePosition = -1; @@ -82,9 +82,17 @@ public class SplitscreenEventLogger { /** * 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); + } + + /** + * 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(InstanceId enterSessionId) { + mEnterSessionId = enterSessionId; } /** @@ -97,7 +105,7 @@ public class SplitscreenEventLogger { mLoggerSessionId = mIdSequence.newInstanceId(); int enterReason = mDragEnterPosition != SPLIT_POSITION_UNDEFINED ? getDragEnterReasonFromSplitPosition(mDragEnterPosition, isLandscape) - : SPLITSCREEN_UICHANGED__ENTER_REASON__OVERVIEW; + : SPLITSCREEN_UICHANGED__ENTER_REASON__LAUNCHER; updateMainStageState(getMainStagePositionFromSplitPosition(mainStagePosition, isLandscape), mainStageUid); updateSideStageState(getSideStagePositionFromSplitPosition(sideStagePosition, isLandscape), @@ -112,7 +120,7 @@ public class SplitscreenEventLogger { mLastMainStageUid, mLastSideStagePosition, mLastSideStageUid, - mDragEnterSessionId != null ? mDragEnterSessionId.getId() : 0, + mEnterSessionId != null ? mEnterSessionId.getId() : 0, mLoggerSessionId.getId()); } @@ -176,7 +184,7 @@ public class SplitscreenEventLogger { // Reset states mLoggerSessionId = null; mDragEnterPosition = SPLIT_POSITION_UNDEFINED; - mDragEnterSessionId = null; + mEnterSessionId = null; mLastMainStagePosition = -1; mLastMainStageUid = -1; mLastSideStagePosition = -1; 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..25793ed0317b 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 @@ -22,15 +22,22 @@ 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; @@ -47,7 +54,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 +68,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 +81,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 +92,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 +103,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 +121,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 +144,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 +154,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 +178,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 +196,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 +213,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 +273,7 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, mMainStageListener, mSyncQueue, mSurfaceSession, - iconProvider, - mMainUnfoldController); + iconProvider); mSideStage = new SideStage( mContext, mTaskOrganizer, @@ -235,8 +281,7 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, mSideStageListener, mSyncQueue, mSurfaceSession, - iconProvider, - mSideUnfoldController); + iconProvider); mDisplayController = displayController; mDisplayImeController = displayImeController; mDisplayInsetsController = displayInsetsController; @@ -250,6 +295,7 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, mDisplayController.addDisplayWindowListener(this); mDisplayLayout = new DisplayLayout(displayController.getDisplayLayout(displayId)); transitions.addHandler(this); + mTaskOrganizer.addFocusListener(this); } @VisibleForTesting @@ -258,9 +304,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 +319,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 +327,10 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, transitions.addHandler(this); } + public void setMixedHandler(DefaultMixedHandler mixedHandler) { + mMixedHandler = mixedHandler; + } + @VisibleForTesting SplitScreenTransitions getSplitTransitions() { return mSplitTransitions; @@ -329,15 +376,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 +412,120 @@ 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); + + 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 +536,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); + } + + 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); } - 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; + /** + * @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 +607,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 +627,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 +646,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 onRemoteAnimationFinishedOrCancelled(WindowContainerTransaction evictWct) { + private void setEnterInstanceId(InstanceId instanceId) { + if (instanceId != null) { + mLogger.enterRequested(instanceId); + } + } + + 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 +701,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 +779,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 +820,7 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, onLayoutSizeChanged(mSplitLayout); } else { updateWindowBounds(mSplitLayout, wct); - updateUnfoldBounds(); + sendOnBoundsChanged(); } } } @@ -642,7 +835,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 +868,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 +905,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 +918,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 +976,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 +1063,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 +1071,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(); @@ -860,6 +1127,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 +1143,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 +1176,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 +1202,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 +1216,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 +1237,6 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, mDisplayInsetsController.addInsetsChangedListener(mDisplayId, mSplitLayout); } - if (mMainUnfoldController != null && mSideUnfoldController != null) { - mMainUnfoldController.init(); - mSideUnfoldController.init(); - } - onRootTaskAppeared(); } @@ -979,13 +1250,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 +1275,7 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, } mRootTaskInfo = null; + mRootTaskLeash = null; } @@ -1025,10 +1292,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 +1408,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,21 +1432,40 @@ 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()) { + // TODO (b/238697912) : Add the validation to prevent entering non-recovered status + onSplitScreenEnter(); final WindowContainerTransaction wct = new WindowContainerTransaction(); mSplitLayout.init(); - prepareEnterSplitScreen(wct); + 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 -> { + mSplitLayout.flingDividerToCenter(); + }); } if (mMainStageListener.mHasChildren && mSideStageListener.mHasChildren) { mShouldUpdateRecents = true; @@ -1190,27 +1480,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 +1535,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 +1548,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 +1558,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 +1585,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 +1616,7 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, if (displayId != DEFAULT_DISPLAY) { return; } - mDisplayController.addDisplayChangingController(this::onRotateDisplay); + mDisplayController.addDisplayChangingController(this::onDisplayChange); } @Override @@ -1340,16 +1627,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 +1666,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 +1709,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 +1737,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 +1752,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 +1762,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 +1777,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 +1818,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 +1835,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 +1879,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 +1938,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 +1986,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 +2025,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 +2042,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 +2082,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 +2205,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 +2218,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/splitscreen/tv/OWNERS b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/OWNERS new file mode 100644 index 000000000000..28be0efc38f6 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/OWNERS @@ -0,0 +1,3 @@ +# WM shell sub-module TV splitscreen owner +galinap@google.com +bronger@google.com diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvSplitMenuController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvSplitMenuController.java new file mode 100644 index 000000000000..1d8a8d506c5c --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvSplitMenuController.java @@ -0,0 +1,218 @@ +/* + * 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.tv; + +import static android.view.Display.DEFAULT_DISPLAY; +import static android.view.View.INVISIBLE; +import static android.view.View.VISIBLE; +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_DOCK_DIVIDER; +import static android.view.WindowManager.SHELL_ROOT_LAYER_DIVIDER; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.graphics.PixelFormat; +import android.os.Handler; +import android.os.RemoteException; +import android.view.LayoutInflater; +import android.view.WindowManager; +import android.view.WindowManagerGlobal; + +import com.android.internal.protolog.common.ProtoLog; +import com.android.wm.shell.R; +import com.android.wm.shell.common.SystemWindows; +import com.android.wm.shell.common.split.SplitScreenConstants; +import com.android.wm.shell.protolog.ShellProtoLogGroup; + +/** + * Handles the interaction logic with the {@link TvSplitMenuView}. + * A bridge between {@link TvStageCoordinator} and {@link TvSplitMenuView}. + */ +public class TvSplitMenuController implements TvSplitMenuView.Listener { + + private static final String TAG = TvSplitMenuController.class.getSimpleName(); + private static final String ACTION_SHOW_MENU = "com.android.wm.shell.splitscreen.SHOW_MENU"; + private static final String SYSTEMUI_PERMISSION = "com.android.systemui.permission.SELF"; + + private final Context mContext; + private final StageController mStageController; + private final SystemWindows mSystemWindows; + private final Handler mMainHandler; + + private final TvSplitMenuView mSplitMenuView; + + private final ActionBroadcastReceiver mActionBroadcastReceiver; + + private final int mTvButtonFadeAnimationDuration; + + public TvSplitMenuController(Context context, StageController stageController, + SystemWindows systemWindows, Handler mainHandler) { + mContext = context; + mMainHandler = mainHandler; + mStageController = stageController; + mSystemWindows = systemWindows; + + mTvButtonFadeAnimationDuration = context.getResources() + .getInteger(R.integer.tv_window_menu_fade_animation_duration); + + mSplitMenuView = (TvSplitMenuView) LayoutInflater.from(context) + .inflate(R.layout.tv_split_menu_view, null); + mSplitMenuView.setListener(this); + + mActionBroadcastReceiver = new ActionBroadcastReceiver(); + } + + /** + * Adds the menu view for the splitscreen to SystemWindows. + */ + void addSplitMenuViewToSystemWindows() { + final WindowManager.LayoutParams lp = new WindowManager.LayoutParams( + mContext.getResources().getDisplayMetrics().widthPixels, + mContext.getResources().getDisplayMetrics().heightPixels, + TYPE_DOCK_DIVIDER, + FLAG_NOT_TOUCHABLE, + PixelFormat.TRANSLUCENT); + lp.privateFlags |= PRIVATE_FLAG_NO_MOVE_ANIMATION | PRIVATE_FLAG_TRUSTED_OVERLAY; + mSplitMenuView.setAlpha(0); + mSystemWindows.addView(mSplitMenuView, lp, DEFAULT_DISPLAY, SHELL_ROOT_LAYER_DIVIDER); + } + + /** + * Removes the menu view for the splitscreen from SystemWindows. + */ + void removeSplitMenuViewFromSystemWindows() { + mSystemWindows.removeView(mSplitMenuView); + } + + /** + * Registers BroadcastReceiver when split screen mode is entered. + */ + void registerBroadcastReceiver() { + mActionBroadcastReceiver.register(); + } + + /** + * Unregisters BroadcastReceiver when split screen mode is entered. + */ + void unregisterBroadcastReceiver() { + mActionBroadcastReceiver.unregister(); + } + + @Override + public void onBackPress() { + setMenuVisibility(false); + } + + @Override + public void onFocusStage(@SplitScreenConstants.SplitPosition int stageToFocus) { + setMenuVisibility(false); + mStageController.grantFocusToStage(stageToFocus); + } + + @Override + public void onCloseStage(@SplitScreenConstants.SplitPosition int stageToClose) { + setMenuVisibility(false); + mStageController.exitStage(stageToClose); + } + + @Override + public void onSwapPress() { + mStageController.swapStages(); + } + + private void setMenuVisibility(boolean visible) { + applyMenuVisibility(visible); + setMenuFocus(visible); + } + + private void applyMenuVisibility(boolean visible) { + float alphaTarget = visible ? 1F : 0F; + + if (mSplitMenuView.getAlpha() == alphaTarget) { + return; + } + + mSplitMenuView.animate() + .alpha(alphaTarget) + .setDuration(mTvButtonFadeAnimationDuration) + .withStartAction(() -> { + if (alphaTarget != 0) { + mSplitMenuView.setVisibility(VISIBLE); + } + }) + .withEndAction(() -> { + if (alphaTarget == 0) { + mSplitMenuView.setVisibility(INVISIBLE); + } + }); + + } + + private void setMenuFocus(boolean focused) { + try { + WindowManagerGlobal.getWindowSession().grantEmbeddedWindowFocus(null, + mSystemWindows.getFocusGrantToken(mSplitMenuView), focused); + } catch (RemoteException e) { + ProtoLog.e(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, + "%s: Unable to update focus, %s", TAG, e); + } + } + + interface StageController { + void grantFocusToStage(@SplitScreenConstants.SplitPosition int stageToFocus); + void exitStage(@SplitScreenConstants.SplitPosition int stageToClose); + void swapStages(); + } + + private class ActionBroadcastReceiver extends BroadcastReceiver { + + final IntentFilter mIntentFilter; + { + mIntentFilter = new IntentFilter(); + mIntentFilter.addAction(ACTION_SHOW_MENU); + } + boolean mRegistered = false; + + void register() { + if (mRegistered) return; + + mContext.registerReceiverForAllUsers(this, mIntentFilter, SYSTEMUI_PERMISSION, + mMainHandler); + mRegistered = true; + } + + void unregister() { + if (!mRegistered) return; + + mContext.unregisterReceiver(this); + mRegistered = false; + } + + @Override + public void onReceive(Context context, Intent intent) { + final String action = intent.getAction(); + + if (ACTION_SHOW_MENU.equals(action)) { + setMenuVisibility(true); + } + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvSplitMenuView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvSplitMenuView.java new file mode 100644 index 000000000000..88e9757a9b31 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvSplitMenuView.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.splitscreen.tv; + +import static android.view.KeyEvent.ACTION_DOWN; +import static android.view.KeyEvent.KEYCODE_BACK; + +import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; +import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.KeyEvent; +import android.view.View; +import android.widget.LinearLayout; + +import androidx.annotation.Nullable; + +import com.android.wm.shell.R; +import com.android.wm.shell.common.split.SplitScreenConstants; + +/** + * A View for the Menu Window. + */ +public class TvSplitMenuView extends LinearLayout implements View.OnClickListener { + + private Listener mListener; + + public TvSplitMenuView(Context context) { + super(context); + } + + public TvSplitMenuView(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + } + + public TvSplitMenuView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + initButtons(); + } + + @Override + public void onClick(View v) { + if (mListener == null) return; + + final int id = v.getId(); + if (id == R.id.tv_split_main_menu_focus_button) { + mListener.onFocusStage(SPLIT_POSITION_TOP_OR_LEFT); + } else if (id == R.id.tv_split_main_menu_close_button) { + mListener.onCloseStage(SPLIT_POSITION_TOP_OR_LEFT); + } else if (id == R.id.tv_split_side_menu_focus_button) { + mListener.onFocusStage(SPLIT_POSITION_BOTTOM_OR_RIGHT); + } else if (id == R.id.tv_split_side_menu_close_button) { + mListener.onCloseStage(SPLIT_POSITION_BOTTOM_OR_RIGHT); + } else if (id == R.id.tv_split_menu_swap_stages) { + mListener.onSwapPress(); + } + } + + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + if (event.getAction() == ACTION_DOWN) { + if (event.getKeyCode() == KEYCODE_BACK) { + if (mListener != null) { + mListener.onBackPress(); + return true; + } + } + } + return super.dispatchKeyEvent(event); + } + + private void initButtons() { + findViewById(R.id.tv_split_main_menu_focus_button).setOnClickListener(this); + findViewById(R.id.tv_split_main_menu_close_button).setOnClickListener(this); + findViewById(R.id.tv_split_side_menu_focus_button).setOnClickListener(this); + findViewById(R.id.tv_split_side_menu_close_button).setOnClickListener(this); + findViewById(R.id.tv_split_menu_swap_stages).setOnClickListener(this); + } + + void setListener(Listener listener) { + mListener = listener; + } + + interface Listener { + /** "Back" button from the remote control */ + void onBackPress(); + + /** Menu Action Buttons */ + + void onFocusStage(@SplitScreenConstants.SplitPosition int stageToFocus); + + void onCloseStage(@SplitScreenConstants.SplitPosition int stageToClose); + + void onSwapPress(); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvSplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvSplitScreenController.java new file mode 100644 index 000000000000..46d2a5a11671 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvSplitScreenController.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.splitscreen.tv; + +import static android.view.Display.DEFAULT_DISPLAY; + +import android.content.Context; +import android.os.Handler; + +import com.android.launcher3.icons.IconProvider; +import com.android.wm.shell.RootTaskDisplayAreaOrganizer; +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.common.DisplayController; +import com.android.wm.shell.common.DisplayImeController; +import com.android.wm.shell.common.DisplayInsetsController; +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.SyncTransactionQueue; +import com.android.wm.shell.common.SystemWindows; +import com.android.wm.shell.common.TransactionPool; +import com.android.wm.shell.draganddrop.DragAndDropController; +import com.android.wm.shell.recents.RecentTasksController; +import com.android.wm.shell.splitscreen.SplitScreenController; +import com.android.wm.shell.splitscreen.StageCoordinator; +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.util.Optional; + +/** + * Class inherits from {@link SplitScreenController} and provides {@link TvStageCoordinator} + * for Split Screen on TV. + */ +public class TvSplitScreenController extends SplitScreenController { + private final ShellTaskOrganizer mTaskOrganizer; + private final SyncTransactionQueue mSyncQueue; + private final Context mContext; + private final ShellExecutor mMainExecutor; + private final DisplayController mDisplayController; + private final DisplayImeController mDisplayImeController; + private final DisplayInsetsController mDisplayInsetsController; + private final Transitions mTransitions; + private final TransactionPool mTransactionPool; + private final IconProvider mIconProvider; + private final Optional<RecentTasksController> mRecentTasksOptional; + + private final Handler mMainHandler; + private final SystemWindows mSystemWindows; + + public TvSplitScreenController(Context context, + ShellInit shellInit, + ShellCommandHandler shellCommandHandler, + ShellController shellController, + ShellTaskOrganizer shellTaskOrganizer, + SyncTransactionQueue syncQueue, + RootTaskDisplayAreaOrganizer rootTDAOrganizer, + DisplayController displayController, + DisplayImeController displayImeController, + DisplayInsetsController displayInsetsController, + DragAndDropController dragAndDropController, + Transitions transitions, + TransactionPool transactionPool, + IconProvider iconProvider, + Optional<RecentTasksController> recentTasks, + ShellExecutor mainExecutor, + Handler mainHandler, + SystemWindows systemWindows) { + super(context, shellInit, shellCommandHandler, shellController, shellTaskOrganizer, + syncQueue, rootTDAOrganizer, displayController, displayImeController, + displayInsetsController, dragAndDropController, transitions, transactionPool, + iconProvider, recentTasks, mainExecutor); + + mTaskOrganizer = shellTaskOrganizer; + mSyncQueue = syncQueue; + mContext = context; + mMainExecutor = mainExecutor; + mDisplayController = displayController; + mDisplayImeController = displayImeController; + mDisplayInsetsController = displayInsetsController; + mTransitions = transitions; + mTransactionPool = transactionPool; + mIconProvider = iconProvider; + mRecentTasksOptional = recentTasks; + + mMainHandler = mainHandler; + mSystemWindows = systemWindows; + } + + /** + * Provides Tv-specific StageCoordinator. + * @return {@link TvStageCoordinator} + */ + @Override + protected StageCoordinator createStageCoordinator() { + return new TvStageCoordinator(mContext, DEFAULT_DISPLAY, mSyncQueue, + mTaskOrganizer, mDisplayController, mDisplayImeController, + mDisplayInsetsController, mTransitions, mTransactionPool, + mIconProvider, mMainExecutor, mMainHandler, + mRecentTasksOptional, mSystemWindows); + } + +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvStageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvStageCoordinator.java new file mode 100644 index 000000000000..4d563fbb7f04 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvStageCoordinator.java @@ -0,0 +1,94 @@ +/* + * 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.tv; + +import android.content.Context; +import android.os.Handler; + +import com.android.launcher3.icons.IconProvider; +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 com.android.wm.shell.common.SystemWindows; +import com.android.wm.shell.common.TransactionPool; +import com.android.wm.shell.common.split.SplitScreenConstants; +import com.android.wm.shell.recents.RecentTasksController; +import com.android.wm.shell.splitscreen.StageCoordinator; +import com.android.wm.shell.transition.Transitions; + +import java.util.Optional; + +/** + * Expands {@link StageCoordinator} functionality with Tv-specific methods. + */ +public class TvStageCoordinator extends StageCoordinator + implements TvSplitMenuController.StageController { + + private final TvSplitMenuController mTvSplitMenuController; + + public TvStageCoordinator(Context context, int displayId, SyncTransactionQueue syncQueue, + ShellTaskOrganizer taskOrganizer, DisplayController displayController, + DisplayImeController displayImeController, + DisplayInsetsController displayInsetsController, Transitions transitions, + TransactionPool transactionPool, + IconProvider iconProvider, ShellExecutor mainExecutor, + Handler mainHandler, + Optional<RecentTasksController> recentTasks, + SystemWindows systemWindows) { + super(context, displayId, syncQueue, taskOrganizer, displayController, displayImeController, + displayInsetsController, transitions, transactionPool, iconProvider, + mainExecutor, recentTasks); + + mTvSplitMenuController = new TvSplitMenuController(context, this, + systemWindows, mainHandler); + + } + + @Override + protected void onSplitScreenEnter() { + mTvSplitMenuController.addSplitMenuViewToSystemWindows(); + mTvSplitMenuController.registerBroadcastReceiver(); + } + + @Override + protected void onSplitScreenExit() { + mTvSplitMenuController.unregisterBroadcastReceiver(); + mTvSplitMenuController.removeSplitMenuViewFromSystemWindows(); + } + + @Override + public void grantFocusToStage(@SplitScreenConstants.SplitPosition int stageToFocus) { + super.grantFocusToStage(stageToFocus); + } + + @Override + public void exitStage(@SplitScreenConstants.SplitPosition int stageToClose) { + super.exitStage(stageToClose); + } + + /** + * Swaps the stages inside the SplitLayout. + */ + @Override + public void swapStages() { + onDoubleTappedDivider(); + } + +} 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/ISplitScreenListener.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/ISplitScreenListener.aidl deleted file mode 100644 index 46e4299f99fa..000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/ISplitScreenListener.aidl +++ /dev/null @@ -1,33 +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; - -/** - * Listener interface that Launcher attaches to SystemUI to get split-screen callbacks. - */ -oneway interface ISplitScreenListener { - - /** - * Called when the stage position changes. - */ - void onStagePositionChanged(int stage, int position); - - /** - * Called when a task changes stages. - */ - void onTaskStageChanged(int taskId, int stage, boolean visible); -}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/MainStage.java b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/MainStage.java 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..1ae779e25bbf 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 @@ -24,6 +24,7 @@ import static android.app.ActivityOptions.ANIM_OPEN_CROSS_PROFILE_APPS; import static android.app.ActivityOptions.ANIM_SCALE_UP; import static android.app.ActivityOptions.ANIM_THUMBNAIL_SCALE_DOWN; import static android.app.ActivityOptions.ANIM_THUMBNAIL_SCALE_UP; +import static android.app.WindowConfiguration.ACTIVITY_TYPE_DREAM; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.app.admin.DevicePolicyManager.ACTION_DEVICE_POLICY_RESOURCE_UPDATED; import static android.app.admin.DevicePolicyManager.EXTRA_RESOURCE_TYPE; @@ -42,6 +43,9 @@ 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.view.WindowManager.transitTypeToString; +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_IS_DISPLAY; import static android.window.TransitionInfo.FLAG_IS_VOICE_INTERACTION; @@ -62,6 +66,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; @@ -91,6 +96,7 @@ import android.view.WindowManager.TransitionType; import android.view.animation.AlphaAnimation; import android.view.animation.Animation; import android.view.animation.Transformation; +import android.window.ScreenCapture; import android.window.TransitionInfo; import android.window.TransitionMetrics; import android.window.TransitionRequestInfo; @@ -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; @@ -134,6 +141,7 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { 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 +158,6 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { private final int mCurrentUserId; - private ScreenRotationAnimation mRotationAnimation; - private Drawable mEnterpriseThumbnailDrawable; private BroadcastReceiver mEnterpriseResourceUpdatedReceiver = new BroadcastReceiver() { @@ -165,27 +171,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 +207,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 +237,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 +326,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 +342,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 { @@ -403,6 +390,13 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { 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. @@ -470,8 +464,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 +475,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 +515,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(); @@ -593,16 +631,16 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { .setBufferSize(extensionRect.width(), extensionRect.height()) .build(); - SurfaceControl.LayerCaptureArgs captureArgs = - new SurfaceControl.LayerCaptureArgs.Builder(surfaceToExtend) + ScreenCapture.LayerCaptureArgs captureArgs = + new ScreenCapture.LayerCaptureArgs.Builder(surfaceToExtend) .setSourceCrop(edgeBounds) .setFrameScale(1) .setPixelFormat(PixelFormat.RGBA_8888) .setChildrenOnly(true) .setAllowProtected(true) .build(); - final SurfaceControl.ScreenshotHardwareBuffer edgeBuffer = - SurfaceControl.captureLayers(captureArgs); + final ScreenCapture.ScreenshotHardwareBuffer edgeBuffer = + ScreenCapture.captureLayers(captureArgs); if (edgeBuffer == null) { ProtoLog.e(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, @@ -684,6 +722,8 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { final Rect endBounds = Transitions.isClosingType(changeMode) ? mRotator.getEndBoundsInStartRotation(change) : change.getEndAbsBounds(); + final boolean isDream = + isTask && change.getTaskInfo().topActivityType == ACTIVITY_TYPE_DREAM; if (info.isKeyguardGoingAway()) { a = mTransitionAnimation.loadKeyguardExitAnimation(flags, @@ -726,7 +766,17 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { } else { int animAttr = 0; boolean translucent = false; - if (wallpaperTransit == WALLPAPER_TRANSITION_INTRA_OPEN) { + 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; @@ -790,6 +840,11 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { a = mTransitionAnimation.loadDefaultAnimationAttr(animAttr, translucent); } } + + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, + "loadAnimation: anim=%s animAttr=0x%x type=%s isEntrance=%b", a, animAttr, + transitTypeToString(type), + enter); } if (a != null) { @@ -834,13 +889,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 +912,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 +930,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 +961,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 +986,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 +1019,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 +1027,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/IShellTransitions.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/IShellTransitions.aidl index bdcdb63d2cd6..cc4d268a0000 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/IShellTransitions.aidl +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/IShellTransitions.aidl @@ -34,4 +34,9 @@ interface IShellTransitions { * Unregisters a remote transition handler. */ oneway void unregisterRemote(in RemoteTransition remoteTransition) = 2; + + /** + * Retrieves the apply-token used by transactions in Shell + */ + IBinder getShellApplyToken() = 3; } 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..2b27baeb515a 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; @@ -47,6 +46,7 @@ import android.view.SurfaceControl.Transaction; import android.view.SurfaceSession; import android.view.animation.Animation; import android.view.animation.AnimationUtils; +import android.window.ScreenCapture; import android.window.TransitionInfo; import com.android.internal.R; @@ -85,12 +85,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 +104,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 +132,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 +140,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 { + ScreenCapture.LayerCaptureArgs args = + new ScreenCapture.LayerCaptureArgs.Builder(mSurfaceControl) + .setCaptureSecureLayers(true) + .setAllowProtected(true) + .setSourceCrop(new Rect(0, 0, mStartWidth, mStartHeight)) + .build(); + ScreenCapture.ScreenshotHardwareBuffer screenshotBuffer = + ScreenCapture.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 +200,7 @@ class ScreenRotationAnimation { Slog.w(TAG, "Unable to allocate freeze surface", e); } - setRotation(t); + setScreenshotTransform(t); t.apply(); } @@ -210,19 +208,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 +245,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 +310,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 +389,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); @@ -477,8 +482,8 @@ class ScreenRotationAnimation { } Rect crop = new Rect(0, 0, bounds.width(), bounds.height()); - SurfaceControl.ScreenshotHardwareBuffer buffer = - SurfaceControl.captureLayers(surfaceControl, crop, 1); + ScreenCapture.ScreenshotHardwareBuffer buffer = + ScreenCapture.captureLayers(surfaceControl, crop, 1); if (buffer == null) { return 0; } @@ -486,27 +491,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/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java index 435d67087f34..d2e8624171f6 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 @@ -809,6 +958,11 @@ public class Transitions implements RemoteCallable<Transitions> { transitions.mRemoteTransitionHandler.removeFiltered(remoteTransition); }); } + + @Override + public IBinder getShellApplyToken() { + return SurfaceControl.Transaction.getDefaultApplyToken(); + } } private class SettingsObserver extends ContentObserver { @@ -820,9 +974,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/ShellInit.java b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/qualifier/UnfoldShellTransition.java index d7010b174744..4c868305dcdb 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellInit.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/qualifier/UnfoldShellTransition.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,16 @@ * limitations under the License. */ -package com.android.wm.shell; +package com.android.wm.shell.unfold.qualifier; -import com.android.wm.shell.common.annotations.ExternalThread; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import javax.inject.Qualifier; /** - * An entry point into the shell for initializing shell internal state. + * Indicates that this class is used for the shell unfold transition */ -@ExternalThread -public interface ShellInit { - /** - * Initializes the shell state. - */ - void init(); -} +@Qualifier +@Retention(RetentionPolicy.RUNTIME) +public @interface UnfoldShellTransition {} 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/UnfoldTransition.java index edeff6e37182..4d2b3e6f899b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDrop.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/qualifier/UnfoldTransition.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,17 @@ * 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 unfold transition implemented + * without using Shell transitions */ -@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 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..e8a2cb160880 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java @@ -0,0 +1,246 @@ +/* + * 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.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.DesktopMode; +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; + + public CaptionWindowDecorViewModel( + Context context, + Handler mainHandler, + Choreographer mainChoreographer, + ShellTaskOrganizer taskOrganizer, + DisplayController displayController, + SyncTransactionQueue syncQueue) { + mContext = context; + mMainHandler = mainHandler; + mMainChoreographer = mainChoreographer; + mActivityTaskManager = mContext.getSystemService(ActivityTaskManager.class); + mTaskOrganizer = taskOrganizer; + mDisplayController = displayController; + mSyncQueue = syncQueue; + } + + @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) { + if (mTaskOrganizer.getRunningTaskInfo(mTaskId).getWindowingMode() + == 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); + mDragResizeCallback.onDragResizeEnd( + e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx)); + break; + } + } + } + } + + private boolean shouldShowWindowDecor(RunningTaskInfo taskInfo) { + if (taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM) return true; + return DesktopMode.IS_SUPPORTED + && 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..5040bc37c614 --- /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.DesktopMode; + +/** + * 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 (DesktopMode.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/legacysplitscreen/DividerState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskFocusStateConsumer.java index af2ab158ab46..1c61802bbd5c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/DividerState.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskFocusStateConsumer.java @@ -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,8 @@ * limitations under the License. */ -package com.android.wm.shell.legacysplitscreen; +package com.android.wm.shell.windowdecor; -/** - * Class to hold state of divider that needs to persist across configuration changes. - */ -final class DividerState { - public boolean animateAfterRecentsDrawn; - public float mRatioPositionBeforeMinimized; +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/BaseTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/BaseTest.kt new file mode 100644 index 000000000000..2b162aec34ca --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/BaseTest.kt @@ -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.flicker + +import android.app.Instrumentation +import android.platform.test.annotations.Presubmit +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.entireScreenCovered +import com.android.server.wm.flicker.navBarLayerIsVisibleAtStartAndEnd +import com.android.server.wm.flicker.navBarLayerPositionAtStartAndEnd +import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible +import com.android.server.wm.flicker.statusBarLayerIsVisibleAtStartAndEnd +import com.android.server.wm.flicker.statusBarLayerPositionAtStartAndEnd +import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible +import com.android.server.wm.flicker.taskBarLayerIsVisibleAtStartAndEnd +import com.android.server.wm.flicker.taskBarWindowIsAlwaysVisible +import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper +import org.junit.Assume +import org.junit.Test + +/** + * Base test class containing common assertions for [ComponentMatcher.NAV_BAR], + * [ComponentMatcher.TASK_BAR], [ComponentMatcher.STATUS_BAR], and general assertions + * (layers visible in consecutive states, entire screen covered, etc.) + */ +abstract class BaseTest @JvmOverloads constructor( + protected val testSpec: FlickerTestParameter, + protected val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation(), + protected val tapl: LauncherInstrumentation = LauncherInstrumentation() +) { + init { + testSpec.setIsTablet( + WindowManagerStateHelper( + instrumentation, + clearCacheAfterParsing = false + ).currentState.wmState.isTablet + ) + } + + /** + * Specification of the test transition to execute + */ + abstract val transition: FlickerBuilder.() -> Unit + + /** + * Entry point for the test runner. It will use this method to initialize and cache + * flicker executions + */ + @FlickerBuilderProvider + fun buildFlicker(): FlickerBuilder { + return FlickerBuilder(instrumentation).apply { + setup { + testSpec.setIsTablet(wmHelper.currentState.wmState.isTablet) + } + transition() + } + } + + /** + * Checks that all parts of the screen are covered during the transition + */ + open fun entireScreenCovered() = testSpec.entireScreenCovered() + + /** + * Checks that the [ComponentMatcher.NAV_BAR] layer is visible during the whole transition + */ + @Presubmit + @Test + open fun navBarLayerIsVisibleAtStartAndEnd() { + Assume.assumeFalse(testSpec.isTablet) + testSpec.navBarLayerIsVisibleAtStartAndEnd() + } + + /** + * Checks the position of the [ComponentMatcher.NAV_BAR] at the start and end of the transition + */ + @Presubmit + @Test + open fun navBarLayerPositionAtStartAndEnd() { + Assume.assumeFalse(testSpec.isTablet) + testSpec.navBarLayerPositionAtStartAndEnd() + } + + /** + * Checks that the [ComponentMatcher.NAV_BAR] window is visible during the whole transition + * + * Note: Phones only + */ + @Presubmit + @Test + open fun navBarWindowIsAlwaysVisible() { + Assume.assumeFalse(testSpec.isTablet) + testSpec.navBarWindowIsAlwaysVisible() + } + + /** + * Checks that the [ComponentMatcher.TASK_BAR] layer is visible during the whole transition + */ + @Presubmit + @Test + open fun taskBarLayerIsVisibleAtStartAndEnd() { + Assume.assumeTrue(testSpec.isTablet) + testSpec.taskBarLayerIsVisibleAtStartAndEnd() + } + + /** + * Checks that the [ComponentMatcher.TASK_BAR] window is visible during the whole transition + * + * Note: Large screen only + */ + @Presubmit + @Test + open fun taskBarWindowIsAlwaysVisible() { + Assume.assumeTrue(testSpec.isTablet) + testSpec.taskBarWindowIsAlwaysVisible() + } + + /** + * Checks that the [ComponentMatcher.STATUS_BAR] layer is visible during the whole transition + */ + @Presubmit + @Test + open fun statusBarLayerIsVisibleAtStartAndEnd() = + testSpec.statusBarLayerIsVisibleAtStartAndEnd() + + /** + * Checks the position of the [ComponentMatcher.STATUS_BAR] at the start and end of the transition + */ + @Presubmit + @Test + open fun statusBarLayerPositionAtStartAndEnd() = testSpec.statusBarLayerPositionAtStartAndEnd() + + /** + * Checks that the [ComponentMatcher.STATUS_BAR] window is visible during the whole transition + */ + @Presubmit + @Test + open fun statusBarWindowIsAlwaysVisible() = testSpec.statusBarWindowIsAlwaysVisible() + + /** + * Checks that all layers that are visible on the trace, are visible for at least 2 + * consecutive entries. + */ + @Presubmit + @Test + open fun visibleLayersShownMoreThanOneConsecutiveEntry() { + testSpec.assertLayers { + this.visibleLayersShownMoreThanOneConsecutiveEntry() + } + } + + /** + * Checks that all windows that are visible on the trace, are visible for at least 2 + * consecutive entries. + */ + @Presubmit + @Test + open fun visibleWindowsShownMoreThanOneConsecutiveEntry() { + testSpec.assertWm { + this.visibleWindowsShownMoreThanOneConsecutiveEntry() + } + } +} 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..55979905cc4f 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 @@ -20,7 +20,10 @@ package com.android.wm.shell.flicker import android.view.Surface import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.helpers.WindowUtils -import com.android.server.wm.traces.common.FlickerComponentName +import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled +import com.android.server.wm.flicker.traces.layers.LayerTraceEntrySubject +import com.android.server.wm.flicker.traces.layers.LayersTraceSubject +import com.android.server.wm.traces.common.IComponentMatcher import com.android.server.wm.traces.common.region.Region fun FlickerTestParameter.appPairsDividerIsVisibleAtEnd() { @@ -43,6 +46,247 @@ fun FlickerTestParameter.appPairsDividerBecomesVisible() { } } +fun FlickerTestParameter.splitScreenDividerBecomesVisible() { + layerBecomesVisible(SPLIT_SCREEN_DIVIDER_COMPONENT) +} + +fun FlickerTestParameter.splitScreenDividerBecomesInvisible() { + assertLayers { + this.isVisible(SPLIT_SCREEN_DIVIDER_COMPONENT) + .then() + .isInvisible(SPLIT_SCREEN_DIVIDER_COMPONENT) + } +} + +fun FlickerTestParameter.layerBecomesVisible( + component: IComponentMatcher +) { + assertLayers { + this.isInvisible(component) + .then() + .isVisible(component) + } +} + +fun FlickerTestParameter.layerBecomesInvisible( + component: IComponentMatcher +) { + assertLayers { + this.isVisible(component) + .then() + .isInvisible(component) + } +} + +fun FlickerTestParameter.layerIsVisibleAtEnd( + component: IComponentMatcher +) { + assertLayersEnd { + this.isVisible(component) + } +} + +fun FlickerTestParameter.layerKeepVisible( + component: IComponentMatcher +) { + assertLayers { + this.isVisible(component) + } +} + +fun FlickerTestParameter.splitAppLayerBoundsBecomesVisible( + component: IComponentMatcher, + landscapePosLeft: Boolean, + portraitPosTop: Boolean +) { + assertLayers { + this.notContains(SPLIT_SCREEN_DIVIDER_COMPONENT.or(component)) + .then() + .isInvisible(SPLIT_SCREEN_DIVIDER_COMPONENT.or(component)) + .then() + .splitAppLayerBoundsSnapToDivider( + component, landscapePosLeft, portraitPosTop, endRotation) + } +} + +fun FlickerTestParameter.splitAppLayerBoundsBecomesVisibleByDrag( + component: IComponentMatcher +) { + assertLayers { + if (isShellTransitionsEnabled) { + this.notContains(SPLIT_SCREEN_DIVIDER_COMPONENT.or(component)) + .then() + .isInvisible(SPLIT_SCREEN_DIVIDER_COMPONENT.or(component)) + .then() + // TODO(b/245472831): Verify the component should snap to divider. + .isVisible(component) + } else { + this.notContains(SPLIT_SCREEN_DIVIDER_COMPONENT.or(component)) + .then() + .isInvisible(SPLIT_SCREEN_DIVIDER_COMPONENT.or(component)) + .then() + // TODO(b/245472831): Verify the component should snap to divider. + .isVisible(component) + .then() + .isInvisible(component, isOptional = true) + .then() + .isVisible(component) + } + } +} + +fun FlickerTestParameter.splitAppLayerBoundsBecomesInvisible( + component: IComponentMatcher, + landscapePosLeft: Boolean, + portraitPosTop: Boolean +) { + assertLayers { + this.splitAppLayerBoundsSnapToDivider( + component, landscapePosLeft, portraitPosTop, endRotation) + .then() + .isVisible(component, true) + .then() + .isInvisible(component) + } +} + +fun FlickerTestParameter.splitAppLayerBoundsIsVisibleAtEnd( + component: IComponentMatcher, + landscapePosLeft: Boolean, + portraitPosTop: Boolean +) { + assertLayersEnd { + splitAppLayerBoundsSnapToDivider(component, landscapePosLeft, portraitPosTop, endRotation) + } +} + +fun FlickerTestParameter.splitAppLayerBoundsKeepVisible( + component: IComponentMatcher, + landscapePosLeft: Boolean, + portraitPosTop: Boolean +) { + assertLayers { + splitAppLayerBoundsSnapToDivider(component, landscapePosLeft, portraitPosTop, endRotation) + } +} + +fun FlickerTestParameter.splitAppLayerBoundsChanges( + component: IComponentMatcher, + landscapePosLeft: Boolean, + portraitPosTop: Boolean +) { + assertLayers { + if (landscapePosLeft) { + this.splitAppLayerBoundsSnapToDivider( + component, landscapePosLeft, portraitPosTop, endRotation) + } else { + this.splitAppLayerBoundsSnapToDivider( + component, landscapePosLeft, portraitPosTop, endRotation) + .then() + .isInvisible(component) + .then() + .splitAppLayerBoundsSnapToDivider( + component, landscapePosLeft, portraitPosTop, endRotation) + } + } +} + +fun LayersTraceSubject.splitAppLayerBoundsSnapToDivider( + component: IComponentMatcher, + landscapePosLeft: Boolean, + portraitPosTop: Boolean, + rotation: Int +): LayersTraceSubject { + return invoke("splitAppLayerBoundsSnapToDivider") { + it.splitAppLayerBoundsSnapToDivider(component, landscapePosLeft, portraitPosTop, rotation) + } +} + +fun LayerTraceEntrySubject.splitAppLayerBoundsSnapToDivider( + component: IComponentMatcher, + landscapePosLeft: Boolean, + portraitPosTop: Boolean, + rotation: Int +): LayerTraceEntrySubject { + val displayBounds = WindowUtils.getDisplayBounds(rotation) + return invoke { + val dividerRegion = layer(SPLIT_SCREEN_DIVIDER_COMPONENT).visibleRegion.region + visibleRegion(component).coversAtMost( + if (displayBounds.width > displayBounds.height) { + if (landscapePosLeft) { + Region.from( + 0, + 0, + (dividerRegion.bounds.left + dividerRegion.bounds.right) / 2, + displayBounds.bounds.bottom) + } else { + Region.from( + (dividerRegion.bounds.left + dividerRegion.bounds.right) / 2, + 0, + displayBounds.bounds.right, + displayBounds.bounds.bottom + ) + } + } else { + if (portraitPosTop) { + Region.from( + 0, + 0, + displayBounds.bounds.right, + (dividerRegion.bounds.top + dividerRegion.bounds.bottom) / 2) + } else { + Region.from( + 0, + (dividerRegion.bounds.top + dividerRegion.bounds.bottom) / 2, + displayBounds.bounds.right, + displayBounds.bounds.bottom + ) + } + } + ) + } +} + +fun FlickerTestParameter.appWindowBecomesVisible( + component: IComponentMatcher +) { + assertWm { + this.isAppWindowInvisible(component) + .then() + .notContains(component, isOptional = true) + .then() + .isAppWindowInvisible(component, isOptional = true) + .then() + .isAppWindowVisible(component) + } +} + +fun FlickerTestParameter.appWindowBecomesInvisible( + component: IComponentMatcher +) { + assertWm { + this.isAppWindowVisible(component) + .then() + .isAppWindowInvisible(component) + } +} + +fun FlickerTestParameter.appWindowIsVisibleAtEnd( + component: IComponentMatcher +) { + assertWmEnd { + this.isAppWindowVisible(component) + } +} + +fun FlickerTestParameter.appWindowKeepVisible( + component: IComponentMatcher +) { + assertWm { + this.isAppWindowVisible(component) + } +} + fun FlickerTestParameter.dockedStackDividerIsVisibleAtEnd() { assertLayersEnd { this.isVisible(DOCKED_STACK_DIVIDER_COMPONENT) @@ -73,7 +317,7 @@ fun FlickerTestParameter.dockedStackDividerNotExistsAtEnd() { fun FlickerTestParameter.appPairsPrimaryBoundsIsVisibleAtEnd( rotation: Int, - primaryComponent: FlickerComponentName + primaryComponent: IComponentMatcher ) { assertLayersEnd { val dividerRegion = layer(APP_PAIR_SPLIT_DIVIDER_COMPONENT).visibleRegion.region @@ -84,7 +328,7 @@ fun FlickerTestParameter.appPairsPrimaryBoundsIsVisibleAtEnd( fun FlickerTestParameter.dockedStackPrimaryBoundsIsVisibleAtEnd( rotation: Int, - primaryComponent: FlickerComponentName + primaryComponent: IComponentMatcher ) { assertLayersEnd { val dividerRegion = layer(DOCKED_STACK_DIVIDER_COMPONENT).visibleRegion.region @@ -95,7 +339,7 @@ fun FlickerTestParameter.dockedStackPrimaryBoundsIsVisibleAtEnd( fun FlickerTestParameter.appPairsSecondaryBoundsIsVisibleAtEnd( rotation: Int, - secondaryComponent: FlickerComponentName + secondaryComponent: IComponentMatcher ) { assertLayersEnd { val dividerRegion = layer(APP_PAIR_SPLIT_DIVIDER_COMPONENT).visibleRegion.region @@ -106,7 +350,7 @@ fun FlickerTestParameter.appPairsSecondaryBoundsIsVisibleAtEnd( fun FlickerTestParameter.dockedStackSecondaryBoundsIsVisibleAtEnd( rotation: Int, - secondaryComponent: FlickerComponentName + secondaryComponent: IComponentMatcher ) { assertLayersEnd { val dividerRegion = layer(DOCKED_STACK_DIVIDER_COMPONENT).visibleRegion.region @@ -118,21 +362,29 @@ 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 +} 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..53dd8b04afeb 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 @@ -17,8 +17,14 @@ @file:JvmName("CommonConstants") package com.android.wm.shell.flicker -import com.android.server.wm.traces.common.FlickerComponentName +import com.android.server.wm.traces.common.ComponentNameMatcher 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 APP_PAIR_SPLIT_DIVIDER_COMPONENT = ComponentNameMatcher("", "AppPairSplitDivider#") +val DOCKED_STACK_DIVIDER_COMPONENT = ComponentNameMatcher("", "DockedStackDivider#") +val SPLIT_SCREEN_DIVIDER_COMPONENT = ComponentNameMatcher("", "StageCoordinatorSplitDivider#") +val SPLIT_DECOR_MANAGER = ComponentNameMatcher("", "SplitDecorManager#") + +enum class Direction { + UP, DOWN, LEFT, RIGHT +} 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/bubble/BaseBubbleScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/BaseBubbleScreen.kt index 278ba9b0f4db..1390334f7938 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/BaseBubbleScreen.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/BaseBubbleScreen.kt @@ -17,40 +17,37 @@ package com.android.wm.shell.flicker.bubble import android.app.INotificationManager -import android.app.Instrumentation import android.app.NotificationManager import android.content.Context import android.os.ServiceManager import android.view.Surface -import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.By import androidx.test.uiautomator.UiObject2 import androidx.test.uiautomator.Until import com.android.server.wm.flicker.Flicker -import com.android.server.wm.flicker.FlickerBuilderProvider import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.FlickerTestParameterFactory import com.android.server.wm.flicker.dsl.FlickerBuilder import com.android.server.wm.flicker.helpers.SYSTEMUI_PACKAGE +import com.android.wm.shell.flicker.BaseTest import com.android.wm.shell.flicker.helpers.LaunchBubbleHelper import org.junit.runners.Parameterized /** * Base configurations for Bubble flicker tests */ -abstract class BaseBubbleScreen(protected val testSpec: FlickerTestParameter) { +abstract class BaseBubbleScreen( + testSpec: FlickerTestParameter +) : BaseTest(testSpec) { - protected val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation() protected val context: Context = instrumentation.context protected val testApp = LaunchBubbleHelper(instrumentation) - protected val notifyManager = INotificationManager.Stub.asInterface( + private val notifyManager = INotificationManager.Stub.asInterface( ServiceManager.getService(Context.NOTIFICATION_SERVICE)) - protected val uid = context.packageManager.getApplicationInfo( - testApp.component.packageName, 0).uid - - protected abstract val transition: FlickerBuilder.() -> Unit + private val uid = context.packageManager.getApplicationInfo( + testApp.`package`, 0).uid @JvmOverloads protected open fun buildTransition( @@ -58,21 +55,17 @@ abstract class BaseBubbleScreen(protected val testSpec: FlickerTestParameter) { ): FlickerBuilder.() -> Unit { return { setup { - test { - notifyManager.setBubblesAllowed(testApp.component.packageName, - uid, NotificationManager.BUBBLE_PREFERENCE_ALL) - testApp.launchViaIntent(wmHelper) - waitAndGetAddBubbleBtn() - waitAndGetCancelAllBtn() - } + notifyManager.setBubblesAllowed(testApp.`package`, + uid, NotificationManager.BUBBLE_PREFERENCE_ALL) + testApp.launchViaIntent(wmHelper) + waitAndGetAddBubbleBtn() + waitAndGetCancelAllBtn() } teardown { - test { - notifyManager.setBubblesAllowed(testApp.component.packageName, - uid, NotificationManager.BUBBLE_PREFERENCE_NONE) - testApp.exit() - } + notifyManager.setBubblesAllowed(testApp.`package`, + uid, NotificationManager.BUBBLE_PREFERENCE_NONE) + testApp.exit() } extraSpec(this) @@ -84,20 +77,12 @@ abstract class BaseBubbleScreen(protected val testSpec: FlickerTestParameter) { protected fun Flicker.waitAndGetCancelAllBtn(): UiObject2? = device.wait(Until.findObject( By.text("Cancel All Bubble")), FIND_OBJECT_TIMEOUT) - @FlickerBuilderProvider - fun buildFlicker(): FlickerBuilder { - return FlickerBuilder(instrumentation).apply { - transition(this) - } - } - companion object { @Parameterized.Parameters(name = "{0}") @JvmStatic fun getParams(): List<FlickerTestParameter> { return FlickerTestParameterFactory.getInstance() - .getConfigNonRotationTests(supportedRotations = listOf(Surface.ROTATION_0), - repetitions = 3) + .getConfigNonRotationTests(supportedRotations = listOf(Surface.ROTATION_0)) } const val FIND_OBJECT_TIMEOUT = 2000L diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/DismissBubbleScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/DismissBubbleScreen.kt index b137e92881a5..ac4de4780746 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/DismissBubbleScreen.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/DismissBubbleScreen.kt @@ -18,6 +18,7 @@ package com.android.wm.shell.flicker.bubble import android.content.Context import android.graphics.Point +import android.platform.test.annotations.Postsubmit import android.platform.test.annotations.Presubmit import android.util.DisplayMetrics import android.view.WindowManager @@ -28,8 +29,8 @@ import com.android.server.wm.flicker.FlickerParametersRunnerFactory import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.annotation.Group4 import com.android.server.wm.flicker.dsl.FlickerBuilder -import org.junit.runner.RunWith import org.junit.Test +import org.junit.runner.RunWith import org.junit.runners.Parameterized /** @@ -49,19 +50,21 @@ open class DismissBubbleScreen(testSpec: FlickerTestParameter) : BaseBubbleScree private val wm = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager private val displaySize = DisplayMetrics() + /** {@inheritDoc} */ override val transition: FlickerBuilder.() -> Unit get() = buildTransition { setup { - eachRun { - val addBubbleBtn = waitAndGetAddBubbleBtn() - addBubbleBtn?.click() ?: error("Add Bubble not found") - } + val addBubbleBtn = waitAndGetAddBubbleBtn() + addBubbleBtn?.click() ?: error("Add Bubble not found") } transitions { - wm.run { wm.getDefaultDisplay().getMetrics(displaySize) } + wm.run { wm.defaultDisplay.getMetrics(displaySize) } val dist = Point((displaySize.widthPixels / 2), displaySize.heightPixels) - val showBubble = device.wait(Until.findObject( - By.res(SYSTEM_UI_PACKAGE, BUBBLE_RES_NAME)), FIND_OBJECT_TIMEOUT) + val showBubble = device.wait( + Until.findObject( + By.res(SYSTEM_UI_PACKAGE, BUBBLE_RES_NAME) + ), FIND_OBJECT_TIMEOUT + ) showBubble?.run { drag(dist, 1000) } ?: error("Show bubble not found") } } @@ -70,7 +73,25 @@ open class DismissBubbleScreen(testSpec: FlickerTestParameter) : BaseBubbleScree @Test open fun testAppIsAlwaysVisible() { testSpec.assertLayers { - this.isVisible(testApp.component) + this.isVisible(testApp) } } + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun taskBarLayerIsVisibleAtStartAndEnd() = + super.taskBarLayerIsVisibleAtStartAndEnd() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun taskBarWindowIsAlwaysVisible() = + super.taskBarWindowIsAlwaysVisible() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun visibleLayersShownMoreThanOneConsecutiveEntry() = + super.visibleLayersShownMoreThanOneConsecutiveEntry() } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/ExpandBubbleScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/ExpandBubbleScreen.kt index f288b0a24d9d..7807854ac70a 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/ExpandBubbleScreen.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/ExpandBubbleScreen.kt @@ -24,8 +24,8 @@ import com.android.server.wm.flicker.FlickerParametersRunnerFactory import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.annotation.Group4 import com.android.server.wm.flicker.dsl.FlickerBuilder -import org.junit.runner.RunWith import org.junit.Test +import org.junit.runner.RunWith import org.junit.runners.Parameterized /** @@ -44,17 +44,19 @@ import org.junit.runners.Parameterized @Group4 open class ExpandBubbleScreen(testSpec: FlickerTestParameter) : BaseBubbleScreen(testSpec) { + /** {@inheritDoc} */ override val transition: FlickerBuilder.() -> Unit get() = buildTransition { setup { - test { - val addBubbleBtn = waitAndGetAddBubbleBtn() - addBubbleBtn?.click() ?: error("Add Bubble not found") - } + val addBubbleBtn = waitAndGetAddBubbleBtn() + addBubbleBtn?.click() ?: error("Add Bubble not found") } transitions { - val showBubble = device.wait(Until.findObject( - By.res("com.android.systemui", "bubble_view")), FIND_OBJECT_TIMEOUT) + val showBubble = device.wait( + Until.findObject( + By.res("com.android.systemui", "bubble_view") + ), FIND_OBJECT_TIMEOUT + ) showBubble?.run { showBubble.click() } ?: error("Bubble notify not found") } } @@ -63,7 +65,7 @@ open class ExpandBubbleScreen(testSpec: FlickerTestParameter) : BaseBubbleScreen @Test open fun testAppIsAlwaysVisible() { testSpec.assertLayers { - this.isVisible(testApp.component) + this.isVisible(testApp) } } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/LaunchBubbleFromLockScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/LaunchBubbleFromLockScreen.kt index 684e5cad0e67..49681e140cef 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/LaunchBubbleFromLockScreen.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/LaunchBubbleFromLockScreen.kt @@ -16,10 +16,10 @@ package com.android.wm.shell.flicker.bubble -import android.platform.test.annotations.Presubmit +import android.platform.test.annotations.FlakyTest +import android.platform.test.annotations.Postsubmit import android.view.WindowInsets import android.view.WindowManager -import androidx.test.filters.FlakyTest import androidx.test.filters.RequiresDevice import androidx.test.uiautomator.By import androidx.test.uiautomator.Until @@ -27,10 +27,8 @@ import com.android.server.wm.flicker.FlickerParametersRunnerFactory import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.annotation.Group4 import com.android.server.wm.flicker.dsl.FlickerBuilder -import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled -import org.junit.Assume -import org.junit.runner.RunWith import org.junit.Test +import org.junit.runner.RunWith import org.junit.runners.Parameterized /** @@ -47,36 +45,43 @@ import org.junit.runners.Parameterized @Group4 class LaunchBubbleFromLockScreen(testSpec: FlickerTestParameter) : BaseBubbleScreen(testSpec) { + /** {@inheritDoc} */ override val transition: FlickerBuilder.() -> Unit get() = buildTransition { setup { - eachRun { - val addBubbleBtn = waitAndGetAddBubbleBtn() - addBubbleBtn?.click() ?: error("Bubble widget not found") - device.sleep() - wmHelper.waitFor("noAppWindowsOnTop") { - it.wmState.topVisibleAppWindow.isEmpty() - } - device.wakeUp() - } + val addBubbleBtn = waitAndGetAddBubbleBtn() + addBubbleBtn?.click() ?: error("Bubble widget not found") + device.sleep() + wmHelper.StateSyncBuilder() + .withoutTopVisibleAppWindows() + .waitForAndVerify() + device.wakeUp() } transitions { // Swipe & wait for the notification shade to expand so all can be seen val wm = context.getSystemService(WindowManager::class.java) - val metricInsets = wm.getCurrentWindowMetrics().windowInsets + ?: error("Unable to obtain WM service") + val metricInsets = wm.currentWindowMetrics.windowInsets val insets = metricInsets.getInsetsIgnoringVisibility( - WindowInsets.Type.statusBars() - or WindowInsets.Type.displayCutout()) - device.swipe(100, insets.top + 100, 100, device.getDisplayHeight() / 2, 4) + WindowInsets.Type.statusBars() + or WindowInsets.Type.displayCutout() + ) + device.swipe(100, insets.top + 100, 100, device.displayHeight / 2, 4) device.waitForIdle(2000) instrumentation.uiAutomation.syncInputTransactions() - val notification = device.wait(Until.findObject( - By.text("BubbleChat")), FIND_OBJECT_TIMEOUT) + val notification = device.wait( + Until.findObject( + By.text("BubbleChat") + ), FIND_OBJECT_TIMEOUT + ) notification?.click() ?: error("Notification not found") instrumentation.uiAutomation.syncInputTransactions() - val showBubble = device.wait(Until.findObject( - By.res("com.android.systemui", "bubble_view")), FIND_OBJECT_TIMEOUT) + val showBubble = device.wait( + Until.findObject( + By.res("com.android.systemui", "bubble_view") + ), FIND_OBJECT_TIMEOUT + ) showBubble?.click() ?: error("Bubble notify not found") instrumentation.uiAutomation.syncInputTransactions() val cancelAllBtn = waitAndGetCancelAllBtn() @@ -84,21 +89,71 @@ class LaunchBubbleFromLockScreen(testSpec: FlickerTestParameter) : BaseBubbleScr } } - @Presubmit + @FlakyTest(bugId = 242088970) @Test fun testAppIsVisibleAtEnd() { - Assume.assumeFalse(isShellTransitionsEnabled) testSpec.assertLayersEnd { - this.isVisible(testApp.component) + this.isVisible(testApp) } } + /** {@inheritDoc} */ @FlakyTest @Test - fun testAppIsVisibleAtEnd_ShellTransit() { - Assume.assumeTrue(isShellTransitionsEnabled) - testSpec.assertLayersEnd { - this.isVisible(testApp.component) - } - } + override fun visibleLayersShownMoreThanOneConsecutiveEntry() = + super.visibleLayersShownMoreThanOneConsecutiveEntry() + + /** {@inheritDoc} */ + @FlakyTest(bugId = 206753786) + @Test + override fun navBarLayerIsVisibleAtStartAndEnd() = + super.navBarLayerIsVisibleAtStartAndEnd() + + /** {@inheritDoc} */ + @FlakyTest(bugId = 206753786) + @Test + override fun navBarLayerPositionAtStartAndEnd() = + super.navBarLayerPositionAtStartAndEnd() + + /** {@inheritDoc} */ + @FlakyTest(bugId = 206753786) + @Test + override fun navBarWindowIsAlwaysVisible() = + super.navBarWindowIsAlwaysVisible() + + /** {@inheritDoc} */ + @FlakyTest(bugId = 242088970) + @Test + override fun taskBarLayerIsVisibleAtStartAndEnd() = + super.taskBarLayerIsVisibleAtStartAndEnd() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun taskBarWindowIsAlwaysVisible() = + super.taskBarWindowIsAlwaysVisible() + + /** {@inheritDoc} */ + @FlakyTest(bugId = 242088970) + @Test + override fun statusBarLayerIsVisibleAtStartAndEnd() = + super.statusBarLayerIsVisibleAtStartAndEnd() + + /** {@inheritDoc} */ + @FlakyTest(bugId = 242088970) + @Test + override fun statusBarLayerPositionAtStartAndEnd() = + super.statusBarLayerPositionAtStartAndEnd() + + /** {@inheritDoc} */ + @FlakyTest(bugId = 242088970) + @Test + override fun statusBarWindowIsAlwaysVisible() = + super.statusBarWindowIsAlwaysVisible() + + /** {@inheritDoc} */ + @FlakyTest(bugId = 242088970) + @Test + override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = + super.visibleWindowsShownMoreThanOneConsecutiveEntry() } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/LaunchBubbleScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/LaunchBubbleScreen.kt index 0bb4d398bff4..effd33095530 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/LaunchBubbleScreen.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/LaunchBubbleScreen.kt @@ -41,6 +41,7 @@ import org.junit.runners.Parameterized @Group4 open class LaunchBubbleScreen(testSpec: FlickerTestParameter) : BaseBubbleScreen(testSpec) { + /** {@inheritDoc} */ override val transition: FlickerBuilder.() -> Unit get() = buildTransition { transitions { @@ -53,7 +54,7 @@ open class LaunchBubbleScreen(testSpec: FlickerTestParameter) : BaseBubbleScreen @Test open fun testAppIsAlwaysVisible() { testSpec.assertLayers { - this.isVisible(testApp.component) + this.isVisible(testApp) } } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/MultiBubblesScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/MultiBubblesScreen.kt index 8d1e315e2d5e..fac0f73e9e10 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/MultiBubblesScreen.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/MultiBubblesScreen.kt @@ -20,6 +20,7 @@ import android.os.SystemClock import android.platform.test.annotations.Presubmit import androidx.test.filters.RequiresDevice import androidx.test.uiautomator.By +import androidx.test.uiautomator.UiObject2 import androidx.test.uiautomator.Until import com.android.server.wm.flicker.FlickerParametersRunnerFactory import com.android.server.wm.flicker.FlickerTestParameter @@ -28,8 +29,8 @@ import com.android.server.wm.flicker.dsl.FlickerBuilder import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled import org.junit.Assume import org.junit.Before -import org.junit.runner.RunWith import org.junit.Test +import org.junit.runner.RunWith import org.junit.runners.Parameterized /** @@ -51,25 +52,31 @@ open class MultiBubblesScreen(testSpec: FlickerTestParameter) : BaseBubbleScreen Assume.assumeFalse(isShellTransitionsEnabled) } + /** {@inheritDoc} */ override val transition: FlickerBuilder.() -> Unit get() = buildTransition { setup { - test { - for (i in 1..3) { - val addBubbleBtn = waitAndGetAddBubbleBtn() - addBubbleBtn?.run { addBubbleBtn.click() } ?: error("Add Bubble not found") - } - val showBubble = device.wait(Until.findObject( - By.res(SYSTEM_UI_PACKAGE, BUBBLE_RES_NAME)), FIND_OBJECT_TIMEOUT) - showBubble?.run { showBubble.click() } ?: error("Show bubble not found") + for (i in 1..3) { + val addBubbleBtn = waitAndGetAddBubbleBtn() ?: error("Add Bubble not found") + addBubbleBtn.click() SystemClock.sleep(1000) } + val showBubble = device.wait( + Until.findObject( + By.res(SYSTEM_UI_PACKAGE, BUBBLE_RES_NAME) + ), FIND_OBJECT_TIMEOUT + ) ?: error("Show bubble not found") + showBubble.click() + SystemClock.sleep(1000) } transitions { - val bubbles = device.wait(Until.findObjects( - By.res(SYSTEM_UI_PACKAGE, BUBBLE_RES_NAME)), FIND_OBJECT_TIMEOUT) + val bubbles: List<UiObject2> = device.wait( + Until.findObjects( + By.res(SYSTEM_UI_PACKAGE, BUBBLE_RES_NAME) + ), FIND_OBJECT_TIMEOUT + ) ?: error("No bubbles found") for (entry in bubbles) { - entry?.run { entry.click() } ?: error("Bubble not found") + entry.click() SystemClock.sleep(1000) } } @@ -79,7 +86,7 @@ open class MultiBubblesScreen(testSpec: FlickerTestParameter) : BaseBubbleScreen @Test open fun testAppIsAlwaysVisible() { testSpec.assertLayers { - this.isVisible(testApp.component) + this.isVisible(testApp) } } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/MultiBubblesScreenShellTransit.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/MultiBubblesScreenShellTransit.kt index ddebb6fed636..971097deed06 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/MultiBubblesScreenShellTransit.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/MultiBubblesScreenShellTransit.kt @@ -16,7 +16,7 @@ package com.android.wm.shell.flicker.bubble -import androidx.test.filters.FlakyTest +import android.platform.test.annotations.FlakyTest import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.FlickerParametersRunnerFactory import com.android.server.wm.flicker.FlickerTestParameter diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/AppPairsHelper.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/AppPairsHelper.kt index 41cd31aabf05..826cc2ef16d6 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/AppPairsHelper.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/AppPairsHelper.kt @@ -17,44 +17,10 @@ package com.android.wm.shell.flicker.helpers import android.app.Instrumentation -import com.android.server.wm.flicker.Flicker -import com.android.server.wm.flicker.helpers.WindowUtils -import com.android.server.wm.traces.common.FlickerComponentName -import com.android.server.wm.traces.common.region.Region +import com.android.server.wm.traces.common.ComponentNameMatcher class AppPairsHelper( instrumentation: Instrumentation, activityLabel: String, - component: FlickerComponentName -) : BaseAppHelper(instrumentation, activityLabel, component) { - fun getPrimaryBounds(dividerBounds: Region): Region { - val primaryAppBounds = Region.from(0, 0, dividerBounds.bounds.right, - dividerBounds.bounds.bottom + WindowUtils.dockedStackDividerInset) - return primaryAppBounds - } - - fun getSecondaryBounds(dividerBounds: Region): Region { - val displayBounds = WindowUtils.displayBounds - val secondaryAppBounds = Region.from(0, - dividerBounds.bounds.bottom - WindowUtils.dockedStackDividerInset, - displayBounds.right, displayBounds.bottom - WindowUtils.navigationBarFrameHeight) - return secondaryAppBounds - } - - companion object { - const val TEST_REPETITIONS = 1 - const val TIMEOUT_MS = 3_000L - - fun Flicker.waitAppsShown(app1: SplitScreenHelper?, app2: SplitScreenHelper?) { - wmHelper.waitFor("primaryAndSecondaryAppsVisible") { dump -> - val primaryAppVisible = app1?.let { - dump.wmState.isWindowSurfaceShown(app1.defaultWindowName) - } ?: false - val secondaryAppVisible = app2?.let { - dump.wmState.isWindowSurfaceShown(app2.defaultWindowName) - } ?: false - primaryAppVisible && secondaryAppVisible - } - } - } -} + component: ComponentNameMatcher +) : BaseAppHelper(instrumentation, activityLabel, component) diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/BaseAppHelper.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/BaseAppHelper.kt index 3dd9e0572947..01ba9907c24c 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/BaseAppHelper.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/BaseAppHelper.kt @@ -19,7 +19,6 @@ package com.android.wm.shell.flicker.helpers import android.app.Instrumentation import android.content.pm.PackageManager.FEATURE_LEANBACK import android.content.pm.PackageManager.FEATURE_LEANBACK_ONLY -import android.os.SystemProperties import android.support.test.launcherhelper.LauncherStrategyFactory import android.util.Log import androidx.test.uiautomator.By @@ -27,29 +26,26 @@ import androidx.test.uiautomator.UiObject2 import androidx.test.uiautomator.Until import com.android.compatibility.common.util.SystemUtil import com.android.server.wm.flicker.helpers.StandardAppHelper -import com.android.server.wm.traces.common.FlickerComponentName +import com.android.server.wm.traces.common.IComponentNameMatcher import java.io.IOException abstract class BaseAppHelper( instrumentation: Instrumentation, launcherName: String, - component: FlickerComponentName + component: IComponentNameMatcher ) : StandardAppHelper( instrumentation, launcherName, component, LauncherStrategyFactory.getInstance(instrumentation).launcherStrategy ) { - private val appSelector = By.pkg(component.packageName).depth(0) + private val appSelector = By.pkg(`package`).depth(0) protected val isTelevision: Boolean get() = context.packageManager.run { hasSystemFeature(FEATURE_LEANBACK) || hasSystemFeature(FEATURE_LEANBACK_ONLY) } - val defaultWindowName: String - get() = component.toWindowName() - val ui: UiObject2? get() = uiDevice.findObject(appSelector) @@ -60,9 +56,6 @@ abstract class BaseAppHelper( companion object { private const val APP_CLOSE_WAIT_TIME_MS = 3_000L - fun isShellTransitionsEnabled() = - SystemProperties.getBoolean("persist.wm.debug.shell_transit", false) - fun executeShellCommand(instrumentation: Instrumentation, cmd: String) { try { SystemUtil.runShellCommand(instrumentation, cmd) diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/ImeAppHelper.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/ImeAppHelper.kt index cc5b9f9eb26d..2e690de666f4 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/ImeAppHelper.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/ImeAppHelper.kt @@ -18,7 +18,6 @@ package com.android.wm.shell.flicker.helpers import android.app.Instrumentation import androidx.test.uiautomator.By -import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.Until import com.android.server.wm.flicker.helpers.FIND_TIMEOUT import com.android.server.wm.traces.parser.toFlickerComponent @@ -35,8 +34,7 @@ open class ImeAppHelper(instrumentation: Instrumentation) : BaseAppHelper( * * @param wmHelper Helper used to wait for WindowManager states */ - @JvmOverloads - open fun openIME(wmHelper: WindowManagerStateHelper? = null) { + open fun openIME(wmHelper: WindowManagerStateHelper) { if (!isTelevision) { val editText = uiDevice.wait( Until.findObject(By.res(getPackage(), "plain_text_input")), @@ -47,7 +45,9 @@ open class ImeAppHelper(instrumentation: Instrumentation) : BaseAppHelper( "was left in an unknown state (e.g. in split screen)" } editText.click() - waitAndAssertIMEShown(uiDevice, wmHelper) + wmHelper.StateSyncBuilder() + .withImeShown() + .waitForAndVerify() } else { // If we do the same thing as above - editText.click() - on TV, that's going to force TV // into the touch mode. We really don't want that. @@ -55,36 +55,22 @@ open class ImeAppHelper(instrumentation: Instrumentation) : BaseAppHelper( } } - protected fun waitAndAssertIMEShown( - device: UiDevice, - wmHelper: WindowManagerStateHelper? = null - ) { - if (wmHelper == null) { - device.waitForIdle() - } else { - wmHelper.waitImeShown() - } - } - /** * Opens the IME and wait for it to be gone * * @param wmHelper Helper used to wait for WindowManager states */ - @JvmOverloads - open fun closeIME(wmHelper: WindowManagerStateHelper? = null) { + open fun closeIME(wmHelper: WindowManagerStateHelper) { if (!isTelevision) { uiDevice.pressBack() // Using only the AccessibilityInfo it is not possible to identify if the IME is active - if (wmHelper == null) { - uiDevice.waitForIdle() - } else { - wmHelper.waitImeGone() - } + wmHelper.StateSyncBuilder() + .withImeGone() + .waitForAndVerify() } else { // While pressing the back button should close the IME on TV as well, it may also lead // to the app closing. So let's instead just ask the app to close the IME. launchViaIntent(action = Components.ImeActivity.ACTION_CLOSE_IME) } } -}
\ No newline at end of file +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/LaunchBubbleHelper.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/LaunchBubbleHelper.kt index 6695c17ed514..1b8a44bbe78e 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/LaunchBubbleHelper.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/LaunchBubbleHelper.kt @@ -27,7 +27,6 @@ class LaunchBubbleHelper(instrumentation: Instrumentation) : BaseAppHelper( ) { companion object { - const val TEST_REPETITIONS = 1 const val TIMEOUT_MS = 3_000L } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/MultiWindowHelper.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/MultiWindowHelper.kt index 12ccbafce651..245a82f938b3 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/MultiWindowHelper.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/MultiWindowHelper.kt @@ -19,12 +19,12 @@ package com.android.wm.shell.flicker.helpers import android.app.Instrumentation import android.content.Context import android.provider.Settings -import com.android.server.wm.traces.common.FlickerComponentName +import com.android.server.wm.traces.common.ComponentNameMatcher class MultiWindowHelper( instrumentation: Instrumentation, activityLabel: String, - componentsInfo: FlickerComponentName + componentsInfo: ComponentNameMatcher ) : BaseAppHelper(instrumentation, activityLabel, componentsInfo) { companion object { diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/PipAppHelper.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/PipAppHelper.kt index e9d438a569d5..bdc05e7ecb52 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 @@ -19,13 +19,13 @@ package com.android.wm.shell.flicker.helpers import android.app.Instrumentation import android.media.session.MediaController import android.media.session.MediaSessionManager -import android.os.SystemClock import androidx.test.uiautomator.By import androidx.test.uiautomator.BySelector import androidx.test.uiautomator.Until import com.android.server.wm.flicker.helpers.FIND_TIMEOUT import com.android.server.wm.flicker.helpers.SYSTEMUI_PACKAGE import com.android.server.wm.traces.common.Rect +import com.android.server.wm.traces.common.WindowManagerConditionsFactory import com.android.server.wm.traces.parser.toFlickerComponent import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper import com.android.wm.shell.flicker.pip.tv.closeTvPipWindow @@ -39,15 +39,15 @@ 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 { - it.packageName == component.packageName + it.packageName == `package` } fun clickObject(resId: String) { - val selector = By.res(component.packageName, resId) + val selector = By.res(`package`, resId) val obj = uiDevice.findObject(selector) ?: error("Could not find `$resId` object") if (!isTelevision) { @@ -69,8 +69,14 @@ 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(WindowManagerConditionsFactory.hasPipWindow()) + ) + + wmHelper.StateSyncBuilder() + .withPipShown() + .waitForAndVerify() } /** @@ -85,7 +91,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() @@ -93,36 +99,47 @@ class PipAppHelper(instrumentation: Instrumentation) : BaseAppHelper( return false } - @JvmOverloads - fun clickEnterPipButton(wmHelper: WindowManagerStateHelper? = null) { + fun clickEnterPipButton(wmHelper: WindowManagerStateHelper) { clickObject(ENTER_PIP_BUTTON_ID) // Wait on WMHelper or simply wait for 3 seconds - wmHelper?.waitPipShown() ?: SystemClock.sleep(3_000) + wmHelper.StateSyncBuilder() + .withPipShown() + .waitForAndVerify() // 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(`package`, 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() @@ -132,7 +149,7 @@ class PipAppHelper(instrumentation: Instrumentation) : BaseAppHelper( } private fun getWindowRect(wmHelper: WindowManagerStateHelper): Rect { - val windowRegion = wmHelper.getWindowRegion(component) + val windowRegion = wmHelper.getWindowRegion(this) require(!windowRegion.isEmpty) { "Unable to find a PIP window in the current state" } @@ -152,14 +169,16 @@ 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()) } // Wait for animation to complete. - wmHelper.waitPipGone() - wmHelper.waitForHomeActivityVisible() + wmHelper.StateSyncBuilder() + .withPipGone() + .withHomeActivityVisible() + .waitForAndVerify() } /** @@ -172,11 +191,13 @@ 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() - wmHelper.waitForAppTransitionIdle() + wmHelper.StateSyncBuilder() + .withPipGone() + .withFullScreenApp(this) + .waitForAndVerify() } /** @@ -186,7 +207,9 @@ class PipAppHelper(instrumentation: Instrumentation) : BaseAppHelper( val windowRect = getWindowRect(wmHelper) uiDevice.click(windowRect.centerX(), windowRect.centerY()) uiDevice.click(windowRect.centerX(), windowRect.centerY()) - wmHelper.waitForAppTransitionIdle() + wmHelper.StateSyncBuilder() + .withAppTransitionIdle() + .waitForAndVerify() } companion object { @@ -194,5 +217,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..52e5d7e14ab9 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,39 +17,338 @@ package com.android.wm.shell.flicker.helpers import android.app.Instrumentation -import android.content.res.Resources -import com.android.server.wm.traces.common.FlickerComponentName +import android.graphics.Point +import android.os.SystemClock +import android.view.InputDevice +import android.view.MotionEvent +import android.view.ViewConfiguration +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.IComponentMatcher +import com.android.server.wm.traces.common.IComponentNameMatcher import com.android.server.wm.traces.parser.toFlickerComponent +import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper +import com.android.wm.shell.flicker.SPLIT_DECOR_MANAGER +import com.android.wm.shell.flicker.SYSTEM_UI_PACKAGE_NAME import com.android.wm.shell.flicker.testapp.Components class SplitScreenHelper( instrumentation: Instrumentation, activityLabel: String, - componentsInfo: FlickerComponentName -) : BaseAppHelper(instrumentation, activityLabel, componentsInfo) { + componentInfo: IComponentNameMatcher +) : BaseAppHelper(instrumentation, activityLabel, componentInfo) { 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 DIVIDER_BAR = "docked_divider_handle" + const val GESTURE_STEP_MS = 16L + const val LONG_PRESS_TIME_MS = 100L - // 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") + private val dividerBarSelector: BySelector + get() = By.res(SYSTEM_UI_PACKAGE_NAME, DIVIDER_BAR) 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 getIme(instrumentation: Instrumentation): SplitScreenHelper = + SplitScreenHelper( + instrumentation, + Components.ImeActivity.LABEL, + Components.ImeActivity.COMPONENT.toFlickerComponent() + ) + + fun waitForSplitComplete( + wmHelper: WindowManagerStateHelper, + primaryApp: IComponentMatcher, + secondaryApp: IComponentMatcher, + ) { + wmHelper.StateSyncBuilder() + .withWindowSurfaceAppeared(primaryApp) + .withWindowSurfaceAppeared(secondaryApp) + .withSplitDividerVisible() + .waitForAndVerify() + } + + fun enterSplit( + wmHelper: WindowManagerStateHelper, + tapl: LauncherInstrumentation, + primaryApp: SplitScreenHelper, + secondaryApp: SplitScreenHelper + ) { + tapl.workspace.switchToOverview().dismissAllTasks() + primaryApp.launchViaIntent(wmHelper) + secondaryApp.launchViaIntent(wmHelper) + tapl.goHome() + wmHelper.StateSyncBuilder() + .withHomeActivityVisible() + .waitForAndVerify() + splitFromOverview(tapl) + waitForSplitComplete(wmHelper, primaryApp, secondaryApp) + } + + fun splitFromOverview(tapl: LauncherInstrumentation) { + // Note: The initial split position in landscape is different between tablet and phone. + // In landscape, tablet will let the first app split to right side, and phone will + // split to left side. + if (tapl.isTablet) { + tapl.workspace.switchToOverview().overviewActions + .clickSplit() + .currentTask + .open() + } else { + tapl.workspace.switchToOverview().currentTask + .tapMenu() + .tapSplitMenuItem() + .currentTask + .open() + } + SystemClock.sleep(TIMEOUT_MS) + } + + 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 + val dragStart = notificationContent.visibleCenter + val dragMiddle = Point(dragStart.x + 50, dragStart.y) + val 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 longPress( + instrumentation: Instrumentation, + point: Point + ) { + val downTime = SystemClock.uptimeMillis() + touch(instrumentation, MotionEvent.ACTION_DOWN, downTime, downTime, TIMEOUT_MS, point) + SystemClock.sleep(LONG_PRESS_TIME_MS) + touch(instrumentation, MotionEvent.ACTION_UP, downTime, downTime, TIMEOUT_MS, point) + } + + fun createShortcutOnHotseatIfNotExist( + tapl: LauncherInstrumentation, + appName: String + ) { + tapl.workspace.deleteAppIcon(tapl.workspace.getHotseatAppIcon(0)) + val allApps = tapl.workspace.switchToAllApps() + allApps.freeze() + try { + allApps.getAppIcon(appName).dragToHotseat(0) + } finally { + allApps.unfreeze() + } + } + + fun dragDividerToResizeAndWait( + device: UiDevice, + wmHelper: WindowManagerStateHelper + ) { + val displayBounds = wmHelper.currentState.layerState + .displays.firstOrNull { !it.isVirtual } + ?.layerStackSpace + ?: error("Display not found") + val dividerBar = device.wait(Until.findObject(dividerBarSelector), TIMEOUT_MS) + dividerBar.drag(Point(displayBounds.width * 1 / 3, displayBounds.height * 2 / 3)) + + wmHelper.StateSyncBuilder() + .withWindowSurfaceDisappeared(SPLIT_DECOR_MANAGER) + .waitForAndVerify() + } + + fun dragDividerToDismissSplit( + device: UiDevice, + wmHelper: WindowManagerStateHelper, + dragToRight: Boolean, + dragToBottom: Boolean + ) { + val displayBounds = wmHelper.currentState.layerState + .displays.firstOrNull { !it.isVirtual } + ?.layerStackSpace + ?: error("Display not found") + val dividerBar = device.wait(Until.findObject(dividerBarSelector), TIMEOUT_MS) + dividerBar.drag(Point( + if (dragToRight) { + displayBounds.width * 4 / 5 + } else { + displayBounds.width * 1 / 5 + }, + if (dragToBottom) { + displayBounds.height * 4 / 5 + } else { + displayBounds.height * 1 / 5 + })) + } + + fun doubleTapDividerToSwitch(device: UiDevice) { + val dividerBar = device.wait(Until.findObject(dividerBarSelector), TIMEOUT_MS) + val interval = (ViewConfiguration.getDoubleTapTimeout() + + ViewConfiguration.getDoubleTapMinTime()) / 2 + dividerBar.click() + SystemClock.sleep(interval.toLong()) + dividerBar.click() + } + + fun copyContentInSplit( + instrumentation: Instrumentation, + device: UiDevice, + sourceApp: IComponentNameMatcher, + destinationApp: IComponentNameMatcher, + ) { + // Copy text from sourceApp + val textView = device.wait(Until.findObject( + By.res(sourceApp.packageName, "SplitScreenTest")), TIMEOUT_MS) + longPress(instrumentation, textView.getVisibleCenter()) + + val copyBtn = device.wait(Until.findObject(By.text("Copy")), TIMEOUT_MS) + copyBtn.click() + + // Paste text to destinationApp + val editText = device.wait(Until.findObject( + By.res(destinationApp.packageName, "plain_text_input")), TIMEOUT_MS) + longPress(instrumentation, editText.getVisibleCenter()) + + val pasteBtn = device.wait(Until.findObject(By.text("Paste")), TIMEOUT_MS) + pasteBtn.click() + + // Verify text + if (!textView.getText().contentEquals(editText.getText())) { + error("Fail to copy content in split") + } + } } } 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..9684bb32f48a --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/AutoEnterPipOnGoToHomeTest.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.pip + +import android.platform.test.annotations.FlakyTest +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.annotation.Group3 +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.rules.RemoveAllTasksButHomeRule +import com.android.server.wm.flicker.rules.RemoveAllTasksButHomeRule.Companion.removeAllTasksButHome +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) { + /** + * Defines the transition used to run the test + */ + override val transition: FlickerBuilder.() -> Unit + get() = { + setup { + removeAllTasksButHome() + device.wakeUpAndGoToHomeScreen() + pipApp.launchViaIntent(wmHelper) + pipApp.enableAutoEnterForPipActivity() + } + teardown { + // close gracefully so that onActivityUnpinned() can be called before force exit + pipApp.closePipWindow(wmHelper) + + setRotation(Surface.ROTATION_0) + RemoveAllTasksButHomeRule.removeAllTasksButHome() + pipApp.exit(wmHelper) + } + transitions { + tapl.goHome() + } + } + + @FlakyTest + @Test + override fun pipLayerReduces() { + testSpec.assertLayers { + val pipLayerList = this.layers { pipApp.layerMatchesAnyOf(it) && 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 + */ + @Presubmit + @Test + fun pipLayerMovesTowardsRightBottomCorner() { + // in gestural nav the swipe makes PiP first go upwards + Assume.assumeFalse(testSpec.isGesturalNavigation) + testSpec.assertLayers { + val pipLayerList = this.layers { pipApp.layerMatchesAnyOf(it) && 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) + } + } + } + + @Presubmit + @Test + 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..59f7ecf4d100 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipOnUserLeaveHintTest.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.pip + +import android.platform.test.annotations.FlakyTest +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.annotation.Group3 +import com.android.server.wm.flicker.dsl.FlickerBuilder +import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled +import com.android.server.wm.flicker.helpers.setRotation +import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen +import com.android.server.wm.flicker.rules.RemoveAllTasksButHomeRule +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 [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) { + /** + * Defines the transition used to run the test + */ + override val transition: FlickerBuilder.() -> Unit + get() = { + setup { + RemoveAllTasksButHomeRule.removeAllTasksButHome() + device.wakeUpAndGoToHomeScreen() + device.wakeUpAndGoToHomeScreen() + pipApp.launchViaIntent(wmHelper) + pipApp.enableEnterPipOnUserLeaveHint() + } + teardown { + setRotation(Surface.ROTATION_0) + RemoveAllTasksButHomeRule.removeAllTasksButHome() + pipApp.exit(wmHelper) + } + transitions { + tapl.goHome() + } + } + + @Presubmit + @Test + override fun pipAppLayerAlwaysVisible() { + if (!testSpec.isGesturalNavigation) super.pipAppLayerAlwaysVisible() else { + // pip layer in gesture nav will disappear during transition + testSpec.assertLayers { + this.isVisible(pipApp) + .then().isInvisible(pipApp) + .then().isVisible(pipApp) + } + } + } + + @Presubmit + @Test + override fun pipLayerReduces() { + // in gestural nav the pip enters through alpha animation + Assume.assumeFalse(testSpec.isGesturalNavigation) + super.pipLayerReduces() + } + + @Presubmit + @Test + override fun focusChanges() { + // in gestural nav the focus goes to different activity on swipe up + Assume.assumeFalse(testSpec.isGesturalNavigation) + super.focusChanges() + } + + @Presubmit + @Test + override fun entireScreenCovered() { + Assume.assumeFalse(isShellTransitionsEnabled) + super.entireScreenCovered() + } + + @FlakyTest(bugId = 227313015) + @Test + fun entireScreenCovered_ShellTransit() { + Assume.assumeTrue(isShellTransitionsEnabled) + super.entireScreenCovered() + } + + @Presubmit + @Test + override fun pipLayerRemainInsideVisibleBounds() { + if (!testSpec.isGesturalNavigation) super.pipLayerRemainInsideVisibleBounds() else { + // pip layer in gesture nav will disappear during transition + testSpec.assertLayersStart { + this.visibleRegion(pipApp).coversAtMost(displayBounds) + } + testSpec.assertLayersEnd { + this.visibleRegion(pipApp).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..c9e38e4231b6 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 @@ -16,16 +16,19 @@ package com.android.wm.shell.flicker.pip +import android.platform.test.annotations.FlakyTest import android.platform.test.annotations.Presubmit import android.view.Surface -import androidx.test.filters.FlakyTest import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.FlickerParametersRunnerFactory import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.FlickerTestParameterFactory -import com.android.server.wm.flicker.LAUNCHER_COMPONENT import com.android.server.wm.flicker.annotation.Group3 import com.android.server.wm.flicker.dsl.FlickerBuilder +import com.android.server.wm.flicker.helpers.setRotation +import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen +import com.android.server.wm.flicker.rules.RemoveAllTasksButHomeRule +import com.android.server.wm.traces.common.ComponentNameMatcher import org.junit.FixMethodOrder import org.junit.Test import org.junit.runner.RunWith @@ -43,7 +46,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,53 +57,45 @@ 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 - */ + /** {@inheritDoc} */ override val transition: FlickerBuilder.() -> Unit get() = { - setupAndTeardown(this) setup { - eachRun { - pipApp.launchViaIntent(wmHelper) - } + RemoveAllTasksButHomeRule.removeAllTasksButHome() + device.wakeUpAndGoToHomeScreen() + pipApp.launchViaIntent(wmHelper) } teardown { - eachRun { - pipApp.exit(wmHelper) - } + setRotation(Surface.ROTATION_0) + RemoveAllTasksButHomeRule.removeAllTasksButHome() + pipApp.exit(wmHelper) } transitions { pipApp.clickEnterPipButton(wmHelper) } } - /** {@inheritDoc} */ - @FlakyTest(bugId = 206753786) - @Test - override fun statusBarLayerRotatesScales() = super.statusBarLayerRotatesScales() - /** * Checks [pipApp] window remains visible throughout the animation */ @Presubmit @Test - fun pipAppWindowAlwaysVisible() { + open fun pipAppWindowAlwaysVisible() { testSpec.assertWm { - this.isAppWindowVisible(pipApp.component) + this.isAppWindowVisible(pipApp) } } /** * Checks [pipApp] layer remains visible throughout the animation */ - @Presubmit + @FlakyTest(bugId = 239807171) @Test - fun pipAppLayerAlwaysVisible() { + open fun pipAppLayerAlwaysVisible() { testSpec.assertLayers { - this.isVisible(pipApp.component) + this.isVisible(pipApp) } } @@ -111,7 +106,7 @@ class EnterPipTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) { @Presubmit @Test fun pipWindowRemainInsideVisibleBounds() { - testSpec.assertWmVisibleRegion(pipApp.component) { + testSpec.assertWmVisibleRegion(pipApp) { coversAtMost(displayBounds) } } @@ -120,10 +115,10 @@ class EnterPipTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) { * Checks that the pip app layer remains inside the display bounds throughout the whole * animation */ - @Presubmit + @FlakyTest(bugId = 239807171) @Test - fun pipLayerRemainInsideVisibleBounds() { - testSpec.assertLayersVisibleRegion(pipApp.component) { + open fun pipLayerRemainInsideVisibleBounds() { + testSpec.assertLayersVisibleRegion(pipApp) { coversAtMost(displayBounds) } } @@ -133,12 +128,11 @@ class EnterPipTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) { */ @Presubmit @Test - fun pipLayerReduces() { - val layerName = pipApp.component.toLayerName() + open fun pipLayerReduces() { testSpec.assertLayers { - val pipLayerList = this.layers { it.name.contains(layerName) && it.isVisible } + val pipLayerList = this.layers { pipApp.layerMatchesAnyOf(it) && it.isVisible } pipLayerList.zipWithNext { previous, current -> - current.visibleRegion.coversAtMost(previous.visibleRegion.region) + current.visibleRegion.notBiggerThan(previous.visibleRegion.region) } } } @@ -150,22 +144,22 @@ class EnterPipTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) { @Test fun pipWindowBecomesPinned() { testSpec.assertWm { - invoke("pipWindowIsNotPinned") { it.isNotPinned(pipApp.component) } + invoke("pipWindowIsNotPinned") { it.isNotPinned(pipApp) } .then() - .invoke("pipWindowIsPinned") { it.isPinned(pipApp.component) } + .invoke("pipWindowIsPinned") { it.isPinned(pipApp) } } } /** - * Checks [LAUNCHER_COMPONENT] layer remains visible throughout the animation + * Checks [ComponentMatcher.LAUNCHER] layer remains visible throughout the animation */ @Presubmit @Test fun launcherLayerBecomesVisible() { testSpec.assertLayers { - isInvisible(LAUNCHER_COMPONENT) + isInvisible(ComponentNameMatcher.LAUNCHER) .then() - .isVisible(LAUNCHER_COMPONENT) + .isVisible(ComponentNameMatcher.LAUNCHER) } } @@ -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,9 @@ 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) + ) } } } 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..87d800c3dc6a 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 @@ -16,9 +16,11 @@ package com.android.wm.shell.flicker.pip +import android.app.Activity +import android.platform.test.annotations.FlakyTest +import android.platform.test.annotations.Postsubmit 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 @@ -27,14 +29,17 @@ import com.android.server.wm.flicker.annotation.Group3 import com.android.server.wm.flicker.dsl.FlickerBuilder import com.android.server.wm.flicker.entireScreenCovered import com.android.server.wm.flicker.helpers.WindowUtils -import com.android.server.wm.flicker.navBarLayerRotatesAndScales -import com.android.server.wm.flicker.statusBarLayerRotatesScales -import com.android.server.wm.traces.common.FlickerComponentName +import com.android.server.wm.flicker.helpers.setRotation +import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen +import com.android.server.wm.flicker.navBarLayerPositionAtStartAndEnd +import com.android.server.wm.flicker.rules.RemoveAllTasksButHomeRule 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.Assume +import org.junit.Before import org.junit.FixMethodOrder import org.junit.Test import org.junit.runner.RunWith @@ -44,7 +49,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 @@ -76,50 +81,58 @@ class EnterPipToOtherOrientationTest( */ override val transition: FlickerBuilder.() -> Unit get() = { - setupAndTeardown(this) - setup { - eachRun { - // Launch a portrait only app on the fullscreen stack - testApp.launchViaIntent(wmHelper, stringExtras = mapOf( - EXTRA_FIXED_ORIENTATION to ORIENTATION_PORTRAIT.toString())) - // Launch the PiP activity fixed as landscape - pipApp.launchViaIntent(wmHelper, stringExtras = mapOf( - EXTRA_FIXED_ORIENTATION to ORIENTATION_LANDSCAPE.toString())) - } + RemoveAllTasksButHomeRule.removeAllTasksButHome() + device.wakeUpAndGoToHomeScreen() + + // Launch a portrait only app on the fullscreen stack + testApp.launchViaIntent( + wmHelper, stringExtras = mapOf( + EXTRA_FIXED_ORIENTATION to ORIENTATION_PORTRAIT.toString() + ) + ) + // Launch the PiP activity fixed as landscape + pipApp.launchViaIntent( + wmHelper, stringExtras = mapOf( + EXTRA_FIXED_ORIENTATION to ORIENTATION_LANDSCAPE.toString() + ) + ) } teardown { - eachRun { - pipApp.exit(wmHelper) - testApp.exit(wmHelper) - } + setRotation(Surface.ROTATION_0) + RemoveAllTasksButHomeRule.removeAllTasksButHome() + pipApp.exit(wmHelper) + testApp.exit(wmHelper) } transitions { // Enter PiP, and assert that the PiP is within bounds now that the device is back // in portrait broadcastActionTrigger.doAction(ACTION_ENTER_PIP) - wmHelper.waitPipShown() - wmHelper.waitForAppTransitionIdle() // during rotation the status bar becomes invisible and reappears at the end - wmHelper.waitForNavBarStatusBarVisible() + wmHelper.StateSyncBuilder() + .withPipShown() + .withNavOrTaskBarVisible() + .withStatusBarVisible() + .waitForAndVerify() } } /** - * Checks that the [FlickerComponentName.NAV_BAR] has the correct position at - * the start and end of the transition + * This test is not compatible with Tablets. When using [Activity.setRequestedOrientation] + * to fix a orientation, Tablets instead keep the same orientation and add letterboxes */ - @FlakyTest - @Test - override fun navBarLayerRotatesAndScales() = testSpec.navBarLayerRotatesAndScales() + @Before + fun setup() { + Assume.assumeFalse(testSpec.isTablet) + } /** - * Checks that the [FlickerComponentName.STATUS_BAR] has the correct position at + * Checks that the [ComponentMatcher.NAV_BAR] has the correct position at * the start and end of the transition */ - @FlakyTest(bugId = 206753786) + @FlakyTest @Test - override fun statusBarLayerRotatesScales() = testSpec.statusBarLayerRotatesScales() + override fun navBarLayerPositionAtStartAndEnd() = testSpec.navBarLayerPositionAtStartAndEnd() /** * Checks that all parts of the screen are covered at the start and end of the transition @@ -128,7 +141,7 @@ class EnterPipToOtherOrientationTest( */ @Presubmit @Test - override fun entireScreenCovered() = testSpec.entireScreenCovered(allStates = false) + fun entireScreenCoveredAtStartAndEnd() = testSpec.entireScreenCovered(allStates = false) /** * Checks [pipApp] window remains visible and on top throughout the transition @@ -137,7 +150,7 @@ class EnterPipToOtherOrientationTest( @Test fun pipAppWindowIsAlwaysOnTop() { testSpec.assertWm { - isAppWindowOnTop(pipApp.component) + isAppWindowOnTop(pipApp) } } @@ -148,7 +161,7 @@ class EnterPipToOtherOrientationTest( @Test fun testAppWindowInvisibleOnStart() { testSpec.assertWmStart { - isAppWindowInvisible(testApp.component) + isAppWindowInvisible(testApp) } } @@ -159,7 +172,7 @@ class EnterPipToOtherOrientationTest( @Test fun testAppWindowVisibleOnEnd() { testSpec.assertWmEnd { - isAppWindowVisible(testApp.component) + isAppWindowVisible(testApp) } } @@ -170,7 +183,7 @@ class EnterPipToOtherOrientationTest( @Test fun testAppLayerInvisibleOnStart() { testSpec.assertLayersStart { - isInvisible(testApp.component) + isInvisible(testApp) } } @@ -181,7 +194,7 @@ class EnterPipToOtherOrientationTest( @Test fun testAppLayerVisibleOnEnd() { testSpec.assertLayersEnd { - isVisible(testApp.component) + isVisible(testApp) } } @@ -193,7 +206,7 @@ class EnterPipToOtherOrientationTest( @Test fun pipAppLayerCoversFullScreenOnStart() { testSpec.assertLayersStart { - visibleRegion(pipApp.component).coversExactly(startingBounds) + visibleRegion(pipApp).coversExactly(startingBounds) } } @@ -205,13 +218,19 @@ class EnterPipToOtherOrientationTest( @Test fun testAppPlusPipLayerCoversFullScreenOnEnd() { testSpec.assertLayersEnd { - val pipRegion = visibleRegion(pipApp.component).region - visibleRegion(testApp.component) + val pipRegion = visibleRegion(pipApp).region + visibleRegion(testApp) .plus(pipRegion) .coversExactly(endingBounds) } } + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun visibleLayersShownMoreThanOneConsecutiveEntry() = + super.visibleLayersShownMoreThanOneConsecutiveEntry() + companion object { /** * Creates the test configurations. @@ -223,8 +242,9 @@ class EnterPipToOtherOrientationTest( @JvmStatic fun getParams(): Collection<FlickerTestParameter> { return FlickerTestParameterFactory.getInstance() - .getConfigNonRotationTests(supportedRotations = listOf(Surface.ROTATION_0), - repetitions = 3) + .getConfigNonRotationTests( + supportedRotations = listOf(Surface.ROTATION_0) + ) } } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipToAppTransition.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipToAppTransition.kt index 990872f58dc1..45851c8b6326 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipToAppTransition.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipToAppTransition.kt @@ -34,7 +34,7 @@ abstract class ExitPipToAppTransition(testSpec: FlickerTestParameter) : PipTrans @Presubmit @Test open fun pipAppWindowRemainInsideVisibleBounds() { - testSpec.assertWmVisibleRegion(pipApp.component) { + testSpec.assertWmVisibleRegion(pipApp) { coversAtMost(displayBounds) } } @@ -46,7 +46,7 @@ abstract class ExitPipToAppTransition(testSpec: FlickerTestParameter) : PipTrans @Presubmit @Test open fun pipAppLayerRemainInsideVisibleBounds() { - testSpec.assertLayersVisibleRegion(pipApp.component) { + testSpec.assertLayersVisibleRegion(pipApp) { coversAtMost(displayBounds) } } @@ -62,11 +62,11 @@ abstract class ExitPipToAppTransition(testSpec: FlickerTestParameter) : PipTrans // when the activity is STOPPING, sometimes it becomes invisible in an entry before // the window, sometimes in the same entry. This occurs because we log 1x per frame // thus we ignore activity here - isAppWindowVisible(testApp.component) - .isAppWindowOnTop(pipApp.component) - .then() - .isAppWindowInvisible(testApp.component) - .isAppWindowVisible(pipApp.component) + isAppWindowVisible(testApp) + .isAppWindowOnTop(pipApp) + .then() + .isAppWindowInvisible(testApp) + .isAppWindowVisible(pipApp) } } @@ -78,11 +78,11 @@ abstract class ExitPipToAppTransition(testSpec: FlickerTestParameter) : PipTrans @Test open fun showBothAppLayersThenHidePip() { testSpec.assertLayers { - isVisible(testApp.component) - .isVisible(pipApp.component) - .then() - .isInvisible(testApp.component) - .isVisible(pipApp.component) + isVisible(testApp) + .isVisible(pipApp) + .then() + .isInvisible(testApp) + .isVisible(pipApp) } } @@ -94,10 +94,10 @@ abstract class ExitPipToAppTransition(testSpec: FlickerTestParameter) : PipTrans @Test open fun testPlusPipAppsCoverFullScreenAtStart() { testSpec.assertLayersStart { - val pipRegion = visibleRegion(pipApp.component).region - visibleRegion(testApp.component) - .plus(pipRegion) - .coversExactly(displayBounds) + val pipRegion = visibleRegion(pipApp).region + visibleRegion(testApp) + .plus(pipRegion) + .coversExactly(displayBounds) } } @@ -109,7 +109,7 @@ abstract class ExitPipToAppTransition(testSpec: FlickerTestParameter) : PipTrans @Test open fun pipAppCoversFullScreenAtEnd() { testSpec.assertLayersEnd { - visibleRegion(pipApp.component).coversExactly(displayBounds) + visibleRegion(pipApp).coversExactly(displayBounds) } } @@ -119,12 +119,16 @@ abstract class ExitPipToAppTransition(testSpec: FlickerTestParameter) : PipTrans @Presubmit @Test open fun pipLayerExpands() { - val layerName = pipApp.component.toLayerName() testSpec.assertLayers { - val pipLayerList = this.layers { it.name.contains(layerName) && it.isVisible } + val pipLayerList = this.layers { pipApp.layerMatchesAnyOf(it) && it.isVisible } pipLayerList.zipWithNext { previous, current -> current.visibleRegion.coversAtLeast(previous.visibleRegion.region) } } } + + /** {@inheritDoc} */ + @Presubmit + @Test + override fun entireScreenCovered() = super.entireScreenCovered() } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipTransition.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipTransition.kt index 0b4bc761838d..39be89d2592b 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipTransition.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipTransition.kt @@ -19,10 +19,10 @@ package com.android.wm.shell.flicker.pip import android.platform.test.annotations.Presubmit import android.view.Surface import com.android.server.wm.flicker.FlickerTestParameter -import com.android.server.wm.flicker.LAUNCHER_COMPONENT import com.android.server.wm.flicker.dsl.FlickerBuilder import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled import com.android.server.wm.flicker.helpers.setRotation +import com.android.server.wm.traces.common.ComponentNameMatcher.Companion.LAUNCHER import org.junit.Test /** @@ -30,16 +30,12 @@ import org.junit.Test */ abstract class ExitPipTransition(testSpec: FlickerTestParameter) : PipTransition(testSpec) { override val transition: FlickerBuilder.() -> Unit - get() = buildTransition(eachRun = true) { + get() = buildTransition { setup { - eachRun { - this.setRotation(testSpec.startRotation) - } + this.setRotation(testSpec.startRotation) } teardown { - eachRun { - this.setRotation(Surface.ROTATION_0) - } + this.setRotation(Surface.ROTATION_0) } } @@ -57,28 +53,28 @@ abstract class ExitPipTransition(testSpec: FlickerTestParameter) : PipTransition // and isAppWindowInvisible in the same assertion block. testSpec.assertWm { this.invoke("hasPipWindow") { - it.isPinned(pipApp.component) - .isAppWindowVisible(pipApp.component) - .isAppWindowOnTop(pipApp.component) + it.isPinned(pipApp) + .isAppWindowVisible(pipApp) + .isAppWindowOnTop(pipApp) }.then().invoke("!hasPipWindow") { - it.isNotPinned(pipApp.component) - .isAppWindowNotOnTop(pipApp.component) + it.isNotPinned(pipApp) + .isAppWindowNotOnTop(pipApp) } } - testSpec.assertWmEnd { isAppWindowInvisible(pipApp.component) } + testSpec.assertWmEnd { isAppWindowInvisible(pipApp) } } else { testSpec.assertWm { this.invoke("hasPipWindow") { - it.isPinned(pipApp.component).isAppWindowVisible(pipApp.component) + it.isPinned(pipApp).isAppWindowVisible(pipApp) }.then().invoke("!hasPipWindow") { - it.isNotPinned(pipApp.component).isAppWindowInvisible(pipApp.component) + it.isNotPinned(pipApp).isAppWindowInvisible(pipApp) } } } } /** - * Checks that [pipApp] and [LAUNCHER_COMPONENT] layers are visible at the start + * Checks that [pipApp] and [LAUNCHER] layers are visible at the start * of the transition. Then [pipApp] layer becomes invisible, and remains invisible * until the end of the transition */ @@ -86,11 +82,11 @@ abstract class ExitPipTransition(testSpec: FlickerTestParameter) : PipTransition @Test open fun pipLayerBecomesInvisible() { testSpec.assertLayers { - this.isVisible(pipApp.component) - .isVisible(LAUNCHER_COMPONENT) + this.isVisible(pipApp) + .isVisible(LAUNCHER) .then() - .isInvisible(pipApp.component) - .isVisible(LAUNCHER_COMPONENT) + .isInvisible(pipApp) + .isVisible(LAUNCHER) } } } 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..c2fd0d78f0d5 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 @@ -16,8 +16,8 @@ package com.android.wm.shell.flicker.pip +import android.platform.test.annotations.FlakyTest 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 @@ -54,7 +54,6 @@ import org.junit.runners.Parameterized @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) @Group3 -@FlakyTest(bugId = 219750830) class ExitPipViaExpandButtonClickTest( testSpec: FlickerTestParameter ) : ExitPipToAppTransition(testSpec) { @@ -63,25 +62,25 @@ class ExitPipViaExpandButtonClickTest( * Defines the transition used to run the test */ override val transition: FlickerBuilder.() -> Unit - get() = buildTransition(eachRun = true) { + get() = buildTransition { setup { - eachRun { - // launch an app behind the pip one - testApp.launchViaIntent(wmHelper) - } + // launch an app behind the pip one + testApp.launchViaIntent(wmHelper) } transitions { // This will bring PipApp to fullscreen pipApp.expandPipWindowToApp(wmHelper) // Wait until the other app is no longer visible - wmHelper.waitForWindowSurfaceDisappeared(testApp.component) + wmHelper.StateSyncBuilder() + .withWindowSurfaceDisappeared(testApp) + .waitForAndVerify() } } /** {@inheritDoc} */ - @FlakyTest(bugId = 206753786) + @FlakyTest(bugId = 227313015) @Test - override fun statusBarLayerRotatesScales() = super.statusBarLayerRotatesScales() + override fun entireScreenCovered() = super.entireScreenCovered() /** {@inheritDoc} */ @FlakyTest(bugId = 197726610) @@ -99,7 +98,7 @@ class ExitPipViaExpandButtonClickTest( @JvmStatic fun getParams(): List<FlickerTestParameter> { return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests( - supportedRotations = listOf(Surface.ROTATION_0), repetitions = 3) + supportedRotations = listOf(Surface.ROTATION_0)) } } } 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..0d75e02bd467 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 @@ -16,9 +16,9 @@ package com.android.wm.shell.flicker.pip +import android.platform.test.annotations.FlakyTest 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 @@ -62,25 +62,40 @@ class ExitPipViaIntentTest(testSpec: FlickerTestParameter) : ExitPipToAppTransit * Defines the transition used to run the test */ override val transition: FlickerBuilder.() -> Unit - get() = buildTransition(eachRun = true) { + get() = buildTransition { setup { - eachRun { - // launch an app behind the pip one - testApp.launchViaIntent(wmHelper) - } + // launch an app behind the pip one + testApp.launchViaIntent(wmHelper) } transitions { // This will bring PipApp to fullscreen pipApp.exitPipToFullScreenViaIntent(wmHelper) // Wait until the other app is no longer visible - wmHelper.waitForWindowSurfaceDisappeared(testApp.component) + wmHelper.StateSyncBuilder() + .withWindowSurfaceDisappeared(testApp) + .waitForAndVerify() } } /** {@inheritDoc} */ - @FlakyTest(bugId = 206753786) + @FlakyTest @Test - override fun statusBarLayerRotatesScales() = super.statusBarLayerRotatesScales() + override fun entireScreenCovered() = super.entireScreenCovered() + + /** {@inheritDoc} */ + @Presubmit + @Test + override fun statusBarLayerPositionAtStartAndEnd() { + Assume.assumeFalse(isShellTransitionsEnabled) + super.statusBarLayerPositionAtStartAndEnd() + } + + @Presubmit + @Test + fun statusBarLayerRotatesScales_ShellTransit() { + Assume.assumeTrue(isShellTransitionsEnabled) + super.statusBarLayerPositionAtStartAndEnd() + } /** {@inheritDoc} */ @FlakyTest(bugId = 197726610) @@ -108,7 +123,7 @@ class ExitPipViaIntentTest(testSpec: FlickerTestParameter) : ExitPipToAppTransit @JvmStatic fun getParams(): List<FlickerTestParameter> { return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests( - supportedRotations = listOf(Surface.ROTATION_0), repetitions = 3) + supportedRotations = listOf(Surface.ROTATION_0)) } } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipWithDismissButtonTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipWithDismissButtonTest.kt index 437ad893f1d9..3bffef0ca793 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipWithDismissButtonTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipWithDismissButtonTest.kt @@ -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 @@ -64,18 +63,13 @@ class ExitPipWithDismissButtonTest(testSpec: FlickerTestParameter) : ExitPipTran } } - /** {@inheritDoc} */ - @FlakyTest(bugId = 206753786) - @Test - override fun statusBarLayerRotatesScales() = super.statusBarLayerRotatesScales() - /** * Checks that the focus changes between the pip menu window and the launcher when clicking the * dismiss button on pip menu to close the pip window. */ @Presubmit @Test - fun focusDoesNotChange() { + fun focusChanges() { testSpec.assertEventLog { this.focusChanges("PipMenuView", "NexusLauncherActivity") } @@ -92,8 +86,7 @@ class ExitPipWithDismissButtonTest(testSpec: FlickerTestParameter) : ExitPipTran @JvmStatic fun getParams(): List<FlickerTestParameter> { return FlickerTestParameterFactory.getInstance() - .getConfigNonRotationTests(supportedRotations = listOf(Surface.ROTATION_0), - repetitions = 3) + .getConfigNonRotationTests(supportedRotations = listOf(Surface.ROTATION_0)) } } } 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..75d25e6ef67e 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 @@ -18,14 +18,13 @@ package com.android.wm.shell.flicker.pip import android.platform.test.annotations.Presubmit import android.view.Surface -import androidx.test.filters.FlakyTest import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.FlickerParametersRunnerFactory import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.FlickerTestParameterFactory import com.android.server.wm.flicker.annotation.Group3 import com.android.server.wm.flicker.dsl.FlickerBuilder -import com.android.server.wm.flicker.statusBarLayerRotatesScales +import com.android.server.wm.traces.common.ComponentNameMatcher import org.junit.FixMethodOrder import org.junit.Test import org.junit.runner.RunWith @@ -59,29 +58,31 @@ class ExitPipWithSwipeDownTest(testSpec: FlickerTestParameter) : ExitPipTransiti get() = { super.transition(this) transitions { - val pipRegion = wmHelper.getWindowRegion(pipApp.component).bounds + val pipRegion = wmHelper.getWindowRegion(pipApp).bounds val pipCenterX = pipRegion.centerX() val pipCenterY = pipRegion.centerY() val displayCenterX = device.displayWidth / 2 - device.swipe(pipCenterX, pipCenterY, displayCenterX, device.displayHeight, 10) - wmHelper.waitPipGone() - wmHelper.waitForWindowSurfaceDisappeared(pipApp.component) - wmHelper.waitForAppTransitionIdle() + val barComponent = if (testSpec.isTablet) { + ComponentNameMatcher.TASK_BAR + } else { + ComponentNameMatcher.NAV_BAR + } + val barLayerHeight = wmHelper.currentState.layerState + .getLayerWithBuffer(barComponent) + ?.visibleRegion + ?.height ?: error("Couldn't find Nav or Task bar layer") + // The dismiss button doesn't appear at the complete bottom of the screen, + val displayY = device.displayHeight - barLayerHeight + device.swipe(pipCenterX, pipCenterY, displayCenterX, displayY, 50) + // Wait until the other app is no longer visible + wmHelper.StateSyncBuilder() + .withPipGone() + .withWindowSurfaceDisappeared(pipApp) + .withAppTransitionIdle() + .waitForAndVerify() } } - @FlakyTest - @Test - override fun pipWindowBecomesInvisible() = super.pipWindowBecomesInvisible() - - @FlakyTest - @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 */ @@ -104,8 +105,7 @@ class ExitPipWithSwipeDownTest(testSpec: FlickerTestParameter) : ExitPipTransiti @JvmStatic fun getParams(): List<FlickerTestParameter> { return FlickerTestParameterFactory.getInstance() - .getConfigNonRotationTests(supportedRotations = listOf(Surface.ROTATION_0), - repetitions = 3) + .getConfigNonRotationTests(supportedRotations = listOf(Surface.ROTATION_0)) } } -}
\ No newline at end of file +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnDoubleClickTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnDoubleClickTest.kt index 28b7fc9bd29e..513ce95042d0 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnDoubleClickTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnDoubleClickTest.kt @@ -16,16 +16,16 @@ package com.android.wm.shell.flicker.pip -import androidx.test.filters.FlakyTest +import android.platform.test.annotations.FlakyTest 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.LAUNCHER_COMPONENT import com.android.server.wm.flicker.annotation.Group3 import com.android.server.wm.flicker.dsl.FlickerBuilder +import com.android.server.wm.traces.common.ComponentNameMatcher import org.junit.FixMethodOrder import org.junit.Test import org.junit.runner.RunWith @@ -56,7 +56,7 @@ import org.junit.runners.Parameterized @Group3 class ExpandPipOnDoubleClickTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) { override val transition: FlickerBuilder.() -> Unit - get() = buildTransition(eachRun = true) { + get() = buildTransition { transitions { pipApp.doubleClickPipWindow(wmHelper) } @@ -69,7 +69,7 @@ class ExpandPipOnDoubleClickTest(testSpec: FlickerTestParameter) : PipTransition @Presubmit @Test fun pipWindowRemainInsideVisibleBounds() { - testSpec.assertWmVisibleRegion(pipApp.component) { + testSpec.assertWmVisibleRegion(pipApp) { coversAtMost(displayBounds) } } @@ -81,7 +81,7 @@ class ExpandPipOnDoubleClickTest(testSpec: FlickerTestParameter) : PipTransition @Presubmit @Test fun pipLayerRemainInsideVisibleBounds() { - testSpec.assertLayersVisibleRegion(pipApp.component) { + testSpec.assertLayersVisibleRegion(pipApp) { coversAtMost(displayBounds) } } @@ -93,7 +93,7 @@ class ExpandPipOnDoubleClickTest(testSpec: FlickerTestParameter) : PipTransition @Test fun pipWindowIsAlwaysVisible() { testSpec.assertWm { - isAppWindowVisible(pipApp.component) + isAppWindowVisible(pipApp) } } @@ -104,19 +104,18 @@ class ExpandPipOnDoubleClickTest(testSpec: FlickerTestParameter) : PipTransition @Test fun pipLayerIsAlwaysVisible() { testSpec.assertLayers { - isVisible(pipApp.component) + isVisible(pipApp) } } /** * Checks that the visible region of [pipApp] always expands during the animation */ - @FlakyTest(bugId = 228012337) + @Presubmit @Test fun pipLayerExpands() { - val layerName = pipApp.component.toLayerName() testSpec.assertLayers { - val pipLayerList = this.layers { it.name.contains(layerName) && it.isVisible } + val pipLayerList = this.layers { pipApp.layerMatchesAnyOf(it) && it.isVisible } pipLayerList.zipWithNext { previous, current -> current.visibleRegion.coversAtLeast(previous.visibleRegion.region) } @@ -126,9 +125,8 @@ class ExpandPipOnDoubleClickTest(testSpec: FlickerTestParameter) : PipTransition @Presubmit @Test fun pipSameAspectRatio() { - val layerName = pipApp.component.toLayerName() testSpec.assertLayers { - val pipLayerList = this.layers { it.name.contains(layerName) && it.isVisible } + val pipLayerList = this.layers { pipApp.layerMatchesAnyOf(it) && it.isVisible } pipLayerList.zipWithNext { previous, current -> current.visibleRegion.isSameAspectRatio(previous.visibleRegion) } @@ -142,18 +140,18 @@ class ExpandPipOnDoubleClickTest(testSpec: FlickerTestParameter) : PipTransition @Test fun windowIsAlwaysPinned() { testSpec.assertWm { - this.invoke("hasPipWindow") { it.isPinned(pipApp.component) } + this.invoke("hasPipWindow") { it.isPinned(pipApp) } } } /** - * Checks [pipApp] layer remains visible throughout the animation + * Checks [ComponentMatcher.LAUNCHER] layer remains visible throughout the animation */ @Presubmit @Test fun launcherIsAlwaysVisible() { testSpec.assertLayers { - isVisible(LAUNCHER_COMPONENT) + isVisible(ComponentNameMatcher.LAUNCHER) } } @@ -168,10 +166,6 @@ class ExpandPipOnDoubleClickTest(testSpec: FlickerTestParameter) : PipTransition } } - @FlakyTest(bugId = 206753786) - @Test - override fun statusBarLayerRotatesScales() = super.statusBarLayerRotatesScales() - companion object { /** * Creates the test configurations. @@ -183,8 +177,9 @@ class ExpandPipOnDoubleClickTest(testSpec: FlickerTestParameter) : PipTransition @JvmStatic fun getParams(): List<FlickerTestParameter> { return FlickerTestParameterFactory.getInstance() - .getConfigNonRotationTests(supportedRotations = listOf(Surface.ROTATION_0), - repetitions = 3) + .getConfigNonRotationTests( + supportedRotations = listOf(Surface.ROTATION_0) + ) } } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipDownShelfHeightChangeTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipDownShelfHeightChangeTest.kt index 8729bb6776f0..746ce913c587 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipDownShelfHeightChangeTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipDownShelfHeightChangeTest.kt @@ -16,15 +16,15 @@ package com.android.wm.shell.flicker.pip +import android.platform.test.annotations.Presubmit +import android.platform.test.annotations.RequiresDevice import android.view.Surface -import androidx.test.filters.FlakyTest -import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.FlickerParametersRunnerFactory import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.FlickerTestParameterFactory import com.android.server.wm.flicker.annotation.Group3 import com.android.server.wm.flicker.dsl.FlickerBuilder -import com.android.server.wm.flicker.traces.region.RegionSubject +import com.android.wm.shell.flicker.Direction import org.junit.FixMethodOrder import org.junit.Test import org.junit.runner.RunWith @@ -32,14 +32,14 @@ import org.junit.runners.MethodSorters import org.junit.runners.Parameterized /** - * Test Pip movement with Launcher shelf height change (decrease). + * Test Pip movement with Launcher shelf height change (increase). * - * To run this test: `atest WMShellFlickerTests:MovePipDownShelfHeightChangeTest` + * To run this test: `atest WMShellFlickerTests:MovePipUpShelfHeightChangeTest` * * Actions: * Launch [pipApp] in pip mode - * Launch [testApp] * Press home + * Launch [testApp] * Check if pip window moves down (visually) * * Notes: @@ -55,35 +55,41 @@ import org.junit.runners.Parameterized @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) @Group3 -open class MovePipDownShelfHeightChangeTest( +class MovePipDownShelfHeightChangeTest( testSpec: FlickerTestParameter ) : MovePipShelfHeightTransition(testSpec) { +// @Before +// fun before() { +// Assume.assumeFalse(isShellTransitionsEnabled) +// } + /** * Defines the transition used to run the test */ override val transition: FlickerBuilder.() -> Unit - get() = buildTransition(eachRun = false) { + get() = buildTransition { teardown { - eachRun { - testApp.launchViaIntent(wmHelper) - } - test { - testApp.exit(wmHelper) - } + tapl.pressHome() + testApp.exit(wmHelper) } transitions { - taplInstrumentation.pressHome() + testApp.launchViaIntent(wmHelper) } } - override fun assertRegionMovement(previous: RegionSubject, current: RegionSubject) { - current.isHigherOrEqual(previous.region) - } + /** + * Checks that the visible region of [pipApp] window always moves down during the animation. + */ + @Presubmit + @Test + fun pipWindowMovesDown() = pipWindowMoves(Direction.DOWN) - /** {@inheritDoc} */ - @FlakyTest(bugId = 206753786) + /** + * Checks that the visible region of [pipApp] layer always moves down during the animation. + */ + @Presubmit @Test - override fun statusBarLayerRotatesScales() = super.statusBarLayerRotatesScales() + fun pipLayerMovesDown() = pipLayerMoves(Direction.DOWN) companion object { /** @@ -96,7 +102,8 @@ open class MovePipDownShelfHeightChangeTest( @JvmStatic fun getParams(): List<FlickerTestParameter> { return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests( - supportedRotations = listOf(Surface.ROTATION_0), repetitions = 3) + supportedRotations = listOf(Surface.ROTATION_0) + ) } } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipShelfHeightTransition.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipShelfHeightTransition.kt index 0499e7de9a0a..19bdca51f8af 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipShelfHeightTransition.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipShelfHeightTransition.kt @@ -17,9 +17,9 @@ package com.android.wm.shell.flicker.pip import android.platform.test.annotations.Presubmit -import com.android.launcher3.tapl.LauncherInstrumentation import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.traces.region.RegionSubject +import com.android.wm.shell.flicker.Direction import com.android.wm.shell.flicker.helpers.FixedAppHelper import org.junit.Test @@ -29,22 +29,16 @@ import org.junit.Test abstract class MovePipShelfHeightTransition( testSpec: FlickerTestParameter ) : PipTransition(testSpec) { - protected val taplInstrumentation = LauncherInstrumentation() protected val testApp = FixedAppHelper(instrumentation) /** - * Checks if the window movement direction is valid - */ - protected abstract fun assertRegionMovement(previous: RegionSubject, current: RegionSubject) - - /** * Checks [pipApp] window remains visible throughout the animation */ @Presubmit @Test open fun pipWindowIsAlwaysVisible() { testSpec.assertWm { - isAppWindowVisible(pipApp.component) + isAppWindowVisible(pipApp) } } @@ -55,7 +49,7 @@ abstract class MovePipShelfHeightTransition( @Test open fun pipLayerIsAlwaysVisible() { testSpec.assertLayers { - isVisible(pipApp.component) + isVisible(pipApp) } } @@ -66,7 +60,7 @@ abstract class MovePipShelfHeightTransition( @Presubmit @Test open fun pipWindowRemainInsideVisibleBounds() { - testSpec.assertWmVisibleRegion(pipApp.component) { + testSpec.assertWmVisibleRegion(pipApp) { coversAtMost(displayBounds) } } @@ -78,39 +72,52 @@ abstract class MovePipShelfHeightTransition( @Presubmit @Test open fun pipLayerRemainInsideVisibleBounds() { - testSpec.assertLayersVisibleRegion(pipApp.component) { + testSpec.assertLayersVisibleRegion(pipApp) { coversAtMost(displayBounds) } } /** - * Checks that the visible region of [pipApp] always moves in the correct direction + * Checks that the visible region of [pipApp] window always moves in the specified direction * during the animation. */ - @Presubmit - @Test - open fun pipWindowMoves() { - val windowName = pipApp.component.toWindowName() + protected fun pipWindowMoves(direction: Direction) { testSpec.assertWm { - val pipWindowList = this.windowStates { it.name.contains(windowName) && it.isVisible } - pipWindowList.zipWithNext { previous, current -> - assertRegionMovement(previous.frame, current.frame) + val pipWindowFrameList = this.windowStates { + pipApp.windowMatchesAnyOf(it) && it.isVisible + }.map { it.frame } + when (direction) { + Direction.UP -> assertRegionMovementUp(pipWindowFrameList) + Direction.DOWN -> assertRegionMovementDown(pipWindowFrameList) + else -> error("Unhandled direction") } } } /** - * Checks that the visible region of [pipApp] always moves up during the animation + * Checks that the visible region of [pipApp] layer always moves in the specified direction + * during the animation. */ - @Presubmit - @Test - open fun pipLayerMoves() { - val layerName = pipApp.component.toLayerName() + protected fun pipLayerMoves(direction: Direction) { testSpec.assertLayers { - val pipLayerList = this.layers { it.name.contains(layerName) && it.isVisible } - pipLayerList.zipWithNext { previous, current -> - assertRegionMovement(previous.visibleRegion, current.visibleRegion) + val pipLayerRegionList = this.layers { + pipApp.layerMatchesAnyOf(it) && it.isVisible + }.map { it.visibleRegion } + when (direction) { + Direction.UP -> assertRegionMovementUp(pipLayerRegionList) + Direction.DOWN -> assertRegionMovementDown(pipLayerRegionList) + else -> error("Unhandled direction") } } } -}
\ No newline at end of file + + private fun assertRegionMovementDown(regions: List<RegionSubject>) { + regions.zipWithNext { previous, current -> current.isLowerOrEqual(previous) } + regions.last().isLower(regions.first()) + } + + private fun assertRegionMovementUp(regions: List<RegionSubject>) { + regions.zipWithNext { previous, current -> current.isHigherOrEqual(previous.region) } + regions.last().isHigher(regions.first()) + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipUpShelfHeightChangeTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipUpShelfHeightChangeTest.kt index 388b5e0b5e47..93e7d5ca45ad 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipUpShelfHeightChangeTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipUpShelfHeightChangeTest.kt @@ -16,18 +16,15 @@ package com.android.wm.shell.flicker.pip -import androidx.test.filters.FlakyTest -import android.platform.test.annotations.RequiresDevice +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.Group3 import com.android.server.wm.flicker.dsl.FlickerBuilder -import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled -import com.android.server.wm.flicker.traces.region.RegionSubject -import org.junit.Assume -import org.junit.Before +import com.android.wm.shell.flicker.Direction import org.junit.FixMethodOrder import org.junit.Test import org.junit.runner.RunWith @@ -35,14 +32,14 @@ import org.junit.runners.MethodSorters import org.junit.runners.Parameterized /** - * Test Pip movement with Launcher shelf height change (increase). + * Test Pip movement with Launcher shelf height change (decrease). * - * To run this test: `atest WMShellFlickerTests:MovePipUpShelfHeightChangeTest` + * To run this test: `atest WMShellFlickerTests:MovePipDownShelfHeightChangeTest` * * Actions: * Launch [pipApp] in pip mode - * Press home * Launch [testApp] + * Press home * Check if pip window moves up (visually) * * Notes: @@ -58,40 +55,38 @@ import org.junit.runners.Parameterized @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) @Group3 -class MovePipUpShelfHeightChangeTest( +open class MovePipUpShelfHeightChangeTest( testSpec: FlickerTestParameter ) : MovePipShelfHeightTransition(testSpec) { - @Before - fun before() { - Assume.assumeFalse(isShellTransitionsEnabled) - } - /** * Defines the transition used to run the test */ override val transition: FlickerBuilder.() -> Unit - get() = buildTransition(eachRun = false) { - teardown { - eachRun { - taplInstrumentation.pressHome() - } - test { - testApp.exit(wmHelper) - } + get() = buildTransition() { + setup { + testApp.launchViaIntent(wmHelper) } transitions { - testApp.launchViaIntent(wmHelper) + tapl.pressHome() + } + teardown { + testApp.exit(wmHelper) } } - override fun assertRegionMovement(previous: RegionSubject, current: RegionSubject) { - current.isLowerOrEqual(previous.region) - } + /** + * Checks that the visible region of [pipApp] window always moves up during the animation. + */ + @Presubmit + @Test + fun pipWindowMovesUp() = pipWindowMoves(Direction.UP) - /** {@inheritDoc} */ - @FlakyTest(bugId = 206753786) + /** + * Checks that the visible region of [pipApp] layer always moves up during the animation. + */ + @Presubmit @Test - override fun statusBarLayerRotatesScales() = super.statusBarLayerRotatesScales() + fun pipLayerMovesUp() = pipLayerMoves(Direction.UP) companion object { /** @@ -104,7 +99,8 @@ class MovePipUpShelfHeightChangeTest( @JvmStatic fun getParams(): List<FlickerTestParameter> { return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests( - supportedRotations = listOf(Surface.ROTATION_0), repetitions = 3) + supportedRotations = listOf(Surface.ROTATION_0) + ) } } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipKeyboardTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipKeyboardTest.kt index 1e30f6b83874..3d3b53de3dc8 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipKeyboardTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipKeyboardTest.kt @@ -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 @@ -28,7 +27,7 @@ import com.android.server.wm.flicker.dsl.FlickerBuilder import com.android.server.wm.flicker.helpers.WindowUtils import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled import com.android.server.wm.flicker.helpers.setRotation -import com.android.server.wm.traces.common.FlickerComponentName +import com.android.server.wm.traces.common.ComponentNameMatcher import com.android.wm.shell.flicker.helpers.ImeAppHelper import org.junit.Assume.assumeFalse import org.junit.Before @@ -47,7 +46,6 @@ import org.junit.runners.Parameterized @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) @Group4 -@FlakyTest(bugId = 218604389) open class PipKeyboardTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) { private val imeApp = ImeAppHelper(instrumentation) @@ -56,19 +54,16 @@ open class PipKeyboardTest(testSpec: FlickerTestParameter) : PipTransition(testS assumeFalse(isShellTransitionsEnabled) } + /** {@inheritDoc} */ override val transition: FlickerBuilder.() -> Unit - get() = buildTransition(eachRun = false) { + get() = buildTransition { setup { - test { - imeApp.launchViaIntent(wmHelper) - setRotation(testSpec.startRotation) - } + imeApp.launchViaIntent(wmHelper) + setRotation(testSpec.startRotation) } teardown { - test { - imeApp.exit(wmHelper) - setRotation(Surface.ROTATION_0) - } + imeApp.exit(wmHelper) + setRotation(Surface.ROTATION_0) } transitions { // open the soft keyboard @@ -80,18 +75,13 @@ open class PipKeyboardTest(testSpec: FlickerTestParameter) : PipTransition(testS } } - /** {@inheritDoc} */ - @FlakyTest(bugId = 206753786) - @Test - override fun statusBarLayerRotatesScales() = super.statusBarLayerRotatesScales() - /** * Ensure the pip window remains visible throughout any keyboard interactions */ @Presubmit @Test open fun pipInVisibleBounds() { - testSpec.assertWmVisibleRegion(pipApp.component) { + testSpec.assertWmVisibleRegion(pipApp) { val displayBounds = WindowUtils.getDisplayBounds(testSpec.startRotation) coversAtMost(displayBounds) } @@ -104,7 +94,7 @@ open class PipKeyboardTest(testSpec: FlickerTestParameter) : PipTransition(testS @Test open fun pipIsAboveAppWindow() { testSpec.assertWmTag(TAG_IME_VISIBLE) { - isAboveWindow(FlickerComponentName.IME, pipApp.component) + isAboveWindow(ComponentNameMatcher.IME, pipApp) } } @@ -115,8 +105,7 @@ open class PipKeyboardTest(testSpec: FlickerTestParameter) : PipTransition(testS @JvmStatic fun getParams(): Collection<FlickerTestParameter> { return FlickerTestParameterFactory.getInstance() - .getConfigNonRotationTests(supportedRotations = listOf(Surface.ROTATION_0), - repetitions = 3) + .getConfigNonRotationTests(supportedRotations = listOf(Surface.ROTATION_0)) } } } 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..3e00b19e2f19 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,8 @@ class PipKeyboardTestShellTransit(testSpec: FlickerTestParameter) : PipKeyboardT Assume.assumeTrue(isShellTransitionsEnabled) } - @FlakyTest(bugId = 214452854) + @Presubmit @Test - override fun statusBarLayerRotatesScales() = super.statusBarLayerRotatesScales() -}
\ No newline at end of file + override fun statusBarLayerPositionAtStartAndEnd() = + super.statusBarLayerPositionAtStartAndEnd() +} 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..f13698f1d04e 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 @@ -16,23 +16,21 @@ package com.android.wm.shell.flicker.pip +import android.platform.test.annotations.FlakyTest 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.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.Before import org.junit.FixMethodOrder import org.junit.Test import org.junit.runner.RunWith @@ -68,15 +66,16 @@ open class PipRotationTest(testSpec: FlickerTestParameter) : PipTransition(testS private val screenBoundsStart = WindowUtils.getDisplayBounds(testSpec.startRotation) private val screenBoundsEnd = WindowUtils.getDisplayBounds(testSpec.endRotation) + @Before + open fun before() { + Assume.assumeFalse(isShellTransitionsEnabled) + } + override val transition: FlickerBuilder.() -> Unit - get() = buildTransition(eachRun = false) { + get() = buildTransition { setup { - test { - fixedApp.launchViaIntent(wmHelper) - } - eachRun { - setRotation(testSpec.startRotation) - } + fixedApp.launchViaIntent(wmHelper) + setRotation(testSpec.startRotation) } transitions { setRotation(testSpec.endRotation) @@ -84,45 +83,55 @@ open class PipRotationTest(testSpec: FlickerTestParameter) : PipTransition(testS } /** - * Checks that all parts of the screen are covered at the start and end of the transition + * Checks the position of the navigation bar at the start and end of the transition */ - @Presubmit + @FlakyTest(bugId = 240499181) @Test - override fun entireScreenCovered() = testSpec.entireScreenCovered() + override fun navBarLayerPositionAtStartAndEnd() = super.navBarLayerPositionAtStartAndEnd() /** - * Checks the position of the navigation bar at the start and end of the transition + * Checks that [fixedApp] layer is within [screenBoundsStart] at the start of the transition */ - @FlakyTest + @Presubmit @Test - override fun navBarLayerRotatesAndScales() = testSpec.navBarLayerRotatesAndScales() + fun fixedAppLayer_StartingBounds() { + testSpec.assertLayersStart { + visibleRegion(fixedApp).coversAtMost(screenBoundsStart) + } + } /** - * Checks the position of the status bar at the start and end of the transition + * Checks that [fixedApp] layer is within [screenBoundsEnd] at the end of the transition */ - @FlakyTest(bugId = 206753786) + @Presubmit @Test - override fun statusBarLayerRotatesScales() = testSpec.statusBarLayerRotatesScales() + fun fixedAppLayer_EndingBounds() { + testSpec.assertLayersEnd { + visibleRegion(fixedApp).coversAtMost(screenBoundsEnd) + } + } /** - * Checks that [fixedApp] layer is within [screenBoundsStart] at the start of the transition + * Checks that [fixedApp] plus [pipApp] layers are within [screenBoundsEnd] at the start + * of the transition */ @Presubmit @Test - fun appLayerRotates_StartingBounds() { + fun appLayers_StartingBounds() { testSpec.assertLayersStart { - visibleRegion(fixedApp.component).coversExactly(screenBoundsStart) + visibleRegion(fixedApp.or(pipApp)).coversExactly(screenBoundsStart) } } /** - * Checks that [fixedApp] layer is within [screenBoundsEnd] at the end of the transition + * Checks that [fixedApp] plus [pipApp] layers are within [screenBoundsEnd] at the end + * of the transition */ @Presubmit @Test - fun appLayerRotates_EndingBounds() { + fun appLayers_EndingBounds() { testSpec.assertLayersEnd { - visibleRegion(fixedApp.component).coversExactly(screenBoundsEnd) + visibleRegion(fixedApp.or(pipApp)).coversExactly(screenBoundsEnd) } } @@ -131,7 +140,7 @@ open class PipRotationTest(testSpec: FlickerTestParameter) : PipTransition(testS */ private fun pipLayerRotates_StartingBounds_internal() { testSpec.assertLayersStart { - visibleRegion(pipApp.component).coversAtMost(screenBoundsStart) + visibleRegion(pipApp).coversAtMost(screenBoundsStart) } } @@ -141,14 +150,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() } @@ -159,7 +160,7 @@ open class PipRotationTest(testSpec: FlickerTestParameter) : PipTransition(testS @Test fun pipLayerRotates_EndingBounds() { testSpec.assertLayersEnd { - visibleRegion(pipApp.component).coversAtMost(screenBoundsEnd) + visibleRegion(pipApp).coversAtMost(screenBoundsEnd) } } @@ -171,7 +172,7 @@ open class PipRotationTest(testSpec: FlickerTestParameter) : PipTransition(testS @Test fun pipIsAboveFixedAppWindow_Start() { testSpec.assertWmStart { - isAboveWindow(pipApp.component, fixedApp.component) + isAboveWindow(pipApp, fixedApp) } } @@ -183,10 +184,16 @@ open class PipRotationTest(testSpec: FlickerTestParameter) : PipTransition(testS @Test fun pipIsAboveFixedAppWindow_End() { testSpec.assertWmEnd { - isAboveWindow(pipApp.component, fixedApp.component) + isAboveWindow(pipApp, fixedApp) } } + @FlakyTest(bugId = 240499181) + @Test + override fun navBarLayerIsVisibleAtStartAndEnd() { + super.navBarLayerIsVisibleAtStartAndEnd() + } + companion object { /** * Creates the test configurations. @@ -198,8 +205,8 @@ open class PipRotationTest(testSpec: FlickerTestParameter) : PipTransition(testS @JvmStatic fun getParams(): Collection<FlickerTestParameter> { return FlickerTestParameterFactory.getInstance().getConfigRotationTests( - supportedRotations = listOf(Surface.ROTATION_0, Surface.ROTATION_90), - repetitions = 3) + supportedRotations = listOf(Surface.ROTATION_0, Surface.ROTATION_90) + ) } } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipRotationTest_ShellTransit.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipRotationTest_ShellTransit.kt new file mode 100644 index 000000000000..737f16ad6024 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipRotationTest_ShellTransit.kt @@ -0,0 +1,69 @@ +/* + * 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.FlakyTest +import androidx.test.filters.RequiresDevice +import com.android.server.wm.flicker.FlickerParametersRunnerFactory +import com.android.server.wm.flicker.FlickerTestParameter +import com.android.server.wm.flicker.annotation.Group4 +import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled +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 Pip Stack in bounds after rotations. + * + * To run this test: `atest WMShellFlickerTests:PipRotationTest_ShellTransit` + * + * Actions: + * Launch a [pipApp] in pip mode + * Launch another app [fixedApp] (appears below pip) + * Rotate the screen from [testSpec.startRotation] to [testSpec.endRotation] + * (usually, 0->90 and 90->0) + * + * Notes: + * 1. Some default assertions (e.g., nav bar, status bar and screen covered) + * are inherited from [PipTransition] + * 2. Part of the test setup occurs automatically via + * [com.android.server.wm.flicker.TransitionRunnerWithRules], + * including configuring navigation mode, initial orientation and ensuring no + * apps are running before setup + */ +@RequiresDevice +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +@Group4 +@FlakyTest(bugId = 239575053) +class PipRotationTest_ShellTransit(testSpec: FlickerTestParameter) : PipRotationTest(testSpec) { + @Before + override fun before() { + Assume.assumeTrue(isShellTransitionsEnabled) + } + + /** {@inheritDoc} */ + @FlakyTest + @Test + override fun navBarLayerPositionAtStartAndEnd() = + super.navBarLayerPositionAtStartAndEnd() +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipTestBase.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipTestBase.kt deleted file mode 100644 index 7ba085d3cf1a..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipTestBase.kt +++ /dev/null @@ -1,37 +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 com.android.wm.shell.flicker.FlickerTestBase -import com.android.wm.shell.flicker.helpers.PipAppHelper -import org.junit.Before - -abstract class PipTestBase( - rotationName: String, - rotation: Int -) : FlickerTestBase(rotationName, rotation) { - protected val testApp = PipAppHelper(instrumentation) - - @Before - override fun televisionSetUp() { - /** - * The super implementation assumes ([org.junit.Assume]) that not running on TV, thus - * disabling the test on TV. This test, however, *should run on TV*, so we overriding this - * method and simply leaving it blank. - */ - } -} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipTransition.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipTransition.kt index 8d542c8ec9e6..b67cf772358b 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipTransition.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipTransition.kt @@ -18,33 +18,21 @@ package com.android.wm.shell.flicker.pip import android.app.Instrumentation import android.content.Intent -import android.platform.test.annotations.Presubmit import android.view.Surface -import androidx.test.platform.app.InstrumentationRegistry -import com.android.server.wm.flicker.FlickerBuilderProvider import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.dsl.FlickerBuilder -import com.android.server.wm.flicker.entireScreenCovered import com.android.server.wm.flicker.helpers.WindowUtils import com.android.server.wm.flicker.helpers.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.rules.RemoveAllTasksButHomeRule.Companion.removeAllTasksButHome -import com.android.server.wm.flicker.statusBarLayerIsVisible -import com.android.server.wm.flicker.statusBarLayerRotatesScales -import com.android.server.wm.flicker.statusBarWindowIsVisible +import com.android.wm.shell.flicker.BaseTest import com.android.wm.shell.flicker.helpers.PipAppHelper import com.android.wm.shell.flicker.testapp.Components -import org.junit.Test -abstract class PipTransition(protected val testSpec: FlickerTestParameter) { - protected val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation() +abstract class PipTransition(testSpec: FlickerTestParameter) : BaseTest(testSpec) { protected val pipApp = PipAppHelper(instrumentation) protected val displayBounds = WindowUtils.getDisplayBounds(testSpec.startRotation) protected val broadcastActionTrigger = BroadcastActionTrigger(instrumentation) - protected abstract val transition: FlickerBuilder.() -> Unit + // Helper class to process test actions by broadcast. protected class BroadcastActionTrigger(private val instrumentation: Instrumentation) { private fun createIntentWithAction(broadcastAction: String): Intent { @@ -67,35 +55,6 @@ abstract class PipTransition(protected val testSpec: FlickerTestParameter) { } } - @FlickerBuilderProvider - fun buildFlicker(): FlickerBuilder { - return FlickerBuilder(instrumentation).apply { - transition(this) - } - } - - /** - * Gets a configuration that handles basic setup and teardown of pip tests - */ - protected val setupAndTeardown: FlickerBuilder.() -> Unit - get() = { - setup { - test { - removeAllTasksButHome() - device.wakeUpAndGoToHomeScreen() - } - } - teardown { - eachRun { - setRotation(Surface.ROTATION_0) - } - test { - removeAllTasksButHome() - pipApp.exit(wmHelper) - } - } - } - /** * Gets a configuration that handles basic setup and teardown of pip tests and that * launches the Pip app for test @@ -106,69 +65,22 @@ abstract class PipTransition(protected val testSpec: FlickerTestParameter) { */ @JvmOverloads protected open fun buildTransition( - eachRun: Boolean, stringExtras: Map<String, String> = mapOf(Components.PipActivity.EXTRA_ENTER_PIP to "true"), extraSpec: FlickerBuilder.() -> Unit = {} ): FlickerBuilder.() -> Unit { return { - setupAndTeardown(this) - setup { - test { - if (!eachRun) { - pipApp.launchViaIntentAndWaitForPip(wmHelper, stringExtras = stringExtras) - wmHelper.waitPipShown() - } - } - eachRun { - if (eachRun) { - pipApp.launchViaIntentAndWaitForPip(wmHelper, stringExtras = stringExtras) - wmHelper.waitPipShown() - } - } + setRotation(Surface.ROTATION_0) + removeAllTasksButHome() + pipApp.launchViaIntentAndWaitForPip(wmHelper, stringExtras = stringExtras) } teardown { - eachRun { - if (eachRun) { - pipApp.exit(wmHelper) - } - } - test { - if (!eachRun) { - pipApp.exit(wmHelper) - } - } + setRotation(Surface.ROTATION_0) + removeAllTasksButHome() + pipApp.exit(wmHelper) } extraSpec(this) } } - - @Presubmit - @Test - open fun navBarWindowIsVisible() = testSpec.navBarWindowIsVisible() - - @Presubmit - @Test - open fun statusBarWindowIsVisible() = testSpec.statusBarWindowIsVisible() - - @Presubmit - @Test - open fun navBarLayerIsVisible() = testSpec.navBarLayerIsVisible() - - @Presubmit - @Test - open fun statusBarLayerIsVisible() = testSpec.statusBarLayerIsVisible() - - @Presubmit - @Test - open fun navBarLayerRotatesAndScales() = testSpec.navBarLayerRotatesAndScales() - - @Presubmit - @Test - open fun statusBarLayerRotatesScales() = testSpec.statusBarLayerRotatesScales() - - @Presubmit - @Test - open fun entireScreenCovered() = testSpec.entireScreenCovered() -}
\ No newline at end of file +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/SetRequestedOrientationWhilePinnedTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/SetRequestedOrientationWhilePinnedTest.kt index e40f2bc1ed5a..866e4e83ec1e 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/SetRequestedOrientationWhilePinnedTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/SetRequestedOrientationWhilePinnedTest.kt @@ -16,22 +16,26 @@ package com.android.wm.shell.flicker.pip +import android.app.Activity +import android.platform.test.annotations.FlakyTest +import android.platform.test.annotations.Postsubmit 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.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 import com.android.wm.shell.flicker.testapp.Components.FixedActivity.EXTRA_FIXED_ORIENTATION +import org.junit.Assume +import org.junit.Before import org.junit.FixMethodOrder import org.junit.Test import org.junit.runner.RunWith @@ -53,46 +57,53 @@ open class SetRequestedOrientationWhilePinnedTest( private val startingBounds = WindowUtils.getDisplayBounds(Surface.ROTATION_0) private val endingBounds = WindowUtils.getDisplayBounds(Surface.ROTATION_90) + /** {@inheritDoc} */ override val transition: FlickerBuilder.() -> Unit get() = { setup { - test { - removeAllTasksButHome() - device.wakeUpAndGoToHomeScreen() - } - eachRun { - // Launch the PiP activity fixed as landscape. - pipApp.launchViaIntent(wmHelper, stringExtras = mapOf( - EXTRA_FIXED_ORIENTATION to ORIENTATION_LANDSCAPE.toString())) - // Enter PiP. - broadcastActionTrigger.doAction(Components.PipActivity.ACTION_ENTER_PIP) - wmHelper.waitPipShown() - wmHelper.waitForRotation(Surface.ROTATION_0) - wmHelper.waitForAppTransitionIdle() - // System bar may fade out during fixed rotation. - wmHelper.waitForNavBarStatusBarVisible() - } + removeAllTasksButHome() + device.wakeUpAndGoToHomeScreen() + + // Launch the PiP activity fixed as landscape. + pipApp.launchViaIntent(wmHelper, stringExtras = mapOf( + EXTRA_FIXED_ORIENTATION to ORIENTATION_LANDSCAPE.toString())) + // Enter PiP. + broadcastActionTrigger.doAction(Components.PipActivity.ACTION_ENTER_PIP) + // System bar may fade out during fixed rotation. + wmHelper.StateSyncBuilder() + .withPipShown() + .withRotation(Surface.ROTATION_0) + .withNavOrTaskBarVisible() + .withStatusBarVisible() + .waitForAndVerify() } teardown { - eachRun { - pipApp.exit(wmHelper) - setRotation(Surface.ROTATION_0) - } - test { - removeAllTasksButHome() - } + pipApp.exit(wmHelper) + setRotation(Surface.ROTATION_0) + removeAllTasksButHome() } transitions { // Launch the activity back into fullscreen and ensure that it is now in landscape pipApp.launchViaIntent(wmHelper) - wmHelper.waitForFullScreenApp(pipApp.component) - wmHelper.waitForRotation(Surface.ROTATION_90) - wmHelper.waitForAppTransitionIdle() // System bar may fade out during fixed rotation. - wmHelper.waitForNavBarStatusBarVisible() + wmHelper.StateSyncBuilder() + .withFullScreenApp(pipApp) + .withRotation(Surface.ROTATION_90) + .withNavOrTaskBarVisible() + .withStatusBarVisible() + .waitForAndVerify() } } + /** + * This test is not compatible with Tablets. When using [Activity.setRequestedOrientation] + * to fix a orientation, Tablets instead keep the same orientation and add letterboxes + */ + @Before + fun setup() { + Assume.assumeFalse(testSpec.isTablet) + } + @Presubmit @Test fun displayEndsAt90Degrees() { @@ -101,27 +112,27 @@ open class SetRequestedOrientationWhilePinnedTest( } } + /** {@inheritDoc} */ @Presubmit @Test - override fun navBarLayerIsVisible() = super.navBarLayerIsVisible() + override fun navBarLayerIsVisibleAtStartAndEnd() = super.navBarLayerIsVisibleAtStartAndEnd() + /** {@inheritDoc} */ @Presubmit @Test - override fun statusBarLayerIsVisible() = super.statusBarLayerIsVisible() + override fun statusBarLayerIsVisibleAtStartAndEnd() = + super.statusBarLayerIsVisibleAtStartAndEnd() + /** {@inheritDoc} */ @FlakyTest @Test - override fun navBarLayerRotatesAndScales() = super.navBarLayerRotatesAndScales() - - @FlakyTest(bugId = 206753786) - @Test - override fun statusBarLayerRotatesScales() = super.statusBarLayerRotatesScales() + override fun navBarLayerPositionAtStartAndEnd() = super.navBarLayerPositionAtStartAndEnd() @Presubmit @Test fun pipWindowInsideDisplay() { testSpec.assertWmStart { - frameRegion(pipApp.component).coversAtMost(startingBounds) + visibleRegion(pipApp).coversAtMost(startingBounds) } } @@ -129,7 +140,7 @@ open class SetRequestedOrientationWhilePinnedTest( @Test fun pipAppShowsOnTop() { testSpec.assertWmEnd { - isAppWindowOnTop(pipApp.component) + isAppWindowOnTop(pipApp) } } @@ -137,7 +148,7 @@ open class SetRequestedOrientationWhilePinnedTest( @Test fun pipLayerInsideDisplay() { testSpec.assertLayersStart { - visibleRegion(pipApp.component).coversAtMost(startingBounds) + visibleRegion(pipApp).coversAtMost(startingBounds) } } @@ -145,7 +156,7 @@ open class SetRequestedOrientationWhilePinnedTest( @Test fun pipAlwaysVisible() { testSpec.assertWm { - this.isAppWindowVisible(pipApp.component) + this.isAppWindowVisible(pipApp) } } @@ -153,17 +164,31 @@ open class SetRequestedOrientationWhilePinnedTest( @Test fun pipAppLayerCoversFullScreen() { testSpec.assertLayersEnd { - visibleRegion(pipApp.component).coversExactly(endingBounds) + visibleRegion(pipApp).coversExactly(endingBounds) } } + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun taskBarLayerIsVisibleAtStartAndEnd() = super.taskBarLayerIsVisibleAtStartAndEnd() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun taskBarWindowIsAlwaysVisible() = super.taskBarWindowIsAlwaysVisible() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun entireScreenCovered() = super.entireScreenCovered() + companion object { @Parameterized.Parameters(name = "{0}") @JvmStatic fun getParams(): Collection<FlickerTestParameter> { return FlickerTestParameterFactory.getInstance() - .getConfigNonRotationTests(supportedRotations = listOf(Surface.ROTATION_0), - repetitions = 1) + .getConfigNonRotationTests(supportedRotations = listOf(Surface.ROTATION_0)) } } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/FlickerTestBase.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/PipTestBase.kt index 9c50630095be..180ced0a6814 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/FlickerTestBase.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/PipTestBase.kt @@ -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,29 +14,18 @@ * limitations under the License. */ -package com.android.wm.shell.flicker +package com.android.wm.shell.flicker.pip.tv import android.app.Instrumentation import android.content.pm.PackageManager -import android.content.pm.PackageManager.FEATURE_LEANBACK -import android.content.pm.PackageManager.FEATURE_LEANBACK_ONLY import android.view.Surface import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice -import org.junit.Assume.assumeFalse +import com.android.wm.shell.flicker.helpers.PipAppHelper import org.junit.Before import org.junit.runners.Parameterized -/** - * Base class of all Flicker test that performs common functions for all flicker tests: - * - * - Caches transitions so that a transition is run once and the transition results are used by - * tests multiple times. This is needed for parameterized tests which call the BeforeClass methods - * multiple times. - * - Keeps track of all test artifacts and deletes ones which do not need to be reviewed. - * - Fails tests if results are not available for any test due to jank. - */ -abstract class FlickerTestBase( +abstract class PipTestBase( protected val rotationName: String, protected val rotation: Int ) { @@ -45,16 +34,20 @@ abstract class FlickerTestBase( val packageManager: PackageManager = instrumentation.context.packageManager protected val isTelevision: Boolean by lazy { packageManager.run { - hasSystemFeature(FEATURE_LEANBACK) || hasSystemFeature(FEATURE_LEANBACK_ONLY) + hasSystemFeature(PackageManager.FEATURE_LEANBACK) || + hasSystemFeature(PackageManager.FEATURE_LEANBACK_ONLY) } } + protected val testApp = PipAppHelper(instrumentation) - /** - * By default WmShellFlickerTests do not run on TV devices. - * If the test should run on TV - it should override this method. - */ @Before - open fun televisionSetUp() = assumeFalse(isTelevision) + open fun televisionSetUp() { + /** + * The super implementation assumes ([org.junit.Assume]) that not running on TV, thus + * disabling the test on TV. This test, however, *should run on TV*, so we overriding this + * method and simply leaving it blank. + */ + } companion object { @Parameterized.Parameters(name = "{0}") diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipBasicTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipBasicTest.kt index 49094e609fbc..31fb16ffbd3e 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipBasicTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipBasicTest.kt @@ -43,7 +43,7 @@ class TvPipBasicTest( // Set up ratio and enter Pip testApp.clickObject(radioButtonId) - testApp.clickEnterPipButton() + testApp.clickEnterPipButton(wmHelper) val actualRatio: Float = testApp.ui?.visibleBounds?.ratio ?: fail("Application UI not found") @@ -84,4 +84,4 @@ class TvPipBasicTest( ) } } -}
\ No newline at end of file +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipMenuTests.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipMenuTests.kt index 061218a015e4..4be19d61278b 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipMenuTests.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipMenuTests.kt @@ -244,7 +244,7 @@ class TvPipMenuTests : TvPipTestBase() { } private fun enterPip_openMenu_assertShown(): UiObject2 { - testApp.clickEnterPipButton() + testApp.clickEnterPipButton(wmHelper) // Pressing the Window key should bring up Pip menu uiDevice.pressWindowKey() return uiDevice.waitForTvPipMenu() ?: fail("Pip menu should have been shown") @@ -256,4 +256,4 @@ class TvPipMenuTests : TvPipTestBase() { uiDevice.findTvPipMenuFullscreenButton() ?: fail("\"Full screen\" button should be shown in Pip menu") } -}
\ No newline at end of file +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipNotificationTests.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipNotificationTests.kt index bcf38d340867..134e97bd46e7 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipNotificationTests.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipNotificationTests.kt @@ -56,7 +56,7 @@ class TvPipNotificationTests : TvPipTestBase() { @Test fun pipNotification_postedAndDismissed() { testApp.launchViaIntent() - testApp.clickEnterPipButton() + testApp.clickEnterPipButton(wmHelper) assertNotNull("Pip notification should have been posted", waitForNotificationToAppear { it.isPipNotificationWithTitle(testApp.appName) }) @@ -70,7 +70,7 @@ class TvPipNotificationTests : TvPipTestBase() { @Test fun pipNotification_closeIntent() { testApp.launchViaIntent() - testApp.clickEnterPipButton() + testApp.clickEnterPipButton(wmHelper) val notification: StatusBarNotification = waitForNotificationToAppear { it.isPipNotificationWithTitle(testApp.appName) @@ -87,8 +87,8 @@ class TvPipNotificationTests : TvPipTestBase() { @Test fun pipNotification_menuIntent() { - testApp.launchViaIntent() - testApp.clickEnterPipButton() + testApp.launchViaIntent(wmHelper) + testApp.clickEnterPipButton(wmHelper) val notification: StatusBarNotification = waitForNotificationToAppear { it.isPipNotificationWithTitle(testApp.appName) @@ -106,10 +106,10 @@ class TvPipNotificationTests : TvPipTestBase() { @Test fun pipNotification_mediaSessionTitle_isDisplayed() { - testApp.launchViaIntent() + testApp.launchViaIntent(wmHelper) // Start media session and to PiP testApp.clickStartMediaSessionButton() - testApp.clickEnterPipButton() + testApp.clickEnterPipButton(wmHelper) // Wait for the correct notification to show up... waitForNotificationToAppear { @@ -170,4 +170,4 @@ private val StatusBarNotification.deleteIntent: PendingIntent? get() = tvExtensions?.getParcelable("delete_intent") private fun StatusBarNotification.isPipNotificationWithTitle(expectedTitle: String): Boolean = - tag == "TvPip" && title == expectedTitle
\ No newline at end of file + tag == "TvPip" && title == expectedTitle diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipTestBase.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipTestBase.kt index 9c3b0fa183b6..aeff0ac9f4f2 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipTestBase.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipTestBase.kt @@ -23,8 +23,8 @@ import android.os.SystemClock import android.view.Surface.ROTATION_0 import android.view.Surface.rotationToString import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen +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.pip.PipTestBase import org.junit.After import org.junit.Assert.assertFalse import org.junit.Assume.assumeTrue @@ -33,6 +33,7 @@ import org.junit.Before abstract class TvPipTestBase : PipTestBase(rotationToString(ROTATION_0), ROTATION_0) { private val systemUiProcessObserver = SystemUiProcessObserver() + protected val wmHelper = WindowManagerStateHelper() @Before final override fun televisionSetUp() { @@ -88,4 +89,4 @@ abstract class TvPipTestBase : PipTestBase(rotationToString(ROTATION_0), ROTATIO companion object { private const val AFTER_TEXT_PROCESS_CHECK_DELAY = 1_000L // 1 sec } -}
\ No newline at end of file +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt new file mode 100644 index 000000000000..3c439fdb9d98 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt @@ -0,0 +1,178 @@ +/* + * 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.IwTest +import android.platform.test.annotations.Postsubmit +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.SPLIT_SCREEN_DIVIDER_COMPONENT +import com.android.wm.shell.flicker.appWindowKeepVisible +import com.android.wm.shell.flicker.helpers.SplitScreenHelper +import com.android.wm.shell.flicker.layerKeepVisible +import com.android.wm.shell.flicker.splitAppLayerBoundsKeepVisible +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** + * Test copy content from the left to the right side of the split-screen. + * + * To run this test: `atest WMShellFlickerTests:CopyContentInSplit` + */ +@RequiresDevice +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +@Group1 +class CopyContentInSplit(testSpec: FlickerTestParameter) : SplitScreenBase(testSpec) { + private val textEditApp = SplitScreenHelper.getIme(instrumentation) + + override val transition: FlickerBuilder.() -> Unit + get() = { + super.transition(this) + setup { + SplitScreenHelper.enterSplit(wmHelper, tapl, primaryApp, textEditApp) + } + transitions { + SplitScreenHelper.copyContentInSplit( + instrumentation, device, primaryApp, textEditApp) + } + } + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun splitScreenDividerKeepVisible() = testSpec.layerKeepVisible(SPLIT_SCREEN_DIVIDER_COMPONENT) + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun primaryAppLayerKeepVisible() = testSpec.layerKeepVisible(primaryApp) + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun textEditAppLayerKeepVisible() = testSpec.layerKeepVisible(textEditApp) + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun primaryAppBoundsKeepVisible() = testSpec.splitAppLayerBoundsKeepVisible( + primaryApp, landscapePosLeft = tapl.isTablet, portraitPosTop = false) + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun textEditAppBoundsKeepVisible() = testSpec.splitAppLayerBoundsKeepVisible( + textEditApp, landscapePosLeft = !tapl.isTablet, portraitPosTop = true) + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun primaryAppWindowKeepVisible() = testSpec.appWindowKeepVisible(primaryApp) + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun textEditAppWindowKeepVisible() = testSpec.appWindowKeepVisible(textEditApp) + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun entireScreenCovered() = + super.entireScreenCovered() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun navBarLayerIsVisibleAtStartAndEnd() = + super.navBarLayerIsVisibleAtStartAndEnd() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun navBarLayerPositionAtStartAndEnd() = + super.navBarLayerPositionAtStartAndEnd() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun navBarWindowIsAlwaysVisible() = + super.navBarWindowIsAlwaysVisible() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun statusBarLayerIsVisibleAtStartAndEnd() = + super.statusBarLayerIsVisibleAtStartAndEnd() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun statusBarLayerPositionAtStartAndEnd() = + super.statusBarLayerPositionAtStartAndEnd() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun statusBarWindowIsAlwaysVisible() = + super.statusBarWindowIsAlwaysVisible() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun taskBarLayerIsVisibleAtStartAndEnd() = + super.taskBarLayerIsVisibleAtStartAndEnd() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun taskBarWindowIsAlwaysVisible() = + super.taskBarWindowIsAlwaysVisible() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun visibleLayersShownMoreThanOneConsecutiveEntry() = + super.visibleLayersShownMoreThanOneConsecutiveEntry() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = + super.visibleWindowsShownMoreThanOneConsecutiveEntry() + + companion object { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): List<FlickerTestParameter> { + return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests( + // 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/DismissSplitScreenByDivider.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByDivider.kt new file mode 100644 index 000000000000..60e5f78fa724 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByDivider.kt @@ -0,0 +1,206 @@ +/* + * 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.IwTest +import android.platform.test.annotations.Postsubmit +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.server.wm.flicker.helpers.WindowUtils +import com.android.wm.shell.flicker.SPLIT_SCREEN_DIVIDER_COMPONENT +import com.android.wm.shell.flicker.appWindowBecomesInvisible +import com.android.wm.shell.flicker.appWindowIsVisibleAtEnd +import com.android.wm.shell.flicker.helpers.SplitScreenHelper +import com.android.wm.shell.flicker.layerBecomesInvisible +import com.android.wm.shell.flicker.layerIsVisibleAtEnd +import com.android.wm.shell.flicker.splitAppLayerBoundsBecomesInvisible +import com.android.wm.shell.flicker.splitScreenDividerBecomesInvisible +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** + * Test dismiss split screen by dragging the divider bar. + * + * To run this test: `atest WMShellFlickerTests:DismissSplitScreenByDivider` + */ +@RequiresDevice +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +@Group1 +class DismissSplitScreenByDivider (testSpec: FlickerTestParameter) : SplitScreenBase(testSpec) { + + override val transition: FlickerBuilder.() -> Unit + get() = { + super.transition(this) + setup { + SplitScreenHelper.enterSplit(wmHelper, tapl, primaryApp, secondaryApp) + } + transitions { + if (tapl.isTablet) { + SplitScreenHelper.dragDividerToDismissSplit(device, wmHelper, + dragToRight = false, dragToBottom = true) + } else { + SplitScreenHelper.dragDividerToDismissSplit(device, wmHelper, + dragToRight = true, dragToBottom = true) + } + wmHelper.StateSyncBuilder() + .withFullScreenApp(secondaryApp) + .waitForAndVerify() + } + } + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun splitScreenDividerBecomesInvisible() = testSpec.splitScreenDividerBecomesInvisible() + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun primaryAppLayerBecomesInvisible() = testSpec.layerBecomesInvisible(primaryApp) + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun secondaryAppLayerIsVisibleAtEnd() = testSpec.layerIsVisibleAtEnd(secondaryApp) + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun primaryAppBoundsBecomesInvisible() = testSpec.splitAppLayerBoundsBecomesInvisible( + primaryApp, landscapePosLeft = tapl.isTablet, portraitPosTop = false) + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun secondaryAppBoundsIsFullscreenAtEnd() { + testSpec.assertLayers { + this.isVisible(secondaryApp) + .isVisible(SPLIT_SCREEN_DIVIDER_COMPONENT) + .then() + .isInvisible(secondaryApp) + .isVisible(SPLIT_SCREEN_DIVIDER_COMPONENT) + .then() + .isVisible(secondaryApp, isOptional = true) + .isVisible(SPLIT_SCREEN_DIVIDER_COMPONENT, isOptional = true) + .then() + .contains(SPLIT_SCREEN_DIVIDER_COMPONENT) + .then() + .invoke("secondaryAppBoundsIsFullscreenAtEnd") { + val displayBounds = WindowUtils.getDisplayBounds(testSpec.endRotation) + it.visibleRegion(secondaryApp).coversExactly(displayBounds) + } + } + } + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun primaryAppWindowBecomesInvisible() = testSpec.appWindowBecomesInvisible(primaryApp) + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun secondaryAppWindowIsVisibleAtEnd() = testSpec.appWindowIsVisibleAtEnd(secondaryApp) + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun entireScreenCovered() = + super.entireScreenCovered() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun navBarLayerIsVisibleAtStartAndEnd() = + super.navBarLayerIsVisibleAtStartAndEnd() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun navBarLayerPositionAtStartAndEnd() = + super.navBarLayerPositionAtStartAndEnd() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun navBarWindowIsAlwaysVisible() = + super.navBarWindowIsAlwaysVisible() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun statusBarLayerIsVisibleAtStartAndEnd() = + super.statusBarLayerIsVisibleAtStartAndEnd() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun statusBarLayerPositionAtStartAndEnd() = + super.statusBarLayerPositionAtStartAndEnd() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun statusBarWindowIsAlwaysVisible() = + super.statusBarWindowIsAlwaysVisible() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun taskBarLayerIsVisibleAtStartAndEnd() = + super.taskBarLayerIsVisibleAtStartAndEnd() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun taskBarWindowIsAlwaysVisible() = + super.taskBarWindowIsAlwaysVisible() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun visibleLayersShownMoreThanOneConsecutiveEntry() = + super.visibleLayersShownMoreThanOneConsecutiveEntry() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = + super.visibleWindowsShownMoreThanOneConsecutiveEntry() + + companion object { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): List<FlickerTestParameter> { + return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests( + // 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/DismissSplitScreenByGoHome.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByGoHome.kt new file mode 100644 index 000000000000..2db3009b8d68 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByGoHome.kt @@ -0,0 +1,182 @@ +/* + * 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.FlakyTest +import android.platform.test.annotations.IwTest +import android.platform.test.annotations.Postsubmit +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.appWindowBecomesInvisible +import com.android.wm.shell.flicker.helpers.SplitScreenHelper +import com.android.wm.shell.flicker.layerBecomesInvisible +import com.android.wm.shell.flicker.splitAppLayerBoundsBecomesInvisible +import com.android.wm.shell.flicker.splitScreenDividerBecomesInvisible +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** + * Test dismiss split screen by go home. + * + * To run this test: `atest WMShellFlickerTests:DismissSplitScreenByGoHome` + */ +@RequiresDevice +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +@Group1 +class DismissSplitScreenByGoHome( + testSpec: FlickerTestParameter +) : SplitScreenBase(testSpec) { + + override val transition: FlickerBuilder.() -> Unit + get() = { + super.transition(this) + setup { + SplitScreenHelper.enterSplit(wmHelper, tapl, primaryApp, secondaryApp) + } + transitions { + tapl.goHome() + wmHelper.StateSyncBuilder() + .withHomeActivityVisible() + .waitForAndVerify() + } + } + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun splitScreenDividerBecomesInvisible() = testSpec.splitScreenDividerBecomesInvisible() + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun primaryAppLayerBecomesInvisible() = testSpec.layerBecomesInvisible(primaryApp) + + // TODO(b/245472831): Move back to presubmit after shell transitions landing. + @FlakyTest(bugId = 245472831) + @Test + fun secondaryAppLayerBecomesInvisible() = testSpec.layerBecomesInvisible(primaryApp) + + // TODO(b/245472831): Move back to presubmit after shell transitions landing. + @FlakyTest(bugId = 245472831) + @Test + fun primaryAppBoundsBecomesInvisible() = testSpec.splitAppLayerBoundsBecomesInvisible( + primaryApp, landscapePosLeft = tapl.isTablet, portraitPosTop = false) + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun secondaryAppBoundsBecomesInvisible() = testSpec.splitAppLayerBoundsBecomesInvisible( + secondaryApp, landscapePosLeft = !tapl.isTablet, portraitPosTop = true) + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun primaryAppWindowBecomesInvisible() = testSpec.appWindowBecomesInvisible(primaryApp) + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun secondaryAppWindowBecomesInvisible() = testSpec.appWindowBecomesInvisible(secondaryApp) + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun entireScreenCovered() = + super.entireScreenCovered() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun navBarLayerIsVisibleAtStartAndEnd() = + super.navBarLayerIsVisibleAtStartAndEnd() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun navBarLayerPositionAtStartAndEnd() = + super.navBarLayerPositionAtStartAndEnd() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun navBarWindowIsAlwaysVisible() = + super.navBarWindowIsAlwaysVisible() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun statusBarLayerIsVisibleAtStartAndEnd() = + super.statusBarLayerIsVisibleAtStartAndEnd() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun statusBarLayerPositionAtStartAndEnd() = + super.statusBarLayerPositionAtStartAndEnd() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun statusBarWindowIsAlwaysVisible() = + super.statusBarWindowIsAlwaysVisible() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun taskBarLayerIsVisibleAtStartAndEnd() = + super.taskBarLayerIsVisibleAtStartAndEnd() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun taskBarWindowIsAlwaysVisible() = + super.taskBarWindowIsAlwaysVisible() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun visibleLayersShownMoreThanOneConsecutiveEntry() = + super.visibleLayersShownMoreThanOneConsecutiveEntry() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = + super.visibleWindowsShownMoreThanOneConsecutiveEntry() + + companion object { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): List<FlickerTestParameter> { + return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests( + // 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/DragDividerToResize.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DragDividerToResize.kt new file mode 100644 index 000000000000..fddd84cfe4db --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DragDividerToResize.kt @@ -0,0 +1,186 @@ +/* + * 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.IwTest +import android.platform.test.annotations.Postsubmit +import android.platform.test.annotations.Presubmit +import android.view.Surface +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.SPLIT_SCREEN_DIVIDER_COMPONENT +import com.android.wm.shell.flicker.appWindowKeepVisible +import com.android.wm.shell.flicker.helpers.SplitScreenHelper +import com.android.wm.shell.flicker.layerKeepVisible +import com.android.wm.shell.flicker.splitAppLayerBoundsChanges +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** + * Test resize split by dragging the divider bar. + * + * To run this test: `atest WMShellFlickerTests:DragDividerToResize` + */ +@RequiresDevice +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +@Group1 +class DragDividerToResize (testSpec: FlickerTestParameter) : SplitScreenBase(testSpec) { + + override val transition: FlickerBuilder.() -> Unit + get() = { + super.transition(this) + setup { + SplitScreenHelper.enterSplit(wmHelper, tapl, primaryApp, secondaryApp) + } + transitions { + SplitScreenHelper.dragDividerToResizeAndWait(device, wmHelper) + } + } + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun splitScreenDividerKeepVisible() = testSpec.layerKeepVisible(SPLIT_SCREEN_DIVIDER_COMPONENT) + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun primaryAppLayerKeepVisible() = testSpec.layerKeepVisible(primaryApp) + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun secondaryAppLayerVisibilityChanges() { + testSpec.assertLayers { + this.isVisible(secondaryApp) + .then() + .isInvisible(secondaryApp) + .then() + .isVisible(secondaryApp) + } + } + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun primaryAppWindowKeepVisible() = testSpec.appWindowKeepVisible(primaryApp) + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun secondaryAppWindowKeepVisible() = testSpec.appWindowKeepVisible(secondaryApp) + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun primaryAppBoundsChanges() = testSpec.splitAppLayerBoundsChanges( + primaryApp, landscapePosLeft = true, portraitPosTop = false) + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun secondaryAppBoundsChanges() = testSpec.splitAppLayerBoundsChanges( + secondaryApp, landscapePosLeft = false, portraitPosTop = true) + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun entireScreenCovered() = + super.entireScreenCovered() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun navBarLayerIsVisibleAtStartAndEnd() = + super.navBarLayerIsVisibleAtStartAndEnd() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun navBarLayerPositionAtStartAndEnd() = + super.navBarLayerPositionAtStartAndEnd() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun navBarWindowIsAlwaysVisible() = + super.navBarWindowIsAlwaysVisible() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun statusBarLayerIsVisibleAtStartAndEnd() = + super.statusBarLayerIsVisibleAtStartAndEnd() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun statusBarLayerPositionAtStartAndEnd() = + super.statusBarLayerPositionAtStartAndEnd() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun statusBarWindowIsAlwaysVisible() = + super.statusBarWindowIsAlwaysVisible() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun taskBarLayerIsVisibleAtStartAndEnd() = + super.taskBarLayerIsVisibleAtStartAndEnd() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun taskBarWindowIsAlwaysVisible() = + super.taskBarWindowIsAlwaysVisible() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun visibleLayersShownMoreThanOneConsecutiveEntry() = + super.visibleLayersShownMoreThanOneConsecutiveEntry() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = + super.visibleWindowsShownMoreThanOneConsecutiveEntry() + + companion object { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): List<FlickerTestParameter> { + return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests( + supportedRotations = listOf(Surface.ROTATION_0), + // 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/EnterSplitScreenByDragFromAllApps.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromAllApps.kt new file mode 100644 index 000000000000..a7c689884e98 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromAllApps.kt @@ -0,0 +1,229 @@ +/* + * 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.IwTest +import android.platform.test.annotations.Postsubmit +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.server.wm.flicker.helpers.isShellTransitionsEnabled +import com.android.wm.shell.flicker.SPLIT_SCREEN_DIVIDER_COMPONENT +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.splitAppLayerBoundsBecomesVisibleByDrag +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 + fun before() { + Assume.assumeTrue(tapl.isTablet) + } + + override val transition: FlickerBuilder.() -> Unit + get() = { + super.transition(this) + setup { + tapl.goHome() + primaryApp.launchViaIntent(wmHelper) + } + transitions { + tapl.launchedAppState.taskbar + .openAllApps() + .getAppIcon(secondaryApp.appName) + .dragToSplitscreen(secondaryApp.`package`, primaryApp.`package`) + SplitScreenHelper.waitForSplitComplete(wmHelper, primaryApp, secondaryApp) + } + } + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun splitScreenDividerBecomesVisible() { + Assume.assumeFalse(isShellTransitionsEnabled) + testSpec.splitScreenDividerBecomesVisible() + } + + // TODO(b/245472831): Back to splitScreenDividerBecomesVisible after shell transition ready. + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun splitScreenDividerIsVisibleAtEnd_ShellTransit() { + Assume.assumeTrue(isShellTransitionsEnabled) + testSpec.assertLayersEnd { + this.isVisible(SPLIT_SCREEN_DIVIDER_COMPONENT) + } + } + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun primaryAppLayerIsVisibleAtEnd() = testSpec.layerIsVisibleAtEnd(primaryApp) + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun secondaryAppLayerBecomesVisible() { + Assume.assumeFalse(isShellTransitionsEnabled) + testSpec.assertLayers { + this.isInvisible(secondaryApp) + .then() + .isVisible(secondaryApp) + .then() + .isInvisible(secondaryApp) + .then() + .isVisible(secondaryApp) + } + } + + // TODO(b/245472831): Align to legacy transition after shell transition ready. + @Presubmit + @Test + fun secondaryAppLayerBecomesVisible_ShellTransit() { + Assume.assumeTrue(isShellTransitionsEnabled) + testSpec.layerBecomesVisible(secondaryApp) + } + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun primaryAppBoundsIsVisibleAtEnd() = testSpec.splitAppLayerBoundsIsVisibleAtEnd( + primaryApp, landscapePosLeft = false, portraitPosTop = false) + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun secondaryAppBoundsBecomesVisible() = testSpec.splitAppLayerBoundsBecomesVisibleByDrag( + secondaryApp) + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun primaryAppWindowIsVisibleAtEnd() = testSpec.appWindowIsVisibleAtEnd(primaryApp) + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun secondaryAppWindowBecomesVisible() = testSpec.appWindowBecomesVisible(secondaryApp) + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun entireScreenCovered() = + super.entireScreenCovered() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun navBarLayerIsVisibleAtStartAndEnd() = + super.navBarLayerIsVisibleAtStartAndEnd() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun navBarLayerPositionAtStartAndEnd() = + super.navBarLayerPositionAtStartAndEnd() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun navBarWindowIsAlwaysVisible() = + super.navBarWindowIsAlwaysVisible() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun statusBarLayerIsVisibleAtStartAndEnd() = + super.statusBarLayerIsVisibleAtStartAndEnd() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun statusBarLayerPositionAtStartAndEnd() = + super.statusBarLayerPositionAtStartAndEnd() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun statusBarWindowIsAlwaysVisible() = + super.statusBarWindowIsAlwaysVisible() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun taskBarLayerIsVisibleAtStartAndEnd() = + super.taskBarLayerIsVisibleAtStartAndEnd() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun taskBarWindowIsAlwaysVisible() = + super.taskBarWindowIsAlwaysVisible() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun visibleLayersShownMoreThanOneConsecutiveEntry() = + super.visibleLayersShownMoreThanOneConsecutiveEntry() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = + super.visibleWindowsShownMoreThanOneConsecutiveEntry() + + companion object { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): List<FlickerTestParameter> { + return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests( + // 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..7d8a8db1a1ee --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromNotification.kt @@ -0,0 +1,242 @@ +/* + * 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.IwTest +import android.platform.test.annotations.Postsubmit +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.server.wm.flicker.helpers.isShellTransitionsEnabled +import com.android.wm.shell.flicker.SPLIT_SCREEN_DIVIDER_COMPONENT +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.splitAppLayerBoundsBecomesVisibleByDrag +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(tapl.isTablet) + } + + /** {@inheritDoc} */ + override val transition: FlickerBuilder.() -> Unit + get() = { + super.transition(this) + setup { + // 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") + + tapl.goHome() + primaryApp.launchViaIntent(wmHelper) + } + transitions { + SplitScreenHelper.dragFromNotificationToSplit(instrumentation, device, wmHelper) + SplitScreenHelper.waitForSplitComplete(wmHelper, primaryApp, sendNotificationApp) + } + teardown { + sendNotificationApp.exit(wmHelper) + } + } + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun splitScreenDividerBecomesVisible() { + Assume.assumeFalse(isShellTransitionsEnabled) + testSpec.splitScreenDividerBecomesVisible() + } + + // TODO(b/245472831): Back to splitScreenDividerBecomesVisible after shell transition ready. + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun splitScreenDividerIsVisibleAtEnd_ShellTransit() { + Assume.assumeTrue(isShellTransitionsEnabled) + testSpec.assertLayersEnd { + this.isVisible(SPLIT_SCREEN_DIVIDER_COMPONENT) + } + } + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun primaryAppLayerIsVisibleAtEnd() = testSpec.layerIsVisibleAtEnd(primaryApp) + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun secondaryAppLayerBecomesVisible() { + Assume.assumeFalse(isShellTransitionsEnabled) + testSpec.assertLayers { + this.isInvisible(sendNotificationApp) + .then() + .isVisible(sendNotificationApp) + .then() + .isInvisible(sendNotificationApp) + .then() + .isVisible(sendNotificationApp) + } + } + + // TODO(b/245472831): Align to legacy transition after shell transition ready. + @Presubmit + @Test + fun secondaryAppLayerBecomesVisible_ShellTransit() { + Assume.assumeTrue(isShellTransitionsEnabled) + testSpec.layerBecomesVisible(sendNotificationApp) + } + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun primaryAppBoundsIsVisibleAtEnd() = testSpec.splitAppLayerBoundsIsVisibleAtEnd( + primaryApp, landscapePosLeft = false, portraitPosTop = false) + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun secondaryAppBoundsBecomesVisible() = testSpec.splitAppLayerBoundsBecomesVisibleByDrag( + sendNotificationApp) + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun primaryAppWindowIsVisibleAtEnd() = testSpec.appWindowIsVisibleAtEnd(primaryApp) + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun secondaryAppWindowIsVisibleAtEnd() = testSpec.appWindowIsVisibleAtEnd(sendNotificationApp) + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun entireScreenCovered() = + super.entireScreenCovered() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun navBarLayerIsVisibleAtStartAndEnd() = + super.navBarLayerIsVisibleAtStartAndEnd() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun navBarLayerPositionAtStartAndEnd() = + super.navBarLayerPositionAtStartAndEnd() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun navBarWindowIsAlwaysVisible() = + super.navBarWindowIsAlwaysVisible() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun statusBarLayerIsVisibleAtStartAndEnd() = + super.statusBarLayerIsVisibleAtStartAndEnd() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun statusBarLayerPositionAtStartAndEnd() = + super.statusBarLayerPositionAtStartAndEnd() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun statusBarWindowIsAlwaysVisible() = + super.statusBarWindowIsAlwaysVisible() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun taskBarLayerIsVisibleAtStartAndEnd() = + super.taskBarLayerIsVisibleAtStartAndEnd() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun taskBarWindowIsAlwaysVisible() = + super.taskBarWindowIsAlwaysVisible() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun visibleLayersShownMoreThanOneConsecutiveEntry() = + super.visibleLayersShownMoreThanOneConsecutiveEntry() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = + super.visibleWindowsShownMoreThanOneConsecutiveEntry() + + companion object { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): List<FlickerTestParameter> { + return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests( + // 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..bfd8a3a53437 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromTaskbar.kt @@ -0,0 +1,232 @@ +/* + * 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.IwTest +import android.platform.test.annotations.Postsubmit +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.server.wm.flicker.helpers.isShellTransitionsEnabled +import com.android.wm.shell.flicker.SPLIT_SCREEN_DIVIDER_COMPONENT +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.splitAppLayerBoundsBecomesVisibleByDrag +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(tapl.isTablet) + } + + /** {@inheritDoc} */ + override val transition: FlickerBuilder.() -> Unit + get() = { + super.transition(this) + setup { + tapl.goHome() + SplitScreenHelper.createShortcutOnHotseatIfNotExist( + tapl, secondaryApp.appName + ) + primaryApp.launchViaIntent(wmHelper) + } + transitions { + tapl.launchedAppState.taskbar + .getAppIcon(secondaryApp.appName) + .dragToSplitscreen(secondaryApp.`package`, primaryApp.`package`) + SplitScreenHelper.waitForSplitComplete(wmHelper, primaryApp, secondaryApp) + } + } + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun splitScreenDividerBecomesVisible() { + Assume.assumeFalse(isShellTransitionsEnabled) + testSpec.splitScreenDividerBecomesVisible() + } + + // TODO(b/245472831): Back to splitScreenDividerBecomesVisible after shell transition ready. + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun splitScreenDividerIsVisibleAtEnd_ShellTransit() { + Assume.assumeTrue(isShellTransitionsEnabled) + testSpec.assertLayersEnd { + this.isVisible(SPLIT_SCREEN_DIVIDER_COMPONENT) + } + } + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun primaryAppLayerIsVisibleAtEnd() = testSpec.layerIsVisibleAtEnd(primaryApp) + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun secondaryAppLayerBecomesVisible() { + Assume.assumeFalse(isShellTransitionsEnabled) + testSpec.assertLayers { + this.isInvisible(secondaryApp) + .then() + .isVisible(secondaryApp) + .then() + .isInvisible(secondaryApp) + .then() + .isVisible(secondaryApp) + } + } + + // TODO(b/245472831): Align to legacy transition after shell transition ready. + @Presubmit + @Test + fun secondaryAppLayerBecomesVisible_ShellTransit() { + Assume.assumeTrue(isShellTransitionsEnabled) + testSpec.layerBecomesVisible(secondaryApp) + } + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun primaryAppBoundsIsVisibleAtEnd() = testSpec.splitAppLayerBoundsIsVisibleAtEnd( + primaryApp, landscapePosLeft = false, portraitPosTop = false) + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun secondaryAppBoundsBecomesVisible() = testSpec.splitAppLayerBoundsBecomesVisibleByDrag( + secondaryApp) + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun primaryAppWindowIsVisibleAtEnd() = testSpec.appWindowIsVisibleAtEnd(primaryApp) + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun secondaryAppWindowBecomesVisible() = testSpec.appWindowBecomesVisible(secondaryApp) + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun entireScreenCovered() = + super.entireScreenCovered() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun navBarLayerIsVisibleAtStartAndEnd() = + super.navBarLayerIsVisibleAtStartAndEnd() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun navBarLayerPositionAtStartAndEnd() = + super.navBarLayerPositionAtStartAndEnd() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun navBarWindowIsAlwaysVisible() = + super.navBarWindowIsAlwaysVisible() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun statusBarLayerIsVisibleAtStartAndEnd() = + super.statusBarLayerIsVisibleAtStartAndEnd() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun statusBarLayerPositionAtStartAndEnd() = + super.statusBarLayerPositionAtStartAndEnd() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun statusBarWindowIsAlwaysVisible() = + super.statusBarWindowIsAlwaysVisible() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun taskBarLayerIsVisibleAtStartAndEnd() = + super.taskBarLayerIsVisibleAtStartAndEnd() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun taskBarWindowIsAlwaysVisible() = + super.taskBarWindowIsAlwaysVisible() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun visibleLayersShownMoreThanOneConsecutiveEntry() = + super.visibleLayersShownMoreThanOneConsecutiveEntry() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = + super.visibleWindowsShownMoreThanOneConsecutiveEntry() + + companion object { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): List<FlickerTestParameter> { + return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests( + supportedNavigationModes = + listOf(WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL_OVERLAY) + ) + } + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenFromOverview.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenFromOverview.kt new file mode 100644 index 000000000000..cefb9f52d4fe --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenFromOverview.kt @@ -0,0 +1,182 @@ +/* + * 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.IwTest +import android.platform.test.annotations.Postsubmit +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 +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.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.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 Overview. + * + * To run this test: `atest WMShellFlickerTests:EnterSplitScreenFromOverview` + */ +@RequiresDevice +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +@Group1 +class EnterSplitScreenFromOverview(testSpec: FlickerTestParameter) : SplitScreenBase(testSpec) { + + override val transition: FlickerBuilder.() -> Unit + get() = { + super.transition(this) + setup { + tapl.workspace.switchToOverview().dismissAllTasks() + primaryApp.launchViaIntent(wmHelper) + secondaryApp.launchViaIntent(wmHelper) + tapl.goHome() + wmHelper.StateSyncBuilder() + .withAppTransitionIdle() + .withHomeActivityVisible() + .waitForAndVerify() + } + transitions { + SplitScreenHelper.splitFromOverview(tapl) + SplitScreenHelper.waitForSplitComplete(wmHelper, primaryApp, secondaryApp) + } + } + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun splitScreenDividerBecomesVisible() = testSpec.splitScreenDividerBecomesVisible() + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun primaryAppLayerIsVisibleAtEnd() = testSpec.layerIsVisibleAtEnd(primaryApp) + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun secondaryAppLayerBecomesVisible() = testSpec.layerBecomesVisible(secondaryApp) + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun primaryAppBoundsIsVisibleAtEnd() = testSpec.splitAppLayerBoundsIsVisibleAtEnd( + primaryApp, landscapePosLeft = tapl.isTablet, portraitPosTop = false) + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun secondaryAppBoundsBecomesVisible() = testSpec.splitAppLayerBoundsBecomesVisible( + secondaryApp, landscapePosLeft = !tapl.isTablet, portraitPosTop = true) + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun primaryAppWindowBecomesVisible() = testSpec.appWindowBecomesVisible(primaryApp) + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun secondaryAppWindowBecomesVisible() = testSpec.appWindowBecomesVisible(secondaryApp) + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun entireScreenCovered() = + super.entireScreenCovered() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun navBarLayerIsVisibleAtStartAndEnd() = + super.navBarLayerIsVisibleAtStartAndEnd() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun navBarLayerPositionAtStartAndEnd() = + super.navBarLayerPositionAtStartAndEnd() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun navBarWindowIsAlwaysVisible() = + super.navBarWindowIsAlwaysVisible() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun statusBarLayerIsVisibleAtStartAndEnd() = + super.statusBarLayerIsVisibleAtStartAndEnd() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun statusBarLayerPositionAtStartAndEnd() = + super.statusBarLayerPositionAtStartAndEnd() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun statusBarWindowIsAlwaysVisible() = + super.statusBarWindowIsAlwaysVisible() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun taskBarLayerIsVisibleAtStartAndEnd() = + super.taskBarLayerIsVisibleAtStartAndEnd() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun taskBarWindowIsAlwaysVisible() = + super.taskBarWindowIsAlwaysVisible() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun visibleLayersShownMoreThanOneConsecutiveEntry() = + super.visibleLayersShownMoreThanOneConsecutiveEntry() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = + super.visibleWindowsShownMoreThanOneConsecutiveEntry() + + companion object { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): List<FlickerTestParameter> { + return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests() + } + } +} 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..eab473ca55a1 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SplitScreenBase.kt @@ -0,0 +1,44 @@ +/* + * 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.content.Context +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.BaseTest +import com.android.wm.shell.flicker.helpers.SplitScreenHelper + +abstract class SplitScreenBase(testSpec: FlickerTestParameter) : BaseTest(testSpec) { + protected val context: Context = instrumentation.context + protected val primaryApp = SplitScreenHelper.getPrimary(instrumentation) + protected val secondaryApp = SplitScreenHelper.getSecondary(instrumentation) + + /** {@inheritDoc} */ + override val transition: FlickerBuilder.() -> Unit + get() = { + setup { + tapl.setEnableRotation(true) + setRotation(testSpec.startRotation) + tapl.setExpectedRotation(testSpec.startRotation) + } + teardown { + primaryApp.exit(wmHelper) + secondaryApp.exit(wmHelper) + } + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchAppByDoubleTapDivider.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchAppByDoubleTapDivider.kt new file mode 100644 index 000000000000..2d5d2b71b270 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchAppByDoubleTapDivider.kt @@ -0,0 +1,182 @@ +/* + * 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.FlakyTest +import android.platform.test.annotations.IwTest +import android.platform.test.annotations.Postsubmit +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.SPLIT_SCREEN_DIVIDER_COMPONENT +import com.android.wm.shell.flicker.appWindowIsVisibleAtEnd +import com.android.wm.shell.flicker.helpers.SplitScreenHelper +import com.android.wm.shell.flicker.layerIsVisibleAtEnd +import com.android.wm.shell.flicker.layerKeepVisible +import com.android.wm.shell.flicker.splitAppLayerBoundsIsVisibleAtEnd +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** + * Test double tap the divider bar to switch the two apps. + * + * To run this test: `atest WMShellFlickerTests:SwitchAppByDoubleTapDivider` + */ +@RequiresDevice +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +@Group1 +class SwitchAppByDoubleTapDivider (testSpec: FlickerTestParameter) : SplitScreenBase(testSpec) { + + override val transition: FlickerBuilder.() -> Unit + get() = { + super.transition(this) + setup { + SplitScreenHelper.enterSplit(wmHelper, tapl, primaryApp, secondaryApp) + } + transitions { + SplitScreenHelper.doubleTapDividerToSwitch(device) + wmHelper.StateSyncBuilder() + .withAppTransitionIdle() + .waitForAndVerify() + } + } + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun splitScreenDividerKeepVisible() = testSpec.layerKeepVisible(SPLIT_SCREEN_DIVIDER_COMPONENT) + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun primaryAppLayerIsVisibleAtEnd() = testSpec.layerIsVisibleAtEnd(primaryApp) + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun secondaryAppLayerIsVisibleAtEnd() = testSpec.layerIsVisibleAtEnd(secondaryApp) + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun primaryAppBoundsIsVisibleAtEnd() = testSpec.splitAppLayerBoundsIsVisibleAtEnd( + primaryApp, landscapePosLeft = !tapl.isTablet, portraitPosTop = true) + + // TODO(b/246490534): Move back to presubmit after withAppTransitionIdle is robust enough to + // get the correct end state. + @FlakyTest(bugId = 246490534) + @Test + fun secondaryAppBoundsIsVisibleAtEnd() = testSpec.splitAppLayerBoundsIsVisibleAtEnd( + secondaryApp, landscapePosLeft = tapl.isTablet, portraitPosTop = false) + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun primaryAppWindowIsVisibleAtEnd() = testSpec.appWindowIsVisibleAtEnd(primaryApp) + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun secondaryAppWindowIsVisibleAtEnd() = testSpec.appWindowIsVisibleAtEnd(secondaryApp) + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun entireScreenCovered() = + super.entireScreenCovered() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun navBarLayerIsVisibleAtStartAndEnd() = + super.navBarLayerIsVisibleAtStartAndEnd() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun navBarLayerPositionAtStartAndEnd() = + super.navBarLayerPositionAtStartAndEnd() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun navBarWindowIsAlwaysVisible() = + super.navBarWindowIsAlwaysVisible() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun statusBarLayerIsVisibleAtStartAndEnd() = + super.statusBarLayerIsVisibleAtStartAndEnd() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun statusBarLayerPositionAtStartAndEnd() = + super.statusBarLayerPositionAtStartAndEnd() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun statusBarWindowIsAlwaysVisible() = + super.statusBarWindowIsAlwaysVisible() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun taskBarLayerIsVisibleAtStartAndEnd() = + super.taskBarLayerIsVisibleAtStartAndEnd() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun taskBarWindowIsAlwaysVisible() = + super.taskBarWindowIsAlwaysVisible() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun visibleLayersShownMoreThanOneConsecutiveEntry() = + super.visibleLayersShownMoreThanOneConsecutiveEntry() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = + super.visibleWindowsShownMoreThanOneConsecutiveEntry() + + companion object { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): List<FlickerTestParameter> { + return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests( + // 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/SwitchBackToSplitFromAnotherApp.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromAnotherApp.kt new file mode 100644 index 000000000000..20c6af7c0530 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromAnotherApp.kt @@ -0,0 +1,183 @@ +/* + * 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.IwTest +import android.platform.test.annotations.Postsubmit +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.helpers.SplitScreenHelper +import com.android.wm.shell.flicker.layerBecomesVisible +import com.android.wm.shell.flicker.splitAppLayerBoundsIsVisibleAtEnd +import com.android.wm.shell.flicker.splitScreenDividerBecomesVisible +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** + * Test quick switch to split pair from another app. + * + * To run this test: `atest WMShellFlickerTests:SwitchBackToSplitFromAnotherApp` + */ +@RequiresDevice +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +@Group1 +class SwitchBackToSplitFromAnotherApp(testSpec: FlickerTestParameter) : SplitScreenBase(testSpec) { + val thirdApp = SplitScreenHelper.getNonResizeable(instrumentation) + + override val transition: FlickerBuilder.() -> Unit + get() = { + super.transition(this) + setup { + SplitScreenHelper.enterSplit(wmHelper, tapl, primaryApp, secondaryApp) + + thirdApp.launchViaIntent(wmHelper) + wmHelper.StateSyncBuilder() + .withWindowSurfaceAppeared(thirdApp) + .waitForAndVerify() + } + transitions { + tapl.launchedAppState.quickSwitchToPreviousApp() + SplitScreenHelper.waitForSplitComplete(wmHelper, primaryApp, secondaryApp) + } + } + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun splitScreenDividerBecomesVisible() = testSpec.splitScreenDividerBecomesVisible() + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun primaryAppLayerBecomesVisible() = testSpec.layerBecomesVisible(primaryApp) + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun secondaryAppLayerBecomesVisible() = testSpec.layerBecomesVisible(secondaryApp) + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun primaryAppBoundsIsVisibleAtEnd() = testSpec.splitAppLayerBoundsIsVisibleAtEnd( + primaryApp, landscapePosLeft = tapl.isTablet, portraitPosTop = false) + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun secondaryAppBoundsIsVisibleAtEnd() = testSpec.splitAppLayerBoundsIsVisibleAtEnd( + secondaryApp, landscapePosLeft = !tapl.isTablet, portraitPosTop = true) + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun primaryAppWindowBecomesVisible() = testSpec.appWindowBecomesVisible(primaryApp) + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun secondaryAppWindowBecomesVisible() = testSpec.appWindowBecomesVisible(secondaryApp) + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun entireScreenCovered() = + super.entireScreenCovered() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun navBarLayerIsVisibleAtStartAndEnd() = + super.navBarLayerIsVisibleAtStartAndEnd() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun navBarLayerPositionAtStartAndEnd() = + super.navBarLayerPositionAtStartAndEnd() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun navBarWindowIsAlwaysVisible() = + super.navBarWindowIsAlwaysVisible() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun statusBarLayerIsVisibleAtStartAndEnd() = + super.statusBarLayerIsVisibleAtStartAndEnd() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun statusBarLayerPositionAtStartAndEnd() = + super.statusBarLayerPositionAtStartAndEnd() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun statusBarWindowIsAlwaysVisible() = + super.statusBarWindowIsAlwaysVisible() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun taskBarLayerIsVisibleAtStartAndEnd() = + super.taskBarLayerIsVisibleAtStartAndEnd() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun taskBarWindowIsAlwaysVisible() = + super.taskBarWindowIsAlwaysVisible() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun visibleLayersShownMoreThanOneConsecutiveEntry() = + super.visibleLayersShownMoreThanOneConsecutiveEntry() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = + super.visibleWindowsShownMoreThanOneConsecutiveEntry() + + companion object { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): List<FlickerTestParameter> { + return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests( + // 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/SwitchBackToSplitFromHome.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromHome.kt new file mode 100644 index 000000000000..cb9ca9f13849 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromHome.kt @@ -0,0 +1,182 @@ +/* + * 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.IwTest +import android.platform.test.annotations.Postsubmit +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.helpers.SplitScreenHelper +import com.android.wm.shell.flicker.layerBecomesVisible +import com.android.wm.shell.flicker.splitAppLayerBoundsIsVisibleAtEnd +import com.android.wm.shell.flicker.splitScreenDividerBecomesVisible +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** + * Test quick switch to split pair from home. + * + * To run this test: `atest WMShellFlickerTests:SwitchBackToSplitFromHome` + */ +@RequiresDevice +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +@Group1 +class SwitchBackToSplitFromHome(testSpec: FlickerTestParameter) : SplitScreenBase(testSpec) { + + override val transition: FlickerBuilder.() -> Unit + get() = { + super.transition(this) + setup { + SplitScreenHelper.enterSplit(wmHelper, tapl, primaryApp, secondaryApp) + + tapl.goHome() + wmHelper.StateSyncBuilder() + .withHomeActivityVisible() + .waitForAndVerify() + } + transitions { + tapl.workspace.quickSwitchToPreviousApp() + SplitScreenHelper.waitForSplitComplete(wmHelper, primaryApp, secondaryApp) + } + } + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun splitScreenDividerBecomesVisible() = testSpec.splitScreenDividerBecomesVisible() + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun primaryAppLayerBecomesVisible() = testSpec.layerBecomesVisible(primaryApp) + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun secondaryAppLayerBecomesVisible() = testSpec.layerBecomesVisible(secondaryApp) + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun primaryAppBoundsIsVisibleAtEnd() = testSpec.splitAppLayerBoundsIsVisibleAtEnd( + primaryApp, landscapePosLeft = tapl.isTablet, portraitPosTop = false) + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun secondaryAppBoundsIsVisibleAtEnd() = testSpec.splitAppLayerBoundsIsVisibleAtEnd( + secondaryApp, landscapePosLeft = !tapl.isTablet, portraitPosTop = true) + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun primaryAppWindowBecomesVisible() = testSpec.appWindowBecomesVisible(primaryApp) + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun secondaryAppWindowBecomesVisible() = testSpec.appWindowBecomesVisible(secondaryApp) + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun entireScreenCovered() = + super.entireScreenCovered() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun navBarLayerIsVisibleAtStartAndEnd() = + super.navBarLayerIsVisibleAtStartAndEnd() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun navBarLayerPositionAtStartAndEnd() = + super.navBarLayerPositionAtStartAndEnd() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun navBarWindowIsAlwaysVisible() = + super.navBarWindowIsAlwaysVisible() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun statusBarLayerIsVisibleAtStartAndEnd() = + super.statusBarLayerIsVisibleAtStartAndEnd() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun statusBarLayerPositionAtStartAndEnd() = + super.statusBarLayerPositionAtStartAndEnd() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun statusBarWindowIsAlwaysVisible() = + super.statusBarWindowIsAlwaysVisible() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun taskBarLayerIsVisibleAtStartAndEnd() = + super.taskBarLayerIsVisibleAtStartAndEnd() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun taskBarWindowIsAlwaysVisible() = + super.taskBarWindowIsAlwaysVisible() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun visibleLayersShownMoreThanOneConsecutiveEntry() = + super.visibleLayersShownMoreThanOneConsecutiveEntry() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = + super.visibleWindowsShownMoreThanOneConsecutiveEntry() + + companion object { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): List<FlickerTestParameter> { + return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests( + // 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/SwitchBackToSplitFromRecent.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromRecent.kt new file mode 100644 index 000000000000..266276749c13 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromRecent.kt @@ -0,0 +1,184 @@ +/* + * 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.IwTest +import android.platform.test.annotations.Postsubmit +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.helpers.SplitScreenHelper +import com.android.wm.shell.flicker.layerBecomesVisible +import com.android.wm.shell.flicker.splitAppLayerBoundsIsVisibleAtEnd +import com.android.wm.shell.flicker.splitScreenDividerBecomesVisible +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** + * Test switch back to split pair from recent. + * + * To run this test: `atest WMShellFlickerTests:SwitchBackToSplitFromRecent` + */ +@RequiresDevice +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +@Group1 +class SwitchBackToSplitFromRecent(testSpec: FlickerTestParameter) : SplitScreenBase(testSpec) { + + override val transition: FlickerBuilder.() -> Unit + get() = { + super.transition(this) + setup { + SplitScreenHelper.enterSplit(wmHelper, tapl, primaryApp, secondaryApp) + + tapl.goHome() + wmHelper.StateSyncBuilder() + .withHomeActivityVisible() + .waitForAndVerify() + } + transitions { + tapl.workspace.switchToOverview() + .currentTask + .open() + SplitScreenHelper.waitForSplitComplete(wmHelper, primaryApp, secondaryApp) + } + } + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun splitScreenDividerBecomesVisible() = testSpec.splitScreenDividerBecomesVisible() + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun primaryAppLayerBecomesVisible() = testSpec.layerBecomesVisible(primaryApp) + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun secondaryAppLayerBecomesVisible() = testSpec.layerBecomesVisible(secondaryApp) + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun primaryAppBoundsIsVisibleAtEnd() = testSpec.splitAppLayerBoundsIsVisibleAtEnd( + primaryApp, landscapePosLeft = tapl.isTablet, portraitPosTop = false) + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun secondaryAppBoundsIsVisibleAtEnd() = testSpec.splitAppLayerBoundsIsVisibleAtEnd( + secondaryApp, landscapePosLeft = !tapl.isTablet, portraitPosTop = true) + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun primaryAppWindowBecomesVisible() = testSpec.appWindowBecomesVisible(primaryApp) + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun secondaryAppWindowBecomesVisible() = testSpec.appWindowBecomesVisible(secondaryApp) + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun entireScreenCovered() = + super.entireScreenCovered() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun navBarLayerIsVisibleAtStartAndEnd() = + super.navBarLayerIsVisibleAtStartAndEnd() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun navBarLayerPositionAtStartAndEnd() = + super.navBarLayerPositionAtStartAndEnd() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun navBarWindowIsAlwaysVisible() = + super.navBarWindowIsAlwaysVisible() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun statusBarLayerIsVisibleAtStartAndEnd() = + super.statusBarLayerIsVisibleAtStartAndEnd() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun statusBarLayerPositionAtStartAndEnd() = + super.statusBarLayerPositionAtStartAndEnd() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun statusBarWindowIsAlwaysVisible() = + super.statusBarWindowIsAlwaysVisible() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun taskBarLayerIsVisibleAtStartAndEnd() = + super.taskBarLayerIsVisibleAtStartAndEnd() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun taskBarWindowIsAlwaysVisible() = + super.taskBarWindowIsAlwaysVisible() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun visibleLayersShownMoreThanOneConsecutiveEntry() = + super.visibleLayersShownMoreThanOneConsecutiveEntry() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = + super.visibleWindowsShownMoreThanOneConsecutiveEntry() + + companion object { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): List<FlickerTestParameter> { + return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests( + // 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/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/res/layout/activity_splitscreen.xml b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_splitscreen.xml index 84789f5a6c02..642a08b5bbe0 100644 --- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_splitscreen.xml +++ b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_splitscreen.xml @@ -26,6 +26,7 @@ android:layout_width="fill_parent" android:layout_height="fill_parent" android:gravity="center_vertical|center_horizontal" + android:textIsSelectable="true" android:text="PrimaryActivity" android:textAppearance="?android:attr/textAppearanceLarge"/> 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..c0720cf04028 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,10 @@ 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; public static WindowContainerToken createMockWCToken() { final IWindowContainerToken itoken = mock(IWindowContainerToken.class); @@ -68,17 +74,42 @@ 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 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; return info; } } 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..a7234c1d3cb8 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunnerTests.java @@ -0,0 +1,79 @@ +/* + * 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 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.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); + } +} 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..5b3b8fd7ad71 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; @@ -57,7 +60,9 @@ 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 +79,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 = @@ -107,10 +113,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(); } @@ -120,24 +128,22 @@ public class BackAnimationControllerTest { 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(); - } + BackNavigationInfo.Builder builder = new BackNavigationInfo.Builder() + .setType(backType) + .setDepartingAnimationTarget(topAnimationTarget) + .setScreenshotSurface(screenshotSurface) + .setScreenshotBuffer(hardwareBuffer) + .setTaskWindowConfiguration(new WindowConfiguration()) + .setOnBackNavigationDone(new RemoteCallback((bundle) -> {})) + .setOnBackInvokedCallback(onBackInvokedCallback); + + createNavigationInfo(builder); } private void createNavigationInfo(BackNavigationInfo.Builder builder) { try { - doReturn(builder.build()).when(mActivityTaskManager).startBackNavigation(anyBoolean()); + doReturn(builder.build()).when(mActivityTaskManager) + .startBackNavigation(anyBoolean(), any()); } catch (RemoteException ex) { ex.rethrowFromSystemServer(); } @@ -159,6 +165,11 @@ 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(); @@ -232,10 +243,12 @@ 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(); @@ -271,9 +284,14 @@ 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); verify(mIOnBackInvokedCallback).onBackStarted(); @@ -296,6 +314,34 @@ public class BackAnimationControllerTest { 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); + + doMotionEvent(MotionEvent.ACTION_DOWN, 0); + // Check that back start and progress is dispatched when first move. + doMotionEvent(MotionEvent.ACTION_MOVE, 100); + 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, 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..577942505b13 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java @@ -0,0 +1,197 @@ +/* + * 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 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.WindowConfiguration; +import android.os.Handler; +import android.os.IBinder; +import android.testing.AndroidTestingRunner; +import android.window.WindowContainerToken; +import android.window.WindowContainerTransaction; +import android.window.WindowContainerTransaction.Change; + +import androidx.test.filters.SmallTest; + +import com.android.wm.shell.RootDisplayAreaOrganizer; +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.ShellTestCase; +import com.android.wm.shell.common.ShellExecutor; +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.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.Mockito; + +@SmallTest +@RunWith(AndroidTestingRunner.class) +public class DesktopModeControllerTest extends ShellTestCase { + + @Mock + private ShellTaskOrganizer mShellTaskOrganizer; + @Mock + private RootDisplayAreaOrganizer mRootDisplayAreaOrganizer; + @Mock + private ShellExecutor mTestExecutor; + @Mock + private Handler mMockHandler; + @Mock + private Transitions mMockTransitions; + + private DesktopModeController mController; + private ShellInit mShellInit; + + @Before + public void setUp() { + mShellInit = Mockito.spy(new ShellInit(mTestExecutor)); + + mController = new DesktopModeController(mContext, mShellInit, mShellTaskOrganizer, + mRootDisplayAreaOrganizer, mMockHandler, mMockTransitions); + + mShellInit.init(); + } + + @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 WCT to simulate setting display windowing mode to freeform + WindowContainerTransaction displayWct = new WindowContainerTransaction(); + MockToken displayMockToken = new MockToken(); + displayWct.setWindowingMode(displayMockToken.token(), WINDOWING_MODE_FREEFORM); + when(mRootDisplayAreaOrganizer.prepareWindowingModeChange(mContext.getDisplayId(), + WINDOWING_MODE_FREEFORM)).thenReturn(displayWct); + + // The test + mController.updateDesktopModeActive(true); + + ArgumentCaptor<WindowContainerTransaction> arg = ArgumentCaptor.forClass( + WindowContainerTransaction.class); + verify(mRootDisplayAreaOrganizer).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(displayMockToken.binder()); + 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 WCT to simulate setting display windowing mode to fullscreen + WindowContainerTransaction displayWct = new WindowContainerTransaction(); + MockToken displayMockToken = new MockToken(); + displayWct.setWindowingMode(displayMockToken.token(), WINDOWING_MODE_FULLSCREEN); + when(mRootDisplayAreaOrganizer.prepareWindowingModeChange(mContext.getDisplayId(), + WINDOWING_MODE_FULLSCREEN)).thenReturn(displayWct); + + // The test + mController.updateDesktopModeActive(false); + + ArgumentCaptor<WindowContainerTransaction> arg = ArgumentCaptor.forClass( + WindowContainerTransaction.class); + verify(mRootDisplayAreaOrganizer).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 taskWmModeChange = wct.getChanges().get(taskWmMockToken.binder()); + assertThat(taskWmModeChange).isNotNull(); + assertThat(taskWmModeChange.getWindowingMode()).isEqualTo(WINDOWING_MODE_UNDEFINED); + + // Verify executed WCT has a change for clearing task bounds + Change taskBoundsChange = wct.getChanges().get(taskBoundsMockToken.binder()); + assertThat(taskBoundsChange).isNotNull(); + assertThat(taskBoundsChange.getWindowSetMask() + & WindowConfiguration.WINDOW_CONFIG_BOUNDS).isNotEqualTo(0); + assertThat(taskBoundsChange.getConfiguration().windowConfiguration.getBounds().isEmpty()) + .isTrue(); + + // Verify executed WCT has a change for setting display windowing mode to fullscreen + Change displayWmModeChange = wct.getChanges().get(displayMockToken.binder()); + assertThat(displayWmModeChange).isNotNull(); + assertThat(displayWmModeChange.getWindowingMode()).isEqualTo(WINDOWING_MODE_FULLSCREEN); + } + + 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/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..a8d3bdcb7c96 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,6 +43,7 @@ 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; @@ -53,11 +56,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 +79,19 @@ 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 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 +100,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 +113,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, 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 +181,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, mMockPipAppOpsListener, + mMockPipBoundsAlgorithm, mMockPipKeepClearAlgorithm, + mMockPipBoundsState, mMockPipMotionHelper, mMockPipMediaController, + mMockPhonePipMenuController, mMockPipTaskOrganizer, mMockPipTransitionState, + mMockPipTouchHandler, mMockPipTransitionController, mMockWindowManagerShellWrapper, + mMockTaskStackListener, mMockPipParamsChangedForwarder, + mMockDisplayInsetsController, mMockOneHandedController, mMockExecutor)); } @Test @@ -199,7 +250,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 +266,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 +293,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/pip/tv/TvPipBoundsControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipBoundsControllerTest.kt index 05e472245b4a..cc51efd7e16b 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipBoundsControllerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipBoundsControllerTest.kt @@ -179,6 +179,16 @@ class TvPipBoundsControllerTest { } @Test + fun testImmediatePlacement_DoNotStashIfAlreadyUnstashed() { + triggerImmediatePlacement(STASHED_PLACEMENT_RESTASH) + assertMovement(STASHED_BOUNDS) + assertMovementAt(time + STASH_DURATION, ANCHOR_BOUNDS) + + triggerImmediatePlacement(STASHED_PLACEMENT) + assertNoMovementUpTo(time + FAR_FUTURE) + } + + @Test fun testInMoveMode_KeepAtAnchor() { startMoveMode() triggerImmediatePlacement(STASHED_MOVED_PLACEMENT_RESTASH) 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..cadfeb0de312 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.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.DesktopMode; +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; @@ -69,18 +81,39 @@ public class RecentTasksControllerTest extends ShellTestCase { private Context mContext; @Mock private TaskStackListenerImpl mTaskStackListener; + @Mock + private ShellCommandHandler mShellCommandHandler; + @Mock + private DesktopModeTaskRepository mDesktopModeTaskRepository; private ShellTaskOrganizer mShellTaskOrganizer; private RecentTasksController mRecentTasksController; + private ShellInit mShellInit; private ShellExecutor mMainExecutor; @Before public void setUp() { mMainExecutor = new TestShellExecutor(); - mRecentTasksController = spy(new RecentTasksController(mContext, mTaskStackListener, + when(mContext.getPackageManager()).thenReturn(mock(PackageManager.class)); + mShellInit = spy(new ShellInit(mMainExecutor)); + mRecentTasksController = spy(new RecentTasksController(mContext, mShellInit, + mShellCommandHandler, mTaskStackListener, Optional.of(mDesktopModeTaskRepository), mMainExecutor)); - mShellTaskOrganizer = new ShellTaskOrganizer(mMainExecutor, mContext, - null /* sizeCompatUI */, Optional.of(mRecentTasksController)); + 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 +122,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 +137,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 +172,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 +188,46 @@ public class RecentTasksControllerTest extends ShellTestCase { } @Test + public void testGetRecentTasks_groupActiveFreeformTasks() { + StaticMockitoSession mockitoSession = mockitoSession().mockStatic( + DesktopMode.class).startMocking(); + when(DesktopMode.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 +235,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); @@ -230,6 +303,7 @@ public class RecentTasksControllerTest extends ShellTestCase { /** * 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 */ @@ -238,22 +312,23 @@ public class RecentTasksControllerTest extends ShellTestCase { 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..9240abfbe47f 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 @@ -34,6 +34,7 @@ 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; @@ -58,6 +59,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 +70,6 @@ import org.mockito.MockitoAnnotations; import java.util.Optional; -import javax.inject.Provider; - /** * Tests for {@link StageCoordinator} */ @@ -85,10 +85,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 +97,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 +113,21 @@ 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())); + mDisplayInsetsController, mSplitLayout, mTransitions, mTransactionPool, + mMainExecutor, Optional.empty())); doNothing().when(mStageCoordinator).updateActivityOptions(any(), anyInt()); 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 +167,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 +176,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 +225,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 +237,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 @@ -314,20 +303,4 @@ 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); - } - } - } } 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/androidfw/Android.bp b/libs/androidfw/Android.bp index c80fb188e70f..779c4b75efc4 100644 --- a/libs/androidfw/Android.bp +++ b/libs/androidfw/Android.bp @@ -33,6 +33,7 @@ license { cc_defaults { name: "libandroidfw_defaults", + cpp_std: "gnu++2b", cflags: [ "-Werror", "-Wunreachable-code", @@ -60,6 +61,7 @@ cc_library { "AssetManager2.cpp", "AssetsProvider.cpp", "AttributeResolution.cpp", + "BigBuffer.cpp", "ChunkIterator.cpp", "ConfigDescription.cpp", "Idmap.cpp", @@ -72,6 +74,7 @@ cc_library { "ResourceTypes.cpp", "ResourceUtils.cpp", "StreamingZipInflater.cpp", + "StringPool.cpp", "TypeWrappers.cpp", "Util.cpp", "ZipFileRO.cpp", @@ -161,6 +164,7 @@ cc_test { "tests/AssetManager2_test.cpp", "tests/AttributeFinder_test.cpp", "tests/AttributeResolution_test.cpp", + "tests/BigBuffer_test.cpp", "tests/ByteBucketArray_test.cpp", "tests/Config_test.cpp", "tests/ConfigDescription_test.cpp", @@ -173,6 +177,7 @@ cc_test { "tests/ResTable_test.cpp", "tests/Split_test.cpp", "tests/StringPiece_test.cpp", + "tests/StringPool_test.cpp", "tests/Theme_test.cpp", "tests/TypeWrappers_test.cpp", "tests/ZipUtils_test.cpp", diff --git a/libs/androidfw/ApkAssets.cpp b/libs/androidfw/ApkAssets.cpp index 2beb33abe782..9aa37872b8de 100755 --- a/libs/androidfw/ApkAssets.cpp +++ b/libs/androidfw/ApkAssets.cpp @@ -141,6 +141,9 @@ std::unique_ptr<ApkAssets> ApkAssets::LoadImpl(std::unique_ptr<Asset> resources_ return {}; } loaded_arsc = LoadedArsc::Load(data, length, loaded_idmap.get(), property_flags); + } else if (loaded_idmap != nullptr && + IsFabricatedOverlay(std::string(loaded_idmap->OverlayApkPath()))) { + loaded_arsc = LoadedArsc::Load(loaded_idmap.get()); } else { loaded_arsc = LoadedArsc::CreateEmpty(); } diff --git a/libs/androidfw/AssetManager2.cpp b/libs/androidfw/AssetManager2.cpp index 136fc6ca4e2a..235700b27c25 100644 --- a/libs/androidfw/AssetManager2.cpp +++ b/libs/androidfw/AssetManager2.cpp @@ -611,7 +611,21 @@ base::expected<FindEntryResult, NullOrIOError> AssetManager2::FindEntry( } if (overlay_entry.IsInlineValue()) { // The target resource is overlaid by an inline value not represented by a resource. - result->entry = overlay_entry.GetInlineValue(); + ConfigDescription best_frro_config; + Res_value best_frro_value; + bool frro_found = false; + for( const auto& [config, value] : overlay_entry.GetInlineValue()) { + if ((!frro_found || config.isBetterThan(best_frro_config, desired_config)) + && config.match(*desired_config)) { + frro_found = true; + best_frro_config = config; + best_frro_value = value; + } + } + if (!frro_found) { + continue; + } + result->entry = best_frro_value; result->dynamic_ref_table = id_map.overlay_res_maps_.GetOverlayDynamicRefTable(); result->cookie = id_map.cookie; diff --git a/libs/androidfw/BigBuffer.cpp b/libs/androidfw/BigBuffer.cpp new file mode 100644 index 000000000000..bedfc49a1b0d --- /dev/null +++ b/libs/androidfw/BigBuffer.cpp @@ -0,0 +1,87 @@ +/* + * 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. + */ + +#include <androidfw/BigBuffer.h> + +#include <algorithm> +#include <memory> +#include <vector> + +#include "android-base/logging.h" + +namespace android { + +void* BigBuffer::NextBlockImpl(size_t size) { + if (!blocks_.empty()) { + Block& block = blocks_.back(); + if (block.block_size_ - block.size >= size) { + void* out_buffer = block.buffer.get() + block.size; + block.size += size; + size_ += size; + return out_buffer; + } + } + + const size_t actual_size = std::max(block_size_, size); + + Block block = {}; + + // Zero-allocate the block's buffer. + block.buffer = std::unique_ptr<uint8_t[]>(new uint8_t[actual_size]()); + CHECK(block.buffer); + + block.size = size; + block.block_size_ = actual_size; + + blocks_.push_back(std::move(block)); + size_ += size; + return blocks_.back().buffer.get(); +} + +void* BigBuffer::NextBlock(size_t* out_size) { + if (!blocks_.empty()) { + Block& block = blocks_.back(); + if (block.size != block.block_size_) { + void* out_buffer = block.buffer.get() + block.size; + size_t size = block.block_size_ - block.size; + block.size = block.block_size_; + size_ += size; + *out_size = size; + return out_buffer; + } + } + + // Zero-allocate the block's buffer. + Block block = {}; + block.buffer = std::unique_ptr<uint8_t[]>(new uint8_t[block_size_]()); + CHECK(block.buffer); + block.size = block_size_; + block.block_size_ = block_size_; + blocks_.push_back(std::move(block)); + size_ += block_size_; + *out_size = block_size_; + return blocks_.back().buffer.get(); +} + +std::string BigBuffer::to_string() const { + std::string result; + for (const Block& block : blocks_) { + result.append(block.buffer.get(), block.buffer.get() + block.size); + } + return result; +} + +} // namespace android diff --git a/libs/androidfw/Idmap.cpp b/libs/androidfw/Idmap.cpp index efd1f6a25786..e122d4844ee9 100644 --- a/libs/androidfw/Idmap.cpp +++ b/libs/androidfw/Idmap.cpp @@ -56,6 +56,8 @@ struct Idmap_header { struct Idmap_data_header { uint32_t target_entry_count; uint32_t target_inline_entry_count; + uint32_t target_inline_entry_value_count; + uint32_t configuration_count; uint32_t overlay_entry_count; uint32_t string_pool_index_offset; @@ -68,6 +70,12 @@ struct Idmap_target_entry { struct Idmap_target_entry_inline { uint32_t target_id; + uint32_t start_value_index; + uint32_t value_count; +}; + +struct Idmap_target_entry_inline_value { + uint32_t config_index; Res_value value; }; @@ -138,11 +146,15 @@ status_t OverlayDynamicRefTable::lookupResourceIdNoRewrite(uint32_t* resId) cons IdmapResMap::IdmapResMap(const Idmap_data_header* data_header, const Idmap_target_entry* entries, const Idmap_target_entry_inline* inline_entries, + const Idmap_target_entry_inline_value* inline_entry_values, + const ConfigDescription* configs, uint8_t target_assigned_package_id, const OverlayDynamicRefTable* overlay_ref_table) : data_header_(data_header), entries_(entries), inline_entries_(inline_entries), + inline_entry_values_(inline_entry_values), + configurations_(configs), target_assigned_package_id_(target_assigned_package_id), overlay_ref_table_(overlay_ref_table) { } @@ -183,7 +195,13 @@ IdmapResMap::Result IdmapResMap::Lookup(uint32_t target_res_id) const { if (inline_entry != end_inline_entry && (0x00FFFFFFU & dtohl(inline_entry->target_id)) == target_res_id) { - return Result(inline_entry->value); + std::map<ConfigDescription, Res_value> values_map; + for (int i = 0; i < inline_entry->value_count; i++) { + const auto& value = inline_entry_values_[inline_entry->start_value_index + i]; + const auto& config = configurations_[value.config_index]; + values_map[config] = value.value; + } + return Result(values_map); } return {}; } @@ -237,6 +255,8 @@ LoadedIdmap::LoadedIdmap(std::string&& idmap_path, const Idmap_data_header* data_header, const Idmap_target_entry* target_entries, const Idmap_target_entry_inline* target_inline_entries, + const Idmap_target_entry_inline_value* inline_entry_values, + const ConfigDescription* configs, const Idmap_overlay_entry* overlay_entries, std::unique_ptr<ResStringPool>&& string_pool, std::string_view overlay_apk_path, @@ -245,6 +265,8 @@ LoadedIdmap::LoadedIdmap(std::string&& idmap_path, data_header_(data_header), target_entries_(target_entries), target_inline_entries_(target_inline_entries), + inline_entry_values_(inline_entry_values), + configurations_(configs), overlay_entries_(overlay_entries), string_pool_(std::move(string_pool)), idmap_path_(std::move(idmap_path)), @@ -303,6 +325,21 @@ std::unique_ptr<LoadedIdmap> LoadedIdmap::Load(const StringPiece& idmap_path, if (target_inline_entries == nullptr) { return {}; } + + auto target_inline_entry_values = ReadType<Idmap_target_entry_inline_value>( + &data_ptr, &data_size, "target inline values", + dtohl(data_header->target_inline_entry_value_count)); + if (target_inline_entry_values == nullptr) { + return {}; + } + + auto configurations = ReadType<ConfigDescription>( + &data_ptr, &data_size, "configurations", + dtohl(data_header->configuration_count)); + if (configurations == nullptr) { + return {}; + } + auto overlay_entries = ReadType<Idmap_overlay_entry>(&data_ptr, &data_size, "target inline", dtohl(data_header->overlay_entry_count)); if (overlay_entries == nullptr) { @@ -329,8 +366,8 @@ std::unique_ptr<LoadedIdmap> LoadedIdmap::Load(const StringPiece& idmap_path, // Can't use make_unique because LoadedIdmap constructor is private. return std::unique_ptr<LoadedIdmap>( new LoadedIdmap(idmap_path.to_string(), header, data_header, target_entries, - target_inline_entries, overlay_entries, std::move(idmap_string_pool), - *target_path, *overlay_path)); + target_inline_entries, target_inline_entry_values, configurations, + overlay_entries, std::move(idmap_string_pool), *target_path, *overlay_path)); } bool LoadedIdmap::IsUpToDate() const { diff --git a/libs/androidfw/LoadedArsc.cpp b/libs/androidfw/LoadedArsc.cpp index 35b6170fae5b..5b69cca2d747 100644 --- a/libs/androidfw/LoadedArsc.cpp +++ b/libs/androidfw/LoadedArsc.cpp @@ -820,6 +820,13 @@ bool LoadedArsc::LoadTable(const Chunk& chunk, const LoadedIdmap* loaded_idmap, return true; } +bool LoadedArsc::LoadStringPool(const LoadedIdmap* loaded_idmap) { + if (loaded_idmap != nullptr) { + global_string_pool_ = util::make_unique<OverlayStringPool>(loaded_idmap); + } + return true; +} + std::unique_ptr<LoadedArsc> LoadedArsc::Load(incfs::map_ptr<void> data, const size_t length, const LoadedIdmap* loaded_idmap, @@ -855,6 +862,16 @@ std::unique_ptr<LoadedArsc> LoadedArsc::Load(incfs::map_ptr<void> data, return loaded_arsc; } +std::unique_ptr<LoadedArsc> LoadedArsc::Load(const LoadedIdmap* loaded_idmap) { + ATRACE_NAME("LoadedArsc::Load"); + + // Not using make_unique because the constructor is private. + std::unique_ptr<LoadedArsc> loaded_arsc(new LoadedArsc()); + loaded_arsc->LoadStringPool(loaded_idmap); + return loaded_arsc; +} + + std::unique_ptr<LoadedArsc> LoadedArsc::CreateEmpty() { return std::unique_ptr<LoadedArsc>(new LoadedArsc()); } diff --git a/libs/androidfw/LocaleDataTables.cpp b/libs/androidfw/LocaleDataTables.cpp index 2c005fd81de5..b3fb1452919b 100644 --- a/libs/androidfw/LocaleDataTables.cpp +++ b/libs/androidfw/LocaleDataTables.cpp @@ -39,204 +39,205 @@ const char SCRIPT_CODES[][4] = { /* 35 */ {'J', 'p', 'a', 'n'}, /* 36 */ {'K', 'a', 'l', 'i'}, /* 37 */ {'K', 'a', 'n', 'a'}, - /* 38 */ {'K', 'h', 'a', 'r'}, - /* 39 */ {'K', 'h', 'm', 'r'}, - /* 40 */ {'K', 'i', 't', 's'}, - /* 41 */ {'K', 'n', 'd', 'a'}, - /* 42 */ {'K', 'o', 'r', 'e'}, - /* 43 */ {'L', 'a', 'n', 'a'}, - /* 44 */ {'L', 'a', 'o', 'o'}, - /* 45 */ {'L', 'a', 't', 'n'}, - /* 46 */ {'L', 'e', 'p', 'c'}, - /* 47 */ {'L', 'i', 'n', 'a'}, - /* 48 */ {'L', 'i', 's', 'u'}, - /* 49 */ {'L', 'y', 'c', 'i'}, - /* 50 */ {'L', 'y', 'd', 'i'}, - /* 51 */ {'M', 'a', 'n', 'd'}, - /* 52 */ {'M', 'a', 'n', 'i'}, - /* 53 */ {'M', 'e', 'd', 'f'}, - /* 54 */ {'M', 'e', 'r', 'c'}, - /* 55 */ {'M', 'l', 'y', 'm'}, - /* 56 */ {'M', 'o', 'n', 'g'}, - /* 57 */ {'M', 'r', 'o', 'o'}, - /* 58 */ {'M', 'y', 'm', 'r'}, - /* 59 */ {'N', 'a', 'r', 'b'}, - /* 60 */ {'N', 'k', 'o', 'o'}, - /* 61 */ {'N', 's', 'h', 'u'}, - /* 62 */ {'O', 'g', 'a', 'm'}, - /* 63 */ {'O', 'l', 'c', 'k'}, - /* 64 */ {'O', 'r', 'k', 'h'}, - /* 65 */ {'O', 'r', 'y', 'a'}, - /* 66 */ {'O', 's', 'g', 'e'}, - /* 67 */ {'O', 'u', 'g', 'r'}, - /* 68 */ {'P', 'a', 'u', 'c'}, - /* 69 */ {'P', 'h', 'l', 'i'}, - /* 70 */ {'P', 'h', 'n', 'x'}, - /* 71 */ {'P', 'l', 'r', 'd'}, - /* 72 */ {'P', 'r', 't', 'i'}, - /* 73 */ {'R', 'o', 'h', 'g'}, - /* 74 */ {'R', 'u', 'n', 'r'}, - /* 75 */ {'S', 'a', 'm', 'r'}, - /* 76 */ {'S', 'a', 'r', 'b'}, - /* 77 */ {'S', 'a', 'u', 'r'}, - /* 78 */ {'S', 'g', 'n', 'w'}, - /* 79 */ {'S', 'i', 'n', 'h'}, - /* 80 */ {'S', 'o', 'g', 'd'}, - /* 81 */ {'S', 'o', 'r', 'a'}, - /* 82 */ {'S', 'o', 'y', 'o'}, - /* 83 */ {'S', 'y', 'r', 'c'}, - /* 84 */ {'T', 'a', 'l', 'e'}, - /* 85 */ {'T', 'a', 'l', 'u'}, - /* 86 */ {'T', 'a', 'm', 'l'}, - /* 87 */ {'T', 'a', 'n', 'g'}, - /* 88 */ {'T', 'a', 'v', 't'}, - /* 89 */ {'T', 'e', 'l', 'u'}, - /* 90 */ {'T', 'f', 'n', 'g'}, - /* 91 */ {'T', 'h', 'a', 'a'}, - /* 92 */ {'T', 'h', 'a', 'i'}, - /* 93 */ {'T', 'i', 'b', 't'}, - /* 94 */ {'T', 'n', 's', 'a'}, - /* 95 */ {'T', 'o', 't', 'o'}, - /* 96 */ {'U', 'g', 'a', 'r'}, - /* 97 */ {'V', 'a', 'i', 'i'}, - /* 98 */ {'W', 'c', 'h', 'o'}, - /* 99 */ {'X', 'p', 'e', 'o'}, - /* 100 */ {'X', 's', 'u', 'x'}, - /* 101 */ {'Y', 'i', 'i', 'i'}, - /* 102 */ {'~', '~', '~', 'A'}, - /* 103 */ {'~', '~', '~', 'B'}, + /* 38 */ {'K', 'a', 'w', 'i'}, + /* 39 */ {'K', 'h', 'a', 'r'}, + /* 40 */ {'K', 'h', 'm', 'r'}, + /* 41 */ {'K', 'i', 't', 's'}, + /* 42 */ {'K', 'n', 'd', 'a'}, + /* 43 */ {'K', 'o', 'r', 'e'}, + /* 44 */ {'L', 'a', 'n', 'a'}, + /* 45 */ {'L', 'a', 'o', 'o'}, + /* 46 */ {'L', 'a', 't', 'n'}, + /* 47 */ {'L', 'e', 'p', 'c'}, + /* 48 */ {'L', 'i', 'n', 'a'}, + /* 49 */ {'L', 'i', 's', 'u'}, + /* 50 */ {'L', 'y', 'c', 'i'}, + /* 51 */ {'L', 'y', 'd', 'i'}, + /* 52 */ {'M', 'a', 'n', 'd'}, + /* 53 */ {'M', 'a', 'n', 'i'}, + /* 54 */ {'M', 'e', 'd', 'f'}, + /* 55 */ {'M', 'e', 'r', 'c'}, + /* 56 */ {'M', 'l', 'y', 'm'}, + /* 57 */ {'M', 'o', 'n', 'g'}, + /* 58 */ {'M', 'r', 'o', 'o'}, + /* 59 */ {'M', 'y', 'm', 'r'}, + /* 60 */ {'N', 'a', 'r', 'b'}, + /* 61 */ {'N', 'k', 'o', 'o'}, + /* 62 */ {'N', 's', 'h', 'u'}, + /* 63 */ {'O', 'g', 'a', 'm'}, + /* 64 */ {'O', 'l', 'c', 'k'}, + /* 65 */ {'O', 'r', 'k', 'h'}, + /* 66 */ {'O', 'r', 'y', 'a'}, + /* 67 */ {'O', 's', 'g', 'e'}, + /* 68 */ {'O', 'u', 'g', 'r'}, + /* 69 */ {'P', 'a', 'u', 'c'}, + /* 70 */ {'P', 'h', 'l', 'i'}, + /* 71 */ {'P', 'h', 'n', 'x'}, + /* 72 */ {'P', 'l', 'r', 'd'}, + /* 73 */ {'P', 'r', 't', 'i'}, + /* 74 */ {'R', 'o', 'h', 'g'}, + /* 75 */ {'R', 'u', 'n', 'r'}, + /* 76 */ {'S', 'a', 'm', 'r'}, + /* 77 */ {'S', 'a', 'r', 'b'}, + /* 78 */ {'S', 'a', 'u', 'r'}, + /* 79 */ {'S', 'g', 'n', 'w'}, + /* 80 */ {'S', 'i', 'n', 'h'}, + /* 81 */ {'S', 'o', 'g', 'd'}, + /* 82 */ {'S', 'o', 'r', 'a'}, + /* 83 */ {'S', 'o', 'y', 'o'}, + /* 84 */ {'S', 'y', 'r', 'c'}, + /* 85 */ {'T', 'a', 'l', 'e'}, + /* 86 */ {'T', 'a', 'l', 'u'}, + /* 87 */ {'T', 'a', 'm', 'l'}, + /* 88 */ {'T', 'a', 'n', 'g'}, + /* 89 */ {'T', 'a', 'v', 't'}, + /* 90 */ {'T', 'e', 'l', 'u'}, + /* 91 */ {'T', 'f', 'n', 'g'}, + /* 92 */ {'T', 'h', 'a', 'a'}, + /* 93 */ {'T', 'h', 'a', 'i'}, + /* 94 */ {'T', 'i', 'b', 't'}, + /* 95 */ {'T', 'n', 's', 'a'}, + /* 96 */ {'T', 'o', 't', 'o'}, + /* 97 */ {'U', 'g', 'a', 'r'}, + /* 98 */ {'V', 'a', 'i', 'i'}, + /* 99 */ {'W', 'c', 'h', 'o'}, + /* 100 */ {'X', 'p', 'e', 'o'}, + /* 101 */ {'X', 's', 'u', 'x'}, + /* 102 */ {'Y', 'i', 'i', 'i'}, + /* 103 */ {'~', '~', '~', 'A'}, + /* 104 */ {'~', '~', '~', 'B'}, }; const std::unordered_map<uint32_t, uint8_t> LIKELY_SCRIPTS({ - {0x61610000u, 45u}, // aa -> Latn - {0xA0000000u, 45u}, // aai -> Latn - {0xA8000000u, 45u}, // aak -> Latn - {0xD0000000u, 45u}, // aau -> Latn + {0x61610000u, 46u}, // aa -> Latn + {0xA0000000u, 46u}, // aai -> Latn + {0xA8000000u, 46u}, // aak -> Latn + {0xD0000000u, 46u}, // aau -> Latn {0x61620000u, 18u}, // ab -> Cyrl - {0xA0200000u, 45u}, // abi -> Latn + {0xA0200000u, 46u}, // abi -> Latn {0xC0200000u, 18u}, // abq -> Cyrl - {0xC4200000u, 45u}, // abr -> Latn - {0xCC200000u, 45u}, // abt -> Latn - {0xE0200000u, 45u}, // aby -> Latn - {0x8C400000u, 45u}, // acd -> Latn - {0x90400000u, 45u}, // ace -> Latn - {0x9C400000u, 45u}, // ach -> Latn - {0x80600000u, 45u}, // ada -> Latn - {0x90600000u, 45u}, // ade -> Latn - {0xA4600000u, 45u}, // adj -> Latn - {0xBC600000u, 93u}, // adp -> Tibt + {0xC4200000u, 46u}, // abr -> Latn + {0xCC200000u, 46u}, // abt -> Latn + {0xE0200000u, 46u}, // aby -> Latn + {0x8C400000u, 46u}, // acd -> Latn + {0x90400000u, 46u}, // ace -> Latn + {0x9C400000u, 46u}, // ach -> Latn + {0x80600000u, 46u}, // ada -> Latn + {0x90600000u, 46u}, // ade -> Latn + {0xA4600000u, 46u}, // adj -> Latn + {0xBC600000u, 94u}, // adp -> Tibt {0xE0600000u, 18u}, // ady -> Cyrl - {0xE4600000u, 45u}, // adz -> Latn + {0xE4600000u, 46u}, // adz -> Latn {0x61650000u, 5u}, // ae -> Avst {0x84800000u, 2u}, // aeb -> Arab - {0xE0800000u, 45u}, // aey -> Latn - {0x61660000u, 45u}, // af -> Latn - {0x88C00000u, 45u}, // agc -> Latn - {0x8CC00000u, 45u}, // agd -> Latn - {0x98C00000u, 45u}, // agg -> Latn - {0xB0C00000u, 45u}, // agm -> Latn - {0xB8C00000u, 45u}, // ago -> Latn - {0xC0C00000u, 45u}, // agq -> Latn - {0x80E00000u, 45u}, // aha -> Latn - {0xACE00000u, 45u}, // ahl -> Latn + {0xE0800000u, 46u}, // aey -> Latn + {0x61660000u, 46u}, // af -> Latn + {0x88C00000u, 46u}, // agc -> Latn + {0x8CC00000u, 46u}, // agd -> Latn + {0x98C00000u, 46u}, // agg -> Latn + {0xB0C00000u, 46u}, // agm -> Latn + {0xB8C00000u, 46u}, // ago -> Latn + {0xC0C00000u, 46u}, // agq -> Latn + {0x80E00000u, 46u}, // aha -> Latn + {0xACE00000u, 46u}, // ahl -> Latn {0xB8E00000u, 1u}, // aho -> Ahom - {0x99200000u, 45u}, // ajg -> Latn - {0x616B0000u, 45u}, // ak -> Latn - {0xA9400000u, 100u}, // akk -> Xsux - {0x81600000u, 45u}, // ala -> Latn - {0xA1600000u, 45u}, // ali -> Latn - {0xB5600000u, 45u}, // aln -> Latn + {0x99200000u, 46u}, // ajg -> Latn + {0x616B0000u, 46u}, // ak -> Latn + {0xA9400000u, 101u}, // akk -> Xsux + {0x81600000u, 46u}, // ala -> Latn + {0xA1600000u, 46u}, // ali -> Latn + {0xB5600000u, 46u}, // aln -> Latn {0xCD600000u, 18u}, // alt -> Cyrl {0x616D0000u, 21u}, // am -> Ethi - {0xB1800000u, 45u}, // amm -> Latn - {0xB5800000u, 45u}, // amn -> Latn - {0xB9800000u, 45u}, // amo -> Latn - {0xBD800000u, 45u}, // amp -> Latn - {0x616E0000u, 45u}, // an -> Latn - {0x89A00000u, 45u}, // anc -> Latn - {0xA9A00000u, 45u}, // ank -> Latn - {0xB5A00000u, 45u}, // ann -> Latn - {0xE1A00000u, 45u}, // any -> Latn - {0xA5C00000u, 45u}, // aoj -> Latn - {0xB1C00000u, 45u}, // aom -> Latn - {0xE5C00000u, 45u}, // aoz -> Latn + {0xB1800000u, 46u}, // amm -> Latn + {0xB5800000u, 46u}, // amn -> Latn + {0xB9800000u, 46u}, // amo -> Latn + {0xBD800000u, 46u}, // amp -> Latn + {0x616E0000u, 46u}, // an -> Latn + {0x89A00000u, 46u}, // anc -> Latn + {0xA9A00000u, 46u}, // ank -> Latn + {0xB5A00000u, 46u}, // ann -> Latn + {0xE1A00000u, 46u}, // any -> Latn + {0xA5C00000u, 46u}, // aoj -> Latn + {0xB1C00000u, 46u}, // aom -> Latn + {0xE5C00000u, 46u}, // aoz -> Latn {0x89E00000u, 2u}, // apc -> Arab {0x8DE00000u, 2u}, // apd -> Arab - {0x91E00000u, 45u}, // ape -> Latn - {0xC5E00000u, 45u}, // apr -> Latn - {0xC9E00000u, 45u}, // aps -> Latn - {0xE5E00000u, 45u}, // apz -> Latn + {0x91E00000u, 46u}, // ape -> Latn + {0xC5E00000u, 46u}, // apr -> Latn + {0xC9E00000u, 46u}, // aps -> Latn + {0xE5E00000u, 46u}, // apz -> Latn {0x61720000u, 2u}, // ar -> Arab - {0x61725842u, 103u}, // ar-XB -> ~~~B + {0x61725842u, 104u}, // ar-XB -> ~~~B {0x8A200000u, 3u}, // arc -> Armi - {0x9E200000u, 45u}, // arh -> Latn - {0xB6200000u, 45u}, // arn -> Latn - {0xBA200000u, 45u}, // aro -> Latn + {0x9E200000u, 46u}, // arh -> Latn + {0xB6200000u, 46u}, // arn -> Latn + {0xBA200000u, 46u}, // aro -> Latn {0xC2200000u, 2u}, // arq -> Arab {0xCA200000u, 2u}, // ars -> Arab {0xE2200000u, 2u}, // ary -> Arab {0xE6200000u, 2u}, // arz -> Arab {0x61730000u, 8u}, // as -> Beng - {0x82400000u, 45u}, // asa -> Latn - {0x92400000u, 78u}, // ase -> Sgnw - {0x9A400000u, 45u}, // asg -> Latn - {0xBA400000u, 45u}, // aso -> Latn - {0xCE400000u, 45u}, // ast -> Latn - {0x82600000u, 45u}, // ata -> Latn - {0x9A600000u, 45u}, // atg -> Latn - {0xA6600000u, 45u}, // atj -> Latn - {0xE2800000u, 45u}, // auy -> Latn + {0x82400000u, 46u}, // asa -> Latn + {0x92400000u, 79u}, // ase -> Sgnw + {0x9A400000u, 46u}, // asg -> Latn + {0xBA400000u, 46u}, // aso -> Latn + {0xCE400000u, 46u}, // ast -> Latn + {0x82600000u, 46u}, // ata -> Latn + {0x9A600000u, 46u}, // atg -> Latn + {0xA6600000u, 46u}, // atj -> Latn + {0xE2800000u, 46u}, // auy -> Latn {0x61760000u, 18u}, // av -> Cyrl {0xAEA00000u, 2u}, // avl -> Arab - {0xB6A00000u, 45u}, // avn -> Latn - {0xCEA00000u, 45u}, // avt -> Latn - {0xD2A00000u, 45u}, // avu -> Latn + {0xB6A00000u, 46u}, // avn -> Latn + {0xCEA00000u, 46u}, // avt -> Latn + {0xD2A00000u, 46u}, // avu -> Latn {0x82C00000u, 19u}, // awa -> Deva - {0x86C00000u, 45u}, // awb -> Latn - {0xBAC00000u, 45u}, // awo -> Latn - {0xDEC00000u, 45u}, // awx -> Latn - {0x61790000u, 45u}, // ay -> Latn - {0x87000000u, 45u}, // ayb -> Latn - {0x617A0000u, 45u}, // az -> Latn + {0x86C00000u, 46u}, // awb -> Latn + {0xBAC00000u, 46u}, // awo -> Latn + {0xDEC00000u, 46u}, // awx -> Latn + {0x61790000u, 46u}, // ay -> Latn + {0x87000000u, 46u}, // ayb -> Latn + {0x617A0000u, 46u}, // az -> Latn {0x617A4951u, 2u}, // az-IQ -> Arab {0x617A4952u, 2u}, // az-IR -> Arab {0x617A5255u, 18u}, // az-RU -> Cyrl {0x62610000u, 18u}, // ba -> Cyrl {0xAC010000u, 2u}, // bal -> Arab - {0xB4010000u, 45u}, // ban -> Latn + {0xB4010000u, 46u}, // ban -> Latn {0xBC010000u, 19u}, // bap -> Deva - {0xC4010000u, 45u}, // bar -> Latn - {0xC8010000u, 45u}, // bas -> Latn - {0xD4010000u, 45u}, // bav -> Latn + {0xC4010000u, 46u}, // bar -> Latn + {0xC8010000u, 46u}, // bas -> Latn + {0xD4010000u, 46u}, // bav -> Latn {0xDC010000u, 6u}, // bax -> Bamu - {0x80210000u, 45u}, // bba -> Latn - {0x84210000u, 45u}, // bbb -> Latn - {0x88210000u, 45u}, // bbc -> Latn - {0x8C210000u, 45u}, // bbd -> Latn - {0xA4210000u, 45u}, // bbj -> Latn - {0xBC210000u, 45u}, // bbp -> Latn - {0xC4210000u, 45u}, // bbr -> Latn - {0x94410000u, 45u}, // bcf -> Latn - {0x9C410000u, 45u}, // bch -> Latn - {0xA0410000u, 45u}, // bci -> Latn - {0xB0410000u, 45u}, // bcm -> Latn - {0xB4410000u, 45u}, // bcn -> Latn - {0xB8410000u, 45u}, // bco -> Latn + {0x80210000u, 46u}, // bba -> Latn + {0x84210000u, 46u}, // bbb -> Latn + {0x88210000u, 46u}, // bbc -> Latn + {0x8C210000u, 46u}, // bbd -> Latn + {0xA4210000u, 46u}, // bbj -> Latn + {0xBC210000u, 46u}, // bbp -> Latn + {0xC4210000u, 46u}, // bbr -> Latn + {0x94410000u, 46u}, // bcf -> Latn + {0x9C410000u, 46u}, // bch -> Latn + {0xA0410000u, 46u}, // bci -> Latn + {0xB0410000u, 46u}, // bcm -> Latn + {0xB4410000u, 46u}, // bcn -> Latn + {0xB8410000u, 46u}, // bco -> Latn {0xC0410000u, 21u}, // bcq -> Ethi - {0xD0410000u, 45u}, // bcu -> Latn - {0x8C610000u, 45u}, // bdd -> Latn + {0xD0410000u, 46u}, // bcu -> Latn + {0x8C610000u, 46u}, // bdd -> Latn {0x62650000u, 18u}, // be -> Cyrl - {0x94810000u, 45u}, // bef -> Latn - {0x9C810000u, 45u}, // beh -> Latn + {0x94810000u, 46u}, // bef -> Latn + {0x9C810000u, 46u}, // beh -> Latn {0xA4810000u, 2u}, // bej -> Arab - {0xB0810000u, 45u}, // bem -> Latn - {0xCC810000u, 45u}, // bet -> Latn - {0xD8810000u, 45u}, // bew -> Latn - {0xDC810000u, 45u}, // bex -> Latn - {0xE4810000u, 45u}, // bez -> Latn - {0x8CA10000u, 45u}, // bfd -> Latn - {0xC0A10000u, 86u}, // bfq -> Taml + {0xB0810000u, 46u}, // bem -> Latn + {0xCC810000u, 46u}, // bet -> Latn + {0xD8810000u, 46u}, // bew -> Latn + {0xDC810000u, 46u}, // bex -> Latn + {0xE4810000u, 46u}, // bez -> Latn + {0x8CA10000u, 46u}, // bfd -> Latn + {0xC0A10000u, 87u}, // bfq -> Taml {0xCCA10000u, 2u}, // bft -> Arab {0xE0A10000u, 19u}, // bfy -> Deva {0x62670000u, 18u}, // bg -> Cyrl @@ -244,1239 +245,1248 @@ const std::unordered_map<uint32_t, uint8_t> LIKELY_SCRIPTS({ {0xB4C10000u, 2u}, // bgn -> Arab {0xDCC10000u, 26u}, // bgx -> Grek {0x84E10000u, 19u}, // bhb -> Deva - {0x98E10000u, 45u}, // bhg -> Latn + {0x98E10000u, 46u}, // bhg -> Latn {0xA0E10000u, 19u}, // bhi -> Deva - {0xACE10000u, 45u}, // bhl -> Latn + {0xACE10000u, 46u}, // bhl -> Latn {0xB8E10000u, 19u}, // bho -> Deva - {0xE0E10000u, 45u}, // bhy -> Latn - {0x62690000u, 45u}, // bi -> Latn - {0x85010000u, 45u}, // bib -> Latn - {0x99010000u, 45u}, // big -> Latn - {0xA9010000u, 45u}, // bik -> Latn - {0xB1010000u, 45u}, // bim -> Latn - {0xB5010000u, 45u}, // bin -> Latn - {0xB9010000u, 45u}, // bio -> Latn - {0xC1010000u, 45u}, // biq -> Latn - {0x9D210000u, 45u}, // bjh -> Latn + {0xE0E10000u, 46u}, // bhy -> Latn + {0x62690000u, 46u}, // bi -> Latn + {0x85010000u, 46u}, // bib -> Latn + {0x99010000u, 46u}, // big -> Latn + {0xA9010000u, 46u}, // bik -> Latn + {0xB1010000u, 46u}, // bim -> Latn + {0xB5010000u, 46u}, // bin -> Latn + {0xB9010000u, 46u}, // bio -> Latn + {0xC1010000u, 46u}, // biq -> Latn + {0x9D210000u, 46u}, // bjh -> Latn {0xA1210000u, 21u}, // bji -> Ethi {0xA5210000u, 19u}, // bjj -> Deva - {0xB5210000u, 45u}, // bjn -> Latn - {0xB9210000u, 45u}, // bjo -> Latn - {0xC5210000u, 45u}, // bjr -> Latn - {0xCD210000u, 45u}, // bjt -> Latn - {0xE5210000u, 45u}, // bjz -> Latn - {0x89410000u, 45u}, // bkc -> Latn - {0xB1410000u, 45u}, // bkm -> Latn - {0xC1410000u, 45u}, // bkq -> Latn - {0xD1410000u, 45u}, // bku -> Latn - {0xD5410000u, 45u}, // bkv -> Latn - {0x99610000u, 45u}, // blg -> Latn - {0xCD610000u, 88u}, // blt -> Tavt - {0x626D0000u, 45u}, // bm -> Latn - {0x9D810000u, 45u}, // bmh -> Latn - {0xA9810000u, 45u}, // bmk -> Latn - {0xC1810000u, 45u}, // bmq -> Latn - {0xD1810000u, 45u}, // bmu -> Latn + {0xB5210000u, 46u}, // bjn -> Latn + {0xB9210000u, 46u}, // bjo -> Latn + {0xC5210000u, 46u}, // bjr -> Latn + {0xCD210000u, 46u}, // bjt -> Latn + {0xE5210000u, 46u}, // bjz -> Latn + {0x89410000u, 46u}, // bkc -> Latn + {0xB1410000u, 46u}, // bkm -> Latn + {0xC1410000u, 46u}, // bkq -> Latn + {0xD1410000u, 46u}, // bku -> Latn + {0xD5410000u, 46u}, // bkv -> Latn + {0x81610000u, 46u}, // bla -> Latn + {0x99610000u, 46u}, // blg -> Latn + {0xCD610000u, 89u}, // blt -> Tavt + {0x626D0000u, 46u}, // bm -> Latn + {0x9D810000u, 46u}, // bmh -> Latn + {0xA9810000u, 46u}, // bmk -> Latn + {0xC1810000u, 46u}, // bmq -> Latn + {0xD1810000u, 46u}, // bmu -> Latn {0x626E0000u, 8u}, // bn -> Beng - {0x99A10000u, 45u}, // bng -> Latn - {0xB1A10000u, 45u}, // bnm -> Latn - {0xBDA10000u, 45u}, // bnp -> Latn - {0x626F0000u, 93u}, // bo -> Tibt - {0xA5C10000u, 45u}, // boj -> Latn - {0xB1C10000u, 45u}, // bom -> Latn - {0xB5C10000u, 45u}, // bon -> Latn + {0x99A10000u, 46u}, // bng -> Latn + {0xB1A10000u, 46u}, // bnm -> Latn + {0xBDA10000u, 46u}, // bnp -> Latn + {0x626F0000u, 94u}, // bo -> Tibt + {0xA5C10000u, 46u}, // boj -> Latn + {0xB1C10000u, 46u}, // bom -> Latn + {0xB5C10000u, 46u}, // bon -> Latn {0xE1E10000u, 8u}, // bpy -> Beng - {0x8A010000u, 45u}, // bqc -> Latn + {0x8A010000u, 46u}, // bqc -> Latn {0xA2010000u, 2u}, // bqi -> Arab - {0xBE010000u, 45u}, // bqp -> Latn - {0xD6010000u, 45u}, // bqv -> Latn - {0x62720000u, 45u}, // br -> Latn + {0xBE010000u, 46u}, // bqp -> Latn + {0xD6010000u, 46u}, // bqv -> Latn + {0x62720000u, 46u}, // br -> Latn {0x82210000u, 19u}, // bra -> Deva {0x9E210000u, 2u}, // brh -> Arab {0xDE210000u, 19u}, // brx -> Deva - {0xE6210000u, 45u}, // brz -> Latn - {0x62730000u, 45u}, // bs -> Latn - {0xA6410000u, 45u}, // bsj -> Latn + {0xE6210000u, 46u}, // brz -> Latn + {0x62730000u, 46u}, // bs -> Latn + {0xA6410000u, 46u}, // bsj -> Latn {0xC2410000u, 7u}, // bsq -> Bass - {0xCA410000u, 45u}, // bss -> Latn + {0xCA410000u, 46u}, // bss -> Latn {0xCE410000u, 21u}, // bst -> Ethi - {0xBA610000u, 45u}, // bto -> Latn - {0xCE610000u, 45u}, // btt -> Latn + {0xBA610000u, 46u}, // bto -> Latn + {0xCE610000u, 46u}, // btt -> Latn {0xD6610000u, 19u}, // btv -> Deva {0x82810000u, 18u}, // bua -> Cyrl - {0x8A810000u, 45u}, // buc -> Latn - {0x8E810000u, 45u}, // bud -> Latn - {0x9A810000u, 45u}, // bug -> Latn - {0xAA810000u, 45u}, // buk -> Latn - {0xB2810000u, 45u}, // bum -> Latn - {0xBA810000u, 45u}, // buo -> Latn - {0xCA810000u, 45u}, // bus -> Latn - {0xD2810000u, 45u}, // buu -> Latn - {0x86A10000u, 45u}, // bvb -> Latn - {0x8EC10000u, 45u}, // bwd -> Latn - {0xC6C10000u, 45u}, // bwr -> Latn - {0x9EE10000u, 45u}, // bxh -> Latn - {0x93010000u, 45u}, // bye -> Latn + {0x8A810000u, 46u}, // buc -> Latn + {0x8E810000u, 46u}, // bud -> Latn + {0x9A810000u, 46u}, // bug -> Latn + {0xAA810000u, 46u}, // buk -> Latn + {0xB2810000u, 46u}, // bum -> Latn + {0xBA810000u, 46u}, // buo -> Latn + {0xCA810000u, 46u}, // bus -> Latn + {0xD2810000u, 46u}, // buu -> Latn + {0x86A10000u, 46u}, // bvb -> Latn + {0x8EC10000u, 46u}, // bwd -> Latn + {0xC6C10000u, 46u}, // bwr -> Latn + {0x9EE10000u, 46u}, // bxh -> Latn + {0x93010000u, 46u}, // bye -> Latn {0xB7010000u, 21u}, // byn -> Ethi - {0xC7010000u, 45u}, // byr -> Latn - {0xCB010000u, 45u}, // bys -> Latn - {0xD7010000u, 45u}, // byv -> Latn - {0xDF010000u, 45u}, // byx -> Latn - {0x83210000u, 45u}, // bza -> Latn - {0x93210000u, 45u}, // bze -> Latn - {0x97210000u, 45u}, // bzf -> Latn - {0x9F210000u, 45u}, // bzh -> Latn - {0xDB210000u, 45u}, // bzw -> Latn - {0x63610000u, 45u}, // ca -> Latn - {0x8C020000u, 45u}, // cad -> Latn - {0xB4020000u, 45u}, // can -> Latn - {0xA4220000u, 45u}, // cbj -> Latn - {0x9C420000u, 45u}, // cch -> Latn + {0xC7010000u, 46u}, // byr -> Latn + {0xCB010000u, 46u}, // bys -> Latn + {0xD7010000u, 46u}, // byv -> Latn + {0xDF010000u, 46u}, // byx -> Latn + {0x83210000u, 46u}, // bza -> Latn + {0x93210000u, 46u}, // bze -> Latn + {0x97210000u, 46u}, // bzf -> Latn + {0x9F210000u, 46u}, // bzh -> Latn + {0xDB210000u, 46u}, // bzw -> Latn + {0x63610000u, 46u}, // ca -> Latn + {0x8C020000u, 46u}, // cad -> Latn + {0xB4020000u, 46u}, // can -> Latn + {0xA4220000u, 46u}, // cbj -> Latn + {0x9C420000u, 46u}, // cch -> Latn {0xBC420000u, 10u}, // ccp -> Cakm {0x63650000u, 18u}, // ce -> Cyrl - {0x84820000u, 45u}, // ceb -> Latn - {0x80A20000u, 45u}, // cfa -> Latn - {0x98C20000u, 45u}, // cgg -> Latn - {0x63680000u, 45u}, // ch -> Latn - {0xA8E20000u, 45u}, // chk -> Latn + {0x84820000u, 46u}, // ceb -> Latn + {0x80A20000u, 46u}, // cfa -> Latn + {0x98C20000u, 46u}, // cgg -> Latn + {0x63680000u, 46u}, // ch -> Latn + {0xA8E20000u, 46u}, // chk -> Latn {0xB0E20000u, 18u}, // chm -> Cyrl - {0xB8E20000u, 45u}, // cho -> Latn - {0xBCE20000u, 45u}, // chp -> Latn + {0xB8E20000u, 46u}, // cho -> Latn + {0xBCE20000u, 46u}, // chp -> Latn {0xC4E20000u, 14u}, // chr -> Cher - {0x89020000u, 45u}, // cic -> Latn + {0x89020000u, 46u}, // cic -> Latn {0x81220000u, 2u}, // cja -> Arab {0xB1220000u, 13u}, // cjm -> Cham - {0xD5220000u, 45u}, // cjv -> Latn + {0xD5220000u, 46u}, // cjv -> Latn {0x85420000u, 2u}, // ckb -> Arab - {0xAD420000u, 45u}, // ckl -> Latn - {0xB9420000u, 45u}, // cko -> Latn - {0xE1420000u, 45u}, // cky -> Latn - {0x81620000u, 45u}, // cla -> Latn - {0x91820000u, 45u}, // cme -> Latn - {0x99820000u, 82u}, // cmg -> Soyo - {0x636F0000u, 45u}, // co -> Latn + {0xAD420000u, 46u}, // ckl -> Latn + {0xB9420000u, 46u}, // cko -> Latn + {0xE1420000u, 46u}, // cky -> Latn + {0x81620000u, 46u}, // cla -> Latn + {0x89620000u, 46u}, // clc -> Latn + {0x91820000u, 46u}, // cme -> Latn + {0x99820000u, 83u}, // cmg -> Soyo + {0x636F0000u, 46u}, // co -> Latn {0xBDC20000u, 16u}, // cop -> Copt - {0xC9E20000u, 45u}, // cps -> Latn + {0xC9E20000u, 46u}, // cps -> Latn {0x63720000u, 11u}, // cr -> Cans + {0x9A220000u, 46u}, // crg -> Latn {0x9E220000u, 18u}, // crh -> Cyrl - {0xA6220000u, 11u}, // crj -> Cans {0xAA220000u, 11u}, // crk -> Cans {0xAE220000u, 11u}, // crl -> Cans - {0xB2220000u, 11u}, // crm -> Cans - {0xCA220000u, 45u}, // crs -> Latn - {0x63730000u, 45u}, // cs -> Latn - {0x86420000u, 45u}, // csb -> Latn + {0xCA220000u, 46u}, // crs -> Latn + {0x63730000u, 46u}, // cs -> Latn + {0x86420000u, 46u}, // csb -> Latn {0xDA420000u, 11u}, // csw -> Cans - {0x8E620000u, 68u}, // ctd -> Pauc + {0x8E620000u, 69u}, // ctd -> Pauc {0x63750000u, 18u}, // cu -> Cyrl {0x63760000u, 18u}, // cv -> Cyrl - {0x63790000u, 45u}, // cy -> Latn - {0x64610000u, 45u}, // da -> Latn - {0x8C030000u, 45u}, // dad -> Latn - {0x94030000u, 45u}, // daf -> Latn - {0x98030000u, 45u}, // dag -> Latn - {0x9C030000u, 45u}, // dah -> Latn - {0xA8030000u, 45u}, // dak -> Latn + {0x63790000u, 46u}, // cy -> Latn + {0x64610000u, 46u}, // da -> Latn + {0x8C030000u, 46u}, // dad -> Latn + {0x94030000u, 46u}, // daf -> Latn + {0x98030000u, 46u}, // dag -> Latn + {0x9C030000u, 46u}, // dah -> Latn + {0xA8030000u, 46u}, // dak -> Latn {0xC4030000u, 18u}, // dar -> Cyrl - {0xD4030000u, 45u}, // dav -> Latn - {0x8C230000u, 45u}, // dbd -> Latn - {0xC0230000u, 45u}, // dbq -> Latn + {0xD4030000u, 46u}, // dav -> Latn + {0x8C230000u, 46u}, // dbd -> Latn + {0xC0230000u, 46u}, // dbq -> Latn {0x88430000u, 2u}, // dcc -> Arab - {0xB4630000u, 45u}, // ddn -> Latn - {0x64650000u, 45u}, // de -> Latn - {0x8C830000u, 45u}, // ded -> Latn - {0xB4830000u, 45u}, // den -> Latn - {0x80C30000u, 45u}, // dga -> Latn - {0x9CC30000u, 45u}, // dgh -> Latn - {0xA0C30000u, 45u}, // dgi -> Latn + {0xB4630000u, 46u}, // ddn -> Latn + {0x64650000u, 46u}, // de -> Latn + {0x8C830000u, 46u}, // ded -> Latn + {0xB4830000u, 46u}, // den -> Latn + {0x80C30000u, 46u}, // dga -> Latn + {0x9CC30000u, 46u}, // dgh -> Latn + {0xA0C30000u, 46u}, // dgi -> Latn {0xACC30000u, 2u}, // dgl -> Arab - {0xC4C30000u, 45u}, // dgr -> Latn - {0xE4C30000u, 45u}, // dgz -> Latn - {0x81030000u, 45u}, // dia -> Latn - {0x91230000u, 45u}, // dje -> Latn - {0x95830000u, 53u}, // dmf -> Medf - {0xA5A30000u, 45u}, // dnj -> Latn - {0x85C30000u, 45u}, // dob -> Latn + {0xC4C30000u, 46u}, // dgr -> Latn + {0xE4C30000u, 46u}, // dgz -> Latn + {0x81030000u, 46u}, // dia -> Latn + {0x91230000u, 46u}, // dje -> Latn + {0x95830000u, 54u}, // dmf -> Medf + {0xA5A30000u, 46u}, // dnj -> Latn + {0x85C30000u, 46u}, // dob -> Latn {0xA1C30000u, 19u}, // doi -> Deva - {0xBDC30000u, 45u}, // dop -> Latn - {0xD9C30000u, 45u}, // dow -> Latn - {0x9E230000u, 56u}, // drh -> Mong - {0xA2230000u, 45u}, // dri -> Latn + {0xBDC30000u, 46u}, // dop -> Latn + {0xD9C30000u, 46u}, // dow -> Latn + {0x9E230000u, 57u}, // drh -> Mong + {0xA2230000u, 46u}, // dri -> Latn {0xCA230000u, 21u}, // drs -> Ethi - {0x86430000u, 45u}, // dsb -> Latn - {0xB2630000u, 45u}, // dtm -> Latn - {0xBE630000u, 45u}, // dtp -> Latn - {0xCA630000u, 45u}, // dts -> Latn + {0x86430000u, 46u}, // dsb -> Latn + {0xB2630000u, 46u}, // dtm -> Latn + {0xBE630000u, 46u}, // dtp -> Latn + {0xCA630000u, 46u}, // dts -> Latn {0xE2630000u, 19u}, // dty -> Deva - {0x82830000u, 45u}, // dua -> Latn - {0x8A830000u, 45u}, // duc -> Latn - {0x8E830000u, 45u}, // dud -> Latn - {0x9A830000u, 45u}, // dug -> Latn - {0x64760000u, 91u}, // dv -> Thaa - {0x82A30000u, 45u}, // dva -> Latn - {0xDAC30000u, 45u}, // dww -> Latn - {0xBB030000u, 45u}, // dyo -> Latn - {0xD3030000u, 45u}, // dyu -> Latn - {0x647A0000u, 93u}, // dz -> Tibt - {0x9B230000u, 45u}, // dzg -> Latn - {0xD0240000u, 45u}, // ebu -> Latn - {0x65650000u, 45u}, // ee -> Latn - {0xA0A40000u, 45u}, // efi -> Latn - {0xACC40000u, 45u}, // egl -> Latn + {0x82830000u, 46u}, // dua -> Latn + {0x8A830000u, 46u}, // duc -> Latn + {0x8E830000u, 46u}, // dud -> Latn + {0x9A830000u, 46u}, // dug -> Latn + {0x64760000u, 92u}, // dv -> Thaa + {0x82A30000u, 46u}, // dva -> Latn + {0xDAC30000u, 46u}, // dww -> Latn + {0xBB030000u, 46u}, // dyo -> Latn + {0xD3030000u, 46u}, // dyu -> Latn + {0x647A0000u, 94u}, // dz -> Tibt + {0x9B230000u, 46u}, // dzg -> Latn + {0xD0240000u, 46u}, // ebu -> Latn + {0x65650000u, 46u}, // ee -> Latn + {0xA0A40000u, 46u}, // efi -> Latn + {0xACC40000u, 46u}, // egl -> Latn {0xE0C40000u, 20u}, // egy -> Egyp - {0x81440000u, 45u}, // eka -> Latn + {0x81440000u, 46u}, // eka -> Latn {0xE1440000u, 36u}, // eky -> Kali {0x656C0000u, 26u}, // el -> Grek - {0x81840000u, 45u}, // ema -> Latn - {0xA1840000u, 45u}, // emi -> Latn - {0x656E0000u, 45u}, // en -> Latn - {0x656E5841u, 102u}, // en-XA -> ~~~A - {0xB5A40000u, 45u}, // enn -> Latn - {0xC1A40000u, 45u}, // enq -> Latn - {0x656F0000u, 45u}, // eo -> Latn - {0xA2240000u, 45u}, // eri -> Latn - {0x65730000u, 45u}, // es -> Latn + {0x81840000u, 46u}, // ema -> Latn + {0xA1840000u, 46u}, // emi -> Latn + {0x656E0000u, 46u}, // en -> Latn + {0x656E5841u, 103u}, // en-XA -> ~~~A + {0xB5A40000u, 46u}, // enn -> Latn + {0xC1A40000u, 46u}, // enq -> Latn + {0x656F0000u, 46u}, // eo -> Latn + {0xA2240000u, 46u}, // eri -> Latn + {0x65730000u, 46u}, // es -> Latn {0x9A440000u, 24u}, // esg -> Gonm - {0xD2440000u, 45u}, // esu -> Latn - {0x65740000u, 45u}, // et -> Latn - {0xC6640000u, 45u}, // etr -> Latn + {0xD2440000u, 46u}, // esu -> Latn + {0x65740000u, 46u}, // et -> Latn + {0xC6640000u, 46u}, // etr -> Latn {0xCE640000u, 34u}, // ett -> Ital - {0xD2640000u, 45u}, // etu -> Latn - {0xDE640000u, 45u}, // etx -> Latn - {0x65750000u, 45u}, // eu -> Latn - {0xBAC40000u, 45u}, // ewo -> Latn - {0xCEE40000u, 45u}, // ext -> Latn - {0x83240000u, 45u}, // eza -> Latn + {0xD2640000u, 46u}, // etu -> Latn + {0xDE640000u, 46u}, // etx -> Latn + {0x65750000u, 46u}, // eu -> Latn + {0xBAC40000u, 46u}, // ewo -> Latn + {0xCEE40000u, 46u}, // ext -> Latn + {0x83240000u, 46u}, // eza -> Latn {0x66610000u, 2u}, // fa -> Arab - {0x80050000u, 45u}, // faa -> Latn - {0x84050000u, 45u}, // fab -> Latn - {0x98050000u, 45u}, // fag -> Latn - {0xA0050000u, 45u}, // fai -> Latn - {0xB4050000u, 45u}, // fan -> Latn - {0x66660000u, 45u}, // ff -> Latn - {0xA0A50000u, 45u}, // ffi -> Latn - {0xB0A50000u, 45u}, // ffm -> Latn - {0x66690000u, 45u}, // fi -> Latn + {0x80050000u, 46u}, // faa -> Latn + {0x84050000u, 46u}, // fab -> Latn + {0x98050000u, 46u}, // fag -> Latn + {0xA0050000u, 46u}, // fai -> Latn + {0xB4050000u, 46u}, // fan -> Latn + {0x66660000u, 46u}, // ff -> Latn + {0xA0A50000u, 46u}, // ffi -> Latn + {0xB0A50000u, 46u}, // ffm -> Latn + {0x66690000u, 46u}, // fi -> Latn {0x81050000u, 2u}, // fia -> Arab - {0xAD050000u, 45u}, // fil -> Latn - {0xCD050000u, 45u}, // fit -> Latn - {0x666A0000u, 45u}, // fj -> Latn - {0xC5650000u, 45u}, // flr -> Latn - {0xBD850000u, 45u}, // fmp -> Latn - {0x666F0000u, 45u}, // fo -> Latn - {0x8DC50000u, 45u}, // fod -> Latn - {0xB5C50000u, 45u}, // fon -> Latn - {0xC5C50000u, 45u}, // for -> Latn - {0x91E50000u, 45u}, // fpe -> Latn - {0xCA050000u, 45u}, // fqs -> Latn - {0x66720000u, 45u}, // fr -> Latn - {0x8A250000u, 45u}, // frc -> Latn - {0xBE250000u, 45u}, // frp -> Latn - {0xC6250000u, 45u}, // frr -> Latn - {0xCA250000u, 45u}, // frs -> Latn + {0xAD050000u, 46u}, // fil -> Latn + {0xCD050000u, 46u}, // fit -> Latn + {0x666A0000u, 46u}, // fj -> Latn + {0xC5650000u, 46u}, // flr -> Latn + {0xBD850000u, 46u}, // fmp -> Latn + {0x666F0000u, 46u}, // fo -> Latn + {0x8DC50000u, 46u}, // fod -> Latn + {0xB5C50000u, 46u}, // fon -> Latn + {0xC5C50000u, 46u}, // for -> Latn + {0x91E50000u, 46u}, // fpe -> Latn + {0xCA050000u, 46u}, // fqs -> Latn + {0x66720000u, 46u}, // fr -> Latn + {0x8A250000u, 46u}, // frc -> Latn + {0xBE250000u, 46u}, // frp -> Latn + {0xC6250000u, 46u}, // frr -> Latn + {0xCA250000u, 46u}, // frs -> Latn {0x86850000u, 2u}, // fub -> Arab - {0x8E850000u, 45u}, // fud -> Latn - {0x92850000u, 45u}, // fue -> Latn - {0x96850000u, 45u}, // fuf -> Latn - {0x9E850000u, 45u}, // fuh -> Latn - {0xC2850000u, 45u}, // fuq -> Latn - {0xC6850000u, 45u}, // fur -> Latn - {0xD6850000u, 45u}, // fuv -> Latn - {0xE2850000u, 45u}, // fuy -> Latn - {0xC6A50000u, 45u}, // fvr -> Latn - {0x66790000u, 45u}, // fy -> Latn - {0x67610000u, 45u}, // ga -> Latn - {0x80060000u, 45u}, // gaa -> Latn - {0x94060000u, 45u}, // gaf -> Latn - {0x98060000u, 45u}, // gag -> Latn - {0x9C060000u, 45u}, // gah -> Latn - {0xA4060000u, 45u}, // gaj -> Latn - {0xB0060000u, 45u}, // gam -> Latn + {0x8E850000u, 46u}, // fud -> Latn + {0x92850000u, 46u}, // fue -> Latn + {0x96850000u, 46u}, // fuf -> Latn + {0x9E850000u, 46u}, // fuh -> Latn + {0xC2850000u, 46u}, // fuq -> Latn + {0xC6850000u, 46u}, // fur -> Latn + {0xD6850000u, 46u}, // fuv -> Latn + {0xE2850000u, 46u}, // fuy -> Latn + {0xC6A50000u, 46u}, // fvr -> Latn + {0x66790000u, 46u}, // fy -> Latn + {0x67610000u, 46u}, // ga -> Latn + {0x80060000u, 46u}, // gaa -> Latn + {0x94060000u, 46u}, // gaf -> Latn + {0x98060000u, 46u}, // gag -> Latn + {0x9C060000u, 46u}, // gah -> Latn + {0xA4060000u, 46u}, // gaj -> Latn + {0xB0060000u, 46u}, // gam -> Latn {0xB4060000u, 29u}, // gan -> Hans - {0xD8060000u, 45u}, // gaw -> Latn - {0xE0060000u, 45u}, // gay -> Latn - {0x80260000u, 45u}, // gba -> Latn - {0x94260000u, 45u}, // gbf -> Latn + {0xD8060000u, 46u}, // gaw -> Latn + {0xE0060000u, 46u}, // gay -> Latn + {0x80260000u, 46u}, // gba -> Latn + {0x94260000u, 46u}, // gbf -> Latn {0xB0260000u, 19u}, // gbm -> Deva - {0xE0260000u, 45u}, // gby -> Latn + {0xE0260000u, 46u}, // gby -> Latn {0xE4260000u, 2u}, // gbz -> Arab - {0xC4460000u, 45u}, // gcr -> Latn - {0x67640000u, 45u}, // gd -> Latn - {0x90660000u, 45u}, // gde -> Latn - {0xB4660000u, 45u}, // gdn -> Latn - {0xC4660000u, 45u}, // gdr -> Latn - {0x84860000u, 45u}, // geb -> Latn - {0xA4860000u, 45u}, // gej -> Latn - {0xAC860000u, 45u}, // gel -> Latn + {0xC4460000u, 46u}, // gcr -> Latn + {0x67640000u, 46u}, // gd -> Latn + {0x90660000u, 46u}, // gde -> Latn + {0xB4660000u, 46u}, // gdn -> Latn + {0xC4660000u, 46u}, // gdr -> Latn + {0x84860000u, 46u}, // geb -> Latn + {0xA4860000u, 46u}, // gej -> Latn + {0xAC860000u, 46u}, // gel -> Latn {0xE4860000u, 21u}, // gez -> Ethi - {0xA8A60000u, 45u}, // gfk -> Latn + {0xA8A60000u, 46u}, // gfk -> Latn {0xB4C60000u, 19u}, // ggn -> Deva - {0xC8E60000u, 45u}, // ghs -> Latn - {0xAD060000u, 45u}, // gil -> Latn - {0xB1060000u, 45u}, // gim -> Latn + {0xC8E60000u, 46u}, // ghs -> Latn + {0xAD060000u, 46u}, // gil -> Latn + {0xB1060000u, 46u}, // gim -> Latn {0xA9260000u, 2u}, // gjk -> Arab - {0xB5260000u, 45u}, // gjn -> Latn + {0xB5260000u, 46u}, // gjn -> Latn {0xD1260000u, 2u}, // gju -> Arab - {0xB5460000u, 45u}, // gkn -> Latn - {0xBD460000u, 45u}, // gkp -> Latn - {0x676C0000u, 45u}, // gl -> Latn + {0xB5460000u, 46u}, // gkn -> Latn + {0xBD460000u, 46u}, // gkp -> Latn + {0x676C0000u, 46u}, // gl -> Latn {0xA9660000u, 2u}, // glk -> Arab - {0xB1860000u, 45u}, // gmm -> Latn + {0xB1860000u, 46u}, // gmm -> Latn {0xD5860000u, 21u}, // gmv -> Ethi - {0x676E0000u, 45u}, // gn -> Latn - {0x8DA60000u, 45u}, // gnd -> Latn - {0x99A60000u, 45u}, // gng -> Latn - {0x8DC60000u, 45u}, // god -> Latn + {0x676E0000u, 46u}, // gn -> Latn + {0x8DA60000u, 46u}, // gnd -> Latn + {0x99A60000u, 46u}, // gng -> Latn + {0x8DC60000u, 46u}, // god -> Latn {0x95C60000u, 21u}, // gof -> Ethi - {0xA1C60000u, 45u}, // goi -> Latn + {0xA1C60000u, 46u}, // goi -> Latn {0xB1C60000u, 19u}, // gom -> Deva - {0xB5C60000u, 89u}, // gon -> Telu - {0xC5C60000u, 45u}, // gor -> Latn - {0xC9C60000u, 45u}, // gos -> Latn + {0xB5C60000u, 90u}, // gon -> Telu + {0xC5C60000u, 46u}, // gor -> Latn + {0xC9C60000u, 46u}, // gos -> Latn {0xCDC60000u, 25u}, // got -> Goth - {0x86260000u, 45u}, // grb -> Latn + {0x86260000u, 46u}, // grb -> Latn {0x8A260000u, 17u}, // grc -> Cprt {0xCE260000u, 8u}, // grt -> Beng - {0xDA260000u, 45u}, // grw -> Latn - {0xDA460000u, 45u}, // gsw -> Latn + {0xDA260000u, 46u}, // grw -> Latn + {0xDA460000u, 46u}, // gsw -> Latn {0x67750000u, 27u}, // gu -> Gujr - {0x86860000u, 45u}, // gub -> Latn - {0x8A860000u, 45u}, // guc -> Latn - {0x8E860000u, 45u}, // gud -> Latn - {0xC6860000u, 45u}, // gur -> Latn - {0xDA860000u, 45u}, // guw -> Latn - {0xDE860000u, 45u}, // gux -> Latn - {0xE6860000u, 45u}, // guz -> Latn - {0x67760000u, 45u}, // gv -> Latn - {0x96A60000u, 45u}, // gvf -> Latn + {0x86860000u, 46u}, // gub -> Latn + {0x8A860000u, 46u}, // guc -> Latn + {0x8E860000u, 46u}, // gud -> Latn + {0xC6860000u, 46u}, // gur -> Latn + {0xDA860000u, 46u}, // guw -> Latn + {0xDE860000u, 46u}, // gux -> Latn + {0xE6860000u, 46u}, // guz -> Latn + {0x67760000u, 46u}, // gv -> Latn + {0x96A60000u, 46u}, // gvf -> Latn {0xC6A60000u, 19u}, // gvr -> Deva - {0xCAA60000u, 45u}, // gvs -> Latn + {0xCAA60000u, 46u}, // gvs -> Latn {0x8AC60000u, 2u}, // gwc -> Arab - {0xA2C60000u, 45u}, // gwi -> Latn + {0xA2C60000u, 46u}, // gwi -> Latn {0xCEC60000u, 2u}, // gwt -> Arab - {0xA3060000u, 45u}, // gyi -> Latn - {0x68610000u, 45u}, // ha -> Latn + {0xA3060000u, 46u}, // gyi -> Latn + {0x68610000u, 46u}, // ha -> Latn {0x6861434Du, 2u}, // ha-CM -> Arab {0x68615344u, 2u}, // ha-SD -> Arab - {0x98070000u, 45u}, // hag -> Latn + {0x98070000u, 46u}, // hag -> Latn {0xA8070000u, 29u}, // hak -> Hans - {0xB0070000u, 45u}, // ham -> Latn - {0xD8070000u, 45u}, // haw -> Latn + {0xB0070000u, 46u}, // ham -> Latn + {0xD8070000u, 46u}, // haw -> Latn {0xE4070000u, 2u}, // haz -> Arab - {0x84270000u, 45u}, // hbb -> Latn + {0x84270000u, 46u}, // hbb -> Latn {0xE0670000u, 21u}, // hdy -> Ethi {0x68650000u, 31u}, // he -> Hebr - {0xE0E70000u, 45u}, // hhy -> Latn + {0xE0E70000u, 46u}, // hhy -> Latn {0x68690000u, 19u}, // hi -> Deva - {0x81070000u, 45u}, // hia -> Latn - {0x95070000u, 45u}, // hif -> Latn - {0x99070000u, 45u}, // hig -> Latn - {0x9D070000u, 45u}, // hih -> Latn - {0xAD070000u, 45u}, // hil -> Latn - {0x81670000u, 45u}, // hla -> Latn + {0x81070000u, 46u}, // hia -> Latn + {0x95070000u, 46u}, // hif -> Latn + {0x99070000u, 46u}, // hig -> Latn + {0x9D070000u, 46u}, // hih -> Latn + {0xAD070000u, 46u}, // hil -> Latn + {0x81670000u, 46u}, // hla -> Latn {0xD1670000u, 32u}, // hlu -> Hluw - {0x8D870000u, 71u}, // hmd -> Plrd - {0xCD870000u, 45u}, // hmt -> Latn + {0x8D870000u, 72u}, // hmd -> Plrd + {0xCD870000u, 46u}, // hmt -> Latn {0x8DA70000u, 2u}, // hnd -> Arab {0x91A70000u, 19u}, // hne -> Deva {0xA5A70000u, 33u}, // hnj -> Hmnp - {0xB5A70000u, 45u}, // hnn -> Latn + {0xB5A70000u, 46u}, // hnn -> Latn {0xB9A70000u, 2u}, // hno -> Arab - {0x686F0000u, 45u}, // ho -> Latn + {0x686F0000u, 46u}, // ho -> Latn {0x89C70000u, 19u}, // hoc -> Deva {0xA5C70000u, 19u}, // hoj -> Deva - {0xCDC70000u, 45u}, // hot -> Latn - {0x68720000u, 45u}, // hr -> Latn - {0x86470000u, 45u}, // hsb -> Latn + {0xCDC70000u, 46u}, // hot -> Latn + {0x68720000u, 46u}, // hr -> Latn + {0x86470000u, 46u}, // hsb -> Latn {0xB6470000u, 29u}, // hsn -> Hans - {0x68740000u, 45u}, // ht -> Latn - {0x68750000u, 45u}, // hu -> Latn - {0xA2870000u, 45u}, // hui -> Latn + {0x68740000u, 46u}, // ht -> Latn + {0x68750000u, 46u}, // hu -> Latn + {0xA2870000u, 46u}, // hui -> Latn + {0xC6870000u, 46u}, // hur -> Latn {0x68790000u, 4u}, // hy -> Armn - {0x687A0000u, 45u}, // hz -> Latn - {0x69610000u, 45u}, // ia -> Latn - {0xB4080000u, 45u}, // ian -> Latn - {0xC4080000u, 45u}, // iar -> Latn - {0x80280000u, 45u}, // iba -> Latn - {0x84280000u, 45u}, // ibb -> Latn - {0xE0280000u, 45u}, // iby -> Latn - {0x80480000u, 45u}, // ica -> Latn - {0x9C480000u, 45u}, // ich -> Latn - {0x69640000u, 45u}, // id -> Latn - {0x8C680000u, 45u}, // idd -> Latn - {0xA0680000u, 45u}, // idi -> Latn - {0xD0680000u, 45u}, // idu -> Latn - {0x90A80000u, 45u}, // ife -> Latn - {0x69670000u, 45u}, // ig -> Latn - {0x84C80000u, 45u}, // igb -> Latn - {0x90C80000u, 45u}, // ige -> Latn - {0x69690000u, 101u}, // ii -> Yiii - {0xA5280000u, 45u}, // ijj -> Latn - {0x696B0000u, 45u}, // ik -> Latn - {0xA9480000u, 45u}, // ikk -> Latn - {0xCD480000u, 45u}, // ikt -> Latn - {0xD9480000u, 45u}, // ikw -> Latn - {0xDD480000u, 45u}, // ikx -> Latn - {0xB9680000u, 45u}, // ilo -> Latn - {0xB9880000u, 45u}, // imo -> Latn - {0x696E0000u, 45u}, // in -> Latn + {0x687A0000u, 46u}, // hz -> Latn + {0x69610000u, 46u}, // ia -> Latn + {0xB4080000u, 46u}, // ian -> Latn + {0xC4080000u, 46u}, // iar -> Latn + {0x80280000u, 46u}, // iba -> Latn + {0x84280000u, 46u}, // ibb -> Latn + {0xE0280000u, 46u}, // iby -> Latn + {0x80480000u, 46u}, // ica -> Latn + {0x9C480000u, 46u}, // ich -> Latn + {0x69640000u, 46u}, // id -> Latn + {0x8C680000u, 46u}, // idd -> Latn + {0xA0680000u, 46u}, // idi -> Latn + {0xD0680000u, 46u}, // idu -> Latn + {0x90A80000u, 46u}, // ife -> Latn + {0x69670000u, 46u}, // ig -> Latn + {0x84C80000u, 46u}, // igb -> Latn + {0x90C80000u, 46u}, // ige -> Latn + {0x69690000u, 102u}, // ii -> Yiii + {0xA5280000u, 46u}, // ijj -> Latn + {0x696B0000u, 46u}, // ik -> Latn + {0xA9480000u, 46u}, // ikk -> Latn + {0xD9480000u, 46u}, // ikw -> Latn + {0xDD480000u, 46u}, // ikx -> Latn + {0xB9680000u, 46u}, // ilo -> Latn + {0xB9880000u, 46u}, // imo -> Latn + {0x696E0000u, 46u}, // in -> Latn {0x9DA80000u, 18u}, // inh -> Cyrl - {0x696F0000u, 45u}, // io -> Latn - {0xD1C80000u, 45u}, // iou -> Latn - {0xA2280000u, 45u}, // iri -> Latn - {0x69730000u, 45u}, // is -> Latn - {0x69740000u, 45u}, // it -> Latn + {0x696F0000u, 46u}, // io -> Latn + {0xD1C80000u, 46u}, // iou -> Latn + {0xA2280000u, 46u}, // iri -> Latn + {0x69730000u, 46u}, // is -> Latn + {0x69740000u, 46u}, // it -> Latn {0x69750000u, 11u}, // iu -> Cans {0x69770000u, 31u}, // iw -> Hebr - {0xB2C80000u, 45u}, // iwm -> Latn - {0xCAC80000u, 45u}, // iws -> Latn - {0x9F280000u, 45u}, // izh -> Latn - {0xA3280000u, 45u}, // izi -> Latn + {0xB2C80000u, 46u}, // iwm -> Latn + {0xCAC80000u, 46u}, // iws -> Latn + {0x9F280000u, 46u}, // izh -> Latn + {0xA3280000u, 46u}, // izi -> Latn {0x6A610000u, 35u}, // ja -> Jpan - {0x84090000u, 45u}, // jab -> Latn - {0xB0090000u, 45u}, // jam -> Latn - {0xC4090000u, 45u}, // jar -> Latn - {0xB8290000u, 45u}, // jbo -> Latn - {0xD0290000u, 45u}, // jbu -> Latn - {0xB4890000u, 45u}, // jen -> Latn - {0xA8C90000u, 45u}, // jgk -> Latn - {0xB8C90000u, 45u}, // jgo -> Latn + {0x84090000u, 46u}, // jab -> Latn + {0xB0090000u, 46u}, // jam -> Latn + {0xC4090000u, 46u}, // jar -> Latn + {0xB8290000u, 46u}, // jbo -> Latn + {0xD0290000u, 46u}, // jbu -> Latn + {0xB4890000u, 46u}, // jen -> Latn + {0xA8C90000u, 46u}, // jgk -> Latn + {0xB8C90000u, 46u}, // jgo -> Latn {0x6A690000u, 31u}, // ji -> Hebr - {0x85090000u, 45u}, // jib -> Latn - {0x89890000u, 45u}, // jmc -> Latn + {0x85090000u, 46u}, // jib -> Latn + {0x89890000u, 46u}, // jmc -> Latn {0xAD890000u, 19u}, // jml -> Deva - {0x82290000u, 45u}, // jra -> Latn - {0xCE890000u, 45u}, // jut -> Latn - {0x6A760000u, 45u}, // jv -> Latn - {0x6A770000u, 45u}, // jw -> Latn + {0x82290000u, 46u}, // jra -> Latn + {0xCE890000u, 46u}, // jut -> Latn + {0x6A760000u, 46u}, // jv -> Latn + {0x6A770000u, 46u}, // jw -> Latn {0x6B610000u, 22u}, // ka -> Geor {0x800A0000u, 18u}, // kaa -> Cyrl - {0x840A0000u, 45u}, // kab -> Latn - {0x880A0000u, 45u}, // kac -> Latn - {0x8C0A0000u, 45u}, // kad -> Latn - {0xA00A0000u, 45u}, // kai -> Latn - {0xA40A0000u, 45u}, // kaj -> Latn - {0xB00A0000u, 45u}, // kam -> Latn - {0xB80A0000u, 45u}, // kao -> Latn + {0x840A0000u, 46u}, // kab -> Latn + {0x880A0000u, 46u}, // kac -> Latn + {0x8C0A0000u, 46u}, // kad -> Latn + {0xA00A0000u, 46u}, // kai -> Latn + {0xA40A0000u, 46u}, // kaj -> Latn + {0xB00A0000u, 46u}, // kam -> Latn + {0xB80A0000u, 46u}, // kao -> Latn + {0xD80A0000u, 38u}, // kaw -> Kawi {0x8C2A0000u, 18u}, // kbd -> Cyrl - {0xB02A0000u, 45u}, // kbm -> Latn - {0xBC2A0000u, 45u}, // kbp -> Latn - {0xC02A0000u, 45u}, // kbq -> Latn - {0xDC2A0000u, 45u}, // kbx -> Latn + {0xB02A0000u, 46u}, // kbm -> Latn + {0xBC2A0000u, 46u}, // kbp -> Latn + {0xC02A0000u, 46u}, // kbq -> Latn + {0xDC2A0000u, 46u}, // kbx -> Latn {0xE02A0000u, 2u}, // kby -> Arab - {0x984A0000u, 45u}, // kcg -> Latn - {0xA84A0000u, 45u}, // kck -> Latn - {0xAC4A0000u, 45u}, // kcl -> Latn - {0xCC4A0000u, 45u}, // kct -> Latn - {0x906A0000u, 45u}, // kde -> Latn - {0x9C6A0000u, 45u}, // kdh -> Latn - {0xAC6A0000u, 45u}, // kdl -> Latn - {0xCC6A0000u, 92u}, // kdt -> Thai - {0x808A0000u, 45u}, // kea -> Latn - {0xB48A0000u, 45u}, // ken -> Latn - {0xE48A0000u, 45u}, // kez -> Latn - {0xB8AA0000u, 45u}, // kfo -> Latn + {0x984A0000u, 46u}, // kcg -> Latn + {0xA84A0000u, 46u}, // kck -> Latn + {0xAC4A0000u, 46u}, // kcl -> Latn + {0xCC4A0000u, 46u}, // kct -> Latn + {0x906A0000u, 46u}, // kde -> Latn + {0x9C6A0000u, 46u}, // kdh -> Latn + {0xAC6A0000u, 46u}, // kdl -> Latn + {0xCC6A0000u, 93u}, // kdt -> Thai + {0x808A0000u, 46u}, // kea -> Latn + {0xB48A0000u, 46u}, // ken -> Latn + {0xE48A0000u, 46u}, // kez -> Latn + {0xB8AA0000u, 46u}, // kfo -> Latn {0xC4AA0000u, 19u}, // kfr -> Deva {0xE0AA0000u, 19u}, // kfy -> Deva - {0x6B670000u, 45u}, // kg -> Latn - {0x90CA0000u, 45u}, // kge -> Latn - {0x94CA0000u, 45u}, // kgf -> Latn - {0xBCCA0000u, 45u}, // kgp -> Latn - {0x80EA0000u, 45u}, // kha -> Latn - {0x84EA0000u, 85u}, // khb -> Talu + {0x6B670000u, 46u}, // kg -> Latn + {0x90CA0000u, 46u}, // kge -> Latn + {0x94CA0000u, 46u}, // kgf -> Latn + {0xBCCA0000u, 46u}, // kgp -> Latn + {0x80EA0000u, 46u}, // kha -> Latn + {0x84EA0000u, 86u}, // khb -> Talu {0xB4EA0000u, 19u}, // khn -> Deva - {0xC0EA0000u, 45u}, // khq -> Latn - {0xC8EA0000u, 45u}, // khs -> Latn - {0xCCEA0000u, 58u}, // kht -> Mymr + {0xC0EA0000u, 46u}, // khq -> Latn + {0xC8EA0000u, 46u}, // khs -> Latn + {0xCCEA0000u, 59u}, // kht -> Mymr {0xD8EA0000u, 2u}, // khw -> Arab - {0xE4EA0000u, 45u}, // khz -> Latn - {0x6B690000u, 45u}, // ki -> Latn - {0xA50A0000u, 45u}, // kij -> Latn - {0xD10A0000u, 45u}, // kiu -> Latn - {0xD90A0000u, 45u}, // kiw -> Latn - {0x6B6A0000u, 45u}, // kj -> Latn - {0x8D2A0000u, 45u}, // kjd -> Latn - {0x992A0000u, 44u}, // kjg -> Laoo - {0xC92A0000u, 45u}, // kjs -> Latn - {0xE12A0000u, 45u}, // kjy -> Latn + {0xE4EA0000u, 46u}, // khz -> Latn + {0x6B690000u, 46u}, // ki -> Latn + {0xA50A0000u, 46u}, // kij -> Latn + {0xD10A0000u, 46u}, // kiu -> Latn + {0xD90A0000u, 46u}, // kiw -> Latn + {0x6B6A0000u, 46u}, // kj -> Latn + {0x8D2A0000u, 46u}, // kjd -> Latn + {0x992A0000u, 45u}, // kjg -> Laoo + {0xC92A0000u, 46u}, // kjs -> Latn + {0xE12A0000u, 46u}, // kjy -> Latn {0x6B6B0000u, 18u}, // kk -> Cyrl {0x6B6B4146u, 2u}, // kk-AF -> Arab {0x6B6B434Eu, 2u}, // kk-CN -> Arab {0x6B6B4952u, 2u}, // kk-IR -> Arab {0x6B6B4D4Eu, 2u}, // kk-MN -> Arab - {0x894A0000u, 45u}, // kkc -> Latn - {0xA54A0000u, 45u}, // kkj -> Latn - {0x6B6C0000u, 45u}, // kl -> Latn - {0xB56A0000u, 45u}, // kln -> Latn - {0xC16A0000u, 45u}, // klq -> Latn - {0xCD6A0000u, 45u}, // klt -> Latn - {0xDD6A0000u, 45u}, // klx -> Latn - {0x6B6D0000u, 39u}, // km -> Khmr - {0x858A0000u, 45u}, // kmb -> Latn - {0x9D8A0000u, 45u}, // kmh -> Latn - {0xB98A0000u, 45u}, // kmo -> Latn - {0xC98A0000u, 45u}, // kms -> Latn - {0xD18A0000u, 45u}, // kmu -> Latn - {0xD98A0000u, 45u}, // kmw -> Latn - {0x6B6E0000u, 41u}, // kn -> Knda - {0x95AA0000u, 45u}, // knf -> Latn - {0xBDAA0000u, 45u}, // knp -> Latn - {0x6B6F0000u, 42u}, // ko -> Kore + {0x894A0000u, 46u}, // kkc -> Latn + {0xA54A0000u, 46u}, // kkj -> Latn + {0x6B6C0000u, 46u}, // kl -> Latn + {0xB56A0000u, 46u}, // kln -> Latn + {0xC16A0000u, 46u}, // klq -> Latn + {0xCD6A0000u, 46u}, // klt -> Latn + {0xDD6A0000u, 46u}, // klx -> Latn + {0x6B6D0000u, 40u}, // km -> Khmr + {0x858A0000u, 46u}, // kmb -> Latn + {0x9D8A0000u, 46u}, // kmh -> Latn + {0xB98A0000u, 46u}, // kmo -> Latn + {0xC98A0000u, 46u}, // kms -> Latn + {0xD18A0000u, 46u}, // kmu -> Latn + {0xD98A0000u, 46u}, // kmw -> Latn + {0x6B6E0000u, 42u}, // kn -> Knda + {0x95AA0000u, 46u}, // knf -> Latn + {0xBDAA0000u, 46u}, // knp -> Latn + {0x6B6F0000u, 43u}, // ko -> Kore {0xA1CA0000u, 18u}, // koi -> Cyrl {0xA9CA0000u, 19u}, // kok -> Deva - {0xADCA0000u, 45u}, // kol -> Latn - {0xC9CA0000u, 45u}, // kos -> Latn - {0xE5CA0000u, 45u}, // koz -> Latn - {0x91EA0000u, 45u}, // kpe -> Latn - {0x95EA0000u, 45u}, // kpf -> Latn - {0xB9EA0000u, 45u}, // kpo -> Latn - {0xC5EA0000u, 45u}, // kpr -> Latn - {0xDDEA0000u, 45u}, // kpx -> Latn - {0x860A0000u, 45u}, // kqb -> Latn - {0x960A0000u, 45u}, // kqf -> Latn - {0xCA0A0000u, 45u}, // kqs -> Latn + {0xADCA0000u, 46u}, // kol -> Latn + {0xC9CA0000u, 46u}, // kos -> Latn + {0xE5CA0000u, 46u}, // koz -> Latn + {0x91EA0000u, 46u}, // kpe -> Latn + {0x95EA0000u, 46u}, // kpf -> Latn + {0xB9EA0000u, 46u}, // kpo -> Latn + {0xC5EA0000u, 46u}, // kpr -> Latn + {0xDDEA0000u, 46u}, // kpx -> Latn + {0x860A0000u, 46u}, // kqb -> Latn + {0x960A0000u, 46u}, // kqf -> Latn + {0xCA0A0000u, 46u}, // kqs -> Latn {0xE20A0000u, 21u}, // kqy -> Ethi - {0x6B720000u, 45u}, // kr -> Latn + {0x6B720000u, 46u}, // kr -> Latn {0x8A2A0000u, 18u}, // krc -> Cyrl - {0xA22A0000u, 45u}, // kri -> Latn - {0xA62A0000u, 45u}, // krj -> Latn - {0xAE2A0000u, 45u}, // krl -> Latn - {0xCA2A0000u, 45u}, // krs -> Latn + {0xA22A0000u, 46u}, // kri -> Latn + {0xA62A0000u, 46u}, // krj -> Latn + {0xAE2A0000u, 46u}, // krl -> Latn + {0xCA2A0000u, 46u}, // krs -> Latn {0xD22A0000u, 19u}, // kru -> Deva {0x6B730000u, 2u}, // ks -> Arab - {0x864A0000u, 45u}, // ksb -> Latn - {0x8E4A0000u, 45u}, // ksd -> Latn - {0x964A0000u, 45u}, // ksf -> Latn - {0x9E4A0000u, 45u}, // ksh -> Latn - {0xA64A0000u, 45u}, // ksj -> Latn - {0xC64A0000u, 45u}, // ksr -> Latn + {0x864A0000u, 46u}, // ksb -> Latn + {0x8E4A0000u, 46u}, // ksd -> Latn + {0x964A0000u, 46u}, // ksf -> Latn + {0x9E4A0000u, 46u}, // ksh -> Latn + {0xA64A0000u, 46u}, // ksj -> Latn + {0xC64A0000u, 46u}, // ksr -> Latn {0x866A0000u, 21u}, // ktb -> Ethi - {0xB26A0000u, 45u}, // ktm -> Latn - {0xBA6A0000u, 45u}, // kto -> Latn - {0xC66A0000u, 45u}, // ktr -> Latn - {0x6B750000u, 45u}, // ku -> Latn + {0xB26A0000u, 46u}, // ktm -> Latn + {0xBA6A0000u, 46u}, // kto -> Latn + {0xC66A0000u, 46u}, // ktr -> Latn + {0x6B750000u, 46u}, // ku -> Latn {0x6B754952u, 2u}, // ku-IR -> Arab {0x6B754C42u, 2u}, // ku-LB -> Arab - {0x868A0000u, 45u}, // kub -> Latn - {0x8E8A0000u, 45u}, // kud -> Latn - {0x928A0000u, 45u}, // kue -> Latn - {0xA68A0000u, 45u}, // kuj -> Latn + {0x868A0000u, 46u}, // kub -> Latn + {0x8E8A0000u, 46u}, // kud -> Latn + {0x928A0000u, 46u}, // kue -> Latn + {0xA68A0000u, 46u}, // kuj -> Latn {0xB28A0000u, 18u}, // kum -> Cyrl - {0xB68A0000u, 45u}, // kun -> Latn - {0xBE8A0000u, 45u}, // kup -> Latn - {0xCA8A0000u, 45u}, // kus -> Latn + {0xB68A0000u, 46u}, // kun -> Latn + {0xBE8A0000u, 46u}, // kup -> Latn + {0xCA8A0000u, 46u}, // kus -> Latn {0x6B760000u, 18u}, // kv -> Cyrl - {0x9AAA0000u, 45u}, // kvg -> Latn - {0xC6AA0000u, 45u}, // kvr -> Latn + {0x9AAA0000u, 46u}, // kvg -> Latn + {0xC6AA0000u, 46u}, // kvr -> Latn {0xDEAA0000u, 2u}, // kvx -> Arab - {0x6B770000u, 45u}, // kw -> Latn - {0xA6CA0000u, 45u}, // kwj -> Latn - {0xBACA0000u, 45u}, // kwo -> Latn - {0xC2CA0000u, 45u}, // kwq -> Latn - {0x82EA0000u, 45u}, // kxa -> Latn + {0x6B770000u, 46u}, // kw -> Latn + {0xA6CA0000u, 46u}, // kwj -> Latn + {0xAACA0000u, 46u}, // kwk -> Latn + {0xBACA0000u, 46u}, // kwo -> Latn + {0xC2CA0000u, 46u}, // kwq -> Latn + {0x82EA0000u, 46u}, // kxa -> Latn {0x8AEA0000u, 21u}, // kxc -> Ethi - {0x92EA0000u, 45u}, // kxe -> Latn + {0x92EA0000u, 46u}, // kxe -> Latn {0xAEEA0000u, 19u}, // kxl -> Deva - {0xB2EA0000u, 92u}, // kxm -> Thai + {0xB2EA0000u, 93u}, // kxm -> Thai {0xBEEA0000u, 2u}, // kxp -> Arab - {0xDAEA0000u, 45u}, // kxw -> Latn - {0xE6EA0000u, 45u}, // kxz -> Latn + {0xDAEA0000u, 46u}, // kxw -> Latn + {0xE6EA0000u, 46u}, // kxz -> Latn {0x6B790000u, 18u}, // ky -> Cyrl {0x6B79434Eu, 2u}, // ky-CN -> Arab - {0x6B795452u, 45u}, // ky-TR -> Latn - {0x930A0000u, 45u}, // kye -> Latn - {0xDF0A0000u, 45u}, // kyx -> Latn + {0x6B795452u, 46u}, // ky-TR -> Latn + {0x930A0000u, 46u}, // kye -> Latn + {0xDF0A0000u, 46u}, // kyx -> Latn {0x9F2A0000u, 2u}, // kzh -> Arab - {0xA72A0000u, 45u}, // kzj -> Latn - {0xC72A0000u, 45u}, // kzr -> Latn - {0xCF2A0000u, 45u}, // kzt -> Latn - {0x6C610000u, 45u}, // la -> Latn - {0x840B0000u, 47u}, // lab -> Lina + {0xA72A0000u, 46u}, // kzj -> Latn + {0xC72A0000u, 46u}, // kzr -> Latn + {0xCF2A0000u, 46u}, // kzt -> Latn + {0x6C610000u, 46u}, // la -> Latn + {0x840B0000u, 48u}, // lab -> Lina {0x8C0B0000u, 31u}, // lad -> Hebr - {0x980B0000u, 45u}, // lag -> Latn + {0x980B0000u, 46u}, // lag -> Latn {0x9C0B0000u, 2u}, // lah -> Arab - {0xA40B0000u, 45u}, // laj -> Latn - {0xC80B0000u, 45u}, // las -> Latn - {0x6C620000u, 45u}, // lb -> Latn + {0xA40B0000u, 46u}, // laj -> Latn + {0xC80B0000u, 46u}, // las -> Latn + {0x6C620000u, 46u}, // lb -> Latn {0x902B0000u, 18u}, // lbe -> Cyrl - {0xD02B0000u, 45u}, // lbu -> Latn - {0xD82B0000u, 45u}, // lbw -> Latn - {0xB04B0000u, 45u}, // lcm -> Latn - {0xBC4B0000u, 92u}, // lcp -> Thai - {0x846B0000u, 45u}, // ldb -> Latn - {0x8C8B0000u, 45u}, // led -> Latn - {0x908B0000u, 45u}, // lee -> Latn - {0xB08B0000u, 45u}, // lem -> Latn - {0xBC8B0000u, 46u}, // lep -> Lepc - {0xC08B0000u, 45u}, // leq -> Latn - {0xD08B0000u, 45u}, // leu -> Latn + {0xD02B0000u, 46u}, // lbu -> Latn + {0xD82B0000u, 46u}, // lbw -> Latn + {0xB04B0000u, 46u}, // lcm -> Latn + {0xBC4B0000u, 93u}, // lcp -> Thai + {0x846B0000u, 46u}, // ldb -> Latn + {0x8C8B0000u, 46u}, // led -> Latn + {0x908B0000u, 46u}, // lee -> Latn + {0xB08B0000u, 46u}, // lem -> Latn + {0xBC8B0000u, 47u}, // lep -> Lepc + {0xC08B0000u, 46u}, // leq -> Latn + {0xD08B0000u, 46u}, // leu -> Latn {0xE48B0000u, 18u}, // lez -> Cyrl - {0x6C670000u, 45u}, // lg -> Latn - {0x98CB0000u, 45u}, // lgg -> Latn - {0x6C690000u, 45u}, // li -> Latn - {0x810B0000u, 45u}, // lia -> Latn - {0x8D0B0000u, 45u}, // lid -> Latn + {0x6C670000u, 46u}, // lg -> Latn + {0x98CB0000u, 46u}, // lgg -> Latn + {0x6C690000u, 46u}, // li -> Latn + {0x810B0000u, 46u}, // lia -> Latn + {0x8D0B0000u, 46u}, // lid -> Latn {0x950B0000u, 19u}, // lif -> Deva - {0x990B0000u, 45u}, // lig -> Latn - {0x9D0B0000u, 45u}, // lih -> Latn - {0xA50B0000u, 45u}, // lij -> Latn - {0xC90B0000u, 48u}, // lis -> Lisu - {0xBD2B0000u, 45u}, // ljp -> Latn + {0x990B0000u, 46u}, // lig -> Latn + {0x9D0B0000u, 46u}, // lih -> Latn + {0xA50B0000u, 46u}, // lij -> Latn + {0xAD0B0000u, 46u}, // lil -> Latn + {0xC90B0000u, 49u}, // lis -> Lisu + {0xBD2B0000u, 46u}, // ljp -> Latn {0xA14B0000u, 2u}, // lki -> Arab - {0xCD4B0000u, 45u}, // lkt -> Latn - {0x916B0000u, 45u}, // lle -> Latn - {0xB56B0000u, 45u}, // lln -> Latn - {0xB58B0000u, 89u}, // lmn -> Telu - {0xB98B0000u, 45u}, // lmo -> Latn - {0xBD8B0000u, 45u}, // lmp -> Latn - {0x6C6E0000u, 45u}, // ln -> Latn - {0xC9AB0000u, 45u}, // lns -> Latn - {0xD1AB0000u, 45u}, // lnu -> Latn - {0x6C6F0000u, 44u}, // lo -> Laoo - {0xA5CB0000u, 45u}, // loj -> Latn - {0xA9CB0000u, 45u}, // lok -> Latn - {0xADCB0000u, 45u}, // lol -> Latn - {0xC5CB0000u, 45u}, // lor -> Latn - {0xC9CB0000u, 45u}, // los -> Latn - {0xE5CB0000u, 45u}, // loz -> Latn + {0xCD4B0000u, 46u}, // lkt -> Latn + {0x916B0000u, 46u}, // lle -> Latn + {0xB56B0000u, 46u}, // lln -> Latn + {0xB58B0000u, 90u}, // lmn -> Telu + {0xB98B0000u, 46u}, // lmo -> Latn + {0xBD8B0000u, 46u}, // lmp -> Latn + {0x6C6E0000u, 46u}, // ln -> Latn + {0xC9AB0000u, 46u}, // lns -> Latn + {0xD1AB0000u, 46u}, // lnu -> Latn + {0x6C6F0000u, 45u}, // lo -> Laoo + {0xA5CB0000u, 46u}, // loj -> Latn + {0xA9CB0000u, 46u}, // lok -> Latn + {0xADCB0000u, 46u}, // lol -> Latn + {0xC5CB0000u, 46u}, // lor -> Latn + {0xC9CB0000u, 46u}, // los -> Latn + {0xE5CB0000u, 46u}, // loz -> Latn {0x8A2B0000u, 2u}, // lrc -> Arab - {0x6C740000u, 45u}, // lt -> Latn - {0x9A6B0000u, 45u}, // ltg -> Latn - {0x6C750000u, 45u}, // lu -> Latn - {0x828B0000u, 45u}, // lua -> Latn - {0xBA8B0000u, 45u}, // luo -> Latn - {0xE28B0000u, 45u}, // luy -> Latn + {0x6C740000u, 46u}, // lt -> Latn + {0x9A6B0000u, 46u}, // ltg -> Latn + {0x6C750000u, 46u}, // lu -> Latn + {0x828B0000u, 46u}, // lua -> Latn + {0xBA8B0000u, 46u}, // luo -> Latn + {0xE28B0000u, 46u}, // luy -> Latn {0xE68B0000u, 2u}, // luz -> Arab - {0x6C760000u, 45u}, // lv -> Latn - {0xAECB0000u, 92u}, // lwl -> Thai + {0x6C760000u, 46u}, // lv -> Latn + {0xAECB0000u, 93u}, // lwl -> Thai {0x9F2B0000u, 29u}, // lzh -> Hans - {0xE72B0000u, 45u}, // lzz -> Latn - {0x8C0C0000u, 45u}, // mad -> Latn - {0x940C0000u, 45u}, // maf -> Latn + {0xE72B0000u, 46u}, // lzz -> Latn + {0x8C0C0000u, 46u}, // mad -> Latn + {0x940C0000u, 46u}, // maf -> Latn {0x980C0000u, 19u}, // mag -> Deva {0xA00C0000u, 19u}, // mai -> Deva - {0xA80C0000u, 45u}, // mak -> Latn - {0xB40C0000u, 45u}, // man -> Latn - {0xB40C474Eu, 60u}, // man-GN -> Nkoo - {0xC80C0000u, 45u}, // mas -> Latn - {0xD80C0000u, 45u}, // maw -> Latn - {0xE40C0000u, 45u}, // maz -> Latn - {0x9C2C0000u, 45u}, // mbh -> Latn - {0xB82C0000u, 45u}, // mbo -> Latn - {0xC02C0000u, 45u}, // mbq -> Latn - {0xD02C0000u, 45u}, // mbu -> Latn - {0xD82C0000u, 45u}, // mbw -> Latn - {0xA04C0000u, 45u}, // mci -> Latn - {0xBC4C0000u, 45u}, // mcp -> Latn - {0xC04C0000u, 45u}, // mcq -> Latn - {0xC44C0000u, 45u}, // mcr -> Latn - {0xD04C0000u, 45u}, // mcu -> Latn - {0x806C0000u, 45u}, // mda -> Latn + {0xA80C0000u, 46u}, // mak -> Latn + {0xB40C0000u, 46u}, // man -> Latn + {0xB40C474Eu, 61u}, // man-GN -> Nkoo + {0xC80C0000u, 46u}, // mas -> Latn + {0xD80C0000u, 46u}, // maw -> Latn + {0xE40C0000u, 46u}, // maz -> Latn + {0x9C2C0000u, 46u}, // mbh -> Latn + {0xB82C0000u, 46u}, // mbo -> Latn + {0xC02C0000u, 46u}, // mbq -> Latn + {0xD02C0000u, 46u}, // mbu -> Latn + {0xD82C0000u, 46u}, // mbw -> Latn + {0xA04C0000u, 46u}, // mci -> Latn + {0xBC4C0000u, 46u}, // mcp -> Latn + {0xC04C0000u, 46u}, // mcq -> Latn + {0xC44C0000u, 46u}, // mcr -> Latn + {0xD04C0000u, 46u}, // mcu -> Latn + {0x806C0000u, 46u}, // mda -> Latn {0x906C0000u, 2u}, // mde -> Arab {0x946C0000u, 18u}, // mdf -> Cyrl - {0x9C6C0000u, 45u}, // mdh -> Latn - {0xA46C0000u, 45u}, // mdj -> Latn - {0xC46C0000u, 45u}, // mdr -> Latn + {0x9C6C0000u, 46u}, // mdh -> Latn + {0xA46C0000u, 46u}, // mdj -> Latn + {0xC46C0000u, 46u}, // mdr -> Latn {0xDC6C0000u, 21u}, // mdx -> Ethi - {0x8C8C0000u, 45u}, // med -> Latn - {0x908C0000u, 45u}, // mee -> Latn - {0xA88C0000u, 45u}, // mek -> Latn - {0xB48C0000u, 45u}, // men -> Latn - {0xC48C0000u, 45u}, // mer -> Latn - {0xCC8C0000u, 45u}, // met -> Latn - {0xD08C0000u, 45u}, // meu -> Latn + {0x8C8C0000u, 46u}, // med -> Latn + {0x908C0000u, 46u}, // mee -> Latn + {0xA88C0000u, 46u}, // mek -> Latn + {0xB48C0000u, 46u}, // men -> Latn + {0xC48C0000u, 46u}, // mer -> Latn + {0xCC8C0000u, 46u}, // met -> Latn + {0xD08C0000u, 46u}, // meu -> Latn {0x80AC0000u, 2u}, // mfa -> Arab - {0x90AC0000u, 45u}, // mfe -> Latn - {0xB4AC0000u, 45u}, // mfn -> Latn - {0xB8AC0000u, 45u}, // mfo -> Latn - {0xC0AC0000u, 45u}, // mfq -> Latn - {0x6D670000u, 45u}, // mg -> Latn - {0x9CCC0000u, 45u}, // mgh -> Latn - {0xACCC0000u, 45u}, // mgl -> Latn - {0xB8CC0000u, 45u}, // mgo -> Latn + {0x90AC0000u, 46u}, // mfe -> Latn + {0xB4AC0000u, 46u}, // mfn -> Latn + {0xB8AC0000u, 46u}, // mfo -> Latn + {0xC0AC0000u, 46u}, // mfq -> Latn + {0x6D670000u, 46u}, // mg -> Latn + {0x9CCC0000u, 46u}, // mgh -> Latn + {0xACCC0000u, 46u}, // mgl -> Latn + {0xB8CC0000u, 46u}, // mgo -> Latn {0xBCCC0000u, 19u}, // mgp -> Deva - {0xE0CC0000u, 45u}, // mgy -> Latn - {0x6D680000u, 45u}, // mh -> Latn - {0xA0EC0000u, 45u}, // mhi -> Latn - {0xACEC0000u, 45u}, // mhl -> Latn - {0x6D690000u, 45u}, // mi -> Latn - {0x950C0000u, 45u}, // mif -> Latn - {0xB50C0000u, 45u}, // min -> Latn - {0xD90C0000u, 45u}, // miw -> Latn + {0xE0CC0000u, 46u}, // mgy -> Latn + {0x6D680000u, 46u}, // mh -> Latn + {0xA0EC0000u, 46u}, // mhi -> Latn + {0xACEC0000u, 46u}, // mhl -> Latn + {0x6D690000u, 46u}, // mi -> Latn + {0x890C0000u, 46u}, // mic -> Latn + {0x950C0000u, 46u}, // mif -> Latn + {0xB50C0000u, 46u}, // min -> Latn + {0xD90C0000u, 46u}, // miw -> Latn {0x6D6B0000u, 18u}, // mk -> Cyrl {0xA14C0000u, 2u}, // mki -> Arab - {0xAD4C0000u, 45u}, // mkl -> Latn - {0xBD4C0000u, 45u}, // mkp -> Latn - {0xD94C0000u, 45u}, // mkw -> Latn - {0x6D6C0000u, 55u}, // ml -> Mlym - {0x916C0000u, 45u}, // mle -> Latn - {0xBD6C0000u, 45u}, // mlp -> Latn - {0xC96C0000u, 45u}, // mls -> Latn - {0xB98C0000u, 45u}, // mmo -> Latn - {0xD18C0000u, 45u}, // mmu -> Latn - {0xDD8C0000u, 45u}, // mmx -> Latn + {0xAD4C0000u, 46u}, // mkl -> Latn + {0xBD4C0000u, 46u}, // mkp -> Latn + {0xD94C0000u, 46u}, // mkw -> Latn + {0x6D6C0000u, 56u}, // ml -> Mlym + {0x916C0000u, 46u}, // mle -> Latn + {0xBD6C0000u, 46u}, // mlp -> Latn + {0xC96C0000u, 46u}, // mls -> Latn + {0xB98C0000u, 46u}, // mmo -> Latn + {0xD18C0000u, 46u}, // mmu -> Latn + {0xDD8C0000u, 46u}, // mmx -> Latn {0x6D6E0000u, 18u}, // mn -> Cyrl - {0x6D6E434Eu, 56u}, // mn-CN -> Mong - {0x81AC0000u, 45u}, // mna -> Latn - {0x95AC0000u, 45u}, // mnf -> Latn + {0x6D6E434Eu, 57u}, // mn-CN -> Mong + {0x81AC0000u, 46u}, // mna -> Latn + {0x95AC0000u, 46u}, // mnf -> Latn {0xA1AC0000u, 8u}, // mni -> Beng - {0xD9AC0000u, 58u}, // mnw -> Mymr - {0x6D6F0000u, 45u}, // mo -> Latn - {0x81CC0000u, 45u}, // moa -> Latn - {0x91CC0000u, 45u}, // moe -> Latn - {0x9DCC0000u, 45u}, // moh -> Latn - {0xC9CC0000u, 45u}, // mos -> Latn - {0xDDCC0000u, 45u}, // mox -> Latn - {0xBDEC0000u, 45u}, // mpp -> Latn - {0xC9EC0000u, 45u}, // mps -> Latn - {0xCDEC0000u, 45u}, // mpt -> Latn - {0xDDEC0000u, 45u}, // mpx -> Latn - {0xAE0C0000u, 45u}, // mql -> Latn + {0xD9AC0000u, 59u}, // mnw -> Mymr + {0x6D6F0000u, 46u}, // mo -> Latn + {0x81CC0000u, 46u}, // moa -> Latn + {0x91CC0000u, 46u}, // moe -> Latn + {0x9DCC0000u, 46u}, // moh -> Latn + {0xC9CC0000u, 46u}, // mos -> Latn + {0xDDCC0000u, 46u}, // mox -> Latn + {0xBDEC0000u, 46u}, // mpp -> Latn + {0xC9EC0000u, 46u}, // mps -> Latn + {0xCDEC0000u, 46u}, // mpt -> Latn + {0xDDEC0000u, 46u}, // mpx -> Latn + {0xAE0C0000u, 46u}, // mql -> Latn {0x6D720000u, 19u}, // mr -> Deva {0x8E2C0000u, 19u}, // mrd -> Deva {0xA62C0000u, 18u}, // mrj -> Cyrl - {0xBA2C0000u, 57u}, // mro -> Mroo - {0x6D730000u, 45u}, // ms -> Latn + {0xBA2C0000u, 58u}, // mro -> Mroo + {0x6D730000u, 46u}, // ms -> Latn {0x6D734343u, 2u}, // ms-CC -> Arab - {0x6D740000u, 45u}, // mt -> Latn - {0x8A6C0000u, 45u}, // mtc -> Latn - {0x966C0000u, 45u}, // mtf -> Latn - {0xA26C0000u, 45u}, // mti -> Latn + {0x6D740000u, 46u}, // mt -> Latn + {0x8A6C0000u, 46u}, // mtc -> Latn + {0x966C0000u, 46u}, // mtf -> Latn + {0xA26C0000u, 46u}, // mti -> Latn {0xC66C0000u, 19u}, // mtr -> Deva - {0x828C0000u, 45u}, // mua -> Latn - {0xC68C0000u, 45u}, // mur -> Latn - {0xCA8C0000u, 45u}, // mus -> Latn - {0x82AC0000u, 45u}, // mva -> Latn - {0xB6AC0000u, 45u}, // mvn -> Latn + {0x828C0000u, 46u}, // mua -> Latn + {0xC68C0000u, 46u}, // mur -> Latn + {0xCA8C0000u, 46u}, // mus -> Latn + {0x82AC0000u, 46u}, // mva -> Latn + {0xB6AC0000u, 46u}, // mvn -> Latn {0xE2AC0000u, 2u}, // mvy -> Arab - {0xAACC0000u, 45u}, // mwk -> Latn + {0xAACC0000u, 46u}, // mwk -> Latn {0xC6CC0000u, 19u}, // mwr -> Deva - {0xD6CC0000u, 45u}, // mwv -> Latn + {0xD6CC0000u, 46u}, // mwv -> Latn {0xDACC0000u, 33u}, // mww -> Hmnp - {0x8AEC0000u, 45u}, // mxc -> Latn - {0xB2EC0000u, 45u}, // mxm -> Latn - {0x6D790000u, 58u}, // my -> Mymr - {0xAB0C0000u, 45u}, // myk -> Latn + {0x8AEC0000u, 46u}, // mxc -> Latn + {0xB2EC0000u, 46u}, // mxm -> Latn + {0x6D790000u, 59u}, // my -> Mymr + {0xAB0C0000u, 46u}, // myk -> Latn {0xB30C0000u, 21u}, // mym -> Ethi {0xD70C0000u, 18u}, // myv -> Cyrl - {0xDB0C0000u, 45u}, // myw -> Latn - {0xDF0C0000u, 45u}, // myx -> Latn - {0xE70C0000u, 51u}, // myz -> Mand - {0xAB2C0000u, 45u}, // mzk -> Latn - {0xB32C0000u, 45u}, // mzm -> Latn + {0xDB0C0000u, 46u}, // myw -> Latn + {0xDF0C0000u, 46u}, // myx -> Latn + {0xE70C0000u, 52u}, // myz -> Mand + {0xAB2C0000u, 46u}, // mzk -> Latn + {0xB32C0000u, 46u}, // mzm -> Latn {0xB72C0000u, 2u}, // mzn -> Arab - {0xBF2C0000u, 45u}, // mzp -> Latn - {0xDB2C0000u, 45u}, // mzw -> Latn - {0xE72C0000u, 45u}, // mzz -> Latn - {0x6E610000u, 45u}, // na -> Latn - {0x880D0000u, 45u}, // nac -> Latn - {0x940D0000u, 45u}, // naf -> Latn - {0xA80D0000u, 45u}, // nak -> Latn + {0xBF2C0000u, 46u}, // mzp -> Latn + {0xDB2C0000u, 46u}, // mzw -> Latn + {0xE72C0000u, 46u}, // mzz -> Latn + {0x6E610000u, 46u}, // na -> Latn + {0x880D0000u, 46u}, // nac -> Latn + {0x940D0000u, 46u}, // naf -> Latn + {0xA80D0000u, 46u}, // nak -> Latn {0xB40D0000u, 29u}, // nan -> Hans - {0xBC0D0000u, 45u}, // nap -> Latn - {0xC00D0000u, 45u}, // naq -> Latn - {0xC80D0000u, 45u}, // nas -> Latn - {0x6E620000u, 45u}, // nb -> Latn - {0x804D0000u, 45u}, // nca -> Latn - {0x904D0000u, 45u}, // nce -> Latn - {0x944D0000u, 45u}, // ncf -> Latn - {0x9C4D0000u, 45u}, // nch -> Latn - {0xB84D0000u, 45u}, // nco -> Latn - {0xD04D0000u, 45u}, // ncu -> Latn - {0x6E640000u, 45u}, // nd -> Latn - {0x886D0000u, 45u}, // ndc -> Latn - {0xC86D0000u, 45u}, // nds -> Latn + {0xBC0D0000u, 46u}, // nap -> Latn + {0xC00D0000u, 46u}, // naq -> Latn + {0xC80D0000u, 46u}, // nas -> Latn + {0x6E620000u, 46u}, // nb -> Latn + {0x804D0000u, 46u}, // nca -> Latn + {0x904D0000u, 46u}, // nce -> Latn + {0x944D0000u, 46u}, // ncf -> Latn + {0x9C4D0000u, 46u}, // nch -> Latn + {0xB84D0000u, 46u}, // nco -> Latn + {0xD04D0000u, 46u}, // ncu -> Latn + {0x6E640000u, 46u}, // nd -> Latn + {0x886D0000u, 46u}, // ndc -> Latn + {0xC86D0000u, 46u}, // nds -> Latn {0x6E650000u, 19u}, // ne -> Deva - {0x848D0000u, 45u}, // neb -> Latn + {0x848D0000u, 46u}, // neb -> Latn {0xD88D0000u, 19u}, // new -> Deva - {0xDC8D0000u, 45u}, // nex -> Latn - {0xC4AD0000u, 45u}, // nfr -> Latn - {0x6E670000u, 45u}, // ng -> Latn - {0x80CD0000u, 45u}, // nga -> Latn - {0x84CD0000u, 45u}, // ngb -> Latn - {0xACCD0000u, 45u}, // ngl -> Latn - {0x84ED0000u, 45u}, // nhb -> Latn - {0x90ED0000u, 45u}, // nhe -> Latn - {0xD8ED0000u, 45u}, // nhw -> Latn - {0x950D0000u, 45u}, // nif -> Latn - {0xA10D0000u, 45u}, // nii -> Latn - {0xA50D0000u, 45u}, // nij -> Latn - {0xB50D0000u, 45u}, // nin -> Latn - {0xD10D0000u, 45u}, // niu -> Latn - {0xE10D0000u, 45u}, // niy -> Latn - {0xE50D0000u, 45u}, // niz -> Latn - {0xB92D0000u, 45u}, // njo -> Latn - {0x994D0000u, 45u}, // nkg -> Latn - {0xB94D0000u, 45u}, // nko -> Latn - {0x6E6C0000u, 45u}, // nl -> Latn - {0x998D0000u, 45u}, // nmg -> Latn - {0xE58D0000u, 45u}, // nmz -> Latn - {0x6E6E0000u, 45u}, // nn -> Latn - {0x95AD0000u, 45u}, // nnf -> Latn - {0x9DAD0000u, 45u}, // nnh -> Latn - {0xA9AD0000u, 45u}, // nnk -> Latn - {0xB1AD0000u, 45u}, // nnm -> Latn - {0xBDAD0000u, 98u}, // nnp -> Wcho - {0x6E6F0000u, 45u}, // no -> Latn - {0x8DCD0000u, 43u}, // nod -> Lana + {0xDC8D0000u, 46u}, // nex -> Latn + {0xC4AD0000u, 46u}, // nfr -> Latn + {0x6E670000u, 46u}, // ng -> Latn + {0x80CD0000u, 46u}, // nga -> Latn + {0x84CD0000u, 46u}, // ngb -> Latn + {0xACCD0000u, 46u}, // ngl -> Latn + {0x84ED0000u, 46u}, // nhb -> Latn + {0x90ED0000u, 46u}, // nhe -> Latn + {0xD8ED0000u, 46u}, // nhw -> Latn + {0x950D0000u, 46u}, // nif -> Latn + {0xA10D0000u, 46u}, // nii -> Latn + {0xA50D0000u, 46u}, // nij -> Latn + {0xB50D0000u, 46u}, // nin -> Latn + {0xD10D0000u, 46u}, // niu -> Latn + {0xE10D0000u, 46u}, // niy -> Latn + {0xE50D0000u, 46u}, // niz -> Latn + {0xB92D0000u, 46u}, // njo -> Latn + {0x994D0000u, 46u}, // nkg -> Latn + {0xB94D0000u, 46u}, // nko -> Latn + {0x6E6C0000u, 46u}, // nl -> Latn + {0x998D0000u, 46u}, // nmg -> Latn + {0xE58D0000u, 46u}, // nmz -> Latn + {0x6E6E0000u, 46u}, // nn -> Latn + {0x95AD0000u, 46u}, // nnf -> Latn + {0x9DAD0000u, 46u}, // nnh -> Latn + {0xA9AD0000u, 46u}, // nnk -> Latn + {0xB1AD0000u, 46u}, // nnm -> Latn + {0xBDAD0000u, 99u}, // nnp -> Wcho + {0x6E6F0000u, 46u}, // no -> Latn + {0x8DCD0000u, 44u}, // nod -> Lana {0x91CD0000u, 19u}, // noe -> Deva - {0xB5CD0000u, 74u}, // non -> Runr - {0xBDCD0000u, 45u}, // nop -> Latn - {0xD1CD0000u, 45u}, // nou -> Latn - {0xBA0D0000u, 60u}, // nqo -> Nkoo - {0x6E720000u, 45u}, // nr -> Latn - {0x862D0000u, 45u}, // nrb -> Latn + {0xB5CD0000u, 75u}, // non -> Runr + {0xBDCD0000u, 46u}, // nop -> Latn + {0xD1CD0000u, 46u}, // nou -> Latn + {0xBA0D0000u, 61u}, // nqo -> Nkoo + {0x6E720000u, 46u}, // nr -> Latn + {0x862D0000u, 46u}, // nrb -> Latn {0xAA4D0000u, 11u}, // nsk -> Cans - {0xB64D0000u, 45u}, // nsn -> Latn - {0xBA4D0000u, 45u}, // nso -> Latn - {0xCA4D0000u, 45u}, // nss -> Latn - {0xCE4D0000u, 94u}, // nst -> Tnsa - {0xB26D0000u, 45u}, // ntm -> Latn - {0xC66D0000u, 45u}, // ntr -> Latn - {0xA28D0000u, 45u}, // nui -> Latn - {0xBE8D0000u, 45u}, // nup -> Latn - {0xCA8D0000u, 45u}, // nus -> Latn - {0xD68D0000u, 45u}, // nuv -> Latn - {0xDE8D0000u, 45u}, // nux -> Latn - {0x6E760000u, 45u}, // nv -> Latn - {0x86CD0000u, 45u}, // nwb -> Latn - {0xC2ED0000u, 45u}, // nxq -> Latn - {0xC6ED0000u, 45u}, // nxr -> Latn - {0x6E790000u, 45u}, // ny -> Latn - {0xB30D0000u, 45u}, // nym -> Latn - {0xB70D0000u, 45u}, // nyn -> Latn - {0xA32D0000u, 45u}, // nzi -> Latn - {0x6F630000u, 45u}, // oc -> Latn - {0x88CE0000u, 45u}, // ogc -> Latn - {0xC54E0000u, 45u}, // okr -> Latn - {0xD54E0000u, 45u}, // okv -> Latn - {0x6F6D0000u, 45u}, // om -> Latn - {0x99AE0000u, 45u}, // ong -> Latn - {0xB5AE0000u, 45u}, // onn -> Latn - {0xC9AE0000u, 45u}, // ons -> Latn - {0xB1EE0000u, 45u}, // opm -> Latn - {0x6F720000u, 65u}, // or -> Orya - {0xBA2E0000u, 45u}, // oro -> Latn + {0xB64D0000u, 46u}, // nsn -> Latn + {0xBA4D0000u, 46u}, // nso -> Latn + {0xCA4D0000u, 46u}, // nss -> Latn + {0xCE4D0000u, 95u}, // nst -> Tnsa + {0xB26D0000u, 46u}, // ntm -> Latn + {0xC66D0000u, 46u}, // ntr -> Latn + {0xA28D0000u, 46u}, // nui -> Latn + {0xBE8D0000u, 46u}, // nup -> Latn + {0xCA8D0000u, 46u}, // nus -> Latn + {0xD68D0000u, 46u}, // nuv -> Latn + {0xDE8D0000u, 46u}, // nux -> Latn + {0x6E760000u, 46u}, // nv -> Latn + {0x86CD0000u, 46u}, // nwb -> Latn + {0xC2ED0000u, 46u}, // nxq -> Latn + {0xC6ED0000u, 46u}, // nxr -> Latn + {0x6E790000u, 46u}, // ny -> Latn + {0xB30D0000u, 46u}, // nym -> Latn + {0xB70D0000u, 46u}, // nyn -> Latn + {0xA32D0000u, 46u}, // nzi -> Latn + {0x6F630000u, 46u}, // oc -> Latn + {0x88CE0000u, 46u}, // ogc -> Latn + {0x6F6A0000u, 11u}, // oj -> Cans + {0xC92E0000u, 11u}, // ojs -> Cans + {0x814E0000u, 46u}, // oka -> Latn + {0xC54E0000u, 46u}, // okr -> Latn + {0xD54E0000u, 46u}, // okv -> Latn + {0x6F6D0000u, 46u}, // om -> Latn + {0x99AE0000u, 46u}, // ong -> Latn + {0xB5AE0000u, 46u}, // onn -> Latn + {0xC9AE0000u, 46u}, // ons -> Latn + {0xB1EE0000u, 46u}, // opm -> Latn + {0x6F720000u, 66u}, // or -> Orya + {0xBA2E0000u, 46u}, // oro -> Latn {0xD22E0000u, 2u}, // oru -> Arab {0x6F730000u, 18u}, // os -> Cyrl - {0x824E0000u, 66u}, // osa -> Osge + {0x824E0000u, 67u}, // osa -> Osge {0x826E0000u, 2u}, // ota -> Arab - {0xAA6E0000u, 64u}, // otk -> Orkh - {0xA28E0000u, 67u}, // oui -> Ougr - {0xB32E0000u, 45u}, // ozm -> Latn + {0xAA6E0000u, 65u}, // otk -> Orkh + {0xA28E0000u, 68u}, // oui -> Ougr + {0xB32E0000u, 46u}, // ozm -> Latn {0x70610000u, 28u}, // pa -> Guru {0x7061504Bu, 2u}, // pa-PK -> Arab - {0x980F0000u, 45u}, // pag -> Latn - {0xAC0F0000u, 69u}, // pal -> Phli - {0xB00F0000u, 45u}, // pam -> Latn - {0xBC0F0000u, 45u}, // pap -> Latn - {0xD00F0000u, 45u}, // pau -> Latn - {0xA02F0000u, 45u}, // pbi -> Latn - {0x8C4F0000u, 45u}, // pcd -> Latn - {0xB04F0000u, 45u}, // pcm -> Latn - {0x886F0000u, 45u}, // pdc -> Latn - {0xCC6F0000u, 45u}, // pdt -> Latn - {0x8C8F0000u, 45u}, // ped -> Latn - {0xB88F0000u, 99u}, // peo -> Xpeo - {0xDC8F0000u, 45u}, // pex -> Latn - {0xACAF0000u, 45u}, // pfl -> Latn + {0x980F0000u, 46u}, // pag -> Latn + {0xAC0F0000u, 70u}, // pal -> Phli + {0xB00F0000u, 46u}, // pam -> Latn + {0xBC0F0000u, 46u}, // pap -> Latn + {0xD00F0000u, 46u}, // pau -> Latn + {0xA02F0000u, 46u}, // pbi -> Latn + {0x8C4F0000u, 46u}, // pcd -> Latn + {0xB04F0000u, 46u}, // pcm -> Latn + {0x886F0000u, 46u}, // pdc -> Latn + {0xCC6F0000u, 46u}, // pdt -> Latn + {0x8C8F0000u, 46u}, // ped -> Latn + {0xB88F0000u, 100u}, // peo -> Xpeo + {0xDC8F0000u, 46u}, // pex -> Latn + {0xACAF0000u, 46u}, // pfl -> Latn {0xACEF0000u, 2u}, // phl -> Arab - {0xB4EF0000u, 70u}, // phn -> Phnx - {0xAD0F0000u, 45u}, // pil -> Latn - {0xBD0F0000u, 45u}, // pip -> Latn + {0xB4EF0000u, 71u}, // phn -> Phnx + {0xAD0F0000u, 46u}, // pil -> Latn + {0xBD0F0000u, 46u}, // pip -> Latn {0x814F0000u, 9u}, // pka -> Brah - {0xB94F0000u, 45u}, // pko -> Latn - {0x706C0000u, 45u}, // pl -> Latn - {0x816F0000u, 45u}, // pla -> Latn - {0xC98F0000u, 45u}, // pms -> Latn - {0x99AF0000u, 45u}, // png -> Latn - {0xB5AF0000u, 45u}, // pnn -> Latn + {0xB94F0000u, 46u}, // pko -> Latn + {0x706C0000u, 46u}, // pl -> Latn + {0x816F0000u, 46u}, // pla -> Latn + {0xC98F0000u, 46u}, // pms -> Latn + {0x99AF0000u, 46u}, // png -> Latn + {0xB5AF0000u, 46u}, // pnn -> Latn {0xCDAF0000u, 26u}, // pnt -> Grek - {0xB5CF0000u, 45u}, // pon -> Latn + {0xB5CF0000u, 46u}, // pon -> Latn {0x81EF0000u, 19u}, // ppa -> Deva - {0xB9EF0000u, 45u}, // ppo -> Latn - {0x822F0000u, 38u}, // pra -> Khar + {0xB9EF0000u, 46u}, // ppo -> Latn + {0xB20F0000u, 46u}, // pqm -> Latn + {0x822F0000u, 39u}, // pra -> Khar {0x8E2F0000u, 2u}, // prd -> Arab - {0x9A2F0000u, 45u}, // prg -> Latn + {0x9A2F0000u, 46u}, // prg -> Latn {0x70730000u, 2u}, // ps -> Arab - {0xCA4F0000u, 45u}, // pss -> Latn - {0x70740000u, 45u}, // pt -> Latn - {0xBE6F0000u, 45u}, // ptp -> Latn - {0xD28F0000u, 45u}, // puu -> Latn - {0x82CF0000u, 45u}, // pwa -> Latn - {0x71750000u, 45u}, // qu -> Latn - {0x8A900000u, 45u}, // quc -> Latn - {0x9A900000u, 45u}, // qug -> Latn - {0xA0110000u, 45u}, // rai -> Latn + {0xCA4F0000u, 46u}, // pss -> Latn + {0x70740000u, 46u}, // pt -> Latn + {0xBE6F0000u, 46u}, // ptp -> Latn + {0xD28F0000u, 46u}, // puu -> Latn + {0x82CF0000u, 46u}, // pwa -> Latn + {0x71750000u, 46u}, // qu -> Latn + {0x8A900000u, 46u}, // quc -> Latn + {0x9A900000u, 46u}, // qug -> Latn + {0xA0110000u, 46u}, // rai -> Latn {0xA4110000u, 19u}, // raj -> Deva - {0xB8110000u, 45u}, // rao -> Latn - {0x94510000u, 45u}, // rcf -> Latn - {0xA4910000u, 45u}, // rej -> Latn - {0xAC910000u, 45u}, // rel -> Latn - {0xC8910000u, 45u}, // res -> Latn - {0xB4D10000u, 45u}, // rgn -> Latn - {0x98F10000u, 73u}, // rhg -> Rohg - {0x81110000u, 45u}, // ria -> Latn - {0x95110000u, 90u}, // rif -> Tfng - {0x95114E4Cu, 45u}, // rif-NL -> Latn + {0xB8110000u, 46u}, // rao -> Latn + {0x94510000u, 46u}, // rcf -> Latn + {0xA4910000u, 46u}, // rej -> Latn + {0xAC910000u, 46u}, // rel -> Latn + {0xC8910000u, 46u}, // res -> Latn + {0xB4D10000u, 46u}, // rgn -> Latn + {0x98F10000u, 74u}, // rhg -> Rohg + {0x81110000u, 46u}, // ria -> Latn + {0x95110000u, 91u}, // rif -> Tfng + {0x95114E4Cu, 46u}, // rif-NL -> Latn {0xC9310000u, 19u}, // rjs -> Deva {0xCD510000u, 8u}, // rkt -> Beng - {0x726D0000u, 45u}, // rm -> Latn - {0x95910000u, 45u}, // rmf -> Latn - {0xB9910000u, 45u}, // rmo -> Latn + {0x726D0000u, 46u}, // rm -> Latn + {0x95910000u, 46u}, // rmf -> Latn + {0xB9910000u, 46u}, // rmo -> Latn {0xCD910000u, 2u}, // rmt -> Arab - {0xD1910000u, 45u}, // rmu -> Latn - {0x726E0000u, 45u}, // rn -> Latn - {0x81B10000u, 45u}, // rna -> Latn - {0x99B10000u, 45u}, // rng -> Latn - {0x726F0000u, 45u}, // ro -> Latn - {0x85D10000u, 45u}, // rob -> Latn - {0x95D10000u, 45u}, // rof -> Latn - {0xB9D10000u, 45u}, // roo -> Latn - {0xBA310000u, 45u}, // rro -> Latn - {0xB2710000u, 45u}, // rtm -> Latn + {0xD1910000u, 46u}, // rmu -> Latn + {0x726E0000u, 46u}, // rn -> Latn + {0x81B10000u, 46u}, // rna -> Latn + {0x99B10000u, 46u}, // rng -> Latn + {0x726F0000u, 46u}, // ro -> Latn + {0x85D10000u, 46u}, // rob -> Latn + {0x95D10000u, 46u}, // rof -> Latn + {0xB9D10000u, 46u}, // roo -> Latn + {0xBA310000u, 46u}, // rro -> Latn + {0xB2710000u, 46u}, // rtm -> Latn {0x72750000u, 18u}, // ru -> Cyrl {0x92910000u, 18u}, // rue -> Cyrl - {0x9A910000u, 45u}, // rug -> Latn - {0x72770000u, 45u}, // rw -> Latn - {0xAAD10000u, 45u}, // rwk -> Latn - {0xBAD10000u, 45u}, // rwo -> Latn + {0x9A910000u, 46u}, // rug -> Latn + {0x72770000u, 46u}, // rw -> Latn + {0xAAD10000u, 46u}, // rwk -> Latn + {0xBAD10000u, 46u}, // rwo -> Latn {0xD3110000u, 37u}, // ryu -> Kana {0x73610000u, 19u}, // sa -> Deva - {0x94120000u, 45u}, // saf -> Latn + {0x94120000u, 46u}, // saf -> Latn {0x9C120000u, 18u}, // sah -> Cyrl - {0xC0120000u, 45u}, // saq -> Latn - {0xC8120000u, 45u}, // sas -> Latn - {0xCC120000u, 63u}, // sat -> Olck - {0xD4120000u, 45u}, // sav -> Latn - {0xE4120000u, 77u}, // saz -> Saur - {0x80320000u, 45u}, // sba -> Latn - {0x90320000u, 45u}, // sbe -> Latn - {0xBC320000u, 45u}, // sbp -> Latn - {0x73630000u, 45u}, // sc -> Latn + {0xC0120000u, 46u}, // saq -> Latn + {0xC8120000u, 46u}, // sas -> Latn + {0xCC120000u, 64u}, // sat -> Olck + {0xD4120000u, 46u}, // sav -> Latn + {0xE4120000u, 78u}, // saz -> Saur + {0x80320000u, 46u}, // sba -> Latn + {0x90320000u, 46u}, // sbe -> Latn + {0xBC320000u, 46u}, // sbp -> Latn + {0x73630000u, 46u}, // sc -> Latn {0xA8520000u, 19u}, // sck -> Deva {0xAC520000u, 2u}, // scl -> Arab - {0xB4520000u, 45u}, // scn -> Latn - {0xB8520000u, 45u}, // sco -> Latn - {0xC8520000u, 45u}, // scs -> Latn + {0xB4520000u, 46u}, // scn -> Latn + {0xB8520000u, 46u}, // sco -> Latn {0x73640000u, 2u}, // sd -> Arab - {0x88720000u, 45u}, // sdc -> Latn + {0x7364494Eu, 19u}, // sd-IN -> Deva + {0x88720000u, 46u}, // sdc -> Latn {0x9C720000u, 2u}, // sdh -> Arab - {0x73650000u, 45u}, // se -> Latn - {0x94920000u, 45u}, // sef -> Latn - {0x9C920000u, 45u}, // seh -> Latn - {0xA0920000u, 45u}, // sei -> Latn - {0xC8920000u, 45u}, // ses -> Latn - {0x73670000u, 45u}, // sg -> Latn - {0x80D20000u, 62u}, // sga -> Ogam - {0xC8D20000u, 45u}, // sgs -> Latn + {0x73650000u, 46u}, // se -> Latn + {0x94920000u, 46u}, // sef -> Latn + {0x9C920000u, 46u}, // seh -> Latn + {0xA0920000u, 46u}, // sei -> Latn + {0xC8920000u, 46u}, // ses -> Latn + {0x73670000u, 46u}, // sg -> Latn + {0x80D20000u, 63u}, // sga -> Ogam + {0xC8D20000u, 46u}, // sgs -> Latn {0xD8D20000u, 21u}, // sgw -> Ethi - {0xE4D20000u, 45u}, // sgz -> Latn - {0x73680000u, 45u}, // sh -> Latn - {0xA0F20000u, 90u}, // shi -> Tfng - {0xA8F20000u, 45u}, // shk -> Latn - {0xB4F20000u, 58u}, // shn -> Mymr + {0xE4D20000u, 46u}, // sgz -> Latn + {0x73680000u, 46u}, // sh -> Latn + {0xA0F20000u, 91u}, // shi -> Tfng + {0xA8F20000u, 46u}, // shk -> Latn + {0xB4F20000u, 59u}, // shn -> Mymr {0xD0F20000u, 2u}, // shu -> Arab - {0x73690000u, 79u}, // si -> Sinh - {0x8D120000u, 45u}, // sid -> Latn - {0x99120000u, 45u}, // sig -> Latn - {0xAD120000u, 45u}, // sil -> Latn - {0xB1120000u, 45u}, // sim -> Latn - {0xC5320000u, 45u}, // sjr -> Latn - {0x736B0000u, 45u}, // sk -> Latn - {0x89520000u, 45u}, // skc -> Latn + {0x73690000u, 80u}, // si -> Sinh + {0x8D120000u, 46u}, // sid -> Latn + {0x99120000u, 46u}, // sig -> Latn + {0xAD120000u, 46u}, // sil -> Latn + {0xB1120000u, 46u}, // sim -> Latn + {0xC5320000u, 46u}, // sjr -> Latn + {0x736B0000u, 46u}, // sk -> Latn + {0x89520000u, 46u}, // skc -> Latn {0xC5520000u, 2u}, // skr -> Arab - {0xC9520000u, 45u}, // sks -> Latn - {0x736C0000u, 45u}, // sl -> Latn - {0x8D720000u, 45u}, // sld -> Latn - {0xA1720000u, 45u}, // sli -> Latn - {0xAD720000u, 45u}, // sll -> Latn - {0xE1720000u, 45u}, // sly -> Latn - {0x736D0000u, 45u}, // sm -> Latn - {0x81920000u, 45u}, // sma -> Latn - {0xA5920000u, 45u}, // smj -> Latn - {0xB5920000u, 45u}, // smn -> Latn - {0xBD920000u, 75u}, // smp -> Samr - {0xC1920000u, 45u}, // smq -> Latn - {0xC9920000u, 45u}, // sms -> Latn - {0x736E0000u, 45u}, // sn -> Latn - {0x89B20000u, 45u}, // snc -> Latn - {0xA9B20000u, 45u}, // snk -> Latn - {0xBDB20000u, 45u}, // snp -> Latn - {0xDDB20000u, 45u}, // snx -> Latn - {0xE1B20000u, 45u}, // sny -> Latn - {0x736F0000u, 45u}, // so -> Latn - {0x99D20000u, 80u}, // sog -> Sogd - {0xA9D20000u, 45u}, // sok -> Latn - {0xC1D20000u, 45u}, // soq -> Latn - {0xD1D20000u, 92u}, // sou -> Thai - {0xE1D20000u, 45u}, // soy -> Latn - {0x8DF20000u, 45u}, // spd -> Latn - {0xADF20000u, 45u}, // spl -> Latn - {0xC9F20000u, 45u}, // sps -> Latn - {0x73710000u, 45u}, // sq -> Latn + {0xC9520000u, 46u}, // sks -> Latn + {0x736C0000u, 46u}, // sl -> Latn + {0x8D720000u, 46u}, // sld -> Latn + {0xA1720000u, 46u}, // sli -> Latn + {0xAD720000u, 46u}, // sll -> Latn + {0xE1720000u, 46u}, // sly -> Latn + {0x736D0000u, 46u}, // sm -> Latn + {0x81920000u, 46u}, // sma -> Latn + {0xA5920000u, 46u}, // smj -> Latn + {0xB5920000u, 46u}, // smn -> Latn + {0xBD920000u, 76u}, // smp -> Samr + {0xC1920000u, 46u}, // smq -> Latn + {0xC9920000u, 46u}, // sms -> Latn + {0x736E0000u, 46u}, // sn -> Latn + {0x89B20000u, 46u}, // snc -> Latn + {0xA9B20000u, 46u}, // snk -> Latn + {0xBDB20000u, 46u}, // snp -> Latn + {0xDDB20000u, 46u}, // snx -> Latn + {0xE1B20000u, 46u}, // sny -> Latn + {0x736F0000u, 46u}, // so -> Latn + {0x99D20000u, 81u}, // sog -> Sogd + {0xA9D20000u, 46u}, // sok -> Latn + {0xC1D20000u, 46u}, // soq -> Latn + {0xD1D20000u, 93u}, // sou -> Thai + {0xE1D20000u, 46u}, // soy -> Latn + {0x8DF20000u, 46u}, // spd -> Latn + {0xADF20000u, 46u}, // spl -> Latn + {0xC9F20000u, 46u}, // sps -> Latn + {0x73710000u, 46u}, // sq -> Latn {0x73720000u, 18u}, // sr -> Cyrl - {0x73724D45u, 45u}, // sr-ME -> Latn - {0x7372524Fu, 45u}, // sr-RO -> Latn - {0x73725255u, 45u}, // sr-RU -> Latn - {0x73725452u, 45u}, // sr-TR -> Latn - {0x86320000u, 81u}, // srb -> Sora - {0xB6320000u, 45u}, // srn -> Latn - {0xC6320000u, 45u}, // srr -> Latn + {0x73724D45u, 46u}, // sr-ME -> Latn + {0x7372524Fu, 46u}, // sr-RO -> Latn + {0x73725255u, 46u}, // sr-RU -> Latn + {0x73725452u, 46u}, // sr-TR -> Latn + {0x86320000u, 82u}, // srb -> Sora + {0xB6320000u, 46u}, // srn -> Latn + {0xC6320000u, 46u}, // srr -> Latn {0xDE320000u, 19u}, // srx -> Deva - {0x73730000u, 45u}, // ss -> Latn - {0x8E520000u, 45u}, // ssd -> Latn - {0x9A520000u, 45u}, // ssg -> Latn - {0xE2520000u, 45u}, // ssy -> Latn - {0x73740000u, 45u}, // st -> Latn - {0xAA720000u, 45u}, // stk -> Latn - {0xC2720000u, 45u}, // stq -> Latn - {0x73750000u, 45u}, // su -> Latn - {0x82920000u, 45u}, // sua -> Latn - {0x92920000u, 45u}, // sue -> Latn - {0xAA920000u, 45u}, // suk -> Latn - {0xC6920000u, 45u}, // sur -> Latn - {0xCA920000u, 45u}, // sus -> Latn - {0x73760000u, 45u}, // sv -> Latn - {0x73770000u, 45u}, // sw -> Latn + {0x73730000u, 46u}, // ss -> Latn + {0x8E520000u, 46u}, // ssd -> Latn + {0x9A520000u, 46u}, // ssg -> Latn + {0xE2520000u, 46u}, // ssy -> Latn + {0x73740000u, 46u}, // st -> Latn + {0xAA720000u, 46u}, // stk -> Latn + {0xC2720000u, 46u}, // stq -> Latn + {0x73750000u, 46u}, // su -> Latn + {0x82920000u, 46u}, // sua -> Latn + {0x92920000u, 46u}, // sue -> Latn + {0xAA920000u, 46u}, // suk -> Latn + {0xC6920000u, 46u}, // sur -> Latn + {0xCA920000u, 46u}, // sus -> Latn + {0x73760000u, 46u}, // sv -> Latn + {0x73770000u, 46u}, // sw -> Latn {0x86D20000u, 2u}, // swb -> Arab - {0x8AD20000u, 45u}, // swc -> Latn - {0x9AD20000u, 45u}, // swg -> Latn - {0xBED20000u, 45u}, // swp -> Latn + {0x8AD20000u, 46u}, // swc -> Latn + {0x9AD20000u, 46u}, // swg -> Latn + {0xBED20000u, 46u}, // swp -> Latn {0xD6D20000u, 19u}, // swv -> Deva - {0xB6F20000u, 45u}, // sxn -> Latn - {0xDAF20000u, 45u}, // sxw -> Latn + {0xB6F20000u, 46u}, // sxn -> Latn + {0xDAF20000u, 46u}, // sxw -> Latn {0xAF120000u, 8u}, // syl -> Beng - {0xC7120000u, 83u}, // syr -> Syrc - {0xAF320000u, 45u}, // szl -> Latn - {0x74610000u, 86u}, // ta -> Taml + {0xC7120000u, 84u}, // syr -> Syrc + {0xAF320000u, 46u}, // szl -> Latn + {0x74610000u, 87u}, // ta -> Taml {0xA4130000u, 19u}, // taj -> Deva - {0xAC130000u, 45u}, // tal -> Latn - {0xB4130000u, 45u}, // tan -> Latn - {0xC0130000u, 45u}, // taq -> Latn - {0x88330000u, 45u}, // tbc -> Latn - {0x8C330000u, 45u}, // tbd -> Latn - {0x94330000u, 45u}, // tbf -> Latn - {0x98330000u, 45u}, // tbg -> Latn - {0xB8330000u, 45u}, // tbo -> Latn - {0xD8330000u, 45u}, // tbw -> Latn - {0xE4330000u, 45u}, // tbz -> Latn - {0xA0530000u, 45u}, // tci -> Latn - {0xE0530000u, 41u}, // tcy -> Knda - {0x8C730000u, 84u}, // tdd -> Tale + {0xAC130000u, 46u}, // tal -> Latn + {0xB4130000u, 46u}, // tan -> Latn + {0xC0130000u, 46u}, // taq -> Latn + {0x88330000u, 46u}, // tbc -> Latn + {0x8C330000u, 46u}, // tbd -> Latn + {0x94330000u, 46u}, // tbf -> Latn + {0x98330000u, 46u}, // tbg -> Latn + {0xB8330000u, 46u}, // tbo -> Latn + {0xD8330000u, 46u}, // tbw -> Latn + {0xE4330000u, 46u}, // tbz -> Latn + {0xA0530000u, 46u}, // tci -> Latn + {0xE0530000u, 42u}, // tcy -> Knda + {0x8C730000u, 85u}, // tdd -> Tale {0x98730000u, 19u}, // tdg -> Deva {0x9C730000u, 19u}, // tdh -> Deva - {0xD0730000u, 45u}, // tdu -> Latn - {0x74650000u, 89u}, // te -> Telu - {0x8C930000u, 45u}, // ted -> Latn - {0xB0930000u, 45u}, // tem -> Latn - {0xB8930000u, 45u}, // teo -> Latn - {0xCC930000u, 45u}, // tet -> Latn - {0xA0B30000u, 45u}, // tfi -> Latn + {0xD0730000u, 46u}, // tdu -> Latn + {0x74650000u, 90u}, // te -> Telu + {0x8C930000u, 46u}, // ted -> Latn + {0xB0930000u, 46u}, // tem -> Latn + {0xB8930000u, 46u}, // teo -> Latn + {0xCC930000u, 46u}, // tet -> Latn + {0xA0B30000u, 46u}, // tfi -> Latn {0x74670000u, 18u}, // tg -> Cyrl {0x7467504Bu, 2u}, // tg-PK -> Arab - {0x88D30000u, 45u}, // tgc -> Latn - {0xB8D30000u, 45u}, // tgo -> Latn - {0xD0D30000u, 45u}, // tgu -> Latn - {0x74680000u, 92u}, // th -> Thai + {0x88D30000u, 46u}, // tgc -> Latn + {0xB8D30000u, 46u}, // tgo -> Latn + {0xD0D30000u, 46u}, // tgu -> Latn + {0x74680000u, 93u}, // th -> Thai {0xACF30000u, 19u}, // thl -> Deva {0xC0F30000u, 19u}, // thq -> Deva {0xC4F30000u, 19u}, // thr -> Deva {0x74690000u, 21u}, // ti -> Ethi - {0x95130000u, 45u}, // tif -> Latn + {0x95130000u, 46u}, // tif -> Latn {0x99130000u, 21u}, // tig -> Ethi - {0xA9130000u, 45u}, // tik -> Latn - {0xB1130000u, 45u}, // tim -> Latn - {0xB9130000u, 45u}, // tio -> Latn - {0xD5130000u, 45u}, // tiv -> Latn - {0x746B0000u, 45u}, // tk -> Latn - {0xAD530000u, 45u}, // tkl -> Latn - {0xC5530000u, 45u}, // tkr -> Latn + {0xA9130000u, 46u}, // tik -> Latn + {0xB1130000u, 46u}, // tim -> Latn + {0xB9130000u, 46u}, // tio -> Latn + {0xD5130000u, 46u}, // tiv -> Latn + {0x746B0000u, 46u}, // tk -> Latn + {0xAD530000u, 46u}, // tkl -> Latn + {0xC5530000u, 46u}, // tkr -> Latn {0xCD530000u, 19u}, // tkt -> Deva - {0x746C0000u, 45u}, // tl -> Latn - {0x95730000u, 45u}, // tlf -> Latn - {0xDD730000u, 45u}, // tlx -> Latn - {0xE1730000u, 45u}, // tly -> Latn - {0x9D930000u, 45u}, // tmh -> Latn - {0xE1930000u, 45u}, // tmy -> Latn - {0x746E0000u, 45u}, // tn -> Latn - {0x9DB30000u, 45u}, // tnh -> Latn - {0x746F0000u, 45u}, // to -> Latn - {0x95D30000u, 45u}, // tof -> Latn - {0x99D30000u, 45u}, // tog -> Latn - {0xC1D30000u, 45u}, // toq -> Latn - {0xA1F30000u, 45u}, // tpi -> Latn - {0xB1F30000u, 45u}, // tpm -> Latn - {0xE5F30000u, 45u}, // tpz -> Latn - {0xBA130000u, 45u}, // tqo -> Latn - {0x74720000u, 45u}, // tr -> Latn - {0xD2330000u, 45u}, // tru -> Latn - {0xD6330000u, 45u}, // trv -> Latn + {0x746C0000u, 46u}, // tl -> Latn + {0x95730000u, 46u}, // tlf -> Latn + {0xDD730000u, 46u}, // tlx -> Latn + {0xE1730000u, 46u}, // tly -> Latn + {0x9D930000u, 46u}, // tmh -> Latn + {0xE1930000u, 46u}, // tmy -> Latn + {0x746E0000u, 46u}, // tn -> Latn + {0x9DB30000u, 46u}, // tnh -> Latn + {0x746F0000u, 46u}, // to -> Latn + {0x95D30000u, 46u}, // tof -> Latn + {0x99D30000u, 46u}, // tog -> Latn + {0xC1D30000u, 46u}, // toq -> Latn + {0xA1F30000u, 46u}, // tpi -> Latn + {0xB1F30000u, 46u}, // tpm -> Latn + {0xE5F30000u, 46u}, // tpz -> Latn + {0xBA130000u, 46u}, // tqo -> Latn + {0x74720000u, 46u}, // tr -> Latn + {0xD2330000u, 46u}, // tru -> Latn + {0xD6330000u, 46u}, // trv -> Latn {0xDA330000u, 2u}, // trw -> Arab - {0x74730000u, 45u}, // ts -> Latn + {0x74730000u, 46u}, // ts -> Latn {0x8E530000u, 26u}, // tsd -> Grek {0x96530000u, 19u}, // tsf -> Deva - {0x9A530000u, 45u}, // tsg -> Latn - {0xA6530000u, 93u}, // tsj -> Tibt - {0xDA530000u, 45u}, // tsw -> Latn + {0x9A530000u, 46u}, // tsg -> Latn + {0xA6530000u, 94u}, // tsj -> Tibt + {0xDA530000u, 46u}, // tsw -> Latn {0x74740000u, 18u}, // tt -> Cyrl - {0x8E730000u, 45u}, // ttd -> Latn - {0x92730000u, 45u}, // tte -> Latn - {0xA6730000u, 45u}, // ttj -> Latn - {0xC6730000u, 45u}, // ttr -> Latn - {0xCA730000u, 92u}, // tts -> Thai - {0xCE730000u, 45u}, // ttt -> Latn - {0x9E930000u, 45u}, // tuh -> Latn - {0xAE930000u, 45u}, // tul -> Latn - {0xB2930000u, 45u}, // tum -> Latn - {0xC2930000u, 45u}, // tuq -> Latn - {0x8EB30000u, 45u}, // tvd -> Latn - {0xAEB30000u, 45u}, // tvl -> Latn - {0xD2B30000u, 45u}, // tvu -> Latn - {0x9ED30000u, 45u}, // twh -> Latn - {0xC2D30000u, 45u}, // twq -> Latn - {0x9AF30000u, 87u}, // txg -> Tang - {0xBAF30000u, 95u}, // txo -> Toto - {0x74790000u, 45u}, // ty -> Latn - {0x83130000u, 45u}, // tya -> Latn + {0x8E730000u, 46u}, // ttd -> Latn + {0x92730000u, 46u}, // tte -> Latn + {0xA6730000u, 46u}, // ttj -> Latn + {0xC6730000u, 46u}, // ttr -> Latn + {0xCA730000u, 93u}, // tts -> Thai + {0xCE730000u, 46u}, // ttt -> Latn + {0x9E930000u, 46u}, // tuh -> Latn + {0xAE930000u, 46u}, // tul -> Latn + {0xB2930000u, 46u}, // tum -> Latn + {0xC2930000u, 46u}, // tuq -> Latn + {0x8EB30000u, 46u}, // tvd -> Latn + {0xAEB30000u, 46u}, // tvl -> Latn + {0xD2B30000u, 46u}, // tvu -> Latn + {0x9ED30000u, 46u}, // twh -> Latn + {0xC2D30000u, 46u}, // twq -> Latn + {0x9AF30000u, 88u}, // txg -> Tang + {0xBAF30000u, 96u}, // txo -> Toto + {0x74790000u, 46u}, // ty -> Latn + {0x83130000u, 46u}, // tya -> Latn {0xD7130000u, 18u}, // tyv -> Cyrl - {0xB3330000u, 45u}, // tzm -> Latn - {0xD0340000u, 45u}, // ubu -> Latn + {0xB3330000u, 46u}, // tzm -> Latn + {0xD0340000u, 46u}, // ubu -> Latn {0xA0740000u, 0u}, // udi -> Aghb {0xB0740000u, 18u}, // udm -> Cyrl {0x75670000u, 2u}, // ug -> Arab {0x75674B5Au, 18u}, // ug-KZ -> Cyrl {0x75674D4Eu, 18u}, // ug-MN -> Cyrl - {0x80D40000u, 96u}, // uga -> Ugar + {0x80D40000u, 97u}, // uga -> Ugar {0x756B0000u, 18u}, // uk -> Cyrl - {0xA1740000u, 45u}, // uli -> Latn - {0x85940000u, 45u}, // umb -> Latn + {0xA1740000u, 46u}, // uli -> Latn + {0x85940000u, 46u}, // umb -> Latn {0xC5B40000u, 8u}, // unr -> Beng {0xC5B44E50u, 19u}, // unr-NP -> Deva {0xDDB40000u, 8u}, // unx -> Beng - {0xA9D40000u, 45u}, // uok -> Latn + {0xA9D40000u, 46u}, // uok -> Latn {0x75720000u, 2u}, // ur -> Arab - {0xA2340000u, 45u}, // uri -> Latn - {0xCE340000u, 45u}, // urt -> Latn - {0xDA340000u, 45u}, // urw -> Latn - {0x82540000u, 45u}, // usa -> Latn - {0x9E740000u, 45u}, // uth -> Latn - {0xC6740000u, 45u}, // utr -> Latn - {0x9EB40000u, 45u}, // uvh -> Latn - {0xAEB40000u, 45u}, // uvl -> Latn - {0x757A0000u, 45u}, // uz -> Latn + {0xA2340000u, 46u}, // uri -> Latn + {0xCE340000u, 46u}, // urt -> Latn + {0xDA340000u, 46u}, // urw -> Latn + {0x82540000u, 46u}, // usa -> Latn + {0x9E740000u, 46u}, // uth -> Latn + {0xC6740000u, 46u}, // utr -> Latn + {0x9EB40000u, 46u}, // uvh -> Latn + {0xAEB40000u, 46u}, // uvl -> Latn + {0x757A0000u, 46u}, // uz -> Latn {0x757A4146u, 2u}, // uz-AF -> Arab {0x757A434Eu, 18u}, // uz-CN -> Cyrl - {0x98150000u, 45u}, // vag -> Latn - {0xA0150000u, 97u}, // vai -> Vaii - {0xB4150000u, 45u}, // van -> Latn - {0x76650000u, 45u}, // ve -> Latn - {0x88950000u, 45u}, // vec -> Latn - {0xBC950000u, 45u}, // vep -> Latn - {0x76690000u, 45u}, // vi -> Latn - {0x89150000u, 45u}, // vic -> Latn - {0xD5150000u, 45u}, // viv -> Latn - {0xC9750000u, 45u}, // vls -> Latn - {0x95950000u, 45u}, // vmf -> Latn - {0xD9950000u, 45u}, // vmw -> Latn - {0x766F0000u, 45u}, // vo -> Latn - {0xCDD50000u, 45u}, // vot -> Latn - {0xBA350000u, 45u}, // vro -> Latn - {0xB6950000u, 45u}, // vun -> Latn - {0xCE950000u, 45u}, // vut -> Latn - {0x77610000u, 45u}, // wa -> Latn - {0x90160000u, 45u}, // wae -> Latn - {0xA4160000u, 45u}, // waj -> Latn + {0x98150000u, 46u}, // vag -> Latn + {0xA0150000u, 98u}, // vai -> Vaii + {0xB4150000u, 46u}, // van -> Latn + {0x76650000u, 46u}, // ve -> Latn + {0x88950000u, 46u}, // vec -> Latn + {0xBC950000u, 46u}, // vep -> Latn + {0x76690000u, 46u}, // vi -> Latn + {0x89150000u, 46u}, // vic -> Latn + {0xD5150000u, 46u}, // viv -> Latn + {0xC9750000u, 46u}, // vls -> Latn + {0x95950000u, 46u}, // vmf -> Latn + {0xD9950000u, 46u}, // vmw -> Latn + {0x766F0000u, 46u}, // vo -> Latn + {0xCDD50000u, 46u}, // vot -> Latn + {0xBA350000u, 46u}, // vro -> Latn + {0xB6950000u, 46u}, // vun -> Latn + {0xCE950000u, 46u}, // vut -> Latn + {0x77610000u, 46u}, // wa -> Latn + {0x90160000u, 46u}, // wae -> Latn + {0xA4160000u, 46u}, // waj -> Latn {0xAC160000u, 21u}, // wal -> Ethi - {0xB4160000u, 45u}, // wan -> Latn - {0xC4160000u, 45u}, // war -> Latn - {0xBC360000u, 45u}, // wbp -> Latn - {0xC0360000u, 89u}, // wbq -> Telu + {0xB4160000u, 46u}, // wan -> Latn + {0xC4160000u, 46u}, // war -> Latn + {0xBC360000u, 46u}, // wbp -> Latn + {0xC0360000u, 90u}, // wbq -> Telu {0xC4360000u, 19u}, // wbr -> Deva - {0xA0560000u, 45u}, // wci -> Latn - {0xC4960000u, 45u}, // wer -> Latn - {0xA0D60000u, 45u}, // wgi -> Latn - {0x98F60000u, 45u}, // whg -> Latn - {0x85160000u, 45u}, // wib -> Latn - {0xD1160000u, 45u}, // wiu -> Latn - {0xD5160000u, 45u}, // wiv -> Latn - {0x81360000u, 45u}, // wja -> Latn - {0xA1360000u, 45u}, // wji -> Latn - {0xC9760000u, 45u}, // wls -> Latn - {0xB9960000u, 45u}, // wmo -> Latn - {0x89B60000u, 45u}, // wnc -> Latn + {0xA0560000u, 46u}, // wci -> Latn + {0xC4960000u, 46u}, // wer -> Latn + {0xA0D60000u, 46u}, // wgi -> Latn + {0x98F60000u, 46u}, // whg -> Latn + {0x85160000u, 46u}, // wib -> Latn + {0xD1160000u, 46u}, // wiu -> Latn + {0xD5160000u, 46u}, // wiv -> Latn + {0x81360000u, 46u}, // wja -> Latn + {0xA1360000u, 46u}, // wji -> Latn + {0xC9760000u, 46u}, // wls -> Latn + {0xB9960000u, 46u}, // wmo -> Latn + {0x89B60000u, 46u}, // wnc -> Latn {0xA1B60000u, 2u}, // wni -> Arab - {0xD1B60000u, 45u}, // wnu -> Latn - {0x776F0000u, 45u}, // wo -> Latn - {0x85D60000u, 45u}, // wob -> Latn - {0xC9D60000u, 45u}, // wos -> Latn - {0xCA360000u, 45u}, // wrs -> Latn + {0xD1B60000u, 46u}, // wnu -> Latn + {0x776F0000u, 46u}, // wo -> Latn + {0x85D60000u, 46u}, // wob -> Latn + {0xC9D60000u, 46u}, // wos -> Latn + {0xCA360000u, 46u}, // wrs -> Latn {0x9A560000u, 23u}, // wsg -> Gong - {0xAA560000u, 45u}, // wsk -> Latn + {0xAA560000u, 46u}, // wsk -> Latn {0xB2760000u, 19u}, // wtm -> Deva {0xD2960000u, 29u}, // wuu -> Hans - {0xD6960000u, 45u}, // wuv -> Latn - {0x82D60000u, 45u}, // wwa -> Latn - {0xD4170000u, 45u}, // xav -> Latn - {0xA0370000u, 45u}, // xbi -> Latn + {0xD6960000u, 46u}, // wuv -> Latn + {0x82D60000u, 46u}, // wwa -> Latn + {0xD4170000u, 46u}, // xav -> Latn + {0xA0370000u, 46u}, // xbi -> Latn {0xB8570000u, 15u}, // xco -> Chrs {0xC4570000u, 12u}, // xcr -> Cari - {0xC8970000u, 45u}, // xes -> Latn - {0x78680000u, 45u}, // xh -> Latn - {0x81770000u, 45u}, // xla -> Latn - {0x89770000u, 49u}, // xlc -> Lyci - {0x8D770000u, 50u}, // xld -> Lydi + {0xC8970000u, 46u}, // xes -> Latn + {0x78680000u, 46u}, // xh -> Latn + {0x81770000u, 46u}, // xla -> Latn + {0x89770000u, 50u}, // xlc -> Lyci + {0x8D770000u, 51u}, // xld -> Lydi {0x95970000u, 22u}, // xmf -> Geor - {0xB5970000u, 52u}, // xmn -> Mani - {0xC5970000u, 54u}, // xmr -> Merc - {0x81B70000u, 59u}, // xna -> Narb + {0xB5970000u, 53u}, // xmn -> Mani + {0xC5970000u, 55u}, // xmr -> Merc + {0x81B70000u, 60u}, // xna -> Narb {0xC5B70000u, 19u}, // xnr -> Deva - {0x99D70000u, 45u}, // xog -> Latn - {0xB5D70000u, 45u}, // xon -> Latn - {0xC5F70000u, 72u}, // xpr -> Prti - {0x86370000u, 45u}, // xrb -> Latn - {0x82570000u, 76u}, // xsa -> Sarb - {0xA2570000u, 45u}, // xsi -> Latn - {0xB2570000u, 45u}, // xsm -> Latn + {0x99D70000u, 46u}, // xog -> Latn + {0xB5D70000u, 46u}, // xon -> Latn + {0xC5F70000u, 73u}, // xpr -> Prti + {0x86370000u, 46u}, // xrb -> Latn + {0x82570000u, 77u}, // xsa -> Sarb + {0xA2570000u, 46u}, // xsi -> Latn + {0xB2570000u, 46u}, // xsm -> Latn {0xC6570000u, 19u}, // xsr -> Deva - {0x92D70000u, 45u}, // xwe -> Latn - {0xB0180000u, 45u}, // yam -> Latn - {0xB8180000u, 45u}, // yao -> Latn - {0xBC180000u, 45u}, // yap -> Latn - {0xC8180000u, 45u}, // yas -> Latn - {0xCC180000u, 45u}, // yat -> Latn - {0xD4180000u, 45u}, // yav -> Latn - {0xE0180000u, 45u}, // yay -> Latn - {0xE4180000u, 45u}, // yaz -> Latn - {0x80380000u, 45u}, // yba -> Latn - {0x84380000u, 45u}, // ybb -> Latn - {0xE0380000u, 45u}, // yby -> Latn - {0xC4980000u, 45u}, // yer -> Latn - {0xC4D80000u, 45u}, // ygr -> Latn - {0xD8D80000u, 45u}, // ygw -> Latn + {0x92D70000u, 46u}, // xwe -> Latn + {0xB0180000u, 46u}, // yam -> Latn + {0xB8180000u, 46u}, // yao -> Latn + {0xBC180000u, 46u}, // yap -> Latn + {0xC8180000u, 46u}, // yas -> Latn + {0xCC180000u, 46u}, // yat -> Latn + {0xD4180000u, 46u}, // yav -> Latn + {0xE0180000u, 46u}, // yay -> Latn + {0xE4180000u, 46u}, // yaz -> Latn + {0x80380000u, 46u}, // yba -> Latn + {0x84380000u, 46u}, // ybb -> Latn + {0xE0380000u, 46u}, // yby -> Latn + {0xC4980000u, 46u}, // yer -> Latn + {0xC4D80000u, 46u}, // ygr -> Latn + {0xD8D80000u, 46u}, // ygw -> Latn {0x79690000u, 31u}, // yi -> Hebr - {0xB9580000u, 45u}, // yko -> Latn - {0x91780000u, 45u}, // yle -> Latn - {0x99780000u, 45u}, // ylg -> Latn - {0xAD780000u, 45u}, // yll -> Latn - {0xAD980000u, 45u}, // yml -> Latn - {0x796F0000u, 45u}, // yo -> Latn - {0xB5D80000u, 45u}, // yon -> Latn - {0x86380000u, 45u}, // yrb -> Latn - {0x92380000u, 45u}, // yre -> Latn - {0xAE380000u, 45u}, // yrl -> Latn - {0xCA580000u, 45u}, // yss -> Latn - {0x82980000u, 45u}, // yua -> Latn + {0xB9580000u, 46u}, // yko -> Latn + {0x91780000u, 46u}, // yle -> Latn + {0x99780000u, 46u}, // ylg -> Latn + {0xAD780000u, 46u}, // yll -> Latn + {0xAD980000u, 46u}, // yml -> Latn + {0x796F0000u, 46u}, // yo -> Latn + {0xB5D80000u, 46u}, // yon -> Latn + {0x86380000u, 46u}, // yrb -> Latn + {0x92380000u, 46u}, // yre -> Latn + {0xAE380000u, 46u}, // yrl -> Latn + {0xCA580000u, 46u}, // yss -> Latn + {0x82980000u, 46u}, // yua -> Latn {0x92980000u, 30u}, // yue -> Hant {0x9298434Eu, 29u}, // yue-CN -> Hans - {0xA6980000u, 45u}, // yuj -> Latn - {0xCE980000u, 45u}, // yut -> Latn - {0xDA980000u, 45u}, // yuw -> Latn - {0x7A610000u, 45u}, // za -> Latn - {0x98190000u, 45u}, // zag -> Latn + {0xA6980000u, 46u}, // yuj -> Latn + {0xCE980000u, 46u}, // yut -> Latn + {0xDA980000u, 46u}, // yuw -> Latn + {0x7A610000u, 46u}, // za -> Latn + {0x98190000u, 46u}, // zag -> Latn {0xA4790000u, 2u}, // zdj -> Arab - {0x80990000u, 45u}, // zea -> Latn - {0x9CD90000u, 90u}, // zgh -> Tfng + {0x80990000u, 46u}, // zea -> Latn + {0x9CD90000u, 91u}, // zgh -> Tfng {0x7A680000u, 29u}, // zh -> Hans {0x7A684155u, 30u}, // zh-AU -> Hant {0x7A68424Eu, 30u}, // zh-BN -> Hant @@ -1493,14 +1503,14 @@ const std::unordered_map<uint32_t, uint8_t> LIKELY_SCRIPTS({ {0x7A685457u, 30u}, // zh-TW -> Hant {0x7A685553u, 30u}, // zh-US -> Hant {0x7A68564Eu, 30u}, // zh-VN -> Hant - {0xDCF90000u, 61u}, // zhx -> Nshu - {0x81190000u, 45u}, // zia -> Latn - {0xCD590000u, 40u}, // zkt -> Kits - {0xB1790000u, 45u}, // zlm -> Latn - {0xA1990000u, 45u}, // zmi -> Latn - {0x91B90000u, 45u}, // zne -> Latn - {0x7A750000u, 45u}, // zu -> Latn - {0x83390000u, 45u}, // zza -> Latn + {0xDCF90000u, 62u}, // zhx -> Nshu + {0x81190000u, 46u}, // zia -> Latn + {0xCD590000u, 41u}, // zkt -> Kits + {0xB1790000u, 46u}, // zlm -> Latn + {0xA1990000u, 46u}, // zmi -> Latn + {0x91B90000u, 46u}, // zne -> Latn + {0x7A750000u, 46u}, // zu -> Latn + {0x83390000u, 46u}, // zza -> Latn }); std::unordered_set<uint64_t> REPRESENTATIVE_LOCALES({ @@ -1580,6 +1590,7 @@ std::unordered_set<uint64_t> REPRESENTATIVE_LOCALES({ 0xCD21534E4C61746ELLU, // bjt_Latn_SN 0xB141434D4C61746ELLU, // bkm_Latn_CM 0xD14150484C61746ELLU, // bku_Latn_PH + 0x816143414C61746ELLU, // bla_Latn_CA 0x99614D594C61746ELLU, // blg_Latn_MY 0xCD61564E54617674LLU, // blt_Tavt_VN 0x626D4D4C4C61746ELLU, // bm_Latn_ML @@ -1623,16 +1634,16 @@ std::unordered_set<uint64_t> REPRESENTATIVE_LOCALES({ 0x81224B4841726162LLU, // cja_Arab_KH 0xB122564E4368616DLLU, // cjm_Cham_VN 0x8542495141726162LLU, // ckb_Arab_IQ + 0x896243414C61746ELLU, // clc_Latn_CA 0x99824D4E536F796FLLU, // cmg_Soyo_MN 0x636F46524C61746ELLU, // co_Latn_FR 0xBDC24547436F7074LLU, // cop_Copt_EG 0xC9E250484C61746ELLU, // cps_Latn_PH 0x6372434143616E73LLU, // cr_Cans_CA + 0x9A2243414C61746ELLU, // crg_Latn_CA 0x9E2255414379726CLLU, // crh_Cyrl_UA - 0xA622434143616E73LLU, // crj_Cans_CA 0xAA22434143616E73LLU, // crk_Cans_CA 0xAE22434143616E73LLU, // crl_Cans_CA - 0xB222434143616E73LLU, // crm_Cans_CA 0xCA2253434C61746ELLU, // crs_Latn_SC 0x6373435A4C61746ELLU, // cs_Latn_CZ 0x8642504C4C61746ELLU, // csb_Latn_PL @@ -1750,6 +1761,7 @@ std::unordered_set<uint64_t> REPRESENTATIVE_LOCALES({ 0xE407414641726162LLU, // haz_Arab_AF 0x6865494C48656272LLU, // he_Hebr_IL 0x6869494E44657661LLU, // hi_Deva_IN + 0x6869494E4C61746ELLU, // hi_Latn_IN 0x9507464A4C61746ELLU, // hif_Latn_FJ 0xAD0750484C61746ELLU, // hil_Latn_PH 0xD1675452486C7577LLU, // hlu_Hluw_TR @@ -1767,6 +1779,7 @@ std::unordered_set<uint64_t> REPRESENTATIVE_LOCALES({ 0xB647434E48616E73LLU, // hsn_Hans_CN 0x687448544C61746ELLU, // ht_Latn_HT 0x687548554C61746ELLU, // hu_Latn_HU + 0xC68743414C61746ELLU, // hur_Latn_CA 0x6879414D41726D6ELLU, // hy_Armn_AM 0x687A4E414C61746ELLU, // hz_Latn_NA 0x80284D594C61746ELLU, // iba_Latn_MY @@ -1776,7 +1789,6 @@ std::unordered_set<uint64_t> REPRESENTATIVE_LOCALES({ 0x69674E474C61746ELLU, // ig_Latn_NG 0x6969434E59696969LLU, // ii_Yiii_CN 0x696B55534C61746ELLU, // ik_Latn_US - 0xCD4843414C61746ELLU, // ikt_Latn_CA 0xB96850484C61746ELLU, // ilo_Latn_PH 0x696E49444C61746ELLU, // in_Latn_ID 0x9DA852554379726CLLU, // inh_Cyrl_RU @@ -1800,6 +1812,7 @@ std::unordered_set<uint64_t> REPRESENTATIVE_LOCALES({ 0xA40A4E474C61746ELLU, // kaj_Latn_NG 0xB00A4B454C61746ELLU, // kam_Latn_KE 0xB80A4D4C4C61746ELLU, // kao_Latn_ML + 0xD80A49444B617769LLU, // kaw_Kawi_ID 0x8C2A52554379726CLLU, // kbd_Cyrl_RU 0xE02A4E4541726162LLU, // kby_Arab_NE 0x984A4E474C61746ELLU, // kcg_Latn_NG @@ -1857,6 +1870,7 @@ std::unordered_set<uint64_t> REPRESENTATIVE_LOCALES({ 0xC6AA49444C61746ELLU, // kvr_Latn_ID 0xDEAA504B41726162LLU, // kvx_Arab_PK 0x6B7747424C61746ELLU, // kw_Latn_GB + 0xAACA43414C61746ELLU, // kwk_Latn_CA 0xAEEA494E44657661LLU, // kxl_Deva_IN 0xB2EA544854686169LLU, // kxm_Thai_TH 0xBEEA504B41726162LLU, // kxp_Arab_PK @@ -1882,6 +1896,7 @@ std::unordered_set<uint64_t> REPRESENTATIVE_LOCALES({ 0x950B4E5044657661LLU, // lif_Deva_NP 0x950B494E4C696D62LLU, // lif_Limb_IN 0xA50B49544C61746ELLU, // lij_Latn_IT + 0xAD0B43414C61746ELLU, // lil_Latn_CA 0xC90B434E4C697375LLU, // lis_Lisu_CN 0xBD2B49444C61746ELLU, // ljp_Latn_ID 0xA14B495241726162LLU, // lki_Arab_IR @@ -1927,6 +1942,7 @@ std::unordered_set<uint64_t> REPRESENTATIVE_LOCALES({ 0xE0CC545A4C61746ELLU, // mgy_Latn_TZ 0x6D684D484C61746ELLU, // mh_Latn_MH 0x6D694E5A4C61746ELLU, // mi_Latn_NZ + 0x890C43414C61746ELLU, // mic_Latn_CA 0xB50C49444C61746ELLU, // min_Latn_ID 0x6D6B4D4B4379726CLLU, // mk_Cyrl_MK 0x6D6C494E4D6C796DLLU, // ml_Mlym_IN @@ -1999,6 +2015,9 @@ std::unordered_set<uint64_t> REPRESENTATIVE_LOCALES({ 0xB70D55474C61746ELLU, // nyn_Latn_UG 0xA32D47484C61746ELLU, // nzi_Latn_GH 0x6F6346524C61746ELLU, // oc_Latn_FR + 0x6F6A434143616E73LLU, // oj_Cans_CA + 0xC92E434143616E73LLU, // ojs_Cans_CA + 0x814E43414C61746ELLU, // oka_Latn_CA 0x6F6D45544C61746ELLU, // om_Latn_ET 0x6F72494E4F727961LLU, // or_Orya_IN 0x6F7347454379726CLLU, // os_Cyrl_GE @@ -2027,6 +2046,7 @@ std::unordered_set<uint64_t> REPRESENTATIVE_LOCALES({ 0xCDAF47524772656BLLU, // pnt_Grek_GR 0xB5CF464D4C61746ELLU, // pon_Latn_FM 0x81EF494E44657661LLU, // ppa_Deva_IN + 0xB20F43414C61746ELLU, // pqm_Latn_CA 0x822F504B4B686172LLU, // pra_Khar_PK 0x8E2F495241726162LLU, // prd_Arab_IR 0x7073414641726162LLU, // ps_Arab_AF @@ -2074,7 +2094,6 @@ std::unordered_set<uint64_t> REPRESENTATIVE_LOCALES({ 0xA852494E44657661LLU, // sck_Deva_IN 0xB45249544C61746ELLU, // scn_Latn_IT 0xB85247424C61746ELLU, // sco_Latn_GB - 0xC85243414C61746ELLU, // scs_Latn_CA 0x7364504B41726162LLU, // sd_Arab_PK 0x7364494E44657661LLU, // sd_Deva_IN 0x7364494E4B686F6ALLU, // sd_Khoj_IN @@ -2275,6 +2294,10 @@ const std::unordered_map<uint32_t, uint32_t> ARAB_PARENTS({ {0x6172544Eu, 0x61729420u}, // ar-TN -> ar-015 }); +const std::unordered_map<uint32_t, uint32_t> DEVA_PARENTS({ + {0x68690000u, 0x656E494Eu}, // hi-Latn -> en-IN +}); + const std::unordered_map<uint32_t, uint32_t> HANT_PARENTS({ {0x7A684D4Fu, 0x7A68484Bu}, // zh-Hant-MO -> zh-Hant-HK }); @@ -2333,6 +2356,7 @@ const std::unordered_map<uint32_t, uint32_t> LATN_PARENTS({ {0x656E4D53u, 0x656E8400u}, // en-MS -> en-001 {0x656E4D54u, 0x656E8400u}, // en-MT -> en-001 {0x656E4D55u, 0x656E8400u}, // en-MU -> en-001 + {0x656E4D56u, 0x656E8400u}, // en-MV -> en-001 {0x656E4D57u, 0x656E8400u}, // en-MW -> en-001 {0x656E4D59u, 0x656E8400u}, // en-MY -> en-001 {0x656E4E41u, 0x656E8400u}, // en-NA -> en-001 @@ -2417,6 +2441,7 @@ const struct { const std::unordered_map<uint32_t, uint32_t>* map; } SCRIPT_PARENTS[] = { {{'A', 'r', 'a', 'b'}, &ARAB_PARENTS}, + {{'D', 'e', 'v', 'a'}, &DEVA_PARENTS}, {{'H', 'a', 'n', 't'}, &HANT_PARENTS}, {{'L', 'a', 't', 'n'}, &LATN_PARENTS}, {{'~', '~', '~', 'B'}, &___B_PARENTS}, diff --git a/libs/androidfw/StringPool.cpp b/libs/androidfw/StringPool.cpp new file mode 100644 index 000000000000..b59e906f6281 --- /dev/null +++ b/libs/androidfw/StringPool.cpp @@ -0,0 +1,508 @@ +/* + * 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. + */ + +#include <androidfw/BigBuffer.h> +#include <androidfw/StringPool.h> + +#include <algorithm> +#include <memory> +#include <string> + +#include "android-base/logging.h" +#include "androidfw/ResourceTypes.h" +#include "androidfw/StringPiece.h" +#include "androidfw/Util.h" + +using ::android::StringPiece; + +namespace android { + +StringPool::Ref::Ref() : entry_(nullptr) { +} + +StringPool::Ref::Ref(const StringPool::Ref& rhs) : entry_(rhs.entry_) { + if (entry_ != nullptr) { + entry_->ref_++; + } +} + +StringPool::Ref::Ref(StringPool::Entry* entry) : entry_(entry) { + if (entry_ != nullptr) { + entry_->ref_++; + } +} + +StringPool::Ref::~Ref() { + if (entry_ != nullptr) { + entry_->ref_--; + } +} + +StringPool::Ref& StringPool::Ref::operator=(const StringPool::Ref& rhs) { + if (rhs.entry_ != nullptr) { + rhs.entry_->ref_++; + } + + if (entry_ != nullptr) { + entry_->ref_--; + } + entry_ = rhs.entry_; + return *this; +} + +bool StringPool::Ref::operator==(const Ref& rhs) const { + return entry_->value == rhs.entry_->value; +} + +bool StringPool::Ref::operator!=(const Ref& rhs) const { + return entry_->value != rhs.entry_->value; +} + +const std::string* StringPool::Ref::operator->() const { + return &entry_->value; +} + +const std::string& StringPool::Ref::operator*() const { + return entry_->value; +} + +size_t StringPool::Ref::index() const { + // Account for the styles, which *always* come first. + return entry_->pool_->styles_.size() + entry_->index_; +} + +const StringPool::Context& StringPool::Ref::GetContext() const { + return entry_->context; +} + +StringPool::StyleRef::StyleRef() : entry_(nullptr) { +} + +StringPool::StyleRef::StyleRef(const StringPool::StyleRef& rhs) : entry_(rhs.entry_) { + if (entry_ != nullptr) { + entry_->ref_++; + } +} + +StringPool::StyleRef::StyleRef(StringPool::StyleEntry* entry) : entry_(entry) { + if (entry_ != nullptr) { + entry_->ref_++; + } +} + +StringPool::StyleRef::~StyleRef() { + if (entry_ != nullptr) { + entry_->ref_--; + } +} + +StringPool::StyleRef& StringPool::StyleRef::operator=(const StringPool::StyleRef& rhs) { + if (rhs.entry_ != nullptr) { + rhs.entry_->ref_++; + } + + if (entry_ != nullptr) { + entry_->ref_--; + } + entry_ = rhs.entry_; + return *this; +} + +bool StringPool::StyleRef::operator==(const StyleRef& rhs) const { + if (entry_->value != rhs.entry_->value) { + return false; + } + + if (entry_->spans.size() != rhs.entry_->spans.size()) { + return false; + } + + auto rhs_iter = rhs.entry_->spans.begin(); + for (const Span& span : entry_->spans) { + const Span& rhs_span = *rhs_iter; + if (span.first_char != rhs_span.first_char || span.last_char != rhs_span.last_char || + span.name != rhs_span.name) { + return false; + } + } + return true; +} + +bool StringPool::StyleRef::operator!=(const StyleRef& rhs) const { + return !operator==(rhs); +} + +const StringPool::StyleEntry* StringPool::StyleRef::operator->() const { + return entry_; +} + +const StringPool::StyleEntry& StringPool::StyleRef::operator*() const { + return *entry_; +} + +size_t StringPool::StyleRef::index() const { + return entry_->index_; +} + +const StringPool::Context& StringPool::StyleRef::GetContext() const { + return entry_->context; +} + +StringPool::Ref StringPool::MakeRef(const StringPiece& str) { + return MakeRefImpl(str, Context{}, true); +} + +StringPool::Ref StringPool::MakeRef(const StringPiece& str, const Context& context) { + return MakeRefImpl(str, context, true); +} + +StringPool::Ref StringPool::MakeRefImpl(const StringPiece& str, const Context& context, + bool unique) { + if (unique) { + auto range = indexed_strings_.equal_range(str); + for (auto iter = range.first; iter != range.second; ++iter) { + if (context.priority == iter->second->context.priority) { + return Ref(iter->second); + } + } + } + + std::unique_ptr<Entry> entry(new Entry()); + entry->value = str.to_string(); + entry->context = context; + entry->index_ = strings_.size(); + entry->ref_ = 0; + entry->pool_ = this; + + Entry* borrow = entry.get(); + strings_.emplace_back(std::move(entry)); + indexed_strings_.insert(std::make_pair(StringPiece(borrow->value), borrow)); + return Ref(borrow); +} + +StringPool::Ref StringPool::MakeRef(const Ref& ref) { + if (ref.entry_->pool_ == this) { + return ref; + } + return MakeRef(ref.entry_->value, ref.entry_->context); +} + +StringPool::StyleRef StringPool::MakeRef(const StyleString& str) { + return MakeRef(str, Context{}); +} + +StringPool::StyleRef StringPool::MakeRef(const StyleString& str, const Context& context) { + std::unique_ptr<StyleEntry> entry(new StyleEntry()); + entry->value = str.str; + entry->context = context; + entry->index_ = styles_.size(); + entry->ref_ = 0; + for (const android::Span& span : str.spans) { + entry->spans.emplace_back(Span{MakeRef(span.name), span.first_char, span.last_char}); + } + + StyleEntry* borrow = entry.get(); + styles_.emplace_back(std::move(entry)); + return StyleRef(borrow); +} + +StringPool::StyleRef StringPool::MakeRef(const StyleRef& ref) { + std::unique_ptr<StyleEntry> entry(new StyleEntry()); + entry->value = ref.entry_->value; + entry->context = ref.entry_->context; + entry->index_ = styles_.size(); + entry->ref_ = 0; + for (const Span& span : ref.entry_->spans) { + entry->spans.emplace_back(Span{MakeRef(*span.name), span.first_char, span.last_char}); + } + + StyleEntry* borrow = entry.get(); + styles_.emplace_back(std::move(entry)); + return StyleRef(borrow); +} + +void StringPool::ReAssignIndices() { + // Assign the style indices. + const size_t style_len = styles_.size(); + for (size_t index = 0; index < style_len; index++) { + styles_[index]->index_ = index; + } + + // Assign the string indices. + const size_t string_len = strings_.size(); + for (size_t index = 0; index < string_len; index++) { + strings_[index]->index_ = index; + } +} + +void StringPool::Merge(StringPool&& pool) { + // First, change the owning pool for the incoming strings. + for (std::unique_ptr<Entry>& entry : pool.strings_) { + entry->pool_ = this; + } + + // Now move the styles, strings, and indices over. + std::move(pool.styles_.begin(), pool.styles_.end(), std::back_inserter(styles_)); + pool.styles_.clear(); + std::move(pool.strings_.begin(), pool.strings_.end(), std::back_inserter(strings_)); + pool.strings_.clear(); + indexed_strings_.insert(pool.indexed_strings_.begin(), pool.indexed_strings_.end()); + pool.indexed_strings_.clear(); + + ReAssignIndices(); +} + +void StringPool::HintWillAdd(size_t string_count, size_t style_count) { + strings_.reserve(strings_.size() + string_count); + styles_.reserve(styles_.size() + style_count); +} + +void StringPool::Prune() { + const auto iter_end = indexed_strings_.end(); + auto index_iter = indexed_strings_.begin(); + while (index_iter != iter_end) { + if (index_iter->second->ref_ <= 0) { + index_iter = indexed_strings_.erase(index_iter); + } else { + ++index_iter; + } + } + + auto end_iter2 = + std::remove_if(strings_.begin(), strings_.end(), + [](const std::unique_ptr<Entry>& entry) -> bool { return entry->ref_ <= 0; }); + auto end_iter3 = std::remove_if( + styles_.begin(), styles_.end(), + [](const std::unique_ptr<StyleEntry>& entry) -> bool { return entry->ref_ <= 0; }); + + // Remove the entries at the end or else we'll be accessing a deleted string from the StyleEntry. + strings_.erase(end_iter2, strings_.end()); + styles_.erase(end_iter3, styles_.end()); + + ReAssignIndices(); +} + +template <typename E> +static void SortEntries( + std::vector<std::unique_ptr<E>>& entries, + const std::function<int(const StringPool::Context&, const StringPool::Context&)>& cmp) { + using UEntry = std::unique_ptr<E>; + + if (cmp != nullptr) { + std::sort(entries.begin(), entries.end(), [&cmp](const UEntry& a, const UEntry& b) -> bool { + int r = cmp(a->context, b->context); + if (r == 0) { + r = a->value.compare(b->value); + } + return r < 0; + }); + } else { + std::sort(entries.begin(), entries.end(), + [](const UEntry& a, const UEntry& b) -> bool { return a->value < b->value; }); + } +} + +void StringPool::Sort(const std::function<int(const Context&, const Context&)>& cmp) { + SortEntries(styles_, cmp); + SortEntries(strings_, cmp); + ReAssignIndices(); +} + +template <typename T> +static T* EncodeLength(T* data, size_t length) { + static_assert(std::is_integral<T>::value, "wat."); + + constexpr size_t kMask = 1 << ((sizeof(T) * 8) - 1); + constexpr size_t kMaxSize = kMask - 1; + if (length > kMaxSize) { + *data++ = kMask | (kMaxSize & (length >> (sizeof(T) * 8))); + } + *data++ = length; + return data; +} + +/** + * Returns the maximum possible string length that can be successfully encoded + * using 2 units of the specified T. + * EncodeLengthMax<char> -> maximum unit length of 0x7FFF + * EncodeLengthMax<char16_t> -> maximum unit length of 0x7FFFFFFF + **/ +template <typename T> +static size_t EncodeLengthMax() { + static_assert(std::is_integral<T>::value, "wat."); + + constexpr size_t kMask = 1 << ((sizeof(T) * 8 * 2) - 1); + constexpr size_t max = kMask - 1; + return max; +} + +/** + * Returns the number of units (1 or 2) needed to encode the string length + * before writing the string. + */ +template <typename T> +static size_t EncodedLengthUnits(size_t length) { + static_assert(std::is_integral<T>::value, "wat."); + + constexpr size_t kMask = 1 << ((sizeof(T) * 8) - 1); + constexpr size_t kMaxSize = kMask - 1; + return length > kMaxSize ? 2 : 1; +} + +const std::string kStringTooLarge = "STRING_TOO_LARGE"; + +static bool EncodeString(const std::string& str, const bool utf8, BigBuffer* out, + IDiagnostics* diag) { + if (utf8) { + const std::string& encoded = util::Utf8ToModifiedUtf8(str); + const ssize_t utf16_length = + utf8_to_utf16_length(reinterpret_cast<const uint8_t*>(encoded.data()), encoded.size()); + CHECK(utf16_length >= 0); + + // Make sure the lengths to be encoded do not exceed the maximum length that + // can be encoded using chars + if ((((size_t)encoded.size()) > EncodeLengthMax<char>()) || + (((size_t)utf16_length) > EncodeLengthMax<char>())) { + diag->Error(DiagMessage() << "string too large to encode using UTF-8 " + << "written instead as '" << kStringTooLarge << "'"); + + EncodeString(kStringTooLarge, utf8, out, diag); + return false; + } + + const size_t total_size = EncodedLengthUnits<char>(utf16_length) + + EncodedLengthUnits<char>(encoded.size()) + encoded.size() + 1; + + char* data = out->NextBlock<char>(total_size); + + // First encode the UTF16 string length. + data = EncodeLength(data, utf16_length); + + // Now encode the size of the real UTF8 string. + data = EncodeLength(data, encoded.size()); + strncpy(data, encoded.data(), encoded.size()); + + } else { + const std::u16string encoded = util::Utf8ToUtf16(str); + const ssize_t utf16_length = encoded.size(); + + // Make sure the length to be encoded does not exceed the maximum possible + // length that can be encoded + if (((size_t)utf16_length) > EncodeLengthMax<char16_t>()) { + diag->Error(DiagMessage() << "string too large to encode using UTF-16 " + << "written instead as '" << kStringTooLarge << "'"); + + EncodeString(kStringTooLarge, utf8, out, diag); + return false; + } + + // Total number of 16-bit words to write. + const size_t total_size = EncodedLengthUnits<char16_t>(utf16_length) + encoded.size() + 1; + + char16_t* data = out->NextBlock<char16_t>(total_size); + + // Encode the actual UTF16 string length. + data = EncodeLength(data, utf16_length); + const size_t byte_length = encoded.size() * sizeof(char16_t); + + // NOTE: For some reason, strncpy16(data, entry->value.data(), + // entry->value.size()) truncates the string. + memcpy(data, encoded.data(), byte_length); + + // The null-terminating character is already here due to the block of data + // being set to 0s on allocation. + } + + return true; +} + +bool StringPool::Flatten(BigBuffer* out, const StringPool& pool, bool utf8, IDiagnostics* diag) { + bool no_error = true; + const size_t start_index = out->size(); + android::ResStringPool_header* header = out->NextBlock<android::ResStringPool_header>(); + header->header.type = util::HostToDevice16(android::RES_STRING_POOL_TYPE); + header->header.headerSize = util::HostToDevice16(sizeof(*header)); + header->stringCount = util::HostToDevice32(pool.size()); + header->styleCount = util::HostToDevice32(pool.styles_.size()); + if (utf8) { + header->flags |= android::ResStringPool_header::UTF8_FLAG; + } + + uint32_t* indices = pool.size() != 0 ? out->NextBlock<uint32_t>(pool.size()) : nullptr; + uint32_t* style_indices = + pool.styles_.size() != 0 ? out->NextBlock<uint32_t>(pool.styles_.size()) : nullptr; + + const size_t before_strings_index = out->size(); + header->stringsStart = before_strings_index - start_index; + + // Styles always come first. + for (const std::unique_ptr<StyleEntry>& entry : pool.styles_) { + *indices++ = out->size() - before_strings_index; + no_error = EncodeString(entry->value, utf8, out, diag) && no_error; + } + + for (const std::unique_ptr<Entry>& entry : pool.strings_) { + *indices++ = out->size() - before_strings_index; + no_error = EncodeString(entry->value, utf8, out, diag) && no_error; + } + + out->Align4(); + + if (style_indices != nullptr) { + const size_t before_styles_index = out->size(); + header->stylesStart = util::HostToDevice32(before_styles_index - start_index); + + for (const std::unique_ptr<StyleEntry>& entry : pool.styles_) { + *style_indices++ = out->size() - before_styles_index; + + if (!entry->spans.empty()) { + android::ResStringPool_span* span = + out->NextBlock<android::ResStringPool_span>(entry->spans.size()); + for (const Span& s : entry->spans) { + span->name.index = util::HostToDevice32(s.name.index()); + span->firstChar = util::HostToDevice32(s.first_char); + span->lastChar = util::HostToDevice32(s.last_char); + span++; + } + } + + uint32_t* spanEnd = out->NextBlock<uint32_t>(); + *spanEnd = android::ResStringPool_span::END; + } + + // The error checking code in the platform looks for an entire + // ResStringPool_span structure worth of 0xFFFFFFFF at the end + // of the style block, so fill in the remaining 2 32bit words + // with 0xFFFFFFFF. + const size_t padding_length = + sizeof(android::ResStringPool_span) - sizeof(android::ResStringPool_span::name); + uint8_t* padding = out->NextBlock<uint8_t>(padding_length); + memset(padding, 0xff, padding_length); + out->Align4(); + } + header->header.size = util::HostToDevice32(out->size() - start_index); + return no_error; +} + +bool StringPool::FlattenUtf8(BigBuffer* out, const StringPool& pool, IDiagnostics* diag) { + return Flatten(out, pool, true, diag); +} + +bool StringPool::FlattenUtf16(BigBuffer* out, const StringPool& pool, IDiagnostics* diag) { + return Flatten(out, pool, false, diag); +} + +} // namespace android diff --git a/libs/androidfw/Util.cpp b/libs/androidfw/Util.cpp index 59c9d640bb91..52ad0dce8187 100644 --- a/libs/androidfw/Util.cpp +++ b/libs/androidfw/Util.cpp @@ -68,6 +68,108 @@ std::string Utf16ToUtf8(const StringPiece16& utf16) { return utf8; } +std::string Utf8ToModifiedUtf8(const std::string& utf8) { + // Java uses Modified UTF-8 which only supports the 1, 2, and 3 byte formats of UTF-8. To encode + // 4 byte UTF-8 codepoints, Modified UTF-8 allows the use of surrogate pairs in the same format + // of CESU-8 surrogate pairs. Calculate the size of the utf8 string with all 4 byte UTF-8 + // codepoints replaced with 2 3 byte surrogate pairs + size_t modified_size = 0; + const size_t size = utf8.size(); + for (size_t i = 0; i < size; i++) { + if (((uint8_t)utf8[i] >> 4) == 0xF) { + modified_size += 6; + i += 3; + } else { + modified_size++; + } + } + + // Early out if no 4 byte codepoints are found + if (size == modified_size) { + return utf8; + } + + std::string output; + output.reserve(modified_size); + for (size_t i = 0; i < size; i++) { + if (((uint8_t)utf8[i] >> 4) == 0xF) { + int32_t codepoint = utf32_from_utf8_at(utf8.data(), size, i, nullptr); + + // Calculate the high and low surrogates as UTF-16 would + int32_t high = ((codepoint - 0x10000) / 0x400) + 0xD800; + int32_t low = ((codepoint - 0x10000) % 0x400) + 0xDC00; + + // Encode each surrogate in UTF-8 + output.push_back((char)(0xE4 | ((high >> 12) & 0xF))); + output.push_back((char)(0x80 | ((high >> 6) & 0x3F))); + output.push_back((char)(0x80 | (high & 0x3F))); + output.push_back((char)(0xE4 | ((low >> 12) & 0xF))); + output.push_back((char)(0x80 | ((low >> 6) & 0x3F))); + output.push_back((char)(0x80 | (low & 0x3F))); + i += 3; + } else { + output.push_back(utf8[i]); + } + } + + return output; +} + +std::string ModifiedUtf8ToUtf8(const std::string& modified_utf8) { + // The UTF-8 representation will have a byte length less than or equal to the Modified UTF-8 + // representation. + std::string output; + output.reserve(modified_utf8.size()); + + size_t index = 0; + const size_t modified_size = modified_utf8.size(); + while (index < modified_size) { + size_t next_index; + int32_t high_surrogate = + utf32_from_utf8_at(modified_utf8.data(), modified_size, index, &next_index); + if (high_surrogate < 0) { + return {}; + } + + // Check that the first codepoint is within the high surrogate range + if (high_surrogate >= 0xD800 && high_surrogate <= 0xDB7F) { + int32_t low_surrogate = + utf32_from_utf8_at(modified_utf8.data(), modified_size, next_index, &next_index); + if (low_surrogate < 0) { + return {}; + } + + // Check that the second codepoint is within the low surrogate range + if (low_surrogate >= 0xDC00 && low_surrogate <= 0xDFFF) { + const char32_t codepoint = + (char32_t)(((high_surrogate - 0xD800) * 0x400) + (low_surrogate - 0xDC00) + 0x10000); + + // The decoded codepoint should represent a 4 byte, UTF-8 character + const size_t utf8_length = (size_t)utf32_to_utf8_length(&codepoint, 1); + if (utf8_length != 4) { + return {}; + } + + // Encode the UTF-8 representation of the codepoint into the string + const size_t start_index = output.size(); + output.resize(start_index + utf8_length); + char* start = &output[start_index]; + utf32_to_utf8((char32_t*)&codepoint, 1, start, utf8_length + 1); + + index = next_index; + continue; + } + } + + // Append non-surrogate pairs to the output string + for (size_t i = index; i < next_index; i++) { + output.push_back(modified_utf8[i]); + } + index = next_index; + } + return output; +} + static std::vector<std::string> SplitAndTransform( const StringPiece& str, char sep, const std::function<char(char)>& f) { std::vector<std::string> parts; @@ -90,6 +192,29 @@ std::vector<std::string> SplitAndLowercase(const StringPiece& str, char sep) { return SplitAndTransform(str, sep, ::tolower); } +std::unique_ptr<uint8_t[]> Copy(const BigBuffer& buffer) { + std::unique_ptr<uint8_t[]> data = std::unique_ptr<uint8_t[]>(new uint8_t[buffer.size()]); + uint8_t* p = data.get(); + for (const auto& block : buffer) { + memcpy(p, block.buffer.get(), block.size); + p += block.size; + } + return data; +} + +StringPiece16 GetString16(const android::ResStringPool& pool, size_t idx) { + if (auto str = pool.stringAt(idx); str.ok()) { + return *str; + } + return StringPiece16(); +} + +std::string GetString(const android::ResStringPool& pool, size_t idx) { + if (auto str = pool.string8At(idx); str.ok()) { + return ModifiedUtf8ToUtf8(str->to_string()); + } + return Utf16ToUtf8(GetString16(pool, idx)); +} } // namespace util } // namespace android diff --git a/libs/androidfw/include/androidfw/BigBuffer.h b/libs/androidfw/include/androidfw/BigBuffer.h new file mode 100644 index 000000000000..b99a4edf9d88 --- /dev/null +++ b/libs/androidfw/include/androidfw/BigBuffer.h @@ -0,0 +1,192 @@ +/* + * 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. + */ + +#ifndef _ANDROID_BIG_BUFFER_H +#define _ANDROID_BIG_BUFFER_H + +#include <cstring> +#include <memory> +#include <string> +#include <type_traits> +#include <vector> + +#include "android-base/logging.h" +#include "android-base/macros.h" + +namespace android { + +/** + * Inspired by protobuf's ZeroCopyOutputStream, offers blocks of memory + * in which to write without knowing the full size of the entire payload. + * This is essentially a list of memory blocks. As one fills up, another + * block is allocated and appended to the end of the list. + */ +class BigBuffer { + public: + /** + * A contiguous block of allocated memory. + */ + struct Block { + /** + * Pointer to the memory. + */ + std::unique_ptr<uint8_t[]> buffer; + + /** + * Size of memory that is currently occupied. The actual + * allocation may be larger. + */ + size_t size; + + private: + friend class BigBuffer; + + /** + * The size of the memory block allocation. + */ + size_t block_size_; + }; + + typedef std::vector<Block>::const_iterator const_iterator; + + /** + * Create a BigBuffer with block allocation sizes + * of block_size. + */ + explicit BigBuffer(size_t block_size); + + BigBuffer(BigBuffer&& rhs) noexcept; + + /** + * Number of occupied bytes in all the allocated blocks. + */ + size_t size() const; + + /** + * Returns a pointer to an array of T, where T is + * a POD type. The elements are zero-initialized. + */ + template <typename T> + T* NextBlock(size_t count = 1); + + /** + * Returns the next block available and puts the size in out_count. + * This is useful for grabbing blocks where the size doesn't matter. + * Use BackUp() to give back any bytes that were not used. + */ + void* NextBlock(size_t* out_count); + + /** + * Backs up count bytes. This must only be called after NextBlock() + * and can not be larger than sizeof(T) * count of the last NextBlock() + * call. + */ + void BackUp(size_t count); + + /** + * Moves the specified BigBuffer into this one. When this method + * returns, buffer is empty. + */ + void AppendBuffer(BigBuffer&& buffer); + + /** + * Pads the block with 'bytes' bytes of zero values. + */ + void Pad(size_t bytes); + + /** + * Pads the block so that it aligns on a 4 byte boundary. + */ + void Align4(); + + size_t block_size() const; + + const_iterator begin() const; + const_iterator end() const; + + std::string to_string() const; + + private: + DISALLOW_COPY_AND_ASSIGN(BigBuffer); + + /** + * Returns a pointer to a buffer of the requested size. + * The buffer is zero-initialized. + */ + void* NextBlockImpl(size_t size); + + size_t block_size_; + size_t size_; + std::vector<Block> blocks_; +}; + +inline BigBuffer::BigBuffer(size_t block_size) : block_size_(block_size), size_(0) { +} + +inline BigBuffer::BigBuffer(BigBuffer&& rhs) noexcept + : block_size_(rhs.block_size_), size_(rhs.size_), blocks_(std::move(rhs.blocks_)) { +} + +inline size_t BigBuffer::size() const { + return size_; +} + +inline size_t BigBuffer::block_size() const { + return block_size_; +} + +template <typename T> +inline T* BigBuffer::NextBlock(size_t count) { + static_assert(std::is_standard_layout<T>::value, "T must be standard_layout type"); + CHECK(count != 0); + return reinterpret_cast<T*>(NextBlockImpl(sizeof(T) * count)); +} + +inline void BigBuffer::BackUp(size_t count) { + Block& block = blocks_.back(); + block.size -= count; + size_ -= count; +} + +inline void BigBuffer::AppendBuffer(BigBuffer&& buffer) { + std::move(buffer.blocks_.begin(), buffer.blocks_.end(), std::back_inserter(blocks_)); + size_ += buffer.size_; + buffer.blocks_.clear(); + buffer.size_ = 0; +} + +inline void BigBuffer::Pad(size_t bytes) { + NextBlock<char>(bytes); +} + +inline void BigBuffer::Align4() { + const size_t unaligned = size_ % 4; + if (unaligned != 0) { + Pad(4 - unaligned); + } +} + +inline BigBuffer::const_iterator BigBuffer::begin() const { + return blocks_.begin(); +} + +inline BigBuffer::const_iterator BigBuffer::end() const { + return blocks_.end(); +} + +} // namespace android + +#endif // _ANDROID_BIG_BUFFER_H diff --git a/libs/androidfw/include/androidfw/IDiagnostics.h b/libs/androidfw/include/androidfw/IDiagnostics.h new file mode 100644 index 000000000000..273b05ac7689 --- /dev/null +++ b/libs/androidfw/include/androidfw/IDiagnostics.h @@ -0,0 +1,130 @@ +/* + * 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. + */ + +#ifndef _ANDROID_DIAGNOSTICS_H +#define _ANDROID_DIAGNOSTICS_H + +#include <sstream> +#include <string> + +#include "Source.h" +#include "android-base/macros.h" +#include "androidfw/StringPiece.h" + +namespace android { + +struct DiagMessageActual { + Source source; + std::string message; +}; + +struct DiagMessage { + public: + DiagMessage() = default; + + explicit DiagMessage(const android::StringPiece& src) : source_(src) { + } + + explicit DiagMessage(const Source& src) : source_(src) { + } + + explicit DiagMessage(size_t line) : source_(Source().WithLine(line)) { + } + + template <typename T> + DiagMessage& operator<<(const T& value) { + message_ << value; + return *this; + } + + DiagMessageActual Build() const { + return DiagMessageActual{source_, message_.str()}; + } + + private: + Source source_; + std::stringstream message_; +}; + +template <> +inline DiagMessage& DiagMessage::operator<<(const ::std::u16string& value) { + message_ << android::StringPiece16(value); + return *this; +} + +struct IDiagnostics { + virtual ~IDiagnostics() = default; + + enum class Level { Note, Warn, Error }; + + virtual void Log(Level level, DiagMessageActual& actualMsg) = 0; + + virtual void Error(const DiagMessage& message) { + DiagMessageActual actual = message.Build(); + Log(Level::Error, actual); + } + + virtual void Warn(const DiagMessage& message) { + DiagMessageActual actual = message.Build(); + Log(Level::Warn, actual); + } + + virtual void Note(const DiagMessage& message) { + DiagMessageActual actual = message.Build(); + Log(Level::Note, actual); + } +}; + +class SourcePathDiagnostics : public IDiagnostics { + public: + SourcePathDiagnostics(const Source& src, IDiagnostics* diag) : source_(src), diag_(diag) { + } + + void Log(Level level, DiagMessageActual& actual_msg) override { + actual_msg.source.path = source_.path; + diag_->Log(level, actual_msg); + if (level == Level::Error) { + error = true; + } + } + + bool HadError() { + return error; + } + + private: + Source source_; + IDiagnostics* diag_; + bool error = false; + + DISALLOW_COPY_AND_ASSIGN(SourcePathDiagnostics); +}; + +class NoOpDiagnostics : public IDiagnostics { + public: + NoOpDiagnostics() = default; + + void Log(Level level, DiagMessageActual& actual_msg) override { + (void)level; + (void)actual_msg; + } + + DISALLOW_COPY_AND_ASSIGN(NoOpDiagnostics); +}; + +} // namespace android + +#endif /* _ANDROID_DIAGNOSTICS_H */ diff --git a/libs/androidfw/include/androidfw/Idmap.h b/libs/androidfw/include/androidfw/Idmap.h index 6804472b3d17..a1cbbbf2271b 100644 --- a/libs/androidfw/include/androidfw/Idmap.h +++ b/libs/androidfw/include/androidfw/Idmap.h @@ -23,6 +23,7 @@ #include <variant> #include "android-base/macros.h" +#include "androidfw/ConfigDescription.h" #include "androidfw/StringPiece.h" #include "androidfw/ResourceTypes.h" #include "utils/ByteOrder.h" @@ -35,6 +36,7 @@ struct Idmap_header; struct Idmap_data_header; struct Idmap_target_entry; struct Idmap_target_entry_inline; +struct Idmap_target_entry_inline_value; struct Idmap_overlay_entry; // A string pool for overlay apk assets. The string pool holds the strings of the overlay resources @@ -91,7 +93,8 @@ class IdmapResMap { public: Result() = default; explicit Result(uint32_t value) : data_(value) {}; - explicit Result(const Res_value& value) : data_(value) { }; + explicit Result(const std::map<ConfigDescription, Res_value> &value) + : data_(value) { }; // Returns `true` if the resource is overlaid. explicit operator bool() const { @@ -107,15 +110,16 @@ class IdmapResMap { } bool IsInlineValue() const { - return std::get_if<Res_value>(&data_) != nullptr; + return std::get_if<2>(&data_) != nullptr; } - const Res_value& GetInlineValue() const { - return std::get<Res_value>(data_); + const std::map<ConfigDescription, Res_value>& GetInlineValue() const { + return std::get<2>(data_); } private: - std::variant<std::monostate, uint32_t, Res_value> data_; + std::variant<std::monostate, uint32_t, + std::map<ConfigDescription, Res_value> > data_; }; // Looks up the value that overlays the target resource id. @@ -129,12 +133,16 @@ class IdmapResMap { explicit IdmapResMap(const Idmap_data_header* data_header, const Idmap_target_entry* entries, const Idmap_target_entry_inline* inline_entries, + const Idmap_target_entry_inline_value* inline_entry_values, + const ConfigDescription* configs, uint8_t target_assigned_package_id, const OverlayDynamicRefTable* overlay_ref_table); const Idmap_data_header* data_header_; const Idmap_target_entry* entries_; const Idmap_target_entry_inline* inline_entries_; + const Idmap_target_entry_inline_value* inline_entry_values_; + const ConfigDescription* configurations_; const uint8_t target_assigned_package_id_; const OverlayDynamicRefTable* overlay_ref_table_; @@ -170,8 +178,8 @@ class LoadedIdmap { // Returns a mapping from target resource ids to overlay values. const IdmapResMap GetTargetResourcesMap(uint8_t target_assigned_package_id, const OverlayDynamicRefTable* overlay_ref_table) const { - return IdmapResMap(data_header_, target_entries_, target_inline_entries_, - target_assigned_package_id, overlay_ref_table); + return IdmapResMap(data_header_, target_entries_, target_inline_entries_, inline_entry_values_, + configurations_, target_assigned_package_id, overlay_ref_table); } // Returns a dynamic reference table for a loaded overlay package. @@ -191,6 +199,8 @@ class LoadedIdmap { const Idmap_data_header* data_header_; const Idmap_target_entry* target_entries_; const Idmap_target_entry_inline* target_inline_entries_; + const Idmap_target_entry_inline_value* inline_entry_values_; + const ConfigDescription* configurations_; const Idmap_overlay_entry* overlay_entries_; const std::unique_ptr<ResStringPool> string_pool_; @@ -207,6 +217,8 @@ class LoadedIdmap { const Idmap_data_header* data_header, const Idmap_target_entry* target_entries, const Idmap_target_entry_inline* target_inline_entries, + const Idmap_target_entry_inline_value* inline_entry_values_, + const ConfigDescription* configs, const Idmap_overlay_entry* overlay_entries, std::unique_ptr<ResStringPool>&& string_pool, std::string_view overlay_apk_path, diff --git a/libs/androidfw/include/androidfw/LoadedArsc.h b/libs/androidfw/include/androidfw/LoadedArsc.h index b3d6a4dcb955..e45963950b04 100644 --- a/libs/androidfw/include/androidfw/LoadedArsc.h +++ b/libs/androidfw/include/androidfw/LoadedArsc.h @@ -314,6 +314,8 @@ class LoadedArsc { const LoadedIdmap* loaded_idmap = nullptr, package_property_t property_flags = 0U); + static std::unique_ptr<LoadedArsc> Load(const LoadedIdmap* loaded_idmap = nullptr); + // Create an empty LoadedArsc. This is used when an APK has no resources.arsc. static std::unique_ptr<LoadedArsc> CreateEmpty(); @@ -338,6 +340,7 @@ class LoadedArsc { LoadedArsc() = default; bool LoadTable( const Chunk& chunk, const LoadedIdmap* loaded_idmap, package_property_t property_flags); + bool LoadStringPool(const LoadedIdmap* loaded_idmap); std::unique_ptr<ResStringPool> global_string_pool_ = util::make_unique<ResStringPool>(); std::vector<std::unique_ptr<const LoadedPackage>> packages_; diff --git a/libs/androidfw/include/androidfw/ResourceTypes.h b/libs/androidfw/include/androidfw/ResourceTypes.h index 3d66244646d5..9309091f4124 100644 --- a/libs/androidfw/include/androidfw/ResourceTypes.h +++ b/libs/androidfw/include/androidfw/ResourceTypes.h @@ -45,7 +45,7 @@ namespace android { constexpr const uint32_t kIdmapMagic = 0x504D4449u; -constexpr const uint32_t kIdmapCurrentVersion = 0x00000008u; +constexpr const uint32_t kIdmapCurrentVersion = 0x00000009u; // This must never change. constexpr const uint32_t kFabricatedOverlayMagic = 0x4f525246; // FRRO (big endian) @@ -53,7 +53,7 @@ constexpr const uint32_t kFabricatedOverlayMagic = 0x4f525246; // FRRO (big endi // The version should only be changed when a backwards-incompatible change must be made to the // fabricated overlay file format. Old fabricated overlays must be migrated to the new file format // to prevent losing fabricated overlay data. -constexpr const uint32_t kFabricatedOverlayCurrentVersion = 1; +constexpr const uint32_t kFabricatedOverlayCurrentVersion = 2; // Returns whether or not the path represents a fabricated overlay. bool IsFabricatedOverlay(const std::string& path); @@ -1098,7 +1098,7 @@ struct ResTable_config SDKVERSION_ANY = 0 }; - enum { + enum { MINORVERSION_ANY = 0 }; diff --git a/libs/androidfw/include/androidfw/Source.h b/libs/androidfw/include/androidfw/Source.h new file mode 100644 index 000000000000..0421a91d3eba --- /dev/null +++ b/libs/androidfw/include/androidfw/Source.h @@ -0,0 +1,91 @@ +/* + * 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. + */ + +#ifndef _ANDROID_SOURCE_H +#define _ANDROID_SOURCE_H + +#include <optional> +#include <ostream> +#include <string> + +#include "android-base/stringprintf.h" +#include "androidfw/StringPiece.h" + +namespace android { + +// Represents a file on disk. Used for logging and showing errors. +struct Source { + std::string path; + std::optional<size_t> line; + std::optional<std::string> archive; + + Source() = default; + + inline Source(const android::StringPiece& path) : path(path.to_string()) { // NOLINT(implicit) + } + + inline Source(const android::StringPiece& path, const android::StringPiece& archive) + : path(path.to_string()), archive(archive.to_string()) { + } + + inline Source(const android::StringPiece& path, size_t line) + : path(path.to_string()), line(line) { + } + + inline Source WithLine(size_t line) const { + return Source(path, line); + } + + std::string to_string() const { + std::string s = path; + if (archive) { + s = ::android::base::StringPrintf("%s@%s", archive.value().c_str(), s.c_str()); + } + if (line) { + s = ::android::base::StringPrintf("%s:%zd", s.c_str(), line.value()); + } + return s; + } +}; + +// +// Implementations +// + +inline ::std::ostream& operator<<(::std::ostream& out, const Source& source) { + return out << source.to_string(); +} + +inline bool operator==(const Source& lhs, const Source& rhs) { + return lhs.path == rhs.path && lhs.line == rhs.line; +} + +inline bool operator<(const Source& lhs, const Source& rhs) { + int cmp = lhs.path.compare(rhs.path); + if (cmp < 0) return true; + if (cmp > 0) return false; + if (lhs.line) { + if (rhs.line) { + return lhs.line.value() < rhs.line.value(); + } + return false; + } + return bool(rhs.line); +} + +} // namespace android + +#endif // _ANDROID_SOURCE_H diff --git a/libs/androidfw/include/androidfw/StringPiece.h b/libs/androidfw/include/androidfw/StringPiece.h index 921877dc4982..fac2fa4fa575 100644 --- a/libs/androidfw/include/androidfw/StringPiece.h +++ b/libs/androidfw/include/androidfw/StringPiece.h @@ -288,12 +288,12 @@ inline ::std::basic_string<TChar>& operator+=(::std::basic_string<TChar>& lhs, template <typename TChar> inline bool operator==(const ::std::basic_string<TChar>& lhs, const BasicStringPiece<TChar>& rhs) { - return rhs == lhs; + return BasicStringPiece<TChar>(lhs) == rhs; } template <typename TChar> inline bool operator!=(const ::std::basic_string<TChar>& lhs, const BasicStringPiece<TChar>& rhs) { - return rhs != lhs; + return BasicStringPiece<TChar>(lhs) != rhs; } } // namespace android diff --git a/libs/androidfw/include/androidfw/StringPool.h b/libs/androidfw/include/androidfw/StringPool.h new file mode 100644 index 000000000000..25174d8fe3c8 --- /dev/null +++ b/libs/androidfw/include/androidfw/StringPool.h @@ -0,0 +1,228 @@ +/* + * 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. + */ + +#ifndef _ANDROID_STRING_POOL_H +#define _ANDROID_STRING_POOL_H + +#include <functional> +#include <memory> +#include <string> +#include <unordered_map> +#include <vector> + +#include "BigBuffer.h" +#include "IDiagnostics.h" +#include "android-base/macros.h" +#include "androidfw/ConfigDescription.h" +#include "androidfw/StringPiece.h" + +namespace android { + +struct Span { + std::string name; + uint32_t first_char; + uint32_t last_char; + + bool operator==(const Span& right) const { + return name == right.name && first_char == right.first_char && last_char == right.last_char; + } +}; + +struct StyleString { + std::string str; + std::vector<Span> spans; +}; + +// A StringPool for storing the value of String and StyledString resources. +// Styles and Strings are stored separately, since the runtime variant of this +// class -- ResStringPool -- requires that styled strings *always* appear first, since their +// style data is stored as an array indexed by the same indices as the main string pool array. +// Otherwise, the style data array would have to be sparse and take up more space. +class StringPool { + public: + using size_type = size_t; + + class Context { + public: + enum : uint32_t { + kHighPriority = 1u, + kNormalPriority = 0x7fffffffu, + kLowPriority = 0xffffffffu, + }; + uint32_t priority = kNormalPriority; + android::ConfigDescription config; + + Context() = default; + Context(uint32_t p, const android::ConfigDescription& c) : priority(p), config(c) { + } + explicit Context(uint32_t p) : priority(p) { + } + explicit Context(const android::ConfigDescription& c) : priority(kNormalPriority), config(c) { + } + }; + + class Entry; + + class Ref { + public: + Ref(); + Ref(const Ref&); + ~Ref(); + + Ref& operator=(const Ref& rhs); + bool operator==(const Ref& rhs) const; + bool operator!=(const Ref& rhs) const; + const std::string* operator->() const; + const std::string& operator*() const; + + size_t index() const; + const Context& GetContext() const; + + private: + friend class StringPool; + + explicit Ref(Entry* entry); + + Entry* entry_; + }; + + class StyleEntry; + + class StyleRef { + public: + StyleRef(); + StyleRef(const StyleRef&); + ~StyleRef(); + + StyleRef& operator=(const StyleRef& rhs); + bool operator==(const StyleRef& rhs) const; + bool operator!=(const StyleRef& rhs) const; + const StyleEntry* operator->() const; + const StyleEntry& operator*() const; + + size_t index() const; + const Context& GetContext() const; + + private: + friend class StringPool; + + explicit StyleRef(StyleEntry* entry); + + StyleEntry* entry_; + }; + + class Entry { + public: + std::string value; + Context context; + + private: + friend class StringPool; + friend class Ref; + + size_t index_; + int ref_; + const StringPool* pool_; + }; + + struct Span { + Ref name; + uint32_t first_char; + uint32_t last_char; + }; + + class StyleEntry { + public: + std::string value; + Context context; + std::vector<Span> spans; + + private: + friend class StringPool; + friend class StyleRef; + + size_t index_; + int ref_; + }; + + static bool FlattenUtf8(BigBuffer* out, const StringPool& pool, IDiagnostics* diag); + static bool FlattenUtf16(BigBuffer* out, const StringPool& pool, IDiagnostics* diag); + + StringPool() = default; + StringPool(StringPool&&) = default; + StringPool& operator=(StringPool&&) = default; + + // Adds a string to the pool, unless it already exists. Returns a reference to the string in the + // pool. + Ref MakeRef(const android::StringPiece& str); + + // Adds a string to the pool, unless it already exists, with a context object that can be used + // when sorting the string pool. Returns a reference to the string in the pool. + Ref MakeRef(const android::StringPiece& str, const Context& context); + + // Adds a string from another string pool. Returns a reference to the string in the string pool. + Ref MakeRef(const Ref& ref); + + // Adds a style to the string pool and returns a reference to it. + StyleRef MakeRef(const StyleString& str); + + // Adds a style to the string pool with a context object that can be used when sorting the string + // pool. Returns a reference to the style in the string pool. + StyleRef MakeRef(const StyleString& str, const Context& context); + + // Adds a style from another string pool. Returns a reference to the style in the string pool. + StyleRef MakeRef(const StyleRef& ref); + + // Moves pool into this one without coalescing strings. When this function returns, pool will be + // empty. + void Merge(StringPool&& pool); + + inline const std::vector<std::unique_ptr<Entry>>& strings() const { + return strings_; + } + + // Returns the number of strings in the table. + inline size_t size() const { + return styles_.size() + strings_.size(); + } + + // Reserves space for strings and styles as an optimization. + void HintWillAdd(size_t string_count, size_t style_count); + + // Sorts the strings according to their Context using some comparison function. + // Equal Contexts are further sorted by string value, lexicographically. + // If no comparison function is provided, values are only sorted lexicographically. + void Sort(const std::function<int(const Context&, const Context&)>& cmp = nullptr); + + // Removes any strings that have no references. + void Prune(); + + private: + DISALLOW_COPY_AND_ASSIGN(StringPool); + + static bool Flatten(BigBuffer* out, const StringPool& pool, bool utf8, IDiagnostics* diag); + + Ref MakeRefImpl(const android::StringPiece& str, const Context& context, bool unique); + void ReAssignIndices(); + + std::vector<std::unique_ptr<Entry>> strings_; + std::vector<std::unique_ptr<StyleEntry>> styles_; + std::unordered_multimap<android::StringPiece, Entry*> indexed_strings_; +}; + +} // namespace android + +#endif // _ANDROID_STRING_POOL_H diff --git a/libs/androidfw/include/androidfw/Util.h b/libs/androidfw/include/androidfw/Util.h index c59b5b6c51a2..1bbc7f5716bc 100644 --- a/libs/androidfw/include/androidfw/Util.h +++ b/libs/androidfw/include/androidfw/Util.h @@ -17,15 +17,18 @@ #ifndef UTIL_H_ #define UTIL_H_ +#include <android-base/macros.h> +#include <util/map_ptr.h> + #include <cstdlib> #include <memory> #include <sstream> #include <vector> -#include <android-base/macros.h> -#include <util/map_ptr.h> - +#include "androidfw/BigBuffer.h" +#include "androidfw/ResourceTypes.h" #include "androidfw/StringPiece.h" +#include "utils/ByteOrder.h" #ifdef __ANDROID__ #define ANDROID_LOG(x) LOG(x) @@ -125,6 +128,28 @@ std::u16string Utf8ToUtf16(const StringPiece& utf8); // Converts a UTF-16 string to a UTF-8 string. std::string Utf16ToUtf8(const StringPiece16& utf16); +// Converts a UTF8 string into Modified UTF8 +std::string Utf8ToModifiedUtf8(const std::string& utf8); + +// Converts a Modified UTF8 string into a UTF8 string +std::string ModifiedUtf8ToUtf8(const std::string& modified_utf8); + +inline uint16_t HostToDevice16(uint16_t value) { + return htods(value); +} + +inline uint32_t HostToDevice32(uint32_t value) { + return htodl(value); +} + +inline uint16_t DeviceToHost16(uint16_t value) { + return dtohs(value); +} + +inline uint32_t DeviceToHost32(uint32_t value) { + return dtohl(value); +} + std::vector<std::string> SplitAndLowercase(const android::StringPiece& str, char sep); template <typename T> @@ -136,6 +161,18 @@ inline bool IsFourByteAligned(const void* data) { return ((size_t)data & 0x3U) == 0; } +// Helper method to extract a UTF-16 string from a StringPool. If the string is stored as UTF-8, +// the conversion to UTF-16 happens within ResStringPool. +android::StringPiece16 GetString16(const android::ResStringPool& pool, size_t idx); + +// Helper method to extract a UTF-8 string from a StringPool. If the string is stored as UTF-16, +// the conversion from UTF-16 to UTF-8 does not happen in ResStringPool and is done by this method, +// which maintains no state or cache. This means we must return an std::string copy. +std::string GetString(const android::ResStringPool& pool, size_t idx); + +// Copies the entire BigBuffer into a single buffer. +std::unique_ptr<uint8_t[]> Copy(const android::BigBuffer& buffer); + } // namespace util } // namespace android diff --git a/libs/androidfw/tests/BigBuffer_test.cpp b/libs/androidfw/tests/BigBuffer_test.cpp new file mode 100644 index 000000000000..382d21e20846 --- /dev/null +++ b/libs/androidfw/tests/BigBuffer_test.cpp @@ -0,0 +1,101 @@ +/* + * 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. + */ + +#include "androidfw/BigBuffer.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +using ::testing::NotNull; + +namespace android { + +TEST(BigBufferTest, AllocateSingleBlock) { + BigBuffer buffer(4); + + EXPECT_THAT(buffer.NextBlock<char>(2), NotNull()); + EXPECT_EQ(2u, buffer.size()); +} + +TEST(BigBufferTest, ReturnSameBlockIfNextAllocationFits) { + BigBuffer buffer(16); + + char* b1 = buffer.NextBlock<char>(8); + EXPECT_THAT(b1, NotNull()); + + char* b2 = buffer.NextBlock<char>(4); + EXPECT_THAT(b2, NotNull()); + + EXPECT_EQ(b1 + 8, b2); +} + +TEST(BigBufferTest, AllocateExactSizeBlockIfLargerThanBlockSize) { + BigBuffer buffer(16); + + EXPECT_THAT(buffer.NextBlock<char>(32), NotNull()); + EXPECT_EQ(32u, buffer.size()); +} + +TEST(BigBufferTest, AppendAndMoveBlock) { + BigBuffer buffer(16); + + uint32_t* b1 = buffer.NextBlock<uint32_t>(); + ASSERT_THAT(b1, NotNull()); + *b1 = 33; + + { + BigBuffer buffer2(16); + b1 = buffer2.NextBlock<uint32_t>(); + ASSERT_THAT(b1, NotNull()); + *b1 = 44; + + buffer.AppendBuffer(std::move(buffer2)); + EXPECT_EQ(0u, buffer2.size()); // NOLINT + EXPECT_EQ(buffer2.begin(), buffer2.end()); + } + + EXPECT_EQ(2 * sizeof(uint32_t), buffer.size()); + + auto b = buffer.begin(); + ASSERT_NE(b, buffer.end()); + ASSERT_EQ(sizeof(uint32_t), b->size); + ASSERT_EQ(33u, *reinterpret_cast<uint32_t*>(b->buffer.get())); + ++b; + + ASSERT_NE(b, buffer.end()); + ASSERT_EQ(sizeof(uint32_t), b->size); + ASSERT_EQ(44u, *reinterpret_cast<uint32_t*>(b->buffer.get())); + ++b; + + ASSERT_EQ(b, buffer.end()); +} + +TEST(BigBufferTest, PadAndAlignProperly) { + BigBuffer buffer(16); + + ASSERT_THAT(buffer.NextBlock<char>(2), NotNull()); + ASSERT_EQ(2u, buffer.size()); + buffer.Pad(2); + ASSERT_EQ(4u, buffer.size()); + buffer.Align4(); + ASSERT_EQ(4u, buffer.size()); + buffer.Pad(2); + ASSERT_EQ(6u, buffer.size()); + buffer.Align4(); + ASSERT_EQ(8u, buffer.size()); +} + +} // namespace android diff --git a/libs/androidfw/tests/StringPool_test.cpp b/libs/androidfw/tests/StringPool_test.cpp new file mode 100644 index 000000000000..047d45785409 --- /dev/null +++ b/libs/androidfw/tests/StringPool_test.cpp @@ -0,0 +1,388 @@ +/* + * 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. + */ + +#include "androidfw/StringPool.h" + +#include <string> + +#include "androidfw/IDiagnostics.h" +#include "androidfw/StringPiece.h" +#include "androidfw/Util.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +using ::android::StringPiece; +using ::android::StringPiece16; +using ::testing::Eq; +using ::testing::Ne; +using ::testing::NotNull; +using ::testing::Pointee; + +namespace android { + +TEST(StringPoolTest, InsertOneString) { + StringPool pool; + + StringPool::Ref ref = pool.MakeRef("wut"); + EXPECT_THAT(*ref, Eq("wut")); +} + +TEST(StringPoolTest, InsertTwoUniqueStrings) { + StringPool pool; + + StringPool::Ref ref_a = pool.MakeRef("wut"); + StringPool::Ref ref_b = pool.MakeRef("hey"); + + EXPECT_THAT(*ref_a, Eq("wut")); + EXPECT_THAT(*ref_b, Eq("hey")); +} + +TEST(StringPoolTest, DoNotInsertNewDuplicateString) { + StringPool pool; + + StringPool::Ref ref_a = pool.MakeRef("wut"); + StringPool::Ref ref_b = pool.MakeRef("wut"); + + EXPECT_THAT(*ref_a, Eq("wut")); + EXPECT_THAT(*ref_b, Eq("wut")); + EXPECT_THAT(pool.size(), Eq(1u)); +} + +TEST(StringPoolTest, DoNotDedupeSameStringDifferentPriority) { + StringPool pool; + + StringPool::Ref ref_a = pool.MakeRef("wut", StringPool::Context(0x81010001)); + StringPool::Ref ref_b = pool.MakeRef("wut", StringPool::Context(0x81010002)); + + EXPECT_THAT(*ref_a, Eq("wut")); + EXPECT_THAT(*ref_b, Eq("wut")); + EXPECT_THAT(pool.size(), Eq(2u)); +} + +TEST(StringPoolTest, MaintainInsertionOrderIndex) { + StringPool pool; + + StringPool::Ref ref_a = pool.MakeRef("z"); + StringPool::Ref ref_b = pool.MakeRef("a"); + StringPool::Ref ref_c = pool.MakeRef("m"); + + EXPECT_THAT(ref_a.index(), Eq(0u)); + EXPECT_THAT(ref_b.index(), Eq(1u)); + EXPECT_THAT(ref_c.index(), Eq(2u)); +} + +TEST(StringPoolTest, PruneStringsWithNoReferences) { + StringPool pool; + + StringPool::Ref ref_a = pool.MakeRef("foo"); + + { + StringPool::Ref ref_b = pool.MakeRef("wut"); + EXPECT_THAT(*ref_b, Eq("wut")); + EXPECT_THAT(pool.size(), Eq(2u)); + pool.Prune(); + EXPECT_THAT(pool.size(), Eq(2u)); + } + EXPECT_THAT(pool.size(), Eq(2u)); + + { + StringPool::Ref ref_c = pool.MakeRef("bar"); + EXPECT_THAT(pool.size(), Eq(3u)); + + pool.Prune(); + EXPECT_THAT(pool.size(), Eq(2u)); + } + EXPECT_THAT(pool.size(), Eq(2u)); + + pool.Prune(); + EXPECT_THAT(pool.size(), Eq(1u)); +} + +TEST(StringPoolTest, SortAndMaintainIndexesInStringReferences) { + StringPool pool; + + StringPool::Ref ref_a = pool.MakeRef("z"); + StringPool::Ref ref_b = pool.MakeRef("a"); + StringPool::Ref ref_c = pool.MakeRef("m"); + + EXPECT_THAT(*ref_a, Eq("z")); + EXPECT_THAT(ref_a.index(), Eq(0u)); + + EXPECT_THAT(*ref_b, Eq("a")); + EXPECT_THAT(ref_b.index(), Eq(1u)); + + EXPECT_THAT(*ref_c, Eq("m")); + EXPECT_THAT(ref_c.index(), Eq(2u)); + + pool.Sort(); + + EXPECT_THAT(*ref_a, Eq("z")); + EXPECT_THAT(ref_a.index(), Eq(2u)); + + EXPECT_THAT(*ref_b, Eq("a")); + EXPECT_THAT(ref_b.index(), Eq(0u)); + + EXPECT_THAT(*ref_c, Eq("m")); + EXPECT_THAT(ref_c.index(), Eq(1u)); +} + +TEST(StringPoolTest, SortAndStillDedupe) { + StringPool pool; + + StringPool::Ref ref_a = pool.MakeRef("z"); + StringPool::Ref ref_b = pool.MakeRef("a"); + StringPool::Ref ref_c = pool.MakeRef("m"); + + pool.Sort(); + + StringPool::Ref ref_d = pool.MakeRef("z"); + StringPool::Ref ref_e = pool.MakeRef("a"); + StringPool::Ref ref_f = pool.MakeRef("m"); + + EXPECT_THAT(ref_d.index(), Eq(ref_a.index())); + EXPECT_THAT(ref_e.index(), Eq(ref_b.index())); + EXPECT_THAT(ref_f.index(), Eq(ref_c.index())); +} + +TEST(StringPoolTest, AddStyles) { + StringPool pool; + + StringPool::StyleRef ref = pool.MakeRef(StyleString{{"android"}, {Span{{"b"}, 2, 6}}}); + EXPECT_THAT(ref.index(), Eq(0u)); + EXPECT_THAT(ref->value, Eq("android")); + ASSERT_THAT(ref->spans.size(), Eq(1u)); + + const StringPool::Span& span = ref->spans.front(); + EXPECT_THAT(*span.name, Eq("b")); + EXPECT_THAT(span.first_char, Eq(2u)); + EXPECT_THAT(span.last_char, Eq(6u)); +} + +TEST(StringPoolTest, DoNotDedupeStyleWithSameStringAsNonStyle) { + StringPool pool; + + StringPool::Ref ref = pool.MakeRef("android"); + + StyleString str{{"android"}, {}}; + StringPool::StyleRef style_ref = pool.MakeRef(StyleString{{"android"}, {}}); + + EXPECT_THAT(ref.index(), Ne(style_ref.index())); +} + +TEST(StringPoolTest, StylesAndStringsAreSeparateAfterSorting) { + StringPool pool; + + StringPool::StyleRef ref_a = pool.MakeRef(StyleString{{"beta"}, {}}); + StringPool::Ref ref_b = pool.MakeRef("alpha"); + StringPool::StyleRef ref_c = pool.MakeRef(StyleString{{"alpha"}, {}}); + + EXPECT_THAT(ref_b.index(), Ne(ref_c.index())); + + pool.Sort(); + + EXPECT_THAT(ref_c.index(), Eq(0u)); + EXPECT_THAT(ref_a.index(), Eq(1u)); + EXPECT_THAT(ref_b.index(), Eq(2u)); +} + +TEST(StringPoolTest, FlattenEmptyStringPoolUtf8) { + using namespace android; // For NO_ERROR on Windows. + NoOpDiagnostics diag; + + StringPool pool; + BigBuffer buffer(1024); + StringPool::FlattenUtf8(&buffer, pool, &diag); + + std::unique_ptr<uint8_t[]> data = android::util::Copy(buffer); + ResStringPool test; + ASSERT_THAT(test.setTo(data.get(), buffer.size()), Eq(NO_ERROR)); +} + +TEST(StringPoolTest, FlattenOddCharactersUtf16) { + using namespace android; // For NO_ERROR on Windows. + NoOpDiagnostics diag; + + StringPool pool; + pool.MakeRef("\u093f"); + BigBuffer buffer(1024); + StringPool::FlattenUtf16(&buffer, pool, &diag); + + std::unique_ptr<uint8_t[]> data = android::util::Copy(buffer); + ResStringPool test; + ASSERT_EQ(test.setTo(data.get(), buffer.size()), NO_ERROR); + auto str = test.stringAt(0); + ASSERT_TRUE(str.has_value()); + EXPECT_THAT(str->size(), Eq(1u)); + EXPECT_THAT(str->data(), Pointee(Eq(u'\u093f'))); + EXPECT_THAT(str->data()[1], Eq(0u)); +} + +constexpr const char* sLongString = + "バッテリーを長持ちさせるため、バッテリーセーバーは端末のパフォーマンスを抑" + "え、バイブレーション、位置情報サービス、大半のバックグラウンドデータを制限" + "します。メール、SMSや、同期を使 " + "用するその他のアプリは、起動しても更新されないことがあります。バッテリーセ" + "ーバーは端末の充電中は自動的にOFFになります。"; + +TEST(StringPoolTest, Flatten) { + using namespace android; // For NO_ERROR on Windows. + NoOpDiagnostics diag; + + StringPool pool; + + StringPool::Ref ref_a = pool.MakeRef("hello"); + StringPool::Ref ref_b = pool.MakeRef("goodbye"); + StringPool::Ref ref_c = pool.MakeRef(sLongString); + StringPool::Ref ref_d = pool.MakeRef(""); + StringPool::StyleRef ref_e = + pool.MakeRef(StyleString{{"style"}, {Span{{"b"}, 0, 1}, Span{{"i"}, 2, 3}}}); + + // Styles are always first. + EXPECT_THAT(ref_e.index(), Eq(0u)); + + EXPECT_THAT(ref_a.index(), Eq(1u)); + EXPECT_THAT(ref_b.index(), Eq(2u)); + EXPECT_THAT(ref_c.index(), Eq(3u)); + EXPECT_THAT(ref_d.index(), Eq(4u)); + + BigBuffer buffers[2] = {BigBuffer(1024), BigBuffer(1024)}; + StringPool::FlattenUtf8(&buffers[0], pool, &diag); + StringPool::FlattenUtf16(&buffers[1], pool, &diag); + + // Test both UTF-8 and UTF-16 buffers. + for (const BigBuffer& buffer : buffers) { + std::unique_ptr<uint8_t[]> data = android::util::Copy(buffer); + + ResStringPool test; + ASSERT_EQ(test.setTo(data.get(), buffer.size()), NO_ERROR); + + EXPECT_THAT(android::util::GetString(test, 1), Eq("hello")); + EXPECT_THAT(android::util::GetString16(test, 1), Eq(u"hello")); + + EXPECT_THAT(android::util::GetString(test, 2), Eq("goodbye")); + EXPECT_THAT(android::util::GetString16(test, 2), Eq(u"goodbye")); + + EXPECT_THAT(android::util::GetString(test, 3), Eq(sLongString)); + EXPECT_THAT(android::util::GetString16(test, 3), Eq(util::Utf8ToUtf16(sLongString))); + + EXPECT_TRUE(test.stringAt(4).has_value() || test.string8At(4).has_value()); + + EXPECT_THAT(android::util::GetString(test, 0), Eq("style")); + EXPECT_THAT(android::util::GetString16(test, 0), Eq(u"style")); + + auto span_result = test.styleAt(0); + ASSERT_TRUE(span_result.has_value()); + + const ResStringPool_span* span = span_result->unsafe_ptr(); + EXPECT_THAT(android::util::GetString(test, span->name.index), Eq("b")); + EXPECT_THAT(android::util::GetString16(test, span->name.index), Eq(u"b")); + EXPECT_THAT(span->firstChar, Eq(0u)); + EXPECT_THAT(span->lastChar, Eq(1u)); + span++; + + ASSERT_THAT(span->name.index, Ne(ResStringPool_span::END)); + EXPECT_THAT(android::util::GetString(test, span->name.index), Eq("i")); + EXPECT_THAT(android::util::GetString16(test, span->name.index), Eq(u"i")); + EXPECT_THAT(span->firstChar, Eq(2u)); + EXPECT_THAT(span->lastChar, Eq(3u)); + span++; + + EXPECT_THAT(span->name.index, Eq(ResStringPool_span::END)); + } +} + +TEST(StringPoolTest, ModifiedUTF8) { + using namespace android; // For NO_ERROR on Windows. + NoOpDiagnostics diag; + StringPool pool; + StringPool::Ref ref_a = pool.MakeRef("\xF0\x90\x90\x80"); // 𐐀 (U+10400) + StringPool::Ref ref_b = pool.MakeRef("foo \xF0\x90\x90\xB7 bar"); // 𐐷 (U+10437) + StringPool::Ref ref_c = pool.MakeRef("\xF0\x90\x90\x80\xF0\x90\x90\xB7"); + + BigBuffer buffer(1024); + StringPool::FlattenUtf8(&buffer, pool, &diag); + std::unique_ptr<uint8_t[]> data = android::util::Copy(buffer); + + // Check that the codepoints are encoded using two three-byte surrogate pairs + ResStringPool test; + ASSERT_EQ(test.setTo(data.get(), buffer.size()), NO_ERROR); + auto str = test.string8At(0); + ASSERT_TRUE(str.has_value()); + EXPECT_THAT(str->to_string(), Eq("\xED\xA0\x81\xED\xB0\x80")); + + str = test.string8At(1); + ASSERT_TRUE(str.has_value()); + EXPECT_THAT(str->to_string(), Eq("foo \xED\xA0\x81\xED\xB0\xB7 bar")); + + str = test.string8At(2); + ASSERT_TRUE(str.has_value()); + EXPECT_THAT(str->to_string(), Eq("\xED\xA0\x81\xED\xB0\x80\xED\xA0\x81\xED\xB0\xB7")); + + // Check that retrieving the strings returns the original UTF-8 character bytes + EXPECT_THAT(android::util::GetString(test, 0), Eq("\xF0\x90\x90\x80")); + EXPECT_THAT(android::util::GetString(test, 1), Eq("foo \xF0\x90\x90\xB7 bar")); + EXPECT_THAT(android::util::GetString(test, 2), Eq("\xF0\x90\x90\x80\xF0\x90\x90\xB7")); +} + +TEST(StringPoolTest, MaxEncodingLength) { + NoOpDiagnostics diag; + using namespace android; // For NO_ERROR on Windows. + ResStringPool test; + + StringPool pool; + pool.MakeRef("aaaaaaaaaa"); + BigBuffer buffers[2] = {BigBuffer(1024), BigBuffer(1024)}; + + // Make sure a UTF-8 string under the maximum length does not produce an error + EXPECT_THAT(StringPool::FlattenUtf8(&buffers[0], pool, &diag), Eq(true)); + std::unique_ptr<uint8_t[]> data = android::util::Copy(buffers[0]); + test.setTo(data.get(), buffers[0].size()); + EXPECT_THAT(android::util::GetString(test, 0), Eq("aaaaaaaaaa")); + + // Make sure a UTF-16 string under the maximum length does not produce an error + EXPECT_THAT(StringPool::FlattenUtf16(&buffers[1], pool, &diag), Eq(true)); + data = android::util::Copy(buffers[1]); + test.setTo(data.get(), buffers[1].size()); + EXPECT_THAT(android::util::GetString16(test, 0), Eq(u"aaaaaaaaaa")); + + StringPool pool2; + std::string longStr(50000, 'a'); + pool2.MakeRef("this fits1"); + pool2.MakeRef(longStr); + pool2.MakeRef("this fits2"); + BigBuffer buffers2[2] = {BigBuffer(1024), BigBuffer(1024)}; + + // Make sure a string that exceeds the maximum length of UTF-8 produces an + // error and writes a shorter error string instead + EXPECT_THAT(StringPool::FlattenUtf8(&buffers2[0], pool2, &diag), Eq(false)); + data = android::util::Copy(buffers2[0]); + test.setTo(data.get(), buffers2[0].size()); + EXPECT_THAT(android::util::GetString(test, 0), "this fits1"); + EXPECT_THAT(android::util::GetString(test, 1), "STRING_TOO_LARGE"); + EXPECT_THAT(android::util::GetString(test, 2), "this fits2"); + + // Make sure a string that a string that exceeds the maximum length of UTF-8 + // but not UTF-16 does not error for UTF-16 + StringPool pool3; + std::u16string longStr16(50000, 'a'); + pool3.MakeRef(longStr); + EXPECT_THAT(StringPool::FlattenUtf16(&buffers2[1], pool3, &diag), Eq(true)); + data = android::util::Copy(buffers2[1]); + test.setTo(data.get(), buffers2[1].size()); + EXPECT_THAT(android::util::GetString16(test, 0), Eq(longStr16)); +} + +} // namespace android diff --git a/libs/androidfw/tests/data/overlay/overlay.idmap b/libs/androidfw/tests/data/overlay/overlay.idmap Binary files differindex 88eadccb38cf..8e847e81aa31 100644 --- a/libs/androidfw/tests/data/overlay/overlay.idmap +++ b/libs/androidfw/tests/data/overlay/overlay.idmap 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/Android.bp b/libs/hwui/Android.bp index ad9aa6cdd3d9..b11e542472da 100644 --- a/libs/hwui/Android.bp +++ b/libs/hwui/Android.bp @@ -93,6 +93,10 @@ cc_defaults { cc_defaults { name: "hwui_static_deps", + defaults: [ + "android.hardware.graphics.common-ndk_shared", + "android.hardware.graphics.composer3-ndk_shared", + ], shared_libs: [ "libbase", "libharfbuzz_ng", @@ -106,9 +110,7 @@ cc_defaults { target: { android: { shared_libs: [ - "android.hardware.graphics.common-V3-ndk", "android.hardware.graphics.common@1.2", - "android.hardware.graphics.composer3-V1-ndk", "liblog", "libcutils", "libutils", @@ -345,6 +347,7 @@ cc_defaults { "jni/PaintFilter.cpp", "jni/Path.cpp", "jni/PathEffect.cpp", + "jni/PathIterator.cpp", "jni/PathMeasure.cpp", "jni/Picture.cpp", "jni/Region.cpp", diff --git a/libs/hwui/CopyRequest.h b/libs/hwui/CopyRequest.h new file mode 100644 index 000000000000..5fbd5f900716 --- /dev/null +++ b/libs/hwui/CopyRequest.h @@ -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. + */ + +#pragma once + +#include "Rect.h" +#include "hwui/Bitmap.h" + +namespace android::uirenderer { + +// Keep in sync with PixelCopy.java codes +enum class CopyResult { + Success = 0, + UnknownError = 1, + Timeout = 2, + SourceEmpty = 3, + SourceInvalid = 4, + DestinationInvalid = 5, +}; + +struct CopyRequest { + Rect srcRect; + CopyRequest(Rect srcRect) : srcRect(srcRect) {} + virtual ~CopyRequest() {} + virtual SkBitmap getDestinationBitmap(int srcWidth, int srcHeight) = 0; + virtual void onCopyFinished(CopyResult result) = 0; +}; + +} // namespace android::uirenderer diff --git a/libs/hwui/DeviceInfo.h b/libs/hwui/DeviceInfo.h index d5fee3f667a9..2e6e36a9ff22 100644 --- a/libs/hwui/DeviceInfo.h +++ b/libs/hwui/DeviceInfo.h @@ -16,7 +16,9 @@ #ifndef DEVICEINFO_H #define DEVICEINFO_H +#include <SkColorSpace.h> #include <SkImageInfo.h> +#include <SkRefCnt.h> #include <android/data_space.h> #include <mutex> 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/HardwareBitmapUploader.cpp b/libs/hwui/HardwareBitmapUploader.cpp index c24cabb287de..7291cab364e2 100644 --- a/libs/hwui/HardwareBitmapUploader.cpp +++ b/libs/hwui/HardwareBitmapUploader.cpp @@ -22,8 +22,11 @@ #include <GLES2/gl2ext.h> #include <GLES3/gl3.h> #include <GrDirectContext.h> +#include <SkBitmap.h> #include <SkCanvas.h> #include <SkImage.h> +#include <SkImageInfo.h> +#include <SkRefCnt.h> #include <gui/TraceUtils.h> #include <utils/GLUtils.h> #include <utils/NdkUtils.h> diff --git a/libs/hwui/HardwareBitmapUploader.h b/libs/hwui/HardwareBitmapUploader.h index 81057a24c29c..00ee99648889 100644 --- a/libs/hwui/HardwareBitmapUploader.h +++ b/libs/hwui/HardwareBitmapUploader.h @@ -17,6 +17,9 @@ #pragma once #include <hwui/Bitmap.h> +#include <SkRefCnt.h> + +class SkBitmap; namespace android::uirenderer { 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/Readback.cpp b/libs/hwui/Readback.cpp index 4cce87ad1a2f..02c2e67a319b 100644 --- a/libs/hwui/Readback.cpp +++ b/libs/hwui/Readback.cpp @@ -26,6 +26,18 @@ #include "pipeline/skia/LayerDrawable.h" #include "renderthread/EglManager.h" #include "renderthread/VulkanManager.h" +#include <SkBitmap.h> +#include <SkBlendMode.h> +#include <SkCanvas.h> +#include <SkColorSpace.h> +#include <SkImage.h> +#include <SkImageInfo.h> +#include <SkMatrix.h> +#include <SkPaint.h> +#include <SkRect.h> +#include <SkRefCnt.h> +#include <SkSamplingOptions.h> +#include <SkSurface.h> #include "utils/Color.h" #include "utils/MathUtils.h" #include "utils/NdkUtils.h" @@ -37,8 +49,7 @@ namespace uirenderer { #define ARECT_ARGS(r) float((r).left), float((r).top), float((r).right), float((r).bottom) -CopyResult Readback::copySurfaceInto(ANativeWindow* window, const Rect& inSrcRect, - SkBitmap* bitmap) { +void Readback::copySurfaceInto(ANativeWindow* window, const std::shared_ptr<CopyRequest>& request) { ATRACE_CALL(); // Setup the source AHardwareBuffer* rawSourceBuffer; @@ -51,30 +62,33 @@ CopyResult Readback::copySurfaceInto(ANativeWindow* window, const Rect& inSrcRec // Really this shouldn't ever happen, but better safe than sorry. if (err == UNKNOWN_TRANSACTION) { ALOGW("Readback failed to ANativeWindow_getLastQueuedBuffer2 - who are we talking to?"); - return copySurfaceIntoLegacy(window, inSrcRect, bitmap); + return request->onCopyFinished(CopyResult::SourceInvalid); } ALOGV("Using new path, cropRect=" RECT_STRING ", transform=%x", ARECT_ARGS(cropRect), windowTransform); if (err != NO_ERROR) { ALOGW("Failed to get last queued buffer, error = %d", err); - return CopyResult::UnknownError; + return request->onCopyFinished(CopyResult::SourceInvalid); } if (rawSourceBuffer == nullptr) { ALOGW("Surface doesn't have any previously queued frames, nothing to readback from"); - return CopyResult::SourceEmpty; + return request->onCopyFinished(CopyResult::SourceEmpty); } UniqueAHardwareBuffer sourceBuffer{rawSourceBuffer}; AHardwareBuffer_Desc description; AHardwareBuffer_describe(sourceBuffer.get(), &description); if (description.usage & AHARDWAREBUFFER_USAGE_PROTECTED_CONTENT) { ALOGW("Surface is protected, unable to copy from it"); - return CopyResult::SourceInvalid; + return request->onCopyFinished(CopyResult::SourceInvalid); } - if (sourceFence != -1 && sync_wait(sourceFence.get(), 500 /* ms */) != NO_ERROR) { - ALOGE("Timeout (500ms) exceeded waiting for buffer fence, abandoning readback attempt"); - return CopyResult::Timeout; + { + ATRACE_NAME("sync_wait"); + if (sourceFence != -1 && sync_wait(sourceFence.get(), 500 /* ms */) != NO_ERROR) { + ALOGE("Timeout (500ms) exceeded waiting for buffer fence, abandoning readback attempt"); + return request->onCopyFinished(CopyResult::Timeout); + } } sk_sp<SkColorSpace> colorSpace = DataSpaceToColorSpace( @@ -83,18 +97,43 @@ CopyResult Readback::copySurfaceInto(ANativeWindow* window, const Rect& inSrcRec SkImage::MakeFromAHardwareBuffer(sourceBuffer.get(), kPremul_SkAlphaType, colorSpace); if (!image.get()) { - return CopyResult::UnknownError; + return request->onCopyFinished(CopyResult::UnknownError); } sk_sp<GrDirectContext> grContext = mRenderThread.requireGrContext(); - SkRect srcRect = inSrcRect.toSkRect(); + SkRect srcRect = request->srcRect.toSkRect(); + + SkRect imageSrcRect = SkRect::MakeIWH(description.width, description.height); + SkISize imageWH = SkISize::Make(description.width, description.height); + if (cropRect.left < cropRect.right && cropRect.top < cropRect.bottom) { + imageSrcRect = + SkRect::MakeLTRB(cropRect.left, cropRect.top, cropRect.right, cropRect.bottom); + imageWH = SkISize::Make(cropRect.right - cropRect.left, cropRect.bottom - cropRect.top); + + // Chroma channels of YUV420 images are subsampled we may need to shrink the crop region by + // a whole texel on each side. Since skia still adds its own 0.5 inset, we apply an + // additional 0.5 inset. See GLConsumer::computeTransformMatrix for details. + float shrinkAmount = 0.0f; + switch (description.format) { + // Use HAL formats since some AHB formats are only available in vndk + case HAL_PIXEL_FORMAT_YCBCR_420_888: + case HAL_PIXEL_FORMAT_YV12: + case HAL_PIXEL_FORMAT_IMPLEMENTATION_DEFINED: + shrinkAmount = 0.5f; + break; + default: + break; + } + + // Shrink the crop if it has more than 1-px and differs from the buffer size. + if (imageWH.width() > 1 && imageWH.width() < (int32_t)description.width) + imageSrcRect = imageSrcRect.makeInset(shrinkAmount, 0); - SkRect imageSrcRect = - SkRect::MakeLTRB(cropRect.left, cropRect.top, cropRect.right, cropRect.bottom); - if (imageSrcRect.isEmpty()) { - imageSrcRect = SkRect::MakeIWH(description.width, description.height); + if (imageWH.height() > 1 && imageWH.height() < (int32_t)description.height) + imageSrcRect = imageSrcRect.makeInset(0, shrinkAmount); } + ALOGV("imageSrcRect = " RECT_STRING, SK_RECT_ARGS(imageSrcRect)); // Represents the "logical" width/height of the texture. That is, the dimensions of the buffer @@ -111,10 +150,12 @@ CopyResult Readback::copySurfaceInto(ANativeWindow* window, const Rect& inSrcRec ALOGV("intersecting " RECT_STRING " with " RECT_STRING, SK_RECT_ARGS(srcRect), SK_RECT_ARGS(textureRect)); if (!srcRect.intersect(textureRect)) { - return CopyResult::UnknownError; + return request->onCopyFinished(CopyResult::UnknownError); } } + SkBitmap skBitmap = request->getDestinationBitmap(srcRect.width(), srcRect.height()); + SkBitmap* bitmap = &skBitmap; sk_sp<SkSurface> tmpSurface = SkSurface::MakeRenderTarget(mRenderThread.getGrContext(), SkBudgeted::kYes, bitmap->info(), 0, kTopLeft_GrSurfaceOrigin, nullptr); @@ -127,7 +168,7 @@ CopyResult Readback::copySurfaceInto(ANativeWindow* window, const Rect& inSrcRec tmpInfo, 0, kTopLeft_GrSurfaceOrigin, nullptr); if (!tmpSurface.get()) { ALOGW("Unable to generate GPU buffer in a format compatible with the provided bitmap"); - return CopyResult::UnknownError; + return request->onCopyFinished(CopyResult::UnknownError); } } @@ -153,7 +194,7 @@ CopyResult Readback::copySurfaceInto(ANativeWindow* window, const Rect& inSrcRec */ SkMatrix m; - const SkRect imageDstRect = SkRect::MakeIWH(imageSrcRect.width(), imageSrcRect.height()); + const SkRect imageDstRect = SkRect::Make(imageWH); const float px = imageDstRect.centerX(); const float py = imageDstRect.centerY(); if (windowTransform & NATIVE_WINDOW_TRANSFORM_FLIP_H) { @@ -198,52 +239,13 @@ CopyResult Readback::copySurfaceInto(ANativeWindow* window, const Rect& inSrcRec !tmpBitmap.tryAllocPixels(tmpInfo) || !tmpSurface->readPixels(tmpBitmap, 0, 0) || !tmpBitmap.readPixels(bitmap->info(), bitmap->getPixels(), bitmap->rowBytes(), 0, 0)) { ALOGW("Unable to convert content into the provided bitmap"); - return CopyResult::UnknownError; + return request->onCopyFinished(CopyResult::UnknownError); } } bitmap->notifyPixelsChanged(); - return CopyResult::Success; -} - -CopyResult Readback::copySurfaceIntoLegacy(ANativeWindow* window, const Rect& srcRect, - SkBitmap* bitmap) { - // Setup the source - AHardwareBuffer* rawSourceBuffer; - int rawSourceFence; - Matrix4 texTransform; - status_t err = ANativeWindow_getLastQueuedBuffer(window, &rawSourceBuffer, &rawSourceFence, - texTransform.data); - base::unique_fd sourceFence(rawSourceFence); - texTransform.invalidateType(); - if (err != NO_ERROR) { - ALOGW("Failed to get last queued buffer, error = %d", err); - return CopyResult::UnknownError; - } - if (rawSourceBuffer == nullptr) { - ALOGW("Surface doesn't have any previously queued frames, nothing to readback from"); - return CopyResult::SourceEmpty; - } - - UniqueAHardwareBuffer sourceBuffer{rawSourceBuffer}; - AHardwareBuffer_Desc description; - AHardwareBuffer_describe(sourceBuffer.get(), &description); - if (description.usage & AHARDWAREBUFFER_USAGE_PROTECTED_CONTENT) { - ALOGW("Surface is protected, unable to copy from it"); - return CopyResult::SourceInvalid; - } - - if (sourceFence != -1 && sync_wait(sourceFence.get(), 500 /* ms */) != NO_ERROR) { - ALOGE("Timeout (500ms) exceeded waiting for buffer fence, abandoning readback attempt"); - return CopyResult::Timeout; - } - - sk_sp<SkColorSpace> colorSpace = DataSpaceToColorSpace( - static_cast<android_dataspace>(ANativeWindow_getBuffersDataSpace(window))); - sk_sp<SkImage> image = - SkImage::MakeFromAHardwareBuffer(sourceBuffer.get(), kPremul_SkAlphaType, colorSpace); - return copyImageInto(image, srcRect, bitmap); + return request->onCopyFinished(CopyResult::Success); } CopyResult Readback::copyHWBitmapInto(Bitmap* hwBitmap, SkBitmap* bitmap) { @@ -281,14 +283,14 @@ CopyResult Readback::copyImageInto(const sk_sp<SkImage>& image, SkBitmap* bitmap CopyResult Readback::copyImageInto(const sk_sp<SkImage>& image, const Rect& srcRect, SkBitmap* bitmap) { ATRACE_CALL(); + if (!image.get()) { + return CopyResult::UnknownError; + } if (Properties::getRenderPipelineType() == RenderPipelineType::SkiaGL) { mRenderThread.requireGlContext(); } else { mRenderThread.requireVkContext(); } - if (!image.get()) { - return CopyResult::UnknownError; - } int imgWidth = image->width(); int imgHeight = image->height(); sk_sp<GrDirectContext> grContext = sk_ref_sp(mRenderThread.getGrContext()); diff --git a/libs/hwui/Readback.h b/libs/hwui/Readback.h index d0d748ff5c16..a092d472abf0 100644 --- a/libs/hwui/Readback.h +++ b/libs/hwui/Readback.h @@ -16,11 +16,16 @@ #pragma once +#include <SkRefCnt.h> + +#include "CopyRequest.h" #include "Matrix.h" #include "Rect.h" #include "renderthread/RenderThread.h" -#include <SkBitmap.h> +class SkBitmap; +class SkImage; +struct SkRect; namespace android { class Bitmap; @@ -31,23 +36,13 @@ namespace uirenderer { class DeferredLayerUpdater; class Layer; -// Keep in sync with PixelCopy.java codes -enum class CopyResult { - Success = 0, - UnknownError = 1, - Timeout = 2, - SourceEmpty = 3, - SourceInvalid = 4, - DestinationInvalid = 5, -}; - class Readback { public: explicit Readback(renderthread::RenderThread& thread) : mRenderThread(thread) {} /** * Copies the surface's most recently queued buffer into the provided bitmap. */ - CopyResult copySurfaceInto(ANativeWindow* window, const Rect& srcRect, SkBitmap* bitmap); + void copySurfaceInto(ANativeWindow* window, const std::shared_ptr<CopyRequest>& request); CopyResult copyHWBitmapInto(Bitmap* hwBitmap, SkBitmap* bitmap); CopyResult copyImageInto(const sk_sp<SkImage>& image, SkBitmap* bitmap); @@ -55,7 +50,6 @@ public: CopyResult copyLayerInto(DeferredLayerUpdater* layer, SkBitmap* bitmap); private: - CopyResult copySurfaceIntoLegacy(ANativeWindow* window, const Rect& srcRect, SkBitmap* bitmap); CopyResult copyImageInto(const sk_sp<SkImage>& image, const Rect& srcRect, SkBitmap* bitmap); bool copyLayerInto(Layer* layer, const SkRect* srcRect, const SkRect* dstRect, diff --git a/libs/hwui/RecordingCanvas.cpp b/libs/hwui/RecordingCanvas.cpp index a285462eef74..f5ebfd5d9e23 100644 --- a/libs/hwui/RecordingCanvas.cpp +++ b/libs/hwui/RecordingCanvas.cpp @@ -29,10 +29,14 @@ #include "SkDrawShadowInfo.h" #include "SkImage.h" #include "SkImageFilter.h" +#include "SkImageInfo.h" #include "SkLatticeIter.h" #include "SkMath.h" +#include "SkPaint.h" #include "SkPicture.h" +#include "SkRRect.h" #include "SkRSXform.h" +#include "SkRect.h" #include "SkRegion.h" #include "SkTextBlob.h" #include "SkVertices.h" diff --git a/libs/hwui/RecordingCanvas.h b/libs/hwui/RecordingCanvas.h index 212b4e72dcb2..35bec9335d7c 100644 --- a/libs/hwui/RecordingCanvas.h +++ b/libs/hwui/RecordingCanvas.h @@ -34,6 +34,8 @@ #include <SkRuntimeEffect.h> #include <vector> +class SkRRect; + namespace android { namespace uirenderer { diff --git a/libs/hwui/RenderNode.h b/libs/hwui/RenderNode.h index da0476259b97..bdc48e91f6cb 100644 --- a/libs/hwui/RenderNode.h +++ b/libs/hwui/RenderNode.h @@ -16,7 +16,6 @@ #pragma once -#include <SkCamera.h> #include <SkMatrix.h> #include <utils/LinearAllocator.h> diff --git a/libs/hwui/SkiaCanvas.cpp b/libs/hwui/SkiaCanvas.cpp index 53c6db0cdf3a..473afbd2aa2f 100644 --- a/libs/hwui/SkiaCanvas.cpp +++ b/libs/hwui/SkiaCanvas.cpp @@ -18,6 +18,7 @@ #include "CanvasProperty.h" #include "NinePatchUtils.h" +#include "SkBlendMode.h" #include "VectorDrawable.h" #include "hwui/Bitmap.h" #include "hwui/MinikinUtils.h" @@ -27,6 +28,7 @@ #include <SkAndroidFrameworkUtils.h> #include <SkAnimatedImage.h> +#include <SkBitmap.h> #include <SkCanvasPriv.h> #include <SkCanvasStateUtils.h> #include <SkColorFilter.h> @@ -36,8 +38,13 @@ #include <SkGraphics.h> #include <SkImage.h> #include <SkImagePriv.h> +#include <SkMatrix.h> +#include <SkPaint.h> #include <SkPicture.h> #include <SkRSXform.h> +#include <SkRRect.h> +#include <SkRect.h> +#include <SkRefCnt.h> #include <SkShader.h> #include <SkTemplates.h> #include <SkTextBlob.h> @@ -51,6 +58,49 @@ namespace android { using uirenderer::PaintUtils; +class SkiaCanvas::Clip { +public: + Clip(const SkRect& rect, SkClipOp op, const SkMatrix& m) + : mType(Type::Rect), mOp(op), mMatrix(m), mRRect(SkRRect::MakeRect(rect)) {} + Clip(const SkRRect& rrect, SkClipOp op, const SkMatrix& m) + : mType(Type::RRect), mOp(op), mMatrix(m), mRRect(rrect) {} + Clip(const SkPath& path, SkClipOp op, const SkMatrix& m) + : mType(Type::Path), mOp(op), mMatrix(m), mPath(std::in_place, path) {} + + void apply(SkCanvas* canvas) const { + canvas->setMatrix(mMatrix); + switch (mType) { + case Type::Rect: + // Don't anti-alias rectangular clips + canvas->clipRect(mRRect.rect(), mOp, false); + break; + case Type::RRect: + // Ensure rounded rectangular clips are anti-aliased + canvas->clipRRect(mRRect, mOp, true); + break; + case Type::Path: + // Ensure path clips are anti-aliased + canvas->clipPath(mPath.value(), mOp, true); + break; + } + } + +private: + enum class Type { + Rect, + RRect, + Path, + }; + + Type mType; + SkClipOp mOp; + SkMatrix mMatrix; + + // These are logically a union (tracked separately due to non-POD path). + std::optional<SkPath> mPath; + SkRRect mRRect; +}; + Canvas* Canvas::create_canvas(const SkBitmap& bitmap) { return new SkiaCanvas(bitmap); } @@ -194,49 +244,6 @@ void SkiaCanvas::restoreUnclippedLayer(int restoreCount, const Paint& paint) { } } -class SkiaCanvas::Clip { -public: - Clip(const SkRect& rect, SkClipOp op, const SkMatrix& m) - : mType(Type::Rect), mOp(op), mMatrix(m), mRRect(SkRRect::MakeRect(rect)) {} - Clip(const SkRRect& rrect, SkClipOp op, const SkMatrix& m) - : mType(Type::RRect), mOp(op), mMatrix(m), mRRect(rrect) {} - Clip(const SkPath& path, SkClipOp op, const SkMatrix& m) - : mType(Type::Path), mOp(op), mMatrix(m), mPath(std::in_place, path) {} - - void apply(SkCanvas* canvas) const { - canvas->setMatrix(mMatrix); - switch (mType) { - case Type::Rect: - // Don't anti-alias rectangular clips - canvas->clipRect(mRRect.rect(), mOp, false); - break; - case Type::RRect: - // Ensure rounded rectangular clips are anti-aliased - canvas->clipRRect(mRRect, mOp, true); - break; - case Type::Path: - // Ensure path clips are anti-aliased - canvas->clipPath(mPath.value(), mOp, true); - break; - } - } - -private: - enum class Type { - Rect, - RRect, - Path, - }; - - Type mType; - SkClipOp mOp; - SkMatrix mMatrix; - - // These are logically a union (tracked separately due to non-POD path). - std::optional<SkPath> mPath; - SkRRect mRRect; -}; - const SkiaCanvas::SaveRec* SkiaCanvas::currentSaveRec() const { const SaveRec* rec = mSaveStack ? static_cast<const SaveRec*>(mSaveStack->back()) : nullptr; int currentSaveCount = mCanvas->getSaveCount(); @@ -245,10 +252,11 @@ const SkiaCanvas::SaveRec* SkiaCanvas::currentSaveRec() const { return (rec && rec->saveCount == currentSaveCount) ? rec : nullptr; } -void SkiaCanvas::punchHole(const SkRRect& rect) { +void SkiaCanvas::punchHole(const SkRRect& rect, float alpha) { SkPaint paint = SkPaint(); - paint.setColor(0); - paint.setBlendMode(SkBlendMode::kClear); + paint.setColor(SkColors::kBlack); + paint.setAlphaf(alpha); + paint.setBlendMode(SkBlendMode::kDstOut); mCanvas->drawRRect(rect, paint); } diff --git a/libs/hwui/SkiaCanvas.h b/libs/hwui/SkiaCanvas.h index 715007cdcd3b..51007c52260d 100644 --- a/libs/hwui/SkiaCanvas.h +++ b/libs/hwui/SkiaCanvas.h @@ -33,6 +33,8 @@ #include <cassert> #include <optional> +class SkRRect; + namespace android { // Holds an SkCanvas reference plus additional native data. @@ -63,7 +65,7 @@ public: LOG_ALWAYS_FATAL("SkiaCanvas does not support enableZ"); } - virtual void punchHole(const SkRRect& rect) override; + virtual void punchHole(const SkRRect& rect, float alpha) override; virtual void setBitmap(const SkBitmap& bitmap) override; diff --git a/libs/hwui/VectorDrawable.cpp b/libs/hwui/VectorDrawable.cpp index 983c7766273a..536ff781badc 100644 --- a/libs/hwui/VectorDrawable.cpp +++ b/libs/hwui/VectorDrawable.cpp @@ -21,9 +21,10 @@ #include <utils/Log.h> #include "PathParser.h" -#include "SkColorFilter.h" +#include "SkImage.h" #include "SkImageInfo.h" -#include "SkShader.h" +#include "SkSamplingOptions.h" +#include "SkScalar.h" #include "hwui/Paint.h" #ifdef __ANDROID__ diff --git a/libs/hwui/VectorDrawable.h b/libs/hwui/VectorDrawable.h index 30bb04ae8361..c92654c479c1 100644 --- a/libs/hwui/VectorDrawable.h +++ b/libs/hwui/VectorDrawable.h @@ -31,6 +31,7 @@ #include <SkPath.h> #include <SkPathMeasure.h> #include <SkRect.h> +#include <SkRefCnt.h> #include <SkShader.h> #include <SkSurface.h> diff --git a/libs/hwui/apex/LayoutlibLoader.cpp b/libs/hwui/apex/LayoutlibLoader.cpp index 942c0506321c..b7a15633ff6d 100644 --- a/libs/hwui/apex/LayoutlibLoader.cpp +++ b/libs/hwui/apex/LayoutlibLoader.cpp @@ -53,6 +53,7 @@ extern int register_android_graphics_FontFamily(JNIEnv* env); extern int register_android_graphics_Matrix(JNIEnv* env); extern int register_android_graphics_Paint(JNIEnv* env); extern int register_android_graphics_Path(JNIEnv* env); +extern int register_android_graphics_PathIterator(JNIEnv* env); extern int register_android_graphics_PathMeasure(JNIEnv* env); extern int register_android_graphics_Picture(JNIEnv* env); extern int register_android_graphics_Region(JNIEnv* env); @@ -100,6 +101,7 @@ static const std::unordered_map<std::string, RegJNIRec> gRegJNIMap = { {"android.graphics.Paint", REG_JNI(register_android_graphics_Paint)}, {"android.graphics.Path", REG_JNI(register_android_graphics_Path)}, {"android.graphics.PathEffect", REG_JNI(register_android_graphics_PathEffect)}, + {"android.graphics.PathIterator", REG_JNI(register_android_graphics_PathIterator)}, {"android.graphics.PathMeasure", REG_JNI(register_android_graphics_PathMeasure)}, {"android.graphics.Picture", REG_JNI(register_android_graphics_Picture)}, {"android.graphics.RecordingCanvas", REG_JNI(register_android_view_DisplayListCanvas)}, diff --git a/libs/hwui/apex/android_bitmap.cpp b/libs/hwui/apex/android_bitmap.cpp index bc6bc456ba5a..c442a7b1d17c 100644 --- a/libs/hwui/apex/android_bitmap.cpp +++ b/libs/hwui/apex/android_bitmap.cpp @@ -24,6 +24,11 @@ #include <GraphicsJNI.h> #include <hwui/Bitmap.h> +#include <SkBitmap.h> +#include <SkColorSpace.h> +#include <SkImageInfo.h> +#include <SkRefCnt.h> +#include <SkStream.h> #include <utils/Color.h> using namespace android; diff --git a/libs/hwui/apex/android_canvas.cpp b/libs/hwui/apex/android_canvas.cpp index 2a939efed9bb..905b123076a2 100644 --- a/libs/hwui/apex/android_canvas.cpp +++ b/libs/hwui/apex/android_canvas.cpp @@ -23,7 +23,9 @@ #include <utils/Color.h> #include <SkBitmap.h> +#include <SkColorSpace.h> #include <SkSurface.h> +#include <SkRefCnt.h> using namespace android; diff --git a/libs/hwui/apex/jni_runtime.cpp b/libs/hwui/apex/jni_runtime.cpp index e1f5abd786bf..39725a55b594 100644 --- a/libs/hwui/apex/jni_runtime.cpp +++ b/libs/hwui/apex/jni_runtime.cpp @@ -59,6 +59,7 @@ extern int register_android_graphics_HardwareRendererObserver(JNIEnv* env); extern int register_android_graphics_Matrix(JNIEnv* env); extern int register_android_graphics_Paint(JNIEnv* env); extern int register_android_graphics_Path(JNIEnv* env); +extern int register_android_graphics_PathIterator(JNIEnv* env); extern int register_android_graphics_PathMeasure(JNIEnv* env); extern int register_android_graphics_Picture(JNIEnv*); extern int register_android_graphics_Region(JNIEnv* env); @@ -94,59 +95,60 @@ extern int register_android_view_ThreadedRenderer(JNIEnv* env); }; #endif -static const RegJNIRec gRegJNI[] = { - REG_JNI(register_android_graphics_Canvas), - // This needs to be before register_android_graphics_Graphics, or the latter - // will not be able to find the jmethodID for ColorSpace.get(). - REG_JNI(register_android_graphics_ColorSpace), - REG_JNI(register_android_graphics_Graphics), - REG_JNI(register_android_graphics_Bitmap), - REG_JNI(register_android_graphics_BitmapFactory), - REG_JNI(register_android_graphics_BitmapRegionDecoder), - REG_JNI(register_android_graphics_ByteBufferStreamAdaptor), - REG_JNI(register_android_graphics_Camera), - REG_JNI(register_android_graphics_CreateJavaOutputStreamAdaptor), - REG_JNI(register_android_graphics_CanvasProperty), - REG_JNI(register_android_graphics_ColorFilter), - REG_JNI(register_android_graphics_DrawFilter), - REG_JNI(register_android_graphics_FontFamily), - REG_JNI(register_android_graphics_HardwareRendererObserver), - REG_JNI(register_android_graphics_ImageDecoder), - REG_JNI(register_android_graphics_drawable_AnimatedImageDrawable), - REG_JNI(register_android_graphics_Interpolator), - REG_JNI(register_android_graphics_MaskFilter), - REG_JNI(register_android_graphics_Matrix), - REG_JNI(register_android_graphics_Movie), - REG_JNI(register_android_graphics_NinePatch), - REG_JNI(register_android_graphics_Paint), - REG_JNI(register_android_graphics_Path), - REG_JNI(register_android_graphics_PathMeasure), - REG_JNI(register_android_graphics_PathEffect), - REG_JNI(register_android_graphics_Picture), - REG_JNI(register_android_graphics_Region), - REG_JNI(register_android_graphics_Shader), - REG_JNI(register_android_graphics_RenderEffect), - REG_JNI(register_android_graphics_TextureLayer), - REG_JNI(register_android_graphics_Typeface), - REG_JNI(register_android_graphics_YuvImage), - REG_JNI(register_android_graphics_animation_NativeInterpolatorFactory), - REG_JNI(register_android_graphics_animation_RenderNodeAnimator), - REG_JNI(register_android_graphics_drawable_AnimatedVectorDrawable), - REG_JNI(register_android_graphics_drawable_VectorDrawable), - REG_JNI(register_android_graphics_fonts_Font), - REG_JNI(register_android_graphics_fonts_FontFamily), - REG_JNI(register_android_graphics_pdf_PdfDocument), - REG_JNI(register_android_graphics_pdf_PdfEditor), - REG_JNI(register_android_graphics_pdf_PdfRenderer), - REG_JNI(register_android_graphics_text_MeasuredText), - REG_JNI(register_android_graphics_text_LineBreaker), - REG_JNI(register_android_graphics_text_TextShaper), - - REG_JNI(register_android_util_PathParser), - REG_JNI(register_android_view_RenderNode), - REG_JNI(register_android_view_DisplayListCanvas), - REG_JNI(register_android_view_ThreadedRenderer), -}; + static const RegJNIRec gRegJNI[] = { + REG_JNI(register_android_graphics_Canvas), + // This needs to be before register_android_graphics_Graphics, or the latter + // will not be able to find the jmethodID for ColorSpace.get(). + REG_JNI(register_android_graphics_ColorSpace), + REG_JNI(register_android_graphics_Graphics), + REG_JNI(register_android_graphics_Bitmap), + REG_JNI(register_android_graphics_BitmapFactory), + REG_JNI(register_android_graphics_BitmapRegionDecoder), + REG_JNI(register_android_graphics_ByteBufferStreamAdaptor), + REG_JNI(register_android_graphics_Camera), + REG_JNI(register_android_graphics_CreateJavaOutputStreamAdaptor), + REG_JNI(register_android_graphics_CanvasProperty), + REG_JNI(register_android_graphics_ColorFilter), + REG_JNI(register_android_graphics_DrawFilter), + REG_JNI(register_android_graphics_FontFamily), + REG_JNI(register_android_graphics_HardwareRendererObserver), + REG_JNI(register_android_graphics_ImageDecoder), + REG_JNI(register_android_graphics_drawable_AnimatedImageDrawable), + REG_JNI(register_android_graphics_Interpolator), + REG_JNI(register_android_graphics_MaskFilter), + REG_JNI(register_android_graphics_Matrix), + REG_JNI(register_android_graphics_Movie), + REG_JNI(register_android_graphics_NinePatch), + REG_JNI(register_android_graphics_Paint), + REG_JNI(register_android_graphics_Path), + REG_JNI(register_android_graphics_PathIterator), + REG_JNI(register_android_graphics_PathMeasure), + REG_JNI(register_android_graphics_PathEffect), + REG_JNI(register_android_graphics_Picture), + REG_JNI(register_android_graphics_Region), + REG_JNI(register_android_graphics_Shader), + REG_JNI(register_android_graphics_RenderEffect), + REG_JNI(register_android_graphics_TextureLayer), + REG_JNI(register_android_graphics_Typeface), + REG_JNI(register_android_graphics_YuvImage), + REG_JNI(register_android_graphics_animation_NativeInterpolatorFactory), + REG_JNI(register_android_graphics_animation_RenderNodeAnimator), + REG_JNI(register_android_graphics_drawable_AnimatedVectorDrawable), + REG_JNI(register_android_graphics_drawable_VectorDrawable), + REG_JNI(register_android_graphics_fonts_Font), + REG_JNI(register_android_graphics_fonts_FontFamily), + REG_JNI(register_android_graphics_pdf_PdfDocument), + REG_JNI(register_android_graphics_pdf_PdfEditor), + REG_JNI(register_android_graphics_pdf_PdfRenderer), + REG_JNI(register_android_graphics_text_MeasuredText), + REG_JNI(register_android_graphics_text_LineBreaker), + REG_JNI(register_android_graphics_text_TextShaper), + + REG_JNI(register_android_util_PathParser), + REG_JNI(register_android_view_RenderNode), + REG_JNI(register_android_view_DisplayListCanvas), + REG_JNI(register_android_view_ThreadedRenderer), + }; } // namespace android diff --git a/libs/hwui/canvas/CanvasOps.h b/libs/hwui/canvas/CanvasOps.h index fdc97a4fd8ba..2dcbca8273e7 100644 --- a/libs/hwui/canvas/CanvasOps.h +++ b/libs/hwui/canvas/CanvasOps.h @@ -17,13 +17,19 @@ #pragma once #include <SkAndroidFrameworkUtils.h> +#include <SkBlendMode.h> #include <SkCanvas.h> -#include <SkPath.h> -#include <SkRegion.h> -#include <SkVertices.h> +#include <SkClipOp.h> #include <SkImage.h> +#include <SkPaint.h> +#include <SkPath.h> #include <SkPicture.h> +#include <SkRRect.h> +#include <SkRect.h> +#include <SkRegion.h> #include <SkRuntimeEffect.h> +#include <SkSamplingOptions.h> +#include <SkVertices.h> #include <log/log.h> diff --git a/libs/hwui/hwui/Bitmap.cpp b/libs/hwui/hwui/Bitmap.cpp index 67f47580a70f..feafc2372442 100644 --- a/libs/hwui/hwui/Bitmap.cpp +++ b/libs/hwui/hwui/Bitmap.cpp @@ -35,9 +35,15 @@ #endif #include <SkCanvas.h> +#include <SkColor.h> +#include <SkEncodedImageFormat.h> +#include <SkHighContrastFilter.h> +#include <SkImageEncoder.h> #include <SkImagePriv.h> +#include <SkPixmap.h> +#include <SkRect.h> +#include <SkStream.h> #include <SkWebpEncoder.h> -#include <SkHighContrastFilter.h> #include <limits> namespace android { diff --git a/libs/hwui/hwui/Bitmap.h b/libs/hwui/hwui/Bitmap.h index 94a047c06ced..133f1fe0a1e7 100644 --- a/libs/hwui/hwui/Bitmap.h +++ b/libs/hwui/hwui/Bitmap.h @@ -19,9 +19,9 @@ #include <SkColorFilter.h> #include <SkColorSpace.h> #include <SkImage.h> -#include <SkImage.h> #include <SkImageInfo.h> #include <SkPixelRef.h> +#include <SkRefCnt.h> #include <cutils/compiler.h> #ifdef __ANDROID__ // Layoutlib does not support hardware acceleration #include <android/hardware_buffer.h> diff --git a/libs/hwui/hwui/BlurDrawLooper.cpp b/libs/hwui/hwui/BlurDrawLooper.cpp index 270d24af99fd..d4b0198d015d 100644 --- a/libs/hwui/hwui/BlurDrawLooper.cpp +++ b/libs/hwui/hwui/BlurDrawLooper.cpp @@ -15,6 +15,7 @@ */ #include "BlurDrawLooper.h" +#include <SkColorSpace.h> #include <SkMaskFilter.h> namespace android { diff --git a/libs/hwui/hwui/Canvas.cpp b/libs/hwui/hwui/Canvas.cpp index b046f45d9c57..cd8af3d933b1 100644 --- a/libs/hwui/hwui/Canvas.cpp +++ b/libs/hwui/hwui/Canvas.cpp @@ -26,6 +26,7 @@ #include "hwui/PaintFilter.h" #include <SkFontMetrics.h> +#include <SkRRect.h> namespace android { diff --git a/libs/hwui/hwui/Canvas.h b/libs/hwui/hwui/Canvas.h index 82777646f3a2..82d23b51b12a 100644 --- a/libs/hwui/hwui/Canvas.h +++ b/libs/hwui/hwui/Canvas.h @@ -31,6 +31,7 @@ class SkAnimatedImage; class SkCanvasState; +class SkRRect; class SkRuntimeShaderBuilder; class SkVertices; @@ -151,7 +152,7 @@ public: LOG_ALWAYS_FATAL("Not supported"); } - virtual void punchHole(const SkRRect& rect) = 0; + virtual void punchHole(const SkRRect& rect, float alpha) = 0; // ---------------------------------------------------------------------------- // Canvas state operations diff --git a/libs/hwui/hwui/ImageDecoder.h b/libs/hwui/hwui/ImageDecoder.h index cef2233fc371..b6d73b39d8d0 100644 --- a/libs/hwui/hwui/ImageDecoder.h +++ b/libs/hwui/hwui/ImageDecoder.h @@ -17,9 +17,11 @@ #include <SkAndroidCodec.h> #include <SkCodec.h> +#include <SkColorSpace.h> #include <SkImageInfo.h> #include <SkPngChunkReader.h> #include <SkRect.h> +#include <SkRefCnt.h> #include <SkSize.h> #include <cutils/compiler.h> diff --git a/libs/hwui/hwui/MinikinSkia.cpp b/libs/hwui/hwui/MinikinSkia.cpp index 2db3ace1cd43..34cb4aef70d9 100644 --- a/libs/hwui/hwui/MinikinSkia.cpp +++ b/libs/hwui/hwui/MinikinSkia.cpp @@ -16,10 +16,13 @@ #include "MinikinSkia.h" -#include <SkFontDescriptor.h> #include <SkFont.h> +#include <SkFontDescriptor.h> #include <SkFontMetrics.h> #include <SkFontMgr.h> +#include <SkRect.h> +#include <SkScalar.h> +#include <SkStream.h> #include <SkTypeface.h> #include <log/log.h> diff --git a/libs/hwui/hwui/Typeface.cpp b/libs/hwui/hwui/Typeface.cpp index 5a9d2508230e..3c67edc9a428 100644 --- a/libs/hwui/hwui/Typeface.cpp +++ b/libs/hwui/hwui/Typeface.cpp @@ -125,9 +125,14 @@ Typeface* Typeface::createWithDifferentBaseWeight(Typeface* src, int weight) { } Typeface* Typeface::createFromFamilies(std::vector<std::shared_ptr<minikin::FontFamily>>&& families, - int weight, int italic) { + int weight, int italic, const Typeface* fallback) { Typeface* result = new Typeface; - result->fFontCollection.reset(new minikin::FontCollection(families)); + if (fallback == nullptr) { + result->fFontCollection = minikin::FontCollection::create(std::move(families)); + } else { + result->fFontCollection = + fallback->fFontCollection->createCollectionWithFamilies(std::move(families)); + } if (weight == RESOLVE_BY_FONT_TABLE || italic == RESOLVE_BY_FONT_TABLE) { int weightFromFont; @@ -191,8 +196,8 @@ void Typeface::setRobotoTypefaceForTest() { std::vector<std::shared_ptr<minikin::Font>> fonts; fonts.push_back(minikin::Font::Builder(font).build()); - std::shared_ptr<minikin::FontCollection> collection = std::make_shared<minikin::FontCollection>( - std::make_shared<minikin::FontFamily>(std::move(fonts))); + std::shared_ptr<minikin::FontCollection> collection = + minikin::FontCollection::create(minikin::FontFamily::create(std::move(fonts))); Typeface* hwTypeface = new Typeface(); hwTypeface->fFontCollection = collection; diff --git a/libs/hwui/hwui/Typeface.h b/libs/hwui/hwui/Typeface.h index 0c3ef01ab26b..565136e53676 100644 --- a/libs/hwui/hwui/Typeface.h +++ b/libs/hwui/hwui/Typeface.h @@ -78,7 +78,8 @@ public: Typeface* src, const std::vector<minikin::FontVariation>& variations); static Typeface* createFromFamilies( - std::vector<std::shared_ptr<minikin::FontFamily>>&& families, int weight, int italic); + std::vector<std::shared_ptr<minikin::FontFamily>>&& families, int weight, int italic, + const Typeface* fallback); static void setDefault(const Typeface* face); diff --git a/libs/hwui/jni/AnimatedImageDrawable.cpp b/libs/hwui/jni/AnimatedImageDrawable.cpp index c40b858268be..373e893b9a25 100644 --- a/libs/hwui/jni/AnimatedImageDrawable.cpp +++ b/libs/hwui/jni/AnimatedImageDrawable.cpp @@ -21,8 +21,11 @@ #include <SkAndroidCodec.h> #include <SkAnimatedImage.h> #include <SkColorFilter.h> +#include <SkEncodedImageFormat.h> #include <SkPicture.h> #include <SkPictureRecorder.h> +#include <SkRect.h> +#include <SkRefCnt.h> #include <hwui/AnimatedImageDrawable.h> #include <hwui/ImageDecoder.h> #include <hwui/Canvas.h> diff --git a/libs/hwui/jni/Bitmap.cpp b/libs/hwui/jni/Bitmap.cpp index 5db0783cf83e..94cea65897cf 100755 --- a/libs/hwui/jni/Bitmap.cpp +++ b/libs/hwui/jni/Bitmap.cpp @@ -2,17 +2,25 @@ #define LOG_TAG "Bitmap" #include "Bitmap.h" +#include "GraphicsJNI.h" #include "SkBitmap.h" +#include "SkBlendMode.h" #include "SkCanvas.h" #include "SkColor.h" #include "SkColorSpace.h" -#include "SkPixelRef.h" +#include "SkData.h" #include "SkImageEncoder.h" #include "SkImageInfo.h" -#include "GraphicsJNI.h" +#include "SkPaint.h" +#include "SkPixelRef.h" +#include "SkPixmap.h" +#include "SkPoint.h" +#include "SkRefCnt.h" #include "SkStream.h" +#include "SkTypes.h" #include "SkWebpEncoder.h" + #include "android_nio_utils.h" #include "CreateJavaOutputStreamAdaptor.h" #include <hwui/Paint.h> diff --git a/libs/hwui/jni/Bitmap.h b/libs/hwui/jni/Bitmap.h index 73eca3aa8ef8..21a93f066d9b 100644 --- a/libs/hwui/jni/Bitmap.h +++ b/libs/hwui/jni/Bitmap.h @@ -19,7 +19,6 @@ #include <jni.h> #include <android/bitmap.h> -class SkBitmap; struct SkImageInfo; namespace android { diff --git a/libs/hwui/jni/BitmapFactory.cpp b/libs/hwui/jni/BitmapFactory.cpp index 4e9daa4b0c16..320d3322904f 100644 --- a/libs/hwui/jni/BitmapFactory.cpp +++ b/libs/hwui/jni/BitmapFactory.cpp @@ -8,9 +8,19 @@ #include "MimeType.h" #include "NinePatchPeeker.h" #include "SkAndroidCodec.h" +#include "SkBitmap.h" +#include "SkBlendMode.h" #include "SkCanvas.h" +#include "SkColorSpace.h" +#include "SkEncodedImageFormat.h" +#include "SkImageInfo.h" #include "SkMath.h" +#include "SkPaint.h" #include "SkPixelRef.h" +#include "SkRect.h" +#include "SkRefCnt.h" +#include "SkSamplingOptions.h" +#include "SkSize.h" #include "SkStream.h" #include "SkString.h" #include "SkUtils.h" diff --git a/libs/hwui/jni/BitmapRegionDecoder.cpp b/libs/hwui/jni/BitmapRegionDecoder.cpp index 1c20415dcc8f..eb56ae310231 100644 --- a/libs/hwui/jni/BitmapRegionDecoder.cpp +++ b/libs/hwui/jni/BitmapRegionDecoder.cpp @@ -25,6 +25,7 @@ #include "BitmapRegionDecoder.h" #include "SkBitmap.h" #include "SkCodec.h" +#include "SkColorSpace.h" #include "SkData.h" #include "SkStream.h" diff --git a/libs/hwui/jni/ByteBufferStreamAdaptor.cpp b/libs/hwui/jni/ByteBufferStreamAdaptor.cpp index b10540cb3fbd..97dbc9ac171f 100644 --- a/libs/hwui/jni/ByteBufferStreamAdaptor.cpp +++ b/libs/hwui/jni/ByteBufferStreamAdaptor.cpp @@ -2,6 +2,7 @@ #include "GraphicsJNI.h" #include "Utils.h" +#include <SkData.h> #include <SkStream.h> using namespace android; diff --git a/libs/hwui/jni/FontFamily.cpp b/libs/hwui/jni/FontFamily.cpp index ce5ac382aeff..c146adac6b69 100644 --- a/libs/hwui/jni/FontFamily.cpp +++ b/libs/hwui/jni/FontFamily.cpp @@ -24,6 +24,7 @@ #include "SkData.h" #include "SkFontMgr.h" #include "SkRefCnt.h" +#include "SkStream.h" #include "SkTypeface.h" #include "Utils.h" #include "fonts/Font.h" @@ -84,9 +85,9 @@ static jlong FontFamily_create(CRITICAL_JNI_PARAMS_COMMA jlong builderPtr) { if (builder->fonts.empty()) { return 0; } - std::shared_ptr<minikin::FontFamily> family = std::make_shared<minikin::FontFamily>( - builder->langId, builder->variant, std::move(builder->fonts), - true /* isCustomFallback */); + std::shared_ptr<minikin::FontFamily> family = + minikin::FontFamily::create(builder->langId, builder->variant, + std::move(builder->fonts), true /* isCustomFallback */); if (family->getCoverage().length() == 0) { return 0; } diff --git a/libs/hwui/jni/GIFMovie.cpp b/libs/hwui/jni/GIFMovie.cpp index fef51b8d2f79..ae6ac4ce4ecc 100644 --- a/libs/hwui/jni/GIFMovie.cpp +++ b/libs/hwui/jni/GIFMovie.cpp @@ -7,9 +7,11 @@ #include "Movie.h" +#include "SkBitmap.h" #include "SkColor.h" #include "SkColorPriv.h" #include "SkStream.h" +#include "SkTypes.h" #include "gif_lib.h" diff --git a/libs/hwui/jni/Graphics.cpp b/libs/hwui/jni/Graphics.cpp index 33669ac0a34e..6a3bc8fe1152 100644 --- a/libs/hwui/jni/Graphics.cpp +++ b/libs/hwui/jni/Graphics.cpp @@ -8,10 +8,18 @@ #include <nativehelper/JNIHelp.h> #include "GraphicsJNI.h" +#include "include/private/SkTemplates.h" // SkTAddOffset +#include "SkBitmap.h" #include "SkCanvas.h" +#include "SkColorSpace.h" #include "SkFontMetrics.h" +#include "SkImageInfo.h" #include "SkMath.h" +#include "SkPixelRef.h" +#include "SkPoint.h" +#include "SkRect.h" #include "SkRegion.h" +#include "SkTypes.h" #include <cutils/ashmem.h> #include <hwui/Canvas.h> diff --git a/libs/hwui/jni/ImageDecoder.cpp b/libs/hwui/jni/ImageDecoder.cpp index f7b8c014be6e..bad710dec274 100644 --- a/libs/hwui/jni/ImageDecoder.cpp +++ b/libs/hwui/jni/ImageDecoder.cpp @@ -29,8 +29,12 @@ #include <FrontBufferedStream.h> #include <SkAndroidCodec.h> -#include <SkEncodedImageFormat.h> +#include <SkBitmap.h> +#include <SkColorSpace.h> +#include <SkImageInfo.h> +#include <SkRect.h> #include <SkStream.h> +#include <SkString.h> #include <androidfw/Asset.h> #include <fcntl.h> diff --git a/libs/hwui/jni/Movie.h b/libs/hwui/jni/Movie.h index 736890d5215e..02113dd58ec8 100644 --- a/libs/hwui/jni/Movie.h +++ b/libs/hwui/jni/Movie.h @@ -13,6 +13,7 @@ #include "SkBitmap.h" #include "SkCanvas.h" #include "SkRefCnt.h" +#include "SkTypes.h" class SkStreamRewindable; diff --git a/libs/hwui/jni/MovieImpl.cpp b/libs/hwui/jni/MovieImpl.cpp index ae9e04e617b0..abb75fa99c94 100644 --- a/libs/hwui/jni/MovieImpl.cpp +++ b/libs/hwui/jni/MovieImpl.cpp @@ -5,11 +5,12 @@ * found in the LICENSE file. */ #include "Movie.h" -#include "SkCanvas.h" -#include "SkPaint.h" +#include "SkBitmap.h" +#include "SkStream.h" +#include "SkTypes.h" // We should never see this in normal operation since our time values are -// 0-based. So we use it as a sentinal. +// 0-based. So we use it as a sentinel. #define UNINITIALIZED_MSEC ((SkMSec)-1) Movie::Movie() @@ -81,8 +82,6 @@ const SkBitmap& Movie::bitmap() //////////////////////////////////////////////////////////////////// -#include "SkStream.h" - Movie* Movie::DecodeMemory(const void* data, size_t length) { SkMemoryStream stream(data, length, false); return Movie::DecodeStream(&stream); diff --git a/libs/hwui/jni/NinePatch.cpp b/libs/hwui/jni/NinePatch.cpp index 08fc80fbdafd..d50a8a22b5cb 100644 --- a/libs/hwui/jni/NinePatch.cpp +++ b/libs/hwui/jni/NinePatch.cpp @@ -24,8 +24,10 @@ #include <hwui/Paint.h> #include <utils/Log.h> +#include "SkBitmap.h" #include "SkCanvas.h" #include "SkLatticeIter.h" +#include "SkRect.h" #include "SkRegion.h" #include "GraphicsJNI.h" #include "NinePatchPeeker.h" diff --git a/libs/hwui/jni/NinePatchPeeker.cpp b/libs/hwui/jni/NinePatchPeeker.cpp index 9171fc687276..d85ede5dc6d2 100644 --- a/libs/hwui/jni/NinePatchPeeker.cpp +++ b/libs/hwui/jni/NinePatchPeeker.cpp @@ -16,7 +16,7 @@ #include "NinePatchPeeker.h" -#include <SkBitmap.h> +#include <SkScalar.h> #include <cutils/compiler.h> using namespace android; diff --git a/libs/hwui/jni/Paint.cpp b/libs/hwui/jni/Paint.cpp index f76863255153..f0a4bd0f00f0 100644 --- a/libs/hwui/jni/Paint.cpp +++ b/libs/hwui/jni/Paint.cpp @@ -26,6 +26,7 @@ #include <nativehelper/ScopedPrimitiveArray.h> #include "SkColorFilter.h" +#include "SkColorSpace.h" #include "SkFont.h" #include "SkFontMetrics.h" #include "SkFontTypes.h" @@ -496,17 +497,32 @@ namespace PaintGlue { return true; } - static jfloat doRunAdvance(const Paint* paint, const Typeface* typeface, const jchar buf[], - jint start, jint count, jint bufSize, jboolean isRtl, jint offset) { + static jfloat doRunAdvance(JNIEnv* env, const Paint* paint, const Typeface* typeface, + const jchar buf[], jint start, jint count, jint bufSize, + jboolean isRtl, jint offset, jfloatArray advances, + jint advancesIndex) { + if (advances) { + size_t advancesLength = env->GetArrayLength(advances); + if ((size_t)(count + advancesIndex) > advancesLength) { + doThrowAIOOBE(env); + return 0; + } + } minikin::Bidi bidiFlags = isRtl ? minikin::Bidi::FORCE_RTL : minikin::Bidi::FORCE_LTR; - if (offset == start + count) { + if (offset == start + count && advances == nullptr) { return MinikinUtils::measureText(paint, bidiFlags, typeface, buf, start, count, bufSize, nullptr); } std::unique_ptr<float[]> advancesArray(new float[count]); MinikinUtils::measureText(paint, bidiFlags, typeface, buf, start, count, bufSize, advancesArray.get()); - return minikin::getRunAdvance(advancesArray.get(), buf, start, count, offset); + + float result = minikin::getRunAdvance(advancesArray.get(), buf, start, count, offset); + if (advances) { + minikin::distributeAdvances(advancesArray.get(), buf, start, count); + env->SetFloatArrayRegion(advances, advancesIndex, count, advancesArray.get()); + } + return result; } static jfloat getRunAdvance___CIIIIZI_F(JNIEnv *env, jclass, jlong paintHandle, jcharArray text, @@ -514,9 +530,23 @@ namespace PaintGlue { const Paint* paint = reinterpret_cast<Paint*>(paintHandle); const Typeface* typeface = paint->getAndroidTypeface(); ScopedCharArrayRO textArray(env, text); - jfloat result = doRunAdvance(paint, typeface, textArray.get() + contextStart, - start - contextStart, end - start, contextEnd - contextStart, isRtl, - offset - contextStart); + jfloat result = doRunAdvance(env, paint, typeface, textArray.get() + contextStart, + start - contextStart, end - start, contextEnd - contextStart, + isRtl, offset - contextStart, nullptr, 0); + return result; + } + + static jfloat getRunCharacterAdvance___CIIIIZI_FI_F(JNIEnv* env, jclass, jlong paintHandle, + jcharArray text, jint start, jint end, + jint contextStart, jint contextEnd, + jboolean isRtl, jint offset, + jfloatArray advances, jint advancesIndex) { + const Paint* paint = reinterpret_cast<Paint*>(paintHandle); + const Typeface* typeface = paint->getAndroidTypeface(); + ScopedCharArrayRO textArray(env, text); + jfloat result = doRunAdvance(env, paint, typeface, textArray.get() + contextStart, + start - contextStart, end - start, contextEnd - contextStart, + isRtl, offset - contextStart, advances, advancesIndex); return result; } @@ -1033,113 +1063,112 @@ namespace PaintGlue { }; // namespace PaintGlue static const JNINativeMethod methods[] = { - {"nGetNativeFinalizer", "()J", (void*) PaintGlue::getNativeFinalizer}, - {"nInit","()J", (void*) PaintGlue::init}, - {"nInitWithPaint","(J)J", (void*) PaintGlue::initWithPaint}, - {"nBreakText","(J[CIIFI[F)I", (void*) PaintGlue::breakTextC}, - {"nBreakText","(JLjava/lang/String;ZFI[F)I", (void*) PaintGlue::breakTextS}, - {"nGetTextAdvances","(J[CIIIII[FI)F", - (void*) PaintGlue::getTextAdvances___CIIIII_FI}, - {"nGetTextAdvances","(JLjava/lang/String;IIIII[FI)F", - (void*) PaintGlue::getTextAdvances__StringIIIII_FI}, - - {"nGetTextRunCursor", "(J[CIIIII)I", (void*) PaintGlue::getTextRunCursor___C}, - {"nGetTextRunCursor", "(JLjava/lang/String;IIIII)I", - (void*) PaintGlue::getTextRunCursor__String}, - {"nGetTextPath", "(JI[CIIFFJ)V", (void*) PaintGlue::getTextPath___C}, - {"nGetTextPath", "(JILjava/lang/String;IIFFJ)V", (void*) PaintGlue::getTextPath__String}, - {"nGetStringBounds", "(JLjava/lang/String;IIILandroid/graphics/Rect;)V", - (void*) PaintGlue::getStringBounds }, - {"nGetCharArrayBounds", "(J[CIIILandroid/graphics/Rect;)V", - (void*) PaintGlue::getCharArrayBounds }, - {"nHasGlyph", "(JILjava/lang/String;)Z", (void*) PaintGlue::hasGlyph }, - {"nGetRunAdvance", "(J[CIIIIZI)F", (void*) PaintGlue::getRunAdvance___CIIIIZI_F}, - {"nGetOffsetForAdvance", "(J[CIIIIZF)I", - (void*) PaintGlue::getOffsetForAdvance___CIIIIZF_I}, - {"nGetFontMetricsIntForText", "(J[CIIIIZLandroid/graphics/Paint$FontMetricsInt;)V", - (void*)PaintGlue::getFontMetricsIntForText___C}, - {"nGetFontMetricsIntForText", - "(JLjava/lang/String;IIIIZLandroid/graphics/Paint$FontMetricsInt;)V", - (void*)PaintGlue::getFontMetricsIntForText___String}, - - // --------------- @FastNative ---------------------- - - {"nSetTextLocales","(JLjava/lang/String;)I", (void*) PaintGlue::setTextLocales}, - {"nSetFontFeatureSettings","(JLjava/lang/String;)V", - (void*) PaintGlue::setFontFeatureSettings}, - {"nGetFontMetrics", "(JLandroid/graphics/Paint$FontMetrics;)F", - (void*)PaintGlue::getFontMetrics}, - {"nGetFontMetricsInt", "(JLandroid/graphics/Paint$FontMetricsInt;)I", - (void*)PaintGlue::getFontMetricsInt}, - - // --------------- @CriticalNative ------------------ - - {"nReset","(J)V", (void*) PaintGlue::reset}, - {"nSet","(JJ)V", (void*) PaintGlue::assign}, - {"nGetFlags","(J)I", (void*) PaintGlue::getFlags}, - {"nSetFlags","(JI)V", (void*) PaintGlue::setFlags}, - {"nGetHinting","(J)I", (void*) PaintGlue::getHinting}, - {"nSetHinting","(JI)V", (void*) PaintGlue::setHinting}, - {"nSetAntiAlias","(JZ)V", (void*) PaintGlue::setAntiAlias}, - {"nSetSubpixelText","(JZ)V", (void*) PaintGlue::setSubpixelText}, - {"nSetLinearText","(JZ)V", (void*) PaintGlue::setLinearText}, - {"nSetUnderlineText","(JZ)V", (void*) PaintGlue::setUnderlineText}, - {"nSetStrikeThruText","(JZ)V", (void*) PaintGlue::setStrikeThruText}, - {"nSetFakeBoldText","(JZ)V", (void*) PaintGlue::setFakeBoldText}, - {"nSetFilterBitmap","(JZ)V", (void*) PaintGlue::setFilterBitmap}, - {"nSetDither","(JZ)V", (void*) PaintGlue::setDither}, - {"nGetStyle","(J)I", (void*) PaintGlue::getStyle}, - {"nSetStyle","(JI)V", (void*) PaintGlue::setStyle}, - {"nSetColor","(JI)V", (void*) PaintGlue::setColor}, - {"nSetColor","(JJJ)V", (void*) PaintGlue::setColorLong}, - {"nSetAlpha","(JI)V", (void*) PaintGlue::setAlpha}, - {"nGetStrokeWidth","(J)F", (void*) PaintGlue::getStrokeWidth}, - {"nSetStrokeWidth","(JF)V", (void*) PaintGlue::setStrokeWidth}, - {"nGetStrokeMiter","(J)F", (void*) PaintGlue::getStrokeMiter}, - {"nSetStrokeMiter","(JF)V", (void*) PaintGlue::setStrokeMiter}, - {"nGetStrokeCap","(J)I", (void*) PaintGlue::getStrokeCap}, - {"nSetStrokeCap","(JI)V", (void*) PaintGlue::setStrokeCap}, - {"nGetStrokeJoin","(J)I", (void*) PaintGlue::getStrokeJoin}, - {"nSetStrokeJoin","(JI)V", (void*) PaintGlue::setStrokeJoin}, - {"nGetFillPath","(JJJ)Z", (void*) PaintGlue::getFillPath}, - {"nSetShader","(JJ)J", (void*) PaintGlue::setShader}, - {"nSetColorFilter","(JJ)J", (void*) PaintGlue::setColorFilter}, - {"nSetXfermode","(JI)V", (void*) PaintGlue::setXfermode}, - {"nSetPathEffect","(JJ)J", (void*) PaintGlue::setPathEffect}, - {"nSetMaskFilter","(JJ)J", (void*) PaintGlue::setMaskFilter}, - {"nSetTypeface","(JJ)V", (void*) PaintGlue::setTypeface}, - {"nGetTextAlign","(J)I", (void*) PaintGlue::getTextAlign}, - {"nSetTextAlign","(JI)V", (void*) PaintGlue::setTextAlign}, - {"nSetTextLocalesByMinikinLocaleListId","(JI)V", - (void*) PaintGlue::setTextLocalesByMinikinLocaleListId}, - {"nIsElegantTextHeight","(J)Z", (void*) PaintGlue::isElegantTextHeight}, - {"nSetElegantTextHeight","(JZ)V", (void*) PaintGlue::setElegantTextHeight}, - {"nGetTextSize","(J)F", (void*) PaintGlue::getTextSize}, - {"nSetTextSize","(JF)V", (void*) PaintGlue::setTextSize}, - {"nGetTextScaleX","(J)F", (void*) PaintGlue::getTextScaleX}, - {"nSetTextScaleX","(JF)V", (void*) PaintGlue::setTextScaleX}, - {"nGetTextSkewX","(J)F", (void*) PaintGlue::getTextSkewX}, - {"nSetTextSkewX","(JF)V", (void*) PaintGlue::setTextSkewX}, - {"nGetLetterSpacing","(J)F", (void*) PaintGlue::getLetterSpacing}, - {"nSetLetterSpacing","(JF)V", (void*) PaintGlue::setLetterSpacing}, - {"nGetWordSpacing","(J)F", (void*) PaintGlue::getWordSpacing}, - {"nSetWordSpacing","(JF)V", (void*) PaintGlue::setWordSpacing}, - {"nGetStartHyphenEdit", "(J)I", (void*) PaintGlue::getStartHyphenEdit}, - {"nGetEndHyphenEdit", "(J)I", (void*) PaintGlue::getEndHyphenEdit}, - {"nSetStartHyphenEdit", "(JI)V", (void*) PaintGlue::setStartHyphenEdit}, - {"nSetEndHyphenEdit", "(JI)V", (void*) PaintGlue::setEndHyphenEdit}, - {"nAscent","(J)F", (void*) PaintGlue::ascent}, - {"nDescent","(J)F", (void*) PaintGlue::descent}, - {"nGetUnderlinePosition","(J)F", (void*) PaintGlue::getUnderlinePosition}, - {"nGetUnderlineThickness","(J)F", (void*) PaintGlue::getUnderlineThickness}, - {"nGetStrikeThruPosition","(J)F", (void*) PaintGlue::getStrikeThruPosition}, - {"nGetStrikeThruThickness","(J)F", (void*) PaintGlue::getStrikeThruThickness}, - {"nSetShadowLayer", "(JFFFJJ)V", (void*)PaintGlue::setShadowLayer}, - {"nHasShadowLayer", "(J)Z", (void*)PaintGlue::hasShadowLayer}, - {"nEqualsForTextMeasurement", "(JJ)Z", (void*)PaintGlue::equalsForTextMeasurement}, + {"nGetNativeFinalizer", "()J", (void*)PaintGlue::getNativeFinalizer}, + {"nInit", "()J", (void*)PaintGlue::init}, + {"nInitWithPaint", "(J)J", (void*)PaintGlue::initWithPaint}, + {"nBreakText", "(J[CIIFI[F)I", (void*)PaintGlue::breakTextC}, + {"nBreakText", "(JLjava/lang/String;ZFI[F)I", (void*)PaintGlue::breakTextS}, + {"nGetTextAdvances", "(J[CIIIII[FI)F", (void*)PaintGlue::getTextAdvances___CIIIII_FI}, + {"nGetTextAdvances", "(JLjava/lang/String;IIIII[FI)F", + (void*)PaintGlue::getTextAdvances__StringIIIII_FI}, + + {"nGetTextRunCursor", "(J[CIIIII)I", (void*)PaintGlue::getTextRunCursor___C}, + {"nGetTextRunCursor", "(JLjava/lang/String;IIIII)I", + (void*)PaintGlue::getTextRunCursor__String}, + {"nGetTextPath", "(JI[CIIFFJ)V", (void*)PaintGlue::getTextPath___C}, + {"nGetTextPath", "(JILjava/lang/String;IIFFJ)V", (void*)PaintGlue::getTextPath__String}, + {"nGetStringBounds", "(JLjava/lang/String;IIILandroid/graphics/Rect;)V", + (void*)PaintGlue::getStringBounds}, + {"nGetCharArrayBounds", "(J[CIIILandroid/graphics/Rect;)V", + (void*)PaintGlue::getCharArrayBounds}, + {"nHasGlyph", "(JILjava/lang/String;)Z", (void*)PaintGlue::hasGlyph}, + {"nGetRunAdvance", "(J[CIIIIZI)F", (void*)PaintGlue::getRunAdvance___CIIIIZI_F}, + {"nGetRunCharacterAdvance", "(J[CIIIIZI[FI)F", + (void*)PaintGlue::getRunCharacterAdvance___CIIIIZI_FI_F}, + {"nGetOffsetForAdvance", "(J[CIIIIZF)I", (void*)PaintGlue::getOffsetForAdvance___CIIIIZF_I}, + {"nGetFontMetricsIntForText", "(J[CIIIIZLandroid/graphics/Paint$FontMetricsInt;)V", + (void*)PaintGlue::getFontMetricsIntForText___C}, + {"nGetFontMetricsIntForText", + "(JLjava/lang/String;IIIIZLandroid/graphics/Paint$FontMetricsInt;)V", + (void*)PaintGlue::getFontMetricsIntForText___String}, + + // --------------- @FastNative ---------------------- + + {"nSetTextLocales", "(JLjava/lang/String;)I", (void*)PaintGlue::setTextLocales}, + {"nSetFontFeatureSettings", "(JLjava/lang/String;)V", + (void*)PaintGlue::setFontFeatureSettings}, + {"nGetFontMetrics", "(JLandroid/graphics/Paint$FontMetrics;)F", + (void*)PaintGlue::getFontMetrics}, + {"nGetFontMetricsInt", "(JLandroid/graphics/Paint$FontMetricsInt;)I", + (void*)PaintGlue::getFontMetricsInt}, + + // --------------- @CriticalNative ------------------ + + {"nReset", "(J)V", (void*)PaintGlue::reset}, + {"nSet", "(JJ)V", (void*)PaintGlue::assign}, + {"nGetFlags", "(J)I", (void*)PaintGlue::getFlags}, + {"nSetFlags", "(JI)V", (void*)PaintGlue::setFlags}, + {"nGetHinting", "(J)I", (void*)PaintGlue::getHinting}, + {"nSetHinting", "(JI)V", (void*)PaintGlue::setHinting}, + {"nSetAntiAlias", "(JZ)V", (void*)PaintGlue::setAntiAlias}, + {"nSetSubpixelText", "(JZ)V", (void*)PaintGlue::setSubpixelText}, + {"nSetLinearText", "(JZ)V", (void*)PaintGlue::setLinearText}, + {"nSetUnderlineText", "(JZ)V", (void*)PaintGlue::setUnderlineText}, + {"nSetStrikeThruText", "(JZ)V", (void*)PaintGlue::setStrikeThruText}, + {"nSetFakeBoldText", "(JZ)V", (void*)PaintGlue::setFakeBoldText}, + {"nSetFilterBitmap", "(JZ)V", (void*)PaintGlue::setFilterBitmap}, + {"nSetDither", "(JZ)V", (void*)PaintGlue::setDither}, + {"nGetStyle", "(J)I", (void*)PaintGlue::getStyle}, + {"nSetStyle", "(JI)V", (void*)PaintGlue::setStyle}, + {"nSetColor", "(JI)V", (void*)PaintGlue::setColor}, + {"nSetColor", "(JJJ)V", (void*)PaintGlue::setColorLong}, + {"nSetAlpha", "(JI)V", (void*)PaintGlue::setAlpha}, + {"nGetStrokeWidth", "(J)F", (void*)PaintGlue::getStrokeWidth}, + {"nSetStrokeWidth", "(JF)V", (void*)PaintGlue::setStrokeWidth}, + {"nGetStrokeMiter", "(J)F", (void*)PaintGlue::getStrokeMiter}, + {"nSetStrokeMiter", "(JF)V", (void*)PaintGlue::setStrokeMiter}, + {"nGetStrokeCap", "(J)I", (void*)PaintGlue::getStrokeCap}, + {"nSetStrokeCap", "(JI)V", (void*)PaintGlue::setStrokeCap}, + {"nGetStrokeJoin", "(J)I", (void*)PaintGlue::getStrokeJoin}, + {"nSetStrokeJoin", "(JI)V", (void*)PaintGlue::setStrokeJoin}, + {"nGetFillPath", "(JJJ)Z", (void*)PaintGlue::getFillPath}, + {"nSetShader", "(JJ)J", (void*)PaintGlue::setShader}, + {"nSetColorFilter", "(JJ)J", (void*)PaintGlue::setColorFilter}, + {"nSetXfermode", "(JI)V", (void*)PaintGlue::setXfermode}, + {"nSetPathEffect", "(JJ)J", (void*)PaintGlue::setPathEffect}, + {"nSetMaskFilter", "(JJ)J", (void*)PaintGlue::setMaskFilter}, + {"nSetTypeface", "(JJ)V", (void*)PaintGlue::setTypeface}, + {"nGetTextAlign", "(J)I", (void*)PaintGlue::getTextAlign}, + {"nSetTextAlign", "(JI)V", (void*)PaintGlue::setTextAlign}, + {"nSetTextLocalesByMinikinLocaleListId", "(JI)V", + (void*)PaintGlue::setTextLocalesByMinikinLocaleListId}, + {"nIsElegantTextHeight", "(J)Z", (void*)PaintGlue::isElegantTextHeight}, + {"nSetElegantTextHeight", "(JZ)V", (void*)PaintGlue::setElegantTextHeight}, + {"nGetTextSize", "(J)F", (void*)PaintGlue::getTextSize}, + {"nSetTextSize", "(JF)V", (void*)PaintGlue::setTextSize}, + {"nGetTextScaleX", "(J)F", (void*)PaintGlue::getTextScaleX}, + {"nSetTextScaleX", "(JF)V", (void*)PaintGlue::setTextScaleX}, + {"nGetTextSkewX", "(J)F", (void*)PaintGlue::getTextSkewX}, + {"nSetTextSkewX", "(JF)V", (void*)PaintGlue::setTextSkewX}, + {"nGetLetterSpacing", "(J)F", (void*)PaintGlue::getLetterSpacing}, + {"nSetLetterSpacing", "(JF)V", (void*)PaintGlue::setLetterSpacing}, + {"nGetWordSpacing", "(J)F", (void*)PaintGlue::getWordSpacing}, + {"nSetWordSpacing", "(JF)V", (void*)PaintGlue::setWordSpacing}, + {"nGetStartHyphenEdit", "(J)I", (void*)PaintGlue::getStartHyphenEdit}, + {"nGetEndHyphenEdit", "(J)I", (void*)PaintGlue::getEndHyphenEdit}, + {"nSetStartHyphenEdit", "(JI)V", (void*)PaintGlue::setStartHyphenEdit}, + {"nSetEndHyphenEdit", "(JI)V", (void*)PaintGlue::setEndHyphenEdit}, + {"nAscent", "(J)F", (void*)PaintGlue::ascent}, + {"nDescent", "(J)F", (void*)PaintGlue::descent}, + {"nGetUnderlinePosition", "(J)F", (void*)PaintGlue::getUnderlinePosition}, + {"nGetUnderlineThickness", "(J)F", (void*)PaintGlue::getUnderlineThickness}, + {"nGetStrikeThruPosition", "(J)F", (void*)PaintGlue::getStrikeThruPosition}, + {"nGetStrikeThruThickness", "(J)F", (void*)PaintGlue::getStrikeThruThickness}, + {"nSetShadowLayer", "(JFFFJJ)V", (void*)PaintGlue::setShadowLayer}, + {"nHasShadowLayer", "(J)Z", (void*)PaintGlue::hasShadowLayer}, + {"nEqualsForTextMeasurement", "(JJ)Z", (void*)PaintGlue::equalsForTextMeasurement}, }; - int register_android_graphics_Paint(JNIEnv* env) { return RegisterMethodsOrDie(env, "android/graphics/Paint", methods, NELEM(methods)); } diff --git a/libs/hwui/jni/Path.cpp b/libs/hwui/jni/Path.cpp index d67bcf221681..3694ce07b972 100644 --- a/libs/hwui/jni/Path.cpp +++ b/libs/hwui/jni/Path.cpp @@ -102,6 +102,18 @@ public: obj->rQuadTo(dx1, dy1, dx2, dy2); } + static void conicTo(JNIEnv* env, jclass clazz, jlong objHandle, jfloat x1, jfloat y1, jfloat x2, + jfloat y2, jfloat weight) { + SkPath* obj = reinterpret_cast<SkPath*>(objHandle); + obj->conicTo(x1, y1, x2, y2, weight); + } + + static void rConicTo(JNIEnv* env, jclass clazz, jlong objHandle, jfloat dx1, jfloat dy1, + jfloat dx2, jfloat dy2, jfloat weight) { + SkPath* obj = reinterpret_cast<SkPath*>(objHandle); + obj->rConicTo(dx1, dy1, dx2, dy2, weight); + } + static void cubicTo__FFFFFF(JNIEnv* env, jclass clazz, jlong objHandle, jfloat x1, jfloat y1, jfloat x2, jfloat y2, jfloat x3, jfloat y3) { SkPath* obj = reinterpret_cast<SkPath*>(objHandle); @@ -209,6 +221,14 @@ public: obj->setLastPt(dx, dy); } + static jboolean interpolate(JNIEnv* env, jclass clazz, jlong startHandle, jlong endHandle, + jfloat t, jlong interpolatedHandle) { + SkPath* startPath = reinterpret_cast<SkPath*>(startHandle); + SkPath* endPath = reinterpret_cast<SkPath*>(endHandle); + SkPath* interpolatedPath = reinterpret_cast<SkPath*>(interpolatedHandle); + return startPath->interpolate(*endPath, t, interpolatedPath); + } + static void transform__MatrixPath(JNIEnv* env, jclass clazz, jlong objHandle, jlong matrixHandle, jlong dstHandle) { SkPath* obj = reinterpret_cast<SkPath*>(objHandle); @@ -473,6 +493,16 @@ public: // ---------------- @CriticalNative ------------------------- + static jint getGenerationID(CRITICAL_JNI_PARAMS_COMMA jlong pathHandle) { + return (reinterpret_cast<SkPath*>(pathHandle)->getGenerationID()); + } + + static jboolean isInterpolatable(CRITICAL_JNI_PARAMS_COMMA jlong startHandle, jlong endHandle) { + SkPath* startPath = reinterpret_cast<SkPath*>(startHandle); + SkPath* endPath = reinterpret_cast<SkPath*>(endHandle); + return startPath->isInterpolatable(*endPath); + } + static void reset(CRITICAL_JNI_PARAMS_COMMA jlong objHandle) { SkPath* obj = reinterpret_cast<SkPath*>(objHandle); obj->reset(); @@ -506,48 +536,53 @@ public: }; static const JNINativeMethod methods[] = { - {"nInit","()J", (void*) SkPathGlue::init}, - {"nInit","(J)J", (void*) SkPathGlue::init_Path}, - {"nGetFinalizer", "()J", (void*) SkPathGlue::getFinalizer}, - {"nSet","(JJ)V", (void*) SkPathGlue::set}, - {"nComputeBounds","(JLandroid/graphics/RectF;)V", (void*) SkPathGlue::computeBounds}, - {"nIncReserve","(JI)V", (void*) SkPathGlue::incReserve}, - {"nMoveTo","(JFF)V", (void*) SkPathGlue::moveTo__FF}, - {"nRMoveTo","(JFF)V", (void*) SkPathGlue::rMoveTo}, - {"nLineTo","(JFF)V", (void*) SkPathGlue::lineTo__FF}, - {"nRLineTo","(JFF)V", (void*) SkPathGlue::rLineTo}, - {"nQuadTo","(JFFFF)V", (void*) SkPathGlue::quadTo__FFFF}, - {"nRQuadTo","(JFFFF)V", (void*) SkPathGlue::rQuadTo}, - {"nCubicTo","(JFFFFFF)V", (void*) SkPathGlue::cubicTo__FFFFFF}, - {"nRCubicTo","(JFFFFFF)V", (void*) SkPathGlue::rCubicTo}, - {"nArcTo","(JFFFFFFZ)V", (void*) SkPathGlue::arcTo}, - {"nClose","(J)V", (void*) SkPathGlue::close}, - {"nAddRect","(JFFFFI)V", (void*) SkPathGlue::addRect}, - {"nAddOval","(JFFFFI)V", (void*) SkPathGlue::addOval}, - {"nAddCircle","(JFFFI)V", (void*) SkPathGlue::addCircle}, - {"nAddArc","(JFFFFFF)V", (void*) SkPathGlue::addArc}, - {"nAddRoundRect","(JFFFFFFI)V", (void*) SkPathGlue::addRoundRectXY}, - {"nAddRoundRect","(JFFFF[FI)V", (void*) SkPathGlue::addRoundRect8}, - {"nAddPath","(JJFF)V", (void*) SkPathGlue::addPath__PathFF}, - {"nAddPath","(JJ)V", (void*) SkPathGlue::addPath__Path}, - {"nAddPath","(JJJ)V", (void*) SkPathGlue::addPath__PathMatrix}, - {"nOffset","(JFF)V", (void*) SkPathGlue::offset__FF}, - {"nSetLastPoint","(JFF)V", (void*) SkPathGlue::setLastPoint}, - {"nTransform","(JJJ)V", (void*) SkPathGlue::transform__MatrixPath}, - {"nTransform","(JJ)V", (void*) SkPathGlue::transform__Matrix}, - {"nOp","(JJIJ)Z", (void*) SkPathGlue::op}, - {"nApproximate", "(JF)[F", (void*) SkPathGlue::approximate}, - - // ------- @FastNative below here ---------------------- - {"nIsRect","(JLandroid/graphics/RectF;)Z", (void*) SkPathGlue::isRect}, - - // ------- @CriticalNative below here ------------------ - {"nReset","(J)V", (void*) SkPathGlue::reset}, - {"nRewind","(J)V", (void*) SkPathGlue::rewind}, - {"nIsEmpty","(J)Z", (void*) SkPathGlue::isEmpty}, - {"nIsConvex","(J)Z", (void*) SkPathGlue::isConvex}, - {"nGetFillType","(J)I", (void*) SkPathGlue::getFillType}, - {"nSetFillType","(JI)V", (void*) SkPathGlue::setFillType}, + {"nInit", "()J", (void*)SkPathGlue::init}, + {"nInit", "(J)J", (void*)SkPathGlue::init_Path}, + {"nGetFinalizer", "()J", (void*)SkPathGlue::getFinalizer}, + {"nSet", "(JJ)V", (void*)SkPathGlue::set}, + {"nComputeBounds", "(JLandroid/graphics/RectF;)V", (void*)SkPathGlue::computeBounds}, + {"nIncReserve", "(JI)V", (void*)SkPathGlue::incReserve}, + {"nMoveTo", "(JFF)V", (void*)SkPathGlue::moveTo__FF}, + {"nRMoveTo", "(JFF)V", (void*)SkPathGlue::rMoveTo}, + {"nLineTo", "(JFF)V", (void*)SkPathGlue::lineTo__FF}, + {"nRLineTo", "(JFF)V", (void*)SkPathGlue::rLineTo}, + {"nQuadTo", "(JFFFF)V", (void*)SkPathGlue::quadTo__FFFF}, + {"nRQuadTo", "(JFFFF)V", (void*)SkPathGlue::rQuadTo}, + {"nConicTo", "(JFFFFF)V", (void*)SkPathGlue::conicTo}, + {"nRConicTo", "(JFFFFF)V", (void*)SkPathGlue::rConicTo}, + {"nCubicTo", "(JFFFFFF)V", (void*)SkPathGlue::cubicTo__FFFFFF}, + {"nRCubicTo", "(JFFFFFF)V", (void*)SkPathGlue::rCubicTo}, + {"nArcTo", "(JFFFFFFZ)V", (void*)SkPathGlue::arcTo}, + {"nClose", "(J)V", (void*)SkPathGlue::close}, + {"nAddRect", "(JFFFFI)V", (void*)SkPathGlue::addRect}, + {"nAddOval", "(JFFFFI)V", (void*)SkPathGlue::addOval}, + {"nAddCircle", "(JFFFI)V", (void*)SkPathGlue::addCircle}, + {"nAddArc", "(JFFFFFF)V", (void*)SkPathGlue::addArc}, + {"nAddRoundRect", "(JFFFFFFI)V", (void*)SkPathGlue::addRoundRectXY}, + {"nAddRoundRect", "(JFFFF[FI)V", (void*)SkPathGlue::addRoundRect8}, + {"nAddPath", "(JJFF)V", (void*)SkPathGlue::addPath__PathFF}, + {"nAddPath", "(JJ)V", (void*)SkPathGlue::addPath__Path}, + {"nAddPath", "(JJJ)V", (void*)SkPathGlue::addPath__PathMatrix}, + {"nInterpolate", "(JJFJ)Z", (void*)SkPathGlue::interpolate}, + {"nOffset", "(JFF)V", (void*)SkPathGlue::offset__FF}, + {"nSetLastPoint", "(JFF)V", (void*)SkPathGlue::setLastPoint}, + {"nTransform", "(JJJ)V", (void*)SkPathGlue::transform__MatrixPath}, + {"nTransform", "(JJ)V", (void*)SkPathGlue::transform__Matrix}, + {"nOp", "(JJIJ)Z", (void*)SkPathGlue::op}, + {"nApproximate", "(JF)[F", (void*)SkPathGlue::approximate}, + + // ------- @FastNative below here ---------------------- + {"nIsRect", "(JLandroid/graphics/RectF;)Z", (void*)SkPathGlue::isRect}, + + // ------- @CriticalNative below here ------------------ + {"nGetGenerationID", "(J)I", (void*)SkPathGlue::getGenerationID}, + {"nIsInterpolatable", "(JJ)Z", (void*)SkPathGlue::isInterpolatable}, + {"nReset", "(J)V", (void*)SkPathGlue::reset}, + {"nRewind", "(J)V", (void*)SkPathGlue::rewind}, + {"nIsEmpty", "(J)Z", (void*)SkPathGlue::isEmpty}, + {"nIsConvex", "(J)Z", (void*)SkPathGlue::isConvex}, + {"nGetFillType", "(J)I", (void*)SkPathGlue::getFillType}, + {"nSetFillType", "(JI)V", (void*)SkPathGlue::setFillType}, }; int register_android_graphics_Path(JNIEnv* env) { diff --git a/libs/hwui/jni/PathIterator.cpp b/libs/hwui/jni/PathIterator.cpp new file mode 100644 index 000000000000..3884342d8d37 --- /dev/null +++ b/libs/hwui/jni/PathIterator.cpp @@ -0,0 +1,81 @@ +/* libs/android_runtime/android/graphics/PathMeasure.cpp +** +** Copyright 2007, The Android Open Source Project +** +** Licensed under the Apache License, Version 2.0 (the "License"); +** you may not use this file except in compliance with the License. +** You may obtain a copy of the License at +** +** http://www.apache.org/licenses/LICENSE-2.0 +** +** Unless required by applicable law or agreed to in writing, software +** distributed under the License is distributed on an "AS IS" BASIS, +** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +** See the License for the specific language governing permissions and +** limitations under the License. +*/ + +#include <log/log.h> + +#include "GraphicsJNI.h" +#include "SkPath.h" +#include "SkPoint.h" + +namespace android { + +class SkPathIteratorGlue { +public: + static void finalizer(SkPath::RawIter* obj) { delete obj; } + + static jlong getFinalizer(JNIEnv* env, jclass clazz) { + return static_cast<jlong>(reinterpret_cast<uintptr_t>(&finalizer)); + } + + static jlong create(JNIEnv* env, jobject clazz, jlong pathHandle) { + const SkPath* path = reinterpret_cast<SkPath*>(pathHandle); + return reinterpret_cast<jlong>(new SkPath::RawIter(*path)); + } + + // ---------------- @CriticalNative ------------------------- + + static jint peek(CRITICAL_JNI_PARAMS_COMMA jlong iteratorHandle) { + SkPath::RawIter* iterator = reinterpret_cast<SkPath::RawIter*>(iteratorHandle); + return iterator->peek(); + } + + static jint next(CRITICAL_JNI_PARAMS_COMMA jlong iteratorHandle, jlong pointsArray) { + static_assert(SkPath::kMove_Verb == 0, "SkPath::Verb unexpected index"); + static_assert(SkPath::kLine_Verb == 1, "SkPath::Verb unexpected index"); + static_assert(SkPath::kQuad_Verb == 2, "SkPath::Verb unexpected index"); + static_assert(SkPath::kConic_Verb == 3, "SkPath::Verb unexpected index"); + static_assert(SkPath::kCubic_Verb == 4, "SkPath::Verb unexpected index"); + static_assert(SkPath::kClose_Verb == 5, "SkPath::Verb unexpected index"); + static_assert(SkPath::kDone_Verb == 6, "SkPath::Verb unexpected index"); + + SkPath::RawIter* iterator = reinterpret_cast<SkPath::RawIter*>(iteratorHandle); + float* points = reinterpret_cast<float*>(pointsArray); + SkPath::Verb verb = + static_cast<SkPath::Verb>(iterator->next(reinterpret_cast<SkPoint*>(points))); + if (verb == SkPath::kConic_Verb) { + float weight = iterator->conicWeight(); + points[6] = weight; + } + return static_cast<int>(verb); + } +}; + +static const JNINativeMethod methods[] = { + {"nCreate", "(J)J", (void*)SkPathIteratorGlue::create}, + {"nGetFinalizer", "()J", (void*)SkPathIteratorGlue::getFinalizer}, + + // ------- @CriticalNative below here ------------------ + + {"nPeek", "(J)I", (void*)SkPathIteratorGlue::peek}, + {"nNext", "(JJ)I", (void*)SkPathIteratorGlue::next}, +}; + +int register_android_graphics_PathIterator(JNIEnv* env) { + return RegisterMethodsOrDie(env, "android/graphics/PathIterator", methods, NELEM(methods)); +} + +} // namespace android diff --git a/libs/hwui/jni/Shader.cpp b/libs/hwui/jni/Shader.cpp index 0bbd8a8cf97c..fa8e2e79c831 100644 --- a/libs/hwui/jni/Shader.cpp +++ b/libs/hwui/jni/Shader.cpp @@ -2,11 +2,21 @@ #define LOG_TAG "ShaderJNI" #include "GraphicsJNI.h" +#include "SkBitmap.h" +#include "SkBlendMode.h" +#include "SkColor.h" #include "SkColorFilter.h" #include "SkGradientShader.h" +#include "SkImage.h" #include "SkImagePriv.h" +#include "SkMatrix.h" +#include "SkPoint.h" +#include "SkRefCnt.h" +#include "SkSamplingOptions.h" +#include "SkScalar.h" #include "SkShader.h" -#include "SkBlendMode.h" +#include "SkString.h" +#include "SkTileMode.h" #include "include/effects/SkRuntimeEffect.h" #include <vector> @@ -16,7 +26,7 @@ using namespace android::uirenderer; /** * By default Skia gradients will interpolate their colors in unpremul space * and then premultiply each of the results. We must set this flag to preserve - * backwards compatiblity by premultiplying the colors of the gradient first, + * backwards compatibility by premultiplying the colors of the gradient first, * and then interpolating between them. */ static const uint32_t sGradientShaderFlags = SkGradientShader::kInterpolateColorsInPremul_Flag; diff --git a/libs/hwui/jni/Typeface.cpp b/libs/hwui/jni/Typeface.cpp index d86d9ee56f4c..209b35c5537c 100644 --- a/libs/hwui/jni/Typeface.cpp +++ b/libs/hwui/jni/Typeface.cpp @@ -20,18 +20,21 @@ #include <minikin/FontCollection.h> #include <minikin/FontFamily.h> #include <minikin/FontFileParser.h> +#include <minikin/LocaleList.h> +#include <minikin/MinikinFontFactory.h> #include <minikin/SystemFonts.h> #include <nativehelper/ScopedPrimitiveArray.h> #include <nativehelper/ScopedUtfChars.h> + +#include <mutex> +#include <unordered_map> + #include "FontUtils.h" #include "GraphicsJNI.h" #include "SkData.h" #include "SkTypeface.h" #include "fonts/Font.h" -#include <mutex> -#include <unordered_map> - #ifdef __ANDROID__ #include <sys/stat.h> #endif @@ -106,27 +109,14 @@ static jint Typeface_getWeight(CRITICAL_JNI_PARAMS_COMMA jlong faceHandle) { static jlong Typeface_createFromArray(JNIEnv *env, jobject, jlongArray familyArray, jlong fallbackPtr, int weight, int italic) { ScopedLongArrayRO families(env, familyArray); - std::vector<std::shared_ptr<minikin::FontFamily>> familyVec; Typeface* typeface = (fallbackPtr == 0) ? nullptr : toTypeface(fallbackPtr); - if (typeface != nullptr) { - const std::vector<std::shared_ptr<minikin::FontFamily>>& fallbackFamilies = - toTypeface(fallbackPtr)->fFontCollection->getFamilies(); - familyVec.reserve(families.size() + fallbackFamilies.size()); - for (size_t i = 0; i < families.size(); i++) { - FontFamilyWrapper* family = reinterpret_cast<FontFamilyWrapper*>(families[i]); - familyVec.emplace_back(family->family); - } - for (size_t i = 0; i < fallbackFamilies.size(); i++) { - familyVec.emplace_back(fallbackFamilies[i]); - } - } else { - familyVec.reserve(families.size()); - for (size_t i = 0; i < families.size(); i++) { - FontFamilyWrapper* family = reinterpret_cast<FontFamilyWrapper*>(families[i]); - familyVec.emplace_back(family->family); - } + std::vector<std::shared_ptr<minikin::FontFamily>> familyVec; + familyVec.reserve(families.size()); + for (size_t i = 0; i < families.size(); i++) { + FontFamilyWrapper* family = reinterpret_cast<FontFamilyWrapper*>(families[i]); + familyVec.emplace_back(family->family); } - return toJLong(Typeface::createFromFamilies(std::move(familyVec), weight, italic)); + return toJLong(Typeface::createFromFamilies(std::move(familyVec), weight, italic, typeface)); } // CriticalNative @@ -137,15 +127,13 @@ static void Typeface_setDefault(CRITICAL_JNI_PARAMS_COMMA jlong faceHandle) { static jobject Typeface_getSupportedAxes(JNIEnv *env, jobject, jlong faceHandle) { Typeface* face = toTypeface(faceHandle); - const std::unordered_set<minikin::AxisTag>& tagSet = face->fFontCollection->getSupportedTags(); - const size_t length = tagSet.size(); + const size_t length = face->fFontCollection->getSupportedAxesCount(); if (length == 0) { return nullptr; } std::vector<jint> tagVec(length); - int index = 0; - for (const auto& tag : tagSet) { - tagVec[index++] = tag; + for (size_t i = 0; i < length; i++) { + tagVec[i] = face->fFontCollection->getSupportedAxisAt(i); } std::sort(tagVec.begin(), tagVec.end()); const jintArray result = env->NewIntArray(length); @@ -204,9 +192,18 @@ static sk_sp<SkData> makeSkDataCached(const std::string& path, bool hasVerity) { return entry; } -static std::shared_ptr<minikin::MinikinFont> loadMinikinFontSkia(minikin::BufferReader); +class MinikinFontSkiaFactory : minikin::MinikinFontFactory { +private: + MinikinFontSkiaFactory() : MinikinFontFactory() { MinikinFontFactory::setInstance(this); } + +public: + static void init() { static MinikinFontSkiaFactory factory; } + void skip(minikin::BufferReader* reader) const override; + std::shared_ptr<minikin::MinikinFont> create(minikin::BufferReader reader) const override; + void write(minikin::BufferWriter* writer, const minikin::MinikinFont* typeface) const override; +}; -static minikin::Font::TypefaceLoader* readMinikinFontSkia(minikin::BufferReader* reader) { +void MinikinFontSkiaFactory::skip(minikin::BufferReader* reader) const { // Advance reader's position. reader->skipString(); // fontPath reader->skip<int>(); // fontIndex @@ -216,10 +213,10 @@ static minikin::Font::TypefaceLoader* readMinikinFontSkia(minikin::BufferReader* reader->skip<uint32_t>(); // expectedFontRevision reader->skipString(); // expectedPostScriptName } - return &loadMinikinFontSkia; } -static std::shared_ptr<minikin::MinikinFont> loadMinikinFontSkia(minikin::BufferReader reader) { +std::shared_ptr<minikin::MinikinFont> MinikinFontSkiaFactory::create( + minikin::BufferReader reader) const { std::string_view fontPath = reader.readString(); std::string path(fontPath.data(), fontPath.size()); ATRACE_FORMAT("Loading font %s", path.c_str()); @@ -268,8 +265,8 @@ static std::shared_ptr<minikin::MinikinFont> loadMinikinFontSkia(minikin::Buffer return minikinFont; } -static void writeMinikinFontSkia(minikin::BufferWriter* writer, - const minikin::MinikinFont* typeface) { +void MinikinFontSkiaFactory::write(minikin::BufferWriter* writer, + const minikin::MinikinFont* typeface) const { // When you change the format of font metadata, please update code to parse // typefaceMetadataReader() in // frameworks/base/libs/hwui/jni/fonts/Font.cpp too. @@ -293,7 +290,9 @@ static void writeMinikinFontSkia(minikin::BufferWriter* writer, } } -static jint Typeface_writeTypefaces(JNIEnv *env, jobject, jobject buffer, jlongArray faceHandles) { +static jint Typeface_writeTypefaces(JNIEnv* env, jobject, jobject buffer, jint position, + jlongArray faceHandles) { + MinikinFontSkiaFactory::init(); ScopedLongArrayRO faces(env, faceHandles); std::vector<Typeface*> typefaces; typefaces.reserve(faces.size()); @@ -301,7 +300,12 @@ static jint Typeface_writeTypefaces(JNIEnv *env, jobject, jobject buffer, jlongA typefaces.push_back(toTypeface(faces[i])); } void* addr = buffer == nullptr ? nullptr : env->GetDirectBufferAddress(buffer); - minikin::BufferWriter writer(addr); + if (addr != nullptr && + reinterpret_cast<intptr_t>(addr) % minikin::BufferReader::kMaxAlignment != 0) { + ALOGE("addr (%p) must be aligned at kMaxAlignment, but it was not.", addr); + return 0; + } + minikin::BufferWriter writer(addr, position); std::vector<std::shared_ptr<minikin::FontCollection>> fontCollections; std::unordered_map<std::shared_ptr<minikin::FontCollection>, size_t> fcToIndex; for (Typeface* typeface : typefaces) { @@ -310,7 +314,7 @@ static jint Typeface_writeTypefaces(JNIEnv *env, jobject, jobject buffer, jlongA fontCollections.push_back(typeface->fFontCollection); } } - minikin::FontCollection::writeVector<writeMinikinFontSkia>(&writer, fontCollections); + minikin::FontCollection::writeVector(&writer, fontCollections); writer.write<uint32_t>(typefaces.size()); for (Typeface* typeface : typefaces) { writer.write<uint32_t>(fcToIndex.find(typeface->fFontCollection)->second); @@ -321,12 +325,20 @@ static jint Typeface_writeTypefaces(JNIEnv *env, jobject, jobject buffer, jlongA return static_cast<jint>(writer.size()); } -static jlongArray Typeface_readTypefaces(JNIEnv *env, jobject, jobject buffer) { +static jlongArray Typeface_readTypefaces(JNIEnv* env, jobject, jobject buffer, jint position) { + MinikinFontSkiaFactory::init(); void* addr = buffer == nullptr ? nullptr : env->GetDirectBufferAddress(buffer); - if (addr == nullptr) return nullptr; - minikin::BufferReader reader(addr); + if (addr == nullptr) { + ALOGE("Passed a null buffer."); + return nullptr; + } + if (reinterpret_cast<intptr_t>(addr) % minikin::BufferReader::kMaxAlignment != 0) { + ALOGE("addr (%p) must be aligned at kMaxAlignment, but it was not.", addr); + return nullptr; + } + minikin::BufferReader reader(addr, position); std::vector<std::shared_ptr<minikin::FontCollection>> fontCollections = - minikin::FontCollection::readVector<readMinikinFontSkia>(&reader); + minikin::FontCollection::readVector(&reader); uint32_t typefaceCount = reader.read<uint32_t>(); std::vector<jlong> faceHandles; faceHandles.reserve(typefaceCount); @@ -343,7 +355,6 @@ static jlongArray Typeface_readTypefaces(JNIEnv *env, jobject, jobject buffer) { return result; } - static void Typeface_forceSetStaticFinalField(JNIEnv *env, jclass cls, jstring fieldName, jobject typeface) { ScopedUtfChars fieldNameChars(env, fieldName); @@ -356,18 +367,6 @@ static void Typeface_forceSetStaticFinalField(JNIEnv *env, jclass cls, jstring f env->SetStaticObjectField(cls, fid, typeface); } -// Critical Native -static jint Typeface_getFamilySize(CRITICAL_JNI_PARAMS_COMMA jlong faceHandle) { - return toTypeface(faceHandle)->fFontCollection->getFamilies().size(); -} - -// Critical Native -static jlong Typeface_getFamily(CRITICAL_JNI_PARAMS_COMMA jlong faceHandle, jint index) { - std::shared_ptr<minikin::FontFamily> family = - toTypeface(faceHandle)->fFontCollection->getFamilies()[index]; - return reinterpret_cast<jlong>(new FontFamilyWrapper(std::move(family))); -} - // Regular JNI static void Typeface_warmUpCache(JNIEnv* env, jobject, jstring jFilePath) { ScopedUtfChars filePath(env, jFilePath); @@ -380,6 +379,12 @@ static void Typeface_addFontCollection(CRITICAL_JNI_PARAMS_COMMA jlong faceHandl minikin::SystemFonts::addFontMap(std::move(collection)); } +// Fast Native +static void Typeface_registerLocaleList(JNIEnv* env, jobject, jstring jLocales) { + ScopedUtfChars locales(env, jLocales); + minikin::registerLocaleList(locales.c_str()); +} + /////////////////////////////////////////////////////////////////////////////// static const JNINativeMethod gTypefaceMethods[] = { @@ -397,14 +402,13 @@ static const JNINativeMethod gTypefaceMethods[] = { {"nativeGetSupportedAxes", "(J)[I", (void*)Typeface_getSupportedAxes}, {"nativeRegisterGenericFamily", "(Ljava/lang/String;J)V", (void*)Typeface_registerGenericFamily}, - {"nativeWriteTypefaces", "(Ljava/nio/ByteBuffer;[J)I", (void*)Typeface_writeTypefaces}, - {"nativeReadTypefaces", "(Ljava/nio/ByteBuffer;)[J", (void*)Typeface_readTypefaces}, + {"nativeWriteTypefaces", "(Ljava/nio/ByteBuffer;I[J)I", (void*)Typeface_writeTypefaces}, + {"nativeReadTypefaces", "(Ljava/nio/ByteBuffer;I)[J", (void*)Typeface_readTypefaces}, {"nativeForceSetStaticFinalField", "(Ljava/lang/String;Landroid/graphics/Typeface;)V", (void*)Typeface_forceSetStaticFinalField}, - {"nativeGetFamilySize", "(J)I", (void*)Typeface_getFamilySize}, - {"nativeGetFamily", "(JI)J", (void*)Typeface_getFamily}, {"nativeWarmUpCache", "(Ljava/lang/String;)V", (void*)Typeface_warmUpCache}, {"nativeAddFontCollections", "(J)V", (void*)Typeface_addFontCollection}, + {"nativeRegisterLocaleList", "(Ljava/lang/String;)V", (void*)Typeface_registerLocaleList}, }; int register_android_graphics_Typeface(JNIEnv* env) diff --git a/libs/hwui/jni/Utils.h b/libs/hwui/jni/Utils.h index 6cdf44d85a5a..f6e3a0eeaa0e 100644 --- a/libs/hwui/jni/Utils.h +++ b/libs/hwui/jni/Utils.h @@ -17,8 +17,11 @@ #ifndef _ANDROID_GRAPHICS_UTILS_H_ #define _ANDROID_GRAPHICS_UTILS_H_ +#include "SkRefCnt.h" #include "SkStream.h" +class SkData; + #include <jni.h> #include <androidfw/Asset.h> diff --git a/libs/hwui/jni/YuvToJpegEncoder.cpp b/libs/hwui/jni/YuvToJpegEncoder.cpp index 77f42ae70268..1c5f126d8672 100644 --- a/libs/hwui/jni/YuvToJpegEncoder.cpp +++ b/libs/hwui/jni/YuvToJpegEncoder.cpp @@ -1,11 +1,15 @@ #include "CreateJavaOutputStreamAdaptor.h" #include "SkJPEGWriteUtility.h" +#include "SkStream.h" +#include "SkTypes.h" #include "YuvToJpegEncoder.h" #include <ui/PixelFormat.h> #include <hardware/hardware.h> #include "graphics_jni_helpers.h" +#include <csetjmp> + YuvToJpegEncoder* YuvToJpegEncoder::create(int format, int* strides) { // Only ImageFormat.NV21 and ImageFormat.YUY2 are supported // for now. diff --git a/libs/hwui/jni/YuvToJpegEncoder.h b/libs/hwui/jni/YuvToJpegEncoder.h index 7e7b935df276..a69726b17e9d 100644 --- a/libs/hwui/jni/YuvToJpegEncoder.h +++ b/libs/hwui/jni/YuvToJpegEncoder.h @@ -1,13 +1,13 @@ #ifndef _ANDROID_GRAPHICS_YUV_TO_JPEG_ENCODER_H_ #define _ANDROID_GRAPHICS_YUV_TO_JPEG_ENCODER_H_ -#include "SkTypes.h" -#include "SkStream.h" extern "C" { #include "jpeglib.h" #include "jerror.h" } +class SkWStream; + class YuvToJpegEncoder { public: /** Create an encoder based on the YUV format. diff --git a/libs/hwui/jni/android_graphics_Canvas.cpp b/libs/hwui/jni/android_graphics_Canvas.cpp index 0ef80ee10708..0513447ed05e 100644 --- a/libs/hwui/jni/android_graphics_Canvas.cpp +++ b/libs/hwui/jni/android_graphics_Canvas.cpp @@ -32,10 +32,22 @@ #include "FontUtils.h" #include "Bitmap.h" +#include "SkBitmap.h" +#include "SkBlendMode.h" +#include "SkClipOp.h" +#include "SkColor.h" +#include "SkColorSpace.h" #include "SkGraphics.h" +#include "SkImageInfo.h" +#include "SkMatrix.h" +#include "SkPath.h" +#include "SkPoint.h" +#include "SkRect.h" +#include "SkRefCnt.h" #include "SkRegion.h" -#include "SkVertices.h" #include "SkRRect.h" +#include "SkScalar.h" +#include "SkVertices.h" namespace minikin { class MeasuredText; @@ -407,14 +419,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, @@ -687,9 +713,10 @@ static void setCompatibilityVersion(JNIEnv* env, jobject, jint apiLevel) { } static void punchHole(JNIEnv* env, jobject, jlong canvasPtr, jfloat left, jfloat top, jfloat right, - jfloat bottom, jfloat rx, jfloat ry) { + jfloat bottom, jfloat rx, jfloat ry, jfloat alpha) { auto canvas = reinterpret_cast<Canvas*>(canvasPtr); - canvas->punchHole(SkRRect::MakeRectXY(SkRect::MakeLTRB(left, top, right, bottom), rx, ry)); + canvas->punchHole(SkRRect::MakeRectXY(SkRect::MakeLTRB(left, top, right, bottom), rx, ry), + alpha); } }; // namespace CanvasJNI @@ -764,7 +791,7 @@ static const JNINativeMethod gDrawMethods[] = { {"nDrawTextRun","(JLjava/lang/String;IIIIFFZJ)V", (void*) CanvasJNI::drawTextRunString}, {"nDrawTextOnPath","(J[CIIJFFIJ)V", (void*) CanvasJNI::drawTextOnPathChars}, {"nDrawTextOnPath","(JLjava/lang/String;JFFIJ)V", (void*) CanvasJNI::drawTextOnPathString}, - {"nPunchHole", "(JFFFFFF)V", (void*) CanvasJNI::punchHole} + {"nPunchHole", "(JFFFFFFF)V", (void*) CanvasJNI::punchHole} }; int register_android_graphics_Canvas(JNIEnv* env) { diff --git a/libs/hwui/jni/android_graphics_HardwareRenderer.cpp b/libs/hwui/jni/android_graphics_HardwareRenderer.cpp index c48448dffdd2..4f281fcde61d 100644 --- a/libs/hwui/jni/android_graphics_HardwareRenderer.cpp +++ b/libs/hwui/jni/android_graphics_HardwareRenderer.cpp @@ -23,8 +23,16 @@ #include <Picture.h> #include <Properties.h> #include <RootRenderNode.h> +#include <SkBitmap.h> +#include <SkColorSpace.h> +#include <SkData.h> +#include <SkImage.h> #include <SkImagePriv.h> +#include <SkPicture.h> +#include <SkPixmap.h> #include <SkSerialProcs.h> +#include <SkStream.h> +#include <SkTypeface.h> #include <dlfcn.h> #include <gui/TraceUtils.h> #include <inttypes.h> @@ -80,6 +88,11 @@ struct { jmethodID onFrameComplete; } gFrameCompleteCallback; +struct { + jmethodID onCopyFinished; + jmethodID getDestinationBitmap; +} gCopyRequest; + static JNIEnv* getenv(JavaVM* vm) { JNIEnv* env; if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) { @@ -451,7 +464,7 @@ struct PictureCaptureState { }; // TODO: This & Multi-SKP & Single-SKP should all be de-duped into -// a single "make a SkPicture serailizable-safe" utility somewhere +// a single "make a SkPicture serializable-safe" utility somewhere class PictureWrapper : public Picture { public: PictureWrapper(sk_sp<SkPicture>&& src, const std::shared_ptr<PictureCaptureState>& state) @@ -664,15 +677,41 @@ static void android_view_ThreadedRenderer_setFrameCompleteCallback(JNIEnv* env, } } -static jint android_view_ThreadedRenderer_copySurfaceInto(JNIEnv* env, - jobject clazz, jobject jsurface, jint left, jint top, - jint right, jint bottom, jlong bitmapPtr) { - SkBitmap bitmap; - bitmap::toBitmap(bitmapPtr).getSkBitmap(&bitmap); +class CopyRequestAdapter : public CopyRequest { +public: + CopyRequestAdapter(JavaVM* vm, jobject jCopyRequest, Rect srcRect) + : CopyRequest(srcRect), mRefHolder(vm, jCopyRequest) {} + + virtual SkBitmap getDestinationBitmap(int srcWidth, int srcHeight) override { + JNIEnv* env = getenv(mRefHolder.vm()); + jlong bitmapPtr = env->CallLongMethod( + mRefHolder.object(), gCopyRequest.getDestinationBitmap, srcWidth, srcHeight); + SkBitmap bitmap; + bitmap::toBitmap(bitmapPtr).getSkBitmap(&bitmap); + return bitmap; + } + + virtual void onCopyFinished(CopyResult result) override { + JNIEnv* env = getenv(mRefHolder.vm()); + env->CallVoidMethod(mRefHolder.object(), gCopyRequest.onCopyFinished, + static_cast<jint>(result)); + } + +private: + JGlobalRefHolder mRefHolder; +}; + +static void android_view_ThreadedRenderer_copySurfaceInto(JNIEnv* env, jobject clazz, + jobject jsurface, jint left, jint top, + jint right, jint bottom, + jobject jCopyRequest) { + JavaVM* vm = nullptr; + LOG_ALWAYS_FATAL_IF(env->GetJavaVM(&vm) != JNI_OK, "Unable to get Java VM"); + auto copyRequest = std::make_shared<CopyRequestAdapter>(vm, env->NewGlobalRef(jCopyRequest), + Rect(left, top, right, bottom)); ANativeWindow* window = fromSurface(env, jsurface); - jint result = RenderProxy::copySurfaceInto(window, left, top, right, bottom, &bitmap); + RenderProxy::copySurfaceInto(window, std::move(copyRequest)); ANativeWindow_release(window); - return result; } class ContextFactory : public IContextFactory { @@ -961,7 +1000,8 @@ static const JNINativeMethod gMethods[] = { (void*)android_view_ThreadedRenderer_setFrameCompleteCallback}, {"nAddObserver", "(JJ)V", (void*)android_view_ThreadedRenderer_addObserver}, {"nRemoveObserver", "(JJ)V", (void*)android_view_ThreadedRenderer_removeObserver}, - {"nCopySurfaceInto", "(Landroid/view/Surface;IIIIJ)I", + {"nCopySurfaceInto", + "(Landroid/view/Surface;IIIILandroid/graphics/HardwareRenderer$CopyRequest;)V", (void*)android_view_ThreadedRenderer_copySurfaceInto}, {"nCreateHardwareBitmap", "(JII)Landroid/graphics/Bitmap;", (void*)android_view_ThreadedRenderer_createHardwareBitmapFromRenderNode}, @@ -1034,6 +1074,11 @@ int register_android_view_ThreadedRenderer(JNIEnv* env) { gFrameCompleteCallback.onFrameComplete = GetMethodIDOrDie(env, frameCompleteClass, "onFrameComplete", "()V"); + jclass copyRequest = FindClassOrDie(env, "android/graphics/HardwareRenderer$CopyRequest"); + gCopyRequest.onCopyFinished = GetMethodIDOrDie(env, copyRequest, "onCopyFinished", "(I)V"); + gCopyRequest.getDestinationBitmap = + GetMethodIDOrDie(env, copyRequest, "getDestinationBitmap", "(II)J"); + void* handle_ = dlopen("libandroid.so", RTLD_NOW | RTLD_NODELETE); fromSurface = (ANW_fromSurface)dlsym(handle_, "ANativeWindow_fromSurface"); LOG_ALWAYS_FATAL_IF(fromSurface == nullptr, diff --git a/libs/hwui/jni/fonts/Font.cpp b/libs/hwui/jni/fonts/Font.cpp index 09be630dc741..f17129c8d953 100644 --- a/libs/hwui/jni/fonts/Font.cpp +++ b/libs/hwui/jni/fonts/Font.cpp @@ -22,7 +22,10 @@ #include "SkFont.h" #include "SkFontMetrics.h" #include "SkFontMgr.h" +#include "SkRect.h" #include "SkRefCnt.h" +#include "SkScalar.h" +#include "SkStream.h" #include "SkTypeface.h" #include "GraphicsJNI.h" #include <nativehelper/ScopedUtfChars.h> @@ -225,7 +228,7 @@ static jlong Font_getReleaseNativeFontFunc(CRITICAL_JNI_PARAMS) { static jstring Font_getFontPath(JNIEnv* env, jobject, jlong fontPtr) { FontWrapper* font = reinterpret_cast<FontWrapper*>(fontPtr); minikin::BufferReader reader = font->font->typefaceMetadataReader(); - if (reader.data() != nullptr) { + if (reader.current() != nullptr) { std::string path = std::string(reader.readString()); if (path.empty()) { return nullptr; @@ -267,7 +270,7 @@ static jint Font_getPackedStyle(CRITICAL_JNI_PARAMS_COMMA jlong fontPtr) { static jint Font_getIndex(CRITICAL_JNI_PARAMS_COMMA jlong fontPtr) { FontWrapper* font = reinterpret_cast<FontWrapper*>(fontPtr); minikin::BufferReader reader = font->font->typefaceMetadataReader(); - if (reader.data() != nullptr) { + if (reader.current() != nullptr) { reader.skipString(); // fontPath return reader.read<int>(); } else { @@ -280,7 +283,7 @@ static jint Font_getIndex(CRITICAL_JNI_PARAMS_COMMA jlong fontPtr) { static jint Font_getAxisCount(CRITICAL_JNI_PARAMS_COMMA jlong fontPtr) { FontWrapper* font = reinterpret_cast<FontWrapper*>(fontPtr); minikin::BufferReader reader = font->font->typefaceMetadataReader(); - if (reader.data() != nullptr) { + if (reader.current() != nullptr) { reader.skipString(); // fontPath reader.skip<int>(); // fontIndex return reader.readArray<minikin::FontVariation>().second; @@ -295,7 +298,7 @@ static jlong Font_getAxisInfo(CRITICAL_JNI_PARAMS_COMMA jlong fontPtr, jint inde FontWrapper* font = reinterpret_cast<FontWrapper*>(fontPtr); minikin::BufferReader reader = font->font->typefaceMetadataReader(); minikin::FontVariation var; - if (reader.data() != nullptr) { + if (reader.current() != nullptr) { reader.skipString(); // fontPath reader.skip<int>(); // fontIndex var = reader.readArray<minikin::FontVariation>().first[index]; diff --git a/libs/hwui/jni/fonts/FontFamily.cpp b/libs/hwui/jni/fonts/FontFamily.cpp index b68213549938..fbfc07e1f89d 100644 --- a/libs/hwui/jni/fonts/FontFamily.cpp +++ b/libs/hwui/jni/fonts/FontFamily.cpp @@ -66,9 +66,9 @@ static jlong FontFamily_Builder_build(JNIEnv* env, jobject clazz, jlong builderP ScopedUtfChars str(env, langTags); localeId = minikin::registerLocaleList(str.c_str()); } - std::shared_ptr<minikin::FontFamily> family = std::make_shared<minikin::FontFamily>( - localeId, static_cast<minikin::FamilyVariant>(variant), std::move(builder->fonts), - isCustomFallback); + std::shared_ptr<minikin::FontFamily> family = + minikin::FontFamily::create(localeId, static_cast<minikin::FamilyVariant>(variant), + std::move(builder->fonts), isCustomFallback); if (family->getCoverage().length() == 0) { // No coverage means minikin rejected given font for some reasons. jniThrowException(env, "java/lang/IllegalArgumentException", diff --git a/libs/hwui/pipeline/skia/DumpOpsCanvas.h b/libs/hwui/pipeline/skia/DumpOpsCanvas.h index 3f89c0712407..6a052dbb7cea 100644 --- a/libs/hwui/pipeline/skia/DumpOpsCanvas.h +++ b/libs/hwui/pipeline/skia/DumpOpsCanvas.h @@ -19,6 +19,8 @@ #include "RenderNode.h" #include "SkiaDisplayList.h" +class SkRRect; + namespace android { namespace uirenderer { namespace skiapipeline { diff --git a/libs/hwui/pipeline/skia/HolePunch.h b/libs/hwui/pipeline/skia/HolePunch.h index 92c6f7721a08..d0e1ca35049a 100644 --- a/libs/hwui/pipeline/skia/HolePunch.h +++ b/libs/hwui/pipeline/skia/HolePunch.h @@ -17,7 +17,6 @@ #pragma once #include <string> -#include "SkRRect.h" namespace android { namespace uirenderer { diff --git a/libs/hwui/pipeline/skia/RenderNodeDrawable.cpp b/libs/hwui/pipeline/skia/RenderNodeDrawable.cpp index 507d3dcdcde9..f2282e661535 100644 --- a/libs/hwui/pipeline/skia/RenderNodeDrawable.cpp +++ b/libs/hwui/pipeline/skia/RenderNodeDrawable.cpp @@ -15,7 +15,11 @@ */ #include "RenderNodeDrawable.h" +#include <SkPaint.h> #include <SkPaintFilterCanvas.h> +#include <SkPoint.h> +#include <SkRRect.h> +#include <SkRect.h> #include <gui/TraceUtils.h> #include "RenderNode.h" #include "SkiaDisplayList.h" @@ -197,6 +201,7 @@ protected: paint.setAlpha((uint8_t)paint.getAlpha() * mAlpha); return true; } + void onDrawDrawable(SkDrawable* drawable, const SkMatrix* matrix) override { // We unroll the drawable using "this" canvas, so that draw calls contained inside will // get their alpha applied. The default SkPaintFilterCanvas::onDrawDrawable does not unroll. @@ -288,7 +293,7 @@ void RenderNodeDrawable::drawContent(SkCanvas* canvas) const { // with the same canvas transformation + clip into the target // canvas then draw the layer on top if (renderNode->hasHolePunches()) { - TransformCanvas transformCanvas(canvas, SkBlendMode::kClear); + TransformCanvas transformCanvas(canvas, SkBlendMode::kDstOut); displayList->draw(&transformCanvas); } canvas->drawImageRect(snapshotImage, SkRect::Make(srcBounds), diff --git a/libs/hwui/pipeline/skia/ReorderBarrierDrawables.cpp b/libs/hwui/pipeline/skia/ReorderBarrierDrawables.cpp index 7cfccb56382c..11977bd54c2c 100644 --- a/libs/hwui/pipeline/skia/ReorderBarrierDrawables.cpp +++ b/libs/hwui/pipeline/skia/ReorderBarrierDrawables.cpp @@ -19,8 +19,15 @@ #include "SkiaDisplayList.h" #include "LightingInfo.h" +#include <SkColor.h> +#include <SkMatrix.h> +#include <SkPath.h> #include <SkPathOps.h> +#include <SkPoint3.h> +#include <SkRect.h> +#include <SkScalar.h> #include <SkShadowUtils.h> +#include <include/private/SkShadowFlags.h> namespace android { namespace uirenderer { diff --git a/libs/hwui/pipeline/skia/ShaderCache.cpp b/libs/hwui/pipeline/skia/ShaderCache.cpp index 90c4440c8339..a55de95035a7 100644 --- a/libs/hwui/pipeline/skia/ShaderCache.cpp +++ b/libs/hwui/pipeline/skia/ShaderCache.cpp @@ -16,6 +16,7 @@ #include "ShaderCache.h" #include <GrDirectContext.h> +#include <SkData.h> #include <gui/TraceUtils.h> #include <log/log.h> #include <openssl/sha.h> diff --git a/libs/hwui/pipeline/skia/ShaderCache.h b/libs/hwui/pipeline/skia/ShaderCache.h index 3e0fd5164011..bc35fa5f9987 100644 --- a/libs/hwui/pipeline/skia/ShaderCache.h +++ b/libs/hwui/pipeline/skia/ShaderCache.h @@ -17,12 +17,15 @@ #pragma once #include <GrContextOptions.h> +#include <SkRefCnt.h> #include <cutils/compiler.h> #include <memory> #include <mutex> #include <string> #include <vector> +class SkData; + namespace android { class BlobCache; @@ -45,7 +48,7 @@ public: * and puts the ShaderCache into an initialized state, such that it is * able to insert and retrieve entries from the cache. If identity is * non-null and validation fails, the cache is initialized but contains - * no data. If size is less than zero, the cache is initilaized but + * no data. If size is less than zero, the cache is initialized but * contains no data. * * This should be called when HWUI pipeline is initialized. When not in diff --git a/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp b/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp index 2aca41e41905..19cd7bdd6fcb 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; @@ -112,7 +116,7 @@ IRenderPipeline::DrawResult SkiaOpenGLPipeline::draw( if (CC_UNLIKELY(Properties::showDirtyRegions || ProfileType::None != Properties::getProfileType())) { SkCanvas* profileCanvas = surface->getCanvas(); - SkiaProfileRenderer profileRenderer(profileCanvas); + SkiaProfileRenderer profileRenderer(profileCanvas, frame.width(), frame.height()); profiler->draw(profileRenderer); } @@ -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/SkiaPipeline.cpp b/libs/hwui/pipeline/skia/SkiaPipeline.cpp index bc386feb2d6f..c546adaaf779 100644 --- a/libs/hwui/pipeline/skia/SkiaPipeline.cpp +++ b/libs/hwui/pipeline/skia/SkiaPipeline.cpp @@ -16,15 +16,25 @@ #include "SkiaPipeline.h" +#include <SkCanvas.h> +#include <SkColor.h> +#include <SkColorSpace.h> +#include <SkData.h> +#include <SkImage.h> #include <SkImageEncoder.h> #include <SkImageInfo.h> #include <SkImagePriv.h> +#include <SkMatrix.h> #include <SkMultiPictureDocument.h> #include <SkOverdrawCanvas.h> #include <SkOverdrawColorFilter.h> #include <SkPicture.h> #include <SkPictureRecorder.h> +#include <SkRect.h> +#include <SkRefCnt.h> #include <SkSerialProcs.h> +#include <SkStream.h> +#include <SkString.h> #include <SkTypeface.h> #include <android-base/properties.h> #include <unistd.h> diff --git a/libs/hwui/pipeline/skia/SkiaPipeline.h b/libs/hwui/pipeline/skia/SkiaPipeline.h index bc8a5659dd83..7887d1ae2117 100644 --- a/libs/hwui/pipeline/skia/SkiaPipeline.h +++ b/libs/hwui/pipeline/skia/SkiaPipeline.h @@ -16,14 +16,16 @@ #pragma once -#include <SkSurface.h> +#include <SkColorSpace.h> #include <SkDocument.h> #include <SkMultiPictureDocument.h> +#include <SkSurface.h> #include "Lighting.h" #include "hwui/AnimatedImageDrawable.h" #include "renderthread/CanvasContext.h" #include "renderthread/IRenderPipeline.h" +class SkFILEWStream; class SkPictureRecorder; struct SkSharingSerialContext; diff --git a/libs/hwui/pipeline/skia/SkiaProfileRenderer.cpp b/libs/hwui/pipeline/skia/SkiaProfileRenderer.cpp index 492c39f1288c..81cfc5d93419 100644 --- a/libs/hwui/pipeline/skia/SkiaProfileRenderer.cpp +++ b/libs/hwui/pipeline/skia/SkiaProfileRenderer.cpp @@ -33,13 +33,5 @@ void SkiaProfileRenderer::drawRects(const float* rects, int count, const SkPaint } } -uint32_t SkiaProfileRenderer::getViewportWidth() { - return mCanvas->imageInfo().width(); -} - -uint32_t SkiaProfileRenderer::getViewportHeight() { - return mCanvas->imageInfo().height(); -} - } /* namespace uirenderer */ } /* namespace android */ diff --git a/libs/hwui/pipeline/skia/SkiaProfileRenderer.h b/libs/hwui/pipeline/skia/SkiaProfileRenderer.h index dc8420f4e01b..96d2a5e58139 100644 --- a/libs/hwui/pipeline/skia/SkiaProfileRenderer.h +++ b/libs/hwui/pipeline/skia/SkiaProfileRenderer.h @@ -23,18 +23,21 @@ namespace uirenderer { class SkiaProfileRenderer : public IProfileRenderer { public: - explicit SkiaProfileRenderer(SkCanvas* canvas) : mCanvas(canvas) {} + explicit SkiaProfileRenderer(SkCanvas* canvas, uint32_t width, uint32_t height) + : mCanvas(canvas), mWidth(width), mHeight(height) {} void drawRect(float left, float top, float right, float bottom, const SkPaint& paint) override; void drawRects(const float* rects, int count, const SkPaint& paint) override; - uint32_t getViewportWidth() override; - uint32_t getViewportHeight() override; + uint32_t getViewportWidth() override { return mWidth; } + uint32_t getViewportHeight() override { return mHeight; } virtual ~SkiaProfileRenderer() {} private: // Does not have ownership. SkCanvas* mCanvas; + uint32_t mWidth; + uint32_t mHeight; }; } /* namespace uirenderer */ diff --git a/libs/hwui/pipeline/skia/SkiaRecordingCanvas.cpp b/libs/hwui/pipeline/skia/SkiaRecordingCanvas.cpp index 9c51e628e04a..1f87865f2672 100644 --- a/libs/hwui/pipeline/skia/SkiaRecordingCanvas.cpp +++ b/libs/hwui/pipeline/skia/SkiaRecordingCanvas.cpp @@ -16,7 +16,20 @@ #include "SkiaRecordingCanvas.h" #include "hwui/Paint.h" +#include <include/private/SkTemplates.h> // SkAutoSTMalloc +#include <SkBlendMode.h> +#include <SkData.h> +#include <SkDrawable.h> +#include <SkImage.h> #include <SkImagePriv.h> +#include <SkMatrix.h> +#include <SkPaint.h> +#include <SkPoint.h> +#include <SkRect.h> +#include <SkRefCnt.h> +#include <SkRRect.h> +#include <SkSamplingOptions.h> +#include <SkTypes.h> #include "CanvasTransform.h" #ifdef __ANDROID__ // Layoutlib does not support Layers #include "Layer.h" @@ -56,20 +69,22 @@ void SkiaRecordingCanvas::initDisplayList(uirenderer::RenderNode* renderNode, in mDisplayList->setHasHolePunches(false); } -void SkiaRecordingCanvas::punchHole(const SkRRect& rect) { - // Add the marker annotation to allow HWUI to determine where the current - // clip/transformation should be applied +void SkiaRecordingCanvas::punchHole(const SkRRect& rect, float alpha) { + // Add the marker annotation to allow HWUI to determine the current + // clip/transformation and alpha should be applied SkVector vector = rect.getSimpleRadii(); - float data[2]; + float data[3]; data[0] = vector.x(); data[1] = vector.y(); + data[2] = alpha; mRecorder.drawAnnotation(rect.rect(), HOLE_PUNCH_ANNOTATION.c_str(), - SkData::MakeWithCopy(data, 2 * sizeof(float))); + SkData::MakeWithCopy(data, sizeof(data))); // Clear the current rect within the layer itself SkPaint paint = SkPaint(); - paint.setColor(0); - paint.setBlendMode(SkBlendMode::kClear); + paint.setColor(SkColors::kBlack); + paint.setAlphaf(alpha); + paint.setBlendMode(SkBlendMode::kDstOut); mRecorder.drawRRect(rect, paint); mDisplayList->setHasHolePunches(true); diff --git a/libs/hwui/pipeline/skia/SkiaRecordingCanvas.h b/libs/hwui/pipeline/skia/SkiaRecordingCanvas.h index 1445a27e4248..7844e2cc2a73 100644 --- a/libs/hwui/pipeline/skia/SkiaRecordingCanvas.h +++ b/libs/hwui/pipeline/skia/SkiaRecordingCanvas.h @@ -22,6 +22,11 @@ #include "SkiaDisplayList.h" #include "pipeline/skia/AnimatedDrawables.h" +class SkBitmap; +class SkMatrix; +class SkPaint; +class SkRRect; + namespace android { namespace uirenderer { namespace skiapipeline { @@ -45,7 +50,7 @@ public: initDisplayList(renderNode, width, height); } - virtual void punchHole(const SkRRect& rect) override; + virtual void punchHole(const SkRRect& rect, float alpha) override; virtual void finishRecording(uirenderer::RenderNode* destination) override; std::unique_ptr<SkiaDisplayList> finishRecording(); diff --git a/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp b/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp index 905d46e58014..f10bca675bf7 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() { @@ -88,7 +93,9 @@ IRenderPipeline::DrawResult SkiaVulkanPipeline::draw( if (CC_UNLIKELY(Properties::showDirtyRegions || ProfileType::None != Properties::getProfileType())) { SkCanvas* profileCanvas = backBuffer->getCanvas(); - SkiaProfileRenderer profileRenderer(profileCanvas); + SkAutoCanvasRestore saver(profileCanvas, true); + profileCanvas->concat(mVkSurface->getCurrentPreTransform()); + SkiaProfileRenderer profileRenderer(profileCanvas, frame.width(), frame.height()); profiler->draw(profileRenderer); } @@ -130,7 +137,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..f3d36131a22f 100644 --- a/libs/hwui/pipeline/skia/SkiaVulkanPipeline.h +++ b/libs/hwui/pipeline/skia/SkiaVulkanPipeline.h @@ -22,6 +22,11 @@ #include "renderstate/RenderState.h" +#include "SkRefCnt.h" + +class SkBitmap; +struct SkRect; + namespace android { namespace uirenderer { namespace skiapipeline { @@ -61,6 +66,7 @@ private: renderthread::VulkanManager& vulkanManager(); renderthread::VulkanSurface* mVkSurface = nullptr; + sp<ANativeWindow> mNativeWindow; }; } /* namespace skiapipeline */ diff --git a/libs/hwui/pipeline/skia/TransformCanvas.cpp b/libs/hwui/pipeline/skia/TransformCanvas.cpp index 41e36874b862..c320df035d08 100644 --- a/libs/hwui/pipeline/skia/TransformCanvas.cpp +++ b/libs/hwui/pipeline/skia/TransformCanvas.cpp @@ -19,19 +19,25 @@ #include "HolePunch.h" #include "SkData.h" #include "SkDrawable.h" +#include "SkMatrix.h" +#include "SkPaint.h" +#include "SkRect.h" +#include "SkRRect.h" using namespace android::uirenderer::skiapipeline; void TransformCanvas::onDrawAnnotation(const SkRect& rect, const char* key, SkData* value) { if (HOLE_PUNCH_ANNOTATION == key) { auto* rectParams = reinterpret_cast<const float*>(value->data()); - float radiusX = rectParams[0]; - float radiusY = rectParams[1]; + const float radiusX = rectParams[0]; + const float radiusY = rectParams[1]; + const float alpha = rectParams[2]; SkRRect roundRect = SkRRect::MakeRectXY(rect, radiusX, radiusY); SkPaint paint; paint.setColor(SkColors::kBlack); paint.setBlendMode(mHolePunchBlendMode); + paint.setAlphaf(alpha); mWrappedCanvas->drawRRect(roundRect, paint); } } diff --git a/libs/hwui/pipeline/skia/TransformCanvas.h b/libs/hwui/pipeline/skia/TransformCanvas.h index 685b71d017e9..15f0c1abc55a 100644 --- a/libs/hwui/pipeline/skia/TransformCanvas.h +++ b/libs/hwui/pipeline/skia/TransformCanvas.h @@ -19,6 +19,13 @@ #include "SkPaintFilterCanvas.h" #include <effects/StretchEffect.h> +class SkData; +class SkDrawable; +class SkMatrix; +class SkPaint; +enum class SkBlendMode; +struct SkRect; + class TransformCanvas : public SkPaintFilterCanvas { public: TransformCanvas(SkCanvas* target, SkBlendMode blendmode) : 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/renderthread/EglManager.h b/libs/hwui/renderthread/EglManager.h index fc6b28d2e1ad..b8f8c9267ad8 100644 --- a/libs/hwui/renderthread/EglManager.h +++ b/libs/hwui/renderthread/EglManager.h @@ -18,6 +18,7 @@ #include <EGL/egl.h> #include <EGL/eglext.h> +#include <SkColorSpace.h> #include <SkImageInfo.h> #include <SkRect.h> #include <cutils/compiler.h> diff --git a/libs/hwui/renderthread/IRenderPipeline.h b/libs/hwui/renderthread/IRenderPipeline.h index ef58bc553c23..35e370f52fda 100644 --- a/libs/hwui/renderthread/IRenderPipeline.h +++ b/libs/hwui/renderthread/IRenderPipeline.h @@ -24,6 +24,7 @@ #include "hwui/Bitmap.h" #include "ColorMode.h" +#include <SkColorSpace.h> #include <SkRect.h> #include <utils/RefBase.h> diff --git a/libs/hwui/renderthread/RenderProxy.cpp b/libs/hwui/renderthread/RenderProxy.cpp index a44b498c81c1..40a0bac3762d 100644 --- a/libs/hwui/renderthread/RenderProxy.cpp +++ b/libs/hwui/renderthread/RenderProxy.cpp @@ -29,6 +29,10 @@ #include "utils/Macros.h" #include "utils/TimeUtils.h" +#include <SkBitmap.h> +#include <SkImage.h> +#include <SkPicture.h> + #include <pthread.h> namespace android { @@ -360,12 +364,13 @@ void RenderProxy::setForceDark(bool enable) { mRenderThread.queue().post([this, enable]() { mContext->setForceDark(enable); }); } -int RenderProxy::copySurfaceInto(ANativeWindow* window, int left, int top, int right, int bottom, - SkBitmap* bitmap) { +void RenderProxy::copySurfaceInto(ANativeWindow* window, std::shared_ptr<CopyRequest>&& request) { auto& thread = RenderThread::getInstance(); - return static_cast<int>(thread.queue().runSync([&]() -> auto { - return thread.readback().copySurfaceInto(window, Rect(left, top, right, bottom), bitmap); - })); + ANativeWindow_acquire(window); + thread.queue().post([&thread, window, request = std::move(request)] { + thread.readback().copySurfaceInto(window, request); + ANativeWindow_release(window); + }); } void RenderProxy::prepareToDraw(Bitmap& bitmap) { diff --git a/libs/hwui/renderthread/RenderProxy.h b/libs/hwui/renderthread/RenderProxy.h index ee9efd46e307..2a99a736180f 100644 --- a/libs/hwui/renderthread/RenderProxy.h +++ b/libs/hwui/renderthread/RenderProxy.h @@ -17,19 +17,24 @@ #ifndef RENDERPROXY_H_ #define RENDERPROXY_H_ -#include <SkBitmap.h> +#include <SkRefCnt.h> #include <android/native_window.h> -#include <cutils/compiler.h> #include <android/surface_control.h> +#include <cutils/compiler.h> #include <utils/Functor.h> #include "../FrameMetricsObserver.h" #include "../IContextFactory.h" #include "ColorMode.h" +#include "CopyRequest.h" #include "DrawFrameTask.h" #include "SwapBehavior.h" #include "hwui/Bitmap.h" +class SkBitmap; +class SkPicture; +class SkImage; + namespace android { class GraphicBuffer; class Surface; @@ -133,8 +138,7 @@ public: void removeFrameMetricsObserver(FrameMetricsObserver* observer); void setForceDark(bool enable); - static int copySurfaceInto(ANativeWindow* window, int left, int top, int right, - int bottom, SkBitmap* bitmap); + static void copySurfaceInto(ANativeWindow* window, std::shared_ptr<CopyRequest>&& request); static void prepareToDraw(Bitmap& bitmap); static int copyHWBitmapInto(Bitmap* hwBitmap, SkBitmap* bitmap); diff --git a/libs/hwui/renderthread/RenderThread.cpp b/libs/hwui/renderthread/RenderThread.cpp index 01b956cb3dd5..3ff4081726f3 100644 --- a/libs/hwui/renderthread/RenderThread.cpp +++ b/libs/hwui/renderthread/RenderThread.cpp @@ -266,7 +266,7 @@ void RenderThread::requireGlContext() { } mEglManager->initialize(); - sk_sp<const GrGLInterface> glInterface(GrGLCreateNativeInterface()); + sk_sp<const GrGLInterface> glInterface = GrGLMakeNativeInterface(); LOG_ALWAYS_FATAL_IF(!glInterface.get()); GrContextOptions options; diff --git a/libs/hwui/renderthread/VulkanManager.h b/libs/hwui/renderthread/VulkanManager.h index b8c2bdf112f8..c5196eeccea3 100644 --- a/libs/hwui/renderthread/VulkanManager.h +++ b/libs/hwui/renderthread/VulkanManager.h @@ -51,7 +51,8 @@ typedef void(VKAPI_PTR* PFN_vkFrameBoundaryANDROID)(VkDevice device, VkSemaphore #include "VulkanSurface.h" #include "private/hwui/DrawVkInfo.h" -class GrVkExtensions; +#include <SkColorSpace.h> +#include <SkRefCnt.h> namespace android { namespace uirenderer { diff --git a/libs/hwui/renderthread/VulkanSurface.h b/libs/hwui/renderthread/VulkanSurface.h index beb71b727f51..26486669e712 100644 --- a/libs/hwui/renderthread/VulkanSurface.h +++ b/libs/hwui/renderthread/VulkanSurface.h @@ -20,6 +20,7 @@ #include <system/window.h> #include <vulkan/vulkan.h> +#include <SkColorSpace.h> #include <SkRefCnt.h> #include <SkSize.h> diff --git a/libs/hwui/tests/common/TestUtils.cpp b/libs/hwui/tests/common/TestUtils.cpp index 491af4336f97..a4890ede8faa 100644 --- a/libs/hwui/tests/common/TestUtils.cpp +++ b/libs/hwui/tests/common/TestUtils.cpp @@ -26,7 +26,13 @@ #include <renderthread/VulkanManager.h> #include <utils/Unicode.h> +#include "SkCanvas.h" #include "SkColorData.h" +#include "SkMatrix.h" +#include "SkPath.h" +#include "SkPixmap.h" +#include "SkRect.h" +#include "SkSurface.h" #include "SkUnPreMultiply.h" namespace android { diff --git a/libs/hwui/tests/common/TestUtils.h b/libs/hwui/tests/common/TestUtils.h index 5092675a8104..75865c751d52 100644 --- a/libs/hwui/tests/common/TestUtils.h +++ b/libs/hwui/tests/common/TestUtils.h @@ -27,10 +27,20 @@ #include <renderstate/RenderState.h> #include <renderthread/RenderThread.h> +#include <SkBitmap.h> +#include <SkColor.h> +#include <SkImageInfo.h> +#include <SkRefCnt.h> + #include <gtest/gtest.h> #include <memory> #include <unordered_map> +class SkCanvas; +class SkMatrix; +class SkPath; +struct SkRect; + namespace android { namespace uirenderer { diff --git a/libs/hwui/tests/common/scenes/BitmapShaders.cpp b/libs/hwui/tests/common/scenes/BitmapShaders.cpp index 03aeb55f129b..a07cdf720b50 100644 --- a/libs/hwui/tests/common/scenes/BitmapShaders.cpp +++ b/libs/hwui/tests/common/scenes/BitmapShaders.cpp @@ -14,7 +14,17 @@ * limitations under the License. */ -#include <SkImagePriv.h> +#include <SkBitmap.h> +#include <SkBlendMode.h> +#include <SkCanvas.h> +#include <SkImage.h> +#include <SkImageInfo.h> +#include <SkPaint.h> +#include <SkRect.h> +#include <SkRefCnt.h> +#include <SkSamplingOptions.h> +#include <SkShader.h> +#include <SkTileMode.h> #include "hwui/Paint.h" #include "TestSceneBase.h" #include "tests/common/BitmapAllocationTestUtils.h" diff --git a/libs/hwui/tests/common/scenes/HwBitmap565.cpp b/libs/hwui/tests/common/scenes/HwBitmap565.cpp index cbdb756b8fa7..de0ef6d595f8 100644 --- a/libs/hwui/tests/common/scenes/HwBitmap565.cpp +++ b/libs/hwui/tests/common/scenes/HwBitmap565.cpp @@ -18,6 +18,12 @@ #include "tests/common/BitmapAllocationTestUtils.h" #include "utils/Color.h" +#include <SkBitmap.h> +#include <SkBlendMode.h> +#include <SkCanvas.h> +#include <SkPaint.h> +#include <SkRefCnt.h> + class HwBitmap565; static TestScene::Registrar _HwBitmap565(TestScene::Info{ diff --git a/libs/hwui/tests/common/scenes/HwBitmapInCompositeShader.cpp b/libs/hwui/tests/common/scenes/HwBitmapInCompositeShader.cpp index 564354f04674..0d5ca6df9ff3 100644 --- a/libs/hwui/tests/common/scenes/HwBitmapInCompositeShader.cpp +++ b/libs/hwui/tests/common/scenes/HwBitmapInCompositeShader.cpp @@ -17,6 +17,7 @@ #include "TestSceneBase.h" #include "utils/Color.h" +#include <SkColorSpace.h> #include <SkGradientShader.h> #include <SkImagePriv.h> #include <ui/PixelFormat.h> diff --git a/libs/hwui/tests/common/scenes/ListViewAnimation.cpp b/libs/hwui/tests/common/scenes/ListViewAnimation.cpp index d031923a112b..4a5d9468cd88 100644 --- a/libs/hwui/tests/common/scenes/ListViewAnimation.cpp +++ b/libs/hwui/tests/common/scenes/ListViewAnimation.cpp @@ -17,7 +17,16 @@ #include "TestSceneBase.h" #include "tests/common/TestListViewSceneBase.h" #include "hwui/Paint.h" +#include <SkBitmap.h> +#include <SkCanvas.h> +#include <SkColor.h> #include <SkFont.h> +#include <SkFontTypes.h> +#include <SkPaint.h> +#include <SkPoint.h> +#include <SkRect.h> +#include <SkRefCnt.h> +#include <SkScalar.h> #include <cstdio> class ListViewAnimation; @@ -48,7 +57,7 @@ class ListViewAnimation : public TestListViewSceneBase { 128 * 3; paint.setColor(bgDark ? Color::White : Color::Grey_700); - SkFont font; + SkFont font; font.setSize(size / 2); char charToShow = 'A' + (rand() % 26); const SkPoint pos = {SkIntToScalar(size / 2), diff --git a/libs/hwui/tests/common/scenes/MagnifierAnimation.cpp b/libs/hwui/tests/common/scenes/MagnifierAnimation.cpp index edadf78db051..13a438199ae5 100644 --- a/libs/hwui/tests/common/scenes/MagnifierAnimation.cpp +++ b/libs/hwui/tests/common/scenes/MagnifierAnimation.cpp @@ -19,19 +19,57 @@ #include "utils/Color.h" #include "hwui/Paint.h" +#include <SkBitmap.h> +#include <SkBlendMode.h> +#include <SkFont.h> + class MagnifierAnimation; +using Rect = android::uirenderer::Rect; + static TestScene::Registrar _Magnifier(TestScene::Info{ "magnifier", "A sample magnifier using Readback", TestScene::simpleCreateScene<MagnifierAnimation>}); +class BlockingCopyRequest : public CopyRequest { + sk_sp<Bitmap> mDestination; + std::mutex mLock; + std::condition_variable mCondVar; + CopyResult mResult; + +public: + BlockingCopyRequest(::Rect rect, sk_sp<Bitmap> bitmap) + : CopyRequest(rect), mDestination(bitmap) {} + + virtual SkBitmap getDestinationBitmap(int srcWidth, int srcHeight) override { + SkBitmap bitmap; + mDestination->getSkBitmap(&bitmap); + return bitmap; + } + + virtual void onCopyFinished(CopyResult result) override { + std::unique_lock _lock{mLock}; + mResult = result; + mCondVar.notify_all(); + } + + CopyResult waitForResult() { + std::unique_lock _lock{mLock}; + mCondVar.wait(_lock); + return mResult; + } +}; + class MagnifierAnimation : public TestScene { public: sp<RenderNode> card; sp<RenderNode> zoomImageView; + sk_sp<Bitmap> magnifier; + std::shared_ptr<BlockingCopyRequest> copyRequest; void createContent(int width, int height, Canvas& canvas) override { magnifier = TestUtils::createBitmap(200, 100); + setupCopyRequest(); SkBitmap temp; magnifier->getSkBitmap(&temp); temp.eraseColor(Color::White); @@ -61,19 +99,20 @@ public: canvas.enableZ(false); } + void setupCopyRequest() { + constexpr int x = 90; + constexpr int y = 325; + copyRequest = std::make_shared<BlockingCopyRequest>( + ::Rect(x, y, x + magnifier->width(), y + magnifier->height()), magnifier); + } + void doFrame(int frameNr) override { int curFrame = frameNr % 150; card->mutateStagingProperties().setTranslationX(curFrame); card->setPropertyFieldsDirty(RenderNode::X | RenderNode::Y); if (renderTarget) { - SkBitmap temp; - magnifier->getSkBitmap(&temp); - constexpr int x = 90; - constexpr int y = 325; - RenderProxy::copySurfaceInto(renderTarget.get(), x, y, x + magnifier->width(), - y + magnifier->height(), &temp); + RenderProxy::copySurfaceInto(renderTarget.get(), copyRequest); + copyRequest->waitForResult(); } } - - sk_sp<Bitmap> magnifier; }; diff --git a/libs/hwui/tests/common/scenes/ReadbackFromHardwareBitmap.cpp b/libs/hwui/tests/common/scenes/ReadbackFromHardwareBitmap.cpp index 716d3979bdcb..3caaf8236d8a 100644 --- a/libs/hwui/tests/common/scenes/ReadbackFromHardwareBitmap.cpp +++ b/libs/hwui/tests/common/scenes/ReadbackFromHardwareBitmap.cpp @@ -16,6 +16,12 @@ #include "TestSceneBase.h" +#include <SkBitmap.h> +#include <SkCanvas.h> +#include <SkPaint.h> +#include <SkRect.h> +#include <SkRefCnt.h> + class ReadbackFromHardware; static TestScene::Registrar _SaveLayer(TestScene::Info{ diff --git a/libs/hwui/tests/common/scenes/RecentsAnimation.cpp b/libs/hwui/tests/common/scenes/RecentsAnimation.cpp index 1c2507867f6e..27948f8b4b43 100644 --- a/libs/hwui/tests/common/scenes/RecentsAnimation.cpp +++ b/libs/hwui/tests/common/scenes/RecentsAnimation.cpp @@ -17,6 +17,11 @@ #include "TestSceneBase.h" #include "utils/Color.h" +#include <SkBitmap.h> +#include <SkBlendMode.h> +#include <SkColor.h> +#include <SkRefCnt.h> + class RecentsAnimation; static TestScene::Registrar _Recents(TestScene::Info{ diff --git a/libs/hwui/tests/common/scenes/SimpleColorMatrixAnimation.cpp b/libs/hwui/tests/common/scenes/SimpleColorMatrixAnimation.cpp index a0bc5aa245d5..2aeb42cc0e20 100644 --- a/libs/hwui/tests/common/scenes/SimpleColorMatrixAnimation.cpp +++ b/libs/hwui/tests/common/scenes/SimpleColorMatrixAnimation.cpp @@ -16,7 +16,8 @@ #include "TestSceneBase.h" -#include <SkColorMatrixFilter.h> +#include <SkColorFilter.h> +#include <SkColorMatrix.h> #include <SkGradientShader.h> class SimpleColorMatrixAnimation; diff --git a/libs/hwui/tests/common/scenes/StretchyListViewAnimation.cpp b/libs/hwui/tests/common/scenes/StretchyListViewAnimation.cpp index e677549b7894..7d3ca9642458 100644 --- a/libs/hwui/tests/common/scenes/StretchyListViewAnimation.cpp +++ b/libs/hwui/tests/common/scenes/StretchyListViewAnimation.cpp @@ -14,7 +14,15 @@ * limitations under the License. */ +#include <SkBitmap.h> +#include <SkCanvas.h> +#include <SkColor.h> #include <SkFont.h> +#include <SkFontTypes.h> +#include <SkPaint.h> +#include <SkPoint.h> +#include <SkRefCnt.h> +#include <SkRRect.h> #include <cstdio> #include "TestSceneBase.h" #include "hwui/Paint.h" @@ -130,7 +138,7 @@ private: roundRectPaint.setColor(Color::White); if (addHolePunch) { // Punch a hole but then cover it up, we don't want to actually see it - canvas.punchHole(SkRRect::MakeRect(SkRect::MakeWH(itemWidth, itemHeight))); + canvas.punchHole(SkRRect::MakeRect(SkRect::MakeWH(itemWidth, itemHeight)), 1.f); } canvas.drawRoundRect(0, 0, itemWidth, itemHeight, dp(6), dp(6), roundRectPaint); @@ -227,4 +235,4 @@ class StretchyUniformLayerListViewHolePunch : public StretchyListViewAnimation { StretchEffectBehavior stretchBehavior() override { return StretchEffectBehavior::UniformScale; } bool haveHolePunch() override { return true; } bool forceLayer() override { return true; } -};
\ No newline at end of file +}; diff --git a/libs/hwui/tests/common/scenes/TvApp.cpp b/libs/hwui/tests/common/scenes/TvApp.cpp index c6219c485b85..aff8ca1e26c7 100644 --- a/libs/hwui/tests/common/scenes/TvApp.cpp +++ b/libs/hwui/tests/common/scenes/TvApp.cpp @@ -14,7 +14,12 @@ * limitations under the License. */ +#include "SkBitmap.h" #include "SkBlendMode.h" +#include "SkColorFilter.h" +#include "SkFont.h" +#include "SkImageInfo.h" +#include "SkRefCnt.h" #include "TestSceneBase.h" #include "tests/common/BitmapAllocationTestUtils.h" #include "hwui/Paint.h" diff --git a/libs/hwui/tests/unit/CanvasOpTests.cpp b/libs/hwui/tests/unit/CanvasOpTests.cpp index 2cf3456694b0..d2b1ef91a898 100644 --- a/libs/hwui/tests/unit/CanvasOpTests.cpp +++ b/libs/hwui/tests/unit/CanvasOpTests.cpp @@ -23,9 +23,17 @@ #include <tests/common/CallCountingCanvas.h> -#include "SkPictureRecorder.h" +#include "SkBitmap.h" +#include "SkCanvas.h" #include "SkColor.h" +#include "SkImageInfo.h" #include "SkLatticeIter.h" +#include "SkPaint.h" +#include "SkPath.h" +#include "SkPictureRecorder.h" +#include "SkRRect.h" +#include "SkRect.h" +#include "SkRegion.h" #include "pipeline/skia/AnimatedDrawables.h" #include <SkNoDrawCanvas.h> diff --git a/libs/hwui/tests/unit/EglManagerTests.cpp b/libs/hwui/tests/unit/EglManagerTests.cpp index 7f2e1589ae6c..ec9ab90fa46b 100644 --- a/libs/hwui/tests/unit/EglManagerTests.cpp +++ b/libs/hwui/tests/unit/EglManagerTests.cpp @@ -20,6 +20,8 @@ #include "renderthread/RenderEffectCapabilityQuery.h" #include "tests/common/TestContext.h" +#include <SkColorSpace.h> + using namespace android; using namespace android::uirenderer; using namespace android::uirenderer::renderthread; diff --git a/libs/hwui/tests/unit/FatalTestCanvas.h b/libs/hwui/tests/unit/FatalTestCanvas.h index 2a74afc5bb7a..96a0c6114682 100644 --- a/libs/hwui/tests/unit/FatalTestCanvas.h +++ b/libs/hwui/tests/unit/FatalTestCanvas.h @@ -19,6 +19,8 @@ #include <SkCanvas.h> #include <gtest/gtest.h> +class SkRRect; + namespace { class TestCanvasBase : public SkCanvas { 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/ShaderCacheTests.cpp b/libs/hwui/tests/unit/ShaderCacheTests.cpp index 974d85a453db..576e9466d322 100644 --- a/libs/hwui/tests/unit/ShaderCacheTests.cpp +++ b/libs/hwui/tests/unit/ShaderCacheTests.cpp @@ -25,6 +25,8 @@ #include <cstdint> #include "FileBlobCache.h" #include "pipeline/skia/ShaderCache.h" +#include <SkData.h> +#include <SkRefCnt.h> using namespace android::uirenderer::skiapipeline; diff --git a/libs/hwui/tests/unit/SkiaBehaviorTests.cpp b/libs/hwui/tests/unit/SkiaBehaviorTests.cpp index dc1b2e668dd0..c1ddbd36bcfd 100644 --- a/libs/hwui/tests/unit/SkiaBehaviorTests.cpp +++ b/libs/hwui/tests/unit/SkiaBehaviorTests.cpp @@ -16,9 +16,14 @@ #include "tests/common/TestUtils.h" +#include <SkBitmap.h> +#include <SkBlendMode.h> +#include <SkColor.h> #include <SkColorMatrixFilter.h> #include <SkColorSpace.h> -#include <SkImagePriv.h> +#include <SkImageInfo.h> +#include <SkPaint.h> +#include <SkPath.h> #include <SkPathOps.h> #include <SkShader.h> #include <gtest/gtest.h> diff --git a/libs/hwui/tests/unit/SkiaCanvasTests.cpp b/libs/hwui/tests/unit/SkiaCanvasTests.cpp index dae3c9435712..50d9f5683a8b 100644 --- a/libs/hwui/tests/unit/SkiaCanvasTests.cpp +++ b/libs/hwui/tests/unit/SkiaCanvasTests.cpp @@ -18,6 +18,7 @@ #include <hwui/Paint.h> #include <SkCanvasStateUtils.h> +#include <SkColorSpace.h> #include <SkPicture.h> #include <SkPictureRecorder.h> #include <gtest/gtest.h> 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) { diff --git a/libs/hwui/tests/unit/SkiaRenderPropertiesTests.cpp b/libs/hwui/tests/unit/SkiaRenderPropertiesTests.cpp index 15ecf5831f3a..ced667eb76e5 100644 --- a/libs/hwui/tests/unit/SkiaRenderPropertiesTests.cpp +++ b/libs/hwui/tests/unit/SkiaRenderPropertiesTests.cpp @@ -17,6 +17,7 @@ #include <VectorDrawable.h> #include <gtest/gtest.h> +#include <SkCanvas.h> #include <SkClipStack.h> #include <SkSurface_Base.h> #include <string.h> diff --git a/libs/hwui/tests/unit/TypefaceTests.cpp b/libs/hwui/tests/unit/TypefaceTests.cpp index ab23448ab93f..499afa039d1f 100644 --- a/libs/hwui/tests/unit/TypefaceTests.cpp +++ b/libs/hwui/tests/unit/TypefaceTests.cpp @@ -21,8 +21,11 @@ #include <sys/stat.h> #include <utils/Log.h> +#include "SkData.h" #include "SkFontMgr.h" +#include "SkRefCnt.h" #include "SkStream.h" +#include "SkTypeface.h" #include "hwui/MinikinSkia.h" #include "hwui/Typeface.h" @@ -61,7 +64,7 @@ std::shared_ptr<minikin::FontFamily> buildFamily(const char* fileName) { std::vector<minikin::FontVariation>()); std::vector<std::shared_ptr<minikin::Font>> fonts; fonts.push_back(minikin::Font::Builder(font).build()); - return std::make_shared<minikin::FontFamily>(std::move(fonts)); + return minikin::FontFamily::create(std::move(fonts)); } std::vector<std::shared_ptr<minikin::FontFamily>> makeSingleFamlyVector(const char* fileName) { @@ -70,7 +73,8 @@ std::vector<std::shared_ptr<minikin::FontFamily>> makeSingleFamlyVector(const ch TEST(TypefaceTest, resolveDefault_and_setDefaultTest) { std::unique_ptr<Typeface> regular(Typeface::createFromFamilies( - makeSingleFamlyVector(kRobotoVariable), RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE)); + makeSingleFamlyVector(kRobotoVariable), RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE, + nullptr /* fallback */)); EXPECT_EQ(regular.get(), Typeface::resolveDefault(regular.get())); // Keep the original to restore it later. @@ -348,24 +352,24 @@ TEST(TypefaceTest, createAbsolute) { TEST(TypefaceTest, createFromFamilies_Single) { // In Java, new // Typeface.Builder("Roboto-Regular.ttf").setWeight(400).setItalic(false).build(); - std::unique_ptr<Typeface> regular( - Typeface::createFromFamilies(makeSingleFamlyVector(kRobotoVariable), 400, false)); + std::unique_ptr<Typeface> regular(Typeface::createFromFamilies( + makeSingleFamlyVector(kRobotoVariable), 400, false, nullptr /* fallback */)); EXPECT_EQ(400, regular->fStyle.weight()); EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, regular->fStyle.slant()); EXPECT_EQ(Typeface::kNormal, regular->fAPIStyle); // In Java, new // Typeface.Builder("Roboto-Regular.ttf").setWeight(700).setItalic(false).build(); - std::unique_ptr<Typeface> bold( - Typeface::createFromFamilies(makeSingleFamlyVector(kRobotoVariable), 700, false)); + std::unique_ptr<Typeface> bold(Typeface::createFromFamilies( + makeSingleFamlyVector(kRobotoVariable), 700, false, nullptr /* fallback */)); EXPECT_EQ(700, bold->fStyle.weight()); EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->fStyle.slant()); EXPECT_EQ(Typeface::kBold, bold->fAPIStyle); // In Java, new // Typeface.Builder("Roboto-Regular.ttf").setWeight(400).setItalic(true).build(); - std::unique_ptr<Typeface> italic( - Typeface::createFromFamilies(makeSingleFamlyVector(kRobotoVariable), 400, true)); + std::unique_ptr<Typeface> italic(Typeface::createFromFamilies( + makeSingleFamlyVector(kRobotoVariable), 400, true, nullptr /* fallback */)); EXPECT_EQ(400, italic->fStyle.weight()); EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->fStyle.slant()); EXPECT_EQ(Typeface::kItalic, italic->fAPIStyle); @@ -373,8 +377,8 @@ TEST(TypefaceTest, createFromFamilies_Single) { // In Java, // new // Typeface.Builder("Roboto-Regular.ttf").setWeight(700).setItalic(true).build(); - std::unique_ptr<Typeface> boldItalic( - Typeface::createFromFamilies(makeSingleFamlyVector(kRobotoVariable), 700, true)); + std::unique_ptr<Typeface> boldItalic(Typeface::createFromFamilies( + makeSingleFamlyVector(kRobotoVariable), 700, true, nullptr /* fallback */)); EXPECT_EQ(700, boldItalic->fStyle.weight()); EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->fStyle.slant()); EXPECT_EQ(Typeface::kItalic, italic->fAPIStyle); @@ -382,8 +386,8 @@ TEST(TypefaceTest, createFromFamilies_Single) { // In Java, // new // Typeface.Builder("Roboto-Regular.ttf").setWeight(1100).setItalic(false).build(); - std::unique_ptr<Typeface> over1000( - Typeface::createFromFamilies(makeSingleFamlyVector(kRobotoVariable), 1100, false)); + std::unique_ptr<Typeface> over1000(Typeface::createFromFamilies( + makeSingleFamlyVector(kRobotoVariable), 1100, false, nullptr /* fallback */)); EXPECT_EQ(1000, over1000->fStyle.weight()); EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, over1000->fStyle.slant()); EXPECT_EQ(Typeface::kBold, over1000->fAPIStyle); @@ -391,30 +395,33 @@ TEST(TypefaceTest, createFromFamilies_Single) { TEST(TypefaceTest, createFromFamilies_Single_resolveByTable) { // In Java, new Typeface.Builder("Family-Regular.ttf").build(); - std::unique_ptr<Typeface> regular(Typeface::createFromFamilies( - makeSingleFamlyVector(kRegularFont), RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE)); + std::unique_ptr<Typeface> regular( + Typeface::createFromFamilies(makeSingleFamlyVector(kRegularFont), RESOLVE_BY_FONT_TABLE, + RESOLVE_BY_FONT_TABLE, nullptr /* fallback */)); EXPECT_EQ(400, regular->fStyle.weight()); EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, regular->fStyle.slant()); EXPECT_EQ(Typeface::kNormal, regular->fAPIStyle); // In Java, new Typeface.Builder("Family-Bold.ttf").build(); - std::unique_ptr<Typeface> bold(Typeface::createFromFamilies( - makeSingleFamlyVector(kBoldFont), RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE)); + std::unique_ptr<Typeface> bold( + Typeface::createFromFamilies(makeSingleFamlyVector(kBoldFont), RESOLVE_BY_FONT_TABLE, + RESOLVE_BY_FONT_TABLE, nullptr /* fallback */)); EXPECT_EQ(700, bold->fStyle.weight()); EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->fStyle.slant()); EXPECT_EQ(Typeface::kBold, bold->fAPIStyle); // In Java, new Typeface.Builder("Family-Italic.ttf").build(); - std::unique_ptr<Typeface> italic(Typeface::createFromFamilies( - makeSingleFamlyVector(kItalicFont), RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE)); + std::unique_ptr<Typeface> italic( + Typeface::createFromFamilies(makeSingleFamlyVector(kItalicFont), RESOLVE_BY_FONT_TABLE, + RESOLVE_BY_FONT_TABLE, nullptr /* fallback */)); EXPECT_EQ(400, italic->fStyle.weight()); EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->fStyle.slant()); EXPECT_EQ(Typeface::kItalic, italic->fAPIStyle); // In Java, new Typeface.Builder("Family-BoldItalic.ttf").build(); - std::unique_ptr<Typeface> boldItalic( - Typeface::createFromFamilies(makeSingleFamlyVector(kBoldItalicFont), - RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE)); + std::unique_ptr<Typeface> boldItalic(Typeface::createFromFamilies( + makeSingleFamlyVector(kBoldItalicFont), RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE, + nullptr /* fallback */)); EXPECT_EQ(700, boldItalic->fStyle.weight()); EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->fStyle.slant()); EXPECT_EQ(Typeface::kItalic, italic->fAPIStyle); @@ -424,8 +431,9 @@ TEST(TypefaceTest, createFromFamilies_Family) { std::vector<std::shared_ptr<minikin::FontFamily>> families = { buildFamily(kRegularFont), buildFamily(kBoldFont), buildFamily(kItalicFont), buildFamily(kBoldItalicFont)}; - std::unique_ptr<Typeface> typeface(Typeface::createFromFamilies( - std::move(families), RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE)); + std::unique_ptr<Typeface> typeface( + Typeface::createFromFamilies(std::move(families), RESOLVE_BY_FONT_TABLE, + RESOLVE_BY_FONT_TABLE, nullptr /* fallback */)); EXPECT_EQ(400, typeface->fStyle.weight()); EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, typeface->fStyle.slant()); } @@ -433,10 +441,24 @@ TEST(TypefaceTest, createFromFamilies_Family) { TEST(TypefaceTest, createFromFamilies_Family_withoutRegular) { std::vector<std::shared_ptr<minikin::FontFamily>> families = { buildFamily(kBoldFont), buildFamily(kItalicFont), buildFamily(kBoldItalicFont)}; - std::unique_ptr<Typeface> typeface(Typeface::createFromFamilies( - std::move(families), RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE)); + std::unique_ptr<Typeface> typeface( + Typeface::createFromFamilies(std::move(families), RESOLVE_BY_FONT_TABLE, + RESOLVE_BY_FONT_TABLE, nullptr /* fallback */)); EXPECT_EQ(700, typeface->fStyle.weight()); EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, typeface->fStyle.slant()); } +TEST(TypefaceTest, createFromFamilies_Family_withFallback) { + std::vector<std::shared_ptr<minikin::FontFamily>> fallbackFamilies = { + buildFamily(kBoldFont), buildFamily(kItalicFont), buildFamily(kBoldItalicFont)}; + std::unique_ptr<Typeface> fallback( + Typeface::createFromFamilies(std::move(fallbackFamilies), RESOLVE_BY_FONT_TABLE, + RESOLVE_BY_FONT_TABLE, nullptr /* fallback */)); + std::unique_ptr<Typeface> regular( + Typeface::createFromFamilies(makeSingleFamlyVector(kRegularFont), RESOLVE_BY_FONT_TABLE, + RESOLVE_BY_FONT_TABLE, fallback.get())); + EXPECT_EQ(400, regular->fStyle.weight()); + EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, regular->fStyle.slant()); +} + } // namespace diff --git a/libs/hwui/tests/unit/VectorDrawableTests.cpp b/libs/hwui/tests/unit/VectorDrawableTests.cpp index 6d4c57413f00..c1c21bd7dfbf 100644 --- a/libs/hwui/tests/unit/VectorDrawableTests.cpp +++ b/libs/hwui/tests/unit/VectorDrawableTests.cpp @@ -21,6 +21,12 @@ #include "utils/MathUtils.h" #include "utils/VectorDrawableUtils.h" +#include <SkBitmap.h> +#include <SkCanvas.h> +#include <SkPath.h> +#include <SkRefCnt.h> +#include <SkShader.h> + #include <functional> namespace android { diff --git a/libs/hwui/thread/WorkQueue.h b/libs/hwui/thread/WorkQueue.h index 46b8bc07b432..f2751d2a6cc7 100644 --- a/libs/hwui/thread/WorkQueue.h +++ b/libs/hwui/thread/WorkQueue.h @@ -57,7 +57,7 @@ private: public: WorkQueue(std::function<void()>&& wakeFunc, std::mutex& lock) - : mWakeFunc(move(wakeFunc)), mLock(lock) {} + : mWakeFunc(std::move(wakeFunc)), mLock(lock) {} void process() { auto now = clock::now(); diff --git a/libs/incident/libincident.map.txt b/libs/incident/libincident.map.txt index f157763f1a03..f75cceaf59fa 100644 --- a/libs/incident/libincident.map.txt +++ b/libs/incident/libincident.map.txt @@ -1,15 +1,15 @@ LIBINCIDENT { global: - AIncidentReportArgs_init; # apex # introduced=30 - AIncidentReportArgs_clone; # apex # introduced=30 - AIncidentReportArgs_delete; # apex # introduced=30 - AIncidentReportArgs_setAll; # apex # introduced=30 - AIncidentReportArgs_setPrivacyPolicy; # apex # introduced=30 - AIncidentReportArgs_addSection; # apex # introduced=30 - AIncidentReportArgs_setReceiverPackage; # apex # introduced=30 - AIncidentReportArgs_setReceiverClass; # apex # introduced=30 - AIncidentReportArgs_addHeader; # apex # introduced=30 - AIncidentReportArgs_takeReport; # apex # introduced=30 + AIncidentReportArgs_init; # systemapi # introduced=30 + AIncidentReportArgs_clone; # systemapi # introduced=30 + AIncidentReportArgs_delete; # systemapi # introduced=30 + AIncidentReportArgs_setAll; # systemapi # introduced=30 + AIncidentReportArgs_setPrivacyPolicy; # systemapi # introduced=30 + AIncidentReportArgs_addSection; # systemapi # introduced=30 + AIncidentReportArgs_setReceiverPackage; # systemapi # introduced=30 + AIncidentReportArgs_setReceiverClass; # systemapi # introduced=30 + AIncidentReportArgs_addHeader; # systemapi # introduced=30 + AIncidentReportArgs_takeReport; # systemapi # introduced=30 local: *; }; diff --git a/libs/input/MouseCursorController.cpp b/libs/input/MouseCursorController.cpp index 45da008c3e8e..0e7b7ff4319d 100644 --- a/libs/input/MouseCursorController.cpp +++ b/libs/input/MouseCursorController.cpp @@ -22,14 +22,9 @@ #include "MouseCursorController.h" +#include <input/Input.h> #include <log/log.h> -#include <SkBitmap.h> -#include <SkBlendMode.h> -#include <SkCanvas.h> -#include <SkColor.h> -#include <SkPaint.h> - namespace { // Time to spend fading out the pointer completely. const nsecs_t POINTER_FADE_DURATION = 500 * 1000000LL; // 500 ms @@ -292,7 +287,7 @@ void MouseCursorController::setDisplayViewport(const DisplayViewport& viewport, updatePointerLocked(); } -void MouseCursorController::updatePointerIcon(int32_t iconId) { +void MouseCursorController::updatePointerIcon(PointerIconStyle iconId) { std::scoped_lock lock(mLock); if (mLocked.requestedPointerType != iconId) { @@ -305,7 +300,7 @@ void MouseCursorController::updatePointerIcon(int32_t iconId) { void MouseCursorController::setCustomPointerIcon(const SpriteIcon& icon) { std::scoped_lock lock(mLock); - const int32_t iconId = mContext.getPolicy()->getCustomPointerIconId(); + const PointerIconStyle iconId = mContext.getPolicy()->getCustomPointerIconId(); mLocked.additionalMouseResources[iconId] = icon; mLocked.requestedPointerType = iconId; mLocked.updatePointerIcon = true; @@ -340,7 +335,7 @@ bool MouseCursorController::doFadingAnimationLocked(nsecs_t timestamp) REQUIRES( } bool MouseCursorController::doBitmapAnimationLocked(nsecs_t timestamp) REQUIRES(mLock) { - std::map<int32_t, PointerAnimation>::const_iterator iter = + std::map<PointerIconStyle, PointerAnimation>::const_iterator iter = mLocked.animationResources.find(mLocked.requestedPointerType); if (iter == mLocked.animationResources.end()) { return false; @@ -386,10 +381,10 @@ void MouseCursorController::updatePointerLocked() REQUIRES(mLock) { if (mLocked.requestedPointerType == mContext.getPolicy()->getDefaultPointerIconId()) { mLocked.pointerSprite->setIcon(mLocked.pointerIcon); } else { - std::map<int32_t, SpriteIcon>::const_iterator iter = + std::map<PointerIconStyle, SpriteIcon>::const_iterator iter = mLocked.additionalMouseResources.find(mLocked.requestedPointerType); if (iter != mLocked.additionalMouseResources.end()) { - std::map<int32_t, PointerAnimation>::const_iterator anim_iter = + std::map<PointerIconStyle, PointerAnimation>::const_iterator anim_iter = mLocked.animationResources.find(mLocked.requestedPointerType); if (anim_iter != mLocked.animationResources.end()) { mLocked.animationFrameIndex = 0; diff --git a/libs/input/MouseCursorController.h b/libs/input/MouseCursorController.h index c0ab58bd2e7e..208d33d7c717 100644 --- a/libs/input/MouseCursorController.h +++ b/libs/input/MouseCursorController.h @@ -54,7 +54,7 @@ public: void unfade(PointerControllerInterface::Transition transition); void setDisplayViewport(const DisplayViewport& viewport, bool getAdditionalMouseResources); - void updatePointerIcon(int32_t iconId); + void updatePointerIcon(PointerIconStyle iconId); void setCustomPointerIcon(const SpriteIcon& icon); void reloadPointerResources(bool getAdditionalMouseResources); @@ -88,10 +88,10 @@ private: bool resourcesLoaded; - std::map<int32_t, SpriteIcon> additionalMouseResources; - std::map<int32_t, PointerAnimation> animationResources; + std::map<PointerIconStyle, SpriteIcon> additionalMouseResources; + std::map<PointerIconStyle, PointerAnimation> animationResources; - int32_t requestedPointerType; + PointerIconStyle requestedPointerType; int32_t buttonState; diff --git a/libs/input/PointerController.cpp b/libs/input/PointerController.cpp index 10ea6512c724..54f893e165f7 100644 --- a/libs/input/PointerController.cpp +++ b/libs/input/PointerController.cpp @@ -264,7 +264,7 @@ void PointerController::setDisplayViewport(const DisplayViewport& viewport) { } } -void PointerController::updatePointerIcon(int32_t iconId) { +void PointerController::updatePointerIcon(PointerIconStyle iconId) { std::scoped_lock lock(getLock()); mCursorController.updatePointerIcon(iconId); } diff --git a/libs/input/PointerController.h b/libs/input/PointerController.h index eab030f71e1a..33480e8fa194 100644 --- a/libs/input/PointerController.h +++ b/libs/input/PointerController.h @@ -65,7 +65,7 @@ public: BitSet32 spotIdBits, int32_t displayId); virtual void clearSpots(); - void updatePointerIcon(int32_t iconId); + void updatePointerIcon(PointerIconStyle iconId); void setCustomPointerIcon(const SpriteIcon& icon); void setInactivityTimeout(InactivityTimeout inactivityTimeout); void doInactivityTimeout(); diff --git a/libs/input/PointerControllerContext.h b/libs/input/PointerControllerContext.h index c2bc1e020279..1797428b343f 100644 --- a/libs/input/PointerControllerContext.h +++ b/libs/input/PointerControllerContext.h @@ -75,10 +75,11 @@ public: virtual void loadPointerIcon(SpriteIcon* icon, int32_t displayId) = 0; virtual void loadPointerResources(PointerResources* outResources, int32_t displayId) = 0; virtual void loadAdditionalMouseResources( - std::map<int32_t, SpriteIcon>* outResources, - std::map<int32_t, PointerAnimation>* outAnimationResources, int32_t displayId) = 0; - virtual int32_t getDefaultPointerIconId() = 0; - virtual int32_t getCustomPointerIconId() = 0; + std::map<PointerIconStyle, SpriteIcon>* outResources, + std::map<PointerIconStyle, PointerAnimation>* outAnimationResources, + int32_t displayId) = 0; + virtual PointerIconStyle getDefaultPointerIconId() = 0; + virtual PointerIconStyle getCustomPointerIconId() = 0; virtual void onPointerDisplayIdChanged(int32_t displayId, float xPos, float yPos) = 0; }; diff --git a/libs/input/SpriteController.cpp b/libs/input/SpriteController.cpp index 2b809eab4ae4..130b204954b4 100644 --- a/libs/input/SpriteController.cpp +++ b/libs/input/SpriteController.cpp @@ -131,8 +131,9 @@ void SpriteController::doUpdateSprites() { update.state.surfaceHeight = update.state.icon.height(); update.state.surfaceDrawn = false; update.state.surfaceVisible = false; - update.state.surfaceControl = obtainSurface( - update.state.surfaceWidth, update.state.surfaceHeight); + update.state.surfaceControl = + obtainSurface(update.state.surfaceWidth, update.state.surfaceHeight, + update.state.displayId); if (update.state.surfaceControl != NULL) { update.surfaceChanged = surfaceChanged = true; } @@ -168,8 +169,8 @@ void SpriteController::doUpdateSprites() { } } - // If surface is a new one, we have to set right layer stack. - if (update.surfaceChanged || update.state.dirty & DIRTY_DISPLAY_ID) { + // If surface has changed to a new display, we have to reparent it. + if (update.state.dirty & DIRTY_DISPLAY_ID) { t.reparent(update.state.surfaceControl, mParentSurfaceProvider(update.state.displayId)); needApplyTransaction = true; } @@ -242,15 +243,14 @@ void SpriteController::doUpdateSprites() { && (becomingVisible || (update.state.dirty & (DIRTY_HOTSPOT | DIRTY_ICON_STYLE)))) { Parcel p; - p.writeInt32(update.state.icon.style); + p.writeInt32(static_cast<int32_t>(update.state.icon.style)); p.writeFloat(update.state.icon.hotSpotX); p.writeFloat(update.state.icon.hotSpotY); // Pass cursor metadata in the sprite surface so that when Android is running as a // client OS (e.g. ARC++) the host OS can get the requested cursor metadata and // update mouse cursor in the host OS. - t.setMetadata( - update.state.surfaceControl, METADATA_MOUSE_CURSOR, p); + t.setMetadata(update.state.surfaceControl, gui::METADATA_MOUSE_CURSOR, p); } int32_t surfaceLayer = mOverlayLayer + update.state.layer; @@ -331,21 +331,28 @@ void SpriteController::ensureSurfaceComposerClient() { } } -sp<SurfaceControl> SpriteController::obtainSurface(int32_t width, int32_t height) { +sp<SurfaceControl> SpriteController::obtainSurface(int32_t width, int32_t height, + int32_t displayId) { ensureSurfaceComposerClient(); - sp<SurfaceControl> surfaceControl = mSurfaceComposerClient->createSurface( - String8("Sprite"), width, height, PIXEL_FORMAT_RGBA_8888, - ISurfaceComposerClient::eHidden | - ISurfaceComposerClient::eCursorWindow); - if (surfaceControl == NULL || !surfaceControl->isValid()) { + const sp<SurfaceControl> parent = mParentSurfaceProvider(displayId); + if (parent == nullptr) { + ALOGE("Failed to get the parent surface for pointers on display %d", displayId); + } + + const sp<SurfaceControl> surfaceControl = + mSurfaceComposerClient->createSurface(String8("Sprite"), width, height, + PIXEL_FORMAT_RGBA_8888, + ISurfaceComposerClient::eHidden | + ISurfaceComposerClient::eCursorWindow, + parent ? parent->getHandle() : nullptr); + if (surfaceControl == nullptr || !surfaceControl->isValid()) { ALOGE("Error creating sprite surface."); - return NULL; + return nullptr; } return surfaceControl; } - // --- SpriteController::SpriteImpl --- SpriteController::SpriteImpl::SpriteImpl(const sp<SpriteController> controller) : diff --git a/libs/input/SpriteController.h b/libs/input/SpriteController.h index 2e9cb9685c46..1f113c045360 100644 --- a/libs/input/SpriteController.h +++ b/libs/input/SpriteController.h @@ -265,7 +265,7 @@ private: void doDisposeSurfaces(); void ensureSurfaceComposerClient(); - sp<SurfaceControl> obtainSurface(int32_t width, int32_t height); + sp<SurfaceControl> obtainSurface(int32_t width, int32_t height, int32_t displayId); }; } // namespace android diff --git a/libs/input/SpriteIcon.h b/libs/input/SpriteIcon.h index a257d7e89ebc..5f085bbd2374 100644 --- a/libs/input/SpriteIcon.h +++ b/libs/input/SpriteIcon.h @@ -19,6 +19,7 @@ #include <android/graphics/bitmap.h> #include <gui/Surface.h> +#include <input/Input.h> namespace android { @@ -26,12 +27,13 @@ namespace android { * Icon that a sprite displays, including its hotspot. */ struct SpriteIcon { - inline SpriteIcon() : style(0), hotSpotX(0), hotSpotY(0) {} - inline SpriteIcon(const graphics::Bitmap& bitmap, int32_t style, float hotSpotX, float hotSpotY) + inline SpriteIcon() : style(PointerIconStyle::TYPE_NULL), hotSpotX(0), hotSpotY(0) {} + inline SpriteIcon(const graphics::Bitmap& bitmap, PointerIconStyle style, float hotSpotX, + float hotSpotY) : bitmap(bitmap), style(style), hotSpotX(hotSpotX), hotSpotY(hotSpotY) {} graphics::Bitmap bitmap; - int32_t style; + PointerIconStyle style; float hotSpotX; float hotSpotY; @@ -41,7 +43,7 @@ struct SpriteIcon { inline void reset() { bitmap.reset(); - style = 0; + style = PointerIconStyle::TYPE_NULL; hotSpotX = 0; hotSpotY = 0; } diff --git a/libs/input/TouchSpotController.cpp b/libs/input/TouchSpotController.cpp index f7c685ff8ba6..4ac66c4ffb6a 100644 --- a/libs/input/TouchSpotController.cpp +++ b/libs/input/TouchSpotController.cpp @@ -23,12 +23,6 @@ #include <log/log.h> -#include <SkBitmap.h> -#include <SkBlendMode.h> -#include <SkCanvas.h> -#include <SkColor.h> -#include <SkPaint.h> - namespace { // Time to spend fading out the spot completely. const nsecs_t SPOT_FADE_DURATION = 200 * 1000000LL; // 200 ms diff --git a/libs/input/tests/PointerController_test.cpp b/libs/input/tests/PointerController_test.cpp index f9752ed155df..a6a4115476df 100644 --- a/libs/input/tests/PointerController_test.cpp +++ b/libs/input/tests/PointerController_test.cpp @@ -14,17 +14,18 @@ * limitations under the License. */ -#include "mocks/MockSprite.h" -#include "mocks/MockSpriteController.h" - +#include <gmock/gmock.h> +#include <gtest/gtest.h> #include <input/PointerController.h> #include <input/SpriteController.h> #include <atomic> -#include <gmock/gmock.h> -#include <gtest/gtest.h> #include <thread> +#include "input/Input.h" +#include "mocks/MockSprite.h" +#include "mocks/MockSpriteController.h" + namespace android { enum TestCursorType { @@ -39,7 +40,6 @@ enum TestCursorType { using ::testing::AllOf; using ::testing::Field; -using ::testing::Mock; using ::testing::NiceMock; using ::testing::Return; using ::testing::Test; @@ -52,10 +52,12 @@ class MockPointerControllerPolicyInterface : public PointerControllerPolicyInter public: virtual void loadPointerIcon(SpriteIcon* icon, int32_t displayId) override; virtual void loadPointerResources(PointerResources* outResources, int32_t displayId) override; - virtual void loadAdditionalMouseResources(std::map<int32_t, SpriteIcon>* outResources, - std::map<int32_t, PointerAnimation>* outAnimationResources, int32_t displayId) override; - virtual int32_t getDefaultPointerIconId() override; - virtual int32_t getCustomPointerIconId() override; + virtual void loadAdditionalMouseResources( + std::map<PointerIconStyle, SpriteIcon>* outResources, + std::map<PointerIconStyle, PointerAnimation>* outAnimationResources, + int32_t displayId) override; + virtual PointerIconStyle getDefaultPointerIconId() override; + virtual PointerIconStyle getCustomPointerIconId() override; virtual void onPointerDisplayIdChanged(int32_t displayId, float xPos, float yPos) override; bool allResourcesAreLoaded(); @@ -85,34 +87,33 @@ void MockPointerControllerPolicyInterface::loadPointerResources(PointerResources } void MockPointerControllerPolicyInterface::loadAdditionalMouseResources( - std::map<int32_t, SpriteIcon>* outResources, - std::map<int32_t, PointerAnimation>* outAnimationResources, - int32_t) { + std::map<PointerIconStyle, SpriteIcon>* outResources, + std::map<PointerIconStyle, PointerAnimation>* outAnimationResources, int32_t) { SpriteIcon icon; PointerAnimation anim; // CURSOR_TYPE_ADDITIONAL doesn't have animation resource. int32_t cursorType = CURSOR_TYPE_ADDITIONAL; loadPointerIconForType(&icon, cursorType); - (*outResources)[cursorType] = icon; + (*outResources)[static_cast<PointerIconStyle>(cursorType)] = icon; // CURSOR_TYPE_ADDITIONAL_ANIM has animation resource. cursorType = CURSOR_TYPE_ADDITIONAL_ANIM; loadPointerIconForType(&icon, cursorType); anim.animationFrames.push_back(icon); anim.durationPerFrame = 10; - (*outResources)[cursorType] = icon; - (*outAnimationResources)[cursorType] = anim; + (*outResources)[static_cast<PointerIconStyle>(cursorType)] = icon; + (*outAnimationResources)[static_cast<PointerIconStyle>(cursorType)] = anim; additionalMouseResourcesLoaded = true; } -int32_t MockPointerControllerPolicyInterface::getDefaultPointerIconId() { - return CURSOR_TYPE_DEFAULT; +PointerIconStyle MockPointerControllerPolicyInterface::getDefaultPointerIconId() { + return static_cast<PointerIconStyle>(CURSOR_TYPE_DEFAULT); } -int32_t MockPointerControllerPolicyInterface::getCustomPointerIconId() { - return CURSOR_TYPE_CUSTOM; +PointerIconStyle MockPointerControllerPolicyInterface::getCustomPointerIconId() { + return static_cast<PointerIconStyle>(CURSOR_TYPE_CUSTOM); } bool MockPointerControllerPolicyInterface::allResourcesAreLoaded() { @@ -124,7 +125,7 @@ bool MockPointerControllerPolicyInterface::noResourcesAreLoaded() { } void MockPointerControllerPolicyInterface::loadPointerIconForType(SpriteIcon* icon, int32_t type) { - icon->style = type; + icon->style = static_cast<PointerIconStyle>(type); std::pair<float, float> hotSpot = getHotSpotCoordinatesForType(type); icon->hotSpotX = hotSpot.first; icon->hotSpotY = hotSpot.second; @@ -205,11 +206,11 @@ TEST_F(PointerControllerTest, useDefaultCursorTypeByDefault) { std::pair<float, float> hotspot = getHotSpotCoordinatesForType(CURSOR_TYPE_DEFAULT); EXPECT_CALL(*mPointerSprite, setVisible(true)); EXPECT_CALL(*mPointerSprite, setAlpha(1.0f)); - EXPECT_CALL(*mPointerSprite, setIcon( - AllOf( - Field(&SpriteIcon::style, CURSOR_TYPE_DEFAULT), - Field(&SpriteIcon::hotSpotX, hotspot.first), - Field(&SpriteIcon::hotSpotY, hotspot.second)))); + EXPECT_CALL(*mPointerSprite, + setIcon(AllOf(Field(&SpriteIcon::style, + static_cast<PointerIconStyle>(CURSOR_TYPE_DEFAULT)), + Field(&SpriteIcon::hotSpotX, hotspot.first), + Field(&SpriteIcon::hotSpotY, hotspot.second)))); mPointerController->reloadPointerResources(); } @@ -222,12 +223,11 @@ TEST_F(PointerControllerTest, updatePointerIcon) { std::pair<float, float> hotspot = getHotSpotCoordinatesForType(type); EXPECT_CALL(*mPointerSprite, setVisible(true)); EXPECT_CALL(*mPointerSprite, setAlpha(1.0f)); - EXPECT_CALL(*mPointerSprite, setIcon( - AllOf( - Field(&SpriteIcon::style, type), - Field(&SpriteIcon::hotSpotX, hotspot.first), - Field(&SpriteIcon::hotSpotY, hotspot.second)))); - mPointerController->updatePointerIcon(type); + EXPECT_CALL(*mPointerSprite, + setIcon(AllOf(Field(&SpriteIcon::style, static_cast<PointerIconStyle>(type)), + Field(&SpriteIcon::hotSpotX, hotspot.first), + Field(&SpriteIcon::hotSpotY, hotspot.second)))); + mPointerController->updatePointerIcon(static_cast<PointerIconStyle>(type)); } TEST_F(PointerControllerTest, setCustomPointerIcon) { @@ -239,17 +239,16 @@ TEST_F(PointerControllerTest, setCustomPointerIcon) { float hotSpotY = 20; SpriteIcon icon; - icon.style = style; + icon.style = static_cast<PointerIconStyle>(style); icon.hotSpotX = hotSpotX; icon.hotSpotY = hotSpotY; EXPECT_CALL(*mPointerSprite, setVisible(true)); EXPECT_CALL(*mPointerSprite, setAlpha(1.0f)); - EXPECT_CALL(*mPointerSprite, setIcon( - AllOf( - Field(&SpriteIcon::style, style), - Field(&SpriteIcon::hotSpotX, hotSpotX), - Field(&SpriteIcon::hotSpotY, hotSpotY)))); + EXPECT_CALL(*mPointerSprite, + setIcon(AllOf(Field(&SpriteIcon::style, static_cast<PointerIconStyle>(style)), + Field(&SpriteIcon::hotSpotX, hotSpotX), + Field(&SpriteIcon::hotSpotY, hotSpotY)))); mPointerController->setCustomPointerIcon(icon); } |