diff options
Diffstat (limited to 'libs')
1175 files changed, 81042 insertions, 35541 deletions
diff --git a/libs/WindowManager/Jetpack/Android.bp b/libs/WindowManager/Jetpack/Android.bp index dc4b5636a246..a5b192cd7ceb 100644 --- a/libs/WindowManager/Jetpack/Android.bp +++ b/libs/WindowManager/Jetpack/Android.bp @@ -63,6 +63,12 @@ android_library_import { sdk_version: "current", } +android_library_import { + name: "window-extensions-core", + aars: ["window-extensions-core-release.aar"], + sdk_version: "current", +} + java_library { name: "androidx.window.extensions", srcs: [ @@ -70,7 +76,10 @@ java_library { "src/androidx/window/util/**/*.java", "src/androidx/window/common/**/*.java", ], - static_libs: ["window-extensions"], + static_libs: [ + "window-extensions", + "window-extensions-core", + ], installable: true, sdk_version: "core_platform", system_ext_specific: true, diff --git a/libs/WindowManager/Jetpack/src/androidx/window/common/CommonFoldingFeature.java b/libs/WindowManager/Jetpack/src/androidx/window/common/CommonFoldingFeature.java index 921552b6cfbb..65955b1d9bcc 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/common/CommonFoldingFeature.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/common/CommonFoldingFeature.java @@ -21,6 +21,7 @@ import static androidx.window.util.ExtensionHelper.isZero; import android.annotation.IntDef; import android.annotation.Nullable; import android.graphics.Rect; +import android.hardware.devicestate.DeviceStateManager; import android.util.Log; import androidx.annotation.NonNull; @@ -33,7 +34,8 @@ import java.util.Objects; import java.util.regex.Matcher; import java.util.regex.Pattern; -/** A representation of a folding feature for both Extension and Sidecar. +/** + * A representation of a folding feature for both Extension and Sidecar. * For Sidecar this is the same as combining {@link androidx.window.sidecar.SidecarDeviceState} and * {@link androidx.window.sidecar.SidecarDisplayFeature}. For Extensions this is the mirror of * {@link androidx.window.extensions.layout.FoldingFeature}. @@ -67,10 +69,11 @@ public final class CommonFoldingFeature { public static final int COMMON_STATE_UNKNOWN = -1; /** - * A common state to represent a FLAT hinge. This is needed because the definitions in Sidecar - * and Extensions do not match exactly. + * A common state that contains no folding features. For example, an in-folding device in the + * "closed" device state. */ - public static final int COMMON_STATE_FLAT = 3; + public static final int COMMON_STATE_NO_FOLDING_FEATURES = 1; + /** * A common state to represent a HALF_OPENED hinge. This is needed because the definitions in * Sidecar and Extensions do not match exactly. @@ -78,9 +81,27 @@ public final class CommonFoldingFeature { public static final int COMMON_STATE_HALF_OPENED = 2; /** - * The possible states for a folding hinge. + * A common state to represent a FLAT hinge. This is needed because the definitions in Sidecar + * and Extensions do not match exactly. + */ + public static final int COMMON_STATE_FLAT = 3; + + /** + * A common state where the hinge state should be derived using the base state from + * {@link DeviceStateManager.DeviceStateCallback#onBaseStateChanged(int)} instead of the + * emulated state. This is an internal state and must not be passed to clients. */ - @IntDef({COMMON_STATE_UNKNOWN, COMMON_STATE_FLAT, COMMON_STATE_HALF_OPENED}) + public static final int COMMON_STATE_USE_BASE_STATE = 1000; + + /** + * The possible states for a folding hinge. Common in this context means normalized between + * extensions and sidecar. + */ + @IntDef({COMMON_STATE_UNKNOWN, + COMMON_STATE_NO_FOLDING_FEATURES, + COMMON_STATE_HALF_OPENED, + COMMON_STATE_FLAT, + COMMON_STATE_USE_BASE_STATE}) @Retention(RetentionPolicy.SOURCE) public @interface State { } @@ -167,7 +188,7 @@ public final class CommonFoldingFeature { } String stateString = featureMatcher.group(6); stateString = stateString == null ? "" : stateString; - final int state; + @State final int state; switch (stateString) { case PATTERN_STATE_FLAT: state = COMMON_STATE_FLAT; @@ -191,15 +212,15 @@ public final class CommonFoldingFeature { @NonNull private final Rect mRect; - CommonFoldingFeature(int type, int state, @NonNull Rect rect) { - assertValidState(state); + CommonFoldingFeature(int type, @State int state, @NonNull Rect rect) { + assertReportableState(state); this.mType = type; this.mState = state; if (rect.width() == 0 && rect.height() == 0) { 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 +238,7 @@ public final class CommonFoldingFeature { /** Returns the bounds of the feature. */ @NonNull public Rect getRect() { - return mRect; + return new Rect(mRect); } @Override @@ -231,13 +252,22 @@ public final class CommonFoldingFeature { } @Override + public String toString() { + return "CommonFoldingFeature=[Type: " + mType + ", state: " + mState + "]"; + } + + @Override public int hashCode() { return Objects.hash(mType, mState, mRect); } - private static void assertValidState(@Nullable Integer state) { - if (state != null && state != COMMON_STATE_FLAT - && state != COMMON_STATE_HALF_OPENED && state != COMMON_STATE_UNKNOWN) { + /** + * Checks if the provided folding feature state should be reported to clients. See + * {@link androidx.window.extensions.layout.FoldingFeature} + */ + private static void assertReportableState(@State int state) { + if (state != COMMON_STATE_FLAT && state != COMMON_STATE_HALF_OPENED + && state != COMMON_STATE_UNKNOWN) { throw new IllegalArgumentException("Invalid state: " + state + "must be either COMMON_STATE_FLAT or COMMON_STATE_HALF_OPENED"); } diff --git a/libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java b/libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java index fdcb7be597d5..66f27f517ab3 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java @@ -19,10 +19,10 @@ package androidx.window.common; import static android.hardware.devicestate.DeviceStateManager.INVALID_DEVICE_STATE; import static androidx.window.common.CommonFoldingFeature.COMMON_STATE_UNKNOWN; +import static androidx.window.common.CommonFoldingFeature.COMMON_STATE_USE_BASE_STATE; 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,39 +30,79 @@ 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; + /** + * Emulated device state {@link DeviceStateManager.DeviceStateCallback#onStateChanged(int)} to + * {@link CommonFoldingFeature.State} map. + */ private final SparseIntArray mDeviceStateToPostureMap = new SparseIntArray(); + /** + * Emulated device state received via + * {@link DeviceStateManager.DeviceStateCallback#onStateChanged(int)}. + * "Emulated" states differ from "base" state in the sense that they may not correspond 1:1 with + * physical device states. They represent the state of the device when various software + * features and APIs are applied. The emulated states generally consist of all "base" states, + * but may have additional states such as "concurrent" or "rear display". Concurrent mode for + * example is activated via public API and can be active in both the "open" and "half folded" + * device states. + */ private int mCurrentDeviceState = INVALID_DEVICE_STATE; - private final DeviceStateCallback mDeviceStateCallback = (state) -> { - mCurrentDeviceState = state; - notifyDataChanged(); - }; + /** + * Base device state received via + * {@link DeviceStateManager.DeviceStateCallback#onBaseStateChanged(int)}. + * "Base" in this context means the "physical" state of the device. + */ + private int mCurrentBaseDeviceState = INVALID_DEVICE_STATE; + @NonNull - private final DataProducer<String> mRawFoldSupplier; + private final BaseDataProducer<String> mRawFoldSupplier; + + private final DeviceStateCallback mDeviceStateCallback = new DeviceStateCallback() { + @Override + public void onStateChanged(int state) { + mCurrentDeviceState = state; + mRawFoldSupplier.getData(DeviceStateManagerFoldingFeatureProducer + .this::notifyFoldingFeatureChange); + } + + @Override + public void onBaseStateChanged(int state) { + mCurrentBaseDeviceState = state; + + if (mDeviceStateToPostureMap.get(mCurrentDeviceState) + == COMMON_STATE_USE_BASE_STATE) { + mRawFoldSupplier.getData(DeviceStateManagerFoldingFeatureProducer + .this::notifyFoldingFeatureChange); + } + } + }; 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 +110,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 +123,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,33 +134,100 @@ public final class DeviceStateManagerFoldingFeatureProducer extends } if (mDeviceStateToPostureMap.size() > 0) { - context.getSystemService(DeviceStateManager.class) + Objects.requireNonNull(context.getSystemService(DeviceStateManager.class)) .registerCallback(context.getMainExecutor(), mDeviceStateCallback); } } - @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 { - mRawFoldSupplier.addDataChangedCallback(this::notifyDataChanged); + return displayFeaturesString.map(this::calculateFoldingFeature); } } - private int globalHingeState() { - return mDeviceStateToPostureMap.get(mCurrentDeviceState, COMMON_STATE_UNKNOWN); + /** + * Adds the data to the storeFeaturesConsumer when the data is ready. + * @param storeFeaturesConsumer a consumer to collect the data when it is first available. + */ + @Override + 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 { + notifyDataChanged(calculateFoldingFeature(displayFeaturesString)); + } + } + + private List<CommonFoldingFeature> calculateFoldingFeature(String displayFeaturesString) { + return parseListFromString(displayFeaturesString, currentHingeState()); + } + + @CommonFoldingFeature.State + private int currentHingeState() { + @CommonFoldingFeature.State + int posture = mDeviceStateToPostureMap.get(mCurrentDeviceState, COMMON_STATE_UNKNOWN); + + if (posture == CommonFoldingFeature.COMMON_STATE_USE_BASE_STATE) { + posture = mDeviceStateToPostureMap.get(mCurrentBaseDeviceState, COMMON_STATE_UNKNOWN); + } + + return posture; } } 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..76e0e1eb7a95 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java @@ -17,26 +17,81 @@ package androidx.window.extensions; import android.app.ActivityThread; +import android.app.Application; import android.content.Context; +import android.window.TaskFragmentOrganizer; import androidx.annotation.NonNull; +import androidx.window.common.DeviceStateManagerFoldingFeatureProducer; +import androidx.window.common.RawFoldingFeatureProducer; +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; +import java.util.Objects; + + /** * The reference implementation of {@link WindowExtensions} that implements the initial API version. */ public class WindowExtensionsImpl implements WindowExtensions { private final Object mLock = new Object(); - private volatile WindowLayoutComponent mWindowLayoutComponent; + private volatile DeviceStateManagerFoldingFeatureProducer mFoldingFeatureProducer; + private volatile WindowLayoutComponentImpl 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 3; + } + + @NonNull + private Application getApplication() { + return Objects.requireNonNull(ActivityThread.currentApplication()); + } + + @NonNull + private DeviceStateManagerFoldingFeatureProducer getFoldingFeatureProducer() { + if (mFoldingFeatureProducer == null) { + synchronized (mLock) { + if (mFoldingFeatureProducer == null) { + Context context = getApplication(); + RawFoldingFeatureProducer foldingFeatureProducer = + new RawFoldingFeatureProducer(context); + mFoldingFeatureProducer = + new DeviceStateManagerFoldingFeatureProducer(context, + foldingFeatureProducer); + } + } + } + return mFoldingFeatureProducer; + } + + @NonNull + private WindowLayoutComponentImpl getWindowLayoutComponentImpl() { + if (mWindowLayoutComponent == null) { + synchronized (mLock) { + if (mWindowLayoutComponent == null) { + Context context = getApplication(); + DeviceStateManagerFoldingFeatureProducer producer = + getFoldingFeatureProducer(); + // TODO(b/263263909) Use the organizer to tell if an Activity is embededed. + // Need to improve our Dependency Injection and centralize the logic. + TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(command -> { + throw new RuntimeException("Not allowed!"); + }); + mWindowLayoutComponent = new WindowLayoutComponentImpl(context, organizer, + producer); + } + } + } + return mWindowLayoutComponent; } /** @@ -47,15 +102,7 @@ public class WindowExtensionsImpl implements WindowExtensions { */ @Override public WindowLayoutComponent getWindowLayoutComponent() { - if (mWindowLayoutComponent == null) { - synchronized (mLock) { - if (mWindowLayoutComponent == null) { - Context context = ActivityThread.currentApplication(); - mWindowLayoutComponent = new WindowLayoutComponentImpl(context); - } - } - } - return mWindowLayoutComponent; + return getWindowLayoutComponentImpl(); } /** @@ -69,10 +116,32 @@ public class WindowExtensionsImpl implements WindowExtensions { if (mSplitController == null) { synchronized (mLock) { if (mSplitController == null) { - mSplitController = new SplitController(); + mSplitController = new SplitController( + getWindowLayoutComponentImpl(), + getFoldingFeatureProducer() + ); } } } 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/RearDisplayPresentation.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/RearDisplayPresentation.java new file mode 100644 index 000000000000..1ff169433b9d --- /dev/null +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/RearDisplayPresentation.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.window.extensions.area; + +import android.app.Presentation; +import android.content.Context; +import android.view.Display; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.window.extensions.core.util.function.Consumer; + +/** + * {@link Presentation} object that is used to present extra content + * on the rear facing display when in a rear display presentation feature. + */ +class RearDisplayPresentation extends Presentation implements ExtensionWindowAreaPresentation { + + @NonNull + private final Consumer<@WindowAreaComponent.WindowAreaSessionState Integer> mStateConsumer; + + RearDisplayPresentation(@NonNull Context outerContext, @NonNull Display display, + @NonNull Consumer<@WindowAreaComponent.WindowAreaSessionState Integer> stateConsumer) { + super(outerContext, display); + mStateConsumer = stateConsumer; + } + + /** + * {@code mStateConsumer} is notified that their content is now visible when the + * {@link Presentation} object is started. There is no comparable callback for + * {@link WindowAreaComponent#SESSION_STATE_INVISIBLE} in {@link #onStop()} due to the + * timing of when a {@link android.hardware.devicestate.DeviceStateRequest} is cancelled + * ending rear display presentation mode happening before the {@link Presentation} is stopped. + */ + @Override + protected void onStart() { + super.onStart(); + mStateConsumer.accept(WindowAreaComponent.SESSION_STATE_VISIBLE); + } + + @NonNull + @Override + public Context getPresentationContext() { + return getContext(); + } + + @Override + public void setPresentationView(View view) { + setContentView(view); + if (!isShowing()) { + show(); + } + } +} diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/RearDisplayPresentationController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/RearDisplayPresentationController.java new file mode 100644 index 000000000000..141a6ad48771 --- /dev/null +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/RearDisplayPresentationController.java @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.window.extensions.area; + +import static androidx.window.extensions.area.WindowAreaComponent.SESSION_STATE_ACTIVE; +import static androidx.window.extensions.area.WindowAreaComponent.SESSION_STATE_INACTIVE; + +import android.content.Context; +import android.hardware.devicestate.DeviceStateRequest; +import android.hardware.display.DisplayManager; +import android.util.Log; +import android.view.Display; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.window.extensions.core.util.function.Consumer; + +import java.util.Objects; + +/** + * Controller class that keeps track of the status of the device state request + * to enable the rear display presentation feature. This controller notifies the session callback + * when the state request is active, and notifies the callback when the request is canceled. + * + * Clients are notified via {@link Consumer} provided with + * {@link androidx.window.extensions.area.WindowAreaComponent.WindowAreaStatus} values to signify + * when the request becomes active and cancelled. + */ +class RearDisplayPresentationController implements DeviceStateRequest.Callback { + + private static final String TAG = "RearDisplayPresentationController"; + + // Original context that requested to enable rear display presentation mode + @NonNull + private final Context mContext; + @NonNull + private final Consumer<@WindowAreaComponent.WindowAreaSessionState Integer> mStateConsumer; + @Nullable + private ExtensionWindowAreaPresentation mExtensionWindowAreaPresentation; + @NonNull + private final DisplayManager mDisplayManager; + + /** + * Creates the RearDisplayPresentationController + * @param context Originating {@link android.content.Context} that is initiating the rear + * display presentation session. + * @param stateConsumer {@link Consumer} that will be notified that the session is active when + * the device state request is active and the session has been created. If the device + * state request is cancelled, the callback will be notified that the session has been + * ended. This could occur through a call to cancel the feature or if the device is + * manipulated in a way that cancels any device state override. + */ + RearDisplayPresentationController(@NonNull Context context, + @NonNull Consumer<@WindowAreaComponent.WindowAreaSessionState Integer> stateConsumer) { + Objects.requireNonNull(context); + Objects.requireNonNull(stateConsumer); + + mContext = context; + mStateConsumer = stateConsumer; + mDisplayManager = context.getSystemService(DisplayManager.class); + } + + @Override + public void onRequestActivated(@NonNull DeviceStateRequest request) { + Display[] rearDisplays = mDisplayManager.getDisplays(DisplayManager.DISPLAY_CATEGORY_REAR); + if (rearDisplays.length == 0) { + mStateConsumer.accept(SESSION_STATE_INACTIVE); + Log.e(TAG, "Rear display list should not be empty"); + return; + } + + mExtensionWindowAreaPresentation = + new RearDisplayPresentation(mContext, rearDisplays[0], mStateConsumer); + mStateConsumer.accept(SESSION_STATE_ACTIVE); + } + + @Override + public void onRequestCanceled(@NonNull DeviceStateRequest request) { + mStateConsumer.accept(SESSION_STATE_INACTIVE); + } + + @Nullable + public ExtensionWindowAreaPresentation getWindowAreaPresentation() { + return mExtensionWindowAreaPresentation; + } +} diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/RearDisplayPresentationStatus.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/RearDisplayPresentationStatus.java new file mode 100644 index 000000000000..0b1423ae48c0 --- /dev/null +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/RearDisplayPresentationStatus.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.window.extensions.area; + +import android.util.DisplayMetrics; + +import androidx.annotation.NonNull; + +/** + * Class that provides information around the current status of a window area feature. Contains + * the current {@link WindowAreaComponent.WindowAreaStatus} value corresponding to the + * rear display presentation feature, as well as the {@link DisplayMetrics} for the rear facing + * display. + */ +class RearDisplayPresentationStatus implements ExtensionWindowAreaStatus { + + @WindowAreaComponent.WindowAreaStatus + private final int mWindowAreaStatus; + + @NonNull + private final DisplayMetrics mDisplayMetrics; + + RearDisplayPresentationStatus(@WindowAreaComponent.WindowAreaStatus int status, + @NonNull DisplayMetrics displayMetrics) { + mWindowAreaStatus = status; + mDisplayMetrics = displayMetrics; + } + + /** + * Returns the {@link androidx.window.extensions.area.WindowAreaComponent.WindowAreaStatus} + * value that relates to the current status of a feature. + */ + @Override + @WindowAreaComponent.WindowAreaStatus + public int getWindowAreaStatus() { + return mWindowAreaStatus; + } + + /** + * Returns the {@link DisplayMetrics} that corresponds to the window area that a feature + * interacts with. This is converted to size class information provided to developers. + */ + @Override + @NonNull + public DisplayMetrics getWindowAreaDisplayMetrics() { + return mDisplayMetrics; + } +} 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..575b0cea78f7 --- /dev/null +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/WindowAreaComponentImpl.java @@ -0,0 +1,557 @@ +/* + * 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.hardware.display.DisplayManager; +import android.util.ArraySet; +import android.util.DisplayMetrics; +import android.util.Pair; +import android.view.Display; +import android.view.DisplayAddress; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.window.extensions.core.util.function.Consumer; + +import com.android.internal.R; +import com.android.internal.annotations.GuardedBy; +import com.android.internal.util.ArrayUtils; + +import java.util.concurrent.Executor; + +/** + * 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(); + + @NonNull + private final DeviceStateManager mDeviceStateManager; + @NonNull + private final DisplayManager mDisplayManager; + @NonNull + private final Executor mExecutor; + + @GuardedBy("mLock") + private final ArraySet<Consumer<Integer>> mRearDisplayStatusListeners = new ArraySet<>(); + @GuardedBy("mLock") + private final ArraySet<Consumer<ExtensionWindowAreaStatus>> + mRearDisplayPresentationStatusListeners = new ArraySet<>(); + private final int mRearDisplayState; + private final int mConcurrentDisplayState; + @NonNull + private final int[] mFoldedDeviceStates; + @NonNull + private long mRearDisplayAddress = 0; + @WindowAreaSessionState + private int mRearDisplaySessionStatus = WindowAreaComponent.SESSION_STATE_INACTIVE; + + @GuardedBy("mLock") + private int mCurrentDeviceState = INVALID_DEVICE_STATE; + @GuardedBy("mLock") + private int[] mCurrentSupportedDeviceStates; + + @GuardedBy("mLock") + private DeviceStateRequest mRearDisplayStateRequest; + @GuardedBy("mLock") + private RearDisplayPresentationController mRearDisplayPresentationController; + + @Nullable + @GuardedBy("mLock") + private DisplayMetrics mRearDisplayMetrics; + + @WindowAreaSessionState + @GuardedBy("mLock") + private int mLastReportedRearDisplayPresentationStatus; + + public WindowAreaComponentImpl(@NonNull Context context) { + mDeviceStateManager = context.getSystemService(DeviceStateManager.class); + mDisplayManager = context.getSystemService(DisplayManager.class); + mExecutor = context.getMainExecutor(); + + mCurrentSupportedDeviceStates = mDeviceStateManager.getSupportedStates(); + mFoldedDeviceStates = context.getResources().getIntArray( + R.array.config_foldedDeviceStates); + + // TODO(b/236022708) Move rear display state to device state config file + mRearDisplayState = context.getResources().getInteger( + R.integer.config_deviceStateRearDisplay); + + mConcurrentDisplayState = context.getResources().getInteger( + R.integer.config_deviceStateConcurrentRearDisplay); + + mDeviceStateManager.registerCallback(mExecutor, this); + if (mConcurrentDisplayState != INVALID_DEVICE_STATE) { + mRearDisplayAddress = Long.parseLong(context.getResources().getString( + R.string.config_rearDisplayPhysicalAddress)); + } + } + + /** + * Adds a listener interested in receiving updates on the RearDisplayStatus + * of the device. Because this is being called from the OEM provided + * extensions, the result of the listener will be posted on the executor + * provided by the developer at the initial call site. + * + * Rear display mode moves the calling application to the display on the device that is + * facing the same direction as the rear cameras. This would be the cover display on a fold-in + * style device when the device is opened. + * + * Depending on the initial state of the device, the {@link Consumer} will receive 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, the status is updated 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. + */ + @Override + public void addRearDisplayStatusListener( + @NonNull Consumer<@WindowAreaStatus Integer> consumer) { + synchronized (mLock) { + mRearDisplayStatusListeners.add(consumer); + + // If current device state is still invalid, the initial value has not been provided. + if (mCurrentDeviceState == INVALID_DEVICE_STATE) { + return; + } + consumer.accept(getCurrentRearDisplayModeStatus()); + } + } + + /** + * Removes a listener no longer interested in receiving updates. + * @param consumer no longer interested in receiving updates to RearDisplayStatus + */ + @Override + 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, the result of the listener will be posted on the executor + * provided by the developer at the initial call site. + * + * Rear display mode moves the calling application to the display on the device that is + * facing the same direction as the rear cameras. This would be the cover display on a fold-in + * style device when the device is opened. + * + * When rear display mode is enabled, a request is made to {@link DeviceStateManager} + * to override the device state to the state that corresponds to RearDisplay + * mode. When the {@link DeviceStateRequest} is activated, the provided {@link Consumer} is + * notified that the session is active by receiving + * {@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 + */ + @Override + public void startRearDisplaySession(@NonNull Activity activity, + @NonNull Consumer<@WindowAreaSessionState Integer> rearDisplaySessionCallback) { + synchronized (mLock) { + if (mRearDisplayStateRequest != null) { + // Rear display session is already active + throw new IllegalStateException( + "Unable to start new rear display session as one is already active"); + } + mRearDisplayStateRequest = DeviceStateRequest.newBuilder(mRearDisplayState).build(); + mDeviceStateManager.requestState( + mRearDisplayStateRequest, + mExecutor, + new RearDisplayStateRequestCallbackAdapter(rearDisplaySessionCallback) + ); + } + } + + /** + * Ends the current rear display session and provides updates to the + * callback provided. Because this is being called from the OEM provided + * extensions, the result of the listener will be posted on the executor + * provided by the developer at the initial call site. + */ + @Override + public void endRearDisplaySession() { + synchronized (mLock) { + if (mRearDisplayStateRequest != null || isRearDisplayActive()) { + mRearDisplayStateRequest = null; + mDeviceStateManager.cancelStateRequest(); + } else { + throw new IllegalStateException( + "Unable to cancel a rear display session as there is no active session"); + } + } + } + + /** + * Adds a listener interested in receiving updates on the RearDisplayPresentationStatus + * of the device. Because this is being called from the OEM provided + * extensions, the result of the listener will be posted on the executor + * provided by the developer at the initial call site. + * + * Rear display presentation mode is a feature where an {@link Activity} can present + * additional content on a device with a second display that is facing the same direction + * as the rear camera (i.e. the cover display on a fold-in style device). The calling + * {@link Activity} does not move, whereas in rear display mode it does. + * + * This listener receives a {@link Pair} with the first item being the + * {@link WindowAreaComponent.WindowAreaStatus} that corresponds to the current status of the + * feature, and the second being the {@link DisplayMetrics} of the display that would be + * presented to when the feature is active. + * + * Depending on the initial state of the device, the {@link Consumer} will receive either + * {@link WindowAreaComponent#STATUS_AVAILABLE} or + * {@link WindowAreaComponent#STATUS_UNAVAILABLE} for the status value of the {@link Pair} if + * the feature is supported or not in that state respectively. Rear display presentation mode is + * currently not supported when the device is folded. When the rear display presentation feature + * is triggered, the status is updated 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 presentation mode. + */ + @Override + public void addRearDisplayPresentationStatusListener( + @NonNull Consumer<ExtensionWindowAreaStatus> consumer) { + synchronized (mLock) { + mRearDisplayPresentationStatusListeners.add(consumer); + + // If current device state is still invalid, the initial value has not been provided + if (mCurrentDeviceState == INVALID_DEVICE_STATE) { + return; + } + @WindowAreaStatus int currentStatus = getCurrentRearDisplayPresentationModeStatus(); + DisplayMetrics metrics = + currentStatus == STATUS_UNSUPPORTED ? null : getRearDisplayMetrics(); + consumer.accept( + new RearDisplayPresentationStatus(currentStatus, metrics)); + } + } + + /** + * Removes a listener no longer interested in receiving updates. + * @param consumer no longer interested in receiving updates to RearDisplayPresentationStatus + */ + @Override + public void removeRearDisplayPresentationStatusListener( + @NonNull Consumer<ExtensionWindowAreaStatus> consumer) { + synchronized (mLock) { + mRearDisplayPresentationStatusListeners.remove(consumer); + } + } + + /** + * Creates and starts a rear display presentation session and sends state updates to the + * consumer provided. This consumer will receive a constant represented by + * {@link WindowAreaSessionState} to represent the state of the current rear display + * session. It will be translated to a more friendly interface in the library. + * + * Because this is being called from the OEM provided extensions, the library + * will post the result of the listener on the executor provided by the developer. + * + * Rear display presentation mode refers to a feature where an {@link Activity} can present + * additional content on a device with a second display that is facing the same direction + * as the rear camera (i.e. the cover display on a fold-in style device). The calling + * {@link Activity} stays on the user-facing display. + * + * @param activity that the OEM implementation will use as a base + * context and to identify the source display area of the request. + * The reference to the activity instance must not be stored in the OEM + * implementation to prevent memory leaks. + * @param consumer to provide updates to the client on the status of the session + * @throws UnsupportedOperationException if this method is called when rear display presentation + * mode is not available. This could be to an incompatible device state or when + * another process is currently in this mode. + */ + @Override + public void startRearDisplayPresentationSession(@NonNull Activity activity, + @NonNull Consumer<@WindowAreaSessionState Integer> consumer) { + synchronized (mLock) { + if (mRearDisplayPresentationController != null) { + // Rear display presentation session is already active + throw new IllegalStateException( + "Unable to start new rear display presentation session as one is already " + + "active"); + } + if (getCurrentRearDisplayPresentationModeStatus() + != WindowAreaComponent.STATUS_AVAILABLE) { + throw new IllegalStateException( + "Unable to start new rear display presentation session as the feature is " + + "is not currently available"); + } + + mRearDisplayPresentationController = new RearDisplayPresentationController(activity, + stateStatus -> { + synchronized (mLock) { + if (stateStatus == SESSION_STATE_INACTIVE) { + // If the last reported session status was VISIBLE + // then the INVISIBLE state should be dispatched before INACTIVE + // due to not having a good mechanism to know when + // the content is no longer visible before it's fully removed + if (getLastReportedRearDisplayPresentationStatus() + == SESSION_STATE_VISIBLE) { + consumer.accept(SESSION_STATE_INVISIBLE); + } + mRearDisplayPresentationController = null; + } + mLastReportedRearDisplayPresentationStatus = stateStatus; + consumer.accept(stateStatus); + } + }); + + DeviceStateRequest concurrentDisplayStateRequest = DeviceStateRequest.newBuilder( + mConcurrentDisplayState).build(); + mDeviceStateManager.requestState( + concurrentDisplayStateRequest, + mExecutor, + mRearDisplayPresentationController + ); + } + } + + /** + * Ends the current rear display presentation session and provides updates to the + * callback provided. When this is ended, the presented content from the calling + * {@link Activity} will also be removed from the rear facing display. + * Because this is being called from the OEM provided extensions, the result of the listener + * will be posted on the executor provided by the developer at the initial call site. + * + * Cancelling the {@link DeviceStateRequest} and exiting the rear display presentation state, + * will remove the presentation window from the cover display as the cover display is no longer + * enabled. + */ + @Override + public void endRearDisplayPresentationSession() { + synchronized (mLock) { + if (mRearDisplayPresentationController != null) { + mDeviceStateManager.cancelStateRequest(); + } else { + throw new IllegalStateException( + "Unable to cancel a rear display presentation session as there is no " + + "active session"); + } + } + } + + @Nullable + @Override + public ExtensionWindowAreaPresentation getRearDisplayPresentation() { + synchronized (mLock) { + ExtensionWindowAreaPresentation presentation = null; + if (mRearDisplayPresentationController != null) { + presentation = mRearDisplayPresentationController.getWindowAreaPresentation(); + } + return presentation; + } + } + + @Override + public void onSupportedStatesChanged(int[] supportedStates) { + synchronized (mLock) { + mCurrentSupportedDeviceStates = supportedStates; + updateRearDisplayStatusListeners(getCurrentRearDisplayModeStatus()); + updateRearDisplayPresentationStatusListeners( + getCurrentRearDisplayPresentationModeStatus()); + } + } + + @Override + public void onStateChanged(int state) { + synchronized (mLock) { + mCurrentDeviceState = state; + updateRearDisplayStatusListeners(getCurrentRearDisplayModeStatus()); + updateRearDisplayPresentationStatusListeners( + getCurrentRearDisplayPresentationModeStatus()); + } + } + + + @GuardedBy("mLock") + private int getCurrentRearDisplayModeStatus() { + if (mRearDisplayState == INVALID_DEVICE_STATE) { + return WindowAreaComponent.STATUS_UNSUPPORTED; + } + + if (mRearDisplaySessionStatus == WindowAreaComponent.SESSION_STATE_ACTIVE + || !ArrayUtils.contains(mCurrentSupportedDeviceStates, mRearDisplayState) + || 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 state is that which corresponds to {@code mRearDisplayState}. + * + * @return {@code true} if the device is in rear display state {@code false} if not + */ + @GuardedBy("mLock") + private boolean isRearDisplayActive() { + return mCurrentDeviceState == mRearDisplayState; + } + + @GuardedBy("mLock") + private void updateRearDisplayStatusListeners(@WindowAreaStatus int windowAreaStatus) { + if (mRearDisplayState == INVALID_DEVICE_STATE) { + return; + } + synchronized (mLock) { + for (int i = 0; i < mRearDisplayStatusListeners.size(); i++) { + mRearDisplayStatusListeners.valueAt(i).accept(windowAreaStatus); + } + } + } + + @GuardedBy("mLock") + private int getCurrentRearDisplayPresentationModeStatus() { + if (mConcurrentDisplayState == INVALID_DEVICE_STATE) { + return WindowAreaComponent.STATUS_UNSUPPORTED; + } + + if (mCurrentDeviceState == mConcurrentDisplayState + || !ArrayUtils.contains(mCurrentSupportedDeviceStates, mConcurrentDisplayState) + || isDeviceFolded()) { + return WindowAreaComponent.STATUS_UNAVAILABLE; + } + return WindowAreaComponent.STATUS_AVAILABLE; + } + + @GuardedBy("mLock") + private boolean isDeviceFolded() { + return ArrayUtils.contains(mFoldedDeviceStates, mCurrentDeviceState); + } + + @GuardedBy("mLock") + private void updateRearDisplayPresentationStatusListeners( + @WindowAreaStatus int windowAreaStatus) { + if (mConcurrentDisplayState == INVALID_DEVICE_STATE) { + return; + } + RearDisplayPresentationStatus consumerValue = new RearDisplayPresentationStatus( + windowAreaStatus, getRearDisplayMetrics()); + synchronized (mLock) { + for (int i = 0; i < mRearDisplayPresentationStatusListeners.size(); i++) { + mRearDisplayPresentationStatusListeners.valueAt(i).accept(consumerValue); + } + } + } + + /** + * Returns the{@link DisplayMetrics} associated with the rear facing display. If the rear facing + * display was not found in the display list, but we have already computed the + * {@link DisplayMetrics} for that display, we return the cached value. + * + * TODO(b/267563768): Update with guidance from Display team for missing displays. + * + * @throws IllegalArgumentException if the display is not found and there is no cached + * {@link DisplayMetrics} for this display. + */ + @GuardedBy("mLock") + private DisplayMetrics getRearDisplayMetrics() { + Display[] displays = mDisplayManager.getDisplays( + DisplayManager.DISPLAY_CATEGORY_ALL_INCLUDING_DISABLED); + for (int i = 0; i < displays.length; i++) { + DisplayAddress.Physical address = + (DisplayAddress.Physical) displays[i].getAddress(); + if (mRearDisplayAddress == address.getPhysicalDisplayId()) { + if (mRearDisplayMetrics == null) { + mRearDisplayMetrics = new DisplayMetrics(); + } + displays[i].getRealMetrics(mRearDisplayMetrics); + return mRearDisplayMetrics; + } + } + if (mRearDisplayMetrics != null) { + return mRearDisplayMetrics; + } else { + throw new IllegalArgumentException( + "No display found with the provided display address"); + } + } + + @GuardedBy("mLock") + @WindowAreaSessionState + private int getLastReportedRearDisplayPresentationStatus() { + return mLastReportedRearDisplayPresentationStatus; + } + + /** + * 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 RearDisplayStateRequestCallbackAdapter implements DeviceStateRequest.Callback { + + private final Consumer<Integer> mRearDisplaySessionCallback; + + RearDisplayStateRequestCallbackAdapter(@NonNull Consumer<Integer> callback) { + mRearDisplaySessionCallback = callback; + } + + @Override + public void onRequestActivated(@NonNull DeviceStateRequest request) { + synchronized (mLock) { + if (request.equals(mRearDisplayStateRequest)) { + mRearDisplaySessionStatus = WindowAreaComponent.SESSION_STATE_ACTIVE; + mRearDisplaySessionCallback.accept(mRearDisplaySessionStatus); + updateRearDisplayStatusListeners(getCurrentRearDisplayModeStatus()); + } + } + } + + @Override + public void onRequestCanceled(DeviceStateRequest request) { + synchronized (mLock) { + if (request.equals(mRearDisplayStateRequest)) { + mRearDisplayStateRequest = null; + } + mRearDisplaySessionStatus = WindowAreaComponent.SESSION_STATE_INACTIVE; + mRearDisplaySessionCallback.accept(mRearDisplaySessionStatus); + updateRearDisplayStatusListeners(getCurrentRearDisplayModeStatus()); + } + } + } +} 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..d94e8e426c4b 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java @@ -17,18 +17,27 @@ package androidx.window.extensions.embedding; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; +import static android.window.TaskFragmentOperation.OP_TYPE_SET_ANIMATION_PARAMS; + +import static androidx.window.extensions.embedding.SplitContainer.getFinishPrimaryWithSecondaryBehavior; +import static androidx.window.extensions.embedding.SplitContainer.getFinishSecondaryWithPrimaryBehavior; +import static androidx.window.extensions.embedding.SplitContainer.shouldFinishAssociatedContainerWhenStacked; +import static androidx.window.extensions.embedding.SplitContainer.shouldFinishPrimaryWithSecondary; +import static androidx.window.extensions.embedding.SplitContainer.shouldFinishSecondaryWithPrimary; import android.app.Activity; import android.app.WindowConfiguration.WindowingMode; import android.content.Intent; -import android.content.res.Configuration; import android.graphics.Rect; import android.os.Bundle; import android.os.IBinder; import android.util.ArrayMap; +import android.window.TaskFragmentAnimationParams; import android.window.TaskFragmentCreationParams; import android.window.TaskFragmentInfo; +import android.window.TaskFragmentOperation; import android.window.TaskFragmentOrganizer; +import android.window.TaskFragmentTransaction; import android.window.WindowContainerTransaction; import androidx.annotation.NonNull; @@ -51,34 +60,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; } @@ -86,25 +87,20 @@ class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer { @Override public void unregisterOrganizer() { if (mAnimationController != null) { - mAnimationController.unregisterAllRemoteAnimations(); + mAnimationController.unregisterRemoteAnimations(); mAnimationController = null; } super.unregisterOrganizer(); } - /** Overrides the animation if the transition is on the given Task. */ - void startOverrideSplitAnimation(int taskId) { + /** + * Overrides the animation for transitions of embedded activities organized by this organizer. + */ + void overrideSplitAnimation() { if (mAnimationController == null) { mAnimationController = new TaskFragmentAnimationController(this); } - mAnimationController.registerRemoteAnimations(taskId); - } - - /** No longer overrides the animation if the transition is on the given Task. */ - void stopOverrideSplitAnimation(int taskId) { - if (mAnimationController != null) { - mAnimationController.unregisterRemoteAnimations(taskId); - } + mAnimationController.registerRemoteAnimations(); } /** @@ -113,39 +109,54 @@ class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer { * be resized based on {@param launchingFragmentBounds}. * Otherwise, we will create a new TaskFragment with the given * token for the {@param launchingActivity}. - * @param launchingFragmentBounds the initial bounds for the launching TaskFragment. + * @param launchingRelBounds the initial relative bounds for the launching TaskFragment. * @param launchingActivity the Activity to put on the left hand side of the split as the * primary. * @param secondaryFragmentToken token to create the secondary TaskFragment with. - * @param secondaryFragmentBounds the initial bounds for the secondary TaskFragment + * @param secondaryRelBounds the initial relative bounds for the secondary TaskFragment * @param activityIntent Intent to start the secondary Activity with. * @param activityOptions ActivityOptions to start the secondary Activity with. * @param windowingMode the windowing mode to set for the TaskFragments. + * @param splitAttributes the {@link SplitAttributes} to represent the split. */ void startActivityToSide(@NonNull WindowContainerTransaction wct, - @NonNull IBinder launchingFragmentToken, @NonNull Rect launchingFragmentBounds, + @NonNull IBinder launchingFragmentToken, @NonNull Rect launchingRelBounds, @NonNull Activity launchingActivity, @NonNull IBinder secondaryFragmentToken, - @NonNull Rect secondaryFragmentBounds, @NonNull Intent activityIntent, + @NonNull Rect secondaryRelBounds, @NonNull Intent activityIntent, @Nullable Bundle activityOptions, @NonNull SplitRule rule, - @WindowingMode int windowingMode) { + @WindowingMode int windowingMode, @NonNull SplitAttributes splitAttributes) { final IBinder ownerToken = launchingActivity.getActivityToken(); // Create or resize the launching TaskFragment. if (mFragmentInfos.containsKey(launchingFragmentToken)) { - resizeTaskFragment(wct, launchingFragmentToken, launchingFragmentBounds); + resizeTaskFragment(wct, launchingFragmentToken, launchingRelBounds); updateWindowingMode(wct, launchingFragmentToken, windowingMode); } else { createTaskFragmentAndReparentActivity(wct, launchingFragmentToken, ownerToken, - launchingFragmentBounds, windowingMode, launchingActivity); + launchingRelBounds, windowingMode, launchingActivity); } + updateAnimationParams(wct, launchingFragmentToken, splitAttributes); // Create a TaskFragment for the secondary activity. - createTaskFragmentAndStartActivity(wct, secondaryFragmentToken, ownerToken, - secondaryFragmentBounds, windowingMode, activityIntent, + final TaskFragmentCreationParams fragmentOptions = new TaskFragmentCreationParams.Builder( + getOrganizerToken(), secondaryFragmentToken, ownerToken) + .setInitialRelativeBounds(secondaryRelBounds) + .setWindowingMode(windowingMode) + // Make sure to set the paired fragment token so that the new TaskFragment will be + // positioned right above the paired TaskFragment. + // This is needed in case we need to launch a placeholder Activity to split below a + // transparent always-expand Activity. + .setPairedPrimaryFragmentToken(launchingFragmentToken) + .build(); + createTaskFragment(wct, fragmentOptions); + updateAnimationParams(wct, secondaryFragmentToken, splitAttributes); + wct.startActivityInTaskFragment(secondaryFragmentToken, ownerToken, activityIntent, activityOptions); // Set adjacent to each other so that the containers below will be invisible. - setAdjacentTaskFragments(wct, launchingFragmentToken, secondaryFragmentToken, rule); + setAdjacentTaskFragmentsWithRule(wct, launchingFragmentToken, secondaryFragmentToken, rule); + setCompanionTaskFragment(wct, launchingFragmentToken, secondaryFragmentToken, rule, + false /* isStacked */); } /** @@ -153,20 +164,12 @@ 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 */); + clearAdjacentTaskFragments(wct, fragmentToken); 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); + updateAnimationParams(wct, fragmentToken, TaskFragmentAnimationParams.DEFAULT); } /** @@ -174,93 +177,139 @@ class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer { * @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); + updateAnimationParams(wct, fragmentToken, TaskFragmentAnimationParams.DEFAULT); } /** * @param ownerToken The token of the activity that creates this task fragment. It does not * have to be a child of this task fragment, but must belong to the same task. */ - void createTaskFragment(WindowContainerTransaction wct, IBinder fragmentToken, - IBinder ownerToken, @NonNull Rect bounds, @WindowingMode int windowingMode) { - final TaskFragmentCreationParams fragmentOptions = - createFragmentOptions(fragmentToken, ownerToken, bounds, windowingMode); - wct.createTaskFragment(fragmentOptions); + void createTaskFragment(@NonNull WindowContainerTransaction wct, @NonNull IBinder fragmentToken, + @NonNull IBinder ownerToken, @NonNull Rect relBounds, + @WindowingMode int windowingMode) { + createTaskFragment(wct, fragmentToken, ownerToken, relBounds, windowingMode, + null /* pairedActivityToken */); } /** * @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. + * @param pairedActivityToken The token of the activity that will be reparented to this task + * fragment. When it is not {@code null}, the task fragment will be + * positioned right above it. */ - private void createTaskFragmentAndReparentActivity( - WindowContainerTransaction wct, IBinder fragmentToken, IBinder ownerToken, - @NonNull Rect bounds, @WindowingMode int windowingMode, Activity activity) { - createTaskFragment(wct, fragmentToken, ownerToken, bounds, windowingMode); - wct.reparentActivityToTaskFragment(fragmentToken, activity.getActivityToken()); + void createTaskFragment(@NonNull WindowContainerTransaction wct, @NonNull IBinder fragmentToken, + @NonNull IBinder ownerToken, @NonNull Rect relBounds, @WindowingMode int windowingMode, + @Nullable IBinder pairedActivityToken) { + final TaskFragmentCreationParams fragmentOptions = new TaskFragmentCreationParams.Builder( + getOrganizerToken(), fragmentToken, ownerToken) + .setInitialRelativeBounds(relBounds) + .setWindowingMode(windowingMode) + .setPairedActivityToken(pairedActivityToken) + .build(); + createTaskFragment(wct, fragmentOptions); + } + + void createTaskFragment(@NonNull WindowContainerTransaction wct, + @NonNull TaskFragmentCreationParams fragmentOptions) { + if (mFragmentInfos.containsKey(fragmentOptions.getFragmentToken())) { + throw new IllegalArgumentException( + "There is an existing TaskFragment with fragmentToken=" + + fragmentOptions.getFragmentToken()); + } + wct.createTaskFragment(fragmentOptions); } /** * @param ownerToken The token of the activity that creates this task fragment. It does not * have to be a child of this task fragment, but must belong to the same task. */ - private void createTaskFragmentAndStartActivity( - WindowContainerTransaction wct, IBinder fragmentToken, IBinder ownerToken, - @NonNull Rect bounds, @WindowingMode int windowingMode, Intent activityIntent, - @Nullable Bundle activityOptions) { - createTaskFragment(wct, fragmentToken, ownerToken, bounds, windowingMode); - wct.startActivityInTaskFragment(fragmentToken, ownerToken, activityIntent, activityOptions); + private void createTaskFragmentAndReparentActivity(@NonNull WindowContainerTransaction wct, + @NonNull IBinder fragmentToken, @NonNull IBinder ownerToken, @NonNull Rect relBounds, + @WindowingMode int windowingMode, @NonNull Activity activity) { + final IBinder reparentActivityToken = activity.getActivityToken(); + createTaskFragment(wct, fragmentToken, ownerToken, relBounds, windowingMode, + reparentActivityToken); + wct.reparentActivityToTaskFragment(fragmentToken, reparentActivityToken); } - void setAdjacentTaskFragments(@NonNull WindowContainerTransaction wct, - @NonNull IBinder primary, @Nullable IBinder secondary, @Nullable SplitRule splitRule) { + /** + * Sets the two given TaskFragments as adjacent to each other with respecting the given + * {@link SplitRule} for {@link WindowContainerTransaction.TaskFragmentAdjacentParams}. + */ + void setAdjacentTaskFragmentsWithRule(@NonNull WindowContainerTransaction wct, + @NonNull IBinder primary, @NonNull IBinder secondary, @NonNull SplitRule splitRule) { WindowContainerTransaction.TaskFragmentAdjacentParams adjacentParams = null; final boolean finishSecondaryWithPrimary = - splitRule != null && SplitContainer.shouldFinishSecondaryWithPrimary(splitRule); + SplitContainer.shouldFinishSecondaryWithPrimary(splitRule); final boolean finishPrimaryWithSecondary = - splitRule != null && SplitContainer.shouldFinishPrimaryWithSecondary(splitRule); + SplitContainer.shouldFinishPrimaryWithSecondary(splitRule); if (finishSecondaryWithPrimary || finishPrimaryWithSecondary) { adjacentParams = new WindowContainerTransaction.TaskFragmentAdjacentParams(); adjacentParams.setShouldDelayPrimaryLastActivityRemoval(finishSecondaryWithPrimary); adjacentParams.setShouldDelaySecondaryLastActivityRemoval(finishPrimaryWithSecondary); } + setAdjacentTaskFragments(wct, primary, secondary, adjacentParams); + } + + void setAdjacentTaskFragments(@NonNull WindowContainerTransaction wct, + @NonNull IBinder primary, @NonNull IBinder secondary, + @Nullable WindowContainerTransaction.TaskFragmentAdjacentParams adjacentParams) { wct.setAdjacentTaskFragments(primary, secondary, adjacentParams); } - TaskFragmentCreationParams createFragmentOptions(IBinder fragmentToken, IBinder ownerToken, - Rect bounds, @WindowingMode int windowingMode) { - if (mFragmentInfos.containsKey(fragmentToken)) { - throw new IllegalArgumentException( - "There is an existing TaskFragment with fragmentToken=" + fragmentToken); + void clearAdjacentTaskFragments(@NonNull WindowContainerTransaction wct, + @NonNull IBinder fragmentToken) { + // Clear primary will also clear secondary. + wct.clearAdjacentTaskFragments(fragmentToken); + } + + void setCompanionTaskFragment(@NonNull WindowContainerTransaction wct, + @NonNull IBinder primary, @NonNull IBinder secondary, @NonNull SplitRule splitRule, + boolean isStacked) { + final boolean finishPrimaryWithSecondary; + if (isStacked) { + finishPrimaryWithSecondary = shouldFinishAssociatedContainerWhenStacked( + getFinishPrimaryWithSecondaryBehavior(splitRule)); + } else { + finishPrimaryWithSecondary = shouldFinishPrimaryWithSecondary(splitRule); } + setCompanionTaskFragment(wct, primary, finishPrimaryWithSecondary ? secondary : null); - return new TaskFragmentCreationParams.Builder( - getOrganizerToken(), - fragmentToken, - ownerToken) - .setInitialBounds(bounds) - .setWindowingMode(windowingMode) - .build(); + final boolean finishSecondaryWithPrimary; + if (isStacked) { + finishSecondaryWithPrimary = shouldFinishAssociatedContainerWhenStacked( + getFinishSecondaryWithPrimaryBehavior(splitRule)); + } else { + finishSecondaryWithPrimary = shouldFinishSecondaryWithPrimary(splitRule); + } + setCompanionTaskFragment(wct, secondary, finishSecondaryWithPrimary ? primary : null); + } + + void setCompanionTaskFragment(@NonNull WindowContainerTransaction wct, @NonNull IBinder primary, + @Nullable IBinder secondary) { + wct.setCompanionTaskFragment(primary, secondary); } - void resizeTaskFragment(WindowContainerTransaction wct, IBinder fragmentToken, - @Nullable Rect bounds) { + void resizeTaskFragment(@NonNull WindowContainerTransaction wct, @NonNull IBinder fragmentToken, + @Nullable Rect relBounds) { if (!mFragmentInfos.containsKey(fragmentToken)) { throw new IllegalArgumentException( "Can't find an existing TaskFragment with fragmentToken=" + fragmentToken); } - if (bounds == null) { - bounds = new Rect(); + if (relBounds == null) { + relBounds = new Rect(); } - wct.setBounds(mFragmentInfos.get(fragmentToken).getToken(), bounds); + wct.setRelativeBounds(mFragmentInfos.get(fragmentToken).getToken(), relBounds); } - 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,59 +317,49 @@ class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer { wct.setWindowingMode(mFragmentInfos.get(fragmentToken).getToken(), windowingMode); } - void deleteTaskFragment(WindowContainerTransaction wct, IBinder fragmentToken) { - if (!mFragmentInfos.containsKey(fragmentToken)) { - throw new IllegalArgumentException( - "Can't find an existing TaskFragment with fragmentToken=" + fragmentToken); - } - wct.deleteTaskFragment(mFragmentInfos.get(fragmentToken).getToken()); + /** + * Updates the {@link TaskFragmentAnimationParams} for the given TaskFragment based on + * {@link SplitAttributes}. + */ + void updateAnimationParams(@NonNull WindowContainerTransaction wct, + @NonNull IBinder fragmentToken, @NonNull SplitAttributes splitAttributes) { + updateAnimationParams(wct, fragmentToken, createAnimationParamsOrDefault(splitAttributes)); } - @Override - public void onTaskFragmentAppeared(@NonNull TaskFragmentInfo taskFragmentInfo) { - final IBinder fragmentToken = taskFragmentInfo.getFragmentToken(); - mFragmentInfos.put(fragmentToken, taskFragmentInfo); - - if (mCallback != null) { - mCallback.onTaskFragmentAppeared(taskFragmentInfo); - } + void updateAnimationParams(@NonNull WindowContainerTransaction wct, + @NonNull IBinder fragmentToken, @NonNull TaskFragmentAnimationParams animationParams) { + final TaskFragmentOperation operation = new TaskFragmentOperation.Builder( + OP_TYPE_SET_ANIMATION_PARAMS) + .setAnimationParams(animationParams) + .build(); + wct.addTaskFragmentOperation(fragmentToken, operation); } - @Override - public void onTaskFragmentInfoChanged(@NonNull TaskFragmentInfo taskFragmentInfo) { - final IBinder fragmentToken = taskFragmentInfo.getFragmentToken(); - mFragmentInfos.put(fragmentToken, taskFragmentInfo); + void deleteTaskFragment(@NonNull WindowContainerTransaction wct, + @NonNull IBinder fragmentToken) { + wct.deleteTaskFragment(fragmentToken); + } - 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); - } + public void onTransactionReady(@NonNull TaskFragmentTransaction transaction) { + mCallback.onTransactionReady(transaction); } - @Override - public void onActivityReparentToTask(int taskId, @NonNull Intent activityIntent, - @NonNull IBinder activityToken) { - if (mCallback != null) { - mCallback.onActivityReparentToTask(taskId, activityIntent, activityToken); + private static TaskFragmentAnimationParams createAnimationParamsOrDefault( + @Nullable SplitAttributes splitAttributes) { + if (splitAttributes == null) { + return TaskFragmentAnimationParams.DEFAULT; } + return new TaskFragmentAnimationParams.Builder() + .setAnimationBackgroundColor(splitAttributes.getAnimationBackgroundColor()) + .build(); } } 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..18497ad249ee 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java @@ -16,26 +16,47 @@ package androidx.window.extensions.embedding; -import android.annotation.NonNull; import android.app.Activity; +import android.os.Binder; +import android.os.IBinder; import android.util.Pair; import android.util.Size; +import android.window.TaskFragmentParentInfo; +import android.window.WindowContainerTransaction; + +import androidx.annotation.NonNull; +import androidx.window.extensions.core.util.function.Function; /** * 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; + /** @see SplitContainer#getCurrentSplitAttributes() */ + @NonNull + private SplitAttributes mCurrentSplitAttributes; + /** @see SplitContainer#getDefaultSplitAttributes() */ + @NonNull + private SplitAttributes mDefaultSplitAttributes; + @NonNull + private final IBinder mToken; SplitContainer(@NonNull TaskFragmentContainer primaryContainer, @NonNull Activity primaryActivity, @NonNull TaskFragmentContainer secondaryContainer, - @NonNull SplitRule splitRule) { + @NonNull SplitRule splitRule, + @NonNull SplitAttributes splitAttributes) { mPrimaryContainer = primaryContainer; mSecondaryContainer = secondaryContainer; mSplitRule = splitRule; + mDefaultSplitAttributes = splitRule.getDefaultSplitAttributes(); + mCurrentSplitAttributes = splitAttributes; + mToken = new Binder("SplitContainer"); if (shouldFinishPrimaryWithSecondary(splitRule)) { if (mPrimaryContainer.getRunningActivityCount() == 1 @@ -68,6 +89,67 @@ class SplitContainer { return mSplitRule; } + /** + * Returns the current {@link SplitAttributes} this {@code SplitContainer} is showing. + * <p> + * If the {@code SplitAttributes} calculator function is not set by + * {@link SplitController#setSplitAttributesCalculator(Function)}, the current + * {@code SplitAttributes} is either to expand the containers if the size constraints of + * {@link #getSplitRule()} are not satisfied, + * or the {@link #getDefaultSplitAttributes()}, otherwise. + * </p><p> + * If the {@code SplitAttributes} calculator function is set, the current + * {@code SplitAttributes} will be customized by the function, which can be any + * {@code SplitAttributes}. + * </p> + * + * @see SplitAttributes.SplitType.ExpandContainersSplitType + */ + @NonNull + SplitAttributes getCurrentSplitAttributes() { + return mCurrentSplitAttributes; + } + + /** + * Returns the default {@link SplitAttributes} when the parent task container bounds satisfy + * {@link #getSplitRule()} constraints. + * <p> + * The value is usually from {@link SplitRule#getDefaultSplitAttributes} unless it is overridden + * by {@link SplitController#updateSplitAttributes(IBinder, SplitAttributes)}. + */ + @NonNull + SplitAttributes getDefaultSplitAttributes() { + return mDefaultSplitAttributes; + } + + @NonNull + IBinder getToken() { + return mToken; + } + + /** + * Updates the {@link SplitAttributes} to this container. + * It is usually used when there's a folding state change or + * {@link SplitController#onTaskFragmentParentInfoChanged(WindowContainerTransaction, + * int, TaskFragmentParentInfo)}. + */ + void updateCurrentSplitAttributes(@NonNull SplitAttributes splitAttributes) { + mCurrentSplitAttributes = splitAttributes; + } + + /** + * Overrides the default {@link SplitAttributes} to this container, which may be different + * from {@link SplitRule#getDefaultSplitAttributes}. + */ + void updateDefaultSplitAttributes(@NonNull SplitAttributes splitAttributes) { + mDefaultSplitAttributes = splitAttributes; + } + + @NonNull + TaskContainer getTaskContainer() { + return getPrimaryContainer().getTaskContainer(); + } + /** Returns the minimum dimension pair of primary container and secondary container. */ @NonNull Pair<Size, Size> getMinDimensionsPair() { @@ -79,6 +161,12 @@ class SplitContainer { return (mSplitRule instanceof SplitPlaceholderRule); } + @NonNull + SplitInfo toSplitInfo() { + return new SplitInfo(mPrimaryContainer.toActivityStack(), + mSecondaryContainer.toActivityStack(), mCurrentSplitAttributes, mToken); + } + static boolean shouldFinishPrimaryWithSecondary(@NonNull SplitRule splitRule) { final boolean isPlaceholderContainer = splitRule instanceof SplitPlaceholderRule; final boolean shouldFinishPrimaryWithSecondary = (splitRule instanceof SplitPairRule) @@ -135,8 +223,10 @@ class SplitContainer { public String toString() { return "SplitContainer{" + " primaryContainer=" + mPrimaryContainer - + " secondaryContainer=" + mSecondaryContainer - + " splitRule=" + mSplitRule + + ", secondaryContainer=" + mSecondaryContainer + + ", splitRule=" + mSplitRule + + ", currentSplitAttributes" + mCurrentSplitAttributes + + ", defaultSplitAttributes" + mDefaultSplitAttributes + "}"; } } 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..89f4890c254e 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,23 @@ package androidx.window.extensions.embedding; +import static android.app.ActivityManager.START_SUCCESS; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; +import static android.view.Display.DEFAULT_DISPLAY; +import static android.window.TaskFragmentOperation.OP_TYPE_REPARENT_ACTIVITY_TO_TASK_FRAGMENT; +import static android.window.TaskFragmentOperation.OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT; +import static android.window.TaskFragmentOrganizer.KEY_ERROR_CALLBACK_OP_TYPE; +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.TASK_FRAGMENT_TRANSIT_CLOSE; +import static android.window.TaskFragmentOrganizer.TASK_FRAGMENT_TRANSIT_OPEN; +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 androidx.window.extensions.embedding.SplitContainer.getFinishPrimaryWithSecondaryBehavior; import static androidx.window.extensions.embedding.SplitContainer.getFinishSecondaryWithPrimaryBehavior; @@ -25,14 +40,16 @@ import static androidx.window.extensions.embedding.SplitContainer.isStickyPlaceh import static androidx.window.extensions.embedding.SplitContainer.shouldFinishAssociatedContainerWhenAdjacent; import static androidx.window.extensions.embedding.SplitContainer.shouldFinishAssociatedContainerWhenStacked; import static androidx.window.extensions.embedding.SplitPresenter.RESULT_EXPAND_FAILED_NO_TF_INFO; +import static androidx.window.extensions.embedding.SplitPresenter.getActivitiesMinDimensionsPair; import static androidx.window.extensions.embedding.SplitPresenter.getActivityIntentMinDimensionsPair; -import static androidx.window.extensions.embedding.SplitPresenter.getNonEmbeddedActivityBounds; -import static androidx.window.extensions.embedding.SplitPresenter.shouldShowSideBySide; +import static androidx.window.extensions.embedding.SplitPresenter.getTaskWindowMetrics; +import static androidx.window.extensions.embedding.SplitPresenter.shouldShowSplit; import android.app.Activity; import android.app.ActivityClient; import android.app.ActivityOptions; import android.app.ActivityThread; +import android.app.Application; import android.app.Instrumentation; import android.content.ComponentName; import android.content.Context; @@ -43,26 +60,40 @@ import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.Looper; +import android.os.SystemProperties; import android.util.ArraySet; import android.util.Log; import android.util.Pair; import android.util.Size; import android.util.SparseArray; +import android.view.WindowMetrics; +import android.window.TaskFragmentAnimationParams; import android.window.TaskFragmentInfo; +import android.window.TaskFragmentOperation; +import android.window.TaskFragmentParentInfo; +import android.window.TaskFragmentTransaction; import android.window.WindowContainerTransaction; import androidx.annotation.GuardedBy; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.window.common.CommonFoldingFeature; +import androidx.window.common.DeviceStateManagerFoldingFeatureProducer; import androidx.window.common.EmptyLifecycleCallbacksAdapter; +import androidx.window.extensions.WindowExtensionsImpl; +import androidx.window.extensions.core.util.function.Consumer; +import androidx.window.extensions.core.util.function.Function; +import androidx.window.extensions.embedding.TransactionManager.TransactionRecord; +import androidx.window.extensions.layout.WindowLayoutComponentImpl; import com.android.internal.annotations.VisibleForTesting; import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import java.util.Objects; import java.util.Set; import java.util.concurrent.Executor; -import java.util.function.Consumer; /** * Main controller class that manages split states and presentation. @@ -70,14 +101,39 @@ 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", true); @VisibleForTesting @GuardedBy("mLock") final SplitPresenter mPresenter; + @VisibleForTesting + @GuardedBy("mLock") + final TransactionManager mTransactionManager; + // Currently applied split configuration. @GuardedBy("mLock") private final List<EmbeddingRule> mSplitRules = new ArrayList<>(); + + /** + * A developer-defined {@link SplitAttributes} calculator to compute the current + * {@link SplitAttributes} with the current device and window states. + * It is registered via {@link #setSplitAttributesCalculator(Function)} + * and unregistered via {@link #clearSplitAttributesCalculator()}. + * This is called when: + * <ul> + * <li>{@link SplitPresenter#updateSplitContainer(SplitContainer, TaskFragmentContainer, + * WindowContainerTransaction)}</li> + * <li>There's a started Activity which matches {@link SplitPairRule} </li> + * <li>Checking whether the place holder should be launched if there's a Activity matches + * {@link SplitPlaceholderRule} </li> + * </ul> + */ + @GuardedBy("mLock") + @Nullable + private Function<SplitAttributesCalculatorParams, SplitAttributes> mSplitAttributesCalculator; + /** * Map from Task id to {@link TaskContainer} which contains all TaskFragment and split pair info * below it. @@ -88,24 +144,63 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen @GuardedBy("mLock") final SparseArray<TaskContainer> mTaskContainers = new SparseArray<>(); - // Callback to Jetpack to notify about changes to split states. - @NonNull + /** Callback to Jetpack to notify about changes to split states. */ + @GuardedBy("mLock") + @Nullable private Consumer<List<SplitInfo>> mEmbeddingCallback; private final List<SplitInfo> mLastReportedSplitStates = new ArrayList<>(); private final Handler mHandler; - private final Object mLock = new Object(); + final Object mLock = new Object(); + private final ActivityStartMonitor mActivityStartMonitor; - public SplitController() { + public SplitController(@NonNull WindowLayoutComponentImpl windowLayoutComponent, + @NonNull DeviceStateManagerFoldingFeatureProducer foldingFeatureProducer) { final MainThreadExecutor executor = new MainThreadExecutor(); mHandler = executor.mHandler; - mPresenter = new SplitPresenter(executor, this); - ActivityThread activityThread = ActivityThread.currentActivityThread(); + mPresenter = new SplitPresenter(executor, windowLayoutComponent, this); + mTransactionManager = new TransactionManager(mPresenter); + final ActivityThread activityThread = ActivityThread.currentActivityThread(); + final Application application = activityThread.getApplication(); // Register a callback to be notified about activities being created. - activityThread.getApplication().registerActivityLifecycleCallbacks( - new LifecycleCallbacks()); + application.registerActivityLifecycleCallbacks(new LifecycleCallbacks()); // Intercept activity starts to route activities to new containers if necessary. Instrumentation instrumentation = activityThread.getInstrumentation(); - instrumentation.addMonitor(new ActivityStartMonitor()); + + mActivityStartMonitor = new ActivityStartMonitor(); + instrumentation.addMonitor(mActivityStartMonitor); + foldingFeatureProducer.addDataChangedCallback(new FoldingFeatureListener()); + } + + private class FoldingFeatureListener + implements java.util.function.Consumer<List<CommonFoldingFeature>> { + @Override + public void accept(List<CommonFoldingFeature> foldingFeatures) { + synchronized (mLock) { + final TransactionRecord transactionRecord = mTransactionManager + .startNewTransaction(); + final WindowContainerTransaction wct = transactionRecord.getTransaction(); + for (int i = 0; i < mTaskContainers.size(); i++) { + final TaskContainer taskContainer = mTaskContainers.valueAt(i); + if (!taskContainer.isVisible()) { + continue; + } + if (taskContainer.getDisplayId() != DEFAULT_DISPLAY) { + continue; + } + // TODO(b/238948678): Support reporting display features in all windowing modes. + if (taskContainer.isInMultiWindow()) { + continue; + } + if (taskContainer.isEmpty()) { + continue; + } + updateContainersInTask(wct, taskContainer); + } + // The WCT should be applied and merged to the device state change transition if + // there is one. + transactionRecord.apply(false /* shouldApplyIndependently */); + } + } } /** Updates the embedding rules applied to future activity launches. */ @@ -114,260 +209,552 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen synchronized (mLock) { mSplitRules.clear(); mSplitRules.addAll(rules); - for (int i = mTaskContainers.size() - 1; i >= 0; i--) { - updateAnimationOverride(mTaskContainers.valueAt(i)); - } } } + @Override + public void setSplitAttributesCalculator( + @NonNull Function<SplitAttributesCalculatorParams, SplitAttributes> calculator) { + synchronized (mLock) { + mSplitAttributesCalculator = calculator; + } + } + + @Override + public void clearSplitAttributesCalculator() { + synchronized (mLock) { + mSplitAttributesCalculator = null; + } + } + + @GuardedBy("mLock") + @Nullable + Function<SplitAttributesCalculatorParams, SplitAttributes> getSplitAttributesCalculator() { + return mSplitAttributesCalculator; + } + + @Override + @NonNull + public ActivityOptions setLaunchingActivityStack(@NonNull ActivityOptions options, + @NonNull IBinder token) { + options.setLaunchTaskFragmentToken(token); + return options; + } + @NonNull + @GuardedBy("mLock") + @VisibleForTesting List<EmbeddingRule> getSplitRules() { return mSplitRules; } /** * Registers the split organizer callback to notify about changes to active splits. + * @deprecated Use {@link #setSplitInfoCallback(Consumer)} starting with + * {@link WindowExtensionsImpl#getVendorApiLevel()} 2. */ + @Deprecated @Override - public void setSplitInfoCallback(@NonNull Consumer<List<SplitInfo>> callback) { + public void setSplitInfoCallback( + @NonNull java.util.function.Consumer<List<SplitInfo>> callback) { + Consumer<List<SplitInfo>> oemConsumer = callback::accept; + setSplitInfoCallback(oemConsumer); + } + + /** + * Registers the split organizer callback to notify about changes to active splits. + * @since {@link WindowExtensionsImpl#getVendorApiLevel()} 2 + */ + public void setSplitInfoCallback(Consumer<List<SplitInfo>> callback) { synchronized (mLock) { mEmbeddingCallback = callback; updateCallbackIfNecessary(); } } + /** + * Clears the listener set in {@link SplitController#setSplitInfoCallback(Consumer)}. + */ @Override - public void onTaskFragmentAppeared(@NonNull TaskFragmentInfo taskFragmentInfo) { + public void clearSplitInfoCallback() { synchronized (mLock) { - TaskFragmentContainer container = getContainer(taskFragmentInfo.getFragmentToken()); - if (container == null) { - return; - } - - container.setInfo(taskFragmentInfo); - if (container.isFinished()) { - mPresenter.cleanupContainer(container, false /* shouldFinishDependent */); - } - updateCallbackIfNecessary(); + mEmbeddingCallback = null; } } @Override - public void onTaskFragmentInfoChanged(@NonNull TaskFragmentInfo taskFragmentInfo) { + public void finishActivityStacks(@NonNull Set<IBinder> activityStackTokens) { + if (activityStackTokens.isEmpty()) { + return; + } synchronized (mLock) { - TaskFragmentContainer container = getContainer(taskFragmentInfo.getFragmentToken()); - if (container == null) { - return; - } + // Translate ActivityStack to TaskFragmentContainer. + final List<TaskFragmentContainer> pendingFinishingContainers = + activityStackTokens.stream() + .map(token -> { + synchronized (mLock) { + return getContainer(token); + } + }).filter(Objects::nonNull) + .toList(); - 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. + if (pendingFinishingContainers.isEmpty()) { return; - } else if (isInPip) { - // Enter PIP. - // All overrides will be cleanup. - container.setLastRequestedBounds(null /* bounds */); - container.setLastRequestedWindowingMode(WINDOWING_MODE_UNDEFINED); - cleanupForEnterPip(wct, container); - } else if (wasInPip) { - // Exit PIP. - // Updates the presentation of the container. Expand or launch placeholder if - // needed. - updateContainer(wct, container); } - mPresenter.applyTransaction(wct); - updateCallbackIfNecessary(); + // Start transaction with close transit type. + final TransactionRecord transactionRecord = mTransactionManager.startNewTransaction(); + transactionRecord.setOriginType(TASK_FRAGMENT_TRANSIT_CLOSE); + final WindowContainerTransaction wct = transactionRecord.getTransaction(); + + forAllTaskContainers(taskContainer -> { + synchronized (mLock) { + final List<TaskFragmentContainer> containers = taskContainer.mContainers; + // Clean up the TaskFragmentContainers by the z-order from the lowest. + for (int i = 0; i < containers.size(); i++) { + final TaskFragmentContainer container = containers.get(i); + if (pendingFinishingContainers.contains(container)) { + // Don't update records here to prevent double invocation. + container.finish(false /* shouldFinishDependant */, mPresenter, + wct, this, false /* shouldRemoveRecord */); + } + } + // Remove container records. + removeContainers(taskContainer, pendingFinishingContainers); + // Update the change to the server side. + updateContainersInTaskIfVisible(wct, taskContainer.getTaskId()); + } + }); + + // Apply the transaction. + transactionRecord.apply(false /* shouldApplyIndependently */); } } @Override - public void onTaskFragmentVanished(@NonNull TaskFragmentInfo taskFragmentInfo) { + public void invalidateTopVisibleSplitAttributes() { 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); + WindowContainerTransaction wct = mTransactionManager.startNewTransaction() + .getTransaction(); + forAllTaskContainers(taskContainer -> { + synchronized (mLock) { + updateContainersInTaskIfVisible(wct, taskContainer.getTaskId()); } - updateCallbackIfNecessary(); - } - cleanupTaskFragment(taskFragmentInfo.getFragmentToken()); + }); + mTransactionManager.getCurrentTransactionRecord() + .apply(false /* shouldApplyIndependently */); + } + } + + @GuardedBy("mLock") + private void forAllTaskContainers(@NonNull Consumer<TaskContainer> callback) { + for (int i = mTaskContainers.size() - 1; i >= 0; --i) { + callback.accept(mTaskContainers.valueAt(i)); } } @Override - public void onTaskFragmentParentInfoChanged(@NonNull IBinder fragmentToken, - @NonNull Configuration parentConfig) { + public void updateSplitAttributes(@NonNull IBinder splitInfoToken, + @NonNull SplitAttributes splitAttributes) { + Objects.requireNonNull(splitInfoToken); + Objects.requireNonNull(splitAttributes); 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(); + final SplitContainer splitContainer = getSplitContainer(splitInfoToken); + if (splitContainer == null) { + Log.w(TAG, "Cannot find SplitContainer for token:" + splitInfoToken); + return; + } + // Override the default split Attributes so that it will be applied + // if the SplitContainer is not visible currently. + splitContainer.updateDefaultSplitAttributes(splitAttributes); + + final TransactionRecord transactionRecord = mTransactionManager.startNewTransaction(); + final WindowContainerTransaction wct = transactionRecord.getTransaction(); + if (updateSplitContainerIfNeeded(splitContainer, wct, splitAttributes)) { + transactionRecord.apply(false /* shouldApplyIndependently */); + } else { + // Abort if the SplitContainer wasn't updated. + transactionRecord.abort(); } } } + /** + * Called when the transaction is ready so that the organizer can update the TaskFragments based + * on the changes in transaction. + */ @Override - public void onActivityReparentToTask(int taskId, @NonNull Intent activityIntent, - @NonNull IBinder activityToken) { + public void onTransactionReady(@NonNull TaskFragmentTransaction transaction) { 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); + final TransactionRecord transactionRecord = mTransactionManager.startNewTransaction( + transaction.getTransactionToken()); + final WindowContainerTransaction wct = transactionRecord.getTransaction(); + final List<TaskFragmentTransaction.Change> changes = transaction.getChanges(); + for (TaskFragmentTransaction.Change change : changes) { + final int taskId = change.getTaskId(); + final TaskFragmentInfo info = change.getTaskFragmentInfo(); + switch (change.getType()) { + case TYPE_TASK_FRAGMENT_APPEARED: + mPresenter.updateTaskFragmentInfo(info); + onTaskFragmentAppeared(wct, info); + break; + case TYPE_TASK_FRAGMENT_INFO_CHANGED: + mPresenter.updateTaskFragmentInfo(info); + onTaskFragmentInfoChanged(wct, info); + break; + case TYPE_TASK_FRAGMENT_VANISHED: + mPresenter.removeTaskFragmentInfo(info); + onTaskFragmentVanished(wct, info); + break; + case TYPE_TASK_FRAGMENT_PARENT_INFO_CHANGED: + onTaskFragmentParentInfoChanged(wct, taskId, + change.getTaskFragmentParentInfo()); + break; + case TYPE_TASK_FRAGMENT_ERROR: + final Bundle errorBundle = change.getErrorBundle(); + final IBinder errorToken = change.getErrorCallbackToken(); + final TaskFragmentInfo errorTaskFragmentInfo = errorBundle.getParcelable( + KEY_ERROR_CALLBACK_TASK_FRAGMENT_INFO, TaskFragmentInfo.class); + final int opType = errorBundle.getInt(KEY_ERROR_CALLBACK_OP_TYPE); + final Throwable exception = errorBundle.getSerializable( + KEY_ERROR_CALLBACK_THROWABLE, Throwable.class); + if (errorTaskFragmentInfo != null) { + mPresenter.updateTaskFragmentInfo(errorTaskFragmentInfo); + } + onTaskFragmentError(wct, errorToken, errorTaskFragmentInfo, opType, + exception); + break; + case TYPE_ACTIVITY_REPARENTED_TO_TASK: + onActivityReparentedToTask( + wct, + taskId, + change.getActivityIntent(), + change.getActivityToken()); + break; + default: + throw new IllegalArgumentException( + "Unknown TaskFragmentEvent=" + change.getType()); } - updateCallbackIfNecessary(); - return; } - final TaskContainer taskContainer = getTaskContainer(taskId); - if (taskContainer == null || taskContainer.isInPictureInPicture()) { - // We don't embed activity when it is in PIP. - return; - } + // Notify the server, and the server should apply and merge the + // WindowContainerTransaction to the active sync to finish the TaskFragmentTransaction. + transactionRecord.apply(false /* shouldApplyIndependently */); + updateCallbackIfNecessary(); + } + } - // 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) { - // 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); - mPresenter.applyTransaction(wct); - // Because the activity does not belong to the organizer process, we wait until - // onTaskFragmentAppeared to trigger updateCallbackIfNecessary(). + /** + * 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; + } + + container.setInfo(wct, taskFragmentInfo); + if (container.isFinished()) { + mTransactionManager.getCurrentTransactionRecord() + .setOriginType(TASK_FRAGMENT_TRANSIT_CLOSE); + mPresenter.cleanupContainer(wct, container, false /* shouldFinishDependent */); + } else { + // Update with the latest Task configuration. + updateContainer(wct, container); } } - /** Called on receiving {@link #onTaskFragmentVanished(TaskFragmentInfo)} for cleanup. */ - private void cleanupTaskFragment(@NonNull IBinder taskFragmentToken) { - for (int i = mTaskContainers.size() - 1; i >= 0; i--) { - final TaskContainer taskContainer = mTaskContainers.valueAt(i); - if (!taskContainer.mFinishedContainer.remove(taskFragmentToken)) { - continue; - } - if (taskContainer.isEmpty()) { - // Cleanup the TaskContainer if it becomes empty. - mPresenter.stopOverrideSplitAnimation(taskContainer.getTaskId()); - mTaskContainers.remove(taskContainer.getTaskId()); + /** + * 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. + mTransactionManager.getCurrentTransactionRecord() + .setOriginType(TASK_FRAGMENT_TRANSIT_CLOSE); + cleanupForEnterPip(wct, container); + mPresenter.cleanupContainer(wct, container, false /* shouldFinishDependent */); + } else if (taskFragmentInfo.isTaskClearedForReuse()) { + // Do not finish the dependents if this TaskFragment was cleared due to + // launching activity in the Task. + mTransactionManager.getCurrentTransactionRecord() + .setOriginType(TASK_FRAGMENT_TRANSIT_CLOSE); + mPresenter.cleanupContainer(wct, container, false /* shouldFinishDependent */); + } else if (taskFragmentInfo.isClearedForReorderActivityToFront()) { + // Do not finish the dependents if this TaskFragment was cleared to reorder + // the launching Activity to front of the Task. + mTransactionManager.getCurrentTransactionRecord() + .setOriginType(TASK_FRAGMENT_TRANSIT_CLOSE); + mPresenter.cleanupContainer(wct, container, false /* shouldFinishDependent */); + } else if (!container.isWaitingActivityAppear()) { + // Do not finish the container before the expected activity appear until + // timeout. + mTransactionManager.getCurrentTransactionRecord() + .setOriginType(TASK_FRAGMENT_TRANSIT_CLOSE); + mPresenter.cleanupContainer(wct, container, true /* shouldFinishDependent */); } + } 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.clearLastAdjacentTaskFragment(); + container.setLastCompanionTaskFragment(null /* fragmentToken */); + container.setLastRequestAnimationParams(TaskFragmentAnimationParams.DEFAULT); + cleanupForEnterPip(wct, container); + } else if (wasInPip) { + // Exit PIP. + // Updates the presentation of the container. Expand or launch placeholder if + // needed. + updateContainer(wct, container); } } - private void onTaskConfigurationChanged(int taskId, @NonNull Configuration config) { - final TaskContainer taskContainer = mTaskContainers.get(taskId); - if (taskContainer == null) { + /** + * 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 containers in the Task are up-to-date. + updateContainersInTaskIfVisible(wct, container.getTaskId()); + } + cleanupTaskFragment(taskFragmentInfo.getFragmentToken()); + } + + /** + * Called when the parent leaf Task of organized TaskFragments is changed. + * When the leaf Task is changed, the organizer may want to update the TaskFragments in one + * transaction. + * + * For case like screen size change, it will trigger {@link #onTaskFragmentParentInfoChanged} + * with new Task bounds, but may not trigger {@link #onTaskFragmentInfoChanged} because there + * can be an override bounds. + * + * @param wct The {@link WindowContainerTransaction} to make any changes with if needed. + * @param taskId Id of the parent Task that is changed. + * @param parentInfo {@link TaskFragmentParentInfo} of the parent Task. + */ + @VisibleForTesting + @GuardedBy("mLock") + void onTaskFragmentParentInfoChanged(@NonNull WindowContainerTransaction wct, + int taskId, @NonNull TaskFragmentParentInfo parentInfo) { + final TaskContainer taskContainer = getTaskContainer(taskId); + if (taskContainer == null || taskContainer.isEmpty()) { + Log.e(TAG, "onTaskFragmentParentInfoChanged on empty Task id=" + taskId); + return; + } + taskContainer.updateTaskFragmentParentInfo(parentInfo); + if (!taskContainer.isVisible()) { + // Don't update containers if the task is not visible. We only update containers when + // parentInfo#isVisibleRequested is true. return; } - final boolean wasInPip = taskContainer.isInPictureInPicture(); - final boolean isInPIp = isInPictureInPicture(config); - taskContainer.setWindowingMode(config.windowConfiguration.getWindowingMode()); + if (isInPictureInPicture(parentInfo.getConfiguration())) { + // No need to update presentation in PIP until the Task exit PIP. + return; + } + updateContainersInTask(wct, taskContainer); + } - // We need to check the animation override when enter/exit PIP or has bounds changed. - boolean shouldUpdateAnimationOverride = wasInPip != isInPIp; - if (taskContainer.setTaskBounds(config.windowConfiguration.getBounds()) - && !isInPIp) { - // We don't care the bounds change when it has already entered PIP. - shouldUpdateAnimationOverride = true; + @GuardedBy("mLock") + void updateContainersInTaskIfVisible(@NonNull WindowContainerTransaction wct, int taskId) { + final TaskContainer taskContainer = getTaskContainer(taskId); + if (taskContainer != null && taskContainer.isVisible()) { + updateContainersInTask(wct, taskContainer); } - if (shouldUpdateAnimationOverride) { - updateAnimationOverride(taskContainer); + } + + @GuardedBy("mLock") + private void updateContainersInTask(@NonNull WindowContainerTransaction wct, + @NonNull TaskContainer taskContainer) { + // Update all TaskFragments in the Task. Make a copy of the list since some may be + // removed on updating. + final List<TaskFragmentContainer> containers = + new ArrayList<>(taskContainer.mContainers); + for (int i = containers.size() - 1; i >= 0; i--) { + final TaskFragmentContainer container = containers.get(i); + // Wait until onTaskFragmentAppeared to update new container. + if (!container.isFinished() && !container.isWaitingActivityAppear()) { + updateContainer(wct, container); + } } } /** - * Updates if we should override transition animation. We only want to override if the Task - * bounds is large enough for at least one split rule. + * 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. */ - private void updateAnimationOverride(@NonNull TaskContainer taskContainer) { - if (!taskContainer.isTaskBoundsInitialized() - || !taskContainer.isWindowingModeInitialized()) { - // We don't know about the Task bounds/windowingMode yet. + @VisibleForTesting + @GuardedBy("mLock") + void onActivityReparentedToTask(@NonNull WindowContainerTransaction wct, + int taskId, @NonNull Intent activityIntent, + @NonNull IBinder activityToken) { + // If the activity belongs to the current app process, we treat it as a new activity + // launch. + final Activity activity = getActivity(activityToken); + if (activity != null) { + // We don't allow split as primary for new launch because we currently only support + // launching to top. We allow split as primary for activity reparent because the + // activity may be split as primary before it is reparented out. In that case, we + // want to show it as primary again when it is reparented back. + if (!resolveActivityToContainer(wct, activity, true /* isOnReparent */)) { + // When there is no embedding rule matched, try to place it in the top container + // like a normal launch. + placeActivityInTopContainer(wct, activity); + } return; } - // We only want to override if it supports split. - if (supportSplit(taskContainer)) { - mPresenter.startOverrideSplitAnimation(taskContainer.getTaskId()); - } else { - mPresenter.stopOverrideSplitAnimation(taskContainer.getTaskId()); + 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(). } - private boolean supportSplit(@NonNull TaskContainer taskContainer) { - // No split inside PIP. - if (taskContainer.isInPictureInPicture()) { - return false; + /** + * 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, + @TaskFragmentOperation.OperationType int opType, @NonNull Throwable exception) { + Log.e(TAG, "onTaskFragmentError=" + exception.getMessage()); + switch (opType) { + case OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT: + case 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()) { + mTransactionManager.getCurrentTransactionRecord() + .setOriginType(TASK_FRAGMENT_TRANSIT_CLOSE); + mPresenter.cleanupContainer(wct, container, false /* shouldFinishDependent */); + } + break; + } + default: + Log.e(TAG, "onTaskFragmentError: taskFragmentInfo = " + taskFragmentInfo + + ", opType = " + opType); } - // Check if the parent container bounds can support any split rule. - for (EmbeddingRule rule : mSplitRules) { - if (!(rule instanceof SplitRule)) { + } + + /** Called on receiving {@link #onTaskFragmentVanished} for cleanup. */ + @GuardedBy("mLock") + private void cleanupTaskFragment(@NonNull IBinder taskFragmentToken) { + for (int i = mTaskContainers.size() - 1; i >= 0; i--) { + final TaskContainer taskContainer = mTaskContainers.valueAt(i); + if (!taskContainer.mFinishedContainer.remove(taskFragmentToken)) { continue; } - if (shouldShowSideBySide(taskContainer.getTaskBounds(), (SplitRule) rule)) { - return true; + if (taskContainer.isEmpty()) { + // Cleanup the TaskContainer if it becomes empty. + mTaskContainers.remove(taskContainer.getTaskId()); } + return; } - return false; } @VisibleForTesting - void onActivityCreated(@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 */); + @GuardedBy("mLock") + void onActivityCreated(@NonNull WindowContainerTransaction wct, + @NonNull Activity launchedActivity) { + resolveActivityToContainer(wct, launchedActivity, false /* isOnReparent */); updateCallbackIfNecessary(); } @@ -381,7 +768,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 +778,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. @@ -400,6 +789,13 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen return true; } + final TaskFragmentContainer container = getContainerWithActivity(activity); + if (!isOnReparent && container != null + && container.getTaskContainer().getTopTaskFragmentContainer() != container) { + // Do not resolve if the launched activity is not the top-most container in the Task. + return true; + } + /* * We will check the following to see if there is any embedding rule matched: * 1. Whether the new launched activity should always expand. @@ -413,12 +809,19 @@ 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; + } + + // Skip resolving the following split-rules if the launched activity has been requested + // to be launched into its current container. + if (container != null && container.isActivityInRequestedTaskFragment( + activity.getActivityToken())) { return true; } @@ -433,11 +836,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 +863,21 @@ 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. */ + @GuardedBy("mLock") @VisibleForTesting - void placeActivityInTopContainer(@NonNull Activity activity) { + void placeActivityInTopContainer(@NonNull WindowContainerTransaction wct, + @NonNull Activity activity) { if (getContainerWithActivity(activity) != null) { // The activity has already been put in a TaskFragment. This is likely to be done by // the server when the activity is started. @@ -488,21 +893,25 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen return; } targetContainer.addPendingAppearedActivity(activity); - final WindowContainerTransaction wct = new WindowContainerTransaction(); wct.reparentActivityToTaskFragment(targetContainer.getTaskFragmentToken(), activity.getActivityToken()); - mPresenter.applyTransaction(wct); } /** * Starts an activity to side of the launchingActivity with the provided split config. */ - private void startActivityToSide(@NonNull Activity launchingActivity, @NonNull Intent intent, + // Suppress GuardedBy warning because lint ask to mark this method as + // @GuardedBy(container.mController.mLock), which is mLock itself + @SuppressWarnings("GuardedBy") + @GuardedBy("mLock") + private void startActivityToSide(@NonNull WindowContainerTransaction wct, + @NonNull Activity launchingActivity, @NonNull Intent intent, @Nullable Bundle options, @NonNull SplitRule sideRule, - @Nullable Consumer<Exception> failureCallback, boolean isPlaceholder) { + @NonNull SplitAttributes splitAttributes, @Nullable Consumer<Exception> failureCallback, + boolean isPlaceholder) { try { - mPresenter.startActivityToSide(launchingActivity, intent, options, sideRule, - isPlaceholder); + mPresenter.startActivityToSide(wct, launchingActivity, intent, options, sideRule, + splitAttributes, isPlaceholder); } catch (Exception e) { if (failureCallback != null) { failureCallback.accept(e); @@ -514,19 +923,25 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen * Expands the given activity by either expanding the TaskFragment it is currently in or putting * it into a new expanded TaskFragment. */ - private void expandActivity(@NonNull Activity activity) { + @GuardedBy("mLock") + private void expandActivity(@NonNull WindowContainerTransaction wct, + @NonNull Activity activity) { final TaskFragmentContainer container = getContainerWithActivity(activity); if (shouldContainerBeExpanded(container)) { // Make sure that the existing container is expanded. - mPresenter.expandTaskFragment(container.getTaskFragmentToken()); + mPresenter.expandTaskFragment(wct, container.getTaskFragmentToken()); } else { // Put activity into a new expanded container. final TaskFragmentContainer newContainer = newContainer(activity, getTaskId(activity)); - mPresenter.expandActivity(newContainer.getTaskFragmentToken(), activity); + mPresenter.expandActivity(wct, newContainer.getTaskFragmentToken(), activity); } } /** Whether the given new launched activity is in a split with a rule matched. */ + // Suppress GuardedBy warning because lint asks to mark this method as + // @GuardedBy(mPresenter.mController.mLock), which is mLock itself + @SuppressWarnings("GuardedBy") + @GuardedBy("mLock") private boolean isNewActivityInSplitWithRuleMatched(@NonNull Activity launchedActivity) { final TaskFragmentContainer container = getContainerWithActivity(launchedActivity); final SplitContainer splitContainer = getActiveSplitForContainer(container); @@ -583,10 +998,14 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen /** Finds the activity below the given activity. */ @VisibleForTesting @Nullable + @GuardedBy("mLock") Activity findActivityBelow(@NonNull Activity activity) { Activity activityBelow = null; final TaskFragmentContainer container = getContainerWithActivity(activity); - if (container != null) { + // Looking for the activity below from the information we already have if the container + // only embeds activities of the same process because activities of other processes are not + // available in this embedding host process for security concern. + if (container != null && !container.hasCrossProcessActivities()) { final List<Activity> containerActivities = container.collectNonFinishingActivities(); final int index = containerActivities.indexOf(activity); if (index > 0) { @@ -607,8 +1026,12 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen * Checks if there is a rule to split the two activities. If there is one, puts them into split * and returns {@code true}. Otherwise, returns {@code false}. */ - private boolean putActivitiesIntoSplitIfNecessary(@NonNull Activity primaryActivity, - @NonNull Activity secondaryActivity) { + // Suppress GuardedBy warning because lint ask to mark this method as + // @GuardedBy(mPresenter.mController.mLock), which is mLock itself + @SuppressWarnings("GuardedBy") + @GuardedBy("mLock") + private boolean putActivitiesIntoSplitIfNecessary(@NonNull WindowContainerTransaction wct, + @NonNull Activity primaryActivity, @NonNull Activity secondaryActivity) { final SplitPairRule splitRule = getSplitRule(primaryActivity, secondaryActivity); if (splitRule == null) { return false; @@ -616,8 +1039,15 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen final TaskFragmentContainer primaryContainer = getContainerWithActivity( primaryActivity); final SplitContainer splitContainer = getActiveSplitForContainer(primaryContainer); + final TaskContainer.TaskProperties taskProperties = mPresenter + .getTaskProperties(primaryActivity); + final SplitAttributes calculatedSplitAttributes = mPresenter.computeSplitAttributes( + taskProperties, splitRule, splitRule.getDefaultSplitAttributes(), + getActivitiesMinDimensionsPair(primaryActivity, secondaryActivity)); if (splitContainer != null && primaryContainer == splitContainer.getPrimaryContainer() - && canReuseContainer(splitRule, splitContainer.getSplitRule())) { + && canReuseContainer(splitRule, splitContainer.getSplitRule(), + getTaskWindowMetrics(taskProperties.getConfiguration()), + calculatedSplitAttributes, splitContainer.getCurrentSplitAttributes())) { // Can launch in the existing secondary container if the rules share the same // presentation. final TaskFragmentContainer secondaryContainer = splitContainer.getSecondaryContainer(); @@ -626,23 +1056,24 @@ 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, + calculatedSplitAttributes); 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 +1092,22 @@ 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) { + if (!activity.isFinishing()) { + // onDestroyed is triggered without finishing. This happens when the activity is + // relaunched. In this case, we don't want to cleanup the record. + return; + } // Remove any pending appeared activity, as the server won't send finished activity to the // organizer. + final IBinder activityToken = activity.getActivityToken(); for (int i = mTaskContainers.size() - 1; i >= 0; i--) { - mTaskContainers.valueAt(i).cleanupPendingAppearedActivity(activity); + mTaskContainers.valueAt(i).onActivityDestroyed(activityToken); } // We didn't trigger the callback if there were any pending appeared activities, so check // again after the pending is removed. @@ -680,8 +1118,54 @@ 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 TransactionRecord transactionRecord = mTransactionManager.startNewTransaction(); + onTaskFragmentAppearEmptyTimeout(transactionRecord.getTransaction(), container); + // Can be applied independently as a timeout callback. + transactionRecord.apply(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) { + mTransactionManager.getCurrentTransactionRecord() + .setOriginType(TASK_FRAGMENT_TRANSIT_CLOSE); + mPresenter.cleanupContainer(wct, container, false /* shouldFinishDependent */); + } + + @Nullable + @GuardedBy("mLock") + private TaskFragmentContainer resolveStartActivityIntentFromNonActivityContext( + @NonNull WindowContainerTransaction wct, @NonNull Intent intent) { + final int taskCount = mTaskContainers.size(); + if (taskCount == 0) { + // We don't have other Activity to check split with. + return null; + } + if (taskCount > 1) { + Log.w(TAG, "App is calling startActivity from a non-Activity context when it has" + + " more than one Task. If the new launch Activity is in a different process," + + " and it is expected to be embedded, please start it from an Activity" + + " instead."); + return null; + } + + // Check whether the Intent should be embedded in the known Task. + final TaskContainer taskContainer = mTaskContainers.valueAt(0); + if (taskContainer.isInPictureInPicture() + || taskContainer.getTopNonFinishingActivity() == null) { + // We don't embed activity when it is in PIP, or if we can't find any other owner + // activity in the Task. + return null; + } + + return resolveStartActivityIntent(wct, taskContainer.getTaskId(), intent, + null /* launchingActivity */); } /** @@ -701,6 +1185,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen */ @VisibleForTesting @Nullable + @GuardedBy("mLock") TaskFragmentContainer resolveStartActivityIntent(@NonNull WindowContainerTransaction wct, int taskId, @NonNull Intent intent, @Nullable Activity launchingActivity) { /* @@ -763,6 +1248,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen /** * Returns an empty expanded {@link TaskFragmentContainer} that we can launch an activity into. */ + @GuardedBy("mLock") @Nullable private TaskFragmentContainer createEmptyExpandedContainer( @NonNull WindowContainerTransaction wct, @NonNull Intent intent, int taskId, @@ -786,6 +1272,8 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen taskId); mPresenter.createTaskFragment(wct, expandedContainer.getTaskFragmentToken(), activityInTask.getActivityToken(), new Rect(), WINDOWING_MODE_UNDEFINED); + mPresenter.updateAnimationParams(wct, expandedContainer.getTaskFragmentToken(), + TaskFragmentAnimationParams.DEFAULT); return expandedContainer; } @@ -793,6 +1281,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen * Returns a container for the new activity intent to launch into as splitting with the primary * activity. */ + @GuardedBy("mLock") @Nullable private TaskFragmentContainer getSecondaryContainerForSplitIfAny( @NonNull WindowContainerTransaction wct, @NonNull Activity primaryActivity, @@ -803,8 +1292,16 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen } final TaskFragmentContainer existingContainer = getContainerWithActivity(primaryActivity); final SplitContainer splitContainer = getActiveSplitForContainer(existingContainer); + final TaskContainer.TaskProperties taskProperties = mPresenter + .getTaskProperties(primaryActivity); + final WindowMetrics taskWindowMetrics = getTaskWindowMetrics( + taskProperties.getConfiguration()); + final SplitAttributes calculatedSplitAttributes = mPresenter.computeSplitAttributes( + taskProperties, splitRule, splitRule.getDefaultSplitAttributes(), + getActivityIntentMinDimensionsPair(primaryActivity, intent)); if (splitContainer != null && existingContainer == splitContainer.getPrimaryContainer() - && (canReuseContainer(splitRule, splitContainer.getSplitRule()) + && (canReuseContainer(splitRule, splitContainer.getSplitRule(), taskWindowMetrics, + calculatedSplitAttributes, splitContainer.getCurrentSplitAttributes()) // TODO(b/231845476) we should always respect clearTop. || !respectClearTop) && mPresenter.expandSplitContainerIfNeeded(wct, splitContainer, primaryActivity, @@ -815,23 +1312,40 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen } // Create a new TaskFragment to split with the primary activity for the new activity. return mPresenter.createNewSplitWithEmptySideContainer(wct, primaryActivity, intent, - splitRule); + splitRule, calculatedSplitAttributes); } /** * Returns a container that this activity is registered with. An activity can only belong to one * container, or no container at all. */ + @GuardedBy("mLock") @Nullable TaskFragmentContainer getContainerWithActivity(@NonNull Activity activity) { - final IBinder activityToken = activity.getActivityToken(); + return getContainerWithActivity(activity.getActivityToken()); + } + + @GuardedBy("mLock") + @Nullable + TaskFragmentContainer getContainerWithActivity(@NonNull IBinder activityToken) { + // Check pending appeared activity first because there can be a delay for the server + // update. + for (int i = mTaskContainers.size() - 1; i >= 0; i--) { + final List<TaskFragmentContainer> containers = mTaskContainers.valueAt(i).mContainers; + for (int j = containers.size() - 1; j >= 0; j--) { + final TaskFragmentContainer container = containers.get(j); + if (container.hasPendingAppearedActivity(activityToken)) { + return container; + } + } + } + + // Check appeared activity if there is no such pending appeared activity. for (int i = mTaskContainers.size() - 1; i >= 0; i--) { final List<TaskFragmentContainer> containers = mTaskContainers.valueAt(i).mContainers; - // Traverse from top to bottom in case an activity is added to top pending, and hasn't - // received update from server yet. for (int j = containers.size() - 1; j >= 0; j--) { final TaskFragmentContainer container = containers.get(j); - if (container.hasActivity(activityToken)) { + if (container.hasAppearedActivity(activityToken)) { return container; } } @@ -839,20 +1353,23 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen return null; } + @GuardedBy("mLock") TaskFragmentContainer newContainer(@NonNull Activity pendingAppearedActivity, int taskId) { return newContainer(pendingAppearedActivity, pendingAppearedActivity, taskId); } + @GuardedBy("mLock") TaskFragmentContainer newContainer(@NonNull Activity pendingAppearedActivity, @NonNull Activity activityInTask, int taskId) { return newContainer(pendingAppearedActivity, null /* pendingAppearedIntent */, - activityInTask, taskId); + activityInTask, taskId, null /* pairedPrimaryContainer */); } + @GuardedBy("mLock") TaskFragmentContainer newContainer(@NonNull Intent pendingAppearedIntent, @NonNull Activity activityInTask, int taskId) { return newContainer(null /* pendingAppearedActivity */, pendingAppearedIntent, - activityInTask, taskId); + activityInTask, taskId, null /* pairedPrimaryContainer */); } /** @@ -864,30 +1381,22 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen * @param activityInTask activity in the same Task so that we can get the Task bounds * if needed. * @param taskId parent Task of the new TaskFragment. + * @param pairedPrimaryContainer the paired primary {@link TaskFragmentContainer}. When it is + * set, the new container will be added right above it. */ + @GuardedBy("mLock") TaskFragmentContainer newContainer(@Nullable Activity pendingAppearedActivity, - @Nullable Intent pendingAppearedIntent, @NonNull Activity activityInTask, int taskId) { + @Nullable Intent pendingAppearedIntent, @NonNull Activity activityInTask, int taskId, + @Nullable TaskFragmentContainer pairedPrimaryContainer) { if (activityInTask == null) { throw new IllegalArgumentException("activityInTask must not be null,"); } if (!mTaskContainers.contains(taskId)) { - mTaskContainers.put(taskId, new TaskContainer(taskId)); + mTaskContainers.put(taskId, new TaskContainer(taskId, activityInTask)); } final TaskContainer taskContainer = mTaskContainers.get(taskId); final TaskFragmentContainer container = new TaskFragmentContainer(pendingAppearedActivity, - pendingAppearedIntent, taskContainer, this); - if (!taskContainer.isTaskBoundsInitialized()) { - // Get the initial bounds before the TaskFragment has appeared. - final Rect taskBounds = getNonEmbeddedActivityBounds(activityInTask); - if (!taskContainer.setTaskBounds(taskBounds)) { - Log.w(TAG, "Can't find bounds from activity=" + activityInTask); - } - } - if (!taskContainer.isWindowingModeInitialized()) { - taskContainer.setWindowingMode(activityInTask.getResources().getConfiguration() - .windowConfiguration.getWindowingMode()); - } - updateAnimationOverride(taskContainer); + pendingAppearedIntent, taskContainer, this, pairedPrimaryContainer); return container; } @@ -895,12 +1404,16 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen * Creates and registers a new split with the provided containers and configuration. Finishes * existing secondary containers if found for the given primary container. */ + // Suppress GuardedBy warning because lint ask to mark this method as + // @GuardedBy(mPresenter.mController.mLock), which is mLock itself + @SuppressWarnings("GuardedBy") + @GuardedBy("mLock") void registerSplit(@NonNull WindowContainerTransaction wct, @NonNull TaskFragmentContainer primaryContainer, @NonNull Activity primaryActivity, @NonNull TaskFragmentContainer secondaryContainer, - @NonNull SplitRule splitRule) { + @NonNull SplitRule splitRule, @NonNull SplitAttributes splitAttributes) { final SplitContainer splitContainer = new SplitContainer(primaryContainer, primaryActivity, - secondaryContainer, splitRule); + secondaryContainer, splitRule, splitAttributes); // Remove container later to prevent pinning escaping toast showing in lock task mode. if (splitRule instanceof SplitPairRule && ((SplitPairRule) splitRule).shouldClearTop()) { removeExistingSecondaryContainers(wct, primaryContainer); @@ -909,6 +1422,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(); @@ -948,23 +1462,33 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen * Removes the container from bookkeeping records. */ void removeContainer(@NonNull TaskFragmentContainer container) { + removeContainers(container.getTaskContainer(), Collections.singletonList(container)); + } + + /** + * Removes containers from bookkeeping records. + */ + void removeContainers(@NonNull TaskContainer taskContainer, + @NonNull List<TaskFragmentContainer> containers) { // Remove all split containers that included this one - final TaskContainer taskContainer = container.getTaskContainer(); - if (taskContainer == null) { - return; - } - taskContainer.mContainers.remove(container); + taskContainer.mContainers.removeAll(containers); // Marked as a pending removal which will be removed after it is actually removed on the // server side (#onTaskFragmentVanished). // In this way, we can keep track of the Task bounds until we no longer have any // TaskFragment there. - taskContainer.mFinishedContainer.add(container.getTaskFragmentToken()); + taskContainer.mFinishedContainer.addAll(containers.stream().map( + TaskFragmentContainer::getTaskFragmentToken).toList()); // Cleanup any split references. final List<SplitContainer> containersToRemove = new ArrayList<>(); for (SplitContainer splitContainer : taskContainer.mSplitContainers) { - if (container.equals(splitContainer.getSecondaryContainer()) - || container.equals(splitContainer.getPrimaryContainer())) { + if (containersToRemove.contains(splitContainer)) { + // Don't need to check because it has been in the remove list. + continue; + } + if (containers.stream().anyMatch(container -> + splitContainer.getPrimaryContainer().equals(container) + || splitContainer.getSecondaryContainer().equals(container))) { containersToRemove.add(splitContainer); } } @@ -972,7 +1496,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen // Cleanup any dependent references. for (TaskFragmentContainer containerToUpdate : taskContainer.mContainers) { - containerToUpdate.removeContainerToFinishOnExit(container); + containerToUpdate.removeContainersToFinishOnExit(containers); } } @@ -980,7 +1504,12 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen * Removes a secondary container for the given primary container if an existing split is * already registered. */ - void removeExistingSecondaryContainers(@NonNull WindowContainerTransaction wct, + // Suppress GuardedBy warning because lint asks to mark this method as + // @GuardedBy(existingSplitContainer.getSecondaryContainer().mController.mLock), which is mLock + // itself + @SuppressWarnings("GuardedBy") + @GuardedBy("mLock") + private void removeExistingSecondaryContainers(@NonNull WindowContainerTransaction wct, @NonNull TaskFragmentContainer primaryContainer) { // If the primary container was already in a split - remove the secondary container that // is now covered by the new one that replaced it. @@ -998,6 +1527,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen /** * Returns the topmost not finished container in Task of given task id. */ + @GuardedBy("mLock") @Nullable TaskFragmentContainer getTopActiveContainer(int taskId) { final TaskContainer taskContainer = mTaskContainers.get(taskId); @@ -1022,9 +1552,15 @@ 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 (!container.getTaskContainer().isVisible()) { + // Wait until the Task is visible to avoid unnecessary update when the Task is still in + // background. + return; + } + if (launchPlaceholderIfNecessary(wct, container)) { // Placeholder was launched, the positions will be updated when the activity is added // to the secondary container. return; @@ -1040,20 +1576,54 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen if (splitContainer == null) { return; } + + updateSplitContainerIfNeeded(splitContainer, wct, null /* splitAttributes */); + } + + /** + * Updates {@link SplitContainer} with the given {@link SplitAttributes} if the + * {@link SplitContainer} is the top most and not finished. If passed {@link SplitAttributes} + * are {@code null}, the {@link SplitAttributes} will be calculated with + * {@link SplitPresenter#computeSplitAttributes(TaskContainer.TaskProperties, SplitRule, Pair)}. + * + * @param splitContainer The {@link SplitContainer} to update + * @param splitAttributes Update with this {@code splitAttributes} if it is not {@code null}. + * Otherwise, use the value calculated by + * {@link SplitPresenter#computeSplitAttributes( + * TaskContainer.TaskProperties, SplitRule, Pair)} + * + * @return {@code true} if the update succeed. Otherwise, returns {@code false}. + */ + @VisibleForTesting + @GuardedBy("mLock") + boolean updateSplitContainerIfNeeded(@NonNull SplitContainer splitContainer, + @NonNull WindowContainerTransaction wct, @Nullable SplitAttributes splitAttributes) { if (!isTopMostSplit(splitContainer)) { // Skip position update - it isn't the topmost split. - return; + return false; } if (splitContainer.getPrimaryContainer().isFinished() || splitContainer.getSecondaryContainer().isFinished()) { // Skip position update - one or both containers are finished. - return; + return false; } - if (dismissPlaceholderIfNecessary(splitContainer)) { + if (splitAttributes == null) { + final TaskContainer.TaskProperties taskProperties = splitContainer.getTaskContainer() + .getTaskProperties(); + final SplitRule splitRule = splitContainer.getSplitRule(); + final SplitAttributes defaultSplitAttributes = splitContainer + .getDefaultSplitAttributes(); + final Pair<Size, Size> minDimensionsPair = splitContainer.getMinDimensionsPair(); + splitAttributes = mPresenter.computeSplitAttributes(taskProperties, splitRule, + defaultSplitAttributes, minDimensionsPair); + } + splitContainer.updateCurrentSplitAttributes(splitAttributes); + if (dismissPlaceholderIfNecessary(wct, splitContainer)) { // Placeholder was finished, the positions will be updated when its container is emptied - return; + return true; } - mPresenter.updateSplitContainer(splitContainer, container, wct); + mPresenter.updateSplitContainer(splitContainer, wct); + return true; } /** Whether the given split is the topmost split in the Task. */ @@ -1089,7 +1659,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen * Returns the active split that has the provided containers as primary and secondary or as * secondary and primary, if available. */ - @VisibleForTesting + @GuardedBy("mLock") @Nullable SplitContainer getActiveSplitForContainers( @NonNull TaskFragmentContainer firstContainer, @@ -1111,29 +1681,30 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen /** * Checks if the container requires a placeholder and launches it if necessary. */ - private boolean launchPlaceholderIfNecessary(@NonNull TaskFragmentContainer container) { + @GuardedBy("mLock") + private boolean launchPlaceholderIfNecessary(@NonNull WindowContainerTransaction wct, + @NonNull TaskFragmentContainer container) { final Activity topActivity = container.getTopNonFinishingActivity(); if (topActivity == null) { return false; } - return launchPlaceholderIfNecessary(topActivity, false /* isOnCreated */); + return launchPlaceholderIfNecessary(wct, topActivity, false /* isOnCreated */); } - boolean launchPlaceholderIfNecessary(@NonNull Activity activity, boolean isOnCreated) { + // Suppress GuardedBy warning because lint ask to mark this method as + // @GuardedBy(mPresenter.mController.mLock), which is mLock itself + @SuppressWarnings("GuardedBy") + @GuardedBy("mLock") + boolean launchPlaceholderIfNecessary(@NonNull WindowContainerTransaction wct, + @NonNull Activity activity, boolean isOnCreated) { if (activity.isFinishing()) { return false; } final TaskFragmentContainer container = getContainerWithActivity(activity); - // Don't launch placeholder if the container is occluded. - if (container != null && container != getTopActiveContainer(container.getTaskId())) { - return false; - } - - final SplitContainer splitContainer = getActiveSplitForContainer(container); - if (splitContainer != null && container.equals(splitContainer.getPrimaryContainer())) { - // Don't launch placeholder in primary split container + if (container != null && !allowLaunchPlaceholder(container)) { + // We don't allow activity in this TaskFragment to launch placeholder. return false; } @@ -1144,18 +1715,46 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen return false; } + final TaskContainer.TaskProperties taskProperties = mPresenter.getTaskProperties(activity); final Pair<Size, Size> minDimensionsPair = getActivityIntentMinDimensionsPair(activity, placeholderRule.getPlaceholderIntent()); - if (!shouldShowSideBySide( - mPresenter.getParentContainerBounds(activity), placeholderRule, - minDimensionsPair)) { + final SplitAttributes splitAttributes = mPresenter.computeSplitAttributes(taskProperties, + placeholderRule, placeholderRule.getDefaultSplitAttributes(), minDimensionsPair); + if (!SplitPresenter.shouldShowSplit(splitAttributes)) { return false; } // TODO(b/190433398): Handle failed request final Bundle options = getPlaceholderOptions(activity, isOnCreated); - startActivityToSide(activity, placeholderRule.getPlaceholderIntent(), options, - placeholderRule, null /* failureCallback */, true /* isPlaceholder */); + startActivityToSide(wct, activity, placeholderRule.getPlaceholderIntent(), options, + placeholderRule, splitAttributes, null /* failureCallback */, + true /* isPlaceholder */); + return true; + } + + /** Whether or not to allow activity in this container to launch placeholder. */ + @GuardedBy("mLock") + private boolean allowLaunchPlaceholder(@NonNull TaskFragmentContainer container) { + final TaskFragmentContainer topContainer = getTopActiveContainer(container.getTaskId()); + if (container != topContainer) { + // The container is not the top most. + if (!container.isVisible()) { + // In case the container is visible (the one on top may be transparent), we may + // still want to launch placeholder even if it is not the top most. + return false; + } + if (topContainer.isWaitingActivityAppear()) { + // When the top container appeared info is not sent by the server yet, the visible + // check above may not be reliable. + return false; + } + } + + final SplitContainer splitContainer = getActiveSplitForContainer(container); + if (splitContainer != null && container.equals(splitContainer.getPrimaryContainer())) { + // Don't launch placeholder for primary split container. + return false; + } return true; } @@ -1166,6 +1765,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen * @param isOnCreated whether this happens during the primary activity onCreated. */ @VisibleForTesting + @GuardedBy("mLock") @Nullable Bundle getPlaceholderOptions(@NonNull Activity primaryActivity, boolean isOnCreated) { // Setting avoid move to front will also skip the animation. We only want to do that when @@ -1173,6 +1773,9 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen // Check if the primary is resumed or if this is called when the primary is onCreated // (not resumed yet). if (isOnCreated || primaryActivity.isResumed()) { + // Only set trigger type if the launch happens in foreground. + mTransactionManager.getCurrentTransactionRecord() + .setOriginType(TASK_FRAGMENT_TRANSIT_OPEN); return null; } final ActivityOptions options = ActivityOptions.makeBasic(); @@ -1180,8 +1783,13 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen return options.toBundle(); } + // Suppress GuardedBy warning because lint ask to mark this method as + // @GuardedBy(mPresenter.mController.mLock), which is mLock itself + @SuppressWarnings("GuardedBy") @VisibleForTesting - boolean dismissPlaceholderIfNecessary(@NonNull SplitContainer splitContainer) { + @GuardedBy("mLock") + boolean dismissPlaceholderIfNecessary(@NonNull WindowContainerTransaction wct, + @NonNull SplitContainer splitContainer) { if (!splitContainer.isPlaceholderContainer()) { return false; } @@ -1190,12 +1798,14 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen // The placeholder should remain after it was first shown. return false; } - - if (shouldShowSideBySide(splitContainer)) { + final SplitAttributes splitAttributes = splitContainer.getCurrentSplitAttributes(); + if (SplitPresenter.shouldShowSplit(splitAttributes)) { return false; } - mPresenter.cleanupContainer(splitContainer.getSecondaryContainer(), + mTransactionManager.getCurrentTransactionRecord() + .setOriginType(TASK_FRAGMENT_TRANSIT_CLOSE); + mPresenter.cleanupContainer(wct, splitContainer.getSecondaryContainer(), false /* shouldFinishDependent */); return true; } @@ -1204,6 +1814,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen * Returns the rule to launch a placeholder for the activity with the provided component name * if it is configured in the split config. */ + @GuardedBy("mLock") private SplitPlaceholderRule getPlaceholderRule(@NonNull Activity activity) { for (EmbeddingRule rule : mSplitRules) { if (!(rule instanceof SplitPlaceholderRule)) { @@ -1220,15 +1831,14 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen /** * Notifies listeners about changes to split states if necessary. */ - private void updateCallbackIfNecessary() { - if (mEmbeddingCallback == null) { - return; - } - if (!allActivitiesCreated()) { + @VisibleForTesting + @GuardedBy("mLock") + void updateCallbackIfNecessary() { + if (mEmbeddingCallback == null || !readyToReportToClient()) { return; } - List<SplitInfo> currentSplitStates = getActiveSplitStates(); - if (currentSplitStates == null || mLastReportedSplitStates.equals(currentSplitStates)) { + final List<SplitInfo> currentSplitStates = getActiveSplitStates(); + if (mLastReportedSplitStates.equals(currentSplitStates)) { return; } mLastReportedSplitStates.clear(); @@ -1237,52 +1847,27 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen } /** - * @return a list of descriptors for currently active split states. If the value returned is - * null, that indicates that the active split states are in an intermediate state and should - * not be reported. + * Returns a list of descriptors for currently active split states. */ - @Nullable + @GuardedBy("mLock") + @NonNull private List<SplitInfo> getActiveSplitStates() { - List<SplitInfo> splitStates = new ArrayList<>(); + final List<SplitInfo> splitStates = new ArrayList<>(); for (int i = mTaskContainers.size() - 1; i >= 0; i--) { - final List<SplitContainer> splitContainers = mTaskContainers.valueAt(i) - .mSplitContainers; - for (SplitContainer container : splitContainers) { - if (container.getPrimaryContainer().isEmpty() - || container.getSecondaryContainer().isEmpty()) { - // We are in an intermediate state because either the split container is about - // to be removed or the primary or secondary container are about to receive an - // activity. - return null; - } - final ActivityStack primaryContainer = container.getPrimaryContainer() - .toActivityStack(); - final ActivityStack secondaryContainer = container.getSecondaryContainer() - .toActivityStack(); - final SplitInfo splitState = new SplitInfo(primaryContainer, secondaryContainer, - // Splits that are not showing side-by-side are reported as having 0 split - // ratio, since by definition in the API the primary container occupies no - // width of the split when covered by the secondary. - shouldShowSideBySide(container) - ? container.getSplitRule().getSplitRatio() - : 0.0f); - splitStates.add(splitState); - } + mTaskContainers.valueAt(i).getSplitStates(splitStates); } return splitStates; } /** - * Checks if all activities that are registered with the containers have already appeared in - * the client. + * Whether we can now report the split states to the client. */ - private boolean allActivitiesCreated() { + @GuardedBy("mLock") + private boolean readyToReportToClient() { for (int i = mTaskContainers.size() - 1; i >= 0; i--) { - final List<TaskFragmentContainer> containers = mTaskContainers.valueAt(i).mContainers; - for (TaskFragmentContainer container : containers) { - if (!container.taskInfoActivityCountMatchesCreated()) { - return false; - } + if (mTaskContainers.valueAt(i).isInIntermediateState()) { + // If any Task is in an intermediate state, wait for the server update. + return false; } } return true; @@ -1303,6 +1888,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen * Returns a split rule for the provided pair of primary activity and secondary activity intent * if available. */ + @GuardedBy("mLock") @Nullable private SplitPairRule getSplitRule(@NonNull Activity primaryActivity, @NonNull Intent secondaryActivityIntent) { @@ -1321,6 +1907,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen /** * Returns a split rule for the provided pair of primary and secondary activities if available. */ + @GuardedBy("mLock") @Nullable private SplitPairRule getSplitRule(@NonNull Activity primaryActivity, @NonNull Activity secondaryActivity) { @@ -1340,6 +1927,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen } @Nullable + @GuardedBy("mLock") TaskFragmentContainer getContainer(@NonNull IBinder fragmentToken) { for (int i = mTaskContainers.size() - 1; i >= 0; i--) { final List<TaskFragmentContainer> containers = mTaskContainers.valueAt(i).mContainers; @@ -1352,7 +1940,23 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen return null; } + @VisibleForTesting + @Nullable + @GuardedBy("mLock") + SplitContainer getSplitContainer(@NonNull IBinder token) { + for (int i = mTaskContainers.size() - 1; i >= 0; i--) { + final List<SplitContainer> containers = mTaskContainers.valueAt(i).mSplitContainers; + for (SplitContainer container : containers) { + if (container.getToken().equals(token)) { + return container; + } + } + } + return null; + } + @Nullable + @GuardedBy("mLock") TaskContainer getTaskContainer(int taskId) { return mTaskContainers.get(taskId); } @@ -1361,6 +1965,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen return mHandler; } + @GuardedBy("mLock") int getTaskId(@NonNull Activity activity) { // Prefer to get the taskId from TaskFragmentContainer because Activity.getTaskId() is an // IPC call. @@ -1373,22 +1978,29 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen return ActivityThread.currentActivityThread().getActivity(activityToken); } + @VisibleForTesting + ActivityStartMonitor getActivityStartMonitor() { + return mActivityStartMonitor; + } + /** - * Gets the token of the initial TaskFragment that embedded this activity. Do not rely on it - * after creation because the activity could be reparented. + * Gets the token of the TaskFragment that embedded this activity. It is available as soon as + * the activity is created and attached, so it can be used during {@link #onActivityCreated} + * before the server notifies the organizer to avoid racing condition. */ @VisibleForTesting @Nullable - IBinder getInitialTaskFragmentToken(@NonNull Activity activity) { + IBinder getTaskFragmentTokenFromActivityClientRecord(@NonNull Activity activity) { final ActivityThread.ActivityClientRecord record = ActivityThread.currentActivityThread() .getActivityClient(activity.getActivityToken()); - return record != null ? record.mInitialTaskFragmentToken : null; + return record != null ? record.mTaskFragmentToken : null; } /** * Returns {@code true} if an Activity with the provided component name should always be * expanded to occupy full task bounds. Such activity must not be put in a split. */ + @GuardedBy("mLock") private boolean shouldExpand(@Nullable Activity activity, @Nullable Intent intent) { for (EmbeddingRule rule : mSplitRules) { if (!(rule instanceof ActivityRule)) { @@ -1414,6 +2026,10 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen * 'sticky' and the placeholder was finished when fully overlapping the primary container. * @return {@code true} if the associated container should be retained (and not be finished). */ + // Suppress GuardedBy warning because lint ask to mark this method as + // @GuardedBy(mPresenter.mController.mLock), which is mLock itself + @SuppressWarnings("GuardedBy") + @GuardedBy("mLock") boolean shouldRetainAssociatedContainer(@NonNull TaskFragmentContainer finishingContainer, @NonNull TaskFragmentContainer associatedContainer) { SplitContainer splitContainer = getActiveSplitForContainers(associatedContainer, @@ -1432,7 +2048,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen } // Decide whether the associated container should be retained based on the current // presentation mode. - if (shouldShowSideBySide(splitContainer)) { + if (shouldShowSplit(splitContainer)) { return !shouldFinishAssociatedContainerWhenAdjacent(finishBehavior); } else { return !shouldFinishAssociatedContainerWhenStacked(finishBehavior); @@ -1442,6 +2058,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen /** * @see #shouldRetainAssociatedContainer(TaskFragmentContainer, TaskFragmentContainer) */ + @GuardedBy("mLock") boolean shouldRetainAssociatedActivity(@NonNull TaskFragmentContainer finishingContainer, @NonNull Activity associatedActivity) { final TaskFragmentContainer associatedContainer = getContainerWithActivity( @@ -1456,10 +2073,17 @@ 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) { + if (activity.isChild()) { + // Skip Activity that is child of another Activity (ActivityGroup) because it's + // window will just be a child of the parent Activity window. + return; + } 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) { @@ -1473,6 +2097,12 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen if (!container.hasActivity(activityToken) && container.getTaskFragmentToken() .equals(initialTaskFragmentToken)) { + if (ActivityClient.getInstance().isRequestedToLaunchInTaskFragment( + activityToken, initialTaskFragmentToken)) { + container.addPendingAppearedInRequestedTaskFragmentActivity( + activity); + } + // The onTaskFragmentInfoChanged callback containing this activity has // not reached the client yet, so add the activity to the pending // appeared activities. @@ -1485,25 +2115,53 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen } @Override - public void onActivityPostCreated(Activity activity, Bundle savedInstanceState) { + public void onActivityPostCreated(@NonNull Activity activity, + @Nullable Bundle savedInstanceState) { + if (activity.isChild()) { + // Skip Activity that is child of another Activity (ActivityGroup) because it's + // window will just be a child of the parent Activity window. + return; + } // 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 TransactionRecord transactionRecord = mTransactionManager + .startNewTransaction(); + transactionRecord.setOriginType(TASK_FRAGMENT_TRANSIT_OPEN); + SplitController.this.onActivityCreated(transactionRecord.getTransaction(), + activity); + // The WCT should be applied and merged to the activity launch transition. + transactionRecord.apply(false /* shouldApplyIndependently */); } } @Override - public void onActivityConfigurationChanged(Activity activity) { + public void onActivityConfigurationChanged(@NonNull Activity activity) { + if (activity.isChild()) { + // Skip Activity that is child of another Activity (ActivityGroup) because it's + // window will just be a child of the parent Activity window. + return; + } synchronized (mLock) { - SplitController.this.onActivityConfigurationChanged(activity); + final TransactionRecord transactionRecord = mTransactionManager + .startNewTransaction(); + SplitController.this.onActivityConfigurationChanged( + transactionRecord.getTransaction(), activity); + // The WCT should be applied and merged to the Task change transition so that the + // placeholder is launched in the same transition. + transactionRecord.apply(false /* shouldApplyIndependently */); } } @Override - public void onActivityPostDestroyed(Activity activity) { + public void onActivityPostDestroyed(@NonNull Activity activity) { + if (activity.isChild()) { + // Skip Activity that is child of another Activity (ActivityGroup) because it's + // window will just be a child of the parent Activity window. + return; + } synchronized (mLock) { SplitController.this.onActivityDestroyed(activity); } @@ -1515,7 +2173,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 +2182,99 @@ 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 + @GuardedBy("mLock") + 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. + // TODO(b/232042367): Consolidate the activity create handling so that we can handle + // cross-process the same as normal. - // Check if activity should be put in a split with the activity that launched it. - if (!(who instanceof Activity)) { + // Early return if the launching taskfragment is already been set. + if (options.getBinder(ActivityOptions.KEY_LAUNCH_TASK_FRAGMENT_TOKEN) != null) { + synchronized (mLock) { + mCurrentIntent = intent; + } 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); + + final Activity launchingActivity; + if (who instanceof Activity) { + // We will check if the new activity should be split with the activity that launched + // it. + final Activity activity = (Activity) who; + // For Activity that is child of another Activity (ActivityGroup), treat the parent + // Activity as the launching one because it's window will just be a child of the + // parent Activity window. + launchingActivity = activity.isChild() ? activity.getParent() : activity; + 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 TransactionRecord transactionRecord = mTransactionManager + .startNewTransaction(); + transactionRecord.setOriginType(TASK_FRAGMENT_TRANSIT_OPEN); + final WindowContainerTransaction wct = transactionRecord.getTransaction(); + 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. + transactionRecord.apply(false /* shouldApplyIndependently */); // Amend the request to let the WM know that the activity should be placed in // the dedicated container. + // TODO(b/229680885): skip override launching TaskFragment token by split-rule options.putBinder(ActivityOptions.KEY_LAUNCH_TASK_FRAGMENT_TOKEN, launchedInTaskFragment.getTaskFragmentToken()); + mCurrentIntent = intent; + } else { + transactionRecord.abort(); } } return super.onStartActivity(who, intent, options); } + + @Override + public void onStartActivityResult(int result, @NonNull Bundle bOptions) { + super.onStartActivityResult(result, bOptions); + synchronized (mLock) { + 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; + } + } } /** @@ -1571,32 +2289,51 @@ 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. + * If the two rules have the same presentation, and the calculated {@link SplitAttributes} + * matches the {@link SplitAttributes} of {@link SplitContainer}, we can reuse the same + * {@link SplitContainer} if there is any. */ - private static boolean canReuseContainer(SplitRule rule1, SplitRule rule2) { + private static boolean canReuseContainer(@NonNull SplitRule rule1, @NonNull SplitRule rule2, + @NonNull WindowMetrics parentWindowMetrics, + @NonNull SplitAttributes calculatedSplitAttributes, + @NonNull SplitAttributes containerSplitAttributes) { if (!isContainerReusableRule(rule1) || !isContainerReusableRule(rule2)) { return false; } - return haveSamePresentation((SplitPairRule) rule1, (SplitPairRule) rule2); + return areRulesSamePresentation((SplitPairRule) rule1, (SplitPairRule) rule2, + parentWindowMetrics) + // Besides rules, we should also check whether the SplitContainer's splitAttributes + // matches the current splitAttributes or not. The splitAttributes may change + // if the app chooses different SplitAttributes calculator function before a new + // activity is started even they match the same splitRule. + && calculatedSplitAttributes.equals(containerSplitAttributes); } /** Whether the two rules have the same presentation. */ - private static boolean haveSamePresentation(SplitPairRule rule1, SplitPairRule rule2) { + @VisibleForTesting + static boolean areRulesSamePresentation(@NonNull SplitPairRule rule1, + @NonNull SplitPairRule rule2, @NonNull WindowMetrics parentWindowMetrics) { + if (rule1.getTag() != null || rule2.getTag() != null) { + // Tag must be unique if it is set. We don't want to reuse the container if the rules + // have different tags because they can have different SplitAttributes later through + // SplitAttributesCalculator. + return Objects.equals(rule1.getTag(), rule2.getTag()); + } + // If both rules don't have tag, compare all SplitRules' properties that may affect their + // SplitAttributes. // TODO(b/231655482): add util method to do the comparison in SplitPairRule. - return rule1.getSplitRatio() == rule2.getSplitRatio() - && rule1.getLayoutDirection() == rule2.getLayoutDirection() - && rule1.getFinishPrimaryWithSecondary() - == rule2.getFinishPrimaryWithSecondary() - && rule1.getFinishSecondaryWithPrimary() - == rule2.getFinishSecondaryWithPrimary(); + return rule1.getDefaultSplitAttributes().equals(rule2.getDefaultSplitAttributes()) + && rule1.checkParentMetrics(parentWindowMetrics) + == rule2.checkParentMetrics(parentWindowMetrics) + && rule1.getFinishPrimaryWithSecondary() == rule2.getFinishPrimaryWithSecondary() + && rule1.getFinishSecondaryWithPrimary() == rule2.getFinishSecondaryWithPrimary(); } /** * Whether it is ok for other rule to reuse the {@link TaskFragmentContainer} of the given * rule. */ - private static boolean isContainerReusableRule(SplitRule rule) { + private static boolean isContainerReusableRule(@NonNull SplitRule rule) { // We don't expect to reuse the placeholder rule. if (!(rule instanceof SplitPairRule)) { return false; diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java index a89847a30d20..53d39d9fa28e 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java @@ -22,34 +22,53 @@ import android.app.Activity; import android.app.ActivityThread; import android.app.WindowConfiguration; import android.app.WindowConfiguration.WindowingMode; -import android.content.Context; import android.content.Intent; import android.content.pm.ActivityInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; +import android.content.res.Configuration; import android.graphics.Rect; import android.os.Bundle; import android.os.IBinder; +import android.util.DisplayMetrics; import android.util.LayoutDirection; import android.util.Pair; import android.util.Size; import android.view.View; import android.view.WindowInsets; import android.view.WindowMetrics; +import android.window.TaskFragmentAnimationParams; +import android.window.TaskFragmentCreationParams; import android.window.WindowContainerTransaction; import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.window.extensions.core.util.function.Function; +import androidx.window.extensions.embedding.SplitAttributes.SplitType; +import androidx.window.extensions.embedding.SplitAttributes.SplitType.ExpandContainersSplitType; +import androidx.window.extensions.embedding.SplitAttributes.SplitType.HingeSplitType; +import androidx.window.extensions.embedding.SplitAttributes.SplitType.RatioSplitType; +import androidx.window.extensions.embedding.TaskContainer.TaskProperties; +import androidx.window.extensions.layout.DisplayFeature; +import androidx.window.extensions.layout.FoldingFeature; +import androidx.window.extensions.layout.WindowLayoutComponentImpl; +import androidx.window.extensions.layout.WindowLayoutInfo; import com.android.internal.annotations.VisibleForTesting; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; import java.util.concurrent.Executor; /** * Controls the visual presentation of the splits according to the containers formed by * {@link SplitController}. + * + * Note that all calls into this class must hold the {@link SplitController} internal lock. */ +@SuppressWarnings("GuardedBy") class SplitPresenter extends JetpackTaskFragmentOrganizer { @VisibleForTesting static final int POSITION_START = 0; @@ -65,11 +84,25 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { }) private @interface Position {} + private static final int CONTAINER_POSITION_LEFT = 0; + private static final int CONTAINER_POSITION_TOP = 1; + private static final int CONTAINER_POSITION_RIGHT = 2; + private static final int CONTAINER_POSITION_BOTTOM = 3; + + @IntDef(value = { + CONTAINER_POSITION_LEFT, + CONTAINER_POSITION_TOP, + CONTAINER_POSITION_RIGHT, + CONTAINER_POSITION_BOTTOM, + }) + private @interface ContainerPosition {} + /** * Result of {@link #expandSplitContainerIfNeeded(WindowContainerTransaction, SplitContainer, * Activity, Activity, Intent)}. * No need to expand the splitContainer because screen is big enough to - * {@link #shouldShowSideBySide(Rect, SplitRule, Pair)} and minimum dimensions is satisfied. + * {@link #shouldShowSplit(SplitAttributes)} and minimum dimensions is + * satisfied. */ static final int RESULT_NOT_EXPANDED = 0; /** @@ -77,7 +110,7 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { * Activity, Activity, Intent)}. * The splitContainer should be expanded. It is usually because minimum dimensions is not * satisfied. - * @see #shouldShowSideBySide(Rect, SplitRule, Pair) + * @see #shouldShowSplit(SplitAttributes) */ static final int RESULT_EXPANDED = 1; /** @@ -100,46 +133,38 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { }) private @interface ResultCode {} + @VisibleForTesting + static final SplitAttributes EXPAND_CONTAINERS_ATTRIBUTES = + new SplitAttributes.Builder() + .setSplitType(new ExpandContainersSplitType()) + .build(); + + private final WindowLayoutComponentImpl mWindowLayoutComponent; private final SplitController mController; - SplitPresenter(@NonNull Executor executor, SplitController controller) { + SplitPresenter(@NonNull Executor executor, + @NonNull WindowLayoutComponentImpl windowLayoutComponent, + @NonNull SplitController controller) { super(executor, controller); + mWindowLayoutComponent = windowLayoutComponent; 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); + if (!SplitController.ENABLE_SHELL_TRANSITIONS) { + // TODO(b/207070762): cleanup with legacy app transition + // Animation will be handled by WM Shell when Shell transition is enabled. + overrideSplitAnimation(); + } } /** * 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( - container.getTaskId()); - if (newTopContainer != null) { - mController.updateContainer(wct, newTopContainer); - } + // Make sure the containers in the Task is up-to-date. + mController.updateContainersInTaskIfVisible(wct, container.getTaskId()); } /** @@ -149,32 +174,32 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { @NonNull TaskFragmentContainer createNewSplitWithEmptySideContainer( @NonNull WindowContainerTransaction wct, @NonNull Activity primaryActivity, - @NonNull Intent secondaryIntent, @NonNull SplitPairRule rule) { - final Rect parentBounds = getParentContainerBounds(primaryActivity); - final Pair<Size, Size> minDimensionsPair = getActivityIntentMinDimensionsPair( - primaryActivity, secondaryIntent); - final Rect primaryRectBounds = getBoundsForPosition(POSITION_START, parentBounds, rule, - primaryActivity, minDimensionsPair); + @NonNull Intent secondaryIntent, @NonNull SplitPairRule rule, + @NonNull SplitAttributes splitAttributes) { + final TaskProperties taskProperties = getTaskProperties(primaryActivity); + final Rect primaryRelBounds = getRelBoundsForPosition(POSITION_START, taskProperties, + splitAttributes); final TaskFragmentContainer primaryContainer = prepareContainerForActivity(wct, - primaryActivity, primaryRectBounds, null); + primaryActivity, primaryRelBounds, splitAttributes, null /* containerToAvoid */); // Create new empty task fragment final int taskId = primaryContainer.getTaskId(); final TaskFragmentContainer secondaryContainer = mController.newContainer( secondaryIntent, primaryActivity, taskId); - final Rect secondaryRectBounds = getBoundsForPosition(POSITION_END, parentBounds, - rule, primaryActivity, minDimensionsPair); + final Rect secondaryRelBounds = getRelBoundsForPosition(POSITION_END, taskProperties, + splitAttributes); final int windowingMode = mController.getTaskContainer(taskId) - .getWindowingModeForSplitTaskFragment(secondaryRectBounds); + .getWindowingModeForSplitTaskFragment(secondaryRelBounds); createTaskFragment(wct, secondaryContainer.getTaskFragmentToken(), - primaryActivity.getActivityToken(), secondaryRectBounds, - windowingMode); + primaryActivity.getActivityToken(), secondaryRelBounds, windowingMode); + updateAnimationParams(wct, secondaryContainer.getTaskFragmentToken(), splitAttributes); // Set adjacent to each other so that the containers below will be invisible. setAdjacentTaskFragments(wct, primaryContainer, secondaryContainer, rule, - minDimensionsPair); + splitAttributes); - mController.registerSplit(wct, primaryContainer, primaryActivity, secondaryContainer, rule); + mController.registerSplit(wct, primaryContainer, primaryActivity, secondaryContainer, rule, + splitAttributes); return secondaryContainer; } @@ -189,38 +214,37 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { * same container as the primary activity, a new container will be * created and the activity will be re-parented to it. * @param rule The split rule to be applied to the container. + * @param splitAttributes The {@link SplitAttributes} to apply */ - void createNewSplitContainer(@NonNull Activity primaryActivity, - @NonNull Activity secondaryActivity, @NonNull SplitPairRule rule) { - final WindowContainerTransaction wct = new WindowContainerTransaction(); - - final Rect parentBounds = getParentContainerBounds(primaryActivity); - final Pair<Size, Size> minDimensionsPair = getActivitiesMinDimensionsPair(primaryActivity, - secondaryActivity); - final Rect primaryRectBounds = getBoundsForPosition(POSITION_START, parentBounds, rule, - primaryActivity, minDimensionsPair); + void createNewSplitContainer(@NonNull WindowContainerTransaction wct, + @NonNull Activity primaryActivity, @NonNull Activity secondaryActivity, + @NonNull SplitPairRule rule, @NonNull SplitAttributes splitAttributes) { + final TaskProperties taskProperties = getTaskProperties(primaryActivity); + final Rect primaryRelBounds = getRelBoundsForPosition(POSITION_START, taskProperties, + splitAttributes); final TaskFragmentContainer primaryContainer = prepareContainerForActivity(wct, - primaryActivity, primaryRectBounds, null); + primaryActivity, primaryRelBounds, splitAttributes, null /* containerToAvoid */); - final Rect secondaryRectBounds = getBoundsForPosition(POSITION_END, parentBounds, rule, - primaryActivity, minDimensionsPair); + final Rect secondaryRelBounds = getRelBoundsForPosition(POSITION_END, taskProperties, + splitAttributes); final TaskFragmentContainer curSecondaryContainer = mController.getContainerWithActivity( secondaryActivity); TaskFragmentContainer containerToAvoid = primaryContainer; - if (rule.shouldClearTop() && curSecondaryContainer != null) { - // Do not reuse the current TaskFragment if the rule is to clear top. + if (curSecondaryContainer != null && curSecondaryContainer != primaryContainer + && (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, - secondaryActivity, secondaryRectBounds, containerToAvoid); + secondaryActivity, secondaryRelBounds, splitAttributes, containerToAvoid); // Set adjacent to each other so that the containers below will be invisible. setAdjacentTaskFragments(wct, primaryContainer, secondaryContainer, rule, - minDimensionsPair); + splitAttributes); - mController.registerSplit(wct, primaryContainer, primaryActivity, secondaryContainer, rule); - - applyTransaction(wct); + mController.registerSplit(wct, primaryContainer, primaryActivity, secondaryContainer, rule, + splitAttributes); } /** @@ -230,23 +254,26 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { */ private TaskFragmentContainer prepareContainerForActivity( @NonNull WindowContainerTransaction wct, @NonNull Activity activity, - @NonNull Rect bounds, @Nullable TaskFragmentContainer containerToAvoid) { + @NonNull Rect relBounds, @NonNull SplitAttributes splitAttributes, + @Nullable TaskFragmentContainer containerToAvoid) { TaskFragmentContainer container = mController.getContainerWithActivity(activity); final int taskId = container != null ? container.getTaskId() : activity.getTaskId(); if (container == null || container == containerToAvoid) { container = mController.newContainer(activity, taskId); final int windowingMode = mController.getTaskContainer(taskId) - .getWindowingModeForSplitTaskFragment(bounds); - createTaskFragment(wct, container.getTaskFragmentToken(), activity.getActivityToken(), - bounds, windowingMode); + .getWindowingModeForSplitTaskFragment(relBounds); + final IBinder reparentActivityToken = activity.getActivityToken(); + createTaskFragment(wct, container.getTaskFragmentToken(), reparentActivityToken, + relBounds, windowingMode, reparentActivityToken); wct.reparentActivityToTaskFragment(container.getTaskFragmentToken(), - activity.getActivityToken()); + reparentActivityToken); } else { - resizeTaskFragmentIfRegistered(wct, container, bounds); + resizeTaskFragmentIfRegistered(wct, container, relBounds); final int windowingMode = mController.getTaskContainer(taskId) - .getWindowingModeForSplitTaskFragment(bounds); + .getWindowingModeForSplitTaskFragment(relBounds); updateTaskFragmentWindowingModeIfRegistered(wct, container, windowingMode); } + updateAnimationParams(wct, container.getTaskFragmentToken(), splitAttributes); return container; } @@ -262,15 +289,15 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { * @param rule The split rule to be applied to the container. * @param isPlaceholder Whether the launch is a placeholder. */ - void startActivityToSide(@NonNull Activity launchingActivity, @NonNull Intent activityIntent, - @Nullable Bundle activityOptions, @NonNull SplitRule rule, boolean isPlaceholder) { - final Rect parentBounds = getParentContainerBounds(launchingActivity); - final Pair<Size, Size> minDimensionsPair = getActivityIntentMinDimensionsPair( - launchingActivity, activityIntent); - final Rect primaryRectBounds = getBoundsForPosition(POSITION_START, parentBounds, rule, - launchingActivity, minDimensionsPair); - final Rect secondaryRectBounds = getBoundsForPosition(POSITION_END, parentBounds, rule, - launchingActivity, minDimensionsPair); + void startActivityToSide(@NonNull WindowContainerTransaction wct, + @NonNull Activity launchingActivity, @NonNull Intent activityIntent, + @Nullable Bundle activityOptions, @NonNull SplitRule rule, + @NonNull SplitAttributes splitAttributes, boolean isPlaceholder) { + final TaskProperties taskProperties = getTaskProperties(launchingActivity); + final Rect primaryRelBounds = getRelBoundsForPosition(POSITION_START, taskProperties, + splitAttributes); + final Rect secondaryRelBounds = getRelBoundsForPosition(POSITION_END, taskProperties, + splitAttributes); TaskFragmentContainer primaryContainer = mController.getContainerWithActivity( launchingActivity); @@ -280,82 +307,85 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { } final int taskId = primaryContainer.getTaskId(); - final TaskFragmentContainer secondaryContainer = mController.newContainer(activityIntent, - launchingActivity, taskId); - final int windowingMode = mController.getTaskContainer(taskId) - .getWindowingModeForSplitTaskFragment(primaryRectBounds); - final WindowContainerTransaction wct = new WindowContainerTransaction(); + final TaskFragmentContainer secondaryContainer = mController.newContainer( + null /* pendingAppearedActivity */, activityIntent, launchingActivity, taskId, + // Pass in the primary container to make sure it is added right above the primary. + primaryContainer); + final TaskContainer taskContainer = mController.getTaskContainer(taskId); + final int windowingMode = taskContainer.getWindowingModeForSplitTaskFragment( + primaryRelBounds); mController.registerSplit(wct, primaryContainer, launchingActivity, secondaryContainer, - rule); - startActivityToSide(wct, primaryContainer.getTaskFragmentToken(), primaryRectBounds, - launchingActivity, secondaryContainer.getTaskFragmentToken(), secondaryRectBounds, - activityIntent, activityOptions, rule, windowingMode); + rule, splitAttributes); + startActivityToSide(wct, primaryContainer.getTaskFragmentToken(), primaryRelBounds, + launchingActivity, secondaryContainer.getTaskFragmentToken(), secondaryRelBounds, + activityIntent, activityOptions, rule, windowingMode, splitAttributes); if (isPlaceholder) { // When placeholder is launched in split, we should keep the focus on the primary. wct.requestFocusOnTaskFragment(primaryContainer.getTaskFragmentToken()); } - applyTransaction(wct); } /** * Updates the positions of containers in an existing split. * @param splitContainer The split container to be updated. - * @param updatedContainer The task fragment that was updated and caused this split update. * @param wct WindowContainerTransaction that this update should be performed with. */ void updateSplitContainer(@NonNull SplitContainer splitContainer, - @NonNull TaskFragmentContainer updatedContainer, @NonNull WindowContainerTransaction wct) { - // Getting the parent bounds using the updated container - it will have the recent value. - final Rect parentBounds = getParentContainerBounds(updatedContainer); + // Getting the parent configuration using the updated container - it will have the recent + // value. final SplitRule rule = splitContainer.getSplitRule(); final TaskFragmentContainer primaryContainer = splitContainer.getPrimaryContainer(); final Activity activity = primaryContainer.getTopNonFinishingActivity(); if (activity == null) { return; } - final Pair<Size, Size> minDimensionsPair = splitContainer.getMinDimensionsPair(); - final Rect primaryRectBounds = getBoundsForPosition(POSITION_START, parentBounds, rule, - activity, minDimensionsPair); - final Rect secondaryRectBounds = getBoundsForPosition(POSITION_END, parentBounds, rule, - activity, minDimensionsPair); + final TaskContainer taskContainer = splitContainer.getTaskContainer(); + final TaskProperties taskProperties = taskContainer.getTaskProperties(); + final SplitAttributes splitAttributes = splitContainer.getCurrentSplitAttributes(); + final Rect primaryRelBounds = getRelBoundsForPosition(POSITION_START, taskProperties, + splitAttributes); + final Rect secondaryRelBounds = getRelBoundsForPosition(POSITION_END, taskProperties, + splitAttributes); final TaskFragmentContainer secondaryContainer = splitContainer.getSecondaryContainer(); // Whether the placeholder is becoming side-by-side with the primary from fullscreen. final boolean isPlaceholderBecomingSplit = splitContainer.isPlaceholderContainer() && secondaryContainer.areLastRequestedBoundsEqual(null /* bounds */) - && !secondaryRectBounds.isEmpty(); + && !secondaryRelBounds.isEmpty(); // If the task fragments are not registered yet, the positions will be updated after they // are created again. - resizeTaskFragmentIfRegistered(wct, primaryContainer, primaryRectBounds); - resizeTaskFragmentIfRegistered(wct, secondaryContainer, secondaryRectBounds); + resizeTaskFragmentIfRegistered(wct, primaryContainer, primaryRelBounds); + resizeTaskFragmentIfRegistered(wct, secondaryContainer, secondaryRelBounds); setAdjacentTaskFragments(wct, primaryContainer, secondaryContainer, rule, - minDimensionsPair); + splitAttributes); if (isPlaceholderBecomingSplit) { // When placeholder is shown in split, we should keep the focus on the primary. wct.requestFocusOnTaskFragment(primaryContainer.getTaskFragmentToken()); } - final TaskContainer taskContainer = updatedContainer.getTaskContainer(); final int windowingMode = taskContainer.getWindowingModeForSplitTaskFragment( - primaryRectBounds); + primaryRelBounds); updateTaskFragmentWindowingModeIfRegistered(wct, primaryContainer, windowingMode); updateTaskFragmentWindowingModeIfRegistered(wct, secondaryContainer, windowingMode); + updateAnimationParams(wct, primaryContainer.getTaskFragmentToken(), splitAttributes); + updateAnimationParams(wct, secondaryContainer.getTaskFragmentToken(), splitAttributes); } private void setAdjacentTaskFragments(@NonNull WindowContainerTransaction wct, @NonNull TaskFragmentContainer primaryContainer, @NonNull TaskFragmentContainer secondaryContainer, @NonNull SplitRule splitRule, - @NonNull Pair<Size, Size> minDimensionsPair) { - final Rect parentBounds = getParentContainerBounds(primaryContainer); + @NonNull SplitAttributes splitAttributes) { // Clear adjacent TaskFragments if the container is shown in fullscreen, or the // secondaryContainer could not be finished. - if (!shouldShowSideBySide(parentBounds, splitRule, minDimensionsPair)) { - setAdjacentTaskFragments(wct, primaryContainer.getTaskFragmentToken(), - null /* secondary */, null /* splitRule */); + boolean isStacked = !shouldShowSplit(splitAttributes); + if (isStacked) { + clearAdjacentTaskFragments(wct, primaryContainer.getTaskFragmentToken()); } else { - setAdjacentTaskFragments(wct, primaryContainer.getTaskFragmentToken(), + setAdjacentTaskFragmentsWithRule(wct, primaryContainer.getTaskFragmentToken(), secondaryContainer.getTaskFragmentToken(), splitRule); } + setCompanionTaskFragment(wct, primaryContainer.getTaskFragmentToken(), + secondaryContainer.getTaskFragmentToken(), splitRule, isStacked); } /** @@ -363,13 +393,13 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { * creation has not been reported from the server yet. */ // TODO(b/190433398): Handle resize if the fragment hasn't appeared yet. - void resizeTaskFragmentIfRegistered(@NonNull WindowContainerTransaction wct, + private void resizeTaskFragmentIfRegistered(@NonNull WindowContainerTransaction wct, @NonNull TaskFragmentContainer container, - @Nullable Rect bounds) { + @Nullable Rect relBounds) { if (container.getInfo() == null) { return; } - resizeTaskFragment(wct, container.getTaskFragmentToken(), bounds); + resizeTaskFragment(wct, container.getTaskFragmentToken(), relBounds); } private void updateTaskFragmentWindowingModeIfRegistered( @@ -382,35 +412,36 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { } @Override - void createTaskFragment(@NonNull WindowContainerTransaction wct, @NonNull IBinder fragmentToken, - @NonNull IBinder ownerToken, @NonNull Rect bounds, @WindowingMode int windowingMode) { - final TaskFragmentContainer container = mController.getContainer(fragmentToken); + void createTaskFragment(@NonNull WindowContainerTransaction wct, + @NonNull TaskFragmentCreationParams fragmentOptions) { + final TaskFragmentContainer container = mController.getContainer( + fragmentOptions.getFragmentToken()); if (container == null) { throw new IllegalStateException( - "Creating a task fragment that is not registered with controller."); + "Creating a TaskFragment that is not registered with controller."); } - container.setLastRequestedBounds(bounds); - container.setLastRequestedWindowingMode(windowingMode); - super.createTaskFragment(wct, fragmentToken, ownerToken, bounds, windowingMode); + container.setLastRequestedBounds(fragmentOptions.getInitialRelativeBounds()); + container.setLastRequestedWindowingMode(fragmentOptions.getWindowingMode()); + super.createTaskFragment(wct, fragmentOptions); } @Override void resizeTaskFragment(@NonNull WindowContainerTransaction wct, @NonNull IBinder fragmentToken, - @Nullable Rect bounds) { + @Nullable Rect relBounds) { TaskFragmentContainer container = mController.getContainer(fragmentToken); if (container == null) { throw new IllegalStateException( - "Resizing a task fragment that is not registered with controller."); + "Resizing a TaskFragment that is not registered with controller."); } - if (container.areLastRequestedBoundsEqual(bounds)) { + if (container.areLastRequestedBoundsEqual(relBounds)) { // Return early if the provided bounds were already requested return; } - container.setLastRequestedBounds(bounds); - super.resizeTaskFragment(wct, fragmentToken, bounds); + container.setLastRequestedBounds(relBounds); + super.resizeTaskFragment(wct, fragmentToken, relBounds); } @Override @@ -418,7 +449,7 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { @NonNull IBinder fragmentToken, @WindowingMode int windowingMode) { final TaskFragmentContainer container = mController.getContainer(fragmentToken); if (container == null) { - throw new IllegalStateException("Setting windowing mode for a task fragment that is" + throw new IllegalStateException("Setting windowing mode for a TaskFragment that is" + " not registered with controller."); } @@ -431,12 +462,89 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { super.updateWindowingMode(wct, fragmentToken, windowingMode); } + @Override + void updateAnimationParams(@NonNull WindowContainerTransaction wct, + @NonNull IBinder fragmentToken, @NonNull TaskFragmentAnimationParams animationParams) { + final TaskFragmentContainer container = mController.getContainer(fragmentToken); + if (container == null) { + throw new IllegalStateException("Setting animation params for a TaskFragment that is" + + " not registered with controller."); + } + + if (container.areLastRequestedAnimationParamsEqual(animationParams)) { + // Return early if the animation params were already requested + return; + } + + container.setLastRequestAnimationParams(animationParams); + super.updateAnimationParams(wct, fragmentToken, animationParams); + } + + @Override + void setAdjacentTaskFragments(@NonNull WindowContainerTransaction wct, + @NonNull IBinder primary, @NonNull IBinder secondary, + @Nullable WindowContainerTransaction.TaskFragmentAdjacentParams adjacentParams) { + final TaskFragmentContainer primaryContainer = mController.getContainer(primary); + final TaskFragmentContainer secondaryContainer = mController.getContainer(secondary); + if (primaryContainer == null || secondaryContainer == null) { + throw new IllegalStateException("setAdjacentTaskFragments on TaskFragment that is" + + " not registered with controller."); + } + + if (primaryContainer.isLastAdjacentTaskFragmentEqual(secondary, adjacentParams) + && secondaryContainer.isLastAdjacentTaskFragmentEqual(primary, adjacentParams)) { + // Return early if the same adjacent TaskFragments were already requested + return; + } + + primaryContainer.setLastAdjacentTaskFragment(secondary, adjacentParams); + secondaryContainer.setLastAdjacentTaskFragment(primary, adjacentParams); + super.setAdjacentTaskFragments(wct, primary, secondary, adjacentParams); + } + + @Override + void clearAdjacentTaskFragments(@NonNull WindowContainerTransaction wct, + @NonNull IBinder fragmentToken) { + final TaskFragmentContainer container = mController.getContainer(fragmentToken); + if (container == null) { + throw new IllegalStateException("clearAdjacentTaskFragments on TaskFragment that is" + + " not registered with controller."); + } + + if (container.isLastAdjacentTaskFragmentEqual(null /* fragmentToken*/, null /* params */)) { + // Return early if no adjacent TaskFragment was yet requested + return; + } + + container.clearLastAdjacentTaskFragment(); + super.clearAdjacentTaskFragments(wct, fragmentToken); + } + + @Override + void setCompanionTaskFragment(@NonNull WindowContainerTransaction wct, @NonNull IBinder primary, + @Nullable IBinder secondary) { + final TaskFragmentContainer container = mController.getContainer(primary); + if (container == null) { + throw new IllegalStateException("setCompanionTaskFragment on TaskFragment that is" + + " not registered with controller."); + } + + if (container.isLastCompanionTaskFragmentEqual(secondary)) { + // Return early if the same companion TaskFragment was already requested + return; + } + + container.setLastCompanionTaskFragment(secondary); + super.setCompanionTaskFragment(wct, primary, secondary); + } + /** * Expands the split container if the current split bounds are smaller than the Activity or * Intent that is added to the container. * - * @return the {@link ResultCode} based on {@link #shouldShowSideBySide(Rect, SplitRule, Pair)} - * and if {@link android.window.TaskFragmentInfo} has reported to the client side. + * @return the {@link ResultCode} based on + * {@link #shouldShowSplit(SplitAttributes)} and if + * {@link android.window.TaskFragmentInfo} has reported to the client side. */ @ResultCode int expandSplitContainerIfNeeded(@NonNull WindowContainerTransaction wct, @@ -446,7 +554,6 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { throw new IllegalArgumentException("Either secondaryActivity or secondaryIntent must be" + " non-null."); } - final Rect taskBounds = getParentContainerBounds(primaryActivity); final Pair<Size, Size> minDimensionsPair; if (secondaryActivity != null) { minDimensionsPair = getActivitiesMinDimensionsPair(primaryActivity, secondaryActivity); @@ -455,61 +562,100 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { secondaryIntent); } // Expand the splitContainer if minimum dimensions are not satisfied. - if (!shouldShowSideBySide(taskBounds, splitContainer.getSplitRule(), minDimensionsPair)) { + final TaskContainer taskContainer = splitContainer.getTaskContainer(); + final SplitAttributes splitAttributes = sanitizeSplitAttributes( + taskContainer.getTaskProperties(), splitContainer.getCurrentSplitAttributes(), + minDimensionsPair); + splitContainer.updateCurrentSplitAttributes(splitAttributes); + if (!shouldShowSplit(splitAttributes)) { // If the client side hasn't received TaskFragmentInfo yet, we can't change TaskFragment // bounds. Return failure to create a new SplitContainer which fills task bounds. if (splitContainer.getPrimaryContainer().getInfo() == null || splitContainer.getSecondaryContainer().getInfo() == null) { return RESULT_EXPAND_FAILED_NO_TF_INFO; } - expandTaskFragment(wct, splitContainer.getPrimaryContainer().getTaskFragmentToken()); - expandTaskFragment(wct, splitContainer.getSecondaryContainer().getTaskFragmentToken()); + final IBinder primaryToken = + splitContainer.getPrimaryContainer().getTaskFragmentToken(); + final IBinder secondaryToken = + splitContainer.getSecondaryContainer().getTaskFragmentToken(); + expandTaskFragment(wct, primaryToken); + expandTaskFragment(wct, secondaryToken); + // Set the companion TaskFragment when the two containers stacked. + setCompanionTaskFragment(wct, primaryToken, secondaryToken, + splitContainer.getSplitRule(), true /* isStacked */); return RESULT_EXPANDED; } return RESULT_NOT_EXPANDED; } - static boolean shouldShowSideBySide(@NonNull Rect parentBounds, @NonNull SplitRule rule) { - return shouldShowSideBySide(parentBounds, rule, null /* minimumDimensionPair */); + static boolean shouldShowSplit(@NonNull SplitContainer splitContainer) { + return shouldShowSplit(splitContainer.getCurrentSplitAttributes()); } - static boolean shouldShowSideBySide(@NonNull SplitContainer splitContainer) { - final Rect parentBounds = getParentContainerBounds(splitContainer.getPrimaryContainer()); - - return shouldShowSideBySide(parentBounds, splitContainer.getSplitRule(), - splitContainer.getMinDimensionsPair()); + static boolean shouldShowSplit(@NonNull SplitAttributes splitAttributes) { + return !(splitAttributes.getSplitType() instanceof ExpandContainersSplitType); } - static boolean shouldShowSideBySide(@NonNull Rect parentBounds, @NonNull SplitRule rule, + @NonNull + SplitAttributes computeSplitAttributes(@NonNull TaskProperties taskProperties, + @NonNull SplitRule rule, @NonNull SplitAttributes defaultSplitAttributes, @Nullable Pair<Size, Size> minDimensionsPair) { - // TODO(b/190433398): Supply correct insets. - final WindowMetrics parentMetrics = new WindowMetrics(parentBounds, - new WindowInsets(new Rect())); - // Don't show side by side if bounds is not qualified. - if (!rule.checkParentMetrics(parentMetrics)) { - return false; + final Configuration taskConfiguration = taskProperties.getConfiguration(); + final WindowMetrics taskWindowMetrics = getTaskWindowMetrics(taskConfiguration); + final Function<SplitAttributesCalculatorParams, SplitAttributes> calculator = + mController.getSplitAttributesCalculator(); + final boolean areDefaultConstraintsSatisfied = rule.checkParentMetrics(taskWindowMetrics); + if (calculator == null) { + if (!areDefaultConstraintsSatisfied) { + return EXPAND_CONTAINERS_ATTRIBUTES; + } + return sanitizeSplitAttributes(taskProperties, defaultSplitAttributes, + minDimensionsPair); } - final float splitRatio = rule.getSplitRatio(); - // We only care the size of the bounds regardless of its position. - final Rect primaryBounds = getPrimaryBounds(parentBounds, splitRatio, true /* isLtr */); - final Rect secondaryBounds = getSecondaryBounds(parentBounds, splitRatio, true /* isLtr */); + final WindowLayoutInfo windowLayoutInfo = mWindowLayoutComponent + .getCurrentWindowLayoutInfo(taskProperties.getDisplayId(), + taskConfiguration.windowConfiguration); + final SplitAttributesCalculatorParams params = new SplitAttributesCalculatorParams( + taskWindowMetrics, taskConfiguration, windowLayoutInfo, defaultSplitAttributes, + areDefaultConstraintsSatisfied, rule.getTag()); + final SplitAttributes splitAttributes = calculator.apply(params); + return sanitizeSplitAttributes(taskProperties, splitAttributes, minDimensionsPair); + } + /** + * Returns {@link #EXPAND_CONTAINERS_ATTRIBUTES} if the passed {@link SplitAttributes} doesn't + * meet the minimum dimensions set in {@link ActivityInfo.WindowLayout}. Otherwise, returns + * the passed {@link SplitAttributes}. + */ + @NonNull + private SplitAttributes sanitizeSplitAttributes(@NonNull TaskProperties taskProperties, + @NonNull SplitAttributes splitAttributes, + @Nullable Pair<Size, Size> minDimensionsPair) { if (minDimensionsPair == null) { - return true; + return splitAttributes; } - return !boundsSmallerThanMinDimensions(primaryBounds, minDimensionsPair.first) - && !boundsSmallerThanMinDimensions(secondaryBounds, minDimensionsPair.second); + final FoldingFeature foldingFeature = getFoldingFeature(taskProperties); + final Configuration taskConfiguration = taskProperties.getConfiguration(); + final Rect primaryBounds = getPrimaryBounds(taskConfiguration, splitAttributes, + foldingFeature); + final Rect secondaryBounds = getSecondaryBounds(taskConfiguration, splitAttributes, + foldingFeature); + if (boundsSmallerThanMinDimensions(primaryBounds, minDimensionsPair.first) + || boundsSmallerThanMinDimensions(secondaryBounds, minDimensionsPair.second)) { + return EXPAND_CONTAINERS_ATTRIBUTES; + } + return splitAttributes; } @NonNull - static Pair<Size, Size> getActivitiesMinDimensionsPair(Activity primaryActivity, - Activity secondaryActivity) { + static Pair<Size, Size> getActivitiesMinDimensionsPair( + @NonNull Activity primaryActivity, @NonNull Activity secondaryActivity) { return new Pair<>(getMinDimensions(primaryActivity), getMinDimensions(secondaryActivity)); } @NonNull - static Pair<Size, Size> getActivityIntentMinDimensionsPair(Activity primaryActivity, - Intent secondaryIntent) { + static Pair<Size, Size> getActivityIntentMinDimensionsPair(@NonNull Activity primaryActivity, + @NonNull Intent secondaryIntent) { return new Pair<>(getMinDimensions(primaryActivity), getMinDimensions(secondaryIntent)); } @@ -549,7 +695,7 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { return new Size(windowLayout.minWidth, windowLayout.minHeight); } - static boolean boundsSmallerThanMinDimensions(@NonNull Rect bounds, + private static boolean boundsSmallerThanMinDimensions(@NonNull Rect bounds, @Nullable Size minDimensions) { if (minDimensions == null) { return false; @@ -560,103 +706,344 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { @VisibleForTesting @NonNull - static Rect getBoundsForPosition(@Position int position, @NonNull Rect parentBounds, - @NonNull SplitRule rule, @NonNull Activity primaryActivity, - @Nullable Pair<Size, Size> minDimensionsPair) { - if (!shouldShowSideBySide(parentBounds, rule, minDimensionsPair)) { + Rect getRelBoundsForPosition(@Position int position, @NonNull TaskProperties taskProperties, + @NonNull SplitAttributes splitAttributes) { + final Configuration taskConfiguration = taskProperties.getConfiguration(); + final FoldingFeature foldingFeature = getFoldingFeature(taskProperties); + if (!shouldShowSplit(splitAttributes)) { return new Rect(); } - final boolean isLtr = isLtr(primaryActivity, rule); - final float splitRatio = rule.getSplitRatio(); - + final Rect bounds; switch (position) { case POSITION_START: - return getPrimaryBounds(parentBounds, splitRatio, isLtr); + bounds = getPrimaryBounds(taskConfiguration, splitAttributes, foldingFeature); + break; case POSITION_END: - return getSecondaryBounds(parentBounds, splitRatio, isLtr); + bounds = getSecondaryBounds(taskConfiguration, splitAttributes, foldingFeature); + break; case POSITION_FILL: default: - return new Rect(); + bounds = new Rect(); } + // Convert to relative bounds in parent coordinate. This is to avoid flicker when the Task + // resized before organizer requests have been applied. + taskProperties.translateAbsoluteBoundsToRelativeBounds(bounds); + return bounds; } @NonNull - private static Rect getPrimaryBounds(@NonNull Rect parentBounds, float splitRatio, - boolean isLtr) { - return isLtr ? getLeftContainerBounds(parentBounds, splitRatio) - : getRightContainerBounds(parentBounds, 1 - splitRatio); + private Rect getPrimaryBounds(@NonNull Configuration taskConfiguration, + @NonNull SplitAttributes splitAttributes, @Nullable FoldingFeature foldingFeature) { + final SplitAttributes computedSplitAttributes = updateSplitAttributesType(splitAttributes, + computeSplitType(splitAttributes, taskConfiguration, foldingFeature)); + if (!shouldShowSplit(computedSplitAttributes)) { + return new Rect(); + } + switch (computedSplitAttributes.getLayoutDirection()) { + case SplitAttributes.LayoutDirection.LEFT_TO_RIGHT: { + return getLeftContainerBounds(taskConfiguration, computedSplitAttributes, + foldingFeature); + } + case SplitAttributes.LayoutDirection.RIGHT_TO_LEFT: { + return getRightContainerBounds(taskConfiguration, computedSplitAttributes, + foldingFeature); + } + case SplitAttributes.LayoutDirection.LOCALE: { + final boolean isLtr = taskConfiguration.getLayoutDirection() + == View.LAYOUT_DIRECTION_LTR; + return isLtr + ? getLeftContainerBounds(taskConfiguration, computedSplitAttributes, + foldingFeature) + : getRightContainerBounds(taskConfiguration, computedSplitAttributes, + foldingFeature); + } + case SplitAttributes.LayoutDirection.TOP_TO_BOTTOM: { + return getTopContainerBounds(taskConfiguration, computedSplitAttributes, + foldingFeature); + } + case SplitAttributes.LayoutDirection.BOTTOM_TO_TOP: { + return getBottomContainerBounds(taskConfiguration, computedSplitAttributes, + foldingFeature); + } + default: + throw new IllegalArgumentException("Unknown layout direction:" + + computedSplitAttributes.getLayoutDirection()); + } } @NonNull - private static Rect getSecondaryBounds(@NonNull Rect parentBounds, float splitRatio, - boolean isLtr) { - return isLtr ? getRightContainerBounds(parentBounds, splitRatio) - : getLeftContainerBounds(parentBounds, 1 - splitRatio); + private Rect getSecondaryBounds(@NonNull Configuration taskConfiguration, + @NonNull SplitAttributes splitAttributes, @Nullable FoldingFeature foldingFeature) { + final SplitAttributes computedSplitAttributes = updateSplitAttributesType(splitAttributes, + computeSplitType(splitAttributes, taskConfiguration, foldingFeature)); + if (!shouldShowSplit(computedSplitAttributes)) { + return new Rect(); + } + switch (computedSplitAttributes.getLayoutDirection()) { + case SplitAttributes.LayoutDirection.LEFT_TO_RIGHT: { + return getRightContainerBounds(taskConfiguration, computedSplitAttributes, + foldingFeature); + } + case SplitAttributes.LayoutDirection.RIGHT_TO_LEFT: { + return getLeftContainerBounds(taskConfiguration, computedSplitAttributes, + foldingFeature); + } + case SplitAttributes.LayoutDirection.LOCALE: { + final boolean isLtr = taskConfiguration.getLayoutDirection() + == View.LAYOUT_DIRECTION_LTR; + return isLtr + ? getRightContainerBounds(taskConfiguration, computedSplitAttributes, + foldingFeature) + : getLeftContainerBounds(taskConfiguration, computedSplitAttributes, + foldingFeature); + } + case SplitAttributes.LayoutDirection.TOP_TO_BOTTOM: { + return getBottomContainerBounds(taskConfiguration, computedSplitAttributes, + foldingFeature); + } + case SplitAttributes.LayoutDirection.BOTTOM_TO_TOP: { + return getTopContainerBounds(taskConfiguration, computedSplitAttributes, + foldingFeature); + } + default: + throw new IllegalArgumentException("Unknown layout direction:" + + splitAttributes.getLayoutDirection()); + } + } + + /** + * Returns the {@link SplitAttributes} that update the {@link SplitType} to + * {@code splitTypeToUpdate}. + */ + private static SplitAttributes updateSplitAttributesType( + @NonNull SplitAttributes splitAttributes, @NonNull SplitType splitTypeToUpdate) { + return new SplitAttributes.Builder() + .setSplitType(splitTypeToUpdate) + .setLayoutDirection(splitAttributes.getLayoutDirection()) + .setAnimationBackgroundColor(splitAttributes.getAnimationBackgroundColor()) + .build(); + } + + @NonNull + private Rect getLeftContainerBounds(@NonNull Configuration taskConfiguration, + @NonNull SplitAttributes splitAttributes, @Nullable FoldingFeature foldingFeature) { + final int right = computeBoundaryBetweenContainers(taskConfiguration, splitAttributes, + CONTAINER_POSITION_LEFT, foldingFeature); + final Rect taskBounds = taskConfiguration.windowConfiguration.getBounds(); + return new Rect(taskBounds.left, taskBounds.top, right, taskBounds.bottom); + } + + @NonNull + private Rect getRightContainerBounds(@NonNull Configuration taskConfiguration, + @NonNull SplitAttributes splitAttributes, @Nullable FoldingFeature foldingFeature) { + final int left = computeBoundaryBetweenContainers(taskConfiguration, splitAttributes, + CONTAINER_POSITION_RIGHT, foldingFeature); + final Rect parentBounds = taskConfiguration.windowConfiguration.getBounds(); + return new Rect(left, parentBounds.top, parentBounds.right, parentBounds.bottom); } - private static Rect getLeftContainerBounds(@NonNull Rect parentBounds, float splitRatio) { - return new Rect( - parentBounds.left, - parentBounds.top, - (int) (parentBounds.left + parentBounds.width() * splitRatio), - parentBounds.bottom); + @NonNull + private Rect getTopContainerBounds(@NonNull Configuration taskConfiguration, + @NonNull SplitAttributes splitAttributes, @Nullable FoldingFeature foldingFeature) { + final int bottom = computeBoundaryBetweenContainers(taskConfiguration, splitAttributes, + CONTAINER_POSITION_TOP, foldingFeature); + final Rect parentBounds = taskConfiguration.windowConfiguration.getBounds(); + return new Rect(parentBounds.left, parentBounds.top, parentBounds.right, bottom); } - private static Rect getRightContainerBounds(@NonNull Rect parentBounds, float splitRatio) { - return new Rect( - (int) (parentBounds.left + parentBounds.width() * splitRatio), - parentBounds.top, - parentBounds.right, - parentBounds.bottom); + @NonNull + private Rect getBottomContainerBounds(@NonNull Configuration taskConfiguration, + @NonNull SplitAttributes splitAttributes, @Nullable FoldingFeature foldingFeature) { + final int top = computeBoundaryBetweenContainers(taskConfiguration, splitAttributes, + CONTAINER_POSITION_BOTTOM, foldingFeature); + final Rect parentBounds = taskConfiguration.windowConfiguration.getBounds(); + return new Rect(parentBounds.left, top, parentBounds.right, parentBounds.bottom); } /** - * Checks if a split with the provided rule should be displays in left-to-right layout - * direction, either always or with the current configuration. + * Computes the boundary position between the primary and the secondary containers for the given + * {@link ContainerPosition} with {@link SplitAttributes}, current window and device states. + * <ol> + * <li>For {@link #CONTAINER_POSITION_TOP}, it computes the boundary with the bottom + * container, which is {@link Rect#bottom} of the top container bounds.</li> + * <li>For {@link #CONTAINER_POSITION_BOTTOM}, it computes the boundary with the top + * container, which is {@link Rect#top} of the bottom container bounds.</li> + * <li>For {@link #CONTAINER_POSITION_LEFT}, it computes the boundary with the right + * container, which is {@link Rect#right} of the left container bounds.</li> + * <li>For {@link #CONTAINER_POSITION_RIGHT}, it computes the boundary with the bottom + * container, which is {@link Rect#left} of the right container bounds.</li> + * </ol> + * + * @see #getTopContainerBounds(Configuration, SplitAttributes, FoldingFeature) + * @see #getBottomContainerBounds(Configuration, SplitAttributes, FoldingFeature) + * @see #getLeftContainerBounds(Configuration, SplitAttributes, FoldingFeature) + * @see #getRightContainerBounds(Configuration, SplitAttributes, FoldingFeature) */ - private static boolean isLtr(@NonNull Context context, @NonNull SplitRule rule) { - switch (rule.getLayoutDirection()) { - case LayoutDirection.LOCALE: - return context.getResources().getConfiguration().getLayoutDirection() - == View.LAYOUT_DIRECTION_LTR; - case LayoutDirection.RTL: - return false; - case LayoutDirection.LTR: + private int computeBoundaryBetweenContainers(@NonNull Configuration taskConfiguration, + @NonNull SplitAttributes splitAttributes, @ContainerPosition int position, + @Nullable FoldingFeature foldingFeature) { + final Rect parentBounds = taskConfiguration.windowConfiguration.getBounds(); + final int startPoint = shouldSplitHorizontally(splitAttributes) + ? parentBounds.top + : parentBounds.left; + final int dimen = shouldSplitHorizontally(splitAttributes) + ? parentBounds.height() + : parentBounds.width(); + final SplitType splitType = splitAttributes.getSplitType(); + if (splitType instanceof RatioSplitType) { + final RatioSplitType splitRatio = (RatioSplitType) splitType; + return (int) (startPoint + dimen * splitRatio.getRatio()); + } + // At this point, SplitType must be a HingeSplitType and foldingFeature must be + // non-null. RatioSplitType and ExpandContainerSplitType have been handled earlier. + Objects.requireNonNull(foldingFeature); + if (!(splitType instanceof HingeSplitType)) { + throw new IllegalArgumentException("Unknown splitType:" + splitType); + } + final Rect hingeArea = foldingFeature.getBounds(); + switch (position) { + case CONTAINER_POSITION_LEFT: + return hingeArea.left; + case CONTAINER_POSITION_TOP: + return hingeArea.top; + case CONTAINER_POSITION_RIGHT: + return hingeArea.right; + case CONTAINER_POSITION_BOTTOM: + return hingeArea.bottom; default: - return true; + throw new IllegalArgumentException("Unknown position:" + position); } } - @NonNull - static Rect getParentContainerBounds(@NonNull TaskFragmentContainer container) { - return container.getTaskContainer().getTaskBounds(); + @Nullable + @VisibleForTesting + FoldingFeature getFoldingFeature(@NonNull TaskProperties taskProperties) { + final int displayId = taskProperties.getDisplayId(); + final WindowConfiguration windowConfiguration = taskProperties.getConfiguration() + .windowConfiguration; + final WindowLayoutInfo info = mWindowLayoutComponent + .getCurrentWindowLayoutInfo(displayId, windowConfiguration); + final List<DisplayFeature> displayFeatures = info.getDisplayFeatures(); + if (displayFeatures.isEmpty()) { + return null; + } + final List<FoldingFeature> foldingFeatures = new ArrayList<>(); + for (DisplayFeature displayFeature : displayFeatures) { + if (displayFeature instanceof FoldingFeature) { + foldingFeatures.add((FoldingFeature) displayFeature); + } + } + // TODO(b/240219484): Support device with multiple hinges. + if (foldingFeatures.size() != 1) { + return null; + } + return foldingFeatures.get(0); } - @NonNull - Rect getParentContainerBounds(@NonNull Activity activity) { - final TaskFragmentContainer container = mController.getContainerWithActivity(activity); - if (container != null) { - return getParentContainerBounds(container); + /** + * Indicates that this {@link SplitAttributes} splits the task horizontally. Returns + * {@code false} if this {@link SplitAttributes} splits the task vertically. + */ + private static boolean shouldSplitHorizontally(SplitAttributes splitAttributes) { + switch (splitAttributes.getLayoutDirection()) { + case SplitAttributes.LayoutDirection.TOP_TO_BOTTOM: + case SplitAttributes.LayoutDirection.BOTTOM_TO_TOP: + return true; + default: + return false; } - // Obtain bounds from Activity instead because the Activity hasn't been embedded yet. - return getNonEmbeddedActivityBounds(activity); } /** - * Obtains the bounds from a non-embedded Activity. - * <p> - * Note that callers should use {@link #getParentContainerBounds(Activity)} instead for most - * cases unless we want to obtain task bounds before - * {@link TaskContainer#isTaskBoundsInitialized()}. + * Computes the {@link SplitType} with the {@link SplitAttributes} and the current device and + * window state. + * If passed {@link SplitAttributes#getSplitType} is a {@link RatioSplitType}. It reversed + * the ratio if the computed {@link SplitAttributes#getLayoutDirection} is + * {@link SplitAttributes.LayoutDirection.LEFT_TO_RIGHT} or + * {@link SplitAttributes.LayoutDirection.BOTTOM_TO_TOP} to make the bounds calculation easier. + * If passed {@link SplitAttributes#getSplitType} is a {@link HingeSplitType}, it checks + * the current device and window states to determine whether the split container should split + * by hinge or use {@link HingeSplitType#getFallbackSplitType}. */ + private SplitType computeSplitType(@NonNull SplitAttributes splitAttributes, + @NonNull Configuration taskConfiguration, @Nullable FoldingFeature foldingFeature) { + final int layoutDirection = splitAttributes.getLayoutDirection(); + final SplitType splitType = splitAttributes.getSplitType(); + if (splitType instanceof ExpandContainersSplitType) { + return splitType; + } else if (splitType instanceof RatioSplitType) { + final RatioSplitType splitRatio = (RatioSplitType) splitType; + // Reverse the ratio for RIGHT_TO_LEFT and BOTTOM_TO_TOP to make the boundary + // computation have the same direction, which is from (top, left) to (bottom, right). + final SplitType reversedSplitType = new RatioSplitType(1 - splitRatio.getRatio()); + switch (layoutDirection) { + case SplitAttributes.LayoutDirection.LEFT_TO_RIGHT: + case SplitAttributes.LayoutDirection.TOP_TO_BOTTOM: + return splitType; + case SplitAttributes.LayoutDirection.RIGHT_TO_LEFT: + case SplitAttributes.LayoutDirection.BOTTOM_TO_TOP: + return reversedSplitType; + case LayoutDirection.LOCALE: { + boolean isLtr = taskConfiguration.getLayoutDirection() + == View.LAYOUT_DIRECTION_LTR; + return isLtr ? splitType : reversedSplitType; + } + } + } else if (splitType instanceof HingeSplitType) { + final HingeSplitType hinge = (HingeSplitType) splitType; + @WindowingMode + final int windowingMode = taskConfiguration.windowConfiguration.getWindowingMode(); + return shouldSplitByHinge(splitAttributes, foldingFeature, windowingMode) + ? hinge : hinge.getFallbackSplitType(); + } + throw new IllegalArgumentException("Unknown SplitType:" + splitType); + } + + private static boolean shouldSplitByHinge(@NonNull SplitAttributes splitAttributes, + @Nullable FoldingFeature foldingFeature, @WindowingMode int taskWindowingMode) { + // Only HingeSplitType may split the task bounds by hinge. + if (!(splitAttributes.getSplitType() instanceof HingeSplitType)) { + return false; + } + // Device is not foldable, so there's no hinge to match. + if (foldingFeature == null) { + return false; + } + // The task is in multi-window mode. Match hinge doesn't make sense because current task + // bounds may not fit display bounds. + if (WindowConfiguration.inMultiWindowMode(taskWindowingMode)) { + return false; + } + // Return true if how the split attributes split the task bounds matches the orientation of + // folding area orientation. + return shouldSplitHorizontally(splitAttributes) == isFoldingAreaHorizontal(foldingFeature); + } + + private static boolean isFoldingAreaHorizontal(@NonNull FoldingFeature foldingFeature) { + final Rect bounds = foldingFeature.getBounds(); + return bounds.width() > bounds.height(); + } + + @NonNull + TaskProperties getTaskProperties(@NonNull Activity activity) { + final TaskContainer taskContainer = mController.getTaskContainer( + mController.getTaskId(activity)); + if (taskContainer != null) { + return taskContainer.getTaskProperties(); + } + return TaskProperties.getTaskPropertiesFromActivity(activity); + } + @NonNull - static Rect getNonEmbeddedActivityBounds(@NonNull Activity activity) { - final WindowConfiguration windowConfiguration = - activity.getResources().getConfiguration().windowConfiguration; - if (!activity.isInMultiWindowMode()) { - // In fullscreen mode the max bounds should correspond to the task bounds. - return windowConfiguration.getMaxBounds(); - } - return windowConfiguration.getBounds(); + WindowMetrics getTaskWindowMetrics(@NonNull Activity activity) { + return getTaskWindowMetrics(getTaskProperties(activity).getConfiguration()); + } + + @NonNull + static WindowMetrics getTaskWindowMetrics(@NonNull Configuration taskConfiguration) { + final Rect taskBounds = taskConfiguration.windowConfiguration.getBounds(); + // TODO(b/190433398): Supply correct insets. + final float density = taskConfiguration.densityDpi * DisplayMetrics.DENSITY_DEFAULT_SCALE; + return new WindowMetrics(taskBounds, WindowInsets.CONSUMED, density); } } 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..4b15bb187035 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java @@ -20,16 +20,23 @@ import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; +import static android.app.WindowConfiguration.inMultiWindowMode; -import android.annotation.NonNull; -import android.annotation.Nullable; import android.app.Activity; +import android.app.ActivityClient; import android.app.WindowConfiguration; import android.app.WindowConfiguration.WindowingMode; +import android.content.res.Configuration; import android.graphics.Rect; import android.os.IBinder; import android.util.ArraySet; +import android.util.Log; import android.window.TaskFragmentInfo; +import android.window.TaskFragmentParentInfo; +import android.window.WindowContainerTransaction; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import java.util.ArrayList; import java.util.List; @@ -37,17 +44,11 @@ import java.util.Set; /** Represents TaskFragments and split pairs below a Task. */ class TaskContainer { + private static final String TAG = TaskContainer.class.getSimpleName(); /** The unique task id. */ private final int mTaskId; - /** Available window bounds of this Task. */ - private final Rect mTaskBounds = new Rect(); - - /** Windowing mode of this Task. */ - @WindowingMode - private int mWindowingMode = WINDOWING_MODE_UNDEFINED; - /** Active TaskFragments in this Task. */ @NonNull final List<TaskFragmentContainer> mContainers = new ArrayList<>(); @@ -56,50 +57,65 @@ class TaskContainer { @NonNull final List<SplitContainer> mSplitContainers = new ArrayList<>(); + @NonNull + private final Configuration mConfiguration; + + private int mDisplayId; + + private boolean mIsVisible; + /** * TaskFragments that the organizer has requested to be closed. They should be removed when - * the organizer receives {@link SplitController#onTaskFragmentVanished(TaskFragmentInfo)} event - * for them. + * the organizer receives + * {@link SplitController#onTaskFragmentVanished(WindowContainerTransaction, TaskFragmentInfo)} + * event for them. */ final Set<IBinder> mFinishedContainer = new ArraySet<>(); - TaskContainer(int taskId) { + /** + * The {@link TaskContainer} constructor + * + * @param taskId The ID of the Task, which must match {@link Activity#getTaskId()} with + * {@code activityInTask}. + * @param activityInTask The {@link Activity} in the Task with {@code taskId}. It is used to + * initialize the {@link TaskContainer} properties. + * + */ + TaskContainer(int taskId, @NonNull Activity activityInTask) { if (taskId == INVALID_TASK_ID) { throw new IllegalArgumentException("Invalid Task id"); } mTaskId = taskId; + final TaskProperties taskProperties = TaskProperties + .getTaskPropertiesFromActivity(activityInTask); + mConfiguration = taskProperties.getConfiguration(); + mDisplayId = taskProperties.getDisplayId(); + // Note that it is always called when there's a new Activity is started, which implies + // the host task is visible. + mIsVisible = true; } int getTaskId() { return mTaskId; } - @NonNull - Rect getTaskBounds() { - return mTaskBounds; + int getDisplayId() { + return mDisplayId; } - /** Returns {@code true} if the bounds is changed. */ - boolean setTaskBounds(@NonNull Rect taskBounds) { - if (!taskBounds.isEmpty() && !mTaskBounds.equals(taskBounds)) { - mTaskBounds.set(taskBounds); - return true; - } - return false; - } - - /** Whether the Task bounds has been initialized. */ - boolean isTaskBoundsInitialized() { - return !mTaskBounds.isEmpty(); + boolean isVisible() { + return mIsVisible; } - void setWindowingMode(int windowingMode) { - mWindowingMode = windowingMode; + @NonNull + TaskProperties getTaskProperties() { + return new TaskProperties(mDisplayId, mConfiguration); } - /** Whether the Task windowing mode has been initialized. */ - boolean isWindowingModeInitialized() { - return mWindowingMode != WINDOWING_MODE_UNDEFINED; + void updateTaskFragmentParentInfo(@NonNull TaskFragmentParentInfo info) { + mConfiguration.setTo(info.getConfiguration()); + mDisplayId = info.getDisplayId(); + mIsVisible = info.isVisible(); } /** @@ -122,13 +138,20 @@ class TaskContainer { // DecorCaptionView won't work correctly. As a result, have the TaskFragment to be in the // Task windowing mode if the Task is in multi window. // TODO we won't need this anymore after we migrate Freeform caption to WM Shell. - return WindowConfiguration.inMultiWindowMode(mWindowingMode) - ? mWindowingMode - : WINDOWING_MODE_MULTI_WINDOW; + return isInMultiWindow() ? getWindowingMode() : WINDOWING_MODE_MULTI_WINDOW; } boolean isInPictureInPicture() { - return mWindowingMode == WINDOWING_MODE_PINNED; + return getWindowingMode() == WINDOWING_MODE_PINNED; + } + + boolean isInMultiWindow() { + return WindowConfiguration.inMultiWindowMode(getWindowingMode()); + } + + @WindowingMode + private int getWindowingMode() { + return mConfiguration.windowConfiguration.getWindowingMode(); } /** Whether there is any {@link TaskFragmentContainer} below this Task. */ @@ -136,10 +159,17 @@ class TaskContainer { return mContainers.isEmpty() && mFinishedContainer.isEmpty(); } + /** Called when the activity is destroyed. */ + void onActivityDestroyed(@NonNull IBinder activityToken) { + for (TaskFragmentContainer container : mContainers) { + container.onActivityDestroyed(activityToken); + } + } + /** Removes the pending appeared activity from all TaskFragments in this Task. */ - void cleanupPendingAppearedActivity(@NonNull Activity pendingAppearedActivity) { + void cleanupPendingAppearedActivity(@NonNull IBinder activityToken) { for (TaskFragmentContainer container : mContainers) { - container.removePendingAppearedActivity(pendingAppearedActivity); + container.removePendingAppearedActivity(activityToken); } } @@ -161,4 +191,96 @@ class TaskContainer { } return null; } + + int indexOf(@NonNull TaskFragmentContainer child) { + return mContainers.indexOf(child); + } + + /** Whether the Task is in an intermediate state waiting for the server update.*/ + boolean isInIntermediateState() { + for (TaskFragmentContainer container : mContainers) { + if (container.isInIntermediateState()) { + // We are in an intermediate state to wait for server update on this TaskFragment. + return true; + } + } + return false; + } + + /** Adds the descriptors of split states in this Task to {@code outSplitStates}. */ + void getSplitStates(@NonNull List<SplitInfo> outSplitStates) { + for (SplitContainer container : mSplitContainers) { + outSplitStates.add(container.toSplitInfo()); + } + } + + /** A wrapper class which contains the information of {@link TaskContainer} */ + static final class TaskProperties { + private final int mDisplayId; + @NonNull + private final Configuration mConfiguration; + + TaskProperties(int displayId, @NonNull Configuration configuration) { + mDisplayId = displayId; + mConfiguration = configuration; + } + + int getDisplayId() { + return mDisplayId; + } + + @NonNull + Configuration getConfiguration() { + return mConfiguration; + } + + /** Translates the given absolute bounds to relative bounds in this Task coordinate. */ + void translateAbsoluteBoundsToRelativeBounds(@NonNull Rect inOutBounds) { + if (inOutBounds.isEmpty()) { + return; + } + final Rect taskBounds = mConfiguration.windowConfiguration.getBounds(); + inOutBounds.offset(-taskBounds.left, -taskBounds.top); + } + + /** + * Obtains the {@link TaskProperties} for the task that the provided {@link Activity} is + * associated with. + * <p> + * Note that for most case, caller should use + * {@link SplitPresenter#getTaskProperties(Activity)} instead. This method is used before + * the {@code activity} goes into split. + * </p><p> + * If the {@link Activity} is in fullscreen, override + * {@link WindowConfiguration#getBounds()} with {@link WindowConfiguration#getMaxBounds()} + * in case the {@link Activity} is letterboxed. Otherwise, get the Task + * {@link Configuration} from the server side or use {@link Activity}'s + * {@link Configuration} as a fallback if the Task {@link Configuration} cannot be obtained. + */ + @NonNull + static TaskProperties getTaskPropertiesFromActivity(@NonNull Activity activity) { + final int displayId = activity.getDisplayId(); + // Use a copy of configuration because activity's configuration may be updated later, + // or we may get unexpected TaskContainer's configuration if Activity's configuration is + // updated. An example is Activity is going to be in split. + final Configuration activityConfig = new Configuration( + activity.getResources().getConfiguration()); + final WindowConfiguration windowConfiguration = activityConfig.windowConfiguration; + final int windowingMode = windowConfiguration.getWindowingMode(); + if (!inMultiWindowMode(windowingMode)) { + // Use the max bounds in fullscreen in case the Activity is letterboxed. + windowConfiguration.setBounds(windowConfiguration.getMaxBounds()); + return new TaskProperties(displayId, activityConfig); + } + final Configuration taskConfig = ActivityClient.getInstance() + .getTaskConfiguration(activity.getActivityToken()); + if (taskConfig == null) { + Log.w(TAG, "Could not obtain task configuration for activity:" + activity); + // Still report activity config if task config cannot be obtained from the server + // side. + return new TaskProperties(displayId, activityConfig); + } + return new TaskProperties(displayId, taskConfig); + } + } } 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..33220c44a3b5 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationAdapter.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationAdapter.java @@ -16,10 +16,11 @@ 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; +import static android.view.RemoteAnimationTarget.MODE_CLOSING; +import android.graphics.Point; import android.graphics.Rect; import android.view.Choreographer; import android.view.RemoteAnimationTarget; @@ -41,30 +42,69 @@ 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(); + /** + * Area in absolute coordinate that should represent all the content to show for this window. + * This should be the end bounds for opening window, and start bounds for closing window in case + * the window is resizing during the open/close transition. + */ + @NonNull + private final Rect mContentBounds = new Rect(); + /** Offset relative to the window parent surface for {@link #mContentBounds}. */ + @NonNull + private final Point mContentRelOffset = new Point(); + @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); + if (target.mode == MODE_CLOSING) { + // When it is closing, we want to show the content at the start position in case the + // window is resizing as well. For example, when the activities is changing from split + // to stack, the bottom TaskFragment will be resized to fullscreen when hiding. + final Rect startBounds = target.startBounds; + final Rect endBounds = target.screenSpaceBounds; + mContentBounds.set(startBounds); + mContentRelOffset.set(target.localBounds.left, target.localBounds.top); + mContentRelOffset.offset( + startBounds.left - endBounds.left, + startBounds.top - endBounds.top); + } else { + mContentBounds.set(target.screenSpaceBounds); + mContentRelOffset.set(target.localBounds.left, target.localBounds.top); + } } /** @@ -94,23 +134,30 @@ class TaskFragmentAnimationAdapter { /** To be overridden by subclasses to adjust the animation surface change. */ void onAnimationUpdateInner(@NonNull SurfaceControl.Transaction t) { - mTransformation.getMatrix().postTranslate( - mTarget.localBounds.left, mTarget.localBounds.top); + // Update the surface position and alpha. + mTransformation.getMatrix().postTranslate(mContentRelOffset.x, mContentRelOffset.y); 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(mContentBounds); + cropRect.offset(positionX - mContentRelOffset.x, positionY - mContentRelOffset.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); } @@ -124,52 +171,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 +178,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..d7eb9a01f57c 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationController.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationController.java @@ -18,18 +18,17 @@ package androidx.window.extensions.embedding; import static android.view.WindowManager.TRANSIT_OLD_ACTIVITY_CLOSE; import static android.view.WindowManager.TRANSIT_OLD_ACTIVITY_OPEN; -import static android.view.WindowManager.TRANSIT_OLD_TASK_CLOSE; import static android.view.WindowManager.TRANSIT_OLD_TASK_FRAGMENT_CHANGE; import static android.view.WindowManager.TRANSIT_OLD_TASK_FRAGMENT_CLOSE; import static android.view.WindowManager.TRANSIT_OLD_TASK_FRAGMENT_OPEN; -import static android.view.WindowManager.TRANSIT_OLD_TASK_OPEN; -import android.util.ArraySet; import android.util.Log; 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. */ @@ -42,49 +41,39 @@ class TaskFragmentAnimationController { private final TaskFragmentAnimationRunner mRemoteRunner = new TaskFragmentAnimationRunner(); @VisibleForTesting final RemoteAnimationDefinition mDefinition; - /** Task Ids that we have registered for remote animation. */ - private final ArraySet<Integer> mRegisterTasks = new ArraySet<>(); + private boolean mIsRegistered; - TaskFragmentAnimationController(TaskFragmentOrganizer organizer) { + TaskFragmentAnimationController(@NonNull TaskFragmentOrganizer organizer) { mOrganizer = organizer; mDefinition = new RemoteAnimationDefinition(); final RemoteAnimationAdapter animationAdapter = new RemoteAnimationAdapter(mRemoteRunner, 0, 0, true /* changeNeedsSnapshot */); mDefinition.addRemoteAnimation(TRANSIT_OLD_ACTIVITY_OPEN, animationAdapter); mDefinition.addRemoteAnimation(TRANSIT_OLD_TASK_FRAGMENT_OPEN, animationAdapter); - mDefinition.addRemoteAnimation(TRANSIT_OLD_TASK_OPEN, animationAdapter); mDefinition.addRemoteAnimation(TRANSIT_OLD_ACTIVITY_CLOSE, animationAdapter); mDefinition.addRemoteAnimation(TRANSIT_OLD_TASK_FRAGMENT_CLOSE, animationAdapter); - mDefinition.addRemoteAnimation(TRANSIT_OLD_TASK_CLOSE, animationAdapter); mDefinition.addRemoteAnimation(TRANSIT_OLD_TASK_FRAGMENT_CHANGE, animationAdapter); } - void registerRemoteAnimations(int taskId) { + void registerRemoteAnimations() { if (DEBUG) { Log.v(TAG, "registerRemoteAnimations"); } - if (mRegisterTasks.contains(taskId)) { + if (mIsRegistered) { return; } - mOrganizer.registerRemoteAnimations(taskId, mDefinition); - mRegisterTasks.add(taskId); + mOrganizer.registerRemoteAnimations(mDefinition); + mIsRegistered = true; } - void unregisterRemoteAnimations(int taskId) { + void unregisterRemoteAnimations() { if (DEBUG) { Log.v(TAG, "unregisterRemoteAnimations"); } - if (!mRegisterTasks.contains(taskId)) { + if (!mIsRegistered) { return; } - mOrganizer.unregisterRemoteAnimations(taskId); - mRegisterTasks.remove(taskId); - } - - void unregisterAllRemoteAnimations() { - final ArraySet<Integer> tasks = new ArraySet<>(mRegisterTasks); - for (int taskId : tasks) { - unregisterRemoteAnimations(taskId); - } + mOrganizer.unregisterRemoteAnimations(); + mIsRegistered = false; } } 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..b917ac80256c 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationRunner.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationRunner.java @@ -17,14 +17,14 @@ package androidx.window.extensions.embedding; import static android.os.Process.THREAD_PRIORITY_DISPLAY; +import static android.view.RemoteAnimationTarget.MODE_CHANGING; import static android.view.RemoteAnimationTarget.MODE_CLOSING; +import static android.view.RemoteAnimationTarget.MODE_OPENING; import static android.view.WindowManager.TRANSIT_OLD_ACTIVITY_CLOSE; import static android.view.WindowManager.TRANSIT_OLD_ACTIVITY_OPEN; -import static android.view.WindowManager.TRANSIT_OLD_TASK_CLOSE; import static android.view.WindowManager.TRANSIT_OLD_TASK_FRAGMENT_CHANGE; import static android.view.WindowManager.TRANSIT_OLD_TASK_FRAGMENT_CLOSE; import static android.view.WindowManager.TRANSIT_OLD_TASK_FRAGMENT_OPEN; -import static android.view.WindowManager.TRANSIT_OLD_TASK_OPEN; import static android.view.WindowManagerPolicyConstants.TYPE_LAYER_OFFSET; import android.animation.Animator; @@ -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,17 +162,16 @@ 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) { switch (transit) { case TRANSIT_OLD_ACTIVITY_OPEN: case TRANSIT_OLD_TASK_FRAGMENT_OPEN: - case TRANSIT_OLD_TASK_OPEN: return createOpenAnimationAdapters(targets); case TRANSIT_OLD_ACTIVITY_CLOSE: case TRANSIT_OLD_TASK_FRAGMENT_CLOSE: - case TRANSIT_OLD_TASK_CLOSE: return createCloseAnimationAdapters(targets); case TRANSIT_OLD_TASK_FRAGMENT_CHANGE: return createChangeAnimationAdapters(targets); @@ -180,12 +180,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 +198,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 +211,12 @@ 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); + // Union the start bounds since this may be the ClosingChanging animation. + closingWholeScreenBounds.union(target.startBounds); } } @@ -238,32 +243,26 @@ 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) { + if (shouldUseJumpCutForChangeAnimation(targets)) { + return new ArrayList<>(); + } + final List<TaskFragmentAnimationAdapter> adapters = new ArrayList<>(); for (RemoteAnimationTarget target : targets) { - if (target.startBounds != null) { + if (target.mode == MODE_CHANGING) { // This is the target with bounds change. final Animation[] animations = mAnimationSpec.createChangeBoundsChangeAnimations(target); @@ -290,4 +289,24 @@ class TaskFragmentAnimationRunner extends IRemoteAnimationRunner.Stub { } return adapters; } + + /** + * Whether we should use jump cut for the change transition. + * This normally happens when opening a new secondary with the existing primary using a + * different split layout. This can be complicated, like from horizontal to vertical split with + * new split pairs. + * Uses a jump cut animation to simplify. + */ + private boolean shouldUseJumpCutForChangeAnimation(@NonNull RemoteAnimationTarget[] targets) { + boolean hasOpeningWindow = false; + boolean hasClosingWindow = false; + for (RemoteAnimationTarget target : targets) { + if (target.hasAnimatingParent) { + continue; + } + hasOpeningWindow |= target.mode == MODE_OPENING; + hasClosingWindow |= target.mode == MODE_CLOSING; + } + return hasOpeningWindow && hasClosingWindow; + } } 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..1f866c3b99c9 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,14 +84,25 @@ 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. - final int startLeft = bounds.left == 0 ? -bounds.width() : bounds.width(); + final Rect parentBounds = target.taskInfo.configuration.windowConfiguration.getBounds(); + final Rect bounds = target.screenSpaceBounds; + final int startLeft; + final int startTop; + if (parentBounds.top == bounds.top && parentBounds.bottom == bounds.bottom) { + // The window will be animated in from left or right depending on its position. + startTop = 0; + startLeft = parentBounds.left == bounds.left ? -bounds.width() : bounds.width(); + } else { + // The window will be animated in from top or bottom depending on its position. + startTop = parentBounds.top == bounds.top ? -bounds.height() : bounds.height(); + startLeft = 0; + } // The position should be 0-based as we will post translate in // TaskFragmentAnimationAdapter#onAnimationUpdate - final Animation animation = new TranslateAnimation(startLeft, 0, 0, 0); + final Animation animation = new TranslateAnimation(startLeft, 0, startTop, 0); animation.setInterpolator(mFastOutExtraSlowInInterpolator); animation.setDuration(CHANGE_ANIMATION_DURATION); animation.initialize(bounds.width(), bounds.height(), bounds.width(), bounds.height()); @@ -101,14 +111,26 @@ 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. - final int endLeft = bounds.left == 0 ? -bounds.width() : bounds.width(); + final Rect parentBounds = target.taskInfo.configuration.windowConfiguration.getBounds(); + // Use startBounds if the window is closing in case it may also resize. + final Rect bounds = target.startBounds; + final int endTop; + final int endLeft; + if (parentBounds.top == bounds.top && parentBounds.bottom == bounds.bottom) { + // The window will be animated out to left or right depending on its position. + endTop = 0; + endLeft = parentBounds.left == bounds.left ? -bounds.width() : bounds.width(); + } else { + // The window will be animated out to top or bottom depending on its position. + endTop = parentBounds.top == bounds.top ? -bounds.height() : bounds.height(); + endLeft = 0; + } // The position should be 0-based as we will post translate in // TaskFragmentAnimationAdapter#onAnimationUpdate - final Animation animation = new TranslateAnimation(0, endLeft, 0, 0); + final Animation animation = new TranslateAnimation(0, endLeft, 0, endTop); animation.setInterpolator(mFastOutExtraSlowInInterpolator); animation.setDuration(CHANGE_ANIMATION_DURATION); animation.initialize(bounds.width(), bounds.height(), bounds.width(), bounds.height()); @@ -121,6 +143,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 @@ -159,7 +182,7 @@ class TaskFragmentAnimationSpec { // The position should be 0-based as we will post translate in // TaskFragmentAnimationAdapter#onAnimationUpdate final Animation endTranslate = new TranslateAnimation(startBounds.left - endBounds.left, 0, - 0, 0); + startBounds.top - endBounds.top, 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. @@ -177,30 +200,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 +261,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..60be9d16d749 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java @@ -18,28 +18,37 @@ 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; import android.os.Binder; import android.os.IBinder; import android.util.Size; +import android.window.TaskFragmentAnimationParams; 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; +import java.util.Collections; import java.util.Iterator; import java.util.List; +import java.util.Objects; /** * Client-side container for a stack of activities. Corresponds to an instance of TaskFragment * on the server side. */ +// Suppress GuardedBy warning because all the TaskFragmentContainers are stored in +// SplitController.mTaskContainers which is guarded. +@SuppressWarnings("GuardedBy") class TaskFragmentContainer { private static final int APPEAR_EMPTY_TIMEOUT_MS = 3000; @@ -63,11 +72,11 @@ class TaskFragmentContainer { TaskFragmentInfo mInfo; /** - * Activities that are being reparented or being started to this container, but haven't been - * added to {@link #mInfo} yet. + * Activity tokens that are being reparented or being started to this container, but haven't + * been added to {@link #mInfo} yet. */ @VisibleForTesting - final ArrayList<Activity> mPendingAppearedActivities = new ArrayList<>(); + final ArrayList<IBinder> mPendingAppearedActivities = new ArrayList<>(); /** * When this container is created for an {@link Intent} to start within, we store that Intent @@ -77,12 +86,21 @@ class TaskFragmentContainer { @Nullable private Intent mPendingAppearedIntent; + /** + * The activities that were explicitly requested to be launched in its current TaskFragment, + * but haven't been added to {@link #mInfo} yet. + */ + final ArrayList<IBinder> mPendingAppearedInRequestedTaskFragmentActivities = new ArrayList<>(); + /** Containers that are dependent on this one and should be completely destroyed on exit. */ private final List<TaskFragmentContainer> mContainersToFinishOnExit = new ArrayList<>(); - /** Individual associated activities in different containers that should be finished on exit. */ - private final List<Activity> mActivitiesToFinishOnExit = new ArrayList<>(); + /** + * Individual associated activity tokens in different containers that should be finished on + * exit. + */ + private final List<IBinder> mActivitiesToFinishOnExit = new ArrayList<>(); /** Indicates whether the container was cleaned up after the last activity was removed. */ private boolean mIsFinished; @@ -99,6 +117,34 @@ class TaskFragmentContainer { private int mLastRequestedWindowingMode = WINDOWING_MODE_UNDEFINED; /** + * TaskFragmentAnimationParams that was requested last via + * {@link android.window.WindowContainerTransaction}. + */ + @NonNull + private TaskFragmentAnimationParams mLastAnimationParams = TaskFragmentAnimationParams.DEFAULT; + + /** + * TaskFragment token that was requested last via + * {@link android.window.TaskFragmentOperation#OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS}. + */ + @Nullable + private IBinder mLastAdjacentTaskFragment; + + /** + * {@link WindowContainerTransaction.TaskFragmentAdjacentParams} token that was requested last + * via {@link android.window.TaskFragmentOperation#OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS}. + */ + @Nullable + private WindowContainerTransaction.TaskFragmentAdjacentParams mLastAdjacentParams; + + /** + * TaskFragment token that was requested last via + * {@link android.window.TaskFragmentOperation#OP_TYPE_SET_COMPANION_TASK_FRAGMENT}. + */ + @Nullable + private IBinder mLastCompanionTaskFragment; + + /** * When the TaskFragment has appeared in server, but is empty, we should remove the TaskFragment * if it is still empty after the timeout. */ @@ -107,12 +153,19 @@ class TaskFragmentContainer { Runnable mAppearEmptyTimeout; /** + * Whether this TaskFragment contains activities of another process/package. + */ + private boolean mHasCrossProcessActivities; + + /** * Creates a container with an existing activity that will be re-parented to it in a window * container transaction. + * @param pairedPrimaryContainer when it is set, the new container will be add right above it */ TaskFragmentContainer(@Nullable Activity pendingAppearedActivity, @Nullable Intent pendingAppearedIntent, @NonNull TaskContainer taskContainer, - @NonNull SplitController controller) { + @NonNull SplitController controller, + @Nullable TaskFragmentContainer pairedPrimaryContainer) { if ((pendingAppearedActivity == null && pendingAppearedIntent == null) || (pendingAppearedActivity != null && pendingAppearedIntent != null)) { throw new IllegalArgumentException( @@ -121,7 +174,30 @@ class TaskFragmentContainer { mController = controller; mToken = new Binder("TaskFragmentContainer"); mTaskContainer = taskContainer; - taskContainer.mContainers.add(this); + if (pairedPrimaryContainer != null) { + // The TaskFragment will be positioned right above the paired container. + if (pairedPrimaryContainer.getTaskContainer() != taskContainer) { + throw new IllegalArgumentException( + "pairedPrimaryContainer must be in the same Task"); + } + final int primaryIndex = taskContainer.mContainers.indexOf(pairedPrimaryContainer); + taskContainer.mContainers.add(primaryIndex + 1, this); + } else if (pendingAppearedActivity != null) { + // The TaskFragment will be positioned right above the pending appeared Activity. If any + // existing TaskFragment is empty with pending Intent, it is likely that the Activity of + // the pending Intent hasn't been created yet, so the new Activity should be below the + // empty TaskFragment. + int i = taskContainer.mContainers.size() - 1; + for (; i >= 0; i--) { + final TaskFragmentContainer container = taskContainer.mContainers.get(i); + if (!container.isEmpty() || container.getPendingAppearedIntent() == null) { + break; + } + } + taskContainer.mContainers.add(i + 1, this); + } else { + taskContainer.mContainers.add(this); + } if (pendingAppearedActivity != null) { addPendingAppearedActivity(pendingAppearedActivity); } @@ -155,42 +231,112 @@ class TaskFragmentContainer { // in this intermediate state. // Place those on top of the list since they will be on the top after reported from the // server. - for (Activity activity : mPendingAppearedActivities) { - if (!activity.isFinishing()) { + for (IBinder token : mPendingAppearedActivities) { + final Activity activity = mController.getActivity(token); + if (activity != null && !activity.isFinishing()) { allActivities.add(activity); } } return allActivities; } - /** - * Checks if the count of activities from the same process in task fragment info corresponds to - * the ones created and available on the client side. - */ - boolean taskInfoActivityCountMatchesCreated() { + /** Whether this TaskFragment is visible. */ + boolean isVisible() { + return mInfo != null && mInfo.isVisible(); + } + + /** Whether the TaskFragment is in an intermediate state waiting for the server update.*/ + boolean isInIntermediateState() { if (mInfo == null) { - return false; + // Haven't received onTaskFragmentAppeared event. + return true; + } + if (mInfo.isEmpty()) { + // Empty TaskFragment will be removed or will have activity launched into it soon. + return true; + } + if (!mPendingAppearedActivities.isEmpty()) { + // Reparented activity hasn't appeared. + return true; } - return mPendingAppearedActivities.isEmpty() - && mInfo.getActivities().size() == collectNonFinishingActivities().size(); + // Check if there is any reported activity that is no longer alive. + for (IBinder token : mInfo.getActivities()) { + final Activity activity = mController.getActivity(token); + if (activity == null && !mTaskContainer.isVisible()) { + // Activity can be null if the activity is not attached to process yet. That can + // happen when the activity is started in background. + continue; + } + if (activity == null || activity.isFinishing()) { + // One of the reported activity is no longer alive, wait for the server update. + return true; + } + } + return false; } + @NonNull ActivityStack toActivityStack() { - return new ActivityStack(collectNonFinishingActivities(), isEmpty()); + return new ActivityStack(collectNonFinishingActivities(), isEmpty(), mToken); } /** Adds the activity that will be reparented to this container. */ void addPendingAppearedActivity(@NonNull Activity pendingAppearedActivity) { - if (hasActivity(pendingAppearedActivity.getActivityToken())) { + final IBinder activityToken = pendingAppearedActivity.getActivityToken(); + if (hasActivity(activityToken)) { return; } - // Remove the pending activity from other TaskFragments. - mTaskContainer.cleanupPendingAppearedActivity(pendingAppearedActivity); - mPendingAppearedActivities.add(pendingAppearedActivity); + // Remove the pending activity from other TaskFragments in case the activity is reparented + // again before the server update. + mTaskContainer.cleanupPendingAppearedActivity(activityToken); + mPendingAppearedActivities.add(activityToken); + updateActivityClientRecordTaskFragmentToken(activityToken); + } + + /** + * 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 IBinder activityToken) { + final ActivityThread.ActivityClientRecord record = ActivityThread + .currentActivityThread().getActivityClient(activityToken); + if (record != null) { + record.mTaskFragmentToken = mToken; + } } - void removePendingAppearedActivity(@NonNull Activity pendingAppearedActivity) { - mPendingAppearedActivities.remove(pendingAppearedActivity); + void removePendingAppearedActivity(@NonNull IBinder activityToken) { + mPendingAppearedActivities.remove(activityToken); + // Also remove the activity from the mPendingInRequestedTaskFragmentActivities. + mPendingAppearedInRequestedTaskFragmentActivities.remove(activityToken); + } + + @GuardedBy("mController.mLock") + void clearPendingAppearedActivities() { + final List<IBinder> 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 (IBinder activityToken : cleanupActivities) { + final TaskFragmentContainer curContainer = mController.getContainerWithActivity( + activityToken); + if (curContainer != null) { + curContainer.updateActivityClientRecordTaskFragmentToken(activityToken); + } + } + } + + /** Called when the activity is destroyed. */ + void onActivityDestroyed(@NonNull IBinder activityToken) { + removePendingAppearedActivity(activityToken); + if (mInfo != null) { + // Remove the activity now because there can be a delay before the server callback. + mInfo.getActivities().remove(activityToken); + } + mActivitiesToFinishOnExit.remove(activityToken); } @Nullable @@ -198,16 +344,40 @@ class TaskFragmentContainer { return mPendingAppearedIntent; } - boolean hasActivity(@NonNull IBinder token) { - if (mInfo != null && mInfo.getActivities().contains(token)) { - return true; - } - for (Activity activity : mPendingAppearedActivities) { - if (activity.getActivityToken().equals(token)) { - return true; - } + 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; } - return false; + mPendingAppearedIntent = null; + } + + boolean hasActivity(@NonNull IBinder activityToken) { + // Instead of using (hasAppearedActivity() || hasPendingAppearedActivity), we want to make + // sure the controller considers this container as the one containing the activity. + // This is needed when the activity is added as pending appeared activity to one + // TaskFragment while it is also an appeared activity in another. + return mController.getContainerWithActivity(activityToken) == this; + } + + /** Whether this activity has appeared in the TaskFragment on the server side. */ + boolean hasAppearedActivity(@NonNull IBinder activityToken) { + return mInfo != null && mInfo.getActivities().contains(activityToken); + } + + /** + * Whether we are waiting for this activity to appear in the TaskFragment on the server side. + */ + boolean hasPendingAppearedActivity(@NonNull IBinder activityToken) { + return mPendingAppearedActivities.contains(activityToken); } int getRunningActivityCount() { @@ -228,24 +398,43 @@ 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; } + mHasCrossProcessActivities = false; mInfo = info; if (mInfo == null || mInfo.isEmpty()) { return; } + + // Contains activities of another process if the activities size is not matched to the + // running activity count + if (mInfo.getRunningActivityCount() != mInfo.getActivities().size()) { + mHasCrossProcessActivities = true; + } + // Only track the pending Intent when the container is empty. mPendingAppearedIntent = null; if (mPendingAppearedActivities.isEmpty()) { @@ -254,9 +443,9 @@ class TaskFragmentContainer { // Cleanup activities that were being re-parented List<IBinder> infoActivities = mInfo.getActivities(); for (int i = mPendingAppearedActivities.size() - 1; i >= 0; --i) { - final Activity activity = mPendingAppearedActivities.get(i); - if (infoActivities.contains(activity.getActivityToken())) { - mPendingAppearedActivities.remove(i); + final IBinder activityToken = mPendingAppearedActivities.get(i); + if (infoActivities.contains(activityToken)) { + removePendingAppearedActivity(activityToken); } } } @@ -291,10 +480,17 @@ class TaskFragmentContainer { * Removes a container that should be finished when this container is finished. */ void removeContainerToFinishOnExit(@NonNull TaskFragmentContainer containerToRemove) { + removeContainersToFinishOnExit(Collections.singletonList(containerToRemove)); + } + + /** + * Removes container list that should be finished when this container is finished. + */ + void removeContainersToFinishOnExit(@NonNull List<TaskFragmentContainer> containersToRemove) { if (mIsFinished) { return; } - mContainersToFinishOnExit.remove(containerToRemove); + mContainersToFinishOnExit.removeAll(containersToRemove); } /** @@ -304,7 +500,7 @@ class TaskFragmentContainer { if (mIsFinished) { return; } - mActivitiesToFinishOnExit.add(activityToFinish); + mActivitiesToFinishOnExit.add(activityToFinish.getActivityToken()); } /** @@ -314,7 +510,7 @@ class TaskFragmentContainer { if (mIsFinished) { return; } - mActivitiesToFinishOnExit.remove(activityToRemove); + mActivitiesToFinishOnExit.remove(activityToRemove.getActivityToken()); } /** Removes all dependencies that should be finished when this container is finished. */ @@ -330,8 +526,19 @@ class TaskFragmentContainer { * Removes all activities that belong to this process and finishes other containers/activities * configured to finish together. */ + @GuardedBy("mController.mLock") void finish(boolean shouldFinishDependent, @NonNull SplitPresenter presenter, @NonNull WindowContainerTransaction wct, @NonNull SplitController controller) { + finish(shouldFinishDependent, presenter, wct, controller, true /* shouldRemoveRecord */); + } + + /** + * Removes all activities that belong to this process and finishes other containers/activities + * configured to finish together. + */ + void finish(boolean shouldFinishDependent, @NonNull SplitPresenter presenter, + @NonNull WindowContainerTransaction wct, @NonNull SplitController controller, + boolean shouldRemoveRecord) { if (!mIsFinished) { mIsFinished = true; if (mAppearEmptyTimeout != null) { @@ -348,12 +555,15 @@ class TaskFragmentContainer { // Cleanup the visuals presenter.deleteTaskFragment(wct, getTaskFragmentToken()); - // Cleanup the records - controller.removeContainer(this); + if (shouldRemoveRecord) { + // Cleanup the records + controller.removeContainer(this); + } // Clean up task fragment information mInfo = null; } + @GuardedBy("mController.mLock") private void finishActivities(boolean shouldFinishDependent, @NonNull SplitPresenter presenter, @NonNull WindowContainerTransaction wct, @NonNull SplitController controller) { // Finish own activities @@ -362,11 +572,13 @@ class TaskFragmentContainer { // In case we have requested to reparent the activity to another container (as // pendingAppeared), we don't want to finish it with this container. && mController.getContainerWithActivity(activity) == this) { - activity.finish(); + wct.finishActivity(activity.getActivityToken()); } } if (!shouldFinishDependent) { + // Always finish the placeholder when the primary is finished. + finishPlaceholderIfAny(wct, presenter); return; } @@ -382,46 +594,69 @@ class TaskFragmentContainer { mContainersToFinishOnExit.clear(); // Finish associated activities - for (Activity activity : mActivitiesToFinishOnExit) { - if (activity.isFinishing() + for (IBinder activityToken : mActivitiesToFinishOnExit) { + final Activity activity = mController.getActivity(activityToken); + if (activity == null || activity.isFinishing() || controller.shouldRetainAssociatedActivity(this, activity)) { continue; } - activity.finish(); + wct.finishActivity(activity.getActivityToken()); } mActivitiesToFinishOnExit.clear(); } + @GuardedBy("mController.mLock") + private void finishPlaceholderIfAny(@NonNull WindowContainerTransaction wct, + @NonNull SplitPresenter presenter) { + final List<TaskFragmentContainer> containersToRemove = new ArrayList<>(); + for (TaskFragmentContainer container : mContainersToFinishOnExit) { + if (container.mIsFinished) { + continue; + } + final SplitContainer splitContainer = mController.getActiveSplitForContainers( + this, container); + if (splitContainer != null && splitContainer.isPlaceholderContainer() + && splitContainer.getSecondaryContainer() == container) { + // Remove the placeholder secondary TaskFragment. + containersToRemove.add(container); + } + } + mContainersToFinishOnExit.removeAll(containersToRemove); + for (TaskFragmentContainer container : containersToRemove) { + container.finish(false /* shouldFinishDependent */, presenter, wct, mController); + } + } + boolean isFinished() { return mIsFinished; } /** * Checks if last requested bounds are equal to the provided value. + * The requested bounds are relative bounds in parent coordinate. + * @see WindowContainerTransaction#setRelativeBounds */ - boolean areLastRequestedBoundsEqual(@Nullable Rect bounds) { - return (bounds == null && mLastRequestedBounds.isEmpty()) - || mLastRequestedBounds.equals(bounds); + boolean areLastRequestedBoundsEqual(@Nullable Rect relBounds) { + return (relBounds == null && mLastRequestedBounds.isEmpty()) + || mLastRequestedBounds.equals(relBounds); } /** * Updates the last requested bounds. + * The requested bounds are relative bounds in parent coordinate. + * @see WindowContainerTransaction#setRelativeBounds */ - void setLastRequestedBounds(@Nullable Rect bounds) { - if (bounds == null) { + void setLastRequestedBounds(@Nullable Rect relBounds) { + if (relBounds == null) { mLastRequestedBounds.setEmpty(); } else { - mLastRequestedBounds.set(bounds); + mLastRequestedBounds.set(relBounds); } } - @NonNull - Rect getLastRequestedBounds() { - return mLastRequestedBounds; - } - /** * Checks if last requested windowing mode is equal to the provided value. + * @see WindowContainerTransaction#setWindowingMode */ boolean isLastRequestedWindowingModeEqual(@WindowingMode int windowingMode) { return mLastRequestedWindowingMode == windowingMode; @@ -429,11 +664,111 @@ class TaskFragmentContainer { /** * Updates the last requested windowing mode. + * @see WindowContainerTransaction#setWindowingMode */ void setLastRequestedWindowingMode(@WindowingMode int windowingModes) { mLastRequestedWindowingMode = windowingModes; } + /** + * Checks if last requested {@link TaskFragmentAnimationParams} are equal to the provided value. + * @see android.window.TaskFragmentOperation#OP_TYPE_SET_ANIMATION_PARAMS + */ + boolean areLastRequestedAnimationParamsEqual( + @NonNull TaskFragmentAnimationParams animationParams) { + return mLastAnimationParams.equals(animationParams); + } + + /** + * Updates the last requested {@link TaskFragmentAnimationParams}. + * @see android.window.TaskFragmentOperation#OP_TYPE_SET_ANIMATION_PARAMS + */ + void setLastRequestAnimationParams(@NonNull TaskFragmentAnimationParams animationParams) { + mLastAnimationParams = animationParams; + } + + /** + * Checks if last requested adjacent TaskFragment token and params are equal to the provided + * values. + * @see android.window.TaskFragmentOperation#OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS + * @see android.window.TaskFragmentOperation#OP_TYPE_CLEAR_ADJACENT_TASK_FRAGMENTS + */ + boolean isLastAdjacentTaskFragmentEqual(@Nullable IBinder fragmentToken, + @Nullable WindowContainerTransaction.TaskFragmentAdjacentParams params) { + return Objects.equals(mLastAdjacentTaskFragment, fragmentToken) + && Objects.equals(mLastAdjacentParams, params); + } + + /** + * Updates the last requested adjacent TaskFragment token and params. + * @see android.window.TaskFragmentOperation#OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS + */ + void setLastAdjacentTaskFragment(@NonNull IBinder fragmentToken, + @NonNull WindowContainerTransaction.TaskFragmentAdjacentParams params) { + mLastAdjacentTaskFragment = fragmentToken; + mLastAdjacentParams = params; + } + + /** + * Clears the last requested adjacent TaskFragment token and params. + * @see android.window.TaskFragmentOperation#OP_TYPE_CLEAR_ADJACENT_TASK_FRAGMENTS + */ + void clearLastAdjacentTaskFragment() { + final TaskFragmentContainer lastAdjacentTaskFragment = mLastAdjacentTaskFragment != null + ? mController.getContainer(mLastAdjacentTaskFragment) + : null; + mLastAdjacentTaskFragment = null; + mLastAdjacentParams = null; + if (lastAdjacentTaskFragment != null) { + // Clear the previous adjacent TaskFragment as well. + lastAdjacentTaskFragment.clearLastAdjacentTaskFragment(); + } + } + + /** + * Checks if last requested companion TaskFragment token is equal to the provided value. + * @see android.window.TaskFragmentOperation#OP_TYPE_SET_COMPANION_TASK_FRAGMENT + */ + boolean isLastCompanionTaskFragmentEqual(@Nullable IBinder fragmentToken) { + return Objects.equals(mLastCompanionTaskFragment, fragmentToken); + } + + /** + * Updates the last requested companion TaskFragment token. + * @see android.window.TaskFragmentOperation#OP_TYPE_SET_COMPANION_TASK_FRAGMENT + */ + void setLastCompanionTaskFragment(@Nullable IBinder fragmentToken) { + mLastCompanionTaskFragment = fragmentToken; + } + + /** + * Adds the pending appeared activity that has requested to be launched in this task fragment. + * @see android.app.ActivityClient#isRequestedToLaunchInTaskFragment + */ + void addPendingAppearedInRequestedTaskFragmentActivity(Activity activity) { + final IBinder activityToken = activity.getActivityToken(); + if (hasActivity(activityToken)) { + return; + } + mPendingAppearedInRequestedTaskFragmentActivities.add(activity.getActivityToken()); + } + + /** + * Checks if the given activity has requested to be launched in this task fragment. + * @see #addPendingAppearedInRequestedTaskFragmentActivity + */ + boolean isActivityInRequestedTaskFragment(IBinder activityToken) { + if (mInfo != null && mInfo.getActivitiesRequestedInTaskFragment().contains(activityToken)) { + return true; + } + return mPendingAppearedInRequestedTaskFragmentActivities.contains(activityToken); + } + + /** Whether contains activities of another process */ + boolean hasCrossProcessActivities() { + return mHasCrossProcessActivities; + } + /** Gets the parent leaf Task id. */ int getTaskId() { return mTaskContainer.getTaskId(); @@ -452,7 +787,8 @@ class TaskFragmentContainer { } int maxMinWidth = mInfo.getMinimumWidth(); int maxMinHeight = mInfo.getMinimumHeight(); - for (Activity activity : mPendingAppearedActivities) { + for (IBinder activityToken : mPendingAppearedActivities) { + final Activity activity = mController.getActivity(activityToken); final Size minDimensions = SplitPresenter.getMinDimensions(activity); if (minDimensions == null) { continue; @@ -470,6 +806,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/embedding/TransactionManager.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TransactionManager.java new file mode 100644 index 000000000000..396956e56bb5 --- /dev/null +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TransactionManager.java @@ -0,0 +1,203 @@ +/* + * 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.embedding; + +import static android.window.TaskFragmentOrganizer.TASK_FRAGMENT_TRANSIT_CHANGE; +import static android.window.TaskFragmentOrganizer.TASK_FRAGMENT_TRANSIT_NONE; + +import android.os.IBinder; +import android.window.TaskFragmentOrganizer; +import android.window.TaskFragmentOrganizer.TaskFragmentTransitionType; +import android.window.WindowContainerTransaction; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.internal.annotations.VisibleForTesting; + +/** + * Responsible for managing the current {@link WindowContainerTransaction} as a response to device + * state changes and app interactions. + * + * A typical use flow: + * 1. Call {@link #startNewTransaction} to start tracking the changes. + * 2. Use {@link TransactionRecord#setOriginType(int)} (int)} to record the type of operation that + * will start a new transition on system server. + * 3. Use {@link #getCurrentTransactionRecord()} to get current {@link TransactionRecord} for + * changes. + * 4. Call {@link TransactionRecord#apply(boolean)} to request the system server to apply changes in + * the current {@link WindowContainerTransaction}, or call {@link TransactionRecord#abort()} to + * dispose the current one. + * + * Note: + * There should be only one transaction at a time. The caller should not call + * {@link #startNewTransaction} again before calling {@link TransactionRecord#apply(boolean)} or + * {@link TransactionRecord#abort()} to the previous transaction. + */ +class TransactionManager { + + @NonNull + private final TaskFragmentOrganizer mOrganizer; + + @Nullable + private TransactionRecord mCurrentTransaction; + + TransactionManager(@NonNull TaskFragmentOrganizer organizer) { + mOrganizer = organizer; + } + + @NonNull + TransactionRecord startNewTransaction() { + return startNewTransaction(null /* taskFragmentTransactionToken */); + } + + /** + * Starts tracking the changes in a new {@link WindowContainerTransaction}. Caller can call + * {@link #getCurrentTransactionRecord()} later to continue adding change to the current + * transaction until {@link TransactionRecord#apply(boolean)} or + * {@link TransactionRecord#abort()} is called. + * @param taskFragmentTransactionToken {@link android.window.TaskFragmentTransaction + * #getTransactionToken()} if this is a response to a + * {@link android.window.TaskFragmentTransaction}. + */ + @NonNull + TransactionRecord startNewTransaction(@Nullable IBinder taskFragmentTransactionToken) { + if (mCurrentTransaction != null) { + mCurrentTransaction = null; + throw new IllegalStateException( + "The previous transaction has not been applied or aborted,"); + } + mCurrentTransaction = new TransactionRecord(taskFragmentTransactionToken); + return mCurrentTransaction; + } + + /** + * Gets the current {@link TransactionRecord} started from {@link #startNewTransaction}. + */ + @NonNull + TransactionRecord getCurrentTransactionRecord() { + if (mCurrentTransaction == null) { + throw new IllegalStateException("startNewTransaction() is not invoked before calling" + + " getCurrentTransactionRecord()."); + } + return mCurrentTransaction; + } + + /** The current transaction. The manager should only handle one transaction at a time. */ + class TransactionRecord { + /** + * {@link WindowContainerTransaction} containing the current change. + * @see #startNewTransaction(IBinder) + * @see #apply (boolean) + */ + @NonNull + private final WindowContainerTransaction mTransaction = new WindowContainerTransaction(); + + /** + * If the current transaction is a response to a + * {@link android.window.TaskFragmentTransaction}, this is the + * {@link android.window.TaskFragmentTransaction#getTransactionToken()}. + * @see #startNewTransaction(IBinder) + */ + @Nullable + private final IBinder mTaskFragmentTransactionToken; + + /** + * To track of the origin type of the current {@link #mTransaction}. When + * {@link #apply (boolean)} to start a new transition, this is the type to request. + * @see #setOriginType(int) + * @see #getTransactionTransitionType() + */ + @TaskFragmentTransitionType + private int mOriginType = TASK_FRAGMENT_TRANSIT_NONE; + + TransactionRecord(@Nullable IBinder taskFragmentTransactionToken) { + mTaskFragmentTransactionToken = taskFragmentTransactionToken; + } + + @NonNull + WindowContainerTransaction getTransaction() { + ensureCurrentTransaction(); + return mTransaction; + } + + /** + * Sets the {@link TaskFragmentTransitionType} that triggers this transaction. If there are + * multiple calls, only the first call will be respected as the "origin" type. + */ + void setOriginType(@TaskFragmentTransitionType int type) { + ensureCurrentTransaction(); + if (mOriginType != TASK_FRAGMENT_TRANSIT_NONE) { + // Skip if the origin type has already been set. + return; + } + mOriginType = type; + } + + /** + * Requests the system server to apply the current transaction started from + * {@link #startNewTransaction}. + * @param shouldApplyIndependently If {@code true}, the {@link #mCurrentTransaction} will + * request a new transition, which will be queued until the + * sync engine is free if there is any other active sync. + * If {@code false}, the {@link #startNewTransaction} will + * be directly applied to the active sync. + */ + void apply(boolean shouldApplyIndependently) { + ensureCurrentTransaction(); + if (mTaskFragmentTransactionToken != null) { + // If this is a response to a TaskFragmentTransaction. + mOrganizer.onTransactionHandled(mTaskFragmentTransactionToken, mTransaction, + getTransactionTransitionType(), shouldApplyIndependently); + } else { + mOrganizer.applyTransaction(mTransaction, getTransactionTransitionType(), + shouldApplyIndependently); + } + dispose(); + } + + /** Called when there is no need to {@link #apply(boolean)} the current transaction. */ + void abort() { + ensureCurrentTransaction(); + dispose(); + } + + private void dispose() { + TransactionManager.this.mCurrentTransaction = null; + } + + private void ensureCurrentTransaction() { + if (TransactionManager.this.mCurrentTransaction != this) { + throw new IllegalStateException( + "This transaction has already been apply() or abort()."); + } + } + + /** + * Gets the {@link TaskFragmentTransitionType} that we will request transition with for the + * current {@link WindowContainerTransaction}. + */ + @VisibleForTesting + @TaskFragmentTransitionType + int getTransactionTransitionType() { + // Use TASK_FRAGMENT_TRANSIT_CHANGE as default if there is not opening/closing window. + return mOriginType != TASK_FRAGMENT_TRANSIT_NONE + ? mOriginType + : TASK_FRAGMENT_TRANSIT_CHANGE; + } + } +} 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..8386131b177d 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java @@ -20,35 +20,40 @@ 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.view.WindowManager; +import android.window.TaskFragmentOrganizer; +import androidx.annotation.GuardedBy; 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; -import androidx.window.common.RawFoldingFeatureProducer; +import androidx.window.extensions.core.util.function.Consumer; import androidx.window.util.DataProducer; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Map; -import java.util.Optional; +import java.util.Objects; import java.util.Set; -import java.util.function.Consumer; /** * Reference implementation of androidx.window.extensions.layout OEM interface for use with @@ -61,18 +66,44 @@ 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 Object mLock = new Object(); + + @GuardedBy("mLock") + private final Map<Context, Consumer<WindowLayoutInfo>> mWindowLayoutChangeListeners = new ArrayMap<>(); + @GuardedBy("mLock") private final DataProducer<List<CommonFoldingFeature>> mFoldingFeatureProducer; - public WindowLayoutComponentImpl(Context context) { + @GuardedBy("mLock") + private final List<CommonFoldingFeature> mLastReportedFoldingFeatures = new ArrayList<>(); + + @GuardedBy("mLock") + private final Map<IBinder, ConfigurationChangeListener> mConfigurationChangeListeners = + new ArrayMap<>(); + + @GuardedBy("mLock") + private final Map<java.util.function.Consumer<WindowLayoutInfo>, Consumer<WindowLayoutInfo>> + mJavaToExtConsumers = new ArrayMap<>(); + + private final TaskFragmentOrganizer mTaskFragmentOrganizer; + + public WindowLayoutComponentImpl(@NonNull Context context, + @NonNull TaskFragmentOrganizer taskFragmentOrganizer, + @NonNull DeviceStateManagerFoldingFeatureProducer foldingFeatureProducer) { ((Application) context.getApplicationContext()) .registerActivityLifecycleCallbacks(new NotifyOnConfigurationChanged()); - RawFoldingFeatureProducer foldingFeatureProducer = new RawFoldingFeatureProducer(context); - mFoldingFeatureProducer = new DeviceStateManagerFoldingFeatureProducer(context, - foldingFeatureProducer); + mFoldingFeatureProducer = foldingFeatureProducer; mFoldingFeatureProducer.addDataChangedCallback(this::onDisplayFeaturesChanged); + mTaskFragmentOrganizer = taskFragmentOrganizer; + } + + /** Registers to listen to {@link CommonFoldingFeature} changes */ + public void addFoldingStateChangedCallback( + java.util.function.Consumer<List<CommonFoldingFeature>> consumer) { + synchronized (mLock) { + mFoldingFeatureProducer.addDataChangedCallback(consumer); + } } /** @@ -81,59 +112,129 @@ 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(); + @NonNull java.util.function.Consumer<WindowLayoutInfo> consumer) { + final Consumer<WindowLayoutInfo> extConsumer = consumer::accept; + synchronized (mLock) { + mJavaToExtConsumers.put(consumer, extConsumer); + } + addWindowLayoutInfoListener(activity, extConsumer); + } + + @Override + public void addWindowLayoutInfoListener(@NonNull @UiContext Context context, + @NonNull java.util.function.Consumer<WindowLayoutInfo> consumer) { + final Consumer<WindowLayoutInfo> extConsumer = consumer::accept; + synchronized (mLock) { + mJavaToExtConsumers.put(consumer, extConsumer); + } + addWindowLayoutInfoListener(context, extConsumer); } /** - * Removes a listener no longer interested in receiving updates. + * Similar to {@link #addWindowLayoutInfoListener(Activity, java.util.function.Consumer)}, but + * takes a UI Context as a parameter. * - * @param consumer no longer interested in receiving updates to {@link WindowLayoutInfo} + * Jetpack {@link androidx.window.layout.ExtensionWindowLayoutInfoBackend} makes sure all + * consumers related to the same {@link Context} gets updated {@link WindowLayoutInfo} + * together. However only the first registered consumer of a {@link Context} will actually + * invoke {@link #addWindowLayoutInfoListener(Context, Consumer)}. + * Here we enforce that {@link #addWindowLayoutInfoListener(Context, Consumer)} can only be + * called once for each {@link Context}. */ - public void removeWindowLayoutInfoListener( + @Override + public void addWindowLayoutInfoListener(@NonNull @UiContext Context context, @NonNull Consumer<WindowLayoutInfo> consumer) { - mWindowLayoutChangeListeners.values().remove(consumer); - onDisplayFeaturesChanged(); + synchronized (mLock) { + if (mWindowLayoutChangeListeners.containsKey(context) + // In theory this method can be called on the same consumer with different + // context. + || mWindowLayoutChangeListeners.containsValue(consumer)) { + return; + } + if (!context.isUiContext()) { + throw new IllegalArgumentException("Context must be a UI Context, which should be" + + " an Activity, WindowContext or InputMethodService"); + } + mFoldingFeatureProducer.getData((features) -> { + WindowLayoutInfo newWindowLayout = getWindowLayoutInfo(context, features); + consumer.accept(newWindowLayout); + }); + mWindowLayoutChangeListeners.put(context, consumer); + + final IBinder windowContextToken = context.getWindowContextToken(); + if (windowContextToken != null) { + // We register component callbacks for window contexts. For activity contexts, they + // will receive callbacks from NotifyOnConfigurationChanged instead. + final ConfigurationChangeListener listener = + new ConfigurationChangeListener(windowContextToken); + context.registerComponentCallbacks(listener); + mConfigurationChangeListeners.put(windowContextToken, listener); + } + } + } + + @Override + public void removeWindowLayoutInfoListener( + @NonNull java.util.function.Consumer<WindowLayoutInfo> consumer) { + final Consumer<WindowLayoutInfo> extConsumer; + synchronized (mLock) { + extConsumer = mJavaToExtConsumers.remove(consumer); + } + if (extConsumer != null) { + removeWindowLayoutInfoListener(extConsumer); + } } - 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) { + synchronized (mLock) { + for (Context context : mWindowLayoutChangeListeners.keySet()) { + if (!mWindowLayoutChangeListeners.get(context).equals(consumer)) { + continue; + } + final IBinder token = context.getWindowContextToken(); + if (token != null) { + context.unregisterComponentCallbacks(mConfigurationChangeListeners.get(token)); + mConfigurationChangeListeners.remove(token); + } + break; + } + mWindowLayoutChangeListeners.values().remove(consumer); } } + @GuardedBy("mLock") @NonNull - Set<Activity> getActivitiesListeningForLayoutChanges() { + private Set<Context> getContextsListeningForLayoutChanges() { return mWindowLayoutChangeListeners.keySet(); } - @NonNull + @GuardedBy("mLock") 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; } } return false; } - protected boolean hasListeners() { - return !mWindowLayoutChangeListeners.isEmpty(); - } - /** * 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 +248,56 @@ 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) { + synchronized (mLock) { + mLastReportedFoldingFeatures.clear(); + mLastReportedFoldingFeatures.addAll(storedFeatures); + for (Context context : getContextsListeningForLayoutChanges()) { + // Get the WindowLayoutInfo from the activity and pass the value to the + // layoutConsumer. + Consumer<WindowLayoutInfo> layoutConsumer = mWindowLayoutChangeListeners.get( + context); + WindowLayoutInfo newWindowLayout = getWindowLayoutInfo(context, storedFeatures); + layoutConsumer.accept(newWindowLayout); + } } } + /** + * Translates the {@link DisplayFeature} into a {@link WindowLayoutInfo} when a + * valid state is found. + * + * @param context a proxy for the {@link android.view.Window} that contains the + * {@link DisplayFeature}. + */ + private WindowLayoutInfo getWindowLayoutInfo(@NonNull @UiContext Context context, + List<CommonFoldingFeature> storedFeatures) { + List<DisplayFeature> displayFeatureList = getDisplayFeatures(context, storedFeatures); + return new WindowLayoutInfo(displayFeatureList); + } + + /** + * Gets the current {@link WindowLayoutInfo} computed with passed {@link WindowConfiguration}. + * + * @return current {@link WindowLayoutInfo} on the default display. Returns + * empty {@link WindowLayoutInfo} on secondary displays. + */ @NonNull - private WindowLayoutInfo getWindowLayoutInfo(@NonNull Activity activity) { - List<DisplayFeature> displayFeatures = getDisplayFeatures(activity); - return new WindowLayoutInfo(displayFeatures); + public WindowLayoutInfo getCurrentWindowLayoutInfo(int displayId, + @NonNull WindowConfiguration windowConfiguration) { + synchronized (mLock) { + return getWindowLayoutInfo(displayId, windowConfiguration, + mLastReportedFoldingFeatures); + } + } + + /** @see #getWindowLayoutInfo(Context, List) */ + private WindowLayoutInfo getWindowLayoutInfo(int displayId, + @NonNull WindowConfiguration windowConfiguration, + List<CommonFoldingFeature> storedFeatures) { + List<DisplayFeature> displayFeatureList = getDisplayFeatures(displayId, windowConfiguration, + storedFeatures); + return new WindowLayoutInfo(displayFeatureList); } /** @@ -173,97 +313,145 @@ 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} + * @return a {@link List} of {@link DisplayFeature}s that are within the + * {@link android.view.Window} of the {@link Activity} */ - private List<DisplayFeature> getDisplayFeatures(@NonNull Activity activity) { - List<DisplayFeature> features = new ArrayList<>(); - int displayId = activity.getDisplay().getDisplayId(); - if (displayId != DEFAULT_DISPLAY) { - Log.w(TAG, "This sample doesn't support display features on secondary displays"); - return features; + private List<DisplayFeature> getDisplayFeatures( + @NonNull @UiContext Context context, List<CommonFoldingFeature> storedFeatures) { + if (!shouldReportDisplayFeatures(context)) { + return Collections.emptyList(); } + return getDisplayFeatures(context.getDisplayId(), + context.getResources().getConfiguration().windowConfiguration, + storedFeatures); + } - 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. + /** @see #getDisplayFeatures(Context, List) */ + private List<DisplayFeature> getDisplayFeatures(int displayId, + @NonNull WindowConfiguration windowConfiguration, + List<CommonFoldingFeature> storedFeatures) { + List<DisplayFeature> features = new ArrayList<>(); + if (displayId != DEFAULT_DISPLAY) { return features; } - 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); + for (CommonFoldingFeature baseFeature : storedFeatures) { + Integer state = convertToExtensionState(baseFeature.getState()); + if (state == null) { + continue; + } + Rect featureRect = baseFeature.getRect(); + rotateRectToDisplayRotation(displayId, featureRect); + transformToWindowSpaceRect(windowConfiguration, 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}. + * Calculates if the display features should be reported for the UI Context. The calculation + * uses the task information because that is accurate for Activities in ActivityEmbedding mode. + * TODO(b/238948678): Support reporting display features in all windowing modes. + * + * @return true if the display features should be reported for the UI Context, false otherwise. */ - 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; + IBinder activityToken = context.getActivityToken(); + if (activityToken != null) { + final Configuration taskConfig = ActivityClient.getInstance().getTaskConfiguration( + activityToken); + if (taskConfig == null) { + // If we cannot determine the task configuration 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; } + final Rect taskBounds = taskConfig.windowConfiguration.getBounds(); + final WindowManager windowManager = Objects.requireNonNull( + context.getSystemService(WindowManager.class)); + final Rect currentBounds = windowManager.getCurrentWindowMetrics().getBounds(); + final Rect maxBounds = windowManager.getMaximumWindowMetrics().getBounds(); + boolean isTaskExpanded = maxBounds.equals(taskBounds); + boolean isActivityExpanded = maxBounds.equals(currentBounds); + /* + * We need to proxy being in full screen because when a user enters PiP and exits PiP + * the task windowingMode will report multi-window/pinned until the transition is + * finished in WM Shell. + * maxBounds == taskWindowBounds is a proxy check to verify the window is full screen + * For tasks that are letterboxed, we use currentBounds == maxBounds to filter these + * out. + */ + // TODO(b/262900133) remove currentBounds check when letterboxed apps report bounds. + // currently we don't want to report to letterboxed apps since they do not update the + // window bounds when the Activity is moved. An inaccurate fold will be reported so + // we skip. + return isTaskExpanded && (isActivityExpanded + || mTaskFragmentOrganizer.isActivityEmbedded(activityToken)); + } else { + // TODO(b/242674941): use task windowing mode for window context that associates with + // activity. + windowingMode = context.getResources().getConfiguration().windowConfiguration + .getWindowingMode(); } - if (task == null) { - // The task might be removed on the server already. - return true; - } - 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; + @GuardedBy("mLock") + 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); + synchronized (mLock) { + onDisplayFeaturesChangedIfListening(activity.getActivityToken()); + } } @Override public void onActivityConfigurationChanged(Activity activity) { super.onActivityConfigurationChanged(activity); - onDisplayFeaturesChangedIfListening(activity); + synchronized (mLock) { + onDisplayFeaturesChangedIfListening(activity.getActivityToken()); + } + } + } + + private final class ConfigurationChangeListener implements ComponentCallbacks { + final IBinder mToken; + + ConfigurationChangeListener(IBinder token) { + mToken = token; } - private void onDisplayFeaturesChangedIfListening(Activity activity) { - IBinder token = activity.getWindow().getAttributes().token; - if (token == null || isListeningForLayoutChanges(token)) { - onDisplayFeaturesChanged(); + @Override + public void onConfigurationChanged(@NonNull Configuration newConfig) { + synchronized (mLock) { + 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..fe60037483c4 --- /dev/null +++ b/libs/WindowManager/Jetpack/src/androidx/window/util/AcceptOnceConsumer.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 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 AcceptOnceProducerCallback<T> mProducer; + + public AcceptOnceConsumer(@NonNull AcceptOnceProducerCallback<T> producer, + @NonNull Consumer<T> callback) { + mProducer = producer; + mCallback = callback; + } + + @Override + public void accept(@NonNull T t) { + mCallback.accept(t); + mProducer.onConsumerReadyToBeRemoved(this); + } + + /** + * Interface to allow the {@link AcceptOnceConsumer} to notify the client that created it, + * when it is ready to be removed. This allows the client to remove the consumer object + * when it deems it is safe to do so. + * @param <T> The type of data this callback accepts through {@link #onConsumerReadyToBeRemoved} + */ + public interface AcceptOnceProducerCallback<T> { + + /** + * Notifies that the given {@code callback} is ready to be removed + */ + void onConsumerReadyToBeRemoved(Consumer<T> callback); + } +} diff --git a/libs/WindowManager/Jetpack/src/androidx/window/util/BaseDataProducer.java b/libs/WindowManager/Jetpack/src/androidx/window/util/BaseDataProducer.java index 930db3b701b7..46c925aaf8a2 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/util/BaseDataProducer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/util/BaseDataProducer.java @@ -16,41 +16,99 @@ package androidx.window.util; +import androidx.annotation.GuardedBy; import androidx.annotation.NonNull; +import java.util.HashSet; 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<>(); +public abstract class BaseDataProducer<T> implements DataProducer<T>, + AcceptOnceConsumer.AcceptOnceProducerCallback<T> { + private final Object mLock = new Object(); + @GuardedBy("mLock") + private final Set<Consumer<T>> mCallbacks = new LinkedHashSet<>(); + @GuardedBy("mLock") + private final Set<Consumer<T>> mCallbacksToRemove = new HashSet<>(); + + /** + * 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); + } + removeFinishedCallbacksLocked(); + } + } + + /** + * Removes any callbacks that notified us through {@link #onConsumerReadyToBeRemoved(Consumer)} + * that they are ready to be removed. + */ + @GuardedBy("mLock") + private void removeFinishedCallbacksLocked() { + for (Consumer<T> callback: mCallbacksToRemove) { + mCallbacks.remove(callback); + } + mCallbacksToRemove.clear(); + } + + @Override + public void onConsumerReadyToBeRemoved(Consumer<T> callback) { + synchronized (mLock) { + mCallbacksToRemove.add(callback); } } -} +}
\ No newline at end of file diff --git a/libs/WindowManager/Jetpack/src/androidx/window/util/DataProducer.java b/libs/WindowManager/Jetpack/src/androidx/window/util/DataProducer.java index d4d1a23b756b..ec301dc34aaa 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/util/DataProducer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/util/DataProducer.java @@ -18,26 +18,27 @@ package androidx.window.util; import android.annotation.NonNull; -import java.util.Optional; +import java.util.function.Consumer; /** - * Produces data through {@link #getData()} and provides a mechanism for receiving a callback when - * the data managed by the produces has changed. + * Produces data through {@link DataProducer#getData} and provides a mechanism for receiving + * a callback when the data managed by the produces has changed. * - * @param <T> The type of data this producer returns through {@link #getData()}. + * @param <T> The type of data this producer returns through {@link DataProducer#getData}. */ public interface DataProducer<T> { /** - * Returns the data currently stored in the provider, or {@link Optional#empty()} if the - * provider has no data. + * Emits the first available data at that point in time. + * @param dataConsumer a {@link Consumer} that will receive one value. */ - Optional<T> getData(); + void getData(@NonNull Consumer<T> dataConsumer); /** - * Adds a callback to be notified when the data returned from {@link #getData()} has changed. + * Adds a callback to be notified when the data returned + * from {@link DataProducer#getData} has changed. */ - void addDataChangedCallback(@NonNull Runnable callback); + void addDataChangedCallback(@NonNull Consumer<T> callback); - /** Removes a callback previously added with {@link #addDataChangedCallback(Runnable)}. */ - void removeDataChangedCallback(@NonNull Runnable callback); + /** Removes a callback previously added with {@link #addDataChangedCallback(Consumer)}. */ + void removeDataChangedCallback(@NonNull Consumer<T> callback); } diff --git a/libs/WindowManager/Jetpack/src/androidx/window/util/ExtensionHelper.java b/libs/WindowManager/Jetpack/src/androidx/window/util/ExtensionHelper.java index 2a593f15a9de..9e2611f392a3 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/util/ExtensionHelper.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/util/ExtensionHelper.java @@ -21,14 +21,16 @@ import static android.view.Surface.ROTATION_180; import static android.view.Surface.ROTATION_270; import static android.view.Surface.ROTATION_90; -import android.app.Activity; +import android.app.WindowConfiguration; +import android.content.Context; import android.graphics.Rect; import android.hardware.display.DisplayManagerGlobal; import android.view.DisplayInfo; import android.view.Surface; +import android.view.WindowManager; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; +import androidx.annotation.UiContext; /** * Util class for both Sidecar and Extensions. @@ -86,26 +88,31 @@ public final class ExtensionHelper { } /** Transforms rectangle from absolute coordinate space to the window coordinate space. */ - public static void transformToWindowSpaceRect(Activity activity, Rect inOutRect) { - Rect windowRect = getWindowBounds(activity); - if (windowRect == null) { - inOutRect.setEmpty(); - return; - } - if (!Rect.intersects(inOutRect, windowRect)) { + public static void transformToWindowSpaceRect(@NonNull @UiContext Context context, + Rect inOutRect) { + transformToWindowSpaceRect(getWindowBounds(context), inOutRect); + } + + /** @see ExtensionHelper#transformToWindowSpaceRect(Context, Rect) */ + public static void transformToWindowSpaceRect(@NonNull WindowConfiguration windowConfiguration, + Rect inOutRect) { + transformToWindowSpaceRect(windowConfiguration.getBounds(), inOutRect); + } + + private static void transformToWindowSpaceRect(@NonNull Rect bounds, @NonNull Rect inOutRect) { + if (!inOutRect.intersect(bounds)) { inOutRect.setEmpty(); return; } - inOutRect.intersect(windowRect); - inOutRect.offset(-windowRect.left, -windowRect.top); + inOutRect.offset(-bounds.left, -bounds.top); } /** * Gets the current window bounds in absolute coordinates. */ - @Nullable - private static Rect getWindowBounds(@NonNull Activity activity) { - return activity.getWindowManager().getCurrentWindowMetrics().getBounds(); + @NonNull + private static Rect getWindowBounds(@NonNull @UiContext Context context) { + return context.getSystemService(WindowManager.class).getCurrentWindowMetrics().getBounds(); } /** diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/WindowExtensionsTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/WindowExtensionsTest.java index 13a2c78d463e..d189ae2cf72e 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/WindowExtensionsTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/WindowExtensionsTest.java @@ -22,6 +22,7 @@ import android.platform.test.annotations.Presubmit; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; +import androidx.window.extensions.embedding.SplitAttributes; import org.junit.Before; import org.junit.Test; @@ -53,4 +54,15 @@ public class WindowExtensionsTest { public void testGetActivityEmbeddingComponent() { assertThat(mExtensions.getActivityEmbeddingComponent()).isNotNull(); } + + @Test + public void testSplitAttributes_default() { + // Make sure the default value in the extensions aar. + final SplitAttributes splitAttributes = new SplitAttributes.Builder().build(); + assertThat(splitAttributes.getLayoutDirection()) + .isEqualTo(SplitAttributes.LayoutDirection.LOCALE); + assertThat(splitAttributes.getSplitType()) + .isEqualTo(new SplitAttributes.SplitType.RatioSplitType(0.5f)); + assertThat(splitAttributes.getAnimationBackgroundColor()).isEqualTo(0); + } } diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/EmbeddingTestUtils.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/EmbeddingTestUtils.java index effc1a3ef3ea..a069ac7256d6 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/EmbeddingTestUtils.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/EmbeddingTestUtils.java @@ -16,9 +16,13 @@ package androidx.window.extensions.embedding; +import static android.view.Display.DEFAULT_DISPLAY; + import static androidx.window.extensions.embedding.SplitRule.FINISH_ALWAYS; import static androidx.window.extensions.embedding.SplitRule.FINISH_NEVER; +import static org.junit.Assert.assertFalse; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import android.annotation.NonNull; @@ -26,32 +30,72 @@ import android.app.Activity; import android.content.Intent; import android.content.pm.ActivityInfo; import android.content.res.Configuration; +import android.content.res.Resources; import android.graphics.Point; import android.graphics.Rect; import android.util.Pair; +import android.view.WindowMetrics; import android.window.TaskFragmentInfo; import android.window.WindowContainerToken; +import androidx.window.extensions.core.util.function.Predicate; +import androidx.window.extensions.embedding.SplitAttributes.SplitType; +import androidx.window.extensions.layout.DisplayFeature; +import androidx.window.extensions.layout.FoldingFeature; +import androidx.window.extensions.layout.WindowLayoutInfo; + +import java.util.ArrayList; import java.util.Collections; +import java.util.List; +// Suppress GuardedBy warning on unit tests +@SuppressWarnings("GuardedBy") public class EmbeddingTestUtils { static final Rect TASK_BOUNDS = new Rect(0, 0, 600, 1200); static final int TASK_ID = 10; - static final float SPLIT_RATIO = 0.5f; + static final SplitType SPLIT_TYPE = SplitType.RatioSplitType.splitEqually(); + static final SplitAttributes SPLIT_ATTRIBUTES = new SplitAttributes.Builder().build(); + static final String TEST_TAG = "test"; /** Default finish behavior in Jetpack. */ static final int DEFAULT_FINISH_PRIMARY_WITH_SECONDARY = FINISH_NEVER; static final int DEFAULT_FINISH_SECONDARY_WITH_PRIMARY = FINISH_ALWAYS; + private static final float SPLIT_RATIO = 0.5f; private EmbeddingTestUtils() {} /** Gets the bounds of a TaskFragment that is in split. */ static Rect getSplitBounds(boolean isPrimary) { - final int width = (int) (TASK_BOUNDS.width() * SPLIT_RATIO); + return getSplitBounds(isPrimary, false /* shouldSplitHorizontally */); + } + + /** Gets the bounds of a TaskFragment that is in split. */ + static Rect getSplitBounds(boolean isPrimary, boolean shouldSplitHorizontally) { + final int dimension = (int) ( + (shouldSplitHorizontally ? TASK_BOUNDS.height() : TASK_BOUNDS.width()) + * SPLIT_RATIO); + if (shouldSplitHorizontally) { + return isPrimary + ? new Rect( + TASK_BOUNDS.left, + TASK_BOUNDS.top, + TASK_BOUNDS.right, + TASK_BOUNDS.top + dimension) + : new Rect( + TASK_BOUNDS.left, + TASK_BOUNDS.top + dimension, + TASK_BOUNDS.right, + TASK_BOUNDS.bottom); + } return isPrimary - ? new Rect(TASK_BOUNDS.left, TASK_BOUNDS.top, TASK_BOUNDS.left + width, - TASK_BOUNDS.bottom) + ? new Rect( + TASK_BOUNDS.left, + TASK_BOUNDS.top, + TASK_BOUNDS.left + dimension, + TASK_BOUNDS.bottom) : new Rect( - TASK_BOUNDS.left + width, TASK_BOUNDS.top, TASK_BOUNDS.right, + TASK_BOUNDS.left + dimension, + TASK_BOUNDS.top, + TASK_BOUNDS.right, TASK_BOUNDS.bottom); } @@ -65,14 +109,19 @@ public class EmbeddingTestUtils { static SplitRule createSplitRule(@NonNull Activity primaryActivity, @NonNull Intent secondaryIntent, boolean clearTop) { final Pair<Activity, Intent> targetPair = new Pair<>(primaryActivity, secondaryIntent); - return new SplitPairRule.Builder( + return createSplitPairRuleBuilder( activityPair -> false, targetPair::equals, w -> true) - .setSplitRatio(SPLIT_RATIO) + .setDefaultSplitAttributes( + new SplitAttributes.Builder() + .setSplitType(SPLIT_TYPE) + .build() + ) .setShouldClearTop(clearTop) .setFinishPrimaryWithSecondary(DEFAULT_FINISH_PRIMARY_WITH_SECONDARY) .setFinishSecondaryWithPrimary(DEFAULT_FINISH_SECONDARY_WITH_PRIMARY) + .setTag(TEST_TAG) .build(); } @@ -97,29 +146,42 @@ public class EmbeddingTestUtils { @NonNull Activity secondaryActivity, int finishPrimaryWithSecondary, int finishSecondaryWithPrimary, boolean clearTop) { final Pair<Activity, Activity> targetPair = new Pair<>(primaryActivity, secondaryActivity); - return new SplitPairRule.Builder( + return createSplitPairRuleBuilder( targetPair::equals, activityIntentPair -> false, w -> true) - .setSplitRatio(SPLIT_RATIO) + .setDefaultSplitAttributes( + new SplitAttributes.Builder() + .setSplitType(SPLIT_TYPE) + .build() + ) .setFinishPrimaryWithSecondary(finishPrimaryWithSecondary) .setFinishSecondaryWithPrimary(finishSecondaryWithPrimary) .setShouldClearTop(clearTop) + .setTag(TEST_TAG) .build(); } /** Creates a mock TaskFragmentInfo for the given TaskFragment. */ static TaskFragmentInfo createMockTaskFragmentInfo(@NonNull TaskFragmentContainer container, @NonNull Activity activity) { + return createMockTaskFragmentInfo(container, activity, true /* isVisible */); + } + + /** Creates a mock TaskFragmentInfo for the given TaskFragment. */ + static TaskFragmentInfo createMockTaskFragmentInfo(@NonNull TaskFragmentContainer container, + @NonNull Activity activity, boolean isVisible) { return new TaskFragmentInfo(container.getTaskFragmentToken(), mock(WindowContainerToken.class), new Configuration(), 1, - true /* isVisible */, + isVisible, Collections.singletonList(activity.getActivityToken()), + new ArrayList<>(), new Point(), false /* isTaskClearedForReuse */, false /* isTaskFragmentClearedForPip */, + false /* isClearedForReorderActivityToFront */, new Point()); } @@ -130,4 +192,60 @@ public class EmbeddingTestUtils { primaryBounds.width() + 1, primaryBounds.height() + 1); return aInfo; } + + static TaskContainer createTestTaskContainer() { + Resources resources = mock(Resources.class); + doReturn(new Configuration()).when(resources).getConfiguration(); + Activity activity = mock(Activity.class); + doReturn(resources).when(activity).getResources(); + doReturn(DEFAULT_DISPLAY).when(activity).getDisplayId(); + + return new TaskContainer(TASK_ID, activity); + } + + static TaskContainer createTestTaskContainer(@NonNull SplitController controller) { + final TaskContainer taskContainer = createTestTaskContainer(); + final int taskId = taskContainer.getTaskId(); + // Should not call to create TaskContainer with the same task id twice. + assertFalse(controller.mTaskContainers.contains(taskId)); + controller.mTaskContainers.put(taskId, taskContainer); + return taskContainer; + } + + static WindowLayoutInfo createWindowLayoutInfo() { + final FoldingFeature foldingFeature = new FoldingFeature( + new Rect( + TASK_BOUNDS.left, + TASK_BOUNDS.top + TASK_BOUNDS.height() / 2 - 5, + TASK_BOUNDS.right, + TASK_BOUNDS.top + TASK_BOUNDS.height() / 2 + 5 + ), + FoldingFeature.TYPE_HINGE, + FoldingFeature.STATE_HALF_OPENED); + final List<DisplayFeature> displayFeatures = new ArrayList<>(); + displayFeatures.add(foldingFeature); + return new WindowLayoutInfo(displayFeatures); + } + + static ActivityRule.Builder createActivityBuilder( + @NonNull Predicate<Activity> activityPredicate, + @NonNull Predicate<Intent> intentPredicate) { + return new ActivityRule.Builder(activityPredicate, intentPredicate); + } + + static SplitPairRule.Builder createSplitPairRuleBuilder( + @NonNull Predicate<Pair<Activity, Activity>> activitiesPairPredicate, + @NonNull Predicate<Pair<Activity, Intent>> activityIntentPairPredicate, + @NonNull Predicate<WindowMetrics> windowMetricsPredicate) { + return new SplitPairRule.Builder(activitiesPairPredicate, activityIntentPairPredicate, + windowMetricsPredicate); + } + + static SplitPlaceholderRule.Builder createSplitPlaceholderRuleBuilder( + @NonNull Intent placeholderIntent, @NonNull Predicate<Activity> activityPredicate, + @NonNull Predicate<Intent> intentPredicate, + @NonNull Predicate<WindowMetrics> windowMetricsPredicate) { + return new SplitPlaceholderRule.Builder(placeholderIntent, activityPredicate, + intentPredicate, windowMetricsPredicate); + } } 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..dd087e8eb7c9 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 @@ -18,17 +18,15 @@ package androidx.window.extensions.embedding; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; -import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_ID; +import static androidx.window.extensions.embedding.EmbeddingTestUtils.createTestTaskContainer; -import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; -import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; import android.content.Intent; import android.content.res.Configuration; @@ -36,6 +34,7 @@ import android.graphics.Point; import android.os.Handler; import android.platform.test.annotations.Presubmit; import android.window.TaskFragmentInfo; +import android.window.TaskFragmentTransaction; import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; @@ -56,6 +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) @@ -81,45 +82,30 @@ public class JetpackTaskFragmentOrganizerTest { @Test public void testUnregisterOrganizer() { - mOrganizer.startOverrideSplitAnimation(TASK_ID); - mOrganizer.startOverrideSplitAnimation(TASK_ID + 1); + mOrganizer.overrideSplitAnimation(); mOrganizer.unregisterOrganizer(); - verify(mOrganizer).unregisterRemoteAnimations(TASK_ID); - verify(mOrganizer).unregisterRemoteAnimations(TASK_ID + 1); + verify(mOrganizer).unregisterRemoteAnimations(); } @Test - public void testStartOverrideSplitAnimation() { + public void testOverrideSplitAnimation() { assertNull(mOrganizer.mAnimationController); - mOrganizer.startOverrideSplitAnimation(TASK_ID); + mOrganizer.overrideSplitAnimation(); assertNotNull(mOrganizer.mAnimationController); - verify(mOrganizer).registerRemoteAnimations(TASK_ID, - mOrganizer.mAnimationController.mDefinition); - } - - @Test - public void testStopOverrideSplitAnimation() { - mOrganizer.stopOverrideSplitAnimation(TASK_ID); - - verify(mOrganizer, never()).unregisterRemoteAnimations(anyInt()); - - mOrganizer.startOverrideSplitAnimation(TASK_ID); - mOrganizer.stopOverrideSplitAnimation(TASK_ID); - - verify(mOrganizer).unregisterRemoteAnimations(TASK_ID); + verify(mOrganizer).registerRemoteAnimations(mOrganizer.mAnimationController.mDefinition); } @Test public void testExpandTaskFragment() { - final TaskContainer taskContainer = new TaskContainer(TASK_ID); + final TaskContainer taskContainer = createTestTaskContainer(); final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */, - new Intent(), taskContainer, mSplitController); + new Intent(), taskContainer, mSplitController, null /* pairedPrimaryContainer */); final TaskFragmentInfo info = createMockInfo(container); mOrganizer.mFragmentInfos.put(container.getTaskFragmentToken(), info); - container.setInfo(info); + container.setInfo(mTransaction, info); mOrganizer.expandTaskFragment(mTransaction, container.getTaskFragmentToken()); @@ -127,11 +113,19 @@ 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 */, - false /* isVisible */, new ArrayList<>(), new Point(), + false /* isVisible */, new ArrayList<>(), new ArrayList<>(), new Point(), false /* isTaskClearedForReuse */, false /* isTaskFragmentClearedForPip */, - new Point()); + false /* isClearedForReorderActivityToFront */, new Point()); } } 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..ff08782e8cd8 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java @@ -16,18 +16,35 @@ package androidx.window.extensions.embedding; +import static android.app.ActivityManager.START_CANCELED; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; - -import static androidx.window.extensions.embedding.EmbeddingTestUtils.SPLIT_RATIO; +import static android.view.Display.DEFAULT_DISPLAY; +import static android.window.TaskFragmentOperation.OP_TYPE_CREATE_TASK_FRAGMENT; +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 androidx.window.extensions.embedding.EmbeddingTestUtils.DEFAULT_FINISH_PRIMARY_WITH_SECONDARY; +import static androidx.window.extensions.embedding.EmbeddingTestUtils.DEFAULT_FINISH_SECONDARY_WITH_PRIMARY; +import static androidx.window.extensions.embedding.EmbeddingTestUtils.SPLIT_ATTRIBUTES; import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_BOUNDS; import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_ID; +import static androidx.window.extensions.embedding.EmbeddingTestUtils.TEST_TAG; +import static androidx.window.extensions.embedding.EmbeddingTestUtils.createActivityBuilder; import static androidx.window.extensions.embedding.EmbeddingTestUtils.createActivityInfoWithMinDimensions; import static androidx.window.extensions.embedding.EmbeddingTestUtils.createMockTaskFragmentInfo; +import static androidx.window.extensions.embedding.EmbeddingTestUtils.createSplitPairRuleBuilder; +import static androidx.window.extensions.embedding.EmbeddingTestUtils.createSplitPlaceholderRuleBuilder; import static androidx.window.extensions.embedding.EmbeddingTestUtils.createSplitRule; +import static androidx.window.extensions.embedding.EmbeddingTestUtils.createTestTaskContainer; import static androidx.window.extensions.embedding.EmbeddingTestUtils.getSplitBounds; import static androidx.window.extensions.embedding.SplitRule.FINISH_ALWAYS; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doCallRealMethod; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; @@ -50,6 +67,7 @@ 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.times; import android.annotation.NonNull; import android.app.Activity; @@ -65,22 +83,34 @@ import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.platform.test.annotations.Presubmit; +import android.util.ArraySet; +import android.view.WindowInsets; +import android.view.WindowMetrics; import android.window.TaskFragmentInfo; +import android.window.TaskFragmentOrganizer; +import android.window.TaskFragmentParentInfo; +import android.window.TaskFragmentTransaction; import android.window.WindowContainerTransaction; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; +import androidx.window.common.DeviceStateManagerFoldingFeatureProducer; +import androidx.window.extensions.layout.WindowLayoutComponentImpl; +import androidx.window.extensions.layout.WindowLayoutInfo; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Set; +import java.util.function.Consumer; /** * Test class for {@link SplitController}. @@ -88,6 +118,8 @@ import java.util.List; * Build/Install/Run: * atest WMJetpackUnitTests:SplitControllerTest */ +// Suppress GuardedBy warning on unit tests +@SuppressWarnings("GuardedBy") @Presubmit @SmallTest @RunWith(AndroidJUnit4.class) @@ -104,18 +136,36 @@ public class SplitControllerTest { private WindowContainerTransaction mTransaction; @Mock private Handler mHandler; + @Mock + private WindowLayoutComponentImpl mWindowLayoutComponent; private SplitController mSplitController; private SplitPresenter mSplitPresenter; + private Consumer<List<SplitInfo>> mEmbeddingCallback; + private List<SplitInfo> mSplitInfos; + private TransactionManager mTransactionManager; @Before public void setUp() { MockitoAnnotations.initMocks(this); - mSplitController = new SplitController(); + doReturn(new WindowLayoutInfo(new ArrayList<>())).when(mWindowLayoutComponent) + .getCurrentWindowLayoutInfo(anyInt(), any()); + DeviceStateManagerFoldingFeatureProducer producer = + mock(DeviceStateManagerFoldingFeatureProducer.class); + mSplitController = new SplitController(mWindowLayoutComponent, producer); mSplitPresenter = mSplitController.mPresenter; + mSplitInfos = new ArrayList<>(); + mEmbeddingCallback = splitInfos -> { + mSplitInfos.clear(); + mSplitInfos.addAll(splitInfos); + }; + mSplitController.setSplitInfoCallback(mEmbeddingCallback); + mTransactionManager = mSplitController.mTransactionManager; spyOn(mSplitController); spyOn(mSplitPresenter); - doNothing().when(mSplitPresenter).applyTransaction(any()); + spyOn(mEmbeddingCallback); + spyOn(mTransactionManager); + doNothing().when(mSplitPresenter).applyTransaction(any(), anyInt(), anyBoolean()); final Configuration activityConfig = new Configuration(); activityConfig.windowConfiguration.setBounds(TASK_BOUNDS); activityConfig.windowConfiguration.setMaxBounds(TASK_BOUNDS); @@ -126,10 +176,10 @@ public class SplitControllerTest { @Test public void testGetTopActiveContainer() { - final TaskContainer taskContainer = new TaskContainer(TASK_ID); + final TaskContainer taskContainer = createTestTaskContainer(); // tf1 has no running activity so is not active. final TaskFragmentContainer tf1 = new TaskFragmentContainer(null /* activity */, - new Intent(), taskContainer, mSplitController); + new Intent(), taskContainer, mSplitController, null /* pairedPrimaryContainer */); // tf2 has running activity so is active. final TaskFragmentContainer tf2 = mock(TaskFragmentContainer.class); doReturn(1).when(tf2).getRunningActivityCount(); @@ -157,14 +207,14 @@ public class SplitControllerTest { final TaskFragmentInfo info = mock(TaskFragmentInfo.class); doReturn(new ArrayList<>()).when(info).getActivities(); doReturn(true).when(info).isEmpty(); - tf1.setInfo(info); + tf1.setInfo(mTransaction, info); assertWithMessage("Must return tf because we are waiting for tf1 to become non-empty after" + " creation.") .that(mSplitController.getTopActiveContainer(TASK_ID)).isEqualTo(tf1); doReturn(false).when(info).isEmpty(); - tf1.setInfo(info); + tf1.setInfo(mTransaction, info); assertWithMessage("Must return null because tf1 becomes empty.") .that(mSplitController.getTopActiveContainer(TASK_ID)).isNull(); @@ -176,19 +226,23 @@ public class SplitControllerTest { doReturn(tf.getTaskFragmentToken()).when(mInfo).getFragmentToken(); // The TaskFragment has been removed in the server, we only need to cleanup the reference. - mSplitController.onTaskFragmentVanished(mInfo); + mSplitController.onTaskFragmentVanished(mTransaction, mInfo); verify(mSplitPresenter, never()).deleteTaskFragment(any(), any()); verify(mSplitController).removeContainer(tf); - verify(mActivity, never()).finish(); + verify(mTransaction, never()).finishActivity(any()); } @Test public void testOnTaskFragmentAppearEmptyTimeout() { + // Setup to make sure a transaction record is started. + mTransactionManager.startNewTransaction(); final TaskFragmentContainer tf = mSplitController.newContainer(mActivity, TASK_ID); - mSplitController.onTaskFragmentAppearEmptyTimeout(tf); + doCallRealMethod().when(mSplitController).onTaskFragmentAppearEmptyTimeout(any(), any()); + mSplitController.onTaskFragmentAppearEmptyTimeout(mTransaction, tf); - verify(mSplitPresenter).cleanupContainer(tf, false /* shouldFinishDependent */); + verify(mSplitPresenter).cleanupContainer(mTransaction, tf, + false /* shouldFinishDependent */); } @Test @@ -198,6 +252,14 @@ public class SplitControllerTest { assertTrue(tf.hasActivity(mActivity.getActivityToken())); + // When the activity is not finishing, do not clear the record. + doReturn(false).when(mActivity).isFinishing(); + mSplitController.onActivityDestroyed(mActivity); + + assertTrue(tf.hasActivity(mActivity.getActivityToken())); + + // Clear the record when the activity is finishing and destroyed. + doReturn(true).when(mActivity).isFinishing(); mSplitController.onActivityDestroyed(mActivity); assertFalse(tf.hasActivity(mActivity.getActivityToken())); @@ -217,7 +279,8 @@ public class SplitControllerTest { assertNotNull(tf); assertNotNull(taskContainer); - assertEquals(TASK_BOUNDS, taskContainer.getTaskBounds()); + assertEquals(TASK_BOUNDS, taskContainer.getTaskProperties().getConfiguration() + .windowConfiguration.getBounds()); } @Test @@ -228,9 +291,9 @@ public class SplitControllerTest { spyOn(tf); doReturn(mActivity).when(tf).getTopNonFinishingActivity(); doReturn(true).when(tf).isEmpty(); - doReturn(true).when(mSplitController).launchPlaceholderIfNecessary(mActivity, - false /* isOnCreated */); - doNothing().when(mSplitPresenter).updateSplitContainer(any(), any(), any()); + doReturn(true).when(mSplitController).launchPlaceholderIfNecessary(mTransaction, + mActivity, false /* isOnCreated */); + doNothing().when(mSplitPresenter).updateSplitContainer(any(), any()); mSplitController.updateContainer(mTransaction, tf); @@ -249,12 +312,14 @@ public class SplitControllerTest { mSplitController.updateContainer(mTransaction, tf); - verify(mSplitController, never()).dismissPlaceholderIfNecessary(any()); + verify(mSplitController, never()).dismissPlaceholderIfNecessary(any(), any()); // Verify if tf is not in the top splitContainer, final SplitContainer splitContainer = mock(SplitContainer.class); doReturn(tf).when(splitContainer).getPrimaryContainer(); doReturn(tf).when(splitContainer).getSecondaryContainer(); + doReturn(createTestTaskContainer()).when(splitContainer).getTaskContainer(); + doReturn(createSplitRule(mActivity, mActivity)).when(splitContainer).getSplitRule(); final List<SplitContainer> splitContainers = mSplitController.getTaskContainer(TASK_ID).mSplitContainers; splitContainers.add(splitContainer); @@ -263,7 +328,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,56 +337,102 @@ 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); - verify(mSplitPresenter, never()).updateSplitContainer(any(), any(), any()); + verify(mSplitPresenter, never()).updateSplitContainer(any(), any()); // 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); - verify(mSplitPresenter).updateSplitContainer(splitContainer, tf, mTransaction); + verify(mSplitPresenter).updateSplitContainer(splitContainer, mTransaction); + } + + @Test + public void testUpdateContainer_skipIfTaskIsInvisible() { + final Activity r0 = createMockActivity(); + final Activity r1 = createMockActivity(); + addSplitTaskFragments(r0, r1); + final TaskContainer taskContainer = mSplitController.getTaskContainer(TASK_ID); + final TaskFragmentContainer taskFragmentContainer = taskContainer.mContainers.get(0); + spyOn(taskContainer); + + // No update when the Task is invisible. + clearInvocations(mSplitPresenter); + doReturn(false).when(taskContainer).isVisible(); + mSplitController.updateContainer(mTransaction, taskFragmentContainer); + + verify(mSplitPresenter, never()).updateSplitContainer(any(), any()); + + // Update the split when the Task is visible. + doReturn(true).when(taskContainer).isVisible(); + mSplitController.updateContainer(mTransaction, taskFragmentContainer); + + verify(mSplitPresenter).updateSplitContainer(taskContainer.mSplitContainers.get(0), + mTransaction); + } + + @Test + public void testOnStartActivityResultError() { + final Intent intent = new Intent(); + final TaskContainer taskContainer = createTestTaskContainer(); + final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */, + intent, taskContainer, mSplitController, null /* pairedPrimaryContainer */); + 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()); } @@ -329,7 +440,7 @@ public class SplitControllerTest { @Test public void testResolveStartActivityIntent_withoutLaunchingActivity() { final Intent intent = new Intent(); - final ActivityRule expandRule = new ActivityRule.Builder(r -> false, i -> i == intent) + final ActivityRule expandRule = createActivityBuilder(r -> false, i -> i == intent) .setShouldAlwaysExpand(true) .build(); mSplitController.setEmbeddingRules(Collections.singleton(expandRule)); @@ -454,7 +565,6 @@ public class SplitControllerTest { assertNotNull(mSplitController.getActiveSplitForContainers(primaryContainer, container)); assertTrue(primaryContainer.areLastRequestedBoundsEqual(null)); assertTrue(container.areLastRequestedBoundsEqual(null)); - assertEquals(container, mSplitController.getContainerWithActivity(secondaryActivity)); } @Test @@ -483,30 +593,33 @@ 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); - verify(mSplitController, never()).newContainer(any(), any(), any(), anyInt()); + verify(mSplitController, never()).newContainer(any(), any(), any(), anyInt(), any()); } @Test @@ -514,7 +627,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 +635,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 +645,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,30 +659,33 @@ 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 public void testResolveActivityToContainer_placeholderRule_notInTaskFragment() { + // Setup to make sure a transaction record is started. + mTransactionManager.startNewTransaction(); setupPlaceholderRule(mActivity); final SplitPlaceholderRule placeholderRule = (SplitPlaceholderRule) mSplitController.getSplitRules().get(0); // Launch placeholder if the activity is not in any TaskFragment. - final boolean result = mSplitController.resolveActivityToContainer(mActivity, + final boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity, false /* isOnReparent */); assertTrue(result); - verify(mSplitPresenter).startActivityToSide(mActivity, PLACEHOLDER_INTENT, + verify(mSplitPresenter).startActivityToSide(mTransaction, mActivity, PLACEHOLDER_INTENT, mSplitController.getPlaceholderOptions(mActivity, true /* isOnCreated */), - placeholderRule, true /* isPlaceholder */); + placeholderRule, SPLIT_ATTRIBUTES, true /* isPlaceholder */); } @Test @@ -579,29 +696,31 @@ public class SplitControllerTest { final Activity activity = createMockActivity(); mSplitController.newContainer(mActivity, TASK_ID); mSplitController.newContainer(activity, TASK_ID); - final boolean result = mSplitController.resolveActivityToContainer(mActivity, + final boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity, false /* isOnReparent */); - assertFalse(result); - verify(mSplitPresenter, never()).startActivityToSide(any(), any(), any(), any(), - anyBoolean()); + assertTrue(result); + verify(mSplitPresenter, never()).startActivityToSide(any(), any(), any(), any(), any(), + any(), anyBoolean()); } @Test public void testResolveActivityToContainer_placeholderRule_inTopMostTaskFragment() { + // Setup to make sure a transaction record is started. + mTransactionManager.startNewTransaction(); setupPlaceholderRule(mActivity); final SplitPlaceholderRule placeholderRule = (SplitPlaceholderRule) mSplitController.getSplitRules().get(0); // Launch placeholder if the activity is in the topmost expanded TaskFragment. mSplitController.newContainer(mActivity, TASK_ID); - final boolean result = mSplitController.resolveActivityToContainer(mActivity, + final boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity, false /* isOnReparent */); assertTrue(result); - verify(mSplitPresenter).startActivityToSide(mActivity, PLACEHOLDER_INTENT, + verify(mSplitPresenter).startActivityToSide(mTransaction, mActivity, PLACEHOLDER_INTENT, mSplitController.getPlaceholderOptions(mActivity, true /* isOnCreated */), - placeholderRule, true /* isPlaceholder */); + placeholderRule, SPLIT_ATTRIBUTES, true /* isPlaceholder */); } @Test @@ -611,16 +730,18 @@ public class SplitControllerTest { // Don't launch placeholder if the activity is in primary split. final Activity secondaryActivity = createMockActivity(); addSplitTaskFragments(mActivity, secondaryActivity); - final boolean result = mSplitController.resolveActivityToContainer(mActivity, + final boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity, false /* isOnReparent */); - assertFalse(result); - verify(mSplitPresenter, never()).startActivityToSide(any(), any(), any(), any(), - anyBoolean()); + assertTrue(result); + verify(mSplitPresenter, never()).startActivityToSide(any(), any(), any(), any(), any(), + any(), anyBoolean()); } @Test public void testResolveActivityToContainer_placeholderRule_inSecondarySplit() { + // Setup to make sure a transaction record is started. + mTransactionManager.startNewTransaction(); setupPlaceholderRule(mActivity); final SplitPlaceholderRule placeholderRule = (SplitPlaceholderRule) mSplitController.getSplitRules().get(0); @@ -628,13 +749,13 @@ public class SplitControllerTest { // Launch placeholder if the activity is in secondary split. final Activity primaryActivity = createMockActivity(); addSplitTaskFragments(primaryActivity, mActivity); - final boolean result = mSplitController.resolveActivityToContainer(mActivity, + final boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity, false /* isOnReparent */); assertTrue(result); - verify(mSplitPresenter).startActivityToSide(mActivity, PLACEHOLDER_INTENT, + verify(mSplitPresenter).startActivityToSide(mTransaction, mActivity, PLACEHOLDER_INTENT, mSplitController.getPlaceholderOptions(mActivity, true /* isOnCreated */), - placeholderRule, true /* isPlaceholder */); + placeholderRule, SPLIT_ATTRIBUTES, true /* isPlaceholder */); } @Test @@ -653,14 +774,15 @@ public class SplitControllerTest { primaryContainer, mActivity, secondaryContainer, - splitRule); + splitRule, + SPLIT_ATTRIBUTES); clearInvocations(mSplitController); - final boolean result = mSplitController.resolveActivityToContainer(mActivity, + final boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity, false /* isOnReparent */); assertTrue(result); - verify(mSplitController, never()).newContainer(any(), any(), any(), anyInt()); - verify(mSplitController, never()).registerSplit(any(), any(), any(), any(), any()); + verify(mSplitController, never()).newContainer(any(), any(), any(), anyInt(), any()); + verify(mSplitController, never()).registerSplit(any(), any(), any(), any(), any(), any()); } @Test @@ -680,11 +802,12 @@ public class SplitControllerTest { primaryContainer, mActivity, secondaryContainer, - splitRule); + splitRule, + SPLIT_ATTRIBUTES); final Activity launchedActivity = createMockActivity(); primaryContainer.addPendingAppearedActivity(launchedActivity); - assertFalse(mSplitController.resolveActivityToContainer(launchedActivity, + assertTrue(mSplitController.resolveActivityToContainer(mTransaction, launchedActivity, false /* isOnReparent */)); } @@ -696,12 +819,12 @@ public class SplitControllerTest { // Activity is already in secondary split, no need to create new split. addSplitTaskFragments(primaryActivity, mActivity); clearInvocations(mSplitController); - final boolean result = mSplitController.resolveActivityToContainer(mActivity, + final boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity, false /* isOnReparent */); assertTrue(result); - verify(mSplitController, never()).newContainer(any(), any(), any(), anyInt()); - verify(mSplitController, never()).registerSplit(any(), any(), any(), any(), any()); + verify(mSplitController, never()).newContainer(any(), any(), any(), anyInt(), any()); + verify(mSplitController, never()).registerSplit(any(), any(), any(), any(), any(), any()); } @Test @@ -714,7 +837,7 @@ public class SplitControllerTest { addSplitTaskFragments(primaryActivity, secondaryActivity); mSplitController.getContainerWithActivity(secondaryActivity) .addPendingAppearedActivity(mActivity); - final boolean result = mSplitController.resolveActivityToContainer(mActivity, + final boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity, false /* isOnReparent */); assertFalse(result); @@ -738,8 +861,9 @@ public class SplitControllerTest { primaryContainer, mActivity, secondaryContainer, - placeholderRule); - final boolean result = mSplitController.resolveActivityToContainer(mActivity, + placeholderRule, + SPLIT_ATTRIBUTES); + final boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity, false /* isOnReparent */); assertTrue(result); @@ -753,7 +877,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 +893,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 +919,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 +940,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); + assertTrue(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 +966,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 +984,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,27 +1002,48 @@ 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()); + assertTrue(mSplitController.getContainerWithActivity(mActivity) + .areLastRequestedBoundsEqual(new Rect())); + } + + @Test + public void testFindActivityBelow() { + // Create a container with two activities + final TaskFragmentContainer container = createMockTaskFragmentContainer(mActivity); + final Activity pendingAppearedActivity = createMockActivity(); + container.addPendingAppearedActivity(pendingAppearedActivity); + + // Ensure the activity below matches + assertEquals(mActivity, + mSplitController.findActivityBelow(pendingAppearedActivity)); + + // Ensure that the activity look up won't search for the in-process activities and should + // IPC to WM core to get the activity below. It should be `null` for this mock test. + spyOn(container); + doReturn(true).when(container).hasCrossProcessActivities(); + assertNotEquals(mActivity, + mSplitController.findActivityBelow(pendingAppearedActivity)); } @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 */)); } @Test public void testGetPlaceholderOptions() { + // Setup to make sure a transaction record is started. + mTransactionManager.startNewTransaction(); doReturn(true).when(mActivity).isResumed(); assertNull(mSplitController.getPlaceholderOptions(mActivity, false /* isOnCreated */)); @@ -940,28 +1087,404 @@ public class SplitControllerTest { assertTrue(primaryContainer.isFinished()); assertTrue(secondaryContainer0.isFinished()); assertTrue(secondaryContainer1.isFinished()); - verify(mActivity).finish(); - verify(secondaryActivity0).finish(); - verify(secondaryActivity1).finish(); + verify(mTransaction).finishActivity(mActivity.getActivityToken()); + verify(mTransaction).finishActivity(secondaryActivity0.getActivityToken()); + verify(mTransaction).finishActivity(secondaryActivity1.getActivityToken()); assertTrue(taskContainer.mContainers.isEmpty()); assertTrue(taskContainer.mSplitContainers.isEmpty()); } + @Test + public void testOnTransactionReady_taskFragmentAppeared() { + final TaskFragmentTransaction transaction = new TaskFragmentTransaction(); + final TaskFragmentInfo info = mock(TaskFragmentInfo.class); + transaction.addChange(new TaskFragmentTransaction.Change(TYPE_TASK_FRAGMENT_APPEARED) + .setTaskId(TASK_ID) + .setTaskFragmentToken(new Binder()) + .setTaskFragmentInfo(info)); + mSplitController.onTransactionReady(transaction); + + verify(mSplitController).onTaskFragmentAppeared(any(), eq(info)); + verify(mSplitPresenter).onTransactionHandled(eq(transaction.getTransactionToken()), any(), + anyInt(), anyBoolean()); + } + + @Test + public void testOnTransactionReady_taskFragmentInfoChanged() { + final TaskFragmentTransaction transaction = new TaskFragmentTransaction(); + final TaskFragmentInfo info = mock(TaskFragmentInfo.class); + transaction.addChange(new TaskFragmentTransaction.Change(TYPE_TASK_FRAGMENT_INFO_CHANGED) + .setTaskId(TASK_ID) + .setTaskFragmentToken(new Binder()) + .setTaskFragmentInfo(info)); + mSplitController.onTransactionReady(transaction); + + verify(mSplitController).onTaskFragmentInfoChanged(any(), eq(info)); + verify(mSplitPresenter).onTransactionHandled(eq(transaction.getTransactionToken()), any(), + anyInt(), anyBoolean()); + } + + @Test + public void testOnTransactionReady_taskFragmentVanished() { + final TaskFragmentTransaction transaction = new TaskFragmentTransaction(); + final TaskFragmentInfo info = mock(TaskFragmentInfo.class); + transaction.addChange(new TaskFragmentTransaction.Change(TYPE_TASK_FRAGMENT_VANISHED) + .setTaskId(TASK_ID) + .setTaskFragmentToken(new Binder()) + .setTaskFragmentInfo(info)); + mSplitController.onTransactionReady(transaction); + + verify(mSplitController).onTaskFragmentVanished(any(), eq(info)); + verify(mSplitPresenter).onTransactionHandled(eq(transaction.getTransactionToken()), any(), + anyInt(), anyBoolean()); + } + + @Test + public void testOnTransactionReady_taskFragmentParentInfoChanged() { + final TaskFragmentTransaction transaction = new TaskFragmentTransaction(); + final TaskFragmentParentInfo parentInfo = new TaskFragmentParentInfo(Configuration.EMPTY, + DEFAULT_DISPLAY, true); + transaction.addChange(new TaskFragmentTransaction.Change( + TYPE_TASK_FRAGMENT_PARENT_INFO_CHANGED) + .setTaskId(TASK_ID) + .setTaskFragmentParentInfo(parentInfo)); + mSplitController.onTransactionReady(transaction); + + verify(mSplitController).onTaskFragmentParentInfoChanged(any(), eq(TASK_ID), + eq(parentInfo)); + verify(mSplitPresenter).onTransactionHandled(eq(transaction.getTransactionToken()), any(), + anyInt(), anyBoolean()); + } + + @Test + public void testOnTransactionReady_taskFragmentParentError() { + final TaskFragmentTransaction transaction = new TaskFragmentTransaction(); + final IBinder errorToken = new Binder(); + final TaskFragmentInfo info = mock(TaskFragmentInfo.class); + final int opType = OP_TYPE_CREATE_TASK_FRAGMENT; + final Exception exception = new SecurityException("test"); + final Bundle errorBundle = TaskFragmentOrganizer.putErrorInfoInBundle(exception, info, + opType); + transaction.addChange(new TaskFragmentTransaction.Change(TYPE_TASK_FRAGMENT_ERROR) + .setErrorCallbackToken(errorToken) + .setErrorBundle(errorBundle)); + mSplitController.onTransactionReady(transaction); + + verify(mSplitController).onTaskFragmentError(any(), eq(errorToken), eq(info), eq(opType), + eq(exception)); + verify(mSplitPresenter).onTransactionHandled(eq(transaction.getTransactionToken()), any(), + anyInt(), anyBoolean()); + } + + @Test + public void testOnTransactionReady_activityReparentedToTask() { + final TaskFragmentTransaction transaction = new TaskFragmentTransaction(); + final Intent intent = mock(Intent.class); + final IBinder activityToken = new Binder(); + transaction.addChange(new TaskFragmentTransaction.Change(TYPE_ACTIVITY_REPARENTED_TO_TASK) + .setTaskId(TASK_ID) + .setActivityIntent(intent) + .setActivityToken(activityToken)); + mSplitController.onTransactionReady(transaction); + + verify(mSplitController).onActivityReparentedToTask(any(), eq(TASK_ID), eq(intent), + eq(activityToken)); + verify(mSplitPresenter).onTransactionHandled(eq(transaction.getTransactionToken()), any(), + anyInt(), anyBoolean()); + } + + @Test + public void testHasSamePresentation() { + SplitPairRule splitRule1 = createSplitPairRuleBuilder( + activityPair -> true, + activityIntentPair -> true, + windowMetrics -> true) + .setFinishSecondaryWithPrimary(DEFAULT_FINISH_SECONDARY_WITH_PRIMARY) + .setFinishPrimaryWithSecondary(DEFAULT_FINISH_PRIMARY_WITH_SECONDARY) + .setDefaultSplitAttributes(SPLIT_ATTRIBUTES) + .build(); + SplitPairRule splitRule2 = createSplitPairRuleBuilder( + activityPair -> true, + activityIntentPair -> true, + windowMetrics -> true) + .setFinishSecondaryWithPrimary(DEFAULT_FINISH_SECONDARY_WITH_PRIMARY) + .setFinishPrimaryWithSecondary(DEFAULT_FINISH_PRIMARY_WITH_SECONDARY) + .setDefaultSplitAttributes(SPLIT_ATTRIBUTES) + .build(); + + assertTrue("Rules must have same presentation if tags are null and has same properties.", + SplitController.areRulesSamePresentation(splitRule1, splitRule2, + new WindowMetrics(TASK_BOUNDS, WindowInsets.CONSUMED))); + + splitRule2 = createSplitPairRuleBuilder( + activityPair -> true, + activityIntentPair -> true, + windowMetrics -> true) + .setFinishSecondaryWithPrimary(DEFAULT_FINISH_SECONDARY_WITH_PRIMARY) + .setFinishPrimaryWithSecondary(DEFAULT_FINISH_PRIMARY_WITH_SECONDARY) + .setDefaultSplitAttributes(SPLIT_ATTRIBUTES) + .setTag(TEST_TAG) + .build(); + + assertFalse("Rules must have different presentations if tags are not equal regardless" + + "of other properties", + SplitController.areRulesSamePresentation(splitRule1, splitRule2, + new WindowMetrics(TASK_BOUNDS, WindowInsets.CONSUMED))); + } + + @Test + public void testSplitInfoCallback_reportSplit() { + final Activity r0 = createMockActivity(); + final Activity r1 = createMockActivity(); + addSplitTaskFragments(r0, r1); + + mSplitController.updateCallbackIfNecessary(); + assertEquals(1, mSplitInfos.size()); + final SplitInfo splitInfo = mSplitInfos.get(0); + assertEquals(1, splitInfo.getPrimaryActivityStack().getActivities().size()); + assertEquals(1, splitInfo.getSecondaryActivityStack().getActivities().size()); + assertEquals(r0, splitInfo.getPrimaryActivityStack().getActivities().get(0)); + assertEquals(r1, splitInfo.getSecondaryActivityStack().getActivities().get(0)); + } + + @Test + public void testSplitInfoCallback_reportSplitInMultipleTasks() { + final int taskId0 = 1; + final int taskId1 = 2; + final Activity r0 = createMockActivity(taskId0); + final Activity r1 = createMockActivity(taskId0); + final Activity r2 = createMockActivity(taskId1); + final Activity r3 = createMockActivity(taskId1); + addSplitTaskFragments(r0, r1); + addSplitTaskFragments(r2, r3); + + mSplitController.updateCallbackIfNecessary(); + assertEquals(2, mSplitInfos.size()); + } + + @Test + public void testSplitInfoCallback_doNotReportIfInIntermediateState() { + final Activity r0 = createMockActivity(); + final Activity r1 = createMockActivity(); + addSplitTaskFragments(r0, r1); + final TaskFragmentContainer tf0 = mSplitController.getContainerWithActivity(r0); + final TaskFragmentContainer tf1 = mSplitController.getContainerWithActivity(r1); + spyOn(tf0); + spyOn(tf1); + + // Do not report if activity has not appeared in the TaskFragmentContainer in split. + doReturn(true).when(tf0).isInIntermediateState(); + mSplitController.updateCallbackIfNecessary(); + verify(mEmbeddingCallback, never()).accept(any()); + + doReturn(false).when(tf0).isInIntermediateState(); + mSplitController.updateCallbackIfNecessary(); + verify(mEmbeddingCallback).accept(any()); + } + + @Test + public void testLaunchPlaceholderIfNecessary_nonEmbeddedActivity() { + // Launch placeholder for non embedded activity. + setupPlaceholderRule(mActivity); + mTransactionManager.startNewTransaction(); + mSplitController.launchPlaceholderIfNecessary(mTransaction, mActivity, + true /* isOnCreated */); + + verify(mTransaction).startActivityInTaskFragment(any(), any(), eq(PLACEHOLDER_INTENT), + any()); + } + + @Test + public void testLaunchPlaceholderIfNecessary_embeddedInTopTaskFragment() { + // Launch placeholder for activity in top TaskFragment. + setupPlaceholderRule(mActivity); + mTransactionManager.startNewTransaction(); + final TaskFragmentContainer container = mSplitController.newContainer(mActivity, TASK_ID); + mSplitController.launchPlaceholderIfNecessary(mTransaction, mActivity, + true /* isOnCreated */); + + assertTrue(container.hasActivity(mActivity.getActivityToken())); + verify(mTransaction).startActivityInTaskFragment(any(), any(), eq(PLACEHOLDER_INTENT), + any()); + } + + @Test + public void testLaunchPlaceholderIfNecessary_embeddedBelowTaskFragment() { + // Do not launch placeholder for invisible activity below the top TaskFragment. + setupPlaceholderRule(mActivity); + mTransactionManager.startNewTransaction(); + final TaskFragmentContainer bottomTf = mSplitController.newContainer(mActivity, TASK_ID); + final TaskFragmentContainer topTf = mSplitController.newContainer(new Intent(), mActivity, + TASK_ID); + bottomTf.setInfo(mTransaction, createMockTaskFragmentInfo(bottomTf, mActivity, + false /* isVisible */)); + topTf.setInfo(mTransaction, createMockTaskFragmentInfo(topTf, createMockActivity())); + assertFalse(bottomTf.isVisible()); + mSplitController.launchPlaceholderIfNecessary(mTransaction, mActivity, + true /* isOnCreated */); + + verify(mTransaction, never()).startActivityInTaskFragment(any(), any(), any(), any()); + } + + @Test + public void testLaunchPlaceholderIfNecessary_embeddedBelowTransparentTaskFragment() { + // Launch placeholder for visible activity below the top TaskFragment. + setupPlaceholderRule(mActivity); + mTransactionManager.startNewTransaction(); + final TaskFragmentContainer bottomTf = mSplitController.newContainer(mActivity, TASK_ID); + final TaskFragmentContainer topTf = mSplitController.newContainer(new Intent(), mActivity, + TASK_ID); + bottomTf.setInfo(mTransaction, createMockTaskFragmentInfo(bottomTf, mActivity, + true /* isVisible */)); + topTf.setInfo(mTransaction, createMockTaskFragmentInfo(topTf, createMockActivity())); + assertTrue(bottomTf.isVisible()); + mSplitController.launchPlaceholderIfNecessary(mTransaction, mActivity, + true /* isOnCreated */); + + verify(mTransaction).startActivityInTaskFragment(any(), any(), any(), any()); + } + + @Test + public void testFinishActivityStacks_emptySet_earlyReturn() { + mSplitController.finishActivityStacks(Collections.emptySet()); + + verify(mSplitController, never()).updateContainersInTaskIfVisible(any(), anyInt()); + } + + @Test + public void testFinishActivityStacks_invalidStacks_earlyReturn() { + mSplitController.finishActivityStacks(Collections.singleton(new Binder())); + + verify(mSplitController, never()).updateContainersInTaskIfVisible(any(), anyInt()); + } + + @Test + public void testFinishActivityStacks_finishSingleActivityStack() { + TaskFragmentContainer tf = mSplitController.newContainer(mActivity, TASK_ID); + tf.setInfo(mTransaction, createMockTaskFragmentInfo(tf, mActivity)); + + List<TaskFragmentContainer> containers = mSplitController.mTaskContainers.get(TASK_ID) + .mContainers; + + assertEquals(containers.get(0), tf); + + mSplitController.finishActivityStacks(Collections.singleton(tf.getTaskFragmentToken())); + + verify(mSplitPresenter).deleteTaskFragment(any(), eq(tf.getTaskFragmentToken())); + assertTrue(containers.isEmpty()); + } + + @Test + public void testFinishActivityStacks_finishActivityStacksInOrder() { + TaskFragmentContainer bottomTf = mSplitController.newContainer(mActivity, TASK_ID); + TaskFragmentContainer topTf = mSplitController.newContainer(mActivity, TASK_ID); + bottomTf.setInfo(mTransaction, createMockTaskFragmentInfo(bottomTf, mActivity)); + topTf.setInfo(mTransaction, createMockTaskFragmentInfo(topTf, createMockActivity())); + + List<TaskFragmentContainer> containers = mSplitController.mTaskContainers.get(TASK_ID) + .mContainers; + + assertEquals(containers.size(), 2); + + Set<IBinder> activityStackTokens = new ArraySet<>(new IBinder[]{ + topTf.getTaskFragmentToken(), bottomTf.getTaskFragmentToken()}); + + mSplitController.finishActivityStacks(activityStackTokens); + + ArgumentCaptor<IBinder> argumentCaptor = ArgumentCaptor.forClass(IBinder.class); + + verify(mSplitPresenter, times(2)).deleteTaskFragment(any(), argumentCaptor.capture()); + + List<IBinder> fragmentTokens = argumentCaptor.getAllValues(); + assertEquals("The ActivityStack must be deleted from the lowest z-order " + + "regardless of the order in ActivityStack set", + bottomTf.getTaskFragmentToken(), fragmentTokens.get(0)); + assertEquals("The ActivityStack must be deleted from the lowest z-order " + + "regardless of the order in ActivityStack set", + topTf.getTaskFragmentToken(), fragmentTokens.get(1)); + + assertTrue(containers.isEmpty()); + } + + @Test + public void testUpdateSplitAttributes_invalidSplitContainerToken_earlyReturn() { + mSplitController.updateSplitAttributes(new Binder(), SPLIT_ATTRIBUTES); + + verify(mTransactionManager, never()).startNewTransaction(); + } + + @Test + public void testUpdateSplitAttributes_nullParams_throwException() { + assertThrows(NullPointerException.class, + () -> mSplitController.updateSplitAttributes(null, SPLIT_ATTRIBUTES)); + + final SplitContainer splitContainer = mock(SplitContainer.class); + final IBinder token = new Binder(); + doReturn(token).when(splitContainer).getToken(); + doReturn(splitContainer).when(mSplitController).getSplitContainer(eq(token)); + + assertThrows(NullPointerException.class, + () -> mSplitController.updateSplitAttributes(token, null)); + } + + @Test + public void testUpdateSplitAttributes_doNotNeedToUpdateSplitContainer_doNotApplyTransaction() { + final SplitContainer splitContainer = mock(SplitContainer.class); + final IBinder token = new Binder(); + doReturn(token).when(splitContainer).getToken(); + doReturn(splitContainer).when(mSplitController).getSplitContainer(eq(token)); + doReturn(false).when(mSplitController).updateSplitContainerIfNeeded( + eq(splitContainer), any(), eq(SPLIT_ATTRIBUTES)); + TransactionManager.TransactionRecord testRecord = + mock(TransactionManager.TransactionRecord.class); + doReturn(testRecord).when(mTransactionManager).startNewTransaction(); + + mSplitController.updateSplitAttributes(token, SPLIT_ATTRIBUTES); + + verify(splitContainer).updateDefaultSplitAttributes(eq(SPLIT_ATTRIBUTES)); + verify(testRecord).abort(); + } + + @Test + public void testUpdateSplitAttributes_splitContainerUpdated_updateAttrs() { + final SplitContainer splitContainer = mock(SplitContainer.class); + final IBinder token = new Binder(); + doReturn(token).when(splitContainer).getToken(); + doReturn(splitContainer).when(mSplitController).getSplitContainer(eq(token)); + doReturn(true).when(mSplitController).updateSplitContainerIfNeeded( + eq(splitContainer), any(), eq(SPLIT_ATTRIBUTES)); + TransactionManager.TransactionRecord testRecord = + mock(TransactionManager.TransactionRecord.class); + doReturn(testRecord).when(mTransactionManager).startNewTransaction(); + + mSplitController.updateSplitAttributes(token, SPLIT_ATTRIBUTES); + + verify(splitContainer).updateDefaultSplitAttributes(eq(SPLIT_ATTRIBUTES)); + verify(testRecord).apply(eq(false)); + } + /** Creates a mock activity in the organizer process. */ private Activity createMockActivity() { + return createMockActivity(TASK_ID); + } + + /** Creates a mock activity in the organizer process. */ + private Activity createMockActivity(int taskId) { final Activity activity = mock(Activity.class); doReturn(mActivityResources).when(activity).getResources(); final IBinder activityToken = new Binder(); doReturn(activityToken).when(activity).getActivityToken(); doReturn(activity).when(mSplitController).getActivity(activityToken); - doReturn(TASK_ID).when(activity).getTaskId(); + doReturn(taskId).when(activity).getTaskId(); doReturn(new ActivityInfo()).when(activity).getActivityInfo(); + doReturn(DEFAULT_DISPLAY).when(activity).getDisplayId(); return activity; } /** Creates a mock TaskFragment that has been registered and appeared in the organizer. */ private TaskFragmentContainer createMockTaskFragmentContainer(@NonNull Activity activity) { - final TaskFragmentContainer container = mSplitController.newContainer(activity, TASK_ID); + final TaskFragmentContainer container = mSplitController.newContainer(activity, + activity.getTaskId()); setupTaskFragmentInfo(container, activity); return container; } @@ -970,13 +1493,13 @@ 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); } /** Setups a rule to always expand the given intent. */ private void setupExpandRule(@NonNull Intent expandIntent) { - final ActivityRule expandRule = new ActivityRule.Builder(r -> false, expandIntent::equals) + final ActivityRule expandRule = createActivityBuilder(r -> false, expandIntent::equals) .setShouldAlwaysExpand(true) .build(); mSplitController.setEmbeddingRules(Collections.singleton(expandRule)); @@ -984,7 +1507,7 @@ public class SplitControllerTest { /** Setups a rule to always expand the given activity. */ private void setupExpandRule(@NonNull Activity expandActivity) { - final ActivityRule expandRule = new ActivityRule.Builder(expandActivity::equals, i -> false) + final ActivityRule expandRule = createActivityBuilder(expandActivity::equals, i -> false) .setShouldAlwaysExpand(true) .build(); mSplitController.setEmbeddingRules(Collections.singleton(expandRule)); @@ -992,9 +1515,9 @@ public class SplitControllerTest { /** Setups a rule to launch placeholder for the given activity. */ private void setupPlaceholderRule(@NonNull Activity primaryActivity) { - final SplitRule placeholderRule = new SplitPlaceholderRule.Builder(PLACEHOLDER_INTENT, + final SplitRule placeholderRule = createSplitPlaceholderRuleBuilder(PLACEHOLDER_INTENT, primaryActivity::equals, i -> false, w -> true) - .setSplitRatio(SPLIT_RATIO) + .setDefaultSplitAttributes(SPLIT_ATTRIBUTES) .build(); mSplitController.setEmbeddingRules(Collections.singleton(placeholderRule)); } @@ -1047,11 +1570,12 @@ public class SplitControllerTest { primaryContainer, primaryContainer.getTopNonFinishingActivity(), secondaryContainer, - rule); + rule, + SPLIT_ATTRIBUTES); // We need to set those in case we are not respecting clear top. // TODO(b/231845476) we should always respect clearTop. - final int windowingMode = mSplitController.getTaskContainer(TASK_ID) + final int windowingMode = mSplitController.getTaskContainer(primaryContainer.getTaskId()) .getWindowingModeForSplitTaskFragment(TASK_BOUNDS); primaryContainer.setLastRequestedWindowingMode(windowingMode); secondaryContainer.setLastRequestedWindowingMode(windowingMode); 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..6981d9d7ebb8 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java @@ -16,29 +16,37 @@ package androidx.window.extensions.embedding; +import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; +import static android.view.Display.DEFAULT_DISPLAY; +import static android.window.TaskFragmentOperation.OP_TYPE_SET_ANIMATION_PARAMS; +import static androidx.window.extensions.embedding.EmbeddingTestUtils.DEFAULT_FINISH_PRIMARY_WITH_SECONDARY; +import static androidx.window.extensions.embedding.EmbeddingTestUtils.DEFAULT_FINISH_SECONDARY_WITH_PRIMARY; +import static androidx.window.extensions.embedding.EmbeddingTestUtils.SPLIT_ATTRIBUTES; import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_BOUNDS; import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_ID; import static androidx.window.extensions.embedding.EmbeddingTestUtils.createActivityInfoWithMinDimensions; import static androidx.window.extensions.embedding.EmbeddingTestUtils.createMockTaskFragmentInfo; +import static androidx.window.extensions.embedding.EmbeddingTestUtils.createSplitPairRuleBuilder; import static androidx.window.extensions.embedding.EmbeddingTestUtils.createSplitRule; +import static androidx.window.extensions.embedding.EmbeddingTestUtils.createWindowLayoutInfo; import static androidx.window.extensions.embedding.EmbeddingTestUtils.getSplitBounds; +import static androidx.window.extensions.embedding.SplitPresenter.EXPAND_CONTAINERS_ATTRIBUTES; import static androidx.window.extensions.embedding.SplitPresenter.POSITION_END; import static androidx.window.extensions.embedding.SplitPresenter.POSITION_FILL; import static androidx.window.extensions.embedding.SplitPresenter.POSITION_START; import static androidx.window.extensions.embedding.SplitPresenter.RESULT_EXPANDED; import static androidx.window.extensions.embedding.SplitPresenter.RESULT_EXPAND_FAILED_NO_TF_INFO; import static androidx.window.extensions.embedding.SplitPresenter.RESULT_NOT_EXPANDED; -import static androidx.window.extensions.embedding.SplitPresenter.getBoundsForPosition; import static androidx.window.extensions.embedding.SplitPresenter.getMinDimensions; -import static androidx.window.extensions.embedding.SplitPresenter.shouldShowSideBySide; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; @@ -54,17 +62,27 @@ import android.content.Intent; import android.content.pm.ActivityInfo; import android.content.res.Configuration; import android.content.res.Resources; +import android.graphics.Color; import android.graphics.Rect; import android.os.IBinder; import android.platform.test.annotations.Presubmit; +import android.util.DisplayMetrics; import android.util.Pair; import android.util.Size; +import android.view.WindowMetrics; +import android.window.TaskFragmentAnimationParams; import android.window.TaskFragmentInfo; +import android.window.TaskFragmentOperation; import android.window.WindowContainerTransaction; +import androidx.annotation.NonNull; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; +import androidx.window.common.DeviceStateManagerFoldingFeatureProducer; +import androidx.window.extensions.core.util.function.Function; +import androidx.window.extensions.layout.WindowLayoutComponentImpl; +import androidx.window.extensions.layout.WindowLayoutInfo; import org.junit.Before; import org.junit.Test; @@ -72,18 +90,21 @@ import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import java.util.ArrayList; + /** * Test class for {@link SplitPresenter}. * * Build/Install/Run: * atest WMJetpackUnitTests:SplitPresenterTest */ +// Suppress GuardedBy warning on unit tests +@SuppressWarnings("GuardedBy") @Presubmit @SmallTest @RunWith(AndroidJUnit4.class) public class SplitPresenterTest { - @Mock private Activity mActivity; @Mock private Resources mActivityResources; @@ -91,13 +112,19 @@ public class SplitPresenterTest { private TaskFragmentInfo mTaskFragmentInfo; @Mock private WindowContainerTransaction mTransaction; + @Mock + private WindowLayoutComponentImpl mWindowLayoutComponent; private SplitController mController; private SplitPresenter mPresenter; @Before public void setUp() { MockitoAnnotations.initMocks(this); - mController = new SplitController(); + doReturn(new WindowLayoutInfo(new ArrayList<>())).when(mWindowLayoutComponent) + .getCurrentWindowLayoutInfo(anyInt(), any()); + DeviceStateManagerFoldingFeatureProducer producer = + mock(DeviceStateManagerFoldingFeatureProducer.class); + mController = new SplitController(mWindowLayoutComponent, producer); mPresenter = mController.mPresenter; spyOn(mController); spyOn(mPresenter); @@ -122,13 +149,13 @@ public class SplitPresenterTest { mPresenter.resizeTaskFragment(mTransaction, container.getTaskFragmentToken(), TASK_BOUNDS); assertTrue(container.areLastRequestedBoundsEqual(TASK_BOUNDS)); - verify(mTransaction).setBounds(any(), eq(TASK_BOUNDS)); + verify(mTransaction).setRelativeBounds(any(), eq(TASK_BOUNDS)); // No request to set the same bounds. clearInvocations(mTransaction); mPresenter.resizeTaskFragment(mTransaction, container.getTaskFragmentToken(), TASK_BOUNDS); - verify(mTransaction, never()).setBounds(any(), any()); + verify(mTransaction, never()).setRelativeBounds(any(), any()); } @Test @@ -147,7 +174,96 @@ public class SplitPresenterTest { WINDOWING_MODE_MULTI_WINDOW); verify(mTransaction, never()).setWindowingMode(any(), anyInt()); + } + + @Test + public void testSetAdjacentTaskFragments() { + final TaskFragmentContainer container0 = mController.newContainer(mActivity, TASK_ID); + final TaskFragmentContainer container1 = mController.newContainer(mActivity, TASK_ID); + + mPresenter.setAdjacentTaskFragments(mTransaction, container0.getTaskFragmentToken(), + container1.getTaskFragmentToken(), null /* adjacentParams */); + verify(mTransaction).setAdjacentTaskFragments(container0.getTaskFragmentToken(), + container1.getTaskFragmentToken(), null /* adjacentParams */); + + // No request to set the same adjacent TaskFragments. + clearInvocations(mTransaction); + mPresenter.setAdjacentTaskFragments(mTransaction, container0.getTaskFragmentToken(), + container1.getTaskFragmentToken(), null /* adjacentParams */); + + verify(mTransaction, never()).setAdjacentTaskFragments(any(), any(), any()); + } + + @Test + public void testClearAdjacentTaskFragments() { + final TaskFragmentContainer container0 = mController.newContainer(mActivity, TASK_ID); + final TaskFragmentContainer container1 = mController.newContainer(mActivity, TASK_ID); + + // No request to clear as it is not set by default. + mPresenter.clearAdjacentTaskFragments(mTransaction, container0.getTaskFragmentToken()); + verify(mTransaction, never()).clearAdjacentTaskFragments(any()); + + mPresenter.setAdjacentTaskFragments(mTransaction, container0.getTaskFragmentToken(), + container1.getTaskFragmentToken(), null /* adjacentParams */); + mPresenter.clearAdjacentTaskFragments(mTransaction, container0.getTaskFragmentToken()); + verify(mTransaction).clearAdjacentTaskFragments(container0.getTaskFragmentToken()); + + // No request to clear on either of the previous cleared TasKFragments. + clearInvocations(mTransaction); + mPresenter.clearAdjacentTaskFragments(mTransaction, container0.getTaskFragmentToken()); + mPresenter.clearAdjacentTaskFragments(mTransaction, container1.getTaskFragmentToken()); + + verify(mTransaction, never()).clearAdjacentTaskFragments(any()); + } + + @Test + public void testSetCompanionTaskFragment() { + final TaskFragmentContainer container0 = mController.newContainer(mActivity, TASK_ID); + final TaskFragmentContainer container1 = mController.newContainer(mActivity, TASK_ID); + + mPresenter.setCompanionTaskFragment(mTransaction, container0.getTaskFragmentToken(), + container1.getTaskFragmentToken()); + verify(mTransaction).setCompanionTaskFragment(container0.getTaskFragmentToken(), + container1.getTaskFragmentToken()); + // No request to set the same adjacent TaskFragments. + clearInvocations(mTransaction); + mPresenter.setCompanionTaskFragment(mTransaction, container0.getTaskFragmentToken(), + container1.getTaskFragmentToken()); + + verify(mTransaction, never()).setCompanionTaskFragment(any(), any()); + } + + @Test + public void testUpdateAnimationParams() { + final TaskFragmentContainer container = mController.newContainer(mActivity, TASK_ID); + + // Verify the default. + assertTrue(container.areLastRequestedAnimationParamsEqual( + TaskFragmentAnimationParams.DEFAULT)); + + final int bgColor = Color.GREEN; + final TaskFragmentAnimationParams animationParams = + new TaskFragmentAnimationParams.Builder() + .setAnimationBackgroundColor(bgColor) + .build(); + mPresenter.updateAnimationParams(mTransaction, container.getTaskFragmentToken(), + animationParams); + + final TaskFragmentOperation expectedOperation = new TaskFragmentOperation.Builder( + OP_TYPE_SET_ANIMATION_PARAMS) + .setAnimationParams(animationParams) + .build(); + verify(mTransaction).addTaskFragmentOperation(container.getTaskFragmentToken(), + expectedOperation); + assertTrue(container.areLastRequestedAnimationParamsEqual(animationParams)); + + // No request to set the same animation params. + clearInvocations(mTransaction); + mPresenter.updateAnimationParams(mTransaction, container.getTaskFragmentToken(), + animationParams); + + verify(mTransaction, never()).addTaskFragmentOperation(any(), any()); } @Test @@ -159,59 +275,361 @@ public class SplitPresenterTest { @Test public void testShouldShowSideBySide() { - Activity secondaryActivity = createMockActivity(); - final SplitRule splitRule = createSplitRule(mActivity, secondaryActivity); + assertTrue(SplitPresenter.shouldShowSplit(SPLIT_ATTRIBUTES)); + + final SplitAttributes expandContainers = new SplitAttributes.Builder() + .setSplitType(new SplitAttributes.SplitType.ExpandContainersSplitType()) + .build(); - assertTrue(shouldShowSideBySide(TASK_BOUNDS, splitRule)); + assertFalse(SplitPresenter.shouldShowSplit(expandContainers)); + } + + @Test + public void testGetRelBoundsForPosition_expandContainers() { + final TaskContainer.TaskProperties taskProperties = getTaskProperties(); + final SplitAttributes splitAttributes = new SplitAttributes.Builder() + .setSplitType(new SplitAttributes.SplitType.ExpandContainersSplitType()) + .build(); - // Set minDimensions of primary container to larger than primary bounds. - final Rect primaryBounds = getSplitBounds(true /* isPrimary */); - Pair<Size, Size> minDimensionsPair = new Pair<>( - new Size(primaryBounds.width() + 1, primaryBounds.height() + 1), null); + assertEquals("Task bounds must be reported.", + new Rect(), + mPresenter.getRelBoundsForPosition(POSITION_START, taskProperties, + splitAttributes)); - assertFalse(shouldShowSideBySide(TASK_BOUNDS, splitRule, minDimensionsPair)); + assertEquals("Task bounds must be reported.", + new Rect(), + mPresenter.getRelBoundsForPosition(POSITION_END, taskProperties, splitAttributes)); + assertEquals("Task bounds must be reported.", + new Rect(), + mPresenter.getRelBoundsForPosition(POSITION_FILL, taskProperties, splitAttributes)); } @Test - public void testGetBoundsForPosition() { - Activity secondaryActivity = createMockActivity(); - final SplitRule splitRule = createSplitRule(mActivity, secondaryActivity); - final Rect primaryBounds = getSplitBounds(true /* isPrimary */); - final Rect secondaryBounds = getSplitBounds(false /* isPrimary */); + public void testGetRelBoundsForPosition_expandContainers_isRelativeToParent() { + final TaskContainer.TaskProperties taskProperties = getTaskProperties( + new Rect(100, 100, 500, 1000)); + final SplitAttributes splitAttributes = new SplitAttributes.Builder() + .setSplitType(new SplitAttributes.SplitType.ExpandContainersSplitType()) + .build(); + + assertEquals("Task bounds must be reported.", + new Rect(), + mPresenter.getRelBoundsForPosition(POSITION_START, taskProperties, + splitAttributes)); + + assertEquals("Task bounds must be reported.", + new Rect(), + mPresenter.getRelBoundsForPosition(POSITION_END, taskProperties, splitAttributes)); + assertEquals("Task bounds must be reported.", + new Rect(), + mPresenter.getRelBoundsForPosition(POSITION_FILL, taskProperties, splitAttributes)); + } + + @Test + public void testGetRelBoundsForPosition_splitVertically() { + final Rect primaryBounds = getSplitBounds(true /* isPrimary */, + false /* splitHorizontally */); + final Rect secondaryBounds = getSplitBounds(false /* isPrimary */, + false /* splitHorizontally */); + final TaskContainer.TaskProperties taskProperties = getTaskProperties(); + SplitAttributes splitAttributes = new SplitAttributes.Builder() + .setSplitType(SplitAttributes.SplitType.RatioSplitType.splitEqually()) + .setLayoutDirection(SplitAttributes.LayoutDirection.LEFT_TO_RIGHT) + .build(); assertEquals("Primary bounds must be reported.", primaryBounds, - getBoundsForPosition(POSITION_START, TASK_BOUNDS, splitRule, - mActivity, null /* miniDimensionsPair */)); + mPresenter.getRelBoundsForPosition(POSITION_START, taskProperties, + splitAttributes)); + + assertEquals("Secondary bounds must be reported.", + secondaryBounds, + mPresenter.getRelBoundsForPosition(POSITION_END, taskProperties, splitAttributes)); + assertEquals("Task bounds must be reported.", + new Rect(), + mPresenter.getRelBoundsForPosition(POSITION_FILL, taskProperties, splitAttributes)); + + splitAttributes = new SplitAttributes.Builder() + .setSplitType(SplitAttributes.SplitType.RatioSplitType.splitEqually()) + .setLayoutDirection(SplitAttributes.LayoutDirection.RIGHT_TO_LEFT) + .build(); assertEquals("Secondary bounds must be reported.", secondaryBounds, - getBoundsForPosition(POSITION_END, TASK_BOUNDS, splitRule, - mActivity, null /* miniDimensionsPair */)); + mPresenter.getRelBoundsForPosition(POSITION_START, taskProperties, + splitAttributes)); + + assertEquals("Primary bounds must be reported.", + primaryBounds, + mPresenter.getRelBoundsForPosition(POSITION_END, taskProperties, splitAttributes)); assertEquals("Task bounds must be reported.", new Rect(), - getBoundsForPosition(POSITION_FILL, TASK_BOUNDS, splitRule, - mActivity, null /* miniDimensionsPair */)); + mPresenter.getRelBoundsForPosition(POSITION_FILL, taskProperties, splitAttributes)); + + splitAttributes = new SplitAttributes.Builder() + .setSplitType(SplitAttributes.SplitType.RatioSplitType.splitEqually()) + .setLayoutDirection(SplitAttributes.LayoutDirection.LOCALE) + .build(); + // Layout direction should follow screen layout for SplitAttributes.LayoutDirection.LOCALE. + taskProperties.getConfiguration().screenLayout |= Configuration.SCREENLAYOUT_LAYOUTDIR_RTL; - Pair<Size, Size> minDimensionsPair = new Pair<>( - new Size(primaryBounds.width() + 1, primaryBounds.height() + 1), null); + assertEquals("Secondary bounds must be reported.", + secondaryBounds, + mPresenter.getRelBoundsForPosition(POSITION_START, taskProperties, + splitAttributes)); - assertEquals("Fullscreen bounds must be reported because of min dimensions.", + assertEquals("Primary bounds must be reported.", + primaryBounds, + mPresenter.getRelBoundsForPosition(POSITION_END, taskProperties, splitAttributes)); + assertEquals("Task bounds must be reported.", new Rect(), - getBoundsForPosition(POSITION_START, TASK_BOUNDS, - splitRule, mActivity, minDimensionsPair)); + mPresenter.getRelBoundsForPosition(POSITION_FILL, taskProperties, splitAttributes)); + } + + @Test + public void testGetRelBoundsForPosition_splitVertically_isRelativeToParent() { + // Calculate based on TASK_BOUNDS. + final Rect primaryBounds = getSplitBounds(true /* isPrimary */, + false /* splitHorizontally */); + final Rect secondaryBounds = getSplitBounds(false /* isPrimary */, + false /* splitHorizontally */); + + // Offset TaskBounds to 100, 100. The returned rel bounds shouldn't be affected. + final Rect taskBounds = new Rect(TASK_BOUNDS); + taskBounds.offset(100, 100); + final TaskContainer.TaskProperties taskProperties = getTaskProperties(taskBounds); + SplitAttributes splitAttributes = new SplitAttributes.Builder() + .setSplitType(SplitAttributes.SplitType.RatioSplitType.splitEqually()) + .setLayoutDirection(SplitAttributes.LayoutDirection.LEFT_TO_RIGHT) + .build(); + + assertEquals("Primary bounds must be reported.", + primaryBounds, + mPresenter.getRelBoundsForPosition(POSITION_START, taskProperties, + splitAttributes)); + + assertEquals("Secondary bounds must be reported.", + secondaryBounds, + mPresenter.getRelBoundsForPosition(POSITION_END, taskProperties, splitAttributes)); + assertEquals("Task bounds must be reported.", + new Rect(), + mPresenter.getRelBoundsForPosition(POSITION_FILL, taskProperties, splitAttributes)); + + splitAttributes = new SplitAttributes.Builder() + .setSplitType(SplitAttributes.SplitType.RatioSplitType.splitEqually()) + .setLayoutDirection(SplitAttributes.LayoutDirection.RIGHT_TO_LEFT) + .build(); + + assertEquals("Secondary bounds must be reported.", + secondaryBounds, + mPresenter.getRelBoundsForPosition(POSITION_START, taskProperties, + splitAttributes)); + + assertEquals("Primary bounds must be reported.", + primaryBounds, + mPresenter.getRelBoundsForPosition(POSITION_END, taskProperties, splitAttributes)); + assertEquals("Task bounds must be reported.", + new Rect(), + mPresenter.getRelBoundsForPosition(POSITION_FILL, taskProperties, splitAttributes)); + + splitAttributes = new SplitAttributes.Builder() + .setSplitType(SplitAttributes.SplitType.RatioSplitType.splitEqually()) + .setLayoutDirection(SplitAttributes.LayoutDirection.LOCALE) + .build(); + // Layout direction should follow screen layout for SplitAttributes.LayoutDirection.LOCALE. + taskProperties.getConfiguration().screenLayout |= Configuration.SCREENLAYOUT_LAYOUTDIR_RTL; + + assertEquals("Secondary bounds must be reported.", + secondaryBounds, + mPresenter.getRelBoundsForPosition(POSITION_START, taskProperties, + splitAttributes)); + + assertEquals("Primary bounds must be reported.", + primaryBounds, + mPresenter.getRelBoundsForPosition(POSITION_END, taskProperties, splitAttributes)); + assertEquals("Task bounds must be reported.", + new Rect(), + mPresenter.getRelBoundsForPosition(POSITION_FILL, taskProperties, splitAttributes)); + } + + @Test + public void testGetRelBoundsForPosition_splitHorizontally() { + final Rect primaryBounds = getSplitBounds(true /* isPrimary */, + true /* splitHorizontally */); + final Rect secondaryBounds = getSplitBounds(false /* isPrimary */, + true /* splitHorizontally */); + final TaskContainer.TaskProperties taskProperties = getTaskProperties(); + SplitAttributes splitAttributes = new SplitAttributes.Builder() + .setSplitType(SplitAttributes.SplitType.RatioSplitType.splitEqually()) + .setLayoutDirection(SplitAttributes.LayoutDirection.TOP_TO_BOTTOM) + .build(); + + assertEquals("Primary bounds must be reported.", + primaryBounds, + mPresenter.getRelBoundsForPosition(POSITION_START, taskProperties, + splitAttributes)); + + assertEquals("Secondary bounds must be reported.", + secondaryBounds, + mPresenter.getRelBoundsForPosition(POSITION_END, taskProperties, splitAttributes)); + assertEquals("Task bounds must be reported.", + new Rect(), + mPresenter.getRelBoundsForPosition(POSITION_FILL, taskProperties, splitAttributes)); + + splitAttributes = new SplitAttributes.Builder() + .setSplitType(SplitAttributes.SplitType.RatioSplitType.splitEqually()) + .setLayoutDirection(SplitAttributes.LayoutDirection.BOTTOM_TO_TOP) + .build(); + + assertEquals("Secondary bounds must be reported.", + secondaryBounds, + mPresenter.getRelBoundsForPosition(POSITION_START, taskProperties, + splitAttributes)); + + assertEquals("Primary bounds must be reported.", + primaryBounds, + mPresenter.getRelBoundsForPosition(POSITION_END, taskProperties, splitAttributes)); + assertEquals("Task bounds must be reported.", + new Rect(), + mPresenter.getRelBoundsForPosition(POSITION_FILL, taskProperties, splitAttributes)); + } + + @Test + public void testGetRelBoundsForPosition_useHingeFallback() { + final Rect primaryBounds = getSplitBounds(true /* isPrimary */, + false /* splitHorizontally */); + final Rect secondaryBounds = getSplitBounds(false /* isPrimary */, + false /* splitHorizontally */); + final TaskContainer.TaskProperties taskProperties = getTaskProperties(); + final SplitAttributes splitAttributes = new SplitAttributes.Builder() + .setSplitType(new SplitAttributes.SplitType.HingeSplitType( + SplitAttributes.SplitType.RatioSplitType.splitEqually() + )).setLayoutDirection(SplitAttributes.LayoutDirection.LEFT_TO_RIGHT) + .build(); + + // There's no hinge on the device. Use fallback SplitType. + doReturn(new WindowLayoutInfo(new ArrayList<>())).when(mWindowLayoutComponent) + .getCurrentWindowLayoutInfo(anyInt(), any()); + + assertEquals("PrimaryBounds must be reported.", + primaryBounds, + mPresenter.getRelBoundsForPosition(POSITION_START, taskProperties, + splitAttributes)); + + assertEquals("SecondaryBounds must be reported.", + secondaryBounds, + mPresenter.getRelBoundsForPosition(POSITION_END, taskProperties, splitAttributes)); + assertEquals("Task bounds must be reported.", + new Rect(), + mPresenter.getRelBoundsForPosition(POSITION_FILL, taskProperties, splitAttributes)); + + // Hinge is reported, but the host task is in multi-window mode. Still use fallback + // splitType. + doReturn(createWindowLayoutInfo()).when(mWindowLayoutComponent) + .getCurrentWindowLayoutInfo(anyInt(), any()); + taskProperties.getConfiguration().windowConfiguration + .setWindowingMode(WINDOWING_MODE_MULTI_WINDOW); + + assertEquals("PrimaryBounds must be reported.", + primaryBounds, + mPresenter.getRelBoundsForPosition(POSITION_START, taskProperties, + splitAttributes)); + + assertEquals("SecondaryBounds must be reported.", + secondaryBounds, + mPresenter.getRelBoundsForPosition(POSITION_END, taskProperties, splitAttributes)); + assertEquals("Task bounds must be reported.", + new Rect(), + mPresenter.getRelBoundsForPosition(POSITION_FILL, taskProperties, splitAttributes)); + + // Hinge is reported, and the host task is in fullscreen, but layout direction doesn't match + // folding area orientation. Still use fallback splitType. + doReturn(createWindowLayoutInfo()).when(mWindowLayoutComponent) + .getCurrentWindowLayoutInfo(anyInt(), any()); + taskProperties.getConfiguration().windowConfiguration + .setWindowingMode(WINDOWING_MODE_FULLSCREEN); + + assertEquals("PrimaryBounds must be reported.", + primaryBounds, + mPresenter.getRelBoundsForPosition(POSITION_START, taskProperties, + splitAttributes)); + + assertEquals("SecondaryBounds must be reported.", + secondaryBounds, + mPresenter.getRelBoundsForPosition(POSITION_END, taskProperties, splitAttributes)); + assertEquals("Task bounds must be reported.", + new Rect(), + mPresenter.getRelBoundsForPosition(POSITION_FILL, taskProperties, splitAttributes)); + } + + @Test + public void testGetRelBoundsForPosition_fallbackToExpandContainers() { + final TaskContainer.TaskProperties taskProperties = getTaskProperties(); + final SplitAttributes splitAttributes = new SplitAttributes.Builder() + .setSplitType(new SplitAttributes.SplitType.HingeSplitType( + new SplitAttributes.SplitType.ExpandContainersSplitType() + )).setLayoutDirection(SplitAttributes.LayoutDirection.LEFT_TO_RIGHT) + .build(); + + assertEquals("Task bounds must be reported.", + new Rect(), + mPresenter.getRelBoundsForPosition(POSITION_START, taskProperties, + splitAttributes)); + + assertEquals("Task bounds must be reported.", + new Rect(), + mPresenter.getRelBoundsForPosition(POSITION_END, taskProperties, splitAttributes)); + assertEquals("Task bounds must be reported.", + new Rect(), + mPresenter.getRelBoundsForPosition(POSITION_FILL, taskProperties, splitAttributes)); + } + + @Test + public void testGetRelBoundsForPosition_useHingeSplitType() { + final TaskContainer.TaskProperties taskProperties = getTaskProperties(); + final SplitAttributes splitAttributes = new SplitAttributes.Builder() + .setSplitType(new SplitAttributes.SplitType.HingeSplitType( + new SplitAttributes.SplitType.ExpandContainersSplitType() + )).setLayoutDirection(SplitAttributes.LayoutDirection.TOP_TO_BOTTOM) + .build(); + final WindowLayoutInfo windowLayoutInfo = createWindowLayoutInfo(); + doReturn(windowLayoutInfo).when(mWindowLayoutComponent) + .getCurrentWindowLayoutInfo(anyInt(), any()); + final Rect hingeBounds = windowLayoutInfo.getDisplayFeatures().get(0).getBounds(); + final Rect primaryBounds = new Rect( + TASK_BOUNDS.left, + TASK_BOUNDS.top, + TASK_BOUNDS.right, + hingeBounds.top + ); + final Rect secondaryBounds = new Rect( + TASK_BOUNDS.left, + hingeBounds.bottom, + TASK_BOUNDS.right, + TASK_BOUNDS.bottom + ); + + assertEquals("PrimaryBounds must be reported.", + primaryBounds, + mPresenter.getRelBoundsForPosition(POSITION_START, taskProperties, + splitAttributes)); + + assertEquals("SecondaryBounds must be reported.", + secondaryBounds, + mPresenter.getRelBoundsForPosition(POSITION_END, taskProperties, splitAttributes)); + assertEquals("Task bounds must be reported.", + new Rect(), + mPresenter.getRelBoundsForPosition(POSITION_FILL, taskProperties, splitAttributes)); } @Test public void testExpandSplitContainerIfNeeded() { - SplitContainer splitContainer = mock(SplitContainer.class); Activity secondaryActivity = createMockActivity(); SplitRule splitRule = createSplitRule(mActivity, secondaryActivity); TaskFragmentContainer primaryTf = mController.newContainer(mActivity, TASK_ID); TaskFragmentContainer secondaryTf = mController.newContainer(secondaryActivity, TASK_ID); - doReturn(splitRule).when(splitContainer).getSplitRule(); - doReturn(primaryTf).when(splitContainer).getPrimaryContainer(); - doReturn(secondaryTf).when(splitContainer).getSecondaryContainer(); + SplitContainer splitContainer = new SplitContainer(primaryTf, secondaryActivity, + secondaryTf, splitRule, SPLIT_ATTRIBUTES); assertThrows(IllegalArgumentException.class, () -> mPresenter.expandSplitContainerIfNeeded(mTransaction, splitContainer, mActivity, @@ -221,31 +639,130 @@ public class SplitPresenterTest { splitContainer, mActivity, secondaryActivity, null /* secondaryIntent */)); verify(mPresenter, never()).expandTaskFragment(any(), any()); + splitContainer.updateCurrentSplitAttributes(SPLIT_ATTRIBUTES); doReturn(createActivityInfoWithMinDimensions()).when(secondaryActivity).getActivityInfo(); assertEquals(RESULT_EXPAND_FAILED_NO_TF_INFO, mPresenter.expandSplitContainerIfNeeded( mTransaction, splitContainer, mActivity, secondaryActivity, null /* secondaryIntent */)); - primaryTf.setInfo(createMockTaskFragmentInfo(primaryTf, mActivity)); - secondaryTf.setInfo(createMockTaskFragmentInfo(secondaryTf, secondaryActivity)); + splitContainer.updateCurrentSplitAttributes(SPLIT_ATTRIBUTES); + primaryTf.setInfo(mTransaction, createMockTaskFragmentInfo(primaryTf, mActivity)); + secondaryTf.setInfo(mTransaction, + createMockTaskFragmentInfo(secondaryTf, secondaryActivity)); assertEquals(RESULT_EXPANDED, mPresenter.expandSplitContainerIfNeeded(mTransaction, splitContainer, mActivity, secondaryActivity, null /* secondaryIntent */)); - verify(mPresenter).expandTaskFragment(eq(mTransaction), - eq(primaryTf.getTaskFragmentToken())); - verify(mPresenter).expandTaskFragment(eq(mTransaction), - eq(secondaryTf.getTaskFragmentToken())); + verify(mPresenter).expandTaskFragment(mTransaction, primaryTf.getTaskFragmentToken()); + verify(mPresenter).expandTaskFragment(mTransaction, secondaryTf.getTaskFragmentToken()); + splitContainer.updateCurrentSplitAttributes(SPLIT_ATTRIBUTES); clearInvocations(mPresenter); assertEquals(RESULT_EXPANDED, mPresenter.expandSplitContainerIfNeeded(mTransaction, splitContainer, mActivity, null /* secondaryActivity */, new Intent(ApplicationProvider.getApplicationContext(), MinimumDimensionActivity.class))); - verify(mPresenter).expandTaskFragment(eq(mTransaction), - eq(primaryTf.getTaskFragmentToken())); - verify(mPresenter).expandTaskFragment(eq(mTransaction), - eq(secondaryTf.getTaskFragmentToken())); + verify(mPresenter).expandTaskFragment(mTransaction, primaryTf.getTaskFragmentToken()); + verify(mPresenter).expandTaskFragment(mTransaction, secondaryTf.getTaskFragmentToken()); + } + + @Test + public void testCreateNewSplitContainer_secondaryAbovePrimary() { + final Activity secondaryActivity = createMockActivity(); + final TaskFragmentContainer bottomTf = mController.newContainer(secondaryActivity, TASK_ID); + final TaskFragmentContainer primaryTf = mController.newContainer(mActivity, TASK_ID); + final SplitPairRule rule = createSplitPairRuleBuilder(pair -> + pair.first == mActivity && pair.second == secondaryActivity, pair -> false, + metrics -> true) + .setDefaultSplitAttributes(SPLIT_ATTRIBUTES) + .setShouldClearTop(false) + .build(); + + mPresenter.createNewSplitContainer(mTransaction, mActivity, secondaryActivity, rule, + SPLIT_ATTRIBUTES); + + assertEquals(primaryTf, mController.getContainerWithActivity(mActivity)); + final TaskFragmentContainer secondaryTf = mController.getContainerWithActivity( + secondaryActivity); + assertNotEquals(bottomTf, secondaryTf); + assertTrue(secondaryTf.isAbove(primaryTf)); + } + + @Test + public void testComputeSplitAttributes() { + final SplitPairRule splitPairRule = createSplitPairRuleBuilder( + activityPair -> true, + activityIntentPair -> true, + windowMetrics -> windowMetrics.getBounds().equals(TASK_BOUNDS)) + .setFinishSecondaryWithPrimary(DEFAULT_FINISH_SECONDARY_WITH_PRIMARY) + .setFinishPrimaryWithSecondary(DEFAULT_FINISH_PRIMARY_WITH_SECONDARY) + .setDefaultSplitAttributes(SPLIT_ATTRIBUTES) + .build(); + final TaskContainer.TaskProperties taskProperties = getTaskProperties(); + + assertEquals(SPLIT_ATTRIBUTES, mPresenter.computeSplitAttributes(taskProperties, + splitPairRule, SPLIT_ATTRIBUTES, null /* minDimensionsPair */)); + + final Pair<Size, Size> minDimensionsPair = new Pair<>( + new Size(TASK_BOUNDS.width(), TASK_BOUNDS.height()), null); + + assertEquals(EXPAND_CONTAINERS_ATTRIBUTES, mPresenter.computeSplitAttributes(taskProperties, + splitPairRule, SPLIT_ATTRIBUTES, minDimensionsPair)); + + taskProperties.getConfiguration().windowConfiguration.setBounds(new Rect( + TASK_BOUNDS.left + 1, TASK_BOUNDS.top + 1, TASK_BOUNDS.right + 1, + TASK_BOUNDS.bottom + 1)); + + assertEquals(EXPAND_CONTAINERS_ATTRIBUTES, mPresenter.computeSplitAttributes(taskProperties, + splitPairRule, SPLIT_ATTRIBUTES, null /* minDimensionsPair */)); + + final SplitAttributes splitAttributes = new SplitAttributes.Builder() + .setSplitType(new SplitAttributes.SplitType.HingeSplitType( + SplitAttributes.SplitType.RatioSplitType.splitEqually())) + .build(); + final Function<SplitAttributesCalculatorParams, SplitAttributes> calculator = + params -> splitAttributes; + + mController.setSplitAttributesCalculator(calculator); + + assertEquals(splitAttributes, mPresenter.computeSplitAttributes(taskProperties, + splitPairRule, SPLIT_ATTRIBUTES, null /* minDimensionsPair */)); + } + + @Test + public void testComputeSplitAttributesOnHingeSplitTypeOnDeviceWithoutFoldingFeature() { + final SplitAttributes hingeSplitAttrs = new SplitAttributes.Builder() + .setSplitType(new SplitAttributes.SplitType.HingeSplitType( + SplitAttributes.SplitType.RatioSplitType.splitEqually())) + .build(); + final SplitPairRule splitPairRule = createSplitPairRuleBuilder( + activityPair -> true, + activityIntentPair -> true, + windowMetrics -> windowMetrics.getBounds().equals(TASK_BOUNDS)) + .setFinishSecondaryWithPrimary(DEFAULT_FINISH_SECONDARY_WITH_PRIMARY) + .setFinishPrimaryWithSecondary(DEFAULT_FINISH_PRIMARY_WITH_SECONDARY) + .setDefaultSplitAttributes(hingeSplitAttrs) + .build(); + final TaskContainer.TaskProperties taskProperties = getTaskProperties(); + doReturn(null).when(mPresenter).getFoldingFeature(any()); + + assertEquals(hingeSplitAttrs, mPresenter.computeSplitAttributes(taskProperties, + splitPairRule, hingeSplitAttrs, null /* minDimensionsPair */)); + } + + @Test + public void testGetTaskWindowMetrics() { + final Configuration taskConfig = new Configuration(); + taskConfig.windowConfiguration.setBounds(TASK_BOUNDS); + taskConfig.densityDpi = 123; + final TaskContainer.TaskProperties taskProperties = new TaskContainer.TaskProperties( + DEFAULT_DISPLAY, taskConfig); + doReturn(taskProperties).when(mPresenter).getTaskProperties(mActivity); + + final WindowMetrics windowMetrics = mPresenter.getTaskWindowMetrics(mActivity); + assertEquals(TASK_BOUNDS, windowMetrics.getBounds()); + assertEquals(123 * DisplayMetrics.DENSITY_DEFAULT_SCALE, + windowMetrics.getDensity(), 0f); } private Activity createMockActivity() { @@ -257,6 +774,17 @@ public class SplitPresenterTest { doReturn(activityConfig).when(mActivityResources).getConfiguration(); doReturn(new ActivityInfo()).when(activity).getActivityInfo(); doReturn(mock(IBinder.class)).when(activity).getActivityToken(); + doReturn(TASK_ID).when(activity).getTaskId(); return activity; } + + private static TaskContainer.TaskProperties getTaskProperties() { + return getTaskProperties(TASK_BOUNDS); + } + + private static TaskContainer.TaskProperties getTaskProperties(@NonNull Rect taskBounds) { + final Configuration configuration = new Configuration(); + configuration.windowConfiguration.setBounds(taskBounds); + return new TaskContainer.TaskProperties(DEFAULT_DISPLAY, configuration); + } } diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java index dd67e48ef353..13e709271221 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java @@ -21,9 +21,9 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; +import static android.view.Display.DEFAULT_DISPLAY; -import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_BOUNDS; -import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_ID; +import static androidx.window.extensions.embedding.EmbeddingTestUtils.createTestTaskContainer; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -34,8 +34,10 @@ import static org.mockito.Mockito.mock; import android.app.Activity; import android.content.Intent; +import android.content.res.Configuration; import android.graphics.Rect; import android.platform.test.annotations.Presubmit; +import android.window.TaskFragmentParentInfo; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; @@ -65,52 +67,24 @@ public class TaskContainerTest { } @Test - public void testIsTaskBoundsInitialized() { - final TaskContainer taskContainer = new TaskContainer(TASK_ID); - - assertFalse(taskContainer.isTaskBoundsInitialized()); - - taskContainer.setTaskBounds(TASK_BOUNDS); - - assertTrue(taskContainer.isTaskBoundsInitialized()); - } - - @Test - public void testSetTaskBounds() { - final TaskContainer taskContainer = new TaskContainer(TASK_ID); - - assertFalse(taskContainer.setTaskBounds(new Rect())); - - assertTrue(taskContainer.setTaskBounds(TASK_BOUNDS)); - - assertFalse(taskContainer.setTaskBounds(TASK_BOUNDS)); - } - - @Test - public void testIsWindowingModeInitialized() { - final TaskContainer taskContainer = new TaskContainer(TASK_ID); - - assertFalse(taskContainer.isWindowingModeInitialized()); - - taskContainer.setWindowingMode(WINDOWING_MODE_FULLSCREEN); - - assertTrue(taskContainer.isWindowingModeInitialized()); - } - - @Test public void testGetWindowingModeForSplitTaskFragment() { - final TaskContainer taskContainer = new TaskContainer(TASK_ID); + final TaskContainer taskContainer = createTestTaskContainer(); final Rect splitBounds = new Rect(0, 0, 500, 1000); + final Configuration configuration = new Configuration(); assertEquals(WINDOWING_MODE_MULTI_WINDOW, taskContainer.getWindowingModeForSplitTaskFragment(splitBounds)); - taskContainer.setWindowingMode(WINDOWING_MODE_FULLSCREEN); + configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN); + taskContainer.updateTaskFragmentParentInfo(new TaskFragmentParentInfo(configuration, + DEFAULT_DISPLAY, true /* visible */)); assertEquals(WINDOWING_MODE_MULTI_WINDOW, taskContainer.getWindowingModeForSplitTaskFragment(splitBounds)); - taskContainer.setWindowingMode(WINDOWING_MODE_FREEFORM); + configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM); + taskContainer.updateTaskFragmentParentInfo(new TaskFragmentParentInfo(configuration, + DEFAULT_DISPLAY, true /* visible */)); assertEquals(WINDOWING_MODE_FREEFORM, taskContainer.getWindowingModeForSplitTaskFragment(splitBounds)); @@ -123,27 +97,32 @@ public class TaskContainerTest { @Test public void testIsInPictureInPicture() { - final TaskContainer taskContainer = new TaskContainer(TASK_ID); + final TaskContainer taskContainer = createTestTaskContainer(); + final Configuration configuration = new Configuration(); assertFalse(taskContainer.isInPictureInPicture()); - taskContainer.setWindowingMode(WINDOWING_MODE_FULLSCREEN); + configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN); + taskContainer.updateTaskFragmentParentInfo(new TaskFragmentParentInfo(configuration, + DEFAULT_DISPLAY, true /* visible */)); assertFalse(taskContainer.isInPictureInPicture()); - taskContainer.setWindowingMode(WINDOWING_MODE_PINNED); + configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_PINNED); + taskContainer.updateTaskFragmentParentInfo(new TaskFragmentParentInfo(configuration, + DEFAULT_DISPLAY, true /* visible */)); assertTrue(taskContainer.isInPictureInPicture()); } @Test public void testIsEmpty() { - final TaskContainer taskContainer = new TaskContainer(TASK_ID); + final TaskContainer taskContainer = createTestTaskContainer(); assertTrue(taskContainer.isEmpty()); final TaskFragmentContainer tf = new TaskFragmentContainer(null /* activity */, - new Intent(), taskContainer, mController); + new Intent(), taskContainer, mController, null /* pairedPrimaryContainer */); assertFalse(taskContainer.isEmpty()); @@ -155,21 +134,21 @@ public class TaskContainerTest { @Test public void testGetTopTaskFragmentContainer() { - final TaskContainer taskContainer = new TaskContainer(TASK_ID); + final TaskContainer taskContainer = createTestTaskContainer(); assertNull(taskContainer.getTopTaskFragmentContainer()); final TaskFragmentContainer tf0 = new TaskFragmentContainer(null /* activity */, - new Intent(), taskContainer, mController); + new Intent(), taskContainer, mController, null /* pairedPrimaryContainer */); assertEquals(tf0, taskContainer.getTopTaskFragmentContainer()); final TaskFragmentContainer tf1 = new TaskFragmentContainer(null /* activity */, - new Intent(), taskContainer, mController); + new Intent(), taskContainer, mController, null /* pairedPrimaryContainer */); assertEquals(tf1, taskContainer.getTopTaskFragmentContainer()); } @Test public void testGetTopNonFinishingActivity() { - final TaskContainer taskContainer = new TaskContainer(TASK_ID); + final TaskContainer taskContainer = createTestTaskContainer(); assertNull(taskContainer.getTopNonFinishingActivity()); final TaskFragmentContainer tf0 = mock(TaskFragmentContainer.class); diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentAnimationControllerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentAnimationControllerTest.java index d31342bfb309..379ea0c534ba 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentAnimationControllerTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentAnimationControllerTest.java @@ -16,11 +16,8 @@ package androidx.window.extensions.embedding; -import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_ID; - import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; -import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.never; import android.platform.test.annotations.Presubmit; @@ -57,41 +54,31 @@ public class TaskFragmentAnimationControllerTest { @Test public void testRegisterRemoteAnimations() { - mAnimationController.registerRemoteAnimations(TASK_ID); + mAnimationController.registerRemoteAnimations(); - verify(mOrganizer).registerRemoteAnimations(TASK_ID, mAnimationController.mDefinition); + verify(mOrganizer).registerRemoteAnimations(mAnimationController.mDefinition); - mAnimationController.registerRemoteAnimations(TASK_ID); + mAnimationController.registerRemoteAnimations(); // No extra call if it has been registered. - verify(mOrganizer).registerRemoteAnimations(TASK_ID, mAnimationController.mDefinition); + verify(mOrganizer).registerRemoteAnimations(mAnimationController.mDefinition); } @Test public void testUnregisterRemoteAnimations() { - mAnimationController.unregisterRemoteAnimations(TASK_ID); + mAnimationController.unregisterRemoteAnimations(); // No call if it is not registered. - verify(mOrganizer, never()).unregisterRemoteAnimations(anyInt()); + verify(mOrganizer, never()).unregisterRemoteAnimations(); - mAnimationController.registerRemoteAnimations(TASK_ID); - mAnimationController.unregisterRemoteAnimations(TASK_ID); + mAnimationController.registerRemoteAnimations(); + mAnimationController.unregisterRemoteAnimations(); - verify(mOrganizer).unregisterRemoteAnimations(TASK_ID); + verify(mOrganizer).unregisterRemoteAnimations(); - mAnimationController.unregisterRemoteAnimations(TASK_ID); + mAnimationController.unregisterRemoteAnimations(); // No extra call if it has been unregistered. - verify(mOrganizer).unregisterRemoteAnimations(TASK_ID); - } - - @Test - public void testUnregisterAllRemoteAnimations() { - mAnimationController.registerRemoteAnimations(TASK_ID); - mAnimationController.registerRemoteAnimations(TASK_ID + 1); - mAnimationController.unregisterAllRemoteAnimations(); - - verify(mOrganizer).unregisterRemoteAnimations(TASK_ID); - verify(mOrganizer).unregisterRemoteAnimations(TASK_ID + 1); + verify(mOrganizer).unregisterRemoteAnimations(); } } 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..78b85e642c13 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java @@ -16,9 +16,11 @@ package androidx.window.extensions.embedding; -import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_ID; import static androidx.window.extensions.embedding.EmbeddingTestUtils.createMockTaskFragmentInfo; +import static androidx.window.extensions.embedding.EmbeddingTestUtils.createTestTaskContainer; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; import static org.junit.Assert.assertEquals; @@ -36,7 +38,6 @@ import static org.mockito.Mockito.never; import android.app.Activity; import android.content.Intent; import android.os.Binder; -import android.os.Handler; import android.os.IBinder; import android.platform.test.annotations.Presubmit; import android.window.TaskFragmentInfo; @@ -44,6 +45,8 @@ import android.window.WindowContainerTransaction; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; +import androidx.window.common.DeviceStateManagerFoldingFeatureProducer; +import androidx.window.extensions.layout.WindowLayoutComponentImpl; import com.google.android.collect.Lists; @@ -62,178 +65,227 @@ 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(); + DeviceStateManagerFoldingFeatureProducer producer = + mock(DeviceStateManagerFoldingFeatureProducer.class); + WindowLayoutComponentImpl component = mock(WindowLayoutComponentImpl.class); + mController = new SplitController(component, producer); + spyOn(mController); mActivity = createMockActivity(); mIntent = new Intent(); } @Test public void testNewContainer() { - final TaskContainer taskContainer = new TaskContainer(TASK_ID); + final TaskContainer taskContainer = createTestTaskContainer(); // One of the activity and the intent must be non-null assertThrows(IllegalArgumentException.class, - () -> new TaskFragmentContainer(null, null, taskContainer, mController)); + () -> new TaskFragmentContainer(null, null, taskContainer, mController, + null /* pairedPrimaryContainer */)); // One of the activity and the intent must be null. assertThrows(IllegalArgumentException.class, - () -> new TaskFragmentContainer(mActivity, mIntent, taskContainer, mController)); + () -> new TaskFragmentContainer(mActivity, mIntent, taskContainer, mController, + null /* pairedPrimaryContainer */)); } @Test public void testFinish() { - final TaskContainer taskContainer = new TaskContainer(TASK_ID); + final TaskContainer taskContainer = createTestTaskContainer(); final TaskFragmentContainer container = new TaskFragmentContainer(mActivity, - null /* pendingAppearedIntent */, taskContainer, mController); + null /* pendingAppearedIntent */, taskContainer, mController, + null /* pairedPrimaryContainer */); doReturn(container).when(mController).getContainerWithActivity(mActivity); - final WindowContainerTransaction wct = new WindowContainerTransaction(); // Only remove the activity, but not clear the reference until appeared. - container.finish(true /* shouldFinishDependent */, mPresenter, wct, mController); + container.finish(true /* shouldFinishDependent */, mPresenter, mTransaction, mController); - verify(mActivity).finish(); + verify(mTransaction).finishActivity(mActivity.getActivityToken()); verify(mPresenter, never()).deleteTaskFragment(any(), any()); verify(mController, never()).removeContainer(any()); // Calling twice should not finish activity again. - clearInvocations(mActivity); - container.finish(true /* shouldFinishDependent */, mPresenter, wct, mController); + clearInvocations(mTransaction); + container.finish(true /* shouldFinishDependent */, mPresenter, mTransaction, mController); - verify(mActivity, never()).finish(); + verify(mTransaction, never()).finishActivity(any()); verify(mPresenter, never()).deleteTaskFragment(any(), any()); verify(mController, never()).removeContainer(any()); // Remove all references after the container has appeared in server. doReturn(new ArrayList<>()).when(mInfo).getActivities(); - container.setInfo(mInfo); - container.finish(true /* shouldFinishDependent */, mPresenter, wct, mController); + container.setInfo(mTransaction, mInfo); + container.finish(true /* shouldFinishDependent */, mPresenter, mTransaction, mController); - verify(mActivity, never()).finish(); - verify(mPresenter).deleteTaskFragment(wct, container.getTaskFragmentToken()); + verify(mTransaction, never()).finishActivity(any()); + verify(mPresenter).deleteTaskFragment(mTransaction, container.getTaskFragmentToken()); verify(mController).removeContainer(container); } @Test public void testFinish_notFinishActivityThatIsReparenting() { - final TaskContainer taskContainer = new TaskContainer(TASK_ID); + final TaskContainer taskContainer = createTestTaskContainer(); final TaskFragmentContainer container0 = new TaskFragmentContainer(mActivity, - null /* pendingAppearedIntent */, taskContainer, mController); + null /* pendingAppearedIntent */, taskContainer, mController, + null /* pairedPrimaryContainer */); 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); + null /* pendingAppearedIntent */, taskContainer, mController, + null /* pairedPrimaryContainer */); doReturn(container1).when(mController).getContainerWithActivity(mActivity); - final WindowContainerTransaction wct = new WindowContainerTransaction(); // The activity is requested to be reparented, so don't finish it. - container0.finish(true /* shouldFinishDependent */, mPresenter, wct, mController); + container0.finish(true /* shouldFinishDependent */, mPresenter, mTransaction, mController); + + verify(mTransaction, never()).finishActivity(any()); + verify(mPresenter).deleteTaskFragment(mTransaction, container0.getTaskFragmentToken()); + verify(mController).removeContainer(container0); + } - verify(mActivity, never()).finish(); - verify(mPresenter).deleteTaskFragment(wct, container0.getTaskFragmentToken()); + @Test + public void testFinish_alwaysFinishPlaceholder() { + // Register container1 as a placeholder + final TaskContainer taskContainer = createTestTaskContainer(); + final TaskFragmentContainer container0 = new TaskFragmentContainer(mActivity, + null /* pendingAppearedIntent */, taskContainer, mController, + null /* pairedPrimaryContainer */); + final TaskFragmentInfo info0 = createMockTaskFragmentInfo(container0, mActivity); + container0.setInfo(mTransaction, info0); + final Activity placeholderActivity = createMockActivity(); + final TaskFragmentContainer container1 = new TaskFragmentContainer(placeholderActivity, + null /* pendingAppearedIntent */, taskContainer, mController, + null /* pairedPrimaryContainer */); + final TaskFragmentInfo info1 = createMockTaskFragmentInfo(container1, placeholderActivity); + container1.setInfo(mTransaction, info1); + final SplitAttributes splitAttributes = new SplitAttributes.Builder().build(); + final SplitPlaceholderRule rule = new SplitPlaceholderRule.Builder(new Intent(), + mActivity::equals, (java.util.function.Predicate) i -> false, + (java.util.function.Predicate) w -> true) + .setDefaultSplitAttributes(splitAttributes) + .build(); + mController.registerSplit(mTransaction, container0, mActivity, container1, rule, + splitAttributes); + + // The placeholder TaskFragment should be finished even if the primary is finished with + // shouldFinishDependent = false. + container0.finish(false /* shouldFinishDependent */, mPresenter, mTransaction, mController); + + assertTrue(container0.isFinished()); + assertTrue(container1.isFinished()); + verify(mPresenter).deleteTaskFragment(mTransaction, container0.getTaskFragmentToken()); + verify(mPresenter).deleteTaskFragment(mTransaction, container1.getTaskFragmentToken()); verify(mController).removeContainer(container0); + verify(mController).removeContainer(container1); } @Test public void testSetInfo() { - final TaskContainer taskContainer = new TaskContainer(TASK_ID); + final TaskContainer taskContainer = createTestTaskContainer(); // Pending activity should be cleared when it has appeared on server side. final TaskFragmentContainer pendingActivityContainer = new TaskFragmentContainer(mActivity, - null /* pendingAppearedIntent */, taskContainer, mController); + null /* pendingAppearedIntent */, taskContainer, mController, + null /* pairedPrimaryContainer */); - assertTrue(pendingActivityContainer.mPendingAppearedActivities.contains(mActivity)); + assertTrue(pendingActivityContainer.mPendingAppearedActivities.contains( + mActivity.getActivityToken())); final TaskFragmentInfo info0 = createMockTaskFragmentInfo(pendingActivityContainer, mActivity); - pendingActivityContainer.setInfo(info0); + pendingActivityContainer.setInfo(mTransaction, info0); assertTrue(pendingActivityContainer.mPendingAppearedActivities.isEmpty()); // Pending intent should be cleared when the container becomes non-empty. final TaskFragmentContainer pendingIntentContainer = new TaskFragmentContainer( - null /* pendingAppearedActivity */, mIntent, taskContainer, mController); + null /* pendingAppearedActivity */, mIntent, taskContainer, mController, + null /* pairedPrimaryContainer */); assertEquals(mIntent, pendingIntentContainer.getPendingAppearedIntent()); final TaskFragmentInfo info1 = createMockTaskFragmentInfo(pendingIntentContainer, mActivity); - pendingIntentContainer.setInfo(info1); + pendingIntentContainer.setInfo(mTransaction, info1); assertNull(pendingIntentContainer.getPendingAppearedIntent()); } @Test public void testIsWaitingActivityAppear() { - final TaskContainer taskContainer = new TaskContainer(TASK_ID); + final TaskContainer taskContainer = createTestTaskContainer(); final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */, - mIntent, taskContainer, mController); + mIntent, taskContainer, mController, null /* pairedPrimaryContainer */); assertTrue(container.isWaitingActivityAppear()); final TaskFragmentInfo info = mock(TaskFragmentInfo.class); doReturn(new ArrayList<>()).when(info).getActivities(); doReturn(true).when(info).isEmpty(); - container.setInfo(info); + container.setInfo(mTransaction, info); assertTrue(container.isWaitingActivityAppear()); doReturn(false).when(info).isEmpty(); - container.setInfo(info); + container.setInfo(mTransaction, info); assertFalse(container.isWaitingActivityAppear()); } @Test public void testAppearEmptyTimeout() { - final TaskContainer taskContainer = new TaskContainer(TASK_ID); + doNothing().when(mController).onTaskFragmentAppearEmptyTimeout(any(), any()); + final TaskContainer taskContainer = createTestTaskContainer(); final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */, - mIntent, taskContainer, mController); - - assertNull(container.mAppearEmptyTimeout); - - // Not set if it is not appeared empty. - final TaskFragmentInfo info = mock(TaskFragmentInfo.class); - doReturn(new ArrayList<>()).when(info).getActivities(); - doReturn(false).when(info).isEmpty(); - container.setInfo(info); + mIntent, taskContainer, mController, null /* pairedPrimaryContainer */); assertNull(container.mAppearEmptyTimeout); // Set timeout if the first info set is empty. + final TaskFragmentInfo info = mock(TaskFragmentInfo.class); container.mInfo = null; doReturn(true).when(info).isEmpty(); - container.setInfo(info); + container.setInfo(mTransaction, info); assertNotNull(container.mAppearEmptyTimeout); + // Not set if it is not appeared empty. + doReturn(new ArrayList<>()).when(info).getActivities(); + doReturn(false).when(info).isEmpty(); + container.setInfo(mTransaction, info); + + assertNull(container.mAppearEmptyTimeout); + // Remove timeout after the container becomes non-empty. doReturn(false).when(info).isEmpty(); - container.setInfo(info); + container.setInfo(mTransaction, info); assertNull(container.mAppearEmptyTimeout); // Running the timeout will call into SplitController.onTaskFragmentAppearEmptyTimeout. container.mInfo = null; + container.setPendingAppearedIntent(mIntent); doReturn(true).when(info).isEmpty(); - container.setInfo(info); + container.setInfo(mTransaction, info); container.mAppearEmptyTimeout.run(); assertNull(container.mAppearEmptyTimeout); @@ -242,9 +294,9 @@ public class TaskFragmentContainerTest { @Test public void testCollectNonFinishingActivities() { - final TaskContainer taskContainer = new TaskContainer(TASK_ID); + final TaskContainer taskContainer = createTestTaskContainer(); final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */, - mIntent, taskContainer, mController); + mIntent, taskContainer, mController, null /* pairedPrimaryContainer */); List<Activity> activities = container.collectNonFinishingActivities(); assertTrue(activities.isEmpty()); @@ -259,7 +311,7 @@ public class TaskFragmentContainerTest { final List<IBinder> runningActivities = Lists.newArrayList(activity0.getActivityToken(), activity1.getActivityToken()); doReturn(runningActivities).when(mInfo).getActivities(); - container.setInfo(mInfo); + container.setInfo(mTransaction, mInfo); activities = container.collectNonFinishingActivities(); assertEquals(3, activities.size()); @@ -270,9 +322,9 @@ public class TaskFragmentContainerTest { @Test public void testAddPendingActivity() { - final TaskContainer taskContainer = new TaskContainer(TASK_ID); + final TaskContainer taskContainer = createTestTaskContainer(); final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */, - mIntent, taskContainer, mController); + mIntent, taskContainer, mController, null /* pairedPrimaryContainer */); container.addPendingAppearedActivity(mActivity); assertEquals(1, container.collectNonFinishingActivities().size()); @@ -283,10 +335,22 @@ public class TaskFragmentContainerTest { } @Test + public void testIsAbove() { + final TaskContainer taskContainer = createTestTaskContainer(); + final TaskFragmentContainer container0 = new TaskFragmentContainer(null /* activity */, + mIntent, taskContainer, mController, null /* pairedPrimaryContainer */); + final TaskFragmentContainer container1 = new TaskFragmentContainer(null /* activity */, + mIntent, taskContainer, mController, null /* pairedPrimaryContainer */); + + assertTrue(container1.isAbove(container0)); + assertFalse(container0.isAbove(container1)); + } + + @Test public void testGetBottomMostActivity() { - final TaskContainer taskContainer = new TaskContainer(TASK_ID); + final TaskContainer taskContainer = createTestTaskContainer(); final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */, - mIntent, taskContainer, mController); + mIntent, taskContainer, mController, null /* pairedPrimaryContainer */); container.addPendingAppearedActivity(mActivity); assertEquals(mActivity, container.getBottomMostActivity()); @@ -294,11 +358,229 @@ public class TaskFragmentContainerTest { final Activity activity = createMockActivity(); final List<IBinder> runningActivities = Lists.newArrayList(activity.getActivityToken()); doReturn(runningActivities).when(mInfo).getActivities(); - container.setInfo(mInfo); + container.setInfo(mTransaction, mInfo); assertEquals(activity, container.getBottomMostActivity()); } + @Test + public void testOnActivityDestroyed() { + final TaskContainer taskContainer = createTestTaskContainer(mController); + final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */, + mIntent, taskContainer, mController, null /* pairedPrimaryContainer */); + 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.getActivityToken()); + + // It should not contain the destroyed Activity. + assertFalse(container.hasActivity(mActivity.getActivityToken())); + } + + @Test + public void testIsInIntermediateState() { + // True if no info set. + final TaskContainer taskContainer = createTestTaskContainer(); + final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */, + mIntent, taskContainer, mController, null /* pairedPrimaryContainer */); + spyOn(taskContainer); + doReturn(true).when(taskContainer).isVisible(); + + assertTrue(container.isInIntermediateState()); + assertTrue(taskContainer.isInIntermediateState()); + + // True if empty info set. + final List<IBinder> activities = new ArrayList<>(); + doReturn(activities).when(mInfo).getActivities(); + doReturn(true).when(mInfo).isEmpty(); + container.setInfo(mTransaction, mInfo); + + assertTrue(container.isInIntermediateState()); + assertTrue(taskContainer.isInIntermediateState()); + + // False if info is not empty. + doReturn(false).when(mInfo).isEmpty(); + container.setInfo(mTransaction, mInfo); + + assertFalse(container.isInIntermediateState()); + assertFalse(taskContainer.isInIntermediateState()); + + // True if there is pending appeared activity. + container.addPendingAppearedActivity(mActivity); + + assertTrue(container.isInIntermediateState()); + assertTrue(taskContainer.isInIntermediateState()); + + // True if the activity is finishing. + activities.add(mActivity.getActivityToken()); + doReturn(true).when(mActivity).isFinishing(); + container.setInfo(mTransaction, mInfo); + + assertTrue(container.isInIntermediateState()); + assertTrue(taskContainer.isInIntermediateState()); + + // False if the activity is not finishing. + doReturn(false).when(mActivity).isFinishing(); + container.setInfo(mTransaction, mInfo); + + assertFalse(container.isInIntermediateState()); + assertFalse(taskContainer.isInIntermediateState()); + + // True if there is a token that can't find associated activity. + activities.clear(); + activities.add(new Binder()); + container.setInfo(mTransaction, mInfo); + + assertTrue(container.isInIntermediateState()); + assertTrue(taskContainer.isInIntermediateState()); + + // False if there is a token that can't find associated activity when the Task is invisible. + doReturn(false).when(taskContainer).isVisible(); + + assertFalse(container.isInIntermediateState()); + assertFalse(taskContainer.isInIntermediateState()); + } + + @Test + public void testHasAppearedActivity() { + final TaskContainer taskContainer = createTestTaskContainer(); + final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */, + mIntent, taskContainer, mController, null /* pairedPrimaryContainer */); + container.addPendingAppearedActivity(mActivity); + + assertFalse(container.hasAppearedActivity(mActivity.getActivityToken())); + + final List<IBinder> activities = new ArrayList<>(); + activities.add(mActivity.getActivityToken()); + doReturn(activities).when(mInfo).getActivities(); + container.setInfo(mTransaction, mInfo); + + assertTrue(container.hasAppearedActivity(mActivity.getActivityToken())); + } + + @Test + public void testHasPendingAppearedActivity() { + final TaskContainer taskContainer = createTestTaskContainer(); + final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */, + mIntent, taskContainer, mController, null /* pairedPrimaryContainer */); + container.addPendingAppearedActivity(mActivity); + + assertTrue(container.hasPendingAppearedActivity(mActivity.getActivityToken())); + + final List<IBinder> activities = new ArrayList<>(); + activities.add(mActivity.getActivityToken()); + doReturn(activities).when(mInfo).getActivities(); + container.setInfo(mTransaction, mInfo); + + assertFalse(container.hasPendingAppearedActivity(mActivity.getActivityToken())); + } + + @Test + public void testHasActivity() { + final TaskContainer taskContainer = createTestTaskContainer(mController); + final TaskFragmentContainer container1 = new TaskFragmentContainer(null /* activity */, + mIntent, taskContainer, mController, null /* pairedPrimaryContainer */); + final TaskFragmentContainer container2 = new TaskFragmentContainer(null /* activity */, + mIntent, taskContainer, mController, null /* pairedPrimaryContainer */); + + // Activity is pending appeared on container2. + container2.addPendingAppearedActivity(mActivity); + + assertFalse(container1.hasActivity(mActivity.getActivityToken())); + assertTrue(container2.hasActivity(mActivity.getActivityToken())); + + // Activity is pending appeared on container1 (removed from container2). + container1.addPendingAppearedActivity(mActivity); + + assertTrue(container1.hasActivity(mActivity.getActivityToken())); + assertFalse(container2.hasActivity(mActivity.getActivityToken())); + + final List<IBinder> activities = new ArrayList<>(); + activities.add(mActivity.getActivityToken()); + doReturn(activities).when(mInfo).getActivities(); + + // Although Activity is appeared on container2, we prioritize pending appeared record on + // container1. + container2.setInfo(mTransaction, mInfo); + + assertTrue(container1.hasActivity(mActivity.getActivityToken())); + assertFalse(container2.hasActivity(mActivity.getActivityToken())); + + // When the pending appeared record is removed from container1, we respect the appeared + // record in container2. + container1.removePendingAppearedActivity(mActivity.getActivityToken()); + + assertFalse(container1.hasActivity(mActivity.getActivityToken())); + assertTrue(container2.hasActivity(mActivity.getActivityToken())); + } + + @Test + public void testNewContainerWithPairedPrimaryContainer() { + final TaskContainer taskContainer = createTestTaskContainer(); + final TaskFragmentContainer tf0 = new TaskFragmentContainer( + null /* pendingAppearedActivity */, new Intent(), taskContainer, mController, + null /* pairedPrimaryTaskFragment */); + final TaskFragmentContainer tf1 = new TaskFragmentContainer( + null /* pendingAppearedActivity */, new Intent(), taskContainer, mController, + null /* pairedPrimaryTaskFragment */); + + // When tf2 is created with using tf0 as pairedPrimaryContainer, tf2 should be inserted + // right above tf0. + final TaskFragmentContainer tf2 = new TaskFragmentContainer( + null /* pendingAppearedActivity */, new Intent(), taskContainer, mController, tf0); + assertEquals(0, taskContainer.indexOf(tf0)); + assertEquals(1, taskContainer.indexOf(tf2)); + assertEquals(2, taskContainer.indexOf(tf1)); + } + + @Test + public void testNewContainerWithPairedPendingAppearedActivity() { + final TaskContainer taskContainer = createTestTaskContainer(); + final TaskFragmentContainer tf0 = new TaskFragmentContainer( + createMockActivity(), null /* pendingAppearedIntent */, taskContainer, mController, + null /* pairedPrimaryTaskFragment */); + final TaskFragmentContainer tf1 = new TaskFragmentContainer( + null /* pendingAppearedActivity */, new Intent(), taskContainer, mController, + null /* pairedPrimaryTaskFragment */); + + // When tf2 is created with pendingAppearedActivity, tf2 should be inserted below any + // TaskFragment without any Activity. + final TaskFragmentContainer tf2 = new TaskFragmentContainer( + createMockActivity(), null /* pendingAppearedIntent */, taskContainer, mController, + null /* pairedPrimaryTaskFragment */); + assertEquals(0, taskContainer.indexOf(tf0)); + assertEquals(1, taskContainer.indexOf(tf2)); + assertEquals(2, taskContainer.indexOf(tf1)); + } + + @Test + public void testIsVisible() { + final TaskContainer taskContainer = createTestTaskContainer(); + final TaskFragmentContainer container = new TaskFragmentContainer( + null /* pendingAppearedActivity */, new Intent(), taskContainer, mController, + null /* pairedPrimaryTaskFragment */); + + // Not visible when there is not appeared. + assertFalse(container.isVisible()); + + // Respect info.isVisible. + TaskFragmentInfo info = createMockTaskFragmentInfo(container, mActivity, + true /* isVisible */); + container.setInfo(mTransaction, info); + + assertTrue(container.isVisible()); + + info = createMockTaskFragmentInfo(container, mActivity, false /* isVisible */); + container.setInfo(mTransaction, info); + + assertFalse(container.isVisible()); + } + /** Creates a mock activity in the organizer process. */ private Activity createMockActivity() { final Activity activity = mock(Activity.class); diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TransactionManagerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TransactionManagerTest.java new file mode 100644 index 000000000000..459b6d2c31f9 --- /dev/null +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TransactionManagerTest.java @@ -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 androidx.window.extensions.embedding; + +import static android.window.TaskFragmentOrganizer.TASK_FRAGMENT_TRANSIT_CHANGE; +import static android.window.TaskFragmentOrganizer.TASK_FRAGMENT_TRANSIT_CLOSE; +import static android.window.TaskFragmentOrganizer.TASK_FRAGMENT_TRANSIT_OPEN; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.verifyNoMoreInteractions; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThrows; +import static org.mockito.Mockito.clearInvocations; + +import android.os.Binder; +import android.os.IBinder; +import android.platform.test.annotations.Presubmit; +import android.window.TaskFragmentOrganizer; +import android.window.WindowContainerTransaction; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; +import androidx.window.extensions.embedding.TransactionManager.TransactionRecord; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** + * Test class for {@link TransactionManager}. + * + * Build/Install/Run: + * atest WMJetpackUnitTests:TransactionManagerTest + */ +@Presubmit +@SmallTest +@RunWith(AndroidJUnit4.class) +public class TransactionManagerTest { + + @Mock + private TaskFragmentOrganizer mOrganizer; + private TransactionManager mTransactionManager; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + mTransactionManager = new TransactionManager(mOrganizer); + } + + @Test + public void testStartNewTransaction() { + mTransactionManager.startNewTransaction(); + + // Throw exception if #startNewTransaction is called twice without #apply() or #abort(). + assertThrows(IllegalStateException.class, mTransactionManager::startNewTransaction); + + // Allow to start new after #apply() the last transaction. + TransactionRecord transactionRecord = mTransactionManager.startNewTransaction(); + transactionRecord.apply(false /* shouldApplyIndependently */); + transactionRecord = mTransactionManager.startNewTransaction(); + + // Allow to start new after #abort() the last transaction. + transactionRecord.abort(); + mTransactionManager.startNewTransaction(); + } + + @Test + public void testSetTransactionOriginType() { + // Return TASK_FRAGMENT_TRANSIT_CHANGE if there is no trigger type set. + TransactionRecord transactionRecord = mTransactionManager.startNewTransaction(); + + assertEquals(TASK_FRAGMENT_TRANSIT_CHANGE, + transactionRecord.getTransactionTransitionType()); + + // Return the first set type. + mTransactionManager.getCurrentTransactionRecord().abort(); + transactionRecord = mTransactionManager.startNewTransaction(); + transactionRecord.setOriginType(TASK_FRAGMENT_TRANSIT_OPEN); + + assertEquals(TASK_FRAGMENT_TRANSIT_OPEN, transactionRecord.getTransactionTransitionType()); + + transactionRecord.setOriginType(TASK_FRAGMENT_TRANSIT_CLOSE); + + assertEquals(TASK_FRAGMENT_TRANSIT_OPEN, transactionRecord.getTransactionTransitionType()); + + // Reset when #startNewTransaction(). + transactionRecord.abort(); + transactionRecord = mTransactionManager.startNewTransaction(); + + assertEquals(TASK_FRAGMENT_TRANSIT_CHANGE, + transactionRecord.getTransactionTransitionType()); + } + + @Test + public void testGetCurrentTransactionRecord() { + // Throw exception if #getTransaction is called without calling #startNewTransaction(). + assertThrows(IllegalStateException.class, mTransactionManager::getCurrentTransactionRecord); + + TransactionRecord transactionRecord = mTransactionManager.startNewTransaction(); + assertNotNull(transactionRecord); + + // Same WindowContainerTransaction should be returned. + assertSame(transactionRecord, mTransactionManager.getCurrentTransactionRecord()); + + // Reset after #abort(). + transactionRecord.abort(); + assertThrows(IllegalStateException.class, mTransactionManager::getCurrentTransactionRecord); + + // New WindowContainerTransaction after #startNewTransaction(). + mTransactionManager.startNewTransaction(); + assertNotEquals(transactionRecord, mTransactionManager.getCurrentTransactionRecord()); + + // Reset after #apply(). + mTransactionManager.getCurrentTransactionRecord().apply( + false /* shouldApplyIndependently */); + assertThrows(IllegalStateException.class, mTransactionManager::getCurrentTransactionRecord); + } + + @Test + public void testApply() { + // #applyTransaction(false) + TransactionRecord transactionRecord = mTransactionManager.startNewTransaction(); + int transitionType = transactionRecord.getTransactionTransitionType(); + WindowContainerTransaction wct = transactionRecord.getTransaction(); + transactionRecord.apply(false /* shouldApplyIndependently */); + + verify(mOrganizer).applyTransaction(wct, transitionType, + false /* shouldApplyIndependently */); + + // #applyTransaction(true) + clearInvocations(mOrganizer); + transactionRecord = mTransactionManager.startNewTransaction(); + transitionType = transactionRecord.getTransactionTransitionType(); + wct = transactionRecord.getTransaction(); + transactionRecord.apply(true /* shouldApplyIndependently */); + + verify(mOrganizer).applyTransaction(wct, transitionType, + true /* shouldApplyIndependently */); + + // #onTransactionHandled(false) + clearInvocations(mOrganizer); + IBinder token = new Binder(); + transactionRecord = mTransactionManager.startNewTransaction(token); + transitionType = transactionRecord.getTransactionTransitionType(); + wct = transactionRecord.getTransaction(); + transactionRecord.apply(false /* shouldApplyIndependently */); + + verify(mOrganizer).onTransactionHandled(token, wct, transitionType, + false /* shouldApplyIndependently */); + + // #onTransactionHandled(true) + clearInvocations(mOrganizer); + token = new Binder(); + transactionRecord = mTransactionManager.startNewTransaction(token); + transitionType = transactionRecord.getTransactionTransitionType(); + wct = transactionRecord.getTransaction(); + transactionRecord.apply(true /* shouldApplyIndependently */); + + verify(mOrganizer).onTransactionHandled(token, wct, transitionType, + true /* shouldApplyIndependently */); + + // Throw exception if there is any more interaction. + final TransactionRecord record = transactionRecord; + assertThrows(IllegalStateException.class, + () -> record.apply(false /* shouldApplyIndependently */)); + assertThrows(IllegalStateException.class, + () -> record.apply(true /* shouldApplyIndependently */)); + assertThrows(IllegalStateException.class, + record::abort); + } + + @Test + public void testAbort() { + final TransactionRecord transactionRecord = mTransactionManager.startNewTransaction(); + transactionRecord.abort(); + + // Throw exception if there is any more interaction. + verifyNoMoreInteractions(mOrganizer); + assertThrows(IllegalStateException.class, + () -> transactionRecord.apply(false /* shouldApplyIndependently */)); + assertThrows(IllegalStateException.class, + () -> transactionRecord.apply(true /* shouldApplyIndependently */)); + assertThrows(IllegalStateException.class, + transactionRecord::abort); + } +} diff --git a/libs/WindowManager/Jetpack/window-extensions-core-release.aar b/libs/WindowManager/Jetpack/window-extensions-core-release.aar Binary files differnew file mode 100644 index 000000000000..96ff840b984b --- /dev/null +++ b/libs/WindowManager/Jetpack/window-extensions-core-release.aar diff --git a/libs/WindowManager/Jetpack/window-extensions-release.aar b/libs/WindowManager/Jetpack/window-extensions-release.aar Binary files differindex f54ab08d8a8a..c3b6916121d0 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/Android.bp b/libs/WindowManager/Shell/Android.bp index 7960dec5080b..54978bd4496d 100644 --- a/libs/WindowManager/Shell/Android.bp +++ b/libs/WindowManager/Shell/Android.bp @@ -44,6 +44,14 @@ filegroup { srcs: [ "src/com/android/wm/shell/util/**/*.java", "src/com/android/wm/shell/common/split/SplitScreenConstants.java", + "src/com/android/wm/shell/sysui/ShellSharedConstants.java", + "src/com/android/wm/shell/common/TransactionPool.java", + "src/com/android/wm/shell/common/bubbles/*.java", + "src/com/android/wm/shell/common/TriangleShape.java", + "src/com/android/wm/shell/animation/Interpolators.java", + "src/com/android/wm/shell/pip/PipContentOverlay.java", + "src/com/android/wm/shell/startingsurface/SplashScreenExitAnimationUtils.java", + "src/com/android/wm/shell/draganddrop/DragAndDropConstants.java", ], path: "src", } @@ -100,6 +108,21 @@ genrule { out: ["wm_shell_protolog.json"], } +genrule { + name: "protolog.json.gz", + srcs: [":generate-wm_shell_protolog.json"], + out: ["wmshell.protolog.json.gz"], + cmd: "$(location minigzip) -c < $(in) > $(out)", + tools: ["minigzip"], +} + +prebuilt_etc { + name: "wmshell.protolog.json.gz", + system_ext_specific: true, + src: ":protolog.json.gz", + filename_from_src: true, +} + // End ProtoLog java_library { @@ -123,9 +146,6 @@ android_library { resource_dirs: [ "res", ], - java_resources: [ - ":generate-wm_shell_protolog.json", - ], static_libs: [ "androidx.appcompat_appcompat", "androidx.arch.core_core-runtime", diff --git a/libs/WindowManager/Shell/AndroidManifest.xml b/libs/WindowManager/Shell/AndroidManifest.xml index 8881be7326a9..36d3313a9f3b 100644 --- a/libs/WindowManager/Shell/AndroidManifest.xml +++ b/libs/WindowManager/Shell/AndroidManifest.xml @@ -21,5 +21,6 @@ <uses-permission android:name="android.permission.CAPTURE_BLACKOUT_CONTENT" /> <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL" /> <uses-permission android:name="android.permission.ROTATE_SURFACE_FLINGER" /> + <uses-permission android:name="android.permission.WAKEUP_SURFACE_FLINGER" /> <uses-permission android:name="android.permission.READ_FRAME_BUFFER" /> </manifest> diff --git a/libs/WindowManager/Shell/OWNERS b/libs/WindowManager/Shell/OWNERS new file mode 100644 index 000000000000..852edef544b8 --- /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, jorgegil@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..b2d59396d58d 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 @@ -18,6 +18,20 @@ <selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:state_pressed="true"> + <set> + <objectAnimator + android:duration="200" + android:propertyName="scaleX" + android:valueTo="1.0" + android:valueType="floatType"/> + <objectAnimator + android:duration="200" + android:propertyName="scaleY" + android:valueTo="1.0" + android:valueType="floatType"/> + </set> + </item> <item android:state_focused="true"> <set> <objectAnimator diff --git a/libs/WindowManager/Shell/res/color-night/taskbar_background.xml b/libs/WindowManager/Shell/res/color-night/taskbar_background.xml new file mode 100644 index 000000000000..01df006f1bd2 --- /dev/null +++ b/libs/WindowManager/Shell/res/color-night/taskbar_background.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2023 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<!-- Should be the same as in packages/apps/Launcher3/res/color-night-v31/taskbar_background.xml --> +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:color="@android:color/system_neutral1_500" android:lStar="20" /> +</selector>
\ No newline at end of file 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_title_color.xml b/libs/WindowManager/Shell/res/color/decor_title_color.xml new file mode 100644 index 000000000000..1ecc13e4da38 --- /dev/null +++ b/libs/WindowManager/Shell/res/color/decor_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/split_divider_background.xml b/libs/WindowManager/Shell/res/color/letterbox_restart_button_background_ripple.xml index 049980803ee3..a3ca74fac4e6 100644 --- a/libs/WindowManager/Shell/res/color/split_divider_background.xml +++ b/libs/WindowManager/Shell/res/color/letterbox_restart_button_background_ripple.xml @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="utf-8"?> <!-- - ~ Copyright (C) 2021 The Android Open Source Project + ~ Copyright (C) 2023 The Android Open Source Project ~ ~ Licensed under the Apache License, Version 2.0 (the "License"); ~ you may not use this file except in compliance with the License. @@ -15,5 +15,5 @@ ~ limitations under the License. --> <selector xmlns:android="http://schemas.android.com/apk/res/android"> - <item android:color="@android:color/system_neutral1_500" android:lStar="15" /> + <item android:color="@android:color/system_neutral1_900" android:alpha="0.6" /> </selector>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/color/letterbox_restart_dismiss_button_background_ripple.xml b/libs/WindowManager/Shell/res/color/letterbox_restart_dismiss_button_background_ripple.xml new file mode 100644 index 000000000000..a3ca74fac4e6 --- /dev/null +++ b/libs/WindowManager/Shell/res/color/letterbox_restart_dismiss_button_background_ripple.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2023 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:color="@android:color/system_neutral1_900" android:alpha="0.6" /> +</selector>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/color/taskbar_background.xml b/libs/WindowManager/Shell/res/color/taskbar_background.xml index 329e5b9b31a0..876ee02a8adf 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="98" /> </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/color/unfold_background.xml b/libs/WindowManager/Shell/res/color/unfold_background.xml new file mode 100644 index 000000000000..e33eb126012d --- /dev/null +++ b/libs/WindowManager/Shell/res/color/unfold_background.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:color="@android:color/system_neutral1_500" android:lStar="5" /> +</selector>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/drawable/caption_decor_title.xml b/libs/WindowManager/Shell/res/drawable/caption_decor_title.xml new file mode 100644 index 000000000000..6114ad6e277a --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/caption_decor_title.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2023 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<shape android:shape="rectangle" + android:tintMode="multiply" + android:tint="@color/decor_title_color" + xmlns:android="http://schemas.android.com/apk/res/android"> + <solid android:color="@android:color/white" /> +</shape> diff --git a/libs/WindowManager/Shell/res/drawable/decor_back_button_dark.xml b/libs/WindowManager/Shell/res/drawable/decor_back_button_dark.xml new file mode 100644 index 000000000000..5ecba380fb60 --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/decor_back_button_dark.xml @@ -0,0 +1,32 @@ +<!-- + ~ 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" + > + <group android:scaleX="0.5" + android:scaleY="0.5" + android:translateX="4.0" + android:translateY="4.0" > + <path + android:fillColor="@android:color/black" + android:pathData="MM24,40.3 L7.7,24 24,7.7 26.8,10.45 15.3,22H40.3V26H15.3L26.8,37.5Z"/> + + </group> +</vector>
\ No newline at end of file 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..cf9e632f6941 --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/decor_close_button_dark.xml @@ -0,0 +1,30 @@ +<?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"> + <group android:scaleX="0.5" + android:scaleY="0.5" + android:translateX="4.0" + android:translateY="4.0" > + <path + android:fillColor="@android:color/black" + android:pathData="M12.45,38.35 L9.65,35.55 21.2,24 9.65,12.45 12.45,9.65 24,21.2 35.55,9.65 38.35,12.45 26.8,24 38.35,35.55 35.55,38.35 24,26.8Z"/> + </group> +</vector> diff --git a/libs/WindowManager/Shell/res/drawable/decor_handle_dark.xml b/libs/WindowManager/Shell/res/drawable/decor_handle_dark.xml new file mode 100644 index 000000000000..5d7771366bec --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/decor_handle_dark.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="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <group android:translateY="8.0"> + <path + android:fillColor="@android:color/black" android:pathData="M3,5V3H21V5Z"/> + </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..91edbf1a7bd4 --- /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="32.0dp" + android:height="32.0dp" + android:viewportWidth="32.0" + android:viewportHeight="32.0" + android:tint="@color/decor_button_dark_color"> + <path + android:fillColor="@android:color/white" android:pathData="M6,21V19H18V21Z"/> +</vector> diff --git a/libs/WindowManager/Shell/res/drawable/desktop_mode_decor_menu_background.xml b/libs/WindowManager/Shell/res/drawable/desktop_mode_decor_menu_background.xml new file mode 100644 index 000000000000..4ee10f429b37 --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/desktop_mode_decor_menu_background.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 android:shape="rectangle" + xmlns:android="http://schemas.android.com/apk/res/android"> + <solid android:color="@android:color/white" /> + <corners android:radius="@dimen/desktop_mode_handle_menu_corner_radius" /> +</shape> diff --git a/libs/WindowManager/Shell/res/drawable/desktop_mode_decor_title.xml b/libs/WindowManager/Shell/res/drawable/desktop_mode_decor_title.xml new file mode 100644 index 000000000000..ef3006042261 --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/desktop_mode_decor_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_title_color" + xmlns:android="http://schemas.android.com/apk/res/android"> + <solid android:color="@android:color/white" /> +</shape> diff --git a/libs/WindowManager/Shell/res/drawable/desktop_mode_ic_handle_menu_close.xml b/libs/WindowManager/Shell/res/drawable/desktop_mode_ic_handle_menu_close.xml new file mode 100644 index 000000000000..b7521d4200c0 --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/desktop_mode_ic_handle_menu_close.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2023 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:height="20dp" + android:tint="#000000" + android:viewportHeight="24" + android:viewportWidth="24" + android:width="20dp"> + <path + android:fillColor="@android:color/white" + android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/> +</vector> diff --git a/libs/WindowManager/Shell/res/drawable/desktop_mode_ic_handle_menu_desktop.xml b/libs/WindowManager/Shell/res/drawable/desktop_mode_ic_handle_menu_desktop.xml new file mode 100644 index 000000000000..e2b724b8abfd --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/desktop_mode_ic_handle_menu_desktop.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2023 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="20dp" + android:height="20dp" + android:viewportWidth="20" + android:viewportHeight="20"> + <path + android:pathData="M16.667,15H3.333V5H16.667V15ZM16.667,16.667C17.583,16.667 18.333,15.917 18.333,15V5C18.333,4.083 17.583,3.333 16.667,3.333H3.333C2.417,3.333 1.667,4.083 1.667,5V15C1.667,15.917 2.417,16.667 3.333,16.667H16.667ZM15,6.667H9.167V8.333H13.333V10H15V6.667ZM5,9.167H12.5V13.333H5V9.167Z" + android:fillColor="#1C1C14" + android:fillType="evenOdd"/> +</vector> diff --git a/libs/WindowManager/Shell/res/drawable/desktop_mode_ic_handle_menu_floating.xml b/libs/WindowManager/Shell/res/drawable/desktop_mode_ic_handle_menu_floating.xml new file mode 100644 index 000000000000..b0ea98e5f788 --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/desktop_mode_ic_handle_menu_floating.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2023 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="21dp" + android:height="20dp" + android:viewportWidth="21" + android:viewportHeight="20"> + <path + android:pathData="M3.667,15H17V5H3.667V15ZM18.667,15C18.667,15.917 17.917,16.667 17,16.667H3.667C2.75,16.667 2,15.917 2,15V5C2,4.083 2.75,3.333 3.667,3.333H17C17.917,3.333 18.667,4.083 18.667,5V15ZM11.167,6.667H15.333V11.667H11.167V6.667Z" + android:fillColor="#1C1C14" + android:fillType="evenOdd"/> +</vector> diff --git a/libs/WindowManager/Shell/res/drawable/desktop_mode_ic_handle_menu_fullscreen.xml b/libs/WindowManager/Shell/res/drawable/desktop_mode_ic_handle_menu_fullscreen.xml new file mode 100644 index 000000000000..99e1d268c97c --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/desktop_mode_ic_handle_menu_fullscreen.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2023 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="20dp" + android:height="20dp" + android:viewportWidth="20" + android:viewportHeight="20"> + <path + android:pathData="M3.333,15H16.667V5H3.333V15ZM18.333,15C18.333,15.917 17.583,16.667 16.667,16.667H3.333C2.417,16.667 1.667,15.917 1.667,15V5C1.667,4.083 2.417,3.333 3.333,3.333H16.667C17.583,3.333 18.333,4.083 18.333,5V15ZM5,6.667H15V13.333H5V6.667Z" + android:fillColor="#1C1C14" + android:fillType="evenOdd"/> +</vector> diff --git a/libs/WindowManager/Shell/res/drawable/desktop_mode_ic_handle_menu_screenshot.xml b/libs/WindowManager/Shell/res/drawable/desktop_mode_ic_handle_menu_screenshot.xml new file mode 100644 index 000000000000..79a91250bb78 --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/desktop_mode_ic_handle_menu_screenshot.xml @@ -0,0 +1,34 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2023 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="20dp" + android:height="20dp" + android:viewportWidth="20" + android:viewportHeight="20"> + <path + android:pathData="M18.333,5.833L18.333,8.333L16.667,8.333L16.667,5.833L13.333,5.833L13.333,4.167L16.667,4.167C17.587,4.167 18.333,4.913 18.333,5.833Z" + android:fillColor="#1C1C14"/> + <path + android:pathData="M6.667,4.167L3.333,4.167C2.413,4.167 1.667,4.913 1.667,5.833L1.667,8.333L3.333,8.333L3.333,5.833L6.667,5.833L6.667,4.167Z" + android:fillColor="#1C1C14"/> + <path + android:pathData="M6.667,14.167L3.333,14.167L3.333,11.667L1.667,11.667L1.667,14.167C1.667,15.087 2.413,15.833 3.333,15.833L6.667,15.833L6.667,14.167Z" + android:fillColor="#1C1C14"/> + <path + android:pathData="M13.333,15.833L16.667,15.833C17.587,15.833 18.333,15.087 18.333,14.167L18.333,11.667L16.667,11.667L16.667,14.167L13.333,14.167L13.333,15.833Z" + android:fillColor="#1C1C14"/> +</vector> diff --git a/libs/WindowManager/Shell/res/drawable/desktop_mode_ic_handle_menu_select.xml b/libs/WindowManager/Shell/res/drawable/desktop_mode_ic_handle_menu_select.xml new file mode 100644 index 000000000000..7c4f49979455 --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/desktop_mode_ic_handle_menu_select.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2023 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="20dp" + android:height="20dp" + android:viewportWidth="20" + android:viewportHeight="20"> + <path + android:pathData="M15.701,14.583L18.567,17.5L17.425,18.733L14.525,15.833L12.442,17.917V12.5H17.917L15.701,14.583ZM15.833,5.833H17.5V7.5H15.833V5.833ZM17.5,4.167H15.833V2.567C16.75,2.567 17.5,3.333 17.5,4.167ZM12.5,2.5H14.167V4.167H12.5V2.5ZM15.833,9.167H17.5V10.833H15.833V9.167ZM7.5,17.5H5.833V15.833H7.5V17.5ZM4.167,7.5H2.5V5.833H4.167V7.5ZM4.167,2.567V4.167H2.5C2.5,3.333 3.333,2.567 4.167,2.567ZM4.167,14.167H2.5V12.5H4.167V14.167ZM7.5,4.167H5.833V2.5H7.5V4.167ZM10.833,4.167H9.167V2.5H10.833V4.167ZM10.833,17.5H9.167V15.833H10.833V17.5ZM4.167,10.833H2.5V9.167H4.167V10.833ZM4.167,17.567C3.25,17.567 2.5,16.667 2.5,15.833H4.167V17.567Z" + android:fillColor="#1C1C14"/> +</vector> diff --git a/libs/WindowManager/Shell/res/drawable/desktop_mode_ic_handle_menu_splitscreen.xml b/libs/WindowManager/Shell/res/drawable/desktop_mode_ic_handle_menu_splitscreen.xml new file mode 100644 index 000000000000..853ab60e046f --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/desktop_mode_ic_handle_menu_splitscreen.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2023 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="21dp" + android:height="20dp" + android:viewportWidth="21" + android:viewportHeight="20"> + <path + android:pathData="M17.333,15H4V5H17.333V15ZM17.333,16.667C18.25,16.667 19,15.917 19,15V5C19,4.083 18.25,3.333 17.333,3.333H4C3.083,3.333 2.333,4.083 2.333,5V15C2.333,15.917 3.083,16.667 4,16.667H17.333ZM9.833,6.667H5.667V13.333H9.833V6.667ZM11.5,6.667H15.667V13.333H11.5V6.667Z" + android:fillColor="#1C1C14" + android:fillType="evenOdd"/> +</vector> diff --git a/libs/WindowManager/Shell/res/drawable/desktop_windowing_transition_background.xml b/libs/WindowManager/Shell/res/drawable/desktop_windowing_transition_background.xml new file mode 100644 index 000000000000..022594982ca3 --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/desktop_windowing_transition_background.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2023 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<shape android:shape="rectangle" + xmlns:android="http://schemas.android.com/apk/res/android"> + <solid android:color="#bf309fb5" /> + <corners android:radius="20dp" /> + <stroke android:width="1dp" color="#A00080FF"/> +</shape> diff --git a/libs/WindowManager/Shell/res/drawable/handle_menu_background.xml b/libs/WindowManager/Shell/res/drawable/handle_menu_background.xml new file mode 100644 index 000000000000..e307f007e4a4 --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/handle_menu_background.xml @@ -0,0 +1,30 @@ +<?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="210.0dp" + android:height="64.0dp" + android:tint="@color/decor_button_light_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="M18.3334 14L13.3334 14L13.3334 2L18.3334 2L18.3334 14ZM20.3334 14L20.3334 2C20.3334 0.9 19.4334 -3.93402e-08 18.3334 -8.74228e-08L13.3334 -3.0598e-07C12.2334 -3.54062e-07 11.3334 0.9 11.3334 2L11.3334 14C11.3334 15.1 12.2334 16 13.3334 16L18.3334 16C19.4334 16 20.3334 15.1 20.3334 14ZM7.33337 14L2.33337 14L2.33337 2L7.33337 2L7.33337 14ZM9.33337 14L9.33337 2C9.33337 0.899999 8.43337 -5.20166e-07 7.33337 -5.68248e-07L2.33337 -7.86805e-07C1.23337 -8.34888e-07 0.333374 0.899999 0.333374 2L0.333373 14C0.333373 15.1 1.23337 16 2.33337 16L7.33337 16C8.43337 16 9.33337 15.1 9.33337 14Z"/> + </group> +</vector> diff --git a/libs/WindowManager/Shell/res/drawable/ic_baseline_expand_more_24.xml b/libs/WindowManager/Shell/res/drawable/ic_baseline_expand_more_24.xml new file mode 100644 index 000000000000..3e0297ab612b --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/ic_baseline_expand_more_24.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2023 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License + --> +<vector android:height="24dp" android:tint="#000000" + android:viewportHeight="24" android:viewportWidth="24" + android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> + <path android:fillColor="@android:color/black" android:pathData="M16.59,8.59L12,13.17 7.41,8.59 6,10l6,6 6,-6z"/> +</vector> diff --git a/libs/WindowManager/Shell/res/drawable/letterbox_education_dismiss_button_background_ripple.xml b/libs/WindowManager/Shell/res/drawable/letterbox_education_dismiss_button_background_ripple.xml index 42572d64b96f..a2699681e656 100644 --- a/libs/WindowManager/Shell/res/drawable/letterbox_education_dismiss_button_background_ripple.xml +++ b/libs/WindowManager/Shell/res/drawable/letterbox_education_dismiss_button_background_ripple.xml @@ -14,7 +14,30 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License. --> -<ripple xmlns:android="http://schemas.android.com/apk/res/android" - android:color="@color/letterbox_education_dismiss_button_background_ripple"> - <item android:drawable="@drawable/letterbox_education_dismiss_button_background"/> -</ripple>
\ No newline at end of file +<inset xmlns:android="http://schemas.android.com/apk/res/android" + android:insetTop="@dimen/letterbox_education_dialog_vertical_inset" + android:insetBottom="@dimen/letterbox_education_dialog_vertical_inset"> + <ripple android:color="@color/letterbox_education_dismiss_button_background_ripple"> + <item android:id="@android:id/mask"> + <shape android:shape="rectangle"> + <corners android:radius="@dimen/letterbox_education_dialog_button_radius"/> + <solid android:color="@android:color/white"/> + </shape> + </item> + <item> + <shape android:shape="rectangle"> + <solid android:color="@android:color/transparent"/> + </shape> + </item> + <item> + <shape android:shape="rectangle"> + <solid android:color="@color/letterbox_education_accent_primary"/> + <corners android:radius="@dimen/letterbox_education_dialog_button_radius"/> + <padding android:left="@dimen/letterbox_education_dialog_horizontal_padding" + android:top="@dimen/letterbox_education_dialog_vertical_padding" + android:right="@dimen/letterbox_education_dialog_horizontal_padding" + android:bottom="@dimen/letterbox_education_dialog_vertical_padding"/> + </shape> + </item> + </ripple> +</inset> 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/letterbox_restart_button_background_ripple.xml b/libs/WindowManager/Shell/res/drawable/letterbox_restart_button_background_ripple.xml new file mode 100644 index 000000000000..1f125148775d --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/letterbox_restart_button_background_ripple.xml @@ -0,0 +1,44 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2023 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<inset xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" + android:insetTop="@dimen/letterbox_restart_dialog_vertical_inset" + android:insetBottom="@dimen/letterbox_restart_dialog_vertical_inset"> + <ripple android:color="@color/letterbox_restart_dismiss_button_background_ripple"> + <item android:id="@android:id/mask"> + <shape android:shape="rectangle"> + <corners android:radius="@dimen/letterbox_restart_dialog_button_radius"/> + <solid android:color="@android:color/white"/> + </shape> + </item> + <item> + <shape android:shape="rectangle"> + <solid android:color="@android:color/transparent"/> + </shape> + </item> + <item> + <shape android:shape="rectangle"> + <solid android:color="?androidprv:attr/colorAccentPrimaryVariant"/> + <corners android:radius="@dimen/letterbox_restart_dialog_button_radius"/> + <padding android:left="@dimen/letterbox_restart_dialog_horizontal_padding" + android:top="@dimen/letterbox_restart_dialog_vertical_padding" + android:right="@dimen/letterbox_restart_dialog_horizontal_padding" + android:bottom="@dimen/letterbox_restart_dialog_vertical_padding"/> + </shape> + </item> + </ripple> +</inset>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/drawable/letterbox_restart_checkbox_button.xml b/libs/WindowManager/Shell/res/drawable/letterbox_restart_checkbox_button.xml new file mode 100644 index 000000000000..c247c6e4c8cf --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/letterbox_restart_checkbox_button.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2023 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<selector xmlns:android="http://schemas.android.com/apk/res/android" > + <item android:state_checked="true" + android:drawable="@drawable/letterbox_restart_checkbox_checked" /> + <item android:state_pressed="true" + android:drawable="@drawable/letterbox_restart_checkbox_checked" /> + <item android:state_pressed="false" + android:drawable="@drawable/letterbox_restart_checkbox_unchecked" /> +</selector>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/drawable/letterbox_restart_checkbox_checked.xml b/libs/WindowManager/Shell/res/drawable/letterbox_restart_checkbox_checked.xml new file mode 100644 index 000000000000..4f97e2c7ea0d --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/letterbox_restart_checkbox_checked.xml @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2023 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="20dp" + android:height="20dp" + android:viewportWidth="20" + android:viewportHeight="20" + android:tint="?android:attr/textColorSecondary"> + <group + android:scaleX="0.83333333333" + android:scaleY="0.83333333333" + android:translateX="0" + android:translateY="0"> + <path + android:fillColor="?android:attr/textColorSecondary" + android:pathData="M10.6,16.2 L17.65,9.15 16.25,7.75 10.6,13.4 7.75,10.55 6.35,11.95ZM5,21Q4.175,21 3.587,20.413Q3,19.825 3,19V5Q3,4.175 3.587,3.587Q4.175,3 5,3H19Q19.825,3 20.413,3.587Q21,4.175 21,5V19Q21,19.825 20.413,20.413Q19.825,21 19,21Z"/> + </group> +</vector>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/drawable/letterbox_restart_checkbox_unchecked.xml b/libs/WindowManager/Shell/res/drawable/letterbox_restart_checkbox_unchecked.xml new file mode 100644 index 000000000000..bb14d1961e81 --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/letterbox_restart_checkbox_unchecked.xml @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2023 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="20dp" + android:height="20dp" + android:viewportWidth="20" + android:viewportHeight="20" + android:tint="?android:attr/textColorSecondary"> + <group + android:scaleX="0.83333333333" + android:scaleY="0.83333333333" + android:translateX="0" + android:translateY="0"> + <path + android:fillColor="?android:attr/textColorSecondary" + android:pathData="M5,21Q4.175,21 3.587,20.413Q3,19.825 3,19V5Q3,4.175 3.587,3.587Q4.175,3 5,3H19Q19.825,3 20.413,3.587Q21,4.175 21,5V19Q21,19.825 20.413,20.413Q19.825,21 19,21ZM5,19H19Q19,19 19,19Q19,19 19,19V5Q19,5 19,5Q19,5 19,5H5Q5,5 5,5Q5,5 5,5V19Q5,19 5,19Q5,19 5,19Z"/> + </group> +</vector>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/drawable/letterbox_restart_dialog_background.xml b/libs/WindowManager/Shell/res/drawable/letterbox_restart_dialog_background.xml new file mode 100644 index 000000000000..e3c18a2db66f --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/letterbox_restart_dialog_background.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2023 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<shape xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" + android:shape="rectangle"> + <solid android:color="?androidprv:attr/colorSurface"/> + <corners android:radius="@dimen/letterbox_restart_dialog_corner_radius"/> +</shape>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/drawable/letterbox_restart_dismiss_button_background_ripple.xml b/libs/WindowManager/Shell/res/drawable/letterbox_restart_dismiss_button_background_ripple.xml new file mode 100644 index 000000000000..3aa0981e45aa --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/letterbox_restart_dismiss_button_background_ripple.xml @@ -0,0 +1,46 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2023 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<inset xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" + android:insetTop="@dimen/letterbox_restart_dialog_vertical_inset" + android:insetBottom="@dimen/letterbox_restart_dialog_vertical_inset"> + <ripple android:color="@color/letterbox_restart_dismiss_button_background_ripple"> + <item android:id="@android:id/mask"> + <shape android:shape="rectangle"> + <corners android:radius="@dimen/letterbox_restart_dialog_button_radius"/> + <solid android:color="@android:color/white"/> + </shape> + </item> + <item> + <shape android:shape="rectangle"> + <solid android:color="@android:color/transparent"/> + </shape> + </item> + <item> + <shape android:shape="rectangle"> + <stroke android:color="?androidprv:attr/colorAccentPrimaryVariant" + android:width="1dp"/> + <solid android:color="?androidprv:attr/colorSurface"/> + <corners android:radius="@dimen/letterbox_restart_dialog_button_radius"/> + <padding android:left="@dimen/letterbox_restart_dialog_horizontal_padding" + android:top="@dimen/letterbox_restart_dialog_vertical_padding" + android:right="@dimen/letterbox_restart_dialog_horizontal_padding" + android:bottom="@dimen/letterbox_restart_dialog_vertical_padding"/> + </shape> + </item> + </ripple> +</inset>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/drawable/letterbox_restart_header_ic_arrows.xml b/libs/WindowManager/Shell/res/drawable/letterbox_restart_header_ic_arrows.xml new file mode 100644 index 000000000000..5053971a17d3 --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/letterbox_restart_header_ic_arrows.xml @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2023 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" + android:width="@dimen/letterbox_restart_dialog_title_icon_width" + android:height="@dimen/letterbox_restart_dialog_title_icon_height" + android:viewportWidth="45" + android:viewportHeight="44"> + <group + android:scaleX="0.8" + android:scaleY="0.8" + android:translateX="8" + android:translateY="8"> + <path + android:pathData="M0,36V24.5H3V30.85L10.4,23.45L12.55,25.6L5.15,33H11.5V36H0ZM24.5,36V33H30.85L23.5,25.65L25.65,23.5L33,30.85V24.5H36V36H24.5ZM10.35,12.5L3,5.15V11.5H0V0H11.5V3H5.15L12.5,10.35L10.35,12.5ZM25.65,12.5L23.5,10.35L30.85,3H24.5V0H36V11.5H33V5.15L25.65,12.5Z" + android:fillColor="?androidprv:attr/colorAccentPrimaryVariant"/> + </group> +</vector>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/drawable/letterbox_restart_ic_arrows.xml b/libs/WindowManager/Shell/res/drawable/letterbox_restart_ic_arrows.xml new file mode 100644 index 000000000000..b6e0172af1df --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/letterbox_restart_ic_arrows.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. + --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="@dimen/letterbox_restart_dialog_title_icon_width" + android:height="@dimen/letterbox_restart_dialog_title_icon_height" + android:viewportWidth="45" + android:viewportHeight="44"> + <group + android:scaleX="0.8" + android:scaleY="0.8" + android:translateX="8" + android:translateY="8"> + <path + android:pathData="M0,36V24.5H3V30.85L10.4,23.45L12.55,25.6L5.15,33H11.5V36H0ZM24.5,36V33H30.85L23.5,25.65L25.65,23.5L33,30.85V24.5H36V36H24.5ZM10.35,12.5L3,5.15V11.5H0V0H11.5V3H5.15L12.5,10.35L10.35,12.5ZM25.65,12.5L23.5,10.35L30.85,3H24.5V0H36V11.5H33V5.15L25.65,12.5Z" + android:fillColor="@color/compat_controls_text"/> + </group> +</vector>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/drawable/reachability_education_ic_left_hand.xml b/libs/WindowManager/Shell/res/drawable/reachability_education_ic_left_hand.xml new file mode 100644 index 000000000000..c400dc676325 --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/reachability_education_ic_left_hand.xml @@ -0,0 +1,699 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2023 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<animated-vector xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:aapt="http://schemas.android.com/aapt"> + <aapt:attr name="android:drawable"> + <vector android:height="30dp" android:width="30dp" android:viewportHeight="30" + android:viewportWidth="30"> + <group android:name="_R_G" android:scaleX="-1" android:translateX="30"> + <group android:name="_R_G_L_0_G" android:translateX="-135" android:translateY="-135" + android:pivotX="150" android:pivotY="150" android:scaleX="0.1" + android:scaleY="0.1"> + <group android:name="_R_G_L_0_G_L_1_G" android:translateX="134.624" + android:translateY="87.514" android:pivotX="11.625" android:pivotY="6.39" + android:scaleX="10" android:scaleY="10"> + <group android:name="_R_G_L_0_G_L_1_G_D_0_P_0_G_0_T_0" + android:translateX="11.625" android:translateY="6.464" + android:scaleX="1" android:scaleY="1"> + <path android:name="_R_G_L_0_G_L_1_G_D_0_P_0" + android:fillColor="@color/letterbox_reachability_education_item_color" + android:fillAlpha="1" + android:fillType="nonZero" + android:pathData=" M-1.54 5.39 C-3.87,4.71 -5.49,2.54 -5.49,0.11 C-5.49,-2.92 -3.03,-5.38 0,-5.38 C3.03,-5.38 5.49,-2.92 5.49,0.11 C5.49,2.11 4.41,3.95 2.66,4.92 C2.66,4.92 1.69,3.17 1.69,3.17 C2.8,2.55 3.49,1.38 3.49,0.11 C3.49,-1.82 1.93,-3.38 0,-3.38 C-1.93,-3.38 -3.49,-1.82 -3.49,0.11 C-3.49,1.65 -2.46,3.03 -0.98,3.47 C-0.98,3.47 -1.54,5.39 -1.54,5.39c "/> + </group> + </group> + <group android:name="_R_G_L_0_G_L_0_G" android:translateX="138" + android:translateY="138" android:pivotX="12" android:pivotY="12" + android:scaleX="10" android:scaleY="10"> + <path android:name="_R_G_L_0_G_L_0_G_D_0_P_0" + android:fillColor="@color/letterbox_reachability_education_item_color" + android:fillAlpha="1" android:fillType="nonZero" + android:pathData=" M19.81 13.64 C19.62,13.23 19.33,12.93 18.93,12.75 C18.93,12.75 15.23,10.95 15.23,10.95 C15.16,10.9 15.09,10.86 15.01,10.84 C14.99,10.83 14.96,10.83 14.94,10.83 C14.88,10.81 14.83,10.8 14.78,10.8 C14.78,10.8 14.13,10.8 14.13,10.8 C14.13,10.8 14.13,8.9 14.13,8.9 C14.13,8.9 14.13,6.5 14.13,6.5 C14.13,5.8 13.88,5.21 13.4,4.72 C12.92,4.24 12.33,4 11.63,4 C10.93,4 10.33,4.24 9.85,4.72 C9.37,5.21 9.13,5.8 9.13,6.5 C9.13,6.5 9.13,8.95 9.13,8.95 C9.13,8.95 9.13,11.4 9.13,11.4 C9.13,11.4 9.13,14.65 9.13,14.65 C9.13,14.65 7.18,14.2 7.18,14.2 C6.86,14.12 6.56,14.14 6.26,14.26 C5.97,14.39 5.71,14.57 5.48,14.8 C5.48,14.8 4.08,16.25 4.08,16.25 C4.08,16.25 9.23,21.4 9.23,21.4 C9.41,21.58 9.63,21.73 9.88,21.84 C10.13,21.95 10.39,22 10.68,22 C10.68,22 17.08,22 17.08,22 C17.56,22 17.99,21.85 18.38,21.54 C18.76,21.23 18.99,20.83 19.08,20.35 C19.08,20.35 19.98,14.9 19.98,14.9 C20.06,14.47 20,14.05 19.81,13.64c "/> + </group> + </group> + </group> + <group android:name="time_group"/> + </vector> + </aapt:attr> + <target android:name="_R_G_L_0_G_L_1_G_D_0_P_0"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator android:propertyName="fillAlpha" android:duration="500" + android:startOffset="0" android:valueFrom="1" android:valueTo="1" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator + android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="fillAlpha" android:duration="250" + android:startOffset="500" android:valueFrom="1" + android:valueTo="0.1" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator + android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="fillAlpha" android:duration="83" + android:startOffset="750" android:valueFrom="0.1" + android:valueTo="1" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator + android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="fillAlpha" android:duration="250" + android:startOffset="833" android:valueFrom="1" + android:valueTo="0.1" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator + android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="fillAlpha" android:duration="83" + android:startOffset="1083" android:valueFrom="0.1" + android:valueTo="1" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator + android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="fillAlpha" android:duration="833" + android:startOffset="1167" android:valueFrom="1" android:valueTo="1" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator + android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="fillAlpha" android:duration="250" + android:startOffset="2000" android:valueFrom="1" + android:valueTo="0.1" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator + android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="fillAlpha" android:duration="83" + android:startOffset="2250" android:valueFrom="0.1" + android:valueTo="1" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator + android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="fillAlpha" android:duration="250" + android:startOffset="2333" android:valueFrom="1" + android:valueTo="0.1" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator + android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="fillAlpha" android:duration="83" + android:startOffset="2583" android:valueFrom="0.1" + android:valueTo="1" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator + android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="fillAlpha" android:duration="833" + android:startOffset="2667" android:valueFrom="1" android:valueTo="1" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator + android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="fillAlpha" android:duration="250" + android:startOffset="3500" android:valueFrom="1" + android:valueTo="0.1" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator + android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="fillAlpha" android:duration="83" + android:startOffset="3750" android:valueFrom="0.1" + android:valueTo="1" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator + android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="fillAlpha" android:duration="250" + android:startOffset="3833" android:valueFrom="1" + android:valueTo="0.1" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator + android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="fillAlpha" android:duration="83" + android:startOffset="4083" android:valueFrom="0.1" + android:valueTo="1" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator + android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="fillAlpha" android:duration="833" + android:startOffset="4167" android:valueFrom="1" android:valueTo="1" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator + android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="fillAlpha" android:duration="250" + android:startOffset="5000" android:valueFrom="1" + android:valueTo="0.1" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator + android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="fillAlpha" android:duration="83" + android:startOffset="5250" android:valueFrom="0.1" + android:valueTo="1" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator + android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="fillAlpha" android:duration="250" + android:startOffset="5333" android:valueFrom="1" + android:valueTo="0.1" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator + android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="fillAlpha" android:duration="83" + android:startOffset="5583" android:valueFrom="0.1" + android:valueTo="1" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator + android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="_R_G_L_0_G_L_1_G_D_0_P_0_G_0_T_0"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator android:propertyName="scaleX" android:duration="500" + android:startOffset="0" android:valueFrom="1" android:valueTo="1" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.001,0 0,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="scaleY" android:duration="500" + android:startOffset="0" android:valueFrom="1" android:valueTo="1" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.001,0 0,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="scaleX" android:duration="250" + android:startOffset="500" android:valueFrom="1" + android:valueTo="1.4000000000000001" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.001,0 0,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="scaleY" android:duration="250" + android:startOffset="500" android:valueFrom="1" + android:valueTo="1.4000000000000001" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.001,0 0,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="scaleX" android:duration="83" + android:startOffset="750" android:valueFrom="1.4000000000000001" + android:valueTo="1" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.999,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="scaleY" android:duration="83" + android:startOffset="750" android:valueFrom="1.4000000000000001" + android:valueTo="1" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.999,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="scaleX" android:duration="250" + android:startOffset="833" android:valueFrom="1" + android:valueTo="1.4000000000000001" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.001,0 0,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="scaleY" android:duration="250" + android:startOffset="833" android:valueFrom="1" + android:valueTo="1.4000000000000001" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.001,0 0,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="scaleX" android:duration="83" + android:startOffset="1083" android:valueFrom="1.4000000000000001" + android:valueTo="1" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.999,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="scaleY" android:duration="83" + android:startOffset="1083" android:valueFrom="1.4000000000000001" + android:valueTo="1" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.999,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="scaleX" android:duration="833" + android:startOffset="1167" android:valueFrom="1" android:valueTo="1" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.833,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="scaleY" android:duration="833" + android:startOffset="1167" android:valueFrom="1" android:valueTo="1" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.833,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="scaleX" android:duration="250" + android:startOffset="2000" android:valueFrom="1" + android:valueTo="1.4000000000000001" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.001,0 0,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="scaleY" android:duration="250" + android:startOffset="2000" android:valueFrom="1" + android:valueTo="1.4000000000000001" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.001,0 0,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="scaleX" android:duration="83" + android:startOffset="2250" android:valueFrom="1.4000000000000001" + android:valueTo="1" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.999,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="scaleY" android:duration="83" + android:startOffset="2250" android:valueFrom="1.4000000000000001" + android:valueTo="1" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.999,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="scaleX" android:duration="250" + android:startOffset="2333" android:valueFrom="1" + android:valueTo="1.4000000000000001" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.001,0 0,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="scaleY" android:duration="250" + android:startOffset="2333" android:valueFrom="1" + android:valueTo="1.4000000000000001" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.001,0 0,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="scaleX" android:duration="83" + android:startOffset="2583" android:valueFrom="1.4000000000000001" + android:valueTo="1" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.999,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="scaleY" android:duration="83" + android:startOffset="2583" android:valueFrom="1.4000000000000001" + android:valueTo="1" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.999,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="scaleX" android:duration="833" + android:startOffset="2667" android:valueFrom="1" android:valueTo="1" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.833,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="scaleY" android:duration="833" + android:startOffset="2667" android:valueFrom="1" android:valueTo="1" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.833,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="scaleX" android:duration="250" + android:startOffset="3500" android:valueFrom="1" + android:valueTo="1.4000000000000001" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.001,0 0,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="scaleY" android:duration="250" + android:startOffset="3500" android:valueFrom="1" + android:valueTo="1.4000000000000001" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.001,0 0,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="scaleX" android:duration="83" + android:startOffset="3750" android:valueFrom="1.4000000000000001" + android:valueTo="1" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.999,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="scaleY" android:duration="83" + android:startOffset="3750" android:valueFrom="1.4000000000000001" + android:valueTo="1" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.999,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="scaleX" android:duration="250" + android:startOffset="3833" android:valueFrom="1" + android:valueTo="1.4000000000000001" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.001,0 0,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="scaleY" android:duration="250" + android:startOffset="3833" android:valueFrom="1" + android:valueTo="1.4000000000000001" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.001,0 0,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="scaleX" android:duration="83" + android:startOffset="4083" android:valueFrom="1.4000000000000001" + android:valueTo="1" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.999,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="scaleY" android:duration="83" + android:startOffset="4083" android:valueFrom="1.4000000000000001" + android:valueTo="1" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.999,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="scaleX" android:duration="833" + android:startOffset="4167" android:valueFrom="1" android:valueTo="1" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.833,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="scaleY" android:duration="833" + android:startOffset="4167" android:valueFrom="1" android:valueTo="1" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.833,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="scaleX" android:duration="250" + android:startOffset="5000" android:valueFrom="1" + android:valueTo="1.4000000000000001" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.001,0 0,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="scaleY" android:duration="250" + android:startOffset="5000" android:valueFrom="1" + android:valueTo="1.4000000000000001" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.001,0 0,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="scaleX" android:duration="83" + android:startOffset="5250" android:valueFrom="1.4000000000000001" + android:valueTo="1" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.999,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="scaleY" android:duration="83" + android:startOffset="5250" android:valueFrom="1.4000000000000001" + android:valueTo="1" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.999,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="scaleX" android:duration="250" + android:startOffset="5333" android:valueFrom="1" + android:valueTo="1.4000000000000001" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.001,0 0,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="scaleY" android:duration="250" + android:startOffset="5333" android:valueFrom="1" + android:valueTo="1.4000000000000001" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.001,0 0,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="scaleX" android:duration="83" + android:startOffset="5583" android:valueFrom="1.4000000000000001" + android:valueTo="1" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.999,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="scaleY" android:duration="83" + android:startOffset="5583" android:valueFrom="1.4000000000000001" + android:valueTo="1" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.999,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="_R_G_L_0_G_L_0_G_D_0_P_0"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator android:propertyName="pathData" android:duration="500" + android:startOffset="0" + android:valueFrom="M19.81 13.64 C19.62,13.23 19.33,12.93 18.93,12.75 C18.93,12.75 15.23,10.95 15.23,10.95 C15.16,10.9 15.09,10.86 15.01,10.84 C14.99,10.83 14.96,10.83 14.94,10.83 C14.88,10.81 14.83,10.8 14.78,10.8 C14.78,10.8 14.13,10.8 14.13,10.8 C14.13,10.8 14.13,8.9 14.13,8.9 C14.13,8.9 14.13,6.5 14.13,6.5 C14.13,5.8 13.88,5.21 13.4,4.72 C12.92,4.24 12.33,4 11.63,4 C10.93,4 10.33,4.24 9.85,4.72 C9.37,5.21 9.13,5.8 9.13,6.5 C9.13,6.5 9.13,8.95 9.13,8.95 C9.13,8.95 9.13,11.4 9.13,11.4 C9.13,11.4 9.13,14.65 9.13,14.65 C9.13,14.65 7.18,14.2 7.18,14.2 C6.86,14.12 6.56,14.14 6.26,14.26 C5.97,14.39 5.71,14.57 5.48,14.8 C5.48,14.8 4.08,16.25 4.08,16.25 C4.08,16.25 9.23,21.4 9.23,21.4 C9.41,21.58 9.63,21.73 9.88,21.84 C10.13,21.95 10.39,22 10.68,22 C10.68,22 17.08,22 17.08,22 C17.56,22 17.99,21.85 18.38,21.54 C18.76,21.23 18.99,20.83 19.08,20.35 C19.08,20.35 19.98,14.9 19.98,14.9 C20.06,14.47 20,14.05 19.81,13.64c M17.08 20 C17.08,20 10.68,20 10.68,20 C10.68,20 6.88,16.2 6.88,16.2 C6.88,16.2 11.13,17.1 11.13,17.1 C11.13,17.1 11.13,6.5 11.13,6.5 C11.13,6.35 11.17,6.23 11.26,6.14 C11.35,6.05 11.48,6 11.63,6 C11.78,6 11.9,6.05 11.99,6.14 C12.08,6.23 12.13,6.35 12.13,6.5 C12.13,6.5 12.13,12.5 12.13,12.5 C12.13,12.5 13.88,12.5 13.88,12.5 C13.88,12.5 18.02,14.55 18.02,14.55 C18.02,14.55 17.08,20 17.08,20c " + android:valueTo="M19.81 13.64 C19.62,13.23 19.33,12.93 18.93,12.75 C18.93,12.75 15.23,10.95 15.23,10.95 C15.16,10.9 15.09,10.86 15.01,10.84 C14.99,10.83 14.96,10.83 14.94,10.83 C14.88,10.81 14.83,10.8 14.78,10.8 C14.78,10.8 14.13,10.8 14.13,10.8 C14.13,10.8 14.13,8.9 14.13,8.9 C14.13,8.9 14.13,6.5 14.13,6.5 C14.13,5.8 13.88,5.21 13.4,4.72 C12.92,4.24 12.33,4 11.63,4 C10.93,4 10.33,4.24 9.85,4.72 C9.37,5.21 9.13,5.8 9.13,6.5 C9.13,6.5 9.13,8.95 9.13,8.95 C9.13,8.95 9.13,11.4 9.13,11.4 C9.13,11.4 9.13,14.65 9.13,14.65 C9.13,14.65 7.18,14.2 7.18,14.2 C6.86,14.12 6.56,14.14 6.26,14.26 C5.97,14.39 5.71,14.57 5.48,14.8 C5.48,14.8 4.08,16.25 4.08,16.25 C4.08,16.25 9.23,21.4 9.23,21.4 C9.41,21.58 9.63,21.73 9.88,21.84 C10.13,21.95 10.39,22 10.68,22 C10.68,22 17.08,22 17.08,22 C17.56,22 17.99,21.85 18.38,21.54 C18.76,21.23 18.99,20.83 19.08,20.35 C19.08,20.35 19.98,14.9 19.98,14.9 C20.06,14.47 20,14.05 19.81,13.64c M17.08 20 C17.08,20 10.68,20 10.68,20 C10.68,20 6.88,16.2 6.88,16.2 C6.88,16.2 11.13,17.1 11.13,17.1 C11.13,17.1 11.13,6.5 11.13,6.5 C11.13,6.35 11.17,6.23 11.26,6.14 C11.35,6.05 11.48,6 11.63,6 C11.78,6 11.9,6.05 11.99,6.14 C12.08,6.23 12.13,6.35 12.13,6.5 C12.13,6.5 12.13,12.5 12.13,12.5 C12.13,12.5 13.88,12.5 13.88,12.5 C13.88,12.5 18.02,14.55 18.02,14.55 C18.02,14.55 17.08,20 17.08,20c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.2,0 0,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="pathData" android:duration="250" + android:startOffset="500" + android:valueFrom="M19.81 13.64 C19.62,13.23 19.33,12.93 18.93,12.75 C18.93,12.75 15.23,10.95 15.23,10.95 C15.16,10.9 15.09,10.86 15.01,10.84 C14.99,10.83 14.96,10.83 14.94,10.83 C14.88,10.81 14.83,10.8 14.78,10.8 C14.78,10.8 14.13,10.8 14.13,10.8 C14.13,10.8 14.13,8.9 14.13,8.9 C14.13,8.9 14.13,6.5 14.13,6.5 C14.13,5.8 13.88,5.21 13.4,4.72 C12.92,4.24 12.33,4 11.63,4 C10.93,4 10.33,4.24 9.85,4.72 C9.37,5.21 9.13,5.8 9.13,6.5 C9.13,6.5 9.13,8.95 9.13,8.95 C9.13,8.95 9.13,11.4 9.13,11.4 C9.13,11.4 9.13,14.65 9.13,14.65 C9.13,14.65 7.18,14.2 7.18,14.2 C6.86,14.12 6.56,14.14 6.26,14.26 C5.97,14.39 5.71,14.57 5.48,14.8 C5.48,14.8 4.08,16.25 4.08,16.25 C4.08,16.25 9.23,21.4 9.23,21.4 C9.41,21.58 9.63,21.73 9.88,21.84 C10.13,21.95 10.39,22 10.68,22 C10.68,22 17.08,22 17.08,22 C17.56,22 17.99,21.85 18.38,21.54 C18.76,21.23 18.99,20.83 19.08,20.35 C19.08,20.35 19.98,14.9 19.98,14.9 C20.06,14.47 20,14.05 19.81,13.64c M17.08 20 C17.08,20 10.68,20 10.68,20 C10.68,20 6.88,16.2 6.88,16.2 C6.88,16.2 11.13,17.1 11.13,17.1 C11.13,17.1 11.13,6.5 11.13,6.5 C11.13,6.35 11.17,6.23 11.26,6.14 C11.35,6.05 11.48,6 11.63,6 C11.78,6 11.9,6.05 11.99,6.14 C12.08,6.23 12.13,6.35 12.13,6.5 C12.13,6.5 12.13,12.5 12.13,12.5 C12.13,12.5 13.88,12.5 13.88,12.5 C13.88,12.5 18.02,14.55 18.02,14.55 C18.02,14.55 17.08,20 17.08,20c " + android:valueTo="M19.81 13.64 C19.62,13.23 19.33,12.93 18.93,12.75 C18.93,12.75 15.23,10.95 15.23,10.95 C15.16,10.9 15.09,10.86 15.01,10.84 C14.99,10.83 14.96,10.83 14.94,10.83 C14.88,10.81 14.83,10.8 14.78,10.8 C14.78,10.8 14.13,10.8 14.13,10.8 C14.13,10.8 14.13,8.9 14.13,8.9 C14.13,8.9 14.13,8 14.13,8 C14.13,7.3 13.88,6.71 13.4,6.23 C12.92,5.74 12.33,5.5 11.63,5.5 C10.93,5.5 10.33,5.74 9.85,6.23 C9.37,6.71 9.13,7.3 9.13,8 C9.13,8 9.13,8.95 9.13,8.95 C9.13,8.95 9.13,11.4 9.13,11.4 C9.13,11.4 9.13,14.65 9.13,14.65 C9.13,14.65 7.18,14.2 7.18,14.2 C6.86,14.12 6.56,14.14 6.26,14.26 C5.97,14.39 5.71,14.57 5.48,14.8 C5.48,14.8 4.08,16.25 4.08,16.25 C4.08,16.25 9.23,21.4 9.23,21.4 C9.41,21.58 9.63,21.73 9.88,21.84 C10.13,21.95 10.39,22 10.68,22 C10.68,22 17.08,22 17.08,22 C17.56,22 17.99,21.85 18.38,21.54 C18.76,21.23 18.99,20.83 19.08,20.35 C19.08,20.35 19.98,14.9 19.98,14.9 C20.06,14.47 20,14.05 19.81,13.64c M17.08 20 C17.08,20 10.68,20 10.68,20 C10.68,20 6.88,16.2 6.88,16.2 C6.88,16.2 11.13,17.1 11.13,17.1 C11.13,17.1 11.13,8 11.13,8 C11.13,7.85 11.17,7.73 11.26,7.64 C11.35,7.55 11.48,7.5 11.63,7.5 C11.78,7.5 11.9,7.55 11.99,7.64 C12.08,7.73 12.13,7.85 12.13,8 C12.13,8 12.13,12.5 12.13,12.5 C12.13,12.5 13.88,12.5 13.88,12.5 C13.88,12.5 18.02,14.55 18.02,14.55 C18.02,14.55 17.08,20 17.08,20c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.2,0 0,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="pathData" android:duration="83" + android:startOffset="750" + android:valueFrom="M19.81 13.64 C19.62,13.23 19.33,12.93 18.93,12.75 C18.93,12.75 15.23,10.95 15.23,10.95 C15.16,10.9 15.09,10.86 15.01,10.84 C14.99,10.83 14.96,10.83 14.94,10.83 C14.88,10.81 14.83,10.8 14.78,10.8 C14.78,10.8 14.13,10.8 14.13,10.8 C14.13,10.8 14.13,8.9 14.13,8.9 C14.13,8.9 14.13,8 14.13,8 C14.13,7.3 13.88,6.71 13.4,6.23 C12.92,5.74 12.33,5.5 11.63,5.5 C10.93,5.5 10.33,5.74 9.85,6.23 C9.37,6.71 9.13,7.3 9.13,8 C9.13,8 9.13,8.95 9.13,8.95 C9.13,8.95 9.13,11.4 9.13,11.4 C9.13,11.4 9.13,14.65 9.13,14.65 C9.13,14.65 7.18,14.2 7.18,14.2 C6.86,14.12 6.56,14.14 6.26,14.26 C5.97,14.39 5.71,14.57 5.48,14.8 C5.48,14.8 4.08,16.25 4.08,16.25 C4.08,16.25 9.23,21.4 9.23,21.4 C9.41,21.58 9.63,21.73 9.88,21.84 C10.13,21.95 10.39,22 10.68,22 C10.68,22 17.08,22 17.08,22 C17.56,22 17.99,21.85 18.38,21.54 C18.76,21.23 18.99,20.83 19.08,20.35 C19.08,20.35 19.98,14.9 19.98,14.9 C20.06,14.47 20,14.05 19.81,13.64c M17.08 20 C17.08,20 10.68,20 10.68,20 C10.68,20 6.88,16.2 6.88,16.2 C6.88,16.2 11.13,17.1 11.13,17.1 C11.13,17.1 11.13,8 11.13,8 C11.13,7.85 11.17,7.73 11.26,7.64 C11.35,7.55 11.48,7.5 11.63,7.5 C11.78,7.5 11.9,7.55 11.99,7.64 C12.08,7.73 12.13,7.85 12.13,8 C12.13,8 12.13,12.5 12.13,12.5 C12.13,12.5 13.88,12.5 13.88,12.5 C13.88,12.5 18.02,14.55 18.02,14.55 C18.02,14.55 17.08,20 17.08,20c " + android:valueTo="M19.81 13.64 C19.62,13.23 19.33,12.93 18.93,12.75 C18.93,12.75 15.23,10.95 15.23,10.95 C15.16,10.9 15.09,10.86 15.01,10.84 C14.99,10.83 14.96,10.83 14.94,10.83 C14.88,10.81 14.83,10.8 14.78,10.8 C14.78,10.8 14.13,10.8 14.13,10.8 C14.13,10.8 14.13,8.9 14.13,8.9 C14.13,8.9 14.13,6.5 14.13,6.5 C14.13,5.8 13.88,5.21 13.4,4.72 C12.92,4.24 12.33,4 11.63,4 C10.93,4 10.33,4.24 9.85,4.72 C9.37,5.21 9.13,5.8 9.13,6.5 C9.13,6.5 9.13,8.95 9.13,8.95 C9.13,8.95 9.13,11.4 9.13,11.4 C9.13,11.4 9.13,14.65 9.13,14.65 C9.13,14.65 7.18,14.2 7.18,14.2 C6.86,14.12 6.56,14.14 6.26,14.26 C5.97,14.39 5.71,14.57 5.48,14.8 C5.48,14.8 4.08,16.25 4.08,16.25 C4.08,16.25 9.23,21.4 9.23,21.4 C9.41,21.58 9.63,21.73 9.88,21.84 C10.13,21.95 10.39,22 10.68,22 C10.68,22 17.08,22 17.08,22 C17.56,22 17.99,21.85 18.38,21.54 C18.76,21.23 18.99,20.83 19.08,20.35 C19.08,20.35 19.98,14.9 19.98,14.9 C20.06,14.47 20,14.05 19.81,13.64c M17.08 20 C17.08,20 10.68,20 10.68,20 C10.68,20 6.88,16.2 6.88,16.2 C6.88,16.2 11.13,17.1 11.13,17.1 C11.13,17.1 11.13,6.5 11.13,6.5 C11.13,6.35 11.17,6.23 11.26,6.14 C11.35,6.05 11.48,6 11.63,6 C11.78,6 11.9,6.05 11.99,6.14 C12.08,6.23 12.13,6.35 12.13,6.5 C12.13,6.5 12.13,12.5 12.13,12.5 C12.13,12.5 13.88,12.5 13.88,12.5 C13.88,12.5 18.02,14.55 18.02,14.55 C18.02,14.55 17.08,20 17.08,20c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.001,0 0,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="pathData" android:duration="250" + android:startOffset="833" + android:valueFrom="M19.81 13.64 C19.62,13.23 19.33,12.93 18.93,12.75 C18.93,12.75 15.23,10.95 15.23,10.95 C15.16,10.9 15.09,10.86 15.01,10.84 C14.99,10.83 14.96,10.83 14.94,10.83 C14.88,10.81 14.83,10.8 14.78,10.8 C14.78,10.8 14.13,10.8 14.13,10.8 C14.13,10.8 14.13,8.9 14.13,8.9 C14.13,8.9 14.13,6.5 14.13,6.5 C14.13,5.8 13.88,5.21 13.4,4.72 C12.92,4.24 12.33,4 11.63,4 C10.93,4 10.33,4.24 9.85,4.72 C9.37,5.21 9.13,5.8 9.13,6.5 C9.13,6.5 9.13,8.95 9.13,8.95 C9.13,8.95 9.13,11.4 9.13,11.4 C9.13,11.4 9.13,14.65 9.13,14.65 C9.13,14.65 7.18,14.2 7.18,14.2 C6.86,14.12 6.56,14.14 6.26,14.26 C5.97,14.39 5.71,14.57 5.48,14.8 C5.48,14.8 4.08,16.25 4.08,16.25 C4.08,16.25 9.23,21.4 9.23,21.4 C9.41,21.58 9.63,21.73 9.88,21.84 C10.13,21.95 10.39,22 10.68,22 C10.68,22 17.08,22 17.08,22 C17.56,22 17.99,21.85 18.38,21.54 C18.76,21.23 18.99,20.83 19.08,20.35 C19.08,20.35 19.98,14.9 19.98,14.9 C20.06,14.47 20,14.05 19.81,13.64c M17.08 20 C17.08,20 10.68,20 10.68,20 C10.68,20 6.88,16.2 6.88,16.2 C6.88,16.2 11.13,17.1 11.13,17.1 C11.13,17.1 11.13,6.5 11.13,6.5 C11.13,6.35 11.17,6.23 11.26,6.14 C11.35,6.05 11.48,6 11.63,6 C11.78,6 11.9,6.05 11.99,6.14 C12.08,6.23 12.13,6.35 12.13,6.5 C12.13,6.5 12.13,12.5 12.13,12.5 C12.13,12.5 13.88,12.5 13.88,12.5 C13.88,12.5 18.02,14.55 18.02,14.55 C18.02,14.55 17.08,20 17.08,20c " + android:valueTo="M19.81 13.64 C19.62,13.23 19.33,12.93 18.93,12.75 C18.93,12.75 15.23,10.95 15.23,10.95 C15.16,10.9 15.09,10.86 15.01,10.84 C14.99,10.83 14.96,10.83 14.94,10.83 C14.88,10.81 14.83,10.8 14.78,10.8 C14.78,10.8 14.13,10.8 14.13,10.8 C14.13,10.8 14.13,8.9 14.13,8.9 C14.13,8.9 14.13,8 14.13,8 C14.13,7.3 13.88,6.71 13.4,6.23 C12.92,5.74 12.33,5.5 11.63,5.5 C10.93,5.5 10.33,5.74 9.85,6.23 C9.37,6.71 9.13,7.3 9.13,8 C9.13,8 9.13,8.95 9.13,8.95 C9.13,8.95 9.13,11.4 9.13,11.4 C9.13,11.4 9.13,14.65 9.13,14.65 C9.13,14.65 7.18,14.2 7.18,14.2 C6.86,14.12 6.56,14.14 6.26,14.26 C5.97,14.39 5.71,14.57 5.48,14.8 C5.48,14.8 4.08,16.25 4.08,16.25 C4.08,16.25 9.23,21.4 9.23,21.4 C9.41,21.58 9.63,21.73 9.88,21.84 C10.13,21.95 10.39,22 10.68,22 C10.68,22 17.08,22 17.08,22 C17.56,22 17.99,21.85 18.38,21.54 C18.76,21.23 18.99,20.83 19.08,20.35 C19.08,20.35 19.98,14.9 19.98,14.9 C20.06,14.47 20,14.05 19.81,13.64c M17.08 20 C17.08,20 10.68,20 10.68,20 C10.68,20 6.88,16.2 6.88,16.2 C6.88,16.2 11.13,17.1 11.13,17.1 C11.13,17.1 11.13,8 11.13,8 C11.13,7.85 11.17,7.73 11.26,7.64 C11.35,7.55 11.48,7.5 11.63,7.5 C11.78,7.5 11.9,7.55 11.99,7.64 C12.08,7.73 12.13,7.85 12.13,8 C12.13,8 12.13,12.5 12.13,12.5 C12.13,12.5 13.88,12.5 13.88,12.5 C13.88,12.5 18.02,14.55 18.02,14.55 C18.02,14.55 17.08,20 17.08,20c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.2,0 0,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="pathData" android:duration="83" + android:startOffset="1083" + android:valueFrom="M19.81 13.64 C19.62,13.23 19.33,12.93 18.93,12.75 C18.93,12.75 15.23,10.95 15.23,10.95 C15.16,10.9 15.09,10.86 15.01,10.84 C14.99,10.83 14.96,10.83 14.94,10.83 C14.88,10.81 14.83,10.8 14.78,10.8 C14.78,10.8 14.13,10.8 14.13,10.8 C14.13,10.8 14.13,8.9 14.13,8.9 C14.13,8.9 14.13,8 14.13,8 C14.13,7.3 13.88,6.71 13.4,6.23 C12.92,5.74 12.33,5.5 11.63,5.5 C10.93,5.5 10.33,5.74 9.85,6.23 C9.37,6.71 9.13,7.3 9.13,8 C9.13,8 9.13,8.95 9.13,8.95 C9.13,8.95 9.13,11.4 9.13,11.4 C9.13,11.4 9.13,14.65 9.13,14.65 C9.13,14.65 7.18,14.2 7.18,14.2 C6.86,14.12 6.56,14.14 6.26,14.26 C5.97,14.39 5.71,14.57 5.48,14.8 C5.48,14.8 4.08,16.25 4.08,16.25 C4.08,16.25 9.23,21.4 9.23,21.4 C9.41,21.58 9.63,21.73 9.88,21.84 C10.13,21.95 10.39,22 10.68,22 C10.68,22 17.08,22 17.08,22 C17.56,22 17.99,21.85 18.38,21.54 C18.76,21.23 18.99,20.83 19.08,20.35 C19.08,20.35 19.98,14.9 19.98,14.9 C20.06,14.47 20,14.05 19.81,13.64c M17.08 20 C17.08,20 10.68,20 10.68,20 C10.68,20 6.88,16.2 6.88,16.2 C6.88,16.2 11.13,17.1 11.13,17.1 C11.13,17.1 11.13,8 11.13,8 C11.13,7.85 11.17,7.73 11.26,7.64 C11.35,7.55 11.48,7.5 11.63,7.5 C11.78,7.5 11.9,7.55 11.99,7.64 C12.08,7.73 12.13,7.85 12.13,8 C12.13,8 12.13,12.5 12.13,12.5 C12.13,12.5 13.88,12.5 13.88,12.5 C13.88,12.5 18.02,14.55 18.02,14.55 C18.02,14.55 17.08,20 17.08,20c " + android:valueTo="M19.81 13.64 C19.62,13.23 19.33,12.93 18.93,12.75 C18.93,12.75 15.23,10.95 15.23,10.95 C15.16,10.9 15.09,10.86 15.01,10.84 C14.99,10.83 14.96,10.83 14.94,10.83 C14.88,10.81 14.83,10.8 14.78,10.8 C14.78,10.8 14.13,10.8 14.13,10.8 C14.13,10.8 14.13,8.9 14.13,8.9 C14.13,8.9 14.13,6.5 14.13,6.5 C14.13,5.8 13.88,5.21 13.4,4.72 C12.92,4.24 12.33,4 11.63,4 C10.93,4 10.33,4.24 9.85,4.72 C9.37,5.21 9.13,5.8 9.13,6.5 C9.13,6.5 9.13,8.95 9.13,8.95 C9.13,8.95 9.13,11.4 9.13,11.4 C9.13,11.4 9.13,14.65 9.13,14.65 C9.13,14.65 7.18,14.2 7.18,14.2 C6.86,14.12 6.56,14.14 6.26,14.26 C5.97,14.39 5.71,14.57 5.48,14.8 C5.48,14.8 4.08,16.25 4.08,16.25 C4.08,16.25 9.23,21.4 9.23,21.4 C9.41,21.58 9.63,21.73 9.88,21.84 C10.13,21.95 10.39,22 10.68,22 C10.68,22 17.08,22 17.08,22 C17.56,22 17.99,21.85 18.38,21.54 C18.76,21.23 18.99,20.83 19.08,20.35 C19.08,20.35 19.98,14.9 19.98,14.9 C20.06,14.47 20,14.05 19.81,13.64c M17.08 20 C17.08,20 10.68,20 10.68,20 C10.68,20 6.88,16.2 6.88,16.2 C6.88,16.2 11.13,17.1 11.13,17.1 C11.13,17.1 11.13,6.5 11.13,6.5 C11.13,6.35 11.17,6.23 11.26,6.14 C11.35,6.05 11.48,6 11.63,6 C11.78,6 11.9,6.05 11.99,6.14 C12.08,6.23 12.13,6.35 12.13,6.5 C12.13,6.5 12.13,12.5 12.13,12.5 C12.13,12.5 13.88,12.5 13.88,12.5 C13.88,12.5 18.02,14.55 18.02,14.55 C18.02,14.55 17.08,20 17.08,20c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.001,0 0,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="pathData" android:duration="833" + android:startOffset="1167" + android:valueFrom="M19.81 13.64 C19.62,13.23 19.33,12.93 18.93,12.75 C18.93,12.75 15.23,10.95 15.23,10.95 C15.16,10.9 15.09,10.86 15.01,10.84 C14.99,10.83 14.96,10.83 14.94,10.83 C14.88,10.81 14.83,10.8 14.78,10.8 C14.78,10.8 14.13,10.8 14.13,10.8 C14.13,10.8 14.13,8.9 14.13,8.9 C14.13,8.9 14.13,6.5 14.13,6.5 C14.13,5.8 13.88,5.21 13.4,4.72 C12.92,4.24 12.33,4 11.63,4 C10.93,4 10.33,4.24 9.85,4.72 C9.37,5.21 9.13,5.8 9.13,6.5 C9.13,6.5 9.13,8.95 9.13,8.95 C9.13,8.95 9.13,11.4 9.13,11.4 C9.13,11.4 9.13,14.65 9.13,14.65 C9.13,14.65 7.18,14.2 7.18,14.2 C6.86,14.12 6.56,14.14 6.26,14.26 C5.97,14.39 5.71,14.57 5.48,14.8 C5.48,14.8 4.08,16.25 4.08,16.25 C4.08,16.25 9.23,21.4 9.23,21.4 C9.41,21.58 9.63,21.73 9.88,21.84 C10.13,21.95 10.39,22 10.68,22 C10.68,22 17.08,22 17.08,22 C17.56,22 17.99,21.85 18.38,21.54 C18.76,21.23 18.99,20.83 19.08,20.35 C19.08,20.35 19.98,14.9 19.98,14.9 C20.06,14.47 20,14.05 19.81,13.64c M17.08 20 C17.08,20 10.68,20 10.68,20 C10.68,20 6.88,16.2 6.88,16.2 C6.88,16.2 11.13,17.1 11.13,17.1 C11.13,17.1 11.13,6.5 11.13,6.5 C11.13,6.35 11.17,6.23 11.26,6.14 C11.35,6.05 11.48,6 11.63,6 C11.78,6 11.9,6.05 11.99,6.14 C12.08,6.23 12.13,6.35 12.13,6.5 C12.13,6.5 12.13,12.5 12.13,12.5 C12.13,12.5 13.88,12.5 13.88,12.5 C13.88,12.5 18.02,14.55 18.02,14.55 C18.02,14.55 17.08,20 17.08,20c " + android:valueTo="M19.81 13.64 C19.62,13.23 19.33,12.93 18.93,12.75 C18.93,12.75 15.23,10.95 15.23,10.95 C15.16,10.9 15.09,10.86 15.01,10.84 C14.99,10.83 14.96,10.83 14.94,10.83 C14.88,10.81 14.83,10.8 14.78,10.8 C14.78,10.8 14.13,10.8 14.13,10.8 C14.13,10.8 14.13,8.9 14.13,8.9 C14.13,8.9 14.13,6.5 14.13,6.5 C14.13,5.8 13.88,5.21 13.4,4.72 C12.92,4.24 12.33,4 11.63,4 C10.93,4 10.33,4.24 9.85,4.72 C9.37,5.21 9.13,5.8 9.13,6.5 C9.13,6.5 9.13,8.95 9.13,8.95 C9.13,8.95 9.13,11.4 9.13,11.4 C9.13,11.4 9.13,14.65 9.13,14.65 C9.13,14.65 7.18,14.2 7.18,14.2 C6.86,14.12 6.56,14.14 6.26,14.26 C5.97,14.39 5.71,14.57 5.48,14.8 C5.48,14.8 4.08,16.25 4.08,16.25 C4.08,16.25 9.23,21.4 9.23,21.4 C9.41,21.58 9.63,21.73 9.88,21.84 C10.13,21.95 10.39,22 10.68,22 C10.68,22 17.08,22 17.08,22 C17.56,22 17.99,21.85 18.38,21.54 C18.76,21.23 18.99,20.83 19.08,20.35 C19.08,20.35 19.98,14.9 19.98,14.9 C20.06,14.47 20,14.05 19.81,13.64c M17.08 20 C17.08,20 10.68,20 10.68,20 C10.68,20 6.88,16.2 6.88,16.2 C6.88,16.2 11.13,17.1 11.13,17.1 C11.13,17.1 11.13,6.5 11.13,6.5 C11.13,6.35 11.17,6.23 11.26,6.14 C11.35,6.05 11.48,6 11.63,6 C11.78,6 11.9,6.05 11.99,6.14 C12.08,6.23 12.13,6.35 12.13,6.5 C12.13,6.5 12.13,12.5 12.13,12.5 C12.13,12.5 13.88,12.5 13.88,12.5 C13.88,12.5 18.02,14.55 18.02,14.55 C18.02,14.55 17.08,20 17.08,20c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.833,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="pathData" android:duration="250" + android:startOffset="2000" + android:valueFrom="M19.81 13.64 C19.62,13.23 19.33,12.93 18.93,12.75 C18.93,12.75 15.23,10.95 15.23,10.95 C15.16,10.9 15.09,10.86 15.01,10.84 C14.99,10.83 14.96,10.83 14.94,10.83 C14.88,10.81 14.83,10.8 14.78,10.8 C14.78,10.8 14.13,10.8 14.13,10.8 C14.13,10.8 14.13,8.9 14.13,8.9 C14.13,8.9 14.13,6.5 14.13,6.5 C14.13,5.8 13.88,5.21 13.4,4.72 C12.92,4.24 12.33,4 11.63,4 C10.93,4 10.33,4.24 9.85,4.72 C9.37,5.21 9.13,5.8 9.13,6.5 C9.13,6.5 9.13,8.95 9.13,8.95 C9.13,8.95 9.13,11.4 9.13,11.4 C9.13,11.4 9.13,14.65 9.13,14.65 C9.13,14.65 7.18,14.2 7.18,14.2 C6.86,14.12 6.56,14.14 6.26,14.26 C5.97,14.39 5.71,14.57 5.48,14.8 C5.48,14.8 4.08,16.25 4.08,16.25 C4.08,16.25 9.23,21.4 9.23,21.4 C9.41,21.58 9.63,21.73 9.88,21.84 C10.13,21.95 10.39,22 10.68,22 C10.68,22 17.08,22 17.08,22 C17.56,22 17.99,21.85 18.38,21.54 C18.76,21.23 18.99,20.83 19.08,20.35 C19.08,20.35 19.98,14.9 19.98,14.9 C20.06,14.47 20,14.05 19.81,13.64c M17.08 20 C17.08,20 10.68,20 10.68,20 C10.68,20 6.88,16.2 6.88,16.2 C6.88,16.2 11.13,17.1 11.13,17.1 C11.13,17.1 11.13,6.5 11.13,6.5 C11.13,6.35 11.17,6.23 11.26,6.14 C11.35,6.05 11.48,6 11.63,6 C11.78,6 11.9,6.05 11.99,6.14 C12.08,6.23 12.13,6.35 12.13,6.5 C12.13,6.5 12.13,12.5 12.13,12.5 C12.13,12.5 13.88,12.5 13.88,12.5 C13.88,12.5 18.02,14.55 18.02,14.55 C18.02,14.55 17.08,20 17.08,20c " + android:valueTo="M19.81 13.64 C19.62,13.23 19.33,12.93 18.93,12.75 C18.93,12.75 15.23,10.95 15.23,10.95 C15.16,10.9 15.09,10.86 15.01,10.84 C14.99,10.83 14.96,10.83 14.94,10.83 C14.88,10.81 14.83,10.8 14.78,10.8 C14.78,10.8 14.13,10.8 14.13,10.8 C14.13,10.8 14.13,8.9 14.13,8.9 C14.13,8.9 14.13,8 14.13,8 C14.13,7.3 13.88,6.71 13.4,6.23 C12.92,5.74 12.33,5.5 11.63,5.5 C10.93,5.5 10.33,5.74 9.85,6.23 C9.37,6.71 9.13,7.3 9.13,8 C9.13,8 9.13,8.95 9.13,8.95 C9.13,8.95 9.13,11.4 9.13,11.4 C9.13,11.4 9.13,14.65 9.13,14.65 C9.13,14.65 7.18,14.2 7.18,14.2 C6.86,14.12 6.56,14.14 6.26,14.26 C5.97,14.39 5.71,14.57 5.48,14.8 C5.48,14.8 4.08,16.25 4.08,16.25 C4.08,16.25 9.23,21.4 9.23,21.4 C9.41,21.58 9.63,21.73 9.88,21.84 C10.13,21.95 10.39,22 10.68,22 C10.68,22 17.08,22 17.08,22 C17.56,22 17.99,21.85 18.38,21.54 C18.76,21.23 18.99,20.83 19.08,20.35 C19.08,20.35 19.98,14.9 19.98,14.9 C20.06,14.47 20,14.05 19.81,13.64c M17.08 20 C17.08,20 10.68,20 10.68,20 C10.68,20 6.88,16.2 6.88,16.2 C6.88,16.2 11.13,17.1 11.13,17.1 C11.13,17.1 11.13,8 11.13,8 C11.13,7.85 11.17,7.73 11.26,7.64 C11.35,7.55 11.48,7.5 11.63,7.5 C11.78,7.5 11.9,7.55 11.99,7.64 C12.08,7.73 12.13,7.85 12.13,8 C12.13,8 12.13,12.5 12.13,12.5 C12.13,12.5 13.88,12.5 13.88,12.5 C13.88,12.5 18.02,14.55 18.02,14.55 C18.02,14.55 17.08,20 17.08,20c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.2,0 0,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="pathData" android:duration="83" + android:startOffset="2250" + android:valueFrom="M19.81 13.64 C19.62,13.23 19.33,12.93 18.93,12.75 C18.93,12.75 15.23,10.95 15.23,10.95 C15.16,10.9 15.09,10.86 15.01,10.84 C14.99,10.83 14.96,10.83 14.94,10.83 C14.88,10.81 14.83,10.8 14.78,10.8 C14.78,10.8 14.13,10.8 14.13,10.8 C14.13,10.8 14.13,8.9 14.13,8.9 C14.13,8.9 14.13,8 14.13,8 C14.13,7.3 13.88,6.71 13.4,6.23 C12.92,5.74 12.33,5.5 11.63,5.5 C10.93,5.5 10.33,5.74 9.85,6.23 C9.37,6.71 9.13,7.3 9.13,8 C9.13,8 9.13,8.95 9.13,8.95 C9.13,8.95 9.13,11.4 9.13,11.4 C9.13,11.4 9.13,14.65 9.13,14.65 C9.13,14.65 7.18,14.2 7.18,14.2 C6.86,14.12 6.56,14.14 6.26,14.26 C5.97,14.39 5.71,14.57 5.48,14.8 C5.48,14.8 4.08,16.25 4.08,16.25 C4.08,16.25 9.23,21.4 9.23,21.4 C9.41,21.58 9.63,21.73 9.88,21.84 C10.13,21.95 10.39,22 10.68,22 C10.68,22 17.08,22 17.08,22 C17.56,22 17.99,21.85 18.38,21.54 C18.76,21.23 18.99,20.83 19.08,20.35 C19.08,20.35 19.98,14.9 19.98,14.9 C20.06,14.47 20,14.05 19.81,13.64c M17.08 20 C17.08,20 10.68,20 10.68,20 C10.68,20 6.88,16.2 6.88,16.2 C6.88,16.2 11.13,17.1 11.13,17.1 C11.13,17.1 11.13,8 11.13,8 C11.13,7.85 11.17,7.73 11.26,7.64 C11.35,7.55 11.48,7.5 11.63,7.5 C11.78,7.5 11.9,7.55 11.99,7.64 C12.08,7.73 12.13,7.85 12.13,8 C12.13,8 12.13,12.5 12.13,12.5 C12.13,12.5 13.88,12.5 13.88,12.5 C13.88,12.5 18.02,14.55 18.02,14.55 C18.02,14.55 17.08,20 17.08,20c " + android:valueTo="M19.81 13.64 C19.62,13.23 19.33,12.93 18.93,12.75 C18.93,12.75 15.23,10.95 15.23,10.95 C15.16,10.9 15.09,10.86 15.01,10.84 C14.99,10.83 14.96,10.83 14.94,10.83 C14.88,10.81 14.83,10.8 14.78,10.8 C14.78,10.8 14.13,10.8 14.13,10.8 C14.13,10.8 14.13,8.9 14.13,8.9 C14.13,8.9 14.13,6.5 14.13,6.5 C14.13,5.8 13.88,5.21 13.4,4.72 C12.92,4.24 12.33,4 11.63,4 C10.93,4 10.33,4.24 9.85,4.72 C9.37,5.21 9.13,5.8 9.13,6.5 C9.13,6.5 9.13,8.95 9.13,8.95 C9.13,8.95 9.13,11.4 9.13,11.4 C9.13,11.4 9.13,14.65 9.13,14.65 C9.13,14.65 7.18,14.2 7.18,14.2 C6.86,14.12 6.56,14.14 6.26,14.26 C5.97,14.39 5.71,14.57 5.48,14.8 C5.48,14.8 4.08,16.25 4.08,16.25 C4.08,16.25 9.23,21.4 9.23,21.4 C9.41,21.58 9.63,21.73 9.88,21.84 C10.13,21.95 10.39,22 10.68,22 C10.68,22 17.08,22 17.08,22 C17.56,22 17.99,21.85 18.38,21.54 C18.76,21.23 18.99,20.83 19.08,20.35 C19.08,20.35 19.98,14.9 19.98,14.9 C20.06,14.47 20,14.05 19.81,13.64c M17.08 20 C17.08,20 10.68,20 10.68,20 C10.68,20 6.88,16.2 6.88,16.2 C6.88,16.2 11.13,17.1 11.13,17.1 C11.13,17.1 11.13,6.5 11.13,6.5 C11.13,6.35 11.17,6.23 11.26,6.14 C11.35,6.05 11.48,6 11.63,6 C11.78,6 11.9,6.05 11.99,6.14 C12.08,6.23 12.13,6.35 12.13,6.5 C12.13,6.5 12.13,12.5 12.13,12.5 C12.13,12.5 13.88,12.5 13.88,12.5 C13.88,12.5 18.02,14.55 18.02,14.55 C18.02,14.55 17.08,20 17.08,20c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.001,0 0,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="pathData" android:duration="250" + android:startOffset="2333" + android:valueFrom="M19.81 13.64 C19.62,13.23 19.33,12.93 18.93,12.75 C18.93,12.75 15.23,10.95 15.23,10.95 C15.16,10.9 15.09,10.86 15.01,10.84 C14.99,10.83 14.96,10.83 14.94,10.83 C14.88,10.81 14.83,10.8 14.78,10.8 C14.78,10.8 14.13,10.8 14.13,10.8 C14.13,10.8 14.13,8.9 14.13,8.9 C14.13,8.9 14.13,6.5 14.13,6.5 C14.13,5.8 13.88,5.21 13.4,4.72 C12.92,4.24 12.33,4 11.63,4 C10.93,4 10.33,4.24 9.85,4.72 C9.37,5.21 9.13,5.8 9.13,6.5 C9.13,6.5 9.13,8.95 9.13,8.95 C9.13,8.95 9.13,11.4 9.13,11.4 C9.13,11.4 9.13,14.65 9.13,14.65 C9.13,14.65 7.18,14.2 7.18,14.2 C6.86,14.12 6.56,14.14 6.26,14.26 C5.97,14.39 5.71,14.57 5.48,14.8 C5.48,14.8 4.08,16.25 4.08,16.25 C4.08,16.25 9.23,21.4 9.23,21.4 C9.41,21.58 9.63,21.73 9.88,21.84 C10.13,21.95 10.39,22 10.68,22 C10.68,22 17.08,22 17.08,22 C17.56,22 17.99,21.85 18.38,21.54 C18.76,21.23 18.99,20.83 19.08,20.35 C19.08,20.35 19.98,14.9 19.98,14.9 C20.06,14.47 20,14.05 19.81,13.64c M17.08 20 C17.08,20 10.68,20 10.68,20 C10.68,20 6.88,16.2 6.88,16.2 C6.88,16.2 11.13,17.1 11.13,17.1 C11.13,17.1 11.13,6.5 11.13,6.5 C11.13,6.35 11.17,6.23 11.26,6.14 C11.35,6.05 11.48,6 11.63,6 C11.78,6 11.9,6.05 11.99,6.14 C12.08,6.23 12.13,6.35 12.13,6.5 C12.13,6.5 12.13,12.5 12.13,12.5 C12.13,12.5 13.88,12.5 13.88,12.5 C13.88,12.5 18.02,14.55 18.02,14.55 C18.02,14.55 17.08,20 17.08,20c " + android:valueTo="M19.81 13.64 C19.62,13.23 19.33,12.93 18.93,12.75 C18.93,12.75 15.23,10.95 15.23,10.95 C15.16,10.9 15.09,10.86 15.01,10.84 C14.99,10.83 14.96,10.83 14.94,10.83 C14.88,10.81 14.83,10.8 14.78,10.8 C14.78,10.8 14.13,10.8 14.13,10.8 C14.13,10.8 14.13,8.9 14.13,8.9 C14.13,8.9 14.13,8 14.13,8 C14.13,7.3 13.88,6.71 13.4,6.23 C12.92,5.74 12.33,5.5 11.63,5.5 C10.93,5.5 10.33,5.74 9.85,6.23 C9.37,6.71 9.13,7.3 9.13,8 C9.13,8 9.13,8.95 9.13,8.95 C9.13,8.95 9.13,11.4 9.13,11.4 C9.13,11.4 9.13,14.65 9.13,14.65 C9.13,14.65 7.18,14.2 7.18,14.2 C6.86,14.12 6.56,14.14 6.26,14.26 C5.97,14.39 5.71,14.57 5.48,14.8 C5.48,14.8 4.08,16.25 4.08,16.25 C4.08,16.25 9.23,21.4 9.23,21.4 C9.41,21.58 9.63,21.73 9.88,21.84 C10.13,21.95 10.39,22 10.68,22 C10.68,22 17.08,22 17.08,22 C17.56,22 17.99,21.85 18.38,21.54 C18.76,21.23 18.99,20.83 19.08,20.35 C19.08,20.35 19.98,14.9 19.98,14.9 C20.06,14.47 20,14.05 19.81,13.64c M17.08 20 C17.08,20 10.68,20 10.68,20 C10.68,20 6.88,16.2 6.88,16.2 C6.88,16.2 11.13,17.1 11.13,17.1 C11.13,17.1 11.13,8 11.13,8 C11.13,7.85 11.17,7.73 11.26,7.64 C11.35,7.55 11.48,7.5 11.63,7.5 C11.78,7.5 11.9,7.55 11.99,7.64 C12.08,7.73 12.13,7.85 12.13,8 C12.13,8 12.13,12.5 12.13,12.5 C12.13,12.5 13.88,12.5 13.88,12.5 C13.88,12.5 18.02,14.55 18.02,14.55 C18.02,14.55 17.08,20 17.08,20c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.2,0 0,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="pathData" android:duration="83" + android:startOffset="2583" + android:valueFrom="M19.81 13.64 C19.62,13.23 19.33,12.93 18.93,12.75 C18.93,12.75 15.23,10.95 15.23,10.95 C15.16,10.9 15.09,10.86 15.01,10.84 C14.99,10.83 14.96,10.83 14.94,10.83 C14.88,10.81 14.83,10.8 14.78,10.8 C14.78,10.8 14.13,10.8 14.13,10.8 C14.13,10.8 14.13,8.9 14.13,8.9 C14.13,8.9 14.13,8 14.13,8 C14.13,7.3 13.88,6.71 13.4,6.23 C12.92,5.74 12.33,5.5 11.63,5.5 C10.93,5.5 10.33,5.74 9.85,6.23 C9.37,6.71 9.13,7.3 9.13,8 C9.13,8 9.13,8.95 9.13,8.95 C9.13,8.95 9.13,11.4 9.13,11.4 C9.13,11.4 9.13,14.65 9.13,14.65 C9.13,14.65 7.18,14.2 7.18,14.2 C6.86,14.12 6.56,14.14 6.26,14.26 C5.97,14.39 5.71,14.57 5.48,14.8 C5.48,14.8 4.08,16.25 4.08,16.25 C4.08,16.25 9.23,21.4 9.23,21.4 C9.41,21.58 9.63,21.73 9.88,21.84 C10.13,21.95 10.39,22 10.68,22 C10.68,22 17.08,22 17.08,22 C17.56,22 17.99,21.85 18.38,21.54 C18.76,21.23 18.99,20.83 19.08,20.35 C19.08,20.35 19.98,14.9 19.98,14.9 C20.06,14.47 20,14.05 19.81,13.64c M17.08 20 C17.08,20 10.68,20 10.68,20 C10.68,20 6.88,16.2 6.88,16.2 C6.88,16.2 11.13,17.1 11.13,17.1 C11.13,17.1 11.13,8 11.13,8 C11.13,7.85 11.17,7.73 11.26,7.64 C11.35,7.55 11.48,7.5 11.63,7.5 C11.78,7.5 11.9,7.55 11.99,7.64 C12.08,7.73 12.13,7.85 12.13,8 C12.13,8 12.13,12.5 12.13,12.5 C12.13,12.5 13.88,12.5 13.88,12.5 C13.88,12.5 18.02,14.55 18.02,14.55 C18.02,14.55 17.08,20 17.08,20c " + android:valueTo="M19.81 13.64 C19.62,13.23 19.33,12.93 18.93,12.75 C18.93,12.75 15.23,10.95 15.23,10.95 C15.16,10.9 15.09,10.86 15.01,10.84 C14.99,10.83 14.96,10.83 14.94,10.83 C14.88,10.81 14.83,10.8 14.78,10.8 C14.78,10.8 14.13,10.8 14.13,10.8 C14.13,10.8 14.13,8.9 14.13,8.9 C14.13,8.9 14.13,6.5 14.13,6.5 C14.13,5.8 13.88,5.21 13.4,4.72 C12.92,4.24 12.33,4 11.63,4 C10.93,4 10.33,4.24 9.85,4.72 C9.37,5.21 9.13,5.8 9.13,6.5 C9.13,6.5 9.13,8.95 9.13,8.95 C9.13,8.95 9.13,11.4 9.13,11.4 C9.13,11.4 9.13,14.65 9.13,14.65 C9.13,14.65 7.18,14.2 7.18,14.2 C6.86,14.12 6.56,14.14 6.26,14.26 C5.97,14.39 5.71,14.57 5.48,14.8 C5.48,14.8 4.08,16.25 4.08,16.25 C4.08,16.25 9.23,21.4 9.23,21.4 C9.41,21.58 9.63,21.73 9.88,21.84 C10.13,21.95 10.39,22 10.68,22 C10.68,22 17.08,22 17.08,22 C17.56,22 17.99,21.85 18.38,21.54 C18.76,21.23 18.99,20.83 19.08,20.35 C19.08,20.35 19.98,14.9 19.98,14.9 C20.06,14.47 20,14.05 19.81,13.64c M17.08 20 C17.08,20 10.68,20 10.68,20 C10.68,20 6.88,16.2 6.88,16.2 C6.88,16.2 11.13,17.1 11.13,17.1 C11.13,17.1 11.13,6.5 11.13,6.5 C11.13,6.35 11.17,6.23 11.26,6.14 C11.35,6.05 11.48,6 11.63,6 C11.78,6 11.9,6.05 11.99,6.14 C12.08,6.23 12.13,6.35 12.13,6.5 C12.13,6.5 12.13,12.5 12.13,12.5 C12.13,12.5 13.88,12.5 13.88,12.5 C13.88,12.5 18.02,14.55 18.02,14.55 C18.02,14.55 17.08,20 17.08,20c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.001,0 0,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="pathData" android:duration="833" + android:startOffset="2667" + android:valueFrom="M19.81 13.64 C19.62,13.23 19.33,12.93 18.93,12.75 C18.93,12.75 15.23,10.95 15.23,10.95 C15.16,10.9 15.09,10.86 15.01,10.84 C14.99,10.83 14.96,10.83 14.94,10.83 C14.88,10.81 14.83,10.8 14.78,10.8 C14.78,10.8 14.13,10.8 14.13,10.8 C14.13,10.8 14.13,8.9 14.13,8.9 C14.13,8.9 14.13,6.5 14.13,6.5 C14.13,5.8 13.88,5.21 13.4,4.72 C12.92,4.24 12.33,4 11.63,4 C10.93,4 10.33,4.24 9.85,4.72 C9.37,5.21 9.13,5.8 9.13,6.5 C9.13,6.5 9.13,8.95 9.13,8.95 C9.13,8.95 9.13,11.4 9.13,11.4 C9.13,11.4 9.13,14.65 9.13,14.65 C9.13,14.65 7.18,14.2 7.18,14.2 C6.86,14.12 6.56,14.14 6.26,14.26 C5.97,14.39 5.71,14.57 5.48,14.8 C5.48,14.8 4.08,16.25 4.08,16.25 C4.08,16.25 9.23,21.4 9.23,21.4 C9.41,21.58 9.63,21.73 9.88,21.84 C10.13,21.95 10.39,22 10.68,22 C10.68,22 17.08,22 17.08,22 C17.56,22 17.99,21.85 18.38,21.54 C18.76,21.23 18.99,20.83 19.08,20.35 C19.08,20.35 19.98,14.9 19.98,14.9 C20.06,14.47 20,14.05 19.81,13.64c M17.08 20 C17.08,20 10.68,20 10.68,20 C10.68,20 6.88,16.2 6.88,16.2 C6.88,16.2 11.13,17.1 11.13,17.1 C11.13,17.1 11.13,6.5 11.13,6.5 C11.13,6.35 11.17,6.23 11.26,6.14 C11.35,6.05 11.48,6 11.63,6 C11.78,6 11.9,6.05 11.99,6.14 C12.08,6.23 12.13,6.35 12.13,6.5 C12.13,6.5 12.13,12.5 12.13,12.5 C12.13,12.5 13.88,12.5 13.88,12.5 C13.88,12.5 18.02,14.55 18.02,14.55 C18.02,14.55 17.08,20 17.08,20c " + android:valueTo="M19.81 13.64 C19.62,13.23 19.33,12.93 18.93,12.75 C18.93,12.75 15.23,10.95 15.23,10.95 C15.16,10.9 15.09,10.86 15.01,10.84 C14.99,10.83 14.96,10.83 14.94,10.83 C14.88,10.81 14.83,10.8 14.78,10.8 C14.78,10.8 14.13,10.8 14.13,10.8 C14.13,10.8 14.13,8.9 14.13,8.9 C14.13,8.9 14.13,6.5 14.13,6.5 C14.13,5.8 13.88,5.21 13.4,4.72 C12.92,4.24 12.33,4 11.63,4 C10.93,4 10.33,4.24 9.85,4.72 C9.37,5.21 9.13,5.8 9.13,6.5 C9.13,6.5 9.13,8.95 9.13,8.95 C9.13,8.95 9.13,11.4 9.13,11.4 C9.13,11.4 9.13,14.65 9.13,14.65 C9.13,14.65 7.18,14.2 7.18,14.2 C6.86,14.12 6.56,14.14 6.26,14.26 C5.97,14.39 5.71,14.57 5.48,14.8 C5.48,14.8 4.08,16.25 4.08,16.25 C4.08,16.25 9.23,21.4 9.23,21.4 C9.41,21.58 9.63,21.73 9.88,21.84 C10.13,21.95 10.39,22 10.68,22 C10.68,22 17.08,22 17.08,22 C17.56,22 17.99,21.85 18.38,21.54 C18.76,21.23 18.99,20.83 19.08,20.35 C19.08,20.35 19.98,14.9 19.98,14.9 C20.06,14.47 20,14.05 19.81,13.64c M17.08 20 C17.08,20 10.68,20 10.68,20 C10.68,20 6.88,16.2 6.88,16.2 C6.88,16.2 11.13,17.1 11.13,17.1 C11.13,17.1 11.13,6.5 11.13,6.5 C11.13,6.35 11.17,6.23 11.26,6.14 C11.35,6.05 11.48,6 11.63,6 C11.78,6 11.9,6.05 11.99,6.14 C12.08,6.23 12.13,6.35 12.13,6.5 C12.13,6.5 12.13,12.5 12.13,12.5 C12.13,12.5 13.88,12.5 13.88,12.5 C13.88,12.5 18.02,14.55 18.02,14.55 C18.02,14.55 17.08,20 17.08,20c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.833,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="pathData" android:duration="250" + android:startOffset="3500" + android:valueFrom="M19.81 13.64 C19.62,13.23 19.33,12.93 18.93,12.75 C18.93,12.75 15.23,10.95 15.23,10.95 C15.16,10.9 15.09,10.86 15.01,10.84 C14.99,10.83 14.96,10.83 14.94,10.83 C14.88,10.81 14.83,10.8 14.78,10.8 C14.78,10.8 14.13,10.8 14.13,10.8 C14.13,10.8 14.13,8.9 14.13,8.9 C14.13,8.9 14.13,6.5 14.13,6.5 C14.13,5.8 13.88,5.21 13.4,4.72 C12.92,4.24 12.33,4 11.63,4 C10.93,4 10.33,4.24 9.85,4.72 C9.37,5.21 9.13,5.8 9.13,6.5 C9.13,6.5 9.13,8.95 9.13,8.95 C9.13,8.95 9.13,11.4 9.13,11.4 C9.13,11.4 9.13,14.65 9.13,14.65 C9.13,14.65 7.18,14.2 7.18,14.2 C6.86,14.12 6.56,14.14 6.26,14.26 C5.97,14.39 5.71,14.57 5.48,14.8 C5.48,14.8 4.08,16.25 4.08,16.25 C4.08,16.25 9.23,21.4 9.23,21.4 C9.41,21.58 9.63,21.73 9.88,21.84 C10.13,21.95 10.39,22 10.68,22 C10.68,22 17.08,22 17.08,22 C17.56,22 17.99,21.85 18.38,21.54 C18.76,21.23 18.99,20.83 19.08,20.35 C19.08,20.35 19.98,14.9 19.98,14.9 C20.06,14.47 20,14.05 19.81,13.64c M17.08 20 C17.08,20 10.68,20 10.68,20 C10.68,20 6.88,16.2 6.88,16.2 C6.88,16.2 11.13,17.1 11.13,17.1 C11.13,17.1 11.13,6.5 11.13,6.5 C11.13,6.35 11.17,6.23 11.26,6.14 C11.35,6.05 11.48,6 11.63,6 C11.78,6 11.9,6.05 11.99,6.14 C12.08,6.23 12.13,6.35 12.13,6.5 C12.13,6.5 12.13,12.5 12.13,12.5 C12.13,12.5 13.88,12.5 13.88,12.5 C13.88,12.5 18.02,14.55 18.02,14.55 C18.02,14.55 17.08,20 17.08,20c " + android:valueTo="M19.81 13.64 C19.62,13.23 19.33,12.93 18.93,12.75 C18.93,12.75 15.23,10.95 15.23,10.95 C15.16,10.9 15.09,10.86 15.01,10.84 C14.99,10.83 14.96,10.83 14.94,10.83 C14.88,10.81 14.83,10.8 14.78,10.8 C14.78,10.8 14.13,10.8 14.13,10.8 C14.13,10.8 14.13,8.9 14.13,8.9 C14.13,8.9 14.13,8 14.13,8 C14.13,7.3 13.88,6.71 13.4,6.23 C12.92,5.74 12.33,5.5 11.63,5.5 C10.93,5.5 10.33,5.74 9.85,6.23 C9.37,6.71 9.13,7.3 9.13,8 C9.13,8 9.13,8.95 9.13,8.95 C9.13,8.95 9.13,11.4 9.13,11.4 C9.13,11.4 9.13,14.65 9.13,14.65 C9.13,14.65 7.18,14.2 7.18,14.2 C6.86,14.12 6.56,14.14 6.26,14.26 C5.97,14.39 5.71,14.57 5.48,14.8 C5.48,14.8 4.08,16.25 4.08,16.25 C4.08,16.25 9.23,21.4 9.23,21.4 C9.41,21.58 9.63,21.73 9.88,21.84 C10.13,21.95 10.39,22 10.68,22 C10.68,22 17.08,22 17.08,22 C17.56,22 17.99,21.85 18.38,21.54 C18.76,21.23 18.99,20.83 19.08,20.35 C19.08,20.35 19.98,14.9 19.98,14.9 C20.06,14.47 20,14.05 19.81,13.64c M17.08 20 C17.08,20 10.68,20 10.68,20 C10.68,20 6.88,16.2 6.88,16.2 C6.88,16.2 11.13,17.1 11.13,17.1 C11.13,17.1 11.13,8 11.13,8 C11.13,7.85 11.17,7.73 11.26,7.64 C11.35,7.55 11.48,7.5 11.63,7.5 C11.78,7.5 11.9,7.55 11.99,7.64 C12.08,7.73 12.13,7.85 12.13,8 C12.13,8 12.13,12.5 12.13,12.5 C12.13,12.5 13.88,12.5 13.88,12.5 C13.88,12.5 18.02,14.55 18.02,14.55 C18.02,14.55 17.08,20 17.08,20c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.2,0 0,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="pathData" android:duration="83" + android:startOffset="3750" + android:valueFrom="M19.81 13.64 C19.62,13.23 19.33,12.93 18.93,12.75 C18.93,12.75 15.23,10.95 15.23,10.95 C15.16,10.9 15.09,10.86 15.01,10.84 C14.99,10.83 14.96,10.83 14.94,10.83 C14.88,10.81 14.83,10.8 14.78,10.8 C14.78,10.8 14.13,10.8 14.13,10.8 C14.13,10.8 14.13,8.9 14.13,8.9 C14.13,8.9 14.13,8 14.13,8 C14.13,7.3 13.88,6.71 13.4,6.23 C12.92,5.74 12.33,5.5 11.63,5.5 C10.93,5.5 10.33,5.74 9.85,6.23 C9.37,6.71 9.13,7.3 9.13,8 C9.13,8 9.13,8.95 9.13,8.95 C9.13,8.95 9.13,11.4 9.13,11.4 C9.13,11.4 9.13,14.65 9.13,14.65 C9.13,14.65 7.18,14.2 7.18,14.2 C6.86,14.12 6.56,14.14 6.26,14.26 C5.97,14.39 5.71,14.57 5.48,14.8 C5.48,14.8 4.08,16.25 4.08,16.25 C4.08,16.25 9.23,21.4 9.23,21.4 C9.41,21.58 9.63,21.73 9.88,21.84 C10.13,21.95 10.39,22 10.68,22 C10.68,22 17.08,22 17.08,22 C17.56,22 17.99,21.85 18.38,21.54 C18.76,21.23 18.99,20.83 19.08,20.35 C19.08,20.35 19.98,14.9 19.98,14.9 C20.06,14.47 20,14.05 19.81,13.64c M17.08 20 C17.08,20 10.68,20 10.68,20 C10.68,20 6.88,16.2 6.88,16.2 C6.88,16.2 11.13,17.1 11.13,17.1 C11.13,17.1 11.13,8 11.13,8 C11.13,7.85 11.17,7.73 11.26,7.64 C11.35,7.55 11.48,7.5 11.63,7.5 C11.78,7.5 11.9,7.55 11.99,7.64 C12.08,7.73 12.13,7.85 12.13,8 C12.13,8 12.13,12.5 12.13,12.5 C12.13,12.5 13.88,12.5 13.88,12.5 C13.88,12.5 18.02,14.55 18.02,14.55 C18.02,14.55 17.08,20 17.08,20c " + android:valueTo="M19.81 13.64 C19.62,13.23 19.33,12.93 18.93,12.75 C18.93,12.75 15.23,10.95 15.23,10.95 C15.16,10.9 15.09,10.86 15.01,10.84 C14.99,10.83 14.96,10.83 14.94,10.83 C14.88,10.81 14.83,10.8 14.78,10.8 C14.78,10.8 14.13,10.8 14.13,10.8 C14.13,10.8 14.13,8.9 14.13,8.9 C14.13,8.9 14.13,6.5 14.13,6.5 C14.13,5.8 13.88,5.21 13.4,4.72 C12.92,4.24 12.33,4 11.63,4 C10.93,4 10.33,4.24 9.85,4.72 C9.37,5.21 9.13,5.8 9.13,6.5 C9.13,6.5 9.13,8.95 9.13,8.95 C9.13,8.95 9.13,11.4 9.13,11.4 C9.13,11.4 9.13,14.65 9.13,14.65 C9.13,14.65 7.18,14.2 7.18,14.2 C6.86,14.12 6.56,14.14 6.26,14.26 C5.97,14.39 5.71,14.57 5.48,14.8 C5.48,14.8 4.08,16.25 4.08,16.25 C4.08,16.25 9.23,21.4 9.23,21.4 C9.41,21.58 9.63,21.73 9.88,21.84 C10.13,21.95 10.39,22 10.68,22 C10.68,22 17.08,22 17.08,22 C17.56,22 17.99,21.85 18.38,21.54 C18.76,21.23 18.99,20.83 19.08,20.35 C19.08,20.35 19.98,14.9 19.98,14.9 C20.06,14.47 20,14.05 19.81,13.64c M17.08 20 C17.08,20 10.68,20 10.68,20 C10.68,20 6.88,16.2 6.88,16.2 C6.88,16.2 11.13,17.1 11.13,17.1 C11.13,17.1 11.13,6.5 11.13,6.5 C11.13,6.35 11.17,6.23 11.26,6.14 C11.35,6.05 11.48,6 11.63,6 C11.78,6 11.9,6.05 11.99,6.14 C12.08,6.23 12.13,6.35 12.13,6.5 C12.13,6.5 12.13,12.5 12.13,12.5 C12.13,12.5 13.88,12.5 13.88,12.5 C13.88,12.5 18.02,14.55 18.02,14.55 C18.02,14.55 17.08,20 17.08,20c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.001,0 0,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="pathData" android:duration="250" + android:startOffset="3833" + android:valueFrom="M19.81 13.64 C19.62,13.23 19.33,12.93 18.93,12.75 C18.93,12.75 15.23,10.95 15.23,10.95 C15.16,10.9 15.09,10.86 15.01,10.84 C14.99,10.83 14.96,10.83 14.94,10.83 C14.88,10.81 14.83,10.8 14.78,10.8 C14.78,10.8 14.13,10.8 14.13,10.8 C14.13,10.8 14.13,8.9 14.13,8.9 C14.13,8.9 14.13,6.5 14.13,6.5 C14.13,5.8 13.88,5.21 13.4,4.72 C12.92,4.24 12.33,4 11.63,4 C10.93,4 10.33,4.24 9.85,4.72 C9.37,5.21 9.13,5.8 9.13,6.5 C9.13,6.5 9.13,8.95 9.13,8.95 C9.13,8.95 9.13,11.4 9.13,11.4 C9.13,11.4 9.13,14.65 9.13,14.65 C9.13,14.65 7.18,14.2 7.18,14.2 C6.86,14.12 6.56,14.14 6.26,14.26 C5.97,14.39 5.71,14.57 5.48,14.8 C5.48,14.8 4.08,16.25 4.08,16.25 C4.08,16.25 9.23,21.4 9.23,21.4 C9.41,21.58 9.63,21.73 9.88,21.84 C10.13,21.95 10.39,22 10.68,22 C10.68,22 17.08,22 17.08,22 C17.56,22 17.99,21.85 18.38,21.54 C18.76,21.23 18.99,20.83 19.08,20.35 C19.08,20.35 19.98,14.9 19.98,14.9 C20.06,14.47 20,14.05 19.81,13.64c M17.08 20 C17.08,20 10.68,20 10.68,20 C10.68,20 6.88,16.2 6.88,16.2 C6.88,16.2 11.13,17.1 11.13,17.1 C11.13,17.1 11.13,6.5 11.13,6.5 C11.13,6.35 11.17,6.23 11.26,6.14 C11.35,6.05 11.48,6 11.63,6 C11.78,6 11.9,6.05 11.99,6.14 C12.08,6.23 12.13,6.35 12.13,6.5 C12.13,6.5 12.13,12.5 12.13,12.5 C12.13,12.5 13.88,12.5 13.88,12.5 C13.88,12.5 18.02,14.55 18.02,14.55 C18.02,14.55 17.08,20 17.08,20c " + android:valueTo="M19.81 13.64 C19.62,13.23 19.33,12.93 18.93,12.75 C18.93,12.75 15.23,10.95 15.23,10.95 C15.16,10.9 15.09,10.86 15.01,10.84 C14.99,10.83 14.96,10.83 14.94,10.83 C14.88,10.81 14.83,10.8 14.78,10.8 C14.78,10.8 14.13,10.8 14.13,10.8 C14.13,10.8 14.13,8.9 14.13,8.9 C14.13,8.9 14.13,8 14.13,8 C14.13,7.3 13.88,6.71 13.4,6.23 C12.92,5.74 12.33,5.5 11.63,5.5 C10.93,5.5 10.33,5.74 9.85,6.23 C9.37,6.71 9.13,7.3 9.13,8 C9.13,8 9.13,8.95 9.13,8.95 C9.13,8.95 9.13,11.4 9.13,11.4 C9.13,11.4 9.13,14.65 9.13,14.65 C9.13,14.65 7.18,14.2 7.18,14.2 C6.86,14.12 6.56,14.14 6.26,14.26 C5.97,14.39 5.71,14.57 5.48,14.8 C5.48,14.8 4.08,16.25 4.08,16.25 C4.08,16.25 9.23,21.4 9.23,21.4 C9.41,21.58 9.63,21.73 9.88,21.84 C10.13,21.95 10.39,22 10.68,22 C10.68,22 17.08,22 17.08,22 C17.56,22 17.99,21.85 18.38,21.54 C18.76,21.23 18.99,20.83 19.08,20.35 C19.08,20.35 19.98,14.9 19.98,14.9 C20.06,14.47 20,14.05 19.81,13.64c M17.08 20 C17.08,20 10.68,20 10.68,20 C10.68,20 6.88,16.2 6.88,16.2 C6.88,16.2 11.13,17.1 11.13,17.1 C11.13,17.1 11.13,8 11.13,8 C11.13,7.85 11.17,7.73 11.26,7.64 C11.35,7.55 11.48,7.5 11.63,7.5 C11.78,7.5 11.9,7.55 11.99,7.64 C12.08,7.73 12.13,7.85 12.13,8 C12.13,8 12.13,12.5 12.13,12.5 C12.13,12.5 13.88,12.5 13.88,12.5 C13.88,12.5 18.02,14.55 18.02,14.55 C18.02,14.55 17.08,20 17.08,20c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.2,0 0,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="pathData" android:duration="83" + android:startOffset="4083" + android:valueFrom="M19.81 13.64 C19.62,13.23 19.33,12.93 18.93,12.75 C18.93,12.75 15.23,10.95 15.23,10.95 C15.16,10.9 15.09,10.86 15.01,10.84 C14.99,10.83 14.96,10.83 14.94,10.83 C14.88,10.81 14.83,10.8 14.78,10.8 C14.78,10.8 14.13,10.8 14.13,10.8 C14.13,10.8 14.13,8.9 14.13,8.9 C14.13,8.9 14.13,8 14.13,8 C14.13,7.3 13.88,6.71 13.4,6.23 C12.92,5.74 12.33,5.5 11.63,5.5 C10.93,5.5 10.33,5.74 9.85,6.23 C9.37,6.71 9.13,7.3 9.13,8 C9.13,8 9.13,8.95 9.13,8.95 C9.13,8.95 9.13,11.4 9.13,11.4 C9.13,11.4 9.13,14.65 9.13,14.65 C9.13,14.65 7.18,14.2 7.18,14.2 C6.86,14.12 6.56,14.14 6.26,14.26 C5.97,14.39 5.71,14.57 5.48,14.8 C5.48,14.8 4.08,16.25 4.08,16.25 C4.08,16.25 9.23,21.4 9.23,21.4 C9.41,21.58 9.63,21.73 9.88,21.84 C10.13,21.95 10.39,22 10.68,22 C10.68,22 17.08,22 17.08,22 C17.56,22 17.99,21.85 18.38,21.54 C18.76,21.23 18.99,20.83 19.08,20.35 C19.08,20.35 19.98,14.9 19.98,14.9 C20.06,14.47 20,14.05 19.81,13.64c M17.08 20 C17.08,20 10.68,20 10.68,20 C10.68,20 6.88,16.2 6.88,16.2 C6.88,16.2 11.13,17.1 11.13,17.1 C11.13,17.1 11.13,8 11.13,8 C11.13,7.85 11.17,7.73 11.26,7.64 C11.35,7.55 11.48,7.5 11.63,7.5 C11.78,7.5 11.9,7.55 11.99,7.64 C12.08,7.73 12.13,7.85 12.13,8 C12.13,8 12.13,12.5 12.13,12.5 C12.13,12.5 13.88,12.5 13.88,12.5 C13.88,12.5 18.02,14.55 18.02,14.55 C18.02,14.55 17.08,20 17.08,20c " + android:valueTo="M19.81 13.64 C19.62,13.23 19.33,12.93 18.93,12.75 C18.93,12.75 15.23,10.95 15.23,10.95 C15.16,10.9 15.09,10.86 15.01,10.84 C14.99,10.83 14.96,10.83 14.94,10.83 C14.88,10.81 14.83,10.8 14.78,10.8 C14.78,10.8 14.13,10.8 14.13,10.8 C14.13,10.8 14.13,8.9 14.13,8.9 C14.13,8.9 14.13,6.5 14.13,6.5 C14.13,5.8 13.88,5.21 13.4,4.72 C12.92,4.24 12.33,4 11.63,4 C10.93,4 10.33,4.24 9.85,4.72 C9.37,5.21 9.13,5.8 9.13,6.5 C9.13,6.5 9.13,8.95 9.13,8.95 C9.13,8.95 9.13,11.4 9.13,11.4 C9.13,11.4 9.13,14.65 9.13,14.65 C9.13,14.65 7.18,14.2 7.18,14.2 C6.86,14.12 6.56,14.14 6.26,14.26 C5.97,14.39 5.71,14.57 5.48,14.8 C5.48,14.8 4.08,16.25 4.08,16.25 C4.08,16.25 9.23,21.4 9.23,21.4 C9.41,21.58 9.63,21.73 9.88,21.84 C10.13,21.95 10.39,22 10.68,22 C10.68,22 17.08,22 17.08,22 C17.56,22 17.99,21.85 18.38,21.54 C18.76,21.23 18.99,20.83 19.08,20.35 C19.08,20.35 19.98,14.9 19.98,14.9 C20.06,14.47 20,14.05 19.81,13.64c M17.08 20 C17.08,20 10.68,20 10.68,20 C10.68,20 6.88,16.2 6.88,16.2 C6.88,16.2 11.13,17.1 11.13,17.1 C11.13,17.1 11.13,6.5 11.13,6.5 C11.13,6.35 11.17,6.23 11.26,6.14 C11.35,6.05 11.48,6 11.63,6 C11.78,6 11.9,6.05 11.99,6.14 C12.08,6.23 12.13,6.35 12.13,6.5 C12.13,6.5 12.13,12.5 12.13,12.5 C12.13,12.5 13.88,12.5 13.88,12.5 C13.88,12.5 18.02,14.55 18.02,14.55 C18.02,14.55 17.08,20 17.08,20c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.001,0 0,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="pathData" android:duration="833" + android:startOffset="4167" + android:valueFrom="M19.81 13.64 C19.62,13.23 19.33,12.93 18.93,12.75 C18.93,12.75 15.23,10.95 15.23,10.95 C15.16,10.9 15.09,10.86 15.01,10.84 C14.99,10.83 14.96,10.83 14.94,10.83 C14.88,10.81 14.83,10.8 14.78,10.8 C14.78,10.8 14.13,10.8 14.13,10.8 C14.13,10.8 14.13,8.9 14.13,8.9 C14.13,8.9 14.13,6.5 14.13,6.5 C14.13,5.8 13.88,5.21 13.4,4.72 C12.92,4.24 12.33,4 11.63,4 C10.93,4 10.33,4.24 9.85,4.72 C9.37,5.21 9.13,5.8 9.13,6.5 C9.13,6.5 9.13,8.95 9.13,8.95 C9.13,8.95 9.13,11.4 9.13,11.4 C9.13,11.4 9.13,14.65 9.13,14.65 C9.13,14.65 7.18,14.2 7.18,14.2 C6.86,14.12 6.56,14.14 6.26,14.26 C5.97,14.39 5.71,14.57 5.48,14.8 C5.48,14.8 4.08,16.25 4.08,16.25 C4.08,16.25 9.23,21.4 9.23,21.4 C9.41,21.58 9.63,21.73 9.88,21.84 C10.13,21.95 10.39,22 10.68,22 C10.68,22 17.08,22 17.08,22 C17.56,22 17.99,21.85 18.38,21.54 C18.76,21.23 18.99,20.83 19.08,20.35 C19.08,20.35 19.98,14.9 19.98,14.9 C20.06,14.47 20,14.05 19.81,13.64c M17.08 20 C17.08,20 10.68,20 10.68,20 C10.68,20 6.88,16.2 6.88,16.2 C6.88,16.2 11.13,17.1 11.13,17.1 C11.13,17.1 11.13,6.5 11.13,6.5 C11.13,6.35 11.17,6.23 11.26,6.14 C11.35,6.05 11.48,6 11.63,6 C11.78,6 11.9,6.05 11.99,6.14 C12.08,6.23 12.13,6.35 12.13,6.5 C12.13,6.5 12.13,12.5 12.13,12.5 C12.13,12.5 13.88,12.5 13.88,12.5 C13.88,12.5 18.02,14.55 18.02,14.55 C18.02,14.55 17.08,20 17.08,20c " + android:valueTo="M19.81 13.64 C19.62,13.23 19.33,12.93 18.93,12.75 C18.93,12.75 15.23,10.95 15.23,10.95 C15.16,10.9 15.09,10.86 15.01,10.84 C14.99,10.83 14.96,10.83 14.94,10.83 C14.88,10.81 14.83,10.8 14.78,10.8 C14.78,10.8 14.13,10.8 14.13,10.8 C14.13,10.8 14.13,8.9 14.13,8.9 C14.13,8.9 14.13,6.5 14.13,6.5 C14.13,5.8 13.88,5.21 13.4,4.72 C12.92,4.24 12.33,4 11.63,4 C10.93,4 10.33,4.24 9.85,4.72 C9.37,5.21 9.13,5.8 9.13,6.5 C9.13,6.5 9.13,8.95 9.13,8.95 C9.13,8.95 9.13,11.4 9.13,11.4 C9.13,11.4 9.13,14.65 9.13,14.65 C9.13,14.65 7.18,14.2 7.18,14.2 C6.86,14.12 6.56,14.14 6.26,14.26 C5.97,14.39 5.71,14.57 5.48,14.8 C5.48,14.8 4.08,16.25 4.08,16.25 C4.08,16.25 9.23,21.4 9.23,21.4 C9.41,21.58 9.63,21.73 9.88,21.84 C10.13,21.95 10.39,22 10.68,22 C10.68,22 17.08,22 17.08,22 C17.56,22 17.99,21.85 18.38,21.54 C18.76,21.23 18.99,20.83 19.08,20.35 C19.08,20.35 19.98,14.9 19.98,14.9 C20.06,14.47 20,14.05 19.81,13.64c M17.08 20 C17.08,20 10.68,20 10.68,20 C10.68,20 6.88,16.2 6.88,16.2 C6.88,16.2 11.13,17.1 11.13,17.1 C11.13,17.1 11.13,6.5 11.13,6.5 C11.13,6.35 11.17,6.23 11.26,6.14 C11.35,6.05 11.48,6 11.63,6 C11.78,6 11.9,6.05 11.99,6.14 C12.08,6.23 12.13,6.35 12.13,6.5 C12.13,6.5 12.13,12.5 12.13,12.5 C12.13,12.5 13.88,12.5 13.88,12.5 C13.88,12.5 18.02,14.55 18.02,14.55 C18.02,14.55 17.08,20 17.08,20c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.833,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="pathData" android:duration="250" + android:startOffset="5000" + android:valueFrom="M19.81 13.64 C19.62,13.23 19.33,12.93 18.93,12.75 C18.93,12.75 15.23,10.95 15.23,10.95 C15.16,10.9 15.09,10.86 15.01,10.84 C14.99,10.83 14.96,10.83 14.94,10.83 C14.88,10.81 14.83,10.8 14.78,10.8 C14.78,10.8 14.13,10.8 14.13,10.8 C14.13,10.8 14.13,8.9 14.13,8.9 C14.13,8.9 14.13,6.5 14.13,6.5 C14.13,5.8 13.88,5.21 13.4,4.72 C12.92,4.24 12.33,4 11.63,4 C10.93,4 10.33,4.24 9.85,4.72 C9.37,5.21 9.13,5.8 9.13,6.5 C9.13,6.5 9.13,8.95 9.13,8.95 C9.13,8.95 9.13,11.4 9.13,11.4 C9.13,11.4 9.13,14.65 9.13,14.65 C9.13,14.65 7.18,14.2 7.18,14.2 C6.86,14.12 6.56,14.14 6.26,14.26 C5.97,14.39 5.71,14.57 5.48,14.8 C5.48,14.8 4.08,16.25 4.08,16.25 C4.08,16.25 9.23,21.4 9.23,21.4 C9.41,21.58 9.63,21.73 9.88,21.84 C10.13,21.95 10.39,22 10.68,22 C10.68,22 17.08,22 17.08,22 C17.56,22 17.99,21.85 18.38,21.54 C18.76,21.23 18.99,20.83 19.08,20.35 C19.08,20.35 19.98,14.9 19.98,14.9 C20.06,14.47 20,14.05 19.81,13.64c M17.08 20 C17.08,20 10.68,20 10.68,20 C10.68,20 6.88,16.2 6.88,16.2 C6.88,16.2 11.13,17.1 11.13,17.1 C11.13,17.1 11.13,6.5 11.13,6.5 C11.13,6.35 11.17,6.23 11.26,6.14 C11.35,6.05 11.48,6 11.63,6 C11.78,6 11.9,6.05 11.99,6.14 C12.08,6.23 12.13,6.35 12.13,6.5 C12.13,6.5 12.13,12.5 12.13,12.5 C12.13,12.5 13.88,12.5 13.88,12.5 C13.88,12.5 18.02,14.55 18.02,14.55 C18.02,14.55 17.08,20 17.08,20c " + android:valueTo="M19.81 13.64 C19.62,13.23 19.33,12.93 18.93,12.75 C18.93,12.75 15.23,10.95 15.23,10.95 C15.16,10.9 15.09,10.86 15.01,10.84 C14.99,10.83 14.96,10.83 14.94,10.83 C14.88,10.81 14.83,10.8 14.78,10.8 C14.78,10.8 14.13,10.8 14.13,10.8 C14.13,10.8 14.13,8.9 14.13,8.9 C14.13,8.9 14.13,8 14.13,8 C14.13,7.3 13.88,6.71 13.4,6.23 C12.92,5.74 12.33,5.5 11.63,5.5 C10.93,5.5 10.33,5.74 9.85,6.23 C9.37,6.71 9.13,7.3 9.13,8 C9.13,8 9.13,8.95 9.13,8.95 C9.13,8.95 9.13,11.4 9.13,11.4 C9.13,11.4 9.13,14.65 9.13,14.65 C9.13,14.65 7.18,14.2 7.18,14.2 C6.86,14.12 6.56,14.14 6.26,14.26 C5.97,14.39 5.71,14.57 5.48,14.8 C5.48,14.8 4.08,16.25 4.08,16.25 C4.08,16.25 9.23,21.4 9.23,21.4 C9.41,21.58 9.63,21.73 9.88,21.84 C10.13,21.95 10.39,22 10.68,22 C10.68,22 17.08,22 17.08,22 C17.56,22 17.99,21.85 18.38,21.54 C18.76,21.23 18.99,20.83 19.08,20.35 C19.08,20.35 19.98,14.9 19.98,14.9 C20.06,14.47 20,14.05 19.81,13.64c M17.08 20 C17.08,20 10.68,20 10.68,20 C10.68,20 6.88,16.2 6.88,16.2 C6.88,16.2 11.13,17.1 11.13,17.1 C11.13,17.1 11.13,8 11.13,8 C11.13,7.85 11.17,7.73 11.26,7.64 C11.35,7.55 11.48,7.5 11.63,7.5 C11.78,7.5 11.9,7.55 11.99,7.64 C12.08,7.73 12.13,7.85 12.13,8 C12.13,8 12.13,12.5 12.13,12.5 C12.13,12.5 13.88,12.5 13.88,12.5 C13.88,12.5 18.02,14.55 18.02,14.55 C18.02,14.55 17.08,20 17.08,20c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.2,0 0,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="pathData" android:duration="83" + android:startOffset="5250" + android:valueFrom="M19.81 13.64 C19.62,13.23 19.33,12.93 18.93,12.75 C18.93,12.75 15.23,10.95 15.23,10.95 C15.16,10.9 15.09,10.86 15.01,10.84 C14.99,10.83 14.96,10.83 14.94,10.83 C14.88,10.81 14.83,10.8 14.78,10.8 C14.78,10.8 14.13,10.8 14.13,10.8 C14.13,10.8 14.13,8.9 14.13,8.9 C14.13,8.9 14.13,8 14.13,8 C14.13,7.3 13.88,6.71 13.4,6.23 C12.92,5.74 12.33,5.5 11.63,5.5 C10.93,5.5 10.33,5.74 9.85,6.23 C9.37,6.71 9.13,7.3 9.13,8 C9.13,8 9.13,8.95 9.13,8.95 C9.13,8.95 9.13,11.4 9.13,11.4 C9.13,11.4 9.13,14.65 9.13,14.65 C9.13,14.65 7.18,14.2 7.18,14.2 C6.86,14.12 6.56,14.14 6.26,14.26 C5.97,14.39 5.71,14.57 5.48,14.8 C5.48,14.8 4.08,16.25 4.08,16.25 C4.08,16.25 9.23,21.4 9.23,21.4 C9.41,21.58 9.63,21.73 9.88,21.84 C10.13,21.95 10.39,22 10.68,22 C10.68,22 17.08,22 17.08,22 C17.56,22 17.99,21.85 18.38,21.54 C18.76,21.23 18.99,20.83 19.08,20.35 C19.08,20.35 19.98,14.9 19.98,14.9 C20.06,14.47 20,14.05 19.81,13.64c M17.08 20 C17.08,20 10.68,20 10.68,20 C10.68,20 6.88,16.2 6.88,16.2 C6.88,16.2 11.13,17.1 11.13,17.1 C11.13,17.1 11.13,8 11.13,8 C11.13,7.85 11.17,7.73 11.26,7.64 C11.35,7.55 11.48,7.5 11.63,7.5 C11.78,7.5 11.9,7.55 11.99,7.64 C12.08,7.73 12.13,7.85 12.13,8 C12.13,8 12.13,12.5 12.13,12.5 C12.13,12.5 13.88,12.5 13.88,12.5 C13.88,12.5 18.02,14.55 18.02,14.55 C18.02,14.55 17.08,20 17.08,20c " + android:valueTo="M19.81 13.64 C19.62,13.23 19.33,12.93 18.93,12.75 C18.93,12.75 15.23,10.95 15.23,10.95 C15.16,10.9 15.09,10.86 15.01,10.84 C14.99,10.83 14.96,10.83 14.94,10.83 C14.88,10.81 14.83,10.8 14.78,10.8 C14.78,10.8 14.13,10.8 14.13,10.8 C14.13,10.8 14.13,8.9 14.13,8.9 C14.13,8.9 14.13,6.5 14.13,6.5 C14.13,5.8 13.88,5.21 13.4,4.72 C12.92,4.24 12.33,4 11.63,4 C10.93,4 10.33,4.24 9.85,4.72 C9.37,5.21 9.13,5.8 9.13,6.5 C9.13,6.5 9.13,8.95 9.13,8.95 C9.13,8.95 9.13,11.4 9.13,11.4 C9.13,11.4 9.13,14.65 9.13,14.65 C9.13,14.65 7.18,14.2 7.18,14.2 C6.86,14.12 6.56,14.14 6.26,14.26 C5.97,14.39 5.71,14.57 5.48,14.8 C5.48,14.8 4.08,16.25 4.08,16.25 C4.08,16.25 9.23,21.4 9.23,21.4 C9.41,21.58 9.63,21.73 9.88,21.84 C10.13,21.95 10.39,22 10.68,22 C10.68,22 17.08,22 17.08,22 C17.56,22 17.99,21.85 18.38,21.54 C18.76,21.23 18.99,20.83 19.08,20.35 C19.08,20.35 19.98,14.9 19.98,14.9 C20.06,14.47 20,14.05 19.81,13.64c M17.08 20 C17.08,20 10.68,20 10.68,20 C10.68,20 6.88,16.2 6.88,16.2 C6.88,16.2 11.13,17.1 11.13,17.1 C11.13,17.1 11.13,6.5 11.13,6.5 C11.13,6.35 11.17,6.23 11.26,6.14 C11.35,6.05 11.48,6 11.63,6 C11.78,6 11.9,6.05 11.99,6.14 C12.08,6.23 12.13,6.35 12.13,6.5 C12.13,6.5 12.13,12.5 12.13,12.5 C12.13,12.5 13.88,12.5 13.88,12.5 C13.88,12.5 18.02,14.55 18.02,14.55 C18.02,14.55 17.08,20 17.08,20c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.001,0 0,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="pathData" android:duration="250" + android:startOffset="5333" + android:valueFrom="M19.81 13.64 C19.62,13.23 19.33,12.93 18.93,12.75 C18.93,12.75 15.23,10.95 15.23,10.95 C15.16,10.9 15.09,10.86 15.01,10.84 C14.99,10.83 14.96,10.83 14.94,10.83 C14.88,10.81 14.83,10.8 14.78,10.8 C14.78,10.8 14.13,10.8 14.13,10.8 C14.13,10.8 14.13,8.9 14.13,8.9 C14.13,8.9 14.13,6.5 14.13,6.5 C14.13,5.8 13.88,5.21 13.4,4.72 C12.92,4.24 12.33,4 11.63,4 C10.93,4 10.33,4.24 9.85,4.72 C9.37,5.21 9.13,5.8 9.13,6.5 C9.13,6.5 9.13,8.95 9.13,8.95 C9.13,8.95 9.13,11.4 9.13,11.4 C9.13,11.4 9.13,14.65 9.13,14.65 C9.13,14.65 7.18,14.2 7.18,14.2 C6.86,14.12 6.56,14.14 6.26,14.26 C5.97,14.39 5.71,14.57 5.48,14.8 C5.48,14.8 4.08,16.25 4.08,16.25 C4.08,16.25 9.23,21.4 9.23,21.4 C9.41,21.58 9.63,21.73 9.88,21.84 C10.13,21.95 10.39,22 10.68,22 C10.68,22 17.08,22 17.08,22 C17.56,22 17.99,21.85 18.38,21.54 C18.76,21.23 18.99,20.83 19.08,20.35 C19.08,20.35 19.98,14.9 19.98,14.9 C20.06,14.47 20,14.05 19.81,13.64c M17.08 20 C17.08,20 10.68,20 10.68,20 C10.68,20 6.88,16.2 6.88,16.2 C6.88,16.2 11.13,17.1 11.13,17.1 C11.13,17.1 11.13,6.5 11.13,6.5 C11.13,6.35 11.17,6.23 11.26,6.14 C11.35,6.05 11.48,6 11.63,6 C11.78,6 11.9,6.05 11.99,6.14 C12.08,6.23 12.13,6.35 12.13,6.5 C12.13,6.5 12.13,12.5 12.13,12.5 C12.13,12.5 13.88,12.5 13.88,12.5 C13.88,12.5 18.02,14.55 18.02,14.55 C18.02,14.55 17.08,20 17.08,20c " + android:valueTo="M19.81 13.64 C19.62,13.23 19.33,12.93 18.93,12.75 C18.93,12.75 15.23,10.95 15.23,10.95 C15.16,10.9 15.09,10.86 15.01,10.84 C14.99,10.83 14.96,10.83 14.94,10.83 C14.88,10.81 14.83,10.8 14.78,10.8 C14.78,10.8 14.13,10.8 14.13,10.8 C14.13,10.8 14.13,8.9 14.13,8.9 C14.13,8.9 14.13,8 14.13,8 C14.13,7.3 13.88,6.71 13.4,6.23 C12.92,5.74 12.33,5.5 11.63,5.5 C10.93,5.5 10.33,5.74 9.85,6.23 C9.37,6.71 9.13,7.3 9.13,8 C9.13,8 9.13,8.95 9.13,8.95 C9.13,8.95 9.13,11.4 9.13,11.4 C9.13,11.4 9.13,14.65 9.13,14.65 C9.13,14.65 7.18,14.2 7.18,14.2 C6.86,14.12 6.56,14.14 6.26,14.26 C5.97,14.39 5.71,14.57 5.48,14.8 C5.48,14.8 4.08,16.25 4.08,16.25 C4.08,16.25 9.23,21.4 9.23,21.4 C9.41,21.58 9.63,21.73 9.88,21.84 C10.13,21.95 10.39,22 10.68,22 C10.68,22 17.08,22 17.08,22 C17.56,22 17.99,21.85 18.38,21.54 C18.76,21.23 18.99,20.83 19.08,20.35 C19.08,20.35 19.98,14.9 19.98,14.9 C20.06,14.47 20,14.05 19.81,13.64c M17.08 20 C17.08,20 10.68,20 10.68,20 C10.68,20 6.88,16.2 6.88,16.2 C6.88,16.2 11.13,17.1 11.13,17.1 C11.13,17.1 11.13,8 11.13,8 C11.13,7.85 11.17,7.73 11.26,7.64 C11.35,7.55 11.48,7.5 11.63,7.5 C11.78,7.5 11.9,7.55 11.99,7.64 C12.08,7.73 12.13,7.85 12.13,8 C12.13,8 12.13,12.5 12.13,12.5 C12.13,12.5 13.88,12.5 13.88,12.5 C13.88,12.5 18.02,14.55 18.02,14.55 C18.02,14.55 17.08,20 17.08,20c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.2,0 0,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="pathData" android:duration="83" + android:startOffset="5583" + android:valueFrom="M19.81 13.64 C19.62,13.23 19.33,12.93 18.93,12.75 C18.93,12.75 15.23,10.95 15.23,10.95 C15.16,10.9 15.09,10.86 15.01,10.84 C14.99,10.83 14.96,10.83 14.94,10.83 C14.88,10.81 14.83,10.8 14.78,10.8 C14.78,10.8 14.13,10.8 14.13,10.8 C14.13,10.8 14.13,8.9 14.13,8.9 C14.13,8.9 14.13,8 14.13,8 C14.13,7.3 13.88,6.71 13.4,6.23 C12.92,5.74 12.33,5.5 11.63,5.5 C10.93,5.5 10.33,5.74 9.85,6.23 C9.37,6.71 9.13,7.3 9.13,8 C9.13,8 9.13,8.95 9.13,8.95 C9.13,8.95 9.13,11.4 9.13,11.4 C9.13,11.4 9.13,14.65 9.13,14.65 C9.13,14.65 7.18,14.2 7.18,14.2 C6.86,14.12 6.56,14.14 6.26,14.26 C5.97,14.39 5.71,14.57 5.48,14.8 C5.48,14.8 4.08,16.25 4.08,16.25 C4.08,16.25 9.23,21.4 9.23,21.4 C9.41,21.58 9.63,21.73 9.88,21.84 C10.13,21.95 10.39,22 10.68,22 C10.68,22 17.08,22 17.08,22 C17.56,22 17.99,21.85 18.38,21.54 C18.76,21.23 18.99,20.83 19.08,20.35 C19.08,20.35 19.98,14.9 19.98,14.9 C20.06,14.47 20,14.05 19.81,13.64c M17.08 20 C17.08,20 10.68,20 10.68,20 C10.68,20 6.88,16.2 6.88,16.2 C6.88,16.2 11.13,17.1 11.13,17.1 C11.13,17.1 11.13,8 11.13,8 C11.13,7.85 11.17,7.73 11.26,7.64 C11.35,7.55 11.48,7.5 11.63,7.5 C11.78,7.5 11.9,7.55 11.99,7.64 C12.08,7.73 12.13,7.85 12.13,8 C12.13,8 12.13,12.5 12.13,12.5 C12.13,12.5 13.88,12.5 13.88,12.5 C13.88,12.5 18.02,14.55 18.02,14.55 C18.02,14.55 17.08,20 17.08,20c " + android:valueTo="M19.81 13.64 C19.62,13.23 19.33,12.93 18.93,12.75 C18.93,12.75 15.23,10.95 15.23,10.95 C15.16,10.9 15.09,10.86 15.01,10.84 C14.99,10.83 14.96,10.83 14.94,10.83 C14.88,10.81 14.83,10.8 14.78,10.8 C14.78,10.8 14.13,10.8 14.13,10.8 C14.13,10.8 14.13,8.9 14.13,8.9 C14.13,8.9 14.13,6.5 14.13,6.5 C14.13,5.8 13.88,5.21 13.4,4.72 C12.92,4.24 12.33,4 11.63,4 C10.93,4 10.33,4.24 9.85,4.72 C9.37,5.21 9.13,5.8 9.13,6.5 C9.13,6.5 9.13,8.95 9.13,8.95 C9.13,8.95 9.13,11.4 9.13,11.4 C9.13,11.4 9.13,14.65 9.13,14.65 C9.13,14.65 7.18,14.2 7.18,14.2 C6.86,14.12 6.56,14.14 6.26,14.26 C5.97,14.39 5.71,14.57 5.48,14.8 C5.48,14.8 4.08,16.25 4.08,16.25 C4.08,16.25 9.23,21.4 9.23,21.4 C9.41,21.58 9.63,21.73 9.88,21.84 C10.13,21.95 10.39,22 10.68,22 C10.68,22 17.08,22 17.08,22 C17.56,22 17.99,21.85 18.38,21.54 C18.76,21.23 18.99,20.83 19.08,20.35 C19.08,20.35 19.98,14.9 19.98,14.9 C20.06,14.47 20,14.05 19.81,13.64c M17.08 20 C17.08,20 10.68,20 10.68,20 C10.68,20 6.88,16.2 6.88,16.2 C6.88,16.2 11.13,17.1 11.13,17.1 C11.13,17.1 11.13,6.5 11.13,6.5 C11.13,6.35 11.17,6.23 11.26,6.14 C11.35,6.05 11.48,6 11.63,6 C11.78,6 11.9,6.05 11.99,6.14 C12.08,6.23 12.13,6.35 12.13,6.5 C12.13,6.5 12.13,12.5 12.13,12.5 C12.13,12.5 13.88,12.5 13.88,12.5 C13.88,12.5 18.02,14.55 18.02,14.55 C18.02,14.55 17.08,20 17.08,20c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.001,0 0,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="time_group"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator android:propertyName="translateX" android:duration="6000" + android:startOffset="0" android:valueFrom="0" android:valueTo="1" + android:valueType="floatType"/> + </set> + </aapt:attr> + </target> +</animated-vector> diff --git a/libs/WindowManager/Shell/res/drawable/reachability_education_ic_right_hand.xml b/libs/WindowManager/Shell/res/drawable/reachability_education_ic_right_hand.xml new file mode 100644 index 000000000000..a807a770aa22 --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/reachability_education_ic_right_hand.xml @@ -0,0 +1,699 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2023 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<animated-vector xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:aapt="http://schemas.android.com/aapt"> + <aapt:attr name="android:drawable"> + <vector android:height="30dp" android:width="30dp" android:viewportHeight="30" + android:viewportWidth="30"> + <group android:name="_R_G"> + <group android:name="_R_G_L_0_G" android:translateX="-135" android:translateY="-135" + android:pivotX="150" android:pivotY="150" android:scaleX="0.1" + android:scaleY="0.1"> + <group android:name="_R_G_L_0_G_L_1_G" android:translateX="134.624" + android:translateY="87.514" android:pivotX="11.625" android:pivotY="6.39" + android:scaleX="10" android:scaleY="10"> + <group android:name="_R_G_L_0_G_L_1_G_D_0_P_0_G_0_T_0" + android:translateX="11.625" android:translateY="6.464" + android:scaleX="1" android:scaleY="1"> + <path android:name="_R_G_L_0_G_L_1_G_D_0_P_0" + android:fillColor="@color/letterbox_reachability_education_item_color" + android:fillAlpha="1" + android:fillType="nonZero" + android:pathData=" M-1.54 5.39 C-3.87,4.71 -5.49,2.54 -5.49,0.11 C-5.49,-2.92 -3.03,-5.38 0,-5.38 C3.03,-5.38 5.49,-2.92 5.49,0.11 C5.49,2.11 4.41,3.95 2.66,4.92 C2.66,4.92 1.69,3.17 1.69,3.17 C2.8,2.55 3.49,1.38 3.49,0.11 C3.49,-1.82 1.93,-3.38 0,-3.38 C-1.93,-3.38 -3.49,-1.82 -3.49,0.11 C-3.49,1.65 -2.46,3.03 -0.98,3.47 C-0.98,3.47 -1.54,5.39 -1.54,5.39c "/> + </group> + </group> + <group android:name="_R_G_L_0_G_L_0_G" android:translateX="138" + android:translateY="138" android:pivotX="12" android:pivotY="12" + android:scaleX="10" android:scaleY="10"> + <path android:name="_R_G_L_0_G_L_0_G_D_0_P_0" + android:fillColor="@color/letterbox_reachability_education_item_color" + android:fillAlpha="1" android:fillType="nonZero" + android:pathData=" M19.81 13.64 C19.62,13.23 19.33,12.93 18.93,12.75 C18.93,12.75 15.23,10.95 15.23,10.95 C15.16,10.9 15.09,10.86 15.01,10.84 C14.99,10.83 14.96,10.83 14.94,10.83 C14.88,10.81 14.83,10.8 14.78,10.8 C14.78,10.8 14.13,10.8 14.13,10.8 C14.13,10.8 14.13,8.9 14.13,8.9 C14.13,8.9 14.13,6.5 14.13,6.5 C14.13,5.8 13.88,5.21 13.4,4.72 C12.92,4.24 12.33,4 11.63,4 C10.93,4 10.33,4.24 9.85,4.72 C9.37,5.21 9.13,5.8 9.13,6.5 C9.13,6.5 9.13,8.95 9.13,8.95 C9.13,8.95 9.13,11.4 9.13,11.4 C9.13,11.4 9.13,14.65 9.13,14.65 C9.13,14.65 7.18,14.2 7.18,14.2 C6.86,14.12 6.56,14.14 6.26,14.26 C5.97,14.39 5.71,14.57 5.48,14.8 C5.48,14.8 4.08,16.25 4.08,16.25 C4.08,16.25 9.23,21.4 9.23,21.4 C9.41,21.58 9.63,21.73 9.88,21.84 C10.13,21.95 10.39,22 10.68,22 C10.68,22 17.08,22 17.08,22 C17.56,22 17.99,21.85 18.38,21.54 C18.76,21.23 18.99,20.83 19.08,20.35 C19.08,20.35 19.98,14.9 19.98,14.9 C20.06,14.47 20,14.05 19.81,13.64c "/> + </group> + </group> + </group> + <group android:name="time_group"/> + </vector> + </aapt:attr> + <target android:name="_R_G_L_0_G_L_1_G_D_0_P_0"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator android:propertyName="fillAlpha" android:duration="500" + android:startOffset="0" android:valueFrom="1" android:valueTo="1" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator + android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="fillAlpha" android:duration="250" + android:startOffset="500" android:valueFrom="1" + android:valueTo="0.1" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator + android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="fillAlpha" android:duration="83" + android:startOffset="750" android:valueFrom="0.1" + android:valueTo="1" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator + android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="fillAlpha" android:duration="250" + android:startOffset="833" android:valueFrom="1" + android:valueTo="0.1" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator + android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="fillAlpha" android:duration="83" + android:startOffset="1083" android:valueFrom="0.1" + android:valueTo="1" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator + android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="fillAlpha" android:duration="833" + android:startOffset="1167" android:valueFrom="1" android:valueTo="1" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator + android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="fillAlpha" android:duration="250" + android:startOffset="2000" android:valueFrom="1" + android:valueTo="0.1" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator + android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="fillAlpha" android:duration="83" + android:startOffset="2250" android:valueFrom="0.1" + android:valueTo="1" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator + android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="fillAlpha" android:duration="250" + android:startOffset="2333" android:valueFrom="1" + android:valueTo="0.1" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator + android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="fillAlpha" android:duration="83" + android:startOffset="2583" android:valueFrom="0.1" + android:valueTo="1" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator + android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="fillAlpha" android:duration="833" + android:startOffset="2667" android:valueFrom="1" android:valueTo="1" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator + android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="fillAlpha" android:duration="250" + android:startOffset="3500" android:valueFrom="1" + android:valueTo="0.1" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator + android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="fillAlpha" android:duration="83" + android:startOffset="3750" android:valueFrom="0.1" + android:valueTo="1" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator + android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="fillAlpha" android:duration="250" + android:startOffset="3833" android:valueFrom="1" + android:valueTo="0.1" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator + android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="fillAlpha" android:duration="83" + android:startOffset="4083" android:valueFrom="0.1" + android:valueTo="1" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator + android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="fillAlpha" android:duration="833" + android:startOffset="4167" android:valueFrom="1" android:valueTo="1" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator + android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="fillAlpha" android:duration="250" + android:startOffset="5000" android:valueFrom="1" + android:valueTo="0.1" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator + android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="fillAlpha" android:duration="83" + android:startOffset="5250" android:valueFrom="0.1" + android:valueTo="1" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator + android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="fillAlpha" android:duration="250" + android:startOffset="5333" android:valueFrom="1" + android:valueTo="0.1" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator + android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="fillAlpha" android:duration="83" + android:startOffset="5583" android:valueFrom="0.1" + android:valueTo="1" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator + android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="_R_G_L_0_G_L_1_G_D_0_P_0_G_0_T_0"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator android:propertyName="scaleX" android:duration="500" + android:startOffset="0" android:valueFrom="1" android:valueTo="1" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.001,0 0,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="scaleY" android:duration="500" + android:startOffset="0" android:valueFrom="1" android:valueTo="1" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.001,0 0,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="scaleX" android:duration="250" + android:startOffset="500" android:valueFrom="1" + android:valueTo="1.4000000000000001" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.001,0 0,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="scaleY" android:duration="250" + android:startOffset="500" android:valueFrom="1" + android:valueTo="1.4000000000000001" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.001,0 0,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="scaleX" android:duration="83" + android:startOffset="750" android:valueFrom="1.4000000000000001" + android:valueTo="1" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.999,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="scaleY" android:duration="83" + android:startOffset="750" android:valueFrom="1.4000000000000001" + android:valueTo="1" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.999,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="scaleX" android:duration="250" + android:startOffset="833" android:valueFrom="1" + android:valueTo="1.4000000000000001" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.001,0 0,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="scaleY" android:duration="250" + android:startOffset="833" android:valueFrom="1" + android:valueTo="1.4000000000000001" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.001,0 0,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="scaleX" android:duration="83" + android:startOffset="1083" android:valueFrom="1.4000000000000001" + android:valueTo="1" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.999,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="scaleY" android:duration="83" + android:startOffset="1083" android:valueFrom="1.4000000000000001" + android:valueTo="1" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.999,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="scaleX" android:duration="833" + android:startOffset="1167" android:valueFrom="1" android:valueTo="1" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.833,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="scaleY" android:duration="833" + android:startOffset="1167" android:valueFrom="1" android:valueTo="1" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.833,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="scaleX" android:duration="250" + android:startOffset="2000" android:valueFrom="1" + android:valueTo="1.4000000000000001" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.001,0 0,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="scaleY" android:duration="250" + android:startOffset="2000" android:valueFrom="1" + android:valueTo="1.4000000000000001" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.001,0 0,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="scaleX" android:duration="83" + android:startOffset="2250" android:valueFrom="1.4000000000000001" + android:valueTo="1" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.999,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="scaleY" android:duration="83" + android:startOffset="2250" android:valueFrom="1.4000000000000001" + android:valueTo="1" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.999,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="scaleX" android:duration="250" + android:startOffset="2333" android:valueFrom="1" + android:valueTo="1.4000000000000001" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.001,0 0,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="scaleY" android:duration="250" + android:startOffset="2333" android:valueFrom="1" + android:valueTo="1.4000000000000001" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.001,0 0,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="scaleX" android:duration="83" + android:startOffset="2583" android:valueFrom="1.4000000000000001" + android:valueTo="1" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.999,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="scaleY" android:duration="83" + android:startOffset="2583" android:valueFrom="1.4000000000000001" + android:valueTo="1" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.999,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="scaleX" android:duration="833" + android:startOffset="2667" android:valueFrom="1" android:valueTo="1" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.833,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="scaleY" android:duration="833" + android:startOffset="2667" android:valueFrom="1" android:valueTo="1" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.833,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="scaleX" android:duration="250" + android:startOffset="3500" android:valueFrom="1" + android:valueTo="1.4000000000000001" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.001,0 0,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="scaleY" android:duration="250" + android:startOffset="3500" android:valueFrom="1" + android:valueTo="1.4000000000000001" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.001,0 0,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="scaleX" android:duration="83" + android:startOffset="3750" android:valueFrom="1.4000000000000001" + android:valueTo="1" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.999,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="scaleY" android:duration="83" + android:startOffset="3750" android:valueFrom="1.4000000000000001" + android:valueTo="1" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.999,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="scaleX" android:duration="250" + android:startOffset="3833" android:valueFrom="1" + android:valueTo="1.4000000000000001" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.001,0 0,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="scaleY" android:duration="250" + android:startOffset="3833" android:valueFrom="1" + android:valueTo="1.4000000000000001" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.001,0 0,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="scaleX" android:duration="83" + android:startOffset="4083" android:valueFrom="1.4000000000000001" + android:valueTo="1" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.999,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="scaleY" android:duration="83" + android:startOffset="4083" android:valueFrom="1.4000000000000001" + android:valueTo="1" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.999,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="scaleX" android:duration="833" + android:startOffset="4167" android:valueFrom="1" android:valueTo="1" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.833,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="scaleY" android:duration="833" + android:startOffset="4167" android:valueFrom="1" android:valueTo="1" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.833,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="scaleX" android:duration="250" + android:startOffset="5000" android:valueFrom="1" + android:valueTo="1.4000000000000001" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.001,0 0,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="scaleY" android:duration="250" + android:startOffset="5000" android:valueFrom="1" + android:valueTo="1.4000000000000001" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.001,0 0,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="scaleX" android:duration="83" + android:startOffset="5250" android:valueFrom="1.4000000000000001" + android:valueTo="1" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.999,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="scaleY" android:duration="83" + android:startOffset="5250" android:valueFrom="1.4000000000000001" + android:valueTo="1" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.999,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="scaleX" android:duration="250" + android:startOffset="5333" android:valueFrom="1" + android:valueTo="1.4000000000000001" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.001,0 0,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="scaleY" android:duration="250" + android:startOffset="5333" android:valueFrom="1" + android:valueTo="1.4000000000000001" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.001,0 0,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="scaleX" android:duration="83" + android:startOffset="5583" android:valueFrom="1.4000000000000001" + android:valueTo="1" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.999,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="scaleY" android:duration="83" + android:startOffset="5583" android:valueFrom="1.4000000000000001" + android:valueTo="1" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.999,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="_R_G_L_0_G_L_0_G_D_0_P_0"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator android:propertyName="pathData" android:duration="500" + android:startOffset="0" + android:valueFrom="M19.81 13.64 C19.62,13.23 19.33,12.93 18.93,12.75 C18.93,12.75 15.23,10.95 15.23,10.95 C15.16,10.9 15.09,10.86 15.01,10.84 C14.99,10.83 14.96,10.83 14.94,10.83 C14.88,10.81 14.83,10.8 14.78,10.8 C14.78,10.8 14.13,10.8 14.13,10.8 C14.13,10.8 14.13,8.9 14.13,8.9 C14.13,8.9 14.13,6.5 14.13,6.5 C14.13,5.8 13.88,5.21 13.4,4.72 C12.92,4.24 12.33,4 11.63,4 C10.93,4 10.33,4.24 9.85,4.72 C9.37,5.21 9.13,5.8 9.13,6.5 C9.13,6.5 9.13,8.95 9.13,8.95 C9.13,8.95 9.13,11.4 9.13,11.4 C9.13,11.4 9.13,14.65 9.13,14.65 C9.13,14.65 7.18,14.2 7.18,14.2 C6.86,14.12 6.56,14.14 6.26,14.26 C5.97,14.39 5.71,14.57 5.48,14.8 C5.48,14.8 4.08,16.25 4.08,16.25 C4.08,16.25 9.23,21.4 9.23,21.4 C9.41,21.58 9.63,21.73 9.88,21.84 C10.13,21.95 10.39,22 10.68,22 C10.68,22 17.08,22 17.08,22 C17.56,22 17.99,21.85 18.38,21.54 C18.76,21.23 18.99,20.83 19.08,20.35 C19.08,20.35 19.98,14.9 19.98,14.9 C20.06,14.47 20,14.05 19.81,13.64c M17.08 20 C17.08,20 10.68,20 10.68,20 C10.68,20 6.88,16.2 6.88,16.2 C6.88,16.2 11.13,17.1 11.13,17.1 C11.13,17.1 11.13,6.5 11.13,6.5 C11.13,6.35 11.17,6.23 11.26,6.14 C11.35,6.05 11.48,6 11.63,6 C11.78,6 11.9,6.05 11.99,6.14 C12.08,6.23 12.13,6.35 12.13,6.5 C12.13,6.5 12.13,12.5 12.13,12.5 C12.13,12.5 13.88,12.5 13.88,12.5 C13.88,12.5 18.02,14.55 18.02,14.55 C18.02,14.55 17.08,20 17.08,20c " + android:valueTo="M19.81 13.64 C19.62,13.23 19.33,12.93 18.93,12.75 C18.93,12.75 15.23,10.95 15.23,10.95 C15.16,10.9 15.09,10.86 15.01,10.84 C14.99,10.83 14.96,10.83 14.94,10.83 C14.88,10.81 14.83,10.8 14.78,10.8 C14.78,10.8 14.13,10.8 14.13,10.8 C14.13,10.8 14.13,8.9 14.13,8.9 C14.13,8.9 14.13,6.5 14.13,6.5 C14.13,5.8 13.88,5.21 13.4,4.72 C12.92,4.24 12.33,4 11.63,4 C10.93,4 10.33,4.24 9.85,4.72 C9.37,5.21 9.13,5.8 9.13,6.5 C9.13,6.5 9.13,8.95 9.13,8.95 C9.13,8.95 9.13,11.4 9.13,11.4 C9.13,11.4 9.13,14.65 9.13,14.65 C9.13,14.65 7.18,14.2 7.18,14.2 C6.86,14.12 6.56,14.14 6.26,14.26 C5.97,14.39 5.71,14.57 5.48,14.8 C5.48,14.8 4.08,16.25 4.08,16.25 C4.08,16.25 9.23,21.4 9.23,21.4 C9.41,21.58 9.63,21.73 9.88,21.84 C10.13,21.95 10.39,22 10.68,22 C10.68,22 17.08,22 17.08,22 C17.56,22 17.99,21.85 18.38,21.54 C18.76,21.23 18.99,20.83 19.08,20.35 C19.08,20.35 19.98,14.9 19.98,14.9 C20.06,14.47 20,14.05 19.81,13.64c M17.08 20 C17.08,20 10.68,20 10.68,20 C10.68,20 6.88,16.2 6.88,16.2 C6.88,16.2 11.13,17.1 11.13,17.1 C11.13,17.1 11.13,6.5 11.13,6.5 C11.13,6.35 11.17,6.23 11.26,6.14 C11.35,6.05 11.48,6 11.63,6 C11.78,6 11.9,6.05 11.99,6.14 C12.08,6.23 12.13,6.35 12.13,6.5 C12.13,6.5 12.13,12.5 12.13,12.5 C12.13,12.5 13.88,12.5 13.88,12.5 C13.88,12.5 18.02,14.55 18.02,14.55 C18.02,14.55 17.08,20 17.08,20c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.2,0 0,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="pathData" android:duration="250" + android:startOffset="500" + android:valueFrom="M19.81 13.64 C19.62,13.23 19.33,12.93 18.93,12.75 C18.93,12.75 15.23,10.95 15.23,10.95 C15.16,10.9 15.09,10.86 15.01,10.84 C14.99,10.83 14.96,10.83 14.94,10.83 C14.88,10.81 14.83,10.8 14.78,10.8 C14.78,10.8 14.13,10.8 14.13,10.8 C14.13,10.8 14.13,8.9 14.13,8.9 C14.13,8.9 14.13,6.5 14.13,6.5 C14.13,5.8 13.88,5.21 13.4,4.72 C12.92,4.24 12.33,4 11.63,4 C10.93,4 10.33,4.24 9.85,4.72 C9.37,5.21 9.13,5.8 9.13,6.5 C9.13,6.5 9.13,8.95 9.13,8.95 C9.13,8.95 9.13,11.4 9.13,11.4 C9.13,11.4 9.13,14.65 9.13,14.65 C9.13,14.65 7.18,14.2 7.18,14.2 C6.86,14.12 6.56,14.14 6.26,14.26 C5.97,14.39 5.71,14.57 5.48,14.8 C5.48,14.8 4.08,16.25 4.08,16.25 C4.08,16.25 9.23,21.4 9.23,21.4 C9.41,21.58 9.63,21.73 9.88,21.84 C10.13,21.95 10.39,22 10.68,22 C10.68,22 17.08,22 17.08,22 C17.56,22 17.99,21.85 18.38,21.54 C18.76,21.23 18.99,20.83 19.08,20.35 C19.08,20.35 19.98,14.9 19.98,14.9 C20.06,14.47 20,14.05 19.81,13.64c M17.08 20 C17.08,20 10.68,20 10.68,20 C10.68,20 6.88,16.2 6.88,16.2 C6.88,16.2 11.13,17.1 11.13,17.1 C11.13,17.1 11.13,6.5 11.13,6.5 C11.13,6.35 11.17,6.23 11.26,6.14 C11.35,6.05 11.48,6 11.63,6 C11.78,6 11.9,6.05 11.99,6.14 C12.08,6.23 12.13,6.35 12.13,6.5 C12.13,6.5 12.13,12.5 12.13,12.5 C12.13,12.5 13.88,12.5 13.88,12.5 C13.88,12.5 18.02,14.55 18.02,14.55 C18.02,14.55 17.08,20 17.08,20c " + android:valueTo="M19.81 13.64 C19.62,13.23 19.33,12.93 18.93,12.75 C18.93,12.75 15.23,10.95 15.23,10.95 C15.16,10.9 15.09,10.86 15.01,10.84 C14.99,10.83 14.96,10.83 14.94,10.83 C14.88,10.81 14.83,10.8 14.78,10.8 C14.78,10.8 14.13,10.8 14.13,10.8 C14.13,10.8 14.13,8.9 14.13,8.9 C14.13,8.9 14.13,8 14.13,8 C14.13,7.3 13.88,6.71 13.4,6.23 C12.92,5.74 12.33,5.5 11.63,5.5 C10.93,5.5 10.33,5.74 9.85,6.23 C9.37,6.71 9.13,7.3 9.13,8 C9.13,8 9.13,8.95 9.13,8.95 C9.13,8.95 9.13,11.4 9.13,11.4 C9.13,11.4 9.13,14.65 9.13,14.65 C9.13,14.65 7.18,14.2 7.18,14.2 C6.86,14.12 6.56,14.14 6.26,14.26 C5.97,14.39 5.71,14.57 5.48,14.8 C5.48,14.8 4.08,16.25 4.08,16.25 C4.08,16.25 9.23,21.4 9.23,21.4 C9.41,21.58 9.63,21.73 9.88,21.84 C10.13,21.95 10.39,22 10.68,22 C10.68,22 17.08,22 17.08,22 C17.56,22 17.99,21.85 18.38,21.54 C18.76,21.23 18.99,20.83 19.08,20.35 C19.08,20.35 19.98,14.9 19.98,14.9 C20.06,14.47 20,14.05 19.81,13.64c M17.08 20 C17.08,20 10.68,20 10.68,20 C10.68,20 6.88,16.2 6.88,16.2 C6.88,16.2 11.13,17.1 11.13,17.1 C11.13,17.1 11.13,8 11.13,8 C11.13,7.85 11.17,7.73 11.26,7.64 C11.35,7.55 11.48,7.5 11.63,7.5 C11.78,7.5 11.9,7.55 11.99,7.64 C12.08,7.73 12.13,7.85 12.13,8 C12.13,8 12.13,12.5 12.13,12.5 C12.13,12.5 13.88,12.5 13.88,12.5 C13.88,12.5 18.02,14.55 18.02,14.55 C18.02,14.55 17.08,20 17.08,20c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.2,0 0,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="pathData" android:duration="83" + android:startOffset="750" + android:valueFrom="M19.81 13.64 C19.62,13.23 19.33,12.93 18.93,12.75 C18.93,12.75 15.23,10.95 15.23,10.95 C15.16,10.9 15.09,10.86 15.01,10.84 C14.99,10.83 14.96,10.83 14.94,10.83 C14.88,10.81 14.83,10.8 14.78,10.8 C14.78,10.8 14.13,10.8 14.13,10.8 C14.13,10.8 14.13,8.9 14.13,8.9 C14.13,8.9 14.13,8 14.13,8 C14.13,7.3 13.88,6.71 13.4,6.23 C12.92,5.74 12.33,5.5 11.63,5.5 C10.93,5.5 10.33,5.74 9.85,6.23 C9.37,6.71 9.13,7.3 9.13,8 C9.13,8 9.13,8.95 9.13,8.95 C9.13,8.95 9.13,11.4 9.13,11.4 C9.13,11.4 9.13,14.65 9.13,14.65 C9.13,14.65 7.18,14.2 7.18,14.2 C6.86,14.12 6.56,14.14 6.26,14.26 C5.97,14.39 5.71,14.57 5.48,14.8 C5.48,14.8 4.08,16.25 4.08,16.25 C4.08,16.25 9.23,21.4 9.23,21.4 C9.41,21.58 9.63,21.73 9.88,21.84 C10.13,21.95 10.39,22 10.68,22 C10.68,22 17.08,22 17.08,22 C17.56,22 17.99,21.85 18.38,21.54 C18.76,21.23 18.99,20.83 19.08,20.35 C19.08,20.35 19.98,14.9 19.98,14.9 C20.06,14.47 20,14.05 19.81,13.64c M17.08 20 C17.08,20 10.68,20 10.68,20 C10.68,20 6.88,16.2 6.88,16.2 C6.88,16.2 11.13,17.1 11.13,17.1 C11.13,17.1 11.13,8 11.13,8 C11.13,7.85 11.17,7.73 11.26,7.64 C11.35,7.55 11.48,7.5 11.63,7.5 C11.78,7.5 11.9,7.55 11.99,7.64 C12.08,7.73 12.13,7.85 12.13,8 C12.13,8 12.13,12.5 12.13,12.5 C12.13,12.5 13.88,12.5 13.88,12.5 C13.88,12.5 18.02,14.55 18.02,14.55 C18.02,14.55 17.08,20 17.08,20c " + android:valueTo="M19.81 13.64 C19.62,13.23 19.33,12.93 18.93,12.75 C18.93,12.75 15.23,10.95 15.23,10.95 C15.16,10.9 15.09,10.86 15.01,10.84 C14.99,10.83 14.96,10.83 14.94,10.83 C14.88,10.81 14.83,10.8 14.78,10.8 C14.78,10.8 14.13,10.8 14.13,10.8 C14.13,10.8 14.13,8.9 14.13,8.9 C14.13,8.9 14.13,6.5 14.13,6.5 C14.13,5.8 13.88,5.21 13.4,4.72 C12.92,4.24 12.33,4 11.63,4 C10.93,4 10.33,4.24 9.85,4.72 C9.37,5.21 9.13,5.8 9.13,6.5 C9.13,6.5 9.13,8.95 9.13,8.95 C9.13,8.95 9.13,11.4 9.13,11.4 C9.13,11.4 9.13,14.65 9.13,14.65 C9.13,14.65 7.18,14.2 7.18,14.2 C6.86,14.12 6.56,14.14 6.26,14.26 C5.97,14.39 5.71,14.57 5.48,14.8 C5.48,14.8 4.08,16.25 4.08,16.25 C4.08,16.25 9.23,21.4 9.23,21.4 C9.41,21.58 9.63,21.73 9.88,21.84 C10.13,21.95 10.39,22 10.68,22 C10.68,22 17.08,22 17.08,22 C17.56,22 17.99,21.85 18.38,21.54 C18.76,21.23 18.99,20.83 19.08,20.35 C19.08,20.35 19.98,14.9 19.98,14.9 C20.06,14.47 20,14.05 19.81,13.64c M17.08 20 C17.08,20 10.68,20 10.68,20 C10.68,20 6.88,16.2 6.88,16.2 C6.88,16.2 11.13,17.1 11.13,17.1 C11.13,17.1 11.13,6.5 11.13,6.5 C11.13,6.35 11.17,6.23 11.26,6.14 C11.35,6.05 11.48,6 11.63,6 C11.78,6 11.9,6.05 11.99,6.14 C12.08,6.23 12.13,6.35 12.13,6.5 C12.13,6.5 12.13,12.5 12.13,12.5 C12.13,12.5 13.88,12.5 13.88,12.5 C13.88,12.5 18.02,14.55 18.02,14.55 C18.02,14.55 17.08,20 17.08,20c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.001,0 0,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="pathData" android:duration="250" + android:startOffset="833" + android:valueFrom="M19.81 13.64 C19.62,13.23 19.33,12.93 18.93,12.75 C18.93,12.75 15.23,10.95 15.23,10.95 C15.16,10.9 15.09,10.86 15.01,10.84 C14.99,10.83 14.96,10.83 14.94,10.83 C14.88,10.81 14.83,10.8 14.78,10.8 C14.78,10.8 14.13,10.8 14.13,10.8 C14.13,10.8 14.13,8.9 14.13,8.9 C14.13,8.9 14.13,6.5 14.13,6.5 C14.13,5.8 13.88,5.21 13.4,4.72 C12.92,4.24 12.33,4 11.63,4 C10.93,4 10.33,4.24 9.85,4.72 C9.37,5.21 9.13,5.8 9.13,6.5 C9.13,6.5 9.13,8.95 9.13,8.95 C9.13,8.95 9.13,11.4 9.13,11.4 C9.13,11.4 9.13,14.65 9.13,14.65 C9.13,14.65 7.18,14.2 7.18,14.2 C6.86,14.12 6.56,14.14 6.26,14.26 C5.97,14.39 5.71,14.57 5.48,14.8 C5.48,14.8 4.08,16.25 4.08,16.25 C4.08,16.25 9.23,21.4 9.23,21.4 C9.41,21.58 9.63,21.73 9.88,21.84 C10.13,21.95 10.39,22 10.68,22 C10.68,22 17.08,22 17.08,22 C17.56,22 17.99,21.85 18.38,21.54 C18.76,21.23 18.99,20.83 19.08,20.35 C19.08,20.35 19.98,14.9 19.98,14.9 C20.06,14.47 20,14.05 19.81,13.64c M17.08 20 C17.08,20 10.68,20 10.68,20 C10.68,20 6.88,16.2 6.88,16.2 C6.88,16.2 11.13,17.1 11.13,17.1 C11.13,17.1 11.13,6.5 11.13,6.5 C11.13,6.35 11.17,6.23 11.26,6.14 C11.35,6.05 11.48,6 11.63,6 C11.78,6 11.9,6.05 11.99,6.14 C12.08,6.23 12.13,6.35 12.13,6.5 C12.13,6.5 12.13,12.5 12.13,12.5 C12.13,12.5 13.88,12.5 13.88,12.5 C13.88,12.5 18.02,14.55 18.02,14.55 C18.02,14.55 17.08,20 17.08,20c " + android:valueTo="M19.81 13.64 C19.62,13.23 19.33,12.93 18.93,12.75 C18.93,12.75 15.23,10.95 15.23,10.95 C15.16,10.9 15.09,10.86 15.01,10.84 C14.99,10.83 14.96,10.83 14.94,10.83 C14.88,10.81 14.83,10.8 14.78,10.8 C14.78,10.8 14.13,10.8 14.13,10.8 C14.13,10.8 14.13,8.9 14.13,8.9 C14.13,8.9 14.13,8 14.13,8 C14.13,7.3 13.88,6.71 13.4,6.23 C12.92,5.74 12.33,5.5 11.63,5.5 C10.93,5.5 10.33,5.74 9.85,6.23 C9.37,6.71 9.13,7.3 9.13,8 C9.13,8 9.13,8.95 9.13,8.95 C9.13,8.95 9.13,11.4 9.13,11.4 C9.13,11.4 9.13,14.65 9.13,14.65 C9.13,14.65 7.18,14.2 7.18,14.2 C6.86,14.12 6.56,14.14 6.26,14.26 C5.97,14.39 5.71,14.57 5.48,14.8 C5.48,14.8 4.08,16.25 4.08,16.25 C4.08,16.25 9.23,21.4 9.23,21.4 C9.41,21.58 9.63,21.73 9.88,21.84 C10.13,21.95 10.39,22 10.68,22 C10.68,22 17.08,22 17.08,22 C17.56,22 17.99,21.85 18.38,21.54 C18.76,21.23 18.99,20.83 19.08,20.35 C19.08,20.35 19.98,14.9 19.98,14.9 C20.06,14.47 20,14.05 19.81,13.64c M17.08 20 C17.08,20 10.68,20 10.68,20 C10.68,20 6.88,16.2 6.88,16.2 C6.88,16.2 11.13,17.1 11.13,17.1 C11.13,17.1 11.13,8 11.13,8 C11.13,7.85 11.17,7.73 11.26,7.64 C11.35,7.55 11.48,7.5 11.63,7.5 C11.78,7.5 11.9,7.55 11.99,7.64 C12.08,7.73 12.13,7.85 12.13,8 C12.13,8 12.13,12.5 12.13,12.5 C12.13,12.5 13.88,12.5 13.88,12.5 C13.88,12.5 18.02,14.55 18.02,14.55 C18.02,14.55 17.08,20 17.08,20c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.2,0 0,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="pathData" android:duration="83" + android:startOffset="1083" + android:valueFrom="M19.81 13.64 C19.62,13.23 19.33,12.93 18.93,12.75 C18.93,12.75 15.23,10.95 15.23,10.95 C15.16,10.9 15.09,10.86 15.01,10.84 C14.99,10.83 14.96,10.83 14.94,10.83 C14.88,10.81 14.83,10.8 14.78,10.8 C14.78,10.8 14.13,10.8 14.13,10.8 C14.13,10.8 14.13,8.9 14.13,8.9 C14.13,8.9 14.13,8 14.13,8 C14.13,7.3 13.88,6.71 13.4,6.23 C12.92,5.74 12.33,5.5 11.63,5.5 C10.93,5.5 10.33,5.74 9.85,6.23 C9.37,6.71 9.13,7.3 9.13,8 C9.13,8 9.13,8.95 9.13,8.95 C9.13,8.95 9.13,11.4 9.13,11.4 C9.13,11.4 9.13,14.65 9.13,14.65 C9.13,14.65 7.18,14.2 7.18,14.2 C6.86,14.12 6.56,14.14 6.26,14.26 C5.97,14.39 5.71,14.57 5.48,14.8 C5.48,14.8 4.08,16.25 4.08,16.25 C4.08,16.25 9.23,21.4 9.23,21.4 C9.41,21.58 9.63,21.73 9.88,21.84 C10.13,21.95 10.39,22 10.68,22 C10.68,22 17.08,22 17.08,22 C17.56,22 17.99,21.85 18.38,21.54 C18.76,21.23 18.99,20.83 19.08,20.35 C19.08,20.35 19.98,14.9 19.98,14.9 C20.06,14.47 20,14.05 19.81,13.64c M17.08 20 C17.08,20 10.68,20 10.68,20 C10.68,20 6.88,16.2 6.88,16.2 C6.88,16.2 11.13,17.1 11.13,17.1 C11.13,17.1 11.13,8 11.13,8 C11.13,7.85 11.17,7.73 11.26,7.64 C11.35,7.55 11.48,7.5 11.63,7.5 C11.78,7.5 11.9,7.55 11.99,7.64 C12.08,7.73 12.13,7.85 12.13,8 C12.13,8 12.13,12.5 12.13,12.5 C12.13,12.5 13.88,12.5 13.88,12.5 C13.88,12.5 18.02,14.55 18.02,14.55 C18.02,14.55 17.08,20 17.08,20c " + android:valueTo="M19.81 13.64 C19.62,13.23 19.33,12.93 18.93,12.75 C18.93,12.75 15.23,10.95 15.23,10.95 C15.16,10.9 15.09,10.86 15.01,10.84 C14.99,10.83 14.96,10.83 14.94,10.83 C14.88,10.81 14.83,10.8 14.78,10.8 C14.78,10.8 14.13,10.8 14.13,10.8 C14.13,10.8 14.13,8.9 14.13,8.9 C14.13,8.9 14.13,6.5 14.13,6.5 C14.13,5.8 13.88,5.21 13.4,4.72 C12.92,4.24 12.33,4 11.63,4 C10.93,4 10.33,4.24 9.85,4.72 C9.37,5.21 9.13,5.8 9.13,6.5 C9.13,6.5 9.13,8.95 9.13,8.95 C9.13,8.95 9.13,11.4 9.13,11.4 C9.13,11.4 9.13,14.65 9.13,14.65 C9.13,14.65 7.18,14.2 7.18,14.2 C6.86,14.12 6.56,14.14 6.26,14.26 C5.97,14.39 5.71,14.57 5.48,14.8 C5.48,14.8 4.08,16.25 4.08,16.25 C4.08,16.25 9.23,21.4 9.23,21.4 C9.41,21.58 9.63,21.73 9.88,21.84 C10.13,21.95 10.39,22 10.68,22 C10.68,22 17.08,22 17.08,22 C17.56,22 17.99,21.85 18.38,21.54 C18.76,21.23 18.99,20.83 19.08,20.35 C19.08,20.35 19.98,14.9 19.98,14.9 C20.06,14.47 20,14.05 19.81,13.64c M17.08 20 C17.08,20 10.68,20 10.68,20 C10.68,20 6.88,16.2 6.88,16.2 C6.88,16.2 11.13,17.1 11.13,17.1 C11.13,17.1 11.13,6.5 11.13,6.5 C11.13,6.35 11.17,6.23 11.26,6.14 C11.35,6.05 11.48,6 11.63,6 C11.78,6 11.9,6.05 11.99,6.14 C12.08,6.23 12.13,6.35 12.13,6.5 C12.13,6.5 12.13,12.5 12.13,12.5 C12.13,12.5 13.88,12.5 13.88,12.5 C13.88,12.5 18.02,14.55 18.02,14.55 C18.02,14.55 17.08,20 17.08,20c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.001,0 0,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="pathData" android:duration="833" + android:startOffset="1167" + android:valueFrom="M19.81 13.64 C19.62,13.23 19.33,12.93 18.93,12.75 C18.93,12.75 15.23,10.95 15.23,10.95 C15.16,10.9 15.09,10.86 15.01,10.84 C14.99,10.83 14.96,10.83 14.94,10.83 C14.88,10.81 14.83,10.8 14.78,10.8 C14.78,10.8 14.13,10.8 14.13,10.8 C14.13,10.8 14.13,8.9 14.13,8.9 C14.13,8.9 14.13,6.5 14.13,6.5 C14.13,5.8 13.88,5.21 13.4,4.72 C12.92,4.24 12.33,4 11.63,4 C10.93,4 10.33,4.24 9.85,4.72 C9.37,5.21 9.13,5.8 9.13,6.5 C9.13,6.5 9.13,8.95 9.13,8.95 C9.13,8.95 9.13,11.4 9.13,11.4 C9.13,11.4 9.13,14.65 9.13,14.65 C9.13,14.65 7.18,14.2 7.18,14.2 C6.86,14.12 6.56,14.14 6.26,14.26 C5.97,14.39 5.71,14.57 5.48,14.8 C5.48,14.8 4.08,16.25 4.08,16.25 C4.08,16.25 9.23,21.4 9.23,21.4 C9.41,21.58 9.63,21.73 9.88,21.84 C10.13,21.95 10.39,22 10.68,22 C10.68,22 17.08,22 17.08,22 C17.56,22 17.99,21.85 18.38,21.54 C18.76,21.23 18.99,20.83 19.08,20.35 C19.08,20.35 19.98,14.9 19.98,14.9 C20.06,14.47 20,14.05 19.81,13.64c M17.08 20 C17.08,20 10.68,20 10.68,20 C10.68,20 6.88,16.2 6.88,16.2 C6.88,16.2 11.13,17.1 11.13,17.1 C11.13,17.1 11.13,6.5 11.13,6.5 C11.13,6.35 11.17,6.23 11.26,6.14 C11.35,6.05 11.48,6 11.63,6 C11.78,6 11.9,6.05 11.99,6.14 C12.08,6.23 12.13,6.35 12.13,6.5 C12.13,6.5 12.13,12.5 12.13,12.5 C12.13,12.5 13.88,12.5 13.88,12.5 C13.88,12.5 18.02,14.55 18.02,14.55 C18.02,14.55 17.08,20 17.08,20c " + android:valueTo="M19.81 13.64 C19.62,13.23 19.33,12.93 18.93,12.75 C18.93,12.75 15.23,10.95 15.23,10.95 C15.16,10.9 15.09,10.86 15.01,10.84 C14.99,10.83 14.96,10.83 14.94,10.83 C14.88,10.81 14.83,10.8 14.78,10.8 C14.78,10.8 14.13,10.8 14.13,10.8 C14.13,10.8 14.13,8.9 14.13,8.9 C14.13,8.9 14.13,6.5 14.13,6.5 C14.13,5.8 13.88,5.21 13.4,4.72 C12.92,4.24 12.33,4 11.63,4 C10.93,4 10.33,4.24 9.85,4.72 C9.37,5.21 9.13,5.8 9.13,6.5 C9.13,6.5 9.13,8.95 9.13,8.95 C9.13,8.95 9.13,11.4 9.13,11.4 C9.13,11.4 9.13,14.65 9.13,14.65 C9.13,14.65 7.18,14.2 7.18,14.2 C6.86,14.12 6.56,14.14 6.26,14.26 C5.97,14.39 5.71,14.57 5.48,14.8 C5.48,14.8 4.08,16.25 4.08,16.25 C4.08,16.25 9.23,21.4 9.23,21.4 C9.41,21.58 9.63,21.73 9.88,21.84 C10.13,21.95 10.39,22 10.68,22 C10.68,22 17.08,22 17.08,22 C17.56,22 17.99,21.85 18.38,21.54 C18.76,21.23 18.99,20.83 19.08,20.35 C19.08,20.35 19.98,14.9 19.98,14.9 C20.06,14.47 20,14.05 19.81,13.64c M17.08 20 C17.08,20 10.68,20 10.68,20 C10.68,20 6.88,16.2 6.88,16.2 C6.88,16.2 11.13,17.1 11.13,17.1 C11.13,17.1 11.13,6.5 11.13,6.5 C11.13,6.35 11.17,6.23 11.26,6.14 C11.35,6.05 11.48,6 11.63,6 C11.78,6 11.9,6.05 11.99,6.14 C12.08,6.23 12.13,6.35 12.13,6.5 C12.13,6.5 12.13,12.5 12.13,12.5 C12.13,12.5 13.88,12.5 13.88,12.5 C13.88,12.5 18.02,14.55 18.02,14.55 C18.02,14.55 17.08,20 17.08,20c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.833,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="pathData" android:duration="250" + android:startOffset="2000" + android:valueFrom="M19.81 13.64 C19.62,13.23 19.33,12.93 18.93,12.75 C18.93,12.75 15.23,10.95 15.23,10.95 C15.16,10.9 15.09,10.86 15.01,10.84 C14.99,10.83 14.96,10.83 14.94,10.83 C14.88,10.81 14.83,10.8 14.78,10.8 C14.78,10.8 14.13,10.8 14.13,10.8 C14.13,10.8 14.13,8.9 14.13,8.9 C14.13,8.9 14.13,6.5 14.13,6.5 C14.13,5.8 13.88,5.21 13.4,4.72 C12.92,4.24 12.33,4 11.63,4 C10.93,4 10.33,4.24 9.85,4.72 C9.37,5.21 9.13,5.8 9.13,6.5 C9.13,6.5 9.13,8.95 9.13,8.95 C9.13,8.95 9.13,11.4 9.13,11.4 C9.13,11.4 9.13,14.65 9.13,14.65 C9.13,14.65 7.18,14.2 7.18,14.2 C6.86,14.12 6.56,14.14 6.26,14.26 C5.97,14.39 5.71,14.57 5.48,14.8 C5.48,14.8 4.08,16.25 4.08,16.25 C4.08,16.25 9.23,21.4 9.23,21.4 C9.41,21.58 9.63,21.73 9.88,21.84 C10.13,21.95 10.39,22 10.68,22 C10.68,22 17.08,22 17.08,22 C17.56,22 17.99,21.85 18.38,21.54 C18.76,21.23 18.99,20.83 19.08,20.35 C19.08,20.35 19.98,14.9 19.98,14.9 C20.06,14.47 20,14.05 19.81,13.64c M17.08 20 C17.08,20 10.68,20 10.68,20 C10.68,20 6.88,16.2 6.88,16.2 C6.88,16.2 11.13,17.1 11.13,17.1 C11.13,17.1 11.13,6.5 11.13,6.5 C11.13,6.35 11.17,6.23 11.26,6.14 C11.35,6.05 11.48,6 11.63,6 C11.78,6 11.9,6.05 11.99,6.14 C12.08,6.23 12.13,6.35 12.13,6.5 C12.13,6.5 12.13,12.5 12.13,12.5 C12.13,12.5 13.88,12.5 13.88,12.5 C13.88,12.5 18.02,14.55 18.02,14.55 C18.02,14.55 17.08,20 17.08,20c " + android:valueTo="M19.81 13.64 C19.62,13.23 19.33,12.93 18.93,12.75 C18.93,12.75 15.23,10.95 15.23,10.95 C15.16,10.9 15.09,10.86 15.01,10.84 C14.99,10.83 14.96,10.83 14.94,10.83 C14.88,10.81 14.83,10.8 14.78,10.8 C14.78,10.8 14.13,10.8 14.13,10.8 C14.13,10.8 14.13,8.9 14.13,8.9 C14.13,8.9 14.13,8 14.13,8 C14.13,7.3 13.88,6.71 13.4,6.23 C12.92,5.74 12.33,5.5 11.63,5.5 C10.93,5.5 10.33,5.74 9.85,6.23 C9.37,6.71 9.13,7.3 9.13,8 C9.13,8 9.13,8.95 9.13,8.95 C9.13,8.95 9.13,11.4 9.13,11.4 C9.13,11.4 9.13,14.65 9.13,14.65 C9.13,14.65 7.18,14.2 7.18,14.2 C6.86,14.12 6.56,14.14 6.26,14.26 C5.97,14.39 5.71,14.57 5.48,14.8 C5.48,14.8 4.08,16.25 4.08,16.25 C4.08,16.25 9.23,21.4 9.23,21.4 C9.41,21.58 9.63,21.73 9.88,21.84 C10.13,21.95 10.39,22 10.68,22 C10.68,22 17.08,22 17.08,22 C17.56,22 17.99,21.85 18.38,21.54 C18.76,21.23 18.99,20.83 19.08,20.35 C19.08,20.35 19.98,14.9 19.98,14.9 C20.06,14.47 20,14.05 19.81,13.64c M17.08 20 C17.08,20 10.68,20 10.68,20 C10.68,20 6.88,16.2 6.88,16.2 C6.88,16.2 11.13,17.1 11.13,17.1 C11.13,17.1 11.13,8 11.13,8 C11.13,7.85 11.17,7.73 11.26,7.64 C11.35,7.55 11.48,7.5 11.63,7.5 C11.78,7.5 11.9,7.55 11.99,7.64 C12.08,7.73 12.13,7.85 12.13,8 C12.13,8 12.13,12.5 12.13,12.5 C12.13,12.5 13.88,12.5 13.88,12.5 C13.88,12.5 18.02,14.55 18.02,14.55 C18.02,14.55 17.08,20 17.08,20c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.2,0 0,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="pathData" android:duration="83" + android:startOffset="2250" + android:valueFrom="M19.81 13.64 C19.62,13.23 19.33,12.93 18.93,12.75 C18.93,12.75 15.23,10.95 15.23,10.95 C15.16,10.9 15.09,10.86 15.01,10.84 C14.99,10.83 14.96,10.83 14.94,10.83 C14.88,10.81 14.83,10.8 14.78,10.8 C14.78,10.8 14.13,10.8 14.13,10.8 C14.13,10.8 14.13,8.9 14.13,8.9 C14.13,8.9 14.13,8 14.13,8 C14.13,7.3 13.88,6.71 13.4,6.23 C12.92,5.74 12.33,5.5 11.63,5.5 C10.93,5.5 10.33,5.74 9.85,6.23 C9.37,6.71 9.13,7.3 9.13,8 C9.13,8 9.13,8.95 9.13,8.95 C9.13,8.95 9.13,11.4 9.13,11.4 C9.13,11.4 9.13,14.65 9.13,14.65 C9.13,14.65 7.18,14.2 7.18,14.2 C6.86,14.12 6.56,14.14 6.26,14.26 C5.97,14.39 5.71,14.57 5.48,14.8 C5.48,14.8 4.08,16.25 4.08,16.25 C4.08,16.25 9.23,21.4 9.23,21.4 C9.41,21.58 9.63,21.73 9.88,21.84 C10.13,21.95 10.39,22 10.68,22 C10.68,22 17.08,22 17.08,22 C17.56,22 17.99,21.85 18.38,21.54 C18.76,21.23 18.99,20.83 19.08,20.35 C19.08,20.35 19.98,14.9 19.98,14.9 C20.06,14.47 20,14.05 19.81,13.64c M17.08 20 C17.08,20 10.68,20 10.68,20 C10.68,20 6.88,16.2 6.88,16.2 C6.88,16.2 11.13,17.1 11.13,17.1 C11.13,17.1 11.13,8 11.13,8 C11.13,7.85 11.17,7.73 11.26,7.64 C11.35,7.55 11.48,7.5 11.63,7.5 C11.78,7.5 11.9,7.55 11.99,7.64 C12.08,7.73 12.13,7.85 12.13,8 C12.13,8 12.13,12.5 12.13,12.5 C12.13,12.5 13.88,12.5 13.88,12.5 C13.88,12.5 18.02,14.55 18.02,14.55 C18.02,14.55 17.08,20 17.08,20c " + android:valueTo="M19.81 13.64 C19.62,13.23 19.33,12.93 18.93,12.75 C18.93,12.75 15.23,10.95 15.23,10.95 C15.16,10.9 15.09,10.86 15.01,10.84 C14.99,10.83 14.96,10.83 14.94,10.83 C14.88,10.81 14.83,10.8 14.78,10.8 C14.78,10.8 14.13,10.8 14.13,10.8 C14.13,10.8 14.13,8.9 14.13,8.9 C14.13,8.9 14.13,6.5 14.13,6.5 C14.13,5.8 13.88,5.21 13.4,4.72 C12.92,4.24 12.33,4 11.63,4 C10.93,4 10.33,4.24 9.85,4.72 C9.37,5.21 9.13,5.8 9.13,6.5 C9.13,6.5 9.13,8.95 9.13,8.95 C9.13,8.95 9.13,11.4 9.13,11.4 C9.13,11.4 9.13,14.65 9.13,14.65 C9.13,14.65 7.18,14.2 7.18,14.2 C6.86,14.12 6.56,14.14 6.26,14.26 C5.97,14.39 5.71,14.57 5.48,14.8 C5.48,14.8 4.08,16.25 4.08,16.25 C4.08,16.25 9.23,21.4 9.23,21.4 C9.41,21.58 9.63,21.73 9.88,21.84 C10.13,21.95 10.39,22 10.68,22 C10.68,22 17.08,22 17.08,22 C17.56,22 17.99,21.85 18.38,21.54 C18.76,21.23 18.99,20.83 19.08,20.35 C19.08,20.35 19.98,14.9 19.98,14.9 C20.06,14.47 20,14.05 19.81,13.64c M17.08 20 C17.08,20 10.68,20 10.68,20 C10.68,20 6.88,16.2 6.88,16.2 C6.88,16.2 11.13,17.1 11.13,17.1 C11.13,17.1 11.13,6.5 11.13,6.5 C11.13,6.35 11.17,6.23 11.26,6.14 C11.35,6.05 11.48,6 11.63,6 C11.78,6 11.9,6.05 11.99,6.14 C12.08,6.23 12.13,6.35 12.13,6.5 C12.13,6.5 12.13,12.5 12.13,12.5 C12.13,12.5 13.88,12.5 13.88,12.5 C13.88,12.5 18.02,14.55 18.02,14.55 C18.02,14.55 17.08,20 17.08,20c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.001,0 0,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="pathData" android:duration="250" + android:startOffset="2333" + android:valueFrom="M19.81 13.64 C19.62,13.23 19.33,12.93 18.93,12.75 C18.93,12.75 15.23,10.95 15.23,10.95 C15.16,10.9 15.09,10.86 15.01,10.84 C14.99,10.83 14.96,10.83 14.94,10.83 C14.88,10.81 14.83,10.8 14.78,10.8 C14.78,10.8 14.13,10.8 14.13,10.8 C14.13,10.8 14.13,8.9 14.13,8.9 C14.13,8.9 14.13,6.5 14.13,6.5 C14.13,5.8 13.88,5.21 13.4,4.72 C12.92,4.24 12.33,4 11.63,4 C10.93,4 10.33,4.24 9.85,4.72 C9.37,5.21 9.13,5.8 9.13,6.5 C9.13,6.5 9.13,8.95 9.13,8.95 C9.13,8.95 9.13,11.4 9.13,11.4 C9.13,11.4 9.13,14.65 9.13,14.65 C9.13,14.65 7.18,14.2 7.18,14.2 C6.86,14.12 6.56,14.14 6.26,14.26 C5.97,14.39 5.71,14.57 5.48,14.8 C5.48,14.8 4.08,16.25 4.08,16.25 C4.08,16.25 9.23,21.4 9.23,21.4 C9.41,21.58 9.63,21.73 9.88,21.84 C10.13,21.95 10.39,22 10.68,22 C10.68,22 17.08,22 17.08,22 C17.56,22 17.99,21.85 18.38,21.54 C18.76,21.23 18.99,20.83 19.08,20.35 C19.08,20.35 19.98,14.9 19.98,14.9 C20.06,14.47 20,14.05 19.81,13.64c M17.08 20 C17.08,20 10.68,20 10.68,20 C10.68,20 6.88,16.2 6.88,16.2 C6.88,16.2 11.13,17.1 11.13,17.1 C11.13,17.1 11.13,6.5 11.13,6.5 C11.13,6.35 11.17,6.23 11.26,6.14 C11.35,6.05 11.48,6 11.63,6 C11.78,6 11.9,6.05 11.99,6.14 C12.08,6.23 12.13,6.35 12.13,6.5 C12.13,6.5 12.13,12.5 12.13,12.5 C12.13,12.5 13.88,12.5 13.88,12.5 C13.88,12.5 18.02,14.55 18.02,14.55 C18.02,14.55 17.08,20 17.08,20c " + android:valueTo="M19.81 13.64 C19.62,13.23 19.33,12.93 18.93,12.75 C18.93,12.75 15.23,10.95 15.23,10.95 C15.16,10.9 15.09,10.86 15.01,10.84 C14.99,10.83 14.96,10.83 14.94,10.83 C14.88,10.81 14.83,10.8 14.78,10.8 C14.78,10.8 14.13,10.8 14.13,10.8 C14.13,10.8 14.13,8.9 14.13,8.9 C14.13,8.9 14.13,8 14.13,8 C14.13,7.3 13.88,6.71 13.4,6.23 C12.92,5.74 12.33,5.5 11.63,5.5 C10.93,5.5 10.33,5.74 9.85,6.23 C9.37,6.71 9.13,7.3 9.13,8 C9.13,8 9.13,8.95 9.13,8.95 C9.13,8.95 9.13,11.4 9.13,11.4 C9.13,11.4 9.13,14.65 9.13,14.65 C9.13,14.65 7.18,14.2 7.18,14.2 C6.86,14.12 6.56,14.14 6.26,14.26 C5.97,14.39 5.71,14.57 5.48,14.8 C5.48,14.8 4.08,16.25 4.08,16.25 C4.08,16.25 9.23,21.4 9.23,21.4 C9.41,21.58 9.63,21.73 9.88,21.84 C10.13,21.95 10.39,22 10.68,22 C10.68,22 17.08,22 17.08,22 C17.56,22 17.99,21.85 18.38,21.54 C18.76,21.23 18.99,20.83 19.08,20.35 C19.08,20.35 19.98,14.9 19.98,14.9 C20.06,14.47 20,14.05 19.81,13.64c M17.08 20 C17.08,20 10.68,20 10.68,20 C10.68,20 6.88,16.2 6.88,16.2 C6.88,16.2 11.13,17.1 11.13,17.1 C11.13,17.1 11.13,8 11.13,8 C11.13,7.85 11.17,7.73 11.26,7.64 C11.35,7.55 11.48,7.5 11.63,7.5 C11.78,7.5 11.9,7.55 11.99,7.64 C12.08,7.73 12.13,7.85 12.13,8 C12.13,8 12.13,12.5 12.13,12.5 C12.13,12.5 13.88,12.5 13.88,12.5 C13.88,12.5 18.02,14.55 18.02,14.55 C18.02,14.55 17.08,20 17.08,20c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.2,0 0,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="pathData" android:duration="83" + android:startOffset="2583" + android:valueFrom="M19.81 13.64 C19.62,13.23 19.33,12.93 18.93,12.75 C18.93,12.75 15.23,10.95 15.23,10.95 C15.16,10.9 15.09,10.86 15.01,10.84 C14.99,10.83 14.96,10.83 14.94,10.83 C14.88,10.81 14.83,10.8 14.78,10.8 C14.78,10.8 14.13,10.8 14.13,10.8 C14.13,10.8 14.13,8.9 14.13,8.9 C14.13,8.9 14.13,8 14.13,8 C14.13,7.3 13.88,6.71 13.4,6.23 C12.92,5.74 12.33,5.5 11.63,5.5 C10.93,5.5 10.33,5.74 9.85,6.23 C9.37,6.71 9.13,7.3 9.13,8 C9.13,8 9.13,8.95 9.13,8.95 C9.13,8.95 9.13,11.4 9.13,11.4 C9.13,11.4 9.13,14.65 9.13,14.65 C9.13,14.65 7.18,14.2 7.18,14.2 C6.86,14.12 6.56,14.14 6.26,14.26 C5.97,14.39 5.71,14.57 5.48,14.8 C5.48,14.8 4.08,16.25 4.08,16.25 C4.08,16.25 9.23,21.4 9.23,21.4 C9.41,21.58 9.63,21.73 9.88,21.84 C10.13,21.95 10.39,22 10.68,22 C10.68,22 17.08,22 17.08,22 C17.56,22 17.99,21.85 18.38,21.54 C18.76,21.23 18.99,20.83 19.08,20.35 C19.08,20.35 19.98,14.9 19.98,14.9 C20.06,14.47 20,14.05 19.81,13.64c M17.08 20 C17.08,20 10.68,20 10.68,20 C10.68,20 6.88,16.2 6.88,16.2 C6.88,16.2 11.13,17.1 11.13,17.1 C11.13,17.1 11.13,8 11.13,8 C11.13,7.85 11.17,7.73 11.26,7.64 C11.35,7.55 11.48,7.5 11.63,7.5 C11.78,7.5 11.9,7.55 11.99,7.64 C12.08,7.73 12.13,7.85 12.13,8 C12.13,8 12.13,12.5 12.13,12.5 C12.13,12.5 13.88,12.5 13.88,12.5 C13.88,12.5 18.02,14.55 18.02,14.55 C18.02,14.55 17.08,20 17.08,20c " + android:valueTo="M19.81 13.64 C19.62,13.23 19.33,12.93 18.93,12.75 C18.93,12.75 15.23,10.95 15.23,10.95 C15.16,10.9 15.09,10.86 15.01,10.84 C14.99,10.83 14.96,10.83 14.94,10.83 C14.88,10.81 14.83,10.8 14.78,10.8 C14.78,10.8 14.13,10.8 14.13,10.8 C14.13,10.8 14.13,8.9 14.13,8.9 C14.13,8.9 14.13,6.5 14.13,6.5 C14.13,5.8 13.88,5.21 13.4,4.72 C12.92,4.24 12.33,4 11.63,4 C10.93,4 10.33,4.24 9.85,4.72 C9.37,5.21 9.13,5.8 9.13,6.5 C9.13,6.5 9.13,8.95 9.13,8.95 C9.13,8.95 9.13,11.4 9.13,11.4 C9.13,11.4 9.13,14.65 9.13,14.65 C9.13,14.65 7.18,14.2 7.18,14.2 C6.86,14.12 6.56,14.14 6.26,14.26 C5.97,14.39 5.71,14.57 5.48,14.8 C5.48,14.8 4.08,16.25 4.08,16.25 C4.08,16.25 9.23,21.4 9.23,21.4 C9.41,21.58 9.63,21.73 9.88,21.84 C10.13,21.95 10.39,22 10.68,22 C10.68,22 17.08,22 17.08,22 C17.56,22 17.99,21.85 18.38,21.54 C18.76,21.23 18.99,20.83 19.08,20.35 C19.08,20.35 19.98,14.9 19.98,14.9 C20.06,14.47 20,14.05 19.81,13.64c M17.08 20 C17.08,20 10.68,20 10.68,20 C10.68,20 6.88,16.2 6.88,16.2 C6.88,16.2 11.13,17.1 11.13,17.1 C11.13,17.1 11.13,6.5 11.13,6.5 C11.13,6.35 11.17,6.23 11.26,6.14 C11.35,6.05 11.48,6 11.63,6 C11.78,6 11.9,6.05 11.99,6.14 C12.08,6.23 12.13,6.35 12.13,6.5 C12.13,6.5 12.13,12.5 12.13,12.5 C12.13,12.5 13.88,12.5 13.88,12.5 C13.88,12.5 18.02,14.55 18.02,14.55 C18.02,14.55 17.08,20 17.08,20c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.001,0 0,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="pathData" android:duration="833" + android:startOffset="2667" + android:valueFrom="M19.81 13.64 C19.62,13.23 19.33,12.93 18.93,12.75 C18.93,12.75 15.23,10.95 15.23,10.95 C15.16,10.9 15.09,10.86 15.01,10.84 C14.99,10.83 14.96,10.83 14.94,10.83 C14.88,10.81 14.83,10.8 14.78,10.8 C14.78,10.8 14.13,10.8 14.13,10.8 C14.13,10.8 14.13,8.9 14.13,8.9 C14.13,8.9 14.13,6.5 14.13,6.5 C14.13,5.8 13.88,5.21 13.4,4.72 C12.92,4.24 12.33,4 11.63,4 C10.93,4 10.33,4.24 9.85,4.72 C9.37,5.21 9.13,5.8 9.13,6.5 C9.13,6.5 9.13,8.95 9.13,8.95 C9.13,8.95 9.13,11.4 9.13,11.4 C9.13,11.4 9.13,14.65 9.13,14.65 C9.13,14.65 7.18,14.2 7.18,14.2 C6.86,14.12 6.56,14.14 6.26,14.26 C5.97,14.39 5.71,14.57 5.48,14.8 C5.48,14.8 4.08,16.25 4.08,16.25 C4.08,16.25 9.23,21.4 9.23,21.4 C9.41,21.58 9.63,21.73 9.88,21.84 C10.13,21.95 10.39,22 10.68,22 C10.68,22 17.08,22 17.08,22 C17.56,22 17.99,21.85 18.38,21.54 C18.76,21.23 18.99,20.83 19.08,20.35 C19.08,20.35 19.98,14.9 19.98,14.9 C20.06,14.47 20,14.05 19.81,13.64c M17.08 20 C17.08,20 10.68,20 10.68,20 C10.68,20 6.88,16.2 6.88,16.2 C6.88,16.2 11.13,17.1 11.13,17.1 C11.13,17.1 11.13,6.5 11.13,6.5 C11.13,6.35 11.17,6.23 11.26,6.14 C11.35,6.05 11.48,6 11.63,6 C11.78,6 11.9,6.05 11.99,6.14 C12.08,6.23 12.13,6.35 12.13,6.5 C12.13,6.5 12.13,12.5 12.13,12.5 C12.13,12.5 13.88,12.5 13.88,12.5 C13.88,12.5 18.02,14.55 18.02,14.55 C18.02,14.55 17.08,20 17.08,20c " + android:valueTo="M19.81 13.64 C19.62,13.23 19.33,12.93 18.93,12.75 C18.93,12.75 15.23,10.95 15.23,10.95 C15.16,10.9 15.09,10.86 15.01,10.84 C14.99,10.83 14.96,10.83 14.94,10.83 C14.88,10.81 14.83,10.8 14.78,10.8 C14.78,10.8 14.13,10.8 14.13,10.8 C14.13,10.8 14.13,8.9 14.13,8.9 C14.13,8.9 14.13,6.5 14.13,6.5 C14.13,5.8 13.88,5.21 13.4,4.72 C12.92,4.24 12.33,4 11.63,4 C10.93,4 10.33,4.24 9.85,4.72 C9.37,5.21 9.13,5.8 9.13,6.5 C9.13,6.5 9.13,8.95 9.13,8.95 C9.13,8.95 9.13,11.4 9.13,11.4 C9.13,11.4 9.13,14.65 9.13,14.65 C9.13,14.65 7.18,14.2 7.18,14.2 C6.86,14.12 6.56,14.14 6.26,14.26 C5.97,14.39 5.71,14.57 5.48,14.8 C5.48,14.8 4.08,16.25 4.08,16.25 C4.08,16.25 9.23,21.4 9.23,21.4 C9.41,21.58 9.63,21.73 9.88,21.84 C10.13,21.95 10.39,22 10.68,22 C10.68,22 17.08,22 17.08,22 C17.56,22 17.99,21.85 18.38,21.54 C18.76,21.23 18.99,20.83 19.08,20.35 C19.08,20.35 19.98,14.9 19.98,14.9 C20.06,14.47 20,14.05 19.81,13.64c M17.08 20 C17.08,20 10.68,20 10.68,20 C10.68,20 6.88,16.2 6.88,16.2 C6.88,16.2 11.13,17.1 11.13,17.1 C11.13,17.1 11.13,6.5 11.13,6.5 C11.13,6.35 11.17,6.23 11.26,6.14 C11.35,6.05 11.48,6 11.63,6 C11.78,6 11.9,6.05 11.99,6.14 C12.08,6.23 12.13,6.35 12.13,6.5 C12.13,6.5 12.13,12.5 12.13,12.5 C12.13,12.5 13.88,12.5 13.88,12.5 C13.88,12.5 18.02,14.55 18.02,14.55 C18.02,14.55 17.08,20 17.08,20c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.833,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="pathData" android:duration="250" + android:startOffset="3500" + android:valueFrom="M19.81 13.64 C19.62,13.23 19.33,12.93 18.93,12.75 C18.93,12.75 15.23,10.95 15.23,10.95 C15.16,10.9 15.09,10.86 15.01,10.84 C14.99,10.83 14.96,10.83 14.94,10.83 C14.88,10.81 14.83,10.8 14.78,10.8 C14.78,10.8 14.13,10.8 14.13,10.8 C14.13,10.8 14.13,8.9 14.13,8.9 C14.13,8.9 14.13,6.5 14.13,6.5 C14.13,5.8 13.88,5.21 13.4,4.72 C12.92,4.24 12.33,4 11.63,4 C10.93,4 10.33,4.24 9.85,4.72 C9.37,5.21 9.13,5.8 9.13,6.5 C9.13,6.5 9.13,8.95 9.13,8.95 C9.13,8.95 9.13,11.4 9.13,11.4 C9.13,11.4 9.13,14.65 9.13,14.65 C9.13,14.65 7.18,14.2 7.18,14.2 C6.86,14.12 6.56,14.14 6.26,14.26 C5.97,14.39 5.71,14.57 5.48,14.8 C5.48,14.8 4.08,16.25 4.08,16.25 C4.08,16.25 9.23,21.4 9.23,21.4 C9.41,21.58 9.63,21.73 9.88,21.84 C10.13,21.95 10.39,22 10.68,22 C10.68,22 17.08,22 17.08,22 C17.56,22 17.99,21.85 18.38,21.54 C18.76,21.23 18.99,20.83 19.08,20.35 C19.08,20.35 19.98,14.9 19.98,14.9 C20.06,14.47 20,14.05 19.81,13.64c M17.08 20 C17.08,20 10.68,20 10.68,20 C10.68,20 6.88,16.2 6.88,16.2 C6.88,16.2 11.13,17.1 11.13,17.1 C11.13,17.1 11.13,6.5 11.13,6.5 C11.13,6.35 11.17,6.23 11.26,6.14 C11.35,6.05 11.48,6 11.63,6 C11.78,6 11.9,6.05 11.99,6.14 C12.08,6.23 12.13,6.35 12.13,6.5 C12.13,6.5 12.13,12.5 12.13,12.5 C12.13,12.5 13.88,12.5 13.88,12.5 C13.88,12.5 18.02,14.55 18.02,14.55 C18.02,14.55 17.08,20 17.08,20c " + android:valueTo="M19.81 13.64 C19.62,13.23 19.33,12.93 18.93,12.75 C18.93,12.75 15.23,10.95 15.23,10.95 C15.16,10.9 15.09,10.86 15.01,10.84 C14.99,10.83 14.96,10.83 14.94,10.83 C14.88,10.81 14.83,10.8 14.78,10.8 C14.78,10.8 14.13,10.8 14.13,10.8 C14.13,10.8 14.13,8.9 14.13,8.9 C14.13,8.9 14.13,8 14.13,8 C14.13,7.3 13.88,6.71 13.4,6.23 C12.92,5.74 12.33,5.5 11.63,5.5 C10.93,5.5 10.33,5.74 9.85,6.23 C9.37,6.71 9.13,7.3 9.13,8 C9.13,8 9.13,8.95 9.13,8.95 C9.13,8.95 9.13,11.4 9.13,11.4 C9.13,11.4 9.13,14.65 9.13,14.65 C9.13,14.65 7.18,14.2 7.18,14.2 C6.86,14.12 6.56,14.14 6.26,14.26 C5.97,14.39 5.71,14.57 5.48,14.8 C5.48,14.8 4.08,16.25 4.08,16.25 C4.08,16.25 9.23,21.4 9.23,21.4 C9.41,21.58 9.63,21.73 9.88,21.84 C10.13,21.95 10.39,22 10.68,22 C10.68,22 17.08,22 17.08,22 C17.56,22 17.99,21.85 18.38,21.54 C18.76,21.23 18.99,20.83 19.08,20.35 C19.08,20.35 19.98,14.9 19.98,14.9 C20.06,14.47 20,14.05 19.81,13.64c M17.08 20 C17.08,20 10.68,20 10.68,20 C10.68,20 6.88,16.2 6.88,16.2 C6.88,16.2 11.13,17.1 11.13,17.1 C11.13,17.1 11.13,8 11.13,8 C11.13,7.85 11.17,7.73 11.26,7.64 C11.35,7.55 11.48,7.5 11.63,7.5 C11.78,7.5 11.9,7.55 11.99,7.64 C12.08,7.73 12.13,7.85 12.13,8 C12.13,8 12.13,12.5 12.13,12.5 C12.13,12.5 13.88,12.5 13.88,12.5 C13.88,12.5 18.02,14.55 18.02,14.55 C18.02,14.55 17.08,20 17.08,20c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.2,0 0,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="pathData" android:duration="83" + android:startOffset="3750" + android:valueFrom="M19.81 13.64 C19.62,13.23 19.33,12.93 18.93,12.75 C18.93,12.75 15.23,10.95 15.23,10.95 C15.16,10.9 15.09,10.86 15.01,10.84 C14.99,10.83 14.96,10.83 14.94,10.83 C14.88,10.81 14.83,10.8 14.78,10.8 C14.78,10.8 14.13,10.8 14.13,10.8 C14.13,10.8 14.13,8.9 14.13,8.9 C14.13,8.9 14.13,8 14.13,8 C14.13,7.3 13.88,6.71 13.4,6.23 C12.92,5.74 12.33,5.5 11.63,5.5 C10.93,5.5 10.33,5.74 9.85,6.23 C9.37,6.71 9.13,7.3 9.13,8 C9.13,8 9.13,8.95 9.13,8.95 C9.13,8.95 9.13,11.4 9.13,11.4 C9.13,11.4 9.13,14.65 9.13,14.65 C9.13,14.65 7.18,14.2 7.18,14.2 C6.86,14.12 6.56,14.14 6.26,14.26 C5.97,14.39 5.71,14.57 5.48,14.8 C5.48,14.8 4.08,16.25 4.08,16.25 C4.08,16.25 9.23,21.4 9.23,21.4 C9.41,21.58 9.63,21.73 9.88,21.84 C10.13,21.95 10.39,22 10.68,22 C10.68,22 17.08,22 17.08,22 C17.56,22 17.99,21.85 18.38,21.54 C18.76,21.23 18.99,20.83 19.08,20.35 C19.08,20.35 19.98,14.9 19.98,14.9 C20.06,14.47 20,14.05 19.81,13.64c M17.08 20 C17.08,20 10.68,20 10.68,20 C10.68,20 6.88,16.2 6.88,16.2 C6.88,16.2 11.13,17.1 11.13,17.1 C11.13,17.1 11.13,8 11.13,8 C11.13,7.85 11.17,7.73 11.26,7.64 C11.35,7.55 11.48,7.5 11.63,7.5 C11.78,7.5 11.9,7.55 11.99,7.64 C12.08,7.73 12.13,7.85 12.13,8 C12.13,8 12.13,12.5 12.13,12.5 C12.13,12.5 13.88,12.5 13.88,12.5 C13.88,12.5 18.02,14.55 18.02,14.55 C18.02,14.55 17.08,20 17.08,20c " + android:valueTo="M19.81 13.64 C19.62,13.23 19.33,12.93 18.93,12.75 C18.93,12.75 15.23,10.95 15.23,10.95 C15.16,10.9 15.09,10.86 15.01,10.84 C14.99,10.83 14.96,10.83 14.94,10.83 C14.88,10.81 14.83,10.8 14.78,10.8 C14.78,10.8 14.13,10.8 14.13,10.8 C14.13,10.8 14.13,8.9 14.13,8.9 C14.13,8.9 14.13,6.5 14.13,6.5 C14.13,5.8 13.88,5.21 13.4,4.72 C12.92,4.24 12.33,4 11.63,4 C10.93,4 10.33,4.24 9.85,4.72 C9.37,5.21 9.13,5.8 9.13,6.5 C9.13,6.5 9.13,8.95 9.13,8.95 C9.13,8.95 9.13,11.4 9.13,11.4 C9.13,11.4 9.13,14.65 9.13,14.65 C9.13,14.65 7.18,14.2 7.18,14.2 C6.86,14.12 6.56,14.14 6.26,14.26 C5.97,14.39 5.71,14.57 5.48,14.8 C5.48,14.8 4.08,16.25 4.08,16.25 C4.08,16.25 9.23,21.4 9.23,21.4 C9.41,21.58 9.63,21.73 9.88,21.84 C10.13,21.95 10.39,22 10.68,22 C10.68,22 17.08,22 17.08,22 C17.56,22 17.99,21.85 18.38,21.54 C18.76,21.23 18.99,20.83 19.08,20.35 C19.08,20.35 19.98,14.9 19.98,14.9 C20.06,14.47 20,14.05 19.81,13.64c M17.08 20 C17.08,20 10.68,20 10.68,20 C10.68,20 6.88,16.2 6.88,16.2 C6.88,16.2 11.13,17.1 11.13,17.1 C11.13,17.1 11.13,6.5 11.13,6.5 C11.13,6.35 11.17,6.23 11.26,6.14 C11.35,6.05 11.48,6 11.63,6 C11.78,6 11.9,6.05 11.99,6.14 C12.08,6.23 12.13,6.35 12.13,6.5 C12.13,6.5 12.13,12.5 12.13,12.5 C12.13,12.5 13.88,12.5 13.88,12.5 C13.88,12.5 18.02,14.55 18.02,14.55 C18.02,14.55 17.08,20 17.08,20c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.001,0 0,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="pathData" android:duration="250" + android:startOffset="3833" + android:valueFrom="M19.81 13.64 C19.62,13.23 19.33,12.93 18.93,12.75 C18.93,12.75 15.23,10.95 15.23,10.95 C15.16,10.9 15.09,10.86 15.01,10.84 C14.99,10.83 14.96,10.83 14.94,10.83 C14.88,10.81 14.83,10.8 14.78,10.8 C14.78,10.8 14.13,10.8 14.13,10.8 C14.13,10.8 14.13,8.9 14.13,8.9 C14.13,8.9 14.13,6.5 14.13,6.5 C14.13,5.8 13.88,5.21 13.4,4.72 C12.92,4.24 12.33,4 11.63,4 C10.93,4 10.33,4.24 9.85,4.72 C9.37,5.21 9.13,5.8 9.13,6.5 C9.13,6.5 9.13,8.95 9.13,8.95 C9.13,8.95 9.13,11.4 9.13,11.4 C9.13,11.4 9.13,14.65 9.13,14.65 C9.13,14.65 7.18,14.2 7.18,14.2 C6.86,14.12 6.56,14.14 6.26,14.26 C5.97,14.39 5.71,14.57 5.48,14.8 C5.48,14.8 4.08,16.25 4.08,16.25 C4.08,16.25 9.23,21.4 9.23,21.4 C9.41,21.58 9.63,21.73 9.88,21.84 C10.13,21.95 10.39,22 10.68,22 C10.68,22 17.08,22 17.08,22 C17.56,22 17.99,21.85 18.38,21.54 C18.76,21.23 18.99,20.83 19.08,20.35 C19.08,20.35 19.98,14.9 19.98,14.9 C20.06,14.47 20,14.05 19.81,13.64c M17.08 20 C17.08,20 10.68,20 10.68,20 C10.68,20 6.88,16.2 6.88,16.2 C6.88,16.2 11.13,17.1 11.13,17.1 C11.13,17.1 11.13,6.5 11.13,6.5 C11.13,6.35 11.17,6.23 11.26,6.14 C11.35,6.05 11.48,6 11.63,6 C11.78,6 11.9,6.05 11.99,6.14 C12.08,6.23 12.13,6.35 12.13,6.5 C12.13,6.5 12.13,12.5 12.13,12.5 C12.13,12.5 13.88,12.5 13.88,12.5 C13.88,12.5 18.02,14.55 18.02,14.55 C18.02,14.55 17.08,20 17.08,20c " + android:valueTo="M19.81 13.64 C19.62,13.23 19.33,12.93 18.93,12.75 C18.93,12.75 15.23,10.95 15.23,10.95 C15.16,10.9 15.09,10.86 15.01,10.84 C14.99,10.83 14.96,10.83 14.94,10.83 C14.88,10.81 14.83,10.8 14.78,10.8 C14.78,10.8 14.13,10.8 14.13,10.8 C14.13,10.8 14.13,8.9 14.13,8.9 C14.13,8.9 14.13,8 14.13,8 C14.13,7.3 13.88,6.71 13.4,6.23 C12.92,5.74 12.33,5.5 11.63,5.5 C10.93,5.5 10.33,5.74 9.85,6.23 C9.37,6.71 9.13,7.3 9.13,8 C9.13,8 9.13,8.95 9.13,8.95 C9.13,8.95 9.13,11.4 9.13,11.4 C9.13,11.4 9.13,14.65 9.13,14.65 C9.13,14.65 7.18,14.2 7.18,14.2 C6.86,14.12 6.56,14.14 6.26,14.26 C5.97,14.39 5.71,14.57 5.48,14.8 C5.48,14.8 4.08,16.25 4.08,16.25 C4.08,16.25 9.23,21.4 9.23,21.4 C9.41,21.58 9.63,21.73 9.88,21.84 C10.13,21.95 10.39,22 10.68,22 C10.68,22 17.08,22 17.08,22 C17.56,22 17.99,21.85 18.38,21.54 C18.76,21.23 18.99,20.83 19.08,20.35 C19.08,20.35 19.98,14.9 19.98,14.9 C20.06,14.47 20,14.05 19.81,13.64c M17.08 20 C17.08,20 10.68,20 10.68,20 C10.68,20 6.88,16.2 6.88,16.2 C6.88,16.2 11.13,17.1 11.13,17.1 C11.13,17.1 11.13,8 11.13,8 C11.13,7.85 11.17,7.73 11.26,7.64 C11.35,7.55 11.48,7.5 11.63,7.5 C11.78,7.5 11.9,7.55 11.99,7.64 C12.08,7.73 12.13,7.85 12.13,8 C12.13,8 12.13,12.5 12.13,12.5 C12.13,12.5 13.88,12.5 13.88,12.5 C13.88,12.5 18.02,14.55 18.02,14.55 C18.02,14.55 17.08,20 17.08,20c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.2,0 0,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="pathData" android:duration="83" + android:startOffset="4083" + android:valueFrom="M19.81 13.64 C19.62,13.23 19.33,12.93 18.93,12.75 C18.93,12.75 15.23,10.95 15.23,10.95 C15.16,10.9 15.09,10.86 15.01,10.84 C14.99,10.83 14.96,10.83 14.94,10.83 C14.88,10.81 14.83,10.8 14.78,10.8 C14.78,10.8 14.13,10.8 14.13,10.8 C14.13,10.8 14.13,8.9 14.13,8.9 C14.13,8.9 14.13,8 14.13,8 C14.13,7.3 13.88,6.71 13.4,6.23 C12.92,5.74 12.33,5.5 11.63,5.5 C10.93,5.5 10.33,5.74 9.85,6.23 C9.37,6.71 9.13,7.3 9.13,8 C9.13,8 9.13,8.95 9.13,8.95 C9.13,8.95 9.13,11.4 9.13,11.4 C9.13,11.4 9.13,14.65 9.13,14.65 C9.13,14.65 7.18,14.2 7.18,14.2 C6.86,14.12 6.56,14.14 6.26,14.26 C5.97,14.39 5.71,14.57 5.48,14.8 C5.48,14.8 4.08,16.25 4.08,16.25 C4.08,16.25 9.23,21.4 9.23,21.4 C9.41,21.58 9.63,21.73 9.88,21.84 C10.13,21.95 10.39,22 10.68,22 C10.68,22 17.08,22 17.08,22 C17.56,22 17.99,21.85 18.38,21.54 C18.76,21.23 18.99,20.83 19.08,20.35 C19.08,20.35 19.98,14.9 19.98,14.9 C20.06,14.47 20,14.05 19.81,13.64c M17.08 20 C17.08,20 10.68,20 10.68,20 C10.68,20 6.88,16.2 6.88,16.2 C6.88,16.2 11.13,17.1 11.13,17.1 C11.13,17.1 11.13,8 11.13,8 C11.13,7.85 11.17,7.73 11.26,7.64 C11.35,7.55 11.48,7.5 11.63,7.5 C11.78,7.5 11.9,7.55 11.99,7.64 C12.08,7.73 12.13,7.85 12.13,8 C12.13,8 12.13,12.5 12.13,12.5 C12.13,12.5 13.88,12.5 13.88,12.5 C13.88,12.5 18.02,14.55 18.02,14.55 C18.02,14.55 17.08,20 17.08,20c " + android:valueTo="M19.81 13.64 C19.62,13.23 19.33,12.93 18.93,12.75 C18.93,12.75 15.23,10.95 15.23,10.95 C15.16,10.9 15.09,10.86 15.01,10.84 C14.99,10.83 14.96,10.83 14.94,10.83 C14.88,10.81 14.83,10.8 14.78,10.8 C14.78,10.8 14.13,10.8 14.13,10.8 C14.13,10.8 14.13,8.9 14.13,8.9 C14.13,8.9 14.13,6.5 14.13,6.5 C14.13,5.8 13.88,5.21 13.4,4.72 C12.92,4.24 12.33,4 11.63,4 C10.93,4 10.33,4.24 9.85,4.72 C9.37,5.21 9.13,5.8 9.13,6.5 C9.13,6.5 9.13,8.95 9.13,8.95 C9.13,8.95 9.13,11.4 9.13,11.4 C9.13,11.4 9.13,14.65 9.13,14.65 C9.13,14.65 7.18,14.2 7.18,14.2 C6.86,14.12 6.56,14.14 6.26,14.26 C5.97,14.39 5.71,14.57 5.48,14.8 C5.48,14.8 4.08,16.25 4.08,16.25 C4.08,16.25 9.23,21.4 9.23,21.4 C9.41,21.58 9.63,21.73 9.88,21.84 C10.13,21.95 10.39,22 10.68,22 C10.68,22 17.08,22 17.08,22 C17.56,22 17.99,21.85 18.38,21.54 C18.76,21.23 18.99,20.83 19.08,20.35 C19.08,20.35 19.98,14.9 19.98,14.9 C20.06,14.47 20,14.05 19.81,13.64c M17.08 20 C17.08,20 10.68,20 10.68,20 C10.68,20 6.88,16.2 6.88,16.2 C6.88,16.2 11.13,17.1 11.13,17.1 C11.13,17.1 11.13,6.5 11.13,6.5 C11.13,6.35 11.17,6.23 11.26,6.14 C11.35,6.05 11.48,6 11.63,6 C11.78,6 11.9,6.05 11.99,6.14 C12.08,6.23 12.13,6.35 12.13,6.5 C12.13,6.5 12.13,12.5 12.13,12.5 C12.13,12.5 13.88,12.5 13.88,12.5 C13.88,12.5 18.02,14.55 18.02,14.55 C18.02,14.55 17.08,20 17.08,20c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.001,0 0,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="pathData" android:duration="833" + android:startOffset="4167" + android:valueFrom="M19.81 13.64 C19.62,13.23 19.33,12.93 18.93,12.75 C18.93,12.75 15.23,10.95 15.23,10.95 C15.16,10.9 15.09,10.86 15.01,10.84 C14.99,10.83 14.96,10.83 14.94,10.83 C14.88,10.81 14.83,10.8 14.78,10.8 C14.78,10.8 14.13,10.8 14.13,10.8 C14.13,10.8 14.13,8.9 14.13,8.9 C14.13,8.9 14.13,6.5 14.13,6.5 C14.13,5.8 13.88,5.21 13.4,4.72 C12.92,4.24 12.33,4 11.63,4 C10.93,4 10.33,4.24 9.85,4.72 C9.37,5.21 9.13,5.8 9.13,6.5 C9.13,6.5 9.13,8.95 9.13,8.95 C9.13,8.95 9.13,11.4 9.13,11.4 C9.13,11.4 9.13,14.65 9.13,14.65 C9.13,14.65 7.18,14.2 7.18,14.2 C6.86,14.12 6.56,14.14 6.26,14.26 C5.97,14.39 5.71,14.57 5.48,14.8 C5.48,14.8 4.08,16.25 4.08,16.25 C4.08,16.25 9.23,21.4 9.23,21.4 C9.41,21.58 9.63,21.73 9.88,21.84 C10.13,21.95 10.39,22 10.68,22 C10.68,22 17.08,22 17.08,22 C17.56,22 17.99,21.85 18.38,21.54 C18.76,21.23 18.99,20.83 19.08,20.35 C19.08,20.35 19.98,14.9 19.98,14.9 C20.06,14.47 20,14.05 19.81,13.64c M17.08 20 C17.08,20 10.68,20 10.68,20 C10.68,20 6.88,16.2 6.88,16.2 C6.88,16.2 11.13,17.1 11.13,17.1 C11.13,17.1 11.13,6.5 11.13,6.5 C11.13,6.35 11.17,6.23 11.26,6.14 C11.35,6.05 11.48,6 11.63,6 C11.78,6 11.9,6.05 11.99,6.14 C12.08,6.23 12.13,6.35 12.13,6.5 C12.13,6.5 12.13,12.5 12.13,12.5 C12.13,12.5 13.88,12.5 13.88,12.5 C13.88,12.5 18.02,14.55 18.02,14.55 C18.02,14.55 17.08,20 17.08,20c " + android:valueTo="M19.81 13.64 C19.62,13.23 19.33,12.93 18.93,12.75 C18.93,12.75 15.23,10.95 15.23,10.95 C15.16,10.9 15.09,10.86 15.01,10.84 C14.99,10.83 14.96,10.83 14.94,10.83 C14.88,10.81 14.83,10.8 14.78,10.8 C14.78,10.8 14.13,10.8 14.13,10.8 C14.13,10.8 14.13,8.9 14.13,8.9 C14.13,8.9 14.13,6.5 14.13,6.5 C14.13,5.8 13.88,5.21 13.4,4.72 C12.92,4.24 12.33,4 11.63,4 C10.93,4 10.33,4.24 9.85,4.72 C9.37,5.21 9.13,5.8 9.13,6.5 C9.13,6.5 9.13,8.95 9.13,8.95 C9.13,8.95 9.13,11.4 9.13,11.4 C9.13,11.4 9.13,14.65 9.13,14.65 C9.13,14.65 7.18,14.2 7.18,14.2 C6.86,14.12 6.56,14.14 6.26,14.26 C5.97,14.39 5.71,14.57 5.48,14.8 C5.48,14.8 4.08,16.25 4.08,16.25 C4.08,16.25 9.23,21.4 9.23,21.4 C9.41,21.58 9.63,21.73 9.88,21.84 C10.13,21.95 10.39,22 10.68,22 C10.68,22 17.08,22 17.08,22 C17.56,22 17.99,21.85 18.38,21.54 C18.76,21.23 18.99,20.83 19.08,20.35 C19.08,20.35 19.98,14.9 19.98,14.9 C20.06,14.47 20,14.05 19.81,13.64c M17.08 20 C17.08,20 10.68,20 10.68,20 C10.68,20 6.88,16.2 6.88,16.2 C6.88,16.2 11.13,17.1 11.13,17.1 C11.13,17.1 11.13,6.5 11.13,6.5 C11.13,6.35 11.17,6.23 11.26,6.14 C11.35,6.05 11.48,6 11.63,6 C11.78,6 11.9,6.05 11.99,6.14 C12.08,6.23 12.13,6.35 12.13,6.5 C12.13,6.5 12.13,12.5 12.13,12.5 C12.13,12.5 13.88,12.5 13.88,12.5 C13.88,12.5 18.02,14.55 18.02,14.55 C18.02,14.55 17.08,20 17.08,20c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.833,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="pathData" android:duration="250" + android:startOffset="5000" + android:valueFrom="M19.81 13.64 C19.62,13.23 19.33,12.93 18.93,12.75 C18.93,12.75 15.23,10.95 15.23,10.95 C15.16,10.9 15.09,10.86 15.01,10.84 C14.99,10.83 14.96,10.83 14.94,10.83 C14.88,10.81 14.83,10.8 14.78,10.8 C14.78,10.8 14.13,10.8 14.13,10.8 C14.13,10.8 14.13,8.9 14.13,8.9 C14.13,8.9 14.13,6.5 14.13,6.5 C14.13,5.8 13.88,5.21 13.4,4.72 C12.92,4.24 12.33,4 11.63,4 C10.93,4 10.33,4.24 9.85,4.72 C9.37,5.21 9.13,5.8 9.13,6.5 C9.13,6.5 9.13,8.95 9.13,8.95 C9.13,8.95 9.13,11.4 9.13,11.4 C9.13,11.4 9.13,14.65 9.13,14.65 C9.13,14.65 7.18,14.2 7.18,14.2 C6.86,14.12 6.56,14.14 6.26,14.26 C5.97,14.39 5.71,14.57 5.48,14.8 C5.48,14.8 4.08,16.25 4.08,16.25 C4.08,16.25 9.23,21.4 9.23,21.4 C9.41,21.58 9.63,21.73 9.88,21.84 C10.13,21.95 10.39,22 10.68,22 C10.68,22 17.08,22 17.08,22 C17.56,22 17.99,21.85 18.38,21.54 C18.76,21.23 18.99,20.83 19.08,20.35 C19.08,20.35 19.98,14.9 19.98,14.9 C20.06,14.47 20,14.05 19.81,13.64c M17.08 20 C17.08,20 10.68,20 10.68,20 C10.68,20 6.88,16.2 6.88,16.2 C6.88,16.2 11.13,17.1 11.13,17.1 C11.13,17.1 11.13,6.5 11.13,6.5 C11.13,6.35 11.17,6.23 11.26,6.14 C11.35,6.05 11.48,6 11.63,6 C11.78,6 11.9,6.05 11.99,6.14 C12.08,6.23 12.13,6.35 12.13,6.5 C12.13,6.5 12.13,12.5 12.13,12.5 C12.13,12.5 13.88,12.5 13.88,12.5 C13.88,12.5 18.02,14.55 18.02,14.55 C18.02,14.55 17.08,20 17.08,20c " + android:valueTo="M19.81 13.64 C19.62,13.23 19.33,12.93 18.93,12.75 C18.93,12.75 15.23,10.95 15.23,10.95 C15.16,10.9 15.09,10.86 15.01,10.84 C14.99,10.83 14.96,10.83 14.94,10.83 C14.88,10.81 14.83,10.8 14.78,10.8 C14.78,10.8 14.13,10.8 14.13,10.8 C14.13,10.8 14.13,8.9 14.13,8.9 C14.13,8.9 14.13,8 14.13,8 C14.13,7.3 13.88,6.71 13.4,6.23 C12.92,5.74 12.33,5.5 11.63,5.5 C10.93,5.5 10.33,5.74 9.85,6.23 C9.37,6.71 9.13,7.3 9.13,8 C9.13,8 9.13,8.95 9.13,8.95 C9.13,8.95 9.13,11.4 9.13,11.4 C9.13,11.4 9.13,14.65 9.13,14.65 C9.13,14.65 7.18,14.2 7.18,14.2 C6.86,14.12 6.56,14.14 6.26,14.26 C5.97,14.39 5.71,14.57 5.48,14.8 C5.48,14.8 4.08,16.25 4.08,16.25 C4.08,16.25 9.23,21.4 9.23,21.4 C9.41,21.58 9.63,21.73 9.88,21.84 C10.13,21.95 10.39,22 10.68,22 C10.68,22 17.08,22 17.08,22 C17.56,22 17.99,21.85 18.38,21.54 C18.76,21.23 18.99,20.83 19.08,20.35 C19.08,20.35 19.98,14.9 19.98,14.9 C20.06,14.47 20,14.05 19.81,13.64c M17.08 20 C17.08,20 10.68,20 10.68,20 C10.68,20 6.88,16.2 6.88,16.2 C6.88,16.2 11.13,17.1 11.13,17.1 C11.13,17.1 11.13,8 11.13,8 C11.13,7.85 11.17,7.73 11.26,7.64 C11.35,7.55 11.48,7.5 11.63,7.5 C11.78,7.5 11.9,7.55 11.99,7.64 C12.08,7.73 12.13,7.85 12.13,8 C12.13,8 12.13,12.5 12.13,12.5 C12.13,12.5 13.88,12.5 13.88,12.5 C13.88,12.5 18.02,14.55 18.02,14.55 C18.02,14.55 17.08,20 17.08,20c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.2,0 0,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="pathData" android:duration="83" + android:startOffset="5250" + android:valueFrom="M19.81 13.64 C19.62,13.23 19.33,12.93 18.93,12.75 C18.93,12.75 15.23,10.95 15.23,10.95 C15.16,10.9 15.09,10.86 15.01,10.84 C14.99,10.83 14.96,10.83 14.94,10.83 C14.88,10.81 14.83,10.8 14.78,10.8 C14.78,10.8 14.13,10.8 14.13,10.8 C14.13,10.8 14.13,8.9 14.13,8.9 C14.13,8.9 14.13,8 14.13,8 C14.13,7.3 13.88,6.71 13.4,6.23 C12.92,5.74 12.33,5.5 11.63,5.5 C10.93,5.5 10.33,5.74 9.85,6.23 C9.37,6.71 9.13,7.3 9.13,8 C9.13,8 9.13,8.95 9.13,8.95 C9.13,8.95 9.13,11.4 9.13,11.4 C9.13,11.4 9.13,14.65 9.13,14.65 C9.13,14.65 7.18,14.2 7.18,14.2 C6.86,14.12 6.56,14.14 6.26,14.26 C5.97,14.39 5.71,14.57 5.48,14.8 C5.48,14.8 4.08,16.25 4.08,16.25 C4.08,16.25 9.23,21.4 9.23,21.4 C9.41,21.58 9.63,21.73 9.88,21.84 C10.13,21.95 10.39,22 10.68,22 C10.68,22 17.08,22 17.08,22 C17.56,22 17.99,21.85 18.38,21.54 C18.76,21.23 18.99,20.83 19.08,20.35 C19.08,20.35 19.98,14.9 19.98,14.9 C20.06,14.47 20,14.05 19.81,13.64c M17.08 20 C17.08,20 10.68,20 10.68,20 C10.68,20 6.88,16.2 6.88,16.2 C6.88,16.2 11.13,17.1 11.13,17.1 C11.13,17.1 11.13,8 11.13,8 C11.13,7.85 11.17,7.73 11.26,7.64 C11.35,7.55 11.48,7.5 11.63,7.5 C11.78,7.5 11.9,7.55 11.99,7.64 C12.08,7.73 12.13,7.85 12.13,8 C12.13,8 12.13,12.5 12.13,12.5 C12.13,12.5 13.88,12.5 13.88,12.5 C13.88,12.5 18.02,14.55 18.02,14.55 C18.02,14.55 17.08,20 17.08,20c " + android:valueTo="M19.81 13.64 C19.62,13.23 19.33,12.93 18.93,12.75 C18.93,12.75 15.23,10.95 15.23,10.95 C15.16,10.9 15.09,10.86 15.01,10.84 C14.99,10.83 14.96,10.83 14.94,10.83 C14.88,10.81 14.83,10.8 14.78,10.8 C14.78,10.8 14.13,10.8 14.13,10.8 C14.13,10.8 14.13,8.9 14.13,8.9 C14.13,8.9 14.13,6.5 14.13,6.5 C14.13,5.8 13.88,5.21 13.4,4.72 C12.92,4.24 12.33,4 11.63,4 C10.93,4 10.33,4.24 9.85,4.72 C9.37,5.21 9.13,5.8 9.13,6.5 C9.13,6.5 9.13,8.95 9.13,8.95 C9.13,8.95 9.13,11.4 9.13,11.4 C9.13,11.4 9.13,14.65 9.13,14.65 C9.13,14.65 7.18,14.2 7.18,14.2 C6.86,14.12 6.56,14.14 6.26,14.26 C5.97,14.39 5.71,14.57 5.48,14.8 C5.48,14.8 4.08,16.25 4.08,16.25 C4.08,16.25 9.23,21.4 9.23,21.4 C9.41,21.58 9.63,21.73 9.88,21.84 C10.13,21.95 10.39,22 10.68,22 C10.68,22 17.08,22 17.08,22 C17.56,22 17.99,21.85 18.38,21.54 C18.76,21.23 18.99,20.83 19.08,20.35 C19.08,20.35 19.98,14.9 19.98,14.9 C20.06,14.47 20,14.05 19.81,13.64c M17.08 20 C17.08,20 10.68,20 10.68,20 C10.68,20 6.88,16.2 6.88,16.2 C6.88,16.2 11.13,17.1 11.13,17.1 C11.13,17.1 11.13,6.5 11.13,6.5 C11.13,6.35 11.17,6.23 11.26,6.14 C11.35,6.05 11.48,6 11.63,6 C11.78,6 11.9,6.05 11.99,6.14 C12.08,6.23 12.13,6.35 12.13,6.5 C12.13,6.5 12.13,12.5 12.13,12.5 C12.13,12.5 13.88,12.5 13.88,12.5 C13.88,12.5 18.02,14.55 18.02,14.55 C18.02,14.55 17.08,20 17.08,20c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.001,0 0,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="pathData" android:duration="250" + android:startOffset="5333" + android:valueFrom="M19.81 13.64 C19.62,13.23 19.33,12.93 18.93,12.75 C18.93,12.75 15.23,10.95 15.23,10.95 C15.16,10.9 15.09,10.86 15.01,10.84 C14.99,10.83 14.96,10.83 14.94,10.83 C14.88,10.81 14.83,10.8 14.78,10.8 C14.78,10.8 14.13,10.8 14.13,10.8 C14.13,10.8 14.13,8.9 14.13,8.9 C14.13,8.9 14.13,6.5 14.13,6.5 C14.13,5.8 13.88,5.21 13.4,4.72 C12.92,4.24 12.33,4 11.63,4 C10.93,4 10.33,4.24 9.85,4.72 C9.37,5.21 9.13,5.8 9.13,6.5 C9.13,6.5 9.13,8.95 9.13,8.95 C9.13,8.95 9.13,11.4 9.13,11.4 C9.13,11.4 9.13,14.65 9.13,14.65 C9.13,14.65 7.18,14.2 7.18,14.2 C6.86,14.12 6.56,14.14 6.26,14.26 C5.97,14.39 5.71,14.57 5.48,14.8 C5.48,14.8 4.08,16.25 4.08,16.25 C4.08,16.25 9.23,21.4 9.23,21.4 C9.41,21.58 9.63,21.73 9.88,21.84 C10.13,21.95 10.39,22 10.68,22 C10.68,22 17.08,22 17.08,22 C17.56,22 17.99,21.85 18.38,21.54 C18.76,21.23 18.99,20.83 19.08,20.35 C19.08,20.35 19.98,14.9 19.98,14.9 C20.06,14.47 20,14.05 19.81,13.64c M17.08 20 C17.08,20 10.68,20 10.68,20 C10.68,20 6.88,16.2 6.88,16.2 C6.88,16.2 11.13,17.1 11.13,17.1 C11.13,17.1 11.13,6.5 11.13,6.5 C11.13,6.35 11.17,6.23 11.26,6.14 C11.35,6.05 11.48,6 11.63,6 C11.78,6 11.9,6.05 11.99,6.14 C12.08,6.23 12.13,6.35 12.13,6.5 C12.13,6.5 12.13,12.5 12.13,12.5 C12.13,12.5 13.88,12.5 13.88,12.5 C13.88,12.5 18.02,14.55 18.02,14.55 C18.02,14.55 17.08,20 17.08,20c " + android:valueTo="M19.81 13.64 C19.62,13.23 19.33,12.93 18.93,12.75 C18.93,12.75 15.23,10.95 15.23,10.95 C15.16,10.9 15.09,10.86 15.01,10.84 C14.99,10.83 14.96,10.83 14.94,10.83 C14.88,10.81 14.83,10.8 14.78,10.8 C14.78,10.8 14.13,10.8 14.13,10.8 C14.13,10.8 14.13,8.9 14.13,8.9 C14.13,8.9 14.13,8 14.13,8 C14.13,7.3 13.88,6.71 13.4,6.23 C12.92,5.74 12.33,5.5 11.63,5.5 C10.93,5.5 10.33,5.74 9.85,6.23 C9.37,6.71 9.13,7.3 9.13,8 C9.13,8 9.13,8.95 9.13,8.95 C9.13,8.95 9.13,11.4 9.13,11.4 C9.13,11.4 9.13,14.65 9.13,14.65 C9.13,14.65 7.18,14.2 7.18,14.2 C6.86,14.12 6.56,14.14 6.26,14.26 C5.97,14.39 5.71,14.57 5.48,14.8 C5.48,14.8 4.08,16.25 4.08,16.25 C4.08,16.25 9.23,21.4 9.23,21.4 C9.41,21.58 9.63,21.73 9.88,21.84 C10.13,21.95 10.39,22 10.68,22 C10.68,22 17.08,22 17.08,22 C17.56,22 17.99,21.85 18.38,21.54 C18.76,21.23 18.99,20.83 19.08,20.35 C19.08,20.35 19.98,14.9 19.98,14.9 C20.06,14.47 20,14.05 19.81,13.64c M17.08 20 C17.08,20 10.68,20 10.68,20 C10.68,20 6.88,16.2 6.88,16.2 C6.88,16.2 11.13,17.1 11.13,17.1 C11.13,17.1 11.13,8 11.13,8 C11.13,7.85 11.17,7.73 11.26,7.64 C11.35,7.55 11.48,7.5 11.63,7.5 C11.78,7.5 11.9,7.55 11.99,7.64 C12.08,7.73 12.13,7.85 12.13,8 C12.13,8 12.13,12.5 12.13,12.5 C12.13,12.5 13.88,12.5 13.88,12.5 C13.88,12.5 18.02,14.55 18.02,14.55 C18.02,14.55 17.08,20 17.08,20c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.2,0 0,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="pathData" android:duration="83" + android:startOffset="5583" + android:valueFrom="M19.81 13.64 C19.62,13.23 19.33,12.93 18.93,12.75 C18.93,12.75 15.23,10.95 15.23,10.95 C15.16,10.9 15.09,10.86 15.01,10.84 C14.99,10.83 14.96,10.83 14.94,10.83 C14.88,10.81 14.83,10.8 14.78,10.8 C14.78,10.8 14.13,10.8 14.13,10.8 C14.13,10.8 14.13,8.9 14.13,8.9 C14.13,8.9 14.13,8 14.13,8 C14.13,7.3 13.88,6.71 13.4,6.23 C12.92,5.74 12.33,5.5 11.63,5.5 C10.93,5.5 10.33,5.74 9.85,6.23 C9.37,6.71 9.13,7.3 9.13,8 C9.13,8 9.13,8.95 9.13,8.95 C9.13,8.95 9.13,11.4 9.13,11.4 C9.13,11.4 9.13,14.65 9.13,14.65 C9.13,14.65 7.18,14.2 7.18,14.2 C6.86,14.12 6.56,14.14 6.26,14.26 C5.97,14.39 5.71,14.57 5.48,14.8 C5.48,14.8 4.08,16.25 4.08,16.25 C4.08,16.25 9.23,21.4 9.23,21.4 C9.41,21.58 9.63,21.73 9.88,21.84 C10.13,21.95 10.39,22 10.68,22 C10.68,22 17.08,22 17.08,22 C17.56,22 17.99,21.85 18.38,21.54 C18.76,21.23 18.99,20.83 19.08,20.35 C19.08,20.35 19.98,14.9 19.98,14.9 C20.06,14.47 20,14.05 19.81,13.64c M17.08 20 C17.08,20 10.68,20 10.68,20 C10.68,20 6.88,16.2 6.88,16.2 C6.88,16.2 11.13,17.1 11.13,17.1 C11.13,17.1 11.13,8 11.13,8 C11.13,7.85 11.17,7.73 11.26,7.64 C11.35,7.55 11.48,7.5 11.63,7.5 C11.78,7.5 11.9,7.55 11.99,7.64 C12.08,7.73 12.13,7.85 12.13,8 C12.13,8 12.13,12.5 12.13,12.5 C12.13,12.5 13.88,12.5 13.88,12.5 C13.88,12.5 18.02,14.55 18.02,14.55 C18.02,14.55 17.08,20 17.08,20c " + android:valueTo="M19.81 13.64 C19.62,13.23 19.33,12.93 18.93,12.75 C18.93,12.75 15.23,10.95 15.23,10.95 C15.16,10.9 15.09,10.86 15.01,10.84 C14.99,10.83 14.96,10.83 14.94,10.83 C14.88,10.81 14.83,10.8 14.78,10.8 C14.78,10.8 14.13,10.8 14.13,10.8 C14.13,10.8 14.13,8.9 14.13,8.9 C14.13,8.9 14.13,6.5 14.13,6.5 C14.13,5.8 13.88,5.21 13.4,4.72 C12.92,4.24 12.33,4 11.63,4 C10.93,4 10.33,4.24 9.85,4.72 C9.37,5.21 9.13,5.8 9.13,6.5 C9.13,6.5 9.13,8.95 9.13,8.95 C9.13,8.95 9.13,11.4 9.13,11.4 C9.13,11.4 9.13,14.65 9.13,14.65 C9.13,14.65 7.18,14.2 7.18,14.2 C6.86,14.12 6.56,14.14 6.26,14.26 C5.97,14.39 5.71,14.57 5.48,14.8 C5.48,14.8 4.08,16.25 4.08,16.25 C4.08,16.25 9.23,21.4 9.23,21.4 C9.41,21.58 9.63,21.73 9.88,21.84 C10.13,21.95 10.39,22 10.68,22 C10.68,22 17.08,22 17.08,22 C17.56,22 17.99,21.85 18.38,21.54 C18.76,21.23 18.99,20.83 19.08,20.35 C19.08,20.35 19.98,14.9 19.98,14.9 C20.06,14.47 20,14.05 19.81,13.64c M17.08 20 C17.08,20 10.68,20 10.68,20 C10.68,20 6.88,16.2 6.88,16.2 C6.88,16.2 11.13,17.1 11.13,17.1 C11.13,17.1 11.13,6.5 11.13,6.5 C11.13,6.35 11.17,6.23 11.26,6.14 C11.35,6.05 11.48,6 11.63,6 C11.78,6 11.9,6.05 11.99,6.14 C12.08,6.23 12.13,6.35 12.13,6.5 C12.13,6.5 12.13,12.5 12.13,12.5 C12.13,12.5 13.88,12.5 13.88,12.5 C13.88,12.5 18.02,14.55 18.02,14.55 C18.02,14.55 17.08,20 17.08,20c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.001,0 0,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="time_group"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator android:propertyName="translateX" android:duration="6000" + android:startOffset="0" android:valueFrom="0" android:valueTo="1" + android:valueType="floatType"/> + </set> + </aapt:attr> + </target> +</animated-vector> diff --git a/libs/WindowManager/Shell/res/drawable/size_compat_restart_button.xml b/libs/WindowManager/Shell/res/drawable/size_compat_restart_button.xml index e6ae28207970..b3f8e801bac4 100644 --- a/libs/WindowManager/Shell/res/drawable/size_compat_restart_button.xml +++ b/libs/WindowManager/Shell/res/drawable/size_compat_restart_button.xml @@ -1,19 +1,19 @@ <?xml version="1.0" encoding="utf-8"?> <!-- - 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. ---> + ~ 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="48dp" android:height="48dp" @@ -29,9 +29,6 @@ android:translateY="12"> <path android:fillColor="@color/compat_controls_text" - android:pathData="M6,13c0,-1.65 0.67,-3.15 1.76,-4.24L6.34,7.34C4.9,8.79 4,10.79 4,13c0,4.08 3.05,7.44 7,7.93v-2.02C8.17,18.43 6,15.97 6,13z"/> - <path - android:fillColor="@color/compat_controls_text" - android:pathData="M20,13c0,-4.42 -3.58,-8 -8,-8c-0.06,0 -0.12,0.01 -0.18,0.01v0l1.09,-1.09L11.5,2.5L8,6l3.5,3.5l1.41,-1.41l-1.08,-1.08C11.89,7.01 11.95,7 12,7c3.31,0 6,2.69 6,6c0,2.97 -2.17,5.43 -5,5.91v2.02C16.95,20.44 20,17.08 20,13z"/> + android:pathData="M3,21V15H5V17.6L8.1,14.5L9.5,15.9L6.4,19H9V21ZM15,21V19H17.6L14.5,15.9L15.9,14.5L19,17.6V15H21V21ZM8.1,9.5 L5,6.4V9H3V3H9V5H6.4L9.5,8.1ZM15.9,9.5 L14.5,8.1 17.6,5H15V3H21V9H19V6.4Z"/> </group> </vector> 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/letterbox_education_dismiss_button_background.xml b/libs/WindowManager/Shell/res/drawable/tv_window_button_bg.xml index 0d8811357c05..4c28e519afa7 100644 --- a/libs/WindowManager/Shell/res/drawable/letterbox_education_dismiss_button_background.xml +++ b/libs/WindowManager/Shell/res/drawable/tv_window_button_bg.xml @@ -15,7 +15,7 @@ ~ limitations under the License. --> <shape xmlns:android="http://schemas.android.com/apk/res/android" - android:shape="rectangle"> - <solid android:color="@color/letterbox_education_accent_primary"/> - <corners android:radius="12dp"/> + android:shape="rectangle"> + <corners android:radius="@dimen/tv_window_menu_button_radius" /> + <solid android:color="@android:color/white" /> </shape>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/layout/bubble_manage_menu.xml b/libs/WindowManager/Shell/res/layout/bubble_manage_menu.xml index 298ad3025b00..8d1da0f7ad1b 100644 --- a/libs/WindowManager/Shell/res/layout/bubble_manage_menu.xml +++ b/libs/WindowManager/Shell/res/layout/bubble_manage_menu.xml @@ -63,11 +63,11 @@ android:tint="@color/bubbles_icon_tint"/> <TextView + android:id="@+id/bubble_manage_menu_dont_bubble_text" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="16dp" - android:textAppearance="@*android:style/TextAppearance.DeviceDefault" - android:text="@string/bubbles_dont_bubble_conversation" /> + android:textAppearance="@*android:style/TextAppearance.DeviceDefault" /> </LinearLayout> 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_decor.xml b/libs/WindowManager/Shell/res/layout/caption_window_decor.xml new file mode 100644 index 000000000000..f3d219872001 --- /dev/null +++ b/libs/WindowManager/Shell/res/layout/caption_window_decor.xml @@ -0,0 +1,56 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2023 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<com.android.wm.shell.windowdecor.WindowDecorLinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/caption" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:gravity="end" + android:background="@drawable/caption_decor_title"> + <Button + style="@style/CaptionButtonStyle" + android:id="@+id/back_button" + android:layout_gravity="center_vertical|end" + android:contentDescription="@string/back_button_text" + android:background="@drawable/decor_back_button_dark" + android:duplicateParentState="true"/> + <Space + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:layout_weight="1" + android:elevation="2dp"/> + <Button + style="@style/CaptionButtonStyle" + android:id="@+id/minimize_window" + android:layout_gravity="center_vertical|end" + android:contentDescription="@string/minimize_button_text" + android:background="@drawable/decor_minimize_button_dark" + android:duplicateParentState="true"/> + <Button + style="@style/CaptionButtonStyle" + android:id="@+id/maximize_window" + android:layout_gravity="center_vertical|end" + android:contentDescription="@string/maximize_button_text" + android:background="@drawable/decor_maximize_button_dark" + android:duplicateParentState="true"/> + <Button + style="@style/CaptionButtonStyle" + android:id="@+id/close_window" + android:contentDescription="@string/close_button_text" + android:background="@drawable/decor_close_button_dark" + android:duplicateParentState="true"/> +</com.android.wm.shell.windowdecor.WindowDecorLinearLayout>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/layout/compat_mode_hint.xml b/libs/WindowManager/Shell/res/layout/compat_mode_hint.xml index 44b2f45052ba..3d3c00381164 100644 --- a/libs/WindowManager/Shell/res/layout/compat_mode_hint.xml +++ b/libs/WindowManager/Shell/res/layout/compat_mode_hint.xml @@ -29,11 +29,15 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:lineSpacingExtra="4sp" + android:letterSpacing="0.02" android:background="@drawable/compat_hint_bubble" android:padding="16dp" android:textAlignment="viewStart" android:textColor="@color/compat_controls_text" - android:textSize="14sp"/> + android:textSize="14sp" + android:fontFamily="@*android:string/config_bodyFontFamily" + android:textAppearance="@*android:style/TextAppearance.DeviceDefault.Subhead" + /> <ImageView android:layout_width="wrap_content" diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_app_controls_window_decor.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_app_controls_window_decor.xml new file mode 100644 index 000000000000..f6b21bad63f4 --- /dev/null +++ b/libs/WindowManager/Shell/res/layout/desktop_mode_app_controls_window_decor.xml @@ -0,0 +1,93 @@ +<?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" + xmlns:tools="http://schemas.android.com/tools" + android:id="@+id/desktop_mode_caption" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:gravity="center_horizontal" + android:orientation="horizontal" + android:background="@drawable/desktop_mode_decor_title"> + + <LinearLayout + android:id="@+id/open_menu_button" + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:orientation="horizontal" + android:clickable="true" + android:focusable="true" + android:paddingStart="8dp" + android:background="?android:selectableItemBackgroundBorderless"> + + <ImageView + android:id="@+id/application_icon" + android:layout_width="24dp" + android:layout_height="24dp" + android:layout_margin="4dp" + android:layout_gravity="center_vertical" + android:contentDescription="@string/app_icon_text" /> + + <TextView + android:id="@+id/application_name" + android:layout_width="0dp" + android:layout_height="match_parent" + android:minWidth="80dp" + android:textColor="@color/desktop_mode_caption_app_name_dark" + android:textSize="14sp" + android:textFontWeight="500" + android:gravity="center_vertical" + android:layout_weight="1" + android:paddingStart="4dp" + android:paddingEnd="4dp" + tools:text="Gmail"/> + + <ImageButton + android:id="@+id/expand_menu_button" + android:layout_width="32dp" + android:layout_height="32dp" + android:padding="4dp" + android:contentDescription="@string/expand_menu_text" + android:src="@drawable/ic_baseline_expand_more_24" + android:tint="@color/desktop_mode_caption_expand_button_dark" + android:background="@null" + android:scaleType="fitCenter" + android:clickable="false" + android:focusable="false" + android:layout_gravity="center_vertical"/> + + </LinearLayout> + + <View + android:id="@+id/caption_handle" + android:layout_width="wrap_content" + android:layout_height="40dp" + android:layout_weight="1"/> + + <ImageButton + android:id="@+id/close_window" + android:layout_width="40dp" + android:layout_height="40dp" + android:padding="4dp" + android:layout_marginEnd="8dp" + android:contentDescription="@string/close_button_text" + android:src="@drawable/decor_close_button_dark" + android:scaleType="fitCenter" + android:gravity="end" + android:background="?android:selectableItemBackgroundBorderless" + android:tint="@color/desktop_mode_caption_close_button_dark"/> +</com.android.wm.shell.windowdecor.WindowDecorLinearLayout>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_focused_window_decor.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_focused_window_decor.xml new file mode 100644 index 000000000000..1d6864c152c2 --- /dev/null +++ b/libs/WindowManager/Shell/res/layout/desktop_mode_focused_window_decor.xml @@ -0,0 +1,37 @@ +<?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" + xmlns:tools="http://schemas.android.com/tools" + android:id="@+id/desktop_mode_caption" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:gravity="center_horizontal" + android:background="@drawable/desktop_mode_decor_title"> + + <ImageButton + android:id="@+id/caption_handle" + android:layout_width="176dp" + android:layout_height="42dp" + android:paddingHorizontal="24dp" + android:contentDescription="@string/handle_text" + android:src="@drawable/decor_handle_dark" + tools:tint="@color/desktop_mode_caption_handle_bar_dark" + android:scaleType="fitXY" + android:background="?android:selectableItemBackground"/> + +</com.android.wm.shell.windowdecor.WindowDecorLinearLayout>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu_app_info_pill.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu_app_info_pill.xml new file mode 100644 index 000000000000..167a003932d6 --- /dev/null +++ b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu_app_info_pill.xml @@ -0,0 +1,57 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2023 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="@dimen/desktop_mode_handle_menu_width" + android:layout_height="@dimen/desktop_mode_handle_menu_app_info_pill_height" + android:orientation="horizontal" + android:background="@drawable/desktop_mode_decor_menu_background" + android:gravity="center_vertical"> + + <ImageView + android:id="@+id/application_icon" + android:layout_width="24dp" + android:layout_height="24dp" + android:layout_marginStart="14dp" + android:layout_marginEnd="14dp" + android:contentDescription="@string/app_icon_text"/> + + <TextView + android:id="@+id/application_name" + android:layout_width="0dp" + android:layout_height="wrap_content" + tools:text="Gmail" + android:textColor="@color/desktop_mode_caption_menu_text_color" + android:textSize="14sp" + android:textFontWeight="500" + android:lineHeight="20dp" + android:textStyle="normal" + android:layout_weight="1"/> + + <ImageButton + android:id="@+id/collapse_menu_button" + android:layout_width="32dp" + android:layout_height="32dp" + android:padding="4dp" + android:layout_marginEnd="14dp" + android:layout_marginStart="14dp" + android:contentDescription="@string/collapse_menu_text" + android:src="@drawable/ic_baseline_expand_more_24" + android:rotation="180" + android:tint="@color/desktop_mode_caption_menu_buttons_color_inactive" + android:background="?android:selectableItemBackgroundBorderless"/> +</LinearLayout>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu_more_actions_pill.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu_more_actions_pill.xml new file mode 100644 index 000000000000..40a4b53f3e1d --- /dev/null +++ b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu_more_actions_pill.xml @@ -0,0 +1,47 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2023 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="@dimen/desktop_mode_handle_menu_width" + android:layout_height="@dimen/desktop_mode_handle_menu_more_actions_pill_height" + android:orientation="vertical" + android:background="@drawable/desktop_mode_decor_menu_background"> + + <Button + android:id="@+id/screenshot_button" + android:contentDescription="@string/screenshot_text" + android:text="@string/screenshot_text" + android:drawableStart="@drawable/desktop_mode_ic_handle_menu_screenshot" + android:drawableTint="@color/desktop_mode_caption_menu_buttons_color_inactive" + style="@style/DesktopModeHandleMenuActionButton"/> + + <Button + android:id="@+id/select_button" + android:contentDescription="@string/select_text" + android:text="@string/select_text" + android:drawableStart="@drawable/desktop_mode_ic_handle_menu_select" + android:drawableTint="@color/desktop_mode_caption_menu_buttons_color_inactive" + style="@style/DesktopModeHandleMenuActionButton"/> + + <Button + android:id="@+id/close_button" + android:contentDescription="@string/close_text" + android:text="@string/close_text" + android:drawableStart="@drawable/desktop_mode_ic_handle_menu_close" + android:drawableTint="@color/desktop_mode_caption_menu_buttons_color_inactive" + style="@style/DesktopModeHandleMenuActionButton"/> + +</LinearLayout>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu_windowing_pill.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu_windowing_pill.xml new file mode 100644 index 000000000000..95283b9e214a --- /dev/null +++ b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu_windowing_pill.xml @@ -0,0 +1,62 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2023 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="@dimen/desktop_mode_handle_menu_width" + android:layout_height="@dimen/desktop_mode_handle_menu_windowing_pill_height" + android:orientation="horizontal" + android:background="@drawable/desktop_mode_decor_menu_background" + android:gravity="center_vertical"> + + <ImageButton + android:id="@+id/fullscreen_button" + android:layout_marginEnd="4dp" + android:contentDescription="@string/fullscreen_text" + android:src="@drawable/desktop_mode_ic_handle_menu_fullscreen" + android:tint="@color/desktop_mode_caption_menu_buttons_color_inactive" + android:layout_weight="1" + style="@style/DesktopModeHandleMenuWindowingButton"/> + + <ImageButton + android:id="@+id/split_screen_button" + android:layout_marginStart="4dp" + android:layout_marginEnd="4dp" + android:contentDescription="@string/split_screen_text" + android:src="@drawable/desktop_mode_ic_handle_menu_splitscreen" + android:tint="@color/desktop_mode_caption_menu_buttons_color_inactive" + android:layout_weight="1" + style="@style/DesktopModeHandleMenuWindowingButton"/> + + <ImageButton + android:id="@+id/floating_button" + android:layout_marginStart="4dp" + android:layout_marginEnd="4dp" + android:contentDescription="@string/float_button_text" + android:src="@drawable/desktop_mode_ic_handle_menu_floating" + android:tint="@color/desktop_mode_caption_menu_buttons_color_inactive" + android:layout_weight="1" + style="@style/DesktopModeHandleMenuWindowingButton"/> + + <ImageButton + android:id="@+id/desktop_button" + android:layout_marginStart="4dp" + android:contentDescription="@string/desktop_text" + android:src="@drawable/desktop_mode_ic_handle_menu_desktop" + android:tint="@color/desktop_mode_caption_menu_buttons_color_active" + android:layout_weight="1" + style="@style/DesktopModeHandleMenuWindowingButton"/> + +</LinearLayout>
\ No newline at end of file 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..095576b581df 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,9 +26,11 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" - android:layout_marginBottom="12dp"/> + android:layout_marginBottom="20dp"/> <TextView + android:fontFamily="@*android:string/config_bodyFontFamily" + android:textAppearance="@*android:style/TextAppearance.DeviceDefault.Body2" android:id="@+id/letterbox_education_dialog_action_text" android:layout_width="match_parent" android:layout_height="wrap_content" 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..a993469aaccf 100644 --- a/libs/WindowManager/Shell/res/layout/letterbox_education_dialog_layout.xml +++ b/libs/WindowManager/Shell/res/layout/letterbox_education_dialog_layout.xml @@ -1,5 +1,5 @@ <!-- - ~ Copyright (C) 2022 The Android Open Source Project + ~ Copyright (C) 2023 The Android Open Source Project ~ ~ Licensed under the Apache License, Version 2.0 (the "License"); ~ you may not use this file except in compliance with the License. @@ -13,12 +13,10 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License. --> -<com.android.wm.shell.compatui.letterboxedu.LetterboxEduDialogLayout +<com.android.wm.shell.compatui.LetterboxEduDialogLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:background="@android:color/system_neutral1_900"> + style="@style/LetterboxDialog"> <!-- The background of the top-level layout acts as the background dim. --> @@ -50,13 +48,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" @@ -66,18 +67,10 @@ android:text="@string/letterbox_education_dialog_title" android:textAlignment="center" android:textColor="@color/compat_controls_text" + android:fontFamily="@*android:string/config_bodyFontFamilyMedium" + android:textAppearance="@*android:style/TextAppearance.DeviceDefault.Headline" 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" @@ -85,27 +78,33 @@ android:orientation="horizontal" android:paddingTop="48dp"> - <com.android.wm.shell.compatui.letterboxedu.LetterboxEduDialogActionLayout + <com.android.wm.shell.compatui.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 + <com.android.wm.shell.compatui.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> <Button + android:fontFamily="@*android:string/config_bodyFontFamily" + android:fontWeight="500" + android:lineHeight="20dp" + android:textAppearance="@*android:style/TextAppearance.DeviceDefault.Small" android:id="@+id/letterbox_education_dialog_dismiss_button" + android:textStyle="bold" android:layout_width="match_parent" android:layout_height="56dp" - android:layout_marginTop="48dp" + android:layout_marginTop="40dp" + android:textSize="14sp" android:background= "@drawable/letterbox_education_dismiss_button_background_ripple" android:text="@string/letterbox_education_got_it" @@ -119,4 +118,4 @@ </FrameLayout> -</com.android.wm.shell.compatui.letterboxedu.LetterboxEduDialogLayout> +</com.android.wm.shell.compatui.LetterboxEduDialogLayout> diff --git a/libs/WindowManager/Shell/res/layout/letterbox_restart_dialog_layout.xml b/libs/WindowManager/Shell/res/layout/letterbox_restart_dialog_layout.xml new file mode 100644 index 000000000000..5aff4159e135 --- /dev/null +++ b/libs/WindowManager/Shell/res/layout/letterbox_restart_dialog_layout.xml @@ -0,0 +1,138 @@ +<!-- + ~ Copyright (C) 2023 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<com.android.wm.shell.compatui.RestartDialogLayout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + style="@style/LetterboxDialog"> + + <!-- The background of the top-level layout acts as the background dim. --> + + <!-- Vertical margin will be set dynamically since it depends on task bounds. + Setting the alpha of the dialog container to 0, since it shouldn't be visible until the + enter animation starts. --> + <FrameLayout + android:id="@+id/letterbox_restart_dialog_container" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginHorizontal="@dimen/letterbox_restart_dialog_margin" + android:background="@drawable/letterbox_restart_dialog_background" + android:alpha="0" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintWidth_max="@dimen/letterbox_restart_dialog_width"> + + <!-- The ScrollView should only wrap the content of the dialog, otherwise the background + corner radius will be cut off when scrolling to the top/bottom. --> + + <ScrollView android:layout_width="match_parent" + android:layout_height="wrap_content"> + + <LinearLayout + android:padding="24dp" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:gravity="center_horizontal" + android:orientation="vertical"> + + <ImageView + android:importantForAccessibility="no" + android:layout_width="@dimen/letterbox_restart_dialog_title_icon_width" + android:layout_height="@dimen/letterbox_restart_dialog_title_icon_height" + android:src="@drawable/letterbox_restart_header_ic_arrows"/> + + <TextView + android:layout_marginVertical="16dp" + android:id="@+id/letterbox_restart_dialog_title" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="@string/letterbox_restart_dialog_title" + android:textAlignment="center" + android:textAppearance="@style/RestartDialogTitleText"/> + + <TextView + android:textAppearance="@style/RestartDialogBodyText" + android:id="@+id/letterbox_restart_dialog_description" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="@string/letterbox_restart_dialog_description" + android:textAlignment="center"/> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:layout_gravity="center_vertical" + android:layout_marginVertical="32dp"> + + <CheckBox + android:id="@+id/letterbox_restart_dialog_checkbox" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:button="@drawable/letterbox_restart_checkbox_button"/> + + <TextView + android:textAppearance="@style/RestartDialogCheckboxText" + android:layout_marginStart="12dp" + android:id="@+id/letterbox_restart_dialog_checkbox_description" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="@string/letterbox_restart_dialog_checkbox_title" + android:textAlignment="textStart"/> + + </LinearLayout> + + <FrameLayout + android:minHeight="@dimen/letterbox_restart_dialog_button_height" + android:layout_width="match_parent" + android:layout_height="wrap_content"> + + <Button + android:textAppearance="@style/RestartDialogDismissButton" + android:id="@+id/letterbox_restart_dialog_dismiss_button" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:minWidth="@dimen/letterbox_restart_dialog_button_width" + android:minHeight="@dimen/letterbox_restart_dialog_button_height" + android:layout_gravity="start" + android:background= + "@drawable/letterbox_restart_dismiss_button_background_ripple" + android:text="@string/letterbox_restart_cancel" + android:contentDescription="@string/letterbox_restart_cancel"/> + + <Button + android:textAppearance="@style/RestartDialogConfirmButton" + android:id="@+id/letterbox_restart_dialog_restart_button" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:minWidth="@dimen/letterbox_restart_dialog_button_width" + android:minHeight="@dimen/letterbox_restart_dialog_button_height" + android:layout_gravity="end" + android:background= + "@drawable/letterbox_restart_button_background_ripple" + android:text="@string/letterbox_restart_restart" + android:contentDescription="@string/letterbox_restart_restart"/> + + </FrameLayout> + + </LinearLayout> + + </ScrollView> + + </FrameLayout> + +</com.android.wm.shell.compatui.RestartDialogLayout>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/layout/reachability_ui_layout.xml b/libs/WindowManager/Shell/res/layout/reachability_ui_layout.xml new file mode 100644 index 000000000000..1e36fb62f8da --- /dev/null +++ b/libs/WindowManager/Shell/res/layout/reachability_ui_layout.xml @@ -0,0 +1,70 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2023 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<com.android.wm.shell.compatui.ReachabilityEduLayout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:focusable="false" + android:focusableInTouchMode="false" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <com.android.wm.shell.compatui.ReachabilityEduHandLayout + style="@style/ReachabilityEduHandLayout" + android:text="@string/letterbox_reachability_reposition_text" + app:drawableTopCompat="@drawable/reachability_education_ic_right_hand" + android:layout_gravity="center_horizontal|top" + android:layout_marginTop="@dimen/letterbox_reachability_education_dialog_margin" + android:id="@+id/reachability_move_up_button" + android:maxWidth="@dimen/letterbox_reachability_education_item_width" + android:layout_width="wrap_content" + android:layout_height="wrap_content"/> + + <com.android.wm.shell.compatui.ReachabilityEduHandLayout + style="@style/ReachabilityEduHandLayout" + android:text="@string/letterbox_reachability_reposition_text" + app:drawableTopCompat="@drawable/reachability_education_ic_right_hand" + android:layout_gravity="center_vertical|right" + android:layout_marginTop="@dimen/letterbox_reachability_education_dialog_margin" + android:id="@+id/reachability_move_right_button" + android:maxWidth="@dimen/letterbox_reachability_education_item_width" + android:layout_width="wrap_content" + android:layout_height="wrap_content"/> + + + <com.android.wm.shell.compatui.ReachabilityEduHandLayout + style="@style/ReachabilityEduHandLayout" + android:text="@string/letterbox_reachability_reposition_text" + app:drawableTopCompat="@drawable/reachability_education_ic_left_hand" + android:layout_gravity="center_vertical|left" + android:layout_marginTop="@dimen/letterbox_reachability_education_dialog_margin" + android:id="@+id/reachability_move_left_button" + android:maxWidth="@dimen/letterbox_reachability_education_item_width" + android:layout_width="wrap_content" + android:layout_height="wrap_content"/> + + <com.android.wm.shell.compatui.ReachabilityEduHandLayout + style="@style/ReachabilityEduHandLayout" + android:text="@string/letterbox_reachability_reposition_text" + app:drawableTopCompat="@drawable/reachability_education_ic_right_hand" + android:layout_gravity="center_horizontal|bottom" + android:layout_marginTop="@dimen/letterbox_reachability_education_dialog_margin" + android:id="@+id/reachability_move_down_button" + android:maxWidth="@dimen/letterbox_reachability_education_item_width" + android:layout_width="wrap_content" + android:layout_height="wrap_content"/> + +</com.android.wm.shell.compatui.ReachabilityEduLayout> diff --git a/libs/WindowManager/Shell/res/layout/tv_pip_menu.xml b/libs/WindowManager/Shell/res/layout/tv_pip_menu.xml index 7a3ee23d8cdc..dcce4698c252 100644 --- a/libs/WindowManager/Shell/res/layout/tv_pip_menu.xml +++ b/libs/WindowManager/Shell/res/layout/tv_pip_menu.xml @@ -16,92 +16,46 @@ --> <!-- 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"/> + 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"/> + <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"/> - <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"> + <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"/> - <LinearLayout + <com.android.internal.widget.RecyclerView android:id="@+id/tv_pip_menu_action_buttons" android:layout_width="wrap_content" android:layout_height="wrap_content" - 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.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" /> - - <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" /> - - <!-- More TvPipMenuActionButtons may be added here at runtime. --> - - <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" /> - - <com.android.wm.shell.pip.tv.TvPipMenuActionButton - 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> + android:layout_gravity="center" + android:padding="@dimen/pip_menu_button_start_end_offset" + android:clipToPadding="false" + android:alpha="0" + android:contentDescription="@string/a11y_pip_menu_entered"/> + </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,8 +65,9 @@ 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:id="@+id/tv_pip_menu_edu_text_drawer_placeholder" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_below="@+id/tv_pip" @@ -120,23 +75,10 @@ android:layout_alignStart="@+id/tv_pip" android:layout_alignEnd="@+id/tv_pip" android:background="@color/tv_pip_menu_background" - android:clipChildren="true"> - - <TextView - android:id="@+id/tv_pip_menu_edu_text" - android:layout_width="wrap_content" - android:layout_height="@dimen/pip_menu_edu_text_view_height" - android:layout_gravity="bottom|center" - android:gravity="center" - android:paddingBottom="@dimen/pip_menu_border_width" - android:text="@string/pip_edu_text" - android:singleLine="true" - android:ellipsize="marquee" - android:marqueeRepeatLimit="1" - android:scrollHorizontally="true" - android:textAppearance="@style/TvPipEduText"/> - </FrameLayout> + android:paddingBottom="@dimen/pip_menu_border_width" + android:paddingTop="@dimen/pip_menu_border_width"/> + <!-- 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 +86,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 +104,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 +115,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 +126,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 +137,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_pip_menu_background.xml b/libs/WindowManager/Shell/res/layout/tv_pip_menu_background.xml index 5af40200d240..bd48ad2cef44 100644 --- a/libs/WindowManager/Shell/res/layout/tv_pip_menu_background.xml +++ b/libs/WindowManager/Shell/res/layout/tv_pip_menu_background.xml @@ -19,10 +19,11 @@ android:layout_width="match_parent" android:layout_height="match_parent"> <View + android:id="@+id/background_view" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_margin="@dimen/pip_menu_outer_space_frame" android:background="@drawable/tv_pip_menu_background" - android:elevation="@dimen/pip_menu_elevation"/> + android:elevation="@dimen/pip_menu_elevation_no_menu"/> </FrameLayout> 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..b2ac85b018be --- /dev/null +++ b/libs/WindowManager/Shell/res/layout/tv_window_menu_action_button.xml @@ -0,0 +1,41 @@ +<?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:duplicateParentState="true" + 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..40c35be9200f 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> @@ -31,11 +32,17 @@ <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Verander grootte"</string> <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Hou vas"</string> <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Laat los"</string> - <string name="dock_forced_resizable" msgid="1749750436092293116">"Program sal dalk nie met verdeelde skerm werk nie."</string> - <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Program steun nie verdeelde skerm nie."</string> + <!-- no translation found for dock_forced_resizable (7429086980048964687) --> + <skip /> + <!-- no translation found for dock_non_resizeble_failed_to_dock_text (2733543750291266047) --> + <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Hierdie app kan net in 1 venster oopgemaak word."</string> <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> + <!-- no translation found for accessibility_divider (6407584574218956849) --> + <skip /> + <!-- no translation found for divider_title (1963391955593749442) --> + <skip /> <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> @@ -46,6 +53,10 @@ <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Bo 50%"</string> <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Bo 30%"</string> <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Volskerm onder"</string> + <string name="accessibility_split_left" msgid="1713683765575562458">"Verdeel links"</string> + <string name="accessibility_split_right" msgid="8441001008181296837">"Verdeel regs"</string> + <string name="accessibility_split_top" msgid="2789329702027147146">"Verdeel bo"</string> + <string name="accessibility_split_bottom" msgid="8694551025220868191">"Verdeel onder"</string> <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Gebruik eenhandmodus"</string> <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Swiep van die onderkant van die skerm af op of tik enige plek bo die program om uit te gaan"</string> <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Begin eenhandmodus"</string> @@ -61,6 +72,7 @@ <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Beweeg na regs onder"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>-instellings"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Maak borrel toe"</string> + <string name="bubbles_dont_bubble" msgid="3216183855437329223">"Moenie borrels wys nie"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Moenie dat gesprek \'n borrel word nie"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Klets met borrels"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"Nuwe gesprekke verskyn as swerwende ikone, of borrels Tik op borrel om dit oop te maak. Sleep om dit te skuif."</string> @@ -72,17 +84,35 @@ <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) --> - <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) --> + <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Sien en doen meer"</string> + <!-- no translation found for letterbox_education_split_screen_text (449233070804658627) --> <skip /> + <string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Dubbeltik buite ’n program om dit te herposisioneer"</string> <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="letterbox_restart_dialog_title" msgid="8543049527871033505">"Herbegin vir ’n beter aansig?"</string> + <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"Jy kan die app herbegin sodat dit beter op jou skerm lyk, maar jy sal dalk jou vordering of enige ongestoorde veranderinge verloor"</string> + <string name="letterbox_restart_cancel" msgid="1342209132692537805">"Kanselleer"</string> + <string name="letterbox_restart_restart" msgid="8529976234412442973">"Herbegin"</string> + <string name="letterbox_restart_dialog_checkbox_title" msgid="5252918008140768386">"Moenie weer wys nie"</string> + <string name="letterbox_reachability_reposition_text" msgid="4507890186297500893">"Dubbeltik om hierdie app te skuif"</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> + <string name="back_button_text" msgid="1469718707134137085">"Terug"</string> + <string name="handle_text" msgid="1766582106752184456">"Handvatsel"</string> + <string name="app_icon_text" msgid="2823268023931811747">"Appikoon"</string> + <string name="fullscreen_text" msgid="1162316685217676079">"Volskerm"</string> + <string name="desktop_text" msgid="1077633567027630454">"Rekenaarmodus"</string> + <string name="split_screen_text" msgid="1396336058129570886">"Verdeelde skerm"</string> + <string name="more_button_text" msgid="3655388105592893530">"Meer"</string> + <string name="float_button_text" msgid="9221657008391364581">"Sweef"</string> + <string name="select_text" msgid="5139083974039906583">"Kies"</string> + <string name="screenshot_text" msgid="1477704010087786671">"Skermskoot"</string> + <string name="close_text" msgid="4986518933445178928">"Maak toe"</string> + <string name="collapse_menu_text" msgid="7515008122450342029">"Maak kieslys 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..2254fc9beb11 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="7930546669915337998">"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..2559c6fe148d 100644 --- a/libs/WindowManager/Shell/res/values-am/strings.xml +++ b/libs/WindowManager/Shell/res/values-am/strings.xml @@ -22,7 +22,8 @@ <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_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> በስዕል-ላይ-ስዕል ውስጥ ነው"</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> <string name="pip_pause" msgid="690688849510295232">"ባለበት አቁም"</string> @@ -31,11 +32,17 @@ <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"መጠን ይቀይሩ"</string> <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Stash"</string> <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Unstash"</string> - <string name="dock_forced_resizable" msgid="1749750436092293116">"መተግበሪያ ከተከፈለ ማያ ገጽ ጋር ላይሠራ ይችላል"</string> - <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"መተግበሪያው የተከፈለ ማያ ገጽን አይደግፍም።"</string> + <!-- no translation found for dock_forced_resizable (7429086980048964687) --> + <skip /> + <!-- no translation found for dock_non_resizeble_failed_to_dock_text (2733543750291266047) --> + <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"ይህ መተግበሪያ መከፈት የሚችለው በ1 መስኮት ብቻ ነው።"</string> <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> + <!-- no translation found for accessibility_divider (6407584574218956849) --> + <skip /> + <!-- no translation found for divider_title (1963391955593749442) --> + <skip /> <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> @@ -46,6 +53,10 @@ <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"ከላይ 50%"</string> <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"ከላይ 30%"</string> <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"የታች ሙሉ ማያ ገጽ"</string> + <string name="accessibility_split_left" msgid="1713683765575562458">"ወደ ግራ ከፋፍል"</string> + <string name="accessibility_split_right" msgid="8441001008181296837">"ወደ ቀኝ ከፋፍል"</string> + <string name="accessibility_split_top" msgid="2789329702027147146">"ወደ ላይ ከፋፍል"</string> + <string name="accessibility_split_bottom" msgid="8694551025220868191">"ወደ ታች ከፋፍል"</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> @@ -61,6 +72,7 @@ <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"ታችኛውን ቀኝ ያንቀሳቅሱ"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"የ<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> ቅንብሮች"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"አረፋን አሰናብት"</string> + <string name="bubbles_dont_bubble" msgid="3216183855437329223">"ዓረፋ አትፍጠር"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"ውይይቶችን በአረፋ አታሳይ"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"አረፋዎችን በመጠቀም ይወያዩ"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"አዲስ ውይይቶች እንደ ተንሳፋፊ አዶዎች ወይም አረፋዎች ሆነው ይታያሉ። አረፋን ለመክፈት መታ ያድርጉ። ለመውሰድ ይጎትቱት።"</string> @@ -72,17 +84,36 @@ <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) --> - <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) --> + <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"ተጨማሪ ይመልከቱ እና ያድርጉ"</string> + <!-- no translation found for letterbox_education_split_screen_text (449233070804658627) --> <skip /> + <string name="letterbox_education_reposition_text" msgid="4589957299813220661">"ቦታውን ለመቀየር ከመተግበሪያው ውጪ ሁለቴ መታ ያድርጉ"</string> <string name="letterbox_education_got_it" msgid="4057634570866051177">"ገባኝ"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"ለተጨማሪ መረጃ ይዘርጉ።"</string> + <string name="letterbox_restart_dialog_title" msgid="8543049527871033505">"ለተሻለ ዕይታ እንደገና ይጀመር?"</string> + <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"በማያ ገጽዎ ላይ የተሻለ ሆኖ እንዲታይ መተግበሪያውን እንደገና ማስጀመር ይችላሉ ነገር ግን የደረሱበትን የሂደት ደረጃ ወይም ማናቸውንም ያልተቀመጡ ለውጦች ሊያጡ ይችላሉ"</string> + <string name="letterbox_restart_cancel" msgid="1342209132692537805">"ይቅር"</string> + <string name="letterbox_restart_restart" msgid="8529976234412442973">"እንደገና ያስጀምሩ"</string> + <string name="letterbox_restart_dialog_checkbox_title" msgid="5252918008140768386">"ዳግም አታሳይ"</string> + <!-- no translation found for letterbox_reachability_reposition_text (4507890186297500893) --> + <skip /> + <string name="maximize_button_text" msgid="1650859196290301963">"አስፋ"</string> + <string name="minimize_button_text" msgid="271592547935841753">"አሳንስ"</string> + <string name="close_button_text" msgid="2913281996024033299">"ዝጋ"</string> + <string name="back_button_text" msgid="1469718707134137085">"ተመለስ"</string> + <string name="handle_text" msgid="1766582106752184456">"መያዣ"</string> + <string name="app_icon_text" msgid="2823268023931811747">"የመተግበሪያ አዶ"</string> + <string name="fullscreen_text" msgid="1162316685217676079">"ሙሉ ማያ"</string> + <string name="desktop_text" msgid="1077633567027630454">"የዴስክቶፕ ሁነታ"</string> + <string name="split_screen_text" msgid="1396336058129570886">"የተከፈለ ማያ ገጽ"</string> + <string name="more_button_text" msgid="3655388105592893530">"ተጨማሪ"</string> + <string name="float_button_text" msgid="9221657008391364581">"ተንሳፋፊ"</string> + <string name="select_text" msgid="5139083974039906583">"ምረጥ"</string> + <string name="screenshot_text" msgid="1477704010087786671">"ቅጽበታዊ ገጽ እይታ"</string> + <string name="close_text" msgid="4986518933445178928">"ዝጋ"</string> + <string name="collapse_menu_text" msgid="7515008122450342029">"ምናሌ ዝጋ"</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..a6be57889a4e 100644 --- a/libs/WindowManager/Shell/res/values-am/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-am/strings_tv.xml @@ -17,9 +17,18 @@ <resources xmlns:android="http://schemas.android.com/apk/res/android" xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> - <string name="notification_channel_tv_pip" msgid="2576686079160402435">"ስዕል-ላይ-ስዕል"</string> + <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="7930546669915337998">"ለመቆጣጠሪያዎች "<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..2cbeb7cd9eff 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> @@ -31,11 +32,17 @@ <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"تغيير الحجم"</string> <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"إخفاء"</string> <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"إظهار"</string> - <string name="dock_forced_resizable" msgid="1749750436092293116">"قد لا يعمل التطبيق بشكل سليم في وضع \"تقسيم الشاشة\"."</string> - <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"التطبيق لا يتيح تقسيم الشاشة."</string> + <!-- no translation found for dock_forced_resizable (7429086980048964687) --> + <skip /> + <!-- no translation found for dock_non_resizeble_failed_to_dock_text (2733543750291266047) --> + <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"لا يمكن فتح هذا التطبيق إلا في نافذة واحدة."</string> <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> + <!-- no translation found for accessibility_divider (6407584574218956849) --> + <skip /> + <!-- no translation found for divider_title (1963391955593749442) --> + <skip /> <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> @@ -46,6 +53,10 @@ <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"ضبط حجم النافذة العلوية ليكون ٥٠%"</string> <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"ضبط حجم النافذة العلوية ليكون ٣٠%"</string> <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"عرض النافذة السفلية بملء الشاشة"</string> + <string name="accessibility_split_left" msgid="1713683765575562458">"تقسيم لليسار"</string> + <string name="accessibility_split_right" msgid="8441001008181296837">"تقسيم لليمين"</string> + <string name="accessibility_split_top" msgid="2789329702027147146">"تقسيم للأعلى"</string> + <string name="accessibility_split_bottom" msgid="8694551025220868191">"تقسيم للأسفل"</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> @@ -61,6 +72,7 @@ <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"نقل إلى أسفل اليسار"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"إعدادات <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"إغلاق فقاعة المحادثة"</string> + <string name="bubbles_dont_bubble" msgid="3216183855437329223">"عدم إظهار فقاعات المحادثات"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"عدم عرض المحادثة كفقاعة محادثة"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"الدردشة باستخدام فقاعات المحادثات"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"تظهر المحادثات الجديدة كرموز عائمة أو كفقاعات. انقر لفتح فقاعة المحادثة، واسحبها لتحريكها."</string> @@ -72,17 +84,36 @@ <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) --> - <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) --> + <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"استخدام تطبيقات متعدّدة في وقت واحد"</string> + <!-- no translation found for letterbox_education_split_screen_text (449233070804658627) --> <skip /> + <string name="letterbox_education_reposition_text" msgid="4589957299813220661">"انقر مرّتين خارج تطبيق لتغيير موضعه."</string> <string name="letterbox_education_got_it" msgid="4057634570866051177">"حسنًا"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"التوسيع للحصول على مزيد من المعلومات"</string> + <string name="letterbox_restart_dialog_title" msgid="8543049527871033505">"هل تريد إعادة تشغيل التطبيق لعرضه بشكل أفضل؟"</string> + <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"يمكنك إعادة تشغيل التطبيق حتى يظهر بشكل أفضل على شاشتك، ولكن قد تفقد تقدمك أو أي تغييرات غير محفوظة."</string> + <string name="letterbox_restart_cancel" msgid="1342209132692537805">"إلغاء"</string> + <string name="letterbox_restart_restart" msgid="8529976234412442973">"إعادة التشغيل"</string> + <string name="letterbox_restart_dialog_checkbox_title" msgid="5252918008140768386">"عدم عرض مربّع حوار التأكيد مجددًا"</string> + <!-- no translation found for letterbox_reachability_reposition_text (4507890186297500893) --> + <skip /> + <string name="maximize_button_text" msgid="1650859196290301963">"تكبير"</string> + <string name="minimize_button_text" msgid="271592547935841753">"تصغير"</string> + <string name="close_button_text" msgid="2913281996024033299">"إغلاق"</string> + <string name="back_button_text" msgid="1469718707134137085">"رجوع"</string> + <string name="handle_text" msgid="1766582106752184456">"مقبض"</string> + <string name="app_icon_text" msgid="2823268023931811747">"رمز التطبيق"</string> + <string name="fullscreen_text" msgid="1162316685217676079">"ملء الشاشة"</string> + <string name="desktop_text" msgid="1077633567027630454">"وضع سطح المكتب"</string> + <string name="split_screen_text" msgid="1396336058129570886">"تقسيم الشاشة"</string> + <string name="more_button_text" msgid="3655388105592893530">"المزيد"</string> + <string name="float_button_text" msgid="9221657008391364581">"نافذة عائمة"</string> + <string name="select_text" msgid="5139083974039906583">"اختيار"</string> + <string name="screenshot_text" msgid="1477704010087786671">"لقطة شاشة"</string> + <string name="close_text" msgid="4986518933445178928">"إغلاق"</string> + <string name="collapse_menu_text" msgid="7515008122450342029">"إغلاق القائمة"</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..82ab8e9ee15b 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="7930546669915337998">"انقر مرتين على "<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..160c5a623771 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> @@ -31,21 +32,31 @@ <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"আকাৰ সলনি কৰক"</string> <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"লুকুৱাওক"</string> <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"দেখুৱাওক"</string> - <string name="dock_forced_resizable" msgid="1749750436092293116">"এপ্টোৱে বিভাজিত স্ক্ৰীনৰ সৈতে কাম নকৰিব পাৰে।"</string> - <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"এপ্টোৱে বিভাজিত স্ক্ৰীন সমৰ্থন নকৰে।"</string> + <!-- no translation found for dock_forced_resizable (7429086980048964687) --> + <skip /> + <!-- no translation found for dock_non_resizeble_failed_to_dock_text (2733543750291266047) --> + <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"এই এপ্টো কেৱল ১ খন ৱিণ্ড’ত খুলিব পাৰি।"</string> <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> + <!-- no translation found for accessibility_divider (6407584574218956849) --> + <skip /> + <!-- no translation found for divider_title (1963391955593749442) --> + <skip /> <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> - <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"বাওঁফালৰ স্ক্ৰীণখন ৩০% কৰক"</string> + <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"বাওঁফালৰ স্ক্ৰীনখন ৭০% কৰক"</string> + <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"বাওঁফালৰ স্ক্ৰীনখন ৫০% কৰক"</string> + <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"বাওঁফালৰ স্ক্ৰীনখন ৩০% কৰক"</string> <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"সোঁফালৰ স্ক্ৰীনখন সম্পূৰ্ণ স্ক্ৰীন কৰক"</string> <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"শীৰ্ষ স্ক্ৰীনখন সম্পূৰ্ণ স্ক্ৰীন কৰক"</string> - <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"শীর্ষ স্ক্ৰীণখন ৭০% কৰক"</string> - <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"শীর্ষ স্ক্ৰীণখন ৫০% কৰক"</string> - <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"শীর্ষ স্ক্ৰীণখন ৩০% কৰক"</string> + <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"শীর্ষ স্ক্ৰীনখন ৭০% কৰক"</string> + <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"শীর্ষ স্ক্ৰীনখন ৫০% কৰক"</string> + <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"শীর্ষ স্ক্ৰীনখন ৩০% কৰক"</string> <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"তলৰ স্ক্ৰীনখন সম্পূৰ্ণ স্ক্ৰীন কৰক"</string> + <string name="accessibility_split_left" msgid="1713683765575562458">"বাওঁফালে বিভাজন কৰক"</string> + <string name="accessibility_split_right" msgid="8441001008181296837">"সোঁফালে বিভাজন কৰক"</string> + <string name="accessibility_split_top" msgid="2789329702027147146">"একেবাৰে ওপৰৰফালে বিভাজন কৰক"</string> + <string name="accessibility_split_bottom" msgid="8694551025220868191">"একেবাৰে তলৰফালে বিভাজন কৰক"</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> @@ -61,6 +72,7 @@ <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"তলৰ সোঁফালে নিয়ক"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> ছেটিং"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"বাবল অগ্ৰাহ্য কৰক"</string> + <string name="bubbles_dont_bubble" msgid="3216183855437329223">"বাবল কৰাটো বন্ধ কৰক"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"বাৰ্তালাপ বাবল নকৰিব"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Bubbles ব্যৱহাৰ কৰি চাট কৰক"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"নতুন বাৰ্তালাপ উপঙি থকা চিহ্নসমূহ অথবা bubbles হিচাপে প্ৰদর্শিত হয়। Bubbles খুলিবলৈ টিপক। এইটো স্থানান্তৰ কৰিবলৈ টানি নিয়ক।"</string> @@ -72,17 +84,35 @@ <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) --> - <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) --> + <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"চাওক আৰু অধিক কৰক"</string> + <!-- no translation found for letterbox_education_split_screen_text (449233070804658627) --> <skip /> + <string name="letterbox_education_reposition_text" msgid="4589957299813220661">"এপ্টোৰ স্থান সলনি কৰিবলৈ ইয়াৰ বাহিৰত দুবাৰ টিপক"</string> <string name="letterbox_education_got_it" msgid="4057634570866051177">"বুজি পালোঁ"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"অধিক তথ্যৰ বাবে বিস্তাৰ কৰক।"</string> + <string name="letterbox_restart_dialog_title" msgid="8543049527871033505">"উন্নতভাৱে দেখা পাবলৈ ৰিষ্টাৰ্ট কৰিবনে?"</string> + <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"আপোনাৰ স্ক্ৰীনত উন্নতভাৱে দেখা পাবলৈ আপুনি এপ্টো ৰিষ্টাৰ্ট কৰিব পাৰে, কিন্তু আপুনি আপোনাৰ অগ্ৰগতি অথবা ছেভ নকৰা যিকোনো সালসলনি হেৰুৱাব পাৰে"</string> + <string name="letterbox_restart_cancel" msgid="1342209132692537805">"বাতিল কৰক"</string> + <string name="letterbox_restart_restart" msgid="8529976234412442973">"ৰিষ্টাৰ্ট কৰক"</string> + <string name="letterbox_restart_dialog_checkbox_title" msgid="5252918008140768386">"পুনৰাই নেদেখুৱাব"</string> + <string name="letterbox_reachability_reposition_text" msgid="4507890186297500893">"এই এপ্টো স্থানান্তৰ কৰিবলৈ দুবাৰ টিপক"</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> + <string name="back_button_text" msgid="1469718707134137085">"উভতি যাওক"</string> + <string name="handle_text" msgid="1766582106752184456">"হেণ্ডেল"</string> + <string name="app_icon_text" msgid="2823268023931811747">"এপৰ চিহ্ন"</string> + <string name="fullscreen_text" msgid="1162316685217676079">"সম্পূৰ্ণ স্ক্ৰীন"</string> + <string name="desktop_text" msgid="1077633567027630454">"ডেস্কটপ ম’ড"</string> + <string name="split_screen_text" msgid="1396336058129570886">"বিভাজিত স্ক্ৰীন"</string> + <string name="more_button_text" msgid="3655388105592893530">"অধিক"</string> + <string name="float_button_text" msgid="9221657008391364581">"ওপঙা"</string> + <string name="select_text" msgid="5139083974039906583">"বাছনি কৰক"</string> + <string name="screenshot_text" msgid="1477704010087786671">"স্ক্ৰীনশ্বট"</string> + <string name="close_text" msgid="4986518933445178928">"বন্ধ কৰক"</string> + <string name="collapse_menu_text" msgid="7515008122450342029">"মেনু বন্ধ কৰক"</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..34eaaea33609 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="7930546669915337998">"নিয়ন্ত্ৰণৰ বাবে "<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..6e2fd53d1e0a 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> @@ -31,11 +32,17 @@ <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Ölçüsünü dəyişin"</string> <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Güvənli məkanda saxlayın"</string> <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Güvənli məkandan çıxarın"</string> - <string name="dock_forced_resizable" msgid="1749750436092293116">"Tətbiq bölünmüş ekran ilə işləməyə bilər."</string> - <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Tətbiq ekran bölünməsini dəstəkləmir."</string> + <!-- no translation found for dock_forced_resizable (7429086980048964687) --> + <skip /> + <!-- no translation found for dock_non_resizeble_failed_to_dock_text (2733543750291266047) --> + <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Bu tətbiq yalnız 1 pəncərədə açıla bilər."</string> <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> + <!-- no translation found for accessibility_divider (6407584574218956849) --> + <skip /> + <!-- no translation found for divider_title (1963391955593749442) --> + <skip /> <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> @@ -46,6 +53,10 @@ <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Yuxarı 50%"</string> <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Yuxarı 30%"</string> <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Aşağı tam ekran"</string> + <string name="accessibility_split_left" msgid="1713683765575562458">"Sola ayırın"</string> + <string name="accessibility_split_right" msgid="8441001008181296837">"Sağa ayırın"</string> + <string name="accessibility_split_top" msgid="2789329702027147146">"Yuxarı ayırın"</string> + <string name="accessibility_split_bottom" msgid="8694551025220868191">"Aşağı ayırın"</string> <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Birəlli rejim istifadəsi"</string> <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Çıxmaq üçün ekranın aşağısından yuxarıya doğru sürüşdürün və ya tətbiqin yuxarısında istənilən yerə toxunun"</string> <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Birəlli rejim başlasın"</string> @@ -61,6 +72,7 @@ <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Aşağıya sağa köçürün"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> ayarları"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Yumrucuğu ləğv edin"</string> + <string name="bubbles_dont_bubble" msgid="3216183855437329223">"Yumrucuqları dayandırın"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Söhbəti yumrucuqda göstərmə"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Yumrucuqlardan istifadə edərək söhbət edin"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"Yeni söhbətlər üzən nişanlar və ya yumrucuqlar kimi görünür. Yumrucuğu açmaq üçün toxunun. Hərəkət etdirmək üçün sürüşdürün."</string> @@ -72,17 +84,36 @@ <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) --> - <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) --> + <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Ardını görün və edin"</string> + <!-- no translation found for letterbox_education_split_screen_text (449233070804658627) --> <skip /> + <string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Tətbiqin yerini dəyişmək üçün kənarına iki dəfə toxunun"</string> <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="letterbox_restart_dialog_title" msgid="8543049527871033505">"Daha yaxşı görünüş üçün yenidən başladılsın?"</string> + <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"Tətbiqi yenidən başlada bilərsiniz ki, ekranınızda daha yaxşı görünsün, lakin irəliləyişi və ya yadda saxlanmamış dəyişiklikləri itirə bilərsiniz"</string> + <string name="letterbox_restart_cancel" msgid="1342209132692537805">"Ləğv edin"</string> + <string name="letterbox_restart_restart" msgid="8529976234412442973">"Yenidən başladın"</string> + <string name="letterbox_restart_dialog_checkbox_title" msgid="5252918008140768386">"Yenidən göstərməyin"</string> + <!-- no translation found for letterbox_reachability_reposition_text (4507890186297500893) --> + <skip /> + <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> + <string name="back_button_text" msgid="1469718707134137085">"Geriyə"</string> + <string name="handle_text" msgid="1766582106752184456">"Hər kəsə açıq istifadəçi adı"</string> + <string name="app_icon_text" msgid="2823268023931811747">"Tətbiq ikonası"</string> + <string name="fullscreen_text" msgid="1162316685217676079">"Tam Ekran"</string> + <string name="desktop_text" msgid="1077633567027630454">"Masaüstü Rejimi"</string> + <string name="split_screen_text" msgid="1396336058129570886">"Bölünmüş Ekran"</string> + <string name="more_button_text" msgid="3655388105592893530">"Ardı"</string> + <string name="float_button_text" msgid="9221657008391364581">"Üzən pəncərə"</string> + <string name="select_text" msgid="5139083974039906583">"Seçin"</string> + <string name="screenshot_text" msgid="1477704010087786671">"Skrinşot"</string> + <string name="close_text" msgid="4986518933445178928">"Bağlayın"</string> + <string name="collapse_menu_text" msgid="7515008122450342029">"Menyunu 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..c45a09645075 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="7930546669915337998">"Nizamlayıcılar üçün "<annotation icon="home_icon">"ƏSAS SƏHİFƏ "</annotation>" seçiminə 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..f0bfbad156ef 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> @@ -31,11 +32,17 @@ <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Promenite veličinu"</string> <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Stavite u tajnu memoriju"</string> <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Uklonite iz tajne memorije"</string> - <string name="dock_forced_resizable" msgid="1749750436092293116">"Aplikacija možda neće raditi sa podeljenim ekranom."</string> - <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Aplikacija ne podržava podeljeni ekran."</string> + <!-- no translation found for dock_forced_resizable (7429086980048964687) --> + <skip /> + <!-- no translation found for dock_non_resizeble_failed_to_dock_text (2733543750291266047) --> + <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Ova aplikacija može da se otvori samo u jednom prozoru."</string> <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> + <!-- no translation found for accessibility_divider (6407584574218956849) --> + <skip /> + <!-- no translation found for divider_title (1963391955593749442) --> + <skip /> <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> @@ -46,6 +53,10 @@ <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Gornji ekran 50%"</string> <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Gornji ekran 30%"</string> <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Režim celog ekrana za donji ekran"</string> + <string name="accessibility_split_left" msgid="1713683765575562458">"Podelite levo"</string> + <string name="accessibility_split_right" msgid="8441001008181296837">"Podelite desno"</string> + <string name="accessibility_split_top" msgid="2789329702027147146">"Podelite u vrhu"</string> + <string name="accessibility_split_bottom" msgid="8694551025220868191">"Podelite u dnu"</string> <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Korišćenje režima jednom rukom"</string> <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Da biste izašli, prevucite nagore od dna ekrana ili dodirnite bilo gde iznad aplikacije"</string> <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Pokrenite režim jednom rukom"</string> @@ -61,6 +72,7 @@ <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Premesti dole desno"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"Podešavanja za <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Odbaci oblačić"</string> + <string name="bubbles_dont_bubble" msgid="3216183855437329223">"Bez oblačića"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Ne koristi oblačiće za konverzaciju"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Ćaskajte u oblačićima"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"Nove konverzacije se prikazuju kao plutajuće ikone ili oblačići. Dodirnite da biste otvorili oblačić. Prevucite da biste ga premestili."</string> @@ -72,17 +84,35 @@ <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) --> - <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) --> + <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Vidite i uradite više"</string> + <!-- no translation found for letterbox_education_split_screen_text (449233070804658627) --> <skip /> + <string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Dvaput dodirnite izvan aplikacije da biste promenili njenu poziciju"</string> <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="letterbox_restart_dialog_title" msgid="8543049527871033505">"Želite li da restartujete radi boljeg prikaza?"</string> + <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"Možete da restartujete aplikaciju da bi izgledala bolje na ekranu, s tim što možete da izgubite ono što ste uradili ili nesačuvane promene, ako ih ima"</string> + <string name="letterbox_restart_cancel" msgid="1342209132692537805">"Otkaži"</string> + <string name="letterbox_restart_restart" msgid="8529976234412442973">"Restartuj"</string> + <string name="letterbox_restart_dialog_checkbox_title" msgid="5252918008140768386">"Ne prikazuj ponovo"</string> + <string name="letterbox_reachability_reposition_text" msgid="4507890186297500893">"Dvaput dodirnite da biste premestili ovu aplikaciju"</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> + <string name="back_button_text" msgid="1469718707134137085">"Nazad"</string> + <string name="handle_text" msgid="1766582106752184456">"Identifikator"</string> + <string name="app_icon_text" msgid="2823268023931811747">"Ikona aplikacije"</string> + <string name="fullscreen_text" msgid="1162316685217676079">"Preko celog ekrana"</string> + <string name="desktop_text" msgid="1077633567027630454">"Režim za računare"</string> + <string name="split_screen_text" msgid="1396336058129570886">"Podeljeni ekran"</string> + <string name="more_button_text" msgid="3655388105592893530">"Još"</string> + <string name="float_button_text" msgid="9221657008391364581">"Plutajuće"</string> + <string name="select_text" msgid="5139083974039906583">"Izaberite"</string> + <string name="screenshot_text" msgid="1477704010087786671">"Snimak ekrana"</string> + <string name="close_text" msgid="4986518933445178928">"Zatvorite"</string> + <string name="collapse_menu_text" msgid="7515008122450342029">"Zatvorite meni"</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..6dc4ab1cea79 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="7930546669915337998">"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..65bd7b3585f7 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> @@ -31,11 +32,17 @@ <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Змяніць памер"</string> <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Схаваць"</string> <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Паказаць"</string> - <string name="dock_forced_resizable" msgid="1749750436092293116">"Праграма можа не працаваць у рэжыме падзеленага экрана."</string> - <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Праграма не падтрымлівае функцыю дзялення экрана."</string> + <!-- no translation found for dock_forced_resizable (7429086980048964687) --> + <skip /> + <!-- no translation found for dock_non_resizeble_failed_to_dock_text (2733543750291266047) --> + <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Гэту праграму можна адкрыць толькі ў адным акне."</string> <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> + <!-- no translation found for accessibility_divider (6407584574218956849) --> + <skip /> + <!-- no translation found for divider_title (1963391955593749442) --> + <skip /> <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> @@ -46,6 +53,10 @@ <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Верхні экран – 50%"</string> <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Верхні экран – 30%"</string> <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Ніжні экран – поўнаэкранны рэжым"</string> + <string name="accessibility_split_left" msgid="1713683765575562458">"Падзяліць злева"</string> + <string name="accessibility_split_right" msgid="8441001008181296837">"Падзяліць справа"</string> + <string name="accessibility_split_top" msgid="2789329702027147146">"Падзяліць уверсе"</string> + <string name="accessibility_split_bottom" msgid="8694551025220868191">"Падзяліць унізе"</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> @@ -61,6 +72,7 @@ <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Перамясціць правей і ніжэй"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"Налады \"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>\""</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Адхіліць апавяшчэнне"</string> + <string name="bubbles_dont_bubble" msgid="3216183855437329223">"Выключыць усплывальныя апавяшчэнні"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Не паказваць размову ў выглядзе ўсплывальных апавяшчэнняў"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Усплывальныя апавяшчэнні"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"Новыя размовы будуць паказвацца як рухомыя значкі ці ўсплывальныя апавяшчэнні. Націсніце, каб адкрыць усплывальнае апавяшчэнне. Перацягніце яго, каб перамясціць."</string> @@ -72,17 +84,36 @@ <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) --> - <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) --> + <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Адначасова выконвайце розныя задачы"</string> + <!-- no translation found for letterbox_education_split_screen_text (449233070804658627) --> <skip /> + <string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Двойчы націсніце экран па-за праграмай, каб перамясціць яе"</string> <string name="letterbox_education_got_it" msgid="4057634570866051177">"Зразумела"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Разгарнуць для дадатковай інфармацыі"</string> + <string name="letterbox_restart_dialog_title" msgid="8543049527871033505">"Перазапусціць?"</string> + <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"Вы можаце перазапусціць праграму, каб яна выглядала лепш на вашым экране, але пры гэтым могуць знікнуць даныя пра ваш прагрэс або незахаваныя змяненні"</string> + <string name="letterbox_restart_cancel" msgid="1342209132692537805">"Скасаваць"</string> + <string name="letterbox_restart_restart" msgid="8529976234412442973">"Перазапусціць"</string> + <string name="letterbox_restart_dialog_checkbox_title" msgid="5252918008140768386">"Больш не паказваць"</string> + <!-- no translation found for letterbox_reachability_reposition_text (4507890186297500893) --> + <skip /> + <string name="maximize_button_text" msgid="1650859196290301963">"Разгарнуць"</string> + <string name="minimize_button_text" msgid="271592547935841753">"Згарнуць"</string> + <string name="close_button_text" msgid="2913281996024033299">"Закрыць"</string> + <string name="back_button_text" msgid="1469718707134137085">"Назад"</string> + <string name="handle_text" msgid="1766582106752184456">"Маркер"</string> + <string name="app_icon_text" msgid="2823268023931811747">"Значок праграмы"</string> + <string name="fullscreen_text" msgid="1162316685217676079">"На ўвесь экран"</string> + <string name="desktop_text" msgid="1077633567027630454">"Рэжым працоўнага стала"</string> + <string name="split_screen_text" msgid="1396336058129570886">"Падзяліць экран"</string> + <string name="more_button_text" msgid="3655388105592893530">"Яшчэ"</string> + <string name="float_button_text" msgid="9221657008391364581">"Зрабіць рухомым акном"</string> + <string name="select_text" msgid="5139083974039906583">"Выбраць"</string> + <string name="screenshot_text" msgid="1477704010087786671">"Здымак экрана"</string> + <string name="close_text" msgid="4986518933445178928">"Закрыць"</string> + <string name="collapse_menu_text" msgid="7515008122450342029">"Закрыць меню"</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..20e725f1aa09 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="7930546669915337998">"Двойчы націсніце "<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..0de565058d0f 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> @@ -31,11 +32,17 @@ <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Преоразмеряване"</string> <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Съхраняване"</string> <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Отмяна на съхраняването"</string> - <string name="dock_forced_resizable" msgid="1749750436092293116">"Приложението може да не работи в режим на разделен екран."</string> - <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Приложението не поддържа разделен екран."</string> + <!-- no translation found for dock_forced_resizable (7429086980048964687) --> + <skip /> + <!-- no translation found for dock_non_resizeble_failed_to_dock_text (2733543750291266047) --> + <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Това приложение може да се отвори само в 1 прозорец."</string> <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> + <!-- no translation found for accessibility_divider (6407584574218956849) --> + <skip /> + <!-- no translation found for divider_title (1963391955593749442) --> + <skip /> <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> @@ -46,6 +53,10 @@ <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Горен екран: 50%"</string> <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Горен екран: 30%"</string> <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Долен екран: Показване на цял екран"</string> + <string name="accessibility_split_left" msgid="1713683765575562458">"Разделяне в лявата част"</string> + <string name="accessibility_split_right" msgid="8441001008181296837">"Разделяне в дясната част"</string> + <string name="accessibility_split_top" msgid="2789329702027147146">"Разделяне в горната част"</string> + <string name="accessibility_split_bottom" msgid="8694551025220868191">"Разделяне в долната част"</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> @@ -61,6 +72,7 @@ <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Преместване долу вдясно"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"Настройки за <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Отхвърляне на балончетата"</string> + <string name="bubbles_dont_bubble" msgid="3216183855437329223">"Без балончета"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Без балончета за разговора"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Чат с балончета"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"Новите разговори се показват като плаващи икони, или балончета. Докоснете балонче, за да го отворите, или го плъзнете, за да го преместите."</string> @@ -72,17 +84,36 @@ <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) --> - <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) --> + <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Преглеждайте и правете повече неща"</string> + <!-- no translation found for letterbox_education_split_screen_text (449233070804658627) --> <skip /> + <string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Докоснете два пъти извън дадено приложение, за да промените позицията му"</string> <string name="letterbox_education_got_it" msgid="4057634570866051177">"Разбрах"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Разгъване за още информация."</string> + <string name="letterbox_restart_dialog_title" msgid="8543049527871033505">"Да се рестартира ли с цел подобряване на изгледа?"</string> + <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"Можете да рестартирате приложението, за да изглежда по-добре на екрана. Възможно е обаче да загубите напредъка си или незапазените промени"</string> + <string name="letterbox_restart_cancel" msgid="1342209132692537805">"Отказ"</string> + <string name="letterbox_restart_restart" msgid="8529976234412442973">"Рестартиране"</string> + <string name="letterbox_restart_dialog_checkbox_title" msgid="5252918008140768386">"Да не се показва отново"</string> + <!-- no translation found for letterbox_reachability_reposition_text (4507890186297500893) --> + <skip /> + <string name="maximize_button_text" msgid="1650859196290301963">"Увеличаване"</string> + <string name="minimize_button_text" msgid="271592547935841753">"Намаляване"</string> + <string name="close_button_text" msgid="2913281996024033299">"Затваряне"</string> + <string name="back_button_text" msgid="1469718707134137085">"Назад"</string> + <string name="handle_text" msgid="1766582106752184456">"Манипулатор"</string> + <string name="app_icon_text" msgid="2823268023931811747">"Икона на приложението"</string> + <string name="fullscreen_text" msgid="1162316685217676079">"Цял екран"</string> + <string name="desktop_text" msgid="1077633567027630454">"Режим за настолни компютри"</string> + <string name="split_screen_text" msgid="1396336058129570886">"Разделяне на екрана"</string> + <string name="more_button_text" msgid="3655388105592893530">"Още"</string> + <string name="float_button_text" msgid="9221657008391364581">"Плаващо"</string> + <string name="select_text" msgid="5139083974039906583">"Избиране"</string> + <string name="screenshot_text" msgid="1477704010087786671">"Екранна снимка"</string> + <string name="close_text" msgid="4986518933445178928">"Затваряне"</string> + <string name="collapse_menu_text" msgid="7515008122450342029">"Затваряне на менюто"</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..e9906f952455 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="7930546669915337998">"За достъп до контролите натиснете два пъти "<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..da206d69090d 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> @@ -31,11 +32,17 @@ <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"রিসাইজ করুন"</string> <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"স্ট্যাস করুন"</string> <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"আনস্ট্যাস করুন"</string> - <string name="dock_forced_resizable" msgid="1749750436092293116">"অ্যাপটি স্প্লিট স্ক্রিনে কাজ নাও করতে পারে।"</string> - <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"অ্যাপ্লিকেশান বিভক্ত-স্ক্রিন সমর্থন করে না৷"</string> + <!-- no translation found for dock_forced_resizable (7429086980048964687) --> + <skip /> + <!-- no translation found for dock_non_resizeble_failed_to_dock_text (2733543750291266047) --> + <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"এই অ্যাপটি শুধু ১টি উইন্ডোয় খোলা যেতে পারে।"</string> <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> + <!-- no translation found for accessibility_divider (6407584574218956849) --> + <skip /> + <!-- no translation found for divider_title (1963391955593749442) --> + <skip /> <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> @@ -46,6 +53,10 @@ <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"শীর্ষ ৫০%"</string> <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"শীর্ষ ৩০%"</string> <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"নীচের অংশ নিয়ে পূর্ণ স্ক্রিন"</string> + <string name="accessibility_split_left" msgid="1713683765575562458">"স্ক্রিনের বাঁদিকে স্প্লিট করুন"</string> + <string name="accessibility_split_right" msgid="8441001008181296837">"স্ক্রিনের ডানদিকে স্প্লিট করুন"</string> + <string name="accessibility_split_top" msgid="2789329702027147146">"স্ক্রিনের উপরের দিকে স্প্লিট করুন"</string> + <string name="accessibility_split_bottom" msgid="8694551025220868191">"স্প্লিট করার বোতাম"</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> @@ -61,6 +72,7 @@ <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"নিচে ডান দিকে সরান"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> সেটিংস"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"বাবল খারিজ করুন"</string> + <string name="bubbles_dont_bubble" msgid="3216183855437329223">"বাবল করা বন্ধ করুন"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"কথোপকথন বাবল হিসেবে দেখাবে না"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"বাবল ব্যবহার করে চ্যাট করুন"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"নতুন কথোপকথন ভেসে থাকা আইকন বা বাবল হিসেবে দেখানো হয়। বাবল খুলতে ট্যাপ করুন। সেটি সরাতে ধরে টেনে আনুন।"</string> @@ -72,17 +84,36 @@ <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) --> - <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) --> + <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"দেখুন ও আরও অনেক কিছু করুন"</string> + <!-- no translation found for letterbox_education_split_screen_text (449233070804658627) --> <skip /> + <string name="letterbox_education_reposition_text" msgid="4589957299813220661">"কোনও অ্যাপের স্থান পরিবর্তন করতে তার বাইরে ডবল ট্যাপ করুন"</string> <string name="letterbox_education_got_it" msgid="4057634570866051177">"বুঝেছি"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"আরও তথ্যের জন্য বড় করুন।"</string> + <string name="letterbox_restart_dialog_title" msgid="8543049527871033505">"আরও ভালভাবে দেখার জন্য রিস্টার্ট করবেন?"</string> + <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"স্ক্রিনে আরও ভালভাবে দেখার জন্য আপনি অ্যাপ রিস্টার্ট করতে পারবেন, এর ফলে চলতে থাকা কোনও প্রক্রিয়া বা সেভ না করা পরিবর্তন হারিয়ে যেতে পারে"</string> + <string name="letterbox_restart_cancel" msgid="1342209132692537805">"বাতিল করুন"</string> + <string name="letterbox_restart_restart" msgid="8529976234412442973">"রিস্টার্ট করুন"</string> + <string name="letterbox_restart_dialog_checkbox_title" msgid="5252918008140768386">"আর দেখতে চাই না"</string> + <!-- no translation found for letterbox_reachability_reposition_text (4507890186297500893) --> + <skip /> + <string name="maximize_button_text" msgid="1650859196290301963">"বড় করুন"</string> + <string name="minimize_button_text" msgid="271592547935841753">"ছোট করুন"</string> + <string name="close_button_text" msgid="2913281996024033299">"বন্ধ করুন"</string> + <string name="back_button_text" msgid="1469718707134137085">"ফিরে যান"</string> + <string name="handle_text" msgid="1766582106752184456">"হাতল"</string> + <string name="app_icon_text" msgid="2823268023931811747">"অ্যাপ আইকন"</string> + <string name="fullscreen_text" msgid="1162316685217676079">"ফুলস্ক্রিন"</string> + <string name="desktop_text" msgid="1077633567027630454">"ডেস্কটপ মোড"</string> + <string name="split_screen_text" msgid="1396336058129570886">"স্প্লিট স্ক্রিন"</string> + <string name="more_button_text" msgid="3655388105592893530">"আরও"</string> + <string name="float_button_text" msgid="9221657008391364581">"ফ্লোট"</string> + <string name="select_text" msgid="5139083974039906583">"বেছে নিন"</string> + <string name="screenshot_text" msgid="1477704010087786671">"স্ক্রিনশট"</string> + <string name="close_text" msgid="4986518933445178928">"বন্ধ করুন"</string> + <string name="collapse_menu_text" msgid="7515008122450342029">"\'মেনু\' বন্ধ করুন"</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..b515154ec3e0 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="7930546669915337998">"কন্ট্রোলের জন্য "<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..26ea7cc3f804 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> @@ -31,11 +32,17 @@ <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Promjena veličine"</string> <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Stavljanje u stash"</string> <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Vađenje iz stasha"</string> - <string name="dock_forced_resizable" msgid="1749750436092293116">"Aplikacija možda neće raditi na podijeljenom ekranu."</string> - <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Aplikacija ne podržava dijeljenje ekrana."</string> + <!-- no translation found for dock_forced_resizable (7429086980048964687) --> + <skip /> + <!-- no translation found for dock_non_resizeble_failed_to_dock_text (2733543750291266047) --> + <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Ova aplikacija se može otvoriti samo u 1 prozoru."</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> + <!-- no translation found for accessibility_divider (6407584574218956849) --> + <skip /> + <!-- no translation found for divider_title (1963391955593749442) --> + <skip /> <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> @@ -46,6 +53,10 @@ <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Gore 50%"</string> <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Gore 30%"</string> <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Donji ekran kao cijeli ekran"</string> + <string name="accessibility_split_left" msgid="1713683765575562458">"Podjela ulijevo"</string> + <string name="accessibility_split_right" msgid="8441001008181296837">"Podjela udesno"</string> + <string name="accessibility_split_top" msgid="2789329702027147146">"Podjela nagore"</string> + <string name="accessibility_split_bottom" msgid="8694551025220868191">"Podjela nadolje"</string> <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Korištenje načina rada jednom rukom"</string> <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Da izađete, prevucite s dna ekrana prema gore ili dodirnite bilo gdje iznad aplikacije"</string> <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Započinjanje načina rada jednom rukom"</string> @@ -61,6 +72,7 @@ <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Pomjerite dolje desno"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"Postavke aplikacije <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Odbaci oblačić"</string> + <string name="bubbles_dont_bubble" msgid="3216183855437329223">"Zaustavi oblačiće"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Nemoj prikazivati razgovor u oblačićima"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Chatajte koristeći oblačiće"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"Novi razgovori se prikazuju kao plutajuće ikone ili oblačići. Dodirnite da otvorite oblačić. Prevucite da ga premjestite."</string> @@ -72,17 +84,35 @@ <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) --> - <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) --> + <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Pogledajte i učinite više"</string> + <!-- no translation found for letterbox_education_split_screen_text (449233070804658627) --> <skip /> + <string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Dvaput dodirnite izvan aplikacije da promijenite njen položaj"</string> <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="letterbox_restart_dialog_title" msgid="8543049527871033505">"Ponovo pokrenuti za bolji prikaz?"</string> + <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"Možete ponovo pokrenuti aplikaciju da bolje izgleda na ekranu, ali možete izgubiti napredak ili izmjene koje nisu sačuvane"</string> + <string name="letterbox_restart_cancel" msgid="1342209132692537805">"Otkaži"</string> + <string name="letterbox_restart_restart" msgid="8529976234412442973">"Ponovo pokreni"</string> + <string name="letterbox_restart_dialog_checkbox_title" msgid="5252918008140768386">"Ne prikazuj ponovo"</string> + <string name="letterbox_reachability_reposition_text" msgid="4507890186297500893">"Dvaput dodirnite da biste premjestili ovu aplikaciju"</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> + <string name="back_button_text" msgid="1469718707134137085">"Nazad"</string> + <string name="handle_text" msgid="1766582106752184456">"Identifikator"</string> + <string name="app_icon_text" msgid="2823268023931811747">"Ikona aplikacije"</string> + <string name="fullscreen_text" msgid="1162316685217676079">"Cijeli ekran"</string> + <string name="desktop_text" msgid="1077633567027630454">"Način rada radne površine"</string> + <string name="split_screen_text" msgid="1396336058129570886">"Podijeljeni ekran"</string> + <string name="more_button_text" msgid="3655388105592893530">"Više"</string> + <string name="float_button_text" msgid="9221657008391364581">"Lebdeći"</string> + <string name="select_text" msgid="5139083974039906583">"Odabir"</string> + <string name="screenshot_text" msgid="1477704010087786671">"Snimak ekrana"</string> + <string name="close_text" msgid="4986518933445178928">"Zatvaranje"</string> + <string name="collapse_menu_text" msgid="7515008122450342029">"Zatvaranje menija"</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..99e076b31180 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="7930546669915337998">"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..e086adf8357a 100644 --- a/libs/WindowManager/Shell/res/values-ca/strings.xml +++ b/libs/WindowManager/Shell/res/values-ca/strings.xml @@ -22,20 +22,27 @@ <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_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> està en pantalla en pantalla"</string> + <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Menú d\'imatge sobre imatge"</string> + <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> està en mode d\'imatge sobre imatge"</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> - <string name="dock_forced_resizable" msgid="1749750436092293116">"És possible que l\'aplicació no funcioni amb la pantalla dividida."</string> - <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"L\'aplicació no admet la pantalla dividida."</string> + <!-- no translation found for dock_forced_resizable (7429086980048964687) --> + <skip /> + <!-- no translation found for dock_non_resizeble_failed_to_dock_text (2733543750291266047) --> + <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Aquesta aplicació només pot obrir-se en 1 finestra."</string> <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> + <!-- no translation found for accessibility_divider (6407584574218956849) --> + <skip /> + <!-- no translation found for divider_title (1963391955593749442) --> + <skip /> <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> @@ -46,12 +53,16 @@ <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Pantalla superior al 50%"</string> <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Pantalla superior al 30%"</string> <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Pantalla inferior completa"</string> + <string name="accessibility_split_left" msgid="1713683765575562458">"Divideix a l\'esquerra"</string> + <string name="accessibility_split_right" msgid="8441001008181296837">"Divideix a la dreta"</string> + <string name="accessibility_split_top" msgid="2789329702027147146">"Divideix a la part superior"</string> + <string name="accessibility_split_bottom" msgid="8694551025220868191">"Divideix a la part inferior"</string> <string name="one_handed_tutorial_title" msgid="4583241688067426350">"S\'està utilitzant el mode d\'una mà"</string> <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Per sortir, llisca cap amunt des de la part inferior de la pantalla o toca qualsevol lloc a sobre de l\'aplicació"</string> <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Inicia el mode d\'una mà"</string> <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"Surt del mode d\'una mà"</string> <string name="bubbles_settings_button_description" msgid="1301286017420516912">"Configuració de les bombolles: <xliff:g id="APP_NAME">%1$s</xliff:g>"</string> - <string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"Menú addicional"</string> + <string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"Menú de desbordament"</string> <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"Torna a afegir a la pila"</string> <string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> de: <xliff:g id="APP_NAME">%2$s</xliff:g>"</string> <string name="bubble_content_description_stack" msgid="8071515017164630429">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> (<xliff:g id="APP_NAME">%2$s</xliff:g>) i <xliff:g id="BUBBLE_COUNT">%3$d</xliff:g> més"</string> @@ -61,6 +72,7 @@ <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Mou a baix a la dreta"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"Configuració de l\'aplicació <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Ignora la bombolla"</string> + <string name="bubbles_dont_bubble" msgid="3216183855437329223">"No mostris com a bombolla"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"No mostris la conversa com a bombolla"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Xateja amb bombolles"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"Les converses noves es mostren com a icones flotants o bombolles. Toca per obrir una bombolla. Arrossega-la per moure-la."</string> @@ -72,17 +84,36 @@ <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) --> - <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) --> + <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Consulta i fes més coses"</string> + <!-- no translation found for letterbox_education_split_screen_text (449233070804658627) --> <skip /> + <string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Fes doble toc fora d\'una aplicació per canviar-ne la posició"</string> <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="letterbox_restart_dialog_title" msgid="8543049527871033505">"Vols reiniciar per a una millor visualització?"</string> + <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"Pots reiniciar l\'aplicació perquè es vegi millor en pantalla, però és possible que perdis el teu progrés o qualsevol canvi que no hagis desat"</string> + <string name="letterbox_restart_cancel" msgid="1342209132692537805">"Cancel·la"</string> + <string name="letterbox_restart_restart" msgid="8529976234412442973">"Reinicia"</string> + <string name="letterbox_restart_dialog_checkbox_title" msgid="5252918008140768386">"No ho tornis a mostrar"</string> + <!-- no translation found for letterbox_reachability_reposition_text (4507890186297500893) --> + <skip /> + <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> + <string name="back_button_text" msgid="1469718707134137085">"Enrere"</string> + <string name="handle_text" msgid="1766582106752184456">"Ansa"</string> + <string name="app_icon_text" msgid="2823268023931811747">"Icona de l\'aplicació"</string> + <string name="fullscreen_text" msgid="1162316685217676079">"Pantalla completa"</string> + <string name="desktop_text" msgid="1077633567027630454">"Mode d\'escriptori"</string> + <string name="split_screen_text" msgid="1396336058129570886">"Pantalla dividida"</string> + <string name="more_button_text" msgid="3655388105592893530">"Més"</string> + <string name="float_button_text" msgid="9221657008391364581">"Flotant"</string> + <string name="select_text" msgid="5139083974039906583">"Selecciona"</string> + <string name="screenshot_text" msgid="1477704010087786671">"Captura de pantalla"</string> + <string name="close_text" msgid="4986518933445178928">"Tanca"</string> + <string name="collapse_menu_text" msgid="7515008122450342029">"Tanca el menú"</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..d51a78b8e92d 100644 --- a/libs/WindowManager/Shell/res/values-ca/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-ca/strings_tv.xml @@ -17,9 +17,18 @@ <resources xmlns:android="http://schemas.android.com/apk/res/android" xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> - <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Pantalla en pantalla"</string> + <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Imatge sobre imatge"</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="7930546669915337998">"Prem dos cops "<annotation icon="home_icon">"INICI"</annotation>" per accedir als controls"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Menú d\'imatge sobre imatge."</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..abefd9f1cf6c 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> @@ -31,11 +32,17 @@ <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Změnit velikost"</string> <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Uložit"</string> <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Zrušit uložení"</string> - <string name="dock_forced_resizable" msgid="1749750436092293116">"Aplikace v režimu rozdělené obrazovky nemusí fungovat."</string> - <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Aplikace nepodporuje režim rozdělené obrazovky."</string> + <!-- no translation found for dock_forced_resizable (7429086980048964687) --> + <skip /> + <!-- no translation found for dock_non_resizeble_failed_to_dock_text (2733543750291266047) --> + <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Tuto aplikaci lze otevřít jen na jednom okně."</string> <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> + <!-- no translation found for accessibility_divider (6407584574218956849) --> + <skip /> + <!-- no translation found for divider_title (1963391955593749442) --> + <skip /> <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> @@ -46,6 +53,10 @@ <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"50 % nahoře"</string> <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"30 % nahoře"</string> <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Dolní část na celou obrazovku"</string> + <string name="accessibility_split_left" msgid="1713683765575562458">"Rozdělit vlevo"</string> + <string name="accessibility_split_right" msgid="8441001008181296837">"Rozdělit vpravo"</string> + <string name="accessibility_split_top" msgid="2789329702027147146">"Rozdělit nahoře"</string> + <string name="accessibility_split_bottom" msgid="8694551025220868191">"Rozdělit dole"</string> <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Používání režimu jedné ruky"</string> <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Režim ukončíte, když přejedete prstem z dolní části obrazovky nahoru nebo klepnete kamkoli nad aplikaci"</string> <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Spustit režim jedné ruky"</string> @@ -61,6 +72,7 @@ <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Přesunout vpravo dolů"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"Nastavení <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Zavřít bublinu"</string> + <string name="bubbles_dont_bubble" msgid="3216183855437329223">"Nezobrazovat bubliny"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Nezobrazovat konverzaci v bublinách"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Chatujte pomocí bublin"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"Nové konverzace se zobrazují jako plovoucí ikony, neboli bubliny. Klepnutím bublinu otevřete. Přetažením ji posunete."</string> @@ -72,17 +84,36 @@ <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) --> - <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) --> + <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Lepší zobrazení a více možností"</string> + <!-- no translation found for letterbox_education_split_screen_text (449233070804658627) --> <skip /> + <string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Dvojitým klepnutím mimo aplikaci změníte její umístění"</string> <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="letterbox_restart_dialog_title" msgid="8543049527871033505">"Restartovat pro lepší zobrazení?"</string> + <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"Aplikaci můžete restartovat, aby na obrazovce vypadala lépe, ale můžete přijít o svůj postup nebo o neuložené změny"</string> + <string name="letterbox_restart_cancel" msgid="1342209132692537805">"Zrušit"</string> + <string name="letterbox_restart_restart" msgid="8529976234412442973">"Restartovat"</string> + <string name="letterbox_restart_dialog_checkbox_title" msgid="5252918008140768386">"Tuto zprávu příště nezobrazovat"</string> + <!-- no translation found for letterbox_reachability_reposition_text (4507890186297500893) --> + <skip /> + <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> + <string name="back_button_text" msgid="1469718707134137085">"Zpět"</string> + <string name="handle_text" msgid="1766582106752184456">"Úchyt"</string> + <string name="app_icon_text" msgid="2823268023931811747">"Ikona aplikace"</string> + <string name="fullscreen_text" msgid="1162316685217676079">"Celá obrazovka"</string> + <string name="desktop_text" msgid="1077633567027630454">"Režim počítače"</string> + <string name="split_screen_text" msgid="1396336058129570886">"Rozdělená obrazovka"</string> + <string name="more_button_text" msgid="3655388105592893530">"Více"</string> + <string name="float_button_text" msgid="9221657008391364581">"Plovoucí"</string> + <string name="select_text" msgid="5139083974039906583">"Vybrat"</string> + <string name="screenshot_text" msgid="1477704010087786671">"Snímek obrazovky"</string> + <string name="close_text" msgid="4986518933445178928">"Zavřít"</string> + <string name="collapse_menu_text" msgid="7515008122450342029">"Zavřít nabídku"</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..72e1ae907cfb 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="7930546669915337998">"Ovládací prvky zobrazíte dvojitým stisknutím "<annotation icon="home_icon">"HOME"</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..adf29cad1b2f 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> @@ -31,11 +32,17 @@ <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Rediger størrelse"</string> <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Skjul"</string> <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Vis"</string> - <string name="dock_forced_resizable" msgid="1749750436092293116">"Appen fungerer muligvis ikke i opdelt skærm."</string> - <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Appen understøtter ikke opdelt skærm."</string> + <!-- no translation found for dock_forced_resizable (7429086980048964687) --> + <skip /> + <!-- no translation found for dock_non_resizeble_failed_to_dock_text (2733543750291266047) --> + <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Denne app kan kun åbnes i 1 vindue."</string> <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> + <!-- no translation found for accessibility_divider (6407584574218956849) --> + <skip /> + <!-- no translation found for divider_title (1963391955593749442) --> + <skip /> <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> @@ -46,6 +53,10 @@ <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Øverste 50 %"</string> <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Øverste 30 %"</string> <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Vis nederste del i fuld skærm"</string> + <string name="accessibility_split_left" msgid="1713683765575562458">"Vis i venstre side"</string> + <string name="accessibility_split_right" msgid="8441001008181296837">"Vis i højre side"</string> + <string name="accessibility_split_top" msgid="2789329702027147146">"Vis øverst"</string> + <string name="accessibility_split_bottom" msgid="8694551025220868191">"Vis nederst"</string> <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Brug af enhåndstilstand"</string> <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Du kan afslutte ved at stryge opad fra bunden af skærmen eller trykke et vilkårligt sted over appen"</string> <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Start enhåndstilstand"</string> @@ -61,6 +72,7 @@ <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Flyt ned til højre"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"Indstillinger for <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Afvis boble"</string> + <string name="bubbles_dont_bubble" msgid="3216183855437329223">"Stop med at vise bobler"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Vis ikke samtaler i bobler"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Chat ved hjælp af bobler"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"Nye samtaler vises som svævende ikoner eller bobler. Tryk for at åbne boblen. Træk for at flytte den."</string> @@ -72,17 +84,35 @@ <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) --> - <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) --> + <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Se og gør mere"</string> + <!-- no translation found for letterbox_education_split_screen_text (449233070804658627) --> <skip /> + <string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Tryk to gange uden for en app for at justere dens placering"</string> <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="letterbox_restart_dialog_title" msgid="8543049527871033505">"Vil du genstarte for at få en bedre visning?"</string> + <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"Du kan genstarte appen, så den ser bedre ud på din skærm, men du mister muligvis dine fremskridt og de ændringer, der ikke gemt"</string> + <string name="letterbox_restart_cancel" msgid="1342209132692537805">"Annuller"</string> + <string name="letterbox_restart_restart" msgid="8529976234412442973">"Genstart"</string> + <string name="letterbox_restart_dialog_checkbox_title" msgid="5252918008140768386">"Vis ikke igen"</string> + <string name="letterbox_reachability_reposition_text" msgid="4507890186297500893">"Tryk to gange for at flytte appen"</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> + <string name="back_button_text" msgid="1469718707134137085">"Tilbage"</string> + <string name="handle_text" msgid="1766582106752184456">"Håndtag"</string> + <string name="app_icon_text" msgid="2823268023931811747">"Appikon"</string> + <string name="fullscreen_text" msgid="1162316685217676079">"Fuld skærm"</string> + <string name="desktop_text" msgid="1077633567027630454">"Computertilstand"</string> + <string name="split_screen_text" msgid="1396336058129570886">"Opdelt skærm"</string> + <string name="more_button_text" msgid="3655388105592893530">"Mere"</string> + <string name="float_button_text" msgid="9221657008391364581">"Svævende"</string> + <string name="select_text" msgid="5139083974039906583">"Vælg"</string> + <string name="screenshot_text" msgid="1477704010087786671">"Screenshot"</string> + <string name="close_text" msgid="4986518933445178928">"Luk"</string> + <string name="collapse_menu_text" msgid="7515008122450342029">"Luk menu"</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..5881b0674ad2 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="7930546669915337998">"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..46101570caa2 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,17 @@ <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_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Das Teilen des Bildschirms wird in dieser App nicht unterstützt."</string> + <!-- no translation found for dock_forced_resizable (7429086980048964687) --> + <skip /> + <!-- no translation found for dock_non_resizeble_failed_to_dock_text (2733543750291266047) --> + <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Diese App kann nur in einem einzigen Fenster geöffnet werden."</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> + <!-- no translation found for accessibility_divider (6407584574218956849) --> + <skip /> + <!-- no translation found for divider_title (1963391955593749442) --> + <skip /> <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> @@ -46,6 +53,10 @@ <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"50 % oben"</string> <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"30 % oben"</string> <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Vollbild unten"</string> + <string name="accessibility_split_left" msgid="1713683765575562458">"Links teilen"</string> + <string name="accessibility_split_right" msgid="8441001008181296837">"Rechts teilen"</string> + <string name="accessibility_split_top" msgid="2789329702027147146">"Oben teilen"</string> + <string name="accessibility_split_bottom" msgid="8694551025220868191">"Unten teilen"</string> <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Einhandmodus wird verwendet"</string> <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Wenn du die App schließen möchtest, wische vom unteren Rand des Displays nach oben oder tippe auf eine beliebige Stelle oberhalb der App"</string> <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Einhandmodus starten"</string> @@ -61,6 +72,7 @@ <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Nach unten rechts verschieben"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"Einstellungen für <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Bubble schließen"</string> + <string name="bubbles_dont_bubble" msgid="3216183855437329223">"Keine Bubbles zulassen"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Unterhaltung nicht als Bubble anzeigen"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Bubbles zum Chatten verwenden"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"Neue Unterhaltungen erscheinen als unverankerte Symbole, „Bubbles“ genannt. Wenn du eine Bubble öffnen möchtest, tippe sie an. Wenn du sie verschieben möchtest, zieh an ihr."</string> @@ -72,17 +84,36 @@ <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) --> - <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) --> + <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Mehr sehen und erledigen"</string> + <!-- no translation found for letterbox_education_split_screen_text (449233070804658627) --> <skip /> + <string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Außerhalb einer App doppeltippen, um die Position zu ändern"</string> <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="letterbox_restart_dialog_title" msgid="8543049527871033505">"Für bessere Darstellung neu starten?"</string> + <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"Du kannst die App neu starten, damit sie an die Bildschirmabmessungen deines Geräts angepasst dargestellt wird – jedoch können dadurch dein Fortschritt oder nicht gespeicherte Änderungen verloren gehen."</string> + <string name="letterbox_restart_cancel" msgid="1342209132692537805">"Abbrechen"</string> + <string name="letterbox_restart_restart" msgid="8529976234412442973">"Neu starten"</string> + <string name="letterbox_restart_dialog_checkbox_title" msgid="5252918008140768386">"Nicht mehr anzeigen"</string> + <!-- no translation found for letterbox_reachability_reposition_text (4507890186297500893) --> + <skip /> + <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> + <string name="back_button_text" msgid="1469718707134137085">"Zurück"</string> + <string name="handle_text" msgid="1766582106752184456">"Ziehpunkt"</string> + <string name="app_icon_text" msgid="2823268023931811747">"App-Symbol"</string> + <string name="fullscreen_text" msgid="1162316685217676079">"Vollbild"</string> + <string name="desktop_text" msgid="1077633567027630454">"Desktopmodus"</string> + <string name="split_screen_text" msgid="1396336058129570886">"Geteilter Bildschirm"</string> + <string name="more_button_text" msgid="3655388105592893530">"Mehr"</string> + <string name="float_button_text" msgid="9221657008391364581">"Frei schwebend"</string> + <string name="select_text" msgid="5139083974039906583">"Auswählen"</string> + <string name="screenshot_text" msgid="1477704010087786671">"Screenshot"</string> + <string name="close_text" msgid="4986518933445178928">"Schließen"</string> + <string name="collapse_menu_text" msgid="7515008122450342029">"Menü 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..5c5cbda296a1 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="7930546669915337998">"Für Steuerelemente 2× "<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..4ac9f7f63865 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> @@ -31,11 +32,17 @@ <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Αλλαγή μεγέθους"</string> <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Απόκρυψη"</string> <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Κατάργηση απόκρυψης"</string> - <string name="dock_forced_resizable" msgid="1749750436092293116">"Η εφαρμογή ενδέχεται να μην λειτουργεί με διαχωρισμό οθόνης."</string> - <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Η εφαρμογή δεν υποστηρίζει διαχωρισμό οθόνης."</string> + <!-- no translation found for dock_forced_resizable (7429086980048964687) --> + <skip /> + <!-- no translation found for dock_non_resizeble_failed_to_dock_text (2733543750291266047) --> + <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Αυτή η εφαρμογή μπορεί να ανοιχθεί μόνο σε 1 παράθυρο."</string> <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> + <!-- no translation found for accessibility_divider (6407584574218956849) --> + <skip /> + <!-- no translation found for divider_title (1963391955593749442) --> + <skip /> <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> @@ -46,6 +53,10 @@ <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Πάνω 50%"</string> <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Πάνω 30%"</string> <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Κάτω πλήρης οθόνη"</string> + <string name="accessibility_split_left" msgid="1713683765575562458">"Διαχωρισμός αριστερά"</string> + <string name="accessibility_split_right" msgid="8441001008181296837">"Διαχωρισμός δεξιά"</string> + <string name="accessibility_split_top" msgid="2789329702027147146">"Διαχωρισμός επάνω"</string> + <string name="accessibility_split_bottom" msgid="8694551025220868191">"Διαχωρισμός κάτω"</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> @@ -61,6 +72,7 @@ <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Μετακίνηση κάτω δεξιά"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"Ρυθμίσεις <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Παράβλ. για συννεφ."</string> + <string name="bubbles_dont_bubble" msgid="3216183855437329223">"Να μην εμφανίζει συννεφάκια"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Να μην γίνει προβολή της συζήτησης σε συννεφάκια."</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Συζητήστε χρησιμοποιώντας συννεφάκια."</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"Οι νέες συζητήσεις εμφανίζονται ως κινούμενα εικονίδια ή συννεφάκια. Πατήστε για να ανοίξετε το συννεφάκι. Σύρετε για να το μετακινήσετε."</string> @@ -72,17 +84,35 @@ <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) --> - <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) --> + <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Δείτε και κάντε περισσότερα"</string> + <!-- no translation found for letterbox_education_split_screen_text (449233070804658627) --> <skip /> + <string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Πατήστε δύο φορές έξω από μια εφαρμογή για να αλλάξετε τη θέση της"</string> <string name="letterbox_education_got_it" msgid="4057634570866051177">"Το κατάλαβα"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Ανάπτυξη για περισσότερες πληροφορίες."</string> + <string name="letterbox_restart_dialog_title" msgid="8543049527871033505">"Επανεκκίνηση για καλύτερη προβολή;"</string> + <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"Μπορείτε να επανεκκινήσετε την εφαρμογή για να προβάλλεται καλύτερα στην οθόνη σας, αλλά η πρόοδός σας και τυχόν μη αποθηκευμένες αλλαγές ενδέχεται να χαθούν."</string> + <string name="letterbox_restart_cancel" msgid="1342209132692537805">"Ακύρωση"</string> + <string name="letterbox_restart_restart" msgid="8529976234412442973">"Επανεκκίνηση"</string> + <string name="letterbox_restart_dialog_checkbox_title" msgid="5252918008140768386">"Να μην εμφανιστεί ξανά"</string> + <string name="letterbox_reachability_reposition_text" msgid="4507890186297500893">"Διπλό πάτημα για μεταφορά αυτής της εφαρμογής"</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> + <string name="back_button_text" msgid="1469718707134137085">"Πίσω"</string> + <string name="handle_text" msgid="1766582106752184456">"Λαβή"</string> + <string name="app_icon_text" msgid="2823268023931811747">"Εικονίδιο εφαρμογής"</string> + <string name="fullscreen_text" msgid="1162316685217676079">"Πλήρης οθόνη"</string> + <string name="desktop_text" msgid="1077633567027630454">"Λειτουργία επιφάνειας εργασίας"</string> + <string name="split_screen_text" msgid="1396336058129570886">"Διαχωρισμός οθόνης"</string> + <string name="more_button_text" msgid="3655388105592893530">"Περισσότερα"</string> + <string name="float_button_text" msgid="9221657008391364581">"Κινούμενο"</string> + <string name="select_text" msgid="5139083974039906583">"Επιλογή"</string> + <string name="screenshot_text" msgid="1477704010087786671">"Στιγμιότυπο οθόνης"</string> + <string name="close_text" msgid="4986518933445178928">"Κλείσιμο"</string> + <string name="collapse_menu_text" msgid="7515008122450342029">"Κλείσιμο μενού"</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..a80e2c72de7e 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="7930546669915337998">"Πατ. δύο φορές το κουμπί "<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..8dee9ae82c5f 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> @@ -31,11 +32,17 @@ <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Resize"</string> <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Stash"</string> <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Unstash"</string> - <string name="dock_forced_resizable" msgid="1749750436092293116">"App may not work with split-screen."</string> - <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"App does not support split-screen."</string> + <!-- no translation found for dock_forced_resizable (7429086980048964687) --> + <skip /> + <!-- no translation found for dock_non_resizeble_failed_to_dock_text (2733543750291266047) --> + <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"This app can only be opened in one window."</string> <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> + <!-- no translation found for accessibility_divider (6407584574218956849) --> + <skip /> + <!-- no translation found for divider_title (1963391955593749442) --> + <skip /> <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> @@ -46,6 +53,10 @@ <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Top 50%"</string> <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Top 30%"</string> <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Bottom full screen"</string> + <string name="accessibility_split_left" msgid="1713683765575562458">"Split left"</string> + <string name="accessibility_split_right" msgid="8441001008181296837">"Split right"</string> + <string name="accessibility_split_top" msgid="2789329702027147146">"Split top"</string> + <string name="accessibility_split_bottom" msgid="8694551025220868191">"Split bottom"</string> <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Using one-handed mode"</string> <string name="one_handed_tutorial_description" msgid="3486582858591353067">"To exit, swipe up from the bottom of the screen or tap anywhere above the app"</string> <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Start one-handed mode"</string> @@ -61,6 +72,7 @@ <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Move bottom right"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> settings"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Dismiss bubble"</string> + <string name="bubbles_dont_bubble" msgid="3216183855437329223">"Don’t bubble"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Don’t bubble conversation"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Chat using bubbles"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"New conversations appear as floating icons, or bubbles. Tap to open bubble. Drag to move it."</string> @@ -72,17 +84,35 @@ <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) --> + <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"See and do more"</string> + <!-- no translation found for letterbox_education_split_screen_text (449233070804658627) --> <skip /> + <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="letterbox_restart_dialog_title" msgid="8543049527871033505">"Restart for a better view?"</string> + <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"You can restart the app so that it looks better on your screen, but you may lose your progress or any unsaved changes"</string> + <string name="letterbox_restart_cancel" msgid="1342209132692537805">"Cancel"</string> + <string name="letterbox_restart_restart" msgid="8529976234412442973">"Restart"</string> + <string name="letterbox_restart_dialog_checkbox_title" msgid="5252918008140768386">"Don\'t show again"</string> + <string name="letterbox_reachability_reposition_text" msgid="4507890186297500893">"Double-tap to move this app"</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> + <string name="back_button_text" msgid="1469718707134137085">"Back"</string> + <string name="handle_text" msgid="1766582106752184456">"Handle"</string> + <string name="app_icon_text" msgid="2823268023931811747">"App icon"</string> + <string name="fullscreen_text" msgid="1162316685217676079">"Full screen"</string> + <string name="desktop_text" msgid="1077633567027630454">"Desktop mode"</string> + <string name="split_screen_text" msgid="1396336058129570886">"Split screen"</string> + <string name="more_button_text" msgid="3655388105592893530">"More"</string> + <string name="float_button_text" msgid="9221657008391364581">"Float"</string> + <string name="select_text" msgid="5139083974039906583">"Select"</string> + <string name="screenshot_text" msgid="1477704010087786671">"Screenshot"</string> + <string name="close_text" msgid="4986518933445178928">"Close"</string> + <string name="collapse_menu_text" msgid="7515008122450342029">"Close menu"</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..71d02271090d 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="7930546669915337998">"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..137ebe47c0b5 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> @@ -31,11 +32,13 @@ <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Resize"</string> <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Stash"</string> <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Unstash"</string> - <string name="dock_forced_resizable" msgid="1749750436092293116">"App may not work with split-screen."</string> - <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"App does not support split-screen."</string> + <string name="dock_forced_resizable" msgid="7429086980048964687">"App may not work with split screen"</string> + <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"App does not support split screen"</string> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"This app can only be opened in 1 window."</string> <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="accessibility_divider" msgid="6407584574218956849">"Split screen divider"</string> + <string name="divider_title" msgid="1963391955593749442">"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> @@ -46,6 +49,10 @@ <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Top 50%"</string> <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Top 30%"</string> <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Bottom full screen"</string> + <string name="accessibility_split_left" msgid="1713683765575562458">"Split left"</string> + <string name="accessibility_split_right" msgid="8441001008181296837">"Split right"</string> + <string name="accessibility_split_top" msgid="2789329702027147146">"Split top"</string> + <string name="accessibility_split_bottom" msgid="8694551025220868191">"Split bottom"</string> <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Using one-handed mode"</string> <string name="one_handed_tutorial_description" msgid="3486582858591353067">"To exit, swipe up from the bottom of the screen or tap anywhere above the app"</string> <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Start one-handed mode"</string> @@ -61,28 +68,46 @@ <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Move bottom right"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> settings"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Dismiss bubble"</string> + <string name="bubbles_dont_bubble" msgid="3216183855437329223">"Don’t bubble"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Don’t bubble conversation"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Chat using bubbles"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"New conversations appear as floating icons, or bubbles. Tap to open bubble. Drag to move it."</string> - <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"Control bubbles at any time"</string> + <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"Control bubbles anytime"</string> <string name="bubbles_user_education_manage" msgid="3460756219946517198">"Tap Manage to turn off bubbles from this app"</string> - <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"OK"</string> + <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"Got it"</string> <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"No recent bubbles"</string> <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"Recent bubbles and dismissed bubbles will appear here"</string> <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="449233070804658627">"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="letterbox_restart_dialog_title" msgid="8543049527871033505">"Restart for a better view?"</string> + <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"You can restart the app so it looks better on your screen, but you may lose your progress or any unsaved changes"</string> + <string name="letterbox_restart_cancel" msgid="1342209132692537805">"Cancel"</string> + <string name="letterbox_restart_restart" msgid="8529976234412442973">"Restart"</string> + <string name="letterbox_restart_dialog_checkbox_title" msgid="5252918008140768386">"Don’t show again"</string> + <string name="letterbox_reachability_reposition_text" msgid="4507890186297500893">"Double-tap to move this app"</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> + <string name="back_button_text" msgid="1469718707134137085">"Back"</string> + <string name="handle_text" msgid="1766582106752184456">"Handle"</string> + <string name="app_icon_text" msgid="2823268023931811747">"App Icon"</string> + <string name="fullscreen_text" msgid="1162316685217676079">"Fullscreen"</string> + <string name="desktop_text" msgid="1077633567027630454">"Desktop Mode"</string> + <string name="split_screen_text" msgid="1396336058129570886">"Split Screen"</string> + <string name="more_button_text" msgid="3655388105592893530">"More"</string> + <string name="float_button_text" msgid="9221657008391364581">"Float"</string> + <string name="select_text" msgid="5139083974039906583">"Select"</string> + <string name="screenshot_text" msgid="1477704010087786671">"Screenshot"</string> + <string name="close_text" msgid="4986518933445178928">"Close"</string> + <string name="collapse_menu_text" msgid="7515008122450342029">"Close Menu"</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..09def6b69f06 100644 --- a/libs/WindowManager/Shell/res/values-en-rCA/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-en-rCA/strings_tv.xml @@ -17,9 +17,18 @@ <resources xmlns:android="http://schemas.android.com/apk/res/android" xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> - <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Picture-in-picture"</string> + <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="7930546669915337998">"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..8dee9ae82c5f 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> @@ -31,11 +32,17 @@ <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Resize"</string> <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Stash"</string> <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Unstash"</string> - <string name="dock_forced_resizable" msgid="1749750436092293116">"App may not work with split-screen."</string> - <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"App does not support split-screen."</string> + <!-- no translation found for dock_forced_resizable (7429086980048964687) --> + <skip /> + <!-- no translation found for dock_non_resizeble_failed_to_dock_text (2733543750291266047) --> + <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"This app can only be opened in one window."</string> <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> + <!-- no translation found for accessibility_divider (6407584574218956849) --> + <skip /> + <!-- no translation found for divider_title (1963391955593749442) --> + <skip /> <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> @@ -46,6 +53,10 @@ <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Top 50%"</string> <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Top 30%"</string> <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Bottom full screen"</string> + <string name="accessibility_split_left" msgid="1713683765575562458">"Split left"</string> + <string name="accessibility_split_right" msgid="8441001008181296837">"Split right"</string> + <string name="accessibility_split_top" msgid="2789329702027147146">"Split top"</string> + <string name="accessibility_split_bottom" msgid="8694551025220868191">"Split bottom"</string> <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Using one-handed mode"</string> <string name="one_handed_tutorial_description" msgid="3486582858591353067">"To exit, swipe up from the bottom of the screen or tap anywhere above the app"</string> <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Start one-handed mode"</string> @@ -61,6 +72,7 @@ <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Move bottom right"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> settings"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Dismiss bubble"</string> + <string name="bubbles_dont_bubble" msgid="3216183855437329223">"Don’t bubble"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Don’t bubble conversation"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Chat using bubbles"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"New conversations appear as floating icons, or bubbles. Tap to open bubble. Drag to move it."</string> @@ -72,17 +84,35 @@ <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) --> + <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"See and do more"</string> + <!-- no translation found for letterbox_education_split_screen_text (449233070804658627) --> <skip /> + <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="letterbox_restart_dialog_title" msgid="8543049527871033505">"Restart for a better view?"</string> + <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"You can restart the app so that it looks better on your screen, but you may lose your progress or any unsaved changes"</string> + <string name="letterbox_restart_cancel" msgid="1342209132692537805">"Cancel"</string> + <string name="letterbox_restart_restart" msgid="8529976234412442973">"Restart"</string> + <string name="letterbox_restart_dialog_checkbox_title" msgid="5252918008140768386">"Don\'t show again"</string> + <string name="letterbox_reachability_reposition_text" msgid="4507890186297500893">"Double-tap to move this app"</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> + <string name="back_button_text" msgid="1469718707134137085">"Back"</string> + <string name="handle_text" msgid="1766582106752184456">"Handle"</string> + <string name="app_icon_text" msgid="2823268023931811747">"App icon"</string> + <string name="fullscreen_text" msgid="1162316685217676079">"Full screen"</string> + <string name="desktop_text" msgid="1077633567027630454">"Desktop mode"</string> + <string name="split_screen_text" msgid="1396336058129570886">"Split screen"</string> + <string name="more_button_text" msgid="3655388105592893530">"More"</string> + <string name="float_button_text" msgid="9221657008391364581">"Float"</string> + <string name="select_text" msgid="5139083974039906583">"Select"</string> + <string name="screenshot_text" msgid="1477704010087786671">"Screenshot"</string> + <string name="close_text" msgid="4986518933445178928">"Close"</string> + <string name="collapse_menu_text" msgid="7515008122450342029">"Close menu"</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..71d02271090d 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="7930546669915337998">"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..8dee9ae82c5f 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> @@ -31,11 +32,17 @@ <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Resize"</string> <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Stash"</string> <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Unstash"</string> - <string name="dock_forced_resizable" msgid="1749750436092293116">"App may not work with split-screen."</string> - <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"App does not support split-screen."</string> + <!-- no translation found for dock_forced_resizable (7429086980048964687) --> + <skip /> + <!-- no translation found for dock_non_resizeble_failed_to_dock_text (2733543750291266047) --> + <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"This app can only be opened in one window."</string> <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> + <!-- no translation found for accessibility_divider (6407584574218956849) --> + <skip /> + <!-- no translation found for divider_title (1963391955593749442) --> + <skip /> <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> @@ -46,6 +53,10 @@ <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Top 50%"</string> <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Top 30%"</string> <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Bottom full screen"</string> + <string name="accessibility_split_left" msgid="1713683765575562458">"Split left"</string> + <string name="accessibility_split_right" msgid="8441001008181296837">"Split right"</string> + <string name="accessibility_split_top" msgid="2789329702027147146">"Split top"</string> + <string name="accessibility_split_bottom" msgid="8694551025220868191">"Split bottom"</string> <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Using one-handed mode"</string> <string name="one_handed_tutorial_description" msgid="3486582858591353067">"To exit, swipe up from the bottom of the screen or tap anywhere above the app"</string> <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Start one-handed mode"</string> @@ -61,6 +72,7 @@ <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Move bottom right"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> settings"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Dismiss bubble"</string> + <string name="bubbles_dont_bubble" msgid="3216183855437329223">"Don’t bubble"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Don’t bubble conversation"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Chat using bubbles"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"New conversations appear as floating icons, or bubbles. Tap to open bubble. Drag to move it."</string> @@ -72,17 +84,35 @@ <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) --> + <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"See and do more"</string> + <!-- no translation found for letterbox_education_split_screen_text (449233070804658627) --> <skip /> + <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="letterbox_restart_dialog_title" msgid="8543049527871033505">"Restart for a better view?"</string> + <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"You can restart the app so that it looks better on your screen, but you may lose your progress or any unsaved changes"</string> + <string name="letterbox_restart_cancel" msgid="1342209132692537805">"Cancel"</string> + <string name="letterbox_restart_restart" msgid="8529976234412442973">"Restart"</string> + <string name="letterbox_restart_dialog_checkbox_title" msgid="5252918008140768386">"Don\'t show again"</string> + <string name="letterbox_reachability_reposition_text" msgid="4507890186297500893">"Double-tap to move this app"</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> + <string name="back_button_text" msgid="1469718707134137085">"Back"</string> + <string name="handle_text" msgid="1766582106752184456">"Handle"</string> + <string name="app_icon_text" msgid="2823268023931811747">"App icon"</string> + <string name="fullscreen_text" msgid="1162316685217676079">"Full screen"</string> + <string name="desktop_text" msgid="1077633567027630454">"Desktop mode"</string> + <string name="split_screen_text" msgid="1396336058129570886">"Split screen"</string> + <string name="more_button_text" msgid="3655388105592893530">"More"</string> + <string name="float_button_text" msgid="9221657008391364581">"Float"</string> + <string name="select_text" msgid="5139083974039906583">"Select"</string> + <string name="screenshot_text" msgid="1477704010087786671">"Screenshot"</string> + <string name="close_text" msgid="4986518933445178928">"Close"</string> + <string name="collapse_menu_text" msgid="7515008122450342029">"Close menu"</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..71d02271090d 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="7930546669915337998">"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..b63af4c69cdd 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> @@ -31,11 +32,13 @@ <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Resize"</string> <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Stash"</string> <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Unstash"</string> - <string name="dock_forced_resizable" msgid="1749750436092293116">"App may not work with split-screen."</string> - <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"App does not support split-screen."</string> + <string name="dock_forced_resizable" msgid="7429086980048964687">"App may not work with split screen"</string> + <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"App does not support split screen"</string> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"This app can only be opened in 1 window."</string> <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="accessibility_divider" msgid="6407584574218956849">"Split screen divider"</string> + <string name="divider_title" msgid="1963391955593749442">"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> @@ -46,6 +49,10 @@ <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Top 50%"</string> <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Top 30%"</string> <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Bottom full screen"</string> + <string name="accessibility_split_left" msgid="1713683765575562458">"Split left"</string> + <string name="accessibility_split_right" msgid="8441001008181296837">"Split right"</string> + <string name="accessibility_split_top" msgid="2789329702027147146">"Split top"</string> + <string name="accessibility_split_bottom" msgid="8694551025220868191">"Split bottom"</string> <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Using one-handed mode"</string> <string name="one_handed_tutorial_description" msgid="3486582858591353067">"To exit, swipe up from the bottom of the screen or tap anywhere above the app"</string> <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Start one-handed mode"</string> @@ -61,6 +68,7 @@ <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Move bottom right"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> settings"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Dismiss bubble"</string> + <string name="bubbles_dont_bubble" msgid="3216183855437329223">"Don’t bubble"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Don’t bubble conversation"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Chat using bubbles"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"New conversations appear as floating icons, or bubbles. Tap to open bubble. Drag to move it."</string> @@ -72,13 +80,34 @@ <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="449233070804658627">"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="letterbox_restart_dialog_title" msgid="8543049527871033505">"Restart for a better view?"</string> + <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"You can restart the app so it looks better on your screen, but you may lose your progress or any unsaved changes"</string> + <string name="letterbox_restart_cancel" msgid="1342209132692537805">"Cancel"</string> + <string name="letterbox_restart_restart" msgid="8529976234412442973">"Restart"</string> + <string name="letterbox_restart_dialog_checkbox_title" msgid="5252918008140768386">"Don’t show again"</string> + <string name="letterbox_reachability_reposition_text" msgid="4507890186297500893">"Double-tap to move this app"</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> + <string name="back_button_text" msgid="1469718707134137085">"Back"</string> + <string name="handle_text" msgid="1766582106752184456">"Handle"</string> + <string name="app_icon_text" msgid="2823268023931811747">"App Icon"</string> + <string name="fullscreen_text" msgid="1162316685217676079">"Fullscreen"</string> + <string name="desktop_text" msgid="1077633567027630454">"Desktop Mode"</string> + <string name="split_screen_text" msgid="1396336058129570886">"Split Screen"</string> + <string name="more_button_text" msgid="3655388105592893530">"More"</string> + <string name="float_button_text" msgid="9221657008391364581">"Float"</string> + <string name="select_text" msgid="5139083974039906583">"Select"</string> + <string name="screenshot_text" msgid="1477704010087786671">"Screenshot"</string> + <string name="close_text" msgid="4986518933445178928">"Close"</string> + <string name="collapse_menu_text" msgid="7515008122450342029">"Close Menu"</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..405770166274 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="7930546669915337998">"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..6faae3c1b83c 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> @@ -31,11 +32,17 @@ <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Cambiar el tamaño"</string> <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Almacenar de manera segura"</string> <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Dejar de almacenar de manera segura"</string> - <string name="dock_forced_resizable" msgid="1749750436092293116">"Es posible que la app no funcione en el modo de pantalla dividida."</string> - <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"La app no es compatible con la función de pantalla dividida."</string> + <!-- no translation found for dock_forced_resizable (7429086980048964687) --> + <skip /> + <!-- no translation found for dock_non_resizeble_failed_to_dock_text (2733543750291266047) --> + <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Esta app solo puede estar abierta en 1 ventana."</string> <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> + <!-- no translation found for accessibility_divider (6407584574218956849) --> + <skip /> + <!-- no translation found for divider_title (1963391955593749442) --> + <skip /> <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,6 +53,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="accessibility_split_left" msgid="1713683765575562458">"Dividir a la izquierda"</string> + <string name="accessibility_split_right" msgid="8441001008181296837">"Dividir a la derecha"</string> + <string name="accessibility_split_top" msgid="2789329702027147146">"Dividir en la parte superior"</string> + <string name="accessibility_split_bottom" msgid="8694551025220868191">"Dividir en la parte inferior"</string> <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Cómo usar el modo de 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 presiona cualquier parte arriba de la app"</string> <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Iniciar el modo de una mano"</string> @@ -61,6 +72,7 @@ <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Ubicar abajo a la derecha"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"Configuración de <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Descartar burbuja"</string> + <string name="bubbles_dont_bubble" msgid="3216183855437329223">"No mostrar burbujas"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"No mostrar la conversación en burbuja"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Chat con burbujas"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"Las conversaciones nuevas aparecen como elementos flotantes o burbujas. Presiona para abrir la burbuja. Arrástrala para moverla."</string> @@ -72,17 +84,36 @@ <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) --> - <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) --> + <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Aprovecha más"</string> + <!-- no translation found for letterbox_education_split_screen_text (449233070804658627) --> <skip /> + <string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Presiona dos veces fuera de una app para cambiar su ubicación"</string> <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="letterbox_restart_dialog_title" msgid="8543049527871033505">"¿Quieres reiniciar para que se vea mejor?"</string> + <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"Puedes reiniciar la app para que se vea mejor en la pantalla, pero podrías perder tu progreso o cualquier cambio que no hayas guardado"</string> + <string name="letterbox_restart_cancel" msgid="1342209132692537805">"Cancelar"</string> + <string name="letterbox_restart_restart" msgid="8529976234412442973">"Reiniciar"</string> + <string name="letterbox_restart_dialog_checkbox_title" msgid="5252918008140768386">"No volver a mostrar"</string> + <!-- no translation found for letterbox_reachability_reposition_text (4507890186297500893) --> + <skip /> + <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> + <string name="back_button_text" msgid="1469718707134137085">"Atrás"</string> + <string name="handle_text" msgid="1766582106752184456">"Controlador"</string> + <string name="app_icon_text" msgid="2823268023931811747">"Ícono de la app"</string> + <string name="fullscreen_text" msgid="1162316685217676079">"Pantalla completa"</string> + <string name="desktop_text" msgid="1077633567027630454">"Modo de escritorio"</string> + <string name="split_screen_text" msgid="1396336058129570886">"Pantalla dividida"</string> + <string name="more_button_text" msgid="3655388105592893530">"Más"</string> + <string name="float_button_text" msgid="9221657008391364581">"Flotante"</string> + <string name="select_text" msgid="5139083974039906583">"Seleccionar"</string> + <string name="screenshot_text" msgid="1477704010087786671">"Captura de pantalla"</string> + <string name="close_text" msgid="4986518933445178928">"Cerrar"</string> + <string name="collapse_menu_text" msgid="7515008122450342029">"Cerrar menú"</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..e0f3297ff966 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="7930546669915337998">"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..8ec63b9bc23c 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> @@ -31,11 +32,17 @@ <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Cambiar tamaño"</string> <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Esconder"</string> <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"No esconder"</string> - <string name="dock_forced_resizable" msgid="1749750436092293116">"Es posible que la aplicación no funcione con la pantalla dividida."</string> - <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"La aplicación no admite la pantalla dividida."</string> + <!-- no translation found for dock_forced_resizable (7429086980048964687) --> + <skip /> + <!-- no translation found for dock_non_resizeble_failed_to_dock_text (2733543750291266047) --> + <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Esta aplicación solo puede abrirse en una ventana."</string> <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> + <!-- no translation found for accessibility_divider (6407584574218956849) --> + <skip /> + <!-- no translation found for divider_title (1963391955593749442) --> + <skip /> <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 +53,14 @@ <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="accessibility_split_left" msgid="1713683765575562458">"Dividir en la parte izquierda"</string> + <string name="accessibility_split_right" msgid="8441001008181296837">"Dividir en la parte derecha"</string> + <string name="accessibility_split_top" msgid="2789329702027147146">"Dividir en la parte superior"</string> + <string name="accessibility_split_bottom" msgid="8694551025220868191">"Dividir en la parte inferior"</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> @@ -61,9 +72,10 @@ <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Mover abajo a la derecha"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"Ajustes de <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Cerrar burbuja"</string> + <string name="bubbles_dont_bubble" msgid="3216183855437329223">"No mostrar burbujas"</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 +84,36 @@ <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) --> - <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) --> + <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Consulta más información y haz más"</string> + <!-- no translation found for letterbox_education_split_screen_text (449233070804658627) --> <skip /> + <string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Toca dos veces fuera de una aplicación para cambiarla de posición"</string> <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="letterbox_restart_dialog_title" msgid="8543049527871033505">"¿Reiniciar para que se vea mejor?"</string> + <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"Puedes reiniciar la aplicación para que se vea mejor en la pantalla, pero puedes perder tu progreso o cualquier cambio que no hayas guardado"</string> + <string name="letterbox_restart_cancel" msgid="1342209132692537805">"Cancelar"</string> + <string name="letterbox_restart_restart" msgid="8529976234412442973">"Reiniciar"</string> + <string name="letterbox_restart_dialog_checkbox_title" msgid="5252918008140768386">"No volver a mostrar"</string> + <!-- no translation found for letterbox_reachability_reposition_text (4507890186297500893) --> + <skip /> + <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> + <string name="back_button_text" msgid="1469718707134137085">"Atrás"</string> + <string name="handle_text" msgid="1766582106752184456">"Controlador"</string> + <string name="app_icon_text" msgid="2823268023931811747">"Icono de la aplicación"</string> + <string name="fullscreen_text" msgid="1162316685217676079">"Pantalla completa"</string> + <string name="desktop_text" msgid="1077633567027630454">"Modo Escritorio"</string> + <string name="split_screen_text" msgid="1396336058129570886">"Pantalla dividida"</string> + <string name="more_button_text" msgid="3655388105592893530">"Más"</string> + <string name="float_button_text" msgid="9221657008391364581">"Flotante"</string> + <string name="select_text" msgid="5139083974039906583">"Seleccionar"</string> + <string name="screenshot_text" msgid="1477704010087786671">"Captura de pantalla"</string> + <string name="close_text" msgid="4986518933445178928">"Cerrar"</string> + <string name="collapse_menu_text" msgid="7515008122450342029">"Cerrar menú"</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..38be3effe356 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="7930546669915337998">"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..5323bb5eec22 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> @@ -31,11 +32,17 @@ <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Suuruse muutmine"</string> <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Pane hoidlasse"</string> <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Eemalda hoidlast"</string> - <string name="dock_forced_resizable" msgid="1749750436092293116">"Rakendus ei pruugi poolitatud ekraaniga töötada."</string> - <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Rakendus ei toeta jagatud ekraani."</string> + <!-- no translation found for dock_forced_resizable (7429086980048964687) --> + <skip /> + <!-- no translation found for dock_non_resizeble_failed_to_dock_text (2733543750291266047) --> + <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Selle rakenduse saab avada ainult ühes aknas."</string> <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> + <!-- no translation found for accessibility_divider (6407584574218956849) --> + <skip /> + <!-- no translation found for divider_title (1963391955593749442) --> + <skip /> <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> @@ -46,6 +53,10 @@ <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Ülemine: 50%"</string> <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Ülemine: 30%"</string> <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Alumine täisekraan"</string> + <string name="accessibility_split_left" msgid="1713683765575562458">"Jaga vasakule"</string> + <string name="accessibility_split_right" msgid="8441001008181296837">"Jaga paremale"</string> + <string name="accessibility_split_top" msgid="2789329702027147146">"Jaga üles"</string> + <string name="accessibility_split_bottom" msgid="8694551025220868191">"Jaga alla"</string> <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Ühekäerežiimi kasutamine"</string> <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Väljumiseks pühkige ekraani alaosast üles või puudutage rakenduse kohal olevat ala"</string> <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Ühekäerežiimi käivitamine"</string> @@ -61,6 +72,7 @@ <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Teisalda alla paremale"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"Rakenduse <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> seaded"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Sule mull"</string> + <string name="bubbles_dont_bubble" msgid="3216183855437329223">"Ära kuva mulle"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Ära kuva vestlust mullina"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Vestelge mullide abil"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"Uued vestlused kuvatakse hõljuvate ikoonidena ehk mullidena. Puudutage mulli avamiseks. Lohistage mulli, et seda liigutada."</string> @@ -72,17 +84,36 @@ <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) --> - <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) --> + <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Vaadake ja tehke rohkem"</string> + <!-- no translation found for letterbox_education_split_screen_text (449233070804658627) --> <skip /> + <string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Topeltpuudutage rakendusest väljaspool, et selle asendit muuta"</string> <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="letterbox_restart_dialog_title" msgid="8543049527871033505">"Kas taaskäivitada parema vaate saavutamiseks?"</string> + <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"Saate rakenduse taaskäivitada, et see näeks ekraanikuval parem välja, kuid võite kaotada edenemise või salvestamata muudatused"</string> + <string name="letterbox_restart_cancel" msgid="1342209132692537805">"Tühista"</string> + <string name="letterbox_restart_restart" msgid="8529976234412442973">"Taaskäivita"</string> + <string name="letterbox_restart_dialog_checkbox_title" msgid="5252918008140768386">"Ära kuva uuesti"</string> + <!-- no translation found for letterbox_reachability_reposition_text (4507890186297500893) --> + <skip /> + <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> + <string name="back_button_text" msgid="1469718707134137085">"Tagasi"</string> + <string name="handle_text" msgid="1766582106752184456">"Käepide"</string> + <string name="app_icon_text" msgid="2823268023931811747">"Rakenduse ikoon"</string> + <string name="fullscreen_text" msgid="1162316685217676079">"Täisekraan"</string> + <string name="desktop_text" msgid="1077633567027630454">"Lauaarvuti režiim"</string> + <string name="split_screen_text" msgid="1396336058129570886">"Jagatud ekraanikuva"</string> + <string name="more_button_text" msgid="3655388105592893530">"Rohkem"</string> + <string name="float_button_text" msgid="9221657008391364581">"Hõljuv"</string> + <string name="select_text" msgid="5139083974039906583">"Vali"</string> + <string name="screenshot_text" msgid="1477704010087786671">"Ekraanipilt"</string> + <string name="close_text" msgid="4986518933445178928">"Sule"</string> + <string name="collapse_menu_text" msgid="7515008122450342029">"Sule menüü"</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..a93cee51ce07 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="7930546669915337998">"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..e7bdd8010727 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> @@ -31,11 +32,17 @@ <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Aldatu tamaina"</string> <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Gorde"</string> <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Ez gorde"</string> - <string name="dock_forced_resizable" msgid="1749750436092293116">"Baliteke aplikazioak ez funtzionatzea pantaila zatituan."</string> - <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Aplikazioak ez du onartzen pantaila zatitua"</string> + <!-- no translation found for dock_forced_resizable (7429086980048964687) --> + <skip /> + <!-- no translation found for dock_non_resizeble_failed_to_dock_text (2733543750291266047) --> + <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Leiho bakar batean ireki daiteke aplikazioa."</string> <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> + <!-- no translation found for accessibility_divider (6407584574218956849) --> + <skip /> + <!-- no translation found for divider_title (1963391955593749442) --> + <skip /> <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> @@ -46,6 +53,10 @@ <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Ezarri goialdea % 50en"</string> <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Ezarri goialdea % 30en"</string> <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Ezarri behealdea pantaila osoan"</string> + <string name="accessibility_split_left" msgid="1713683765575562458">"Zatitu ezkerraldean"</string> + <string name="accessibility_split_right" msgid="8441001008181296837">"Zatitu eskuinaldean"</string> + <string name="accessibility_split_top" msgid="2789329702027147146">"Zatitu goialdean"</string> + <string name="accessibility_split_bottom" msgid="8694551025220868191">"Zatitu behealdean"</string> <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Esku bakarreko modua erabiltzea"</string> <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Irteteko, pasatu hatza pantailaren behealdetik gora edo sakatu aplikazioaren gainaldea"</string> <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Abiarazi esku bakarreko modua"</string> @@ -61,6 +72,7 @@ <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Eraman behealdera, eskuinetara"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> aplikazioaren ezarpenak"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Baztertu burbuila"</string> + <string name="bubbles_dont_bubble" msgid="3216183855437329223">"Ez erakutsi burbuilarik"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Ez erakutsi elkarrizketak burbuila gisa"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Txateatu burbuilen bidez"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"Elkarrizketa berriak ikono gainerakor edo burbuila gisa agertzen dira. Sakatu burbuila irekitzeko. Arrasta ezazu mugitzeko."</string> @@ -72,17 +84,35 @@ <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) --> - <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) --> + <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Ikusi eta egin gauza gehiago"</string> + <!-- no translation found for letterbox_education_split_screen_text (449233070804658627) --> <skip /> + <string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Aplikazioaren posizioa aldatzeko, sakatu birritan haren kanpoaldea"</string> <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="letterbox_restart_dialog_title" msgid="8543049527871033505">"Aplikazioa berrabiarazi nahi duzu itxura hobea izan dezan?"</string> + <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"Aplikazioa berrabiarazi egin dezakezu itxura hobea izan dezan, baina agian garapena edo gorde gabeko aldaketak galduko dituzu"</string> + <string name="letterbox_restart_cancel" msgid="1342209132692537805">"Utzi"</string> + <string name="letterbox_restart_restart" msgid="8529976234412442973">"Berrabiarazi"</string> + <string name="letterbox_restart_dialog_checkbox_title" msgid="5252918008140768386">"Ez erakutsi berriro"</string> + <string name="letterbox_reachability_reposition_text" msgid="4507890186297500893">"Sakatu birritan aplikazioa mugitzeko"</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> + <string name="back_button_text" msgid="1469718707134137085">"Atzera"</string> + <string name="handle_text" msgid="1766582106752184456">"Kontu-izena"</string> + <string name="app_icon_text" msgid="2823268023931811747">"Aplikazioaren ikonoa"</string> + <string name="fullscreen_text" msgid="1162316685217676079">"Pantaila osoa"</string> + <string name="desktop_text" msgid="1077633567027630454">"Ordenagailuetarako modua"</string> + <string name="split_screen_text" msgid="1396336058129570886">"Pantaila zatitua"</string> + <string name="more_button_text" msgid="3655388105592893530">"Gehiago"</string> + <string name="float_button_text" msgid="9221657008391364581">"Leiho gainerakorra"</string> + <string name="select_text" msgid="5139083974039906583">"Hautatu"</string> + <string name="screenshot_text" msgid="1477704010087786671">"Pantaila-argazkia"</string> + <string name="close_text" msgid="4986518933445178928">"Itxi"</string> + <string name="collapse_menu_text" msgid="7515008122450342029">"Itxi menua"</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..4b752fc9d1c4 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="7930546669915337998">"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..c6ad275e5b18 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> @@ -31,11 +32,17 @@ <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"تغییر اندازه"</string> <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"مخفیسازی"</string> <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"لغو مخفیسازی"</string> - <string name="dock_forced_resizable" msgid="1749750436092293116">"ممکن است برنامه با «صفحهٔ دونیمه» کار نکند."</string> - <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"برنامه از تقسیم صفحه پشتیبانی نمیکند."</string> + <!-- no translation found for dock_forced_resizable (7429086980048964687) --> + <skip /> + <!-- no translation found for dock_non_resizeble_failed_to_dock_text (2733543750291266047) --> + <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"این برنامه فقط در ۱ پنجره میتواند باز شود."</string> <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> + <!-- no translation found for accessibility_divider (6407584574218956849) --> + <skip /> + <!-- no translation found for divider_title (1963391955593749442) --> + <skip /> <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> @@ -46,12 +53,16 @@ <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"٪۵۰ بالا"</string> <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"٪۳۰ بالا"</string> <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"تمامصفحه پایین"</string> + <string name="accessibility_split_left" msgid="1713683765575562458">"تقسیم از چپ"</string> + <string name="accessibility_split_right" msgid="8441001008181296837">"تقسیم از راست"</string> + <string name="accessibility_split_top" msgid="2789329702027147146">"تقسیم از بالا"</string> + <string name="accessibility_split_bottom" msgid="8694551025220868191">"تقسیم از پایین"</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_overflow_button_content_description" msgid="8160974472718594382">"سرریز"</string> <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"افزودن برگشت به پشته"</string> <string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> از <xliff:g id="APP_NAME">%2$s</xliff:g>"</string> <string name="bubble_content_description_stack" msgid="8071515017164630429">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> از <xliff:g id="APP_NAME">%2$s</xliff:g> و <xliff:g id="BUBBLE_COUNT">%3$d</xliff:g> مورد بیشتر"</string> @@ -61,6 +72,7 @@ <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"انتقال به پایین سمت چپ"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"تنظیمات <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"رد کردن حبابک"</string> + <string name="bubbles_dont_bubble" msgid="3216183855437329223">"حبابک نشان داده نشود"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"مکالمه در حباب نشان داده نشود"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"گپ بااستفاده از حبابکها"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"مکالمههای جدید بهصورت نمادهای شناور یا حبابکها نشان داده میشوند. برای باز کردن حبابکها ضربه بزنید. برای جابهجایی، آن را بکشید."</string> @@ -72,17 +84,35 @@ <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) --> - <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) --> + <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"از چندین برنامه بهطور همزمان استفاده کنید"</string> + <!-- no translation found for letterbox_education_split_screen_text (449233070804658627) --> <skip /> + <string name="letterbox_education_reposition_text" msgid="4589957299813220661">"برای جابهجا کردن برنامه، بیرون از آن دوضربه بزنید"</string> <string name="letterbox_education_got_it" msgid="4057634570866051177">"متوجهام"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"برای اطلاعات بیشتر، گسترده کنید."</string> + <string name="letterbox_restart_dialog_title" msgid="8543049527871033505">"برای نمایش بهتر بازراهاندازی شود؟"</string> + <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"میتوانید برنامه را بازراهاندازی کنید تا بهتر روی صفحهنمایش نشان داده شود، اما ممکن است پیشرفت یا تغییرات ذخیرهنشده را ازدست بدهید"</string> + <string name="letterbox_restart_cancel" msgid="1342209132692537805">"لغو کردن"</string> + <string name="letterbox_restart_restart" msgid="8529976234412442973">"بازراهاندازی"</string> + <string name="letterbox_restart_dialog_checkbox_title" msgid="5252918008140768386">"دوباره نشان داده نشود"</string> + <string name="letterbox_reachability_reposition_text" msgid="4507890186297500893">"برای جابهجایی این برنامه، دوضربه بزنید"</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> + <string name="back_button_text" msgid="1469718707134137085">"برگشتن"</string> + <string name="handle_text" msgid="1766582106752184456">"دستگیره"</string> + <string name="app_icon_text" msgid="2823268023931811747">"نماد برنامه"</string> + <string name="fullscreen_text" msgid="1162316685217676079">"تمامصفحه"</string> + <string name="desktop_text" msgid="1077633567027630454">"حالت رایانه"</string> + <string name="split_screen_text" msgid="1396336058129570886">"صفحهٔ دونیمه"</string> + <string name="more_button_text" msgid="3655388105592893530">"بیشتر"</string> + <string name="float_button_text" msgid="9221657008391364581">"شناور"</string> + <string name="select_text" msgid="5139083974039906583">"انتخاب"</string> + <string name="screenshot_text" msgid="1477704010087786671">"نماگرفت"</string> + <string name="close_text" msgid="4986518933445178928">"بستن"</string> + <string name="collapse_menu_text" msgid="7515008122450342029">"بستن منو"</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..55394cbdc31a 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="7930546669915337998">"برای کنترلها، دکمه "<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..b9f72721d4ed 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> @@ -31,11 +32,17 @@ <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Muuta kokoa"</string> <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Lisää turvasäilytykseen"</string> <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Poista turvasäilytyksestä"</string> - <string name="dock_forced_resizable" msgid="1749750436092293116">"Sovellus ei ehkä toimi jaetulla näytöllä."</string> - <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Sovellus ei tue jaetun näytön tilaa."</string> + <!-- no translation found for dock_forced_resizable (7429086980048964687) --> + <skip /> + <!-- no translation found for dock_non_resizeble_failed_to_dock_text (2733543750291266047) --> + <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Tämän sovelluksen voi avata vain yhdessä ikkunassa."</string> <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> + <!-- no translation found for accessibility_divider (6407584574218956849) --> + <skip /> + <!-- no translation found for divider_title (1963391955593749442) --> + <skip /> <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> @@ -46,6 +53,10 @@ <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Yläosa 50 %"</string> <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Yläosa 30 %"</string> <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Alaosa koko näytölle"</string> + <string name="accessibility_split_left" msgid="1713683765575562458">"Vasemmalla"</string> + <string name="accessibility_split_right" msgid="8441001008181296837">"Oikealla"</string> + <string name="accessibility_split_top" msgid="2789329702027147146">"Ylhäällä"</string> + <string name="accessibility_split_bottom" msgid="8694551025220868191">"Alhaalla"</string> <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Yhden käden moodin käyttö"</string> <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Poistu pyyhkäisemällä ylös näytön alareunasta tai napauttamalla sovelluksen yllä"</string> <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Käynnistä yhden käden moodi"</string> @@ -61,6 +72,7 @@ <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Siirrä oikeaan alareunaan"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>: asetukset"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Ohita kupla"</string> + <string name="bubbles_dont_bubble" msgid="3216183855437329223">"Älä näytä puhekuplia"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Älä näytä kuplia keskusteluista"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Chattaile kuplien avulla"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"Uudet keskustelut näkyvät kelluvina kuvakkeina tai kuplina. Avaa kupla napauttamalla. Siirrä sitä vetämällä."</string> @@ -72,17 +84,36 @@ <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) --> - <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) --> + <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Näe ja tee enemmän"</string> + <!-- no translation found for letterbox_education_split_screen_text (449233070804658627) --> <skip /> + <string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Kaksoisnapauta sovelluksen ulkopuolella, jos haluat siirtää sitä"</string> <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="letterbox_restart_dialog_title" msgid="8543049527871033505">"Käynnistetäänkö sovellus uudelleen, niin saat paremman näkymän?"</string> + <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"Voit käynnistää sovelluksen uudelleen, jotta se näyttää paremmalta näytöllä, mutta saatat menettää edistymisesi tai tallentamattomat muutokset"</string> + <string name="letterbox_restart_cancel" msgid="1342209132692537805">"Peru"</string> + <string name="letterbox_restart_restart" msgid="8529976234412442973">"Käynnistä uudelleen"</string> + <string name="letterbox_restart_dialog_checkbox_title" msgid="5252918008140768386">"Älä näytä uudelleen"</string> + <!-- no translation found for letterbox_reachability_reposition_text (4507890186297500893) --> + <skip /> + <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> + <string name="back_button_text" msgid="1469718707134137085">"Takaisin"</string> + <string name="handle_text" msgid="1766582106752184456">"Kahva"</string> + <string name="app_icon_text" msgid="2823268023931811747">"Sovelluskuvake"</string> + <string name="fullscreen_text" msgid="1162316685217676079">"Koko näyttö"</string> + <string name="desktop_text" msgid="1077633567027630454">"Työpöytätila"</string> + <string name="split_screen_text" msgid="1396336058129570886">"Jaettu näyttö"</string> + <string name="more_button_text" msgid="3655388105592893530">"Lisää"</string> + <string name="float_button_text" msgid="9221657008391364581">"Kelluva ikkuna"</string> + <string name="select_text" msgid="5139083974039906583">"Valitse"</string> + <string name="screenshot_text" msgid="1477704010087786671">"Kuvakaappaus"</string> + <string name="close_text" msgid="4986518933445178928">"Sulje"</string> + <string name="collapse_menu_text" msgid="7515008122450342029">"Sulje valikko"</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..f580d01691f9 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="7930546669915337998">"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..8db7790c72b1 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> @@ -31,11 +32,17 @@ <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Redimensionner"</string> <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Ajouter à la réserve"</string> <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Retirer de la réserve"</string> - <string name="dock_forced_resizable" msgid="1749750436092293116">"Il est possible que l\'application ne fonctionne pas en mode Écran partagé."</string> - <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"L\'application n\'est pas compatible avec l\'écran partagé."</string> + <!-- no translation found for dock_forced_resizable (7429086980048964687) --> + <skip /> + <!-- no translation found for dock_non_resizeble_failed_to_dock_text (2733543750291266047) --> + <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Cette application ne peut être ouverte que dans une fenêtre."</string> <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> + <!-- no translation found for accessibility_divider (6407584574218956849) --> + <skip /> + <!-- no translation found for divider_title (1963391955593749442) --> + <skip /> <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> @@ -46,6 +53,10 @@ <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"50 % dans le haut"</string> <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"30 % dans le haut"</string> <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Plein écran dans le bas"</string> + <string name="accessibility_split_left" msgid="1713683765575562458">"Diviser à gauche"</string> + <string name="accessibility_split_right" msgid="8441001008181296837">"Diviser à droite"</string> + <string name="accessibility_split_top" msgid="2789329702027147146">"Diviser dans la partie supérieure"</string> + <string name="accessibility_split_bottom" msgid="8694551025220868191">"Diviser dans la partie inférieure"</string> <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Utiliser le mode Une main"</string> <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Pour quitter, balayez l\'écran du bas vers le haut, ou touchez n\'importe où sur l\'écran en haut de l\'application"</string> <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Démarrer le mode Une main"</string> @@ -61,6 +72,7 @@ <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Déplacer dans coin inf. droit"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"Paramètres <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Ignorer la bulle"</string> + <string name="bubbles_dont_bubble" msgid="3216183855437329223">"Ne pas afficher de bulles"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Ne pas afficher les conversations dans des bulles"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Clavarder en utilisant des bulles"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"Les nouvelles conversations s\'affichent sous forme d\'icônes flottantes (de bulles). Touchez une bulle pour l\'ouvrir. Faites-la glisser pour la déplacer."</string> @@ -72,17 +84,36 @@ <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) --> - <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) --> + <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Voir et en faire plus"</string> + <!-- no translation found for letterbox_education_split_screen_text (449233070804658627) --> <skip /> + <string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Touchez deux fois à côté d\'une application pour la repositionner"</string> <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="letterbox_restart_dialog_title" msgid="8543049527871033505">"Redémarrer pour un meilleur affichage?"</string> + <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"Vous pouvez redémarrer l\'application pour qu\'elle s\'affiche mieux sur votre écran, mais il se peut que vous perdiez votre progression ou toute modification non enregistrée"</string> + <string name="letterbox_restart_cancel" msgid="1342209132692537805">"Annuler"</string> + <string name="letterbox_restart_restart" msgid="8529976234412442973">"Redémarrer"</string> + <string name="letterbox_restart_dialog_checkbox_title" msgid="5252918008140768386">"Ne plus afficher"</string> + <!-- no translation found for letterbox_reachability_reposition_text (4507890186297500893) --> + <skip /> + <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> + <string name="back_button_text" msgid="1469718707134137085">"Retour"</string> + <string name="handle_text" msgid="1766582106752184456">"Identifiant"</string> + <string name="app_icon_text" msgid="2823268023931811747">"Icône de l\'application"</string> + <string name="fullscreen_text" msgid="1162316685217676079">"Plein écran"</string> + <string name="desktop_text" msgid="1077633567027630454">"Mode Bureau"</string> + <string name="split_screen_text" msgid="1396336058129570886">"Écran partagé"</string> + <string name="more_button_text" msgid="3655388105592893530">"Plus"</string> + <string name="float_button_text" msgid="9221657008391364581">"Flottant"</string> + <string name="select_text" msgid="5139083974039906583">"Sélectionner"</string> + <string name="screenshot_text" msgid="1477704010087786671">"Capture d\'écran"</string> + <string name="close_text" msgid="4986518933445178928">"Fermer"</string> + <string name="collapse_menu_text" msgid="7515008122450342029">"Fermer le menu"</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..39a785d4fcc0 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="7930546669915337998">"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..8d4bccab9d9f 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> @@ -31,11 +32,17 @@ <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Redimensionner"</string> <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Stash"</string> <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Unstash"</string> - <string name="dock_forced_resizable" msgid="1749750436092293116">"Il est possible que l\'application ne fonctionne pas en mode Écran partagé."</string> - <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Application incompatible avec l\'écran partagé."</string> + <!-- no translation found for dock_forced_resizable (7429086980048964687) --> + <skip /> + <!-- no translation found for dock_non_resizeble_failed_to_dock_text (2733543750291266047) --> + <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Cette appli ne peut être ouverte que dans 1 fenêtre."</string> <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> + <!-- no translation found for accessibility_divider (6407584574218956849) --> + <skip /> + <!-- no translation found for divider_title (1963391955593749442) --> + <skip /> <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> @@ -46,6 +53,10 @@ <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Écran du haut à 50 %"</string> <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Écran du haut à 30 %"</string> <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Écran du bas en plein écran"</string> + <string name="accessibility_split_left" msgid="1713683765575562458">"Affichée à gauche"</string> + <string name="accessibility_split_right" msgid="8441001008181296837">"Affichée à droite"</string> + <string name="accessibility_split_top" msgid="2789329702027147146">"Affichée en haut"</string> + <string name="accessibility_split_bottom" msgid="8694551025220868191">"Affichée en haut"</string> <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Utiliser le mode une main"</string> <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Pour quitter, balayez l\'écran de bas en haut ou appuyez n\'importe où au-dessus de l\'application"</string> <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Démarrer le mode une main"</string> @@ -61,9 +72,10 @@ <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Déplacer en bas à droite"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"Paramètres <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Fermer la bulle"</string> + <string name="bubbles_dont_bubble" msgid="3216183855437329223">"Désactiver les info-bulles"</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 +84,36 @@ <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) --> - <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) --> + <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Voir et interagir plus"</string> + <!-- no translation found for letterbox_education_split_screen_text (449233070804658627) --> <skip /> + <string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Appuyez deux fois en dehors d\'une appli pour la repositionner"</string> <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="letterbox_restart_dialog_title" msgid="8543049527871033505">"Redémarrer pour améliorer l\'affichage ?"</string> + <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"Vous pouvez redémarrer l\'appli pour en améliorer son aspect sur votre écran, mais vous risquez de perdre votre progression ou les modifications non enregistrées"</string> + <string name="letterbox_restart_cancel" msgid="1342209132692537805">"Annuler"</string> + <string name="letterbox_restart_restart" msgid="8529976234412442973">"Redémarrer"</string> + <string name="letterbox_restart_dialog_checkbox_title" msgid="5252918008140768386">"Ne plus afficher"</string> + <!-- no translation found for letterbox_reachability_reposition_text (4507890186297500893) --> + <skip /> + <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> + <string name="back_button_text" msgid="1469718707134137085">"Retour"</string> + <string name="handle_text" msgid="1766582106752184456">"Poignée"</string> + <string name="app_icon_text" msgid="2823268023931811747">"Icône d\'application"</string> + <string name="fullscreen_text" msgid="1162316685217676079">"Plein écran"</string> + <string name="desktop_text" msgid="1077633567027630454">"Mode ordinateur"</string> + <string name="split_screen_text" msgid="1396336058129570886">"Écran partagé"</string> + <string name="more_button_text" msgid="3655388105592893530">"Plus"</string> + <string name="float_button_text" msgid="9221657008391364581">"Flottante"</string> + <string name="select_text" msgid="5139083974039906583">"Sélectionner"</string> + <string name="screenshot_text" msgid="1477704010087786671">"Capture d\'écran"</string> + <string name="close_text" msgid="4986518933445178928">"Fermer"</string> + <string name="collapse_menu_text" msgid="7515008122450342029">"Fermer le menu"</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..db4bc54cf665 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="7930546669915337998">"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..7c09c76f3185 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> @@ -31,11 +32,17 @@ <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Cambiar tamaño"</string> <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Esconder"</string> <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Non esconder"</string> - <string name="dock_forced_resizable" msgid="1749750436092293116">"Pode que a aplicación non funcione coa pantalla dividida."</string> - <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"A aplicación non é compatible coa función de pantalla dividida."</string> + <!-- no translation found for dock_forced_resizable (7429086980048964687) --> + <skip /> + <!-- no translation found for dock_non_resizeble_failed_to_dock_text (2733543750291266047) --> + <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Esta aplicación só se pode abrir en 1 ventá."</string> <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> + <!-- no translation found for accessibility_divider (6407584574218956849) --> + <skip /> + <!-- no translation found for divider_title (1963391955593749442) --> + <skip /> <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> @@ -46,6 +53,10 @@ <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"50 % arriba"</string> <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"30 % arriba"</string> <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Pantalla completa abaixo"</string> + <string name="accessibility_split_left" msgid="1713683765575562458">"Dividir (esquerda)"</string> + <string name="accessibility_split_right" msgid="8441001008181296837">"Dividir (dereita)"</string> + <string name="accessibility_split_top" msgid="2789329702027147146">"Dividir (arriba)"</string> + <string name="accessibility_split_bottom" msgid="8694551025220868191">"Dividir (abaixo)"</string> <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Como se usa o modo dunha soa man?"</string> <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Para saír, pasa o dedo cara arriba desde a parte inferior da pantalla ou toca calquera lugar da zona situada encima da aplicación"</string> <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Iniciar modo dunha soa man"</string> @@ -61,6 +72,7 @@ <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Mover á parte inferior dereita"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"Configuración de <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Ignorar burbulla"</string> + <string name="bubbles_dont_bubble" msgid="3216183855437329223">"Non mostrar burbullas"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Non mostrar a conversa como burbulla"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Chatear usando burbullas"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"As conversas novas aparecen como iconas flotantes ou burbullas. Toca para abrir a burbulla e arrastra para movela."</string> @@ -72,17 +84,36 @@ <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) --> - <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) --> + <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Ver e facer máis"</string> + <!-- no translation found for letterbox_education_split_screen_text (449233070804658627) --> <skip /> + <string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Toca dúas veces fóra da aplicación para cambiala de posición"</string> <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="letterbox_restart_dialog_title" msgid="8543049527871033505">"Queres reiniciar a aplicación para que se vexa mellor?"</string> + <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"Podes reiniciar a aplicación para que se vexa mellor na pantalla, pero podes perder o progreso que levas feito ou calquera cambio que non gardases"</string> + <string name="letterbox_restart_cancel" msgid="1342209132692537805">"Cancelar"</string> + <string name="letterbox_restart_restart" msgid="8529976234412442973">"Reiniciar"</string> + <string name="letterbox_restart_dialog_checkbox_title" msgid="5252918008140768386">"Non mostrar outra vez"</string> + <!-- no translation found for letterbox_reachability_reposition_text (4507890186297500893) --> + <skip /> + <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> + <string name="back_button_text" msgid="1469718707134137085">"Atrás"</string> + <string name="handle_text" msgid="1766582106752184456">"Controlador"</string> + <string name="app_icon_text" msgid="2823268023931811747">"Icona de aplicación"</string> + <string name="fullscreen_text" msgid="1162316685217676079">"Pantalla completa"</string> + <string name="desktop_text" msgid="1077633567027630454">"Modo de escritorio"</string> + <string name="split_screen_text" msgid="1396336058129570886">"Pantalla dividida"</string> + <string name="more_button_text" msgid="3655388105592893530">"Máis"</string> + <string name="float_button_text" msgid="9221657008391364581">"Flotante"</string> + <string name="select_text" msgid="5139083974039906583">"Seleccionar"</string> + <string name="screenshot_text" msgid="1477704010087786671">"Captura de pantalla"</string> + <string name="close_text" msgid="4986518933445178928">"Pechar"</string> + <string name="collapse_menu_text" msgid="7515008122450342029">"Pechar o menú"</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..22e68d3707ac 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="7930546669915337998">"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..f968bd5be1a8 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> @@ -31,11 +32,17 @@ <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"કદ બદલો"</string> <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"છુપાવો"</string> <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"બતાવો"</string> - <string name="dock_forced_resizable" msgid="1749750436092293116">"વિભાજિત-સ્ક્રીન સાથે ઍપ કદાચ કામ ન કરે."</string> - <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"ઍપ્લિકેશન સ્ક્રીન-વિભાજનનું સમર્થન કરતી નથી."</string> + <!-- no translation found for dock_forced_resizable (7429086980048964687) --> + <skip /> + <!-- no translation found for dock_non_resizeble_failed_to_dock_text (2733543750291266047) --> + <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"આ ઍપ માત્ર 1 વિન્ડોમાં ખોલી શકાય છે."</string> <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> + <!-- no translation found for accessibility_divider (6407584574218956849) --> + <skip /> + <!-- no translation found for divider_title (1963391955593749442) --> + <skip /> <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> @@ -46,6 +53,10 @@ <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"શીર્ષ 50%"</string> <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"શીર્ષ 30%"</string> <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"તળિયાની પૂર્ણ સ્ક્રીન"</string> + <string name="accessibility_split_left" msgid="1713683765575562458">"ડાબે વિભાજિત કરો"</string> + <string name="accessibility_split_right" msgid="8441001008181296837">"જમણે વિભાજિત કરો"</string> + <string name="accessibility_split_top" msgid="2789329702027147146">"ઉપર વિભાજિત કરો"</string> + <string name="accessibility_split_bottom" msgid="8694551025220868191">"નીચે વિભાજિત કરો"</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> @@ -61,6 +72,7 @@ <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"નીચે જમણે ખસેડો"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> સેટિંગ"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"બબલને છોડી દો"</string> + <string name="bubbles_dont_bubble" msgid="3216183855437329223">"બબલ બતાવશો નહીં"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"વાતચીતને બબલ કરશો નહીં"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"બબલનો ઉપયોગ કરીને ચૅટ કરો"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"નવી વાતચીત ફ્લોટિંગ આઇકન અથવા બબલ જેવી દેખાશે. બબલને ખોલવા માટે ટૅપ કરો. તેને ખસેડવા માટે ખેંચો."</string> @@ -72,17 +84,36 @@ <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) --> - <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) --> + <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"જુઓ અને બીજું ઘણું કરો"</string> + <!-- no translation found for letterbox_education_split_screen_text (449233070804658627) --> <skip /> + <string name="letterbox_education_reposition_text" msgid="4589957299813220661">"કોઈ ઍપની જગ્યા બદલવા માટે, તેની બહાર બે વાર ટૅપ કરો"</string> <string name="letterbox_education_got_it" msgid="4057634570866051177">"સમજાઈ ગયું"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"વધુ માહિતી માટે મોટું કરો."</string> + <string name="letterbox_restart_dialog_title" msgid="8543049527871033505">"બહેતર વ્યૂ માટે ફરીથી શરૂ કરીએ?"</string> + <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"તમે ઍપને ફરીથી શરૂ કરી શકો છો, જેથી તે તમારી સ્ક્રીન પર વધુ સારી રીતે દેખાય, પરંતુ આમ કરવાથી તમે તમારી ઍપ પર કરી હોય એવી કોઈ પ્રક્રિયાની પ્રગતિ અથવા સાચવ્યા ન હોય એવો કોઈપણ ફેરફાર ગુમાવી શકો છો"</string> + <string name="letterbox_restart_cancel" msgid="1342209132692537805">"રદ કરો"</string> + <string name="letterbox_restart_restart" msgid="8529976234412442973">"ફરી શરૂ કરો"</string> + <string name="letterbox_restart_dialog_checkbox_title" msgid="5252918008140768386">"ફરીથી બતાવશો નહીં"</string> + <!-- no translation found for letterbox_reachability_reposition_text (4507890186297500893) --> + <skip /> + <string name="maximize_button_text" msgid="1650859196290301963">"મોટું કરો"</string> + <string name="minimize_button_text" msgid="271592547935841753">"નાનું કરો"</string> + <string name="close_button_text" msgid="2913281996024033299">"બંધ કરો"</string> + <string name="back_button_text" msgid="1469718707134137085">"પાછળ"</string> + <string name="handle_text" msgid="1766582106752184456">"હૅન્ડલ"</string> + <string name="app_icon_text" msgid="2823268023931811747">"ઍપનું આઇકન"</string> + <string name="fullscreen_text" msgid="1162316685217676079">"પૂર્ણસ્ક્રીન"</string> + <string name="desktop_text" msgid="1077633567027630454">"ડેસ્કટૉપ મોડ"</string> + <string name="split_screen_text" msgid="1396336058129570886">"સ્ક્રીનને વિભાજિત કરો"</string> + <string name="more_button_text" msgid="3655388105592893530">"વધુ"</string> + <string name="float_button_text" msgid="9221657008391364581">"ફ્લોટિંગ વિન્ડો"</string> + <string name="select_text" msgid="5139083974039906583">"પસંદ કરો"</string> + <string name="screenshot_text" msgid="1477704010087786671">"સ્ક્રીનશૉટ"</string> + <string name="close_text" msgid="4986518933445178928">"બંધ કરો"</string> + <string name="collapse_menu_text" msgid="7515008122450342029">"મેનૂ બંધ કરો"</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..01b9b4b987d0 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="7930546669915337998">"નિયંત્રણો માટે "<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..805a8813844b 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> @@ -31,11 +32,17 @@ <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"आकार बदलें"</string> <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"छिपाएं"</string> <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"दिखाएं"</string> - <string name="dock_forced_resizable" msgid="1749750436092293116">"ऐप्लिकेशन शायद स्प्लिट स्क्रीन मोड में काम न करे."</string> - <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"ऐप विभाजित स्क्रीन का समर्थन नहीं करता है."</string> + <!-- no translation found for dock_forced_resizable (7429086980048964687) --> + <skip /> + <!-- no translation found for dock_non_resizeble_failed_to_dock_text (2733543750291266047) --> + <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"इस ऐप्लिकेशन को सिर्फ़ एक विंडो में खोला जा सकता है."</string> <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> + <!-- no translation found for accessibility_divider (6407584574218956849) --> + <skip /> + <!-- no translation found for divider_title (1963391955593749442) --> + <skip /> <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> @@ -46,6 +53,10 @@ <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"ऊपर की स्क्रीन को 50% बनाएं"</string> <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"ऊपर की स्क्रीन को 30% बनाएं"</string> <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"नीचे की स्क्रीन को फ़ुल स्क्रीन बनाएं"</string> + <string name="accessibility_split_left" msgid="1713683765575562458">"स्क्रीन को बाएं हिस्से में स्प्लिट करें"</string> + <string name="accessibility_split_right" msgid="8441001008181296837">"स्क्रीन को दाएं हिस्से में स्प्लिट करें"</string> + <string name="accessibility_split_top" msgid="2789329702027147146">"स्क्रीन को ऊपर के हिस्से में स्प्लिट करें"</string> + <string name="accessibility_split_bottom" msgid="8694551025220868191">"स्क्रीन को सबसे नीचे वाले हिस्से में स्प्लिट करें"</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> @@ -61,28 +72,48 @@ <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"सबसे नीचे दाईं ओर ले जाएं"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> की सेटिंग"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"बबल खारिज करें"</string> + <string name="bubbles_dont_bubble" msgid="3216183855437329223">"बबल होने से रोकें"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"बातचीत को बबल न करें"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"बबल्स का इस्तेमाल करके चैट करें"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"नई बातचीत फ़्लोटिंग आइकॉन या बबल्स की तरह दिखेंगी. बबल को खोलने के लिए टैप करें. इसे एक जगह से दूसरी जगह ले जाने के लिए खींचें और छोड़ें."</string> <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) --> - <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) --> + <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"पूरी जानकारी लेकर, बेहतर तरीके से काम करें"</string> + <!-- no translation found for letterbox_education_split_screen_text (449233070804658627) --> <skip /> + <string name="letterbox_education_reposition_text" msgid="4589957299813220661">"किसी ऐप्लिकेशन की जगह बदलने के लिए, उसके बाहर दो बार टैप करें"</string> <string name="letterbox_education_got_it" msgid="4057634570866051177">"ठीक है"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"ज़्यादा जानकारी के लिए बड़ा करें."</string> + <string name="letterbox_restart_dialog_title" msgid="8543049527871033505">"बेहतर व्यू पाने के लिए ऐप्लिकेशन को रीस्टार्ट करना है?"</string> + <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"स्क्रीन पर ऐप्लिकेशन का बेहतर व्यू पाने के लिए उसे रीस्टार्ट करें. हालांकि, आपने जो बदलाव सेव नहीं किए हैं या अब तक जो काम किए हैं उनका डेटा, ऐप्लिकेशन रीस्टार्ट करने पर मिट सकता है"</string> + <string name="letterbox_restart_cancel" msgid="1342209132692537805">"रद्द करें"</string> + <string name="letterbox_restart_restart" msgid="8529976234412442973">"रीस्टार्ट करें"</string> + <string name="letterbox_restart_dialog_checkbox_title" msgid="5252918008140768386">"फिर से न दिखाएं"</string> + <!-- no translation found for letterbox_reachability_reposition_text (4507890186297500893) --> + <skip /> + <string name="maximize_button_text" msgid="1650859196290301963">"बड़ा करें"</string> + <string name="minimize_button_text" msgid="271592547935841753">"विंडो छोटी करें"</string> + <string name="close_button_text" msgid="2913281996024033299">"बंद करें"</string> + <string name="back_button_text" msgid="1469718707134137085">"वापस जाएं"</string> + <string name="handle_text" msgid="1766582106752184456">"हैंडल"</string> + <string name="app_icon_text" msgid="2823268023931811747">"ऐप्लिकेशन आइकॉन"</string> + <string name="fullscreen_text" msgid="1162316685217676079">"फ़ुलस्क्रीन"</string> + <string name="desktop_text" msgid="1077633567027630454">"डेस्कटॉप मोड"</string> + <string name="split_screen_text" msgid="1396336058129570886">"स्प्लिट स्क्रीन मोड"</string> + <string name="more_button_text" msgid="3655388105592893530">"ज़्यादा देखें"</string> + <string name="float_button_text" msgid="9221657008391364581">"फ़्लोट"</string> + <string name="select_text" msgid="5139083974039906583">"चुनें"</string> + <string name="screenshot_text" msgid="1477704010087786671">"स्क्रीनशॉट"</string> + <string name="close_text" msgid="4986518933445178928">"बंद करें"</string> + <string name="collapse_menu_text" msgid="7515008122450342029">"मेन्यू बंद करें"</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..e2f272c36329 100644 --- a/libs/WindowManager/Shell/res/values-hi/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-hi/strings_tv.xml @@ -18,8 +18,17 @@ <resources xmlns:android="http://schemas.android.com/apk/res/android" 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_notification_unknown_title" msgid="2729870284350772311">"(कोई टाइटल कार्यक्रम नहीं)"</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="7930546669915337998">"कंट्रोल मेन्यू पर जाने के लिए "<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..69373348b41e 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> @@ -31,11 +32,17 @@ <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Promjena veličine"</string> <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Sakrijte"</string> <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Poništite sakrivanje"</string> - <string name="dock_forced_resizable" msgid="1749750436092293116">"Aplikacija možda neće funkcionirati s podijeljenim zaslonom."</string> - <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Aplikacija ne podržava podijeljeni zaslon."</string> + <!-- no translation found for dock_forced_resizable (7429086980048964687) --> + <skip /> + <!-- no translation found for dock_non_resizeble_failed_to_dock_text (2733543750291266047) --> + <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Ova se aplikacija može otvoriti samo u jednom prozoru."</string> <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> + <!-- no translation found for accessibility_divider (6407584574218956849) --> + <skip /> + <!-- no translation found for divider_title (1963391955593749442) --> + <skip /> <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> @@ -46,6 +53,10 @@ <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Gornji zaslon na 50%"</string> <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Gornji zaslon na 30%"</string> <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Donji zaslon u cijeli zaslon"</string> + <string name="accessibility_split_left" msgid="1713683765575562458">"Podijeli lijevo"</string> + <string name="accessibility_split_right" msgid="8441001008181296837">"Podijeli desno"</string> + <string name="accessibility_split_top" msgid="2789329702027147146">"Podijeli gore"</string> + <string name="accessibility_split_bottom" msgid="8694551025220868191">"Podijeli dolje"</string> <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Korištenje načina rada jednom rukom"</string> <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Za izlaz prijeđite prstom od dna zaslona prema gore ili dodirnite bio gdje iznad aplikacije"</string> <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Pokretanje načina rada jednom rukom"</string> @@ -61,6 +72,7 @@ <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Premjestite u donji desni kut"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"Postavke za <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Odbaci oblačić"</string> + <string name="bubbles_dont_bubble" msgid="3216183855437329223">"Ne prikazuj oblačiće"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Zaustavi razgovor u oblačićima"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Oblačići u chatu"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"Novi razgovori pojavljuju se kao pomične ikone ili oblačići. Dodirnite za otvaranje oblačića. Povucite da biste ga premjestili."</string> @@ -72,17 +84,35 @@ <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) --> - <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) --> + <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Gledajte i učinite više"</string> + <!-- no translation found for letterbox_education_split_screen_text (449233070804658627) --> <skip /> + <string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Dvaput dodirnite izvan aplikacije da biste je premjestili"</string> <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="letterbox_restart_dialog_title" msgid="8543049527871033505">"Želite li ponovno pokrenuti za bolji pregled?"</string> + <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"Možete ponovno pokrenuti aplikaciju tako da bolje izgleda na zaslonu, no mogli biste izgubiti napredak ili sve nespremljene promjene"</string> + <string name="letterbox_restart_cancel" msgid="1342209132692537805">"Odustani"</string> + <string name="letterbox_restart_restart" msgid="8529976234412442973">"Pokreni ponovno"</string> + <string name="letterbox_restart_dialog_checkbox_title" msgid="5252918008140768386">"Ne prikazuj ponovno"</string> + <string name="letterbox_reachability_reposition_text" msgid="4507890186297500893">"Dvaput dodirnite da biste premjestili ovu aplikaciju"</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> + <string name="back_button_text" msgid="1469718707134137085">"Natrag"</string> + <string name="handle_text" msgid="1766582106752184456">"Pokazivač"</string> + <string name="app_icon_text" msgid="2823268023931811747">"Ikona aplikacije"</string> + <string name="fullscreen_text" msgid="1162316685217676079">"Puni zaslon"</string> + <string name="desktop_text" msgid="1077633567027630454">"Stolni način rada"</string> + <string name="split_screen_text" msgid="1396336058129570886">"Razdvojeni zaslon"</string> + <string name="more_button_text" msgid="3655388105592893530">"Više"</string> + <string name="float_button_text" msgid="9221657008391364581">"Plutajući"</string> + <string name="select_text" msgid="5139083974039906583">"Odaberite"</string> + <string name="screenshot_text" msgid="1477704010087786671">"Snimka zaslona"</string> + <string name="close_text" msgid="4986518933445178928">"Zatvorite"</string> + <string name="collapse_menu_text" msgid="7515008122450342029">"Zatvorite izbornik"</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..965b9b8c31c6 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="7930546669915337998">"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..4ef9f465dc41 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> @@ -31,11 +32,17 @@ <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Átméretezés"</string> <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Félretevés"</string> <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Félretevés megszüntetése"</string> - <string name="dock_forced_resizable" msgid="1749750436092293116">"Lehet, hogy az alkalmazás nem működik osztott képernyős nézetben."</string> - <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Az alkalmazás nem támogatja az osztott képernyős nézetet."</string> + <!-- no translation found for dock_forced_resizable (7429086980048964687) --> + <skip /> + <!-- no translation found for dock_non_resizeble_failed_to_dock_text (2733543750291266047) --> + <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Ez az alkalmazás csak egy ablakban nyitható meg."</string> <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> + <!-- no translation found for accessibility_divider (6407584574218956849) --> + <skip /> + <!-- no translation found for divider_title (1963391955593749442) --> + <skip /> <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> @@ -46,6 +53,10 @@ <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Felső 50%-ra"</string> <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Felső 30%-ra"</string> <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Alsó teljes képernyőre"</string> + <string name="accessibility_split_left" msgid="1713683765575562458">"Osztás a képernyő bal oldalán"</string> + <string name="accessibility_split_right" msgid="8441001008181296837">"Osztás a képernyő jobb oldalán"</string> + <string name="accessibility_split_top" msgid="2789329702027147146">"Osztás a képernyő tetején"</string> + <string name="accessibility_split_bottom" msgid="8694551025220868191">"Osztás alul"</string> <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Egykezes mód használata"</string> <string name="one_handed_tutorial_description" msgid="3486582858591353067">"A kilépéshez csúsztasson felfelé a képernyő aljáról, vagy koppintson az alkalmazás felett a képernyő bármelyik részére"</string> <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Egykezes mód indítása"</string> @@ -61,6 +72,7 @@ <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Áthelyezés le és jobbra"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> beállításai"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Buborék elvetése"</string> + <string name="bubbles_dont_bubble" msgid="3216183855437329223">"Ne jelenjen meg buborékban"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Ne jelenjen meg a beszélgetés buborékban"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Buborékokat használó csevegés"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"Az új beszélgetések lebegő ikonként, vagyis buborékként jelennek meg. A buborék megnyitásához koppintson rá. Áthelyezéshez húzza a kívánt helyre."</string> @@ -72,17 +84,36 @@ <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) --> - <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) --> + <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Több mindent láthat és tehet"</string> + <!-- no translation found for letterbox_education_split_screen_text (449233070804658627) --> <skip /> + <string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Koppintson duplán az alkalmazáson kívül az áthelyezéséhez"</string> <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="letterbox_restart_dialog_title" msgid="8543049527871033505">"Újraindítja a jobb megjelenítés érdekében?"</string> + <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"Újraindíthatja az alkalmazást a képernyőn való jobb megjelenítés érdekében, de elveszítheti az előrehaladását és az esetleges nem mentett változásokat"</string> + <string name="letterbox_restart_cancel" msgid="1342209132692537805">"Mégse"</string> + <string name="letterbox_restart_restart" msgid="8529976234412442973">"Újraindítás"</string> + <string name="letterbox_restart_dialog_checkbox_title" msgid="5252918008140768386">"Ne jelenjen meg többé"</string> + <!-- no translation found for letterbox_reachability_reposition_text (4507890186297500893) --> + <skip /> + <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> + <string name="back_button_text" msgid="1469718707134137085">"Vissza"</string> + <string name="handle_text" msgid="1766582106752184456">"Fogópont"</string> + <string name="app_icon_text" msgid="2823268023931811747">"Alkalmazásikon"</string> + <string name="fullscreen_text" msgid="1162316685217676079">"Teljes képernyő"</string> + <string name="desktop_text" msgid="1077633567027630454">"Asztali üzemmód"</string> + <string name="split_screen_text" msgid="1396336058129570886">"Osztott képernyő"</string> + <string name="more_button_text" msgid="3655388105592893530">"Továbbiak"</string> + <string name="float_button_text" msgid="9221657008391364581">"Lebegő"</string> + <string name="select_text" msgid="5139083974039906583">"Kiválasztás"</string> + <string name="screenshot_text" msgid="1477704010087786671">"Képernyőkép"</string> + <string name="close_text" msgid="4986518933445178928">"Bezárás"</string> + <string name="collapse_menu_text" msgid="7515008122450342029">"Menü bezárása"</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..90cbfe643c82 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="7930546669915337998">"Vezérlők: A "<annotation icon="home_icon">"KEZDŐKÉPERNYŐ"</annotation>" gomb kétszeri megnyomása"</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..d01ff713c7c3 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> @@ -31,11 +32,17 @@ <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Փոխել չափը"</string> <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Թաքցնել"</string> <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Ցուցադրել"</string> - <string name="dock_forced_resizable" msgid="1749750436092293116">"Հավելվածը չի կարող աշխատել տրոհված էկրանի ռեժիմում։"</string> - <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Հավելվածը չի աջակցում էկրանի տրոհումը:"</string> + <!-- no translation found for dock_forced_resizable (7429086980048964687) --> + <skip /> + <!-- no translation found for dock_non_resizeble_failed_to_dock_text (2733543750291266047) --> + <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Այս հավելվածը հնարավոր է բացել միայն մեկ պատուհանում։"</string> <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> + <!-- no translation found for accessibility_divider (6407584574218956849) --> + <skip /> + <!-- no translation found for divider_title (1963391955593749442) --> + <skip /> <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> @@ -46,6 +53,10 @@ <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Վերևի էկրանը՝ 50%"</string> <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Վերևի էկրանը՝ 30%"</string> <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Ներքևի էկրանը՝ լիաէկրան"</string> + <string name="accessibility_split_left" msgid="1713683765575562458">"Հավելվածը ձախ կողմում"</string> + <string name="accessibility_split_right" msgid="8441001008181296837">"Հավելվածը աջ կողմում"</string> + <string name="accessibility_split_top" msgid="2789329702027147146">"Հավելվածը վերևում"</string> + <string name="accessibility_split_bottom" msgid="8694551025220868191">"Հավելվածը ներքևում"</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> @@ -61,6 +72,7 @@ <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Տեղափոխել ներքև՝ աջ"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> – կարգավորումներ"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Փակել ամպիկը"</string> + <string name="bubbles_dont_bubble" msgid="3216183855437329223">"Ցույց չտալ ամպիկները"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Զրույցը չցուցադրել ամպիկի տեսքով"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Զրույցի ամպիկներ"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"Նոր զրույցները կհայտնվեն լողացող պատկերակների կամ ամպիկների տեսքով։ Հպեք՝ ամպիկը բացելու համար։ Քաշեք՝ այն տեղափոխելու համար։"</string> @@ -72,17 +84,36 @@ <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) --> - <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) --> + <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Միաժամանակ կատարեք մի քանի առաջադրանք"</string> + <!-- no translation found for letterbox_education_split_screen_text (449233070804658627) --> <skip /> + <string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Կրկնակի հպեք հավելվածի կողքին՝ այն տեղափոխելու համար"</string> <string name="letterbox_education_got_it" msgid="4057634570866051177">"Եղավ"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Ծավալեք՝ ավելին իմանալու համար։"</string> + <string name="letterbox_restart_dialog_title" msgid="8543049527871033505">"Վերագործարկե՞լ հավելվածը"</string> + <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"Դուք կարող եք վերագործարկել հավելվածը, որպեսզի այն ավելի լավ ցուցադրվի ձեր էկրանին, սակայն ձեր առաջընթացը և չպահված փոփոխությունները կկորեն"</string> + <string name="letterbox_restart_cancel" msgid="1342209132692537805">"Չեղարկել"</string> + <string name="letterbox_restart_restart" msgid="8529976234412442973">"Վերագործարկել"</string> + <string name="letterbox_restart_dialog_checkbox_title" msgid="5252918008140768386">"Այլևս ցույց չտալ"</string> + <!-- no translation found for letterbox_reachability_reposition_text (4507890186297500893) --> + <skip /> + <string name="maximize_button_text" msgid="1650859196290301963">"Ծավալել"</string> + <string name="minimize_button_text" msgid="271592547935841753">"Ծալել"</string> + <string name="close_button_text" msgid="2913281996024033299">"Փակել"</string> + <string name="back_button_text" msgid="1469718707134137085">"Հետ"</string> + <string name="handle_text" msgid="1766582106752184456">"Նշիչ"</string> + <string name="app_icon_text" msgid="2823268023931811747">"Հավելվածի պատկերակ"</string> + <string name="fullscreen_text" msgid="1162316685217676079">"Լիաէկրան"</string> + <string name="desktop_text" msgid="1077633567027630454">"Համակարգչի ռեժիմ"</string> + <string name="split_screen_text" msgid="1396336058129570886">"Տրոհված էկրան"</string> + <string name="more_button_text" msgid="3655388105592893530">"Ավելին"</string> + <string name="float_button_text" msgid="9221657008391364581">"Լողացող պատուհան"</string> + <string name="select_text" msgid="5139083974039906583">"Ընտրել"</string> + <string name="screenshot_text" msgid="1477704010087786671">"Սքրինշոթ"</string> + <string name="close_text" msgid="4986518933445178928">"Փակել"</string> + <string name="collapse_menu_text" msgid="7515008122450342029">"Փակել ընտրացանկը"</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..30b5911147b5 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="7930546669915337998">"Կարգավորումների համար կրկնակի սեղմեք "<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..123e5b9ef7de 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> @@ -31,11 +32,17 @@ <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Ubah ukuran"</string> <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Stash"</string> <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Batalkan stash"</string> - <string name="dock_forced_resizable" msgid="1749750436092293116">"Aplikasi mungkin tidak berfungsi dengan layar terpisah."</string> - <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"App tidak mendukung layar terpisah."</string> + <!-- no translation found for dock_forced_resizable (7429086980048964687) --> + <skip /> + <!-- no translation found for dock_non_resizeble_failed_to_dock_text (2733543750291266047) --> + <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Aplikasi ini hanya dapat dibuka di 1 jendela."</string> <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> + <!-- no translation found for accessibility_divider (6407584574218956849) --> + <skip /> + <!-- no translation found for divider_title (1963391955593749442) --> + <skip /> <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> @@ -46,6 +53,10 @@ <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Atas 50%"</string> <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Atas 30%"</string> <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Layar penuh di bawah"</string> + <string name="accessibility_split_left" msgid="1713683765575562458">"Pisahkan ke kiri"</string> + <string name="accessibility_split_right" msgid="8441001008181296837">"Pisahkan ke kanan"</string> + <string name="accessibility_split_top" msgid="2789329702027147146">"Pisahkan ke atas"</string> + <string name="accessibility_split_bottom" msgid="8694551025220868191">"Pisahkan ke bawah"</string> <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Menggunakan mode satu tangan"</string> <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Untuk keluar, geser layar dari bawah ke atas atau ketuk di mana saja di atas aplikasi"</string> <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Mulai mode satu tangan"</string> @@ -61,6 +72,7 @@ <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Pindahkan ke kanan bawah"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"Setelan <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Tutup balon"</string> + <string name="bubbles_dont_bubble" msgid="3216183855437329223">"Berhenti menampilkan balon"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Jangan gunakan percakapan balon"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Chat dalam tampilan balon"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"Percakapan baru muncul sebagai ikon mengambang, atau balon. Ketuk untuk membuka balon. Tarik untuk memindahkannya."</string> @@ -72,17 +84,36 @@ <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) --> - <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) --> + <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Lihat dan lakukan lebih banyak hal"</string> + <!-- no translation found for letterbox_education_split_screen_text (449233070804658627) --> <skip /> + <string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Ketuk dua kali di luar aplikasi untuk mengubah posisinya"</string> <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="letterbox_restart_dialog_title" msgid="8543049527871033505">"Mulai ulang untuk tampilan yang lebih baik?"</string> + <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"Anda dapat memulai ulang aplikasi agar terlihat lebih baik di layar, tetapi Anda mungkin kehilangan progres atau perubahan yang belum disimpan"</string> + <string name="letterbox_restart_cancel" msgid="1342209132692537805">"Batal"</string> + <string name="letterbox_restart_restart" msgid="8529976234412442973">"Mulai ulang"</string> + <string name="letterbox_restart_dialog_checkbox_title" msgid="5252918008140768386">"Jangan tampilkan lagi"</string> + <!-- no translation found for letterbox_reachability_reposition_text (4507890186297500893) --> + <skip /> + <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> + <string name="back_button_text" msgid="1469718707134137085">"Kembali"</string> + <string name="handle_text" msgid="1766582106752184456">"Tuas"</string> + <string name="app_icon_text" msgid="2823268023931811747">"Ikon Aplikasi"</string> + <string name="fullscreen_text" msgid="1162316685217676079">"Layar Penuh"</string> + <string name="desktop_text" msgid="1077633567027630454">"Mode Desktop"</string> + <string name="split_screen_text" msgid="1396336058129570886">"Layar Terpisah"</string> + <string name="more_button_text" msgid="3655388105592893530">"Lainnya"</string> + <string name="float_button_text" msgid="9221657008391364581">"Mengambang"</string> + <string name="select_text" msgid="5139083974039906583">"Pilih"</string> + <string name="screenshot_text" msgid="1477704010087786671">"Screenshot"</string> + <string name="close_text" msgid="4986518933445178928">"Tutup"</string> + <string name="collapse_menu_text" msgid="7515008122450342029">"Tutup Menu"</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..0fda69f6c0e4 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="7930546669915337998">"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..bd80936781d0 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> @@ -31,11 +32,17 @@ <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Breyta stærð"</string> <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Geymsla"</string> <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Taka úr geymslu"</string> - <string name="dock_forced_resizable" msgid="1749750436092293116">"Hugsanlega virkar forritið ekki með skjáskiptingu."</string> - <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Forritið styður ekki að skjánum sé skipt."</string> + <!-- no translation found for dock_forced_resizable (7429086980048964687) --> + <skip /> + <!-- no translation found for dock_non_resizeble_failed_to_dock_text (2733543750291266047) --> + <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Aðeins er hægt að opna þetta forrit í 1 glugga."</string> <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> + <!-- no translation found for accessibility_divider (6407584574218956849) --> + <skip /> + <!-- no translation found for divider_title (1963391955593749442) --> + <skip /> <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> @@ -46,6 +53,10 @@ <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Efri 50%"</string> <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Efri 30%"</string> <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Neðri á öllum skjánum"</string> + <string name="accessibility_split_left" msgid="1713683765575562458">"Skipta vinstra megin"</string> + <string name="accessibility_split_right" msgid="8441001008181296837">"Skipta hægra megin"</string> + <string name="accessibility_split_top" msgid="2789329702027147146">"Skipta efst"</string> + <string name="accessibility_split_bottom" msgid="8694551025220868191">"Skipta neðst"</string> <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Notkun einhentrar stillingar"</string> <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Til að loka skaltu strjúka upp frá neðri hluta skjásins eða ýta hvar sem er fyrir ofan forritið"</string> <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Ræsa einhenta stillingu"</string> @@ -61,6 +72,7 @@ <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Færðu neðst til hægri"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"Stillingar <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Loka blöðru"</string> + <string name="bubbles_dont_bubble" msgid="3216183855437329223">"Ekki sýna blöðrur"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Ekki setja samtal í blöðru"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Spjalla með blöðrum"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"Ný samtöl birtast sem fljótandi tákn eða blöðrur. Ýttu til að opna blöðru. Dragðu hana til að færa."</string> @@ -72,17 +84,36 @@ <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) --> - <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) --> + <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Sjáðu og gerðu meira"</string> + <!-- no translation found for letterbox_education_split_screen_text (449233070804658627) --> <skip /> + <string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Ýttu tvisvar utan við forrit til að færa það"</string> <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="letterbox_restart_dialog_title" msgid="8543049527871033505">"Viltu endurræsa til að fá betri sýn?"</string> + <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"Þú getur endurræst forritið svo það falli betur að skjánum en þú gætir tapað framvindunni eða óvistuðum breytingum"</string> + <string name="letterbox_restart_cancel" msgid="1342209132692537805">"Hætta við"</string> + <string name="letterbox_restart_restart" msgid="8529976234412442973">"Endurræsa"</string> + <string name="letterbox_restart_dialog_checkbox_title" msgid="5252918008140768386">"Ekki sýna þetta aftur"</string> + <!-- no translation found for letterbox_reachability_reposition_text (4507890186297500893) --> + <skip /> + <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> + <string name="back_button_text" msgid="1469718707134137085">"Til baka"</string> + <string name="handle_text" msgid="1766582106752184456">"Handfang"</string> + <string name="app_icon_text" msgid="2823268023931811747">"Tákn forrits"</string> + <string name="fullscreen_text" msgid="1162316685217676079">"Allur skjárinn"</string> + <string name="desktop_text" msgid="1077633567027630454">"Skjáborðsstilling"</string> + <string name="split_screen_text" msgid="1396336058129570886">"Skjáskipting"</string> + <string name="more_button_text" msgid="3655388105592893530">"Meira"</string> + <string name="float_button_text" msgid="9221657008391364581">"Reikult"</string> + <string name="select_text" msgid="5139083974039906583">"Velja"</string> + <string name="screenshot_text" msgid="1477704010087786671">"Skjámynd"</string> + <string name="close_text" msgid="4986518933445178928">"Loka"</string> + <string name="collapse_menu_text" msgid="7515008122450342029">"Loka valmynd"</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..e0d604f30d1a 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="7930546669915337998">"Ý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..90e6a6feedf0 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> @@ -31,11 +32,17 @@ <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Ridimensiona"</string> <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Accantona"</string> <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Annulla accantonamento"</string> - <string name="dock_forced_resizable" msgid="1749750436092293116">"L\'app potrebbe non funzionare con lo schermo diviso."</string> - <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"L\'app non supporta la modalità Schermo diviso."</string> + <!-- no translation found for dock_forced_resizable (7429086980048964687) --> + <skip /> + <!-- no translation found for dock_non_resizeble_failed_to_dock_text (2733543750291266047) --> + <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Questa app può essere aperta soltanto in 1 finestra."</string> <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> + <!-- no translation found for accessibility_divider (6407584574218956849) --> + <skip /> + <!-- no translation found for divider_title (1963391955593749442) --> + <skip /> <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 +53,14 @@ <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="accessibility_split_left" msgid="1713683765575562458">"Dividi a sinistra"</string> + <string name="accessibility_split_right" msgid="8441001008181296837">"Dividi a destra"</string> + <string name="accessibility_split_top" msgid="2789329702027147146">"Dividi in alto"</string> + <string name="accessibility_split_bottom" msgid="8694551025220868191">"Dividi in basso"</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> @@ -61,6 +72,7 @@ <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Sposta in basso a destra"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"Impostazioni <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Ignora bolla"</string> + <string name="bubbles_dont_bubble" msgid="3216183855437329223">"Non mostrare i fumetti"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Non mettere la conversazione nella bolla"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Chatta utilizzando le bolle"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"Le nuove conversazioni vengono mostrate come icone mobili o bolle. Tocca per aprire la bolla. Trascinala per spostarla."</string> @@ -72,17 +84,35 @@ <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) --> - <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) --> + <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Visualizza più contenuti e fai di più"</string> + <!-- no translation found for letterbox_education_split_screen_text (449233070804658627) --> <skip /> + <string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Tocca due volte fuori da un\'app per riposizionarla"</string> <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="letterbox_restart_dialog_title" msgid="8543049527871033505">"Vuoi riavviare per migliorare la visualizzazione?"</string> + <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"Puoi riavviare l\'app affinché venga visualizzata meglio sullo schermo, ma potresti perdere i tuoi progressi o eventuali modifiche non salvate"</string> + <string name="letterbox_restart_cancel" msgid="1342209132692537805">"Annulla"</string> + <string name="letterbox_restart_restart" msgid="8529976234412442973">"Riavvia"</string> + <string name="letterbox_restart_dialog_checkbox_title" msgid="5252918008140768386">"Non mostrare più"</string> + <string name="letterbox_reachability_reposition_text" msgid="4507890186297500893">"Tocca due volte per spostare questa app"</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> + <string name="back_button_text" msgid="1469718707134137085">"Indietro"</string> + <string name="handle_text" msgid="1766582106752184456">"Handle"</string> + <string name="app_icon_text" msgid="2823268023931811747">"Icona dell\'app"</string> + <string name="fullscreen_text" msgid="1162316685217676079">"Schermo intero"</string> + <string name="desktop_text" msgid="1077633567027630454">"Modalità desktop"</string> + <string name="split_screen_text" msgid="1396336058129570886">"Schermo diviso"</string> + <string name="more_button_text" msgid="3655388105592893530">"Altro"</string> + <string name="float_button_text" msgid="9221657008391364581">"Mobile"</string> + <string name="select_text" msgid="5139083974039906583">"Seleziona"</string> + <string name="screenshot_text" msgid="1477704010087786671">"Screenshot"</string> + <string name="close_text" msgid="4986518933445178928">"Chiudi"</string> + <string name="collapse_menu_text" msgid="7515008122450342029">"Chiudi il menu"</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..267f67463917 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="7930546669915337998">"Premi due volte "<annotation icon="home_icon">"HOME"</annotation>" per accedere ai 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..8d5c4a4cad9f 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> @@ -31,11 +32,17 @@ <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"שינוי גודל"</string> <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"הסתרה זמנית"</string> <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"ביטול ההסתרה הזמנית"</string> - <string name="dock_forced_resizable" msgid="1749750436092293116">"ייתכן שהאפליקציה לא תפעל במסך מפוצל."</string> - <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"האפליקציה אינה תומכת במסך מפוצל."</string> + <!-- no translation found for dock_forced_resizable (7429086980048964687) --> + <skip /> + <!-- no translation found for dock_non_resizeble_failed_to_dock_text (2733543750291266047) --> + <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"ניתן לפתוח את האפליקציה הזו רק בחלון אחד."</string> <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> + <!-- no translation found for accessibility_divider (6407584574218956849) --> + <skip /> + <!-- no translation found for divider_title (1963391955593749442) --> + <skip /> <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> @@ -46,6 +53,10 @@ <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"עליון 50%"</string> <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"למעלה 30%"</string> <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"מסך תחתון מלא"</string> + <string name="accessibility_split_left" msgid="1713683765575562458">"פיצול שמאלה"</string> + <string name="accessibility_split_right" msgid="8441001008181296837">"פיצול ימינה"</string> + <string name="accessibility_split_top" msgid="2789329702027147146">"פיצול למעלה"</string> + <string name="accessibility_split_bottom" msgid="8694551025220868191">"פיצול למטה"</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> @@ -61,6 +72,7 @@ <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"העברה לפינה הימנית התחתונה"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"הגדרות <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"סגירת בועה"</string> + <string name="bubbles_dont_bubble" msgid="3216183855437329223">"ללא בועות"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"אין להציג בועות לשיחה"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"לדבר בבועות"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"שיחות חדשות מופיעות כסמלים צפים, או בועות. יש להקיש כדי לפתוח בועה. יש לגרור כדי להזיז אותה."</string> @@ -72,17 +84,35 @@ <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) --> - <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) --> + <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"רוצה לראות ולעשות יותר?"</string> + <!-- no translation found for letterbox_education_split_screen_text (449233070804658627) --> <skip /> + <string name="letterbox_education_reposition_text" msgid="4589957299813220661">"צריך להקיש הקשה כפולה מחוץ לאפליקציה כדי למקם אותה מחדש"</string> <string name="letterbox_education_got_it" msgid="4057634570866051177">"הבנתי"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"מרחיבים כדי לקבל מידע נוסף."</string> + <string name="letterbox_restart_dialog_title" msgid="8543049527871033505">"להפעיל מחדש לתצוגה טובה יותר?"</string> + <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"אפשר להפעיל מחדש את האפליקציה כדי שהיא תוצג באופן טוב יותר במסך, אבל ייתכן שההתקדמות שלך או כל שינוי שלא נשמר יאבדו"</string> + <string name="letterbox_restart_cancel" msgid="1342209132692537805">"ביטול"</string> + <string name="letterbox_restart_restart" msgid="8529976234412442973">"הפעלה מחדש"</string> + <string name="letterbox_restart_dialog_checkbox_title" msgid="5252918008140768386">"אין צורך להציג את זה שוב"</string> + <string name="letterbox_reachability_reposition_text" msgid="4507890186297500893">"אפשר להקיש הקשה כפולה כדי להזיז את האפליקציה הזו"</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> + <string name="back_button_text" msgid="1469718707134137085">"חזרה"</string> + <string name="handle_text" msgid="1766582106752184456">"נקודת אחיזה"</string> + <string name="app_icon_text" msgid="2823268023931811747">"סמל האפליקציה"</string> + <string name="fullscreen_text" msgid="1162316685217676079">"מסך מלא"</string> + <string name="desktop_text" msgid="1077633567027630454">"ממשק המחשב"</string> + <string name="split_screen_text" msgid="1396336058129570886">"מסך מפוצל"</string> + <string name="more_button_text" msgid="3655388105592893530">"עוד"</string> + <string name="float_button_text" msgid="9221657008391364581">"בלונים"</string> + <string name="select_text" msgid="5139083974039906583">"בחירה"</string> + <string name="screenshot_text" msgid="1477704010087786671">"צילום מסך"</string> + <string name="close_text" msgid="4986518933445178928">"סגירה"</string> + <string name="collapse_menu_text" msgid="7515008122450342029">"סגירת התפריט"</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..6b30f5642ad3 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="7930546669915337998">"לחיצה כפולה על "<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..6b1f6991afc5 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> @@ -31,11 +32,17 @@ <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"サイズ変更"</string> <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"非表示"</string> <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"表示"</string> - <string name="dock_forced_resizable" msgid="1749750436092293116">"アプリは分割画面では動作しないことがあります。"</string> - <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"アプリで分割画面がサポートされていません。"</string> + <!-- no translation found for dock_forced_resizable (7429086980048964687) --> + <skip /> + <!-- no translation found for dock_non_resizeble_failed_to_dock_text (2733543750291266047) --> + <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"このアプリはウィンドウが 1 つの場合のみ開くことができます。"</string> <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> + <!-- no translation found for accessibility_divider (6407584574218956849) --> + <skip /> + <!-- no translation found for divider_title (1963391955593749442) --> + <skip /> <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> @@ -46,6 +53,10 @@ <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"上 50%"</string> <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"上 30%"</string> <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"下部全画面"</string> + <string name="accessibility_split_left" msgid="1713683765575562458">"左に分割"</string> + <string name="accessibility_split_right" msgid="8441001008181296837">"右に分割"</string> + <string name="accessibility_split_top" msgid="2789329702027147146">"上に分割"</string> + <string name="accessibility_split_bottom" msgid="8694551025220868191">"下に分割"</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> @@ -61,6 +72,7 @@ <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"右下に移動"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> の設定"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"バブルを閉じる"</string> + <string name="bubbles_dont_bubble" msgid="3216183855437329223">"バブルで表示しない"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"会話をバブルで表示しない"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"チャットでバブルを使う"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"新しい会話はフローティング アイコン(バブル)として表示されます。タップするとバブルが開きます。ドラッグしてバブルを移動できます。"</string> @@ -72,17 +84,35 @@ <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) --> - <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) --> + <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"表示を拡大して機能を強化"</string> + <!-- no translation found for letterbox_education_split_screen_text (449233070804658627) --> <skip /> + <string name="letterbox_education_reposition_text" msgid="4589957299813220661">"位置を変えるにはアプリの外側をダブルタップしてください"</string> <string name="letterbox_education_got_it" msgid="4057634570866051177">"OK"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"開くと詳細が表示されます。"</string> + <string name="letterbox_restart_dialog_title" msgid="8543049527871033505">"再起動して画面をすっきりさせますか?"</string> + <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"アプリを再起動して画面をすっきりさせることはできますが、進捗状況が失われ、保存されていない変更が消える可能性があります"</string> + <string name="letterbox_restart_cancel" msgid="1342209132692537805">"キャンセル"</string> + <string name="letterbox_restart_restart" msgid="8529976234412442973">"再起動"</string> + <string name="letterbox_restart_dialog_checkbox_title" msgid="5252918008140768386">"次回から表示しない"</string> + <string name="letterbox_reachability_reposition_text" msgid="4507890186297500893">"ダブルタップすると、このアプリを移動できます"</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> + <string name="back_button_text" msgid="1469718707134137085">"戻る"</string> + <string name="handle_text" msgid="1766582106752184456">"ハンドル"</string> + <string name="app_icon_text" msgid="2823268023931811747">"アプリのアイコン"</string> + <string name="fullscreen_text" msgid="1162316685217676079">"全画面表示"</string> + <string name="desktop_text" msgid="1077633567027630454">"デスクトップ モード"</string> + <string name="split_screen_text" msgid="1396336058129570886">"分割画面"</string> + <string name="more_button_text" msgid="3655388105592893530">"その他"</string> + <string name="float_button_text" msgid="9221657008391364581">"フローティング"</string> + <string name="select_text" msgid="5139083974039906583">"選択"</string> + <string name="screenshot_text" msgid="1477704010087786671">"スクリーンショット"</string> + <string name="close_text" msgid="4986518933445178928">"閉じる"</string> + <string name="collapse_menu_text" msgid="7515008122450342029">"メニューを閉じる"</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..2a79e3c651a1 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="7930546669915337998">"コントロールにアクセス: "<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..05430e10f45f 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> @@ -31,11 +32,17 @@ <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"ზომის შეცვლა"</string> <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"გადანახვა"</string> <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"გადანახვის გაუქმება"</string> - <string name="dock_forced_resizable" msgid="1749750436092293116">"აპმა შეიძლება არ იმუშაოს გაყოფილი ეკრანის რეჟიმში."</string> - <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"ეკრანის გაყოფა არ არის მხარდაჭერილი აპის მიერ."</string> + <!-- no translation found for dock_forced_resizable (7429086980048964687) --> + <skip /> + <!-- no translation found for dock_non_resizeble_failed_to_dock_text (2733543750291266047) --> + <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"ამ აპის გახსნა შესაძლებელია მხოლოდ 1 ფანჯარაში."</string> <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> + <!-- no translation found for accessibility_divider (6407584574218956849) --> + <skip /> + <!-- no translation found for divider_title (1963391955593749442) --> + <skip /> <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> @@ -46,6 +53,10 @@ <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"ზედა ეკრანი — 50%"</string> <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"ზედა ეკრანი — 30%"</string> <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"ქვედა ნაწილის სრულ ეკრანზე გაშლა"</string> + <string name="accessibility_split_left" msgid="1713683765575562458">"გაყოფა მარცხნივ"</string> + <string name="accessibility_split_right" msgid="8441001008181296837">"გაყოფა მარჯვნივ"</string> + <string name="accessibility_split_top" msgid="2789329702027147146">"გაყოფა ზემოთ"</string> + <string name="accessibility_split_bottom" msgid="8694551025220868191">"გაყოფა ქვემოთ"</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> @@ -61,6 +72,7 @@ <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"გადაანაცვ. ქვემოთ და მარჯვნივ"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>-ის პარამეტრები"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"ბუშტის დახურვა"</string> + <string name="bubbles_dont_bubble" msgid="3216183855437329223">"ბუშტის გამორთვა"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"აიკრძალოს საუბრის ბუშტები"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"ჩეთი ბუშტების გამოყენებით"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"ახალი საუბრები გამოჩნდება როგორც მოტივტივე ხატულები ან ბუშტები. შეეხეთ ბუშტის გასახსნელად. გადაიტანეთ ჩავლებით."</string> @@ -72,17 +84,35 @@ <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) --> - <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) --> + <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"მეტის ნახვა და გაკეთება"</string> + <!-- no translation found for letterbox_education_split_screen_text (449233070804658627) --> <skip /> + <string name="letterbox_education_reposition_text" msgid="4589957299813220661">"ორმაგად შეეხეთ აპის გარშემო სივრცეს, რათა ის სხვაგან გადაიტანოთ"</string> <string name="letterbox_education_got_it" msgid="4057634570866051177">"გასაგებია"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"დამატებითი ინფორმაციისთვის გააფართოეთ."</string> + <string name="letterbox_restart_dialog_title" msgid="8543049527871033505">"გსურთ გადატვირთვა უკეთესი ხედისთვის?"</string> + <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"შეგიძლიათ გადატვირთოთ აპი იმისათვის, რომ თქვენს ეკრანზე უკეთესად გამოჩნდეს, თუმცა თქვენ მიერ შესრულებული მოქმედებები შეიძლება დაიკარგოს ან ცვლილებების შენახვა ვერ მოხერხდეს"</string> + <string name="letterbox_restart_cancel" msgid="1342209132692537805">"გაუქმება"</string> + <string name="letterbox_restart_restart" msgid="8529976234412442973">"გადატვირთვა"</string> + <string name="letterbox_restart_dialog_checkbox_title" msgid="5252918008140768386">"აღარ გამოჩნდეს"</string> + <string name="letterbox_reachability_reposition_text" msgid="4507890186297500893">"ამ აპის გადასატანად ორმაგად შეეხეთ მას"</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> + <string name="back_button_text" msgid="1469718707134137085">"უკან"</string> + <string name="handle_text" msgid="1766582106752184456">"იდენტიფიკატორი"</string> + <string name="app_icon_text" msgid="2823268023931811747">"აპის ხატულა"</string> + <string name="fullscreen_text" msgid="1162316685217676079">"სრულ ეკრანზე"</string> + <string name="desktop_text" msgid="1077633567027630454">"დესკტოპის რეჟიმი"</string> + <string name="split_screen_text" msgid="1396336058129570886">"ეკრანის გაყოფა"</string> + <string name="more_button_text" msgid="3655388105592893530">"სხვა"</string> + <string name="float_button_text" msgid="9221657008391364581">"ფარფატი"</string> + <string name="select_text" msgid="5139083974039906583">"არჩევა"</string> + <string name="screenshot_text" msgid="1477704010087786671">"ეკრანის ანაბეჭდი"</string> + <string name="close_text" msgid="4986518933445178928">"დახურვა"</string> + <string name="collapse_menu_text" msgid="7515008122450342029">"მენიუს დახურვა"</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..58bae02120ff 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="7930546669915337998">"სახლის მართვის საშუალებებზე წვდომისთვის ორმაგად დააჭირეთ "<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..7f006abbd0f5 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> @@ -31,11 +32,17 @@ <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Өлшемін өзгерту"</string> <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Жасыру"</string> <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Көрсету"</string> - <string name="dock_forced_resizable" msgid="1749750436092293116">"Қолданба экранды бөлу режимінде жұмыс істемеуі мүмкін."</string> - <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Қодланба бөлінген экранды қолдамайды."</string> + <!-- no translation found for dock_forced_resizable (7429086980048964687) --> + <skip /> + <!-- no translation found for dock_non_resizeble_failed_to_dock_text (2733543750291266047) --> + <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Бұл қолданбаны тек 1 терезеден ашуға болады."</string> <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> + <!-- no translation found for accessibility_divider (6407584574218956849) --> + <skip /> + <!-- no translation found for divider_title (1963391955593749442) --> + <skip /> <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> @@ -46,6 +53,10 @@ <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"50% жоғарғы жақта"</string> <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"30% жоғарғы жақта"</string> <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Төменгісін толық экранға шығару"</string> + <string name="accessibility_split_left" msgid="1713683765575562458">"Сол жақтан шығару"</string> + <string name="accessibility_split_right" msgid="8441001008181296837">"Оң жақтан шығару"</string> + <string name="accessibility_split_top" msgid="2789329702027147146">"Жоғарыдан шығару"</string> + <string name="accessibility_split_bottom" msgid="8694551025220868191">"Астынан шығару"</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> @@ -61,6 +72,7 @@ <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Төменгі оң жаққа жылжыту"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> параметрлері"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Қалқымалы хабарды жабу"</string> + <string name="bubbles_dont_bubble" msgid="3216183855437329223">"Қалқыма хабарлар көрсетпеу"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Әңгіменің қалқыма хабары көрсетілмесін"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Қалқыма хабарлар арқылы сөйлесу"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"Жаңа әңгімелер қалқыма белгішелер немесе хабарлар түрінде көрсетіледі. Қалқыма хабарды ашу үшін түртіңіз. Жылжыту үшін сүйреңіз."</string> @@ -72,17 +84,36 @@ <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) --> - <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) --> + <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Қосымша ақпаратты қарап, әрекеттер жасау"</string> + <!-- no translation found for letterbox_education_split_screen_text (449233070804658627) --> <skip /> + <string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Қолданбаның орнын өзгерту үшін одан тыс жерді екі рет түртіңіз."</string> <string name="letterbox_education_got_it" msgid="4057634570866051177">"Түсінікті"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Толығырақ ақпарат алу үшін терезені жайыңыз."</string> + <string name="letterbox_restart_dialog_title" msgid="8543049527871033505">"Көріністі жақсарту үшін өшіріп қосу керек пе?"</string> + <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"Экранда жақсырақ көрінуі үшін қолданбаны өшіріп қосуыңызға болады, бірақ мұндайда ағымдағы прогресс өшіп қалуы немесе сақталмаған өзгерістерді жоғалтуыңыз мүмкін."</string> + <string name="letterbox_restart_cancel" msgid="1342209132692537805">"Бас тарту"</string> + <string name="letterbox_restart_restart" msgid="8529976234412442973">"Өшіріп қосу"</string> + <string name="letterbox_restart_dialog_checkbox_title" msgid="5252918008140768386">"Қайта көрсетілмесін"</string> + <!-- no translation found for letterbox_reachability_reposition_text (4507890186297500893) --> + <skip /> + <string name="maximize_button_text" msgid="1650859196290301963">"Жаю"</string> + <string name="minimize_button_text" msgid="271592547935841753">"Кішірейту"</string> + <string name="close_button_text" msgid="2913281996024033299">"Жабу"</string> + <string name="back_button_text" msgid="1469718707134137085">"Артқа"</string> + <string name="handle_text" msgid="1766582106752184456">"Идентификатор"</string> + <string name="app_icon_text" msgid="2823268023931811747">"Қолданба белгішесі"</string> + <string name="fullscreen_text" msgid="1162316685217676079">"Толық экран"</string> + <string name="desktop_text" msgid="1077633567027630454">"Компьютер режимі"</string> + <string name="split_screen_text" msgid="1396336058129570886">"Экранды бөлу"</string> + <string name="more_button_text" msgid="3655388105592893530">"Қосымша"</string> + <string name="float_button_text" msgid="9221657008391364581">"Қалқыма"</string> + <string name="select_text" msgid="5139083974039906583">"Таңдау"</string> + <string name="screenshot_text" msgid="1477704010087786671">"Скриншот"</string> + <string name="close_text" msgid="4986518933445178928">"Жабу"</string> + <string name="collapse_menu_text" msgid="7515008122450342029">"Мәзірді жабу"</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..df5f6171b11b 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="7930546669915337998">"Басқару: "<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..c1a3abd150e0 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> @@ -31,11 +32,17 @@ <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"ប្ដូរទំហំ"</string> <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"លាក់ជាបណ្ដោះអាសន្ន"</string> <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"ឈប់លាក់ជាបណ្ដោះអាសន្ន"</string> - <string name="dock_forced_resizable" msgid="1749750436092293116">"កម្មវិធីអាចនឹងមិនដំណើរការជាមួយមុខងារបំបែកអេក្រង់ទេ។"</string> - <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"កម្មវិធីមិនគាំទ្រអេក្រង់បំបែកជាពីរទេ"</string> + <!-- no translation found for dock_forced_resizable (7429086980048964687) --> + <skip /> + <!-- no translation found for dock_non_resizeble_failed_to_dock_text (2733543750291266047) --> + <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"កម្មវិធីនេះអាចបើកនៅក្នុងវិនដូតែ 1 ប៉ុណ្ណោះ។"</string> <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> + <!-- no translation found for accessibility_divider (6407584574218956849) --> + <skip /> + <!-- no translation found for divider_title (1963391955593749442) --> + <skip /> <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> @@ -46,6 +53,10 @@ <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"ខាងលើ 50%"</string> <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"ខាងលើ 30%"</string> <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"អេក្រង់ពេញខាងក្រោម"</string> + <string name="accessibility_split_left" msgid="1713683765575562458">"បំបែកខាងឆ្វេង"</string> + <string name="accessibility_split_right" msgid="8441001008181296837">"បំបែកខាងស្ដាំ"</string> + <string name="accessibility_split_top" msgid="2789329702027147146">"បំបែកខាងលើ"</string> + <string name="accessibility_split_bottom" msgid="8694551025220868191">"បំបែកខាងក្រោម"</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> @@ -61,6 +72,7 @@ <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"ផ្លាស់ទីទៅផ្នែកខាងក្រោមខាងស្ដាំ"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"ការកំណត់ <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"ច្រានចោលពពុះ"</string> + <string name="bubbles_dont_bubble" msgid="3216183855437329223">"កុំបង្ហាញពពុះ"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"កុំបង្ហាញការសន្ទនាជាពពុះ"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"ជជែកដោយប្រើពពុះ"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"ការសន្ទនាថ្មីៗបង្ហាញជាពពុះ ឬរូបអណ្ដែត។ ចុច ដើម្បីបើកពពុះ។ អូស ដើម្បីផ្លាស់ទីពពុះនេះ។"</string> @@ -72,17 +84,35 @@ <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) --> - <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) --> + <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"មើលឃើញ និងធ្វើបានកាន់តែច្រើន"</string> + <!-- no translation found for letterbox_education_split_screen_text (449233070804658627) --> <skip /> + <string name="letterbox_education_reposition_text" msgid="4589957299813220661">"ចុចពីរដងនៅក្រៅកម្មវិធី ដើម្បីប្ដូរទីតាំងកម្មវិធីនោះ"</string> <string name="letterbox_education_got_it" msgid="4057634570866051177">"យល់ហើយ"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"ពង្រីកដើម្បីទទួលបានព័ត៌មានបន្ថែម។"</string> + <string name="letterbox_restart_dialog_title" msgid="8543049527871033505">"ចាប់ផ្ដើមឡើងវិញ ដើម្បីទទួលបានទិដ្ឋភាពកាន់តែស្អាតឬ?"</string> + <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"អ្នកអាចចាប់ផ្ដើមកម្មវិធីឡើងវិញ ដើម្បីឱ្យកម្មវិធីនេះមើលទៅស្អាតជាងមុននៅលើអេក្រង់របស់អ្នក ប៉ុន្តែអ្នកអាចនឹងបាត់បង់ដំណើរការរបស់អ្នក ឬការកែប្រែណាមួយដែលអ្នកមិនបានរក្សាទុក"</string> + <string name="letterbox_restart_cancel" msgid="1342209132692537805">"បោះបង់"</string> + <string name="letterbox_restart_restart" msgid="8529976234412442973">"ចាប់ផ្ដើមឡើងវិញ"</string> + <string name="letterbox_restart_dialog_checkbox_title" msgid="5252918008140768386">"កុំបង្ហាញម្ដងទៀត"</string> + <string name="letterbox_reachability_reposition_text" msgid="4507890186297500893">"ចុចពីរដង ដើម្បីផ្លាស់ទីកម្មវិធីនេះ"</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> + <string name="back_button_text" msgid="1469718707134137085">"ថយក្រោយ"</string> + <string name="handle_text" msgid="1766582106752184456">"ឈ្មោះអ្នកប្រើប្រាស់"</string> + <string name="app_icon_text" msgid="2823268023931811747">"រូបកម្មវិធី"</string> + <string name="fullscreen_text" msgid="1162316685217676079">"អេក្រង់ពេញ"</string> + <string name="desktop_text" msgid="1077633567027630454">"មុខងារកុំព្យូទ័រ"</string> + <string name="split_screen_text" msgid="1396336058129570886">"មុខងារបំបែកអេក្រង់"</string> + <string name="more_button_text" msgid="3655388105592893530">"ច្រើនទៀត"</string> + <string name="float_button_text" msgid="9221657008391364581">"អណ្ដែត"</string> + <string name="select_text" msgid="5139083974039906583">"ជ្រើសរើស"</string> + <string name="screenshot_text" msgid="1477704010087786671">"រូបថតអេក្រង់"</string> + <string name="close_text" msgid="4986518933445178928">"បិទ"</string> + <string name="collapse_menu_text" msgid="7515008122450342029">"បិទម៉ឺនុយ"</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..a3c7e22f268a 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="7930546669915337998">"ចុចពីរដងលើ"<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..e04f00e55b17 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> @@ -31,11 +32,17 @@ <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"ಮರುಗಾತ್ರಗೊಳಿಸಿ"</string> <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"ಸ್ಟ್ಯಾಶ್ ಮಾಡಿ"</string> <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"ಅನ್ಸ್ಟ್ಯಾಶ್ ಮಾಡಿ"</string> - <string name="dock_forced_resizable" msgid="1749750436092293116">"ವಿಭಜಿಸಿದ ಸ್ಕ್ರೀನ್ನಲ್ಲಿ ಆ್ಯಪ್ ಕೆಲಸ ಮಾಡದೇ ಇರಬಹುದು."</string> - <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"ಅಪ್ಲಿಕೇಶನ್ ಸ್ಪ್ಲಿಟ್ ಸ್ಕ್ರೀನ್ ಅನ್ನು ಬೆಂಬಲಿಸುವುದಿಲ್ಲ."</string> + <!-- no translation found for dock_forced_resizable (7429086980048964687) --> + <skip /> + <!-- no translation found for dock_non_resizeble_failed_to_dock_text (2733543750291266047) --> + <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"ಈ ಆ್ಯಪ್ ಅನ್ನು 1 ವಿಂಡೋದಲ್ಲಿ ಮಾತ್ರ ತೆರೆಯಬಹುದು."</string> <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> + <!-- no translation found for accessibility_divider (6407584574218956849) --> + <skip /> + <!-- no translation found for divider_title (1963391955593749442) --> + <skip /> <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> @@ -46,6 +53,10 @@ <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"50% ಮೇಲಕ್ಕೆ"</string> <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"30% ಮೇಲಕ್ಕೆ"</string> <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"ಕೆಳಗಿನ ಪೂರ್ಣ ಪರದೆ"</string> + <string name="accessibility_split_left" msgid="1713683765575562458">"ಎಡಕ್ಕೆ ವಿಭಜಿಸಿ"</string> + <string name="accessibility_split_right" msgid="8441001008181296837">"ಬಲಕ್ಕೆ ವಿಭಜಿಸಿ"</string> + <string name="accessibility_split_top" msgid="2789329702027147146">"ಮೇಲಕ್ಕೆ ವಿಭಜಿಸಿ"</string> + <string name="accessibility_split_bottom" msgid="8694551025220868191">"ಕೆಳಕ್ಕೆ ವಿಭಜಿಸಿ"</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> @@ -61,6 +72,7 @@ <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"ಕೆಳಗಿನ ಬಲಭಾಗಕ್ಕೆ ಸರಿಸಿ"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> ಸೆಟ್ಟಿಂಗ್ಗಳು"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"ಬಬಲ್ ವಜಾಗೊಳಿಸಿ"</string> + <string name="bubbles_dont_bubble" msgid="3216183855437329223">"ಬಬಲ್ ತೋರಿಸಬೇಡಿ"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"ಸಂಭಾಷಣೆಯನ್ನು ಬಬಲ್ ಮಾಡಬೇಡಿ"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"ಬಬಲ್ಸ್ ಬಳಸಿ ಚಾಟ್ ಮಾಡಿ"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"ಹೊಸ ಸಂಭಾಷಣೆಗಳು ತೇಲುವ ಐಕಾನ್ಗಳು ಅಥವಾ ಬಬಲ್ಸ್ ಆಗಿ ಗೋಚರಿಸುತ್ತವೆ. ಬಬಲ್ ತೆರೆಯಲು ಟ್ಯಾಪ್ ಮಾಡಿ. ಅದನ್ನು ಡ್ರ್ಯಾಗ್ ಮಾಡಲು ಎಳೆಯಿರಿ."</string> @@ -72,17 +84,35 @@ <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) --> - <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) --> + <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"ನೋಡಿ ಮತ್ತು ಹೆಚ್ಚಿನದನ್ನು ಮಾಡಿ"</string> + <!-- no translation found for letterbox_education_split_screen_text (449233070804658627) --> <skip /> + <string name="letterbox_education_reposition_text" msgid="4589957299813220661">"ಆ್ಯಪ್ ಒಂದರ ಸ್ಥಾನವನ್ನು ಬದಲಾಯಿಸಲು ಅದರ ಹೊರಗೆ ಡಬಲ್-ಟ್ಯಾಪ್ ಮಾಡಿ"</string> <string name="letterbox_education_got_it" msgid="4057634570866051177">"ಸರಿ"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"ಇನ್ನಷ್ಟು ಮಾಹಿತಿಗಾಗಿ ವಿಸ್ತೃತಗೊಳಿಸಿ."</string> + <string name="letterbox_restart_dialog_title" msgid="8543049527871033505">"ಉತ್ತಮ ವೀಕ್ಷಣೆಗಾಗಿ ಆ್ಯಪ್ ಅನ್ನು ಮರುಪ್ರಾರಂಭಿಸಬೇಕೆ?"</string> + <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"ನೀವು ಆ್ಯಪ್ ಅನ್ನು ಮರುಪ್ರಾರಂಭಿಸಬಹುದು, ಇದರಿಂದ ನಿಮ್ಮ ಸ್ಕ್ರೀನ್ನಲ್ಲಿ ಆ್ಯಪ್ ಉತ್ತಮವಾಗಿ ಕಾಣಿಸುತ್ತದೆ, ಆದರೆ ನಿಮ್ಮ ಪ್ರಗತಿಯನ್ನು ಅಥವಾ ಯಾವುದೇ ಉಳಿಸದ ಬದಲಾವಣೆಗಳನ್ನು ನೀವು ಕಳೆದುಕೊಳ್ಳಬಹುದು"</string> + <string name="letterbox_restart_cancel" msgid="1342209132692537805">"ರದ್ದುಮಾಡಿ"</string> + <string name="letterbox_restart_restart" msgid="8529976234412442973">"ಮರುಪ್ರಾರಂಭಿಸಿ"</string> + <string name="letterbox_restart_dialog_checkbox_title" msgid="5252918008140768386">"ಮತ್ತೊಮ್ಮೆ ತೋರಿಸಬೇಡಿ"</string> + <string name="letterbox_reachability_reposition_text" msgid="4507890186297500893">"ಈ ಆ್ಯಪ್ ಅನ್ನು ಸರಿಸಲು ಡಬಲ್-ಟ್ಯಾಪ್ ಮಾಡಿ"</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> + <string name="back_button_text" msgid="1469718707134137085">"ಹಿಂದಕ್ಕೆ"</string> + <string name="handle_text" msgid="1766582106752184456">"ಹ್ಯಾಂಡಲ್"</string> + <string name="app_icon_text" msgid="2823268023931811747">"ಆ್ಯಪ್ ಐಕಾನ್"</string> + <string name="fullscreen_text" msgid="1162316685217676079">"ಫುಲ್ಸ್ಕ್ರೀನ್"</string> + <string name="desktop_text" msgid="1077633567027630454">"ಡೆಸ್ಕ್ಟಾಪ್ ಮೋಡ್"</string> + <string name="split_screen_text" msgid="1396336058129570886">"ಸ್ಪ್ಲಿಟ್ ಸ್ಕ್ರೀನ್"</string> + <string name="more_button_text" msgid="3655388105592893530">"ಇನ್ನಷ್ಟು"</string> + <string name="float_button_text" msgid="9221657008391364581">"ಫ್ಲೋಟ್"</string> + <string name="select_text" msgid="5139083974039906583">"ಆಯ್ಕೆಮಾಡಿ"</string> + <string name="screenshot_text" msgid="1477704010087786671">"ಸ್ಕ್ರೀನ್ಶಾಟ್"</string> + <string name="close_text" msgid="4986518933445178928">"ಮುಚ್ಚಿ"</string> + <string name="collapse_menu_text" msgid="7515008122450342029">"ಮೆನು ಮುಚ್ಚಿ"</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..3dfe573a6506 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="7930546669915337998">"ಕಂಟ್ರೋಲ್ಗಳಿಗಾಗಿ "<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..0ebeef1befa1 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> @@ -31,11 +32,17 @@ <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"크기 조절"</string> <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"숨기기"</string> <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"숨기기 취소"</string> - <string name="dock_forced_resizable" msgid="1749750436092293116">"앱이 분할 화면에서 작동하지 않을 수 있습니다."</string> - <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"앱이 화면 분할을 지원하지 않습니다."</string> + <!-- no translation found for dock_forced_resizable (7429086980048964687) --> + <skip /> + <!-- no translation found for dock_non_resizeble_failed_to_dock_text (2733543750291266047) --> + <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"이 앱은 창 1개에서만 열 수 있습니다."</string> <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> + <!-- no translation found for accessibility_divider (6407584574218956849) --> + <skip /> + <!-- no translation found for divider_title (1963391955593749442) --> + <skip /> <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> @@ -46,6 +53,10 @@ <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"위쪽 화면 50%"</string> <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"위쪽 화면 30%"</string> <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"아래쪽 화면 전체화면"</string> + <string name="accessibility_split_left" msgid="1713683765575562458">"왼쪽으로 분할"</string> + <string name="accessibility_split_right" msgid="8441001008181296837">"오른쪽으로 분할"</string> + <string name="accessibility_split_top" msgid="2789329702027147146">"위쪽으로 분할"</string> + <string name="accessibility_split_bottom" msgid="8694551025220868191">"아래쪽으로 분할"</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> @@ -61,6 +72,7 @@ <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"오른쪽 하단으로 이동"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> 설정"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"대화창 닫기"</string> + <string name="bubbles_dont_bubble" msgid="3216183855437329223">"도움말 풍선 중지"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"대화를 대화창으로 표시하지 않기"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"대화창으로 채팅하기"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"새로운 대화가 플로팅 아이콘인 대화창으로 표시됩니다. 대화창을 열려면 탭하세요. 드래그하여 이동할 수 있습니다."</string> @@ -72,17 +84,36 @@ <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) --> - <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) --> + <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"더 많은 정보를 보고 더 많은 작업을 처리하세요"</string> + <!-- no translation found for letterbox_education_split_screen_text (449233070804658627) --> <skip /> + <string name="letterbox_education_reposition_text" msgid="4589957299813220661">"앱 위치를 조정하려면 앱 외부를 두 번 탭합니다."</string> <string name="letterbox_education_got_it" msgid="4057634570866051177">"확인"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"추가 정보는 펼쳐서 확인하세요."</string> + <string name="letterbox_restart_dialog_title" msgid="8543049527871033505">"화면에 맞게 보도록 다시 시작할까요?"</string> + <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"앱을 다시 시작하면 화면에 더 잘 맞게 볼 수는 있지만 진행 상황 또는 저장되지 않은 변경사항을 잃을 수도 있습니다."</string> + <string name="letterbox_restart_cancel" msgid="1342209132692537805">"취소"</string> + <string name="letterbox_restart_restart" msgid="8529976234412442973">"다시 시작"</string> + <string name="letterbox_restart_dialog_checkbox_title" msgid="5252918008140768386">"다시 표시 안함"</string> + <!-- no translation found for letterbox_reachability_reposition_text (4507890186297500893) --> + <skip /> + <string name="maximize_button_text" msgid="1650859196290301963">"최대화"</string> + <string name="minimize_button_text" msgid="271592547935841753">"최소화"</string> + <string name="close_button_text" msgid="2913281996024033299">"닫기"</string> + <string name="back_button_text" msgid="1469718707134137085">"뒤로"</string> + <string name="handle_text" msgid="1766582106752184456">"핸들"</string> + <string name="app_icon_text" msgid="2823268023931811747">"앱 아이콘"</string> + <string name="fullscreen_text" msgid="1162316685217676079">"전체 화면"</string> + <string name="desktop_text" msgid="1077633567027630454">"데스크톱 모드"</string> + <string name="split_screen_text" msgid="1396336058129570886">"화면 분할"</string> + <string name="more_button_text" msgid="3655388105592893530">"더보기"</string> + <string name="float_button_text" msgid="9221657008391364581">"플로팅"</string> + <string name="select_text" msgid="5139083974039906583">"선택"</string> + <string name="screenshot_text" msgid="1477704010087786671">"스크린샷"</string> + <string name="close_text" msgid="4986518933445178928">"닫기"</string> + <string name="collapse_menu_text" msgid="7515008122450342029">"메뉴 닫기"</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..969a68d0346e 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="7930546669915337998">"제어 메뉴에 액세스하려면 "<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..d20f21060ad7 100644 --- a/libs/WindowManager/Shell/res/values-ky/strings.xml +++ b/libs/WindowManager/Shell/res/values-ky/strings.xml @@ -19,9 +19,10 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="pip_phone_close" msgid="5783752637260411309">"Жабуу"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Жайып көрсөтүү"</string> - <string name="pip_phone_settings" msgid="5468987116750491918">"Жөндөөлөр"</string> + <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> @@ -31,11 +32,17 @@ <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Өлчөмүн өзгөртүү"</string> <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Сейфке салуу"</string> <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Сейфтен чыгаруу"</string> - <string name="dock_forced_resizable" msgid="1749750436092293116">"Колдонмодо экран бөлүнбөшү мүмкүн."</string> - <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Колдонмодо экран бөлүнбөйт."</string> + <!-- no translation found for dock_forced_resizable (7429086980048964687) --> + <skip /> + <!-- no translation found for dock_non_resizeble_failed_to_dock_text (2733543750291266047) --> + <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Бул колдонмону 1 терезеде гана ачууга болот."</string> <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> + <!-- no translation found for accessibility_divider (6407584574218956849) --> + <skip /> + <!-- no translation found for divider_title (1963391955593749442) --> + <skip /> <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> @@ -46,21 +53,26 @@ <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Үстүнкү экранды 50%"</string> <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Үстүнкү экранды 30%"</string> <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Ылдыйкы экранды толук экран режимине өткөрүү"</string> + <string name="accessibility_split_left" msgid="1713683765575562458">"Солго бөлүү"</string> + <string name="accessibility_split_right" msgid="8441001008181296837">"Оңго бөлүү"</string> + <string name="accessibility_split_top" msgid="2789329702027147146">"Өйдө бөлүү"</string> + <string name="accessibility_split_bottom" msgid="8694551025220868191">"Ылдый бөлүү"</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="bubbles_settings_button_description" msgid="1301286017420516912">"<xliff:g id="APP_NAME">%1$s</xliff:g> калкып чыкма билдирмелер жөндөөлөрү"</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> <string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="APP_NAME">%2$s</xliff:g> колдонмосунан <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> <string name="bubble_content_description_stack" msgid="8071515017164630429">"<xliff:g id="APP_NAME">%2$s</xliff:g> жана дагы <xliff:g id="BUBBLE_COUNT">%3$d</xliff:g> колдонмодон <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"Жогорку сол жакка жылдыруу"</string> - <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Жогорку оң жакка жылдырыңыз"</string> + <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Жогорку оң жакка жылдыруу"</string> <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Төмөнкү сол жакка жылдыруу"</string> - <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Төмөнкү оң жакка жылдырыңыз"</string> - <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> жөндөөлөрү"</string> + <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Төмөнкү оң жакка жылдыруу"</string> + <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> параметрлери"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Калкып чыкма билдирмени жабуу"</string> + <string name="bubbles_dont_bubble" msgid="3216183855437329223">"Калкып чыкма билдирмелер көрсөтүлбөсүн"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Жазышууда калкып чыкма билдирмелер көрүнбөсүн"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Калкып чыкма билдирмелер аркылуу маектешүү"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"Жаңы жазышуулар калкыма сүрөтчөлөр же калкып чыкма билдирмелер түрүндө көрүнөт. Калкып чыкма билдирмелерди ачуу үчүн таптап коюңуз. Жылдыруу үчүн сүйрөңүз."</string> @@ -72,17 +84,36 @@ <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) --> - <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) --> + <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Көрүп, көбүрөөк нерселерди жасаңыз"</string> + <!-- no translation found for letterbox_education_split_screen_text (449233070804658627) --> <skip /> + <string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Колдонмону жылдыруу үчүн сырт жагын эки жолу таптаңыз"</string> <string name="letterbox_education_got_it" msgid="4057634570866051177">"Түшүндүм"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Толук маалымат алуу үчүн жайып көрүңүз."</string> + <string name="letterbox_restart_dialog_title" msgid="8543049527871033505">"Жакшыраак көрүү үчүн өчүрүп күйгүзөсүзбү?"</string> + <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"Экранда жакшыраак көрүү үчүн колдонмону өчүрүп күйгүзө аласыз, бирок аткарылган иш же сакталбаган өзгөрүүлөр өчүрүлүшү мүмкүн"</string> + <string name="letterbox_restart_cancel" msgid="1342209132692537805">"Токтотуу"</string> + <string name="letterbox_restart_restart" msgid="8529976234412442973">"Өчүрүп күйгүзүү"</string> + <string name="letterbox_restart_dialog_checkbox_title" msgid="5252918008140768386">"Экинчи көрүнбөсүн"</string> + <!-- no translation found for letterbox_reachability_reposition_text (4507890186297500893) --> + <skip /> + <string name="maximize_button_text" msgid="1650859196290301963">"Чоңойтуу"</string> + <string name="minimize_button_text" msgid="271592547935841753">"Кичирейтүү"</string> + <string name="close_button_text" msgid="2913281996024033299">"Жабуу"</string> + <string name="back_button_text" msgid="1469718707134137085">"Артка"</string> + <string name="handle_text" msgid="1766582106752184456">"Маркер"</string> + <string name="app_icon_text" msgid="2823268023931811747">"Колдонмонун сүрөтчөсү"</string> + <string name="fullscreen_text" msgid="1162316685217676079">"Толук экран"</string> + <string name="desktop_text" msgid="1077633567027630454">"Компьютер режими"</string> + <string name="split_screen_text" msgid="1396336058129570886">"Экранды бөлүү"</string> + <string name="more_button_text" msgid="3655388105592893530">"Дагы"</string> + <string name="float_button_text" msgid="9221657008391364581">"Калкыма"</string> + <string name="select_text" msgid="5139083974039906583">"Тандоо"</string> + <string name="screenshot_text" msgid="1477704010087786671">"Скриншот"</string> + <string name="close_text" msgid="4986518933445178928">"Жабуу"</string> + <string name="collapse_menu_text" msgid="7515008122450342029">"Менюну жабуу"</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..68262e521f4a 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="7930546669915337998">"Башкаруу элементтерин ачуу үчүн "<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..064717a8faf0 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> @@ -31,11 +32,17 @@ <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"ປ່ຽນຂະໜາດ"</string> <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"ເກັບໄວ້ບ່ອນເກັບສ່ວນຕົວ"</string> <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"ເອົາອອກຈາກບ່ອນເກັບສ່ວນຕົວ"</string> - <string name="dock_forced_resizable" msgid="1749750436092293116">"ແອັບອາດໃຊ້ບໍ່ໄດ້ກັບການແບ່ງໜ້າຈໍ."</string> - <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"ແອັບບໍ່ຮອງຮັບໜ້າຈໍແບບແຍກກັນ."</string> + <!-- no translation found for dock_forced_resizable (7429086980048964687) --> + <skip /> + <!-- no translation found for dock_non_resizeble_failed_to_dock_text (2733543750291266047) --> + <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"ແອັບນີ້ສາມາດເປີດໄດ້ໃນ 1 ໜ້າຈໍເທົ່ານັ້ນ."</string> <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> + <!-- no translation found for accessibility_divider (6407584574218956849) --> + <skip /> + <!-- no translation found for divider_title (1963391955593749442) --> + <skip /> <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> @@ -46,6 +53,10 @@ <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"ເທິງສຸດ 50%"</string> <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"ເທິງສຸດ 30%"</string> <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"ເຕັມໜ້າຈໍລຸ່ມສຸດ"</string> + <string name="accessibility_split_left" msgid="1713683765575562458">"ແຍກຊ້າຍ"</string> + <string name="accessibility_split_right" msgid="8441001008181296837">"ແຍກຂວາ"</string> + <string name="accessibility_split_top" msgid="2789329702027147146">"ແຍກເທິງ"</string> + <string name="accessibility_split_bottom" msgid="8694551025220868191">"ແຍກລຸ່ມ"</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> @@ -61,6 +72,7 @@ <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"ຍ້າຍຂວາລຸ່ມ"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"ການຕັ້ງຄ່າ <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"ປິດຟອງໄວ້"</string> + <string name="bubbles_dont_bubble" msgid="3216183855437329223">"ບໍ່ຕ້ອງສະແດງ bubble"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"ຢ່າໃຊ້ຟອງໃນການສົນທະນາ"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"ສົນທະນາໂດຍໃຊ້ຟອງ"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"ການສົນທະນາໃໝ່ຈະປາກົດເປັນໄອຄອນ ຫຼື ຟອງແບບລອຍ. ແຕະເພື່ອເປີດຟອງ. ລາກເພື່ອຍ້າຍມັນ."</string> @@ -72,17 +84,35 @@ <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) --> - <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) --> + <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"ເບິ່ງ ແລະ ເຮັດຫຼາຍຂຶ້ນ"</string> + <!-- no translation found for letterbox_education_split_screen_text (449233070804658627) --> <skip /> + <string name="letterbox_education_reposition_text" msgid="4589957299813220661">"ແຕະສອງເທື່ອໃສ່ນອກແອັບໃດໜຶ່ງເພື່ອຈັດຕຳແໜ່ງຂອງມັນຄືນໃໝ່"</string> <string name="letterbox_education_got_it" msgid="4057634570866051177">"ເຂົ້າໃຈແລ້ວ"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"ຂະຫຍາຍເພື່ອເບິ່ງຂໍ້ມູນເພີ່ມເຕີມ."</string> + <string name="letterbox_restart_dialog_title" msgid="8543049527871033505">"ຣີສະຕາດເພື່ອໃຫ້ມີມຸມມອງທີ່ດີຂຶ້ນບໍ?"</string> + <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"ທ່ານສາມາດຣີສະຕາດແອັບໄດ້ເພື່ອໃຫ້ມັນເບິ່ງດີຂຶ້ນໃນໜ້າຈໍຂອງທ່ານ ແຕ່ທ່ານອາດຈະສູນເສຍຄວາມຄືບໜ້າ ຫຼື ການປ່ຽນແປງທີ່ບໍ່ໄດ້ບັນທຶກໄວ້"</string> + <string name="letterbox_restart_cancel" msgid="1342209132692537805">"ຍົກເລີກ"</string> + <string name="letterbox_restart_restart" msgid="8529976234412442973">"ຣີສະຕາດ"</string> + <string name="letterbox_restart_dialog_checkbox_title" msgid="5252918008140768386">"ບໍ່ຕ້ອງສະແດງອີກ"</string> + <string name="letterbox_reachability_reposition_text" msgid="4507890186297500893">"ແຕະສອງເທື່ອເພື່ອຍ້າຍແອັບນີ້"</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> + <string name="back_button_text" msgid="1469718707134137085">"ກັບຄືນ"</string> + <string name="handle_text" msgid="1766582106752184456">"ມືບັງຄັບ"</string> + <string name="app_icon_text" msgid="2823268023931811747">"ໄອຄອນແອັບ"</string> + <string name="fullscreen_text" msgid="1162316685217676079">"ເຕັມຈໍ"</string> + <string name="desktop_text" msgid="1077633567027630454">"ໂໝດເດັສທັອບ"</string> + <string name="split_screen_text" msgid="1396336058129570886">"ແບ່ງໜ້າຈໍ"</string> + <string name="more_button_text" msgid="3655388105592893530">"ເພີ່ມເຕີມ"</string> + <string name="float_button_text" msgid="9221657008391364581">"ລອຍ"</string> + <string name="select_text" msgid="5139083974039906583">"ເລືອກ"</string> + <string name="screenshot_text" msgid="1477704010087786671">"ຮູບໜ້າຈໍ"</string> + <string name="close_text" msgid="4986518933445178928">"ປິດ"</string> + <string name="collapse_menu_text" msgid="7515008122450342029">"ປິດເມນູ"</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..b84c83555ea0 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="7930546669915337998">"ກົດ "<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..12a81b67e9ec 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> @@ -31,11 +32,17 @@ <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Pakeisti dydį"</string> <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Paslėpti"</string> <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Nebeslėpti"</string> - <string name="dock_forced_resizable" msgid="1749750436092293116">"Programa gali neveikti naudojant išskaidyto ekrano režimą."</string> - <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Programoje nepalaikomas skaidytas ekranas."</string> + <!-- no translation found for dock_forced_resizable (7429086980048964687) --> + <skip /> + <!-- no translation found for dock_non_resizeble_failed_to_dock_text (2733543750291266047) --> + <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Šią programą galima atidaryti tik viename lange."</string> <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> + <!-- no translation found for accessibility_divider (6407584574218956849) --> + <skip /> + <!-- no translation found for divider_title (1963391955593749442) --> + <skip /> <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> @@ -46,6 +53,10 @@ <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Viršutinis ekranas 50 %"</string> <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Viršutinis ekranas 30 %"</string> <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Apatinis ekranas viso ekrano režimu"</string> + <string name="accessibility_split_left" msgid="1713683765575562458">"Išskaidyti kairėn"</string> + <string name="accessibility_split_right" msgid="8441001008181296837">"Išskaidyti dešinėn"</string> + <string name="accessibility_split_top" msgid="2789329702027147146">"Išskaidyti viršuje"</string> + <string name="accessibility_split_bottom" msgid="8694551025220868191">"Išskaidyti apačioje"</string> <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Vienos rankos režimo naudojimas"</string> <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Jei norite išeiti, perbraukite aukštyn nuo ekrano apačios arba palieskite bet kur virš programos"</string> <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Pradėti vienos rankos režimą"</string> @@ -61,6 +72,7 @@ <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Perkelti į apačią dešinėje"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"„<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>“ nustatymai"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Atsisakyti burbulo"</string> + <string name="bubbles_dont_bubble" msgid="3216183855437329223">"Nerodyti debesėlių"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Nerodyti pokalbio burbule"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Pokalbis naudojant burbulus"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"Nauji pokalbiai rodomi kaip slankiosios piktogramos arba burbulai. Palieskite, kad atidarytumėte burbulą. Vilkite, kad perkeltumėte."</string> @@ -72,17 +84,35 @@ <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) --> - <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) --> + <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Daugiau turinio ir funkcijų"</string> + <!-- no translation found for letterbox_education_split_screen_text (449233070804658627) --> <skip /> + <string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Dukart palieskite už programos ribų, kad pakeistumėte jos poziciją"</string> <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="letterbox_restart_dialog_title" msgid="8543049527871033505">"Paleisti iš naujo, kad būtų geresnis vaizdas?"</string> + <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"Galite iš naujo paleisti programą, kad ji geriau atrodytų ekrane, bet galite prarasti eigą ir neišsaugotus pakeitimus"</string> + <string name="letterbox_restart_cancel" msgid="1342209132692537805">"Atšaukti"</string> + <string name="letterbox_restart_restart" msgid="8529976234412442973">"Paleisti iš naujo"</string> + <string name="letterbox_restart_dialog_checkbox_title" msgid="5252918008140768386">"Daugiau neberodyti"</string> + <string name="letterbox_reachability_reposition_text" msgid="4507890186297500893">"Dukart palieskite, kad perkeltumėte šią programą"</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> + <string name="back_button_text" msgid="1469718707134137085">"Atgal"</string> + <string name="handle_text" msgid="1766582106752184456">"Rankenėlė"</string> + <string name="app_icon_text" msgid="2823268023931811747">"Programos piktograma"</string> + <string name="fullscreen_text" msgid="1162316685217676079">"Visas ekranas"</string> + <string name="desktop_text" msgid="1077633567027630454">"Stalinio kompiuterio režimas"</string> + <string name="split_screen_text" msgid="1396336058129570886">"Išskaidyto ekrano režimas"</string> + <string name="more_button_text" msgid="3655388105592893530">"Daugiau"</string> + <string name="float_button_text" msgid="9221657008391364581">"Slankusis langas"</string> + <string name="select_text" msgid="5139083974039906583">"Pasirinkti"</string> + <string name="screenshot_text" msgid="1477704010087786671">"Ekrano kopija"</string> + <string name="close_text" msgid="4986518933445178928">"Uždaryti"</string> + <string name="collapse_menu_text" msgid="7515008122450342029">"Uždaryti meniu"</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..0537553cc36a 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="7930546669915337998">"Jei reikia valdiklių, dukart pasp. "<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..102f3c82dd0a 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> @@ -31,11 +32,17 @@ <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Mainīt lielumu"</string> <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Paslēpt"</string> <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Rādīt"</string> - <string name="dock_forced_resizable" msgid="1749750436092293116">"Iespējams, lietotne nedarbosies ekrāna sadalīšanas režīmā."</string> - <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Lietotnē netiek atbalstīta ekrāna sadalīšana."</string> + <!-- no translation found for dock_forced_resizable (7429086980048964687) --> + <skip /> + <!-- no translation found for dock_non_resizeble_failed_to_dock_text (2733543750291266047) --> + <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Šo lietotni var atvērt tikai vienā logā."</string> <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> + <!-- no translation found for accessibility_divider (6407584574218956849) --> + <skip /> + <!-- no translation found for divider_title (1963391955593749442) --> + <skip /> <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> @@ -46,6 +53,10 @@ <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Augšdaļa 50%"</string> <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Augšdaļa 30%"</string> <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Apakšdaļu pa visu ekrānu"</string> + <string name="accessibility_split_left" msgid="1713683765575562458">"Sadalījums pa kreisi"</string> + <string name="accessibility_split_right" msgid="8441001008181296837">"Sadalījums pa labi"</string> + <string name="accessibility_split_top" msgid="2789329702027147146">"Sadalījums augšdaļā"</string> + <string name="accessibility_split_bottom" msgid="8694551025220868191">"Sadalījums apakšdaļā"</string> <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Vienas rokas režīma izmantošana"</string> <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Lai izietu, velciet augšup no ekrāna apakšdaļas vai pieskarieties jebkurā vietā virs lietotnes"</string> <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Pāriet vienas rokas režīmā"</string> @@ -61,6 +72,7 @@ <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Pārvietot apakšpusē pa labi"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"Lietotnes <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> iestatījumi"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Nerādīt burbuli"</string> + <string name="bubbles_dont_bubble" msgid="3216183855437329223">"Pārtraukt burbuļu rādīšanu"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Nerādīt sarunu burbuļos"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Tērzēšana, izmantojot burbuļus"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"Jaunas sarunas tiek rādītas kā peldošas ikonas vai burbuļi. Pieskarieties, lai atvērtu burbuli. Velciet, lai to pārvietotu."</string> @@ -72,17 +84,36 @@ <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) --> - <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) --> + <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Uzziniet un paveiciet vairāk"</string> + <!-- no translation found for letterbox_education_split_screen_text (449233070804658627) --> <skip /> + <string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Lai pārvietotu lietotni, veiciet dubultskārienu ārpus lietotnes"</string> <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="letterbox_restart_dialog_title" msgid="8543049527871033505">"Vai restartēt, lai uzlabotu skatu?"</string> + <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"Varat restartēt lietotni, lai tā labāk izskatītos ekrānā, taču, iespējams, zaudēsiet paveikto vai nesaglabātas izmaiņas (ja tādas ir)."</string> + <string name="letterbox_restart_cancel" msgid="1342209132692537805">"Atcelt"</string> + <string name="letterbox_restart_restart" msgid="8529976234412442973">"Restartēt"</string> + <string name="letterbox_restart_dialog_checkbox_title" msgid="5252918008140768386">"Vairs nerādīt"</string> + <!-- no translation found for letterbox_reachability_reposition_text (4507890186297500893) --> + <skip /> + <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> + <string name="back_button_text" msgid="1469718707134137085">"Atpakaļ"</string> + <string name="handle_text" msgid="1766582106752184456">"Turis"</string> + <string name="app_icon_text" msgid="2823268023931811747">"Lietotnes ikona"</string> + <string name="fullscreen_text" msgid="1162316685217676079">"Pilnekrāna režīms"</string> + <string name="desktop_text" msgid="1077633567027630454">"Darbvirsmas režīms"</string> + <string name="split_screen_text" msgid="1396336058129570886">"Sadalīt ekrānu"</string> + <string name="more_button_text" msgid="3655388105592893530">"Vairāk"</string> + <string name="float_button_text" msgid="9221657008391364581">"Peldošs"</string> + <string name="select_text" msgid="5139083974039906583">"Atlasīt"</string> + <string name="screenshot_text" msgid="1477704010087786671">"Ekrānuzņēmums"</string> + <string name="close_text" msgid="4986518933445178928">"Aizvērt"</string> + <string name="collapse_menu_text" msgid="7515008122450342029">"Aizvērt izvēlni"</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..13baa9bc46eb 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="7930546669915337998">"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..1adb7aab2f34 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> @@ -31,11 +32,17 @@ <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Промени големина"</string> <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Сокријте"</string> <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Прикажете"</string> - <string name="dock_forced_resizable" msgid="1749750436092293116">"Апликацијата може да не работи со поделен екран."</string> - <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Апликацијата не поддржува поделен екран."</string> + <!-- no translation found for dock_forced_resizable (7429086980048964687) --> + <skip /> + <!-- no translation found for dock_non_resizeble_failed_to_dock_text (2733543750291266047) --> + <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Апликацијава може да се отвори само во еден прозорец."</string> <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> + <!-- no translation found for accessibility_divider (6407584574218956849) --> + <skip /> + <!-- no translation found for divider_title (1963391955593749442) --> + <skip /> <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> @@ -46,6 +53,10 @@ <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Горниот 50%"</string> <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Горниот 30%"</string> <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Долниот на цел екран"</string> + <string name="accessibility_split_left" msgid="1713683765575562458">"Подели налево"</string> + <string name="accessibility_split_right" msgid="8441001008181296837">"Подели надесно"</string> + <string name="accessibility_split_top" msgid="2789329702027147146">"Подели нагоре"</string> + <string name="accessibility_split_bottom" msgid="8694551025220868191">"Подели долу"</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> @@ -61,6 +72,7 @@ <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Премести долу десно"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"Поставки за <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Отфрли балонче"</string> + <string name="bubbles_dont_bubble" msgid="3216183855437329223">"Не прикажувај балонче"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Не прикажувај го разговорот во балончиња"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Разговор во балончиња"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"Новите разговори ќе се појавуваат како лебдечки икони или балончиња. Допрете за отворање на балончето. Повлечете за да го преместите."</string> @@ -72,17 +84,35 @@ <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) --> - <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) --> + <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Погледнете и направете повеќе"</string> + <!-- no translation found for letterbox_education_split_screen_text (449233070804658627) --> <skip /> + <string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Допрете двапати надвор од некоја апликација за да ја преместите"</string> <string name="letterbox_education_got_it" msgid="4057634570866051177">"Сфатив"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Проширете за повеќе информации."</string> + <string name="letterbox_restart_dialog_title" msgid="8543049527871033505">"Да се рестартира за подобар приказ?"</string> + <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"Може да ја рестартирате апликацијата за да изгледа подобро на екранот, но може да го изгубите напредокот или незачуваните промени"</string> + <string name="letterbox_restart_cancel" msgid="1342209132692537805">"Откажи"</string> + <string name="letterbox_restart_restart" msgid="8529976234412442973">"Рестартирај"</string> + <string name="letterbox_restart_dialog_checkbox_title" msgid="5252918008140768386">"Не прикажувај повторно"</string> + <string name="letterbox_reachability_reposition_text" msgid="4507890186297500893">"Допрете двапати за да ја поместите апликацијава"</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> + <string name="back_button_text" msgid="1469718707134137085">"Назад"</string> + <string name="handle_text" msgid="1766582106752184456">"Прекар"</string> + <string name="app_icon_text" msgid="2823268023931811747">"Икона на апликацијата"</string> + <string name="fullscreen_text" msgid="1162316685217676079">"Цел екран"</string> + <string name="desktop_text" msgid="1077633567027630454">"Режим за компјутер"</string> + <string name="split_screen_text" msgid="1396336058129570886">"Поделен екран"</string> + <string name="more_button_text" msgid="3655388105592893530">"Повеќе"</string> + <string name="float_button_text" msgid="9221657008391364581">"Лебдечко"</string> + <string name="select_text" msgid="5139083974039906583">"Изберете"</string> + <string name="screenshot_text" msgid="1477704010087786671">"Слика од екранот"</string> + <string name="close_text" msgid="4986518933445178928">"Затворете"</string> + <string name="collapse_menu_text" msgid="7515008122450342029">"Затворете го менито"</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..d7a9516bea7f 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="7930546669915337998">"Притиснете двапати на "<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..923fbc252f0e 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> @@ -31,11 +32,17 @@ <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"വലുപ്പം മാറ്റുക"</string> <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"സ്റ്റാഷ് ചെയ്യൽ"</string> <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"അൺസ്റ്റാഷ് ചെയ്യൽ"</string> - <string name="dock_forced_resizable" msgid="1749750436092293116">"സ്ക്രീൻ വിഭജന മോഡിൽ ആപ്പ് പ്രവർത്തിച്ചേക്കില്ല."</string> - <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"സ്പ്ലിറ്റ്-സ്ക്രീനിനെ ആപ്പ് പിന്തുണയ്ക്കുന്നില്ല."</string> + <!-- no translation found for dock_forced_resizable (7429086980048964687) --> + <skip /> + <!-- no translation found for dock_non_resizeble_failed_to_dock_text (2733543750291266047) --> + <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"ഈ ആപ്പ് ഒരു വിൻഡോയിൽ മാത്രമേ തുറക്കാനാകൂ."</string> <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> + <!-- no translation found for accessibility_divider (6407584574218956849) --> + <skip /> + <!-- no translation found for divider_title (1963391955593749442) --> + <skip /> <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> @@ -46,6 +53,10 @@ <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"മുകളിൽ 50%"</string> <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"മുകളിൽ 30%"</string> <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"താഴെ പൂർണ്ണ സ്ക്രീൻ"</string> + <string name="accessibility_split_left" msgid="1713683765575562458">"ഇടത് ഭാഗത്തേക്ക് വിഭജിക്കുക"</string> + <string name="accessibility_split_right" msgid="8441001008181296837">"വലത് ഭാഗത്തേക്ക് വിഭജിക്കുക"</string> + <string name="accessibility_split_top" msgid="2789329702027147146">"മുകളിലേക്ക് വിഭജിക്കുക"</string> + <string name="accessibility_split_bottom" msgid="8694551025220868191">"താഴേക്ക് വിഭജിക്കുക"</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> @@ -61,6 +72,7 @@ <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"ചുവടെ വലതുഭാഗത്തേക്ക് നീക്കുക"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> ക്രമീകരണം"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"ബബിൾ ഡിസ്മിസ് ചെയ്യൂ"</string> + <string name="bubbles_dont_bubble" msgid="3216183855437329223">"ബബിൾ ചെയ്യരുത്"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"സംഭാഷണം ബബിൾ ചെയ്യരുത്"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"ബബിളുകൾ ഉപയോഗിച്ച് ചാറ്റ് ചെയ്യുക"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"പുതിയ സംഭാഷണങ്ങൾ ഫ്ലോട്ടിംഗ് ഐക്കണുകളോ ബബിളുകളോ ആയി ദൃശ്യമാവുന്നു. ബബിൾ തുറക്കാൻ ടാപ്പ് ചെയ്യൂ. ഇത് നീക്കാൻ വലിച്ചിടുക."</string> @@ -72,17 +84,35 @@ <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) --> - <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) --> + <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"കൂടുതൽ കാണുക, ചെയ്യുക"</string> + <!-- no translation found for letterbox_education_split_screen_text (449233070804658627) --> <skip /> + <string name="letterbox_education_reposition_text" msgid="4589957299813220661">"ആപ്പിന്റെ സ്ഥാനം മാറ്റാൻ അതിന് പുറത്ത് ഡബിൾ ടാപ്പ് ചെയ്യുക"</string> <string name="letterbox_education_got_it" msgid="4057634570866051177">"മനസ്സിലായി"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"കൂടുതൽ വിവരങ്ങൾക്ക് വികസിപ്പിക്കുക."</string> + <string name="letterbox_restart_dialog_title" msgid="8543049527871033505">"മെച്ചപ്പെട്ട കാഴ്ചയ്ക്കായി റീസ്റ്റാർട്ട് ചെയ്യണോ?"</string> + <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"ആപ്പ് റീസ്റ്റാർട്ട് ചെയ്യുകയാണെങ്കിൽ ഇത് നിങ്ങളുടെ സ്ക്രീനിൽ മെച്ചപ്പെട്ടതായി കാണും, എന്നാൽ ഇതുവരെയുള്ള പുരോഗതിയും സംരക്ഷിക്കാത്ത മാറ്റങ്ങളും നിങ്ങൾക്ക് നഷ്ടമാകും"</string> + <string name="letterbox_restart_cancel" msgid="1342209132692537805">"റദ്ദാക്കുക"</string> + <string name="letterbox_restart_restart" msgid="8529976234412442973">"റീസ്റ്റാർട്ട് ചെയ്യൂ"</string> + <string name="letterbox_restart_dialog_checkbox_title" msgid="5252918008140768386">"വീണ്ടും കാണിക്കരുത്"</string> + <string name="letterbox_reachability_reposition_text" msgid="4507890186297500893">"ഈ ആപ്പ് നീക്കാൻ ഡബിൾ ടാപ്പ് ചെയ്യുക"</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> + <string name="back_button_text" msgid="1469718707134137085">"മടങ്ങുക"</string> + <string name="handle_text" msgid="1766582106752184456">"ഹാൻഡിൽ"</string> + <string name="app_icon_text" msgid="2823268023931811747">"ആപ്പ് ഐക്കൺ"</string> + <string name="fullscreen_text" msgid="1162316685217676079">"പൂർണ്ണസ്ക്രീൻ"</string> + <string name="desktop_text" msgid="1077633567027630454">"ഡെസ്ക്ടോപ്പ് മോഡ്"</string> + <string name="split_screen_text" msgid="1396336058129570886">"സ്ക്രീൻ വിഭജനം"</string> + <string name="more_button_text" msgid="3655388105592893530">"കൂടുതൽ"</string> + <string name="float_button_text" msgid="9221657008391364581">"ഫ്ലോട്ട്"</string> + <string name="select_text" msgid="5139083974039906583">"തിരഞ്ഞെടുക്കുക"</string> + <string name="screenshot_text" msgid="1477704010087786671">"സ്ക്രീൻഷോട്ട്"</string> + <string name="close_text" msgid="4986518933445178928">"അടയ്ക്കുക"</string> + <string name="collapse_menu_text" msgid="7515008122450342029">"മെനു അടയ്ക്കുക"</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..56f2b196421b 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="7930546669915337998">"നിയന്ത്രണങ്ങൾക്കായി "<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..eccbc7e1d001 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> @@ -31,11 +32,17 @@ <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Хэмжээг өөрчлөх"</string> <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Нуух"</string> <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Ил гаргах"</string> - <string name="dock_forced_resizable" msgid="1749750436092293116">"Апп хуваагдсан дэлгэц дээр ажиллахгүй байж болзошгүй."</string> - <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Энэ апп нь дэлгэц хуваах тохиргоог дэмждэггүй."</string> + <!-- no translation found for dock_forced_resizable (7429086980048964687) --> + <skip /> + <!-- no translation found for dock_non_resizeble_failed_to_dock_text (2733543750291266047) --> + <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Энэ аппыг зөвхөн 1 цонхонд нээх боломжтой."</string> <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> + <!-- no translation found for accessibility_divider (6407584574218956849) --> + <skip /> + <!-- no translation found for divider_title (1963391955593749442) --> + <skip /> <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> @@ -46,6 +53,10 @@ <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Дээд 50%"</string> <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Дээд 30%"</string> <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Доод бүтэн дэлгэц"</string> + <string name="accessibility_split_left" msgid="1713683765575562458">"Зүүн талд хуваах"</string> + <string name="accessibility_split_right" msgid="8441001008181296837">"Баруун талд хуваах"</string> + <string name="accessibility_split_top" msgid="2789329702027147146">"Дээд талд хуваах"</string> + <string name="accessibility_split_bottom" msgid="8694551025220868191">"Доод талд хуваах"</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> @@ -61,6 +72,7 @@ <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Баруун доош зөөх"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>-н тохиргоо"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Бөмбөлгийг хаах"</string> + <string name="bubbles_dont_bubble" msgid="3216183855437329223">"Бөмбөлөг бүү харуул"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Харилцан яриаг бүү бөмбөлөг болго"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Бөмбөлөг ашиглан чатлаарай"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"Шинэ харилцан яриа нь хөвөгч дүрс тэмдэг эсвэл бөмбөлөг хэлбэрээр харагддаг. Бөмбөлгийг нээхийн тулд товшино уу. Түүнийг зөөхийн тулд чирнэ үү."</string> @@ -72,17 +84,36 @@ <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) --> - <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) --> + <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Харж илүү ихийг хий"</string> + <!-- no translation found for letterbox_education_split_screen_text (449233070804658627) --> <skip /> + <string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Аппыг дахин байрлуулахын тулд гадна талд нь хоёр товшино"</string> <string name="letterbox_education_got_it" msgid="4057634570866051177">"Ойлголоо"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Нэмэлт мэдээлэл авах бол дэлгэнэ үү."</string> + <string name="letterbox_restart_dialog_title" msgid="8543049527871033505">"Харагдах байдлыг сайжруулахын тулд дахин эхлүүлэх үү?"</string> + <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"Та аппыг дахин эхлүүлэх боломжтой бөгөөд ингэснээр энэ нь таны дэлгэцэд илүү сайн харагдах хэдий ч та явцаа эсвэл хадгалаагүй аливаа өөрчлөлтөө алдаж магадгүй"</string> + <string name="letterbox_restart_cancel" msgid="1342209132692537805">"Цуцлах"</string> + <string name="letterbox_restart_restart" msgid="8529976234412442973">"Дахин эхлүүлэх"</string> + <string name="letterbox_restart_dialog_checkbox_title" msgid="5252918008140768386">"Дахиж бүү харуул"</string> + <!-- no translation found for letterbox_reachability_reposition_text (4507890186297500893) --> + <skip /> + <string name="maximize_button_text" msgid="1650859196290301963">"Томруулах"</string> + <string name="minimize_button_text" msgid="271592547935841753">"Багасгах"</string> + <string name="close_button_text" msgid="2913281996024033299">"Хаах"</string> + <string name="back_button_text" msgid="1469718707134137085">"Буцах"</string> + <string name="handle_text" msgid="1766582106752184456">"Бариул"</string> + <string name="app_icon_text" msgid="2823268023931811747">"Aппын дүрс тэмдэг"</string> + <string name="fullscreen_text" msgid="1162316685217676079">"Бүтэн дэлгэц"</string> + <string name="desktop_text" msgid="1077633567027630454">"Дэлгэцийн горим"</string> + <string name="split_screen_text" msgid="1396336058129570886">"Дэлгэцийг хуваах"</string> + <string name="more_button_text" msgid="3655388105592893530">"Бусад"</string> + <string name="float_button_text" msgid="9221657008391364581">"Хөвөгч"</string> + <string name="select_text" msgid="5139083974039906583">"Сонгох"</string> + <string name="screenshot_text" msgid="1477704010087786671">"Дэлгэцийн агшин"</string> + <string name="close_text" msgid="4986518933445178928">"Хаах"</string> + <string name="collapse_menu_text" msgid="7515008122450342029">"Цэсийг хаах"</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..0e6dcca17e38 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="7930546669915337998">"Хяналтад хандах бол "<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..26cadf6ca5d1 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> @@ -31,11 +32,17 @@ <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"आकार बदला"</string> <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"स्टॅश करा"</string> <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"अनस्टॅश करा"</string> - <string name="dock_forced_resizable" msgid="1749750436092293116">"अॅप कदाचित स्प्लिट स्क्रीनसह काम करू शकत नाही."</string> - <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"अॅप स्क्रीन-विभाजनास समर्थन देत नाही."</string> + <!-- no translation found for dock_forced_resizable (7429086980048964687) --> + <skip /> + <!-- no translation found for dock_non_resizeble_failed_to_dock_text (2733543750291266047) --> + <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"हे अॅप फक्त एका विंडोमध्ये उघडले जाऊ शकते."</string> <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> + <!-- no translation found for accessibility_divider (6407584574218956849) --> + <skip /> + <!-- no translation found for divider_title (1963391955593749442) --> + <skip /> <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> @@ -46,6 +53,10 @@ <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"शीर्ष 50%"</string> <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"शीर्ष 10"</string> <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"तळाशी फुल स्क्रीन"</string> + <string name="accessibility_split_left" msgid="1713683765575562458">"डावीकडे स्प्लिट करा"</string> + <string name="accessibility_split_right" msgid="8441001008181296837">"उजवीकडे स्प्लिट करा"</string> + <string name="accessibility_split_top" msgid="2789329702027147146">"सर्वात वरती स्प्लिट करा"</string> + <string name="accessibility_split_bottom" msgid="8694551025220868191">"खालती स्प्लिट करा"</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> @@ -61,6 +72,7 @@ <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"तळाशी उजवीकडे हलवा"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> सेटिंग्ज"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"बबल डिसमिस करा"</string> + <string name="bubbles_dont_bubble" msgid="3216183855437329223">"बबल दाखवू नका"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"संभाषणाला बबल करू नका"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"बबल वापरून चॅट करा"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"नवीन संभाषणे फ्लोटिंग आयकन किंवा बबल म्हणून दिसतात. बबल उघडण्यासाठी टॅप करा. हे हलवण्यासाठी ड्रॅग करा."</string> @@ -72,17 +84,35 @@ <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) --> - <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) --> + <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"पहा आणि आणखी बरेच काही करा"</string> + <!-- no translation found for letterbox_education_split_screen_text (449233070804658627) --> <skip /> + <string name="letterbox_education_reposition_text" msgid="4589957299813220661">"ॲपची स्थिती पुन्हा बदलण्यासाठी, त्याच्या बाहेर दोनदा टॅप करा"</string> <string name="letterbox_education_got_it" msgid="4057634570866051177">"समजले"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"अधिक माहितीसाठी विस्तार करा."</string> + <string name="letterbox_restart_dialog_title" msgid="8543049527871033505">"आणखी चांगल्या प्रकारे दिसावे यासाठी रीस्टार्ट करायचे आहे का?"</string> + <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"तुम्ही अॅप रीस्टार्ट करू शकता, जेणेकरून ते तुमच्या स्क्रीनवर आणखी चांगल्या प्रकारे दिसेल, पण तुमची प्रगती किंवा कोणतेही सेव्ह न केलेले बदल तुम्ही गमवाल"</string> + <string name="letterbox_restart_cancel" msgid="1342209132692537805">"रद्द करा"</string> + <string name="letterbox_restart_restart" msgid="8529976234412442973">"रीस्टार्ट करा"</string> + <string name="letterbox_restart_dialog_checkbox_title" msgid="5252918008140768386">"पुन्हा दाखवू नका"</string> + <string name="letterbox_reachability_reposition_text" msgid="4507890186297500893">"हे ॲप हलवण्यासाठी दोनदा टॅप करा"</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> + <string name="back_button_text" msgid="1469718707134137085">"मागे जा"</string> + <string name="handle_text" msgid="1766582106752184456">"हँडल"</string> + <string name="app_icon_text" msgid="2823268023931811747">"अॅप आयकन"</string> + <string name="fullscreen_text" msgid="1162316685217676079">"फुलस्क्रीन"</string> + <string name="desktop_text" msgid="1077633567027630454">"डेस्कटॉप मोड"</string> + <string name="split_screen_text" msgid="1396336058129570886">"स्प्लिट स्क्रीन"</string> + <string name="more_button_text" msgid="3655388105592893530">"आणखी"</string> + <string name="float_button_text" msgid="9221657008391364581">"फ्लोट"</string> + <string name="select_text" msgid="5139083974039906583">"निवडा"</string> + <string name="screenshot_text" msgid="1477704010087786671">"स्क्रीनशॉट"</string> + <string name="close_text" msgid="4986518933445178928">"बंद करा"</string> + <string name="collapse_menu_text" msgid="7515008122450342029">"मेनू बंद करा"</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..89654d0a5750 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="7930546669915337998">"नियंत्रणांसाठी "<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..32524320b1bc 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> @@ -31,11 +32,17 @@ <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Ubah saiz"</string> <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Sembunyikan"</string> <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Tunjukkan"</string> - <string name="dock_forced_resizable" msgid="1749750436092293116">"Apl mungkin tidak berfungsi dengan skrin pisah."</string> - <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Apl tidak menyokong skrin pisah."</string> + <!-- no translation found for dock_forced_resizable (7429086980048964687) --> + <skip /> + <!-- no translation found for dock_non_resizeble_failed_to_dock_text (2733543750291266047) --> + <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Apl ini hanya boleh dibuka dalam 1 tetingkap."</string> <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> + <!-- no translation found for accessibility_divider (6407584574218956849) --> + <skip /> + <!-- no translation found for divider_title (1963391955593749442) --> + <skip /> <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> @@ -46,6 +53,10 @@ <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Atas 50%"</string> <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Atas 30%"</string> <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Skrin penuh bawah"</string> + <string name="accessibility_split_left" msgid="1713683765575562458">"Pisah kiri"</string> + <string name="accessibility_split_right" msgid="8441001008181296837">"Pisah kanan"</string> + <string name="accessibility_split_top" msgid="2789329702027147146">"Pisah atas"</string> + <string name="accessibility_split_bottom" msgid="8694551025220868191">"Pisah bawah"</string> <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Menggunakan mod sebelah tangan"</string> <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Untuk keluar, leret ke atas daripada bahagian bawah skrin atau ketik pada mana-mana di bahagian atas apl"</string> <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Mulakan mod sebelah tangan"</string> @@ -61,6 +72,7 @@ <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Alihkan ke bawah sebelah kanan"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"Tetapan <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Ketepikan gelembung"</string> + <string name="bubbles_dont_bubble" msgid="3216183855437329223">"Hentikan gelembung"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Jangan jadikan perbualan dalam bentuk gelembung"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Bersembang menggunakan gelembung"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"Perbualan baharu muncul sebagai ikon terapung atau gelembung. Ketik untuk membuka gelembung. Seret untuk mengalihkan gelembung tersebut."</string> @@ -72,17 +84,35 @@ <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) --> - <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) --> + <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Lihat dan lakukan lebih"</string> + <!-- no translation found for letterbox_education_split_screen_text (449233070804658627) --> <skip /> + <string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Ketik dua kali di luar apl untuk menempatkan semula apl itu"</string> <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="letterbox_restart_dialog_title" msgid="8543049527871033505">"Mulakan semula untuk mendapatkan paparan yang lebih baik?"</string> + <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"Anda boleh memulakan semula apl supaya apl kelihatan lebih baik pada skrin anda tetapi anda mungkin akan hilang kemajuan anda atau apa-apa perubahan yang belum disimpan"</string> + <string name="letterbox_restart_cancel" msgid="1342209132692537805">"Batal"</string> + <string name="letterbox_restart_restart" msgid="8529976234412442973">"Mulakan semula"</string> + <string name="letterbox_restart_dialog_checkbox_title" msgid="5252918008140768386">"Jangan tunjukkan lagi"</string> + <string name="letterbox_reachability_reposition_text" msgid="4507890186297500893">"Ketik dua kali untuk mengalihkan apl ini"</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> + <string name="back_button_text" msgid="1469718707134137085">"Kembali"</string> + <string name="handle_text" msgid="1766582106752184456">"Pemegang"</string> + <string name="app_icon_text" msgid="2823268023931811747">"Ikon Apl"</string> + <string name="fullscreen_text" msgid="1162316685217676079">"Skrin penuh"</string> + <string name="desktop_text" msgid="1077633567027630454">"Mod Desktop"</string> + <string name="split_screen_text" msgid="1396336058129570886">"Skrin Pisah"</string> + <string name="more_button_text" msgid="3655388105592893530">"Lagi"</string> + <string name="float_button_text" msgid="9221657008391364581">"Terapung"</string> + <string name="select_text" msgid="5139083974039906583">"Pilih"</string> + <string name="screenshot_text" msgid="1477704010087786671">"Tangkapan skrin"</string> + <string name="close_text" msgid="4986518933445178928">"Tutup"</string> + <string name="collapse_menu_text" msgid="7515008122450342029">"Tutup Menu"</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..afea48d7b510 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="7930546669915337998">"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..b7b2b87b1e55 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> @@ -31,11 +32,17 @@ <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"အရွယ်အစားပြောင်းရန်"</string> <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"သိုဝှက်ရန်"</string> <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"မသိုဝှက်ရန်"</string> - <string name="dock_forced_resizable" msgid="1749750436092293116">"မျက်နှာပြင် ခွဲ၍ပြသခြင်းဖြင့် အက်ပ်သည် အလုပ်မလုပ်ပါ။"</string> - <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"အက်ပ်သည် မျက်နှာပြင်ခွဲပြရန် ပံ့ပိုးထားခြင်းမရှိပါ။"</string> + <!-- no translation found for dock_forced_resizable (7429086980048964687) --> + <skip /> + <!-- no translation found for dock_non_resizeble_failed_to_dock_text (2733543750291266047) --> + <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"ဤအက်ပ်ကို ဝင်းဒိုး ၁ ခုတွင်သာ ဖွင့်နိုင်သည်။"</string> <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> + <!-- no translation found for accessibility_divider (6407584574218956849) --> + <skip /> + <!-- no translation found for divider_title (1963391955593749442) --> + <skip /> <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> @@ -46,6 +53,10 @@ <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"အပေါ်ဘက် မျက်နှာပြင် ၅၀%"</string> <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"အပေါ်ဘက် မျက်နှာပြင် ၃၀%"</string> <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"အောက်ခြေ မျက်နှာပြင်အပြည့်"</string> + <string name="accessibility_split_left" msgid="1713683765575562458">"ဘယ်ဘက်ကို ခွဲရန်"</string> + <string name="accessibility_split_right" msgid="8441001008181296837">"ညာဘက်ကို ခွဲရန်"</string> + <string name="accessibility_split_top" msgid="2789329702027147146">"ထိပ်ပိုင်းကို ခွဲရန်"</string> + <string name="accessibility_split_bottom" msgid="8694551025220868191">"အောက်ခြေကို ခွဲရန်"</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> @@ -61,28 +72,48 @@ <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"ညာအောက်ခြေသို့ ရွှေ့ပါ"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> ဆက်တင်များ"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"ပူဖောင်းကွက် ပယ်ရန်"</string> + <string name="bubbles_dont_bubble" msgid="3216183855437329223">"ပူဖောင်းကွက် မပြပါနှင့်"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"စကားဝိုင်းကို ပူဖောင်းကွက် မပြုလုပ်ပါနှင့်"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"ပူဖောင်းကွက် သုံး၍ ချတ်လုပ်ခြင်း"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"စကားဝိုင်းအသစ်များကို မျောနေသည့် သင်္ကေတများ သို့မဟုတ် ပူဖောင်းကွက်များအဖြစ် မြင်ရပါမည်။ ပူဖောင်းကွက်ကိုဖွင့်ရန် တို့ပါ။ ရွှေ့ရန် ၎င်းကို ဖိဆွဲပါ။"</string> <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"ပူဖောင်းကွက်ကို အချိန်မရွေး ထိန်းချုပ်ရန်"</string> <string name="bubbles_user_education_manage" msgid="3460756219946517198">"ဤအက်ပ်မှနေ၍ ပူဖောင်းများကို ပိတ်ရန်အတွက် \'စီမံရန်\' ကို တို့ပါ"</string> - <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"ရပြီ"</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_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) --> - <skip /> - <!-- no translation found for letterbox_education_dialog_subtext (4853542518367719562) --> - <skip /> - <!-- no translation found for letterbox_education_screen_rotation_text (5085786687366339027) --> + <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"ကြည့်ပြီး ပိုမိုလုပ်ဆောင်ပါ"</string> + <!-- no translation found for letterbox_education_split_screen_text (449233070804658627) --> <skip /> - <!-- no translation found for letterbox_education_reposition_text (1068293354123934727) --> + <string name="letterbox_education_reposition_text" msgid="4589957299813220661">"နေရာပြန်ချရန် အက်ပ်အပြင်ဘက်ကို နှစ်ချက်တို့ပါ"</string> + <string name="letterbox_education_got_it" msgid="4057634570866051177">"နားလည်ပြီ"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"နောက်ထပ်အချက်အလက်များအတွက် ချဲ့နိုင်သည်။"</string> + <string name="letterbox_restart_dialog_title" msgid="8543049527871033505">"ပိုကောင်းသောမြင်ကွင်းအတွက် ပြန်စမလား။"</string> + <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"အက်ပ်ကို ပြန်စပါက ၎င်းသည် စခရင်ပေါ်တွင် ပိုကြည့်ကောင်းသွားသော်လည်း သင်၏ လုပ်ငန်းစဉ် (သို့) မသိမ်းရသေးသော အပြောင်းအလဲများကို ဆုံးရှုံးနိုင်သည်"</string> + <string name="letterbox_restart_cancel" msgid="1342209132692537805">"မလုပ်တော့"</string> + <string name="letterbox_restart_restart" msgid="8529976234412442973">"ပြန်စရန်"</string> + <string name="letterbox_restart_dialog_checkbox_title" msgid="5252918008140768386">"နောက်ထပ်မပြပါနှင့်"</string> + <!-- no translation found for letterbox_reachability_reposition_text (4507890186297500893) --> <skip /> - <string name="letterbox_education_got_it" msgid="4057634570866051177">"ရပြီ"</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> + <string name="back_button_text" msgid="1469718707134137085">"နောက်သို့"</string> + <string name="handle_text" msgid="1766582106752184456">"သုံးသူအမည်"</string> + <string name="app_icon_text" msgid="2823268023931811747">"အက်ပ်သင်္ကေတ"</string> + <string name="fullscreen_text" msgid="1162316685217676079">"ဖန်သားပြင်အပြည့်"</string> + <string name="desktop_text" msgid="1077633567027630454">"ဒက်စ်တော့မုဒ်"</string> + <string name="split_screen_text" msgid="1396336058129570886">"မျက်နှာပြင် ခွဲ၍ပြသရန်"</string> + <string name="more_button_text" msgid="3655388105592893530">"ပိုပြပါ"</string> + <string name="float_button_text" msgid="9221657008391364581">"မျှောရန်"</string> + <string name="select_text" msgid="5139083974039906583">"ရွေးရန်"</string> + <string name="screenshot_text" msgid="1477704010087786671">"ဖန်သားပြင်ဓာတ်ပုံ"</string> + <string name="close_text" msgid="4986518933445178928">"ပိတ်ရန်"</string> + <string name="collapse_menu_text" msgid="7515008122450342029">"မီနူး ပိတ်ရန်"</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..f3ed65da43da 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="7930546669915337998">"ထိန်းချုပ်မှုအတွက် "<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..a184e8a7c72c 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> @@ -31,11 +32,17 @@ <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Endre størrelse"</string> <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Oppbevar"</string> <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Avslutt oppbevaring"</string> - <string name="dock_forced_resizable" msgid="1749750436092293116">"Det kan hende at appen ikke fungerer med delt skjerm."</string> - <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Appen støtter ikke delt skjerm."</string> + <!-- no translation found for dock_forced_resizable (7429086980048964687) --> + <skip /> + <!-- no translation found for dock_non_resizeble_failed_to_dock_text (2733543750291266047) --> + <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Denne appen kan bare åpnes i ett vindu."</string> <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> + <!-- no translation found for accessibility_divider (6407584574218956849) --> + <skip /> + <!-- no translation found for divider_title (1963391955593749442) --> + <skip /> <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> @@ -46,6 +53,10 @@ <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Sett størrelsen på den øverste delen av skjermen til 50 %"</string> <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Sett størrelsen på den øverste delen av skjermen til 30 %"</string> <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Utvid den nederste delen av skjermen til hele skjermen"</string> + <string name="accessibility_split_left" msgid="1713683765575562458">"Del opp til venstre"</string> + <string name="accessibility_split_right" msgid="8441001008181296837">"Del opp til høyre"</string> + <string name="accessibility_split_top" msgid="2789329702027147146">"Del opp øverst"</string> + <string name="accessibility_split_bottom" msgid="8694551025220868191">"Del opp nederst"</string> <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Bruk av enhåndsmodus"</string> <string name="one_handed_tutorial_description" msgid="3486582858591353067">"For å avslutte, sveip opp fra bunnen av skjermen eller trykk hvor som helst over appen"</string> <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Start enhåndsmodus"</string> @@ -61,10 +72,11 @@ <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Flytt til nederst til høyre"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>-innstillinger"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Lukk boblen"</string> + <string name="bubbles_dont_bubble" msgid="3216183855437329223">"Ikke vis bobler"</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_manage_title" msgid="7042699946735628035">"Kontrollér bobler når som helst"</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">"Kontroller 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> <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"Ingen nylige bobler"</string> @@ -72,17 +84,36 @@ <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) --> - <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) --> + <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Se og gjør mer"</string> + <!-- no translation found for letterbox_education_split_screen_text (449233070804658627) --> <skip /> + <string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Dobbelttrykk utenfor en app for å flytte den"</string> <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="letterbox_restart_dialog_title" msgid="8543049527871033505">"Vil du starte på nytt for bedre visning?"</string> + <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"Du kan starte appen på nytt, slik at den ser bedre ut på skjermen, men du kan miste fremdrift eller ulagrede endringer"</string> + <string name="letterbox_restart_cancel" msgid="1342209132692537805">"Avbryt"</string> + <string name="letterbox_restart_restart" msgid="8529976234412442973">"Start på nytt"</string> + <string name="letterbox_restart_dialog_checkbox_title" msgid="5252918008140768386">"Ikke vis dette igjen"</string> + <!-- no translation found for letterbox_reachability_reposition_text (4507890186297500893) --> + <skip /> + <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> + <string name="back_button_text" msgid="1469718707134137085">"Tilbake"</string> + <string name="handle_text" msgid="1766582106752184456">"Håndtak"</string> + <string name="app_icon_text" msgid="2823268023931811747">"Appikon"</string> + <string name="fullscreen_text" msgid="1162316685217676079">"Fullskjerm"</string> + <string name="desktop_text" msgid="1077633567027630454">"Skrivebordmodus"</string> + <string name="split_screen_text" msgid="1396336058129570886">"Delt skjerm"</string> + <string name="more_button_text" msgid="3655388105592893530">"Mer"</string> + <string name="float_button_text" msgid="9221657008391364581">"Svevende"</string> + <string name="select_text" msgid="5139083974039906583">"Velg"</string> + <string name="screenshot_text" msgid="1477704010087786671">"Skjermdump"</string> + <string name="close_text" msgid="4986518933445178928">"Lukk"</string> + <string name="collapse_menu_text" msgid="7515008122450342029">"Lukk menyen"</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..1402e3c9c9bc 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="7930546669915337998">"Dobbelttrykk på "<annotation icon="home_icon">"HJEM"</annotation>" for å åpne kontrollene"</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..56e421f0e3b9 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> @@ -31,11 +32,17 @@ <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"आकार बदल्नुहोस्"</string> <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"स्ट्यास गर्नुहोस्"</string> <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"अनस्ट्यास गर्नुहोस्"</string> - <string name="dock_forced_resizable" msgid="1749750436092293116">"एप विभाजित स्क्रिनमा काम नगर्न सक्छ।"</string> - <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"अनुप्रयोगले विभाजित-स्क्रिनलाई समर्थन गर्दैन।"</string> + <!-- no translation found for dock_forced_resizable (7429086980048964687) --> + <skip /> + <!-- no translation found for dock_non_resizeble_failed_to_dock_text (2733543750291266047) --> + <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"यो एप एउटा विन्डोमा मात्र खोल्न मिल्छ।"</string> <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> + <!-- no translation found for accessibility_divider (6407584574218956849) --> + <skip /> + <!-- no translation found for divider_title (1963391955593749442) --> + <skip /> <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> @@ -46,6 +53,10 @@ <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"माथिल्लो भाग ५०%"</string> <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"माथिल्लो भाग ३०%"</string> <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"तल्लो भाग फुल स्क्रिन"</string> + <string name="accessibility_split_left" msgid="1713683765575562458">"बायाँतिर स्प्लिट गर्नुहोस्"</string> + <string name="accessibility_split_right" msgid="8441001008181296837">"दायाँतिर स्प्लिट गर्नुहोस्"</string> + <string name="accessibility_split_top" msgid="2789329702027147146">"सिरानतिर स्प्लिट गर्नुहोस्"</string> + <string name="accessibility_split_bottom" msgid="8694551025220868191">"पुछारतिर स्प्लिट गर्नुहोस्"</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> @@ -61,6 +72,7 @@ <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"पुछारमा दायाँतिर सार्नुहोस्"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> का सेटिङहरू"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"बबल खारेज गर्नुहोस्"</string> + <string name="bubbles_dont_bubble" msgid="3216183855437329223">"बबल नदेखाइयोस्"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"वार्तालाप बबलको रूपमा नदेखाइयोस्"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"बबलहरू प्रयोग गरी कुराकानी गर्नुहोस्"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"नयाँ वार्तालापहरू तैरने आइकन वा बबलका रूपमा देखिन्छन्। बबल खोल्न ट्याप गर्नुहोस्। बबल सार्न सो बबललाई ड्र्याग गर्नुहोस्।"</string> @@ -72,17 +84,35 @@ <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) --> - <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) --> + <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"थप कुरा हेर्नुहोस् र गर्नुहोस्"</string> + <!-- no translation found for letterbox_education_split_screen_text (449233070804658627) --> <skip /> + <string name="letterbox_education_reposition_text" msgid="4589957299813220661">"तपाईं जुन एपको स्थिति मिलाउन चाहनुहुन्छ सोही एपको बाहिर डबल ट्याप गर्नुहोस्"</string> <string name="letterbox_education_got_it" msgid="4057634570866051177">"बुझेँ"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"थप जानकारी प्राप्त गर्न चाहनुहुन्छ भने एक्स्पान्ड गर्नुहोस्।"</string> + <string name="letterbox_restart_dialog_title" msgid="8543049527871033505">"अझ राम्रोसँग देखिने बनाउन एप रिस्टार्ट गर्ने हो?"</string> + <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"यो एप तपाईंको स्क्रिनमा अझ राम्रोसँग देखियोस् भन्नाका लागि तपाईं सो एप रिस्टार्ट गर्न सक्नुहुन्छ तर तपाईंले अहिलेसम्म गरेका क्रियाकलाप वा सेभ गर्न बाँकी परिवर्तनहरू हट्न सक्छन्"</string> + <string name="letterbox_restart_cancel" msgid="1342209132692537805">"रद्द गर्नुहोस्"</string> + <string name="letterbox_restart_restart" msgid="8529976234412442973">"रिस्टार्ट गर्नुहोस्"</string> + <string name="letterbox_restart_dialog_checkbox_title" msgid="5252918008140768386">"फेरि नदेखाइयोस्"</string> + <string name="letterbox_reachability_reposition_text" msgid="4507890186297500893">"यो एप सार्न डबल ट्याप गर्नुहोस्"</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> + <string name="back_button_text" msgid="1469718707134137085">"पछाडि"</string> + <string name="handle_text" msgid="1766582106752184456">"ह्यान्डल"</string> + <string name="app_icon_text" msgid="2823268023931811747">"एपको आइकन"</string> + <string name="fullscreen_text" msgid="1162316685217676079">"फुल स्क्रिन"</string> + <string name="desktop_text" msgid="1077633567027630454">"डेस्कटप मोड"</string> + <string name="split_screen_text" msgid="1396336058129570886">"स्प्लिट स्क्रिन"</string> + <string name="more_button_text" msgid="3655388105592893530">"थप"</string> + <string name="float_button_text" msgid="9221657008391364581">"फ्लोट"</string> + <string name="select_text" msgid="5139083974039906583">"चयन गर्नुहोस्"</string> + <string name="screenshot_text" msgid="1477704010087786671">"स्क्रिनसट"</string> + <string name="close_text" msgid="4986518933445178928">"बन्द गर्नुहोस्"</string> + <string name="collapse_menu_text" msgid="7515008122450342029">"मेनु बन्द गर्नुहोस्"</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..2b1f20fba4e2 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="7930546669915337998">"कन्ट्रोल मेनु खोल्न "<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-night/colors.xml b/libs/WindowManager/Shell/res/values-night/colors.xml index 83c4d93982f4..5c6bb57a7f1c 100644 --- a/libs/WindowManager/Shell/res/values-night/colors.xml +++ b/libs/WindowManager/Shell/res/values-night/colors.xml @@ -15,6 +15,7 @@ --> <resources> + <color name="docked_divider_handle">#ffffff</color> <!-- Bubbles --> <color name="bubbles_icon_tint">@color/GM2_grey_200</color> <!-- Splash screen--> diff --git a/libs/WindowManager/Shell/res/values-nl/strings.xml b/libs/WindowManager/Shell/res/values-nl/strings.xml index 428cb3fb65d7..6347ee6df5d6 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> @@ -31,11 +32,17 @@ <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Formaat aanpassen"</string> <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Verbergen"</string> <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Niet meer verbergen"</string> - <string name="dock_forced_resizable" msgid="1749750436092293116">"De app werkt mogelijk niet met gesplitst scherm."</string> - <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"App biedt geen ondersteuning voor gesplitst scherm."</string> + <!-- no translation found for dock_forced_resizable (7429086980048964687) --> + <skip /> + <!-- no translation found for dock_non_resizeble_failed_to_dock_text (2733543750291266047) --> + <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Deze app kan slechts in 1 venster worden geopend."</string> <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> + <!-- no translation found for accessibility_divider (6407584574218956849) --> + <skip /> + <!-- no translation found for divider_title (1963391955593749442) --> + <skip /> <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> @@ -46,6 +53,10 @@ <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Bovenste scherm 50%"</string> <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Bovenste scherm 30%"</string> <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Onderste scherm op volledig scherm"</string> + <string name="accessibility_split_left" msgid="1713683765575562458">"Gesplitst scherm links"</string> + <string name="accessibility_split_right" msgid="8441001008181296837">"Gesplitst scherm rechts"</string> + <string name="accessibility_split_top" msgid="2789329702027147146">"Gesplitst scherm boven"</string> + <string name="accessibility_split_bottom" msgid="8694551025220868191">"Gesplitst scherm onder"</string> <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Bediening met 1 hand gebruiken"</string> <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Als je wilt afsluiten, swipe je omhoog vanaf de onderkant van het scherm of tik je ergens boven de app"</string> <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Bediening met 1 hand starten"</string> @@ -61,6 +72,7 @@ <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Naar rechtsonder verplaatsen"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"Instellingen voor <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Bubbel sluiten"</string> + <string name="bubbles_dont_bubble" msgid="3216183855437329223">"Niet als bubbel tonen"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Gesprekken niet in bubbels tonen"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Chatten met bubbels"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"Nieuwe gesprekken worden als zwevende iconen of bubbels getoond. Tik om een bubbel te openen. Sleep om een bubbel te verplaatsen."</string> @@ -72,17 +84,35 @@ <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) --> - <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) --> + <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Zie en doe meer"</string> + <!-- no translation found for letterbox_education_split_screen_text (449233070804658627) --> <skip /> + <string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Dubbeltik naast een app om deze opnieuw te positioneren"</string> <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="letterbox_restart_dialog_title" msgid="8543049527871033505">"Opnieuw opstarten voor een betere weergave?"</string> + <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"Je kunt de app opnieuw opstarten zodat deze er beter uitziet op je scherm, maar je kunt je voortgang of niet-opgeslagen wijzigingen kwijtraken"</string> + <string name="letterbox_restart_cancel" msgid="1342209132692537805">"Annuleren"</string> + <string name="letterbox_restart_restart" msgid="8529976234412442973">"Opnieuw opstarten"</string> + <string name="letterbox_restart_dialog_checkbox_title" msgid="5252918008140768386">"Niet opnieuw tonen"</string> + <string name="letterbox_reachability_reposition_text" msgid="4507890186297500893">"Dubbeltik om deze app te verplaatsen"</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> + <string name="back_button_text" msgid="1469718707134137085">"Terug"</string> + <string name="handle_text" msgid="1766582106752184456">"Gebruikersnaam"</string> + <string name="app_icon_text" msgid="2823268023931811747">"App-icoon"</string> + <string name="fullscreen_text" msgid="1162316685217676079">"Volledig scherm"</string> + <string name="desktop_text" msgid="1077633567027630454">"Desktopmodus"</string> + <string name="split_screen_text" msgid="1396336058129570886">"Gesplitst scherm"</string> + <string name="more_button_text" msgid="3655388105592893530">"Meer"</string> + <string name="float_button_text" msgid="9221657008391364581">"Zwevend"</string> + <string name="select_text" msgid="5139083974039906583">"Selecteren"</string> + <string name="screenshot_text" msgid="1477704010087786671">"Screenshot"</string> + <string name="close_text" msgid="4986518933445178928">"Sluiten"</string> + <string name="collapse_menu_text" msgid="7515008122450342029">"Menu 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..6766773fa866 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="7930546669915337998">"Druk 2 keer op "<annotation icon="home_icon">"HOME"</annotation>" voor bedieningsopties"</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..f302bf51c180 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> @@ -31,11 +32,17 @@ <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"ରିସାଇଜ୍ କରନ୍ତୁ"</string> <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"ଲୁଚାନ୍ତୁ"</string> <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"ଦେଖାନ୍ତୁ"</string> - <string name="dock_forced_resizable" msgid="1749750436092293116">"ସ୍ପ୍ଲିଟ୍-ସ୍କ୍ରିନରେ ଆପ୍ କାମ କରିନପାରେ।"</string> - <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"ଆପ୍ ସ୍ପ୍ଲିଟ୍-ସ୍କ୍ରୀନକୁ ସପୋର୍ଟ କରେ ନାହିଁ।"</string> + <!-- no translation found for dock_forced_resizable (7429086980048964687) --> + <skip /> + <!-- no translation found for dock_non_resizeble_failed_to_dock_text (2733543750291266047) --> + <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"ଏହି ଆପକୁ କେବଳ 1ଟି ୱିଣ୍ଡୋରେ ଖୋଲାଯାଇପାରିବ।"</string> <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> + <!-- no translation found for accessibility_divider (6407584574218956849) --> + <skip /> + <!-- no translation found for divider_title (1963391955593749442) --> + <skip /> <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> @@ -46,6 +53,10 @@ <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"ଉପର ଆଡ଼କୁ 50% କରନ୍ତୁ"</string> <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"ଉପର ଆଡ଼କୁ 30% କରନ୍ତୁ"</string> <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"ତଳ ଅଂଶର ପୂର୍ଣ୍ଣ ସ୍କ୍ରୀନ୍"</string> + <string name="accessibility_split_left" msgid="1713683765575562458">"ବାମପଟକୁ ସ୍ପ୍ଲିଟ କରନ୍ତୁ"</string> + <string name="accessibility_split_right" msgid="8441001008181296837">"ଡାହାଣପଟକୁ ସ୍ପ୍ଲିଟ କରନ୍ତୁ"</string> + <string name="accessibility_split_top" msgid="2789329702027147146">"ଶୀର୍ଷକୁ ସ୍ପ୍ଲିଟ କରନ୍ତୁ"</string> + <string name="accessibility_split_bottom" msgid="8694551025220868191">"ନିମ୍ନକୁ ସ୍ଲିଟ କରନ୍ତୁ"</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> @@ -61,6 +72,7 @@ <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"ତଳ ଡାହାଣକୁ ନିଅନ୍ତୁ"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> ସେଟିଂସ୍"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"ବବଲ୍ ଖାରଜ କରନ୍ତୁ"</string> + <string name="bubbles_dont_bubble" msgid="3216183855437329223">"ବବଲ କରନ୍ତୁ ନାହିଁ"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"ବାର୍ତ୍ତାଳାପକୁ ବବଲ୍ କରନ୍ତୁ ନାହିଁ"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"ବବଲଗୁଡ଼ିକୁ ବ୍ୟବହାର କରି ଚାଟ୍ କରନ୍ତୁ"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"ନୂଆ ବାର୍ତ୍ତାଳାପଗୁଡ଼ିକ ଫ୍ଲୋଟିଂ ଆଇକନ୍ କିମ୍ବା ବବଲ୍ ଭାବେ ଦେଖାଯିବ। ବବଲ୍ ଖୋଲିବାକୁ ଟାପ୍ କରନ୍ତୁ। ଏହାକୁ ମୁଭ୍ କରିବାକୁ ଟାଣନ୍ତୁ।"</string> @@ -72,17 +84,36 @@ <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) --> - <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) --> + <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"ଦେଖନ୍ତୁ ଏବଂ ଆହୁରି ଅନେକ କିଛି କରନ୍ତୁ"</string> + <!-- no translation found for letterbox_education_split_screen_text (449233070804658627) --> <skip /> + <string name="letterbox_education_reposition_text" msgid="4589957299813220661">"ଏକ ଆପକୁ ରିପୋଜିସନ କରିବା ପାଇଁ ଏହାର ବାହାରେ ଦୁଇଥର-ଟାପ କରନ୍ତୁ"</string> <string name="letterbox_education_got_it" msgid="4057634570866051177">"ବୁଝିଗଲି"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"ଅଧିକ ସୂଚନା ପାଇଁ ବିସ୍ତାର କରନ୍ତୁ।"</string> + <string name="letterbox_restart_dialog_title" msgid="8543049527871033505">"ଏକ ଭଲ ଭ୍ୟୁ ପାଇଁ ରିଷ୍ଟାର୍ଟ କରିବେ?"</string> + <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"ଆପଣ ଆପକୁ ରିଷ୍ଟାର୍ଟ କରିପାରିବେ ଯାହା ଫଳରେ ଏହା ଆପଣଙ୍କ ସ୍କ୍ରିନରେ ଆହୁରି ଭଲ ଦେଖାଯିବ, କିନ୍ତୁ ଆପଣ ଆପଣଙ୍କ ପ୍ରଗତି କିମ୍ବା ସେଭ ହୋଇନଥିବା ଯେ କୌଣସି ପରିବର୍ତ୍ତନ ହରାଇପାରନ୍ତି"</string> + <string name="letterbox_restart_cancel" msgid="1342209132692537805">"ବାତିଲ କରନ୍ତୁ"</string> + <string name="letterbox_restart_restart" msgid="8529976234412442973">"ରିଷ୍ଟାର୍ଟ କରନ୍ତୁ"</string> + <string name="letterbox_restart_dialog_checkbox_title" msgid="5252918008140768386">"ପୁଣି ଦେଖାନ୍ତୁ ନାହିଁ"</string> + <!-- no translation found for letterbox_reachability_reposition_text (4507890186297500893) --> + <skip /> + <string name="maximize_button_text" msgid="1650859196290301963">"ବଡ଼ କରନ୍ତୁ"</string> + <string name="minimize_button_text" msgid="271592547935841753">"ଛୋଟ କରନ୍ତୁ"</string> + <string name="close_button_text" msgid="2913281996024033299">"ବନ୍ଦ କରନ୍ତୁ"</string> + <string name="back_button_text" msgid="1469718707134137085">"ପଛକୁ ଫେରନ୍ତୁ"</string> + <string name="handle_text" msgid="1766582106752184456">"ହେଣ୍ଡେଲ"</string> + <string name="app_icon_text" msgid="2823268023931811747">"ଆପ ଆଇକନ"</string> + <string name="fullscreen_text" msgid="1162316685217676079">"ପୂର୍ଣ୍ଣସ୍କ୍ରିନ"</string> + <string name="desktop_text" msgid="1077633567027630454">"ଡେସ୍କଟପ ମୋଡ"</string> + <string name="split_screen_text" msgid="1396336058129570886">"ସ୍ପ୍ଲିଟ ସ୍କ୍ରିନ"</string> + <string name="more_button_text" msgid="3655388105592893530">"ଅଧିକ"</string> + <string name="float_button_text" msgid="9221657008391364581">"ଫ୍ଲୋଟ"</string> + <string name="select_text" msgid="5139083974039906583">"ଚୟନ କରନ୍ତୁ"</string> + <string name="screenshot_text" msgid="1477704010087786671">"ସ୍କ୍ରିନସଟ"</string> + <string name="close_text" msgid="4986518933445178928">"ବନ୍ଦ କରନ୍ତୁ"</string> + <string name="collapse_menu_text" msgid="7515008122450342029">"ମେନୁ ବନ୍ଦ କରନ୍ତୁ"</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..1e81f4d80f32 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="7930546669915337998">"ନିୟନ୍ତ୍ରଣଗୁଡ଼ିକ ପାଇଁ "<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..eb32e25c7d08 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> @@ -31,11 +32,17 @@ <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"ਆਕਾਰ ਬਦਲੋ"</string> <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"ਸਟੈਸ਼"</string> <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"ਅਣਸਟੈਸ਼"</string> - <string name="dock_forced_resizable" msgid="1749750436092293116">"ਹੋ ਸਕਦਾ ਹੈ ਕਿ ਐਪ ਸਪਲਿਟ-ਸਕ੍ਰੀਨ ਨਾਲ ਕੰਮ ਨਾ ਕਰੇ।"</string> - <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"ਐਪ ਸਪਲਿਟ-ਸਕ੍ਰੀਨ ਨੂੰ ਸਮਰਥਨ ਨਹੀਂ ਕਰਦੀ।"</string> + <!-- no translation found for dock_forced_resizable (7429086980048964687) --> + <skip /> + <!-- no translation found for dock_non_resizeble_failed_to_dock_text (2733543750291266047) --> + <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"ਇਹ ਐਪ ਸਿਰਫ਼ 1 ਵਿੰਡੋ ਵਿੱਚ ਖੋਲ੍ਹੀ ਜਾ ਸਕਦੀ ਹੈ।"</string> <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> + <!-- no translation found for accessibility_divider (6407584574218956849) --> + <skip /> + <!-- no translation found for divider_title (1963391955593749442) --> + <skip /> <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> @@ -46,6 +53,10 @@ <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"ਉੱਪਰ 50%"</string> <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"ਉੱਪਰ 30%"</string> <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"ਹੇਠਾਂ ਪੂਰੀ ਸਕ੍ਰੀਨ"</string> + <string name="accessibility_split_left" msgid="1713683765575562458">"ਖੱਬੇ ਪਾਸੇ ਵੰਡੋ"</string> + <string name="accessibility_split_right" msgid="8441001008181296837">"ਸੱਜੇ ਪਾਸੇ ਵੰਡੋ"</string> + <string name="accessibility_split_top" msgid="2789329702027147146">"ਸਿਖਰ \'ਤੇ ਵੰਡੋ"</string> + <string name="accessibility_split_bottom" msgid="8694551025220868191">"ਹੇਠਾਂ ਵੰਡੋ"</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> @@ -61,6 +72,7 @@ <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"ਹੇਠਾਂ ਵੱਲ ਸੱਜੇ ਲਿਜਾਓ"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> ਸੈਟਿੰਗਾਂ"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"ਬਬਲ ਨੂੰ ਖਾਰਜ ਕਰੋ"</string> + <string name="bubbles_dont_bubble" msgid="3216183855437329223">"ਬਬਲ ਨਾ ਕਰੋ"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"ਗੱਲਬਾਤ \'ਤੇ ਬਬਲ ਨਾ ਲਾਓ"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"ਬਬਲ ਵਰਤਦੇ ਹੋਏ ਚੈਟ ਕਰੋ"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"ਨਵੀਆਂ ਗੱਲਾਂਬਾਤਾਂ ਫਲੋਟਿੰਗ ਪ੍ਰਤੀਕਾਂ ਜਾਂ ਬਬਲ ਦੇ ਰੂਪ ਵਿੱਚ ਦਿਸਦੀਆਂ ਹਨ। ਬਬਲ ਨੂੰ ਖੋਲ੍ਹਣ ਲਈ ਟੈਪ ਕਰੋ। ਇਸਨੂੰ ਲਿਜਾਣ ਲਈ ਘਸੀਟੋ।"</string> @@ -72,17 +84,36 @@ <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) --> - <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) --> + <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"ਦੇਖੋ ਅਤੇ ਹੋਰ ਬਹੁਤ ਕੁਝ ਕਰੋ"</string> + <!-- no translation found for letterbox_education_split_screen_text (449233070804658627) --> <skip /> + <string name="letterbox_education_reposition_text" msgid="4589957299813220661">"ਕਿਸੇ ਐਪ ਦੀ ਜਗ੍ਹਾ ਬਦਲਣ ਲਈ ਉਸ ਦੇ ਬਾਹਰ ਡਬਲ ਟੈਪ ਕਰੋ"</string> <string name="letterbox_education_got_it" msgid="4057634570866051177">"ਸਮਝ ਲਿਆ"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"ਹੋਰ ਜਾਣਕਾਰੀ ਲਈ ਵਿਸਤਾਰ ਕਰੋ।"</string> + <string name="letterbox_restart_dialog_title" msgid="8543049527871033505">"ਕੀ ਬਿਹਤਰ ਦ੍ਰਿਸ਼ ਲਈ ਐਪ ਨੂੰ ਮੁੜ-ਸ਼ੁਰੂ ਕਰਨਾ ਹੈ?"</string> + <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"ਤੁਸੀਂ ਐਪ ਨੂੰ ਮੁੜ-ਸ਼ੁਰੂ ਕਰ ਸਕਦੇ ਹੋ ਤਾਂ ਜੋ ਇਹ ਤੁਹਾਡੀ ਸਕ੍ਰੀਨ \'ਤੇ ਬਿਹਤਰ ਦਿਸੇ, ਪਰ ਤੁਸੀਂ ਆਪਣੀ ਪ੍ਰਗਤੀ ਜਾਂ ਕਿਸੇ ਅਣਰੱਖਿਅਤ ਤਬਦੀਲੀ ਨੂੰ ਗੁਆ ਸਕਦੇ ਹੋ"</string> + <string name="letterbox_restart_cancel" msgid="1342209132692537805">"ਰੱਦ ਕਰੋ"</string> + <string name="letterbox_restart_restart" msgid="8529976234412442973">"ਮੁੜ-ਸ਼ੁਰੂ ਕਰੋ"</string> + <string name="letterbox_restart_dialog_checkbox_title" msgid="5252918008140768386">"ਦੁਬਾਰਾ ਨਾ ਦਿਖਾਓ"</string> + <!-- no translation found for letterbox_reachability_reposition_text (4507890186297500893) --> + <skip /> + <string name="maximize_button_text" msgid="1650859196290301963">"ਵੱਡਾ ਕਰੋ"</string> + <string name="minimize_button_text" msgid="271592547935841753">"ਛੋਟਾ ਕਰੋ"</string> + <string name="close_button_text" msgid="2913281996024033299">"ਬੰਦ ਕਰੋ"</string> + <string name="back_button_text" msgid="1469718707134137085">"ਪਿੱਛੇ"</string> + <string name="handle_text" msgid="1766582106752184456">"ਹੈਂਡਲ"</string> + <string name="app_icon_text" msgid="2823268023931811747">"ਐਪ ਪ੍ਰਤੀਕ"</string> + <string name="fullscreen_text" msgid="1162316685217676079">"ਪੂਰੀ-ਸਕ੍ਰੀਨ"</string> + <string name="desktop_text" msgid="1077633567027630454">"ਡੈਸਕਟਾਪ ਮੋਡ"</string> + <string name="split_screen_text" msgid="1396336058129570886">"ਸਪਲਿਟ ਸਕ੍ਰੀਨ"</string> + <string name="more_button_text" msgid="3655388105592893530">"ਹੋਰ"</string> + <string name="float_button_text" msgid="9221657008391364581">"ਫ਼ਲੋਟ"</string> + <string name="select_text" msgid="5139083974039906583">"ਚੁਣੋ"</string> + <string name="screenshot_text" msgid="1477704010087786671">"ਸਕ੍ਰੀਨਸ਼ਾਟ"</string> + <string name="close_text" msgid="4986518933445178928">"ਬੰਦ ਕਰੋ"</string> + <string name="collapse_menu_text" msgid="7515008122450342029">"ਮੀਨੂ ਬੰਦ ਕਰੋ"</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..758aafad457a 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="7930546669915337998">"ਕੰਟਰੋਲਾਂ ਲਈ "<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..d61cbf549a10 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> @@ -31,11 +32,17 @@ <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Zmień rozmiar"</string> <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Przenieś do schowka"</string> <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Zabierz ze schowka"</string> - <string name="dock_forced_resizable" msgid="1749750436092293116">"Aplikacja może nie działać przy podzielonym ekranie."</string> - <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Aplikacja nie obsługuje dzielonego ekranu."</string> + <!-- no translation found for dock_forced_resizable (7429086980048964687) --> + <skip /> + <!-- no translation found for dock_non_resizeble_failed_to_dock_text (2733543750291266047) --> + <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Ta aplikacja może być otwarta tylko w 1 oknie."</string> <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> + <!-- no translation found for accessibility_divider (6407584574218956849) --> + <skip /> + <!-- no translation found for divider_title (1963391955593749442) --> + <skip /> <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> @@ -46,6 +53,10 @@ <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"50% górnej części ekranu"</string> <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"30% górnej części ekranu"</string> <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Dolna część ekranu na pełnym ekranie"</string> + <string name="accessibility_split_left" msgid="1713683765575562458">"Podziel po lewej"</string> + <string name="accessibility_split_right" msgid="8441001008181296837">"Podziel po prawej"</string> + <string name="accessibility_split_top" msgid="2789329702027147146">"Podziel u góry"</string> + <string name="accessibility_split_bottom" msgid="8694551025220868191">"Podziel u dołu"</string> <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Korzystanie z trybu jednej ręki"</string> <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Aby zamknąć, przesuń palcem z dołu ekranu w górę lub kliknij dowolne miejsce nad aplikacją"</string> <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Uruchom tryb jednej ręki"</string> @@ -61,6 +72,7 @@ <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Przenieś w prawy dolny róg"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> – ustawienia"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Zamknij dymek"</string> + <string name="bubbles_dont_bubble" msgid="3216183855437329223">"Nie twórz dymków"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Nie wyświetlaj rozmowy jako dymka"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Czatuj, korzystając z dymków"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"Nowe rozmowy będą wyświetlane jako pływające ikony lub dymki. Kliknij, by otworzyć dymek. Przeciągnij, by go przenieść."</string> @@ -72,17 +84,35 @@ <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) --> - <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) --> + <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Zobacz i zrób więcej"</string> + <!-- no translation found for letterbox_education_split_screen_text (449233070804658627) --> <skip /> + <string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Kliknij dwukrotnie poza aplikacją, aby ją przenieść"</string> <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="letterbox_restart_dialog_title" msgid="8543049527871033505">"Uruchomić ponownie dla lepszego wyglądu?"</string> + <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"Możesz ponownie uruchomić aplikację, aby lepiej wyglądała na ekranie, ale istnieje ryzyko, że utracisz postępy i niezapisane zmiany"</string> + <string name="letterbox_restart_cancel" msgid="1342209132692537805">"Anuluj"</string> + <string name="letterbox_restart_restart" msgid="8529976234412442973">"Uruchom ponownie"</string> + <string name="letterbox_restart_dialog_checkbox_title" msgid="5252918008140768386">"Nie pokazuj ponownie"</string> + <string name="letterbox_reachability_reposition_text" msgid="4507890186297500893">"Kliknij dwukrotnie, aby przenieść tę aplikację"</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> + <string name="back_button_text" msgid="1469718707134137085">"Wstecz"</string> + <string name="handle_text" msgid="1766582106752184456">"Uchwyt"</string> + <string name="app_icon_text" msgid="2823268023931811747">"Ikona aplikacji"</string> + <string name="fullscreen_text" msgid="1162316685217676079">"Pełny ekran"</string> + <string name="desktop_text" msgid="1077633567027630454">"Tryb pulpitu"</string> + <string name="split_screen_text" msgid="1396336058129570886">"Podzielony ekran"</string> + <string name="more_button_text" msgid="3655388105592893530">"Więcej"</string> + <string name="float_button_text" msgid="9221657008391364581">"Pływające"</string> + <string name="select_text" msgid="5139083974039906583">"Wybierz"</string> + <string name="screenshot_text" msgid="1477704010087786671">"Zrzut ekranu"</string> + <string name="close_text" msgid="4986518933445178928">"Zamknij"</string> + <string name="collapse_menu_text" msgid="7515008122450342029">"Zamknij menu"</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..b598351b5127 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="7930546669915337998">"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..c431100b2901 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> @@ -31,11 +32,17 @@ <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Redimensionar"</string> <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Ocultar"</string> <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Exibir"</string> - <string name="dock_forced_resizable" msgid="1749750436092293116">"É possível que o app não funcione com a tela dividida."</string> - <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"O app não é compatível com a divisão de tela."</string> + <!-- no translation found for dock_forced_resizable (7429086980048964687) --> + <skip /> + <!-- no translation found for dock_non_resizeble_failed_to_dock_text (2733543750291266047) --> + <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Este app só pode ser aberto em uma única janela."</string> <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> + <!-- no translation found for accessibility_divider (6407584574218956849) --> + <skip /> + <!-- no translation found for divider_title (1963391955593749442) --> + <skip /> <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> @@ -46,6 +53,10 @@ <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Parte superior a 50%"</string> <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Parte superior a 30%"</string> <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Parte inferior em tela cheia"</string> + <string name="accessibility_split_left" msgid="1713683765575562458">"Dividir para a esquerda"</string> + <string name="accessibility_split_right" msgid="8441001008181296837">"Dividir para a direita"</string> + <string name="accessibility_split_top" msgid="2789329702027147146">"Dividir para cima"</string> + <string name="accessibility_split_bottom" msgid="8694551025220868191">"Dividir para baixo"</string> <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Como usar o modo para uma mão"</string> <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Para sair, deslize de baixo para cima na tela ou toque em qualquer lugar acima do app"</string> <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Iniciar o modo para uma mão"</string> @@ -61,6 +72,7 @@ <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Mover para canto inferior direito"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"Configurações de <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Dispensar balão"</string> + <string name="bubbles_dont_bubble" msgid="3216183855437329223">"Parar de mostrar balões"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Não criar balões de conversa"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Converse usando balões"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"Novas conversas aparecerão como ícones flutuantes, ou balões. Toque para abrir o balão. Arraste para movê-lo."</string> @@ -72,17 +84,35 @@ <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) --> - <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) --> + <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Veja e faça mais"</string> + <!-- no translation found for letterbox_education_split_screen_text (449233070804658627) --> <skip /> + <string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Toque duas vezes fora de um app para reposicionar"</string> <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="letterbox_restart_dialog_title" msgid="8543049527871033505">"Reiniciar para melhorar a visualização?"</string> + <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"Você pode reiniciar o app para melhorar a visualização dele, mas talvez perca seu progresso ou mudanças não salvas"</string> + <string name="letterbox_restart_cancel" msgid="1342209132692537805">"Cancelar"</string> + <string name="letterbox_restart_restart" msgid="8529976234412442973">"Reiniciar"</string> + <string name="letterbox_restart_dialog_checkbox_title" msgid="5252918008140768386">"Não mostrar novamente"</string> + <string name="letterbox_reachability_reposition_text" msgid="4507890186297500893">"Toque duas vezes para mover o app"</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> + <string name="back_button_text" msgid="1469718707134137085">"Voltar"</string> + <string name="handle_text" msgid="1766582106752184456">"Alça"</string> + <string name="app_icon_text" msgid="2823268023931811747">"Ícone do app"</string> + <string name="fullscreen_text" msgid="1162316685217676079">"Tela cheia"</string> + <string name="desktop_text" msgid="1077633567027630454">"Modo área de trabalho"</string> + <string name="split_screen_text" msgid="1396336058129570886">"Tela dividida"</string> + <string name="more_button_text" msgid="3655388105592893530">"Mais"</string> + <string name="float_button_text" msgid="9221657008391364581">"Ponto flutuante"</string> + <string name="select_text" msgid="5139083974039906583">"Selecionar"</string> + <string name="screenshot_text" msgid="1477704010087786671">"Captura de tela"</string> + <string name="close_text" msgid="4986518933445178928">"Fechar"</string> + <string name="collapse_menu_text" msgid="7515008122450342029">"Fechar menu"</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..2528ea9b8ebb 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="7930546669915337998">"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..a8dbb808c073 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> @@ -31,11 +32,17 @@ <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Redimensionar"</string> <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Armazenar"</string> <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Remover do armazenamento"</string> - <string name="dock_forced_resizable" msgid="1749750436092293116">"A app pode não funcionar com o ecrã dividido."</string> - <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"A app não é compatível com o ecrã dividido."</string> + <!-- no translation found for dock_forced_resizable (7429086980048964687) --> + <skip /> + <!-- no translation found for dock_non_resizeble_failed_to_dock_text (2733543750291266047) --> + <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Esta app só pode ser aberta em 1 janela."</string> <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> + <!-- no translation found for accessibility_divider (6407584574218956849) --> + <skip /> + <!-- no translation found for divider_title (1963391955593749442) --> + <skip /> <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> @@ -46,6 +53,10 @@ <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"50% no ecrã superior"</string> <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"30% no ecrã superior"</string> <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Ecrã inferior inteiro"</string> + <string name="accessibility_split_left" msgid="1713683765575562458">"Divisão à esquerda"</string> + <string name="accessibility_split_right" msgid="8441001008181296837">"Divisão à direita"</string> + <string name="accessibility_split_top" msgid="2789329702027147146">"Divisão na parte superior"</string> + <string name="accessibility_split_bottom" msgid="8694551025220868191">"Divisão na parte inferior"</string> <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Utilize o modo para uma mão"</string> <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Para sair, deslize rapidamente para cima a partir da parte inferior do ecrã ou toque em qualquer ponto acima da app."</string> <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Iniciar o modo para uma mão"</string> @@ -61,6 +72,7 @@ <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Mover parte inferior direita"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"Definições de <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Ignorar balão"</string> + <string name="bubbles_dont_bubble" msgid="3216183855437329223">"Não apresentar balões"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Não apresentar a conversa em balões"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Converse no chat através de balões"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"As novas conversas aparecem como ícones flutuantes ou balões. Toque para abrir o balão. Arraste para o mover."</string> @@ -72,17 +84,35 @@ <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) --> - <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) --> + <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Veja e faça mais"</string> + <!-- no translation found for letterbox_education_split_screen_text (449233070804658627) --> <skip /> + <string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Toque duas vezes fora de uma app para a reposicionar"</string> <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="letterbox_restart_dialog_title" msgid="8543049527871033505">"Reiniciar para uma melhor visualização?"</string> + <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"Pode reiniciar a app para ficar com um melhor aspeto no seu ecrã, mas pode perder o seu progresso ou eventuais alterações não guardadas"</string> + <string name="letterbox_restart_cancel" msgid="1342209132692537805">"Cancelar"</string> + <string name="letterbox_restart_restart" msgid="8529976234412442973">"Reiniciar"</string> + <string name="letterbox_restart_dialog_checkbox_title" msgid="5252918008140768386">"Não mostrar de novo"</string> + <string name="letterbox_reachability_reposition_text" msgid="4507890186297500893">"Toque duas vezes para mover esta app"</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> + <string name="back_button_text" msgid="1469718707134137085">"Anterior"</string> + <string name="handle_text" msgid="1766582106752184456">"Indicador"</string> + <string name="app_icon_text" msgid="2823268023931811747">"Ícone da app"</string> + <string name="fullscreen_text" msgid="1162316685217676079">"Ecrã inteiro"</string> + <string name="desktop_text" msgid="1077633567027630454">"Modo de ambiente de trabalho"</string> + <string name="split_screen_text" msgid="1396336058129570886">"Ecrã dividido"</string> + <string name="more_button_text" msgid="3655388105592893530">"Mais"</string> + <string name="float_button_text" msgid="9221657008391364581">"Flutuar"</string> + <string name="select_text" msgid="5139083974039906583">"Selecionar"</string> + <string name="screenshot_text" msgid="1477704010087786671">"Captura de ecrã"</string> + <string name="close_text" msgid="4986518933445178928">"Fechar"</string> + <string name="collapse_menu_text" msgid="7515008122450342029">"Fechar menu"</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..a678f581c272 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="7930546669915337998">"Prima duas vezes "<annotation icon="home_icon">"PÁGINA INICIAL"</annotation>" para os 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..c431100b2901 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> @@ -31,11 +32,17 @@ <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Redimensionar"</string> <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Ocultar"</string> <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Exibir"</string> - <string name="dock_forced_resizable" msgid="1749750436092293116">"É possível que o app não funcione com a tela dividida."</string> - <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"O app não é compatível com a divisão de tela."</string> + <!-- no translation found for dock_forced_resizable (7429086980048964687) --> + <skip /> + <!-- no translation found for dock_non_resizeble_failed_to_dock_text (2733543750291266047) --> + <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Este app só pode ser aberto em uma única janela."</string> <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> + <!-- no translation found for accessibility_divider (6407584574218956849) --> + <skip /> + <!-- no translation found for divider_title (1963391955593749442) --> + <skip /> <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> @@ -46,6 +53,10 @@ <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Parte superior a 50%"</string> <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Parte superior a 30%"</string> <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Parte inferior em tela cheia"</string> + <string name="accessibility_split_left" msgid="1713683765575562458">"Dividir para a esquerda"</string> + <string name="accessibility_split_right" msgid="8441001008181296837">"Dividir para a direita"</string> + <string name="accessibility_split_top" msgid="2789329702027147146">"Dividir para cima"</string> + <string name="accessibility_split_bottom" msgid="8694551025220868191">"Dividir para baixo"</string> <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Como usar o modo para uma mão"</string> <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Para sair, deslize de baixo para cima na tela ou toque em qualquer lugar acima do app"</string> <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Iniciar o modo para uma mão"</string> @@ -61,6 +72,7 @@ <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Mover para canto inferior direito"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"Configurações de <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Dispensar balão"</string> + <string name="bubbles_dont_bubble" msgid="3216183855437329223">"Parar de mostrar balões"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Não criar balões de conversa"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Converse usando balões"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"Novas conversas aparecerão como ícones flutuantes, ou balões. Toque para abrir o balão. Arraste para movê-lo."</string> @@ -72,17 +84,35 @@ <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) --> - <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) --> + <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Veja e faça mais"</string> + <!-- no translation found for letterbox_education_split_screen_text (449233070804658627) --> <skip /> + <string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Toque duas vezes fora de um app para reposicionar"</string> <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="letterbox_restart_dialog_title" msgid="8543049527871033505">"Reiniciar para melhorar a visualização?"</string> + <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"Você pode reiniciar o app para melhorar a visualização dele, mas talvez perca seu progresso ou mudanças não salvas"</string> + <string name="letterbox_restart_cancel" msgid="1342209132692537805">"Cancelar"</string> + <string name="letterbox_restart_restart" msgid="8529976234412442973">"Reiniciar"</string> + <string name="letterbox_restart_dialog_checkbox_title" msgid="5252918008140768386">"Não mostrar novamente"</string> + <string name="letterbox_reachability_reposition_text" msgid="4507890186297500893">"Toque duas vezes para mover o app"</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> + <string name="back_button_text" msgid="1469718707134137085">"Voltar"</string> + <string name="handle_text" msgid="1766582106752184456">"Alça"</string> + <string name="app_icon_text" msgid="2823268023931811747">"Ícone do app"</string> + <string name="fullscreen_text" msgid="1162316685217676079">"Tela cheia"</string> + <string name="desktop_text" msgid="1077633567027630454">"Modo área de trabalho"</string> + <string name="split_screen_text" msgid="1396336058129570886">"Tela dividida"</string> + <string name="more_button_text" msgid="3655388105592893530">"Mais"</string> + <string name="float_button_text" msgid="9221657008391364581">"Ponto flutuante"</string> + <string name="select_text" msgid="5139083974039906583">"Selecionar"</string> + <string name="screenshot_text" msgid="1477704010087786671">"Captura de tela"</string> + <string name="close_text" msgid="4986518933445178928">"Fechar"</string> + <string name="collapse_menu_text" msgid="7515008122450342029">"Fechar menu"</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..2528ea9b8ebb 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="7930546669915337998">"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..15682687459b 100644 --- a/libs/WindowManager/Shell/res/values-ro/strings.xml +++ b/libs/WindowManager/Shell/res/values-ro/strings.xml @@ -17,25 +17,32 @@ <resources xmlns:android="http://schemas.android.com/apk/res/android" xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> - <string name="pip_phone_close" msgid="5783752637260411309">"Închideți"</string> - <string name="pip_phone_expand" msgid="2579292903468287504">"Extindeți"</string> + <string name="pip_phone_close" msgid="5783752637260411309">"Închide"</string> + <string name="pip_phone_expand" msgid="2579292903468287504">"Extinde"</string> <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_phone_enter_split" msgid="7042877263880641911">"Accesează 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> - <string name="pip_pause" msgid="690688849510295232">"Întrerupeți"</string> - <string name="pip_skip_to_next" msgid="8403429188794867653">"Treceți la următorul"</string> - <string name="pip_skip_to_prev" msgid="7172158111196394092">"Treceți la cel anterior"</string> - <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Redimensionați"</string> - <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Stocați"</string> - <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Anulați stocarea"</string> - <string name="dock_forced_resizable" msgid="1749750436092293116">"Este posibil ca aplicația să nu funcționeze cu ecranul împărțit."</string> - <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Aplicația nu acceptă ecranul împărțit."</string> + <string name="pip_notification_message" msgid="8854051911700302620">"Dacă nu vrei ca <xliff:g id="NAME">%s</xliff:g> să folosească această funcție, atinge pentru a deschide setările și dezactiveaz-o."</string> + <string name="pip_play" msgid="3496151081459417097">"Redă"</string> + <string name="pip_pause" msgid="690688849510295232">"Întrerupe"</string> + <string name="pip_skip_to_next" msgid="8403429188794867653">"Treci la următorul"</string> + <string name="pip_skip_to_prev" msgid="7172158111196394092">"Treci la cel anterior"</string> + <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Redimensionează"</string> + <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Stochează"</string> + <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Anulează stocarea"</string> + <!-- no translation found for dock_forced_resizable (7429086980048964687) --> + <skip /> + <!-- no translation found for dock_non_resizeble_failed_to_dock_text (2733543750291266047) --> + <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Aplicația poate fi deschisă într-o singură fereastră."</string> <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> + <!-- no translation found for accessibility_divider (6407584574218956849) --> + <skip /> + <!-- no translation found for divider_title (1963391955593749442) --> + <skip /> <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> @@ -46,43 +53,67 @@ <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Partea de sus: 50%"</string> <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Partea de sus: 30%"</string> <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Partea de jos pe ecran complet"</string> + <string name="accessibility_split_left" msgid="1713683765575562458">"Împarte în stânga"</string> + <string name="accessibility_split_right" msgid="8441001008181296837">"Împarte în dreapta"</string> + <string name="accessibility_split_top" msgid="2789329702027147146">"Împarte în sus"</string> + <string name="accessibility_split_bottom" msgid="8694551025220868191">"Împarte în jos"</string> <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Folosirea modului cu o mână"</string> - <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Pentru a ieși, glisați în sus din partea de jos a ecranului sau atingeți oriunde deasupra ferestrei aplicației"</string> - <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Activați modul cu o mână"</string> - <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"Părăsiți modul cu o mână"</string> + <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Pentru a ieși, glisează în sus din partea de jos a ecranului sau atinge oriunde deasupra ferestrei aplicației"</string> + <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Activează modul cu o mână"</string> + <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"Ieși din modul cu o mână"</string> <string name="bubbles_settings_button_description" msgid="1301286017420516912">"Setări pentru baloanele <xliff:g id="APP_NAME">%1$s</xliff:g>"</string> <string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"Suplimentar"</string> - <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"Adăugați înapoi în stivă"</string> + <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"Adaugă înapoi în stivă"</string> <string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> de la <xliff:g id="APP_NAME">%2$s</xliff:g>"</string> <string name="bubble_content_description_stack" msgid="8071515017164630429">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> de la <xliff:g id="APP_NAME">%2$s</xliff:g> și încă <xliff:g id="BUBBLE_COUNT">%3$d</xliff:g>"</string> - <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"Mutați în stânga sus"</string> - <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Mutați în dreapta sus"</string> - <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Mutați în stânga jos"</string> - <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Mutați în dreapta jos"</string> + <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"Mută în stânga sus"</string> + <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Mută în dreapta sus"</string> + <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Mută în stânga jos"</string> + <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Mută în dreapta jos"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"Setări <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> - <string name="bubble_dismiss_text" msgid="8816558050659478158">"Închideți balonul"</string> - <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Nu afișați conversația în balon"</string> + <string name="bubble_dismiss_text" msgid="8816558050659478158">"Închide balonul"</string> + <string name="bubbles_dont_bubble" msgid="3216183855437329223">"Nu afișa bule"</string> + <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Nu afișa conversația în balon"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Chat cu baloane"</string> - <string name="bubbles_user_education_description" msgid="4215862563054175407">"Conversațiile noi apar ca pictograme flotante sau baloane. Atingeți pentru a deschide balonul. Trageți pentru a-l muta."</string> - <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"Controlați oricând baloanele"</string> - <string name="bubbles_user_education_manage" msgid="3460756219946517198">"Atingeți Gestionați pentru a dezactiva baloanele din această aplicație"</string> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"Conversațiile noi apar ca pictograme flotante sau baloane. Atinge pentru a deschide balonul. Trage pentru a-l muta."</string> + <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"Controlează oricând baloanele"</string> + <string name="bubbles_user_education_manage" msgid="3460756219946517198">"Atinge Gestionează pentru a dezactiva baloanele din această aplicație"</string> <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"OK"</string> <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"Nu există baloane recente"</string> <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"Baloanele recente și baloanele respinse vor apărea aici"</string> <string name="notification_bubble_title" msgid="6082910224488253378">"Balon"</string> - <string name="manage_bubbles_text" msgid="7730624269650594419">"Gestionați"</string> + <string name="manage_bubbles_text" msgid="7730624269650594419">"Gestionează"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Balonul a fost respins."</string> - <string name="restart_button_description" msgid="5887656107651190519">"Atingeți ca să reporniți aplicația și să treceți în modul ecran complet."</string> - <string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"Aveți probleme cu camera foto?\nAtingeți pentru a reîncadra"</string> - <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Nu ați remediat problema?\nAtingeți pentru a reveni"</string> - <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Nu aveți probleme cu camera foto? Atingeți pentru a închide."</string> - <!-- 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) --> + <string name="restart_button_description" msgid="6712141648865547958">"Atinge ca să repornești aplicația pentru o vizualizare mai bună."</string> + <string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"Ai probleme cu camera foto?\nAtinge pentru a reîncadra"</string> + <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Nu ai remediat problema?\nAtinge pentru a reveni"</string> + <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Nu ai probleme cu camera foto? Atinge pentru a închide."</string> + <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Vezi și fă mai multe"</string> + <!-- no translation found for letterbox_education_split_screen_text (449233070804658627) --> <skip /> + <string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Atinge de două ori lângă o aplicație pentru a o repoziționa"</string> <string name="letterbox_education_got_it" msgid="4057634570866051177">"OK"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Extinde pentru mai multe informații"</string> + <string name="letterbox_restart_dialog_title" msgid="8543049527871033505">"Repornești pentru o vizualizare mai bună?"</string> + <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"Poți să repornești aplicația ca să arate mai bine pe ecran, dar este posibil să pierzi progresul sau modificările nesalvate"</string> + <string name="letterbox_restart_cancel" msgid="1342209132692537805">"Anulează"</string> + <string name="letterbox_restart_restart" msgid="8529976234412442973">"Repornește"</string> + <string name="letterbox_restart_dialog_checkbox_title" msgid="5252918008140768386">"Nu mai afișa"</string> + <!-- no translation found for letterbox_reachability_reposition_text (4507890186297500893) --> + <skip /> + <string name="maximize_button_text" msgid="1650859196290301963">"Maximizează"</string> + <string name="minimize_button_text" msgid="271592547935841753">"Minimizează"</string> + <string name="close_button_text" msgid="2913281996024033299">"Închide"</string> + <string name="back_button_text" msgid="1469718707134137085">"Înapoi"</string> + <string name="handle_text" msgid="1766582106752184456">"Ghidaj"</string> + <string name="app_icon_text" msgid="2823268023931811747">"Pictograma aplicației"</string> + <string name="fullscreen_text" msgid="1162316685217676079">"Ecran complet"</string> + <string name="desktop_text" msgid="1077633567027630454">"Modul desktop"</string> + <string name="split_screen_text" msgid="1396336058129570886">"Ecran împărțit"</string> + <string name="more_button_text" msgid="3655388105592893530">"Mai multe"</string> + <string name="float_button_text" msgid="9221657008391364581">"Flotantă"</string> + <string name="select_text" msgid="5139083974039906583">"Selectează"</string> + <string name="screenshot_text" msgid="1477704010087786671">"Captură de ecran"</string> + <string name="close_text" msgid="4986518933445178928">"Închide"</string> + <string name="collapse_menu_text" msgid="7515008122450342029">"Închide meniul"</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..c3226ff28934 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"</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">"Mută"</string> + <string name="pip_expand" msgid="1051966011679297308">"Extinde"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Restrânge"</string> + <string name="pip_edu_text" msgid="7930546669915337998">"Apasă de 2 ori "<annotation icon="home_icon">"ECRANUL DE PORNIRE"</annotation>" pentru comenzi"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Meniu picture-in-picture."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Mută la stânga"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Mută la dreapta"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Mută în sus"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Mută în jos"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Gata"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ru/strings.xml b/libs/WindowManager/Shell/res/values-ru/strings.xml index 64e74a27d32b..83934c476fae 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> @@ -31,11 +32,17 @@ <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Изменить размер"</string> <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Скрыть"</string> <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Показать"</string> - <string name="dock_forced_resizable" msgid="1749750436092293116">"В режиме разделения экрана приложение может работать нестабильно."</string> - <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Приложение не поддерживает разделение экрана."</string> + <!-- no translation found for dock_forced_resizable (7429086980048964687) --> + <skip /> + <!-- no translation found for dock_non_resizeble_failed_to_dock_text (2733543750291266047) --> + <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Это приложение можно открыть только в одном окне."</string> <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> + <!-- no translation found for accessibility_divider (6407584574218956849) --> + <skip /> + <!-- no translation found for divider_title (1963391955593749442) --> + <skip /> <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> @@ -46,6 +53,10 @@ <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Верхний на 50%"</string> <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Верхний на 30%"</string> <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Нижний во весь экран"</string> + <string name="accessibility_split_left" msgid="1713683765575562458">"Приложение слева"</string> + <string name="accessibility_split_right" msgid="8441001008181296837">"Приложение справа"</string> + <string name="accessibility_split_top" msgid="2789329702027147146">"Приложение сверху"</string> + <string name="accessibility_split_bottom" msgid="8694551025220868191">"Приложение снизу"</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> @@ -61,6 +72,7 @@ <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Перенести в правый нижний угол"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>: настройки"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Скрыть всплывающий чат"</string> + <string name="bubbles_dont_bubble" msgid="3216183855437329223">"Отключить всплывающие подсказки"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Не показывать всплывающий чат для разговора"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Всплывающие чаты"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"Новые разговоры будут появляться в виде плавающих значков, или всплывающих чатов. Чтобы открыть чат, нажмите на него, а чтобы переместить – перетащите."</string> @@ -72,17 +84,35 @@ <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) --> - <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) --> + <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Выполняйте несколько задач одновременно"</string> + <!-- no translation found for letterbox_education_split_screen_text (449233070804658627) --> <skip /> + <string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Чтобы переместить приложение, дважды нажмите рядом с ним."</string> <string name="letterbox_education_got_it" msgid="4057634570866051177">"ОК"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Развернуть, чтобы узнать больше."</string> + <string name="letterbox_restart_dialog_title" msgid="8543049527871033505">"Перезапустить приложение?"</string> + <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"Вы можете перезапустить приложение, чтобы оно лучше смотрелось на экране. При этом ваш прогресс или несохраненные изменения могут быть утеряны."</string> + <string name="letterbox_restart_cancel" msgid="1342209132692537805">"Отмена"</string> + <string name="letterbox_restart_restart" msgid="8529976234412442973">"Перезапустить"</string> + <string name="letterbox_restart_dialog_checkbox_title" msgid="5252918008140768386">"Больше не показывать"</string> + <string name="letterbox_reachability_reposition_text" msgid="4507890186297500893">"Нажмите дважды, чтобы переместить приложение."</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> + <string name="back_button_text" msgid="1469718707134137085">"Назад"</string> + <string name="handle_text" msgid="1766582106752184456">"Маркер"</string> + <string name="app_icon_text" msgid="2823268023931811747">"Значок приложения"</string> + <string name="fullscreen_text" msgid="1162316685217676079">"Полноэкранный режим"</string> + <string name="desktop_text" msgid="1077633567027630454">"Режим компьютера"</string> + <string name="split_screen_text" msgid="1396336058129570886">"Разделить экран"</string> + <string name="more_button_text" msgid="3655388105592893530">"Ещё"</string> + <string name="float_button_text" msgid="9221657008391364581">"Плавающее окно"</string> + <string name="select_text" msgid="5139083974039906583">"Выбрать"</string> + <string name="screenshot_text" msgid="1477704010087786671">"Скриншот"</string> + <string name="close_text" msgid="4986518933445178928">"Закрыть"</string> + <string name="collapse_menu_text" msgid="7515008122450342029">"Закрыть меню"</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..c8fb47913ec9 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="7930546669915337998">"Параметры: дважды нажмите "<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..21ae496903c5 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> @@ -31,11 +32,17 @@ <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"ප්රතිප්රමාණ කරන්න"</string> <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"සඟවා තබන්න"</string> <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"සඟවා තැබීම ඉවත් කරන්න"</string> - <string name="dock_forced_resizable" msgid="1749750436092293116">"යෙදුම බෙදුම් තිරය සමග ක්රියා නොකළ හැකිය"</string> - <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"යෙදුම බෙදුණු-තිරය සඳහා සහාය නොදක්වයි."</string> + <!-- no translation found for dock_forced_resizable (7429086980048964687) --> + <skip /> + <!-- no translation found for dock_non_resizeble_failed_to_dock_text (2733543750291266047) --> + <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"මෙම යෙදුම විවෘත කළ හැක්කේ 1 කවුළුවක පමණයි."</string> <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> + <!-- no translation found for accessibility_divider (6407584574218956849) --> + <skip /> + <!-- no translation found for divider_title (1963391955593749442) --> + <skip /> <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> @@ -46,6 +53,10 @@ <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"ඉහළම 50%"</string> <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"ඉහළම 30%"</string> <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"පහළ පූර්ණ තිරය"</string> + <string name="accessibility_split_left" msgid="1713683765575562458">"වම බෙදන්න"</string> + <string name="accessibility_split_right" msgid="8441001008181296837">"දකුණ බෙදන්න"</string> + <string name="accessibility_split_top" msgid="2789329702027147146">"ඉහළ බෙදන්න"</string> + <string name="accessibility_split_bottom" msgid="8694551025220868191">"පහළ බෙදන්න"</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> @@ -61,6 +72,7 @@ <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"පහළ දකුණට ගෙන යන්න"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> සැකසීම්"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"බුබුලු ඉවත ලන්න"</string> + <string name="bubbles_dont_bubble" msgid="3216183855437329223">"බුබුළු නොකරන්න"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"සංවාදය බුබුලු නොදමන්න"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"බුබුලු භාවිතයෙන් කතාබහ කරන්න"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"නව සංවාද පාවෙන අයිකන හෝ බුබුලු ලෙස දිස් වේ. බුබුල විවෘත කිරීමට තට්ටු කරන්න. එය ගෙන යාමට අදින්න."</string> @@ -72,17 +84,36 @@ <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) --> - <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) --> + <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"බලන්න සහ තවත් දේ කරන්න"</string> + <!-- no translation found for letterbox_education_split_screen_text (449233070804658627) --> <skip /> + <string name="letterbox_education_reposition_text" msgid="4589957299813220661">"යෙදුමක් නැවත ස්ථානගත කිරීමට පිටතින් දෙවරක් තට්ටු කරන්න"</string> <string name="letterbox_education_got_it" msgid="4057634570866051177">"තේරුණා"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"වැඩිදුර තොරතුරු සඳහා දිග හරින්න"</string> + <string name="letterbox_restart_dialog_title" msgid="8543049527871033505">"වඩා හොඳ දසුනක් සඳහා යළි අරඹන්න ද?"</string> + <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"ඔබට එය ඔබේ තිරයෙහි වඩා හොඳින් පෙනෙන පරිදි යෙදුම යළි ඇරඹිය හැකි නමුත්, ඔබට ඔබේ ප්රගතිය හෝ නොසුරකින ලද වෙනස්කම් අහිමි විය හැක"</string> + <string name="letterbox_restart_cancel" msgid="1342209132692537805">"අවලංගු කරන්න"</string> + <string name="letterbox_restart_restart" msgid="8529976234412442973">"යළි අරඹන්න"</string> + <string name="letterbox_restart_dialog_checkbox_title" msgid="5252918008140768386">"නැවත නොපෙන්වන්න"</string> + <!-- no translation found for letterbox_reachability_reposition_text (4507890186297500893) --> + <skip /> + <string name="maximize_button_text" msgid="1650859196290301963">"විහිදන්න"</string> + <string name="minimize_button_text" msgid="271592547935841753">"කුඩා කරන්න"</string> + <string name="close_button_text" msgid="2913281996024033299">"වසන්න"</string> + <string name="back_button_text" msgid="1469718707134137085">"ආපසු"</string> + <string name="handle_text" msgid="1766582106752184456">"හැඬලය"</string> + <string name="app_icon_text" msgid="2823268023931811747">"යෙදුම් නිරූපකය"</string> + <string name="fullscreen_text" msgid="1162316685217676079">"පූර්ණ තිරය"</string> + <string name="desktop_text" msgid="1077633567027630454">"ඩෙස්ක්ටොප් ප්රකාරය"</string> + <string name="split_screen_text" msgid="1396336058129570886">"බෙදුම් තිරය"</string> + <string name="more_button_text" msgid="3655388105592893530">"තව"</string> + <string name="float_button_text" msgid="9221657008391364581">"පාවෙන"</string> + <string name="select_text" msgid="5139083974039906583">"තෝරන්න"</string> + <string name="screenshot_text" msgid="1477704010087786671">"තිර රුව"</string> + <string name="close_text" msgid="4986518933445178928">"වසන්න"</string> + <string name="collapse_menu_text" msgid="7515008122450342029">"මෙනුව වසන්න"</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..aa949ec75b3d 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="7930546669915337998">"පාලන සඳහා "<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..fb43ba88dfd8 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> @@ -31,11 +32,17 @@ <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Zmeniť veľkosť"</string> <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Skryť"</string> <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Zrušiť skrytie"</string> - <string name="dock_forced_resizable" msgid="1749750436092293116">"Aplikácia nemusí fungovať s rozdelenou obrazovkou."</string> - <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Aplikácia nepodporuje rozdelenú obrazovku."</string> + <!-- no translation found for dock_forced_resizable (7429086980048964687) --> + <skip /> + <!-- no translation found for dock_non_resizeble_failed_to_dock_text (2733543750291266047) --> + <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Táto aplikácia môže byť otvorená iba v jednom okne."</string> <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> + <!-- no translation found for accessibility_divider (6407584574218956849) --> + <skip /> + <!-- no translation found for divider_title (1963391955593749442) --> + <skip /> <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> @@ -46,6 +53,10 @@ <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Horná – 50 %"</string> <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Horná – 30 %"</string> <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Dolná – na celú obrazovku"</string> + <string name="accessibility_split_left" msgid="1713683765575562458">"Rozdeliť vľavo"</string> + <string name="accessibility_split_right" msgid="8441001008181296837">"Rozdeliť vpravo"</string> + <string name="accessibility_split_top" msgid="2789329702027147146">"Rozdeliť hore"</string> + <string name="accessibility_split_bottom" msgid="8694551025220868191">"Rozdeliť dole"</string> <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Používanie režimu jednej ruky"</string> <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Ukončíte potiahnutím z dolnej časti obrazovky nahor alebo klepnutím kdekoľvek nad aplikáciu"</string> <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Spustiť režim jednej ruky"</string> @@ -61,6 +72,7 @@ <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Presunúť doprava nadol"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"Nastavenia aplikácie <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Zavrieť bublinu"</string> + <string name="bubbles_dont_bubble" msgid="3216183855437329223">"Nezobrazovať bubliny"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Nezobrazovať konverzáciu ako bublinu"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Čet pomocou bublín"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"Nové konverzácie sa zobrazujú ako plávajúce ikony či bubliny. Bublinu otvoríte klepnutím. Premiestnite ju presunutím."</string> @@ -72,17 +84,35 @@ <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) --> - <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) --> + <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Zobrazte si a zvládnite toho viac"</string> + <!-- no translation found for letterbox_education_split_screen_text (449233070804658627) --> <skip /> + <string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Dvojitým klepnutím mimo aplikácie zmeníte jej pozíciu"</string> <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="letterbox_restart_dialog_title" msgid="8543049527871033505">"Chcete ju reštartovať, aby mala lepší vzhľad?"</string> + <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"Aplikáciu môžete reštartovať, aby mala na obrazovke lepší vzhľad, ale môžete prísť o postup a všetky neuložené zmeny."</string> + <string name="letterbox_restart_cancel" msgid="1342209132692537805">"Zrušiť"</string> + <string name="letterbox_restart_restart" msgid="8529976234412442973">"Reštartovať"</string> + <string name="letterbox_restart_dialog_checkbox_title" msgid="5252918008140768386">"Už nezobrazovať"</string> + <string name="letterbox_reachability_reposition_text" msgid="4507890186297500893">"Túto aplikáciu presuniete dvojitým klepnutím"</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> + <string name="back_button_text" msgid="1469718707134137085">"Späť"</string> + <string name="handle_text" msgid="1766582106752184456">"Rukoväť"</string> + <string name="app_icon_text" msgid="2823268023931811747">"Ikona aplikácie"</string> + <string name="fullscreen_text" msgid="1162316685217676079">"Celá obrazovka"</string> + <string name="desktop_text" msgid="1077633567027630454">"Režim počítača"</string> + <string name="split_screen_text" msgid="1396336058129570886">"Rozdelená obrazovka"</string> + <string name="more_button_text" msgid="3655388105592893530">"Viac"</string> + <string name="float_button_text" msgid="9221657008391364581">"Plávajúce"</string> + <string name="select_text" msgid="5139083974039906583">"Vybrať"</string> + <string name="screenshot_text" msgid="1477704010087786671">"Snímka obrazovky"</string> + <string name="close_text" msgid="4986518933445178928">"Zavrieť"</string> + <string name="collapse_menu_text" msgid="7515008122450342029">"Zavrieť ponuku"</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..d5562d519d4b 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="7930546669915337998">"Ovládanie zobrazíte dvojitým stlačením "<annotation icon="home_icon">"DOMOV"</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..331b3fd0d375 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> @@ -31,11 +32,17 @@ <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Spremeni velikost"</string> <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Zakrij"</string> <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Razkrij"</string> - <string name="dock_forced_resizable" msgid="1749750436092293116">"Aplikacija morda ne deluje v načinu razdeljenega zaslona."</string> - <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Aplikacija ne podpira načina razdeljenega zaslona."</string> + <!-- no translation found for dock_forced_resizable (7429086980048964687) --> + <skip /> + <!-- no translation found for dock_non_resizeble_failed_to_dock_text (2733543750291266047) --> + <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"To aplikacijo je mogoče odpreti samo v enem oknu."</string> <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> + <!-- no translation found for accessibility_divider (6407584574218956849) --> + <skip /> + <!-- no translation found for divider_title (1963391955593749442) --> + <skip /> <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> @@ -46,6 +53,10 @@ <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Zgornji 50 %"</string> <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Zgornji 30 %"</string> <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Spodnji v celozaslonski način"</string> + <string name="accessibility_split_left" msgid="1713683765575562458">"Delitev levo"</string> + <string name="accessibility_split_right" msgid="8441001008181296837">"Delitev desno"</string> + <string name="accessibility_split_top" msgid="2789329702027147146">"Delitev zgoraj"</string> + <string name="accessibility_split_bottom" msgid="8694551025220868191">"Delitev spodaj"</string> <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Uporaba enoročnega načina"</string> <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Za izhod povlecite z dna zaslona navzgor ali se dotaknite na poljubnem mestu nad aplikacijo"</string> <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Zagon enoročnega načina"</string> @@ -61,6 +72,7 @@ <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Premakni spodaj desno"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"Nastavitve za <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Opusti oblaček"</string> + <string name="bubbles_dont_bubble" msgid="3216183855437329223">"Ne prikazuj oblačkov aplikacij"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Pogovora ne prikaži v oblačku"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Klepet z oblački"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"Novi pogovori so prikazani kot lebdeče ikone ali oblački. Če želite odpreti oblaček, se ga dotaknite. Če ga želite premakniti, ga povlecite."</string> @@ -72,17 +84,35 @@ <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) --> - <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) --> + <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Oglejte si in naredite več"</string> + <!-- no translation found for letterbox_education_split_screen_text (449233070804658627) --> <skip /> + <string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Dvakrat se dotaknite zunaj aplikacije, če jo želite prestaviti."</string> <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="letterbox_restart_dialog_title" msgid="8543049527871033505">"Želite znova zagnati za boljši pregled?"</string> + <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"Če znova zaženete aplikacijo, bo prikaz na zaslonu boljši, vendar lahko izgubite napredek ali neshranjene spremembe."</string> + <string name="letterbox_restart_cancel" msgid="1342209132692537805">"Prekliči"</string> + <string name="letterbox_restart_restart" msgid="8529976234412442973">"Znova zaženi"</string> + <string name="letterbox_restart_dialog_checkbox_title" msgid="5252918008140768386">"Ne prikaži več"</string> + <string name="letterbox_reachability_reposition_text" msgid="4507890186297500893">"Dvakrat se dotaknite, če želite premakniti to aplikacijo"</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> + <string name="back_button_text" msgid="1469718707134137085">"Nazaj"</string> + <string name="handle_text" msgid="1766582106752184456">"Ročica"</string> + <string name="app_icon_text" msgid="2823268023931811747">"Ikona aplikacije"</string> + <string name="fullscreen_text" msgid="1162316685217676079">"Celozaslonsko"</string> + <string name="desktop_text" msgid="1077633567027630454">"Namizni način"</string> + <string name="split_screen_text" msgid="1396336058129570886">"Razdeljen zaslon"</string> + <string name="more_button_text" msgid="3655388105592893530">"Več"</string> + <string name="float_button_text" msgid="9221657008391364581">"Lebdeče"</string> + <string name="select_text" msgid="5139083974039906583">"Izberi"</string> + <string name="screenshot_text" msgid="1477704010087786671">"Posnetek zaslona"</string> + <string name="close_text" msgid="4986518933445178928">"Zapri"</string> + <string name="collapse_menu_text" msgid="7515008122450342029">"Zapri meni"</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..a37375e1ae9c 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="7930546669915337998">"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..c7a1d9cec212 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> @@ -31,11 +32,17 @@ <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Ndrysho përmasat"</string> <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Fshih"</string> <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Mos e fshih"</string> - <string name="dock_forced_resizable" msgid="1749750436092293116">"Aplikacioni mund të mos funksionojë me ekranin e ndarë."</string> - <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Aplikacioni nuk mbështet ekranin e ndarë."</string> + <!-- no translation found for dock_forced_resizable (7429086980048964687) --> + <skip /> + <!-- no translation found for dock_non_resizeble_failed_to_dock_text (2733543750291266047) --> + <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Ky aplikacion mund të hapet vetëm në 1 dritare."</string> <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> + <!-- no translation found for accessibility_divider (6407584574218956849) --> + <skip /> + <!-- no translation found for divider_title (1963391955593749442) --> + <skip /> <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> @@ -46,6 +53,10 @@ <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Lart 50%"</string> <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Lart 30%"</string> <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Ekrani i plotë poshtë"</string> + <string name="accessibility_split_left" msgid="1713683765575562458">"Ndaj majtas"</string> + <string name="accessibility_split_right" msgid="8441001008181296837">"Ndaj djathtas"</string> + <string name="accessibility_split_top" msgid="2789329702027147146">"Ndaj lart"</string> + <string name="accessibility_split_bottom" msgid="8694551025220868191">"Ndaj në fund"</string> <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Po përdor modalitetin e përdorimit me një dorë"</string> <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Për të dalë, rrëshqit lart nga fundi i ekranit ose trokit diku mbi aplikacion"</string> <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Modaliteti i përdorimit me një dorë"</string> @@ -61,6 +72,7 @@ <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Lëvize poshtë djathtas"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"Cilësimet e <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Hiqe flluskën"</string> + <string name="bubbles_dont_bubble" msgid="3216183855437329223">"Mos shfaq flluska"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Mos e vendos bisedën në flluskë"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Bisedo duke përdorur flluskat"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"Bisedat e reja shfaqen si ikona pluskuese ose flluska. Trokit për të hapur flluskën. Zvarrit për ta zhvendosur."</string> @@ -72,17 +84,35 @@ <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) --> - <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) --> + <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Shiko dhe bëj më shumë"</string> + <!-- no translation found for letterbox_education_split_screen_text (449233070804658627) --> <skip /> + <string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Trokit dy herë jashtë një aplikacioni për ta ripozicionuar"</string> <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="letterbox_restart_dialog_title" msgid="8543049527871033505">"Rinis për një pamje më të mirë?"</string> + <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"Mund të rinisësh aplikacionin në mënyrë që të duket më mirë në ekranin tënd, por mund të humbësh progresin ose çdo ndryshim të paruajtur"</string> + <string name="letterbox_restart_cancel" msgid="1342209132692537805">"Anulo"</string> + <string name="letterbox_restart_restart" msgid="8529976234412442973">"Rinis"</string> + <string name="letterbox_restart_dialog_checkbox_title" msgid="5252918008140768386">"Mos e shfaq përsëri"</string> + <string name="letterbox_reachability_reposition_text" msgid="4507890186297500893">"Trokit dy herë për ta lëvizur këtë aplikacion"</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> + <string name="back_button_text" msgid="1469718707134137085">"Pas"</string> + <string name="handle_text" msgid="1766582106752184456">"Emërtimi"</string> + <string name="app_icon_text" msgid="2823268023931811747">"Ikona e aplikacionit"</string> + <string name="fullscreen_text" msgid="1162316685217676079">"Ekrani i plotë"</string> + <string name="desktop_text" msgid="1077633567027630454">"Modaliteti i desktopit"</string> + <string name="split_screen_text" msgid="1396336058129570886">"Ekrani i ndarë"</string> + <string name="more_button_text" msgid="3655388105592893530">"Më shumë"</string> + <string name="float_button_text" msgid="9221657008391364581">"Pluskuese"</string> + <string name="select_text" msgid="5139083974039906583">"Zgjidh"</string> + <string name="screenshot_text" msgid="1477704010087786671">"Pamja e ekranit"</string> + <string name="close_text" msgid="4986518933445178928">"Mbyll"</string> + <string name="collapse_menu_text" msgid="7515008122450342029">"Mbyll menynë"</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..3fbaaac2d3a2 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="7930546669915337998">"Trokit dy herë te "<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..c4ea1f7a912e 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> @@ -31,11 +32,17 @@ <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Промените величину"</string> <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Ставите у тајну меморију"</string> <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Уклоните из тајне меморије"</string> - <string name="dock_forced_resizable" msgid="1749750436092293116">"Апликација можда неће радити са подељеним екраном."</string> - <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Апликација не подржава подељени екран."</string> + <!-- no translation found for dock_forced_resizable (7429086980048964687) --> + <skip /> + <!-- no translation found for dock_non_resizeble_failed_to_dock_text (2733543750291266047) --> + <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Ова апликација може да се отвори само у једном прозору."</string> <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> + <!-- no translation found for accessibility_divider (6407584574218956849) --> + <skip /> + <!-- no translation found for divider_title (1963391955593749442) --> + <skip /> <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> @@ -46,6 +53,10 @@ <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Горњи екран 50%"</string> <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Горњи екран 30%"</string> <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Режим целог екрана за доњи екран"</string> + <string name="accessibility_split_left" msgid="1713683765575562458">"Поделите лево"</string> + <string name="accessibility_split_right" msgid="8441001008181296837">"Поделите десно"</string> + <string name="accessibility_split_top" msgid="2789329702027147146">"Поделите у врху"</string> + <string name="accessibility_split_bottom" msgid="8694551025220868191">"Поделите у дну"</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> @@ -61,6 +72,7 @@ <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Премести доле десно"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"Подешавања за <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Одбаци облачић"</string> + <string name="bubbles_dont_bubble" msgid="3216183855437329223">"Без облачића"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Не користи облачиће за конверзацију"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Ћаскајте у облачићима"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"Нове конверзације се приказују као плутајуће иконе или облачићи. Додирните да бисте отворили облачић. Превуците да бисте га преместили."</string> @@ -72,17 +84,35 @@ <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) --> - <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) --> + <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Видите и урадите више"</string> + <!-- no translation found for letterbox_education_split_screen_text (449233070804658627) --> <skip /> + <string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Двапут додирните изван апликације да бисте променили њену позицију"</string> <string name="letterbox_education_got_it" msgid="4057634570866051177">"Важи"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Проширите за још информација."</string> + <string name="letterbox_restart_dialog_title" msgid="8543049527871033505">"Желите ли да рестартујете ради бољег приказа?"</string> + <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"Можете да рестартујете апликацију да би изгледала боље на екрану, с тим што можете да изгубите оно што сте урадили или несачуване промене, ако их има"</string> + <string name="letterbox_restart_cancel" msgid="1342209132692537805">"Откажи"</string> + <string name="letterbox_restart_restart" msgid="8529976234412442973">"Рестартуј"</string> + <string name="letterbox_restart_dialog_checkbox_title" msgid="5252918008140768386">"Не приказуј поново"</string> + <string name="letterbox_reachability_reposition_text" msgid="4507890186297500893">"Двапут додирните да бисте преместили ову апликацију"</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> + <string name="back_button_text" msgid="1469718707134137085">"Назад"</string> + <string name="handle_text" msgid="1766582106752184456">"Идентификатор"</string> + <string name="app_icon_text" msgid="2823268023931811747">"Икона апликације"</string> + <string name="fullscreen_text" msgid="1162316685217676079">"Преко целог екрана"</string> + <string name="desktop_text" msgid="1077633567027630454">"Режим за рачунаре"</string> + <string name="split_screen_text" msgid="1396336058129570886">"Подељени екран"</string> + <string name="more_button_text" msgid="3655388105592893530">"Још"</string> + <string name="float_button_text" msgid="9221657008391364581">"Плутајуће"</string> + <string name="select_text" msgid="5139083974039906583">"Изаберите"</string> + <string name="screenshot_text" msgid="1477704010087786671">"Снимак екрана"</string> + <string name="close_text" msgid="4986518933445178928">"Затворите"</string> + <string name="collapse_menu_text" msgid="7515008122450342029">"Затворите мени"</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..34950027772b 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="7930546669915337998">"Двапут притисните "<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..5ae673a3340f 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> @@ -31,11 +32,17 @@ <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Ändra storlek"</string> <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Utför stash"</string> <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Återställ stash"</string> - <string name="dock_forced_resizable" msgid="1749750436092293116">"Appen kanske inte fungerar med delad skärm."</string> - <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Appen har inte stöd för delad skärm."</string> + <!-- no translation found for dock_forced_resizable (7429086980048964687) --> + <skip /> + <!-- no translation found for dock_non_resizeble_failed_to_dock_text (2733543750291266047) --> + <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Denna app kan bara vara öppen i ett fönster."</string> <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> + <!-- no translation found for accessibility_divider (6407584574218956849) --> + <skip /> + <!-- no translation found for divider_title (1963391955593749442) --> + <skip /> <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> @@ -46,6 +53,10 @@ <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Övre 50 %"</string> <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Övre 30 %"</string> <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Helskärm på nedre skärm"</string> + <string name="accessibility_split_left" msgid="1713683765575562458">"Till vänster på delad skärm"</string> + <string name="accessibility_split_right" msgid="8441001008181296837">"Till höger på delad skärm"</string> + <string name="accessibility_split_top" msgid="2789329702027147146">"Upptill på delad skärm"</string> + <string name="accessibility_split_bottom" msgid="8694551025220868191">"Nedtill på delad skärm"</string> <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Använda enhandsläge"</string> <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Avsluta genom att svepa uppåt från skärmens nederkant eller trycka ovanför appen"</string> <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Starta enhandsläge"</string> @@ -61,6 +72,7 @@ <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Flytta längst ned till höger"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"Inställningar för <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Stäng bubbla"</string> + <string name="bubbles_dont_bubble" msgid="3216183855437329223">"Visa inte bubblor"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Visa inte konversationen i bubblor"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Chatta med bubblor"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"Nya konversationer visas som flytande ikoner, så kallade bubblor. Tryck på bubblan om du vill öppna den. Dra den om du vill flytta den."</string> @@ -72,17 +84,36 @@ <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) --> - <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) --> + <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Se och gör mer"</string> + <!-- no translation found for letterbox_education_split_screen_text (449233070804658627) --> <skip /> + <string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Tryck snabbt två gånger utanför en app för att flytta den"</string> <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="letterbox_restart_dialog_title" msgid="8543049527871033505">"Vill du starta om för en bättre vy?"</string> + <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"Du kan starta om appen så den passar bättre på skärmen men du kan förlora dina framsteg och eventuella osparade ändringar."</string> + <string name="letterbox_restart_cancel" msgid="1342209132692537805">"Avbryt"</string> + <string name="letterbox_restart_restart" msgid="8529976234412442973">"Starta om"</string> + <string name="letterbox_restart_dialog_checkbox_title" msgid="5252918008140768386">"Visa inte igen"</string> + <!-- no translation found for letterbox_reachability_reposition_text (4507890186297500893) --> + <skip /> + <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> + <string name="back_button_text" msgid="1469718707134137085">"Tillbaka"</string> + <string name="handle_text" msgid="1766582106752184456">"Handtag"</string> + <string name="app_icon_text" msgid="2823268023931811747">"Appikon"</string> + <string name="fullscreen_text" msgid="1162316685217676079">"Helskärm"</string> + <string name="desktop_text" msgid="1077633567027630454">"Datorläge"</string> + <string name="split_screen_text" msgid="1396336058129570886">"Delad skärm"</string> + <string name="more_button_text" msgid="3655388105592893530">"Mer"</string> + <string name="float_button_text" msgid="9221657008391364581">"Svävande"</string> + <string name="select_text" msgid="5139083974039906583">"Välj"</string> + <string name="screenshot_text" msgid="1477704010087786671">"Skärmbild"</string> + <string name="close_text" msgid="4986518933445178928">"Stäng"</string> + <string name="collapse_menu_text" msgid="7515008122450342029">"Stäng menyn"</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..7116ac162fbd 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="7930546669915337998">"Tryck snabbt två gånger på "<annotation icon="home_icon">"HEM"</annotation>" för inställningar"</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..9c79b3bc663a 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> @@ -31,11 +32,17 @@ <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Badilisha ukubwa"</string> <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Ficha"</string> <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Fichua"</string> - <string name="dock_forced_resizable" msgid="1749750436092293116">"Huenda programu isifanye kazi kwenye skrini inayogawanywa."</string> - <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Programu haiwezi kutumia skrini iliyogawanywa."</string> + <!-- no translation found for dock_forced_resizable (7429086980048964687) --> + <skip /> + <!-- no translation found for dock_non_resizeble_failed_to_dock_text (2733543750291266047) --> + <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Programu hii inaweza kufunguliwa katika dirisha 1 pekee."</string> <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> + <!-- no translation found for accessibility_divider (6407584574218956849) --> + <skip /> + <!-- no translation found for divider_title (1963391955593749442) --> + <skip /> <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> @@ -46,6 +53,10 @@ <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Juu 50%"</string> <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Juu 30%"</string> <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Skrini nzima ya chini"</string> + <string name="accessibility_split_left" msgid="1713683765575562458">"Gawanya sehemu ya kushoto"</string> + <string name="accessibility_split_right" msgid="8441001008181296837">"Gawanya sehemu ya kulia"</string> + <string name="accessibility_split_top" msgid="2789329702027147146">"Gawanya sehemu ya juu"</string> + <string name="accessibility_split_bottom" msgid="8694551025220868191">"Gawanya sehemu ya chini"</string> <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Kutumia hali ya kutumia kwa mkono mmoja"</string> <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Ili ufunge, telezesha kidole juu kutoka sehemu ya chini ya skrini au uguse mahali popote juu ya programu"</string> <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Anzisha hali ya kutumia kwa mkono mmoja"</string> @@ -61,6 +72,7 @@ <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Sogeza chini kulia"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"Mipangilio ya <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Ondoa kiputo"</string> + <string name="bubbles_dont_bubble" msgid="3216183855437329223">"Isifanye viputo"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Usiweke viputo kwenye mazungumzo"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Piga gumzo ukitumia viputo"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"Mazungumzo mapya huonekena kama aikoni au viputo vinavyoelea. Gusa ili ufungue kiputo. Buruta ili ukisogeze."</string> @@ -72,17 +84,35 @@ <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) --> - <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) --> + <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Angalia na ufanye zaidi"</string> + <!-- no translation found for letterbox_education_split_screen_text (449233070804658627) --> <skip /> + <string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Gusa mara mbili nje ya programu ili uihamishe"</string> <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="letterbox_restart_dialog_title" msgid="8543049527871033505">"Ungependa kuzima kisha uwashe ili upate mwonekano bora zaidi?"</string> + <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"Unaweza kuzima kisha uwashe programu yako ili ifanye kazi vizuri zaidi kwenye skrini yako, lakini unaweza kupoteza maendeleo yako au mabadiliko yoyote ambayo hayajahifadhiwa"</string> + <string name="letterbox_restart_cancel" msgid="1342209132692537805">"Ghairi"</string> + <string name="letterbox_restart_restart" msgid="8529976234412442973">"Zima kisha uwashe"</string> + <string name="letterbox_restart_dialog_checkbox_title" msgid="5252918008140768386">"Usionyeshe tena"</string> + <string name="letterbox_reachability_reposition_text" msgid="4507890186297500893">"Gusa mara mbili ili usogeze programu hii"</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> + <string name="back_button_text" msgid="1469718707134137085">"Rudi nyuma"</string> + <string name="handle_text" msgid="1766582106752184456">"Ncha"</string> + <string name="app_icon_text" msgid="2823268023931811747">"Aikoni ya Programu"</string> + <string name="fullscreen_text" msgid="1162316685217676079">"Skrini nzima"</string> + <string name="desktop_text" msgid="1077633567027630454">"Hali ya Kompyuta ya mezani"</string> + <string name="split_screen_text" msgid="1396336058129570886">"Gawa Skrini"</string> + <string name="more_button_text" msgid="3655388105592893530">"Zaidi"</string> + <string name="float_button_text" msgid="9221657008391364581">"Inayoelea"</string> + <string name="select_text" msgid="5139083974039906583">"Chagua"</string> + <string name="screenshot_text" msgid="1477704010087786671">"Picha ya skrini"</string> + <string name="close_text" msgid="4986518933445178928">"Funga"</string> + <string name="collapse_menu_text" msgid="7515008122450342029">"Funga Menyu"</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..1e9406f01433 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="7930546669915337998">"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..3b9c9f3630ba 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> @@ -31,11 +32,17 @@ <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"அளவு மாற்று"</string> <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Stash"</string> <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Unstash"</string> - <string name="dock_forced_resizable" msgid="1749750436092293116">"திரைப் பிரிப்பு அம்சத்தில் ஆப்ஸ் செயல்படாமல் போகக்கூடும்."</string> - <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"திரையைப் பிரிப்பதைப் ஆப்ஸ் ஆதரிக்கவில்லை."</string> + <!-- no translation found for dock_forced_resizable (7429086980048964687) --> + <skip /> + <!-- no translation found for dock_non_resizeble_failed_to_dock_text (2733543750291266047) --> + <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"இந்த ஆப்ஸை 1 சாளரத்தில் மட்டுமே திறக்க முடியும்."</string> <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> + <!-- no translation found for accessibility_divider (6407584574218956849) --> + <skip /> + <!-- no translation found for divider_title (1963391955593749442) --> + <skip /> <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> @@ -46,6 +53,10 @@ <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"மேலே 50%"</string> <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"மேலே 30%"</string> <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"கீழ்ப்புறம் முழுத் திரை"</string> + <string name="accessibility_split_left" msgid="1713683765575562458">"இடதுபுறமாகப் பிரிக்கும்"</string> + <string name="accessibility_split_right" msgid="8441001008181296837">"வலதுபுறமாகப் பிரிக்கும்"</string> + <string name="accessibility_split_top" msgid="2789329702027147146">"மேற்புறமாகப் பிரிக்கும்"</string> + <string name="accessibility_split_bottom" msgid="8694551025220868191">"கீழ்புறமாகப் பிரிக்கும்"</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> @@ -61,6 +72,7 @@ <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"கீழே வலதுபுறமாக நகர்த்து"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> அமைப்புகள்"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"குமிழை அகற்று"</string> + <string name="bubbles_dont_bubble" msgid="3216183855437329223">"குமிழ்களைக் காட்ட வேண்டாம்"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"உரையாடலைக் குமிழாக்காதே"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"குமிழ்களைப் பயன்படுத்தி அரட்டையடியுங்கள்"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"புதிய உரையாடல்கள் மிதக்கும் ஐகான்களாகவோ குமிழ்களாகவோ தோன்றும். குமிழைத் திறக்க தட்டவும். நகர்த்த இழுக்கவும்."</string> @@ -72,17 +84,36 @@ <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) --> - <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) --> + <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"பலவற்றைப் பார்த்தல் மற்றும் செய்தல்"</string> + <!-- no translation found for letterbox_education_split_screen_text (449233070804658627) --> <skip /> + <string name="letterbox_education_reposition_text" msgid="4589957299813220661">"ஆப்ஸை இடம் மாற்ற அதன் வெளியில் இருமுறை தட்டலாம்"</string> <string name="letterbox_education_got_it" msgid="4057634570866051177">"சரி"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"கூடுதல் தகவல்களுக்கு விரிவாக்கலாம்."</string> + <string name="letterbox_restart_dialog_title" msgid="8543049527871033505">"சரியான விதத்தில் காட்டப்பட மீண்டும் தொடங்கவா?"</string> + <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"ஆப்ஸை மீண்டும் தொடங்குவதன் மூலம் திரையில் அது சரியான விதத்தில் தோற்றமளிக்கும். ஆனால் செயல்நிலையையோ சேமிக்கப்படாமல் இருக்கும் மாற்றங்களையோ நீங்கள் இழக்கக்கூடும்."</string> + <string name="letterbox_restart_cancel" msgid="1342209132692537805">"ரத்துசெய்"</string> + <string name="letterbox_restart_restart" msgid="8529976234412442973">"மீண்டும் தொடங்கு"</string> + <string name="letterbox_restart_dialog_checkbox_title" msgid="5252918008140768386">"மீண்டும் காட்டாதே"</string> + <!-- no translation found for letterbox_reachability_reposition_text (4507890186297500893) --> + <skip /> + <string name="maximize_button_text" msgid="1650859196290301963">"பெரிதாக்கும்"</string> + <string name="minimize_button_text" msgid="271592547935841753">"சிறிதாக்கும்"</string> + <string name="close_button_text" msgid="2913281996024033299">"மூடும்"</string> + <string name="back_button_text" msgid="1469718707134137085">"பின்செல்லும்"</string> + <string name="handle_text" msgid="1766582106752184456">"ஹேண்டில்"</string> + <string name="app_icon_text" msgid="2823268023931811747">"ஆப்ஸ் ஐகான்"</string> + <string name="fullscreen_text" msgid="1162316685217676079">"முழுத்திரை"</string> + <string name="desktop_text" msgid="1077633567027630454">"டெஸ்க்டாப் பயன்முறை"</string> + <string name="split_screen_text" msgid="1396336058129570886">"திரையைப் பிரிக்கும்"</string> + <string name="more_button_text" msgid="3655388105592893530">"கூடுதல் விருப்பத்தேர்வுகள்"</string> + <string name="float_button_text" msgid="9221657008391364581">"மிதக்கும் சாளரம்"</string> + <string name="select_text" msgid="5139083974039906583">"தேர்ந்தெடுக்கும்"</string> + <string name="screenshot_text" msgid="1477704010087786671">"ஸ்கிரீன்ஷாட்"</string> + <string name="close_text" msgid="4986518933445178928">"மூடும்"</string> + <string name="collapse_menu_text" msgid="7515008122450342029">"மெனுவை மூடும்"</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..ef1bcf913eaf 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="7930546669915337998">"கட்டுப்பாடுகளுக்கு "<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..2b0725c93694 100644 --- a/libs/WindowManager/Shell/res/values-te/strings.xml +++ b/libs/WindowManager/Shell/res/values-te/strings.xml @@ -22,20 +22,27 @@ <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> <string name="pip_pause" msgid="690688849510295232">"పాజ్ చేయి"</string> <string name="pip_skip_to_next" msgid="8403429188794867653">"దాటవేసి తర్వాత దానికి వెళ్లు"</string> <string name="pip_skip_to_prev" msgid="7172158111196394092">"దాటవేసి మునుపటి దానికి వెళ్లు"</string> - <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"పరిమాణం మార్చు"</string> + <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"సైజ్ మార్చు"</string> <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"స్టాచ్"</string> <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"ఆన్స్టాచ్"</string> - <string name="dock_forced_resizable" msgid="1749750436092293116">"స్క్రీన్ విభజనతో యాప్ పని చేయకపోవచ్చు."</string> - <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"యాప్లో స్క్రీన్ విభజనకు మద్దతు లేదు."</string> + <!-- no translation found for dock_forced_resizable (7429086980048964687) --> + <skip /> + <!-- no translation found for dock_non_resizeble_failed_to_dock_text (2733543750291266047) --> + <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"ఈ యాప్ను 1 విండోలో మాత్రమే తెరవవచ్చు."</string> <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> + <!-- no translation found for accessibility_divider (6407584574218956849) --> + <skip /> + <!-- no translation found for divider_title (1963391955593749442) --> + <skip /> <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> @@ -46,6 +53,10 @@ <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"ఎగువ 50%"</string> <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"ఎగువ 30%"</string> <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"దిగువ ఫుల్-స్క్రీన్"</string> + <string name="accessibility_split_left" msgid="1713683765575562458">"ఎడమ వైపున్న భాగంలో విభజించండి"</string> + <string name="accessibility_split_right" msgid="8441001008181296837">"కుడి వైపున్న భాగంలో విభజించండి"</string> + <string name="accessibility_split_top" msgid="2789329702027147146">"ఎగువ భాగంలో విభజించండి"</string> + <string name="accessibility_split_bottom" msgid="8694551025220868191">"దిగువ భాగంలో విభజించండి"</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> @@ -61,6 +72,7 @@ <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"దిగవు కుడివైపునకు జరుపు"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> సెట్టింగ్లు"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"బబుల్ను విస్మరించు"</string> + <string name="bubbles_dont_bubble" msgid="3216183855437329223">"బబుల్ను చూపడం ఆపండి"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"సంభాషణను బబుల్ చేయవద్దు"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"బబుల్స్ను ఉపయోగించి చాట్ చేయండి"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"కొత్త సంభాషణలు తేలియాడే చిహ్నాలుగా లేదా బబుల్స్ లాగా కనిపిస్తాయి. బబుల్ని తెరవడానికి నొక్కండి. తరలించడానికి లాగండి."</string> @@ -72,17 +84,35 @@ <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) --> - <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) --> + <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"చూసి, మరిన్ని చేయండి"</string> + <!-- no translation found for letterbox_education_split_screen_text (449233070804658627) --> <skip /> + <string name="letterbox_education_reposition_text" msgid="4589957299813220661">"యాప్ స్థానాన్ని మార్చడానికి దాని వెలుపల డబుల్-ట్యాప్ చేయండి"</string> <string name="letterbox_education_got_it" msgid="4057634570866051177">"అర్థమైంది"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"మరింత సమాచారం కోసం విస్తరించండి."</string> + <string name="letterbox_restart_dialog_title" msgid="8543049527871033505">"మెరుగైన వీక్షణ కోసం రీస్టార్ట్ చేయాలా?"</string> + <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"మీరు యాప్ని రీస్టార్ట్ చేయవచ్చు, తద్వారా ఇది మీ స్క్రీన్పై మెరుగ్గా కనిపిస్తుంది, కానీ మీరు మీ ప్రోగ్రెస్ను గానీ లేదా సేవ్ చేయని ఏవైనా మార్పులను గానీ కోల్పోవచ్చు"</string> + <string name="letterbox_restart_cancel" msgid="1342209132692537805">"రద్దు చేయండి"</string> + <string name="letterbox_restart_restart" msgid="8529976234412442973">"రీస్టార్ట్ చేయండి"</string> + <string name="letterbox_restart_dialog_checkbox_title" msgid="5252918008140768386">"మళ్లీ చూపవద్దు"</string> + <string name="letterbox_reachability_reposition_text" msgid="4507890186297500893">"ఈ యాప్ను తరలించడానికి డబుల్-ట్యాప్ చేయండి"</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> + <string name="back_button_text" msgid="1469718707134137085">"వెనుకకు"</string> + <string name="handle_text" msgid="1766582106752184456">"హ్యాండిల్"</string> + <string name="app_icon_text" msgid="2823268023931811747">"యాప్ చిహ్నం"</string> + <string name="fullscreen_text" msgid="1162316685217676079">"ఫుల్-స్క్రీన్"</string> + <string name="desktop_text" msgid="1077633567027630454">"డెస్క్టాప్ మోడ్"</string> + <string name="split_screen_text" msgid="1396336058129570886">"స్ప్లిట్ స్క్రీన్"</string> + <string name="more_button_text" msgid="3655388105592893530">"మరిన్ని"</string> + <string name="float_button_text" msgid="9221657008391364581">"ఫ్లోట్"</string> + <string name="select_text" msgid="5139083974039906583">"ఎంచుకోండి"</string> + <string name="screenshot_text" msgid="1477704010087786671">"స్క్రీన్షాట్"</string> + <string name="close_text" msgid="4986518933445178928">"మూసివేయండి"</string> + <string name="collapse_menu_text" msgid="7515008122450342029">"మెనూను మూసివేయండి"</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..d9237dff6dd8 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="7930546669915337998">"కంట్రోల్స్ కోసం "<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..a9b8086f4c73 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> @@ -31,11 +32,17 @@ <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"ปรับขนาด"</string> <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"เก็บเข้าที่เก็บส่วนตัว"</string> <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"เอาออกจากที่เก็บส่วนตัว"</string> - <string name="dock_forced_resizable" msgid="1749750436092293116">"แอปอาจใช้ไม่ได้กับโหมดแบ่งหน้าจอ"</string> - <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"แอปไม่สนับสนุนการแยกหน้าจอ"</string> + <!-- no translation found for dock_forced_resizable (7429086980048964687) --> + <skip /> + <!-- no translation found for dock_non_resizeble_failed_to_dock_text (2733543750291266047) --> + <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"แอปนี้เปิดได้ใน 1 หน้าต่างเท่านั้น"</string> <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> + <!-- no translation found for accessibility_divider (6407584574218956849) --> + <skip /> + <!-- no translation found for divider_title (1963391955593749442) --> + <skip /> <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> @@ -46,6 +53,10 @@ <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"ด้านบน 50%"</string> <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"ด้านบน 30%"</string> <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"เต็มหน้าจอด้านล่าง"</string> + <string name="accessibility_split_left" msgid="1713683765575562458">"แยกไปทางซ้าย"</string> + <string name="accessibility_split_right" msgid="8441001008181296837">"แยกไปทางขวา"</string> + <string name="accessibility_split_top" msgid="2789329702027147146">"แยกไปด้านบน"</string> + <string name="accessibility_split_bottom" msgid="8694551025220868191">"แยกไปด้านล่าง"</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> @@ -61,6 +72,7 @@ <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"ย้ายไปด้านขาวล่าง"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"การตั้งค่า <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"ปิดบับเบิล"</string> + <string name="bubbles_dont_bubble" msgid="3216183855437329223">"ไม่ต้องแสดงบับเบิล"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"ไม่ต้องแสดงการสนทนาเป็นบับเบิล"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"แชทโดยใช้บับเบิล"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"การสนทนาใหม่ๆ จะปรากฏเป็นไอคอนแบบลอยหรือบับเบิล แตะเพื่อเปิดบับเบิล ลากเพื่อย้ายที่"</string> @@ -72,17 +84,36 @@ <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) --> - <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) --> + <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"รับชมและทำสิ่งต่างๆ ได้มากขึ้น"</string> + <!-- no translation found for letterbox_education_split_screen_text (449233070804658627) --> <skip /> + <string name="letterbox_education_reposition_text" msgid="4589957299813220661">"แตะสองครั้งด้านนอกแอปเพื่อเปลี่ยนตำแหน่ง"</string> <string name="letterbox_education_got_it" msgid="4057634570866051177">"รับทราบ"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"ขยายเพื่อดูข้อมูลเพิ่มเติม"</string> + <string name="letterbox_restart_dialog_title" msgid="8543049527871033505">"รีสตาร์ทเพื่อรับมุมมองที่ดียิ่งขึ้นใช่ไหม"</string> + <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"คุณรีสตาร์ทแอปเพื่อรับมุมมองที่ดียิ่งขึ้นบนหน้าจอได้ แต่ความคืบหน้าและการเปลี่ยนแปลงใดๆ ที่ไม่ได้บันทึกอาจหายไป"</string> + <string name="letterbox_restart_cancel" msgid="1342209132692537805">"ยกเลิก"</string> + <string name="letterbox_restart_restart" msgid="8529976234412442973">"รีสตาร์ท"</string> + <string name="letterbox_restart_dialog_checkbox_title" msgid="5252918008140768386">"ไม่ต้องแสดงข้อความนี้อีก"</string> + <!-- no translation found for letterbox_reachability_reposition_text (4507890186297500893) --> + <skip /> + <string name="maximize_button_text" msgid="1650859196290301963">"ขยายใหญ่สุด"</string> + <string name="minimize_button_text" msgid="271592547935841753">"ย่อ"</string> + <string name="close_button_text" msgid="2913281996024033299">"ปิด"</string> + <string name="back_button_text" msgid="1469718707134137085">"กลับ"</string> + <string name="handle_text" msgid="1766582106752184456">"แฮนเดิล"</string> + <string name="app_icon_text" msgid="2823268023931811747">"ไอคอนแอป"</string> + <string name="fullscreen_text" msgid="1162316685217676079">"เต็มหน้าจอ"</string> + <string name="desktop_text" msgid="1077633567027630454">"โหมดเดสก์ท็อป"</string> + <string name="split_screen_text" msgid="1396336058129570886">"แยกหน้าจอ"</string> + <string name="more_button_text" msgid="3655388105592893530">"เพิ่มเติม"</string> + <string name="float_button_text" msgid="9221657008391364581">"ล่องลอย"</string> + <string name="select_text" msgid="5139083974039906583">"เลือก"</string> + <string name="screenshot_text" msgid="1477704010087786671">"ภาพหน้าจอ"</string> + <string name="close_text" msgid="4986518933445178928">"ปิด"</string> + <string name="collapse_menu_text" msgid="7515008122450342029">"ปิดเมนู"</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..47a6bd1be812 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="7930546669915337998">"กดปุ่ม "<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..9cf3576aae74 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> @@ -31,11 +32,17 @@ <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"I-resize"</string> <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"I-stash"</string> <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"I-unstash"</string> - <string name="dock_forced_resizable" msgid="1749750436092293116">"Posibleng hindi gumana ang app sa split screen."</string> - <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Hindi sinusuportahan ng app ang split-screen."</string> + <!-- no translation found for dock_forced_resizable (7429086980048964687) --> + <skip /> + <!-- no translation found for dock_non_resizeble_failed_to_dock_text (2733543750291266047) --> + <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Sa 1 window lang puwedeng buksan ang app na ito."</string> <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> + <!-- no translation found for accessibility_divider (6407584574218956849) --> + <skip /> + <!-- no translation found for divider_title (1963391955593749442) --> + <skip /> <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> @@ -46,6 +53,10 @@ <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Gawing 50% ang nasa itaas"</string> <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Gawing 30% ang nasa itaas"</string> <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"I-full screen ang nasa ibaba"</string> + <string name="accessibility_split_left" msgid="1713683765575562458">"Hatiin sa kaliwa"</string> + <string name="accessibility_split_right" msgid="8441001008181296837">"Hatiin sa kanan"</string> + <string name="accessibility_split_top" msgid="2789329702027147146">"Hatiin sa itaas"</string> + <string name="accessibility_split_bottom" msgid="8694551025220868191">"Hatiin sa ilalim"</string> <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Paggamit ng one-hand mode"</string> <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Para lumabas, mag-swipe pataas mula sa ibaba ng screen o mag-tap kahit saan sa itaas ng app"</string> <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Simulan ang one-hand mode"</string> @@ -61,6 +72,7 @@ <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Ilipat sa kanan sa ibaba"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"Mga setting ng <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"I-dismiss ang bubble"</string> + <string name="bubbles_dont_bubble" msgid="3216183855437329223">"Huwag i-bubble"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Huwag ipakita sa bubble ang mga pag-uusap"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Mag-chat gamit ang bubbles"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"Lumalabas bilang mga nakalutang na icon o bubble ang mga bagong pag-uusap. I-tap para buksan ang bubble. I-drag para ilipat ito."</string> @@ -72,17 +84,35 @@ <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) --> - <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) --> + <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Tumingin at gumawa ng higit pa"</string> + <!-- no translation found for letterbox_education_split_screen_text (449233070804658627) --> <skip /> + <string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Mag-double tap sa labas ng app para baguhin ang posisyon nito"</string> <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="letterbox_restart_dialog_title" msgid="8543049527871033505">"I-restart para sa mas magandang hitsura?"</string> + <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"Puwede mong i-restart ang app para maging mas maganda ang itsura nito sa iyong screen, pero posibleng mawala ang pag-usad mo o anumang hindi na-save na pagbabago"</string> + <string name="letterbox_restart_cancel" msgid="1342209132692537805">"Kanselahin"</string> + <string name="letterbox_restart_restart" msgid="8529976234412442973">"I-restart"</string> + <string name="letterbox_restart_dialog_checkbox_title" msgid="5252918008140768386">"Huwag nang ipakita ulit"</string> + <string name="letterbox_reachability_reposition_text" msgid="4507890186297500893">"I-double tap para ilipat ang app na ito"</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> + <string name="back_button_text" msgid="1469718707134137085">"Bumalik"</string> + <string name="handle_text" msgid="1766582106752184456">"Handle"</string> + <string name="app_icon_text" msgid="2823268023931811747">"Icon ng App"</string> + <string name="fullscreen_text" msgid="1162316685217676079">"Fullscreen"</string> + <string name="desktop_text" msgid="1077633567027630454">"Desktop Mode"</string> + <string name="split_screen_text" msgid="1396336058129570886">"Split Screen"</string> + <string name="more_button_text" msgid="3655388105592893530">"Higit pa"</string> + <string name="float_button_text" msgid="9221657008391364581">"Float"</string> + <string name="select_text" msgid="5139083974039906583">"Piliin"</string> + <string name="screenshot_text" msgid="1477704010087786671">"Screenshot"</string> + <string name="close_text" msgid="4986518933445178928">"Isara"</string> + <string name="collapse_menu_text" msgid="7515008122450342029">"Isara ang Menu"</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..2d890d4126b6 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="7930546669915337998">"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..4e398e57d558 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> @@ -31,11 +32,17 @@ <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Yeniden boyutlandır"</string> <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Depola"</string> <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Depolama"</string> - <string name="dock_forced_resizable" msgid="1749750436092293116">"Uygulama bölünmüş ekranda çalışmayabilir."</string> - <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Uygulama bölünmüş ekranı desteklemiyor."</string> + <!-- no translation found for dock_forced_resizable (7429086980048964687) --> + <skip /> + <!-- no translation found for dock_non_resizeble_failed_to_dock_text (2733543750291266047) --> + <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Bu uygulama yalnızca 1 pencerede açılabilir."</string> <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> + <!-- no translation found for accessibility_divider (6407584574218956849) --> + <skip /> + <!-- no translation found for divider_title (1963391955593749442) --> + <skip /> <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> @@ -46,6 +53,10 @@ <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Üstte %50"</string> <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Üstte %30"</string> <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Altta tam ekran"</string> + <string name="accessibility_split_left" msgid="1713683765575562458">"Sol tarafta böl"</string> + <string name="accessibility_split_right" msgid="8441001008181296837">"Sağ tarafta böl"</string> + <string name="accessibility_split_top" msgid="2789329702027147146">"Üst tarafta böl"</string> + <string name="accessibility_split_bottom" msgid="8694551025220868191">"Alt tarafta böl"</string> <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Tek el modunu kullanma"</string> <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Çıkmak için ekranın alt kısmından yukarı kaydırın veya uygulamanın üzerinde herhangi bir yere dokunun"</string> <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Tek el modunu başlat"</string> @@ -61,6 +72,7 @@ <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Sağ alta taşı"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> ayarları"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Baloncuğu kapat"</string> + <string name="bubbles_dont_bubble" msgid="3216183855437329223">"Bildirim baloncuğu gösterme"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Görüşmeyi baloncuk olarak görüntüleme"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Baloncukları kullanarak sohbet edin"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"Yeni görüşmeler kayan simgeler veya baloncuk olarak görünür. Açmak için baloncuğa dokunun. Baloncuğu taşımak için sürükleyin."</string> @@ -72,17 +84,36 @@ <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) --> - <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) --> + <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Daha fazlasını görün ve yapın"</string> + <!-- no translation found for letterbox_education_split_screen_text (449233070804658627) --> <skip /> + <string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Yeniden konumlandırmak için uygulamanın dışına iki kez dokunun"</string> <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="letterbox_restart_dialog_title" msgid="8543049527871033505">"Daha iyi bir görünüm için yeniden başlatılsın mı?"</string> + <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"Ekranınızda daha iyi görünmesi için uygulamayı yeniden başlatabilirsiniz, ancak ilerlemenizi ve kaydedilmemiş değişikliklerinizi kaybedebilirsiniz"</string> + <string name="letterbox_restart_cancel" msgid="1342209132692537805">"İptal"</string> + <string name="letterbox_restart_restart" msgid="8529976234412442973">"Yeniden başlat"</string> + <string name="letterbox_restart_dialog_checkbox_title" msgid="5252918008140768386">"Bir daha gösterme"</string> + <!-- no translation found for letterbox_reachability_reposition_text (4507890186297500893) --> + <skip /> + <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> + <string name="back_button_text" msgid="1469718707134137085">"Geri"</string> + <string name="handle_text" msgid="1766582106752184456">"Herkese açık kullanıcı adı"</string> + <string name="app_icon_text" msgid="2823268023931811747">"Uygulama Simgesi"</string> + <string name="fullscreen_text" msgid="1162316685217676079">"Tam Ekran"</string> + <string name="desktop_text" msgid="1077633567027630454">"Masaüstü Modu"</string> + <string name="split_screen_text" msgid="1396336058129570886">"Bölünmüş Ekran"</string> + <string name="more_button_text" msgid="3655388105592893530">"Daha Fazla"</string> + <string name="float_button_text" msgid="9221657008391364581">"Havada Süzülen"</string> + <string name="select_text" msgid="5139083974039906583">"Seç"</string> + <string name="screenshot_text" msgid="1477704010087786671">"Ekran görüntüsü"</string> + <string name="close_text" msgid="4986518933445178928">"Kapat"</string> + <string name="collapse_menu_text" msgid="7515008122450342029">"Menüyü 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..9e3e59b74c19 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="7930546669915337998">"Kontroller için "<annotation icon="home_icon">"ANA EKRAN"</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..adbf65648dd1 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_start_end_offset">30dp</dimen> <!-- outer space minus border width --> <dimen name="pip_menu_outer_space_frame">20dp</dimen> @@ -36,13 +36,17 @@ <dimen name="pip_menu_arrow_size">24dp</dimen> <dimen name="pip_menu_arrow_elevation">5dp</dimen> - <dimen name="pip_menu_elevation">1dp</dimen> + <dimen name="pip_menu_elevation_no_menu">1dp</dimen> + <dimen name="pip_menu_elevation_move_menu">7dp</dimen> + <dimen name="pip_menu_elevation_all_actions_menu">4dp</dimen> <dimen name="pip_menu_edu_text_view_height">24dp</dimen> <dimen name="pip_menu_edu_text_home_icon">9sp</dimen> <dimen name="pip_menu_edu_text_home_icon_outline">14sp</dimen> - <integer name="pip_edu_text_show_duration_ms">10500</integer> - <integer name="pip_edu_text_window_exit_animation_duration_ms">1000</integer> - <integer name="pip_edu_text_view_exit_animation_duration_ms">300</integer> + <integer name="pip_edu_text_scroll_times">2</integer> + <integer name="pip_edu_text_non_scroll_show_duration">10500</integer> + <integer name="pip_edu_text_start_scroll_delay">2000</integer> + <integer name="pip_edu_text_window_exit_animation_duration">1000</integer> + <integer name="pip_edu_text_view_exit_animation_duration">300</integer> </resources> diff --git a/libs/WindowManager/Shell/res/values-uk/strings.xml b/libs/WindowManager/Shell/res/values-uk/strings.xml index aac9031a7ca7..4ccb0bcbaf07 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> @@ -31,11 +32,17 @@ <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Змінити розмір"</string> <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Сховати"</string> <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Показати"</string> - <string name="dock_forced_resizable" msgid="1749750436092293116">"Додаток може не працювати в режимі розділеного екрана."</string> - <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Додаток не підтримує розділення екрана."</string> + <!-- no translation found for dock_forced_resizable (7429086980048964687) --> + <skip /> + <!-- no translation found for dock_non_resizeble_failed_to_dock_text (2733543750291266047) --> + <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Цей додаток можна відкрити лише в одному вікні."</string> <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> + <!-- no translation found for accessibility_divider (6407584574218956849) --> + <skip /> + <!-- no translation found for divider_title (1963391955593749442) --> + <skip /> <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> @@ -46,6 +53,10 @@ <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Верхнє вікно на 50%"</string> <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Верхнє вікно на 30%"</string> <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Нижнє вікно на весь екран"</string> + <string name="accessibility_split_left" msgid="1713683765575562458">"Розділити зліва"</string> + <string name="accessibility_split_right" msgid="8441001008181296837">"Розділити справа"</string> + <string name="accessibility_split_top" msgid="2789329702027147146">"Розділити вгорі"</string> + <string name="accessibility_split_bottom" msgid="8694551025220868191">"Розділити внизу"</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> @@ -61,6 +72,7 @@ <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Перемістити праворуч униз"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"Налаштування параметра \"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>\""</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Закрити підказку"</string> + <string name="bubbles_dont_bubble" msgid="3216183855437329223">"Не показувати спливаючі чати"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Не показувати спливаючі чати для розмов"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Спливаючий чат"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"Нові повідомлення чату з\'являються у вигляді спливаючих значків. Щоб відкрити чат, натисніть його, а щоб перемістити – перетягніть."</string> @@ -72,17 +84,36 @@ <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) --> - <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) --> + <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Більше простору та можливостей"</string> + <!-- no translation found for letterbox_education_split_screen_text (449233070804658627) --> <skip /> + <string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Щоб перемістити додаток, двічі торкніться області поза ним"</string> <string name="letterbox_education_got_it" msgid="4057634570866051177">"ОK"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Розгорніть, щоб дізнатися більше."</string> + <string name="letterbox_restart_dialog_title" msgid="8543049527871033505">"Перезапустити для зручнішого перегляду?"</string> + <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"Ви можете перезапустити додаток, щоб покращити його вигляд на екрані, але ваші досягнення або незбережені зміни може бути втрачено"</string> + <string name="letterbox_restart_cancel" msgid="1342209132692537805">"Скасувати"</string> + <string name="letterbox_restart_restart" msgid="8529976234412442973">"Перезапустити"</string> + <string name="letterbox_restart_dialog_checkbox_title" msgid="5252918008140768386">"Більше не показувати"</string> + <!-- no translation found for letterbox_reachability_reposition_text (4507890186297500893) --> + <skip /> + <string name="maximize_button_text" msgid="1650859196290301963">"Збільшити"</string> + <string name="minimize_button_text" msgid="271592547935841753">"Згорнути"</string> + <string name="close_button_text" msgid="2913281996024033299">"Закрити"</string> + <string name="back_button_text" msgid="1469718707134137085">"Назад"</string> + <string name="handle_text" msgid="1766582106752184456">"Маркер"</string> + <string name="app_icon_text" msgid="2823268023931811747">"Значок додатка"</string> + <string name="fullscreen_text" msgid="1162316685217676079">"На весь екран"</string> + <string name="desktop_text" msgid="1077633567027630454">"Режим комп’ютера"</string> + <string name="split_screen_text" msgid="1396336058129570886">"Розділити екран"</string> + <string name="more_button_text" msgid="3655388105592893530">"Більше"</string> + <string name="float_button_text" msgid="9221657008391364581">"Плаваюче вікно"</string> + <string name="select_text" msgid="5139083974039906583">"Вибрати"</string> + <string name="screenshot_text" msgid="1477704010087786671">"Знімок екрана"</string> + <string name="close_text" msgid="4986518933445178928">"Закрити"</string> + <string name="collapse_menu_text" msgid="7515008122450342029">"Закрити меню"</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..5edb26977fe7 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="7930546669915337998">"Відкрити елементи керування: двічі натисніть "<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..4aef27c45895 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> @@ -31,11 +32,17 @@ <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"سائز تبدیل کریں"</string> <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Stash"</string> <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Unstash"</string> - <string name="dock_forced_resizable" msgid="1749750436092293116">"ممکن ہے کہ ایپ اسپلٹ اسکرین کے ساتھ کام نہ کرے۔"</string> - <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"ایپ سپلٹ اسکرین کو سپورٹ نہیں کرتی۔"</string> + <!-- no translation found for dock_forced_resizable (7429086980048964687) --> + <skip /> + <!-- no translation found for dock_non_resizeble_failed_to_dock_text (2733543750291266047) --> + <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"یہ ایپ صرف 1 ونڈو میں کھولی جا سکتی ہے۔"</string> <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> + <!-- no translation found for accessibility_divider (6407584574218956849) --> + <skip /> + <!-- no translation found for divider_title (1963391955593749442) --> + <skip /> <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> @@ -46,6 +53,10 @@ <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"اوپر %50"</string> <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"اوپر %30"</string> <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"نچلی فل اسکرین"</string> + <string name="accessibility_split_left" msgid="1713683765575562458">"دائیں طرف تقسیم کریں"</string> + <string name="accessibility_split_right" msgid="8441001008181296837">"بائیں طرف تقسیم کریں"</string> + <string name="accessibility_split_top" msgid="2789329702027147146">"اوپر کی طرف تقسیم کریں"</string> + <string name="accessibility_split_bottom" msgid="8694551025220868191">"نیچے کی طرف تقسیم کریں"</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> @@ -61,6 +72,7 @@ <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"نیچے دائیں جانب لے جائیں"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> ترتیبات"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"بلبلہ برخاست کریں"</string> + <string name="bubbles_dont_bubble" msgid="3216183855437329223">"بلبلہ دکھانا بند کریں"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"گفتگو بلبلہ نہ کریں"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"بلبلے کے ذریعے چیٹ کریں"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"نئی گفتگوئیں فلوٹنگ آئیکن یا بلبلے کے طور پر ظاہر ہوں گی۔ بلبلہ کھولنے کے لیے تھپتھپائیں۔ اسے منتقل کرنے کے لیے گھسیٹیں۔"</string> @@ -72,17 +84,35 @@ <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) --> - <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) --> + <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"دیکھیں اور بہت کچھ کریں"</string> + <!-- no translation found for letterbox_education_split_screen_text (449233070804658627) --> <skip /> + <string name="letterbox_education_reposition_text" msgid="4589957299813220661">"کسی ایپ کی پوزیشن تبدیل کرنے کے لیے اس ایپ کے باہر دو بار تھپتھپائیں"</string> <string name="letterbox_education_got_it" msgid="4057634570866051177">"سمجھ آ گئی"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"مزید معلومات کے لیے پھیلائیں۔"</string> + <string name="letterbox_restart_dialog_title" msgid="8543049527871033505">"بہتر منظر کے لیے ری سٹارٹ کریں؟"</string> + <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"آپ ایپ کو ری سٹارٹ کر سکتے ہیں تاکہ یہ آپ کی اسکرین پر بہتر نظر آئے، تاہم آپ اپنی پیشرفت سے یا کسی غیر محفوظ شدہ تبدیلیوں سے محروم ہو سکتے ہیں"</string> + <string name="letterbox_restart_cancel" msgid="1342209132692537805">"منسوخ کریں"</string> + <string name="letterbox_restart_restart" msgid="8529976234412442973">"ری اسٹارٹ کریں"</string> + <string name="letterbox_restart_dialog_checkbox_title" msgid="5252918008140768386">"دوبارہ نہ دکھائیں"</string> + <string name="letterbox_reachability_reposition_text" msgid="4507890186297500893">"اس ایپ کو منتقل کرنے کیلئے دو بار تھپتھپائیں"</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> + <string name="back_button_text" msgid="1469718707134137085">"پیچھے"</string> + <string name="handle_text" msgid="1766582106752184456">"ہینڈل"</string> + <string name="app_icon_text" msgid="2823268023931811747">"ایپ کا آئیکن"</string> + <string name="fullscreen_text" msgid="1162316685217676079">"مکمل اسکرین"</string> + <string name="desktop_text" msgid="1077633567027630454">"ڈیسک ٹاپ موڈ"</string> + <string name="split_screen_text" msgid="1396336058129570886">"اسپلٹ اسکرین"</string> + <string name="more_button_text" msgid="3655388105592893530">"مزید"</string> + <string name="float_button_text" msgid="9221657008391364581">"فلوٹ"</string> + <string name="select_text" msgid="5139083974039906583">"منتخب کریں"</string> + <string name="screenshot_text" msgid="1477704010087786671">"اسکرین شاٹ"</string> + <string name="close_text" msgid="4986518933445178928">"بند کریں"</string> + <string name="collapse_menu_text" msgid="7515008122450342029">"مینو بند کریں"</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..42b9564ff549 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="7930546669915337998">"کنٹرولز کے لیے "<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..04cc7ee24abc 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> @@ -31,11 +32,17 @@ <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Oʻlchamini oʻzgartirish"</string> <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Berkitish"</string> <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Chiqarish"</string> - <string name="dock_forced_resizable" msgid="1749750436092293116">"Bu ilova ekranni ikkiga ajratish rejimini dastaklamaydi."</string> - <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Bu ilova ekranni bo‘lish xususiyatini qo‘llab-quvvatlamaydi."</string> + <!-- no translation found for dock_forced_resizable (7429086980048964687) --> + <skip /> + <!-- no translation found for dock_non_resizeble_failed_to_dock_text (2733543750291266047) --> + <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Bu ilovani faqat 1 ta oynada ochish mumkin."</string> <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> + <!-- no translation found for accessibility_divider (6407584574218956849) --> + <skip /> + <!-- no translation found for divider_title (1963391955593749442) --> + <skip /> <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> @@ -46,6 +53,10 @@ <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Tepada 50%"</string> <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Tepada 30%"</string> <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Pastda to‘liq ekran"</string> + <string name="accessibility_split_left" msgid="1713683765575562458">"Chapga ajratish"</string> + <string name="accessibility_split_right" msgid="8441001008181296837">"Oʻngga ajratish"</string> + <string name="accessibility_split_top" msgid="2789329702027147146">"Yuqoriga ajratish"</string> + <string name="accessibility_split_bottom" msgid="8694551025220868191">"Pastga ajratish"</string> <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Ixcham rejimdan foydalanish"</string> <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Chiqish uchun ekran pastidan tepaga suring yoki ilovaning tepasidagi istalgan joyga bosing."</string> <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Ixcham rejimni ishga tushirish"</string> @@ -61,6 +72,7 @@ <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Quyi oʻngga surish"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> sozlamalari"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Bulutchani yopish"</string> + <string name="bubbles_dont_bubble" msgid="3216183855437329223">"Qalqib chiqmasin"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Suhbatlar bulutchalar shaklida chiqmasin"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Bulutchalar yordamida subhatlashish"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"Yangi xabarlar qalqib chiquvchi belgilar yoki bulutchalar kabi chiqadi. Xabarni ochish uchun bildirishnoma ustiga bosing. Xabarni qayta joylash uchun bildirishnomani suring."</string> @@ -72,17 +84,35 @@ <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) --> - <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) --> + <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Yana boshqa amallar"</string> + <!-- no translation found for letterbox_education_split_screen_text (449233070804658627) --> <skip /> + <string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Qayta joylash uchun ilova tashqarisiga ikki marta bosing"</string> <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="letterbox_restart_dialog_title" msgid="8543049527871033505">"Yaxshi koʻrinishi uchun qayta ishga tushirilsinmi?"</string> + <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"Ilovani ekranda yaxshiroq koʻrinishi uchun qayta ishga tushirishingiz mumkin. Bunda jarayonlar yoki saqlanmagan oʻzgarishlar yoʻqolishi mumkin."</string> + <string name="letterbox_restart_cancel" msgid="1342209132692537805">"Bekor qilish"</string> + <string name="letterbox_restart_restart" msgid="8529976234412442973">"Qaytadan"</string> + <string name="letterbox_restart_dialog_checkbox_title" msgid="5252918008140768386">"Boshqa chiqmasin"</string> + <string name="letterbox_reachability_reposition_text" msgid="4507890186297500893">"Bu ilovaga olish uchun ikki marta bosing"</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> + <string name="back_button_text" msgid="1469718707134137085">"Orqaga"</string> + <string name="handle_text" msgid="1766582106752184456">"Identifikator"</string> + <string name="app_icon_text" msgid="2823268023931811747">"Ilova belgisi"</string> + <string name="fullscreen_text" msgid="1162316685217676079">"Butun ekran"</string> + <string name="desktop_text" msgid="1077633567027630454">"Desktop rejimi"</string> + <string name="split_screen_text" msgid="1396336058129570886">"Ekranni ikkiga ajratish"</string> + <string name="more_button_text" msgid="3655388105592893530">"Yana"</string> + <string name="float_button_text" msgid="9221657008391364581">"Pufakli"</string> + <string name="select_text" msgid="5139083974039906583">"Tanlash"</string> + <string name="screenshot_text" msgid="1477704010087786671">"Skrinshot"</string> + <string name="close_text" msgid="4986518933445178928">"Yopish"</string> + <string name="collapse_menu_text" msgid="7515008122450342029">"Menyuni 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..83fd8b4e5425 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="7930546669915337998">"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..a207471b4f1b 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> @@ -31,11 +32,17 @@ <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Đổi kích thước"</string> <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Ẩn"</string> <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Hiện"</string> - <string name="dock_forced_resizable" msgid="1749750436092293116">"Ứng dụng có thể không hoạt động với tính năng chia đôi màn hình."</string> - <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Ứng dụng không hỗ trợ chia đôi màn hình."</string> + <!-- no translation found for dock_forced_resizable (7429086980048964687) --> + <skip /> + <!-- no translation found for dock_non_resizeble_failed_to_dock_text (2733543750291266047) --> + <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Ứng dụng này chỉ có thể mở 1 cửa sổ."</string> <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> + <!-- no translation found for accessibility_divider (6407584574218956849) --> + <skip /> + <!-- no translation found for divider_title (1963391955593749442) --> + <skip /> <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> @@ -46,6 +53,10 @@ <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Trên 50%"</string> <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Trên 30%"</string> <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Toàn màn hình phía dưới"</string> + <string name="accessibility_split_left" msgid="1713683765575562458">"Chia đôi màn hình về bên trái"</string> + <string name="accessibility_split_right" msgid="8441001008181296837">"Chia đôi màn hình về bên phải"</string> + <string name="accessibility_split_top" msgid="2789329702027147146">"Chia đôi màn hình lên trên cùng"</string> + <string name="accessibility_split_bottom" msgid="8694551025220868191">"Chia đôi màn hình xuống dưới cùng"</string> <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Cách dùng chế độ một tay"</string> <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Để thoát, hãy vuốt lên từ cuối màn hình hoặc nhấn vào vị trí bất kỳ phía trên ứng dụng"</string> <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Bắt đầu chế độ một tay"</string> @@ -61,6 +72,7 @@ <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Chuyển tới dưới cùng bên phải"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"Cài đặt <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Đóng bong bóng"</string> + <string name="bubbles_dont_bubble" msgid="3216183855437329223">"Không hiện bong bóng trò chuyện"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Dừng sử dụng bong bóng cho cuộc trò chuyện"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Trò chuyện bằng bong bóng trò chuyện"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"Các cuộc trò chuyện mới sẽ xuất hiện dưới dạng biểu tượng nổi hoặc bong bóng trò chuyện. Nhấn để mở bong bóng trò chuyện. Kéo để di chuyển bong bóng trò chuyện."</string> @@ -72,17 +84,36 @@ <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) --> - <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) --> + <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Xem và làm được nhiều việc hơn"</string> + <!-- no translation found for letterbox_education_split_screen_text (449233070804658627) --> <skip /> + <string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Nhấn đúp bên ngoài ứng dụng để đặt lại vị trí"</string> <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="letterbox_restart_dialog_title" msgid="8543049527871033505">"Khởi động lại để ứng dụng trông vừa vặn hơn?"</string> + <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"Bạn có thể khởi động lại ứng dụng để ứng dụng nhìn đẹp hơn trên màn hình. Tuy nhiên, nếu làm vậy, bạn có thể mất tiến trình hoặc mọi thay đổi chưa lưu"</string> + <string name="letterbox_restart_cancel" msgid="1342209132692537805">"Huỷ"</string> + <string name="letterbox_restart_restart" msgid="8529976234412442973">"Khởi động lại"</string> + <string name="letterbox_restart_dialog_checkbox_title" msgid="5252918008140768386">"Không hiện lại"</string> + <!-- no translation found for letterbox_reachability_reposition_text (4507890186297500893) --> + <skip /> + <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> + <string name="back_button_text" msgid="1469718707134137085">"Quay lại"</string> + <string name="handle_text" msgid="1766582106752184456">"Xử lý"</string> + <string name="app_icon_text" msgid="2823268023931811747">"Biểu tượng ứng dụng"</string> + <string name="fullscreen_text" msgid="1162316685217676079">"Toàn màn hình"</string> + <string name="desktop_text" msgid="1077633567027630454">"Chế độ máy tính"</string> + <string name="split_screen_text" msgid="1396336058129570886">"Chia đôi màn hình"</string> + <string name="more_button_text" msgid="3655388105592893530">"Tuỳ chọn khác"</string> + <string name="float_button_text" msgid="9221657008391364581">"Nổi"</string> + <string name="select_text" msgid="5139083974039906583">"Chọn"</string> + <string name="screenshot_text" msgid="1477704010087786671">"Ảnh chụp màn hình"</string> + <string name="close_text" msgid="4986518933445178928">"Đóng"</string> + <string name="collapse_menu_text" msgid="7515008122450342029">"Đóng trình đơn"</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..986690f0444c 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="7930546669915337998">"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..f291bf3eb4d5 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> @@ -31,11 +32,17 @@ <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"调整大小"</string> <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"隐藏"</string> <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"取消隐藏"</string> - <string name="dock_forced_resizable" msgid="1749750436092293116">"应用可能无法在分屏模式下正常运行。"</string> - <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"应用不支持分屏。"</string> + <!-- no translation found for dock_forced_resizable (7429086980048964687) --> + <skip /> + <!-- no translation found for dock_non_resizeble_failed_to_dock_text (2733543750291266047) --> + <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"此应用只能在 1 个窗口中打开。"</string> <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> + <!-- no translation found for accessibility_divider (6407584574218956849) --> + <skip /> + <!-- no translation found for divider_title (1963391955593749442) --> + <skip /> <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> @@ -46,6 +53,10 @@ <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"顶部 50%"</string> <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"顶部 30%"</string> <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"底部全屏"</string> + <string name="accessibility_split_left" msgid="1713683765575562458">"左分屏"</string> + <string name="accessibility_split_right" msgid="8441001008181296837">"右分屏"</string> + <string name="accessibility_split_top" msgid="2789329702027147146">"上分屏"</string> + <string name="accessibility_split_bottom" msgid="8694551025220868191">"下分屏"</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> @@ -61,6 +72,7 @@ <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"移至右下角"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>设置"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"关闭对话泡"</string> + <string name="bubbles_dont_bubble" msgid="3216183855437329223">"不显示对话泡"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"不以对话泡形式显示对话"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"使用对话泡聊天"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"新对话会以浮动图标或对话泡形式显示。点按即可打开对话泡。拖动即可移动对话泡。"</string> @@ -72,17 +84,36 @@ <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) --> - <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) --> + <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"查看和处理更多任务"</string> + <!-- no translation found for letterbox_education_split_screen_text (449233070804658627) --> <skip /> + <string name="letterbox_education_reposition_text" msgid="4589957299813220661">"在某个应用外连续点按两次,即可调整它的位置"</string> <string name="letterbox_education_got_it" msgid="4057634570866051177">"知道了"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"展开即可了解详情。"</string> + <string name="letterbox_restart_dialog_title" msgid="8543049527871033505">"重启以改进外观?"</string> + <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"您可以重启应用,使其在屏幕上的显示效果更好,但您可能会丢失进度或任何未保存的更改"</string> + <string name="letterbox_restart_cancel" msgid="1342209132692537805">"取消"</string> + <string name="letterbox_restart_restart" msgid="8529976234412442973">"重启"</string> + <string name="letterbox_restart_dialog_checkbox_title" msgid="5252918008140768386">"不再显示"</string> + <!-- no translation found for letterbox_reachability_reposition_text (4507890186297500893) --> + <skip /> + <string name="maximize_button_text" msgid="1650859196290301963">"最大化"</string> + <string name="minimize_button_text" msgid="271592547935841753">"最小化"</string> + <string name="close_button_text" msgid="2913281996024033299">"关闭"</string> + <string name="back_button_text" msgid="1469718707134137085">"返回"</string> + <string name="handle_text" msgid="1766582106752184456">"处理"</string> + <string name="app_icon_text" msgid="2823268023931811747">"应用图标"</string> + <string name="fullscreen_text" msgid="1162316685217676079">"全屏"</string> + <string name="desktop_text" msgid="1077633567027630454">"桌面模式"</string> + <string name="split_screen_text" msgid="1396336058129570886">"分屏"</string> + <string name="more_button_text" msgid="3655388105592893530">"更多"</string> + <string name="float_button_text" msgid="9221657008391364581">"悬浮"</string> + <string name="select_text" msgid="5139083974039906583">"选择"</string> + <string name="screenshot_text" msgid="1477704010087786671">"屏幕截图"</string> + <string name="close_text" msgid="4986518933445178928">"关闭"</string> + <string name="collapse_menu_text" msgid="7515008122450342029">"关闭菜单"</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..4da96e89f136 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="7930546669915337998">"按两次"<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..ade02ba983ce 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> @@ -31,11 +32,17 @@ <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"調整大小"</string> <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"保護"</string> <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"取消保護"</string> - <string name="dock_forced_resizable" msgid="1749750436092293116">"應用程式可能無法在分割畫面中運作。"</string> - <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"應用程式不支援分割畫面。"</string> + <!-- no translation found for dock_forced_resizable (7429086980048964687) --> + <skip /> + <!-- no translation found for dock_non_resizeble_failed_to_dock_text (2733543750291266047) --> + <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"此應用程式只可在 1 個視窗中開啟"</string> <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> + <!-- no translation found for accessibility_divider (6407584574218956849) --> + <skip /> + <!-- no translation found for divider_title (1963391955593749442) --> + <skip /> <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> @@ -46,6 +53,10 @@ <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"頂部 50%"</string> <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"頂部 30%"</string> <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"底部全螢幕"</string> + <string name="accessibility_split_left" msgid="1713683765575562458">"分割左側區域"</string> + <string name="accessibility_split_right" msgid="8441001008181296837">"分割右側區域"</string> + <string name="accessibility_split_top" msgid="2789329702027147146">"分割上方區域"</string> + <string name="accessibility_split_bottom" msgid="8694551025220868191">"分割下方區域"</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> @@ -61,6 +72,7 @@ <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"移去右下角"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"「<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>」設定"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"關閉小視窗氣泡"</string> + <string name="bubbles_dont_bubble" msgid="3216183855437329223">"不要顯示對話氣泡"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"不要透過小視窗顯示對話"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"使用小視窗進行即時通訊"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"新對話會以浮動圖示 (小視窗) 顯示。輕按即可開啟小視窗。拖曳即可移動小視窗。"</string> @@ -72,17 +84,36 @@ <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) --> - <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) --> + <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"瀏覽更多內容及執行更多操作"</string> + <!-- no translation found for letterbox_education_split_screen_text (449233070804658627) --> <skip /> + <string name="letterbox_education_reposition_text" msgid="4589957299813220661">"在應用程式外輕按兩下即可調整位置"</string> <string name="letterbox_education_got_it" msgid="4057634570866051177">"知道了"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"展開即可查看詳情。"</string> + <string name="letterbox_restart_dialog_title" msgid="8543049527871033505">"要重新啟動改善檢視畫面嗎?"</string> + <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"您可重新啟動應用程式,讓系統更新檢視畫面;但系統可能不會儲存目前進度及您作出的任何變更"</string> + <string name="letterbox_restart_cancel" msgid="1342209132692537805">"取消"</string> + <string name="letterbox_restart_restart" msgid="8529976234412442973">"重新啟動"</string> + <string name="letterbox_restart_dialog_checkbox_title" msgid="5252918008140768386">"不要再顯示"</string> + <!-- no translation found for letterbox_reachability_reposition_text (4507890186297500893) --> + <skip /> + <string name="maximize_button_text" msgid="1650859196290301963">"最大化"</string> + <string name="minimize_button_text" msgid="271592547935841753">"最小化"</string> + <string name="close_button_text" msgid="2913281996024033299">"關閉"</string> + <string name="back_button_text" msgid="1469718707134137085">"返去"</string> + <string name="handle_text" msgid="1766582106752184456">"控點"</string> + <string name="app_icon_text" msgid="2823268023931811747">"應用程式圖示"</string> + <string name="fullscreen_text" msgid="1162316685217676079">"全螢幕"</string> + <string name="desktop_text" msgid="1077633567027630454">"桌面模式"</string> + <string name="split_screen_text" msgid="1396336058129570886">"分割螢幕"</string> + <string name="more_button_text" msgid="3655388105592893530">"更多"</string> + <string name="float_button_text" msgid="9221657008391364581">"浮動"</string> + <string name="select_text" msgid="5139083974039906583">"選取"</string> + <string name="screenshot_text" msgid="1477704010087786671">"螢幕截圖"</string> + <string name="close_text" msgid="4986518933445178928">"關閉"</string> + <string name="collapse_menu_text" msgid="7515008122450342029">"關閉選單"</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..ce850ef3c675 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="7930546669915337998">"按兩下"<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..a9b3beb84027 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> @@ -31,11 +32,17 @@ <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"調整大小"</string> <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"暫時隱藏"</string> <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"取消暫時隱藏"</string> - <string name="dock_forced_resizable" msgid="1749750436092293116">"應用程式可能無法在分割畫面中運作。"</string> - <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"這個應用程式不支援分割畫面。"</string> + <!-- no translation found for dock_forced_resizable (7429086980048964687) --> + <skip /> + <!-- no translation found for dock_non_resizeble_failed_to_dock_text (2733543750291266047) --> + <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"這個應用程式只能在 1 個視窗中開啟。"</string> <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> + <!-- no translation found for accessibility_divider (6407584574218956849) --> + <skip /> + <!-- no translation found for divider_title (1963391955593749442) --> + <skip /> <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> @@ -46,6 +53,10 @@ <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"以 50% 的螢幕空間顯示頂端畫面"</string> <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"以 30% 的螢幕空間顯示頂端畫面"</string> <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"以全螢幕顯示底部畫面"</string> + <string name="accessibility_split_left" msgid="1713683765575562458">"分割左側區域"</string> + <string name="accessibility_split_right" msgid="8441001008181296837">"分割右側區域"</string> + <string name="accessibility_split_top" msgid="2789329702027147146">"分割上方區域"</string> + <string name="accessibility_split_bottom" msgid="8694551025220868191">"分割下方區域"</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> @@ -61,6 +72,7 @@ <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"移至右下方"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"「<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>」設定"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"關閉對話框"</string> + <string name="bubbles_dont_bubble" msgid="3216183855437329223">"不要顯示對話框"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"不要以對話框形式顯示對話"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"透過對話框來聊天"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"新的對話會以浮動圖示或對話框形式顯示。輕觸即可開啟對話框,拖曳則可移動對話框。"</string> @@ -72,17 +84,36 @@ <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) --> - <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) --> + <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"瀏覽更多內容及執行更多操作"</string> + <!-- no translation found for letterbox_education_split_screen_text (449233070804658627) --> <skip /> + <string name="letterbox_education_reposition_text" msgid="4589957299813220661">"在應用程式外輕觸兩下即可調整位置"</string> <string name="letterbox_education_got_it" msgid="4057634570866051177">"我知道了"</string> + <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"展開即可查看詳細資訊。"</string> + <string name="letterbox_restart_dialog_title" msgid="8543049527871033505">"要重新啟動改善檢視畫面嗎?"</string> + <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"你可以重新啟動應用程式,讓系統更新檢視畫面。不過,系統可能不會儲存目前進度及你所做的任何變更"</string> + <string name="letterbox_restart_cancel" msgid="1342209132692537805">"取消"</string> + <string name="letterbox_restart_restart" msgid="8529976234412442973">"重新啟動"</string> + <string name="letterbox_restart_dialog_checkbox_title" msgid="5252918008140768386">"不要再顯示"</string> + <!-- no translation found for letterbox_reachability_reposition_text (4507890186297500893) --> + <skip /> + <string name="maximize_button_text" msgid="1650859196290301963">"最大化"</string> + <string name="minimize_button_text" msgid="271592547935841753">"最小化"</string> + <string name="close_button_text" msgid="2913281996024033299">"關閉"</string> + <string name="back_button_text" msgid="1469718707134137085">"返回"</string> + <string name="handle_text" msgid="1766582106752184456">"控點"</string> + <string name="app_icon_text" msgid="2823268023931811747">"應用程式圖示"</string> + <string name="fullscreen_text" msgid="1162316685217676079">"全螢幕"</string> + <string name="desktop_text" msgid="1077633567027630454">"電腦模式"</string> + <string name="split_screen_text" msgid="1396336058129570886">"分割畫面"</string> + <string name="more_button_text" msgid="3655388105592893530">"更多"</string> + <string name="float_button_text" msgid="9221657008391364581">"浮動"</string> + <string name="select_text" msgid="5139083974039906583">"選取"</string> + <string name="screenshot_text" msgid="1477704010087786671">"螢幕截圖"</string> + <string name="close_text" msgid="4986518933445178928">"關閉"</string> + <string name="collapse_menu_text" msgid="7515008122450342029">"關閉選單"</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..df870851e72b 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="7930546669915337998">"按兩下"<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..12a4703656e5 100644 --- a/libs/WindowManager/Shell/res/values-zu/strings.xml +++ b/libs/WindowManager/Shell/res/values-zu/strings.xml @@ -19,9 +19,10 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="pip_phone_close" msgid="5783752637260411309">"Vala"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Nweba"</string> - <string name="pip_phone_settings" msgid="5468987116750491918">"Izilungiselelo"</string> + <string name="pip_phone_settings" msgid="5468987116750491918">"Amasethingi"</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> @@ -31,11 +32,17 @@ <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Shintsha usayizi"</string> <string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Yenza isiteshi"</string> <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Susa isiteshi"</string> - <string name="dock_forced_resizable" msgid="1749750436092293116">"Izinhlelo zokusebenza kungenzeka zingasebenzi ngesikrini esihlukanisiwe."</string> - <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Uhlelo lokusebenza alusekeli isikrini esihlukanisiwe."</string> + <!-- no translation found for dock_forced_resizable (7429086980048964687) --> + <skip /> + <!-- no translation found for dock_non_resizeble_failed_to_dock_text (2733543750291266047) --> + <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Le-app ingavulwa kuphela ewindini eli-1."</string> <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> + <!-- no translation found for accessibility_divider (6407584574218956849) --> + <skip /> + <!-- no translation found for divider_title (1963391955593749442) --> + <skip /> <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> @@ -46,6 +53,10 @@ <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Okuphezulu okungu-50%"</string> <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Okuphezulu okungu-30%"</string> <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Ngaphansi kwesikrini esigcwele"</string> + <string name="accessibility_split_left" msgid="1713683765575562458">"Hlukanisa ngakwesobunxele"</string> + <string name="accessibility_split_right" msgid="8441001008181296837">"Hlukanisa ngakwesokudla"</string> + <string name="accessibility_split_top" msgid="2789329702027147146">"Hlukanisa phezulu"</string> + <string name="accessibility_split_bottom" msgid="8694551025220868191">"Hlukanisa phansi"</string> <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Ukusebenzisa imodi yesandla esisodwa"</string> <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Ukuze uphume, swayipha ngaphezulu kusuka ngezansi kwesikrini noma thepha noma kuphi ngenhla kohlelo lokusebenza"</string> <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Qalisa imodi yesandla esisodwa"</string> @@ -61,6 +72,7 @@ <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Hambisa inkinobho ngakwesokudla"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> izilungiselelo"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Cashisa ibhamuza"</string> + <string name="bubbles_dont_bubble" msgid="3216183855437329223">"Ungabhamuzi"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Ungayibhamuzi ingxoxo"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Xoxa usebenzisa amabhamuza"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"Izingxoxo ezintsha zivela njengezithonjana ezintantayo, noma amabhamuza. Thepha ukuze uvule ibhamuza. Hudula ukuze ulihambise."</string> @@ -72,17 +84,35 @@ <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) --> - <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) --> + <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Bona futhi wenze okuningi"</string> + <!-- no translation found for letterbox_education_split_screen_text (449233070804658627) --> <skip /> + <string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Thepha kabili ngaphandle kwe-app ukuze uyimise kabusha"</string> <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="letterbox_restart_dialog_title" msgid="8543049527871033505">"Qala kabusha ukuze uthole ukubuka okungcono?"</string> + <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"Ungakwazi ukuqala kabusha i-app ukuze ibukeke kangcono esikrinini sakho, kodwa ungase ulahlekelwe ukuqhubeka kwakho nanoma yiziphi izinguquko ezingalondoloziwe"</string> + <string name="letterbox_restart_cancel" msgid="1342209132692537805">"Khansela"</string> + <string name="letterbox_restart_restart" msgid="8529976234412442973">"Qala kabusha"</string> + <string name="letterbox_restart_dialog_checkbox_title" msgid="5252918008140768386">"Ungabonisi futhi"</string> + <string name="letterbox_reachability_reposition_text" msgid="4507890186297500893">"Thepha kabili ukuze uhambise le-app"</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> + <string name="back_button_text" msgid="1469718707134137085">"Emuva"</string> + <string name="handle_text" msgid="1766582106752184456">"Isibambo"</string> + <string name="app_icon_text" msgid="2823268023931811747">"Isithonjana Se-app"</string> + <string name="fullscreen_text" msgid="1162316685217676079">"Isikrini esigcwele"</string> + <string name="desktop_text" msgid="1077633567027630454">"Imodi Yedeskithophu"</string> + <string name="split_screen_text" msgid="1396336058129570886">"Hlukanisa isikrini"</string> + <string name="more_button_text" msgid="3655388105592893530">"Okwengeziwe"</string> + <string name="float_button_text" msgid="9221657008391364581">"Iflowuthi"</string> + <string name="select_text" msgid="5139083974039906583">"Khetha"</string> + <string name="screenshot_text" msgid="1477704010087786671">"Isithombe-skrini"</string> + <string name="close_text" msgid="4986518933445178928">"Vala"</string> + <string name="collapse_menu_text" msgid="7515008122450342029">"Vala Imenyu"</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..34cc8f1f1647 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="7930546669915337998">"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..fbb5caa508de 100644 --- a/libs/WindowManager/Shell/res/values/attrs.xml +++ b/libs/WindowManager/Shell/res/values/attrs.xml @@ -1,5 +1,5 @@ <!-- - ~ Copyright (C) 2022 The Android Open Source Project + ~ Copyright (C) 2023 The Android Open Source Project ~ ~ Licensed under the Apache License, Version 2.0 (the "License"); ~ you may not use this file except in compliance with the License. @@ -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.xml b/libs/WindowManager/Shell/res/values/colors.xml index 6e750a3d5e34..4b885c278a7a 100644 --- a/libs/WindowManager/Shell/res/values/colors.xml +++ b/libs/WindowManager/Shell/res/values/colors.xml @@ -17,7 +17,8 @@ */ --> <resources> - <color name="docked_divider_handle">#ffffff</color> + <color name="docked_divider_handle">#000000</color> + <color name="split_divider_background">@color/taskbar_background</color> <drawable name="forced_resizable_background">#59000000</drawable> <color name="minimize_dock_shadow_start">#60000000</color> <color name="minimize_dock_shadow_end">#00000000</color> @@ -41,6 +42,12 @@ <color name="letterbox_education_accent_primary">@android:color/system_accent1_100</color> <color name="letterbox_education_text_secondary">@android:color/system_neutral2_200</color> + <!-- Letterbox Dialog --> + <color name="letterbox_dialog_background">@android:color/system_neutral1_900</color> + + <!-- Reachability Education color for hand icon and text--> + <color name="letterbox_reachability_education_item_color">#BFC8CC</color> + <!-- GM2 colors --> <color name="GM2_grey_200">#E8EAED</color> <color name="GM2_grey_700">#5F6368</color> @@ -50,4 +57,17 @@ <color name="splash_screen_bg_light">#FFFFFF</color> <color name="splash_screen_bg_dark">#000000</color> <color name="splash_window_background_default">@color/splash_screen_bg_light</color> + + <!-- Desktop Mode --> + <color name="desktop_mode_caption_handle_bar_light">#EFF1F2</color> + <color name="desktop_mode_caption_handle_bar_dark">#1C1C17</color> + <color name="desktop_mode_caption_expand_button_light">#EFF1F2</color> + <color name="desktop_mode_caption_expand_button_dark">#48473A</color> + <color name="desktop_mode_caption_close_button_light">#EFF1F2</color> + <color name="desktop_mode_caption_close_button_dark">#1C1C17</color> + <color name="desktop_mode_caption_app_name_light">#EFF1F2</color> + <color name="desktop_mode_caption_app_name_dark">#1C1C17</color> + <color name="desktop_mode_caption_menu_text_color">#191C1D</color> + <color name="desktop_mode_caption_menu_buttons_color_inactive">#191C1D</color> + <color name="desktop_mode_caption_menu_buttons_color_active">#00677E</color> </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/config.xml b/libs/WindowManager/Shell/res/values/config.xml index f03b7f66cdc8..76eb0945d990 100644 --- a/libs/WindowManager/Shell/res/values/config.xml +++ b/libs/WindowManager/Shell/res/values/config.xml @@ -19,6 +19,14 @@ by the resources of the app using the Shell library. --> <bool name="config_enableShellMainThread">false</bool> + <!-- Determines whether to register the shell task organizer on init. + TODO(b/238217847): This config is temporary until we refactor the base WMComponent. --> + <bool name="config_registerShellTaskOrganizerOnInit">true</bool> + + <!-- Determines whether to register the shell transitions on init. + TODO(b/238217847): This config is temporary until we refactor the base WMComponent. --> + <bool name="config_registerShellTransitionsOnInit">true</bool> + <!-- Animation duration for PIP when entering. --> <integer name="config_pipEnterAnimationDuration">425</integer> @@ -97,10 +105,24 @@ 1.777778 </item> + <!-- The aspect ratio that by which optimizations to large screen sizes are made. + Needs to be less that or equal to 1. --> + <item name="config_pipLargeScreenOptimizedAspectRatio" format="float" type="dimen">0.5625</item> + <!-- The default gravity for the picture-in-picture window. Currently, this maps to Gravity.BOTTOM | Gravity.RIGHT --> <integer name="config_defaultPictureInPictureGravity">0x55</integer> <!-- Whether to dim a split-screen task when the other is the IME target --> <bool name="config_dimNonImeAttachedSide">true</bool> + + <!-- Components support to launch multiple instances into split-screen --> + <string-array name="config_appsSupportMultiInstancesSplit"> + </string-array> + + <!-- Whether the extended restart dialog is enabled --> + <bool name="config_letterboxIsRestartDialogEnabled">false</bool> + + <!-- Whether the additional education about reachability is enabled --> + <bool name="config_letterboxIsReachabilityEducationEnabled">false</bool> </resources> diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml index 1dac9caba01e..9049ed574ba5 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> @@ -223,9 +226,11 @@ <dimen name="bubble_user_education_padding_end">58dp</dimen> <!-- Padding between the bubble and the user education text. --> <dimen name="bubble_user_education_stack_padding">16dp</dimen> + <!-- Size of the bubble bar (height), should match transient_taskbar_size in Launcher. --> + <dimen name="bubblebar_size">72dp</dimen> <!-- Bottom and end margin for compat buttons. --> - <dimen name="compat_button_margin">16dp</dimen> + <dimen name="compat_button_margin">24dp</dimen> <!-- The radius of the corners of the compat hint bubble. --> <dimen name="compat_hint_corner_radius">28dp</dimen> @@ -246,8 +251,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> @@ -261,6 +275,69 @@ <!-- The space between two actions in the letterbox education dialog --> <dimen name="letterbox_education_dialog_space_between_actions">24dp</dimen> + <!-- The corner radius of the buttons in the letterbox education dialog --> + <dimen name="letterbox_education_dialog_button_radius">12dp</dimen> + + <!-- The horizontal padding for the buttons in the letterbox education dialog --> + <dimen name="letterbox_education_dialog_horizontal_padding">16dp</dimen> + + <!-- The vertical padding for the buttons in the letterbox education dialog --> + <dimen name="letterbox_education_dialog_vertical_padding">8dp</dimen> + + <!-- The insets for the buttons in the letterbox education dialog --> + <dimen name="letterbox_education_dialog_vertical_inset">6dp</dimen> + + <!-- The margin between the dialog container and its parent. --> + <dimen name="letterbox_restart_dialog_margin">24dp</dimen> + + <!-- The corner radius of the restart confirmation dialog. --> + <dimen name="letterbox_restart_dialog_corner_radius">28dp</dimen> + + <!-- The fixed width of the dialog if there is enough space in the parent. --> + <dimen name="letterbox_restart_dialog_width">348dp</dimen> + + <!-- The width of the top icon in the restart confirmation dialog. --> + <dimen name="letterbox_restart_dialog_title_icon_width">32dp</dimen> + + <!-- The height of the top icon in the restart confirmation dialog. --> + <dimen name="letterbox_restart_dialog_title_icon_height">32dp</dimen> + + <!-- The width of an icon in the restart confirmation dialog. --> + <dimen name="letterbox_restart_dialog_icon_width">40dp</dimen> + + <!-- The height of an icon in the restart confirmation dialog. --> + <dimen name="letterbox_restart_dialog_icon_height">32dp</dimen> + + <!-- The space between two actions in the restart confirmation dialog --> + <dimen name="letterbox_restart_dialog_space_between_actions">24dp</dimen> + + <!-- The width of the buttons in the restart dialog --> + <dimen name="letterbox_restart_dialog_button_width">82dp</dimen> + + <!-- The width of the buttons in the restart dialog --> + <dimen name="letterbox_restart_dialog_button_height">36dp</dimen> + + <!-- The corner radius of the buttons in the restart dialog --> + <dimen name="letterbox_restart_dialog_button_radius">18dp</dimen> + + <!-- The insets for the buttons in the letterbox restart dialog --> + <dimen name="letterbox_restart_dialog_vertical_inset">6dp</dimen> + + <!-- The horizontal padding for the buttons in the letterbox restart dialog --> + <dimen name="letterbox_restart_dialog_horizontal_padding">16dp</dimen> + + <!-- The vertical padding for the buttons in the letterbox restart dialog --> + <dimen name="letterbox_restart_dialog_vertical_padding">8dp</dimen> + + <!-- The margin between the reachability dialog container and its parent. --> + <dimen name="letterbox_reachability_education_dialog_margin">16dp</dimen> + + <!-- The width of each item in the reachability education --> + <dimen name="letterbox_reachability_education_item_width">118dp</dimen> + + <!-- The size of the icon in the item of reachability education --> + <dimen name="letterbox_reachability_education_item_image_size">24dp</dimen> + <!-- The width of the brand image on staring surface. --> <dimen name="starting_surface_brand_image_width">200dp</dimen> @@ -285,4 +362,44 @@ when the pinned stack size is overridden by app via minWidth/minHeight. --> <dimen name="overridable_minimal_size_pip_resizable_task">48dp</dimen> + + <!-- The thickness of shadows of a window that has focus in DIP. --> + <dimen name="freeform_decor_shadow_focused_thickness">20dp</dimen> + + <!-- The thickness of shadows of a window that doesn't have focus in DIP. --> + <dimen name="freeform_decor_shadow_unfocused_thickness">5dp</dimen> + + <!-- Height of button (32dp) + 2 * margin (5dp each). --> + <dimen name="freeform_decor_caption_height">42dp</dimen> + + <!-- The width of the handle menu in desktop mode. --> + <dimen name="desktop_mode_handle_menu_width">216dp</dimen> + + <!-- The height of the handle menu's "App Info" pill in desktop mode. --> + <dimen name="desktop_mode_handle_menu_app_info_pill_height">52dp</dimen> + + <!-- The height of the handle menu's "Windowing" pill in desktop mode. --> + <dimen name="desktop_mode_handle_menu_windowing_pill_height">52dp</dimen> + + <!-- The height of the handle menu's "More Actions" pill in desktop mode. --> + <dimen name="desktop_mode_handle_menu_more_actions_pill_height">156dp</dimen> + + <!-- The top margin of the handle menu in desktop mode. --> + <dimen name="desktop_mode_handle_menu_margin_top">4dp</dimen> + + <!-- The start margin of the handle menu in desktop mode. --> + <dimen name="desktop_mode_handle_menu_margin_start">6dp</dimen> + + <!-- The margin between pills of the handle menu in desktop mode. --> + <dimen name="desktop_mode_handle_menu_pill_spacing_margin">2dp</dimen> + + <!-- The radius of the caption menu corners. --> + <dimen name="desktop_mode_handle_menu_corner_radius">26dp</dimen> + + <!-- The radius of the caption menu shadow. --> + <dimen name="desktop_mode_handle_menu_shadow_radius">2dp</dimen> + + <dimen name="freeform_resize_handle">30dp</dimen> + + <dimen name="freeform_resize_corner">44dp</dimen> </resources> diff --git a/libs/WindowManager/Shell/res/values/strings.xml b/libs/WindowManager/Shell/res/values/strings.xml index a24311fb1f21..563fb4d88941 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> @@ -64,17 +67,21 @@ <string name="pip_phone_dismiss_hint">Drag down to dismiss</string> <!-- Multi-Window strings --> - <!-- Text that gets shown on top of current activity to inform the user that the system force-resized the current activity to be displayed in split-screen and that things might crash/not work properly [CHAR LIMIT=NONE] --> - <string name="dock_forced_resizable">App may not work with split-screen.</string> - <!-- Warning message when we try to dock a non-resizeable task and launch it in fullscreen instead. --> - <string name="dock_non_resizeble_failed_to_dock_text">App does not support split-screen.</string> + <!-- Text that gets shown on top of current activity to inform the user that the system force-resized the current activity to be displayed in split screen and that things might crash/not work properly [CHAR LIMIT=NONE] --> + <string name="dock_forced_resizable">App may not work with split screen</string> + <!-- Warning message when we try to dock a non-resizeable task and launch it in fullscreen instead [CHAR LIMIT=NONE] --> + <string name="dock_non_resizeble_failed_to_dock_text">App does not support split screen</string> + <!-- Warning message when we try to dock an app not supporting multiple instances split into multiple sides [CHAR LIMIT=NONE] --> + <string name="dock_multi_instances_not_supported_text">This app can only be opened in 1 window.</string> <!-- Text that gets shown on top of current activity to inform the user that the system force-resized the current activity to be displayed on a secondary display and that things might crash/not work properly [CHAR LIMIT=NONE] --> <string name="forced_resizable_secondary_display">App may not work on a secondary display.</string> <!-- 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] --> - <string name="accessibility_divider">Split-screen divider</string> + <!-- 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> @@ -98,6 +105,15 @@ <!-- Accessibility action for moving docked stack divider to make the bottom screen full screen [CHAR LIMIT=NONE] --> <string name="accessibility_action_divider_bottom_full">Bottom full screen</string> + <!-- Accessibility label for splitting to the left drop zone [CHAR LIMIT=NONE] --> + <string name="accessibility_split_left">Split left</string> + <!-- Accessibility label for splitting to the right drop zone [CHAR LIMIT=NONE] --> + <string name="accessibility_split_right">Split right</string> + <!-- Accessibility label for splitting to the top drop zone [CHAR LIMIT=NONE] --> + <string name="accessibility_split_top">Split top</string> + <!-- Accessibility label for splitting to the bottom drop zone [CHAR LIMIT=NONE] --> + <string name="accessibility_split_bottom">Split bottom</string> + <!-- One-Handed Tutorial title [CHAR LIMIT=60] --> <string name="one_handed_tutorial_title">Using one-handed mode</string> <!-- One-Handed Tutorial description [CHAR LIMIT=NONE] --> @@ -130,6 +146,8 @@ <string name="bubbles_app_settings"><xliff:g id="notification_title" example="Android Messages">%1$s</xliff:g> settings</string> <!-- Text used for the bubble dismiss area. Bubbles dragged to, or flung towards, this area will go away. [CHAR LIMIT=30] --> <string name="bubble_dismiss_text">Dismiss bubble</string> + <!-- Button text to stop an app from bubbling [CHAR LIMIT=60]--> + <string name="bubbles_dont_bubble">Don\u2019t bubble</string> <!-- Button text to stop a conversation from bubbling [CHAR LIMIT=60]--> <string name="bubbles_dont_bubble_conversation">Don\u2019t bubble conversation</string> <!-- Title text for the bubbles feature education cling shown when a bubble is on screen for the first time. [CHAR LIMIT=60]--> @@ -157,7 +175,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 +190,79 @@ <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> + + <!-- The title of the restart confirmation dialog. [CHAR LIMIT=NONE] --> + <string name="letterbox_restart_dialog_title">Restart for a better view?</string> + + <!-- The description of the restart confirmation dialog. [CHAR LIMIT=NONE] --> + <string name="letterbox_restart_dialog_description">You can restart the app so it looks better on + your screen, but you may lose your progress or any unsaved changes + </string> + + <!-- Button text for dismissing the restart confirmation dialog. [CHAR LIMIT=20] --> + <string name="letterbox_restart_cancel">Cancel</string> + + <!-- Button text for dismissing the restart confirmation dialog. [CHAR LIMIT=20] --> + <string name="letterbox_restart_restart">Restart</string> + + <!-- Checkbox text for asking to not show the restart confirmation dialog again. [CHAR LIMIT=NONE] --> + <string name="letterbox_restart_dialog_checkbox_title">Don\u2019t show again</string> + + <!-- When an app is letterboxed, it is initially centered on the screen but the user can + double tap to move the app to a different position. With a double-tap on the right, + the app moves the right of the screen and with a double-tap on the left the app moves + on the left. The same happens if the app has space to be moved to the top or bottom of + the screen. This time the double-tap can happen on the top or bottom of the screen. + To teach the user about this feature, we display an education explaining how the double-tap + works and how the app can be moved on the screen. + This is the text we show to the user below an animated icon visualizing the double-tap + action. [CHAR LIMIT=NONE] --> + <string name="letterbox_reachability_reposition_text">Double-tap to move this app</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> + <!-- Accessibility text for the caption back button [CHAR LIMIT=NONE] --> + <string name="back_button_text">Back</string> + <!-- Accessibility text for the caption handle [CHAR LIMIT=NONE] --> + <string name="handle_text">Handle</string> + <!-- Accessibility text for the handle menu app icon [CHAR LIMIT=NONE] --> + <string name="app_icon_text">App Icon</string> + <!-- Accessibility text for the handle fullscreen button [CHAR LIMIT=NONE] --> + <string name="fullscreen_text">Fullscreen</string> + <!-- Accessibility text for the handle desktop button [CHAR LIMIT=NONE] --> + <string name="desktop_text">Desktop Mode</string> + <!-- Accessibility text for the handle split screen button [CHAR LIMIT=NONE] --> + <string name="split_screen_text">Split Screen</string> + <!-- Accessibility text for the handle more options button [CHAR LIMIT=NONE] --> + <string name="more_button_text">More</string> + <!-- Accessibility text for the handle floating window button [CHAR LIMIT=NONE] --> + <string name="float_button_text">Float</string> + <!-- Accessibility text for the handle menu select button [CHAR LIMIT=NONE] --> + <string name="select_text">Select</string> + <!-- Accessibility text for the handle menu screenshot button [CHAR LIMIT=NONE] --> + <string name="screenshot_text">Screenshot</string> + <!-- Accessibility text for the handle menu close button [CHAR LIMIT=NONE] --> + <string name="close_text">Close</string> + <!-- Accessibility text for the handle menu close menu button [CHAR LIMIT=NONE] --> + <string name="collapse_menu_text">Close Menu</string> + <!-- Accessibility text for the handle menu open menu button [CHAR LIMIT=NONE] --> + <string name="expand_menu_text">Open Menu</string> </resources> diff --git a/libs/WindowManager/Shell/res/values/strings_tv.xml b/libs/WindowManager/Shell/res/values/strings_tv.xml index 2b7a13eac6ca..8f806cf56c9b 100644 --- a/libs/WindowManager/Shell/res/values/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values/strings_tv.xml @@ -42,8 +42,8 @@ <!-- Educative text instructing the user to double press the HOME button to access the pip controls menu [CHAR LIMIT=50] --> - <string name="pip_edu_text"> Double press <annotation icon="home_icon"> HOME </annotation> for - controls </string> + <string name="pip_edu_text">Double press <annotation icon="home_icon">HOME</annotation> for + controls</string> <!-- Accessibility announcement when opening the PiP menu. [CHAR LIMIT=NONE] --> <string name="a11y_pip_menu_entered">Picture-in-Picture menu.</string> diff --git a/libs/WindowManager/Shell/res/values/styles.xml b/libs/WindowManager/Shell/res/values/styles.xml index 19f7c3ef4364..8cad385e1d3f 100644 --- a/libs/WindowManager/Shell/res/values/styles.xml +++ b/libs/WindowManager/Shell/res/values/styles.xml @@ -30,6 +30,33 @@ <item name="android:activityCloseExitAnimation">@anim/forced_resizable_exit</item> </style> + <style name="DesktopModeHandleMenuActionButton"> + <item name="android:layout_width">match_parent</item> + <item name="android:layout_height">52dp</item> + <item name="android:gravity">start|center_vertical</item> + <item name="android:padding">16dp</item> + <item name="android:textSize">14sp</item> + <item name="android:textFontWeight">500</item> + <item name="android:textColor">@color/desktop_mode_caption_menu_text_color</item> + <item name="android:drawablePadding">16dp</item> + <item name="android:background">?android:selectableItemBackground</item> + </style> + + <style name="DesktopModeHandleMenuWindowingButton"> + <item name="android:layout_width">48dp</item> + <item name="android:layout_height">48dp</item> + <item name="android:padding">14dp</item> + <item name="android:scaleType">fitCenter</item> + <item name="android:background">?android:selectableItemBackgroundBorderless</item> + </style> + + <style name="CaptionButtonStyle"> + <item name="android:layout_width">32dp</item> + <item name="android:layout_height">32dp</item> + <item name="android:layout_margin">5dp</item> + <item name="android:padding">4dp</item> + </style> + <style name="DockedDividerBackground"> <item name="android:layout_width">match_parent</item> <item name="android:layout_height">@dimen/split_divider_bar_width</item> @@ -56,4 +83,87 @@ <item name="android:lineHeight">16sp</item> <item name="android:textColor">@color/tv_pip_edu_text</item> </style> + + <style name="LetterboxDialog" parent="@android:style/Theme.Holo"> + <item name="android:layout_width">wrap_content</item> + <item name="android:layout_height">wrap_content</item> + <item name="android:background">@color/letterbox_dialog_background</item> + </style> + + <style name="RestartDialogTitleText"> + <item name="android:textSize">24sp</item> + <item name="android:textColor">?android:attr/textColorPrimary</item> + <item name="android:lineSpacingExtra">2sp</item> + <item name="android:textAppearance"> + @*android:style/TextAppearance.DeviceDefault.Headline + </item> + <item name="android:fontFamily"> + @*android:string/config_bodyFontFamilyMedium + </item> + </style> + + <style name="RestartDialogBodyText"> + <item name="android:textSize">14sp</item> + <item name="android:letterSpacing">0.02</item> + <item name="android:textColor">?android:attr/textColorSecondary</item> + <item name="android:lineSpacingExtra">2sp</item> + <item name="android:textAppearance"> + @*android:style/TextAppearance.DeviceDefault.Body2 + </item> + <item name="android:fontFamily"> + @*android:string/config_bodyFontFamily + </item> + </style> + + <style name="RestartDialogCheckboxText"> + <item name="android:textSize">16sp</item> + <item name="android:textColor">?android:attr/textColorPrimary</item> + <item name="android:lineSpacingExtra">4sp</item> + <item name="android:textAppearance"> + @*android:style/TextAppearance.DeviceDefault.Headline + </item> + <item name="android:fontFamily"> + @*android:string/config_bodyFontFamilyMedium + </item> + </style> + + <style name="RestartDialogDismissButton"> + <item name="android:lineSpacingExtra">2sp</item> + <item name="android:textSize">14sp</item> + <item name="android:textColor">?android:attr/textColorPrimary</item> + <item name="android:textAppearance"> + @*android:style/TextAppearance.DeviceDefault.Body2 + </item> + <item name="android:fontFamily"> + @*android:string/config_bodyFontFamily + </item> + </style> + + <style name="RestartDialogConfirmButton"> + <item name="android:lineSpacingExtra">2sp</item> + <item name="android:textSize">14sp</item> + <item name="android:textColor">?android:attr/textColorPrimaryInverse</item> + <item name="android:textAppearance"> + @*android:style/TextAppearance.DeviceDefault.Body2 + </item> + <item name="android:fontFamily"> + @*android:string/config_bodyFontFamily + </item> + </style> + + <style name="ReachabilityEduHandLayout" parent="Theme.AppCompat"> + <item name="android:focusable">false</item> + <item name="android:focusableInTouchMode">false</item> + <item name="android:background">@android:color/transparent</item> + <item name="android:contentDescription">@string/restart_button_description</item> + <item name="android:visibility">invisible</item> + <item name="android:lineSpacingExtra">-1sp</item> + <item name="android:textSize">12sp</item> + <item name="android:textAlignment">center</item> + <item name="android:textColor">@color/letterbox_reachability_education_item_color</item> + <item name="android:textAppearance"> + @*android:style/TextAppearance.DeviceDefault.Body2 + </item> + </style> + </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..88525aabe53b --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/ProtoLogController.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; + +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; + } + case "save-for-bugreport": { + if (!mShellProtoLog.isProtoEnabled()) { + pw.println("Logging to proto is not enabled for WMShell."); + return false; + } + mShellProtoLog.stopProtoLog(pw, true /* writeToFile */); + mShellProtoLog.startProtoLog(pw); + return true; + } + 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."); + pw.println(prefix + "save-for-bugreport"); + pw.println(prefix + " Flush proto logging to file, only if it's enabled."); + } +} 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..34bf9e0dd98f 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; @@ -70,6 +76,7 @@ public class RootDisplayAreaOrganizer extends DisplayAreaOrganizer { + " mDisplayAreasInfo.get():" + mDisplayAreasInfo.get(displayId)); } + leash.setUnreleasedWarningCallSite("RootDisplayAreaOrganizer.onDisplayAreaAppeared"); mDisplayAreasInfo.put(displayId, displayAreaInfo); mLeashes.put(displayId, leash); } @@ -85,6 +92,8 @@ public class RootDisplayAreaOrganizer extends DisplayAreaOrganizer { } mDisplayAreasInfo.remove(displayId); + mLeashes.get(displayId).release(); + mLeashes.remove(displayId); } @Override @@ -100,10 +109,44 @@ public class RootDisplayAreaOrganizer extends DisplayAreaOrganizer { mDisplayAreasInfo.put(displayId, displayAreaInfo); } + /** + * Create a {@link WindowContainerTransaction} to update display windowing mode. + * + * @param displayId display id to update windowing mode for + * @param windowingMode target {@link WindowConfiguration.WindowingMode} + * @return {@link WindowContainerTransaction} with pending operation to set windowing mode + */ + public WindowContainerTransaction prepareWindowingModeChange(int displayId, + @WindowConfiguration.WindowingMode int windowingMode) { + WindowContainerTransaction wct = new WindowContainerTransaction(); + DisplayAreaInfo displayAreaInfo = mDisplayAreasInfo.get(displayId); + if (displayAreaInfo == null) { + ProtoLog.e(WM_SHELL_DESKTOP_MODE, + "unable to update windowing mode for display %d display not found", displayId); + return wct; + } + + ProtoLog.d(WM_SHELL_DESKTOP_MODE, + "setWindowingMode: displayId=%d current wmMode=%d new wmMode=%d", displayId, + displayAreaInfo.configuration.windowConfiguration.getWindowingMode(), + windowingMode); + + wct.setWindowingMode(displayAreaInfo.token, windowingMode); + return wct; + } + public void dump(@NonNull PrintWriter pw, String prefix) { final String innerPrefix = prefix + " "; final String childPrefix = innerPrefix + " "; pw.println(prefix + this); + + for (int i = 0; i < mDisplayAreasInfo.size(); i++) { + int displayId = mDisplayAreasInfo.keyAt(i); + DisplayAreaInfo displayAreaInfo = mDisplayAreasInfo.get(displayId); + int windowingMode = + displayAreaInfo.configuration.windowConfiguration.getWindowingMode(); + pw.println(innerPrefix + "# displayId=" + displayId + " wmMode=" + windowingMode); + } } @Override diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/RootTaskDisplayAreaOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/RootTaskDisplayAreaOrganizer.java index 9230c22c5d95..544d75739547 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/RootTaskDisplayAreaOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/RootTaskDisplayAreaOrganizer.java @@ -122,6 +122,8 @@ public class RootTaskDisplayAreaOrganizer extends DisplayAreaOrganizer { + " mDisplayAreasInfo.get():" + mDisplayAreasInfo.get(displayId)); } + leash.setUnreleasedWarningCallSite( + "RootTaskDisplayAreaOrganizer.onDisplayAreaAppeared"); mDisplayAreasInfo.put(displayId, displayAreaInfo); mLeashes.put(displayId, leash); @@ -179,6 +181,14 @@ public class RootTaskDisplayAreaOrganizer extends DisplayAreaOrganizer { } /** + * Returns the {@link DisplayAreaInfo} of the {@link DisplayAreaInfo#displayId}. + */ + @Nullable + public DisplayAreaInfo getDisplayAreaInfo(int displayId) { + return mDisplayAreasInfo.get(displayId); + } + + /** * Applies the {@link DisplayAreaInfo} to the {@link DisplayAreaContext} specified by * {@link DisplayAreaInfo#displayId}. */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/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..b6fd0bbafc71 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java @@ -23,6 +23,7 @@ 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_TASK_ORG; +import static com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS; import android.annotation.IntDef; import android.annotation.NonNull; @@ -30,7 +31,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,6 +42,7 @@ 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; @@ -55,6 +56,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 +74,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 +93,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,40 +178,60 @@ 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 @@ -234,12 +257,30 @@ public class ShellTaskOrganizer extends TaskOrganizer implements } } + /** + * Creates a persistent root task in WM for a particular windowing-mode. + * @param displayId The display to create the root task on. + * @param windowingMode Windowing mode to put the root task in. + * @param listener The listener to get the created task callback. + */ public void createRootTask(int displayId, int windowingMode, TaskListener listener) { - ProtoLog.v(WM_SHELL_TASK_ORG, "createRootTask() displayId=%d winMode=%d listener=%s", + createRootTask(displayId, windowingMode, listener, false /* removeWithTaskOrganizer */); + } + + /** + * Creates a persistent root task in WM for a particular windowing-mode. + * @param displayId The display to create the root task on. + * @param windowingMode Windowing mode to put the root task in. + * @param listener The listener to get the created task callback. + * @param removeWithTaskOrganizer True if this task should be removed when organizer destroyed. + */ + public void createRootTask(int displayId, int windowingMode, TaskListener listener, + boolean removeWithTaskOrganizer) { + ProtoLog.v(WM_SHELL_TASK_ORG, "createRootTask() displayId=%d winMode=%d listener=%s" , displayId, windowingMode, listener.toString()); final IBinder cookie = new Binder(); setPendingLaunchCookieListener(cookie, listener); - super.createRootTask(displayId, windowingMode, cookie); + super.createRootTask(displayId, windowingMode, cookie, removeWithTaskOrganizer); } /** @@ -387,9 +428,9 @@ public class ShellTaskOrganizer extends TaskOrganizer implements } @Override - public void addStartingWindow(StartingWindowInfo info, IBinder appToken) { + public void addStartingWindow(StartingWindowInfo info) { if (mStartingWindow != null) { - mStartingWindow.addStartingWindow(info, appToken); + mStartingWindow.addStartingWindow(info); } } @@ -423,6 +464,9 @@ public class ShellTaskOrganizer extends TaskOrganizer implements @Override public void onTaskAppeared(RunningTaskInfo taskInfo, SurfaceControl leash) { + if (leash != null) { + leash.setUnreleasedWarningCallSite("ShellTaskOrganizer.onTaskAppeared"); + } synchronized (mLock) { onTaskAppeared(new TaskAppearedInfo(taskInfo, leash)); } @@ -437,15 +481,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 +506,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 +535,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 +562,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,9 +578,30 @@ 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(); + } } } + /** + * Return list of {@link RunningTaskInfo}s for the given display. + * + * @return filtered list of tasks or empty list + */ + public ArrayList<RunningTaskInfo> getRunningTasks(int displayId) { + ArrayList<RunningTaskInfo> result = new ArrayList<>(); + for (int i = 0; i < mTasks.size(); i++) { + RunningTaskInfo taskInfo = mTasks.valueAt(i).getTaskInfo(); + if (taskInfo.displayId == displayId) { + result.add(taskInfo); + } + } + return result; + } + /** Gets running task by taskId. Returns {@code null} if no such task observed. */ @Nullable public RunningTaskInfo getRunningTaskInfo(int taskId) { @@ -773,7 +854,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 +875,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/activityembedding/ActivityEmbeddingAnimationAdapter.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java new file mode 100644 index 000000000000..c767376d4f29 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java @@ -0,0 +1,239 @@ +/* + * 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.SurfaceControl; +import android.view.animation.Animation; +import android.view.animation.Transformation; +import android.window.TransitionInfo; + +import androidx.annotation.NonNull; + +import com.android.wm.shell.util.TransitionUtil; + +/** + * 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(); + /** + * Area in absolute coordinate that should represent all the content to show for this window. + * This should be the end bounds for opening window, and start bounds for closing window in case + * the window is resizing during the open/close transition. + */ + @NonNull + private final Rect mContentBounds = new Rect(); + /** Offset relative to the window parent surface for {@link #mContentBounds}. */ + @NonNull + private final Point mContentRelOffset = new Point(); + + @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 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); + if (TransitionUtil.isClosingType(change.getMode())) { + // When it is closing, we want to show the content at the start position in case the + // window is resizing as well. For example, when the activities is changing from split + // to stack, the bottom TaskFragment will be resized to fullscreen when hiding. + final Rect startBounds = change.getStartAbsBounds(); + final Rect endBounds = change.getEndAbsBounds(); + mContentBounds.set(startBounds); + mContentRelOffset.set(change.getEndRelOffset()); + mContentRelOffset.offset( + startBounds.left - endBounds.left, + startBounds.top - endBounds.top); + } else { + mContentBounds.set(change.getEndAbsBounds()); + mContentRelOffset.set(change.getEndRelOffset()); + } + } + + /** + * 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 to prepare for the starting state. */ + final void prepareForFirstFrame(@NonNull SurfaceControl.Transaction startTransaction) { + startTransaction.show(mLeash); + if (mOverrideLayer != LAYER_NO_OVERRIDE) { + startTransaction.setLayer(mLeash, mOverrideLayer); + } + mAnimation.getTransformationAt(0, mTransformation); + onAnimationUpdateInner(startTransaction); + } + + /** Called on frame update. */ + final void onAnimationUpdate(@NonNull SurfaceControl.Transaction t, long currentPlayTime) { + // Extract the transformation to the current time. + mAnimation.getTransformation(Math.min(currentPlayTime, mAnimation.getDuration()), + mTransformation); + 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. + mTransformation.getMatrix().postTranslate(mContentRelOffset.x, mContentRelOffset.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(mContentBounds); + cropRect.offset(positionX - mContentRelOffset.x, positionY - mContentRelOffset.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); + } else if (mAnimation.hasExtension()) { + // Allow the surface to be shown in its original bounds in case we want to use edge + // extensions. + cropRect.union(mContentBounds); + } + + // 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..1df6ecda78c3 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java @@ -0,0 +1,523 @@ +/* + * 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.WindowManager.TRANSIT_CLOSE; +import static android.view.WindowManagerPolicyConstants.TYPE_LAYER_OFFSET; +import static android.window.TransitionInfo.FLAG_IS_BEHIND_STARTING_WINDOW; + +import static com.android.wm.shell.transition.TransitionAnimationHelper.addBackgroundToTransition; +import static com.android.wm.shell.transition.TransitionAnimationHelper.edgeExtendWindow; +import static com.android.wm.shell.transition.TransitionAnimationHelper.getTransitionBackgroundColorIfSet; + +import android.animation.Animator; +import android.animation.ValueAnimator; +import android.content.Context; +import android.graphics.Rect; +import android.os.IBinder; +import android.util.ArraySet; +import android.util.Log; +import android.view.Choreographer; +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.util.TransitionUtil; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.function.Consumer; + +/** 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) { + // There may be some surface change that we want to apply after the start transaction is + // applied to make sure the surface is ready. + final List<Consumer<SurfaceControl.Transaction>> postStartTransactionCallbacks = + new ArrayList<>(); + final Animator animator = createAnimator(info, startTransaction, finishTransaction, + () -> mController.onAnimationFinished(transition), postStartTransactionCallbacks); + + // Start the animation. + if (!postStartTransactionCallbacks.isEmpty()) { + // postStartTransactionCallbacks require that the start transaction is already + // applied to run otherwise they may result in flickers and UI inconsistencies. + startTransaction.apply(true /* sync */); + + // Run tasks that require startTransaction to already be applied + final SurfaceControl.Transaction t = new SurfaceControl.Transaction(); + for (Consumer<SurfaceControl.Transaction> postStartTransactionCallback : + postStartTransactionCallbacks) { + postStartTransactionCallback.accept(t); + } + t.apply(); + animator.start(); + } else { + 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, + @NonNull List<Consumer<SurfaceControl.Transaction>> postStartTransactionCallbacks) { + final List<ActivityEmbeddingAnimationAdapter> adapters = createAnimationAdapters(info, + startTransaction); + final ValueAnimator animator = ValueAnimator.ofFloat(0, 1); + long duration = 0; + if (adapters.isEmpty()) { + // Jump cut + // No need to modify the animator, but to update the startTransaction with the changes' + // ending states. + prepareForJumpCut(info, startTransaction); + } else { + addEdgeExtensionIfNeeded(startTransaction, finishTransaction, + postStartTransactionCallbacks, adapters); + addBackgroundColorIfNeeded(info, startTransaction, finishTransaction, adapters); + for (ActivityEmbeddingAnimationAdapter adapter : adapters) { + duration = Math.max(duration, adapter.getDurationHint()); + } + animator.addUpdateListener((anim) -> { + // Update all adapters in the same transaction. + final SurfaceControl.Transaction t = new SurfaceControl.Transaction(); + t.setFrameTimelineVsync(Choreographer.getInstance().getVsyncId()); + for (ActivityEmbeddingAnimationAdapter adapter : adapters) { + adapter.onAnimationUpdate(t, animator.getCurrentPlayTime()); + } + t.apply(); + }); + prepareForFirstFrame(startTransaction, adapters); + } + animator.setDuration(duration); + 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) { + boolean isChangeTransition = false; + for (TransitionInfo.Change change : info.getChanges()) { + if (change.hasFlags(FLAG_IS_BEHIND_STARTING_WINDOW)) { + // Skip the animation if the windows are behind an app starting window. + return new ArrayList<>(); + } + if (!isChangeTransition && change.getMode() == TRANSIT_CHANGE + && !change.getStartAbsBounds().equals(change.getEndAbsBounds())) { + isChangeTransition = true; + } + } + if (isChangeTransition) { + return createChangeAnimationAdapters(info, startTransaction); + } + if (TransitionUtil.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 AnimationProvider animationProvider) { + // We need to know if the change window is only a partial of the whole animation screen. + // If so, we will need to adjust it to make the whole animation screen looks like one. + final List<TransitionInfo.Change> openingChanges = new ArrayList<>(); + final List<TransitionInfo.Change> closingChanges = new ArrayList<>(); + final Rect openingWholeScreenBounds = new Rect(); + final Rect closingWholeScreenBounds = new Rect(); + for (TransitionInfo.Change change : info.getChanges()) { + if (TransitionUtil.isOpeningType(change.getMode())) { + openingChanges.add(change); + openingWholeScreenBounds.union(change.getEndAbsBounds()); + } else { + closingChanges.add(change); + closingWholeScreenBounds.union(change.getEndAbsBounds()); + } + } + + // For OPEN transition, open windows should be above close windows. + // For CLOSE transition, open windows should be below close windows. + int offsetLayer = TYPE_LAYER_OFFSET; + final List<ActivityEmbeddingAnimationAdapter> adapters = new ArrayList<>(); + for (TransitionInfo.Change change : openingChanges) { + final ActivityEmbeddingAnimationAdapter adapter = createOpenCloseAnimationAdapter( + info, change, animationProvider, openingWholeScreenBounds); + if (isOpening) { + adapter.overrideLayer(offsetLayer++); + } + adapters.add(adapter); + } + for (TransitionInfo.Change change : closingChanges) { + final ActivityEmbeddingAnimationAdapter adapter = createOpenCloseAnimationAdapter( + info, change, animationProvider, closingWholeScreenBounds); + if (!isOpening) { + adapter.overrideLayer(offsetLayer++); + } + adapters.add(adapter); + } + return adapters; + } + + /** Sets the first frame to the {@code startTransaction} to avoid any flicker on start. */ + private void prepareForFirstFrame(@NonNull SurfaceControl.Transaction startTransaction, + @NonNull List<ActivityEmbeddingAnimationAdapter> adapters) { + startTransaction.setFrameTimelineVsync(Choreographer.getInstance().getVsyncId()); + for (ActivityEmbeddingAnimationAdapter adapter : adapters) { + adapter.prepareForFirstFrame(startTransaction); + } + } + + /** Adds edge extension to the surfaces that have such an animation property. */ + private void addEdgeExtensionIfNeeded(@NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, + @NonNull List<Consumer<SurfaceControl.Transaction>> postStartTransactionCallbacks, + @NonNull List<ActivityEmbeddingAnimationAdapter> adapters) { + for (ActivityEmbeddingAnimationAdapter adapter : adapters) { + final Animation animation = adapter.mAnimation; + if (!animation.hasExtension()) { + continue; + } + final TransitionInfo.Change change = adapter.mChange; + if (TransitionUtil.isOpeningType(adapter.mChange.getMode())) { + // Need to screenshot after startTransaction is applied otherwise activity + // may not be visible or ready yet. + postStartTransactionCallbacks.add( + t -> edgeExtendWindow(change, animation, t, finishTransaction)); + } else { + // Can screenshot now (before startTransaction is applied) + edgeExtendWindow(change, animation, startTransaction, finishTransaction); + } + } + } + + /** Adds background color to the transition if any animation has such a property. */ + private void addBackgroundColorIfNeeded(@NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, + @NonNull List<ActivityEmbeddingAnimationAdapter> adapters) { + for (ActivityEmbeddingAnimationAdapter adapter : adapters) { + final int backgroundColor = getTransitionBackgroundColorIfSet(info, adapter.mChange, + adapter.mAnimation, 0 /* defaultColor */); + if (backgroundColor != 0) { + // We only need to show one color. + addBackgroundToTransition(info.getRootLeash(), backgroundColor, startTransaction, + finishTransaction); + return; + } + } + } + + @NonNull + private ActivityEmbeddingAnimationAdapter createOpenCloseAnimationAdapter( + @NonNull TransitionInfo info, @NonNull TransitionInfo.Change change, + @NonNull AnimationProvider animationProvider, @NonNull Rect wholeAnimationBounds) { + final Animation animation = animationProvider.get(info, change, wholeAnimationBounds); + return new ActivityEmbeddingAnimationAdapter(animation, change, change.getLeash(), + wholeAnimationBounds); + } + + @NonNull + private List<ActivityEmbeddingAnimationAdapter> createChangeAnimationAdapters( + @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction) { + if (shouldUseJumpCutForChangeTransition(info)) { + return new ArrayList<>(); + } + + 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. + Animation changeAnimation = null; + Rect parentBounds = new Rect(); + 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 && TransitionUtil.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; + } + } + + // The TaskFragment may be enter/exit split, so we take the union of both as the parent + // size. + parentBounds.union(boundsAnimationChange.getStartAbsBounds()); + parentBounds.union(boundsAnimationChange.getEndAbsBounds()); + + // There are two animations in the array. The first one is for the start leash + // (snapshot), and the second one is for the end leash (TaskFragment). + final Animation[] animations = mAnimationSpec.createChangeBoundsChangeAnimations(change, + parentBounds); + // Keep track as we might need to add background color for the animation. + // Although there may be multiple change animation, record one of them is sufficient + // because the background color will be added to the root leash for the whole animation. + changeAnimation = animations[1]; + + // 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)); + } + + if (parentBounds.isEmpty()) { + throw new IllegalStateException( + "There should be at least one changing window to play the change animation"); + } + + // If there is no corresponding open/close window with the change, we should show background + // color to cover the empty part of the screen. + boolean shouldShouldBackgroundColor = true; + // Handle the other windows that don't have bounds change in the same transition. + for (TransitionInfo.Change change : info.getChanges()) { + if (handledChanges.contains(change)) { + // Skip windows that we have already handled in the previous iteration. + continue; + } + + final Animation animation; + if ((change.getParent() != null + && handledChanges.contains(info.getChange(change.getParent()))) + || change.getMode() == TRANSIT_CHANGE) { + // No-op if it will be covered by the changing parent window, or it is a changing + // window without bounds change. + animation = ActivityEmbeddingAnimationSpec.createNoopAnimation(change); + } else if (TransitionUtil.isClosingType(change.getMode())) { + animation = mAnimationSpec.createChangeBoundsCloseAnimation(change, parentBounds); + shouldShouldBackgroundColor = false; + } else { + animation = mAnimationSpec.createChangeBoundsOpenAnimation(change, parentBounds); + shouldShouldBackgroundColor = false; + } + adapters.add(new ActivityEmbeddingAnimationAdapter(animation, change)); + } + + if (shouldShouldBackgroundColor && changeAnimation != null) { + // Change animation may leave part of the screen empty. Show background color to cover + // that. + changeAnimation.setShowBackdrop(true); + } + + 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); + } + + /** + * Whether we should use jump cut for the change transition. + * This normally happens when opening a new secondary with the existing primary using a + * different split layout. This can be complicated, like from horizontal to vertical split with + * new split pairs. + * Uses a jump cut animation to simplify. + */ + private boolean shouldUseJumpCutForChangeTransition(@NonNull TransitionInfo info) { + // There can be reparenting of changing Activity to new open TaskFragment, so we need to + // exclude both in the first iteration. + final List<TransitionInfo.Change> changingChanges = new ArrayList<>(); + for (TransitionInfo.Change change : info.getChanges()) { + if (change.getMode() != TRANSIT_CHANGE + || change.getStartAbsBounds().equals(change.getEndAbsBounds())) { + continue; + } + changingChanges.add(change); + final WindowContainerToken parentToken = change.getParent(); + 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 && TransitionUtil.isOpeningType(parentChange.getMode())) { + changingChanges.add(parentChange); + } + } + } + if (changingChanges.isEmpty()) { + // No changing target found. + return true; + } + + // Check if the transition contains both opening and closing windows. + boolean hasOpeningWindow = false; + boolean hasClosingWindow = false; + for (TransitionInfo.Change change : info.getChanges()) { + if (changingChanges.contains(change)) { + continue; + } + if (change.getParent() != null + && changingChanges.contains(info.getChange(change.getParent()))) { + // No-op if it will be covered by the changing parent window. + continue; + } + hasOpeningWindow |= TransitionUtil.isOpeningType(change.getMode()); + hasClosingWindow |= TransitionUtil.isClosingType(change.getMode()); + } + return hasOpeningWindow && hasClosingWindow; + } + + /** Updates the changes to end states in {@code startTransaction} for jump cut animation. */ + private void prepareForJumpCut(@NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction startTransaction) { + for (TransitionInfo.Change change : info.getChanges()) { + final SurfaceControl leash = change.getLeash(); + startTransaction.setPosition(leash, + change.getEndRelOffset().x, change.getEndRelOffset().y); + startTransaction.setWindowCrop(leash, + change.getEndAbsBounds().width(), change.getEndAbsBounds().height()); + if (change.getMode() == TRANSIT_CLOSE) { + startTransaction.hide(leash); + } else { + startTransaction.show(leash); + startTransaction.setAlpha(leash, 1f); + } + } + } + + /** To provide an {@link Animation} based on the transition infos. */ + private interface AnimationProvider { + Animation get(@NonNull TransitionInfo info, @NonNull TransitionInfo.Change change, + @NonNull Rect animationBounds); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java new file mode 100644 index 000000000000..cb8342a10a6a --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java @@ -0,0 +1,252 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.activityembedding; + + +import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_NONE; +import static com.android.wm.shell.transition.TransitionAnimationHelper.loadAttributeAnimation; + +import android.content.Context; +import android.graphics.Rect; +import android.view.animation.AlphaAnimation; +import android.view.animation.Animation; +import android.view.animation.AnimationSet; +import android.view.animation.AnimationUtils; +import android.view.animation.ClipRectAnimation; +import android.view.animation.Interpolator; +import android.view.animation.LinearInterpolator; +import android.view.animation.ScaleAnimation; +import android.view.animation.TranslateAnimation; +import android.window.TransitionInfo; + +import androidx.annotation.NonNull; + +import com.android.internal.policy.TransitionAnimation; +import com.android.wm.shell.util.TransitionUtil; + +/** 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 = TransitionUtil.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, + @NonNull Rect parentBounds) { + // Use end bounds for opening. + final Rect bounds = change.getEndAbsBounds(); + final int startLeft; + final int startTop; + if (parentBounds.top == bounds.top && parentBounds.bottom == bounds.bottom) { + // The window will be animated in from left or right depending on its position. + startTop = 0; + startLeft = parentBounds.left == bounds.left ? -bounds.width() : bounds.width(); + } else { + // The window will be animated in from top or bottom depending on its position. + startTop = parentBounds.top == bounds.top ? -bounds.height() : bounds.height(); + startLeft = 0; + } + + // The position should be 0-based as we will post translate in + // ActivityEmbeddingAnimationAdapter#onAnimationUpdate + final Animation animation = new TranslateAnimation(startLeft, 0, startTop, 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, + @NonNull Rect parentBounds) { + // Use start bounds for closing. + final Rect bounds = change.getStartAbsBounds(); + final int endTop; + final int endLeft; + if (parentBounds.top == bounds.top && parentBounds.bottom == bounds.bottom) { + // The window will be animated out to left or right depending on its position. + endTop = 0; + endLeft = parentBounds.left == bounds.left ? -bounds.width() : bounds.width(); + } else { + // The window will be animated out to top or bottom depending on its position. + endTop = parentBounds.top == bounds.top ? -bounds.height() : bounds.height(); + endLeft = 0; + } + + // The position should be 0-based as we will post translate in + // ActivityEmbeddingAnimationAdapter#onAnimationUpdate + final Animation animation = new TranslateAnimation(0, endLeft, 0, endTop); + 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, + startBounds.top - endBounds.top, 0); + endTranslate.setDuration(CHANGE_ANIMATION_DURATION); + endSet.addAnimation(endTranslate); + // The end leash is resizing, we should update the window crop based on the clip rect. + final Rect startClip = new Rect(startBounds); + final Rect endClip = new Rect(endBounds); + startClip.offsetTo(0, 0); + endClip.offsetTo(0, 0); + final Animation clipAnim = new ClipRectAnimation(startClip, endClip); + clipAnim.setDuration(CHANGE_ANIMATION_DURATION); + endSet.addAnimation(clipAnim); + endSet.initialize(startBounds.width(), startBounds.height(), parentBounds.width(), + parentBounds.height()); + endSet.scaleCurrentDuration(mTransitionAnimationScaleSetting); + + return new Animation[]{startSet, endSet}; + } + + @NonNull + Animation loadOpenAnimation(@NonNull TransitionInfo info, + @NonNull TransitionInfo.Change change, @NonNull Rect wholeAnimationBounds) { + final boolean isEnter = TransitionUtil.isOpeningType(change.getMode()); + final Animation animation; + if (shouldShowBackdrop(info, change)) { + animation = mTransitionAnimation.loadDefaultAnimationRes(isEnter + ? com.android.internal.R.anim.task_fragment_clear_top_open_enter + : com.android.internal.R.anim.task_fragment_clear_top_open_exit); + } else { + // Use the same edge extension animation as regular activity open. + animation = mTransitionAnimation.loadDefaultAnimationRes(isEnter + ? com.android.internal.R.anim.activity_open_enter + : com.android.internal.R.anim.activity_open_exit); + } + // Use the whole animation bounds instead of the change bounds, so that when multiple change + // targets are opening at the same time, the animation applied to each will be the same. + // Otherwise, we may see gap between the activities that are launching together. + animation.initialize(wholeAnimationBounds.width(), wholeAnimationBounds.height(), + wholeAnimationBounds.width(), wholeAnimationBounds.height()); + animation.scaleCurrentDuration(mTransitionAnimationScaleSetting); + return animation; + } + + @NonNull + Animation loadCloseAnimation(@NonNull TransitionInfo info, + @NonNull TransitionInfo.Change change, @NonNull Rect wholeAnimationBounds) { + final boolean isEnter = TransitionUtil.isOpeningType(change.getMode()); + final Animation animation; + if (shouldShowBackdrop(info, change)) { + animation = mTransitionAnimation.loadDefaultAnimationRes(isEnter + ? com.android.internal.R.anim.task_fragment_clear_top_close_enter + : com.android.internal.R.anim.task_fragment_clear_top_close_exit); + } else { + // Use the same edge extension animation as regular activity close. + animation = mTransitionAnimation.loadDefaultAnimationRes(isEnter + ? com.android.internal.R.anim.activity_close_enter + : com.android.internal.R.anim.activity_close_exit); + } + // Use the whole animation bounds instead of the change bounds, so that when multiple change + // targets are closing at the same time, the animation applied to each will be the same. + // Otherwise, we may see gap between the activities that are finishing together. + animation.initialize(wholeAnimationBounds.width(), wholeAnimationBounds.height(), + wholeAnimationBounds.width(), wholeAnimationBounds.height()); + animation.scaleCurrentDuration(mTransitionAnimationScaleSetting); + return animation; + } + + private boolean shouldShowBackdrop(@NonNull TransitionInfo info, + @NonNull TransitionInfo.Change change) { + final Animation a = loadAttributeAnimation(info, change, WALLPAPER_TRANSITION_NONE, + mTransitionAnimation); + return a != null && a.getShowBackdrop(); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingController.java new file mode 100644 index 000000000000..521a65cc4df6 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingController.java @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.activityembedding; + +import static android.window.TransitionInfo.FLAG_FILLS_TASK; +import static android.window.TransitionInfo.FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY; + +import static java.util.Objects.requireNonNull; + +import android.content.Context; +import android.os.IBinder; +import android.util.ArrayMap; +import android.view.SurfaceControl; +import android.window.TransitionInfo; +import android.window.TransitionRequestInfo; +import android.window.WindowContainerTransaction; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.wm.shell.sysui.ShellInit; +import com.android.wm.shell.transition.Transitions; + +/** + * Responsible for handling ActivityEmbedding related transitions. + */ +public class ActivityEmbeddingController implements Transitions.TransitionHandler { + + private final Context mContext; + @VisibleForTesting + final Transitions mTransitions; + @VisibleForTesting + final ActivityEmbeddingAnimationRunner mAnimationRunner; + + /** + * Keeps track of the currently-running transition callback associated with each transition + * token. + */ + private final ArrayMap<IBinder, Transitions.TransitionFinishCallback> mTransitionCallbacks = + new ArrayMap<>(); + + private ActivityEmbeddingController(@NonNull Context context, @NonNull ShellInit shellInit, + @NonNull Transitions transitions) { + mContext = requireNonNull(context); + mTransitions = requireNonNull(transitions); + mAnimationRunner = new ActivityEmbeddingAnimationRunner(context, this); + + shellInit.addInitCallback(this::onInit, this); + } + + /** + * Creates {@link ActivityEmbeddingController}, returns {@code null} if the feature is not + * supported. + */ + @Nullable + public static ActivityEmbeddingController create(@NonNull Context context, + @NonNull ShellInit shellInit, @NonNull Transitions transitions) { + return Transitions.ENABLE_SHELL_TRANSITIONS + ? new ActivityEmbeddingController(context, shellInit, transitions) + : null; + } + + /** Registers to handle transitions. */ + public void onInit() { + mTransitions.addHandler(this); + } + + @Override + public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, + @NonNull Transitions.TransitionFinishCallback finishCallback) { + boolean containsEmbeddingSplit = false; + for (TransitionInfo.Change change : info.getChanges()) { + if (!change.hasFlags(FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY)) { + // Only animate the transition if all changes are in a Task with ActivityEmbedding. + return false; + } + if (!containsEmbeddingSplit && !change.hasFlags(FLAG_FILLS_TASK)) { + // Whether the Task contains any ActivityEmbedding split before or after the + // transition. + containsEmbeddingSplit = true; + } + } + if (!containsEmbeddingSplit) { + // Let the system to play the default animation if there is no ActivityEmbedding split + // window. This allows to play the app customized animation when there is no embedding, + // such as the device is in a folded state. + return false; + } + + // Start ActivityEmbedding animation. + mTransitionCallbacks.put(transition, finishCallback); + mAnimationRunner.startAnimation(transition, info, startTransaction, finishTransaction); + return true; + } + + @Nullable + @Override + public WindowContainerTransaction handleRequest(@NonNull IBinder transition, + @NonNull TransitionRequestInfo request) { + return null; + } + + @Override + public void setAnimScaleSetting(float scale) { + mAnimationRunner.setAnimScaleSetting(scale); + } + + /** Called when the animation is finished. */ + void onAnimationFinished(@NonNull IBinder transition) { + final Transitions.TransitionFinishCallback callback = + mTransitionCallbacks.remove(transition); + if (callback == null) { + throw new IllegalStateException("No finish callback found"); + } + callback.onTransitionFinished(null /* wct */, null /* wctCB */); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/animation/Interpolators.java b/libs/WindowManager/Shell/src/com/android/wm/shell/animation/Interpolators.java index 2aead9392e59..2ec9e8b12fc6 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 @@ -16,6 +16,7 @@ package com.android.wm.shell.animation; +import android.graphics.Path; import android.view.animation.Interpolator; import android.view.animation.LinearInterpolator; import android.view.animation.PathInterpolator; @@ -53,6 +54,24 @@ public class Interpolators { public static final Interpolator LINEAR_OUT_SLOW_IN = new PathInterpolator(0f, 0f, 0.2f, 1f); /** + * The default emphasized interpolator. Used for hero / emphasized movement of content. + */ + public static final Interpolator EMPHASIZED = createEmphasizedInterpolator(); + + /** + * 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); @@ -68,4 +87,14 @@ public class Interpolators { public static final PathInterpolator DIM_INTERPOLATOR = new PathInterpolator(.23f, .87f, .52f, -0.11f); + + // Create the default emphasized interpolator + private static PathInterpolator createEmphasizedInterpolator() { + Path path = new Path(); + // Doing the same as fast_out_extra_slow_in + path.moveTo(0f, 0f); + path.cubicTo(0.05f, 0f, 0.133333f, 0.06f, 0.166666f, 0.4f); + path.cubicTo(0.208333f, 0.82f, 0.25f, 1f, 1f, 1f); + return new PathInterpolator(path); + } } 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..e84a78f42616 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 @@ -47,16 +47,15 @@ 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. */ void setSwipeThresholds(float triggerThreshold, float progressThreshold); + + /** + * Sets the system bar listener to control the system bar color. + * @param customizer the controller to control system bar color. + */ + void setStatusBarCustomizer(StatusBarCustomizer customizer); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationBackground.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationBackground.java new file mode 100644 index 000000000000..9bf3b80d262e --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationBackground.java @@ -0,0 +1,120 @@ +/* + * 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.back; + +import static android.view.Display.DEFAULT_DISPLAY; +import static android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS; + +import static com.android.wm.shell.back.BackAnimationConstants.UPDATE_SYSUI_FLAGS_THRESHOLD; + +import android.annotation.NonNull; +import android.graphics.Color; +import android.graphics.Rect; +import android.view.SurfaceControl; + +import com.android.internal.graphics.ColorUtils; +import com.android.internal.view.AppearanceRegion; +import com.android.wm.shell.RootTaskDisplayAreaOrganizer; + +/** + * Controls background surface for the back animations + */ +public class BackAnimationBackground { + private static final int BACKGROUND_LAYER = -1; + + private static final int NO_APPEARANCE = 0; + + private final RootTaskDisplayAreaOrganizer mRootTaskDisplayAreaOrganizer; + private SurfaceControl mBackgroundSurface; + + private StatusBarCustomizer mCustomizer; + private boolean mIsRequestingStatusBarAppearance; + private boolean mBackgroundIsDark; + private Rect mStartBounds; + + public BackAnimationBackground(RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer) { + mRootTaskDisplayAreaOrganizer = rootTaskDisplayAreaOrganizer; + } + + /** + * Ensures the back animation background color layer is present. + * @param startRect The start bounds of the closing target. + * @param color The background color. + * @param transaction The animation transaction. + */ + void ensureBackground(Rect startRect, int color, + @NonNull SurfaceControl.Transaction transaction) { + if (mBackgroundSurface != null) { + return; + } + + mBackgroundIsDark = ColorUtils.calculateLuminance(color) < 0.5f; + + final float[] colorComponents = new float[] { Color.red(color) / 255.f, + Color.green(color) / 255.f, Color.blue(color) / 255.f }; + + final SurfaceControl.Builder colorLayerBuilder = new SurfaceControl.Builder() + .setName("back-animation-background") + .setCallsite("BackAnimationBackground") + .setColorLayer(); + + mRootTaskDisplayAreaOrganizer.attachToDisplayArea(DEFAULT_DISPLAY, colorLayerBuilder); + mBackgroundSurface = colorLayerBuilder.build(); + transaction.setColor(mBackgroundSurface, colorComponents) + .setLayer(mBackgroundSurface, BACKGROUND_LAYER) + .show(mBackgroundSurface); + mStartBounds = startRect; + mIsRequestingStatusBarAppearance = false; + } + + void removeBackground(@NonNull SurfaceControl.Transaction transaction) { + if (mBackgroundSurface == null) { + return; + } + + if (mBackgroundSurface.isValid()) { + transaction.remove(mBackgroundSurface); + } + mBackgroundSurface = null; + mIsRequestingStatusBarAppearance = false; + } + + void setStatusBarCustomizer(StatusBarCustomizer customizer) { + mCustomizer = customizer; + } + + void onBackProgressed(float progress) { + if (mCustomizer == null || mStartBounds.isEmpty()) { + return; + } + + final boolean shouldCustomizeSystemBar = progress > UPDATE_SYSUI_FLAGS_THRESHOLD; + if (shouldCustomizeSystemBar == mIsRequestingStatusBarAppearance) { + return; + } + + mIsRequestingStatusBarAppearance = shouldCustomizeSystemBar; + if (mIsRequestingStatusBarAppearance) { + final AppearanceRegion region = new AppearanceRegion(!mBackgroundIsDark + ? APPEARANCE_LIGHT_STATUS_BARS : NO_APPEARANCE, + mStartBounds); + mCustomizer.customizeStatusBarAppearance(region); + } else { + mCustomizer.customizeStatusBarAppearance(null); + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/DividerState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationConstants.java index af2ab158ab46..e06d3ef4e1ab 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/DividerState.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationConstants.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016 The Android Open Source Project + * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,12 +14,12 @@ * limitations under the License. */ -package com.android.wm.shell.legacysplitscreen; +package com.android.wm.shell.back; /** - * Class to hold state of divider that needs to persist across configuration changes. + * The common constant values used in back animators. */ -final class DividerState { - public boolean animateAfterRecentsDrawn; - public float mRatioPositionBeforeMinimized; +class BackAnimationConstants { + static final float UPDATE_SYSUI_FLAGS_THRESHOLD = 0.20f; + static final float PROGRESS_COMMIT_THRESHOLD = 0.1f; } 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..210c9aab14d6 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 @@ -18,38 +18,51 @@ package com.android.wm.shell.back; import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission; import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BACK_PREVIEW; +import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_BACK_ANIMATION; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.ActivityTaskManager; import android.app.IActivityTaskManager; -import android.app.WindowConfiguration; import android.content.ContentResolver; import android.content.Context; import android.database.ContentObserver; -import android.graphics.Point; -import android.graphics.PointF; -import android.hardware.HardwareBuffer; +import android.hardware.input.InputManager; import android.net.Uri; +import android.os.Bundle; import android.os.Handler; +import android.os.RemoteCallback; 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.util.SparseArray; +import android.view.IRemoteAnimationRunner; +import android.view.InputDevice; +import android.view.KeyCharacterMap; +import android.view.KeyEvent; import android.view.MotionEvent; import android.view.RemoteAnimationTarget; -import android.view.SurfaceControl; +import android.window.BackAnimationAdapter; import android.window.BackEvent; +import android.window.BackMotionEvent; import android.window.BackNavigationInfo; +import android.window.IBackAnimationFinishedCallback; +import android.window.IBackAnimationRunner; import android.window.IOnBackInvokedCallback; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.view.AppearanceRegion; +import com.android.wm.shell.common.ExternalInterfaceBinder; import com.android.wm.shell.common.RemoteCallable; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.annotations.ShellBackgroundThread; import com.android.wm.shell.common.annotations.ShellMainThread; +import com.android.wm.shell.sysui.ShellController; +import com.android.wm.shell.sysui.ShellInit; import java.util.concurrent.atomic.AtomicBoolean; @@ -57,75 +70,142 @@ import java.util.concurrent.atomic.AtomicBoolean; * Controls the window animation run when a user initiates a back gesture. */ public class BackAnimationController implements RemoteCallable<BackAnimationController> { - private static final String TAG = "BackAnimationController"; + private static final String TAG = "ShellBackPreview"; private static final int SETTING_VALUE_OFF = 0; private static final int SETTING_VALUE_ON = 1; - private static final String PREDICTIVE_BACK_PROGRESS_THRESHOLD_PROP = - "persist.wm.debug.predictive_back_progress_threshold"; public static final boolean IS_ENABLED = SystemProperties.getInt("persist.wm.debug.predictive_back", - SETTING_VALUE_ON) != SETTING_VALUE_OFF; - private static final int PROGRESS_THRESHOLD = SystemProperties - .getInt(PREDICTIVE_BACK_PROGRESS_THRESHOLD_PROP, -1); + SETTING_VALUE_ON) == SETTING_VALUE_ON; + /** Flag for U animation features */ + public static boolean IS_U_ANIMATION_ENABLED = + SystemProperties.getInt("persist.wm.debug.predictive_back_anim", + SETTING_VALUE_ON) == SETTING_VALUE_ON; + /** Predictive back animation developer option */ private final AtomicBoolean mEnableAnimations = new AtomicBoolean(false); /** - * Max duration to wait for a transition to finish before accepting another gesture start - * request. + * Max duration to wait for an animation to finish before triggering the real back. */ - private static final long MAX_TRANSITION_DURATION = 2000; - - /** - * Location of the initial touch event of the back gesture. - */ - private final PointF mInitTouchLocation = new PointF(); - - /** - * Raw delta between {@link #mInitTouchLocation} and the last touch location. - */ - private final Point mTouchEventDelta = new Point(); - private final ShellExecutor mShellExecutor; + private static final long MAX_ANIMATION_DURATION = 2000; /** 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 an uninterruptible animation is in progress */ + private boolean mPostCommitAnimationInProgress = 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; @Nullable private BackNavigationInfo mBackNavigationInfo; - private final SurfaceControl.Transaction mTransaction; private final IActivityTaskManager mActivityTaskManager; private final Context mContext; - @Nullable - private IOnBackInvokedCallback mBackToLauncherCallback; - private float mTriggerThreshold; - private float mProgressThreshold; - private final Runnable mResetTransitionRunnable = () -> { - finishAnimation(); - mTransitionInProgress = false; + private final ContentResolver mContentResolver; + private final ShellController mShellController; + private final ShellExecutor mShellExecutor; + private final Handler mBgHandler; + private final Runnable mAnimationTimeoutRunnable = () -> { + ProtoLog.w(WM_SHELL_BACK_PREVIEW, "Animation didn't finish in %d ms. Resetting...", + MAX_ANIMATION_DURATION); + onBackAnimationFinished(); }; + private IBackAnimationFinishedCallback mBackAnimationFinishedCallback; + @VisibleForTesting + BackAnimationAdapter mBackAnimationAdapter; + + private final TouchTracker mTouchTracker = new TouchTracker(); + + private final SparseArray<BackAnimationRunner> mAnimationDefinition = new SparseArray<>(); + @Nullable + private IOnBackInvokedCallback mActiveCallback; + + private CrossActivityAnimation mDefaultActivityAnimation; + private CustomizeActivityAnimation mCustomizeActivityAnimation; + + @VisibleForTesting + final RemoteCallback mNavigationObserver = new RemoteCallback( + new RemoteCallback.OnResultListener() { + @Override + public void onResult(@Nullable Bundle result) { + mShellExecutor.execute(() -> { + if (!mBackGestureStarted || mPostCommitAnimationInProgress) { + // If an uninterruptible animation is already in progress, we should + // ignore this due to it may cause focus lost. (alpha = 0) + return; + } + ProtoLog.i(WM_SHELL_BACK_PREVIEW, "Navigation window gone."); + setTriggerBack(false); + onGestureFinished(false); + }); + } + }); + + private final BackAnimationBackground mAnimationBackground; + private StatusBarCustomizer mCustomizer; + public BackAnimationController( + @NonNull ShellInit shellInit, + @NonNull ShellController shellController, @NonNull @ShellMainThread ShellExecutor shellExecutor, @NonNull @ShellBackgroundThread Handler backgroundHandler, - Context context) { - this(shellExecutor, backgroundHandler, new SurfaceControl.Transaction(), - ActivityTaskManager.getService(), context, context.getContentResolver()); + Context context, + @NonNull BackAnimationBackground backAnimationBackground) { + this(shellInit, shellController, shellExecutor, backgroundHandler, + ActivityTaskManager.getService(), context, context.getContentResolver(), + backAnimationBackground); } @VisibleForTesting - BackAnimationController(@NonNull @ShellMainThread ShellExecutor shellExecutor, - @NonNull @ShellBackgroundThread Handler handler, - @NonNull SurfaceControl.Transaction transaction, + BackAnimationController( + @NonNull ShellInit shellInit, + @NonNull ShellController shellController, + @NonNull @ShellMainThread ShellExecutor shellExecutor, + @NonNull @ShellBackgroundThread Handler bgHandler, @NonNull IActivityTaskManager activityTaskManager, - Context context, ContentResolver contentResolver) { + Context context, ContentResolver contentResolver, + @NonNull BackAnimationBackground backAnimationBackground) { + mShellController = shellController; mShellExecutor = shellExecutor; - mTransaction = transaction; mActivityTaskManager = activityTaskManager; mContext = context; - setupAnimationDeveloperSettingsObserver(contentResolver, handler); + mContentResolver = contentResolver; + mBgHandler = bgHandler; + shellInit.addInitCallback(this::onInit, this); + mAnimationBackground = backAnimationBackground; + } + + @VisibleForTesting + void setEnableUAnimation(boolean enable) { + IS_U_ANIMATION_ENABLED = enable; + } + + private void onInit() { + setupAnimationDeveloperSettingsObserver(mContentResolver, mBgHandler); + createAdapter(); + mShellController.addExternalInterface(KEY_EXTRA_SHELL_BACK_ANIMATION, + this::createExternalInterface, this); + + initBackAnimationRunners(); + } + + private void initBackAnimationRunners() { + if (!IS_U_ANIMATION_ENABLED) { + return; + } + + final CrossTaskBackAnimation crossTaskAnimation = + new CrossTaskBackAnimation(mContext, mAnimationBackground); + mAnimationDefinition.set(BackNavigationInfo.TYPE_CROSS_TASK, + crossTaskAnimation.mBackAnimationRunner); + mDefaultActivityAnimation = + new CrossActivityAnimation(mContext, mAnimationBackground); + mAnimationDefinition.set(BackNavigationInfo.TYPE_CROSS_ACTIVITY, + mDefaultActivityAnimation.mBackAnimationRunner); + mCustomizeActivityAnimation = + new CustomizeActivityAnimation(mContext, mAnimationBackground); + // TODO (236760237): register dialog close animation when it's completed. } private void setupAnimationDeveloperSettingsObserver( @@ -150,15 +230,18 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont Global.ENABLE_BACK_ANIMATION, SETTING_VALUE_OFF); boolean isEnabled = settingValue == SETTING_VALUE_ON; mEnableAnimations.set(isEnabled); - ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Back animation enabled=%s", - isEnabled); + ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Back animation enabled=%s", isEnabled); } public BackAnimation getBackAnimationImpl() { return mBackAnimation; } - private final BackAnimation mBackAnimation = new BackAnimationImpl(); + private ExternalInterfaceBinder createExternalInterface() { + return new IBackAnimationImpl(this); + } + + private final BackAnimationImpl mBackAnimation = new BackAnimationImpl(); @Override public Context getContext() { @@ -171,17 +254,6 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont } private class BackAnimationImpl implements BackAnimation { - private IBackAnimationImpl mBackAnimation; - - @Override - public IBackAnimation createExternalInterface() { - if (mBackAnimation != null) { - mBackAnimation.invalidate(); - } - mBackAnimation = new IBackAnimationImpl(BackAnimationController.this); - return mBackAnimation; - } - @Override public void onBackMotion( float touchX, float touchY, int keyAction, @BackEvent.SwipeEdge int swipeEdge) { @@ -198,9 +270,16 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont mShellExecutor.execute(() -> BackAnimationController.this.setSwipeThresholds( triggerThreshold, progressThreshold)); } + + @Override + public void setStatusBarCustomizer(StatusBarCustomizer customizer) { + mCustomizer = customizer; + mAnimationBackground.setStatusBarCustomizer(customizer); + } } - private static class IBackAnimationImpl extends IBackAnimation.Stub { + private static class IBackAnimationImpl extends IBackAnimation.Stub + implements ExternalInterfaceBinder { private BackAnimationController mController; IBackAnimationImpl(BackAnimationController controller) { @@ -208,48 +287,45 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont } @Override - public void setBackToLauncherCallback(IOnBackInvokedCallback callback) { + public void setBackToLauncherCallback(IOnBackInvokedCallback callback, + IRemoteAnimationRunner runner) { executeRemoteCallWithTaskPermission(mController, "setBackToLauncherCallback", - (controller) -> controller.setBackToLauncherCallback(callback)); + (controller) -> controller.registerAnimation( + BackNavigationInfo.TYPE_RETURN_TO_HOME, + new BackAnimationRunner(callback, runner))); } @Override public void clearBackToLauncherCallback() { executeRemoteCallWithTaskPermission(mController, "clearBackToLauncherCallback", - (controller) -> controller.clearBackToLauncherCallback()); + (controller) -> controller.unregisterAnimation( + BackNavigationInfo.TYPE_RETURN_TO_HOME)); } - @Override - public void onBackToLauncherAnimationFinished() { - executeRemoteCallWithTaskPermission(mController, "onBackToLauncherAnimationFinished", - (controller) -> controller.onBackToLauncherAnimationFinished()); + public void customizeStatusBarAppearance(AppearanceRegion appearance) { + executeRemoteCallWithTaskPermission(mController, "useLauncherSysBarFlags", + (controller) -> controller.customizeStatusBarAppearance(appearance)); } - void invalidate() { + @Override + public void invalidate() { mController = null; } } - @VisibleForTesting - void setBackToLauncherCallback(IOnBackInvokedCallback callback) { - mBackToLauncherCallback = callback; + private void customizeStatusBarAppearance(AppearanceRegion appearance) { + if (mCustomizer != null) { + mCustomizer.customizeStatusBarAppearance(appearance); + } } - private void clearBackToLauncherCallback() { - mBackToLauncherCallback = null; + void registerAnimation(@BackNavigationInfo.BackTargetType int type, + @NonNull BackAnimationRunner runner) { + mAnimationDefinition.set(type, runner); } - @VisibleForTesting - void onBackToLauncherAnimationFinished() { - if (mBackNavigationInfo != null) { - IOnBackInvokedCallback callback = mBackNavigationInfo.getOnBackInvokedCallback(); - if (mTriggerBack) { - dispatchOnBackInvoked(callback); - } else { - dispatchOnBackCancelled(callback); - } - } - finishAnimation(); + void unregisterAnimation(@BackNavigationInfo.BackTargetType int type) { + mAnimationDefinition.remove(type); } /** @@ -258,41 +334,51 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont */ public void onMotionEvent(float touchX, float touchY, int keyAction, @BackEvent.SwipeEdge int swipeEdge) { - if (mTransitionInProgress) { + if (mPostCommitAnimationInProgress) { return; } - if (keyAction == MotionEvent.ACTION_MOVE) { + + mTouchTracker.update(touchX, touchY); + 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, swipeEdge); + mShouldStartOnNextMoveEvent = false; } - onMove(touchX, touchY, swipeEdge); + onMove(); } 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, @BackEvent.SwipeEdge int swipeEdge) { ProtoLog.d(WM_SHELL_BACK_PREVIEW, "initAnimation mMotionStarted=%b", mBackGestureStarted); if (mBackGestureStarted || mBackNavigationInfo != null) { Log.e(TAG, "Animation is being initialized but is already started."); - finishAnimation(); + finishBackNavigation(); } - mInitTouchLocation.set(touchX, touchY); + mTouchTracker.setGestureStartLocation(touchX, touchY, swipeEdge); mBackGestureStarted = true; try { - boolean requestAnimation = mEnableAnimations.get(); - mBackNavigationInfo = mActivityTaskManager.startBackNavigation(requestAnimation); + mBackNavigationInfo = mActivityTaskManager.startBackNavigation( + mNavigationObserver, mEnableAnimations.get() ? mBackAnimationAdapter : null); onBackNavigationInfoReceived(mBackNavigationInfo); } catch (RemoteException remoteException) { Log.e(TAG, "Failed to initAnimation", remoteException); - finishAnimation(); + finishBackNavigation(); } } @@ -300,124 +386,69 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Received backNavigationInfo:%s", backNavigationInfo); if (backNavigationInfo == null) { Log.e(TAG, "Received BackNavigationInfo is null."); - finishAnimation(); return; } - int backType = backNavigationInfo.getType(); - IOnBackInvokedCallback targetCallback = null; - if (backType == BackNavigationInfo.TYPE_CROSS_ACTIVITY) { - HardwareBuffer hardwareBuffer = backNavigationInfo.getScreenshotHardwareBuffer(); - if (hardwareBuffer != null) { - displayTargetScreenshot(hardwareBuffer, - backNavigationInfo.getTaskWindowConfiguration()); + final int backType = backNavigationInfo.getType(); + final boolean shouldDispatchToAnimator = shouldDispatchToAnimator(); + if (shouldDispatchToAnimator) { + if (mAnimationDefinition.contains(backType)) { + mAnimationDefinition.get(backType).startGesture(); + } else { + mActiveCallback = null; } - mTransaction.apply(); - } else if (shouldDispatchToLauncher(backType)) { - targetCallback = mBackToLauncherCallback; - } else if (backType == BackNavigationInfo.TYPE_CALLBACK) { - targetCallback = mBackNavigationInfo.getOnBackInvokedCallback(); + } else { + mActiveCallback = mBackNavigationInfo.getOnBackInvokedCallback(); + dispatchOnBackStarted(mActiveCallback, mTouchTracker.createStartEvent(null)); } - dispatchOnBackStarted(targetCallback); } - /** - * Display the screenshot of the activity beneath. - * - * @param hardwareBuffer The buffer containing the screenshot. - */ - private void displayTargetScreenshot(@NonNull HardwareBuffer hardwareBuffer, - WindowConfiguration taskWindowConfiguration) { - SurfaceControl screenshotSurface = - mBackNavigationInfo == null ? null : mBackNavigationInfo.getScreenshotSurface(); - if (screenshotSurface == null) { - Log.e(TAG, "BackNavigationInfo doesn't contain a surface for the screenshot. "); + private void onMove() { + if (!mBackGestureStarted || mBackNavigationInfo == null || mActiveCallback == null) { return; } - // Scale the buffer to fill the whole Task - float sx = 1; - float sy = 1; - float w = taskWindowConfiguration.getBounds().width(); - float h = taskWindowConfiguration.getBounds().height(); - - if (w != hardwareBuffer.getWidth()) { - sx = w / hardwareBuffer.getWidth(); - } - - if (h != hardwareBuffer.getHeight()) { - sy = h / hardwareBuffer.getHeight(); - } - mTransaction.setScale(screenshotSurface, sx, sy); - mTransaction.setBuffer(screenshotSurface, hardwareBuffer); - mTransaction.setVisibility(screenshotSurface, true); + final BackMotionEvent backEvent = mTouchTracker.createProgressEvent(); + dispatchOnBackProgressed(mActiveCallback, backEvent); } - private void onMove(float touchX, float touchY, @BackEvent.SwipeEdge int swipeEdge) { - if (!mBackGestureStarted || mBackNavigationInfo == null) { - return; - } - int deltaX = Math.round(touchX - mInitTouchLocation.x); - float progressThreshold = PROGRESS_THRESHOLD >= 0 ? PROGRESS_THRESHOLD : mProgressThreshold; - float progress = Math.min(Math.max(Math.abs(deltaX) / progressThreshold, 0), 1); - int backType = mBackNavigationInfo.getType(); - RemoteAnimationTarget animationTarget = mBackNavigationInfo.getDepartingAnimationTarget(); - - BackEvent backEvent = new BackEvent( - touchX, touchY, progress, swipeEdge, animationTarget); - IOnBackInvokedCallback targetCallback = null; - if (shouldDispatchToLauncher(backType)) { - targetCallback = mBackToLauncherCallback; - } else if (backType == BackNavigationInfo.TYPE_CROSS_TASK - || backType == BackNavigationInfo.TYPE_CROSS_ACTIVITY) { - // TODO(208427216) Run the actual animation - } else if (backType == BackNavigationInfo.TYPE_CALLBACK) { - targetCallback = mBackNavigationInfo.getOnBackInvokedCallback(); - } - dispatchOnBackProgressed(targetCallback, backEvent); + private void injectBackKey() { + sendBackEvent(KeyEvent.ACTION_DOWN); + sendBackEvent(KeyEvent.ACTION_UP); } - private void onGestureFinished() { - ProtoLog.d(WM_SHELL_BACK_PREVIEW, "onGestureFinished() mTriggerBack == %s", mTriggerBack); - if (!mBackGestureStarted || mBackNavigationInfo == null) { - return; - } - int backType = mBackNavigationInfo.getType(); - boolean shouldDispatchToLauncher = shouldDispatchToLauncher(backType); - IOnBackInvokedCallback targetCallback = shouldDispatchToLauncher - ? mBackToLauncherCallback - : mBackNavigationInfo.getOnBackInvokedCallback(); - if (shouldDispatchToLauncher) { - startTransition(); - } - if (mTriggerBack) { - dispatchOnBackInvoked(targetCallback); - } else { - dispatchOnBackCancelled(targetCallback); - } - if (backType != BackNavigationInfo.TYPE_RETURN_TO_HOME || !shouldDispatchToLauncher) { - // Launcher callback missing. Simply finish animation. - finishAnimation(); + 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 (!mContext.getSystemService(InputManager.class) + .injectInputEvent(ev, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC)) { + Log.e(TAG, "Inject input event fail"); } } - private boolean shouldDispatchToLauncher(int backType) { - return backType == BackNavigationInfo.TYPE_RETURN_TO_HOME - && mBackToLauncherCallback != null - && mEnableAnimations.get(); + private boolean shouldDispatchToAnimator() { + return mEnableAnimations.get() + && mBackNavigationInfo != null + && mBackNavigationInfo.isPrepareRemoteAnimation(); } - private static void dispatchOnBackStarted(IOnBackInvokedCallback callback) { + private void dispatchOnBackStarted(IOnBackInvokedCallback callback, + BackMotionEvent backEvent) { if (callback == null) { return; } try { - callback.onBackStarted(); + callback.onBackStarted(backEvent); } catch (RemoteException e) { Log.e(TAG, "dispatchOnBackStarted error: ", e); } } - private static void dispatchOnBackInvoked(IOnBackInvokedCallback callback) { + private void dispatchOnBackInvoked(IOnBackInvokedCallback callback) { if (callback == null) { return; } @@ -428,7 +459,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont } } - private static void dispatchOnBackCancelled(IOnBackInvokedCallback callback) { + private void dispatchOnBackCancelled(IOnBackInvokedCallback callback) { if (callback == null) { return; } @@ -439,8 +470,8 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont } } - private static void dispatchOnBackProgressed(IOnBackInvokedCallback callback, - BackEvent backEvent) { + private void dispatchOnBackProgressed(IOnBackInvokedCallback callback, + BackMotionEvent backEvent) { if (callback == null) { return; } @@ -455,57 +486,230 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont * Sets to true when the back gesture has passed the triggering threshold, false otherwise. */ public void setTriggerBack(boolean triggerBack) { - if (mTransitionInProgress) { + if (mPostCommitAnimationInProgress) { return; } mTriggerBack = triggerBack; + mTouchTracker.setTriggerBack(triggerBack); } private void setSwipeThresholds(float triggerThreshold, float progressThreshold) { - mProgressThreshold = progressThreshold; - mTriggerThreshold = triggerThreshold; - } - - 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; - if (backNavigationInfo == null) { + mTouchTracker.setProgressThreshold(progressThreshold); + } + + private void invokeOrCancelBack() { + // Make a synchronized call to core before dispatch back event to client side. + // If the close transition happens before the core receives onAnimationFinished, there will + // play a second close animation for that transition. + if (mBackAnimationFinishedCallback != null) { + try { + mBackAnimationFinishedCallback.onAnimationFinished(mTriggerBack); + } catch (RemoteException e) { + Log.e(TAG, "Failed call IBackAnimationFinishedCallback", e); + } + mBackAnimationFinishedCallback = null; + } + + if (mBackNavigationInfo != null) { + final IOnBackInvokedCallback callback = mBackNavigationInfo.getOnBackInvokedCallback(); + if (mTriggerBack) { + dispatchOnBackInvoked(callback); + } else { + dispatchOnBackCancelled(callback); + } + } + finishBackNavigation(); + } + + /** + * Called when the gesture is released, then it could start the post commit animation. + */ + private void onGestureFinished(boolean fromTouch) { + ProtoLog.d(WM_SHELL_BACK_PREVIEW, "onGestureFinished() mTriggerBack == %s", mTriggerBack); + if (!mBackGestureStarted) { + finishBackNavigation(); return; } - RemoteAnimationTarget animationTarget = backNavigationInfo.getDepartingAnimationTarget(); - if (animationTarget != null) { - if (animationTarget.leash != null && animationTarget.leash.isValid()) { - mTransaction.remove(animationTarget.leash); + + 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 (mPostCommitAnimationInProgress) { + ProtoLog.w(WM_SHELL_BACK_PREVIEW, "Animation is still running"); + return; + } + + if (mBackNavigationInfo == null) { + // No focus window found or core are running recents animation, inject back key as + // legacy behavior. + if (mTriggerBack) { + injectBackKey(); } + finishBackNavigation(); + return; } - SurfaceControl screenshotSurface = backNavigationInfo.getScreenshotSurface(); - if (screenshotSurface != null && screenshotSurface.isValid()) { - mTransaction.remove(screenshotSurface); + + final int backType = mBackNavigationInfo.getType(); + final BackAnimationRunner runner = mAnimationDefinition.get(backType); + // Simply trigger and finish back navigation when no animator defined. + if (!shouldDispatchToAnimator() || runner == null) { + invokeOrCancelBack(); + return; } - mTransaction.apply(); - stopTransition(); - backNavigationInfo.onBackNavigationFinished(triggerBack); + if (runner.isWaitingAnimation()) { + ProtoLog.w(WM_SHELL_BACK_PREVIEW, "Gesture released, but animation didn't ready."); + return; + } else if (runner.isAnimationCancelled()) { + invokeOrCancelBack(); + return; + } + startPostCommitAnimation(); } - private void startTransition() { - if (mTransitionInProgress) { + /** + * Start the phase 2 animation when gesture is released. + * Callback to {@link #onBackAnimationFinished} when it is finished or timeout. + */ + private void startPostCommitAnimation() { + if (mPostCommitAnimationInProgress) { return; } - mTransitionInProgress = true; - mShellExecutor.executeDelayed(mResetTransitionRunnable, MAX_TRANSITION_DURATION); + ProtoLog.d(WM_SHELL_BACK_PREVIEW, "BackAnimationController: startPostCommitAnimation()"); + mPostCommitAnimationInProgress = true; + mShellExecutor.executeDelayed(mAnimationTimeoutRunnable, MAX_ANIMATION_DURATION); + + // The next callback should be {@link #onBackAnimationFinished}. + if (mTriggerBack) { + dispatchOnBackInvoked(mActiveCallback); + } else { + dispatchOnBackCancelled(mActiveCallback); + } } - private void stopTransition() { - if (!mTransitionInProgress) { + /** + * Called when the post commit animation is completed or timeout. + * This will trigger the real {@link IOnBackInvokedCallback} behavior. + */ + @VisibleForTesting + void onBackAnimationFinished() { + if (!mPostCommitAnimationInProgress) { return; } - mShellExecutor.removeCallbacks(mResetTransitionRunnable); - mTransitionInProgress = false; + // Stop timeout runner. + mShellExecutor.removeCallbacks(mAnimationTimeoutRunnable); + mPostCommitAnimationInProgress = false; + + ProtoLog.d(WM_SHELL_BACK_PREVIEW, "BackAnimationController: onBackAnimationFinished()"); + + // Trigger the real back. + invokeOrCancelBack(); + } + + /** + * This should be called after the whole back navigation is completed. + */ + @VisibleForTesting + void finishBackNavigation() { + ProtoLog.d(WM_SHELL_BACK_PREVIEW, "BackAnimationController: finishBackNavigation()"); + mShouldStartOnNextMoveEvent = false; + mTouchTracker.reset(); + mActiveCallback = null; + // reset to default + if (mDefaultActivityAnimation != null + && mAnimationDefinition.contains(BackNavigationInfo.TYPE_CROSS_ACTIVITY)) { + mAnimationDefinition.set(BackNavigationInfo.TYPE_CROSS_ACTIVITY, + mDefaultActivityAnimation.mBackAnimationRunner); + } + if (mBackNavigationInfo != null) { + mBackNavigationInfo.onBackNavigationFinished(mTriggerBack); + mBackNavigationInfo = null; + } + mTriggerBack = false; + } + + private BackAnimationRunner getAnimationRunnerAndInit() { + int type = mBackNavigationInfo.getType(); + // Initiate customized cross-activity animation, or fall back to cross activity animation + if (type == BackNavigationInfo.TYPE_CROSS_ACTIVITY && mAnimationDefinition.contains(type)) { + final BackNavigationInfo.CustomAnimationInfo animationInfo = + mBackNavigationInfo.getCustomAnimationInfo(); + if (animationInfo != null && mCustomizeActivityAnimation != null + && mCustomizeActivityAnimation.prepareNextAnimation(animationInfo)) { + mAnimationDefinition.get(type).resetWaitingAnimation(); + mAnimationDefinition.set(BackNavigationInfo.TYPE_CROSS_ACTIVITY, + mCustomizeActivityAnimation.mBackAnimationRunner); + } + } + return mAnimationDefinition.get(type); + } + + private void createAdapter() { + IBackAnimationRunner runner = new IBackAnimationRunner.Stub() { + @Override + public void onAnimationStart(RemoteAnimationTarget[] apps, + RemoteAnimationTarget[] wallpapers, RemoteAnimationTarget[] nonApps, + IBackAnimationFinishedCallback finishedCallback) { + mShellExecutor.execute(() -> { + if (mBackNavigationInfo == null) { + Log.e(TAG, "Lack of navigation info to start animation."); + return; + } + final int type = mBackNavigationInfo.getType(); + final BackAnimationRunner runner = getAnimationRunnerAndInit(); + if (runner == null) { + Log.e(TAG, "Animation didn't be defined for type " + + BackNavigationInfo.typeToString(type)); + if (finishedCallback != null) { + try { + finishedCallback.onAnimationFinished(false); + } catch (RemoteException e) { + Log.w(TAG, "Failed call IBackNaviAnimationController", e); + } + } + return; + } + mActiveCallback = runner.getCallback(); + mBackAnimationFinishedCallback = finishedCallback; + + ProtoLog.d(WM_SHELL_BACK_PREVIEW, "BackAnimationController: startAnimation()"); + runner.startAnimation(apps, wallpapers, nonApps, () -> mShellExecutor.execute( + BackAnimationController.this::onBackAnimationFinished)); + + if (apps.length >= 1) { + dispatchOnBackStarted( + mActiveCallback, mTouchTracker.createStartEvent(apps[0])); + } + + // Dispatch the first progress after animation start for smoothing the initial + // animation, instead of waiting for next onMove. + final BackMotionEvent backFinish = mTouchTracker.createProgressEvent(); + dispatchOnBackProgressed(mActiveCallback, backFinish); + if (!mBackGestureStarted) { + // if the down -> up gesture happened before animation start, we have to + // trigger the uninterruptible transition to finish the back animation. + startPostCommitAnimation(); + } + }); + } + + @Override + public void onAnimationCancelled() { + mShellExecutor.execute(() -> { + final BackAnimationRunner runner = mAnimationDefinition.get( + mBackNavigationInfo.getType()); + if (runner == null) { + return; + } + runner.cancelAnimation(); + if (!mBackGestureStarted) { + invokeOrCancelBack(); + } + }); + } + }; + mBackAnimationAdapter = new BackAnimationAdapter(runner); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationRunner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationRunner.java new file mode 100644 index 000000000000..913239f74bf2 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationRunner.java @@ -0,0 +1,106 @@ +/* + * 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.back; + +import static android.view.WindowManager.TRANSIT_OLD_UNSET; + +import android.annotation.NonNull; +import android.os.RemoteException; +import android.util.Log; +import android.view.IRemoteAnimationFinishedCallback; +import android.view.IRemoteAnimationRunner; +import android.view.RemoteAnimationTarget; +import android.window.IBackAnimationRunner; +import android.window.IOnBackInvokedCallback; + +/** + * Used to register the animation callback and runner, it will trigger result if gesture was finish + * before it received IBackAnimationRunner#onAnimationStart, so the controller could continue + * trigger the real back behavior. + */ +class BackAnimationRunner { + private static final String TAG = "ShellBackPreview"; + + private final IOnBackInvokedCallback mCallback; + private final IRemoteAnimationRunner mRunner; + + // Whether we are waiting to receive onAnimationStart + private boolean mWaitingAnimation; + + /** True when the back animation is cancelled */ + private boolean mAnimationCancelled; + + BackAnimationRunner(@NonNull IOnBackInvokedCallback callback, + @NonNull IRemoteAnimationRunner runner) { + mCallback = callback; + mRunner = runner; + } + + /** Returns the registered animation runner */ + IRemoteAnimationRunner getRunner() { + return mRunner; + } + + /** Returns the registered animation callback */ + IOnBackInvokedCallback getCallback() { + return mCallback; + } + + /** + * Called from {@link IBackAnimationRunner}, it will deliver these + * {@link RemoteAnimationTarget}s to the corresponding runner. + */ + void startAnimation(RemoteAnimationTarget[] apps, RemoteAnimationTarget[] wallpapers, + RemoteAnimationTarget[] nonApps, Runnable finishedCallback) { + final IRemoteAnimationFinishedCallback callback = + new IRemoteAnimationFinishedCallback.Stub() { + @Override + public void onAnimationFinished() { + finishedCallback.run(); + } + }; + mWaitingAnimation = false; + try { + getRunner().onAnimationStart(TRANSIT_OLD_UNSET, apps, wallpapers, + nonApps, callback); + } catch (RemoteException e) { + Log.w(TAG, "Failed call onAnimationStart", e); + } + } + + void startGesture() { + mWaitingAnimation = true; + mAnimationCancelled = false; + } + + boolean isWaitingAnimation() { + return mWaitingAnimation; + } + + void cancelAnimation() { + mWaitingAnimation = false; + mAnimationCancelled = true; + } + + boolean isAnimationCancelled() { + return mAnimationCancelled; + } + + void resetWaitingAnimation() { + mWaitingAnimation = false; + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityAnimation.java new file mode 100644 index 000000000000..22c90153bb39 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityAnimation.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.back; + +import static android.view.RemoteAnimationTarget.MODE_CLOSING; +import static android.view.RemoteAnimationTarget.MODE_OPENING; + +import static com.android.wm.shell.back.BackAnimationConstants.PROGRESS_COMMIT_THRESHOLD; +import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BACK_PREVIEW; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; +import android.annotation.NonNull; +import android.content.Context; +import android.graphics.Matrix; +import android.graphics.PointF; +import android.graphics.Rect; +import android.graphics.RectF; +import android.os.RemoteException; +import android.util.FloatProperty; +import android.util.TypedValue; +import android.view.IRemoteAnimationFinishedCallback; +import android.view.IRemoteAnimationRunner; +import android.view.RemoteAnimationTarget; +import android.view.SurfaceControl; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.Interpolator; +import android.window.BackEvent; +import android.window.BackMotionEvent; +import android.window.BackProgressAnimator; +import android.window.IOnBackInvokedCallback; + +import com.android.internal.dynamicanimation.animation.SpringAnimation; +import com.android.internal.dynamicanimation.animation.SpringForce; +import com.android.internal.policy.ScreenDecorationsUtils; +import com.android.internal.protolog.common.ProtoLog; +import com.android.wm.shell.common.annotations.ShellMainThread; + +/** Class that defines cross-activity animation. */ +@ShellMainThread +class CrossActivityAnimation { + /** + * Minimum scale of the entering/closing window. + */ + private static final float MIN_WINDOW_SCALE = 0.9f; + + /** Duration of post animation after gesture committed. */ + private static final int POST_ANIMATION_DURATION = 350; + private static final Interpolator INTERPOLATOR = new DecelerateInterpolator(); + private static final FloatProperty<CrossActivityAnimation> ENTER_PROGRESS_PROP = + new FloatProperty<>("enter-alpha") { + @Override + public void setValue(CrossActivityAnimation anim, float value) { + anim.setEnteringProgress(value); + } + + @Override + public Float get(CrossActivityAnimation object) { + return object.getEnteringProgress(); + } + }; + private static final FloatProperty<CrossActivityAnimation> LEAVE_PROGRESS_PROP = + new FloatProperty<>("leave-alpha") { + @Override + public void setValue(CrossActivityAnimation anim, float value) { + anim.setLeavingProgress(value); + } + + @Override + public Float get(CrossActivityAnimation object) { + return object.getLeavingProgress(); + } + }; + private static final float MIN_WINDOW_ALPHA = 0.01f; + private static final float WINDOW_X_SHIFT_DP = 96; + private static final int SCALE_FACTOR = 100; + // TODO(b/264710590): Use the progress commit threshold from ViewConfiguration once it exists. + private static final float TARGET_COMMIT_PROGRESS = 0.5f; + private static final float ENTER_ALPHA_THRESHOLD = 0.22f; + + private final Rect mStartTaskRect = new Rect(); + private final float mCornerRadius; + + // The closing window properties. + private final RectF mClosingRect = new RectF(); + + // The entering window properties. + private final Rect mEnteringStartRect = new Rect(); + private final RectF mEnteringRect = new RectF(); + private final SpringAnimation mEnteringProgressSpring; + private final SpringAnimation mLeavingProgressSpring; + // Max window x-shift in pixels. + private final float mWindowXShift; + + private float mEnteringProgress = 0f; + private float mLeavingProgress = 0f; + + private final PointF mInitialTouchPos = new PointF(); + + private final Matrix mTransformMatrix = new Matrix(); + + private final float[] mTmpFloat9 = new float[9]; + + private RemoteAnimationTarget mEnteringTarget; + private RemoteAnimationTarget mClosingTarget; + private SurfaceControl.Transaction mTransaction = new SurfaceControl.Transaction(); + + private boolean mBackInProgress = false; + + private PointF mTouchPos = new PointF(); + private IRemoteAnimationFinishedCallback mFinishCallback; + + private final BackProgressAnimator mProgressAnimator = new BackProgressAnimator(); + final BackAnimationRunner mBackAnimationRunner; + + private final BackAnimationBackground mBackground; + + CrossActivityAnimation(Context context, BackAnimationBackground background) { + mCornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context); + mBackAnimationRunner = new BackAnimationRunner(new Callback(), new Runner()); + mBackground = background; + mEnteringProgressSpring = new SpringAnimation(this, ENTER_PROGRESS_PROP); + mEnteringProgressSpring.setSpring(new SpringForce() + .setStiffness(SpringForce.STIFFNESS_MEDIUM) + .setDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY)); + mLeavingProgressSpring = new SpringAnimation(this, LEAVE_PROGRESS_PROP); + mLeavingProgressSpring.setSpring(new SpringForce() + .setStiffness(SpringForce.STIFFNESS_MEDIUM) + .setDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY)); + mWindowXShift = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, WINDOW_X_SHIFT_DP, + context.getResources().getDisplayMetrics()); + } + + /** + * Returns 1 if x >= edge1, 0 if x <= edge0, and a smoothed value between the two. + * From https://en.wikipedia.org/wiki/Smoothstep + */ + private static float smoothstep(float edge0, float edge1, float x) { + if (x < edge0) return 0; + if (x >= edge1) return 1; + + x = (x - edge0) / (edge1 - edge0); + return x * x * (3 - 2 * x); + } + + /** + * Linearly map x from range (a1, a2) to range (b1, b2). + */ + private static float mapLinear(float x, float a1, float a2, float b1, float b2) { + return b1 + (x - a1) * (b2 - b1) / (a2 - a1); + } + + /** + * Linearly map a normalized value from (0, 1) to (min, max). + */ + private static float mapRange(float value, float min, float max) { + return min + (value * (max - min)); + } + + private void startBackAnimation() { + if (mEnteringTarget == null || mClosingTarget == null) { + ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Entering target or closing target is null."); + return; + } + mTransaction.setAnimationTransaction(); + + // Offset start rectangle to align task bounds. + mStartTaskRect.set(mClosingTarget.windowConfiguration.getBounds()); + mStartTaskRect.offsetTo(0, 0); + + // Draw background with task background color. + mBackground.ensureBackground(mClosingTarget.windowConfiguration.getBounds(), + mEnteringTarget.taskInfo.taskDescription.getBackgroundColor(), mTransaction); + } + + private void applyTransform(SurfaceControl leash, RectF targetRect, float targetAlpha) { + final float scale = targetRect.width() / mStartTaskRect.width(); + mTransformMatrix.reset(); + mTransformMatrix.setScale(scale, scale); + mTransformMatrix.postTranslate(targetRect.left, targetRect.top); + mTransaction.setAlpha(leash, targetAlpha) + .setMatrix(leash, mTransformMatrix, mTmpFloat9) + .setWindowCrop(leash, mStartTaskRect) + .setCornerRadius(leash, mCornerRadius); + } + + private void finishAnimation() { + if (mEnteringTarget != null) { + mEnteringTarget.leash.release(); + mEnteringTarget = null; + } + if (mClosingTarget != null) { + mClosingTarget.leash.release(); + mClosingTarget = null; + } + if (mBackground != null) { + mBackground.removeBackground(mTransaction); + } + + mTransaction.apply(); + mBackInProgress = false; + mTransformMatrix.reset(); + mInitialTouchPos.set(0, 0); + + if (mFinishCallback != null) { + try { + mFinishCallback.onAnimationFinished(); + } catch (RemoteException e) { + e.printStackTrace(); + } + mFinishCallback = null; + } + mEnteringProgressSpring.animateToFinalPosition(0); + mEnteringProgressSpring.skipToEnd(); + mLeavingProgressSpring.animateToFinalPosition(0); + mLeavingProgressSpring.skipToEnd(); + } + + private void onGestureProgress(@NonNull BackEvent backEvent) { + if (!mBackInProgress) { + mInitialTouchPos.set(backEvent.getTouchX(), backEvent.getTouchY()); + mBackInProgress = true; + } + mTouchPos.set(backEvent.getTouchX(), backEvent.getTouchY()); + + float progress = backEvent.getProgress(); + float springProgress = (progress > PROGRESS_COMMIT_THRESHOLD + ? mapLinear(progress, 0.1f, 1, TARGET_COMMIT_PROGRESS, 1) + : mapLinear(progress, 0, 1f, 0, TARGET_COMMIT_PROGRESS)) * SCALE_FACTOR; + mLeavingProgressSpring.animateToFinalPosition(springProgress); + mEnteringProgressSpring.animateToFinalPosition(springProgress); + mBackground.onBackProgressed(progress); + } + + private void onGestureCommitted() { + if (mEnteringTarget == null || mClosingTarget == null) { + finishAnimation(); + return; + } + // End the fade animations + mLeavingProgressSpring.cancel(); + mEnteringProgressSpring.cancel(); + + // We enter phase 2 of the animation, the starting coordinates for phase 2 are the current + // coordinate of the gesture driven phase. + mEnteringRect.round(mEnteringStartRect); + mTransaction.hide(mClosingTarget.leash); + + ValueAnimator valueAnimator = + ValueAnimator.ofFloat(1f, 0f).setDuration(POST_ANIMATION_DURATION); + valueAnimator.setInterpolator(new DecelerateInterpolator()); + valueAnimator.addUpdateListener(animation -> { + float progress = animation.getAnimatedFraction(); + updatePostCommitEnteringAnimation(progress); + mTransaction.apply(); + }); + + valueAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + finishAnimation(); + } + }); + valueAnimator.start(); + } + + private void updatePostCommitEnteringAnimation(float progress) { + float left = mapRange(progress, mEnteringStartRect.left, mStartTaskRect.left); + float top = mapRange(progress, mEnteringStartRect.top, mStartTaskRect.top); + float width = mapRange(progress, mEnteringStartRect.width(), mStartTaskRect.width()); + float height = mapRange(progress, mEnteringStartRect.height(), mStartTaskRect.height()); + float alpha = mapRange(progress, mEnteringProgress, 1.0f); + + mEnteringRect.set(left, top, left + width, top + height); + applyTransform(mEnteringTarget.leash, mEnteringRect, alpha); + } + + private float getEnteringProgress() { + return mEnteringProgress * SCALE_FACTOR; + } + + private void setEnteringProgress(float value) { + mEnteringProgress = value / SCALE_FACTOR; + if (mEnteringTarget != null && mEnteringTarget.leash != null) { + transformWithProgress( + mEnteringProgress, + Math.max( + smoothstep(ENTER_ALPHA_THRESHOLD, 1, mEnteringProgress), + MIN_WINDOW_ALPHA), /* alpha */ + mEnteringTarget.leash, + mEnteringRect, + -mWindowXShift, + 0 + ); + } + } + + private float getLeavingProgress() { + return mLeavingProgress * SCALE_FACTOR; + } + + private void setLeavingProgress(float value) { + mLeavingProgress = value / SCALE_FACTOR; + if (mClosingTarget != null && mClosingTarget.leash != null) { + transformWithProgress( + mLeavingProgress, + Math.max( + 1 - smoothstep(0, ENTER_ALPHA_THRESHOLD, mLeavingProgress), + MIN_WINDOW_ALPHA), + mClosingTarget.leash, + mClosingRect, + 0, + mWindowXShift + ); + } + } + + private void transformWithProgress(float progress, float alpha, SurfaceControl surface, + RectF targetRect, float deltaXMin, float deltaXMax) { + final float touchY = mTouchPos.y; + + final int width = mStartTaskRect.width(); + final int height = mStartTaskRect.height(); + + final float interpolatedProgress = INTERPOLATOR.getInterpolation(progress); + final float closingScale = MIN_WINDOW_SCALE + + (1 - interpolatedProgress) * (1 - MIN_WINDOW_SCALE); + final float closingWidth = closingScale * width; + final float closingHeight = (float) height / width * closingWidth; + + // Move the window along the X axis. + float closingLeft = mStartTaskRect.left + (width - closingWidth) / 2; + closingLeft += mapRange(interpolatedProgress, deltaXMin, deltaXMax); + + // Move the window along the Y axis. + final float deltaYRatio = (touchY - mInitialTouchPos.y) / height; + final float closingTop = (height - closingHeight) * 0.5f; + targetRect.set( + closingLeft, closingTop, closingLeft + closingWidth, closingTop + closingHeight); + + applyTransform(surface, targetRect, Math.max(alpha, MIN_WINDOW_ALPHA)); + mTransaction.apply(); + } + + private final class Callback extends IOnBackInvokedCallback.Default { + @Override + public void onBackStarted(BackMotionEvent backEvent) { + mProgressAnimator.onBackStarted(backEvent, + CrossActivityAnimation.this::onGestureProgress); + } + + @Override + public void onBackProgressed(@NonNull BackMotionEvent backEvent) { + mProgressAnimator.onBackProgressed(backEvent); + } + + @Override + public void onBackCancelled() { + mProgressAnimator.onBackCancelled(CrossActivityAnimation.this::finishAnimation); + } + + @Override + public void onBackInvoked() { + mProgressAnimator.reset(); + onGestureCommitted(); + } + } + + private final class Runner extends IRemoteAnimationRunner.Default { + @Override + public void onAnimationStart( + int transit, + RemoteAnimationTarget[] apps, + RemoteAnimationTarget[] wallpapers, + RemoteAnimationTarget[] nonApps, + IRemoteAnimationFinishedCallback finishedCallback) { + ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Start back to activity animation."); + for (RemoteAnimationTarget a : apps) { + if (a.mode == MODE_CLOSING) { + mClosingTarget = a; + } + if (a.mode == MODE_OPENING) { + mEnteringTarget = a; + } + } + + startBackAnimation(); + mFinishCallback = finishedCallback; + } + + @Override + public void onAnimationCancelled(boolean isKeyguardOccluded) { + finishAnimation(); + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java new file mode 100644 index 000000000000..a7dd27a0784f --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java @@ -0,0 +1,364 @@ +/* + * 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.back; + +import static android.view.RemoteAnimationTarget.MODE_CLOSING; +import static android.view.RemoteAnimationTarget.MODE_OPENING; +import static android.window.BackEvent.EDGE_RIGHT; + +import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BACK_PREVIEW; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; +import android.annotation.NonNull; +import android.content.Context; +import android.graphics.Matrix; +import android.graphics.PointF; +import android.graphics.Rect; +import android.graphics.RectF; +import android.os.RemoteException; +import android.view.IRemoteAnimationFinishedCallback; +import android.view.IRemoteAnimationRunner; +import android.view.RemoteAnimationTarget; +import android.view.SurfaceControl; +import android.view.animation.AccelerateDecelerateInterpolator; +import android.view.animation.Interpolator; +import android.window.BackEvent; +import android.window.BackMotionEvent; +import android.window.BackProgressAnimator; +import android.window.IOnBackInvokedCallback; + +import com.android.internal.policy.ScreenDecorationsUtils; +import com.android.internal.protolog.common.ProtoLog; +import com.android.wm.shell.common.annotations.ShellMainThread; + +/** + * Controls the animation of swiping back and returning to another task. + * + * This is a two part animation. The first part is an animation that tracks gesture location to + * scale and move the closing and entering app windows. + * Once the gesture is committed, the second part remains the closing window in place. + * The entering window plays the rest of app opening transition to enter full screen. + * + * This animation is used only for apps that enable back dispatching via + * {@link android.window.OnBackInvokedDispatcher}. The controller registers + * an {@link IOnBackInvokedCallback} with WM Shell and receives back dispatches when a back + * navigation to launcher starts. + */ +@ShellMainThread +class CrossTaskBackAnimation { + private static final int BACKGROUNDCOLOR = 0x43433A; + + /** + * Minimum scale of the entering window. + */ + private static final float ENTERING_MIN_WINDOW_SCALE = 0.85f; + + /** + * Minimum scale of the closing window. + */ + private static final float CLOSING_MIN_WINDOW_SCALE = 0.75f; + + /** + * Minimum color scale of the closing window. + */ + private static final float CLOSING_MIN_WINDOW_COLOR_SCALE = 0.1f; + + /** + * The margin between the entering window and the closing window + */ + private static final int WINDOW_MARGIN = 35; + + /** Max window translation in the Y axis. */ + private static final int WINDOW_MAX_DELTA_Y = 160; + + private final Rect mStartTaskRect = new Rect(); + private final float mCornerRadius; + + // The closing window properties. + private final RectF mClosingCurrentRect = new RectF(); + + // The entering window properties. + private final Rect mEnteringStartRect = new Rect(); + private final RectF mEnteringCurrentRect = new RectF(); + + private final PointF mInitialTouchPos = new PointF(); + private final Interpolator mInterpolator = new AccelerateDecelerateInterpolator(); + + private final Matrix mTransformMatrix = new Matrix(); + + private final float[] mTmpFloat9 = new float[9]; + private final float[] mTmpTranslate = {0, 0, 0}; + + private RemoteAnimationTarget mEnteringTarget; + private RemoteAnimationTarget mClosingTarget; + private SurfaceControl.Transaction mTransaction = new SurfaceControl.Transaction(); + + private boolean mBackInProgress = false; + + private boolean mIsRightEdge; + private float mProgress = 0; + private PointF mTouchPos = new PointF(); + private IRemoteAnimationFinishedCallback mFinishCallback; + private BackProgressAnimator mProgressAnimator = new BackProgressAnimator(); + final BackAnimationRunner mBackAnimationRunner; + + private final BackAnimationBackground mBackground; + + CrossTaskBackAnimation(Context context, BackAnimationBackground background) { + mCornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context); + mBackAnimationRunner = new BackAnimationRunner(new Callback(), new Runner()); + mBackground = background; + } + + private float getInterpolatedProgress(float backProgress) { + return 1 - (1 - backProgress) * (1 - backProgress) * (1 - backProgress); + } + + private void startBackAnimation() { + if (mEnteringTarget == null || mClosingTarget == null) { + ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Entering target or closing target is null."); + return; + } + + // Offset start rectangle to align task bounds. + mStartTaskRect.set(mClosingTarget.windowConfiguration.getBounds()); + mStartTaskRect.offsetTo(0, 0); + + // Draw background. + mBackground.ensureBackground(mClosingTarget.windowConfiguration.getBounds(), + BACKGROUNDCOLOR, mTransaction); + } + + private void updateGestureBackProgress(float progress, BackEvent event) { + if (mEnteringTarget == null || mClosingTarget == null) { + return; + } + + float touchX = event.getTouchX(); + float touchY = event.getTouchY(); + float dX = Math.abs(touchX - mInitialTouchPos.x); + + // The 'follow width' is the width of the window if it completely matches + // the gesture displacement. + final int width = mStartTaskRect.width(); + final int height = mStartTaskRect.height(); + + // The 'progress width' is the width of the window if it strictly linearly interpolates + // to minimum scale base on progress. + float enteringScale = mapRange(progress, 1, ENTERING_MIN_WINDOW_SCALE); + float closingScale = mapRange(progress, 1, CLOSING_MIN_WINDOW_SCALE); + float closingColorScale = mapRange(progress, 1, CLOSING_MIN_WINDOW_COLOR_SCALE); + + // The final width is derived from interpolating between the follow with and progress width + // using gesture progress. + float enteringWidth = enteringScale * width; + float closingWidth = closingScale * width; + float enteringHeight = (float) height / width * enteringWidth; + float closingHeight = (float) height / width * closingWidth; + + float deltaYRatio = (touchY - mInitialTouchPos.y) / height; + // Base the window movement in the Y axis on the touch movement in the Y axis. + float deltaY = (float) Math.sin(deltaYRatio * Math.PI * 0.5f) * WINDOW_MAX_DELTA_Y; + // Move the window along the Y axis. + float closingTop = (height - closingHeight) * 0.5f + deltaY; + float enteringTop = (height - enteringHeight) * 0.5f + deltaY; + // Move the window along the X axis. + float right = width - (progress * WINDOW_MARGIN); + float left = right - closingWidth; + + mClosingCurrentRect.set(left, closingTop, right, closingTop + closingHeight); + mEnteringCurrentRect.set(left - enteringWidth - WINDOW_MARGIN, enteringTop, + left - WINDOW_MARGIN, enteringTop + enteringHeight); + + applyTransform(mClosingTarget.leash, mClosingCurrentRect, mCornerRadius); + applyColorTransform(mClosingTarget.leash, closingColorScale); + applyTransform(mEnteringTarget.leash, mEnteringCurrentRect, mCornerRadius); + mTransaction.apply(); + + mBackground.onBackProgressed(progress); + } + + private void updatePostCommitClosingAnimation(float progress) { + mTransaction.setLayer(mClosingTarget.leash, 0); + float alpha = mapRange(progress, 1, 0); + mTransaction.setAlpha(mClosingTarget.leash, alpha); + } + + private void updatePostCommitEnteringAnimation(float progress) { + float left = mapRange(progress, mEnteringStartRect.left, mStartTaskRect.left); + float top = mapRange(progress, mEnteringStartRect.top, mStartTaskRect.top); + float width = mapRange(progress, mEnteringStartRect.width(), mStartTaskRect.width()); + float height = mapRange(progress, mEnteringStartRect.height(), mStartTaskRect.height()); + + mEnteringCurrentRect.set(left, top, left + width, top + height); + applyTransform(mEnteringTarget.leash, mEnteringCurrentRect, mCornerRadius); + } + + /** Transform the target window to match the target rect. */ + private void applyTransform(SurfaceControl leash, RectF targetRect, float cornerRadius) { + if (leash == null) { + return; + } + + final float scale = targetRect.width() / mStartTaskRect.width(); + mTransformMatrix.reset(); + mTransformMatrix.setScale(scale, scale); + mTransformMatrix.postTranslate(targetRect.left, targetRect.top); + mTransaction.setMatrix(leash, mTransformMatrix, mTmpFloat9) + .setWindowCrop(leash, mStartTaskRect) + .setCornerRadius(leash, cornerRadius); + } + + private void applyColorTransform(SurfaceControl leash, float colorScale) { + if (leash == null) { + return; + } + computeScaleTransformMatrix(colorScale, mTmpFloat9); + mTransaction.setColorTransform(leash, mTmpFloat9, mTmpTranslate); + } + + static void computeScaleTransformMatrix(float scale, float[] matrix) { + matrix[0] = scale; + matrix[1] = 0; + matrix[2] = 0; + matrix[3] = 0; + matrix[4] = scale; + matrix[5] = 0; + matrix[6] = 0; + matrix[7] = 0; + matrix[8] = scale; + } + + private void finishAnimation() { + if (mEnteringTarget != null) { + mEnteringTarget.leash.release(); + mEnteringTarget = null; + } + if (mClosingTarget != null) { + mClosingTarget.leash.release(); + mClosingTarget = null; + } + + if (mBackground != null) { + mBackground.removeBackground(mTransaction); + } + + mTransaction.apply(); + mBackInProgress = false; + mTransformMatrix.reset(); + mClosingCurrentRect.setEmpty(); + mInitialTouchPos.set(0, 0); + + if (mFinishCallback != null) { + try { + mFinishCallback.onAnimationFinished(); + } catch (RemoteException e) { + e.printStackTrace(); + } + mFinishCallback = null; + } + } + + private void onGestureProgress(@NonNull BackEvent backEvent) { + if (!mBackInProgress) { + mInitialTouchPos.set(backEvent.getTouchX(), backEvent.getTouchY()); + mIsRightEdge = backEvent.getSwipeEdge() == EDGE_RIGHT; + mBackInProgress = true; + } + mProgress = backEvent.getProgress(); + mTouchPos.set(backEvent.getTouchX(), backEvent.getTouchY()); + updateGestureBackProgress(getInterpolatedProgress(mProgress), backEvent); + } + + private void onGestureCommitted() { + if (mEnteringTarget == null || mClosingTarget == null) { + finishAnimation(); + return; + } + + // We enter phase 2 of the animation, the starting coordinates for phase 2 are the current + // coordinate of the gesture driven phase. + mEnteringCurrentRect.round(mEnteringStartRect); + + ValueAnimator valueAnimator = ValueAnimator.ofFloat(1f, 0f).setDuration(300); + valueAnimator.setInterpolator(mInterpolator); + valueAnimator.addUpdateListener(animation -> { + float progress = animation.getAnimatedFraction(); + updatePostCommitEnteringAnimation(progress); + updatePostCommitClosingAnimation(progress); + mTransaction.apply(); + }); + + valueAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + finishAnimation(); + } + }); + valueAnimator.start(); + } + + private static float mapRange(float value, float min, float max) { + return min + (value * (max - min)); + } + + private final class Callback extends IOnBackInvokedCallback.Default { + @Override + public void onBackStarted(BackMotionEvent backEvent) { + mProgressAnimator.onBackStarted(backEvent, + CrossTaskBackAnimation.this::onGestureProgress); + } + + @Override + public void onBackProgressed(@NonNull BackMotionEvent backEvent) { + mProgressAnimator.onBackProgressed(backEvent); + } + + @Override + public void onBackCancelled() { + mProgressAnimator.onBackCancelled(CrossTaskBackAnimation.this::finishAnimation); + } + + @Override + public void onBackInvoked() { + mProgressAnimator.reset(); + onGestureCommitted(); + } + }; + + private final class Runner extends IRemoteAnimationRunner.Default { + @Override + public void onAnimationStart(int transit, RemoteAnimationTarget[] apps, + RemoteAnimationTarget[] wallpapers, RemoteAnimationTarget[] nonApps, + IRemoteAnimationFinishedCallback finishedCallback) { + ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Start back to task animation."); + for (RemoteAnimationTarget a : apps) { + if (a.mode == MODE_CLOSING) { + mClosingTarget = a; + } + if (a.mode == MODE_OPENING) { + mEnteringTarget = a; + } + } + + startBackAnimation(); + mFinishCallback = finishedCallback; + } + }; +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CustomizeActivityAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CustomizeActivityAnimation.java new file mode 100644 index 000000000000..f0c5d8b29b2f --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CustomizeActivityAnimation.java @@ -0,0 +1,419 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.back; + +import static android.view.RemoteAnimationTarget.MODE_CLOSING; +import static android.view.RemoteAnimationTarget.MODE_OPENING; + +import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BACK_PREVIEW; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.Activity; +import android.content.Context; +import android.graphics.Color; +import android.graphics.Rect; +import android.os.RemoteException; +import android.util.FloatProperty; +import android.view.Choreographer; +import android.view.IRemoteAnimationFinishedCallback; +import android.view.IRemoteAnimationRunner; +import android.view.RemoteAnimationTarget; +import android.view.SurfaceControl; +import android.view.WindowManager.LayoutParams; +import android.view.animation.Animation; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.Transformation; +import android.window.BackEvent; +import android.window.BackMotionEvent; +import android.window.BackNavigationInfo; +import android.window.BackProgressAnimator; +import android.window.IOnBackInvokedCallback; + +import com.android.internal.R; +import com.android.internal.dynamicanimation.animation.SpringAnimation; +import com.android.internal.dynamicanimation.animation.SpringForce; +import com.android.internal.policy.ScreenDecorationsUtils; +import com.android.internal.policy.TransitionAnimation; +import com.android.internal.protolog.common.ProtoLog; +import com.android.wm.shell.common.annotations.ShellMainThread; + +/** + * Class that handle customized close activity transition animation. + */ +@ShellMainThread +class CustomizeActivityAnimation { + private final BackProgressAnimator mProgressAnimator = new BackProgressAnimator(); + final BackAnimationRunner mBackAnimationRunner; + private final float mCornerRadius; + private final SurfaceControl.Transaction mTransaction; + private final BackAnimationBackground mBackground; + private RemoteAnimationTarget mEnteringTarget; + private RemoteAnimationTarget mClosingTarget; + private IRemoteAnimationFinishedCallback mFinishCallback; + /** Duration of post animation after gesture committed. */ + private static final int POST_ANIMATION_DURATION = 250; + + private static final int SCALE_FACTOR = 1000; + private final SpringAnimation mProgressSpring; + private float mLatestProgress = 0.0f; + + private static final float TARGET_COMMIT_PROGRESS = 0.5f; + + private final float[] mTmpFloat9 = new float[9]; + private final DecelerateInterpolator mDecelerateInterpolator = new DecelerateInterpolator(); + + final CustomAnimationLoader mCustomAnimationLoader; + private Animation mEnterAnimation; + private Animation mCloseAnimation; + private int mNextBackgroundColor; + final Transformation mTransformation = new Transformation(); + + private final Choreographer mChoreographer; + + CustomizeActivityAnimation(Context context, BackAnimationBackground background) { + this(context, background, new SurfaceControl.Transaction(), null); + } + + CustomizeActivityAnimation(Context context, BackAnimationBackground background, + SurfaceControl.Transaction transaction, Choreographer choreographer) { + mCornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context); + mBackground = background; + mBackAnimationRunner = new BackAnimationRunner(new Callback(), new Runner()); + mCustomAnimationLoader = new CustomAnimationLoader(context); + + mProgressSpring = new SpringAnimation(this, ENTER_PROGRESS_PROP); + mProgressSpring.setSpring(new SpringForce() + .setStiffness(SpringForce.STIFFNESS_MEDIUM) + .setDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY)); + mTransaction = transaction == null ? new SurfaceControl.Transaction() : transaction; + mChoreographer = choreographer != null ? choreographer : Choreographer.getInstance(); + } + + private float getLatestProgress() { + return mLatestProgress * SCALE_FACTOR; + } + private void setLatestProgress(float value) { + mLatestProgress = value / SCALE_FACTOR; + applyTransformTransaction(mLatestProgress); + } + + private static final FloatProperty<CustomizeActivityAnimation> ENTER_PROGRESS_PROP = + new FloatProperty<>("enter") { + @Override + public void setValue(CustomizeActivityAnimation anim, float value) { + anim.setLatestProgress(value); + } + + @Override + public Float get(CustomizeActivityAnimation object) { + return object.getLatestProgress(); + } + }; + + // The target will lose focus when alpha == 0, so keep a minimum value for it. + private static float keepMinimumAlpha(float transAlpha) { + return Math.max(transAlpha, 0.005f); + } + + private static void initializeAnimation(Animation animation, Rect bounds) { + final int width = bounds.width(); + final int height = bounds.height(); + animation.initialize(width, height, width, height); + } + + private void startBackAnimation() { + if (mEnteringTarget == null || mClosingTarget == null + || mCloseAnimation == null || mEnterAnimation == null) { + ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Entering target or closing target is null."); + return; + } + initializeAnimation(mCloseAnimation, mClosingTarget.localBounds); + initializeAnimation(mEnterAnimation, mEnteringTarget.localBounds); + + // Draw background with task background color. + if (mEnteringTarget.taskInfo != null && mEnteringTarget.taskInfo.taskDescription != null) { + mBackground.ensureBackground(mClosingTarget.windowConfiguration.getBounds(), + mNextBackgroundColor == Color.TRANSPARENT + ? mEnteringTarget.taskInfo.taskDescription.getBackgroundColor() + : mNextBackgroundColor, + mTransaction); + } + } + + private void applyTransformTransaction(float progress) { + if (mClosingTarget == null || mEnteringTarget == null) { + return; + } + applyTransform(mClosingTarget.leash, progress, mCloseAnimation); + applyTransform(mEnteringTarget.leash, progress, mEnterAnimation); + mTransaction.setFrameTimelineVsync(mChoreographer.getVsyncId()); + mTransaction.apply(); + } + + private void applyTransform(SurfaceControl leash, float progress, Animation animation) { + mTransformation.clear(); + animation.getTransformationAt(progress, mTransformation); + mTransaction.setMatrix(leash, mTransformation.getMatrix(), mTmpFloat9); + mTransaction.setAlpha(leash, keepMinimumAlpha(mTransformation.getAlpha())); + mTransaction.setCornerRadius(leash, mCornerRadius); + } + + void finishAnimation() { + if (mCloseAnimation != null) { + mCloseAnimation.reset(); + mCloseAnimation = null; + } + if (mEnterAnimation != null) { + mEnterAnimation.reset(); + mEnterAnimation = null; + } + if (mEnteringTarget != null) { + mEnteringTarget.leash.release(); + mEnteringTarget = null; + } + if (mClosingTarget != null) { + mClosingTarget.leash.release(); + mClosingTarget = null; + } + if (mBackground != null) { + mBackground.removeBackground(mTransaction); + } + mTransaction.setFrameTimelineVsync(mChoreographer.getVsyncId()); + mTransaction.apply(); + mTransformation.clear(); + mLatestProgress = 0; + mNextBackgroundColor = Color.TRANSPARENT; + if (mFinishCallback != null) { + try { + mFinishCallback.onAnimationFinished(); + } catch (RemoteException e) { + e.printStackTrace(); + } + mFinishCallback = null; + } + mProgressSpring.animateToFinalPosition(0); + mProgressSpring.skipToEnd(); + } + + void onGestureProgress(@NonNull BackEvent backEvent) { + if (mEnteringTarget == null || mClosingTarget == null + || mCloseAnimation == null || mEnterAnimation == null) { + return; + } + + final float progress = backEvent.getProgress(); + + float springProgress = (progress > 0.1f + ? mapLinear(progress, 0.1f, 1f, TARGET_COMMIT_PROGRESS, 1f) + : mapLinear(progress, 0, 1f, 0f, TARGET_COMMIT_PROGRESS)) * SCALE_FACTOR; + + mProgressSpring.animateToFinalPosition(springProgress); + } + + static float mapLinear(float x, float a1, float a2, float b1, float b2) { + return b1 + (x - a1) * (b2 - b1) / (a2 - a1); + } + + void onGestureCommitted() { + if (mEnteringTarget == null || mClosingTarget == null + || mCloseAnimation == null || mEnterAnimation == null) { + finishAnimation(); + return; + } + mProgressSpring.cancel(); + + // Enter phase 2 of the animation + final ValueAnimator valueAnimator = ValueAnimator.ofFloat(mLatestProgress, 1f) + .setDuration(POST_ANIMATION_DURATION); + valueAnimator.setInterpolator(mDecelerateInterpolator); + valueAnimator.addUpdateListener(animation -> { + float progress = (float) animation.getAnimatedValue(); + applyTransformTransaction(progress); + }); + + valueAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + finishAnimation(); + } + }); + valueAnimator.start(); + } + + /** + * Load customize animation before animation start. + */ + boolean prepareNextAnimation(BackNavigationInfo.CustomAnimationInfo animationInfo) { + final AnimationLoadResult result = mCustomAnimationLoader.loadAll(animationInfo); + if (result != null) { + mCloseAnimation = result.mCloseAnimation; + mEnterAnimation = result.mEnterAnimation; + mNextBackgroundColor = result.mBackgroundColor; + return true; + } + return false; + } + + private final class Callback extends IOnBackInvokedCallback.Default { + @Override + public void onBackStarted(BackMotionEvent backEvent) { + mProgressAnimator.onBackStarted(backEvent, + CustomizeActivityAnimation.this::onGestureProgress); + } + + @Override + public void onBackProgressed(@NonNull BackMotionEvent backEvent) { + mProgressAnimator.onBackProgressed(backEvent); + } + + @Override + public void onBackCancelled() { + mProgressAnimator.onBackCancelled(CustomizeActivityAnimation.this::finishAnimation); + } + + @Override + public void onBackInvoked() { + mProgressAnimator.reset(); + onGestureCommitted(); + } + } + + private final class Runner extends IRemoteAnimationRunner.Default { + @Override + public void onAnimationStart( + int transit, + RemoteAnimationTarget[] apps, + RemoteAnimationTarget[] wallpapers, + RemoteAnimationTarget[] nonApps, + IRemoteAnimationFinishedCallback finishedCallback) { + ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Start back to customize animation."); + for (RemoteAnimationTarget a : apps) { + if (a.mode == MODE_CLOSING) { + mClosingTarget = a; + } + if (a.mode == MODE_OPENING) { + mEnteringTarget = a; + } + } + if (mCloseAnimation == null || mEnterAnimation == null) { + ProtoLog.d(WM_SHELL_BACK_PREVIEW, + "No animation loaded, should choose cross-activity animation?"); + } + + startBackAnimation(); + mFinishCallback = finishedCallback; + } + + @Override + public void onAnimationCancelled(boolean isKeyguardOccluded) { + finishAnimation(); + } + } + + + static final class AnimationLoadResult { + Animation mCloseAnimation; + Animation mEnterAnimation; + int mBackgroundColor; + } + + /** + * Helper class to load custom animation. + */ + static class CustomAnimationLoader { + final TransitionAnimation mTransitionAnimation; + + CustomAnimationLoader(Context context) { + mTransitionAnimation = new TransitionAnimation( + context, false /* debug */, "CustomizeBackAnimation"); + } + + /** + * Load both enter and exit animation for the close activity transition. + * Note that the result is only valid if the exit animation has set and loaded success. + * If the entering animation has not set(i.e. 0), here will load the default entering + * animation for it. + * + * @param animationInfo The information of customize animation, which can be set from + * {@link Activity#overrideActivityTransition} and/or + * {@link LayoutParams#windowAnimations} + */ + AnimationLoadResult loadAll(BackNavigationInfo.CustomAnimationInfo animationInfo) { + if (animationInfo.getPackageName().isEmpty()) { + return null; + } + final Animation close = loadAnimation(animationInfo, false); + if (close == null) { + return null; + } + final Animation open = loadAnimation(animationInfo, true); + AnimationLoadResult result = new AnimationLoadResult(); + result.mCloseAnimation = close; + result.mEnterAnimation = open; + result.mBackgroundColor = animationInfo.getCustomBackground(); + return result; + } + + /** + * Load enter or exit animation from CustomAnimationInfo + * @param animationInfo The information for customize animation. + * @param enterAnimation true when load for enter animation, false for exit animation. + * @return Loaded animation. + */ + @Nullable + Animation loadAnimation(BackNavigationInfo.CustomAnimationInfo animationInfo, + boolean enterAnimation) { + Animation a = null; + // Activity#overrideActivityTransition has higher priority than windowAnimations + // Try to get animation from Activity#overrideActivityTransition + if ((enterAnimation && animationInfo.getCustomEnterAnim() != 0) + || (!enterAnimation && animationInfo.getCustomExitAnim() != 0)) { + a = mTransitionAnimation.loadAppTransitionAnimation( + animationInfo.getPackageName(), + enterAnimation ? animationInfo.getCustomEnterAnim() + : animationInfo.getCustomExitAnim()); + } else if (animationInfo.getWindowAnimations() != 0) { + // try to get animation from LayoutParams#windowAnimations + a = mTransitionAnimation.loadAnimationAttr(animationInfo.getPackageName(), + animationInfo.getWindowAnimations(), enterAnimation + ? R.styleable.WindowAnimation_activityCloseEnterAnimation + : R.styleable.WindowAnimation_activityCloseExitAnimation, + false /* translucent */); + } + // Only allow to load default animation for opening target. + if (a == null && enterAnimation) { + a = loadDefaultOpenAnimation(); + } + if (a != null) { + ProtoLog.d(WM_SHELL_BACK_PREVIEW, "custom animation loaded %s", a); + } else { + ProtoLog.e(WM_SHELL_BACK_PREVIEW, "No custom animation loaded"); + } + return a; + } + + private Animation loadDefaultOpenAnimation() { + return mTransitionAnimation.loadDefaultAnimationAttr( + R.styleable.WindowAnimation_activityCloseEnterAnimation, + false /* translucent */); + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/IBackAnimation.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/back/IBackAnimation.aidl index 6311f879fd45..1a35de47e977 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/IBackAnimation.aidl +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/IBackAnimation.aidl @@ -16,18 +16,20 @@ package com.android.wm.shell.back; +import com.android.internal.view.AppearanceRegion; +import android.view.IRemoteAnimationRunner; import android.window.IOnBackInvokedCallback; /** * Interface for Launcher process to register back invocation callbacks. */ interface IBackAnimation { - /** - * Sets a {@link IOnBackInvokedCallback} to be invoked when + * Sets a {@link IOnBackInvokedCallback} and a {@link IRemoteAnimationRunner} to be invoked when * back navigation has type {@link BackNavigationInfo#TYPE_RETURN_TO_HOME}. */ - void setBackToLauncherCallback(in IOnBackInvokedCallback callback); + void setBackToLauncherCallback(in IOnBackInvokedCallback callback, + in IRemoteAnimationRunner runner); /** * Clears the previously registered {@link IOnBackInvokedCallback}. @@ -35,11 +37,7 @@ interface IBackAnimation { void clearBackToLauncherCallback(); /** - * Notifies Shell that the back to launcher animation has fully finished - * (including the transition animation that runs after the finger is lifted). - * - * At this point the top window leash (if one was created) should be ready to be released. - * //TODO: Remove once we play the transition animation through shell transitions. + * Uses launcher flags to update the system bar color. */ - void onBackToLauncherAnimationFinished(); + void customizeStatusBarAppearance(in AppearanceRegion appearance); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/OWNERS b/libs/WindowManager/Shell/src/com/android/wm/shell/back/OWNERS new file mode 100644 index 000000000000..1e0f9bc6322f --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/OWNERS @@ -0,0 +1,5 @@ +# WM shell sub-module back navigation owners +# Bug component: 1152663 +shanh@google.com +arthurhung@google.com +wilsonshih@google.com diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/StatusBarCustomizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/StatusBarCustomizer.java new file mode 100644 index 000000000000..5e876127e9b4 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/StatusBarCustomizer.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.back; + +import com.android.internal.view.AppearanceRegion; + +/** + * Interface to customize the system bar color. + */ +public interface StatusBarCustomizer { + /** + * Called when the status bar color needs to be changed. + * @param appearance The region of appearance. + */ + void customizeStatusBarAppearance(AppearanceRegion appearance); +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/TEST_MAPPING b/libs/WindowManager/Shell/src/com/android/wm/shell/back/TEST_MAPPING new file mode 100644 index 000000000000..837d5ff3b073 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/TEST_MAPPING @@ -0,0 +1,32 @@ +{ + "presubmit": [ + { + "name": "WMShellUnitTests", + "options": [ + { + "exclude-annotation": "androidx.test.filters.FlakyTest" + }, + { + "include-filter": "com.android.wm.shell.back" + } + ] + }, + { + "name": "CtsWindowManagerDeviceTestCases", + "options": [ + { + "exclude-annotation": "androidx.test.filters.FlakyTest" + }, + { + "include-filter": "android.server.wm.BackGestureInvokedTest" + }, + { + "include-filter": "android.server.wm.BackNavigationTests" + }, + { + "include-filter": "android.server.wm.OnBackInvokedCallbackGestureTest" + } + ] + } + ] +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/TouchTracker.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/TouchTracker.java new file mode 100644 index 000000000000..695ef4e66302 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/TouchTracker.java @@ -0,0 +1,120 @@ +/* + * 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.back; + +import android.os.SystemProperties; +import android.view.RemoteAnimationTarget; +import android.window.BackEvent; +import android.window.BackMotionEvent; + +/** + * Helper class to record the touch location for gesture and generate back events. + */ +class TouchTracker { + private static final String PREDICTIVE_BACK_PROGRESS_THRESHOLD_PROP = + "persist.wm.debug.predictive_back_progress_threshold"; + private static final int PROGRESS_THRESHOLD = SystemProperties + .getInt(PREDICTIVE_BACK_PROGRESS_THRESHOLD_PROP, -1); + private float mProgressThreshold; + /** + * Location of the latest touch event + */ + private float mLatestTouchX; + private float mLatestTouchY; + private boolean mTriggerBack; + + /** + * Location of the initial touch event of the back gesture. + */ + private float mInitTouchX; + private float mInitTouchY; + private float mStartThresholdX; + private int mSwipeEdge; + private boolean mCancelled; + + void update(float touchX, float touchY) { + /** + * If back was previously cancelled but the user has started swiping in the forward + * direction again, restart back. + */ + if (mCancelled && ((touchX > mLatestTouchX && mSwipeEdge == BackEvent.EDGE_LEFT) + || touchX < mLatestTouchX && mSwipeEdge == BackEvent.EDGE_RIGHT)) { + mCancelled = false; + mStartThresholdX = touchX; + } + mLatestTouchX = touchX; + mLatestTouchY = touchY; + } + + void setTriggerBack(boolean triggerBack) { + if (mTriggerBack != triggerBack && !triggerBack) { + mCancelled = true; + } + mTriggerBack = triggerBack; + } + + void setGestureStartLocation(float touchX, float touchY, int swipeEdge) { + mInitTouchX = touchX; + mInitTouchY = touchY; + mSwipeEdge = swipeEdge; + mStartThresholdX = mInitTouchX; + } + + void reset() { + mInitTouchX = 0; + mInitTouchY = 0; + mStartThresholdX = 0; + mCancelled = false; + mTriggerBack = false; + mSwipeEdge = BackEvent.EDGE_LEFT; + } + + BackMotionEvent createStartEvent(RemoteAnimationTarget target) { + return new BackMotionEvent(mInitTouchX, mInitTouchY, 0, mSwipeEdge, target); + } + + BackMotionEvent createProgressEvent() { + float progressThreshold = PROGRESS_THRESHOLD >= 0 + ? PROGRESS_THRESHOLD : mProgressThreshold; + progressThreshold = progressThreshold == 0 ? 1 : progressThreshold; + float progress = 0; + // Progress is always 0 when back is cancelled and not restarted. + if (!mCancelled) { + // If back is committed, progress is the distance between the last and first touch + // point, divided by the max drag distance. Otherwise, it's the distance between + // the last touch point and the starting threshold, divided by max drag distance. + // The starting threshold is initially the first touch location, and updated to + // the location everytime back is restarted after being cancelled. + float startX = mTriggerBack ? mInitTouchX : mStartThresholdX; + float deltaX = Math.max( + mSwipeEdge == BackEvent.EDGE_LEFT + ? mLatestTouchX - startX + : startX - mLatestTouchX, + 0); + progress = Math.min(Math.max(deltaX / progressThreshold, 0), 1); + } + return createProgressEvent(progress); + } + + BackMotionEvent createProgressEvent(float progress) { + return new BackMotionEvent(mLatestTouchX, mLatestTouchY, progress, mSwipeEdge, null); + } + + public void setProgressThreshold(float progressThreshold) { + mProgressThreshold = progressThreshold; + } +} 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..5f2b63089009 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 @@ -21,6 +21,7 @@ import static android.os.AsyncTask.Status.FINISHED; import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE; import android.annotation.DimenRes; +import android.annotation.Hide; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.Notification; @@ -46,6 +47,7 @@ import android.util.Log; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.logging.InstanceId; +import com.android.wm.shell.common.bubbles.BubbleInfo; import java.io.PrintWriter; import java.util.List; @@ -59,6 +61,8 @@ import java.util.concurrent.Executor; public class Bubble implements BubbleViewProvider { private static final String TAG = "Bubble"; + public static final String KEY_APP_BUBBLE = "key_app_bubble"; + private final String mKey; @Nullable private final String mGroupKey; @@ -123,7 +127,7 @@ public class Bubble implements BubbleViewProvider { private Icon mIcon; private boolean mIsBubble; private boolean mIsTextChanged; - private boolean mIsClearable; + private boolean mIsDismissable; private boolean mShouldSuppressNotificationDot; private boolean mShouldSuppressNotificationList; private boolean mShouldSuppressPeek; @@ -164,13 +168,22 @@ public class Bubble implements BubbleViewProvider { private PendingIntent mDeleteIntent; /** + * Used only for a special bubble in the stack that has the key {@link #KEY_APP_BUBBLE}. + * There can only be one of these bubbles in the stack and this intent will be populated for + * that bubble. + */ + @Nullable + private Intent mAppIntent; + + /** * Create a bubble with limited information based on given {@link ShortcutInfo}. * Note: Currently this is only being used when the bubble is persisted to disk. */ @VisibleForTesting(visibility = PRIVATE) public Bubble(@NonNull final String key, @NonNull final ShortcutInfo shortcutInfo, final int desiredHeight, final int desiredHeightResId, @Nullable final String title, - int taskId, @Nullable final String locus, Executor mainExecutor) { + int taskId, @Nullable final String locus, boolean isDismissable, Executor mainExecutor, + final Bubbles.BubbleMetadataFlagListener listener) { Objects.requireNonNull(key); Objects.requireNonNull(shortcutInfo); mMetadataShortcutId = shortcutInfo.getId(); @@ -178,6 +191,7 @@ public class Bubble implements BubbleViewProvider { mKey = key; mGroupKey = null; mLocusId = locus != null ? new LocusId(locus) : null; + mIsDismissable = isDismissable; mFlags = 0; mUser = shortcutInfo.getUserHandle(); mPackageName = shortcutInfo.getPackage(); @@ -188,11 +202,30 @@ public class Bubble implements BubbleViewProvider { mShowBubbleUpdateDot = false; mMainExecutor = mainExecutor; mTaskId = taskId; + mBubbleMetadataFlagListener = listener; + } + + public Bubble(Intent intent, + UserHandle user, + @Nullable Icon icon, + Executor mainExecutor) { + mKey = KEY_APP_BUBBLE; + mGroupKey = null; + mLocusId = null; + mFlags = 0; + mUser = user; + mIcon = icon; + mShowBubbleUpdateDot = false; + mMainExecutor = mainExecutor; + mTaskId = INVALID_TASK_ID; + mAppIntent = intent; + mDesiredHeight = Integer.MAX_VALUE; + mPackageName = intent.getPackage(); } @VisibleForTesting(visibility = PRIVATE) public Bubble(@NonNull final BubbleEntry entry, - @Nullable final Bubbles.BubbleMetadataFlagListener listener, + final Bubbles.BubbleMetadataFlagListener listener, final Bubbles.PendingIntentCanceledListener intentCancelListener, Executor mainExecutor) { mKey = entry.getKey(); @@ -212,11 +245,26 @@ public class Bubble implements BubbleViewProvider { setEntry(entry); } + /** Converts this bubble into a {@link BubbleInfo} object to be shared with external callers. */ + public BubbleInfo asBubbleBarBubble() { + return new BubbleInfo(getKey(), + getFlags(), + getShortcutInfo().getId(), + getIcon(), + getUser().getIdentifier(), + getPackageName()); + } + @Override public String getKey() { return mKey; } + @Hide + public boolean isDismissable() { + return mIsDismissable; + } + /** * @see StatusBarNotification#getGroupKey() * @return the group key for this bubble, if one exists. @@ -415,6 +463,9 @@ public class Bubble implements BubbleViewProvider { mShortcutInfo = info.shortcutInfo; mAppName = info.appName; + if (mTitle == null) { + mTitle = mAppName; + } mFlyoutMessage = info.flyoutMessage; mBadgeBitmap = info.badgeBitmap; @@ -452,6 +503,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(); @@ -494,14 +546,23 @@ public class Bubble implements BubbleViewProvider { mDeleteIntent = entry.getBubbleMetadata().getDeleteIntent(); } - mIsClearable = entry.isClearable(); + mIsDismissable = entry.isDismissable(); mShouldSuppressNotificationDot = entry.shouldSuppressNotificationDot(); mShouldSuppressNotificationList = entry.shouldSuppressNotificationList(); mShouldSuppressPeek = entry.shouldSuppressPeek(); + if (showingDotPreviously != showDot()) { + // This will update the UI if needed + setShowDot(showDot()); + } } + /** + * @return the icon set on BubbleMetadata, if it exists. This is only non-null for bubbles + * created via a PendingIntent. This is null for bubbles created by a shortcut, as we use the + * icon from the shortcut. + */ @Nullable - Icon getIcon() { + public Icon getIcon() { return mIcon; } @@ -513,7 +574,7 @@ public class Bubble implements BubbleViewProvider { * @return the last time this bubble was updated or accessed, whichever is most recent. */ long getLastActivity() { - return Math.max(mLastUpdated, mLastAccessed); + return isAppBubble() ? Long.MAX_VALUE : Math.max(mLastUpdated, mLastAccessed); } /** @@ -569,7 +630,7 @@ public class Bubble implements BubbleViewProvider { * Whether this notification should be shown in the shade. */ boolean showInShade() { - return !shouldSuppressNotification() || !mIsClearable; + return !shouldSuppressNotification() || !mIsDismissable; } /** @@ -595,6 +656,13 @@ public class Bubble implements BubbleViewProvider { } /** + * Whether this bubble is conversation + */ + public boolean isConversation() { + return null != mShortcutInfo; + } + + /** * Sets whether this notification should be suppressed in the shade. */ @VisibleForTesting @@ -712,6 +780,15 @@ public class Bubble implements BubbleViewProvider { return mDeleteIntent; } + @Nullable + Intent getAppBubbleIntent() { + return mAppIntent; + } + + boolean isAppBubble() { + return KEY_APP_BUBBLE.equals(mKey); + } + Intent getSettingsIntent(final Context context) { final Intent intent = new Intent(Settings.ACTION_APP_NOTIFICATION_BUBBLE_SETTINGS); intent.putExtra(Settings.EXTRA_APP_PACKAGE, getPackageName()); @@ -816,7 +893,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 +902,10 @@ 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(" isDismissable: "); pw.println(mIsDismissable); + pw.println(" 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..56b13b8dcd46 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,13 @@ 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.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 +43,78 @@ 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, MODE_WITH_SHADOW); + return createIconBitmap(userBadgedBitmap); + } + + private class CircularRingDrawable extends CircularAdaptiveIcon { + + final int mImportantConversationColor; + final int mRingWidth; + final Rect mInnerBounds = 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); + mRingWidth = mContext.getResources().getDimensionPixelSize( + com.android.internal.R.dimen.importance_ring_stroke_width); + } + + @Override + public void draw(Canvas canvas) { + int save = canvas.save(); + canvas.clipPath(getIconMask()); + canvas.drawColor(mImportantConversationColor); + mInnerBounds.set(getBounds()); + mInnerBounds.inset(mRingWidth, mRingWidth); + canvas.translate(mInnerBounds.left, mInnerBounds.top); + mDr.setBounds(0, 0, mInnerBounds.width(), mInnerBounds.height()); + 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()); + + 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..3dbb745f0c6c 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 @@ -24,13 +24,11 @@ import static android.view.View.INVISIBLE; 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.Bubble.KEY_APP_BUBBLE; 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; -import static com.android.wm.shell.bubbles.BubblePositioner.TASKBAR_POSITION_LEFT; -import static com.android.wm.shell.bubbles.BubblePositioner.TASKBAR_POSITION_NONE; -import static com.android.wm.shell.bubbles.BubblePositioner.TASKBAR_POSITION_RIGHT; import static com.android.wm.shell.bubbles.Bubbles.DISMISS_BLOCKED; import static com.android.wm.shell.bubbles.Bubbles.DISMISS_GROUP_CANCELLED; import static com.android.wm.shell.bubbles.Bubbles.DISMISS_INVALID_INTENT; @@ -40,7 +38,9 @@ import static com.android.wm.shell.bubbles.Bubbles.DISMISS_NO_LONGER_BUBBLE; import static com.android.wm.shell.bubbles.Bubbles.DISMISS_PACKAGE_REMOVED; import static com.android.wm.shell.bubbles.Bubbles.DISMISS_SHORTCUT_REMOVED; import static com.android.wm.shell.bubbles.Bubbles.DISMISS_USER_CHANGED; +import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_BUBBLES; +import android.annotation.BinderThread; import android.annotation.NonNull; import android.annotation.UserIdInt; import android.app.ActivityManager; @@ -58,59 +58,69 @@ import android.content.pm.ShortcutInfo; import android.content.pm.UserInfo; import android.content.res.Configuration; import android.graphics.PixelFormat; -import android.graphics.PointF; import android.graphics.Rect; +import android.graphics.drawable.Icon; import android.os.Binder; import android.os.Bundle; import android.os.Handler; import android.os.RemoteException; import android.os.ServiceManager; +import android.os.SystemProperties; 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.IWindowManager; import android.view.View; import android.view.ViewGroup; import android.view.WindowInsets; import android.view.WindowManager; -import android.window.WindowContainerTransaction; +import android.window.ScreenCapture; +import android.window.ScreenCapture.ScreenCaptureListener; +import android.window.ScreenCapture.ScreenshotSync; 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.ExternalInterfaceBinder; import com.android.wm.shell.common.FloatingContentCoordinator; +import com.android.wm.shell.common.RemoteCallable; import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.SingleInstanceRemoteListener; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.common.TaskStackListenerCallback; import com.android.wm.shell.common.TaskStackListenerImpl; import com.android.wm.shell.common.annotations.ShellBackgroundThread; import com.android.wm.shell.common.annotations.ShellMainThread; +import com.android.wm.shell.common.bubbles.BubbleBarUpdate; import com.android.wm.shell.draganddrop.DragAndDropController; import com.android.wm.shell.onehanded.OneHandedController; import com.android.wm.shell.onehanded.OneHandedTransitionCallback; import com.android.wm.shell.pip.PinnedStackListenerForwarder; +import com.android.wm.shell.sysui.ConfigurationChangeListener; +import com.android.wm.shell.sysui.ShellCommandHandler; +import com.android.wm.shell.sysui.ShellController; +import com.android.wm.shell.sysui.ShellInit; +import com.android.wm.shell.taskview.TaskView; +import com.android.wm.shell.taskview.TaskViewTransitions; 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,26 +131,48 @@ 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, + RemoteCallable<BubbleController> { private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleController" : TAG_BUBBLES; - // TODO(b/173386799) keep in sync with Launcher3, not hooked up to anything - public static final String EXTRA_TASKBAR_CREATED = "taskbarCreated"; - public static final String EXTRA_BUBBLE_OVERFLOW_OPENED = "bubbleOverflowOpened"; - public static final String EXTRA_TASKBAR_VISIBLE = "taskbarVisible"; - public static final String EXTRA_TASKBAR_POSITION = "taskbarPosition"; - public static final String EXTRA_TASKBAR_ICON_SIZE = "taskbarIconSize"; - public static final String EXTRA_TASKBAR_BUBBLE_XY = "taskbarBubbleXY"; - public static final String EXTRA_TASKBAR_SIZE = "taskbarSize"; - public static final String LEFT_POSITION = "Left"; - public static final String RIGHT_POSITION = "Right"; - public static final String BOTTOM_POSITION = "Bottom"; - // Should match with PhoneWindowManager private static final String SYSTEM_DIALOG_REASON_KEY = "reason"; private static final String SYSTEM_DIALOG_REASON_GESTURE_NAV = "gestureNav"; + // TODO(b/256873975) Should use proper flag when available to shell/launcher + /** + * Whether bubbles are showing in the bubble bar from launcher. This is only available + * on large screens and {@link BubbleController#isShowingAsBubbleBar()} should be used + * to check all conditions that indicate if the bubble bar is in use. + */ + private static final boolean BUBBLE_BAR_ENABLED = + SystemProperties.getBoolean("persist.wm.debug.bubble_bar", false); + + + /** + * Common interface to send updates to bubble views. + */ + public interface BubbleViewCallback { + /** Called when the provided bubble should be removed. */ + void removeBubble(Bubble removedBubble); + /** Called when the provided bubble should be added. */ + void addBubble(Bubble addedBubble); + /** Called when the provided bubble should be updated. */ + void updateBubble(Bubble updatedBubble); + /** Called when the provided bubble should be selected. */ + void selectionChanged(BubbleViewProvider selectedBubble); + /** Called when the provided bubble's suppression state has changed. */ + void suppressionChanged(Bubble bubble, boolean isSuppressed); + /** Called when the expansion state of bubbles has changed. */ + void expansionChanged(boolean isExpanded); + /** + * Called when the order of the bubble list has changed. Depending on the expanded state + * the pointer might need to be updated. + */ + void bubbleOrderChanged(List<Bubble> bubbleOrder, boolean updatePointer); + } + private final Context mContext; private final BubblesImpl mImpl = new BubblesImpl(); private Bubbles.BubbleExpandListener mExpandListener; @@ -157,11 +189,13 @@ public class BubbleController { private final DisplayController mDisplayController; private final TaskViewTransitions mTaskViewTransitions; private final SyncTransactionQueue mSyncQueue; + private final ShellController mShellController; + private final ShellCommandHandler mShellCommandHandler; + private final IWindowManager mWmService; // Used to post to main UI thread private final ShellExecutor mMainExecutor; private final Handler mMainHandler; - private final ShellExecutor mBackgroundExecutor; private BubbleLogger mLogger; @@ -176,8 +210,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; @@ -223,45 +257,13 @@ public class BubbleController { private Optional<OneHandedController> mOneHandedOptional; /** Drag and drop controller to register listener for onDragStarted. */ private DragAndDropController mDragAndDropController; + /** Used to send bubble events to launcher. */ + private Bubbles.BubbleStateListener mBubbleStateListener; - /** - * 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, @@ -282,8 +284,11 @@ public class BubbleController { @ShellMainThread Handler mainHandler, @ShellBackgroundThread ShellExecutor bgExecutor, TaskViewTransitions taskViewTransitions, - SyncTransactionQueue syncQueue) { + SyncTransactionQueue syncQueue, + IWindowManager wmService) { mContext = context; + mShellCommandHandler = shellCommandHandler; + mShellController = shellController; mLauncherApps = launcherApps; mBarService = statusBarService == null ? IStatusBarService.Stub.asInterface( @@ -304,7 +309,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 +317,8 @@ public class BubbleController { mOneHandedOptional = oneHandedOptional; mDragAndDropController = dragAndDropController; mSyncQueue = syncQueue; + mWmService = wmService; + shellInit.addInitCallback(this::onInit, this); } private void registerOneHandedState(OneHandedController oneHanded) { @@ -333,9 +340,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 +443,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 +460,22 @@ 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); + mShellController.addExternalInterface(KEY_EXTRA_SHELL_BUBBLES, + this::createExternalInterface, this); + mShellCommandHandler.addDumpCallback(this::dump, this); + } + + private ExternalInterfaceBinder createExternalInterface() { + return new BubbleController.IBubblesImpl(this); } @VisibleForTesting @@ -472,6 +492,48 @@ public class BubbleController { return mMainExecutor; } + @Override + public Context getContext() { + return mContext; + } + + @Override + public ShellExecutor getRemoteCallExecutor() { + return mMainExecutor; + } + + /** + * Sets a listener to be notified of bubble updates. This is used by launcher so that + * it may render bubbles in itself. Only one listener is supported. + */ + public void registerBubbleStateListener(Bubbles.BubbleStateListener listener) { + if (isShowingAsBubbleBar()) { + // Only set the listener if bubble bar is showing. + mBubbleStateListener = listener; + sendInitialListenerUpdate(); + } else { + mBubbleStateListener = null; + } + } + + /** + * Unregisters the {@link Bubbles.BubbleStateListener}. + */ + public void unregisterBubbleStateListener() { + mBubbleStateListener = null; + } + + /** + * If a {@link Bubbles.BubbleStateListener} is present, this will send the current bubble + * state to it. + */ + private void sendInitialListenerUpdate() { + if (mBubbleStateListener != null) { + BubbleBarUpdate update = mBubbleData.getInitialStateForBubbleBar(); + mBubbleStateListener.onBubbleStateChange(update); + } + } + /** * Hides the current input method, wherever it may be focused, via InputMethodManagerInternal. */ @@ -490,52 +552,6 @@ public class BubbleController { mBubbleData.setExpanded(true); } - /** Called when any taskbar state changes (e.g. visibility, position, sizes). */ - private void onTaskbarChanged(Bundle b) { - if (b == null) { - return; - } - boolean isVisible = b.getBoolean(EXTRA_TASKBAR_VISIBLE, false /* default */); - String position = b.getString(EXTRA_TASKBAR_POSITION, RIGHT_POSITION /* default */); - @BubblePositioner.TaskbarPosition int taskbarPosition = TASKBAR_POSITION_NONE; - switch (position) { - case LEFT_POSITION: - taskbarPosition = TASKBAR_POSITION_LEFT; - break; - case RIGHT_POSITION: - taskbarPosition = TASKBAR_POSITION_RIGHT; - break; - case BOTTOM_POSITION: - taskbarPosition = TASKBAR_POSITION_BOTTOM; - break; - } - int[] itemPosition = b.getIntArray(EXTRA_TASKBAR_BUBBLE_XY); - int iconSize = b.getInt(EXTRA_TASKBAR_ICON_SIZE); - int taskbarSize = b.getInt(EXTRA_TASKBAR_SIZE); - Log.w(TAG, "onTaskbarChanged:" - + " isVisible: " + isVisible - + " position: " + position - + " itemPosition: " + itemPosition[0] + "," + itemPosition[1] - + " iconSize: " + iconSize); - PointF point = new PointF(itemPosition[0], itemPosition[1]); - mBubblePositioner.setPinnedLocation(isVisible ? point : null); - mBubblePositioner.updateForTaskbar(iconSize, taskbarPosition, isVisible, taskbarSize); - if (mStackView != null) { - if (isVisible && b.getBoolean(EXTRA_TASKBAR_CREATED, false /* default */)) { - // If taskbar was created, add and remove the window so that bubbles display on top - removeFromWindowManagerMaybe(); - addToWindowManagerMaybe(); - } - mStackView.updateStackPosition(); - mBubbleIconFactory = new BubbleIconFactory(mContext); - mBubbleBadgeIconFactory = new BubbleBadgeIconFactory(mContext); - mStackView.onDisplaySizeChanged(); - } - if (b.getBoolean(EXTRA_BUBBLE_OVERFLOW_OPENED, false)) { - openBubbleOverflow(); - } - } - /** * Called when the status bar has become visible or invisible (either permanently or * temporarily). @@ -556,14 +572,18 @@ public class BubbleController { @VisibleForTesting public void onStatusBarStateChanged(boolean isShade) { + boolean didChange = mIsStatusBarShade != isShade; + if (DEBUG_BUBBLE_CONTROLLER) { + Log.d(TAG, "onStatusBarStateChanged isShade=" + isShade + " didChange=" + didChange); + } mIsStatusBarShade = isShade; - if (!mIsStatusBarShade) { + if (!mIsStatusBarShade && didChange) { + // Only collapse stack on change collapseStack(); } if (mNotifEntryToExpandOnShadeUnlock != null) { expandStackAndSelectBubble(mNotifEntryToExpandOnShadeUnlock); - mNotifEntryToExpandOnShadeUnlock = null; } updateStack(); @@ -610,6 +630,12 @@ public class BubbleController { mDataRepository.removeBubblesForUser(removedUserId, parentUserId); } + /** Whether bubbles are showing in the bubble bar. */ + public boolean isShowingAsBubbleBar() { + // TODO(b/269670598): should also check that we're in gesture nav + return BUBBLE_BAR_ENABLED && mBubblePositioner.isLargeScreen(); + } + /** Whether this userId belongs to the current user. */ private boolean isCurrentProfile(int userId) { return userId == UserHandle.USER_ALL @@ -750,10 +776,18 @@ public class BubbleController { return; } + mAddedToWindowManager = false; + // Put on background for this binder call, was causing jank + mBackgroundExecutor.execute(() -> { + try { + mContext.unregisterReceiver(mBroadcastReceiver); + } catch (IllegalArgumentException e) { + // Not sure if this happens in production, but was happening in tests + // (b/253647225) + e.printStackTrace(); + } + }); try { - mAddedToWindowManager = false; - // Put on background for this binder call, was causing jank - mBackgroundExecutor.execute(() -> mContext.unregisterReceiver(mBroadcastReceiver)); if (mStackView != null) { mWindowManager.removeView(mStackView); mBubbleData.getOverflow().cleanUpExpandedState(); @@ -771,7 +805,7 @@ public class BubbleController { IntentFilter filter = new IntentFilter(); filter.addAction(Intent.ACTION_CLOSE_SYSTEM_DIALOGS); filter.addAction(Intent.ACTION_SCREEN_OFF); - mContext.registerReceiver(mBroadcastReceiver, filter); + mContext.registerReceiver(mBroadcastReceiver, filter, Context.RECEIVER_EXPORTED); } private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { @@ -809,11 +843,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 +858,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 +898,8 @@ public class BubbleController { } } - private void onConfigChanged(Configuration newConfig) { + @Override + public void onConfigurationChanged(Configuration newConfig) { if (mBubblePositioner != null) { mBubblePositioner.update(); } @@ -885,6 +924,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 +984,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 +1056,98 @@ 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); + } + } + } + + /** + * This method has different behavior depending on: + * - if an app bubble exists + * - if an app bubble is expanded + * + * If no app bubble exists, this will add and expand a bubble with the provided intent. The + * intent must be explicit (i.e. include a package name or fully qualified component class name) + * and the activity for it should be resizable. + * + * If an app bubble exists, this will toggle the visibility of it, i.e. if the app bubble is + * expanded, calling this method will collapse it. If the app bubble is not expanded, calling + * this method will expand it. + * + * These bubbles are <b>not</b> backed by a notification and remain until the user dismisses + * the bubble or bubble stack. + * + * Some notes: + * - Only one app bubble is supported at a time, regardless of users. Multi-users support is + * tracked in b/273533235. + * - Calling this method with a different intent than the existing app bubble will do nothing + * + * @param intent the intent to display in the bubble expanded view. + * @param user the {@link UserHandle} of the user to start this activity for. + * @param icon the {@link Icon} to use for the bubble view. + */ + public void showOrHideAppBubble(Intent intent, UserHandle user, @Nullable Icon icon) { + if (intent == null || intent.getPackage() == null) { + Log.w(TAG, "App bubble failed to show, invalid intent: " + intent + + ((intent != null) ? " with package: " + intent.getPackage() : " ")); + return; + } + + PackageManager packageManager = getPackageManagerForUser(mContext, user.getIdentifier()); + if (!isResizableActivity(intent, packageManager, KEY_APP_BUBBLE)) return; + + Bubble existingAppBubble = mBubbleData.getBubbleInStackWithKey(KEY_APP_BUBBLE); + if (existingAppBubble != null) { + BubbleViewProvider selectedBubble = mBubbleData.getSelectedBubble(); + if (isStackExpanded()) { + if (selectedBubble != null && KEY_APP_BUBBLE.equals(selectedBubble.getKey())) { + // App bubble is expanded, lets collapse + collapseStack(); + } else { + // App bubble is not selected, select it + mBubbleData.setSelectedBubble(existingAppBubble); + } + } else { + // App bubble is not selected, select it & expand + mBubbleData.setSelectedBubble(existingAppBubble); + mBubbleData.setExpanded(true); + } + } else { + // App bubble does not exist, lets add and expand it + Bubble b = new Bubble(intent, user, icon, mMainExecutor); + b.setShouldAutoExpand(true); + inflateAndAdd(b, /* suppressFlyout= */ true, /* showInShade= */ false); + } + } + + /** + * Performs a screenshot that may exclude the bubble layer, if one is present. The screenshot + * can be access via the supplied {@link ScreenshotSync#get()} asynchronously. + * + * TODO(b/267324693): Implement the exclude layer functionality in screenshot. + */ + public void getScreenshotExcludingBubble(int displayId, + Pair<ScreenCaptureListener, ScreenshotSync> screenCaptureListener) { + try { + mWmService.captureDisplay(displayId, null, screenCaptureListener.first); + } catch (RemoteException e) { + Log.e(TAG, "Failed to capture screenshot"); + } + } + + /** Sets the app bubble's taskId which is cached for SysUI. */ + public void setAppBubbleTaskId(int taskId) { + mImpl.mCachedState.setAppBubbleTaskId(taskId); } /** @@ -1050,18 +1184,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 +1215,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 +1265,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 +1329,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); } } } @@ -1244,6 +1406,58 @@ public class BubbleController { }); } + private final BubbleViewCallback mBubbleViewCallback = new BubbleViewCallback() { + @Override + public void removeBubble(Bubble removedBubble) { + if (mStackView != null) { + mStackView.removeBubble(removedBubble); + } + } + + @Override + public void addBubble(Bubble addedBubble) { + if (mStackView != null) { + mStackView.addBubble(addedBubble); + } + } + + @Override + public void updateBubble(Bubble updatedBubble) { + if (mStackView != null) { + mStackView.updateBubble(updatedBubble); + } + } + + @Override + public void bubbleOrderChanged(List<Bubble> bubbleOrder, boolean updatePointer) { + if (mStackView != null) { + mStackView.updateBubbleOrder(bubbleOrder, updatePointer); + } + } + + @Override + public void suppressionChanged(Bubble bubble, boolean isSuppressed) { + if (mStackView != null) { + mStackView.setBubbleSuppressed(bubble, isSuppressed); + } + } + + @Override + public void expansionChanged(boolean isExpanded) { + if (mStackView != null) { + mStackView.setExpanded(isExpanded); + } + } + + @Override + public void selectionChanged(BubbleViewProvider selectedBubble) { + if (mStackView != null) { + mStackView.setSelectedBubble(selectedBubble); + } + + } + }; + @SuppressWarnings("FieldCanBeLocal") private final BubbleData.Listener mBubbleDataListener = new BubbleData.Listener() { @@ -1266,7 +1480,8 @@ public class BubbleController { // Lazy load overflow bubbles from disk loadOverflowBubblesFromDisk(); - mStackView.updateOverflowButtonDot(); + // If bubbles in the overflow have a dot, make sure the overflow shows a dot + updateOverflowButtonDot(); // Update bubbles in overflow. if (mOverflowListener != null) { @@ -1281,9 +1496,7 @@ public class BubbleController { final Bubble bubble = removed.first; @Bubbles.DismissReason final int reason = removed.second; - if (mStackView != null) { - mStackView.removeBubble(bubble); - } + mBubbleViewCallback.removeBubble(bubble); // Leave the notification in place if we're dismissing due to user switching, or // because DND is suppressing the bubble. In both of those cases, we need to be able @@ -1309,64 +1522,51 @@ 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); - if (update.addedBubble != null && mStackView != null) { + if (update.addedBubble != null) { mDataRepository.addBubble(mCurrentUserId, update.addedBubble); - mStackView.addBubble(update.addedBubble); + mBubbleViewCallback.addBubble(update.addedBubble); } - if (update.updatedBubble != null && mStackView != null) { - mStackView.updateBubble(update.updatedBubble); + if (update.updatedBubble != null) { + mBubbleViewCallback.updateBubble(update.updatedBubble); } - if (update.suppressedBubble != null && mStackView != null) { - mStackView.setBubbleSuppressed(update.suppressedBubble, true); + if (update.suppressedBubble != null) { + mBubbleViewCallback.suppressionChanged(update.suppressedBubble, true); } - if (update.unsuppressedBubble != null && mStackView != null) { - mStackView.setBubbleSuppressed(update.unsuppressedBubble, false); + if (update.unsuppressedBubble != null) { + mBubbleViewCallback.suppressionChanged(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) { + if (update.orderChanged) { mDataRepository.addBubbles(mCurrentUserId, update.bubbles); - mStackView.updateBubbleOrder(update.bubbles); + // if the stack is going to be collapsed, do not update pointer position + // after reordering + mBubbleViewCallback.bubbleOrderChanged(update.bubbles, !collapseStack); } - if (update.expandedChanged && !update.expanded) { - mStackView.setExpanded(false); + if (collapseStack) { + mBubbleViewCallback.expansionChanged(/* expanded= */ false); mSysuiProxy.requestNotificationShadeTopUi(false, TAG); } - if (update.selectionChanged && mStackView != null) { - mStackView.setSelectedBubble(update.selectedBubble); - if (update.selectedBubble != null) { - mSysuiProxy.updateNotificationSuppression(update.selectedBubble.getKey()); - } + if (update.selectionChanged) { + mBubbleViewCallback.selectionChanged(update.selectedBubble); } // Expanding? Apply this last. if (update.expandedChanged && update.expanded) { - if (mStackView != null) { - mStackView.setExpanded(true); - mSysuiProxy.requestNotificationShadeTopUi(true, TAG); - } + mBubbleViewCallback.expansionChanged(/* expanded= */ true); + mSysuiProxy.requestNotificationShadeTopUi(true, TAG); } mSysuiProxy.notifyInvalidateNotifications("BubbleData.Listener.applyUpdate"); @@ -1377,6 +1577,19 @@ public class BubbleController { } }; + private void updateOverflowButtonDot() { + BubbleOverflow overflow = mBubbleData.getOverflow(); + if (overflow == null) return; + + for (Bubble b : mBubbleData.getOverflowBubbles()) { + if (b.showDot()) { + overflow.setShowDot(true); + return; + } + } + overflow.setShowDot(false); + } + private boolean handleDismissalInterception(BubbleEntry entry, @Nullable List<BubbleEntry> children, IntConsumer removeCallback) { if (isSummaryOfBubbles(entry)) { @@ -1417,7 +1630,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,21 +1680,34 @@ 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); } /** * Whether an intent is properly configured to display in a - * {@link com.android.wm.shell.TaskView}. + * {@link TaskView}. * * Keep checks in sync with BubbleExtractor#canLaunchInTaskView. Typically * that should filter out any invalid bubbles, but should protect SysUI side just in case. @@ -1504,18 +1729,23 @@ public class BubbleController { } PackageManager packageManager = getPackageManagerForUser( context, entry.getStatusBarNotification().getUser().getIdentifier()); - ActivityInfo info = - intent.getIntent().resolveActivityInfo(packageManager, 0); + return isResizableActivity(intent.getIntent(), packageManager, entry.getKey()); + } + + static boolean isResizableActivity(Intent intent, PackageManager packageManager, String key) { + if (intent == null) { + Log.w(TAG, "Unable to send as bubble: " + key + " null intent"); + return false; + } + ActivityInfo info = intent.resolveActivityInfo(packageManager, 0); if (info == null) { - Log.w(TAG, "Unable to send as bubble, " - + entry.getKey() + " couldn't find activity info for intent: " - + intent); + Log.w(TAG, "Unable to send as bubble: " + key + + " couldn't find activity info for intent: " + intent); return false; } if (!ActivityInfo.isResizeableMode(info.resizeMode)) { - Log.w(TAG, "Unable to send as bubble, " - + entry.getKey() + " activity is not resizable for intent: " - + intent); + Log.w(TAG, "Unable to send as bubble: " + key + + " activity is not resizable for intent: " + intent); return false; } return true; @@ -1546,11 +1776,78 @@ public class BubbleController { public void onImeVisibilityChanged(boolean imeVisible, int imeHeight) { mBubblePositioner.setImeVisible(imeVisible, imeHeight); if (mStackView != null) { - mStackView.animateForIme(imeVisible); + mStackView.setImeVisible(imeVisible); } } } + /** + * The interface for calls from outside the host process. + */ + @BinderThread + private class IBubblesImpl extends IBubbles.Stub implements ExternalInterfaceBinder { + private BubbleController mController; + private final SingleInstanceRemoteListener<BubbleController, IBubblesListener> mListener; + private final Bubbles.BubbleStateListener mBubbleListener = + new Bubbles.BubbleStateListener() { + + @Override + public void onBubbleStateChange(BubbleBarUpdate update) { + Bundle b = new Bundle(); + b.setClassLoader(BubbleBarUpdate.class.getClassLoader()); + b.putParcelable(BubbleBarUpdate.BUNDLE_KEY, update); + mListener.call(l -> l.onBubbleStateChange(b)); + } + }; + + IBubblesImpl(BubbleController controller) { + mController = controller; + mListener = new SingleInstanceRemoteListener<>(mController, + c -> c.registerBubbleStateListener(mBubbleListener), + c -> c.unregisterBubbleStateListener()); + } + + /** + * Invalidates this instance, preventing future calls from updating the controller. + */ + @Override + public void invalidate() { + mController = null; + } + + @Override + public void registerBubbleListener(IBubblesListener listener) { + mMainExecutor.execute(() -> { + mListener.register(listener); + }); + } + + @Override + public void unregisterBubbleListener(IBubblesListener listener) { + mMainExecutor.execute(() -> mListener.unregister()); + } + + @Override + public void showBubble(String key, boolean onLauncherHome) { + // TODO + } + + @Override + public void removeBubble(String key, int reason) { + // TODO + } + + @Override + public void collapseBubbles() { + // TODO + } + + @Override + public void onTaskbarStateChanged(int newState) { + // TODO (b/269670598) + } + } + private class BubblesImpl implements Bubbles { // Up-to-date cached state of bubbles data for SysUI to query from the calling thread @VisibleForTesting @@ -1560,6 +1857,7 @@ public class BubbleController { private HashSet<String> mSuppressedBubbleKeys = new HashSet<>(); private HashMap<String, String> mSuppressedGroupToNotifKeys = new HashMap<>(); private HashMap<String, Bubble> mShortcutIdToBubble = new HashMap<>(); + private int mAppBubbleTaskId = INVALID_TASK_ID; private ArrayList<Bubble> mTmpBubbles = new ArrayList<>(); @@ -1591,12 +1889,22 @@ public class BubbleController { mSuppressedBubbleKeys.clear(); mShortcutIdToBubble.clear(); + mAppBubbleTaskId = INVALID_TASK_ID; for (Bubble b : mTmpBubbles) { mShortcutIdToBubble.put(b.getShortcutId(), b); updateBubbleSuppressedState(b); + + if (KEY_APP_BUBBLE.equals(b.getKey())) { + mAppBubbleTaskId = b.getTaskId(); + } } } + /** Sets the app bubble's taskId which is cached for SysUI. */ + synchronized void setAppBubbleTaskId(int taskId) { + mAppBubbleTaskId = taskId; + } + /** * Updates a specific bubble suppressed state. This is used mainly because notification * suppression changes don't go through the same BubbleData update mechanism. @@ -1646,11 +1954,24 @@ public class BubbleController { for (String key : mSuppressedGroupToNotifKeys.keySet()) { pw.println(" suppressing: " + key); } + + pw.print("mAppBubbleTaskId: " + mAppBubbleTaskId); } } private CachedState mCachedState = new CachedState(); + private IBubblesImpl mIBubbles; + + @Override + public IBubbles createExternalInterface() { + if (mIBubbles != null) { + mIBubbles.invalidate(); + } + mIBubbles = new IBubblesImpl(BubbleController.this); + return mIBubbles; + } + @Override public boolean isBubbleNotificationSuppressedFromShade(String key, String groupKey) { return mCachedState.isBubbleNotificationSuppressedFromShade(key, groupKey); @@ -1662,28 +1983,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 +1996,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); @@ -1712,17 +2010,27 @@ public class BubbleController { } @Override - public void onTaskbarChanged(Bundle b) { - mMainExecutor.execute(() -> { - BubbleController.this.onTaskbarChanged(b); - }); + public void showOrHideAppBubble(Intent intent, UserHandle user, @Nullable Icon icon) { + mMainExecutor.execute( + () -> BubbleController.this.showOrHideAppBubble(intent, user, icon)); } @Override - public void openBubbleOverflow() { - mMainExecutor.execute(() -> { - BubbleController.this.openBubbleOverflow(); - }); + public boolean isAppBubbleTaskId(int taskId) { + return mCachedState.mAppBubbleTaskId == taskId; + } + + @Override + @Nullable + public ScreenshotSync getScreenshotExcludingBubble(int displayId) { + Pair<ScreenCaptureListener, ScreenshotSync> screenCaptureListener = + ScreenCapture.createSyncCaptureListener(); + + mMainExecutor.execute( + () -> BubbleController.this.getScreenshotExcludingBubble(displayId, + screenCaptureListener)); + + return screenCaptureListener.second; } @Override @@ -1759,9 +2067,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 +2144,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..a26c0c487d19 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 @@ -17,6 +17,7 @@ package com.android.wm.shell.bubbles; import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE; import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE; +import static com.android.wm.shell.bubbles.Bubble.KEY_APP_BUBBLE; import static com.android.wm.shell.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_DATA; import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_BUBBLES; import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME; @@ -39,6 +40,8 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.FrameworkStatsLog; import com.android.wm.shell.R; import com.android.wm.shell.bubbles.Bubbles.DismissReason; +import com.android.wm.shell.common.bubbles.BubbleBarUpdate; +import com.android.wm.shell.common.bubbles.RemovedBubble; import java.io.PrintWriter; import java.util.ArrayList; @@ -112,6 +115,61 @@ public class BubbleData { void bubbleRemoved(Bubble bubbleToRemove, @DismissReason int reason) { removedBubbles.add(new Pair<>(bubbleToRemove, reason)); } + + /** + * Converts the update to a {@link BubbleBarUpdate} which contains updates relevant + * to the bubble bar. Only used when {@link BubbleController#isShowingAsBubbleBar()} is + * true. + */ + BubbleBarUpdate toBubbleBarUpdate() { + BubbleBarUpdate bubbleBarUpdate = new BubbleBarUpdate(); + + bubbleBarUpdate.expandedChanged = expandedChanged; + bubbleBarUpdate.expanded = expanded; + if (selectionChanged) { + bubbleBarUpdate.selectedBubbleKey = selectedBubble != null + ? selectedBubble.getKey() + : null; + } + bubbleBarUpdate.addedBubble = addedBubble != null + ? addedBubble.asBubbleBarBubble() + : null; + // TODO(b/269670235): We need to handle updates better, I think for the bubble bar only + // certain updates need to be sent instead of any updatedBubble. + bubbleBarUpdate.updatedBubble = updatedBubble != null + ? updatedBubble.asBubbleBarBubble() + : null; + bubbleBarUpdate.suppressedBubbleKey = suppressedBubble != null + ? suppressedBubble.getKey() + : null; + bubbleBarUpdate.unsupressedBubbleKey = unsuppressedBubble != null + ? unsuppressedBubble.getKey() + : null; + for (int i = 0; i < removedBubbles.size(); i++) { + Pair<Bubble, Integer> pair = removedBubbles.get(i); + bubbleBarUpdate.removedBubbles.add( + new RemovedBubble(pair.first.getKey(), pair.second)); + } + if (orderChanged) { + // Include the new order + for (int i = 0; i < bubbles.size(); i++) { + bubbleBarUpdate.bubbleKeysInOrder.add(bubbles.get(i).getKey()); + } + } + return bubbleBarUpdate; + } + + /** + * Gets the current state of active bubbles and populates the update with that. Only + * used when {@link BubbleController#isShowingAsBubbleBar()} is true. + */ + BubbleBarUpdate getInitialState() { + BubbleBarUpdate bubbleBarUpdate = new BubbleBarUpdate(); + for (int i = 0; i < bubbles.size(); i++) { + bubbleBarUpdate.currentBubbleList.add(bubbles.get(i).asBubbleBarBubble()); + } + return bubbleBarUpdate; + } } /** @@ -158,7 +216,6 @@ public class BubbleData { @Nullable private Listener mListener; - @Nullable private Bubbles.BubbleMetadataFlagListener mBubbleMetadataFlagListener; private Bubbles.PendingIntentCanceledListener mCancelledListener; @@ -190,6 +247,13 @@ public class BubbleData { mMaxOverflowBubbles = mContext.getResources().getInteger(R.integer.bubbles_max_overflow); } + /** + * Returns a bubble bar update populated with the current list of active bubbles. + */ + public BubbleBarUpdate getInitialStateForBubbleBar() { + return mStateChange.getInitialState(); + } + public void setSuppressionChangedListener(Bubbles.BubbleMetadataFlagListener listener) { mBubbleMetadataFlagListener = listener; } @@ -283,7 +347,7 @@ public class BubbleData { } boolean isShowingOverflow() { - return mShowingOverflow && (isExpanded() || mPositioner.showingInTaskbar()); + return mShowingOverflow && isExpanded(); } /** @@ -685,7 +749,8 @@ public class BubbleData { if (bubble.getPendingIntentCanceled() || !(reason == Bubbles.DISMISS_AGED || reason == Bubbles.DISMISS_USER_GESTURE - || reason == Bubbles.DISMISS_RELOAD_FROM_DISK)) { + || reason == Bubbles.DISMISS_RELOAD_FROM_DISK) + || KEY_APP_BUBBLE.equals(bubble.getKey())) { return; } if (DEBUG_BUBBLE_DATA) { @@ -1136,7 +1201,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 +1212,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..e37c785f15f5 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. @@ -101,7 +109,8 @@ internal class BubbleDataRepository( b.rawDesiredHeightResId, b.title, b.taskId, - b.locusId?.id + b.locusId?.id, + b.isDismissable ) } } @@ -197,7 +206,9 @@ internal class BubbleDataRepository( entity.title, entity.taskId, entity.locus, - mainExecutor + entity.isDismissable, + 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/BubbleEntry.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleEntry.java index 5f428269fb06..afe19c4b7363 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleEntry.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleEntry.java @@ -38,18 +38,18 @@ public class BubbleEntry { private StatusBarNotification mSbn; private Ranking mRanking; - private boolean mIsClearable; + private boolean mIsDismissable; private boolean mShouldSuppressNotificationDot; private boolean mShouldSuppressNotificationList; private boolean mShouldSuppressPeek; public BubbleEntry(@NonNull StatusBarNotification sbn, - Ranking ranking, boolean isClearable, boolean shouldSuppressNotificationDot, + Ranking ranking, boolean isDismissable, boolean shouldSuppressNotificationDot, boolean shouldSuppressNotificationList, boolean shouldSuppressPeek) { mSbn = sbn; mRanking = ranking; - mIsClearable = isClearable; + mIsDismissable = isDismissable; mShouldSuppressNotificationDot = shouldSuppressNotificationDot; mShouldSuppressNotificationList = shouldSuppressNotificationList; mShouldSuppressPeek = shouldSuppressPeek; @@ -115,9 +115,9 @@ public class BubbleEntry { return mRanking.canBubble(); } - /** @return true if this notification is clearable. */ - public boolean isClearable() { - return mIsClearable; + /** @return true if this notification can be dismissed. */ + public boolean isDismissable() { + return mIsDismissable; } /** @return true if {@link Policy#SUPPRESSED_EFFECT_BADGE} set for this notification. */ 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..a317c449621b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java @@ -16,6 +16,7 @@ package com.android.wm.shell.bubbles; +import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED; import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK; import static android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT; @@ -43,29 +44,33 @@ 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; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.policy.ScreenDecorationsUtils; import com.android.wm.shell.R; -import com.android.wm.shell.TaskView; import com.android.wm.shell.common.AlphaOptimizedButton; import com.android.wm.shell.common.TriangleShape; +import com.android.wm.shell.taskview.TaskView; +import com.android.wm.shell.taskview.TaskViewTaskController; import java.io.PrintWriter; @@ -75,12 +80,69 @@ 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; private AlphaOptimizedButton mManageButton; private TaskView mTaskView; + private TaskViewTaskController mTaskViewTaskController; private BubbleOverflowContainerView mOverflowView; private int mTaskId = INVALID_TASK_ID; @@ -90,7 +152,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 +161,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 +176,13 @@ public class BubbleExpandedView extends LinearLayout { private float mCornerRadius = 0f; private int mBackgroundColorFloating; private boolean mUsingMaxHeight; - + private int mTopClip = 0; + private int mBottomClip = 0; @Nullable private Bubble mBubble; private PendingIntent mPendingIntent; // TODO(b/170891664): Don't use a flag, set the BubbleOverflow object instead private boolean mIsOverflow; + private boolean mIsClipping; private BubbleController mController; private BubbleStackView mStackView; @@ -162,15 +227,34 @@ public class BubbleExpandedView extends LinearLayout { try { options.setTaskAlwaysOnTop(true); options.setLaunchedFromBubble(true); - if (!mIsOverflow && mBubble.hasMetadataShortcutId()) { + options.setPendingIntentBackgroundActivityStartMode( + MODE_BACKGROUND_ACTIVITY_START_ALLOWED); + options.setPendingIntentBackgroundActivityLaunchAllowedByPermission(true); + + Intent fillInIntent = new Intent(); + // Apply flags to make behaviour match documentLaunchMode=always. + fillInIntent.addFlags(FLAG_ACTIVITY_NEW_DOCUMENT); + fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK); + + if (mBubble.isAppBubble()) { + Context context = + mContext.createContextAsUser( + mBubble.getUser(), Context.CONTEXT_RESTRICTED); + PendingIntent pi = PendingIntent.getActivity( + context, + /* requestCode= */ 0, + mBubble.getAppBubbleIntent() + .addFlags(FLAG_ACTIVITY_NEW_DOCUMENT) + .addFlags(FLAG_ACTIVITY_MULTIPLE_TASK), + PendingIntent.FLAG_IMMUTABLE, + /* options= */ null); + mTaskView.startActivity(pi, /* fillInIntent= */ null, options, + launchBounds); + } else if (!mIsOverflow && mBubble.hasMetadataShortcutId()) { options.setApplyActivityFlagsForBubbles(true); mTaskView.startShortcutActivity(mBubble.getShortcutInfo(), options, launchBounds); } else { - Intent fillInIntent = new Intent(); - // Apply flags to make behaviour match documentLaunchMode=always. - fillInIntent.addFlags(FLAG_ACTIVITY_NEW_DOCUMENT); - fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK); if (mBubble != null) { mBubble.setIntentActive(); } @@ -203,6 +287,11 @@ public class BubbleExpandedView extends LinearLayout { // The taskId is saved to use for removeTask, preventing appearance in recent tasks. mTaskId = taskId; + if (Bubble.KEY_APP_BUBBLE.equals(getBubbleKey())) { + // Let the controller know sooner what the taskId is. + mController.setAppBubbleTaskId(mTaskId); + } + // With the task org, the taskAppeared callback will only happen once the task has // already drawn setContentVisibility(true); @@ -268,7 +357,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 +390,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; } @@ -335,8 +425,10 @@ public class BubbleExpandedView extends LinearLayout { bringChildToFront(mOverflowView); mManageButton.setVisibility(GONE); } else { - mTaskView = new TaskView(mContext, mController.getTaskOrganizer(), + mTaskViewTaskController = new TaskViewTaskController(mContext, + mController.getTaskOrganizer(), mController.getTaskViewTransitions(), mController.getSyncTransactionQueue()); + mTaskView = new TaskView(mContext, mTaskViewTaskController); mTaskView.setListener(mController.getMainExecutor(), mTaskViewListener); mExpandedViewContainer.addView(mTaskView); bringChildToFront(mTaskView); @@ -384,7 +476,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 +521,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 +537,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 +546,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 +555,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 +602,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 +616,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 +756,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 +773,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 +938,9 @@ public class BubbleExpandedView extends LinearLayout { mTaskView.onLocationChanged(); } if (mIsOverflow) { - mOverflowView.show(); + post(() -> { + mOverflowView.show(); + }); } } @@ -730,38 +983,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 +1073,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..4ded3ea951e5 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], 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..d101b0c4d7e8 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,9 +16,8 @@ package com.android.wm.shell.bubbles; -import static java.lang.annotation.RetentionPolicy.SOURCE; +import static android.view.View.LAYOUT_DIRECTION_RTL; -import android.annotation.IntDef; import android.content.Context; import android.content.res.Configuration; import android.content.res.Resources; @@ -28,7 +27,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; @@ -38,8 +36,6 @@ import androidx.annotation.VisibleForTesting; import com.android.launcher3.icons.IconNormalizer; import com.android.wm.shell.R; -import java.lang.annotation.Retention; - /** * Keeps track of display size, configuration, and specific bubble sizes. One place for all * placement and positioning calculations to refer to. @@ -49,15 +45,6 @@ public class BubblePositioner { ? "BubblePositioner" : BubbleDebugConfig.TAG_BUBBLES; - @Retention(SOURCE) - @IntDef({TASKBAR_POSITION_NONE, TASKBAR_POSITION_RIGHT, TASKBAR_POSITION_LEFT, - TASKBAR_POSITION_BOTTOM}) - @interface TaskbarPosition {} - public static final int TASKBAR_POSITION_NONE = -1; - public static final int TASKBAR_POSITION_RIGHT = 0; - public static final int TASKBAR_POSITION_LEFT = 1; - public static final int TASKBAR_POSITION_BOTTOM = 2; - /** When the bubbles are collapsed in a stack only some of them are shown, this is how many. **/ public static final int NUM_VISIBLE_WHEN_RESTING = 2; /** Indicates a bubble's height should be the maximum available space. **/ @@ -66,12 +53,16 @@ public class BubblePositioner { public static final float FLYOUT_MAX_WIDTH_PERCENT_LARGE_SCREEN = 0.3f; /** The max percent of screen width to use for the flyout on phone. */ public static final float FLYOUT_MAX_WIDTH_PERCENT = 0.6f; - /** The percent of screen width that should be used for the expanded view on a large screen. **/ + /** The percent of screen width for the expanded view on a large screen. **/ private static final float EXPANDED_VIEW_LARGE_SCREEN_LANDSCAPE_WIDTH_PERCENT = 0.48f; - /** The percent of screen width that should be used for the expanded view on a large screen. **/ + /** The percent of screen width for the expanded view on a large screen. **/ private static final float EXPANDED_VIEW_LARGE_SCREEN_PORTRAIT_WIDTH_PERCENT = 0.70f; - /** The percent of screen width that should be used for the expanded view on a small tablet. **/ + /** The percent of screen width for the expanded view on a small tablet. **/ private static final float EXPANDED_VIEW_SMALL_TABLET_WIDTH_PERCENT = 0.72f; + /** The percent of screen width for the expanded view when shown in the bubble bar. **/ + private static final float EXPANDED_VIEW_BUBBLE_BAR_PORTRAIT_WIDTH_PERCENT = 0.7f; + /** The percent of screen width for the expanded view when shown in the bubble bar. **/ + private static final float EXPANDED_VIEW_BUBBLE_BAR_LANDSCAPE_WIDTH_PERCENT = 0.4f; private Context mContext; private WindowManager mWindowManager; @@ -107,14 +98,14 @@ public class BubblePositioner { private int mOverflowHeight; private int mMinimumFlyoutWidthLargeScreen; - private PointF mPinLocation; private PointF mRestingStackPosition; private int[] mPaddings = new int[4]; - private boolean mShowingInTaskbar; - private @TaskbarPosition int mTaskbarPosition = TASKBAR_POSITION_NONE; - private int mTaskbarIconSize; - private int mTaskbarSize; + private boolean mShowingInBubbleBar; + private boolean mBubblesOnHome; + private int mBubbleBarSize; + private int mBubbleBarHomeAdjustment; + private final PointF mBubbleBarPosition = new PointF(); public BubblePositioner(Context context, WindowManager windowManager) { mContext = context; @@ -152,27 +143,12 @@ public class BubblePositioner { + " insets: " + insets + " isLargeScreen: " + mIsLargeScreen + " isSmallTablet: " + mIsSmallTablet - + " bounds: " + bounds - + " showingInTaskbar: " + mShowingInTaskbar); + + " showingInBubbleBar: " + mShowingInBubbleBar + + " bounds: " + bounds); } updateInternal(mRotation, insets, bounds); } - /** - * Updates position information to account for taskbar state. - * - * @param taskbarPosition which position the taskbar is displayed in. - * @param showingInTaskbar whether the taskbar is being shown. - */ - public void updateForTaskbar(int iconSize, - @TaskbarPosition int taskbarPosition, boolean showingInTaskbar, int taskbarSize) { - mShowingInTaskbar = showingInTaskbar; - mTaskbarIconSize = iconSize; - mTaskbarPosition = taskbarPosition; - mTaskbarSize = taskbarSize; - update(); - } - @VisibleForTesting public void updateInternal(int rotation, Insets insets, Rect bounds) { mRotation = rotation; @@ -190,11 +166,17 @@ public class BubblePositioner { mSpacingBetweenBubbles = res.getDimensionPixelSize(R.dimen.bubble_spacing); mDefaultMaxBubbles = res.getInteger(R.integer.bubbles_max_rendered); mExpandedViewPadding = res.getDimensionPixelSize(R.dimen.bubble_expanded_view_padding); + mBubbleBarHomeAdjustment = mExpandedViewPadding / 2; mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top); mBubbleOffscreenAmount = res.getDimensionPixelSize(R.dimen.bubble_stack_offscreen); mStackOffset = res.getDimensionPixelSize(R.dimen.bubble_stack_offset); + mBubbleBarSize = res.getDimensionPixelSize(R.dimen.bubblebar_size); - if (mIsSmallTablet) { + if (mShowingInBubbleBar) { + mExpandedViewLargeScreenWidth = isLandscape() + ? (int) (bounds.width() * EXPANDED_VIEW_BUBBLE_BAR_LANDSCAPE_WIDTH_PERCENT) + : (int) (bounds.width() * EXPANDED_VIEW_BUBBLE_BAR_PORTRAIT_WIDTH_PERCENT); + } else if (mIsSmallTablet) { mExpandedViewLargeScreenWidth = (int) (bounds.width() * EXPANDED_VIEW_SMALL_TABLET_WIDTH_PERCENT); } else { @@ -231,10 +213,6 @@ public class BubblePositioner { R.dimen.bubbles_flyout_min_width_large_screen); mMaxBubbles = calculateMaxBubbles(); - - if (mShowingInTaskbar) { - adjustForTaskbar(); - } } /** @@ -259,30 +237,6 @@ public class BubblePositioner { return mDefaultMaxBubbles; } - /** - * Taskbar insets appear as navigationBar insets, however, unlike navigationBar this should - * not inset bubbles UI as bubbles floats above the taskbar. This adjust the available space - * and insets to account for the taskbar. - */ - // TODO(b/171559950): When the insets are reported correctly we can remove this logic - private void adjustForTaskbar() { - // When bar is showing on edges... subtract that inset because we appear on top - if (mShowingInTaskbar && mTaskbarPosition != TASKBAR_POSITION_BOTTOM) { - WindowInsets metricInsets = mWindowManager.getCurrentWindowMetrics().getWindowInsets(); - Insets navBarInsets = metricInsets.getInsetsIgnoringVisibility( - WindowInsets.Type.navigationBars()); - int newInsetLeft = mInsets.left; - int newInsetRight = mInsets.right; - if (mTaskbarPosition == TASKBAR_POSITION_LEFT) { - mPositionRect.left -= navBarInsets.left; - newInsetLeft -= navBarInsets.left; - } else if (mTaskbarPosition == TASKBAR_POSITION_RIGHT) { - mPositionRect.right += navBarInsets.right; - newInsetRight -= navBarInsets.right; - } - mInsets = Insets.of(newInsetLeft, mInsets.top, newInsetRight, mInsets.bottom); - } - } /** * @return a rect of available screen space accounting for orientation, system bars and cutouts. @@ -326,14 +280,12 @@ public class BubblePositioner { * to the left or right side. */ public boolean showBubblesVertically() { - return isLandscape() || mShowingInTaskbar || mIsLargeScreen; + return isLandscape() || mIsLargeScreen; } /** Size of the bubble. */ public int getBubbleSize() { - return (mShowingInTaskbar && mTaskbarIconSize > 0) - ? mTaskbarIconSize - : mBubbleSize; + return mBubbleSize; } /** The amount of padding at the top of the screen that the bubbles avoid when being placed. */ @@ -366,6 +318,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 +517,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 +557,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); } @@ -676,9 +650,6 @@ public class BubblePositioner { /** The position the bubble stack should rest at when collapsed. */ public PointF getRestingPosition() { - if (mPinLocation != null) { - return mPinLocation; - } if (mRestingStackPosition == null) { return getDefaultStartPosition(); } @@ -693,7 +664,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 @@ -704,7 +675,6 @@ public class BubblePositioner { 1 /* default starts with 1 bubble */)); } - /** * Returns the region that the stack position must stay within. This goes slightly off the left * and right sides of the screen, below the status bar/cutout and above the navigation bar. @@ -725,28 +695,80 @@ public class BubblePositioner { } /** - * @return whether the bubble stack is pinned to the taskbar. + * 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); + } + + // + // Bubble bar specific sizes below. + // + + /** + * Sets whether bubbles are showing in the bubble bar from launcher. + */ + public void setShowingInBubbleBar(boolean showingInBubbleBar) { + mShowingInBubbleBar = showingInBubbleBar; + } + + /** + * Sets whether bubbles are showing on launcher home, in which case positions are different. */ - public boolean showingInTaskbar() { - return mShowingInTaskbar; + public void setBubblesOnHome(boolean bubblesOnHome) { + mBubblesOnHome = bubblesOnHome; } /** - * @return the taskbar position if set. + * How wide the expanded view should be when showing from the bubble bar. */ - public int getTaskbarPosition() { - return mTaskbarPosition; + public int getExpandedViewWidthForBubbleBar() { + return mExpandedViewLargeScreenWidth; } - public int getTaskbarSize() { - return mTaskbarSize; + /** + * How tall the expanded view should be when showing from the bubble bar. + */ + public int getExpandedViewHeightForBubbleBar() { + return getAvailableRect().height() + - mBubbleBarSize + - mExpandedViewPadding * 2 + - getBubbleBarHomeAdjustment(); + } + + /** + * The amount of padding from the edge of the screen to the expanded view when in bubble bar. + */ + public int getBubbleBarExpandedViewPadding() { + return mExpandedViewPadding; + } + + /** + * Returns the on screen co-ordinates of the bubble bar. + */ + public PointF getBubbleBarPosition() { + mBubbleBarPosition.set(getAvailableRect().width() - mBubbleBarSize, + getAvailableRect().height() - mBubbleBarSize + - mExpandedViewPadding - getBubbleBarHomeAdjustment()); + return mBubbleBarPosition; } /** - * In some situations bubbles will be pinned to a specific onscreen location. This sets the - * location to anchor the stack to. + * When bubbles are shown on launcher home, there's an extra bit of padding that needs to + * be applied between the expanded view and the bubble bar. This returns the adjustment value + * if bubbles are showing on home. */ - public void setPinnedLocation(PointF point) { - mPinLocation = point; + private int getBubbleBarHomeAdjustment() { + return mBubblesOnHome ? mBubbleBarHomeAdjustment : 0; } } 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..66241628fc77 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; @@ -32,7 +33,6 @@ import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.animation.ValueAnimator; import android.annotation.SuppressLint; -import android.app.ActivityManager; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; @@ -44,23 +44,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 +77,11 @@ 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.PhysicsAnimationLayout; import com.android.wm.shell.bubbles.animation.StackAnimationController; import com.android.wm.shell.common.FloatingContentCoordinator; @@ -89,6 +94,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 +103,10 @@ import java.util.stream.Collectors; */ public class BubbleStackView extends FrameLayout implements ViewTreeObserver.OnComputeInternalInsetsListener { + + 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 +133,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 +161,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 +210,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 +237,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 +291,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 +309,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:"); @@ -556,7 +574,7 @@ public class BubbleStackView extends FrameLayout if (maybeShowStackEdu()) { mShowedUserEducationInTouchListenerActive = true; return true; - } else if (isStackEduShowing()) { + } else if (isStackEduVisible()) { mStackEduView.hide(false /* fromExpansion */); } @@ -588,16 +606,11 @@ public class BubbleStackView extends FrameLayout mBubbleContainer.setActiveController(mStackAnimationController); hideFlyoutImmediate(); - if (mPositioner.showingInTaskbar()) { - // In taskbar, the stack isn't draggable so we shouldn't dispatch touch events. - mMagnetizedObject = null; - } else { - // Save the magnetized stack so we can dispatch touch events to it. - mMagnetizedObject = mStackAnimationController.getMagnetizedStack(); - mMagnetizedObject.clearAllTargets(); - mMagnetizedObject.addTarget(mMagneticTarget); - mMagnetizedObject.setMagnetListener(mStackMagnetListener); - } + // Save the magnetized stack so we can dispatch touch events to it. + mMagnetizedObject = mStackAnimationController.getMagnetizedStack(); + mMagnetizedObject.clearAllTargets(); + mMagnetizedObject.addTarget(mMagneticTarget); + mMagnetizedObject.setMagnetListener(mStackMagnetListener); mIsDraggingStack = true; @@ -616,10 +629,7 @@ public class BubbleStackView extends FrameLayout public void onMove(@NonNull View v, @NonNull MotionEvent ev, float viewInitialX, float viewInitialY, float dx, float dy) { // If we're expanding or collapsing, ignore all touch events. - if (mIsExpansionAnimating - // Also ignore events if we shouldn't be draggable. - || (mPositioner.showingInTaskbar() && !mIsExpanded) - || mShowedUserEducationInTouchListenerActive) { + if (mIsExpansionAnimating || mShowedUserEducationInTouchListenerActive) { return; } @@ -636,11 +646,11 @@ public class BubbleStackView extends FrameLayout // bubble since it's stuck to the target. if (!passEventToMagnetizedObject(ev)) { updateBubbleShadows(true /* showForAllBubbles */); - if (mBubbleData.isExpanded() || mPositioner.showingInTaskbar()) { + if (mBubbleData.isExpanded()) { mExpandedAnimationController.dragBubbleOut( v, viewInitialX + dx, viewInitialY + dy); } else { - if (isStackEduShowing()) { + if (isStackEduVisible()) { mStackEduView.hide(false /* fromExpansion */); } mStackAnimationController.moveStackFromTouch( @@ -653,9 +663,7 @@ public class BubbleStackView extends FrameLayout public void onUp(@NonNull View v, @NonNull MotionEvent ev, float viewInitialX, float viewInitialY, float dx, float dy, float velX, float velY) { // If we're expanding or collapsing, ignore all touch events. - if (mIsExpansionAnimating - // Also ignore events if we shouldn't be draggable. - || (mPositioner.showingInTaskbar() && !mIsExpanded)) { + if (mIsExpansionAnimating) { return; } if (mShowedUserEducationInTouchListenerActive) { @@ -693,6 +701,89 @@ 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 (isManageEduVisible() || isStackEduVisible()) { + 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 @@ -751,6 +842,8 @@ public class BubbleStackView extends FrameLayout private DismissView mDismissView; private ViewGroup mManageMenu; + private TextView mManageDontBubbleText; + private ViewGroup mManageSettingsView; private ImageView mManageSettingsIcon; private TextView mManageSettingsText; private boolean mShowingManage = false; @@ -766,7 +859,7 @@ public class BubbleStackView extends FrameLayout ShellExecutor mainExecutor) { super(context); - mDelayedAnimationExecutor = mainExecutor; + mMainExecutor = mainExecutor; mBubbleController = bubbleController; mBubbleData = data; @@ -796,6 +889,10 @@ public class BubbleStackView extends FrameLayout mExpandedAnimationController = new ExpandedAnimationController(mPositioner, onBubbleAnimatedOut, this); + + mExpandedViewAnimationController = + new ExpandedViewAnimationControllerImpl(context, mPositioner); + mSurfaceSynchronizer = synchronizer != null ? synchronizer : DEFAULT_SURFACE_SYNCHRONIZER; // Force LTR by default since most of the Bubbles UI is positioned manually by the user, or @@ -821,7 +918,6 @@ public class BubbleStackView extends FrameLayout addView(mAnimatingOutSurfaceContainer); mAnimatingOutSurfaceView = new SurfaceView(getContext()); - mAnimatingOutSurfaceView.setUseAlpha(); mAnimatingOutSurfaceView.setZOrderOnTop(true); boolean supportsRoundedCorners = ScreenDecorationsUtils.supportsRoundedCornersOnWindows( mContext.getResources()); @@ -898,7 +994,7 @@ public class BubbleStackView extends FrameLayout mStackAnimationController.updateResources(); mBubbleOverflow.updateResources(); - if (!isStackEduShowing() && mRelativeStackPositionBeforeRotation != null) { + if (!isStackEduVisible() && mRelativeStackPositionBeforeRotation != null) { mStackAnimationController.setStackPosition( mRelativeStackPositionBeforeRotation); mRelativeStackPositionBeforeRotation = null; @@ -948,9 +1044,9 @@ public class BubbleStackView extends FrameLayout setOnClickListener(view -> { if (mShowingManage) { showManageMenu(false /* show */); - } else if (mManageEduView != null && mManageEduView.getVisibility() == VISIBLE) { + } else if (isManageEduVisible()) { mManageEduView.hide(); - } else if (isStackEduShowing()) { + } else if (isStackEduVisible()) { mStackEduView.hide(false /* isExpanding */); } else if (mBubbleData.isExpanded()) { mBubbleData.setExpanded(false); @@ -971,7 +1067,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 +1081,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); } }); @@ -1120,7 +1217,11 @@ public class BubbleStackView extends FrameLayout mUnbubbleConversationCallback.accept(mBubbleData.getSelectedBubble().getKey()); }); - mManageMenu.findViewById(R.id.bubble_manage_menu_settings_container).setOnClickListener( + mManageDontBubbleText = mManageMenu + .findViewById(R.id.bubble_manage_menu_dont_bubble_text); + + mManageSettingsView = mManageMenu.findViewById(R.id.bubble_manage_menu_settings_container); + mManageSettingsView.setOnClickListener( view -> { showManageMenu(false /* show */); final BubbleViewProvider bubble = mBubbleData.getSelectedBubble(); @@ -1144,15 +1245,24 @@ public class BubbleStackView extends FrameLayout } /** + * Whether the selected bubble is conversation bubble + */ + private boolean isConversationBubble() { + BubbleViewProvider bubble = mBubbleData.getSelectedBubble(); + return bubble instanceof Bubble && ((Bubble) bubble).isConversation(); + } + + /** * Whether the educational view should show for the expanded view "manage" menu. */ private boolean shouldShowManageEdu() { - if (ActivityManager.isRunningInTestHarness()) { + if (!isConversationBubble()) { + // We only show user education for conversation bubbles right now return false; } 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); } @@ -1170,11 +1280,17 @@ public class BubbleStackView extends FrameLayout mManageEduView.show(mExpandedBubble.getExpandedView()); } + @VisibleForTesting + public boolean isManageEduVisible() { + return mManageEduView != null && mManageEduView.getVisibility() == VISIBLE; + } + /** * Whether education view should show for the collapsed stack. */ private boolean shouldShowStackEdu() { - if (ActivityManager.isRunningInTestHarness()) { + if (!isConversationBubble()) { + // We only show user education for conversation bubbles right now return false; } final boolean seen = getPrefBoolean(StackEducationViewKt.PREF_STACK_EDUCATION); @@ -1207,13 +1323,14 @@ public class BubbleStackView extends FrameLayout return mStackEduView.show(mPositioner.getDefaultStartPosition()); } - private boolean isStackEduShowing() { + @VisibleForTesting + public boolean isStackEduVisible() { return mStackEduView != null && mStackEduView.getVisibility() == VISIBLE; } // Recreates & shows the education views. Call when a theme/config change happens. private void updateUserEdu() { - if (isStackEduShowing()) { + if (isStackEduVisible()) { removeView(mStackEduView); mStackEduView = new StackEducationView(mContext, mPositioner, mBubbleController); addView(mStackEduView); @@ -1222,7 +1339,7 @@ public class BubbleStackView extends FrameLayout mStackAnimationController.setStackPosition(mPositioner.getDefaultStartPosition()); mStackEduView.show(mPositioner.getDefaultStartPosition()); } - if (mManageEduView != null && mManageEduView.getVisibility() == VISIBLE) { + if (isManageEduVisible()) { removeView(mManageEduView); mManageEduView = new ManageEducationView(mContext, mPositioner); addView(mManageEduView); @@ -1262,16 +1379,6 @@ public class BubbleStackView extends FrameLayout updateOverflowVisibility(); } - void updateOverflowButtonDot() { - for (Bubble b : mBubbleData.getOverflowBubbles()) { - if (b.showDot()) { - mBubbleOverflow.setShowDot(true); - return; - } - } - mBubbleOverflow.setShowDot(false); - } - /** * Handle theme changes. */ @@ -1336,7 +1443,7 @@ public class BubbleStackView extends FrameLayout mStackAnimationController.updateResources(); mDismissView.updateResources(); mMagneticTarget.setMagneticFieldRadiusPx(mBubbleSize * 2); - if (!isStackEduShowing()) { + if (!isStackEduVisible()) { mStackAnimationController.setStackPosition( new RelativeStackPosition( mPositioner.getRestingPosition(), @@ -1708,7 +1815,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 +1831,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 +1905,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 +1954,16 @@ public class BubbleStackView extends FrameLayout return; } + boolean wasExpanded = mIsExpanded; + hideCurrentInputMethod(); mBubbleController.getSysuiProxy().onStackExpandChanged(shouldExpand); - if (mIsExpanded) { + if (wasExpanded) { + stopMonitoringSwipeUpGesture(); animateCollapse(); + showManageMenu(false); logBubbleEvent(mExpandedBubble, FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__COLLAPSED); } else { animateExpansion(); @@ -1856,18 +1971,63 @@ public class BubbleStackView extends FrameLayout logBubbleEvent(mExpandedBubble, FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__EXPANDED); logBubbleEvent(mExpandedBubble, FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__STACK_EXPANDED); + 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() { if (mIsExpanded) { if (mShowingManage) { showManageMenu(false); - } else if (mManageEduView != null && mManageEduView.getVisibility() == VISIBLE) { + } else if (isManageEduVisible()) { mManageEduView.hide(); } else { mBubbleData.setExpanded(false); @@ -1982,15 +2142,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(); } } @@ -1999,7 +2172,7 @@ public class BubbleStackView extends FrameLayout cancelDelayedExpandCollapseSwitchAnimations(); final boolean showVertically = mPositioner.showBubblesVertically(); mIsExpanded = true; - if (isStackEduShowing()) { + if (isStackEduVisible()) { mStackEduView.hide(true /* fromExpansion */); } beforeExpandedViewAnimation(); @@ -2072,11 +2245,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,17 +2288,15 @@ public class BubbleStackView extends FrameLayout }) .start(); }; - mDelayedAnimationExecutor.executeDelayed(mDelayedAnimation, startDelay); + mMainExecutor.executeDelayed(mDelayedAnimation, startDelay); } private void animateCollapse() { cancelDelayedExpandCollapseSwitchAnimations(); - if (mManageEduView != null && mManageEduView.getVisibility() == VISIBLE) { + if (isManageEduVisible()) { mManageEduView.hide(); } - // Hide the menu if it's visible. - showManageMenu(false); mIsExpanded = false; mIsExpansionAnimating = true; @@ -2143,78 +2315,42 @@ public class BubbleStackView extends FrameLayout // since we're about to animate collapsed. mExpandedAnimationController.notifyPreparingToCollapse(); - mExpandedAnimationController.collapseBackToStack( - mStackAnimationController.getStackPositionAlongNearestHorizontalEdge() + final Runnable collapseBackToStack = () -> mExpandedAnimationController.collapseBackToStack( + mStackAnimationController + .getStackPositionAlongNearestHorizontalEdge() /* collapseTo */, () -> mBubbleContainer.setActiveController(mStackAnimationController)); - int index; - if (mExpandedBubble != null && BubbleOverflow.KEY.equals(mExpandedBubble.getKey())) { - index = mBubbleData.getBubbles().size(); - } else { - index = mBubbleData.getBubbles().indexOf(mExpandedBubble); - } - // Value the bubble is animating from (back into the stack). - final PointF p = mPositioner.getExpandedBubbleXY(index, getState()); - if (mPositioner.showBubblesVertically()) { - float pivotX; - float pivotY = p.y + mBubbleSize / 2f; - if (mStackOnLeftOrWillBe) { - pivotX = mPositioner.getAvailableRect().left + mBubbleSize + mExpandedViewPadding; - } else { - pivotX = mPositioner.getAvailableRect().right - mBubbleSize - mExpandedViewPadding; + final Runnable after = () -> { + final BubbleViewProvider previouslySelected = mExpandedBubble; + // TODO(b/231350255): investigate why this call is needed here + beforeExpandedViewAnimation(); + if (mManageEduView != null) { + mManageEduView.hide(); } - mExpandedViewContainerMatrix.setScale( - 1f, 1f, - pivotX, pivotY); - } else { - mExpandedViewContainerMatrix.setScale( - 1f, 1f, - p.x + mBubbleSize / 2f, - p.y + mBubbleSize + mExpandedViewPadding); - } - - mExpandedViewAlphaAnimator.reverse(); - // When the animation completes, we should no longer be showing the content. - if (mExpandedBubble.getExpandedView() != null) { + 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); } - - PhysicsAnimator.getInstance(mExpandedViewContainerMatrix).cancel(); - PhysicsAnimator.getInstance(mExpandedViewContainerMatrix) - .spring(AnimatableScaleMatrix.SCALE_X, - AnimatableScaleMatrix.getAnimatableValueForScaleFactor( - 1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT), - mScaleOutSpringConfig) - .spring(AnimatableScaleMatrix.SCALE_Y, - AnimatableScaleMatrix.getAnimatableValueForScaleFactor( - 1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT), - mScaleOutSpringConfig) - .addUpdateListener((target, values) -> { - mExpandedViewContainer.setAnimationMatrix(mExpandedViewContainerMatrix); - }) - .withEndActions(() -> { - final BubbleViewProvider previouslySelected = mExpandedBubble; - 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); - } - }) - .start(); } private void animateSwitchBubbles() { @@ -2277,7 +2413,7 @@ public class BubbleStackView extends FrameLayout mExpandedViewContainer.setAnimationMatrix(mExpandedViewContainerMatrix); - mDelayedAnimationExecutor.executeDelayed(() -> { + mMainExecutor.executeDelayed(() -> { if (!mIsExpanded) { mIsBubbleSwitchAnimating = false; return; @@ -2308,7 +2444,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,14 +2468,16 @@ 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 ((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; } @@ -2361,27 +2499,31 @@ public class BubbleStackView extends FrameLayout FLYOUT_IME_ANIMATION_SPRING_CONFIG) .start(); } - } else if (mPositioner.showBubblesVertically() && mIsExpanded - && 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); + 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(); } - updatePointerPosition(true /* forIme */); - AnimatorSet set = new AnimatorSet(); - set.playTogether(animList); - set.start(); } } @@ -2473,8 +2615,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); @@ -2547,7 +2691,7 @@ public class BubbleStackView extends FrameLayout if (flyoutMessage == null || flyoutMessage.message == null || !bubble.showFlyout() - || isStackEduShowing() + || isStackEduVisible() || isExpanded() || mIsExpansionAnimating || mIsGestureInProgress @@ -2670,7 +2814,7 @@ public class BubbleStackView extends FrameLayout * them. */ public void getTouchableRegion(Rect outRect) { - if (isStackEduShowing()) { + if (isStackEduVisible()) { // When user education shows then capture all touches outRect.set(0, 0, getWidth(), getHeight()); return; @@ -2744,10 +2888,19 @@ public class BubbleStackView extends FrameLayout // name and icon. if (show) { final Bubble bubble = mBubbleData.getBubbleInStackWithKey(mExpandedBubble.getKey()); - if (bubble != null) { + if (bubble != null && !bubble.isAppBubble()) { + // Setup options for non app bubbles + mManageDontBubbleText.setText(R.string.bubbles_dont_bubble_conversation); mManageSettingsIcon.setImageBitmap(bubble.getRawAppBadge()); mManageSettingsText.setText(getResources().getString( R.string.bubbles_app_settings, bubble.getAppName())); + mManageSettingsView.setVisibility(VISIBLE); + } else { + // Setup options for app bubbles + mManageDontBubbleText.setText(R.string.bubbles_dont_bubble); + // App bubbles are not notification based + // so we don't show the option to go to notification settings + mManageSettingsView.setVisibility(GONE); } } @@ -2786,8 +2939,10 @@ public class BubbleStackView extends FrameLayout .withEndActions(() -> { View child = mManageMenu.getChildAt(0); child.requestAccessibilityFocus(); - // Update the AV's obscured touchable region for the new visibility state. - mExpandedBubble.getExpandedView().updateObscuredTouchableRegion(); + if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) { + // Update the AV's obscured touchable region for the new state. + mExpandedBubble.getExpandedView().updateObscuredTouchableRegion(); + } }) .start(); @@ -2810,6 +2965,15 @@ public class BubbleStackView extends FrameLayout } } + /** + * Checks whether manage menu notification settings action is available and visible + * Used for testing + */ + @VisibleForTesting + public boolean isManageMenuSettingsVisible() { + return mManageSettingsView != null && mManageSettingsView.getVisibility() == VISIBLE; + } + private void updateExpandedBubble() { if (DEBUG_BUBBLE_STACK_VIEW) { Log.d(TAG, "updateExpandedBubble()"); @@ -2820,7 +2984,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/BubbleTaskViewHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java new file mode 100644 index 000000000000..7a5815994dd0 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java @@ -0,0 +1,282 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.wm.shell.bubbles; + +import static android.app.ActivityTaskManager.INVALID_TASK_ID; +import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK; +import static android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT; + +import static com.android.wm.shell.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_EXPANDED_VIEW; + +import android.app.ActivityOptions; +import android.app.ActivityTaskManager; +import android.app.PendingIntent; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.graphics.Rect; +import android.os.RemoteException; +import android.util.Log; +import android.view.View; + +import androidx.annotation.Nullable; + +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.annotations.ShellMainThread; +import com.android.wm.shell.taskview.TaskView; +import com.android.wm.shell.taskview.TaskViewTaskController; + +/** + * Handles creating and updating the {@link TaskView} associated with a {@link Bubble}. + */ +public class BubbleTaskViewHelper { + + private static final String TAG = BubbleTaskViewHelper.class.getSimpleName(); + + /** + * Listener for users of {@link BubbleTaskViewHelper} to use to be notified of events + * on the task. + */ + public interface Listener { + + /** Called when the task is first created. */ + void onTaskCreated(); + + /** Called when the visibility of the task changes. */ + void onContentVisibilityChanged(boolean visible); + + /** Called when back is pressed on the task root. */ + void onBackPressed(); + } + + private final Context mContext; + private final BubbleController mController; + private final @ShellMainThread ShellExecutor mMainExecutor; + private final BubbleTaskViewHelper.Listener mListener; + private final View mParentView; + + @Nullable + private Bubble mBubble; + @Nullable + private PendingIntent mPendingIntent; + private TaskViewTaskController mTaskViewTaskController; + @Nullable + private TaskView mTaskView; + private int mTaskId = INVALID_TASK_ID; + + private final TaskView.Listener mTaskViewListener = new TaskView.Listener() { + private boolean mInitialized = false; + private boolean mDestroyed = false; + + @Override + public void onInitialized() { + if (DEBUG_BUBBLE_EXPANDED_VIEW) { + Log.d(TAG, "onInitialized: destroyed=" + mDestroyed + + " initialized=" + mInitialized + + " bubble=" + getBubbleKey()); + } + + if (mDestroyed || mInitialized) { + return; + } + + // Custom options so there is no activity transition animation + ActivityOptions options = ActivityOptions.makeCustomAnimation(mContext, + 0 /* enterResId */, 0 /* exitResId */); + + Rect launchBounds = new Rect(); + mTaskView.getBoundsOnScreen(launchBounds); + + // TODO: I notice inconsistencies in lifecycle + // Post to keep the lifecycle normal + mParentView.post(() -> { + if (DEBUG_BUBBLE_EXPANDED_VIEW) { + Log.d(TAG, "onInitialized: calling startActivity, bubble=" + + getBubbleKey()); + } + try { + options.setTaskAlwaysOnTop(true); + options.setLaunchedFromBubble(true); + + Intent fillInIntent = new Intent(); + // Apply flags to make behaviour match documentLaunchMode=always. + fillInIntent.addFlags(FLAG_ACTIVITY_NEW_DOCUMENT); + fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK); + + if (mBubble.isAppBubble()) { + PendingIntent pi = PendingIntent.getActivity(mContext, 0, + mBubble.getAppBubbleIntent(), + PendingIntent.FLAG_MUTABLE, + null); + mTaskView.startActivity(pi, fillInIntent, options, launchBounds); + } else if (mBubble.hasMetadataShortcutId()) { + options.setApplyActivityFlagsForBubbles(true); + mTaskView.startShortcutActivity(mBubble.getShortcutInfo(), + options, launchBounds); + } else { + if (mBubble != null) { + mBubble.setIntentActive(); + } + mTaskView.startActivity(mPendingIntent, fillInIntent, options, + launchBounds); + } + } catch (RuntimeException e) { + // If there's a runtime exception here then there's something + // wrong with the intent, we can't really recover / try to populate + // the bubble again so we'll just remove it. + Log.w(TAG, "Exception while displaying bubble: " + getBubbleKey() + + ", " + e.getMessage() + "; removing bubble"); + mController.removeBubble(getBubbleKey(), Bubbles.DISMISS_INVALID_INTENT); + } + mInitialized = true; + }); + } + + @Override + public void onReleased() { + mDestroyed = true; + } + + @Override + public void onTaskCreated(int taskId, ComponentName name) { + if (DEBUG_BUBBLE_EXPANDED_VIEW) { + Log.d(TAG, "onTaskCreated: taskId=" + taskId + + " bubble=" + getBubbleKey()); + } + // The taskId is saved to use for removeTask, preventing appearance in recent tasks. + mTaskId = taskId; + + // With the task org, the taskAppeared callback will only happen once the task has + // already drawn + mListener.onTaskCreated(); + } + + @Override + public void onTaskVisibilityChanged(int taskId, boolean visible) { + mListener.onContentVisibilityChanged(visible); + } + + @Override + public void onTaskRemovalStarted(int taskId) { + if (DEBUG_BUBBLE_EXPANDED_VIEW) { + Log.d(TAG, "onTaskRemovalStarted: taskId=" + taskId + + " bubble=" + getBubbleKey()); + } + if (mBubble != null) { + mController.removeBubble(mBubble.getKey(), Bubbles.DISMISS_TASK_FINISHED); + } + } + + @Override + public void onBackPressedOnTaskRoot(int taskId) { + if (mTaskId == taskId && mController.isStackExpanded()) { + mListener.onBackPressed(); + } + } + }; + + public BubbleTaskViewHelper(Context context, + BubbleController controller, + BubbleTaskViewHelper.Listener listener, + View parent) { + mContext = context; + mController = controller; + mMainExecutor = mController.getMainExecutor(); + mListener = listener; + mParentView = parent; + mTaskViewTaskController = new TaskViewTaskController(mContext, + mController.getTaskOrganizer(), + mController.getTaskViewTransitions(), mController.getSyncTransactionQueue()); + mTaskView = new TaskView(mContext, mTaskViewTaskController); + mTaskView.setListener(mMainExecutor, mTaskViewListener); + } + + /** + * Sets the bubble or updates the bubble used to populate the view. + * + * @return true if the bubble is new, false if it was an update to the same bubble. + */ + public boolean update(Bubble bubble) { + boolean isNew = mBubble == null || didBackingContentChange(bubble); + mBubble = bubble; + if (isNew) { + mPendingIntent = mBubble.getBubbleIntent(); + return true; + } + return false; + } + + /** Cleans up anything related to the task and {@code TaskView}. */ + public void cleanUpTaskView() { + if (DEBUG_BUBBLE_EXPANDED_VIEW) { + Log.d(TAG, "cleanUpExpandedState: bubble=" + getBubbleKey() + " task=" + mTaskId); + } + if (mTaskId != INVALID_TASK_ID) { + try { + ActivityTaskManager.getService().removeTask(mTaskId); + } catch (RemoteException e) { + Log.w(TAG, e.getMessage()); + } + } + if (mTaskView != null) { + mTaskView.release(); + mTaskView = null; + } + } + + /** Returns the bubble key associated with this view. */ + @Nullable + public String getBubbleKey() { + return mBubble != null ? mBubble.getKey() : null; + } + + /** Returns the TaskView associated with this view. */ + @Nullable + public TaskView getTaskView() { + return mTaskView; + } + + /** + * Returns the task id associated with the task in this view. If the task doesn't exist then + * {@link ActivityTaskManager#INVALID_TASK_ID}. + */ + public int getTaskId() { + return mTaskId; + } + + /** Returns whether the bubble set on the helper is valid to populate the task view. */ + public boolean isValidBubble() { + return mBubble != null && (mPendingIntent != null || mBubble.hasMetadataShortcutId()); + } + + // TODO (b/274980695): Is this still relevant? + /** + * Bubbles are backed by a pending intent or a shortcut, once the activity is + * started we never change it / restart it on notification updates -- unless the bubble's + * backing data switches. + * + * This indicates if the new bubble is backed by a different data source than what was + * previously shown here (e.g. previously a pending intent & now a shortcut). + * + * @param newBubble the bubble this view is being updated with. + * @return true if the backing content has changed. + */ + private boolean didBackingContentChange(Bubble newBubble) { + boolean prevWasIntentBased = mBubble != null && mPendingIntent != null; + boolean newIsIntentBased = newBubble.getBubbleIntent() != null; + return prevWasIntentBased != newIsIntentBased; + } +} 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..259f69296ac7 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 @@ -16,32 +16,36 @@ package com.android.wm.shell.bubbles; +import static android.window.ScreenCapture.ScreenshotSync; + import static java.lang.annotation.ElementType.FIELD; import static java.lang.annotation.ElementType.LOCAL_VARIABLE; import static java.lang.annotation.ElementType.PARAMETER; import static java.lang.annotation.RetentionPolicy.SOURCE; import android.app.NotificationChannel; +import android.content.Intent; import android.content.pm.UserInfo; -import android.content.res.Configuration; -import android.os.Bundle; +import android.graphics.drawable.Icon; +import android.hardware.HardwareBuffer; 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; +import android.window.ScreenCapture.ScreenshotHardwareBuffer; import androidx.annotation.IntDef; import androidx.annotation.Nullable; import com.android.wm.shell.common.annotations.ExternalThread; +import com.android.wm.shell.common.bubbles.BubbleBarUpdate; -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; @@ -78,6 +82,11 @@ public interface Bubbles { int DISMISS_RELOAD_FROM_DISK = 15; int DISMISS_USER_REMOVED = 16; + /** Returns a binder that can be passed to an external process to manipulate Bubbles. */ + default IBubbles createExternalInterface() { + return null; + } + /** * @return {@code true} if there is a bubble associated with the provided key and if its * notification is hidden from the shade or there is a group summary associated with the @@ -92,24 +101,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. @@ -126,17 +120,50 @@ public interface Bubbles { void expandStackAndSelectBubble(Bubble bubble); /** + * This method has different behavior depending on: + * - if an app bubble exists + * - if an app bubble is expanded + * + * If no app bubble exists, this will add and expand a bubble with the provided intent. The + * intent must be explicit (i.e. include a package name or fully qualified component class name) + * and the activity for it should be resizable. + * + * If an app bubble exists, this will toggle the visibility of it, i.e. if the app bubble is + * expanded, calling this method will collapse it. If the app bubble is not expanded, calling + * this method will expand it. + * + * These bubbles are <b>not</b> backed by a notification and remain until the user dismisses + * the bubble or bubble stack. + * + * Some notes: + * - Only one app bubble is supported at a time, regardless of users. Multi-users support is + * tracked in b/273533235. + * - Calling this method with a different intent than the existing app bubble will do nothing + * + * @param intent the intent to display in the bubble expanded view. + * @param user the {@link UserHandle} of the user to start this activity for. + * @param icon the {@link Icon} to use for the bubble view. + */ + void showOrHideAppBubble(Intent intent, UserHandle user, @Nullable Icon icon); + + /** @return true if the specified {@code taskId} corresponds to app bubble's taskId. */ + boolean isAppBubbleTaskId(int taskId); + + /** + * @return a {@link ScreenshotSync} after performing a screenshot that may exclude the bubble + * layer, if one is present. The underlying {@link ScreenshotHardwareBuffer} can be access via + * {@link ScreenshotSync#get()} asynchronously and care should be taken to + * {@link HardwareBuffer#close()} the associated + * {@link ScreenshotHardwareBuffer#getHardwareBuffer()} when no longer required. + */ + ScreenshotSync getScreenshotExcludingBubble(int displayId); + + /** * @return a bubble that matches the provided shortcutId, if one exists. */ @Nullable Bubble getBubbleWithShortcutId(String shortcutId); - /** 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 +202,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 +242,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). */ @@ -251,14 +284,15 @@ public interface Bubbles { void onUserRemoved(int removedUserId); /** - * Called when config changed. - * - * @param newConfig the new config. + * A listener to be notified of bubble state changes, used by launcher to render bubbles in + * its process. */ - void onConfigChanged(Configuration newConfig); - - /** Description of current bubble state. */ - void dump(PrintWriter pw, String[] args); + interface BubbleStateListener { + /** + * Called when the bubbles state changes. + */ + void onBubbleStateChange(BubbleBarUpdate update); + } /** Listener to find out about stack expansion / collapse events. */ interface BubbleExpandListener { @@ -285,11 +319,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 +334,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..3a3a378e00d3 --- /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 = mContext.getSystemService(InputManager.class) + .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/IBubbles.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl new file mode 100644 index 000000000000..862e818a998b --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.bubbles; + +import android.content.Intent; +import com.android.wm.shell.bubbles.IBubblesListener; + +/** + * Interface that is exposed to remote callers (launcher) to manipulate the bubbles feature when + * showing in the bubble bar. + */ +interface IBubbles { + + oneway void registerBubbleListener(in IBubblesListener listener) = 1; + + oneway void unregisterBubbleListener(in IBubblesListener listener) = 2; + + oneway void showBubble(in String key, in boolean onLauncherHome) = 3; + + oneway void removeBubble(in String key, in int reason) = 4; + + oneway void collapseBubbles() = 5; + + oneway void onTaskbarStateChanged(in int newState) = 6; + +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellInit.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubblesListener.aidl index d7010b174744..e48f8d5f1c84 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellInit.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubblesListener.aidl @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 The Android Open Source Project + * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,17 +14,16 @@ * limitations under the License. */ -package com.android.wm.shell; - -import com.android.wm.shell.common.annotations.ExternalThread; +package com.android.wm.shell.bubbles; +import android.os.Bundle; /** - * An entry point into the shell for initializing shell internal state. + * Listener interface that Launcher attaches to SystemUI to get bubbles callbacks. */ -@ExternalThread -public interface ShellInit { +oneway interface IBubblesListener { + /** - * Initializes the shell state. + * Called when the bubbles state changes. */ - void init(); -} + void onBubbleStateChange(in Bundle update); +}
\ 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..33629f9f4622 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,16 @@ 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 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 +65,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 @@ -233,6 +240,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 +279,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 +301,14 @@ public class ExpandedAnimationController (firstBubbleLeads && index == 0) || (!firstBubbleLeads && index == mLayout.getChildCount() - 1); + Interpolator interpolator = expanding + ? Interpolators.EMPHASIZED_ACCELERATE : Interpolators.EMPHASIZED_DECELERATE; + 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 +355,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 +458,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); 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/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..5533842f2d89 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; @@ -419,9 +420,6 @@ public class StackAnimationController extends * Where the stack would be if it were snapped to the nearest horizontal edge (left or right). */ public PointF getStackPositionAlongNearestHorizontalEdge() { - if (mPositioner.showingInTaskbar()) { - return mPositioner.getRestingPosition(); - } final PointF stackPos = getStackPosition(); final boolean onLeft = mLayout.isFirstChildXLeftOfCenter(stackPos.x); final RectF bounds = mPositioner.getAllowableStackPositionRegion(getBubbleCount()); @@ -431,7 +429,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 +1026,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/bubbles/storage/BubbleEntity.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/storage/BubbleEntity.kt index 186b9b1efa9a..9b2e26394605 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/storage/BubbleEntity.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/storage/BubbleEntity.kt @@ -27,5 +27,6 @@ data class BubbleEntity( @DimenRes val desiredHeightResId: Int, val title: String? = null, val taskId: Int, - val locus: String? = null + val locus: String? = null, + val isDismissable: Boolean = false ) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/storage/BubbleXmlHelper.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/storage/BubbleXmlHelper.kt index f4fa1835b7a5..48d8ccf40174 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/storage/BubbleXmlHelper.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/storage/BubbleXmlHelper.kt @@ -43,6 +43,7 @@ private const val ATTR_DESIRED_HEIGHT_RES_ID = "hid" private const val ATTR_TITLE = "t" private const val ATTR_TASK_ID = "tid" private const val ATTR_LOCUS = "l" +private const val ATTR_DISMISSABLE = "d" /** * Writes the bubbles in xml format into given output stream. @@ -84,6 +85,7 @@ private fun writeXmlEntry(serializer: XmlSerializer, bubble: BubbleEntity) { bubble.title?.let { serializer.attribute(null, ATTR_TITLE, it) } serializer.attribute(null, ATTR_TASK_ID, bubble.taskId.toString()) bubble.locus?.let { serializer.attribute(null, ATTR_LOCUS, it) } + serializer.attribute(null, ATTR_DISMISSABLE, bubble.isDismissable.toString()) serializer.endTag(null, TAG_BUBBLE) } catch (e: IOException) { throw RuntimeException(e) @@ -142,7 +144,8 @@ private fun readXmlEntry(parser: XmlPullParser): BubbleEntity? { parser.getAttributeWithName(ATTR_DESIRED_HEIGHT_RES_ID)?.toInt() ?: return null, parser.getAttributeWithName(ATTR_TITLE), parser.getAttributeWithName(ATTR_TASK_ID)?.toInt() ?: INVALID_TASK_ID, - parser.getAttributeWithName(ATTR_LOCUS) + parser.getAttributeWithName(ATTR_LOCUS), + parser.getAttributeWithName(ATTR_DISMISSABLE)?.toBoolean() ?: false ) } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DevicePostureController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DevicePostureController.java new file mode 100644 index 000000000000..8b4ac1a8dc79 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DevicePostureController.java @@ -0,0 +1,150 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.common; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.content.Context; +import android.hardware.devicestate.DeviceStateManager; +import android.util.SparseIntArray; + +import com.android.internal.R; +import com.android.internal.annotations.VisibleForTesting; +import com.android.wm.shell.sysui.ShellInit; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.List; + +/** + * Wrapper class to track the device posture change on Fold-ables. + * See also <a + * href="https://developer.android.com/guide/topics/large-screens/learn-about-foldables + * #foldable_postures">Foldable states and postures</a> for reference. + * + * Note that most of the implementation here inherits from + * {@link com.android.systemui.statusbar.policy.DevicePostureController}. + * + * Use the {@link TabletopModeController} if you are interested in tabletop mode change only, + * which is more common. + */ +public class DevicePostureController { + @IntDef(prefix = {"DEVICE_POSTURE_"}, value = { + DEVICE_POSTURE_UNKNOWN, + DEVICE_POSTURE_CLOSED, + DEVICE_POSTURE_HALF_OPENED, + DEVICE_POSTURE_OPENED, + DEVICE_POSTURE_FLIPPED + }) + @Retention(RetentionPolicy.SOURCE) + public @interface DevicePostureInt {} + + // NOTE: These constants **must** match those defined for Jetpack Sidecar. This is because we + // use the Device State -> Jetpack Posture map to translate between the two. + public static final int DEVICE_POSTURE_UNKNOWN = 0; + public static final int DEVICE_POSTURE_CLOSED = 1; + public static final int DEVICE_POSTURE_HALF_OPENED = 2; + public static final int DEVICE_POSTURE_OPENED = 3; + public static final int DEVICE_POSTURE_FLIPPED = 4; + + private final Context mContext; + private final ShellExecutor mMainExecutor; + private final List<OnDevicePostureChangedListener> mListeners = new ArrayList<>(); + private final SparseIntArray mDeviceStateToPostureMap = new SparseIntArray(); + + private int mDevicePosture = DEVICE_POSTURE_UNKNOWN; + + public DevicePostureController( + Context context, ShellInit shellInit, ShellExecutor mainExecutor) { + mContext = context; + mMainExecutor = mainExecutor; + shellInit.addInitCallback(this::onInit, this); + } + + private void onInit() { + // Most of this is borrowed from WindowManager/Jetpack/DeviceStateManagerPostureProducer. + // Using the sidecar/extension libraries directly brings in a new dependency that it'd be + // good to avoid (along with the fact that sidecar is deprecated, and extensions isn't fully + // ready yet), and we'd have to make our own layer over the sidecar library anyway to easily + // allow the implementation to change, so it was easier to just interface with + // DeviceStateManager directly. + String[] deviceStatePosturePairs = mContext.getResources() + .getStringArray(R.array.config_device_state_postures); + for (String deviceStatePosturePair : deviceStatePosturePairs) { + String[] deviceStatePostureMapping = deviceStatePosturePair.split(":"); + if (deviceStatePostureMapping.length != 2) { + continue; + } + + int deviceState; + int posture; + try { + deviceState = Integer.parseInt(deviceStatePostureMapping[0]); + posture = Integer.parseInt(deviceStatePostureMapping[1]); + } catch (NumberFormatException e) { + continue; + } + + mDeviceStateToPostureMap.put(deviceState, posture); + } + + final DeviceStateManager deviceStateManager = mContext.getSystemService( + DeviceStateManager.class); + if (deviceStateManager != null) { + deviceStateManager.registerCallback(mMainExecutor, state -> onDevicePostureChanged( + mDeviceStateToPostureMap.get(state, DEVICE_POSTURE_UNKNOWN))); + } + } + + @VisibleForTesting + void onDevicePostureChanged(int devicePosture) { + if (devicePosture == mDevicePosture) return; + mDevicePosture = devicePosture; + mListeners.forEach(l -> l.onDevicePostureChanged(mDevicePosture)); + } + + /** + * Register {@link OnDevicePostureChangedListener} for device posture changes. + * The listener will receive callback with current device posture upon registration. + */ + public void registerOnDevicePostureChangedListener( + @NonNull OnDevicePostureChangedListener listener) { + if (mListeners.contains(listener)) return; + mListeners.add(listener); + listener.onDevicePostureChanged(mDevicePosture); + } + + /** + * Unregister {@link OnDevicePostureChangedListener} for device posture changes. + */ + public void unregisterOnDevicePostureChangedListener( + @NonNull OnDevicePostureChangedListener listener) { + mListeners.remove(listener); + } + + /** + * Listener interface for device posture change. + */ + public interface OnDevicePostureChangedListener { + /** + * Callback when device posture changes. + * See {@link DevicePostureInt} for callback values. + */ + void onDevicePostureChanged(@DevicePostureInt int posture); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DismissCircleView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DismissCircleView.java index 976fba52b9e2..e0c782d1675b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DismissCircleView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DismissCircleView.java @@ -49,6 +49,8 @@ public class DismissCircleView extends FrameLayout { @Override protected void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); + final Resources res = getResources(); + setBackground(res.getDrawable(R.drawable.dismiss_circle_background)); setViewSizes(); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayChangeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayChangeController.java index c32733d4f73c..ae1f43320c8b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayChangeController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayChangeController.java @@ -16,16 +16,19 @@ package com.android.wm.shell.common; +import android.annotation.Nullable; import android.os.RemoteException; import android.util.Slog; -import android.view.IDisplayWindowRotationCallback; -import android.view.IDisplayWindowRotationController; +import android.view.IDisplayChangeWindowCallback; +import android.view.IDisplayChangeWindowController; import android.view.IWindowManager; +import android.window.DisplayAreaInfo; import android.window.WindowContainerTransaction; import androidx.annotation.BinderThread; import com.android.wm.shell.common.annotations.ShellMainThread; +import com.android.wm.shell.sysui.ShellInit; import java.util.concurrent.CopyOnWriteArrayList; @@ -40,17 +43,22 @@ public class DisplayChangeController { private final ShellExecutor mMainExecutor; private final IWindowManager mWmService; - private final IDisplayWindowRotationController mControllerImpl; + private final IDisplayChangeWindowController mControllerImpl; - private final CopyOnWriteArrayList<OnDisplayChangingListener> mRotationListener = + private final CopyOnWriteArrayList<OnDisplayChangingListener> mDisplayChangeListener = new CopyOnWriteArrayList<>(); - public DisplayChangeController(IWindowManager wmService, ShellExecutor mainExecutor) { + public DisplayChangeController(IWindowManager wmService, ShellInit shellInit, + ShellExecutor mainExecutor) { mMainExecutor = mainExecutor; mWmService = wmService; - mControllerImpl = new DisplayWindowRotationControllerImpl(); + mControllerImpl = new DisplayChangeWindowControllerImpl(); + shellInit.addInitCallback(this::onInit, this); + } + + private void onInit() { try { - mWmService.setDisplayWindowRotationController(mControllerImpl); + mWmService.setDisplayChangeWindowController(mControllerImpl); } catch (RemoteException e) { throw new RuntimeException("Unable to register rotation controller"); } @@ -59,63 +67,64 @@ public class DisplayChangeController { /** * Adds a display rotation controller. */ - public void addRotationListener(OnDisplayChangingListener listener) { - mRotationListener.add(listener); + public void addDisplayChangeListener(OnDisplayChangingListener listener) { + mDisplayChangeListener.add(listener); } /** * Removes a display rotation controller. */ - public void removeRotationListener(OnDisplayChangingListener listener) { - mRotationListener.remove(listener); + public void removeDisplayChangeListener(OnDisplayChangingListener listener) { + mDisplayChangeListener.remove(listener); } - /** Query all listeners for changes that should happen on rotation. */ - public void dispatchOnRotateDisplay(WindowContainerTransaction outWct, int displayId, - final int fromRotation, final int toRotation) { - for (OnDisplayChangingListener c : mRotationListener) { - c.onRotateDisplay(displayId, fromRotation, toRotation, outWct); + /** Query all listeners for changes that should happen on display change. */ + public void dispatchOnDisplayChange(WindowContainerTransaction outWct, int displayId, + int fromRotation, int toRotation, DisplayAreaInfo newDisplayAreaInfo) { + for (OnDisplayChangingListener c : mDisplayChangeListener) { + c.onDisplayChange(displayId, fromRotation, toRotation, newDisplayAreaInfo, outWct); } } - private void onRotateDisplay(int displayId, final int fromRotation, final int toRotation, - IDisplayWindowRotationCallback callback) { + private void onDisplayChange(int displayId, int fromRotation, int toRotation, + DisplayAreaInfo newDisplayAreaInfo, IDisplayChangeWindowCallback callback) { WindowContainerTransaction t = new WindowContainerTransaction(); - dispatchOnRotateDisplay(t, displayId, fromRotation, toRotation); + dispatchOnDisplayChange(t, displayId, fromRotation, toRotation, newDisplayAreaInfo); try { - callback.continueRotateDisplay(toRotation, t); + callback.continueDisplayChange(t); } catch (RemoteException e) { - Slog.e(TAG, "Failed to continue rotation", e); + Slog.e(TAG, "Failed to continue handling display change", e); } } @BinderThread - private class DisplayWindowRotationControllerImpl - extends IDisplayWindowRotationController.Stub { + private class DisplayChangeWindowControllerImpl + extends IDisplayChangeWindowController.Stub { @Override - public void onRotateDisplay(int displayId, final int fromRotation, - final int toRotation, IDisplayWindowRotationCallback callback) { - mMainExecutor.execute(() -> { - DisplayChangeController.this.onRotateDisplay(displayId, fromRotation, toRotation, - callback); - }); + public void onDisplayChange(int displayId, int fromRotation, int toRotation, + DisplayAreaInfo newDisplayAreaInfo, IDisplayChangeWindowCallback callback) { + mMainExecutor.execute(() -> DisplayChangeController.this + .onDisplayChange(displayId, fromRotation, toRotation, + newDisplayAreaInfo, callback)); } } /** * Give a listener a chance to queue up configuration changes to execute as part of a - * display rotation. The contents of {@link #onRotateDisplay} must run synchronously. + * display rotation. The contents of {@link #onDisplayChange} must run synchronously. */ @ShellMainThread public interface OnDisplayChangingListener { /** - * Called before the display is rotated. Contents of this method must run synchronously. - * @param displayId Id of display that is rotating. - * @param fromRotation starting rotation of the display. - * @param toRotation target rotation of the display (after rotating). + * Called before the display size has changed. + * Contents of this method must run synchronously. + * @param displayId display id of the display that is under the change + * @param fromRotation rotation before the change + * @param toRotation rotation after the change + * @param newDisplayAreaInfo display area info after applying the update * @param t A task transaction to populate. */ - void onRotateDisplay(int displayId, int fromRotation, int toRotation, - WindowContainerTransaction t); + void onDisplayChange(int displayId, int fromRotation, int toRotation, + @Nullable DisplayAreaInfo newDisplayAreaInfo, WindowContainerTransaction t); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java index 4ba32e93fb3d..f07ea751b044 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java @@ -34,6 +34,7 @@ import androidx.annotation.BinderThread; import com.android.wm.shell.common.DisplayChangeController.OnDisplayChangingListener; import com.android.wm.shell.common.annotations.ShellMainThread; +import com.android.wm.shell.sysui.ShellInit; import java.util.ArrayList; import java.util.List; @@ -57,19 +58,23 @@ public class DisplayController { private final SparseArray<DisplayRecord> mDisplays = new SparseArray<>(); private final ArrayList<OnDisplaysChangedListener> mDisplayChangedListeners = new ArrayList<>(); - public DisplayController(Context context, IWindowManager wmService, + public DisplayController(Context context, IWindowManager wmService, ShellInit shellInit, ShellExecutor mainExecutor) { mMainExecutor = mainExecutor; mContext = context; mWmService = wmService; - mChangeController = new DisplayChangeController(mWmService, mainExecutor); + // TODO: Inject this instead + mChangeController = new DisplayChangeController(mWmService, shellInit, mainExecutor); mDisplayContainerListener = new DisplayWindowListenerImpl(); + // Note, add this after DisplaceChangeController is constructed to ensure that is + // initialized first + shellInit.addInitCallback(this::onInit, this); } /** * Initializes the window listener. */ - public void initialize() { + public void onInit() { try { int[] displayIds = mWmService.registerDisplayWindowListener(mDisplayContainerListener); for (int i = 0; i < displayIds.length; i++) { @@ -156,14 +161,14 @@ public class DisplayController { * Adds a display rotation controller. */ public void addDisplayChangingController(OnDisplayChangingListener controller) { - mChangeController.addRotationListener(controller); + mChangeController.addDisplayChangeListener(controller); } /** * Removes a display rotation controller. */ public void removeDisplayChangingController(OnDisplayChangingListener controller) { - mChangeController.removeRotationListener(controller); + mChangeController.removeDisplayChangeListener(controller); } private void onDisplayAdded(int displayId) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java index 6a2acf438302..2ea43162d225 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 @@ -16,16 +16,23 @@ package com.android.wm.shell.common; +import static android.view.EventLogTags.IMF_IME_REMOTE_ANIM_CANCEL; +import static android.view.EventLogTags.IMF_IME_REMOTE_ANIM_END; +import static android.view.EventLogTags.IMF_IME_REMOTE_ANIM_START; +import static android.view.inputmethod.ImeTracker.DEBUG_IME_VISIBILITY; +import static android.view.inputmethod.ImeTracker.TOKEN_NONE; + import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; import android.annotation.IntDef; -import android.content.Context; +import android.annotation.Nullable; +import android.content.ComponentName; import android.content.res.Configuration; import android.graphics.Point; import android.graphics.Rect; import android.os.RemoteException; -import android.os.ServiceManager; +import android.util.EventLog; import android.util.Slog; import android.util.SparseArray; import android.view.IDisplayWindowInsetsController; @@ -33,18 +40,21 @@ import android.view.IWindowManager; import android.view.InsetsSource; import android.view.InsetsSourceControl; import android.view.InsetsState; -import android.view.InsetsVisibilities; import android.view.Surface; import android.view.SurfaceControl; import android.view.WindowInsets; +import android.view.WindowInsets.Type.InsetsType; import android.view.animation.Interpolator; import android.view.animation.PathInterpolator; +import android.view.inputmethod.ImeTracker; +import android.view.inputmethod.InputMethodManagerGlobal; import androidx.annotation.VisibleForTesting; -import com.android.internal.view.IInputMethodManager; +import com.android.wm.shell.sysui.ShellInit; import java.util.ArrayList; +import java.util.Objects; import java.util.concurrent.Executor; /** @@ -73,18 +83,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); } @@ -106,7 +122,7 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged } if (mDisplayController.getDisplayLayout(displayId).rotation() != pd.mRotation && isImeShowing(displayId)) { - pd.startAnimation(true, false /* forceRestart */); + pd.startAnimation(true, false /* forceRestart */, null /* statsToken */); } } @@ -125,7 +141,7 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged if (pd == null) { return false; } - final InsetsSource imeSource = pd.mInsetsState.getSource(InsetsState.ITYPE_IME); + final InsetsSource imeSource = pd.mInsetsState.peekSource(InsetsSource.ID_IME); return imeSource != null && pd.mImeSourceControl != null && imeSource.isVisible(); } @@ -201,7 +217,7 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged public class PerDisplay implements DisplayInsetsController.OnInsetsChangedListener { final int mDisplayId; final InsetsState mInsetsState = new InsetsState(); - final InsetsVisibilities mRequestedVisibilities = new InsetsVisibilities(); + @InsetsType int mRequestedVisibleTypes = WindowInsets.Type.defaultVisible(); InsetsSourceControl mImeSourceControl = null; int mAnimationDirection = DIRECTION_NONE; ValueAnimator mAnimation = null; @@ -229,16 +245,19 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged return; } - updateImeVisibility(insetsState.getSourceOrDefaultVisibility(InsetsState.ITYPE_IME)); + updateImeVisibility(insetsState.isSourceOrDefaultVisible(InsetsSource.ID_IME, + WindowInsets.Type.ime())); - final InsetsSource newSource = insetsState.getSource(InsetsState.ITYPE_IME); - final Rect newFrame = newSource.getFrame(); - final Rect oldFrame = mInsetsState.getSource(InsetsState.ITYPE_IME).getFrame(); + final InsetsSource newSource = insetsState.peekSource(InsetsSource.ID_IME); + final Rect newFrame = newSource != null ? newSource.getFrame() : null; + final boolean newSourceVisible = newSource != null && newSource.isVisible(); + final InsetsSource oldSource = mInsetsState.peekSource(InsetsSource.ID_IME); + final Rect oldFrame = oldSource != null ? oldSource.getFrame() : null; mInsetsState.set(insetsState, true /* copySources */); - if (mImeShowing && !newFrame.equals(oldFrame) && newSource.isVisible()) { + if (mImeShowing && !Objects.equals(oldFrame, newFrame) && newSourceVisible) { if (DEBUG) Slog.d(TAG, "insetsChanged when IME showing, restart animation"); - startAnimation(mImeShowing, true /* forceRestart */); + startAnimation(mImeShowing, true /* forceRestart */, null /* statsToken */); } } @@ -253,7 +272,7 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged if (activeControl == null) { continue; } - if (activeControl.getType() == InsetsState.ITYPE_IME) { + if (activeControl.getType() == WindowInsets.Type.ime()) { imeSourceControl = activeControl; } } @@ -266,29 +285,30 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged } if (hasImeSourceControl) { - final Point lastSurfacePosition = mImeSourceControl != null - ? mImeSourceControl.getSurfacePosition() : null; - final boolean positionChanged = - !imeSourceControl.getSurfacePosition().equals(lastSurfacePosition); - final boolean leashChanged = - !haveSameLeash(mImeSourceControl, imeSourceControl); if (mAnimation != null) { + final Point lastSurfacePosition = hadImeSourceControl + ? mImeSourceControl.getSurfacePosition() : null; + final boolean positionChanged = + !imeSourceControl.getSurfacePosition().equals(lastSurfacePosition); if (positionChanged) { - startAnimation(mImeShowing, true /* forceRestart */); + startAnimation(mImeShowing, true /* forceRestart */, null /* statsToken */); } } else { - if (leashChanged) { + if (!haveSameLeash(mImeSourceControl, imeSourceControl)) { applyVisibilityToLeash(imeSourceControl); } if (!mImeShowing) { removeImeSurface(); } - if (mImeSourceControl != null) { - mImeSourceControl.release(SurfaceControl::release); - } } - mImeSourceControl = imeSourceControl; + } else if (mAnimation != null) { + mAnimation.cancel(); + } + + if (hadImeSourceControl && mImeSourceControl != imeSourceControl) { + mImeSourceControl.release(SurfaceControl::release); } + mImeSourceControl = imeSourceControl; } private void applyVisibilityToLeash(InsetsSourceControl imeSourceControl) { @@ -306,26 +326,27 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged } @Override - public void showInsets(int types, boolean fromIme) { + public void showInsets(@InsetsType int types, boolean fromIme, + @Nullable ImeTracker.Token statsToken) { if ((types & WindowInsets.Type.ime()) == 0) { return; } if (DEBUG) Slog.d(TAG, "Got showInsets for ime"); - startAnimation(true /* show */, false /* forceRestart */); + startAnimation(true /* show */, false /* forceRestart */, statsToken); } @Override - public void hideInsets(int types, boolean fromIme) { + public void hideInsets(@InsetsType int types, boolean fromIme, + @Nullable ImeTracker.Token statsToken) { if ((types & WindowInsets.Type.ime()) == 0) { return; } if (DEBUG) Slog.d(TAG, "Got hideInsets for ime"); - startAnimation(false /* show */, false /* forceRestart */); + startAnimation(false /* show */, false /* forceRestart */, statsToken); } @Override - public void topFocusedWindowChanged(String packageName, - InsetsVisibilities requestedVisibilities) { + public void topFocusedWindowChanged(ComponentName component, int requestedVisibleTypes) { // Do nothing } @@ -333,11 +354,13 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged * Sends the local visibility state back to window manager. Needed for legacy adjustForIme. */ private void setVisibleDirectly(boolean visible) { - mInsetsState.getSource(InsetsState.ITYPE_IME).setVisible(visible); - mRequestedVisibilities.setVisibility(InsetsState.ITYPE_IME, visible); + mInsetsState.setSourceVisible(InsetsSource.ID_IME, visible); + mRequestedVisibleTypes = visible + ? mRequestedVisibleTypes | WindowInsets.Type.ime() + : mRequestedVisibleTypes & ~WindowInsets.Type.ime(); try { - mWmService.updateDisplayWindowRequestedVisibilities(mDisplayId, - mRequestedVisibilities); + mWmService.updateDisplayWindowRequestedVisibleTypes(mDisplayId, + mRequestedVisibleTypes); } catch (RemoteException e) { } } @@ -360,9 +383,11 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged .navBarFrameHeight(); } - private void startAnimation(final boolean show, final boolean forceRestart) { - final InsetsSource imeSource = mInsetsState.getSource(InsetsState.ITYPE_IME); + private void startAnimation(final boolean show, final boolean forceRestart, + @Nullable ImeTracker.Token statsToken) { + final InsetsSource imeSource = mInsetsState.peekSource(InsetsSource.ID_IME); if (imeSource == null || mImeSourceControl == null) { + ImeTracker.forLogging().onFailed(statsToken, ImeTracker.PHASE_WM_ANIMATION_CREATE); return; } final Rect newFrame = imeSource.getFrame(); @@ -383,8 +408,10 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged + (mAnimationDirection == DIRECTION_SHOW ? "SHOW" : (mAnimationDirection == DIRECTION_HIDE ? "HIDE" : "NONE"))); } - if (!forceRestart && (mAnimationDirection == DIRECTION_SHOW && show) + if ((!forceRestart && (mAnimationDirection == DIRECTION_SHOW && show)) || (mAnimationDirection == DIRECTION_HIDE && !show)) { + ImeTracker.forLogging().onCancelled( + statsToken, ImeTracker.PHASE_WM_ANIMATION_CREATE); return; } boolean seek = false; @@ -428,8 +455,11 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged mTransactionPool.release(t); }); mAnimation.setInterpolator(INTERPOLATOR); + ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_WM_ANIMATION_CREATE); mAnimation.addListener(new AnimatorListenerAdapter() { private boolean mCancelled = false; + @Nullable + private final ImeTracker.Token mStatsToken = statsToken; @Override public void onAnimationStart(Animator animation) { @@ -448,8 +478,19 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged : 1.f; t.setAlpha(mImeSourceControl.getLeash(), alpha); if (mAnimationDirection == DIRECTION_SHOW) { + ImeTracker.forLogging().onProgress(mStatsToken, + ImeTracker.PHASE_WM_ANIMATION_RUNNING); t.show(mImeSourceControl.getLeash()); } + if (DEBUG_IME_VISIBILITY) { + EventLog.writeEvent(IMF_IME_REMOTE_ANIM_START, + statsToken != null ? statsToken.getTag() : TOKEN_NONE, + mDisplayId, mAnimationDirection, alpha, startY , endY, + Objects.toString(mImeSourceControl.getLeash()), + Objects.toString(mImeSourceControl.getInsetsHint()), + Objects.toString(mImeSourceControl.getSurfacePosition()), + Objects.toString(mImeFrame)); + } t.apply(); mTransactionPool.release(t); } @@ -457,6 +498,11 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged @Override public void onAnimationCancel(Animator animation) { mCancelled = true; + if (DEBUG_IME_VISIBILITY) { + EventLog.writeEvent(IMF_IME_REMOTE_ANIM_CANCEL, + statsToken != null ? statsToken.getTag() : TOKEN_NONE, mDisplayId, + Objects.toString(mImeSourceControl.getInsetsHint())); + } } @Override @@ -469,8 +515,25 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged } dispatchEndPositioning(mDisplayId, mCancelled, t); if (mAnimationDirection == DIRECTION_HIDE && !mCancelled) { + ImeTracker.forLogging().onProgress(mStatsToken, + ImeTracker.PHASE_WM_ANIMATION_RUNNING); t.hide(mImeSourceControl.getLeash()); removeImeSurface(); + ImeTracker.forLogging().onHidden(mStatsToken); + } else if (mAnimationDirection == DIRECTION_SHOW && !mCancelled) { + ImeTracker.forLogging().onShown(mStatsToken); + } else if (mCancelled) { + ImeTracker.forLogging().onCancelled(mStatsToken, + ImeTracker.PHASE_WM_ANIMATION_RUNNING); + } + if (DEBUG_IME_VISIBILITY) { + EventLog.writeEvent(IMF_IME_REMOTE_ANIM_END, + statsToken != null ? statsToken.getTag() : TOKEN_NONE, + mDisplayId, mAnimationDirection, endY, + Objects.toString(mImeSourceControl.getLeash()), + Objects.toString(mImeSourceControl.getInsetsHint()), + Objects.toString(mImeSourceControl.getSurfacePosition()), + Objects.toString(mImeFrame)); } t.apply(); mTransactionPool.release(t); @@ -506,16 +569,10 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged } void removeImeSurface() { - final IInputMethodManager imms = getImms(); - if (imms != null) { - try { - // Remove the IME surface to make the insets invisible for - // non-client controlled insets. - imms.removeImeSurface(); - } catch (RemoteException e) { - Slog.e(TAG, "Failed to remove IME surface.", e); - } - } + // Remove the IME surface to make the insets invisible for + // non-client controlled insets. + InputMethodManagerGlobal.removeImeSurface( + e -> Slog.e(TAG, "Failed to remove IME surface.", e)); } /** @@ -589,11 +646,6 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged } } - public IInputMethodManager getImms() { - return IInputMethodManager.Stub.asInterface( - ServiceManager.getService(Context.INPUT_METHOD_SERVICE)); - } - private static boolean haveSameLeash(InsetsSourceControl a, InsetsSourceControl b) { if (a == b) { return true; 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..9bdda14cf00b 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,8 @@ package com.android.wm.shell.common; +import android.annotation.Nullable; +import android.content.ComponentName; import android.os.RemoteException; import android.util.Slog; import android.util.SparseArray; @@ -23,11 +25,13 @@ import android.view.IDisplayWindowInsetsController; import android.view.IWindowManager; import android.view.InsetsSourceControl; import android.view.InsetsState; -import android.view.InsetsVisibilities; +import android.view.WindowInsets.Type.InsetsType; +import android.view.inputmethod.ImeTracker; 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 +48,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); } @@ -151,34 +158,44 @@ public class DisplayInsetsController implements DisplayController.OnDisplaysChan } } - private void showInsets(int types, boolean fromIme) { + private void showInsets(@InsetsType int types, boolean fromIme, + @Nullable ImeTracker.Token statsToken) { CopyOnWriteArrayList<OnInsetsChangedListener> listeners = mListeners.get(mDisplayId); if (listeners == null) { + ImeTracker.forLogging().onFailed( + statsToken, ImeTracker.PHASE_WM_REMOTE_INSETS_CONTROLLER); return; } + ImeTracker.forLogging().onProgress( + statsToken, ImeTracker.PHASE_WM_REMOTE_INSETS_CONTROLLER); for (OnInsetsChangedListener listener : listeners) { - listener.showInsets(types, fromIme); + listener.showInsets(types, fromIme, statsToken); } } - private void hideInsets(int types, boolean fromIme) { + private void hideInsets(@InsetsType int types, boolean fromIme, + @Nullable ImeTracker.Token statsToken) { CopyOnWriteArrayList<OnInsetsChangedListener> listeners = mListeners.get(mDisplayId); if (listeners == null) { + ImeTracker.forLogging().onFailed( + statsToken, ImeTracker.PHASE_WM_REMOTE_INSETS_CONTROLLER); return; } + ImeTracker.forLogging().onProgress( + statsToken, ImeTracker.PHASE_WM_REMOTE_INSETS_CONTROLLER); for (OnInsetsChangedListener listener : listeners) { - listener.hideInsets(types, fromIme); + listener.hideInsets(types, fromIme, statsToken); } } - private void topFocusedWindowChanged(String packageName, - InsetsVisibilities requestedVisibilities) { + private void topFocusedWindowChanged(ComponentName component, + @InsetsType int requestedVisibleTypes) { CopyOnWriteArrayList<OnInsetsChangedListener> listeners = mListeners.get(mDisplayId); if (listeners == null) { return; } for (OnInsetsChangedListener listener : listeners) { - listener.topFocusedWindowChanged(packageName, requestedVisibilities); + listener.topFocusedWindowChanged(component, requestedVisibleTypes); } } @@ -186,10 +203,10 @@ public class DisplayInsetsController implements DisplayController.OnDisplaysChan private class DisplayWindowInsetsControllerImpl extends IDisplayWindowInsetsController.Stub { @Override - public void topFocusedWindowChanged(String packageName, - InsetsVisibilities requestedVisibilities) throws RemoteException { + public void topFocusedWindowChanged(ComponentName component, + @InsetsType int requestedVisibleTypes) throws RemoteException { mMainExecutor.execute(() -> { - PerDisplay.this.topFocusedWindowChanged(packageName, requestedVisibilities); + PerDisplay.this.topFocusedWindowChanged(component, requestedVisibleTypes); }); } @@ -209,16 +226,18 @@ public class DisplayInsetsController implements DisplayController.OnDisplaysChan } @Override - public void showInsets(int types, boolean fromIme) throws RemoteException { + public void showInsets(@InsetsType int types, boolean fromIme, + @Nullable ImeTracker.Token statsToken) throws RemoteException { mMainExecutor.execute(() -> { - PerDisplay.this.showInsets(types, fromIme); + PerDisplay.this.showInsets(types, fromIme, statsToken); }); } @Override - public void hideInsets(int types, boolean fromIme) throws RemoteException { + public void hideInsets(@InsetsType int types, boolean fromIme, + @Nullable ImeTracker.Token statsToken) throws RemoteException { mMainExecutor.execute(() -> { - PerDisplay.this.hideInsets(types, fromIme); + PerDisplay.this.hideInsets(types, fromIme, statsToken); }); } } @@ -234,11 +253,13 @@ 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 requestedVisibilities The insets visibilities requested by the focussed window. + * + * @param component The application component that is open in the top focussed window. + * @param requestedVisibleTypes The {@link InsetsType} requested visible by the focused + * window. */ - default void topFocusedWindowChanged(String packageName, - InsetsVisibilities requestedVisibilities) {} + default void topFocusedWindowChanged(ComponentName component, + @InsetsType int requestedVisibleTypes) {} /** * Called when the window insets configuration has changed. @@ -254,17 +275,23 @@ public class DisplayInsetsController implements DisplayController.OnDisplaysChan /** * Called when a set of insets source window should be shown by policy. * - * @param types internal insets types (WindowInsets.Type.InsetsType) to show + * @param types {@link InsetsType} to show * @param fromIme true if this request originated from IME (InputMethodService). + * @param statsToken the token tracking the current IME show request + * or {@code null} otherwise. */ - default void showInsets(int types, boolean fromIme) {} + default void showInsets(@InsetsType int types, boolean fromIme, + @Nullable ImeTracker.Token statsToken) {} /** * Called when a set of insets source window should be hidden by policy. * - * @param types internal insets types (WindowInsets.Type.InsetsType) to hide + * @param types {@link InsetsType} to hide * @param fromIme true if this request originated from IME (InputMethodService). + * @param statsToken the token tracking the current IME hide request + * or {@code null} otherwise. */ - default void hideInsets(int types, boolean fromIme) {} + default void hideInsets(@InsetsType int types, boolean fromIme, + @Nullable ImeTracker.Token statsToken) {} } -}
\ No newline at end of file +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayLayout.java index 47f1e2e18255..d6e1a82a68ff 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayLayout.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayLayout.java @@ -25,7 +25,6 @@ import static android.provider.Settings.Global.DEVELOPMENT_FORCE_DESKTOP_MODE_ON import static android.util.RotationUtils.rotateBounds; import static android.util.RotationUtils.rotateInsets; import static android.view.Display.FLAG_SHOULD_SHOW_SYSTEM_DECORATIONS; -import static android.view.InsetsState.ITYPE_EXTRA_NAVIGATION_BAR; import static android.view.Surface.ROTATION_0; import static android.view.Surface.ROTATION_270; import static android.view.Surface.ROTATION_90; @@ -46,9 +45,9 @@ import android.view.Display; import android.view.DisplayCutout; import android.view.DisplayInfo; import android.view.Gravity; -import android.view.InsetsSource; import android.view.InsetsState; import android.view.Surface; +import android.view.WindowInsets; import androidx.annotation.VisibleForTesting; @@ -96,7 +95,7 @@ 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. * @return {@code true} if the given {@link DisplayLayout} is identical geometry wise. */ public boolean isSameGeometry(@NonNull DisplayLayout other) { @@ -372,23 +371,20 @@ public class DisplayLayout { // Only navigation bar if (hasNavigationBar) { - final InsetsSource extraNavBar = insetsState.getSource(ITYPE_EXTRA_NAVIGATION_BAR); - final boolean hasExtraNav = extraNavBar != null && extraNavBar.isVisible(); + final Insets insets = insetsState.calculateInsets( + insetsState.getDisplayFrame(), + WindowInsets.Type.navigationBars(), + false /* ignoreVisibility */); + outInsets.set(insets.left, insets.top, insets.right, insets.bottom); int position = navigationBarPosition(res, displayWidth, displayHeight, displayRotation); int navBarSize = getNavigationBarSize(res, position, displayWidth > displayHeight, uiMode); if (position == NAV_BAR_BOTTOM) { - outInsets.bottom = hasExtraNav - ? Math.max(navBarSize, extraNavBar.getFrame().height()) - : navBarSize; + outInsets.bottom = Math.max(outInsets.bottom , navBarSize); } else if (position == NAV_BAR_RIGHT) { - outInsets.right = hasExtraNav - ? Math.max(navBarSize, extraNavBar.getFrame().width()) - : navBarSize; + outInsets.right = Math.max(outInsets.right , navBarSize); } else if (position == NAV_BAR_LEFT) { - outInsets.left = hasExtraNav - ? Math.max(navBarSize, extraNavBar.getFrame().width()) - : navBarSize; + outInsets.left = Math.max(outInsets.left , navBarSize); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DockStateReader.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DockStateReader.java new file mode 100644 index 000000000000..e029358cb3a2 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DockStateReader.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.common; + +import static android.content.Intent.EXTRA_DOCK_STATE; + +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; + +import com.android.wm.shell.dagger.WMSingleton; + +import javax.inject.Inject; + +/** + * Provides information about the docked state of the device. + */ +@WMSingleton +public class DockStateReader { + + private static final IntentFilter DOCK_INTENT_FILTER = new IntentFilter( + Intent.ACTION_DOCK_EVENT); + + private final Context mContext; + + @Inject + public DockStateReader(Context context) { + mContext = context; + } + + /** + * @return True if the device is docked and false otherwise. + */ + public boolean isDocked() { + Intent dockStatus = mContext.registerReceiver(/* receiver */ null, DOCK_INTENT_FILTER); + if (dockStatus != null) { + int dockState = dockStatus.getIntExtra(EXTRA_DOCK_STATE, + Intent.EXTRA_DOCK_STATE_UNDOCKED); + return dockState != Intent.EXTRA_DOCK_STATE_UNDOCKED; + } + return false; + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/ISplitScreenListener.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/common/ExternalInterfaceBinder.java index 46e4299f99fa..aa5b0cb628e1 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/ISplitScreenListener.aidl +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/ExternalInterfaceBinder.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021 The Android Open Source Project + * Copyright (C) 2022 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,20 +14,21 @@ * limitations under the License. */ -package com.android.wm.shell.stagesplit; +package com.android.wm.shell.common; + +import android.os.IBinder; /** - * Listener interface that Launcher attaches to SystemUI to get split-screen callbacks. + * An interface for binders which can be registered to be sent to other processes. */ -oneway interface ISplitScreenListener { - +public interface ExternalInterfaceBinder { /** - * Called when the stage position changes. + * Invalidates this binder (detaches it from the controller it would call). */ - void onStagePositionChanged(int stage, int position); + void invalidate(); /** - * Called when a task changes stages. + * Returns the IBinder to send. */ - void onTaskStageChanged(int taskId, int stage, boolean visible); -}
\ No newline at end of file + IBinder asBinder(); +} 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/SingleInstanceRemoteListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/SingleInstanceRemoteListener.java index b77ac8a2b951..e46ee28b3ddb 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/SingleInstanceRemoteListener.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/SingleInstanceRemoteListener.java @@ -29,6 +29,9 @@ import java.util.function.Consumer; * Manages the lifecycle of a single instance of a remote listener, including the clean up if the * remote process dies. All calls on this class should happen on the main shell thread. * + * Any external interface using this listener should also unregister the listener when it is + * invalidated, otherwise it may leak binder death recipients. + * * @param <C> The controller (must be RemoteCallable) * @param <L> The remote listener interface type */ 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..5e42782431fd 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 @@ -19,6 +19,7 @@ package com.android.wm.shell.common; import static android.view.WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED; import android.annotation.NonNull; +import android.annotation.Nullable; import android.content.Context; import android.content.res.Configuration; import android.graphics.Region; @@ -46,6 +47,7 @@ import android.view.View; import android.view.ViewGroup; import android.view.WindowManager; import android.view.WindowlessWindowManager; +import android.view.inputmethod.ImeTracker; import android.window.ClientWindowFrames; import com.android.internal.os.IResultReceiver; @@ -221,8 +223,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, "SystemWindows"); attrs.flags |= FLAG_HARDWARE_ACCELERATED; viewRoot.setView(view, attrs); mViewRoots.put(view, viewRoot); @@ -305,7 +306,9 @@ public class SystemWindows { } } - protected void attachToParentSurface(IWindow window, SurfaceControl.Builder b) { + @Override + protected SurfaceControl getParentSurface(IWindow window, + WindowManager.LayoutParams attrs) { SurfaceControl leash = new SurfaceControl.Builder(new SurfaceSession()) .setContainerLayer() .setName("SystemWindowLeash") @@ -315,7 +318,7 @@ public class SystemWindows { synchronized (this) { mLeashForWindow.put(window.asBinder(), leash); } - b.setParent(leash); + return leash; } @Override @@ -345,17 +348,17 @@ public class SystemWindows { public void resized(ClientWindowFrames frames, boolean reportDraw, MergedConfiguration newMergedConfiguration, InsetsState insetsState, boolean forceLayout, boolean alwaysConsumeSystemBars, int displayId, int syncSeqId, - int resizeMode) {} + boolean dragResizing) {} @Override public void insetsControlChanged(InsetsState insetsState, InsetsSourceControl[] activeControls) {} @Override - public void showInsets(int types, boolean fromIme) {} + public void showInsets(int types, boolean fromIme, @Nullable ImeTracker.Token statsToken) {} @Override - public void hideInsets(int types, boolean fromIme) {} + public void hideInsets(int types, boolean fromIme, @Nullable ImeTracker.Token statsToken) {} @Override public void moved(int newX, int newY) {} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/TabletopModeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/TabletopModeController.java new file mode 100644 index 000000000000..ac6e4c2a6521 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/TabletopModeController.java @@ -0,0 +1,254 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.common; + +import static android.view.Display.DEFAULT_DISPLAY; + +import static com.android.wm.shell.common.DevicePostureController.DEVICE_POSTURE_HALF_OPENED; +import static com.android.wm.shell.common.DevicePostureController.DEVICE_POSTURE_UNKNOWN; +import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_FOLDABLE; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.app.WindowConfiguration; +import android.content.Context; +import android.content.res.Configuration; +import android.os.SystemProperties; +import android.util.ArraySet; +import android.view.Surface; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.protolog.common.ProtoLog; +import com.android.wm.shell.common.annotations.ShellMainThread; +import com.android.wm.shell.sysui.ShellInit; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +/** + * Wrapper class to track the tabletop (aka. flex) mode change on Fold-ables. + * See also <a + * href="https://developer.android.com/guide/topics/large-screens/learn-about-foldables + * #foldable_postures">Foldable states and postures</a> for reference. + * + * Use the {@link DevicePostureController} for more detailed posture changes. + */ +public class TabletopModeController implements + DevicePostureController.OnDevicePostureChangedListener, + DisplayController.OnDisplaysChangedListener { + /** + * When {@code true}, floating windows like PiP would auto move to the position + * specified by {@link #PREFER_TOP_HALF_IN_TABLETOP} when in tabletop mode. + */ + private static final boolean ENABLE_MOVE_FLOATING_WINDOW_IN_TABLETOP = + SystemProperties.getBoolean( + "persist.wm.debug.enable_move_floating_window_in_tabletop", true); + + /** + * Prefer the {@link #PREFERRED_TABLETOP_HALF_TOP} if this flag is enabled, + * {@link #PREFERRED_TABLETOP_HALF_BOTTOM} otherwise. + * See also {@link #getPreferredHalfInTabletopMode()}. + */ + private static final boolean PREFER_TOP_HALF_IN_TABLETOP = + SystemProperties.getBoolean("persist.wm.debug.prefer_top_half_in_tabletop", true); + + private static final long TABLETOP_MODE_DELAY_MILLIS = 1_000; + + @IntDef(prefix = {"PREFERRED_TABLETOP_HALF_"}, value = { + PREFERRED_TABLETOP_HALF_TOP, + PREFERRED_TABLETOP_HALF_BOTTOM + }) + @Retention(RetentionPolicy.SOURCE) + public @interface PreferredTabletopHalf {} + + public static final int PREFERRED_TABLETOP_HALF_TOP = 0; + public static final int PREFERRED_TABLETOP_HALF_BOTTOM = 1; + + private final Context mContext; + + private final DevicePostureController mDevicePostureController; + + private final DisplayController mDisplayController; + + private final ShellExecutor mMainExecutor; + + private final Set<Integer> mTabletopModeRotations = new ArraySet<>(); + + private final List<OnTabletopModeChangedListener> mListeners = new ArrayList<>(); + + @VisibleForTesting + final Runnable mOnEnterTabletopModeCallback = () -> { + if (isInTabletopMode()) { + // We are still in tabletop mode, go ahead. + mayBroadcastOnTabletopModeChange(true /* isInTabletopMode */); + } + }; + + @DevicePostureController.DevicePostureInt + private int mDevicePosture = DEVICE_POSTURE_UNKNOWN; + + @Surface.Rotation + private int mDisplayRotation = WindowConfiguration.ROTATION_UNDEFINED; + + /** + * Track the last callback value for {@link OnTabletopModeChangedListener}. + * This is to avoid duplicated {@code false} callback to {@link #mListeners}. + */ + private Boolean mLastIsInTabletopModeForCallback; + + public TabletopModeController(Context context, + ShellInit shellInit, + DevicePostureController postureController, + DisplayController displayController, + @ShellMainThread ShellExecutor mainExecutor) { + mContext = context; + mDevicePostureController = postureController; + mDisplayController = displayController; + mMainExecutor = mainExecutor; + shellInit.addInitCallback(this::onInit, this); + } + + @VisibleForTesting + void onInit() { + mDevicePostureController.registerOnDevicePostureChangedListener(this); + mDisplayController.addDisplayWindowListener(this); + // Aligns with what's in {@link com.android.server.wm.DisplayRotation}. + final int[] deviceTabletopRotations = mContext.getResources().getIntArray( + com.android.internal.R.array.config_deviceTabletopRotations); + if (deviceTabletopRotations == null || deviceTabletopRotations.length == 0) { + ProtoLog.e(WM_SHELL_FOLDABLE, + "No valid config_deviceTabletopRotations, can not tell" + + " tabletop mode in WMShell"); + return; + } + for (int angle : deviceTabletopRotations) { + switch (angle) { + case 0: + mTabletopModeRotations.add(Surface.ROTATION_0); + break; + case 90: + mTabletopModeRotations.add(Surface.ROTATION_90); + break; + case 180: + mTabletopModeRotations.add(Surface.ROTATION_180); + break; + case 270: + mTabletopModeRotations.add(Surface.ROTATION_270); + break; + default: + ProtoLog.e(WM_SHELL_FOLDABLE, + "Invalid surface rotation angle in " + + "config_deviceTabletopRotations: %d", + angle); + break; + } + } + } + + /** + * @return {@code true} if floating windows like PiP would auto move to the position + * specified by {@link #getPreferredHalfInTabletopMode()} when in tabletop mode. + */ + public boolean enableMoveFloatingWindowInTabletop() { + return ENABLE_MOVE_FLOATING_WINDOW_IN_TABLETOP; + } + + /** @return Preferred half for floating windows like PiP when in tabletop mode. */ + @PreferredTabletopHalf + public int getPreferredHalfInTabletopMode() { + return PREFER_TOP_HALF_IN_TABLETOP + ? PREFERRED_TABLETOP_HALF_TOP + : PREFERRED_TABLETOP_HALF_BOTTOM; + } + + /** Register {@link OnTabletopModeChangedListener} to listen for tabletop mode change. */ + public void registerOnTabletopModeChangedListener( + @NonNull OnTabletopModeChangedListener listener) { + if (listener == null || mListeners.contains(listener)) return; + mListeners.add(listener); + listener.onTabletopModeChanged(isInTabletopMode()); + } + + /** Unregister {@link OnTabletopModeChangedListener} for tabletop mode change. */ + public void unregisterOnTabletopModeChangedListener( + @NonNull OnTabletopModeChangedListener listener) { + mListeners.remove(listener); + } + + @Override + public void onDevicePostureChanged(@DevicePostureController.DevicePostureInt int posture) { + if (mDevicePosture != posture) { + onDevicePostureOrDisplayRotationChanged(posture, mDisplayRotation); + } + } + + @Override + public void onDisplayConfigurationChanged(int displayId, Configuration newConfig) { + final int newDisplayRotation = newConfig.windowConfiguration.getDisplayRotation(); + if (displayId == DEFAULT_DISPLAY && newDisplayRotation != mDisplayRotation) { + onDevicePostureOrDisplayRotationChanged(mDevicePosture, newDisplayRotation); + } + } + + private void onDevicePostureOrDisplayRotationChanged( + @DevicePostureController.DevicePostureInt int newPosture, + @Surface.Rotation int newDisplayRotation) { + final boolean wasInTabletopMode = isInTabletopMode(); + mDevicePosture = newPosture; + mDisplayRotation = newDisplayRotation; + final boolean couldBeInTabletopMode = isInTabletopMode(); + mMainExecutor.removeCallbacks(mOnEnterTabletopModeCallback); + if (!wasInTabletopMode && couldBeInTabletopMode) { + // May enter tabletop mode, but we need to wait for additional time since this + // could be an intermediate state. + mMainExecutor.executeDelayed(mOnEnterTabletopModeCallback, TABLETOP_MODE_DELAY_MILLIS); + } else { + // Cancel entering tabletop mode if any condition's changed. + mayBroadcastOnTabletopModeChange(false /* isInTabletopMode */); + } + } + + private boolean isHalfOpened(@DevicePostureController.DevicePostureInt int posture) { + return posture == DEVICE_POSTURE_HALF_OPENED; + } + + private boolean isInTabletopMode() { + return isHalfOpened(mDevicePosture) && mTabletopModeRotations.contains(mDisplayRotation); + } + + private void mayBroadcastOnTabletopModeChange(boolean isInTabletopMode) { + if (mLastIsInTabletopModeForCallback == null + || mLastIsInTabletopModeForCallback != isInTabletopMode) { + mListeners.forEach(l -> l.onTabletopModeChanged(isInTabletopMode)); + mLastIsInTabletopModeForCallback = isInTabletopMode; + } + } + + /** + * Listener interface for tabletop mode change. + */ + public interface OnTabletopModeChangedListener { + /** + * Callback when tabletop mode changes. Expect duplicated callbacks with {@code false}. + * @param isInTabletopMode {@code true} if enters tabletop mode, {@code false} otherwise. + */ + void onTabletopModeChanged(boolean isInTabletopMode); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/TaskStackListenerImpl.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/TaskStackListenerImpl.java index 9e0a48b13413..e2106e478bb3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/TaskStackListenerImpl.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/TaskStackListenerImpl.java @@ -216,7 +216,6 @@ public class TaskStackListenerImpl extends TaskStackListener implements Handler. args.argi1 = homeTaskVisible ? 1 : 0; args.argi2 = clearedTask ? 1 : 0; args.argi3 = wasVisible ? 1 : 0; - mMainHandler.removeMessages(ON_ACTIVITY_RESTART_ATTEMPT); mMainHandler.obtainMessage(ON_ACTIVITY_RESTART_ATTEMPT, args).sendToTarget(); } 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..931cf0cee28c 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,11 +14,13 @@ * 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; import android.graphics.drawable.Drawable; +import android.graphics.drawable.Icon; +import android.os.Handler; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.View; @@ -28,36 +30,34 @@ 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 { private final ImageView mIconImageView; private final View mButtonBackgroundView; - private final View mButtonView; - private OnClickListener mOnClickListener; - public TvPipMenuActionButton(Context context) { + private Icon mCurrentIcon; + + 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); mButtonBackgroundView = findViewById(R.id.background); final int[] values = new int[]{android.R.attr.src, android.R.attr.text}; @@ -70,23 +70,8 @@ public class TvPipMenuActionButton extends RelativeLayout implements View.OnClic setTextAndDescription(textResId); } typedArray.recycle(); - } - @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" - // listener to the ImageView. - mOnClickListener = listener; - mButtonView.setOnClickListener(listener != null ? this : null); - } - - @Override - public void onClick(View v) { - if (mOnClickListener != null) { - // Pass the correct view - this. - mOnClickListener.onClick(this); - } + setIsCustomCloseAction(false); } /** @@ -105,11 +90,24 @@ public class TvPipMenuActionButton extends RelativeLayout implements View.OnClic } } + public void setImageIconAsync(Icon icon, Handler handler) { + mCurrentIcon = icon; + // Remove old image while waiting for the new one to load. + mIconImageView.setImageDrawable(null); + icon.loadDrawableAsync(mContext, d -> { + // The image hasn't been set any other way and the drawable belongs to the most + // recently set Icon. + if (mIconImageView.getDrawable() == null && mCurrentIcon == icon) { + mIconImageView.setImageDrawable(d); + } + }, handler); + } + /** * Sets the text for description the with the given string. */ public void setTextAndDescription(CharSequence text) { - mButtonView.setContentDescription(text); + setContentDescription(text); } /** @@ -119,32 +117,29 @@ public class TvPipMenuActionButton extends RelativeLayout implements View.OnClic setTextAndDescription(getContext().getString(resId)); } - @Override - public void setEnabled(boolean enabled) { - mButtonView.setEnabled(enabled); - } - - @Override - public boolean isEnabled() { - 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(); + if (getContentDescription() == null) { + return TvWindowMenuActionButton.class.getSimpleName(); } - return mButtonView.getContentDescription().toString(); + return getContentDescription().toString(); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleBarUpdate.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleBarUpdate.java new file mode 100644 index 000000000000..81423473171d --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleBarUpdate.java @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.common.bubbles; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.Parcel; +import android.os.Parcelable; + +import java.util.ArrayList; +import java.util.List; + +/** + * Represents an update to bubbles state. This is passed through + * {@link com.android.wm.shell.bubbles.IBubblesListener} to launcher so that taskbar may render + * bubbles. This should be kept this as minimal as possible in terms of data. + */ +public class BubbleBarUpdate implements Parcelable { + + public static final String BUNDLE_KEY = "update"; + + public boolean expandedChanged; + public boolean expanded; + @Nullable + public String selectedBubbleKey; + @Nullable + public BubbleInfo addedBubble; + @Nullable + public BubbleInfo updatedBubble; + @Nullable + public String suppressedBubbleKey; + @Nullable + public String unsupressedBubbleKey; + + // This is only populated if bubbles have been removed. + public List<RemovedBubble> removedBubbles = new ArrayList<>(); + + // This is only populated if the order of the bubbles has changed. + public List<String> bubbleKeysInOrder = new ArrayList<>(); + + // This is only populated the first time a listener is connected so it gets the current state. + public List<BubbleInfo> currentBubbleList = new ArrayList<>(); + + public BubbleBarUpdate() { + } + + public BubbleBarUpdate(Parcel parcel) { + expandedChanged = parcel.readBoolean(); + expanded = parcel.readBoolean(); + selectedBubbleKey = parcel.readString(); + addedBubble = parcel.readParcelable(BubbleInfo.class.getClassLoader(), + BubbleInfo.class); + updatedBubble = parcel.readParcelable(BubbleInfo.class.getClassLoader(), + BubbleInfo.class); + suppressedBubbleKey = parcel.readString(); + unsupressedBubbleKey = parcel.readString(); + removedBubbles = parcel.readParcelableList(new ArrayList<>(), + RemovedBubble.class.getClassLoader()); + parcel.readStringList(bubbleKeysInOrder); + currentBubbleList = parcel.readParcelableList(new ArrayList<>(), + BubbleInfo.class.getClassLoader()); + } + + /** + * Returns whether anything has changed in this update. + */ + public boolean anythingChanged() { + return expandedChanged + || selectedBubbleKey != null + || addedBubble != null + || updatedBubble != null + || !removedBubbles.isEmpty() + || !bubbleKeysInOrder.isEmpty() + || suppressedBubbleKey != null + || unsupressedBubbleKey != null + || !currentBubbleList.isEmpty(); + } + + @Override + public String toString() { + return "BubbleBarUpdate{ expandedChanged=" + expandedChanged + + " expanded=" + expanded + + " selectedBubbleKey=" + selectedBubbleKey + + " addedBubble=" + addedBubble + + " updatedBubble=" + updatedBubble + + " suppressedBubbleKey=" + suppressedBubbleKey + + " unsuppressedBubbleKey=" + unsupressedBubbleKey + + " removedBubbles=" + removedBubbles + + " bubbles=" + bubbleKeysInOrder + + " currentBubbleList=" + currentBubbleList + + " }"; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel parcel, int flags) { + parcel.writeBoolean(expandedChanged); + parcel.writeBoolean(expanded); + parcel.writeString(selectedBubbleKey); + parcel.writeParcelable(addedBubble, flags); + parcel.writeParcelable(updatedBubble, flags); + parcel.writeString(suppressedBubbleKey); + parcel.writeString(unsupressedBubbleKey); + parcel.writeParcelableList(removedBubbles, flags); + parcel.writeStringList(bubbleKeysInOrder); + parcel.writeParcelableList(currentBubbleList, flags); + } + + @NonNull + public static final Creator<BubbleBarUpdate> CREATOR = + new Creator<BubbleBarUpdate>() { + public BubbleBarUpdate createFromParcel(Parcel source) { + return new BubbleBarUpdate(source); + } + public BubbleBarUpdate[] newArray(int size) { + return new BubbleBarUpdate[size]; + } + }; +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleInfo.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleInfo.java new file mode 100644 index 000000000000..b0dea7231a1e --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleInfo.java @@ -0,0 +1,154 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.common.bubbles; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.Notification; +import android.graphics.drawable.Icon; +import android.os.Parcel; +import android.os.Parcelable; + +import java.util.Objects; + +/** + * Contains information necessary to present a bubble. + */ +public class BubbleInfo implements Parcelable { + + // TODO(b/269672147): needs a title string for a11y & that comes from notification + // TODO(b/269671451): needs whether the bubble is an 'important person' or not + + private String mKey; // Same key as the Notification + private int mFlags; // Flags from BubbleMetadata + private String mShortcutId; + private int mUserId; + private String mPackageName; + /** + * All notification bubbles require a shortcut to be set on the notification, however, the + * app could still specify an Icon and PendingIntent to use for the bubble. In that case + * this icon will be populated. If the bubble is entirely shortcut based, this will be null. + */ + @Nullable + private Icon mIcon; + + public BubbleInfo(String key, int flags, String shortcutId, @Nullable Icon icon, + int userId, String packageName) { + mKey = key; + mFlags = flags; + mShortcutId = shortcutId; + mIcon = icon; + mUserId = userId; + mPackageName = packageName; + } + + public BubbleInfo(Parcel source) { + mKey = source.readString(); + mFlags = source.readInt(); + mShortcutId = source.readString(); + mIcon = source.readTypedObject(Icon.CREATOR); + mUserId = source.readInt(); + mPackageName = source.readString(); + } + + public String getKey() { + return mKey; + } + + public String getShortcutId() { + return mShortcutId; + } + + public Icon getIcon() { + return mIcon; + } + + public int getFlags() { + return mFlags; + } + + public int getUserId() { + return mUserId; + } + + public String getPackageName() { + return mPackageName; + } + + /** + * Whether this bubble is currently being hidden from the stack. + */ + public boolean isBubbleSuppressed() { + return (mFlags & Notification.BubbleMetadata.FLAG_SUPPRESS_BUBBLE) != 0; + } + + /** + * Whether this bubble is able to be suppressed (i.e. has the developer opted into the API + * to + * hide the bubble when in the same content). + */ + public boolean isBubbleSuppressable() { + return (mFlags & Notification.BubbleMetadata.FLAG_SUPPRESSABLE_BUBBLE) != 0; + } + + /** + * Whether the notification for this bubble is hidden from the shade. + */ + public boolean isNotificationSuppressed() { + return (mFlags & Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION) != 0; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof BubbleInfo)) return false; + BubbleInfo bubble = (BubbleInfo) o; + return Objects.equals(mKey, bubble.mKey); + } + + @Override + public int hashCode() { + return mKey.hashCode(); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel parcel, int flags) { + parcel.writeString(mKey); + parcel.writeInt(mFlags); + parcel.writeString(mShortcutId); + parcel.writeTypedObject(mIcon, flags); + parcel.writeInt(mUserId); + parcel.writeString(mPackageName); + } + + @NonNull + public static final Creator<BubbleInfo> CREATOR = + new Creator<BubbleInfo>() { + public BubbleInfo createFromParcel(Parcel source) { + return new BubbleInfo(source); + } + + public BubbleInfo[] newArray(int size) { + return new BubbleInfo[size]; + } + }; +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/RemovedBubble.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/RemovedBubble.java new file mode 100644 index 000000000000..f90591b84b7e --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/RemovedBubble.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.common.bubbles; + +import android.annotation.NonNull; +import android.os.Parcel; +import android.os.Parcelable; + +/** + * Represents a removed bubble, defining the key and reason the bubble was removed. + */ +public class RemovedBubble implements Parcelable { + + private final String mKey; + private final int mRemovalReason; + + public RemovedBubble(String key, int removalReason) { + mKey = key; + mRemovalReason = removalReason; + } + + public RemovedBubble(Parcel parcel) { + mKey = parcel.readString(); + mRemovalReason = parcel.readInt(); + } + + public String getKey() { + return mKey; + } + + public int getRemovalReason() { + return mRemovalReason; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(mKey); + dest.writeInt(mRemovalReason); + } + + @NonNull + public static final Creator<RemovedBubble> CREATOR = + new Creator<RemovedBubble>() { + public RemovedBubble createFromParcel(Parcel source) { + return new RemovedBubble(source); + } + public RemovedBubble[] newArray(int size) { + return new RemovedBubble[size]; + } + }; +} 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..69f0bad4fb45 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 @@ -37,6 +37,7 @@ import android.view.VelocityTracker; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; +import android.view.WindowInsets; import android.view.WindowManager; import android.view.accessibility.AccessibilityNodeInfo; import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; @@ -46,8 +47,10 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.internal.policy.DividerSnapAlgorithm; +import com.android.internal.protolog.common.ProtoLog; import com.android.wm.shell.R; import com.android.wm.shell.animation.Interpolators; +import com.android.wm.shell.protolog.ShellProtoLogGroup; /** * Divider for multi window splits. @@ -56,9 +59,6 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { public static final long TOUCH_ANIMATION_DURATION = 150; public static final long TOUCH_RELEASE_ANIMATION_DURATION = 200; - /** The task bar expanded height. Used to determine whether to insets divider bounds or not. */ - private float mExpandedTaskBarHeight; - private final int mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); private SplitLayout mSplitLayout; @@ -74,6 +74,7 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { private GestureDetector mDoubleTapDetector; private boolean mInteractive; private boolean mSetTouchRegion = true; + private int mLastDraggingPosition; /** * Tracks divider bar visible bounds in screen-based coordination. Used to calculate with @@ -206,18 +207,25 @@ 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()); - final InsetsSource taskBarInsetsSource = - insetsState.getSource(InsetsState.ITYPE_EXTRA_NAVIGATION_BAR); + mSplitLayout.getDividerBounds(mTempRect); // Only insets the divider bar with task bar when it's expanded so that the rounded corners // will be drawn against task bar. - if (taskBarInsetsSource.getFrame().height() >= mExpandedTaskBarHeight) { - mTempRect.inset(taskBarInsetsSource.calculateVisibleInsets(mTempRect)); + // But there is no need to do it when IME showing because there are no rounded corners at + // the bottom. This also avoids the problem of task bar height not changing when IME + // floating. + if (!insetsState.isSourceOrDefaultVisible(InsetsSource.ID_IME, WindowInsets.Type.ime())) { + for (int i = insetsState.sourceSize() - 1; i >= 0; i--) { + final InsetsSource source = insetsState.sourceAt(i); + if (source.getType() == WindowInsets.Type.navigationBars() + && source.insetsRoundedCornerFrame()) { + mTempRect.inset(source.calculateVisibleInsets(mTempRect)); + } + } } if (!mTempRect.equals(mDividerBounds)) { @@ -242,8 +250,6 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { mDividerBar = findViewById(R.id.divider_bar); mHandle = findViewById(R.id.docked_divider_handle); mBackground = findViewById(R.id.docked_divider_background); - mExpandedTaskBarHeight = getResources().getDimensionPixelSize( - com.android.internal.R.dimen.taskbar_frame_height); mTouchElevation = getResources().getDimensionPixelSize( R.dimen.docked_stack_divider_lift_elevation); mDoubleTapDetector = new GestureDetector(getContext(), new DoubleTapListener()); @@ -286,6 +292,7 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { setTouching(); mStartPos = touchPos; mMoving = false; + mSplitLayout.onStartDragging(); break; case MotionEvent.ACTION_MOVE: mVelocityTracker.addMovement(event); @@ -295,13 +302,17 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { } if (mMoving) { final int position = mSplitLayout.getDividePosition() + touchPos - mStartPos; + mLastDraggingPosition = position; mSplitLayout.updateDivideBounds(position); } break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: releaseTouching(); - if (!mMoving) break; + if (!mMoving) { + mSplitLayout.onDraggingCancelled(); + break; + } mVelocityTracker.addMovement(event); mVelocityTracker.computeCurrentVelocity(1000 /* units */); @@ -360,11 +371,30 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { mViewHost.relayout(lp); } - void setInteractive(boolean interactive) { + /** + * Set divider should interactive to user or not. + * + * @param interactive divider interactive. + * @param hideHandle divider handle hidden or not, only work when interactive is false. + * @param from caller from where. + */ + void setInteractive(boolean interactive, boolean hideHandle, String from) { if (interactive == mInteractive) return; + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, + "Set divider bar %s from %s", interactive ? "interactive" : "non-interactive", + from); mInteractive = interactive; + if (!mInteractive && mMoving) { + final int position = mSplitLayout.getDividePosition(); + mSplitLayout.flingDividePosition( + mLastDraggingPosition, + position, + mSplitLayout.FLING_RESIZE_DURATION, + () -> mSplitLayout.setDividePosition(position, true /* applyLayoutChange */)); + mMoving = false; + } releaseTouching(); - mHandle.setVisibility(mInteractive ? View.VISIBLE : View.INVISIBLE); + mHandle.setVisibility(!mInteractive && hideHandle ? View.INVISIBLE : View.VISIBLE); } private boolean isLandscape() { @@ -379,5 +409,10 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { } return true; } + + @Override + public boolean onDoubleTapEvent(@NonNull MotionEvent e) { + return true; + } } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/OWNERS b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/OWNERS new file mode 100644 index 000000000000..7237d2bde39f --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/OWNERS @@ -0,0 +1,2 @@ +# WM shell sub-modules splitscreen owner +chenghsiuchang@google.com 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..4970fa0cb087 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 @@ -48,6 +48,7 @@ import androidx.annotation.NonNull; import com.android.launcher3.icons.IconProvider; import com.android.wm.shell.R; +import com.android.wm.shell.common.ScreenshotUtils; import com.android.wm.shell.common.SurfaceUtils; /** @@ -56,6 +57,7 @@ import com.android.wm.shell.common.SurfaceUtils; 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,13 +69,21 @@ public class SplitDecorManager extends WindowlessWindowManager { private SurfaceControl mHostLeash; private SurfaceControl mIconLeash; private SurfaceControl mBackgroundLeash; + private SurfaceControl mGapBackgroundLeash; + private SurfaceControl mScreenshot; private boolean mShown; private boolean mIsResizing; - private Rect mBounds = new Rect(); + private final Rect mOldBounds = new Rect(); + private final Rect mResizingBounds = new Rect(); + private final Rect mTempRect = new Rect(); private ValueAnimator mFadeAnimator; + private ValueAnimator mScreenshotAnimator; private int mIconSize; + private int mOffsetX; + private int mOffsetY; + private int mRunningAnimationCount = 0; public SplitDecorManager(Configuration configuration, IconProvider iconProvider, SurfaceSession surfaceSession) { @@ -83,7 +93,7 @@ public class SplitDecorManager extends WindowlessWindowManager { } @Override - protected void attachToParentSurface(IWindow window, SurfaceControl.Builder b) { + protected SurfaceControl getParentSurface(IWindow window, WindowManager.LayoutParams attrs) { // Can't set position for the ViewRootImpl SC directly. Create a leash to manipulate later. final SurfaceControl.Builder builder = new SurfaceControl.Builder(new SurfaceSession()) .setContainerLayer() @@ -92,7 +102,7 @@ public class SplitDecorManager extends WindowlessWindowManager { .setParent(mHostLeash) .setCallsite("SplitDecorManager#attachToParentSurface"); mIconLeash = builder.build(); - b.setParent(mIconLeash); + return mIconLeash; } /** Inflates split decor surface on the root surface. */ @@ -104,7 +114,8 @@ public class SplitDecorManager extends WindowlessWindowManager { context = context.createWindowContext(context.getDisplay(), TYPE_APPLICATION_OVERLAY, null /* options */); mHostLeash = rootLeash; - mViewHost = new SurfaceControlViewHost(context, context.getDisplay(), this); + mViewHost = new SurfaceControlViewHost(context, context.getDisplay(), this, + "SplitDecorManager"); mIconSize = context.getResources().getDimensionPixelSize(R.dimen.split_icon_size); final FrameLayout rootLayout = (FrameLayout) LayoutInflater.from(context) @@ -126,8 +137,17 @@ public class SplitDecorManager extends WindowlessWindowManager { /** Releases the surfaces for split decor. */ public void release(SurfaceControl.Transaction t) { - if (mFadeAnimator != null && mFadeAnimator.isRunning()) { - mFadeAnimator.cancel(); + if (mFadeAnimator != null) { + if (mFadeAnimator.isRunning()) { + mFadeAnimator.cancel(); + } + mFadeAnimator = null; + } + if (mScreenshotAnimator != null) { + if (mScreenshotAnimator.isRunning()) { + mScreenshotAnimator.cancel(); + } + mScreenshotAnimator = null; } if (mViewHost != null) { mViewHost.release(); @@ -141,29 +161,39 @@ public class SplitDecorManager extends WindowlessWindowManager { t.remove(mBackgroundLeash); mBackgroundLeash = null; } + if (mGapBackgroundLeash != null) { + t.remove(mGapBackgroundLeash); + mGapBackgroundLeash = null; + } mHostLeash = null; mIcon = null; mResizingIconView = null; mIsResizing = false; mShown = false; + mOldBounds.setEmpty(); + mResizingBounds.setEmpty(); } /** Showing resizing hint. */ public void onResizing(ActivityManager.RunningTaskInfo resizingTask, Rect newBounds, - SurfaceControl.Transaction t) { + Rect sideBounds, SurfaceControl.Transaction t, int offsetX, int offsetY, + boolean immediately) { if (mResizingIconView == null) { return; } if (!mIsResizing) { mIsResizing = true; - mBounds.set(newBounds); + mOldBounds.set(newBounds); } + mResizingBounds.set(newBounds); + mOffsetX = offsetX; + mOffsetY = offsetY; final boolean show = - newBounds.width() > mBounds.width() || newBounds.height() > mBounds.height(); - final boolean animate = show != mShown; - if (animate && mFadeAnimator != null && mFadeAnimator.isRunning()) { + newBounds.width() > mOldBounds.width() || newBounds.height() > mOldBounds.height(); + final boolean update = show != mShown; + if (update && mFadeAnimator != null && mFadeAnimator.isRunning()) { // If we need to animate and animator still running, cancel it before we ensure both // background and icon surfaces are non null for next animation. mFadeAnimator.cancel(); @@ -176,6 +206,19 @@ public class SplitDecorManager extends WindowlessWindowManager { .setLayer(mBackgroundLeash, Integer.MAX_VALUE - 1); } + if (mGapBackgroundLeash == null && !immediately) { + final boolean isLandscape = newBounds.height() == sideBounds.height(); + final int left = isLandscape ? mOldBounds.width() : 0; + final int top = isLandscape ? 0 : mOldBounds.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); @@ -192,19 +235,64 @@ public class SplitDecorManager extends WindowlessWindowManager { newBounds.width() / 2 - mIconSize / 2, newBounds.height() / 2 - mIconSize / 2); - if (animate) { - startFadeAnimation(show, false /* isResized */); + if (update) { + if (immediately) { + t.setVisibility(mBackgroundLeash, show); + t.setVisibility(mIconLeash, show); + } else { + startFadeAnimation(show, false, null); + } mShown = show; } } /** Stops showing resizing hint. */ - public void onResized(SurfaceControl.Transaction t) { + public void onResized(SurfaceControl.Transaction t, Runnable animFinishedCallback) { + if (mScreenshotAnimator != null && mScreenshotAnimator.isRunning()) { + mScreenshotAnimator.cancel(); + } + + if (mScreenshot != null) { + t.setPosition(mScreenshot, mOffsetX, mOffsetY); + + final SurfaceControl.Transaction animT = new SurfaceControl.Transaction(); + mScreenshotAnimator = ValueAnimator.ofFloat(1, 0); + mScreenshotAnimator.addUpdateListener(valueAnimator -> { + final float progress = (float) valueAnimator.getAnimatedValue(); + animT.setAlpha(mScreenshot, progress); + animT.apply(); + }); + mScreenshotAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + mRunningAnimationCount++; + } + + @Override + public void onAnimationEnd(@androidx.annotation.NonNull Animator animation) { + mRunningAnimationCount--; + animT.remove(mScreenshot); + animT.apply(); + animT.close(); + mScreenshot = null; + + if (mRunningAnimationCount == 0 && animFinishedCallback != null) { + animFinishedCallback.run(); + } + } + }); + mScreenshotAnimator.start(); + } + if (mResizingIconView == null) { return; } mIsResizing = false; + mOffsetX = 0; + mOffsetY = 0; + mOldBounds.setEmpty(); + mResizingBounds.setEmpty(); if (mFadeAnimator != null && mFadeAnimator.isRunning()) { if (!mShown) { // If fade-out animation is running, just add release callback to it. @@ -219,19 +307,65 @@ public class SplitDecorManager extends WindowlessWindowManager { }); return; } - - // If fade-in animation is running, cancel it and re-run fade-out one. - mFadeAnimator.cancel(); } if (mShown) { - startFadeAnimation(false /* show */, true /* isResized */); + fadeOutDecor(animFinishedCallback); } else { // Decor surface is hidden so release it directly. releaseDecor(t); + if (mRunningAnimationCount == 0 && animFinishedCallback != null) { + animFinishedCallback.run(); + } } } - private void startFadeAnimation(boolean show, boolean isResized) { + /** Screenshot host leash and attach on it if meet some conditions */ + public void screenshotIfNeeded(SurfaceControl.Transaction t) { + if (!mShown && mIsResizing && !mOldBounds.equals(mResizingBounds)) { + if (mScreenshotAnimator != null && mScreenshotAnimator.isRunning()) { + mScreenshotAnimator.cancel(); + } + + mTempRect.set(mOldBounds); + mTempRect.offsetTo(0, 0); + mScreenshot = ScreenshotUtils.takeScreenshot(t, mHostLeash, mTempRect, + Integer.MAX_VALUE - 1); + } + } + + /** Set screenshot and attach on host leash it if meet some conditions */ + public void setScreenshotIfNeeded(SurfaceControl screenshot, SurfaceControl.Transaction t) { + if (screenshot == null || !screenshot.isValid()) return; + + if (!mShown && mIsResizing && !mOldBounds.equals(mResizingBounds)) { + if (mScreenshotAnimator != null && mScreenshotAnimator.isRunning()) { + mScreenshotAnimator.cancel(); + } + + mScreenshot = screenshot; + t.reparent(screenshot, mHostLeash); + t.setLayer(screenshot, Integer.MAX_VALUE - 1); + } + } + + /** Fade-out decor surface with animation end callback, if decor is hidden, run the callback + * directly. */ + public void fadeOutDecor(Runnable finishedCallback) { + if (mShown) { + // If previous animation is running, just cancel it. + if (mFadeAnimator != null && mFadeAnimator.isRunning()) { + mFadeAnimator.cancel(); + } + + startFadeAnimation(false /* show */, true, finishedCallback); + mShown = false; + } else { + if (finishedCallback != null) finishedCallback.run(); + } + } + + private void startFadeAnimation(boolean show, boolean releaseSurface, + Runnable finishedCallback) { final SurfaceControl.Transaction animT = new SurfaceControl.Transaction(); mFadeAnimator = ValueAnimator.ofFloat(0f, 1f); mFadeAnimator.setDuration(FADE_DURATION); @@ -248,13 +382,19 @@ public class SplitDecorManager extends WindowlessWindowManager { mFadeAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationStart(@NonNull Animator animation) { + mRunningAnimationCount++; if (show) { - animT.show(mBackgroundLeash).show(mIconLeash).apply(); + animT.show(mBackgroundLeash).show(mIconLeash); + } + if (mGapBackgroundLeash != null) { + animT.setVisibility(mGapBackgroundLeash, show); } + animT.apply(); } @Override public void onAnimationEnd(@NonNull Animator animation) { + mRunningAnimationCount--; if (!show) { if (mBackgroundLeash != null) { animT.hide(mBackgroundLeash); @@ -263,11 +403,15 @@ public class SplitDecorManager extends WindowlessWindowManager { animT.hide(mIconLeash); } } - if (isResized) { + if (releaseSurface) { releaseDecor(animT); } animT.apply(); animT.close(); + + if (mRunningAnimationCount == 0 && finishedCallback != null) { + finishedCallback.run(); + } } }); mFadeAnimator.start(); @@ -280,6 +424,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..9eba5ecd36f1 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; @@ -67,6 +69,7 @@ import com.android.wm.shell.common.InteractionJankMonitorUtils; import com.android.wm.shell.common.split.SplitScreenConstants.SplitPosition; import java.io.PrintWriter; +import java.util.function.Consumer; /** * Records and handles layout of splits. Helps to calculate proper bounds when configuration or @@ -78,15 +81,25 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange public static final int PARALLAX_DISMISSING = 1; public static final int PARALLAX_ALIGN_CENTER = 2; - private final int mDividerWindowWidth; - private final int mDividerInsets; - private final int mDividerSize; + public static final int FLING_RESIZE_DURATION = 250; + private static final int FLING_SWITCH_DURATION = 350; + private static final int FLING_ENTER_DURATION = 450; + private static final int FLING_EXIT_DURATION = 450; + + private int mDividerWindowWidth; + private int mDividerInsets; + private int mDividerSize; 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; @@ -106,8 +119,11 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange private boolean mFreezeDividerWindow = false; private int mOrientation; private int mRotation; + private int mDensity; + private int mUiMode; private final boolean mDimNonImeSide; + private ValueAnimator mDividerFlingAnimator; public SplitLayout(String windowName, Context context, Configuration configuration, SplitLayoutHandler splitLayoutHandler, @@ -117,6 +133,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange mContext = context.createConfigurationContext(configuration); mOrientation = configuration.orientation; mRotation = configuration.windowConfiguration.getRotation(); + mDensity = configuration.densityDpi; mSplitLayoutHandler = splitLayoutHandler; mDisplayImeController = displayImeController; mSplitWindowManager = new SplitWindowManager(windowName, mContext, configuration, @@ -125,22 +142,22 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange mImePositionProcessor = new ImePositionProcessor(mContext.getDisplayId()); mSurfaceEffectPolicy = new ResizingEffectPolicy(parallaxType); - final Resources resources = context.getResources(); - mDividerSize = resources.getDimensionPixelSize(R.dimen.split_divider_bar_width); - mDividerInsets = getDividerInsets(resources, context.getDisplay()); - mDividerWindowWidth = mDividerSize + 2 * mDividerInsets; + updateDividerConfig(mContext); mRootBounds.set(configuration.windowConfiguration.getBounds()); mDividerSnapAlgorithm = getSnapAlgorithm(mContext, mRootBounds, null); resetDividerPosition(); - mDimNonImeSide = resources.getBoolean(R.bool.config_dimNonImeAttachedSide); + mDimNonImeSide = mContext.getResources().getBoolean(R.bool.config_dimNonImeAttachedSide); + + updateInvisibleRect(); } - private int getDividerInsets(Resources resources, Display display) { + private void updateDividerConfig(Context context) { + final Resources resources = context.getResources(); + final Display display = context.getDisplay(); final int dividerInset = resources.getDimensionPixelSize( com.android.internal.R.dimen.docked_stack_divider_insets); - int radius = 0; RoundedCorner corner = display.getRoundedCorner(RoundedCorner.POSITION_TOP_LEFT); radius = corner != null ? Math.max(radius, corner.getRadius()) : radius; @@ -151,7 +168,9 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange corner = display.getRoundedCorner(RoundedCorner.POSITION_BOTTOM_LEFT); radius = corner != null ? Math.max(radius, corner.getRadius()) : radius; - return Math.max(dividerInset, radius); + mDividerInsets = Math.max(dividerInset, radius); + mDividerSize = resources.getDimensionPixelSize(R.dimen.split_divider_bar_width); + mDividerWindowWidth = mDividerSize + 2 * mDividerInsets; } /** Gets bounds of the primary split with screen based coordinate. */ @@ -178,6 +197,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 +214,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 +277,47 @@ 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; + final int density = configuration.densityDpi; + final int uiMode = configuration.uiMode; + + if (mOrientation == orientation + && mRotation == rotation + && mDensity == density + && mUiMode == uiMode + && mRootBounds.equals(rootBounds)) { return false; } + mContext = mContext.createConfigurationContext(configuration); + mSplitWindowManager.setConfiguration(configuration); + mOrientation = orientation; mTempRect.set(mRootBounds); mRootBounds.set(rootBounds); mRotation = rotation; + mDensity = density; + mUiMode = uiMode; mDividerSnapAlgorithm = getSnapAlgorithm(mContext, mRootBounds, null); + updateDividerConfig(mContext); initDividerPosition(mTempRect); + updateInvisibleRect(); return true; } @@ -270,28 +354,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. */ @@ -309,6 +400,10 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange mSplitWindowManager.release(t); mDisplayImeController.removePositionProcessor(mImePositionProcessor); mImePositionProcessor.reset(); + if (mDividerFlingAnimator != null) { + mDividerFlingAnimator.cancel(); + } + resetDividerPosition(); } public void release() { @@ -349,13 +444,21 @@ 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. */ void updateDivideBounds(int position) { updateBounds(position); - mSplitLayoutHandler.onLayoutSizeChanging(this); + mSplitLayoutHandler.onLayoutSizeChanging(this, mSurfaceEffectPolicy.mParallaxOffset.x, + mSurfaceEffectPolicy.mParallaxOffset.y); } void setDividePosition(int position, boolean applyLayoutChange) { @@ -387,26 +490,48 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange } /** + * Set divider should interactive to user or not. + * + * @param interactive divider interactive. + * @param hideHandle divider handle hidden or not, only work when interactive is false. + * @param from caller from where. + */ + public void setDividerInteractive(boolean interactive, boolean hideHandle, String from) { + mSplitWindowManager.setInteractive(interactive, hideHandle, from); + } + + /** * Sets new divide position and updates bounds correspondingly. Notifies listener if the new * target indicates dismissing split. */ 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,47 +548,158 @@ 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 + + if (mDividerFlingAnimator != null) { + mDividerFlingAnimator.cancel(); + } + + mDividerFlingAnimator = ValueAnimator .ofInt(from, to) - .setDuration(250); - animator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN); - animator.addUpdateListener( + .setDuration(duration); + mDividerFlingAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN); + mDividerFlingAnimator.addUpdateListener( animation -> updateDivideBounds((int) animation.getAnimatedValue())); - animator.addListener(new AnimatorListenerAdapter() { + mDividerFlingAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { if (flingFinishedCallback != null) { flingFinishedCallback.run(); } InteractionJankMonitorUtils.endTracing( - InteractionJankMonitor.CUJ_SPLIT_SCREEN_RESIZE); + CUJ_SPLIT_SCREEN_RESIZE); + mDividerFlingAnimator = null; } @Override public void onAnimationCancel(Animator animation) { - setDividePosition(to, true /* applyLayoutChange */); + mDividerFlingAnimator = null; + } + }); + mDividerFlingAnimator.start(); + } + + /** Switch both surface position with animation. */ + public void splitSwitching(SurfaceControl.Transaction t, SurfaceControl leash1, + SurfaceControl leash2, Consumer<Rect> 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); + + ValueAnimator animator1 = moveSurface(t, leash1, getRefBounds1(), distBounds1, + -insets.left, -insets.top); + ValueAnimator animator2 = moveSurface(t, leash2, getRefBounds2(), distBounds2, + insets.left, insets.top); + ValueAnimator animator3 = moveSurface(t, getDividerLeash(), getRefDividerBounds(), + distDividerBounds, 0 /* offsetX */, 0 /* offsetY */); + + AnimatorSet set = new AnimatorSet(); + set.playTogether(animator1, animator2, animator3); + set.setDuration(FLING_SWITCH_DURATION); + set.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mDividePosition = dividerPos; + updateBounds(mDividePosition); + finishCallback.accept(insets); } }); - animator.start(); + set.start(); + } + + private ValueAnimator moveSurface(SurfaceControl.Transaction t, SurfaceControl leash, + Rect start, Rect end, float offsetX, float offsetY) { + 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 (offsetX == 0 && offsetY == 0) { + t.setPosition(leash, distX, distY); + t.setWindowCrop(leash, width, height); + } else { + final int diffOffsetX = (int) (scale * offsetX); + final int diffOffsetY = (int) (scale * offsetY); + t.setPosition(leash, distX + diffOffsetX, distY + diffOffsetY); + mTempRect.set(0, 0, width, height); + mTempRect.offsetTo(-diffOffsetX, -diffOffsetY); + t.setCrop(leash, mTempRect); + } + t.apply(); + }); + return animator; } private static Rect getDisplayInsets(Context context) { @@ -478,19 +714,6 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange return bounds.width() > bounds.height(); } - /** Reverse the split position. */ - @SplitPosition - public static int reversePosition(@SplitPosition int position) { - switch (position) { - case SPLIT_POSITION_TOP_OR_LEFT: - return SPLIT_POSITION_BOTTOM_OR_RIGHT; - case SPLIT_POSITION_BOTTOM_OR_RIGHT: - return SPLIT_POSITION_TOP_OR_LEFT; - default: - return SPLIT_POSITION_UNDEFINED; - } - } - /** * Return if this layout is landscape. */ @@ -504,17 +727,21 @@ 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()); + // Make right or bottom side surface always higher than left or top side to avoid weird + // animation when dismiss split. e.g. App surface fling above on decor surface. + t.setLayer(leash1, 1); + t.setLayer(leash2, 2); if (mImePositionProcessor.adjustSurfaceLayoutForIme( t, dividerLeash, leash1, leash2, dimLayer1, dimLayer2)) { @@ -527,21 +754,28 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange } } - /** Apply recorded task layout to the {@link WindowContainerTransaction}. */ - public void applyTaskChanges(WindowContainerTransaction wct, + /** Apply recorded task layout to the {@link WindowContainerTransaction}. + * + * @return true if stage bounds actually update. + */ + public boolean applyTaskChanges(WindowContainerTransaction wct, ActivityManager.RunningTaskInfo task1, ActivityManager.RunningTaskInfo task2) { + boolean boundsChanged = false; if (!mBounds1.equals(mWinBounds1) || !task1.token.equals(mWinToken1)) { wct.setBounds(task1.token, mBounds1); wct.setSmallestScreenWidthDp(task1.token, getSmallestWidthDp(mBounds1)); mWinBounds1.set(mBounds1); mWinToken1 = task1.token; + boundsChanged = true; } if (!mBounds2.equals(mWinBounds2) || !task2.token.equals(mWinToken2)) { wct.setBounds(task2.token, mBounds2); wct.setSmallestScreenWidthDp(task2.token, getSmallestWidthDp(mBounds2)); mWinBounds2.set(mBounds2); mWinToken2 = task2.token; + boundsChanged = true; } + return boundsChanged; } private int getSmallestWidthDp(Rect bounds) { @@ -560,31 +794,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 +828,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. @@ -610,7 +836,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange * @see #applySurfaceChanges(SurfaceControl.Transaction, SurfaceControl, SurfaceControl, * SurfaceControl, SurfaceControl, boolean) */ - void onLayoutSizeChanging(SplitLayout layout); + void onLayoutSizeChanging(SplitLayout layout, int offsetX, int offsetY); /** * Calls when finish resizing the split bounds. @@ -891,9 +1117,10 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange // ImePositionProcessor#onImeVisibilityChanged directly in DividerView is not enough // because DividerView won't receive onImeVisibilityChanged callback after it being // re-inflated. - mSplitWindowManager.setInteractive(!mImeShown || !mHasImeFocus); + setDividerInteractive(!mImeShown || !mHasImeFocus || isFloating, true, + "onImeStartPositioning"); - return needOffset ? IME_ANIMATION_NO_ALPHA : 0; + return mTargetYOffset != mLastYOffset ? IME_ANIMATION_NO_ALPHA : 0; } @Override @@ -917,7 +1144,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange // Restore the split layout when wm-shell is not controlling IME insets anymore. if (!controlling && mImeShown) { reset(); - mSplitWindowManager.setInteractive(true); + setDividerInteractive(true, true, "onImeControlTargetChanged"); mSplitLayoutHandler.setLayoutOffsetTarget(0, 0, SplitLayout.this); mSplitLayoutHandler.onLayoutPositionChanging(SplitLayout.this); } @@ -971,16 +1198,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/SplitScreenUtils.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenUtils.java new file mode 100644 index 000000000000..042721c97053 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenUtils.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.common.split; + +import static com.android.wm.shell.common.split.SplitScreenConstants.CONTROLLED_ACTIVITY_TYPES; +import static com.android.wm.shell.common.split.SplitScreenConstants.CONTROLLED_WINDOWING_MODES; +import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; +import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT; +import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED; + +import android.annotation.Nullable; +import android.app.ActivityManager; +import android.app.PendingIntent; +import android.content.Intent; + +import com.android.internal.util.ArrayUtils; +import com.android.wm.shell.ShellTaskOrganizer; + +/** Helper utility class for split screen components to use. */ +public class SplitScreenUtils { + /** Reverse the split position. */ + @SplitScreenConstants.SplitPosition + public static int reverseSplitPosition(@SplitScreenConstants.SplitPosition int position) { + switch (position) { + case SPLIT_POSITION_TOP_OR_LEFT: + return SPLIT_POSITION_BOTTOM_OR_RIGHT; + case SPLIT_POSITION_BOTTOM_OR_RIGHT: + return SPLIT_POSITION_TOP_OR_LEFT; + case SPLIT_POSITION_UNDEFINED: + default: + return SPLIT_POSITION_UNDEFINED; + } + } + + /** Returns true if the task is valid for split screen. */ + public static boolean isValidToSplit(ActivityManager.RunningTaskInfo taskInfo) { + return taskInfo != null && taskInfo.supportsMultiWindow + && ArrayUtils.contains(CONTROLLED_ACTIVITY_TYPES, taskInfo.getActivityType()) + && ArrayUtils.contains(CONTROLLED_WINDOWING_MODES, taskInfo.getWindowingMode()); + } + + /** Retrieve package name from an intent */ + @Nullable + public static String getPackageName(Intent intent) { + if (intent == null || intent.getComponent() == null) { + return null; + } + return intent.getComponent().getPackageName(); + } + + /** Retrieve package name from a PendingIntent */ + @Nullable + public static String getPackageName(PendingIntent pendingIntent) { + if (pendingIntent == null || pendingIntent.getIntent() == null) { + return null; + } + return getPackageName(pendingIntent.getIntent()); + } + + /** Retrieve package name from a taskId */ + @Nullable + public static String getPackageName(int taskId, ShellTaskOrganizer taskOrganizer) { + final ActivityManager.RunningTaskInfo taskInfo = taskOrganizer.getRunningTaskInfo(taskId); + return taskInfo != null ? getPackageName(taskInfo.baseIntent) : null; + } + + /** Returns true if they are the same package. */ + public static boolean samePackage(String packageName1, String packageName2) { + return packageName1 != null && packageName1.equals(packageName2); + } +} 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..00361d9dd9cf 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 @@ -93,7 +93,7 @@ public final class SplitWindowManager extends WindowlessWindowManager { } @Override - protected void attachToParentSurface(IWindow window, SurfaceControl.Builder b) { + protected SurfaceControl getParentSurface(IWindow window, WindowManager.LayoutParams attrs) { // Can't set position for the ViewRootImpl SC directly. Create a leash to manipulate later. final SurfaceControl.Builder builder = new SurfaceControl.Builder(new SurfaceSession()) .setContainerLayer() @@ -103,7 +103,7 @@ public final class SplitWindowManager extends WindowlessWindowManager { mParentContainerCallbacks.attachToParentSurface(builder); mLeash = builder.build(); mParentContainerCallbacks.onLeashReady(mLeash); - b.setParent(mLeash); + return mLeash; } /** Inflates {@link DividerView} on to the root surface. */ @@ -113,7 +113,8 @@ public final class SplitWindowManager extends WindowlessWindowManager { "Try to inflate divider view again without release first"); } - mViewHost = new SurfaceControlViewHost(mContext, mContext.getDisplay(), this); + mViewHost = new SurfaceControlViewHost(mContext, mContext.getDisplay(), this, + "SplitWindowManager"); mDividerView = (DividerView) LayoutInflater.from(mContext) .inflate(R.layout.split_divider, null /* root */); @@ -126,6 +127,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); } @@ -166,9 +168,16 @@ public final class SplitWindowManager extends WindowlessWindowManager { } } - void setInteractive(boolean interactive) { + /** + * Set divider should interactive to user or not. + * + * @param interactive divider interactive. + * @param hideHandle divider handle hidden or not, only work when interactive is false. + * @param from caller from where. + */ + void setInteractive(boolean interactive, boolean hideHandle, String from) { if (mDividerView == null) return; - mDividerView.setInteractive(interactive); + mDividerView.setInteractive(interactive, hideHandle, from); } View getDividerView() { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIConfiguration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIConfiguration.java new file mode 100644 index 000000000000..4e10ce82f365 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIConfiguration.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.compatui; + +import android.annotation.NonNull; +import android.app.TaskInfo; +import android.content.Context; +import android.content.SharedPreferences; +import android.provider.DeviceConfig; + +import com.android.wm.shell.R; +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.annotations.ShellMainThread; +import com.android.wm.shell.dagger.WMSingleton; + +import javax.inject.Inject; + +/** + * Configuration flags for the CompatUX implementation + */ +@WMSingleton +public class CompatUIConfiguration implements DeviceConfig.OnPropertiesChangedListener { + + private static final String KEY_ENABLE_LETTERBOX_RESTART_DIALOG = + "enable_letterbox_restart_confirmation_dialog"; + + private static final String KEY_ENABLE_LETTERBOX_REACHABILITY_EDUCATION = + "enable_letterbox_education_for_reachability"; + + private static final boolean DEFAULT_VALUE_ENABLE_LETTERBOX_RESTART_DIALOG = true; + + private static final boolean DEFAULT_VALUE_ENABLE_LETTERBOX_REACHABILITY_EDUCATION = true; + + /** + * The name of the {@link SharedPreferences} that holds information about compat ui. + */ + private static final String COMPAT_UI_SHARED_PREFERENCES = "dont_show_restart_dialog"; + + /** + * The name of the {@link SharedPreferences} that holds which user has seen the Letterbox + * Education dialog. + */ + private static final String HAS_SEEN_LETTERBOX_EDUCATION_SHARED_PREFERENCES = + "has_seen_letterbox_education"; + + /** + * Key prefix for the {@link SharedPreferences} entries related to the reachability + * education. + */ + private static final String HAS_SEEN_REACHABILITY_EDUCATION_KEY_PREFIX = + "has_seen_reachability_education"; + + /** + * The {@link SharedPreferences} instance for the restart dialog and the reachability + * education. + */ + private final SharedPreferences mCompatUISharedPreferences; + + /** + * The {@link SharedPreferences} instance for the letterbox education dialog. + */ + private final SharedPreferences mLetterboxEduSharedPreferences; + + // Whether the extended restart dialog is enabled + private boolean mIsRestartDialogEnabled; + + // Whether the additional education about reachability is enabled + private boolean mIsReachabilityEducationEnabled; + + // Whether the extended restart dialog is enabled + private boolean mIsRestartDialogOverrideEnabled; + + // Whether the additional education about reachability is enabled + private boolean mIsReachabilityEducationOverrideEnabled; + + // Whether the extended restart dialog is allowed from backend + private boolean mIsLetterboxRestartDialogAllowed; + + // Whether the additional education about reachability is allowed from backend + private boolean mIsLetterboxReachabilityEducationAllowed; + + @Inject + public CompatUIConfiguration(Context context, @ShellMainThread ShellExecutor mainExecutor) { + mIsRestartDialogEnabled = context.getResources().getBoolean( + R.bool.config_letterboxIsRestartDialogEnabled); + mIsReachabilityEducationEnabled = context.getResources().getBoolean( + R.bool.config_letterboxIsReachabilityEducationEnabled); + mIsLetterboxRestartDialogAllowed = DeviceConfig.getBoolean( + DeviceConfig.NAMESPACE_WINDOW_MANAGER, KEY_ENABLE_LETTERBOX_RESTART_DIALOG, + DEFAULT_VALUE_ENABLE_LETTERBOX_RESTART_DIALOG); + mIsLetterboxReachabilityEducationAllowed = DeviceConfig.getBoolean( + DeviceConfig.NAMESPACE_WINDOW_MANAGER, KEY_ENABLE_LETTERBOX_REACHABILITY_EDUCATION, + DEFAULT_VALUE_ENABLE_LETTERBOX_REACHABILITY_EDUCATION); + DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_APP_COMPAT, mainExecutor, + this); + mCompatUISharedPreferences = context.getSharedPreferences(getCompatUISharedPreferenceName(), + Context.MODE_PRIVATE); + mLetterboxEduSharedPreferences = context.getSharedPreferences( + getHasSeenLetterboxEducationSharedPreferencedName(), Context.MODE_PRIVATE); + } + + /** + * @return {@value true} if the restart dialog is enabled. + */ + boolean isRestartDialogEnabled() { + return mIsRestartDialogOverrideEnabled || (mIsRestartDialogEnabled + && mIsLetterboxRestartDialogAllowed); + } + + /** + * Enables/Disables the restart education dialog + */ + void setIsRestartDialogOverrideEnabled(boolean enabled) { + mIsRestartDialogOverrideEnabled = enabled; + } + + /** + * @return {@value true} if the reachability education is enabled. + */ + boolean isReachabilityEducationEnabled() { + return mIsReachabilityEducationOverrideEnabled || (mIsReachabilityEducationEnabled + && mIsLetterboxReachabilityEducationAllowed); + } + + /** + * Enables/Disables the reachability education + */ + void setIsReachabilityEducationOverrideEnabled(boolean enabled) { + mIsReachabilityEducationOverrideEnabled = enabled; + } + + void setDontShowRestartDialogAgain(TaskInfo taskInfo) { + mCompatUISharedPreferences.edit().putBoolean( + getDontShowAgainRestartKey(taskInfo.userId, taskInfo.topActivity.getPackageName()), + true).apply(); + } + + boolean shouldShowRestartDialogAgain(TaskInfo taskInfo) { + return !mCompatUISharedPreferences.getBoolean(getDontShowAgainRestartKey(taskInfo.userId, + taskInfo.topActivity.getPackageName()), /* default= */ false); + } + + void setDontShowReachabilityEducationAgain(TaskInfo taskInfo) { + mCompatUISharedPreferences.edit().putBoolean( + getDontShowAgainReachabilityEduKey(taskInfo.userId), true).apply(); + } + + boolean shouldShowReachabilityEducation(@NonNull TaskInfo taskInfo) { + return getHasSeenLetterboxEducation(taskInfo.userId) + && !mCompatUISharedPreferences.getBoolean( + getDontShowAgainReachabilityEduKey(taskInfo.userId), /* default= */false); + } + + boolean getHasSeenLetterboxEducation(int userId) { + return mLetterboxEduSharedPreferences + .getBoolean(getDontShowLetterboxEduKey(userId), /* default= */ false); + } + + void setSeenLetterboxEducation(int userId) { + mLetterboxEduSharedPreferences.edit().putBoolean(getDontShowLetterboxEduKey(userId), + true).apply(); + } + + protected String getCompatUISharedPreferenceName() { + return COMPAT_UI_SHARED_PREFERENCES; + } + + protected String getHasSeenLetterboxEducationSharedPreferencedName() { + return HAS_SEEN_LETTERBOX_EDUCATION_SHARED_PREFERENCES; + } + + /** + * Updates the {@link DeviceConfig} state for the CompatUI + * @param properties Contains the complete collection of properties which have changed for a + * single namespace. This includes only those which were added, updated, + */ + @Override + public void onPropertiesChanged(@NonNull DeviceConfig.Properties properties) { + if (properties.getKeyset().contains(KEY_ENABLE_LETTERBOX_RESTART_DIALOG)) { + mIsLetterboxRestartDialogAllowed = DeviceConfig.getBoolean( + DeviceConfig.NAMESPACE_WINDOW_MANAGER, KEY_ENABLE_LETTERBOX_RESTART_DIALOG, + DEFAULT_VALUE_ENABLE_LETTERBOX_RESTART_DIALOG); + } + // TODO(b/263349751): Update flag and default value to true + if (properties.getKeyset().contains(KEY_ENABLE_LETTERBOX_REACHABILITY_EDUCATION)) { + mIsLetterboxReachabilityEducationAllowed = DeviceConfig.getBoolean( + DeviceConfig.NAMESPACE_WINDOW_MANAGER, + KEY_ENABLE_LETTERBOX_REACHABILITY_EDUCATION, + DEFAULT_VALUE_ENABLE_LETTERBOX_REACHABILITY_EDUCATION); + } + } + + private static String getDontShowAgainReachabilityEduKey(int userId) { + return HAS_SEEN_REACHABILITY_EDUCATION_KEY_PREFIX + "@" + userId; + } + + private static String getDontShowLetterboxEduKey(int userId) { + return String.valueOf(userId); + } + + private String getDontShowAgainRestartKey(int userId, String packageName) { + return packageName + "@" + userId; + } +}
\ No newline at end of file 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..4d83247e5c03 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 @@ -24,6 +24,7 @@ import android.content.res.Configuration; import android.hardware.display.DisplayManager; import android.util.ArraySet; import android.util.Log; +import android.util.Pair; import android.util.SparseArray; import android.view.Display; import android.view.InsetsSourceControl; @@ -37,15 +38,18 @@ import com.android.wm.shell.common.DisplayImeController; import com.android.wm.shell.common.DisplayInsetsController; import com.android.wm.shell.common.DisplayInsetsController.OnInsetsChangedListener; import com.android.wm.shell.common.DisplayLayout; +import com.android.wm.shell.common.DockStateReader; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; -import com.android.wm.shell.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; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.function.Consumer; @@ -58,7 +62,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 { @@ -88,6 +92,18 @@ public class CompatUIController implements OnDisplaysChangedListener, private final SparseArray<CompatUIWindowManager> mActiveCompatLayouts = new SparseArray<>(0); /** + * {@link SparseArray} that maps task ids to {@link RestartDialogWindowManager} that are + * currently visible + */ + private final SparseArray<RestartDialogWindowManager> mTaskIdToRestartDialogWindowManagerMap = + new SparseArray<>(0); + + /** + * {@link Set} of task ids for which we need to display a restart confirmation dialog + */ + private Set<Integer> mSetOfTaskIdsShowingRestartDialog = new HashSet<>(); + + /** * The active Letterbox Education layout if there is one (there can be at most one active). * * <p>An active layout is a layout that is eligible to be shown for the associated task but @@ -96,49 +112,67 @@ public class CompatUIController implements OnDisplaysChangedListener, @Nullable private LetterboxEduWindowManager mActiveLetterboxEduLayout; + /** + * The active Reachability UI layout. + */ + @Nullable + private ReachabilityEduWindowManager mActiveReachabilityEduLayout; + /** Avoid creating display context frequently for non-default display. */ 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; - + private final DockStateReader mDockStateReader; + private final CompatUIConfiguration mCompatUIConfiguration; // Only show each hint once automatically in the process life. private final CompatUIHintsState mCompatUIHintsState; + private final CompatUIShellCommandHandler mCompatUIShellCommandHandler; + + private CompatUICallback mCallback; // Indicates if the keyguard is currently showing, in which case compat UIs shouldn't // be shown. private boolean mKeyguardShowing; public CompatUIController(Context context, + ShellInit shellInit, + ShellController shellController, DisplayController displayController, DisplayInsetsController displayInsetsController, DisplayImeController imeController, SyncTransactionQueue syncQueue, ShellExecutor mainExecutor, - Lazy<Transitions> transitionsLazy) { + Lazy<Transitions> transitionsLazy, + DockStateReader dockStateReader, + CompatUIConfiguration compatUIConfiguration, + CompatUIShellCommandHandler compatUIShellCommandHandler) { 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(); + mDockStateReader = dockStateReader; + mCompatUIConfiguration = compatUIConfiguration; + mCompatUIShellCommandHandler = compatUIShellCommandHandler; + 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); + mCompatUIShellCommandHandler.onInit(); } /** Sets the callback for UI interactions. */ @@ -155,6 +189,9 @@ public class CompatUIController implements OnDisplaysChangedListener, */ public void onCompatInfoChanged(TaskInfo taskInfo, @Nullable ShellTaskOrganizer.TaskListener taskListener) { + if (taskInfo != null && !taskInfo.topActivityInSizeCompat) { + mSetOfTaskIdsShowingRestartDialog.remove(taskInfo.taskId); + } if (taskInfo.configuration == null || taskListener == null) { // Null token means the current foreground activity is not in compatibility mode. removeLayouts(taskInfo.taskId); @@ -163,6 +200,8 @@ public class CompatUIController implements OnDisplaysChangedListener, createOrUpdateCompatLayout(taskInfo, taskListener); createOrUpdateLetterboxEduLayout(taskInfo, taskListener); + createOrUpdateRestartDialogLayout(taskInfo, taskListener); + createOrUpdateReachabilityEduLayout(taskInfo, taskListener, false); } @Override @@ -223,9 +262,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()))); } @@ -268,7 +308,21 @@ public class CompatUIController implements OnDisplaysChangedListener, ShellTaskOrganizer.TaskListener taskListener) { return new CompatUIWindowManager(context, taskInfo, mSyncQueue, mCallback, taskListener, - mDisplayController.getDisplayLayout(taskInfo.displayId), mCompatUIHintsState); + mDisplayController.getDisplayLayout(taskInfo.displayId), mCompatUIHintsState, + mCompatUIConfiguration, this::onRestartButtonClicked); + } + + private void onRestartButtonClicked( + Pair<TaskInfo, ShellTaskOrganizer.TaskListener> taskInfoState) { + if (mCompatUIConfiguration.isRestartDialogEnabled() + && mCompatUIConfiguration.shouldShowRestartDialogAgain( + taskInfoState.first)) { + // We need to show the dialog + mSetOfTaskIdsShowingRestartDialog.add(taskInfoState.first.taskId); + onCompatInfoChanged(taskInfoState.first, taskInfoState.second); + } else { + mCallback.onSizeCompatRestartButtonClicked(taskInfoState.first.taskId); + } } private void createOrUpdateLetterboxEduLayout(TaskInfo taskInfo, @@ -308,14 +362,113 @@ public class CompatUIController implements OnDisplaysChangedListener, ShellTaskOrganizer.TaskListener taskListener) { return new LetterboxEduWindowManager(context, taskInfo, mSyncQueue, taskListener, mDisplayController.getDisplayLayout(taskInfo.displayId), - mTransitionsLazy.get(), - this::onLetterboxEduDismissed); + mTransitionsLazy.get(), this::onLetterboxEduDismissed, mDockStateReader, + mCompatUIConfiguration); } - private void onLetterboxEduDismissed() { + private void onLetterboxEduDismissed( + Pair<TaskInfo, ShellTaskOrganizer.TaskListener> stateInfo) { mActiveLetterboxEduLayout = null; + // We need to update the UI + createOrUpdateReachabilityEduLayout(stateInfo.first, stateInfo.second, true); + } + + private void createOrUpdateRestartDialogLayout(TaskInfo taskInfo, + ShellTaskOrganizer.TaskListener taskListener) { + RestartDialogWindowManager layout = + mTaskIdToRestartDialogWindowManagerMap.get(taskInfo.taskId); + if (layout != null) { + if (layout.needsToBeRecreated(taskInfo, taskListener)) { + mTaskIdToRestartDialogWindowManagerMap.remove(taskInfo.taskId); + layout.release(); + } else { + layout.setRequestRestartDialog( + mSetOfTaskIdsShowingRestartDialog.contains(taskInfo.taskId)); + // UI already exists, update the UI layout. + if (!layout.updateCompatInfo(taskInfo, taskListener, + showOnDisplay(layout.getDisplayId()))) { + // The layout is no longer eligible to be shown, remove from active layouts. + mTaskIdToRestartDialogWindowManagerMap.remove(taskInfo.taskId); + } + return; + } + } + // Create a new UI layout. + final Context context = getOrCreateDisplayContext(taskInfo.displayId); + if (context == null) { + return; + } + layout = createRestartDialogWindowManager(context, taskInfo, taskListener); + layout.setRequestRestartDialog( + mSetOfTaskIdsShowingRestartDialog.contains(taskInfo.taskId)); + if (layout.createLayout(showOnDisplay(taskInfo.displayId))) { + // The new layout is eligible to be shown, add it the active layouts. + mTaskIdToRestartDialogWindowManagerMap.put(taskInfo.taskId, layout); + } } + @VisibleForTesting + RestartDialogWindowManager createRestartDialogWindowManager(Context context, TaskInfo taskInfo, + ShellTaskOrganizer.TaskListener taskListener) { + return new RestartDialogWindowManager(context, taskInfo, mSyncQueue, taskListener, + mDisplayController.getDisplayLayout(taskInfo.displayId), mTransitionsLazy.get(), + this::onRestartDialogCallback, this::onRestartDialogDismissCallback, + mCompatUIConfiguration); + } + + private void onRestartDialogCallback( + Pair<TaskInfo, ShellTaskOrganizer.TaskListener> stateInfo) { + mTaskIdToRestartDialogWindowManagerMap.remove(stateInfo.first.taskId); + mCallback.onSizeCompatRestartButtonClicked(stateInfo.first.taskId); + } + + private void onRestartDialogDismissCallback( + Pair<TaskInfo, ShellTaskOrganizer.TaskListener> stateInfo) { + mSetOfTaskIdsShowingRestartDialog.remove(stateInfo.first.taskId); + onCompatInfoChanged(stateInfo.first, stateInfo.second); + } + + private void createOrUpdateReachabilityEduLayout(TaskInfo taskInfo, + ShellTaskOrganizer.TaskListener taskListener, boolean forceUpdate) { + if (mActiveReachabilityEduLayout != null) { + mActiveReachabilityEduLayout.forceUpdate(forceUpdate); + // UI already exists, update the UI layout. + if (!mActiveReachabilityEduLayout.updateCompatInfo(taskInfo, taskListener, + showOnDisplay(mActiveReachabilityEduLayout.getDisplayId()))) { + // The layout is no longer eligible to be shown, remove from active layouts. + mActiveReachabilityEduLayout = null; + } + return; + } + // Create a new UI layout. + final Context context = getOrCreateDisplayContext(taskInfo.displayId); + if (context == null) { + return; + } + ReachabilityEduWindowManager newLayout = createReachabilityEduWindowManager(context, + taskInfo, taskListener); + if (newLayout.createLayout(showOnDisplay(taskInfo.displayId))) { + // The new layout is eligible to be shown, make it the active layout. + if (mActiveReachabilityEduLayout != null) { + // Release the previous layout since at most one can be active. + // Since letterbox reachability education is only shown once to the user, + // releasing the previous layout is only a precaution. + mActiveReachabilityEduLayout.release(); + } + mActiveReachabilityEduLayout = newLayout; + } + } + + @VisibleForTesting + ReachabilityEduWindowManager createReachabilityEduWindowManager(Context context, + TaskInfo taskInfo, + ShellTaskOrganizer.TaskListener taskListener) { + return new ReachabilityEduWindowManager(context, taskInfo, mSyncQueue, mCallback, + taskListener, mDisplayController.getDisplayLayout(taskInfo.displayId), + mCompatUIConfiguration, mMainExecutor); + } + + private void removeLayouts(int taskId) { final CompatUIWindowManager layout = mActiveCompatLayouts.get(taskId); if (layout != null) { @@ -327,6 +480,19 @@ public class CompatUIController implements OnDisplaysChangedListener, mActiveLetterboxEduLayout.release(); mActiveLetterboxEduLayout = null; } + + final RestartDialogWindowManager restartLayout = + mTaskIdToRestartDialogWindowManagerMap.get(taskId); + if (restartLayout != null) { + restartLayout.release(); + mTaskIdToRestartDialogWindowManagerMap.remove(taskId); + mSetOfTaskIdsShowingRestartDialog.remove(taskId); + } + if (mActiveReachabilityEduLayout != null + && mActiveReachabilityEduLayout.getTaskId() == taskId) { + mActiveReachabilityEduLayout.release(); + mActiveReachabilityEduLayout = null; + } } private Context getOrCreateDisplayContext(int displayId) { @@ -371,18 +537,16 @@ public class CompatUIController implements OnDisplaysChangedListener, if (mActiveLetterboxEduLayout != null && condition.test(mActiveLetterboxEduLayout)) { callback.accept(mActiveLetterboxEduLayout); } - } - - /** - * 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); - }); + for (int i = 0; i < mTaskIdToRestartDialogWindowManagerMap.size(); i++) { + final int taskId = mTaskIdToRestartDialogWindowManagerMap.keyAt(i); + final RestartDialogWindowManager layout = + mTaskIdToRestartDialogWindowManagerMap.get(taskId); + if (layout != null && condition.test(layout)) { + callback.accept(layout); + } + } + if (mActiveReachabilityEduLayout != null && condition.test(mActiveReachabilityEduLayout)) { + callback.accept(mActiveReachabilityEduLayout); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUILayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUILayout.java index d44b4d8f63b6..f65c26ada04d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUILayout.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUILayout.java @@ -21,6 +21,7 @@ import android.app.TaskInfo; import android.app.TaskInfo.CameraCompatControlState; import android.content.Context; import android.util.AttributeSet; +import android.view.MotionEvent; import android.view.View; import android.widget.ImageButton; import android.widget.LinearLayout; @@ -112,6 +113,14 @@ class CompatUILayout extends LinearLayout { } @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + if (ev.getAction() == MotionEvent.ACTION_DOWN) { + mWindowManager.relayout(); + } + return super.onInterceptTouchEvent(ev); + } + + @Override protected void onFinishInflate() { super.onFinishInflate(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIShellCommandHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIShellCommandHandler.java new file mode 100644 index 000000000000..4fb18e27b145 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIShellCommandHandler.java @@ -0,0 +1,103 @@ +/* + * 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.compatui; + +import com.android.wm.shell.dagger.WMSingleton; +import com.android.wm.shell.sysui.ShellCommandHandler; + +import java.io.PrintWriter; +import java.util.function.Consumer; + +import javax.inject.Inject; + +/** + * Handles the shell commands for the CompatUX. + * + * <p> Use with {@code adb shell dumpsys activity service SystemUIService WMShell compatui + * <command>}. + */ +@WMSingleton +public final class CompatUIShellCommandHandler implements + ShellCommandHandler.ShellCommandActionHandler { + + private final CompatUIConfiguration mCompatUIConfiguration; + private final ShellCommandHandler mShellCommandHandler; + + @Inject + public CompatUIShellCommandHandler(ShellCommandHandler shellCommandHandler, + CompatUIConfiguration compatUIConfiguration) { + mShellCommandHandler = shellCommandHandler; + mCompatUIConfiguration = compatUIConfiguration; + } + + void onInit() { + mShellCommandHandler.addCommandCallback("compatui", this, this); + } + + @Override + public boolean onShellCommand(String[] args, PrintWriter pw) { + if (args.length != 2) { + pw.println("Invalid command: " + args[0]); + return false; + } + switch (args[0]) { + case "restartDialogEnabled": + return invokeOrError(args[1], pw, + mCompatUIConfiguration::setIsRestartDialogOverrideEnabled); + case "reachabilityEducationEnabled": + return invokeOrError(args[1], pw, + mCompatUIConfiguration::setIsReachabilityEducationOverrideEnabled); + default: + pw.println("Invalid command: " + args[0]); + return false; + } + } + + @Override + public void printShellCommandHelp(PrintWriter pw, String prefix) { + pw.println(prefix + "restartDialogEnabled [0|false|1|true]"); + pw.println(prefix + " Enable/Disable the restart education dialog for Size Compat Mode"); + pw.println(prefix + "reachabilityEducationEnabled [0|false|1|true]"); + pw.println(prefix + + " Enable/Disable the restart education dialog for letterbox reachability"); + pw.println(prefix + " Disable the restart education dialog for letterbox reachability"); + } + + private static boolean invokeOrError(String input, PrintWriter pw, + Consumer<Boolean> setter) { + Boolean asBoolean = strToBoolean(input); + if (asBoolean == null) { + pw.println("Error: expected true, 1, false, 0."); + return false; + } + setter.accept(asBoolean); + return true; + } + + // Converts a String to boolean if possible or it returns null otherwise + private static Boolean strToBoolean(String str) { + switch(str) { + case "1": + case "true": + return true; + case "0": + case "false": + return false; + } + return null; + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java index bce3ec4128e8..170c0ee91b40 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java @@ -21,12 +21,14 @@ import static android.app.TaskInfo.CAMERA_COMPAT_CONTROL_HIDDEN; import static android.app.TaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED; import static android.app.TaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED; +import android.annotation.NonNull; import android.annotation.Nullable; import android.app.TaskInfo; import android.app.TaskInfo.CameraCompatControlState; import android.content.Context; import android.graphics.Rect; import android.util.Log; +import android.util.Pair; import android.view.LayoutInflater; import android.view.View; @@ -36,7 +38,8 @@ import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.compatui.CompatUIController.CompatUICallback; -import com.android.wm.shell.compatui.letterboxedu.LetterboxEduWindowManager; + +import java.util.function.Consumer; /** * Window manager for the Size Compat restart button and Camera Compat control. @@ -50,6 +53,13 @@ class CompatUIWindowManager extends CompatUIWindowManagerAbstract { private final CompatUICallback mCallback; + private final CompatUIConfiguration mCompatUIConfiguration; + + private final Consumer<Pair<TaskInfo, ShellTaskOrganizer.TaskListener>> mOnRestartButtonClicked; + + @NonNull + private TaskInfo mTaskInfo; + // Remember the last reported states in case visibility changes due to keyguard or IME updates. @VisibleForTesting boolean mHasSizeCompat; @@ -68,12 +78,16 @@ class CompatUIWindowManager extends CompatUIWindowManagerAbstract { CompatUIWindowManager(Context context, TaskInfo taskInfo, SyncTransactionQueue syncQueue, CompatUICallback callback, ShellTaskOrganizer.TaskListener taskListener, DisplayLayout displayLayout, - CompatUIHintsState compatUIHintsState) { + CompatUIHintsState compatUIHintsState, CompatUIConfiguration compatUIConfiguration, + Consumer<Pair<TaskInfo, ShellTaskOrganizer.TaskListener>> onRestartButtonClicked) { super(context, taskInfo, syncQueue, taskListener, displayLayout); + mTaskInfo = taskInfo; mCallback = callback; mHasSizeCompat = taskInfo.topActivityInSizeCompat; mCameraCompatControlState = taskInfo.cameraCompatControlState; mCompatUIHintsState = compatUIHintsState; + mCompatUIConfiguration = compatUIConfiguration; + mOnRestartButtonClicked = onRestartButtonClicked; } @Override @@ -119,6 +133,7 @@ class CompatUIWindowManager extends CompatUIWindowManagerAbstract { @Override public boolean updateCompatInfo(TaskInfo taskInfo, ShellTaskOrganizer.TaskListener taskListener, boolean canShow) { + mTaskInfo = taskInfo; final boolean prevHasSizeCompat = mHasSizeCompat; final int prevCameraCompatControlState = mCameraCompatControlState; mHasSizeCompat = taskInfo.topActivityInSizeCompat; @@ -138,7 +153,7 @@ class CompatUIWindowManager extends CompatUIWindowManagerAbstract { /** Called when the restart button is clicked. */ void onRestartButtonClicked() { - mCallback.onSizeCompatRestartButtonClicked(mTaskId); + mOnRestartButtonClicked.accept(Pair.create(mTaskInfo, getTaskListener())); } /** Called when the camera treatment button is clicked. */ @@ -199,8 +214,14 @@ class CompatUIWindowManager extends CompatUIWindowManagerAbstract { : taskStableBounds.right - taskBounds.left - mLayout.getMeasuredWidth(); final int positionY = taskStableBounds.bottom - taskBounds.top - mLayout.getMeasuredHeight(); - + // To secure a proper visualisation, we hide the layout while updating the position of + // the {@link SurfaceControl} it belongs. + final int oldVisibility = mLayout.getVisibility(); + if (oldVisibility == View.VISIBLE) { + mLayout.setVisibility(View.GONE); + } updateSurfacePosition(positionX, positionY); + mLayout.setVisibility(oldVisibility); } private void updateVisibilityOfViews() { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManagerAbstract.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManagerAbstract.java index face24340a4e..9c4e79cd631b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManagerAbstract.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManagerAbstract.java @@ -155,7 +155,7 @@ public abstract class CompatUIWindowManagerAbstract extends WindowlessWindowMana } @Override - protected void attachToParentSurface(IWindow window, SurfaceControl.Builder b) { + protected SurfaceControl getParentSurface(IWindow window, WindowManager.LayoutParams attrs) { String className = getClass().getSimpleName(); final SurfaceControl.Builder builder = new SurfaceControl.Builder(new SurfaceSession()) .setContainerLayer() @@ -164,9 +164,12 @@ public abstract class CompatUIWindowManagerAbstract extends WindowlessWindowMana .setCallsite(className + "#attachToParentSurface"); attachToParentSurface(builder); mLeash = builder.build(); - b.setParent(mLeash); - initSurface(mLeash); + return mLeash; + } + + protected ShellTaskOrganizer.TaskListener getTaskListener() { + return mTaskListener; } /** Inits the z-order of the surface. */ @@ -206,7 +209,8 @@ public abstract class CompatUIWindowManagerAbstract extends WindowlessWindowMana } View layout = getLayout(); - if (layout == null || prevTaskListener != taskListener) { + if (layout == null || prevTaskListener != taskListener + || mTaskConfig.uiMode != prevTaskConfig.uiMode) { // Layout wasn't created yet or TaskListener changed, recreate the layout for new // surface parent. release(); @@ -359,7 +363,8 @@ public abstract class CompatUIWindowManagerAbstract extends WindowlessWindowMana /** Creates a {@link SurfaceControlViewHost} for this window manager. */ @VisibleForTesting(visibility = PRIVATE) public SurfaceControlViewHost createSurfaceViewHost() { - return new SurfaceControlViewHost(mContext, mContext.getDisplay(), this); + return new SurfaceControlViewHost(mContext, mContext.getDisplay(), this, + getClass().getSimpleName()); } /** Gets the layout params. */ @@ -379,7 +384,7 @@ public abstract class CompatUIWindowManagerAbstract extends WindowlessWindowMana // Cannot be wrap_content as this determines the actual window size width, height, TYPE_APPLICATION_OVERLAY, - FLAG_NOT_FOCUSABLE | FLAG_NOT_TOUCH_MODAL, + getWindowManagerLayoutParamsFlags(), PixelFormat.TRANSLUCENT); winParams.token = new Binder(); winParams.setTitle(getClass().getSimpleName() + mTaskId); @@ -387,6 +392,13 @@ public abstract class CompatUIWindowManagerAbstract extends WindowlessWindowMana return winParams; } + /** + * @return Flags to use for the {@link WindowManager} layout + */ + protected int getWindowManagerLayoutParamsFlags() { + return FLAG_NOT_FOCUSABLE | FLAG_NOT_TOUCH_MODAL; + } + protected final String getTag() { return getClass().getSimpleName(); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/DialogAnimationController.java index 3061eab17d24..7475feac5b12 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduAnimationController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/DialogAnimationController.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.compatui.letterboxedu; +package com.android.wm.shell.compatui; import static com.android.internal.R.styleable.WindowAnimation_windowEnterAnimation; import static com.android.internal.R.styleable.WindowAnimation_windowExitAnimation; @@ -38,10 +38,15 @@ import android.view.animation.Animation; import com.android.internal.policy.TransitionAnimation; /** - * Controls the enter/exit animations of the letterbox education. + * Controls the enter/exit a dialog. + * + * @param <T> The {@link DialogContainerSupplier} to use */ -class LetterboxEduAnimationController { - private static final String TAG = "LetterboxEduAnimation"; +public class DialogAnimationController<T extends DialogContainerSupplier> { + + // The alpha of a background is a number between 0 (fully transparent) to 255 (fully opaque). + // 204 is simply 255 * 0.8. + static final int BACKGROUND_DIM_ALPHA = 204; // If shell transitions are enabled, startEnterAnimation will be called after all transitions // have finished, and therefore the start delay should be shorter. @@ -49,6 +54,7 @@ class LetterboxEduAnimationController { private final TransitionAnimation mTransitionAnimation; private final String mPackageName; + private final String mTag; @AnyRes private final int mAnimStyleResId; @@ -57,23 +63,24 @@ class LetterboxEduAnimationController { @Nullable private Animator mBackgroundDimAnimator; - LetterboxEduAnimationController(Context context) { - mTransitionAnimation = new TransitionAnimation(context, /* debug= */ false, TAG); + public DialogAnimationController(Context context, String tag) { + mTransitionAnimation = new TransitionAnimation(context, /* debug= */ false, tag); mAnimStyleResId = (new ContextThemeWrapper(context, android.R.style.ThemeOverlay_Material_Dialog).getTheme()).obtainStyledAttributes( com.android.internal.R.styleable.Window).getResourceId( com.android.internal.R.styleable.Window_windowAnimationStyle, 0); mPackageName = context.getPackageName(); + mTag = tag; } /** * Starts both background dim fade-in animation and the dialog enter animation. */ - void startEnterAnimation(@NonNull LetterboxEduDialogLayout layout, Runnable endCallback) { + public void startEnterAnimation(@NonNull T layout, Runnable endCallback) { // Cancel any previous animation if it's still running. cancelAnimation(); - final View dialogContainer = layout.getDialogContainer(); + final View dialogContainer = layout.getDialogContainerView(); mDialogAnimation = loadAnimation(WindowAnimation_windowEnterAnimation); if (mDialogAnimation == null) { endCallback.run(); @@ -86,8 +93,8 @@ class LetterboxEduAnimationController { endCallback.run(); })); - mBackgroundDimAnimator = getAlphaAnimator(layout.getBackgroundDim(), - /* endAlpha= */ LetterboxEduDialogLayout.BACKGROUND_DIM_ALPHA, + mBackgroundDimAnimator = getAlphaAnimator(layout.getBackgroundDimDrawable(), + /* endAlpha= */ BACKGROUND_DIM_ALPHA, mDialogAnimation.getDuration()); mBackgroundDimAnimator.addListener(getDimAnimatorListener()); @@ -101,11 +108,11 @@ class LetterboxEduAnimationController { /** * Starts both the background dim fade-out animation and the dialog exit animation. */ - void startExitAnimation(@NonNull LetterboxEduDialogLayout layout, Runnable endCallback) { + public void startExitAnimation(@NonNull T layout, Runnable endCallback) { // Cancel any previous animation if it's still running. cancelAnimation(); - final View dialogContainer = layout.getDialogContainer(); + final View dialogContainer = layout.getDialogContainerView(); mDialogAnimation = loadAnimation(WindowAnimation_windowExitAnimation); if (mDialogAnimation == null) { endCallback.run(); @@ -119,8 +126,8 @@ class LetterboxEduAnimationController { endCallback.run(); })); - mBackgroundDimAnimator = getAlphaAnimator(layout.getBackgroundDim(), /* endAlpha= */ 0, - mDialogAnimation.getDuration()); + mBackgroundDimAnimator = getAlphaAnimator(layout.getBackgroundDimDrawable(), + /* endAlpha= */ 0, mDialogAnimation.getDuration()); mBackgroundDimAnimator.addListener(getDimAnimatorListener()); dialogContainer.startAnimation(mDialogAnimation); @@ -130,7 +137,7 @@ class LetterboxEduAnimationController { /** * Cancels all animations and resets the state of the controller. */ - void cancelAnimation() { + public void cancelAnimation() { if (mDialogAnimation != null) { mDialogAnimation.cancel(); mDialogAnimation = null; @@ -145,7 +152,7 @@ class LetterboxEduAnimationController { Animation animation = mTransitionAnimation.loadAnimationAttr(mPackageName, mAnimStyleResId, animAttr, /* translucent= */ false); if (animation == null) { - Log.e(TAG, "Failed to load animation " + animAttr); + Log.e(mTag, "Failed to load animation " + animAttr); } return animation; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/DialogContainerSupplier.java index 60123ab97fd7..7eea446fce26 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutout.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/DialogContainerSupplier.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,23 @@ * limitations under the License. */ -package com.android.wm.shell.hidedisplaycutout; +package com.android.wm.shell.compatui; -import android.content.res.Configuration; - -import androidx.annotation.NonNull; - -import com.android.wm.shell.common.annotations.ExternalThread; - -import java.io.PrintWriter; +import android.graphics.drawable.Drawable; +import android.view.View; /** - * Interface to engage hide display cutout feature. + * A component which can provide a {@link View} to use as a container for a Dialog */ -@ExternalThread -public interface HideDisplayCutout { +public interface DialogContainerSupplier { + + /** + * @return The {@link View} to use as a container for a Dialog + */ + View getDialogContainerView(); + /** - * Notifies {@link Configuration} changed. + * @return The {@link Drawable} to use as background of the dialog. */ - void onConfigurationChanged(Configuration newConfig); + Drawable getBackgroundDimDrawable(); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduDialogActionLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/LetterboxEduDialogActionLayout.java index 02197f644a39..9974295123b7 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduDialogActionLayout.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/LetterboxEduDialogActionLayout.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.compatui.letterboxedu; +package com.android.wm.shell.compatui; import android.content.Context; import android.content.res.TypedArray; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduDialogLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/LetterboxEduDialogLayout.java index 2e0b09e9d230..df2f6ce24ebc 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduDialogLayout.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/LetterboxEduDialogLayout.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.compatui.letterboxedu; +package com.android.wm.shell.compatui; import android.annotation.Nullable; import android.content.Context; @@ -33,11 +33,7 @@ import com.android.wm.shell.R; * <p>This layout should fill the entire task and the background around the dialog acts as the * background dim which dismisses the dialog when clicked. */ -class LetterboxEduDialogLayout extends ConstraintLayout { - - // The alpha of a background is a number between 0 (fully transparent) to 255 (fully opaque). - // 204 is simply 255 * 0.8. - static final int BACKGROUND_DIM_ALPHA = 204; +class LetterboxEduDialogLayout extends ConstraintLayout implements DialogContainerSupplier { private View mDialogContainer; private TextView mDialogTitle; @@ -60,16 +56,18 @@ class LetterboxEduDialogLayout extends ConstraintLayout { super(context, attrs, defStyleAttr, defStyleRes); } - View getDialogContainer() { + @Override + public View getDialogContainerView() { return mDialogContainer; } - TextView getDialogTitle() { - return mDialogTitle; + @Override + public Drawable getBackgroundDimDrawable() { + return mBackgroundDim; } - Drawable getBackgroundDim() { - return mBackgroundDim; + TextView getDialogTitle() { + return mDialogTitle; } /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/LetterboxEduWindowManager.java index 35f1038a6853..0c21c8ccd686 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduWindowManager.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/LetterboxEduWindowManager.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,16 +14,17 @@ * limitations under the License. */ -package com.android.wm.shell.compatui.letterboxedu; +package com.android.wm.shell.compatui; import static android.provider.Settings.Secure.LAUNCHER_TASKBAR_EDUCATION_SHOWING; +import android.annotation.NonNull; import android.annotation.Nullable; import android.app.TaskInfo; import android.content.Context; -import android.content.SharedPreferences; import android.graphics.Rect; import android.provider.Settings; +import android.util.Pair; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup.MarginLayoutParams; @@ -34,14 +35,16 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.wm.shell.R; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.DisplayLayout; +import com.android.wm.shell.common.DockStateReader; import com.android.wm.shell.common.SyncTransactionQueue; -import com.android.wm.shell.compatui.CompatUIWindowManagerAbstract; import com.android.wm.shell.transition.Transitions; +import java.util.function.Consumer; + /** * Window manager for the Letterbox Education. */ -public class LetterboxEduWindowManager extends CompatUIWindowManagerAbstract { +class LetterboxEduWindowManager extends CompatUIWindowManagerAbstract { /** * The Letterbox Education should be the topmost child of the Task in case there can be more @@ -49,20 +52,7 @@ public class LetterboxEduWindowManager extends CompatUIWindowManagerAbstract { */ public static final int Z_ORDER = Integer.MAX_VALUE; - /** - * The name of the {@link SharedPreferences} that holds which user has seen the Letterbox - * Education dialog. - */ - @VisibleForTesting - static final String HAS_SEEN_LETTERBOX_EDUCATION_PREF_NAME = - "has_seen_letterbox_education"; - - /** - * The {@link SharedPreferences} instance for {@link #HAS_SEEN_LETTERBOX_EDUCATION_PREF_NAME}. - */ - private final SharedPreferences mSharedPreferences; - - private final LetterboxEduAnimationController mAnimationController; + private final DialogAnimationController<LetterboxEduDialogLayout> mAnimationController; private final Transitions mTransitions; @@ -73,6 +63,10 @@ public class LetterboxEduWindowManager extends CompatUIWindowManagerAbstract { */ private final int mUserId; + private final Consumer<Pair<TaskInfo, ShellTaskOrganizer.TaskListener>> mOnDismissCallback; + + private final CompatUIConfiguration mCompatUIConfiguration; + // Remember the last reported state in case visibility changes due to keyguard or IME updates. private boolean mEligibleForLetterboxEducation; @@ -80,7 +74,8 @@ public class LetterboxEduWindowManager extends CompatUIWindowManagerAbstract { @VisibleForTesting LetterboxEduDialogLayout mLayout; - private final Runnable mOnDismissCallback; + @NonNull + private TaskInfo mTaskInfo; /** * The vertical margin between the dialog container and the task stable bounds (excluding @@ -88,29 +83,37 @@ public class LetterboxEduWindowManager extends CompatUIWindowManagerAbstract { */ private final int mDialogVerticalMargin; - public LetterboxEduWindowManager(Context context, TaskInfo taskInfo, + private final DockStateReader mDockStateReader; + + LetterboxEduWindowManager(Context context, TaskInfo taskInfo, SyncTransactionQueue syncQueue, ShellTaskOrganizer.TaskListener taskListener, DisplayLayout displayLayout, Transitions transitions, - Runnable onDismissCallback) { + Consumer<Pair<TaskInfo, ShellTaskOrganizer.TaskListener>> onDismissCallback, + DockStateReader dockStateReader, CompatUIConfiguration compatUIConfiguration) { this(context, taskInfo, syncQueue, taskListener, displayLayout, transitions, - onDismissCallback, new LetterboxEduAnimationController(context)); + onDismissCallback, + new DialogAnimationController<>(context, /* tag */ "LetterboxEduWindowManager"), + dockStateReader, compatUIConfiguration); } @VisibleForTesting LetterboxEduWindowManager(Context context, TaskInfo taskInfo, SyncTransactionQueue syncQueue, ShellTaskOrganizer.TaskListener taskListener, - DisplayLayout displayLayout, Transitions transitions, Runnable onDismissCallback, - LetterboxEduAnimationController animationController) { + DisplayLayout displayLayout, Transitions transitions, + Consumer<Pair<TaskInfo, ShellTaskOrganizer.TaskListener>> onDismissCallback, + DialogAnimationController<LetterboxEduDialogLayout> animationController, + DockStateReader dockStateReader, CompatUIConfiguration compatUIConfiguration) { super(context, taskInfo, syncQueue, taskListener, displayLayout); + mTaskInfo = taskInfo; mTransitions = transitions; mOnDismissCallback = onDismissCallback; mAnimationController = animationController; mUserId = taskInfo.userId; - mEligibleForLetterboxEducation = taskInfo.topActivityEligibleForLetterboxEducation; - mSharedPreferences = mContext.getSharedPreferences(HAS_SEEN_LETTERBOX_EDUCATION_PREF_NAME, - Context.MODE_PRIVATE); mDialogVerticalMargin = (int) mContext.getResources().getDimension( R.dimen.letterbox_education_dialog_margin); + mDockStateReader = dockStateReader; + mCompatUIConfiguration = compatUIConfiguration; + mEligibleForLetterboxEducation = taskInfo.topActivityEligibleForLetterboxEducation; } @Override @@ -130,13 +133,15 @@ public class LetterboxEduWindowManager extends CompatUIWindowManagerAbstract { @Override protected boolean eligibleToShowLayout() { + // - The letterbox education should not be visible if the device is docked. // - If taskbar education is showing, the letterbox education shouldn't be shown for the // given task until the taskbar education is dismissed and the compat info changes (then // the controller will create a new instance of this class since this one isn't eligible). // - If the layout isn't null then it was previously showing, and we shouldn't check if the // user has seen the letterbox education before. return mEligibleForLetterboxEducation && !isTaskbarEduShowing() && (mLayout != null - || !getHasSeenLetterboxEducation()); + || !mCompatUIConfiguration.getHasSeenLetterboxEducation(mUserId)) + && !mDockStateReader.isDocked(); } @Override @@ -154,7 +159,7 @@ public class LetterboxEduWindowManager extends CompatUIWindowManagerAbstract { if (mLayout == null) { return; } - final View dialogContainer = mLayout.getDialogContainer(); + final View dialogContainer = mLayout.getDialogContainerView(); MarginLayoutParams marginParams = (MarginLayoutParams) dialogContainer.getLayoutParams(); final Rect taskBounds = getTaskBounds(); @@ -184,7 +189,6 @@ public class LetterboxEduWindowManager extends CompatUIWindowManagerAbstract { // Dialog has already been released. return; } - setSeenLetterboxEducation(); mLayout.setDismissOnClickListener(this::onDismiss); // Focus on the dialog title for accessibility. mLayout.getDialogTitle().sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED); @@ -194,10 +198,11 @@ public class LetterboxEduWindowManager extends CompatUIWindowManagerAbstract { if (mLayout == null) { return; } + mCompatUIConfiguration.setSeenLetterboxEducation(mUserId); mLayout.setDismissOnClickListener(null); mAnimationController.startExitAnimation(mLayout, () -> { release(); - mOnDismissCallback.run(); + mOnDismissCallback.accept(Pair.create(mTaskInfo, getTaskListener())); }); } @@ -210,6 +215,7 @@ public class LetterboxEduWindowManager extends CompatUIWindowManagerAbstract { @Override public boolean updateCompatInfo(TaskInfo taskInfo, ShellTaskOrganizer.TaskListener taskListener, boolean canShow) { + mTaskInfo = taskInfo; mEligibleForLetterboxEducation = taskInfo.topActivityEligibleForLetterboxEducation; return super.updateCompatInfo(taskInfo, taskListener, canShow); @@ -240,18 +246,6 @@ public class LetterboxEduWindowManager extends CompatUIWindowManagerAbstract { taskBounds.height()); } - private boolean getHasSeenLetterboxEducation() { - return mSharedPreferences.getBoolean(getPrefKey(), /* default= */ false); - } - - private void setSeenLetterboxEducation() { - mSharedPreferences.edit().putBoolean(getPrefKey(), true).apply(); - } - - private String getPrefKey() { - return String.valueOf(mUserId); - } - @VisibleForTesting boolean isTaskbarEduShowing() { return Settings.Secure.getInt(mContext.getContentResolver(), diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/ReachabilityEduHandLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/ReachabilityEduHandLayout.java new file mode 100644 index 000000000000..6081ef1ca307 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/ReachabilityEduHandLayout.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.compatui; + +import android.content.Context; +import android.graphics.drawable.Animatable; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.view.View; + +import androidx.appcompat.widget.AppCompatTextView; + +/** + * Custom layout for Reachability Education hand. + */ +public class ReachabilityEduHandLayout extends AppCompatTextView { + + private Drawable mHandDrawable; + + public ReachabilityEduHandLayout(Context context) { + this(context, null); + } + + public ReachabilityEduHandLayout(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public ReachabilityEduHandLayout(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + mHandDrawable = getCompoundDrawables()[/* top */ 1]; + } + + void hide() { + stopAnimation(); + setAlpha(0); + setVisibility(View.INVISIBLE); + } + + void startAnimation() { + if (mHandDrawable instanceof Animatable) { + final Animatable animatedBg = (Animatable) mHandDrawable; + animatedBg.start(); + } + } + + void stopAnimation() { + if (mHandDrawable instanceof Animatable) { + final Animatable animatedBg = (Animatable) mHandDrawable; + animatedBg.stop(); + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/ReachabilityEduLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/ReachabilityEduLayout.java new file mode 100644 index 000000000000..6a72d28521b8 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/ReachabilityEduLayout.java @@ -0,0 +1,278 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.compatui; + +import android.animation.Animator; +import android.animation.ValueAnimator; +import android.app.TaskInfo; +import android.content.Context; +import android.util.AttributeSet; +import android.view.View; +import android.view.animation.AlphaAnimation; +import android.view.animation.Animation; +import android.widget.FrameLayout; + +import com.android.wm.shell.R; + +import java.util.function.BiConsumer; +import java.util.function.Function; + +/** + * Container for reachability education which handles all the show/hide animations. + */ +public class ReachabilityEduLayout extends FrameLayout { + + private static final float ALPHA_FULL_TRANSPARENT = 0f; + + private static final float ALPHA_FULL_OPAQUE = 1f; + + private static final long VISIBILITY_SHOW_ANIMATION_DURATION_MS = 167; + + private static final long VISIBILITY_SHOW_ANIMATION_DELAY_MS = 250; + + private static final long VISIBILITY_SHOW_DOUBLE_TAP_ANIMATION_DELAY_MS = 80; + + private static final long MARGINS_ANIMATION_DURATION_MS = 250; + + private ReachabilityEduWindowManager mWindowManager; + + private ReachabilityEduHandLayout mMoveLeftButton; + private ReachabilityEduHandLayout mMoveRightButton; + private ReachabilityEduHandLayout mMoveUpButton; + private ReachabilityEduHandLayout mMoveDownButton; + + private int mLastLeftMargin = TaskInfo.PROPERTY_VALUE_UNSET; + private int mLastRightMargin = TaskInfo.PROPERTY_VALUE_UNSET; + private int mLastTopMargin = TaskInfo.PROPERTY_VALUE_UNSET; + private int mLastBottomMargin = TaskInfo.PROPERTY_VALUE_UNSET; + + private boolean mIsLayoutActive; + + public ReachabilityEduLayout(Context context) { + this(context, null); + } + + public ReachabilityEduLayout(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public ReachabilityEduLayout(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public ReachabilityEduLayout(Context context, AttributeSet attrs, int defStyleAttr, + int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + void inject(ReachabilityEduWindowManager windowManager) { + mWindowManager = windowManager; + } + + void handleVisibility(boolean isActivityLetterboxed, int letterboxVerticalPosition, + int letterboxHorizontalPosition, int availableWidth, int availableHeight, + boolean isDoubleTap) { + // If the app is not letterboxed we hide all the buttons. + if (!mIsLayoutActive || !isActivityLetterboxed || ( + letterboxHorizontalPosition == TaskInfo.PROPERTY_VALUE_UNSET + && letterboxVerticalPosition == TaskInfo.PROPERTY_VALUE_UNSET)) { + hideAllImmediately(); + } else if (letterboxHorizontalPosition != TaskInfo.PROPERTY_VALUE_UNSET) { + handleLetterboxHorizontalPosition(availableWidth, letterboxHorizontalPosition, + isDoubleTap); + } else { + handleLetterboxVerticalPosition(availableHeight, letterboxVerticalPosition, + isDoubleTap); + } + } + + void hideAllImmediately() { + mMoveLeftButton.hide(); + mMoveRightButton.hide(); + mMoveUpButton.hide(); + mMoveDownButton.hide(); + mLastLeftMargin = TaskInfo.PROPERTY_VALUE_UNSET; + mLastRightMargin = TaskInfo.PROPERTY_VALUE_UNSET; + mLastTopMargin = TaskInfo.PROPERTY_VALUE_UNSET; + mLastBottomMargin = TaskInfo.PROPERTY_VALUE_UNSET; + } + + void setIsLayoutActive(boolean isLayoutActive) { + this.mIsLayoutActive = isLayoutActive; + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mMoveLeftButton = findViewById(R.id.reachability_move_left_button); + mMoveRightButton = findViewById(R.id.reachability_move_right_button); + mMoveUpButton = findViewById(R.id.reachability_move_up_button); + mMoveDownButton = findViewById(R.id.reachability_move_down_button); + mMoveLeftButton.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); + mMoveRightButton.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); + mMoveUpButton.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); + mMoveDownButton.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); + } + + private Animator marginAnimator(View view, Function<LayoutParams, Integer> marginSupplier, + BiConsumer<LayoutParams, Integer> marginConsumer, int from, int to) { + final LayoutParams layoutParams = ((LayoutParams) view.getLayoutParams()); + ValueAnimator animator = ValueAnimator.ofInt(marginSupplier.apply(layoutParams), from, to); + animator.addUpdateListener(valueAnimator -> { + marginConsumer.accept(layoutParams, (Integer) valueAnimator.getAnimatedValue()); + view.requestLayout(); + }); + animator.setDuration(MARGINS_ANIMATION_DURATION_MS); + return animator; + } + + private void handleLetterboxHorizontalPosition(int availableWidth, + int letterboxHorizontalPosition, boolean isDoubleTap) { + mMoveUpButton.hide(); + mMoveDownButton.hide(); + mLastTopMargin = TaskInfo.PROPERTY_VALUE_UNSET; + mLastBottomMargin = TaskInfo.PROPERTY_VALUE_UNSET; + // We calculate the available space on the left and right + final int horizontalGap = availableWidth / 2; + final int leftAvailableSpace = letterboxHorizontalPosition * horizontalGap; + final int rightAvailableSpace = availableWidth - leftAvailableSpace; + // We show the button if we have enough space + if (leftAvailableSpace >= mMoveLeftButton.getMeasuredWidth()) { + int newLeftMargin = (horizontalGap - mMoveLeftButton.getMeasuredWidth()) / 2; + if (mLastLeftMargin == TaskInfo.PROPERTY_VALUE_UNSET) { + mLastLeftMargin = newLeftMargin; + } + if (mLastLeftMargin != newLeftMargin) { + marginAnimator(mMoveLeftButton, layoutParams -> layoutParams.leftMargin, + (layoutParams, margin) -> layoutParams.leftMargin = margin, + mLastLeftMargin, newLeftMargin).start(); + } else { + final LayoutParams leftParams = ((LayoutParams) mMoveLeftButton.getLayoutParams()); + leftParams.leftMargin = mLastLeftMargin; + mMoveLeftButton.setLayoutParams(leftParams); + } + showItem(mMoveLeftButton, isDoubleTap); + } else { + mMoveLeftButton.hide(); + mLastLeftMargin = TaskInfo.PROPERTY_VALUE_UNSET; + } + if (rightAvailableSpace >= mMoveRightButton.getMeasuredWidth()) { + int newRightMargin = (horizontalGap - mMoveRightButton.getMeasuredWidth()) / 2; + if (mLastRightMargin == TaskInfo.PROPERTY_VALUE_UNSET) { + mLastRightMargin = newRightMargin; + } + if (mLastRightMargin != newRightMargin) { + marginAnimator(mMoveRightButton, layoutParams -> layoutParams.rightMargin, + (layoutParams, margin) -> layoutParams.rightMargin = margin, + mLastRightMargin, newRightMargin).start(); + } else { + final LayoutParams rightParams = + ((LayoutParams) mMoveRightButton.getLayoutParams()); + rightParams.rightMargin = mLastRightMargin; + mMoveRightButton.setLayoutParams(rightParams); + } + showItem(mMoveRightButton, isDoubleTap); + } else { + mMoveRightButton.hide(); + mLastRightMargin = TaskInfo.PROPERTY_VALUE_UNSET; + } + } + + private void handleLetterboxVerticalPosition(int availableHeight, + int letterboxVerticalPosition, boolean isDoubleTap) { + mMoveLeftButton.hide(); + mMoveRightButton.hide(); + mLastLeftMargin = TaskInfo.PROPERTY_VALUE_UNSET; + mLastRightMargin = TaskInfo.PROPERTY_VALUE_UNSET; + // We calculate the available space on the left and right + final int verticalGap = availableHeight / 2; + final int topAvailableSpace = letterboxVerticalPosition * verticalGap; + final int bottomAvailableSpace = availableHeight - topAvailableSpace; + if (topAvailableSpace >= mMoveUpButton.getMeasuredHeight()) { + int newTopMargin = (verticalGap - mMoveUpButton.getMeasuredHeight()) / 2; + if (mLastTopMargin == TaskInfo.PROPERTY_VALUE_UNSET) { + mLastTopMargin = newTopMargin; + } + if (mLastTopMargin != newTopMargin) { + marginAnimator(mMoveUpButton, layoutParams -> layoutParams.topMargin, + (layoutParams, margin) -> layoutParams.topMargin = margin, + mLastTopMargin, newTopMargin).start(); + } else { + final LayoutParams topParams = ((LayoutParams) mMoveUpButton.getLayoutParams()); + topParams.topMargin = mLastTopMargin; + mMoveUpButton.setLayoutParams(topParams); + } + showItem(mMoveUpButton, isDoubleTap); + } else { + mMoveUpButton.hide(); + mLastTopMargin = TaskInfo.PROPERTY_VALUE_UNSET; + } + if (bottomAvailableSpace >= mMoveDownButton.getMeasuredHeight()) { + int newBottomMargin = (verticalGap - mMoveDownButton.getMeasuredHeight()) / 2; + if (mLastBottomMargin == TaskInfo.PROPERTY_VALUE_UNSET) { + mLastBottomMargin = newBottomMargin; + } + if (mLastBottomMargin != newBottomMargin) { + marginAnimator(mMoveDownButton, layoutParams -> layoutParams.bottomMargin, + (layoutParams, margin) -> layoutParams.bottomMargin = margin, + mLastBottomMargin, newBottomMargin).start(); + } else { + final LayoutParams bottomParams = + ((LayoutParams) mMoveDownButton.getLayoutParams()); + bottomParams.bottomMargin = mLastBottomMargin; + mMoveDownButton.setLayoutParams(bottomParams); + } + showItem(mMoveDownButton, isDoubleTap); + } else { + mMoveDownButton.hide(); + mLastBottomMargin = TaskInfo.PROPERTY_VALUE_UNSET; + } + } + + private void showItem(ReachabilityEduHandLayout view, boolean fromDoubleTap) { + if (view.getVisibility() == View.VISIBLE) { + // Already visible we just start animation + view.startAnimation(); + return; + } + view.setVisibility(View.VISIBLE); + final long delay = fromDoubleTap ? VISIBILITY_SHOW_DOUBLE_TAP_ANIMATION_DELAY_MS + : VISIBILITY_SHOW_ANIMATION_DELAY_MS; + AlphaAnimation alphaAnimation = new AlphaAnimation(ALPHA_FULL_TRANSPARENT, + ALPHA_FULL_OPAQUE); + alphaAnimation.setDuration(VISIBILITY_SHOW_ANIMATION_DURATION_MS); + alphaAnimation.setStartOffset(delay); + alphaAnimation.setFillAfter(true); + alphaAnimation.setAnimationListener(new Animation.AnimationListener() { + @Override + public void onAnimationStart(Animation animation) { + } + + @Override + public void onAnimationEnd(Animation animation) { + // We trigger the hand animation + view.setAlpha(ALPHA_FULL_OPAQUE); + view.startAnimation(); + } + + @Override + public void onAnimationRepeat(Animation animation) { + } + }); + view.startAnimation(alphaAnimation); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/ReachabilityEduWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/ReachabilityEduWindowManager.java new file mode 100644 index 000000000000..f1b098ef27c7 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/ReachabilityEduWindowManager.java @@ -0,0 +1,278 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.compatui; + +import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; +import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.TaskInfo; +import android.content.Context; +import android.graphics.Rect; +import android.os.SystemClock; +import android.view.LayoutInflater; +import android.view.View; +import android.view.WindowManager; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.wm.shell.R; +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.common.DisplayLayout; +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.SyncTransactionQueue; +import com.android.wm.shell.compatui.CompatUIController.CompatUICallback; + +/** + * Window manager for the reachability education + */ +class ReachabilityEduWindowManager extends CompatUIWindowManagerAbstract { + + /** + * The Compat UI should be below the Letterbox Education. + */ + private static final int Z_ORDER = LetterboxEduWindowManager.Z_ORDER - 1; + + // The time to wait before hiding the education + private static final long DISAPPEAR_DELAY_MS = 4000L; + + private final CompatUICallback mCallback; + + private final CompatUIConfiguration mCompatUIConfiguration; + + private final ShellExecutor mMainExecutor; + + @NonNull + private TaskInfo mTaskInfo; + + private boolean mIsActivityLetterboxed; + + private int mLetterboxVerticalPosition; + + private int mLetterboxHorizontalPosition; + + private int mTopActivityLetterboxWidth; + + private int mTopActivityLetterboxHeight; + + private long mNextHideTime = -1L; + + private boolean mForceUpdate = false; + + // We decided to force the visualization of the double-tap animated icons every time the user + // double-taps. + private boolean mHasUserDoubleTapped; + + // When the size of the letterboxed app changes and the icons are visible + // we need to animate them. + private boolean mHasLetterboxSizeChanged; + + @Nullable + @VisibleForTesting + ReachabilityEduLayout mLayout; + + ReachabilityEduWindowManager(Context context, TaskInfo taskInfo, + SyncTransactionQueue syncQueue, CompatUICallback callback, + ShellTaskOrganizer.TaskListener taskListener, DisplayLayout displayLayout, + CompatUIConfiguration compatUIConfiguration, ShellExecutor mainExecutor) { + super(context, taskInfo, syncQueue, taskListener, displayLayout); + mCallback = callback; + mTaskInfo = taskInfo; + mIsActivityLetterboxed = taskInfo.isLetterboxDoubleTapEnabled; + mLetterboxVerticalPosition = taskInfo.topActivityLetterboxVerticalPosition; + mLetterboxHorizontalPosition = taskInfo.topActivityLetterboxHorizontalPosition; + mTopActivityLetterboxWidth = taskInfo.topActivityLetterboxWidth; + mTopActivityLetterboxHeight = taskInfo.topActivityLetterboxHeight; + mCompatUIConfiguration = compatUIConfiguration; + mMainExecutor = mainExecutor; + } + + @Override + protected int getZOrder() { + return Z_ORDER; + } + + @Override + protected @Nullable View getLayout() { + return mLayout; + } + + @Override + protected void removeLayout() { + mLayout = null; + } + + @Override + protected boolean eligibleToShowLayout() { + return mCompatUIConfiguration.isReachabilityEducationEnabled() + && mIsActivityLetterboxed + && (mLetterboxVerticalPosition != -1 || mLetterboxHorizontalPosition != -1); + } + + @Override + protected View createLayout() { + mLayout = inflateLayout(); + mLayout.inject(this); + + updateVisibilityOfViews(); + + return mLayout; + } + + @VisibleForTesting + ReachabilityEduLayout inflateLayout() { + return (ReachabilityEduLayout) LayoutInflater.from(mContext).inflate( + R.layout.reachability_ui_layout, null); + } + + @Override + public boolean updateCompatInfo(TaskInfo taskInfo, ShellTaskOrganizer.TaskListener taskListener, + boolean canShow) { + mTaskInfo = taskInfo; + final boolean prevIsActivityLetterboxed = mIsActivityLetterboxed; + final int prevLetterboxVerticalPosition = mLetterboxVerticalPosition; + final int prevLetterboxHorizontalPosition = mLetterboxHorizontalPosition; + final int prevTopActivityLetterboxWidth = mTopActivityLetterboxWidth; + final int prevTopActivityLetterboxHeight = mTopActivityLetterboxHeight; + mIsActivityLetterboxed = taskInfo.isLetterboxDoubleTapEnabled; + mLetterboxVerticalPosition = taskInfo.topActivityLetterboxVerticalPosition; + mLetterboxHorizontalPosition = taskInfo.topActivityLetterboxHorizontalPosition; + mTopActivityLetterboxWidth = taskInfo.topActivityLetterboxWidth; + mTopActivityLetterboxHeight = taskInfo.topActivityLetterboxHeight; + mHasUserDoubleTapped = taskInfo.isFromLetterboxDoubleTap; + + if (taskInfo.isFromLetterboxDoubleTap) { + // In this case we disable the reachability for the following launch of + // the current application. Anyway because a double tap event happened, + // the reachability education is displayed + mCompatUIConfiguration.setDontShowReachabilityEducationAgain(taskInfo); + } + if (!super.updateCompatInfo(taskInfo, taskListener, canShow)) { + return false; + } + + mHasLetterboxSizeChanged = prevTopActivityLetterboxWidth != mTopActivityLetterboxWidth + || prevTopActivityLetterboxHeight != mTopActivityLetterboxHeight; + + if (mForceUpdate || prevIsActivityLetterboxed != mIsActivityLetterboxed + || prevLetterboxVerticalPosition != mLetterboxVerticalPosition + || prevLetterboxHorizontalPosition != mLetterboxHorizontalPosition + || prevTopActivityLetterboxWidth != mTopActivityLetterboxWidth + || prevTopActivityLetterboxHeight != mTopActivityLetterboxHeight) { + updateVisibilityOfViews(); + mForceUpdate = false; + } + + return true; + } + + void forceUpdate(boolean forceUpdate) { + mForceUpdate = forceUpdate; + } + + @Override + protected void onParentBoundsChanged() { + if (mLayout == null) { + return; + } + // Both the layout dimensions and dialog margins depend on the parent bounds. + WindowManager.LayoutParams windowLayoutParams = getWindowLayoutParams(); + mLayout.setLayoutParams(windowLayoutParams); + relayout(windowLayoutParams); + } + + /** Gets the layout params. */ + protected WindowManager.LayoutParams getWindowLayoutParams() { + View layout = getLayout(); + if (layout == null) { + return new WindowManager.LayoutParams(); + } + // Measure how big the hint is since its size depends on the text size. + final Rect taskBounds = getTaskBounds(); + layout.measure(View.MeasureSpec.makeMeasureSpec(taskBounds.width(), + View.MeasureSpec.EXACTLY), + View.MeasureSpec.makeMeasureSpec(taskBounds.height(), + View.MeasureSpec.EXACTLY)); + return getWindowLayoutParams(layout.getMeasuredWidth(), layout.getMeasuredHeight()); + } + + /** + * @return Flags to use for the WindowManager layout + */ + @Override + protected int getWindowManagerLayoutParamsFlags() { + return FLAG_NOT_FOCUSABLE | FLAG_NOT_TOUCHABLE; + } + + @Override + @VisibleForTesting + public void updateSurfacePosition() { + if (mLayout == null) { + return; + } + updateSurfacePosition(0, 0); + } + + void updateHideTime() { + mNextHideTime = SystemClock.uptimeMillis() + DISAPPEAR_DELAY_MS; + } + + private void updateVisibilityOfViews() { + if (mLayout == null) { + return; + } + if (shouldUpdateEducation()) { + if (!mHasLetterboxSizeChanged) { + mLayout.setIsLayoutActive(true); + } + int availableWidth = getTaskBounds().width() - mTopActivityLetterboxWidth; + int availableHeight = getTaskBounds().height() - mTopActivityLetterboxHeight; + mLayout.handleVisibility(mIsActivityLetterboxed, mLetterboxVerticalPosition, + mLetterboxHorizontalPosition, availableWidth, availableHeight, + mHasUserDoubleTapped); + if (!mHasLetterboxSizeChanged) { + updateHideTime(); + mMainExecutor.executeDelayed(this::hideReachability, DISAPPEAR_DELAY_MS); + } + mHasUserDoubleTapped = false; + } else { + hideReachability(); + } + } + + private void hideReachability() { + if (mLayout != null) { + mLayout.setIsLayoutActive(false); + } + if (mLayout == null || !shouldHideEducation()) { + return; + } + mLayout.hideAllImmediately(); + // We need this in case the icons disappear after the timeout without an explicit + // double tap of the user. + mCompatUIConfiguration.setDontShowReachabilityEducationAgain(mTaskInfo); + } + + private boolean shouldUpdateEducation() { + return mForceUpdate || mHasUserDoubleTapped || mHasLetterboxSizeChanged + || mCompatUIConfiguration.shouldShowReachabilityEducation(mTaskInfo); + } + + private boolean shouldHideEducation() { + return SystemClock.uptimeMillis() >= mNextHideTime; + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/RestartDialogLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/RestartDialogLayout.java new file mode 100644 index 000000000000..c53e6389331a --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/RestartDialogLayout.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.compatui; + +import android.annotation.Nullable; +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.view.View; +import android.widget.CheckBox; +import android.widget.TextView; + +import androidx.constraintlayout.widget.ConstraintLayout; + +import com.android.wm.shell.R; + +import java.util.function.Consumer; + +/** + * Container for a SCM restart confirmation dialog and background dim. + */ +public class RestartDialogLayout extends ConstraintLayout implements DialogContainerSupplier { + + private View mDialogContainer; + private TextView mDialogTitle; + private Drawable mBackgroundDim; + + public RestartDialogLayout(Context context) { + this(context, null); + } + + public RestartDialogLayout(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public RestartDialogLayout(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public RestartDialogLayout(Context context, AttributeSet attrs, int defStyleAttr, + int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + @Override + public View getDialogContainerView() { + return mDialogContainer; + } + + TextView getDialogTitle() { + return mDialogTitle; + } + + @Override + public Drawable getBackgroundDimDrawable() { + return mBackgroundDim; + } + + /** + * Register a callback for the dismiss button and background dim. + * + * @param callback The callback to register or null if all on click listeners should be removed. + */ + void setDismissOnClickListener(@Nullable Runnable callback) { + final OnClickListener listener = callback == null ? null : view -> callback.run(); + findViewById(R.id.letterbox_restart_dialog_dismiss_button).setOnClickListener(listener); + } + + /** + * Register a callback for the restart button + * + * @param callback The callback to register or null if all on click listeners should be removed. + */ + void setRestartOnClickListener(@Nullable Consumer<Boolean> callback) { + final CheckBox dontShowAgainCheckbox = findViewById(R.id.letterbox_restart_dialog_checkbox); + final OnClickListener listener = callback == null ? null : view -> callback.accept( + dontShowAgainCheckbox.isChecked()); + findViewById(R.id.letterbox_restart_dialog_restart_button).setOnClickListener(listener); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mDialogContainer = findViewById(R.id.letterbox_restart_dialog_container); + mDialogTitle = findViewById(R.id.letterbox_restart_dialog_title); + mBackgroundDim = getBackground().mutate(); + // Set the alpha of the background dim to 0 for enter animation. + mBackgroundDim.setAlpha(0); + // We add a no-op on-click listener to the dialog container so that clicks on it won't + // propagate to the listener of the layout (which represents the background dim). + mDialogContainer.setOnClickListener(view -> {}); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/RestartDialogWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/RestartDialogWindowManager.java new file mode 100644 index 000000000000..aab123a843ea --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/RestartDialogWindowManager.java @@ -0,0 +1,264 @@ +/* + * 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.compatui; + +import static android.provider.Settings.Secure.LAUNCHER_TASKBAR_EDUCATION_SHOWING; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.TaskInfo; +import android.content.Context; +import android.graphics.Rect; +import android.provider.Settings; +import android.util.Pair; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.view.accessibility.AccessibilityEvent; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.wm.shell.R; +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.common.DisplayLayout; +import com.android.wm.shell.common.SyncTransactionQueue; +import com.android.wm.shell.transition.Transitions; + +import java.util.function.Consumer; + +/** + * Window manager for the Restart Dialog. + * + * TODO(b/263484314): Create abstraction of RestartDialogWindowManager and LetterboxEduWindowManager + */ +class RestartDialogWindowManager extends CompatUIWindowManagerAbstract { + + /** + * The restart dialog should be the topmost child of the Task in case there can be more + * than one child. + */ + private static final int Z_ORDER = Integer.MAX_VALUE; + + private final DialogAnimationController<RestartDialogLayout> mAnimationController; + + private final Transitions mTransitions; + + // Remember the last reported state in case visibility changes due to keyguard or IME updates. + private boolean mRequestRestartDialog; + + private final Consumer<Pair<TaskInfo, ShellTaskOrganizer.TaskListener>> mOnDismissCallback; + + private final Consumer<Pair<TaskInfo, ShellTaskOrganizer.TaskListener>> mOnRestartCallback; + + private final CompatUIConfiguration mCompatUIConfiguration; + + /** + * The vertical margin between the dialog container and the task stable bounds (excluding + * insets). + */ + private final int mDialogVerticalMargin; + + @NonNull + private TaskInfo mTaskInfo; + + @Nullable + @VisibleForTesting + RestartDialogLayout mLayout; + + RestartDialogWindowManager(Context context, TaskInfo taskInfo, + SyncTransactionQueue syncQueue, ShellTaskOrganizer.TaskListener taskListener, + DisplayLayout displayLayout, Transitions transitions, + Consumer<Pair<TaskInfo, ShellTaskOrganizer.TaskListener>> onRestartCallback, + Consumer<Pair<TaskInfo, ShellTaskOrganizer.TaskListener>> onDismissCallback, + CompatUIConfiguration compatUIConfiguration) { + this(context, taskInfo, syncQueue, taskListener, displayLayout, transitions, + onRestartCallback, onDismissCallback, + new DialogAnimationController<>(context, "RestartDialogWindowManager"), + compatUIConfiguration); + } + + @VisibleForTesting + RestartDialogWindowManager(Context context, TaskInfo taskInfo, + SyncTransactionQueue syncQueue, ShellTaskOrganizer.TaskListener taskListener, + DisplayLayout displayLayout, Transitions transitions, + Consumer<Pair<TaskInfo, ShellTaskOrganizer.TaskListener>> onRestartCallback, + Consumer<Pair<TaskInfo, ShellTaskOrganizer.TaskListener>> onDismissCallback, + DialogAnimationController<RestartDialogLayout> animationController, + CompatUIConfiguration compatUIConfiguration) { + super(context, taskInfo, syncQueue, taskListener, displayLayout); + mTaskInfo = taskInfo; + mTransitions = transitions; + mOnDismissCallback = onDismissCallback; + mOnRestartCallback = onRestartCallback; + mAnimationController = animationController; + mDialogVerticalMargin = (int) mContext.getResources().getDimension( + R.dimen.letterbox_restart_dialog_margin); + mCompatUIConfiguration = compatUIConfiguration; + } + + @Override + protected int getZOrder() { + return Z_ORDER; + } + + @Override + @Nullable + protected View getLayout() { + return mLayout; + } + + @Override + protected void removeLayout() { + mLayout = null; + } + + @Override + protected boolean eligibleToShowLayout() { + // We don't show this dialog if the user has explicitly selected so clicking on a checkbox. + return mRequestRestartDialog && !isTaskbarEduShowing() && (mLayout != null + || mCompatUIConfiguration.shouldShowRestartDialogAgain(mTaskInfo)); + } + + @Override + protected View createLayout() { + mLayout = inflateLayout(); + updateDialogMargins(); + + // startEnterAnimation will be called immediately if shell-transitions are disabled. + mTransitions.runOnIdle(this::startEnterAnimation); + + return mLayout; + } + + void setRequestRestartDialog(boolean enabled) { + mRequestRestartDialog = enabled; + } + + @Override + public boolean updateCompatInfo(TaskInfo taskInfo, ShellTaskOrganizer.TaskListener taskListener, + boolean canShow) { + mTaskInfo = taskInfo; + return super.updateCompatInfo(taskInfo, taskListener, canShow); + } + + boolean needsToBeRecreated(TaskInfo taskInfo, ShellTaskOrganizer.TaskListener taskListener) { + return taskInfo.configuration.uiMode != mTaskInfo.configuration.uiMode + || !getTaskListener().equals(taskListener); + } + + private void updateDialogMargins() { + if (mLayout == null) { + return; + } + final View dialogContainer = mLayout.getDialogContainerView(); + ViewGroup.MarginLayoutParams marginParams = + (ViewGroup.MarginLayoutParams) dialogContainer.getLayoutParams(); + + final Rect taskBounds = getTaskBounds(); + final Rect taskStableBounds = getTaskStableBounds(); + + marginParams.topMargin = taskStableBounds.top - taskBounds.top + mDialogVerticalMargin; + marginParams.bottomMargin = + taskBounds.bottom - taskStableBounds.bottom + mDialogVerticalMargin; + dialogContainer.setLayoutParams(marginParams); + } + + private RestartDialogLayout inflateLayout() { + return (RestartDialogLayout) LayoutInflater.from(mContext).inflate( + R.layout.letterbox_restart_dialog_layout, null); + } + + private void startEnterAnimation() { + if (mLayout == null) { + // Dialog has already been released. + return; + } + mAnimationController.startEnterAnimation(mLayout, /* endCallback= */ + this::onDialogEnterAnimationEnded); + } + + private void onDialogEnterAnimationEnded() { + if (mLayout == null) { + // Dialog has already been released. + return; + } + mLayout.setDismissOnClickListener(this::onDismiss); + mLayout.setRestartOnClickListener(dontShowAgain -> { + if (mLayout != null) { + mLayout.setDismissOnClickListener(null); + mAnimationController.startExitAnimation(mLayout, () -> { + release(); + }); + } + if (dontShowAgain) { + mCompatUIConfiguration.setDontShowRestartDialogAgain(mTaskInfo); + } + mOnRestartCallback.accept(Pair.create(mTaskInfo, getTaskListener())); + }); + // Focus on the dialog title for accessibility. + mLayout.getDialogTitle().sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED); + } + + private void onDismiss() { + if (mLayout == null) { + return; + } + + mLayout.setDismissOnClickListener(null); + mAnimationController.startExitAnimation(mLayout, () -> { + release(); + mOnDismissCallback.accept(Pair.create(mTaskInfo, getTaskListener())); + }); + } + + @Override + public void release() { + mAnimationController.cancelAnimation(); + super.release(); + } + + @Override + protected void onParentBoundsChanged() { + if (mLayout == null) { + return; + } + // Both the layout dimensions and dialog margins depend on the parent bounds. + WindowManager.LayoutParams windowLayoutParams = getWindowLayoutParams(); + mLayout.setLayoutParams(windowLayoutParams); + updateDialogMargins(); + relayout(windowLayoutParams); + } + + @Override + protected void updateSurfacePosition() { + // Nothing to do, since the position of the surface is fixed to the top left corner (0,0) + // of the task (parent surface), which is the default position of a surface. + } + + @Override + protected WindowManager.LayoutParams getWindowLayoutParams() { + final Rect taskBounds = getTaskBounds(); + return getWindowLayoutParams(/* width= */ taskBounds.width(), /* height= */ + taskBounds.height()); + } + + @VisibleForTesting + boolean isTaskbarEduShowing() { + return Settings.Secure.getInt(mContext.getContentResolver(), + LAUNCHER_TASKBAR_EDUCATION_SHOWING, /* def= */ 0) == 1; + } +} 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..14daae03e6b5 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 @@ -31,6 +31,7 @@ import com.android.wm.shell.common.annotations.ShellMainThread; import com.android.wm.shell.pip.Pip; import com.android.wm.shell.pip.PipAnimationController; import com.android.wm.shell.pip.PipAppOpsListener; +import com.android.wm.shell.pip.PipDisplayLayoutState; import com.android.wm.shell.pip.PipMediaController; import com.android.wm.shell.pip.PipParamsChangedForwarder; import com.android.wm.shell.pip.PipSnapAlgorithm; @@ -39,6 +40,7 @@ import com.android.wm.shell.pip.PipTaskOrganizer; import com.android.wm.shell.pip.PipTransitionController; import com.android.wm.shell.pip.PipTransitionState; import com.android.wm.shell.pip.PipUiEventLogger; +import com.android.wm.shell.pip.phone.PipSizeSpecHandler; import com.android.wm.shell.pip.tv.TvPipBoundsAlgorithm; import com.android.wm.shell.pip.tv.TvPipBoundsController; import com.android.wm.shell.pip.tv.TvPipBoundsState; @@ -48,13 +50,15 @@ 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; - import dagger.Module; import dagger.Provides; +import java.util.Optional; + /** * Provides TV specific dependencies for Pip. */ @@ -64,7 +68,10 @@ public abstract class TvPipModule { @Provides static Optional<Pip> providePip( Context context, + ShellInit shellInit, + ShellController shellController, TvPipBoundsState tvPipBoundsState, + PipDisplayLayoutState pipDisplayLayoutState, TvPipBoundsAlgorithm tvPipBoundsAlgorithm, TvPipBoundsController tvPipBoundsController, PipAppOpsListener pipAppOpsListener, @@ -77,11 +84,15 @@ public abstract class TvPipModule { PipParamsChangedForwarder pipParamsChangedForwarder, DisplayController displayController, WindowManagerShellWrapper windowManagerShellWrapper, + @ShellMainThread Handler mainHandler, // needed for registerReceiverForAllUsers() @ShellMainThread ShellExecutor mainExecutor) { return Optional.of( TvPipController.create( context, + shellInit, + shellController, tvPipBoundsState, + pipDisplayLayoutState, tvPipBoundsAlgorithm, tvPipBoundsController, pipAppOpsListener, @@ -94,6 +105,7 @@ public abstract class TvPipModule { pipParamsChangedForwarder, displayController, windowManagerShellWrapper, + mainHandler, mainExecutor)); } @@ -121,26 +133,45 @@ public abstract class TvPipModule { @WMSingleton @Provides static TvPipBoundsAlgorithm provideTvPipBoundsAlgorithm(Context context, - TvPipBoundsState tvPipBoundsState, PipSnapAlgorithm pipSnapAlgorithm) { - return new TvPipBoundsAlgorithm(context, tvPipBoundsState, pipSnapAlgorithm); + TvPipBoundsState tvPipBoundsState, PipSnapAlgorithm pipSnapAlgorithm, + PipSizeSpecHandler pipSizeSpecHandler) { + return new TvPipBoundsAlgorithm(context, tvPipBoundsState, pipSnapAlgorithm, + pipSizeSpecHandler); } @WMSingleton @Provides - static TvPipBoundsState provideTvPipBoundsState(Context context) { - return new TvPipBoundsState(context); + static TvPipBoundsState provideTvPipBoundsState(Context context, + PipSizeSpecHandler pipSizeSpecHandler, PipDisplayLayoutState pipDisplayLayoutState) { + return new TvPipBoundsState(context, pipSizeSpecHandler, pipDisplayLayoutState); + } + + @WMSingleton + @Provides + static PipSizeSpecHandler providePipSizeSpecHelper(Context context, + PipDisplayLayoutState pipDisplayLayoutState) { + return new PipSizeSpecHandler(context, pipDisplayLayoutState); } // Handler needed for loadDrawableAsync() in PipControlsViewController @WMSingleton @Provides static PipTransitionController provideTvPipTransition( - Transitions transitions, ShellTaskOrganizer shellTaskOrganizer, - PipAnimationController pipAnimationController, + Context context, + ShellInit shellInit, + ShellTaskOrganizer shellTaskOrganizer, + Transitions transitions, + TvPipBoundsState tvPipBoundsState, + PipDisplayLayoutState pipDisplayLayoutState, + PipTransitionState pipTransitionState, + TvPipMenuController pipMenuController, TvPipBoundsAlgorithm tvPipBoundsAlgorithm, - TvPipBoundsState tvPipBoundsState, TvPipMenuController pipMenuController) { - return new TvPipTransition(tvPipBoundsState, pipMenuController, - tvPipBoundsAlgorithm, pipAnimationController, transitions, shellTaskOrganizer); + PipAnimationController pipAnimationController, + PipSurfaceTransactionHelper pipSurfaceTransactionHelper) { + return new TvPipTransition(context, shellInit, shellTaskOrganizer, transitions, + tvPipBoundsState, pipDisplayLayoutState, pipTransitionState, pipMenuController, + tvPipBoundsAlgorithm, pipAnimationController, pipSurfaceTransactionHelper, + Optional.empty()); } @WMSingleton @@ -149,22 +180,17 @@ public abstract class TvPipModule { Context context, TvPipBoundsState tvPipBoundsState, SystemWindows systemWindows, - PipMediaController pipMediaController, @ShellMainThread Handler mainHandler) { - return new TvPipMenuController(context, tvPipBoundsState, systemWindows, pipMediaController, - mainHandler); + return new TvPipMenuController(context, tvPipBoundsState, systemWindows, mainHandler); } - // Handler needed for registerReceiverForAllUsers() @WMSingleton @Provides static TvPipNotificationController provideTvPipNotificationController(Context context, PipMediaController pipMediaController, - PipParamsChangedForwarder pipParamsChangedForwarder, - TvPipBoundsState tvPipBoundsState, - @ShellMainThread Handler mainHandler) { + PipParamsChangedForwarder pipParamsChangedForwarder) { return new TvPipNotificationController(context, pipMediaController, - pipParamsChangedForwarder, tvPipBoundsState, mainHandler); + pipParamsChangedForwarder); } @WMSingleton @@ -186,6 +212,7 @@ public abstract class TvPipModule { TvPipMenuController tvPipMenuController, SyncTransactionQueue syncTransactionQueue, TvPipBoundsState tvPipBoundsState, + PipDisplayLayoutState pipDisplayLayoutState, PipTransitionState pipTransitionState, TvPipBoundsAlgorithm tvPipBoundsAlgorithm, PipAnimationController pipAnimationController, @@ -197,10 +224,11 @@ public abstract class TvPipModule { PipUiEventLogger pipUiEventLogger, ShellTaskOrganizer shellTaskOrganizer, @ShellMainThread ShellExecutor mainExecutor) { return new TvPipTaskOrganizer(context, - syncTransactionQueue, pipTransitionState, tvPipBoundsState, tvPipBoundsAlgorithm, - tvPipMenuController, pipAnimationController, pipSurfaceTransactionHelper, - pipTransitionController, pipParamsChangedForwarder, splitScreenControllerOptional, - displayController, pipUiEventLogger, shellTaskOrganizer, mainExecutor); + syncTransactionQueue, pipTransitionState, tvPipBoundsState, pipDisplayLayoutState, + tvPipBoundsAlgorithm, tvPipMenuController, pipAnimationController, + pipSurfaceTransactionHelper, pipTransitionController, pipParamsChangedForwarder, + splitScreenControllerOptional, displayController, pipUiEventLogger, + shellTaskOrganizer, mainExecutor); } @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..9808c591592f 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,51 +27,49 @@ 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.R; import com.android.wm.shell.RootDisplayAreaOrganizer; import com.android.wm.shell.RootTaskDisplayAreaOrganizer; -import com.android.wm.shell.ShellCommandHandler; -import com.android.wm.shell.ShellCommandHandlerImpl; -import com.android.wm.shell.ShellInit; -import com.android.wm.shell.ShellInitImpl; import com.android.wm.shell.ShellTaskOrganizer; -import com.android.wm.shell.TaskViewFactory; -import com.android.wm.shell.TaskViewFactoryController; -import com.android.wm.shell.TaskViewTransitions; import com.android.wm.shell.WindowManagerShellWrapper; -import com.android.wm.shell.apppairs.AppPairs; -import com.android.wm.shell.apppairs.AppPairsController; +import com.android.wm.shell.activityembedding.ActivityEmbeddingController; import com.android.wm.shell.back.BackAnimation; +import com.android.wm.shell.back.BackAnimationBackground; import com.android.wm.shell.back.BackAnimationController; import com.android.wm.shell.bubbles.BubbleController; import com.android.wm.shell.bubbles.Bubbles; +import com.android.wm.shell.common.DevicePostureController; 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.DockStateReader; import com.android.wm.shell.common.FloatingContentCoordinator; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.common.SystemWindows; +import com.android.wm.shell.common.TabletopModeController; import com.android.wm.shell.common.TaskStackListenerImpl; import com.android.wm.shell.common.TransactionPool; import com.android.wm.shell.common.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.CompatUIConfiguration; import com.android.wm.shell.compatui.CompatUIController; +import com.android.wm.shell.compatui.CompatUIShellCommandHandler; +import com.android.wm.shell.desktopmode.DesktopMode; +import com.android.wm.shell.desktopmode.DesktopModeController; +import com.android.wm.shell.desktopmode.DesktopModeStatus; +import com.android.wm.shell.desktopmode.DesktopModeTaskRepository; +import com.android.wm.shell.desktopmode.DesktopTasksController; 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; @@ -81,26 +79,34 @@ import com.android.wm.shell.pip.PipUiEventLogger; import com.android.wm.shell.pip.phone.PipTouchHandler; import com.android.wm.shell.recents.RecentTasks; import com.android.wm.shell.recents.RecentTasksController; +import com.android.wm.shell.recents.RecentsTransitionHandler; import com.android.wm.shell.splitscreen.SplitScreen; import com.android.wm.shell.splitscreen.SplitScreenController; 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.taskview.TaskViewFactory; +import com.android.wm.shell.taskview.TaskViewFactoryController; +import com.android.wm.shell.taskview.TaskViewTransitions; 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 java.util.Optional; +import com.android.wm.shell.windowdecor.WindowDecorViewModel; import dagger.BindsOptionalOf; import dagger.Lazy; import dagger.Module; import dagger.Provides; +import java.util.Optional; + /** * Provides basic dependencies from {@link com.android.wm.shell}, these dependencies are only * accessible from components within the WM subcomponent (can be explicitly exposed to the @@ -120,38 +126,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 @@ -162,57 +164,71 @@ 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); + static DevicePostureController provideDevicePostureController( + Context context, + ShellInit shellInit, + @ShellMainThread ShellExecutor mainExecutor + ) { + return new DevicePostureController(context, shellInit, mainExecutor); } @WMSingleton @Provides - static Optional<DragAndDrop> provideDragAndDrop(DragAndDropController dragAndDropController) { - return Optional.of(dragAndDropController.asDragAndDrop()); + static TabletopModeController provideTabletopModeController( + Context context, + ShellInit shellInit, + DevicePostureController postureController, + DisplayController displayController, + @ShellMainThread ShellExecutor mainExecutor) { + return new TabletopModeController( + context, shellInit, postureController, displayController, mainExecutor); } @WMSingleton @Provides - static ShellTaskOrganizer provideShellTaskOrganizer(@ShellMainThread ShellExecutor mainExecutor, - Context context, - CompatUIController compatUI, - Optional<RecentTasksController> recentTasksOptional - ) { - return new ShellTaskOrganizer(mainExecutor, context, compatUI, recentTasksOptional); + static DragAndDropController provideDragAndDropController(Context context, + 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 KidsModeTaskOrganizer provideKidsModeTaskOrganizer( - @ShellMainThread ShellExecutor mainExecutor, - @ShellMainThread Handler mainHandler, + static ShellTaskOrganizer provideShellTaskOrganizer( Context context, - SyncTransactionQueue syncTransactionQueue, - DisplayController displayController, - DisplayInsetsController displayInsetsController, - Optional<RecentTasksController> recentTasksOptional + ShellInit shellInit, + ShellCommandHandler shellCommandHandler, + CompatUIController compatUI, + Optional<UnfoldAnimationController> unfoldAnimationController, + Optional<RecentTasksController> recentTasksOptional, + @ShellMainThread ShellExecutor mainExecutor ) { - return new KidsModeTaskOrganizer(mainExecutor, mainHandler, context, syncTransactionQueue, - displayController, displayInsetsController, recentTasksOptional); - } - - @WMSingleton - @Provides static Optional<CompatUI> provideCompatUI(CompatUIController compatUIController) { - return Optional.of(compatUIController.asCompatUI()); + if (!context.getResources().getBoolean(R.bool.config_registerShellTaskOrganizerOnInit)) { + // TODO(b/238217847): Force override shell init if registration is disabled + shellInit = new ShellInit(mainExecutor); + } + return new ShellTaskOrganizer(shellInit, shellCommandHandler, compatUI, + unfoldAnimationController, recentTasksOptional, mainExecutor); } @WMSingleton @Provides static 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); + @ShellMainThread ShellExecutor mainExecutor, Lazy<Transitions> transitionsLazy, + DockStateReader dockStateReader, CompatUIConfiguration compatUIConfiguration, + CompatUIShellCommandHandler compatUIShellCommandHandler) { + return new CompatUIController(context, shellInit, shellController, displayController, + displayInsetsController, imeController, syncQueue, mainExecutor, transitionsLazy, + dockStateReader, compatUIConfiguration, compatUIShellCommandHandler); } @WMSingleton @@ -267,6 +283,32 @@ public abstract class WMShellBaseModule { return backAnimationController.map(BackAnimationController::getBackAnimationImpl); } + @WMSingleton + @Provides + static BackAnimationBackground provideBackAnimationBackground( + RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer) { + return new BackAnimationBackground(rootTaskDisplayAreaOrganizer); + } + + @WMSingleton + @Provides + static Optional<BackAnimationController> provideBackAnimationController( + Context context, + ShellInit shellInit, + ShellController shellController, + @ShellMainThread ShellExecutor shellExecutor, + @ShellBackgroundThread Handler backgroundHandler, + BackAnimationBackground backAnimationBackground + ) { + if (BackAnimationController.IS_ENABLED) { + return Optional.of( + new BackAnimationController(shellInit, shellController, shellExecutor, + backgroundHandler, context, backAnimationBackground)); + } + return Optional.empty(); + } + + // // Bubbles (optional feature) // @@ -293,18 +335,27 @@ public abstract class WMShellBaseModule { @Provides 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 +365,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 +403,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 +422,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 +459,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 +478,8 @@ public abstract class WMShellBaseModule { @WMSingleton @Provides - static PipSurfaceTransactionHelper providePipSurfaceTransactionHelper() { - return new PipSurfaceTransactionHelper(); + static PipSurfaceTransactionHelper providePipSurfaceTransactionHelper(Context context) { + return new PipSurfaceTransactionHelper(context); } @WMSingleton @@ -473,13 +507,23 @@ public abstract class WMShellBaseModule { @Provides static Optional<RecentTasksController> provideRecentTasksController( Context context, + ShellInit shellInit, + ShellController shellController, + ShellCommandHandler shellCommandHandler, TaskStackListenerImpl taskStackListener, + ActivityTaskManager activityTaskManager, + Optional<DesktopModeTaskRepository> desktopModeTaskRepository, @ShellMainThread ShellExecutor mainExecutor ) { return Optional.ofNullable( - RecentTasksController.create(context, taskStackListener, mainExecutor)); + RecentTasksController.create(context, shellInit, shellController, + shellCommandHandler, taskStackListener, activityTaskManager, + desktopModeTaskRepository, mainExecutor)); } + @BindsOptionalOf + abstract RecentsTransitionHandler optionalRecentsTransitionHandler(); + // // Shell transitions // @@ -492,13 +536,21 @@ public abstract class WMShellBaseModule { @WMSingleton @Provides - static Transitions provideTransitions(ShellTaskOrganizer organizer, TransactionPool pool, - DisplayController displayController, Context context, + static Transitions provideTransitions(Context context, + ShellInit shellInit, + ShellController shellController, + ShellTaskOrganizer organizer, + TransactionPool pool, + DisplayController displayController, @ShellMainThread ShellExecutor mainExecutor, @ShellMainThread Handler mainHandler, @ShellAnimationThread ShellExecutor animExecutor) { - return new Transitions(organizer, pool, displayController, context, mainExecutor, - mainHandler, animExecutor); + if (!context.getResources().getBoolean(R.bool.config_registerShellTransitionsOnInit)) { + // TODO(b/238217847): Force override shell init if registration is disabled + shellInit = new ShellInit(mainExecutor); + } + return new Transitions(context, shellInit, shellController, organizer, pool, + displayController, mainExecutor, mainHandler, animExecutor); } @WMSingleton @@ -561,29 +613,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 // @@ -597,12 +626,16 @@ public abstract class WMShellBaseModule { @WMSingleton @Provides - static StartingWindowController provideStartingWindowController(Context context, + static StartingWindowController provideStartingWindowController( + Context context, + ShellInit shellInit, + ShellController shellController, + ShellTaskOrganizer shellTaskOrganizer, @ShellSplashscreenThread ShellExecutor splashScreenExecutor, StartingWindowTypeAlgorithm startingWindowTypeAlgorithm, IconProvider iconProvider, TransactionPool pool) { - return new StartingWindowController(context, splashScreenExecutor, - startingWindowTypeAlgorithm, iconProvider, pool); + return new StartingWindowController(context, shellInit, shellController, shellTaskOrganizer, + splashScreenExecutor, startingWindowTypeAlgorithm, iconProvider, pool); } // Workaround for dynamic overriding with a default implementation, see {@link DynamicOverride} @@ -644,95 +677,164 @@ public abstract class WMShellBaseModule { taskViewTransitions); } + // - // Misc + // ActivityEmbedding + // + + @WMSingleton + @Provides + static Optional<ActivityEmbeddingController> provideActivityEmbeddingController( + Context context, + ShellInit shellInit, + Transitions transitions) { + return Optional.ofNullable( + ActivityEmbeddingController.create(context, shellInit, transitions)); + } + // + // SysUI -> Shell interface + // + + @WMSingleton + @Provides + static ShellInterface provideShellSysuiCallbacks( + @ShellCreateTrigger Object createTrigger, + ShellController shellController) { + return shellController.asShell(); + } @WMSingleton @Provides - static ShellInit provideShellInit(ShellInitImpl impl) { - return impl.asShellInit(); + static ShellController provideShellController(ShellInit shellInit, + ShellCommandHandler shellCommandHandler, + @ShellMainThread ShellExecutor mainExecutor) { + return new ShellController(shellInit, shellCommandHandler, mainExecutor); } + // + // Desktop mode (optional feature) + // + @WMSingleton @Provides - static ShellInitImpl provideShellInitImpl(DisplayController displayController, + static Optional<DesktopMode> provideDesktopMode( + Optional<DesktopModeController> desktopModeController, + Optional<DesktopTasksController> desktopTasksController) { + if (DesktopModeStatus.isProto2Enabled()) { + return desktopTasksController.map(DesktopTasksController::asDesktopMode); + } + return desktopModeController.map(DesktopModeController::asDesktopMode); + } + + @BindsOptionalOf + @DynamicOverride + abstract DesktopModeController optionalDesktopModeController(); + + @WMSingleton + @Provides + static Optional<DesktopModeController> provideDesktopModeController( + @DynamicOverride Optional<Lazy<DesktopModeController>> desktopModeController) { + // Use optional-of-lazy for the dependency that this provider relies on. + // Lazy ensures that this provider will not be the cause the dependency is created + // when it will not be returned due to the condition below. + if (DesktopModeStatus.isProto1Enabled()) { + return desktopModeController.map(Lazy::get); + } + return Optional.empty(); + } + + @BindsOptionalOf + @DynamicOverride + abstract DesktopTasksController optionalDesktopTasksController(); + + @WMSingleton + @Provides + static Optional<DesktopTasksController> providesDesktopTasksController( + @DynamicOverride Optional<Lazy<DesktopTasksController>> desktopTasksController) { + // Use optional-of-lazy for the dependency that this provider relies on. + // Lazy ensures that this provider will not be the cause the dependency is created + // when it will not be returned due to the condition below. + if (DesktopModeStatus.isProto2Enabled()) { + return desktopTasksController.map(Lazy::get); + } + return Optional.empty(); + } + + @BindsOptionalOf + @DynamicOverride + abstract DesktopModeTaskRepository optionalDesktopModeTaskRepository(); + + @WMSingleton + @Provides + static Optional<DesktopModeTaskRepository> provideDesktopTaskRepository( + @DynamicOverride Optional<Lazy<DesktopModeTaskRepository>> desktopModeTaskRepository) { + // Use optional-of-lazy for the dependency that this provider relies on. + // Lazy ensures that this provider will not be the cause the dependency is created + // when it will not be returned due to the condition below. + if (DesktopModeStatus.isAnyEnabled()) { + return desktopModeTaskRepository.map(Lazy::get); + } + 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, ShellTaskOrganizer shellTaskOrganizer, - KidsModeTaskOrganizer kidsModeTaskOrganizer, Optional<BubbleController> bubblesOptional, Optional<SplitScreenController> splitScreenOptional, - Optional<AppPairsController> appPairsOptional, + Optional<Pip> pipOptional, Optional<PipTouchHandler> pipTouchHandlerOptional, FullscreenTaskListener fullscreenTaskListener, - Optional<FullscreenUnfoldController> appUnfoldTransitionController, + Optional<UnfoldAnimationController> unfoldAnimationController, Optional<UnfoldTransitionHandler> unfoldTransitionHandler, - Optional<FreeformTaskListener> freeformTaskListener, + Optional<FreeformComponents> freeformComponents, Optional<RecentTasksController> recentTasksOptional, + Optional<RecentsTransitionHandler> recentsTransitionHandlerOptional, + 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..d8e2f5c4a817 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java @@ -16,11 +16,12 @@ 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.IWindowManager; import android.view.WindowManager; import com.android.internal.jank.InteractionJankMonitor; @@ -29,10 +30,12 @@ import com.android.internal.statusbar.IStatusBarService; import com.android.launcher3.icons.IconProvider; 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; @@ -41,21 +44,30 @@ import com.android.wm.shell.common.FloatingContentCoordinator; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.common.SystemWindows; +import com.android.wm.shell.common.TabletopModeController; import com.android.wm.shell.common.TaskStackListenerImpl; import com.android.wm.shell.common.TransactionPool; -import com.android.wm.shell.common.annotations.ChoreographerSfVsync; import com.android.wm.shell.common.annotations.ShellBackgroundThread; import com.android.wm.shell.common.annotations.ShellMainThread; +import com.android.wm.shell.desktopmode.DesktopModeController; +import com.android.wm.shell.desktopmode.DesktopModeStatus; +import com.android.wm.shell.desktopmode.DesktopModeTaskRepository; +import com.android.wm.shell.desktopmode.DesktopTasksController; +import com.android.wm.shell.desktopmode.EnterDesktopTaskTransitionHandler; +import com.android.wm.shell.desktopmode.ExitDesktopTaskTransitionHandler; 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.kidsmode.KidsModeTaskOrganizer; import com.android.wm.shell.onehanded.OneHandedController; import com.android.wm.shell.pip.Pip; import com.android.wm.shell.pip.PipAnimationController; import com.android.wm.shell.pip.PipAppOpsListener; import com.android.wm.shell.pip.PipBoundsAlgorithm; import com.android.wm.shell.pip.PipBoundsState; +import com.android.wm.shell.pip.PipDisplayLayoutState; import com.android.wm.shell.pip.PipMediaController; import com.android.wm.shell.pip.PipParamsChangedForwarder; import com.android.wm.shell.pip.PipSnapAlgorithm; @@ -65,25 +77,43 @@ 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.PipSizeSpecHandler; import com.android.wm.shell.pip.phone.PipTouchHandler; import com.android.wm.shell.recents.RecentTasksController; +import com.android.wm.shell.recents.RecentsTransitionHandler; 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.taskview.TaskViewTransitions; +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 java.util.Optional; - -import javax.inject.Provider; - +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.DesktopModeWindowDecorViewModel; +import com.android.wm.shell.windowdecor.WindowDecorViewModel; + +import dagger.Binds; import dagger.Lazy; import dagger.Module; import dagger.Provides; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + /** * Provides dependencies from {@link com.android.wm.shell}, these dependencies are only * accessible from components within the WM subcomponent (can be explicitly exposed to the @@ -93,16 +123,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 +166,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, @@ -119,13 +176,52 @@ public class WMShellModule { @ShellMainThread Handler mainHandler, @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, - taskViewTransitions, syncQueue); + SyncTransactionQueue syncQueue, + IWindowManager wmService) { + 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, wmService); + } + + // + // Window decoration + // + + @WMSingleton + @Provides + static WindowDecorViewModel provideWindowDecorViewModel( + Context context, + @ShellMainThread Handler mainHandler, + @ShellMainThread Choreographer mainChoreographer, + ShellTaskOrganizer taskOrganizer, + DisplayController displayController, + SyncTransactionQueue syncQueue, + Optional<DesktopModeController> desktopModeController, + Optional<DesktopTasksController> desktopTasksController, + Optional<SplitScreenController> splitScreenController) { + if (DesktopModeStatus.isAnyEnabled()) { + return new DesktopModeWindowDecorViewModel( + context, + mainHandler, + mainChoreographer, + taskOrganizer, + displayController, + syncQueue, + desktopModeController, + desktopTasksController, + splitScreenController); + } + return new CaptionWindowDecorViewModel( + context, + mainHandler, + mainChoreographer, + taskOrganizer, + displayController, + syncQueue); } // @@ -135,9 +231,49 @@ public class WMShellModule { @WMSingleton @Provides @DynamicOverride + static FreeformComponents provideFreeformComponents( + FreeformTaskListener taskListener, + FreeformTaskTransitionHandler transitionHandler, + FreeformTaskTransitionObserver transitionObserver) { + return new FreeformComponents( + taskListener, Optional.of(transitionHandler), Optional.of(transitionObserver)); + } + + @WMSingleton + @Provides static FreeformTaskListener provideFreeformTaskListener( - SyncTransactionQueue syncQueue) { - return new FreeformTaskListener(syncQueue); + 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, + WindowDecorViewModel windowDecorViewModel) { + return new FreeformTaskTransitionObserver( + context, shellInit, transitions, windowDecorViewModel); } // @@ -150,12 +286,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 +310,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,27 +338,47 @@ public class WMShellModule { @WMSingleton @Provides - static Optional<Pip> providePip(Context context, DisplayController displayController, - PipAppOpsListener pipAppOpsListener, PipBoundsAlgorithm pipBoundsAlgorithm, - PipBoundsState pipBoundsState, PipMediaController pipMediaController, - PhonePipMenuController phonePipMenuController, PipTaskOrganizer pipTaskOrganizer, - PipTouchHandler pipTouchHandler, PipTransitionController pipTransitionController, + static Optional<Pip> providePip(Context context, + ShellInit shellInit, + ShellCommandHandler shellCommandHandler, + ShellController shellController, + DisplayController displayController, + PipAnimationController pipAnimationController, + PipAppOpsListener pipAppOpsListener, + PipBoundsAlgorithm pipBoundsAlgorithm, + PhonePipKeepClearAlgorithm pipKeepClearAlgorithm, + PipBoundsState pipBoundsState, + PipSizeSpecHandler pipSizeSpecHandler, + PipDisplayLayoutState pipDisplayLayoutState, + PipMotionHelper pipMotionHelper, + PipMediaController pipMediaController, + PhonePipMenuController phonePipMenuController, + PipTaskOrganizer pipTaskOrganizer, + PipTransitionState pipTransitionState, + PipTouchHandler pipTouchHandler, + PipTransitionController pipTransitionController, WindowManagerShellWrapper windowManagerShellWrapper, TaskStackListenerImpl taskStackListener, PipParamsChangedForwarder pipParamsChangedForwarder, + DisplayInsetsController displayInsetsController, + TabletopModeController pipTabletopController, Optional<OneHandedController> oneHandedController, @ShellMainThread ShellExecutor mainExecutor) { - return Optional.ofNullable(PipController.create(context, displayController, - pipAppOpsListener, pipBoundsAlgorithm, pipBoundsState, - pipMediaController, phonePipMenuController, pipTaskOrganizer, - pipTouchHandler, pipTransitionController, windowManagerShellWrapper, - taskStackListener, pipParamsChangedForwarder, oneHandedController, mainExecutor)); + return Optional.ofNullable(PipController.create( + context, shellInit, shellCommandHandler, shellController, + displayController, pipAnimationController, pipAppOpsListener, pipBoundsAlgorithm, + pipKeepClearAlgorithm, pipBoundsState, pipSizeSpecHandler, pipDisplayLayoutState, + pipMotionHelper, pipMediaController, phonePipMenuController, pipTaskOrganizer, + pipTransitionState, pipTouchHandler, pipTransitionController, + windowManagerShellWrapper, taskStackListener, pipParamsChangedForwarder, + displayInsetsController, pipTabletopController, oneHandedController, mainExecutor)); } @WMSingleton @Provides - static PipBoundsState providePipBoundsState(Context context) { - return new PipBoundsState(context); + static PipBoundsState providePipBoundsState(Context context, + PipSizeSpecHandler pipSizeSpecHandler, PipDisplayLayoutState pipDisplayLayoutState) { + return new PipBoundsState(context, pipSizeSpecHandler, pipDisplayLayoutState); } @WMSingleton @@ -244,9 +389,25 @@ public class WMShellModule { @WMSingleton @Provides + static PhonePipKeepClearAlgorithm providePhonePipKeepClearAlgorithm(Context context) { + return new PhonePipKeepClearAlgorithm(context); + } + + @WMSingleton + @Provides + static PipSizeSpecHandler providePipSizeSpecHelper(Context context, + PipDisplayLayoutState pipDisplayLayoutState) { + return new PipSizeSpecHandler(context, pipDisplayLayoutState); + } + + @WMSingleton + @Provides static PipBoundsAlgorithm providesPipBoundsAlgorithm(Context context, - PipBoundsState pipBoundsState, PipSnapAlgorithm pipSnapAlgorithm) { - return new PipBoundsAlgorithm(context, pipBoundsState, pipSnapAlgorithm); + PipBoundsState pipBoundsState, PipSnapAlgorithm pipSnapAlgorithm, + PhonePipKeepClearAlgorithm pipKeepClearAlgorithm, + PipSizeSpecHandler pipSizeSpecHandler) { + return new PipBoundsAlgorithm(context, pipBoundsState, pipSnapAlgorithm, + pipKeepClearAlgorithm, pipSizeSpecHandler); } // Handler is used by Icon.loadDrawableAsync @@ -266,15 +427,18 @@ public class WMShellModule { @WMSingleton @Provides static PipTouchHandler providePipTouchHandler(Context context, - PhonePipMenuController menuPhoneController, PipBoundsAlgorithm pipBoundsAlgorithm, + ShellInit shellInit, + PhonePipMenuController menuPhoneController, + PipBoundsAlgorithm pipBoundsAlgorithm, PipBoundsState pipBoundsState, + PipSizeSpecHandler pipSizeSpecHandler, PipTaskOrganizer pipTaskOrganizer, PipMotionHelper pipMotionHelper, FloatingContentCoordinator floatingContentCoordinator, PipUiEventLogger pipUiEventLogger, @ShellMainThread ShellExecutor mainExecutor) { - return new PipTouchHandler(context, menuPhoneController, pipBoundsAlgorithm, - pipBoundsState, pipTaskOrganizer, pipMotionHelper, + return new PipTouchHandler(context, shellInit, menuPhoneController, pipBoundsAlgorithm, + pipBoundsState, pipSizeSpecHandler, pipTaskOrganizer, pipMotionHelper, floatingContentCoordinator, pipUiEventLogger, mainExecutor); } @@ -290,6 +454,7 @@ public class WMShellModule { SyncTransactionQueue syncTransactionQueue, PipTransitionState pipTransitionState, PipBoundsState pipBoundsState, + PipDisplayLayoutState pipDisplayLayoutState, PipBoundsAlgorithm pipBoundsAlgorithm, PhonePipMenuController menuPhoneController, PipAnimationController pipAnimationController, @@ -301,10 +466,11 @@ public class WMShellModule { PipUiEventLogger pipUiEventLogger, ShellTaskOrganizer shellTaskOrganizer, @ShellMainThread ShellExecutor mainExecutor) { return new PipTaskOrganizer(context, - syncTransactionQueue, pipTransitionState, pipBoundsState, pipBoundsAlgorithm, - menuPhoneController, pipAnimationController, pipSurfaceTransactionHelper, - pipTransitionController, pipParamsChangedForwarder, splitScreenControllerOptional, - displayController, pipUiEventLogger, shellTaskOrganizer, mainExecutor); + syncTransactionQueue, pipTransitionState, pipBoundsState, pipDisplayLayoutState, + pipBoundsAlgorithm, menuPhoneController, pipAnimationController, + pipSurfaceTransactionHelper, pipTransitionController, pipParamsChangedForwarder, + splitScreenControllerOptional, displayController, pipUiEventLogger, + shellTaskOrganizer, mainExecutor); } @WMSingleton @@ -317,15 +483,16 @@ 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, + PipBoundsState pipBoundsState, PipDisplayLayoutState pipDisplayLayoutState, + 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, pipDisplayLayoutState, pipTransitionState, pipMenuController, + pipBoundsAlgorithm, pipAnimationController, pipSurfaceTransactionHelper, + splitScreenOptional); } @WMSingleton @@ -348,6 +515,38 @@ 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, + Optional<RecentsTransitionHandler> recentsTransitionHandler, + Transitions transitions) { + return new DefaultMixedHandler(shellInit, transitions, splitScreenOptional, + pipTouchHandlerOptional, recentsTransitionHandler); + } + + @WMSingleton + @Provides + static RecentsTransitionHandler provideRecentsTransitionHandler( + ShellInit shellInit, + Transitions transitions, + Optional<RecentTasksController> recentTasksController) { + return new RecentsTransitionHandler(shellInit, transitions, + recentTasksController.orElse(null)); + } + // // Unfold transition // @@ -355,36 +554,82 @@ 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(), - displayInsetsController); + final List<UnfoldTaskAnimator> animators = new ArrayList<>(); + animators.add(splitAnimator); + animators.add(fullscreenAnimator); + + return new UnfoldAnimationController( + shellInit, + transactionPool, + progressProvider.get(), + animators, + unfoldTransitionHandler, + mainExecutor + ); } @Provides - static Optional<StageTaskUnfoldController> provideStageTaskUnfoldController( - Optional<ShellUnfoldProgressProvider> progressProvider, + static FullscreenUnfoldTaskAnimator provideFullscreenUnfoldTaskAnimator( Context context, - TransactionPool transactionPool, - Lazy<UnfoldBackgroundController> unfoldBackgroundController, - DisplayInsetsController displayInsetsController, - @ShellMainThread ShellExecutor mainExecutor + UnfoldBackgroundController unfoldBackgroundController, + ShellController shellController, + DisplayInsetsController displayInsetsController ) { - return progressProvider.map(shellUnfoldTransitionProgressProvider -> - new StageTaskUnfoldController( - context, - transactionPool, - shellUnfoldTransitionProgressProvider, - displayInsetsController, - unfoldBackgroundController.get(), - mainExecutor - )); + return new FullscreenUnfoldTaskAnimator(context, unfoldBackgroundController, + shellController, displayInsetsController); + } + + @Provides + static SplitTaskUnfoldAnimator provideSplitTaskUnfoldAnimatorBase( + Context context, + UnfoldBackgroundController backgroundController, + ShellController shellController, + @ShellMainThread ShellExecutor executor, + Lazy<Optional<SplitScreenController>> splitScreenOptional, + DisplayInsetsController displayInsetsController + ) { + // 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, + shellController, 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 +644,109 @@ public class WMShellModule { ); } + // + // Desktop mode (optional feature) + // + @WMSingleton @Provides - static PipParamsChangedForwarder providePipParamsChangedForwarder() { - return new PipParamsChangedForwarder(); + @DynamicOverride + static DesktopModeController provideDesktopModeController(Context context, + ShellInit shellInit, + ShellController shellController, + ShellTaskOrganizer shellTaskOrganizer, + RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, + Transitions transitions, + @DynamicOverride DesktopModeTaskRepository desktopModeTaskRepository, + @ShellMainThread Handler mainHandler, + @ShellMainThread ShellExecutor mainExecutor + ) { + return new DesktopModeController(context, shellInit, shellController, shellTaskOrganizer, + rootTaskDisplayAreaOrganizer, transitions, desktopModeTaskRepository, mainHandler, + mainExecutor); + } + + @WMSingleton + @Provides + @DynamicOverride + static DesktopTasksController provideDesktopTasksController( + Context context, + ShellInit shellInit, + ShellController shellController, + DisplayController displayController, + ShellTaskOrganizer shellTaskOrganizer, + SyncTransactionQueue syncQueue, + RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, + Transitions transitions, + EnterDesktopTaskTransitionHandler enterDesktopTransitionHandler, + ExitDesktopTaskTransitionHandler exitDesktopTransitionHandler, + @DynamicOverride DesktopModeTaskRepository desktopModeTaskRepository, + @ShellMainThread ShellExecutor mainExecutor + ) { + return new DesktopTasksController(context, shellInit, shellController, displayController, + shellTaskOrganizer, syncQueue, rootTaskDisplayAreaOrganizer, transitions, + enterDesktopTransitionHandler, exitDesktopTransitionHandler, + desktopModeTaskRepository, mainExecutor); + } + + @WMSingleton + @Provides + static EnterDesktopTaskTransitionHandler provideEnterDesktopModeTaskTransitionHandler( + Transitions transitions) { + return new EnterDesktopTaskTransitionHandler(transitions); + } + + @WMSingleton + @Provides + static ExitDesktopTaskTransitionHandler provideExitDesktopTaskTransitionHandler( + Transitions transitions, + Context context + ) { + return new ExitDesktopTaskTransitionHandler(transitions, context); + } + + @WMSingleton + @Provides + @DynamicOverride + static DesktopModeTaskRepository provideDesktopModeTaskRepository() { + return new DesktopModeTaskRepository(); + } + + // + // Kids mode + // + @WMSingleton + @Provides + static KidsModeTaskOrganizer provideKidsModeTaskOrganizer( + Context context, + ShellInit shellInit, + ShellCommandHandler shellCommandHandler, + SyncTransactionQueue syncTransactionQueue, + DisplayController displayController, + DisplayInsetsController displayInsetsController, + Optional<UnfoldAnimationController> unfoldAnimationController, + Optional<RecentTasksController> recentTasksOptional, + @ShellMainThread ShellExecutor mainExecutor, + @ShellMainThread Handler mainHandler + ) { + return new KidsModeTaskOrganizer(context, shellInit, shellCommandHandler, + syncTransactionQueue, displayController, displayInsetsController, + unfoldAnimationController, recentTasksOptional, mainExecutor, mainHandler); + } + + // + // 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, + KidsModeTaskOrganizer kidsModeTaskOrganizer, + Optional<DesktopModeController> desktopModeController) { + return new Object(); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUI.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java index b87cf47dd93f..cbd544cc4b86 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUI.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.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,22 +14,25 @@ * limitations under the License. */ -package com.android.wm.shell.compatui; +package com.android.wm.shell.desktopmode; import com.android.wm.shell.common.annotations.ExternalThread; +import java.util.concurrent.Executor; + /** - * Interface to engage compat UI. + * Interface to interact with desktop mode feature in shell. */ @ExternalThread -public interface CompatUI { +public interface DesktopMode { + /** - * Called when the keyguard showing state changes. Removes all compat UIs if the - * keyguard is now showing. + * Adds a listener to find out about changes in the visibility of freeform tasks. * - * <p>Note that if the keyguard is occluded it will also be considered showing. - * - * @param showing indicates if the keyguard is now showing. + * @param listener the listener to add. + * @param callbackExecutor the executor to call the listener on. */ - void onKeyguardShowingChanged(boolean showing); + void addListener(DesktopModeTaskRepository.VisibleTasksListener listener, + Executor callbackExecutor); + } 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..2bdbde1b71d4 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java @@ -0,0 +1,473 @@ +/* + * 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.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_UNDEFINED; +import static android.view.WindowManager.TRANSIT_CHANGE; +import static android.view.WindowManager.TRANSIT_NONE; +import static android.view.WindowManager.TRANSIT_OPEN; +import static android.view.WindowManager.TRANSIT_TO_FRONT; + +import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission; +import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE; +import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_DESKTOP_MODE; + +import android.app.ActivityManager.RunningTaskInfo; +import android.app.WindowConfiguration; +import android.content.Context; +import android.database.ContentObserver; +import android.net.Uri; +import android.os.Handler; +import android.os.IBinder; +import android.os.RemoteException; +import android.os.UserHandle; +import android.provider.Settings; +import android.util.ArraySet; +import android.view.SurfaceControl; +import android.view.WindowManager; +import android.window.DisplayAreaInfo; +import android.window.TransitionInfo; +import android.window.TransitionRequestInfo; +import android.window.WindowContainerTransaction; + +import androidx.annotation.BinderThread; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.protolog.common.ProtoLog; +import com.android.wm.shell.RootTaskDisplayAreaOrganizer; +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.common.ExternalInterfaceBinder; +import com.android.wm.shell.common.RemoteCallable; +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.annotations.ExternalThread; +import com.android.wm.shell.common.annotations.ShellMainThread; +import com.android.wm.shell.sysui.ShellController; +import com.android.wm.shell.sysui.ShellInit; +import com.android.wm.shell.transition.Transitions; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.concurrent.Executor; + +/** + * Handles windowing changes when desktop mode system setting changes + */ +public class DesktopModeController implements RemoteCallable<DesktopModeController>, + Transitions.TransitionHandler { + + private final Context mContext; + private final ShellController mShellController; + private final ShellTaskOrganizer mShellTaskOrganizer; + private final RootTaskDisplayAreaOrganizer mRootTaskDisplayAreaOrganizer; + private final Transitions mTransitions; + private final DesktopModeTaskRepository mDesktopModeTaskRepository; + private final ShellExecutor mMainExecutor; + private final DesktopModeImpl mDesktopModeImpl = new DesktopModeImpl(); + private final SettingsObserver mSettingsObserver; + + public DesktopModeController(Context context, + ShellInit shellInit, + ShellController shellController, + ShellTaskOrganizer shellTaskOrganizer, + RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, + Transitions transitions, + DesktopModeTaskRepository desktopModeTaskRepository, + @ShellMainThread Handler mainHandler, + @ShellMainThread ShellExecutor mainExecutor) { + mContext = context; + mShellController = shellController; + mShellTaskOrganizer = shellTaskOrganizer; + mRootTaskDisplayAreaOrganizer = rootTaskDisplayAreaOrganizer; + mTransitions = transitions; + mDesktopModeTaskRepository = desktopModeTaskRepository; + mMainExecutor = mainExecutor; + mSettingsObserver = new SettingsObserver(mContext, mainHandler); + if (DesktopModeStatus.isProto1Enabled()) { + shellInit.addInitCallback(this::onInit, this); + } + } + + private void onInit() { + ProtoLog.d(WM_SHELL_DESKTOP_MODE, "Initialize DesktopModeController"); + mShellController.addExternalInterface(KEY_EXTRA_SHELL_DESKTOP_MODE, + this::createExternalInterface, this); + mSettingsObserver.observe(); + if (DesktopModeStatus.isActive(mContext)) { + updateDesktopModeActive(true); + } + mTransitions.addHandler(this); + } + + @Override + public Context getContext() { + return mContext; + } + + @Override + public ShellExecutor getRemoteCallExecutor() { + return mMainExecutor; + } + + /** + * Get connection interface between sysui and shell + */ + public DesktopMode asDesktopMode() { + return mDesktopModeImpl; + } + + /** + * Creates a new instance of the external interface to pass to another process. + */ + private ExternalInterfaceBinder createExternalInterface() { + return new IDesktopModeImpl(this); + } + + /** + * Adds a listener to find out about changes in the visibility of freeform tasks. + * + * @param listener the listener to add. + * @param callbackExecutor the executor to call the listener on. + */ + public void addListener(DesktopModeTaskRepository.VisibleTasksListener listener, + Executor callbackExecutor) { + mDesktopModeTaskRepository.addVisibleTasksListener(listener, callbackExecutor); + } + + @VisibleForTesting + void updateDesktopModeActive(boolean active) { + ProtoLog.d(WM_SHELL_DESKTOP_MODE, "updateDesktopModeActive: active=%s", active); + + int displayId = mContext.getDisplayId(); + + ArrayList<RunningTaskInfo> runningTasks = mShellTaskOrganizer.getRunningTasks(displayId); + + WindowContainerTransaction wct = new WindowContainerTransaction(); + // Reset freeform windowing mode that is set per task level so tasks inherit it + clearFreeformForStandardTasks(runningTasks, wct); + if (active) { + moveHomeBehindVisibleTasks(runningTasks, wct); + setDisplayAreaWindowingMode(displayId, WINDOWING_MODE_FREEFORM, wct); + } else { + clearBoundsForStandardTasks(runningTasks, wct); + setDisplayAreaWindowingMode(displayId, WINDOWING_MODE_FULLSCREEN, wct); + } + if (Transitions.ENABLE_SHELL_TRANSITIONS) { + mTransitions.startTransition(TRANSIT_CHANGE, wct, null); + } else { + mRootTaskDisplayAreaOrganizer.applyTransaction(wct); + } + } + + private WindowContainerTransaction clearBoundsForStandardTasks( + ArrayList<RunningTaskInfo> runningTasks, WindowContainerTransaction wct) { + ProtoLog.v(WM_SHELL_DESKTOP_MODE, "prepareClearBoundsForTasks"); + for (RunningTaskInfo taskInfo : runningTasks) { + if (taskInfo.getActivityType() == ACTIVITY_TYPE_STANDARD) { + ProtoLog.v(WM_SHELL_DESKTOP_MODE, "clearing bounds for token=%s taskInfo=%s", + taskInfo.token, taskInfo); + wct.setBounds(taskInfo.token, null); + } + } + return wct; + } + + private void clearFreeformForStandardTasks(ArrayList<RunningTaskInfo> runningTasks, + WindowContainerTransaction wct) { + ProtoLog.v(WM_SHELL_DESKTOP_MODE, "prepareClearFreeformForTasks"); + for (RunningTaskInfo taskInfo : runningTasks) { + if (taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM + && taskInfo.getActivityType() == ACTIVITY_TYPE_STANDARD) { + ProtoLog.v(WM_SHELL_DESKTOP_MODE, + "clearing windowing mode for token=%s taskInfo=%s", taskInfo.token, + taskInfo); + wct.setWindowingMode(taskInfo.token, WINDOWING_MODE_UNDEFINED); + } + } + } + + private void moveHomeBehindVisibleTasks(ArrayList<RunningTaskInfo> runningTasks, + WindowContainerTransaction wct) { + ProtoLog.v(WM_SHELL_DESKTOP_MODE, "moveHomeBehindVisibleTasks"); + RunningTaskInfo homeTask = null; + ArrayList<RunningTaskInfo> visibleTasks = new ArrayList<>(); + for (RunningTaskInfo taskInfo : runningTasks) { + if (taskInfo.getActivityType() == ACTIVITY_TYPE_HOME) { + homeTask = taskInfo; + } else if (taskInfo.getActivityType() == ACTIVITY_TYPE_STANDARD + && taskInfo.isVisible()) { + visibleTasks.add(taskInfo); + } + } + if (homeTask == null) { + ProtoLog.w(WM_SHELL_DESKTOP_MODE, "moveHomeBehindVisibleTasks: home task not found"); + } else { + ProtoLog.v(WM_SHELL_DESKTOP_MODE, "moveHomeBehindVisibleTasks: visible tasks %d", + visibleTasks.size()); + wct.reorder(homeTask.getToken(), true /* onTop */); + for (RunningTaskInfo task : visibleTasks) { + wct.reorder(task.getToken(), true /* onTop */); + } + } + } + + private void setDisplayAreaWindowingMode(int displayId, + @WindowConfiguration.WindowingMode int windowingMode, WindowContainerTransaction wct) { + DisplayAreaInfo displayAreaInfo = mRootTaskDisplayAreaOrganizer.getDisplayAreaInfo( + displayId); + if (displayAreaInfo == null) { + ProtoLog.e(WM_SHELL_DESKTOP_MODE, + "unable to update windowing mode for display %d display not found", displayId); + return; + } + + ProtoLog.v(WM_SHELL_DESKTOP_MODE, + "setWindowingMode: displayId=%d current wmMode=%d new wmMode=%d", displayId, + displayAreaInfo.configuration.windowConfiguration.getWindowingMode(), + windowingMode); + + wct.setWindowingMode(displayAreaInfo.token, windowingMode); + } + + /** + * Show apps on desktop + */ + void showDesktopApps() { + // Bring apps to front, ignoring their visibility status to always ensure they are on top. + WindowContainerTransaction wct = new WindowContainerTransaction(); + bringDesktopAppsToFront(wct); + + if (!wct.isEmpty()) { + if (Transitions.ENABLE_SHELL_TRANSITIONS) { + // TODO(b/268662477): add animation for the transition + mTransitions.startTransition(TRANSIT_NONE, wct, null /* handler */); + } else { + mShellTaskOrganizer.applyTransaction(wct); + } + } + } + + /** Get number of tasks that are marked as visible */ + int getVisibleTaskCount() { + return mDesktopModeTaskRepository.getVisibleTaskCount(); + } + + private void bringDesktopAppsToFront(WindowContainerTransaction wct) { + final ArraySet<Integer> activeTasks = mDesktopModeTaskRepository.getActiveTasks(); + ProtoLog.d(WM_SHELL_DESKTOP_MODE, "bringDesktopAppsToFront: tasks=%s", activeTasks.size()); + + final List<RunningTaskInfo> taskInfos = new ArrayList<>(); + for (Integer taskId : activeTasks) { + RunningTaskInfo taskInfo = mShellTaskOrganizer.getRunningTaskInfo(taskId); + if (taskInfo != null) { + taskInfos.add(taskInfo); + } + } + + if (taskInfos.isEmpty()) { + return; + } + + moveHomeTaskToFront(wct); + + ProtoLog.d(WM_SHELL_DESKTOP_MODE, + "bringDesktopAppsToFront: reordering all active tasks to the front"); + final List<Integer> allTasksInZOrder = + mDesktopModeTaskRepository.getFreeformTasksInZOrder(); + // Sort by z-order, bottom to top, so that the top-most task is reordered to the top last + // in the WCT. + taskInfos.sort(Comparator.comparingInt(task -> -allTasksInZOrder.indexOf(task.taskId))); + for (RunningTaskInfo task : taskInfos) { + wct.reorder(task.token, true); + } + } + + private void moveHomeTaskToFront(WindowContainerTransaction wct) { + for (RunningTaskInfo task : mShellTaskOrganizer.getRunningTasks(mContext.getDisplayId())) { + if (task.getActivityType() == ACTIVITY_TYPE_HOME) { + wct.reorder(task.token, true /* onTop */); + return; + } + } + } + + /** + * Moves a specifc task to the front. + * @param taskInfo the task to show in front. + */ + public void moveTaskToFront(RunningTaskInfo taskInfo) { + WindowContainerTransaction wct = new WindowContainerTransaction(); + wct.reorder(taskInfo.token, true /* onTop */); + if (Transitions.ENABLE_SHELL_TRANSITIONS) { + mTransitions.startTransition(TRANSIT_TO_FRONT, wct, null); + } else { + mShellTaskOrganizer.applyTransaction(wct); + } + } + + /** + * Turn desktop mode on or off + * @param active the desired state for desktop mode setting + */ + public void setDesktopModeActive(boolean active) { + int value = active ? 1 : 0; + Settings.System.putInt(mContext.getContentResolver(), Settings.System.DESKTOP_MODE, value); + } + + /** + * Returns the windowing mode of the display area with the specified displayId. + * @param displayId + * @return + */ + public int getDisplayAreaWindowingMode(int displayId) { + return mRootTaskDisplayAreaOrganizer.getDisplayAreaInfo(displayId) + .configuration.windowConfiguration.getWindowingMode(); + } + + @Override + public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, + @NonNull Transitions.TransitionFinishCallback finishCallback) { + // This handler should never be the sole handler, so should not animate anything. + return false; + } + + @Nullable + @Override + public WindowContainerTransaction handleRequest(@NonNull IBinder transition, + @NonNull TransitionRequestInfo request) { + // Only do anything if we are in desktop mode and opening/moving-to-front a task/app in + // freeform + if (!DesktopModeStatus.isActive(mContext)) { + ProtoLog.d(WM_SHELL_DESKTOP_MODE, + "skip shell transition request: desktop mode not active"); + return null; + } + if (request.getType() != TRANSIT_OPEN && request.getType() != TRANSIT_TO_FRONT) { + ProtoLog.d(WM_SHELL_DESKTOP_MODE, + "skip shell transition request: unsupported type %s", + WindowManager.transitTypeToString(request.getType())); + return null; + } + if (request.getTriggerTask() == null + || request.getTriggerTask().getWindowingMode() != WINDOWING_MODE_FREEFORM) { + ProtoLog.d(WM_SHELL_DESKTOP_MODE, "skip shell transition request: not freeform task"); + return null; + } + ProtoLog.d(WM_SHELL_DESKTOP_MODE, "handle shell transition request: %s", request); + + WindowContainerTransaction wct = new WindowContainerTransaction(); + bringDesktopAppsToFront(wct); + wct.reorder(request.getTriggerTask().token, true /* onTop */); + + return 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 = DesktopModeStatus.isActive(mContext); + updateDesktopModeActive(enabled); + } + } + + /** + * The interface for calls from outside the shell, within the host process. + */ + @ExternalThread + private final class DesktopModeImpl implements DesktopMode { + + @Override + public void addListener(DesktopModeTaskRepository.VisibleTasksListener listener, + Executor callbackExecutor) { + mMainExecutor.execute(() -> { + DesktopModeController.this.addListener(listener, callbackExecutor); + }); + } + } + + /** + * The interface for calls from outside the host process. + */ + @BinderThread + private static class IDesktopModeImpl extends IDesktopMode.Stub + implements ExternalInterfaceBinder { + + private DesktopModeController mController; + + IDesktopModeImpl(DesktopModeController controller) { + mController = controller; + } + + /** + * Invalidates this instance, preventing future calls from updating the controller. + */ + @Override + public void invalidate() { + mController = null; + } + + public void showDesktopApps() { + executeRemoteCallWithTaskPermission(mController, "showDesktopApps", + DesktopModeController::showDesktopApps); + } + + @Override + public int getVisibleTaskCount() throws RemoteException { + int[] result = new int[1]; + executeRemoteCallWithTaskPermission(mController, "getVisibleTaskCount", + controller -> result[0] = controller.getVisibleTaskCount(), + true /* blocking */ + ); + return result[0]; + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java new file mode 100644 index 000000000000..055949fd8c89 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java @@ -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 static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE; + +import android.content.Context; +import android.os.SystemProperties; +import android.os.UserHandle; +import android.provider.Settings; + +import com.android.internal.protolog.common.ProtoLog; + +/** + * Constants for desktop mode feature + */ +public class DesktopModeStatus { + + /** + * Flag to indicate whether desktop mode is available on the device + */ + private static final boolean IS_SUPPORTED = SystemProperties.getBoolean( + "persist.wm.debug.desktop_mode", false); + + /** + * Flag to indicate whether desktop mode proto 2 is available on the device + */ + private static final boolean IS_PROTO2_ENABLED = SystemProperties.getBoolean( + "persist.wm.debug.desktop_mode_2", false); + + /** + * Return {@code true} if desktop mode support is enabled + */ + public static boolean isProto1Enabled() { + return IS_SUPPORTED; + } + + /** + * Return {@code true} is desktop windowing proto 2 is enabled + */ + public static boolean isProto2Enabled() { + return IS_PROTO2_ENABLED; + } + + /** + * Return {@code true} if proto 1 or 2 is enabled. + * Can be used to guard logic that is common for both prototypes. + */ + public static boolean isAnyEnabled() { + return isProto1Enabled() || isProto2Enabled(); + } + + /** + * Check if desktop mode is active + * + * @return {@code true} if active + */ + public static boolean isActive(Context context) { + if (!isAnyEnabled()) { + return false; + } + if (isProto2Enabled()) { + // Desktop mode is always active in prototype 2 + return true; + } + try { + int result = Settings.System.getIntForUser(context.getContentResolver(), + Settings.System.DESKTOP_MODE, UserHandle.USER_CURRENT); + return result != 0; + } catch (Exception e) { + ProtoLog.e(WM_SHELL_DESKTOP_MODE, "Failed to read DESKTOP_MODE setting %s", e); + return false; + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt new file mode 100644 index 000000000000..47342c9f21ee --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt @@ -0,0 +1,190 @@ +/* + * 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.ArrayMap +import android.util.ArraySet +import java.util.concurrent.Executor + +/** + * 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 visibleTasks = ArraySet<Int>() + // Tasks currently in freeform mode, ordered from top to bottom (top is at index 0). + private val freeformTasksInZOrder = mutableListOf<Int>() + private val activeTasksListeners = ArraySet<ActiveTasksListener>() + // Track visible tasks separately because a task may be part of the desktop but not visible. + private val visibleTasksListeners = ArrayMap<VisibleTasksListener, Executor>() + + /** + * Add a [ActiveTasksListener] to be notified of updates to active tasks in the repository. + */ + fun addActiveTaskListener(activeTasksListener: ActiveTasksListener) { + activeTasksListeners.add(activeTasksListener) + } + + /** + * Add a [VisibleTasksListener] to be notified when freeform tasks are visible or not. + */ + fun addVisibleTasksListener(visibleTasksListener: VisibleTasksListener, executor: Executor) { + visibleTasksListeners.put(visibleTasksListener, executor) + executor.execute( + Runnable { visibleTasksListener.onVisibilityChanged(visibleTasks.size > 0) }) + } + + /** + * Remove a previously registered [ActiveTasksListener] + */ + fun removeActiveTasksListener(activeTasksListener: ActiveTasksListener) { + activeTasksListeners.remove(activeTasksListener) + } + + /** + * Remove a previously registered [VisibleTasksListener] + */ + fun removeVisibleTasksListener(visibleTasksListener: VisibleTasksListener) { + visibleTasksListeners.remove(visibleTasksListener) + } + + /** + * Mark a task with given [taskId] as active. + * + * @return `true` if the task was not active + */ + fun addActiveTask(taskId: Int): Boolean { + val added = activeTasks.add(taskId) + if (added) { + activeTasksListeners.onEach { it.onActiveTasksChanged() } + } + return added + } + + /** + * Remove task with given [taskId] from active tasks. + * + * @return `true` if the task was active + */ + fun removeActiveTask(taskId: Int): Boolean { + val removed = activeTasks.remove(taskId) + if (removed) { + activeTasksListeners.onEach { it.onActiveTasksChanged() } + } + return removed + } + + /** + * Check if a task with the given [taskId] was marked as an active task + */ + fun isActiveTask(taskId: Int): Boolean { + return activeTasks.contains(taskId) + } + + /** + * Whether a task is visible. + */ + fun isVisibleTask(taskId: Int): Boolean { + return visibleTasks.contains(taskId) + } + + /** + * Get a set of the active tasks + */ + fun getActiveTasks(): ArraySet<Int> { + return ArraySet(activeTasks) + } + + /** + * Get a list of freeform tasks, ordered from top-bottom (top at index 0). + */ + fun getFreeformTasksInZOrder(): List<Int> { + return freeformTasksInZOrder + } + + /** + * Updates whether a freeform task with this id is visible or not and notifies listeners. + */ + fun updateVisibleFreeformTasks(taskId: Int, visible: Boolean) { + val prevCount: Int = visibleTasks.size + if (visible) { + visibleTasks.add(taskId) + } else { + visibleTasks.remove(taskId) + } + if (prevCount == 0 && visibleTasks.size == 1 || + prevCount > 0 && visibleTasks.size == 0) { + for ((listener, executor) in visibleTasksListeners) { + executor.execute( + Runnable { listener.onVisibilityChanged(visibleTasks.size > 0) }) + } + } + } + + /** + * Get number of tasks that are marked as visible + */ + fun getVisibleTaskCount(): Int { + return visibleTasks.size + } + + /** + * Add (or move if it already exists) the task to the top of the ordered list. + */ + fun addOrMoveFreeformTaskToTop(taskId: Int) { + if (freeformTasksInZOrder.contains(taskId)) { + freeformTasksInZOrder.remove(taskId) + } + freeformTasksInZOrder.add(0, taskId) + } + + /** + * Remove the task from the ordered list. + */ + fun removeFreeformTask(taskId: Int) { + freeformTasksInZOrder.remove(taskId) + } + + /** + * Defines interface for classes that can listen to changes for active tasks in desktop mode. + */ + interface ActiveTasksListener { + /** + * Called when the active tasks change in desktop mode. + */ + @JvmDefault + fun onActiveTasksChanged() {} + } + + /** + * Defines interface for classes that can listen to changes for visible tasks in desktop mode. + */ + interface VisibleTasksListener { + /** + * Called when the desktop starts or stops showing freeform tasks. + */ + @JvmDefault + fun onVisibilityChanged(hasVisibleFreeformTasks: Boolean) {} + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java new file mode 100644 index 000000000000..015d5c1705e7 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java @@ -0,0 +1,227 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.desktopmode; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.RectEvaluator; +import android.animation.ValueAnimator; +import android.app.ActivityManager; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.util.DisplayMetrics; +import android.view.SurfaceControl; +import android.view.SurfaceControlViewHost; +import android.view.View; +import android.view.WindowManager; +import android.view.WindowlessWindowManager; +import android.view.animation.DecelerateInterpolator; +import android.widget.ImageView; + +import com.android.wm.shell.R; +import com.android.wm.shell.RootTaskDisplayAreaOrganizer; +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.common.DisplayController; +import com.android.wm.shell.common.SyncTransactionQueue; + +/** + * Animated visual indicator for Desktop Mode windowing transitions. + */ +public class DesktopModeVisualIndicator { + + private final Context mContext; + private final DisplayController mDisplayController; + private final ShellTaskOrganizer mTaskOrganizer; + private final RootTaskDisplayAreaOrganizer mRootTdaOrganizer; + private final ActivityManager.RunningTaskInfo mTaskInfo; + private final SurfaceControl mTaskSurface; + private SurfaceControl mLeash; + + private final SyncTransactionQueue mSyncQueue; + private SurfaceControlViewHost mViewHost; + + public DesktopModeVisualIndicator(SyncTransactionQueue syncQueue, + ActivityManager.RunningTaskInfo taskInfo, DisplayController displayController, + Context context, SurfaceControl taskSurface, ShellTaskOrganizer taskOrganizer, + RootTaskDisplayAreaOrganizer taskDisplayAreaOrganizer) { + mSyncQueue = syncQueue; + mTaskInfo = taskInfo; + mDisplayController = displayController; + mContext = context; + mTaskSurface = taskSurface; + mTaskOrganizer = taskOrganizer; + mRootTdaOrganizer = taskDisplayAreaOrganizer; + } + + /** + * Create and animate the indicator for the exit desktop mode transition. + */ + public void createFullscreenIndicator() { + final SurfaceControl.Transaction t = new SurfaceControl.Transaction(); + final Resources resources = mContext.getResources(); + final DisplayMetrics metrics = resources.getDisplayMetrics(); + final int screenWidth = metrics.widthPixels; + final int screenHeight = metrics.heightPixels; + final int padding = mDisplayController + .getDisplayLayout(mTaskInfo.displayId).stableInsets().top; + final ImageView v = new ImageView(mContext); + v.setImageResource(R.drawable.desktop_windowing_transition_background); + final SurfaceControl.Builder builder = new SurfaceControl.Builder(); + mRootTdaOrganizer.attachToDisplayArea(mTaskInfo.displayId, builder); + mLeash = builder + .setName("Fullscreen Indicator") + .setContainerLayer() + .build(); + t.show(mLeash); + final WindowManager.LayoutParams lp = + new WindowManager.LayoutParams(screenWidth, screenHeight, + WindowManager.LayoutParams.TYPE_APPLICATION, + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, PixelFormat.TRANSPARENT); + lp.setTitle("Fullscreen indicator for Task=" + mTaskInfo.taskId); + lp.setTrustedOverlay(); + final WindowlessWindowManager windowManager = new WindowlessWindowManager( + mTaskInfo.configuration, mLeash, + null /* hostInputToken */); + mViewHost = new SurfaceControlViewHost(mContext, + mDisplayController.getDisplay(mTaskInfo.displayId), windowManager, + "FullscreenVisualIndicator"); + mViewHost.setView(v, lp); + // We want this indicator to be behind the dragged task, but in front of all others. + t.setRelativeLayer(mLeash, mTaskSurface, -1); + + mSyncQueue.runInSync(transaction -> { + transaction.merge(t); + t.close(); + }); + final Rect startBounds = new Rect(padding, padding, + screenWidth - padding, screenHeight - padding); + final VisualIndicatorAnimator animator = VisualIndicatorAnimator.fullscreenIndicator(v, + startBounds); + animator.start(); + } + + /** + * Release the indicator and its components when it is no longer needed. + */ + public void releaseFullscreenIndicator() { + if (mViewHost == null) return; + if (mViewHost != null) { + mViewHost.release(); + mViewHost = null; + } + + if (mLeash != null) { + final SurfaceControl.Transaction t = new SurfaceControl.Transaction(); + t.remove(mLeash); + mLeash = null; + mSyncQueue.runInSync(transaction -> { + transaction.merge(t); + t.close(); + }); + } + } + /** + * Animator for Desktop Mode transitions which supports bounds and alpha animation. + */ + private static class VisualIndicatorAnimator extends ValueAnimator { + private static final int FULLSCREEN_INDICATOR_DURATION = 200; + private static final float SCALE_ADJUSTMENT_PERCENT = 0.015f; + private static final float INDICATOR_FINAL_OPACITY = 0.7f; + + private final ImageView mView; + private final Rect mStartBounds; + private final Rect mEndBounds; + private final RectEvaluator mRectEvaluator; + + private VisualIndicatorAnimator(ImageView view, Rect startBounds, + Rect endBounds) { + mView = view; + mStartBounds = new Rect(startBounds); + mEndBounds = endBounds; + setFloatValues(0, 1); + mRectEvaluator = new RectEvaluator(new Rect()); + } + + /** + * Create animator for visual indicator of fullscreen transition + * + * @param view the view for this indicator + * @param startBounds the starting bounds of the fullscreen indicator + */ + public static VisualIndicatorAnimator fullscreenIndicator(ImageView view, + Rect startBounds) { + view.getDrawable().setBounds(startBounds); + int width = startBounds.width(); + int height = startBounds.height(); + Rect endBounds = new Rect((int) (startBounds.left - (SCALE_ADJUSTMENT_PERCENT * width)), + (int) (startBounds.top - (SCALE_ADJUSTMENT_PERCENT * height)), + (int) (startBounds.right + (SCALE_ADJUSTMENT_PERCENT * width)), + (int) (startBounds.bottom + (SCALE_ADJUSTMENT_PERCENT * height))); + VisualIndicatorAnimator animator = new VisualIndicatorAnimator( + view, startBounds, endBounds); + animator.setInterpolator(new DecelerateInterpolator()); + setupFullscreenIndicatorAnimation(animator); + return animator; + } + + /** + * Add necessary listener for animation of fullscreen indicator + */ + private static void setupFullscreenIndicatorAnimation( + VisualIndicatorAnimator animator) { + animator.addUpdateListener(a -> { + if (animator.mView != null) { + animator.updateBounds(a.getAnimatedFraction(), animator.mView); + animator.updateIndicatorAlpha(a.getAnimatedFraction(), animator.mView); + } else { + animator.cancel(); + } + }); + animator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + animator.mView.getDrawable().setBounds(animator.mEndBounds); + } + }); + animator.setDuration(FULLSCREEN_INDICATOR_DURATION); + } + + /** + * Update bounds of view based on current animation fraction. + * Use of delta is to animate bounds independently, in case we need to + * run multiple animations simultaneously. + * + * @param fraction fraction to use, compared against previous fraction + * @param view the view to update + */ + private void updateBounds(float fraction, ImageView view) { + Rect currentBounds = mRectEvaluator.evaluate(fraction, mStartBounds, mEndBounds); + view.getDrawable().setBounds(currentBounds); + } + + /** + * Fade in the fullscreen indicator + * + * @param fraction current animation fraction + */ + private void updateIndicatorAlpha(float fraction, View view) { + view.setAlpha(fraction * INDICATOR_FINAL_OPACITY); + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt new file mode 100644 index 000000000000..c35cd5a8be02 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt @@ -0,0 +1,481 @@ +/* + * 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.app.ActivityManager +import android.app.ActivityManager.RunningTaskInfo +import android.app.WindowConfiguration.ACTIVITY_TYPE_HOME +import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD +import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM +import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN +import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED +import android.app.WindowConfiguration.WindowingMode +import android.content.Context +import android.graphics.Rect +import android.os.IBinder +import android.os.SystemProperties +import android.view.SurfaceControl +import android.view.WindowManager.TRANSIT_CHANGE +import android.view.WindowManager.TRANSIT_NONE +import android.view.WindowManager.TRANSIT_OPEN +import android.view.WindowManager.TRANSIT_TO_FRONT +import android.window.TransitionInfo +import android.window.TransitionRequestInfo +import android.window.WindowContainerToken +import android.window.WindowContainerTransaction +import androidx.annotation.BinderThread +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.DisplayController +import com.android.wm.shell.common.ExecutorUtils +import com.android.wm.shell.common.ExternalInterfaceBinder +import com.android.wm.shell.common.RemoteCallable +import com.android.wm.shell.common.ShellExecutor +import com.android.wm.shell.common.SyncTransactionQueue +import com.android.wm.shell.common.annotations.ExternalThread +import com.android.wm.shell.common.annotations.ShellMainThread +import com.android.wm.shell.desktopmode.DesktopModeTaskRepository.VisibleTasksListener +import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE +import com.android.wm.shell.sysui.ShellController +import com.android.wm.shell.sysui.ShellInit +import com.android.wm.shell.sysui.ShellSharedConstants +import com.android.wm.shell.transition.Transitions +import java.util.concurrent.Executor +import java.util.function.Consumer + +/** Handles moving tasks in and out of desktop */ +class DesktopTasksController( + private val context: Context, + shellInit: ShellInit, + private val shellController: ShellController, + private val displayController: DisplayController, + private val shellTaskOrganizer: ShellTaskOrganizer, + private val syncQueue: SyncTransactionQueue, + private val rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer, + private val transitions: Transitions, + private val enterDesktopTaskTransitionHandler: EnterDesktopTaskTransitionHandler, + private val exitDesktopTaskTransitionHandler: ExitDesktopTaskTransitionHandler, + private val desktopModeTaskRepository: DesktopModeTaskRepository, + @ShellMainThread private val mainExecutor: ShellExecutor +) : RemoteCallable<DesktopTasksController>, Transitions.TransitionHandler { + + private val desktopMode: DesktopModeImpl + private var visualIndicator: DesktopModeVisualIndicator? = null + + init { + desktopMode = DesktopModeImpl() + if (DesktopModeStatus.isProto2Enabled()) { + shellInit.addInitCallback({ onInit() }, this) + } + } + + private fun onInit() { + ProtoLog.d(WM_SHELL_DESKTOP_MODE, "Initialize DesktopTasksController") + shellController.addExternalInterface( + ShellSharedConstants.KEY_EXTRA_SHELL_DESKTOP_MODE, + { createExternalInterface() }, + this + ) + transitions.addHandler(this) + } + + /** Show all tasks, that are part of the desktop, on top of launcher */ + fun showDesktopApps() { + ProtoLog.v(WM_SHELL_DESKTOP_MODE, "showDesktopApps") + val wct = WindowContainerTransaction() + bringDesktopAppsToFront(wct) + + // Execute transaction if there are pending operations + if (!wct.isEmpty) { + if (Transitions.ENABLE_SHELL_TRANSITIONS) { + // TODO(b/268662477): add animation for the transition + transitions.startTransition(TRANSIT_NONE, wct, null /* handler */) + } else { + shellTaskOrganizer.applyTransaction(wct) + } + } + } + + /** Get number of tasks that are marked as visible */ + fun getVisibleTaskCount(): Int { + return desktopModeTaskRepository.getVisibleTaskCount() + } + + /** Move a task with given `taskId` to desktop */ + fun moveToDesktop(taskId: Int) { + shellTaskOrganizer.getRunningTaskInfo(taskId)?.let { task -> moveToDesktop(task) } + } + + /** Move a task to desktop */ + fun moveToDesktop(task: ActivityManager.RunningTaskInfo) { + ProtoLog.v(WM_SHELL_DESKTOP_MODE, "moveToDesktop: %d", task.taskId) + + val wct = WindowContainerTransaction() + // Bring other apps to front first + bringDesktopAppsToFront(wct) + addMoveToDesktopChanges(wct, task.token) + if (Transitions.ENABLE_SHELL_TRANSITIONS) { + transitions.startTransition(TRANSIT_CHANGE, wct, null /* handler */) + } else { + shellTaskOrganizer.applyTransaction(wct) + } + } + + /** + * Moves a single task to freeform and sets the taskBounds to the passed in bounds, + * startBounds + */ + fun moveToFreeform( + taskInfo: RunningTaskInfo, + startBounds: Rect + ) { + val wct = WindowContainerTransaction() + moveHomeTaskToFront(wct) + addMoveToDesktopChanges(wct, taskInfo.getToken()) + wct.setBounds(taskInfo.token, startBounds) + + if (Transitions.ENABLE_SHELL_TRANSITIONS) { + enterDesktopTaskTransitionHandler.startTransition( + Transitions.TRANSIT_ENTER_FREEFORM, wct) + } else { + shellTaskOrganizer.applyTransaction(wct) + } + } + + /** Brings apps to front and sets freeform task bounds */ + fun moveToDesktopWithAnimation( + taskInfo: RunningTaskInfo, + freeformBounds: Rect + ) { + val wct = WindowContainerTransaction() + bringDesktopAppsToFront(wct) + addMoveToDesktopChanges(wct, taskInfo.getToken()) + wct.setBounds(taskInfo.token, freeformBounds) + + if (Transitions.ENABLE_SHELL_TRANSITIONS) { + enterDesktopTaskTransitionHandler.startTransition( + Transitions.TRANSIT_ENTER_DESKTOP_MODE, wct) + } else { + shellTaskOrganizer.applyTransaction(wct) + } + } + + /** Move a task with given `taskId` to fullscreen */ + fun moveToFullscreen(taskId: Int) { + shellTaskOrganizer.getRunningTaskInfo(taskId)?.let { task -> moveToFullscreen(task) } + } + + /** Move a task to fullscreen */ + fun moveToFullscreen(task: ActivityManager.RunningTaskInfo) { + ProtoLog.v(WM_SHELL_DESKTOP_MODE, "moveToFullscreen: %d", task.taskId) + + val wct = WindowContainerTransaction() + addMoveToFullscreenChanges(wct, task.token) + if (Transitions.ENABLE_SHELL_TRANSITIONS) { + transitions.startTransition(TRANSIT_CHANGE, wct, null /* handler */) + } else { + shellTaskOrganizer.applyTransaction(wct) + } + } + + fun moveToFullscreenWithAnimation(task: ActivityManager.RunningTaskInfo) { + val wct = WindowContainerTransaction() + addMoveToFullscreenChanges(wct, task.token) + + if (Transitions.ENABLE_SHELL_TRANSITIONS) { + exitDesktopTaskTransitionHandler.startTransition( + Transitions.TRANSIT_EXIT_DESKTOP_MODE, wct) + } else { + shellTaskOrganizer.applyTransaction(wct) + } + } + + /** Move a task to the front **/ + fun moveTaskToFront(taskInfo: ActivityManager.RunningTaskInfo) { + val wct = WindowContainerTransaction() + wct.reorder(taskInfo.token, true) + if (Transitions.ENABLE_SHELL_TRANSITIONS) { + transitions.startTransition(TRANSIT_TO_FRONT, wct, null /* handler */) + } else { + shellTaskOrganizer.applyTransaction(wct) + } + } + + /** + * Get windowing move for a given `taskId` + * + * @return [WindowingMode] for the task or [WINDOWING_MODE_UNDEFINED] if task is not found + */ + @WindowingMode + fun getTaskWindowingMode(taskId: Int): Int { + return shellTaskOrganizer.getRunningTaskInfo(taskId)?.windowingMode + ?: WINDOWING_MODE_UNDEFINED + } + + private fun bringDesktopAppsToFront(wct: WindowContainerTransaction) { + ProtoLog.v(WM_SHELL_DESKTOP_MODE, "bringDesktopAppsToFront") + val activeTasks = desktopModeTaskRepository.getActiveTasks() + + // First move home to front and then other tasks on top of it + moveHomeTaskToFront(wct) + + val allTasksInZOrder = desktopModeTaskRepository.getFreeformTasksInZOrder() + activeTasks + // Sort descending as the top task is at index 0. It should be ordered to top last + .sortedByDescending { taskId -> allTasksInZOrder.indexOf(taskId) } + .mapNotNull { taskId -> shellTaskOrganizer.getRunningTaskInfo(taskId) } + .forEach { task -> wct.reorder(task.token, true /* onTop */) } + } + + private fun moveHomeTaskToFront(wct: WindowContainerTransaction) { + shellTaskOrganizer + .getRunningTasks(context.displayId) + .firstOrNull { task -> task.activityType == ACTIVITY_TYPE_HOME } + ?.let { homeTask -> wct.reorder(homeTask.getToken(), true /* onTop */) } + } + + override fun getContext(): Context { + return context + } + + override fun getRemoteCallExecutor(): ShellExecutor { + return mainExecutor + } + + override fun startAnimation( + transition: IBinder, + info: TransitionInfo, + startTransaction: SurfaceControl.Transaction, + finishTransaction: SurfaceControl.Transaction, + finishCallback: Transitions.TransitionFinishCallback + ): Boolean { + // This handler should never be the sole handler, so should not animate anything. + return false + } + + override fun handleRequest( + transition: IBinder, + request: TransitionRequestInfo + ): WindowContainerTransaction? { + // Check if we should skip handling this transition + val task: ActivityManager.RunningTaskInfo? = request.triggerTask + val shouldHandleRequest = + when { + // Only handle open or to front transitions + request.type != TRANSIT_OPEN && request.type != TRANSIT_TO_FRONT -> false + // Only handle when it is a task transition + task == null -> false + // Only handle standard type tasks + task.activityType != ACTIVITY_TYPE_STANDARD -> false + // Only handle fullscreen or freeform tasks + task.windowingMode != WINDOWING_MODE_FULLSCREEN && + task.windowingMode != WINDOWING_MODE_FREEFORM -> false + // Otherwise process it + else -> true + } + + if (!shouldHandleRequest) { + return null + } + + val activeTasks = desktopModeTaskRepository.getActiveTasks() + + // Check if we should switch a fullscreen task to freeform + if (task?.windowingMode == WINDOWING_MODE_FULLSCREEN) { + // If there are any visible desktop tasks, switch the task to freeform + if (activeTasks.any { desktopModeTaskRepository.isVisibleTask(it) }) { + ProtoLog.d( + WM_SHELL_DESKTOP_MODE, + "DesktopTasksController#handleRequest: switch fullscreen task to freeform," + + " taskId=%d", + task.taskId + ) + return WindowContainerTransaction().also { wct -> + addMoveToDesktopChanges(wct, task.token) + } + } + } + + // CHeck if we should switch a freeform task to fullscreen + if (task?.windowingMode == WINDOWING_MODE_FREEFORM) { + // If no visible desktop tasks, switch this task to freeform as the transition came + // outside of this controller + if (activeTasks.none { desktopModeTaskRepository.isVisibleTask(it) }) { + ProtoLog.d( + WM_SHELL_DESKTOP_MODE, + "DesktopTasksController#handleRequest: switch freeform task to fullscreen," + + " taskId=%d", + task.taskId + ) + return WindowContainerTransaction().also { wct -> + addMoveToFullscreenChanges(wct, task.token) + } + } + } + return null + } + + private fun addMoveToDesktopChanges( + wct: WindowContainerTransaction, + token: WindowContainerToken + ) { + wct.setWindowingMode(token, WINDOWING_MODE_FREEFORM) + wct.reorder(token, true /* onTop */) + if (isDesktopDensityOverrideSet()) { + wct.setDensityDpi(token, getDesktopDensityDpi()) + } + } + + private fun addMoveToFullscreenChanges( + wct: WindowContainerTransaction, + token: WindowContainerToken + ) { + wct.setWindowingMode(token, WINDOWING_MODE_FULLSCREEN) + wct.setBounds(token, null) + if (isDesktopDensityOverrideSet()) { + wct.setDensityDpi(token, getFullscreenDensityDpi()) + } + } + + private fun getFullscreenDensityDpi(): Int { + return context.resources.displayMetrics.densityDpi + } + + private fun getDesktopDensityDpi(): Int { + return DESKTOP_DENSITY_OVERRIDE + } + + /** Creates a new instance of the external interface to pass to another process. */ + private fun createExternalInterface(): ExternalInterfaceBinder { + return IDesktopModeImpl(this) + } + + /** Get connection interface between sysui and shell */ + fun asDesktopMode(): DesktopMode { + return desktopMode + } + + /** + * Perform checks required on drag move. Create/release fullscreen indicator as needed. + * + * @param taskInfo the task being dragged. + * @param taskSurface SurfaceControl of dragged task. + * @param y coordinate of dragged task. Used for checks against status bar height. + */ + fun onDragPositioningMove( + taskInfo: RunningTaskInfo, + taskSurface: SurfaceControl, + y: Float + ) { + val statusBarHeight = displayController + .getDisplayLayout(taskInfo.displayId)?.stableInsets()?.top ?: 0 + if (taskInfo.windowingMode == WINDOWING_MODE_FREEFORM) { + if (y <= statusBarHeight && visualIndicator == null) { + visualIndicator = DesktopModeVisualIndicator(syncQueue, taskInfo, + displayController, context, taskSurface, shellTaskOrganizer, + rootTaskDisplayAreaOrganizer) + visualIndicator?.createFullscreenIndicator() + } else if (y > statusBarHeight && visualIndicator != null) { + visualIndicator?.releaseFullscreenIndicator() + visualIndicator = null + } + } + } + + /** + * Perform checks required on drag end. Move to fullscreen if drag ends in status bar area. + * + * @param taskInfo the task being dragged. + * @param y height of drag, to be checked against status bar height. + */ + fun onDragPositioningEnd( + taskInfo: RunningTaskInfo, + y: Float + ) { + val statusBarHeight = displayController + .getDisplayLayout(taskInfo.displayId)?.stableInsets()?.top ?: 0 + if (y <= statusBarHeight && taskInfo.windowingMode == WINDOWING_MODE_FREEFORM) { + visualIndicator?.releaseFullscreenIndicator() + visualIndicator = null + moveToFullscreenWithAnimation(taskInfo) + } + } + + /** + * Adds a listener to find out about changes in the visibility of freeform tasks. + * + * @param listener the listener to add. + * @param callbackExecutor the executor to call the listener on. + */ + fun addListener(listener: VisibleTasksListener, callbackExecutor: Executor) { + desktopModeTaskRepository.addVisibleTasksListener(listener, callbackExecutor) + } + + /** The interface for calls from outside the shell, within the host process. */ + @ExternalThread + private inner class DesktopModeImpl : DesktopMode { + override fun addListener(listener: VisibleTasksListener, callbackExecutor: Executor) { + mainExecutor.execute { + this@DesktopTasksController.addListener(listener, callbackExecutor) + } + } + } + + /** The interface for calls from outside the host process. */ + @BinderThread + private class IDesktopModeImpl(private var controller: DesktopTasksController?) : + IDesktopMode.Stub(), ExternalInterfaceBinder { + /** Invalidates this instance, preventing future calls from updating the controller. */ + override fun invalidate() { + controller = null + } + + override fun showDesktopApps() { + ExecutorUtils.executeRemoteCallWithTaskPermission( + controller, + "showDesktopApps", + Consumer(DesktopTasksController::showDesktopApps) + ) + } + + override fun getVisibleTaskCount(): Int { + val result = IntArray(1) + ExecutorUtils.executeRemoteCallWithTaskPermission( + controller, + "getVisibleTaskCount", + { controller -> result[0] = controller.getVisibleTaskCount() }, + true /* blocking */ + ) + return result[0] + } + } + + companion object { + private val DESKTOP_DENSITY_OVERRIDE = + SystemProperties.getInt("persist.wm.debug.desktop_mode_density", 0) + private val DESKTOP_DENSITY_ALLOWED_RANGE = (100..1000) + + /** + * Check if desktop density override is enabled + */ + @JvmStatic + fun isDesktopDensityOverrideSet(): Boolean { + return DESKTOP_DENSITY_OVERRIDE in DESKTOP_DENSITY_ALLOWED_RANGE + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandler.java new file mode 100644 index 000000000000..3df2340d4524 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandler.java @@ -0,0 +1,185 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.desktopmode; + +import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; +import android.app.ActivityManager; +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 androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.wm.shell.transition.Transitions; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Supplier; + +/** + * The {@link Transitions.TransitionHandler} that handles transitions for desktop mode tasks + * entering and exiting freeform. + */ +public class EnterDesktopTaskTransitionHandler implements Transitions.TransitionHandler { + + private final Transitions mTransitions; + private final Supplier<SurfaceControl.Transaction> mTransactionSupplier; + + // The size of the screen during drag relative to the fullscreen size + public static final float DRAG_FREEFORM_SCALE = 0.4f; + // The size of the screen after drag relative to the fullscreen size + public static final float FINAL_FREEFORM_SCALE = 0.6f; + public static final int FREEFORM_ANIMATION_DURATION = 336; + + private final List<IBinder> mPendingTransitionTokens = new ArrayList<>(); + + public EnterDesktopTaskTransitionHandler( + Transitions transitions) { + this(transitions, SurfaceControl.Transaction::new); + } + + public EnterDesktopTaskTransitionHandler( + Transitions transitions, + Supplier<SurfaceControl.Transaction> supplier) { + mTransitions = transitions; + mTransactionSupplier = supplier; + } + + /** + * Starts Transition of a given type + * @param type Transition type + * @param wct WindowContainerTransaction for transition + */ + public void startTransition(@WindowManager.TransitionType int type, + @NonNull WindowContainerTransaction wct) { + final IBinder token = mTransitions.startTransition(type, wct, this); + mPendingTransitionTokens.add(token); + } + + @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; + } + + if (change.getMode() == WindowManager.TRANSIT_CHANGE) { + transitionHandled |= startChangeTransition( + transition, info.getType(), change, startT, finishCallback); + } + } + + mPendingTransitionTokens.remove(transition); + + return transitionHandled; + } + + private boolean startChangeTransition( + @NonNull IBinder transition, + @WindowManager.TransitionType int type, + @NonNull TransitionInfo.Change change, + @NonNull SurfaceControl.Transaction startT, + @NonNull Transitions.TransitionFinishCallback finishCallback) { + if (!mPendingTransitionTokens.contains(transition)) { + return false; + } + + final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo(); + if (type == Transitions.TRANSIT_ENTER_FREEFORM + && taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM) { + // Transitioning to freeform but keeping fullscreen bounds, so the crop is set + // to null and we don't require an animation + final SurfaceControl sc = change.getLeash(); + startT.setWindowCrop(sc, null); + startT.apply(); + mTransitions.getMainExecutor().execute( + () -> finishCallback.onTransitionFinished(null, null)); + return true; + } + + Rect endBounds = change.getEndAbsBounds(); + if (type == Transitions.TRANSIT_ENTER_DESKTOP_MODE + && taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM + && !endBounds.isEmpty()) { + // This Transition animates a task to freeform bounds after being dragged into freeform + // mode and brings the remaining freeform tasks to front + final SurfaceControl sc = change.getLeash(); + startT.setWindowCrop(sc, endBounds.width(), + endBounds.height()); + startT.apply(); + + // We want to find the scale of the current bounds relative to the end bounds. The + // task is currently scaled to DRAG_FREEFORM_SCALE and the final bounds will be + // scaled to FINAL_FREEFORM_SCALE. So, it is scaled to + // DRAG_FREEFORM_SCALE / FINAL_FREEFORM_SCALE relative to the freeform bounds + final ValueAnimator animator = + ValueAnimator.ofFloat(DRAG_FREEFORM_SCALE / FINAL_FREEFORM_SCALE, 1f); + animator.setDuration(FREEFORM_ANIMATION_DURATION); + final SurfaceControl.Transaction t = mTransactionSupplier.get(); + animator.addUpdateListener(animation -> { + final float animationValue = (float) animation.getAnimatedValue(); + t.setScale(sc, animationValue, animationValue); + + final float animationWidth = endBounds.width() * animationValue; + final float animationHeight = endBounds.height() * animationValue; + final int animationX = endBounds.centerX() - (int) (animationWidth / 2); + final int animationY = endBounds.centerY() - (int) (animationHeight / 2); + + t.setPosition(sc, animationX, animationY); + t.apply(); + }); + + animator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mTransitions.getMainExecutor().execute( + () -> finishCallback.onTransitionFinished(null, null)); + } + }); + + animator.start(); + return true; + } + + return false; + } + + @Nullable + @Override + public WindowContainerTransaction handleRequest(@NonNull IBinder transition, + @NonNull TransitionRequestInfo request) { + return null; + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandler.java new file mode 100644 index 000000000000..d18e98af0988 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandler.java @@ -0,0 +1,162 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.desktopmode; + +import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; + +import android.animation.ValueAnimator; +import android.app.ActivityManager; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Point; +import android.graphics.Rect; +import android.os.IBinder; +import android.util.DisplayMetrics; +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.internal.annotations.VisibleForTesting; +import com.android.wm.shell.transition.Transitions; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Supplier; + + +/** + * The {@link Transitions.TransitionHandler} that handles transitions for desktop mode tasks + * entering and exiting freeform. + */ +public class ExitDesktopTaskTransitionHandler implements Transitions.TransitionHandler { + private static final int FULLSCREEN_ANIMATION_DURATION = 336; + private final Context mContext; + private final Transitions mTransitions; + private final List<IBinder> mPendingTransitionTokens = new ArrayList<>(); + + private Supplier<SurfaceControl.Transaction> mTransactionSupplier; + + public ExitDesktopTaskTransitionHandler( + Transitions transitions, + Context context) { + this(transitions, SurfaceControl.Transaction::new, context); + } + + private ExitDesktopTaskTransitionHandler( + Transitions transitions, + Supplier<SurfaceControl.Transaction> supplier, + Context context) { + mTransitions = transitions; + mTransactionSupplier = supplier; + mContext = context; + } + + /** + * Starts Transition of a given type + * @param type Transition type + * @param wct WindowContainerTransaction for transition + */ + public void startTransition(@WindowManager.TransitionType int type, + @NonNull WindowContainerTransaction wct) { + final IBinder token = mTransitions.startTransition(type, wct, this); + mPendingTransitionTokens.add(token); + } + + @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; + } + + if (change.getMode() == WindowManager.TRANSIT_CHANGE) { + transitionHandled |= startChangeTransition( + transition, info.getType(), change, startT, finishCallback); + } + } + + mPendingTransitionTokens.remove(transition); + + return transitionHandled; + } + + @VisibleForTesting + boolean startChangeTransition( + @NonNull IBinder transition, + @WindowManager.TransitionType int type, + @NonNull TransitionInfo.Change change, + @NonNull SurfaceControl.Transaction startT, + @NonNull Transitions.TransitionFinishCallback finishCallback) { + if (!mPendingTransitionTokens.contains(transition)) { + return false; + } + final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo(); + if (type == Transitions.TRANSIT_EXIT_DESKTOP_MODE + && taskInfo.getWindowingMode() == WINDOWING_MODE_FULLSCREEN) { + // This Transition animates a task to fullscreen after being dragged to status bar + final Resources resources = mContext.getResources(); + final DisplayMetrics metrics = resources.getDisplayMetrics(); + final int screenWidth = metrics.widthPixels; + final int screenHeight = metrics.heightPixels; + final SurfaceControl sc = change.getLeash(); + startT.setCrop(sc, null); + startT.apply(); + final ValueAnimator animator = new ValueAnimator(); + animator.setFloatValues(0f, 1f); + animator.setDuration(FULLSCREEN_ANIMATION_DURATION); + final Rect startBounds = change.getStartAbsBounds(); + final float scaleX = (float) startBounds.width() / screenWidth; + final float scaleY = (float) startBounds.height() / screenHeight; + final SurfaceControl.Transaction t = mTransactionSupplier.get(); + Point startPos = new Point(startBounds.left, + startBounds.top); + animator.addUpdateListener(animation -> { + float fraction = animation.getAnimatedFraction(); + float currentScaleX = scaleX + ((1 - scaleX) * fraction); + float currentScaleY = scaleY + ((1 - scaleY) * fraction); + t.setPosition(sc, startPos.x * (1 - fraction), startPos.y * (1 - fraction)); + t.setScale(sc, currentScaleX, currentScaleY); + t.apply(); + }); + animator.start(); + return true; + } + + return false; + } + + @Nullable + @Override + public WindowContainerTransaction handleRequest(@NonNull IBinder transition, + @NonNull TransitionRequestInfo request) { + return null; + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/SplitScreenActivity.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl index 9c82eea1e8b8..d0739e14675f 100644 --- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/SplitScreenActivity.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl @@ -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,16 +14,16 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.testapp; +package com.android.wm.shell.desktopmode; -import android.app.Activity; -import android.os.Bundle; +/** + * Interface that is exposed to remote callers to manipulate desktop mode features. + */ +interface IDesktopMode { -public class SplitScreenActivity extends Activity { + /** Show apps on the desktop */ + void showDesktopApps(); - @Override - protected void onCreate(Bundle icicle) { - super.onCreate(icicle); - setContentView(R.layout.activity_splitscreen); - } -} + /** Get count of visible desktop tasks */ + int getVisibleTaskCount(); +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/OWNERS b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/OWNERS new file mode 100644 index 000000000000..926cfb3b12ef --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/OWNERS @@ -0,0 +1,2 @@ +# WM shell sub-module desktop owners +madym@google.com 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..fbf326eadcd5 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/changes.md @@ -0,0 +1,106 @@ +# 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 +(see [SysUI/Shell threading](threading.md)). + +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. + +Specifically, to support calling into a controller from an external process (like Launcher): +- Create an implementation of the external interface within the controller +- Have all incoming calls post to the main shell thread (inject @ShellMainThread Executor into the + controller if needed) +- Note that callbacks into SysUI should take an associated executor to call back on + +### 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. + +Specifically, to support calling into a controller from an external process (like Launcher): +- Create an implementation of the interface binder's `Stub` class within the controller, have it + extend `ExternalInterfaceBinder` and implement `invalidate()` to ensure it doesn't hold long + references to the outer controller +- Make the controller implement `RemoteCallable<T>`, and have all incoming calls use one of + the `ExecutorUtils.executeRemoteCallWithTaskPermission()` calls to verify the caller's identity + and ensure the call happens on the main shell thread and not the binder thread +- Inject `ShellController` and add the instance of the implementation as external interface +- In Launcher, update `TouchInteractionService` to pass the interface to `SystemUIProxy`, and then + call the SystemUIProxy method as needed in that code + +### 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: +- Add unit tests for all new components +- Keep controllers simple and break them down as needed +- Any SysUI callbacks should also take an associated executor to run the callback on + +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/DragAndDrop.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropConstants.java index edeff6e37182..20da54efd286 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDrop.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropConstants.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 The Android Open Source Project + * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,19 +16,12 @@ package com.android.wm.shell.draganddrop; -import android.content.res.Configuration; +/** Constants that can be used by both Shell and other users of the library, e.g. Launcher */ +public class DragAndDropConstants { -import com.android.wm.shell.common.annotations.ExternalThread; - -/** - * Interface for telling DragAndDrop stuff. - */ -@ExternalThread -public interface DragAndDrop { - - /** Called when the theme changes. */ - void onThemeChanged(); - - /** Called when the configuration changes. */ - void onConfigChanged(Configuration newConfig); + /** + * An Intent extra that Launcher can use to specify a region of the screen where Shell should + * ignore drag events. + */ + public static final String EXTRA_DISALLOW_HIT_REGION = "DISALLOW_HIT_REGION"; } 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..4cfaae6e51c7 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 @@ -36,6 +36,7 @@ import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_U import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; import android.content.ClipDescription; +import android.content.ComponentCallbacks2; import android.content.Context; import android.content.res.Configuration; import android.graphics.PixelFormat; @@ -58,31 +59,32 @@ import com.android.launcher3.icons.IconProvider; import com.android.wm.shell.R; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.annotations.ExternalMainThread; import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.splitscreen.SplitScreenController; +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, ComponentCallbacks2 { 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 +94,39 @@ 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); } - 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. */ @@ -162,6 +180,7 @@ public class DragAndDropController implements DisplayController.OnDisplaysChange try { wm.addView(rootView, layoutParams); addDisplayDropTarget(displayId, context, wm, rootView, dragLayout); + context.registerComponentCallbacks(this); } catch (WindowManager.InvalidDisplayException e) { Slog.w(TAG, "Unable to add view for display id: " + displayId); } @@ -191,6 +210,7 @@ public class DragAndDropController implements DisplayController.OnDisplaysChange if (pd == null) { return; } + pd.context.unregisterComponentCallbacks(this); pd.wm.removeViewImmediate(pd.rootView); mDisplayDropTargets.remove(displayId); } @@ -240,12 +260,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,16 +330,29 @@ public class DragAndDropController implements DisplayController.OnDisplaysChange return mimeTypes; } - private void onThemeChange() { - for (int i = 0; i < mDisplayDropTargets.size(); i++) { - mDisplayDropTargets.get(i).dragLayout.onThemeChange(); - } + // Note: Component callbacks are always called on the main thread of the process + @ExternalMainThread + @Override + public void onConfigurationChanged(Configuration newConfig) { + mMainExecutor.execute(() -> { + for (int i = 0; i < mDisplayDropTargets.size(); i++) { + mDisplayDropTargets.get(i).dragLayout.onConfigChanged(newConfig); + } + }); } - private void onConfigChanged(Configuration newConfig) { - for (int i = 0; i < mDisplayDropTargets.size(); i++) { - mDisplayDropTargets.get(i).dragLayout.onConfigChanged(newConfig); - } + // Note: Component callbacks are always called on the main thread of the process + @ExternalMainThread + @Override + public void onTrimMemory(int level) { + // Do nothing + } + + // Note: Component callbacks are always called on the main thread of the process + @ExternalMainThread + @Override + public void onLowMemory() { + // Do nothing } private static class PerDisplay { @@ -342,21 +375,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..df94b414c092 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; @@ -32,6 +34,7 @@ import static android.content.Intent.EXTRA_USER; import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT; import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED; +import static com.android.wm.shell.draganddrop.DragAndDropConstants.EXTRA_DISALLOW_HIT_REGION; import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_FULLSCREEN; import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_SPLIT_BOTTOM; import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_SPLIT_LEFT; @@ -45,14 +48,13 @@ 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.graphics.RectF; import android.os.Bundle; import android.os.RemoteException; import android.os.UserHandle; @@ -64,11 +66,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; @@ -88,6 +88,7 @@ public class DragAndDropPolicy { private final Starter mStarter; private final SplitScreenController mSplitScreen; private final ArrayList<DragAndDropPolicy.Target> mTargets = new ArrayList<>(); + private final RectF mDisallowHitRegion = new RectF(); private InstanceId mLoggerSessionId; private DragSession mSession; @@ -113,6 +114,12 @@ public class DragAndDropPolicy { mSession = new DragSession(mActivityTaskManager, displayLayout, data); // TODO(b/169894807): Also update the session data with task stack changes mSession.update(); + RectF disallowHitRegion = (RectF) mSession.dragData.getExtra(EXTRA_DISALLOW_HIT_REGION); + if (disallowHitRegion == null) { + mDisallowHitRegion.setEmpty(); + } else { + mDisallowHitRegion.set(disallowHitRegion); + } } /** @@ -220,6 +227,9 @@ public class DragAndDropPolicy { */ @Nullable Target getTargetAtLocation(int x, int y) { + if (mDisallowHitRegion.contains(x, y)) { + return null; + } for (int i = mTargets.size() - 1; i >= 0; i--) { DragAndDropPolicy.Target t = mTargets.get(i); if (t.hitRegion.contains(x, y)) { @@ -242,7 +252,7 @@ public class DragAndDropPolicy { // Update launch options for the split side we are targeting. position = leftOrTop ? SPLIT_POSITION_TOP_OR_LEFT : SPLIT_POSITION_BOTTOM_OR_RIGHT; // Add some data for logging splitscreen once it is invoked - mSplitScreen.logOnDroppedToSplit(position, mLoggerSessionId); + mSplitScreen.onDroppedToSplit(position, mLoggerSessionId); } final ClipDescription description = data.getDescription(); @@ -267,50 +277,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..fe42822ab6a1 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 @@ -18,10 +18,16 @@ package com.android.wm.shell.draganddrop; import static android.app.StatusBarManager.DISABLE_NONE; import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; +import static android.content.pm.ActivityInfo.CONFIG_ASSETS_PATHS; +import static android.content.pm.ActivityInfo.CONFIG_UI_MODE; import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; 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.draganddrop.DragAndDropPolicy.Target.TYPE_SPLIT_BOTTOM; +import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_SPLIT_LEFT; +import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_SPLIT_RIGHT; +import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_SPLIT_TOP; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; @@ -68,6 +74,7 @@ public class DragLayout extends LinearLayout { private final SplitScreenController mSplitScreenController; private final IconProvider mIconProvider; private final StatusBarManager mStatusBarManager; + private final Configuration mLastConfiguration = new Configuration(); private DragAndDropPolicy.Target mCurrentTarget = null; private DropZoneView mDropZoneView1; @@ -88,6 +95,7 @@ public class DragLayout extends LinearLayout { mIconProvider = iconProvider; mPolicy = new DragAndDropPolicy(context, splitScreenController); mStatusBarManager = context.getSystemService(StatusBarManager.class); + mLastConfiguration.setTo(context.getResources().getConfiguration()); mDisplayMargin = context.getResources().getDimensionPixelSize( R.dimen.drop_layout_display_margin); @@ -105,12 +113,16 @@ 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); } @Override public WindowInsets onApplyWindowInsets(WindowInsets insets) { - mInsets = insets.getInsets(Type.systemBars() | Type.displayCutout()); + mInsets = insets.getInsets(Type.tappableElement() | Type.displayCutout()); recomputeDropTargets(); final int orientation = getResources().getConfiguration().orientation; @@ -124,11 +136,6 @@ public class DragLayout extends LinearLayout { return super.onApplyWindowInsets(insets); } - public void onThemeChange() { - mDropZoneView1.onThemeChange(); - mDropZoneView2.onThemeChange(); - } - public void onConfigChanged(Configuration newConfig) { if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE && getOrientation() != HORIZONTAL) { @@ -139,6 +146,15 @@ public class DragLayout extends LinearLayout { setOrientation(LinearLayout.VERTICAL); updateContainerMargins(newConfig.orientation); } + + final int diff = newConfig.diff(mLastConfiguration); + final boolean themeChanged = (diff & CONFIG_ASSETS_PATHS) != 0 + || (diff & CONFIG_UI_MODE) != 0; + if (themeChanged) { + mDropZoneView1.onThemeChange(); + mDropZoneView2.onThemeChange(); + } + mLastConfiguration.setTo(newConfig); } private void updateContainerMarginsForSingleTask() { @@ -307,10 +323,29 @@ public class DragLayout extends LinearLayout { animateSplitContainers(true, null /* animCompleteCallback */); animateHighlight(target); } - } else { + } else if (mCurrentTarget.type != target.type) { // Switching between targets mDropZoneView1.animateSwitch(); mDropZoneView2.animateSwitch(); + // Announce for accessibility. + switch (target.type) { + case TYPE_SPLIT_LEFT: + mDropZoneView1.announceForAccessibility( + mContext.getString(R.string.accessibility_split_left)); + break; + case TYPE_SPLIT_RIGHT: + mDropZoneView2.announceForAccessibility( + mContext.getString(R.string.accessibility_split_right)); + break; + case TYPE_SPLIT_TOP: + mDropZoneView1.announceForAccessibility( + mContext.getString(R.string.accessibility_split_top)); + break; + case TYPE_SPLIT_BOTTOM: + mDropZoneView2.announceForAccessibility( + mContext.getString(R.string.accessibility_split_bottom)); + break; + } } mCurrentTarget = target; } @@ -342,7 +377,9 @@ public class DragLayout extends LinearLayout { // Start animating the drop UI out with the drag surface hide(event, dropCompleteCallback); - hideDragSurface(dragSurface); + if (handledDrop) { + hideDragSurface(dragSurface); + } return handledDrop; } @@ -420,12 +457,10 @@ public class DragLayout extends LinearLayout { } private void animateHighlight(DragAndDropPolicy.Target target) { - if (target.type == DragAndDropPolicy.Target.TYPE_SPLIT_LEFT - || target.type == DragAndDropPolicy.Target.TYPE_SPLIT_TOP) { + if (target.type == TYPE_SPLIT_LEFT || target.type == TYPE_SPLIT_TOP) { mDropZoneView1.setShowingHighlight(true); mDropZoneView2.setShowingHighlight(false); - } else if (target.type == DragAndDropPolicy.Target.TYPE_SPLIT_RIGHT - || target.type == DragAndDropPolicy.Target.TYPE_SPLIT_BOTTOM) { + } else if (target.type == TYPE_SPLIT_RIGHT || target.type == TYPE_SPLIT_BOTTOM) { mDropZoneView1.setShowingHighlight(false); mDropZoneView2.setShowingHighlight(true); } 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..48487bc4a3d6 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,33 +16,35 @@ 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.SparseArray; import android.view.SurfaceControl; import com.android.internal.protolog.common.ProtoLog; import com.android.wm.shell.ShellTaskOrganizer; -import com.android.wm.shell.common.SyncTransactionQueue; +import com.android.wm.shell.desktopmode.DesktopModeStatus; +import com.android.wm.shell.desktopmode.DesktopModeTaskRepository; import com.android.wm.shell.protolog.ShellProtoLogGroup; +import com.android.wm.shell.sysui.ShellInit; +import com.android.wm.shell.transition.Transitions; +import com.android.wm.shell.windowdecor.WindowDecorViewModel; import java.io.PrintWriter; +import java.util.Optional; /** * {@link ShellTaskOrganizer.TaskListener} for {@link * ShellTaskOrganizer#TASK_LISTENER_TYPE_FREEFORM}. */ -public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener { +public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener, + ShellTaskOrganizer.FocusListener { private static final String TAG = "FreeformTaskListener"; - private final SyncTransactionQueue mSyncQueue; + private final ShellTaskOrganizer mShellTaskOrganizer; + private final Optional<DesktopModeTaskRepository> mDesktopModeTaskRepository; + private final WindowDecorViewModel mWindowDecorationViewModel; private final SparseArray<State> mTasks = new SparseArray<>(); @@ -51,14 +53,30 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener { SurfaceControl mLeash; } - public FreeformTaskListener(SyncTransactionQueue syncQueue) { - mSyncQueue = syncQueue; + public FreeformTaskListener( + ShellInit shellInit, + ShellTaskOrganizer shellTaskOrganizer, + Optional<DesktopModeTaskRepository> desktopModeTaskRepository, + WindowDecorViewModel windowDecorationViewModel) { + mShellTaskOrganizer = shellTaskOrganizer; + mWindowDecorationViewModel = windowDecorationViewModel; + mDesktopModeTaskRepository = desktopModeTaskRepository; + if (shellInit != null) { + shellInit.addInitCallback(this::onInit, this); + } + } + + private void onInit() { + mShellTaskOrganizer.addListenerForType(this, TASK_LISTENER_TYPE_FREEFORM); + if (DesktopModeStatus.isAnyEnabled()) { + mShellTaskOrganizer.addFocusListener(this); + } } @Override public void onTaskAppeared(RunningTaskInfo taskInfo, SurfaceControl leash) { if (mTasks.get(taskInfo.taskId) != null) { - throw new RuntimeException("Task appeared more than once: #" + taskInfo.taskId); + throw new IllegalStateException("Task appeared more than once: #" + taskInfo.taskId); } ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TASK_ORG, "Freeform Task Appeared: #%d", taskInfo.taskId); @@ -66,47 +84,79 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener { state.mTaskInfo = taskInfo; state.mLeash = leash; mTasks.put(taskInfo.taskId, state); + if (!Transitions.ENABLE_SHELL_TRANSITIONS) { + SurfaceControl.Transaction t = new SurfaceControl.Transaction(); + mWindowDecorationViewModel.onTaskOpening(taskInfo, leash, t, t); + t.apply(); + } - 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); - }); + if (DesktopModeStatus.isAnyEnabled()) { + mDesktopModeTaskRepository.ifPresent(repository -> { + repository.addOrMoveFreeformTaskToTop(taskInfo.taskId); + if (taskInfo.isVisible) { + if (repository.addActiveTask(taskInfo.taskId)) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE, + "Adding active freeform task: #%d", taskInfo.taskId); + } + repository.updateVisibleFreeformTasks(taskInfo.taskId, true); + } + }); + } } @Override public void onTaskVanished(RunningTaskInfo taskInfo) { - State state = mTasks.get(taskInfo.taskId); - if (state == null) { - Slog.e(TAG, "Task already vanished: #" + taskInfo.taskId); - return; - } ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TASK_ORG, "Freeform Task Vanished: #%d", taskInfo.taskId); mTasks.remove(taskInfo.taskId); + + if (DesktopModeStatus.isAnyEnabled()) { + mDesktopModeTaskRepository.ifPresent(repository -> { + repository.removeFreeformTask(taskInfo.taskId); + if (repository.removeActiveTask(taskInfo.taskId)) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE, + "Removing active freeform task: #%d", taskInfo.taskId); + } + repository.updateVisibleFreeformTasks(taskInfo.taskId, false); + }); + } + + if (!Transitions.ENABLE_SHELL_TRANSITIONS) { + mWindowDecorationViewModel.destroyWindowDecoration(taskInfo); + } } @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 state = mTasks.get(taskInfo.taskId); + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TASK_ORG, "Freeform Task Info Changed: #%d", taskInfo.taskId); + mWindowDecorationViewModel.onTaskInfoChanged(taskInfo); state.mTaskInfo = taskInfo; + if (DesktopModeStatus.isAnyEnabled()) { + mDesktopModeTaskRepository.ifPresent(repository -> { + if (taskInfo.isVisible) { + if (repository.addActiveTask(taskInfo.taskId)) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE, + "Adding active freeform task: #%d", taskInfo.taskId); + } + } + repository.updateVisibleFreeformTasks(taskInfo.taskId, taskInfo.isVisible); + }); + } + } - final Rect taskBounds = taskInfo.configuration.windowConfiguration.getBounds(); - final SurfaceControl leash = state.mLeash; - mSyncQueue.runInSync(t -> { - Point taskPosition = taskInfo.positionInParent; - t.setPosition(leash, taskPosition.x, taskPosition.y) - .setWindowCrop(leash, taskBounds.width(), taskBounds.height()) - .show(leash); - }); + @Override + public void onFocusTaskChanged(RunningTaskInfo taskInfo) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TASK_ORG, + "Freeform Task Focus Changed: #%d focused=%b", + taskInfo.taskId, taskInfo.isFocused); + if (DesktopModeStatus.isAnyEnabled() && taskInfo.isFocused) { + mDesktopModeTaskRepository.ifPresent(repository -> { + repository.addOrMoveFreeformTaskToTop(taskInfo.taskId); + }); + } } @Override @@ -138,16 +188,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..04fc79acadbd --- /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..e1a56a1a5a7a --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java @@ -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.freeform; + +import android.app.ActivityManager; +import android.content.Context; +import android.os.IBinder; +import android.view.SurfaceControl; +import android.view.WindowManager; +import android.window.TransitionInfo; +import android.window.WindowContainerToken; + +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; + +import com.android.wm.shell.sysui.ShellInit; +import com.android.wm.shell.transition.Transitions; +import com.android.wm.shell.windowdecor.WindowDecorViewModel; + +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 final Transitions mTransitions; + private final WindowDecorViewModel mWindowDecorViewModel; + + private final Map<IBinder, List<ActivityManager.RunningTaskInfo>> mTransitionToTaskInfo = + new HashMap<>(); + + public FreeformTaskTransitionObserver( + Context context, + ShellInit shellInit, + Transitions transitions, + WindowDecorViewModel windowDecorViewModel) { + mTransitions = transitions; + mWindowDecorViewModel = windowDecorViewModel; + 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<ActivityManager.RunningTaskInfo> taskInfoList = new ArrayList<>(); + final ArrayList<WindowContainerToken> taskParents = new ArrayList<>(); + for (TransitionInfo.Change change : info.getChanges()) { + if ((change.getFlags() & TransitionInfo.FLAG_IS_WALLPAPER) != 0) { + continue; + } + + final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo(); + if (taskInfo == null || taskInfo.taskId == -1) { + continue; + } + // Filter out non-leaf tasks. Freeform/fullscreen don't nest tasks, but split-screen + // does, so this prevents adding duplicate captions in that scenario. + if (change.getParent() != null + && info.getChange(change.getParent()).getTaskInfo() != null) { + // This logic relies on 2 assumptions: 1 is that child tasks will be visited before + // parents (due to how z-order works). 2 is that no non-tasks are interleaved + // between tasks (hierarchically). + taskParents.add(change.getParent()); + } + if (taskParents.contains(change.getContainer())) { + continue; + } + + switch (change.getMode()) { + case WindowManager.TRANSIT_OPEN: + onOpenTransitionReady(change, startT, finishT); + break; + case WindowManager.TRANSIT_TO_FRONT: + onToFrontTransitionReady(change, startT, finishT); + break; + case WindowManager.TRANSIT_CLOSE: { + taskInfoList.add(change.getTaskInfo()); + onCloseTransitionReady(change, startT, finishT); + break; + } + case WindowManager.TRANSIT_CHANGE: + onChangeTransitionReady(change, startT, finishT); + break; + } + mWindowDecorViewModel.onTransitionReady(transition, info, change); + } + mTransitionToTaskInfo.put(transition, taskInfoList); + } + + private void onOpenTransitionReady( + TransitionInfo.Change change, + SurfaceControl.Transaction startT, + SurfaceControl.Transaction finishT) { + mWindowDecorViewModel.onTaskOpening( + change.getTaskInfo(), change.getLeash(), startT, finishT); + } + + private void onCloseTransitionReady( + TransitionInfo.Change change, + SurfaceControl.Transaction startT, + SurfaceControl.Transaction finishT) { + mWindowDecorViewModel.onTaskClosing(change.getTaskInfo(), startT, finishT); + } + + private void onChangeTransitionReady( + TransitionInfo.Change change, + SurfaceControl.Transaction startT, + SurfaceControl.Transaction finishT) { + mWindowDecorViewModel.onTaskChanging( + change.getTaskInfo(), change.getLeash(), startT, finishT); + } + + private void onToFrontTransitionReady( + TransitionInfo.Change change, + SurfaceControl.Transaction startT, + SurfaceControl.Transaction finishT) { + mWindowDecorViewModel.onTaskChanging( + change.getTaskInfo(), change.getLeash(), startT, finishT); + } + + @Override + public void onTransitionStarting(@NonNull IBinder transition) {} + + @Override + public void onTransitionMerged(@NonNull IBinder merged, @NonNull IBinder playing) { + final List<ActivityManager.RunningTaskInfo> infoOfMerged = + mTransitionToTaskInfo.get(merged); + if (infoOfMerged == 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; + } + mTransitionToTaskInfo.remove(merged); + + final List<ActivityManager.RunningTaskInfo> infoOfPlaying = + mTransitionToTaskInfo.get(playing); + if (infoOfPlaying != null) { + infoOfPlaying.addAll(infoOfMerged); + } else { + mTransitionToTaskInfo.put(playing, infoOfMerged); + } + + mWindowDecorViewModel.onTransitionMerged(merged, playing); + } + + @Override + public void onTransitionFinished(@NonNull IBinder transition, boolean aborted) { + final List<ActivityManager.RunningTaskInfo> taskInfo = + mTransitionToTaskInfo.getOrDefault(transition, Collections.emptyList()); + mTransitionToTaskInfo.remove(transition); + mWindowDecorViewModel.onTransitionFinished(transition); + for (int i = 0; i < taskInfo.size(); ++i) { + mWindowDecorViewModel.destroyWindowDecoration(taskInfo.get(i)); + } + } +}
\ No newline at end of file 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/freeform/OWNERS b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/OWNERS new file mode 100644 index 000000000000..0c2d5c49f830 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/OWNERS @@ -0,0 +1,2 @@ +# WM shell sub-module freeform owners +madym@google.com 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..998728d65e6a 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,17 +16,13 @@ package com.android.wm.shell.fullscreen; -import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; - import static com.android.wm.shell.ShellTaskOrganizer.TASK_LISTENER_TYPE_FULLSCREEN; import static com.android.wm.shell.ShellTaskOrganizer.taskListenerTypeToString; +import android.app.ActivityManager; import android.app.ActivityManager.RunningTaskInfo; -import android.app.TaskInfo; import android.graphics.Point; -import android.util.Slog; import android.util.SparseArray; -import android.util.SparseBooleanArray; import android.view.SurfaceControl; import androidx.annotation.NonNull; @@ -36,90 +32,140 @@ 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 { 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> mTasks = new SparseArray<>(); - public FullscreenTaskListener(SyncTransactionQueue syncQueue, - Optional<FullscreenUnfoldController> unfoldController) { - this(syncQueue, unfoldController, Optional.empty()); + private static class State { + RunningTaskInfo mTaskInfo; + SurfaceControl mLeash; + } + private final SyncTransactionQueue mSyncQueue; + private final Optional<RecentTasksController> mRecentTasksOptional; + private final Optional<WindowDecorViewModel> 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> 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 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); + boolean createdWindowDecor = false; + if (mWindowDecorViewModelOptional.isPresent()) { + SurfaceControl.Transaction t = new SurfaceControl.Transaction(); + createdWindowDecor = mWindowDecorViewModelOptional.get() + .onTaskOpening(taskInfo, leash, t, t); + t.apply(); + } + if (!createdWindowDecor) { + mSyncQueue.runInSync(t -> { + if (!leash.isValid()) { + // Task vanished before sync completion + return; + } + // 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); + if (taskInfo.isVisible) { + t.show(leash); + } + }); + } } @Override public void onTaskInfoChanged(RunningTaskInfo taskInfo) { - if (Transitions.ENABLE_SHELL_TRANSITIONS) return; + final State state = mTasks.get(taskInfo.taskId); + final Point oldPositionInParent = state.mTaskInfo.positionInParent; + boolean oldVisible = state.mTaskInfo.isVisible; - mAnimatableTasksListener.onTaskInfoChanged(taskInfo); + if (mWindowDecorViewModelOptional.isPresent()) { + mWindowDecorViewModelOptional.get().onTaskInfoChanged(taskInfo); + } + state.mTaskInfo = taskInfo; + if (Transitions.ENABLE_SHELL_TRANSITIONS) return; 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; + boolean positionInParentChanged = !oldPositionInParent.equals(positionInParent); + boolean becameVisible = !oldVisible && state.mTaskInfo.isVisible; + + if (becameVisible || positionInParentChanged) { mSyncQueue.runInSync(t -> { - t.setPosition(data.surface, positionInParent.x, positionInParent.y); + if (!state.mLeash.isValid()) { + // Task vanished before sync completion + return; + } + if (becameVisible) { + t.show(state.mLeash); + } + 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); - return; - } - - mAnimatableTasksListener.onTaskVanished(taskInfo); - mDataByTaskId.remove(taskInfo.taskId); - + public void onTaskVanished(ActivityManager.RunningTaskInfo taskInfo) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TASK_ORG, "Fullscreen Task Vanished: #%d", taskInfo.taskId); + mTasks.remove(taskInfo.taskId); + + if (Transitions.ENABLE_SHELL_TRANSITIONS) return; + if (mWindowDecorViewModelOptional.isPresent()) { + mWindowDecorViewModelOptional.get().destroyWindowDecoration(taskInfo); + } } private void updateRecentsForVisibleFullscreenTask(RunningTaskInfo taskInfo) { @@ -143,95 +189,21 @@ 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 public String toString() { 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..32894cdc5aec 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); @@ -109,6 +117,7 @@ class HideDisplayCutoutOrganizer extends DisplayAreaOrganizer { @Override public void onDisplayAreaAppeared(@NonNull DisplayAreaInfo displayAreaInfo, @NonNull SurfaceControl leash) { + leash.setUnreleasedWarningCallSite("HideDisplayCutoutOrganizer.onDisplayAreaAppeared"); if (!addDisplayAreaInfoAndLeashToMap(displayAreaInfo, leash)) { return; } @@ -128,9 +137,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..318a49a8de31 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 @@ -16,10 +16,13 @@ package com.android.wm.shell.kidsmode; +import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; 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_UNDEFINED; +import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE; +import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE; import static android.view.Display.DEFAULT_DISPLAY; import android.app.ActivityManager; @@ -32,9 +35,11 @@ import android.graphics.Rect; import android.os.Binder; import android.os.Handler; import android.os.IBinder; +import android.view.Display; import android.view.InsetsSource; import android.view.InsetsState; import android.view.SurfaceControl; +import android.view.WindowInsets; import android.window.ITaskOrganizerController; import android.window.TaskAppearedInfo; import android.window.WindowContainerToken; @@ -42,6 +47,7 @@ import android.window.WindowContainerTransaction; import androidx.annotation.NonNull; +import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.DisplayController; @@ -50,11 +56,12 @@ 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; -import java.util.Objects; import java.util.Optional; /** @@ -66,16 +73,23 @@ public class KidsModeTaskOrganizer extends ShellTaskOrganizer { private static final String TAG = "KidsModeTaskOrganizer"; private static final int[] CONTROLLED_ACTIVITY_TYPES = - {ACTIVITY_TYPE_UNDEFINED, ACTIVITY_TYPE_STANDARD}; + {ACTIVITY_TYPE_UNDEFINED, ACTIVITY_TYPE_STANDARD, ACTIVITY_TYPE_HOME}; private static final int[] CONTROLLED_WINDOWING_MODES = {WINDOWING_MODE_FULLSCREEN, WINDOWING_MODE_UNDEFINED}; private final Handler mMainHandler; private final Context mContext; + private final ShellCommandHandler mShellCommandHandler; private final SyncTransactionQueue mSyncQueue; private final DisplayController mDisplayController; private final DisplayInsetsController mDisplayInsetsController; + /** + * The value of the {@link R.bool.config_reverseDefaultRotation} property which defines how + * {@link Display#getRotation} values are mapped to screen orientations + */ + private final boolean mReverseDefaultRotationEnabled; + @VisibleForTesting ActivityManager.RunningTaskInfo mLaunchRootTask; @VisibleForTesting @@ -90,6 +104,8 @@ public class KidsModeTaskOrganizer extends ShellTaskOrganizer { private KidsModeSettingsObserver mKidsModeSettingsObserver; private boolean mEnabled; + private ActivityManager.RunningTaskInfo mHomeTask; + private final BroadcastReceiver mUserSwitchIntentReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { @@ -124,14 +140,34 @@ public class KidsModeTaskOrganizer extends ShellTaskOrganizer { new DisplayInsetsController.OnInsetsChangedListener() { @Override public void insetsChanged(InsetsState insetsState) { - // Update bounds only when the insets of navigation bar or task bar is changed. - if (Objects.equals(insetsState.peekSource(InsetsState.ITYPE_NAVIGATION_BAR), - mInsetsState.peekSource(InsetsState.ITYPE_NAVIGATION_BAR)) - && Objects.equals(insetsState.peekSource( - InsetsState.ITYPE_EXTRA_NAVIGATION_BAR), - mInsetsState.peekSource(InsetsState.ITYPE_EXTRA_NAVIGATION_BAR))) { + final boolean[] navigationBarChanged = {false}; + InsetsState.traverse(insetsState, mInsetsState, new InsetsState.OnTraverseCallbacks() { + @Override + public void onIdMatch(InsetsSource source1, InsetsSource source2) { + if (source1.getType() == WindowInsets.Type.navigationBars() + && !source1.equals(source2)) { + navigationBarChanged[0] = true; + } + } + + @Override + public void onIdNotFoundInState1(int index2, InsetsSource source2) { + if (source2.getType() == WindowInsets.Type.navigationBars()) { + navigationBarChanged[0] = true; + } + } + + @Override + public void onIdNotFoundInState2(int index1, InsetsSource source1) { + if (source1.getType() == WindowInsets.Type.navigationBars()) { + navigationBarChanged[0] = true; + } + } + }); + if (!navigationBarChanged[0]) { return; } + // Update bounds only when the insets of navigation bar or task bar is changed. mInsetsState.set(insetsState); updateBounds(); } @@ -139,45 +175,65 @@ 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); + mReverseDefaultRotationEnabled = context.getResources().getBoolean( + R.bool.config_reverseDefaultRotation); } 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); + mReverseDefaultRotationEnabled = context.getResources().getBoolean( + R.bool.config_reverseDefaultRotation); } /** * 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); } @@ -200,6 +256,13 @@ public class KidsModeTaskOrganizer extends ShellTaskOrganizer { } super.onTaskAppeared(taskInfo, leash); + // Only allow home to draw under system bars. + if (taskInfo.getActivityType() == ACTIVITY_TYPE_HOME) { + final WindowContainerTransaction wct = getWindowContainerTransaction(); + wct.setBounds(taskInfo.token, new Rect(0, 0, mDisplayWidth, mDisplayHeight)); + mSyncQueue.queue(wct); + mHomeTask = taskInfo; + } mSyncQueue.runInSync(t -> { // Reset several properties back to fullscreen (PiP, for example, leaves all these // properties in a bad state). @@ -218,6 +281,11 @@ public class KidsModeTaskOrganizer extends ShellTaskOrganizer { mLaunchRootTask = taskInfo; } + if (mHomeTask != null && mHomeTask.taskId == taskInfo.taskId + && !taskInfo.equals(mHomeTask)) { + mHomeTask = taskInfo; + } + super.onTaskInfoChanged(taskInfo); } @@ -240,7 +308,14 @@ public class KidsModeTaskOrganizer extends ShellTaskOrganizer { // Needed since many Kids apps aren't optimised to support both orientations and it will be // hard for kids to understand the app compat mode. // TODO(229961548): Remove ignoreOrientationRequest exception for Kids Mode once possible. - setIsIgnoreOrientationRequestDisabled(true); + if (mReverseDefaultRotationEnabled) { + setOrientationRequestPolicy(/* isIgnoreOrientationRequestDisabled */ true, + /* fromOrientations */ new int[]{SCREEN_ORIENTATION_REVERSE_LANDSCAPE}, + /* toOrientations */ new int[]{SCREEN_ORIENTATION_LANDSCAPE}); + } else { + setOrientationRequestPolicy(/* isIgnoreOrientationRequestDisabled */ true, + /* fromOrientations */ null, /* toOrientations */ null); + } final DisplayLayout displayLayout = mDisplayController.getDisplayLayout(DEFAULT_DISPLAY); if (displayLayout != null) { mDisplayWidth = displayLayout.width(); @@ -261,7 +336,8 @@ public class KidsModeTaskOrganizer extends ShellTaskOrganizer { @VisibleForTesting void disable() { - setIsIgnoreOrientationRequestDisabled(false); + setOrientationRequestPolicy(/* isIgnoreOrientationRequestDisabled */ false, + /* fromOrientations */ null, /* toOrientations */ null); mDisplayInsetsController.removeInsetsChangedListener(DEFAULT_DISPLAY, mOnInsetsChangedListener); mDisplayController.removeDisplayWindowListener(mOnDisplaysChangedListener); @@ -272,6 +348,13 @@ public class KidsModeTaskOrganizer extends ShellTaskOrganizer { } mLaunchRootTask = null; mLaunchRootLeash = null; + if (mHomeTask != null && mHomeTask.token != null) { + final WindowContainerToken homeToken = mHomeTask.token; + final WindowContainerTransaction wct = getWindowContainerTransaction(); + wct.setBounds(homeToken, null); + mSyncQueue.queue(wct); + } + mHomeTask = null; unregisterOrganizer(); } @@ -297,25 +380,19 @@ 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, mDisplayWidth, mDisplayHeight); + }); + } } private Rect calculateBounds() { final Rect bounds = new Rect(0, 0, mDisplayWidth, mDisplayHeight); - final InsetsSource navBarSource = mInsetsState.peekSource(InsetsState.ITYPE_NAVIGATION_BAR); - final InsetsSource taskBarSource = mInsetsState.peekSource( - InsetsState.ITYPE_EXTRA_NAVIGATION_BAR); - if (navBarSource != null && !navBarSource.getFrame().isEmpty()) { - bounds.inset(navBarSource.calculateInsets(bounds, false /* ignoreVisibility */)); - } else if (taskBarSource != null && !taskBarSource.getFrame().isEmpty()) { - bounds.inset(taskBarSource.calculateInsets(bounds, false /* ignoreVisibility */)); - } else { - bounds.setEmpty(); - } + bounds.inset(mInsetsState.calculateInsets( + bounds, WindowInsets.Type.navigationBars(), false /* ignoreVisibility */)); return bounds; } @@ -326,11 +403,12 @@ public class KidsModeTaskOrganizer extends ShellTaskOrganizer { final WindowContainerTransaction wct = getWindowContainerTransaction(); final Rect taskBounds = calculateBounds(); wct.setBounds(mLaunchRootTask.token, taskBounds); + wct.setBounds(mHomeTask.token, new Rect(0, 0, mDisplayWidth, mDisplayHeight)); mSyncQueue.queue(wct); final SurfaceControl finalLeash = mLaunchRootLeash; mSyncQueue.runInSync(t -> { t.setPosition(finalLeash, taskBounds.left, taskBounds.top); - t.setWindowCrop(finalLeash, taskBounds.width(), taskBounds.height()); + t.setWindowCrop(finalLeash, mDisplayWidth, mDisplayHeight); }); } 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/BackgroundWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/BackgroundWindowManager.java index b310ee2095bf..71cc8df80cad 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/BackgroundWindowManager.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/BackgroundWindowManager.java @@ -104,7 +104,7 @@ public final class BackgroundWindowManager extends WindowlessWindowManager { } @Override - protected void attachToParentSurface(IWindow window, SurfaceControl.Builder b) { + protected SurfaceControl getParentSurface(IWindow window, WindowManager.LayoutParams attrs) { final SurfaceControl.Builder builder = new SurfaceControl.Builder(new SurfaceSession()) .setColorLayer() .setBufferSize(mDisplayBounds.width(), mDisplayBounds.height()) @@ -113,7 +113,7 @@ public final class BackgroundWindowManager extends WindowlessWindowManager { .setName(TAG) .setCallsite("BackgroundWindowManager#attachToParentSurface"); mLeash = builder.build(); - b.setParent(mLeash); + return mLeash; } /** Inflates background view on to the root surface. */ @@ -122,7 +122,8 @@ public final class BackgroundWindowManager extends WindowlessWindowManager { return false; } - mViewHost = new SurfaceControlViewHost(mContext, mContext.getDisplay(), this); + mViewHost = new SurfaceControlViewHost(mContext, mContext.getDisplay(), this, + "BackgroundWindowManager"); mBackgroundView = (View) LayoutInflater.from(mContext) .inflate(R.layout.background_panel, null /* root */); WindowManager.LayoutParams lp = new WindowManager.LayoutParams( 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..2ee334873780 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; @@ -31,23 +30,6 @@ public interface OneHanded { OneHandedController.SUPPORT_ONE_HANDED_MODE, false); /** - * Returns a binder that can be passed to an external process to manipulate OneHanded. - */ - default IOneHanded createExternalInterface() { - return null; - } - - /** - * 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 +63,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..679d4ca2ac48 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 @@ -24,21 +24,21 @@ import static com.android.wm.shell.onehanded.OneHandedState.STATE_ACTIVE; import static com.android.wm.shell.onehanded.OneHandedState.STATE_ENTERING; import static com.android.wm.shell.onehanded.OneHandedState.STATE_EXITING; import static com.android.wm.shell.onehanded.OneHandedState.STATE_NONE; +import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_ONE_HANDED; import 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; @@ -50,11 +50,18 @@ import com.android.wm.shell.R; import com.android.wm.shell.common.DisplayChangeController; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayLayout; +import com.android.wm.shell.common.ExternalInterfaceBinder; import com.android.wm.shell.common.RemoteCallable; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.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 +69,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 +79,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 +90,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 +101,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 +198,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 +220,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 +239,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 +253,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 +278,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,17 +291,25 @@ 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); + mShellController.addExternalInterface(KEY_EXTRA_SHELL_ONE_HANDED, + this::createExternalInterface, this); } public OneHanded asOneHanded() { return mImpl; } + private ExternalInterfaceBinder createExternalInterface() { + return new IOneHandedImpl(this); + } + @Override public Context getContext() { return mContext; @@ -594,7 +619,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 +630,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 +646,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 +689,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 +704,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); } /** @@ -684,29 +717,6 @@ public class OneHandedController implements RemoteCallable<OneHandedController>, */ @ExternalThread private class OneHandedImpl implements OneHanded { - private IOneHandedImpl mIOneHanded; - - @Override - public IOneHanded createExternalInterface() { - if (mIOneHanded != null) { - mIOneHanded.invalidate(); - } - mIOneHanded = new IOneHandedImpl(OneHandedController.this); - return mIOneHanded; - } - - @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(() -> { @@ -748,34 +758,13 @@ 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); - }); - } } /** * The interface for calls from outside the host process. */ @BinderThread - private static class IOneHandedImpl extends IOneHanded.Stub { + private static class IOneHandedImpl extends IOneHanded.Stub implements ExternalInterfaceBinder { private OneHandedController mController; IOneHandedImpl(OneHandedController controller) { @@ -785,7 +774,8 @@ public class OneHandedController implements RemoteCallable<OneHandedController>, /** * Invalidates this instance, preventing future calls from updating the controller. */ - void invalidate() { + @Override + public void invalidate() { mController = null; } 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..38ce16489b06 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 @@ -154,11 +154,17 @@ public class OneHandedDisplayAreaOrganizer extends DisplayAreaOrganizer { @Override public void onDisplayAreaAppeared(@NonNull DisplayAreaInfo displayAreaInfo, @NonNull SurfaceControl leash) { + leash.setUnreleasedWarningCallSite( + "OneHandedSiaplyAreaOrganizer.onDisplayAreaAppeared"); mDisplayAreaTokenMap.put(displayAreaInfo.token, leash); } @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/onehanded/OneHandedTouchHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedTouchHandler.java index 5b9f0c41e31e..4ec1351aa22b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedTouchHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedTouchHandler.java @@ -19,7 +19,7 @@ package com.android.wm.shell.onehanded; import static android.view.Display.DEFAULT_DISPLAY; import android.graphics.Rect; -import android.hardware.input.InputManager; +import android.hardware.input.InputManagerGlobal; import android.os.Looper; import android.view.InputChannel; import android.view.InputEvent; @@ -129,7 +129,7 @@ public class OneHandedTouchHandler implements OneHandedTransitionCallback { private void updateIsEnabled() { disposeInputChannel(); if (mIsEnabled) { - mInputMonitor = InputManager.getInstance().monitorGestureInput( + mInputMonitor = InputManagerGlobal.getInstance().monitorGestureInput( "onehanded-touch", DEFAULT_DISPLAY); try { mMainExecutor.executeBlocking(() -> { 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..78de5f3e7a1f 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 @@ -58,10 +59,25 @@ interface IPip { /** * Sets listener to get pinned stack animation callbacks. */ - oneway void setPinnedStackAnimationListener(IPipAnimationListener listener) = 3; + oneway void setPipAnimationListener(IPipAnimationListener listener) = 3; /** * Sets the shelf height and visibility. */ oneway void setShelfHeight(boolean visible, int shelfHeight) = 4; + + /** + * Sets the next pip animation type to be the alpha animation. + */ + oneway void setPipAnimationTypeToAlpha() = 5; + + /** + * Sets the height and visibility of the Launcher keep clear area. + */ + oneway void setLauncherKeepClearAreaHeight(boolean visible, int height) = 6; + + /** + * Sets the app icon size in pixel used by Launcher + */ + oneway void setLauncherAppIconSize(int iconSizePx) = 7; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/OWNERS b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/OWNERS index afddfab99a2b..ec09827fa4d1 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/OWNERS +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/OWNERS @@ -1,2 +1,3 @@ # WM shell sub-module pip owner hwwang@google.com +mateuszc@google.com 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..f34d2a827e69 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; /** @@ -29,14 +27,6 @@ import java.util.function.Consumer; */ @ExternalThread public interface Pip { - - /** - * Returns a binder that can be passed to an external process to manipulate PIP. - */ - default IPip createExternalInterface() { - return null; - } - /** * Expand PIP, it's possible that specific request to activate the window via Alt-tab. */ @@ -44,24 +34,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,35 +43,12 @@ 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. - * @param height to specify the height for shelf. - */ - default void setShelfHeight(boolean visible, int height) { - } - - /** - * 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) { - } - - /** - * Set the pinned stack with {@link PipAnimationController.AnimationType} - * - * @param animationType The pre-defined {@link PipAnimationController.AnimationType} - */ - default void setPinnedStackAnimationType(int animationType) { - } + default void setOnIsInPipStateChangedListener(Consumer<Boolean> callback) {} /** * Called when showing Pip menu. @@ -118,29 +67,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..4c53f607a5f8 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 @@ -28,14 +28,15 @@ import android.annotation.IntDef; import android.annotation.NonNull; import android.app.TaskInfo; import android.content.Context; +import android.content.pm.ActivityInfo; import android.graphics.Rect; -import android.view.Choreographer; import android.view.Surface; import android.view.SurfaceControl; import android.window.TaskSnapshot; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.graphics.SfVsyncFrameCallbackProvider; +import com.android.launcher3.icons.IconProvider; import com.android.wm.shell.animation.Interpolators; import com.android.wm.shell.transition.Transitions; @@ -183,7 +184,7 @@ public class PipAnimationController { return mCurrentAnimator; } - PipTransitionAnimator getCurrentAnimator() { + public PipTransitionAnimator getCurrentAnimator() { return mCurrentAnimator; } @@ -196,6 +197,17 @@ public class PipAnimationController { } /** + * Returns true if the PiP window is currently being animated. + */ + public boolean isAnimating() { + PipAnimationController.PipTransitionAnimator animator = getCurrentAnimator(); + if (animator != null && animator.isRunning()) { + return true; + } + return false; + } + + /** * Quietly cancel the animator by removing the listeners first. */ static void quietCancel(@NonNull ValueAnimator animator) { @@ -279,14 +291,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 +307,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,20 +363,28 @@ public class PipAnimationController { } void setColorContentOverlay(Context context) { - final SurfaceControl.Transaction tx = newSurfaceControlTransaction(); - if (mContentOverlay != null) { - mContentOverlay.detach(tx); - } - mContentOverlay = new PipContentOverlay.PipColorOverlay(context); - mContentOverlay.attach(tx, mLeash); + reattachContentOverlay(new PipContentOverlay.PipColorOverlay(context)); } void setSnapshotContentOverlay(TaskSnapshot snapshot, Rect sourceRectHint) { - final SurfaceControl.Transaction tx = newSurfaceControlTransaction(); + reattachContentOverlay( + new PipContentOverlay.PipSnapshotOverlay(snapshot, sourceRectHint)); + } + + void setAppIconContentOverlay(Context context, Rect bounds, ActivityInfo activityInfo, + int appIconSizePx) { + reattachContentOverlay( + new PipContentOverlay.PipAppIconOverlay(context, bounds, + new IconProvider(context).getIcon(activityInfo), appIconSizePx)); + } + + private void reattachContentOverlay(PipContentOverlay overlay) { + final SurfaceControl.Transaction tx = + mSurfaceControlTransactionFactory.getTransaction(); if (mContentOverlay != null) { mContentOverlay.detach(tx); } - mContentOverlay = new PipContentOverlay.PipSnapshotOverlay(snapshot, sourceRectHint); + mContentOverlay = overlay; mContentOverlay.attach(tx, mLeash); } @@ -406,7 +429,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 +464,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) { @@ -565,8 +578,9 @@ public class PipAnimationController { final Rect base = getBaseValue(); final Rect start = getStartValue(); final Rect end = getEndValue(); + Rect bounds = mRectEvaluator.evaluate(fraction, start, end); if (mContentOverlay != null) { - mContentOverlay.onAnimationUpdate(tx, fraction); + mContentOverlay.onAnimationUpdate(tx, bounds, fraction); } if (rotatedEndRect != null) { // Animate the bounds in a different orientation. It only happens when @@ -574,7 +588,6 @@ public class PipAnimationController { applyRotation(tx, leash, fraction, start, end); return; } - Rect bounds = mRectEvaluator.evaluate(fraction, start, end); float angle = (1.0f - fraction) * startingAngle; setCurrentValue(bounds); if (inScaleTransition() || sourceHintRect == null) { @@ -591,7 +604,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..867162be4c6d 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 @@ -16,24 +16,19 @@ package com.android.wm.shell.pip; -import static android.util.TypedValue.COMPLEX_UNIT_DIP; - import android.annotation.NonNull; import android.annotation.Nullable; import android.app.PictureInPictureParams; import android.content.Context; import android.content.pm.ActivityInfo; import android.content.res.Resources; -import android.graphics.Point; -import android.graphics.PointF; import android.graphics.Rect; import android.util.DisplayMetrics; import android.util.Size; -import android.util.TypedValue; import android.view.Gravity; import com.android.wm.shell.R; -import com.android.wm.shell.common.DisplayLayout; +import com.android.wm.shell.pip.phone.PipSizeSpecHandler; import java.io.PrintWriter; @@ -45,30 +40,29 @@ public class PipBoundsAlgorithm { private static final String TAG = PipBoundsAlgorithm.class.getSimpleName(); private static final float INVALID_SNAP_FRACTION = -1f; - private final @NonNull PipBoundsState mPipBoundsState; + @NonNull private final PipBoundsState mPipBoundsState; + @NonNull protected final PipSizeSpecHandler mPipSizeSpecHandler; private final PipSnapAlgorithm mSnapAlgorithm; + private final PipKeepClearAlgorithmInterface mPipKeepClearAlgorithm; - private float mDefaultSizePercent; - private float mMinAspectRatioForMinSize; - private float mMaxAspectRatioForMinSize; private float mDefaultAspectRatio; private float mMinAspectRatio; private float mMaxAspectRatio; private int mDefaultStackGravity; - private int mDefaultMinSize; - private int mOverridableMinSize; - protected Point mScreenEdgeInsets; public PipBoundsAlgorithm(Context context, @NonNull PipBoundsState pipBoundsState, - @NonNull PipSnapAlgorithm pipSnapAlgorithm) { + @NonNull PipSnapAlgorithm pipSnapAlgorithm, + @NonNull PipKeepClearAlgorithmInterface pipKeepClearAlgorithm, + @NonNull PipSizeSpecHandler pipSizeSpecHandler) { mPipBoundsState = pipBoundsState; mSnapAlgorithm = pipSnapAlgorithm; + mPipKeepClearAlgorithm = pipKeepClearAlgorithm; + mPipSizeSpecHandler = pipSizeSpecHandler; 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 // triggers a configuration change and the resources to be reloaded. mPipBoundsState.setAspectRatio(mDefaultAspectRatio); - mPipBoundsState.setMinEdgeSize(mDefaultMinSize); } /** @@ -80,27 +74,15 @@ public class PipBoundsAlgorithm { R.dimen.config_pictureInPictureDefaultAspectRatio); mDefaultStackGravity = res.getInteger( R.integer.config_defaultPictureInPictureGravity); - mDefaultMinSize = res.getDimensionPixelSize( - R.dimen.default_minimal_size_pip_resizable_task); - mOverridableMinSize = res.getDimensionPixelSize( - R.dimen.overridable_minimal_size_pip_resizable_task); final String screenEdgeInsetsDpString = res.getString( R.string.config_defaultPictureInPictureScreenEdgeInsets); final Size screenEdgeInsetsDp = !screenEdgeInsetsDpString.isEmpty() ? Size.parseSize(screenEdgeInsetsDpString) : null; - mScreenEdgeInsets = screenEdgeInsetsDp == null ? new Point() - : new Point(dpToPx(screenEdgeInsetsDp.getWidth(), res.getDisplayMetrics()), - dpToPx(screenEdgeInsetsDp.getHeight(), res.getDisplayMetrics())); mMinAspectRatio = res.getFloat( com.android.internal.R.dimen.config_pictureInPictureMinAspectRatio); mMaxAspectRatio = res.getFloat( com.android.internal.R.dimen.config_pictureInPictureMaxAspectRatio); - mDefaultSizePercent = res.getFloat( - R.dimen.config_pictureInPictureDefaultSizePercent); - mMaxAspectRatioForMinSize = res.getFloat( - R.dimen.config_pictureInPictureAspectRatioLimitForMinSize); - mMinAspectRatioForMinSize = 1f / mMaxAspectRatioForMinSize; } /** @@ -129,8 +111,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 +133,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. */ @@ -163,8 +159,9 @@ public class PipBoundsAlgorithm { if (windowLayout.minWidth > 0 && windowLayout.minHeight > 0) { // If either dimension is smaller than the allowed minimum, adjust them // according to mOverridableMinSize - return new Size(Math.max(windowLayout.minWidth, mOverridableMinSize), - Math.max(windowLayout.minHeight, mOverridableMinSize)); + return new Size( + Math.max(windowLayout.minWidth, mPipSizeSpecHandler.getOverrideMinEdgeSize()), + Math.max(windowLayout.minHeight, mPipSizeSpecHandler.getOverrideMinEdgeSize())); } return null; } @@ -226,28 +223,13 @@ public class PipBoundsAlgorithm { final float snapFraction = mSnapAlgorithm.getSnapFraction(stackBounds, getMovementBounds(stackBounds), mPipBoundsState.getStashedState()); - final Size overrideMinSize = mPipBoundsState.getOverrideMinSize(); final Size size; if (useCurrentMinEdgeSize || useCurrentSize) { - // The default minimum edge size, or the override min edge size if set. - final int defaultMinEdgeSize = overrideMinSize == null ? mDefaultMinSize - : mPipBoundsState.getOverrideMinEdgeSize(); - final int minEdgeSize = useCurrentMinEdgeSize ? mPipBoundsState.getMinEdgeSize() - : defaultMinEdgeSize; - // Use the existing size but adjusted to the aspect ratio and min edge size. - size = getSizeForAspectRatio( - new Size(stackBounds.width(), stackBounds.height()), aspectRatio, minEdgeSize); + // Use the existing size but adjusted to the new aspect ratio. + size = mPipSizeSpecHandler.getSizeForAspectRatio( + new Size(stackBounds.width(), stackBounds.height()), aspectRatio); } else { - if (overrideMinSize != null) { - // The override minimal size is set, use that as the default size making sure it's - // adjusted to the aspect ratio. - size = adjustSizeToAspectRatio(overrideMinSize, aspectRatio); - } else { - // Calculate the default size using the display size and default min edge size. - final DisplayLayout displayLayout = mPipBoundsState.getDisplayLayout(); - size = getSizeForAspectRatio(aspectRatio, mDefaultMinSize, - displayLayout.width(), displayLayout.height()); - } + size = mPipSizeSpecHandler.getDefaultSize(aspectRatio); } final int left = (int) (stackBounds.centerX() - size.getWidth() / 2f); @@ -256,18 +238,6 @@ public class PipBoundsAlgorithm { mSnapAlgorithm.applySnapFraction(stackBounds, getMovementBounds(stackBounds), snapFraction); } - /** Adjusts the given size to conform to the given aspect ratio. */ - private Size adjustSizeToAspectRatio(@NonNull Size size, float aspectRatio) { - final float sizeAspectRatio = size.getWidth() / (float) size.getHeight(); - if (sizeAspectRatio > aspectRatio) { - // Size is wider, fix the width and increase the height - return new Size(size.getWidth(), (int) (size.getWidth() / aspectRatio)); - } else { - // Size is taller, fix the height and adjust the width. - return new Size((int) (size.getHeight() * aspectRatio), size.getHeight()); - } - } - /** * @return the default bounds to show the PIP, if a {@param snapFraction} and {@param size} are * provided, then it will apply the default bounds to the provided snap fraction and size. @@ -286,17 +256,9 @@ public class PipBoundsAlgorithm { final Size defaultSize; final Rect insetBounds = new Rect(); getInsetBounds(insetBounds); - final DisplayLayout displayLayout = mPipBoundsState.getDisplayLayout(); - final Size overrideMinSize = mPipBoundsState.getOverrideMinSize(); - if (overrideMinSize != null) { - // The override minimal size is set, use that as the default size making sure it's - // adjusted to the aspect ratio. - defaultSize = adjustSizeToAspectRatio(overrideMinSize, mDefaultAspectRatio); - } else { - // Calculate the default size using the display size and default min edge size. - defaultSize = getSizeForAspectRatio(mDefaultAspectRatio, - mDefaultMinSize, displayLayout.width(), displayLayout.height()); - } + + // Calculate the default size + defaultSize = mPipSizeSpecHandler.getDefaultSize(mDefaultAspectRatio); // Now that we have the default size, apply the snap fraction if valid or position the // bounds using the default gravity. @@ -318,12 +280,7 @@ public class PipBoundsAlgorithm { * Populates the bounds on the screen that the PIP can be visible in. */ public void getInsetBounds(Rect outRect) { - final DisplayLayout displayLayout = mPipBoundsState.getDisplayLayout(); - Rect insets = mPipBoundsState.getDisplayLayout().stableInsets(); - outRect.set(insets.left + mScreenEdgeInsets.x, - insets.top + mScreenEdgeInsets.y, - displayLayout.width() - insets.right - mScreenEdgeInsets.x, - displayLayout.height() - insets.bottom - mScreenEdgeInsets.y); + outRect.set(mPipSizeSpecHandler.getInsetBounds()); } /** @@ -388,71 +345,11 @@ public class PipBoundsAlgorithm { mSnapAlgorithm.applySnapFraction(stackBounds, movementBounds, snapFraction); } - public int getDefaultMinSize() { - return mDefaultMinSize; - } - /** * @return the pixels for a given dp value. */ private int dpToPx(float dpValue, DisplayMetrics dm) { - return (int) TypedValue.applyDimension(COMPLEX_UNIT_DIP, dpValue, dm); - } - - /** - * @return the size of the PiP at the given aspectRatio, ensuring that the minimum edge - * is at least minEdgeSize. - */ - public Size getSizeForAspectRatio(float aspectRatio, float minEdgeSize, int displayWidth, - int displayHeight) { - final int smallestDisplaySize = Math.min(displayWidth, displayHeight); - final int minSize = (int) Math.max(minEdgeSize, smallestDisplaySize * mDefaultSizePercent); - - final int width; - final int height; - if (aspectRatio <= mMinAspectRatioForMinSize || aspectRatio > mMaxAspectRatioForMinSize) { - // Beyond these points, we can just use the min size as the shorter edge - if (aspectRatio <= 1) { - // Portrait, width is the minimum size - width = minSize; - height = Math.round(width / aspectRatio); - } else { - // Landscape, height is the minimum size - height = minSize; - width = Math.round(height * aspectRatio); - } - } else { - // Within these points, we ensure that the bounds fit within the radius of the limits - // at the points - final float widthAtMaxAspectRatioForMinSize = mMaxAspectRatioForMinSize * minSize; - final float radius = PointF.length(widthAtMaxAspectRatioForMinSize, minSize); - height = (int) Math.round(Math.sqrt((radius * radius) - / (aspectRatio * aspectRatio + 1))); - width = Math.round(height * aspectRatio); - } - return new Size(width, height); - } - - /** - * @return the adjusted size so that it conforms to the given aspectRatio, ensuring that the - * minimum edge is at least minEdgeSize. - */ - public Size getSizeForAspectRatio(Size size, float aspectRatio, float minEdgeSize) { - final int smallestSize = Math.min(size.getWidth(), size.getHeight()); - final int minSize = (int) Math.max(minEdgeSize, smallestSize); - - final int width; - final int height; - if (aspectRatio <= 1) { - // Portrait, width is the minimum size. - width = minSize; - height = Math.round(width / aspectRatio); - } else { - // Landscape, height is the minimum size - height = minSize; - width = Math.round(height * aspectRatio); - } - return new Size(width, height); + return PipUtils.dpToPx(dpValue, dm); } /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsState.java index 17d7f5d0d567..9a775dff1f69 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsState.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsState.java @@ -30,20 +30,22 @@ import android.graphics.Rect; import android.os.RemoteException; import android.util.ArraySet; import android.util.Size; -import android.view.Display; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.protolog.common.ProtoLog; import com.android.internal.util.function.TriConsumer; import com.android.wm.shell.R; import com.android.wm.shell.common.DisplayLayout; +import com.android.wm.shell.pip.phone.PipSizeSpecHandler; import com.android.wm.shell.protolog.ShellProtoLogGroup; import java.io.PrintWriter; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.function.Consumer; @@ -76,6 +78,7 @@ public class PipBoundsState { private final @NonNull Rect mExpandedBounds = new Rect(); private final @NonNull Rect mNormalMovementBounds = new Rect(); private final @NonNull Rect mExpandedMovementBounds = new Rect(); + private final @NonNull PipDisplayLayoutState mPipDisplayLayoutState; private final Point mMaxSize = new Point(); private final Point mMinSize = new Point(); private final @NonNull Context mContext; @@ -83,13 +86,9 @@ public class PipBoundsState { private int mStashedState = STASH_TYPE_NONE; private int mStashOffset; private @Nullable PipReentryState mPipReentryState; + private final LauncherState mLauncherState = new LauncherState(); + private final @Nullable PipSizeSpecHandler mPipSizeSpecHandler; private @Nullable ComponentName mLastPipComponentName; - private int mDisplayId = Display.DEFAULT_DISPLAY; - private final @NonNull DisplayLayout mDisplayLayout = new DisplayLayout(); - /** The current minimum edge size of PIP. */ - private int mMinEdgeSize; - /** The preferred minimum (and default) size specified by apps. */ - private @Nullable Size mOverrideMinSize; private final @NonNull MotionBoundsState mMotionBoundsState = new MotionBoundsState(); private boolean mIsImeShowing; private int mImeHeight; @@ -97,6 +96,8 @@ public class PipBoundsState { private int mShelfHeight; /** Whether the user has resized the PIP manually. */ private boolean mHasUserResizedPip; + /** Whether the user has moved the PIP manually. */ + private boolean mHasUserMovedPip; /** * Areas defined by currently visible apps that they prefer to keep clear from overlays such as * the PiP. Restricted areas may only move the PiP a limited amount from its anchor position. @@ -115,14 +116,23 @@ public class PipBoundsState { * @see android.view.View#setPreferKeepClearRects */ private final Set<Rect> mUnrestrictedKeepClearAreas = new ArraySet<>(); + /** + * Additional to {@link #mUnrestrictedKeepClearAreas}, allow the caller to append named bounds + * as unrestricted keep clear area. Values in this map would be appended to + * {@link #getUnrestrictedKeepClearAreas()} and this is meant for internal usage only. + */ + private final Map<String, Rect> mNamedUnrestrictedKeepClearAreas = new HashMap<>(); private @Nullable Runnable mOnMinimalSizeChangeCallback; private @Nullable TriConsumer<Boolean, Integer, Boolean> mOnShelfVisibilityChangeCallback; private List<Consumer<Rect>> mOnPipExclusionBoundsChangeCallbacks = new ArrayList<>(); - public PipBoundsState(@NonNull Context context) { + public PipBoundsState(@NonNull Context context, PipSizeSpecHandler pipSizeSpecHandler, + PipDisplayLayoutState pipDisplayLayoutState) { mContext = context; reloadResources(); + mPipSizeSpecHandler = pipSizeSpecHandler; + mPipDisplayLayoutState = pipDisplayLayoutState; } /** Reloads the resources. */ @@ -279,6 +289,7 @@ public class PipBoundsState { if (changed) { clearReentryState(); setHasUserResizedPip(false); + setHasUserMovedPip(false); } } @@ -288,31 +299,16 @@ public class PipBoundsState { return mLastPipComponentName; } - /** Get the current display id. */ - public int getDisplayId() { - return mDisplayId; - } - - /** Set the current display id for the associated display layout. */ - public void setDisplayId(int displayId) { - mDisplayId = displayId; - } - /** Returns the display's bounds. */ @NonNull public Rect getDisplayBounds() { - return new Rect(0, 0, mDisplayLayout.width(), mDisplayLayout.height()); - } - - /** Update the display layout. */ - public void setDisplayLayout(@NonNull DisplayLayout displayLayout) { - mDisplayLayout.set(displayLayout); + return mPipDisplayLayoutState.getDisplayBounds(); } - /** Get the display layout. */ + /** Get a copy of the display layout. */ @NonNull public DisplayLayout getDisplayLayout() { - return mDisplayLayout; + return mPipDisplayLayoutState.getDisplayLayout(); } @VisibleForTesting @@ -320,20 +316,10 @@ public class PipBoundsState { mPipReentryState = null; } - /** Set the PIP minimum edge size. */ - public void setMinEdgeSize(int minEdgeSize) { - mMinEdgeSize = minEdgeSize; - } - - /** Returns the PIP's current minimum edge size. */ - public int getMinEdgeSize() { - return mMinEdgeSize; - } - /** Sets the preferred size of PIP as specified by the activity in PIP mode. */ public void setOverrideMinSize(@Nullable Size overrideMinSize) { - final boolean changed = !Objects.equals(overrideMinSize, mOverrideMinSize); - mOverrideMinSize = overrideMinSize; + final boolean changed = !Objects.equals(overrideMinSize, getOverrideMinSize()); + mPipSizeSpecHandler.setOverrideMinSize(overrideMinSize); if (changed && mOnMinimalSizeChangeCallback != null) { mOnMinimalSizeChangeCallback.run(); } @@ -342,13 +328,12 @@ public class PipBoundsState { /** Returns the preferred minimal size specified by the activity in PIP. */ @Nullable public Size getOverrideMinSize() { - return mOverrideMinSize; + return mPipSizeSpecHandler.getOverrideMinSize(); } /** Returns the minimum edge size of the override minimum size, or 0 if not set. */ public int getOverrideMinEdgeSize() { - if (mOverrideMinSize == null) return 0; - return Math.min(mOverrideMinSize.getWidth(), mOverrideMinSize.getHeight()); + return mPipSizeSpecHandler.getOverrideMinEdgeSize(); } /** Get the state of the bounds in motion. */ @@ -402,6 +387,16 @@ public class PipBoundsState { mUnrestrictedKeepClearAreas.addAll(unrestrictedAreas); } + /** Add a named unrestricted keep clear area. */ + public void addNamedUnrestrictedKeepClearArea(@NonNull String name, Rect unrestrictedArea) { + mNamedUnrestrictedKeepClearAreas.put(name, unrestrictedArea); + } + + /** Remove a named unrestricted keep clear area. */ + public void removeNamedUnrestrictedKeepClearArea(@NonNull String name) { + mNamedUnrestrictedKeepClearAreas.remove(name); + } + @NonNull public Set<Rect> getRestrictedKeepClearAreas() { return mRestrictedKeepClearAreas; @@ -409,7 +404,10 @@ public class PipBoundsState { @NonNull public Set<Rect> getUnrestrictedKeepClearAreas() { - return mUnrestrictedKeepClearAreas; + if (mNamedUnrestrictedKeepClearAreas.isEmpty()) return mUnrestrictedKeepClearAreas; + final Set<Rect> unrestrictedAreas = new ArraySet<>(mUnrestrictedKeepClearAreas); + unrestrictedAreas.addAll(mNamedUnrestrictedKeepClearAreas.values()); + return unrestrictedAreas; } /** @@ -442,6 +440,16 @@ public class PipBoundsState { mHasUserResizedPip = hasUserResizedPip; } + /** Returns whether the user has moved the PIP. */ + public boolean hasUserMovedPip() { + return mHasUserMovedPip; + } + + /** Set whether the user has moved the PIP. */ + public void setHasUserMovedPip(boolean hasUserMovedPip) { + mHasUserMovedPip = hasUserMovedPip; + } + /** * Registers a callback when the minimal size of PIP that is set by the app changes. */ @@ -475,6 +483,10 @@ public class PipBoundsState { mOnPipExclusionBoundsChangeCallbacks.remove(onPipExclusionBoundsChangeCallback); } + public LauncherState getLauncherState() { + return mLauncherState; + } + /** Source of truth for the current bounds of PIP that may be in motion. */ public static class MotionBoundsState { /** The bounds used when PIP is in motion (e.g. during a drag or animation) */ @@ -527,6 +539,25 @@ public class PipBoundsState { } } + /** Data class for Launcher state. */ + public static final class LauncherState { + private int mAppIconSizePx; + + public void setAppIconSizePx(int appIconSizePx) { + mAppIconSizePx = appIconSizePx; + } + + public int getAppIconSizePx() { + return mAppIconSizePx; + } + + void dump(PrintWriter pw, String prefix) { + final String innerPrefix = prefix + " "; + pw.println(prefix + LauncherState.class.getSimpleName()); + pw.println(innerPrefix + "getAppIconSizePx=" + getAppIconSizePx()); + } + } + static final class PipReentryState { private static final String TAG = PipReentryState.class.getSimpleName(); @@ -567,21 +598,20 @@ public class PipBoundsState { pw.println(innerPrefix + "mExpandedMovementBounds=" + mExpandedMovementBounds); pw.println(innerPrefix + "mLastPipComponentName=" + mLastPipComponentName); pw.println(innerPrefix + "mAspectRatio=" + mAspectRatio); - pw.println(innerPrefix + "mDisplayId=" + mDisplayId); - pw.println(innerPrefix + "mDisplayLayout=" + mDisplayLayout); pw.println(innerPrefix + "mStashedState=" + mStashedState); pw.println(innerPrefix + "mStashOffset=" + mStashOffset); - pw.println(innerPrefix + "mMinEdgeSize=" + mMinEdgeSize); - pw.println(innerPrefix + "mOverrideMinSize=" + mOverrideMinSize); pw.println(innerPrefix + "mIsImeShowing=" + mIsImeShowing); pw.println(innerPrefix + "mImeHeight=" + mImeHeight); pw.println(innerPrefix + "mIsShelfShowing=" + mIsShelfShowing); pw.println(innerPrefix + "mShelfHeight=" + mShelfHeight); + pw.println(innerPrefix + "mHasUserMovedPip=" + mHasUserMovedPip); + pw.println(innerPrefix + "mHasUserResizedPip=" + mHasUserResizedPip); if (mPipReentryState == null) { pw.println(innerPrefix + "mPipReentryState=null"); } else { mPipReentryState.dump(pw, innerPrefix); } + mLauncherState.dump(pw, innerPrefix); mMotionBoundsState.dump(pw, innerPrefix); } } 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..9fa57cacb11f 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 @@ -16,10 +16,18 @@ package com.android.wm.shell.pip; +import static android.util.TypedValue.COMPLEX_UNIT_DIP; + +import android.annotation.Nullable; import android.content.Context; import android.content.res.TypedArray; +import android.graphics.Bitmap; +import android.graphics.Canvas; import android.graphics.Color; +import android.graphics.Matrix; import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.util.TypedValue; import android.view.SurfaceControl; import android.view.SurfaceSession; import android.window.TaskSnapshot; @@ -28,6 +36,9 @@ import android.window.TaskSnapshot; * Represents the content overlay used during the entering PiP animation. */ public abstract class PipContentOverlay { + // Fixed string used in WMShellFlickerTests + protected static final String LAYER_NAME = "PipContentOverlay"; + protected SurfaceControl mLeash; /** Attaches the internal {@link #mLeash} to the given parent leash. */ @@ -41,13 +52,20 @@ public abstract class PipContentOverlay { } } + @Nullable + public SurfaceControl getLeash() { + return mLeash; + } + /** * Animates the internal {@link #mLeash} by a given fraction. * @param atomicTx {@link SurfaceControl.Transaction} to operate, you should not explicitly * call apply on this transaction, it should be applied on the caller side. + * @param currentBounds {@link Rect} of the current animation bounds. * @param fraction progress of the animation ranged from 0f to 1f. */ - public abstract void onAnimationUpdate(SurfaceControl.Transaction atomicTx, float fraction); + public abstract void onAnimationUpdate(SurfaceControl.Transaction atomicTx, + Rect currentBounds, float fraction); /** * Callback when reaches the end of animation on the internal {@link #mLeash}. @@ -60,13 +78,15 @@ public abstract class PipContentOverlay { /** A {@link PipContentOverlay} uses solid color. */ public static final class PipColorOverlay extends PipContentOverlay { + private static final String TAG = PipColorOverlay.class.getSimpleName(); + private final Context mContext; public PipColorOverlay(Context context) { mContext = context; mLeash = new SurfaceControl.Builder(new SurfaceSession()) - .setCallsite("PipAnimation") - .setName(PipColorOverlay.class.getSimpleName()) + .setCallsite(TAG) + .setName(LAYER_NAME) .setColorLayer() .build(); } @@ -82,7 +102,8 @@ public abstract class PipContentOverlay { } @Override - public void onAnimationUpdate(SurfaceControl.Transaction atomicTx, float fraction) { + public void onAnimationUpdate(SurfaceControl.Transaction atomicTx, + Rect currentBounds, float fraction) { atomicTx.setAlpha(mLeash, fraction < 0.5f ? 0 : (fraction - 0.5f) * 2); } @@ -108,59 +129,136 @@ public abstract class PipContentOverlay { /** A {@link PipContentOverlay} uses {@link TaskSnapshot}. */ public static final class PipSnapshotOverlay extends PipContentOverlay { + private static final String TAG = PipSnapshotOverlay.class.getSimpleName(); + 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); mLeash = new SurfaceControl.Builder(new SurfaceSession()) - .setCallsite("PipAnimation") - .setName(PipSnapshotOverlay.class.getSimpleName()) + .setCallsite(TAG) + .setName(LAYER_NAME) .build(); } @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(); } @Override - public void onAnimationUpdate(SurfaceControl.Transaction atomicTx, float fraction) { + public void onAnimationUpdate(SurfaceControl.Transaction atomicTx, + Rect currentBounds, float fraction) { // Do nothing. Keep the snapshot till animation ends. } @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); } } + + /** A {@link PipContentOverlay} shows app icon on solid color background. */ + public static final class PipAppIconOverlay extends PipContentOverlay { + private static final String TAG = PipAppIconOverlay.class.getSimpleName(); + // The maximum size for app icon in pixel. + private static final int MAX_APP_ICON_SIZE_DP = 72; + + private final Context mContext; + private final int mAppIconSizePx; + private final Rect mAppBounds; + private final Matrix mTmpTransform = new Matrix(); + private final float[] mTmpFloat9 = new float[9]; + + private Bitmap mBitmap; + + public PipAppIconOverlay(Context context, Rect appBounds, + Drawable appIcon, int appIconSizePx) { + mContext = context; + final int maxAppIconSizePx = (int) TypedValue.applyDimension(COMPLEX_UNIT_DIP, + MAX_APP_ICON_SIZE_DP, context.getResources().getDisplayMetrics()); + mAppIconSizePx = Math.min(maxAppIconSizePx, appIconSizePx); + mAppBounds = new Rect(appBounds); + mBitmap = Bitmap.createBitmap(appBounds.width(), appBounds.height(), + Bitmap.Config.ARGB_8888); + prepareAppIconOverlay(appIcon); + mLeash = new SurfaceControl.Builder(new SurfaceSession()) + .setCallsite(TAG) + .setName(LAYER_NAME) + .build(); + } + + @Override + public void attach(SurfaceControl.Transaction tx, SurfaceControl parentLeash) { + tx.show(mLeash); + tx.setLayer(mLeash, Integer.MAX_VALUE); + tx.setBuffer(mLeash, mBitmap.getHardwareBuffer()); + tx.reparent(mLeash, parentLeash); + tx.apply(); + } + + @Override + public void onAnimationUpdate(SurfaceControl.Transaction atomicTx, + Rect currentBounds, float fraction) { + mTmpTransform.reset(); + // Scale back the bitmap with the pivot point at center. + mTmpTransform.postScale( + (float) mAppBounds.width() / currentBounds.width(), + (float) mAppBounds.height() / currentBounds.height(), + mAppBounds.centerX(), + mAppBounds.centerY()); + atomicTx.setMatrix(mLeash, mTmpTransform, mTmpFloat9) + .setAlpha(mLeash, fraction < 0.5f ? 0 : (fraction - 0.5f) * 2); + } + + @Override + public void onAnimationEnd(SurfaceControl.Transaction atomicTx, Rect destinationBounds) { + atomicTx.remove(mLeash); + } + + @Override + public void detach(SurfaceControl.Transaction tx) { + super.detach(tx); + if (mBitmap != null && !mBitmap.isRecycled()) { + mBitmap.recycle(); + } + } + + private void prepareAppIconOverlay(Drawable appIcon) { + final Canvas canvas = new Canvas(); + canvas.setBitmap(mBitmap); + final TypedArray ta = mContext.obtainStyledAttributes(new int[] { + android.R.attr.colorBackground }); + try { + int colorAccent = ta.getColor(0, 0); + canvas.drawRGB( + Color.red(colorAccent), + Color.green(colorAccent), + Color.blue(colorAccent)); + } finally { + ta.recycle(); + } + final Rect appIconBounds = new Rect( + mAppBounds.centerX() - mAppIconSizePx / 2, + mAppBounds.centerY() - mAppIconSizePx / 2, + mAppBounds.centerX() + mAppIconSizePx / 2, + mAppBounds.centerY() + mAppIconSizePx / 2); + appIcon.setBounds(appIconBounds); + appIcon.draw(canvas); + mBitmap = mBitmap.copy(Bitmap.Config.HARDWARE, false /* mutable */); + } + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipDisplayLayoutState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipDisplayLayoutState.java new file mode 100644 index 000000000000..0f76af48199f --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipDisplayLayoutState.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.pip; + +import android.content.Context; +import android.graphics.Rect; +import android.view.Surface; + +import androidx.annotation.NonNull; + +import com.android.wm.shell.common.DisplayLayout; +import com.android.wm.shell.dagger.WMSingleton; + +import java.io.PrintWriter; + +import javax.inject.Inject; + +/** + * Acts as a source of truth for display related information for PIP. + */ +@WMSingleton +public class PipDisplayLayoutState { + private static final String TAG = PipDisplayLayoutState.class.getSimpleName(); + + private Context mContext; + private int mDisplayId; + @NonNull private DisplayLayout mDisplayLayout; + + @Inject + public PipDisplayLayoutState(Context context) { + mContext = context; + mDisplayLayout = new DisplayLayout(); + } + + /** Update the display layout. */ + public void setDisplayLayout(@NonNull DisplayLayout displayLayout) { + mDisplayLayout.set(displayLayout); + } + + /** Get a copy of the display layout. */ + @NonNull + public DisplayLayout getDisplayLayout() { + return new DisplayLayout(mDisplayLayout); + } + + /** Get the display bounds */ + @NonNull + public Rect getDisplayBounds() { + return new Rect(0, 0, mDisplayLayout.width(), mDisplayLayout.height()); + } + + /** + * Apply a rotation to this layout and its parameters. + * @param targetRotation + */ + public void rotateTo(@Surface.Rotation int targetRotation) { + mDisplayLayout.rotateTo(mContext.getResources(), targetRotation); + } + + /** Get the current display id */ + public int getDisplayId() { + return mDisplayId; + } + + /** Set the current display id for the associated display layout. */ + public void setDisplayId(int displayId) { + mDisplayId = displayId; + } + + /** Dumps internal state. */ + public void dump(PrintWriter pw, String prefix) { + final String innerPrefix = prefix + " "; + pw.println(prefix + TAG); + pw.println(innerPrefix + "mDisplayId=" + mDisplayId); + pw.println(innerPrefix + "getDisplayBounds=" + getDisplayBounds()); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipKeepClearAlgorithmInterface.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipKeepClearAlgorithmInterface.java new file mode 100644 index 000000000000..5045cf905ee6 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipKeepClearAlgorithmInterface.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 PipKeepClearAlgorithmInterface { + + /** + * 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..cbed4b5a501f 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,25 +109,28 @@ 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 mTmpDestinationRect.offsetTo(0, 0); mTmpDestinationRect.inset(insets); - // Scale by the shortest edge and offset such that the top/left of the scaled inset source - // rect aligns with the top/left of the destination bounds + // Scale to the bounds no smaller than the destination and offset such that the top/left + // of the scaled inset source rect aligns with the top/left of the destination bounds final float scale; 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(); - } else { - scale = sourceBounds.width() <= sourceBounds.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 = Math.max((float) destinationBounds.width() / sourceBounds.width(), + (float) destinationBounds.height() / sourceBounds.height()); } final float left = destinationBounds.left - insets.left * scale; final float top = destinationBounds.top - insets.top * scale; @@ -226,4 +234,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..ec0e770002d6 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 @@ -16,6 +16,7 @@ package com.android.wm.shell.pip; +import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; @@ -62,6 +63,9 @@ import android.content.res.Configuration; import android.graphics.Rect; import android.os.RemoteException; import android.os.SystemClock; +import android.os.SystemProperties; +import android.util.Log; +import android.view.Choreographer; import android.view.Display; import android.view.Surface; import android.view.SurfaceControl; @@ -69,6 +73,7 @@ import android.window.TaskOrganizer; import android.window.TaskSnapshot; import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; +import android.window.WindowContainerTransactionCallback; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.protolog.common.ProtoLog; @@ -123,10 +128,11 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, private final Context mContext; private final SyncTransactionQueue mSyncTransactionQueue; private final PipBoundsState mPipBoundsState; + private final PipDisplayLayoutState mPipDisplayLayoutState; 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; @@ -137,13 +143,32 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, protected final ShellTaskOrganizer mTaskOrganizer; protected final ShellExecutor mMainExecutor; + // the runnable to execute after WindowContainerTransactions is applied to finish resizing pip + private Runnable mPipFinishResizeWCTRunnable; + + private final WindowContainerTransactionCallback mPipFinishResizeWCTCallback = + new WindowContainerTransactionCallback() { + @Override + public void onTransactionReady(int id, SurfaceControl.Transaction t) { + t.apply(); + + // execute the runnable if non-null after WCT is applied to finish resizing pip + if (mPipFinishResizeWCTRunnable != null) { + mPipFinishResizeWCTRunnable.run(); + mPipFinishResizeWCTRunnable = null; + } + } + }; + // These callbacks are called on the update thread private final PipAnimationController.PipAnimationCallback mPipAnimationCallback = new PipAnimationController.PipAnimationCallback() { + private boolean mIsCancelled; @Override public void onPipAnimationStart(TaskInfo taskInfo, PipAnimationController.PipTransitionAnimator animator) { final int direction = animator.getTransitionDirection(); + mIsCancelled = false; sendOnPipTransitionStarted(direction); } @@ -151,6 +176,10 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, public void onPipAnimationEnd(TaskInfo taskInfo, SurfaceControl.Transaction tx, PipAnimationController.PipTransitionAnimator animator) { final int direction = animator.getTransitionDirection(); + if (mIsCancelled) { + sendOnPipTransitionFinished(direction); + return; + } final int animationType = animator.getAnimationType(); final Rect destinationBounds = animator.getDestinationBounds(); if (isInPipDirection(direction) && animator.getContentOverlayLeash() != null) { @@ -178,8 +207,10 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, // This is necessary in case there was a resize animation ongoing when exit PIP // started, in which case the first resize will be skipped to let the exit // operation handle the final resize out of PIP mode. See b/185306679. - finishResize(tx, destinationBounds, direction, animationType); - sendOnPipTransitionFinished(direction); + finishResizeDelayedIfNeeded(() -> { + finishResize(tx, destinationBounds, direction, animationType); + sendOnPipTransitionFinished(direction); + }); } } @@ -187,6 +218,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, public void onPipAnimationCancel(TaskInfo taskInfo, PipAnimationController.PipTransitionAnimator animator) { final int direction = animator.getTransitionDirection(); + mIsCancelled = true; if (isInPipDirection(direction) && animator.getContentOverlayLeash() != null) { fadeOutAndRemoveOverlay(animator.getContentOverlayLeash(), animator::clearContentOverlay, true /* withStartDelay */); @@ -195,12 +227,65 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, } }; + /** + * Finishes resizing the PiP, delaying the operation if it has to be synced with the PiP menu. + * + * This is done to avoid a race condition between the last transaction applied in + * onPipAnimationUpdate and the finishResize in onPipAnimationEnd. The transaction in + * onPipAnimationUpdate is applied directly from WmShell, while onPipAnimationEnd creates a + * WindowContainerTransaction in finishResize, which is to be applied by WmCore later. Normally, + * the WCT should be the last transaction to finish the animation. However, it may happen that + * it gets applied *before* the transaction created by the last onPipAnimationUpdate. This + * happens only when the PiP surface transaction has to be synced with the PiP menu due to the + * necessity for a delay when syncing the PiP surface animation with the PiP menu surface + * animation and redrawing the PiP menu contents. As a result, the PiP surface gets scaled after + * the new bounds are applied by WmCore, which makes the PiP surface have unexpected bounds. + * + * To avoid this, we delay the finishResize operation until + * the next frame. This aligns the last onAnimationUpdate transaction with the WCT application. + */ + private void finishResizeDelayedIfNeeded(Runnable finishResizeRunnable) { + if (!shouldSyncPipTransactionWithMenu()) { + finishResizeRunnable.run(); + return; + } + + // Delay the finishResize to the next frame + Choreographer.getInstance().postCallback(Choreographer.CALLBACK_COMMIT, () -> { + mMainExecutor.execute(finishResizeRunnable); + }, null); + } + + private boolean shouldSyncPipTransactionWithMenu() { + return mPipMenuController.isMenuVisible(); + } + + @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 public boolean handlePipTransaction(SurfaceControl leash, SurfaceControl.Transaction tx, Rect destinationBounds) { - if (mPipMenuController.isMenuVisible()) { + if (shouldSyncPipTransactionWithMenu()) { mPipMenuController.movePipMenu(leash, tx, destinationBounds); return true; } @@ -215,7 +300,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 @@ -255,6 +340,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, @NonNull SyncTransactionQueue syncTransactionQueue, @NonNull PipTransitionState pipTransitionState, @NonNull PipBoundsState pipBoundsState, + @NonNull PipDisplayLayoutState pipDisplayLayoutState, @NonNull PipBoundsAlgorithm boundsHandler, @NonNull PipMenuController pipMenuController, @NonNull PipAnimationController pipAnimationController, @@ -270,6 +356,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, mSyncTransactionQueue = syncTransactionQueue; mPipTransitionState = pipTransitionState; mPipBoundsState = pipBoundsState; + mPipDisplayLayoutState = pipDisplayLayoutState; mPipBoundsAlgorithm = boundsHandler; mPipMenuController = pipMenuController; mPipTransitionController = pipTransitionController; @@ -283,7 +370,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 +383,11 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, mTaskOrganizer.addFocusListener(this); mPipTransitionController.setPipOrganizer(this); displayController.addDisplayWindowListener(this); + pipTransitionController.registerPipTransitionCallback(mPipTransitionCallback); + } + + public PipTransitionController getTransitionController() { + return mPipTransitionController; } public Rect getCurrentOrAnimatingBounds() { @@ -341,6 +434,26 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, } /** + * Override if the PiP should always use a fade-in animation during PiP entry. + * + * @return true if the mOneShotAnimationType should always be + * {@link PipAnimationController#ANIM_TYPE_ALPHA}. + */ + protected boolean shouldAlwaysFadeIn() { + return false; + } + + /** + * Whether the menu should get attached as early as possible when entering PiP. + * + * @return whether the menu should be attached before + * {@link PipBoundsAlgorithm#getEntryDestinationBounds()} is called. + */ + protected boolean shouldAttachMenuEarly() { + return false; + } + + /** * Callback when Launcher starts swipe-pip-to-home operation. * @return {@link Rect} for destination bounds. */ @@ -440,7 +553,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 +571,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. @@ -468,6 +582,15 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, mPipTransitionController.startExitTransition(TRANSIT_EXIT_PIP, wct, destinationBounds); return; } + if (mSplitScreenOptional.isPresent()) { + // If pip activity will reparent to origin task case and if the origin task still under + // split root, just exit split screen here to ensure it could expand to fullscreen. + SplitScreenController split = mSplitScreenOptional.get(); + if (split.isTaskInSplitScreen(mTaskInfo.lastParentTaskIdBeforePip)) { + split.exitSplitScreen(INVALID_TASK_ID, + SplitScreenController.EXIT_REASON_APP_FINISHED); + } + } mSyncTransactionQueue.queue(wct); mSyncTransactionQueue.runInSync(t -> { // Make sure to grab the latest source hint rect as it could have been @@ -538,7 +661,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 */); @@ -580,15 +703,25 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, } mPipUiEventLoggerLogger.setTaskInfo(mTaskInfo); - mPipUiEventLoggerLogger.log(PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_ENTER); // If the displayId of the task is different than what PipBoundsHandler has, then update // it. This is possible if we entered PiP on an external display. - if (info.displayId != mPipBoundsState.getDisplayId() + if (info.displayId != mPipDisplayLayoutState.getDisplayId() && mOnDisplayIdChangeCallback != null) { mOnDisplayIdChangeCallback.accept(info.displayId); } + // UiEvent logging. + final PipUiEventLogger.PipUiEventEnum uiEventEnum; + if (isLaunchIntoPipTask()) { + uiEventEnum = PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_ENTER_CONTENT_PIP; + } else if (mPipTransitionState.getInSwipePipToHomeTransition()) { + uiEventEnum = PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_AUTO_ENTER; + } else { + uiEventEnum = PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_ENTER; + } + mPipUiEventLoggerLogger.log(uiEventEnum); + if (mPipTransitionState.getInSwipePipToHomeTransition()) { if (!mWaitForFixedRotation) { onEndOfSwipePipToHomeTransition(); @@ -614,17 +747,26 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, return; } + if (shouldAlwaysFadeIn()) { + mOneShotAnimationType = ANIM_TYPE_ALPHA; + } + if (mWaitForFixedRotation) { onTaskAppearedWithFixedRotation(); return; } + if (shouldAttachMenuEarly()) { + mPipMenuController.attach(mLeash); + } final Rect destinationBounds = mPipBoundsAlgorithm.getEntryDestinationBounds(); Objects.requireNonNull(destinationBounds, "Missing destination bounds"); final Rect currentBounds = mTaskInfo.configuration.windowConfiguration.getBounds(); if (mOneShotAnimationType == ANIM_TYPE_BOUNDS) { - mPipMenuController.attach(mLeash); + if (!shouldAttachMenuEarly()) { + mPipMenuController.attach(mLeash); + } final Rect sourceHintRect = PipBoundsAlgorithm.getValidSourceHintRect( info.pictureInPictureParams, currentBounds); scheduleAnimateResizePip(currentBounds, destinationBounds, 0 /* startingAngle */, @@ -739,7 +881,9 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, @Nullable SurfaceControl.Transaction boundsChangeTransaction) { // PiP menu is attached late in the process here to avoid any artifacts on the leash // caused by addShellRoot when in gesture navigation mode. - mPipMenuController.attach(mLeash); + if (!shouldAttachMenuEarly()) { + mPipMenuController.attach(mLeash); + } final WindowContainerTransaction wct = new WindowContainerTransaction(); wct.setActivityWindowingMode(mToken, WINDOWING_MODE_UNDEFINED); wct.setBounds(mToken, destinationBounds); @@ -767,11 +911,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 +1064,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 +1083,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); @@ -1079,21 +1225,32 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, final Rect newDestinationBounds = mPipBoundsAlgorithm.getEntryDestinationBounds(); if (newDestinationBounds.equals(currentDestinationBounds)) return; - if (animator.getAnimationType() == ANIM_TYPE_BOUNDS) { - if (mWaitForFixedRotation) { - // The new destination bounds are in next rotation (DisplayLayout has been rotated - // in computeRotatedBounds). The animation runs in previous rotation so the end - // bounds need to be transformed. - final Rect displayBounds = mPipBoundsState.getDisplayBounds(); - final Rect rotatedEndBounds = new Rect(newDestinationBounds); - rotateBounds(rotatedEndBounds, displayBounds, mNextRotation, mCurrentRotation); - animator.updateEndValue(rotatedEndBounds); - } else { - animator.updateEndValue(newDestinationBounds); + updateAnimatorBounds(newDestinationBounds); + destinationBoundsOut.set(newDestinationBounds); + } + + /** + * Directly update the animator bounds. + */ + public void updateAnimatorBounds(Rect bounds) { + final PipAnimationController.PipTransitionAnimator animator = + mPipAnimationController.getCurrentAnimator(); + if (animator != null && animator.isRunning()) { + if (animator.getAnimationType() == ANIM_TYPE_BOUNDS) { + if (mWaitForFixedRotation) { + // The new destination bounds are in next rotation (DisplayLayout has been + // rotated in computeRotatedBounds). The animation runs in previous rotation so + // the end bounds need to be transformed. + final Rect displayBounds = mPipBoundsState.getDisplayBounds(); + final Rect rotatedEndBounds = new Rect(bounds); + rotateBounds(rotatedEndBounds, displayBounds, mNextRotation, mCurrentRotation); + animator.updateEndValue(rotatedEndBounds); + } else { + animator.updateEndValue(bounds); + } } + animator.setDestinationBounds(bounds); } - animator.setDestinationBounds(newDestinationBounds); - destinationBoundsOut.set(newDestinationBounds); } /** @@ -1141,8 +1298,23 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, /** * Animates resizing of the pinned stack given the duration and start bounds. * This is used when the starting bounds is not the current PiP bounds. + * + * @param pipFinishResizeWCTRunnable callback to run after window updates are complete */ public void scheduleAnimateResizePip(Rect fromBounds, Rect toBounds, int duration, + float startingAngle, Consumer<Rect> updateBoundsCallback, + Runnable pipFinishResizeWCTRunnable) { + mPipFinishResizeWCTRunnable = pipFinishResizeWCTRunnable; + if (mPipFinishResizeWCTRunnable != null) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "mPipFinishResizeWCTRunnable is set to be called once window updates"); + } + + scheduleAnimateResizePip(fromBounds, toBounds, duration, startingAngle, + updateBoundsCallback); + } + + private void scheduleAnimateResizePip(Rect fromBounds, Rect toBounds, int duration, float startingAngle, Consumer<Rect> updateBoundsCallback) { if (mWaitForFixedRotation) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, @@ -1193,7 +1365,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, mSurfaceTransactionHelper .crop(tx, mLeash, toBounds) .round(tx, mLeash, mPipTransitionState.isInPip()); - if (mPipMenuController.isMenuVisible()) { + if (shouldSyncPipTransactionWithMenu()) { mPipMenuController.resizePipMenu(mLeash, tx, toBounds); } else { tx.apply(); @@ -1235,7 +1407,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, mSurfaceTransactionHelper .scale(tx, mLeash, startBounds, toBounds, degrees) .round(tx, mLeash, startBounds, toBounds); - if (mPipMenuController.isMenuVisible()) { + if (shouldSyncPipTransactionWithMenu()) { mPipMenuController.movePipMenu(mLeash, tx, toBounds); } else { tx.apply(); @@ -1276,6 +1448,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) { @@ -1331,7 +1509,6 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, @PipAnimationController.TransitionDirection int direction, @PipAnimationController.AnimationType int type) { final Rect preResizeBounds = new Rect(mPipBoundsState.getBounds()); - final boolean isPipTopLeft = isPipTopLeft(); mPipBoundsState.setBounds(destinationBounds); if (direction == TRANSITION_DIRECTION_REMOVE_STACK) { removePipImmediately(); @@ -1377,10 +1554,16 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, null /* callback */, false /* withStartDelay */); }); } else { - applyFinishBoundsResize(wct, direction, isPipTopLeft); + applyFinishBoundsResize(wct, direction, false); } } else { - applyFinishBoundsResize(wct, direction, isPipTopLeft); + applyFinishBoundsResize(wct, direction, isPipToTopLeft()); + // Use sync transaction to apply finish transaction for enter split case. + if (direction == TRANSITION_DIRECTION_LEAVE_PIP_TO_SPLIT_SCREEN) { + mSyncTransactionQueue.runInSync(t -> { + t.merge(tx); + }); + } } finishResizeForMenu(destinationBounds); @@ -1417,7 +1600,10 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, mSurfaceTransactionHelper.round(tx, mLeash, isInPip()); wct.setBounds(mToken, taskBounds); - wct.setBoundsChangeTransaction(mToken, tx); + // Pip to split should use sync transaction to sync split bounds change. + if (direction != TRANSITION_DIRECTION_LEAVE_PIP_TO_SPLIT_SCREEN) { + wct.setBoundsChangeTransaction(mToken, tx); + } } /** @@ -1433,7 +1619,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, mSplitScreenOptional.ifPresent(splitScreenController -> splitScreenController.enterSplitScreen(mTaskInfo.taskId, wasPipTopLeft, wct)); } else { - mTaskOrganizer.applyTransaction(wct); + mTaskOrganizer.applySyncTransaction(wct, mPipFinishResizeWCTCallback); } } @@ -1448,6 +1634,14 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, return topLeft.contains(mPipBoundsState.getBounds()); } + private boolean isPipToTopLeft() { + if (!mSplitScreenOptional.isPresent()) { + return false; + } + return mSplitScreenOptional.get().getActivateSplitPosition(mTaskInfo) + == SPLIT_POSITION_TOP_OR_LEFT; + } + /** * The windowing mode to restore to when resizing out of PIP direction. Defaults to undefined * and can be overridden to restore to an alternate windowing mode. @@ -1467,6 +1661,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; @@ -1491,7 +1690,14 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, // Similar to auto-enter-pip transition, we use content overlay when there is no // source rect hint to enter PiP use bounds animation. if (sourceHintRect == null) { - animator.setColorContentOverlay(mContext); + if (SystemProperties.getBoolean( + "persist.wm.debug.enable_pip_app_icon_overlay", true)) { + animator.setAppIconContentOverlay( + mContext, currentBounds, mTaskInfo.topActivityInfo, + mPipBoundsState.getLauncherState().getAppIconSizePx()); + } else { + animator.setColorContentOverlay(mContext); + } } else { final TaskSnapshot snapshot = PipUtils.getTaskSnapshot( mTaskInfo.launchIntoPipHostTaskId, false /* isLowResolution */); @@ -1513,11 +1719,16 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, return animator; } - /** Computes destination bounds in old rotation and returns source hint rect if available. */ + /** Computes destination bounds in old rotation and returns source hint rect if available. + * + * Note: updates the internal state of {@link PipDisplayLayoutState} by applying a rotation + * transformation onto the display layout. + */ private @Nullable Rect computeRotatedBounds(int rotationDelta, int direction, Rect outDestinationBounds, Rect sourceHintRect) { if (direction == TRANSITION_DIRECTION_TO_PIP) { - mPipBoundsState.getDisplayLayout().rotateTo(mContext.getResources(), mNextRotation); + mPipDisplayLayoutState.rotateTo(mNextRotation); + final Rect displayBounds = mPipBoundsState.getDisplayBounds(); outDestinationBounds.set(mPipBoundsAlgorithm.getEntryDestinationBounds()); // Transform the destination bounds to current display coordinates. @@ -1541,6 +1752,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. * @@ -1554,8 +1779,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, final Rect topLeft = new Rect(); final Rect bottomRight = new Rect(); mSplitScreenOptional.get().getStageBounds(topLeft, bottomRight); - final boolean isPipTopLeft = isPipTopLeft(); - destinationBoundsOut.set(isPipTopLeft ? topLeft : bottomRight); + destinationBoundsOut.set(isPipToTopLeft() ? topLeft : bottomRight); return true; } @@ -1639,6 +1863,11 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, mSurfaceControlTransactionFactory = factory; } + public boolean isLaunchToSplit(TaskInfo taskInfo) { + return mSplitScreenOptional.isPresent() + && mSplitScreenOptional.get().isLaunchToSplit(taskInfo); + } + /** * Dumps internal states. */ 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..4a76a502462c 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; @@ -41,8 +40,8 @@ import static com.android.wm.shell.pip.PipTransitionState.ENTERED_PIP; import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP; import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP_TO_SPLIT; import static com.android.wm.shell.transition.Transitions.TRANSIT_REMOVE_PIP; -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; @@ -50,8 +49,10 @@ import android.graphics.Matrix; import android.graphics.Point; import android.graphics.Rect; import android.os.IBinder; +import android.os.SystemProperties; 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,8 +66,10 @@ 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; +import com.android.wm.shell.util.TransitionUtil; import java.util.Optional; @@ -80,6 +83,7 @@ public class PipTransition extends PipTransitionController { private final Context mContext; private final PipTransitionState mPipTransitionState; + private final PipDisplayLayoutState mPipDisplayLayoutState; private final int mEnterExitAnimationDuration; private final PipSurfaceTransactionHelper mSurfaceTransactionHelper; private final Optional<SplitScreenController> mSplitScreenOptional; @@ -106,19 +110,22 @@ public class PipTransition extends PipTransitionController { private boolean mHasFadeOut; public PipTransition(Context context, + @NonNull ShellInit shellInit, + @NonNull ShellTaskOrganizer shellTaskOrganizer, + @NonNull Transitions transitions, PipBoundsState pipBoundsState, + PipDisplayLayoutState pipDisplayLayoutState, 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; + mPipDisplayLayoutState = pipDisplayLayoutState; mEnterExitAnimationDuration = context.getResources() .getInteger(R.integer.config_pipResizeAnimationDuration); mSurfaceTransactionHelper = pipSurfaceTransactionHelper; @@ -145,6 +152,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 +229,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 +255,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 +271,11 @@ public class PipTransition extends PipTransitionController { @Override public WindowContainerTransaction handleRequest(@NonNull IBinder transition, @NonNull TransitionRequestInfo request) { - if (request.getType() == TRANSIT_PIP) { + if (requestHasPipEnter(request)) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: handle PiP enter request", TAG); 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 +283,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) { @@ -269,7 +313,8 @@ public class PipTransition extends PipTransitionController { // initial state under the new rotation. int rotationDelta = deltaRotation(startRotation, endRotation); if (rotationDelta != Surface.ROTATION_0) { - mPipBoundsState.getDisplayLayout().rotateTo(mContext.getResources(), endRotation); + mPipDisplayLayoutState.rotateTo(endRotation); + final Rect destinationBounds = mPipBoundsAlgorithm.getEntryDestinationBounds(); wct.setBounds(mRequestedEnterTask, destinationBounds); return true; @@ -279,7 +324,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,10 +338,11 @@ 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(), + mPipBoundsState.getBounds(), mPipBoundsState.getBounds(), new Rect(mExitDestinationBounds), Surface.ROTATION_0); } mExitDestinationBounds.setEmpty(); @@ -315,11 +362,29 @@ 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. But if it is running fixed rotation, there will be a seamless + // display transition later. So the last rotation transform needs to be kept to + // avoid flickering, and then the display transition will reset the transform. + if (tx != null && !mInFixedRotation) { + 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 @@ -329,7 +394,7 @@ public class PipTransition extends PipTransitionController { // Launcher may update the Shelf height during the animation, which will update the // destination bounds. Because this is in fixed rotation, We need to make sure the // finishTransaction is using the updated bounds in the display rotation. - final Rect displayBounds = mPipBoundsState.getDisplayBounds(); + final Rect displayBounds = mPipDisplayLayoutState.getDisplayBounds(); final Rect finishBounds = new Rect(destinationBounds); rotateBounds(finishBounds, displayBounds, mEndFixedRotation, displayRotation); mSurfaceTransactionHelper.crop(mFinishTransaction, leash, finishBounds); @@ -397,14 +462,17 @@ public class PipTransition extends PipTransitionController { @NonNull Transitions.TransitionFinishCallback finishCallback, @NonNull TaskInfo taskInfo, @Nullable TransitionInfo.Change pipTaskChange) { TransitionInfo.Change pipChange = pipTaskChange; - if (pipChange == null) { + if (mCurrentPipTaskToken == null) { + ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: There is no existing PiP Task for TRANSIT_EXIT_PIP", TAG); + } else if (pipChange == null) { // The pipTaskChange is null, this can happen if we are reparenting the PIP activity // back to its original Task. In that case, we should animate the activity leash - // instead, which should be the only non-task, independent, TRANSIT_CHANGE window. + // instead, which should be the change whose last parent is the recorded PiP Task. for (int i = info.getChanges().size() - 1; i >= 0; --i) { final TransitionInfo.Change change = info.getChanges().get(i); - if (change.getTaskInfo() == null && change.getMode() == TRANSIT_CHANGE - && TransitionInfo.isIndependent(change, info)) { + if (mCurrentPipTaskToken.equals(change.getLastParent())) { + // Find the activity that is exiting PiP. pipChange = change; break; } @@ -417,6 +485,21 @@ public class PipTransition extends PipTransitionController { taskInfo); return; } + + // When exiting PiP, the PiP leash may be an Activity of a multi-windowing Task, for which + // case it may not be in the screen coordinate. + // Reparent the pip leash to the root with max layer so that we can animate it outside of + // parent crop, and make sure it is not covered by other windows. + final SurfaceControl pipLeash = pipChange.getLeash(); + final int rootIdx = TransitionUtil.rootIndexFor(pipChange, info); + startTransaction.reparent(pipLeash, info.getRoot(rootIdx).getLeash()); + startTransaction.setLayer(pipLeash, Integer.MAX_VALUE); + // Note: because of this, the bounds to animate should be translated to the root coordinate. + final Point offset = info.getRoot(rootIdx).getOffset(); + final Rect currentBounds = mPipBoundsState.getBounds(); + currentBounds.offset(-offset.x, -offset.y); + startTransaction.setPosition(pipLeash, currentBounds.left, currentBounds.top); + mFinishCallback = (wct, wctCB) -> { mPipOrganizer.onExitPipFinished(taskInfo); finishCallback.onTransitionFinished(wct, wctCB); @@ -438,18 +521,17 @@ public class PipTransition extends PipTransitionController { if (displayRotationChange != null) { // Exiting PIP to fullscreen with orientation change. startExpandAndRotationAnimation(info, startTransaction, finishTransaction, - displayRotationChange, taskInfo, pipChange); + displayRotationChange, taskInfo, pipChange, offset); return; } } // Set the initial frame as scaling the end to the start. final Rect destinationBounds = new Rect(pipChange.getEndAbsBounds()); - final Point offset = pipChange.getEndRelOffset(); destinationBounds.offset(-offset.x, -offset.y); - startTransaction.setWindowCrop(pipChange.getLeash(), destinationBounds); - mSurfaceTransactionHelper.scale(startTransaction, pipChange.getLeash(), - destinationBounds, mPipBoundsState.getBounds()); + startTransaction.setWindowCrop(pipLeash, destinationBounds); + mSurfaceTransactionHelper.scale(startTransaction, pipLeash, destinationBounds, + currentBounds); startTransaction.apply(); // Check if it is fixed rotation. @@ -474,19 +556,21 @@ public class PipTransition extends PipTransitionController { y = destinationBounds.bottom; } mSurfaceTransactionHelper.rotateAndScaleWithCrop(finishTransaction, - pipChange.getLeash(), endBounds, endBounds, new Rect(), degree, x, y, + pipLeash, endBounds, endBounds, new Rect(), degree, x, y, true /* isExpanding */, rotationDelta == ROTATION_270 /* clockwise */); } else { rotationDelta = Surface.ROTATION_0; } - startExpandAnimation(taskInfo, pipChange.getLeash(), destinationBounds, rotationDelta); + startExpandAnimation(taskInfo, pipLeash, currentBounds, currentBounds, destinationBounds, + rotationDelta); } private void startExpandAndRotationAnimation(@NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction, @NonNull SurfaceControl.Transaction finishTransaction, @NonNull TransitionInfo.Change displayRotationChange, - @NonNull TaskInfo taskInfo, @NonNull TransitionInfo.Change pipChange) { + @NonNull TaskInfo taskInfo, @NonNull TransitionInfo.Change pipChange, + @NonNull Point offset) { final int rotateDelta = deltaRotation(displayRotationChange.getStartRotation(), displayRotationChange.getEndRotation()); @@ -498,7 +582,6 @@ public class PipTransition extends PipTransitionController { final Rect startBounds = new Rect(pipChange.getStartAbsBounds()); rotateBounds(startBounds, displayRotationChange.getStartAbsBounds(), rotateDelta); final Rect endBounds = new Rect(pipChange.getEndAbsBounds()); - final Point offset = pipChange.getEndRelOffset(); startBounds.offset(-offset.x, -offset.y); endBounds.offset(-offset.x, -offset.y); @@ -534,11 +617,12 @@ public class PipTransition extends PipTransitionController { } private void startExpandAnimation(final TaskInfo taskInfo, final SurfaceControl leash, - final Rect destinationBounds, final int rotationDelta) { + final Rect baseBounds, final Rect startBounds, final Rect endBounds, + final int rotationDelta) { final PipAnimationController.PipTransitionAnimator animator = - mPipAnimationController.getAnimator(taskInfo, leash, mPipBoundsState.getBounds(), - mPipBoundsState.getBounds(), destinationBounds, null, - TRANSITION_DIRECTION_LEAVE_PIP, 0 /* startingAngle */, rotationDelta); + mPipAnimationController.getAnimator(taskInfo, leash, baseBounds, startBounds, + endBounds, null /* sourceHintRect */, TRANSITION_DIRECTION_LEAVE_PIP, + 0 /* startingAngle */, rotationDelta); animator.setTransitionDirection(TRANSITION_DIRECTION_LEAVE_PIP) .setPipAnimationCallback(mPipAnimationCallback) .setDuration(mEnterExitAnimationDuration) @@ -553,100 +637,107 @@ public class PipTransition extends PipTransitionController { @NonNull TaskInfo taskInfo) { startTransaction.apply(); finishTransaction.setWindowCrop(info.getChanges().get(0).getLeash(), - mPipBoundsState.getDisplayBounds()); + mPipDisplayLayoutState.getDisplayBounds()); mPipOrganizer.onExitPipFinished(taskInfo); finishCallback.onTransitionFinished(null, null); } /** 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. + android.util.Slog.e(TAG, "Found new PIP in transition with mis-matched type=" + + transitTypeToString(transitType), new Throwable()); } 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 (isOpeningType(change.getMode())) { + if (change == enterPip) continue; + if (TransitionUtil.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); + + if (mPipOrganizer.shouldAttachMenuEarly()) { + mPipMenuController.attach(leash); + } + final Rect destinationBounds = mPipBoundsAlgorithm.getEntryDestinationBounds(); final Rect currentBounds = taskInfo.configuration.windowConfiguration.getBounds(); int rotationDelta = deltaRotation(startRotation, endRotation); @@ -657,12 +748,14 @@ 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); + if (!mPipOrganizer.shouldAttachMenuEarly()) { + mTransitions.getMainExecutor().executeDelayed( + () -> mPipMenuController.attach(leash), 0); + } if (taskInfo.pictureInPictureParams != null && taskInfo.pictureInPictureParams.isAutoEnterEnabled() @@ -694,7 +787,7 @@ public class PipTransition extends PipTransitionController { null /* callback */, false /* withStartDelay */); } mPipTransitionState.setInSwipePipToHomeTransition(false); - return true; + return; } if (rotationDelta != Surface.ROTATION_0) { @@ -702,6 +795,17 @@ public class PipTransition extends PipTransitionController { tmpTransform.postRotate(rotationDelta); startTransaction.setMatrix(leash, tmpTransform, new float[9]); } + + if (mPipOrganizer.shouldAlwaysFadeIn()) { + mOneShotAnimationType = ANIM_TYPE_ALPHA; + } + + 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, @@ -709,10 +813,23 @@ public class PipTransition extends PipTransitionController { if (sourceHintRect == null) { // We use content overlay when there is no source rect hint to enter PiP use bounds // animation. - animator.setColorContentOverlay(mContext); + // TODO(b/272819817): cleanup the null-check and extra logging. + final boolean hasTopActivityInfo = taskInfo.topActivityInfo != null; + if (!hasTopActivityInfo) { + ProtoLog.w(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, + "%s: TaskInfo.topActivityInfo is null", TAG); + } + if (SystemProperties.getBoolean( + "persist.wm.debug.enable_pip_app_icon_overlay", true) + && hasTopActivityInfo) { + animator.setAppIconContentOverlay( + mContext, currentBounds, taskInfo.topActivityInfo, + mPipBoundsState.getLauncherState().getAppIconSizePx()); + } else { + 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 +837,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,15 +847,14 @@ 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. */ private void computeEnterPipRotatedBounds(int rotationDelta, int startRotation, int endRotation, TaskInfo taskInfo, Rect outDestinationBounds, @Nullable Rect outSourceHintRect) { - mPipBoundsState.getDisplayLayout().rotateTo(mContext.getResources(), endRotation); - final Rect displayBounds = mPipBoundsState.getDisplayBounds(); + mPipDisplayLayoutState.rotateTo(endRotation); + + final Rect displayBounds = mPipDisplayLayoutState.getDisplayBounds(); outDestinationBounds.set(mPipBoundsAlgorithm.getEntryDestinationBounds()); // Transform the destination bounds to current display coordinates. rotateBounds(outDestinationBounds, displayBounds, endRotation, startRotation); @@ -772,7 +887,7 @@ public class PipTransition extends PipTransitionController { continue; } - if (isOpeningType(mode) && change.getParent() == null) { + if (TransitionUtil.isOpeningType(mode) && change.getParent() == null) { final SurfaceControl leash = change.getLeash(); final Rect endBounds = change.getEndAbsBounds(); startTransaction @@ -852,27 +967,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..db6138a0891f 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,17 @@ 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); + } + + /** Returns true if activity is currently entering PiP mode. */ + public boolean isEnteringPip() { + return isEnteringPip(mState); } public void setInSwipePipToHomeTransition(boolean inSwipePipToHomeTransition) { @@ -94,4 +115,33 @@ 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; + } + + /** Returns true if activity is currently entering PiP mode. */ + public static boolean isEnteringPip(@TransitionState int state) { + return state == ENTERING_PIP; + } + + public interface OnPipTransitionStateChangedListener { + void onPipTransitionStateChanged(@TransitionState int oldState, + @TransitionState int newState); + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipUiEventLogger.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipUiEventLogger.java index 513ebba59258..3e5a19b69a59 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipUiEventLogger.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipUiEventLogger.java @@ -78,6 +78,12 @@ public class PipUiEventLogger { @UiEvent(doc = "Activity enters picture-in-picture mode") PICTURE_IN_PICTURE_ENTER(603), + @UiEvent(doc = "Activity enters picture-in-picture mode with auto-enter-pip API") + PICTURE_IN_PICTURE_AUTO_ENTER(1313), + + @UiEvent(doc = "Activity enters picture-in-picture mode from content-pip API") + PICTURE_IN_PICTURE_ENTER_CONTENT_PIP(1314), + @UiEvent(doc = "Expands from picture-in-picture to fullscreen") PICTURE_IN_PICTURE_EXPAND_TO_FULLSCREEN(604), 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..8b98790d3499 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 @@ -18,6 +18,7 @@ package com.android.wm.shell.pip; import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; +import static android.util.TypedValue.COMPLEX_UNIT_DIP; import android.annotation.Nullable; import android.app.ActivityTaskManager; @@ -26,8 +27,10 @@ import android.app.RemoteAction; import android.content.ComponentName; import android.content.Context; import android.os.RemoteException; +import android.util.DisplayMetrics; import android.util.Log; import android.util.Pair; +import android.util.TypedValue; import android.window.TaskSnapshot; import com.android.internal.protolog.common.ProtoLog; @@ -70,6 +73,13 @@ public class PipUtils { } /** + * @return the pixels for a given dp value. + */ + public static int dpToPx(float dpValue, DisplayMetrics dm) { + return (int) TypedValue.applyDimension(COMPLEX_UNIT_DIP, dpValue, dm); + } + + /** * @return true if the aspect ratios differ */ public static boolean aspectRatioChanged(float aspectRatio1, float aspectRatio2) { @@ -83,7 +93,9 @@ public class PipUtils { public static boolean remoteActionsMatch(RemoteAction action1, RemoteAction action2) { if (action1 == action2) return true; if (action1 == null || action2 == null) return false; - return Objects.equals(action1.getTitle(), action2.getTitle()) + return action1.isEnabled() == action2.isEnabled() + && action1.shouldShowIcon() == action2.shouldShowIcon() + && Objects.equals(action1.getTitle(), action2.getTitle()) && Objects.equals(action1.getContentDescription(), action2.getContentDescription()) && Objects.equals(action1.getActionIntent(), action2.getActionIntent()); } @@ -116,7 +128,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..ed8dc7ded654 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipKeepClearAlgorithm.java @@ -0,0 +1,152 @@ +/* + * 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.os.SystemProperties; +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.PipKeepClearAlgorithmInterface; + +import java.util.Set; + +/** + * Calculates the adjusted position that does not occlude keep clear areas. + */ +public class PhonePipKeepClearAlgorithm implements PipKeepClearAlgorithmInterface { + + private boolean mKeepClearAreaGravityEnabled = + SystemProperties.getBoolean( + "persist.wm.debug.enable_pip_keep_clear_algorithm_gravity", false); + + 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(); + Rect insets = new Rect(); + pipBoundsAlgorithm.getInsetBounds(insets); + if (pipBoundsState.isImeShowing()) { + insets.bottom -= pipBoundsState.getImeHeight(); + } + Rect pipBounds = new Rect(startingBounds); + + // move PiP towards corner if user hasn't moved it manually or the flag is on + if (mKeepClearAreaGravityEnabled + || (!pipBoundsState.hasUserMovedPip() && !pipBoundsState.hasUserResizedPip())) { + 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; + } + if (verticalGravity == Gravity.BOTTOM) { + pipBounds.offsetTo(pipBounds.left, + insets.bottom - pipBounds.height()); + } + if (horizontalGravity == Gravity.RIGHT) { + pipBounds.offsetTo(insets.right - pipBounds.width(), pipBounds.top); + } else { + pipBounds.offsetTo(insets.left, pipBounds.top); + } + } + + return findUnoccludedPosition(pipBounds, 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..94e593b106a5 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,18 @@ 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(); + if (surfaceControl == null) { + return; + } + 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 +357,32 @@ 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(); + if (surfaceControl == null) { + return; + } + 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/PipAccessibilityInteractionConnection.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipAccessibilityInteractionConnection.java index 7365b9525919..4a06d84ce90d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipAccessibilityInteractionConnection.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipAccessibilityInteractionConnection.java @@ -24,11 +24,13 @@ import android.graphics.Region; import android.os.Bundle; import android.os.RemoteException; import android.view.MagnificationSpec; +import android.view.SurfaceControl; import android.view.accessibility.AccessibilityManager; import android.view.accessibility.AccessibilityNodeInfo; import android.view.accessibility.AccessibilityWindowInfo; import android.view.accessibility.IAccessibilityInteractionConnection; import android.view.accessibility.IAccessibilityInteractionConnectionCallback; +import android.window.ScreenCapture; import androidx.annotation.BinderThread; @@ -362,6 +364,15 @@ public class PipAccessibilityInteractionConnection { } @Override + public void takeScreenshotOfWindow(int interactionId, + ScreenCapture.ScreenCaptureListener listener, + IAccessibilityInteractionConnectionCallback callback) throws RemoteException { + // AbstractAccessibilityServiceConnection uses the standard + // IAccessibilityInteractionConnection for takeScreenshotOfWindow for Pip windows, + // so do nothing here. + } + + @Override public void clearAccessibilityFocus() throws RemoteException { // Do nothing } @@ -370,5 +381,8 @@ public class PipAccessibilityInteractionConnection { public void notifyOutsideTouch() throws RemoteException { // Do nothing } - } + + @Override + public void attachAccessibilityOverlayToWindow(SurfaceControl sc) {} } +}
\ No newline at end of file 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..463ad77d828f 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 @@ -23,6 +23,7 @@ import static android.view.WindowManager.INPUT_CONSUMER_PIP; import static com.android.internal.jank.InteractionJankMonitor.CUJ_PIP_TRANSITION; import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission; +import static com.android.wm.shell.pip.PipAnimationController.ANIM_TYPE_ALPHA; import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_EXPAND_OR_UNEXPAND; import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_LEAVE_PIP; import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_LEAVE_PIP_TO_SPLIT_SCREEN; @@ -32,6 +33,7 @@ import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTI import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_TO_PIP; import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_USER_RESIZE; import static com.android.wm.shell.pip.PipAnimationController.isOutPipDirection; +import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_PIP; import android.app.ActivityManager; import android.app.ActivityTaskManager; @@ -41,13 +43,14 @@ import android.content.ComponentName; import android.content.Context; import android.content.pm.ActivityInfo; import android.content.res.Configuration; +import android.graphics.Point; import android.graphics.Rect; import android.os.RemoteException; -import android.os.UserHandle; -import android.os.UserManager; +import android.os.SystemProperties; 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,10 +66,13 @@ 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.ExternalInterfaceBinder; import com.android.wm.shell.common.RemoteCallable; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SingleInstanceRemoteListener; +import com.android.wm.shell.common.TabletopModeController; import com.android.wm.shell.common.TaskStackListenerCallback; import com.android.wm.shell.common.TaskStackListenerImpl; import com.android.wm.shell.onehanded.OneHandedController; @@ -79,13 +85,22 @@ 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.PipDisplayLayoutState; +import com.android.wm.shell.pip.PipKeepClearAlgorithmInterface; 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,39 +114,121 @@ 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 String LAUNCHER_KEEP_CLEAR_AREA_TAG = "hotseat"; + + 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", true); + + @VisibleForTesting + void setEnablePipKeepClearAlgorithm(boolean value) { + mEnablePipKeepClearAlgorithm = value; + } + private Context mContext; protected ShellExecutor mMainExecutor; private DisplayController mDisplayController; private PipInputConsumer mPipInputConsumer; private WindowManagerShellWrapper mWindowManagerShellWrapper; + private PipAnimationController mPipAnimationController; private PipAppOpsListener mAppOpsListener; private PipMediaController mMediaController; private PipBoundsAlgorithm mPipBoundsAlgorithm; + private PipKeepClearAlgorithmInterface mPipKeepClearAlgorithm; private PipBoundsState mPipBoundsState; + private PipSizeSpecHandler mPipSizeSpecHandler; + private PipDisplayLayoutState mPipDisplayLayoutState; + private PipMotionHelper mPipMotionHelper; private PipTouchHandler mTouchHandler; private PipTransitionController mPipTransitionController; private TaskStackListenerImpl mTaskStackListener; private PipParamsChangedForwarder mPipParamsChangedForwarder; + private DisplayInsetsController mDisplayInsetsController; + private TabletopModeController mTabletopModeController; 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 (mIsKeyguardShowingOrAnimating) { + // early bail out if the change was caused by keyguard showing up + return; + } + if (!mEnablePipKeepClearAlgorithm) { + // early bail out if the keep clear areas feature is disabled + return; + } + if (mPipBoundsState.isStashed()) { + // don't move when stashed + return; + } + // if there is another animation ongoing, wait for it to finish and try again + if (mPipAnimationController.isAnimating()) { + mMainExecutor.removeCallbacks( + mMovePipInResponseToKeepClearAreasChangeCallback); + mMainExecutor.executeDelayed( + mMovePipInResponseToKeepClearAreasChangeCallback, + PIP_KEEP_CLEAR_AREAS_DELAY); + return; + } + updatePipPositionForKeepClearAreas(); + } + + private void updatePipPositionForKeepClearAreas() { + if (!mEnablePipKeepClearAlgorithm) { + // early bail out if the keep clear areas feature is disabled + return; + } + if (mIsKeyguardShowingOrAnimating) { + // early bail out if the change was caused by keyguard showing up + return; + } + // only move if we're in PiP or transitioning into PiP + if (!mPipTransitionState.shouldBlockResizeRequest()) { + Rect destBounds = mPipKeepClearAlgorithm.adjust(mPipBoundsState, + mPipBoundsAlgorithm); + // only move if the bounds are actually different + if (!destBounds.equals(mPipBoundsState.getBounds())) { + if (mPipTransitionState.hasEnteredPip()) { + // if already in PiP, schedule separate animation + mPipTaskOrganizer.scheduleAnimateResizePip(destBounds, + mEnterAnimationDuration, null); + } else if (mPipTransitionState.isEnteringPip()) { + // while entering PiP we just need to update animator bounds + mPipTaskOrganizer.updateAnimatorBounds(destBounds); + } + } + } + } + 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 interface PipAnimationListener { + private Consumer<Boolean> mOnIsInPipStateChangedListener; + + @VisibleForTesting + interface PipAnimationListener { /** * Notifies the listener that the Pip animation is started. */ @@ -156,7 +253,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; } @@ -227,7 +324,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb @Override public void onDisplayAdded(int displayId) { - if (displayId != mPipBoundsState.getDisplayId()) { + if (displayId != mPipDisplayLayoutState.getDisplayId()) { return; } onDisplayChanged(mDisplayController.getDisplayLayout(displayId), @@ -236,7 +333,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb @Override public void onDisplayConfigurationChanged(int displayId, Configuration newConfig) { - if (displayId != mPipBoundsState.getDisplayId()) { + if (displayId != mPipDisplayLayoutState.getDisplayId()) { return; } onDisplayChanged(mDisplayController.getDisplayLayout(displayId), @@ -246,8 +343,16 @@ public class PipController implements PipTransitionController.PipTransitionCallb @Override public void onKeepClearAreasChanged(int displayId, Set<Rect> restricted, Set<Rect> unrestricted) { - if (mPipBoundsState.getDisplayId() == displayId) { - mPipBoundsState.setKeepClearAreas(restricted, unrestricted); + if (mPipDisplayLayoutState.getDisplayId() == displayId) { + if (mEnablePipKeepClearAlgorithm) { + mPipBoundsState.setKeepClearAreas(restricted, unrestricted); + + mMainExecutor.removeCallbacks( + mMovePipInResponseToKeepClearAreasChangeCallback); + mMainExecutor.executeDelayed( + mMovePipInResponseToKeepClearAreasChangeCallback, + PIP_KEEP_CLEAR_AREAS_DELAY); + } } } }; @@ -261,6 +366,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 +392,30 @@ public class PipController implements PipTransitionController.PipTransitionCallb * Instantiates {@link PipController}, returns {@code null} if the feature not supported. */ @Nullable - public static Pip create(Context context, DisplayController displayController, - PipAppOpsListener pipAppOpsListener, PipBoundsAlgorithm pipBoundsAlgorithm, - PipBoundsState pipBoundsState, PipMediaController pipMediaController, - PhonePipMenuController phonePipMenuController, PipTaskOrganizer pipTaskOrganizer, - PipTouchHandler pipTouchHandler, PipTransitionController pipTransitionController, + public static Pip create(Context context, + ShellInit shellInit, + ShellCommandHandler shellCommandHandler, + ShellController shellController, + DisplayController displayController, + PipAnimationController pipAnimationController, + PipAppOpsListener pipAppOpsListener, + PipBoundsAlgorithm pipBoundsAlgorithm, + PipKeepClearAlgorithmInterface pipKeepClearAlgorithm, + PipBoundsState pipBoundsState, + PipSizeSpecHandler pipSizeSpecHandler, + PipDisplayLayoutState pipDisplayLayoutState, + PipMotionHelper pipMotionHelper, + PipMediaController pipMediaController, + PhonePipMenuController phonePipMenuController, + PipTaskOrganizer pipTaskOrganizer, + PipTransitionState pipTransitionState, + PipTouchHandler pipTouchHandler, + PipTransitionController pipTransitionController, WindowManagerShellWrapper windowManagerShellWrapper, TaskStackListenerImpl taskStackListener, PipParamsChangedForwarder pipParamsChangedForwarder, + DisplayInsetsController displayInsetsController, + TabletopModeController pipTabletopController, Optional<OneHandedController> oneHandedController, ShellExecutor mainExecutor) { if (!context.getPackageManager().hasSystemFeature(FEATURE_PICTURE_IN_PICTURE)) { @@ -300,47 +424,64 @@ 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, pipAnimationController, pipAppOpsListener, + pipBoundsAlgorithm, pipKeepClearAlgorithm, pipBoundsState, pipSizeSpecHandler, + pipDisplayLayoutState, pipMotionHelper, pipMediaController, phonePipMenuController, + pipTaskOrganizer, pipTransitionState, pipTouchHandler, pipTransitionController, windowManagerShellWrapper, taskStackListener, pipParamsChangedForwarder, - oneHandedController, mainExecutor) + displayInsetsController, pipTabletopController, oneHandedController, mainExecutor) .mImpl; } protected PipController(Context context, + ShellInit shellInit, + ShellCommandHandler shellCommandHandler, + ShellController shellController, DisplayController displayController, + PipAnimationController pipAnimationController, PipAppOpsListener pipAppOpsListener, PipBoundsAlgorithm pipBoundsAlgorithm, + PipKeepClearAlgorithmInterface pipKeepClearAlgorithm, @NonNull PipBoundsState pipBoundsState, + PipSizeSpecHandler pipSizeSpecHandler, + @NonNull PipDisplayLayoutState pipDisplayLayoutState, + PipMotionHelper pipMotionHelper, PipMediaController pipMediaController, PhonePipMenuController phonePipMenuController, PipTaskOrganizer pipTaskOrganizer, + PipTransitionState pipTransitionState, PipTouchHandler pipTouchHandler, PipTransitionController pipTransitionController, WindowManagerShellWrapper windowManagerShellWrapper, TaskStackListenerImpl taskStackListener, PipParamsChangedForwarder pipParamsChangedForwarder, + DisplayInsetsController displayInsetsController, + TabletopModeController tabletopModeController, Optional<OneHandedController> oneHandedController, ShellExecutor mainExecutor ) { - // Ensure that we are the primary user's SystemUI. - final int processUser = UserManager.get(context).getProcessUserId(); - if (processUser != UserHandle.USER_SYSTEM) { - throw new IllegalStateException("Non-primary Pip component not currently supported."); - } + mContext = context; + mShellCommandHandler = shellCommandHandler; + mShellController = shellController; mImpl = new PipImpl(); mWindowManagerShellWrapper = windowManagerShellWrapper; mDisplayController = displayController; mPipBoundsAlgorithm = pipBoundsAlgorithm; + mPipKeepClearAlgorithm = pipKeepClearAlgorithm; mPipBoundsState = pipBoundsState; + mPipSizeSpecHandler = pipSizeSpecHandler; + mPipDisplayLayoutState = pipDisplayLayoutState; + mPipMotionHelper = pipMotionHelper; mPipTaskOrganizer = pipTaskOrganizer; + mPipTransitionState = pipTransitionState; mMainExecutor = mainExecutor; mMediaController = pipMediaController; mMenuController = phonePipMenuController; mTouchHandler = pipTouchHandler; + mPipAnimationController = pipAnimationController; mAppOpsListener = pipAppOpsListener; mOneHandedController = oneHandedController; mPipTransitionController = pipTransitionController; @@ -349,20 +490,31 @@ public class PipController implements PipTransitionController.PipTransitionCallb mEnterAnimationDuration = mContext.getResources() .getInteger(R.integer.config_pipEnterAnimationDuration); mPipParamsChangedForwarder = pipParamsChangedForwarder; + mDisplayInsetsController = displayInsetsController; + mTabletopModeController = tabletopModeController; - //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); mPipTaskOrganizer.registerOnDisplayIdChangeCallback((int displayId) -> { - mPipBoundsState.setDisplayId(displayId); + mPipDisplayLayoutState.setDisplayId(displayId); 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. @@ -390,8 +542,10 @@ public class PipController implements PipTransitionController.PipTransitionCallb // Ensure that we have the display info in case we get calls to update the bounds before the // listener calls back - mPipBoundsState.setDisplayId(mContext.getDisplayId()); - mPipBoundsState.setDisplayLayout(new DisplayLayout(mContext, mContext.getDisplay())); + mPipDisplayLayoutState.setDisplayId(mContext.getDisplayId()); + + DisplayLayout layout = new DisplayLayout(mContext, mContext.getDisplay()); + mPipDisplayLayoutState.setDisplayLayout(layout); try { mWindowManagerShellWrapper.addPinnedStackListener(mPinnedTaskListener); @@ -442,8 +596,12 @@ public class PipController implements PipTransitionController.PipTransitionCallb if (task.getWindowingMode() != WINDOWING_MODE_PINNED) { return; } - mTouchHandler.getMotionHelper().expandLeavePip( - clearedTask /* skipAnimation */); + if (mPipTaskOrganizer.isLaunchToSplit(task)) { + mTouchHandler.getMotionHelper().expandIntoSplit(); + } else { + mTouchHandler.getMotionHelper().expandLeavePip( + clearedTask /* skipAnimation */); + } } }); @@ -458,14 +616,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 +640,80 @@ public class PipController implements PipTransitionController.PipTransitionCallb } }); + mDisplayInsetsController.addInsetsChangedListener(mPipDisplayLayoutState.getDisplayId(), + new DisplayInsetsController.OnInsetsChangedListener() { + @Override + public void insetsChanged(InsetsState insetsState) { + DisplayLayout pendingLayout = mDisplayController + .getDisplayLayout(mPipDisplayLayoutState.getDisplayId()); + if (mIsInFixedRotation + || mIsKeyguardShowingOrAnimating + || pendingLayout.rotation() + != mPipBoundsState.getDisplayLayout().rotation()) { + // bail out if there is a pending rotation or fixed rotation change or + // there's a keyguard present + return; + } + int oldMaxMovementBound = mPipBoundsState.getMovementBounds().bottom; + onDisplayChangedUncheck(mDisplayController + .getDisplayLayout(mPipDisplayLayoutState.getDisplayId()), + false /* saveRestoreSnapFraction */); + int newMaxMovementBound = mPipBoundsState.getMovementBounds().bottom; + if (!mEnablePipKeepClearAlgorithm) { + // offset PiP to adjust for bottom inset change + int pipTop = mPipBoundsState.getBounds().top; + int diff = newMaxMovementBound - oldMaxMovementBound; + if (diff < 0 && pipTop > newMaxMovementBound) { + // bottom inset has increased, move PiP up if it is too low + mPipMotionHelper.animateToOffset(mPipBoundsState.getBounds(), + newMaxMovementBound - pipTop); + } + if (diff > 0 && oldMaxMovementBound == pipTop) { + // bottom inset has decreased, move PiP down if it was by the edge + mPipMotionHelper.animateToOffset(mPipBoundsState.getBounds(), diff); + } + } + } + }); + + mTabletopModeController.registerOnTabletopModeChangedListener((isInTabletopMode) -> { + if (!mTabletopModeController.enableMoveFloatingWindowInTabletop()) return; + final String tag = "tabletop-mode"; + if (!isInTabletopMode) { + mPipBoundsState.removeNamedUnrestrictedKeepClearArea(tag); + return; + } + + // To prepare for the entry bounds. + final Rect displayBounds = mPipBoundsState.getDisplayBounds(); + if (mTabletopModeController.getPreferredHalfInTabletopMode() + == TabletopModeController.PREFERRED_TABLETOP_HALF_TOP) { + // Prefer top, avoid the bottom half of the display. + mPipBoundsState.addNamedUnrestrictedKeepClearArea(tag, new Rect( + displayBounds.left, displayBounds.centerY(), + displayBounds.right, displayBounds.bottom)); + } else { + // Prefer bottom, avoid the top half of the display. + mPipBoundsState.addNamedUnrestrictedKeepClearArea(tag, new Rect( + displayBounds.left, displayBounds.top, + displayBounds.right, displayBounds.centerY())); + } + + // Try to move the PiP window if we have entered PiP mode. + if (mPipTransitionState.hasEnteredPip()) { + final Rect pipBounds = mPipBoundsState.getBounds(); + final Point edgeInsets = mPipSizeSpecHandler.getScreenEdgeInsets(); + if ((pipBounds.height() + 2 * edgeInsets.y) > (displayBounds.height() / 2)) { + // PiP bounds is too big to fit either half, bail early. + return; + } + mMainExecutor.removeCallbacks(mMovePipInResponseToKeepClearAreasChangeCallback); + mMainExecutor.execute(mMovePipInResponseToKeepClearAreasChangeCallback); + } + }); + mOneHandedController.ifPresent(controller -> { - controller.asOneHanded().registerTransitionCallback( + controller.registerTransitionCallback( new OneHandedTransitionCallback() { @Override public void onStartFinished(Rect bounds) { @@ -489,6 +726,18 @@ public class PipController implements PipTransitionController.PipTransitionCallb } }); }); + + mMediaController.registerSessionListenerForCurrentUser(); + + mShellController.addConfigurationChangeListener(this); + mShellController.addKeyguardChangeListener(this); + mShellController.addUserChangeListener(this); + mShellController.addExternalInterface(KEY_EXTRA_SHELL_PIP, + this::createExternalInterface, this); + } + + private ExternalInterfaceBinder createExternalInterface() { + return new IPipImpl(this); } @Override @@ -501,31 +750,53 @@ 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(); + mPipSizeSpecHandler.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 */); } private void onDisplayChanged(DisplayLayout layout, boolean saveRestoreSnapFraction) { - if (mPipBoundsState.getDisplayLayout().isSameGeometry(layout)) { - return; + if (!mPipDisplayLayoutState.getDisplayLayout().isSameGeometry(layout)) { + PipAnimationController.PipTransitionAnimator animator = + mPipAnimationController.getCurrentAnimator(); + if (animator != null && animator.isRunning()) { + // cancel any running animator, as it is using stale display layout information + animator.cancel(); + } + onDisplayChangedUncheck(layout, saveRestoreSnapFraction); } + } + + private void onDisplayChangedUncheck(DisplayLayout layout, boolean saveRestoreSnapFraction) { Runnable updateDisplayLayout = () -> { final boolean fromRotation = Transitions.ENABLE_SHELL_TRANSITIONS - && mPipBoundsState.getDisplayLayout().rotation() != layout.rotation(); - mPipBoundsState.setDisplayLayout(layout); + && mPipDisplayLayoutState.getDisplayLayout().rotation() != layout.rotation(); + + // update the internal state of objects subscribed to display changes + mPipDisplayLayoutState.setDisplayLayout(layout); + final WindowContainerTransaction wct = fromRotation ? new WindowContainerTransaction() : null; updateMovementBounds(null /* toBounds */, @@ -542,33 +813,61 @@ 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 = + mPipDisplayLayoutState.getDisplayLayout().densityDpi() != 0 + && (mPipDisplayLayoutState.getDisplayLayout().densityDpi() + != layout.densityDpi()); + if (densityDpiChanged) { + final float scale = (float) layout.densityDpi() + / mPipDisplayLayoutState.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()); + mPipDisplayLayoutState.getDisplayBounds(), + mPipDisplayLayoutState.getDisplayLayout().stableInsets()); + + 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); + } + + // if the pip window size is beyond allowed bounds user resize to normal bounds + if (mPipBoundsState.getBounds().width() < mPipBoundsState.getMinSize().x + || mPipBoundsState.getBounds().width() > mPipBoundsState.getMaxSize().x + || mPipBoundsState.getBounds().height() < mPipBoundsState.getMinSize().y + || mPipBoundsState.getBounds().height() > mPipBoundsState.getMaxSize().y) { + mTouchHandler.userResizeTo(mPipBoundsState.getNormalBounds(), snapFraction); + } - mTouchHandler.getMotionHelper().movePip(postChangeStackBounds); } else { updateDisplayLayout.run(); } } - private void registerSessionListenerForCurrentUser() { - mMediaController.registerSessionListenerForCurrentUser(); - } - private void onSystemUiStateChanged(boolean isValidState, int flag) { mTouchHandler.onSystemUiStateChanged(isValidState); } @@ -600,21 +899,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) { - if (!mPipTaskOrganizer.isInPip()) { + @Override + public void onKeyguardVisibilityChanged(boolean visible, boolean occluded, + boolean animatingDismiss) { + if (!mPipTransitionState.hasEnteredPip()) { 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 +939,30 @@ public class PipController implements PipTransitionController.PipTransitionCallb } } + private void setLauncherKeepClearAreaHeight(boolean visible, int height) { + if (visible) { + Rect rect = new Rect( + 0, mPipBoundsState.getDisplayBounds().bottom - height, + mPipBoundsState.getDisplayBounds().right, + mPipBoundsState.getDisplayBounds().bottom); + mPipBoundsState.addNamedUnrestrictedKeepClearArea(LAUNCHER_KEEP_CLEAR_AREA_TAG, rect); + } else { + mPipBoundsState.removeNamedUnrestrictedKeepClearArea(LAUNCHER_KEEP_CLEAR_AREA_TAG); + } + updatePipPositionForKeepClearAreas(); + } + + private void setLauncherAppIconSize(int iconSizePx) { + mPipBoundsState.getLauncherState().setAppIconSizePx(iconSizePx); + } + + 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); @@ -648,11 +974,17 @@ public class PipController implements PipTransitionController.PipTransitionCallb animationType == PipAnimationController.ANIM_TYPE_BOUNDS); } - private void setPinnedStackAnimationListener(PipAnimationListener callback) { + @VisibleForTesting + void setPinnedStackAnimationListener(PipAnimationListener callback) { mPinnedStackAnimationRecentsCallback = callback; onPipResourceDimensionsChanged(); } + @VisibleForTesting + boolean hasPinnedStackAnimationListener() { + return mPinnedStackAnimationRecentsCallback != null; + } + private void onPipResourceDimensionsChanged() { if (mPinnedStackAnimationRecentsCallback != null) { mPinnedStackAnimationRecentsCallback.onPipResourceDimensionsChanged( @@ -663,8 +995,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); @@ -760,7 +1100,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb // Populate inset / normal bounds and DisplayInfo from mPipBoundsHandler before // passing to mTouchHandler/mPipTaskOrganizer final Rect outBounds = new Rect(toBounds); - final int rotation = mPipBoundsState.getDisplayLayout().rotation(); + final int rotation = mPipDisplayLayoutState.getDisplayLayout().rotation(); mPipBoundsAlgorithm.getInsetBounds(mTmpInsetBounds); mPipBoundsState.setNormalBounds(mPipBoundsAlgorithm.getNormalBounds()); @@ -784,7 +1124,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb private void onDisplayRotationChangedNotInPip(Context context, int toRotation) { // Update the display layout, note that we have to do this on every rotation even if we // aren't in PIP since we need to update the display layout to get the right resources - mPipBoundsState.getDisplayLayout().rotateTo(context.getResources(), toRotation); + mPipDisplayLayoutState.rotateTo(toRotation); } /** @@ -797,7 +1137,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb Rect outInsetBounds, int displayId, int fromRotation, int toRotation, WindowContainerTransaction t) { // Bail early if the event is not sent to current display - if ((displayId != mPipBoundsState.getDisplayId()) || (fromRotation == toRotation)) { + if ((displayId != mPipDisplayLayoutState.getDisplayId()) || (fromRotation == toRotation)) { return false; } @@ -821,7 +1161,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb mPipBoundsState.getStashedState()); // Update the display layout - mPipBoundsState.getDisplayLayout().rotateTo(context.getResources(), toRotation); + mPipDisplayLayoutState.rotateTo(toRotation); // Calculate the stack bounds in the new orientation based on same fraction along the // rotated movement bounds. @@ -829,8 +1169,8 @@ public class PipController implements PipTransitionController.PipTransitionCallb postChangeStackBounds, false /* adjustForIme */); pipSnapAlgorithm.applySnapFraction(postChangeStackBounds, postChangeMovementBounds, snapFraction, mPipBoundsState.getStashedState(), mPipBoundsState.getStashOffset(), - mPipBoundsState.getDisplayBounds(), - mPipBoundsState.getDisplayLayout().stableInsets()); + mPipDisplayLayoutState.getDisplayBounds(), + mPipDisplayLayoutState.getDisplayLayout().stableInsets()); mPipBoundsAlgorithm.getInsetBounds(outInsetBounds); outBounds.set(postChangeStackBounds); @@ -838,7 +1178,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); @@ -847,23 +1187,14 @@ public class PipController implements PipTransitionController.PipTransitionCallb mPipTaskOrganizer.dump(pw, innerPrefix); mPipBoundsState.dump(pw, innerPrefix); mPipInputConsumer.dump(pw, innerPrefix); + mPipSizeSpecHandler.dump(pw, innerPrefix); + mPipDisplayLayoutState.dump(pw, innerPrefix); } /** * The interface for calls from outside the Shell, within the host process. */ private class PipImpl implements Pip { - private IPipImpl mIPip; - - @Override - public IPip createExternalInterface() { - if (mIPip != null) { - mIPip.invalidate(); - } - mIPip = new IPipImpl(PipController.this); - return mIPip; - } - @Override public void expandPip() { mMainExecutor.execute(() -> { @@ -872,27 +1203,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,23 +1210,9 @@ public class PipController implements PipTransitionController.PipTransitionCallb } @Override - public void registerSessionListenerForCurrentUser() { + public void setOnIsInPipStateChangedListener(Consumer<Boolean> callback) { mMainExecutor.execute(() -> { - PipController.this.registerSessionListenerForCurrentUser(); - }); - } - - @Override - public void setShelfHeight(boolean visible, int height) { - mMainExecutor.execute(() -> { - PipController.this.setShelfHeight(visible, height); - }); - } - - @Override - public void setPinnedStackAnimationType(int animationType) { - mMainExecutor.execute(() -> { - PipController.this.setPinnedStackAnimationType(animationType); + PipController.this.setOnIsInPipStateChangedListener(callback); }); } @@ -940,37 +1236,13 @@ 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); - } - } } /** * The interface for calls from outside the host process. */ @BinderThread - private static class IPipImpl extends IPip.Stub { + private static class IPipImpl extends IPip.Stub implements ExternalInterfaceBinder { private PipController mController; private final SingleInstanceRemoteListener<PipController, IPipAnimationListener> mListener; @@ -1001,19 +1273,22 @@ public class PipController implements PipTransitionController.PipTransitionCallb /** * Invalidates this instance, preventing future calls from updating the controller. */ - void invalidate() { + @Override + public void invalidate() { mController = null; + // Unregister the listener to ensure any registered binder death recipients are unlinked + mListener.unregister(); } @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]; } @@ -1021,24 +1296,35 @@ public class PipController implements PipTransitionController.PipTransitionCallb @Override public void stopSwipePipToHome(int taskId, ComponentName componentName, Rect destinationBounds, SurfaceControl overlay) { + if (overlay != null) { + overlay.setUnreleasedWarningCallSite("PipController.stopSwipePipToHome"); + } executeRemoteCallWithTaskPermission(mController, "stopSwipePipToHome", - (controller) -> { - controller.stopSwipePipToHome(taskId, componentName, destinationBounds, - overlay); - }); + (controller) -> controller.stopSwipePipToHome( + taskId, componentName, destinationBounds, overlay)); } @Override public void setShelfHeight(boolean visible, int height) { executeRemoteCallWithTaskPermission(mController, "setShelfHeight", - (controller) -> { - controller.setShelfHeight(visible, height); - }); + (controller) -> controller.setShelfHeight(visible, height)); + } + + @Override + public void setLauncherKeepClearAreaHeight(boolean visible, int height) { + executeRemoteCallWithTaskPermission(mController, "setLauncherKeepClearAreaHeight", + (controller) -> controller.setLauncherKeepClearAreaHeight(visible, height)); + } + + @Override + public void setLauncherAppIconSize(int iconSizePx) { + executeRemoteCallWithTaskPermission(mController, "setLauncherAppIconSize", + (controller) -> controller.setLauncherAppIconSize(iconSizePx)); } @Override - public void setPinnedStackAnimationListener(IPipAnimationListener listener) { - executeRemoteCallWithTaskPermission(mController, "setPinnedStackAnimationListener", + public void setPipAnimationListener(IPipAnimationListener listener) { + executeRemoteCallWithTaskPermission(mController, "setPipAnimationListener", (controller) -> { if (listener != null) { mListener.register(listener); @@ -1047,5 +1333,11 @@ public class PipController implements PipTransitionController.PipTransitionCallb } }); } + + @Override + public void setPipAnimationTypeToAlpha() { + executeRemoteCallWithTaskPermission(mController, "setPipAnimationTypeToAlpha", + (controller) -> controller.setPinnedStackAnimationType(ANIM_TYPE_ALPHA)); + } } } 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..9729a4007bac 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 @@ -235,21 +235,14 @@ public class PipDismissTargetHandler implements ViewTreeObserver.OnPreDrawListen /** Adds the magnetic target view to the WindowManager so it's ready to be animated in. */ public void createOrUpdateDismissTarget() { - if (!mTargetViewContainer.isAttachedToWindow()) { + if (mTargetViewContainer.getParent() == null) { mTargetViewContainer.cancelAnimators(); mTargetViewContainer.setVisibility(View.INVISIBLE); mTargetViewContainer.getViewTreeObserver().removeOnPreDrawListener(this); mHasDismissTargetSurface = false; - try { - mWindowManager.addView(mTargetViewContainer, getDismissTargetLayoutParams()); - } catch (IllegalStateException e) { - // This shouldn't happen, but if the target is already added, just update its layout - // params. - mWindowManager.updateViewLayout( - mTargetViewContainer, getDismissTargetLayoutParams()); - } + mWindowManager.addView(mTargetViewContainer, getDismissTargetLayoutParams()); } else { mWindowManager.updateViewLayout(mTargetViewContainer, getDismissTargetLayoutParams()); } @@ -288,8 +281,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. */ @@ -304,7 +299,7 @@ public class PipDismissTargetHandler implements ViewTreeObserver.OnPreDrawListen * Removes the dismiss target and cancels any pending callbacks to show it. */ public void cleanUpDismissTarget() { - if (mTargetViewContainer.isAttachedToWindow()) { + if (mTargetViewContainer.getParent() != null) { mWindowManager.removeViewImmediate(mTargetViewContainer); } } 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..d7d335b856be --- /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_DEFAULT, + SIZE_SPEC_MAX, + SIZE_SPEC_CUSTOM + }) + @Retention(RetentionPolicy.SOURCE) + @interface PipSizeSpec {} + + static final int SIZE_SPEC_DEFAULT = 0; + static final int SIZE_SPEC_MAX = 1; + static final int SIZE_SPEC_CUSTOM = 2; + + /** + * 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..167c0321d3ad 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); @@ -279,10 +282,11 @@ public class PipMenuView extends FrameLayout { public void onFocusTaskChanged(ActivityManager.RunningTaskInfo taskInfo) { final boolean isSplitScreen = mSplitScreenControllerOptional.isPresent() - && mSplitScreenControllerOptional.get().isTaskInSplitScreen(taskInfo.taskId); + && mSplitScreenControllerOptional.get().isTaskInSplitScreenForeground( + 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..43d3f36f1fe5 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", false); 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..956af709f156 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 @@ -96,6 +96,7 @@ public class PipResizeGestureHandler { private final Rect mDisplayBounds = new Rect(); private final Function<Rect, Rect> mMovementBoundsSupplier; private final Runnable mUpdateMovementBoundsRunnable; + private final Consumer<Rect> mUpdateResizeBoundsCallback; private int mDelta; private float mTouchSlop; @@ -137,6 +138,13 @@ public class PipResizeGestureHandler { mPhonePipMenuController = menuActivityController; mPipUiEventLogger = pipUiEventLogger; mPinchResizingAlgorithm = new PipPinchResizingAlgorithm(); + + mUpdateResizeBoundsCallback = (rect) -> { + mUserResizeBounds.set(rect); + mMotionHelper.synchronizePinnedStackBounds(); + mUpdateMovementBoundsRunnable.run(); + resetState(); + }; } public void init() { @@ -220,7 +228,7 @@ public class PipResizeGestureHandler { if (mIsEnabled) { // Register input event receiver - mInputMonitor = InputManager.getInstance().monitorGestureInput( + mInputMonitor = mContext.getSystemService(InputManager.class).monitorGestureInput( "pip-resize", mDisplayId); try { mMainExecutor.executeBlocking(() -> { @@ -508,15 +516,50 @@ public class PipResizeGestureHandler { } } + private void snapToMovementBoundsEdge(Rect bounds, Rect movementBounds) { + final int leftEdge = bounds.left; + + + final int fromLeft = Math.abs(leftEdge - movementBounds.left); + final int fromRight = Math.abs(movementBounds.right - leftEdge); + + // The PIP will be snapped to either the right or left edge, so calculate which one + // is closest to the current position. + final int newLeft = fromLeft < fromRight + ? movementBounds.left : movementBounds.right; + + bounds.offsetTo(newLeft, mLastResizeBounds.top); + } + + /** + * Resizes the pip window and updates user-resized bounds. + * + * @param bounds target bounds to resize to + * @param snapFraction snap fraction to apply after resizing + */ + void userResizeTo(Rect bounds, float snapFraction) { + Rect finalBounds = new Rect(bounds); + + // get the current movement bounds + final Rect movementBounds = mPipBoundsAlgorithm.getMovementBounds(finalBounds); + + // snap the target bounds to the either left or right edge, by choosing the closer one + snapToMovementBoundsEdge(finalBounds, movementBounds); + + // apply the requested snap fraction onto the target bounds + mPipBoundsAlgorithm.applySnapFraction(finalBounds, snapFraction); + + // resize from current bounds to target bounds without animation + mPipTaskOrganizer.scheduleUserResizePip(mPipBoundsState.getBounds(), finalBounds, null); + // set the flag that pip has been resized + mPipBoundsState.setHasUserResizedPip(true); + + // finish the resize operation and update the state of the bounds + mPipTaskOrganizer.scheduleFinishResizePip(finalBounds, mUpdateResizeBoundsCallback); + } + private void finishResize() { if (!mLastResizeBounds.isEmpty()) { - final Consumer<Rect> callback = (rect) -> { - mUserResizeBounds.set(mLastResizeBounds); - mMotionHelper.synchronizePinnedStackBounds(); - mUpdateMovementBoundsRunnable.run(); - resetState(); - }; - // Pinch-to-resize needs to re-calculate snap fraction and animate to the snapped // position correctly. Drag-resize does not need to move, so just finalize resize. if (mOngoingPinchToResize) { @@ -526,24 +569,31 @@ public class PipResizeGestureHandler { || mLastResizeBounds.height() >= PINCH_RESIZE_AUTO_MAX_RATIO * mMaxSize.y) { resizeRectAboutCenter(mLastResizeBounds, mMaxSize.x, mMaxSize.y); } - final int leftEdge = mLastResizeBounds.left; - final Rect movementBounds = - mPipBoundsAlgorithm.getMovementBounds(mLastResizeBounds); - final int fromLeft = Math.abs(leftEdge - movementBounds.left); - final int fromRight = Math.abs(movementBounds.right - leftEdge); - // The PIP will be snapped to either the right or left edge, so calculate which one - // is closest to the current position. - final int newLeft = fromLeft < fromRight - ? movementBounds.left : movementBounds.right; - mLastResizeBounds.offsetTo(newLeft, mLastResizeBounds.top); + + // get the current movement bounds + final Rect movementBounds = mPipBoundsAlgorithm + .getMovementBounds(mLastResizeBounds); + + // snap mLastResizeBounds to the correct edge based on movement bounds + snapToMovementBoundsEdge(mLastResizeBounds, movementBounds); + final float snapFraction = mPipBoundsAlgorithm.getSnapFraction( mLastResizeBounds, movementBounds); mPipBoundsAlgorithm.applySnapFraction(mLastResizeBounds, snapFraction); + + // disable the pinch resizing until the final bounds are updated + final boolean prevEnablePinchResize = mEnablePinchResize; + mEnablePinchResize = false; + mPipTaskOrganizer.scheduleAnimateResizePip(startBounds, mLastResizeBounds, - PINCH_RESIZE_SNAP_DURATION, mAngle, callback); + PINCH_RESIZE_SNAP_DURATION, mAngle, mUpdateResizeBoundsCallback, () -> { + // reset the pinch resizing to its default state + mEnablePinchResize = prevEnablePinchResize; + }); } else { mPipTaskOrganizer.scheduleFinishResizePip(mLastResizeBounds, - PipAnimationController.TRANSITION_DIRECTION_USER_RESIZE, callback); + PipAnimationController.TRANSITION_DIRECTION_USER_RESIZE, + mUpdateResizeBoundsCallback); } final float magnetRadiusPercent = (float) mLastResizeBounds.width() / mMinSize.x / 2.f; mPipDismissTargetHandler @@ -625,8 +675,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/PipSizeSpecHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipSizeSpecHandler.java new file mode 100644 index 000000000000..a7171fd5b220 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipSizeSpecHandler.java @@ -0,0 +1,526 @@ +/* + * 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.PipUtils.dpToPx; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Point; +import android.graphics.PointF; +import android.graphics.Rect; +import android.os.SystemProperties; +import android.util.Size; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.wm.shell.R; +import com.android.wm.shell.common.DisplayLayout; +import com.android.wm.shell.pip.PipDisplayLayoutState; + +import java.io.PrintWriter; + +/** + * Acts as a source of truth for appropriate size spec for PIP. + */ +public class PipSizeSpecHandler { + private static final String TAG = PipSizeSpecHandler.class.getSimpleName(); + + @NonNull private final PipDisplayLayoutState mPipDisplayLayoutState; + + private final SizeSpecSource mSizeSpecSourceImpl; + + /** The preferred minimum (and default minimum) size specified by apps. */ + @Nullable private Size mOverrideMinSize; + private int mOverridableMinSize; + + /** Used to store values obtained from resource files. */ + private Point mScreenEdgeInsets; + private float mMinAspectRatioForMinSize; + private float mMaxAspectRatioForMinSize; + private int mDefaultMinSize; + + @NonNull private final Context mContext; + + private interface SizeSpecSource { + /** Returns max size allowed for the PIP window */ + Size getMaxSize(float aspectRatio); + + /** Returns default size for the PIP window */ + Size getDefaultSize(float aspectRatio); + + /** Returns min size allowed for the PIP window */ + Size getMinSize(float aspectRatio); + + /** Returns the adjusted size based on current size and target aspect ratio */ + Size getSizeForAspectRatio(Size size, float aspectRatio); + + /** Updates internal resources on configuration changes */ + default void reloadResources() {} + } + + /** + * Determines PIP window size optimized for large screens and aspect ratios close to 1:1 + */ + private class SizeSpecLargeScreenOptimizedImpl implements SizeSpecSource { + private static final float DEFAULT_OPTIMIZED_ASPECT_RATIO = 9f / 16; + + /** Default and minimum percentages for the PIP size logic. */ + private final float mDefaultSizePercent; + private final float mMinimumSizePercent; + + /** Aspect ratio that the PIP size spec logic optimizes for. */ + private float mOptimizedAspectRatio; + + private SizeSpecLargeScreenOptimizedImpl() { + mDefaultSizePercent = Float.parseFloat(SystemProperties + .get("com.android.wm.shell.pip.phone.def_percentage", "0.6")); + mMinimumSizePercent = Float.parseFloat(SystemProperties + .get("com.android.wm.shell.pip.phone.min_percentage", "0.5")); + } + + @Override + public void reloadResources() { + final Resources res = mContext.getResources(); + + mOptimizedAspectRatio = res.getFloat(R.dimen.config_pipLargeScreenOptimizedAspectRatio); + // make sure the optimized aspect ratio is valid with a default value to fall back to + if (mOptimizedAspectRatio > 1) { + mOptimizedAspectRatio = DEFAULT_OPTIMIZED_ASPECT_RATIO; + } + } + + /** + * Calculates the max size of PIP. + * + * Optimizes for 16:9 aspect ratios, making them take full length of shortest display edge. + * As aspect ratio approaches values close to 1:1, the logic does not let PIP occupy the + * whole screen. A linear function is used to calculate these sizes. + * + * @param aspectRatio aspect ratio of the PIP window + * @return dimensions of the max size of the PIP + */ + @Override + public Size getMaxSize(float aspectRatio) { + final int totalHorizontalPadding = getInsetBounds().left + + (getDisplayBounds().width() - getInsetBounds().right); + final int totalVerticalPadding = getInsetBounds().top + + (getDisplayBounds().height() - getInsetBounds().bottom); + + final int shorterLength = (int) (1f * Math.min( + getDisplayBounds().width() - totalHorizontalPadding, + getDisplayBounds().height() - totalVerticalPadding)); + + int maxWidth, maxHeight; + + // use the optimized max sizing logic only within a certain aspect ratio range + if (aspectRatio >= mOptimizedAspectRatio && aspectRatio <= 1 / mOptimizedAspectRatio) { + // this formula and its derivation is explained in b/198643358#comment16 + maxWidth = (int) (mOptimizedAspectRatio * shorterLength + + shorterLength * (aspectRatio - mOptimizedAspectRatio) / (1 + + aspectRatio)); + maxHeight = (int) (maxWidth / aspectRatio); + } else { + if (aspectRatio > 1f) { + maxWidth = shorterLength; + maxHeight = (int) (maxWidth / aspectRatio); + } else { + maxHeight = shorterLength; + maxWidth = (int) (maxHeight * aspectRatio); + } + } + + return new Size(maxWidth, maxHeight); + } + + /** + * Decreases the dimensions by a percentage relative to max size to get default size. + * + * @param aspectRatio aspect ratio of the PIP window + * @return dimensions of the default size of the PIP + */ + @Override + public Size getDefaultSize(float aspectRatio) { + Size minSize = this.getMinSize(aspectRatio); + + if (mOverrideMinSize != null) { + return minSize; + } + + Size maxSize = this.getMaxSize(aspectRatio); + + int defaultWidth = Math.max((int) (maxSize.getWidth() * mDefaultSizePercent), + minSize.getWidth()); + int defaultHeight = Math.max((int) (maxSize.getHeight() * mDefaultSizePercent), + minSize.getHeight()); + + return new Size(defaultWidth, defaultHeight); + } + + /** + * Decreases the dimensions by a certain percentage relative to max size to get min size. + * + * @param aspectRatio aspect ratio of the PIP window + * @return dimensions of the min size of the PIP + */ + @Override + public Size getMinSize(float aspectRatio) { + // if there is an overridden min size provided, return that + if (mOverrideMinSize != null) { + return adjustOverrideMinSizeToAspectRatio(aspectRatio); + } + + Size maxSize = this.getMaxSize(aspectRatio); + + int minWidth = (int) (maxSize.getWidth() * mMinimumSizePercent); + int minHeight = (int) (maxSize.getHeight() * mMinimumSizePercent); + + // make sure the calculated min size is not smaller than the allowed default min size + if (aspectRatio > 1f) { + minHeight = (int) Math.max(minHeight, mDefaultMinSize); + minWidth = (int) (minHeight * aspectRatio); + } else { + minWidth = (int) Math.max(minWidth, mDefaultMinSize); + minHeight = (int) (minWidth / aspectRatio); + } + return new Size(minWidth, minHeight); + } + + /** + * Returns the size for target aspect ratio making sure new size conforms with the rules. + * + * <p>Recalculates the dimensions such that the target aspect ratio is achieved, while + * maintaining the same maximum size to current size ratio. + * + * @param size current size + * @param aspectRatio target aspect ratio + */ + @Override + public Size getSizeForAspectRatio(Size size, float aspectRatio) { + float currAspectRatio = (float) size.getWidth() / size.getHeight(); + + // getting the percentage of the max size that current size takes + Size currentMaxSize = getMaxSize(currAspectRatio); + float currentPercent = (float) size.getWidth() / currentMaxSize.getWidth(); + + // getting the max size for the target aspect ratio + Size updatedMaxSize = getMaxSize(aspectRatio); + + int width = Math.round(updatedMaxSize.getWidth() * currentPercent); + int height = Math.round(updatedMaxSize.getHeight() * currentPercent); + + // adjust the dimensions if below allowed min edge size + if (width < getMinEdgeSize() && aspectRatio <= 1) { + width = getMinEdgeSize(); + height = Math.round(width / aspectRatio); + } else if (height < getMinEdgeSize() && aspectRatio > 1) { + height = getMinEdgeSize(); + width = Math.round(height * aspectRatio); + } + + // reduce the dimensions of the updated size to the calculated percentage + return new Size(width, height); + } + } + + private class SizeSpecDefaultImpl implements SizeSpecSource { + private float mDefaultSizePercent; + private float mMinimumSizePercent; + + @Override + public void reloadResources() { + final Resources res = mContext.getResources(); + + mMaxAspectRatioForMinSize = res.getFloat( + R.dimen.config_pictureInPictureAspectRatioLimitForMinSize); + mMinAspectRatioForMinSize = 1f / mMaxAspectRatioForMinSize; + + mDefaultSizePercent = res.getFloat(R.dimen.config_pictureInPictureDefaultSizePercent); + mMinimumSizePercent = res.getFraction(R.fraction.config_pipShortestEdgePercent, 1, 1); + } + + @Override + public Size getMaxSize(float aspectRatio) { + final int shorterLength = Math.min(getDisplayBounds().width(), + getDisplayBounds().height()); + + final int totalHorizontalPadding = getInsetBounds().left + + (getDisplayBounds().width() - getInsetBounds().right); + final int totalVerticalPadding = getInsetBounds().top + + (getDisplayBounds().height() - getInsetBounds().bottom); + + final int maxWidth, maxHeight; + + if (aspectRatio > 1f) { + maxWidth = (int) Math.max(getDefaultSize(aspectRatio).getWidth(), + shorterLength - totalHorizontalPadding); + maxHeight = (int) (maxWidth / aspectRatio); + } else { + maxHeight = (int) Math.max(getDefaultSize(aspectRatio).getHeight(), + shorterLength - totalVerticalPadding); + maxWidth = (int) (maxHeight * aspectRatio); + } + + return new Size(maxWidth, maxHeight); + } + + @Override + public Size getDefaultSize(float aspectRatio) { + if (mOverrideMinSize != null) { + return this.getMinSize(aspectRatio); + } + + final int smallestDisplaySize = Math.min(getDisplayBounds().width(), + getDisplayBounds().height()); + final int minSize = (int) Math.max(getMinEdgeSize(), + smallestDisplaySize * mDefaultSizePercent); + + final int width; + final int height; + + if (aspectRatio <= mMinAspectRatioForMinSize + || aspectRatio > mMaxAspectRatioForMinSize) { + // Beyond these points, we can just use the min size as the shorter edge + if (aspectRatio <= 1) { + // Portrait, width is the minimum size + width = minSize; + height = Math.round(width / aspectRatio); + } else { + // Landscape, height is the minimum size + height = minSize; + width = Math.round(height * aspectRatio); + } + } else { + // Within these points, ensure that the bounds fit within the radius of the limits + // at the points + final float widthAtMaxAspectRatioForMinSize = mMaxAspectRatioForMinSize * minSize; + final float radius = PointF.length(widthAtMaxAspectRatioForMinSize, minSize); + height = (int) Math.round(Math.sqrt((radius * radius) + / (aspectRatio * aspectRatio + 1))); + width = Math.round(height * aspectRatio); + } + + return new Size(width, height); + } + + @Override + public Size getMinSize(float aspectRatio) { + if (mOverrideMinSize != null) { + return adjustOverrideMinSizeToAspectRatio(aspectRatio); + } + + final int shorterLength = Math.min(getDisplayBounds().width(), + getDisplayBounds().height()); + final int minWidth, minHeight; + + if (aspectRatio > 1f) { + minWidth = (int) Math.min(getDefaultSize(aspectRatio).getWidth(), + shorterLength * mMinimumSizePercent); + minHeight = (int) (minWidth / aspectRatio); + } else { + minHeight = (int) Math.min(getDefaultSize(aspectRatio).getHeight(), + shorterLength * mMinimumSizePercent); + minWidth = (int) (minHeight * aspectRatio); + } + + return new Size(minWidth, minHeight); + } + + @Override + public Size getSizeForAspectRatio(Size size, float aspectRatio) { + final int smallestSize = Math.min(size.getWidth(), size.getHeight()); + final int minSize = Math.max(getMinEdgeSize(), smallestSize); + + final int width; + final int height; + if (aspectRatio <= 1) { + // Portrait, width is the minimum size. + width = minSize; + height = Math.round(width / aspectRatio); + } else { + // Landscape, height is the minimum size + height = minSize; + width = Math.round(height * aspectRatio); + } + + return new Size(width, height); + } + } + + public PipSizeSpecHandler(Context context, PipDisplayLayoutState pipDisplayLayoutState) { + mContext = context; + mPipDisplayLayoutState = pipDisplayLayoutState; + + boolean enablePipSizeLargeScreen = SystemProperties + .getBoolean("persist.wm.debug.enable_pip_size_large_screen", true); + + // choose between two implementations of size spec logic + if (enablePipSizeLargeScreen) { + mSizeSpecSourceImpl = new SizeSpecLargeScreenOptimizedImpl(); + } else { + mSizeSpecSourceImpl = new SizeSpecDefaultImpl(); + } + + reloadResources(); + } + + /** Reloads the resources */ + public void onConfigurationChanged() { + reloadResources(); + } + + private void reloadResources() { + final Resources res = mContext.getResources(); + + mDefaultMinSize = res.getDimensionPixelSize( + R.dimen.default_minimal_size_pip_resizable_task); + mOverridableMinSize = res.getDimensionPixelSize( + R.dimen.overridable_minimal_size_pip_resizable_task); + + final String screenEdgeInsetsDpString = res.getString( + R.string.config_defaultPictureInPictureScreenEdgeInsets); + final Size screenEdgeInsetsDp = !screenEdgeInsetsDpString.isEmpty() + ? Size.parseSize(screenEdgeInsetsDpString) + : null; + mScreenEdgeInsets = screenEdgeInsetsDp == null ? new Point() + : new Point(dpToPx(screenEdgeInsetsDp.getWidth(), res.getDisplayMetrics()), + dpToPx(screenEdgeInsetsDp.getHeight(), res.getDisplayMetrics())); + + // update the internal resources of the size spec source's stub + mSizeSpecSourceImpl.reloadResources(); + } + + @NonNull + private Rect getDisplayBounds() { + return mPipDisplayLayoutState.getDisplayBounds(); + } + + public Point getScreenEdgeInsets() { + return mScreenEdgeInsets; + } + + /** + * Returns the inset bounds the PIP window can be visible in. + */ + public Rect getInsetBounds() { + Rect insetBounds = new Rect(); + DisplayLayout displayLayout = mPipDisplayLayoutState.getDisplayLayout(); + Rect insets = displayLayout.stableInsets(); + insetBounds.set(insets.left + mScreenEdgeInsets.x, + insets.top + mScreenEdgeInsets.y, + displayLayout.width() - insets.right - mScreenEdgeInsets.x, + displayLayout.height() - insets.bottom - mScreenEdgeInsets.y); + return insetBounds; + } + + /** Sets the preferred size of PIP as specified by the activity in PIP mode. */ + public void setOverrideMinSize(@Nullable Size overrideMinSize) { + mOverrideMinSize = overrideMinSize; + } + + /** Returns the preferred minimal size specified by the activity in PIP. */ + @Nullable + public Size getOverrideMinSize() { + if (mOverrideMinSize != null + && (mOverrideMinSize.getWidth() < mOverridableMinSize + || mOverrideMinSize.getHeight() < mOverridableMinSize)) { + return new Size(mOverridableMinSize, mOverridableMinSize); + } + + return mOverrideMinSize; + } + + /** Returns the minimum edge size of the override minimum size, or 0 if not set. */ + public int getOverrideMinEdgeSize() { + if (mOverrideMinSize == null) return 0; + return Math.min(getOverrideMinSize().getWidth(), getOverrideMinSize().getHeight()); + } + + public int getMinEdgeSize() { + return mOverrideMinSize == null ? mDefaultMinSize : getOverrideMinEdgeSize(); + } + + /** + * Returns the size for the max size spec. + */ + public Size getMaxSize(float aspectRatio) { + return mSizeSpecSourceImpl.getMaxSize(aspectRatio); + } + + /** + * Returns the size for the default size spec. + */ + public Size getDefaultSize(float aspectRatio) { + return mSizeSpecSourceImpl.getDefaultSize(aspectRatio); + } + + /** + * Returns the size for the min size spec. + */ + public Size getMinSize(float aspectRatio) { + return mSizeSpecSourceImpl.getMinSize(aspectRatio); + } + + /** + * Returns the adjusted size so that it conforms to the given aspectRatio. + * + * @param size current size + * @param aspectRatio target aspect ratio + */ + public Size getSizeForAspectRatio(@NonNull Size size, float aspectRatio) { + if (size.equals(mOverrideMinSize)) { + return adjustOverrideMinSizeToAspectRatio(aspectRatio); + } + + return mSizeSpecSourceImpl.getSizeForAspectRatio(size, aspectRatio); + } + + /** + * Returns the adjusted overridden min size if it is set; otherwise, returns null. + * + * <p>Overridden min size needs to be adjusted in its own way while making sure that the target + * aspect ratio is maintained + * + * @param aspectRatio target aspect ratio + */ + @Nullable + @VisibleForTesting + Size adjustOverrideMinSizeToAspectRatio(float aspectRatio) { + if (mOverrideMinSize == null) { + return null; + } + final Size size = getOverrideMinSize(); + final float sizeAspectRatio = size.getWidth() / (float) size.getHeight(); + if (sizeAspectRatio > aspectRatio) { + // Size is wider, fix the width and increase the height + return new Size(size.getWidth(), (int) (size.getWidth() / aspectRatio)); + } else { + // Size is taller, fix the height and adjust the width. + return new Size((int) (size.getHeight() * aspectRatio), size.getHeight()); + } + } + + /** Dumps internal state. */ + public void dump(PrintWriter pw, String prefix) { + final String innerPrefix = prefix + " "; + pw.println(prefix + TAG); + pw.println(innerPrefix + "mSizeSpecSourceImpl=" + mSizeSpecSourceImpl); + pw.println(innerPrefix + "mOverrideMinSize=" + mOverrideMinSize); + pw.println(innerPrefix + "mScreenEdgeInsets=" + mScreenEdgeInsets); + } +} 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..466da0e85358 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,11 +71,20 @@ public class PipTouchHandler { private static final String TAG = "PipTouchHandler"; private static final float DEFAULT_STASH_VELOCITY_THRESHOLD = 18000.f; + private boolean mEnablePipKeepClearAlgorithm = + SystemProperties.getBoolean("persist.wm.debug.enable_pip_keep_clear_algorithm", true); + + @VisibleForTesting + void setEnablePipKeepClearAlgorithm(boolean value) { + mEnablePipKeepClearAlgorithm = value; + } + // Allow PIP to resize to a slightly bigger state upon touch private boolean mEnableResize; private final Context mContext; private final PipBoundsAlgorithm mPipBoundsAlgorithm; - private final @NonNull PipBoundsState mPipBoundsState; + @NonNull private final PipBoundsState mPipBoundsState; + @NonNull private final PipSizeSpecHandler mPipSizeSpecHandler; private final PipUiEventLogger mPipUiEventLogger; private final PipDismissTargetHandler mPipDismissTargetHandler; private final PipTaskOrganizer mPipTaskOrganizer; @@ -93,7 +105,6 @@ public class PipTouchHandler { // The reference inset bounds, used to determine the dismiss fraction private final Rect mInsetBounds = new Rect(); - private int mExpandedShortestEdgeSize; // Used to workaround an issue where the WM rotation happens before we are notified, allowing // us to send stale bounds @@ -114,7 +125,6 @@ public class PipTouchHandler { private float mSavedSnapFraction = -1f; private boolean mSendingHoverAccessibilityEvents; private boolean mMovementWithinDismiss; - private float mMinimumSizePercent; // Touch state private final PipTouchState mTouchState; @@ -164,20 +174,22 @@ public class PipTouchHandler { @SuppressLint("InflateParams") public PipTouchHandler(Context context, + ShellInit shellInit, PhonePipMenuController menuController, PipBoundsAlgorithm pipBoundsAlgorithm, @NonNull PipBoundsState pipBoundsState, + @NonNull PipSizeSpecHandler pipSizeSpecHandler, PipTaskOrganizer pipTaskOrganizer, PipMotionHelper pipMotionHelper, FloatingContentCoordinator floatingContentCoordinator, PipUiEventLogger pipUiEventLogger, ShellExecutor mainExecutor) { - // Initialize the Pip input consumer mContext = context; mMainExecutor = mainExecutor; mAccessibilityManager = context.getSystemService(AccessibilityManager.class); mPipBoundsAlgorithm = pipBoundsAlgorithm; mPipBoundsState = pipBoundsState; + mPipSizeSpecHandler = pipSizeSpecHandler; mPipTaskOrganizer = pipTaskOrganizer; mMenuController = menuController; mPipUiEventLogger = pipUiEventLogger; @@ -212,9 +224,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,13 +270,14 @@ 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); - mExpandedShortestEdgeSize = res.getDimensionPixelSize( - R.dimen.pip_expanded_shortest_edge_size); mImeOffset = res.getDimensionPixelSize(R.dimen.pip_ime_offset); - mMinimumSizePercent = res.getFraction(R.fraction.config_pipShortestEdgePercent, 1, 1); mPipDismissTargetHandler.updateMagneticTargetSize(); } @@ -319,8 +340,10 @@ public class PipTouchHandler { mMotionHelper.synchronizePinnedStackBounds(); reloadResources(); - // Recreate the dismiss target for the new orientation. - mPipDismissTargetHandler.createOrUpdateDismissTarget(); + if (mPipTaskOrganizer.isInPip()) { + // Recreate the dismiss target for the new orientation. + mPipDismissTargetHandler.createOrUpdateDismissTarget(); + } } public void onImeVisibilityChanged(boolean imeVisible, int imeHeight) { @@ -387,10 +410,7 @@ public class PipTouchHandler { // Calculate the expanded size float aspectRatio = (float) normalBounds.width() / normalBounds.height(); - Point displaySize = new Point(); - mContext.getDisplay().getRealSize(displaySize); - Size expandedSize = mPipBoundsAlgorithm.getSizeForAspectRatio( - aspectRatio, mExpandedShortestEdgeSize, displaySize.x, displaySize.y); + Size expandedSize = mPipSizeSpecHandler.getDefaultSize(aspectRatio); mPipBoundsState.setExpandedBounds( new Rect(0, 0, expandedSize.getWidth(), expandedSize.getHeight())); Rect expandedMovementBounds = new Rect(); @@ -398,13 +418,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(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 @@ -415,9 +429,12 @@ public class PipTouchHandler { // If this is from an IME or shelf adjustment, then we should move the PiP so that it is not // occluded by the IME or shelf. if (fromImeAdjustment || fromShelfAdjustment) { - if (mTouchState.isUserInteracting()) { + if (mTouchState.isUserInteracting() && mTouchState.isDragging()) { // Defer the update of the current movement bounds until after the user finishes // touching the screen + } else if (mEnablePipKeepClearAlgorithm) { + // 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,26 +490,34 @@ public class PipTouchHandler { } } - private void updatePinchResizeSizeConstraints(Rect insetBounds, Rect normalBounds, + /** + * 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(mPipBoundsState.getNormalBounds(), + aspectRatio); + } + + private void updatePipSizeConstraints(Rect normalBounds, float aspectRatio) { - final int shorterLength = Math.min(mPipBoundsState.getDisplayBounds().width(), - mPipBoundsState.getDisplayBounds().height()); - final int totalHorizontalPadding = insetBounds.left - + (mPipBoundsState.getDisplayBounds().width() - insetBounds.right); - final int totalVerticalPadding = insetBounds.top - + (mPipBoundsState.getDisplayBounds().height() - insetBounds.bottom); - final int minWidth, minHeight, maxWidth, maxHeight; - if (aspectRatio > 1f) { - minWidth = (int) Math.min(normalBounds.width(), shorterLength * mMinimumSizePercent); - minHeight = (int) (minWidth / aspectRatio); - maxWidth = (int) Math.max(normalBounds.width(), shorterLength - totalHorizontalPadding); - maxHeight = (int) (maxWidth / aspectRatio); + if (mPipResizeGestureHandler.isUsingPinchToZoom()) { + updatePinchResizeSizeConstraints(aspectRatio); } else { - minHeight = (int) Math.min(normalBounds.height(), shorterLength * mMinimumSizePercent); - minWidth = (int) (minHeight * aspectRatio); - maxHeight = (int) Math.max(normalBounds.height(), shorterLength - totalVerticalPadding); - maxWidth = (int) (maxHeight * aspectRatio); + mPipResizeGestureHandler.updateMinSize(normalBounds.width(), normalBounds.height()); + mPipResizeGestureHandler.updateMaxSize(mPipBoundsState.getExpandedBounds().width(), + mPipBoundsState.getExpandedBounds().height()); } + } + + private void updatePinchResizeSizeConstraints(float aspectRatio) { + final int minWidth, minHeight, maxWidth, maxHeight; + + minWidth = mPipSizeSpecHandler.getMinSize(aspectRatio).getWidth(); + minHeight = mPipSizeSpecHandler.getMinSize(aspectRatio).getHeight(); + maxWidth = mPipSizeSpecHandler.getMaxSize(aspectRatio).getWidth(); + maxHeight = mPipSizeSpecHandler.getMaxSize(aspectRatio).getHeight(); mPipResizeGestureHandler.updateMinSize(minWidth, minHeight); mPipResizeGestureHandler.updateMaxSize(maxWidth, maxHeight); @@ -789,6 +814,16 @@ public class PipTouchHandler { } /** + * Resizes the pip window and updates user resized bounds + * + * @param bounds target bounds to resize to + * @param snapFraction snap fraction to apply after resizing + */ + void userResizeTo(Rect bounds, float snapFraction) { + mPipResizeGestureHandler.userResizeTo(bounds, snapFraction); + } + + /** * Gesture controlling normal movement of the PIP. */ private class DefaultPipTouchGesture extends PipTouchGesture { @@ -829,6 +864,8 @@ public class PipTouchHandler { } if (touchState.isDragging()) { + mPipBoundsState.setHasUserMovedPip(true); + // Move the pinned stack freely final PointF lastDelta = touchState.getLastTouchDelta(); float lastX = mStartPosition.x + mDelta.x; @@ -900,9 +937,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()); } @@ -1005,11 +1051,6 @@ public class PipTouchHandler { mPipBoundsAlgorithm.getMovementBounds(mPipBoundsState.getBounds(), mInsetBounds, mPipBoundsState.getMovementBounds(), mIsImeShowing ? mImeHeight : 0); mMotionHelper.onMovementBoundsChanged(); - - boolean isMenuExpanded = mMenuState == MENU_STATE_FULL; - mPipBoundsState.setMinEdgeSize( - isMenuExpanded && willResizeMenu() ? mExpandedShortestEdgeSize - : mPipBoundsAlgorithm.getDefaultMinSize()); } private Rect getMovementBounds(Rect curBounds) { 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/TvPipAction.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipAction.java new file mode 100644 index 000000000000..222307fba8c2 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipAction.java @@ -0,0 +1,87 @@ +/* + * 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.tv; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.app.Notification; +import android.app.PendingIntent; +import android.content.Context; +import android.os.Handler; + +import com.android.wm.shell.common.TvWindowMenuActionButton; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Objects; + +abstract class TvPipAction { + + @Retention(RetentionPolicy.SOURCE) + @IntDef(prefix = {"ACTION_"}, value = { + ACTION_FULLSCREEN, + ACTION_CLOSE, + ACTION_MOVE, + ACTION_EXPAND_COLLAPSE, + ACTION_CUSTOM, + ACTION_CUSTOM_CLOSE + }) + public @interface ActionType { + } + + public static final int ACTION_FULLSCREEN = 0; + public static final int ACTION_CLOSE = 1; + public static final int ACTION_MOVE = 2; + public static final int ACTION_EXPAND_COLLAPSE = 3; + public static final int ACTION_CUSTOM = 4; + public static final int ACTION_CUSTOM_CLOSE = 5; + + @ActionType + private final int mActionType; + + @NonNull + private final SystemActionsHandler mSystemActionsHandler; + + TvPipAction(@ActionType int actionType, @NonNull SystemActionsHandler systemActionsHandler) { + Objects.requireNonNull(systemActionsHandler); + mActionType = actionType; + mSystemActionsHandler = systemActionsHandler; + } + + boolean isCloseAction() { + return mActionType == ACTION_CLOSE || mActionType == ACTION_CUSTOM_CLOSE; + } + + @ActionType + int getActionType() { + return mActionType; + } + + abstract void populateButton(@NonNull TvWindowMenuActionButton button, Handler mainHandler); + + abstract PendingIntent getPendingIntent(); + + void executeAction() { + mSystemActionsHandler.executeAction(mActionType); + } + + abstract Notification.Action toNotificationAction(Context context); + + interface SystemActionsHandler { + void executeAction(@TvPipAction.ActionType int actionType); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipActionsProvider.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipActionsProvider.java new file mode 100644 index 000000000000..fa62a73ca9b4 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipActionsProvider.java @@ -0,0 +1,245 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.pip.tv; + +import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE; +import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_CLOSE; +import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_CUSTOM; +import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_CUSTOM_CLOSE; +import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_EXPAND_COLLAPSE; +import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_FULLSCREEN; +import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_MOVE; +import static com.android.wm.shell.pip.tv.TvPipController.ACTION_CLOSE_PIP; +import static com.android.wm.shell.pip.tv.TvPipController.ACTION_MOVE_PIP; +import static com.android.wm.shell.pip.tv.TvPipController.ACTION_TOGGLE_EXPANDED_PIP; +import static com.android.wm.shell.pip.tv.TvPipController.ACTION_TO_FULLSCREEN; + +import android.annotation.NonNull; +import android.app.RemoteAction; +import android.content.Context; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.protolog.common.ProtoLog; +import com.android.wm.shell.R; +import com.android.wm.shell.pip.PipMediaController; +import com.android.wm.shell.pip.PipUtils; +import com.android.wm.shell.protolog.ShellProtoLogGroup; + +import java.util.ArrayList; +import java.util.List; + +/** + * Creates the system TvPipActions (fullscreen, close, move, expand/collapse), and handles all the + * changes to the actions, including the custom app actions and media actions. Other components can + * listen to those changes. + */ +public class TvPipActionsProvider implements TvPipAction.SystemActionsHandler { + private static final String TAG = TvPipActionsProvider.class.getSimpleName(); + + private static final int CLOSE_ACTION_INDEX = 1; + private static final int FIRST_CUSTOM_ACTION_INDEX = 2; + + private final List<Listener> mListeners = new ArrayList<>(); + private final TvPipAction.SystemActionsHandler mSystemActionsHandler; + + private final List<TvPipAction> mActionsList; + private final TvPipSystemAction mDefaultCloseAction; + private final TvPipSystemAction mExpandCollapseAction; + + private final List<RemoteAction> mMediaActions = new ArrayList<>(); + private final List<RemoteAction> mAppActions = new ArrayList<>(); + + public TvPipActionsProvider(Context context, PipMediaController pipMediaController, + TvPipAction.SystemActionsHandler systemActionsHandler) { + mSystemActionsHandler = systemActionsHandler; + + mActionsList = new ArrayList<>(); + mActionsList.add(new TvPipSystemAction(ACTION_FULLSCREEN, R.string.pip_fullscreen, + R.drawable.pip_ic_fullscreen_white, ACTION_TO_FULLSCREEN, context, + mSystemActionsHandler)); + + mDefaultCloseAction = new TvPipSystemAction(ACTION_CLOSE, R.string.pip_close, + R.drawable.pip_ic_close_white, ACTION_CLOSE_PIP, context, mSystemActionsHandler); + mActionsList.add(mDefaultCloseAction); + + mActionsList.add(new TvPipSystemAction(ACTION_MOVE, R.string.pip_move, + R.drawable.pip_ic_move_white, ACTION_MOVE_PIP, context, mSystemActionsHandler)); + + mExpandCollapseAction = new TvPipSystemAction(ACTION_EXPAND_COLLAPSE, R.string.pip_collapse, + R.drawable.pip_ic_collapse, ACTION_TOGGLE_EXPANDED_PIP, context, + mSystemActionsHandler); + mActionsList.add(mExpandCollapseAction); + + pipMediaController.addActionListener(this::onMediaActionsChanged); + } + + @Override + public void executeAction(@TvPipAction.ActionType int actionType) { + if (mSystemActionsHandler != null) { + mSystemActionsHandler.executeAction(actionType); + } + } + + private void notifyActionsChanged(int added, int changed, int startIndex) { + for (Listener listener : mListeners) { + listener.onActionsChanged(added, changed, startIndex); + } + } + + @VisibleForTesting(visibility = PACKAGE) + public void setAppActions(@NonNull List<RemoteAction> appActions, RemoteAction closeAction) { + // Update close action. + mActionsList.set(CLOSE_ACTION_INDEX, + closeAction == null ? mDefaultCloseAction + : new TvPipCustomAction(ACTION_CUSTOM_CLOSE, closeAction, + mSystemActionsHandler)); + notifyActionsChanged(/* added= */ 0, /* updated= */ 1, CLOSE_ACTION_INDEX); + + // Replace custom actions with new ones. + mAppActions.clear(); + for (RemoteAction action : appActions) { + if (action != null && !PipUtils.remoteActionsMatch(action, closeAction)) { + // Only show actions that aren't duplicates of the custom close action. + mAppActions.add(action); + } + } + + updateCustomActions(mAppActions); + } + + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE) + public void onMediaActionsChanged(List<RemoteAction> actions) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: onMediaActionsChanged()", TAG); + + mMediaActions.clear(); + // Don't show disabled actions. + for (RemoteAction remoteAction : actions) { + if (remoteAction.isEnabled()) { + mMediaActions.add(remoteAction); + } + } + + updateCustomActions(mMediaActions); + } + + private void updateCustomActions(@NonNull List<RemoteAction> customActions) { + List<RemoteAction> newCustomActions = customActions; + if (newCustomActions == mMediaActions && !mAppActions.isEmpty()) { + // Don't show the media actions while there are app actions. + return; + } else if (newCustomActions == mAppActions && mAppActions.isEmpty()) { + // If all the app actions were removed, show the media actions. + newCustomActions = mMediaActions; + } + + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: replaceCustomActions, count: %d", TAG, newCustomActions.size()); + int oldCustomActionsCount = 0; + for (TvPipAction action : mActionsList) { + if (action.getActionType() == ACTION_CUSTOM) { + oldCustomActionsCount++; + } + } + mActionsList.removeIf(tvPipAction -> tvPipAction.getActionType() == ACTION_CUSTOM); + + List<TvPipAction> actions = new ArrayList<>(); + for (RemoteAction action : newCustomActions) { + actions.add(new TvPipCustomAction(ACTION_CUSTOM, action, mSystemActionsHandler)); + } + mActionsList.addAll(FIRST_CUSTOM_ACTION_INDEX, actions); + + int added = newCustomActions.size() - oldCustomActionsCount; + int changed = Math.min(newCustomActions.size(), oldCustomActionsCount); + notifyActionsChanged(added, changed, FIRST_CUSTOM_ACTION_INDEX); + } + + @VisibleForTesting(visibility = PACKAGE) + public void updateExpansionEnabled(boolean enabled) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: updateExpansionState, enabled: %b", TAG, enabled); + int actionIndex = mActionsList.indexOf(mExpandCollapseAction); + boolean actionInList = actionIndex != -1; + if (enabled && !actionInList) { + mActionsList.add(mExpandCollapseAction); + actionIndex = mActionsList.size() - 1; + } else if (!enabled && actionInList) { + mActionsList.remove(actionIndex); + } else { + return; + } + notifyActionsChanged(/* added= */ enabled ? 1 : -1, /* updated= */ 0, actionIndex); + } + + @VisibleForTesting(visibility = PACKAGE) + public void onPipExpansionToggled(boolean expanded) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: onPipExpansionToggled, expanded: %b", TAG, expanded); + + mExpandCollapseAction.update( + expanded ? R.string.pip_collapse : R.string.pip_expand, + expanded ? R.drawable.pip_ic_collapse : R.drawable.pip_ic_expand); + + notifyActionsChanged(/* added= */ 0, /* updated= */ 1, + mActionsList.indexOf(mExpandCollapseAction)); + } + + List<TvPipAction> getActionsList() { + return mActionsList; + } + + @NonNull + TvPipAction getCloseAction() { + return mActionsList.get(CLOSE_ACTION_INDEX); + } + + void addListener(Listener listener) { + if (!mListeners.contains(listener)) { + mListeners.add(listener); + } + } + + /** + * Returns the index of the first action of the given action type or -1 if none can be found. + */ + int getFirstIndexOfAction(@TvPipAction.ActionType int actionType) { + for (int i = 0; i < mActionsList.size(); i++) { + if (mActionsList.get(i).getActionType() == actionType) { + return i; + } + } + return -1; + } + + /** + * Allow components to listen to updates to the actions list, including where they happen so + * that changes can be animated. + */ + interface Listener { + /** + * Notifies the listener how many actions were added/removed or updated. + * + * @param added can be positive (number of actions added), negative (number of actions + * removed) or zero (the number of actions stayed the same). + * @param updated the number of actions that might have been updated and need to be + * refreshed. + * @param startIndex The index of the first updated action. The added/removed actions start + * at (startIndex + updated). + */ + void onActionsChanged(int added, int updated, int startIndex); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBackgroundView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBackgroundView.java new file mode 100644 index 000000000000..0221db836dda --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBackgroundView.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.pip.tv; + +import static com.android.wm.shell.pip.tv.TvPipMenuController.MODE_ALL_ACTIONS_MENU; +import static com.android.wm.shell.pip.tv.TvPipMenuController.MODE_MOVE_MENU; +import static com.android.wm.shell.pip.tv.TvPipMenuController.MODE_NO_MENU; + +import android.content.Context; +import android.content.res.Resources; +import android.view.View; +import android.view.animation.Interpolator; +import android.widget.FrameLayout; + +import androidx.annotation.NonNull; + +import com.android.internal.protolog.common.ProtoLog; +import com.android.wm.shell.R; +import com.android.wm.shell.protolog.ShellProtoLogGroup; + +/** + * This view is part of the Tv PiP menu. It is drawn behind the PiP surface and serves as a + * background behind the PiP content. If the PiP content is translucent, this view is visible + * behind it. + * It is also used to draw the shadow behind the Tv PiP menu. The shadow intensity is determined + * by the menu mode that the Tv PiP menu is in. See {@link TvPipMenuController.TvPipMenuMode}. + */ +class TvPipBackgroundView extends FrameLayout { + private static final String TAG = "TvPipBackgroundView"; + + private final View mBackgroundView; + private final int mElevationNoMenu; + private final int mElevationMoveMenu; + private final int mElevationAllActionsMenu; + private final int mPipMenuFadeAnimationDuration; + + private @TvPipMenuController.TvPipMenuMode int mCurrentMenuMode = MODE_NO_MENU; + + TvPipBackgroundView(@NonNull Context context) { + super(context, null, 0, 0); + inflate(context, R.layout.tv_pip_menu_background, this); + + mBackgroundView = findViewById(R.id.background_view); + + final Resources res = mContext.getResources(); + mElevationNoMenu = res.getDimensionPixelSize(R.dimen.pip_menu_elevation_no_menu); + mElevationMoveMenu = res.getDimensionPixelSize(R.dimen.pip_menu_elevation_move_menu); + mElevationAllActionsMenu = + res.getDimensionPixelSize(R.dimen.pip_menu_elevation_all_actions_menu); + mPipMenuFadeAnimationDuration = + res.getInteger(R.integer.tv_window_menu_fade_animation_duration); + } + + void transitionToMenuMode(@TvPipMenuController.TvPipMenuMode int pipMenuMode) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: transitionToMenuMode(), old menu mode = %s, new menu mode = %s", + TAG, TvPipMenuController.getMenuModeString(mCurrentMenuMode), + TvPipMenuController.getMenuModeString(pipMenuMode)); + + if (mCurrentMenuMode == pipMenuMode) return; + + int elevation = mElevationNoMenu; + Interpolator interpolator = TvPipInterpolators.ENTER; + switch(pipMenuMode) { + case MODE_NO_MENU: + elevation = mElevationNoMenu; + interpolator = TvPipInterpolators.EXIT; + break; + case MODE_MOVE_MENU: + elevation = mElevationMoveMenu; + break; + case MODE_ALL_ACTIONS_MENU: + elevation = mElevationAllActionsMenu; + if (mCurrentMenuMode == MODE_MOVE_MENU) { + interpolator = TvPipInterpolators.EXIT; + } + break; + default: + throw new IllegalArgumentException( + "Unknown TV PiP menu mode: " + pipMenuMode); + } + + mBackgroundView.animate() + .translationZ(elevation) + .setInterpolator(interpolator) + .setDuration(mPipMenuFadeAnimationDuration) + .start(); + + mCurrentMenuMode = pipMenuMode; + } + +} 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..825b96921a22 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 @@ -22,14 +22,12 @@ import static android.view.KeyEvent.KEYCODE_DPAD_RIGHT; import static android.view.KeyEvent.KEYCODE_DPAD_UP; import static com.android.wm.shell.pip.tv.TvPipBoundsState.ORIENTATION_HORIZONTAL; -import static com.android.wm.shell.pip.tv.TvPipBoundsState.ORIENTATION_UNDETERMINED; import static com.android.wm.shell.pip.tv.TvPipBoundsState.ORIENTATION_VERTICAL; 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,7 +37,9 @@ 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.PipKeepClearAlgorithmInterface; import com.android.wm.shell.pip.PipSnapAlgorithm; +import com.android.wm.shell.pip.phone.PipSizeSpecHandler; 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; @@ -62,8 +61,10 @@ public class TvPipBoundsAlgorithm extends PipBoundsAlgorithm { public TvPipBoundsAlgorithm(Context context, @NonNull TvPipBoundsState tvPipBoundsState, - @NonNull PipSnapAlgorithm pipSnapAlgorithm) { - super(context, tvPipBoundsState, pipSnapAlgorithm); + @NonNull PipSnapAlgorithm pipSnapAlgorithm, + @NonNull PipSizeSpecHandler pipSizeSpecHandler) { + super(context, tvPipBoundsState, pipSnapAlgorithm, + new PipKeepClearAlgorithmInterface() {}, pipSizeSpecHandler); this.mTvPipBoundsState = tvPipBoundsState; this.mKeepClearAlgorithm = new TvPipKeepClearAlgorithm(); reloadResources(context); @@ -90,16 +91,15 @@ 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 && !mTvPipBoundsState.isTvPipManuallyCollapsed(); if (isPipExpanded) { - updateGravityOnExpandToggled(Gravity.NO_GRAVITY, true); + updateGravityOnExpansionToggled(/* expanding= */ true); } mTvPipBoundsState.setTvPipExpanded(isPipExpanded); return adjustBoundsForTemporaryDecor(getTvPipPlacement().getBounds()); @@ -108,10 +108,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 +137,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,165 +153,105 @@ 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; } - /** - * @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)); - } - - if (!mTvPipBoundsState.isTvExpandedPipSupported()) { - return Gravity.NO_GRAVITY; - } + void updateGravityOnExpansionToggled(boolean expanding) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: updateGravity, expanding: %b, fixedExpandedOrientation: %d", + TAG, expanding, mTvPipBoundsState.getTvFixedPipOrientation()); - if (expanding && mTvPipBoundsState.getTvFixedPipOrientation() == ORIENTATION_UNDETERMINED) { - float expandedRatio = mTvPipBoundsState.getDesiredTvExpandedAspectRatio(); - if (expandedRatio == 0) { - return Gravity.NO_GRAVITY; - } - if (expandedRatio < 1) { - mTvPipBoundsState.setTvFixedPipOrientation(ORIENTATION_VERTICAL); - } else { - mTvPipBoundsState.setTvFixedPipOrientation(ORIENTATION_HORIZONTAL); - } - } + int currentX = mTvPipBoundsState.getTvPipGravity() & Gravity.HORIZONTAL_GRAVITY_MASK; + int currentY = mTvPipBoundsState.getTvPipGravity() & Gravity.VERTICAL_GRAVITY_MASK; + int previousCollapsedX = mTvPipBoundsState.getTvPipPreviousCollapsedGravity() + & Gravity.HORIZONTAL_GRAVITY_MASK; + int previousCollapsedY = mTvPipBoundsState.getTvPipPreviousCollapsedGravity() + & Gravity.VERTICAL_GRAVITY_MASK; - int gravityToSave = Gravity.NO_GRAVITY; - int currentGravity = mTvPipBoundsState.getTvPipGravity(); int updatedGravity; - if (expanding) { - // save collapsed gravity - gravityToSave = mTvPipBoundsState.getTvPipGravity(); + // Save collapsed gravity. + mTvPipBoundsState.setTvPipPreviousCollapsedGravity(mTvPipBoundsState.getTvPipGravity()); if (mTvPipBoundsState.getTvFixedPipOrientation() == ORIENTATION_HORIZONTAL) { - updatedGravity = - Gravity.CENTER_HORIZONTAL | (currentGravity - & Gravity.VERTICAL_GRAVITY_MASK); + updatedGravity = Gravity.CENTER_HORIZONTAL | currentY; } else { - updatedGravity = - Gravity.CENTER_VERTICAL | (currentGravity - & Gravity.HORIZONTAL_GRAVITY_MASK); + updatedGravity = currentX | Gravity.CENTER_VERTICAL; } } else { - if (previousGravity != Gravity.NO_GRAVITY) { - // The pip hasn't been moved since expanding, - // go back to previous collapsed position. - updatedGravity = previousGravity; + // Collapse to the edge that the user moved to before. + if (mTvPipBoundsState.getTvFixedPipOrientation() == ORIENTATION_HORIZONTAL) { + updatedGravity = previousCollapsedX | currentY; } else { - if (mTvPipBoundsState.getTvFixedPipOrientation() == ORIENTATION_HORIZONTAL) { - updatedGravity = - Gravity.RIGHT | (currentGravity & Gravity.VERTICAL_GRAVITY_MASK); - } else { - updatedGravity = - Gravity.BOTTOM | (currentGravity & Gravity.HORIZONTAL_GRAVITY_MASK); - } + updatedGravity = currentX | previousCollapsedY; } } mTvPipBoundsState.setTvPipGravity(updatedGravity); - if (DEBUG) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: new gravity: %s", TAG, Gravity.toString(updatedGravity)); - } - - return gravityToSave; + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: new gravity: %s", TAG, Gravity.toString(updatedGravity)); } /** - * @return true if gravity changed + * @return true if the 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 + // Check if position change is valid. if (mTvPipBoundsState.isTvPipExpanded()) { - int mOrientation = mTvPipBoundsState.getTvFixedPipOrientation(); - if (mOrientation == ORIENTATION_VERTICAL + int fixedOrientation = mTvPipBoundsState.getTvFixedPipOrientation(); + if (fixedOrientation == ORIENTATION_VERTICAL && (keycode == KEYCODE_DPAD_UP || keycode == KEYCODE_DPAD_DOWN) - || mOrientation == ORIENTATION_HORIZONTAL + || fixedOrientation == ORIENTATION_HORIZONTAL && (keycode == KEYCODE_DPAD_RIGHT || keycode == KEYCODE_DPAD_LEFT)) { return false; } } - int currentGravity = mTvPipBoundsState.getTvPipGravity(); - int updatedGravity; - // First axis + int updatedX = mTvPipBoundsState.getTvPipGravity() & Gravity.HORIZONTAL_GRAVITY_MASK; + int updatedY = mTvPipBoundsState.getTvPipGravity() & Gravity.VERTICAL_GRAVITY_MASK; + switch (keycode) { case KEYCODE_DPAD_UP: - updatedGravity = Gravity.TOP; + updatedY = Gravity.TOP; break; case KEYCODE_DPAD_DOWN: - updatedGravity = Gravity.BOTTOM; + updatedY = Gravity.BOTTOM; break; case KEYCODE_DPAD_LEFT: - updatedGravity = Gravity.LEFT; + updatedX = Gravity.LEFT; break; case KEYCODE_DPAD_RIGHT: - updatedGravity = Gravity.RIGHT; + updatedX = Gravity.RIGHT; break; default: - updatedGravity = currentGravity; + // NOOP - unsupported keycode } - // Second axis - switch (keycode) { - case KEYCODE_DPAD_UP: - case KEYCODE_DPAD_DOWN: - if (mTvPipBoundsState.isTvPipExpanded()) { - updatedGravity |= Gravity.CENTER_HORIZONTAL; - } else { - updatedGravity |= (currentGravity & Gravity.HORIZONTAL_GRAVITY_MASK); - } - break; - case KEYCODE_DPAD_LEFT: - case KEYCODE_DPAD_RIGHT: - if (mTvPipBoundsState.isTvPipExpanded()) { - updatedGravity |= Gravity.CENTER_VERTICAL; - } else { - updatedGravity |= (currentGravity & Gravity.VERTICAL_GRAVITY_MASK); - } - break; - default: - break; - } + int updatedGravity = updatedX | updatedY; - if (updatedGravity != currentGravity) { + if (updatedGravity != mTvPipBoundsState.getTvPipGravity()) { 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: updateGravity, new gravity: %s", TAG, Gravity.toString(updatedGravity)); return true; } return false; @@ -360,29 +282,26 @@ 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 if (mTvPipBoundsState.getTvFixedPipOrientation() == ORIENTATION_HORIZONTAL) { expandedSize = mTvPipBoundsState.getTvExpandedSize(); } else { - int maxHeight = displayLayout.height() - (2 * mScreenEdgeInsets.y) + int maxHeight = displayLayout.height() + - (2 * mPipSizeSpecHandler.getScreenEdgeInsets().y) - pipDecorations.top - pipDecorations.bottom; 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); } } @@ -391,30 +310,25 @@ public class TvPipBoundsAlgorithm extends PipBoundsAlgorithm { if (mTvPipBoundsState.getTvFixedPipOrientation() == ORIENTATION_VERTICAL) { expandedSize = mTvPipBoundsState.getTvExpandedSize(); } else { - int maxWidth = displayLayout.width() - (2 * mScreenEdgeInsets.x) + int maxWidth = displayLayout.width() + - (2 * mPipSizeSpecHandler.getScreenEdgeInsets().x) - 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..8d4a38442ce5 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,22 +121,22 @@ 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); + if (mCurrentPlacementBounds != null) { + applyPlacementBounds(mCurrentPlacementBounds, animationDuration); + } schedulePinnedStackPlacement(placement, animationDuration); } } 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,30 +170,27 @@ 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); - } + applyPlacement(mPendingPlacement, mPendingStash, mPendingPlacementAnimationDuration); + mPendingStash = false; + mPendingPlacement = null; + } + } - if (mUnstashRunnable != null) { - // currently stashed, use stashed pos - applyPlacementBounds(mPendingPlacement.getBounds(), - mPendingPlacementAnimationDuration); - } else { - applyPlacementBounds(mPendingPlacement.getUnstashedBounds(), - mPendingPlacementAnimationDuration); - } + private void applyPlacement(@NonNull final Placement placement, boolean shouldStash, + int animationDuration) { + if (placement.getStashType() != STASH_TYPE_NONE && shouldStash) { + scheduleUnstashIfNeeded(placement); } - mPendingPlacement = null; + Rect bounds = + mUnstashRunnable != null ? placement.getBounds() : placement.getUnstashedBounds(); + applyPlacementBounds(bounds, animationDuration); } - void onPipDismissed() { + void reset() { mCurrentPlacementBounds = null; mPipTargetBounds = null; cancelScheduledPlacement(); @@ -227,10 +223,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/TvPipBoundsState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsState.java index ca22882187d8..e1737eccc6e1 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsState.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsState.java @@ -27,9 +27,12 @@ import android.content.pm.PackageManager; import android.graphics.Insets; import android.util.Size; import android.view.Gravity; +import android.view.View; import com.android.wm.shell.pip.PipBoundsAlgorithm; import com.android.wm.shell.pip.PipBoundsState; +import com.android.wm.shell.pip.PipDisplayLayoutState; +import com.android.wm.shell.pip.phone.PipSizeSpecHandler; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -52,24 +55,63 @@ public class TvPipBoundsState extends PipBoundsState { public @interface Orientation { } - public static final int DEFAULT_TV_GRAVITY = Gravity.BOTTOM | Gravity.RIGHT; + private final Context mContext; + + private int mDefaultGravity; + private int mTvPipGravity; + private int mPreviousCollapsedGravity; + private boolean mIsRtl; private final boolean mIsTvExpandedPipSupported; private boolean mIsTvPipExpanded; private boolean mTvPipManuallyCollapsed; private float mDesiredTvExpandedAspectRatio; - private @Orientation int mTvFixedPipOrientation; - private int mTvPipGravity; - private @Nullable Size mTvExpandedSize; - private @NonNull Insets mPipMenuPermanentDecorInsets = Insets.NONE; - private @NonNull Insets mPipMenuTemporaryDecorInsets = Insets.NONE; - - public TvPipBoundsState(@NonNull Context context) { - super(context); + @Orientation + private int mTvFixedPipOrientation; + @Nullable + private Size mTvExpandedSize; + @NonNull + private Insets mPipMenuPermanentDecorInsets = Insets.NONE; + @NonNull + private Insets mPipMenuTemporaryDecorInsets = Insets.NONE; + + public TvPipBoundsState(@NonNull Context context, + @NonNull PipSizeSpecHandler pipSizeSpecHandler, + @NonNull PipDisplayLayoutState pipDisplayLayoutState) { + super(context, pipSizeSpecHandler, pipDisplayLayoutState); + mContext = context; + updateDefaultGravity(); + mPreviousCollapsedGravity = mDefaultGravity; mIsTvExpandedPipSupported = context.getPackageManager().hasSystemFeature( PackageManager.FEATURE_EXPANDED_PICTURE_IN_PICTURE); } + public int getDefaultGravity() { + return mDefaultGravity; + } + + private void updateDefaultGravity() { + boolean isRtl = mContext.getResources().getConfiguration().getLayoutDirection() + == View.LAYOUT_DIRECTION_RTL; + mDefaultGravity = Gravity.BOTTOM | (isRtl ? Gravity.LEFT : Gravity.RIGHT); + + if (mIsRtl != isRtl) { + int prevGravityX = mPreviousCollapsedGravity & Gravity.HORIZONTAL_GRAVITY_MASK; + int prevGravityY = mPreviousCollapsedGravity & Gravity.VERTICAL_GRAVITY_MASK; + if ((prevGravityX & Gravity.RIGHT) == Gravity.RIGHT) { + mPreviousCollapsedGravity = Gravity.LEFT | prevGravityY; + } else if ((prevGravityX & Gravity.LEFT) == Gravity.LEFT) { + mPreviousCollapsedGravity = Gravity.RIGHT | prevGravityY; + } + } + mIsRtl = isRtl; + } + + @Override + public void onConfigurationChanged() { + updateDefaultGravity(); + } + /** * Initialize states when first entering PiP. */ @@ -86,7 +128,9 @@ public class TvPipBoundsState extends PipBoundsState { /** Resets the TV PiP state for a new activity. */ public void resetTvPipState() { mTvFixedPipOrientation = ORIENTATION_UNDETERMINED; - mTvPipGravity = DEFAULT_TV_GRAVITY; + mTvPipGravity = mDefaultGravity; + mPreviousCollapsedGravity = mDefaultGravity; + mTvPipManuallyCollapsed = false; } /** Set the tv expanded bounds of PiP */ @@ -101,16 +145,23 @@ public class TvPipBoundsState extends PipBoundsState { } /** Set the PiP aspect ratio for the expanded PiP (TV) that is desired by the app. */ - public void setDesiredTvExpandedAspectRatio(float aspectRatio, boolean override) { + public void setDesiredTvExpandedAspectRatio(float expandedAspectRatio, boolean override) { if (override || mTvFixedPipOrientation == ORIENTATION_UNDETERMINED) { - mDesiredTvExpandedAspectRatio = aspectRatio; resetTvPipState(); + mDesiredTvExpandedAspectRatio = expandedAspectRatio; + if (expandedAspectRatio != 0) { + if (expandedAspectRatio > 1) { + mTvFixedPipOrientation = ORIENTATION_HORIZONTAL; + } else { + mTvFixedPipOrientation = ORIENTATION_VERTICAL; + } + } return; } - if ((aspectRatio > 1 && mTvFixedPipOrientation == ORIENTATION_HORIZONTAL) - || (aspectRatio <= 1 && mTvFixedPipOrientation == ORIENTATION_VERTICAL) - || aspectRatio == 0) { - mDesiredTvExpandedAspectRatio = aspectRatio; + if ((expandedAspectRatio > 1 && mTvFixedPipOrientation == ORIENTATION_HORIZONTAL) + || (expandedAspectRatio <= 1 && mTvFixedPipOrientation == ORIENTATION_VERTICAL) + || expandedAspectRatio == 0) { + mDesiredTvExpandedAspectRatio = expandedAspectRatio; } } @@ -122,11 +173,6 @@ public class TvPipBoundsState extends PipBoundsState { return mDesiredTvExpandedAspectRatio; } - /** Sets the orientation the expanded TV PiP activity has been fixed to. */ - public void setTvFixedPipOrientation(@Orientation int orientation) { - mTvFixedPipOrientation = orientation; - } - /** Returns the fixed orientation of the expanded PiP on TV. */ @Orientation public int getTvFixedPipOrientation() { @@ -143,6 +189,14 @@ public class TvPipBoundsState extends PipBoundsState { return mTvPipGravity; } + public void setTvPipPreviousCollapsedGravity(int gravity) { + mPreviousCollapsedGravity = gravity; + } + + public int getTvPipPreviousCollapsedGravity() { + return mPreviousCollapsedGravity; + } + /** Sets whether the TV PiP is currently expanded. */ public void setTvPipExpanded(boolean expanded) { mIsTvPipExpanded = expanded; @@ -172,7 +226,8 @@ public class TvPipBoundsState extends PipBoundsState { mPipMenuPermanentDecorInsets = permanentInsets; } - public @NonNull Insets getPipMenuPermanentDecorInsets() { + @NonNull + public Insets getPipMenuPermanentDecorInsets() { return mPipMenuPermanentDecorInsets; } @@ -180,7 +235,8 @@ public class TvPipBoundsState extends PipBoundsState { mPipMenuTemporaryDecorInsets = temporaryDecorInsets; } - public @NonNull Insets getPipMenuTemporaryDecorInsets() { + @NonNull + public Insets getPipMenuTemporaryDecorInsets() { return mPipMenuTemporaryDecorInsets; } } 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..2f74fb636bc9 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 @@ -18,20 +18,27 @@ package com.android.wm.shell.pip.tv; import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; +import static android.view.KeyEvent.KEYCODE_DPAD_LEFT; +import static android.view.KeyEvent.KEYCODE_DPAD_RIGHT; import android.annotation.IntDef; import android.app.ActivityManager; import android.app.ActivityTaskManager; -import android.app.PendingIntent; import android.app.RemoteAction; import android.app.TaskInfo; +import android.content.BroadcastReceiver; import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Rect; +import android.os.Handler; 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; @@ -44,11 +51,16 @@ import com.android.wm.shell.pip.PinnedStackListenerForwarder; import com.android.wm.shell.pip.Pip; import com.android.wm.shell.pip.PipAnimationController; import com.android.wm.shell.pip.PipAppOpsListener; +import com.android.wm.shell.pip.PipDisplayLayoutState; import com.android.wm.shell.pip.PipMediaController; 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 +73,9 @@ import java.util.Set; */ public class TvPipController implements PipTransitionController.PipTransitionCallback, TvPipBoundsController.PipBoundsListener, TvPipMenuController.Delegate, - TvPipNotificationController.Delegate, DisplayController.OnDisplaysChangedListener { + 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 +85,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. @@ -91,33 +104,57 @@ public class TvPipController implements PipTransitionController.PipTransitionCal */ private static final int STATE_PIP_MENU = 2; + static final String ACTION_SHOW_PIP_MENU = + "com.android.wm.shell.pip.tv.notification.action.SHOW_PIP_MENU"; + static final String ACTION_CLOSE_PIP = + "com.android.wm.shell.pip.tv.notification.action.CLOSE_PIP"; + static final String ACTION_MOVE_PIP = + "com.android.wm.shell.pip.tv.notification.action.MOVE_PIP"; + static final String ACTION_TOGGLE_EXPANDED_PIP = + "com.android.wm.shell.pip.tv.notification.action.TOGGLE_EXPANDED_PIP"; + static final String ACTION_TO_FULLSCREEN = + "com.android.wm.shell.pip.tv.notification.action.FULLSCREEN"; + private final Context mContext; + private final ShellController mShellController; private final TvPipBoundsState mTvPipBoundsState; + private final PipDisplayLayoutState mPipDisplayLayoutState; private final TvPipBoundsAlgorithm mTvPipBoundsAlgorithm; private final TvPipBoundsController mTvPipBoundsController; private final PipAppOpsListener mAppOpsListener; private final PipTaskOrganizer mPipTaskOrganizer; private final PipMediaController mPipMediaController; + private final TvPipActionsProvider mTvPipActionsProvider; 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 Handler mMainHandler; // For registering the broadcast receiver private final TvPipImpl mImpl = new TvPipImpl(); - private @State int mState = STATE_NO_PIP; - private int mPreviousGravity = TvPipBoundsState.DEFAULT_TV_GRAVITY; + private final ActionBroadcastReceiver mActionBroadcastReceiver; + + @State + private int mState = STATE_NO_PIP; private int mPinnedTaskId = NONEXISTENT_TASK_ID; - private RemoteAction mCloseAction; // How long the shell will wait for the app to close the PiP if a custom action is set. private int mPipForceCloseDelay; private int mResizeAnimationDuration; - private int mEduTextWindowExitAnimationDurationMs; + private int mEduTextWindowExitAnimationDuration; public static Pip create( Context context, + ShellInit shellInit, + ShellController shellController, TvPipBoundsState tvPipBoundsState, + PipDisplayLayoutState pipDisplayLayoutState, TvPipBoundsAlgorithm tvPipBoundsAlgorithm, TvPipBoundsController tvPipBoundsController, PipAppOpsListener pipAppOpsListener, @@ -130,10 +167,14 @@ public class TvPipController implements PipTransitionController.PipTransitionCal PipParamsChangedForwarder pipParamsChangedForwarder, DisplayController displayController, WindowManagerShellWrapper wmShell, + Handler mainHandler, ShellExecutor mainExecutor) { return new TvPipController( context, + shellInit, + shellController, tvPipBoundsState, + pipDisplayLayoutState, tvPipBoundsAlgorithm, tvPipBoundsController, pipAppOpsListener, @@ -146,12 +187,16 @@ public class TvPipController implements PipTransitionController.PipTransitionCal pipParamsChangedForwarder, displayController, wmShell, + mainHandler, mainExecutor).mImpl; } private TvPipController( Context context, + ShellInit shellInit, + ShellController shellController, TvPipBoundsState tvPipBoundsState, + PipDisplayLayoutState pipDisplayLayoutState, TvPipBoundsAlgorithm tvPipBoundsAlgorithm, TvPipBoundsController tvPipBoundsController, PipAppOpsListener pipAppOpsListener, @@ -163,55 +208,107 @@ public class TvPipController implements PipTransitionController.PipTransitionCal TaskStackListenerImpl taskStackListener, PipParamsChangedForwarder pipParamsChangedForwarder, DisplayController displayController, - WindowManagerShellWrapper wmShell, + WindowManagerShellWrapper wmShellWrapper, + Handler mainHandler, ShellExecutor mainExecutor) { mContext = context; + mMainHandler = mainHandler; mMainExecutor = mainExecutor; + mShellController = shellController; + mDisplayController = displayController; + + DisplayLayout layout = new DisplayLayout(context, context.getDisplay()); mTvPipBoundsState = tvPipBoundsState; - mTvPipBoundsState.setDisplayId(context.getDisplayId()); - mTvPipBoundsState.setDisplayLayout(new DisplayLayout(context, context.getDisplay())); + + mPipDisplayLayoutState = pipDisplayLayoutState; + mPipDisplayLayoutState.setDisplayLayout(layout); + mPipDisplayLayoutState.setDisplayId(context.getDisplayId()); + mTvPipBoundsAlgorithm = tvPipBoundsAlgorithm; mTvPipBoundsController = tvPipBoundsController; mTvPipBoundsController.setListener(this); mPipMediaController = pipMediaController; + mTvPipActionsProvider = new TvPipActionsProvider(context, pipMediaController, + this::executeAction); mPipNotificationController = pipNotificationController; - mPipNotificationController.setDelegate(this); + mPipNotificationController.setTvPipActionsProvider(mTvPipActionsProvider); mTvPipMenuController = tvPipMenuController; mTvPipMenuController.setDelegate(this); + mTvPipMenuController.setTvPipActionsProvider(mTvPipActionsProvider); + + mActionBroadcastReceiver = new ActionBroadcastReceiver(); 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); + + reloadResources(); - loadConfigurations(); + registerPipParamsChangedListener(mPipParamsChangedForwarder); + registerTaskStackListenerCallback(mTaskStackListener); + registerWmShellPinnedStackListener(mWmShellWrapper); + registerSessionListenerForCurrentUser(); + mDisplayController.addDisplayWindowListener(this); - registerPipParamsChangedListener(pipParamsChangedForwarder); - registerTaskStackListenerCallback(taskStackListener); - registerWmShellPinnedStackListener(wmShell); - displayController.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(); + } - if (isPipShown()) { - if (DEBUG) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: > closing Pip.", TAG); - } - closePip(); - } + @Override + public void onConfigurationChanged(Configuration newConfig) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: onConfigurationChanged(), state=%s", TAG, stateToName(mState)); + + int previousDefaultGravityX = mTvPipBoundsState.getDefaultGravity() + & Gravity.HORIZONTAL_GRAVITY_MASK; - loadConfigurations(); - mPipNotificationController.onConfigurationChanged(mContext); + reloadResources(); + + mPipNotificationController.onConfigurationChanged(); mTvPipBoundsAlgorithm.onConfigurationChanged(mContext); + mTvPipBoundsState.onConfigurationChanged(); + + int defaultGravityX = mTvPipBoundsState.getDefaultGravity() + & Gravity.HORIZONTAL_GRAVITY_MASK; + if (isPipShown() && previousDefaultGravityX != defaultGravityX) { + movePipToOppositeSide(); + } + } + + private void reloadResources() { + final Resources res = mContext.getResources(); + mResizeAnimationDuration = res.getInteger(R.integer.config_pipResizeAnimationDuration); + mPipForceCloseDelay = res.getInteger(R.integer.config_pipForceCloseDelay); + mEduTextWindowExitAnimationDuration = + res.getInteger(R.integer.pip_edu_text_window_exit_animation_duration); + } + + private void movePipToOppositeSide() { + ProtoLog.i(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: movePipToOppositeSide", TAG); + if ((mTvPipBoundsState.getTvPipGravity() & Gravity.RIGHT) == Gravity.RIGHT) { + movePip(KEYCODE_DPAD_LEFT); + } else if ((mTvPipBoundsState.getTvPipGravity() & Gravity.LEFT) == Gravity.LEFT) { + movePip(KEYCODE_DPAD_RIGHT); + } } /** @@ -225,33 +322,32 @@ public class TvPipController implements PipTransitionController.PipTransitionCal * Starts the process if bringing up the Pip menu if by issuing a command to move Pip * task/window to the "Menu" position. We'll show the actual Menu UI (eg. actions) once the Pip * task/window is properly positioned in {@link #onPipTransitionFinished(int)}. + * + * @param moveMenu If true, show the moveMenu, otherwise show the regular menu. */ - @Override - public void showPictureInPictureMenu() { - if (DEBUG) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: showPictureInPictureMenu(), state=%s", TAG, stateToName(mState)); - } + private void showPictureInPictureMenu(boolean moveMenu) { + 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; } setState(STATE_PIP_MENU); - mTvPipMenuController.showMenu(); + if (moveMenu) { + mTvPipMenuController.showMovementMenu(); + } else { + mTvPipMenuController.showMenu(); + } updatePinnedStackBounds(); } @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(); } @@ -264,69 +360,40 @@ public class TvPipController implements PipTransitionController.PipTransitionCal /** * Opens the "Pip-ed" Activity fullscreen. */ - @Override - public void movePipToFullscreen() { - if (DEBUG) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: movePipToFullscreen(), state=%s", TAG, stateToName(mState)); - } + private void movePipToFullscreen() { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: movePipToFullscreen(), state=%s", TAG, stateToName(mState)); mPipTaskOrganizer.exitPip(mResizeAnimationDuration, false /* requestEnterSplit */); onPipDisappeared(); } - @Override - public void togglePipExpansion() { - if (DEBUG) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: togglePipExpansion()", TAG); - } + private void togglePipExpansion() { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: togglePipExpansion()", TAG); boolean expanding = !mTvPipBoundsState.isTvPipExpanded(); - int saveGravity = mTvPipBoundsAlgorithm - .updateGravityOnExpandToggled(mPreviousGravity, expanding); - if (saveGravity != Gravity.NO_GRAVITY) { - mPreviousGravity = saveGravity; - } + mTvPipBoundsAlgorithm.updateGravityOnExpansionToggled(expanding); mTvPipBoundsState.setTvPipManuallyCollapsed(!expanding); mTvPipBoundsState.setTvPipExpanded(expanding); - mPipNotificationController.updateExpansionState(); updatePinnedStackBounds(); } @Override - public void enterPipMovementMenu() { - setState(STATE_PIP_MENU); - mTvPipMenuController.showMovementMenuOnly(); - } - - @Override public void movePip(int keycode) { if (mTvPipBoundsAlgorithm.updateGravity(keycode)) { mTvPipMenuController.updateGravity(mTvPipBoundsState.getTvPipGravity()); - 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); } } @Override - public int getPipGravity() { - return mTvPipBoundsState.getTvPipGravity(); - } - - public int getOrientation() { - return mTvPipBoundsState.getTvFixedPipOrientation(); - } - - @Override public void onKeepClearAreasChanged(int displayId, Set<Rect> restricted, Set<Rect> unrestricted) { - if (mTvPipBoundsState.getDisplayId() == displayId) { + if (mPipDisplayLayoutState.getDisplayId() == displayId) { boolean unrestrictedAreasChanged = !Objects.equals(unrestricted, mTvPipBoundsState.getUnrestrictedKeepClearAreas()); mTvPipBoundsState.setKeepClearAreas(restricted, unrestricted); @@ -352,34 +419,25 @@ public class TvPipController implements PipTransitionController.PipTransitionCal } @Override - public void onPipTargetBoundsChange(Rect newTargetBounds, int animationDuration) { - mPipTaskOrganizer.scheduleAnimateResizePip(newTargetBounds, - animationDuration, rect -> mTvPipMenuController.updateExpansionState()); - mTvPipMenuController.onPipTransitionStarted(newTargetBounds); + public void onPipTargetBoundsChange(Rect targetBounds, int animationDuration) { + mPipTaskOrganizer.scheduleAnimateResizePip(targetBounds, + animationDuration, null); + mTvPipMenuController.onPipTransitionToTargetBoundsStarted(targetBounds); } /** * Closes Pip window. */ - @Override public void closePip() { - if (DEBUG) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: closePip(), state=%s, loseAction=%s", TAG, stateToName(mState), - mCloseAction); - } + closeCurrentPiP(mPinnedTaskId); + } - if (mCloseAction != null) { - try { - mCloseAction.getActionIntent().send(); - } catch (PendingIntent.CanceledException e) { - ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: Failed to send close action, %s", TAG, e); - } - mMainExecutor.executeDelayed(() -> closeCurrentPiP(mPinnedTaskId), mPipForceCloseDelay); - } else { - closeCurrentPiP(mPinnedTaskId); - } + /** + * Force close the current PiP after some time in case the custom action hasn't done it by + * itself. + */ + public void customClosePip() { + mMainExecutor.executeDelayed(() -> closeCurrentPiP(mPinnedTaskId), mPipForceCloseDelay); } private void closeCurrentPiP(int pinnedTaskId) { @@ -388,37 +446,22 @@ public class TvPipController implements PipTransitionController.PipTransitionCal "%s: PiP has already been closed by custom close action", TAG); return; } - removeTask(mPinnedTaskId); + mPipTaskOrganizer.removePip(); onPipDisappeared(); } @Override public void closeEduText() { - updatePinnedStackBounds(mEduTextWindowExitAnimationDurationMs, false); + updatePinnedStackBounds(mEduTextWindowExitAnimationDuration, false); } private void registerSessionListenerForCurrentUser() { mPipMediaController.registerSessionListenerForCurrentUser(); } - private void checkIfPinnedTaskAppeared() { - final TaskInfo pinnedTask = getPinnedTaskInfo(); - if (DEBUG) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: checkIfPinnedTaskAppeared(), task=%s", TAG, pinnedTask); - } - if (pinnedTask == null || pinnedTask.topActivity == null) return; - mPinnedTaskId = pinnedTask.taskId; - - mPipMediaController.onActivityPinned(); - mPipNotificationController.show(pinnedTask.topActivity.getPackageName()); - } - 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,71 +471,79 @@ 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(); + mActionBroadcastReceiver.unregister(); + mTvPipMenuController.closeMenu(); mTvPipBoundsState.resetTvPipState(); - mTvPipBoundsController.onPipDismissed(); + mTvPipBoundsController.reset(); setState(STATE_NO_PIP); mPinnedTaskId = NONEXISTENT_TASK_ID; } @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)); + public void onPipTransitionStarted(int direction, Rect currentPipBounds) { + final boolean enterPipTransition = PipAnimationController.isInPipDirection(direction); + if (enterPipTransition && mState == STATE_NO_PIP) { + // Set the initial ability to expand the PiP when entering PiP. + updateExpansionState(); } - mTvPipMenuController.notifyPipAnimating(true); + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: onPipTransition_Started(), state=%s, direction=%d", + TAG, stateToName(mState), direction); } @Override public void onPipTransitionCanceled(int direction) { - if (DEBUG) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: onPipTransition_Canceled(), state=%s", TAG, stateToName(mState)); - } - mTvPipMenuController.notifyPipAnimating(false); + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: onPipTransition_Canceled(), state=%s", TAG, stateToName(mState)); + mTvPipMenuController.onPipTransitionFinished( + PipAnimationController.isInPipDirection(direction)); + mTvPipActionsProvider.onPipExpansionToggled(mTvPipBoundsState.isTvPipExpanded()); } @Override public void onPipTransitionFinished(int direction) { - if (PipAnimationController.isInPipDirection(direction) && mState == STATE_NO_PIP) { + final boolean enterPipTransition = PipAnimationController.isInPipDirection(direction); + if (enterPipTransition && 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)); - } - mTvPipMenuController.notifyPipAnimating(false); + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: onPipTransition_Finished(), state=%s, direction=%d", + TAG, stateToName(mState), direction); + mTvPipMenuController.onPipTransitionFinished(enterPipTransition); + mTvPipActionsProvider.onPipExpansionToggled(mTvPipBoundsState.isTvPipExpanded()); } - 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)); - } - mState = state; + private void updateExpansionState() { + mTvPipActionsProvider.updateExpansionEnabled(mTvPipBoundsState.isTvExpandedPipSupported() + && mTvPipBoundsState.getDesiredTvExpandedAspectRatio() != 0); } - private void loadConfigurations() { - final Resources res = mContext.getResources(); - mResizeAnimationDuration = res.getInteger(R.integer.config_pipResizeAnimationDuration); - mPipForceCloseDelay = res.getInteger(R.integer.config_pipForceCloseDelay); - mEduTextWindowExitAnimationDurationMs = - res.getInteger(R.integer.pip_edu_text_window_exit_animation_duration_ms); + private void setState(@State int state) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: setState(), state=%s, prev=%s", + TAG, stateToName(state), stateToName(mState)); + mState = state; } private void registerTaskStackListenerCallback(TaskStackListenerImpl taskStackListener) { taskStackListener.addListener(new TaskStackListenerCallback() { @Override public void onActivityPinned(String packageName, int userId, int taskId, int stackId) { - checkIfPinnedTaskAppeared(); + final TaskInfo pinnedTask = getPinnedTaskInfo(); + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: onActivityPinned(), task=%s", TAG, pinnedTask); + if (pinnedTask == null || pinnedTask.topActivity == null) return; + mPinnedTaskId = pinnedTask.taskId; + + mPipMediaController.onActivityPinned(); + mActionBroadcastReceiver.register(); + mPipNotificationController.show(pinnedTask.topActivity.getPackageName()); + mTvPipBoundsController.reset(); mAppOpsListener.onActivityPinned(packageName); } @@ -510,11 +561,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,16 +577,15 @@ 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; + mTvPipActionsProvider.setAppActions(actions, closeAction); } @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,10 +596,10 @@ 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(); + updateExpansionState(); // 1) PiP is expanded and only aspect ratio changed, but wasn't disabled // --> update bounds, but don't toggle @@ -564,11 +611,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal // 2) PiP is expanded, but expanded PiP was disabled // --> collapse PiP if (mTvPipBoundsState.isTvPipExpanded() && ratio == 0) { - int saveGravity = mTvPipBoundsAlgorithm - .updateGravityOnExpandToggled(mPreviousGravity, false); - if (saveGravity != Gravity.NO_GRAVITY) { - mPreviousGravity = saveGravity; - } + mTvPipBoundsAlgorithm.updateGravityOnExpansionToggled(/* expanding= */ false); mTvPipBoundsState.setTvPipExpanded(false); updatePinnedStackBounds(); } @@ -578,11 +621,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal if (!mTvPipBoundsState.isTvPipExpanded() && ratio != 0 && !mTvPipBoundsState.isTvPipManuallyCollapsed()) { mTvPipBoundsAlgorithm.updateExpandedPipSize(); - int saveGravity = mTvPipBoundsAlgorithm - .updateGravityOnExpandToggled(mPreviousGravity, true); - if (saveGravity != Gravity.NO_GRAVITY) { - mPreviousGravity = saveGravity; - } + mTvPipBoundsAlgorithm.updateGravityOnExpansionToggled(/* expanding= */ true); mTvPipBoundsState.setTvPipExpanded(true); updatePinnedStackBounds(); } @@ -595,11 +634,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 +658,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, @@ -640,19 +673,6 @@ 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); - } - try { - ActivityTaskManager.getService().removeTask(taskId); - } catch (Exception e) { - ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: Atm.removeTask() failed, %s", TAG, e); - } - } - private static String stateToName(@State int state) { switch (state) { case STATE_NO_PIP: @@ -667,19 +687,91 @@ public class TvPipController implements PipTransitionController.PipTransitionCal } } - private class TvPipImpl implements Pip { - @Override - public void onConfigurationChanged(Configuration newConfig) { - mMainExecutor.execute(() -> { - TvPipController.this.onConfigurationChanged(newConfig); - }); + private void executeAction(@TvPipAction.ActionType int actionType) { + switch (actionType) { + case TvPipAction.ACTION_FULLSCREEN: + movePipToFullscreen(); + break; + case TvPipAction.ACTION_CLOSE: + closePip(); + break; + case TvPipAction.ACTION_MOVE: + showPictureInPictureMenu(/* moveMenu= */ true); + break; + case TvPipAction.ACTION_CUSTOM_CLOSE: + customClosePip(); + break; + case TvPipAction.ACTION_EXPAND_COLLAPSE: + togglePipExpansion(); + break; + default: + // NOOP + break; + } + } + + private class ActionBroadcastReceiver extends BroadcastReceiver { + private static final String SYSTEMUI_PERMISSION = "com.android.systemui.permission.SELF"; + + final IntentFilter mIntentFilter; + + { + mIntentFilter = new IntentFilter(); + mIntentFilter.addAction(ACTION_CLOSE_PIP); + mIntentFilter.addAction(ACTION_SHOW_PIP_MENU); + mIntentFilter.addAction(ACTION_MOVE_PIP); + mIntentFilter.addAction(ACTION_TOGGLE_EXPANDED_PIP); + mIntentFilter.addAction(ACTION_TO_FULLSCREEN); + } + + boolean mRegistered = false; + + void register() { + if (mRegistered) return; + + mContext.registerReceiverForAllUsers(this, mIntentFilter, SYSTEMUI_PERMISSION, + mMainHandler, Context.RECEIVER_NOT_EXPORTED); + mRegistered = true; + } + + void unregister() { + if (!mRegistered) return; + + mContext.unregisterReceiver(this); + mRegistered = false; } @Override - public void registerSessionListenerForCurrentUser() { - mMainExecutor.execute(() -> { - TvPipController.this.registerSessionListenerForCurrentUser(); - }); + public void onReceive(Context context, Intent intent) { + final String action = intent.getAction(); + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: on(Broadcast)Receive(), action=%s", TAG, action); + + if (ACTION_SHOW_PIP_MENU.equals(action)) { + showPictureInPictureMenu(/* moveMenu= */ false); + } else { + executeAction(getCorrespondingActionType(action)); + } + } + + @TvPipAction.ActionType + private int getCorrespondingActionType(String broadcast) { + if (ACTION_CLOSE_PIP.equals(broadcast)) { + return TvPipAction.ACTION_CLOSE; + } else if (ACTION_MOVE_PIP.equals(broadcast)) { + return TvPipAction.ACTION_MOVE; + } else if (ACTION_TOGGLE_EXPANDED_PIP.equals(broadcast)) { + return TvPipAction.ACTION_EXPAND_COLLAPSE; + } else if (ACTION_TO_FULLSCREEN.equals(broadcast)) { + return TvPipAction.ACTION_FULLSCREEN; + } + + // Default: handle it like an action we don't know the content of. + return TvPipAction.ACTION_CUSTOM; } } + + private class TvPipImpl implements Pip { + // Not used + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipCustomAction.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipCustomAction.java new file mode 100644 index 000000000000..bca27a5c6636 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipCustomAction.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.wm.shell.pip.tv; + +import static android.app.Notification.Action.SEMANTIC_ACTION_DELETE; +import static android.app.Notification.Action.SEMANTIC_ACTION_NONE; + +import android.annotation.NonNull; +import android.app.Notification; +import android.app.PendingIntent; +import android.app.RemoteAction; +import android.content.Context; +import android.os.Bundle; +import android.os.Handler; + +import com.android.internal.protolog.common.ProtoLog; +import com.android.wm.shell.common.TvWindowMenuActionButton; +import com.android.wm.shell.protolog.ShellProtoLogGroup; + +import java.util.List; +import java.util.Objects; + +/** + * A TvPipAction for actions that the app provides via {@link + * android.app.PictureInPictureParams.Builder#setCloseAction(RemoteAction)} or {@link + * android.app.PictureInPictureParams.Builder#setActions(List)}. + */ +public class TvPipCustomAction extends TvPipAction { + private static final String TAG = TvPipCustomAction.class.getSimpleName(); + + private final RemoteAction mRemoteAction; + + TvPipCustomAction(@ActionType int actionType, @NonNull RemoteAction remoteAction, + SystemActionsHandler systemActionsHandler) { + super(actionType, systemActionsHandler); + Objects.requireNonNull(remoteAction); + mRemoteAction = remoteAction; + } + + void populateButton(@NonNull TvWindowMenuActionButton button, Handler mainHandler) { + if (button == null || mainHandler == null) return; + if (mRemoteAction.getContentDescription().length() > 0) { + button.setTextAndDescription(mRemoteAction.getContentDescription()); + } else { + button.setTextAndDescription(mRemoteAction.getTitle()); + } + button.setImageIconAsync(mRemoteAction.getIcon(), mainHandler); + button.setEnabled(isCloseAction() || mRemoteAction.isEnabled()); + button.setIsCustomCloseAction(isCloseAction()); + } + + PendingIntent getPendingIntent() { + return mRemoteAction.getActionIntent(); + } + + void executeAction() { + super.executeAction(); + try { + mRemoteAction.getActionIntent().send(); + } catch (PendingIntent.CanceledException e) { + ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: Failed to send action, %s", TAG, e); + } + } + + @Override + Notification.Action toNotificationAction(Context context) { + Notification.Action.Builder builder = new Notification.Action.Builder( + mRemoteAction.getIcon(), + mRemoteAction.getTitle(), + mRemoteAction.getActionIntent()); + Bundle extras = new Bundle(); + extras.putCharSequence(Notification.EXTRA_PICTURE_CONTENT_DESCRIPTION, + mRemoteAction.getContentDescription()); + extras.putBoolean(Notification.EXTRA_CONTAINS_CUSTOM_VIEW, true); + builder.addExtras(extras); + + builder.setSemanticAction(isCloseAction() + ? SEMANTIC_ACTION_DELETE : SEMANTIC_ACTION_NONE); + builder.setContextual(true); + return builder.build(); + } + +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipKeepClearAlgorithm.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipKeepClearAlgorithm.kt index 1e54436ebce9..a94bd6ec1040 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipKeepClearAlgorithm.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipKeepClearAlgorithm.kt @@ -284,8 +284,10 @@ class TvPipKeepClearAlgorithm() { ): Rect? { val movementBounds = transformedMovementBounds val candidateEdgeRects = mutableListOf<Rect>() + val maxRestrictedXDistanceFraction = + if (isPipAnchoredToCorner()) maxRestrictedDistanceFraction else 0.0 val minRestrictedLeft = - pipAnchorBounds.right - screenSize.width * maxRestrictedDistanceFraction + pipAnchorBounds.right - screenSize.width * maxRestrictedXDistanceFraction candidateEdgeRects.add( movementBounds.offsetCopy(movementBounds.width() + pipAreaPadding, 0) @@ -296,7 +298,6 @@ class TvPipKeepClearAlgorithm() { // throw out edges that are too close to the left screen edge to fit the PiP val minLeft = movementBounds.left + pipAnchorBounds.width() candidateEdgeRects.retainAll { it.left - pipAreaPadding > minLeft } - candidateEdgeRects.sortBy { -it.left } val maxRestrictedDY = (screenSize.height * maxRestrictedDistanceFraction).roundToInt() @@ -335,8 +336,7 @@ class TvPipKeepClearAlgorithm() { } } - candidateBounds.sortBy { candidateCost(it, pipAnchorBounds) } - return candidateBounds.firstOrNull() + return candidateBounds.minByOrNull { candidateCost(it, pipAnchorBounds) } } private fun getNearbyStashedPosition(bounds: Rect, keepClearAreas: Set<Rect>): Rect { 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..be1f800b9d2e 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 @@ -18,79 +18,82 @@ package com.android.wm.shell.pip.tv; import static android.view.WindowManager.SHELL_ROOT_LAYER_PIP; -import android.app.ActivityManager; +import android.annotation.IntDef; import android.app.RemoteAction; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.graphics.Insets; -import android.graphics.Matrix; import android.graphics.Rect; -import android.graphics.RectF; import android.os.Handler; -import android.view.LayoutInflater; import android.view.SurfaceControl; -import android.view.SyncRtSurfaceTransactionApplier; import android.view.View; import android.view.ViewRootImpl; import android.view.WindowManagerGlobal; +import android.window.SurfaceSyncGroup; import androidx.annotation.Nullable; +import com.android.internal.annotations.VisibleForTesting; 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.pip.PipMediaController; import com.android.wm.shell.pip.PipMenuController; import com.android.wm.shell.protolog.ShellProtoLogGroup; -import java.util.ArrayList; import java.util.List; -import java.util.Objects; /** * Manages the visibility of the PiP Menu as user interacts with PiP. */ 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; private final SystemWindows mSystemWindows; private final TvPipBoundsState mTvPipBoundsState; private final Handler mMainHandler; - private final int mPipMenuBorderWidth; - private final int mPipEduTextShowDurationMs; - private final int mPipEduTextHeight; + private TvPipActionsProvider mTvPipActionsProvider; private Delegate mDelegate; private SurfaceControl mLeash; private TvPipMenuView mPipMenuView; - private View mPipBackgroundView; + private TvPipBackgroundView mPipBackgroundView; + private boolean mMenuIsFocused; - // User can actively move the PiP via the DPAD. - private boolean mInMoveMode; - // Used when only showing the move menu since we want to close the menu completely when - // exiting the move menu instead of showing the regular button menu. - private boolean mCloseAfterExitMoveMenu; + @TvPipMenuMode + private int mCurrentMenuMode = MODE_NO_MENU; + @TvPipMenuMode + private int mPrevMenuMode = MODE_NO_MENU; - private final List<RemoteAction> mMediaActions = new ArrayList<>(); - private final List<RemoteAction> mAppActions = new ArrayList<>(); - private RemoteAction mCloseAction; + @IntDef(prefix = { "MODE_" }, value = { + MODE_NO_MENU, + MODE_MOVE_MENU, + MODE_ALL_ACTIONS_MENU, + }) + public @interface TvPipMenuMode {} - private SyncRtSurfaceTransactionApplier mApplier; - private SyncRtSurfaceTransactionApplier mBackgroundApplier; - RectF mTmpSourceRectF = new RectF(); - RectF mTmpDestinationRectF = new RectF(); - Matrix mMoveTransform = new Matrix(); + /** + * In this mode the PiP menu is not focused and no user controls are displayed. + */ + static final int MODE_NO_MENU = 0; - private final Runnable mCloseEduTextRunnable = this::closeEduText; + /** + * In this mode the PiP menu is focused and the user can use the DPAD controls to move the PiP + * to a different position on the screen. We draw arrows in all possible movement directions. + */ + static final int MODE_MOVE_MENU = 1; + + /** + * In this mode the PiP menu is focused and we display an array of actions that the user can + * select. See {@link TvPipActionsProvider} for the types of available actions. + */ + static final int MODE_ALL_ACTIONS_MENU = 2; public TvPipMenuController(Context context, TvPipBoundsState tvPipBoundsState, - SystemWindows systemWindows, PipMediaController pipMediaController, - Handler mainHandler) { + SystemWindows systemWindows, Handler mainHandler) { mContext = context; mTvPipBoundsState = tvPipBoundsState; mSystemWindows = systemWindows; @@ -107,22 +110,11 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis context.registerReceiverForAllUsers(closeSystemDialogsBroadcastReceiver, new IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS), null /* permission */, mainHandler, Context.RECEIVER_EXPORTED); - - pipMediaController.addActionListener(this::onMediaActionsChanged); - - mPipEduTextShowDurationMs = context.getResources() - .getInteger(R.integer.pip_edu_text_show_duration_ms); - mPipEduTextHeight = context.getResources() - .getDimensionPixelSize(R.dimen.pip_menu_edu_text_view_height); - mPipMenuBorderWidth = context.getResources() - .getDimensionPixelSize(R.dimen.pip_menu_border_width); } 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."); @@ -134,6 +126,10 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis mDelegate = delegate; } + void setTvPipActionsProvider(TvPipActionsProvider tvPipActionsProvider) { + mTvPipActionsProvider = tvPipActionsProvider; + } + @Override public void attach(SurfaceControl leash) { if (mDelegate == null) { @@ -145,10 +141,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(); @@ -157,117 +151,93 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis attachPipBackgroundView(); attachPipMenuView(); - mTvPipBoundsState.setPipMenuPermanentDecorInsets(Insets.of(-mPipMenuBorderWidth, - -mPipMenuBorderWidth, -mPipMenuBorderWidth, -mPipMenuBorderWidth)); - mTvPipBoundsState.setPipMenuTemporaryDecorInsets(Insets.of(0, 0, 0, -mPipEduTextHeight)); - mMainHandler.postDelayed(mCloseEduTextRunnable, mPipEduTextShowDurationMs); + int pipEduTextHeight = mContext.getResources() + .getDimensionPixelSize(R.dimen.pip_menu_edu_text_view_height); + int pipMenuBorderWidth = mContext.getResources() + .getDimensionPixelSize(R.dimen.pip_menu_border_width); + mTvPipBoundsState.setPipMenuPermanentDecorInsets(Insets.of(-pipMenuBorderWidth, + -pipMenuBorderWidth, -pipMenuBorderWidth, -pipMenuBorderWidth)); + mTvPipBoundsState.setPipMenuTemporaryDecorInsets(Insets.of(0, 0, 0, -pipEduTextHeight)); } private void attachPipMenuView() { - mPipMenuView = new TvPipMenuView(mContext); - mPipMenuView.setListener(this); + if (mTvPipActionsProvider == null) { + ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: Actions provider is not set", TAG); + return; + } + mPipMenuView = createTvPipMenuView(); setUpViewSurfaceZOrder(mPipMenuView, 1); addPipMenuViewToSystemWindows(mPipMenuView, MENU_WINDOW_TITLE); - maybeUpdateMenuViewActions(); + } + + @VisibleForTesting + TvPipMenuView createTvPipMenuView() { + return new TvPipMenuView(mContext, mMainHandler, this, mTvPipActionsProvider); } private void attachPipBackgroundView() { - mPipBackgroundView = LayoutInflater.from(mContext) - .inflate(R.layout.tv_pip_menu_background, null); + mPipBackgroundView = createTvPipBackgroundView(); setUpViewSurfaceZOrder(mPipBackgroundView, -1); addPipMenuViewToSystemWindows(mPipBackgroundView, BACKGROUND_WINDOW_TITLE); } + @VisibleForTesting + TvPipBackgroundView createTvPipBackgroundView() { + return new TvPipBackgroundView(mContext); + } + 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); - } - - void notifyPipAnimating(boolean animating) { - mPipMenuView.setEduTextActive(!animating); - if (!animating) { - mPipMenuView.onPipTransitionFinished(); - } + mSystemWindows.addView(v, getPipMenuLayoutParams(mContext, title, 0 /* width */, + 0 /* height */), 0 /* displayId */, SHELL_ROOT_LAYER_PIP); + } + + void onPipTransitionFinished(boolean enterTransition) { + // There is a race between when this is called and when the last frame of the pip transition + // is drawn. To ensure that view updates are applied only when the animation has fully drawn + // and the menu view has been fully remeasured and relaid out, we add a small delay here by + // posting on the handler. + mMainHandler.post(() -> { + if (mPipMenuView != null) { + mPipMenuView.onPipTransitionFinished(enterTransition); + } + }); } - void showMovementMenuOnly() { - if (DEBUG) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: showMovementMenuOnly()", TAG); - } - setInMoveMode(true); - mCloseAfterExitMoveMenu = true; - showMenuInternal(); + void showMovementMenu() { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: showMovementMenu()", TAG); + switchToMenuMode(MODE_MOVE_MENU); } @Override public void showMenu() { - if (DEBUG) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: showMenu()", TAG); - } - setInMoveMode(false); - mCloseAfterExitMoveMenu = false; - showMenuInternal(); - } - - private void showMenuInternal() { - if (mPipMenuView == null) { - return; - } - maybeCloseEduText(); - maybeUpdateMenuViewActions(); - updateExpansionState(); - - grantPipMenuFocus(true); - if (mInMoveMode) { - mPipMenuView.showMoveMenu(mDelegate.getPipGravity()); - } else { - mPipMenuView.showButtonsMenu(); - } - mPipMenuView.updateBounds(mTvPipBoundsState.getBounds()); + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: showMenu()", TAG); + switchToMenuMode(MODE_ALL_ACTIONS_MENU, true); } - void onPipTransitionStarted(Rect finishBounds) { + void onPipTransitionToTargetBoundsStarted(Rect targetBounds) { if (mPipMenuView != null) { - mPipMenuView.onPipTransitionStarted(finishBounds); - } - } - - private void maybeCloseEduText() { - if (mMainHandler.hasCallbacks(mCloseEduTextRunnable)) { - mMainHandler.removeCallbacks(mCloseEduTextRunnable); - mCloseEduTextRunnable.run(); + mPipMenuView.onPipTransitionToTargetBoundsStarted(targetBounds); } } - private void closeEduText() { - mTvPipBoundsState.setPipMenuTemporaryDecorInsets(Insets.NONE); - mPipMenuView.hideEduText(); - mDelegate.closeEduText(); - } - void updateGravity(int gravity) { - mPipMenuView.showMovementHints(gravity); - } - - void updateExpansionState() { - mPipMenuView.setExpandedModeEnabled(mTvPipBoundsState.isTvExpandedPipSupported() - && mTvPipBoundsState.getDesiredTvExpandedAspectRatio() != 0); - mPipMenuView.setIsExpanded(mTvPipBoundsState.isTvPipExpanded()); + mPipMenuView.setPipGravity(gravity); } private Rect calculateMenuSurfaceBounds(Rect pipBounds) { @@ -275,138 +245,21 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis } void closeMenu() { - if (DEBUG) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: closeMenu()", TAG); - } - - if (mPipMenuView == null) { - return; - } - - mPipMenuView.hideAllUserControls(); - grantPipMenuFocus(false); - mDelegate.onMenuClosed(); - } - - boolean isInMoveMode() { - return mInMoveMode; - } - - private void setInMoveMode(boolean moveMode) { - if (mInMoveMode == moveMode) { - return; - } - - mInMoveMode = moveMode; - if (mDelegate != null) { - mDelegate.onInMoveModeChanged(); - } - } - - @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); - } - 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); - } - if (mCloseAfterExitMoveMenu) { - setInMoveMode(false); - mCloseAfterExitMoveMenu = false; - closeMenu(); - return true; - } - if (mInMoveMode) { - setInMoveMode(false); - mPipMenuView.showButtonsMenu(); - return true; - } - return false; - } - - @Override - public boolean onPipMovement(int keycode) { - if (DEBUG) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: onPipMovement - %b", TAG, mInMoveMode); - } - if (mInMoveMode) { - mDelegate.movePip(keycode); - } - return mInMoveMode; + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: closeMenu()", TAG); + switchToMenuMode(MODE_NO_MENU); } @Override public void detach() { closeMenu(); - mMainHandler.removeCallbacks(mCloseEduTextRunnable); detachPipMenu(); mLeash = null; } @Override public void setAppActions(List<RemoteAction> actions, RemoteAction closeAction) { - if (DEBUG) { - 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); - } - - // Hide disabled actions. - List<RemoteAction> enabledActions = new ArrayList<>(); - for (RemoteAction remoteAction : actions) { - if (remoteAction.isEnabled()) { - enabledActions.add(remoteAction); - } - } - updateAdditionalActionsList(mMediaActions, enabledActions, mCloseAction); - } - - private void updateAdditionalActionsList(List<RemoteAction> destination, - @Nullable List<RemoteAction> source, RemoteAction closeAction) { - final int number = source != null ? source.size() : 0; - if (number == 0 && destination.isEmpty() && Objects.equals(closeAction, mCloseAction)) { - // Nothing changed. - return; - } - - mCloseAction = closeAction; - - destination.clear(); - if (number > 0) { - destination.addAll(source); - } - maybeUpdateMenuViewActions(); - } - - private void maybeUpdateMenuViewActions() { - if (mPipMenuView == null) { - return; - } - if (!mAppActions.isEmpty()) { - mPipMenuView.setAdditionalActions(mAppActions, mCloseAction, mMainHandler); - } else { - mPipMenuView.setAdditionalActions(mMediaActions, mCloseAction, mMainHandler); - } + // NOOP - handled via the TvPipActionsProvider } @Override @@ -419,46 +272,36 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis */ @Override 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()); - } - if (destinationBounds.isEmpty()) { + @Nullable SurfaceControl.Transaction pipTx, + Rect pipBounds) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: resizePipMenu: %s", TAG, pipBounds.toShortString()); + if (pipBounds.isEmpty()) { return; } - if (!maybeCreateSyncApplier()) { + if (!isMenuReadyToMove()) { return; } - final Rect menuBounds = calculateMenuSurfaceBounds(destinationBounds); final SurfaceControl frontSurface = getSurfaceControl(mPipMenuView); - final SyncRtSurfaceTransactionApplier.SurfaceParams frontParams = - new SyncRtSurfaceTransactionApplier.SurfaceParams.Builder(frontSurface) - .withWindowCrop(menuBounds) - .build(); - final SurfaceControl backSurface = getSurfaceControl(mPipBackgroundView); - final SyncRtSurfaceTransactionApplier.SurfaceParams backParams = - new SyncRtSurfaceTransactionApplier.SurfaceParams.Builder(backSurface) - .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 - mBackgroundApplier.scheduleApply(backParams); - if (pipLeash != null && t != null) { - final SyncRtSurfaceTransactionApplier.SurfaceParams - pipParams = new SyncRtSurfaceTransactionApplier.SurfaceParams.Builder(pipLeash) - .withMergeTransaction(t) - .build(); - mApplier.scheduleApply(frontParams, pipParams); - } else { - mApplier.scheduleApply(frontParams); + final Rect menuBounds = calculateMenuSurfaceBounds(pipBounds); + if (pipTx == null) { + pipTx = new SurfaceControl.Transaction(); } + pipTx.setWindowCrop(frontSurface, menuBounds.width(), menuBounds.height()); + pipTx.setWindowCrop(backSurface, menuBounds.width(), menuBounds.height()); + + // Synchronize drawing the content in the front and back surfaces together with the pip + // transaction and the window crop for the front and back surfaces + final SurfaceSyncGroup syncGroup = new SurfaceSyncGroup("TvPip"); + syncGroup.add(mPipMenuView.getRootSurfaceControl(), null); + syncGroup.add(mPipBackgroundView.getRootSurfaceControl(), null); + updateMenuBounds(pipBounds); + syncGroup.addTransaction(pipTx); + syncGroup.markSyncReady(); } private SurfaceControl getSurfaceControl(View v) { @@ -466,127 +309,160 @@ 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()); - } + public void movePipMenu(SurfaceControl pipLeash, SurfaceControl.Transaction pipTx, + Rect pipBounds) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: movePipMenu: %s", TAG, pipBounds.toShortString()); - if (pipDestBounds.isEmpty()) { - if (transaction == null && DEBUG) { + if (pipBounds.isEmpty()) { + if (pipTx == null) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: no transaction given", TAG); } return; } - if (!maybeCreateSyncApplier()) { + if (!isMenuReadyToMove()) { return; } - final Rect menuDestBounds = calculateMenuSurfaceBounds(pipDestBounds); - final Rect tmpSourceBounds = new Rect(); - // If there is no pip leash supplied, that means the PiP leash is already finalized - // 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); - } - mPipMenuView.getBoundsOnScreen(tmpSourceBounds); - } else { - if (DEBUG) { - 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()); - } - - mTmpSourceRectF.set(tmpSourceBounds); - mTmpDestinationRectF.set(menuDestBounds); - mMoveTransform.setTranslate(mTmpDestinationRectF.left, mTmpDestinationRectF.top); - final SurfaceControl frontSurface = getSurfaceControl(mPipMenuView); - final SyncRtSurfaceTransactionApplier.SurfaceParams frontParams = - new SyncRtSurfaceTransactionApplier.SurfaceParams.Builder(frontSurface) - .withMatrix(mMoveTransform) - .build(); - final SurfaceControl backSurface = getSurfaceControl(mPipBackgroundView); - final SyncRtSurfaceTransactionApplier.SurfaceParams backParams = - new SyncRtSurfaceTransactionApplier.SurfaceParams.Builder(backSurface) - .withMatrix(mMoveTransform) - .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 - mBackgroundApplier.scheduleApply(backParams); - if (pipLeash != null && transaction != null) { - final SyncRtSurfaceTransactionApplier.SurfaceParams pipParams = - new SyncRtSurfaceTransactionApplier.SurfaceParams.Builder(pipLeash) - .withMergeTransaction(transaction) - .build(); - mApplier.scheduleApply(frontParams, pipParams); - } else { - mApplier.scheduleApply(frontParams); - } - - updateMenuBounds(pipDestBounds); - } - - private boolean maybeCreateSyncApplier() { - if (mPipMenuView == null || mPipMenuView.getViewRootImpl() == null) { + final Rect menuDestBounds = calculateMenuSurfaceBounds(pipBounds); + if (pipTx == null) { + pipTx = new SurfaceControl.Transaction(); + } + pipTx.setPosition(frontSurface, menuDestBounds.left, menuDestBounds.top); + pipTx.setPosition(backSurface, menuDestBounds.left, menuDestBounds.top); + + // Synchronize drawing the content in the front and back surfaces together with the pip + // transaction and the position change for the front and back surfaces + final SurfaceSyncGroup syncGroup = new SurfaceSyncGroup("TvPip"); + syncGroup.add(mPipMenuView.getRootSurfaceControl(), null); + syncGroup.add(mPipBackgroundView.getRootSurfaceControl(), null); + updateMenuBounds(pipBounds); + syncGroup.addTransaction(pipTx); + syncGroup.markSyncReady(); + } + + private boolean isMenuReadyToMove() { + final boolean ready = mPipMenuView != null && mPipMenuView.getViewRootImpl() != null + && mPipBackgroundView != null && mPipBackgroundView.getViewRootImpl() != null; + if (!ready) { 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); } - if (mBackgroundApplier == null) { - mBackgroundApplier = new SyncRtSurfaceTransactionApplier(mPipBackgroundView); - } - return true; + return ready; } private void detachPipMenu() { if (mPipMenuView != null) { - mApplier = null; mSystemWindows.removeView(mPipMenuView); mPipMenuView = null; } if (mPipBackgroundView != null) { - mBackgroundApplier = null; mSystemWindows.removeView(mPipBackgroundView); mPipBackgroundView = null; } } @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()); - } + public void updateMenuBounds(Rect pipBounds) { + final Rect menuBounds = calculateMenuSurfaceBounds(pipBounds); + 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) { - mPipMenuView.updateBounds(destinationBounds); + mPipMenuView.setPipBounds(pipBounds); + } + } + + // Start methods handling {@link TvPipMenuMode} + + @VisibleForTesting + boolean isMenuOpen() { + return mCurrentMenuMode != MODE_NO_MENU; + } + + @VisibleForTesting + boolean isInMoveMode() { + return mCurrentMenuMode == MODE_MOVE_MENU; + } + + @VisibleForTesting + boolean isInAllActionsMode() { + return mCurrentMenuMode == MODE_ALL_ACTIONS_MENU; + } + + private void switchToMenuMode(@TvPipMenuMode int menuMode) { + switchToMenuMode(menuMode, false); + } + + private void switchToMenuMode(@TvPipMenuMode int menuMode, boolean resetMenu) { + // Note: we intentionally don't return early here, because the TvPipMenuView needs to + // refresh the Ui even if there is no menu mode change. + mPrevMenuMode = mCurrentMenuMode; + mCurrentMenuMode = menuMode; + + ProtoLog.i(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: switchToMenuMode: setting mCurrentMenuMode=%s, mPrevMenuMode=%s", TAG, + getMenuModeString(), getMenuModeString(mPrevMenuMode)); + + updateUiOnNewMenuModeRequest(resetMenu); + updateDelegateOnNewMenuModeRequest(); + } + + private void updateUiOnNewMenuModeRequest(boolean resetMenu) { + if (mPipMenuView == null || mPipBackgroundView == null) return; + + mPipMenuView.setPipGravity(mTvPipBoundsState.getTvPipGravity()); + mPipMenuView.transitionToMenuMode(mCurrentMenuMode, resetMenu); + mPipBackgroundView.transitionToMenuMode(mCurrentMenuMode); + grantPipMenuFocus(mCurrentMenuMode != MODE_NO_MENU); + } + + private void updateDelegateOnNewMenuModeRequest() { + if (mPrevMenuMode == mCurrentMenuMode) return; + if (mDelegate == null) return; + + if (mPrevMenuMode == MODE_MOVE_MENU || isInMoveMode()) { + mDelegate.onInMoveModeChanged(); + } + + if (mCurrentMenuMode == MODE_NO_MENU) { + mDelegate.onMenuClosed(); } } + @VisibleForTesting + String getMenuModeString() { + return getMenuModeString(mCurrentMenuMode); + } + + static String getMenuModeString(@TvPipMenuMode int menuMode) { + switch(menuMode) { + case MODE_NO_MENU: + return "MODE_NO_MENU"; + case MODE_MOVE_MENU: + return "MODE_MOVE_MENU"; + case MODE_ALL_ACTIONS_MENU: + return "MODE_ALL_ACTIONS_MENU"; + default: + return "Unknown"; + } + } + + // Start {@link TvPipMenuView.Delegate} methods + @Override - public void onFocusTaskChanged(ActivityManager.RunningTaskInfo taskInfo) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: onFocusTaskChanged", TAG); + public void onCloseEduText() { + mTvPipBoundsState.setPipMenuTemporaryDecorInsets(Insets.NONE); + mDelegate.closeEduText(); } @Override @@ -597,43 +473,52 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis } @Override - public void onCloseButtonClick() { - mDelegate.closePip(); + public boolean onExitMoveMode() { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: onExitMoveMode - mCurrentMenuMode=%s", TAG, getMenuModeString()); + + final int saveMenuMode = mCurrentMenuMode; + if (isInMoveMode()) { + switchToMenuMode(mPrevMenuMode); + } + return saveMenuMode == MODE_MOVE_MENU; } @Override - public void onFullscreenButtonClick() { - mDelegate.movePipToFullscreen(); + public boolean onPipMovement(int keycode) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: onPipMovement - mCurrentMenuMode=%s", TAG, getMenuModeString()); + if (isInMoveMode()) { + mDelegate.movePip(keycode); + } + return isInMoveMode(); } @Override - public void onToggleExpandedMode() { - mDelegate.togglePipExpansion(); + public void onPipWindowFocusChanged(boolean focused) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: onPipWindowFocusChanged - focused=%b", TAG, focused); + mMenuIsFocused = focused; + if (!focused && isMenuOpen()) { + closeMenu(); + } } interface Delegate { - void movePipToFullscreen(); - void movePip(int keycode); void onInMoveModeChanged(); - int getPipGravity(); - - void togglePipExpansion(); - void onMenuClosed(); void closeEduText(); - - void closePip(); } private void grantPipMenuFocus(boolean grantFocus) { - if (DEBUG) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: grantWindowFocus(%b)", TAG, grantFocus); - } + if (mMenuIsFocused == grantFocus) return; + + 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/TvPipMenuEduTextDrawer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuEduTextDrawer.java new file mode 100644 index 000000000000..6eef22562caa --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuEduTextDrawer.java @@ -0,0 +1,280 @@ +/* + * 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.tv; + +import static android.view.Gravity.BOTTOM; +import static android.view.Gravity.CENTER; +import static android.view.View.GONE; +import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; + +import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE; + +import android.animation.ValueAnimator; +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.os.Handler; +import android.text.Annotation; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.SpannedString; +import android.text.TextUtils; +import android.view.ViewGroup; +import android.view.ViewTreeObserver; +import android.widget.FrameLayout; +import android.widget.FrameLayout.LayoutParams; +import android.widget.TextView; + +import androidx.annotation.NonNull; + +import com.android.internal.protolog.common.ProtoLog; +import com.android.wm.shell.R; + +import java.util.Arrays; + +/** + * The edu text drawer shows the user a hint for how to access the Picture-in-Picture menu. + * It displays a text in a drawer below the Picture-in-Picture window. The drawer has the same + * width as the Picture-in-Picture window. Depending on the Picture-in-Picture mode, there might + * not be enough space to fit the whole educational text in the available space. In such cases we + * apply a marquee animation to the TextView inside the drawer. + * + * The drawer is shown temporarily giving the user enough time to read it, after which it slides + * shut. We show the text for a duration calculated based on whether the text is marqueed or not. + */ +class TvPipMenuEduTextDrawer extends FrameLayout { + private static final String TAG = "TvPipMenuEduTextDrawer"; + + private static final float MARQUEE_DP_PER_SECOND = 30; // Copy of TextView.MARQUEE_DP_PER_SECOND + private static final int MARQUEE_RESTART_DELAY = 1200; // Copy of TextView.MARQUEE_DELAY + private final float mMarqueeAnimSpeed; // pixels per ms + + private final Runnable mCloseDrawerRunnable = this::closeDrawer; + private final Runnable mStartScrollEduTextRunnable = this::startScrollEduText; + + private final Handler mMainHandler; + private final Listener mListener; + private final TextView mEduTextView; + + TvPipMenuEduTextDrawer(@NonNull Context context, Handler mainHandler, Listener listener) { + super(context, null, 0, 0); + + mListener = listener; + mMainHandler = mainHandler; + + // Taken from TextView.Marquee calculation + mMarqueeAnimSpeed = + (MARQUEE_DP_PER_SECOND * context.getResources().getDisplayMetrics().density) / 1000f; + + mEduTextView = new TextView(mContext); + setupDrawer(); + } + + private void setupDrawer() { + final int eduTextHeight = mContext.getResources().getDimensionPixelSize( + R.dimen.pip_menu_edu_text_view_height); + final int marqueeRepeatLimit = mContext.getResources() + .getInteger(R.integer.pip_edu_text_scroll_times); + + mEduTextView.setLayoutParams( + new LayoutParams(MATCH_PARENT, eduTextHeight, BOTTOM | CENTER)); + mEduTextView.setGravity(CENTER); + mEduTextView.setClickable(false); + mEduTextView.setText(createEduTextString()); + mEduTextView.setSingleLine(); + mEduTextView.setTextAppearance(R.style.TvPipEduText); + mEduTextView.setEllipsize(TextUtils.TruncateAt.MARQUEE); + mEduTextView.setMarqueeRepeatLimit(marqueeRepeatLimit); + mEduTextView.setHorizontallyScrolling(true); + mEduTextView.setHorizontalFadingEdgeEnabled(true); + mEduTextView.setSelected(false); + addView(mEduTextView); + + setLayoutParams(new LayoutParams(MATCH_PARENT, eduTextHeight, CENTER)); + setClipChildren(true); + } + + /** + * Initializes the edu text. Should only be called once when the PiP is entered + */ + void init() { + ProtoLog.i(WM_SHELL_PICTURE_IN_PICTURE, "%s: init()", TAG); + scheduleLifecycleEvents(); + } + + private void scheduleLifecycleEvents() { + final int startScrollDelay = mContext.getResources().getInteger( + R.integer.pip_edu_text_start_scroll_delay); + if (isEduTextMarqueed()) { + mMainHandler.postDelayed(mStartScrollEduTextRunnable, startScrollDelay); + } + mMainHandler.postDelayed(mCloseDrawerRunnable, startScrollDelay + getEduTextShowDuration()); + mEduTextView.getViewTreeObserver().addOnWindowAttachListener( + new ViewTreeObserver.OnWindowAttachListener() { + @Override + public void onWindowAttached() { + } + + @Override + public void onWindowDetached() { + mEduTextView.getViewTreeObserver().removeOnWindowAttachListener(this); + mMainHandler.removeCallbacks(mStartScrollEduTextRunnable); + mMainHandler.removeCallbacks(mCloseDrawerRunnable); + } + }); + } + + private int getEduTextShowDuration() { + int eduTextShowDuration; + if (isEduTextMarqueed()) { + // Calculate the time it takes to fully scroll the text once: time = distance / speed + final float singleMarqueeDuration = + getMarqueeAnimEduTextLineWidth() / mMarqueeAnimSpeed; + // The TextView adds a delay between each marquee repetition. Take that into account + final float durationFromStartToStart = singleMarqueeDuration + MARQUEE_RESTART_DELAY; + // Finally, multiply by the number of times we repeat the marquee animation + eduTextShowDuration = + (int) durationFromStartToStart * mEduTextView.getMarqueeRepeatLimit(); + } else { + eduTextShowDuration = mContext.getResources() + .getInteger(R.integer.pip_edu_text_non_scroll_show_duration); + } + + ProtoLog.d(WM_SHELL_PICTURE_IN_PICTURE, "%s: getEduTextShowDuration(), showDuration=%d", + TAG, eduTextShowDuration); + return eduTextShowDuration; + } + + /** + * Returns true if the edu text width is bigger than the width of the text view, which indicates + * that the edu text will be marqueed + */ + private boolean isEduTextMarqueed() { + final int availableWidth = (int) mEduTextView.getWidth() + - mEduTextView.getCompoundPaddingLeft() + - mEduTextView.getCompoundPaddingRight(); + return availableWidth < getEduTextWidth(); + } + + /** + * Returns the width of a single marquee repetition of the edu text in pixels. + * This is the width from the start of the edu text to the start of the next edu + * text when it is marqueed. + * + * This is calculated based on the TextView.Marquee#start calculations + */ + private float getMarqueeAnimEduTextLineWidth() { + // When the TextView has a marquee animation, it puts a gap between the text end and the + // start of the next edu text repetition. The space is equal to a third of the TextView + // width + final float gap = mEduTextView.getWidth() / 3.0f; + return getEduTextWidth() + gap; + } + + private void startScrollEduText() { + ProtoLog.d(WM_SHELL_PICTURE_IN_PICTURE, "%s: startScrollEduText(), repeat=%d", + TAG, mEduTextView.getMarqueeRepeatLimit()); + mEduTextView.setSelected(true); + } + + /** + * Returns the width of the edu text irrespective of the TextView width + */ + private int getEduTextWidth() { + return (int) mEduTextView.getLayout().getLineWidth(0); + } + + /** + * Closes the edu text drawer if it hasn't been closed yet + */ + void closeIfNeeded() { + if (mMainHandler.hasCallbacks(mCloseDrawerRunnable)) { + ProtoLog.d(WM_SHELL_PICTURE_IN_PICTURE, + "%s: close(), closing the edu text drawer because of user action", TAG); + mMainHandler.removeCallbacks(mCloseDrawerRunnable); + mCloseDrawerRunnable.run(); + } else { + // Do nothing, the drawer has already been closed + } + } + + private void closeDrawer() { + ProtoLog.i(WM_SHELL_PICTURE_IN_PICTURE, "%s: closeDrawer()", TAG); + final int eduTextFadeExitAnimationDuration = mContext.getResources().getInteger( + R.integer.pip_edu_text_view_exit_animation_duration); + final int eduTextSlideExitAnimationDuration = mContext.getResources().getInteger( + R.integer.pip_edu_text_window_exit_animation_duration); + + // Start fading out the edu text + mEduTextView.animate() + .alpha(0f) + .setInterpolator(TvPipInterpolators.EXIT) + .setDuration(eduTextFadeExitAnimationDuration) + .start(); + + // Start animation to close the drawer by animating its height to 0 + final ValueAnimator heightAnimation = ValueAnimator.ofInt(getHeight(), 0); + heightAnimation.setDuration(eduTextSlideExitAnimationDuration); + heightAnimation.setInterpolator(TvPipInterpolators.BROWSE); + heightAnimation.addUpdateListener(animator -> { + final ViewGroup.LayoutParams params = getLayoutParams(); + params.height = (int) animator.getAnimatedValue(); + setLayoutParams(params); + if (params.height == 0) { + setVisibility(GONE); + } + }); + heightAnimation.start(); + + mListener.onCloseEduText(); + } + + /** + * Creates the educational text that will be displayed to the user. Here we replace the + * HOME annotation in the String with an icon + */ + private CharSequence createEduTextString() { + final SpannedString eduText = (SpannedString) getResources().getText(R.string.pip_edu_text); + final SpannableString spannableString = new SpannableString(eduText); + Arrays.stream(eduText.getSpans(0, eduText.length(), Annotation.class)).findFirst() + .ifPresent(annotation -> { + final Drawable icon = + getResources().getDrawable(R.drawable.home_icon, mContext.getTheme()); + if (icon != null) { + icon.mutate(); + icon.setBounds(0, 0, icon.getIntrinsicWidth(), icon.getIntrinsicHeight()); + spannableString.setSpan(new CenteredImageSpan(icon), + eduText.getSpanStart(annotation), + eduText.getSpanEnd(annotation), + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + }); + + return spannableString; + } + + /** + * A listener for edu text drawer event states. + */ + interface Listener { + /** + * The edu text closing impacts the size of the Picture-in-Picture window and influences + * how it is positioned on the screen. + */ + void onCloseEduText(); + } + +} 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..ccf65c299613 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 @@ -25,205 +25,138 @@ import static android.view.KeyEvent.KEYCODE_DPAD_RIGHT; import static android.view.KeyEvent.KEYCODE_DPAD_UP; import static android.view.KeyEvent.KEYCODE_ENTER; -import android.animation.ValueAnimator; -import android.app.PendingIntent; -import android.app.RemoteAction; +import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_MOVE; +import static com.android.wm.shell.pip.tv.TvPipMenuController.MODE_ALL_ACTIONS_MENU; +import static com.android.wm.shell.pip.tv.TvPipMenuController.MODE_MOVE_MENU; +import static com.android.wm.shell.pip.tv.TvPipMenuController.MODE_NO_MENU; + import android.content.Context; import android.graphics.Rect; -import android.graphics.drawable.Drawable; import android.os.Handler; -import android.text.Annotation; -import android.text.Spannable; -import android.text.SpannableString; -import android.text.SpannedString; -import android.util.AttributeSet; import android.view.Gravity; import android.view.KeyEvent; -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; -import android.widget.LinearLayout; -import android.widget.ScrollView; -import android.widget.TextView; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.widget.LinearLayoutManager; +import com.android.internal.widget.RecyclerView; 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; -import java.util.ArrayList; -import java.util.Arrays; 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. + * A View that represents Pip Menu on TV. It's responsible for displaying the Pip menu actions from + * the TvPipActionsProvider as well as the buttons for manually moving the PiP. */ -public class TvPipMenuView extends FrameLayout implements View.OnClickListener { +public class TvPipMenuView extends FrameLayout implements TvPipActionsProvider.Listener { private static final String TAG = "TvPipMenuView"; - private static final boolean DEBUG = TvPipController.DEBUG; - private static final int FIRST_CUSTOM_ACTION_POSITION = 3; + private final TvPipMenuView.Listener mListener; - @Nullable - private Listener mListener; + private final TvPipActionsProvider mTvPipActionsProvider; + + private final RecyclerView mActionButtonsRecyclerView; + private final LinearLayoutManager mButtonLayoutManager; + private final RecyclerViewAdapter mRecyclerViewAdapter; - private final LinearLayout mActionButtonsContainer; - private final View mMenuFrameView; - private final List<TvPipMenuActionButton> mAdditionalButtons = new ArrayList<>(); private final View mPipFrameView; + private final View mMenuFrameView; private final View mPipView; - private final TextView mEduTextView; - private final View mEduTextContainerView; + + private final View mPipBackground; + private final View mDimLayer; + + private final TvPipMenuEduTextDrawer mEduTextDrawer; + private final int mPipMenuOuterSpace; private final int mPipMenuBorderWidth; - private final int mEduTextFadeExitAnimationDurationMs; - private final int mEduTextSlideExitAnimationDurationMs; - private int mEduTextHeight; + + private final int mPipMenuFadeAnimationDuration; + private final int mResizeAnimationDuration; private final ImageView mArrowUp; private final ImageView mArrowRight; private final ImageView mArrowDown; private final ImageView mArrowLeft; + private final TvWindowMenuActionButton mA11yDoneButton; - private final ScrollView mScrollView; - private final HorizontalScrollView mHorizontalScrollView; - private View mFocusedButton; - - private Rect mCurrentPipBounds; - private boolean mMoveMenuIsVisible; - private boolean mButtonMenuIsVisible; - - private final TvPipMenuActionButton mExpandButton; - private final TvPipMenuActionButton mCloseButton; - + private @TvPipMenuController.TvPipMenuMode int mCurrentMenuMode = MODE_NO_MENU; + private final Rect mCurrentPipBounds = new Rect(); + private int mCurrentPipGravity; private boolean mSwitchingOrientation; - private final int mPipMenuFadeAnimationDuration; - private final int mResizeAnimationDuration; - - public TvPipMenuView(@NonNull Context context) { - this(context, null); - } - - public TvPipMenuView(@NonNull Context context, @Nullable AttributeSet attrs) { - this(context, attrs, 0); - } - - public TvPipMenuView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { - this(context, attrs, defStyleAttr, 0); - } - - public TvPipMenuView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, - int defStyleRes) { - super(context, attrs, defStyleAttr, defStyleRes); + private final AccessibilityManager mA11yManager; + private final Handler mMainHandler; + public TvPipMenuView(@NonNull Context context, @NonNull Handler mainHandler, + @NonNull Listener listener, TvPipActionsProvider tvPipActionsProvider) { + super(context, null, 0, 0); inflate(context, R.layout.tv_pip_menu, this); - mActionButtonsContainer = findViewById(R.id.tv_pip_menu_action_buttons); - mActionButtonsContainer.findViewById(R.id.tv_pip_menu_fullscreen_button) - .setOnClickListener(this); + mMainHandler = mainHandler; + mListener = listener; + mA11yManager = context.getSystemService(AccessibilityManager.class); - mCloseButton = mActionButtonsContainer.findViewById(R.id.tv_pip_menu_close_button); - mCloseButton.setOnClickListener(this); - mCloseButton.setIsCustomCloseAction(true); + mActionButtonsRecyclerView = findViewById(R.id.tv_pip_menu_action_buttons); + mButtonLayoutManager = new LinearLayoutManager(mContext); + mActionButtonsRecyclerView.setLayoutManager(mButtonLayoutManager); + mActionButtonsRecyclerView.setPreserveFocusAfterLayout(true); - mActionButtonsContainer.findViewById(R.id.tv_pip_menu_move_button) - .setOnClickListener(this); - mExpandButton = findViewById(R.id.tv_pip_menu_expand_button); - mExpandButton.setOnClickListener(this); + mTvPipActionsProvider = tvPipActionsProvider; + mRecyclerViewAdapter = new RecyclerViewAdapter(tvPipActionsProvider.getActionsList()); + mActionButtonsRecyclerView.setAdapter(mRecyclerViewAdapter); - mScrollView = findViewById(R.id.tv_pip_menu_scroll); - mHorizontalScrollView = findViewById(R.id.tv_pip_menu_horizontal_scroll); + tvPipActionsProvider.addListener(this); mMenuFrameView = findViewById(R.id.tv_pip_menu_frame); mPipFrameView = findViewById(R.id.tv_pip_border); mPipView = findViewById(R.id.tv_pip); + mPipBackground = findViewById(R.id.tv_pip_menu_background); + mDimLayer = findViewById(R.id.tv_pip_menu_dim_layer); + mArrowUp = findViewById(R.id.tv_pip_menu_arrow_up); 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); - - mEduTextView = findViewById(R.id.tv_pip_menu_edu_text); - mEduTextContainerView = findViewById(R.id.tv_pip_menu_edu_text_container); + mA11yDoneButton = findViewById(R.id.tv_pip_menu_done_button); 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); mPipMenuBorderWidth = context.getResources() .getDimensionPixelSize(R.dimen.pip_menu_border_width); - mEduTextHeight = context.getResources() - .getDimensionPixelSize(R.dimen.pip_menu_edu_text_view_height); - mEduTextFadeExitAnimationDurationMs = context.getResources() - .getInteger(R.integer.pip_edu_text_view_exit_animation_duration_ms); - mEduTextSlideExitAnimationDurationMs = context.getResources() - .getInteger(R.integer.pip_edu_text_window_exit_animation_duration_ms); - - initEduText(); - } - - void initEduText() { - final SpannedString eduText = (SpannedString) getResources().getText(R.string.pip_edu_text); - final SpannableString spannableString = new SpannableString(eduText); - Arrays.stream(eduText.getSpans(0, eduText.length(), Annotation.class)).findFirst() - .ifPresent(annotation -> { - final Drawable icon = - getResources().getDrawable(R.drawable.home_icon, mContext.getTheme()); - if (icon != null) { - icon.mutate(); - icon.setBounds(0, 0, icon.getIntrinsicWidth(), icon.getIntrinsicHeight()); - spannableString.setSpan(new CenteredImageSpan(icon), - eduText.getSpanStart(annotation), - eduText.getSpanEnd(annotation), - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - } - }); - mEduTextView.setText(spannableString); + mEduTextDrawer = new TvPipMenuEduTextDrawer(mContext, mainHandler, mListener); + ((FrameLayout) findViewById(R.id.tv_pip_menu_edu_text_drawer_placeholder)) + .addView(mEduTextDrawer); } - void setEduTextActive(boolean active) { - mEduTextView.setSelected(active); - } - - void hideEduText() { - final ValueAnimator heightAnimation = ValueAnimator.ofInt(mEduTextHeight, 0); - heightAnimation.setDuration(mEduTextSlideExitAnimationDurationMs); - heightAnimation.setInterpolator(TvPipInterpolators.BROWSE); - heightAnimation.addUpdateListener(animator -> { - mEduTextHeight = (int) animator.getAnimatedValue(); - }); - mEduTextView.animate() - .alpha(0f) - .setInterpolator(TvPipInterpolators.EXIT) - .setDuration(mEduTextFadeExitAnimationDurationMs) - .withEndAction(() -> { - mEduTextContainerView.setVisibility(GONE); - }).start(); - heightAnimation.start(); - } + void onPipTransitionToTargetBoundsStarted(Rect targetBounds) { + if (targetBounds == null) { + return; + } - void onPipTransitionStarted(Rect finishBounds) { // Fade out content by fading in view on top. - if (mCurrentPipBounds != null && finishBounds != null) { + if (mCurrentPipBounds != null) { boolean ratioChanged = PipUtils.aspectRatioChanged( mCurrentPipBounds.width() / (float) mCurrentPipBounds.height(), - finishBounds.width() / (float) finishBounds.height()); + targetBounds.width() / (float) targetBounds.height()); if (ratioChanged) { - mPipView.animate() + mPipBackground.animate() .alpha(1f) .setInterpolator(TvPipInterpolators.EXIT) .setDuration(mResizeAnimationDuration / 2) @@ -232,52 +165,54 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { } // Update buttons. - final boolean vertical = finishBounds.height() > finishBounds.width(); + final boolean vertical = targetBounds.height() > targetBounds.width(); final boolean orientationChanged = - vertical != (mActionButtonsContainer.getOrientation() == LinearLayout.VERTICAL); + vertical != (mButtonLayoutManager.getOrientation() == LinearLayoutManager.VERTICAL); ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: onPipTransitionStarted(), orientation changed %b", TAG, orientationChanged); + "%s: onPipTransitionToTargetBoundsStarted(), orientation changed %b", + TAG, orientationChanged); if (!orientationChanged) { return; } - if (mButtonMenuIsVisible) { + if (mCurrentMenuMode == MODE_ALL_ACTIONS_MENU) { mSwitchingOrientation = true; - mActionButtonsContainer.animate() + mActionButtonsRecyclerView.animate() .alpha(0) .setInterpolator(TvPipInterpolators.EXIT) .setDuration(mResizeAnimationDuration / 2) .withEndAction(() -> { - changeButtonScrollOrientation(finishBounds); - updateButtonGravity(finishBounds); + mButtonLayoutManager.setOrientation(vertical + ? LinearLayoutManager.VERTICAL : LinearLayoutManager.HORIZONTAL); // Only make buttons visible again in onPipTransitionFinished to keep in // sync with PiP content alpha animation. }); } else { - changeButtonScrollOrientation(finishBounds); - updateButtonGravity(finishBounds); + mButtonLayoutManager.setOrientation(vertical + ? LinearLayoutManager.VERTICAL : LinearLayoutManager.HORIZONTAL); } } - void onPipTransitionFinished() { + void onPipTransitionFinished(boolean enterTransition) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: onPipTransitionFinished()", TAG); - // Fade in content by fading out view on top. - mPipView.animate() + // Fade in content by fading out view on top (faded out at every aspect ratio change). + mPipBackground.animate() .alpha(0f) .setDuration(mResizeAnimationDuration / 2) .setInterpolator(TvPipInterpolators.ENTER) .start(); - // Update buttons. + if (enterTransition) { + mEduTextDrawer.init(); + } + if (mSwitchingOrientation) { - mActionButtonsContainer.animate() + mActionButtonsRecyclerView.animate() .alpha(1) .setInterpolator(TvPipInterpolators.ENTER) .setDuration(mResizeAnimationDuration / 2); - } else { - refocusPreviousButton(); } mSwitchingOrientation = false; } @@ -285,119 +220,16 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { /** * Also updates the button gravity. */ - void updateBounds(Rect updatedBounds) { + void setPipBounds(Rect updatedPipBounds) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: updateLayout, width: %s, height: %s", TAG, updatedBounds.width(), - updatedBounds.height()); - mCurrentPipBounds = updatedBounds; - if (!mSwitchingOrientation) { - updateButtonGravity(mCurrentPipBounds); - } + "%s: updateLayout, width: %s, height: %s", TAG, updatedPipBounds.width(), + updatedPipBounds.height()); + if (updatedPipBounds.equals(mCurrentPipBounds)) return; + mCurrentPipBounds.set(updatedPipBounds); updatePipFrameBounds(); } - private void changeButtonScrollOrientation(Rect bounds) { - final boolean vertical = bounds.height() > bounds.width(); - - final ViewGroup oldScrollView = vertical ? mHorizontalScrollView : mScrollView; - final ViewGroup newScrollView = vertical ? mScrollView : mHorizontalScrollView; - - if (oldScrollView.getChildCount() == 1) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: orientation changed", TAG); - oldScrollView.removeView(mActionButtonsContainer); - oldScrollView.setVisibility(GONE); - mActionButtonsContainer.setOrientation(vertical ? LinearLayout.VERTICAL - : LinearLayout.HORIZONTAL); - newScrollView.addView(mActionButtonsContainer); - newScrollView.setVisibility(VISIBLE); - if (mFocusedButton != null) { - mFocusedButton.requestFocus(); - } - } - } - - /** - * Change button gravity based on new dimensions - */ - private void updateButtonGravity(Rect bounds) { - final boolean vertical = bounds.height() > bounds.width(); - // Use Math.max since the possible orientation change might not have been applied yet. - final int buttonsSize = Math.max(mActionButtonsContainer.getHeight(), - mActionButtonsContainer.getWidth()); - - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: buttons container width: %s, height: %s", TAG, - mActionButtonsContainer.getWidth(), mActionButtonsContainer.getHeight()); - - final boolean buttonsFit = - vertical ? buttonsSize < bounds.height() - : buttonsSize < bounds.width(); - final int buttonGravity = buttonsFit ? Gravity.CENTER - : (vertical ? Gravity.CENTER_HORIZONTAL : Gravity.CENTER_VERTICAL); - - final LayoutParams params = (LayoutParams) mActionButtonsContainer.getLayoutParams(); - params.gravity = buttonGravity; - mActionButtonsContainer.setLayoutParams(params); - - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: vertical: %b, buttonsFit: %b, gravity: %s", TAG, vertical, buttonsFit, - Gravity.toString(buttonGravity)); - } - - private void refocusPreviousButton() { - if (mMoveMenuIsVisible || mCurrentPipBounds == null || mFocusedButton == null) { - return; - } - final boolean vertical = mCurrentPipBounds.height() > mCurrentPipBounds.width(); - - if (!mFocusedButton.hasFocus()) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: request focus from: %s", TAG, mFocusedButton); - mFocusedButton.requestFocus(); - } else { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: already focused: %s", TAG, mFocusedButton); - } - - // Do we need to scroll? - final Rect buttonBounds = new Rect(); - final Rect scrollBounds = new Rect(); - if (vertical) { - mScrollView.getDrawingRect(scrollBounds); - } else { - mHorizontalScrollView.getDrawingRect(scrollBounds); - } - mFocusedButton.getHitRect(buttonBounds); - - if (scrollBounds.contains(buttonBounds)) { - // Button is already completely visible, don't scroll - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: not scrolling", TAG); - return; - } - - // Scrolling so the button is visible to the user. - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: scrolling to focused button", TAG); - - if (vertical) { - mScrollView.smoothScrollTo((int) mFocusedButton.getX(), - (int) mFocusedButton.getY()); - } else { - mHorizontalScrollView.smoothScrollTo((int) mFocusedButton.getX(), - (int) mFocusedButton.getY()); - } - } - - Rect getPipMenuContainerBounds(Rect pipBounds) { - final Rect menuUiBounds = new Rect(pipBounds); - menuUiBounds.inset(-mPipMenuOuterSpace, -mPipMenuOuterSpace); - menuUiBounds.bottom += mEduTextHeight; - return menuUiBounds; - } - /** * Update mPipFrameView's bounds according to the new pip window bounds. We can't * make mPipFrameView match_parent, because the pip menu might contain other content around @@ -420,86 +252,130 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { mPipView.setLayoutParams(pipViewParams); } - - } - - void setListener(@Nullable Listener listener) { - mListener = listener; + // Keep focused button within the visible area while the PiP is changing size. Otherwise, + // the button would lose focus which would cause a need for scrolling and re-focusing after + // the animation finishes, which does not look good. + View focusedChild = mActionButtonsRecyclerView.getFocusedChild(); + if (focusedChild != null) { + mActionButtonsRecyclerView.scrollToPosition( + mActionButtonsRecyclerView.getChildLayoutPosition(focusedChild)); + } } - void setExpandedModeEnabled(boolean enabled) { - mExpandButton.setVisibility(enabled ? VISIBLE : GONE); + Rect getPipMenuContainerBounds(Rect pipBounds) { + final Rect menuUiBounds = new Rect(pipBounds); + menuUiBounds.inset(-mPipMenuOuterSpace, -mPipMenuOuterSpace); + menuUiBounds.bottom += mEduTextDrawer.getHeight(); + return menuUiBounds; } - void setIsExpanded(boolean expanded) { - if (DEBUG) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: setIsExpanded, expanded: %b", TAG, expanded); + void transitionToMenuMode(int menuMode, boolean resetMenu) { + switch (menuMode) { + case MODE_NO_MENU: + hideAllUserControls(); + break; + case MODE_MOVE_MENU: + showMoveMenu(); + break; + case MODE_ALL_ACTIONS_MENU: + showAllActionsMenu(resetMenu); + break; + default: + throw new IllegalArgumentException( + "Unknown TV PiP menu mode: " + + TvPipMenuController.getMenuModeString(mCurrentMenuMode)); } - mExpandButton.setImageResource( - expanded ? R.drawable.pip_ic_collapse : R.drawable.pip_ic_expand); - mExpandButton.setTextAndDescription( - expanded ? R.string.pip_collapse : R.string.pip_expand); + + mCurrentMenuMode = menuMode; } - /** - * @param gravity for the arrow hints - */ - void showMoveMenu(int gravity) { - if (DEBUG) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: showMoveMenu()", TAG); - } - mButtonMenuIsVisible = false; - mMoveMenuIsVisible = true; - showButtonsMenu(false); - showMovementHints(gravity); + private void showMoveMenu() { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: showMoveMenu()", TAG); + + if (mCurrentMenuMode == MODE_MOVE_MENU) return; + + showMovementHints(); + setMenuButtonsVisible(false); setFrameHighlighted(true); + + animateAlphaTo(mA11yManager.isEnabled() ? 1f : 0f, mDimLayer); + + mEduTextDrawer.closeIfNeeded(); } - void showButtonsMenu() { - if (DEBUG) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: showButtonsMenu()", TAG); + private void showAllActionsMenu(boolean resetMenu) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: showAllActionsMenu(), resetMenu %b", TAG, resetMenu); + + if (resetMenu) { + scrollToFirstAction(); } - mButtonMenuIsVisible = true; - mMoveMenuIsVisible = false; - showButtonsMenu(true); + if (mCurrentMenuMode == MODE_ALL_ACTIONS_MENU) return; + + setMenuButtonsVisible(true); hideMovementHints(); setFrameHighlighted(true); + animateAlphaTo(1f, mDimLayer); + mEduTextDrawer.closeIfNeeded(); - // 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) - mFocusedButton = mActionButtonsContainer.getChildAt(1); - // Reset scroll position. - mScrollView.scrollTo(0, 0); - mHorizontalScrollView.scrollTo( - isLayoutRtl() ? mActionButtonsContainer.getWidth() : 0, 0); + if (mCurrentMenuMode == MODE_MOVE_MENU) { + refocusButton(mTvPipActionsProvider.getFirstIndexOfAction(ACTION_MOVE)); } - refocusPreviousButton(); + + } + + private void scrollToFirstAction() { + // Clearing the focus here is necessary to allow a smooth scroll even if the first action + // is currently not visible. + final View focusedChild = mActionButtonsRecyclerView.getFocusedChild(); + if (focusedChild != null) { + focusedChild.clearFocus(); + } + + mButtonLayoutManager.scrollToPosition(0); + mActionButtonsRecyclerView.post(() -> refocusButton(0)); } /** - * Hides all menu views, including the menu frame. + * @return true if focus was requested, false if focus request could not be carried out due to + * the view for the position not being available (scrolling beforehand will be necessary). */ - void hideAllUserControls() { + private boolean refocusButton(int position) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: refocusButton, position: %d", TAG, position); + + View itemToFocus = mButtonLayoutManager.findViewByPosition(position); + if (itemToFocus != null) { + itemToFocus.requestFocus(); + itemToFocus.requestAccessibilityFocus(); + } + return itemToFocus != null; + } + + private void hideAllUserControls() { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: hideAllUserControls()", TAG); - mFocusedButton = null; - mButtonMenuIsVisible = false; - mMoveMenuIsVisible = false; - showButtonsMenu(false); + + if (mCurrentMenuMode == MODE_NO_MENU) return; + + setMenuButtonsVisible(false); hideMovementHints(); setFrameHighlighted(false); + animateAlphaTo(0f, mDimLayer); + } + + void setPipGravity(int gravity) { + mCurrentPipGravity = gravity; + if (mCurrentMenuMode == MODE_MOVE_MENU) { + showMovementHints(); + } } @Override public void onWindowFocusChanged(boolean hasWindowFocus) { super.onWindowFocusChanged(hasWindowFocus); - if (!hasWindowFocus) { - hideAllUserControls(); - } + mListener.onPipWindowFocusChanged(hasWindowFocus); } private void animateAlphaTo(float alpha, View view) { @@ -522,143 +398,30 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { }); } - /** - * Button order: - * - Fullscreen - * - Close - * - Custom actions (app or media actions) - * - System actions - */ - void setAdditionalActions(List<RemoteAction> actions, RemoteAction closeAction, - Handler mainHandler) { - if (DEBUG) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: setAdditionalActions()", TAG); - } - - // Replace system close action with custom close action if available - if (closeAction != null) { - setActionForButton(closeAction, mCloseButton, mainHandler); - } else { - mCloseButton.setTextAndDescription(R.string.pip_close); - mCloseButton.setImageResource(R.drawable.pip_ic_close_white); - } - mCloseButton.setIsCustomCloseAction(closeAction != null); - // Make sure the close action is always enabled - mCloseButton.setEnabled(true); - - // Make sure we exactly as many additional buttons as we have actions to display. - final int actionsNumber = actions.size(); - int buttonsNumber = mAdditionalButtons.size(); - if (actionsNumber > buttonsNumber) { - // Add buttons until we have enough to display all the actions. - while (actionsNumber > buttonsNumber) { - TvPipMenuActionButton button = new TvPipMenuActionButton(mContext); - button.setOnClickListener(this); - - mActionButtonsContainer.addView(button, - FIRST_CUSTOM_ACTION_POSITION + buttonsNumber); - mAdditionalButtons.add(button); - - buttonsNumber++; - } - } else if (actionsNumber < buttonsNumber) { - // Hide buttons until we as many as the actions. - while (actionsNumber < buttonsNumber) { - final View button = mAdditionalButtons.get(buttonsNumber - 1); - button.setVisibility(View.GONE); - button.setTag(null); - - buttonsNumber--; - } - } - - // "Assign" actions to the buttons. - for (int index = 0; index < actionsNumber; index++) { - final RemoteAction action = actions.get(index); - final TvPipMenuActionButton button = mAdditionalButtons.get(index); - - // Remove action if it matches the custom close action. - if (PipUtils.remoteActionsMatch(action, closeAction)) { - button.setVisibility(GONE); - continue; - } - setActionForButton(action, button, mainHandler); - } - - if (mCurrentPipBounds != null) { - updateButtonGravity(mCurrentPipBounds); - refocusPreviousButton(); - } - } - - private void setActionForButton(RemoteAction action, TvPipMenuActionButton button, - Handler mainHandler) { - button.setVisibility(View.VISIBLE); // Ensure the button is visible. - if (action.getContentDescription().length() > 0) { - button.setTextAndDescription(action.getContentDescription()); - } else { - button.setTextAndDescription(action.getTitle()); - } - button.setEnabled(action.isEnabled()); - button.setTag(action); - action.getIcon().loadDrawableAsync(mContext, button::setImageDrawable, mainHandler); - } - - @Nullable - SurfaceControl getWindowSurfaceControl() { - final ViewRootImpl root = getViewRootImpl(); - if (root == null) { - return null; - } - final SurfaceControl out = root.getSurfaceControl(); - if (out != null && out.isValid()) { - return out; - } - return null; - } - @Override - public void onClick(View v) { - if (mListener == null) return; - - final int id = v.getId(); - if (id == R.id.tv_pip_menu_fullscreen_button) { - mListener.onFullscreenButtonClick(); - } else if (id == R.id.tv_pip_menu_move_button) { - mListener.onEnterMoveMode(); - } else if (id == R.id.tv_pip_menu_close_button) { - mListener.onCloseButtonClick(); - } else if (id == R.id.tv_pip_menu_expand_button) { - mListener.onToggleExpandedMode(); - } else { - // This should be an "additional action" - final RemoteAction action = (RemoteAction) v.getTag(); - if (action != null) { - try { - action.getActionIntent().send(); - } catch (PendingIntent.CanceledException e) { - ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: Failed to send action, %s", TAG, e); - } - } else { - ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: RemoteAction is null", TAG); - } + public void onActionsChanged(int added, int updated, int startIndex) { + mRecyclerViewAdapter.notifyItemRangeChanged(startIndex, updated); + if (added > 0) { + mRecyclerViewAdapter.notifyItemRangeInserted(startIndex + updated, added); + } else if (added < 0) { + mRecyclerViewAdapter.notifyItemRangeRemoved(startIndex + updated, -added); } } @Override public boolean dispatchKeyEvent(KeyEvent event) { - if (mListener != null && event.getAction() == ACTION_UP) { - if (!mMoveMenuIsVisible) { - mFocusedButton = mActionButtonsContainer.getFocusedChild(); + if (event.getAction() == ACTION_UP) { + + 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: @@ -678,16 +441,38 @@ 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)); + public void showMovementHints() { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: showMovementHints(), position: %s", TAG, Gravity.toString(mCurrentPipGravity)); + animateAlphaTo(checkGravity(mCurrentPipGravity, Gravity.BOTTOM) ? 1f : 0f, mArrowUp); + animateAlphaTo(checkGravity(mCurrentPipGravity, Gravity.TOP) ? 1f : 0f, mArrowDown); + animateAlphaTo(checkGravity(mCurrentPipGravity, Gravity.RIGHT) ? 1f : 0f, mArrowLeft); + animateAlphaTo(checkGravity(mCurrentPipGravity, 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.setVisibility(VISIBLE); + mA11yDoneButton.setOnClickListener(v -> { + mListener.onExitMoveMode(); + }); + mA11yDoneButton.requestFocus(); + mA11yDoneButton.requestAccessibilityFocus(); } + } - 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); + private void setArrowA11yEnabled(View arrowView, boolean enabled, int keycode) { + arrowView.setClickable(enabled); + if (enabled) { + arrowView.setOnClickListener(v -> { + mListener.onPipMovement(keycode); + }); + } } private boolean checkGravity(int gravity, int feature) { @@ -698,40 +483,80 @@ 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); + + if (mCurrentMenuMode != MODE_MOVE_MENU) return; + 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); - } - if (show) { - mActionButtonsContainer.setVisibility(VISIBLE); - refocusPreviousButton(); - } - animateAlphaTo(show ? 1 : 0, mActionButtonsContainer); + private void setMenuButtonsVisible(boolean visible) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: showUserActions: %b", TAG, visible); + animateAlphaTo(visible ? 1 : 0, mActionButtonsRecyclerView); } private void setFrameHighlighted(boolean highlighted) { mMenuFrameView.setActivated(highlighted); } - interface Listener { + private class RecyclerViewAdapter extends + RecyclerView.Adapter<RecyclerViewAdapter.ButtonViewHolder> { - void onBackPress(); + private final List<TvPipAction> mActionList; - void onEnterMoveMode(); + RecyclerViewAdapter(List<TvPipAction> actionList) { + this.mActionList = actionList; + } + + @NonNull + @Override + public ButtonViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + return new ButtonViewHolder(new TvWindowMenuActionButton(mContext)); + } + + @Override + public void onBindViewHolder(@NonNull ButtonViewHolder holder, int position) { + TvPipAction action = mActionList.get(position); + action.populateButton(holder.mButton, mMainHandler); + } + + @Override + public int getItemCount() { + return mActionList.size(); + } + + private class ButtonViewHolder extends RecyclerView.ViewHolder implements OnClickListener { + TvWindowMenuActionButton mButton; + + ButtonViewHolder(@NonNull View itemView) { + super(itemView); + mButton = (TvWindowMenuActionButton) itemView; + mButton.setOnClickListener(this); + } + + @Override + public void onClick(View v) { + TvPipAction action = mActionList.get( + mActionButtonsRecyclerView.getChildLayoutPosition(v)); + if (action != null) { + action.executeAction(); + } + } + } + } + + interface Listener extends TvPipMenuEduTextDrawer.Listener { + + void onBackPress(); /** * Called when a button for exiting move mode was pressed. @@ -746,10 +571,10 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { */ boolean onPipMovement(int keycode); - void onCloseButtonClick(); - - void onFullscreenButtonClick(); - - void onToggleExpandedMode(); + /** + * Called when the TvPipMenuView loses focus. This also means that the TV PiP menu window + * has lost focus. + */ + void onPipWindowFocusChanged(boolean focused); } } 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..f22ee595e6c9 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 @@ -16,18 +16,13 @@ package com.android.wm.shell.pip.tv; -import static android.app.Notification.Action.SEMANTIC_ACTION_DELETE; -import static android.app.Notification.Action.SEMANTIC_ACTION_NONE; - +import android.annotation.NonNull; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; -import android.app.RemoteAction; -import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; import android.content.Intent; -import android.content.IntentFilter; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.graphics.Bitmap; @@ -35,7 +30,6 @@ import android.graphics.drawable.Drawable; import android.graphics.drawable.Icon; import android.media.session.MediaSession; import android.os.Bundle; -import android.os.Handler; import android.text.TextUtils; import com.android.internal.messages.nano.SystemMessageProto.SystemMessage; @@ -47,7 +41,6 @@ import com.android.wm.shell.pip.PipParamsChangedForwarder; import com.android.wm.shell.pip.PipUtils; import com.android.wm.shell.protolog.ShellProtoLogGroup; -import java.util.ArrayList; import java.util.List; /** @@ -55,39 +48,18 @@ import java.util.List; * <p>Once it's created, it will manage the PiP notification UI by itself except for handling * configuration changes and user initiated expanded PiP toggling. */ -public class TvPipNotificationController { - private static final String TAG = "TvPipNotification"; +public class TvPipNotificationController implements TvPipActionsProvider.Listener { + private static final String TAG = TvPipNotificationController.class.getSimpleName(); // Referenced in com.android.systemui.util.NotificationChannels. public static final String NOTIFICATION_CHANNEL = "TVPIP"; private static final String NOTIFICATION_TAG = "TvPip"; - private static final String SYSTEMUI_PERMISSION = "com.android.systemui.permission.SELF"; - - private static final String ACTION_SHOW_PIP_MENU = - "com.android.wm.shell.pip.tv.notification.action.SHOW_PIP_MENU"; - private static final String ACTION_CLOSE_PIP = - "com.android.wm.shell.pip.tv.notification.action.CLOSE_PIP"; - private static final String ACTION_MOVE_PIP = - "com.android.wm.shell.pip.tv.notification.action.MOVE_PIP"; - private static final String ACTION_TOGGLE_EXPANDED_PIP = - "com.android.wm.shell.pip.tv.notification.action.TOGGLE_EXPANDED_PIP"; - private static final String ACTION_FULLSCREEN = - "com.android.wm.shell.pip.tv.notification.action.FULLSCREEN"; private final Context mContext; private final PackageManager mPackageManager; private final NotificationManager mNotificationManager; private final Notification.Builder mNotificationBuilder; - private final ActionBroadcastReceiver mActionBroadcastReceiver; - private final Handler mMainHandler; - private Delegate mDelegate; - private final TvPipBoundsState mTvPipBoundsState; - - private String mDefaultTitle; - - private final List<RemoteAction> mCustomActions = new ArrayList<>(); - private final List<RemoteAction> mMediaActions = new ArrayList<>(); - private RemoteAction mCustomCloseAction; + private TvPipActionsProvider mTvPipActionsProvider; private MediaSession.Token mMediaSessionToken; @@ -95,55 +67,41 @@ public class TvPipNotificationController { private String mPackageName; private boolean mIsNotificationShown; + private String mDefaultTitle; private String mPipTitle; private String mPipSubtitle; + // Saving the actions, so they don't have to be regenerated when e.g. the PiP title changes. + @NonNull + private Notification.Action[] mPipActions; + private Bitmap mActivityIcon; public TvPipNotificationController(Context context, PipMediaController pipMediaController, - PipParamsChangedForwarder pipParamsChangedForwarder, TvPipBoundsState tvPipBoundsState, - Handler mainHandler) { + PipParamsChangedForwarder pipParamsChangedForwarder) { mContext = context; mPackageManager = context.getPackageManager(); mNotificationManager = context.getSystemService(NotificationManager.class); - mMainHandler = mainHandler; - mTvPipBoundsState = tvPipBoundsState; + + mPipActions = new Notification.Action[0]; mNotificationBuilder = new Notification.Builder(context, NOTIFICATION_CHANNEL) .setLocalOnly(true) .setOngoing(true) .setCategory(Notification.CATEGORY_SYSTEM) .setShowWhen(true) + .setOnlyAlertOnce(true) .setSmallIcon(R.drawable.pip_icon) .setAllowSystemGeneratedContextualActions(false) - .setContentIntent(createPendingIntent(context, ACTION_FULLSCREEN)) - .setDeleteIntent(getCloseAction().actionIntent) - .extend(new Notification.TvExtender() - .setContentIntent(createPendingIntent(context, ACTION_SHOW_PIP_MENU)) - .setDeleteIntent(createPendingIntent(context, ACTION_CLOSE_PIP))); - - mActionBroadcastReceiver = new ActionBroadcastReceiver(); + .setContentIntent( + createPendingIntent(context, TvPipController.ACTION_TO_FULLSCREEN)); + // TvExtender and DeleteIntent set later since they might change. - pipMediaController.addActionListener(this::onMediaActionsChanged); pipMediaController.addTokenListener(this::onMediaSessionTokenChanged); pipParamsChangedForwarder.addListener( new PipParamsChangedForwarder.PipParamsChangedCallback() { @Override - public void onExpandedAspectRatioChanged(float ratio) { - updateExpansionState(); - } - - @Override - public void onActionsChanged(List<RemoteAction> actions, - RemoteAction closeAction) { - mCustomActions.clear(); - mCustomActions.addAll(actions); - mCustomCloseAction = closeAction; - updateNotificationContent(); - } - - @Override public void onTitleChanged(String title) { mPipTitle = title; updateNotificationContent(); @@ -156,34 +114,33 @@ public class TvPipNotificationController { } }); - onConfigurationChanged(context); + onConfigurationChanged(); } - void setDelegate(Delegate 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."); - } - if (delegate == null) { - throw new IllegalArgumentException("The delegate must not be null."); - } + /** + * Call before showing any notification. + */ + void setTvPipActionsProvider(@NonNull TvPipActionsProvider tvPipActionsProvider) { + mTvPipActionsProvider = tvPipActionsProvider; + mTvPipActionsProvider.addListener(this); + } - mDelegate = delegate; + void onConfigurationChanged() { + mDefaultTitle = mContext.getResources().getString(R.string.pip_notification_unknown_title); + updateNotificationContent(); } void show(String packageName) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: show %s", TAG, packageName); - if (mDelegate == null) { - throw new IllegalStateException("Delegate is not set."); + if (mTvPipActionsProvider == null) { + ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: Missing TvPipActionsProvider", TAG); + return; } mIsNotificationShown = true; mPackageName = packageName; mActivityIcon = getActivityIcon(); - mActionBroadcastReceiver.register(); updateNotificationContent(); } @@ -193,151 +150,42 @@ public class TvPipNotificationController { mIsNotificationShown = false; mPackageName = null; - mActionBroadcastReceiver.unregister(); - mNotificationManager.cancel(NOTIFICATION_TAG, SystemMessage.NOTE_TV_PIP); } - private Notification.Action getToggleAction(boolean expanded) { - if (expanded) { - return createSystemAction(R.drawable.pip_ic_collapse, - R.string.pip_collapse, ACTION_TOGGLE_EXPANDED_PIP); - } else { - return createSystemAction(R.drawable.pip_ic_expand, R.string.pip_expand, - ACTION_TOGGLE_EXPANDED_PIP); - } - } - - private Notification.Action createSystemAction(int iconRes, int titleRes, String action) { - Notification.Action.Builder builder = new Notification.Action.Builder( - Icon.createWithResource(mContext, iconRes), - mContext.getString(titleRes), - createPendingIntent(mContext, action)); - builder.setContextual(true); - return builder.build(); - } - - private void onMediaActionsChanged(List<RemoteAction> actions) { - mMediaActions.clear(); - mMediaActions.addAll(actions); - if (mCustomActions.isEmpty()) { - updateNotificationContent(); - } - } - private void onMediaSessionTokenChanged(MediaSession.Token token) { mMediaSessionToken = token; updateNotificationContent(); } - private Notification.Action remoteToNotificationAction(RemoteAction action) { - return remoteToNotificationAction(action, SEMANTIC_ACTION_NONE); - } - - private Notification.Action remoteToNotificationAction(RemoteAction action, - int semanticAction) { - Notification.Action.Builder builder = new Notification.Action.Builder(action.getIcon(), - action.getTitle(), - action.getActionIntent()); - if (action.getContentDescription() != null) { - Bundle extras = new Bundle(); - extras.putCharSequence(Notification.EXTRA_PICTURE_CONTENT_DESCRIPTION, - action.getContentDescription()); - builder.addExtras(extras); - } - builder.setSemanticAction(semanticAction); - builder.setContextual(true); - return builder.build(); - } - - private Notification.Action[] getNotificationActions() { - final List<Notification.Action> actions = new ArrayList<>(); - - // 1. Fullscreen - actions.add(getFullscreenAction()); - // 2. Close - actions.add(getCloseAction()); - // 3. App actions - final List<RemoteAction> appActions = - mCustomActions.isEmpty() ? mMediaActions : mCustomActions; - for (RemoteAction appAction : appActions) { - if (PipUtils.remoteActionsMatch(mCustomCloseAction, appAction) - || !appAction.isEnabled()) { - continue; - } - actions.add(remoteToNotificationAction(appAction)); - } - // 4. Move - actions.add(getMoveAction()); - // 5. Toggle expansion (if expanded PiP enabled) - if (mTvPipBoundsState.getDesiredTvExpandedAspectRatio() > 0 - && mTvPipBoundsState.isTvExpandedPipSupported()) { - actions.add(getToggleAction(mTvPipBoundsState.isTvPipExpanded())); - } - return actions.toArray(new Notification.Action[0]); - } - - private Notification.Action getCloseAction() { - if (mCustomCloseAction == null) { - return createSystemAction(R.drawable.pip_ic_close_white, R.string.pip_close, - ACTION_CLOSE_PIP); - } else { - return remoteToNotificationAction(mCustomCloseAction, SEMANTIC_ACTION_DELETE); - } - } - - private Notification.Action getFullscreenAction() { - return createSystemAction(R.drawable.pip_ic_fullscreen_white, - R.string.pip_fullscreen, ACTION_FULLSCREEN); - } - - private Notification.Action getMoveAction() { - return createSystemAction(R.drawable.pip_ic_move_white, R.string.pip_move, - ACTION_MOVE_PIP); - } - - /** - * Called by {@link TvPipController} when the configuration is changed. - */ - void onConfigurationChanged(Context context) { - mDefaultTitle = context.getResources().getString(R.string.pip_notification_unknown_title); - updateNotificationContent(); - } - - void updateExpansionState() { - updateNotificationContent(); - } - private void updateNotificationContent() { if (mPackageManager == null || !mIsNotificationShown) { return; } - Notification.Action[] actions = getNotificationActions(); ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: update(), title: %s, subtitle: %s, mediaSessionToken: %s, #actions: %s", TAG, - getNotificationTitle(), mPipSubtitle, mMediaSessionToken, actions.length); - for (Notification.Action action : actions) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: action: %s", TAG, - action.toString()); - } - + getNotificationTitle(), mPipSubtitle, mMediaSessionToken, mPipActions.length); mNotificationBuilder .setWhen(System.currentTimeMillis()) .setContentTitle(getNotificationTitle()) .setContentText(mPipSubtitle) .setSubText(getApplicationLabel(mPackageName)) - .setActions(actions); + .setActions(mPipActions); setPipIcon(); Bundle extras = new Bundle(); extras.putParcelable(Notification.EXTRA_MEDIA_SESSION, mMediaSessionToken); mNotificationBuilder.setExtras(extras); + PendingIntent closeIntent = mTvPipActionsProvider.getCloseAction().getPendingIntent(); + mNotificationBuilder.setDeleteIntent(closeIntent); // TvExtender not recognized if not set last. mNotificationBuilder.extend(new Notification.TvExtender() - .setContentIntent(createPendingIntent(mContext, ACTION_SHOW_PIP_MENU)) - .setDeleteIntent(createPendingIntent(mContext, ACTION_CLOSE_PIP))); + .setContentIntent( + createPendingIntent(mContext, TvPipController.ACTION_SHOW_PIP_MENU)) + .setDeleteIntent(closeIntent)); + mNotificationManager.notify(NOTIFICATION_TAG, SystemMessage.NOTE_TV_PIP, mNotificationBuilder.build()); } @@ -389,68 +237,20 @@ public class TvPipNotificationController { return ImageUtils.buildScaledBitmap(drawable, width, height, /* allowUpscaling */ true); } - private static PendingIntent createPendingIntent(Context context, String action) { + static PendingIntent createPendingIntent(Context context, String action) { return PendingIntent.getBroadcast(context, 0, new Intent(action).setPackage(context.getPackageName()), PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); } - private class ActionBroadcastReceiver extends BroadcastReceiver { - final IntentFilter mIntentFilter; - { - mIntentFilter = new IntentFilter(); - mIntentFilter.addAction(ACTION_CLOSE_PIP); - mIntentFilter.addAction(ACTION_SHOW_PIP_MENU); - mIntentFilter.addAction(ACTION_MOVE_PIP); - mIntentFilter.addAction(ACTION_TOGGLE_EXPANDED_PIP); - mIntentFilter.addAction(ACTION_FULLSCREEN); - } - 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(); - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: on(Broadcast)Receive(), action=%s", TAG, action); - - if (ACTION_SHOW_PIP_MENU.equals(action)) { - mDelegate.showPictureInPictureMenu(); - } else if (ACTION_CLOSE_PIP.equals(action)) { - mDelegate.closePip(); - } else if (ACTION_MOVE_PIP.equals(action)) { - mDelegate.enterPipMovementMenu(); - } else if (ACTION_TOGGLE_EXPANDED_PIP.equals(action)) { - mDelegate.togglePipExpansion(); - } else if (ACTION_FULLSCREEN.equals(action)) { - mDelegate.movePipToFullscreen(); - } + @Override + public void onActionsChanged(int added, int updated, int startIndex) { + List<TvPipAction> actions = mTvPipActionsProvider.getActionsList(); + mPipActions = new Notification.Action[actions.size()]; + for (int i = 0; i < mPipActions.length; i++) { + mPipActions[i] = actions.get(i).toNotificationAction(mContext); } + updateNotificationContent(); } - interface Delegate { - void showPictureInPictureMenu(); - - void closePip(); - - void enterPipMovementMenu(); - - void togglePipExpansion(); - - void movePipToFullscreen(); - } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipSystemAction.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipSystemAction.java new file mode 100644 index 000000000000..4b82e4bdb64a --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipSystemAction.java @@ -0,0 +1,84 @@ +/* + * 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.tv; + +import static android.app.Notification.Action.SEMANTIC_ACTION_DELETE; +import static android.app.Notification.Action.SEMANTIC_ACTION_NONE; + +import android.annotation.DrawableRes; +import android.annotation.NonNull; +import android.annotation.StringRes; +import android.app.Notification; +import android.app.PendingIntent; +import android.content.Context; +import android.graphics.drawable.Icon; +import android.os.Handler; + +import com.android.wm.shell.common.TvWindowMenuActionButton; + +/** + * A TvPipAction for actions that the system provides, i.e. fullscreen, default close, move, + * expand/collapse. + */ +public class TvPipSystemAction extends TvPipAction { + + @StringRes + private int mTitleResource; + @DrawableRes + private int mIconResource; + + private final PendingIntent mBroadcastIntent; + + TvPipSystemAction(@ActionType int actionType, @StringRes int title, @DrawableRes int icon, + String broadcastAction, @NonNull Context context, + SystemActionsHandler systemActionsHandler) { + super(actionType, systemActionsHandler); + update(title, icon); + mBroadcastIntent = TvPipNotificationController.createPendingIntent(context, + broadcastAction); + } + + void update(@StringRes int title, @DrawableRes int icon) { + mTitleResource = title; + mIconResource = icon; + } + + void populateButton(@NonNull TvWindowMenuActionButton button, Handler mainHandler) { + button.setTextAndDescription(mTitleResource); + button.setImageResource(mIconResource); + button.setEnabled(true); + button.setIsCustomCloseAction(false); + } + + PendingIntent getPendingIntent() { + return mBroadcastIntent; + } + + @Override + Notification.Action toNotificationAction(Context context) { + Notification.Action.Builder builder = new Notification.Action.Builder( + Icon.createWithResource(context, mIconResource), + context.getString(mTitleResource), + mBroadcastIntent); + + builder.setSemanticAction(isCloseAction() + ? SEMANTIC_ACTION_DELETE : SEMANTIC_ACTION_NONE); + builder.setContextual(true); + return builder.build(); + } + +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTaskOrganizer.java index 42fd1aab44f8..0940490e9944 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTaskOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTaskOrganizer.java @@ -28,6 +28,7 @@ import com.android.wm.shell.common.SyncTransactionQueue; 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.PipDisplayLayoutState; import com.android.wm.shell.pip.PipMenuController; import com.android.wm.shell.pip.PipParamsChangedForwarder; import com.android.wm.shell.pip.PipSurfaceTransactionHelper; @@ -50,6 +51,7 @@ public class TvPipTaskOrganizer extends PipTaskOrganizer { @NonNull SyncTransactionQueue syncTransactionQueue, @NonNull PipTransitionState pipTransitionState, @NonNull PipBoundsState pipBoundsState, + @NonNull PipDisplayLayoutState pipDisplayLayoutState, @NonNull PipBoundsAlgorithm boundsHandler, @NonNull PipMenuController pipMenuController, @NonNull PipAnimationController pipAnimationController, @@ -61,10 +63,11 @@ public class TvPipTaskOrganizer extends PipTaskOrganizer { @NonNull PipUiEventLogger pipUiEventLogger, @NonNull ShellTaskOrganizer shellTaskOrganizer, ShellExecutor mainExecutor) { - super(context, syncTransactionQueue, pipTransitionState, pipBoundsState, boundsHandler, - pipMenuController, pipAnimationController, surfaceTransactionHelper, - pipTransitionController, pipParamsChangedForwarder, splitScreenOptional, - displayController, pipUiEventLogger, shellTaskOrganizer, mainExecutor); + super(context, syncTransactionQueue, pipTransitionState, pipBoundsState, + pipDisplayLayoutState, boundsHandler, pipMenuController, pipAnimationController, + surfaceTransactionHelper, pipTransitionController, pipParamsChangedForwarder, + splitScreenOptional, displayController, pipUiEventLogger, shellTaskOrganizer, + mainExecutor); } @Override @@ -82,4 +85,17 @@ public class TvPipTaskOrganizer extends PipTaskOrganizer { mPipParamsChangedForwarder.notifySubtitleChanged(params.getSubtitle()); } } + + /** + * Override for TV since the menu bounds affect the PiP location. Additionally, we want to + * ensure that menu is shown immediately since it should always be visible on TV as it creates + * a border with rounded corners around the PiP. + */ + protected boolean shouldAttachMenuEarly() { + return true; + } + + protected boolean shouldAlwaysFadeIn() { + return true; + } } 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..d3253a5e4d94 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 @@ -16,57 +16,43 @@ package com.android.wm.shell.pip.tv; -import android.app.TaskInfo; -import android.graphics.Rect; -import android.os.IBinder; -import android.view.SurfaceControl; -import android.window.TransitionInfo; -import android.window.TransitionRequestInfo; -import android.window.WindowContainerTransaction; +import android.content.Context; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import com.android.wm.shell.ShellTaskOrganizer; 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.pip.PipDisplayLayoutState; +import com.android.wm.shell.pip.PipSurfaceTransactionHelper; +import com.android.wm.shell.pip.PipTransition; +import com.android.wm.shell.pip.PipTransitionState; +import com.android.wm.shell.splitscreen.SplitScreenController; +import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.transition.Transitions; +import java.util.Optional; + /** * PiP Transition for TV. - * TODO: Implement animation once TV is using Transitions. */ -public class TvPipTransition extends PipTransitionController { - public TvPipTransition(PipBoundsState pipBoundsState, - PipMenuController pipMenuController, +public class TvPipTransition extends PipTransition { + + public TvPipTransition(Context context, + @NonNull ShellInit shellInit, + @NonNull ShellTaskOrganizer shellTaskOrganizer, + @NonNull Transitions transitions, + TvPipBoundsState tvPipBoundsState, + PipDisplayLayoutState pipDisplayLayoutState, + PipTransitionState pipTransitionState, + TvPipMenuController tvPipMenuController, TvPipBoundsAlgorithm tvPipBoundsAlgorithm, PipAnimationController pipAnimationController, - Transitions transitions, - @NonNull ShellTaskOrganizer shellTaskOrganizer) { - super(pipBoundsState, pipMenuController, tvPipBoundsAlgorithm, pipAnimationController, - transitions, shellTaskOrganizer); - } - - @Override - public void onFinishResize(TaskInfo taskInfo, Rect destinationBounds, int direction, - SurfaceControl.Transaction tx) { - + PipSurfaceTransactionHelper pipSurfaceTransactionHelper, + Optional<SplitScreenController> splitScreenOptional) { + super(context, shellInit, shellTaskOrganizer, transitions, tvPipBoundsState, + pipDisplayLayoutState, pipTransitionState, tvPipMenuController, + tvPipBoundsAlgorithm, pipAnimationController, pipSurfaceTransactionHelper, + splitScreenOptional); } - @Override - public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, - @NonNull SurfaceControl.Transaction startTransaction, - @android.annotation.NonNull SurfaceControl.Transaction finishTransaction, - @NonNull Transitions.TransitionFinishCallback finishCallback) { - return false; - } - - @Nullable - @Override - public WindowContainerTransaction handleRequest(@NonNull IBinder transition, - @NonNull TransitionRequestInfo request) { - return null; - } } 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..c9b3a1af6507 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,9 +40,17 @@ 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_SPLIT_SCREEN(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false, + 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, true, + Consts.TAG_WM_SPLIT_SCREEN), + WM_SHELL_SYSUI_EVENTS(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false, + Consts.TAG_WM_SHELL), + WM_SHELL_DESKTOP_MODE(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false, + Consts.TAG_WM_SHELL), + WM_SHELL_FLOATING_APPS(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false, + Consts.TAG_WM_SHELL), + WM_SHELL_FOLDABLE(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false, Consts.TAG_WM_SHELL), TEST_GROUP(true, true, false, "WindowManagerShellProtoLogTest"); @@ -102,6 +112,7 @@ public enum ShellProtoLogGroup implements IProtoLogGroup { private static class Consts { private static final String TAG_WM_SHELL = "WindowManagerShell"; private static final String TAG_WM_STARTING_WINDOW = "ShellStartingWindow"; + private static final String TAG_WM_SPLIT_SCREEN = "ShellSplitScreen"; private static final boolean ENABLE_DEBUG = true; private static final boolean ENABLE_LOG_TO_PROTO_DEBUG = true; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogImpl.java b/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogImpl.java index 552ebde05274..93ffb3dc8115 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogImpl.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogImpl.java @@ -17,22 +17,14 @@ package com.android.wm.shell.protolog; import android.annotation.Nullable; -import android.content.Context; -import android.util.Log; -import com.android.internal.annotations.VisibleForTesting; import com.android.internal.protolog.BaseProtoLogImpl; import com.android.internal.protolog.ProtoLogViewerConfigReader; import com.android.internal.protolog.common.IProtoLogGroup; -import com.android.wm.shell.R; import java.io.File; -import java.io.IOException; -import java.io.InputStream; import java.io.PrintWriter; -import org.json.JSONException; - /** * A service for the ProtoLog logging system. @@ -40,8 +32,9 @@ import org.json.JSONException; public class ShellProtoLogImpl extends BaseProtoLogImpl { private static final String TAG = "ProtoLogImpl"; private static final int BUFFER_CAPACITY = 1024 * 1024; - // TODO: Get the right path for the proto log file when we initialize the shell components - private static final String LOG_FILENAME = new File("wm_shell_log.pb").getAbsolutePath(); + // TODO: find a proper location to save the protolog message file + private static final String LOG_FILENAME = "/data/misc/wmtrace/shell_log.winscope"; + private static final String VIEWER_CONFIG_FILENAME = "/system_ext/etc/wmshell.protolog.json.gz"; private static ShellProtoLogImpl sServiceInstance = null; @@ -111,18 +104,8 @@ public class ShellProtoLogImpl extends BaseProtoLogImpl { } public int startTextLogging(String[] groups, PrintWriter pw) { - try (InputStream is = - getClass().getClassLoader().getResourceAsStream("wm_shell_protolog.json")){ - mViewerConfig.loadViewerConfig(is); - return setLogging(true /* setTextLogging */, true, pw, groups); - } catch (IOException e) { - Log.i(TAG, "Unable to load log definitions: IOException while reading " - + "wm_shell_protolog. " + e); - } catch (JSONException e) { - Log.i(TAG, "Unable to load log definitions: JSON parsing exception while reading " - + "wm_shell_protolog. " + e); - } - return -1; + mViewerConfig.loadViewerConfig(pw, VIEWER_CONFIG_FILENAME); + return setLogging(true /* setTextLogging */, true, pw, groups); } public int stopTextLogging(String[] groups, PrintWriter pw) { @@ -130,7 +113,8 @@ public class ShellProtoLogImpl extends BaseProtoLogImpl { } private ShellProtoLogImpl() { - super(new File(LOG_FILENAME), null, BUFFER_CAPACITY, new ProtoLogViewerConfigReader()); + super(new File(LOG_FILENAME), VIEWER_CONFIG_FILENAME, BUFFER_CAPACITY, + new ProtoLogViewerConfigReader()); } } 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..4048c5b8feab 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,13 @@ package com.android.wm.shell.recents; +import android.app.ActivityManager.RunningTaskInfo; +import android.app.IApplicationThread; +import android.app.PendingIntent; +import android.content.Intent; +import android.os.Bundle; +import android.view.IRecentsAnimationRunner; + import com.android.wm.shell.recents.IRecentTasksListener; import com.android.wm.shell.util.GroupedRecentTaskInfo; @@ -38,4 +45,15 @@ interface IRecentTasks { * Gets the set of recent tasks. */ GroupedRecentTaskInfo[] getRecentTasks(int maxNum, int flags, int userId) = 3; + + /** + * Gets the set of running tasks. + */ + RunningTaskInfo[] getRunningTasks(int maxNum) = 4; + + /** + * Starts a recents transition. + */ + oneway void startRecentsTransition(in PendingIntent intent, in Intent fillIn, in Bundle options, + IApplicationThread appThread, IRecentsAnimationRunner listener) = 5; } 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..e8f58fe2bfad 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.RunningTaskInfo; + /** * 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(); -}
\ No newline at end of file + + /** + * Called when a running task appears. + */ + void onRunningTaskAppeared(in RunningTaskInfo taskInfo); + + /** + * Called when a running task vanishes. + */ + void onRunningTaskVanished(in RunningTaskInfo taskInfo); +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasks.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasks.java index a5748f69388f..069066e4bd49 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasks.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasks.java @@ -17,6 +17,11 @@ package com.android.wm.shell.recents; import com.android.wm.shell.common.annotations.ExternalThread; +import com.android.wm.shell.util.GroupedRecentTaskInfo; + +import java.util.List; +import java.util.concurrent.Executor; +import java.util.function.Consumer; /** * Interface for interacting with the recent tasks. @@ -24,9 +29,9 @@ import com.android.wm.shell.common.annotations.ExternalThread; @ExternalThread public interface RecentTasks { /** - * Returns a binder that can be passed to an external process to fetch recent tasks. + * Gets the set of recent tasks. */ - default IRecentTasks createExternalInterface() { - return null; + default void getRecentTasks(int maxNum, int flags, int userId, Executor callbackExecutor, + Consumer<List<GroupedRecentTaskInfo>> callback) { } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java index c166178e9bbd..c5bfd8753994 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,17 +17,25 @@ package com.android.wm.shell.recents; import static android.app.ActivityTaskManager.INVALID_TASK_ID; +import static android.content.pm.PackageManager.FEATURE_PC; import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission; +import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_RECENT_TASKS; import android.app.ActivityManager; import android.app.ActivityTaskManager; +import android.app.IApplicationThread; +import android.app.PendingIntent; import android.app.TaskInfo; +import android.content.ComponentName; import android.content.Context; +import android.content.Intent; +import android.os.Bundle; import android.os.RemoteException; import android.util.Slog; import android.util.SparseArray; import android.util.SparseIntArray; +import android.view.IRecentsAnimationRunner; import androidx.annotation.BinderThread; import androidx.annotation.NonNull; @@ -35,6 +43,7 @@ import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import com.android.internal.protolog.common.ProtoLog; +import com.android.wm.shell.common.ExternalInterfaceBinder; import com.android.wm.shell.common.RemoteCallable; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SingleInstanceRemoteListener; @@ -42,39 +51,53 @@ import com.android.wm.shell.common.TaskStackListenerCallback; import com.android.wm.shell.common.TaskStackListenerImpl; import com.android.wm.shell.common.annotations.ExternalThread; import com.android.wm.shell.common.annotations.ShellMainThread; +import com.android.wm.shell.desktopmode.DesktopModeStatus; +import com.android.wm.shell.desktopmode.DesktopModeTaskRepository; import com.android.wm.shell.protolog.ShellProtoLogGroup; +import com.android.wm.shell.sysui.ShellCommandHandler; +import com.android.wm.shell.sysui.ShellController; +import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.util.GroupedRecentTaskInfo; -import com.android.wm.shell.util.StagedSplitBounds; +import com.android.wm.shell.util.SplitBounds; import java.io.PrintWriter; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; +import java.util.concurrent.Executor; +import java.util.function.Consumer; /** * Manages the recent task list from the system, caching it as necessary. */ public class RecentTasksController implements TaskStackListenerCallback, - RemoteCallable<RecentTasksController> { + RemoteCallable<RecentTasksController>, DesktopModeTaskRepository.ActiveTasksListener { private static final String TAG = RecentTasksController.class.getSimpleName(); private final Context mContext; + private final ShellController mShellController; + private final ShellCommandHandler mShellCommandHandler; + private final Optional<DesktopModeTaskRepository> mDesktopModeTaskRepository; private final ShellExecutor mMainExecutor; private final TaskStackListenerImpl mTaskStackListener; - private final RecentTasks mImpl = new RecentTasksImpl(); + private final RecentTasksImpl mImpl = new RecentTasksImpl(); + private final ActivityTaskManager mActivityTaskManager; + private RecentsTransitionHandler mTransitionHandler = null; + 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 +106,64 @@ public class RecentTasksController implements TaskStackListenerCallback, @Nullable public static RecentTasksController create( Context context, + ShellInit shellInit, + ShellController shellController, + ShellCommandHandler shellCommandHandler, TaskStackListenerImpl taskStackListener, + ActivityTaskManager activityTaskManager, + Optional<DesktopModeTaskRepository> desktopModeTaskRepository, @ShellMainThread ShellExecutor mainExecutor ) { if (!context.getResources().getBoolean(com.android.internal.R.bool.config_hasRecents)) { return null; } - return new RecentTasksController(context, taskStackListener, mainExecutor); + return new RecentTasksController(context, shellInit, shellController, shellCommandHandler, + taskStackListener, activityTaskManager, desktopModeTaskRepository, mainExecutor); } - RecentTasksController(Context context, TaskStackListenerImpl taskStackListener, + RecentTasksController(Context context, + ShellInit shellInit, + ShellController shellController, + ShellCommandHandler shellCommandHandler, + TaskStackListenerImpl taskStackListener, + ActivityTaskManager activityTaskManager, + Optional<DesktopModeTaskRepository> desktopModeTaskRepository, ShellExecutor mainExecutor) { mContext = context; + mShellController = shellController; + mShellCommandHandler = shellCommandHandler; + mActivityTaskManager = activityTaskManager; + mIsDesktopMode = mContext.getPackageManager().hasSystemFeature(FEATURE_PC); mTaskStackListener = taskStackListener; + mDesktopModeTaskRepository = desktopModeTaskRepository; mMainExecutor = mainExecutor; + shellInit.addInitCallback(this::onInit, this); } public RecentTasks asRecentTasks() { return mImpl; } - public void init() { + private ExternalInterfaceBinder createExternalInterface() { + return new IRecentTasksImpl(this); + } + + private void onInit() { + mShellController.addExternalInterface(KEY_EXTRA_SHELL_RECENT_TASKS, + this::createExternalInterface, this); + mShellCommandHandler.addDumpCallback(this::dump, this); mTaskStackListener.addListener(this); + mDesktopModeTaskRepository.ifPresent(it -> it.addActiveTaskListener(this)); + } + + void setTransitionHandler(RecentsTransitionHandler handler) { + mTransitionHandler = handler; } /** * Adds a split pair. This call does not validate the taskIds, only that they are not the same. */ - public void addSplitPair(int taskId1, int taskId2, StagedSplitBounds splitBounds) { + public void addSplitPair(int taskId1, int taskId2, SplitBounds splitBounds) { if (taskId1 == taskId2) { return; } @@ -176,44 +229,87 @@ public class RecentTasksController implements TaskStackListenerCallback, notifyRecentTasksChanged(); } - public void onTaskRemoved(TaskInfo taskInfo) { + public void onTaskAdded(ActivityManager.RunningTaskInfo taskInfo) { + notifyRunningTaskAppeared(taskInfo); + } + + public void onTaskRemoved(ActivityManager.RunningTaskInfo taskInfo) { // Remove any split pairs associated with this task removeSplitPair(taskInfo.taskId); notifyRecentTasksChanged(); + notifyRunningTaskVanished(taskInfo); } public void onTaskWindowingModeChanged(TaskInfo taskInfo) { notifyRecentTasksChanged(); } + @Override + public void onActiveTasksChanged() { + notifyRecentTasksChanged(); + } + @VisibleForTesting void notifyRecentTasksChanged() { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENT_TASKS, "Notify recent tasks changed"); - for (int i = 0; i < mCallbacks.size(); i++) { - mCallbacks.get(i).run(); + if (mListener == null) { + return; + } + try { + mListener.onRecentTasksChanged(); + } catch (RemoteException e) { + Slog.w(TAG, "Failed call notifyRecentTasksChanged", e); + } + } + + /** + * Notify the running task listener that a task appeared on desktop environment. + */ + private void notifyRunningTaskAppeared(ActivityManager.RunningTaskInfo taskInfo) { + if (mListener == null || !mIsDesktopMode || taskInfo.realActivity == null) { + return; + } + try { + mListener.onRunningTaskAppeared(taskInfo); + } catch (RemoteException e) { + Slog.w(TAG, "Failed call onRunningTaskAppeared", e); } } - private void registerRecentTasksListener(Runnable listener) { - if (!mCallbacks.contains(listener)) { - mCallbacks.add(listener); + /** + * Notify the running task listener that a task was removed on desktop environment. + */ + private void notifyRunningTaskVanished(ActivityManager.RunningTaskInfo taskInfo) { + if (mListener == null || !mIsDesktopMode || taskInfo.realActivity == null) { + return; + } + try { + mListener.onRunningTaskVanished(taskInfo); + } catch (RemoteException e) { + Slog.w(TAG, "Failed call onRunningTaskVanished", e); } } - private void unregisterRecentTasksListener(Runnable listener) { - mCallbacks.remove(listener); + @VisibleForTesting + void registerRecentTasksListener(IRecentTasksListener listener) { + mListener = listener; } @VisibleForTesting - List<ActivityManager.RecentTaskInfo> getRawRecentTasks(int maxNum, int flags, int userId) { - return ActivityTaskManager.getInstance().getRecentTasks(maxNum, flags, userId); + void unregisterRecentTasksListener() { + mListener = null; + } + + @VisibleForTesting + boolean hasRecentTasksListener() { + return mListener != null; } @VisibleForTesting ArrayList<GroupedRecentTaskInfo> getRecentTasks(int maxNum, int flags, int userId) { // Note: the returned task list is from the most-recent to least-recent order - final List<ActivityManager.RecentTaskInfo> rawList = getRawRecentTasks(maxNum, flags, - userId); + final List<ActivityManager.RecentTaskInfo> rawList = mActivityTaskManager.getRecentTasks( + maxNum, flags, userId); // Make a mapping of task id -> task info final SparseArray<ActivityManager.RecentTaskInfo> rawMapping = new SparseArray<>(); @@ -222,6 +318,8 @@ public class RecentTasksController implements TaskStackListenerCallback, rawMapping.put(taskInfo.taskId, taskInfo); } + 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,22 +329,72 @@ public class RecentTasksController implements TaskStackListenerCallback, continue; } + if (DesktopModeStatus.isProto2Enabled() && 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 (pairedTaskId != INVALID_TASK_ID && rawMapping.contains( + pairedTaskId)) { final ActivityManager.RecentTaskInfo pairedTaskInfo = rawMapping.get(pairedTaskId); rawMapping.remove(pairedTaskId); - recentTasks.add(new GroupedRecentTaskInfo(taskInfo, pairedTaskInfo, + recentTasks.add(GroupedRecentTaskInfo.forSplitTasks(taskInfo, pairedTaskInfo, mTaskSplitBoundsMap.get(pairedTaskId))); } else { - recentTasks.add(new GroupedRecentTaskInfo(taskInfo)); + recentTasks.add(GroupedRecentTaskInfo.forSingleTask(taskInfo)); } } + + // Add a special entry for freeform tasks + if (!freeformTasks.isEmpty()) { + recentTasks.add(0, GroupedRecentTaskInfo.forFreeformTasks( + freeformTasks.toArray(new ActivityManager.RecentTaskInfo[0]))); + } + return recentTasks; } + /** + * Returns the top running leaf task. + */ + @Nullable + public ActivityManager.RunningTaskInfo getTopRunningTask() { + List<ActivityManager.RunningTaskInfo> tasks = mActivityTaskManager.getTasks(1, + false /* filterOnlyVisibleRecents */); + return tasks.isEmpty() ? null : tasks.get(0); + } + + /** + * Find the background task that match the given component. + */ + @Nullable + public ActivityManager.RecentTaskInfo findTaskInBackground(ComponentName componentName) { + if (componentName == null) { + return null; + } + List<ActivityManager.RecentTaskInfo> tasks = mActivityTaskManager.getRecentTasks( + Integer.MAX_VALUE, ActivityManager.RECENT_IGNORE_UNAVAILABLE, + ActivityManager.getCurrentUser()); + for (int i = 0; i < tasks.size(); i++) { + final ActivityManager.RecentTaskInfo task = tasks.get(i); + if (task.isVisible) { + continue; + } + if (componentName.equals(task.baseIntent.getComponent())) { + return task; + } + } + return null; + } + public void dump(@NonNull PrintWriter pw, String prefix) { final String innerPrefix = prefix + " "; pw.println(prefix + TAG); + pw.println(prefix + " mListener=" + mListener); + pw.println(prefix + "Tasks:"); ArrayList<GroupedRecentTaskInfo> recentTasks = getRecentTasks(Integer.MAX_VALUE, ActivityManager.RECENT_IGNORE_UNAVAILABLE, ActivityManager.getCurrentUser()); for (int i = 0; i < recentTasks.size(); i++) { @@ -259,15 +407,14 @@ public class RecentTasksController implements TaskStackListenerCallback, */ @ExternalThread private class RecentTasksImpl implements RecentTasks { - private IRecentTasksImpl mIRecentTasks; - @Override - public IRecentTasks createExternalInterface() { - if (mIRecentTasks != null) { - mIRecentTasks.invalidate(); - } - mIRecentTasks = new IRecentTasksImpl(RecentTasksController.this); - return mIRecentTasks; + public void getRecentTasks(int maxNum, int flags, int userId, Executor executor, + Consumer<List<GroupedRecentTaskInfo>> callback) { + mMainExecutor.execute(() -> { + List<GroupedRecentTaskInfo> tasks = + RecentTasksController.this.getRecentTasks(maxNum, flags, userId); + executor.execute(() -> callback.accept(tasks)); + }); } } @@ -276,30 +423,43 @@ public class RecentTasksController implements TaskStackListenerCallback, * The interface for calls from outside the host process. */ @BinderThread - private static class IRecentTasksImpl extends IRecentTasks.Stub { + private static class IRecentTasksImpl extends IRecentTasks.Stub + implements ExternalInterfaceBinder { 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()); } /** * Invalidates this instance, preventing future calls from updating the controller. */ - void invalidate() { + @Override + public void invalidate() { mController = null; + // Unregister the listener to ensure any registered binder death recipients are unlinked + mListener.unregister(); } @Override @@ -331,5 +491,29 @@ 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]; + } + + @Override + public void startRecentsTransition(PendingIntent intent, Intent fillIn, Bundle options, + IApplicationThread appThread, IRecentsAnimationRunner listener) { + if (mController.mTransitionHandler == null) { + Slog.e(TAG, "Used shell-transitions startRecentsTransition without" + + " shell-transitions"); + return; + } + executeRemoteCallWithTaskPermission(mController, "startRecentsTransition", + (controller) -> controller.mTransitionHandler.startRecentsTransition( + intent, fillIn, options, appThread, listener)); + } } -}
\ No newline at end of file +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java new file mode 100644 index 000000000000..c8d6a5e8e00b --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java @@ -0,0 +1,791 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.recents; + +import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; +import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS; +import static android.view.WindowManager.TRANSIT_CHANGE; +import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_LOCKED; +import static android.view.WindowManager.TRANSIT_SLEEP; +import static android.view.WindowManager.TRANSIT_TO_FRONT; + +import android.annotation.Nullable; +import android.annotation.SuppressLint; +import android.app.ActivityManager; +import android.app.ActivityTaskManager; +import android.app.IApplicationThread; +import android.app.PendingIntent; +import android.content.Intent; +import android.graphics.Rect; +import android.os.Bundle; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.ArrayMap; +import android.util.Slog; +import android.view.IRecentsAnimationController; +import android.view.IRecentsAnimationRunner; +import android.view.RemoteAnimationTarget; +import android.view.SurfaceControl; +import android.window.PictureInPictureSurfaceTransaction; +import android.window.TaskSnapshot; +import android.window.TransitionInfo; +import android.window.TransitionRequestInfo; +import android.window.WindowContainerToken; +import android.window.WindowContainerTransaction; + +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.sysui.ShellInit; +import com.android.wm.shell.transition.Transitions; +import com.android.wm.shell.util.TransitionUtil; + +import java.util.ArrayList; + +/** + * Handles the Recents (overview) animation. Only one of these can run at a time. A recents + * transition must be created via {@link #startRecentsTransition}. Anything else will be ignored. + */ +public class RecentsTransitionHandler implements Transitions.TransitionHandler { + private static final String TAG = "RecentsTransitionHandler"; + + private final Transitions mTransitions; + private final ShellExecutor mExecutor; + private IApplicationThread mAnimApp = null; + private final ArrayList<RecentsController> mControllers = new ArrayList<>(); + + /** + * List of other handlers which might need to mix recents with other things. These are checked + * in the order they are added. Ideally there should only be one. + */ + private final ArrayList<RecentsMixedHandler> mMixers = new ArrayList<>(); + + public RecentsTransitionHandler(ShellInit shellInit, Transitions transitions, + @Nullable RecentTasksController recentTasksController) { + mTransitions = transitions; + mExecutor = transitions.getMainExecutor(); + if (!Transitions.ENABLE_SHELL_TRANSITIONS) return; + if (recentTasksController == null) return; + shellInit.addInitCallback(() -> { + recentTasksController.setTransitionHandler(this); + transitions.addHandler(this); + }, this); + } + + /** Register a mixer handler. {@see RecentsMixedHandler}*/ + public void addMixer(RecentsMixedHandler mixer) { + mMixers.add(mixer); + } + + /** Unregister a Mixed Handler */ + public void removeMixer(RecentsMixedHandler mixer) { + mMixers.remove(mixer); + } + + void startRecentsTransition(PendingIntent intent, Intent fillIn, Bundle options, + IApplicationThread appThread, IRecentsAnimationRunner listener) { + // only care about latest one. + mAnimApp = appThread; + WindowContainerTransaction wct = new WindowContainerTransaction(); + wct.sendPendingIntent(intent, fillIn, options); + final RecentsController controller = new RecentsController(listener); + RecentsMixedHandler mixer = null; + Transitions.TransitionHandler mixedHandler = null; + for (int i = 0; i < mMixers.size(); ++i) { + mixedHandler = mMixers.get(i).handleRecentsRequest(wct); + if (mixedHandler != null) { + mixer = mMixers.get(i); + break; + } + } + final IBinder transition = mTransitions.startTransition(TRANSIT_TO_FRONT, wct, + mixedHandler == null ? this : mixedHandler); + if (mixer != null) { + mixer.setRecentsTransition(transition); + } + if (transition == null) { + controller.cancel(); + return; + } + controller.setTransition(transition); + mControllers.add(controller); + } + + @Override + public WindowContainerTransaction handleRequest(IBinder transition, + TransitionRequestInfo request) { + // do not directly handle requests. Only entry point should be via startRecentsTransition + return null; + } + + private int findController(IBinder transition) { + for (int i = mControllers.size() - 1; i >= 0; --i) { + if (mControllers.get(i).mTransition == transition) return i; + } + return -1; + } + + @Override + public boolean startAnimation(IBinder transition, TransitionInfo info, + SurfaceControl.Transaction startTransaction, + SurfaceControl.Transaction finishTransaction, + Transitions.TransitionFinishCallback finishCallback) { + final int controllerIdx = findController(transition); + if (controllerIdx < 0) return false; + final RecentsController controller = mControllers.get(controllerIdx); + Transitions.setRunningRemoteTransitionDelegate(mAnimApp); + mAnimApp = null; + if (!controller.start(info, startTransaction, finishTransaction, finishCallback)) { + return false; + } + return true; + } + + @Override + public void mergeAnimation(IBinder transition, TransitionInfo info, + SurfaceControl.Transaction t, IBinder mergeTarget, + Transitions.TransitionFinishCallback finishCallback) { + final int targetIdx = findController(mergeTarget); + if (targetIdx < 0) return; + final RecentsController controller = mControllers.get(targetIdx); + controller.merge(info, t, finishCallback); + } + + @Override + public void onTransitionConsumed(IBinder transition, boolean aborted, + SurfaceControl.Transaction finishTransaction) { + final int idx = findController(transition); + if (idx < 0) return; + mControllers.get(idx).cancel(); + } + + /** There is only one of these and it gets reset on finish. */ + private class RecentsController extends IRecentsAnimationController.Stub { + private IRecentsAnimationRunner mListener; + private IBinder.DeathRecipient mDeathHandler; + private Transitions.TransitionFinishCallback mFinishCB = null; + private SurfaceControl.Transaction mFinishTransaction = null; + + /** + * List of tasks that we are switching away from via this transition. Upon finish, these + * pausing tasks will become invisible. + * These need to be ordered since the order must be restored if there is no task-switch. + */ + private ArrayList<TaskState> mPausingTasks = null; + + /** + * List of tasks that we are switching to. Upon finish, these will remain visible and + * on top. + */ + private ArrayList<TaskState> mOpeningTasks = null; + + private WindowContainerToken mPipTask = null; + private WindowContainerToken mRecentsTask = null; + private int mRecentsTaskId = -1; + private TransitionInfo mInfo = null; + private boolean mOpeningSeparateHome = false; + private ArrayMap<SurfaceControl, SurfaceControl> mLeashMap = null; + private PictureInPictureSurfaceTransaction mPipTransaction = null; + private IBinder mTransition = null; + private boolean mKeyguardLocked = false; + private boolean mWillFinishToHome = false; + + /** The animation is idle, waiting for the user to choose a task to switch to. */ + private static final int STATE_NORMAL = 0; + + /** The user chose a new task to switch to and the animation is animating to it. */ + private static final int STATE_NEW_TASK = 1; + + /** The latest state that the recents animation is operating in. */ + private int mState = STATE_NORMAL; + + RecentsController(IRecentsAnimationRunner listener) { + mListener = listener; + mDeathHandler = () -> mExecutor.execute(() -> { + if (mListener == null) return; + if (mFinishCB != null) { + finish(mWillFinishToHome, false /* leaveHint */); + } + }); + try { + mListener.asBinder().linkToDeath(mDeathHandler, 0 /* flags */); + } catch (RemoteException e) { + mListener = null; + } + } + + void setTransition(IBinder transition) { + mTransition = transition; + } + + void cancel() { + // restoring (to-home = false) involves submitting more WM changes, so by default, use + // toHome = true when canceling. + cancel(true /* toHome */); + } + + void cancel(boolean toHome) { + if (mListener != null) { + try { + mListener.onAnimationCanceled(null, null); + } catch (RemoteException e) { + Slog.e(TAG, "Error canceling recents animation", e); + } + } + if (mFinishCB != null) { + finish(toHome, false /* userLeave */); + } else { + cleanUp(); + } + } + + /** + * Sends a cancel message to the recents animation with snapshots. Used to trigger a + * "replace-with-screenshot" like behavior. + */ + private boolean sendCancelWithSnapshots() { + int[] taskIds = null; + TaskSnapshot[] snapshots = null; + if (mPausingTasks.size() > 0) { + taskIds = new int[mPausingTasks.size()]; + snapshots = new TaskSnapshot[mPausingTasks.size()]; + try { + for (int i = 0; i < mPausingTasks.size(); ++i) { + snapshots[i] = ActivityTaskManager.getService().takeTaskSnapshot( + mPausingTasks.get(0).mTaskInfo.taskId); + } + } catch (RemoteException e) { + taskIds = null; + snapshots = null; + } + } + try { + mListener.onAnimationCanceled(taskIds, snapshots); + } catch (RemoteException e) { + Slog.e(TAG, "Error canceling recents animation", e); + return false; + } + return true; + } + + void cleanUp() { + if (mListener != null && mDeathHandler != null) { + mListener.asBinder().unlinkToDeath(mDeathHandler, 0 /* flags */); + mDeathHandler = null; + } + mListener = null; + mFinishCB = null; + // clean-up leash surfacecontrols and anything that might reference them. + if (mLeashMap != null) { + for (int i = 0; i < mLeashMap.size(); ++i) { + mLeashMap.valueAt(i).release(); + } + mLeashMap = null; + } + mFinishTransaction = null; + mPausingTasks = null; + mOpeningTasks = null; + mInfo = null; + mTransition = null; + mControllers.remove(this); + } + + boolean start(TransitionInfo info, SurfaceControl.Transaction t, + SurfaceControl.Transaction finishT, Transitions.TransitionFinishCallback finishCB) { + if (mListener == null || mTransition == null) { + cleanUp(); + return false; + } + // First see if this is a valid recents transition. + boolean hasPausingTasks = false; + for (int i = 0; i < info.getChanges().size(); ++i) { + final TransitionInfo.Change change = info.getChanges().get(i); + if (TransitionUtil.isWallpaper(change)) continue; + if (TransitionUtil.isClosingType(change.getMode())) { + hasPausingTasks = true; + continue; + } + final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo(); + if (taskInfo != null && taskInfo.topActivityType == ACTIVITY_TYPE_RECENTS) { + mRecentsTask = taskInfo.token; + mRecentsTaskId = taskInfo.taskId; + } else if (taskInfo != null && taskInfo.topActivityType == ACTIVITY_TYPE_HOME) { + mRecentsTask = taskInfo.token; + mRecentsTaskId = taskInfo.taskId; + } + } + if (mRecentsTask == null && !hasPausingTasks) { + // Recents is already running apparently, so this is a no-op. + Slog.e(TAG, "Tried to start recents while it is already running."); + cleanUp(); + return false; + } + + mInfo = info; + mFinishCB = finishCB; + mFinishTransaction = finishT; + mPausingTasks = new ArrayList<>(); + mOpeningTasks = new ArrayList<>(); + mLeashMap = new ArrayMap<>(); + mKeyguardLocked = (info.getFlags() & TRANSIT_FLAG_KEYGUARD_LOCKED) != 0; + + final ArrayList<RemoteAnimationTarget> apps = new ArrayList<>(); + final ArrayList<RemoteAnimationTarget> wallpapers = new ArrayList<>(); + TransitionUtil.LeafTaskFilter leafTaskFilter = new TransitionUtil.LeafTaskFilter(); + // About layering: we divide up the "layer space" into 3 regions (each the size of + // the change count). This lets us categorize things into above/below/between + // while maintaining their relative ordering. + for (int i = 0; i < info.getChanges().size(); ++i) { + final TransitionInfo.Change change = info.getChanges().get(i); + final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo(); + if (TransitionUtil.isWallpaper(change)) { + final RemoteAnimationTarget target = TransitionUtil.newTarget(change, + // wallpapers go into the "below" layer space + info.getChanges().size() - i, info, t, mLeashMap); + wallpapers.add(target); + // Make all the wallpapers opaque since we want them visible from the start + t.setAlpha(target.leash, 1); + } else if (leafTaskFilter.test(change)) { + // start by putting everything into the "below" layer space. + final RemoteAnimationTarget target = TransitionUtil.newTarget(change, + info.getChanges().size() - i, info, t, mLeashMap); + apps.add(target); + if (TransitionUtil.isClosingType(change.getMode())) { + // raise closing (pausing) task to "above" layer so it isn't covered + t.setLayer(target.leash, info.getChanges().size() * 3 - i); + mPausingTasks.add(new TaskState(change, target.leash)); + if (taskInfo.pictureInPictureParams != null + && taskInfo.pictureInPictureParams.isAutoEnterEnabled()) { + mPipTask = taskInfo.token; + } + } else if (taskInfo != null + && taskInfo.topActivityType == ACTIVITY_TYPE_RECENTS) { + // There's a 3p launcher, so make sure recents goes above that. + t.setLayer(target.leash, info.getChanges().size() * 3 - i); + } else if (taskInfo != null && taskInfo.topActivityType == ACTIVITY_TYPE_HOME) { + // do nothing + } else if (TransitionUtil.isOpeningType(change.getMode())) { + mOpeningTasks.add(new TaskState(change, target.leash)); + } + } + } + t.apply(); + try { + mListener.onAnimationStart(this, + apps.toArray(new RemoteAnimationTarget[apps.size()]), + wallpapers.toArray(new RemoteAnimationTarget[wallpapers.size()]), + new Rect(0, 0, 0, 0), new Rect()); + } catch (RemoteException e) { + Slog.e(TAG, "Error starting recents animation", e); + cancel(); + } + return true; + } + + @SuppressLint("NewApi") + void merge(TransitionInfo info, SurfaceControl.Transaction t, + Transitions.TransitionFinishCallback finishCallback) { + if (mFinishCB == null) { + // This was no-op'd (likely a repeated start) and we've already sent finish. + return; + } + if (info.getType() == TRANSIT_SLEEP) { + // A sleep event means we need to stop animations immediately, so cancel here. + cancel(); + return; + } + ArrayList<TransitionInfo.Change> openingTasks = null; + ArrayList<TransitionInfo.Change> closingTasks = null; + mOpeningSeparateHome = false; + TransitionInfo.Change recentsOpening = null; + boolean foundRecentsClosing = false; + boolean hasChangingApp = false; + final TransitionUtil.LeafTaskFilter leafTaskFilter = + new TransitionUtil.LeafTaskFilter(); + boolean hasTaskChange = false; + for (int i = 0; i < info.getChanges().size(); ++i) { + final TransitionInfo.Change change = info.getChanges().get(i); + final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo(); + if (taskInfo != null + && taskInfo.configuration.windowConfiguration.isAlwaysOnTop()) { + // Tasks that are always on top (e.g. bubbles), will handle their own transition + // as they are on top of everything else. So cancel the merge here. + cancel(); + return; + } + hasTaskChange = hasTaskChange || taskInfo != null; + final boolean isLeafTask = leafTaskFilter.test(change); + if (TransitionUtil.isOpeningType(change.getMode())) { + if (mRecentsTask != null && mRecentsTask.equals(change.getContainer())) { + recentsOpening = change; + } else if (isLeafTask) { + if (taskInfo.topActivityType == ACTIVITY_TYPE_HOME) { + // This is usually a 3p launcher + mOpeningSeparateHome = true; + } + if (openingTasks == null) { + openingTasks = new ArrayList<>(); + } + openingTasks.add(change); + } + } else if (TransitionUtil.isClosingType(change.getMode())) { + if (mRecentsTask != null && mRecentsTask.equals(change.getContainer())) { + foundRecentsClosing = true; + } else if (isLeafTask) { + if (closingTasks == null) { + closingTasks = new ArrayList<>(); + } + closingTasks.add(change); + } + } else if (change.getMode() == TRANSIT_CHANGE) { + // Finish recents animation if the display is changed, so the default + // transition handler can play the animation such as rotation effect. + if (change.hasFlags(TransitionInfo.FLAG_IS_DISPLAY)) { + cancel(mWillFinishToHome); + return; + } + // Don't consider order-only changes as changing apps. + if (!TransitionUtil.isOrderOnly(change)) { + hasChangingApp = true; + } + } + } + if (hasChangingApp && foundRecentsClosing) { + // This happens when a visible app is expanding (usually PiP). In this case, + // that transition probably has a special-purpose animation, so finish recents + // now and let it do its animation (since recents is going to be occluded). + sendCancelWithSnapshots(); + mExecutor.executeDelayed( + () -> finishInner(true /* toHome */, false /* userLeaveHint */), 0); + return; + } + if (recentsOpening != null) { + // the recents task re-appeared. This happens if the user gestures before the + // task-switch (NEW_TASK) animation finishes. + if (mState == STATE_NORMAL) { + Slog.e(TAG, "Returning to recents while recents is already idle."); + } + if (closingTasks == null || closingTasks.size() == 0) { + Slog.e(TAG, "Returning to recents without closing any opening tasks."); + } + // Setup may hide it initially since it doesn't know that overview was still active. + t.show(recentsOpening.getLeash()); + t.setAlpha(recentsOpening.getLeash(), 1.f); + mState = STATE_NORMAL; + } + boolean didMergeThings = false; + if (closingTasks != null) { + // Potentially cancelling a task-switch. Move the tasks back to mPausing if they + // are in mOpening. + for (int i = 0; i < closingTasks.size(); ++i) { + final TransitionInfo.Change change = closingTasks.get(i); + int openingIdx = TaskState.indexOf(mOpeningTasks, change); + if (openingIdx < 0) { + Slog.w(TAG, "Closing a task that wasn't opening, this may be split or" + + " something unexpected: " + change.getTaskInfo().taskId); + continue; + } + mPausingTasks.add(mOpeningTasks.remove(openingIdx)); + didMergeThings = true; + } + } + RemoteAnimationTarget[] appearedTargets = null; + if (openingTasks != null && openingTasks.size() > 0) { + // Switching to some new tasks, add to mOpening and remove from mPausing. Also, + // enter NEW_TASK state since this will start the switch-to animation. + final int layer = mInfo.getChanges().size() * 3; + appearedTargets = new RemoteAnimationTarget[openingTasks.size()]; + for (int i = 0; i < openingTasks.size(); ++i) { + final TransitionInfo.Change change = openingTasks.get(i); + int pausingIdx = TaskState.indexOf(mPausingTasks, change); + if (pausingIdx >= 0) { + // Something is showing/opening a previously-pausing app. + appearedTargets[i] = TransitionUtil.newTarget( + change, layer, mPausingTasks.get(pausingIdx).mLeash); + mOpeningTasks.add(mPausingTasks.remove(pausingIdx)); + // Setup hides opening tasks initially, so make it visible again (since we + // are already showing it). + t.show(change.getLeash()); + t.setAlpha(change.getLeash(), 1.f); + } else { + // We are receiving new opening tasks, so convert to onTasksAppeared. + appearedTargets[i] = TransitionUtil.newTarget( + change, layer, info, t, mLeashMap); + // reparent into the original `mInfo` since that's where we are animating. + final int rootIdx = TransitionUtil.rootIndexFor(change, mInfo); + t.reparent(appearedTargets[i].leash, mInfo.getRoot(rootIdx).getLeash()); + t.setLayer(appearedTargets[i].leash, layer); + mOpeningTasks.add(new TaskState(change, appearedTargets[i].leash)); + } + } + didMergeThings = true; + mState = STATE_NEW_TASK; + } + if (!hasTaskChange) { + // Activity only transition, so consume the merge as it doesn't affect the rest of + // recents. + Slog.d(TAG, "Got an activity only transition during recents, so apply directly"); + mergeActivityOnly(info, t); + } else if (!didMergeThings) { + // Didn't recognize anything in incoming transition so don't merge it. + Slog.w(TAG, "Don't know how to merge this transition."); + return; + } + // At this point, we are accepting the merge. + t.apply(); + // not using the incoming anim-only surfaces + info.releaseAnimSurfaces(); + finishCallback.onTransitionFinished(null /* wct */, null /* wctCB */); + if (appearedTargets == null) return; + try { + mListener.onTasksAppeared(appearedTargets); + } catch (RemoteException e) { + Slog.e(TAG, "Error sending appeared tasks to recents animation", e); + } + } + + /** For now, just set-up a jump-cut to the new activity. */ + private void mergeActivityOnly(TransitionInfo info, SurfaceControl.Transaction t) { + for (int i = 0; i < info.getChanges().size(); ++i) { + final TransitionInfo.Change change = info.getChanges().get(i); + if (TransitionUtil.isOpeningType(change.getMode())) { + t.show(change.getLeash()); + t.setAlpha(change.getLeash(), 1.f); + } else if (TransitionUtil.isClosingType(change.getMode())) { + t.hide(change.getLeash()); + } + } + } + + @Override + public TaskSnapshot screenshotTask(int taskId) { + try { + return ActivityTaskManager.getService().takeTaskSnapshot(taskId); + } catch (RemoteException e) { + Slog.e(TAG, "Failed to screenshot task", e); + } + return null; + } + + @Override + public void setInputConsumerEnabled(boolean enabled) { + mExecutor.execute(() -> { + if (mFinishCB == null || !enabled) return; + // transient launches don't receive focus automatically. Since we are taking over + // the gesture now, take focus explicitly. + // This also moves recents back to top if the user gestured before a switch + // animation finished. + try { + ActivityTaskManager.getService().setFocusedTask(mRecentsTaskId); + } catch (RemoteException e) { + Slog.e(TAG, "Failed to set focused task", e); + } + }); + } + + @Override + public void setAnimationTargetsBehindSystemBars(boolean behindSystemBars) { + } + + @Override + public void setFinishTaskTransaction(int taskId, + PictureInPictureSurfaceTransaction finishTransaction, SurfaceControl overlay) { + mExecutor.execute(() -> { + if (mFinishCB == null) return; + mPipTransaction = finishTransaction; + }); + } + + @Override + @SuppressLint("NewApi") + public void finish(boolean toHome, boolean sendUserLeaveHint) { + mExecutor.execute(() -> finishInner(toHome, sendUserLeaveHint)); + } + + private void finishInner(boolean toHome, boolean sendUserLeaveHint) { + if (mFinishCB == null) { + Slog.e(TAG, "Duplicate call to finish"); + return; + } + final Transitions.TransitionFinishCallback finishCB = mFinishCB; + mFinishCB = null; + + final SurfaceControl.Transaction t = mFinishTransaction; + final WindowContainerTransaction wct = new WindowContainerTransaction(); + + if (mKeyguardLocked && mRecentsTask != null) { + if (toHome) wct.reorder(mRecentsTask, true /* toTop */); + else wct.restoreTransientOrder(mRecentsTask); + } + if (!toHome && !mWillFinishToHome && mPausingTasks != null && mState == STATE_NORMAL) { + // The gesture is returning to the pausing-task(s) rather than continuing with + // recents, so end the transition by moving the app back to the top (and also + // re-showing it's task). + for (int i = mPausingTasks.size() - 1; i >= 0; --i) { + // reverse order so that index 0 ends up on top + wct.reorder(mPausingTasks.get(i).mToken, true /* onTop */); + t.show(mPausingTasks.get(i).mTaskSurface); + } + if (!mKeyguardLocked && mRecentsTask != null) { + wct.restoreTransientOrder(mRecentsTask); + } + } else if (toHome && mOpeningSeparateHome && mPausingTasks != null) { + // Special situation where 3p launcher was changed during recents (this happens + // during tapltests...). Here we get both "return to home" AND "home opening". + // This is basically going home, but we have to restore the recents and home order. + for (int i = 0; i < mOpeningTasks.size(); ++i) { + final TaskState state = mOpeningTasks.get(i); + if (state.mTaskInfo.topActivityType == ACTIVITY_TYPE_HOME) { + // Make sure it is on top. + wct.reorder(state.mToken, true /* onTop */); + } + t.show(state.mTaskSurface); + } + for (int i = mPausingTasks.size() - 1; i >= 0; --i) { + t.hide(mPausingTasks.get(i).mTaskSurface); + } + if (!mKeyguardLocked && mRecentsTask != null) { + wct.restoreTransientOrder(mRecentsTask); + } + } else { + // The general case: committing to recents, going home, or switching tasks. + for (int i = 0; i < mOpeningTasks.size(); ++i) { + t.show(mOpeningTasks.get(i).mTaskSurface); + } + for (int i = 0; i < mPausingTasks.size(); ++i) { + if (!sendUserLeaveHint) { + // This means recents is not *actually* finishing, so of course we gotta + // do special stuff in WMCore to accommodate. + wct.setDoNotPip(mPausingTasks.get(i).mToken); + } + // Since we will reparent out of the leashes, pre-emptively hide the child + // surface to match the leash. Otherwise, there will be a flicker before the + // visibility gets committed in Core when using split-screen (in splitscreen, + // the leaf-tasks are not "independent" so aren't hidden by normal setup). + t.hide(mPausingTasks.get(i).mTaskSurface); + } + if (mPipTask != null && mPipTransaction != null && sendUserLeaveHint) { + t.show(mInfo.getChange(mPipTask).getLeash()); + PictureInPictureSurfaceTransaction.apply(mPipTransaction, + mInfo.getChange(mPipTask).getLeash(), t); + mPipTask = null; + mPipTransaction = null; + } + } + cleanUp(); + finishCB.onTransitionFinished(wct.isEmpty() ? null : wct, null /* wctCB */); + } + + @Override + public void setDeferCancelUntilNextTransition(boolean defer, boolean screenshot) { + } + + @Override + public void cleanupScreenshot() { + } + + @Override + public void setWillFinishToHome(boolean willFinishToHome) { + mExecutor.execute(() -> { + mWillFinishToHome = willFinishToHome; + }); + } + + /** + * @see IRecentsAnimationController#removeTask + */ + @Override + public boolean removeTask(int taskId) { + return false; + } + + /** + * @see IRecentsAnimationController#detachNavigationBarFromApp + */ + @Override + public void detachNavigationBarFromApp(boolean moveHomeToTop) { + mExecutor.execute(() -> { + if (mTransition == null) return; + try { + ActivityTaskManager.getService().detachNavigationBarFromApp(mTransition); + } catch (RemoteException e) { + Slog.e(TAG, "Failed to detach the navigation bar from app", e); + } + }); + } + + /** + * @see IRecentsAnimationController#animateNavigationBarToApp(long) + */ + @Override + public void animateNavigationBarToApp(long duration) { + } + }; + + /** Utility class to track the state of a task as-seen by recents. */ + private static class TaskState { + WindowContainerToken mToken; + ActivityManager.RunningTaskInfo mTaskInfo; + + /** The surface/leash of the task provided by Core. */ + SurfaceControl mTaskSurface; + + /** The (local) animation-leash created for this task. */ + SurfaceControl mLeash; + + TaskState(TransitionInfo.Change change, SurfaceControl leash) { + mToken = change.getContainer(); + mTaskInfo = change.getTaskInfo(); + mTaskSurface = change.getLeash(); + mLeash = leash; + } + + static int indexOf(ArrayList<TaskState> list, TransitionInfo.Change change) { + for (int i = list.size() - 1; i >= 0; --i) { + if (list.get(i).mToken.equals(change.getContainer())) { + return i; + } + } + return -1; + } + + public String toString() { + return "" + mToken + " : " + mLeash; + } + } + + /** + * An interface for a mixed handler to receive information about recents requests (since these + * come into this handler directly vs from WMCore request). + */ + public interface RecentsMixedHandler { + /** + * Called when a recents request comes in. The handler can add operations to outWCT. If + * the handler wants to "accept" the transition, it should return itself; otherwise, it + * should return `null`. + * + * If a mixed-handler accepts this recents, it will be the de-facto handler for this + * transition and is required to call the associated {@link #startAnimation}, + * {@link #mergeAnimation}, and {@link #onTransitionConsumed} methods. + */ + Transitions.TransitionHandler handleRecentsRequest(WindowContainerTransaction outWCT); + + /** + * Reports the transition token associated with the accepted recents request. If there was + * a problem starting the request, this will be called with `null`. + */ + void setRecentsTransition(@Nullable IBinder transition); + } +} 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..81e118a31b73 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,70 @@ 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; + oneway void startTasks(int taskId1, in Bundle options1, int taskId2, in Bundle options2, + int splitPosition, float splitRatio, in RemoteTransition remoteTransition, + in InstanceId instanceId) = 10; + + /** + * Starts a pair of intent and task in one transition. + */ + oneway void startIntentAndTask(in PendingIntent pendingIntent, in Bundle options1, int taskId, + in Bundle options2, int sidePosition, float splitRatio, + in RemoteTransition remoteTransition, in InstanceId instanceId) = 16; + + /** + * Starts a pair of shortcut and task in one transition. + */ + oneway void startShortcutAndTask(in ShortcutInfo shortcutInfo, in Bundle options1, int taskId, + in Bundle options2, int splitPosition, float splitRatio, + in RemoteTransition remoteTransition, in InstanceId instanceId) = 17; /** * 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; + oneway void startTasksWithLegacyTransition(int taskId1, in Bundle options1, int taskId2, + in Bundle options2, int splitPosition, 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; + in Bundle options1, int taskId, in Bundle options2, int splitPosition, float splitRatio, + in RemoteAnimationAdapter adapter, in InstanceId instanceId) = 12; + + /** + * Starts a pair of shortcut and task using legacy transition system. + */ + oneway void startShortcutAndTaskWithLegacyTransition(in ShortcutInfo shortcutInfo, + in Bundle options1, int taskId, in Bundle options2, int splitPosition, float splitRatio, + in RemoteAnimationAdapter adapter, in InstanceId instanceId) = 15; + + /** + * Start a pair of intents using legacy transition system. + */ + oneway void startIntentsWithLegacyTransition(in PendingIntent pendingIntent1, + in ShortcutInfo shortcutInfo1, in Bundle options1, in PendingIntent pendingIntent2, + in ShortcutInfo shortcutInfo2, in Bundle options2, int splitPosition, float splitRatio, + in RemoteAnimationAdapter adapter, in InstanceId instanceId) = 18; + + /** + * Start a pair of intents in one transition. + */ + oneway void startIntents(in PendingIntent pendingIntent1, in Bundle options1, + in PendingIntent pendingIntent2, in Bundle options2, int splitPosition, + float splitRatio, in RemoteTransition remoteTransition, in InstanceId instanceId) = 19; /** * Blocking call that notifies and gets additional split-screen targets when entering @@ -109,3 +147,4 @@ interface ISplitScreen { */ RemoteAnimationTarget[] onStartingSplitLegacy(in RemoteAnimationTarget[] appTargets) = 14; } +// Last id = 19
\ No newline at end of file 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..89538cb394d4 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,6 @@ package com.android.wm.shell.splitscreen; -import android.annotation.Nullable; import android.content.Context; import android.view.SurfaceSession; import android.window.WindowContainerToken; @@ -32,16 +31,13 @@ import com.android.wm.shell.common.SyncTransactionQueue; * @see StageCoordinator */ class MainStage extends StageTaskListener { - private static final String TAG = MainStage.class.getSimpleName(); - private boolean mIsActive = false; 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() { @@ -51,15 +47,8 @@ class MainStage extends StageTaskListener { void activate(WindowContainerTransaction wct, boolean includingTopTask) { if (mIsActive) return; - final WindowContainerToken rootToken = mRootTaskInfo.token; if (includingTopTask) { - wct.reparentTasks( - null /* currentParent */, - rootToken, - CONTROLLED_WINDOWING_MODES, - CONTROLLED_ACTIVITY_TYPES, - true /* onTop */, - true /* reparentTopOnly */); + reparentTopTask(wct); } mIsActive = true; @@ -76,10 +65,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..2f2bc77b804b 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) {} } @@ -68,22 +70,12 @@ public interface SplitScreen { /** 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 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(); + /** Called when requested to go to fullscreen from the current active split app. */ + void goToFullscreenFromSplit(); + /** Get a string representation of a stage type */ static String stageTypeToString(@StageType int stage) { switch (stage) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java index 31b510c38457..7d5ab8428a3e 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,8 @@ package com.android.wm.shell.splitscreen; import static android.app.ActivityManager.START_SUCCESS; import static android.app.ActivityManager.START_TASK_TO_FRONT; +import static android.app.ActivityTaskManager.INVALID_TASK_ID; +import static android.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; @@ -26,18 +28,23 @@ import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTas import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT; import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED; -import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_SIDE; +import static com.android.wm.shell.common.split.SplitScreenUtils.isValidToSplit; +import static com.android.wm.shell.common.split.SplitScreenUtils.reverseSplitPosition; +import static com.android.wm.shell.common.split.SplitScreenUtils.samePackage; import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_UNDEFINED; +import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_SPLIT_SCREEN; import static com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS; +import android.annotation.NonNull; +import android.annotation.Nullable; import android.app.ActivityManager; +import android.app.ActivityOptions; import android.app.ActivityTaskManager; import android.app.PendingIntent; -import android.content.ActivityNotFoundException; -import android.content.ComponentName; +import android.app.TaskInfo; 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,49 +52,54 @@ 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; import android.view.SurfaceSession; import android.view.WindowManager; +import android.widget.Toast; import android.window.RemoteTransition; import android.window.WindowContainerTransaction; import androidx.annotation.BinderThread; 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.R; import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayImeController; import com.android.wm.shell.common.DisplayInsetsController; +import com.android.wm.shell.common.ExternalInterfaceBinder; import com.android.wm.shell.common.RemoteCallable; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SingleInstanceRemoteListener; import com.android.wm.shell.common.SyncTransactionQueue; 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.common.split.SplitScreenUtils; +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.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 +108,21 @@ 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; + public static final int EXIT_REASON_RECREATE_SPLIT = 10; + public static final int EXIT_REASON_FULLSCREEN_SHORTCUT = 11; @IntDef(value = { EXIT_REASON_UNKNOWN, EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW, @@ -120,10 +134,28 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, EXIT_REASON_SCREEN_LOCKED, EXIT_REASON_SCREEN_LOCKED_SHOW_ON_TOP, EXIT_REASON_CHILD_TASK_ENTER_PIP, + EXIT_REASON_RECREATE_SPLIT, + EXIT_REASON_FULLSCREEN_SHORTCUT, }) @Retention(RetentionPolicy.SOURCE) @interface ExitReason{} + public static final int ENTER_REASON_UNKNOWN = 0; + public static final int ENTER_REASON_MULTI_INSTANCE = 1; + public static final int ENTER_REASON_DRAG = 2; + public static final int ENTER_REASON_LAUNCHER = 3; + /** Acts as a mapping to the actual EnterReasons as defined in the logging proto */ + @IntDef(value = { + ENTER_REASON_MULTI_INSTANCE, + ENTER_REASON_DRAG, + ENTER_REASON_LAUNCHER, + ENTER_REASON_UNKNOWN + }) + public @interface SplitEnterReason { + } + + private final ShellCommandHandler mShellCommandHandler; + private final ShellController mShellController; private final ShellTaskOrganizer mTaskOrganizer; private final SyncTransactionQueue mSyncQueue; private final Context mContext; @@ -133,27 +165,40 @@ 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 final String[] mAppsSupportMultiInstances; + + @VisibleForTesting + StageCoordinator mStageCoordinator; - 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 +207,98 @@ 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); + } + + // TODO(255224696): Remove the config once having a way for client apps to opt-in + // multi-instances split. + mAppsSupportMultiInstances = mContext.getResources() + .getStringArray(R.array.config_appsSupportMultiInstancesSplit); + } + + @VisibleForTesting + SplitScreenController(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, + RecentTasksController recentTasks, + ShellExecutor mainExecutor, + StageCoordinator stageCoordinator) { + mShellCommandHandler = shellCommandHandler; + mShellController = shellController; + mTaskOrganizer = shellTaskOrganizer; + mSyncQueue = syncQueue; + mContext = context; + mRootTDAOrganizer = rootTDAOrganizer; + mMainExecutor = mainExecutor; + mDisplayController = displayController; + mDisplayImeController = displayImeController; + mDisplayInsetsController = displayInsetsController; + mDragAndDropController = dragAndDropController; + mTransitions = transitions; + mTransactionPool = transactionPool; + mIconProvider = iconProvider; + mRecentTasksOptional = Optional.of(recentTasks); + mStageCoordinator = stageCoordinator; + mSplitScreenShellCommandHandler = new SplitScreenShellCommandHandler(this); + shellInit.addInitCallback(this::onInit, this); + mAppsSupportMultiInstances = mContext.getResources() + .getStringArray(R.array.config_appsSupportMultiInstancesSplit); } public SplitScreen asSplitScreen() { return mImpl; } + private ExternalInterfaceBinder createExternalInterface() { + return new ISplitScreenImpl(this); + } + + /** + * 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); + mShellController.addExternalInterface(KEY_EXTRA_SHELL_SPLIT_SCREEN, + this::createExternalInterface, 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 +309,14 @@ 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; + } + @Nullable public ActivityManager.RunningTaskInfo getTaskInfo(@SplitPosition int splitPosition) { if (!isSplitScreenVisible() || splitPosition == SPLIT_POSITION_UNDEFINED) { @@ -208,9 +327,14 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, return mTaskOrganizer.getRunningTaskInfo(taskId); } + /** Check task is under split or not by taskId. */ public boolean isTaskInSplitScreen(int taskId) { - return isSplitScreenVisible() - && mStageCoordinator.getStageOfTask(taskId) != STAGE_TYPE_UNDEFINED; + return mStageCoordinator.getStageOfTask(taskId) != STAGE_TYPE_UNDEFINED; + } + + /** Check split is foreground and task is under split or not by taskId. */ + public boolean isTaskInSplitScreenForeground(int taskId) { + return isTaskInSplitScreen(taskId) && isSplitScreenVisible(); } public @SplitPosition int getSplitPosition(int taskId) { @@ -218,17 +342,24 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, } public boolean moveToSideStage(int taskId, @SplitPosition int sideStagePosition) { - return moveToStage(taskId, STAGE_TYPE_SIDE, sideStagePosition, - new WindowContainerTransaction()); + return moveToStage(taskId, sideStagePosition, 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) { + private boolean moveToStage(int taskId, @SplitPosition int stagePosition, + WindowContainerTransaction wct) { final ActivityManager.RunningTaskInfo task = mTaskOrganizer.getRunningTaskInfo(taskId); if (task == null) { throw new IllegalArgumentException("Unknown taskId" + taskId); } - return mStageCoordinator.moveToStage(task, stageType, stagePosition, wct); + return mStageCoordinator.moveToStage(task, stagePosition, wct); } public boolean removeFromSideStage(int taskId) { @@ -253,18 +384,19 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, } public void enterSplitScreen(int taskId, boolean leftOrTop, WindowContainerTransaction wct) { - final int stageType = isSplitScreenVisible() ? STAGE_TYPE_UNDEFINED : STAGE_TYPE_SIDE; final int stagePosition = leftOrTop ? SPLIT_POSITION_TOP_OR_LEFT : SPLIT_POSITION_BOTTOM_OR_RIGHT; - moveToStage(taskId, stageType, stagePosition, wct); + moveToStage(taskId, stagePosition, wct); } public void exitSplitScreen(int toTopTaskId, @ExitReason int exitReason) { 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() { @@ -287,186 +419,370 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, mStageCoordinator.unregisterSplitScreenListener(listener); } + public void goToFullscreenFromSplit() { + mStageCoordinator.goToFullscreenFromSplit(); + } + + /** Move the specified task to fullscreen, regardless of focus state. */ + public void moveTaskToFullscreen(int taskId) { + mStageCoordinator.moveTaskToFullscreen(taskId); + } + + public boolean isLaunchToSplit(TaskInfo taskInfo) { + return mStageCoordinator.isLaunchToSplit(taskInfo); + } + + public int getActivateSplitPosition(TaskInfo taskInfo) { + return mStageCoordinator.getActivateSplitPosition(taskInfo); + } + public void startTask(int taskId, @SplitPosition int position, @Nullable Bundle options) { + final int[] result = new int[1]; + IRemoteAnimationRunner wrapper = new IRemoteAnimationRunner.Stub() { + @Override + public void onAnimationStart(@WindowManager.TransitionOldType int transit, + RemoteAnimationTarget[] apps, + RemoteAnimationTarget[] wallpapers, + RemoteAnimationTarget[] nonApps, + final IRemoteAnimationFinishedCallback finishedCallback) { + try { + finishedCallback.onAnimationFinished(); + } catch (RemoteException e) { + Slog.e(TAG, "Failed to invoke onAnimationFinished", e); + } + if (result[0] == START_SUCCESS || result[0] == START_TASK_TO_FRONT) { + final WindowContainerTransaction evictWct = new WindowContainerTransaction(); + mStageCoordinator.prepareEvictNonOpeningChildTasks(position, apps, evictWct); + mSyncQueue.queue(evictWct); + } + } + @Override + public void onAnimationCancelled(boolean isKeyguardOccluded) { + final WindowContainerTransaction evictWct = new WindowContainerTransaction(); + mStageCoordinator.prepareEvictInvisibleChildTasks(evictWct); + mSyncQueue.queue(evictWct); + } + }; options = mStageCoordinator.resolveStartStage(STAGE_TYPE_UNDEFINED, position, options, null /* wct */); + RemoteAnimationAdapter wrappedAdapter = new RemoteAnimationAdapter(wrapper, + 0 /* duration */, 0 /* statusBarTransitionDelay */); + ActivityOptions activityOptions = ActivityOptions.fromBundle(options); + activityOptions.update(ActivityOptions.makeRemoteAnimation(wrappedAdapter)); try { - final WindowContainerTransaction evictWct = new WindowContainerTransaction(); - mStageCoordinator.prepareEvictChildTasks(position, evictWct); - final int result = - ActivityTaskManager.getService().startActivityFromRecents(taskId, options); - if (result == START_SUCCESS || result == START_TASK_TO_FRONT) { - mSyncQueue.queue(evictWct); - } + result[0] = ActivityTaskManager.getService().startActivityFromRecents(taskId, + activityOptions.toBundle()); } catch (RemoteException e) { Slog.e(TAG, "Failed to launch task", e); } } + /** + * See {@link #startShortcut(String, String, int, Bundle, UserHandle)} + * @param instanceId to be used by {@link SplitscreenEventLogger} + */ + public void startShortcut(String packageName, String shortcutId, @SplitPosition int position, + @Nullable Bundle options, UserHandle user, @NonNull InstanceId instanceId) { + mStageCoordinator.onRequestToSplit(instanceId, ENTER_REASON_LAUNCHER); + startShortcut(packageName, shortcutId, position, options, user); + } + + @Override public void startShortcut(String packageName, String shortcutId, @SplitPosition int position, @Nullable Bundle options, UserHandle user) { - options = mStageCoordinator.resolveStartStage(STAGE_TYPE_UNDEFINED, position, options, - null /* wct */); - final WindowContainerTransaction evictWct = new WindowContainerTransaction(); - mStageCoordinator.prepareEvictChildTasks(position, evictWct); + if (options == null) options = new Bundle(); + final ActivityOptions activityOptions = ActivityOptions.fromBundle(options); + + if (samePackage(packageName, getPackageName(reverseSplitPosition(position)))) { + if (supportMultiInstancesSplit(packageName)) { + activityOptions.setApplyMultipleTaskFlagForShortcut(true); + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Adding MULTIPLE_TASK"); + } else if (isSplitScreenVisible()) { + mStageCoordinator.switchSplitPosition("startShortcut"); + return; + } else { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, + "Cancel entering split as not supporting multi-instances"); + Toast.makeText(mContext, R.string.dock_multi_instances_not_supported_text, + Toast.LENGTH_SHORT).show(); + return; + } + } - try { - LauncherApps launcherApps = - mContext.getSystemService(LauncherApps.class); - launcherApps.startShortcut(packageName, shortcutId, null /* sourceBounds */, - options, user); - mSyncQueue.queue(evictWct); - } catch (ActivityNotFoundException e) { - Slog.e(TAG, "Failed to launch shortcut", e); + mStageCoordinator.startShortcut(packageName, shortcutId, position, + activityOptions.toBundle(), user); + } + + void startShortcutAndTaskWithLegacyTransition(ShortcutInfo shortcutInfo, + @Nullable Bundle options1, int taskId, @Nullable Bundle options2, + @SplitPosition int splitPosition, float splitRatio, RemoteAnimationAdapter adapter, + InstanceId instanceId) { + if (options1 == null) options1 = new Bundle(); + final ActivityOptions activityOptions = ActivityOptions.fromBundle(options1); + + final String packageName1 = shortcutInfo.getPackage(); + final String packageName2 = SplitScreenUtils.getPackageName(taskId, mTaskOrganizer); + if (samePackage(packageName1, packageName2)) { + if (supportMultiInstancesSplit(shortcutInfo.getPackage())) { + activityOptions.setApplyMultipleTaskFlagForShortcut(true); + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Adding MULTIPLE_TASK"); + } else { + taskId = INVALID_TASK_ID; + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, + "Cancel entering split as not supporting multi-instances"); + Toast.makeText(mContext, R.string.dock_multi_instances_not_supported_text, + Toast.LENGTH_SHORT).show(); + } } + + mStageCoordinator.startShortcutAndTaskWithLegacyTransition(shortcutInfo, + activityOptions.toBundle(), taskId, options2, splitPosition, splitRatio, adapter, + instanceId); } + /** + * 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) { - if (!ENABLE_SHELL_TRANSITIONS) { - startIntentLegacy(intent, fillInIntent, position, options); - return; - } + @SplitPosition int position, @Nullable Bundle options, @NonNull InstanceId instanceId) { + mStageCoordinator.onRequestToSplit(instanceId, ENTER_REASON_LAUNCHER); + startIntent(intent, fillInIntent, position, options); + } - try { - options = mStageCoordinator.resolveStartStage(STAGE_TYPE_UNDEFINED, position, options, - null /* wct */); + private void startIntentAndTaskWithLegacyTransition(PendingIntent pendingIntent, + @Nullable Bundle options1, int taskId, @Nullable Bundle options2, + @SplitPosition int splitPosition, float splitRatio, RemoteAnimationAdapter adapter, + InstanceId instanceId) { + Intent fillInIntent = null; + final String packageName1 = SplitScreenUtils.getPackageName(pendingIntent); + final String packageName2 = SplitScreenUtils.getPackageName(taskId, mTaskOrganizer); + if (samePackage(packageName1, packageName2)) { + if (supportMultiInstancesSplit(packageName1)) { + fillInIntent = new Intent(); + fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK); + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Adding MULTIPLE_TASK"); + } else { + taskId = INVALID_TASK_ID; + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, + "Cancel entering split as not supporting multi-instances"); + Toast.makeText(mContext, R.string.dock_multi_instances_not_supported_text, + Toast.LENGTH_SHORT).show(); + } + } + mStageCoordinator.startIntentAndTaskWithLegacyTransition(pendingIntent, fillInIntent, + options1, taskId, options2, splitPosition, splitRatio, adapter, instanceId); + } - // 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) { + private void startIntentAndTask(PendingIntent pendingIntent, @Nullable Bundle options1, + int taskId, @Nullable Bundle options2, @SplitPosition int splitPosition, + float splitRatio, @Nullable RemoteTransition remoteTransition, InstanceId instanceId) { + Intent fillInIntent = null; + final String packageName1 = SplitScreenUtils.getPackageName(pendingIntent); + final String packageName2 = SplitScreenUtils.getPackageName(taskId, mTaskOrganizer); + if (samePackage(packageName1, packageName2)) { + if (supportMultiInstancesSplit(packageName1)) { fillInIntent = new Intent(); + fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK); + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Adding MULTIPLE_TASK"); + } else { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, + "Cancel entering split as not supporting multi-instances"); + Toast.makeText(mContext, R.string.dock_multi_instances_not_supported_text, + Toast.LENGTH_SHORT).show(); } - fillInIntent.addFlags(FLAG_ACTIVITY_NO_USER_ACTION); + } + mStageCoordinator.startIntentAndTask(pendingIntent, fillInIntent, options1, taskId, + options2, splitPosition, splitRatio, remoteTransition, instanceId); + } - intent.send(mContext, 0, fillInIntent, null /* onFinished */, null /* handler */, - null /* requiredPermission */, options); - } catch (PendingIntent.CanceledException e) { - Slog.e(TAG, "Failed to launch task", e); + private void startIntentsWithLegacyTransition(PendingIntent pendingIntent1, + @Nullable ShortcutInfo shortcutInfo1, @Nullable Bundle options1, + PendingIntent pendingIntent2, @Nullable ShortcutInfo shortcutInfo2, + @Nullable Bundle options2, @SplitPosition int splitPosition, float splitRatio, + RemoteAnimationAdapter adapter, InstanceId instanceId) { + Intent fillInIntent1 = null; + Intent fillInIntent2 = null; + final String packageName1 = SplitScreenUtils.getPackageName(pendingIntent1); + final String packageName2 = SplitScreenUtils.getPackageName(pendingIntent2); + if (samePackage(packageName1, packageName2)) { + if (supportMultiInstancesSplit(packageName1)) { + fillInIntent1 = new Intent(); + fillInIntent1.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK); + fillInIntent2 = new Intent(); + fillInIntent2.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK); + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Adding MULTIPLE_TASK"); + } else { + pendingIntent2 = null; + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, + "Cancel entering split as not supporting multi-instances"); + Toast.makeText(mContext, R.string.dock_multi_instances_not_supported_text, + Toast.LENGTH_SHORT).show(); + } } + mStageCoordinator.startIntentsWithLegacyTransition(pendingIntent1, fillInIntent1, + shortcutInfo1, options1, pendingIntent2, fillInIntent2, shortcutInfo2, options2, + splitPosition, splitRatio, adapter, instanceId); } - private void startIntentLegacy(PendingIntent intent, @Nullable Intent fillInIntent, + @Override + public void startIntent(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())); - } + // 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); - // Do nothing when the animation was cancelled. - t.apply(); + final String packageName1 = SplitScreenUtils.getPackageName(intent); + final String packageName2 = getPackageName(reverseSplitPosition(position)); + if (SplitScreenUtils.samePackage(packageName1, packageName2)) { + if (supportMultiInstancesSplit(packageName1)) { + // To prevent accumulating large number of instances in the background, reuse task + // in the background with priority. + final ActivityManager.RecentTaskInfo taskInfo = mRecentTasksOptional + .map(recentTasks -> recentTasks.findTaskInBackground( + intent.getIntent().getComponent())) + .orElse(null); + if (taskInfo != null) { + startTask(taskInfo.taskId, position, options); + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, + "Start task in background"); 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(); + // Flag with MULTIPLE_TASK if this is launching the same activity into both sides of + // the split and there is no reusable background task. + fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK); + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Adding MULTIPLE_TASK"); + } else if (isSplitScreenVisible()) { + mStageCoordinator.switchSplitPosition("startIntent"); + return; + } else { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, + "Cancel entering split as not supporting multi-instances"); + Toast.makeText(mContext, R.string.dock_multi_instances_not_supported_text, + Toast.LENGTH_SHORT).show(); + return; + } + } - if (finishedCallback != null) { - try { - finishedCallback.onAnimationFinished(); - } catch (RemoteException e) { - Slog.e(TAG, "Error finishing legacy transition: ", e); - } - } + mStageCoordinator.startIntent(intent, fillInIntent, position, options); + } - mSyncQueue.queue(evictWct); + /** Retrieve package name of a specific split position if split screen is activated, otherwise + * returns the package name of the top running task. */ + @Nullable + private String getPackageName(@SplitPosition int position) { + ActivityManager.RunningTaskInfo taskInfo; + if (isSplitScreenVisible()) { + taskInfo = getTaskInfo(position); + } else { + taskInfo = mRecentTasksOptional + .map(recentTasks -> recentTasks.getTopRunningTask()) + .orElse(null); + if (!isValidToSplit(taskInfo)) { + return null; } - }; + } - final WindowContainerTransaction wct = new WindowContainerTransaction(); - options = mStageCoordinator.resolveStartStage(STAGE_TYPE_UNDEFINED, position, options, wct); + return taskInfo != null ? SplitScreenUtils.getPackageName(taskInfo.baseIntent) : null; + } - // 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(); + @VisibleForTesting + boolean supportMultiInstancesSplit(String packageName) { + if (packageName != null) { + for (int i = 0; i < mAppsSupportMultiInstances.length; i++) { + if (mAppsSupportMultiInstances[i].equals(packageName)) { + return true; + } + } } - 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. + * Drop callback when splitscreen is entered. */ - public void logOnDroppedToSplit(@SplitPosition int position, InstanceId dragSessionId) { - mStageCoordinator.logOnDroppedToSplit(position, dragSessionId); + public void onDroppedToSplit(@SplitPosition int position, InstanceId dragSessionId) { + mStageCoordinator.onDroppedToSplit(position, dragSessionId); } /** @@ -494,6 +810,8 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, return "APP_DOES_NOT_SUPPORT_MULTIWINDOW"; case EXIT_REASON_CHILD_TASK_ENTER_PIP: return "CHILD_TASK_ENTER_PIP"; + case EXIT_REASON_RECREATE_SPLIT: + return "RECREATE_SPLIT"; default: return "unknown reason, reason int = " + exitReason; } @@ -511,7 +829,6 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, */ @ExternalThread private class SplitScreenImpl implements SplitScreen { - private ISplitScreenImpl mISplitScreen; private final ArrayMap<SplitScreenListener, Executor> mExecutors = new ArrayMap<>(); private final SplitScreen.SplitScreenListener mListener = new SplitScreenListener() { @Override @@ -535,6 +852,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; @@ -546,15 +874,6 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, }; @Override - public ISplitScreen createExternalInterface() { - if (mISplitScreen != null) { - mISplitScreen.invalidate(); - } - mISplitScreen = new ISplitScreenImpl(SplitScreenController.this); - return mISplitScreen; - } - - @Override public void registerSplitScreenListener(SplitScreenListener listener, Executor executor) { if (mExecutors.containsKey(listener)) return; @@ -583,17 +902,13 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, } @Override - public void onKeyguardVisibilityChanged(boolean showing) { - mMainExecutor.execute(() -> { - SplitScreenController.this.onKeyguardVisibilityChanged(showing); - }); + public void onFinishedWakingUp() { + mMainExecutor.execute(SplitScreenController.this::onFinishedWakingUp); } @Override - public void onFinishedWakingUp() { - mMainExecutor.execute(() -> { - SplitScreenController.this.onFinishedWakingUp(); - }); + public void goToFullscreenFromSplit() { + mMainExecutor.execute(SplitScreenController.this::goToFullscreenFromSplit); } } @@ -601,7 +916,8 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, * The interface for calls from outside the host process. */ @BinderThread - private static class ISplitScreenImpl extends ISplitScreen.Stub { + private static class ISplitScreenImpl extends ISplitScreen.Stub + implements ExternalInterfaceBinder { private SplitScreenController mController; private final SingleInstanceRemoteListener<SplitScreenController, ISplitScreenListener> mListener; @@ -628,8 +944,11 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, /** * Invalidates this instance, preventing future calls from updating the controller. */ - void invalidate() { + @Override + public void invalidate() { mController = null; + // Unregister the listener to ensure any registered binder death recipients are unlinked + mListener.unregister(); } @Override @@ -647,82 +966,127 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, @Override public void exitSplitScreen(int toTopTaskId) { executeRemoteCallWithTaskPermission(mController, "exitSplitScreen", - (controller) -> { - controller.exitSplitScreen(toTopTaskId, EXIT_REASON_UNKNOWN); - }); + (controller) -> controller.exitSplitScreen(toTopTaskId, EXIT_REASON_UNKNOWN)); } @Override public void exitSplitScreenOnHide(boolean exitSplitScreenOnHide) { executeRemoteCallWithTaskPermission(mController, "exitSplitScreenOnHide", - (controller) -> { - controller.exitSplitScreenOnHide(exitSplitScreenOnHide); - }); + (controller) -> controller.exitSplitScreenOnHide(exitSplitScreenOnHide)); } @Override public void removeFromSideStage(int taskId) { executeRemoteCallWithTaskPermission(mController, "removeFromSideStage", - (controller) -> { - controller.removeFromSideStage(taskId); - }); + (controller) -> controller.removeFromSideStage(taskId)); } @Override public void startTask(int taskId, int position, @Nullable Bundle options) { executeRemoteCallWithTaskPermission(mController, "startTask", - (controller) -> { - controller.startTask(taskId, position, options); - }); + (controller) -> controller.startTask(taskId, position, options)); } @Override - public void startTasksWithLegacyTransition(int mainTaskId, @Nullable Bundle mainOptions, - int sideTaskId, @Nullable Bundle sideOptions, @SplitPosition int sidePosition, - float splitRatio, RemoteAnimationAdapter adapter) { + public void startTasksWithLegacyTransition(int taskId1, @Nullable Bundle options1, + int taskId2, @Nullable Bundle options2, @SplitPosition int splitPosition, + float splitRatio, RemoteAnimationAdapter adapter, InstanceId instanceId) { executeRemoteCallWithTaskPermission(mController, "startTasks", (controller) -> controller.mStageCoordinator.startTasksWithLegacyTransition( - mainTaskId, mainOptions, sideTaskId, sideOptions, sidePosition, - splitRatio, adapter)); + taskId1, options1, taskId2, options2, splitPosition, + splitRatio, adapter, instanceId)); } @Override public void startIntentAndTaskWithLegacyTransition(PendingIntent pendingIntent, - Intent fillInIntent, int taskId, Bundle mainOptions, Bundle sideOptions, - int sidePosition, float splitRatio, RemoteAnimationAdapter adapter) { + Bundle options1, int taskId, Bundle options2, int splitPosition, float splitRatio, + RemoteAnimationAdapter adapter, InstanceId instanceId) { executeRemoteCallWithTaskPermission(mController, "startIntentAndTaskWithLegacyTransition", (controller) -> - controller.mStageCoordinator.startIntentAndTaskWithLegacyTransition( - pendingIntent, fillInIntent, taskId, mainOptions, sideOptions, - sidePosition, splitRatio, adapter)); + controller.startIntentAndTaskWithLegacyTransition(pendingIntent, + options1, taskId, options2, splitPosition, splitRatio, adapter, + instanceId)); + } + + @Override + public void startShortcutAndTaskWithLegacyTransition(ShortcutInfo shortcutInfo, + @Nullable Bundle options1, int taskId, @Nullable Bundle options2, + @SplitPosition int splitPosition, float splitRatio, RemoteAnimationAdapter adapter, + InstanceId instanceId) { + executeRemoteCallWithTaskPermission(mController, + "startShortcutAndTaskWithLegacyTransition", (controller) -> + controller.startShortcutAndTaskWithLegacyTransition( + shortcutInfo, options1, taskId, options2, splitPosition, + 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) { + public void startTasks(int taskId1, @Nullable Bundle options1, int taskId2, + @Nullable Bundle options2, @SplitPosition int splitPosition, float splitRatio, + @Nullable RemoteTransition remoteTransition, InstanceId instanceId) { executeRemoteCallWithTaskPermission(mController, "startTasks", - (controller) -> controller.mStageCoordinator.startTasks(mainTaskId, mainOptions, - sideTaskId, sideOptions, sidePosition, splitRatio, remoteTransition)); + (controller) -> controller.mStageCoordinator.startTasks(taskId1, options1, + taskId2, options2, splitPosition, splitRatio, remoteTransition, + instanceId)); + } + + @Override + public void startIntentAndTask(PendingIntent pendingIntent, @Nullable Bundle options1, + int taskId, @Nullable Bundle options2, @SplitPosition int splitPosition, + float splitRatio, @Nullable RemoteTransition remoteTransition, + InstanceId instanceId) { + executeRemoteCallWithTaskPermission(mController, "startIntentAndTask", + (controller) -> controller.startIntentAndTask(pendingIntent, options1, taskId, + options2, splitPosition, splitRatio, remoteTransition, instanceId)); + } + + @Override + public void startShortcutAndTask(ShortcutInfo shortcutInfo, @Nullable Bundle options1, + int taskId, @Nullable Bundle options2, @SplitPosition int splitPosition, + float splitRatio, @Nullable RemoteTransition remoteTransition, + InstanceId instanceId) { + executeRemoteCallWithTaskPermission(mController, "startShortcutAndTask", + (controller) -> controller.mStageCoordinator.startShortcutAndTask(shortcutInfo, + options1, taskId, options2, splitPosition, splitRatio, remoteTransition, + instanceId)); + } + + @Override + public void startIntentsWithLegacyTransition(PendingIntent pendingIntent1, + @Nullable ShortcutInfo shortcutInfo1, @Nullable Bundle options1, + PendingIntent pendingIntent2, @Nullable ShortcutInfo shortcutInfo2, + @Nullable Bundle options2, @SplitPosition int splitPosition, float splitRatio, + RemoteAnimationAdapter adapter, InstanceId instanceId) { + executeRemoteCallWithTaskPermission(mController, "startIntentsWithLegacyTransition", + (controller) -> + controller.startIntentsWithLegacyTransition(pendingIntent1, shortcutInfo1, + options1, pendingIntent2, shortcutInfo2, options2, splitPosition, + splitRatio, adapter, instanceId) + ); + } + + @Override + public void startIntents(PendingIntent pendingIntent1, @Nullable Bundle options1, + PendingIntent pendingIntent2, @Nullable Bundle options2, + @SplitPosition int splitPosition, float splitRatio, + @Nullable RemoteTransition remoteTransition, InstanceId instanceId) { + // TODO(b/259368992): To be implemented. } @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) -> 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) -> controller.startIntent(intent, fillInIntent, position, options, + instanceId)); } @Override diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenShellCommandHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenShellCommandHandler.java 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..22800ad8e8a8 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,8 +21,9 @@ 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.STAGE_TYPE_MAIN; +import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_UNDEFINED; import static com.android.wm.shell.splitscreen.SplitScreen.stageTypeToString; import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_DRAG_DIVIDER; import static com.android.wm.shell.splitscreen.SplitScreenController.exitReasonToString; @@ -36,6 +37,7 @@ import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; import android.annotation.NonNull; import android.annotation.Nullable; +import android.graphics.Point; import android.graphics.Rect; import android.os.IBinder; import android.view.SurfaceControl; @@ -48,9 +50,11 @@ import android.window.WindowContainerTransactionCallback; import com.android.internal.protolog.common.ProtoLog; import com.android.wm.shell.common.TransactionPool; +import com.android.wm.shell.common.split.SplitDecorManager; import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.transition.OneShotRemoteHandler; import com.android.wm.shell.transition.Transitions; +import com.android.wm.shell.util.TransitionUtil; import java.util.ArrayList; @@ -58,19 +62,15 @@ 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; + DismissSession mPendingDismiss = null; + TransitSession mPendingEnter = null; + TransitSession mPendingResize = null; private IBinder mAnimatingTransition = null; - private OneShotRemoteHandler mPendingRemoteHandler = null; private OneShotRemoteHandler mActiveRemoteHandler = null; private final Transitions.TransitionFinishCallback mRemoteFinishCB = this::onFinish; @@ -94,24 +94,35 @@ 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; - if (mPendingRemoteHandler != null) { - mPendingRemoteHandler.startAnimation(transition, info, startTransaction, - finishTransaction, mRemoteFinishCB); - mActiveRemoteHandler = mPendingRemoteHandler; - mPendingRemoteHandler = null; - return; + mFinishTransaction = finishTransaction; + + final TransitSession pendingTransition = getPendingTransition(transition); + if (pendingTransition != null) { + if (pendingTransition.mCanceled) { + // The pending transition was canceled, so skip playing animation. + startTransaction.apply(); + onFinish(null /* wct */, null /* wctCB */); + return; + } + + if (pendingTransition.mRemoteHandler != null) { + pendingTransition.mRemoteHandler.startAnimation(transition, info, startTransaction, + finishTransaction, mRemoteFinishCB); + mActiveRemoteHandler = pendingTransition.mRemoteHandler; + 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); @@ -119,6 +130,7 @@ class SplitScreenTransitions { final int mode = info.getChanges().get(i).getMode(); if (mode == TRANSIT_CHANGE) { + final int rootIdx = TransitionUtil.rootIndexFor(change, info); 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()); @@ -126,7 +138,7 @@ class SplitScreenTransitions { 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.reparent(leash, info.getRoot(rootIdx).getLeash()); t.setLayer(leash, info.getChanges().size() - i); // build the finish reparent/reposition mFinishTransaction.reparent(leash, parentChange.getLeash()); @@ -136,15 +148,19 @@ class SplitScreenTransitions { // 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); + final Point rootOffset = info.getRoot(rootIdx).getOffset(); + startBounds.offset(-rootOffset.x, -rootOffset.y); + endBounds.offset(-rootOffset.x, -rootOffset.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,93 +186,244 @@ class SplitScreenTransitions { onFinish(null /* wct */, null /* wctCB */); } - /** 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) { - final IBinder transition = mTransitions.startTransition(transitType, wct, handler); - mPendingEnter = transition; + void applyDismissTransition(@NonNull IBinder transition, @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, + @NonNull Transitions.TransitionFinishCallback finishCallback, + @NonNull WindowContainerToken topRoot, + @NonNull WindowContainerToken mainRoot, @NonNull WindowContainerToken sideRoot, + @NonNull SplitDecorManager mainDecor, @NonNull SplitDecorManager sideDecor) { + if (mPendingDismiss.mDismissTop != STAGE_TYPE_UNDEFINED) { + mFinishCallback = finishCallback; + mAnimatingTransition = transition; + mFinishTransaction = finishTransaction; + + startTransaction.apply(); + + final SplitDecorManager topDecor = mPendingDismiss.mDismissTop == STAGE_TYPE_MAIN + ? mainDecor : sideDecor; + topDecor.fadeOutDecor(() -> { + mTransitions.getMainExecutor().execute(() -> { + onFinish(null /* wct */, null /* wctCB */); + }); + }); + } else { + playAnimation(transition, info, startTransaction, finishTransaction, + finishCallback, mainRoot, sideRoot, topRoot); + } + } + + void playResizeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, + @NonNull Transitions.TransitionFinishCallback finishCallback, + @NonNull WindowContainerToken mainRoot, @NonNull WindowContainerToken sideRoot, + @NonNull SplitDecorManager mainDecor, @NonNull SplitDecorManager sideDecor) { + mFinishCallback = finishCallback; + mAnimatingTransition = transition; + mFinishTransaction = finishTransaction; + + for (int i = info.getChanges().size() - 1; i >= 0; --i) { + final TransitionInfo.Change change = info.getChanges().get(i); + if (mainRoot.equals(change.getContainer()) || sideRoot.equals(change.getContainer())) { + final SurfaceControl leash = change.getLeash(); + startTransaction.setPosition(leash, change.getEndAbsBounds().left, + change.getEndAbsBounds().top); + startTransaction.setWindowCrop(leash, change.getEndAbsBounds().width(), + change.getEndAbsBounds().height()); + + SplitDecorManager decor = mainRoot.equals(change.getContainer()) + ? mainDecor : sideDecor; + + // This is to ensure onFinished be called after all animations ended. + ValueAnimator va = new ValueAnimator(); + mAnimations.add(va); - if (remoteTransition != null) { - // Wrapping it for ease-of-use (OneShot handles all the binder linking/death stuff) - mPendingRemoteHandler = new OneShotRemoteHandler( - mTransitions.getMainExecutor(), remoteTransition); - mPendingRemoteHandler.setTransition(transition); + decor.setScreenshotIfNeeded(change.getSnapshot(), startTransaction); + decor.onResized(startTransaction, () -> { + mTransitions.getMainExecutor().execute(() -> { + mAnimations.remove(va); + onFinish(null /* wct */, null /* wctCB */); + }); + }); + } + } + + startTransaction.apply(); + onFinish(null /* wct */, null /* wctCB */); + } + + boolean isPendingTransition(IBinder transition) { + return getPendingTransition(transition) != null; + } + + boolean isPendingEnter(IBinder transition) { + return mPendingEnter != null && mPendingEnter.mTransition == transition; + } + + boolean isPendingDismiss(IBinder transition) { + return mPendingDismiss != null && mPendingDismiss.mTransition == transition; + } + + boolean isPendingResize(IBinder transition) { + return mPendingResize != null && mPendingResize.mTransition == transition; + } + + @Nullable + private TransitSession getPendingTransition(IBinder transition) { + if (isPendingEnter(transition)) { + return mPendingEnter; + } else if (isPendingDismiss(transition)) { + return mPendingDismiss; + } else if (isPendingResize(transition)) { + return mPendingResize; } + + return null; + } + + + /** Starts a transition to enter split with a remote transition animator. */ + IBinder startEnterTransition( + @WindowManager.TransitionType int transitType, + WindowContainerTransaction wct, + @Nullable RemoteTransition remoteTransition, + Transitions.TransitionHandler handler, + @Nullable TransitionConsumedCallback consumedCallback, + @Nullable TransitionFinishedCallback finishedCallback) { + if (mPendingEnter != null) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " splitTransition " + + " skip to start enter split transition since it already exist. "); + return null; + } + final IBinder transition = mTransitions.startTransition(transitType, wct, handler); + setEnterTransition(transition, remoteTransition, consumedCallback, finishedCallback); return transition; } + /** Sets a transition to enter split. */ + void setEnterTransition(@NonNull IBinder transition, + @Nullable RemoteTransition remoteTransition, + @Nullable TransitionConsumedCallback consumedCallback, + @Nullable TransitionFinishedCallback finishedCallback) { + mPendingEnter = new TransitSession( + transition, consumedCallback, finishedCallback, remoteTransition); + + 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) { + if (mPendingDismiss != null) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " splitTransition " + + " skip to start dismiss split transition since it already exist. reason to " + + " dismiss = %s", exitReasonToString(reason)); + return null; + } final int type = reason == EXIT_REASON_DRAG_DIVIDER ? TRANSIT_SPLIT_DISMISS_SNAP : TRANSIT_SPLIT_DISMISS; - if (transition == null) { - transition = mTransitions.startTransition(type, wct, handler); - } - mPendingDismiss = new DismissTransition(transition, reason, dismissTop); + 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 DismissSession(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); + IBinder startResizeTransition(WindowContainerTransaction wct, + Transitions.TransitionHandler handler, + @Nullable TransitionFinishedCallback finishCallback) { + if (mPendingResize != null) { + mPendingResize.cancel(null); + mAnimations.clear(); + onFinish(null /* wct */, null /* wctCB */); } - mPendingRecent = transition; - if (remoteTransition != null) { - // Wrapping it for ease-of-use (OneShot handles all the binder linking/death stuff) - mPendingRemoteHandler = new OneShotRemoteHandler( - mTransitions.getMainExecutor(), remoteTransition); - mPendingRemoteHandler.setTransition(transition); - } + IBinder transition = mTransitions.startTransition(TRANSIT_CHANGE, wct, handler); + setResizeTransition(transition, finishCallback); + return transition; + } + void setResizeTransition(@NonNull IBinder transition, + @Nullable TransitionFinishedCallback finishCallback) { + mPendingResize = new TransitSession(transition, null /* consumedCb */, finishCallback); ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " splitTransition " - + " deduced Enter recent panel"); - return transition; + + " deduced Resize split screen"); } void mergeAnimation(IBinder transition, TransitionInfo info, SurfaceControl.Transaction t, IBinder mergeTarget, Transitions.TransitionFinishCallback finishCallback) { - if (mergeTarget == mAnimatingTransition && mActiveRemoteHandler != null) { + if (mergeTarget != mAnimatingTransition) return; + + 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 It's 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 entering transition got merged, appends the rest operations to finish entering + // split screen. + mStageCoordinator.finishEnterSplitScreen(finishT); + } + + mPendingEnter.onConsumed(aborted); + mPendingEnter = null; + } else if (isPendingDismiss(transition)) { + mPendingDismiss.onConsumed(aborted); + mPendingDismiss = null; + } else if (isPendingResize(transition)) { + mPendingResize.onConsumed(aborted); + mPendingResize = null; } } void onFinish(WindowContainerTransaction wct, WindowContainerTransactionCallback wctCB) { if (!mAnimations.isEmpty()) return; - if (mAnimatingTransition == mPendingEnter) { + + if (wct == null) wct = new WindowContainerTransaction(); + if (isPendingEnter(mAnimatingTransition)) { + mPendingEnter.onFinished(wct, mFinishTransaction); mPendingEnter = null; - } - if (mPendingDismiss != null && mPendingDismiss.mTransition == mAnimatingTransition) { + } else if (isPendingDismiss(mAnimatingTransition)) { + mPendingDismiss.onFinished(wct, mFinishTransaction); mPendingDismiss = null; + } else if (isPendingResize(mAnimatingTransition)) { + mPendingResize.onFinished(wct, mFinishTransaction); + mPendingResize = 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); - mPendingRecent = null; - } - 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; @@ -284,10 +451,7 @@ class SplitScreenTransitions { onFinish(null /* wct */, null /* wctCB */); }); }; - va.addListener(new Animator.AnimatorListener() { - @Override - public void onAnimationStart(Animator animation) { } - + va.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { finisher.run(); @@ -297,9 +461,6 @@ class SplitScreenTransitions { public void onAnimationCancel(Animator animation) { finisher.run(); } - - @Override - public void onAnimationRepeat(Animator animation) { } }); mAnimations.add(va); mTransitions.getAnimExecutor().execute(va::start); @@ -348,22 +509,96 @@ class SplitScreenTransitions { } private boolean isOpeningTransition(TransitionInfo info) { - return Transitions.isOpeningType(info.getType()) + return TransitionUtil.isOpeningType(info.getType()) || info.getType() == TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE || info.getType() == TRANSIT_SPLIT_SCREEN_PAIR_OPEN; } - /** Bundled information of dismiss transition. */ - static class DismissTransition { - IBinder mTransition; + /** Calls when the transition got consumed. */ + interface TransitionConsumedCallback { + void onConsumed(boolean aborted); + } + + /** Calls when the transition finished. */ + interface TransitionFinishedCallback { + void onFinished(WindowContainerTransaction wct, SurfaceControl.Transaction t); + } - int mReason; + /** Session for a transition and its clean-up callback. */ + class TransitSession { + final IBinder mTransition; + TransitionConsumedCallback mConsumedCallback; + TransitionFinishedCallback mFinishedCallback; + OneShotRemoteHandler mRemoteHandler; - @SplitScreen.StageType - int mDismissTop; + /** Whether the transition was canceled. */ + boolean mCanceled; + + TransitSession(IBinder transition, + @Nullable TransitionConsumedCallback consumedCallback, + @Nullable TransitionFinishedCallback finishedCallback) { + this(transition, consumedCallback, finishedCallback, null /* remoteTransition */); + } + + TransitSession(IBinder transition, + @Nullable TransitionConsumedCallback consumedCallback, + @Nullable TransitionFinishedCallback finishedCallback, + @Nullable RemoteTransition remoteTransition) { + mTransition = transition; + mConsumedCallback = consumedCallback; + mFinishedCallback = finishedCallback; + + if (remoteTransition != null) { + // Wrapping the remote transition for ease-of-use. (OneShot handles all the binder + // linking/death stuff) + mRemoteHandler = new OneShotRemoteHandler( + mTransitions.getMainExecutor(), remoteTransition); + mRemoteHandler.setTransition(transition); + } + } + + /** Sets transition consumed callback. */ + void setConsumedCallback(@Nullable TransitionConsumedCallback callback) { + mConsumedCallback = callback; + } + + /** Sets transition finished callback. */ + void setFinishedCallback(@Nullable TransitionFinishedCallback callback) { + mFinishedCallback = callback; + } + + /** + * Cancels the transition. This should be called before playing animation. A canceled + * transition will skip playing animation. + * + * @param finishedCb new finish callback to override. + */ + void cancel(@Nullable TransitionFinishedCallback finishedCb) { + mCanceled = true; + setFinishedCallback(finishedCb); + } + + void onConsumed(boolean aborted) { + if (mConsumedCallback != null) { + mConsumedCallback.onConsumed(aborted); + } + } + + void onFinished(WindowContainerTransaction finishWct, + SurfaceControl.Transaction finishT) { + if (mFinishedCallback != null) { + mFinishedCallback.onFinished(finishWct, finishT); + } + } + } + + /** Bundled information of dismiss transition. */ + class DismissSession extends TransitSession { + final int mReason; + final @SplitScreen.StageType int mDismissTop; - DismissTransition(IBinder transition, int reason, int dismissTop) { - this.mTransition = transition; + DismissSession(IBinder transition, int reason, int dismissTop) { + super(transition, null /* consumedCallback */, null /* finishedCallback */); 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..5483fa5d29f6 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,11 +16,16 @@ package com.android.wm.shell.splitscreen; -import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__ENTER_REASON__OVERVIEW; +import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__ENTER_REASON__LAUNCHER; +import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__ENTER_REASON__MULTI_INSTANCE; +import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__ENTER_REASON__UNKNOWN_ENTER; import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__APP_DOES_NOT_SUPPORT_MULTIWINDOW; import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__APP_FINISHED; +import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__CHILD_TASK_ENTER_PIP; import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__DEVICE_FOLDED; import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__DRAG_DIVIDER; +import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__FULLSCREEN_SHORTCUT; +import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__RECREATE_SPLIT; import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__RETURN_HOME; import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__ROOT_TASK_VANISHED; import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__SCREEN_LOCKED; @@ -28,16 +33,24 @@ import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED_ import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__UNKNOWN_EXIT; import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT; import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED; +import static com.android.wm.shell.splitscreen.SplitScreenController.ENTER_REASON_DRAG; +import static com.android.wm.shell.splitscreen.SplitScreenController.ENTER_REASON_LAUNCHER; +import static com.android.wm.shell.splitscreen.SplitScreenController.ENTER_REASON_MULTI_INSTANCE; +import static com.android.wm.shell.splitscreen.SplitScreenController.ENTER_REASON_UNKNOWN; import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW; import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_APP_FINISHED; +import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_CHILD_TASK_ENTER_PIP; import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_DEVICE_FOLDED; import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_DRAG_DIVIDER; +import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_FULLSCREEN_SHORTCUT; +import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_RECREATE_SPLIT; import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_RETURN_HOME; import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_ROOT_TASK_VANISHED; import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_SCREEN_LOCKED; import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_SCREEN_LOCKED_SHOW_ON_TOP; import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_UNKNOWN; +import android.annotation.Nullable; import android.util.Slog; import com.android.internal.logging.InstanceId; @@ -59,7 +72,7 @@ public class SplitscreenEventLogger { // Drag info private @SplitPosition int mDragEnterPosition; - private InstanceId mDragEnterSessionId; + private @Nullable InstanceId mEnterSessionId; // For deduping async events private int mLastMainStagePosition = -1; @@ -67,6 +80,7 @@ public class SplitscreenEventLogger { private int mLastSideStagePosition = -1; private int mLastSideStageUid = -1; private float mLastSplitRatio = -1f; + private @SplitScreenController.SplitEnterReason int mEnterReason = ENTER_REASON_UNKNOWN; public SplitscreenEventLogger() { mIdSequence = new InstanceIdSequence(Integer.MAX_VALUE); @@ -79,12 +93,35 @@ public class SplitscreenEventLogger { return mLoggerSessionId != null; } + public boolean isEnterRequestedByDrag() { + return mEnterReason == ENTER_REASON_DRAG; + } + /** * May be called before logEnter() to indicate that the session was started from a drag. */ - public void enterRequestedByDrag(@SplitPosition int position, InstanceId dragSessionId) { + public void enterRequestedByDrag(@SplitPosition int position, InstanceId enterSessionId) { mDragEnterPosition = position; - mDragEnterSessionId = dragSessionId; + enterRequested(enterSessionId, ENTER_REASON_DRAG); + } + + /** + * May be called before logEnter() to indicate that the session was started from launcher. + * This specifically is for all the scenarios where split started without a drag interaction + */ + public void enterRequested(@Nullable InstanceId enterSessionId, + @SplitScreenController.SplitEnterReason int enterReason) { + mEnterSessionId = enterSessionId; + mEnterReason = enterReason; + } + + /** + * @return if an enterSessionId has been set via either + * {@link #enterRequested(InstanceId, int)} or + * {@link #enterRequestedByDrag(int, InstanceId)} + */ + public boolean hasValidEnterSessionId() { + return mEnterSessionId != null; } /** @@ -95,9 +132,7 @@ public class SplitscreenEventLogger { @SplitPosition int sideStagePosition, int sideStageUid, boolean isLandscape) { mLoggerSessionId = mIdSequence.newInstanceId(); - int enterReason = mDragEnterPosition != SPLIT_POSITION_UNDEFINED - ? getDragEnterReasonFromSplitPosition(mDragEnterPosition, isLandscape) - : SPLITSCREEN_UICHANGED__ENTER_REASON__OVERVIEW; + int enterReason = getLoggerEnterReason(isLandscape); updateMainStageState(getMainStagePositionFromSplitPosition(mainStagePosition, isLandscape), mainStageUid); updateSideStageState(getSideStagePositionFromSplitPosition(sideStagePosition, isLandscape), @@ -112,10 +147,24 @@ public class SplitscreenEventLogger { mLastMainStageUid, mLastSideStagePosition, mLastSideStageUid, - mDragEnterSessionId != null ? mDragEnterSessionId.getId() : 0, + mEnterSessionId != null ? mEnterSessionId.getId() : 0, mLoggerSessionId.getId()); } + private int getLoggerEnterReason(boolean isLandscape) { + switch (mEnterReason) { + case ENTER_REASON_MULTI_INSTANCE: + return SPLITSCREEN_UICHANGED__ENTER_REASON__MULTI_INSTANCE; + case ENTER_REASON_LAUNCHER: + return SPLITSCREEN_UICHANGED__ENTER_REASON__LAUNCHER; + case ENTER_REASON_DRAG: + return getDragEnterReasonFromSplitPosition(mDragEnterPosition, isLandscape); + case ENTER_REASON_UNKNOWN: + default: + return SPLITSCREEN_UICHANGED__ENTER_REASON__UNKNOWN_ENTER; + } + } + /** * Returns the framework logging constant given a splitscreen exit reason. */ @@ -137,6 +186,12 @@ public class SplitscreenEventLogger { return SPLITSCREEN_UICHANGED__EXIT_REASON__SCREEN_LOCKED; case EXIT_REASON_SCREEN_LOCKED_SHOW_ON_TOP: return SPLITSCREEN_UICHANGED__EXIT_REASON__SCREEN_LOCKED_SHOW_ON_TOP; + case EXIT_REASON_CHILD_TASK_ENTER_PIP: + return SPLITSCREEN_UICHANGED__EXIT_REASON__CHILD_TASK_ENTER_PIP; + case EXIT_REASON_RECREATE_SPLIT: + return SPLITSCREEN_UICHANGED__EXIT_REASON__RECREATE_SPLIT; + case EXIT_REASON_FULLSCREEN_SHORTCUT: + return SPLITSCREEN_UICHANGED__EXIT_REASON__FULLSCREEN_SHORTCUT; case EXIT_REASON_UNKNOWN: // Fall through default: @@ -176,11 +231,12 @@ public class SplitscreenEventLogger { // Reset states mLoggerSessionId = null; mDragEnterPosition = SPLIT_POSITION_UNDEFINED; - mDragEnterSessionId = null; + mEnterSessionId = null; mLastMainStagePosition = -1; mLastMainStageUid = -1; mLastSideStagePosition = -1; mLastSideStageUid = -1; + mEnterReason = ENTER_REASON_UNKNOWN; } /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java index 30f316efb2b3..dd91a37039e4 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java @@ -18,41 +18,50 @@ package com.android.wm.shell.splitscreen; import static android.app.ActivityOptions.KEY_LAUNCH_ROOT_TASK_TOKEN; import static android.app.ActivityTaskManager.INVALID_TASK_ID; -import static android.app.WindowConfiguration.ACTIVITY_TYPE_ASSISTANT; +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_HOME; import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +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.FLAG_IS_DIVIDER_BAR; import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT; import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED; +import static com.android.wm.shell.common.split.SplitScreenUtils.reverseSplitPosition; import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_MAIN; import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_SIDE; import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_UNDEFINED; +import static com.android.wm.shell.splitscreen.SplitScreenController.ENTER_REASON_LAUNCHER; +import static com.android.wm.shell.splitscreen.SplitScreenController.ENTER_REASON_MULTI_INSTANCE; import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW; import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_APP_FINISHED; import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_CHILD_TASK_ENTER_PIP; import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_DEVICE_FOLDED; import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_DRAG_DIVIDER; +import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_FULLSCREEN_SHORTCUT; +import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_RECREATE_SPLIT; import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_RETURN_HOME; import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_ROOT_TASK_VANISHED; import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_SCREEN_LOCKED_SHOW_ON_TOP; import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_UNKNOWN; import static com.android.wm.shell.splitscreen.SplitScreenController.exitReasonToString; -import static com.android.wm.shell.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; -import static com.android.wm.shell.transition.Transitions.isClosingType; -import static com.android.wm.shell.transition.Transitions.isOpeningType; +import static com.android.wm.shell.util.TransitionUtil.isClosingType; +import static com.android.wm.shell.util.TransitionUtil.isOpeningType; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; @@ -62,11 +71,15 @@ 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.TaskInfo; import android.app.WindowConfiguration; +import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; +import android.content.pm.LauncherApps; +import android.content.pm.ShortcutInfo; import android.content.res.Configuration; import android.graphics.Rect; import android.hardware.devicestate.DeviceStateManager; @@ -74,6 +87,8 @@ import android.os.Bundle; import android.os.Debug; import android.os.IBinder; import android.os.RemoteException; +import android.os.ServiceManager; +import android.os.UserHandle; import android.util.Log; import android.util.Slog; import android.view.Choreographer; @@ -84,6 +99,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,32 +110,37 @@ 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; import com.android.wm.shell.common.DisplayInsetsController; import com.android.wm.shell.common.DisplayLayout; +import com.android.wm.shell.common.ScreenshotUtils; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.common.TransactionPool; import com.android.wm.shell.common.split.SplitLayout; import com.android.wm.shell.common.split.SplitScreenConstants.SplitPosition; +import com.android.wm.shell.common.split.SplitScreenUtils; import com.android.wm.shell.common.split.SplitWindowManager; import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.recents.RecentTasksController; import com.android.wm.shell.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 com.android.wm.shell.util.TransitionUtil; 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,7 +154,7 @@ 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 { @@ -142,10 +164,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; @@ -155,6 +175,7 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, private ValueAnimator mDividerFadeInAnimator; private boolean mDividerVisible; private boolean mKeyguardShowing; + private boolean mShowDecorImmediately; private final SyncTransactionQueue mSyncQueue; private final ShellTaskOrganizer mTaskOrganizer; private final Context mContext; @@ -168,6 +189,9 @@ 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(); + /** * A single-top root task which the split divider attached to. */ @@ -181,11 +205,42 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, private boolean mShouldUpdateRecents; private boolean mExitSplitScreenOnHide; private boolean mIsDividerRemoteAnimating; - private boolean mResizingSplits; + private boolean mIsDropEntering; + private boolean mIsExiting; + private boolean mIsRootTranslucent; - /** The target stage to dismiss to when unlock after folded. */ - @StageType - private int mTopStageAfterFoldDismiss = STAGE_TYPE_UNDEFINED; + private DefaultMixedHandler mMixedHandler; + private final Toast mSplitUnsupportedToast; + private SplitRequest mSplitRequest; + + class SplitRequest { + @SplitPosition + int mActivatePosition; + int mActivateTaskId; + int mActivateTaskId2; + Intent mStartIntent; + Intent mStartIntent2; + + SplitRequest(int taskId, Intent startIntent, int position) { + mActivateTaskId = taskId; + mStartIntent = startIntent; + mActivatePosition = position; + } + SplitRequest(Intent startIntent, int position) { + mStartIntent = startIntent; + mActivatePosition = position; + } + SplitRequest(Intent startIntent, Intent startIntent2, int position) { + mStartIntent = startIntent; + mStartIntent2 = startIntent2; + mActivatePosition = position; + } + SplitRequest(int taskId1, int taskId2, int position) { + mActivateTaskId = taskId1; + mActivateTaskId2 = taskId2; + mActivatePosition = position; + } + } private final SplitWindowManager.ParentContainerCallbacks mParentContainerCallbacks = new SplitWindowManager.ParentContainerCallbacks() { @@ -196,27 +251,29 @@ 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, + 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 +283,7 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, mMainStageListener, mSyncQueue, mSurfaceSession, - iconProvider, - mMainUnfoldController); + iconProvider); mSideStage = new SideStage( mContext, mTaskOrganizer, @@ -235,8 +291,7 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, mSideStageListener, mSyncQueue, mSurfaceSession, - iconProvider, - mSideUnfoldController); + iconProvider); mDisplayController = displayController; mDisplayImeController = displayImeController; mDisplayInsetsController = displayInsetsController; @@ -250,6 +305,8 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, mDisplayController.addDisplayWindowListener(this); mDisplayLayout = new DisplayLayout(displayController.getDisplayLayout(displayId)); transitions.addHandler(this); + mSplitUnsupportedToast = Toast.makeText(mContext, + R.string.dock_non_resizeble_failed_to_dock_text, Toast.LENGTH_SHORT); } @VisibleForTesting @@ -258,9 +315,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,14 +330,18 @@ 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); mDisplayLayout = new DisplayLayout(); transitions.addHandler(this); + mSplitUnsupportedToast = Toast.makeText(mContext, + R.string.dock_non_resizeble_failed_to_dock_text, Toast.LENGTH_SHORT); + } + + public void setMixedHandler(DefaultMixedHandler mixedHandler) { + mMixedHandler = mixedHandler; } @VisibleForTesting @@ -289,10 +349,19 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, return mSplitTransitions; } - boolean isSplitScreenVisible() { + public boolean isSplitScreenVisible() { return mSideStageListener.mVisible && mMainStageListener.mVisible; } + public boolean isSplitActive() { + return mMainStage.isActive(); + } + + /** Checks if `transition` is a pending enter-split transition. */ + public boolean isPendingEnter(IBinder transition) { + return mSplitTransitions.isPendingEnter(transition); + } + @StageType int getStageOfTask(int taskId) { if (mMainStage.containsTask(taskId)) { @@ -304,42 +373,43 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, return STAGE_TYPE_UNDEFINED; } - boolean moveToStage(ActivityManager.RunningTaskInfo task, @StageType int stageType, - @SplitPosition int stagePosition, WindowContainerTransaction wct) { + boolean moveToStage(ActivityManager.RunningTaskInfo task, @SplitPosition int stagePosition, + WindowContainerTransaction wct) { StageTaskListener targetStage; int sideStagePosition; - if (stageType == STAGE_TYPE_MAIN) { - targetStage = mMainStage; - sideStagePosition = SplitLayout.reversePosition(stagePosition); - } else if (stageType == STAGE_TYPE_SIDE) { + if (isSplitScreenVisible()) { + // If the split screen is foreground, retrieves target stage based on position. + targetStage = stagePosition == mSideStagePosition ? mSideStage : mMainStage; + sideStagePosition = mSideStagePosition; + } else { targetStage = mSideStage; sideStagePosition = stagePosition; - } else { - if (mMainStage.isActive()) { - // If the split screen is activated, retrieves target stage based on position. - targetStage = stagePosition == mSideStagePosition ? mSideStage : mMainStage; - sideStagePosition = mSideStagePosition; - } else { - targetStage = mSideStage; - sideStagePosition = stagePosition; - } - } - - setSideStagePosition(sideStagePosition, wct); - 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); + if (!isSplitActive()) { + mSplitLayout.init(); + prepareEnterSplitScreen(wct, task, stagePosition); + mSyncQueue.queue(wct); + mSyncQueue.runInSync(t -> { + updateSurfaceBounds(mSplitLayout, t, false /* applyResizingOffset */); + }); } else { - mTaskOrganizer.applyTransaction(wct); + setSideStagePosition(sideStagePosition, wct); + targetStage.addTask(task, wct); + targetStage.evictAllChildren(wct); + if (!isSplitScreenVisible()) { + final StageTaskListener anotherStage = targetStage == mMainStage + ? mSideStage : mMainStage; + anotherStage.reparentTopTask(wct); + anotherStage.evictAllChildren(wct); + wct.reorder(mRootTaskInfo.token, true); + } + setRootForceTranslucent(false, wct); + mSyncQueue.queue(wct); } + + // Due to drag already pip task entering split by this method so need to reset flag here. + mIsDropEntering = false; return true; } @@ -357,67 +427,484 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, return result; } + SplitscreenEventLogger getLogger() { + return mLogger; + } + + void startShortcut(String packageName, String shortcutId, @SplitPosition int position, + Bundle options, UserHandle user) { + final boolean isEnteringSplit = !isSplitActive(); + + IRemoteAnimationRunner wrapper = new IRemoteAnimationRunner.Stub() { + @Override + public void onAnimationStart(@WindowManager.TransitionOldType int transit, + RemoteAnimationTarget[] apps, + RemoteAnimationTarget[] wallpapers, + RemoteAnimationTarget[] nonApps, + final IRemoteAnimationFinishedCallback finishedCallback) { + boolean openingToSide = false; + if (apps != null) { + for (int i = 0; i < apps.length; ++i) { + if (apps[i].mode == MODE_OPENING + && mSideStage.containsTask(apps[i].taskId)) { + openingToSide = true; + break; + } + } + } else if (mSideStage.getChildCount() != 0) { + // There are chances the entering app transition got canceled by performing + // rotation transition. Checks if there is any child task existed in split + // screen before fallback to cancel entering flow. + openingToSide = true; + } + + if (isEnteringSplit && !openingToSide) { + mMainExecutor.execute(() -> exitSplitScreen( + mSideStage.getChildCount() == 0 ? mMainStage : mSideStage, + EXIT_REASON_UNKNOWN)); + } + + if (finishedCallback != null) { + try { + finishedCallback.onAnimationFinished(); + } catch (RemoteException e) { + Slog.e(TAG, "Error finishing legacy transition: ", e); + } + } + + if (!isEnteringSplit && apps != null) { + final WindowContainerTransaction evictWct = new WindowContainerTransaction(); + prepareEvictNonOpeningChildTasks(position, apps, evictWct); + mSyncQueue.queue(evictWct); + } + } + @Override + public void onAnimationCancelled(boolean isKeyguardOccluded) { + if (isEnteringSplit) { + mMainExecutor.execute(() -> exitSplitScreen( + mSideStage.getChildCount() == 0 ? mMainStage : mSideStage, + EXIT_REASON_UNKNOWN)); + } + } + }; + options = resolveStartStage(STAGE_TYPE_UNDEFINED, position, options, + null /* wct */); + RemoteAnimationAdapter wrappedAdapter = new RemoteAnimationAdapter(wrapper, + 0 /* duration */, 0 /* statusBarTransitionDelay */); + ActivityOptions activityOptions = ActivityOptions.fromBundle(options); + // 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. + activityOptions.setApplyNoUserActionFlagForShortcut(true); + activityOptions.update(ActivityOptions.makeRemoteAnimation(wrappedAdapter)); + try { + LauncherApps launcherApps = mContext.getSystemService(LauncherApps.class); + launcherApps.startShortcut(packageName, shortcutId, null /* sourceBounds */, + activityOptions.toBundle(), user); + } catch (ActivityNotFoundException e) { + Slog.e(TAG, "Failed to launch shortcut", e); + } + } + + /** Launches an activity into split. */ + void startIntent(PendingIntent intent, Intent fillInIntent, @SplitPosition int position, + @Nullable Bundle options) { + if (!ENABLE_SHELL_TRANSITIONS) { + startIntentLegacy(intent, fillInIntent, position, options); + return; + } + + 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); + + // If split screen is not activated, we're expecting to open a pair of apps to split. + final int transitType = mMainStage.isActive() + ? TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE : TRANSIT_SPLIT_SCREEN_PAIR_OPEN; + prepareEnterSplitScreen(wct, null /* taskInfo */, position); + + mSplitTransitions.startEnterTransition(transitType, wct, null, this, + null /* consumedCallback */, + (finishWct, finishT) -> { + if (!evictWct.isEmpty()) { + finishWct.merge(evictWct, true); + } + } /* finishedCallback */); + } + + /** Launches an activity into split by legacy transition. */ + void startIntentLegacy(PendingIntent intent, Intent fillInIntent, @SplitPosition int position, + @Nullable Bundle options) { + final boolean isEnteringSplit = !isSplitActive(); + + LegacyTransitions.ILegacyTransition transition = new LegacyTransitions.ILegacyTransition() { + @Override + public void onAnimationStart(int transit, RemoteAnimationTarget[] apps, + RemoteAnimationTarget[] wallpapers, RemoteAnimationTarget[] nonApps, + IRemoteAnimationFinishedCallback finishedCallback, + SurfaceControl.Transaction t) { + boolean openingToSide = false; + if (apps != null) { + for (int i = 0; i < apps.length; ++i) { + if (apps[i].mode == MODE_OPENING + && mSideStage.containsTask(apps[i].taskId)) { + openingToSide = true; + break; + } + } + } else if (mSideStage.getChildCount() != 0) { + // There are chances the entering app transition got canceled by performing + // rotation transition. Checks if there is any child task existed in split + // screen before fallback to cancel entering flow. + openingToSide = true; + } + + if (isEnteringSplit && !openingToSide && apps != null) { + mMainExecutor.execute(() -> exitSplitScreen( + mSideStage.getChildCount() == 0 ? mMainStage : mSideStage, + EXIT_REASON_UNKNOWN)); + } + + if (apps != null) { + for (int i = 0; i < apps.length; ++i) { + if (apps[i].mode == MODE_OPENING) { + t.show(apps[i].leash); + } + } + } + t.apply(); + + if (finishedCallback != null) { + try { + finishedCallback.onAnimationFinished(); + } catch (RemoteException e) { + Slog.e(TAG, "Error finishing legacy transition: ", e); + } + } + + + if (!isEnteringSplit && apps != null) { + final WindowContainerTransaction evictWct = new WindowContainerTransaction(); + prepareEvictNonOpeningChildTasks(position, apps, evictWct); + mSyncQueue.queue(evictWct); + } + } + }; + + final WindowContainerTransaction wct = new WindowContainerTransaction(); + options = resolveStartStage(STAGE_TYPE_UNDEFINED, position, options, wct); + + // If split still not active, apply windows bounds first to avoid surface reset to + // wrong pos by SurfaceAnimator from wms. + if (isEnteringSplit && mLogger.isEnterRequestedByDrag()) { + updateWindowBounds(mSplitLayout, wct); + } + mSplitRequest = new SplitRequest(intent.getIntent(), position); + 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) { + void startTasks(int taskId1, @Nullable Bundle options1, int taskId2, + @Nullable Bundle options2, @SplitPosition int splitPosition, float splitRatio, + @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); + prepareEvictChildTasksIfSplitActive(wct); + setSideStagePosition(splitPosition, wct); + options1 = options1 != null ? options1 : new Bundle(); + addActivityOptions(options1, mSideStage); + wct.startTask(taskId1, options1); + + startWithTask(wct, taskId2, options2, splitRatio, remoteTransition, instanceId); + } + + /** Start an intent and a task to a split pair in one transition. */ + void startIntentAndTask(PendingIntent pendingIntent, Intent fillInIntent, + @Nullable Bundle options1, int taskId, @Nullable Bundle options2, + @SplitPosition int splitPosition, float splitRatio, + @Nullable RemoteTransition remoteTransition, InstanceId instanceId) { + final WindowContainerTransaction wct = new WindowContainerTransaction(); + prepareEvictChildTasksIfSplitActive(wct); + setSideStagePosition(splitPosition, wct); + options1 = options1 != null ? options1 : new Bundle(); + addActivityOptions(options1, mSideStage); + wct.sendPendingIntent(pendingIntent, fillInIntent, options1); + + startWithTask(wct, taskId, options2, splitRatio, remoteTransition, instanceId); + } + + /** Starts a shortcut and a task to a split pair in one transition. */ + void startShortcutAndTask(ShortcutInfo shortcutInfo, @Nullable Bundle options1, + int taskId, @Nullable Bundle options2, @SplitPosition int splitPosition, + float splitRatio, @Nullable RemoteTransition remoteTransition, InstanceId instanceId) { + final WindowContainerTransaction wct = new WindowContainerTransaction(); + prepareEvictChildTasksIfSplitActive(wct); + setSideStagePosition(splitPosition, wct); + options1 = options1 != null ? options1 : new Bundle(); + addActivityOptions(options1, mSideStage); + wct.startShortcut(mContext.getPackageName(), shortcutInfo, options1); + startWithTask(wct, taskId, options2, splitRatio, remoteTransition, instanceId); + } + + /** + * Starts with the second task to a split pair in one transition. + * + * @param wct transaction to start the first task + * @param instanceId if {@code null}, will not log. Otherwise it will be used in + * {@link SplitscreenEventLogger#logEnter(float, int, int, int, int, boolean)} + */ + private void startWithTask(WindowContainerTransaction wct, int mainTaskId, + @Nullable Bundle mainOptions, float splitRatio, + @Nullable RemoteTransition remoteTransition, InstanceId instanceId) { + 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 */); + } 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); + setRootForceTranslucent(false, wct); // Make sure the launch options will put tasks in the corresponding split roots + mainOptions = mainOptions != null ? mainOptions : new Bundle(); 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); + TRANSIT_SPLIT_SCREEN_PAIR_OPEN, wct, remoteTransition, this, null, 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); + /** Starts a pair of tasks using legacy transition. */ + void startTasksWithLegacyTransition(int taskId1, @Nullable Bundle options1, + int taskId2, @Nullable Bundle options2, @SplitPosition int splitPosition, + float splitRatio, RemoteAnimationAdapter adapter, InstanceId instanceId) { + final WindowContainerTransaction wct = new WindowContainerTransaction(); + if (options1 == null) options1 = new Bundle(); + if (taskId2 == INVALID_TASK_ID) { + // Launching a solo task. + ActivityOptions activityOptions = ActivityOptions.fromBundle(options1); + activityOptions.update(ActivityOptions.makeRemoteAnimation(adapter)); + options1 = activityOptions.toBundle(); + addActivityOptions(options1, null /* launchTarget */); + wct.startTask(taskId1, options1); + mSyncQueue.queue(wct); + return; + } + + addActivityOptions(options1, mSideStage); + wct.startTask(taskId1, options1); + mSplitRequest = new SplitRequest(taskId1, taskId2, splitPosition); + startWithLegacyTransition(wct, taskId2, options2, splitPosition, splitRatio, adapter, + instanceId); + } + + /** Starts a pair of intents using legacy transition. */ + void startIntentsWithLegacyTransition(PendingIntent pendingIntent1, Intent fillInIntent1, + @Nullable ShortcutInfo shortcutInfo1, @Nullable Bundle options1, + @Nullable PendingIntent pendingIntent2, Intent fillInIntent2, + @Nullable ShortcutInfo shortcutInfo2, @Nullable Bundle options2, + @SplitPosition int splitPosition, float splitRatio, RemoteAnimationAdapter adapter, + InstanceId instanceId) { + final WindowContainerTransaction wct = new WindowContainerTransaction(); + if (options1 == null) options1 = new Bundle(); + if (pendingIntent2 == null) { + // Launching a solo intent or shortcut as fullscreen. + launchAsFullscreenWithRemoteAnimation(pendingIntent1, fillInIntent1, shortcutInfo1, + options1, adapter, wct); + return; + } + + addActivityOptions(options1, mSideStage); + if (shortcutInfo1 != null) { + wct.startShortcut(mContext.getPackageName(), shortcutInfo1, options1); + } else { + wct.sendPendingIntent(pendingIntent1, fillInIntent1, options1); + mSplitRequest = new SplitRequest(pendingIntent1.getIntent(), + pendingIntent2 != null ? pendingIntent2.getIntent() : null, splitPosition); + } + startWithLegacyTransition(wct, pendingIntent2, fillInIntent2, shortcutInfo2, options2, + splitPosition, 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); + @Nullable Bundle options1, int taskId, @Nullable Bundle options2, + @SplitPosition int splitPosition, float splitRatio, RemoteAnimationAdapter adapter, + InstanceId instanceId) { + final WindowContainerTransaction wct = new WindowContainerTransaction(); + if (options1 == null) options1 = new Bundle(); + if (taskId == INVALID_TASK_ID) { + // Launching a solo intent as fullscreen. + launchAsFullscreenWithRemoteAnimation(pendingIntent, fillInIntent, null, options1, + adapter, wct); + return; + } + + addActivityOptions(options1, mSideStage); + wct.sendPendingIntent(pendingIntent, fillInIntent, options1); + mSplitRequest = new SplitRequest(taskId, pendingIntent.getIntent(), splitPosition); + startWithLegacyTransition(wct, taskId, options2, splitPosition, splitRatio, adapter, + instanceId); + } + + /** Starts a pair of shortcut and task using legacy transition. */ + void startShortcutAndTaskWithLegacyTransition(ShortcutInfo shortcutInfo, + @Nullable Bundle options1, int taskId, @Nullable Bundle options2, + @SplitPosition int splitPosition, float splitRatio, RemoteAnimationAdapter adapter, + InstanceId instanceId) { + final WindowContainerTransaction wct = new WindowContainerTransaction(); + if (options1 == null) options1 = new Bundle(); + if (taskId == INVALID_TASK_ID) { + // Launching a solo shortcut as fullscreen. + launchAsFullscreenWithRemoteAnimation(null, null, shortcutInfo, options1, adapter, wct); + return; + } + + addActivityOptions(options1, mSideStage); + wct.startShortcut(mContext.getPackageName(), shortcutInfo, options1); + startWithLegacyTransition(wct, taskId, options2, splitPosition, splitRatio, adapter, + instanceId); + } + + private void launchAsFullscreenWithRemoteAnimation(@Nullable PendingIntent pendingIntent, + @Nullable Intent fillInIntent, @Nullable ShortcutInfo shortcutInfo, + @Nullable Bundle options, RemoteAnimationAdapter adapter, + WindowContainerTransaction wct) { + LegacyTransitions.ILegacyTransition transition = + (transit, apps, wallpapers, nonApps, finishedCallback, t) -> { + if (apps == null || apps.length == 0) { + onRemoteAnimationFinished(apps); + t.apply(); + try { + adapter.getRunner().onAnimationCancelled(mKeyguardShowing); + } catch (RemoteException e) { + Slog.e(TAG, "Error starting remote animation", e); + } + return; + } + + for (int i = 0; i < apps.length; ++i) { + if (apps[i].mode == MODE_OPENING) { + t.show(apps[i].leash); + } + } + t.apply(); + + try { + adapter.getRunner().onAnimationStart( + transit, apps, wallpapers, nonApps, finishedCallback); + } catch (RemoteException e) { + Slog.e(TAG, "Error starting remote animation", e); + } + }; + + addActivityOptions(options, null /* launchTarget */); + if (shortcutInfo != null) { + wct.startShortcut(mContext.getPackageName(), shortcutInfo, options); + } else if (pendingIntent != null) { + wct.sendPendingIntent(pendingIntent, fillInIntent, options); + } else { + Slog.e(TAG, "Pending intent and shortcut are null is invalid case."); + } + mSyncQueue.queue(transition, WindowManager.TRANSIT_OPEN, wct); } - private void startWithLegacyTransition(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; + private void startWithLegacyTransition(WindowContainerTransaction wct, + @Nullable PendingIntent mainPendingIntent, @Nullable Intent mainFillInIntent, + @Nullable ShortcutInfo mainShortcutInfo, @Nullable Bundle mainOptions, + @SplitPosition int sidePosition, float splitRatio, RemoteAnimationAdapter adapter, + InstanceId instanceId) { + startWithLegacyTransition(wct, INVALID_TASK_ID, mainPendingIntent, mainFillInIntent, + mainShortcutInfo, mainOptions, sidePosition, splitRatio, adapter, instanceId); + } + + private void startWithLegacyTransition(WindowContainerTransaction wct, int mainTaskId, + @Nullable Bundle mainOptions, @SplitPosition int sidePosition, float splitRatio, + RemoteAnimationAdapter adapter, InstanceId instanceId) { + startWithLegacyTransition(wct, mainTaskId, null /* mainPendingIntent */, + null /* mainFillInIntent */, null /* mainShortcutInfo */, mainOptions, sidePosition, + splitRatio, adapter, instanceId); + } + + /** + * @param wct transaction to start the first task + * @param instanceId if {@code null}, will not log. Otherwise it will be used in + * {@link SplitscreenEventLogger#logEnter(float, int, int, int, int, boolean)} + */ + private void startWithLegacyTransition(WindowContainerTransaction wct, int mainTaskId, + @Nullable PendingIntent mainPendingIntent, @Nullable Intent mainFillInIntent, + @Nullable ShortcutInfo mainShortcutInfo, @Nullable Bundle options, + @SplitPosition int sidePosition, float splitRatio, RemoteAnimationAdapter adapter, + InstanceId instanceId) { + if (!isSplitScreenVisible()) { + exitSplitScreen(null /* childrenToTop */, EXIT_REASON_RECREATE_SPLIT); + } + // Init divider first to make divider leash for remote animation target. mSplitLayout.init(); + mSplitLayout.setDivideRatio(splitRatio); + + // Apply surface bounds before animation start. + SurfaceControl.Transaction startT = mTransactionPool.acquire(); + updateSurfaceBounds(mSplitLayout, startT, false /* applyResizingOffset */); + startT.apply(); + mTransactionPool.release(startT); + // Set false to avoid record new bounds with old task still on top; mShouldUpdateRecents = false; mIsDividerRemoteAnimating = true; - final WindowContainerTransaction wct = new WindowContainerTransaction(); + if (mSplitRequest == null) { + mSplitRequest = new SplitRequest(mainTaskId, + mainPendingIntent != null ? mainPendingIntent.getIntent() : null, + sidePosition); + } + setSideStagePosition(sidePosition, wct); + if (!mMainStage.isActive()) { + mMainStage.activate(wct, false /* reparent */); + } + + if (options == null) options = new Bundle(); + addActivityOptions(options, mMainStage); + + updateWindowBounds(mSplitLayout, wct); + wct.reorder(mRootTaskInfo.token, true); + setRootForceTranslucent(false, wct); + + // TODO(b/268008375): Merge APIs to start a split pair into one. + if (mainTaskId != INVALID_TASK_ID) { + options = wrapAsSplitRemoteAnimation(adapter, options); + wct.startTask(mainTaskId, options); + mSyncQueue.queue(wct); + } else { + if (mainShortcutInfo != null) { + wct.startShortcut(mContext.getPackageName(), mainShortcutInfo, options); + } else { + wct.sendPendingIntent(mainPendingIntent, mainFillInIntent, options); + } + mSyncQueue.queue(wrapAsSplitRemoteAnimation(adapter), WindowManager.TRANSIT_OPEN, wct); + } + + mSyncQueue.runInSync(t -> { + setDividerVisibility(true, t); + }); + + setEnterInstanceId(instanceId); + } + + private Bundle wrapAsSplitRemoteAnimation(RemoteAnimationAdapter adapter, Bundle options) { 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 + if (isSplitScreenVisible()) { + mMainStage.evictAllChildren(evictWct); + mSideStage.evictAllChildren(evictWct); + } + IRemoteAnimationRunner wrapper = new IRemoteAnimationRunner.Stub() { @Override public void onAnimationStart(@WindowManager.TransitionOldType int transit, @@ -425,13 +912,6 @@ 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 @@ -440,16 +920,11 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, 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); } @@ -467,61 +942,106 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, }; RemoteAnimationAdapter wrappedAdapter = new RemoteAnimationAdapter( wrapper, adapter.getDuration(), adapter.getStatusBarTransitionDelay()); + ActivityOptions activityOptions = ActivityOptions.fromBundle(options); + activityOptions.update(ActivityOptions.makeRemoteAnimation(wrappedAdapter)); + return activityOptions.toBundle(); + } + + private LegacyTransitions.ILegacyTransition wrapAsSplitRemoteAnimation( + RemoteAnimationAdapter adapter) { + LegacyTransitions.ILegacyTransition transition = + (transit, apps, wallpapers, nonApps, finishedCallback, t) -> { + if (apps == null || apps.length == 0) { + onRemoteAnimationFinished(apps); + t.apply(); + try { + adapter.getRunner().onAnimationCancelled(mKeyguardShowing); + } catch (RemoteException e) { + Slog.e(TAG, "Error starting remote animation", e); + } + return; + } - if (mainOptions == null) { - mainOptions = ActivityOptions.makeRemoteAnimation(wrappedAdapter).toBundle(); - } else { - ActivityOptions mainActivityOptions = ActivityOptions.fromBundle(mainOptions); - mainActivityOptions.update(ActivityOptions.makeRemoteAnimation(wrappedAdapter)); - mainOptions = mainActivityOptions.toBundle(); - } - - sideOptions = sideOptions != null ? sideOptions : new Bundle(); - setSideStagePosition(sidePosition, wct); + // Wrap the divider bar into non-apps target to animate together. + nonApps = ArrayUtils.appendElement(RemoteAnimationTarget.class, nonApps, + getDividerBarLegacyTarget()); - 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); + for (int i = 0; i < apps.length; ++i) { + if (apps[i].mode == MODE_OPENING) { + t.show(apps[i].leash); + // Reset the surface position of the opening app to prevent offset. + t.setPosition(apps[i].leash, 0, 0); + } + } + t.apply(); + + IRemoteAnimationFinishedCallback wrapCallback = + new IRemoteAnimationFinishedCallback.Stub() { + @Override + public void onAnimationFinished() throws RemoteException { + onRemoteAnimationFinished(apps); + finishedCallback.onAnimationFinished(); + } + }; + Transitions.setRunningRemoteTransitionDelegate(adapter.getCallingApplication()); + try { + adapter.getRunner().onAnimationStart( + transit, apps, wallpapers, nonApps, wrapCallback); + } catch (RemoteException e) { + Slog.e(TAG, "Error starting remote animation", e); + } + }; - // Make sure the launch options will put tasks in the corresponding split roots - addActivityOptions(mainOptions, mMainStage); - addActivityOptions(sideOptions, mSideStage); + return transition; + } - // Add task launch requests - wct.startTask(mainTaskId, mainOptions); - if (withIntent) { - wct.sendPendingIntent(pendingIntent, fillInIntent, sideOptions); - } else { - wct.startTask(sideTaskId, sideOptions); + private void setEnterInstanceId(InstanceId instanceId) { + if (instanceId != null) { + mLogger.enterRequested(instanceId, ENTER_REASON_LAUNCHER); } - // Using legacy transitions, so we can't use blast sync since it conflicts. - mTaskOrganizer.applyTransaction(wct); - mSyncQueue.runInSync(t -> { - setDividerVisibility(true, t); - updateSurfaceBounds(mSplitLayout, t, false /* applyResizingOffset */); - }); } private void onRemoteAnimationFinishedOrCancelled(WindowContainerTransaction evictWct) { mIsDividerRemoteAnimating = false; mShouldUpdateRecents = true; + mSplitRequest = null; // 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. + // multi-instance, we should exit split and expand that app as full screen. if (mMainStage.getChildCount() == 0 || mSideStage.getChildCount() == 0) { mMainExecutor.execute(() -> exitSplitScreen(mMainStage.getChildCount() == 0 - ? mSideStage : mMainStage, EXIT_REASON_UNKNOWN)); + ? mSideStage : mMainStage, EXIT_REASON_UNKNOWN)); + mSplitUnsupportedToast.show(); } else { mSyncQueue.queue(evictWct); + mSyncQueue.runInSync(t -> { + updateSurfaceBounds(mSplitLayout, t, false /* applyResizingOffset */); + }); + } + } + + private void onRemoteAnimationFinished(RemoteAnimationTarget[] apps) { + mIsDividerRemoteAnimating = false; + mShouldUpdateRecents = true; + mSplitRequest = null; + // If any stage has no child after finished animation, that side of the split will display + // nothing. This might happen if starting the same app on the both sides while not + // supporting multi-instance. Exit the split screen and expand that app to full screen. + if (mMainStage.getChildCount() == 0 || mSideStage.getChildCount() == 0) { + mMainExecutor.execute(() -> exitSplitScreen(mMainStage.getChildCount() == 0 + ? mSideStage : mMainStage, EXIT_REASON_UNKNOWN)); + mSplitUnsupportedToast.show(); + return; } + + final WindowContainerTransaction evictWct = new WindowContainerTransaction(); + prepareEvictNonOpeningChildTasks(SPLIT_POSITION_TOP_OR_LEFT, apps, evictWct); + prepareEvictNonOpeningChildTasks(SPLIT_POSITION_BOTTOM_OR_RIGHT, apps, evictWct); + mSyncQueue.queue(evictWct); } + /** * Collects all the current child tasks of a specific split and prepares transaction to evict * them to display. @@ -534,18 +1054,33 @@ 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); } - Bundle resolveStartStage(@StageType int stage, - @SplitPosition int position, @androidx.annotation.Nullable Bundle options, - @androidx.annotation.Nullable WindowContainerTransaction wct) { + void prepareEvictChildTasksIfSplitActive(WindowContainerTransaction wct) { + if (mMainStage.isActive()) { + mMainStage.evictAllChildren(wct); + mSideStage.evictAllChildren(wct); + } + } + + Bundle resolveStartStage(@StageType int stage, @SplitPosition int position, + @Nullable Bundle options, @Nullable WindowContainerTransaction wct) { switch (stage) { case STAGE_TYPE_UNDEFINED: { if (position != SPLIT_POSITION_UNDEFINED) { - if (mMainStage.isActive()) { + if (isSplitScreenVisible()) { // Use the stage of the specified position options = resolveStartStage( position == mSideStagePosition ? STAGE_TYPE_SIDE : STAGE_TYPE_MAIN, @@ -575,7 +1110,7 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, case STAGE_TYPE_MAIN: { if (position != SPLIT_POSITION_UNDEFINED) { // Set the side stage opposite of what we want to the main stage. - setSideStagePosition(SplitLayout.reversePosition(position), wct); + setSideStagePosition(reverseSplitPosition(position), wct); } else { position = getMainStagePosition(); } @@ -599,15 +1134,65 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, @SplitPosition int getMainStagePosition() { - return SplitLayout.reversePosition(mSideStagePosition); + return reverseSplitPosition(mSideStagePosition); } 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 switchSplitPosition(String reason) { + final SurfaceControl.Transaction t = mTransactionPool.acquire(); + mTempRect1.setEmpty(); + final StageTaskListener topLeftStage = + mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mSideStage : mMainStage; + final SurfaceControl topLeftScreenshot = ScreenshotUtils.takeScreenshot(t, + topLeftStage.mRootLeash, mTempRect1, Integer.MAX_VALUE - 1); + final StageTaskListener bottomRightStage = + mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mMainStage : mSideStage; + final SurfaceControl bottomRightScreenshot = ScreenshotUtils.takeScreenshot(t, + bottomRightStage.mRootLeash, mTempRect1, Integer.MAX_VALUE - 1); + mSplitLayout.splitSwitching(t, topLeftStage.mRootLeash, bottomRightStage.mRootLeash, + insets -> { + WindowContainerTransaction wct = new WindowContainerTransaction(); + setSideStagePosition(reverseSplitPosition(mSideStagePosition), wct); + mSyncQueue.queue(wct); + mSyncQueue.runInSync(st -> { + updateSurfaceBounds(mSplitLayout, st, false /* applyResizingOffset */); + st.setPosition(topLeftScreenshot, -insets.left, -insets.top); + st.setPosition(bottomRightScreenshot, insets.left, insets.top); + + final ValueAnimator va = ValueAnimator.ofFloat(1, 0); + va.addUpdateListener(valueAnimator-> { + final float progress = (float) valueAnimator.getAnimatedValue(); + t.setAlpha(topLeftScreenshot, progress); + t.setAlpha(bottomRightScreenshot, progress); + t.apply(); + }); + va.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd( + @androidx.annotation.NonNull Animator animation) { + t.remove(topLeftScreenshot); + t.remove(bottomRightScreenshot); + t.apply(); + mTransactionPool.release(t); + } + }); + va.start(); + }); + }); + + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Switch split position: %s", reason); + mLogger.logSwap(getMainStagePosition(), mMainStage.getTopChildTaskUid(), + getSideStagePosition(), mSideStage.getTopChildTaskUid(), + mSplitLayout.isLandscape()); } void setSideStagePosition(@SplitPosition int sideStagePosition, @@ -627,7 +1212,7 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, onLayoutSizeChanged(mSplitLayout); } else { updateWindowBounds(mSplitLayout, wct); - updateUnfoldBounds(); + sendOnBoundsChanged(); } } } @@ -638,20 +1223,6 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, return; } - if (!mKeyguardShowing && mTopStageAfterFoldDismiss != STAGE_TYPE_UNDEFINED) { - if (ENABLE_SHELL_TRANSITIONS) { - final WindowContainerTransaction wct = new WindowContainerTransaction(); - prepareExitSplitScreen(mTopStageAfterFoldDismiss, wct); - mSplitTransitions.startDismissTransition(null /* transition */, wct, this, - mTopStageAfterFoldDismiss, EXIT_REASON_DEVICE_FOLDED); - } else { - exitSplitScreen( - mTopStageAfterFoldDismiss == STAGE_TYPE_MAIN ? mMainStage : mSideStage, - EXIT_REASON_DEVICE_FOLDED); - } - return; - } - setDividerVisibility(!mKeyguardShowing, null); } @@ -675,8 +1246,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 +1283,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 +1296,56 @@ 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 || childrenToTop.getTopVisibleChildTaskId() == INVALID_TASK_ID) { + mSideStage.removeAllTasks(wct, false /* toTop */); + mMainStage.deactivate(wct, false /* toTop */); + wct.reorder(mRootTaskInfo.token, false /* onTop */); + setRootForceTranslucent(true, wct); + 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); + } + wct.setReparentLeafTaskIfRelaunch(mRootTaskInfo.token, + false /* reparentLeafTaskIfRelaunch */); + 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 */); + setRootForceTranslucent(true, finishedWCT); + 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,22 +1356,59 @@ 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) { + mSplitLayout.flingDividerToDismiss(stageToClose == SPLIT_POSITION_BOTTOM_OR_RIGHT, + 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, + "Unable to update focus on the chosen stage: %s", e.getMessage()); + } + } + + /** * Returns whether the split pair in the recent tasks list should be broken. */ private boolean shouldBreakPairedTaskInRecents(@ExitReason int exitReason) { switch (exitReason) { // One of the apps doesn't support MW case EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW: - // User has explicitly dragged the divider to dismiss split + // User has explicitly dragged the divider to dismiss split case EXIT_REASON_DRAG_DIVIDER: - // Either of the split apps have finished + // Either of the split apps have finished case EXIT_REASON_APP_FINISHED: - // One of the children enters PiP + // One of the children enters PiP case EXIT_REASON_CHILD_TASK_ENTER_PIP: - // One of the apps occludes lock screen. + // One of the apps occludes lock screen. case EXIT_REASON_SCREEN_LOCKED_SHOW_ON_TOP: - // User has unlocked the device after folded + // User has unlocked the device after folded case EXIT_REASON_DEVICE_FOLDED: + // The device is folded + case EXIT_REASON_FULLSCREEN_SHORTCUT: + // User has used a keyboard shortcut to go back to fullscreen from split return true; default: return false; @@ -783,8 +1423,18 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, private void prepareExitSplitScreen(@StageType int stageToTop, @NonNull WindowContainerTransaction wct) { if (!mMainStage.isActive()) return; - mSideStage.removeAllTasks(wct, stageToTop == STAGE_TYPE_SIDE); - mMainStage.deactivate(wct, stageToTop == STAGE_TYPE_MAIN); + // Set the dismiss-to-top side to fullscreen for dismiss transition. + // Reparent the non-dismiss-to-top side to properly update its visibility. + if (stageToTop == STAGE_TYPE_MAIN) { + wct.setBounds(mMainStage.mRootTaskInfo.token, null /* bounds */); + mSideStage.removeAllTasks(wct, false /* toTop */); + } else if (stageToTop == STAGE_TYPE_SIDE) { + wct.setBounds(mSideStage.mRootTaskInfo.token, null /* bounds */); + mMainStage.deactivate(wct, false /* toTop */); + } else { + mSideStage.removeAllTasks(wct, false /* toTop */); + mMainStage.deactivate(wct, false /* toTop */); + } } private void prepareEnterSplitScreen(WindowContainerTransaction wct) { @@ -799,6 +1449,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 +1457,14 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, mMainStage.activate(wct, true /* includingTopTask */); updateWindowBounds(mSplitLayout, wct); wct.reorder(mRootTaskInfo.token, true); + setRootForceTranslucent(false, wct); } void finishEnterSplitScreen(SurfaceControl.Transaction t) { mSplitLayout.init(); setDividerVisibility(true, t); updateSurfaceBounds(mSplitLayout, t, false /* applyResizingOffset */); + t.show(mRootTaskLeash); setSplitsVisible(true); mShouldUpdateRecents = true; updateRecentTasksSplitPair(); @@ -838,8 +1491,14 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, return SPLIT_POSITION_UNDEFINED; } - private void addActivityOptions(Bundle opts, StageTaskListener stage) { - opts.putParcelable(KEY_LAUNCH_ROOT_TASK_TOKEN, stage.mRootTaskInfo.token); + private void addActivityOptions(Bundle opts, @Nullable StageTaskListener launchTarget) { + if (launchTarget != null) { + opts.putParcelable(KEY_LAUNCH_ROOT_TASK_TOKEN, launchTarget.mRootTaskInfo.token); + } + // Put BAL flags to avoid activity start aborted. Otherwise, flows like shortcut to split + // will be canceled. + opts.putBoolean(KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED, true); + opts.putBoolean(KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED_BY_PERMISSION, true); } void updateActivityOptions(Bundle opts, @SplitPosition int position) { @@ -860,6 +1519,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 +1535,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,11 +1568,6 @@ 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 updateRecentTasksSplitPair() { if (!mShouldUpdateRecents) { return; @@ -921,7 +1587,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 +1601,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 +1622,6 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, mDisplayInsetsController.addInsetsChangedListener(mDisplayId, mSplitLayout); } - if (mMainUnfoldController != null && mSideUnfoldController != null) { - mMainUnfoldController.init(); - mSideUnfoldController.init(); - } - onRootTaskAppeared(); } @@ -979,13 +1635,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 +1660,8 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, } mRootTaskInfo = null; + mRootTaskLeash = null; + mIsRootTranslucent = false; } @@ -1025,10 +1678,45 @@ 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); + setRootForceTranslucent(true, wct); + mSplitLayout.getInvisibleBounds(mTempRect1); + wct.setBounds(mSideStage.mRootTaskInfo.token, mTempRect1); + mSyncQueue.queue(wct); + mSyncQueue.runInSync(t -> { + t.setPosition(mSideStage.mRootLeash, mTempRect1.left, mTempRect1.top); + }); + } + + void onChildTaskAppeared(StageListenerImpl stageListener, int taskId) { + // Handle entering split screen while there is a split pair running in the background. + if (stageListener == mSideStageListener && !isSplitScreenVisible() && isSplitActive() + && mSplitRequest == null) { + if (mIsDropEntering) { + mSplitLayout.resetDividerPosition(); + } else { + mSplitLayout.setDividerAtBorder(mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT); + } + final WindowContainerTransaction wct = new WindowContainerTransaction(); + mMainStage.reparentTopTask(wct); + mMainStage.evictAllChildren(wct); + mSideStage.evictOtherChildren(wct, taskId); + updateWindowBounds(mSplitLayout, wct); + wct.reorder(mRootTaskInfo.token, true); + setRootForceTranslucent(false, wct); + + mSyncQueue.queue(wct); + mSyncQueue.runInSync(t -> { + if (mIsDropEntering) { + updateSurfaceBounds(mSplitLayout, t, false /* applyResizingOffset */); + mIsDropEntering = false; + } else { + mShowDecorImmediately = true; + mSplitLayout.flingDividerToCenter(); + } + }); + } } private void onRootTaskVanished() { @@ -1040,7 +1728,20 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, mDisplayInsetsController.removeInsetsChangedListener(mDisplayId, mSplitLayout); } + private void setRootForceTranslucent(boolean translucent, WindowContainerTransaction wct) { + if (mIsRootTranslucent == translucent) return; + + mIsRootTranslucent = translucent; + wct.setForceTranslucent(mRootTaskInfo.token, translucent); + } + private void onStageVisibilityChanged(StageListenerImpl stageListener) { + // If split didn't active, just ignore this callback because we should already did these + // on #applyExitSplitScreen. + if (!isSplitActive()) { + return; + } + final boolean sideStageVisible = mSideStageListener.mVisible; final boolean mainStageVisible = mMainStageListener.mVisible; @@ -1049,22 +1750,26 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, return; } + // Check if it needs to dismiss split screen when both stage invisible. + if (!mainStageVisible && mExitSplitScreenOnHide) { + exitSplitScreen(null /* childrenToTop */, EXIT_REASON_RETURN_HOME); + return; + } + + final WindowContainerTransaction wct = new WindowContainerTransaction(); if (!mainStageVisible) { - // Both stages are not visible, check if it needs to dismiss split screen. - if (mExitSplitScreenOnHide - // Don't dismiss split screen when both stages are not visible due to sleeping - // display. - || (!mMainStage.mRootTaskInfo.isSleeping - && !mSideStage.mRootTaskInfo.isSleeping)) { - exitSplitScreen(null /* childrenToTop */, EXIT_REASON_RETURN_HOME); - } + // Split entering background. + wct.setReparentLeafTaskIfRelaunch(mRootTaskInfo.token, + true /* setReparentLeafTaskIfRelaunch */); + setRootForceTranslucent(true, wct); + } else { + wct.setReparentLeafTaskIfRelaunch(mRootTaskInfo.token, + false /* setReparentLeafTaskIfRelaunch */); + setRootForceTranslucent(false, wct); } - mSyncQueue.runInSync(t -> { - t.setVisibility(mSideStage.mRootLeash, sideStageVisible) - .setVisibility(mMainStage.mRootLeash, mainStageVisible); - setDividerVisibility(mainStageVisible, t); - }); + mSyncQueue.queue(wct); + setDividerVisibility(mainStageVisible, null); } private void setDividerVisibility(boolean visible, @Nullable SurfaceControl.Transaction t) { @@ -1073,14 +1778,14 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, } ProtoLog.d(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, - "%s: Request to %s divider bar from %s.", TAG, + "Request to %s divider bar from %s.", (visible ? "show" : "hide"), Debug.getCaller()); // Defer showing divider bar after keyguard dismissed, so it won't interfere with keyguard // dismissing animation. if (visible && mKeyguardShowing) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, - "%s: Defer showing divider bar due to keyguard showing.", TAG); + " Defer showing divider bar due to keyguard showing."); return; } @@ -1089,7 +1794,7 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, if (mIsDividerRemoteAnimating) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, - "%s: Skip animating divider bar due to it's remote animating.", TAG); + " Skip animating divider bar due to it's remote animating."); return; } @@ -1104,12 +1809,12 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, final SurfaceControl dividerLeash = mSplitLayout.getDividerLeash(); if (dividerLeash == null) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, - "%s: Skip animating divider bar due to divider leash not ready.", TAG); + " Skip animating divider bar due to divider leash not ready."); return; } if (mIsDividerRemoteAnimating) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, - "%s: Skip animating divider bar due to it's remote animating.", TAG); + " Skip animating divider bar due to it's remote animating."); return; } @@ -1136,17 +1841,20 @@ 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(); } @Override public void onAnimationEnd(Animator animation) { + if (dividerLeash != null && dividerLeash.isValid()) { + transaction.setAlpha(dividerLeash, 1); + transaction.apply(); + } mTransactionPool.release(transaction); mDividerFadeInAnimator = null; } @@ -1161,27 +1869,57 @@ 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); + 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); + mSplitLayout.flingDividerToDismiss( + mSideStagePosition != SPLIT_POSITION_BOTTOM_OR_RIGHT, + EXIT_REASON_APP_FINISHED); + } else if (!isSplitScreenVisible() && mSplitRequest == null) { + // Dismiss split screen in the background once any sides of the split become empty. + exitSplitScreen(null /* childrenToTop */, EXIT_REASON_APP_FINISHED); } - } else if (isSideStage && !mMainStage.isActive()) { - final WindowContainerTransaction wct = new WindowContainerTransaction(); + } else if (isSideStage && hasChildren && !mMainStage.isActive()) { mSplitLayout.init(); - prepareEnterSplitScreen(wct); + + final WindowContainerTransaction wct = new WindowContainerTransaction(); + if (mIsDropEntering) { + prepareEnterSplitScreen(wct); + } else { + // TODO (b/238697912) : Add the validation to prevent entering non-recovered status + onSplitScreenEnter(); + mSplitLayout.setDividerAtBorder(mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT); + mMainStage.activate(wct, true /* includingTopTask */); + updateWindowBounds(mSplitLayout, wct); + wct.reorder(mRootTaskInfo.token, true); + setRootForceTranslucent(false, wct); + } + mSyncQueue.queue(wct); - mSyncQueue.runInSync(t -> - updateSurfaceBounds(mSplitLayout, t, false /* applyResizingOffset */)); + mSyncQueue.runInSync(t -> { + if (mIsDropEntering) { + updateSurfaceBounds(mSplitLayout, t, false /* applyResizingOffset */); + mIsDropEntering = false; + } else { + mShowDecorImmediately = true; + mSplitLayout.flingDividerToCenter(); + } + }); } if (mMainStageListener.mHasChildren && mSideStageListener.mHasChildren) { mShouldUpdateRecents = true; + mSplitRequest = null; updateRecentTasksSplitPair(); if (!mLogger.hasStartedSession()) { + if (!mLogger.hasValidEnterSessionId()) { + mLogger.enterRequested(null /*enterSessionId*/, ENTER_REASON_MULTI_INSTANCE); + } mLogger.logEnter(mSplitLayout.getDividerPositionAsFraction(), getMainStagePosition(), mMainStage.getTopChildTaskUid(), getSideStagePosition(), mSideStage.getTopChildTaskUid(), @@ -1191,29 +1929,27 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, } @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 */); - mLogger.logSwap(getMainStagePosition(), mMainStage.getTopChildTaskUid(), - getSideStagePosition(), mSideStage.getTopChildTaskUid(), - mSplitLayout.isLandscape()); + switchSplitPosition("double tap"); } @Override @@ -1226,39 +1962,50 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, } @Override - public void onLayoutSizeChanging(SplitLayout layout) { + public void onLayoutSizeChanging(SplitLayout layout, int offsetX, int offsetY) { 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, offsetX, offsetY, mShowDecorImmediately); + mSideStage.onResizing(mTempRect2, mTempRect1, t, offsetX, offsetY, mShowDecorImmediately); t.apply(); mTransactionPool.release(t); } @Override public void onLayoutSizeChanged(SplitLayout layout) { + // Reset this flag every time onLayoutSizeChanged. + mShowDecorImmediately = false; + + if (!ENABLE_SHELL_TRANSITIONS) { + // Only need screenshot for legacy case because shell transition should screenshot + // itself during transition. + final SurfaceControl.Transaction startT = mTransactionPool.acquire(); + mMainStage.screenshotIfNeeded(startT); + mSideStage.screenshotIfNeeded(startT); + mTransactionPool.release(startT); + } + final WindowContainerTransaction wct = new WindowContainerTransaction(); - updateWindowBounds(layout, wct); - updateUnfoldBounds(); - mSyncQueue.queue(wct); - mSyncQueue.runInSync(t -> { - setResizingSplits(false /* resizing */); - updateSurfaceBounds(layout, t, false /* applyResizingOffset */); - mMainStage.onResized(t); - mSideStage.onResized(t); - }); - mLogger.logResize(mSplitLayout.getDividerPositionAsFraction()); - } + boolean sizeChanged = updateWindowBounds(layout, wct); + if (!sizeChanged) return; - private void updateUnfoldBounds() { - if (mMainUnfoldController != null && mSideUnfoldController != null) { - mMainUnfoldController.onLayoutChanged(getMainStageBounds(), getMainStagePosition(), - isLandscape()); - mSideUnfoldController.onLayoutChanged(getSideStageBounds(), getSideStagePosition(), - isLandscape()); + sendOnBoundsChanged(); + if (ENABLE_SHELL_TRANSITIONS) { + mSplitLayout.setDividerInteractive(false, false, "onSplitResizeStart"); + mSplitTransitions.startResizeTransition(wct, this, (finishWct, t) -> + mSplitLayout.setDividerInteractive(true, false, "onSplitResizeFinish")); + } else { + mSyncQueue.queue(wct); + mSyncQueue.runInSync(t -> { + updateSurfaceBounds(layout, t, false /* applyResizingOffset */); + mMainStage.onResized(t); + mSideStage.onResized(t); + }); } + mLogger.logResize(mSplitLayout.getDividerPositionAsFraction()); } private boolean isLandscape() { @@ -1268,13 +2015,16 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, /** * 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 + * + * @return true if stage bounds actually . */ - private void updateWindowBounds(SplitLayout layout, WindowContainerTransaction wct) { + private boolean 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); + return layout.applyTaskChanges(wct, topLeftStage.mRootTaskInfo, + bottomRightStage.mRootTaskInfo); } void updateSurfaceBounds(@Nullable SplitLayout layout, @NonNull SurfaceControl.Transaction t, @@ -1288,16 +2038,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 +2069,7 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, if (displayId != DEFAULT_DISPLAY) { return; } - mDisplayController.addDisplayChangingController(this::onRotateDisplay); + mDisplayController.addDisplayChangingController(this::onDisplayChange); } @Override @@ -1340,26 +2080,46 @@ 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) { - mTopStageAfterFoldDismiss = STAGE_TYPE_UNDEFINED; + @VisibleForTesting + void onFoldedStateChanged(boolean folded) { + int topStageAfterFoldDismiss = STAGE_TYPE_UNDEFINED; if (!folded) return; + if (!mMainStage.isActive()) return; + if (mMainStage.isFocused()) { - mTopStageAfterFoldDismiss = STAGE_TYPE_MAIN; + topStageAfterFoldDismiss = STAGE_TYPE_MAIN; } else if (mSideStage.isFocused()) { - mTopStageAfterFoldDismiss = STAGE_TYPE_SIDE; + topStageAfterFoldDismiss = STAGE_TYPE_SIDE; + } + + if (ENABLE_SHELL_TRANSITIONS) { + final WindowContainerTransaction wct = new WindowContainerTransaction(); + prepareExitSplitScreen(topStageAfterFoldDismiss, wct); + mSplitTransitions.startDismissTransition(wct, this, + topStageAfterFoldDismiss, EXIT_REASON_DEVICE_FOLDED); + } else { + exitSplitScreen( + topStageAfterFoldDismiss == STAGE_TYPE_MAIN ? mMainStage : mSideStage, + EXIT_REASON_DEVICE_FOLDED); } } @@ -1373,6 +2133,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. @@ -1392,6 +2168,7 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, @StageType private int getStageType(StageTaskListener stage) { + if (stage == null) return STAGE_TYPE_UNDEFINED; return stage == mMainStage ? STAGE_TYPE_MAIN : STAGE_TYPE_SIDE; } @@ -1400,7 +2177,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 +2205,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,23 +2220,15 @@ 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) { final int activityType = triggerTask.getActivityType(); - if (activityType == ACTIVITY_TYPE_ASSISTANT) { - // We don't want assistant panel to dismiss split screen, so do nothing. - } 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. - prepareExitSplitScreen(STAGE_TYPE_UNDEFINED, out); - mSplitTransitions.startDismissTransition(transition, out, this, - STAGE_TYPE_UNDEFINED, EXIT_REASON_UNKNOWN); + if (activityType == ACTIVITY_TYPE_HOME || activityType == ACTIVITY_TYPE_RECENTS) { + // starting recents/home, so don't handle this and let it fall-through to + // the remote handler. + return null; } } } else { @@ -1466,12 +2236,36 @@ 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 /* consumedCallback */, null /* finishedCallback */); } } 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); + } + } + @Override public void mergeAnimation(IBinder transition, TransitionInfo info, SurfaceControl.Transaction t, IBinder mergeTarget, @@ -1479,17 +2273,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 +2290,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,38 +2334,67 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, // Use normal animations. return false; + } else if (mMixedHandler != null && TransitionUtil.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) { - shouldAnimate = startPendingRecentAnimation(transition, info, startTransaction); - } else if (mSplitTransitions.mPendingDismiss != null - && mSplitTransitions.mPendingDismiss.mTransition == transition) { + if (mSplitTransitions.isPendingEnter(transition)) { + shouldAnimate = startPendingEnterAnimation( + transition, info, startTransaction, finishTransaction); + } else if (mSplitTransitions.isPendingDismiss(transition)) { shouldAnimate = startPendingDismissAnimation( mSplitTransitions.mPendingDismiss, info, startTransaction, finishTransaction); + if (shouldAnimate) { + mSplitTransitions.applyDismissTransition(transition, info, + startTransaction, finishTransaction, finishCallback, mRootTaskInfo.token, + mMainStage.mRootTaskInfo.token, mSideStage.mRootTaskInfo.token, + mMainStage.getSplitDecorManager(), mSideStage.getSplitDecorManager()); + return true; + } + } else if (mSplitTransitions.isPendingResize(transition)) { + mSplitTransitions.playResizeAnimation(transition, info, startTransaction, + finishTransaction, finishCallback, mMainStage.mRootTaskInfo.token, + mSideStage.mRootTaskInfo.token, mMainStage.getSplitDecorManager(), + mSideStage.getSplitDecorManager()); + return true; } 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() { + /** 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(); - mSplitLayout.resetDividerPosition(); - mTopStageAfterFoldDismiss = STAGE_TYPE_UNDEFINED; } } 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; @@ -1592,17 +2410,21 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, } } - // TODO: fallback logic. Probably start a new transition to exit split before applying - // anything here. Ideally consolidate with transition-merging. if (info.getType() == TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE) { if (mainChild == null && sideChild == null) { - throw new IllegalStateException("Launched a task in split, but didn't receive any" - + " task in transition."); + Log.w(TAG, "Launched a task in split, but didn't receive any task in transition."); + mSplitTransitions.mPendingEnter.cancel(null /* finishedCb */); + return true; } } else { if (mainChild == null || sideChild == null) { - throw new IllegalStateException("Launched 2 tasks in split, but didn't receive" + Log.w(TAG, "Launched 2 tasks in split, but didn't receive" + " 2 tasks in transition. Possibly one of them failed to launch"); + final int dismissTop = mainChild != null ? STAGE_TYPE_MAIN : + (sideChild != null ? STAGE_TYPE_SIDE : STAGE_TYPE_UNDEFINED); + mSplitTransitions.mPendingEnter.cancel( + (cancelWct, cancelT) -> prepareExitSplitScreen(dismissTop, cancelWct)); + return true; } } @@ -1623,13 +2445,64 @@ 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, + public void goToFullscreenFromSplit() { + boolean leftOrTop; + if (mSideStage.isFocused()) { + leftOrTop = (mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT); + } else { + leftOrTop = (mSideStagePosition == SPLIT_POSITION_BOTTOM_OR_RIGHT); + } + mSplitLayout.flingDividerToDismiss(!leftOrTop, EXIT_REASON_FULLSCREEN_SHORTCUT); + } + + /** Move the specified task to fullscreen, regardless of focus state. */ + public void moveTaskToFullscreen(int taskId) { + boolean leftOrTop; + if (mMainStage.containsTask(taskId)) { + leftOrTop = (mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT); + } else if (mSideStage.containsTask(taskId)) { + leftOrTop = (mSideStagePosition == SPLIT_POSITION_BOTTOM_OR_RIGHT); + } else { + return; + } + mSplitLayout.flingDividerToDismiss(!leftOrTop, EXIT_REASON_FULLSCREEN_SHORTCUT); + + } + + boolean isLaunchToSplit(TaskInfo taskInfo) { + return getActivateSplitPosition(taskInfo) != SPLIT_POSITION_UNDEFINED; + } + + int getActivateSplitPosition(TaskInfo taskInfo) { + if (mSplitRequest == null || taskInfo == null) { + return SPLIT_POSITION_UNDEFINED; + } + if (mSplitRequest.mActivateTaskId != 0 + && mSplitRequest.mActivateTaskId2 == taskInfo.taskId) { + return mSplitRequest.mActivatePosition; + } + if (mSplitRequest.mActivateTaskId == taskInfo.taskId) { + return mSplitRequest.mActivatePosition; + } + final String packageName1 = SplitScreenUtils.getPackageName(mSplitRequest.mStartIntent); + final String basePackageName = SplitScreenUtils.getPackageName(taskInfo.baseIntent); + if (packageName1 != null && packageName1.equals(basePackageName)) { + return mSplitRequest.mActivatePosition; + } + final String packageName2 = SplitScreenUtils.getPackageName(mSplitRequest.mStartIntent2); + if (packageName2 != null && packageName2.equals(basePackageName)) { + return mSplitRequest.mActivatePosition; + } + return SPLIT_POSITION_UNDEFINED; + } + + /** 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 @@ -1638,37 +2511,44 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, // 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)); + if (toStage == STAGE_TYPE_UNDEFINED) { + 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()."); } - 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)); + 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()."); } - Log.w(TAG, "Expected onTaskVanished on " + mSideStage - + " to have been called with [" + tasksLeft.toString() - + "] before startAnimation()."); } 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) { - for (TransitionInfo.Change change : info.getChanges()) { - final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo(); - if (taskInfo != null - && taskInfo.getWindowingMode() == WINDOWING_MODE_FULLSCREEN) { - recentTasks.removeSplitPair(taskInfo.taskId); + if (shouldBreakPairedTaskInRecents(dismissReason) && mShouldUpdateRecents) { + if (toStage == STAGE_TYPE_UNDEFINED) { + for (TransitionInfo.Change change : info.getChanges()) { + final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo(); + if (taskInfo != null + && taskInfo.getWindowingMode() == WINDOWING_MODE_FULLSCREEN) { + recentTasks.removeSplitPair(taskInfo.taskId); + } } + } else { + recentTasks.removeSplitPair(mMainStage.getTopVisibleChildTaskId()); + recentTasks.removeSplitPair(mSideStage.getTopVisibleChildTaskId()); } } }); @@ -1679,79 +2559,115 @@ 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); + // Hide the non-top stage and set the top one to the fullscreen position. + if (toStage != STAGE_TYPE_UNDEFINED) { + t.hide(toStage == STAGE_TYPE_MAIN ? mSideStage.mRootLeash : mMainStage.mRootLeash); + t.setPosition(toStage == STAGE_TYPE_MAIN + ? mMainStage.mRootLeash : mSideStage.mRootLeash, 0, 0); + } - 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). - mSplitLayout.release(t); - mSplitTransitions.mPendingDismiss = null; - return false; + if (toStage == STAGE_TYPE_UNDEFINED) { + logExit(dismissReason); } else { - logExitToStage(dismissTransition.mReason, - dismissTransition.mDismissTop == STAGE_TYPE_MAIN); + logExitToStage(dismissReason, toStage == 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); + } + + private boolean startPendingDismissAnimation( + @NonNull SplitScreenTransitions.DismissSession 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) { + // 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 { + final @SplitScreen.StageType int dismissTop = dismissTransition.mDismissTop; + // Reparent all tasks after dismiss transition finished. + dismissTransition.setFinishedCallback( + new SplitScreenTransitions.TransitionFinishedCallback() { + @Override + public void onFinished(WindowContainerTransaction wct, + SurfaceControl.Transaction t) { + mSideStage.removeAllTasks(wct, dismissTop == STAGE_TYPE_SIDE); + mMainStage.deactivate(wct, dismissTop == STAGE_TYPE_MAIN); + } + }); + } + + addDividerBarToTransition(info, finishT, false /* show */); return true; } - private boolean startPendingRecentAnimation(@NonNull IBinder transition, - @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction t) { + /** Call this when starting the open-recents animation while split-screen is active. */ + public void onRecentsInSplitAnimationStart(@NonNull SurfaceControl.Transaction t) { setDividerVisibility(false, t); - 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; + /** Call this when the recents animation during split-screen finishes. */ + public void onRecentsInSplitAnimationFinish(WindowContainerTransaction finishWct, + SurfaceControl.Transaction finishT, TransitionInfo info) { + // 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; + } } - 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); + // TODO(b/275664132): Remove dismissing split screen here to fit in back-to-split support. + // Dismiss the split screen if it's not returning to split. + prepareExitSplitScreen(STAGE_TYPE_UNDEFINED, finishWct); + for (TransitionInfo.Change change : info.getChanges()) { + if (change.getTaskInfo() != null && TransitionUtil.isClosingType(change.getMode())) { + finishT.setCrop(change.getLeash(), null).hide(change.getLeash()); + } } + setSplitsVisible(false); + setDividerVisibility(false, finishT); + logExit(EXIT_REASON_UNKNOWN); } 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); } } @@ -1797,11 +2713,29 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, /** * Sets drag info to be logged when splitscreen is next entered. */ - public void logOnDroppedToSplit(@SplitPosition int position, InstanceId dragSessionId) { + public void onDroppedToSplit(@SplitPosition int position, InstanceId dragSessionId) { + if (!isSplitScreenVisible()) { + mIsDropEntering = true; + } + if (!isSplitScreenVisible()) { + // If split running background, exit split first. + exitSplitScreen(null /* childrenToTop */, EXIT_REASON_RECREATE_SPLIT); + } mLogger.enterRequestedByDrag(position, dragSessionId); } /** + * Sets info to be logged when splitscreen is next entered. + */ + public void onRequestToSplit(InstanceId sessionId, int enterReason) { + if (!isSplitScreenVisible()) { + // If split running background, exit split first. + exitSplitScreen(null /* childrenToTop */, EXIT_REASON_RECREATE_SPLIT); + } + mLogger.enterRequested(sessionId, enterReason); + } + + /** * Logs the exit of splitscreen. */ private void logExit(@ExitReason int exitReason) { @@ -1836,6 +2770,11 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, } @Override + public void onChildTaskAppeared(int taskId) { + StageCoordinator.this.onChildTaskAppeared(this, taskId); + } + + @Override public void onStatusChanged(boolean visible, boolean hasChildren) { if (!mHasRootTask) return; @@ -1855,11 +2794,6 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, } @Override - public void onChildTaskEnterPip(int taskId) { - StageCoordinator.this.onStageChildTaskEnterPip(this, taskId); - } - - @Override public void onRootTaskVanished() { reset(); StageCoordinator.this.onRootTaskVanished(); @@ -1872,15 +2806,16 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, if (!ENABLE_SHELL_TRANSITIONS) { StageCoordinator.this.exitSplitScreen(isMainStage ? mMainStage : mSideStage, EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW); + mSplitUnsupportedToast.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); + mSplitUnsupportedToast.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..ead0bcd15c73 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; +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,22 +66,16 @@ 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(); + void onChildTaskAppeared(int taskId); + void onStatusChanged(boolean visible, boolean hasChildren); void onChildTaskStatusChanged(int taskId, boolean present, boolean visible); - void onChildTaskEnterPip(int taskId); - void onRootTaskVanished(); void onNoLongerSupportMultiWindow(); @@ -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 @@ -202,25 +188,17 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener { // Status is managed/synchronized by the transition lifecycle. return; } + mCallbacks.onChildTaskAppeared(taskId); 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) { // Inflates split decor view only when the root task is visible. if (mRootTaskInfo.isVisible != taskInfo.isVisible) { @@ -233,12 +211,29 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener { } mRootTaskInfo = taskInfo; } else if (taskInfo.parentTaskId == mRootTaskInfo.taskId) { - mChildrenTaskInfo.put(taskInfo.taskId, taskInfo); + 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; + } + if (taskInfo.topActivity == null && mChildrenTaskInfo.contains(taskInfo.taskId) + && mChildrenTaskInfo.get(taskInfo.taskId).topActivity != null) { + // If top activity become null, it means the task is about to vanish, we use this + // signal to remove it from children list earlier for smooth dismiss transition. + mChildrenTaskInfo.remove(taskInfo.taskId); + mChildrenLeashes.remove(taskInfo.taskId); + } else { + 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 */); + if (!ENABLE_SHELL_TRANSITIONS && mChildrenLeashes.contains(taskInfo.taskId)) { + updateChildTaskSurface(taskInfo, mChildrenLeashes.get(taskInfo.taskId), + false /* firstAppeared */); } } else { throw new IllegalArgumentException(this + "\n Unknown task: " + taskInfo @@ -258,6 +253,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,21 +262,11 @@ 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; } sendStatusChanged(); - } else { - throw new IllegalArgumentException(this + "\n Unknown task: " + taskInfo - + "\n mRootTaskInfo: " + mRootTaskInfo); - } - - if (mStageTaskUnfoldController != null) { - mStageTaskUnfoldController.onTaskVanished(taskInfo); } } @@ -305,18 +291,38 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener { } } - void onResizing(Rect newBounds, SurfaceControl.Transaction t) { + void onResizing(Rect newBounds, Rect sideBounds, SurfaceControl.Transaction t, int offsetX, + int offsetY, boolean immediately) { if (mSplitDecorManager != null && mRootTaskInfo != null) { - mSplitDecorManager.onResizing(mRootTaskInfo, newBounds, t); + mSplitDecorManager.onResizing(mRootTaskInfo, newBounds, sideBounds, t, offsetX, + offsetY, immediately); } } void onResized(SurfaceControl.Transaction t) { if (mSplitDecorManager != null) { - mSplitDecorManager.onResized(t); + mSplitDecorManager.onResized(t, null); } } + void screenshotIfNeeded(SurfaceControl.Transaction t) { + if (mSplitDecorManager != null) { + mSplitDecorManager.screenshotIfNeeded(t); + } + } + + void fadeOutDecor(Runnable finishedCallback) { + if (mSplitDecorManager != null) { + mSplitDecorManager.fadeOutDecor(finishedCallback); + } else { + finishedCallback.run(); + } + } + + SplitDecorManager getSplitDecorManager() { + return mSplitDecorManager; + } + 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 +347,27 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener { } } + void evictOtherChildren(WindowContainerTransaction wct, int taskId) { + for (int i = mChildrenTaskInfo.size() - 1; i >= 0; i--) { + final ActivityManager.RunningTaskInfo taskInfo = mChildrenTaskInfo.valueAt(i); + if (taskId == taskInfo.taskId) continue; + wct.reparent(taskInfo.token, null /* parent */, false /* onTop */); + } + } + + 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 +377,17 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener { } } + void reparentTopTask(WindowContainerTransaction wct) { + wct.reparentTasks(null /* currentParent */, mRootTaskInfo.token, + CONTROLLED_WINDOWING_MODES, CONTROLLED_ACTIVITY_TYPES, + true /* onTop */, true /* reparentTopOnly */); + } + + 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 +401,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/StageTaskUnfoldController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskUnfoldController.java deleted file mode 100644 index 59eecb5db136..000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskUnfoldController.java +++ /dev/null @@ -1,281 +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.splitscreen; - -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 android.animation.RectEvaluator; -import android.animation.TypeEvaluator; -import android.annotation.NonNull; -import android.app.ActivityManager; -import android.content.Context; -import android.graphics.Insets; -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.common.split.SplitScreenConstants.SplitPosition; -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) { - // Only handle child task surface here. - if (!taskInfo.hasParentTask()) return; - - 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) { - if (!taskInfo.hasParentTask()) return; - - 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, @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); - } - - 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 @SplitPosition int mSplitPosition = SPLIT_POSITION_UNDEFINED; - private boolean mIsLandscape = false; - - private AnimationContext(SurfaceControl leash) { - this.mLeash = leash; - update(); - } - - private void update(@SplitPosition int splitPosition, boolean isLandscape) { - this.mSplitPosition = splitPosition; - this.mIsLandscape = isLandscape; - update(); - } - - private void update() { - mStartCropRect.set(mStageBounds); - - boolean taskbarExpanded = isTaskbarExpanded(); - if (taskbarExpanded) { - // Only insets the cropping window with taskbar when taskbar is expanded - 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); - - // Sides adjacent to split bar or task bar are not be animated. - Insets margins; - if (mIsLandscape) { // Left and right splits. - margins = getLandscapeMargins(margin, taskbarExpanded); - } else { // Top and bottom splits. - margins = getPortraitMargins(margin, taskbarExpanded); - } - mStartCropRect.inset(margins); - } - - private Insets getLandscapeMargins(int margin, boolean taskbarExpanded) { - int left = margin; - int right = margin; - int bottom = taskbarExpanded ? 0 : margin; // Taskbar margin. - if (mSplitPosition == SPLIT_POSITION_TOP_OR_LEFT) { - right = 0; // Divider margin. - } else { - left = 0; // Divider margin. - } - return Insets.of(left, /* top= */ margin, right, bottom); - } - - private Insets getPortraitMargins(int margin, boolean taskbarExpanded) { - int bottom = margin; - int top = margin; - if (mSplitPosition == SPLIT_POSITION_TOP_OR_LEFT) { - bottom = 0; // Divider margin. - } else { // Bottom split. - top = 0; // Divider margin. - if (taskbarExpanded) { - bottom = 0; // Taskbar margin. - } - } - return Insets.of(/* left= */ margin, top, /* right= */ margin, bottom); - } - - private boolean isTaskbarExpanded() { - return mTaskbarInsetsSource != null - && mTaskbarInsetsSource.getFrame().height() >= mExpandedTaskBarHeight; - } - } -} 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/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/AbsSplashWindowCreator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/AbsSplashWindowCreator.java new file mode 100644 index 000000000000..1ddd8f9a3a14 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/AbsSplashWindowCreator.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.startingsurface; + +import android.content.Context; +import android.content.pm.ActivityInfo; +import android.hardware.display.DisplayManager; +import android.view.Display; + +import com.android.wm.shell.common.ShellExecutor; + +// abstract class to create splash screen window(or windowless window) +abstract class AbsSplashWindowCreator { + protected static final String TAG = StartingWindowController.TAG; + protected final SplashscreenContentDrawer mSplashscreenContentDrawer; + protected final Context mContext; + protected final DisplayManager mDisplayManager; + protected final ShellExecutor mSplashScreenExecutor; + protected final StartingSurfaceDrawer.StartingWindowRecordManager mStartingWindowRecordManager; + + private StartingSurface.SysuiProxy mSysuiProxy; + + AbsSplashWindowCreator(SplashscreenContentDrawer contentDrawer, Context context, + ShellExecutor splashScreenExecutor, DisplayManager displayManager, + StartingSurfaceDrawer.StartingWindowRecordManager startingWindowRecordManager) { + mSplashscreenContentDrawer = contentDrawer; + mContext = context; + mSplashScreenExecutor = splashScreenExecutor; + mDisplayManager = displayManager; + mStartingWindowRecordManager = startingWindowRecordManager; + } + + int getSplashScreenTheme(int splashScreenThemeResId, ActivityInfo activityInfo) { + return splashScreenThemeResId != 0 + ? splashScreenThemeResId + : activityInfo.getThemeResource() != 0 ? activityInfo.getThemeResource() + : com.android.internal.R.style.Theme_DeviceDefault_DayNight; + } + + protected Display getDisplay(int displayId) { + return mDisplayManager.getDisplay(displayId); + } + + void setSysuiProxy(StartingSurface.SysuiProxy sysuiProxy) { + mSysuiProxy = sysuiProxy; + } + + protected void requestTopUi(boolean requestTopUi) { + if (mSysuiProxy != null) { + mSysuiProxy.requestTopUi(requestTopUi, TAG); + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SnapshotWindowCreator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SnapshotWindowCreator.java new file mode 100644 index 000000000000..20c4d5ae5f58 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SnapshotWindowCreator.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.startingsurface; + +import android.window.StartingWindowInfo; +import android.window.TaskSnapshot; + +import com.android.wm.shell.common.ShellExecutor; + +class SnapshotWindowCreator { + private final ShellExecutor mMainExecutor; + private final StartingSurfaceDrawer.StartingWindowRecordManager + mStartingWindowRecordManager; + + SnapshotWindowCreator(ShellExecutor mainExecutor, + StartingSurfaceDrawer.StartingWindowRecordManager startingWindowRecordManager) { + mMainExecutor = mainExecutor; + mStartingWindowRecordManager = startingWindowRecordManager; + } + + void makeTaskSnapshotWindow(StartingWindowInfo startingWindowInfo, TaskSnapshot snapshot) { + final int taskId = startingWindowInfo.taskInfo.taskId; + // Remove any existing starting window for this task before adding. + mStartingWindowRecordManager.removeWindow(taskId, true); + final TaskSnapshotWindow surface = TaskSnapshotWindow.create(startingWindowInfo, + startingWindowInfo.appToken, snapshot, mMainExecutor, + () -> mStartingWindowRecordManager.removeWindow(taskId, true)); + if (surface != null) { + final SnapshotWindowRecord tView = new SnapshotWindowRecord(surface, + startingWindowInfo.taskInfo.topActivityType, mMainExecutor); + mStartingWindowRecordManager.addRecord(taskId, tView); + } + } + + private static class SnapshotWindowRecord extends StartingSurfaceDrawer.SnapshotRecord { + private final TaskSnapshotWindow mTaskSnapshotWindow; + + SnapshotWindowRecord(TaskSnapshotWindow taskSnapshotWindow, + int activityType, ShellExecutor removeExecutor) { + super(activityType, removeExecutor); + mTaskSnapshotWindow = taskSnapshotWindow; + mBGColor = mTaskSnapshotWindow.getBackgroundColor(); + } + + @Override + protected void removeImmediately() { + super.removeImmediately(); + mTaskSnapshotWindow.removeImmediately(); + } + + @Override + protected boolean hasImeSurface() { + return mTaskSnapshotWindow.hasImeSurface(); + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashScreenExitAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashScreenExitAnimation.java index 014f02bcf8b7..20da8773f387 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashScreenExitAnimation.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashScreenExitAnimation.java @@ -15,38 +15,20 @@ */ package com.android.wm.shell.startingsurface; -import static android.view.Choreographer.CALLBACK_COMMIT; import static android.view.View.GONE; import static com.android.internal.jank.InteractionJankMonitor.CUJ_SPLASHSCREEN_EXIT_ANIM; import android.animation.Animator; -import android.animation.ValueAnimator; import android.content.Context; -import android.graphics.BlendMode; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.Matrix; -import android.graphics.Paint; -import android.graphics.Point; -import android.graphics.RadialGradient; import android.graphics.Rect; -import android.graphics.Shader; -import android.util.MathUtils; import android.util.Slog; -import android.view.Choreographer; import android.view.SurfaceControl; -import android.view.SyncRtSurfaceTransactionApplier; import android.view.View; -import android.view.ViewGroup; -import android.view.WindowManager; -import android.view.animation.Interpolator; -import android.view.animation.PathInterpolator; import android.window.SplashScreenView; import com.android.internal.jank.InteractionJankMonitor; import com.android.wm.shell.R; -import com.android.wm.shell.animation.Interpolators; import com.android.wm.shell.common.TransactionPool; /** @@ -55,14 +37,8 @@ import com.android.wm.shell.common.TransactionPool; */ public class SplashScreenExitAnimation implements Animator.AnimatorListener { private static final boolean DEBUG_EXIT_ANIMATION = false; - private static final boolean DEBUG_EXIT_ANIMATION_BLEND = false; private static final String TAG = StartingWindowController.TAG; - private static final Interpolator ICON_INTERPOLATOR = new PathInterpolator(0.15f, 0f, 1f, 1f); - private static final Interpolator MASK_RADIUS_INTERPOLATOR = - new PathInterpolator(0f, 0f, 0.4f, 1f); - private static final Interpolator SHIFT_UP_INTERPOLATOR = new PathInterpolator(0f, 0f, 0f, 1f); - private final SurfaceControl mFirstWindowSurface; private final Rect mFirstWindowFrame = new Rect(); private final SplashScreenView mSplashScreenView; @@ -74,16 +50,17 @@ public class SplashScreenExitAnimation implements Animator.AnimatorListener { private final float mIconStartAlpha; private final float mBrandingStartAlpha; private final TransactionPool mTransactionPool; + // TODO(b/261167708): Clean enter animation code after moving Letterbox code to Shell + private final float mRoundedCornerRadius; - private ValueAnimator mMainAnimator; - private ShiftUpAnimation mShiftUpAnimation; - private RadialVanishAnimation mRadialVanishAnimation; private Runnable mFinishCallback; SplashScreenExitAnimation(Context context, SplashScreenView view, SurfaceControl leash, - Rect frame, int mainWindowShiftLength, TransactionPool pool, Runnable handleFinish) { + Rect frame, int mainWindowShiftLength, TransactionPool pool, Runnable handleFinish, + float roundedCornerRadius) { mSplashScreenView = view; mFirstWindowSurface = leash; + mRoundedCornerRadius = roundedCornerRadius; if (frame != null) { mFirstWindowFrame.set(frame); } @@ -121,187 +98,10 @@ public class SplashScreenExitAnimation implements Animator.AnimatorListener { } void startAnimations() { - mMainAnimator = createAnimator(); - mMainAnimator.start(); - } - - // fade out icon, reveal app, shift up main window - private ValueAnimator createAnimator() { - // reveal app - final float transparentRatio = 0.8f; - final int globalHeight = mSplashScreenView.getHeight(); - final int verticalCircleCenter = 0; - final int finalVerticalLength = globalHeight - verticalCircleCenter; - final int halfWidth = mSplashScreenView.getWidth() / 2; - final int endRadius = (int) (0.5 + (1f / transparentRatio * (int) - Math.sqrt(finalVerticalLength * finalVerticalLength + halfWidth * halfWidth))); - final int[] colors = {Color.WHITE, Color.WHITE, Color.TRANSPARENT}; - final float[] stops = {0f, transparentRatio, 1f}; - - mRadialVanishAnimation = new RadialVanishAnimation(mSplashScreenView); - mRadialVanishAnimation.setCircleCenter(halfWidth, verticalCircleCenter); - mRadialVanishAnimation.setRadius(0 /* initRadius */, endRadius); - mRadialVanishAnimation.setRadialPaintParam(colors, stops); - - if (mFirstWindowSurface != null && mFirstWindowSurface.isValid()) { - // shift up main window - View occludeHoleView = new View(mSplashScreenView.getContext()); - if (DEBUG_EXIT_ANIMATION_BLEND) { - occludeHoleView.setBackgroundColor(Color.BLUE); - } else { - occludeHoleView.setBackgroundColor(mSplashScreenView.getInitBackgroundColor()); - } - final ViewGroup.LayoutParams params = new ViewGroup.LayoutParams( - WindowManager.LayoutParams.MATCH_PARENT, mMainWindowShiftLength); - mSplashScreenView.addView(occludeHoleView, params); - - mShiftUpAnimation = new ShiftUpAnimation(0, -mMainWindowShiftLength, occludeHoleView); - } - - ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f); - animator.setDuration(mAnimationDuration); - animator.setInterpolator(Interpolators.LINEAR); - animator.addListener(this); - animator.addUpdateListener(a -> onAnimationProgress((float) a.getAnimatedValue())); - return animator; - } - - private static class RadialVanishAnimation extends View { - private final SplashScreenView mView; - private int mInitRadius; - private int mFinishRadius; - - private final Point mCircleCenter = new Point(); - private final Matrix mVanishMatrix = new Matrix(); - private final Paint mVanishPaint = new Paint(Paint.ANTI_ALIAS_FLAG); - - RadialVanishAnimation(SplashScreenView target) { - super(target.getContext()); - mView = target; - mView.addView(this); - mVanishPaint.setAlpha(0); - } - - void onAnimationProgress(float linearProgress) { - if (mVanishPaint.getShader() == null) { - return; - } - - final float radiusProgress = MASK_RADIUS_INTERPOLATOR.getInterpolation(linearProgress); - final float alphaProgress = Interpolators.ALPHA_OUT.getInterpolation(linearProgress); - final float scale = mInitRadius + (mFinishRadius - mInitRadius) * radiusProgress; - - mVanishMatrix.setScale(scale, scale); - mVanishMatrix.postTranslate(mCircleCenter.x, mCircleCenter.y); - mVanishPaint.getShader().setLocalMatrix(mVanishMatrix); - mVanishPaint.setAlpha(Math.round(0xFF * alphaProgress)); - - postInvalidate(); - } - - void setRadius(int initRadius, int finishRadius) { - if (DEBUG_EXIT_ANIMATION) { - Slog.v(TAG, "RadialVanishAnimation setRadius init: " + initRadius - + " final " + finishRadius); - } - mInitRadius = initRadius; - mFinishRadius = finishRadius; - } - - void setCircleCenter(int x, int y) { - if (DEBUG_EXIT_ANIMATION) { - Slog.v(TAG, "RadialVanishAnimation setCircleCenter x: " + x + " y " + y); - } - mCircleCenter.set(x, y); - } - - void setRadialPaintParam(int[] colors, float[] stops) { - // setup gradient shader - final RadialGradient rShader = - new RadialGradient(0, 0, 1, colors, stops, Shader.TileMode.CLAMP); - mVanishPaint.setShader(rShader); - if (!DEBUG_EXIT_ANIMATION_BLEND) { - // We blend the reveal gradient with the splash screen using DST_OUT so that the - // splash screen is fully visible when radius = 0 (or gradient opacity is 0) and - // fully invisible when radius = finishRadius AND gradient opacity is 1. - mVanishPaint.setBlendMode(BlendMode.DST_OUT); - } - } - - @Override - protected void onDraw(Canvas canvas) { - super.onDraw(canvas); - canvas.drawRect(0, 0, mView.getWidth(), mView.getHeight(), mVanishPaint); - } - } - - private final class ShiftUpAnimation { - private final float mFromYDelta; - private final float mToYDelta; - private final View mOccludeHoleView; - private final SyncRtSurfaceTransactionApplier mApplier; - private final Matrix mTmpTransform = new Matrix(); - - ShiftUpAnimation(float fromYDelta, float toYDelta, View occludeHoleView) { - mFromYDelta = fromYDelta; - mToYDelta = toYDelta; - mOccludeHoleView = occludeHoleView; - mApplier = new SyncRtSurfaceTransactionApplier(occludeHoleView); - } - - void onAnimationProgress(float linearProgress) { - if (mFirstWindowSurface == null || !mFirstWindowSurface.isValid() - || !mSplashScreenView.isAttachedToWindow()) { - return; - } - - final float progress = SHIFT_UP_INTERPOLATOR.getInterpolation(linearProgress); - final float dy = mFromYDelta + (mToYDelta - mFromYDelta) * progress; - - mOccludeHoleView.setTranslationY(dy); - mTmpTransform.setTranslate(0 /* dx */, dy); - - // set the vsyncId to ensure the transaction doesn't get applied too early. - final SurfaceControl.Transaction tx = mTransactionPool.acquire(); - tx.setFrameTimelineVsync(Choreographer.getSfInstance().getVsyncId()); - mTmpTransform.postTranslate(mFirstWindowFrame.left, - mFirstWindowFrame.top + mMainWindowShiftLength); - - SyncRtSurfaceTransactionApplier.SurfaceParams - params = new SyncRtSurfaceTransactionApplier.SurfaceParams - .Builder(mFirstWindowSurface) - .withMatrix(mTmpTransform) - .withMergeTransaction(tx) - .build(); - mApplier.scheduleApply(params); - - mTransactionPool.release(tx); - } - - void finish() { - if (mFirstWindowSurface == null || !mFirstWindowSurface.isValid()) { - return; - } - final SurfaceControl.Transaction tx = mTransactionPool.acquire(); - if (mSplashScreenView.isAttachedToWindow()) { - tx.setFrameTimelineVsync(Choreographer.getSfInstance().getVsyncId()); - - SyncRtSurfaceTransactionApplier.SurfaceParams - params = new SyncRtSurfaceTransactionApplier.SurfaceParams - .Builder(mFirstWindowSurface) - .withWindowCrop(null) - .withMergeTransaction(tx) - .build(); - mApplier.scheduleApply(params); - } else { - tx.setWindowCrop(mFirstWindowSurface, null); - tx.apply(); - } - mTransactionPool.release(tx); - - Choreographer.getSfInstance().postCallback(CALLBACK_COMMIT, - mFirstWindowSurface::release, null); - } + SplashScreenExitAnimationUtils.startAnimations(mSplashScreenView, mFirstWindowSurface, + mMainWindowShiftLength, mTransactionPool, mFirstWindowFrame, mAnimationDuration, + mIconFadeOutDuration, mIconStartAlpha, mBrandingStartAlpha, mAppRevealDelay, + mAppRevealDuration, this, mRoundedCornerRadius); } private void reset() { @@ -316,9 +116,6 @@ public class SplashScreenExitAnimation implements Animator.AnimatorListener { mFinishCallback = null; } } - if (mShiftUpAnimation != null) { - mShiftUpAnimation.finish(); - } } @Override @@ -342,40 +139,4 @@ public class SplashScreenExitAnimation implements Animator.AnimatorListener { public void onAnimationRepeat(Animator animation) { // ignore } - - private void onFadeOutProgress(float linearProgress) { - final float iconProgress = ICON_INTERPOLATOR.getInterpolation( - getProgress(linearProgress, 0 /* delay */, mIconFadeOutDuration)); - final View iconView = mSplashScreenView.getIconView(); - final View brandingView = mSplashScreenView.getBrandingView(); - if (iconView != null) { - iconView.setAlpha(mIconStartAlpha * (1 - iconProgress)); - } - if (brandingView != null) { - brandingView.setAlpha(mBrandingStartAlpha * (1 - iconProgress)); - } - } - - private void onAnimationProgress(float linearProgress) { - onFadeOutProgress(linearProgress); - - final float revealLinearProgress = getProgress(linearProgress, mAppRevealDelay, - mAppRevealDuration); - - if (mRadialVanishAnimation != null) { - mRadialVanishAnimation.onAnimationProgress(revealLinearProgress); - } - - if (mShiftUpAnimation != null) { - mShiftUpAnimation.onAnimationProgress(revealLinearProgress); - } - } - - private float getProgress(float linearProgress, long delay, long duration) { - return MathUtils.constrain( - (linearProgress * (mAnimationDuration) - delay) / duration, - 0.0f, - 1.0f - ); - } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashScreenExitAnimationUtils.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashScreenExitAnimationUtils.java new file mode 100644 index 000000000000..a7e4385b60c8 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashScreenExitAnimationUtils.java @@ -0,0 +1,375 @@ +/* + * 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 android.view.Choreographer.CALLBACK_COMMIT; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.res.Configuration; +import android.graphics.BlendMode; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.Point; +import android.graphics.RadialGradient; +import android.graphics.Rect; +import android.graphics.Shader; +import android.util.MathUtils; +import android.util.Slog; +import android.view.Choreographer; +import android.view.SurfaceControl; +import android.view.SyncRtSurfaceTransactionApplier; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.view.animation.Interpolator; +import android.view.animation.PathInterpolator; +import android.window.SplashScreenView; + +import com.android.wm.shell.animation.Interpolators; +import com.android.wm.shell.common.TransactionPool; + +/** + * Utilities for creating the splash screen window animations. + * @hide + */ +public class SplashScreenExitAnimationUtils { + private static final boolean DEBUG_EXIT_ANIMATION = false; + private static final boolean DEBUG_EXIT_ANIMATION_BLEND = false; + private static final String TAG = "SplashScreenExitAnimationUtils"; + + private static final Interpolator ICON_INTERPOLATOR = new PathInterpolator(0.15f, 0f, 1f, 1f); + private static final Interpolator MASK_RADIUS_INTERPOLATOR = + new PathInterpolator(0f, 0f, 0.4f, 1f); + private static final Interpolator SHIFT_UP_INTERPOLATOR = new PathInterpolator(0f, 0f, 0f, 1f); + + /** + * Creates and starts the animator to fade out the icon, reveal the app, and shift up main + * window with rounded corner radius. + */ + static void startAnimations(ViewGroup splashScreenView, + SurfaceControl firstWindowSurface, int mainWindowShiftLength, + TransactionPool transactionPool, Rect firstWindowFrame, int animationDuration, + int iconFadeOutDuration, float iconStartAlpha, float brandingStartAlpha, + int appRevealDelay, int appRevealDuration, Animator.AnimatorListener animatorListener, + float roundedCornerRadius) { + ValueAnimator animator = + createAnimator(splashScreenView, firstWindowSurface, mainWindowShiftLength, + transactionPool, firstWindowFrame, animationDuration, iconFadeOutDuration, + iconStartAlpha, brandingStartAlpha, appRevealDelay, appRevealDuration, + animatorListener, roundedCornerRadius); + animator.start(); + } + + /** + * Creates and starts the animator to fade out the icon, reveal the app, and shift up main + * window. + * @hide + */ + public static void startAnimations(ViewGroup splashScreenView, + SurfaceControl firstWindowSurface, int mainWindowShiftLength, + TransactionPool transactionPool, Rect firstWindowFrame, int animationDuration, + int iconFadeOutDuration, float iconStartAlpha, float brandingStartAlpha, + int appRevealDelay, int appRevealDuration, Animator.AnimatorListener animatorListener) { + startAnimations(splashScreenView, firstWindowSurface, mainWindowShiftLength, + transactionPool, firstWindowFrame, animationDuration, iconFadeOutDuration, + iconStartAlpha, brandingStartAlpha, appRevealDelay, appRevealDuration, + animatorListener, 0f /* roundedCornerRadius */); + } + + /** + * Creates the animator to fade out the icon, reveal the app, and shift up main window. + * @hide + */ + private static ValueAnimator createAnimator(ViewGroup splashScreenView, + SurfaceControl firstWindowSurface, int mMainWindowShiftLength, + TransactionPool transactionPool, Rect firstWindowFrame, int animationDuration, + int iconFadeOutDuration, float iconStartAlpha, float brandingStartAlpha, + int appRevealDelay, int appRevealDuration, Animator.AnimatorListener animatorListener, + float roundedCornerRadius) { + // reveal app + final float transparentRatio = 0.8f; + final int globalHeight = splashScreenView.getHeight(); + final int verticalCircleCenter = 0; + final int finalVerticalLength = globalHeight - verticalCircleCenter; + final int halfWidth = splashScreenView.getWidth() / 2; + final int endRadius = (int) (0.5 + (1f / transparentRatio * (int) + Math.sqrt(finalVerticalLength * finalVerticalLength + halfWidth * halfWidth))); + final int[] colors = {Color.WHITE, Color.WHITE, Color.TRANSPARENT}; + final float[] stops = {0f, transparentRatio, 1f}; + + RadialVanishAnimation radialVanishAnimation = new RadialVanishAnimation(splashScreenView); + radialVanishAnimation.setCircleCenter(halfWidth, verticalCircleCenter); + radialVanishAnimation.setRadius(0 /* initRadius */, endRadius); + radialVanishAnimation.setRadialPaintParam(colors, stops); + + View occludeHoleView = null; + ShiftUpAnimation shiftUpAnimation = null; + if (firstWindowSurface != null && firstWindowSurface.isValid()) { + // shift up main window + occludeHoleView = new View(splashScreenView.getContext()); + if (DEBUG_EXIT_ANIMATION_BLEND) { + occludeHoleView.setBackgroundColor(Color.BLUE); + } else if (splashScreenView instanceof SplashScreenView) { + occludeHoleView.setBackgroundColor( + ((SplashScreenView) splashScreenView).getInitBackgroundColor()); + } else { + occludeHoleView.setBackgroundColor( + isDarkTheme(splashScreenView.getContext()) ? Color.BLACK : Color.WHITE); + } + final ViewGroup.LayoutParams params = new ViewGroup.LayoutParams( + WindowManager.LayoutParams.MATCH_PARENT, mMainWindowShiftLength); + splashScreenView.addView(occludeHoleView, params); + + shiftUpAnimation = new ShiftUpAnimation(0, -mMainWindowShiftLength, occludeHoleView, + firstWindowSurface, splashScreenView, transactionPool, firstWindowFrame, + mMainWindowShiftLength, roundedCornerRadius); + } + + ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f); + animator.setDuration(animationDuration); + animator.setInterpolator(Interpolators.LINEAR); + if (animatorListener != null) { + animator.addListener(animatorListener); + } + View finalOccludeHoleView = occludeHoleView; + ShiftUpAnimation finalShiftUpAnimation = shiftUpAnimation; + animator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + if (finalShiftUpAnimation != null) { + finalShiftUpAnimation.finish(); + } + splashScreenView.removeView(radialVanishAnimation); + splashScreenView.removeView(finalOccludeHoleView); + } + }); + animator.addUpdateListener(animation -> { + float linearProgress = (float) animation.getAnimatedValue(); + + // Fade out progress + final float iconProgress = + ICON_INTERPOLATOR.getInterpolation(getProgress( + linearProgress, 0 /* delay */, iconFadeOutDuration, animationDuration)); + View iconView = null; + View brandingView = null; + if (splashScreenView instanceof SplashScreenView) { + iconView = ((SplashScreenView) splashScreenView).getIconView(); + brandingView = ((SplashScreenView) splashScreenView).getBrandingView(); + } + if (iconView != null) { + iconView.setAlpha(iconStartAlpha * (1 - iconProgress)); + } + if (brandingView != null) { + brandingView.setAlpha(brandingStartAlpha * (1 - iconProgress)); + } + + final float revealLinearProgress = getProgress(linearProgress, appRevealDelay, + appRevealDuration, animationDuration); + + radialVanishAnimation.onAnimationProgress(revealLinearProgress); + + if (finalShiftUpAnimation != null) { + finalShiftUpAnimation.onAnimationProgress(revealLinearProgress); + } + }); + return animator; + } + + private static float getProgress(float linearProgress, long delay, long duration, + int animationDuration) { + return MathUtils.constrain( + (linearProgress * (animationDuration) - delay) / duration, + 0.0f, + 1.0f + ); + } + + private static boolean isDarkTheme(Context context) { + Configuration configuration = context.getResources().getConfiguration(); + int nightMode = configuration.uiMode & Configuration.UI_MODE_NIGHT_MASK; + return nightMode == Configuration.UI_MODE_NIGHT_YES; + } + + /** + * View which creates a circular reveal of the underlying view. + * @hide + */ + @SuppressLint("ViewConstructor") + public static class RadialVanishAnimation extends View { + private final ViewGroup mView; + private int mInitRadius; + private int mFinishRadius; + + private final Point mCircleCenter = new Point(); + private final Matrix mVanishMatrix = new Matrix(); + private final Paint mVanishPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + + public RadialVanishAnimation(ViewGroup target) { + super(target.getContext()); + mView = target; + mView.addView(this); + if (getLayoutParams() instanceof ViewGroup.MarginLayoutParams) { + ((ViewGroup.MarginLayoutParams) getLayoutParams()).setMargins(0, 0, 0, 0); + } + mVanishPaint.setAlpha(0); + } + + void onAnimationProgress(float linearProgress) { + if (mVanishPaint.getShader() == null) { + return; + } + + final float radiusProgress = MASK_RADIUS_INTERPOLATOR.getInterpolation(linearProgress); + final float alphaProgress = Interpolators.ALPHA_OUT.getInterpolation(linearProgress); + final float scale = mInitRadius + (mFinishRadius - mInitRadius) * radiusProgress; + + mVanishMatrix.setScale(scale, scale); + mVanishMatrix.postTranslate(mCircleCenter.x, mCircleCenter.y); + mVanishPaint.getShader().setLocalMatrix(mVanishMatrix); + mVanishPaint.setAlpha(Math.round(0xFF * alphaProgress)); + + postInvalidate(); + } + + void setRadius(int initRadius, int finishRadius) { + if (DEBUG_EXIT_ANIMATION) { + Slog.v(TAG, "RadialVanishAnimation setRadius init: " + initRadius + + " final " + finishRadius); + } + mInitRadius = initRadius; + mFinishRadius = finishRadius; + } + + void setCircleCenter(int x, int y) { + if (DEBUG_EXIT_ANIMATION) { + Slog.v(TAG, "RadialVanishAnimation setCircleCenter x: " + x + " y " + y); + } + mCircleCenter.set(x, y); + } + + void setRadialPaintParam(int[] colors, float[] stops) { + // setup gradient shader + final RadialGradient rShader = + new RadialGradient(0, 0, 1, colors, stops, Shader.TileMode.CLAMP); + mVanishPaint.setShader(rShader); + if (!DEBUG_EXIT_ANIMATION_BLEND) { + // We blend the reveal gradient with the splash screen using DST_OUT so that the + // splash screen is fully visible when radius = 0 (or gradient opacity is 0) and + // fully invisible when radius = finishRadius AND gradient opacity is 1. + mVanishPaint.setBlendMode(BlendMode.DST_OUT); + } + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + canvas.drawRect(0, 0, mView.getWidth(), mView.getHeight(), mVanishPaint); + } + } + + /** + * Shifts up the main window. + * @hide + */ + public static final class ShiftUpAnimation { + private final float mFromYDelta; + private final float mToYDelta; + private final View mOccludeHoleView; + private final SyncRtSurfaceTransactionApplier mApplier; + private final Matrix mTmpTransform = new Matrix(); + private final SurfaceControl mFirstWindowSurface; + private final ViewGroup mSplashScreenView; + private final TransactionPool mTransactionPool; + private final Rect mFirstWindowFrame; + private final int mMainWindowShiftLength; + + public ShiftUpAnimation(float fromYDelta, float toYDelta, View occludeHoleView, + SurfaceControl firstWindowSurface, ViewGroup splashScreenView, + TransactionPool transactionPool, Rect firstWindowFrame, + int mainWindowShiftLength, float roundedCornerRadius) { + mFromYDelta = fromYDelta - roundedCornerRadius; + mToYDelta = toYDelta; + mOccludeHoleView = occludeHoleView; + mApplier = new SyncRtSurfaceTransactionApplier(occludeHoleView); + mFirstWindowSurface = firstWindowSurface; + mSplashScreenView = splashScreenView; + mTransactionPool = transactionPool; + mFirstWindowFrame = firstWindowFrame; + mMainWindowShiftLength = mainWindowShiftLength; + } + + void onAnimationProgress(float linearProgress) { + if (mFirstWindowSurface == null || !mFirstWindowSurface.isValid() + || !mSplashScreenView.isAttachedToWindow()) { + return; + } + + final float progress = SHIFT_UP_INTERPOLATOR.getInterpolation(linearProgress); + final float dy = mFromYDelta + (mToYDelta - mFromYDelta) * progress; + + mOccludeHoleView.setTranslationY(dy); + mTmpTransform.setTranslate(0 /* dx */, dy); + + // set the vsyncId to ensure the transaction doesn't get applied too early. + final SurfaceControl.Transaction tx = mTransactionPool.acquire(); + tx.setFrameTimelineVsync(Choreographer.getSfInstance().getVsyncId()); + mTmpTransform.postTranslate(mFirstWindowFrame.left, + mFirstWindowFrame.top + mMainWindowShiftLength); + + SyncRtSurfaceTransactionApplier.SurfaceParams + params = new SyncRtSurfaceTransactionApplier.SurfaceParams + .Builder(mFirstWindowSurface) + .withMatrix(mTmpTransform) + .withMergeTransaction(tx) + .build(); + mApplier.scheduleApply(params); + + mTransactionPool.release(tx); + } + + void finish() { + if (mFirstWindowSurface == null || !mFirstWindowSurface.isValid()) { + return; + } + final SurfaceControl.Transaction tx = mTransactionPool.acquire(); + if (mSplashScreenView.isAttachedToWindow()) { + tx.setFrameTimelineVsync(Choreographer.getSfInstance().getVsyncId()); + + SyncRtSurfaceTransactionApplier.SurfaceParams + params = new SyncRtSurfaceTransactionApplier.SurfaceParams + .Builder(mFirstWindowSurface) + .withWindowCrop(null) + .withMergeTransaction(tx) + .build(); + mApplier.scheduleApply(params); + } else { + tx.setWindowCrop(mFirstWindowSurface, null); + tx.apply(); + } + mTransactionPool.release(tx); + + Choreographer.getSfInstance().postCallback(CALLBACK_COMMIT, + mFirstWindowSurface::release, null); + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java index 8cee4f1dc8fb..dc91a11dc64f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java @@ -16,19 +16,19 @@ package com.android.wm.shell.startingsurface; +import static android.content.Context.CONTEXT_RESTRICTED; import static android.os.Process.THREAD_PRIORITY_TOP_APP_BOOST; import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER; +import static android.view.Display.DEFAULT_DISPLAY; import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_LEGACY_SPLASH_SCREEN; import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_SOLID_COLOR_SPLASH_SCREEN; import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_SPLASH_SCREEN; -import static com.android.wm.shell.startingsurface.StartingSurfaceDrawer.MAX_ANIMATION_DURATION; -import static com.android.wm.shell.startingsurface.StartingSurfaceDrawer.MINIMAL_ANIMATION_DURATION; - import android.annotation.ColorInt; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; +import android.app.ActivityManager; import android.app.ActivityThread; import android.content.BroadcastReceiver; import android.content.Context; @@ -48,9 +48,11 @@ import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.LayerDrawable; +import android.hardware.display.DisplayManager; import android.net.Uri; import android.os.Handler; import android.os.HandlerThread; +import android.os.IBinder; import android.os.SystemClock; import android.os.Trace; import android.os.UserHandle; @@ -58,7 +60,9 @@ import android.util.ArrayMap; import android.util.DisplayMetrics; import android.util.Slog; import android.view.ContextThemeWrapper; +import android.view.Display; import android.view.SurfaceControl; +import android.view.WindowManager; import android.window.SplashScreenView; import android.window.StartingWindowInfo; import android.window.StartingWindowInfo.StartingWindowType; @@ -89,6 +93,25 @@ import java.util.function.UnaryOperator; public class SplashscreenContentDrawer { private static final String TAG = StartingWindowController.TAG; + /** + * The minimum duration during which the splash screen is shown when the splash screen icon is + * animated. + */ + static final long MINIMAL_ANIMATION_DURATION = 400L; + + /** + * Allow the icon style splash screen to be displayed for longer to give time for the animation + * to finish, i.e. the extra buffer time to keep the splash screen if the animation is slightly + * longer than the {@link #MINIMAL_ANIMATION_DURATION} duration. + */ + static final long TIME_WINDOW_DURATION = 100L; + + /** + * The maximum duration during which the splash screen will be shown if the application is ready + * to show before the icon animation finishes. + */ + static final long MAX_ANIMATION_DURATION = MINIMAL_ANIMATION_DURATION + TIME_WINDOW_DURATION; + // The acceptable area ratio of foreground_icon_area/background_icon_area, if there is an // icon which it's non-transparent foreground area is similar to it's background area, then // do not enlarge the foreground drawable. @@ -134,6 +157,144 @@ public class SplashscreenContentDrawer { } /** + * Help method to create a layout parameters for a window. + */ + static Context createContext(Context initContext, StartingWindowInfo windowInfo, + int theme, @StartingWindowInfo.StartingWindowType int suggestType, + DisplayManager displayManager) { + final ActivityManager.RunningTaskInfo taskInfo = windowInfo.taskInfo; + final ActivityInfo activityInfo = windowInfo.targetActivityInfo != null + ? windowInfo.targetActivityInfo + : taskInfo.topActivityInfo; + if (activityInfo == null || activityInfo.packageName == null) { + return null; + } + + final int displayId = taskInfo.displayId; + final int taskId = taskInfo.taskId; + + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW, + "addSplashScreen for package: %s with theme: %s for task: %d, suggestType: %d", + activityInfo.packageName, Integer.toHexString(theme), taskId, suggestType); + final Display display = displayManager.getDisplay(displayId); + if (display == null) { + // Can't show splash screen on requested display, so skip showing at all. + return null; + } + Context context = displayId == DEFAULT_DISPLAY + ? initContext : initContext.createDisplayContext(display); + if (context == null) { + return null; + } + if (theme != context.getThemeResId()) { + try { + context = context.createPackageContextAsUser(activityInfo.packageName, + CONTEXT_RESTRICTED, UserHandle.of(taskInfo.userId)); + context.setTheme(theme); + } catch (PackageManager.NameNotFoundException e) { + Slog.w(TAG, "Failed creating package context with package name " + + activityInfo.packageName + " for user " + taskInfo.userId, e); + return null; + } + } + + final Configuration taskConfig = taskInfo.getConfiguration(); + if (taskConfig.diffPublicOnly(context.getResources().getConfiguration()) != 0) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW, + "addSplashScreen: creating context based on task Configuration %s", + taskConfig); + final Context overrideContext = context.createConfigurationContext(taskConfig); + overrideContext.setTheme(theme); + final TypedArray typedArray = overrideContext.obtainStyledAttributes( + com.android.internal.R.styleable.Window); + final int resId = typedArray.getResourceId(R.styleable.Window_windowBackground, 0); + try { + if (resId != 0 && overrideContext.getDrawable(resId) != null) { + // We want to use the windowBackground for the override context if it is + // available, otherwise we use the default one to make sure a themed starting + // window is displayed for the app. + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW, + "addSplashScreen: apply overrideConfig %s", + taskConfig); + context = overrideContext; + } + } catch (Resources.NotFoundException e) { + Slog.w(TAG, "failed creating starting window for overrideConfig at taskId: " + + taskId, e); + return null; + } + typedArray.recycle(); + } + return context; + } + + /** + * Creates the window layout parameters for splashscreen window. + */ + static WindowManager.LayoutParams createLayoutParameters(Context context, + StartingWindowInfo windowInfo, + @StartingWindowInfo.StartingWindowType int suggestType, + CharSequence title, int pixelFormat, IBinder appToken) { + final WindowManager.LayoutParams params = new WindowManager.LayoutParams( + WindowManager.LayoutParams.TYPE_APPLICATION_STARTING); + params.setFitInsetsSides(0); + params.setFitInsetsTypes(0); + params.format = pixelFormat; + int windowFlags = WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED + | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN + | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR; + final TypedArray a = context.obtainStyledAttributes(R.styleable.Window); + if (a.getBoolean(R.styleable.Window_windowShowWallpaper, false)) { + windowFlags |= WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER; + } + if (suggestType == STARTING_WINDOW_TYPE_LEGACY_SPLASH_SCREEN) { + if (a.getBoolean(R.styleable.Window_windowDrawsSystemBarBackgrounds, false)) { + windowFlags |= WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS; + } + } else { + windowFlags |= WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS; + } + params.layoutInDisplayCutoutMode = a.getInt( + R.styleable.Window_windowLayoutInDisplayCutoutMode, + params.layoutInDisplayCutoutMode); + params.windowAnimations = a.getResourceId(R.styleable.Window_windowAnimationStyle, 0); + a.recycle(); + + final ActivityManager.RunningTaskInfo taskInfo = windowInfo.taskInfo; + final ActivityInfo activityInfo = windowInfo.targetActivityInfo != null + ? windowInfo.targetActivityInfo + : taskInfo.topActivityInfo; + final int displayId = taskInfo.displayId; + // Assumes it's safe to show starting windows of launched apps while + // the keyguard is being hidden. This is okay because starting windows never show + // secret information. + // TODO(b/113840485): Occluded may not only happen on default display + if (displayId == DEFAULT_DISPLAY && windowInfo.isKeyguardOccluded) { + windowFlags |= WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED; + } + + // Force the window flags: this is a fake window, so it is not really + // touchable or focusable by the user. We also add in the ALT_FOCUSABLE_IM + // flag because we do know that the next window will take input + // focus, so we want to get the IME window up on top of us right away. + // Touches will only pass through to the host activity window and will be blocked from + // passing to any other windows. + windowFlags |= WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE + | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE + | WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; + params.flags = windowFlags; + params.token = appToken; + params.packageName = activityInfo.packageName; + params.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS; + + if (!context.getResources().getCompatibilityInfo().supportsScreen()) { + params.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_COMPATIBLE_WINDOW; + } + + params.setTitle("Splash Screen " + title); + return params; + } + /** * Create a SplashScreenView object. * * In order to speed up the splash screen view to show on first frame, preparing the @@ -223,10 +384,10 @@ public class SplashscreenContentDrawer { private static int estimateWindowBGColor(Drawable themeBGDrawable) { final DrawableColorTester themeBGTester = new DrawableColorTester( - themeBGDrawable, DrawableColorTester.TRANSPARENT_FILTER /* filterType */); - if (themeBGTester.passFilterRatio() == 0) { - // the window background is transparent, unable to draw - Slog.w(TAG, "Window background is transparent, fill background with black color"); + themeBGDrawable, DrawableColorTester.TRANSLUCENT_FILTER /* filterType */); + if (themeBGTester.passFilterRatio() != 1) { + // the window background is translucent, unable to draw + Slog.w(TAG, "Window background is translucent, fill background with black color"); return getSystemBGColor(); } else { return themeBGTester.getDominateColor(); @@ -248,6 +409,26 @@ public class SplashscreenContentDrawer { return null; } + /** + * Creates a SplashScreenView without read animatable icon and branding image. + */ + SplashScreenView makeSimpleSplashScreenContentView(Context context, + StartingWindowInfo info, int themeBGColor) { + updateDensity(); + mTmpAttrs.reset(); + final ActivityInfo ai = info.targetActivityInfo != null + ? info.targetActivityInfo + : info.taskInfo.topActivityInfo; + + final SplashViewBuilder builder = new SplashViewBuilder(context, ai); + final SplashScreenView view = builder + .setWindowBGColor(themeBGColor) + .chooseStyle(STARTING_WINDOW_TYPE_SPLASH_SCREEN) + .build(); + view.setNotCopyable(); + return view; + } + private SplashScreenView makeSplashScreenContentView(Context context, StartingWindowInfo info, @StartingWindowType int suggestType, Consumer<Runnable> uiThreadInitConsumer) { updateDensity(); @@ -263,7 +444,8 @@ public class SplashscreenContentDrawer { final int themeBGColor = legacyDrawable != null ? getBGColorFromCache(ai, () -> estimateWindowBGColor(legacyDrawable)) : getBGColorFromCache(ai, () -> peekWindowBGColor(context, mTmpAttrs)); - return new StartingWindowViewBuilder(context, ai) + + return new SplashViewBuilder(context, ai) .setWindowBGColor(themeBGColor) .overlayDrawable(legacyDrawable) .chooseStyle(suggestType) @@ -322,6 +504,14 @@ public class SplashscreenContentDrawer { private Drawable mSplashScreenIcon = null; private Drawable mBrandingImage = null; private int mIconBgColor = Color.TRANSPARENT; + + void reset() { + mWindowBgResId = 0; + mWindowBgColor = Color.TRANSPARENT; + mSplashScreenIcon = null; + mBrandingImage = null; + mIconBgColor = Color.TRANSPARENT; + } } /** @@ -351,7 +541,7 @@ public class SplashscreenContentDrawer { return appReadyDuration; } - private class StartingWindowViewBuilder { + private class SplashViewBuilder { private final Context mContext; private final ActivityInfo mActivityInfo; @@ -364,27 +554,28 @@ public class SplashscreenContentDrawer { /** @see #setAllowHandleSolidColor(boolean) **/ private boolean mAllowHandleSolidColor; - StartingWindowViewBuilder(@NonNull Context context, @NonNull ActivityInfo aInfo) { + SplashViewBuilder(@NonNull Context context, @NonNull ActivityInfo aInfo) { mContext = context; mActivityInfo = aInfo; } - StartingWindowViewBuilder setWindowBGColor(@ColorInt int background) { + SplashViewBuilder setWindowBGColor(@ColorInt int background) { mThemeColor = background; return this; } - StartingWindowViewBuilder overlayDrawable(Drawable overlay) { + SplashViewBuilder overlayDrawable(Drawable overlay) { mOverlayDrawable = overlay; return this; } - StartingWindowViewBuilder chooseStyle(int suggestType) { + SplashViewBuilder chooseStyle(int suggestType) { mSuggestType = suggestType; return this; } - StartingWindowViewBuilder setUiThreadInitConsumer(Consumer<Runnable> uiThreadInitTask) { + // Set up the UI thread for the View. + SplashViewBuilder setUiThreadInitConsumer(Consumer<Runnable> uiThreadInitTask) { mUiThreadInitTask = uiThreadInitTask; return this; } @@ -395,7 +586,7 @@ public class SplashscreenContentDrawer { * android.window.SplashScreen.OnExitAnimationListener#onSplashScreenExit(SplashScreenView)} * callback, effectively copying the {@link SplashScreenView} into the client process. */ - StartingWindowViewBuilder setAllowHandleSolidColor(boolean allowHandleSolidColor) { + SplashViewBuilder setAllowHandleSolidColor(boolean allowHandleSolidColor) { mAllowHandleSolidColor = allowHandleSolidColor; return this; } @@ -432,7 +623,8 @@ public class SplashscreenContentDrawer { final ShapeIconFactory factory = new ShapeIconFactory( SplashscreenContentDrawer.this.mContext, scaledIconDpi, mFinalIconSize); - final Bitmap bitmap = factory.createScaledBitmapWithoutShadow(iconDrawable); + final Bitmap bitmap = factory.createScaledBitmap(iconDrawable, + BaseIconFactory.MODE_DEFAULT); Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); createIconDrawable(new BitmapDrawable(bitmap), true, mHighResIconProvider.mLoadInDetail); @@ -678,7 +870,7 @@ public class SplashscreenContentDrawer { @Override public float passFilterRatio() { final int alpha = mColorDrawable.getAlpha(); - return (float) (alpha / 255); + return alpha / 255.0f; } @Override @@ -992,10 +1184,11 @@ public class SplashscreenContentDrawer { * Create and play the default exit animation for splash screen view. */ void applyExitAnimation(SplashScreenView view, SurfaceControl leash, - Rect frame, Runnable finishCallback, long createTime) { + Rect frame, Runnable finishCallback, long createTime, float roundedCornerRadius) { final Runnable playAnimation = () -> { final SplashScreenExitAnimation animation = new SplashScreenExitAnimation(mContext, - view, leash, frame, mMainWindowShiftLength, mTransactionPool, finishCallback); + view, leash, frame, mMainWindowShiftLength, mTransactionPool, finishCallback, + roundedCornerRadius); animation.startAnimations(); }; if (view.getIconView() == null) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenIconDrawableFactory.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenIconDrawableFactory.java index 7f6bfd23f72b..e419462012e3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenIconDrawableFactory.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenIconDrawableFactory.java @@ -62,7 +62,7 @@ public class SplashscreenIconDrawableFactory { */ static Drawable[] makeIconDrawable(@ColorInt int backgroundColor, @ColorInt int themeColor, @NonNull Drawable foregroundDrawable, int srcIconSize, int iconSize, - boolean loadInDetail, Handler splashscreenWorkerHandler) { + boolean loadInDetail, Handler preDrawHandler) { Drawable foreground; Drawable background = null; boolean drawBackground = @@ -74,13 +74,13 @@ public class SplashscreenIconDrawableFactory { // If the icon is Adaptive, we already use the icon background. drawBackground = false; foreground = new ImmobileIconDrawable(foregroundDrawable, - srcIconSize, iconSize, loadInDetail, splashscreenWorkerHandler); + srcIconSize, iconSize, loadInDetail, preDrawHandler); } else { // Adaptive icon don't handle transparency so we draw the background of the adaptive // icon with the same color as the window background color instead of using two layers foreground = new ImmobileIconDrawable( new AdaptiveForegroundDrawable(foregroundDrawable), - srcIconSize, iconSize, loadInDetail, splashscreenWorkerHandler); + srcIconSize, iconSize, loadInDetail, preDrawHandler); } if (drawBackground) { @@ -91,9 +91,9 @@ public class SplashscreenIconDrawableFactory { } static Drawable[] makeLegacyIconDrawable(@NonNull Drawable iconDrawable, int srcIconSize, - int iconSize, boolean loadInDetail, Handler splashscreenWorkerHandler) { + int iconSize, boolean loadInDetail, Handler preDrawHandler) { return new Drawable[]{new ImmobileIconDrawable(iconDrawable, srcIconSize, iconSize, - loadInDetail, splashscreenWorkerHandler)}; + loadInDetail, preDrawHandler)}; } /** @@ -107,14 +107,14 @@ public class SplashscreenIconDrawableFactory { private Bitmap mIconBitmap; ImmobileIconDrawable(Drawable drawable, int srcIconSize, int iconSize, boolean loadInDetail, - Handler splashscreenWorkerHandler) { + Handler preDrawHandler) { // This icon has lower density, don't scale it. if (loadInDetail) { - splashscreenWorkerHandler.post(() -> preDrawIcon(drawable, iconSize)); + preDrawHandler.post(() -> preDrawIcon(drawable, iconSize)); } else { final float scale = (float) iconSize / srcIconSize; mMatrix.setScale(scale, scale); - splashscreenWorkerHandler.post(() -> preDrawIcon(drawable, srcIconSize)); + preDrawHandler.post(() -> preDrawIcon(drawable, srcIconSize)); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenWindowCreator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenWindowCreator.java new file mode 100644 index 000000000000..8a4d4c21194a --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenWindowCreator.java @@ -0,0 +1,508 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.startingsurface; + +import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER; +import static android.view.Choreographer.CALLBACK_INSETS_ANIMATION; +import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_LEGACY_SPLASH_SCREEN; + +import android.annotation.Nullable; +import android.app.ActivityManager; +import android.app.ActivityTaskManager; +import android.app.ActivityThread; +import android.app.TaskInfo; +import android.content.Context; +import android.content.pm.ActivityInfo; +import android.content.pm.IPackageManager; +import android.content.pm.PackageManager; +import android.content.res.TypedArray; +import android.graphics.Color; +import android.graphics.PixelFormat; +import android.hardware.display.DisplayManager; +import android.os.IBinder; +import android.os.RemoteCallback; +import android.os.RemoteException; +import android.os.SystemClock; +import android.os.Trace; +import android.os.UserHandle; +import android.util.Slog; +import android.util.SparseArray; +import android.view.Choreographer; +import android.view.Display; +import android.view.SurfaceControlViewHost; +import android.view.View; +import android.view.WindowInsetsController; +import android.view.WindowManager; +import android.view.WindowManagerGlobal; +import android.widget.FrameLayout; +import android.window.SplashScreenView; +import android.window.StartingWindowInfo; +import android.window.StartingWindowRemovalInfo; + +import com.android.internal.R; +import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.util.ContrastColorUtil; +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.protolog.ShellProtoLogGroup; + +import java.util.function.Supplier; + +/** + * A class which able to draw splash screen as the starting window for a task. + * + * In order to speed up, there will use two threads to creating a splash screen in parallel. + * Right now we are still using PhoneWindow to create splash screen window, so the view is added to + * the ViewRootImpl, and those view won't be draw immediately because the ViewRootImpl will call + * scheduleTraversal to register a callback from Choreographer, so the drawing result of the view + * can synchronize on each frame. + * + * The bad thing is that we cannot decide when would Choreographer#doFrame happen, and drawing + * the AdaptiveIconDrawable object can be time consuming, so we use the splash-screen background + * thread to draw the AdaptiveIconDrawable object to a Bitmap and cache it to a BitmapShader after + * the SplashScreenView just created, once we get the BitmapShader then the #draw call can be very + * quickly. + * + * So basically we are using the spare time to prepare the SplashScreenView while splash screen + * thread is waiting for + * 1. WindowManager#addView(binder call to WM), + * 2. Choreographer#doFrame happen(uncertain time for next frame, depends on device), + * 3. Session#relayout(another binder call to WM which under Choreographer#doFrame, but will + * always happen before #draw). + * Because above steps are running on splash-screen thread, so pre-draw the BitmapShader on + * splash-screen background tread can make they execute in parallel, which ensure it is faster then + * to draw the AdaptiveIconDrawable when receive callback from Choreographer#doFrame. + * + * Here is the sequence to compare the difference between using single and two thread. + * + * Single thread: + * => makeSplashScreenContentView -> WM#addView .. waiting for Choreographer#doFrame -> relayout + * -> draw -> AdaptiveIconDrawable#draw + * + * Two threads: + * => makeSplashScreenContentView -> cachePaint(=AdaptiveIconDrawable#draw) + * => WM#addView -> .. waiting for Choreographer#doFrame -> relayout -> draw -> (draw the Paint + * directly). + */ +class SplashscreenWindowCreator extends AbsSplashWindowCreator { + private static final int LIGHT_BARS_MASK = + WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS + | WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS; + + private final WindowManagerGlobal mWindowManagerGlobal; + private Choreographer mChoreographer; + + /** + * Records of {@link SurfaceControlViewHost} where the splash screen icon animation is + * rendered and that have not yet been removed by their client. + */ + private final SparseArray<SurfaceControlViewHost> mAnimatedSplashScreenSurfaceHosts = + new SparseArray<>(1); + + SplashscreenWindowCreator(SplashscreenContentDrawer contentDrawer, Context context, + ShellExecutor splashScreenExecutor, DisplayManager displayManager, + StartingSurfaceDrawer.StartingWindowRecordManager startingWindowRecordManager) { + super(contentDrawer, context, splashScreenExecutor, displayManager, + startingWindowRecordManager); + mSplashScreenExecutor.execute(() -> mChoreographer = Choreographer.getInstance()); + mWindowManagerGlobal = WindowManagerGlobal.getInstance(); + } + + void addSplashScreenStartingWindow(StartingWindowInfo windowInfo, + @StartingWindowInfo.StartingWindowType int suggestType) { + final ActivityManager.RunningTaskInfo taskInfo = windowInfo.taskInfo; + final ActivityInfo activityInfo = windowInfo.targetActivityInfo != null + ? windowInfo.targetActivityInfo + : taskInfo.topActivityInfo; + if (activityInfo == null || activityInfo.packageName == null) { + return; + } + // replace with the default theme if the application didn't set + final int theme = getSplashScreenTheme(windowInfo.splashScreenThemeResId, activityInfo); + final Context context = SplashscreenContentDrawer.createContext(mContext, windowInfo, theme, + suggestType, mDisplayManager); + if (context == null) { + return; + } + final WindowManager.LayoutParams params = SplashscreenContentDrawer.createLayoutParameters( + context, windowInfo, suggestType, activityInfo.packageName, + suggestType == STARTING_WINDOW_TYPE_LEGACY_SPLASH_SCREEN + ? PixelFormat.OPAQUE : PixelFormat.TRANSLUCENT, windowInfo.appToken); + + final int displayId = taskInfo.displayId; + final int taskId = taskInfo.taskId; + final Display display = getDisplay(displayId); + + // TODO(b/173975965) tracking performance + // Prepare the splash screen content view on splash screen worker thread in parallel, so the + // content view won't be blocked by binder call like addWindow and relayout. + // 1. Trigger splash screen worker thread to create SplashScreenView before/while + // Session#addWindow. + // 2. Synchronize the SplashscreenView to splash screen thread before Choreographer start + // traversal, which will call Session#relayout on splash screen thread. + // 3. Pre-draw the BitmapShader if the icon is immobile on splash screen worker thread, at + // the same time the splash screen thread should be executing Session#relayout. Blocking the + // traversal -> draw on splash screen thread until the BitmapShader of the icon is ready. + + // Record whether create splash screen view success, notify to current thread after + // create splash screen view finished. + final SplashScreenViewSupplier viewSupplier = new SplashScreenViewSupplier(); + final FrameLayout rootLayout = new FrameLayout( + mSplashscreenContentDrawer.createViewContextWrapper(context)); + rootLayout.setPadding(0, 0, 0, 0); + rootLayout.setFitsSystemWindows(false); + final Runnable setViewSynchronized = () -> { + Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "addSplashScreenView"); + // waiting for setContentView before relayoutWindow + SplashScreenView contentView = viewSupplier.get(); + final StartingSurfaceDrawer.StartingWindowRecord sRecord = + mStartingWindowRecordManager.getRecord(taskId); + final SplashWindowRecord record = sRecord instanceof SplashWindowRecord + ? (SplashWindowRecord) sRecord : null; + // If record == null, either the starting window added fail or removed already. + // Do not add this view if the token is mismatch. + if (record != null && windowInfo.appToken == record.mAppToken) { + // if view == null then creation of content view was failed. + if (contentView != null) { + try { + rootLayout.addView(contentView); + } catch (RuntimeException e) { + Slog.w(TAG, "failed set content view to starting window " + + "at taskId: " + taskId, e); + contentView = null; + } + } + record.setSplashScreenView(contentView); + } + Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); + }; + requestTopUi(true); + mSplashscreenContentDrawer.createContentView(context, suggestType, windowInfo, + viewSupplier::setView, viewSupplier::setUiThreadInitTask); + try { + if (addWindow(taskId, windowInfo.appToken, rootLayout, display, params, suggestType)) { + // We use the splash screen worker thread to create SplashScreenView while adding + // the window, as otherwise Choreographer#doFrame might be delayed on this thread. + // And since Choreographer#doFrame won't happen immediately after adding the window, + // if the view is not added to the PhoneWindow on the first #doFrame, the view will + // not be rendered on the first frame. So here we need to synchronize the view on + // the window before first round relayoutWindow, which will happen after insets + // animation. + mChoreographer.postCallback(CALLBACK_INSETS_ANIMATION, setViewSynchronized, null); + final SplashWindowRecord record = + (SplashWindowRecord) mStartingWindowRecordManager.getRecord(taskId); + if (record != null) { + record.parseAppSystemBarColor(context); + // Block until we get the background color. + final SplashScreenView contentView = viewSupplier.get(); + if (suggestType != STARTING_WINDOW_TYPE_LEGACY_SPLASH_SCREEN) { + contentView.addOnAttachStateChangeListener( + new View.OnAttachStateChangeListener() { + @Override + public void onViewAttachedToWindow(View v) { + final int lightBarAppearance = + ContrastColorUtil.isColorLight( + contentView.getInitBackgroundColor()) + ? LIGHT_BARS_MASK : 0; + contentView.getWindowInsetsController() + .setSystemBarsAppearance( + lightBarAppearance, LIGHT_BARS_MASK); + } + + @Override + public void onViewDetachedFromWindow(View v) { + } + }); + } + } + } else { + // release the icon view host + final SplashScreenView contentView = viewSupplier.get(); + if (contentView.getSurfaceHost() != null) { + SplashScreenView.releaseIconHost(contentView.getSurfaceHost()); + } + } + } catch (RuntimeException e) { + // don't crash if something else bad happens, for example a + // failure loading resources because we are loading from an app + // on external storage that has been unmounted. + Slog.w(TAG, "failed creating starting window at taskId: " + taskId, e); + } + } + + int estimateTaskBackgroundColor(TaskInfo taskInfo) { + if (taskInfo.topActivityInfo == null) { + return Color.TRANSPARENT; + } + final ActivityInfo activityInfo = taskInfo.topActivityInfo; + final String packageName = activityInfo.packageName; + final int userId = taskInfo.userId; + final Context windowContext; + try { + windowContext = mContext.createPackageContextAsUser( + packageName, Context.CONTEXT_RESTRICTED, UserHandle.of(userId)); + } catch (PackageManager.NameNotFoundException e) { + Slog.w(TAG, "Failed creating package context with package name " + + packageName + " for user " + taskInfo.userId, e); + return Color.TRANSPARENT; + } + try { + final IPackageManager packageManager = ActivityThread.getPackageManager(); + final String splashScreenThemeName = packageManager.getSplashScreenTheme(packageName, + userId); + final int splashScreenThemeId = splashScreenThemeName != null + ? windowContext.getResources().getIdentifier(splashScreenThemeName, null, null) + : 0; + + final int theme = getSplashScreenTheme(splashScreenThemeId, activityInfo); + + if (theme != windowContext.getThemeResId()) { + windowContext.setTheme(theme); + } + return mSplashscreenContentDrawer.estimateTaskBackgroundColor(windowContext); + } catch (RuntimeException | RemoteException e) { + Slog.w(TAG, "failed get starting window background color at taskId: " + + taskInfo.taskId, e); + } + return Color.TRANSPARENT; + } + + /** + * Called when the Task wants to copy the splash screen. + */ + public void copySplashScreenView(int taskId) { + final StartingSurfaceDrawer.StartingWindowRecord record = + mStartingWindowRecordManager.getRecord(taskId); + final SplashWindowRecord preView = record instanceof SplashWindowRecord + ? (SplashWindowRecord) record : null; + SplashScreenView.SplashScreenViewParcelable parcelable; + SplashScreenView splashScreenView = preView != null ? preView.mSplashView : null; + if (splashScreenView != null && splashScreenView.isCopyable()) { + parcelable = new SplashScreenView.SplashScreenViewParcelable(splashScreenView); + parcelable.setClientCallback( + new RemoteCallback((bundle) -> mSplashScreenExecutor.execute( + () -> onAppSplashScreenViewRemoved(taskId, false)))); + splashScreenView.onCopied(); + mAnimatedSplashScreenSurfaceHosts.append(taskId, splashScreenView.getSurfaceHost()); + } else { + parcelable = null; + } + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW, + "Copying splash screen window view for task: %d with parcelable %b", + taskId, parcelable != null); + ActivityTaskManager.getInstance().onSplashScreenViewCopyFinished(taskId, parcelable); + } + + /** + * Called when the {@link SplashScreenView} is removed from the client Activity view's hierarchy + * or when the Activity is clean up. + * + * @param taskId The Task id on which the splash screen was attached + */ + public void onAppSplashScreenViewRemoved(int taskId) { + onAppSplashScreenViewRemoved(taskId, true /* fromServer */); + } + + /** + * @param fromServer If true, this means the removal was notified by the server. This is only + * used for debugging purposes. + * @see #onAppSplashScreenViewRemoved(int) + */ + private void onAppSplashScreenViewRemoved(int taskId, boolean fromServer) { + SurfaceControlViewHost viewHost = + mAnimatedSplashScreenSurfaceHosts.get(taskId); + if (viewHost == null) { + return; + } + mAnimatedSplashScreenSurfaceHosts.remove(taskId); + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW, + "%s the splash screen. Releasing SurfaceControlViewHost for task: %d", + fromServer ? "Server cleaned up" : "App removed", taskId); + SplashScreenView.releaseIconHost(viewHost); + } + + protected boolean addWindow(int taskId, IBinder appToken, View view, Display display, + WindowManager.LayoutParams params, + @StartingWindowInfo.StartingWindowType int suggestType) { + boolean shouldSaveView = true; + final Context context = view.getContext(); + try { + Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "addRootView"); + mWindowManagerGlobal.addView(view, params, display, + null /* parentWindow */, context.getUserId()); + } catch (WindowManager.BadTokenException e) { + // ignore + Slog.w(TAG, appToken + " already running, starting window not displayed. " + + e.getMessage()); + shouldSaveView = false; + } finally { + Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); + if (view.getParent() == null) { + Slog.w(TAG, "view not successfully added to wm, removing view"); + mWindowManagerGlobal.removeView(view, true /* immediate */); + shouldSaveView = false; + } + } + if (shouldSaveView) { + mStartingWindowRecordManager.removeWindow(taskId, true); + saveSplashScreenRecord(appToken, taskId, view, suggestType); + } + return shouldSaveView; + } + + private void saveSplashScreenRecord(IBinder appToken, int taskId, View view, + @StartingWindowInfo.StartingWindowType int suggestType) { + final SplashWindowRecord tView = + new SplashWindowRecord(appToken, view, suggestType); + mStartingWindowRecordManager.addRecord(taskId, tView); + } + + private void removeWindowInner(View decorView, boolean hideView) { + requestTopUi(false); + if (hideView) { + decorView.setVisibility(View.GONE); + } + mWindowManagerGlobal.removeView(decorView, false /* immediate */); + } + + private static class SplashScreenViewSupplier implements Supplier<SplashScreenView> { + private SplashScreenView mView; + private boolean mIsViewSet; + private Runnable mUiThreadInitTask; + void setView(SplashScreenView view) { + synchronized (this) { + mView = view; + mIsViewSet = true; + notify(); + } + } + + void setUiThreadInitTask(Runnable initTask) { + synchronized (this) { + mUiThreadInitTask = initTask; + } + } + + @Override + @Nullable + public SplashScreenView get() { + synchronized (this) { + while (!mIsViewSet) { + try { + wait(); + } catch (InterruptedException ignored) { + } + } + if (mUiThreadInitTask != null) { + mUiThreadInitTask.run(); + mUiThreadInitTask = null; + } + return mView; + } + } + } + + private class SplashWindowRecord extends StartingSurfaceDrawer.StartingWindowRecord { + private final IBinder mAppToken; + private final View mRootView; + @StartingWindowInfo.StartingWindowType private final int mSuggestType; + private final long mCreateTime; + + private boolean mSetSplashScreen; + private SplashScreenView mSplashView; + private int mSystemBarAppearance; + private boolean mDrawsSystemBarBackgrounds; + + SplashWindowRecord(IBinder appToken, View decorView, + @StartingWindowInfo.StartingWindowType int suggestType) { + mAppToken = appToken; + mRootView = decorView; + mSuggestType = suggestType; + mCreateTime = SystemClock.uptimeMillis(); + } + + void setSplashScreenView(@Nullable SplashScreenView splashScreenView) { + if (mSetSplashScreen) { + return; + } + mSplashView = splashScreenView; + mBGColor = mSplashView != null ? mSplashView.getInitBackgroundColor() + : Color.TRANSPARENT; + mSetSplashScreen = true; + } + + void parseAppSystemBarColor(Context context) { + final TypedArray a = context.obtainStyledAttributes(R.styleable.Window); + mDrawsSystemBarBackgrounds = a.getBoolean( + R.styleable.Window_windowDrawsSystemBarBackgrounds, false); + if (a.getBoolean(R.styleable.Window_windowLightStatusBar, false)) { + mSystemBarAppearance |= WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS; + } + if (a.getBoolean(R.styleable.Window_windowLightNavigationBar, false)) { + mSystemBarAppearance |= WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS; + } + a.recycle(); + } + + // Reset the system bar color which set by splash screen, make it align to the app. + void clearSystemBarColor() { + if (mRootView == null || !mRootView.isAttachedToWindow()) { + return; + } + if (mRootView.getLayoutParams() instanceof WindowManager.LayoutParams) { + final WindowManager.LayoutParams lp = + (WindowManager.LayoutParams) mRootView.getLayoutParams(); + if (mDrawsSystemBarBackgrounds) { + lp.flags |= WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS; + } else { + lp.flags &= ~WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS; + } + mRootView.setLayoutParams(lp); + } + mRootView.getWindowInsetsController().setSystemBarsAppearance( + mSystemBarAppearance, LIGHT_BARS_MASK); + } + + @Override + public void removeIfPossible(StartingWindowRemovalInfo info, boolean immediately) { + if (mRootView == null) { + return; + } + if (mSplashView == null) { + // shouldn't happen, the app window may be drawn earlier than starting window? + Slog.e(TAG, "Found empty splash screen, remove!"); + removeWindowInner(mRootView, false); + return; + } + clearSystemBarColor(); + if (immediately + || mSuggestType == STARTING_WINDOW_TYPE_LEGACY_SPLASH_SCREEN) { + removeWindowInner(mRootView, false); + } else { + if (info.playRevealAnimation) { + mSplashscreenContentDrawer.applyExitAnimation(mSplashView, + info.windowAnimationLeash, info.mainFrame, + () -> removeWindowInner(mRootView, true), + mCreateTime, info.roundedCornerRadius); + } else { + // the SplashScreenView has been copied to client, hide the view to skip + // default exit animation + removeWindowInner(mRootView, true); + } + } + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurface.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurface.java index 76105a39189b..538bbec2aa2b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurface.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurface.java @@ -22,14 +22,6 @@ import android.graphics.Color; * Interface to engage starting window feature. */ public interface StartingSurface { - - /** - * Returns a binder that can be passed to an external process to manipulate starting windows. - */ - default IStartingWindow createExternalInterface() { - return null; - } - /** * Returns the background color for a starting window if existing. */ 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..ff06db370d1a 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 @@ -16,172 +16,80 @@ package com.android.wm.shell.startingsurface; -import static android.content.Context.CONTEXT_RESTRICTED; -import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER; -import static android.view.Choreographer.CALLBACK_INSETS_ANIMATION; +import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; import static android.view.Display.DEFAULT_DISPLAY; -import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_LEGACY_SPLASH_SCREEN; -import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_SNAPSHOT; -import android.annotation.Nullable; -import android.app.ActivityManager.RunningTaskInfo; -import android.app.ActivityTaskManager; -import android.app.ActivityThread; +import android.annotation.CallSuper; import android.app.TaskInfo; +import android.app.WindowConfiguration; import android.content.Context; -import android.content.pm.ActivityInfo; -import android.content.pm.IPackageManager; -import android.content.pm.PackageManager; import android.content.res.Configuration; -import android.content.res.Resources; -import android.content.res.TypedArray; import android.graphics.Color; -import android.graphics.PixelFormat; import android.hardware.display.DisplayManager; -import android.os.IBinder; -import android.os.RemoteCallback; -import android.os.RemoteException; -import android.os.SystemClock; -import android.os.Trace; -import android.os.UserHandle; -import android.util.Slog; import android.util.SparseArray; -import android.view.Choreographer; -import android.view.Display; -import android.view.SurfaceControlViewHost; -import android.view.View; -import android.view.WindowInsetsController; +import android.view.IWindow; +import android.view.SurfaceControl; +import android.view.SurfaceSession; import android.view.WindowManager; -import android.view.WindowManagerGlobal; -import android.widget.FrameLayout; +import android.view.WindowlessWindowManager; import android.window.SplashScreenView; -import android.window.SplashScreenView.SplashScreenViewParcelable; import android.window.StartingWindowInfo; import android.window.StartingWindowInfo.StartingWindowType; import android.window.StartingWindowRemovalInfo; import android.window.TaskSnapshot; -import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.protolog.common.ProtoLog; -import com.android.internal.util.ContrastColorUtil; import com.android.launcher3.icons.IconProvider; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.TransactionPool; import com.android.wm.shell.common.annotations.ShellSplashscreenThread; import com.android.wm.shell.protolog.ShellProtoLogGroup; -import java.util.function.Supplier; - /** * A class which able to draw splash screen or snapshot as the starting window for a task. - * - * In order to speed up, there will use two threads to creating a splash screen in parallel. - * Right now we are still using PhoneWindow to create splash screen window, so the view is added to - * the ViewRootImpl, and those view won't be draw immediately because the ViewRootImpl will call - * scheduleTraversal to register a callback from Choreographer, so the drawing result of the view - * can synchronize on each frame. - * - * The bad thing is that we cannot decide when would Choreographer#doFrame happen, and drawing - * the AdaptiveIconDrawable object can be time consuming, so we use the splash-screen background - * thread to draw the AdaptiveIconDrawable object to a Bitmap and cache it to a BitmapShader after - * the SplashScreenView just created, once we get the BitmapShader then the #draw call can be very - * quickly. - * - * So basically we are using the spare time to prepare the SplashScreenView while splash screen - * thread is waiting for - * 1. WindowManager#addView(binder call to WM), - * 2. Choreographer#doFrame happen(uncertain time for next frame, depends on device), - * 3. Session#relayout(another binder call to WM which under Choreographer#doFrame, but will - * always happen before #draw). - * Because above steps are running on splash-screen thread, so pre-draw the BitmapShader on - * splash-screen background tread can make they execute in parallel, which ensure it is faster then - * to draw the AdaptiveIconDrawable when receive callback from Choreographer#doFrame. - * - * Here is the sequence to compare the difference between using single and two thread. - * - * Single thread: - * => makeSplashScreenContentView -> WM#addView .. waiting for Choreographer#doFrame -> relayout - * -> draw -> AdaptiveIconDrawable#draw - * - * Two threads: - * => makeSplashScreenContentView -> cachePaint(=AdaptiveIconDrawable#draw) - * => WM#addView -> .. waiting for Choreographer#doFrame -> relayout -> draw -> (draw the Paint - * directly). */ @ShellSplashscreenThread public class StartingSurfaceDrawer { - private static final String TAG = StartingWindowController.TAG; - private final Context mContext; - private final DisplayManager mDisplayManager; private final ShellExecutor mSplashScreenExecutor; @VisibleForTesting final SplashscreenContentDrawer mSplashscreenContentDrawer; - private Choreographer mChoreographer; - private final WindowManagerGlobal mWindowManagerGlobal; - private StartingSurface.SysuiProxy mSysuiProxy; - private final StartingWindowRemovalInfo mTmpRemovalInfo = new StartingWindowRemovalInfo(); - - private static final int LIGHT_BARS_MASK = - WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS - | WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS; - /** - * The minimum duration during which the splash screen is shown when the splash screen icon is - * animated. - */ - static final long MINIMAL_ANIMATION_DURATION = 400L; - - /** - * Allow the icon style splash screen to be displayed for longer to give time for the animation - * to finish, i.e. the extra buffer time to keep the splash screen if the animation is slightly - * longer than the {@link #MINIMAL_ANIMATION_DURATION} duration. - */ - static final long TIME_WINDOW_DURATION = 100L; - - /** - * The maximum duration during which the splash screen will be shown if the application is ready - * to show before the icon animation finishes. - */ - static final long MAX_ANIMATION_DURATION = MINIMAL_ANIMATION_DURATION + TIME_WINDOW_DURATION; + @VisibleForTesting + final SplashscreenWindowCreator mSplashscreenWindowCreator; + private final SnapshotWindowCreator mSnapshotWindowCreator; + private final WindowlessSplashWindowCreator mWindowlessSplashWindowCreator; + private final WindowlessSnapshotWindowCreator mWindowlessSnapshotWindowCreator; + @VisibleForTesting + final StartingWindowRecordManager mWindowRecords = new StartingWindowRecordManager(); + // Windowless surface could co-exist with starting window in a task. + @VisibleForTesting + final StartingWindowRecordManager mWindowlessRecords = new StartingWindowRecordManager(); /** * @param splashScreenExecutor The thread used to control add and remove starting window. */ public StartingSurfaceDrawer(Context context, ShellExecutor splashScreenExecutor, IconProvider iconProvider, TransactionPool pool) { - mContext = context; - mDisplayManager = mContext.getSystemService(DisplayManager.class); mSplashScreenExecutor = splashScreenExecutor; - mSplashscreenContentDrawer = new SplashscreenContentDrawer(mContext, iconProvider, pool); - mSplashScreenExecutor.execute(() -> mChoreographer = Choreographer.getInstance()); - mWindowManagerGlobal = WindowManagerGlobal.getInstance(); - mDisplayManager.getDisplay(DEFAULT_DISPLAY); - } - - @VisibleForTesting - final SparseArray<StartingWindowRecord> mStartingWindowRecords = new SparseArray<>(); - - /** - * Records of {@link SurfaceControlViewHost} where the splash screen icon animation is - * rendered and that have not yet been removed by their client. - */ - private final SparseArray<SurfaceControlViewHost> mAnimatedSplashScreenSurfaceHosts = - new SparseArray<>(1); - - private Display getDisplay(int displayId) { - return mDisplayManager.getDisplay(displayId); - } - - int getSplashScreenTheme(int splashScreenThemeResId, ActivityInfo activityInfo) { - return splashScreenThemeResId != 0 - ? splashScreenThemeResId - : activityInfo.getThemeResource() != 0 ? activityInfo.getThemeResource() - : com.android.internal.R.style.Theme_DeviceDefault_DayNight; + final DisplayManager displayManager = context.getSystemService(DisplayManager.class); + mSplashscreenContentDrawer = new SplashscreenContentDrawer(context, iconProvider, pool); + displayManager.getDisplay(DEFAULT_DISPLAY); + + mSplashscreenWindowCreator = new SplashscreenWindowCreator(mSplashscreenContentDrawer, + context, splashScreenExecutor, displayManager, mWindowRecords); + mSnapshotWindowCreator = new SnapshotWindowCreator(splashScreenExecutor, + mWindowRecords); + mWindowlessSplashWindowCreator = new WindowlessSplashWindowCreator( + mSplashscreenContentDrawer, context, splashScreenExecutor, displayManager, + mWindowlessRecords, pool); + mWindowlessSnapshotWindowCreator = new WindowlessSnapshotWindowCreator( + mWindowlessRecords, context, displayManager, mSplashscreenContentDrawer, pool); } void setSysuiProxy(StartingSurface.SysuiProxy sysuiProxy) { - mSysuiProxy = sysuiProxy; + mSplashscreenWindowCreator.setSysuiProxy(sysuiProxy); + mWindowlessSplashWindowCreator.setSysuiProxy(sysuiProxy); } /** @@ -189,326 +97,55 @@ public class StartingSurfaceDrawer { * * @param suggestType The suggestion type to draw the splash screen. */ - void addSplashScreenStartingWindow(StartingWindowInfo windowInfo, IBinder appToken, + void addSplashScreenStartingWindow(StartingWindowInfo windowInfo, @StartingWindowType int suggestType) { - final RunningTaskInfo taskInfo = windowInfo.taskInfo; - final ActivityInfo activityInfo = windowInfo.targetActivityInfo != null - ? windowInfo.targetActivityInfo - : taskInfo.topActivityInfo; - if (activityInfo == null || activityInfo.packageName == null) { - return; - } - - final int displayId = taskInfo.displayId; - final int taskId = taskInfo.taskId; - - // replace with the default theme if the application didn't set - final int theme = getSplashScreenTheme(windowInfo.splashScreenThemeResId, activityInfo); - ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW, - "addSplashScreen for package: %s with theme: %s for task: %d, suggestType: %d", - activityInfo.packageName, Integer.toHexString(theme), taskId, suggestType); - final Display display = getDisplay(displayId); - if (display == null) { - // Can't show splash screen on requested display, so skip showing at all. - return; - } - Context context = displayId == DEFAULT_DISPLAY - ? mContext : mContext.createDisplayContext(display); - if (context == null) { - return; - } - if (theme != context.getThemeResId()) { - try { - context = context.createPackageContextAsUser(activityInfo.packageName, - CONTEXT_RESTRICTED, UserHandle.of(taskInfo.userId)); - context.setTheme(theme); - } catch (PackageManager.NameNotFoundException e) { - Slog.w(TAG, "Failed creating package context with package name " - + activityInfo.packageName + " for user " + taskInfo.userId, e); - return; - } - } - - final Configuration taskConfig = taskInfo.getConfiguration(); - if (taskConfig.diffPublicOnly(context.getResources().getConfiguration()) != 0) { - ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW, - "addSplashScreen: creating context based on task Configuration %s", - taskConfig); - final Context overrideContext = context.createConfigurationContext(taskConfig); - overrideContext.setTheme(theme); - final TypedArray typedArray = overrideContext.obtainStyledAttributes( - com.android.internal.R.styleable.Window); - final int resId = typedArray.getResourceId(R.styleable.Window_windowBackground, 0); - try { - if (resId != 0 && overrideContext.getDrawable(resId) != null) { - // We want to use the windowBackground for the override context if it is - // available, otherwise we use the default one to make sure a themed starting - // window is displayed for the app. - ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW, - "addSplashScreen: apply overrideConfig %s", - taskConfig); - context = overrideContext; - } - } catch (Resources.NotFoundException e) { - Slog.w(TAG, "failed creating starting window for overrideConfig at taskId: " - + taskId, e); - return; - } - typedArray.recycle(); - } - - final WindowManager.LayoutParams params = new WindowManager.LayoutParams( - WindowManager.LayoutParams.TYPE_APPLICATION_STARTING); - params.setFitInsetsSides(0); - params.setFitInsetsTypes(0); - params.format = PixelFormat.TRANSLUCENT; - int windowFlags = WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED - | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN - | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR; - final TypedArray a = context.obtainStyledAttributes(R.styleable.Window); - if (a.getBoolean(R.styleable.Window_windowShowWallpaper, false)) { - windowFlags |= WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER; - } - if (suggestType == STARTING_WINDOW_TYPE_LEGACY_SPLASH_SCREEN) { - if (a.getBoolean(R.styleable.Window_windowDrawsSystemBarBackgrounds, false)) { - windowFlags |= WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS; - } - } else { - windowFlags |= WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS; - } - params.layoutInDisplayCutoutMode = a.getInt( - R.styleable.Window_windowLayoutInDisplayCutoutMode, - params.layoutInDisplayCutoutMode); - params.windowAnimations = a.getResourceId(R.styleable.Window_windowAnimationStyle, 0); - a.recycle(); - - // Assumes it's safe to show starting windows of launched apps while - // the keyguard is being hidden. This is okay because starting windows never show - // secret information. - // TODO(b/113840485): Occluded may not only happen on default display - if (displayId == DEFAULT_DISPLAY && windowInfo.isKeyguardOccluded) { - windowFlags |= WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED; - } - - // Force the window flags: this is a fake window, so it is not really - // touchable or focusable by the user. We also add in the ALT_FOCUSABLE_IM - // flag because we do know that the next window will take input - // focus, so we want to get the IME window up on top of us right away. - // Touches will only pass through to the host activity window and will be blocked from - // passing to any other windows. - windowFlags |= WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE - | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE - | WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; - params.flags = windowFlags; - params.token = appToken; - params.packageName = activityInfo.packageName; - params.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS; - - if (!context.getResources().getCompatibilityInfo().supportsScreen()) { - params.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_COMPATIBLE_WINDOW; - } - - params.setTitle("Splash Screen " + activityInfo.packageName); - - // TODO(b/173975965) tracking performance - // Prepare the splash screen content view on splash screen worker thread in parallel, so the - // content view won't be blocked by binder call like addWindow and relayout. - // 1. Trigger splash screen worker thread to create SplashScreenView before/while - // Session#addWindow. - // 2. Synchronize the SplashscreenView to splash screen thread before Choreographer start - // traversal, which will call Session#relayout on splash screen thread. - // 3. Pre-draw the BitmapShader if the icon is immobile on splash screen worker thread, at - // the same time the splash screen thread should be executing Session#relayout. Blocking the - // traversal -> draw on splash screen thread until the BitmapShader of the icon is ready. - - // Record whether create splash screen view success, notify to current thread after - // create splash screen view finished. - final SplashScreenViewSupplier viewSupplier = new SplashScreenViewSupplier(); - final FrameLayout rootLayout = new FrameLayout( - mSplashscreenContentDrawer.createViewContextWrapper(context)); - rootLayout.setPadding(0, 0, 0, 0); - rootLayout.setFitsSystemWindows(false); - final Runnable setViewSynchronized = () -> { - Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "addSplashScreenView"); - // waiting for setContentView before relayoutWindow - SplashScreenView contentView = viewSupplier.get(); - final StartingWindowRecord record = mStartingWindowRecords.get(taskId); - // If record == null, either the starting window added fail or removed already. - // Do not add this view if the token is mismatch. - if (record != null && appToken == record.mAppToken) { - // if view == null then creation of content view was failed. - if (contentView != null) { - try { - rootLayout.addView(contentView); - } catch (RuntimeException e) { - Slog.w(TAG, "failed set content view to starting window " - + "at taskId: " + taskId, e); - contentView = null; - } - } - record.setSplashScreenView(contentView); - } - Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); - }; - if (mSysuiProxy != null) { - mSysuiProxy.requestTopUi(true, TAG); - } - mSplashscreenContentDrawer.createContentView(context, suggestType, windowInfo, - viewSupplier::setView, viewSupplier::setUiThreadInitTask); - try { - if (addWindow(taskId, appToken, rootLayout, display, params, suggestType)) { - // We use the splash screen worker thread to create SplashScreenView while adding - // the window, as otherwise Choreographer#doFrame might be delayed on this thread. - // And since Choreographer#doFrame won't happen immediately after adding the window, - // if the view is not added to the PhoneWindow on the first #doFrame, the view will - // not be rendered on the first frame. So here we need to synchronize the view on - // the window before first round relayoutWindow, which will happen after insets - // animation. - mChoreographer.postCallback(CALLBACK_INSETS_ANIMATION, setViewSynchronized, null); - final StartingWindowRecord record = mStartingWindowRecords.get(taskId); - record.parseAppSystemBarColor(context); - // Block until we get the background color. - final SplashScreenView contentView = viewSupplier.get(); - if (suggestType != STARTING_WINDOW_TYPE_LEGACY_SPLASH_SCREEN) { - contentView.addOnAttachStateChangeListener( - new View.OnAttachStateChangeListener() { - @Override - public void onViewAttachedToWindow(View v) { - final int lightBarAppearance = ContrastColorUtil.isColorLight( - contentView.getInitBackgroundColor()) - ? LIGHT_BARS_MASK : 0; - contentView.getWindowInsetsController().setSystemBarsAppearance( - lightBarAppearance, LIGHT_BARS_MASK); - } - - @Override - public void onViewDetachedFromWindow(View v) { - } - }); - } - record.mBGColor = contentView.getInitBackgroundColor(); - } else { - // release the icon view host - final SplashScreenView contentView = viewSupplier.get(); - if (contentView.getSurfaceHost() != null) { - SplashScreenView.releaseIconHost(contentView.getSurfaceHost()); - } - } - } catch (RuntimeException e) { - // don't crash if something else bad happens, for example a - // failure loading resources because we are loading from an app - // on external storage that has been unmounted. - Slog.w(TAG, "failed creating starting window at taskId: " + taskId, e); - } + mSplashscreenWindowCreator.addSplashScreenStartingWindow(windowInfo, suggestType); } int getStartingWindowBackgroundColorForTask(int taskId) { - final StartingWindowRecord startingWindowRecord = mStartingWindowRecords.get(taskId); + final StartingWindowRecord startingWindowRecord = mWindowRecords.getRecord(taskId); if (startingWindowRecord == null) { return Color.TRANSPARENT; } - return startingWindowRecord.mBGColor; - } - - private static class SplashScreenViewSupplier implements Supplier<SplashScreenView> { - private SplashScreenView mView; - private boolean mIsViewSet; - private Runnable mUiThreadInitTask; - void setView(SplashScreenView view) { - synchronized (this) { - mView = view; - mIsViewSet = true; - notify(); - } - } - - void setUiThreadInitTask(Runnable initTask) { - synchronized (this) { - mUiThreadInitTask = initTask; - } - } - - @Override - public @Nullable SplashScreenView get() { - synchronized (this) { - while (!mIsViewSet) { - try { - wait(); - } catch (InterruptedException ignored) { - } - } - if (mUiThreadInitTask != null) { - mUiThreadInitTask.run(); - mUiThreadInitTask = null; - } - return mView; - } - } + return startingWindowRecord.getBGColor(); } int estimateTaskBackgroundColor(TaskInfo taskInfo) { - if (taskInfo.topActivityInfo == null) { - return Color.TRANSPARENT; - } - final ActivityInfo activityInfo = taskInfo.topActivityInfo; - final String packageName = activityInfo.packageName; - final int userId = taskInfo.userId; - final Context windowContext; - try { - windowContext = mContext.createPackageContextAsUser( - packageName, Context.CONTEXT_RESTRICTED, UserHandle.of(userId)); - } catch (PackageManager.NameNotFoundException e) { - Slog.w(TAG, "Failed creating package context with package name " - + packageName + " for user " + taskInfo.userId, e); - return Color.TRANSPARENT; - } - try { - final IPackageManager packageManager = ActivityThread.getPackageManager(); - final String splashScreenThemeName = packageManager.getSplashScreenTheme(packageName, - userId); - final int splashScreenThemeId = splashScreenThemeName != null - ? windowContext.getResources().getIdentifier(splashScreenThemeName, null, null) - : 0; - - final int theme = getSplashScreenTheme(splashScreenThemeId, activityInfo); - - if (theme != windowContext.getThemeResId()) { - windowContext.setTheme(theme); - } - return mSplashscreenContentDrawer.estimateTaskBackgroundColor(windowContext); - } catch (RuntimeException | RemoteException e) { - Slog.w(TAG, "failed get starting window background color at taskId: " - + taskInfo.taskId, e); - } - return Color.TRANSPARENT; + return mSplashscreenWindowCreator.estimateTaskBackgroundColor(taskInfo); } /** * Called when a task need a snapshot starting window. */ - void makeTaskSnapshotWindow(StartingWindowInfo startingWindowInfo, IBinder appToken, - TaskSnapshot snapshot) { - final int taskId = startingWindowInfo.taskInfo.taskId; - // Remove any existing starting window for this task before adding. - removeWindowNoAnimate(taskId); - final TaskSnapshotWindow surface = TaskSnapshotWindow.create(startingWindowInfo, appToken, - snapshot, mSplashScreenExecutor, () -> removeWindowNoAnimate(taskId)); - if (surface == null) { - return; - } - final StartingWindowRecord tView = new StartingWindowRecord(appToken, - null/* decorView */, surface, STARTING_WINDOW_TYPE_SNAPSHOT); - mStartingWindowRecords.put(taskId, tView); + void makeTaskSnapshotWindow(StartingWindowInfo startingWindowInfo, TaskSnapshot snapshot) { + mSnapshotWindowCreator.makeTaskSnapshotWindow(startingWindowInfo, snapshot); } /** * Called when the content of a task is ready to show, starting window can be removed. */ public void removeStartingWindow(StartingWindowRemovalInfo removalInfo) { - ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW, - "Task start finish, remove starting surface for task: %d", - removalInfo.taskId); - removeWindowSynced(removalInfo, false /* immediately */); + if (removalInfo.windowlessSurface) { + mWindowlessRecords.removeWindow(removalInfo, removalInfo.removeImmediately); + } else { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW, + "Task start finish, remove starting surface for task: %d", + removalInfo.taskId); + mWindowRecords.removeWindow(removalInfo, removalInfo.removeImmediately); + } + } + + /** + * Create a windowless starting surface and attach to the root surface. + */ + void addWindowlessStartingSurface(StartingWindowInfo windowInfo) { + if (windowInfo.taskSnapshot != null) { + mWindowlessSnapshotWindowCreator.makeTaskSnapshotWindow(windowInfo, + windowInfo.rootSurface, windowInfo.taskSnapshot, mSplashScreenExecutor); + } else { + mWindowlessSplashWindowCreator.addSplashScreenStartingWindow( + windowInfo, windowInfo.rootSurface); + } } /** @@ -517,37 +154,15 @@ public class StartingSurfaceDrawer { public void clearAllWindows() { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW, "Clear all starting windows immediately"); - final int taskSize = mStartingWindowRecords.size(); - final int[] taskIds = new int[taskSize]; - for (int i = taskSize - 1; i >= 0; --i) { - taskIds[i] = mStartingWindowRecords.keyAt(i); - } - for (int i = taskSize - 1; i >= 0; --i) { - removeWindowNoAnimate(taskIds[i]); - } + mWindowRecords.clearAllWindows(); + mWindowlessRecords.clearAllWindows(); } /** * Called when the Task wants to copy the splash screen. */ public void copySplashScreenView(int taskId) { - final StartingWindowRecord preView = mStartingWindowRecords.get(taskId); - SplashScreenViewParcelable parcelable; - SplashScreenView splashScreenView = preView != null ? preView.mContentView : null; - if (splashScreenView != null && splashScreenView.isCopyable()) { - parcelable = new SplashScreenViewParcelable(splashScreenView); - parcelable.setClientCallback( - new RemoteCallback((bundle) -> mSplashScreenExecutor.execute( - () -> onAppSplashScreenViewRemoved(taskId, false)))); - splashScreenView.onCopied(); - mAnimatedSplashScreenSurfaceHosts.append(taskId, splashScreenView.getSurfaceHost()); - } else { - parcelable = null; - } - ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW, - "Copying splash screen window view for task: %d with parcelable %b", - taskId, parcelable != null); - ActivityTaskManager.getInstance().onSplashScreenViewCopyFinished(taskId, parcelable); + mSplashscreenWindowCreator.copySplashScreenView(taskId); } /** @@ -557,195 +172,148 @@ public class StartingSurfaceDrawer { * @param taskId The Task id on which the splash screen was attached */ public void onAppSplashScreenViewRemoved(int taskId) { - onAppSplashScreenViewRemoved(taskId, true /* fromServer */); + mSplashscreenWindowCreator.onAppSplashScreenViewRemoved(taskId); } - /** - * @param fromServer If true, this means the removal was notified by the server. This is only - * used for debugging purposes. - * @see #onAppSplashScreenViewRemoved(int) - */ - private void onAppSplashScreenViewRemoved(int taskId, boolean fromServer) { - SurfaceControlViewHost viewHost = - mAnimatedSplashScreenSurfaceHosts.get(taskId); - if (viewHost == null) { - return; + void onImeDrawnOnTask(int taskId) { + onImeDrawnOnTask(mWindowRecords, taskId); + onImeDrawnOnTask(mWindowlessRecords, taskId); + } + + private void onImeDrawnOnTask(StartingWindowRecordManager records, int taskId) { + final StartingSurfaceDrawer.StartingWindowRecord sRecord = + records.getRecord(taskId); + final SnapshotRecord record = sRecord instanceof SnapshotRecord + ? (SnapshotRecord) sRecord : null; + if (record != null && record.hasImeSurface()) { + records.removeWindow(taskId, true); } - mAnimatedSplashScreenSurfaceHosts.remove(taskId); - ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW, - "%s the splash screen. Releasing SurfaceControlViewHost for task: %d", - fromServer ? "Server cleaned up" : "App removed", taskId); - SplashScreenView.releaseIconHost(viewHost); } - protected boolean addWindow(int taskId, IBinder appToken, View view, Display display, - WindowManager.LayoutParams params, @StartingWindowType int suggestType) { - boolean shouldSaveView = true; - final Context context = view.getContext(); - try { - Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "addRootView"); - mWindowManagerGlobal.addView(view, params, display, - null /* parentWindow */, context.getUserId()); - } catch (WindowManager.BadTokenException e) { - // ignore - Slog.w(TAG, appToken + " already running, starting window not displayed. " - + e.getMessage()); - shouldSaveView = false; - } finally { - Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); - if (view.getParent() == null) { - Slog.w(TAG, "view not successfully added to wm, removing view"); - mWindowManagerGlobal.removeView(view, true /* immediate */); - shouldSaveView = false; + static class WindowlessStartingWindow extends WindowlessWindowManager { + SurfaceControl mChildSurface; + + WindowlessStartingWindow(Configuration c, SurfaceControl rootSurface) { + super(c, rootSurface, null /* hostInputToken */); + } + + @Override + protected SurfaceControl getParentSurface(IWindow window, + WindowManager.LayoutParams attrs) { + final SurfaceControl.Builder builder = new SurfaceControl.Builder(new SurfaceSession()) + .setContainerLayer() + .setName("Windowless window") + .setHidden(false) + .setParent(mRootSurface) + .setCallsite("WindowlessStartingWindow#attachToParentSurface"); + mChildSurface = builder.build(); + try (SurfaceControl.Transaction t = new SurfaceControl.Transaction()) { + t.setLayer(mChildSurface, Integer.MAX_VALUE); + t.apply(); } + return mChildSurface; } - if (shouldSaveView) { - removeWindowNoAnimate(taskId); - saveSplashScreenRecord(appToken, taskId, view, suggestType); + } + abstract static class StartingWindowRecord { + protected int mBGColor; + abstract void removeIfPossible(StartingWindowRemovalInfo info, boolean immediately); + int getBGColor() { + return mBGColor; } - return shouldSaveView; } - @VisibleForTesting - void saveSplashScreenRecord(IBinder appToken, int taskId, View view, - @StartingWindowType int suggestType) { - final StartingWindowRecord tView = new StartingWindowRecord(appToken, view, - null/* TaskSnapshotWindow */, suggestType); - mStartingWindowRecords.put(taskId, tView); - } + abstract static class SnapshotRecord extends StartingWindowRecord { + private static final long DELAY_REMOVAL_TIME_GENERAL = 100; + /** + * The max delay time in milliseconds for removing the task snapshot window with IME + * visible. + * Ideally the delay time will be shorter when receiving + * {@link StartingSurfaceDrawer#onImeDrawnOnTask(int)}. + */ + private static final long MAX_DELAY_REMOVAL_TIME_IME_VISIBLE = 600; + private final Runnable mScheduledRunnable = this::removeImmediately; - private void removeWindowNoAnimate(int taskId) { - mTmpRemovalInfo.taskId = taskId; - removeWindowSynced(mTmpRemovalInfo, true /* immediately */); - } + @WindowConfiguration.ActivityType protected final int mActivityType; + protected final ShellExecutor mRemoveExecutor; - void onImeDrawnOnTask(int taskId) { - final StartingWindowRecord record = mStartingWindowRecords.get(taskId); - if (record != null && record.mTaskSnapshotWindow != null - && record.mTaskSnapshotWindow.hasImeSurface()) { - removeWindowNoAnimate(taskId); + SnapshotRecord(int activityType, ShellExecutor removeExecutor) { + mActivityType = activityType; + mRemoveExecutor = removeExecutor; } - } - - protected void removeWindowSynced(StartingWindowRemovalInfo removalInfo, boolean immediately) { - final int taskId = removalInfo.taskId; - final StartingWindowRecord record = mStartingWindowRecords.get(taskId); - if (record != null) { - if (record.mDecorView != null) { - ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW, - "Removing splash screen window for task: %d", taskId); - if (record.mContentView != null) { - record.clearSystemBarColor(); - if (immediately - || record.mSuggestType == STARTING_WINDOW_TYPE_LEGACY_SPLASH_SCREEN) { - removeWindowInner(record.mDecorView, false); - } else { - if (removalInfo.playRevealAnimation) { - mSplashscreenContentDrawer.applyExitAnimation(record.mContentView, - removalInfo.windowAnimationLeash, removalInfo.mainFrame, - () -> removeWindowInner(record.mDecorView, true), - record.mCreateTime); - } else { - // the SplashScreenView has been copied to client, hide the view to skip - // default exit animation - removeWindowInner(record.mDecorView, true); - } - } - } else { - // shouldn't happen - Slog.e(TAG, "Found empty splash screen, remove!"); - removeWindowInner(record.mDecorView, false); - } + @Override + public final void removeIfPossible(StartingWindowRemovalInfo info, boolean immediately) { + if (immediately) { + removeImmediately(); + } else { + scheduleRemove(info.deferRemoveForIme); } - if (record.mTaskSnapshotWindow != null) { - ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW, - "Removing task snapshot window for %d", taskId); - if (immediately) { - record.mTaskSnapshotWindow.removeImmediately(); - } else { - record.mTaskSnapshotWindow.scheduleRemove(removalInfo.deferRemoveForIme); - } - } - mStartingWindowRecords.remove(taskId); } - } - private void removeWindowInner(View decorView, boolean hideView) { - if (mSysuiProxy != null) { - mSysuiProxy.requestTopUi(false, TAG); + void scheduleRemove(boolean deferRemoveForIme) { + // Show the latest content as soon as possible for unlocking to home. + if (mActivityType == ACTIVITY_TYPE_HOME) { + removeImmediately(); + return; + } + mRemoveExecutor.removeCallbacks(mScheduledRunnable); + final long delayRemovalTime = hasImeSurface() && deferRemoveForIme + ? MAX_DELAY_REMOVAL_TIME_IME_VISIBLE + : DELAY_REMOVAL_TIME_GENERAL; + mRemoveExecutor.executeDelayed(mScheduledRunnable, delayRemovalTime); + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW, + "Defer removing snapshot surface in %d", delayRemovalTime); } - if (hideView) { - decorView.setVisibility(View.GONE); + + protected abstract boolean hasImeSurface(); + + @CallSuper + protected void removeImmediately() { + mRemoveExecutor.removeCallbacks(mScheduledRunnable); } - mWindowManagerGlobal.removeView(decorView, false /* immediate */); } - /** - * Record the view or surface for a starting window. - */ - private static class StartingWindowRecord { - private final IBinder mAppToken; - private final View mDecorView; - private final TaskSnapshotWindow mTaskSnapshotWindow; - private SplashScreenView mContentView; - private boolean mSetSplashScreen; - private @StartingWindowType int mSuggestType; - private int mBGColor; - private final long mCreateTime; - private int mSystemBarAppearance; - private boolean mDrawsSystemBarBackgrounds; - - StartingWindowRecord(IBinder appToken, View decorView, - TaskSnapshotWindow taskSnapshotWindow, @StartingWindowType int suggestType) { - mAppToken = appToken; - mDecorView = decorView; - mTaskSnapshotWindow = taskSnapshotWindow; - if (mTaskSnapshotWindow != null) { - mBGColor = mTaskSnapshotWindow.getBackgroundColor(); + static class StartingWindowRecordManager { + private final StartingWindowRemovalInfo mTmpRemovalInfo = new StartingWindowRemovalInfo(); + private final SparseArray<StartingWindowRecord> mStartingWindowRecords = + new SparseArray<>(); + + void clearAllWindows() { + final int taskSize = mStartingWindowRecords.size(); + final int[] taskIds = new int[taskSize]; + for (int i = taskSize - 1; i >= 0; --i) { + taskIds[i] = mStartingWindowRecords.keyAt(i); + } + for (int i = taskSize - 1; i >= 0; --i) { + removeWindow(taskIds[i], true); } - mSuggestType = suggestType; - mCreateTime = SystemClock.uptimeMillis(); } - private void setSplashScreenView(SplashScreenView splashScreenView) { - if (mSetSplashScreen) { - return; - } - mContentView = splashScreenView; - mSetSplashScreen = true; + void addRecord(int taskId, StartingWindowRecord record) { + mStartingWindowRecords.put(taskId, record); } - private void parseAppSystemBarColor(Context context) { - final TypedArray a = context.obtainStyledAttributes(R.styleable.Window); - mDrawsSystemBarBackgrounds = a.getBoolean( - R.styleable.Window_windowDrawsSystemBarBackgrounds, false); - if (a.getBoolean(R.styleable.Window_windowLightStatusBar, false)) { - mSystemBarAppearance |= WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS; - } - if (a.getBoolean(R.styleable.Window_windowLightNavigationBar, false)) { - mSystemBarAppearance |= WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS; + void removeWindow(StartingWindowRemovalInfo removeInfo, boolean immediately) { + final int taskId = removeInfo.taskId; + final StartingWindowRecord record = mStartingWindowRecords.get(taskId); + if (record != null) { + record.removeIfPossible(removeInfo, immediately); + mStartingWindowRecords.remove(taskId); } - a.recycle(); } - // Reset the system bar color which set by splash screen, make it align to the app. - private void clearSystemBarColor() { - if (mDecorView == null) { - return; - } - if (mDecorView.getLayoutParams() instanceof WindowManager.LayoutParams) { - final WindowManager.LayoutParams lp = - (WindowManager.LayoutParams) mDecorView.getLayoutParams(); - if (mDrawsSystemBarBackgrounds) { - lp.flags |= WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS; - } else { - lp.flags &= ~WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS; - } - mDecorView.setLayoutParams(lp); - } - mDecorView.getWindowInsetsController().setSystemBarsAppearance( - mSystemBarAppearance, LIGHT_BARS_MASK); + void removeWindow(int taskId, boolean immediately) { + mTmpRemovalInfo.taskId = taskId; + removeWindow(mTmpRemovalInfo, immediately); + } + + StartingWindowRecord getRecord(int taskId) { + return mStartingWindowRecords.get(taskId); + } + + @VisibleForTesting + int recordSize() { + return mStartingWindowRecords.size(); } } } 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..bec4ba3bf0d1 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 @@ -21,14 +21,15 @@ import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_NONE; import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_SNAPSHOT; import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_SOLID_COLOR_SPLASH_SCREEN; import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_SPLASH_SCREEN; +import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_WINDOWLESS; import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission; +import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_STARTING_WINDOW; import android.app.ActivityManager.RunningTaskInfo; import android.app.TaskInfo; import android.content.Context; import android.graphics.Color; -import android.os.IBinder; import android.os.Trace; import android.util.SparseIntArray; import android.window.StartingWindowInfo; @@ -38,14 +39,19 @@ import android.window.TaskOrganizer; import android.window.TaskSnapshot; import androidx.annotation.BinderThread; +import androidx.annotation.VisibleForTesting; 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.ExternalInterfaceBinder; import com.android.wm.shell.common.RemoteCallable; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SingleInstanceRemoteListener; import com.android.wm.shell.common.TransactionPool; +import com.android.wm.shell.sysui.ShellController; +import com.android.wm.shell.sysui.ShellInit; /** * Implementation to draw the starting window to an application, and remove the starting window @@ -74,6 +80,8 @@ public class StartingWindowController implements RemoteCallable<StartingWindowCo private TriConsumer<Integer, Integer, Integer> mTaskLaunchingCallback; private final StartingSurfaceImpl mImpl = new StartingSurfaceImpl(); private final Context mContext; + private final ShellController mShellController; + private final ShellTaskOrganizer mShellTaskOrganizer; private final ShellExecutor mSplashScreenExecutor; /** * Need guarded because it has exposed to StartingSurface @@ -81,14 +89,22 @@ 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, + ShellController shellController, + ShellTaskOrganizer shellTaskOrganizer, + ShellExecutor splashScreenExecutor, + StartingWindowTypeAlgorithm startingWindowTypeAlgorithm, + IconProvider iconProvider, TransactionPool pool) { mContext = context; + mShellController = shellController; + mShellTaskOrganizer = shellTaskOrganizer; mStartingSurfaceDrawer = new StartingSurfaceDrawer(context, splashScreenExecutor, iconProvider, pool); mStartingWindowTypeAlgorithm = startingWindowTypeAlgorithm; mSplashScreenExecutor = splashScreenExecutor; + shellInit.addInitCallback(this::onInit, this); } /** @@ -98,6 +114,16 @@ public class StartingWindowController implements RemoteCallable<StartingWindowCo return mImpl; } + private ExternalInterfaceBinder createExternalInterface() { + return new IStartingWindowImpl(this); + } + + private void onInit() { + mShellTaskOrganizer.initStartingWindow(this); + mShellController.addExternalInterface(KEY_EXTRA_SHELL_STARTING_WINDOW, + this::createExternalInterface, this); + } + @Override public Context getContext() { return mContext; @@ -113,29 +139,36 @@ public class StartingWindowController implements RemoteCallable<StartingWindowCo * * @param listener The callback when need a starting window. */ + @VisibleForTesting void setStartingWindowListener(TriConsumer<Integer, Integer, Integer> listener) { mTaskLaunchingCallback = listener; } + @VisibleForTesting + boolean hasStartingWindowListener() { + return mTaskLaunchingCallback != null; + } + /** * Called when a task need a starting window. */ - public void addStartingWindow(StartingWindowInfo windowInfo, IBinder appToken) { + public void addStartingWindow(StartingWindowInfo windowInfo) { mSplashScreenExecutor.execute(() -> { Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "addStartingWindow"); final int suggestionType = mStartingWindowTypeAlgorithm.getSuggestedWindowType( windowInfo); final RunningTaskInfo runningTaskInfo = windowInfo.taskInfo; - if (isSplashScreenType(suggestionType)) { - mStartingSurfaceDrawer.addSplashScreenStartingWindow(windowInfo, appToken, - suggestionType); + if (suggestionType == STARTING_WINDOW_TYPE_WINDOWLESS) { + mStartingSurfaceDrawer.addWindowlessStartingSurface(windowInfo); + } else if (isSplashScreenType(suggestionType)) { + mStartingSurfaceDrawer.addSplashScreenStartingWindow(windowInfo, suggestionType); } else if (suggestionType == STARTING_WINDOW_TYPE_SNAPSHOT) { final TaskSnapshot snapshot = windowInfo.taskSnapshot; - mStartingSurfaceDrawer.makeTaskSnapshotWindow(windowInfo, appToken, - snapshot); + mStartingSurfaceDrawer.makeTaskSnapshotWindow(windowInfo, snapshot); } - if (suggestionType != STARTING_WINDOW_TYPE_NONE) { + if (suggestionType != STARTING_WINDOW_TYPE_NONE + && suggestionType != STARTING_WINDOW_TYPE_WINDOWLESS) { int taskId = runningTaskInfo.taskId; int color = mStartingSurfaceDrawer .getStartingWindowBackgroundColorForTask(taskId); @@ -186,11 +219,13 @@ public class StartingWindowController implements RemoteCallable<StartingWindowCo public void removeStartingWindow(StartingWindowRemovalInfo removalInfo) { mSplashScreenExecutor.execute(() -> mStartingSurfaceDrawer.removeStartingWindow( removalInfo)); - mSplashScreenExecutor.executeDelayed(() -> { - synchronized (mTaskBackgroundColors) { - mTaskBackgroundColors.delete(removalInfo.taskId); - } - }, TASK_BG_COLOR_RETAIN_TIME_MS); + if (!removalInfo.windowlessSurface) { + mSplashScreenExecutor.executeDelayed(() -> { + synchronized (mTaskBackgroundColors) { + mTaskBackgroundColors.delete(removalInfo.taskId); + } + }, TASK_BG_COLOR_RETAIN_TIME_MS); + } } /** @@ -209,17 +244,6 @@ public class StartingWindowController implements RemoteCallable<StartingWindowCo * The interface for calls from outside the Shell, within the host process. */ private class StartingSurfaceImpl implements StartingSurface { - private IStartingWindowImpl mIStartingWindow; - - @Override - public IStartingWindowImpl createExternalInterface() { - if (mIStartingWindow != null) { - mIStartingWindow.invalidate(); - } - mIStartingWindow = new IStartingWindowImpl(StartingWindowController.this); - return mIStartingWindow; - } - @Override public int getBackgroundColor(TaskInfo taskInfo) { synchronized (mTaskBackgroundColors) { @@ -243,7 +267,8 @@ public class StartingWindowController implements RemoteCallable<StartingWindowCo * The interface for calls from outside the host process. */ @BinderThread - private static class IStartingWindowImpl extends IStartingWindow.Stub { + private static class IStartingWindowImpl extends IStartingWindow.Stub + implements ExternalInterfaceBinder { private StartingWindowController mController; private SingleInstanceRemoteListener<StartingWindowController, IStartingWindowListener> mListener; @@ -263,8 +288,11 @@ public class StartingWindowController implements RemoteCallable<StartingWindowCo /** * Invalidates this instance, preventing future calls from updating the controller. */ - void invalidate() { + @Override + public void invalidate() { mController = null; + // Unregister the listener to ensure any registered binder death recipients are unlinked + mListener.unregister(); } @Override 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..c964df1452e0 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 @@ -16,55 +16,17 @@ package com.android.wm.shell.startingsurface; -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; -import static android.view.WindowManager.LayoutParams.FLAG_LOCAL_FOCUS_MODE; -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_SCALED; -import static android.view.WindowManager.LayoutParams.FLAG_SECURE; -import static android.view.WindowManager.LayoutParams.FLAG_SLIPPERY; -import static android.view.WindowManager.LayoutParams.FLAG_SPLIT_TOUCH; -import static android.view.WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION; -import static android.view.WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS; -import static android.view.WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH; -import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_FORCE_DRAW_BAR_BACKGROUNDS; -import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY; -import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_USE_BLAST; import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING; -import static com.android.internal.policy.DecorView.NAVIGATION_BAR_COLOR_VIEW_ATTRIBUTES; -import static com.android.internal.policy.DecorView.STATUS_BAR_COLOR_VIEW_ATTRIBUTES; -import static com.android.internal.policy.DecorView.getNavigationBarRect; - import android.annotation.BinderThread; import android.annotation.NonNull; -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; -import android.graphics.GraphicBuffer; -import android.graphics.Matrix; import android.graphics.Paint; -import android.graphics.PixelFormat; import android.graphics.Point; import android.graphics.Rect; -import android.graphics.RectF; -import android.hardware.HardwareBuffer; import android.os.Bundle; import android.os.IBinder; import android.os.RemoteException; @@ -76,20 +38,14 @@ import android.view.InputChannel; import android.view.InsetsSourceControl; import android.view.InsetsState; import android.view.SurfaceControl; -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; +import android.window.SnapshotDrawerUtils; import android.window.StartingWindowInfo; import android.window.TaskSnapshot; -import com.android.internal.R; -import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.policy.DecorView; import com.android.internal.protolog.common.ProtoLog; import com.android.internal.view.BaseIWindow; import com.android.wm.shell.common.ShellExecutor; @@ -103,59 +59,17 @@ import java.lang.ref.WeakReference; * @hide */ public class TaskSnapshotWindow { - /** - * When creating the starting window, we use the exact same layout flags such that we end up - * with a window with the exact same dimensions etc. However, these flags are not used in layout - * and might cause other side effects so we exclude them. - */ - static final int FLAG_INHERIT_EXCLUDES = FLAG_NOT_FOCUSABLE - | FLAG_NOT_TOUCHABLE - | FLAG_NOT_TOUCH_MODAL - | FLAG_ALT_FOCUSABLE_IM - | FLAG_NOT_FOCUSABLE - | FLAG_HARDWARE_ACCELERATED - | FLAG_IGNORE_CHEEK_PRESSES - | FLAG_LOCAL_FOCUS_MODE - | FLAG_SLIPPERY - | FLAG_WATCH_OUTSIDE_TOUCH - | FLAG_SPLIT_TOUCH - | FLAG_SCALED - | FLAG_SECURE; - private static final String TAG = StartingWindowController.TAG; - private static final String TITLE_FORMAT = "SnapshotStartingWindow for taskId=%s"; - - private static final long DELAY_REMOVAL_TIME_GENERAL = 100; - /** - * The max delay time in milliseconds for removing the task snapshot window with IME visible. - * Ideally the delay time will be shorter when receiving - * {@link StartingSurfaceDrawer#onImeDrawnOnTask(int)}. - */ - private static final long MAX_DELAY_REMOVAL_TIME_IME_VISIBLE = 600; + private static final String TITLE_FORMAT = "SnapshotStartingWindow for taskId="; private final Window mWindow; private final Runnable mClearWindowHandler; private final ShellExecutor mSplashScreenExecutor; - private final SurfaceControl mSurfaceControl; private final IWindowSession mSession; - private final Rect mTaskBounds; - private final Rect mFrame = new Rect(); - private final Rect mSystemBarInsets = new Rect(); - private TaskSnapshot mSnapshot; - private final RectF mTmpSnapshotSize = new RectF(); - private final RectF mTmpDstFrame = new RectF(); - private final CharSequence mTitle; private boolean mHasDrawn; - private boolean mSizeMismatch; private final Paint mBackgroundPaint = new Paint(); - private final int mActivityType; - private final int mStatusBarColor; - private final SystemBarBackgroundPainter mSystemBarBackgroundPainter; private final int mOrientationOnCreation; - private final SurfaceControl.Transaction mTransaction; - private final Matrix mSnapshotMatrix = new Matrix(); - private final float[] mTmpFloat9 = new float[9]; - private final Runnable mScheduledRunnable = this::removeImmediately; + private final boolean mHasImeSurface; static TaskSnapshotWindow create(StartingWindowInfo info, IBinder appToken, @@ -166,79 +80,45 @@ public class TaskSnapshotWindow { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW, "create taskSnapshot surface for task: %d", taskId); - final WindowManager.LayoutParams attrs = info.topOpaqueWindowLayoutParams; - final WindowManager.LayoutParams mainWindowParams = info.mainWindowLayoutParams; final InsetsState topWindowInsetsState = info.topOpaqueWindowInsetsState; - if (attrs == null || mainWindowParams == null || topWindowInsetsState == null) { - Slog.w(TAG, "unable to create taskSnapshot surface for task: " + taskId); + + final WindowManager.LayoutParams layoutParams = SnapshotDrawerUtils.createLayoutParameters( + info, TITLE_FORMAT + taskId, TYPE_APPLICATION_STARTING, + snapshot.getHardwareBuffer().getFormat(), appToken); + if (layoutParams == null) { + Slog.e(TAG, "TaskSnapshotWindow no layoutParams"); return null; } - final WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams(); - - final int appearance = attrs.insetsFlags.appearance; - final int windowFlags = attrs.flags; - final int windowPrivateFlags = attrs.privateFlags; - - layoutParams.packageName = mainWindowParams.packageName; - layoutParams.windowAnimations = mainWindowParams.windowAnimations; - layoutParams.dimAmount = mainWindowParams.dimAmount; - layoutParams.type = TYPE_APPLICATION_STARTING; - layoutParams.format = snapshot.getHardwareBuffer().getFormat(); - layoutParams.flags = (windowFlags & ~FLAG_INHERIT_EXCLUDES) - | FLAG_NOT_FOCUSABLE - | FLAG_NOT_TOUCHABLE; - // Setting as trusted overlay to let touches pass through. This is safe because this - // window is controlled by the system. - layoutParams.privateFlags = (windowPrivateFlags & PRIVATE_FLAG_FORCE_DRAW_BAR_BACKGROUNDS) - | PRIVATE_FLAG_TRUSTED_OVERLAY | PRIVATE_FLAG_USE_BLAST; - layoutParams.token = appToken; - layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT; - layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT; - layoutParams.insetsFlags.appearance = appearance; - layoutParams.insetsFlags.behavior = attrs.insetsFlags.behavior; - layoutParams.layoutInDisplayCutoutMode = attrs.layoutInDisplayCutoutMode; - layoutParams.setFitInsetsTypes(attrs.getFitInsetsTypes()); - layoutParams.setFitInsetsSides(attrs.getFitInsetsSides()); - layoutParams.setFitInsetsIgnoringVisibility(attrs.isFitInsetsIgnoringVisibility()); - - layoutParams.setTitle(String.format(TITLE_FORMAT, taskId)); final Point taskSize = snapshot.getTaskSize(); final Rect taskBounds = new Rect(0, 0, taskSize.x, taskSize.y); final int orientation = snapshot.getOrientation(); - final int activityType = runningTaskInfo.topActivityType; final int displayId = runningTaskInfo.displayId; 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 InsetsSourceControl.Array tmpControls = new InsetsSourceControl.Array(); final MergedConfiguration tmpMergedConfiguration = new MergedConfiguration(); - final TaskDescription taskDescription; - if (runningTaskInfo.taskDescription != null) { - taskDescription = runningTaskInfo.taskDescription; - } else { - taskDescription = new TaskDescription(); - taskDescription.setBackgroundColor(WHITE); - } + final TaskDescription taskDescription = + SnapshotDrawerUtils.getOrCreateTaskDescription(runningTaskInfo); final TaskSnapshotWindow snapshotSurface = new TaskSnapshotWindow( - surfaceControl, snapshot, layoutParams.getTitle(), taskDescription, appearance, - windowFlags, windowPrivateFlags, taskBounds, orientation, activityType, - topWindowInsetsState, clearWindowHandler, splashScreenExecutor); + snapshot, taskDescription, orientation, + clearWindowHandler, splashScreenExecutor); final Window window = snapshotSurface.mWindow; 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.requestedVisibleTypes, 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,57 +130,34 @@ 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(); + Slog.w(TAG, "Failed to relayout snapshot starting window"); + return null; } - final Rect systemBarInsets = getSystemBarInsets(tmpFrames.frame, topWindowInsetsState); - snapshotSurface.setFrames(tmpFrames.frame, systemBarInsets); - snapshotSurface.drawSnapshot(); + SnapshotDrawerUtils.drawSnapshotOnSurface(info, layoutParams, surfaceControl, snapshot, + taskBounds, tmpFrames.frame, topWindowInsetsState, true /* releaseAfterDraw */); + snapshotSurface.mHasDrawn = true; + snapshotSurface.reportDrawn(); + return snapshotSurface; } - public TaskSnapshotWindow(SurfaceControl surfaceControl, - TaskSnapshot snapshot, CharSequence title, TaskDescription taskDescription, - int appearance, int windowFlags, int windowPrivateFlags, Rect taskBounds, - int currentOrientation, int activityType, InsetsState topWindowInsetsState, - Runnable clearWindowHandler, ShellExecutor splashScreenExecutor) { + public TaskSnapshotWindow(TaskSnapshot snapshot, TaskDescription taskDescription, + int currentOrientation, Runnable clearWindowHandler, + ShellExecutor splashScreenExecutor) { mSplashScreenExecutor = splashScreenExecutor; mSession = WindowManagerGlobal.getWindowSession(); mWindow = new Window(); mWindow.setSession(mSession); - mSurfaceControl = surfaceControl; - mSnapshot = snapshot; - mTitle = title; int backgroundColor = taskDescription.getBackgroundColor(); mBackgroundPaint.setColor(backgroundColor != 0 ? backgroundColor : WHITE); - mTaskBounds = taskBounds; - mSystemBarBackgroundPainter = new SystemBarBackgroundPainter(windowFlags, - windowPrivateFlags, appearance, taskDescription, 1f, topWindowInsetsState); - mStatusBarColor = taskDescription.getStatusBarColor(); mOrientationOnCreation = currentOrientation; - mActivityType = activityType; - mTransaction = new SurfaceControl.Transaction(); mClearWindowHandler = clearWindowHandler; mHasImeSurface = snapshot.hasImeSurface(); } @@ -313,40 +170,7 @@ public class TaskSnapshotWindow { return mHasImeSurface; } - /** - * Ask system bar background painter to draw status bar background. - * @hide - */ - public void drawStatusBarBackground(Canvas c, @Nullable Rect alreadyDrawnFrame) { - mSystemBarBackgroundPainter.drawStatusBarBackground(c, alreadyDrawnFrame, - mSystemBarBackgroundPainter.getStatusBarColorViewHeight()); - } - - /** - * Ask system bar background painter to draw navigation bar background. - * @hide - */ - public void drawNavigationBarBackground(Canvas c) { - mSystemBarBackgroundPainter.drawNavigationBarBackground(c); - } - - void scheduleRemove(boolean deferRemoveForIme) { - // Show the latest content as soon as possible for unlocking to home. - if (mActivityType == ACTIVITY_TYPE_HOME) { - removeImmediately(); - return; - } - mSplashScreenExecutor.removeCallbacks(mScheduledRunnable); - final long delayRemovalTime = mHasImeSurface && deferRemoveForIme - ? MAX_DELAY_REMOVAL_TIME_IME_VISIBLE - : DELAY_REMOVAL_TIME_GENERAL; - mSplashScreenExecutor.executeDelayed(mScheduledRunnable, delayRemovalTime); - ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW, - "Defer removing snapshot surface in %d", delayRemovalTime); - } - void removeImmediately() { - mSplashScreenExecutor.removeCallbacks(mScheduledRunnable); try { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW, "Removing taskSnapshot surface, mHasDrawn=%b", mHasDrawn); @@ -357,178 +181,6 @@ public class TaskSnapshotWindow { } /** - * Set frame size. - * @hide - */ - public void setFrames(Rect frame, Rect systemBarInsets) { - mFrame.set(frame); - mSystemBarInsets.set(systemBarInsets); - final HardwareBuffer snapshot = mSnapshot.getHardwareBuffer(); - mSizeMismatch = (mFrame.width() != snapshot.getWidth() - || mFrame.height() != snapshot.getHeight()); - mSystemBarBackgroundPainter.setInsets(systemBarInsets); - } - - static Rect getSystemBarInsets(Rect frame, InsetsState state) { - return state.calculateInsets(frame, WindowInsets.Type.systemBars(), - false /* ignoreVisibility */).toRect(); - } - - private void drawSnapshot() { - ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW, - "Drawing snapshot surface sizeMismatch=%b", mSizeMismatch); - if (mSizeMismatch) { - // The dimensions of the buffer and the window don't match, so attaching the buffer - // will fail. Better create a child window with the exact dimensions and fill the parent - // window with the background color! - drawSizeMismatchSnapshot(); - } else { - drawSizeMatchSnapshot(); - } - mHasDrawn = true; - reportDrawn(); - - // In case window manager leaks us, make sure we don't retain the snapshot. - if (mSnapshot.getHardwareBuffer() != null) { - mSnapshot.getHardwareBuffer().close(); - } - mSnapshot = null; - mSurfaceControl.release(); - } - - private void drawSizeMatchSnapshot() { - mTransaction.setBuffer(mSurfaceControl, mSnapshot.getHardwareBuffer()) - .setColorSpace(mSurfaceControl, mSnapshot.getColorSpace()) - .apply(); - } - - private void drawSizeMismatchSnapshot() { - final HardwareBuffer buffer = mSnapshot.getHardwareBuffer(); - final SurfaceSession session = new SurfaceSession(); - - // We consider nearly matched dimensions as there can be rounding errors and the user won't - // notice very minute differences from scaling one dimension more than the other - final boolean aspectRatioMismatch = Math.abs( - ((float) buffer.getWidth() / buffer.getHeight()) - - ((float) mFrame.width() / mFrame.height())) > 0.01f; - - // Keep a reference to it such that it doesn't get destroyed when finalized. - SurfaceControl childSurfaceControl = new SurfaceControl.Builder(session) - .setName(mTitle + " - task-snapshot-surface") - .setBLASTLayer() - .setFormat(buffer.getFormat()) - .setParent(mSurfaceControl) - .setCallsite("TaskSnapshotWindow.drawSizeMismatchSnapshot") - .build(); - - final Rect frame; - // We can just show the surface here as it will still be hidden as the parent is - // still hidden. - mTransaction.show(childSurfaceControl); - if (aspectRatioMismatch) { - // Clip off ugly navigation bar. - final Rect crop = calculateSnapshotCrop(); - frame = calculateSnapshotFrame(crop); - mTransaction.setWindowCrop(childSurfaceControl, crop); - mTransaction.setPosition(childSurfaceControl, frame.left, frame.top); - mTmpSnapshotSize.set(crop); - mTmpDstFrame.set(frame); - } else { - frame = null; - mTmpSnapshotSize.set(0, 0, buffer.getWidth(), buffer.getHeight()); - mTmpDstFrame.set(mFrame); - mTmpDstFrame.offsetTo(0, 0); - } - - // Scale the mismatch dimensions to fill the task bounds - mSnapshotMatrix.setRectToRect(mTmpSnapshotSize, mTmpDstFrame, Matrix.ScaleToFit.FILL); - mTransaction.setMatrix(childSurfaceControl, mSnapshotMatrix, mTmpFloat9); - mTransaction.setColorSpace(childSurfaceControl, mSnapshot.getColorSpace()); - mTransaction.setBuffer(childSurfaceControl, mSnapshot.getHardwareBuffer()); - - if (aspectRatioMismatch) { - GraphicBuffer background = GraphicBuffer.create(mFrame.width(), mFrame.height(), - PixelFormat.RGBA_8888, - GraphicBuffer.USAGE_HW_TEXTURE | GraphicBuffer.USAGE_HW_COMPOSER - | GraphicBuffer.USAGE_SW_WRITE_RARELY); - // TODO: Support this on HardwareBuffer - final Canvas c = background.lockCanvas(); - drawBackgroundAndBars(c, frame); - background.unlockCanvasAndPost(c); - mTransaction.setBuffer(mSurfaceControl, - HardwareBuffer.createFromGraphicBuffer(background)); - } - mTransaction.apply(); - childSurfaceControl.release(); - } - - /** - * Calculates the snapshot crop in snapshot coordinate space. - * - * @return crop rect in snapshot coordinate space. - */ - public Rect calculateSnapshotCrop() { - final Rect rect = new Rect(); - final HardwareBuffer snapshot = mSnapshot.getHardwareBuffer(); - rect.set(0, 0, snapshot.getWidth(), snapshot.getHeight()); - final Rect insets = mSnapshot.getContentInsets(); - - final float scaleX = (float) snapshot.getWidth() / mSnapshot.getTaskSize().x; - final float scaleY = (float) snapshot.getHeight() / mSnapshot.getTaskSize().y; - - // Let's remove all system decorations except the status bar, but only if the task is at the - // very top of the screen. - final boolean isTop = mTaskBounds.top == 0 && mFrame.top == 0; - rect.inset((int) (insets.left * scaleX), - isTop ? 0 : (int) (insets.top * scaleY), - (int) (insets.right * scaleX), - (int) (insets.bottom * scaleY)); - return rect; - } - - /** - * Calculates the snapshot frame in window coordinate space from crop. - * - * @param crop rect that is in snapshot coordinate space. - */ - public Rect calculateSnapshotFrame(Rect crop) { - final HardwareBuffer snapshot = mSnapshot.getHardwareBuffer(); - final float scaleX = (float) snapshot.getWidth() / mSnapshot.getTaskSize().x; - final float scaleY = (float) snapshot.getHeight() / mSnapshot.getTaskSize().y; - - // Rescale the frame from snapshot to window coordinate space - final Rect frame = new Rect(0, 0, - (int) (crop.width() / scaleX + 0.5f), - (int) (crop.height() / scaleY + 0.5f) - ); - - // However, we also need to make space for the navigation bar on the left side. - frame.offset(mSystemBarInsets.left, 0); - return frame; - } - - /** - * Draw status bar and navigation bar background. - * @hide - */ - public void drawBackgroundAndBars(Canvas c, Rect frame) { - final int statusBarHeight = mSystemBarBackgroundPainter.getStatusBarColorViewHeight(); - final boolean fillHorizontally = c.getWidth() > frame.right; - final boolean fillVertically = c.getHeight() > frame.bottom; - if (fillHorizontally) { - c.drawRect(frame.right, alpha(mStatusBarColor) == 0xFF ? statusBarHeight : 0, - c.getWidth(), fillVertically - ? frame.bottom - : c.getHeight(), - mBackgroundPaint); - } - if (fillVertically) { - c.drawRect(0, frame.bottom, c.getWidth(), c.getHeight(), mBackgroundPaint); - } - mSystemBarBackgroundPainter.drawDecors(c, frame); - } - - /** * Clear window from drawer, must be post on main executor. */ private void clearWindowSynced() { @@ -555,7 +207,7 @@ public class TaskSnapshotWindow { public void resized(ClientWindowFrames frames, boolean reportDraw, MergedConfiguration mergedConfiguration, InsetsState insetsState, boolean forceLayout, boolean alwaysConsumeSystemBars, int displayId, int seqId, - int resizeMode) { + boolean dragResizing) { final TaskSnapshotWindow snapshot = mOuter.get(); if (snapshot == null) { return; @@ -576,91 +228,4 @@ public class TaskSnapshotWindow { }); } } - - /** - * Helper class to draw the background of the system bars in regions the task snapshot isn't - * filling the window. - */ - static class SystemBarBackgroundPainter { - private final Paint mStatusBarPaint = new Paint(); - private final Paint mNavigationBarPaint = new Paint(); - private final int mStatusBarColor; - private final int mNavigationBarColor; - private final int mWindowFlags; - private final int mWindowPrivateFlags; - private final float mScale; - private final InsetsState mInsetsState; - private final Rect mSystemBarInsets = new Rect(); - - SystemBarBackgroundPainter(int windowFlags, int windowPrivateFlags, int appearance, - TaskDescription taskDescription, float scale, InsetsState insetsState) { - mWindowFlags = windowFlags; - mWindowPrivateFlags = windowPrivateFlags; - mScale = scale; - final Context context = ActivityThread.currentActivityThread().getSystemUiContext(); - final int semiTransparent = context.getColor( - R.color.system_bar_background_semi_transparent); - mStatusBarColor = DecorView.calculateBarColor(windowFlags, FLAG_TRANSLUCENT_STATUS, - semiTransparent, taskDescription.getStatusBarColor(), appearance, - APPEARANCE_LIGHT_STATUS_BARS, - taskDescription.getEnsureStatusBarContrastWhenTransparent()); - mNavigationBarColor = DecorView.calculateBarColor(windowFlags, - FLAG_TRANSLUCENT_NAVIGATION, semiTransparent, - taskDescription.getNavigationBarColor(), appearance, - APPEARANCE_LIGHT_NAVIGATION_BARS, - taskDescription.getEnsureNavigationBarContrastWhenTransparent() - && context.getResources().getBoolean(R.bool.config_navBarNeedsScrim)); - mStatusBarPaint.setColor(mStatusBarColor); - mNavigationBarPaint.setColor(mNavigationBarColor); - mInsetsState = insetsState; - } - - void setInsets(Rect systemBarInsets) { - mSystemBarInsets.set(systemBarInsets); - } - - int getStatusBarColorViewHeight() { - final boolean forceBarBackground = - (mWindowPrivateFlags & PRIVATE_FLAG_FORCE_DRAW_BAR_BACKGROUNDS) != 0; - if (STATUS_BAR_COLOR_VIEW_ATTRIBUTES.isVisible( - mInsetsState, mStatusBarColor, mWindowFlags, forceBarBackground)) { - return (int) (mSystemBarInsets.top * mScale); - } else { - return 0; - } - } - - private boolean isNavigationBarColorViewVisible() { - final boolean forceBarBackground = - (mWindowPrivateFlags & PRIVATE_FLAG_FORCE_DRAW_BAR_BACKGROUNDS) != 0; - return NAVIGATION_BAR_COLOR_VIEW_ATTRIBUTES.isVisible( - mInsetsState, mNavigationBarColor, mWindowFlags, forceBarBackground); - } - - void drawDecors(Canvas c, @Nullable Rect alreadyDrawnFrame) { - drawStatusBarBackground(c, alreadyDrawnFrame, getStatusBarColorViewHeight()); - drawNavigationBarBackground(c); - } - - void drawStatusBarBackground(Canvas c, @Nullable Rect alreadyDrawnFrame, - int statusBarHeight) { - if (statusBarHeight > 0 && Color.alpha(mStatusBarColor) != 0 - && (alreadyDrawnFrame == null || c.getWidth() > alreadyDrawnFrame.right)) { - final int rightInset = (int) (mSystemBarInsets.right * mScale); - final int left = alreadyDrawnFrame != null ? alreadyDrawnFrame.right : 0; - c.drawRect(left, 0, c.getWidth() - rightInset, statusBarHeight, mStatusBarPaint); - } - } - - @VisibleForTesting - void drawNavigationBarBackground(Canvas c) { - final Rect navigationBarRect = new Rect(); - getNavigationBarRect(c.getWidth(), c.getHeight(), mSystemBarInsets, navigationBarRect, - mScale); - final boolean visible = isNavigationBarColorViewVisible(); - if (visible && Color.alpha(mNavigationBarColor) != 0 && !navigationBarRect.isEmpty()) { - c.drawRect(navigationBarRect, mNavigationBarPaint); - } - } - } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/WindowlessSnapshotWindowCreator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/WindowlessSnapshotWindowCreator.java new file mode 100644 index 000000000000..144547885501 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/WindowlessSnapshotWindowCreator.java @@ -0,0 +1,165 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.startingsurface; + +import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; +import android.app.ActivityManager; +import android.content.Context; +import android.graphics.Point; +import android.graphics.Rect; +import android.hardware.display.DisplayManager; +import android.view.Display; +import android.view.InsetsState; +import android.view.SurfaceControl; +import android.view.SurfaceControlViewHost; +import android.view.WindowManager; +import android.widget.FrameLayout; +import android.window.SnapshotDrawerUtils; +import android.window.StartingWindowInfo; +import android.window.TaskSnapshot; + +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.TransactionPool; + +class WindowlessSnapshotWindowCreator { + private static final int DEFAULT_FADEOUT_DURATION = 233; + private final StartingSurfaceDrawer.StartingWindowRecordManager + mStartingWindowRecordManager; + private final DisplayManager mDisplayManager; + private final Context mContext; + private final SplashscreenContentDrawer mSplashscreenContentDrawer; + private final TransactionPool mTransactionPool; + + WindowlessSnapshotWindowCreator( + StartingSurfaceDrawer.StartingWindowRecordManager startingWindowRecordManager, + Context context, + DisplayManager displayManager, SplashscreenContentDrawer splashscreenContentDrawer, + TransactionPool transactionPool) { + mStartingWindowRecordManager = startingWindowRecordManager; + mContext = context; + mDisplayManager = displayManager; + mSplashscreenContentDrawer = splashscreenContentDrawer; + mTransactionPool = transactionPool; + } + + void makeTaskSnapshotWindow(StartingWindowInfo info, SurfaceControl rootSurface, + TaskSnapshot snapshot, ShellExecutor removeExecutor) { + final ActivityManager.RunningTaskInfo runningTaskInfo = info.taskInfo; + final int taskId = runningTaskInfo.taskId; + final String title = "Windowless Snapshot " + taskId; + final WindowManager.LayoutParams lp = SnapshotDrawerUtils.createLayoutParameters( + info, title, TYPE_APPLICATION_OVERLAY, snapshot.getHardwareBuffer().getFormat(), + null /* token */); + if (lp == null) { + return; + } + final Display display = mDisplayManager.getDisplay(runningTaskInfo.displayId); + final StartingSurfaceDrawer.WindowlessStartingWindow wlw = + new StartingSurfaceDrawer.WindowlessStartingWindow( + runningTaskInfo.configuration, rootSurface); + final SurfaceControlViewHost mViewHost = new SurfaceControlViewHost( + mContext, display, wlw, "WindowlessSnapshotWindowCreator"); + final Point taskSize = snapshot.getTaskSize(); + final Rect snapshotBounds = new Rect(0, 0, taskSize.x, taskSize.y); + final Rect windowBounds = runningTaskInfo.configuration.windowConfiguration.getBounds(); + final InsetsState topWindowInsetsState = info.topOpaqueWindowInsetsState; + final FrameLayout rootLayout = new FrameLayout( + mSplashscreenContentDrawer.createViewContextWrapper(mContext)); + mViewHost.setView(rootLayout, lp); + SnapshotDrawerUtils.drawSnapshotOnSurface(info, lp, wlw.mChildSurface, snapshot, + snapshotBounds, windowBounds, topWindowInsetsState, false /* releaseAfterDraw */); + + final ActivityManager.TaskDescription taskDescription = + SnapshotDrawerUtils.getOrCreateTaskDescription(runningTaskInfo); + + final SnapshotWindowRecord record = new SnapshotWindowRecord(mViewHost, wlw.mChildSurface, + taskDescription.getBackgroundColor(), snapshot.hasImeSurface(), + runningTaskInfo.topActivityType, removeExecutor); + mStartingWindowRecordManager.addRecord(taskId, record); + info.notifyAddComplete(wlw.mChildSurface); + } + + private class SnapshotWindowRecord extends StartingSurfaceDrawer.SnapshotRecord { + private SurfaceControlViewHost mViewHost; + private SurfaceControl mChildSurface; + private final boolean mHasImeSurface; + + SnapshotWindowRecord(SurfaceControlViewHost viewHost, SurfaceControl childSurface, + int bgColor, boolean hasImeSurface, int activityType, + ShellExecutor removeExecutor) { + super(activityType, removeExecutor); + mViewHost = viewHost; + mChildSurface = childSurface; + mBGColor = bgColor; + mHasImeSurface = hasImeSurface; + } + + @Override + protected void removeImmediately() { + super.removeImmediately(); + fadeoutThenRelease(); + } + + void fadeoutThenRelease() { + final ValueAnimator fadeOutAnimator = ValueAnimator.ofFloat(1f, 0f); + fadeOutAnimator.setDuration(DEFAULT_FADEOUT_DURATION); + final SurfaceControl.Transaction t = mTransactionPool.acquire(); + fadeOutAnimator.addUpdateListener(animation -> { + if (mChildSurface == null || !mChildSurface.isValid()) { + fadeOutAnimator.cancel(); + return; + } + t.setAlpha(mChildSurface, (float) animation.getAnimatedValue()); + t.apply(); + }); + + fadeOutAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + if (mChildSurface == null || !mChildSurface.isValid()) { + fadeOutAnimator.cancel(); + } + } + + @Override + public void onAnimationEnd(Animator animation) { + mTransactionPool.release(t); + if (mChildSurface != null) { + final SurfaceControl.Transaction t = mTransactionPool.acquire(); + t.remove(mChildSurface).apply(); + mTransactionPool.release(t); + mChildSurface = null; + } + if (mViewHost != null) { + mViewHost.release(); + mViewHost = null; + } + } + }); + fadeOutAnimator.start(); + } + + @Override + protected boolean hasImeSurface() { + return mHasImeSurface; + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/WindowlessSplashWindowCreator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/WindowlessSplashWindowCreator.java new file mode 100644 index 000000000000..12a0d4054b4d --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/WindowlessSplashWindowCreator.java @@ -0,0 +1,150 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.startingsurface; + +import static android.graphics.Color.WHITE; +import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_SPLASH_SCREEN; + +import android.app.ActivityManager; +import android.content.Context; +import android.content.pm.ActivityInfo; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.hardware.display.DisplayManager; +import android.os.Binder; +import android.os.SystemClock; +import android.view.Display; +import android.view.SurfaceControl; +import android.view.SurfaceControlViewHost; +import android.view.WindowManager; +import android.widget.FrameLayout; +import android.window.SplashScreenView; +import android.window.StartingWindowInfo; +import android.window.StartingWindowRemovalInfo; + +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.TransactionPool; + +class WindowlessSplashWindowCreator extends AbsSplashWindowCreator { + + private final TransactionPool mTransactionPool; + + WindowlessSplashWindowCreator(SplashscreenContentDrawer contentDrawer, + Context context, + ShellExecutor splashScreenExecutor, + DisplayManager displayManager, + StartingSurfaceDrawer.StartingWindowRecordManager startingWindowRecordManager, + TransactionPool pool) { + super(contentDrawer, context, splashScreenExecutor, displayManager, + startingWindowRecordManager); + mTransactionPool = pool; + } + + void addSplashScreenStartingWindow(StartingWindowInfo windowInfo, SurfaceControl rootSurface) { + final ActivityManager.RunningTaskInfo taskInfo = windowInfo.taskInfo; + final ActivityInfo activityInfo = windowInfo.targetActivityInfo != null + ? windowInfo.targetActivityInfo + : taskInfo.topActivityInfo; + if (activityInfo == null || activityInfo.packageName == null) { + return; + } + + final int displayId = taskInfo.displayId; + final Display display = mDisplayManager.getDisplay(displayId); + if (display == null) { + // Can't show splash screen on requested display, so skip showing at all. + return; + } + final Context myContext = SplashscreenContentDrawer.createContext(mContext, windowInfo, + 0 /* theme */, STARTING_WINDOW_TYPE_SPLASH_SCREEN, mDisplayManager); + if (myContext == null) { + return; + } + final StartingSurfaceDrawer.WindowlessStartingWindow wlw = + new StartingSurfaceDrawer.WindowlessStartingWindow( + taskInfo.configuration, rootSurface); + final SurfaceControlViewHost viewHost = new SurfaceControlViewHost( + myContext, display, wlw, "WindowlessSplashWindowCreator"); + final String title = "Windowless Splash " + taskInfo.taskId; + final WindowManager.LayoutParams lp = SplashscreenContentDrawer.createLayoutParameters( + myContext, windowInfo, STARTING_WINDOW_TYPE_SPLASH_SCREEN, title, + PixelFormat.TRANSLUCENT, new Binder()); + final Rect windowBounds = taskInfo.configuration.windowConfiguration.getBounds(); + lp.width = windowBounds.width(); + lp.height = windowBounds.height(); + final ActivityManager.TaskDescription taskDescription; + if (taskInfo.taskDescription != null) { + taskDescription = taskInfo.taskDescription; + } else { + taskDescription = new ActivityManager.TaskDescription(); + taskDescription.setBackgroundColor(WHITE); + } + + final FrameLayout rootLayout = new FrameLayout( + mSplashscreenContentDrawer.createViewContextWrapper(mContext)); + viewHost.setView(rootLayout, lp); + + final int bgColor = taskDescription.getBackgroundColor(); + final SplashScreenView splashScreenView = mSplashscreenContentDrawer + .makeSimpleSplashScreenContentView(myContext, windowInfo, bgColor); + rootLayout.addView(splashScreenView); + final SplashWindowRecord record = new SplashWindowRecord(viewHost, splashScreenView, + wlw.mChildSurface, bgColor); + mStartingWindowRecordManager.addRecord(taskInfo.taskId, record); + windowInfo.notifyAddComplete(wlw.mChildSurface); + } + + private class SplashWindowRecord extends StartingSurfaceDrawer.StartingWindowRecord { + private SurfaceControlViewHost mViewHost; + private final long mCreateTime; + private SurfaceControl mChildSurface; + private final SplashScreenView mSplashView; + + SplashWindowRecord(SurfaceControlViewHost viewHost, SplashScreenView splashView, + SurfaceControl childSurface, int bgColor) { + mViewHost = viewHost; + mSplashView = splashView; + mChildSurface = childSurface; + mBGColor = bgColor; + mCreateTime = SystemClock.uptimeMillis(); + } + + @Override + public void removeIfPossible(StartingWindowRemovalInfo info, boolean immediately) { + if (!immediately) { + mSplashscreenContentDrawer.applyExitAnimation(mSplashView, + info.windowAnimationLeash, info.mainFrame, + this::release, mCreateTime, 0 /* roundedCornerRadius */); + } else { + release(); + } + } + + void release() { + if (mChildSurface != null) { + final SurfaceControl.Transaction t = mTransactionPool.acquire(); + t.remove(mChildSurface).apply(); + mTransactionPool.release(t); + mChildSurface = null; + } + if (mViewHost != null) { + mViewHost.release(); + mViewHost = null; + } + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/phone/PhoneStartingWindowTypeAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/phone/PhoneStartingWindowTypeAlgorithm.java index bb43d7c1a090..72fc8686f648 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/phone/PhoneStartingWindowTypeAlgorithm.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/phone/PhoneStartingWindowTypeAlgorithm.java @@ -22,6 +22,7 @@ import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_NONE; import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_SNAPSHOT; import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_SOLID_COLOR_SPLASH_SCREEN; import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_SPLASH_SCREEN; +import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_WINDOWLESS; import static android.window.StartingWindowInfo.TYPE_PARAMETER_ACTIVITY_CREATED; import static android.window.StartingWindowInfo.TYPE_PARAMETER_ACTIVITY_DRAWN; import static android.window.StartingWindowInfo.TYPE_PARAMETER_ALLOW_TASK_SNAPSHOT; @@ -30,6 +31,7 @@ import static android.window.StartingWindowInfo.TYPE_PARAMETER_NEW_TASK; import static android.window.StartingWindowInfo.TYPE_PARAMETER_PROCESS_RUNNING; import static android.window.StartingWindowInfo.TYPE_PARAMETER_TASK_SWITCH; import static android.window.StartingWindowInfo.TYPE_PARAMETER_USE_SOLID_COLOR_SPLASH_SCREEN; +import static android.window.StartingWindowInfo.TYPE_PARAMETER_WINDOWLESS; import android.window.StartingWindowInfo; @@ -55,6 +57,7 @@ public class PhoneStartingWindowTypeAlgorithm implements StartingWindowTypeAlgor final boolean legacySplashScreen = ((parameter & TYPE_PARAMETER_LEGACY_SPLASH_SCREEN) != 0); final boolean activityDrawn = (parameter & TYPE_PARAMETER_ACTIVITY_DRAWN) != 0; + final boolean windowlessSurface = (parameter & TYPE_PARAMETER_WINDOWLESS) != 0; final boolean topIsHome = windowInfo.taskInfo.topActivityType == ACTIVITY_TYPE_HOME; ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW, @@ -67,10 +70,15 @@ public class PhoneStartingWindowTypeAlgorithm implements StartingWindowTypeAlgor + "isSolidColorSplashScreen=%b, " + "legacySplashScreen=%b, " + "activityDrawn=%b, " + + "windowless=%b, " + "topIsHome=%b", newTask, taskSwitch, processRunning, allowTaskSnapshot, activityCreated, - isSolidColorSplashScreen, legacySplashScreen, activityDrawn, topIsHome); + isSolidColorSplashScreen, legacySplashScreen, activityDrawn, windowlessSurface, + topIsHome); + if (windowlessSurface) { + return STARTING_WINDOW_TYPE_WINDOWLESS; + } if (!topIsHome) { if (!processRunning || newTask || (taskSwitch && !activityCreated)) { return getSplashscreenType(isSolidColorSplashScreen, legacySplashScreen); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/tv/TvStartingWindowTypeAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/tv/TvStartingWindowTypeAlgorithm.java index 74fe8fbbd5e0..04ae2f960102 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/tv/TvStartingWindowTypeAlgorithm.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/tv/TvStartingWindowTypeAlgorithm.java @@ -16,7 +16,7 @@ package com.android.wm.shell.startingsurface.tv; -import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_SOLID_COLOR_SPLASH_SCREEN; +import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_NONE; import android.window.StartingWindowInfo; @@ -24,12 +24,12 @@ import com.android.wm.shell.startingsurface.StartingWindowTypeAlgorithm; /** * Algorithm for determining the type of a new starting window on Android TV. - * For now we always show empty splash screens on Android TV. + * For now we do not want to show any splash screens on Android TV. */ public class TvStartingWindowTypeAlgorithm implements StartingWindowTypeAlgorithm { @Override public int getSuggestedWindowType(StartingWindowInfo windowInfo) { - // For now we want to always show empty splash screens on TV. - return STARTING_WINDOW_TYPE_SOLID_COLOR_SPLASH_SCREEN; + // For now we do not want to show any splash screens on TV. + return STARTING_WINDOW_TYPE_NONE; } } 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..3f944cb6d628 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellController.java @@ -0,0 +1,353 @@ +/* + * 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_INIT; +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 android.os.Bundle; +import android.util.ArrayMap; + +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; + +import com.android.internal.protolog.common.ProtoLog; +import com.android.wm.shell.common.ExternalInterfaceBinder; +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; +import java.util.function.Supplier; + +/** + * 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 ArrayMap<String, Supplier<ExternalInterfaceBinder>> mExternalInterfaceSuppliers = + new ArrayMap<>(); + // References to the existing interfaces, to be invalidated when they are recreated + private ArrayMap<String, ExternalInterfaceBinder> mExternalInterfaces = new ArrayMap<>(); + + private Configuration mLastConfiguration; + + + public ShellController(ShellInit shellInit, ShellCommandHandler shellCommandHandler, + ShellExecutor mainExecutor) { + mShellInit = shellInit; + mShellCommandHandler = shellCommandHandler; + mMainExecutor = mainExecutor; + shellInit.addInitCallback(this::onInit, this); + } + + private void onInit() { + mShellCommandHandler.addDumpCallback(this::dump, this); + } + + /** + * 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); + } + + /** + * Adds an interface that can be called from a remote process. This method takes a supplier + * because each binder reference is valid for a single process, and in multi-user mode, SysUI + * will request new binder instances for each instance of Launcher that it provides binders + * to. + * + * @param extra the key for the interface, {@see ShellSharedConstants} + * @param binderSupplier the supplier of the binder to pass to the external process + * @param callerInstance the instance of the caller, purely for logging + */ + public void addExternalInterface(String extra, Supplier<ExternalInterfaceBinder> binderSupplier, + Object callerInstance) { + ProtoLog.v(WM_SHELL_INIT, "Adding external interface from %s with key %s", + callerInstance.getClass().getSimpleName(), extra); + if (mExternalInterfaceSuppliers.containsKey(extra)) { + throw new IllegalArgumentException("Supplier with same key already exists: " + + extra); + } + mExternalInterfaceSuppliers.put(extra, binderSupplier); + } + + /** + * Updates the given bundle with the set of external interfaces, invalidating the old set of + * binders. + */ + @VisibleForTesting + public void createExternalInterfaces(Bundle output) { + // Invalidate the old binders + for (int i = 0; i < mExternalInterfaces.size(); i++) { + mExternalInterfaces.valueAt(i).invalidate(); + } + mExternalInterfaces.clear(); + + // Create new binders for each key + for (int i = 0; i < mExternalInterfaceSuppliers.size(); i++) { + final String key = mExternalInterfaceSuppliers.keyAt(i); + final ExternalInterfaceBinder b = mExternalInterfaceSuppliers.valueAt(i).get(); + mExternalInterfaces.put(key, b); + output.putBinder(key, b.asBinder()); + } + } + + @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()); + + if (!mExternalInterfaces.isEmpty()) { + pw.println(innerPrefix + "mExternalInterfaces={"); + for (String key : mExternalInterfaces.keySet()) { + pw.println(innerPrefix + "\t" + key + ": " + mExternalInterfaces.get(key)); + } + pw.println(innerPrefix + "}"); + } + } + + /** + * 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 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)); + } + + @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 createExternalInterfaces(Bundle bundle) { + try { + mMainExecutor.executeBlocking(() -> { + ShellController.this.createExternalInterfaces(bundle); + }); + } catch (InterruptedException e) { + throw new RuntimeException("Failed to get Shell command 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); + } + } + } +} 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..2e2f569a52b8 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellInit.java @@ -0,0 +1,91 @@ +/* + * 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 android.view.SurfaceControl; + +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()); + SurfaceControl.setDebugUsageAfterRelease(true); + // 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..bc5dd11ef54e --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellInterface.java @@ -0,0 +1,83 @@ +/* + * 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 android.os.Bundle; + +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() {} + + /** + * 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) {} + + /** + * Handles a shell command. + */ + default boolean handleCommand(final String[] args, PrintWriter pw) { + return false; + } + + /** + * Updates the given {@param bundle} with the set of exposed interfaces. + */ + default void createExternalInterfaces(Bundle bundle) {} + + /** + * Dumps the shell state. + */ + default void dump(PrintWriter pw) {} +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellSharedConstants.java b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellSharedConstants.java new file mode 100644 index 000000000000..bfa63909cd47 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellSharedConstants.java @@ -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 com.android.wm.shell.sysui; + +/** + * General shell-related constants that are shared with users of the library. + */ +public class ShellSharedConstants { + // See IPip.aidl + public static final String KEY_EXTRA_SHELL_PIP = "extra_shell_pip"; + // See IBubbles.aidl + public static final String KEY_EXTRA_SHELL_BUBBLES = "extra_shell_bubbles"; + // See ISplitScreen.aidl + public static final String KEY_EXTRA_SHELL_SPLIT_SCREEN = "extra_shell_split_screen"; + // See IOneHanded.aidl + public static final String KEY_EXTRA_SHELL_ONE_HANDED = "extra_shell_one_handed"; + // See IShellTransitions.aidl + public static final String KEY_EXTRA_SHELL_SHELL_TRANSITIONS = + "extra_shell_shell_transitions"; + // See IStartingWindow.aidl + public static final String KEY_EXTRA_SHELL_STARTING_WINDOW = + "extra_shell_starting_window"; + // See IRecentTasks.aidl + public static final String KEY_EXTRA_SHELL_RECENT_TASKS = "extra_shell_recent_tasks"; + // See IBackAnimation.aidl + public static final String KEY_EXTRA_SHELL_BACK_ANIMATION = "extra_shell_back_animation"; + // See IFloatingTasks.aidl + public static final String KEY_EXTRA_SHELL_FLOATING_TASKS = "extra_shell_floating_tasks"; + // See IDesktopMode.aidl + public static final String KEY_EXTRA_SHELL_DESKTOP_MODE = "extra_shell_desktop_mode"; +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellCommandHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/UserChangeListener.java index 73fd6931066d..3d0909f6128d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellCommandHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/UserChangeListener.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,26 +14,26 @@ * limitations under the License. */ -package com.android.wm.shell; +package com.android.wm.shell.sysui; -import com.android.wm.shell.common.annotations.ExternalThread; +import android.content.Context; +import android.content.pm.UserInfo; -import java.io.PrintWriter; +import androidx.annotation.NonNull; + +import java.util.List; /** - * 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 ...}. + * Callbacks for when the user or user's profiles changes. */ -@ExternalThread -public interface ShellCommandHandler { +public interface UserChangeListener { /** - * Dumps the shell state. + * Called when the current (parent) user changes. */ - void dump(PrintWriter pw); + default void onUserChanged(int newUserId, @NonNull Context userContext) {} /** - * Handles a shell command. + * Called when a profile belonging to the user changes. */ - boolean handleCommand(final String[] args, PrintWriter pw); + 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/taskview/TaskView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskView.java new file mode 100644 index 000000000000..e4d8c32eb5c8 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskView.java @@ -0,0 +1,250 @@ +/* + * 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.taskview; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.ActivityManager; +import android.app.ActivityOptions; +import android.app.PendingIntent; +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.graphics.Region; +import android.view.SurfaceControl; +import android.view.SurfaceHolder; +import android.view.SurfaceView; +import android.view.View; +import android.view.ViewTreeObserver; + +import java.util.concurrent.Executor; + +/** + * A {@link SurfaceView} that can display a task. This is a concrete implementation for + * {@link TaskViewBase} which interacts {@link TaskViewTaskController}. + */ +public class TaskView extends SurfaceView implements SurfaceHolder.Callback, + ViewTreeObserver.OnComputeInternalInsetsListener, TaskViewBase { + /** Callback for listening task state. */ + public interface Listener { + /** + * 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. */ + default void onReleased() {} + + /** Called when a task is created inside the container. */ + default void onTaskCreated(int taskId, ComponentName name) {} + + /** Called when a task visibility changes. */ + default void onTaskVisibilityChanged(int taskId, boolean visible) {} + + /** Called when a task is about to be removed from the stack inside the container. */ + default void onTaskRemovalStarted(int taskId) {} + + /** Called when a task is created inside the container. */ + default void onBackPressedOnTaskRoot(int taskId) {} + } + + private final Rect mTmpRect = new Rect(); + private final Rect mTmpRootRect = new Rect(); + private final int[] mTmpLocation = new int[2]; + private final TaskViewTaskController mTaskViewTaskController; + private Region mObscuredTouchRegion; + + public TaskView(Context context, TaskViewTaskController taskViewTaskController) { + super(context, null, 0, 0, true /* disableBackgroundLayer */); + mTaskViewTaskController = taskViewTaskController; + // TODO(b/266736992): Think about a better way to set the TaskViewBase on the + // TaskViewTaskController and vice-versa + mTaskViewTaskController.setTaskViewBase(this); + getHolder().addCallback(this); + } + + /** + * Launch a new activity. + * + * @param pendingIntent Intent used to launch an activity. + * @param fillInIntent Additional Intent data, see {@link Intent#fillIn Intent.fillIn()} + * @param options options for the activity. + * @param launchBounds the bounds (window size and position) that the activity should be + * launched in, in pixels and in screen coordinates. + */ + public void startActivity(@NonNull PendingIntent pendingIntent, @Nullable Intent fillInIntent, + @NonNull ActivityOptions options, @Nullable Rect launchBounds) { + mTaskViewTaskController.startActivity(pendingIntent, fillInIntent, options, launchBounds); + } + + /** + * Launch an activity represented by {@link ShortcutInfo}. + * <p>The owner of this container must be allowed to access the shortcut information, + * as defined in {@link LauncherApps#hasShortcutHostPermission()} to use this method. + * + * @param shortcut the shortcut used to launch the activity. + * @param options options for the activity. + * @param launchBounds the bounds (window size and position) that the activity should be + * launched in, in pixels and in screen coordinates. + */ + public void startShortcutActivity(@NonNull ShortcutInfo shortcut, + @NonNull ActivityOptions options, @Nullable Rect launchBounds) { + mTaskViewTaskController.startShortcutActivity(shortcut, options, launchBounds); + } + + @Override + public void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash) { + onLocationChanged(); + if (taskInfo.taskDescription != null) { + setResizeBackgroundColor(taskInfo.taskDescription.getBackgroundColor()); + } + } + + @Override + public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) { + if (taskInfo.taskDescription != null) { + setResizeBackgroundColor(taskInfo.taskDescription.getBackgroundColor()); + } + } + + /** + * @return {@code True} when the TaskView's surface has been created, {@code False} otherwise. + */ + public boolean isInitialized() { + return mTaskViewTaskController.isInitialized(); + } + + @Override + public Rect getCurrentBoundsOnScreen() { + getBoundsOnScreen(mTmpRect); + return mTmpRect; + } + + @Override + public void setResizeBgColor(SurfaceControl.Transaction t, int bgColor) { + setResizeBackgroundColor(t, bgColor); + } + + /** + * Only one listener may be set on the view, throws an exception otherwise. + */ + public void setListener(@NonNull Executor executor, TaskView.Listener listener) { + mTaskViewTaskController.setListener(executor, listener); + } + + /** + * Indicates a region of the view that is not touchable. + * + * @param obscuredRect the obscured region of the view. + */ + public void setObscuredTouchRect(Rect obscuredRect) { + mObscuredTouchRegion = obscuredRect != null ? new Region(obscuredRect) : null; + } + + /** + * Indicates a region of the view that is not touchable. + * + * @param obscuredRegion the obscured region of the view. + */ + public void setObscuredTouchRegion(Region obscuredRegion) { + mObscuredTouchRegion = obscuredRegion; + } + + /** + * Call when view position or size has changed. Do not call when animating. + */ + public void onLocationChanged() { + getBoundsOnScreen(mTmpRect); + mTaskViewTaskController.setWindowBounds(mTmpRect); + } + + /** + * Release this container if it is initialized. + */ + public void release() { + getHolder().removeCallback(this); + mTaskViewTaskController.release(); + } + + @Override + public String toString() { + return mTaskViewTaskController.toString(); + } + + @Override + public void surfaceCreated(SurfaceHolder holder) { + mTaskViewTaskController.surfaceCreated(getSurfaceControl()); + } + + @Override + public void surfaceChanged(@androidx.annotation.NonNull SurfaceHolder holder, int format, + int width, int height) { + getBoundsOnScreen(mTmpRect); + mTaskViewTaskController.setWindowBounds(mTmpRect); + } + + @Override + public void surfaceDestroyed(SurfaceHolder holder) { + mTaskViewTaskController.surfaceDestroyed(); + } + + @Override + public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo) { + // TODO(b/176854108): Consider to move the logic into gatherTransparentRegions since this + // is dependent on the order of listener. + // If there are multiple TaskViews, we'll set the touchable area as the root-view, then + // subtract each TaskView from it. + if (inoutInfo.touchableRegion.isEmpty()) { + inoutInfo.setTouchableInsets( + ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION); + View root = getRootView(); + root.getLocationInWindow(mTmpLocation); + mTmpRootRect.set(mTmpLocation[0], mTmpLocation[1], root.getWidth(), root.getHeight()); + inoutInfo.touchableRegion.set(mTmpRootRect); + } + getLocationInWindow(mTmpLocation); + mTmpRect.set(mTmpLocation[0], mTmpLocation[1], + mTmpLocation[0] + getWidth(), mTmpLocation[1] + getHeight()); + inoutInfo.touchableRegion.op(mTmpRect, Region.Op.DIFFERENCE); + + if (mObscuredTouchRegion != null) { + inoutInfo.touchableRegion.op(mObscuredTouchRegion, Region.Op.UNION); + } + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + getViewTreeObserver().addOnComputeInternalInsetsListener(this); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + getViewTreeObserver().removeOnComputeInternalInsetsListener(this); + } + + /** Returns the task info for the task in the TaskView. */ + @Nullable + public ActivityManager.RunningTaskInfo getTaskInfo() { + return mTaskViewTaskController.getTaskInfo(); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewBase.java b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewBase.java new file mode 100644 index 000000000000..5fdb60d2d342 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewBase.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.taskview; + +import android.app.ActivityManager; +import android.graphics.Rect; +import android.view.SurfaceControl; + +/** + * A stub for SurfaceView used by {@link TaskViewTaskController} + */ +public interface TaskViewBase { + /** + * Returns the current bounds on screen for the task view. + * @return + */ + // TODO(b/266242294): Remove getBoundsOnScreen() and instead send the bounds from the TaskView + // to TaskViewTaskController. + Rect getCurrentBoundsOnScreen(); + + /** + * This method should set the resize background color on the SurfaceView that is exposed to + * clients. + * See {@link android.view.SurfaceView#setResizeBackgroundColor(SurfaceControl.Transaction, + * int)} + */ + void setResizeBgColor(SurfaceControl.Transaction transaction, int color); + + /** + * Called when a task appears on the TaskView. See + * {@link TaskViewTaskController#onTaskAppeared(ActivityManager.RunningTaskInfo, + * SurfaceControl)} for details. + */ + default void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash) { + } + + /** + * Called when a task is vanished from the TaskView. See + * {@link TaskViewTaskController#onTaskVanished(ActivityManager.RunningTaskInfo)} for details. + */ + default void onTaskVanished(ActivityManager.RunningTaskInfo taskInfo) { + } + + /** + * Called when the task in the TaskView is changed. See + * {@link TaskViewTaskController#onTaskInfoChanged(ActivityManager.RunningTaskInfo)} for details. + */ + default void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) { + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/TaskViewFactory.java b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewFactory.java index a29e7a085a21..a7e4b0119480 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/TaskViewFactory.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewFactory.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell; +package com.android.wm.shell.taskview; import android.annotation.UiContext; import android.content.Context; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/TaskViewFactoryController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewFactoryController.java index 42844b57b92a..7eed5883043d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/TaskViewFactoryController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewFactoryController.java @@ -14,11 +14,12 @@ * limitations under the License. */ -package com.android.wm.shell; +package com.android.wm.shell.taskview; import android.annotation.UiContext; import android.content.Context; +import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.common.annotations.ExternalThread; @@ -51,13 +52,17 @@ public class TaskViewFactoryController { mTaskViewTransitions = null; } + /** + * @return the underlying {@link TaskViewFactory}. + */ public TaskViewFactory asTaskViewFactory() { return mImpl; } /** Creates an {@link TaskView} */ public void create(@UiContext Context context, Executor executor, Consumer<TaskView> onCreate) { - TaskView taskView = new TaskView(context, mTaskOrganizer, mTaskViewTransitions, mSyncQueue); + TaskView taskView = new TaskView(context, new TaskViewTaskController(context, + mTaskOrganizer, mTaskViewTransitions, mSyncQueue)); executor.execute(() -> { onCreate.accept(taskView); }); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/TaskView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTaskController.java index d28a68a42b2b..36c9077a197b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/TaskView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTaskController.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell; +package com.android.wm.shell.taskview; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; @@ -29,49 +29,25 @@ import android.content.Intent; import android.content.pm.LauncherApps; import android.content.pm.ShortcutInfo; import android.graphics.Rect; -import android.graphics.Region; import android.os.Binder; import android.util.CloseGuard; import android.view.SurfaceControl; -import android.view.SurfaceHolder; -import android.view.SurfaceView; -import android.view.View; -import android.view.ViewTreeObserver; import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; +import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.SyncTransactionQueue; -import com.android.wm.shell.transition.Transitions; import java.io.PrintWriter; import java.util.concurrent.Executor; /** - * View that can display a task. + * This class implements the core logic to show a task on the {@link TaskView}. All the {@link + * TaskView} to {@link TaskViewTaskController} interactions are done via direct method calls. + * + * The reverse communication is done via the {@link TaskViewBase} interface. */ -public class TaskView extends SurfaceView implements SurfaceHolder.Callback, - ShellTaskOrganizer.TaskListener, ViewTreeObserver.OnComputeInternalInsetsListener { - - /** Callback for listening task state. */ - public interface Listener { - /** Called when the container is ready for launching activities. */ - default void onInitialized() {} - - /** Called when the container can no longer launch activities. */ - default void onReleased() {} - - /** Called when a task is created inside the container. */ - default void onTaskCreated(int taskId, ComponentName name) {} - - /** Called when a task visibility changes. */ - default void onTaskVisibilityChanged(int taskId, boolean visible) {} - - /** Called when a task is about to be removed from the stack inside the container. */ - default void onTaskRemovalStarted(int taskId) {} - - /** Called when a task is created inside the container. */ - default void onBackPressedOnTaskRoot(int taskId) {} - } +public class TaskViewTaskController implements ShellTaskOrganizer.TaskListener { private final CloseGuard mGuard = new CloseGuard(); @@ -79,25 +55,23 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback, private final Executor mShellExecutor; private final SyncTransactionQueue mSyncQueue; private final TaskViewTransitions mTaskViewTransitions; + private TaskViewBase mTaskViewBase; + private final Context mContext; - 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 SurfaceControl mSurfaceControl; private boolean mIsInitialized; - private Listener mListener; + private boolean mNotifiedForInitialized; + private TaskView.Listener mListener; private Executor mListenerExecutor; - private Region mObscuredTouchRegion; - - private final Rect mTmpRect = new Rect(); - private final Rect mTmpRootRect = new Rect(); - private final int[] mTmpLocation = new int[2]; - public TaskView(Context context, ShellTaskOrganizer organizer, + public TaskViewTaskController(Context context, ShellTaskOrganizer organizer, TaskViewTransitions taskViewTransitions, SyncTransactionQueue syncQueue) { - super(context, null, 0, 0, true /* disableBackgroundLayer */); - + mContext = context; mTaskOrganizer = organizer; mShellExecutor = organizer.getExecutor(); mSyncQueue = syncQueue; @@ -105,20 +79,33 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback, if (mTaskViewTransitions != null) { mTaskViewTransitions.addTaskView(this); } - setUseAlpha(); - getHolder().addCallback(this); mGuard.open("release"); } + /** + * Sets the provided {@link TaskViewBase}, which is used to notify the client part about the + * task related changes and getting the current bounds. + */ + public void setTaskViewBase(TaskViewBase taskViewBase) { + mTaskViewBase = taskViewBase; + } + + /** + * @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; + return mTaskViewTransitions != null && mTaskViewTransitions.isEnabled(); } /** * Only one listener may be set on the view, throws an exception otherwise. */ - public void setListener(@NonNull Executor executor, Listener listener) { + void setListener(@NonNull Executor executor, TaskView.Listener listener) { if (mListener != null) { throw new IllegalStateException( "Trying to set a listener when one has already been set"); @@ -145,7 +132,7 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback, mShellExecutor.execute(() -> { final WindowContainerTransaction wct = new WindowContainerTransaction(); wct.startShortcut(mContext.getPackageName(), shortcut, options.toBundle()); - mTaskViewTransitions.startTaskView(wct, this); + mTaskViewTransitions.startTaskView(wct, this, options.getLaunchCookie()); }); return; } @@ -172,7 +159,7 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback, mShellExecutor.execute(() -> { WindowContainerTransaction wct = new WindowContainerTransaction(); wct.sendPendingIntent(pendingIntent, fillInIntent, options.toBundle()); - mTaskViewTransitions.startTaskView(wct, this); + mTaskViewTransitions.startTaskView(wct, this, options.getLaunchCookie()); }); return; } @@ -197,50 +184,6 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback, } /** - * Indicates a region of the view that is not touchable. - * - * @param obscuredRect the obscured region of the view. - */ - public void setObscuredTouchRect(Rect obscuredRect) { - mObscuredTouchRegion = obscuredRect != null ? new Region(obscuredRect) : null; - } - - /** - * Indicates a region of the view that is not touchable. - * - * @param obscuredRegion the obscured region of the view. - */ - public void setObscuredTouchRegion(Region obscuredRegion) { - mObscuredTouchRegion = obscuredRegion; - } - - private void onLocationChanged(WindowContainerTransaction wct) { - // Update based on the screen bounds - getBoundsOnScreen(mTmpRect); - getRootView().getBoundsOnScreen(mTmpRootRect); - if (!mTmpRootRect.contains(mTmpRect)) { - mTmpRect.offsetTo(0, 0); - } - wct.setBounds(mTaskToken, mTmpRect); - } - - /** - * Call when view position or size has changed. Do not call when animating. - */ - public void onLocationChanged() { - if (mTaskToken == null) { - return; - } - // Sync Transactions can't operate simultaneously with shell transition collection. - // The transition animation (upon showing) will sync the location itself. - if (isUsingShellTransitions() && mTaskViewTransitions.hasPending()) return; - - WindowContainerTransaction wct = new WindowContainerTransaction(); - onLocationChanged(wct); - mSyncQueue.queue(wct); - } - - /** * Release this container if it is initialized. */ public void release() { @@ -260,7 +203,6 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback, } private void performRelease() { - getHolder().removeCallback(this); if (mTaskViewTransitions != null) { mTaskViewTransitions.removeTaskView(this); } @@ -269,11 +211,17 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback, resetTaskInfo(); }); mGuard.close(); - if (mListener != null && mIsInitialized) { + mIsInitialized = false; + notifyReleased(); + } + + /** Called when the {@link TaskViewTaskController} has been released. */ + protected void notifyReleased() { + if (mListener != null && mNotifiedForInitialized) { mListenerExecutor.execute(() -> { mListener.onReleased(); }); - mIsInitialized = false; + mNotifiedForInitialized = false; } } @@ -311,7 +259,7 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback, if (mSurfaceCreated) { // Surface is ready, so just reparent the task to this surface control - mTransaction.reparent(mTaskLeash, getSurfaceControl()) + mTransaction.reparent(mTaskLeash, mSurfaceControl) .show(mTaskLeash) .apply(); } else { @@ -320,13 +268,9 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback, updateTaskVisibility(); } mTaskOrganizer.setInterceptBackPressedOnTaskRoot(mTaskToken, true); - onLocationChanged(); - if (taskInfo.taskDescription != null) { - int backgroundColor = taskInfo.taskDescription.getBackgroundColor(); - mSyncQueue.runInSync((t) -> { - setResizeBackgroundColor(t, backgroundColor); - }); - } + mSyncQueue.runInSync((t) -> { + mTaskViewBase.onTaskAppeared(taskInfo, leash); + }); if (mListener != null) { final int taskId = taskInfo.taskId; @@ -354,13 +298,12 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback, // Unparent the task when this surface is destroyed mTransaction.reparent(mTaskLeash, null).apply(); resetTaskInfo(); + mTaskViewBase.onTaskVanished(taskInfo); } @Override public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) { - if (taskInfo.taskDescription != null) { - setResizeBackgroundColor(taskInfo.taskDescription.getBackgroundColor()); - } + mTaskViewBase.onTaskInfoChanged(taskInfo); } @Override @@ -401,18 +344,19 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback, @Override public String toString() { - return "TaskView" + ":" + (mTaskInfo != null ? mTaskInfo.taskId : "null"); + return "TaskViewTaskController" + ":" + (mTaskInfo != null ? mTaskInfo.taskId : "null"); } - @Override - public void surfaceCreated(SurfaceHolder holder) { + /** + * Should be called when the client surface is created. + * + * @param surfaceControl the {@link SurfaceControl} for the underlying surface. + */ + public void surfaceCreated(SurfaceControl surfaceControl) { mSurfaceCreated = true; - if (mListener != null && !mIsInitialized) { - mIsInitialized = true; - mListenerExecutor.execute(() -> { - mListener.onInitialized(); - }); - } + mIsInitialized = true; + mSurfaceControl = surfaceControl; + notifyInitialized(); mShellExecutor.execute(() -> { if (mTaskToken == null) { // Nothing to update, task is not yet available @@ -423,24 +367,45 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback, return; } // Reparent the task when this surface is created - mTransaction.reparent(mTaskLeash, getSurfaceControl()) + mTransaction.reparent(mTaskLeash, mSurfaceControl) .show(mTaskLeash) .apply(); updateTaskVisibility(); }); } - @Override - public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { + /** + * Sets the window bounds to {@code boundsOnScreen}. + * Call when view position or size has changed. Can also be called before the animation when + * the final bounds are known. + * Do not call during the animation. + * + * @param boundsOnScreen the on screen bounds of the surface view. + */ + public void setWindowBounds(Rect boundsOnScreen) { if (mTaskToken == null) { return; } - onLocationChanged(); + // Sync Transactions can't operate simultaneously with shell transition collection. + if (isUsingShellTransitions()) { + if (mTaskViewTransitions.hasPending()) { + // There is already a transition in-flight. The window bounds will be synced + // once it is complete. + return; + } + mTaskViewTransitions.setTaskBounds(this, boundsOnScreen); + return; + } + + WindowContainerTransaction wct = new WindowContainerTransaction(); + wct.setBounds(mTaskToken, boundsOnScreen); + mSyncQueue.queue(wct); } - @Override - public void surfaceDestroyed(SurfaceHolder holder) { + /** Should be called when the client surface is destroyed. */ + public void surfaceDestroyed() { mSurfaceCreated = false; + mSurfaceControl = null; mShellExecutor.execute(() -> { if (mTaskToken == null) { // Nothing to update, task is not yet available @@ -458,43 +423,19 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback, }); } - @Override - public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo) { - // TODO(b/176854108): Consider to move the logic into gatherTransparentRegions since this - // is dependent on the order of listener. - // If there are multiple TaskViews, we'll set the touchable area as the root-view, then - // subtract each TaskView from it. - if (inoutInfo.touchableRegion.isEmpty()) { - inoutInfo.setTouchableInsets( - ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION); - View root = getRootView(); - root.getLocationInWindow(mTmpLocation); - mTmpRootRect.set(mTmpLocation[0], mTmpLocation[1], root.getWidth(), root.getHeight()); - inoutInfo.touchableRegion.set(mTmpRootRect); - } - getLocationInWindow(mTmpLocation); - mTmpRect.set(mTmpLocation[0], mTmpLocation[1], - mTmpLocation[0] + getWidth(), mTmpLocation[1] + getHeight()); - inoutInfo.touchableRegion.op(mTmpRect, Region.Op.DIFFERENCE); - - if (mObscuredTouchRegion != null) { - inoutInfo.touchableRegion.op(mObscuredTouchRegion, Region.Op.UNION); + /** Called when the {@link TaskViewTaskController} is initialized. */ + protected void notifyInitialized() { + if (mListener != null && !mNotifiedForInitialized) { + mNotifiedForInitialized = true; + mListenerExecutor.execute(() -> { + mListener.onInitialized(); + }); } } - @Override - protected void onAttachedToWindow() { - super.onAttachedToWindow(); - getViewTreeObserver().addOnComputeInternalInsetsListener(this); - } - - @Override - protected void onDetachedFromWindow() { - super.onDetachedFromWindow(); - getViewTreeObserver().removeOnComputeInternalInsetsListener(this); - } - - ActivityManager.RunningTaskInfo getTaskInfo() { + /** Returns the task info for the task in the TaskView. */ + @Nullable + public ActivityManager.RunningTaskInfo getTaskInfo() { return mTaskInfo; } @@ -524,6 +465,7 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback, mListener.onTaskRemovalStarted(taskId); }); } + mTaskViewBase.onTaskVanished(mTaskInfo); mTaskOrganizer.setInterceptBackPressedOnTaskRoot(mTaskToken, false); } resetTaskInfo(); @@ -539,17 +481,16 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback, mTaskLeash = leash; if (mSurfaceCreated) { // Surface is ready, so just reparent the task to this surface control - startTransaction.reparent(mTaskLeash, getSurfaceControl()) + startTransaction.reparent(mTaskLeash, mSurfaceControl) .show(mTaskLeash) .apply(); // Also reparent on finishTransaction since the finishTransaction will reparent back // to its "original" parent by default. - finishTransaction.reparent(mTaskLeash, getSurfaceControl()) + finishTransaction.reparent(mTaskLeash, mSurfaceControl) .setPosition(mTaskLeash, 0, 0) .apply(); - // TODO: determine if this is really necessary or not - onLocationChanged(wct); + wct.setBounds(mTaskToken, mTaskViewBase.getCurrentBoundsOnScreen()); } else { // The surface has already been destroyed before the task has appeared, // so go ahead and hide the task entirely @@ -562,7 +503,7 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback, if (mTaskInfo.taskDescription != null) { int backgroundColor = mTaskInfo.taskDescription.getBackgroundColor(); - setResizeBackgroundColor(startTransaction, backgroundColor); + mTaskViewBase.setResizeBgColor(startTransaction, backgroundColor); } if (mListener != null) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/TaskViewTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java index 83335ac24799..3b1ce49ebdc7 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/TaskViewTransitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java @@ -14,8 +14,9 @@ * limitations under the License. */ -package com.android.wm.shell; +package com.android.wm.shell.taskview; +import static android.view.WindowManager.TRANSIT_CHANGE; import static android.view.WindowManager.TRANSIT_OPEN; import static android.view.WindowManager.TRANSIT_TO_BACK; import static android.view.WindowManager.TRANSIT_TO_FRONT; @@ -23,6 +24,7 @@ import static android.view.WindowManager.TRANSIT_TO_FRONT; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.ActivityManager; +import android.graphics.Rect; import android.os.IBinder; import android.util.Slog; import android.view.SurfaceControl; @@ -32,6 +34,7 @@ import android.window.TransitionRequestInfo; import android.window.WindowContainerTransaction; import com.android.wm.shell.transition.Transitions; +import com.android.wm.shell.util.TransitionUtil; import java.util.ArrayList; @@ -39,9 +42,9 @@ import java.util.ArrayList; * Handles Shell Transitions that involve TaskView tasks. */ public class TaskViewTransitions implements Transitions.TransitionHandler { - private static final String TAG = "TaskViewTransitions"; + static final String TAG = "TaskViewTransitions"; - private final ArrayList<TaskView> mTaskViews = new ArrayList<>(); + private final ArrayList<TaskViewTaskController> mTaskViews = new ArrayList<>(); private final ArrayList<PendingTransition> mPending = new ArrayList<>(); private final Transitions mTransitions; private final boolean[] mRegistered = new boolean[]{ false }; @@ -54,14 +57,24 @@ public class TaskViewTransitions implements Transitions.TransitionHandler { private static class PendingTransition { final @WindowManager.TransitionType int mType; final WindowContainerTransaction mWct; - final @NonNull TaskView mTaskView; + final @NonNull TaskViewTaskController mTaskView; IBinder mClaimed; + /** + * This is needed because arbitrary activity launches can still "intrude" into any + * transition since `startActivity` is a synchronous call. Once that is solved, we can + * remove this. + */ + final IBinder mLaunchCookie; + PendingTransition(@WindowManager.TransitionType int type, - @Nullable WindowContainerTransaction wct, @NonNull TaskView taskView) { + @Nullable WindowContainerTransaction wct, + @NonNull TaskViewTaskController taskView, + @Nullable IBinder launchCookie) { mType = type; mWct = wct; mTaskView = taskView; + mLaunchCookie = launchCookie; } } @@ -72,7 +85,7 @@ public class TaskViewTransitions implements Transitions.TransitionHandler { // TODO(210041388): register here once we have an explicit ordering mechanism. } - void addTaskView(TaskView tv) { + void addTaskView(TaskViewTaskController tv) { synchronized (mRegistered) { if (!mRegistered[0]) { mRegistered[0] = true; @@ -82,11 +95,15 @@ public class TaskViewTransitions implements Transitions.TransitionHandler { mTaskViews.add(tv); } - void removeTaskView(TaskView tv) { + void removeTaskView(TaskViewTaskController tv) { mTaskViews.remove(tv); // Note: Don't unregister handler since this is a singleton with lifetime bound to Shell } + boolean isEnabled() { + return mTransitions.isRegistered(); + } + /** * Looks through the pending transitions for one matching `taskView`. * @param taskView the pending transition should be for this. @@ -97,10 +114,11 @@ public class TaskViewTransitions implements Transitions.TransitionHandler { * if there is a match earlier. The idea behind this is to check the state of * the taskviews "as if all transitions already happened". */ - private PendingTransition findPending(TaskView taskView, boolean closing, boolean latest) { + private PendingTransition findPending(TaskViewTaskController taskView, boolean closing, + boolean latest) { for (int i = mPending.size() - 1; i >= 0; --i) { if (mPending.get(i).mTaskView != taskView) continue; - if (Transitions.isClosingType(mPending.get(i).mType) == closing) { + if (TransitionUtil.isClosingType(mPending.get(i).mType) == closing) { return mPending.get(i); } if (latest) { @@ -130,13 +148,13 @@ public class TaskViewTransitions implements Transitions.TransitionHandler { if (triggerTask == null) { return null; } - final TaskView taskView = findTaskView(triggerTask); + final TaskViewTaskController taskView = findTaskView(triggerTask); if (taskView == null) return null; // Opening types should all be initiated by shell - if (!Transitions.isClosingType(request.getType())) return null; + if (!TransitionUtil.isClosingType(request.getType())) return null; PendingTransition pending = findPending(taskView, true /* closing */, false /* latest */); if (pending == null) { - pending = new PendingTransition(request.getType(), null, taskView); + pending = new PendingTransition(request.getType(), null, taskView, null /* cookie */); } if (pending.mClaimed != null) { throw new IllegalStateException("Task is closing in 2 collecting transitions?" @@ -146,7 +164,7 @@ public class TaskViewTransitions implements Transitions.TransitionHandler { return new WindowContainerTransaction(); } - private TaskView findTaskView(ActivityManager.RunningTaskInfo taskInfo) { + private TaskViewTaskController findTaskView(ActivityManager.RunningTaskInfo taskInfo) { for (int i = 0; i < mTaskViews.size(); ++i) { if (mTaskViews.get(i).getTaskInfo() == null) continue; if (taskInfo.token.equals(mTaskViews.get(i).getTaskInfo().token)) { @@ -156,12 +174,13 @@ public class TaskViewTransitions implements Transitions.TransitionHandler { return null; } - void startTaskView(WindowContainerTransaction wct, TaskView taskView) { - mPending.add(new PendingTransition(TRANSIT_OPEN, wct, taskView)); + void startTaskView(@NonNull WindowContainerTransaction wct, + @NonNull TaskViewTaskController taskView, @NonNull IBinder launchCookie) { + mPending.add(new PendingTransition(TRANSIT_OPEN, wct, taskView, launchCookie)); startNextTransition(); } - void setTaskViewVisible(TaskView taskView, boolean visible) { + void setTaskViewVisible(TaskViewTaskController taskView, boolean visible) { PendingTransition pending = findPending(taskView, !visible, true /* latest */); if (pending != null) { // Already opening or creating a task, so no need to do anything here. @@ -174,12 +193,19 @@ public class TaskViewTransitions implements Transitions.TransitionHandler { final WindowContainerTransaction wct = new WindowContainerTransaction(); wct.setHidden(taskView.getTaskInfo().token, !visible /* hidden */); pending = new PendingTransition( - visible ? TRANSIT_TO_FRONT : TRANSIT_TO_BACK, wct, taskView); + visible ? TRANSIT_TO_FRONT : TRANSIT_TO_BACK, wct, taskView, null /* cookie */); mPending.add(pending); startNextTransition(); // visibility is reported in transition. } + void setTaskBounds(TaskViewTaskController taskView, Rect boundsOnScreen) { + WindowContainerTransaction wct = new WindowContainerTransaction(); + wct.setBounds(taskView.getTaskInfo().token, boundsOnScreen); + mPending.add(new PendingTransition(TRANSIT_CHANGE, wct, taskView, null /* cookie */)); + startNextTransition(); + } + private void startNextTransition() { if (mPending.isEmpty()) return; final PendingTransition pending = mPending.get(0); @@ -191,58 +217,92 @@ public class TaskViewTransitions implements Transitions.TransitionHandler { } @Override + public void onTransitionConsumed(@NonNull IBinder transition, boolean aborted, + @NonNull SurfaceControl.Transaction finishTransaction) { + if (!aborted) return; + final PendingTransition pending = findPending(transition); + if (pending == null) return; + mPending.remove(pending); + startNextTransition(); + } + + @Override public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction, @NonNull SurfaceControl.Transaction finishTransaction, @NonNull Transitions.TransitionFinishCallback finishCallback) { PendingTransition pending = findPending(transition); - if (pending == null) return false; - mPending.remove(pending); - TaskView taskView = pending.mTaskView; - final ArrayList<TransitionInfo.Change> tasks = new ArrayList<>(); - for (int i = 0; i < info.getChanges().size(); ++i) { - final TransitionInfo.Change chg = info.getChanges().get(i); - if (chg.getTaskInfo() == null) continue; - tasks.add(chg); + if (pending != null) { + mPending.remove(pending); } - if (tasks.isEmpty()) { - Slog.e(TAG, "Got a TaskView transition with no task."); + if (mTaskViews.isEmpty()) { + if (pending != null) { + Slog.e(TAG, "Pending taskview transition but no task-views"); + } return false; } + boolean stillNeedsMatchingLaunch = pending != null && pending.mLaunchCookie != null; + int changesHandled = 0; WindowContainerTransaction wct = null; - for (int i = 0; i < tasks.size(); ++i) { - TransitionInfo.Change chg = tasks.get(i); - if (Transitions.isClosingType(chg.getMode())) { + for (int i = 0; i < info.getChanges().size(); ++i) { + final TransitionInfo.Change chg = info.getChanges().get(i); + if (chg.getTaskInfo() == null) continue; + if (TransitionUtil.isClosingType(chg.getMode())) { final boolean isHide = chg.getMode() == TRANSIT_TO_BACK; - TaskView tv = findTaskView(chg.getTaskInfo()); + TaskViewTaskController tv = findTaskView(chg.getTaskInfo()); if (tv == null) { - throw new IllegalStateException("TaskView transition is closing a " - + "non-taskview task "); + if (pending != null) { + Slog.w(TAG, "Found a non-TaskView task in a TaskView Transition. This " + + "shouldn't happen, so there may be a visual artifact: " + + chg.getTaskInfo().taskId); + } + continue; } if (isHide) { tv.prepareHideAnimation(finishTransaction); } else { tv.prepareCloseAnimation(); } - } else if (Transitions.isOpeningType(chg.getMode())) { + changesHandled++; + } else if (TransitionUtil.isOpeningType(chg.getMode())) { final boolean taskIsNew = chg.getMode() == TRANSIT_OPEN; - if (wct == null) wct = new WindowContainerTransaction(); - TaskView tv = taskView; - if (!taskIsNew) { + final TaskViewTaskController tv; + if (taskIsNew) { + if (pending == null + || !chg.getTaskInfo().containsLaunchCookie(pending.mLaunchCookie)) { + Slog.e(TAG, "Found a launching TaskView in the wrong transition. All " + + "TaskView launches should be initiated by shell and in their " + + "own transition: " + chg.getTaskInfo().taskId); + continue; + } + stillNeedsMatchingLaunch = false; + tv = pending.mTaskView; + } else { tv = findTaskView(chg.getTaskInfo()); if (tv == null) { - throw new IllegalStateException("TaskView transition is showing a " - + "non-taskview task "); + if (pending != null) { + Slog.w(TAG, "Found a non-TaskView task in a TaskView Transition. This " + + "shouldn't happen, so there may be a visual artifact: " + + chg.getTaskInfo().taskId); + } + continue; } } + if (wct == null) wct = new WindowContainerTransaction(); tv.prepareOpenAnimation(taskIsNew, startTransaction, finishTransaction, chg.getTaskInfo(), chg.getLeash(), wct); - } else { - throw new IllegalStateException("Claimed transition isn't an opening or closing" - + " type: " + chg.getMode()); + changesHandled++; } } + if (stillNeedsMatchingLaunch) { + throw new IllegalStateException("Expected a TaskView launch in this transition but" + + " didn't get one."); + } + if (wct == null && pending == null && changesHandled != info.getChanges().size()) { + // Just some house-keeping, let another handler animate. + return false; + } // No animation, just show it immediately. startTransaction.apply(); finishTransaction.apply(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/CounterRotatorHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/CounterRotatorHelper.java index 19133e29de4b..628ce27fe514 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/CounterRotatorHelper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/CounterRotatorHelper.java @@ -28,6 +28,7 @@ import android.window.WindowContainerToken; import androidx.annotation.NonNull; import com.android.wm.shell.util.CounterRotator; +import com.android.wm.shell.util.TransitionUtil; import java.util.List; @@ -57,7 +58,7 @@ public class CounterRotatorHelper { for (int i = numChanges - 1; i >= 0; --i) { final TransitionInfo.Change change = changes.get(i); final WindowContainerToken parent = change.getParent(); - if (!Transitions.isClosingType(change.getMode()) + if (!TransitionUtil.isClosingType(change.getMode()) || !TransitionInfo.isIndependent(change, info) || parent == null) { continue; } 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..5a92f7830194 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java @@ -0,0 +1,601 @@ +/* + * 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.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS; +import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +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 static com.android.wm.shell.util.TransitionUtil.isOpeningType; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.IBinder; +import android.util.Pair; +import android.view.SurfaceControl; +import android.view.WindowManager; +import android.window.TransitionInfo; +import android.window.TransitionRequestInfo; +import android.window.WindowContainerTransaction; +import android.window.WindowContainerTransactionCallback; + +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.recents.RecentsTransitionHandler; +import com.android.wm.shell.splitscreen.SplitScreenController; +import com.android.wm.shell.splitscreen.StageCoordinator; +import com.android.wm.shell.sysui.ShellInit; +import com.android.wm.shell.util.TransitionUtil; + +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, + RecentsTransitionHandler.RecentsMixedHandler { + + private final Transitions mPlayer; + private PipTransitionController mPipHandler; + private RecentsTransitionHandler mRecentsHandler; + 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; + + /** Pip was entered while handling an intent with its own remoteTransition. */ + static final int TYPE_OPTIONS_REMOTE_AND_PIP_CHANGE = 3; + + /** Recents transition while split-screen active. */ + static final int TYPE_RECENTS_DURING_SPLIT = 4; + + /** 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; + + /** For RECENTS_DURING_SPLIT, is set when this turns into a pair->pair task switch. */ + static final int ANIM_TYPE_PAIR_TO_PAIR = 1; + + final int mType; + int mAnimType = ANIM_TYPE_DEFAULT; + final IBinder mTransition; + + 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; + } + + void joinFinishArgs(WindowContainerTransaction wct, + WindowContainerTransactionCallback wctCB) { + if (wctCB != null) { + // Technically can probably support 1, but don't want to encourage CB usage since + // it creates instabliity, so just throw. + throw new IllegalArgumentException("Can't mix transitions that require finish" + + " sync callback"); + } + if (wct != null) { + if (mFinishWCT == null) { + mFinishWCT = wct; + } else { + mFinishWCT.merge(wct, true /* transfer */); + } + } + } + } + + private final ArrayList<MixedTransition> mActiveTransitions = new ArrayList<>(); + + public DefaultMixedHandler(@NonNull ShellInit shellInit, @NonNull Transitions player, + Optional<SplitScreenController> splitScreenControllerOptional, + Optional<PipTouchHandler> pipTouchHandlerOptional, + Optional<RecentsTransitionHandler> recentsHandlerOptional) { + 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); + } + mRecentsHandler = recentsHandlerOptional.orElse(null); + if (mRecentsHandler != null) { + mRecentsHandler.addMixer(this); + } + }, this); + } + } + + @Nullable + @Override + public WindowContainerTransaction handleRequest(@NonNull IBinder transition, + @NonNull TransitionRequestInfo request) { + if (mPipHandler.requestHasPipEnter(request) && mSplitHandler.isSplitScreenVisible()) { + 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; + } else if (request.getRemoteTransition() != null + && TransitionUtil.isOpeningType(request.getType()) + && (request.getTriggerTask() == null + || (request.getTriggerTask().topActivityType != ACTIVITY_TYPE_HOME + && request.getTriggerTask().topActivityType != ACTIVITY_TYPE_RECENTS))) { + // Only select transitions with an intent-provided remote-animation because that will + // usually grab priority and often won't handle PiP. If there isn't an intent-provided + // remote, then the transition will be dispatched normally and the PipHandler will + // pick it up. + Pair<Transitions.TransitionHandler, WindowContainerTransaction> handler = + mPlayer.dispatchRequest(transition, request, this); + if (handler == null) { + return null; + } + final MixedTransition mixed = new MixedTransition( + MixedTransition.TYPE_OPTIONS_REMOTE_AND_PIP_CHANGE, transition); + mixed.mLeftoversHandler = handler.first; + mActiveTransitions.add(mixed); + return handler.second; + } else if (mSplitHandler.isSplitActive() + && isOpeningType(request.getType()) + && request.getTriggerTask() != null + && request.getTriggerTask().getWindowingMode() == WINDOWING_MODE_FULLSCREEN + && request.getTriggerTask().getActivityType() == ACTIVITY_TYPE_HOME) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Got a going-home request while " + + "Split-Screen is active, so treat it as Mixed."); + Pair<Transitions.TransitionHandler, WindowContainerTransaction> handler = + mPlayer.dispatchRequest(transition, request, this); + if (handler == null) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, + " Lean on the remote transition handler to fetch a proper remote via" + + " TransitionFilter"); + handler = new Pair<>( + mPlayer.getRemoteTransitionHandler(), + new WindowContainerTransaction()); + } + final MixedTransition mixed = new MixedTransition( + MixedTransition.TYPE_RECENTS_DURING_SPLIT, transition); + mixed.mLeftoversHandler = handler.first; + mActiveTransitions.add(mixed); + return handler.second; + } + return null; + } + + @Override + public Transitions.TransitionHandler handleRecentsRequest(WindowContainerTransaction outWCT) { + if (mRecentsHandler != null && mSplitHandler.isSplitActive()) { + return this; + } + return null; + } + + @Override + public void setRecentsTransition(IBinder transition) { + if (mSplitHandler.isSplitActive()) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Got a recents request while " + + "Split-Screen is active, so treat it as Mixed."); + final MixedTransition mixed = new MixedTransition( + MixedTransition.TYPE_RECENTS_DURING_SPLIT, transition); + mixed.mLeftoversHandler = mRecentsHandler; + mActiveTransitions.add(mixed); + } else { + throw new IllegalStateException("Accepted a recents transition but don't know how to" + + " handle it"); + } + } + + private TransitionInfo subCopy(@NonNull TransitionInfo info, + @WindowManager.TransitionType int newType, boolean withChanges) { + final TransitionInfo out = new TransitionInfo(newType, withChanges ? info.getFlags() : 0); + out.setDebugId(info.getDebugId()); + if (withChanges) { + for (int i = 0; i < info.getChanges().size(); ++i) { + out.getChanges().add(info.getChanges().get(i)); + } + } + for (int i = 0; i < info.getRootCount(); ++i) { + out.addRoot(info.getRoot(i)); + } + 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 if (mixed.mType == MixedTransition.TYPE_OPTIONS_REMOTE_AND_PIP_CHANGE) { + return animateOpenIntentWithRemoteAndPip(mixed, info, startTransaction, + finishTransaction, finishCallback); + } else if (mixed.mType == MixedTransition.TYPE_RECENTS_DURING_SPLIT) { + return animateRecentsDuringSplit(mixed, info, startTransaction, finishTransaction, + finishCallback); + } else { + mActiveTransitions.remove(mixed); + throw new IllegalStateException("Starting mixed animation without a known mixed type? " + + mixed.mType); + } + } + + private boolean animateOpenIntentWithRemoteAndPip(@NonNull MixedTransition mixed, + @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, + @NonNull Transitions.TransitionFinishCallback finishCallback) { + TransitionInfo.Change pipChange = null; + 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; + info.getChanges().remove(i); + } + } + Transitions.TransitionFinishCallback finishCB = (wct, wctCB) -> { + --mixed.mInFlightSubAnimations; + mixed.joinFinishArgs(wct, wctCB); + if (mixed.mInFlightSubAnimations > 0) return; + mActiveTransitions.remove(mixed); + finishCallback.onTransitionFinished(mixed.mFinishWCT, wctCB); + }; + if (pipChange == null) { + if (mixed.mLeftoversHandler != null) { + mixed.mInFlightSubAnimations = 1; + if (mixed.mLeftoversHandler.startAnimation(mixed.mTransition, + info, startTransaction, finishTransaction, finishCB)) { + return true; + } + } + mActiveTransitions.remove(mixed); + return false; + } + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Splitting PIP into a separate" + + " animation because remote-animation likely doesn't support it"); + // Split the transition into 2 parts: the pip part and the rest. + mixed.mInFlightSubAnimations = 2; + // 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(); + + mPipHandler.startEnterAnimation(pipChange, otherStartT, finishTransaction, finishCB); + + // Dispatch the rest of the transition normally. + if (mixed.mLeftoversHandler != null + && mixed.mLeftoversHandler.startAnimation(mixed.mTransition, info, + startTransaction, finishTransaction, finishCB)) { + return true; + } + mixed.mLeftoversHandler = mPlayer.dispatchTransition(mixed.mTransition, info, + startTransaction, finishTransaction, finishCB, this); + return true; + } + + 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. + mActiveTransitions.remove(mixed); + return false; + } + final boolean isGoingHome = homeIsOpening; + Transitions.TransitionFinishCallback finishCB = (wct, wctCB) -> { + --mixed.mInFlightSubAnimations; + mixed.joinFinishArgs(wct, wctCB); + if (mixed.mInFlightSubAnimations > 0) return; + mActiveTransitions.remove(mixed); + if (isGoingHome) { + mSplitHandler.onTransitionAnimationComplete(); + } + finishCallback.onTransitionFinished(mixed.mFinishWCT, 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); + 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; + mixed.joinFinishArgs(wct, wctCB); + if (mixed.mInFlightSubAnimations > 0) return; + mActiveTransitions.remove(mixed); + finishCallback.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; + } + + private boolean animateRecentsDuringSplit(@NonNull final MixedTransition mixed, + @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, + @NonNull Transitions.TransitionFinishCallback finishCallback) { + // Split-screen is only interested in the recents transition finishing (and merging), so + // just wrap finish and start recents animation directly. + Transitions.TransitionFinishCallback finishCB = (wct, wctCB) -> { + mixed.mInFlightSubAnimations = 0; + mActiveTransitions.remove(mixed); + // If pair-to-pair switching, the post-recents clean-up isn't needed. + if (mixed.mAnimType != MixedTransition.ANIM_TYPE_PAIR_TO_PAIR) { + wct = wct != null ? wct : new WindowContainerTransaction(); + mSplitHandler.onRecentsInSplitAnimationFinish(wct, finishTransaction, info); + } + mSplitHandler.onTransitionAnimationComplete(); + finishCallback.onTransitionFinished(wct, wctCB); + }; + mixed.mInFlightSubAnimations = 1; + mSplitHandler.onRecentsInSplitAnimationStart(startTransaction); + final boolean handled = mixed.mLeftoversHandler.startAnimation(mixed.mTransition, info, + startTransaction, finishTransaction, finishCB); + if (!handled) { + mActiveTransitions.remove(mixed); + } + return handled; + } + + @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).mTransition != 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_DISPLAY_AND_SPLIT_CHANGE) { + // queue since no actual animation. + } else 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_OPTIONS_REMOTE_AND_PIP_CHANGE) { + mPipHandler.end(); + if (mixed.mLeftoversHandler != null) { + mixed.mLeftoversHandler.mergeAnimation(transition, info, t, mergeTarget, + finishCallback); + } + } else if (mixed.mType == MixedTransition.TYPE_RECENTS_DURING_SPLIT) { + if (mSplitHandler.isPendingEnter(transition)) { + // Recents -> enter-split means that we are switching from one pair to + // another pair. + mixed.mAnimType = MixedTransition.ANIM_TYPE_PAIR_TO_PAIR; + } + mixed.mLeftoversHandler.mergeAnimation(transition, info, t, mergeTarget, + finishCallback); + } 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); + } else if (mixed.mType == MixedTransition.TYPE_RECENTS_DURING_SPLIT) { + mixed.mLeftoversHandler.onTransitionConsumed(transition, aborted, finishT); + } else if (mixed.mType == MixedTransition.TYPE_OPTIONS_REMOTE_AND_PIP_CHANGE) { + mixed.mLeftoversHandler.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..3dd10a098310 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java @@ -18,7 +18,6 @@ package com.android.wm.shell.transition; import static android.app.ActivityOptions.ANIM_CLIP_REVEAL; import static android.app.ActivityOptions.ANIM_CUSTOM; -import static android.app.ActivityOptions.ANIM_FROM_STYLE; import static android.app.ActivityOptions.ANIM_NONE; import static android.app.ActivityOptions.ANIM_OPEN_CROSS_PROFILE_APPS; import static android.app.ActivityOptions.ANIM_SCALE_UP; @@ -42,7 +41,13 @@ import static android.view.WindowManager.TRANSIT_OPEN; import static android.view.WindowManager.TRANSIT_RELAUNCH; import static android.view.WindowManager.TRANSIT_TO_BACK; import static android.view.WindowManager.TRANSIT_TO_FRONT; +import static android.window.TransitionInfo.FLAG_BACK_GESTURE_ANIMATED; +import static android.window.TransitionInfo.FLAG_CROSS_PROFILE_OWNER_THUMBNAIL; +import static android.window.TransitionInfo.FLAG_CROSS_PROFILE_WORK_THUMBNAIL; import static android.window.TransitionInfo.FLAG_DISPLAY_HAS_ALERT_WINDOWS; +import static android.window.TransitionInfo.FLAG_FILLS_TASK; +import static android.window.TransitionInfo.FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY; +import static android.window.TransitionInfo.FLAG_IS_BEHIND_STARTING_WINDOW; import static android.window.TransitionInfo.FLAG_IS_DISPLAY; import static android.window.TransitionInfo.FLAG_IS_VOICE_INTERACTION; import static android.window.TransitionInfo.FLAG_IS_WALLPAPER; @@ -55,6 +60,10 @@ import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITI import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_INTRA_OPEN; import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_NONE; import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_OPEN; +import static com.android.wm.shell.transition.TransitionAnimationHelper.addBackgroundToTransition; +import static com.android.wm.shell.transition.TransitionAnimationHelper.edgeExtendWindow; +import static com.android.wm.shell.transition.TransitionAnimationHelper.getTransitionBackgroundColorIfSet; +import static com.android.wm.shell.transition.TransitionAnimationHelper.loadAttributeAnimation; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; @@ -62,28 +71,23 @@ 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; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; -import android.graphics.Canvas; -import android.graphics.Color; import android.graphics.Insets; -import android.graphics.Paint; -import android.graphics.PixelFormat; import android.graphics.Point; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.hardware.HardwareBuffer; import android.os.Handler; import android.os.IBinder; -import android.os.SystemProperties; import android.os.UserHandle; import android.util.ArrayMap; import android.view.Choreographer; -import android.view.Surface; import android.view.SurfaceControl; import android.view.SurfaceSession; import android.view.WindowManager; @@ -107,6 +111,8 @@ 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 com.android.wm.shell.util.TransitionUtil; import java.util.ArrayList; import java.util.List; @@ -116,24 +122,10 @@ import java.util.function.Consumer; public class DefaultTransitionHandler implements Transitions.TransitionHandler { private static final int MAX_ANIMATION_DURATION = 3000; - /** - * Restrict ability of activities overriding transition animation in a way such that - * an activity can do it only when the transition happens within a same task. - * - * @see android.app.Activity#overridePendingTransition(int, int) - */ - private static final String DISABLE_CUSTOM_TASK_ANIMATION_PROPERTY = - "persist.wm.disable_custom_task_animation"; - - /** - * @see #DISABLE_CUSTOM_TASK_ANIMATION_PROPERTY - */ - static boolean sDisableCustomTaskAnimationProperty = - SystemProperties.getBoolean(DISABLE_CUSTOM_TASK_ANIMATION_PROPERTY, true); - private final TransactionPool mTransactionPool; private final DisplayController mDisplayController; private final Context mContext; + private final Handler mMainHandler; private final ShellExecutor mMainExecutor; private final ShellExecutor mAnimExecutor; private final TransitionAnimation mTransitionAnimation; @@ -150,8 +142,6 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { private final int mCurrentUserId; - private ScreenRotationAnimation mRotationAnimation; - private Drawable mEnterpriseThumbnailDrawable; private BroadcastReceiver mEnterpriseResourceUpdatedReceiver = new BroadcastReceiver() { @@ -165,27 +155,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 +191,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 +221,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 @@ -321,6 +301,14 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { return true; } + // Early check if the transition doesn't warrant an animation. + if (Transitions.isAllNoAnimation(info) || Transitions.isAllOrderOnly(info)) { + startTransaction.apply(); + finishTransaction.apply(); + finishCallback.onTransitionFinished(null /* wct */, null /* wctCB */); + return true; + } + if (mAnimations.containsKey(transition)) { throw new IllegalStateException("Got a duplicate startAnimation call for " + transition); @@ -330,12 +318,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 */); }; @@ -347,19 +329,27 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { final int wallpaperTransit = getWallpaperTransitType(info); for (int i = info.getChanges().size() - 1; i >= 0; --i) { final TransitionInfo.Change change = info.getChanges().get(i); + if (change.hasAllFlags(FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY + | FLAG_IS_BEHIND_STARTING_WINDOW)) { + // Don't animate embedded activity if it is covered by the starting window. + // Non-embedded case still needs animation because the container can still animate + // the starting window together, e.g. CLOSE or CHANGE type. + continue; + } + if (change.hasFlags(TransitionInfo.FLAGS_IS_NON_APP_WINDOW)) { + // Wallpaper, IME, and system windows don't need any default animations. + continue; + } final boolean isTask = change.getTaskInfo() != null; boolean isSeamlessDisplayChange = false; 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 { @@ -393,18 +383,33 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { continue; } // No default animation for this, so just update bounds/position. + final int rootIdx = TransitionUtil.rootIndexFor(change, info); startTransaction.setPosition(change.getLeash(), - change.getEndAbsBounds().left - info.getRootOffset().x, - change.getEndAbsBounds().top - info.getRootOffset().y); + change.getEndAbsBounds().left - info.getRoot(rootIdx).getOffset().x, + change.getEndAbsBounds().top - info.getRoot(rootIdx).getOffset().y); // Seamless display transition doesn't need to animate. if (isSeamlessDisplayChange) continue; - if (isTask) { - // Skip non-tasks since those usually have null bounds. + if (isTask || (change.hasFlags(FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY) + && !change.hasFlags(FLAG_FILLS_TASK))) { + // Update Task and embedded split window crop bounds, otherwise we may see crop + // on previous bounds during the rotation animation. startTransaction.setWindowCrop(change.getLeash(), change.getEndAbsBounds().width(), change.getEndAbsBounds().height()); } + // Rotation change of independent non display window container. + if (change.getParent() == null + && change.getStartRotation() != change.getEndRotation()) { + startRotationAnimation(startTransaction, change, info, + ROTATION_ANIMATION_ROTATE, animations, onAnimFinish); + continue; + } } + // The back gesture has animated this change before transition happen, so here we don't + // play the animation again. + if (change.hasFlags(FLAG_BACK_GESTURE_ANIMATED)) { + continue; + } // Don't animate anything that isn't independent. if (!TransitionInfo.isIndependent(change, info)) continue; @@ -438,26 +443,11 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { cornerRadius = 0; } - if (a.getShowBackdrop()) { - if (info.getAnimationOptions().getBackgroundColor() != 0) { - // If available use the background color provided through AnimationOptions - backgroundColorForTransition = - info.getAnimationOptions().getBackgroundColor(); - } else if (a.getBackdropColor() != 0) { - // Otherwise fallback on the background color provided through the animation - // definition. - backgroundColorForTransition = a.getBackdropColor(); - } else if (change.getBackgroundColor() != 0) { - // Otherwise default to the window's background color if provided through - // the theme as the background color for the animation - the top most window - // with a valid background color and showBackground set takes precedence. - backgroundColorForTransition = change.getBackgroundColor(); - } - } + backgroundColorForTransition = getTransitionBackgroundColorIfSet(info, change, a, + backgroundColorForTransition); - boolean delayedEdgeExtension = false; if (!isTask && a.hasExtension()) { - if (!Transitions.isOpeningType(change.getMode())) { + if (!TransitionUtil.isOpeningType(change.getMode())) { // Can screenshot now (before startTransaction is applied) edgeExtendWindow(change, a, startTransaction, finishTransaction); } else { @@ -465,27 +455,17 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { // may not be visible or ready yet. postStartTransactionCallbacks .add(t -> edgeExtendWindow(change, a, t, finishTransaction)); - delayedEdgeExtension = true; } } - final Rect clipRect = Transitions.isClosingType(change.getMode()) - ? mRotator.getEndBoundsInStartRotation(change) - : change.getEndAbsBounds(); - - if (delayedEdgeExtension) { - // If the edge extension needs to happen after the startTransition has been - // applied, then we want to only start the animation after the edge extension - // postStartTransaction callback has been run - postStartTransactionCallbacks.add(t -> - startSurfaceAnimation(animations, a, change.getLeash(), onAnimFinish, - mTransactionPool, mMainExecutor, mAnimExecutor, - null /* position */, cornerRadius, clipRect)); - } else { - startSurfaceAnimation(animations, a, change.getLeash(), onAnimFinish, - mTransactionPool, mMainExecutor, mAnimExecutor, null /* position */, - cornerRadius, clipRect); - } + final Rect clipRect = TransitionUtil.isClosingType(change.getMode()) + ? new Rect(mRotator.getEndBoundsInStartRotation(change)) + : new Rect(change.getEndAbsBounds()); + clipRect.offsetTo(0, 0); + + buildSurfaceAnimation(animations, a, change.getLeash(), onAnimFinish, + mTransactionPool, mMainExecutor, change.getEndRelOffset(), cornerRadius, + clipRect); if (info.getAnimationOptions() != null) { attachThumbnail(animations, onAnimFinish, change, info.getAnimationOptions(), @@ -495,23 +475,31 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { } if (backgroundColorForTransition != 0) { - addBackgroundToTransition(info.getRootLeash(), backgroundColorForTransition, - startTransaction, finishTransaction); + for (int i = 0; i < info.getRootCount(); ++i) { + addBackgroundToTransition(info.getRoot(i).getLeash(), backgroundColorForTransition, + startTransaction, finishTransaction); + } } - // postStartTransactionCallbacks require that the start transaction is already - // applied to run otherwise they may result in flickers and UI inconsistencies. - boolean waitForStartTransactionApply = postStartTransactionCallbacks.size() > 0; - startTransaction.apply(waitForStartTransactionApply); - - // Run tasks that require startTransaction to already be applied - for (Consumer<SurfaceControl.Transaction> postStartTransactionCallback : - postStartTransactionCallbacks) { - final SurfaceControl.Transaction t = mTransactionPool.acquire(); - postStartTransactionCallback.accept(t); - t.apply(); - mTransactionPool.release(t); + if (postStartTransactionCallbacks.size() > 0) { + // postStartTransactionCallbacks require that the start transaction is already + // applied to run otherwise they may result in flickers and UI inconsistencies. + startTransaction.apply(true /* sync */); + // startTransaction is empty now, so fill it with the edge-extension setup + for (Consumer<SurfaceControl.Transaction> postStartTransactionCallback : + postStartTransactionCallbacks) { + postStartTransactionCallback.accept(startTransaction); + } } + startTransaction.apply(); + + // now start animations. they are started on another thread, so we have to post them + // *after* applying the startTransaction + mAnimExecutor.execute(() -> { + for (int i = 0; i < animations.size(); ++i) { + animations.get(i).start(); + } + }); mRotator.cleanUp(finishTransaction); TransitionMetrics.getInstance().reportAnimationStart(transition); @@ -520,138 +508,43 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { return true; } - private void edgeExtendWindow(TransitionInfo.Change change, - Animation a, SurfaceControl.Transaction startTransaction, - SurfaceControl.Transaction finishTransaction) { - final Transformation transformationAtStart = new Transformation(); - a.getTransformationAt(0, transformationAtStart); - final Transformation transformationAtEnd = new Transformation(); - a.getTransformationAt(1, transformationAtEnd); - - // We want to create an extension surface that is the maximal size and the animation will - // take care of cropping any part that overflows. - final Insets maxExtensionInsets = Insets.min( - transformationAtStart.getInsets(), transformationAtEnd.getInsets()); - - final int targetSurfaceHeight = Math.max(change.getStartAbsBounds().height(), - change.getEndAbsBounds().height()); - final int targetSurfaceWidth = Math.max(change.getStartAbsBounds().width(), - change.getEndAbsBounds().width()); - if (maxExtensionInsets.left < 0) { - final Rect edgeBounds = new Rect(0, 0, 1, targetSurfaceHeight); - final Rect extensionRect = new Rect(0, 0, - -maxExtensionInsets.left, targetSurfaceHeight); - final int xPos = maxExtensionInsets.left; - final int yPos = 0; - createExtensionSurface(change.getLeash(), edgeBounds, extensionRect, xPos, yPos, - "Left Edge Extension", startTransaction, finishTransaction); - } - - if (maxExtensionInsets.top < 0) { - final Rect edgeBounds = new Rect(0, 0, targetSurfaceWidth, 1); - final Rect extensionRect = new Rect(0, 0, - targetSurfaceWidth, -maxExtensionInsets.top); - final int xPos = 0; - final int yPos = maxExtensionInsets.top; - createExtensionSurface(change.getLeash(), edgeBounds, extensionRect, xPos, yPos, - "Top Edge Extension", startTransaction, finishTransaction); - } - - if (maxExtensionInsets.right < 0) { - final Rect edgeBounds = new Rect(targetSurfaceWidth - 1, 0, - targetSurfaceWidth, targetSurfaceHeight); - final Rect extensionRect = new Rect(0, 0, - -maxExtensionInsets.right, targetSurfaceHeight); - final int xPos = targetSurfaceWidth; - final int yPos = 0; - createExtensionSurface(change.getLeash(), edgeBounds, extensionRect, xPos, yPos, - "Right Edge Extension", startTransaction, finishTransaction); - } - - if (maxExtensionInsets.bottom < 0) { - final Rect edgeBounds = new Rect(0, targetSurfaceHeight - 1, - targetSurfaceWidth, targetSurfaceHeight); - final Rect extensionRect = new Rect(0, 0, - targetSurfaceWidth, -maxExtensionInsets.bottom); - final int xPos = maxExtensionInsets.left; - final int yPos = targetSurfaceHeight; - createExtensionSurface(change.getLeash(), edgeBounds, extensionRect, xPos, yPos, - "Bottom Edge Extension", startTransaction, finishTransaction); + @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 SurfaceControl createExtensionSurface(SurfaceControl surfaceToExtend, Rect edgeBounds, - Rect extensionRect, int xPos, int yPos, String layerName, - SurfaceControl.Transaction startTransaction, - SurfaceControl.Transaction finishTransaction) { - final SurfaceControl edgeExtensionLayer = new SurfaceControl.Builder() - .setName(layerName) - .setParent(surfaceToExtend) - .setHidden(true) - .setCallsite("DefaultTransitionHandler#startAnimation") - .setOpaque(true) - .setBufferSize(extensionRect.width(), extensionRect.height()) - .build(); - - SurfaceControl.LayerCaptureArgs captureArgs = - new SurfaceControl.LayerCaptureArgs.Builder(surfaceToExtend) - .setSourceCrop(edgeBounds) - .setFrameScale(1) - .setPixelFormat(PixelFormat.RGBA_8888) - .setChildrenOnly(true) - .setAllowProtected(true) - .build(); - final SurfaceControl.ScreenshotHardwareBuffer edgeBuffer = - SurfaceControl.captureLayers(captureArgs); - - if (edgeBuffer == null) { - ProtoLog.e(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, - "Failed to capture edge of window."); - return null; + private void startRotationAnimation(SurfaceControl.Transaction startTransaction, + TransitionInfo.Change change, TransitionInfo info, int animHint, + ArrayList<Animator> animations, Runnable onAnimFinish) { + final int rootIdx = TransitionUtil.rootIndexFor(change, info); + final ScreenRotationAnimation anim = new ScreenRotationAnimation(mContext, mSurfaceSession, + mTransactionPool, startTransaction, change, info.getRoot(rootIdx).getLeash(), + 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.buildAnimation(animGroup, finishCallback, mTransitionAnimationScaleSetting, + mMainExecutor); + for (int i = animGroup.size() - 1; i >= 0; i--) { + final Animator animator = animGroup.get(i); + animGroupStore.add(animator); + animations.add(animator); } - - android.graphics.BitmapShader shader = - new android.graphics.BitmapShader(edgeBuffer.asBitmap(), - android.graphics.Shader.TileMode.CLAMP, - android.graphics.Shader.TileMode.CLAMP); - final Paint paint = new Paint(); - paint.setShader(shader); - - final Surface surface = new Surface(edgeExtensionLayer); - Canvas c = surface.lockHardwareCanvas(); - c.drawRect(extensionRect, paint); - surface.unlockCanvasAndPost(c); - surface.release(); - - startTransaction.setLayer(edgeExtensionLayer, Integer.MIN_VALUE); - startTransaction.setPosition(edgeExtensionLayer, xPos, yPos); - startTransaction.setVisibility(edgeExtensionLayer, true); - finishTransaction.remove(edgeExtensionLayer); - - return edgeExtensionLayer; - } - - private void addBackgroundToTransition( - @NonNull SurfaceControl rootLeash, - @ColorInt int color, - @NonNull SurfaceControl.Transaction startTransaction, - @NonNull SurfaceControl.Transaction finishTransaction - ) { - final Color bgColor = Color.valueOf(color); - final float[] colorArray = new float[] { bgColor.red(), bgColor.green(), bgColor.blue() }; - - final SurfaceControl animationBackgroundSurface = new SurfaceControl.Builder() - .setName("Animation Background") - .setParent(rootLeash) - .setColorLayer() - .setOpaque(true) - .build(); - - startTransaction - .setLayer(animationBackgroundSurface, Integer.MIN_VALUE) - .setColor(animationBackgroundSurface, colorArray) - .show(animationBackgroundSurface); - finishTransaction.remove(animationBackgroundSurface); } @Nullable @@ -667,21 +560,20 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { } @Nullable - private Animation loadAnimation(TransitionInfo info, TransitionInfo.Change change, - int wallpaperTransit) { - Animation a = null; + private Animation loadAnimation(@NonNull TransitionInfo info, + @NonNull TransitionInfo.Change change, int wallpaperTransit) { + Animation a; final int type = info.getType(); final int flags = info.getFlags(); final int changeMode = change.getMode(); final int changeFlags = change.getFlags(); - final boolean isOpeningType = Transitions.isOpeningType(type); - final boolean enter = Transitions.isOpeningType(changeMode); + final boolean isOpeningType = TransitionUtil.isOpeningType(type); + final boolean enter = TransitionUtil.isOpeningType(changeMode); final boolean isTask = change.getTaskInfo() != null; final TransitionInfo.AnimationOptions options = info.getAnimationOptions(); final int overrideType = options != null ? options.getType() : ANIM_NONE; - final boolean canCustomContainer = isTask ? !sDisableCustomTaskAnimationProperty : true; - final Rect endBounds = Transitions.isClosingType(changeMode) + final Rect endBounds = TransitionUtil.isClosingType(changeMode) ? mRotator.getEndBoundsInStartRotation(change) : change.getEndAbsBounds(); @@ -703,7 +595,7 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { } else if (type == TRANSIT_RELAUNCH) { a = mTransitionAnimation.createRelaunchAnimation(endBounds, mInsets, endBounds); } else if (overrideType == ANIM_CUSTOM - && (canCustomContainer || options.getOverrideTaskTransition())) { + && (!isTask || options.getOverrideTaskTransition())) { a = mTransitionAnimation.loadAnimationRes(options.getPackageName(), enter ? options.getEnterResId() : options.getExitResId()); } else if (overrideType == ANIM_OPEN_CROSS_PROFILE_APPS && enter) { @@ -724,72 +616,7 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { // This received a transferred starting window, so don't animate return null; } else { - int animAttr = 0; - boolean translucent = false; - if (wallpaperTransit == WALLPAPER_TRANSITION_INTRA_OPEN) { - animAttr = enter - ? R.styleable.WindowAnimation_wallpaperIntraOpenEnterAnimation - : R.styleable.WindowAnimation_wallpaperIntraOpenExitAnimation; - } else if (wallpaperTransit == WALLPAPER_TRANSITION_INTRA_CLOSE) { - animAttr = enter - ? R.styleable.WindowAnimation_wallpaperIntraCloseEnterAnimation - : R.styleable.WindowAnimation_wallpaperIntraCloseExitAnimation; - } else if (wallpaperTransit == WALLPAPER_TRANSITION_OPEN) { - animAttr = enter - ? R.styleable.WindowAnimation_wallpaperOpenEnterAnimation - : R.styleable.WindowAnimation_wallpaperOpenExitAnimation; - } else if (wallpaperTransit == WALLPAPER_TRANSITION_CLOSE) { - animAttr = enter - ? R.styleable.WindowAnimation_wallpaperCloseEnterAnimation - : R.styleable.WindowAnimation_wallpaperCloseExitAnimation; - } else if (type == TRANSIT_OPEN) { - // We will translucent open animation for translucent activities and tasks. Choose - // WindowAnimation_activityOpenEnterAnimation and set translucent here, then - // TransitionAnimation loads appropriate animation later. - if ((changeFlags & FLAG_TRANSLUCENT) != 0 && enter) { - translucent = true; - } - if (isTask && !translucent) { - animAttr = enter - ? R.styleable.WindowAnimation_taskOpenEnterAnimation - : R.styleable.WindowAnimation_taskOpenExitAnimation; - } else { - animAttr = enter - ? R.styleable.WindowAnimation_activityOpenEnterAnimation - : R.styleable.WindowAnimation_activityOpenExitAnimation; - } - } else if (type == TRANSIT_TO_FRONT) { - animAttr = enter - ? R.styleable.WindowAnimation_taskToFrontEnterAnimation - : R.styleable.WindowAnimation_taskToFrontExitAnimation; - } else if (type == TRANSIT_CLOSE) { - if (isTask) { - animAttr = enter - ? R.styleable.WindowAnimation_taskCloseEnterAnimation - : R.styleable.WindowAnimation_taskCloseExitAnimation; - } else { - if ((changeFlags & FLAG_TRANSLUCENT) != 0 && !enter) { - translucent = true; - } - animAttr = enter - ? R.styleable.WindowAnimation_activityCloseEnterAnimation - : R.styleable.WindowAnimation_activityCloseExitAnimation; - } - } else if (type == TRANSIT_TO_BACK) { - animAttr = enter - ? R.styleable.WindowAnimation_taskToBackEnterAnimation - : R.styleable.WindowAnimation_taskToBackExitAnimation; - } - - if (animAttr != 0) { - if (overrideType == ANIM_FROM_STYLE && canCustomContainer) { - a = mTransitionAnimation - .loadAnimationAttr(options.getPackageName(), options.getAnimations(), - animAttr, translucent); - } else { - a = mTransitionAnimation.loadDefaultAnimationAttr(animAttr, translucent); - } - } + a = loadAttributeAnimation(info, change, wallpaperTransit, mTransitionAnimation); } if (a != null) { @@ -804,11 +631,12 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { return a; } - static void startSurfaceAnimation(@NonNull ArrayList<Animator> animations, + /** Builds an animator for the surface and adds it to the `animations` list. */ + static void buildSurfaceAnimation(@NonNull ArrayList<Animator> animations, @NonNull Animation anim, @NonNull SurfaceControl leash, @NonNull Runnable finishCallback, @NonNull TransactionPool pool, - @NonNull ShellExecutor mainExecutor, @NonNull ShellExecutor animExecutor, - @Nullable Point position, float cornerRadius, @Nullable Rect clipRect) { + @NonNull ShellExecutor mainExecutor, @Nullable Point position, float cornerRadius, + @Nullable Rect clipRect) { final SurfaceControl.Transaction transaction = pool.acquire(); final ValueAnimator va = ValueAnimator.ofFloat(0f, 1f); final Transformation transformation = new Transformation(); @@ -816,12 +644,13 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { // Animation length is already expected to be scaled. va.overrideDurationScale(1.0f); va.setDuration(anim.computeDurationHint()); - va.addUpdateListener(animation -> { + final ValueAnimator.AnimatorUpdateListener updateListener = animation -> { final long currentPlayTime = Math.min(va.getDuration(), va.getCurrentPlayTime()); applyTransformation(currentPlayTime, transaction, leash, anim, transformation, matrix, position, cornerRadius, clipRect); - }); + }; + va.addUpdateListener(updateListener); final Runnable finisher = () -> { applyTransformation(va.getDuration(), transaction, leash, anim, transformation, matrix, @@ -834,28 +663,42 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { }); }; va.addListener(new AnimatorListenerAdapter() { + // It is possible for the end/cancel to be called more than once, which may cause + // issues if the animating surface has already been released. Track the finished + // state here to skip duplicate callbacks. See b/252872225. + private boolean mFinished = false; + @Override public void onAnimationEnd(Animator animation) { - finisher.run(); + onFinish(); } @Override public void onAnimationCancel(Animator animation) { + onFinish(); + } + + private void onFinish() { + if (mFinished) return; + mFinished = true; finisher.run(); + // The update listener can continue to be called after the animation has ended if + // end() is called manually again before the finisher removes the animation. + // Remove it manually here to prevent animating a released surface. + // See b/252872225. + va.removeUpdateListener(updateListener); } }); animations.add(va); - animExecutor.execute(va::start); } 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()); + final boolean isOpen = TransitionUtil.isOpeningType(change.getMode()); + final boolean isClose = TransitionUtil.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 +713,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) { @@ -895,9 +743,8 @@ 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), - cornerRadius, change.getEndAbsBounds()); + buildSurfaceAnimation(animations, a, wt.getSurface(), finisher, mTransactionPool, + mMainExecutor, change.getEndRelOffset(), cornerRadius, change.getEndAbsBounds()); } private void attachThumbnailAnimation(@NonNull ArrayList<Animator> animations, @@ -920,9 +767,8 @@ 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 */, - cornerRadius, change.getEndAbsBounds()); + buildSurfaceAnimation(animations, a, wt.getSurface(), finisher, mTransactionPool, + mMainExecutor, change.getEndRelOffset(), cornerRadius, change.getEndAbsBounds()); } private static int getWallpaperTransitType(TransitionInfo info) { @@ -932,16 +778,16 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { for (int i = info.getChanges().size() - 1; i >= 0; --i) { final TransitionInfo.Change change = info.getChanges().get(i); if ((change.getFlags() & FLAG_SHOW_WALLPAPER) != 0) { - if (Transitions.isOpeningType(change.getMode())) { + if (TransitionUtil.isOpeningType(change.getMode())) { hasOpenWallpaper = true; - } else if (Transitions.isClosingType(change.getMode())) { + } else if (TransitionUtil.isClosingType(change.getMode())) { hasCloseWallpaper = true; } } } if (hasOpenWallpaper && hasCloseWallpaper) { - return Transitions.isOpeningType(info.getType()) + return TransitionUtil.isOpeningType(info.getType()) ? WALLPAPER_TRANSITION_INTRA_OPEN : WALLPAPER_TRANSITION_INTRA_CLOSE; } else if (hasOpenWallpaper) { return WALLPAPER_TRANSITION_OPEN; @@ -954,7 +800,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 +808,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..4e3d220f1ea2 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; @@ -65,7 +63,7 @@ public class OneShotRemoteHandler implements Transitions.TransitionHandler { @NonNull Transitions.TransitionFinishCallback finishCallback) { if (mTransition != transition) return false; ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Using registered One-shot remote" - + " transition %s for %s.", mRemote, transition); + + " transition %s for #%d.", mRemote, info.getDebugId()); final IBinder.DeathRecipient remoteDied = () -> { Log.e(Transitions.TAG, "Remote transition died, finishing"); @@ -79,26 +77,28 @@ public class OneShotRemoteHandler implements Transitions.TransitionHandler { if (mRemote.asBinder() != null) { mRemote.asBinder().unlinkToDeath(remoteDied, 0 /* flags */); } + if (sct != null) { + finishTransaction.merge(sct); + } mMainExecutor.execute(() -> { - if (sct != null) { - finishTransaction.merge(sct); - } finishCallback.onTransitionFinished(wct, null /* wctCB */); }); } }; + 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); + // If the remote is actually in the same process, then make a copy of parameters since + // remote impls assume that they have to clean-up native references. + final SurfaceControl.Transaction remoteStartT = RemoteTransitionHandler.copyIfLocal( + startTransaction, mRemote.getRemoteTransition()); + final TransitionInfo remoteInfo = + remoteStartT == startTransaction ? info : info.localRemoteCopy(); + mRemote.getRemoteTransition().startAnimation(transition, remoteInfo, remoteStartT, 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) { @@ -113,19 +113,27 @@ public class OneShotRemoteHandler implements Transitions.TransitionHandler { public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget, @NonNull Transitions.TransitionFinishCallback finishCallback) { - ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Using registered One-shot remote" - + " transition %s for %s.", mRemote, transition); - IRemoteTransitionFinishedCallback cb = new IRemoteTransitionFinishedCallback.Stub() { @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 */)); } }; try { - mRemote.getRemoteTransition().mergeAnimation(transition, info, t, mergeTarget, cb); + // If the remote is actually in the same process, then make a copy of parameters since + // remote impls assume that they have to clean-up native references. + final SurfaceControl.Transaction remoteT = + RemoteTransitionHandler.copyIfLocal(t, mRemote.getRemoteTransition()); + final TransitionInfo remoteInfo = remoteT == t ? info : info.localRemoteCopy(); + mRemote.getRemoteTransition().mergeAnimation( + transition, remoteInfo, remoteT, mergeTarget, cb); } catch (RemoteException e) { Log.e(Transitions.TAG, "Error merging remote transition.", e); } @@ -143,4 +151,10 @@ public class OneShotRemoteHandler implements Transitions.TransitionHandler { + " for %s: %s", transition, remote); return new WindowContainerTransaction(); } + + @Override + public String toString() { + return "OneShotRemoteHandler:" + mRemote.getDebugName() + ":" + + mRemote.getRemoteTransition(); + } } 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..5b7231c5a5fb 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,8 +18,8 @@ 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.Parcel; import android.os.RemoteException; import android.util.ArrayMap; import android.util.Log; @@ -39,6 +39,7 @@ import androidx.annotation.BinderThread; import com.android.internal.protolog.common.ProtoLog; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.protolog.ShellProtoLogGroup; +import com.android.wm.shell.util.TransitionUtil; import java.util.ArrayList; @@ -83,7 +84,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); } @@ -92,10 +94,15 @@ public class RemoteTransitionHandler implements Transitions.TransitionHandler { @NonNull SurfaceControl.Transaction startTransaction, @NonNull SurfaceControl.Transaction finishTransaction, @NonNull Transitions.TransitionFinishCallback finishCallback) { + if (!Transitions.SHELL_TRANSITIONS_ROTATION && TransitionUtil.hasDisplayChange(info)) { + // Note that if the remote doesn't have permission ACCESS_SURFACE_FLINGER, some + // operations of the start transaction may be ignored. + return false; + } RemoteTransition pendingRemote = mRequestedRemotes.get(transition); if (pendingRemote == null) { - ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Transition %s doesn't have " - + "explicit remote, search filters for match for %s", transition, info); + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Transition doesn't have " + + "explicit remote, search filters for match for %s", info); // If no explicit remote, search filters until one matches for (int i = mFilters.size() - 1; i >= 0; --i) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Checking filter %s", @@ -109,8 +116,8 @@ public class RemoteTransitionHandler implements Transitions.TransitionHandler { } } } - ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Delegate animation for %s to %s", - transition, pendingRemote); + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Delegate animation for #%d to %s", + info.getDebugId(), pendingRemote); if (pendingRemote == null) return false; @@ -120,25 +127,27 @@ public class RemoteTransitionHandler implements Transitions.TransitionHandler { public void onTransitionFinished(WindowContainerTransaction wct, SurfaceControl.Transaction sct) { unhandleDeath(remote.asBinder(), finishCallback); + if (sct != null) { + finishTransaction.merge(sct); + } mMainExecutor.execute(() -> { - if (sct != null) { - finishTransaction.merge(sct); - } mRequestedRemotes.remove(transition); finishCallback.onTransitionFinished(wct, null /* wctCB */); }); } }; + Transitions.setRunningRemoteTransitionDelegate(remote.getAppThread()); try { + // If the remote is actually in the same process, then make a copy of parameters since + // remote impls assume that they have to clean-up native references. + final SurfaceControl.Transaction remoteStartT = + copyIfLocal(startTransaction, remote.getRemoteTransition()); + final TransitionInfo remoteInfo = + remoteStartT == startTransaction ? info : info.localRemoteCopy(); 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); + remote.getRemoteTransition().startAnimation(transition, remoteInfo, remoteStartT, 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); @@ -149,19 +158,47 @@ public class RemoteTransitionHandler implements Transitions.TransitionHandler { return true; } + static SurfaceControl.Transaction copyIfLocal(SurfaceControl.Transaction t, + IRemoteTransition remote) { + // We care more about parceling than local (though they should be the same); so, use + // queryLocalInterface since that's what Binder uses to decide if it needs to parcel. + if (remote.asBinder().queryLocalInterface(IRemoteTransition.DESCRIPTOR) == null) { + // No local interface, so binder itself will parcel and thus we don't need to. + return t; + } + // Binder won't be parceling; however, the remotes assume they have their own native + // objects (and don't know if caller is local or not), so we need to make a COPY here so + // that the remote can clean it up without clearing the original transaction. + // Since there's no direct `copy` for Transaction, we have to parcel/unparcel instead. + final Parcel p = Parcel.obtain(); + try { + t.writeToParcel(p, 0); + p.setDataPosition(0); + return SurfaceControl.Transaction.CREATOR.createFromParcel(p); + } finally { + p.recycle(); + } + } + @Override public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget, @NonNull Transitions.TransitionFinishCallback finishCallback) { - final IRemoteTransition remote = mRequestedRemotes.get(mergeTarget).getRemoteTransition(); - ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Attempt merge %s into %s", - transition, remote); + final RemoteTransition remoteTransition = mRequestedRemotes.get(mergeTarget); + final IRemoteTransition remote = remoteTransition.getRemoteTransition(); + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Merge into remote: %s", + remoteTransition); if (remote == null) return; IRemoteTransitionFinishedCallback cb = new IRemoteTransitionFinishedCallback.Stub() { @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 " @@ -174,7 +211,11 @@ public class RemoteTransitionHandler implements Transitions.TransitionHandler { } }; try { - remote.mergeAnimation(transition, info, t, mergeTarget, cb); + // If the remote is actually in the same process, then make a copy of parameters since + // remote impls assume that they have to clean-up native references. + final SurfaceControl.Transaction remoteT = copyIfLocal(t, remote); + final TransitionInfo remoteInfo = remoteT == t ? info : info.localRemoteCopy(); + remote.mergeAnimation(transition, remoteInfo, remoteT, mergeTarget, cb); } catch (RemoteException e) { Log.e(Transitions.TAG, "Error attempting to merge remote transition.", e); } 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..d25318df6b6a 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 @@ -16,14 +16,12 @@ package com.android.wm.shell.transition; -import static android.hardware.HardwareBuffer.RGBA_8888; -import static android.hardware.HardwareBuffer.USAGE_PROTECTED_CONTENT; import static android.util.RotationUtils.deltaRotation; import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_CROSSFADE; import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_JUMPCUT; import static android.view.WindowManagerPolicyConstants.SCREEN_FREEZE_LAYER_BASE; -import static com.android.wm.shell.transition.DefaultTransitionHandler.startSurfaceAnimation; +import static com.android.wm.shell.transition.DefaultTransitionHandler.buildSurfaceAnimation; import static com.android.wm.shell.transition.Transitions.TAG; import android.animation.Animator; @@ -33,13 +31,9 @@ import android.animation.ValueAnimator; import android.annotation.NonNull; import android.content.Context; import android.graphics.Color; -import android.graphics.ColorSpace; -import android.graphics.GraphicBuffer; import android.graphics.Matrix; import android.graphics.Rect; import android.hardware.HardwareBuffer; -import android.media.Image; -import android.media.ImageReader; import android.util.Slog; import android.view.Surface; import android.view.SurfaceControl; @@ -47,15 +41,15 @@ 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; +import com.android.internal.policy.TransitionAnimation; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.TransactionPool; -import java.nio.ByteBuffer; import java.util.ArrayList; -import java.util.Arrays; /** * This class handles the rotation animation when the device is rotated. @@ -85,12 +79,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 +98,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 +126,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 +134,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()); + TransitionAnimation.configureScreenshotLayer(t, mScreenshotLayer, screenshotBuffer); + final HardwareBuffer hardwareBuffer = screenshotBuffer.getHardwareBuffer(); + t.show(mScreenshotLayer); + if (!isCustomRotate()) { + mStartLuma = TransitionAnimation.getBorderLuma(hardwareBuffer, + screenshotBuffer.getColorSpace()); + } + hardwareBuffer.close(); + } 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 +194,7 @@ class ScreenRotationAnimation { Slog.w(TAG, "Unable to allocate freeze surface", e); } - setRotation(t); + setScreenshotTransform(t); t.apply(); } @@ -210,19 +202,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 / mStartWidth, + (float) mEndHeight / mStartHeight); + matrix.setScale(scale, scale); + } matrix.getValues(mTmpFloats); float x = mTmpFloats[Matrix.MTRANS_X]; float y = mTmpFloats[Matrix.MTRANS_Y]; @@ -230,17 +239,14 @@ 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); } /** - * Returns true if animating. + * Returns true if any animations were added to `animations`. */ - public boolean startAnimation(@NonNull ArrayList<Animator> animations, + boolean buildAnimation(@NonNull ArrayList<Animator> animations, @NonNull Runnable finishCallback, float animationScale, - @NonNull ShellExecutor mainExecutor, @NonNull ShellExecutor animExecutor) { + @NonNull ShellExecutor mainExecutor) { if (mScreenshotLayer == null) { // Can't do animation. return false; @@ -298,19 +304,16 @@ 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); mRotateAlphaAnimation.scaleCurrentDuration(animationScale); - startScreenshotAlphaAnimation(animations, finishCallback, mainExecutor, - animExecutor); - startDisplayRotation(animations, finishCallback, mainExecutor, animExecutor); + buildScreenshotAlphaAnimation(animations, finishCallback, mainExecutor); + startDisplayRotation(animations, finishCallback, mainExecutor); } else { - startDisplayRotation(animations, finishCallback, mainExecutor, animExecutor); - startScreenshotRotationAnimation(animations, finishCallback, mainExecutor, - animExecutor); + startDisplayRotation(animations, finishCallback, mainExecutor); + startScreenshotRotationAnimation(animations, finishCallback, mainExecutor); //startColorAnimation(mTransaction, animationScale); } @@ -318,27 +321,24 @@ class ScreenRotationAnimation { } private void startDisplayRotation(@NonNull ArrayList<Animator> animations, - @NonNull Runnable finishCallback, @NonNull ShellExecutor mainExecutor, - @NonNull ShellExecutor animExecutor) { - startSurfaceAnimation(animations, mRotateEnterAnimation, mSurfaceControl, finishCallback, - mTransactionPool, mainExecutor, animExecutor, null /* position */, - 0 /* cornerRadius */, null /* clipRect */); + @NonNull Runnable finishCallback, @NonNull ShellExecutor mainExecutor) { + buildSurfaceAnimation(animations, mRotateEnterAnimation, mSurfaceControl, finishCallback, + mTransactionPool, mainExecutor, null /* position */, 0 /* cornerRadius */, + null /* clipRect */); } private void startScreenshotRotationAnimation(@NonNull ArrayList<Animator> animations, - @NonNull Runnable finishCallback, @NonNull ShellExecutor mainExecutor, - @NonNull ShellExecutor animExecutor) { - startSurfaceAnimation(animations, mRotateExitAnimation, mAnimLeash, finishCallback, - mTransactionPool, mainExecutor, animExecutor, null /* position */, - 0 /* cornerRadius */, null /* clipRect */); + @NonNull Runnable finishCallback, @NonNull ShellExecutor mainExecutor) { + buildSurfaceAnimation(animations, mRotateExitAnimation, mAnimLeash, finishCallback, + mTransactionPool, mainExecutor, null /* position */, 0 /* cornerRadius */, + null /* clipRect */); } - private void startScreenshotAlphaAnimation(@NonNull ArrayList<Animator> animations, - @NonNull Runnable finishCallback, @NonNull ShellExecutor mainExecutor, - @NonNull ShellExecutor animExecutor) { - startSurfaceAnimation(animations, mRotateAlphaAnimation, mAnimLeash, finishCallback, - mTransactionPool, mainExecutor, animExecutor, null /* position */, - 0 /* cornerRadius */, null /* clipRect */); + private void buildScreenshotAlphaAnimation(@NonNull ArrayList<Animator> animations, + @NonNull Runnable finishCallback, @NonNull ShellExecutor mainExecutor) { + buildSurfaceAnimation(animations, mRotateAlphaAnimation, mAnimLeash, finishCallback, + mTransactionPool, mainExecutor, null /* position */, 0 /* cornerRadius */, + null /* clipRect */); } private void startColorAnimation(float animationScale, @NonNull ShellExecutor animExecutor) { @@ -378,135 +378,21 @@ 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); } - /** - * Converts the provided {@link HardwareBuffer} and converts it to a bitmap to then sample the - * luminance at the borders of the bitmap - * @return the average luminance of all the pixels at the borders of the bitmap - */ - private static float getMedianBorderLuma(HardwareBuffer hardwareBuffer, ColorSpace colorSpace) { - // Cannot read content from buffer with protected usage. - if (hardwareBuffer == null || hardwareBuffer.getFormat() != RGBA_8888 - || hasProtectedContent(hardwareBuffer)) { - return 0; - } - - ImageReader ir = ImageReader.newInstance(hardwareBuffer.getWidth(), - hardwareBuffer.getHeight(), hardwareBuffer.getFormat(), 1); - ir.getSurface().attachAndQueueBufferWithColorSpace(hardwareBuffer, colorSpace); - Image image = ir.acquireLatestImage(); - if (image == null || image.getPlanes().length == 0) { - return 0; - } - - Image.Plane plane = image.getPlanes()[0]; - ByteBuffer buffer = plane.getBuffer(); - int width = image.getWidth(); - int height = image.getHeight(); - int pixelStride = plane.getPixelStride(); - int rowStride = plane.getRowStride(); - float[] borderLumas = new float[2 * width + 2 * height]; - - // Grab the top and bottom borders - int l = 0; - for (int x = 0; x < width; x++) { - borderLumas[l++] = getPixelLuminance(buffer, x, 0, pixelStride, rowStride); - borderLumas[l++] = getPixelLuminance(buffer, x, height - 1, pixelStride, rowStride); - } - - // Grab the left and right borders - for (int y = 0; y < height; y++) { - borderLumas[l++] = getPixelLuminance(buffer, 0, y, pixelStride, rowStride); - borderLumas[l++] = getPixelLuminance(buffer, width - 1, y, pixelStride, rowStride); - } - - // Cleanup - ir.close(); - - // Oh, is this too simple and inefficient for you? - // How about implementing a O(n) solution? https://en.wikipedia.org/wiki/Median_of_medians - Arrays.sort(borderLumas); - return borderLumas[borderLumas.length / 2]; - } - - /** - * @return whether the hardwareBuffer passed in is marked as protected. - */ - private static boolean hasProtectedContent(HardwareBuffer hardwareBuffer) { - return (hardwareBuffer.getUsage() & USAGE_PROTECTED_CONTENT) == USAGE_PROTECTED_CONTENT; - } - - private static float getPixelLuminance(ByteBuffer buffer, int x, int y, - int pixelStride, int rowStride) { - int offset = y * rowStride + x * pixelStride; - int pixel = 0; - pixel |= (buffer.get(offset) & 0xff) << 16; // R - pixel |= (buffer.get(offset + 1) & 0xff) << 8; // G - pixel |= (buffer.get(offset + 2) & 0xff); // B - pixel |= (buffer.get(offset + 3) & 0xff) << 24; // A - return Color.valueOf(pixel).luminance(); - } - - /** - * Gets the average border luma by taking a screenshot of the {@param surfaceControl}. - * @see #getMedianBorderLuma(HardwareBuffer, ColorSpace) - */ - private static float getLumaOfSurfaceControl(Rect bounds, SurfaceControl surfaceControl) { - if (surfaceControl == null) { - return 0; - } - - Rect crop = new Rect(0, 0, bounds.width(), bounds.height()); - SurfaceControl.ScreenshotHardwareBuffer buffer = - SurfaceControl.captureLayers(surfaceControl, crop, 1); - if (buffer == null) { - return 0; - } - - return getMedianBorderLuma(buffer.getHardwareBuffer(), buffer.getColorSpace()); - } - - private static void createRotationMatrix(int rotation, int width, int height, - Matrix outMatrix) { - switch (rotation) { - case Surface.ROTATION_0: - outMatrix.reset(); - break; - case Surface.ROTATION_90: - outMatrix.setRotate(90, 0, 0); - outMatrix.postTranslate(height, 0); - break; - case Surface.ROTATION_180: - outMatrix.setRotate(180, 0, 0); - outMatrix.postTranslate(width, height); - break; - case Surface.ROTATION_270: - outMatrix.setRotate(270, 0, 0); - outMatrix.postTranslate(0, width); - break; - } - } - private static void applyColor(int startColor, int endColor, float[] rgbFloat, float fraction, SurfaceControl surface, SurfaceControl.Transaction t) { final int color = (Integer) ArgbEvaluator.getInstance().evaluate(fraction, startColor, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ShellTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ShellTransitions.java index b34049d4ec42..da39017a0313 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ShellTransitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ShellTransitions.java @@ -27,14 +27,6 @@ import com.android.wm.shell.common.annotations.ExternalThread; */ @ExternalThread public interface ShellTransitions { - - /** - * Returns a binder that can be passed to an external process to manipulate remote transitions. - */ - default IShellTransitions createExternalInterface() { - return null; - } - /** * Registers a remote transition. */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/SleepHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/SleepHandler.java new file mode 100644 index 000000000000..1879bf721fed --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/SleepHandler.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.transition; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.IBinder; +import android.view.SurfaceControl; +import android.window.TransitionInfo; +import android.window.TransitionRequestInfo; +import android.window.WindowContainerTransaction; + +import java.util.ArrayList; + +/** + * A Simple handler that tracks SLEEP transitions. We track them specially since we (ab)use these + * as sentinels for fast-forwarding through animations when the screen is off. + * + * There should only be one SleepHandler and it is used explicitly by {@link Transitions} so we + * don't register it like a normal handler. + */ +class SleepHandler implements Transitions.TransitionHandler { + final ArrayList<IBinder> mSleepTransitions = new ArrayList<>(); + + @Override + public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, + @NonNull Transitions.TransitionFinishCallback finishCallback) { + startTransaction.apply(); + finishCallback.onTransitionFinished(null, null); + mSleepTransitions.remove(transition); + return true; + } + + @Override + @Nullable + public WindowContainerTransaction handleRequest(@NonNull IBinder transition, + @NonNull TransitionRequestInfo request) { + mSleepTransitions.add(transition); + return new WindowContainerTransaction(); + } + + @Override + public void onTransitionConsumed(@NonNull IBinder transition, boolean aborted, + @Nullable SurfaceControl.Transaction finishTransaction) { + mSleepTransitions.remove(transition); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java new file mode 100644 index 000000000000..bcc37baa5b00 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java @@ -0,0 +1,376 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.transition; + +import static android.app.ActivityOptions.ANIM_FROM_STYLE; +import static android.app.ActivityOptions.ANIM_NONE; +import static android.app.WindowConfiguration.ACTIVITY_TYPE_DREAM; +import static android.view.WindowManager.TRANSIT_CLOSE; +import static android.view.WindowManager.TRANSIT_OPEN; +import static android.view.WindowManager.TRANSIT_TO_BACK; +import static android.view.WindowManager.TRANSIT_TO_FRONT; +import static android.view.WindowManager.transitTypeToString; +import static android.window.TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT; +import static android.window.TransitionInfo.FLAG_TRANSLUCENT; + +import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_CLOSE; +import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_INTRA_CLOSE; +import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_INTRA_OPEN; +import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_OPEN; + +import android.annotation.ColorInt; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.graphics.BitmapShader; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Insets; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.Shader; +import android.view.Surface; +import android.view.SurfaceControl; +import android.view.animation.Animation; +import android.view.animation.Transformation; +import android.window.ScreenCapture; +import android.window.TransitionInfo; + +import com.android.internal.R; +import com.android.internal.policy.TransitionAnimation; +import com.android.internal.protolog.common.ProtoLog; +import com.android.wm.shell.protolog.ShellProtoLogGroup; +import com.android.wm.shell.util.TransitionUtil; + +/** The helper class that provides methods for adding styles to transition animations. */ +public class TransitionAnimationHelper { + + /** Loads the animation that is defined through attribute id for the given transition. */ + @Nullable + public static Animation loadAttributeAnimation(@NonNull TransitionInfo info, + @NonNull TransitionInfo.Change change, int wallpaperTransit, + @NonNull TransitionAnimation transitionAnimation) { + final int type = info.getType(); + final int changeMode = change.getMode(); + final int changeFlags = change.getFlags(); + final boolean enter = TransitionUtil.isOpeningType(changeMode); + final boolean isTask = change.getTaskInfo() != null; + final TransitionInfo.AnimationOptions options = info.getAnimationOptions(); + final int overrideType = options != null ? options.getType() : ANIM_NONE; + final boolean isDream = + isTask && change.getTaskInfo().topActivityType == ACTIVITY_TYPE_DREAM; + int animAttr = 0; + boolean translucent = false; + if (isDream) { + if (type == TRANSIT_OPEN) { + animAttr = enter + ? R.styleable.WindowAnimation_dreamActivityOpenEnterAnimation + : R.styleable.WindowAnimation_dreamActivityOpenExitAnimation; + } else if (type == TRANSIT_CLOSE) { + animAttr = enter + ? 0 + : R.styleable.WindowAnimation_dreamActivityCloseExitAnimation; + } + } else if (wallpaperTransit == WALLPAPER_TRANSITION_INTRA_OPEN) { + animAttr = enter + ? R.styleable.WindowAnimation_wallpaperIntraOpenEnterAnimation + : R.styleable.WindowAnimation_wallpaperIntraOpenExitAnimation; + } else if (wallpaperTransit == WALLPAPER_TRANSITION_INTRA_CLOSE) { + animAttr = enter + ? R.styleable.WindowAnimation_wallpaperIntraCloseEnterAnimation + : R.styleable.WindowAnimation_wallpaperIntraCloseExitAnimation; + } else if (wallpaperTransit == WALLPAPER_TRANSITION_OPEN) { + animAttr = enter + ? R.styleable.WindowAnimation_wallpaperOpenEnterAnimation + : R.styleable.WindowAnimation_wallpaperOpenExitAnimation; + } else if (wallpaperTransit == WALLPAPER_TRANSITION_CLOSE) { + animAttr = enter + ? R.styleable.WindowAnimation_wallpaperCloseEnterAnimation + : R.styleable.WindowAnimation_wallpaperCloseExitAnimation; + } else if (type == TRANSIT_OPEN) { + // We will translucent open animation for translucent activities and tasks. Choose + // WindowAnimation_activityOpenEnterAnimation and set translucent here, then + // TransitionAnimation loads appropriate animation later. + if ((changeFlags & FLAG_TRANSLUCENT) != 0 && enter) { + translucent = true; + } + if (isTask && !translucent) { + animAttr = enter + ? R.styleable.WindowAnimation_taskOpenEnterAnimation + : R.styleable.WindowAnimation_taskOpenExitAnimation; + } else { + animAttr = enter + ? R.styleable.WindowAnimation_activityOpenEnterAnimation + : R.styleable.WindowAnimation_activityOpenExitAnimation; + } + } else if (type == TRANSIT_TO_FRONT) { + animAttr = enter + ? R.styleable.WindowAnimation_taskToFrontEnterAnimation + : R.styleable.WindowAnimation_taskToFrontExitAnimation; + } else if (type == TRANSIT_CLOSE) { + if (isTask) { + animAttr = enter + ? R.styleable.WindowAnimation_taskCloseEnterAnimation + : R.styleable.WindowAnimation_taskCloseExitAnimation; + } else { + if ((changeFlags & FLAG_TRANSLUCENT) != 0 && !enter) { + translucent = true; + } + animAttr = enter + ? R.styleable.WindowAnimation_activityCloseEnterAnimation + : R.styleable.WindowAnimation_activityCloseExitAnimation; + } + } else if (type == TRANSIT_TO_BACK) { + animAttr = enter + ? R.styleable.WindowAnimation_taskToBackEnterAnimation + : R.styleable.WindowAnimation_taskToBackExitAnimation; + } + + Animation a = null; + if (animAttr != 0) { + if (overrideType == ANIM_FROM_STYLE && !isTask) { + final TransitionInfo.AnimationOptions.CustomActivityTransition customTransition = + getCustomActivityTransition(animAttr, options); + if (customTransition != null) { + a = loadCustomActivityTransition( + customTransition, options, enter, transitionAnimation); + } else { + a = transitionAnimation + .loadAnimationAttr(options.getPackageName(), options.getAnimations(), + animAttr, translucent); + } + } else { + a = transitionAnimation.loadDefaultAnimationAttr(animAttr, translucent); + } + } + + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, + "loadAnimation: anim=%s animAttr=0x%x type=%s isEntrance=%b", a, animAttr, + transitTypeToString(type), + enter); + return a; + } + + static TransitionInfo.AnimationOptions.CustomActivityTransition getCustomActivityTransition( + int animAttr, TransitionInfo.AnimationOptions options) { + boolean isOpen = false; + switch (animAttr) { + case R.styleable.WindowAnimation_activityOpenEnterAnimation: + case R.styleable.WindowAnimation_activityOpenExitAnimation: + isOpen = true; + break; + case R.styleable.WindowAnimation_activityCloseEnterAnimation: + case R.styleable.WindowAnimation_activityCloseExitAnimation: + break; + default: + return null; + } + + return options.getCustomActivityTransition(isOpen); + } + + static Animation loadCustomActivityTransition( + @NonNull TransitionInfo.AnimationOptions.CustomActivityTransition transitionAnim, + TransitionInfo.AnimationOptions options, boolean enter, + TransitionAnimation transitionAnimation) { + final Animation a = transitionAnimation.loadAppTransitionAnimation(options.getPackageName(), + enter ? transitionAnim.getCustomEnterResId() + : transitionAnim.getCustomExitResId()); + if (a != null && transitionAnim.getCustomBackgroundColor() != 0) { + a.setBackdropColor(transitionAnim.getCustomBackgroundColor()); + } + return a; + } + + /** + * Gets the background {@link ColorInt} for the given transition animation if it is set. + * + * @param defaultColor {@link ColorInt} to return if there is no background color specified by + * the given transition animation. + */ + @ColorInt + public static int getTransitionBackgroundColorIfSet(@NonNull TransitionInfo info, + @NonNull TransitionInfo.Change change, @NonNull Animation a, + @ColorInt int defaultColor) { + if (!a.getShowBackdrop()) { + return defaultColor; + } + if (info.getAnimationOptions() != null + && info.getAnimationOptions().getBackgroundColor() != 0) { + // If available use the background color provided through AnimationOptions + return info.getAnimationOptions().getBackgroundColor(); + } else if (a.getBackdropColor() != 0) { + // Otherwise fallback on the background color provided through the animation + // definition. + return a.getBackdropColor(); + } else if (change.getBackgroundColor() != 0) { + // Otherwise default to the window's background color if provided through + // the theme as the background color for the animation - the top most window + // with a valid background color and showBackground set takes precedence. + return change.getBackgroundColor(); + } + return defaultColor; + } + + /** + * Adds the given {@code backgroundColor} as the background color to the transition animation. + */ + public static void addBackgroundToTransition(@NonNull SurfaceControl rootLeash, + @ColorInt int backgroundColor, @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction) { + if (backgroundColor == 0) { + // No background color. + return; + } + final Color bgColor = Color.valueOf(backgroundColor); + final float[] colorArray = new float[] { bgColor.red(), bgColor.green(), bgColor.blue() }; + final SurfaceControl animationBackgroundSurface = new SurfaceControl.Builder() + .setName("Animation Background") + .setParent(rootLeash) + .setColorLayer() + .setOpaque(true) + .build(); + startTransaction + .setLayer(animationBackgroundSurface, Integer.MIN_VALUE) + .setColor(animationBackgroundSurface, colorArray) + .show(animationBackgroundSurface); + finishTransaction.remove(animationBackgroundSurface); + } + + /** + * Adds edge extension surface to the given {@code change} for edge extension animation. + */ + public static void edgeExtendWindow(@NonNull TransitionInfo.Change change, + @NonNull Animation a, @NonNull SurfaceControl.Transaction startTransaction, + @NonNull 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(); + a.getTransformationAt(1, transformationAtEnd); + + // We want to create an extension surface that is the maximal size and the animation will + // take care of cropping any part that overflows. + final Insets maxExtensionInsets = Insets.min( + transformationAtStart.getInsets(), transformationAtEnd.getInsets()); + + final int targetSurfaceHeight = Math.max(change.getStartAbsBounds().height(), + change.getEndAbsBounds().height()); + final int targetSurfaceWidth = Math.max(change.getStartAbsBounds().width(), + change.getEndAbsBounds().width()); + if (maxExtensionInsets.left < 0) { + final Rect edgeBounds = new Rect(0, 0, 1, targetSurfaceHeight); + final Rect extensionRect = new Rect(0, 0, + -maxExtensionInsets.left, targetSurfaceHeight); + final int xPos = maxExtensionInsets.left; + final int yPos = 0; + createExtensionSurface(change.getLeash(), edgeBounds, extensionRect, xPos, yPos, + "Left Edge Extension", startTransaction, finishTransaction); + } + + if (maxExtensionInsets.top < 0) { + final Rect edgeBounds = new Rect(0, 0, targetSurfaceWidth, 1); + final Rect extensionRect = new Rect(0, 0, + targetSurfaceWidth, -maxExtensionInsets.top); + final int xPos = 0; + final int yPos = maxExtensionInsets.top; + createExtensionSurface(change.getLeash(), edgeBounds, extensionRect, xPos, yPos, + "Top Edge Extension", startTransaction, finishTransaction); + } + + if (maxExtensionInsets.right < 0) { + final Rect edgeBounds = new Rect(targetSurfaceWidth - 1, 0, + targetSurfaceWidth, targetSurfaceHeight); + final Rect extensionRect = new Rect(0, 0, + -maxExtensionInsets.right, targetSurfaceHeight); + final int xPos = targetSurfaceWidth; + final int yPos = 0; + createExtensionSurface(change.getLeash(), edgeBounds, extensionRect, xPos, yPos, + "Right Edge Extension", startTransaction, finishTransaction); + } + + if (maxExtensionInsets.bottom < 0) { + final Rect edgeBounds = new Rect(0, targetSurfaceHeight - 1, + targetSurfaceWidth, targetSurfaceHeight); + final Rect extensionRect = new Rect(0, 0, + targetSurfaceWidth, -maxExtensionInsets.bottom); + final int xPos = maxExtensionInsets.left; + final int yPos = targetSurfaceHeight; + createExtensionSurface(change.getLeash(), edgeBounds, extensionRect, xPos, yPos, + "Bottom Edge Extension", startTransaction, finishTransaction); + } + } + + /** + * Takes a screenshot of {@code surfaceToExtend}'s edge and extends it for edge extension + * animation. + */ + private static SurfaceControl createExtensionSurface(@NonNull SurfaceControl surfaceToExtend, + @NonNull Rect edgeBounds, @NonNull Rect extensionRect, int xPos, int yPos, + @NonNull String layerName, @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction) { + final SurfaceControl edgeExtensionLayer = new SurfaceControl.Builder() + .setName(layerName) + .setParent(surfaceToExtend) + .setHidden(true) + .setCallsite("TransitionAnimationHelper#createExtensionSurface") + .setOpaque(true) + .setBufferSize(extensionRect.width(), extensionRect.height()) + .build(); + + final ScreenCapture.LayerCaptureArgs captureArgs = + new ScreenCapture.LayerCaptureArgs.Builder(surfaceToExtend) + .setSourceCrop(edgeBounds) + .setFrameScale(1) + .setPixelFormat(PixelFormat.RGBA_8888) + .setChildrenOnly(true) + .setAllowProtected(true) + .setCaptureSecureLayers(true) + .build(); + final ScreenCapture.ScreenshotHardwareBuffer edgeBuffer = + ScreenCapture.captureLayers(captureArgs); + + if (edgeBuffer == null) { + ProtoLog.e(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, + "Failed to capture edge of window."); + return null; + } + + final BitmapShader shader = new BitmapShader(edgeBuffer.asBitmap(), + Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); + final Paint paint = new Paint(); + paint.setShader(shader); + + final Surface surface = new Surface(edgeExtensionLayer); + final Canvas c = surface.lockHardwareCanvas(); + c.drawRect(extensionRect, paint); + surface.unlockCanvasAndPost(c); + surface.release(); + + startTransaction.setLayer(edgeExtensionLayer, Integer.MIN_VALUE); + startTransaction.setPosition(edgeExtensionLayer, xPos, yPos); + startTransaction.setVisibility(edgeExtensionLayer, true); + finishTransaction.remove(edgeExtensionLayer); + + return edgeExtensionLayer; + } +} 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..681fa5177da2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java @@ -19,17 +19,27 @@ package com.android.wm.shell.transition; import static android.view.WindowManager.TRANSIT_CHANGE; import static android.view.WindowManager.TRANSIT_CLOSE; import static android.view.WindowManager.TRANSIT_FIRST_CUSTOM; -import static android.view.WindowManager.TRANSIT_KEYGUARD_GOING_AWAY; +import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY; +import static android.view.WindowManager.TRANSIT_KEYGUARD_UNOCCLUDE; import static android.view.WindowManager.TRANSIT_OPEN; +import static android.view.WindowManager.TRANSIT_SLEEP; import static android.view.WindowManager.TRANSIT_TO_BACK; import static android.view.WindowManager.TRANSIT_TO_FRONT; +import static android.view.WindowManager.fixScale; +import static android.window.TransitionInfo.FLAG_IS_OCCLUDED; import static android.window.TransitionInfo.FLAG_IS_WALLPAPER; +import static android.window.TransitionInfo.FLAG_NO_ANIMATION; import static android.window.TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT; import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission; +import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_SHELL_TRANSITIONS; +import static com.android.wm.shell.util.TransitionUtil.isClosingType; +import static com.android.wm.shell.util.TransitionUtil.isOpeningType; 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; @@ -39,6 +49,7 @@ import android.os.RemoteException; import android.os.SystemProperties; import android.provider.Settings; import android.util.Log; +import android.util.Pair; import android.view.SurfaceControl; import android.view.WindowManager; import android.window.ITransitionPlayer; @@ -56,24 +67,44 @@ 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.ExternalInterfaceBinder; import com.android.wm.shell.common.RemoteCallable; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.TransactionPool; import com.android.wm.shell.common.annotations.ExternalThread; import com.android.wm.shell.protolog.ShellProtoLogGroup; +import com.android.wm.shell.sysui.ShellController; +import com.android.wm.shell.sysui.ShellInit; +import com.android.wm.shell.util.TransitionUtil; import java.util.ArrayList; import java.util.Arrays; -/** Plays transition animations */ +/** + * Plays transition animations. Within this player, each transition has a lifecycle. + * 1. When a transition is directly started or requested, it is added to "pending" state. + * 2. Once WMCore applies the transition and notifies, the transition moves to "ready" state. + * 3. When a transition starts animating, it is moved to the "active" state. + * + * Basically: --start--> PENDING --onTransitionReady--> READY --play--> ACTIVE --finish--> | + * --merge--> MERGED --^ + * + * At the moment, only one transition can be animating at a time. While a transition is animating, + * transitions will be queued in the "ready" state for their turn. At the same time, whenever a + * transition makes it to the head of the "ready" queue, it will attempt to merge to with the + * "active" transition. If the merge succeeds, it will be moved to the "active" transition's + * "merged" and then the next "ready" transition can attempt to merge. + * + * Once the "active" transition animation is finished, it will be removed from the "active" list + * and then the next "ready" transition can play. + */ public class Transitions implements RemoteCallable<Transitions> { static final String TAG = "ShellTransitions"; /** Set to {@code true} to enable shell transitions. */ public static final boolean ENABLE_SHELL_TRANSITIONS = - SystemProperties.getBoolean("persist.wm.debug.shell_transit", false); + SystemProperties.getBoolean("persist.wm.debug.shell_transit", true); public static final boolean SHELL_TRANSITIONS_ROTATION = ENABLE_SHELL_TRANSITIONS && SystemProperties.getBoolean("persist.wm.debug.shell_transit_rotate", false); @@ -97,38 +128,88 @@ 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; + + /** Transition type to freeform in desktop mode. */ + public static final int TRANSIT_ENTER_FREEFORM = WindowManager.TRANSIT_FIRST_CUSTOM + 10; + + /** Transition type to freeform in desktop mode. */ + public static final int TRANSIT_ENTER_DESKTOP_MODE = WindowManager.TRANSIT_FIRST_CUSTOM + 11; + + /** Transition type to fullscreen from desktop mode. */ + public static final int TRANSIT_EXIT_DESKTOP_MODE = WindowManager.TRANSIT_FIRST_CUSTOM + 12; + 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 ShellController mShellController; private final ShellTransitionImpl mImpl = new ShellTransitionImpl(); + private final SleepHandler mSleepHandler = new SleepHandler(); + + private boolean mIsRegistered = false; /** 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<>(); private float mTransitionAnimationScaleSetting = 1.0f; + /** + * How much time we allow for an animation to finish itself on sleep. If it takes longer, we + * will force-finish it (on this end) which may leave it in a bad state but won't hang the + * device. This needs to be pretty small because it is an allowance for each queued animation, + * however it can't be too small since there is some potential IPC involved. + */ + private static final int SLEEP_ALLOWANCE_MS = 120; + private static final class ActiveTransition { IBinder mToken; TransitionHandler mHandler; - boolean mMerged; boolean mAborted; TransitionInfo mInfo; SurfaceControl.Transaction mStartT; SurfaceControl.Transaction mFinishT; + + /** Ordered list of transitions which have been merged into this one. */ + private ArrayList<ActiveTransition> mMerged; + + @Override + public String toString() { + if (mInfo != null && mInfo.getDebugId() >= 0) { + return "(#" + mInfo.getDebugId() + ")" + mToken; + } + return mToken.toString(); + } } - /** Keeps track of currently playing transitions in the order of receipt. */ + /** Keeps track of transitions which have been started, but aren't ready yet. */ + private final ArrayList<ActiveTransition> mPendingTransitions = new ArrayList<>(); + + /** Keeps track of transitions which are ready to play but still waiting for their turn. */ + private final ArrayList<ActiveTransition> mReadyTransitions = new ArrayList<>(); + + /** Keeps track of currently playing transitions. For now, there can only be 1 max. */ 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 ShellController shellController, + @NonNull WindowOrganizer organizer, + @NonNull TransactionPool pool, + @NonNull DisplayController displayController, @NonNull ShellExecutor mainExecutor, @NonNull Handler mainHandler, @NonNull ShellExecutor animExecutor) { mOrganizer = organizer; @@ -137,39 +218,66 @@ 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); + mShellController = shellController; // 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"); + shellInit.addInitCallback(this::onInit, this); + } + + private void onInit() { + if (Transitions.ENABLE_SHELL_TRANSITIONS) { + mOrganizer.shareTransactionQueue(); + } + mShellController.addExternalInterface(KEY_EXTRA_SHELL_SHELL_TRANSITIONS, + this::createExternalInterface, this); - 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) { + mIsRegistered = true; + // Register this transition handler with Core + try { + mOrganizer.registerTransitionPlayer(mPlayerImpl); + } catch (RuntimeException e) { + mIsRegistered = false; + throw e; + } + // Pre-load the instance. + TransitionMetrics.getInstance(); + } } - private Transitions() { - mOrganizer = null; - mContext = null; - mMainExecutor = null; - mAnimExecutor = null; - mDisplayController = null; - mPlayerImpl = null; - mRemoteTransitionHandler = null; + public boolean isRegistered() { + return mIsRegistered; + } + + 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() { return mImpl; } + private ExternalInterfaceBinder createExternalInterface() { + return new IShellTransitionsImpl(this); + } + @Override public Context getContext() { return mContext; @@ -186,20 +294,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 +335,33 @@ public class Transitions implements RemoteCallable<Transitions> { mRemoteTransitionHandler.removeFiltered(remoteTransition); } + RemoteTransitionHandler getRemoteTransitionHandler() { + return mRemoteTransitionHandler; + } + + /** 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. @@ -235,25 +370,14 @@ public class Transitions implements RemoteCallable<Transitions> { * will be executed when the last active transition is finished. */ public void runOnIdle(Runnable runnable) { - if (mActiveTransitions.isEmpty()) { + if (mActiveTransitions.isEmpty() && mPendingTransitions.isEmpty() + && mReadyTransitions.isEmpty()) { runnable.run(); } else { mRunWhenIdleQueue.add(runnable); } } - /** @return true if the transition was triggered by opening something vs closing something */ - public static boolean isOpeningType(@WindowManager.TransitionType int type) { - return type == TRANSIT_OPEN - || type == TRANSIT_TO_FRONT - || type == TRANSIT_KEYGUARD_GOING_AWAY; - } - - /** @return true if the transition was triggered by closing something vs opening something */ - public static boolean isClosingType(@WindowManager.TransitionType int type) { - return type == TRANSIT_CLOSE || type == TRANSIT_TO_BACK; - } - /** * Sets up visibility/alpha/transforms to resemble the starting state of an animation. */ @@ -262,9 +386,26 @@ public class Transitions implements RemoteCallable<Transitions> { boolean isOpening = isOpeningType(info.getType()); for (int i = info.getChanges().size() - 1; i >= 0; --i) { final TransitionInfo.Change change = info.getChanges().get(i); + if (change.hasFlags(TransitionInfo.FLAGS_IS_NON_APP_WINDOW)) { + // Currently system windows are controlled by WindowState, so don't change their + // surfaces. Otherwise their surfaces could be hidden or cropped unexpectedly. + // This includes Wallpaper (always z-ordered at bottom) and IME (associated with + // app), because there may not be a transition associated with their visibility + // changes, and currently they don't need transition animation. + continue; + } final SurfaceControl leash = change.getLeash(); final int mode = info.getChanges().get(i).getMode(); + if (mode == TRANSIT_TO_FRONT + && ((change.getStartAbsBounds().height() != change.getEndAbsBounds().height() + || change.getStartAbsBounds().width() != change.getEndAbsBounds().width()))) { + // When the window is moved to front with a different size, make sure the crop is + // updated to prevent it from using the old crop. + t.setWindowCrop(leash, change.getEndAbsBounds().width(), + change.getEndAbsBounds().height()); + } + // Don't move anything that isn't independent within its parents if (!TransitionInfo.isIndependent(change, info)) { if (mode == TRANSIT_OPEN || mode == TRANSIT_TO_FRONT || mode == TRANSIT_CHANGE) { @@ -283,18 +424,9 @@ public class Transitions implements RemoteCallable<Transitions> { // If this is a transferred starting window, we want it immediately visible. && (change.getFlags() & FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT) == 0) { t.setAlpha(leash, 0.f); - // fix alpha in finish transaction in case the animator itself no-ops. - 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) { - finishT.hide(leash); - } + finishT.hide(leash); } } } @@ -306,16 +438,17 @@ public class Transitions implements RemoteCallable<Transitions> { private static void setupAnimHierarchy(@NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction t, @NonNull SurfaceControl.Transaction finishT) { boolean isOpening = isOpeningType(info.getType()); - if (info.getRootLeash().isValid()) { - t.show(info.getRootLeash()); + for (int i = 0; i < info.getRootCount(); ++i) { + t.show(info.getRoot(i).getLeash()); } + 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)) { @@ -324,317 +457,562 @@ public class Transitions implements RemoteCallable<Transitions> { boolean hasParent = change.getParent() != null; + final int rootIdx = TransitionUtil.rootIndexFor(change, info); if (!hasParent) { - t.reparent(leash, info.getRootLeash()); - t.setPosition(leash, change.getStartAbsBounds().left - info.getRootOffset().x, - change.getStartAbsBounds().top - info.getRootOffset().y); + t.reparent(leash, info.getRoot(rootIdx).getLeash()); + t.setPosition(leash, + change.getStartAbsBounds().left - info.getRoot(rootIdx).getOffset().x, + change.getStartAbsBounds().top - info.getRoot(rootIdx).getOffset().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); } } - private int findActiveTransition(IBinder token) { - for (int i = mActiveTransitions.size() - 1; i >= 0; --i) { - if (mActiveTransitions.get(i).mToken == token) return i; + private static int findByToken(ArrayList<ActiveTransition> list, IBinder token) { + for (int i = list.size() - 1; i >= 0; --i) { + if (list.get(i).mToken == token) return i; } return -1; } + /** + * Look through a transition and see if all non-closing changes are no-animation. If so, no + * animation should play. + */ + static boolean isAllNoAnimation(TransitionInfo info) { + if (isClosingType(info.getType())) { + // no-animation is only relevant for launching (open) activities. + return false; + } + boolean hasNoAnimation = false; + final int changeSize = info.getChanges().size(); + for (int i = changeSize - 1; i >= 0; --i) { + final TransitionInfo.Change change = info.getChanges().get(i); + if (isClosingType(change.getMode())) { + // ignore closing apps since they are a side-effect of the transition and don't + // animate. + continue; + } + if (change.hasFlags(FLAG_NO_ANIMATION)) { + hasNoAnimation = true; + } else { + // at-least one relevant participant *is* animated, so we need to animate. + return false; + } + } + return hasNoAnimation; + } + + /** + * Check if all changes in this transition are only ordering changes. If so, we won't animate. + */ + static boolean isAllOrderOnly(TransitionInfo info) { + for (int i = info.getChanges().size() - 1; i >= 0; --i) { + if (!TransitionUtil.isOrderOnly(info.getChanges().get(i))) return false; + } + return true; + } + @VisibleForTesting void onTransitionReady(@NonNull IBinder transitionToken, @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction t, @NonNull SurfaceControl.Transaction finishT) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "onTransitionReady %s: %s", transitionToken, info); - final int activeIdx = findActiveTransition(transitionToken); + final int activeIdx = findByToken(mPendingTransitions, transitionToken); if (activeIdx < 0) { - throw new IllegalStateException("Got transitionReady for non-active transition " + throw new IllegalStateException("Got transitionReady for non-pending transition " + transitionToken + ". expecting one of " - + Arrays.toString(mActiveTransitions.stream().map( + + Arrays.toString(mPendingTransitions.stream().map( + activeTransition -> activeTransition.mToken).toArray())); + } + if (activeIdx > 0) { + Log.e(TAG, "Transition became ready out-of-order " + mPendingTransitions.get(activeIdx) + + ". Expected order: " + Arrays.toString(mPendingTransitions.stream().map( activeTransition -> activeTransition.mToken).toArray())); } - if (!info.getRootLeash().isValid()) { - // Invalid root-leash implies that the transition is empty/no-op, so just do + // Move from pending to ready + final ActiveTransition active = mPendingTransitions.remove(activeIdx); + mReadyTransitions.add(active); + active.mInfo = info; + active.mStartT = t; + active.mFinishT = finishT; + + for (int i = 0; i < mObservers.size(); ++i) { + mObservers.get(i).onTransitionReady(transitionToken, info, t, finishT); + } + + if (info.getType() == TRANSIT_SLEEP) { + if (activeIdx > 0 || !mActiveTransitions.isEmpty() || mReadyTransitions.size() > 1) { + // Sleep starts a process of forcing all prior transitions to finish immediately + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Start finish-for-sleep"); + finishForSleep(null /* forceFinish */); + return; + } + } + + if (info.getRootCount() == 0 && !alwaysReportToKeyguard(info)) { + // No root-leashes 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(); - onAbort(transitionToken); + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "No transition roots in %s so" + + " abort", active); + onAbort(active); return; } - // apply transfer starting window directly if there is no other task change. Since this - // is an activity->activity situation, we can detect it by selecting transitions with only - // 2 changes where neither are tasks and one is a starting-window recipient. final int changeSize = info.getChanges().size(); - if (changeSize == 2) { - boolean nonTaskChange = true; - boolean transferStartingWindow = false; - for (int i = changeSize - 1; i >= 0; --i) { - final TransitionInfo.Change change = info.getChanges().get(i); - if (change.getTaskInfo() != null) { - nonTaskChange = false; - break; - } - if ((change.getFlags() & FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT) != 0) { - transferStartingWindow = true; - } - } - if (nonTaskChange && transferStartingWindow) { - t.apply(); - // Treat this as an abort since we are bypassing any merge logic and effectively - // finishing immediately. - onAbort(transitionToken); - return; + boolean taskChange = false; + boolean transferStartingWindow = false; + boolean allOccluded = changeSize > 0; + for (int i = changeSize - 1; i >= 0; --i) { + final TransitionInfo.Change change = info.getChanges().get(i); + taskChange |= change.getTaskInfo() != null; + transferStartingWindow |= change.hasFlags(FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT); + if (!change.hasFlags(FLAG_IS_OCCLUDED)) { + allOccluded = false; } } + // There does not need animation when: + // A. Transfer starting window. Apply transfer starting window directly if there is no other + // task change. Since this is an activity->activity situation, we can detect it by selecting + // transitions with only 2 changes where neither are tasks and one is a starting-window + // recipient. + if (!taskChange && transferStartingWindow && changeSize == 2 + // B. It's visibility change if the TRANSIT_TO_BACK/TO_FRONT happened when all + // changes are underneath another change. + || ((info.getType() == TRANSIT_TO_BACK || info.getType() == TRANSIT_TO_FRONT) + && allOccluded)) { + // Treat this as an abort since we are bypassing any merge logic and effectively + // finishing immediately. + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, + "Non-visible anim so abort: %s", active); + onAbort(active); + return; + } - final ActiveTransition active = mActiveTransitions.get(activeIdx); - active.mInfo = info; - active.mStartT = t; - active.mFinishT = finishT; setupStartState(active.mInfo, active.mStartT, active.mFinishT); - if (activeIdx > 0) { - // This is now playing at the same time as an existing animation, so try merging it. - attemptMergeTransition(mActiveTransitions.get(0), active); + if (mReadyTransitions.size() > 1) { + // There are already transitions waiting in the queue, so just return. return; } - // The normal case, just play it. - playTransition(active); + processReadyQueue(); } /** - * Attempt to merge by delegating the transition start to the handler of the currently - * playing transition. + * Some transitions we always need to report to keyguard even if they are empty. + * TODO (b/274954192): Remove this once keyguard dispatching moves to Shell. */ - void attemptMergeTransition(@NonNull ActiveTransition playing, - @NonNull ActiveTransition merging) { + private static boolean alwaysReportToKeyguard(TransitionInfo info) { + // occlusion status of activities can change while screen is off so there will be no + // visibility change but we still need keyguardservice to be notified. + if (info.getType() == TRANSIT_KEYGUARD_UNOCCLUDE) return true; + + // It's possible for some activities to stop with bad timing (esp. since we can't yet + // queue activity transitions initiated by apps) that results in an empty transition for + // keyguard going-away. In general, we should should always report Keyguard-going-away. + if ((info.getFlags() & TRANSIT_FLAG_KEYGUARD_GOING_AWAY) != 0) return true; + + return false; + } + + void processReadyQueue() { + if (mReadyTransitions.isEmpty()) { + // Check if idle. + if (mActiveTransitions.isEmpty() && mPendingTransitions.isEmpty()) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "All active transition " + + "animations finished"); + // Run all runnables from the run-when-idle queue. + for (int i = 0; i < mRunWhenIdleQueue.size(); i++) { + mRunWhenIdleQueue.get(i).run(); + } + mRunWhenIdleQueue.clear(); + } + return; + } + final ActiveTransition ready = mReadyTransitions.get(0); + if (mActiveTransitions.isEmpty()) { + // The normal case, just play it (currently we only support 1 active transition). + mReadyTransitions.remove(0); + mActiveTransitions.add(ready); + if (ready.mAborted) { + // finish now since there's nothing to animate. Calls back into processReadyQueue + onFinish(ready, null, null); + return; + } + playTransition(ready); + // Attempt to merge any more queued-up transitions. + processReadyQueue(); + return; + } + // An existing animation is playing, so see if we can merge. + final ActiveTransition playing = mActiveTransitions.get(0); + if (ready.mAborted) { + // record as merged since it is no-op. Calls back into processReadyQueue + onMerged(playing, ready); + return; + } ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Transition %s ready while" - + " another transition %s is still animating. Notify the animating transition" - + " in case they can be merged", merging.mToken, playing.mToken); - playing.mHandler.mergeAnimation(merging.mToken, merging.mInfo, merging.mStartT, - playing.mToken, (wct, cb) -> onFinish(merging.mToken, wct, cb)); + + " %s is still animating. Notify the animating transition" + + " in case they can be merged", ready, playing); + playing.mHandler.mergeAnimation(ready.mToken, ready.mInfo, ready.mStartT, + playing.mToken, (wct, cb) -> onMerged(playing, ready)); } - 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 onMerged(@NonNull ActiveTransition playing, @NonNull ActiveTransition merged) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Transition was merged: %s into %s", + merged, playing); + int readyIdx = 0; + if (mReadyTransitions.isEmpty() || mReadyTransitions.get(0) != merged) { + Log.e(TAG, "Merged transition out-of-order? " + merged); + readyIdx = mReadyTransitions.indexOf(merged); + if (readyIdx < 0) { + Log.e(TAG, "Merged a transition that is no-longer queued? " + merged); + return; + } + } + mReadyTransitions.remove(readyIdx); + if (playing.mMerged == null) { + playing.mMerged = new ArrayList<>(); + } + playing.mMerged.add(merged); + // if it was aborted, then onConsumed has already been reported. + if (merged.mHandler != null && !merged.mAborted) { + merged.mHandler.onTransitionConsumed(merged.mToken, false /* abort */, merged.mFinishT); + } + for (int i = 0; i < mObservers.size(); ++i) { + mObservers.get(i).onTransitionMerged(merged.mToken, playing.mToken); + } + // See if we should merge another transition. + processReadyQueue(); } - void playTransition(@NonNull ActiveTransition active) { + private void playTransition(@NonNull ActiveTransition active) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Playing animation for %s", active); + for (int i = 0; i < mObservers.size(); ++i) { + mObservers.get(i).onTransitionStarting(active.mToken); + } + 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, 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, 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( "This shouldn't happen, maybe the default handler is broken."); } - /** Special version of finish just for dealing with no-op/invalid transitions. */ - private void onAbort(IBinder transition) { - onFinish(transition, null /* wct */, null /* wctCB */, true /* abort */); + /** + * Gives every handler (in order) a chance to handle request until one consumes the transition. + * @return the WindowContainerTransaction given by the handler which consumed the transition. + */ + public Pair<TransitionHandler, WindowContainerTransaction> dispatchRequest( + @NonNull IBinder transition, @NonNull TransitionRequestInfo request, + @Nullable TransitionHandler skip) { + for (int i = mHandlers.size() - 1; i >= 0; --i) { + if (mHandlers.get(i) == skip) continue; + WindowContainerTransaction wct = mHandlers.get(i).handleRequest(transition, request); + if (wct != null) { + return new Pair<>(mHandlers.get(i), wct); + } + } + return null; } - private void onFinish(IBinder transition, - @Nullable WindowContainerTransaction wct, - @Nullable WindowContainerTransactionCallback wctCB) { - onFinish(transition, wct, wctCB, false /* abort */); + /** Aborts a transition. This will still queue it up to maintain order. */ + private void onAbort(ActiveTransition transition) { + // apply immediately since they may be "parallel" operations: We currently we use abort for + // thing which are independent to other transitions (like starting-window transfer). + transition.mStartT.apply(); + transition.mFinishT.apply(); + transition.mAborted = true; + + if (transition.mHandler != null) { + // Notifies to clean-up the aborted transition. + transition.mHandler.onTransitionConsumed( + transition.mToken, true /* aborted */, null /* finishTransaction */); + } + + releaseSurfaces(transition.mInfo); + + // This still went into the queue (to maintain the correct finish ordering). + if (mReadyTransitions.size() > 1) { + // There are already transitions waiting in the queue, so just return. + return; + } + processReadyQueue(); } - private void onFinish(IBinder transition, + /** + * Releases an info's animation-surfaces. These don't need to persist and we need to release + * them asap so that SF can free memory sooner. + */ + private void releaseSurfaces(@Nullable TransitionInfo info) { + if (info == null) return; + info.releaseAnimSurfaces(); + } + + private void onFinish(ActiveTransition active, @Nullable WindowContainerTransaction wct, - @Nullable WindowContainerTransactionCallback wctCB, - boolean abort) { - int activeIdx = findActiveTransition(transition); + @Nullable WindowContainerTransactionCallback wctCB) { + int activeIdx = mActiveTransitions.indexOf(active); if (activeIdx < 0) { Log.e(TAG, "Trying to finish a non-running transition. Either remote crashed or " - + " a handler didn't properly deal with a merge.", new RuntimeException()); - return; - } else if (activeIdx > 0) { - // This transition was merged. - ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Transition was merged (abort=%b:" - + " %s", abort, transition); - final ActiveTransition active = mActiveTransitions.get(activeIdx); - active.mMerged = true; - active.mAborted = abort; - if (active.mHandler != null) { - active.mHandler.onTransitionMerged(active.mToken); - } + + " a handler didn't properly deal with a merge. " + active, + new RuntimeException()); return; + } else if (activeIdx != 0) { + // Relevant right now since we only allow 1 active transition at a time. + Log.e(TAG, "Finishing a transition out of order. " + active); + } + mActiveTransitions.remove(activeIdx); + + for (int i = 0; i < mObservers.size(); ++i) { + mObservers.get(i).onTransitionFinished(active.mToken, active.mAborted); } - mActiveTransitions.get(activeIdx).mAborted = abort; - 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; - for (int iA = activeIdx + 1; iA < mActiveTransitions.size(); ++iA) { - final ActiveTransition toMerge = mActiveTransitions.get(iA); - if (!toMerge.mMerged) break; - // aborted transitions have no start/finish transactions - if (mActiveTransitions.get(iA).mStartT == null) break; - if (fullFinish == null) { - fullFinish = new SurfaceControl.Transaction(); + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Transition animation finished " + + "(aborted=%b), notifying core %s", active.mAborted, active); + if (active.mStartT != null) { + // Applied by now, so clear immediately to remove any references. Do not set to null + // yet, though, since nullness is used later to disambiguate malformed transitions. + active.mStartT.clear(); + } + // Merge all associated transactions together + SurfaceControl.Transaction fullFinish = active.mFinishT; + if (active.mMerged != null) { + for (int iM = 0; iM < active.mMerged.size(); ++iM) { + final ActiveTransition toMerge = active.mMerged.get(iM); + // Include start. It will be a no-op if it was already applied. Otherwise, we need + // it to maintain consistent state. + if (toMerge.mStartT != null) { + if (fullFinish == null) { + fullFinish = toMerge.mStartT; + } else { + fullFinish.merge(toMerge.mStartT); + } + } + if (toMerge.mFinishT != null) { + if (fullFinish == null) { + fullFinish = toMerge.mFinishT; + } else { + fullFinish.merge(toMerge.mFinishT); + } + } } - // Include start. It will be a no-op if it was already applied. Otherwise, we need it - // to maintain consistent state. - fullFinish.merge(mActiveTransitions.get(iA).mStartT); - fullFinish.merge(mActiveTransitions.get(iA).mFinishT); } if (fullFinish != null) { fullFinish.apply(); } - // Now perform all the finishes. - mActiveTransitions.remove(activeIdx); - mOrganizer.finishTransition(transition, wct, wctCB); - while (activeIdx < mActiveTransitions.size()) { - if (!mActiveTransitions.get(activeIdx).mMerged) break; - ActiveTransition merged = mActiveTransitions.remove(activeIdx); - mOrganizer.finishTransition(merged.mToken, null /* wct */, null /* wctCB */); - } - // sift through aborted transitions - while (mActiveTransitions.size() > activeIdx - && mActiveTransitions.get(activeIdx).mAborted) { - ActiveTransition aborted = mActiveTransitions.remove(activeIdx); - mOrganizer.finishTransition(aborted.mToken, null /* wct */, null /* wctCB */); - } - if (mActiveTransitions.size() <= activeIdx) { - ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "All active transition animations " - + "finished"); - // Run all runnables from the run-when-idle queue. - for (int i = 0; i < mRunWhenIdleQueue.size(); i++) { - mRunWhenIdleQueue.get(i).run(); + // Now perform all the finish callbacks (starting with the playing one and then all the + // transitions merged into it). + releaseSurfaces(active.mInfo); + mOrganizer.finishTransition(active.mToken, wct, wctCB); + if (active.mMerged != null) { + for (int iM = 0; iM < active.mMerged.size(); ++iM) { + ActiveTransition merged = active.mMerged.get(iM); + mOrganizer.finishTransition(merged.mToken, null /* wct */, null /* wctCB */); + releaseSurfaces(merged.mInfo); } - mRunWhenIdleQueue.clear(); - return; + active.mMerged.clear(); } - // Start animating the next active transition - final ActiveTransition next = mActiveTransitions.get(activeIdx); - if (next.mInfo == null) { - ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Pending transition after one" - + " finished, but it isn't ready yet."); - return; + + // Now that this is done, check the ready queue for more work. + processReadyQueue(); + } + + private boolean isTransitionKnown(IBinder token) { + for (int i = 0; i < mPendingTransitions.size(); ++i) { + if (mPendingTransitions.get(i).mToken == token) return true; } - ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Pending transitions after one" - + " finished, so start the next one."); - playTransition(next); - // Now try to merge the rest of the transitions (re-acquire activeIdx since next may have - // finished immediately) - activeIdx = findActiveTransition(next.mToken); - if (activeIdx < 0) { - // This means 'next' finished immediately and thus re-entered this function. Since - // that is the case, just return here since all relevant logic has already run in the - // re-entered call. - return; + for (int i = 0; i < mReadyTransitions.size(); ++i) { + if (mReadyTransitions.get(i).mToken == token) return true; } - - // This logic is also convoluted because 'next' may finish immediately in response to any of - // the merge requests (eg. if it decided to "cancel" itself). - int mergeIdx = activeIdx + 1; - while (mergeIdx < mActiveTransitions.size()) { - ActiveTransition mergeCandidate = mActiveTransitions.get(mergeIdx); - if (mergeCandidate.mAborted) { - // transition was aborted, so we can skip for now (still leave it in the list - // so that it gets cleaned-up in the right order). - ++mergeIdx; - continue; - } - if (mergeCandidate.mMerged) { - throw new IllegalStateException("Can't merge a transition after not-merging" - + " a preceding one."); - } - attemptMergeTransition(next, mergeCandidate); - mergeIdx = findActiveTransition(mergeCandidate.mToken); - if (mergeIdx < 0) { - // This means 'next' finished immediately and thus re-entered this function. Since - // that is the case, just return here since all relevant logic has already run in - // the re-entered call. - return; + for (int i = 0; i < mActiveTransitions.size(); ++i) { + final ActiveTransition active = mActiveTransitions.get(i); + if (active.mToken == token) return true; + if (active.mMerged == null) continue; + for (int m = 0; m < active.mMerged.size(); ++m) { + if (active.mMerged.get(m).mToken == token) return true; } - ++mergeIdx; } + return false; } void requestStartTransition(@NonNull IBinder transitionToken, @Nullable TransitionRequestInfo request) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Transition requested: %s %s", transitionToken, request); - if (findActiveTransition(transitionToken) >= 0) { + if (isTransitionKnown(transitionToken)) { throw new RuntimeException("Transition already started " + transitionToken); } final ActiveTransition active = new ActiveTransition(); WindowContainerTransaction wct = null; - for (int i = mHandlers.size() - 1; i >= 0; --i) { - wct = mHandlers.get(i).handleRequest(transitionToken, request); - if (wct != null) { - active.mHandler = mHandlers.get(i); - break; + + // If we have sleep, we use a special handler and we try to finish everything ASAP. + if (request.getType() == TRANSIT_SLEEP) { + mSleepHandler.handleRequest(transitionToken, request); + active.mHandler = mSleepHandler; + } else { + for (int i = mHandlers.size() - 1; i >= 0; --i) { + wct = mHandlers.get(i).handleRequest(transitionToken, request); + if (wct != null) { + active.mHandler = mHandlers.get(i); + break; + } } - } - if (request.getDisplayChange() != null) { - TransitionRequestInfo.DisplayChange change = request.getDisplayChange(); - if (change.getEndRotation() != change.getStartRotation()) { - // Is a rotation, so dispatch to all displayChange listeners - if (wct == null) { - wct = new WindowContainerTransaction(); + if (request.getDisplayChange() != null) { + TransitionRequestInfo.DisplayChange change = request.getDisplayChange(); + if (change.getEndRotation() != change.getStartRotation()) { + // Is a rotation, so dispatch to all displayChange listeners + if (wct == null) { + wct = new WindowContainerTransaction(); + } + mDisplayController.getChangeController().dispatchOnDisplayChange(wct, + change.getDisplayId(), change.getStartRotation(), + change.getEndRotation(), null /* newDisplayAreaInfo */); } - mDisplayController.getChangeController().dispatchOnRotateDisplay(wct, - change.getDisplayId(), change.getStartRotation(), change.getEndRotation()); } } - active.mToken = mOrganizer.startTransition( - request.getType(), transitionToken, wct); - mActiveTransitions.add(active); + mOrganizer.startTransition(transitionToken, wct != null && wct.isEmpty() ? null : wct); + active.mToken = transitionToken; + // Currently, WMCore only does one transition at a time. If it makes a requestStart, it + // is already collecting that transition on core-side, so it will be the next one to + // become ready. There may already be pending transitions added as part of direct + // `startNewTransition` but if we have a request now, it means WM created the request + // transition before it acknowledged any of the pending `startNew` transitions. So, insert + // it at the front. + mPendingTransitions.add(0, active); } /** Start a new transition directly. */ public IBinder startTransition(@WindowManager.TransitionType int type, @NonNull WindowContainerTransaction wct, @Nullable TransitionHandler handler) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Directly starting a new transition " + + "type=%d wct=%s handler=%s", type, wct, handler); final ActiveTransition active = new ActiveTransition(); active.mHandler = handler; - active.mToken = mOrganizer.startTransition(type, null /* token */, wct); - mActiveTransitions.add(active); + active.mToken = mOrganizer.startNewTransition(type, wct); + mPendingTransitions.add(active); return active.mToken; } /** + * Finish running animations (almost) immediately when a SLEEP transition comes in. We use this + * as both a way to reduce unnecessary work (animations not visible while screen off) and as a + * failsafe to unblock "stuck" animations (in particular remote animations). + * + * This works by "merging" the sleep transition into the currently-playing transition (even if + * its out-of-order) -- turning SLEEP into a signal. If the playing transition doesn't finish + * within `SLEEP_ALLOWANCE_MS` from this merge attempt, this will then finish it directly (and + * send an abort/consumed message). + * + * This is then repeated until there are no more pending sleep transitions. + * + * @param forceFinish When non-null, this is the transition that we last sent the SLEEP merge + * signal to -- so it will be force-finished if it's still running. + */ + private void finishForSleep(@Nullable ActiveTransition forceFinish) { + if ((mActiveTransitions.isEmpty() && mReadyTransitions.isEmpty()) + || mSleepHandler.mSleepTransitions.isEmpty()) { + // Done finishing things. + // Prevent any weird leaks... shouldn't happen though. + mSleepHandler.mSleepTransitions.clear(); + return; + } + if (forceFinish != null && mActiveTransitions.contains(forceFinish)) { + Log.e(TAG, "Forcing transition to finish due to sleep timeout: " + forceFinish); + forceFinish.mAborted = true; + // Last notify of it being consumed. Note: mHandler should never be null, + // but check just to be safe. + if (forceFinish.mHandler != null) { + forceFinish.mHandler.onTransitionConsumed( + forceFinish.mToken, true /* aborted */, null /* finishTransaction */); + } + onFinish(forceFinish, null, null); + } + final SurfaceControl.Transaction dummyT = new SurfaceControl.Transaction(); + while (!mActiveTransitions.isEmpty() && !mSleepHandler.mSleepTransitions.isEmpty()) { + final ActiveTransition playing = mActiveTransitions.get(0); + int sleepIdx = findByToken(mReadyTransitions, mSleepHandler.mSleepTransitions.get(0)); + if (sleepIdx >= 0) { + // Try to signal that we are sleeping by attempting to merge the sleep transition + // into the playing one. + final ActiveTransition nextSleep = mReadyTransitions.get(sleepIdx); + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Attempt to merge SLEEP %s" + + " into %s", nextSleep, playing); + playing.mHandler.mergeAnimation(nextSleep.mToken, nextSleep.mInfo, dummyT, + playing.mToken, (wct, cb) -> {}); + } else { + Log.e(TAG, "Couldn't find sleep transition in ready list: " + + mSleepHandler.mSleepTransitions.get(0)); + } + // it's possible to complete immediately. If that happens, just repeat the signal + // loop until we either finish everything or start playing an animation that isn't + // finishing immediately. + if (!mActiveTransitions.isEmpty() && mActiveTransitions.get(0) == playing) { + // Give it a (very) short amount of time to process it before forcing. + mMainExecutor.executeDelayed(() -> finishForSleep(playing), SLEEP_ALLOWANCE_MS); + break; + } + } + } + + /** * Interface for a callback that must be called after a TransitionHandler finishes playing an * animation. */ @@ -714,9 +1092,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 +1110,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 @@ -748,17 +1178,6 @@ public class Transitions implements RemoteCallable<Transitions> { */ @ExternalThread private class ShellTransitionImpl implements ShellTransitions { - private IShellTransitionsImpl mIShellTransitions; - - @Override - public IShellTransitions createExternalInterface() { - if (mIShellTransitions != null) { - mIShellTransitions.invalidate(); - } - mIShellTransitions = new IShellTransitionsImpl(Transitions.this); - return mIShellTransitions; - } - @Override public void registerRemote(@NonNull TransitionFilter filter, @NonNull RemoteTransition remoteTransition) { @@ -779,7 +1198,8 @@ public class Transitions implements RemoteCallable<Transitions> { * The interface for calls from outside the host process. */ @BinderThread - private static class IShellTransitionsImpl extends IShellTransitions.Stub { + private static class IShellTransitionsImpl extends IShellTransitions.Stub + implements ExternalInterfaceBinder { private Transitions mTransitions; IShellTransitionsImpl(Transitions transitions) { @@ -789,7 +1209,8 @@ public class Transitions implements RemoteCallable<Transitions> { /** * Invalidates this instance, preventing future calls from updating the controller. */ - void invalidate() { + @Override + public void invalidate() { mTransitions = null; } @@ -809,6 +1230,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 +1246,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..d7cb490ed0cb --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldAnimationController.java @@ -0,0 +1,240 @@ +/* + * 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 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<>(); + + /** + * Indicates whether we're in stage change process. This should be set to {@code true} in + * {@link #onStateChangeStarted()} and {@code false} in {@link #onStateChangeFinished()}. + */ + private boolean mIsInStageChange; + + 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 + maybeResetTask(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) { + maybeResetTask(animator, taskInfo); + animator.onTaskVanished(taskInfo); + mAnimatorsByTaskId.remove(taskInfo.taskId); + } + } + + @Override + public void onStateChangeStarted() { + if (mUnfoldTransitionHandler.get().get().willHandleTransition()) { + return; + } + + mIsInStageChange = true; + 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); + mIsInStageChange = false; + } + + private void maybeResetTask(UnfoldTaskAnimator animator, TaskInfo taskInfo) { + if (!mIsInStageChange) { + // No need to resetTask if there is no ongoing state change. + 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..fe0a3fb7b9dc 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.unfold_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..f81fc6fbea49 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,40 +14,49 @@ * 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.content.res.Configuration; import android.graphics.Matrix; import android.graphics.Rect; +import android.os.Trace; import android.util.SparseArray; import android.view.InsetsSource; import android.view.InsetsState; import android.view.SurfaceControl; +import android.view.SurfaceControl.Transaction; +import android.view.WindowInsets; 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.sysui.ConfigurationChangeListener; +import com.android.wm.shell.sysui.ShellController; +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, ConfigurationChangeListener { private static final float[] FLOAT_9 = new float[9]; private static final TypeEvaluator<Rect> RECT_EVALUATOR = new RectEvaluator(new Rect()); @@ -57,49 +66,97 @@ 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 DisplayInsetsController mDisplayInsetsController; private final UnfoldBackgroundController mBackgroundController; + private final Context mContext; + private final ShellController mShellController; - private InsetsSource mTaskbarInsetsSource; + private InsetsSource mExpandedTaskbarInsetsSource; + private float mWindowCornerRadiusPx; - 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; + ShellController shellController, DisplayInsetsController displayInsetsController) { + mContext = context; mDisplayInsetsController = displayInsetsController; - mWindowCornerRadiusPx = ScreenDecorationsUtils.getWindowCornerRadius(context); - mExpandedTaskBarHeight = context.getResources().getDimensionPixelSize( - com.android.internal.R.dimen.taskbar_frame_height); mBackgroundController = backgroundController; + mShellController = shellController; + mWindowCornerRadiusPx = ScreenDecorationsUtils.getWindowCornerRadius(context); } - /** - * Initializes the controller - */ public void init() { - mProgressProvider.addListener(mExecutor, this); mDisplayInsetsController.addInsetsChangedListener(DEFAULT_DISPLAY, this); + mShellController.addConfigurationChangeListener(this); } @Override - public void onStateChangeProgress(float progress) { - if (mAnimationContextByTaskId.size() == 0) return; + public void onConfigurationChanged(Configuration newConfiguration) { + Trace.beginSection("FullscreenUnfoldTaskAnimator#onConfigurationChanged"); + mWindowCornerRadiusPx = ScreenDecorationsUtils.getWindowCornerRadius(mContext); + Trace.endSection(); + } + + @Override + public void insetsChanged(InsetsState insetsState) { + mExpandedTaskbarInsetsSource = getExpandedTaskbarSource(insetsState); + for (int i = mAnimationContextByTaskId.size() - 1; i >= 0; i--) { + AnimationContext context = mAnimationContextByTaskId.valueAt(i); + context.update(mExpandedTaskbarInsetsSource, context.mTaskInfo); + } + } - mBackgroundController.ensureBackground(mTransaction); + private static InsetsSource getExpandedTaskbarSource(InsetsState state) { + for (int i = state.sourceSize() - 1; i >= 0; i--) { + final InsetsSource source = state.sourceAt(i); + if (source.getType() == WindowInsets.Type.navigationBars() + && source.insetsRoundedCornerFrame()) { + return source; + } + } + return null; + } + + public boolean hasActiveTasks() { + return mAnimationContextByTaskId.size() > 0; + } + + @Override + public void onTaskAppeared(TaskInfo taskInfo, SurfaceControl leash) { + AnimationContext animationContext = new AnimationContext( + leash, mExpandedTaskbarInsetsSource, taskInfo); + mAnimationContextByTaskId.put(taskInfo.taskId, animationContext); + } + + @Override + public void onTaskChanged(TaskInfo taskInfo) { + AnimationContext animationContext = mAnimationContextByTaskId.get(taskInfo.taskId); + if (animationContext != null) { + animationContext.update(mExpandedTaskbarInsetsSource, taskInfo); + } + } + + @Override + public void onTaskVanished(TaskInfo taskInfo) { + mAnimationContextByTaskId.remove(taskInfo.taskId); + } + + @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 +168,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 +220,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); } @@ -209,11 +231,7 @@ public final class FullscreenUnfoldController implements UnfoldListener, mStartCropRect.set(mTaskInfo.getConfiguration().windowConfiguration.getBounds()); if (taskBarInsetsSource != null) { - // Only insets the cropping window with task bar when it's expanded - if (taskBarInsetsSource.getFrame().height() >= mExpandedTaskBarHeight) { - mStartCropRect.inset(taskBarInsetsSource - .calculateVisibleInsets(mStartCropRect)); - } + mStartCropRect.inset(taskBarInsetsSource.calculateVisibleInsets(mStartCropRect)); } mEndCropRect.set(mStartCropRect); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/animation/SplitTaskUnfoldAnimator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/animation/SplitTaskUnfoldAnimator.java new file mode 100644 index 000000000000..2f0c96487d68 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/animation/SplitTaskUnfoldAnimator.java @@ -0,0 +1,376 @@ +/* + * 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 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.app.TaskInfo; +import android.content.Context; +import android.content.res.Configuration; +import android.graphics.Insets; +import android.graphics.Rect; +import android.os.Trace; +import android.util.SparseArray; +import android.view.InsetsSource; +import android.view.InsetsState; +import android.view.SurfaceControl; +import android.view.SurfaceControl.Transaction; +import android.view.WindowInsets; + +import com.android.internal.policy.ScreenDecorationsUtils; +import com.android.wm.shell.common.DisplayInsetsController; +import com.android.wm.shell.common.split.SplitScreenConstants.SplitPosition; +import com.android.wm.shell.splitscreen.SplitScreen; +import com.android.wm.shell.splitscreen.SplitScreen.SplitScreenListener; +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.unfold.UnfoldAnimationController; +import com.android.wm.shell.unfold.UnfoldBackgroundController; + +import java.util.Optional; +import java.util.concurrent.Executor; + +import dagger.Lazy; + +/** + * 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 SplitTaskUnfoldAnimator implements UnfoldTaskAnimator, + DisplayInsetsController.OnInsetsChangedListener, SplitScreenListener, + ConfigurationChangeListener { + + private static final TypeEvaluator<Rect> RECT_EVALUATOR = new RectEvaluator(new Rect()); + private static final float CROPPING_START_MARGIN_FRACTION = 0.05f; + + private final Context mContext; + private final Executor mExecutor; + private final DisplayInsetsController mDisplayInsetsController; + private final SparseArray<AnimationContext> mAnimationContextByTaskId = new SparseArray<>(); + private final ShellController mShellController; + 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 float mWindowCornerRadiusPx; + private InsetsSource mExpandedTaskbarInsetsSource; + + @SplitPosition + private int mMainStagePosition = SPLIT_POSITION_UNDEFINED; + @SplitPosition + private int mSideStagePosition = SPLIT_POSITION_UNDEFINED; + + public SplitTaskUnfoldAnimator(Context context, Executor executor, + Lazy<Optional<SplitScreenController>> splitScreenController, + ShellController shellController, UnfoldBackgroundController unfoldBackgroundController, + DisplayInsetsController displayInsetsController) { + mDisplayInsetsController = displayInsetsController; + mExecutor = executor; + mContext = context; + mShellController = shellController; + mUnfoldBackgroundController = unfoldBackgroundController; + mSplitScreenController = splitScreenController; + mWindowCornerRadiusPx = ScreenDecorationsUtils.getWindowCornerRadius(context); + } + + /** Initializes the animator, this should be called only once */ + @Override + public void init() { + mDisplayInsetsController.addInsetsChangedListener(DEFAULT_DISPLAY, this); + mShellController.addConfigurationChangeListener(this); + } + + @Override + public void onConfigurationChanged(Configuration newConfiguration) { + Trace.beginSection("SplitTaskUnfoldAnimator#onConfigurationChanged"); + mWindowCornerRadiusPx = ScreenDecorationsUtils.getWindowCornerRadius(mContext); + Trace.endSection(); + } + + /** + * 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) { + mExpandedTaskbarInsetsSource = getExpandedTaskbarSource(insetsState); + updateContexts(); + } + + private static InsetsSource getExpandedTaskbarSource(InsetsState state) { + for (int i = state.sourceSize() - 1; i >= 0; i--) { + final InsetsSource source = state.sourceAt(i); + if (source.getType() == WindowInsets.Type.navigationBars() + && source.insetsRoundedCornerFrame()) { + return source; + } + } + return null; + } + + @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(); + } + } + + /** + * Register a split task in the animator + * @param taskInfo info of the task + * @param leash the surface of the task + */ + @Override + public void onTaskAppeared(TaskInfo taskInfo, SurfaceControl leash) { + AnimationContext context = new AnimationContext(leash); + mAnimationContextByTaskId.put(taskInfo.taskId, context); + } + + /** + * 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 + */ + @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) { + resetSurface(transaction, context); + } + } + + /** + * Reset all surface transformation that could have been introduced by the animator + * @param transaction to write changes to + */ + @Override + 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)); + + transaction.setWindowCrop(context.mLeash, context.mCurrentCropRect) + .setCornerRadius(context.mLeash, mWindowCornerRadiusPx); + } + } + + @Override + public void prepareStartTransaction(Transaction transaction) { + mUnfoldBackgroundController.ensureBackground(transaction); + mSplitScreenController.get().get().updateSplitScreenSurfaces(transaction); + } + + @Override + public void prepareFinishTransaction(Transaction transaction) { + mUnfoldBackgroundController.removeBackground(transaction); + } + + /** + * @return true if there are tasks to animate + */ + @Override + public boolean hasActiveTasks() { + return mAnimationContextByTaskId.size() > 0; + } + + 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(); + + @SplitScreen.StageType + int mStageType = STAGE_TYPE_UNDEFINED; + + private AnimationContext(SurfaceControl leash) { + mLeash = leash; + update(); + } + + private void update() { + final Rect stageBounds = mStageType == STAGE_TYPE_MAIN + ? mMainStageBounds : mSideStageBounds; + + mStartCropRect.set(stageBounds); + + boolean taskbarExpanded = isTaskbarExpanded(); + if (taskbarExpanded) { + // Only insets the cropping window with taskbar when taskbar is expanded + mStartCropRect.inset(mExpandedTaskbarInsetsSource.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); + + // Sides adjacent to split bar or task bar are not be animated. + Insets margins; + 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); + } + mStartCropRect.inset(margins); + } + + private Insets getLandscapeMargins(int margin, boolean taskbarExpanded) { + int left = margin; + int right = margin; + int bottom = taskbarExpanded ? 0 : margin; // Taskbar margin. + 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. + } + return Insets.of(left, /* top= */ margin, right, bottom); + } + + private Insets getPortraitMargins(int margin, boolean taskbarExpanded) { + int bottom = margin; + int top = margin; + 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. + if (taskbarExpanded) { + bottom = 0; // Taskbar margin. + } + } + return Insets.of(/* left= */ margin, top, /* right= */ margin, bottom); + } + + private boolean isTaskbarExpanded() { + return mExpandedTaskbarInsetsSource != null; + } + } +} 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/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/NonResizeableActivity.java b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/qualifier/UnfoldShellTransition.java index 24275e002c7f..4c868305dcdb 100644 --- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/NonResizeableActivity.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/qualifier/UnfoldShellTransition.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 The Android Open Source Project + * Copyright (C) 2022 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,16 +14,16 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.testapp; +package com.android.wm.shell.unfold.qualifier; -import android.app.Activity; -import android.os.Bundle; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; -public class NonResizeableActivity extends Activity { +import javax.inject.Qualifier; - @Override - protected void onCreate(Bundle icicle) { - super.onCreate(icicle); - setContentView(R.layout.activity_non_resizeable); - } -} +/** + * Indicates that this class is used for the shell unfold transition + */ +@Qualifier +@Retention(RetentionPolicy.RUNTIME) +public @interface UnfoldShellTransition {} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/qualifier/UnfoldTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/qualifier/UnfoldTransition.java new file mode 100644 index 000000000000..4d2b3e6f899b --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/qualifier/UnfoldTransition.java @@ -0,0 +1,30 @@ +/* + * 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.qualifier; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import javax.inject.Qualifier; + +/** + * Indicates that this class is used for unfold transition implemented + * without using Shell transitions + */ +@Qualifier +@Retention(RetentionPolicy.RUNTIME) +public @interface UnfoldTransition {} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/util/GroupedRecentTaskInfo.java b/libs/WindowManager/Shell/src/com/android/wm/shell/util/GroupedRecentTaskInfo.java index 603d05d78fc0..c045cebdf4e0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/util/GroupedRecentTaskInfo.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/util/GroupedRecentTaskInfo.java @@ -16,6 +16,7 @@ package com.android.wm.shell.util; +import android.annotation.IntDef; import android.app.ActivityManager; import android.app.WindowConfiguration; import android.os.Parcel; @@ -24,40 +25,143 @@ import android.os.Parcelable; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import java.util.Arrays; +import java.util.List; + /** * Simple container for recent tasks. May contain either a single or pair of tasks. */ public class GroupedRecentTaskInfo implements Parcelable { - public @NonNull ActivityManager.RecentTaskInfo mTaskInfo1; - public @Nullable ActivityManager.RecentTaskInfo mTaskInfo2; - public @Nullable StagedSplitBounds mStagedSplitBounds; - public GroupedRecentTaskInfo(@NonNull ActivityManager.RecentTaskInfo task1) { - this(task1, null, null); + public static final int TYPE_SINGLE = 1; + public static final int TYPE_SPLIT = 2; + public static final int TYPE_FREEFORM = 3; + + @IntDef(prefix = {"TYPE_"}, value = { + TYPE_SINGLE, + TYPE_SPLIT, + TYPE_FREEFORM + }) + public @interface GroupType {} + + @NonNull + private final ActivityManager.RecentTaskInfo[] mTasks; + @Nullable + private final SplitBounds mSplitBounds; + @GroupType + private final int mType; + + /** + * Create new for a single task + */ + public static GroupedRecentTaskInfo forSingleTask( + @NonNull ActivityManager.RecentTaskInfo task) { + return new GroupedRecentTaskInfo(new ActivityManager.RecentTaskInfo[]{task}, null, + TYPE_SINGLE); + } + + /** + * Create new for a pair of tasks in split screen + */ + public static GroupedRecentTaskInfo forSplitTasks(@NonNull ActivityManager.RecentTaskInfo task1, + @NonNull ActivityManager.RecentTaskInfo task2, @Nullable SplitBounds splitBounds) { + return new GroupedRecentTaskInfo(new ActivityManager.RecentTaskInfo[]{task1, task2}, + splitBounds, TYPE_SPLIT); } - public GroupedRecentTaskInfo(@NonNull ActivityManager.RecentTaskInfo task1, - @Nullable ActivityManager.RecentTaskInfo task2, - @Nullable StagedSplitBounds stagedSplitBounds) { - mTaskInfo1 = task1; - mTaskInfo2 = task2; - mStagedSplitBounds = stagedSplitBounds; + /** + * Create new for a group of freeform tasks + */ + public static GroupedRecentTaskInfo forFreeformTasks( + @NonNull ActivityManager.RecentTaskInfo... tasks) { + return new GroupedRecentTaskInfo(tasks, null, TYPE_FREEFORM); + } + + private GroupedRecentTaskInfo(@NonNull ActivityManager.RecentTaskInfo[] tasks, + @Nullable SplitBounds splitBounds, @GroupType int type) { + mTasks = tasks; + mSplitBounds = splitBounds; + mType = type; } GroupedRecentTaskInfo(Parcel parcel) { - mTaskInfo1 = parcel.readTypedObject(ActivityManager.RecentTaskInfo.CREATOR); - mTaskInfo2 = parcel.readTypedObject(ActivityManager.RecentTaskInfo.CREATOR); - mStagedSplitBounds = parcel.readTypedObject(StagedSplitBounds.CREATOR); + mTasks = parcel.createTypedArray(ActivityManager.RecentTaskInfo.CREATOR); + mSplitBounds = parcel.readTypedObject(SplitBounds.CREATOR); + mType = parcel.readInt(); + } + + /** + * Get primary {@link ActivityManager.RecentTaskInfo} + */ + @NonNull + public ActivityManager.RecentTaskInfo getTaskInfo1() { + return mTasks[0]; + } + + /** + * Get secondary {@link ActivityManager.RecentTaskInfo}. + * + * Used in split screen. + */ + @Nullable + public ActivityManager.RecentTaskInfo getTaskInfo2() { + if (mTasks.length > 1) { + return mTasks[1]; + } + return null; + } + + /** + * Get all {@link ActivityManager.RecentTaskInfo}s grouped together. + */ + @NonNull + public List<ActivityManager.RecentTaskInfo> getTaskInfoList() { + return Arrays.asList(mTasks); + } + + /** + * Return {@link SplitBounds} if this is a split screen entry or {@code null} + */ + @Nullable + public SplitBounds getSplitBounds() { + return mSplitBounds; + } + + /** + * Get type of this recents entry. One of {@link GroupType} + */ + @GroupType + public int getType() { + return mType; } @Override public String toString() { - String taskString = "Task1: " + getTaskInfo(mTaskInfo1) - + ", Task2: " + getTaskInfo(mTaskInfo2); - if (mStagedSplitBounds != null) { - taskString += ", SplitBounds: " + mStagedSplitBounds.toString(); + StringBuilder taskString = new StringBuilder(); + for (int i = 0; i < mTasks.length; i++) { + if (i == 0) { + taskString.append("Task"); + } else { + taskString.append(", Task"); + } + taskString.append(i + 1).append(": ").append(getTaskInfo(mTasks[i])); + } + if (mSplitBounds != null) { + taskString.append(", SplitBounds: ").append(mSplitBounds); + } + taskString.append(", Type="); + switch (mType) { + case TYPE_SINGLE: + taskString.append("TYPE_SINGLE"); + break; + case TYPE_SPLIT: + taskString.append("TYPE_SPLIT"); + break; + case TYPE_FREEFORM: + taskString.append("TYPE_FREEFORM"); + break; } - return taskString; + return taskString.toString(); } private String getTaskInfo(ActivityManager.RecentTaskInfo taskInfo) { @@ -74,9 +178,9 @@ public class GroupedRecentTaskInfo implements Parcelable { @Override public void writeToParcel(Parcel parcel, int flags) { - parcel.writeTypedObject(mTaskInfo1, flags); - parcel.writeTypedObject(mTaskInfo2, flags); - parcel.writeTypedObject(mStagedSplitBounds, flags); + parcel.writeTypedArray(mTasks, flags); + parcel.writeTypedObject(mSplitBounds, flags); + parcel.writeInt(mType); } @Override diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/util/StagedSplitBounds.java b/libs/WindowManager/Shell/src/com/android/wm/shell/util/SplitBounds.java index a0c84cc33ebd..f209521b1da4 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 */ @@ -33,6 +33,8 @@ public class StagedSplitBounds implements Parcelable { // This class is orientation-agnostic, so we compute both for later use public final float topTaskPercent; public final float leftTaskPercent; + public final float dividerWidthPercent; + public final float dividerHeightPercent; /** * If {@code true}, that means at the time of creation of this object, the * split-screened apps were vertically stacked. This is useful in scenarios like @@ -43,7 +45,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; @@ -62,11 +64,15 @@ public class StagedSplitBounds implements Parcelable { appsStackedVertically = false; } - leftTaskPercent = this.leftTopBounds.width() / (float) rightBottomBounds.right; - topTaskPercent = this.leftTopBounds.height() / (float) rightBottomBounds.bottom; + float totalWidth = rightBottomBounds.right - leftTopBounds.left; + float totalHeight = rightBottomBounds.bottom - leftTopBounds.top; + leftTaskPercent = leftTopBounds.width() / totalWidth; + topTaskPercent = leftTopBounds.height() / totalHeight; + dividerWidthPercent = visualDividerBounds.width() / totalWidth; + dividerHeightPercent = visualDividerBounds.height() / totalHeight; } - public StagedSplitBounds(Parcel parcel) { + public SplitBounds(Parcel parcel) { leftTopBounds = parcel.readTypedObject(Rect.CREATOR); rightBottomBounds = parcel.readTypedObject(Rect.CREATOR); visualDividerBounds = parcel.readTypedObject(Rect.CREATOR); @@ -75,6 +81,8 @@ public class StagedSplitBounds implements Parcelable { appsStackedVertically = parcel.readBoolean(); leftTopTaskId = parcel.readInt(); rightBottomTaskId = parcel.readInt(); + dividerWidthPercent = parcel.readInt(); + dividerHeightPercent = parcel.readInt(); } @Override @@ -87,6 +95,8 @@ public class StagedSplitBounds implements Parcelable { parcel.writeBoolean(appsStackedVertically); parcel.writeInt(leftTopTaskId); parcel.writeInt(rightBottomTaskId); + parcel.writeFloat(dividerWidthPercent); + parcel.writeFloat(dividerHeightPercent); } @Override @@ -96,11 +106,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 +130,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/util/TransitionUtil.java b/libs/WindowManager/Shell/src/com/android/wm/shell/util/TransitionUtil.java new file mode 100644 index 000000000000..ce102917352d --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/util/TransitionUtil.java @@ -0,0 +1,290 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.util; + +import static android.app.ActivityTaskManager.INVALID_TASK_ID; +import static android.view.RemoteAnimationTarget.MODE_CHANGING; +import static android.view.RemoteAnimationTarget.MODE_CLOSING; +import static android.view.RemoteAnimationTarget.MODE_OPENING; +import static android.view.WindowManager.LayoutParams.INVALID_WINDOW_TYPE; +import static android.view.WindowManager.LayoutParams.TYPE_DOCK_DIVIDER; +import static android.view.WindowManager.TRANSIT_CHANGE; +import static android.view.WindowManager.TRANSIT_CLOSE; +import static android.view.WindowManager.TRANSIT_KEYGUARD_GOING_AWAY; +import static android.view.WindowManager.TRANSIT_OPEN; +import static android.view.WindowManager.TRANSIT_TO_BACK; +import static android.view.WindowManager.TRANSIT_TO_FRONT; +import static android.window.TransitionInfo.FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY; +import static android.window.TransitionInfo.FLAG_IS_DISPLAY; +import static android.window.TransitionInfo.FLAG_IS_WALLPAPER; +import static android.window.TransitionInfo.FLAG_MOVED_TO_TOP; +import static android.window.TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT; + +import static com.android.wm.shell.common.split.SplitScreenConstants.FLAG_IS_DIVIDER_BAR; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SuppressLint; +import android.app.ActivityManager; +import android.app.WindowConfiguration; +import android.graphics.Rect; +import android.util.ArrayMap; +import android.util.SparseBooleanArray; +import android.view.RemoteAnimationTarget; +import android.view.SurfaceControl; +import android.view.WindowManager; +import android.window.TransitionInfo; + +import java.util.function.Predicate; + +/** Various utility functions for transitions. */ +public class TransitionUtil { + + /** @return true if the transition was triggered by opening something vs closing something */ + public static boolean isOpeningType(@WindowManager.TransitionType int type) { + return type == TRANSIT_OPEN + || type == TRANSIT_TO_FRONT + || type == TRANSIT_KEYGUARD_GOING_AWAY; + } + + /** @return true if the transition was triggered by closing something vs opening something */ + public static boolean isClosingType(@WindowManager.TransitionType int type) { + return type == TRANSIT_CLOSE || type == TRANSIT_TO_BACK; + } + + /** Returns {@code true} if the transition has a display change. */ + public static boolean hasDisplayChange(@NonNull TransitionInfo info) { + for (int i = info.getChanges().size() - 1; i >= 0; --i) { + final TransitionInfo.Change change = info.getChanges().get(i); + if (change.getMode() == TRANSIT_CHANGE && change.hasFlags(FLAG_IS_DISPLAY)) { + return true; + } + } + return false; + } + + /** Returns `true` if `change` is a wallpaper. */ + public static boolean isWallpaper(TransitionInfo.Change change) { + return (change.getTaskInfo() == null) + && change.hasFlags(FLAG_IS_WALLPAPER) + && !change.hasFlags(FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY); + } + + /** Returns `true` if `change` is not an app window or wallpaper. */ + public static boolean isNonApp(TransitionInfo.Change change) { + return (change.getTaskInfo() == null) + && !change.hasFlags(FLAG_IS_WALLPAPER) + && !change.hasFlags(FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY); + } + + /** Returns `true` if `change` is only re-ordering. */ + public static boolean isOrderOnly(TransitionInfo.Change change) { + return change.getMode() == TRANSIT_CHANGE + && (change.getFlags() & FLAG_MOVED_TO_TOP) != 0 + && change.getStartAbsBounds().equals(change.getEndAbsBounds()) + && (change.getLastParent() == null + || change.getLastParent().equals(change.getParent())); + } + + /** + * Filter that selects leaf-tasks only. THIS IS ORDER-DEPENDENT! For it to work properly, you + * MUST call `test` in the same order that the changes appear in the TransitionInfo. + */ + public static class LeafTaskFilter implements Predicate<TransitionInfo.Change> { + private final SparseBooleanArray mChildTaskTargets = new SparseBooleanArray(); + + @Override + public boolean test(TransitionInfo.Change change) { + final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo(); + // Children always come before parent since changes are in top-to-bottom z-order. + if ((taskInfo == null) || mChildTaskTargets.get(taskInfo.taskId)) { + // has children, so not a leaf. Skip. + return false; + } + if (taskInfo.hasParentTask()) { + mChildTaskTargets.put(taskInfo.parentTaskId, true); + } + return true; + } + } + + + private static int newModeToLegacyMode(int newMode) { + switch (newMode) { + case WindowManager.TRANSIT_OPEN: + case WindowManager.TRANSIT_TO_FRONT: + return MODE_OPENING; + case WindowManager.TRANSIT_CLOSE: + case WindowManager.TRANSIT_TO_BACK: + return MODE_CLOSING; + default: + return MODE_CHANGING; + } + } + + /** + * Very similar to Transitions#setupAnimHierarchy but specialized for leashes. + */ + @SuppressLint("NewApi") + private static void setupLeash(@NonNull SurfaceControl leash, + @NonNull TransitionInfo.Change change, int layer, + @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction t) { + final boolean isOpening = TransitionUtil.isOpeningType(info.getType()); + // Put animating stuff above this line and put static stuff below it. + int zSplitLine = info.getChanges().size(); + // changes should be ordered top-to-bottom in z + final int mode = change.getMode(); + + final int rootIdx = TransitionUtil.rootIndexFor(change, info); + t.reparent(leash, info.getRoot(rootIdx).getLeash()); + final Rect absBounds = + (mode == TRANSIT_OPEN) ? change.getEndAbsBounds() : change.getStartAbsBounds(); + t.setPosition(leash, absBounds.left - info.getRoot(rootIdx).getOffset().x, + absBounds.top - info.getRoot(rootIdx).getOffset().y); + + // Put all the OPEN/SHOW on top + if (TransitionUtil.isOpeningType(mode)) { + if (isOpening) { + t.setLayer(leash, zSplitLine + info.getChanges().size() - layer); + if ((change.getFlags() & FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT) == 0) { + // if transferred, it should be left visible. + t.setAlpha(leash, 0.f); + } + } else { + // put on bottom and leave it visible + t.setLayer(leash, zSplitLine - layer); + } + } else if (TransitionUtil.isClosingType(mode)) { + if (isOpening) { + // put on bottom and leave visible + t.setLayer(leash, zSplitLine - layer); + } else { + // put on top + t.setLayer(leash, zSplitLine + info.getChanges().size() - layer); + } + } else { // CHANGE + t.setLayer(leash, zSplitLine + info.getChanges().size() - layer); + } + } + + @SuppressLint("NewApi") + private static SurfaceControl createLeash(TransitionInfo info, TransitionInfo.Change change, + int order, SurfaceControl.Transaction t) { + // TODO: once we can properly sync transactions across process, then get rid of this leash. + if (change.getParent() != null && (change.getFlags() & FLAG_IS_WALLPAPER) != 0) { + // Special case for wallpaper atm. Normally these are left alone; but, a quirk of + // making leashes means we have to handle them specially. + return change.getLeash(); + } + final int rootIdx = TransitionUtil.rootIndexFor(change, info); + SurfaceControl leashSurface = new SurfaceControl.Builder() + .setName(change.getLeash().toString() + "_transition-leash") + .setContainerLayer() + // Initial the surface visible to respect the visibility of the original surface. + .setHidden(false) + .setParent(info.getRoot(rootIdx).getLeash()) + .build(); + // Copied Transitions setup code (which expects bottom-to-top order, so we swap here) + setupLeash(leashSurface, change, info.getChanges().size() - order, info, t); + t.reparent(change.getLeash(), leashSurface); + t.setAlpha(change.getLeash(), 1.0f); + t.show(change.getLeash()); + t.setPosition(change.getLeash(), 0, 0); + t.setLayer(change.getLeash(), 0); + return leashSurface; + } + + /** + * Creates a new RemoteAnimationTarget from the provided change info + */ + public static RemoteAnimationTarget newTarget(TransitionInfo.Change change, int order, + TransitionInfo info, SurfaceControl.Transaction t, + @Nullable ArrayMap<SurfaceControl, SurfaceControl> leashMap) { + final SurfaceControl leash = createLeash(info, change, order, t); + if (leashMap != null) { + leashMap.put(change.getLeash(), leash); + } + return newTarget(change, order, leash); + } + + /** + * Creates a new RemoteAnimationTarget from the provided change and leash + */ + public static RemoteAnimationTarget newTarget(TransitionInfo.Change change, int order, + SurfaceControl leash) { + int taskId; + boolean isNotInRecents; + ActivityManager.RunningTaskInfo taskInfo; + WindowConfiguration windowConfiguration; + + taskInfo = change.getTaskInfo(); + if (taskInfo != null) { + taskId = taskInfo.taskId; + isNotInRecents = !taskInfo.isRunning; + windowConfiguration = taskInfo.configuration.windowConfiguration; + } else { + taskId = INVALID_TASK_ID; + isNotInRecents = true; + windowConfiguration = new WindowConfiguration(); + } + + Rect localBounds = new Rect(change.getEndAbsBounds()); + localBounds.offsetTo(change.getEndRelOffset().x, change.getEndRelOffset().y); + + RemoteAnimationTarget target = new RemoteAnimationTarget( + taskId, + newModeToLegacyMode(change.getMode()), + // TODO: once we can properly sync transactions across process, + // then get rid of this leash. + leash, + (change.getFlags() & TransitionInfo.FLAG_TRANSLUCENT) != 0, + null, + // TODO(shell-transitions): we need to send content insets? evaluate how its used. + new Rect(0, 0, 0, 0), + order, + null, + localBounds, + new Rect(change.getEndAbsBounds()), + windowConfiguration, + isNotInRecents, + null, + new Rect(change.getStartAbsBounds()), + taskInfo, + change.getAllowEnterPip(), + (change.getFlags() & FLAG_IS_DIVIDER_BAR) != 0 + ? TYPE_DOCK_DIVIDER : INVALID_WINDOW_TYPE + ); + target.setWillShowImeOnTarget( + (change.getFlags() & TransitionInfo.FLAG_WILL_IME_SHOWN) != 0); + target.setRotationChange(change.getEndRotation() - change.getStartRotation()); + return target; + } + + /** + * Finds the "correct" root idx for a change. The change's end display is prioritized, then + * the start display. If there is no display, it will fallback on the 0th root in the + * transition. There MUST be at-least 1 root in the transition (ie. it's not a no-op). + */ + public static int rootIndexFor(@NonNull TransitionInfo.Change change, + @NonNull TransitionInfo info) { + int rootIdx = info.findRootIndex(change.getEndDisplayId()); + if (rootIdx >= 0) return rootIdx; + rootIdx = info.findRootIndex(change.getStartDisplayId()); + if (rootIdx >= 0) return rootIdx; + return 0; + } +} 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..8e8facadfd5d --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java @@ -0,0 +1,288 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.windowdecor; + +import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; +import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; +import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; + +import android.app.ActivityManager.RunningTaskInfo; +import android.content.Context; +import android.os.Handler; +import android.os.IBinder; +import android.util.SparseArray; +import android.view.Choreographer; +import android.view.MotionEvent; +import android.view.SurfaceControl; +import android.view.View; +import android.window.TransitionInfo; +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.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 { + 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 TaskOperations mTaskOperations; + + private final SparseArray<CaptionWindowDecoration> mWindowDecorByTaskId = new SparseArray<>(); + + public CaptionWindowDecorViewModel( + Context context, + Handler mainHandler, + Choreographer mainChoreographer, + ShellTaskOrganizer taskOrganizer, + DisplayController displayController, + SyncTransactionQueue syncQueue) { + mContext = context; + mMainHandler = mainHandler; + mMainChoreographer = mainChoreographer; + mTaskOrganizer = taskOrganizer; + mDisplayController = displayController; + mSyncQueue = syncQueue; + if (!Transitions.ENABLE_SHELL_TRANSITIONS) { + mTaskOperations = new TaskOperations(null, mContext, mSyncQueue); + } + } + + @Override + public void onTransitionReady(IBinder transition, TransitionInfo info, + TransitionInfo.Change change) {} + + @Override + public void onTransitionMerged(IBinder merged, IBinder playing) {} + + @Override + public void onTransitionFinished(IBinder transition) {} + + @Override + public void setFreeformTaskTransitionStarter(FreeformTaskTransitionStarter transitionStarter) { + mTaskOperations = new TaskOperations(transitionStarter, mContext, mSyncQueue); + } + + @Override + public boolean onTaskOpening( + RunningTaskInfo taskInfo, + SurfaceControl taskSurface, + SurfaceControl.Transaction startT, + SurfaceControl.Transaction finishT) { + if (!shouldShowWindowDecor(taskInfo)) return false; + createWindowDecoration(taskInfo, taskSurface, startT, finishT); + return true; + } + + @Override + public void onTaskInfoChanged(RunningTaskInfo taskInfo) { + final CaptionWindowDecoration decoration = mWindowDecorByTaskId.get(taskInfo.taskId); + + if (decoration == null) return; + + decoration.relayout(taskInfo); + setupCaptionColor(taskInfo, decoration); + } + + @Override + public void onTaskChanging( + RunningTaskInfo taskInfo, + SurfaceControl taskSurface, + SurfaceControl.Transaction startT, + SurfaceControl.Transaction finishT) { + final CaptionWindowDecoration decoration = mWindowDecorByTaskId.get(taskInfo.taskId); + + if (!shouldShowWindowDecor(taskInfo)) { + if (decoration != null) { + destroyWindowDecoration(taskInfo); + } + return; + } + + if (decoration == null) { + createWindowDecoration(taskInfo, taskSurface, startT, finishT); + } else { + decoration.relayout(taskInfo, startT, finishT); + } + } + + @Override + public void onTaskClosing( + RunningTaskInfo taskInfo, + SurfaceControl.Transaction startT, + SurfaceControl.Transaction finishT) { + final CaptionWindowDecoration decoration = mWindowDecorByTaskId.get(taskInfo.taskId); + if (decoration == null) return; + + decoration.relayout(taskInfo, startT, finishT); + } + + @Override + public void destroyWindowDecoration(RunningTaskInfo taskInfo) { + final CaptionWindowDecoration decoration = + mWindowDecorByTaskId.removeReturnOld(taskInfo.taskId); + if (decoration == null) return; + + decoration.close(); + } + + private void setupCaptionColor(RunningTaskInfo taskInfo, CaptionWindowDecoration decoration) { + final int statusBarColor = taskInfo.taskDescription.getStatusBarColor(); + decoration.setCaptionColor(statusBarColor); + } + + private boolean shouldShowWindowDecor(RunningTaskInfo taskInfo) { + return taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM + || (taskInfo.getActivityType() == ACTIVITY_TYPE_STANDARD + && taskInfo.configuration.windowConfiguration.getDisplayWindowingMode() + == WINDOWING_MODE_FREEFORM); + } + + private void createWindowDecoration( + RunningTaskInfo taskInfo, + SurfaceControl taskSurface, + SurfaceControl.Transaction startT, + SurfaceControl.Transaction finishT) { + final CaptionWindowDecoration oldDecoration = mWindowDecorByTaskId.get(taskInfo.taskId); + if (oldDecoration != null) { + // close the old decoration if it exists to avoid two window decorations being added + oldDecoration.close(); + } + final CaptionWindowDecoration windowDecoration = + new CaptionWindowDecoration( + mContext, + mDisplayController, + mTaskOrganizer, + taskInfo, + taskSurface, + mMainHandler, + mMainChoreographer, + mSyncQueue); + mWindowDecorByTaskId.put(taskInfo.taskId, windowDecoration); + + final TaskPositioner taskPositioner = + new TaskPositioner(mTaskOrganizer, windowDecoration, mDisplayController); + final CaptionTouchEventListener touchEventListener = + new CaptionTouchEventListener(taskInfo, taskPositioner); + windowDecoration.setCaptionListeners(touchEventListener, touchEventListener); + windowDecoration.setDragPositioningCallback(taskPositioner); + windowDecoration.setDragDetector(touchEventListener.mDragDetector); + windowDecoration.relayout(taskInfo, startT, finishT); + setupCaptionColor(taskInfo, windowDecoration); + } + + private class CaptionTouchEventListener implements + View.OnClickListener, View.OnTouchListener, DragDetector.MotionEventHandler { + + private final int mTaskId; + private final WindowContainerToken mTaskToken; + private final DragPositioningCallback mDragPositioningCallback; + private final DragDetector mDragDetector; + + private int mDragPointerId = -1; + private boolean mIsDragging; + + private CaptionTouchEventListener( + RunningTaskInfo taskInfo, + DragPositioningCallback dragPositioningCallback) { + mTaskId = taskInfo.taskId; + mTaskToken = taskInfo.token; + mDragPositioningCallback = dragPositioningCallback; + mDragDetector = new DragDetector(this); + } + + @Override + public void onClick(View v) { + final int id = v.getId(); + if (id == R.id.close_window) { + mTaskOperations.closeTask(mTaskToken); + } else if (id == R.id.back_button) { + mTaskOperations.injectBackKey(); + } else if (id == R.id.minimize_window) { + mTaskOperations.minimizeTask(mTaskToken); + } else if (id == R.id.maximize_window) { + RunningTaskInfo taskInfo = mTaskOrganizer.getRunningTaskInfo(mTaskId); + mTaskOperations.maximizeTask(taskInfo); + } + } + + @Override + public boolean onTouch(View v, MotionEvent e) { + if (v.getId() != R.id.caption) { + return false; + } + if (e.getAction() == MotionEvent.ACTION_DOWN) { + final RunningTaskInfo taskInfo = mTaskOrganizer.getRunningTaskInfo(mTaskId); + if (!taskInfo.isFocused) { + final WindowContainerTransaction wct = new WindowContainerTransaction(); + wct.reorder(mTaskToken, true /* onTop */); + mSyncQueue.queue(wct); + } + } + return mDragDetector.onMotionEvent(e); + } + + /** + * @param e {@link MotionEvent} to process + * @return {@code true} if a drag is happening; or {@code false} if it is not + */ + @Override + public boolean handleMotionEvent(MotionEvent e) { + final RunningTaskInfo taskInfo = mTaskOrganizer.getRunningTaskInfo(mTaskId); + if (taskInfo.getWindowingMode() == WINDOWING_MODE_FULLSCREEN) { + return false; + } + switch (e.getActionMasked()) { + case MotionEvent.ACTION_DOWN: { + mDragPointerId = e.getPointerId(0); + mDragPositioningCallback.onDragPositioningStart( + 0 /* ctrlType */, e.getRawX(0), e.getRawY(0)); + mIsDragging = false; + return false; + } + case MotionEvent.ACTION_MOVE: { + int dragPointerIdx = e.findPointerIndex(mDragPointerId); + mDragPositioningCallback.onDragPositioningMove( + e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx)); + mIsDragging = true; + return true; + } + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: { + int dragPointerIdx = e.findPointerIndex(mDragPointerId); + mDragPositioningCallback.onDragPositioningEnd( + e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx)); + final boolean wasDragging = mIsDragging; + mIsDragging = false; + return wasDragging; + } + } + return true; + } + } +}
\ No newline at end of file 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..060dc4e05b46 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java @@ -0,0 +1,231 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.windowdecor; + +import android.app.ActivityManager.RunningTaskInfo; +import android.app.WindowConfiguration; +import android.content.Context; +import android.content.res.ColorStateList; +import android.graphics.Color; +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.view.ViewConfiguration; +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; + +/** + * Defines visuals and behaviors of a window decoration of a caption bar and shadows. It works with + * {@link CaptionWindowDecorViewModel}. The caption bar contains a back button, minimize button, + * maximize button and close button. + */ +public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearLayout> { + private final Handler mHandler; + private final Choreographer mChoreographer; + private final SyncTransactionQueue mSyncQueue; + + private View.OnClickListener mOnCaptionButtonClickListener; + private View.OnTouchListener mOnCaptionTouchListener; + private DragPositioningCallback mDragPositioningCallback; + private DragResizeInputListener mDragResizeListener; + private DragDetector mDragDetector; + + private RelayoutParams mRelayoutParams = new RelayoutParams(); + private final RelayoutResult<WindowDecorLinearLayout> mResult = + new RelayoutResult<>(); + + CaptionWindowDecoration( + Context context, + DisplayController displayController, + ShellTaskOrganizer taskOrganizer, + 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 setDragPositioningCallback(DragPositioningCallback dragPositioningCallback) { + mDragPositioningCallback = dragPositioningCallback; + } + + void setDragDetector(DragDetector dragDetector) { + mDragDetector = dragDetector; + mDragDetector.setTouchSlop(ViewConfiguration.get(mContext).getScaledTouchSlop()); + } + + @Override + void relayout(RunningTaskInfo taskInfo) { + final SurfaceControl.Transaction t = new SurfaceControl.Transaction(); + relayout(taskInfo, t, t); + mSyncQueue.runInSync(transaction -> { + transaction.merge(t); + t.close(); + }); + } + + void relayout(RunningTaskInfo taskInfo, + SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT) { + final int shadowRadiusID = taskInfo.isFocused + ? R.dimen.freeform_decor_shadow_focused_thickness + : R.dimen.freeform_decor_shadow_unfocused_thickness; + final boolean isFreeform = + taskInfo.getWindowingMode() == WindowConfiguration.WINDOWING_MODE_FREEFORM; + final boolean isDragResizeable = isFreeform && taskInfo.isResizeable; + + final WindowDecorLinearLayout oldRootView = mResult.mRootView; + final SurfaceControl oldDecorationSurface = mDecorationContainerSurface; + final WindowContainerTransaction wct = new WindowContainerTransaction(); + + final int outsetLeftId = R.dimen.freeform_resize_handle; + final int outsetTopId = R.dimen.freeform_resize_handle; + final int outsetRightId = R.dimen.freeform_resize_handle; + final int outsetBottomId = R.dimen.freeform_resize_handle; + + mRelayoutParams.reset(); + mRelayoutParams.mRunningTaskInfo = taskInfo; + mRelayoutParams.mLayoutResId = R.layout.caption_window_decor; + mRelayoutParams.mCaptionHeightId = R.dimen.freeform_decor_caption_height; + mRelayoutParams.mShadowRadiusId = shadowRadiusID; + if (isDragResizeable) { + mRelayoutParams.setOutsets(outsetLeftId, outsetTopId, outsetRightId, outsetBottomId); + } + + relayout(mRelayoutParams, startT, finishT, wct, oldRootView, mResult); + // After this line, mTaskInfo is up-to-date and should be used instead of taskInfo + + mTaskOrganizer.applyTransaction(wct); + + 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, + mDragPositioningCallback); + } + + final int touchSlop = ViewConfiguration.get(mResult.mRootView.getContext()) + .getScaledTouchSlop(); + mDragDetector.setTouchSlop(touchSlop); + + final int resize_handle = mResult.mRootView.getResources() + .getDimensionPixelSize(R.dimen.freeform_resize_handle); + final int resize_corner = mResult.mRootView.getResources() + .getDimensionPixelSize(R.dimen.freeform_resize_corner); + mDragResizeListener.setGeometry( + mResult.mWidth, mResult.mHeight, resize_handle, resize_corner, touchSlop); + } + + /** + * Sets up listeners when a new root view is created. + */ + private void setupRootView() { + final View caption = mResult.mRootView.findViewById(R.id.caption); + caption.setOnTouchListener(mOnCaptionTouchListener); + final View close = caption.findViewById(R.id.close_window); + close.setOnClickListener(mOnCaptionButtonClickListener); + final View back = caption.findViewById(R.id.back_button); + back.setOnClickListener(mOnCaptionButtonClickListener); + final View minimize = caption.findViewById(R.id.minimize_window); + minimize.setOnClickListener(mOnCaptionButtonClickListener); + final View maximize = caption.findViewById(R.id.maximize_window); + maximize.setOnClickListener(mOnCaptionButtonClickListener); + } + + void setCaptionColor(int captionColor) { + if (mResult.mRootView == null) { + return; + } + + final View caption = mResult.mRootView.findViewById(R.id.caption); + final GradientDrawable captionDrawable = (GradientDrawable) caption.getBackground(); + captionDrawable.setColor(captionColor); + + final int buttonTintColorRes = + Color.valueOf(captionColor).luminance() < 0.5 + ? R.color.decor_button_light_color + : R.color.decor_button_dark_color; + final ColorStateList buttonTintColor = + caption.getResources().getColorStateList(buttonTintColorRes, null /* theme */); + + final View back = caption.findViewById(R.id.back_button); + final VectorDrawable backBackground = (VectorDrawable) back.getBackground(); + backBackground.setTintList(buttonTintColor); + + final View minimize = caption.findViewById(R.id.minimize_window); + final VectorDrawable minimizeBackground = (VectorDrawable) minimize.getBackground(); + minimizeBackground.setTintList(buttonTintColor); + + final View maximize = caption.findViewById(R.id.maximize_window); + final VectorDrawable maximizeBackground = (VectorDrawable) maximize.getBackground(); + maximizeBackground.setTintList(buttonTintColor); + + final View close = caption.findViewById(R.id.close_window); + final 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/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java new file mode 100644 index 000000000000..c0dcd0b68c6f --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java @@ -0,0 +1,790 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.windowdecor; + +import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; +import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; +import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; + +import static 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.desktopmode.EnterDesktopTaskTransitionHandler.DRAG_FREEFORM_SCALE; +import static com.android.wm.shell.desktopmode.EnterDesktopTaskTransitionHandler.FINAL_FREEFORM_SCALE; +import static com.android.wm.shell.desktopmode.EnterDesktopTaskTransitionHandler.FREEFORM_ANIMATION_DURATION; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; +import android.app.ActivityManager; +import android.app.ActivityManager.RunningTaskInfo; +import android.app.ActivityTaskManager; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Rect; +import android.hardware.input.InputManager; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.util.DisplayMetrics; +import android.util.SparseArray; +import android.view.Choreographer; +import android.view.InputChannel; +import android.view.InputEvent; +import android.view.InputEventReceiver; +import android.view.InputMonitor; +import android.view.MotionEvent; +import android.view.SurfaceControl; +import android.view.SurfaceControl.Transaction; +import android.view.View; +import android.view.WindowManager; +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.R; +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.common.DisplayController; +import com.android.wm.shell.common.SyncTransactionQueue; +import com.android.wm.shell.desktopmode.DesktopModeController; +import com.android.wm.shell.desktopmode.DesktopModeStatus; +import com.android.wm.shell.desktopmode.DesktopTasksController; +import com.android.wm.shell.freeform.FreeformTaskTransitionStarter; +import com.android.wm.shell.splitscreen.SplitScreenController; +import com.android.wm.shell.transition.Transitions; + +import java.util.Optional; +import java.util.function.Supplier; + +/** + * View model for the window decoration with a caption and shadows. Works with + * {@link DesktopModeWindowDecoration}. + */ + +public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { + private static final String TAG = "DesktopModeWindowDecorViewModel"; + private final DesktopModeWindowDecoration.Factory mDesktopModeWindowDecorFactory; + 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 final Optional<DesktopModeController> mDesktopModeController; + private final Optional<DesktopTasksController> mDesktopTasksController; + private boolean mTransitionDragActive; + + private SparseArray<EventReceiver> mEventReceiversByDisplay = new SparseArray<>(); + + private final SparseArray<DesktopModeWindowDecoration> mWindowDecorByTaskId = + new SparseArray<>(); + private final DragStartListenerImpl mDragStartListener = new DragStartListenerImpl(); + private final InputMonitorFactory mInputMonitorFactory; + private TaskOperations mTaskOperations; + private final Supplier<SurfaceControl.Transaction> mTransactionFactory; + + private Optional<SplitScreenController> mSplitScreenController; + + private ValueAnimator mDragToDesktopValueAnimator; + private final Rect mDragToDesktopAnimationStartBounds = new Rect(); + private boolean mDragToDesktopAnimationStarted; + private float mCaptionDragStartX; + + // These values keep track of any transitions to freeform to stop relayout from running on + // changing task so that shellTransitions has a chance to animate the transition + private int mPauseRelayoutForTask = -1; + private IBinder mTransitionPausingRelayout; + + public DesktopModeWindowDecorViewModel( + Context context, + Handler mainHandler, + Choreographer mainChoreographer, + ShellTaskOrganizer taskOrganizer, + DisplayController displayController, + SyncTransactionQueue syncQueue, + Optional<DesktopModeController> desktopModeController, + Optional<DesktopTasksController> desktopTasksController, + Optional<SplitScreenController> splitScreenController) { + this( + context, + mainHandler, + mainChoreographer, + taskOrganizer, + displayController, + syncQueue, + desktopModeController, + desktopTasksController, + splitScreenController, + new DesktopModeWindowDecoration.Factory(), + new InputMonitorFactory(), + SurfaceControl.Transaction::new); + } + + @VisibleForTesting + DesktopModeWindowDecorViewModel( + Context context, + Handler mainHandler, + Choreographer mainChoreographer, + ShellTaskOrganizer taskOrganizer, + DisplayController displayController, + SyncTransactionQueue syncQueue, + Optional<DesktopModeController> desktopModeController, + Optional<DesktopTasksController> desktopTasksController, + Optional<SplitScreenController> splitScreenController, + DesktopModeWindowDecoration.Factory desktopModeWindowDecorFactory, + InputMonitorFactory inputMonitorFactory, + Supplier<SurfaceControl.Transaction> transactionFactory) { + mContext = context; + mMainHandler = mainHandler; + mMainChoreographer = mainChoreographer; + mActivityTaskManager = mContext.getSystemService(ActivityTaskManager.class); + mTaskOrganizer = taskOrganizer; + mDisplayController = displayController; + mSplitScreenController = splitScreenController; + mSyncQueue = syncQueue; + mDesktopModeController = desktopModeController; + mDesktopTasksController = desktopTasksController; + + mDesktopModeWindowDecorFactory = desktopModeWindowDecorFactory; + mInputMonitorFactory = inputMonitorFactory; + mTransactionFactory = transactionFactory; + } + + @Override + public void setFreeformTaskTransitionStarter(FreeformTaskTransitionStarter transitionStarter) { + mTaskOperations = new TaskOperations(transitionStarter, mContext, mSyncQueue); + } + + @Override + public boolean onTaskOpening( + ActivityManager.RunningTaskInfo taskInfo, + SurfaceControl taskSurface, + SurfaceControl.Transaction startT, + SurfaceControl.Transaction finishT) { + if (!shouldShowWindowDecor(taskInfo)) return false; + createWindowDecoration(taskInfo, taskSurface, startT, finishT); + return true; + } + + @Override + public void onTransitionReady( + @NonNull IBinder transition, + @NonNull TransitionInfo info, + @NonNull TransitionInfo.Change change) { + if (change.getMode() == WindowManager.TRANSIT_CHANGE + && info.getType() == Transitions.TRANSIT_ENTER_DESKTOP_MODE) { + mTransitionPausingRelayout = transition; + } + } + + @Override + public void onTransitionMerged(@NonNull IBinder merged, @NonNull IBinder playing) { + if (mTransitionPausingRelayout.equals(merged)) { + mTransitionPausingRelayout = playing; + } + } + + @Override + public void onTransitionFinished(@NonNull IBinder transition) { + if (transition.equals(mTransitionPausingRelayout)) { + mPauseRelayoutForTask = -1; + } + } + + @Override + public void onTaskInfoChanged(RunningTaskInfo taskInfo) { + final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(taskInfo.taskId); + if (decoration == null) return; + final RunningTaskInfo oldTaskInfo = decoration.mTaskInfo; + + if (taskInfo.displayId != oldTaskInfo.displayId) { + removeTaskFromEventReceiver(oldTaskInfo.displayId); + incrementEventReceiverTasks(taskInfo.displayId); + } + + // TaskListener callbacks and shell transitions aren't synchronized, so starting a shell + // transition can trigger an onTaskInfoChanged call that updates the task's SurfaceControl + // and interferes with the transition animation that is playing at the same time. + if (taskInfo.taskId != mPauseRelayoutForTask) { + decoration.relayout(taskInfo); + } + } + + @Override + public void onTaskChanging( + RunningTaskInfo taskInfo, + SurfaceControl taskSurface, + SurfaceControl.Transaction startT, + SurfaceControl.Transaction finishT) { + final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(taskInfo.taskId); + + if (!shouldShowWindowDecor(taskInfo)) { + if (decoration != null) { + destroyWindowDecoration(taskInfo); + } + return; + } + + if (decoration == null) { + createWindowDecoration(taskInfo, taskSurface, startT, finishT); + } else { + decoration.relayout(taskInfo, startT, finishT); + } + } + + @Override + public void onTaskClosing( + RunningTaskInfo taskInfo, + SurfaceControl.Transaction startT, + SurfaceControl.Transaction finishT) { + final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(taskInfo.taskId); + if (decoration == null) return; + + decoration.relayout(taskInfo, startT, finishT); + } + + @Override + public void destroyWindowDecoration(RunningTaskInfo taskInfo) { + final DesktopModeWindowDecoration decoration = + mWindowDecorByTaskId.removeReturnOld(taskInfo.taskId); + if (decoration == null) return; + + decoration.close(); + final int displayId = taskInfo.displayId; + if (mEventReceiversByDisplay.contains(displayId)) { + removeTaskFromEventReceiver(displayId); + } + } + + private class DesktopModeTouchEventListener implements + View.OnClickListener, View.OnTouchListener, DragDetector.MotionEventHandler { + + private final int mTaskId; + private final WindowContainerToken mTaskToken; + private final DragPositioningCallback mDragPositioningCallback; + private final DragDetector mDragDetector; + + private boolean mIsDragging; + private int mDragPointerId = -1; + + private DesktopModeTouchEventListener( + RunningTaskInfo taskInfo, + DragPositioningCallback dragPositioningCallback) { + mTaskId = taskInfo.taskId; + mTaskToken = taskInfo.token; + mDragPositioningCallback = dragPositioningCallback; + mDragDetector = new DragDetector(this); + } + + @Override + public void onClick(View v) { + final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(mTaskId); + final int id = v.getId(); + if (id == R.id.close_window || id == R.id.close_button) { + mTaskOperations.closeTask(mTaskToken); + if (mSplitScreenController.isPresent() + && mSplitScreenController.get().isSplitScreenVisible()) { + int remainingTaskPosition = mTaskId == mSplitScreenController.get() + .getTaskInfo(SPLIT_POSITION_TOP_OR_LEFT).taskId + ? SPLIT_POSITION_BOTTOM_OR_RIGHT : SPLIT_POSITION_TOP_OR_LEFT; + ActivityManager.RunningTaskInfo remainingTask = mSplitScreenController.get() + .getTaskInfo(remainingTaskPosition); + mSplitScreenController.get().moveTaskToFullscreen(remainingTask.taskId); + } + } else if (id == R.id.back_button) { + mTaskOperations.injectBackKey(); + } else if (id == R.id.caption_handle || id == R.id.open_menu_button) { + moveTaskToFront(mTaskOrganizer.getRunningTaskInfo(mTaskId)); + decoration.createHandleMenu(); + } else if (id == R.id.desktop_button) { + mDesktopModeController.ifPresent(c -> c.setDesktopModeActive(true)); + mDesktopTasksController.ifPresent(c -> c.moveToDesktop(mTaskId)); + decoration.closeHandleMenu(); + } else if (id == R.id.fullscreen_button) { + mDesktopModeController.ifPresent(c -> c.setDesktopModeActive(false)); + mDesktopTasksController.ifPresent(c -> c.moveToFullscreen(mTaskId)); + decoration.closeHandleMenu(); + } else if (id == R.id.collapse_menu_button) { + decoration.closeHandleMenu(); + } + } + + @Override + public boolean onTouch(View v, MotionEvent e) { + final int id = v.getId(); + if (id != R.id.caption_handle && id != R.id.desktop_mode_caption) { + return false; + } + moveTaskToFront(mTaskOrganizer.getRunningTaskInfo(mTaskId)); + return mDragDetector.onMotionEvent(e); + } + + private void moveTaskToFront(RunningTaskInfo taskInfo) { + if (!taskInfo.isFocused) { + mDesktopTasksController.ifPresent(c -> c.moveTaskToFront(taskInfo)); + mDesktopModeController.ifPresent(c -> c.moveTaskToFront(taskInfo)); + } + } + + /** + * @param e {@link MotionEvent} to process + * @return {@code true} if the motion event is handled. + */ + @Override + public boolean handleMotionEvent(MotionEvent e) { + final RunningTaskInfo taskInfo = mTaskOrganizer.getRunningTaskInfo(mTaskId); + if (DesktopModeStatus.isProto2Enabled() + && taskInfo.getWindowingMode() == WINDOWING_MODE_FULLSCREEN) { + return false; + } + if (DesktopModeStatus.isProto1Enabled() && mDesktopModeController.isPresent() + && mDesktopModeController.get().getDisplayAreaWindowingMode(taskInfo.displayId) + == WINDOWING_MODE_FULLSCREEN) { + return false; + } + switch (e.getActionMasked()) { + case MotionEvent.ACTION_DOWN: { + mDragPointerId = e.getPointerId(0); + mDragPositioningCallback.onDragPositioningStart( + 0 /* ctrlType */, e.getRawX(0), + e.getRawY(0)); + mIsDragging = false; + return false; + } + case MotionEvent.ACTION_MOVE: { + final DesktopModeWindowDecoration decoration = + mWindowDecorByTaskId.get(mTaskId); + final int dragPointerIdx = e.findPointerIndex(mDragPointerId); + mDesktopTasksController.ifPresent(c -> c.onDragPositioningMove(taskInfo, + decoration.mTaskSurface, e.getRawY(dragPointerIdx))); + mDragPositioningCallback.onDragPositioningMove( + e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx)); + mIsDragging = true; + return true; + } + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: { + final int dragPointerIdx = e.findPointerIndex(mDragPointerId); + mDragPositioningCallback.onDragPositioningEnd( + e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx)); + mDesktopTasksController.ifPresent(c -> c.onDragPositioningEnd(taskInfo, + e.getRawY(dragPointerIdx))); + final boolean wasDragging = mIsDragging; + mIsDragging = false; + return wasDragging; + } + } + return true; + } + } + + // InputEventReceiver to listen for touch input outside of caption bounds + class EventReceiver extends InputEventReceiver { + private InputMonitor mInputMonitor; + private int mTasksOnDisplay; + EventReceiver(InputMonitor inputMonitor, InputChannel channel, Looper looper) { + super(channel, looper); + mInputMonitor = inputMonitor; + mTasksOnDisplay = 1; + } + + @Override + public void onInputEvent(InputEvent event) { + boolean handled = false; + if (event instanceof MotionEvent) { + handled = true; + DesktopModeWindowDecorViewModel.this + .handleReceivedMotionEvent((MotionEvent) event, mInputMonitor); + } + finishInputEvent(event, handled); + } + + @Override + public void dispose() { + if (mInputMonitor != null) { + mInputMonitor.dispose(); + mInputMonitor = null; + } + super.dispose(); + } + + private void incrementTaskNumber() { + mTasksOnDisplay++; + } + + private void decrementTaskNumber() { + mTasksOnDisplay--; + } + + private int getTasksOnDisplay() { + return mTasksOnDisplay; + } + } + + /** + * Check if an EventReceiver exists on a particular display. + * If it does, increment its task count. Otherwise, create one for that display. + * @param displayId the display to check against + */ + private void incrementEventReceiverTasks(int displayId) { + if (mEventReceiversByDisplay.contains(displayId)) { + final EventReceiver eventReceiver = mEventReceiversByDisplay.get(displayId); + eventReceiver.incrementTaskNumber(); + } else { + createInputChannel(displayId); + } + } + + // If all tasks on this display are gone, we don't need to monitor its input. + private void removeTaskFromEventReceiver(int displayId) { + if (!mEventReceiversByDisplay.contains(displayId)) return; + final EventReceiver eventReceiver = mEventReceiversByDisplay.get(displayId); + if (eventReceiver == null) return; + eventReceiver.decrementTaskNumber(); + if (eventReceiver.getTasksOnDisplay() == 0) { + disposeInputChannel(displayId); + } + } + + /** + * Handle MotionEvents relevant to focused task's caption that don't directly touch it + * + * @param ev the {@link MotionEvent} received by {@link EventReceiver} + */ + private void handleReceivedMotionEvent(MotionEvent ev, InputMonitor inputMonitor) { + final DesktopModeWindowDecoration relevantDecor = getRelevantWindowDecor(ev); + if (DesktopModeStatus.isProto2Enabled()) { + if (relevantDecor == null + || relevantDecor.mTaskInfo.getWindowingMode() != WINDOWING_MODE_FREEFORM + || mTransitionDragActive) { + handleCaptionThroughStatusBar(ev, relevantDecor); + } + } + handleEventOutsideFocusedCaption(ev, relevantDecor); + // Prevent status bar from reacting to a caption drag. + if (DesktopModeStatus.isProto2Enabled()) { + if (mTransitionDragActive) { + inputMonitor.pilferPointers(); + } + } else if (DesktopModeStatus.isProto1Enabled()) { + if (mTransitionDragActive && !DesktopModeStatus.isActive(mContext)) { + inputMonitor.pilferPointers(); + } + } + } + + // If an UP/CANCEL action is received outside of caption bounds, turn off handle menu + private void handleEventOutsideFocusedCaption(MotionEvent ev, + DesktopModeWindowDecoration relevantDecor) { + final int action = ev.getActionMasked(); + if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { + if (relevantDecor == null) { + return; + } + + if (!mTransitionDragActive) { + relevantDecor.closeHandleMenuIfNeeded(ev); + } + } + } + + + /** + * Perform caption actions if not able to through normal means. + * Turn on desktop mode if handle is dragged below status bar. + */ + private void handleCaptionThroughStatusBar(MotionEvent ev, + DesktopModeWindowDecoration relevantDecor) { + switch (ev.getActionMasked()) { + case MotionEvent.ACTION_DOWN: { + mCaptionDragStartX = ev.getX(); + // Begin drag through status bar if applicable. + if (relevantDecor != null) { + mDragToDesktopAnimationStartBounds.set( + relevantDecor.mTaskInfo.configuration.windowConfiguration.getBounds()); + boolean dragFromStatusBarAllowed = false; + if (DesktopModeStatus.isProto2Enabled()) { + // In proto2 any full screen task can be dragged to freeform + dragFromStatusBarAllowed = relevantDecor.mTaskInfo.getWindowingMode() + == WINDOWING_MODE_FULLSCREEN; + } + + if (dragFromStatusBarAllowed && relevantDecor.checkTouchEventInHandle(ev)) { + mTransitionDragActive = true; + } + } + break; + } + case MotionEvent.ACTION_UP: { + if (relevantDecor == null) { + mDragToDesktopAnimationStarted = false; + mTransitionDragActive = false; + return; + } + if (mTransitionDragActive) { + mTransitionDragActive = false; + final int statusBarHeight = getStatusBarHeight( + relevantDecor.mTaskInfo.displayId); + if (ev.getY() > statusBarHeight) { + if (DesktopModeStatus.isProto2Enabled()) { + mPauseRelayoutForTask = relevantDecor.mTaskInfo.taskId; + centerAndMoveToDesktopWithAnimation(relevantDecor, ev); + } else if (DesktopModeStatus.isProto1Enabled()) { + mDesktopModeController.ifPresent(c -> c.setDesktopModeActive(true)); + } + mDragToDesktopAnimationStarted = false; + return; + } else if (mDragToDesktopAnimationStarted) { + mDesktopTasksController.ifPresent(c -> + c.moveToFullscreen(relevantDecor.mTaskInfo)); + mDragToDesktopAnimationStarted = false; + return; + } + } + relevantDecor.checkClickEvent(ev); + break; + } + + case MotionEvent.ACTION_MOVE: { + if (relevantDecor == null) { + return; + } + if (mTransitionDragActive) { + final int statusBarHeight = mDisplayController + .getDisplayLayout( + relevantDecor.mTaskInfo.displayId).stableInsets().top; + if (ev.getY() > statusBarHeight) { + if (!mDragToDesktopAnimationStarted) { + mDragToDesktopAnimationStarted = true; + mDesktopTasksController.ifPresent( + c -> c.moveToFreeform(relevantDecor.mTaskInfo, + mDragToDesktopAnimationStartBounds)); + startAnimation(relevantDecor); + } + } + if (mDragToDesktopAnimationStarted) { + Transaction t = mTransactionFactory.get(); + float width = (float) mDragToDesktopValueAnimator.getAnimatedValue() + * mDragToDesktopAnimationStartBounds.width(); + float x = ev.getX() - (width / 2); + t.setPosition(relevantDecor.mTaskSurface, x, ev.getY()); + t.apply(); + } + } + break; + } + + case MotionEvent.ACTION_CANCEL: { + mTransitionDragActive = false; + mDragToDesktopAnimationStarted = false; + } + } + } + + /** + * Gets bounds of a scaled window centered relative to the screen bounds + * @param scale the amount to scale to relative to the Screen Bounds + */ + private Rect calculateFreeformBounds(float scale) { + final Resources resources = mContext.getResources(); + final DisplayMetrics metrics = resources.getDisplayMetrics(); + final int screenWidth = metrics.widthPixels; + final int screenHeight = metrics.heightPixels; + + final float adjustmentPercentage = (1f - scale) / 2; + final Rect endBounds = new Rect((int) (screenWidth * adjustmentPercentage), + (int) (screenHeight * adjustmentPercentage), + (int) (screenWidth * (adjustmentPercentage + scale)), + (int) (screenHeight * (adjustmentPercentage + scale))); + return endBounds; + } + + /** + * Animates a window to the center, grows to freeform size, and transitions to Desktop Mode. + * @param relevantDecor the window decor of the task to be animated + * @param ev the motion event that triggers the animation + */ + private void centerAndMoveToDesktopWithAnimation(DesktopModeWindowDecoration relevantDecor, + MotionEvent ev) { + ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f); + animator.setDuration(FREEFORM_ANIMATION_DURATION); + final SurfaceControl sc = relevantDecor.mTaskSurface; + final Rect endBounds = calculateFreeformBounds(DRAG_FREEFORM_SCALE); + final Transaction t = mTransactionFactory.get(); + final float diffX = endBounds.centerX() - ev.getX(); + final float diffY = endBounds.top - ev.getY(); + final float startingX = ev.getX() - DRAG_FREEFORM_SCALE + * mDragToDesktopAnimationStartBounds.width() / 2; + + animator.addUpdateListener(animation -> { + final float animatorValue = (float) animation.getAnimatedValue(); + final float x = startingX + diffX * animatorValue; + final float y = ev.getY() + diffY * animatorValue; + t.setPosition(sc, x, y); + t.apply(); + }); + animator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mDesktopTasksController.ifPresent( + c -> c.moveToDesktopWithAnimation(relevantDecor.mTaskInfo, + calculateFreeformBounds(FINAL_FREEFORM_SCALE))); + } + }); + animator.start(); + } + + private void startAnimation(@NonNull DesktopModeWindowDecoration focusedDecor) { + mDragToDesktopValueAnimator = ValueAnimator.ofFloat(1f, DRAG_FREEFORM_SCALE); + mDragToDesktopValueAnimator.setDuration(FREEFORM_ANIMATION_DURATION); + final Transaction t = mTransactionFactory.get(); + mDragToDesktopValueAnimator.addUpdateListener(animation -> { + final float animatorValue = (float) animation.getAnimatedValue(); + SurfaceControl sc = focusedDecor.mTaskSurface; + t.setScale(sc, animatorValue, animatorValue); + t.apply(); + }); + + mDragToDesktopValueAnimator.start(); + } + + @Nullable + private DesktopModeWindowDecoration getRelevantWindowDecor(MotionEvent ev) { + if (mSplitScreenController.isPresent() + && mSplitScreenController.get().isSplitScreenVisible()) { + // We can't look at focused task here as only one task will have focus. + return getSplitScreenDecor(ev); + } else { + return getFocusedDecor(); + } + } + + @Nullable + private DesktopModeWindowDecoration getSplitScreenDecor(MotionEvent ev) { + ActivityManager.RunningTaskInfo topOrLeftTask = + mSplitScreenController.get().getTaskInfo(SPLIT_POSITION_TOP_OR_LEFT); + ActivityManager.RunningTaskInfo bottomOrRightTask = + mSplitScreenController.get().getTaskInfo(SPLIT_POSITION_BOTTOM_OR_RIGHT); + if (topOrLeftTask != null && topOrLeftTask.getConfiguration() + .windowConfiguration.getBounds().contains((int) ev.getX(), (int) ev.getY())) { + return mWindowDecorByTaskId.get(topOrLeftTask.taskId); + } else if (bottomOrRightTask != null && bottomOrRightTask.getConfiguration() + .windowConfiguration.getBounds().contains((int) ev.getX(), (int) ev.getY())) { + Rect bottomOrRightBounds = bottomOrRightTask.getConfiguration().windowConfiguration + .getBounds(); + ev.offsetLocation(-bottomOrRightBounds.left, -bottomOrRightBounds.top); + return mWindowDecorByTaskId.get(bottomOrRightTask.taskId); + } else { + return null; + } + + } + + @Nullable + private DesktopModeWindowDecoration getFocusedDecor() { + final int size = mWindowDecorByTaskId.size(); + DesktopModeWindowDecoration focusedDecor = null; + for (int i = 0; i < size; i++) { + final DesktopModeWindowDecoration decor = mWindowDecorByTaskId.valueAt(i); + if (decor != null && decor.isFocused()) { + focusedDecor = decor; + break; + } + } + return focusedDecor; + } + + private int getStatusBarHeight(int displayId) { + return mDisplayController.getDisplayLayout(displayId).stableInsets().top; + } + + private void createInputChannel(int displayId) { + final InputManager inputManager = mContext.getSystemService(InputManager.class); + final InputMonitor inputMonitor = + mInputMonitorFactory.create(inputManager, mContext); + final EventReceiver eventReceiver = new EventReceiver(inputMonitor, + inputMonitor.getInputChannel(), Looper.myLooper()); + mEventReceiversByDisplay.put(displayId, eventReceiver); + } + + private void disposeInputChannel(int displayId) { + final EventReceiver eventReceiver = mEventReceiversByDisplay.removeReturnOld(displayId); + if (eventReceiver != null) { + eventReceiver.dispose(); + } + } + + private boolean shouldShowWindowDecor(RunningTaskInfo taskInfo) { + if (taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM) return true; + return DesktopModeStatus.isProto2Enabled() + && taskInfo.getActivityType() == ACTIVITY_TYPE_STANDARD + && mDisplayController.getDisplayContext(taskInfo.displayId) + .getResources().getConfiguration().smallestScreenWidthDp >= 600; + } + + private void createWindowDecoration( + ActivityManager.RunningTaskInfo taskInfo, + SurfaceControl taskSurface, + SurfaceControl.Transaction startT, + SurfaceControl.Transaction finishT) { + final DesktopModeWindowDecoration oldDecoration = mWindowDecorByTaskId.get(taskInfo.taskId); + if (oldDecoration != null) { + // close the old decoration if it exists to avoid two window decorations being added + oldDecoration.close(); + } + final DesktopModeWindowDecoration windowDecoration = + mDesktopModeWindowDecorFactory.create( + mContext, + mDisplayController, + mTaskOrganizer, + taskInfo, + taskSurface, + mMainHandler, + mMainChoreographer, + mSyncQueue); + mWindowDecorByTaskId.put(taskInfo.taskId, windowDecoration); + + final TaskPositioner taskPositioner = + new TaskPositioner(mTaskOrganizer, windowDecoration, mDisplayController, + mDragStartListener); + final DesktopModeTouchEventListener touchEventListener = + new DesktopModeTouchEventListener(taskInfo, taskPositioner); + windowDecoration.setCaptionListeners(touchEventListener, touchEventListener); + windowDecoration.setDragPositioningCallback(taskPositioner); + windowDecoration.setDragDetector(touchEventListener.mDragDetector); + windowDecoration.relayout(taskInfo, startT, finishT); + incrementEventReceiverTasks(taskInfo.displayId); + } + + private class DragStartListenerImpl implements TaskPositioner.DragStartListener { + @Override + public void onDragStart(int taskId) { + mWindowDecorByTaskId.get(taskId).closeHandleMenu(); + } + } + + static class InputMonitorFactory { + InputMonitor create(InputManager inputManager, Context context) { + return inputManager.monitorGestureInput("caption-touch", context.getDisplayId()); + } + } +} + + diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java new file mode 100644 index 000000000000..e08d40d76c16 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java @@ -0,0 +1,586 @@ +/* + * 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_MULTI_WINDOW; +import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; + +import android.app.ActivityManager; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.res.ColorStateList; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.graphics.Point; +import android.graphics.PointF; +import android.graphics.drawable.Drawable; +import android.os.Handler; +import android.util.Log; +import android.view.Choreographer; +import android.view.MotionEvent; +import android.view.SurfaceControl; +import android.view.View; +import android.view.ViewConfiguration; +import android.widget.Button; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.TextView; +import android.window.WindowContainerTransaction; + +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.SyncTransactionQueue; +import com.android.wm.shell.desktopmode.DesktopModeStatus; +import com.android.wm.shell.desktopmode.DesktopTasksController; +import com.android.wm.shell.windowdecor.viewholder.DesktopModeAppControlsWindowDecorationViewHolder; +import com.android.wm.shell.windowdecor.viewholder.DesktopModeFocusedWindowDecorationViewHolder; +import com.android.wm.shell.windowdecor.viewholder.DesktopModeWindowDecorationViewHolder; + +/** + * Defines visuals and behaviors of a window decoration of a caption bar and shadows. It works with + * {@link DesktopModeWindowDecorViewModel}. + * + * The shadow's thickness is 20dp when the window is in focus and 5dp when the window isn't. + */ +public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLinearLayout> { + private static final String TAG = "DesktopModeWindowDecoration"; + + private final Handler mHandler; + private final Choreographer mChoreographer; + private final SyncTransactionQueue mSyncQueue; + + private DesktopModeWindowDecorationViewHolder mWindowDecorViewHolder; + private View.OnClickListener mOnCaptionButtonClickListener; + private View.OnTouchListener mOnCaptionTouchListener; + private DragPositioningCallback mDragPositioningCallback; + private DragResizeInputListener mDragResizeListener; + private DragDetector mDragDetector; + + private RelayoutParams mRelayoutParams = new RelayoutParams(); + private final WindowDecoration.RelayoutResult<WindowDecorLinearLayout> mResult = + new WindowDecoration.RelayoutResult<>(); + + private final PointF mHandleMenuAppInfoPillPosition = new PointF(); + private final PointF mHandleMenuWindowingPillPosition = new PointF(); + private final PointF mHandleMenuMoreActionsPillPosition = new PointF(); + + // Collection of additional windows that comprise the handle menu. + private AdditionalWindow mHandleMenuAppInfoPill; + private AdditionalWindow mHandleMenuWindowingPill; + private AdditionalWindow mHandleMenuMoreActionsPill; + + private Drawable mAppIcon; + private CharSequence mAppName; + + DesktopModeWindowDecoration( + 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; + + loadAppInfo(); + } + + @Override + protected Configuration getConfigurationWithOverrides( + ActivityManager.RunningTaskInfo taskInfo) { + Configuration configuration = taskInfo.getConfiguration(); + if (DesktopTasksController.isDesktopDensityOverrideSet()) { + // Density is overridden for desktop tasks. Keep system density for window decoration. + configuration.densityDpi = mContext.getResources().getConfiguration().densityDpi; + } + return configuration; + } + + void setCaptionListeners( + View.OnClickListener onCaptionButtonClickListener, + View.OnTouchListener onCaptionTouchListener) { + mOnCaptionButtonClickListener = onCaptionButtonClickListener; + mOnCaptionTouchListener = onCaptionTouchListener; + } + + void setDragPositioningCallback(DragPositioningCallback dragPositioningCallback) { + mDragPositioningCallback = dragPositioningCallback; + } + + void setDragDetector(DragDetector dragDetector) { + mDragDetector = dragDetector; + mDragDetector.setTouchSlop(ViewConfiguration.get(mContext).getScaledTouchSlop()); + } + + @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 shadowRadiusID = taskInfo.isFocused + ? R.dimen.freeform_decor_shadow_focused_thickness + : R.dimen.freeform_decor_shadow_unfocused_thickness; + final boolean isFreeform = + taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM; + final boolean isDragResizeable = isFreeform && taskInfo.isResizeable; + + final WindowDecorLinearLayout oldRootView = mResult.mRootView; + final SurfaceControl oldDecorationSurface = mDecorationContainerSurface; + final WindowContainerTransaction wct = new WindowContainerTransaction(); + + final int outsetLeftId = R.dimen.freeform_resize_handle; + final int outsetTopId = R.dimen.freeform_resize_handle; + final int outsetRightId = R.dimen.freeform_resize_handle; + final int outsetBottomId = R.dimen.freeform_resize_handle; + + final int windowDecorLayoutId = getDesktopModeWindowDecorLayoutId( + taskInfo.getWindowingMode()); + mRelayoutParams.reset(); + mRelayoutParams.mRunningTaskInfo = taskInfo; + mRelayoutParams.mLayoutResId = windowDecorLayoutId; + mRelayoutParams.mCaptionHeightId = R.dimen.freeform_decor_caption_height; + mRelayoutParams.mShadowRadiusId = shadowRadiusID; + if (isDragResizeable) { + mRelayoutParams.setOutsets(outsetLeftId, outsetTopId, outsetRightId, outsetBottomId); + } + + relayout(mRelayoutParams, startT, finishT, wct, oldRootView, mResult); + // After this line, mTaskInfo is up-to-date and should be used instead of taskInfo + + mTaskOrganizer.applyTransaction(wct); + + 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) { + if (mRelayoutParams.mLayoutResId == R.layout.desktop_mode_focused_window_decor) { + mWindowDecorViewHolder = new DesktopModeFocusedWindowDecorationViewHolder( + mResult.mRootView, + mOnCaptionTouchListener, + mOnCaptionButtonClickListener + ); + } else if (mRelayoutParams.mLayoutResId + == R.layout.desktop_mode_app_controls_window_decor) { + mWindowDecorViewHolder = new DesktopModeAppControlsWindowDecorationViewHolder( + mResult.mRootView, + mOnCaptionTouchListener, + mOnCaptionButtonClickListener, + mAppName, + mAppIcon + ); + } else { + throw new IllegalArgumentException("Unexpected layout resource id"); + } + } + mWindowDecorViewHolder.bindData(mTaskInfo); + + if (!mTaskInfo.isFocused) { + closeHandleMenu(); + } + + if (!isDragResizeable) { + closeDragResizeListener(); + return; + } + + if (oldDecorationSurface != mDecorationContainerSurface || mDragResizeListener == null) { + closeDragResizeListener(); + mDragResizeListener = new DragResizeInputListener( + mContext, + mHandler, + mChoreographer, + mDisplay.getDisplayId(), + mDecorationContainerSurface, + mDragPositioningCallback); + } + + final int touchSlop = ViewConfiguration.get(mResult.mRootView.getContext()) + .getScaledTouchSlop(); + mDragDetector.setTouchSlop(touchSlop); + + final int resize_handle = mResult.mRootView.getResources() + .getDimensionPixelSize(R.dimen.freeform_resize_handle); + final int resize_corner = mResult.mRootView.getResources() + .getDimensionPixelSize(R.dimen.freeform_resize_corner); + mDragResizeListener.setGeometry( + mResult.mWidth, mResult.mHeight, resize_handle, resize_corner, touchSlop); + } + + boolean isHandleMenuActive() { + return mHandleMenuAppInfoPill != null; + } + + private void loadAppInfo() { + String packageName = mTaskInfo.realActivity.getPackageName(); + PackageManager pm = mContext.getApplicationContext().getPackageManager(); + try { + IconProvider provider = new IconProvider(mContext); + mAppIcon = provider.getIcon(pm.getActivityInfo(mTaskInfo.baseActivity, + PackageManager.ComponentInfoFlags.of(0))); + ApplicationInfo applicationInfo = pm.getApplicationInfo(packageName, + PackageManager.ApplicationInfoFlags.of(0)); + mAppName = pm.getApplicationLabel(applicationInfo); + } catch (PackageManager.NameNotFoundException e) { + Log.w(TAG, "Package not found: " + packageName, e); + } + } + + private void closeDragResizeListener() { + if (mDragResizeListener == null) { + return; + } + mDragResizeListener.close(); + mDragResizeListener = null; + } + + /** + * Create and display handle menu window + */ + void createHandleMenu() { + final SurfaceControl.Transaction t = new SurfaceControl.Transaction(); + final Resources resources = mDecorWindowContext.getResources(); + final int captionWidth = mTaskInfo.getConfiguration() + .windowConfiguration.getBounds().width(); + final int menuWidth = loadDimensionPixelSize(resources, + R.dimen.desktop_mode_handle_menu_width); + final int shadowRadius = loadDimensionPixelSize(resources, + R.dimen.desktop_mode_handle_menu_shadow_radius); + final int cornerRadius = loadDimensionPixelSize(resources, + R.dimen.desktop_mode_handle_menu_corner_radius); + final int marginMenuTop = loadDimensionPixelSize(resources, + R.dimen.desktop_mode_handle_menu_margin_top); + final int marginMenuStart = loadDimensionPixelSize(resources, + R.dimen.desktop_mode_handle_menu_margin_start); + final int marginMenuSpacing = loadDimensionPixelSize(resources, + R.dimen.desktop_mode_handle_menu_pill_spacing_margin); + final int appInfoPillHeight = loadDimensionPixelSize(resources, + R.dimen.desktop_mode_handle_menu_app_info_pill_height); + final int windowingPillHeight = loadDimensionPixelSize(resources, + R.dimen.desktop_mode_handle_menu_windowing_pill_height); + final int moreActionsPillHeight = loadDimensionPixelSize(resources, + R.dimen.desktop_mode_handle_menu_more_actions_pill_height); + + final int menuX, menuY; + if (mRelayoutParams.mLayoutResId + == R.layout.desktop_mode_app_controls_window_decor) { + // Align the handle menu to the left of the caption. + menuX = mRelayoutParams.mCaptionX - mResult.mDecorContainerOffsetX + marginMenuStart; + menuY = mRelayoutParams.mCaptionY - mResult.mDecorContainerOffsetY + marginMenuTop; + } else { + // Position the handle menu at the center of the caption. + menuX = mRelayoutParams.mCaptionX + (captionWidth / 2) - (menuWidth / 2) + - mResult.mDecorContainerOffsetX; + menuY = mRelayoutParams.mCaptionY - mResult.mDecorContainerOffsetY + marginMenuStart; + } + + final int appInfoPillY = menuY; + createAppInfoPill(t, menuX, appInfoPillY, menuWidth, appInfoPillHeight, shadowRadius, + cornerRadius); + + // Only show windowing buttons in proto2. Proto1 uses a system-level mode only. + final boolean shouldShowWindowingPill = DesktopModeStatus.isProto2Enabled(); + final int windowingPillY = appInfoPillY + appInfoPillHeight + marginMenuSpacing; + if (shouldShowWindowingPill) { + createWindowingPill(t, menuX, windowingPillY, menuWidth, windowingPillHeight, + shadowRadius, + cornerRadius); + } + + final int moreActionsPillY; + if (shouldShowWindowingPill) { + // Take into account the windowing pill height and margins. + moreActionsPillY = windowingPillY + windowingPillHeight + marginMenuSpacing; + } else { + // Just start after the end of the app info pill + margins. + moreActionsPillY = appInfoPillY + appInfoPillHeight + marginMenuSpacing; + } + createMoreActionsPill(t, menuX, moreActionsPillY, menuWidth, moreActionsPillHeight, + shadowRadius, cornerRadius); + + mSyncQueue.runInSync(transaction -> { + transaction.merge(t); + t.close(); + }); + setupHandleMenu(shouldShowWindowingPill); + } + + private void createAppInfoPill(SurfaceControl.Transaction t, int x, int y, int width, + int height, int shadowRadius, int cornerRadius) { + mHandleMenuAppInfoPillPosition.set(x, y); + mHandleMenuAppInfoPill = addWindow( + R.layout.desktop_mode_window_decor_handle_menu_app_info_pill, + "Menu's app info pill", + t, x, y, width, height, shadowRadius, cornerRadius); + } + + private void createWindowingPill(SurfaceControl.Transaction t, int x, int y, int width, + int height, int shadowRadius, int cornerRadius) { + mHandleMenuWindowingPillPosition.set(x, y); + mHandleMenuWindowingPill = addWindow( + R.layout.desktop_mode_window_decor_handle_menu_windowing_pill, + "Menu's windowing pill", + t, x, y, width, height, shadowRadius, cornerRadius); + } + + private void createMoreActionsPill(SurfaceControl.Transaction t, int x, int y, int width, + int height, int shadowRadius, int cornerRadius) { + mHandleMenuMoreActionsPillPosition.set(x, y); + mHandleMenuMoreActionsPill = addWindow( + R.layout.desktop_mode_window_decor_handle_menu_more_actions_pill, + "Menu's more actions pill", + t, x, y, width, height, shadowRadius, cornerRadius); + } + + private void setupHandleMenu(boolean windowingPillShown) { + // App Info pill setup. + final View appInfoPillView = mHandleMenuAppInfoPill.mWindowViewHost.getView(); + final ImageButton collapseBtn = appInfoPillView.findViewById(R.id.collapse_menu_button); + final ImageView appIcon = appInfoPillView.findViewById(R.id.application_icon); + final TextView appName = appInfoPillView.findViewById(R.id.application_name); + collapseBtn.setOnClickListener(mOnCaptionButtonClickListener); + appInfoPillView.setOnTouchListener(mOnCaptionTouchListener); + appIcon.setImageDrawable(mAppIcon); + appName.setText(mAppName); + + // Windowing pill setup. + if (windowingPillShown) { + final View windowingPillView = mHandleMenuWindowingPill.mWindowViewHost.getView(); + final ImageButton fullscreenBtn = windowingPillView.findViewById( + R.id.fullscreen_button); + final ImageButton splitscreenBtn = windowingPillView.findViewById( + R.id.split_screen_button); + final ImageButton floatingBtn = windowingPillView.findViewById(R.id.floating_button); + final ImageButton desktopBtn = windowingPillView.findViewById(R.id.desktop_button); + fullscreenBtn.setOnClickListener(mOnCaptionButtonClickListener); + splitscreenBtn.setOnClickListener(mOnCaptionButtonClickListener); + floatingBtn.setOnClickListener(mOnCaptionButtonClickListener); + desktopBtn.setOnClickListener(mOnCaptionButtonClickListener); + // The button corresponding to the windowing mode that the task is currently in uses a + // different color than the others. + final ColorStateList activeColorStateList = ColorStateList.valueOf( + mContext.getColor(R.color.desktop_mode_caption_menu_buttons_color_active)); + final ColorStateList inActiveColorStateList = ColorStateList.valueOf( + mContext.getColor(R.color.desktop_mode_caption_menu_buttons_color_inactive)); + fullscreenBtn.setImageTintList( + mTaskInfo.getWindowingMode() == WINDOWING_MODE_FULLSCREEN + ? activeColorStateList : inActiveColorStateList); + splitscreenBtn.setImageTintList( + mTaskInfo.getWindowingMode() == WINDOWING_MODE_MULTI_WINDOW + ? activeColorStateList : inActiveColorStateList); + floatingBtn.setImageTintList(mTaskInfo.getWindowingMode() == WINDOWING_MODE_PINNED + ? activeColorStateList : inActiveColorStateList); + desktopBtn.setImageTintList(mTaskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM + ? activeColorStateList : inActiveColorStateList); + } + + // More Actions pill setup. + final View moreActionsPillView = mHandleMenuMoreActionsPill.mWindowViewHost.getView(); + final Button closeBtn = moreActionsPillView.findViewById(R.id.close_button); + closeBtn.setOnClickListener(mOnCaptionButtonClickListener); + } + + /** + * Close the handle menu window + */ + void closeHandleMenu() { + if (!isHandleMenuActive()) return; + mHandleMenuAppInfoPill.releaseView(); + mHandleMenuAppInfoPill = null; + if (mHandleMenuWindowingPill != null) { + mHandleMenuWindowingPill.releaseView(); + mHandleMenuWindowingPill = null; + } + mHandleMenuMoreActionsPill.releaseView(); + mHandleMenuMoreActionsPill = null; + } + + @Override + void releaseViews() { + closeHandleMenu(); + super.releaseViews(); + } + + /** + * Close an open handle menu if input is outside of menu coordinates + * + * @param ev the tapped point to compare against + */ + void closeHandleMenuIfNeeded(MotionEvent ev) { + if (!isHandleMenuActive()) return; + + // When this is called before the layout is fully inflated, width will be 0. + // Menu is not visible in this scenario, so skip the check if that is the case. + if (mHandleMenuAppInfoPill.mWindowViewHost.getView().getWidth() == 0) return; + + PointF inputPoint = offsetCaptionLocation(ev); + final boolean pointInAppInfoPill = pointInView( + mHandleMenuAppInfoPill.mWindowViewHost.getView(), + inputPoint.x - mHandleMenuAppInfoPillPosition.x - mResult.mDecorContainerOffsetX, + inputPoint.y - mHandleMenuAppInfoPillPosition.y + - mResult.mDecorContainerOffsetY); + boolean pointInWindowingPill = false; + if (mHandleMenuWindowingPill != null) { + pointInWindowingPill = pointInView(mHandleMenuWindowingPill.mWindowViewHost.getView(), + inputPoint.x - mHandleMenuWindowingPillPosition.x + - mResult.mDecorContainerOffsetX, + inputPoint.y - mHandleMenuWindowingPillPosition.y + - mResult.mDecorContainerOffsetY); + } + final boolean pointInMoreActionsPill = pointInView( + mHandleMenuMoreActionsPill.mWindowViewHost.getView(), + inputPoint.x - mHandleMenuMoreActionsPillPosition.x + - mResult.mDecorContainerOffsetX, + inputPoint.y - mHandleMenuMoreActionsPillPosition.y + - mResult.mDecorContainerOffsetY); + if (!pointInAppInfoPill && !pointInWindowingPill && !pointInMoreActionsPill) { + closeHandleMenu(); + } + } + + boolean isFocused() { + return mTaskInfo.isFocused; + } + + /** + * Offset the coordinates of a {@link MotionEvent} to be in the same coordinate space as caption + * + * @param ev the {@link MotionEvent} to offset + * @return the point of the input in local space + */ + private PointF offsetCaptionLocation(MotionEvent ev) { + final PointF result = new PointF(ev.getX(), ev.getY()); + final Point positionInParent = mTaskOrganizer.getRunningTaskInfo(mTaskInfo.taskId) + .positionInParent; + result.offset(-mRelayoutParams.mCaptionX, -mRelayoutParams.mCaptionY); + result.offset(-positionInParent.x, -positionInParent.y); + return result; + } + + /** + * Determine if a passed MotionEvent is in a view in caption + * + * @param ev the {@link MotionEvent} to check + * @param layoutId the id of the view + * @return {@code true} if event is inside the specified view, {@code false} if not + */ + private boolean checkEventInCaptionView(MotionEvent ev, int layoutId) { + if (mResult.mRootView == null) return false; + final PointF inputPoint = offsetCaptionLocation(ev); + final View view = mResult.mRootView.findViewById(layoutId); + return view != null && pointInView(view, inputPoint.x, inputPoint.y); + } + + boolean checkTouchEventInHandle(MotionEvent ev) { + if (isHandleMenuActive()) return false; + return checkEventInCaptionView(ev, R.id.caption_handle); + } + + /** + * Check a passed MotionEvent if a click has occurred on any button on this caption + * Note this should only be called when a regular onClick is not possible + * (i.e. the button was clicked through status bar layer) + * + * @param ev the MotionEvent to compare + */ + void checkClickEvent(MotionEvent ev) { + if (mResult.mRootView == null) return; + if (!isHandleMenuActive()) { + final View caption = mResult.mRootView.findViewById(R.id.desktop_mode_caption); + final View handle = caption.findViewById(R.id.caption_handle); + clickIfPointInView(new PointF(ev.getX(), ev.getY()), handle); + } else { + final View appInfoPill = mHandleMenuAppInfoPill.mWindowViewHost.getView(); + final ImageButton collapse = appInfoPill.findViewById(R.id.collapse_menu_button); + // Translate the input point from display coordinates to the same space as the collapse + // button, meaning its parent (app info pill view). + final PointF inputPoint = new PointF(ev.getX() - mHandleMenuAppInfoPillPosition.x, + ev.getY() - mHandleMenuAppInfoPillPosition.y); + clickIfPointInView(inputPoint, collapse); + } + } + + private boolean clickIfPointInView(PointF inputPoint, View v) { + if (pointInView(v, inputPoint.x, inputPoint.y)) { + mOnCaptionButtonClickListener.onClick(v); + return true; + } + return false; + } + + private boolean pointInView(View v, float x, float y) { + return v != null && v.getLeft() <= x && v.getRight() >= x + && v.getTop() <= y && v.getBottom() >= y; + } + + @Override + public void close() { + closeDragResizeListener(); + closeHandleMenu(); + super.close(); + } + + private int getDesktopModeWindowDecorLayoutId(int windowingMode) { + if (DesktopModeStatus.isProto1Enabled()) { + return R.layout.desktop_mode_app_controls_window_decor; + } + return windowingMode == WINDOWING_MODE_FREEFORM + ? R.layout.desktop_mode_app_controls_window_decor + : R.layout.desktop_mode_focused_window_decor; + } + + static class Factory { + + DesktopModeWindowDecoration create( + Context context, + DisplayController displayController, + ShellTaskOrganizer taskOrganizer, + ActivityManager.RunningTaskInfo taskInfo, + SurfaceControl taskSurface, + Handler handler, + Choreographer choreographer, + SyncTransactionQueue syncQueue) { + return new DesktopModeWindowDecoration( + context, + displayController, + taskOrganizer, + taskInfo, + taskSurface, + handler, + choreographer, + syncQueue); + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragDetector.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragDetector.java new file mode 100644 index 000000000000..65b5a7a17afe --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragDetector.java @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.windowdecor; + +import static android.view.InputDevice.SOURCE_TOUCHSCREEN; +import static android.view.MotionEvent.ACTION_CANCEL; +import static android.view.MotionEvent.ACTION_DOWN; +import static android.view.MotionEvent.ACTION_MOVE; +import static android.view.MotionEvent.ACTION_UP; + +import android.graphics.PointF; +import android.view.MotionEvent; + +/** + * A detector for touch inputs that differentiates between drag and click inputs. It receives a flow + * of {@link MotionEvent} and generates a new flow of motion events with slop in consideration to + * the event handler. In particular, it always passes down, up and cancel events. It'll pass move + * events only when there is at least one move event that's beyond the slop threshold. For the + * purpose of convenience it also passes all events of other actions. + * + * All touch events must be passed through this class to track a drag event. + */ +class DragDetector { + private final MotionEventHandler mEventHandler; + + private final PointF mInputDownPoint = new PointF(); + private int mTouchSlop; + private boolean mIsDragEvent; + private int mDragPointerId; + + private boolean mResultOfDownAction; + + DragDetector(MotionEventHandler eventHandler) { + resetState(); + mEventHandler = eventHandler; + } + + /** + * The receiver of the {@link MotionEvent} flow. + * + * @return the result returned by {@link #mEventHandler}, or the result when + * {@link #mEventHandler} handles the previous down event if the event shouldn't be passed + */ + boolean onMotionEvent(MotionEvent ev) { + final boolean isTouchScreen = + (ev.getSource() & SOURCE_TOUCHSCREEN) == SOURCE_TOUCHSCREEN; + if (!isTouchScreen) { + // Only touches generate noisy moves, so mouse/trackpad events don't need to filtered + // to take the slop threshold into consideration. + return mEventHandler.handleMotionEvent(ev); + } + switch (ev.getActionMasked()) { + case ACTION_DOWN: { + mDragPointerId = ev.getPointerId(0); + float rawX = ev.getRawX(0); + float rawY = ev.getRawY(0); + mInputDownPoint.set(rawX, rawY); + mResultOfDownAction = mEventHandler.handleMotionEvent(ev); + return mResultOfDownAction; + } + case ACTION_MOVE: { + if (!mIsDragEvent) { + int dragPointerIndex = ev.findPointerIndex(mDragPointerId); + float dx = ev.getRawX(dragPointerIndex) - mInputDownPoint.x; + float dy = ev.getRawY(dragPointerIndex) - mInputDownPoint.y; + // Touches generate noisy moves, so only once the move is past the touch + // slop threshold should it be considered a drag. + mIsDragEvent = Math.hypot(dx, dy) > mTouchSlop; + } + // The event handler should only be notified about 'move' events if a drag has been + // detected. + if (mIsDragEvent) { + return mEventHandler.handleMotionEvent(ev); + } else { + return mResultOfDownAction; + } + } + case ACTION_UP: + case ACTION_CANCEL: { + resetState(); + return mEventHandler.handleMotionEvent(ev); + } + default: + return mEventHandler.handleMotionEvent(ev); + } + } + + void setTouchSlop(int touchSlop) { + mTouchSlop = touchSlop; + } + + private void resetState() { + mIsDragEvent = false; + mInputDownPoint.set(0, 0); + mDragPointerId = -1; + mResultOfDownAction = false; + } + + interface MotionEventHandler { + boolean handleMotionEvent(MotionEvent ev); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallback.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallback.java new file mode 100644 index 000000000000..0191c609a8b2 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallback.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 DragPositioningCallback { + /** + * Called when a drag-resize or drag-move 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 starts + * @param y y coordinate in window decoration coordinate system where the drag starts + */ + void onDragPositioningStart(@TaskPositioner.CtrlType int ctrlType, float x, float y); + + /** + * Called when the pointer moves during a drag-resize or drag-move. + * @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 onDragPositioningMove(float x, float y); + + /** + * Called when a drag-resize or drag-move 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 onDragPositioningEnd(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..8cb575cc96e3 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java @@ -0,0 +1,420 @@ +/* + * 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.InputDevice.SOURCE_TOUCHSCREEN; +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.ViewConfiguration; +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 + * and corners. Converts them to drag resize requests. + * Task edges are for resizing with a mouse. + * Task corners are for resizing with touch input. + */ +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 DragPositioningCallback mCallback; + + private int mWidth; + private int mHeight; + private int mResizeHandleThickness; + private int mCornerSize; + + private Rect mLeftTopCornerBounds; + private Rect mRightTopCornerBounds; + private Rect mLeftBottomCornerBounds; + private Rect mRightBottomCornerBounds; + + private int mDragPointerId = -1; + private DragDetector mDragDetector; + + DragResizeInputListener( + Context context, + Handler handler, + Choreographer choreographer, + int displayId, + SurfaceControl decorationSurface, + DragPositioningCallback 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, + 0 /* inputFeatures */, + TYPE_APPLICATION, + null /* windowToken */, + mFocusGrantToken, + TAG + " of " + decorationSurface.toString(), + mInputChannel); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + + mInputEventReceiver = new TaskResizeInputEventReceiver( + mInputChannel, mHandler, mChoreographer); + mCallback = callback; + mDragDetector = new DragDetector(mInputEventReceiver); + mDragDetector.setTouchSlop(ViewConfiguration.get(context).getScaledTouchSlop()); + } + + /** + * 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. + * @param cornerSize The size of the resize handle centered in each corner. + * @param touchSlop The distance in pixels user has to drag with touch for it to register as + * a resize action. + */ + void setGeometry(int width, int height, int resizeHandleThickness, int cornerSize, + int touchSlop) { + if (mWidth == width && mHeight == height + && mResizeHandleThickness == resizeHandleThickness + && mCornerSize == cornerSize) { + return; + } + + mWidth = width; + mHeight = height; + mResizeHandleThickness = resizeHandleThickness; + mCornerSize = cornerSize; + mDragDetector.setTouchSlop(touchSlop); + + 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); + + // Set up touch areas in each corner. + int cornerRadius = mCornerSize / 2; + mLeftTopCornerBounds = new Rect( + mResizeHandleThickness - cornerRadius, + mResizeHandleThickness - cornerRadius, + mResizeHandleThickness + cornerRadius, + mResizeHandleThickness + cornerRadius + ); + touchRegion.union(mLeftTopCornerBounds); + + mRightTopCornerBounds = new Rect( + mWidth - mResizeHandleThickness - cornerRadius, + mResizeHandleThickness - cornerRadius, + mWidth - mResizeHandleThickness + cornerRadius, + mResizeHandleThickness + cornerRadius + ); + touchRegion.union(mRightTopCornerBounds); + + mLeftBottomCornerBounds = new Rect( + mResizeHandleThickness - cornerRadius, + mHeight - mResizeHandleThickness - cornerRadius, + mResizeHandleThickness + cornerRadius, + mHeight - mResizeHandleThickness + cornerRadius + ); + touchRegion.union(mLeftBottomCornerBounds); + + mRightBottomCornerBounds = new Rect( + mWidth - mResizeHandleThickness - cornerRadius, + mHeight - mResizeHandleThickness - cornerRadius, + mWidth - mResizeHandleThickness + cornerRadius, + mHeight - mResizeHandleThickness + cornerRadius + ); + touchRegion.union(mRightBottomCornerBounds); + + try { + mWindowSession.updateInputChannel( + mInputChannel.getToken(), + mDisplayId, + mDecorationSurface, + FLAG_NOT_FOCUSABLE, + PRIVATE_FLAG_TRUSTED_OVERLAY, + 0 /* inputFeatures */, + touchRegion); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + } + + @Override + public void close() { + mInputEventReceiver.dispose(); + mInputChannel.dispose(); + try { + mWindowSession.remove(mFakeWindow); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + } + + private class TaskResizeInputEventReceiver extends InputEventReceiver + implements DragDetector.MotionEventHandler { + private final Choreographer mChoreographer; + private final Runnable mConsumeBatchEventRunnable; + private boolean mConsumeBatchEventScheduled; + private boolean mShouldHandleEvents; + + 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; + } + return mDragDetector.onMotionEvent((MotionEvent) inputEvent); + } + + @Override + public boolean handleMotionEvent(MotionEvent e) { + boolean result = false; + // Check if this is a touch event vs mouse event. + // Touch events are tracked in four corners. Other events are tracked in resize edges. + boolean isTouch = (e.getSource() & SOURCE_TOUCHSCREEN) == SOURCE_TOUCHSCREEN; + switch (e.getActionMasked()) { + case MotionEvent.ACTION_DOWN: { + float x = e.getX(0); + float y = e.getY(0); + if (isTouch) { + mShouldHandleEvents = isInCornerBounds(x, y); + } else { + mShouldHandleEvents = isInResizeHandleBounds(x, y); + } + if (mShouldHandleEvents) { + mDragPointerId = e.getPointerId(0); + float rawX = e.getRawX(0); + float rawY = e.getRawY(0); + int ctrlType = calculateCtrlType(isTouch, x, y); + mCallback.onDragPositioningStart(ctrlType, rawX, rawY); + result = true; + } + break; + } + case MotionEvent.ACTION_MOVE: { + if (!mShouldHandleEvents) { + break; + } + int dragPointerIndex = e.findPointerIndex(mDragPointerId); + float rawX = e.getRawX(dragPointerIndex); + float rawY = e.getRawY(dragPointerIndex); + mCallback.onDragPositioningMove(rawX, rawY); + result = true; + break; + } + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: { + if (mShouldHandleEvents) { + int dragPointerIndex = e.findPointerIndex(mDragPointerId); + mCallback.onDragPositioningEnd( + e.getRawX(dragPointerIndex), e.getRawY(dragPointerIndex)); + } + mShouldHandleEvents = false; + mDragPointerId = -1; + result = true; + break; + } + case MotionEvent.ACTION_HOVER_ENTER: + case MotionEvent.ACTION_HOVER_MOVE: { + updateCursorType(e.getXCursorPosition(), e.getYCursorPosition()); + result = true; + break; + } + case MotionEvent.ACTION_HOVER_EXIT: + mInputManager.setPointerIconType(PointerIcon.TYPE_DEFAULT); + result = true; + break; + } + return result; + } + + private boolean isInCornerBounds(float xf, float yf) { + return calculateCornersCtrlType(xf, yf) != 0; + } + + private boolean isInResizeHandleBounds(float x, float y) { + return calculateResizeHandlesCtrlType(x, y) != 0; + } + + @TaskPositioner.CtrlType + private int calculateCtrlType(boolean isTouch, float x, float y) { + if (isTouch) { + return calculateCornersCtrlType(x, y); + } + return calculateResizeHandlesCtrlType(x, y); + } + + @TaskPositioner.CtrlType + private int calculateResizeHandlesCtrlType(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; + } + + @TaskPositioner.CtrlType + private int calculateCornersCtrlType(float x, float y) { + int xi = (int) x; + int yi = (int) y; + if (mLeftTopCornerBounds.contains(xi, yi)) { + return TaskPositioner.CTRL_TYPE_LEFT | TaskPositioner.CTRL_TYPE_TOP; + } + if (mLeftBottomCornerBounds.contains(xi, yi)) { + return TaskPositioner.CTRL_TYPE_LEFT | TaskPositioner.CTRL_TYPE_BOTTOM; + } + if (mRightTopCornerBounds.contains(xi, yi)) { + return TaskPositioner.CTRL_TYPE_RIGHT | TaskPositioner.CTRL_TYPE_TOP; + } + if (mRightBottomCornerBounds.contains(xi, yi)) { + return TaskPositioner.CTRL_TYPE_RIGHT | TaskPositioner.CTRL_TYPE_BOTTOM; + } + return 0; + } + + private void updateCursorType(float x, float y) { + @TaskPositioner.CtrlType int ctrlType = calculateResizeHandlesCtrlType(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); + } + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/OWNERS b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/OWNERS new file mode 100644 index 000000000000..4417209b85ed --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/OWNERS @@ -0,0 +1 @@ +jorgegil@google.com diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/CommonAssertions.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskFocusStateConsumer.java index f9b08000290f..1c61802bbd5c 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/CommonAssertions.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskFocusStateConsumer.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,7 +14,8 @@ * limitations under the License. */ -@file:JvmName("CommonAssertions") -package com.android.wm.shell.flicker.pip +package com.android.wm.shell.windowdecor; -internal const val PIP_WINDOW_COMPONENT = "PipMenuActivity" +interface TaskFocusStateConsumer { + void setTaskFocusState(boolean focused); +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskOperations.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskOperations.java new file mode 100644 index 000000000000..d0fcd8651481 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskOperations.java @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.windowdecor; + +import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; +import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; + +import android.app.ActivityManager.RunningTaskInfo; +import android.content.Context; +import android.hardware.input.InputManager; +import android.os.SystemClock; +import android.util.Log; +import android.view.InputDevice; +import android.view.KeyCharacterMap; +import android.view.KeyEvent; +import android.window.WindowContainerToken; +import android.window.WindowContainerTransaction; + +import com.android.wm.shell.common.SyncTransactionQueue; +import com.android.wm.shell.freeform.FreeformTaskTransitionStarter; +import com.android.wm.shell.transition.Transitions; + +/** + * Utility class to handle task operations performed on a window decoration. + */ +class TaskOperations { + private static final String TAG = "TaskOperations"; + + private final FreeformTaskTransitionStarter mTransitionStarter; + private final Context mContext; + private final SyncTransactionQueue mSyncQueue; + + TaskOperations(FreeformTaskTransitionStarter transitionStarter, Context context, + SyncTransactionQueue syncQueue) { + mTransitionStarter = transitionStarter; + mContext = context; + mSyncQueue = syncQueue; + } + + 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 (!mContext.getSystemService(InputManager.class) + .injectInputEvent(ev, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC)) { + Log.e(TAG, "Inject input event fail"); + } + } + + void closeTask(WindowContainerToken taskToken) { + WindowContainerTransaction wct = new WindowContainerTransaction(); + wct.removeTask(taskToken); + if (Transitions.ENABLE_SHELL_TRANSITIONS) { + mTransitionStarter.startRemoveTransition(wct); + } else { + mSyncQueue.queue(wct); + } + } + + void minimizeTask(WindowContainerToken taskToken) { + WindowContainerTransaction wct = new WindowContainerTransaction(); + wct.reorder(taskToken, false); + if (Transitions.ENABLE_SHELL_TRANSITIONS) { + mTransitionStarter.startMinimizedModeTransition(wct); + } else { + mSyncQueue.queue(wct); + } + } + + void maximizeTask(RunningTaskInfo taskInfo) { + WindowContainerTransaction wct = new WindowContainerTransaction(); + int targetWindowingMode = taskInfo.getWindowingMode() != WINDOWING_MODE_FULLSCREEN + ? WINDOWING_MODE_FULLSCREEN : WINDOWING_MODE_FREEFORM; + int displayWindowingMode = + taskInfo.configuration.windowConfiguration.getDisplayWindowingMode(); + wct.setWindowingMode(taskInfo.token, + targetWindowingMode == displayWindowingMode + ? WINDOWING_MODE_UNDEFINED : targetWindowingMode); + if (targetWindowingMode == WINDOWING_MODE_FULLSCREEN) { + wct.setBounds(taskInfo.token, null); + } + if (Transitions.ENABLE_SHELL_TRANSITIONS) { + mTransitionStarter.startWindowingModeTransition(targetWindowingMode, wct); + } else { + mSyncQueue.queue(wct); + } + } +} 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..0bce3acecb3c --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskPositioner.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.windowdecor; + +import android.annotation.IntDef; +import android.graphics.PointF; +import android.graphics.Rect; +import android.util.DisplayMetrics; +import android.window.WindowContainerTransaction; + +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.common.DisplayController; + +class TaskPositioner implements DragPositioningCallback { + + @IntDef({CTRL_TYPE_UNDEFINED, CTRL_TYPE_LEFT, CTRL_TYPE_RIGHT, CTRL_TYPE_TOP, CTRL_TYPE_BOTTOM}) + @interface CtrlType {} + + static final int CTRL_TYPE_UNDEFINED = 0; + 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 DisplayController mDisplayController; + private final WindowDecoration mWindowDecoration; + + private final Rect mTempBounds = new Rect(); + private final Rect mTaskBoundsAtDragStart = new Rect(); + private final PointF mRepositionStartPoint = new PointF(); + private final Rect mRepositionTaskBounds = new Rect(); + private boolean mHasMoved = false; + + private int mCtrlType; + private DragStartListener mDragStartListener; + + TaskPositioner(ShellTaskOrganizer taskOrganizer, WindowDecoration windowDecoration, + DisplayController displayController) { + this(taskOrganizer, windowDecoration, displayController, dragStartListener -> {}); + } + + TaskPositioner(ShellTaskOrganizer taskOrganizer, WindowDecoration windowDecoration, + DisplayController displayController, DragStartListener dragStartListener) { + mTaskOrganizer = taskOrganizer; + mWindowDecoration = windowDecoration; + mDisplayController = displayController; + mDragStartListener = dragStartListener; + } + + @Override + public void onDragPositioningStart(int ctrlType, float x, float y) { + mHasMoved = false; + + mDragStartListener.onDragStart(mWindowDecoration.mTaskInfo.taskId); + mCtrlType = ctrlType; + + mTaskBoundsAtDragStart.set( + mWindowDecoration.mTaskInfo.configuration.windowConfiguration.getBounds()); + mRepositionStartPoint.set(x, y); + } + + @Override + public void onDragPositioningMove(float x, float y) { + final WindowContainerTransaction wct = new WindowContainerTransaction(); + if (changeBounds(wct, x, y)) { + // The task is being resized, send the |dragResizing| hint to core with the first + // bounds-change wct. + if (!mHasMoved && mCtrlType != CTRL_TYPE_UNDEFINED) { + // This is the first bounds change since drag resize operation started. + wct.setDragResizing(mWindowDecoration.mTaskInfo.token, true /* dragResizing */); + } + mTaskOrganizer.applyTransaction(wct); + mHasMoved = true; + } + } + + @Override + public void onDragPositioningEnd(float x, float y) { + // |mHasMoved| being false means there is no real change to the task bounds in WM core, so + // we don't need a WCT to finish it. + if (mHasMoved) { + final WindowContainerTransaction wct = new WindowContainerTransaction(); + wct.setDragResizing(mWindowDecoration.mTaskInfo.token, false /* dragResizing */); + changeBounds(wct, x, y); + mTaskOrganizer.applyTransaction(wct); + } + + mCtrlType = CTRL_TYPE_UNDEFINED; + mTaskBoundsAtDragStart.setEmpty(); + mRepositionStartPoint.set(0, 0); + mHasMoved = false; + } + + private boolean changeBounds(WindowContainerTransaction wct, float x, float y) { + // |mRepositionTaskBounds| is the bounds last reported if |mHasMoved| is true. If it's not + // true, we can compare it against |mTaskBoundsAtDragStart|. + final int oldLeft = mHasMoved ? mRepositionTaskBounds.left : mTaskBoundsAtDragStart.left; + final int oldTop = mHasMoved ? mRepositionTaskBounds.top : mTaskBoundsAtDragStart.top; + final int oldRight = mHasMoved ? mRepositionTaskBounds.right : mTaskBoundsAtDragStart.right; + final int oldBottom = + mHasMoved ? mRepositionTaskBounds.bottom : mTaskBoundsAtDragStart.bottom; + + final float deltaX = x - mRepositionStartPoint.x; + final float deltaY = y - mRepositionStartPoint.y; + mRepositionTaskBounds.set(mTaskBoundsAtDragStart); + + final Rect stableBounds = mTempBounds; + // Make sure the new resizing destination in any direction falls within the stable bounds. + // If not, set the bounds back to the old location that was valid to avoid conflicts with + // some regions such as the gesture area. + mDisplayController.getDisplayLayout(mWindowDecoration.mDisplay.getDisplayId()) + .getStableBounds(stableBounds); + if ((mCtrlType & CTRL_TYPE_LEFT) != 0) { + final int candidateLeft = mRepositionTaskBounds.left + (int) deltaX; + mRepositionTaskBounds.left = (candidateLeft > stableBounds.left) + ? candidateLeft : oldLeft; + } + if ((mCtrlType & CTRL_TYPE_RIGHT) != 0) { + final int candidateRight = mRepositionTaskBounds.right + (int) deltaX; + mRepositionTaskBounds.right = (candidateRight < stableBounds.right) + ? candidateRight : oldRight; + } + if ((mCtrlType & CTRL_TYPE_TOP) != 0) { + final int candidateTop = mRepositionTaskBounds.top + (int) deltaY; + mRepositionTaskBounds.top = (candidateTop > stableBounds.top) + ? candidateTop : oldTop; + } + if ((mCtrlType & CTRL_TYPE_BOTTOM) != 0) { + final int candidateBottom = mRepositionTaskBounds.bottom + (int) deltaY; + mRepositionTaskBounds.bottom = (candidateBottom < stableBounds.bottom) + ? candidateBottom : oldBottom; + } + if (mCtrlType == CTRL_TYPE_UNDEFINED) { + mRepositionTaskBounds.offset((int) deltaX, (int) deltaY); + } + + // If width or height are negative or less than the minimum width or height, revert the + // respective bounds to use previous bound dimensions. + if (mRepositionTaskBounds.width() < getMinWidth()) { + mRepositionTaskBounds.right = oldRight; + mRepositionTaskBounds.left = oldLeft; + } + if (mRepositionTaskBounds.height() < getMinHeight()) { + mRepositionTaskBounds.top = oldTop; + mRepositionTaskBounds.bottom = oldBottom; + } + // If there are no changes to the bounds after checking new bounds against minimum width + // and height, do not set bounds and return false + if (oldLeft == mRepositionTaskBounds.left && oldTop == mRepositionTaskBounds.top + && oldRight == mRepositionTaskBounds.right + && oldBottom == mRepositionTaskBounds.bottom) { + return false; + } + + wct.setBounds(mWindowDecoration.mTaskInfo.token, mRepositionTaskBounds); + return true; + } + + private float getMinWidth() { + return mWindowDecoration.mTaskInfo.minWidth < 0 ? getDefaultMinSize() + : mWindowDecoration.mTaskInfo.minWidth; + } + + private float getMinHeight() { + return mWindowDecoration.mTaskInfo.minHeight < 0 ? getDefaultMinSize() + : mWindowDecoration.mTaskInfo.minHeight; + } + + private float getDefaultMinSize() { + float density = mDisplayController.getDisplayLayout(mWindowDecoration.mTaskInfo.displayId) + .densityDpi() * DisplayMetrics.DENSITY_DEFAULT_SCALE; + return mWindowDecoration.mTaskInfo.defaultMinSize * density; + } + + interface DragStartListener { + /** + * Inform the implementing class that a drag resize has started + * @param taskId id of this positioner's {@link WindowDecoration} + */ + void onDragStart(int taskId); + } +} 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..9f03d9aec166 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecorViewModel.java @@ -0,0 +1,130 @@ +/* + * 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.os.IBinder; +import android.view.SurfaceControl; +import android.window.TransitionInfo; + +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. + */ +public interface WindowDecorViewModel { + /** + * Sets the transition starter that starts freeform task transitions. Only called when + * {@link com.android.wm.shell.transition.Transitions#ENABLE_SHELL_TRANSITIONS} is {@code true}. + * + * @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 {@code true} if window decoration was created, {@code false} otherwise + */ + boolean onTaskOpening( + ActivityManager.RunningTaskInfo taskInfo, + SurfaceControl taskSurface, + SurfaceControl.Transaction startT, + SurfaceControl.Transaction finishT); + + /** + * Notifies a task info update on the given task, with the window decoration created previously + * for this task by {@link #onTaskOpening}. + * + * @param taskInfo the new task info of the task + */ + void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo); + + /** + * Notifies a transition is about to start about the given task to give the window decoration a + * chance to prepare for this transition. Unlike {@link #onTaskInfoChanged}, this method creates + * a window decoration if one does not exist but is required. + * + * @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 + */ + void onTaskChanging( + ActivityManager.RunningTaskInfo taskInfo, + SurfaceControl taskSurface, + SurfaceControl.Transaction startT, + SurfaceControl.Transaction finishT); + + /** + * Notifies that the given task is about to close to give the window decoration a chance to + * prepare for this transition. + * + * @param taskInfo the initial task info 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 + */ + void onTaskClosing( + ActivityManager.RunningTaskInfo taskInfo, + SurfaceControl.Transaction startT, + SurfaceControl.Transaction finishT); + + /** + * Destroys the window decoration of the give task. + * + * @param taskInfo the info of the task + */ + void destroyWindowDecoration(ActivityManager.RunningTaskInfo taskInfo); + + /** + * Notifies that a shell transition is about to start. If the transition is of type + * TRANSIT_ENTER_DESKTOP, it will save that transition to unpause relayout for the transitioning + * task after the transition has ended. + * + * @param transition the ready transaction + * @param info of Transition to check if relayout needs to be paused for a task + * @param change a change in the given transition + */ + default void onTransitionReady(IBinder transition, TransitionInfo info, + TransitionInfo.Change change) {} + + /** + * Notifies that a shell transition is about to merge with another to give the window + * decoration a chance to prepare for this merge. + * + * @param merged the transaction being merged + * @param playing the transaction being merged into + */ + default void onTransitionMerged(IBinder merged, IBinder playing) {} + + /** + * Notifies that a shell transition is about to finish to give the window decoration a chance + * to clean up. + * + * @param transaction + */ + default void onTransitionFinished(IBinder transaction) {} + +}
\ No newline at end of file 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..4ebd09fdecee --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java @@ -0,0 +1,544 @@ +/* + * 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.content.res.Resources; +import android.graphics.Color; +import android.graphics.PixelFormat; +import android.graphics.Point; +import android.graphics.Rect; +import android.os.Binder; +import android.view.Display; +import android.view.LayoutInflater; +import android.view.SurfaceControl; +import android.view.SurfaceControlViewHost; +import android.view.View; +import android.view.ViewRootImpl; +import android.view.WindowInsets; +import android.view.WindowManager; +import android.view.WindowlessWindowManager; +import android.window.TaskConstants; +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 { + + /** + * 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; + int mLayoutResId; + final SurfaceControl mTaskSurface; + + Display mDisplay; + Context mDecorWindowContext; + SurfaceControl mDecorationContainerSurface; + SurfaceControl mTaskBackgroundSurface; + + SurfaceControl mCaptionContainerSurface; + private WindowlessWindowManager mCaptionWindowManager; + private SurfaceControlViewHost mViewHost; + + private final Binder mOwner = new Binder(); + 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( + getConfigurationWithOverrides(mTaskInfo)); + } + + /** + * Get {@link Configuration} from supplied {@link RunningTaskInfo}. + * + * Allows values to be overridden before returning the configuration. + */ + protected Configuration getConfigurationWithOverrides(RunningTaskInfo taskInfo) { + return taskInfo.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(RelayoutParams params, SurfaceControl.Transaction startT, + SurfaceControl.Transaction finishT, WindowContainerTransaction wct, T rootView, + RelayoutResult<T> outResult) { + outResult.reset(); + + final Configuration oldTaskConfig = mTaskInfo.getConfiguration(); + if (params.mRunningTaskInfo != null) { + mTaskInfo = params.mRunningTaskInfo; + } + final int oldLayoutResId = mLayoutResId; + mLayoutResId = params.mLayoutResId; + + if (!mTaskInfo.isVisible) { + releaseViews(); + finishT.hide(mTaskSurface); + return; + } + + if (rootView == null && params.mLayoutResId == 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 = getConfigurationWithOverrides(mTaskInfo); + if (oldTaskConfig.densityDpi != taskConfig.densityDpi + || mDisplay == null + || mDisplay.getDisplayId() != mTaskInfo.displayId + || oldLayoutResId != mLayoutResId) { + releaseViews(); + + if (!obtainDisplayOrRegisterListener()) { + outResult.mRootView = null; + return; + } + mDecorWindowContext = mContext.createConfigurationContext(taskConfig); + if (params.mLayoutResId != 0) { + outResult.mRootView = (T) LayoutInflater.from(mDecorWindowContext) + .inflate(params.mLayoutResId, null); + } + } + + if (outResult.mRootView == null) { + outResult.mRootView = (T) LayoutInflater.from(mDecorWindowContext) + .inflate(params.mLayoutResId , 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) + .setLayer(mDecorationContainerSurface, + TaskConstants.TASK_CHILD_LAYER_WINDOW_DECORATIONS); + } + + final Rect taskBounds = taskConfig.windowConfiguration.getBounds(); + final Resources resources = mDecorWindowContext.getResources(); + outResult.mDecorContainerOffsetX = -loadDimensionPixelSize(resources, params.mOutsetLeftId); + outResult.mDecorContainerOffsetY = -loadDimensionPixelSize(resources, params.mOutsetTopId); + outResult.mWidth = taskBounds.width() + + loadDimensionPixelSize(resources, params.mOutsetRightId) + - outResult.mDecorContainerOffsetX; + outResult.mHeight = taskBounds.height() + + loadDimensionPixelSize(resources, params.mOutsetBottomId) + - outResult.mDecorContainerOffsetY; + startT.setPosition( + mDecorationContainerSurface, + outResult.mDecorContainerOffsetX, outResult.mDecorContainerOffsetY) + .setWindowCrop(mDecorationContainerSurface, + outResult.mWidth, outResult.mHeight) + .show(mDecorationContainerSurface); + + // TaskBackgroundSurface + if (mTaskBackgroundSurface == null) { + final SurfaceControl.Builder builder = mSurfaceControlBuilderSupplier.get(); + mTaskBackgroundSurface = builder + .setName("Background of Task=" + mTaskInfo.taskId) + .setEffectLayer() + .setParent(mTaskSurface) + .build(); + + startT.setLayer(mTaskBackgroundSurface, TaskConstants.TASK_CHILD_LAYER_TASK_BACKGROUND); + } + + float shadowRadius = loadDimension(resources, params.mShadowRadiusId); + 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) + .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 = loadDimensionPixelSize(resources, params.mCaptionHeightId); + final int captionWidth = taskBounds.width(); + + startT.setPosition( + mCaptionContainerSurface, + -outResult.mDecorContainerOffsetX + params.mCaptionX, + -outResult.mDecorContainerOffsetY + params.mCaptionY) + .setWindowCrop(mCaptionContainerSurface, captionWidth, 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 WindowlessWindowManager( + mTaskInfo.getConfiguration(), mCaptionContainerSurface, + null /* hostInputToken */); + } + + // Caption view + mCaptionWindowManager.setConfiguration(taskConfig); + final WindowManager.LayoutParams lp = + new WindowManager.LayoutParams(captionWidth, 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 + params.mCaptionY; + wct.addInsetsSource(mTaskInfo.token, + mOwner, 0 /* index */, WindowInsets.Type.captionBar(), mCaptionInsetsRect); + } else { + startT.hide(mCaptionContainerSurface); + } + + // Task surface itself + Point taskPosition = mTaskInfo.positionInParent; + mTaskSurfaceCrop.set( + outResult.mDecorContainerOffsetX, + outResult.mDecorContainerOffsetY, + outResult.mWidth + outResult.mDecorContainerOffsetX, + outResult.mHeight + outResult.mDecorContainerOffsetY); + 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; + } + + 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.removeInsetsSource(mTaskInfo.token, + mOwner, 0 /* index */, WindowInsets.Type.captionBar()); + mTaskOrganizer.applyTransaction(wct); + } + + @Override + public void close() { + mDisplayController.removeDisplayWindowListener(mOnDisplaysChangedListener); + releaseViews(); + } + + static int loadDimensionPixelSize(Resources resources, int resourceId) { + if (resourceId == Resources.ID_NULL) { + return 0; + } + return resources.getDimensionPixelSize(resourceId); + } + + static float loadDimension(Resources resources, int resourceId) { + if (resourceId == Resources.ID_NULL) { + return 0; + } + return resources.getDimension(resourceId); + } + + /** + * Create a window associated with this WindowDecoration. + * Note that subclass must dispose of this when the task is hidden/closed. + * @param layoutId layout to make the window from + * @param t the transaction to apply + * @param xPos x position of new window + * @param yPos y position of new window + * @param width width of new window + * @param height height of new window + * @param shadowRadius radius of the shadow of the new window + * @param cornerRadius radius of the corners of the new window + * @return the {@link AdditionalWindow} that was added. + */ + AdditionalWindow addWindow(int layoutId, String namePrefix, SurfaceControl.Transaction t, + int xPos, int yPos, int width, int height, int shadowRadius, int cornerRadius) { + final SurfaceControl.Builder builder = mSurfaceControlBuilderSupplier.get(); + SurfaceControl windowSurfaceControl = builder + .setName(namePrefix + " of Task=" + mTaskInfo.taskId) + .setContainerLayer() + .setParent(mDecorationContainerSurface) + .build(); + View v = LayoutInflater.from(mDecorWindowContext).inflate(layoutId, null); + + t.setPosition(windowSurfaceControl, xPos, yPos) + .setWindowCrop(windowSurfaceControl, width, height) + .setShadowRadius(windowSurfaceControl, shadowRadius) + .setCornerRadius(windowSurfaceControl, cornerRadius) + .show(windowSurfaceControl); + final WindowManager.LayoutParams lp = + new WindowManager.LayoutParams(width, height, + WindowManager.LayoutParams.TYPE_APPLICATION, + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, PixelFormat.TRANSPARENT); + lp.setTitle("Additional window of Task=" + mTaskInfo.taskId); + lp.setTrustedOverlay(); + WindowlessWindowManager windowManager = new WindowlessWindowManager(mTaskInfo.configuration, + windowSurfaceControl, null /* hostInputToken */); + SurfaceControlViewHost viewHost = mSurfaceControlViewHostFactory + .create(mDecorWindowContext, mDisplay, windowManager); + viewHost.setView(v, lp); + return new AdditionalWindow(windowSurfaceControl, viewHost, + mSurfaceControlTransactionSupplier); + } + + static class RelayoutParams{ + RunningTaskInfo mRunningTaskInfo; + int mLayoutResId; + int mCaptionHeightId; + int mCaptionWidthId; + int mShadowRadiusId; + + int mOutsetTopId; + int mOutsetBottomId; + int mOutsetLeftId; + int mOutsetRightId; + + int mCaptionX; + int mCaptionY; + + void setOutsets(int leftId, int topId, int rightId, int bottomId) { + mOutsetLeftId = leftId; + mOutsetTopId = topId; + mOutsetRightId = rightId; + mOutsetBottomId = bottomId; + } + + void setCaptionPosition(int left, int top) { + mCaptionX = left; + mCaptionY = top; + } + + void reset() { + mLayoutResId = Resources.ID_NULL; + mCaptionHeightId = Resources.ID_NULL; + mCaptionWidthId = Resources.ID_NULL; + mShadowRadiusId = Resources.ID_NULL; + + mOutsetTopId = Resources.ID_NULL; + mOutsetBottomId = Resources.ID_NULL; + mOutsetLeftId = Resources.ID_NULL; + mOutsetRightId = Resources.ID_NULL; + + mCaptionX = 0; + mCaptionY = 0; + } + } + + static class RelayoutResult<T extends View & TaskFocusStateConsumer> { + int mWidth; + int mHeight; + T mRootView; + int mDecorContainerOffsetX; + int mDecorContainerOffsetY; + + void reset() { + mWidth = 0; + mHeight = 0; + mDecorContainerOffsetX = 0; + mDecorContainerOffsetY = 0; + mRootView = null; + } + } + + interface SurfaceControlViewHostFactory { + default SurfaceControlViewHost create(Context c, Display d, WindowlessWindowManager wmm) { + return new SurfaceControlViewHost(c, d, wmm, "WindowDecoration"); + } + } + + /** + * Subclass for additional windows associated with this WindowDecoration + */ + static class AdditionalWindow { + SurfaceControl mWindowSurface; + SurfaceControlViewHost mWindowViewHost; + Supplier<SurfaceControl.Transaction> mTransactionSupplier; + + private AdditionalWindow(SurfaceControl surfaceControl, + SurfaceControlViewHost surfaceControlViewHost, + Supplier<SurfaceControl.Transaction> transactionSupplier) { + mWindowSurface = surfaceControl; + mWindowViewHost = surfaceControlViewHost; + mTransactionSupplier = transactionSupplier; + } + + void releaseView() { + WindowlessWindowManager windowManager = mWindowViewHost.getWindowlessWM(); + + if (mWindowViewHost != null) { + mWindowViewHost.release(); + mWindowViewHost = null; + } + windowManager = null; + final SurfaceControl.Transaction t = mTransactionSupplier.get(); + boolean released = false; + if (mWindowSurface != null) { + t.remove(mWindowSurface); + mWindowSurface = null; + released = true; + } + if (released) { + t.apply(); + } + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeAppControlsWindowDecorationViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeAppControlsWindowDecorationViewHolder.kt new file mode 100644 index 000000000000..78cfcbd27ed6 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeAppControlsWindowDecorationViewHolder.kt @@ -0,0 +1,82 @@ +package com.android.wm.shell.windowdecor.viewholder + +import android.app.ActivityManager.RunningTaskInfo +import android.content.res.ColorStateList +import android.graphics.drawable.Drawable +import android.graphics.drawable.GradientDrawable +import android.view.View +import android.widget.ImageButton +import android.widget.ImageView +import android.widget.TextView +import com.android.wm.shell.R + +/** + * A desktop mode window decoration used when the window is floating (i.e. freeform). It hosts + * finer controls such as a close window button and an "app info" section to pull up additional + * controls. + */ +internal class DesktopModeAppControlsWindowDecorationViewHolder( + rootView: View, + onCaptionTouchListener: View.OnTouchListener, + onCaptionButtonClickListener: View.OnClickListener, + appName: CharSequence, + appIcon: Drawable +) : DesktopModeWindowDecorationViewHolder(rootView) { + + private val captionView: View = rootView.findViewById(R.id.desktop_mode_caption) + private val captionHandle: View = rootView.findViewById(R.id.caption_handle) + private val openMenuButton: View = rootView.findViewById(R.id.open_menu_button) + private val closeWindowButton: ImageButton = rootView.findViewById(R.id.close_window) + private val expandMenuButton: ImageButton = rootView.findViewById(R.id.expand_menu_button) + private val appNameTextView: TextView = rootView.findViewById(R.id.application_name) + private val appIconImageView: ImageView = rootView.findViewById(R.id.application_icon) + + init { + captionView.setOnTouchListener(onCaptionTouchListener) + captionHandle.setOnTouchListener(onCaptionTouchListener) + openMenuButton.setOnClickListener(onCaptionButtonClickListener) + closeWindowButton.setOnClickListener(onCaptionButtonClickListener) + appNameTextView.text = appName + appIconImageView.setImageDrawable(appIcon) + } + + override fun bindData(taskInfo: RunningTaskInfo) { + + val captionDrawable = captionView.background as GradientDrawable + captionDrawable.setColor(taskInfo.taskDescription.statusBarColor) + + closeWindowButton.imageTintList = ColorStateList.valueOf( + getCaptionCloseButtonColor(taskInfo)) + expandMenuButton.imageTintList = ColorStateList.valueOf( + getCaptionExpandButtonColor(taskInfo)) + appNameTextView.setTextColor(getCaptionAppNameTextColor(taskInfo)) + } + + private fun getCaptionAppNameTextColor(taskInfo: RunningTaskInfo): Int { + return if (shouldUseLightCaptionColors(taskInfo)) { + context.getColor(R.color.desktop_mode_caption_app_name_light) + } else { + context.getColor(R.color.desktop_mode_caption_app_name_dark) + } + } + + private fun getCaptionCloseButtonColor(taskInfo: RunningTaskInfo): Int { + return if (shouldUseLightCaptionColors(taskInfo)) { + context.getColor(R.color.desktop_mode_caption_close_button_light) + } else { + context.getColor(R.color.desktop_mode_caption_close_button_dark) + } + } + + private fun getCaptionExpandButtonColor(taskInfo: RunningTaskInfo): Int { + return if (shouldUseLightCaptionColors(taskInfo)) { + context.getColor(R.color.desktop_mode_caption_expand_button_light) + } else { + context.getColor(R.color.desktop_mode_caption_expand_button_dark) + } + } + + companion object { + private const val TAG = "DesktopModeAppControlsWindowDecorationViewHolder" + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeFocusedWindowDecorationViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeFocusedWindowDecorationViewHolder.kt new file mode 100644 index 000000000000..47a12a0cb71c --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeFocusedWindowDecorationViewHolder.kt @@ -0,0 +1,44 @@ +package com.android.wm.shell.windowdecor.viewholder + +import android.app.ActivityManager.RunningTaskInfo +import android.content.res.ColorStateList +import android.graphics.drawable.GradientDrawable +import android.view.View +import android.widget.ImageButton +import com.android.wm.shell.R + +/** + * A desktop mode window decoration used when the window is in full "focus" (i.e. fullscreen). It + * hosts a simple handle bar from which to initiate a drag motion to enter desktop mode. + */ +internal class DesktopModeFocusedWindowDecorationViewHolder( + rootView: View, + onCaptionTouchListener: View.OnTouchListener, + onCaptionButtonClickListener: View.OnClickListener +) : DesktopModeWindowDecorationViewHolder(rootView) { + + private val captionView: View = rootView.findViewById(R.id.desktop_mode_caption) + private val captionHandle: ImageButton = rootView.findViewById(R.id.caption_handle) + + init { + captionView.setOnTouchListener(onCaptionTouchListener) + captionHandle.setOnTouchListener(onCaptionTouchListener) + captionHandle.setOnClickListener(onCaptionButtonClickListener) + } + + override fun bindData(taskInfo: RunningTaskInfo) { + val captionColor = taskInfo.taskDescription.statusBarColor + val captionDrawable = captionView.background as GradientDrawable + captionDrawable.setColor(captionColor) + + captionHandle.imageTintList = ColorStateList.valueOf(getCaptionHandleBarColor(taskInfo)) + } + + private fun getCaptionHandleBarColor(taskInfo: RunningTaskInfo): Int { + return if (shouldUseLightCaptionColors(taskInfo)) { + context.getColor(R.color.desktop_mode_caption_handle_bar_light) + } else { + context.getColor(R.color.desktop_mode_caption_handle_bar_dark) + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeWindowDecorationViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeWindowDecorationViewHolder.kt new file mode 100644 index 000000000000..514ea52cb8ae --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeWindowDecorationViewHolder.kt @@ -0,0 +1,28 @@ +package com.android.wm.shell.windowdecor.viewholder + +import android.app.ActivityManager.RunningTaskInfo +import android.content.Context +import android.graphics.Color +import android.view.View + +/** + * Encapsulates the root [View] of a window decoration and its children to facilitate looking up + * children (via findViewById) and updating to the latest data from [RunningTaskInfo]. + */ +internal abstract class DesktopModeWindowDecorationViewHolder(rootView: View) { + val context: Context = rootView.context + + /** + * A signal to the view holder that new data is available and that the views should be updated + * to reflect it. + */ + abstract fun bindData(taskInfo: RunningTaskInfo) + + /** + * Whether the caption items should use the 'light' color variant so that there's good contrast + * with the caption background color. + */ + protected fun shouldUseLightCaptionColors(taskInfo: RunningTaskInfo): Boolean { + return Color.valueOf(taskInfo.taskDescription.statusBarColor).luminance() < 0.5 + } +} diff --git a/libs/WindowManager/Shell/tests/OWNERS b/libs/WindowManager/Shell/tests/OWNERS index f4efc374ecc2..1c28c3d58ccb 100644 --- a/libs/WindowManager/Shell/tests/OWNERS +++ b/libs/WindowManager/Shell/tests/OWNERS @@ -6,3 +6,4 @@ pablogamito@google.com lbill@google.com madym@google.com hwwang@google.com +chenghsiuchang@google.com diff --git a/libs/WindowManager/Shell/tests/flicker/Android.bp b/libs/WindowManager/Shell/tests/flicker/Android.bp index 3ca5b9c38aff..b6696c70dbb1 100644 --- a/libs/WindowManager/Shell/tests/flicker/Android.bp +++ b/libs/WindowManager/Shell/tests/flicker/Android.bp @@ -41,6 +41,8 @@ android_test { static_libs: [ "androidx.test.ext.junit", "flickerlib", + "flickerlib-apphelpers", + "flickerlib-helpers", "truth-prebuilt", "app-helpers-core", "launcher-helper-lib", @@ -48,6 +50,6 @@ android_test { "wm-flicker-common-assertions", "wm-flicker-common-app-helpers", "platform-test-annotations", - "wmshell-flicker-test-components", + "flickertestapplib", ], } diff --git a/libs/WindowManager/Shell/tests/flicker/AndroidManifest.xml b/libs/WindowManager/Shell/tests/flicker/AndroidManifest.xml index 06df9568e01a..4721741611cf 100644 --- a/libs/WindowManager/Shell/tests/flicker/AndroidManifest.xml +++ b/libs/WindowManager/Shell/tests/flicker/AndroidManifest.xml @@ -46,7 +46,7 @@ <uses-permission android:name="android.permission.STATUS_BAR_SERVICE" /> <!-- Allow the test to write directly to /sdcard/ --> - <application android:requestLegacyExternalStorage="true"> + <application android:requestLegacyExternalStorage="true" android:largeHeap="true"> <uses-library android:name="android.test.runner"/> <service android:name=".NotificationListener" diff --git a/libs/WindowManager/Shell/tests/flicker/AndroidTest.xml b/libs/WindowManager/Shell/tests/flicker/AndroidTest.xml index 574a9f4da627..67ca9a1a17f7 100644 --- a/libs/WindowManager/Shell/tests/flicker/AndroidTest.xml +++ b/libs/WindowManager/Shell/tests/flicker/AndroidTest.xml @@ -13,13 +13,27 @@ <option name="run-command" value="cmd window tracing level all" /> <!-- set WM tracing to frame (avoid incomplete states) --> <option name="run-command" value="cmd window tracing frame" /> + <!-- disable betterbug as it's log collection dialogues cause flakes in e2e tests --> + <option name="run-command" value="pm disable com.google.android.internal.betterbug" /> + <!-- ensure lock screen mode is swipe --> + <option name="run-command" value="locksettings set-disabled false" /> <!-- restart launcher to activate TAPL --> <option name="run-command" value="setprop ro.test_harness 1 ; am force-stop com.google.android.apps.nexuslauncher" /> + <!-- Ensure output directory is empty at the start --> + <option name="run-command" value="rm -rf /sdcard/flicker" /> + </target_preparer> + <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer"> + <option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1" /> + <option name="run-command" value="settings put system show_touches 1" /> + <option name="run-command" value="settings put system pointer_location 1" /> + <option name="teardown-command" value="settings delete secure show_ime_with_hard_keyboard" /> + <option name="teardown-command" value="settings delete system show_touches" /> + <option name="teardown-command" value="settings delete system pointer_location" /> </target_preparer> <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller"> <option name="cleanup-apks" value="true"/> <option name="test-file-name" value="WMShellFlickerTests.apk"/> - <option name="test-file-name" value="WMShellFlickerTestApp.apk" /> + <option name="test-file-name" value="FlickerTestApp.apk" /> </target_preparer> <test class="com.android.tradefed.testtype.AndroidJUnitTest"> <option name="package" value="com.android.wm.shell.flicker"/> @@ -32,4 +46,4 @@ <option name="collect-on-run-ended-only" value="true" /> <option name="clean-up" value="true" /> </metrics_collector> -</configuration>
\ No newline at end of file +</configuration> 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..c5ee7b722617 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/BaseTest.kt @@ -0,0 +1,167 @@ +/* + * 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 android.tools.common.datatypes.component.ComponentNameMatcher +import android.tools.device.flicker.junit.FlickerBuilderProvider +import android.tools.device.flicker.legacy.FlickerBuilder +import android.tools.device.flicker.legacy.FlickerTest +import androidx.test.platform.app.InstrumentationRegistry +import com.android.launcher3.tapl.LauncherInstrumentation +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 org.junit.Assume +import org.junit.Test + +/** + * Base test class containing common assertions for [ComponentNameMatcher.NAV_BAR], + * [ComponentNameMatcher.TASK_BAR], [ComponentNameMatcher.STATUS_BAR], and general assertions + * (layers visible in consecutive states, entire screen covered, etc.) + */ +abstract class BaseTest +@JvmOverloads +constructor( + protected val flicker: FlickerTest, + protected val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation(), + protected val tapl: LauncherInstrumentation = LauncherInstrumentation() +) { + /** 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 { flicker.scenario.setIsTablet(tapl.isTablet) } + transition() + } + } + + /** Checks that all parts of the screen are covered during the transition */ + @Presubmit @Test open fun entireScreenCovered() = flicker.entireScreenCovered() + + /** + * Checks that the [ComponentNameMatcher.NAV_BAR] layer is visible during the whole transition + */ + @Presubmit + @Test + open fun navBarLayerIsVisibleAtStartAndEnd() { + Assume.assumeFalse(flicker.scenario.isTablet) + flicker.navBarLayerIsVisibleAtStartAndEnd() + } + + /** + * Checks the position of the [ComponentNameMatcher.NAV_BAR] at the start and end of the + * transition + */ + @Presubmit + @Test + open fun navBarLayerPositionAtStartAndEnd() { + Assume.assumeFalse(flicker.scenario.isTablet) + flicker.navBarLayerPositionAtStartAndEnd() + } + + /** + * Checks that the [ComponentNameMatcher.NAV_BAR] window is visible during the whole transition + * + * Note: Phones only + */ + @Presubmit + @Test + open fun navBarWindowIsAlwaysVisible() { + Assume.assumeFalse(flicker.scenario.isTablet) + flicker.navBarWindowIsAlwaysVisible() + } + + /** + * Checks that the [ComponentNameMatcher.TASK_BAR] layer is visible during the whole transition + */ + @Presubmit + @Test + open fun taskBarLayerIsVisibleAtStartAndEnd() { + Assume.assumeTrue(flicker.scenario.isTablet) + flicker.taskBarLayerIsVisibleAtStartAndEnd() + } + + /** + * Checks that the [ComponentNameMatcher.TASK_BAR] window is visible during the whole transition + * + * Note: Large screen only + */ + @Presubmit + @Test + open fun taskBarWindowIsAlwaysVisible() { + Assume.assumeTrue(flicker.scenario.isTablet) + flicker.taskBarWindowIsAlwaysVisible() + } + + /** + * Checks that the [ComponentNameMatcher.STATUS_BAR] layer is visible during the whole + * transition + */ + @Presubmit + @Test + open fun statusBarLayerIsVisibleAtStartAndEnd() = flicker.statusBarLayerIsVisibleAtStartAndEnd() + + /** + * Checks the position of the [ComponentNameMatcher.STATUS_BAR] at the start and end of the + * transition + */ + @Presubmit + @Test + open fun statusBarLayerPositionAtStartAndEnd() = flicker.statusBarLayerPositionAtStartAndEnd() + + /** + * Checks that the [ComponentNameMatcher.STATUS_BAR] window is visible during the whole + * transition + */ + @Presubmit + @Test + open fun statusBarWindowIsAlwaysVisible() = flicker.statusBarWindowIsAlwaysVisible() + + /** + * Checks that all layers that are visible on the trace, are visible for at least 2 consecutive + * entries. + */ + @Presubmit + @Test + open fun visibleLayersShownMoreThanOneConsecutiveEntry() { + flicker.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() { + flicker.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..e986ee127708 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 @@ -15,27 +15,26 @@ */ @file:JvmName("CommonAssertions") + 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.traces.common.region.Region +import android.tools.common.Rotation +import android.tools.common.datatypes.Region +import android.tools.common.datatypes.component.IComponentMatcher +import android.tools.common.flicker.subject.layers.LayerTraceEntrySubject +import android.tools.common.flicker.subject.layers.LayersTraceSubject +import android.tools.device.flicker.legacy.FlickerTest +import android.tools.device.helpers.WindowUtils -fun FlickerTestParameter.appPairsDividerIsVisibleAtEnd() { - assertLayersEnd { - this.isVisible(APP_PAIR_SPLIT_DIVIDER_COMPONENT) - } +fun FlickerTest.appPairsDividerIsVisibleAtEnd() { + assertLayersEnd { this.isVisible(APP_PAIR_SPLIT_DIVIDER_COMPONENT) } } -fun FlickerTestParameter.appPairsDividerIsInvisibleAtEnd() { - assertLayersEnd { - this.notContains(APP_PAIR_SPLIT_DIVIDER_COMPONENT) - } +fun FlickerTest.appPairsDividerIsInvisibleAtEnd() { + assertLayersEnd { this.notContains(APP_PAIR_SPLIT_DIVIDER_COMPONENT) } } -fun FlickerTestParameter.appPairsDividerBecomesVisible() { +fun FlickerTest.appPairsDividerBecomesVisible() { assertLayers { this.isInvisible(DOCKED_STACK_DIVIDER_COMPONENT) .then() @@ -43,13 +42,322 @@ fun FlickerTestParameter.appPairsDividerBecomesVisible() { } } -fun FlickerTestParameter.dockedStackDividerIsVisibleAtEnd() { +fun FlickerTest.splitScreenEntered( + component1: IComponentMatcher, + component2: IComponentMatcher, + fromOtherApp: Boolean, + appExistAtStart: Boolean = true +) { + if (fromOtherApp) { + if (appExistAtStart) { + appWindowIsInvisibleAtStart(component1) + } else { + appWindowIsNotContainAtStart(component1) + } + } else { + appWindowIsVisibleAtStart(component1) + } + if (appExistAtStart) { + appWindowIsInvisibleAtStart(component2) + } else { + appWindowIsNotContainAtStart(component2) + } + splitScreenDividerIsInvisibleAtStart() + + appWindowIsVisibleAtEnd(component1) + appWindowIsVisibleAtEnd(component2) + splitScreenDividerIsVisibleAtEnd() +} + +fun FlickerTest.splitScreenDismissed( + component1: IComponentMatcher, + component2: IComponentMatcher, + toHome: Boolean +) { + appWindowIsVisibleAtStart(component1) + appWindowIsVisibleAtStart(component2) + splitScreenDividerIsVisibleAtStart() + + appWindowIsInvisibleAtEnd(component1) + if (toHome) { + appWindowIsInvisibleAtEnd(component2) + } else { + appWindowIsVisibleAtEnd(component2) + } + splitScreenDividerIsInvisibleAtEnd() +} + +fun FlickerTest.splitScreenDividerIsVisibleAtStart() { + assertLayersStart { this.isVisible(SPLIT_SCREEN_DIVIDER_COMPONENT) } +} + +fun FlickerTest.splitScreenDividerIsVisibleAtEnd() { + assertLayersEnd { this.isVisible(SPLIT_SCREEN_DIVIDER_COMPONENT) } +} + +fun FlickerTest.splitScreenDividerIsInvisibleAtStart() { + assertLayersStart { this.isInvisible(SPLIT_SCREEN_DIVIDER_COMPONENT) } +} + +fun FlickerTest.splitScreenDividerIsInvisibleAtEnd() { + assertLayersEnd { this.isInvisible(SPLIT_SCREEN_DIVIDER_COMPONENT) } +} + +fun FlickerTest.splitScreenDividerBecomesVisible() { + layerBecomesVisible(SPLIT_SCREEN_DIVIDER_COMPONENT) +} + +fun FlickerTest.splitScreenDividerBecomesInvisible() { + assertLayers { + this.isVisible(SPLIT_SCREEN_DIVIDER_COMPONENT) + .then() + .isInvisible(SPLIT_SCREEN_DIVIDER_COMPONENT) + } +} + +fun FlickerTest.layerBecomesVisible(component: IComponentMatcher) { + assertLayers { this.isInvisible(component).then().isVisible(component) } +} + +fun FlickerTest.layerBecomesInvisible(component: IComponentMatcher) { + assertLayers { this.isVisible(component).then().isInvisible(component) } +} + +fun FlickerTest.layerIsVisibleAtEnd(component: IComponentMatcher) { + assertLayersEnd { this.isVisible(component) } +} + +fun FlickerTest.layerKeepVisible(component: IComponentMatcher) { + assertLayers { this.isVisible(component) } +} + +fun FlickerTest.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, + scenario.endRotation + ) + } +} + +fun FlickerTest.splitAppLayerBoundsBecomesVisibleByDrag(component: IComponentMatcher) { + assertLayers { + this.notContains(SPLIT_SCREEN_DIVIDER_COMPONENT.or(component), isOptional = true) + .then() + .isInvisible(SPLIT_SCREEN_DIVIDER_COMPONENT.or(component)) + .then() + // TODO(b/245472831): Verify the component should snap to divider. + .isVisible(component) + } +} + +fun FlickerTest.splitAppLayerBoundsBecomesInvisible( + component: IComponentMatcher, + landscapePosLeft: Boolean, + portraitPosTop: Boolean +) { + assertLayers { + this.splitAppLayerBoundsSnapToDivider( + component, + landscapePosLeft, + portraitPosTop, + scenario.endRotation + ) + .then() + .isVisible(component, true) + .then() + .isInvisible(component) + } +} + +fun FlickerTest.splitAppLayerBoundsIsVisibleAtEnd( + component: IComponentMatcher, + landscapePosLeft: Boolean, + portraitPosTop: Boolean +) { assertLayersEnd { - this.isVisible(DOCKED_STACK_DIVIDER_COMPONENT) + splitAppLayerBoundsSnapToDivider( + component, + landscapePosLeft, + portraitPosTop, + scenario.endRotation + ) } } -fun FlickerTestParameter.dockedStackDividerBecomesVisible() { +fun FlickerTest.splitAppLayerBoundsKeepVisible( + component: IComponentMatcher, + landscapePosLeft: Boolean, + portraitPosTop: Boolean +) { + assertLayers { + splitAppLayerBoundsSnapToDivider( + component, + landscapePosLeft, + portraitPosTop, + scenario.endRotation + ) + } +} + +fun FlickerTest.splitAppLayerBoundsChanges( + component: IComponentMatcher, + landscapePosLeft: Boolean, + portraitPosTop: Boolean +) { + assertLayers { + if (landscapePosLeft) { + splitAppLayerBoundsSnapToDivider( + component, + landscapePosLeft, + portraitPosTop, + scenario.endRotation + ) + .then() + .isInvisible(component) + .then() + .splitAppLayerBoundsSnapToDivider( + component, + landscapePosLeft, + portraitPosTop, + scenario.endRotation + ) + } else { + splitAppLayerBoundsSnapToDivider( + component, + landscapePosLeft, + portraitPosTop, + scenario.endRotation + ) + .then() + .isInvisible(component) + .then() + .splitAppLayerBoundsSnapToDivider( + component, + landscapePosLeft, + portraitPosTop, + scenario.endRotation + ) + } + } +} + +fun LayersTraceSubject.splitAppLayerBoundsSnapToDivider( + component: IComponentMatcher, + landscapePosLeft: Boolean, + portraitPosTop: Boolean, + rotation: Rotation +): LayersTraceSubject { + return invoke("splitAppLayerBoundsSnapToDivider") { + it.splitAppLayerBoundsSnapToDivider(component, landscapePosLeft, portraitPosTop, rotation) + } +} + +fun LayerTraceEntrySubject.splitAppLayerBoundsSnapToDivider( + component: IComponentMatcher, + landscapePosLeft: Boolean, + portraitPosTop: Boolean, + rotation: Rotation +): LayerTraceEntrySubject { + val displayBounds = WindowUtils.getDisplayBounds(rotation) + return invoke { + val dividerRegion = + layer(SPLIT_SCREEN_DIVIDER_COMPONENT)?.visibleRegion?.region + ?: error("$SPLIT_SCREEN_DIVIDER_COMPONENT component not found") + 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 FlickerTest.appWindowBecomesVisible(component: IComponentMatcher) { + assertWm { + this.isAppWindowInvisible(component) + .then() + .notContains(component, isOptional = true) + .then() + .isAppWindowInvisible(component, isOptional = true) + .then() + .isAppWindowVisible(component) + } +} + +fun FlickerTest.appWindowBecomesInvisible(component: IComponentMatcher) { + assertWm { this.isAppWindowVisible(component).then().isAppWindowInvisible(component) } +} + +fun FlickerTest.appWindowIsVisibleAtStart(component: IComponentMatcher) { + assertWmStart { this.isAppWindowVisible(component) } +} + +fun FlickerTest.appWindowIsVisibleAtEnd(component: IComponentMatcher) { + assertWmEnd { this.isAppWindowVisible(component) } +} + +fun FlickerTest.appWindowIsInvisibleAtStart(component: IComponentMatcher) { + assertWmStart { this.isAppWindowInvisible(component) } +} + +fun FlickerTest.appWindowIsInvisibleAtEnd(component: IComponentMatcher) { + assertWmEnd { this.isAppWindowInvisible(component) } +} + +fun FlickerTest.appWindowIsNotContainAtStart(component: IComponentMatcher) { + assertWmStart { this.notContains(component) } +} + +fun FlickerTest.appWindowKeepVisible(component: IComponentMatcher) { + assertWm { this.isAppWindowVisible(component) } +} + +fun FlickerTest.dockedStackDividerIsVisibleAtEnd() { + assertLayersEnd { this.isVisible(DOCKED_STACK_DIVIDER_COMPONENT) } +} + +fun FlickerTest.dockedStackDividerBecomesVisible() { assertLayers { this.isInvisible(DOCKED_STACK_DIVIDER_COMPONENT) .then() @@ -57,7 +365,7 @@ fun FlickerTestParameter.dockedStackDividerBecomesVisible() { } } -fun FlickerTestParameter.dockedStackDividerBecomesInvisible() { +fun FlickerTest.dockedStackDividerBecomesInvisible() { assertLayers { this.isVisible(DOCKED_STACK_DIVIDER_COMPONENT) .then() @@ -65,74 +373,92 @@ fun FlickerTestParameter.dockedStackDividerBecomesInvisible() { } } -fun FlickerTestParameter.dockedStackDividerNotExistsAtEnd() { - assertLayersEnd { - this.notContains(DOCKED_STACK_DIVIDER_COMPONENT) - } +fun FlickerTest.dockedStackDividerNotExistsAtEnd() { + assertLayersEnd { this.notContains(DOCKED_STACK_DIVIDER_COMPONENT) } } -fun FlickerTestParameter.appPairsPrimaryBoundsIsVisibleAtEnd( - rotation: Int, - primaryComponent: FlickerComponentName +fun FlickerTest.appPairsPrimaryBoundsIsVisibleAtEnd( + rotation: Rotation, + primaryComponent: IComponentMatcher ) { assertLayersEnd { - val dividerRegion = layer(APP_PAIR_SPLIT_DIVIDER_COMPONENT).visibleRegion.region - visibleRegion(primaryComponent) - .overlaps(getPrimaryRegion(dividerRegion, rotation)) + val dividerRegion = + layer(APP_PAIR_SPLIT_DIVIDER_COMPONENT)?.visibleRegion?.region + ?: error("$APP_PAIR_SPLIT_DIVIDER_COMPONENT component not found") + visibleRegion(primaryComponent).overlaps(getPrimaryRegion(dividerRegion, rotation)) } } -fun FlickerTestParameter.dockedStackPrimaryBoundsIsVisibleAtEnd( - rotation: Int, - primaryComponent: FlickerComponentName +fun FlickerTest.dockedStackPrimaryBoundsIsVisibleAtEnd( + rotation: Rotation, + primaryComponent: IComponentMatcher ) { assertLayersEnd { - val dividerRegion = layer(DOCKED_STACK_DIVIDER_COMPONENT).visibleRegion.region - visibleRegion(primaryComponent) - .overlaps(getPrimaryRegion(dividerRegion, rotation)) + val dividerRegion = + layer(DOCKED_STACK_DIVIDER_COMPONENT)?.visibleRegion?.region + ?: error("$DOCKED_STACK_DIVIDER_COMPONENT component not found") + visibleRegion(primaryComponent).overlaps(getPrimaryRegion(dividerRegion, rotation)) } } -fun FlickerTestParameter.appPairsSecondaryBoundsIsVisibleAtEnd( - rotation: Int, - secondaryComponent: FlickerComponentName +fun FlickerTest.appPairsSecondaryBoundsIsVisibleAtEnd( + rotation: Rotation, + secondaryComponent: IComponentMatcher ) { assertLayersEnd { - val dividerRegion = layer(APP_PAIR_SPLIT_DIVIDER_COMPONENT).visibleRegion.region - visibleRegion(secondaryComponent) - .overlaps(getSecondaryRegion(dividerRegion, rotation)) + val dividerRegion = + layer(APP_PAIR_SPLIT_DIVIDER_COMPONENT)?.visibleRegion?.region + ?: error("$APP_PAIR_SPLIT_DIVIDER_COMPONENT component not found") + visibleRegion(secondaryComponent).overlaps(getSecondaryRegion(dividerRegion, rotation)) } } -fun FlickerTestParameter.dockedStackSecondaryBoundsIsVisibleAtEnd( - rotation: Int, - secondaryComponent: FlickerComponentName +fun FlickerTest.dockedStackSecondaryBoundsIsVisibleAtEnd( + rotation: Rotation, + secondaryComponent: IComponentMatcher ) { assertLayersEnd { - val dividerRegion = layer(DOCKED_STACK_DIVIDER_COMPONENT).visibleRegion.region - visibleRegion(secondaryComponent) - .overlaps(getSecondaryRegion(dividerRegion, rotation)) + val dividerRegion = + layer(DOCKED_STACK_DIVIDER_COMPONENT)?.visibleRegion?.region + ?: error("$DOCKED_STACK_DIVIDER_COMPONENT component not found") + visibleRegion(secondaryComponent).overlaps(getSecondaryRegion(dividerRegion, rotation)) } } -fun getPrimaryRegion(dividerRegion: Region, rotation: Int): Region { +fun getPrimaryRegion(dividerRegion: Region, rotation: Rotation): 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) + return if (rotation.isRotated()) { + Region.from( + 0, + 0, + dividerRegion.bounds.left + WindowUtils.dockedStackDividerInset, + displayBounds.bounds.bottom + ) } else { - Region.from(0, 0, dividerRegion.bounds.left + WindowUtils.dockedStackDividerInset, - displayBounds.bounds.bottom) + Region.from( + 0, + 0, + displayBounds.bounds.right, + dividerRegion.bounds.top + WindowUtils.dockedStackDividerInset + ) } } -fun getSecondaryRegion(dividerRegion: Region, rotation: Int): Region { +fun getSecondaryRegion(dividerRegion: Region, rotation: Rotation): 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) + return if (rotation.isRotated()) { + Region.from( + dividerRegion.bounds.right - WindowUtils.dockedStackDividerInset, + 0, + displayBounds.bounds.right, + displayBounds.bounds.bottom + ) } else { - Region.from(dividerRegion.bounds.right - WindowUtils.dockedStackDividerInset, 0, - displayBounds.bounds.right, displayBounds.bounds.bottom) + Region.from( + 0, + dividerRegion.bounds.bottom - WindowUtils.dockedStackDividerInset, + 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..983640a70c4b 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 @@ -15,10 +15,21 @@ */ @file:JvmName("CommonConstants") + package com.android.wm.shell.flicker -import com.android.server.wm.traces.common.FlickerComponentName +import android.tools.common.datatypes.component.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 +const val LAUNCHER_UI_PACKAGE_NAME = "com.google.android.apps.nexuslauncher" +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/MultiWindowUtils.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/MultiWindowUtils.kt new file mode 100644 index 000000000000..87b94ff8668b --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/MultiWindowUtils.kt @@ -0,0 +1,61 @@ +/* + * 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 + +import android.app.Instrumentation +import android.content.Context +import android.provider.Settings +import android.util.Log +import com.android.compatibility.common.util.SystemUtil +import java.io.IOException + +object MultiWindowUtils { + private fun executeShellCommand(instrumentation: Instrumentation, cmd: String) { + try { + SystemUtil.runShellCommand(instrumentation, cmd) + } catch (e: IOException) { + Log.e(MultiWindowUtils::class.simpleName, "executeShellCommand error! $e") + } + } + + fun getDevEnableNonResizableMultiWindow(context: Context): Int = + Settings.Global.getInt( + context.contentResolver, + Settings.Global.DEVELOPMENT_ENABLE_NON_RESIZABLE_MULTI_WINDOW + ) + + fun setDevEnableNonResizableMultiWindow(context: Context, configValue: Int) = + Settings.Global.putInt( + context.contentResolver, + Settings.Global.DEVELOPMENT_ENABLE_NON_RESIZABLE_MULTI_WINDOW, + configValue + ) + + fun setSupportsNonResizableMultiWindow(instrumentation: Instrumentation, configValue: Int) = + executeShellCommand( + instrumentation, + createConfigSupportsNonResizableMultiWindowCommand(configValue) + ) + + fun resetMultiWindowConfig(instrumentation: Instrumentation) = + executeShellCommand(instrumentation, resetMultiWindowConfigCommand) + + private fun createConfigSupportsNonResizableMultiWindowCommand(configValue: Int): String = + "wm set-multi-window-config --supportsNonResizable $configValue" + + private const val resetMultiWindowConfigCommand: String = "wm reset-multi-window-config" +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/NotificationListener.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/NotificationListener.kt index 51f7a18f60dd..e0ef92457f58 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/NotificationListener.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/NotificationListener.kt @@ -51,7 +51,7 @@ class NotificationListener : NotificationListenerService() { private const val CMD_NOTIFICATION_ALLOW_LISTENER = "cmd notification allow_listener %s" private const val CMD_NOTIFICATION_DISALLOW_LISTENER = - "cmd notification disallow_listener %s" + "cmd notification disallow_listener %s" private const val COMPONENT_NAME = "com.android.wm.shell.flicker/.NotificationListener" private var instance: NotificationListener? = null @@ -79,25 +79,23 @@ class NotificationListener : NotificationListenerService() { ): StatusBarNotification? { instance?.run { return notifications.values.firstOrNull(predicate) - } ?: throw IllegalStateException("NotificationListenerService is not connected") + } + ?: throw IllegalStateException("NotificationListenerService is not connected") } fun waitForNotificationToAppear( predicate: (StatusBarNotification) -> Boolean ): StatusBarNotification? { instance?.let { - return waitForResult(extractor = { - it.notifications.values.firstOrNull(predicate) - }).second - } ?: throw IllegalStateException("NotificationListenerService is not connected") + return waitForResult(extractor = { it.notifications.values.firstOrNull(predicate) }) + .second + } + ?: throw IllegalStateException("NotificationListenerService is not connected") } - fun waitForNotificationToDisappear( - predicate: (StatusBarNotification) -> Boolean - ): Boolean { - return instance?.let { - wait { it.notifications.values.none(predicate) } - } ?: throw IllegalStateException("NotificationListenerService is not connected") + fun waitForNotificationToDisappear(predicate: (StatusBarNotification) -> Boolean): Boolean { + return instance?.let { wait { it.notifications.values.none(predicate) } } + ?: throw IllegalStateException("NotificationListenerService is not connected") } } -}
\ No newline at end of file +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/WaitUtils.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/WaitUtils.kt index 4d87ec9e872f..556cb06f3ca1 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/WaitUtils.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/WaitUtils.kt @@ -15,6 +15,7 @@ */ @file:JvmName("WaitUtils") + package com.android.wm.shell.flicker import android.os.SystemClock diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/BaseAppCompat.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/BaseAppCompat.kt new file mode 100644 index 000000000000..d01a0ee67f25 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/BaseAppCompat.kt @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.appcompat + +import android.content.Context +import android.system.helpers.CommandsHelper +import android.tools.device.flicker.legacy.FlickerBuilder +import android.tools.device.flicker.legacy.FlickerTest +import com.android.wm.shell.flicker.BaseTest +import com.android.server.wm.flicker.helpers.setRotation +import com.android.server.wm.flicker.helpers.LetterboxAppHelper +import android.tools.device.flicker.legacy.FlickerTestFactory +import android.tools.device.flicker.legacy.IFlickerTestData +import com.android.wm.shell.flicker.appWindowIsVisibleAtEnd +import com.android.wm.shell.flicker.appWindowIsVisibleAtStart +import org.junit.Assume +import org.junit.Before +import org.junit.runners.Parameterized + +abstract class BaseAppCompat(flicker: FlickerTest) : BaseTest(flicker) { + protected val context: Context = instrumentation.context + protected val letterboxApp = LetterboxAppHelper(instrumentation) + lateinit var cmdHelper: CommandsHelper + lateinit var letterboxStyle: HashMap<String, String> + + /** {@inheritDoc} */ + override val transition: FlickerBuilder.() -> Unit + get() = { + setup { + setStartRotation() + letterboxApp.launchViaIntent(wmHelper) + setEndRotation() + } + } + + @Before + fun before() { + cmdHelper = CommandsHelper.getInstance(instrumentation) + Assume.assumeTrue(tapl.isTablet && isIgnoreOrientationRequest()) + } + + private fun mapLetterboxStyle(): HashMap<String, String> { + val res = cmdHelper.executeShellCommand("wm get-letterbox-style") + val lines = res.lines() + val map = HashMap<String, String>() + for (line in lines) { + val keyValuePair = line.split(":") + if (keyValuePair.size == 2) { + val key = keyValuePair[0].trim() + map[key] = keyValuePair[1].trim() + } + } + return map + } + + private fun isIgnoreOrientationRequest(): Boolean { + val res = cmdHelper.executeShellCommand("wm get-ignore-orientation-request") + return res != null && res.contains("true") + } + + fun IFlickerTestData.setStartRotation() = setRotation(flicker.scenario.startRotation) + + fun IFlickerTestData.setEndRotation() = setRotation(flicker.scenario.endRotation) + + /** Checks that app entering letterboxed state have rounded corners */ + fun assertLetterboxAppAtStartHasRoundedCorners() { + assumeLetterboxRoundedCornersEnabled() + flicker.assertLayersStart { this.hasRoundedCorners(letterboxApp) } + } + + fun assertLetterboxAppAtEndHasRoundedCorners() { + assumeLetterboxRoundedCornersEnabled() + flicker.assertLayersEnd { this.hasRoundedCorners(letterboxApp) } + } + + /** Only run on tests with config_letterboxActivityCornersRadius != 0 in devices */ + private fun assumeLetterboxRoundedCornersEnabled() { + if (!::letterboxStyle.isInitialized) { + letterboxStyle = mapLetterboxStyle() + } + Assume.assumeTrue(letterboxStyle.getValue("Corner radius") != "0") + } + + fun assertLetterboxAppVisibleAtStartAndEnd() { + flicker.appWindowIsVisibleAtStart(letterboxApp) + flicker.appWindowIsVisibleAtEnd(letterboxApp) + } + + companion object { + /** + * Creates the test configurations. + * + * See [FlickerTestFactory.rotationTests] for configuring screen orientation and + * navigation modes. + */ + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): Collection<FlickerTest> { + return FlickerTestFactory.rotationTests() + } + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/OpenAppInSizeCompatModeTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/OpenAppInSizeCompatModeTest.kt new file mode 100644 index 000000000000..c57100e44c17 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/OpenAppInSizeCompatModeTest.kt @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.appcompat + +import android.platform.test.annotations.Postsubmit +import androidx.test.filters.RequiresDevice +import android.tools.device.flicker.legacy.FlickerBuilder +import android.tools.device.flicker.legacy.FlickerTest +import android.tools.device.flicker.junit.FlickerParametersRunnerFactory +import android.tools.common.datatypes.component.ComponentNameMatcher +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized + +/** + * Test launching app in size compat mode. + * + * To run this test: `atest WMShellFlickerTests:OpenAppInSizeCompatModeTest` + * + * Actions: + * ``` + * Rotate non resizable portrait only app to opposite orientation to trigger size compat mode + * ``` + * Notes: + * ``` + * Some default assertions (e.g., nav bar, status bar and screen covered) + * are inherited [BaseTest] + * ``` + */ + +@RequiresDevice +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +class OpenAppInSizeCompatModeTest(flicker: FlickerTest) : BaseAppCompat(flicker) { + + /** {@inheritDoc} */ + override val transition: FlickerBuilder.() -> Unit + get() = { + setup { + setStartRotation() + letterboxApp.launchViaIntent(wmHelper) + } + transitions { setEndRotation() } + teardown { letterboxApp.exit(wmHelper) } + } + + /** + * Windows maybe recreated when rotated. Checks that the focus does not change or if it does, + * focus returns to [letterboxApp] + */ + @Postsubmit + @Test + fun letterboxAppFocusedAtEnd() = flicker.assertEventLog { focusChanges(letterboxApp.`package`) } + + @Postsubmit + @Test + fun letterboxedAppHasRoundedCorners() = assertLetterboxAppAtEndHasRoundedCorners() + + /** + * Checks that the [ComponentNameMatcher.ROTATION] layer appears during the transition, doesn't + * flicker, and disappears before the transition is complete + */ + @Postsubmit + @Test + fun rotationLayerAppearsAndVanishes() { + flicker.assertLayers { + this.isVisible(letterboxApp) + .then() + .isVisible(ComponentNameMatcher.ROTATION) + .then() + .isVisible(letterboxApp) + .isInvisible(ComponentNameMatcher.ROTATION) + } + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/RestartAppInSizeCompatModeTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/RestartAppInSizeCompatModeTest.kt new file mode 100644 index 000000000000..f111a8d62d83 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/RestartAppInSizeCompatModeTest.kt @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.appcompat + +import android.platform.test.annotations.Postsubmit +import androidx.test.filters.RequiresDevice +import android.tools.device.flicker.legacy.FlickerBuilder +import android.tools.device.flicker.legacy.FlickerTest +import android.tools.device.flicker.junit.FlickerParametersRunnerFactory +import android.tools.device.helpers.WindowUtils +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized + +/** + * Test restarting app in size compat mode. + * + * To run this test: `atest WMShellFlickerTests:RestartAppInSizeCompatModeTest` + * + * Actions: + * ``` + * Rotate app to opposite orientation to trigger size compat mode + * Press restart button and wait for letterboxed app to resize + * ``` + * Notes: + * ``` + * Some default assertions (e.g., nav bar, status bar and screen covered) + * are inherited [BaseTest] + * ``` + */ + +@RequiresDevice +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +class RestartAppInSizeCompatModeTest(flicker: FlickerTest) : BaseAppCompat(flicker) { + + /** {@inheritDoc} */ + override val transition: FlickerBuilder.() -> Unit + get() = { + super.transition(this) + transitions { letterboxApp.clickRestart(wmHelper) } + teardown { letterboxApp.exit(wmHelper) } + } + + @Postsubmit + @Test + fun appVisibleAtStartAndEnd() = assertLetterboxAppVisibleAtStartAndEnd() + + @Postsubmit + @Test + fun appLayerVisibilityChanges() { + flicker.assertLayers { + this.isVisible(letterboxApp) + .then() + .isInvisible(letterboxApp) + .then() + .isVisible(letterboxApp) + } + } + + @Postsubmit + @Test + fun letterboxedAppHasRoundedCorners() = assertLetterboxAppAtStartHasRoundedCorners() + + /** Checks that the visible region of [letterboxApp] is still within display bounds */ + @Postsubmit + @Test + fun appWindowRemainInsideVisibleBounds() { + val displayBounds = WindowUtils.getDisplayBounds(flicker.scenario.endRotation) + flicker.assertLayersEnd { visibleRegion(letterboxApp).coversAtMost(displayBounds) } + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestCannotPairNonResizeableApps.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestCannotPairNonResizeableApps.kt 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..bab81d79c804 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,38 @@ 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.content.pm.PackageManager import android.os.ServiceManager -import android.view.Surface -import androidx.test.platform.app.InstrumentationRegistry +import android.tools.common.Rotation +import android.tools.device.flicker.legacy.FlickerBuilder +import android.tools.device.flicker.legacy.FlickerTest +import android.tools.device.flicker.legacy.FlickerTestFactory +import android.tools.device.flicker.legacy.IFlickerTestData +import android.tools.device.helpers.SYSTEMUI_PACKAGE 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.helpers.LaunchBubbleHelper +import com.android.server.wm.flicker.helpers.LaunchBubbleHelper +import com.android.wm.shell.flicker.BaseTest import org.junit.runners.Parameterized -/** - * Base configurations for Bubble flicker tests - */ -abstract class BaseBubbleScreen(protected val testSpec: FlickerTestParameter) { +/** Base configurations for Bubble flicker tests */ +abstract class BaseBubbleScreen(flicker: FlickerTest) : BaseTest(flicker) { - protected val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation() protected val context: Context = instrumentation.context protected val testApp = LaunchBubbleHelper(instrumentation) - protected val notifyManager = INotificationManager.Stub.asInterface( - ServiceManager.getService(Context.NOTIFICATION_SERVICE)) - - protected val uid = context.packageManager.getApplicationInfo( - testApp.component.packageName, 0).uid + private val notifyManager = + INotificationManager.Stub.asInterface( + ServiceManager.getService(Context.NOTIFICATION_SERVICE) + ) - protected abstract val transition: FlickerBuilder.() -> Unit + private val uid = + context.packageManager + .getApplicationInfo(testApp.`package`, PackageManager.ApplicationInfoFlags.of(0)) + .uid @JvmOverloads protected open fun buildTransition( @@ -58,46 +56,41 @@ 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) } } - protected fun Flicker.waitAndGetAddBubbleBtn(): UiObject2? = device.wait(Until.findObject( - By.text("Add Bubble")), FIND_OBJECT_TIMEOUT) - 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) - } - } + protected fun IFlickerTestData.waitAndGetAddBubbleBtn(): UiObject2? = + device.wait(Until.findObject(By.text("Add Bubble")), FIND_OBJECT_TIMEOUT) + protected fun IFlickerTestData.waitAndGetCancelAllBtn(): UiObject2? = + device.wait(Until.findObject(By.text("Cancel All Bubble")), FIND_OBJECT_TIMEOUT) companion object { @Parameterized.Parameters(name = "{0}") @JvmStatic - fun getParams(): List<FlickerTestParameter> { - return FlickerTestParameterFactory.getInstance() - .getConfigNonRotationTests(supportedRotations = listOf(Surface.ROTATION_0), - repetitions = 3) + fun getParams(): List<FlickerTest> { + return FlickerTestFactory.nonRotationTests( + supportedRotations = listOf(Rotation.ROTATION_0) + ) } const val FIND_OBJECT_TIMEOUT = 2000L 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/ChangeActiveActivityFromBubbleTest.kt index 8d1e315e2d5e..2474ecf74cf9 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/ChangeActiveActivityFromBubbleTest.kt @@ -17,19 +17,17 @@ package com.android.wm.shell.flicker.bubble import android.os.SystemClock +import android.platform.test.annotations.FlakyTest import android.platform.test.annotations.Presubmit +import android.tools.device.flicker.junit.FlickerParametersRunnerFactory +import android.tools.device.flicker.legacy.FlickerBuilder +import android.tools.device.flicker.legacy.FlickerTest 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 -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.Before -import org.junit.runner.RunWith import org.junit.Test +import org.junit.runner.RunWith import org.junit.runners.Parameterized /** @@ -38,38 +36,42 @@ import org.junit.runners.Parameterized * To run this test: `atest WMShellFlickerTests:MultiBubblesScreen` * * Actions: + * ``` * Switch in different bubble notifications + * ``` */ @RequiresDevice @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) -@Group4 -open class MultiBubblesScreen(testSpec: FlickerTestParameter) : BaseBubbleScreen(testSpec) { - - @Before - open fun before() { - Assume.assumeFalse(isShellTransitionsEnabled) - } - +@FlakyTest(bugId = 217777115) +open class ChangeActiveActivityFromBubbleTest(flicker: FlickerTest) : BaseBubbleScreen(flicker) { + /** {@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) } } @@ -78,8 +80,6 @@ open class MultiBubblesScreen(testSpec: FlickerTestParameter) : BaseBubbleScreen @Presubmit @Test open fun testAppIsAlwaysVisible() { - testSpec.assertLayers { - this.isVisible(testApp.component) - } + flicker.assertLayers { 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/ChangeActiveActivityFromBubbleTestCfArm.kt index ddebb6fed636..bdfdad59c600 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/ChangeActiveActivityFromBubbleTestCfArm.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,27 +16,12 @@ package com.android.wm.shell.flicker.bubble -import androidx.test.filters.FlakyTest -import androidx.test.filters.RequiresDevice -import com.android.server.wm.flicker.FlickerParametersRunnerFactory -import com.android.server.wm.flicker.FlickerTestParameter -import com.android.server.wm.flicker.annotation.Group4 -import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled -import org.junit.Assume -import org.junit.Before +import android.tools.device.flicker.junit.FlickerParametersRunnerFactory +import android.tools.device.flicker.legacy.FlickerTest import org.junit.runner.RunWith import org.junit.runners.Parameterized -@RequiresDevice @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) -@Group4 -@FlakyTest(bugId = 217777115) -class MultiBubblesScreenShellTransit( - testSpec: FlickerTestParameter -) : MultiBubblesScreen(testSpec) { - @Before - override fun before() { - Assume.assumeTrue(isShellTransitionsEnabled) - } -}
\ No newline at end of file +open class ChangeActiveActivityFromBubbleTestCfArm(flicker: FlickerTest) : + ChangeActiveActivityFromBubbleTest(flicker) 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/DragToDismissBubbleScreenTest.kt index b137e92881a5..8474ce0e64e5 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/DragToDismissBubbleScreenTest.kt @@ -19,17 +19,16 @@ package com.android.wm.shell.flicker.bubble import android.content.Context import android.graphics.Point import android.platform.test.annotations.Presubmit +import android.tools.device.flicker.junit.FlickerParametersRunnerFactory +import android.tools.device.flicker.legacy.FlickerBuilder +import android.tools.device.flicker.legacy.FlickerTest import android.util.DisplayMetrics import android.view.WindowManager import androidx.test.filters.RequiresDevice import androidx.test.uiautomator.By import androidx.test.uiautomator.Until -import com.android.server.wm.flicker.FlickerParametersRunnerFactory -import com.android.server.wm.flicker.FlickerTestParameter -import com.android.server.wm.flicker.annotation.Group4 -import com.android.server.wm.flicker.dsl.FlickerBuilder -import org.junit.runner.RunWith import org.junit.Test +import org.junit.runner.RunWith import org.junit.runners.Parameterized /** @@ -38,30 +37,33 @@ import org.junit.runners.Parameterized * To run this test: `atest WMShellFlickerTests:DismissBubbleScreen` * * Actions: + * ``` * Dismiss a bubble notification + * ``` */ @RequiresDevice @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) -@Group4 -open class DismissBubbleScreen(testSpec: FlickerTestParameter) : BaseBubbleScreen(testSpec) { +open class DragToDismissBubbleScreenTest(flicker: FlickerTest) : BaseBubbleScreen(flicker) { 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") } } @@ -69,8 +71,6 @@ open class DismissBubbleScreen(testSpec: FlickerTestParameter) : BaseBubbleScree @Presubmit @Test open fun testAppIsAlwaysVisible() { - testSpec.assertLayers { - this.isVisible(testApp.component) - } + flicker.assertLayers { this.isVisible(testApp) } } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/DragToDismissBubbleScreenTestCfArm.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/DragToDismissBubbleScreenTestCfArm.kt new file mode 100644 index 000000000000..62fa7b4516c7 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/DragToDismissBubbleScreenTestCfArm.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.bubble + +import android.tools.device.flicker.junit.FlickerParametersRunnerFactory +import android.tools.device.flicker.legacy.FlickerTest +import org.junit.runner.RunWith +import org.junit.runners.Parameterized + +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +class DragToDismissBubbleScreenTestCfArm(flicker: FlickerTest) : + DragToDismissBubbleScreenTest(flicker) 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 deleted file mode 100644 index 684e5cad0e67..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/LaunchBubbleFromLockScreen.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.bubble - -import android.platform.test.annotations.Presubmit -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 -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.runners.Parameterized - -/** - * Test launching a new activity from bubble. - * - * To run this test: `atest WMShellFlickerTests:LaunchBubbleFromLockScreen` - * - * Actions: - * Launch an bubble from notification on lock screen - */ -@RequiresDevice -@RunWith(Parameterized::class) -@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) -@Group4 -class LaunchBubbleFromLockScreen(testSpec: FlickerTestParameter) : BaseBubbleScreen(testSpec) { - - 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() - } - } - 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 - val insets = metricInsets.getInsetsIgnoringVisibility( - WindowInsets.Type.statusBars() - or WindowInsets.Type.displayCutout()) - device.swipe(100, insets.top + 100, 100, device.getDisplayHeight() / 2, 4) - device.waitForIdle(2000) - instrumentation.uiAutomation.syncInputTransactions() - - 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) - showBubble?.click() ?: error("Bubble notify not found") - instrumentation.uiAutomation.syncInputTransactions() - val cancelAllBtn = waitAndGetCancelAllBtn() - cancelAllBtn?.click() ?: error("Cancel widget not found") - } - } - - @Presubmit - @Test - fun testAppIsVisibleAtEnd() { - Assume.assumeFalse(isShellTransitionsEnabled) - testSpec.assertLayersEnd { - this.isVisible(testApp.component) - } - } - - @FlakyTest - @Test - fun testAppIsVisibleAtEnd_ShellTransit() { - Assume.assumeTrue(isShellTransitionsEnabled) - testSpec.assertLayersEnd { - this.isVisible(testApp.component) - } - } -} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/OpenActivityFromBubbleOnLocksreenTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/OpenActivityFromBubbleOnLocksreenTest.kt new file mode 100644 index 000000000000..416315e4b06d --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/OpenActivityFromBubbleOnLocksreenTest.kt @@ -0,0 +1,140 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.bubble + +import android.platform.test.annotations.FlakyTest +import android.platform.test.annotations.Postsubmit +import android.tools.device.flicker.junit.FlickerParametersRunnerFactory +import android.tools.device.flicker.legacy.FlickerBuilder +import android.tools.device.flicker.legacy.FlickerTest +import android.view.WindowInsets +import android.view.WindowManager +import androidx.test.filters.RequiresDevice +import androidx.test.uiautomator.By +import androidx.test.uiautomator.Until +import com.android.server.wm.flicker.navBarLayerIsVisibleAtEnd +import com.android.server.wm.flicker.navBarLayerPositionAtEnd +import org.junit.Assume +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized + +/** + * Test launching a new activity from bubble. + * + * To run this test: `atest WMShellFlickerTests:OpenActivityFromBubbleOnLocksreenTest` + * + * Actions: + * ``` + * Launch an bubble from notification on lock screen + * ``` + */ +@RequiresDevice +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +class OpenActivityFromBubbleOnLocksreenTest(flicker: FlickerTest) : BaseBubbleScreen(flicker) { + + /** {@inheritDoc} */ + override val transition: FlickerBuilder.() -> Unit + get() = buildTransition { + setup { + 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) + ?: 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.displayHeight / 2, 4) + device.waitForIdle(2000) + instrumentation.uiAutomation.syncInputTransactions() + + 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 + ) + showBubble?.click() ?: error("Bubble notify not found") + instrumentation.uiAutomation.syncInputTransactions() + val cancelAllBtn = waitAndGetCancelAllBtn() + cancelAllBtn?.click() ?: error("Cancel widget not found") + } + } + + @FlakyTest(bugId = 242088970) + @Test + fun testAppIsVisibleAtEnd() { + flicker.assertLayersEnd { this.isVisible(testApp) } + } + + @Postsubmit + @Test + fun navBarLayerIsVisibleAtEnd() { + Assume.assumeFalse(flicker.scenario.isTablet) + flicker.navBarLayerIsVisibleAtEnd() + } + + @Postsubmit + @Test + fun navBarLayerPositionAtEnd() { + Assume.assumeFalse(flicker.scenario.isTablet) + flicker.navBarLayerPositionAtEnd() + } + + /** {@inheritDoc} */ + @FlakyTest + @Test + override fun visibleLayersShownMoreThanOneConsecutiveEntry() = + super.visibleLayersShownMoreThanOneConsecutiveEntry() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun navBarLayerIsVisibleAtStartAndEnd() { + Assume.assumeTrue(flicker.scenario.isGesturalNavigation) + super.navBarLayerIsVisibleAtStartAndEnd() + } + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun navBarLayerPositionAtStartAndEnd() { + Assume.assumeTrue(flicker.scenario.isGesturalNavigation) + super.navBarLayerPositionAtStartAndEnd() + } + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun navBarWindowIsAlwaysVisible() { + Assume.assumeTrue(flicker.scenario.isGesturalNavigation) + super.navBarWindowIsAlwaysVisible() + } +} 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/OpenActivityFromBubbleTest.kt index f288b0a24d9d..07ba41333071 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/OpenActivityFromBubbleTest.kt @@ -17,15 +17,14 @@ package com.android.wm.shell.flicker.bubble import android.platform.test.annotations.Presubmit +import android.tools.device.flicker.junit.FlickerParametersRunnerFactory +import android.tools.device.flicker.legacy.FlickerBuilder +import android.tools.device.flicker.legacy.FlickerTest import androidx.test.filters.RequiresDevice import androidx.test.uiautomator.By import androidx.test.uiautomator.Until -import com.android.server.wm.flicker.FlickerParametersRunnerFactory -import com.android.server.wm.flicker.FlickerTestParameter -import com.android.server.wm.flicker.annotation.Group4 -import com.android.server.wm.flicker.dsl.FlickerBuilder -import org.junit.runner.RunWith import org.junit.Test +import org.junit.runner.RunWith import org.junit.runners.Parameterized /** @@ -34,27 +33,30 @@ import org.junit.runners.Parameterized * To run this test: `atest WMShellFlickerTests:ExpandBubbleScreen` * * Actions: + * ``` * Launch an app and enable app's bubble notification * Send a bubble notification * The activity for the bubble is launched + * ``` */ @RequiresDevice @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) -@Group4 -open class ExpandBubbleScreen(testSpec: FlickerTestParameter) : BaseBubbleScreen(testSpec) { +open class OpenActivityFromBubbleTest(flicker: FlickerTest) : BaseBubbleScreen(flicker) { + /** {@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") } } @@ -62,8 +64,6 @@ open class ExpandBubbleScreen(testSpec: FlickerTestParameter) : BaseBubbleScreen @Presubmit @Test open fun testAppIsAlwaysVisible() { - testSpec.assertLayers { - this.isVisible(testApp.component) - } + flicker.assertLayers { this.isVisible(testApp) } } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/OpenActivityFromBubbleTestCfArm.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/OpenActivityFromBubbleTestCfArm.kt new file mode 100644 index 000000000000..6c61710d6284 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/OpenActivityFromBubbleTestCfArm.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.bubble + +import android.tools.device.flicker.junit.FlickerParametersRunnerFactory +import android.tools.device.flicker.legacy.FlickerTest +import org.junit.runner.RunWith +import org.junit.runners.Parameterized + +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +class OpenActivityFromBubbleTestCfArm(flicker: FlickerTest) : OpenActivityFromBubbleTest(flicker) 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/SendBubbleNotificationTest.kt index 0bb4d398bff4..29f76d01af83 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/SendBubbleNotificationTest.kt @@ -17,11 +17,12 @@ package com.android.wm.shell.flicker.bubble import android.platform.test.annotations.Presubmit -import android.platform.test.annotations.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.dsl.FlickerBuilder +import android.tools.device.flicker.junit.FlickerParametersRunnerFactory +import android.tools.device.flicker.legacy.FlickerBuilder +import android.tools.device.flicker.legacy.FlickerTest +import androidx.test.filters.RequiresDevice +import androidx.test.uiautomator.By +import androidx.test.uiautomator.Until import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.Parameterized @@ -32,28 +33,34 @@ import org.junit.runners.Parameterized * To run this test: `atest WMShellFlickerTests:LaunchBubbleScreen` * * Actions: + * ``` * Launch an app and enable app's bubble notification * Send a bubble notification + * ``` */ @RequiresDevice @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) -@Group4 -open class LaunchBubbleScreen(testSpec: FlickerTestParameter) : BaseBubbleScreen(testSpec) { +open class SendBubbleNotificationTest(flicker: FlickerTest) : BaseBubbleScreen(flicker) { + /** {@inheritDoc} */ override val transition: FlickerBuilder.() -> Unit get() = buildTransition { transitions { val addBubbleBtn = waitAndGetAddBubbleBtn() addBubbleBtn?.click() ?: error("Bubble widget not found") + + device.wait( + Until.findObjects(By.res(SYSTEM_UI_PACKAGE, BUBBLE_RES_NAME)), + FIND_OBJECT_TIMEOUT + ) + ?: error("No bubbles found") } } @Presubmit @Test open fun testAppIsAlwaysVisible() { - testSpec.assertLayers { - this.isVisible(testApp.component) - } + flicker.assertLayers { this.isVisible(testApp) } } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/SendBubbleNotificationTestCfArm.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/SendBubbleNotificationTestCfArm.kt new file mode 100644 index 000000000000..e323ebf3b5c8 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/SendBubbleNotificationTestCfArm.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.bubble + +import android.tools.device.flicker.junit.FlickerParametersRunnerFactory +import android.tools.device.flicker.legacy.FlickerTest +import org.junit.runner.RunWith +import org.junit.runners.Parameterized + +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +open class SendBubbleNotificationTestCfArm(flicker: FlickerTest) : + SendBubbleNotificationTest(flicker) 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 deleted file mode 100644 index 41cd31aabf05..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/AppPairsHelper.kt +++ /dev/null @@ -1,60 +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.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 - -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 - } - } - } -} 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 deleted file mode 100644 index 3dd9e0572947..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/BaseAppHelper.kt +++ /dev/null @@ -1,74 +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.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 -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 java.io.IOException - -abstract class BaseAppHelper( - instrumentation: Instrumentation, - launcherName: String, - component: FlickerComponentName -) : StandardAppHelper( - instrumentation, - launcherName, - component, - LauncherStrategyFactory.getInstance(instrumentation).launcherStrategy -) { - private val appSelector = By.pkg(component.packageName).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) - - fun waitUntilClosed(): Boolean { - return uiDevice.wait(Until.gone(appSelector), APP_CLOSE_WAIT_TIME_MS) - } - - 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) - } catch (e: IOException) { - Log.e("BaseAppHelper", "executeShellCommand error! $e") - } - } - } -} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/FixedAppHelper.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/FixedAppHelper.kt deleted file mode 100644 index 471e010cf560..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/FixedAppHelper.kt +++ /dev/null @@ -1,27 +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.helpers - -import android.app.Instrumentation -import com.android.server.wm.traces.parser.toFlickerComponent -import com.android.wm.shell.flicker.testapp.Components - -class FixedAppHelper(instrumentation: Instrumentation) : BaseAppHelper( - instrumentation, - Components.FixedActivity.LABEL, - Components.FixedActivity.COMPONENT.toFlickerComponent() -)
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/ImeAppHelper.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/ImeAppHelper.kt deleted file mode 100644 index cc5b9f9eb26d..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/ImeAppHelper.kt +++ /dev/null @@ -1,90 +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.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 -import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper -import com.android.wm.shell.flicker.testapp.Components - -open class ImeAppHelper(instrumentation: Instrumentation) : BaseAppHelper( - instrumentation, - Components.ImeActivity.LABEL, - Components.ImeActivity.COMPONENT.toFlickerComponent() -) { - /** - * Opens the IME and wait for it to be displayed - * - * @param wmHelper Helper used to wait for WindowManager states - */ - @JvmOverloads - open fun openIME(wmHelper: WindowManagerStateHelper? = null) { - if (!isTelevision) { - val editText = uiDevice.wait( - Until.findObject(By.res(getPackage(), "plain_text_input")), - FIND_TIMEOUT) - - require(editText != null) { - "Text field not found, this usually happens when the device " + - "was left in an unknown state (e.g. in split screen)" - } - editText.click() - waitAndAssertIMEShown(uiDevice, wmHelper) - } 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. - launchViaIntent(action = Components.ImeActivity.ACTION_OPEN_IME) - } - } - - 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) { - 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() - } - } 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 deleted file mode 100644 index 6695c17ed514..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/LaunchBubbleHelper.kt +++ /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.flicker.helpers - -import android.app.Instrumentation -import com.android.server.wm.traces.parser.toFlickerComponent -import com.android.wm.shell.flicker.testapp.Components - -class LaunchBubbleHelper(instrumentation: Instrumentation) : BaseAppHelper( - instrumentation, - Components.LaunchBubbleActivity.LABEL, - Components.LaunchBubbleActivity.COMPONENT.toFlickerComponent() -) { - - companion object { - const val TEST_REPETITIONS = 1 - const val TIMEOUT_MS = 3_000L - } -} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/MultiWindowHelper.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/MultiWindowHelper.kt deleted file mode 100644 index 12ccbafce651..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/MultiWindowHelper.kt +++ /dev/null @@ -1,52 +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.helpers - -import android.app.Instrumentation -import android.content.Context -import android.provider.Settings -import com.android.server.wm.traces.common.FlickerComponentName - -class MultiWindowHelper( - instrumentation: Instrumentation, - activityLabel: String, - componentsInfo: FlickerComponentName -) : BaseAppHelper(instrumentation, activityLabel, componentsInfo) { - - companion object { - fun getDevEnableNonResizableMultiWindow(context: Context): Int = - Settings.Global.getInt(context.contentResolver, - Settings.Global.DEVELOPMENT_ENABLE_NON_RESIZABLE_MULTI_WINDOW) - - fun setDevEnableNonResizableMultiWindow(context: Context, configValue: Int) = - Settings.Global.putInt(context.contentResolver, - Settings.Global.DEVELOPMENT_ENABLE_NON_RESIZABLE_MULTI_WINDOW, configValue) - - fun setSupportsNonResizableMultiWindow(instrumentation: Instrumentation, configValue: Int) = - executeShellCommand( - instrumentation, - createConfigSupportsNonResizableMultiWindowCommand(configValue)) - - fun resetMultiWindowConfig(instrumentation: Instrumentation) = - executeShellCommand(instrumentation, resetMultiWindowConfigCommand) - - private fun createConfigSupportsNonResizableMultiWindowCommand(configValue: Int): String = - "wm set-multi-window-config --supportsNonResizable $configValue" - - private const val resetMultiWindowConfigCommand: String = "wm reset-multi-window-config" - } -} 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 deleted file mode 100644 index e9d438a569d5..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/PipAppHelper.kt +++ /dev/null @@ -1,198 +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.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.parser.toFlickerComponent -import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper -import com.android.wm.shell.flicker.pip.tv.closeTvPipWindow -import com.android.wm.shell.flicker.pip.tv.isFocusedOrHasFocusedChild -import com.android.wm.shell.flicker.testapp.Components - -class PipAppHelper(instrumentation: Instrumentation) : BaseAppHelper( - instrumentation, - Components.PipActivity.LABEL, - Components.PipActivity.COMPONENT.toFlickerComponent() -) { - private val mediaSessionManager: MediaSessionManager - get() = context.getSystemService(MediaSessionManager::class.java) - ?: error("Could not get MediaSessionManager") - - private val mediaController: MediaController? - get() = mediaSessionManager.getActiveSessions(null).firstOrNull { - it.packageName == component.packageName - } - - fun clickObject(resId: String) { - val selector = By.res(component.packageName, resId) - val obj = uiDevice.findObject(selector) ?: error("Could not find `$resId` object") - - if (!isTelevision) { - obj.click() - } else { - focusOnObject(selector) || error("Could not focus on `$resId` object") - uiDevice.pressDPadCenter() - } - } - - /** - * Launches the app through an intent instead of interacting with the launcher and waits - * until the app window is in PIP mode - */ - @JvmOverloads - fun launchViaIntentAndWaitForPip( - wmHelper: WindowManagerStateHelper, - expectedWindowName: String = "", - action: String? = null, - stringExtras: Map<String, String> - ) { - launchViaIntentAndWaitShown(wmHelper, expectedWindowName, action, stringExtras, - waitConditions = arrayOf(WindowManagerStateHelper.pipShownCondition)) - } - - /** - * Expand the PIP window back to full screen via intent and wait until the app is visible - */ - fun exitPipToFullScreenViaIntent(wmHelper: WindowManagerStateHelper) = - launchViaIntentAndWaitShown(wmHelper) - - private fun focusOnObject(selector: BySelector): Boolean { - // We expect all the focusable UI elements to be arranged in a way so that it is possible - // to "cycle" over all them by clicking the D-Pad DOWN button, going back up to "the top" - // from "the bottom". - repeat(FOCUS_ATTEMPTS) { - uiDevice.findObject(selector)?.apply { if (isFocusedOrHasFocusedChild) return true } - ?: error("The object we try to focus on is gone.") - - uiDevice.pressDPadDown() - uiDevice.waitForIdle() - } - return false - } - - @JvmOverloads - fun clickEnterPipButton(wmHelper: WindowManagerStateHelper? = null) { - clickObject(ENTER_PIP_BUTTON_ID) - - // Wait on WMHelper or simply wait for 3 seconds - wmHelper?.waitPipShown() ?: SystemClock.sleep(3_000) - // when entering pip, the dismiss button is visible at the start. to ensure the pip - // animation is complete, wait until the pip dismiss button is no longer visible. - // b/176822698: dismiss-only state will be removed in the future - uiDevice.wait(Until.gone(By.res(SYSTEMUI_PACKAGE, "dismiss")), FIND_TIMEOUT) - } - - 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") - - fun pauseMedia() = mediaController?.transportControls?.pause() - ?: error("No active media session found") - - fun stopMedia() = mediaController?.transportControls?.stop() - ?: error("No active media session found") - - @Deprecated("Use PipAppHelper.closePipWindow(wmHelper) instead", - ReplaceWith("closePipWindow(wmHelper)")) - fun closePipWindow() { - if (isTelevision) { - uiDevice.closeTvPipWindow() - } else { - closePipWindow(WindowManagerStateHelper(mInstrumentation)) - } - } - - private fun getWindowRect(wmHelper: WindowManagerStateHelper): Rect { - val windowRegion = wmHelper.getWindowRegion(component) - require(!windowRegion.isEmpty) { - "Unable to find a PIP window in the current state" - } - return windowRegion.bounds - } - - /** - * Taps the pip window and dismisses it by clicking on the X button. - */ - fun closePipWindow(wmHelper: WindowManagerStateHelper) { - if (isTelevision) { - uiDevice.closeTvPipWindow() - } else { - val windowRect = getWindowRect(wmHelper) - uiDevice.click(windowRect.centerX(), windowRect.centerY()) - // search and interact with the dismiss button - val dismissSelector = By.res(SYSTEMUI_PACKAGE, "dismiss") - uiDevice.wait(Until.hasObject(dismissSelector), FIND_TIMEOUT) - val dismissPipObject = uiDevice.findObject(dismissSelector) - ?: error("PIP window dismiss button not found") - val dismissButtonBounds = dismissPipObject.visibleBounds - uiDevice.click(dismissButtonBounds.centerX(), dismissButtonBounds.centerY()) - } - - // Wait for animation to complete. - wmHelper.waitPipGone() - wmHelper.waitForHomeActivityVisible() - } - - /** - * Close the pip window by pressing the expand button - */ - fun expandPipWindowToApp(wmHelper: WindowManagerStateHelper) { - val windowRect = getWindowRect(wmHelper) - uiDevice.click(windowRect.centerX(), windowRect.centerY()) - // search and interact with the expand button - val expandSelector = By.res(SYSTEMUI_PACKAGE, "expand_button") - uiDevice.wait(Until.hasObject(expandSelector), FIND_TIMEOUT) - val expandPipObject = uiDevice.findObject(expandSelector) - ?: error("PIP window expand button not found") - val expandButtonBounds = expandPipObject.visibleBounds - uiDevice.click(expandButtonBounds.centerX(), expandButtonBounds.centerY()) - wmHelper.waitPipGone() - wmHelper.waitForAppTransitionIdle() - } - - /** - * Double click on the PIP window to expand it - */ - fun doubleClickPipWindow(wmHelper: WindowManagerStateHelper) { - val windowRect = getWindowRect(wmHelper) - uiDevice.click(windowRect.centerX(), windowRect.centerY()) - uiDevice.click(windowRect.centerX(), windowRect.centerY()) - wmHelper.waitForAppTransitionIdle() - } - - companion object { - private const val FOCUS_ATTEMPTS = 20 - 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" - } -} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/SimpleAppHelper.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/SimpleAppHelper.kt deleted file mode 100644 index 4d0fbc4a0e38..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/SimpleAppHelper.kt +++ /dev/null @@ -1,27 +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.helpers - -import android.app.Instrumentation -import com.android.server.wm.traces.parser.toFlickerComponent -import com.android.wm.shell.flicker.testapp.Components - -class SimpleAppHelper(instrumentation: Instrumentation) : BaseAppHelper( - instrumentation, - Components.SimpleActivity.LABEL, - Components.SimpleActivity.COMPONENT.toFlickerComponent() -)
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/SplitScreenHelper.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/SplitScreenHelper.kt deleted file mode 100644 index 0ec9b2d869a8..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/SplitScreenHelper.kt +++ /dev/null @@ -1,55 +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.helpers - -import android.app.Instrumentation -import android.content.res.Resources -import com.android.server.wm.traces.common.FlickerComponentName -import com.android.server.wm.traces.parser.toFlickerComponent -import com.android.wm.shell.flicker.testapp.Components - -class SplitScreenHelper( - instrumentation: Instrumentation, - activityLabel: String, - componentsInfo: FlickerComponentName -) : BaseAppHelper(instrumentation, activityLabel, componentsInfo) { - - companion object { - const val TEST_REPETITIONS = 1 - const val TIMEOUT_MS = 3_000L - - // TODO: remove all legacy split screen flicker tests when legacy split screen is fully - // deprecated. - fun isUsingLegacySplit(): Boolean = - Resources.getSystem().getBoolean(com.android.internal.R.bool.config_useLegacySplit) - - fun getPrimary(instrumentation: Instrumentation): SplitScreenHelper = - SplitScreenHelper(instrumentation, - Components.SplitScreenActivity.LABEL, - Components.SplitScreenActivity.COMPONENT.toFlickerComponent()) - - fun getSecondary(instrumentation: Instrumentation): SplitScreenHelper = - SplitScreenHelper(instrumentation, - Components.SplitScreenSecondaryActivity.LABEL, - Components.SplitScreenSecondaryActivity.COMPONENT.toFlickerComponent()) - - fun getNonResizeable(instrumentation: Instrumentation): SplitScreenHelper = - SplitScreenHelper(instrumentation, - Components.NonResizeableActivity.LABEL, - Components.NonResizeableActivity.COMPONENT.toFlickerComponent()) - } -} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenDockActivity.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenDockActivity.kt 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..93ee6992a98f --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/AutoEnterPipOnGoToHomeTest.kt @@ -0,0 +1,109 @@ +/* + * 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.tools.device.flicker.junit.FlickerParametersRunnerFactory +import android.tools.device.flicker.legacy.FlickerBuilder +import android.tools.device.flicker.legacy.FlickerTest +import androidx.test.filters.RequiresDevice +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 + * [android.tools.device.flicker.legacy.runner.TransitionRunner], + * 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) +class AutoEnterPipOnGoToHomeTest(flicker: FlickerTest) : EnterPipViaAppUiButtonTest(flicker) { + /** Defines the transition used to run the test */ + override val transition: FlickerBuilder.() -> Unit + get() = { + setup { + pipApp.launchViaIntent(wmHelper) + pipApp.enableAutoEnterForPipActivity() + } + teardown { + // close gracefully so that onActivityUnpinned() can be called before force exit + pipApp.closePipWindow(wmHelper) + pipApp.exit(wmHelper) + } + transitions { tapl.goHome() } + } + + @FlakyTest(bugId = 256863309) + @Test + override fun pipLayerReduces() { + flicker.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(flicker.scenario.isGesturalNavigation) + flicker.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(flicker.scenario.isGesturalNavigation) + super.focusChanges() + } +} 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/ClosePipBySwipingDownTest.kt index ab07ede5bb32..59918fb7b6a9 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/ClosePipBySwipingDownTest.kt @@ -17,15 +17,11 @@ package com.android.wm.shell.flicker.pip import android.platform.test.annotations.Presubmit -import android.view.Surface -import androidx.test.filters.FlakyTest +import android.tools.common.datatypes.component.ComponentNameMatcher +import android.tools.device.flicker.junit.FlickerParametersRunnerFactory +import android.tools.device.flicker.legacy.FlickerBuilder +import android.tools.device.flicker.legacy.FlickerTest 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 org.junit.FixMethodOrder import org.junit.Test import org.junit.runner.RunWith @@ -38,74 +34,65 @@ import org.junit.runners.Parameterized * To run this test: `atest WMShellFlickerTests:ExitPipWithSwipeDownTest` * * Actions: + * ``` * Launch an app in pip mode [pipApp], * Swipe the pip window to the bottom-center of the screen and wait it disappear + * ``` * * Notes: + * ``` * 1. Some default assertions (e.g., nav bar, status bar and screen covered) * are inherited [PipTransition] * 2. Part of the test setup occurs automatically via - * [com.android.server.wm.flicker.TransitionRunnerWithRules], + * [android.tools.device.flicker.legacy.runner.TransitionRunner], * 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 ExitPipWithSwipeDownTest(testSpec: FlickerTestParameter) : ExitPipTransition(testSpec) { +open class ClosePipBySwipingDownTest(flicker: FlickerTest) : ClosePipTransition(flicker) { override val transition: FlickerBuilder.() -> Unit 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 (flicker.scenario.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, + // it appears above the hot seat but `hotseatBarSize` is not available outside + // the platform + val displayY = (device.displayHeight * 0.9).toInt() - 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 - */ + /** Checks that the focus doesn't change between windows during the transition */ @Presubmit @Test fun focusDoesNotChange() { - testSpec.assertEventLog { - this.focusDoesNotChange() - } - } - - companion object { - /** - * Creates the test configurations. - * - * See [FlickerTestParameterFactory.getConfigNonRotationTests] for configuring - * repetitions, screen orientation and navigation modes. - */ - @Parameterized.Parameters(name = "{0}") - @JvmStatic - fun getParams(): List<FlickerTestParameter> { - return FlickerTestParameterFactory.getInstance() - .getConfigNonRotationTests(supportedRotations = listOf(Surface.ROTATION_0), - repetitions = 3) - } + flicker.assertEventLog { this.focusDoesNotChange() } } -}
\ No newline at end of file +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ClosePipBySwipingDownTestCfArm.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ClosePipBySwipingDownTestCfArm.kt new file mode 100644 index 000000000000..02f60100d069 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ClosePipBySwipingDownTestCfArm.kt @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.pip + +import android.tools.common.Rotation +import android.tools.device.flicker.junit.FlickerParametersRunnerFactory +import android.tools.device.flicker.legacy.FlickerTest +import android.tools.device.flicker.legacy.FlickerTestFactory +import org.junit.FixMethodOrder +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class ClosePipBySwipingDownTestCfArm(flicker: FlickerTest) : ClosePipBySwipingDownTest(flicker) { + companion object { + /** + * Creates the test configurations. + * + * See [FlickerTestFactory.nonRotationTests] for configuring repetitions, screen orientation + * and navigation modes. + */ + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): List<FlickerTest> { + return FlickerTestFactory.nonRotationTests( + supportedRotations = listOf(Rotation.ROTATION_0) + ) + } + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ClosePipTransition.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ClosePipTransition.kt new file mode 100644 index 000000000000..36c6f7c438c4 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ClosePipTransition.kt @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.pip + +import android.platform.test.annotations.Presubmit +import android.tools.common.Rotation +import android.tools.common.datatypes.component.ComponentNameMatcher.Companion.LAUNCHER +import android.tools.device.flicker.legacy.FlickerBuilder +import android.tools.device.flicker.legacy.FlickerTest +import android.tools.device.flicker.legacy.FlickerTestFactory +import com.android.server.wm.flicker.helpers.setRotation +import org.junit.Test +import org.junit.runners.Parameterized + +/** Base class for exiting pip (closing pip window) without returning to the app */ +abstract class ClosePipTransition(flicker: FlickerTest) : PipTransition(flicker) { + override val transition: FlickerBuilder.() -> Unit + get() = buildTransition { + setup { this.setRotation(flicker.scenario.startRotation) } + teardown { this.setRotation(Rotation.ROTATION_0) } + } + + /** + * Checks that [pipApp] window is pinned and visible at the start and then becomes unpinned and + * invisible at the same moment, and remains unpinned and invisible until the end of the + * transition + */ + @Presubmit + @Test + open fun pipWindowBecomesInvisible() { + // When Shell transition is enabled, we change the windowing mode at start, but + // update the visibility after the transition is finished, so we can't check isNotPinned + // and isAppWindowInvisible in the same assertion block. + flicker.assertWm { + this.invoke("hasPipWindow") { + it.isPinned(pipApp).isAppWindowVisible(pipApp).isAppWindowOnTop(pipApp) + } + .then() + .invoke("!hasPipWindow") { it.isNotPinned(pipApp).isAppWindowNotOnTop(pipApp) } + } + flicker.assertWmEnd { isAppWindowInvisible(pipApp) } + } + + /** + * 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 + */ + @Presubmit + @Test + open fun pipLayerBecomesInvisible() { + flicker.assertLayers { + this.isVisible(pipApp) + .isVisible(LAUNCHER) + .then() + .isInvisible(pipApp) + .isVisible(LAUNCHER) + } + } + + companion object { + /** + * Creates the test configurations. + * + * See [FlickerTestFactory.nonRotationTests] for configuring repetitions, screen orientation + * and navigation modes. + */ + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): List<FlickerTest> { + return FlickerTestFactory.nonRotationTests( + supportedRotations = listOf(Rotation.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/ClosePipWithDismissButtonTest.kt index 437ad893f1d9..d16583271e8c 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/ClosePipWithDismissButtonTest.kt @@ -17,14 +17,10 @@ package com.android.wm.shell.flicker.pip import android.platform.test.annotations.Presubmit -import android.view.Surface -import androidx.test.filters.FlakyTest +import android.tools.device.flicker.junit.FlickerParametersRunnerFactory +import android.tools.device.flicker.legacy.FlickerBuilder +import android.tools.device.flicker.legacy.FlickerTest import androidx.test.filters.RequiresDevice -import com.android.server.wm.flicker.FlickerParametersRunnerFactory -import com.android.server.wm.flicker.FlickerTestParameter -import com.android.server.wm.flicker.FlickerTestParameterFactory -import com.android.server.wm.flicker.annotation.Group3 -import com.android.server.wm.flicker.dsl.FlickerBuilder import org.junit.FixMethodOrder import org.junit.Test import org.junit.runner.RunWith @@ -37,63 +33,41 @@ import org.junit.runners.Parameterized * To run this test: `atest WMShellFlickerTests:ExitPipWithDismissButtonTest` * * Actions: + * ``` * Launch an app in pip mode [pipApp], * Click on the pip window * Click on dismiss button and wait window disappear + * ``` * * Notes: + * ``` * 1. Some default assertions (e.g., nav bar, status bar and screen covered) * are inherited [PipTransition] * 2. Part of the test setup occurs automatically via - * [com.android.server.wm.flicker.TransitionRunnerWithRules], + * [android.tools.device.flicker.legacy.runner.TransitionRunner], * including configuring navigation mode, initial orientation and ensuring no * apps are running before setup + * ``` */ @RequiresDevice @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) -@Group3 -class ExitPipWithDismissButtonTest(testSpec: FlickerTestParameter) : ExitPipTransition(testSpec) { +open class ClosePipWithDismissButtonTest(flicker: FlickerTest) : ClosePipTransition(flicker) { override val transition: FlickerBuilder.() -> Unit get() = { super.transition(this) - transitions { - pipApp.closePipWindow(wmHelper) - } + transitions { pipApp.closePipWindow(wmHelper) } } - /** {@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() { - testSpec.assertEventLog { - this.focusChanges("PipMenuView", "NexusLauncherActivity") - } - } - - companion object { - /** - * Creates the test configurations. - * - * See [FlickerTestParameterFactory.getConfigNonRotationTests] for configuring - * repetitions, screen orientation and navigation modes. - */ - @Parameterized.Parameters(name = "{0}") - @JvmStatic - fun getParams(): List<FlickerTestParameter> { - return FlickerTestParameterFactory.getInstance() - .getConfigNonRotationTests(supportedRotations = listOf(Surface.ROTATION_0), - repetitions = 3) - } + fun focusChanges() { + flicker.assertEventLog { this.focusChanges("PipMenuView", "NexusLauncherActivity") } } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ClosePipWithDismissButtonTestCfArm.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ClosePipWithDismissButtonTestCfArm.kt new file mode 100644 index 000000000000..05262feceba5 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ClosePipWithDismissButtonTestCfArm.kt @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.pip + +import android.tools.common.Rotation +import android.tools.device.flicker.junit.FlickerParametersRunnerFactory +import android.tools.device.flicker.legacy.FlickerTest +import android.tools.device.flicker.legacy.FlickerTestFactory +import org.junit.FixMethodOrder +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +open class ClosePipWithDismissButtonTestCfArm(flicker: FlickerTest) : + ClosePipWithDismissButtonTest(flicker) { + companion object { + /** + * Creates the test configurations. + * + * See [FlickerTestFactory.nonRotationTests] for configuring repetitions, screen orientation + * and navigation modes. + */ + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): List<FlickerTest> { + return FlickerTestFactory.nonRotationTests( + supportedRotations = listOf(Rotation.ROTATION_0) + ) + } + } +} 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..f52e877ec2b1 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipOnUserLeaveHintTest.kt @@ -0,0 +1,154 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.pip + +import android.platform.test.annotations.Presubmit +import android.tools.device.flicker.junit.FlickerParametersRunnerFactory +import android.tools.device.flicker.legacy.FlickerBuilder +import android.tools.device.flicker.legacy.FlickerTest +import androidx.test.filters.RequiresDevice +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 + * ``` + */ +@RequiresDevice +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +open class EnterPipOnUserLeaveHintTest(flicker: FlickerTest) : EnterPipTransition(flicker) { + /** Defines the transition used to run the test */ + override val transition: FlickerBuilder.() -> Unit + get() = { + setup { + pipApp.launchViaIntent(wmHelper) + pipApp.enableEnterPipOnUserLeaveHint() + } + teardown { + // close gracefully so that onActivityUnpinned() can be called before force exit + pipApp.closePipWindow(wmHelper) + pipApp.exit(wmHelper) + } + transitions { tapl.goHome() } + } + + @Presubmit + @Test + override fun pipAppWindowAlwaysVisible() { + // In gestural nav the pip will first move behind home and then above home. The visual + // appearance visible->invisible->visible is asserted by pipAppLayerAlwaysVisible(). + // But the internal states of activity don't need to follow that, such as a temporary + // visibility state can be changed quickly outside a transaction so the test doesn't + // detect that. Hence, skip the case to avoid restricting the internal implementation. + Assume.assumeFalse(flicker.scenario.isGesturalNavigation) + super.pipAppWindowAlwaysVisible() + } + + @Presubmit + @Test + override fun pipAppLayerAlwaysVisible() { + // pip layer in gesture nav will disappear during transition + Assume.assumeFalse(flicker.scenario.isGesturalNavigation) + super.pipAppLayerAlwaysVisible() + } + + @Presubmit + @Test + override fun pipOverlayLayerAppearThenDisappear() { + // no overlay in gesture nav for non-auto enter PiP transition + Assume.assumeFalse(flicker.scenario.isGesturalNavigation) + super.pipOverlayLayerAppearThenDisappear() + } + + @Presubmit + @Test + fun pipAppWindowVisibleChanges() { + // pip layer in gesture nav will disappear during transition + Assume.assumeTrue(flicker.scenario.isGesturalNavigation) + flicker.assertWm { + this.isAppWindowVisible(pipApp) + .then() + .isAppWindowInvisible(pipApp, isOptional = true) + .then() + .isAppWindowVisible(pipApp, isOptional = true) + } + } + + @Presubmit + @Test + fun pipAppLayerVisibleChanges() { + Assume.assumeTrue(flicker.scenario.isGesturalNavigation) + // pip layer in gesture nav will disappear during transition + flicker.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(flicker.scenario.isGesturalNavigation) + super.pipLayerReduces() + } + + @Presubmit + @Test + override fun focusChanges() { + // in gestural nav the focus goes to different activity on swipe up + Assume.assumeFalse(flicker.scenario.isGesturalNavigation) + super.focusChanges() + } + + @Presubmit + @Test + override fun entireScreenCovered() { + super.entireScreenCovered() + } + + @Presubmit + @Test + override fun pipLayerOrOverlayRemainInsideVisibleBounds() { + // pip layer in gesture nav will disappear during transition + Assume.assumeFalse(flicker.scenario.isGesturalNavigation) + super.pipLayerOrOverlayRemainInsideVisibleBounds() + } + + @Presubmit + @Test + fun pipLayerRemainInsideVisibleBounds() { + // pip layer in gesture nav will disappear during transition + Assume.assumeTrue(flicker.scenario.isGesturalNavigation) + // pip layer in gesture nav will disappear during transition + flicker.assertLayersStart { this.visibleRegion(pipApp).coversAtMost(displayBounds) } + flicker.assertLayersEnd { this.visibleRegion(pipApp).coversAtMost(displayBounds) } + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipOnUserLeaveHintTestCfArm.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipOnUserLeaveHintTestCfArm.kt new file mode 100644 index 000000000000..90f99c0c4cae --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipOnUserLeaveHintTestCfArm.kt @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.pip + +import android.tools.device.flicker.junit.FlickerParametersRunnerFactory +import android.tools.device.flicker.legacy.FlickerTest +import org.junit.FixMethodOrder +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** This test will fail because of b/264261596 */ +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class EnterPipOnUserLeaveHintTestCfArm(flicker: FlickerTest) : EnterPipOnUserLeaveHintTest(flicker) 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 deleted file mode 100644 index 0640ac526bd0..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipTest.kt +++ /dev/null @@ -1,199 +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.LAUNCHER_COMPONENT -import com.android.server.wm.flicker.annotation.Group3 -import com.android.server.wm.flicker.dsl.FlickerBuilder -import org.junit.FixMethodOrder -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.MethodSorters -import org.junit.runners.Parameterized - -/** - * Test entering pip from an app by interacting with the app UI - * - * To run this test: `atest WMShellFlickerTests:EnterPipTest` - * - * Actions: - * Launch an app in full screen - * Press an "enter pip" button to put [pipApp] in pip mode - * - * Notes: - * 1. Some default assertions (e.g., nav bar, status bar and screen covered) - * are inherited [PipTransition] - * 2. Part of the test setup occurs automatically via - * [com.android.server.wm.flicker.TransitionRunnerWithRules], - * including configuring navigation mode, initial orientation and ensuring no - * apps are running before setup - */ -@RequiresDevice -@RunWith(Parameterized::class) -@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -@Group3 -class EnterPipTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) { - - /** - * Defines the transition used to run the test - */ - override val transition: FlickerBuilder.() -> Unit - get() = { - setupAndTeardown(this) - setup { - eachRun { - pipApp.launchViaIntent(wmHelper) - } - } - teardown { - eachRun { - 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() { - testSpec.assertWm { - this.isAppWindowVisible(pipApp.component) - } - } - - /** - * Checks [pipApp] layer remains visible throughout the animation - */ - @Presubmit - @Test - fun pipAppLayerAlwaysVisible() { - testSpec.assertLayers { - this.isVisible(pipApp.component) - } - } - - /** - * Checks that the pip app window remains inside the display bounds throughout the whole - * animation - */ - @Presubmit - @Test - fun pipWindowRemainInsideVisibleBounds() { - testSpec.assertWmVisibleRegion(pipApp.component) { - coversAtMost(displayBounds) - } - } - - /** - * Checks that the pip app layer remains inside the display bounds throughout the whole - * animation - */ - @Presubmit - @Test - fun pipLayerRemainInsideVisibleBounds() { - testSpec.assertLayersVisibleRegion(pipApp.component) { - coversAtMost(displayBounds) - } - } - - /** - * Checks that the visible region of [pipApp] always reduces during the animation - */ - @Presubmit - @Test - fun pipLayerReduces() { - val layerName = pipApp.component.toLayerName() - testSpec.assertLayers { - val pipLayerList = this.layers { it.name.contains(layerName) && it.isVisible } - pipLayerList.zipWithNext { previous, current -> - current.visibleRegion.coversAtMost(previous.visibleRegion.region) - } - } - } - - /** - * Checks that [pipApp] window becomes pinned - */ - @Presubmit - @Test - fun pipWindowBecomesPinned() { - testSpec.assertWm { - invoke("pipWindowIsNotPinned") { it.isNotPinned(pipApp.component) } - .then() - .invoke("pipWindowIsPinned") { it.isPinned(pipApp.component) } - } - } - - /** - * Checks [LAUNCHER_COMPONENT] layer remains visible throughout the animation - */ - @Presubmit - @Test - fun launcherLayerBecomesVisible() { - testSpec.assertLayers { - isInvisible(LAUNCHER_COMPONENT) - .then() - .isVisible(LAUNCHER_COMPONENT) - } - } - - /** - * Checks that the focus changes between the [pipApp] window and the launcher when - * closing the pip window - */ - @Presubmit - @Test - fun focusChanges() { - testSpec.assertEventLog { - this.focusChanges(pipApp.`package`, "NexusLauncherActivity") - } - } - - companion object { - /** - * Creates the test configurations. - * - * See [FlickerTestParameterFactory.getConfigNonRotationTests] for configuring - * repetitions, screen orientation and navigation modes. - */ - @Parameterized.Parameters(name = "{0}") - @JvmStatic - fun getParams(): List<FlickerTestParameter> { - return FlickerTestParameterFactory.getInstance() - .getConfigNonRotationTests(supportedRotations = listOf(Surface.ROTATION_0), - repetitions = 3) - } - } -} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientation.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientation.kt new file mode 100644 index 000000000000..db18edba9cc4 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientation.kt @@ -0,0 +1,221 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.pip + +import android.app.Activity +import android.platform.test.annotations.FlakyTest +import android.platform.test.annotations.Postsubmit +import android.platform.test.annotations.Presubmit +import android.tools.common.Rotation +import android.tools.common.datatypes.component.ComponentNameMatcher +import android.tools.device.flicker.junit.FlickerParametersRunnerFactory +import android.tools.device.flicker.legacy.FlickerBuilder +import android.tools.device.flicker.legacy.FlickerTest +import android.tools.device.flicker.legacy.FlickerTestFactory +import android.tools.device.helpers.WindowUtils +import androidx.test.filters.RequiresDevice +import com.android.server.wm.flicker.entireScreenCovered +import com.android.server.wm.flicker.helpers.FixedOrientationAppHelper +import com.android.server.wm.flicker.testapp.ActivityOptions.Pip.ACTION_ENTER_PIP +import com.android.server.wm.flicker.testapp.ActivityOptions.PortraitOnlyActivity.EXTRA_FIXED_ORIENTATION +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 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 entering pip while changing orientation (from app in landscape to pip window in portrait) + * + * To run this test: `atest WMShellFlickerTests:EnterPipToOtherOrientationTest` + * + * Actions: + * ``` + * Launch [testApp] on a fixed portrait orientation + * Launch [pipApp] on a fixed landscape orientation + * Broadcast action [ACTION_ENTER_PIP] to enter pip mode + * ``` + * + * Notes: + * ``` + * 1. Some default assertions (e.g., nav bar, status bar and screen covered) + * are inherited [PipTransition] + * 2. Part of the test setup occurs automatically via + * [android.tools.device.flicker.legacy.runner.TransitionRunner], + * 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) +open class EnterPipToOtherOrientation(flicker: FlickerTest) : PipTransition(flicker) { + private val testApp = FixedOrientationAppHelper(instrumentation) + private val startingBounds = WindowUtils.getDisplayBounds(Rotation.ROTATION_90) + private val endingBounds = WindowUtils.getDisplayBounds(Rotation.ROTATION_0) + + /** Defines the transition used to run the test */ + override val transition: FlickerBuilder.() -> Unit + get() = { + setup { + // 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 { + 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) + // during rotation the status bar becomes invisible and reappears at the end + wmHelper + .StateSyncBuilder() + .withPipShown() + .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(tapl.isTablet) + } + + /** + * Checks that all parts of the screen are covered at the start and end of the transition + * + * TODO b/197726599 Prevents all states from being checked + */ + @Presubmit + @Test + fun entireScreenCoveredAtStartAndEnd() = flicker.entireScreenCovered(allStates = false) + + /** Checks [pipApp] window remains visible and on top throughout the transition */ + @Presubmit + @Test + fun pipAppWindowIsAlwaysOnTop() { + flicker.assertWm { isAppWindowOnTop(pipApp) } + } + + /** Checks that [testApp] window is not visible at the start */ + @Presubmit + @Test + fun testAppWindowInvisibleOnStart() { + flicker.assertWmStart { isAppWindowInvisible(testApp) } + } + + /** Checks that [testApp] window is visible at the end */ + @Presubmit + @Test + fun testAppWindowVisibleOnEnd() { + flicker.assertWmEnd { isAppWindowVisible(testApp) } + } + + /** Checks that [testApp] layer is not visible at the start */ + @Presubmit + @Test + fun testAppLayerInvisibleOnStart() { + flicker.assertLayersStart { isInvisible(testApp) } + } + + /** Checks that [testApp] layer is visible at the end */ + @Presubmit + @Test + fun testAppLayerVisibleOnEnd() { + flicker.assertLayersEnd { isVisible(testApp) } + } + + /** + * Checks that the visible region of [pipApp] covers the full display area at the start of the + * transition + */ + @Presubmit + @Test + fun pipAppLayerCoversFullScreenOnStart() { + Assume.assumeFalse(tapl.isTablet) + flicker.assertLayersStart { visibleRegion(pipApp).coversExactly(startingBounds) } + } + + /** + * Checks that the visible region of [pipApp] covers the full display area at the start of the + * transition + */ + @Postsubmit + @Test + fun pipAppLayerPlusLetterboxCoversFullScreenOnStartTablet() { + Assume.assumeFalse(tapl.isTablet) + flicker.assertLayersStart { + visibleRegion(pipApp.or(ComponentNameMatcher.LETTERBOX)).coversExactly(startingBounds) + } + } + + /** + * Checks that the visible region of [testApp] plus the visible region of [pipApp] cover the + * full display area at the end of the transition + */ + @Presubmit + @Test + fun testAppPlusPipLayerCoversFullScreenOnEnd() { + flicker.assertLayersEnd { + val pipRegion = visibleRegion(pipApp).region + visibleRegion(testApp).plus(pipRegion).coversExactly(endingBounds) + } + } + + /** {@inheritDoc} */ + @FlakyTest(bugId = 267424412) + @Test + override fun visibleLayersShownMoreThanOneConsecutiveEntry() = + super.visibleLayersShownMoreThanOneConsecutiveEntry() + + companion object { + /** + * Creates the test configurations. + * + * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and + * navigation modes. + */ + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): Collection<FlickerTest> { + return FlickerTestFactory.nonRotationTests( + supportedRotations = listOf(Rotation.ROTATION_0) + ) + } + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientationCfArm.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientationCfArm.kt new file mode 100644 index 000000000000..58416660826f --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientationCfArm.kt @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.pip + +import android.tools.common.Rotation +import android.tools.device.flicker.junit.FlickerParametersRunnerFactory +import android.tools.device.flicker.legacy.FlickerTest +import android.tools.device.flicker.legacy.FlickerTestFactory +import org.junit.FixMethodOrder +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** This test fails because of b/264261596 */ +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +open class EnterPipToOtherOrientationCfArm(flicker: FlickerTest) : + EnterPipToOtherOrientation(flicker) { + companion object { + /** + * Creates the test configurations. + * + * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and + * navigation modes. + */ + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): Collection<FlickerTest> { + return FlickerTestFactory.nonRotationTests( + supportedRotations = listOf(Rotation.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 deleted file mode 100644 index accb524d3de1..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientationTest.kt +++ /dev/null @@ -1,230 +0,0 @@ -/* - * Copyright (C) 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.wm.shell.flicker.pip - -import android.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.entireScreenCovered -import com.android.server.wm.flicker.helpers.WindowUtils -import com.android.server.wm.flicker.navBarLayerRotatesAndScales -import com.android.server.wm.flicker.statusBarLayerRotatesScales -import com.android.server.wm.traces.common.FlickerComponentName -import com.android.wm.shell.flicker.helpers.FixedAppHelper -import com.android.wm.shell.flicker.pip.PipTransition.BroadcastActionTrigger.Companion.ORIENTATION_LANDSCAPE -import com.android.wm.shell.flicker.pip.PipTransition.BroadcastActionTrigger.Companion.ORIENTATION_PORTRAIT -import com.android.wm.shell.flicker.testapp.Components.PipActivity.ACTION_ENTER_PIP -import com.android.wm.shell.flicker.testapp.Components.FixedActivity.EXTRA_FIXED_ORIENTATION -import 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 while changing orientation (from app in landscape to pip window in portrait) - * - * To run this test: `atest EnterPipToOtherOrientationTest:EnterPipToOtherOrientationTest` - * - * Actions: - * Launch [testApp] on a fixed portrait orientation - * Launch [pipApp] on a fixed landscape orientation - * Broadcast action [ACTION_ENTER_PIP] to enter pip mode - * - * Notes: - * 1. Some default assertions (e.g., nav bar, status bar and screen covered) - * are inherited [PipTransition] - * 2. Part of the test setup occurs automatically via - * [com.android.server.wm.flicker.TransitionRunnerWithRules], - * including configuring navigation mode, initial orientation and ensuring no - * apps are running before setup - */ -@RequiresDevice -@RunWith(Parameterized::class) -@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -@Group3 -class EnterPipToOtherOrientationTest( - testSpec: FlickerTestParameter -) : PipTransition(testSpec) { - private val testApp = FixedAppHelper(instrumentation) - private val startingBounds = WindowUtils.getDisplayBounds(Surface.ROTATION_90) - private val endingBounds = WindowUtils.getDisplayBounds(Surface.ROTATION_0) - - /** - * Defines the transition used to run the test - */ - override val transition: FlickerBuilder.() -> 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())) - } - } - teardown { - eachRun { - 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() - } - } - - /** - * Checks that the [FlickerComponentName.NAV_BAR] has the correct position at - * the start and end of the transition - */ - @FlakyTest - @Test - override fun navBarLayerRotatesAndScales() = testSpec.navBarLayerRotatesAndScales() - - /** - * Checks that the [FlickerComponentName.STATUS_BAR] has the correct position at - * the start and end of the transition - */ - @FlakyTest(bugId = 206753786) - @Test - override fun statusBarLayerRotatesScales() = testSpec.statusBarLayerRotatesScales() - - /** - * Checks that all parts of the screen are covered at the start and end of the transition - * - * TODO b/197726599 Prevents all states from being checked - */ - @Presubmit - @Test - override fun entireScreenCovered() = testSpec.entireScreenCovered(allStates = false) - - /** - * Checks [pipApp] window remains visible and on top throughout the transition - */ - @Presubmit - @Test - fun pipAppWindowIsAlwaysOnTop() { - testSpec.assertWm { - isAppWindowOnTop(pipApp.component) - } - } - - /** - * Checks that [testApp] window is not visible at the start - */ - @Presubmit - @Test - fun testAppWindowInvisibleOnStart() { - testSpec.assertWmStart { - isAppWindowInvisible(testApp.component) - } - } - - /** - * Checks that [testApp] window is visible at the end - */ - @Presubmit - @Test - fun testAppWindowVisibleOnEnd() { - testSpec.assertWmEnd { - isAppWindowVisible(testApp.component) - } - } - - /** - * Checks that [testApp] layer is not visible at the start - */ - @Presubmit - @Test - fun testAppLayerInvisibleOnStart() { - testSpec.assertLayersStart { - isInvisible(testApp.component) - } - } - - /** - * Checks that [testApp] layer is visible at the end - */ - @Presubmit - @Test - fun testAppLayerVisibleOnEnd() { - testSpec.assertLayersEnd { - isVisible(testApp.component) - } - } - - /** - * Checks that the visible region of [pipApp] covers the full display area at the start of - * the transition - */ - @Presubmit - @Test - fun pipAppLayerCoversFullScreenOnStart() { - testSpec.assertLayersStart { - visibleRegion(pipApp.component).coversExactly(startingBounds) - } - } - - /** - * Checks that the visible region of [testApp] plus the visible region of [pipApp] - * cover the full display area at the end of the transition - */ - @Presubmit - @Test - fun testAppPlusPipLayerCoversFullScreenOnEnd() { - testSpec.assertLayersEnd { - val pipRegion = visibleRegion(pipApp.component).region - visibleRegion(testApp.component) - .plus(pipRegion) - .coversExactly(endingBounds) - } - } - - companion object { - /** - * Creates the test configurations. - * - * See [FlickerTestParameterFactory.getConfigNonRotationTests] for configuring - * repetitions, screen orientation and navigation modes. - */ - @Parameterized.Parameters(name = "{0}") - @JvmStatic - fun getParams(): Collection<FlickerTestParameter> { - return FlickerTestParameterFactory.getInstance() - .getConfigNonRotationTests(supportedRotations = listOf(Surface.ROTATION_0), - repetitions = 3) - } - } -} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipTransition.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipTransition.kt new file mode 100644 index 000000000000..51f01364ec9c --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipTransition.kt @@ -0,0 +1,141 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.pip + +import android.platform.test.annotations.Presubmit +import android.tools.common.Rotation +import android.tools.common.datatypes.component.ComponentNameMatcher +import android.tools.device.flicker.legacy.FlickerBuilder +import android.tools.device.flicker.legacy.FlickerTest +import android.tools.device.flicker.legacy.FlickerTestFactory +import org.junit.Test +import org.junit.runners.Parameterized + +abstract class EnterPipTransition(flicker: FlickerTest) : PipTransition(flicker) { + /** {@inheritDoc} */ + override val transition: FlickerBuilder.() -> Unit + get() = { + setup { pipApp.launchViaIntent(wmHelper) } + teardown { pipApp.exit(wmHelper) } + } + + /** Checks [pipApp] window remains visible throughout the animation */ + @Presubmit + @Test + open fun pipAppWindowAlwaysVisible() { + flicker.assertWm { this.isAppWindowVisible(pipApp) } + } + + /** Checks [pipApp] layer remains visible throughout the animation */ + @Presubmit + @Test + open fun pipAppLayerAlwaysVisible() { + flicker.assertLayers { this.isVisible(pipApp) } + } + + /** Checks the content overlay appears then disappears during the animation */ + @Presubmit + @Test + open fun pipOverlayLayerAppearThenDisappear() { + val overlay = ComponentNameMatcher.PIP_CONTENT_OVERLAY + flicker.assertLayers { + this.notContains(overlay).then().contains(overlay).then().notContains(overlay) + } + } + + /** + * Checks that the pip app window remains inside the display bounds throughout the whole + * animation + */ + @Presubmit + @Test + fun pipWindowRemainInsideVisibleBounds() { + flicker.assertWmVisibleRegion(pipApp) { coversAtMost(displayBounds) } + } + + /** + * Checks that the pip app layer remains inside the display bounds throughout the whole + * animation + */ + @Presubmit + @Test + open fun pipLayerOrOverlayRemainInsideVisibleBounds() { + flicker.assertLayersVisibleRegion(pipApp.or(ComponentNameMatcher.PIP_CONTENT_OVERLAY)) { + coversAtMost(displayBounds) + } + } + + /** Checks that the visible region of [pipApp] always reduces during the animation */ + @Presubmit + @Test + open fun pipLayerReduces() { + flicker.assertLayers { + val pipLayerList = this.layers { pipApp.layerMatchesAnyOf(it) && it.isVisible } + pipLayerList.zipWithNext { previous, current -> + current.visibleRegion.notBiggerThan(previous.visibleRegion.region) + } + } + } + + /** Checks that [pipApp] window becomes pinned */ + @Presubmit + @Test + fun pipWindowBecomesPinned() { + flicker.assertWm { + invoke("pipWindowIsNotPinned") { it.isNotPinned(pipApp) } + .then() + .invoke("pipWindowIsPinned") { it.isPinned(pipApp) } + } + } + + /** Checks [ComponentNameMatcher.LAUNCHER] layer remains visible throughout the animation */ + @Presubmit + @Test + fun launcherLayerBecomesVisible() { + flicker.assertLayers { + isInvisible(ComponentNameMatcher.LAUNCHER) + .then() + .isVisible(ComponentNameMatcher.LAUNCHER) + } + } + + /** + * Checks that the focus changes between the [pipApp] window and the launcher when closing the + * pip window + */ + @Presubmit + @Test + open fun focusChanges() { + flicker.assertEventLog { this.focusChanges(pipApp.`package`, "NexusLauncherActivity") } + } + + companion object { + /** + * Creates the test configurations. + * + * See [FlickerTestFactory.nonRotationTests] for configuring repetitions, screen orientation + * and navigation modes. + */ + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): List<FlickerTest> { + return FlickerTestFactory.nonRotationTests( + supportedRotations = listOf(Rotation.ROTATION_0) + ) + } + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipViaAppUiButtonTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipViaAppUiButtonTest.kt new file mode 100644 index 000000000000..f1925d8c9d85 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipViaAppUiButtonTest.kt @@ -0,0 +1,61 @@ +/* + * 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.tools.device.flicker.junit.FlickerParametersRunnerFactory +import android.tools.device.flicker.legacy.FlickerBuilder +import android.tools.device.flicker.legacy.FlickerTest +import androidx.test.filters.RequiresDevice +import org.junit.FixMethodOrder +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** + * Test entering pip from an app by interacting with the app UI + * + * To run this test: `atest WMShellFlickerTests:EnterPipTest` + * + * Actions: + * ``` + * Launch an app in full screen + * Press an "enter pip" button to put [pipApp] in pip mode + * ``` + * + * Notes: + * ``` + * 1. Some default assertions (e.g., nav bar, status bar and screen covered) + * are inherited from [PipTransition] + * 2. Part of the test setup occurs automatically via + * [android.tools.device.flicker.legacy.runner.TransitionRunner], + * 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) +open class EnterPipViaAppUiButtonTest(flicker: FlickerTest) : EnterPipTransition(flicker) { + + /** {@inheritDoc} */ + override val transition: FlickerBuilder.() -> Unit + get() = { + super.transition(this) + transitions { pipApp.clickEnterPipButton(wmHelper) } + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipViaAppUiButtonTestCfArm.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipViaAppUiButtonTestCfArm.kt new file mode 100644 index 000000000000..4390f0bb70b2 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipViaAppUiButtonTestCfArm.kt @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.pip + +import android.tools.common.Rotation +import android.tools.device.flicker.junit.FlickerParametersRunnerFactory +import android.tools.device.flicker.legacy.FlickerTest +import android.tools.device.flicker.legacy.FlickerTestFactory +import org.junit.FixMethodOrder +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class EnterPipViaAppUiButtonTestCfArm(flicker: FlickerTest) : EnterPipViaAppUiButtonTest(flicker) { + companion object { + /** + * Creates the test configurations. + * + * See [FlickerTestFactory.nonRotationTests] for configuring repetitions, screen orientation + * and navigation modes. + */ + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): List<FlickerTest> { + return FlickerTestFactory.nonRotationTests( + supportedRotations = listOf(Rotation.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..2001f484ed96 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 @@ -17,15 +17,17 @@ package com.android.wm.shell.flicker.pip import android.platform.test.annotations.Presubmit -import com.android.server.wm.flicker.FlickerTestParameter -import com.android.wm.shell.flicker.helpers.FixedAppHelper +import android.tools.common.Rotation +import android.tools.common.datatypes.component.ComponentNameMatcher +import android.tools.device.flicker.legacy.FlickerTest +import android.tools.device.flicker.legacy.FlickerTestFactory +import com.android.server.wm.flicker.helpers.SimpleAppHelper import org.junit.Test +import org.junit.runners.Parameterized -/** - * Base class for pip expand tests - */ -abstract class ExitPipToAppTransition(testSpec: FlickerTestParameter) : PipTransition(testSpec) { - protected val testApp = FixedAppHelper(instrumentation) +/** Base class for pip expand tests */ +abstract class ExitPipToAppTransition(flicker: FlickerTest) : PipTransition(flicker) { + protected val testApp = SimpleAppHelper(instrumentation) /** * Checks that the pip app window remains inside the display bounds throughout the whole @@ -34,7 +36,7 @@ abstract class ExitPipToAppTransition(testSpec: FlickerTestParameter) : PipTrans @Presubmit @Test open fun pipAppWindowRemainInsideVisibleBounds() { - testSpec.assertWmVisibleRegion(pipApp.component) { + flicker.assertWmVisibleRegion(pipApp.or(ComponentNameMatcher.TRANSITION_SNAPSHOT)) { coversAtMost(displayBounds) } } @@ -46,7 +48,7 @@ abstract class ExitPipToAppTransition(testSpec: FlickerTestParameter) : PipTrans @Presubmit @Test open fun pipAppLayerRemainInsideVisibleBounds() { - testSpec.assertLayersVisibleRegion(pipApp.component) { + flicker.assertLayersVisibleRegion(pipApp.or(ComponentNameMatcher.TRANSITION_SNAPSHOT)) { coversAtMost(displayBounds) } } @@ -58,15 +60,15 @@ abstract class ExitPipToAppTransition(testSpec: FlickerTestParameter) : PipTrans @Presubmit @Test open fun showBothAppWindowsThenHidePip() { - testSpec.assertWm { + flicker.assertWm { // when the activity is STOPPING, sometimes it becomes invisible in an entry before // the window, sometimes in the same entry. This occurs because we log 1x per frame // thus we ignore activity here - isAppWindowVisible(testApp.component) - .isAppWindowOnTop(pipApp.component) - .then() - .isAppWindowInvisible(testApp.component) - .isAppWindowVisible(pipApp.component) + isAppWindowVisible(testApp) + .isAppWindowOnTop(pipApp) + .then() + .isAppWindowInvisible(testApp) + .isAppWindowVisible(pipApp) } } @@ -77,54 +79,66 @@ abstract class ExitPipToAppTransition(testSpec: FlickerTestParameter) : PipTrans @Presubmit @Test open fun showBothAppLayersThenHidePip() { - testSpec.assertLayers { - isVisible(testApp.component) - .isVisible(pipApp.component) - .then() - .isInvisible(testApp.component) - .isVisible(pipApp.component) + flicker.assertLayers { + isVisible(testApp) + .isVisible(pipApp.or(ComponentNameMatcher.TRANSITION_SNAPSHOT)) + .then() + .isInvisible(testApp) + .isVisible(pipApp) } } /** - * Checks that the visible region of [testApp] plus the visible region of [pipApp] - * cover the full display area at the start of the transition + * Checks that the visible region of [testApp] plus the visible region of [pipApp] cover the + * full display area at the start of the transition */ @Presubmit @Test open fun testPlusPipAppsCoverFullScreenAtStart() { - testSpec.assertLayersStart { - val pipRegion = visibleRegion(pipApp.component).region - visibleRegion(testApp.component) - .plus(pipRegion) - .coversExactly(displayBounds) + flicker.assertLayersStart { + val pipRegion = visibleRegion(pipApp).region + visibleRegion(testApp).plus(pipRegion).coversExactly(displayBounds) } } /** - * Checks that the visible region oft [pipApp] covers the full display area at the end of - * the transition + * Checks that the visible region oft [pipApp] covers the full display area at the end of the + * transition */ @Presubmit @Test open fun pipAppCoversFullScreenAtEnd() { - testSpec.assertLayersEnd { - visibleRegion(pipApp.component).coversExactly(displayBounds) - } + flicker.assertLayersEnd { visibleRegion(pipApp).coversExactly(displayBounds) } } - /** - * Checks that the visible region of [pipApp] always expands during the animation - */ + /** Checks that the visible region of [pipApp] always expands during the animation */ @Presubmit @Test open fun pipLayerExpands() { - val layerName = pipApp.component.toLayerName() - testSpec.assertLayers { - val pipLayerList = this.layers { it.name.contains(layerName) && it.isVisible } + flicker.assertLayers { + 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() + + companion object { + /** + * Creates the test configurations. + * + * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and + * navigation modes. + */ + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): List<FlickerTest> { + return FlickerTestFactory.nonRotationTests( + supportedRotations = listOf(Rotation.ROTATION_0) + ) + } + } } 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/ExitPipToAppViaExpandButtonTest.kt index a3ed79bf0409..3e0e37dfc997 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/ExitPipToAppViaExpandButtonTest.kt @@ -16,16 +16,11 @@ package com.android.wm.shell.flicker.pip -import android.view.Surface -import androidx.test.filters.FlakyTest +import android.tools.device.flicker.junit.FlickerParametersRunnerFactory +import android.tools.device.flicker.legacy.FlickerBuilder +import android.tools.device.flicker.legacy.FlickerTest import androidx.test.filters.RequiresDevice -import com.android.server.wm.flicker.FlickerParametersRunnerFactory -import com.android.server.wm.flicker.FlickerTestParameter -import com.android.server.wm.flicker.FlickerTestParameterFactory -import com.android.server.wm.flicker.annotation.Group3 -import com.android.server.wm.flicker.dsl.FlickerBuilder import org.junit.FixMethodOrder -import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.MethodSorters import org.junit.runners.Parameterized @@ -36,70 +31,41 @@ import org.junit.runners.Parameterized * To run this test: `atest WMShellFlickerTests:ExitPipViaExpandButtonClickTest` * * Actions: + * ``` * Launch an app in pip mode [pipApp], * Launch another full screen mode [testApp] * Expand [pipApp] app to full screen by clicking on the pip window and * then on the expand button + * ``` * * Notes: + * ``` * 1. Some default assertions (e.g., nav bar, status bar and screen covered) * are inherited [PipTransition] * 2. Part of the test setup occurs automatically via - * [com.android.server.wm.flicker.TransitionRunnerWithRules], + * [android.tools.device.flicker.legacy.runner.TransitionRunner], * 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 -@FlakyTest(bugId = 219750830) -class ExitPipViaExpandButtonClickTest( - testSpec: FlickerTestParameter -) : ExitPipToAppTransition(testSpec) { +open class ExitPipToAppViaExpandButtonTest(flicker: FlickerTest) : ExitPipToAppTransition(flicker) { - /** - * Defines the transition used to run the test - */ + /** 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) - @Test - override fun statusBarLayerRotatesScales() = super.statusBarLayerRotatesScales() - - /** {@inheritDoc} */ - @FlakyTest(bugId = 197726610) - @Test - override fun pipLayerExpands() = super.pipLayerExpands() - - companion object { - /** - * Creates the test configurations. - * - * See [FlickerTestParameterFactory.getConfigNonRotationTests] for configuring - * repetitions, screen orientation and navigation modes. - */ - @Parameterized.Parameters(name = "{0}") - @JvmStatic - fun getParams(): List<FlickerTestParameter> { - return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests( - supportedRotations = listOf(Surface.ROTATION_0), repetitions = 3) - } - } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipToAppViaExpandButtonTestCfArm.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipToAppViaExpandButtonTestCfArm.kt new file mode 100644 index 000000000000..eccb85d98798 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipToAppViaExpandButtonTestCfArm.kt @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.pip + +import android.tools.common.Rotation +import android.tools.device.flicker.junit.FlickerParametersRunnerFactory +import android.tools.device.flicker.legacy.FlickerTest +import android.tools.device.flicker.legacy.FlickerTestFactory +import org.junit.FixMethodOrder +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class ExitPipToAppViaExpandButtonTestCfArm(flicker: FlickerTest) : + ExitPipToAppViaExpandButtonTest(flicker) { + companion object { + /** + * Creates the test configurations. + * + * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and + * navigation modes. + */ + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): List<FlickerTest> { + return FlickerTestFactory.nonRotationTests( + supportedRotations = listOf(Rotation.ROTATION_0) + ) + } + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipToAppViaIntentTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipToAppViaIntentTest.kt new file mode 100644 index 000000000000..603f99541a12 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipToAppViaIntentTest.kt @@ -0,0 +1,70 @@ +/* + * 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.tools.device.flicker.junit.FlickerParametersRunnerFactory +import android.tools.device.flicker.legacy.FlickerBuilder +import android.tools.device.flicker.legacy.FlickerTest +import androidx.test.filters.RequiresDevice +import org.junit.FixMethodOrder +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** + * Test expanding a pip window back to full screen via an intent + * + * To run this test: `atest WMShellFlickerTests:ExitPipViaIntentTest` + * + * Actions: + * ``` + * Launch an app in pip mode [pipApp], + * Launch another full screen mode [testApp] + * Expand [pipApp] app to full screen via an intent + * ``` + * + * Notes: + * ``` + * 1. Some default assertions (e.g., nav bar, status bar and screen covered) + * are inherited from [PipTransition] + * 2. Part of the test setup occurs automatically via + * [android.tools.device.flicker.legacy.runner.TransitionRunner], + * 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) +open class ExitPipToAppViaIntentTest(flicker: FlickerTest) : ExitPipToAppTransition(flicker) { + + /** Defines the transition used to run the test */ + override val transition: FlickerBuilder.() -> Unit + get() = buildTransition { + setup { + // 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.StateSyncBuilder().withWindowSurfaceDisappeared(testApp).waitForAndVerify() + } + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipToAppViaIntentTestCfArm.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipToAppViaIntentTestCfArm.kt new file mode 100644 index 000000000000..6ab6a1f0bb73 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipToAppViaIntentTestCfArm.kt @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.pip + +import android.tools.common.Rotation +import android.tools.device.flicker.junit.FlickerParametersRunnerFactory +import android.tools.device.flicker.legacy.FlickerTest +import android.tools.device.flicker.legacy.FlickerTestFactory +import org.junit.FixMethodOrder +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class ExitPipToAppViaIntentTestCfArm(flicker: FlickerTest) : ExitPipToAppViaIntentTest(flicker) { + companion object { + /** + * Creates the test configurations. + * + * See [FlickerTestFactory.nonRotationTests] for configuring repetitions, screen orientation + * and navigation modes. + */ + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): List<FlickerTest> { + return FlickerTestFactory.nonRotationTests( + supportedRotations = listOf(Rotation.ROTATION_0) + ) + } + } +} 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 deleted file mode 100644 index 0b4bc761838d..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipTransition.kt +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright (C) 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.wm.shell.flicker.pip - -import android.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 org.junit.Test - -/** - * Base class for exiting pip (closing pip window) without returning to the app - */ -abstract class ExitPipTransition(testSpec: FlickerTestParameter) : PipTransition(testSpec) { - override val transition: FlickerBuilder.() -> Unit - get() = buildTransition(eachRun = true) { - setup { - eachRun { - this.setRotation(testSpec.startRotation) - } - } - teardown { - eachRun { - this.setRotation(Surface.ROTATION_0) - } - } - } - - /** - * Checks that [pipApp] window is pinned and visible at the start and then becomes - * unpinned and invisible at the same moment, and remains unpinned and invisible - * until the end of the transition - */ - @Presubmit - @Test - open fun pipWindowBecomesInvisible() { - if (isShellTransitionsEnabled) { - // When Shell transition is enabled, we change the windowing mode at start, but - // update the visibility after the transition is finished, so we can't check isNotPinned - // and isAppWindowInvisible in the same assertion block. - testSpec.assertWm { - this.invoke("hasPipWindow") { - it.isPinned(pipApp.component) - .isAppWindowVisible(pipApp.component) - .isAppWindowOnTop(pipApp.component) - }.then().invoke("!hasPipWindow") { - it.isNotPinned(pipApp.component) - .isAppWindowNotOnTop(pipApp.component) - } - } - testSpec.assertWmEnd { isAppWindowInvisible(pipApp.component) } - } else { - testSpec.assertWm { - this.invoke("hasPipWindow") { - it.isPinned(pipApp.component).isAppWindowVisible(pipApp.component) - }.then().invoke("!hasPipWindow") { - it.isNotPinned(pipApp.component).isAppWindowInvisible(pipApp.component) - } - } - } - } - - /** - * Checks that [pipApp] and [LAUNCHER_COMPONENT] layers are visible at the start - * of the transition. Then [pipApp] layer becomes invisible, and remains invisible - * until the end of the transition - */ - @Presubmit - @Test - open fun pipLayerBecomesInvisible() { - testSpec.assertLayers { - this.isVisible(pipApp.component) - .isVisible(LAUNCHER_COMPONENT) - .then() - .isInvisible(pipApp.component) - .isVisible(LAUNCHER_COMPONENT) - } - } -} 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 deleted file mode 100644 index 37e9344348d9..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaIntentTest.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.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.helpers.isShellTransitionsEnabled -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 expanding a pip window back to full screen via an intent - * - * To run this test: `atest WMShellFlickerTests:ExitPipViaIntentTest` - * - * Actions: - * Launch an app in pip mode [pipApp], - * Launch another full screen mode [testApp] - * Expand [pipApp] app to full screen via an intent - * - * Notes: - * 1. Some default assertions (e.g., nav bar, status bar and screen covered) - * are inherited from [PipTransition] - * 2. Part of the test setup occurs automatically via - * [com.android.server.wm.flicker.TransitionRunnerWithRules], - * including configuring navigation mode, initial orientation and ensuring no - * apps are running before setup - */ -@RequiresDevice -@RunWith(Parameterized::class) -@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -@Group3 -class ExitPipViaIntentTest(testSpec: FlickerTestParameter) : ExitPipToAppTransition(testSpec) { - - /** - * Defines the transition used to run the test - */ - override val transition: FlickerBuilder.() -> Unit - get() = buildTransition(eachRun = true) { - setup { - eachRun { - // launch an app behind the pip one - testApp.launchViaIntent(wmHelper) - } - } - transitions { - // This will bring PipApp to fullscreen - pipApp.exitPipToFullScreenViaIntent(wmHelper) - // Wait until the other app is no longer visible - wmHelper.waitForWindowSurfaceDisappeared(testApp.component) - } - } - - /** {@inheritDoc} */ - @FlakyTest(bugId = 206753786) - @Test - override fun statusBarLayerRotatesScales() = super.statusBarLayerRotatesScales() - - /** {@inheritDoc} */ - @FlakyTest(bugId = 197726610) - @Test - override fun pipLayerExpands() { - Assume.assumeFalse(isShellTransitionsEnabled) - super.pipLayerExpands() - } - - @Presubmit - @Test - fun pipLayerExpands_ShellTransit() { - Assume.assumeTrue(isShellTransitionsEnabled) - super.pipLayerExpands() - } - - companion object { - /** - * Creates the test configurations. - * - * See [FlickerTestParameterFactory.getConfigNonRotationTests] for configuring - * repetitions, screen orientation and navigation modes. - */ - @Parameterized.Parameters(name = "{0}") - @JvmStatic - fun getParams(): List<FlickerTestParameter> { - return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests( - supportedRotations = listOf(Surface.ROTATION_0), repetitions = 3) - } - } -} 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..6deba1b68f38 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,14 @@ package com.android.wm.shell.flicker.pip -import androidx.test.filters.FlakyTest import android.platform.test.annotations.Presubmit -import android.view.Surface +import android.tools.common.Rotation +import android.tools.common.datatypes.component.ComponentNameMatcher +import android.tools.device.flicker.junit.FlickerParametersRunnerFactory +import android.tools.device.flicker.legacy.FlickerBuilder +import android.tools.device.flicker.legacy.FlickerTest +import android.tools.device.flicker.legacy.FlickerTestFactory import androidx.test.filters.RequiresDevice -import com.android.server.wm.flicker.FlickerParametersRunnerFactory -import com.android.server.wm.flicker.FlickerTestParameter -import com.android.server.wm.flicker.FlickerTestParameterFactory -import com.android.server.wm.flicker.LAUNCHER_COMPONENT -import com.android.server.wm.flicker.annotation.Group3 -import com.android.server.wm.flicker.dsl.FlickerBuilder import org.junit.FixMethodOrder import org.junit.Test import org.junit.runner.RunWith @@ -33,34 +31,33 @@ import org.junit.runners.MethodSorters import org.junit.runners.Parameterized /** - * Test expanding a pip window by double clicking it + * Test expanding a pip window by double-clicking it * * To run this test: `atest WMShellFlickerTests:ExpandPipOnDoubleClickTest` * * Actions: + * ``` * Launch an app in pip mode [pipApp], * Expand [pipApp] app to its maximum pip size by double clicking on it + * ``` * * Notes: + * ``` * 1. Some default assertions (e.g., nav bar, status bar and screen covered) * are inherited [PipTransition] * 2. Part of the test setup occurs automatically via - * [com.android.server.wm.flicker.TransitionRunnerWithRules], + * [android.tools.device.flicker.legacy.runner.TransitionRunner], * including configuring navigation mode, initial orientation and ensuring no * apps are running before setup + * ``` */ @RequiresDevice @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) -@Group3 -class ExpandPipOnDoubleClickTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) { +open class ExpandPipOnDoubleClickTest(flicker: FlickerTest) : PipTransition(flicker) { override val transition: FlickerBuilder.() -> Unit - get() = buildTransition(eachRun = true) { - transitions { - pipApp.doubleClickPipWindow(wmHelper) - } - } + get() = buildTransition { transitions { pipApp.doubleClickPipWindow(wmHelper) } } /** * Checks that the pip app window remains inside the display bounds throughout the whole @@ -69,9 +66,7 @@ class ExpandPipOnDoubleClickTest(testSpec: FlickerTestParameter) : PipTransition @Presubmit @Test fun pipWindowRemainInsideVisibleBounds() { - testSpec.assertWmVisibleRegion(pipApp.component) { - coversAtMost(displayBounds) - } + flicker.assertWmVisibleRegion(pipApp) { coversAtMost(displayBounds) } } /** @@ -81,42 +76,29 @@ class ExpandPipOnDoubleClickTest(testSpec: FlickerTestParameter) : PipTransition @Presubmit @Test fun pipLayerRemainInsideVisibleBounds() { - testSpec.assertLayersVisibleRegion(pipApp.component) { - coversAtMost(displayBounds) - } + flicker.assertLayersVisibleRegion(pipApp) { coversAtMost(displayBounds) } } - /** - * Checks [pipApp] window remains visible throughout the animation - */ + /** Checks [pipApp] window remains visible throughout the animation */ @Presubmit @Test fun pipWindowIsAlwaysVisible() { - testSpec.assertWm { - isAppWindowVisible(pipApp.component) - } + flicker.assertWm { isAppWindowVisible(pipApp) } } - /** - * Checks [pipApp] layer remains visible throughout the animation - */ + /** Checks [pipApp] layer remains visible throughout the animation */ @Presubmit @Test fun pipLayerIsAlwaysVisible() { - testSpec.assertLayers { - isVisible(pipApp.component) - } + flicker.assertLayers { isVisible(pipApp) } } - /** - * Checks that the visible region of [pipApp] always expands during the animation - */ - @FlakyTest(bugId = 228012337) + /** Checks that the visible region of [pipApp] always expands during the animation */ + @Presubmit @Test fun pipLayerExpands() { - val layerName = pipApp.component.toLayerName() - testSpec.assertLayers { - val pipLayerList = this.layers { it.name.contains(layerName) && it.isVisible } + flicker.assertLayers { + val pipLayerList = this.layers { pipApp.layerMatchesAnyOf(it) && it.isVisible } pipLayerList.zipWithNext { previous, current -> current.visibleRegion.coversAtLeast(previous.visibleRegion.region) } @@ -126,65 +108,48 @@ 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 } + flicker.assertLayers { + val pipLayerList = this.layers { pipApp.layerMatchesAnyOf(it) && it.isVisible } pipLayerList.zipWithNext { previous, current -> current.visibleRegion.isSameAspectRatio(previous.visibleRegion) } } } - /** - * Checks [pipApp] window remains pinned throughout the animation - */ + /** Checks [pipApp] window remains pinned throughout the animation */ @Presubmit @Test fun windowIsAlwaysPinned() { - testSpec.assertWm { - this.invoke("hasPipWindow") { it.isPinned(pipApp.component) } - } + flicker.assertWm { this.invoke("hasPipWindow") { it.isPinned(pipApp) } } } - /** - * Checks [pipApp] layer remains visible throughout the animation - */ + /** Checks [ComponentNameMatcher.LAUNCHER] layer remains visible throughout the animation */ @Presubmit @Test fun launcherIsAlwaysVisible() { - testSpec.assertLayers { - isVisible(LAUNCHER_COMPONENT) - } + flicker.assertLayers { isVisible(ComponentNameMatcher.LAUNCHER) } } - /** - * Checks that the focus doesn't change between windows during the transition - */ - @FlakyTest(bugId = 216306753) + /** Checks that the focus doesn't change between windows during the transition */ + @Presubmit @Test fun focusDoesNotChange() { - testSpec.assertEventLog { - this.focusDoesNotChange() - } + flicker.assertEventLog { this.focusDoesNotChange() } } - @FlakyTest(bugId = 206753786) - @Test - override fun statusBarLayerRotatesScales() = super.statusBarLayerRotatesScales() - companion object { /** * Creates the test configurations. * - * See [FlickerTestParameterFactory.getConfigNonRotationTests] for configuring - * repetitions, screen orientation and navigation modes. + * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and + * navigation modes. */ @Parameterized.Parameters(name = "{0}") @JvmStatic - fun getParams(): List<FlickerTestParameter> { - return FlickerTestParameterFactory.getInstance() - .getConfigNonRotationTests(supportedRotations = listOf(Surface.ROTATION_0), - repetitions = 3) + fun getParams(): List<FlickerTest> { + return FlickerTestFactory.nonRotationTests( + supportedRotations = listOf(Rotation.ROTATION_0) + ) } } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnDoubleClickTestTestCfArm.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnDoubleClickTestTestCfArm.kt new file mode 100644 index 000000000000..c09623490041 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnDoubleClickTestTestCfArm.kt @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.pip + +import android.tools.common.Rotation +import android.tools.device.flicker.junit.FlickerParametersRunnerFactory +import android.tools.device.flicker.legacy.FlickerTest +import android.tools.device.flicker.legacy.FlickerTestFactory +import org.junit.FixMethodOrder +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class ExpandPipOnDoubleClickTestTestCfArm(flicker: FlickerTest) : + ExpandPipOnDoubleClickTest(flicker) { + companion object { + /** + * Creates the test configurations. + * + * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and + * navigation modes. + */ + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): List<FlickerTest> { + return FlickerTestFactory.nonRotationTests( + supportedRotations = listOf(Rotation.ROTATION_0) + ) + } + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnPinchOpenTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnPinchOpenTest.kt new file mode 100644 index 000000000000..0b73aac02797 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnPinchOpenTest.kt @@ -0,0 +1,68 @@ +/* + * 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.Presubmit +import android.tools.common.Rotation +import android.tools.device.flicker.junit.FlickerParametersRunnerFactory +import android.tools.device.flicker.legacy.FlickerBuilder +import android.tools.device.flicker.legacy.FlickerTest +import android.tools.device.flicker.legacy.FlickerTestFactory +import androidx.test.filters.RequiresDevice +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** Test expanding a pip window via pinch out gesture. */ +@RequiresDevice +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +open class ExpandPipOnPinchOpenTest(flicker: FlickerTest) : PipTransition(flicker) { + override val transition: FlickerBuilder.() -> Unit + get() = buildTransition { transitions { pipApp.pinchOpenPipWindow(wmHelper, 0.4f, 30) } } + + /** Checks that the visible region area of [pipApp] always increases during the animation. */ + @Presubmit + @Test + fun pipLayerAreaIncreases() { + flicker.assertLayers { + val pipLayerList = this.layers { pipApp.layerMatchesAnyOf(it) && it.isVisible } + pipLayerList.zipWithNext { previous, current -> + previous.visibleRegion.notBiggerThan(current.visibleRegion.region) + } + } + } + + companion object { + /** + * Creates the test configurations. + * + * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and + * navigation modes. + */ + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): List<FlickerTest> { + return FlickerTestFactory.nonRotationTests( + supportedRotations = listOf(Rotation.ROTATION_0) + ) + } + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnPinchOpenTestCfArm.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnPinchOpenTestCfArm.kt new file mode 100644 index 000000000000..e064bf2ee921 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnPinchOpenTestCfArm.kt @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.pip + +import android.tools.common.Rotation +import android.tools.device.flicker.junit.FlickerParametersRunnerFactory +import android.tools.device.flicker.legacy.FlickerTest +import android.tools.device.flicker.legacy.FlickerTestFactory +import org.junit.FixMethodOrder +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class ExpandPipOnPinchOpenTestCfArm(flicker: FlickerTest) : ExpandPipOnPinchOpenTest(flicker) { + companion object { + /** + * Creates the test configurations. + * + * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and + * navigation modes. + */ + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): List<FlickerTest> { + return FlickerTestFactory.nonRotationTests( + supportedRotations = listOf(Rotation.ROTATION_0) + ) + } + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipDownOnShelfHeightChange.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipDownOnShelfHeightChange.kt new file mode 100644 index 000000000000..d8d57d219933 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipDownOnShelfHeightChange.kt @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.pip + +import android.platform.test.annotations.Presubmit +import android.tools.device.flicker.junit.FlickerParametersRunnerFactory +import android.tools.device.flicker.legacy.FlickerBuilder +import android.tools.device.flicker.legacy.FlickerTest +import androidx.test.filters.RequiresDevice +import com.android.wm.shell.flicker.Direction +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** + * Test Pip movement with Launcher shelf height change (increase). + * + * To run this test: `atest WMShellFlickerTests:MovePipUpShelfHeightChangeTest` + * + * Actions: + * ``` + * Launch [pipApp] in pip mode + * Press home + * Launch [testApp] + * Check if pip window moves down (visually) + * ``` + * + * Notes: + * ``` + * 1. Some default assertions (e.g., nav bar, status bar and screen covered) + * are inherited [PipTransition] + * 2. Part of the test setup occurs automatically via + * [android.tools.device.flicker.legacy.runner.TransitionRunner], + * 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) +class MovePipDownOnShelfHeightChange(flicker: FlickerTest) : MovePipShelfHeightTransition(flicker) { + /** Defines the transition used to run the test */ + override val transition: FlickerBuilder.() -> Unit + get() = buildTransition { + teardown { + tapl.pressHome() + testApp.exit(wmHelper) + } + transitions { testApp.launchViaIntent(wmHelper) } + } + + /** Checks that the visible region of [pipApp] window always moves down during the animation. */ + @Presubmit @Test fun pipWindowMovesDown() = pipWindowMoves(Direction.DOWN) + + /** Checks that the visible region of [pipApp] layer always moves down during the animation. */ + @Presubmit @Test fun pipLayerMovesDown() = pipLayerMoves(Direction.DOWN) +} 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 deleted file mode 100644 index 8729bb6776f0..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipDownShelfHeightChangeTest.kt +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright (C) 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.wm.shell.flicker.pip - -import android.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 org.junit.FixMethodOrder -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.MethodSorters -import org.junit.runners.Parameterized - -/** - * Test Pip movement with Launcher shelf height change (decrease). - * - * To run this test: `atest WMShellFlickerTests:MovePipDownShelfHeightChangeTest` - * - * Actions: - * Launch [pipApp] in pip mode - * Launch [testApp] - * Press home - * Check if pip window moves down (visually) - * - * Notes: - * 1. Some default assertions (e.g., nav bar, status bar and screen covered) - * are inherited [PipTransition] - * 2. Part of the test setup occurs automatically via - * [com.android.server.wm.flicker.TransitionRunnerWithRules], - * including configuring navigation mode, initial orientation and ensuring no - * apps are running before setup - */ -@RequiresDevice -@RunWith(Parameterized::class) -@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -@Group3 -open class MovePipDownShelfHeightChangeTest( - testSpec: FlickerTestParameter -) : MovePipShelfHeightTransition(testSpec) { - /** - * Defines the transition used to run the test - */ - override val transition: FlickerBuilder.() -> Unit - get() = buildTransition(eachRun = false) { - teardown { - eachRun { - testApp.launchViaIntent(wmHelper) - } - test { - testApp.exit(wmHelper) - } - } - transitions { - taplInstrumentation.pressHome() - } - } - - override fun assertRegionMovement(previous: RegionSubject, current: RegionSubject) { - current.isHigherOrEqual(previous.region) - } - - /** {@inheritDoc} */ - @FlakyTest(bugId = 206753786) - @Test - override fun statusBarLayerRotatesScales() = super.statusBarLayerRotatesScales() - - companion object { - /** - * Creates the test configurations. - * - * See [FlickerTestParameterFactory.getConfigNonRotationTests] for configuring - * repetitions, screen orientation and navigation modes. - */ - @Parameterized.Parameters(name = "{0}") - @JvmStatic - fun getParams(): List<FlickerTestParameter> { - return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests( - supportedRotations = listOf(Surface.ROTATION_0), repetitions = 3) - } - } -} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipOnImeVisibilityChangeTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipOnImeVisibilityChangeTest.kt new file mode 100644 index 000000000000..a626713aaa11 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipOnImeVisibilityChangeTest.kt @@ -0,0 +1,90 @@ +/* + * 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.tools.common.Rotation +import android.tools.common.datatypes.component.ComponentNameMatcher +import android.tools.device.flicker.junit.FlickerParametersRunnerFactory +import android.tools.device.flicker.legacy.FlickerBuilder +import android.tools.device.flicker.legacy.FlickerTest +import android.tools.device.flicker.legacy.FlickerTestFactory +import android.tools.device.helpers.WindowUtils +import androidx.test.filters.RequiresDevice +import com.android.server.wm.flicker.helpers.ImeAppHelper +import com.android.server.wm.flicker.helpers.setRotation +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** Test Pip launch. To run this test: `atest WMShellFlickerTests:PipKeyboardTest` */ +@RequiresDevice +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +open class MovePipOnImeVisibilityChangeTest(flicker: FlickerTest) : PipTransition(flicker) { + private val imeApp = ImeAppHelper(instrumentation) + + /** {@inheritDoc} */ + override val transition: FlickerBuilder.() -> Unit + get() = buildTransition { + setup { + imeApp.launchViaIntent(wmHelper) + setRotation(flicker.scenario.startRotation) + } + teardown { imeApp.exit(wmHelper) } + transitions { + // open the soft keyboard + imeApp.openIME(wmHelper) + createTag(TAG_IME_VISIBLE) + + // then close it again + imeApp.closeIME(wmHelper) + } + } + + /** Ensure the pip window remains visible throughout any keyboard interactions */ + @Presubmit + @Test + open fun pipInVisibleBounds() { + flicker.assertWmVisibleRegion(pipApp) { + val displayBounds = WindowUtils.getDisplayBounds(flicker.scenario.startRotation) + coversAtMost(displayBounds) + } + } + + /** Ensure that the pip window does not obscure the keyboard */ + @Presubmit + @Test + open fun pipIsAboveAppWindow() { + flicker.assertWmTag(TAG_IME_VISIBLE) { isAboveWindow(ComponentNameMatcher.IME, pipApp) } + } + + companion object { + private const val TAG_IME_VISIBLE = "imeIsVisible" + + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): Collection<FlickerTest> { + return FlickerTestFactory.nonRotationTests( + supportedRotations = listOf(Rotation.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/MovePipOnImeVisibilityChangeTestCfArm.kt index 1a21d32f568c..d3d77d20662e 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/MovePipOnImeVisibilityChangeTestCfArm.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,34 +16,29 @@ package com.android.wm.shell.flicker.pip -import androidx.test.filters.FlakyTest -import androidx.test.filters.RequiresDevice -import com.android.server.wm.flicker.FlickerParametersRunnerFactory -import com.android.server.wm.flicker.FlickerTestParameter -import com.android.server.wm.flicker.annotation.Group4 -import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled -import org.junit.Assume -import org.junit.Before +import android.tools.common.Rotation +import android.tools.device.flicker.junit.FlickerParametersRunnerFactory +import android.tools.device.flicker.legacy.FlickerTest +import android.tools.device.flicker.legacy.FlickerTestFactory import org.junit.FixMethodOrder -import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.MethodSorters import org.junit.runners.Parameterized -@RequiresDevice @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) -@Group4 -@FlakyTest(bugId = 217777115) -class PipKeyboardTestShellTransit(testSpec: FlickerTestParameter) : PipKeyboardTest(testSpec) { +class MovePipOnImeVisibilityChangeTestCfArm(flicker: FlickerTest) : + MovePipOnImeVisibilityChangeTest(flicker) { + companion object { + private const val TAG_IME_VISIBLE = "imeIsVisible" - @Before - override fun before() { - Assume.assumeTrue(isShellTransitionsEnabled) + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): Collection<FlickerTest> { + return FlickerTestFactory.nonRotationTests( + supportedRotations = listOf(Rotation.ROTATION_0) + ) + } } - - @FlakyTest(bugId = 214452854) - @Test - override fun statusBarLayerRotatesScales() = super.statusBarLayerRotatesScales() -}
\ No newline at end of file +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipShelfHeightTransition.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipShelfHeightTransition.kt index 0499e7de9a0a..109354ab5c79 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,46 +17,31 @@ 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.helpers.FixedAppHelper +import android.tools.common.Rotation +import android.tools.common.flicker.subject.region.RegionSubject +import android.tools.device.flicker.legacy.FlickerTest +import android.tools.device.flicker.legacy.FlickerTestFactory +import com.android.server.wm.flicker.helpers.FixedOrientationAppHelper +import com.android.wm.shell.flicker.Direction import org.junit.Test +import org.junit.runners.Parameterized -/** - * Base class for pip tests with Launcher shelf height change - */ -abstract class MovePipShelfHeightTransition( - testSpec: FlickerTestParameter -) : PipTransition(testSpec) { - protected val taplInstrumentation = LauncherInstrumentation() - protected val testApp = FixedAppHelper(instrumentation) - - /** - * Checks if the window movement direction is valid - */ - protected abstract fun assertRegionMovement(previous: RegionSubject, current: RegionSubject) +/** Base class for pip tests with Launcher shelf height change */ +abstract class MovePipShelfHeightTransition(flicker: FlickerTest) : PipTransition(flicker) { + protected val testApp = FixedOrientationAppHelper(instrumentation) - /** - * Checks [pipApp] window remains visible throughout the animation - */ + /** Checks [pipApp] window remains visible throughout the animation */ @Presubmit @Test open fun pipWindowIsAlwaysVisible() { - testSpec.assertWm { - isAppWindowVisible(pipApp.component) - } + flicker.assertWm { isAppWindowVisible(pipApp) } } - /** - * Checks [pipApp] layer remains visible throughout the animation - */ + /** Checks [pipApp] layer remains visible throughout the animation */ @Presubmit @Test open fun pipLayerIsAlwaysVisible() { - testSpec.assertLayers { - isVisible(pipApp.component) - } + flicker.assertLayers { isVisible(pipApp) } } /** @@ -66,9 +51,7 @@ abstract class MovePipShelfHeightTransition( @Presubmit @Test open fun pipWindowRemainInsideVisibleBounds() { - testSpec.assertWmVisibleRegion(pipApp.component) { - coversAtMost(displayBounds) - } + flicker.assertWmVisibleRegion(pipApp) { coversAtMost(displayBounds) } } /** @@ -78,39 +61,65 @@ abstract class MovePipShelfHeightTransition( @Presubmit @Test open fun pipLayerRemainInsideVisibleBounds() { - testSpec.assertLayersVisibleRegion(pipApp.component) { - coversAtMost(displayBounds) - } + flicker.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() - testSpec.assertWm { - val pipWindowList = this.windowStates { it.name.contains(windowName) && it.isVisible } - pipWindowList.zipWithNext { previous, current -> - assertRegionMovement(previous.frame, current.frame) + protected fun pipWindowMoves(direction: Direction) { + flicker.assertWm { + 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() - testSpec.assertLayers { - val pipLayerList = this.layers { it.name.contains(layerName) && it.isVisible } - pipLayerList.zipWithNext { previous, current -> - assertRegionMovement(previous.visibleRegion, current.visibleRegion) + protected fun pipLayerMoves(direction: Direction) { + flicker.assertLayers { + 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()) + } + + companion object { + /** + * Creates the test configurations. + * + * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and + * navigation modes. + */ + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): List<FlickerTest> { + return FlickerTestFactory.nonRotationTests( + supportedRotations = listOf(Rotation.ROTATION_0) + ) + } + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipUpOnShelfHeightChangeTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipUpOnShelfHeightChangeTest.kt new file mode 100644 index 000000000000..ae3f87967658 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipUpOnShelfHeightChangeTest.kt @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.pip + +import android.platform.test.annotations.Presubmit +import android.tools.device.flicker.junit.FlickerParametersRunnerFactory +import android.tools.device.flicker.legacy.FlickerBuilder +import android.tools.device.flicker.legacy.FlickerTest +import androidx.test.filters.RequiresDevice +import com.android.wm.shell.flicker.Direction +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** + * Test Pip movement with Launcher shelf height change (decrease). + * + * To run this test: `atest WMShellFlickerTests:MovePipDownShelfHeightChangeTest` + * + * Actions: + * ``` + * Launch [pipApp] in pip mode + * Launch [testApp] + * Press home + * Check if pip window moves up (visually) + * ``` + * + * Notes: + * ``` + * 1. Some default assertions (e.g., nav bar, status bar and screen covered) + * are inherited [PipTransition] + * 2. Part of the test setup occurs automatically via + * [android.tools.device.flicker.legacy.runner.TransitionRunner], + * 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) +open class MovePipUpOnShelfHeightChangeTest(flicker: FlickerTest) : + MovePipShelfHeightTransition(flicker) { + /** Defines the transition used to run the test */ + override val transition: FlickerBuilder.() -> Unit + get() = + buildTransition() { + setup { testApp.launchViaIntent(wmHelper) } + transitions { tapl.pressHome() } + teardown { testApp.exit(wmHelper) } + } + + /** Checks that the visible region of [pipApp] window always moves up during the animation. */ + @Presubmit @Test fun pipWindowMovesUp() = pipWindowMoves(Direction.UP) + + /** Checks that the visible region of [pipApp] layer always moves up during the animation. */ + @Presubmit @Test fun pipLayerMovesUp() = pipLayerMoves(Direction.UP) +} 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 deleted file mode 100644 index 388b5e0b5e47..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipUpShelfHeightChangeTest.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.pip - -import androidx.test.filters.FlakyTest -import android.platform.test.annotations.RequiresDevice -import android.view.Surface -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 org.junit.FixMethodOrder -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.MethodSorters -import org.junit.runners.Parameterized - -/** - * Test Pip movement with Launcher shelf height change (increase). - * - * To run this test: `atest WMShellFlickerTests:MovePipUpShelfHeightChangeTest` - * - * Actions: - * Launch [pipApp] in pip mode - * Press home - * Launch [testApp] - * Check if pip window moves up (visually) - * - * Notes: - * 1. Some default assertions (e.g., nav bar, status bar and screen covered) - * are inherited [PipTransition] - * 2. Part of the test setup occurs automatically via - * [com.android.server.wm.flicker.TransitionRunnerWithRules], - * including configuring navigation mode, initial orientation and ensuring no - * apps are running before setup - */ -@RequiresDevice -@RunWith(Parameterized::class) -@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -@Group3 -class 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) - } - } - transitions { - testApp.launchViaIntent(wmHelper) - } - } - - override fun assertRegionMovement(previous: RegionSubject, current: RegionSubject) { - current.isLowerOrEqual(previous.region) - } - - /** {@inheritDoc} */ - @FlakyTest(bugId = 206753786) - @Test - override fun statusBarLayerRotatesScales() = super.statusBarLayerRotatesScales() - - companion object { - /** - * Creates the test configurations. - * - * See [FlickerTestParameterFactory.getConfigNonRotationTests] for configuring - * repetitions, screen orientation and navigation modes. - */ - @Parameterized.Parameters(name = "{0}") - @JvmStatic - fun getParams(): List<FlickerTestParameter> { - return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests( - supportedRotations = listOf(Surface.ROTATION_0), repetitions = 3) - } - } -} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipDragTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipDragTest.kt new file mode 100644 index 000000000000..4e2a4e700698 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipDragTest.kt @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.pip + +import android.platform.test.annotations.Postsubmit +import android.platform.test.annotations.RequiresDevice +import android.tools.device.flicker.junit.FlickerParametersRunnerFactory +import android.tools.device.flicker.legacy.FlickerBuilder +import android.tools.device.flicker.legacy.FlickerTest +import android.tools.device.flicker.legacy.FlickerTestFactory +import android.tools.device.flicker.rules.RemoveAllTasksButHomeRule +import com.android.server.wm.flicker.testapp.ActivityOptions +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** Test the dragging of a PIP window. */ +@RequiresDevice +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class PipDragTest(flicker: FlickerTest) : PipTransition(flicker) { + private var isDraggedLeft: Boolean = true + override val transition: FlickerBuilder.() -> Unit + get() = { + val stringExtras = mapOf(ActivityOptions.Pip.EXTRA_ENTER_PIP to "true") + + setup { + tapl.setEnableRotation(true) + // Launch the PIP activity and wait for it to enter PiP mode + RemoveAllTasksButHomeRule.removeAllTasksButHome() + pipApp.launchViaIntentAndWaitForPip(wmHelper, stringExtras = stringExtras) + + // determine the direction of dragging to test for + isDraggedLeft = pipApp.isCloserToRightEdge(wmHelper) + } + teardown { + // release the primary pointer after dragging without release + pipApp.releasePipAfterDragging() + + pipApp.exit(wmHelper) + tapl.setEnableRotation(false) + } + transitions { pipApp.dragPipWindowAwayFromEdgeWithoutRelease(wmHelper, 50) } + } + + @Postsubmit + @Test + fun pipLayerMovesAwayFromEdge() { + flicker.assertLayers { + val pipLayerList = layers { pipApp.layerMatchesAnyOf(it) && it.isVisible } + pipLayerList.zipWithNext { previous, current -> + if (isDraggedLeft) { + previous.visibleRegion.isToTheRight(current.visibleRegion.region) + } else { + current.visibleRegion.isToTheRight(previous.visibleRegion.region) + } + } + } + } + + companion object { + /** + * Creates the test configurations. + * + * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and + * navigation modes. + */ + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): List<FlickerTest> { + return FlickerTestFactory.nonRotationTests() + } + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipDragThenSnapTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipDragThenSnapTest.kt new file mode 100644 index 000000000000..9fe9f52fd4af --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipDragThenSnapTest.kt @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.pip + +import android.graphics.Rect +import android.platform.test.annotations.Postsubmit +import android.tools.common.Rotation +import android.tools.device.flicker.junit.FlickerParametersRunnerFactory +import android.tools.device.flicker.legacy.FlickerBuilder +import android.tools.device.flicker.legacy.FlickerTest +import android.tools.device.flicker.legacy.FlickerTestFactory +import android.tools.device.flicker.rules.RemoveAllTasksButHomeRule +import androidx.test.filters.RequiresDevice +import com.android.server.wm.flicker.helpers.setRotation +import com.android.server.wm.flicker.testapp.ActivityOptions +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** Test the snapping of a PIP window via dragging, releasing, and checking its final location. */ +@RequiresDevice +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class PipDragThenSnapTest(flicker: FlickerTest) : PipTransition(flicker) { + // represents the direction in which the pip window should be snapping + private var willSnapRight: Boolean = true + + override val transition: FlickerBuilder.() -> Unit + get() = { + val stringExtras: Map<String, String> = + mapOf(ActivityOptions.Pip.EXTRA_ENTER_PIP to "true") + + // cache the starting bounds here + val startBounds = Rect() + + setup { + // Launch the PIP activity and wait for it to enter PiP mode + setRotation(Rotation.ROTATION_0) + RemoveAllTasksButHomeRule.removeAllTasksButHome() + pipApp.launchViaIntentAndWaitForPip(wmHelper, stringExtras = stringExtras) + + // get the initial region bounds and cache them + val initRegion = pipApp.getWindowRect(wmHelper) + startBounds.set( + initRegion.left, + initRegion.top, + initRegion.right, + initRegion.bottom + ) + + // drag the pip window away from the edge + pipApp.dragPipWindowAwayFromEdge(wmHelper, 50) + + // determine the direction in which the snapping should occur + willSnapRight = pipApp.isCloserToRightEdge(wmHelper) + } + transitions { + // continue the transition until the PIP snaps + pipApp.waitForPipToSnapTo(wmHelper, startBounds) + } + } + + /** + * Checks that the visible region area of [pipApp] moves to closest edge during the animation. + */ + @Postsubmit + @Test + fun pipLayerMovesToClosestEdge() { + flicker.assertLayers { + val pipLayerList = layers { pipApp.layerMatchesAnyOf(it) && it.isVisible } + pipLayerList.zipWithNext { previous, current -> + if (willSnapRight) { + current.visibleRegion.isToTheRight(previous.visibleRegion.region) + } else { + previous.visibleRegion.isToTheRight(current.visibleRegion.region) + } + } + } + } + + companion object { + /** + * Creates the test configurations. + * + * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and + * navigation modes. + */ + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): List<FlickerTest> { + return FlickerTestFactory.nonRotationTests( + supportedRotations = listOf(Rotation.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 deleted file mode 100644 index 1e30f6b83874..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipKeyboardTest.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.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.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.wm.shell.flicker.helpers.ImeAppHelper -import org.junit.Assume.assumeFalse -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 launch. - * To run this test: `atest WMShellFlickerTests:PipKeyboardTest` - */ -@RequiresDevice -@RunWith(Parameterized::class) -@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -@Group4 -@FlakyTest(bugId = 218604389) -open class PipKeyboardTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) { - private val imeApp = ImeAppHelper(instrumentation) - - @Before - open fun before() { - assumeFalse(isShellTransitionsEnabled) - } - - override val transition: FlickerBuilder.() -> Unit - get() = buildTransition(eachRun = false) { - setup { - test { - imeApp.launchViaIntent(wmHelper) - setRotation(testSpec.startRotation) - } - } - teardown { - test { - imeApp.exit(wmHelper) - setRotation(Surface.ROTATION_0) - } - } - transitions { - // open the soft keyboard - imeApp.openIME(wmHelper) - createTag(TAG_IME_VISIBLE) - - // then close it again - imeApp.closeIME(wmHelper) - } - } - - /** {@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) { - val displayBounds = WindowUtils.getDisplayBounds(testSpec.startRotation) - coversAtMost(displayBounds) - } - } - - /** - * Ensure that the pip window does not obscure the keyboard - */ - @Presubmit - @Test - open fun pipIsAboveAppWindow() { - testSpec.assertWmTag(TAG_IME_VISIBLE) { - isAboveWindow(FlickerComponentName.IME, pipApp.component) - } - } - - companion object { - private const val TAG_IME_VISIBLE = "imeIsVisible" - - @Parameterized.Parameters(name = "{0}") - @JvmStatic - fun getParams(): Collection<FlickerTestParameter> { - return FlickerTestParameterFactory.getInstance() - .getConfigNonRotationTests(supportedRotations = listOf(Surface.ROTATION_0), - repetitions = 3) - } - } -} 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/PipPinchInTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipPinchInTest.kt new file mode 100644 index 000000000000..85b2fbce2f21 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipPinchInTest.kt @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.pip + +import android.platform.test.annotations.Postsubmit +import android.tools.common.Rotation +import android.tools.device.flicker.junit.FlickerParametersRunnerFactory +import android.tools.device.flicker.legacy.FlickerBuilder +import android.tools.device.flicker.legacy.FlickerTest +import android.tools.device.flicker.legacy.FlickerTestFactory +import androidx.test.filters.RequiresDevice +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** Test minimizing a pip window via pinch in gesture. */ +@RequiresDevice +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class PipPinchInTest(flicker: FlickerTest) : PipTransition(flicker) { + override val transition: FlickerBuilder.() -> Unit + get() = buildTransition { transitions { pipApp.pinchInPipWindow(wmHelper, 0.4f, 30) } } + + /** Checks that the visible region area of [pipApp] always decreases during the animation. */ + @Postsubmit + @Test + fun pipLayerAreaDecreases() { + flicker.assertLayers { + val pipLayerList = this.layers { pipApp.layerMatchesAnyOf(it) && it.isVisible } + pipLayerList.zipWithNext { previous, current -> + current.visibleRegion.notBiggerThan(previous.visibleRegion.region) + } + } + } + + companion object { + /** + * Creates the test configurations. + * + * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and + * navigation modes. + */ + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): List<FlickerTest> { + return FlickerTestFactory.nonRotationTests( + supportedRotations = listOf(Rotation.ROTATION_0) + ) + } + } +} 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 deleted file mode 100644 index c1ee1a7cbb35..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipRotationTest.kt +++ /dev/null @@ -1,205 +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.entireScreenCovered -import com.android.server.wm.flicker.helpers.WindowUtils -import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled -import com.android.server.wm.flicker.helpers.setRotation -import com.android.server.wm.flicker.navBarLayerRotatesAndScales -import com.android.server.wm.flicker.statusBarLayerRotatesScales -import com.android.wm.shell.flicker.helpers.FixedAppHelper -import org.junit.Assume -import org.junit.FixMethodOrder -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.MethodSorters -import org.junit.runners.Parameterized - -/** - * Test Pip Stack in bounds after rotations. - * - * To run this test: `atest WMShellFlickerTests:PipRotationTest` - * - * Actions: - * Launch a [pipApp] in pip mode - * Launch another app [fixedApp] (appears below pip) - * Rotate the screen from [testSpec.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 -open class PipRotationTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) { - private val fixedApp = FixedAppHelper(instrumentation) - private val screenBoundsStart = WindowUtils.getDisplayBounds(testSpec.startRotation) - private val screenBoundsEnd = WindowUtils.getDisplayBounds(testSpec.endRotation) - - override val transition: FlickerBuilder.() -> Unit - get() = buildTransition(eachRun = false) { - setup { - test { - fixedApp.launchViaIntent(wmHelper) - } - eachRun { - setRotation(testSpec.startRotation) - } - } - transitions { - setRotation(testSpec.endRotation) - } - } - - /** - * Checks that all parts of the screen are covered at the start and end of the transition - */ - @Presubmit - @Test - override fun entireScreenCovered() = testSpec.entireScreenCovered() - - /** - * Checks the position of the navigation bar at the start and end of the transition - */ - @FlakyTest - @Test - override fun navBarLayerRotatesAndScales() = testSpec.navBarLayerRotatesAndScales() - - /** - * Checks the position of the status bar at the start and end of the transition - */ - @FlakyTest(bugId = 206753786) - @Test - override fun statusBarLayerRotatesScales() = testSpec.statusBarLayerRotatesScales() - - /** - * Checks that [fixedApp] layer is within [screenBoundsStart] at the start of the transition - */ - @Presubmit - @Test - fun appLayerRotates_StartingBounds() { - testSpec.assertLayersStart { - visibleRegion(fixedApp.component).coversExactly(screenBoundsStart) - } - } - - /** - * Checks that [fixedApp] layer is within [screenBoundsEnd] at the end of the transition - */ - @Presubmit - @Test - fun appLayerRotates_EndingBounds() { - testSpec.assertLayersEnd { - visibleRegion(fixedApp.component).coversExactly(screenBoundsEnd) - } - } - - /** - * Checks that [pipApp] layer is within [screenBoundsStart] at the start of the transition - */ - private fun pipLayerRotates_StartingBounds_internal() { - testSpec.assertLayersStart { - visibleRegion(pipApp.component).coversAtMost(screenBoundsStart) - } - } - - /** - * Checks that [pipApp] layer is within [screenBoundsStart] at the start of the transition - */ - @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() - } - - /** - * Checks that [pipApp] layer is within [screenBoundsEnd] at the end of the transition - */ - @Presubmit - @Test - fun pipLayerRotates_EndingBounds() { - testSpec.assertLayersEnd { - visibleRegion(pipApp.component).coversAtMost(screenBoundsEnd) - } - } - - /** - * Ensure that the [pipApp] window does not obscure the [fixedApp] at the start of the - * transition - */ - @Presubmit - @Test - fun pipIsAboveFixedAppWindow_Start() { - testSpec.assertWmStart { - isAboveWindow(pipApp.component, fixedApp.component) - } - } - - /** - * Ensure that the [pipApp] window does not obscure the [fixedApp] at the end of the - * transition - */ - @Presubmit - @Test - fun pipIsAboveFixedAppWindow_End() { - testSpec.assertWmEnd { - isAboveWindow(pipApp.component, fixedApp.component) - } - } - - companion object { - /** - * Creates the test configurations. - * - * See [FlickerTestParameterFactory.getConfigNonRotationTests] for configuring - * repetitions, screen orientation and navigation modes. - */ - @Parameterized.Parameters(name = "{0}") - @JvmStatic - fun getParams(): Collection<FlickerTestParameter> { - return FlickerTestParameterFactory.getInstance().getConfigRotationTests( - supportedRotations = listOf(Surface.ROTATION_0, Surface.ROTATION_90), - repetitions = 3) - } - } -} 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..b30f30830156 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 @@ -19,32 +19,24 @@ 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 android.tools.common.Rotation +import android.tools.common.datatypes.component.ComponentNameMatcher +import android.tools.device.flicker.legacy.FlickerBuilder +import android.tools.device.flicker.legacy.FlickerTest +import android.tools.device.flicker.rules.RemoveAllTasksButHomeRule.Companion.removeAllTasksButHome +import android.tools.device.helpers.WindowUtils +import com.android.server.wm.flicker.helpers.PipAppHelper 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.helpers.PipAppHelper -import com.android.wm.shell.flicker.testapp.Components +import com.android.server.wm.flicker.testapp.ActivityOptions +import com.android.wm.shell.flicker.BaseTest +import com.google.common.truth.Truth import org.junit.Test -abstract class PipTransition(protected val testSpec: FlickerTestParameter) { - protected val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation() +abstract class PipTransition(flicker: FlickerTest) : BaseTest(flicker) { protected val pipApp = PipAppHelper(instrumentation) - protected val displayBounds = WindowUtils.getDisplayBounds(testSpec.startRotation) + protected val displayBounds = WindowUtils.getDisplayBounds(flicker.scenario.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 { @@ -52,93 +44,37 @@ abstract class PipTransition(protected val testSpec: FlickerTestParameter) { } fun doAction(broadcastAction: String) { - instrumentation.context - .sendBroadcast(createIntentWithAction(broadcastAction)) + instrumentation.context.sendBroadcast(createIntentWithAction(broadcastAction)) } companion object { // Corresponds to ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE - @JvmStatic - val ORIENTATION_LANDSCAPE = 0 + @JvmStatic val ORIENTATION_LANDSCAPE = 0 // Corresponds to ActivityInfo.SCREEN_ORIENTATION_PORTRAIT - @JvmStatic - val ORIENTATION_PORTRAIT = 1 + @JvmStatic val ORIENTATION_PORTRAIT = 1 } } - @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 + * Gets a configuration that handles basic setup and teardown of pip tests and that launches the + * Pip app for test * - * @param eachRun If the pip app should be launched in each run (otherwise only 1x per test) * @param stringExtras Arguments to pass to the PIP launch intent - * @param extraSpec Addicional segment of flicker specification + * @param extraSpec Additional segment of flicker specification */ @JvmOverloads protected open fun buildTransition( - eachRun: Boolean, - stringExtras: Map<String, String> = mapOf(Components.PipActivity.EXTRA_ENTER_PIP to "true"), + stringExtras: Map<String, String> = mapOf(ActivityOptions.Pip.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() - } - } - } - teardown { - eachRun { - if (eachRun) { - pipApp.exit(wmHelper) - } - } - test { - if (!eachRun) { - pipApp.exit(wmHelper) - } - } + setRotation(Rotation.ROTATION_0) + removeAllTasksButHome() + pipApp.launchViaIntentAndWaitForPip(wmHelper, stringExtras = stringExtras) } + teardown { pipApp.exit(wmHelper) } extraSpec(this) } @@ -146,29 +82,17 @@ abstract class PipTransition(protected val testSpec: FlickerTestParameter) { @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() + fun hasAtMostOnePipDismissOverlayWindow() { + val matcher = ComponentNameMatcher("", "pip-dismiss-overlay") + flicker.assertWm { + val overlaysPerState = + trace.entries.map { entry -> + entry.windowStates.count { window -> matcher.windowMatchesAnyOf(window) } <= 1 + } - @Presubmit - @Test - open fun entireScreenCovered() = testSpec.entireScreenCovered() -}
\ No newline at end of file + Truth.assertWithMessage("Number of dismiss overlays per state") + .that(overlaysPerState) + .doesNotContain(false) + } + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/SetRequestedOrientationWhilePinned.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/SetRequestedOrientationWhilePinned.kt new file mode 100644 index 000000000000..3850c1f6c89a --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/SetRequestedOrientationWhilePinned.kt @@ -0,0 +1,158 @@ +/* + * 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.app.Activity +import android.platform.test.annotations.FlakyTest +import android.platform.test.annotations.Postsubmit +import android.platform.test.annotations.Presubmit +import android.tools.common.Rotation +import android.tools.device.flicker.junit.FlickerParametersRunnerFactory +import android.tools.device.flicker.legacy.FlickerBuilder +import android.tools.device.flicker.legacy.FlickerTest +import android.tools.device.flicker.legacy.FlickerTestFactory +import android.tools.device.helpers.WindowUtils +import androidx.test.filters.RequiresDevice +import com.android.server.wm.flicker.testapp.ActivityOptions +import com.android.server.wm.flicker.testapp.ActivityOptions.PortraitOnlyActivity.EXTRA_FIXED_ORIENTATION +import com.android.wm.shell.flicker.pip.PipTransition.BroadcastActionTrigger.Companion.ORIENTATION_LANDSCAPE +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 exiting Pip with orientation changes. To run this test: `atest + * WMShellFlickerTests:SetRequestedOrientationWhilePinnedTest` + */ +@RequiresDevice +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +open class SetRequestedOrientationWhilePinned(flicker: FlickerTest) : PipTransition(flicker) { + private val startingBounds = WindowUtils.getDisplayBounds(Rotation.ROTATION_0) + private val endingBounds = WindowUtils.getDisplayBounds(Rotation.ROTATION_90) + + /** {@inheritDoc} */ + override val transition: FlickerBuilder.() -> Unit + get() = { + setup { + // Launch the PiP activity fixed as landscape. + pipApp.launchViaIntent( + wmHelper, + stringExtras = + mapOf(EXTRA_FIXED_ORIENTATION to ORIENTATION_LANDSCAPE.toString()) + ) + // Enter PiP. + broadcastActionTrigger.doAction(ActivityOptions.Pip.ACTION_ENTER_PIP) + // System bar may fade out during fixed rotation. + wmHelper + .StateSyncBuilder() + .withPipShown() + .withRotation(Rotation.ROTATION_0) + .withNavOrTaskBarVisible() + .withStatusBarVisible() + .waitForAndVerify() + } + teardown { pipApp.exit(wmHelper) } + transitions { + // Launch the activity back into fullscreen and ensure that it is now in landscape + pipApp.launchViaIntent(wmHelper) + // System bar may fade out during fixed rotation. + wmHelper + .StateSyncBuilder() + .withFullScreenApp(pipApp) + .withRotation(Rotation.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(tapl.isTablet) + } + + @Presubmit + @Test + fun displayEndsAt90Degrees() { + flicker.assertWmEnd { hasRotation(Rotation.ROTATION_90) } + } + + @Presubmit + @Test + fun pipWindowInsideDisplay() { + flicker.assertWmStart { visibleRegion(pipApp).coversAtMost(startingBounds) } + } + + @Presubmit + @Test + fun pipAppShowsOnTop() { + flicker.assertWmEnd { isAppWindowOnTop(pipApp) } + } + + @Presubmit + @Test + fun pipLayerInsideDisplay() { + flicker.assertLayersStart { visibleRegion(pipApp).coversAtMost(startingBounds) } + } + + @Presubmit + @Test + fun pipAlwaysVisible() { + flicker.assertWm { this.isAppWindowVisible(pipApp) } + } + + @Presubmit + @Test + fun pipAppLayerCoversFullScreen() { + flicker.assertLayersEnd { visibleRegion(pipApp).coversExactly(endingBounds) } + } + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun taskBarLayerIsVisibleAtStartAndEnd() = super.taskBarLayerIsVisibleAtStartAndEnd() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun taskBarWindowIsAlwaysVisible() = super.taskBarWindowIsAlwaysVisible() + + /** {@inheritDoc} */ + @FlakyTest(bugId = 264243884) + @Test + override fun entireScreenCovered() = super.entireScreenCovered() + + companion object { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): Collection<FlickerTest> { + return FlickerTestFactory.nonRotationTests( + supportedRotations = listOf(Rotation.ROTATION_0) + ) + } + } +} 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 deleted file mode 100644 index e40f2bc1ed5a..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/SetRequestedOrientationWhilePinnedTest.kt +++ /dev/null @@ -1,169 +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.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.FixMethodOrder -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.MethodSorters -import org.junit.runners.Parameterized - -/** - * Test exiting Pip with orientation changes. - * To run this test: `atest WMShellFlickerTests:SetRequestedOrientationWhilePinnedTest` - */ -@RequiresDevice -@RunWith(Parameterized::class) -@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -@Group4 -open class SetRequestedOrientationWhilePinnedTest( - testSpec: FlickerTestParameter -) : PipTransition(testSpec) { - private val startingBounds = WindowUtils.getDisplayBounds(Surface.ROTATION_0) - private val endingBounds = WindowUtils.getDisplayBounds(Surface.ROTATION_90) - - 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() - } - } - teardown { - eachRun { - pipApp.exit(wmHelper) - setRotation(Surface.ROTATION_0) - } - test { - 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() - } - } - - @Presubmit - @Test - fun displayEndsAt90Degrees() { - testSpec.assertWmEnd { - hasRotation(Surface.ROTATION_90) - } - } - - @Presubmit - @Test - override fun navBarLayerIsVisible() = super.navBarLayerIsVisible() - - @Presubmit - @Test - override fun statusBarLayerIsVisible() = super.statusBarLayerIsVisible() - - @FlakyTest - @Test - override fun navBarLayerRotatesAndScales() = super.navBarLayerRotatesAndScales() - - @FlakyTest(bugId = 206753786) - @Test - override fun statusBarLayerRotatesScales() = super.statusBarLayerRotatesScales() - - @Presubmit - @Test - fun pipWindowInsideDisplay() { - testSpec.assertWmStart { - frameRegion(pipApp.component).coversAtMost(startingBounds) - } - } - - @Presubmit - @Test - fun pipAppShowsOnTop() { - testSpec.assertWmEnd { - isAppWindowOnTop(pipApp.component) - } - } - - @Presubmit - @Test - fun pipLayerInsideDisplay() { - testSpec.assertLayersStart { - visibleRegion(pipApp.component).coversAtMost(startingBounds) - } - } - - @Presubmit - @Test - fun pipAlwaysVisible() { - testSpec.assertWm { - this.isAppWindowVisible(pipApp.component) - } - } - - @Presubmit - @Test - fun pipAppLayerCoversFullScreen() { - testSpec.assertLayersEnd { - visibleRegion(pipApp.component).coversExactly(endingBounds) - } - } - - companion object { - @Parameterized.Parameters(name = "{0}") - @JvmStatic - fun getParams(): Collection<FlickerTestParameter> { - return FlickerTestParameterFactory.getInstance() - .getConfigNonRotationTests(supportedRotations = listOf(Surface.ROTATION_0), - repetitions = 1) - } - } -} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ShowPipAndRotateDisplay.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ShowPipAndRotateDisplay.kt new file mode 100644 index 000000000000..703784dd8c67 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ShowPipAndRotateDisplay.kt @@ -0,0 +1,167 @@ +/* + * 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.tools.device.flicker.junit.FlickerParametersRunnerFactory +import android.tools.device.flicker.legacy.FlickerBuilder +import android.tools.device.flicker.legacy.FlickerTest +import android.tools.device.flicker.legacy.FlickerTestFactory +import android.tools.device.helpers.WindowUtils +import androidx.test.filters.RequiresDevice +import com.android.server.wm.flicker.helpers.SimpleAppHelper +import com.android.server.wm.flicker.helpers.setRotation +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` + * + * Actions: + * ``` + * Launch a [pipApp] in pip mode + * Launch another app [fixedApp] (appears below pip) + * Rotate the screen from [flicker.scenario.startRotation] to [flicker.scenario.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 + * [android.tools.device.flicker.legacy.runner.TransitionRunner], + * 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) +open class ShowPipAndRotateDisplay(flicker: FlickerTest) : PipTransition(flicker) { + private val testApp = SimpleAppHelper(instrumentation) + private val screenBoundsStart = WindowUtils.getDisplayBounds(flicker.scenario.startRotation) + private val screenBoundsEnd = WindowUtils.getDisplayBounds(flicker.scenario.endRotation) + + override val transition: FlickerBuilder.() -> Unit + get() = buildTransition { + setup { + testApp.launchViaIntent(wmHelper) + setRotation(flicker.scenario.startRotation) + } + transitions { setRotation(flicker.scenario.endRotation) } + } + + /** Checks that [testApp] layer is within [screenBoundsStart] at the start of the transition */ + @Presubmit + @Test + fun fixedAppLayer_StartingBounds() { + flicker.assertLayersStart { visibleRegion(testApp).coversAtMost(screenBoundsStart) } + } + + /** Checks that [testApp] layer is within [screenBoundsEnd] at the end of the transition */ + @Presubmit + @Test + fun fixedAppLayer_EndingBounds() { + flicker.assertLayersEnd { visibleRegion(testApp).coversAtMost(screenBoundsEnd) } + } + + /** + * Checks that [testApp] plus [pipApp] layers are within [screenBoundsEnd] at the start of the + * transition + */ + @Presubmit + @Test + fun appLayers_StartingBounds() { + flicker.assertLayersStart { + visibleRegion(testApp.or(pipApp)).coversExactly(screenBoundsStart) + } + } + + /** + * Checks that [testApp] plus [pipApp] layers are within [screenBoundsEnd] at the end of the + * transition + */ + @Presubmit + @Test + fun appLayers_EndingBounds() { + flicker.assertLayersEnd { visibleRegion(testApp.or(pipApp)).coversExactly(screenBoundsEnd) } + } + + /** Checks that [pipApp] layer is within [screenBoundsStart] at the start of the transition */ + private fun pipLayerRotates_StartingBounds_internal() { + flicker.assertLayersStart { visibleRegion(pipApp).coversAtMost(screenBoundsStart) } + } + + /** Checks that [pipApp] layer is within [screenBoundsStart] at the start of the transition */ + @Presubmit + @Test + fun pipLayerRotates_StartingBounds() { + pipLayerRotates_StartingBounds_internal() + } + + /** Checks that [pipApp] layer is within [screenBoundsEnd] at the end of the transition */ + @Presubmit + @Test + fun pipLayerRotates_EndingBounds() { + flicker.assertLayersEnd { visibleRegion(pipApp).coversAtMost(screenBoundsEnd) } + } + + /** + * Ensure that the [pipApp] window does not obscure the [testApp] at the start of the transition + */ + @Presubmit + @Test + fun pipIsAboveFixedAppWindow_Start() { + flicker.assertWmStart { isAboveWindow(pipApp, testApp) } + } + + /** + * Ensure that the [pipApp] window does not obscure the [testApp] at the end of the transition + */ + @Presubmit + @Test + fun pipIsAboveFixedAppWindow_End() { + flicker.assertWmEnd { isAboveWindow(pipApp, testApp) } + } + + @Presubmit + @Test + override fun navBarLayerIsVisibleAtStartAndEnd() { + super.navBarLayerIsVisibleAtStartAndEnd() + } + + companion object { + /** + * Creates the test configurations. + * + * See [FlickerTestFactory.nonRotationTests] for configuring repetitions, screen orientation + * and navigation modes. + */ + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): Collection<FlickerTest> { + return FlickerTestFactory.rotationTests() + } + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ShowPipAndRotateDisplayCfArm.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ShowPipAndRotateDisplayCfArm.kt new file mode 100644 index 000000000000..b7a2c47e3b32 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ShowPipAndRotateDisplayCfArm.kt @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.pip + +import android.tools.device.flicker.junit.FlickerParametersRunnerFactory +import android.tools.device.flicker.legacy.FlickerTest +import android.tools.device.flicker.legacy.FlickerTestFactory +import org.junit.FixMethodOrder +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class ShowPipAndRotateDisplayCfArm(flicker: FlickerTest) : ShowPipAndRotateDisplay(flicker) { + companion object { + /** + * Creates the test configurations. + * + * See [FlickerTestFactory.nonRotationTests] for configuring repetitions, screen orientation + * and navigation modes. + */ + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): Collection<FlickerTest> { + return FlickerTestFactory.rotationTests() + } + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/PipAppHelperTv.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/PipAppHelperTv.kt new file mode 100644 index 000000000000..000ae8f9458e --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/PipAppHelperTv.kt @@ -0,0 +1,79 @@ +/* + * 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.tv + +import android.app.Instrumentation +import android.tools.device.traces.parsers.WindowManagerStateHelper +import androidx.test.uiautomator.By +import androidx.test.uiautomator.BySelector +import androidx.test.uiautomator.UiObject2 +import androidx.test.uiautomator.Until +import com.android.server.wm.flicker.helpers.PipAppHelper + +/** Helper class for PIP app on AndroidTV */ +open class PipAppHelperTv(instrumentation: Instrumentation) : PipAppHelper(instrumentation) { + private val appSelector = By.pkg(`package`).depth(0) + + val ui: UiObject2? + get() = uiDevice.findObject(appSelector) + + private fun focusOnObject(selector: BySelector): Boolean { + // We expect all the focusable UI elements to be arranged in a way so that it is possible + // to "cycle" over all them by clicking the D-Pad DOWN button, going back up to "the top" + // from "the bottom". + repeat(FOCUS_ATTEMPTS) { + uiDevice.findObject(selector)?.apply { if (isFocusedOrHasFocusedChild) return true } + ?: error("The object we try to focus on is gone.") + + uiDevice.pressDPadDown() + uiDevice.waitForIdle() + } + return false + } + + override fun clickObject(resId: String) { + val selector = By.res(`package`, resId) + focusOnObject(selector) || error("Could not focus on `$resId` object") + uiDevice.pressDPadCenter() + } + + @Deprecated( + "Use PipAppHelper.closePipWindow(wmHelper) instead", + ReplaceWith("closePipWindow(wmHelper)") + ) + override fun closePipWindow() { + uiDevice.closeTvPipWindow() + } + + /** Taps the pip window and dismisses it by clicking on the X button. */ + override fun closePipWindow(wmHelper: WindowManagerStateHelper) { + uiDevice.closeTvPipWindow() + + // Wait for animation to complete. + wmHelper.StateSyncBuilder().withPipGone().withHomeActivityVisible().waitForAndVerify() + } + + fun waitUntilClosed(): Boolean { + val appSelector = By.pkg(`package`).depth(0) + return uiDevice.wait(Until.gone(appSelector), APP_CLOSE_WAIT_TIME_MS) + } + + companion object { + private const val FOCUS_ATTEMPTS = 20 + private const val APP_CLOSE_WAIT_TIME_MS = 3_000L + } +} 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..2cb18f948f0e 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,47 +14,36 @@ * 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 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( - protected val rotationName: String, - protected val rotation: Int -) { +abstract class PipTestBase(protected val rotationName: String, protected val rotation: Int) { val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation() val uiDevice = UiDevice.getInstance(instrumentation) 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 = PipAppHelperTv(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..8a073abf032c 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 @@ -25,16 +25,11 @@ import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.Parameterized -/** - * Test Pip Menu on TV. - * To run this test: `atest WMShellFlickerTests:TvPipBasicTest` - */ +/** Test Pip Menu on TV. To run this test: `atest WMShellFlickerTests:TvPipBasicTest` */ @RequiresDevice @RunWith(Parameterized::class) -class TvPipBasicTest( - private val radioButtonId: String, - private val pipWindowRatio: Rational? -) : TvPipTestBase() { +class TvPipBasicTest(private val radioButtonId: String, private val pipWindowRatio: Rational?) : + TvPipTestBase() { @Test fun enterPip_openMenu_pressBack_closePip() { @@ -43,10 +38,10 @@ 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") + val actualRatio: Float = + testApp.ui?.visibleBounds?.ratio ?: fail("Application UI not found") pipWindowRatio?.let { expectedRatio -> assertEquals("Wrong Pip window ratio", expectedRatio.toFloat(), actualRatio) } @@ -62,7 +57,8 @@ class TvPipBasicTest( // Make sure Pip Window ration remained the same after Pip menu was closed testApp.ui?.visibleBounds?.let { newBounds -> assertEquals("Pip window ratio has changed", actualRatio, newBounds.ratio) - } ?: fail("Application UI not found") + } + ?: fail("Application UI not found") // Close Pip testApp.closePipWindow() @@ -77,11 +73,11 @@ class TvPipBasicTest( fun getParams(): Collection<Array<Any?>> { infix fun Int.to(denominator: Int) = Rational(this, denominator) return listOf( - arrayOf("ratio_default", null), - arrayOf("ratio_square", 1 to 1), - arrayOf("ratio_wide", 2 to 1), - arrayOf("ratio_tall", 1 to 2) + arrayOf("ratio_default", null), + arrayOf("ratio_square", 1 to 1), + arrayOf("ratio_wide", 2 to 1), + arrayOf("ratio_tall", 1 to 2) ) } } -}
\ 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..0432a8497fbe 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 @@ -19,35 +19,37 @@ package com.android.wm.shell.flicker.pip.tv import android.graphics.Rect import androidx.test.filters.RequiresDevice import androidx.test.uiautomator.UiObject2 +import com.android.server.wm.flicker.testapp.ActivityOptions import com.android.wm.shell.flicker.SYSTEM_UI_PACKAGE_NAME -import com.android.wm.shell.flicker.testapp.Components import com.android.wm.shell.flicker.wait import org.junit.Assert.assertNull import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test -/** - * Test Pip Menu on TV. - * To run this test: `atest WMShellFlickerTests:TvPipMenuTests` - */ +/** Test Pip Menu on TV. To run this test: `atest WMShellFlickerTests:TvPipMenuTests` */ @RequiresDevice class TvPipMenuTests : TvPipTestBase() { - private val systemUiResources = - packageManager.getResourcesForApplication(SYSTEM_UI_PACKAGE_NAME) - private val pipBoundsWhileInMenu: Rect = systemUiResources.run { - val bounds = getString(getIdentifier("pip_menu_bounds", "string", - SYSTEM_UI_PACKAGE_NAME)) - Rect.unflattenFromString(bounds) ?: error("Could not retrieve PiP menu bounds") + private val systemUiResources by lazy { + packageManager.getResourcesForApplication(SYSTEM_UI_PACKAGE_NAME) + } + private val pipBoundsWhileInMenu: Rect by lazy { + systemUiResources.run { + val bounds = + getString(getIdentifier("pip_menu_bounds", "string", SYSTEM_UI_PACKAGE_NAME)) + Rect.unflattenFromString(bounds) ?: error("Could not retrieve PiP menu bounds") + } } - private val playButtonDescription = systemUiResources.run { - getString(getIdentifier("pip_play", "string", - SYSTEM_UI_PACKAGE_NAME)) + private val playButtonDescription by lazy { + systemUiResources.run { + getString(getIdentifier("pip_play", "string", SYSTEM_UI_PACKAGE_NAME)) + } } - private val pauseButtonDescription = systemUiResources.run { - getString(getIdentifier("pip_pause", "string", - SYSTEM_UI_PACKAGE_NAME)) + private val pauseButtonDescription by lazy { + systemUiResources.run { + getString(getIdentifier("pip_pause", "string", SYSTEM_UI_PACKAGE_NAME)) + } } @Before @@ -61,20 +63,29 @@ class TvPipMenuTests : TvPipTestBase() { enterPip_openMenu_assertShown() // Make sure the PiP task is positioned where it should be. - val activityBounds: Rect = testApp.ui?.visibleBounds - ?: error("Could not retrieve Pip Activity bounds") - assertTrue("Pip Activity is positioned correctly while Pip menu is shown", - pipBoundsWhileInMenu == activityBounds) + val activityBounds: Rect = + testApp.ui?.visibleBounds ?: error("Could not retrieve Pip Activity bounds") + assertTrue( + "Pip Activity is positioned correctly while Pip menu is shown", + pipBoundsWhileInMenu == activityBounds + ) // Make sure the Pip Menu Actions are positioned correctly. uiDevice.findTvPipMenuControls()?.visibleBounds?.run { - assertTrue("Pip Menu Actions should be positioned below the Activity in Pip", - top >= activityBounds.bottom) - assertTrue("Pip Menu Actions should be positioned central horizontally", - centerX() == uiDevice.displayWidth / 2) - assertTrue("Pip Menu Actions should be fully shown on the screen", - left >= 0 && right <= uiDevice.displayWidth && bottom <= uiDevice.displayHeight) - } ?: error("Could not retrieve Pip Menu Actions bounds") + assertTrue( + "Pip Menu Actions should be positioned below the Activity in Pip", + top >= activityBounds.bottom + ) + assertTrue( + "Pip Menu Actions should be positioned central horizontally", + centerX() == uiDevice.displayWidth / 2 + ) + assertTrue( + "Pip Menu Actions should be fully shown on the screen", + left >= 0 && right <= uiDevice.displayWidth && bottom <= uiDevice.displayHeight + ) + } + ?: error("Could not retrieve Pip Menu Actions bounds") testApp.closePipWindow() } @@ -107,7 +118,7 @@ class TvPipMenuTests : TvPipTestBase() { // PiP menu should contain the Close button uiDevice.findTvPipMenuCloseButton() - ?: fail("\"Close PIP\" button should be shown in Pip menu") + ?: fail("\"Close PIP\" button should be shown in Pip menu") // Clicking on the Close button should close the app uiDevice.clickTvPipMenuCloseButton() @@ -120,13 +131,15 @@ class TvPipMenuTests : TvPipTestBase() { // PiP menu should contain the Fullscreen button uiDevice.findTvPipMenuFullscreenButton() - ?: fail("\"Full screen\" button should be shown in Pip menu") + ?: fail("\"Full screen\" button should be shown in Pip menu") // Clicking on the fullscreen button should return app to the fullscreen mode. // Click, wait for the app to go fullscreen uiDevice.clickTvPipMenuFullscreenButton() - assertTrue("\"Full screen\" button should open the app fullscreen", - wait { testApp.ui?.isFullscreen(uiDevice) ?: false }) + assertTrue( + "\"Full screen\" button should open the app fullscreen", + wait { testApp.ui?.isFullscreen(uiDevice) ?: false } + ) // Close the app uiDevice.pressBack() @@ -143,8 +156,10 @@ class TvPipMenuTests : TvPipTestBase() { // PiP menu should contain the Pause button uiDevice.findTvPipMenuElementWithDescription(pauseButtonDescription) - ?: fail("\"Pause\" button should be shown in Pip menu if there is an active " + - "playing media session.") + ?: fail( + "\"Pause\" button should be shown in Pip menu if there is an active " + + "playing media session." + ) // When we pause media, the button should change from Pause to Play uiDevice.clickTvPipMenuElementWithDescription(pauseButtonDescription) @@ -152,8 +167,10 @@ class TvPipMenuTests : TvPipTestBase() { assertFullscreenAndCloseButtonsAreShown() // PiP menu should contain the Play button now uiDevice.waitForTvPipMenuElementWithDescription(playButtonDescription) - ?: fail("\"Play\" button should be shown in Pip menu if there is an active " + - "paused media session.") + ?: fail( + "\"Play\" button should be shown in Pip menu if there is an active " + + "paused media session." + ) testApp.closePipWindow() } @@ -165,44 +182,47 @@ class TvPipMenuTests : TvPipTestBase() { enterPip_openMenu_assertShown() // PiP menu should contain "No-Op", "Off" and "Clear" buttons... - uiDevice.findTvPipMenuElementWithDescription(Components.PipActivity.MENU_ACTION_NO_OP) - ?: fail("\"No-Op\" button should be shown in Pip menu") - uiDevice.findTvPipMenuElementWithDescription(Components.PipActivity.MENU_ACTION_OFF) - ?: fail("\"Off\" button should be shown in Pip menu") - uiDevice.findTvPipMenuElementWithDescription(Components.PipActivity.MENU_ACTION_CLEAR) - ?: fail("\"Clear\" button should be shown in Pip menu") + uiDevice.findTvPipMenuElementWithDescription(ActivityOptions.Pip.MENU_ACTION_NO_OP) + ?: fail("\"No-Op\" button should be shown in Pip menu") + uiDevice.findTvPipMenuElementWithDescription(ActivityOptions.Pip.MENU_ACTION_OFF) + ?: fail("\"Off\" button should be shown in Pip menu") + uiDevice.findTvPipMenuElementWithDescription(ActivityOptions.Pip.MENU_ACTION_CLEAR) + ?: fail("\"Clear\" button should be shown in Pip menu") // ... and should also contain the "Full screen" and "Close" buttons. assertFullscreenAndCloseButtonsAreShown() - uiDevice.clickTvPipMenuElementWithDescription(Components.PipActivity.MENU_ACTION_OFF) + uiDevice.clickTvPipMenuElementWithDescription(ActivityOptions.Pip.MENU_ACTION_OFF) // Invoking the "Off" action should replace it with the "On" action/button and should // remove the "No-Op" action/button. "Clear" action/button should remain in the menu ... - uiDevice.waitForTvPipMenuElementWithDescription(Components.PipActivity.MENU_ACTION_ON) - ?: fail("\"On\" button should be shown in Pip for a corresponding custom action") - assertNull("\"No-Op\" button should not be shown in Pip menu", - uiDevice.findTvPipMenuElementWithDescription( - Components.PipActivity.MENU_ACTION_NO_OP)) - uiDevice.findTvPipMenuElementWithDescription(Components.PipActivity.MENU_ACTION_CLEAR) - ?: fail("\"Clear\" button should be shown in Pip menu") + uiDevice.waitForTvPipMenuElementWithDescription(ActivityOptions.Pip.MENU_ACTION_ON) + ?: fail("\"On\" button should be shown in Pip for a corresponding custom action") + assertNull( + "\"No-Op\" button should not be shown in Pip menu", + uiDevice.findTvPipMenuElementWithDescription(ActivityOptions.Pip.MENU_ACTION_NO_OP) + ) + uiDevice.findTvPipMenuElementWithDescription(ActivityOptions.Pip.MENU_ACTION_CLEAR) + ?: fail("\"Clear\" button should be shown in Pip menu") // ... as well as the "Full screen" and "Close" buttons. assertFullscreenAndCloseButtonsAreShown() - uiDevice.clickTvPipMenuElementWithDescription(Components.PipActivity.MENU_ACTION_CLEAR) + uiDevice.clickTvPipMenuElementWithDescription(ActivityOptions.Pip.MENU_ACTION_CLEAR) // Invoking the "Clear" action should remove all the custom actions and their corresponding // buttons, ... - uiDevice.waitUntilTvPipMenuElementWithDescriptionIsGone( - Components.PipActivity.MENU_ACTION_ON)?.also { - isGone -> if (!isGone) fail("\"On\" button should not be shown in Pip menu") - } - assertNull("\"Off\" button should not be shown in Pip menu", - uiDevice.findTvPipMenuElementWithDescription( - Components.PipActivity.MENU_ACTION_OFF)) - assertNull("\"Clear\" button should not be shown in Pip menu", - uiDevice.findTvPipMenuElementWithDescription( - Components.PipActivity.MENU_ACTION_CLEAR)) - assertNull("\"No-Op\" button should not be shown in Pip menu", - uiDevice.findTvPipMenuElementWithDescription( - Components.PipActivity.MENU_ACTION_NO_OP)) + uiDevice + .waitUntilTvPipMenuElementWithDescriptionIsGone(ActivityOptions.Pip.MENU_ACTION_ON) + ?.also { isGone -> if (!isGone) fail("\"On\" button should not be shown in Pip menu") } + assertNull( + "\"Off\" button should not be shown in Pip menu", + uiDevice.findTvPipMenuElementWithDescription(ActivityOptions.Pip.MENU_ACTION_OFF) + ) + assertNull( + "\"Clear\" button should not be shown in Pip menu", + uiDevice.findTvPipMenuElementWithDescription(ActivityOptions.Pip.MENU_ACTION_CLEAR) + ) + assertNull( + "\"No-Op\" button should not be shown in Pip menu", + uiDevice.findTvPipMenuElementWithDescription(ActivityOptions.Pip.MENU_ACTION_NO_OP) + ) // ... but the menu should still contain the "Full screen" and "Close" buttons. assertFullscreenAndCloseButtonsAreShown() @@ -217,26 +237,32 @@ class TvPipMenuTests : TvPipTestBase() { enterPip_openMenu_assertShown() // PiP menu should contain "No-Op", "Off" and "Clear" buttons for the custom actions... - uiDevice.findTvPipMenuElementWithDescription(Components.PipActivity.MENU_ACTION_NO_OP) - ?: fail("\"No-Op\" button should be shown in Pip menu") - uiDevice.findTvPipMenuElementWithDescription(Components.PipActivity.MENU_ACTION_OFF) - ?: fail("\"Off\" button should be shown in Pip menu") - uiDevice.findTvPipMenuElementWithDescription(Components.PipActivity.MENU_ACTION_CLEAR) - ?: fail("\"Clear\" button should be shown in Pip menu") + uiDevice.findTvPipMenuElementWithDescription(ActivityOptions.Pip.MENU_ACTION_NO_OP) + ?: fail("\"No-Op\" button should be shown in Pip menu") + uiDevice.findTvPipMenuElementWithDescription(ActivityOptions.Pip.MENU_ACTION_OFF) + ?: fail("\"Off\" button should be shown in Pip menu") + uiDevice.findTvPipMenuElementWithDescription(ActivityOptions.Pip.MENU_ACTION_CLEAR) + ?: fail("\"Clear\" button should be shown in Pip menu") // ... should also contain the "Full screen" and "Close" buttons, ... assertFullscreenAndCloseButtonsAreShown() // ... but should not contain media buttons. - assertNull("\"Play\" button should not be shown in menu when there are custom actions", - uiDevice.findTvPipMenuElementWithDescription(playButtonDescription)) - assertNull("\"Pause\" button should not be shown in menu when there are custom actions", - uiDevice.findTvPipMenuElementWithDescription(pauseButtonDescription)) - - uiDevice.clickTvPipMenuElementWithDescription(Components.PipActivity.MENU_ACTION_CLEAR) + assertNull( + "\"Play\" button should not be shown in menu when there are custom actions", + uiDevice.findTvPipMenuElementWithDescription(playButtonDescription) + ) + assertNull( + "\"Pause\" button should not be shown in menu when there are custom actions", + uiDevice.findTvPipMenuElementWithDescription(pauseButtonDescription) + ) + + uiDevice.clickTvPipMenuElementWithDescription(ActivityOptions.Pip.MENU_ACTION_CLEAR) // Invoking the "Clear" action should remove all the custom actions, which should bring up // media buttons... uiDevice.waitForTvPipMenuElementWithDescription(pauseButtonDescription) - ?: fail("\"Pause\" button should be shown in Pip menu if there is an active " + - "playing media session.") + ?: fail( + "\"Pause\" button should be shown in Pip menu if there is an active " + + "playing media session." + ) // ... while the "Full screen" and "Close" buttons should remain in the menu. assertFullscreenAndCloseButtonsAreShown() @@ -244,7 +270,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") @@ -252,8 +278,8 @@ class TvPipMenuTests : TvPipTestBase() { private fun assertFullscreenAndCloseButtonsAreShown() { uiDevice.findTvPipMenuCloseButton() - ?: fail("\"Close PIP\" button should be shown in Pip menu") + ?: fail("\"Close PIP\" button should be shown in Pip menu") uiDevice.findTvPipMenuFullscreenButton() - ?: fail("\"Full screen\" button should be shown in Pip menu") + ?: 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..90406c510bad 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 @@ -34,8 +34,8 @@ import org.junit.Before import org.junit.Test /** - * Test Pip Notifications on TV. - * To run this test: `atest WMShellFlickerTests:TvPipNotificationTests` + * Test Pip Notifications on TV. To run this test: `atest + * WMShellFlickerTests:TvPipNotificationTests` */ @RequiresDevice class TvPipNotificationTests : TvPipTestBase() { @@ -56,49 +56,58 @@ 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) }) + assertNotNull( + "Pip notification should have been posted", + waitForNotificationToAppear { it.isPipNotificationWithTitle(testApp.appName) } + ) testApp.closePipWindow() - assertTrue("Pip notification should have been dismissed", - waitForNotificationToDisappear { it.isPipNotificationWithTitle(testApp.appName) }) + assertTrue( + "Pip notification should have been dismissed", + waitForNotificationToDisappear { it.isPipNotificationWithTitle(testApp.appName) } + ) } @Test fun pipNotification_closeIntent() { testApp.launchViaIntent() - testApp.clickEnterPipButton() - - val notification: StatusBarNotification = waitForNotificationToAppear { - it.isPipNotificationWithTitle(testApp.appName) - } ?: fail("Pip notification should have been posted") - - notification.deleteIntent?.send() - ?: fail("Pip notification should contain `delete_intent`") - - assertTrue("Pip should have closed by sending the `delete_intent`", - testApp.waitUntilClosed()) - assertTrue("Pip notification should have been dismissed", - waitForNotificationToDisappear { it.isPipNotificationWithTitle(testApp.appName) }) + testApp.clickEnterPipButton(wmHelper) + + val notification: StatusBarNotification = + waitForNotificationToAppear { it.isPipNotificationWithTitle(testApp.appName) } + ?: fail("Pip notification should have been posted") + + notification.deleteIntent?.send() ?: fail("Pip notification should contain `delete_intent`") + + assertTrue( + "Pip should have closed by sending the `delete_intent`", + testApp.waitUntilClosed() + ) + assertTrue( + "Pip notification should have been dismissed", + waitForNotificationToDisappear { it.isPipNotificationWithTitle(testApp.appName) } + ) } @Test fun pipNotification_menuIntent() { - testApp.launchViaIntent() - testApp.clickEnterPipButton() + testApp.launchViaIntent(wmHelper) + testApp.clickEnterPipButton(wmHelper) - val notification: StatusBarNotification = waitForNotificationToAppear { - it.isPipNotificationWithTitle(testApp.appName) - } ?: fail("Pip notification should have been posted") + val notification: StatusBarNotification = + waitForNotificationToAppear { it.isPipNotificationWithTitle(testApp.appName) } + ?: fail("Pip notification should have been posted") notification.contentIntent?.send() ?: fail("Pip notification should contain `content_intent`") - assertNotNull("Pip menu should have been shown after sending `content_intent`", - uiDevice.waitForTvPipMenu()) + assertNotNull( + "Pip menu should have been shown after sending `content_intent`", + uiDevice.waitForTvPipMenu() + ) uiDevice.pressBack() testApp.closePipWindow() @@ -106,41 +115,44 @@ 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 { - it.isPipNotificationWithTitle(TITLE_MEDIA_SESSION_PLAYING) - } ?: fail("Pip notification with media session title should have been posted") + waitForNotificationToAppear { it.isPipNotificationWithTitle(TITLE_MEDIA_SESSION_PLAYING) } + ?: fail("Pip notification with media session title should have been posted") // ... and make sure "regular" PiP notification is now shown - assertNull("Regular notification should not have been posted", - findNotification { it.isPipNotificationWithTitle(testApp.appName) }) + assertNull( + "Regular notification should not have been posted", + findNotification { it.isPipNotificationWithTitle(testApp.appName) } + ) // Pause the media session. When paused the application updates the title for the media // session. This change should be reflected in the notification. testApp.pauseMedia() // Wait for the "paused" notification to show up... - waitForNotificationToAppear { - it.isPipNotificationWithTitle(TITLE_MEDIA_SESSION_PAUSED) - } ?: fail("Pip notification with media session title should have been posted") + waitForNotificationToAppear { it.isPipNotificationWithTitle(TITLE_MEDIA_SESSION_PAUSED) } + ?: fail("Pip notification with media session title should have been posted") // ... and make sure "playing" PiP notification is gone - assertNull("Regular notification should not have been posted", - findNotification { it.isPipNotificationWithTitle(TITLE_MEDIA_SESSION_PLAYING) }) + assertNull( + "Regular notification should not have been posted", + findNotification { it.isPipNotificationWithTitle(TITLE_MEDIA_SESSION_PLAYING) } + ) // Now stop the media session, which should revert the title to the "default" one. testApp.stopMedia() // Wait for the "regular" notification to show up... - waitForNotificationToAppear { - it.isPipNotificationWithTitle(testApp.appName) - } ?: fail("Pip notification with media session title should have been posted") + waitForNotificationToAppear { it.isPipNotificationWithTitle(testApp.appName) } + ?: fail("Pip notification with media session title should have been posted") // ... and make sure previous ("paused") notification is gone - assertNull("Regular notification should not have been posted", - findNotification { it.isPipNotificationWithTitle(TITLE_MEDIA_SESSION_PAUSED) }) + assertNull( + "Regular notification should not have been posted", + findNotification { it.isPipNotificationWithTitle(TITLE_MEDIA_SESSION_PAUSED) } + ) testApp.closePipWindow() } @@ -170,4 +182,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..6104b7bdacba 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 @@ -20,11 +20,11 @@ import android.app.ActivityManager import android.app.IActivityManager import android.app.IProcessObserver import android.os.SystemClock +import android.tools.device.helpers.wakeUpAndGoToHomeScreen +import android.tools.device.traces.parsers.WindowManagerStateHelper import android.view.Surface.ROTATION_0 import android.view.Surface.rotationToString -import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen 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() { @@ -67,7 +68,8 @@ abstract class TvPipTestBase : PipTestBase(rotationToString(ROTATION_0), ROTATIO fun start() { hasDied = false uiAutomation.adoptShellPermissionIdentity( - android.Manifest.permission.SET_ACTIVITY_WATCHER) + android.Manifest.permission.SET_ACTIVITY_WATCHER + ) activityManager.registerProcessObserver(this) } @@ -88,4 +90,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/pip/tv/TvUtils.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvUtils.kt index 1c663409b913..b0adbe1d07ce 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvUtils.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvUtils.kt @@ -33,32 +33,31 @@ private const val TV_PIP_MENU_FULLSCREEN_BUTTON_ID = "tv_pip_menu_fullscreen_but private const val FOCUS_ATTEMPTS = 10 private const val WAIT_TIME_MS = 3_000L -private val TV_PIP_MENU_SELECTOR = - By.res(SYSTEM_UI_PACKAGE_NAME, TV_PIP_MENU_ROOT_ID) +private val TV_PIP_MENU_SELECTOR = By.res(SYSTEM_UI_PACKAGE_NAME, TV_PIP_MENU_ROOT_ID) private val TV_PIP_MENU_BUTTONS_CONTAINER_SELECTOR = - By.res(SYSTEM_UI_PACKAGE_NAME, TV_PIP_MENU_BUTTONS_CONTAINER_ID) + By.res(SYSTEM_UI_PACKAGE_NAME, TV_PIP_MENU_BUTTONS_CONTAINER_ID) private val TV_PIP_MENU_CLOSE_BUTTON_SELECTOR = - By.res(SYSTEM_UI_PACKAGE_NAME, TV_PIP_MENU_CLOSE_BUTTON_ID) + By.res(SYSTEM_UI_PACKAGE_NAME, TV_PIP_MENU_CLOSE_BUTTON_ID) private val TV_PIP_MENU_FULLSCREEN_BUTTON_SELECTOR = - By.res(SYSTEM_UI_PACKAGE_NAME, TV_PIP_MENU_FULLSCREEN_BUTTON_ID) + By.res(SYSTEM_UI_PACKAGE_NAME, TV_PIP_MENU_FULLSCREEN_BUTTON_ID) fun UiDevice.waitForTvPipMenu(): UiObject2? = - wait(Until.findObject(TV_PIP_MENU_SELECTOR), WAIT_TIME_MS) + wait(Until.findObject(TV_PIP_MENU_SELECTOR), WAIT_TIME_MS) fun UiDevice.waitForTvPipMenuToClose(): Boolean = - wait(Until.gone(TV_PIP_MENU_SELECTOR), WAIT_TIME_MS) + wait(Until.gone(TV_PIP_MENU_SELECTOR), WAIT_TIME_MS) fun UiDevice.findTvPipMenuControls(): UiObject2? = - findTvPipMenuElement(TV_PIP_MENU_BUTTONS_CONTAINER_SELECTOR) + findTvPipMenuElement(TV_PIP_MENU_BUTTONS_CONTAINER_SELECTOR) fun UiDevice.findTvPipMenuCloseButton(): UiObject2? = - findTvPipMenuElement(TV_PIP_MENU_CLOSE_BUTTON_SELECTOR) + findTvPipMenuElement(TV_PIP_MENU_CLOSE_BUTTON_SELECTOR) fun UiDevice.findTvPipMenuFullscreenButton(): UiObject2? = - findTvPipMenuElement(TV_PIP_MENU_FULLSCREEN_BUTTON_SELECTOR) + findTvPipMenuElement(TV_PIP_MENU_FULLSCREEN_BUTTON_SELECTOR) fun UiDevice.findTvPipMenuElementWithDescription(desc: String): UiObject2? = - findTvPipMenuElement(By.desc(desc)) + findTvPipMenuElement(By.desc(desc)) private fun UiDevice.findTvPipMenuElement(selector: BySelector): UiObject2? = findObject(TV_PIP_MENU_SELECTOR)?.findObject(selector) @@ -70,11 +69,10 @@ fun UiDevice.waitForTvPipMenuElementWithDescription(desc: String): UiObject2? { // descendant and then retrieve the element from the menu and return to the caller of this // method. val elementSelector = By.desc(desc) - val menuContainingElementSelector = By.copy(TV_PIP_MENU_SELECTOR) - .hasDescendant(elementSelector) + val menuContainingElementSelector = By.copy(TV_PIP_MENU_SELECTOR).hasDescendant(elementSelector) return wait(Until.findObject(menuContainingElementSelector), WAIT_TIME_MS) - ?.findObject(elementSelector) + ?.findObject(elementSelector) } fun UiDevice.waitUntilTvPipMenuElementWithDescriptionIsGone(desc: String): Boolean? { @@ -86,18 +84,17 @@ fun UiDevice.waitUntilTvPipMenuElementWithDescriptionIsGone(desc: String): Boole fun UiDevice.clickTvPipMenuCloseButton() { focusOnAndClickTvPipMenuElement(TV_PIP_MENU_CLOSE_BUTTON_SELECTOR) || - error("Could not focus on the Close button") + error("Could not focus on the Close button") } fun UiDevice.clickTvPipMenuFullscreenButton() { focusOnAndClickTvPipMenuElement(TV_PIP_MENU_FULLSCREEN_BUTTON_SELECTOR) || - error("Could not focus on the Fullscreen button") + error("Could not focus on the Fullscreen button") } fun UiDevice.clickTvPipMenuElementWithDescription(desc: String) { - focusOnAndClickTvPipMenuElement(By.desc(desc) - .pkg(SYSTEM_UI_PACKAGE_NAME)) || - error("Could not focus on the Pip menu object with \"$desc\" description") + focusOnAndClickTvPipMenuElement(By.desc(desc).pkg(SYSTEM_UI_PACKAGE_NAME)) || + error("Could not focus on the Pip menu object with \"$desc\" description") // So apparently Accessibility framework on TV is not very reliable and sometimes the state of // the tree of accessibility nodes as seen by the accessibility clients kind of lags behind of // the "real" state of the "UI tree". It seems, however, that moving focus around the tree @@ -110,7 +107,8 @@ fun UiDevice.clickTvPipMenuElementWithDescription(desc: String) { private fun UiDevice.focusOnAndClickTvPipMenuElement(selector: BySelector): Boolean { repeat(FOCUS_ATTEMPTS) { - val element = findTvPipMenuElement(selector) + val element = + findTvPipMenuElement(selector) ?: error("The Pip Menu element we try to focus on is gone.") if (element.isFocusedOrHasFocusedChild) { @@ -119,10 +117,11 @@ private fun UiDevice.focusOnAndClickTvPipMenuElement(selector: BySelector): Bool } findTvPipMenuElement(By.focused(true))?.let { focused -> - if (element.visibleCenter.x < focused.visibleCenter.x) - pressDPadLeft() else pressDPadRight() + if (element.visibleCenter.x < focused.visibleCenter.x) pressDPadLeft() + else pressDPadRight() waitForIdle() - } ?: error("Pip menu does not contain a focused element") + } + ?: error("Pip menu does not contain a focused element") } return false @@ -155,9 +154,8 @@ private fun UiDevice.moveFocus() { fun UiDevice.pressWindowKey() = pressKeyCode(KeyEvent.KEYCODE_WINDOW) -fun UiObject2.isFullscreen(uiDevice: UiDevice): Boolean = visibleBounds.run { - height() == uiDevice.displayHeight && width() == uiDevice.displayWidth -} +fun UiObject2.isFullscreen(uiDevice: UiDevice): Boolean = + visibleBounds.run { height() == uiDevice.displayHeight && width() == uiDevice.displayWidth } val UiObject2.isFocusedOrHasFocusedChild: Boolean get() = isFocused || findObject(By.focused(true)) != null 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..0c9c16153ea3 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt @@ -0,0 +1,151 @@ +/* + * 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.Presubmit +import android.tools.common.datatypes.component.ComponentNameMatcher +import android.tools.common.datatypes.component.EdgeExtensionComponentMatcher +import android.tools.device.flicker.junit.FlickerParametersRunnerFactory +import android.tools.device.flicker.legacy.FlickerBuilder +import android.tools.device.flicker.legacy.FlickerTest +import android.tools.device.flicker.legacy.FlickerTestFactory +import androidx.test.filters.RequiresDevice +import com.android.wm.shell.flicker.SPLIT_SCREEN_DIVIDER_COMPONENT +import com.android.wm.shell.flicker.appWindowIsVisibleAtEnd +import com.android.wm.shell.flicker.appWindowIsVisibleAtStart +import com.android.wm.shell.flicker.appWindowKeepVisible +import com.android.wm.shell.flicker.layerKeepVisible +import com.android.wm.shell.flicker.splitAppLayerBoundsKeepVisible +import com.android.wm.shell.flicker.splitScreenDividerIsVisibleAtEnd +import com.android.wm.shell.flicker.splitScreenDividerIsVisibleAtStart +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) +class CopyContentInSplit(flicker: FlickerTest) : SplitScreenBase(flicker) { + private val textEditApp = SplitScreenUtils.getIme(instrumentation) + private val MagnifierLayer = ComponentNameMatcher("", "magnifier surface bbq wrapper#") + private val PopupWindowLayer = ComponentNameMatcher("", "PopupWindow:") + + override val transition: FlickerBuilder.() -> Unit + get() = { + super.transition(this) + setup { SplitScreenUtils.enterSplit(wmHelper, tapl, device, primaryApp, textEditApp) } + transitions { + SplitScreenUtils.copyContentInSplit( + instrumentation, + device, + primaryApp, + textEditApp + ) + } + } + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun cujCompleted() { + flicker.appWindowIsVisibleAtStart(primaryApp) + flicker.appWindowIsVisibleAtStart(textEditApp) + flicker.splitScreenDividerIsVisibleAtStart() + + flicker.appWindowIsVisibleAtEnd(primaryApp) + flicker.appWindowIsVisibleAtEnd(textEditApp) + flicker.splitScreenDividerIsVisibleAtEnd() + + // The validation of copied text is already done in SplitScreenUtils.copyContentInSplit() + } + + @Presubmit + @Test + fun splitScreenDividerKeepVisible() = flicker.layerKeepVisible(SPLIT_SCREEN_DIVIDER_COMPONENT) + + @Presubmit @Test fun primaryAppLayerKeepVisible() = flicker.layerKeepVisible(primaryApp) + + @Presubmit @Test fun textEditAppLayerKeepVisible() = flicker.layerKeepVisible(textEditApp) + + @Presubmit + @Test + fun primaryAppBoundsKeepVisible() = + flicker.splitAppLayerBoundsKeepVisible( + primaryApp, + landscapePosLeft = tapl.isTablet, + portraitPosTop = false + ) + + @Presubmit + @Test + fun textEditAppBoundsKeepVisible() = + flicker.splitAppLayerBoundsKeepVisible( + textEditApp, + landscapePosLeft = !tapl.isTablet, + portraitPosTop = true + ) + + @Presubmit @Test fun primaryAppWindowKeepVisible() = flicker.appWindowKeepVisible(primaryApp) + + @Presubmit @Test fun textEditAppWindowKeepVisible() = flicker.appWindowKeepVisible(textEditApp) + + /** {@inheritDoc} */ + @Presubmit @Test override fun entireScreenCovered() = super.entireScreenCovered() + + /** {@inheritDoc} */ + @Presubmit + @Test + override fun visibleLayersShownMoreThanOneConsecutiveEntry() { + flicker.assertLayers { + this.visibleLayersShownMoreThanOneConsecutiveEntry( + ignoreLayers = + listOf( + ComponentNameMatcher.SPLASH_SCREEN, + ComponentNameMatcher.SNAPSHOT, + ComponentNameMatcher.IME_SNAPSHOT, + EdgeExtensionComponentMatcher(), + MagnifierLayer, + PopupWindowLayer + ) + ) + } + } + + /** {@inheritDoc} */ + @FlakyTest(bugId = 264241018) + @Test + override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = + super.visibleWindowsShownMoreThanOneConsecutiveEntry() + + companion object { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): List<FlickerTest> { + return FlickerTestFactory.nonRotationTests() + } + } +} 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..1b55f3975e1c --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByDivider.kt @@ -0,0 +1,187 @@ +/* + * 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.tools.device.flicker.junit.FlickerParametersRunnerFactory +import android.tools.device.flicker.legacy.FlickerBuilder +import android.tools.device.flicker.legacy.FlickerTest +import android.tools.device.flicker.legacy.FlickerTestFactory +import android.tools.device.helpers.WindowUtils +import androidx.test.filters.RequiresDevice +import com.android.wm.shell.flicker.appWindowBecomesInvisible +import com.android.wm.shell.flicker.appWindowIsVisibleAtEnd +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.splitScreenDismissed +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) +class DismissSplitScreenByDivider(flicker: FlickerTest) : SplitScreenBase(flicker) { + + override val transition: FlickerBuilder.() -> Unit + get() = { + super.transition(this) + setup { SplitScreenUtils.enterSplit(wmHelper, tapl, device, primaryApp, secondaryApp) } + transitions { + if (tapl.isTablet) { + SplitScreenUtils.dragDividerToDismissSplit( + device, + wmHelper, + dragToRight = false, + dragToBottom = true + ) + } else { + SplitScreenUtils.dragDividerToDismissSplit( + device, + wmHelper, + dragToRight = true, + dragToBottom = true + ) + } + wmHelper.StateSyncBuilder().withFullScreenApp(secondaryApp).waitForAndVerify() + } + } + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun cujCompleted() = flicker.splitScreenDismissed(primaryApp, secondaryApp, toHome = false) + + @Presubmit + @Test + fun splitScreenDividerBecomesInvisible() = flicker.splitScreenDividerBecomesInvisible() + + @Presubmit + @Test + fun primaryAppLayerBecomesInvisible() = flicker.layerBecomesInvisible(primaryApp) + + @Presubmit + @Test + fun secondaryAppLayerIsVisibleAtEnd() = flicker.layerIsVisibleAtEnd(secondaryApp) + + @Presubmit + @Test + fun primaryAppBoundsBecomesInvisible() = + flicker.splitAppLayerBoundsBecomesInvisible( + primaryApp, + landscapePosLeft = tapl.isTablet, + portraitPosTop = false + ) + + @Presubmit + @Test + fun secondaryAppBoundsIsFullscreenAtEnd() { + flicker.assertLayers { + this.isVisible(secondaryApp).then().isInvisible(secondaryApp).then().invoke( + "secondaryAppBoundsIsFullscreenAtEnd" + ) { + val displayBounds = WindowUtils.getDisplayBounds(flicker.scenario.endRotation) + it.visibleRegion(secondaryApp).coversExactly(displayBounds) + } + } + } + + @Presubmit + @Test + fun primaryAppWindowBecomesInvisible() = flicker.appWindowBecomesInvisible(primaryApp) + + @Presubmit + @Test + fun secondaryAppWindowIsVisibleAtEnd() = flicker.appWindowIsVisibleAtEnd(secondaryApp) + + /** {@inheritDoc} */ + @Postsubmit @Test override fun entireScreenCovered() = super.entireScreenCovered() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun navBarLayerIsVisibleAtStartAndEnd() = super.navBarLayerIsVisibleAtStartAndEnd() + + /** {@inheritDoc} */ + @FlakyTest(bugId = 206753786) + @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<FlickerTest> { + return FlickerTestFactory.nonRotationTests() + } + } +} 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..2e81b30d2e9a --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByGoHome.kt @@ -0,0 +1,168 @@ +/* + * 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.Presubmit +import android.tools.device.flicker.junit.FlickerParametersRunnerFactory +import android.tools.device.flicker.legacy.FlickerBuilder +import android.tools.device.flicker.legacy.FlickerTest +import android.tools.device.flicker.legacy.FlickerTestFactory +import androidx.test.filters.RequiresDevice +import com.android.wm.shell.flicker.appWindowBecomesInvisible +import com.android.wm.shell.flicker.layerBecomesInvisible +import com.android.wm.shell.flicker.splitAppLayerBoundsBecomesInvisible +import com.android.wm.shell.flicker.splitScreenDismissed +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) +class DismissSplitScreenByGoHome(flicker: FlickerTest) : SplitScreenBase(flicker) { + + override val transition: FlickerBuilder.() -> Unit + get() = { + super.transition(this) + setup { SplitScreenUtils.enterSplit(wmHelper, tapl, device, primaryApp, secondaryApp) } + transitions { + tapl.goHome() + wmHelper.StateSyncBuilder().withHomeActivityVisible().waitForAndVerify() + } + } + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun cujCompleted() = flicker.splitScreenDismissed(primaryApp, secondaryApp, toHome = true) + + @Presubmit + @Test + fun splitScreenDividerBecomesInvisible() = flicker.splitScreenDividerBecomesInvisible() + + @FlakyTest(bugId = 241525302) + @Test + fun primaryAppLayerBecomesInvisible() = flicker.layerBecomesInvisible(primaryApp) + + // TODO(b/245472831): Move back to presubmit after shell transitions landing. + @FlakyTest(bugId = 245472831) + @Test + fun secondaryAppLayerBecomesInvisible() = flicker.layerBecomesInvisible(secondaryApp) + + // TODO(b/245472831): Move back to presubmit after shell transitions landing. + @FlakyTest(bugId = 245472831) + @Test + fun primaryAppBoundsBecomesInvisible() = + flicker.splitAppLayerBoundsBecomesInvisible( + primaryApp, + landscapePosLeft = tapl.isTablet, + portraitPosTop = false + ) + + @FlakyTest(bugId = 250530241) + @Test + fun secondaryAppBoundsBecomesInvisible() = + flicker.splitAppLayerBoundsBecomesInvisible( + secondaryApp, + landscapePosLeft = !tapl.isTablet, + portraitPosTop = true + ) + + @Presubmit + @Test + fun primaryAppWindowBecomesInvisible() = flicker.appWindowBecomesInvisible(primaryApp) + + @Presubmit + @Test + fun secondaryAppWindowBecomesInvisible() = flicker.appWindowBecomesInvisible(secondaryApp) + + /** {@inheritDoc} */ + @FlakyTest(bugId = 251268711) + @Test + override fun entireScreenCovered() = super.entireScreenCovered() + + /** {@inheritDoc} */ + @Presubmit + @Test + override fun navBarLayerIsVisibleAtStartAndEnd() = super.navBarLayerIsVisibleAtStartAndEnd() + + /** {@inheritDoc} */ + @FlakyTest(bugId = 206753786) + @Test + override fun navBarLayerPositionAtStartAndEnd() = super.navBarLayerPositionAtStartAndEnd() + + /** {@inheritDoc} */ + @Presubmit + @Test + override fun navBarWindowIsAlwaysVisible() = super.navBarWindowIsAlwaysVisible() + + /** {@inheritDoc} */ + @Presubmit + @Test + override fun statusBarLayerIsVisibleAtStartAndEnd() = + super.statusBarLayerIsVisibleAtStartAndEnd() + + /** {@inheritDoc} */ + @Presubmit + @Test + override fun statusBarLayerPositionAtStartAndEnd() = super.statusBarLayerPositionAtStartAndEnd() + + /** {@inheritDoc} */ + @Presubmit + @Test + override fun statusBarWindowIsAlwaysVisible() = super.statusBarWindowIsAlwaysVisible() + + /** {@inheritDoc} */ + @Presubmit + @Test + override fun taskBarLayerIsVisibleAtStartAndEnd() = super.taskBarLayerIsVisibleAtStartAndEnd() + + /** {@inheritDoc} */ + @Presubmit + @Test + override fun taskBarWindowIsAlwaysVisible() = super.taskBarWindowIsAlwaysVisible() + + /** {@inheritDoc} */ + @FlakyTest + @Test + override fun visibleLayersShownMoreThanOneConsecutiveEntry() = + super.visibleLayersShownMoreThanOneConsecutiveEntry() + + /** {@inheritDoc} */ + @Presubmit + @Test + override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = + super.visibleWindowsShownMoreThanOneConsecutiveEntry() + + companion object { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): List<FlickerTest> { + return FlickerTestFactory.nonRotationTests() + } + } +} 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..5180791276a2 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DragDividerToResize.kt @@ -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.flicker.splitscreen + +import android.platform.test.annotations.FlakyTest +import android.platform.test.annotations.IwTest +import android.platform.test.annotations.Presubmit +import android.tools.device.flicker.junit.FlickerParametersRunnerFactory +import android.tools.device.flicker.legacy.FlickerBuilder +import android.tools.device.flicker.legacy.FlickerTest +import android.tools.device.flicker.legacy.FlickerTestFactory +import androidx.test.filters.RequiresDevice +import com.android.wm.shell.flicker.SPLIT_SCREEN_DIVIDER_COMPONENT +import com.android.wm.shell.flicker.appWindowIsVisibleAtEnd +import com.android.wm.shell.flicker.appWindowIsVisibleAtStart +import com.android.wm.shell.flicker.appWindowKeepVisible +import com.android.wm.shell.flicker.layerKeepVisible +import com.android.wm.shell.flicker.splitAppLayerBoundsChanges +import com.android.wm.shell.flicker.splitScreenDividerIsVisibleAtEnd +import com.android.wm.shell.flicker.splitScreenDividerIsVisibleAtStart +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 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) +class DragDividerToResize(flicker: FlickerTest) : SplitScreenBase(flicker) { + + override val transition: FlickerBuilder.() -> Unit + get() = { + super.transition(this) + setup { SplitScreenUtils.enterSplit(wmHelper, tapl, device, primaryApp, secondaryApp) } + transitions { SplitScreenUtils.dragDividerToResizeAndWait(device, wmHelper) } + } + + @Before + fun before() { + Assume.assumeTrue(tapl.isTablet || !flicker.scenario.isLandscapeOrSeascapeAtStart) + } + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun cujCompleted() { + flicker.appWindowIsVisibleAtStart(primaryApp) + flicker.appWindowIsVisibleAtStart(secondaryApp) + flicker.splitScreenDividerIsVisibleAtStart() + + flicker.appWindowIsVisibleAtEnd(primaryApp) + flicker.appWindowIsVisibleAtEnd(secondaryApp) + flicker.splitScreenDividerIsVisibleAtEnd() + + // TODO(b/246490534): Add validation for resized app after withAppTransitionIdle is + // robust enough to get the correct end state. + } + + @Presubmit + @Test + fun splitScreenDividerKeepVisible() = flicker.layerKeepVisible(SPLIT_SCREEN_DIVIDER_COMPONENT) + + @Presubmit + @Test + fun primaryAppLayerVisibilityChanges() { + flicker.assertLayers { + this.isVisible(secondaryApp) + .then() + .isInvisible(secondaryApp) + .then() + .isVisible(secondaryApp) + } + } + + @Presubmit + @Test + fun secondaryAppLayerVisibilityChanges() { + flicker.assertLayers { + this.isVisible(secondaryApp) + .then() + .isInvisible(secondaryApp) + .then() + .isVisible(secondaryApp) + } + } + + @Presubmit @Test fun primaryAppWindowKeepVisible() = flicker.appWindowKeepVisible(primaryApp) + + @Presubmit + @Test + fun secondaryAppWindowKeepVisible() = flicker.appWindowKeepVisible(secondaryApp) + + @FlakyTest(bugId = 245472831) + @Test + fun primaryAppBoundsChanges() { + flicker.splitAppLayerBoundsChanges( + primaryApp, + landscapePosLeft = true, + portraitPosTop = false + ) + } + + @Presubmit + @Test + fun secondaryAppBoundsChanges() = + flicker.splitAppLayerBoundsChanges( + secondaryApp, + landscapePosLeft = false, + portraitPosTop = true + ) + + companion object { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): List<FlickerTest> { + return FlickerTestFactory.nonRotationTests() + } + } +} 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..69da1e29a19c --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromAllApps.kt @@ -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.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.tools.common.NavBar +import android.tools.device.flicker.junit.FlickerParametersRunnerFactory +import android.tools.device.flicker.legacy.FlickerBuilder +import android.tools.device.flicker.legacy.FlickerTest +import android.tools.device.flicker.legacy.FlickerTestFactory +import androidx.test.filters.RequiresDevice +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.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 com.android.wm.shell.flicker.splitScreenEntered +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) +class EnterSplitScreenByDragFromAllApps(flicker: FlickerTest) : SplitScreenBase(flicker) { + + @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`) + SplitScreenUtils.waitForSplitComplete(wmHelper, primaryApp, secondaryApp) + } + } + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun cujCompleted() = + flicker.splitScreenEntered( + primaryApp, + secondaryApp, + fromOtherApp = false, + appExistAtStart = false + ) + + @FlakyTest(bugId = 245472831) + @Test + fun splitScreenDividerBecomesVisible() { + flicker.splitScreenDividerBecomesVisible() + } + + // TODO(b/245472831): Back to splitScreenDividerBecomesVisible after shell transition ready. + @Presubmit + @Test + fun splitScreenDividerIsVisibleAtEnd() { + flicker.assertLayersEnd { this.isVisible(SPLIT_SCREEN_DIVIDER_COMPONENT) } + } + + @Presubmit @Test fun primaryAppLayerIsVisibleAtEnd() = flicker.layerIsVisibleAtEnd(primaryApp) + + @Presubmit + @Test + fun secondaryAppLayerBecomesVisible() = flicker.layerBecomesVisible(secondaryApp) + + @Presubmit + @Test + fun primaryAppBoundsIsVisibleAtEnd() = + flicker.splitAppLayerBoundsIsVisibleAtEnd( + primaryApp, + landscapePosLeft = false, + portraitPosTop = false + ) + + @Presubmit + @Test + fun secondaryAppBoundsBecomesVisible() = + flicker.splitAppLayerBoundsBecomesVisibleByDrag(secondaryApp) + + @Presubmit + @Test + fun primaryAppWindowIsVisibleAtEnd() = flicker.appWindowIsVisibleAtEnd(primaryApp) + + @Presubmit + @Test + fun secondaryAppWindowBecomesVisible() = flicker.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<FlickerTest> { + return FlickerTestFactory.nonRotationTests( + // TODO(b/176061063):The 3 buttons of nav bar do not exist in the hierarchy. + supportedNavigationModes = listOf(NavBar.MODE_GESTURAL) + ) + } + } +} 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..1773846c18e9 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromNotification.kt @@ -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.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.tools.common.NavBar +import android.tools.device.flicker.junit.FlickerParametersRunnerFactory +import android.tools.device.flicker.legacy.FlickerBuilder +import android.tools.device.flicker.legacy.FlickerTest +import android.tools.device.flicker.legacy.FlickerTestFactory +import androidx.test.filters.RequiresDevice +import com.android.wm.shell.flicker.SPLIT_SCREEN_DIVIDER_COMPONENT +import com.android.wm.shell.flicker.appWindowIsVisibleAtEnd +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 com.android.wm.shell.flicker.splitScreenEntered +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) +class EnterSplitScreenByDragFromNotification(flicker: FlickerTest) : SplitScreenBase(flicker) { + + private val sendNotificationApp = SplitScreenUtils.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) + sendNotificationApp.postNotification(wmHelper) + tapl.goHome() + primaryApp.launchViaIntent(wmHelper) + } + transitions { + SplitScreenUtils.dragFromNotificationToSplit(instrumentation, device, wmHelper) + SplitScreenUtils.waitForSplitComplete(wmHelper, primaryApp, sendNotificationApp) + } + teardown { sendNotificationApp.exit(wmHelper) } + } + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun cujCompleted() = + flicker.splitScreenEntered(primaryApp, sendNotificationApp, fromOtherApp = false) + + @FlakyTest(bugId = 245472831) + @Test + fun splitScreenDividerBecomesVisible() { + flicker.splitScreenDividerBecomesVisible() + } + + // TODO(b/245472831): Back to splitScreenDividerBecomesVisible after shell transition ready. + @Presubmit + @Test + fun splitScreenDividerIsVisibleAtEnd() { + flicker.assertLayersEnd { this.isVisible(SPLIT_SCREEN_DIVIDER_COMPONENT) } + } + + @Presubmit @Test fun primaryAppLayerIsVisibleAtEnd() = flicker.layerIsVisibleAtEnd(primaryApp) + + @Presubmit + @Test + fun secondaryAppLayerBecomesVisible() { + flicker.layerBecomesVisible(sendNotificationApp) + } + + @Presubmit + @Test + fun primaryAppBoundsIsVisibleAtEnd() = + flicker.splitAppLayerBoundsIsVisibleAtEnd( + primaryApp, + landscapePosLeft = false, + portraitPosTop = false + ) + + @Presubmit + @Test + fun secondaryAppBoundsBecomesVisible() = + flicker.splitAppLayerBoundsBecomesVisibleByDrag(sendNotificationApp) + + @Presubmit + @Test + fun primaryAppWindowIsVisibleAtEnd() = flicker.appWindowIsVisibleAtEnd(primaryApp) + + @Presubmit + @Test + fun secondaryAppWindowIsVisibleAtEnd() = flicker.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<FlickerTest> { + return FlickerTestFactory.nonRotationTests( + // TODO(b/176061063):The 3 buttons of nav bar do not exist in the hierarchy. + supportedNavigationModes = listOf(NavBar.MODE_GESTURAL) + ) + } + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromShortcut.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromShortcut.kt new file mode 100644 index 000000000000..c1977e9e82f7 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromShortcut.kt @@ -0,0 +1,143 @@ +/* + * 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.Presubmit +import android.tools.common.NavBar +import android.tools.device.flicker.junit.FlickerParametersRunnerFactory +import android.tools.device.flicker.legacy.FlickerBuilder +import android.tools.device.flicker.legacy.FlickerTest +import android.tools.device.flicker.legacy.FlickerTestFactory +import androidx.test.filters.RequiresDevice +import com.android.wm.shell.flicker.appWindowIsVisibleAtEnd +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 com.android.wm.shell.flicker.splitScreenEntered +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 a shortcut. This test is only for large screen devices. + * + * To run this test: `atest WMShellFlickerTests:EnterSplitScreenByDragFromShortcut` + */ +@RequiresDevice +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class EnterSplitScreenByDragFromShortcut(flicker: FlickerTest) : SplitScreenBase(flicker) { + + @Before + fun before() { + Assume.assumeTrue(tapl.isTablet) + } + + override val transition: FlickerBuilder.() -> Unit + get() = { + super.transition(this) + setup { + tapl.goHome() + SplitScreenUtils.createShortcutOnHotseatIfNotExist(tapl, secondaryApp.appName) + primaryApp.launchViaIntent(wmHelper) + } + transitions { + tapl.launchedAppState.taskbar + .getAppIcon(secondaryApp.appName) + .openDeepShortcutMenu() + .getMenuItem("Split Screen Secondary Activity") + .dragToSplitscreen(secondaryApp.`package`, primaryApp.`package`) + SplitScreenUtils.waitForSplitComplete(wmHelper, primaryApp, secondaryApp) + } + } + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun cujCompleted() = + flicker.splitScreenEntered( + primaryApp, + secondaryApp, + fromOtherApp = false, + appExistAtStart = false + ) + + @Presubmit + @Test + fun splitScreenDividerBecomesVisible() = flicker.splitScreenDividerBecomesVisible() + + @Presubmit @Test fun primaryAppLayerIsVisibleAtEnd() = flicker.layerIsVisibleAtEnd(primaryApp) + + @Presubmit + @Test + fun secondaryAppLayerBecomesVisible() = flicker.layerBecomesVisible(secondaryApp) + + @Presubmit + @Test + fun primaryAppBoundsIsVisibleAtEnd() = + flicker.splitAppLayerBoundsIsVisibleAtEnd( + primaryApp, + landscapePosLeft = false, + portraitPosTop = false + ) + + @Presubmit + @Test + fun secondaryAppBoundsBecomesVisible() = + flicker.splitAppLayerBoundsBecomesVisibleByDrag(secondaryApp) + + @Presubmit + @Test + fun primaryAppWindowIsVisibleAtEnd() = flicker.appWindowIsVisibleAtEnd(primaryApp) + + @Presubmit + @Test + fun secondaryAppWindowBecomesVisible() { + flicker.assertWm { + this.notContains(secondaryApp) + .then() + .isAppWindowInvisible(secondaryApp, isOptional = true) + .then() + .isAppWindowVisible(secondaryApp) + } + } + + /** {@inheritDoc} */ + @FlakyTest(bugId = 241523824) + @Test + override fun entireScreenCovered() = super.entireScreenCovered() + + companion object { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): List<FlickerTest> { + return FlickerTestFactory.nonRotationTests( + // TODO(b/176061063):The 3 buttons of nav bar do not exist in the hierarchy. + supportedNavigationModes = listOf(NavBar.MODE_GESTURAL) + ) + } + } +} 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..3bea66ef0a27 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromTaskbar.kt @@ -0,0 +1,199 @@ +/* + * 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.tools.common.NavBar +import android.tools.device.flicker.junit.FlickerParametersRunnerFactory +import android.tools.device.flicker.legacy.FlickerBuilder +import android.tools.device.flicker.legacy.FlickerTest +import android.tools.device.flicker.legacy.FlickerTestFactory +import androidx.test.filters.RequiresDevice +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.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 com.android.wm.shell.flicker.splitScreenEntered +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) +class EnterSplitScreenByDragFromTaskbar(flicker: FlickerTest) : SplitScreenBase(flicker) { + + @Before + fun before() { + Assume.assumeTrue(tapl.isTablet) + } + + /** {@inheritDoc} */ + override val transition: FlickerBuilder.() -> Unit + get() = { + super.transition(this) + setup { + tapl.goHome() + SplitScreenUtils.createShortcutOnHotseatIfNotExist(tapl, secondaryApp.appName) + primaryApp.launchViaIntent(wmHelper) + } + transitions { + tapl.launchedAppState.taskbar + .getAppIcon(secondaryApp.appName) + .dragToSplitscreen(secondaryApp.`package`, primaryApp.`package`) + SplitScreenUtils.waitForSplitComplete(wmHelper, primaryApp, secondaryApp) + } + } + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun cujCompleted() = + flicker.splitScreenEntered( + primaryApp, + secondaryApp, + fromOtherApp = false, + appExistAtStart = false + ) + + @FlakyTest(bugId = 245472831) + @Test + fun splitScreenDividerBecomesVisible() { + flicker.splitScreenDividerBecomesVisible() + } + + // TODO(b/245472831): Back to splitScreenDividerBecomesVisible after shell transition ready. + @Presubmit + @Test + fun splitScreenDividerIsVisibleAtEnd() { + flicker.assertLayersEnd { this.isVisible(SPLIT_SCREEN_DIVIDER_COMPONENT) } + } + + @Presubmit @Test fun primaryAppLayerIsVisibleAtEnd() = flicker.layerIsVisibleAtEnd(primaryApp) + + @Presubmit + @Test + fun secondaryAppLayerBecomesVisible() { + flicker.layerBecomesVisible(secondaryApp) + } + + @Presubmit + @Test + fun primaryAppBoundsIsVisibleAtEnd() = + flicker.splitAppLayerBoundsIsVisibleAtEnd( + primaryApp, + landscapePosLeft = false, + portraitPosTop = false + ) + + @Presubmit + @Test + fun secondaryAppBoundsBecomesVisible() = + flicker.splitAppLayerBoundsBecomesVisibleByDrag(secondaryApp) + + @Presubmit + @Test + fun primaryAppWindowIsVisibleAtEnd() = flicker.appWindowIsVisibleAtEnd(primaryApp) + + @Presubmit + @Test + fun secondaryAppWindowBecomesVisible() = flicker.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<FlickerTest> { + return FlickerTestFactory.nonRotationTests( + supportedNavigationModes = listOf(NavBar.MODE_GESTURAL) + ) + } + } +} 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..c45387722a49 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenFromOverview.kt @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2022 The Android Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.Presubmit +import android.tools.device.flicker.junit.FlickerParametersRunnerFactory +import android.tools.device.flicker.legacy.FlickerBuilder +import android.tools.device.flicker.legacy.FlickerTest +import android.tools.device.flicker.legacy.FlickerTestFactory +import androidx.test.filters.RequiresDevice +import com.android.wm.shell.flicker.appWindowBecomesVisible +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 com.android.wm.shell.flicker.splitScreenEntered +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) +class EnterSplitScreenFromOverview(flicker: FlickerTest) : SplitScreenBase(flicker) { + override val transition: FlickerBuilder.() -> Unit + get() = { + super.transition(this) + setup { + primaryApp.launchViaIntent(wmHelper) + secondaryApp.launchViaIntent(wmHelper) + tapl.goHome() + wmHelper + .StateSyncBuilder() + .withAppTransitionIdle() + .withHomeActivityVisible() + .waitForAndVerify() + } + transitions { + SplitScreenUtils.splitFromOverview(tapl, device) + SplitScreenUtils.waitForSplitComplete(wmHelper, primaryApp, secondaryApp) + } + } + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun cujCompleted() = flicker.splitScreenEntered(primaryApp, secondaryApp, fromOtherApp = true) + + @Presubmit + @Test + fun splitScreenDividerBecomesVisible() = flicker.splitScreenDividerBecomesVisible() + + @Presubmit @Test fun primaryAppLayerIsVisibleAtEnd() = flicker.layerIsVisibleAtEnd(primaryApp) + + @Presubmit + @Test + fun secondaryAppLayerBecomesVisible() = flicker.layerBecomesVisible(secondaryApp) + + @Presubmit + @Test + fun primaryAppBoundsIsVisibleAtEnd() = + flicker.splitAppLayerBoundsIsVisibleAtEnd( + primaryApp, + landscapePosLeft = tapl.isTablet, + portraitPosTop = false + ) + + @Presubmit + @Test + fun secondaryAppBoundsBecomesVisible() { + flicker.splitAppLayerBoundsBecomesVisible( + secondaryApp, + landscapePosLeft = !tapl.isTablet, + portraitPosTop = true + ) + } + + @Presubmit + @Test + fun primaryAppWindowBecomesVisible() = flicker.appWindowBecomesVisible(primaryApp) + + @Presubmit + @Test + fun secondaryAppWindowBecomesVisible() = flicker.appWindowBecomesVisible(secondaryApp) + + /** {@inheritDoc} */ + @FlakyTest(bugId = 251269324) + @Test + override fun entireScreenCovered() = super.entireScreenCovered() + + /** {@inheritDoc} */ + @FlakyTest(bugId = 252736515) + @Test + override fun visibleLayersShownMoreThanOneConsecutiveEntry() = + super.visibleLayersShownMoreThanOneConsecutiveEntry() + + companion object { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): List<FlickerTest> { + return FlickerTestFactory.nonRotationTests() + } + } +} 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..7abdc06820d6 --- /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 android.tools.device.flicker.legacy.FlickerBuilder +import android.tools.device.flicker.legacy.FlickerTest +import com.android.server.wm.flicker.helpers.setRotation +import com.android.wm.shell.flicker.BaseTest + +abstract class SplitScreenBase(flicker: FlickerTest) : BaseTest(flicker) { + protected val context: Context = instrumentation.context + protected val primaryApp = SplitScreenUtils.getPrimary(instrumentation) + protected val secondaryApp = SplitScreenUtils.getSecondary(instrumentation) + + /** {@inheritDoc} */ + override val transition: FlickerBuilder.() -> Unit + get() = { + setup { + tapl.setEnableRotation(true) + setRotation(flicker.scenario.startRotation) + tapl.setExpectedRotation(flicker.scenario.startRotation.value) + tapl.workspace.switchToOverview().dismissAllTasks() + } + teardown { + primaryApp.exit(wmHelper) + secondaryApp.exit(wmHelper) + } + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SplitScreenUtils.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SplitScreenUtils.kt new file mode 100644 index 000000000000..62936e0f5ca8 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SplitScreenUtils.kt @@ -0,0 +1,376 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.splitscreen + +import android.app.Instrumentation +import android.graphics.Point +import android.os.SystemClock +import android.tools.common.datatypes.component.ComponentNameMatcher +import android.tools.common.datatypes.component.IComponentMatcher +import android.tools.common.datatypes.component.IComponentNameMatcher +import android.tools.device.apphelpers.StandardAppHelper +import android.tools.device.traces.parsers.WindowManagerStateHelper +import android.tools.device.traces.parsers.toFlickerComponent +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.UiObject2 +import androidx.test.uiautomator.Until +import com.android.launcher3.tapl.LauncherInstrumentation +import com.android.server.wm.flicker.helpers.ImeAppHelper +import com.android.server.wm.flicker.helpers.NonResizeableAppHelper +import com.android.server.wm.flicker.helpers.NotificationAppHelper +import com.android.server.wm.flicker.helpers.SimpleAppHelper +import com.android.server.wm.flicker.testapp.ActivityOptions +import com.android.wm.shell.flicker.LAUNCHER_UI_PACKAGE_NAME +import com.android.wm.shell.flicker.SYSTEM_UI_PACKAGE_NAME +import org.junit.Assert.assertNotNull + +internal object SplitScreenUtils { + private const val TIMEOUT_MS = 3_000L + private const val DRAG_DURATION_MS = 1_000L + private const val NOTIFICATION_SCROLLER = "notification_stack_scroller" + private const val DIVIDER_BAR = "docked_divider_handle" + private const val OVERVIEW_SNAPSHOT = "snapshot" + private const val GESTURE_STEP_MS = 16L + private val LONG_PRESS_TIME_MS = ViewConfiguration.getLongPressTimeout() * 2L + private val SPLIT_DECOR_MANAGER = ComponentNameMatcher("", "SplitDecorManager#") + + private val notificationScrollerSelector: BySelector + get() = By.res(SYSTEM_UI_PACKAGE_NAME, NOTIFICATION_SCROLLER) + private val notificationContentSelector: BySelector + get() = By.text("Flicker Test Notification") + private val dividerBarSelector: BySelector + get() = By.res(SYSTEM_UI_PACKAGE_NAME, DIVIDER_BAR) + private val overviewSnapshotSelector: BySelector + get() = By.res(LAUNCHER_UI_PACKAGE_NAME, OVERVIEW_SNAPSHOT) + + fun getPrimary(instrumentation: Instrumentation): StandardAppHelper = + SimpleAppHelper( + instrumentation, + ActivityOptions.SplitScreen.Primary.LABEL, + ActivityOptions.SplitScreen.Primary.COMPONENT.toFlickerComponent() + ) + + fun getSecondary(instrumentation: Instrumentation): StandardAppHelper = + SimpleAppHelper( + instrumentation, + ActivityOptions.SplitScreen.Secondary.LABEL, + ActivityOptions.SplitScreen.Secondary.COMPONENT.toFlickerComponent() + ) + + fun getNonResizeable(instrumentation: Instrumentation): NonResizeableAppHelper = + NonResizeableAppHelper(instrumentation) + + fun getSendNotification(instrumentation: Instrumentation): NotificationAppHelper = + NotificationAppHelper(instrumentation) + + fun getIme(instrumentation: Instrumentation): ImeAppHelper = ImeAppHelper(instrumentation) + + fun waitForSplitComplete( + wmHelper: WindowManagerStateHelper, + primaryApp: IComponentMatcher, + secondaryApp: IComponentMatcher, + ) { + wmHelper + .StateSyncBuilder() + .withWindowSurfaceAppeared(primaryApp) + .withWindowSurfaceAppeared(secondaryApp) + .withSplitDividerVisible() + .waitForAndVerify() + } + + fun enterSplit( + wmHelper: WindowManagerStateHelper, + tapl: LauncherInstrumentation, + device: UiDevice, + primaryApp: StandardAppHelper, + secondaryApp: StandardAppHelper + ) { + primaryApp.launchViaIntent(wmHelper) + secondaryApp.launchViaIntent(wmHelper) + tapl.goHome() + wmHelper.StateSyncBuilder().withHomeActivityVisible().waitForAndVerify() + splitFromOverview(tapl, device) + waitForSplitComplete(wmHelper, primaryApp, secondaryApp) + } + + fun splitFromOverview(tapl: LauncherInstrumentation, device: UiDevice) { + // 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's currentTask on tablet is sometimes not what we expected if the overview + // contains more than 3 task views. We need to use uiautomator directly to find the + // second task to split. + tapl.workspace.switchToOverview().overviewActions.clickSplit() + val snapshots = device.wait(Until.findObjects(overviewSnapshotSelector), TIMEOUT_MS) + if (snapshots == null || snapshots.size < 1) { + error("Fail to find a overview snapshot to split.") + } + + // Find the second task in the upper right corner in split select mode by sorting + // 'left' in descending order and 'top' in ascending order. + snapshots.sortWith { t1: UiObject2, t2: UiObject2 -> + t2.getVisibleBounds().left - t1.getVisibleBounds().left + } + snapshots.sortWith { t1: UiObject2, t2: UiObject2 -> + t1.getVisibleBounds().top - t2.getVisibleBounds().top + } + snapshots[0].click() + } 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, + 50 /* steps */ + ) + SystemClock.sleep(TIMEOUT_MS) + + // Find the target notification + val notificationScroller = + device.wait(Until.findObject(notificationScrollerSelector), TIMEOUT_MS) + ?: error("Unable to find view $notificationScrollerSelector") + 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 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), 2000) + + 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 + ) + assertNotNull("Unable to find the TextView", textView) + textView.click(LONG_PRESS_TIME_MS) + + val copyBtn = device.wait(Until.findObject(By.text("Copy")), TIMEOUT_MS) + assertNotNull("Unable to find the copy button", copyBtn) + copyBtn.click() + + // Paste text to destinationApp + val editText = + device.wait( + Until.findObject(By.res(destinationApp.packageName, "plain_text_input")), + TIMEOUT_MS + ) + assertNotNull("Unable to find the EditText", editText) + editText.click(LONG_PRESS_TIME_MS) + + val pasteBtn = device.wait(Until.findObject(By.text("Paste")), TIMEOUT_MS) + assertNotNull("Unable to find the paste button", pasteBtn) + pasteBtn.click() + + // Verify text + if (!textView.text.contentEquals(editText.text)) { + error("Fail to copy content in split") + } + } +} 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..fbb7c7159234 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchAppByDoubleTapDivider.kt @@ -0,0 +1,213 @@ +/* + * 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.tools.common.NavBar +import android.tools.common.Rotation +import android.tools.device.flicker.junit.FlickerParametersRunnerFactory +import android.tools.device.flicker.legacy.FlickerBuilder +import android.tools.device.flicker.legacy.FlickerTest +import android.tools.device.flicker.legacy.FlickerTestFactory +import android.tools.device.helpers.WindowUtils +import android.tools.device.traces.parsers.WindowManagerStateHelper +import androidx.test.filters.RequiresDevice +import com.android.wm.shell.flicker.SPLIT_SCREEN_DIVIDER_COMPONENT +import com.android.wm.shell.flicker.appWindowIsVisibleAtEnd +import com.android.wm.shell.flicker.appWindowIsVisibleAtStart +import com.android.wm.shell.flicker.layerIsVisibleAtEnd +import com.android.wm.shell.flicker.layerKeepVisible +import com.android.wm.shell.flicker.splitAppLayerBoundsIsVisibleAtEnd +import com.android.wm.shell.flicker.splitScreenDividerIsVisibleAtEnd +import com.android.wm.shell.flicker.splitScreenDividerIsVisibleAtStart +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) +class SwitchAppByDoubleTapDivider(flicker: FlickerTest) : SplitScreenBase(flicker) { + + override val transition: FlickerBuilder.() -> Unit + get() = { + super.transition(this) + setup { SplitScreenUtils.enterSplit(wmHelper, tapl, device, primaryApp, secondaryApp) } + transitions { + SplitScreenUtils.doubleTapDividerToSwitch(device) + wmHelper.StateSyncBuilder().withAppTransitionIdle().waitForAndVerify() + + waitForLayersToSwitch(wmHelper) + waitForWindowsToSwitch(wmHelper) + } + } + + private fun waitForWindowsToSwitch(wmHelper: WindowManagerStateHelper) { + wmHelper + .StateSyncBuilder() + .add("appWindowsSwitched") { + val primaryAppWindow = + it.wmState.visibleWindows.firstOrNull { window -> + primaryApp.windowMatchesAnyOf(window) + } + ?: return@add false + val secondaryAppWindow = + it.wmState.visibleWindows.firstOrNull { window -> + secondaryApp.windowMatchesAnyOf(window) + } + ?: return@add false + + if (isLandscape(flicker.scenario.endRotation)) { + return@add if (flicker.scenario.isTablet) { + secondaryAppWindow.frame.right <= primaryAppWindow.frame.left + } else { + primaryAppWindow.frame.right <= secondaryAppWindow.frame.left + } + } else { + return@add if (flicker.scenario.isTablet) { + primaryAppWindow.frame.bottom <= secondaryAppWindow.frame.top + } else { + primaryAppWindow.frame.bottom <= secondaryAppWindow.frame.top + } + } + } + .waitForAndVerify() + } + + private fun waitForLayersToSwitch(wmHelper: WindowManagerStateHelper) { + wmHelper + .StateSyncBuilder() + .add("appLayersSwitched") { + val primaryAppLayer = + it.layerState.visibleLayers.firstOrNull { window -> + primaryApp.layerMatchesAnyOf(window) + } + ?: return@add false + val secondaryAppLayer = + it.layerState.visibleLayers.firstOrNull { window -> + secondaryApp.layerMatchesAnyOf(window) + } + ?: return@add false + + val primaryVisibleRegion = primaryAppLayer.visibleRegion?.bounds ?: return@add false + val secondaryVisibleRegion = + secondaryAppLayer.visibleRegion?.bounds ?: return@add false + + if (isLandscape(flicker.scenario.endRotation)) { + return@add if (flicker.scenario.isTablet) { + secondaryVisibleRegion.right <= primaryVisibleRegion.left + } else { + primaryVisibleRegion.right <= secondaryVisibleRegion.left + } + } else { + return@add if (flicker.scenario.isTablet) { + primaryVisibleRegion.bottom <= secondaryVisibleRegion.top + } else { + primaryVisibleRegion.bottom <= secondaryVisibleRegion.top + } + } + } + .waitForAndVerify() + } + + private fun isLandscape(rotation: Rotation): Boolean { + val displayBounds = WindowUtils.getDisplayBounds(rotation) + return displayBounds.width > displayBounds.height + } + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun cujCompleted() { + flicker.appWindowIsVisibleAtStart(primaryApp) + flicker.appWindowIsVisibleAtStart(secondaryApp) + flicker.splitScreenDividerIsVisibleAtStart() + + flicker.appWindowIsVisibleAtEnd(primaryApp) + flicker.appWindowIsVisibleAtEnd(secondaryApp) + flicker.splitScreenDividerIsVisibleAtEnd() + + // TODO(b/246490534): Add validation for switched app after withAppTransitionIdle is + // robust enough to get the correct end state. + } + + @Presubmit + @Test + fun splitScreenDividerKeepVisible() = flicker.layerKeepVisible(SPLIT_SCREEN_DIVIDER_COMPONENT) + + @Presubmit @Test fun primaryAppLayerIsVisibleAtEnd() = flicker.layerIsVisibleAtEnd(primaryApp) + + @Presubmit + @Test + fun secondaryAppLayerIsVisibleAtEnd() = flicker.layerIsVisibleAtEnd(secondaryApp) + + @Presubmit + @Test + fun primaryAppBoundsIsVisibleAtEnd() = + flicker.splitAppLayerBoundsIsVisibleAtEnd( + primaryApp, + landscapePosLeft = !tapl.isTablet, + portraitPosTop = true + ) + + @Presubmit + @Test + fun secondaryAppBoundsIsVisibleAtEnd() = + flicker.splitAppLayerBoundsIsVisibleAtEnd( + secondaryApp, + landscapePosLeft = tapl.isTablet, + portraitPosTop = false + ) + + @Presubmit + @Test + fun primaryAppWindowIsVisibleAtEnd() = flicker.appWindowIsVisibleAtEnd(primaryApp) + + @Presubmit + @Test + fun secondaryAppWindowIsVisibleAtEnd() = flicker.appWindowIsVisibleAtEnd(secondaryApp) + + /** {@inheritDoc} */ + @Postsubmit @Test override fun entireScreenCovered() = super.entireScreenCovered() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun visibleLayersShownMoreThanOneConsecutiveEntry() = + super.visibleLayersShownMoreThanOneConsecutiveEntry() + + companion object { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): List<FlickerTest> { + return FlickerTestFactory.nonRotationTests( + // TODO(b/176061063):The 3 buttons of nav bar do not exist in the hierarchy. + supportedNavigationModes = listOf(NavBar.MODE_GESTURAL) + ) + } + } +} 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..d675bfb0119d --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromAnotherApp.kt @@ -0,0 +1,173 @@ +/* + * 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.Presubmit +import android.tools.common.NavBar +import android.tools.device.flicker.junit.FlickerParametersRunnerFactory +import android.tools.device.flicker.legacy.FlickerBuilder +import android.tools.device.flicker.legacy.FlickerTest +import android.tools.device.flicker.legacy.FlickerTestFactory +import androidx.test.filters.RequiresDevice +import com.android.wm.shell.flicker.appWindowBecomesVisible +import com.android.wm.shell.flicker.layerBecomesVisible +import com.android.wm.shell.flicker.splitAppLayerBoundsIsVisibleAtEnd +import com.android.wm.shell.flicker.splitScreenDividerBecomesVisible +import com.android.wm.shell.flicker.splitScreenEntered +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) +class SwitchBackToSplitFromAnotherApp(flicker: FlickerTest) : SplitScreenBase(flicker) { + val thirdApp = SplitScreenUtils.getNonResizeable(instrumentation) + + override val transition: FlickerBuilder.() -> Unit + get() = { + super.transition(this) + setup { + SplitScreenUtils.enterSplit(wmHelper, tapl, device, primaryApp, secondaryApp) + + thirdApp.launchViaIntent(wmHelper) + wmHelper.StateSyncBuilder().withWindowSurfaceAppeared(thirdApp).waitForAndVerify() + } + transitions { + tapl.launchedAppState.quickSwitchToPreviousApp() + SplitScreenUtils.waitForSplitComplete(wmHelper, primaryApp, secondaryApp) + } + } + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun cujCompleted() = flicker.splitScreenEntered(primaryApp, secondaryApp, fromOtherApp = true) + + @Presubmit + @Test + fun splitScreenDividerBecomesVisible() = flicker.splitScreenDividerBecomesVisible() + + @Presubmit @Test fun primaryAppLayerBecomesVisible() = flicker.layerBecomesVisible(primaryApp) + + @Presubmit + @Test + fun secondaryAppLayerBecomesVisible() = flicker.layerBecomesVisible(secondaryApp) + + @Presubmit + @Test + fun primaryAppBoundsIsVisibleAtEnd() = + flicker.splitAppLayerBoundsIsVisibleAtEnd( + primaryApp, + landscapePosLeft = tapl.isTablet, + portraitPosTop = false + ) + + @Presubmit + @Test + fun secondaryAppBoundsIsVisibleAtEnd() = + flicker.splitAppLayerBoundsIsVisibleAtEnd( + secondaryApp, + landscapePosLeft = !tapl.isTablet, + portraitPosTop = true + ) + + @Presubmit + @Test + fun primaryAppWindowBecomesVisible() = flicker.appWindowBecomesVisible(primaryApp) + + @Presubmit + @Test + fun secondaryAppWindowBecomesVisible() = flicker.appWindowBecomesVisible(secondaryApp) + + /** {@inheritDoc} */ + @FlakyTest @Test override fun entireScreenCovered() = super.entireScreenCovered() + + /** {@inheritDoc} */ + @Presubmit + @Test + override fun navBarLayerIsVisibleAtStartAndEnd() = super.navBarLayerIsVisibleAtStartAndEnd() + + /** {@inheritDoc} */ + @Presubmit + @Test + override fun navBarLayerPositionAtStartAndEnd() = super.navBarLayerPositionAtStartAndEnd() + + /** {@inheritDoc} */ + @Presubmit + @Test + override fun navBarWindowIsAlwaysVisible() = super.navBarWindowIsAlwaysVisible() + + /** {@inheritDoc} */ + @Presubmit + @Test + override fun statusBarLayerIsVisibleAtStartAndEnd() = + super.statusBarLayerIsVisibleAtStartAndEnd() + + /** {@inheritDoc} */ + @Presubmit + @Test + override fun statusBarLayerPositionAtStartAndEnd() = super.statusBarLayerPositionAtStartAndEnd() + + /** {@inheritDoc} */ + @Presubmit + @Test + override fun statusBarWindowIsAlwaysVisible() = super.statusBarWindowIsAlwaysVisible() + + /** {@inheritDoc} */ + @Presubmit + @Test + override fun taskBarLayerIsVisibleAtStartAndEnd() = super.taskBarLayerIsVisibleAtStartAndEnd() + + /** {@inheritDoc} */ + @Presubmit + @Test + override fun taskBarWindowIsAlwaysVisible() = super.taskBarWindowIsAlwaysVisible() + + /** {@inheritDoc} */ + @Presubmit + @Test + override fun visibleLayersShownMoreThanOneConsecutiveEntry() = + super.visibleLayersShownMoreThanOneConsecutiveEntry() + + /** {@inheritDoc} */ + @Presubmit + @Test + override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = + super.visibleWindowsShownMoreThanOneConsecutiveEntry() + + companion object { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): List<FlickerTest> { + return FlickerTestFactory.nonRotationTests( + // TODO(b/176061063):The 3 buttons of nav bar do not exist in the hierarchy. + supportedNavigationModes = listOf(NavBar.MODE_GESTURAL) + ) + } + } +} 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..9f4cb8c381fc --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromHome.kt @@ -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.flicker.splitscreen + +import android.platform.test.annotations.FlakyTest +import android.platform.test.annotations.IwTest +import android.platform.test.annotations.Presubmit +import android.tools.common.NavBar +import android.tools.device.flicker.junit.FlickerParametersRunnerFactory +import android.tools.device.flicker.legacy.FlickerBuilder +import android.tools.device.flicker.legacy.FlickerTest +import android.tools.device.flicker.legacy.FlickerTestFactory +import androidx.test.filters.RequiresDevice +import com.android.wm.shell.flicker.appWindowBecomesVisible +import com.android.wm.shell.flicker.layerBecomesVisible +import com.android.wm.shell.flicker.splitAppLayerBoundsIsVisibleAtEnd +import com.android.wm.shell.flicker.splitScreenDividerBecomesVisible +import com.android.wm.shell.flicker.splitScreenEntered +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) +class SwitchBackToSplitFromHome(flicker: FlickerTest) : SplitScreenBase(flicker) { + + override val transition: FlickerBuilder.() -> Unit + get() = { + super.transition(this) + setup { + SplitScreenUtils.enterSplit(wmHelper, tapl, device, primaryApp, secondaryApp) + + tapl.goHome() + wmHelper.StateSyncBuilder().withHomeActivityVisible().waitForAndVerify() + } + transitions { + tapl.workspace.quickSwitchToPreviousApp() + SplitScreenUtils.waitForSplitComplete(wmHelper, primaryApp, secondaryApp) + } + } + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun cujCompleted() = flicker.splitScreenEntered(primaryApp, secondaryApp, fromOtherApp = true) + + @Presubmit + @Test + fun splitScreenDividerBecomesVisible() = flicker.splitScreenDividerBecomesVisible() + + @Presubmit @Test fun primaryAppLayerBecomesVisible() = flicker.layerBecomesVisible(primaryApp) + + @Presubmit + @Test + fun secondaryAppLayerBecomesVisible() = flicker.layerBecomesVisible(secondaryApp) + + @Presubmit + @Test + fun primaryAppBoundsIsVisibleAtEnd() = + flicker.splitAppLayerBoundsIsVisibleAtEnd( + primaryApp, + landscapePosLeft = tapl.isTablet, + portraitPosTop = false + ) + + @Presubmit + @Test + fun secondaryAppBoundsIsVisibleAtEnd() = + flicker.splitAppLayerBoundsIsVisibleAtEnd( + secondaryApp, + landscapePosLeft = !tapl.isTablet, + portraitPosTop = true + ) + + @Presubmit + @Test + fun primaryAppWindowBecomesVisible() = flicker.appWindowBecomesVisible(primaryApp) + + @Presubmit + @Test + fun secondaryAppWindowBecomesVisible() = flicker.appWindowBecomesVisible(secondaryApp) + + /** {@inheritDoc} */ + @FlakyTest @Test override fun entireScreenCovered() = super.entireScreenCovered() + + /** {@inheritDoc} */ + @Presubmit + @Test + override fun navBarLayerIsVisibleAtStartAndEnd() = super.navBarLayerIsVisibleAtStartAndEnd() + + /** {@inheritDoc} */ + @Presubmit + @Test + override fun navBarLayerPositionAtStartAndEnd() = super.navBarLayerPositionAtStartAndEnd() + + /** {@inheritDoc} */ + @Presubmit + @Test + override fun navBarWindowIsAlwaysVisible() = super.navBarWindowIsAlwaysVisible() + + /** {@inheritDoc} */ + @Presubmit + @Test + override fun statusBarLayerIsVisibleAtStartAndEnd() = + super.statusBarLayerIsVisibleAtStartAndEnd() + + /** {@inheritDoc} */ + @Presubmit + @Test + override fun statusBarLayerPositionAtStartAndEnd() = super.statusBarLayerPositionAtStartAndEnd() + + /** {@inheritDoc} */ + @Presubmit + @Test + override fun statusBarWindowIsAlwaysVisible() = super.statusBarWindowIsAlwaysVisible() + + /** {@inheritDoc} */ + @Presubmit + @Test + override fun taskBarLayerIsVisibleAtStartAndEnd() = super.taskBarLayerIsVisibleAtStartAndEnd() + + /** {@inheritDoc} */ + @Presubmit + @Test + override fun taskBarWindowIsAlwaysVisible() = super.taskBarWindowIsAlwaysVisible() + + /** {@inheritDoc} */ + @FlakyTest(bugId = 252736515) + @Test + override fun visibleLayersShownMoreThanOneConsecutiveEntry() = + super.visibleLayersShownMoreThanOneConsecutiveEntry() + + /** {@inheritDoc} */ + @Presubmit + @Test + override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = + super.visibleWindowsShownMoreThanOneConsecutiveEntry() + + companion object { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): List<FlickerTest> { + return FlickerTestFactory.nonRotationTests( + // TODO(b/176061063):The 3 buttons of nav bar do not exist in the hierarchy. + supportedNavigationModes = listOf(NavBar.MODE_GESTURAL) + ) + } + } +} 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..a33d8cab9fbd --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromRecent.kt @@ -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.flicker.splitscreen + +import android.platform.test.annotations.FlakyTest +import android.platform.test.annotations.IwTest +import android.platform.test.annotations.Presubmit +import android.tools.common.NavBar +import android.tools.device.flicker.junit.FlickerParametersRunnerFactory +import android.tools.device.flicker.legacy.FlickerBuilder +import android.tools.device.flicker.legacy.FlickerTest +import android.tools.device.flicker.legacy.FlickerTestFactory +import androidx.test.filters.RequiresDevice +import com.android.wm.shell.flicker.appWindowBecomesVisible +import com.android.wm.shell.flicker.layerBecomesVisible +import com.android.wm.shell.flicker.splitAppLayerBoundsIsVisibleAtEnd +import com.android.wm.shell.flicker.splitScreenDividerBecomesVisible +import com.android.wm.shell.flicker.splitScreenEntered +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) +class SwitchBackToSplitFromRecent(flicker: FlickerTest) : SplitScreenBase(flicker) { + + override val transition: FlickerBuilder.() -> Unit + get() = { + super.transition(this) + setup { + SplitScreenUtils.enterSplit(wmHelper, tapl, device, primaryApp, secondaryApp) + + tapl.goHome() + wmHelper.StateSyncBuilder().withHomeActivityVisible().waitForAndVerify() + } + transitions { + tapl.workspace.switchToOverview().currentTask.open() + SplitScreenUtils.waitForSplitComplete(wmHelper, primaryApp, secondaryApp) + } + } + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun cujCompleted() = flicker.splitScreenEntered(primaryApp, secondaryApp, fromOtherApp = true) + + @Presubmit + @Test + fun splitScreenDividerBecomesVisible() = flicker.splitScreenDividerBecomesVisible() + + @Presubmit @Test fun primaryAppLayerBecomesVisible() = flicker.layerBecomesVisible(primaryApp) + + @Presubmit + @Test + fun secondaryAppLayerBecomesVisible() = flicker.layerBecomesVisible(secondaryApp) + + @Presubmit + @Test + fun primaryAppBoundsIsVisibleAtEnd() = + flicker.splitAppLayerBoundsIsVisibleAtEnd( + primaryApp, + landscapePosLeft = tapl.isTablet, + portraitPosTop = false + ) + + @Presubmit + @Test + fun secondaryAppBoundsIsVisibleAtEnd() = + flicker.splitAppLayerBoundsIsVisibleAtEnd( + secondaryApp, + landscapePosLeft = !tapl.isTablet, + portraitPosTop = true + ) + + @Presubmit + @Test + fun primaryAppWindowBecomesVisible() = flicker.appWindowBecomesVisible(primaryApp) + + @Presubmit + @Test + fun secondaryAppWindowBecomesVisible() = flicker.appWindowBecomesVisible(secondaryApp) + + /** {@inheritDoc} */ + @Presubmit @Test override fun entireScreenCovered() = super.entireScreenCovered() + + /** {@inheritDoc} */ + @Presubmit + @Test + override fun navBarLayerIsVisibleAtStartAndEnd() = super.navBarLayerIsVisibleAtStartAndEnd() + + /** {@inheritDoc} */ + @Presubmit + @Test + override fun navBarLayerPositionAtStartAndEnd() = super.navBarLayerPositionAtStartAndEnd() + + /** {@inheritDoc} */ + @Presubmit + @Test + override fun navBarWindowIsAlwaysVisible() = super.navBarWindowIsAlwaysVisible() + + /** {@inheritDoc} */ + @Presubmit + @Test + override fun statusBarLayerIsVisibleAtStartAndEnd() = + super.statusBarLayerIsVisibleAtStartAndEnd() + + /** {@inheritDoc} */ + @Presubmit + @Test + override fun statusBarLayerPositionAtStartAndEnd() = super.statusBarLayerPositionAtStartAndEnd() + + /** {@inheritDoc} */ + @Presubmit + @Test + override fun statusBarWindowIsAlwaysVisible() = super.statusBarWindowIsAlwaysVisible() + + /** {@inheritDoc} */ + @Presubmit + @Test + override fun taskBarLayerIsVisibleAtStartAndEnd() = super.taskBarLayerIsVisibleAtStartAndEnd() + + /** {@inheritDoc} */ + @Presubmit + @Test + override fun taskBarWindowIsAlwaysVisible() = super.taskBarWindowIsAlwaysVisible() + + /** {@inheritDoc} */ + @FlakyTest + @Test + override fun visibleLayersShownMoreThanOneConsecutiveEntry() = + super.visibleLayersShownMoreThanOneConsecutiveEntry() + + /** {@inheritDoc} */ + @Presubmit + @Test + override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = + super.visibleWindowsShownMoreThanOneConsecutiveEntry() + + companion object { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): List<FlickerTest> { + return FlickerTestFactory.nonRotationTests( + // TODO(b/176061063):The 3 buttons of nav bar do not exist in the hierarchy. + supportedNavigationModes = listOf(NavBar.MODE_GESTURAL) + ) + } + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBetweenSplitPairs.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBetweenSplitPairs.kt new file mode 100644 index 000000000000..4c96b3a319d5 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBetweenSplitPairs.kt @@ -0,0 +1,241 @@ +/* + * 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.Presubmit +import android.tools.device.flicker.junit.FlickerParametersRunnerFactory +import android.tools.device.flicker.legacy.FlickerBuilder +import android.tools.device.flicker.legacy.FlickerTest +import android.tools.device.flicker.legacy.FlickerTestFactory +import androidx.test.filters.RequiresDevice +import com.android.wm.shell.flicker.SPLIT_SCREEN_DIVIDER_COMPONENT +import com.android.wm.shell.flicker.appWindowBecomesInvisible +import com.android.wm.shell.flicker.appWindowBecomesVisible +import com.android.wm.shell.flicker.appWindowIsInvisibleAtEnd +import com.android.wm.shell.flicker.appWindowIsVisibleAtEnd +import com.android.wm.shell.flicker.appWindowIsVisibleAtStart +import com.android.wm.shell.flicker.layerBecomesInvisible +import com.android.wm.shell.flicker.layerBecomesVisible +import com.android.wm.shell.flicker.splitAppLayerBoundsIsVisibleAtEnd +import com.android.wm.shell.flicker.splitAppLayerBoundsSnapToDivider +import com.android.wm.shell.flicker.splitScreenDividerIsVisibleAtEnd +import com.android.wm.shell.flicker.splitScreenDividerIsVisibleAtStart +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 between two split pairs. + * + * To run this test: `atest WMShellFlickerTests:SwitchBetweenSplitPairs` + */ +@RequiresDevice +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class SwitchBetweenSplitPairs(flicker: FlickerTest) : SplitScreenBase(flicker) { + private val thirdApp = SplitScreenUtils.getIme(instrumentation) + private val fourthApp = SplitScreenUtils.getSendNotification(instrumentation) + + override val transition: FlickerBuilder.() -> Unit + get() = { + super.transition(this) + setup { + SplitScreenUtils.enterSplit(wmHelper, tapl, device, primaryApp, secondaryApp) + SplitScreenUtils.enterSplit(wmHelper, tapl, device, thirdApp, fourthApp) + SplitScreenUtils.waitForSplitComplete(wmHelper, thirdApp, fourthApp) + } + transitions { + tapl.launchedAppState.quickSwitchToPreviousApp() + SplitScreenUtils.waitForSplitComplete(wmHelper, primaryApp, secondaryApp) + } + teardown { + thirdApp.exit(wmHelper) + fourthApp.exit(wmHelper) + } + } + + @IwTest(focusArea = "sysui") + @Presubmit + @Test + fun cujCompleted() { + flicker.appWindowIsVisibleAtStart(thirdApp) + flicker.appWindowIsVisibleAtStart(fourthApp) + flicker.splitScreenDividerIsVisibleAtStart() + + flicker.appWindowIsVisibleAtEnd(primaryApp) + flicker.appWindowIsVisibleAtEnd(secondaryApp) + flicker.appWindowIsInvisibleAtEnd(thirdApp) + flicker.appWindowIsInvisibleAtEnd(fourthApp) + flicker.splitScreenDividerIsVisibleAtEnd() + } + + @Presubmit + @Test + fun splitScreenDividerInvisibleAtMiddle() = + flicker.assertLayers { + this.isVisible(SPLIT_SCREEN_DIVIDER_COMPONENT) + .then() + .isInvisible(SPLIT_SCREEN_DIVIDER_COMPONENT) + .then() + .isVisible(SPLIT_SCREEN_DIVIDER_COMPONENT) + } + + @FlakyTest(bugId = 247095572) + @Test + fun primaryAppLayerBecomesVisible() = flicker.layerBecomesVisible(primaryApp) + + @FlakyTest(bugId = 247095572) + @Test + fun secondaryAppLayerBecomesVisible() = flicker.layerBecomesVisible(secondaryApp) + + @FlakyTest(bugId = 247095572) + @Test + fun thirdAppLayerBecomesInvisible() = flicker.layerBecomesInvisible(thirdApp) + + @FlakyTest(bugId = 247095572) + @Test + fun fourthAppLayerBecomesInvisible() = flicker.layerBecomesInvisible(fourthApp) + + @Presubmit + @Test + fun primaryAppBoundsIsVisibleAtEnd() = + flicker.splitAppLayerBoundsIsVisibleAtEnd( + primaryApp, + landscapePosLeft = tapl.isTablet, + portraitPosTop = false + ) + + @Presubmit + @Test + fun secondaryAppBoundsIsVisibleAtEnd() = + flicker.splitAppLayerBoundsIsVisibleAtEnd( + secondaryApp, + landscapePosLeft = !tapl.isTablet, + portraitPosTop = true + ) + + @Presubmit + @Test + fun thirdAppBoundsIsVisibleAtBegin() = + flicker.assertLayersStart { + this.splitAppLayerBoundsSnapToDivider( + thirdApp, + landscapePosLeft = tapl.isTablet, + portraitPosTop = false, + flicker.scenario.startRotation + ) + } + + @Presubmit + @Test + fun fourthAppBoundsIsVisibleAtBegin() = + flicker.assertLayersStart { + this.splitAppLayerBoundsSnapToDivider( + fourthApp, + landscapePosLeft = !tapl.isTablet, + portraitPosTop = true, + flicker.scenario.startRotation + ) + } + + @Presubmit + @Test + fun primaryAppWindowBecomesVisible() = flicker.appWindowBecomesVisible(primaryApp) + + @Presubmit + @Test + fun secondaryAppWindowBecomesVisible() = flicker.appWindowBecomesVisible(secondaryApp) + + @Presubmit + @Test + fun thirdAppWindowBecomesVisible() = flicker.appWindowBecomesInvisible(thirdApp) + + @Presubmit + @Test + fun fourthAppWindowBecomesVisible() = flicker.appWindowBecomesInvisible(fourthApp) + + /** {@inheritDoc} */ + @FlakyTest(bugId = 251268711) + @Test + override fun entireScreenCovered() = super.entireScreenCovered() + + /** {@inheritDoc} */ + @Presubmit + @Test + override fun navBarLayerIsVisibleAtStartAndEnd() = super.navBarLayerIsVisibleAtStartAndEnd() + + /** {@inheritDoc} */ + @FlakyTest(bugId = 206753786) + @Test + override fun navBarLayerPositionAtStartAndEnd() = super.navBarLayerPositionAtStartAndEnd() + + /** {@inheritDoc} */ + @Presubmit + @Test + override fun navBarWindowIsAlwaysVisible() = super.navBarWindowIsAlwaysVisible() + + /** {@inheritDoc} */ + @Presubmit + @Test + override fun statusBarLayerIsVisibleAtStartAndEnd() = + super.statusBarLayerIsVisibleAtStartAndEnd() + + /** {@inheritDoc} */ + @Presubmit + @Test + override fun statusBarLayerPositionAtStartAndEnd() = super.statusBarLayerPositionAtStartAndEnd() + + /** {@inheritDoc} */ + @Presubmit + @Test + override fun statusBarWindowIsAlwaysVisible() = super.statusBarWindowIsAlwaysVisible() + + /** {@inheritDoc} */ + @Presubmit + @Test + override fun taskBarLayerIsVisibleAtStartAndEnd() = super.taskBarLayerIsVisibleAtStartAndEnd() + + /** {@inheritDoc} */ + @Presubmit + @Test + override fun taskBarWindowIsAlwaysVisible() = super.taskBarWindowIsAlwaysVisible() + + /** {@inheritDoc} */ + @FlakyTest + @Test + override fun visibleLayersShownMoreThanOneConsecutiveEntry() = + super.visibleLayersShownMoreThanOneConsecutiveEntry() + + /** {@inheritDoc} */ + @Presubmit + @Test + override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = + super.visibleWindowsShownMoreThanOneConsecutiveEntry() + + companion object { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): List<FlickerTest> { + return FlickerTestFactory.nonRotationTests() + } + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/AndroidManifest.xml b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/AndroidManifest.xml deleted file mode 100644 index bd98585b67ec..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/AndroidManifest.xml +++ /dev/null @@ -1,136 +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. ---> - -<manifest xmlns:android="http://schemas.android.com/apk/res/android" - package="com.android.wm.shell.flicker.testapp"> - - <uses-sdk android:minSdkVersion="29" - android:targetSdkVersion="29"/> - <application android:allowBackup="false" - android:supportsRtl="true"> - <activity android:name=".FixedActivity" - android:resizeableActivity="true" - android:supportsPictureInPicture="true" - android:launchMode="singleTop" - android:theme="@style/CutoutShortEdges" - android:label="FixedApp" - 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=".PipActivity" - android:resizeableActivity="true" - android:supportsPictureInPicture="true" - android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation" - android:taskAffinity="com.android.wm.shell.flicker.testapp.PipActivity" - android:theme="@style/CutoutShortEdges" - android:launchMode="singleTop" - android:label="PipApp" - android:exported="true"> - <intent-filter> - <action android:name="android.intent.action.MAIN"/> - <category android:name="android.intent.category.LAUNCHER"/> - </intent-filter> - <intent-filter> - <action android:name="android.intent.action.MAIN"/> - <category android:name="android.intent.category.LEANBACK_LAUNCHER"/> - </intent-filter> - </activity> - - <activity android:name=".ImeActivity" - android:taskAffinity="com.android.wm.shell.flicker.testapp.ImeActivity" - android:theme="@style/CutoutShortEdges" - android:label="ImeApp" - android:launchMode="singleTop" - android:exported="true"> - <intent-filter> - <action android:name="android.intent.action.MAIN"/> - <category android:name="android.intent.category.LAUNCHER"/> - </intent-filter> - <intent-filter> - <action android:name="android.intent.action.MAIN"/> - <category android:name="android.intent.category.LEANBACK_LAUNCHER"/> - </intent-filter> - </activity> - - <activity android:name=".SplitScreenActivity" - android:resizeableActivity="true" - android:taskAffinity="com.android.wm.shell.flicker.testapp.SplitScreenActivity" - android:theme="@style/CutoutShortEdges" - android:label="SplitScreenPrimaryApp" - 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=".SplitScreenSecondaryActivity" - android:resizeableActivity="true" - android:taskAffinity="com.android.wm.shell.flicker.testapp.SplitScreenSecondaryActivity" - android:theme="@style/CutoutShortEdges" - android:label="SplitScreenSecondaryApp" - 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" - android:theme="@style/CutoutShortEdges" - android:label="NonResizeableApp" - 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=".SimpleActivity" - android:taskAffinity="com.android.wm.shell.flicker.testapp.SimpleActivity" - android:theme="@style/CutoutShortEdges" - android:label="SimpleApp" - 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=".LaunchBubbleActivity" - android:label="LaunchBubbleApp" - android:exported="true" - android:theme="@style/CutoutShortEdges" - android:launchMode="singleTop"> - <intent-filter> - <action android:name="android.intent.action.MAIN" /> - <action android:name="android.intent.action.VIEW" /> - <category android:name="android.intent.category.LAUNCHER"/> - </intent-filter> - </activity> - <activity - android:name=".BubbleActivity" - android:label="BubbleApp" - android:exported="false" - android:theme="@style/CutoutShortEdges" - android:resizeableActivity="true" /> - </application> -</manifest> diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/drawable/bg.png b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/drawable/bg.png Binary files differdeleted file mode 100644 index d424a17b4157..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/drawable/bg.png +++ /dev/null diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/drawable/ic_bubble.xml b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/drawable/ic_bubble.xml deleted file mode 100644 index b43f31da748d..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/drawable/ic_bubble.xml +++ /dev/null @@ -1,31 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - Copyright 2021 The Android Open Source Project - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. ---> -<vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="24dp" - android:height="24dp" - android:viewportWidth="24.0" - android:viewportHeight="24.0"> - <path - android:fillColor="#FF000000" - android:pathData="M7.2,14.4m-3.2,0a3.2,3.2 0,1 1,6.4 0a3.2,3.2 0,1 1,-6.4 0"/> - <path - android:fillColor="#FF000000" - android:pathData="M14.8,18m-2,0a2,2 0,1 1,4 0a2,2 0,1 1,-4 0"/> - <path - android:fillColor="#FF000000" - android:pathData="M15.2,8.8m-4.8,0a4.8,4.8 0,1 1,9.6 0a4.8,4.8 0,1 1,-9.6 0"/> -</vector> diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/drawable/ic_message.xml b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/drawable/ic_message.xml deleted file mode 100644 index 0e8c7a0fe64a..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/drawable/ic_message.xml +++ /dev/null @@ -1,26 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - Copyright 2021 The Android Open Source Project - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. ---> -<vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="24dp" - android:height="24dp" - android:viewportWidth="24" - android:viewportHeight="24"> - <path - android:pathData="M12,4c-4.97,0 -9,3.58 -9,8c0,1.53 0.49,2.97 1.33,4.18c0.12,0.18 0.2,0.46 0.1,0.66c-0.33,0.68 -0.79,1.52 -1.38,2.39c-0.12,0.17 0.01,0.41 0.21,0.39c0.63,-0.05 1.86,-0.26 3.38,-0.91c0.17,-0.07 0.36,-0.06 0.52,0.03C8.55,19.54 10.21,20 12,20c4.97,0 9,-3.58 9,-8S16.97,4 12,4zM16.94,11.63l-3.29,3.29c-0.13,0.13 -0.34,0.04 -0.34,-0.14v-1.57c0,-0.11 -0.1,-0.21 -0.21,-0.2c-2.19,0.06 -3.65,0.65 -5.14,1.95c-0.15,0.13 -0.38,0 -0.33,-0.19c0.7,-2.57 2.9,-4.57 5.5,-4.75c0.1,-0.01 0.18,-0.09 0.18,-0.19V8.2c0,-0.18 0.22,-0.27 0.34,-0.14l3.29,3.29C17.02,11.43 17.02,11.55 16.94,11.63z" - android:fillColor="#000000" - android:fillType="evenOdd"/> -</vector> diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_bubble.xml b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_bubble.xml deleted file mode 100644 index f8b0ca3da26e..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_bubble.xml +++ /dev/null @@ -1,48 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - Copyright 2021 The Android Open Source Project - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. ---> -<LinearLayout - xmlns:android="http://schemas.android.com/apk/res/android" - android:layout_width="match_parent" - android:layout_height="match_parent"> - <Button - android:id="@+id/button_finish" - android:layout_width="wrap_content" - android:layout_height="48dp" - android:layout_marginStart="8dp" - android:text="Finish" /> - <Button - android:id="@+id/button_new_task" - android:layout_width="wrap_content" - android:layout_height="46dp" - android:layout_marginStart="8dp" - android:text="New Task" /> - <Button - android:id="@+id/button_new_bubble" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_marginStart="8dp" - android:layout_marginEnd="8dp" - android:text="New Bubble" /> - - <Button - android:id="@+id/button_activity_for_result" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_marginEnd="8dp" - android:layout_marginStart="8dp" - android:text="Activity For Result" /> -</LinearLayout> diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_ime.xml b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_ime.xml deleted file mode 100644 index 4708cfd48381..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_ime.xml +++ /dev/null @@ -1,27 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - Copyright 2018 The Android Open Source Project - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. ---> -<LinearLayout - xmlns:android="http://schemas.android.com/apk/res/android" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:focusableInTouchMode="true" - android:background="@android:color/holo_green_light"> - <EditText android:id="@+id/plain_text_input" - android:layout_height="wrap_content" - android:layout_width="match_parent" - android:inputType="text"/> -</LinearLayout> diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_main.xml b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_main.xml deleted file mode 100644 index f23c46455c63..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_main.xml +++ /dev/null @@ -1,48 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - Copyright 2021 The Android Open Source Project - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. ---> -<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:orientation="vertical" - android:background="@android:color/black"> - - <Button - android:id="@+id/button_create" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_centerHorizontal="true" - android:layout_centerVertical="true" - android:text="Add Bubble" /> - - <Button - android:id="@+id/button_cancel" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_below="@id/button_create" - android:layout_centerHorizontal="true" - android:layout_marginTop="20dp" - android:text="Cancel Bubble" /> - - <Button - android:id="@+id/button_cancel_all" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_below="@id/button_cancel" - android:layout_centerHorizontal="true" - android:layout_marginTop="20dp" - android:text="Cancel All Bubble" /> -</RelativeLayout> diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_non_resizeable.xml b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_non_resizeable.xml deleted file mode 100644 index 45d5917f86d6..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_non_resizeable.xml +++ /dev/null @@ -1,32 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - Copyright 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. ---> -<LinearLayout - 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/holo_orange_light"> - - <TextView - android:id="@+id/NonResizeableTest" - android:layout_width="fill_parent" - android:layout_height="fill_parent" - android:gravity="center_vertical|center_horizontal" - android:text="NonResizeableActivity" - android:textAppearance="?android:attr/textAppearanceLarge"/> - -</LinearLayout> 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 deleted file mode 100644 index 909b77c87894..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_pip.xml +++ /dev/null @@ -1,108 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - Copyright 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. ---> -<LinearLayout - 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/holo_blue_bright"> - - <!-- All the buttons (and other clickable elements) should be arranged in a way so that it is - possible to "cycle" over all them by clicking on the D-Pad DOWN button. The way we do it - here is by arranging them this vertical LL and by relying on the nextFocusDown attribute - where things are arranged differently and to circle back up to the top once we reach the - bottom. --> - - <Button - android:id="@+id/enter_pip" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:text="Enter PIP" - android:onClick="enterPip"/> - - <CheckBox - android:id="@+id/with_custom_actions" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:text="With custom actions"/> - - <RadioGroup - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:orientation="vertical" - android:checkedButton="@id/ratio_default"> - - <TextView - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:text="Ratio"/> - - <RadioButton - android:id="@+id/ratio_default" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:text="Default" - android:onClick="onRatioSelected"/> - - <RadioButton - android:id="@+id/ratio_square" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:text="Square [1:1]" - android:onClick="onRatioSelected"/> - - <RadioButton - android:id="@+id/ratio_wide" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:text="Wide [2:1]" - android:onClick="onRatioSelected"/> - - <RadioButton - android:id="@+id/ratio_tall" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:text="Tall [1:2]" - android:onClick="onRatioSelected"/> - </RadioGroup> - - <TextView - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:text="Media Session"/> - - <LinearLayout - android:layout_width="wrap_content" - android:layout_height="wrap_content"> - - <Button - android:id="@+id/media_session_start" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:nextFocusDown="@id/media_session_stop" - android:text="Start"/> - - <Button - android:id="@+id/media_session_stop" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:nextFocusDown="@id/enter_pip" - android:text="Stop"/> - - </LinearLayout> - -</LinearLayout> diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_simple.xml b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_simple.xml deleted file mode 100644 index 5d94e5177dcc..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_simple.xml +++ /dev/null @@ -1,23 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - Copyright 2018 The Android Open Source Project - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. ---> -<LinearLayout - xmlns:android="http://schemas.android.com/apk/res/android" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:background="@android:color/holo_orange_light"> - -</LinearLayout> 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 deleted file mode 100644 index 84789f5a6c02..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_splitscreen.xml +++ /dev/null @@ -1,32 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - Copyright 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. ---> -<LinearLayout - 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/holo_green_light"> - - <TextView - android:id="@+id/SplitScreenTest" - android:layout_width="fill_parent" - android:layout_height="fill_parent" - android:gravity="center_vertical|center_horizontal" - android:text="PrimaryActivity" - android:textAppearance="?android:attr/textAppearanceLarge"/> - -</LinearLayout> diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_splitscreen_secondary.xml b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_splitscreen_secondary.xml deleted file mode 100644 index 674bb70ad01e..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_splitscreen_secondary.xml +++ /dev/null @@ -1,32 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - Copyright 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. ---> -<LinearLayout - 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/holo_blue_light"> - - <TextView - android:id="@+id/SplitScreenTest" - android:layout_width="fill_parent" - android:layout_height="fill_parent" - android:gravity="center_vertical|center_horizontal" - android:text="SecondaryActivity" - android:textAppearance="?android:attr/textAppearanceLarge"/> - -</LinearLayout> diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/values/styles.xml b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/values/styles.xml deleted file mode 100644 index 23b51cc06f04..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/values/styles.xml +++ /dev/null @@ -1,34 +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. - --> - -<resources> - <style name="DefaultTheme" parent="@android:style/Theme.DeviceDefault"> - <item name="android:windowBackground">@android:color/darker_gray</item> - </style> - - <style name="CutoutDefault" parent="@style/DefaultTheme"> - <item name="android:windowLayoutInDisplayCutoutMode">default</item> - </style> - - <style name="CutoutShortEdges" parent="@style/DefaultTheme"> - <item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item> - </style> - - <style name="CutoutNever" parent="@style/DefaultTheme"> - <item name="android:windowLayoutInDisplayCutoutMode">never</item> - </style> -</resources>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/BubbleActivity.java b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/BubbleActivity.java deleted file mode 100644 index bc3bc75ab903..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/BubbleActivity.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.flicker.testapp; - - -import android.app.Activity; -import android.content.Intent; -import android.os.Bundle; -import android.widget.Toast; - -public class BubbleActivity extends Activity { - private int mNotifId = 0; - - public BubbleActivity() { - super(); - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - Intent intent = getIntent(); - if (intent != null) { - mNotifId = intent.getIntExtra(BubbleHelper.EXTRA_BUBBLE_NOTIF_ID, -1); - } else { - mNotifId = -1; - } - - setContentView(R.layout.activity_bubble); - } - - @Override - protected void onStart() { - super.onStart(); - } - - @Override - protected void onResume() { - super.onResume(); - } - - @Override - protected void onPause() { - super.onPause(); - } - - @Override - protected void onStop() { - super.onStop(); - } - - @Override - protected void onDestroy() { - super.onDestroy(); - } - - @Override - protected void onActivityResult(int requestCode, int resultCode, Intent data) { - super.onActivityResult(requestCode, resultCode, data); - String result = resultCode == Activity.RESULT_OK ? "OK" : "CANCELLED"; - Toast.makeText(this, "Activity result: " + result, Toast.LENGTH_SHORT).show(); - } -} diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/BubbleHelper.java b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/BubbleHelper.java deleted file mode 100644 index 6cd93eff2803..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/BubbleHelper.java +++ /dev/null @@ -1,173 +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.testapp; - - -import android.app.Notification; -import android.app.NotificationChannel; -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.app.Person; -import android.content.Context; -import android.content.Intent; -import android.graphics.Point; -import android.graphics.drawable.Icon; -import android.os.SystemClock; -import android.service.notification.StatusBarNotification; -import android.view.WindowManager; - -import java.util.HashMap; - -public class BubbleHelper { - - static final String EXTRA_BUBBLE_NOTIF_ID = "EXTRA_BUBBLE_NOTIF_ID"; - static final String CHANNEL_ID = "bubbles"; - static final String CHANNEL_NAME = "Bubbles"; - static final int DEFAULT_HEIGHT_DP = 300; - - private static BubbleHelper sInstance; - - private final Context mContext; - private NotificationManager mNotificationManager; - private float mDisplayHeight; - - private HashMap<Integer, BubbleInfo> mBubbleMap = new HashMap<>(); - - private int mNextNotifyId = 0; - private int mColourIndex = 0; - - public static class BubbleInfo { - public int id; - public int height; - public Icon icon; - - public BubbleInfo(int id, int height, Icon icon) { - this.id = id; - this.height = height; - this.icon = icon; - } - } - - public static BubbleHelper getInstance(Context context) { - if (sInstance == null) { - sInstance = new BubbleHelper(context); - } - return sInstance; - } - - private BubbleHelper(Context context) { - mContext = context; - mNotificationManager = context.getSystemService(NotificationManager.class); - - NotificationChannel channel = new NotificationChannel(CHANNEL_ID, CHANNEL_NAME, - NotificationManager.IMPORTANCE_DEFAULT); - channel.setDescription("Channel that posts bubbles"); - channel.setAllowBubbles(true); - mNotificationManager.createNotificationChannel(channel); - - Point p = new Point(); - WindowManager wm = context.getSystemService(WindowManager.class); - wm.getDefaultDisplay().getRealSize(p); - mDisplayHeight = p.y; - - } - - private int getNextNotifyId() { - int id = mNextNotifyId; - mNextNotifyId++; - return id; - } - - private Icon getIcon() { - return Icon.createWithResource(mContext, R.drawable.bg); - } - - public int addNewBubble(boolean autoExpand, boolean suppressNotif) { - int id = getNextNotifyId(); - BubbleInfo info = new BubbleInfo(id, DEFAULT_HEIGHT_DP, getIcon()); - mBubbleMap.put(info.id, info); - - Notification.BubbleMetadata data = getBubbleBuilder(info) - .setSuppressNotification(suppressNotif) - .setAutoExpandBubble(false) - .build(); - Notification notification = getNotificationBuilder(info.id) - .setBubbleMetadata(data).build(); - - mNotificationManager.notify(info.id, notification); - return info.id; - } - - private Notification.Builder getNotificationBuilder(int id) { - Person chatBot = new Person.Builder() - .setBot(true) - .setName("BubbleChat") - .setImportant(true) - .build(); - String shortcutId = "BubbleChat"; - return new Notification.Builder(mContext, CHANNEL_ID) - .setChannelId(CHANNEL_ID) - .setShortcutId(shortcutId) - .setContentTitle("BubbleChat") - .setContentIntent(PendingIntent.getActivity(mContext, 0, - new Intent(mContext, LaunchBubbleActivity.class), - PendingIntent.FLAG_UPDATE_CURRENT)) - .setStyle(new Notification.MessagingStyle(chatBot) - .setConversationTitle("BubbleChat") - .addMessage("BubbleChat", - SystemClock.currentThreadTimeMillis() - 300000, chatBot) - .addMessage("Is it me, " + id + ", you're looking for?", - SystemClock.currentThreadTimeMillis(), chatBot) - ) - .setSmallIcon(R.drawable.ic_bubble); - } - - private Notification.BubbleMetadata.Builder getBubbleBuilder(BubbleInfo info) { - Intent target = new Intent(mContext, BubbleActivity.class); - target.putExtra(EXTRA_BUBBLE_NOTIF_ID, info.id); - PendingIntent bubbleIntent = PendingIntent.getActivity(mContext, info.id, target, - PendingIntent.FLAG_UPDATE_CURRENT); - - return new Notification.BubbleMetadata.Builder() - .setIntent(bubbleIntent) - .setIcon(info.icon) - .setDesiredHeight(info.height); - } - - public void cancel(int id) { - mNotificationManager.cancel(id); - } - - public void cancelAll() { - mNotificationManager.cancelAll(); - } - - public void cancelLast() { - StatusBarNotification[] activeNotifications = mNotificationManager.getActiveNotifications(); - if (activeNotifications.length > 0) { - mNotificationManager.cancel( - activeNotifications[activeNotifications.length - 1].getId()); - } - } - - public void cancelFirst() { - StatusBarNotification[] activeNotifications = mNotificationManager.getActiveNotifications(); - if (activeNotifications.length > 0) { - mNotificationManager.cancel(activeNotifications[0].getId()); - } - } -} diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/Components.java b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/Components.java deleted file mode 100644 index 0ed59bdafd1d..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/Components.java +++ /dev/null @@ -1,102 +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.testapp; - -import android.content.ComponentName; - -public class Components { - public static final String PACKAGE_NAME = "com.android.wm.shell.flicker.testapp"; - - public static class SimpleActivity { - public static final String LABEL = "SimpleApp"; - public static final ComponentName COMPONENT = new ComponentName(PACKAGE_NAME, - PACKAGE_NAME + ".SimpleActivity"); - } - - public static class FixedActivity { - public static final String EXTRA_FIXED_ORIENTATION = "fixed_orientation"; - public static final String LABEL = "FixedApp"; - public static final ComponentName COMPONENT = new ComponentName(PACKAGE_NAME, - PACKAGE_NAME + ".FixedActivity"); - } - - public static class NonResizeableActivity { - public static final String LABEL = "NonResizeableApp"; - public static final ComponentName COMPONENT = new ComponentName(PACKAGE_NAME, - PACKAGE_NAME + ".NonResizeableActivity"); - } - - public static class PipActivity { - // Test App > Pip Activity - public static final String LABEL = "PipApp"; - public static final String MENU_ACTION_NO_OP = "No-Op"; - public static final String MENU_ACTION_ON = "On"; - public static final String MENU_ACTION_OFF = "Off"; - public static final String MENU_ACTION_CLEAR = "Clear"; - - // Intent action that this activity dynamically registers to enter picture-in-picture - public static final String ACTION_ENTER_PIP = PACKAGE_NAME + ".PipActivity.ENTER_PIP"; - // Intent action that this activity dynamically registers to set requested orientation. - // Will apply the oriention to the value set in the EXTRA_FIXED_ORIENTATION extra. - public static final String ACTION_SET_REQUESTED_ORIENTATION = - PACKAGE_NAME + ".PipActivity.SET_REQUESTED_ORIENTATION"; - - // Calls enterPictureInPicture() on creation - public static final String EXTRA_ENTER_PIP = "enter_pip"; - // Sets the fixed orientation (can be one of {@link ActivityInfo.ScreenOrientation} - public static final String EXTRA_PIP_ORIENTATION = "fixed_orientation"; - // Adds a click listener to finish this activity when it is clicked - public static final String EXTRA_TAP_TO_FINISH = "tap_to_finish"; - - public static final ComponentName COMPONENT = new ComponentName(PACKAGE_NAME, - PACKAGE_NAME + ".PipActivity"); - } - - public static class ImeActivity { - public static final String LABEL = "ImeApp"; - public static final String ACTION_CLOSE_IME = - PACKAGE_NAME + ".action.CLOSE_IME"; - public static final String ACTION_OPEN_IME = - PACKAGE_NAME + ".action.OPEN_IME"; - public static final ComponentName COMPONENT = new ComponentName(PACKAGE_NAME, - PACKAGE_NAME + ".ImeActivity"); - } - - public static class SplitScreenActivity { - public static final String LABEL = "SplitScreenPrimaryApp"; - public static final ComponentName COMPONENT = new ComponentName(PACKAGE_NAME, - PACKAGE_NAME + ".SplitScreenActivity"); - } - - public static class SplitScreenSecondaryActivity { - public static final String LABEL = "SplitScreenSecondaryApp"; - public static final ComponentName COMPONENT = new ComponentName(PACKAGE_NAME, - PACKAGE_NAME + ".SplitScreenSecondaryActivity"); - } - - public static class LaunchBubbleActivity { - public static final String LABEL = "LaunchBubbleApp"; - public static final ComponentName COMPONENT = new ComponentName(PACKAGE_NAME, - PACKAGE_NAME + ".LaunchBubbleActivity"); - } - - public static class BubbleActivity { - public static final String LABEL = "BubbleApp"; - public static final ComponentName COMPONENT = new ComponentName(PACKAGE_NAME, - PACKAGE_NAME + ".BubbleActivity"); - } -} diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/FixedActivity.java b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/FixedActivity.java deleted file mode 100644 index d4ae6c1313bf..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/FixedActivity.java +++ /dev/null @@ -1,35 +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.testapp; - -import static com.android.wm.shell.flicker.testapp.Components.FixedActivity.EXTRA_FIXED_ORIENTATION; - -import android.os.Bundle; - -public class FixedActivity extends SimpleActivity { - - @Override - public void onCreate(Bundle icicle) { - super.onCreate(icicle); - - // Set the fixed orientation if requested - if (getIntent().hasExtra(EXTRA_FIXED_ORIENTATION)) { - final int ori = Integer.parseInt(getIntent().getStringExtra(EXTRA_FIXED_ORIENTATION)); - setRequestedOrientation(ori); - } - } -} diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/ImeActivity.java b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/ImeActivity.java deleted file mode 100644 index 59c64a1345ab..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/ImeActivity.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright (C) 2018 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.wm.shell.flicker.testapp; - -import android.app.Activity; -import android.content.Intent; -import android.os.Bundle; -import android.view.View; -import android.view.WindowManager; -import android.view.inputmethod.InputMethodManager; - -public class ImeActivity extends Activity { - private static final String ACTION_OPEN_IME = - "com.android.wm.shell.flicker.testapp.action.OPEN_IME"; - private static final String ACTION_CLOSE_IME = - "com.android.wm.shell.flicker.testapp.action.CLOSE_IME"; - - private InputMethodManager mImm; - private View mEditText; - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - WindowManager.LayoutParams p = getWindow().getAttributes(); - p.layoutInDisplayCutoutMode = WindowManager.LayoutParams - .LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; - getWindow().setAttributes(p); - setContentView(R.layout.activity_ime); - - mEditText = findViewById(R.id.plain_text_input); - mImm = getSystemService(InputMethodManager.class); - - handleIntent(getIntent()); - } - - @Override - protected void onNewIntent(Intent intent) { - super.onNewIntent(intent); - handleIntent(intent); - } - - private void handleIntent(Intent intent) { - final String action = intent.getAction(); - if (ACTION_OPEN_IME.equals(action)) { - mEditText.requestFocus(); - mImm.showSoftInput(mEditText, InputMethodManager.SHOW_FORCED); - } else if (ACTION_CLOSE_IME.equals(action)) { - mImm.hideSoftInputFromWindow(mEditText.getWindowToken(), 0); - mEditText.clearFocus(); - } - } -} diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/LaunchBubbleActivity.java b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/LaunchBubbleActivity.java deleted file mode 100644 index 71fa66d8a61c..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/LaunchBubbleActivity.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.flicker.testapp; - - -import android.app.Activity; -import android.app.Person; -import android.content.Context; -import android.content.Intent; -import android.content.pm.ShortcutInfo; -import android.content.pm.ShortcutManager; -import android.graphics.drawable.Icon; -import android.os.Bundle; -import android.view.View; - -import java.util.Arrays; - -public class LaunchBubbleActivity extends Activity { - - private BubbleHelper mBubbleHelper; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - addInboxShortcut(getApplicationContext()); - mBubbleHelper = BubbleHelper.getInstance(this); - setContentView(R.layout.activity_main); - findViewById(R.id.button_create).setOnClickListener(this::add); - findViewById(R.id.button_cancel).setOnClickListener(this::cancel); - findViewById(R.id.button_cancel_all).setOnClickListener(this::cancelAll); - } - - private void add(View v) { - mBubbleHelper.addNewBubble(false /* autoExpand */, false /* suppressNotif */); - } - - private void cancel(View v) { - mBubbleHelper.cancelLast(); - } - - private void cancelAll(View v) { - mBubbleHelper.cancelAll(); - } - - private void addInboxShortcut(Context context) { - Icon icon = Icon.createWithResource(this, R.drawable.bg); - Person[] persons = new Person[4]; - for (int i = 0; i < persons.length; i++) { - persons[i] = new Person.Builder() - .setBot(false) - .setIcon(icon) - .setName("google" + i) - .setImportant(true) - .build(); - } - - ShortcutInfo shortcut = new ShortcutInfo.Builder(context, "BubbleChat") - .setShortLabel("BubbleChat") - .setLongLived(true) - .setIntent(new Intent(Intent.ACTION_VIEW)) - .setIcon(Icon.createWithResource(context, R.drawable.ic_message)) - .setPersons(persons) - .build(); - ShortcutManager scmanager = context.getSystemService(ShortcutManager.class); - scmanager.addDynamicShortcuts(Arrays.asList(shortcut)); - } - -} diff --git a/libs/WindowManager/Shell/tests/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 deleted file mode 100644 index a6ba7823e22d..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/PipActivity.java +++ /dev/null @@ -1,281 +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.testapp; - -import static android.media.MediaMetadata.METADATA_KEY_TITLE; -import static android.media.session.PlaybackState.ACTION_PAUSE; -import static android.media.session.PlaybackState.ACTION_PLAY; -import static android.media.session.PlaybackState.ACTION_STOP; -import static android.media.session.PlaybackState.STATE_PAUSED; -import static android.media.session.PlaybackState.STATE_PLAYING; -import static android.media.session.PlaybackState.STATE_STOPPED; - -import static com.android.wm.shell.flicker.testapp.Components.PipActivity.ACTION_ENTER_PIP; -import static com.android.wm.shell.flicker.testapp.Components.PipActivity.ACTION_SET_REQUESTED_ORIENTATION; -import static com.android.wm.shell.flicker.testapp.Components.PipActivity.EXTRA_ENTER_PIP; -import static com.android.wm.shell.flicker.testapp.Components.PipActivity.EXTRA_PIP_ORIENTATION; - -import android.app.Activity; -import android.app.PendingIntent; -import android.app.PictureInPictureParams; -import android.app.RemoteAction; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.graphics.drawable.Icon; -import android.media.MediaMetadata; -import android.media.session.MediaSession; -import android.media.session.PlaybackState; -import android.os.Bundle; -import android.util.Log; -import android.util.Rational; -import android.view.View; -import android.view.Window; -import android.view.WindowManager; -import android.widget.CheckBox; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; - -public class PipActivity extends Activity { - private static final String TAG = PipActivity.class.getSimpleName(); - /** - * A media session title for when the session is in {@link STATE_PLAYING}. - * TvPipNotificationTests check whether the actual notification title matches this string. - */ - private static final String TITLE_STATE_PLAYING = "TestApp media is playing"; - /** - * A media session title for when the session is in {@link STATE_PAUSED}. - * TvPipNotificationTests check whether the actual notification title matches this string. - */ - private static final String TITLE_STATE_PAUSED = "TestApp media is paused"; - - private static final Rational RATIO_DEFAULT = null; - private static final Rational RATIO_SQUARE = new Rational(1, 1); - private static final Rational RATIO_WIDE = new Rational(2, 1); - private static final Rational RATIO_TALL = new Rational(1, 2); - - private static final String PIP_ACTION_NO_OP = "No-Op"; - private static final String PIP_ACTION_OFF = "Off"; - private static final String PIP_ACTION_ON = "On"; - private static final String PIP_ACTION_CLEAR = "Clear"; - private static final String ACTION_NO_OP = "com.android.wm.shell.flicker.testapp.NO_OP"; - private static final String ACTION_SWITCH_OFF = - "com.android.wm.shell.flicker.testapp.SWITCH_OFF"; - private static final String ACTION_SWITCH_ON = "com.android.wm.shell.flicker.testapp.SWITCH_ON"; - private static final String ACTION_CLEAR = "com.android.wm.shell.flicker.testapp.CLEAR"; - - private final PictureInPictureParams.Builder mPipParamsBuilder = - new PictureInPictureParams.Builder() - .setAspectRatio(RATIO_DEFAULT); - private MediaSession mMediaSession; - private final PlaybackState.Builder mPlaybackStateBuilder = new PlaybackState.Builder() - .setActions(ACTION_PLAY | ACTION_PAUSE | ACTION_STOP) - .setState(STATE_STOPPED, 0, 1f); - private PlaybackState mPlaybackState = mPlaybackStateBuilder.build(); - private final MediaMetadata.Builder mMediaMetadataBuilder = new MediaMetadata.Builder(); - - private final List<RemoteAction> mSwitchOffActions = new ArrayList<>(); - private final List<RemoteAction> mSwitchOnActions = new ArrayList<>(); - private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - if (isInPictureInPictureMode()) { - switch (intent.getAction()) { - case ACTION_SWITCH_ON: - mPipParamsBuilder.setActions(mSwitchOnActions); - break; - case ACTION_SWITCH_OFF: - mPipParamsBuilder.setActions(mSwitchOffActions); - break; - case ACTION_CLEAR: - mPipParamsBuilder.setActions(Collections.emptyList()); - break; - case ACTION_NO_OP: - return; - default: - Log.w(TAG, "Unhandled action=" + intent.getAction()); - return; - } - setPictureInPictureParams(mPipParamsBuilder.build()); - } else { - switch (intent.getAction()) { - case ACTION_ENTER_PIP: - enterPip(null); - break; - case ACTION_SET_REQUESTED_ORIENTATION: - setRequestedOrientation(Integer.parseInt(intent.getStringExtra( - EXTRA_PIP_ORIENTATION))); - break; - default: - Log.w(TAG, "Unhandled action=" + intent.getAction()); - return; - } - } - } - }; - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - final Window window = getWindow(); - final WindowManager.LayoutParams layoutParams = window.getAttributes(); - layoutParams.layoutInDisplayCutoutMode = WindowManager.LayoutParams - .LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; - window.setAttributes(layoutParams); - - setContentView(R.layout.activity_pip); - - findViewById(R.id.media_session_start) - .setOnClickListener(v -> updateMediaSessionState(STATE_PLAYING)); - findViewById(R.id.media_session_stop) - .setOnClickListener(v -> updateMediaSessionState(STATE_STOPPED)); - - mMediaSession = new MediaSession(this, "WMShell_TestApp"); - mMediaSession.setPlaybackState(mPlaybackStateBuilder.build()); - mMediaSession.setCallback(new MediaSession.Callback() { - @Override - public void onPlay() { - updateMediaSessionState(STATE_PLAYING); - } - - @Override - public void onPause() { - updateMediaSessionState(STATE_PAUSED); - } - - @Override - public void onStop() { - updateMediaSessionState(STATE_STOPPED); - } - }); - - // Build two sets of the custom actions. We'll replace one with the other when 'On'/'Off' - // action is invoked. - // The first set consists of 3 actions: 1) Off; 2) No-Op; 3) Clear. - // The second set consists of 2 actions: 1) On; 2) Clear. - // Upon invocation 'Clear' action clear-off all the custom actions, including itself. - final Icon icon = Icon.createWithResource(this, android.R.drawable.ic_menu_help); - final RemoteAction noOpAction = buildRemoteAction(icon, PIP_ACTION_NO_OP, ACTION_NO_OP); - final RemoteAction switchOnAction = - buildRemoteAction(icon, PIP_ACTION_ON, ACTION_SWITCH_ON); - final RemoteAction switchOffAction = - buildRemoteAction(icon, PIP_ACTION_OFF, ACTION_SWITCH_OFF); - final RemoteAction clearAllAction = buildRemoteAction(icon, PIP_ACTION_CLEAR, ACTION_CLEAR); - mSwitchOffActions.addAll(Arrays.asList(switchOnAction, clearAllAction)); - mSwitchOnActions.addAll(Arrays.asList(noOpAction, switchOffAction, clearAllAction)); - - final IntentFilter filter = new IntentFilter(); - filter.addAction(ACTION_NO_OP); - filter.addAction(ACTION_SWITCH_ON); - filter.addAction(ACTION_SWITCH_OFF); - filter.addAction(ACTION_CLEAR); - filter.addAction(ACTION_SET_REQUESTED_ORIENTATION); - filter.addAction(ACTION_ENTER_PIP); - registerReceiver(mBroadcastReceiver, filter); - - handleIntentExtra(getIntent()); - } - - @Override - protected void onDestroy() { - unregisterReceiver(mBroadcastReceiver); - super.onDestroy(); - } - - private RemoteAction buildRemoteAction(Icon icon, String label, String action) { - final Intent intent = new Intent(action); - final PendingIntent pendingIntent = - PendingIntent.getBroadcast(this, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT); - return new RemoteAction(icon, label, label, pendingIntent); - } - - public void enterPip(View v) { - final boolean withCustomActions = - ((CheckBox) findViewById(R.id.with_custom_actions)).isChecked(); - mPipParamsBuilder.setActions( - withCustomActions ? mSwitchOnActions : Collections.emptyList()); - enterPictureInPictureMode(mPipParamsBuilder.build()); - } - - public void onRatioSelected(View v) { - switch (v.getId()) { - case R.id.ratio_default: - mPipParamsBuilder.setAspectRatio(RATIO_DEFAULT); - break; - - case R.id.ratio_square: - mPipParamsBuilder.setAspectRatio(RATIO_SQUARE); - break; - - case R.id.ratio_wide: - mPipParamsBuilder.setAspectRatio(RATIO_WIDE); - break; - - case R.id.ratio_tall: - mPipParamsBuilder.setAspectRatio(RATIO_TALL); - break; - } - } - - private void updateMediaSessionState(int newState) { - if (mPlaybackState.getState() == newState) { - return; - } - final String title; - switch (newState) { - case STATE_PLAYING: - title = TITLE_STATE_PLAYING; - break; - case STATE_PAUSED: - title = TITLE_STATE_PAUSED; - break; - case STATE_STOPPED: - title = ""; - break; - - default: - throw new IllegalArgumentException("Unknown state " + newState); - } - - mPlaybackStateBuilder.setState(newState, 0, 1f); - mPlaybackState = mPlaybackStateBuilder.build(); - - mMediaMetadataBuilder.putText(METADATA_KEY_TITLE, title); - - mMediaSession.setPlaybackState(mPlaybackState); - mMediaSession.setMetadata(mMediaMetadataBuilder.build()); - mMediaSession.setActive(newState != STATE_STOPPED); - } - - private void handleIntentExtra(Intent intent) { - // Set the fixed orientation if requested - if (intent.hasExtra(EXTRA_PIP_ORIENTATION)) { - final int ori = Integer.parseInt(getIntent().getStringExtra(EXTRA_PIP_ORIENTATION)); - setRequestedOrientation(ori); - } - // Enter picture in picture with the given aspect ratio if provided - if (intent.hasExtra(EXTRA_ENTER_PIP)) { - mPipParamsBuilder.setActions(mSwitchOnActions); - enterPip(null); - } - } -} diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/SimpleActivity.java b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/SimpleActivity.java deleted file mode 100644 index 5343c1893d4e..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/SimpleActivity.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright (C) 2018 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.wm.shell.flicker.testapp; - -import android.app.Activity; -import android.os.Bundle; -import android.view.WindowManager; - -public class SimpleActivity extends Activity { - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - WindowManager.LayoutParams p = getWindow().getAttributes(); - p.layoutInDisplayCutoutMode = WindowManager.LayoutParams - .LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; - getWindow().setAttributes(p); - setContentView(R.layout.activity_simple); - } -} diff --git a/libs/WindowManager/Shell/tests/unittest/Android.bp b/libs/WindowManager/Shell/tests/unittest/Android.bp index ea10be564351..57a698128d77 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,14 @@ android_test { optimize: { enabled: false, }, + + test_suites: ["device-tests"], + + platform_apis: true, + certificate: "platform", + + aaptflags: [ + "--extra-packages", + "com.android.wm.shell.tests", + ], } diff --git a/libs/WindowManager/Shell/tests/unittest/AndroidManifest.xml b/libs/WindowManager/Shell/tests/unittest/AndroidManifest.xml index 59d9104fb5ba..47a116be1b66 100644 --- a/libs/WindowManager/Shell/tests/unittest/AndroidManifest.xml +++ b/libs/WindowManager/Shell/tests/unittest/AndroidManifest.xml @@ -19,6 +19,9 @@ xmlns:tools="http://schemas.android.com/tools" package="com.android.wm.shell.tests"> + <uses-permission android:name="android.permission.READ_DEVICE_CONFIG" /> + <uses-permission android:name="android.permission.VIBRATE"/> + <application android:debuggable="true" android:largeHeap="true"> <uses-library android:name="android.test.mock" /> <uses-library android:name="android.test.runner" /> diff --git a/libs/WindowManager/Shell/tests/unittest/res/values/dimen.xml b/libs/WindowManager/Shell/tests/unittest/res/values/dimen.xml new file mode 100644 index 000000000000..aa1b24189274 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/res/values/dimen.xml @@ -0,0 +1,29 @@ +<?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> + <!-- Resources used in WindowDecorationTests --> + <dimen name="test_freeform_decor_caption_height">32dp</dimen> + <dimen name="test_freeform_decor_caption_menu_width">216dp</dimen> + <dimen name="test_window_decor_left_outset">10dp</dimen> + <dimen name="test_window_decor_top_outset">20dp</dimen> + <dimen name="test_window_decor_right_outset">30dp</dimen> + <dimen name="test_window_decor_bottom_outset">40dp</dimen> + <dimen name="test_window_decor_shadow_radius">5dp</dimen> + <dimen name="test_window_decor_resize_handle">10dp</dimen> + <dimen name="test_caption_menu_shadow_radius">4dp</dimen> + <dimen name="test_caption_menu_corner_radius">20dp</dimen> +</resources>
\ No newline at end of file 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..081c8ae91bdb 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 @@ -26,19 +26,21 @@ 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.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 android.app.ActivityManager.RunningTaskInfo; import android.app.TaskInfo; -import android.content.Context; import android.content.LocusId; import android.content.pm.ParceledListSlice; import android.os.Binder; @@ -46,6 +48,7 @@ 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; @@ -55,9 +58,9 @@ 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 +79,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 +135,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 { @@ -607,5 +634,4 @@ public class ShellTaskOrganizerTests { taskInfo.configuration.windowConfiguration.setWindowingMode(windowingMode); return taskInfo; } - } 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..51a20ee9d090 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 @@ -17,13 +17,17 @@ package com.android.wm.shell; import static android.view.Display.DEFAULT_DISPLAY; +import static org.junit.Assume.assumeTrue; import android.content.Context; +import android.content.pm.PackageManager; import android.hardware.display.DisplayManager; 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; @@ -34,13 +38,18 @@ import org.mockito.MockitoAnnotations; public abstract class ShellTestCase { protected TestableContext mContext; + private PackageManager mPm; @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(); final DisplayManager dm = context.getSystemService(DisplayManager.class); + mPm = context.getPackageManager(); mContext = new TestableContext( context.createDisplayContext(dm.getDisplay(DEFAULT_DISPLAY))); @@ -61,4 +70,20 @@ public abstract class ShellTestCase { protected Context getContext() { return mContext; } + + /** + * Makes an assumption that the test device is a TV device, used to guard tests that should + * only be run on TVs. + */ + protected void assumeTelevision() { + assumeTrue(isTelevision()); + } + + /** + * Returns whether this test device is a TV device. + */ + protected boolean isTelevision() { + return mPm.hasSystemFeature(PackageManager.FEATURE_LEANBACK) + || mPm.hasSystemFeature(PackageManager.FEATURE_LEANBACK_ONLY); + } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestRunningTaskInfoBuilder.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestRunningTaskInfoBuilder.java index 51eec27cfc0e..3672ae386dc4 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestRunningTaskInfoBuilder.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestRunningTaskInfoBuilder.java @@ -25,8 +25,10 @@ import static org.mockito.Mockito.mock; import android.app.ActivityManager; import android.app.WindowConfiguration; +import android.graphics.Point; import android.graphics.Rect; import android.os.IBinder; +import android.view.Display; import android.window.IWindowContainerToken; import android.window.WindowContainerToken; @@ -38,6 +40,11 @@ public final class TestRunningTaskInfoBuilder { private int mParentTaskId = INVALID_TASK_ID; private @WindowConfiguration.ActivityType int mActivityType = ACTIVITY_TYPE_STANDARD; private @WindowConfiguration.WindowingMode int mWindowingMode = WINDOWING_MODE_UNDEFINED; + private int mDisplayId = Display.DEFAULT_DISPLAY; + private ActivityManager.TaskDescription.Builder mTaskDescriptionBuilder = null; + private final Point mPositionInParent = new Point(); + private boolean mIsVisible = false; + private long mLastActiveTime; public static WindowContainerToken createMockWCToken() { final IWindowContainerToken itoken = mock(IWindowContainerToken.class); @@ -46,6 +53,11 @@ public final class TestRunningTaskInfoBuilder { return new WindowContainerToken(itoken); } + public TestRunningTaskInfoBuilder setToken(WindowContainerToken token) { + mToken = token; + return this; + } + public TestRunningTaskInfoBuilder setBounds(Rect bounds) { mBounds.set(bounds); return this; @@ -68,17 +80,48 @@ public final class TestRunningTaskInfoBuilder { return this; } + public TestRunningTaskInfoBuilder setDisplayId(int displayId) { + mDisplayId = displayId; + return this; + } + + public TestRunningTaskInfoBuilder setTaskDescriptionBuilder( + ActivityManager.TaskDescription.Builder builder) { + mTaskDescriptionBuilder = builder; + return this; + } + + public TestRunningTaskInfoBuilder setPositionInParent(int x, int y) { + mPositionInParent.set(x, y); + return this; + } + + public TestRunningTaskInfoBuilder setVisible(boolean isVisible) { + mIsVisible = isVisible; + return this; + } + + public TestRunningTaskInfoBuilder setLastActiveTime(long lastActiveTime) { + mLastActiveTime = lastActiveTime; + return this; + } + public ActivityManager.RunningTaskInfo build() { final ActivityManager.RunningTaskInfo info = new ActivityManager.RunningTaskInfo(); - info.parentTaskId = INVALID_TASK_ID; info.taskId = sNextTaskId++; info.parentTaskId = mParentTaskId; + info.displayId = mDisplayId; info.configuration.windowConfiguration.setBounds(mBounds); info.configuration.windowConfiguration.setActivityType(mActivityType); info.configuration.windowConfiguration.setWindowingMode(mWindowingMode); info.token = mToken; info.isResizeable = true; info.supportsMultiWindow = true; + info.taskDescription = + mTaskDescriptionBuilder != null ? mTaskDescriptionBuilder.build() : null; + info.positionInParent = mPositionInParent; + info.isVisible = mIsVisible; + info.lastActiveTime = mLastActiveTime; return info; } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TransitionInfoBuilder.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TransitionInfoBuilder.java new file mode 100644 index 000000000000..a658375ca38a --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TransitionInfoBuilder.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; + +import static org.mockito.Mockito.mock; + +import android.app.ActivityManager; +import android.view.SurfaceControl; +import android.view.WindowManager; +import android.window.TransitionInfo; + +/** + * Utility for creating/editing synthetic TransitionInfos for tests. + */ +public class TransitionInfoBuilder { + final TransitionInfo mInfo; + static final int DISPLAY_ID = 0; + + public TransitionInfoBuilder(@WindowManager.TransitionType int type) { + this(type, 0 /* flags */); + } + + public TransitionInfoBuilder(@WindowManager.TransitionType int type, + @WindowManager.TransitionFlags int flags) { + this(type, flags, false /* asNoOp */); + } + + public TransitionInfoBuilder(@WindowManager.TransitionType int type, + @WindowManager.TransitionFlags int flags, boolean asNoOp) { + mInfo = new TransitionInfo(type, flags); + if (!asNoOp) { + mInfo.addRootLeash(DISPLAY_ID, createMockSurface(true /* valid */), 0, 0); + } + } + + public TransitionInfoBuilder addChange(@WindowManager.TransitionType int mode, + @TransitionInfo.ChangeFlags int flags, ActivityManager.RunningTaskInfo taskInfo) { + final TransitionInfo.Change change = new TransitionInfo.Change( + taskInfo != null ? taskInfo.token : null, createMockSurface(true /* valid */)); + change.setMode(mode); + change.setFlags(flags); + change.setTaskInfo(taskInfo); + return addChange(change); + } + + public TransitionInfoBuilder addChange(@WindowManager.TransitionType int mode, + ActivityManager.RunningTaskInfo taskInfo) { + return addChange(mode, TransitionInfo.FLAG_NONE, taskInfo); + } + + public TransitionInfoBuilder addChange(@WindowManager.TransitionType int mode) { + return addChange(mode, TransitionInfo.FLAG_NONE, null /* taskInfo */); + } + + public TransitionInfoBuilder addChange(TransitionInfo.Change change) { + change.setDisplayId(DISPLAY_ID, DISPLAY_ID); + mInfo.addChange(change); + return this; + } + + public TransitionInfo build() { + return mInfo; + } + + private static SurfaceControl createMockSurface(boolean valid) { + SurfaceControl sc = mock(SurfaceControl.class); + doReturn(valid).when(sc).isValid(); + doReturn("TestSurface").when(sc).toString(); + return sc; + } +} 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..a625346e69c0 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunnerTests.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.activityembedding; + +import static android.view.WindowManager.TRANSIT_OPEN; +import static android.window.TransitionInfo.FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY; +import static android.window.TransitionInfo.FLAG_IS_BEHIND_STARTING_WINDOW; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +import android.animation.Animator; +import android.window.TransitionInfo; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import com.android.wm.shell.TransitionInfoBuilder; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; + +import java.util.ArrayList; + +/** + * 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 TransitionInfoBuilder(TRANSIT_OPEN, 0) + .addChange(createChange(FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY)) + .build(); + doReturn(mAnimator).when(mAnimRunner).createAnimator(any(), 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(), any()); + verify(mStartTransaction).apply(); + verify(mAnimator).start(); + verifyNoMoreInteractions(mFinishTransaction); + verify(mController, never()).onAnimationFinished(any()); + + // Call onAnimationFinished() when the animation is finished. + finishCallback.getValue().run(); + + verify(mController).onAnimationFinished(mTransition); + } + + @Test + public void testChangesBehindStartingWindow() { + final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN, 0) + .addChange(createChange(FLAG_IS_BEHIND_STARTING_WINDOW)) + .build(); + final Animator animator = mAnimRunner.createAnimator( + info, mStartTransaction, mFinishTransaction, + () -> mFinishCallback.onTransitionFinished(null /* wct */, null /* wctCB */), + new ArrayList()); + + // The animation should be empty when it is behind starting window. + assertEquals(0, animator.getDuration()); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationTestBase.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationTestBase.java new file mode 100644 index 000000000000..4f4f356ef2e6 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationTestBase.java @@ -0,0 +1,109 @@ +/* + * 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 + Animator mAnimator; + + ActivityEmbeddingController mController; + ActivityEmbeddingAnimationRunner mAnimRunner; + ActivityEmbeddingAnimationSpec mAnimSpec; + Transitions.TransitionFinishCallback mFinishCallback; + + @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); + mFinishCallback = (wct, wctCB) -> {}; + spyOn(mController); + spyOn(mAnimRunner); + spyOn(mAnimSpec); + spyOn(mFinishCallback); + } + + /** Creates a mock {@link TransitionInfo.Change}. */ + static TransitionInfo.Change createChange(@TransitionInfo.ChangeFlags int flags) { + TransitionInfo.Change c = new TransitionInfo.Change(mock(WindowContainerToken.class), + mock(SurfaceControl.class)); + c.setFlags(flags); + return c; + } + + /** + * 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(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..cbbb29199d75 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingControllerTests.java @@ -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.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 com.android.wm.shell.TransitionInfoBuilder; + +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(), 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 TransitionInfoBuilder(TRANSIT_OPEN, 0) + .addChange(createEmbeddedChange( + EMBEDDED_LEFT_BOUNDS, EMBEDDED_LEFT_BOUNDS, TASK_BOUNDS)) + .addChange(createChange(0 /* flags */)) + .build(); + + // 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 TransitionInfoBuilder(TRANSIT_OPEN, 0) + .addChange(createEmbeddedChange(TASK_BOUNDS, TASK_BOUNDS, TASK_BOUNDS)) + .build(); + + // 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 TransitionInfoBuilder(TRANSIT_OPEN, 0) + .addChange(createEmbeddedChange( + EMBEDDED_LEFT_BOUNDS, EMBEDDED_LEFT_BOUNDS, TASK_BOUNDS)) + .build(); + + // 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 TransitionInfoBuilder(TRANSIT_OPEN, 0) + .addChange(createEmbeddedChange(TASK_BOUNDS, EMBEDDED_LEFT_BOUNDS, TASK_BOUNDS)) + .build(); + + // 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 TransitionInfoBuilder(TRANSIT_OPEN, 0) + .addChange(createEmbeddedChange(EMBEDDED_RIGHT_BOUNDS, TASK_BOUNDS, TASK_BOUNDS)) + .build(); + + // 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 TransitionInfoBuilder(TRANSIT_OPEN, 0) + .addChange(createEmbeddedChange( + EMBEDDED_LEFT_BOUNDS, EMBEDDED_LEFT_BOUNDS, TASK_BOUNDS)) + .build(); + 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..806bffebd4cb 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 @@ -18,16 +18,16 @@ package com.android.wm.shell.back; 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.anyBoolean; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; @@ -37,7 +37,7 @@ import android.app.WindowConfiguration; import android.content.pm.ApplicationInfo; import android.graphics.Point; import android.graphics.Rect; -import android.hardware.HardwareBuffer; +import android.os.Bundle; import android.os.Handler; import android.os.RemoteCallback; import android.os.RemoteException; @@ -46,21 +46,28 @@ import android.testing.AndroidTestingRunner; import android.testing.TestableContentResolver; import android.testing.TestableContext; import android.testing.TestableLooper; +import android.view.IRemoteAnimationRunner; import android.view.MotionEvent; import android.view.RemoteAnimationTarget; import android.view.SurfaceControl; import android.window.BackEvent; +import android.window.BackMotionEvent; import android.window.BackNavigationInfo; +import android.window.IBackAnimationFinishedCallback; import android.window.IOnBackInvokedCallback; +import androidx.annotation.Nullable; 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.ShellController; +import com.android.wm.shell.sysui.ShellInit; +import com.android.wm.shell.sysui.ShellSharedConstants; import org.junit.Before; -import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -74,27 +81,38 @@ 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 = new TestableContext(InstrumentationRegistry.getInstrumentation().getContext()); @Mock - private SurfaceControl.Transaction mTransaction; + private IActivityTaskManager mActivityTaskManager; @Mock - private IActivityTaskManager mActivityTaskManager; + private IOnBackInvokedCallback mAppCallback; @Mock - private IOnBackInvokedCallback mIOnBackInvokedCallback; + private IOnBackInvokedCallback mAnimatorCallback; - private BackAnimationController mController; + @Mock + private IBackAnimationFinishedCallback mBackAnimationFinishedCallback; + + @Mock + private IRemoteAnimationRunner mBackAnimationRunner; + + @Mock + private ShellController mShellController; + + @Mock + private BackAnimationBackground mAnimationBackground; - private int mEventTime = 0; + private BackAnimationController mController; private TestableContentResolver mContentResolver; private TestableLooper mTestableLooper; @@ -107,37 +125,30 @@ public class BackAnimationControllerTest { Settings.Global.putString(mContentResolver, Settings.Global.ENABLE_BACK_ANIMATION, ANIMATION_ENABLED); mTestableLooper = TestableLooper.get(this); - mController = new BackAnimationController( - mShellExecutor, new Handler(mTestableLooper.getLooper()), mTransaction, + mShellInit = spy(new ShellInit(mShellExecutor)); + mController = new BackAnimationController(mShellInit, mShellController, + mShellExecutor, new Handler(mTestableLooper.getLooper()), mActivityTaskManager, mContext, - mContentResolver); - mEventTime = 0; + mContentResolver, mAnimationBackground); + mController.setEnableUAnimation(true); + mShellInit.init(); mShellExecutor.flushAll(); } - private void createNavigationInfo(RemoteAnimationTarget topAnimationTarget, - SurfaceControl screenshotSurface, - HardwareBuffer hardwareBuffer, - int backType, - IOnBackInvokedCallback onBackInvokedCallback) { - BackNavigationInfo navigationInfo = new BackNavigationInfo( - backType, - topAnimationTarget, - screenshotSurface, - hardwareBuffer, - new WindowConfiguration(), - new RemoteCallback((bundle) -> {}), - onBackInvokedCallback); - try { - doReturn(navigationInfo).when(mActivityTaskManager).startBackNavigation(anyBoolean()); - } catch (RemoteException ex) { - ex.rethrowFromSystemServer(); - } + private void createNavigationInfo(int backType, boolean enableAnimation) { + BackNavigationInfo.Builder builder = new BackNavigationInfo.Builder() + .setType(backType) + .setOnBackNavigationDone(new RemoteCallback((bundle) -> {})) + .setOnBackInvokedCallback(mAppCallback) + .setPrepareRemoteAnimation(enableAnimation); + + createNavigationInfo(builder); } private void createNavigationInfo(BackNavigationInfo.Builder builder) { try { - doReturn(builder.build()).when(mActivityTaskManager).startBackNavigation(anyBoolean()); + doReturn(builder.build()).when(mActivityTaskManager) + .startBackNavigation(any(), any()); } catch (RemoteException ex) { ex.rethrowFromSystemServer(); } @@ -159,141 +170,290 @@ public class BackAnimationControllerTest { } @Test - @Ignore("b/207481538") - public void crossActivity_screenshotAttachedAndVisible() { - SurfaceControl screenshotSurface = new SurfaceControl(); - HardwareBuffer hardwareBuffer = mock(HardwareBuffer.class); - createNavigationInfo(createAnimationTarget(), screenshotSurface, hardwareBuffer, - BackNavigationInfo.TYPE_CROSS_ACTIVITY, null); - doMotionEvent(MotionEvent.ACTION_DOWN, 0); - verify(mTransaction).setBuffer(screenshotSurface, hardwareBuffer); - verify(mTransaction).setVisibility(screenshotSurface, true); - verify(mTransaction).apply(); + public void instantiateController_addInitCallback() { + verify(mShellInit, times(1)).addInitCallback(any(), any()); } @Test - public void crossActivity_surfaceMovesWithGesture() { - SurfaceControl screenshotSurface = new SurfaceControl(); - HardwareBuffer hardwareBuffer = mock(HardwareBuffer.class); - RemoteAnimationTarget animationTarget = createAnimationTarget(); - createNavigationInfo(animationTarget, screenshotSurface, hardwareBuffer, - BackNavigationInfo.TYPE_CROSS_ACTIVITY, null); - doMotionEvent(MotionEvent.ACTION_DOWN, 0); - doMotionEvent(MotionEvent.ACTION_MOVE, 100); - // b/207481538, we check that the surface is not moved for now, we can re-enable this once - // we implement the animation - verify(mTransaction, never()).setScale(eq(screenshotSurface), anyInt(), anyInt()); - verify(mTransaction, never()).setPosition( - animationTarget.leash, 100, 100); - verify(mTransaction, atLeastOnce()).apply(); + public void instantiateController_addExternalInterface() { + verify(mShellController, times(1)).addExternalInterface( + eq(ShellSharedConstants.KEY_EXTRA_SHELL_BACK_ANIMATION), any(), any()); } @Test - public void verifyAnimationFinishes() { - RemoteAnimationTarget animationTarget = createAnimationTarget(); - boolean[] backNavigationDone = new boolean[]{false}; - boolean[] triggerBack = new boolean[]{false}; - createNavigationInfo(new BackNavigationInfo.Builder() - .setDepartingAnimationTarget(animationTarget) - .setType(BackNavigationInfo.TYPE_CROSS_ACTIVITY) - .setOnBackNavigationDone( - new RemoteCallback(result -> { - backNavigationDone[0] = true; - triggerBack[0] = result.getBoolean(KEY_TRIGGER_BACK); - }))); - triggerBackGesture(); - assertTrue("Navigation Done callback not called", backNavigationDone[0]); - assertTrue("TriggerBack should have been true", triggerBack[0]); + public void verifyNavigationFinishes() throws RemoteException { + final int[] testTypes = new int[] {BackNavigationInfo.TYPE_RETURN_TO_HOME, + BackNavigationInfo.TYPE_CROSS_TASK, + BackNavigationInfo.TYPE_CROSS_ACTIVITY, + BackNavigationInfo.TYPE_DIALOG_CLOSE, + BackNavigationInfo.TYPE_CALLBACK }; + + for (int type: testTypes) { + registerAnimation(type); + } + + for (int type: testTypes) { + final ResultListener result = new ResultListener(); + createNavigationInfo(new BackNavigationInfo.Builder() + .setType(type) + .setOnBackInvokedCallback(mAppCallback) + .setPrepareRemoteAnimation(true) + .setOnBackNavigationDone(new RemoteCallback(result))); + triggerBackGesture(); + simulateRemoteAnimationStart(type); + simulateRemoteAnimationFinished(); + mShellExecutor.flushAll(); + + assertTrue("Navigation Done callback not called for " + + BackNavigationInfo.typeToString(type), result.mBackNavigationDone); + assertTrue("TriggerBack should have been true", result.mTriggerBack); + } } @Test public void backToHome_dispatchesEvents() throws RemoteException { - mController.setBackToLauncherCallback(mIOnBackInvokedCallback); - RemoteAnimationTarget animationTarget = createAnimationTarget(); - createNavigationInfo(animationTarget, null, null, - BackNavigationInfo.TYPE_RETURN_TO_HOME, null); + registerAnimation(BackNavigationInfo.TYPE_RETURN_TO_HOME); + createNavigationInfo(BackNavigationInfo.TYPE_RETURN_TO_HOME, true); doMotionEvent(MotionEvent.ACTION_DOWN, 0); // Check that back start and progress is dispatched when first move. doMotionEvent(MotionEvent.ACTION_MOVE, 100); - verify(mIOnBackInvokedCallback).onBackStarted(); - ArgumentCaptor<BackEvent> backEventCaptor = ArgumentCaptor.forClass(BackEvent.class); - verify(mIOnBackInvokedCallback).onBackProgressed(backEventCaptor.capture()); - assertEquals(animationTarget, backEventCaptor.getValue().getDepartingAnimationTarget()); + + simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME); + + verify(mAnimatorCallback).onBackStarted(any(BackMotionEvent.class)); + verify(mBackAnimationRunner).onAnimationStart(anyInt(), any(), any(), any(), any()); + ArgumentCaptor<BackMotionEvent> backEventCaptor = + ArgumentCaptor.forClass(BackMotionEvent.class); + verify(mAnimatorCallback, atLeastOnce()).onBackProgressed(backEventCaptor.capture()); // Check that back invocation is dispatched. mController.setTriggerBack(true); // Fake trigger back doMotionEvent(MotionEvent.ACTION_UP, 0); - verify(mIOnBackInvokedCallback).onBackInvoked(); + verify(mAnimatorCallback).onBackInvoked(); } @Test public void animationDisabledFromSettings() throws RemoteException { // Toggle the setting off Settings.Global.putString(mContentResolver, Settings.Global.ENABLE_BACK_ANIMATION, "0"); - mController = new BackAnimationController( - mShellExecutor, new Handler(mTestableLooper.getLooper()), mTransaction, + ShellInit shellInit = new ShellInit(mShellExecutor); + mController = new BackAnimationController(shellInit, mShellController, + mShellExecutor, new Handler(mTestableLooper.getLooper()), mActivityTaskManager, mContext, - mContentResolver); - mController.setBackToLauncherCallback(mIOnBackInvokedCallback); + mContentResolver, mAnimationBackground); + shellInit.init(); + registerAnimation(BackNavigationInfo.TYPE_RETURN_TO_HOME); - RemoteAnimationTarget animationTarget = createAnimationTarget(); - IOnBackInvokedCallback appCallback = mock(IOnBackInvokedCallback.class); - ArgumentCaptor<BackEvent> backEventCaptor = ArgumentCaptor.forClass(BackEvent.class); - createNavigationInfo(animationTarget, null, null, - BackNavigationInfo.TYPE_RETURN_TO_HOME, appCallback); + ArgumentCaptor<BackMotionEvent> backEventCaptor = + ArgumentCaptor.forClass(BackMotionEvent.class); + + createNavigationInfo(BackNavigationInfo.TYPE_RETURN_TO_HOME, false); triggerBackGesture(); - verify(appCallback, never()).onBackStarted(); - verify(appCallback, never()).onBackProgressed(backEventCaptor.capture()); - verify(appCallback, times(1)).onBackInvoked(); + verify(mAppCallback, times(1)).onBackInvoked(); - verify(mIOnBackInvokedCallback, never()).onBackStarted(); - verify(mIOnBackInvokedCallback, never()).onBackProgressed(backEventCaptor.capture()); - verify(mIOnBackInvokedCallback, never()).onBackInvoked(); + verify(mAnimatorCallback, never()).onBackStarted(any()); + verify(mAnimatorCallback, never()).onBackProgressed(backEventCaptor.capture()); + verify(mAnimatorCallback, never()).onBackInvoked(); + verify(mBackAnimationRunner, never()).onAnimationStart( + anyInt(), any(), any(), any(), any()); } @Test public void ignoresGesture_transitionInProgress() throws RemoteException { - mController.setBackToLauncherCallback(mIOnBackInvokedCallback); - RemoteAnimationTarget animationTarget = createAnimationTarget(); - createNavigationInfo(animationTarget, null, null, - BackNavigationInfo.TYPE_RETURN_TO_HOME, null); + registerAnimation(BackNavigationInfo.TYPE_RETURN_TO_HOME); + createNavigationInfo(BackNavigationInfo.TYPE_RETURN_TO_HOME, true); triggerBackGesture(); + simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME); // Check that back invocation is dispatched. - verify(mIOnBackInvokedCallback).onBackInvoked(); + verify(mAnimatorCallback).onBackInvoked(); + verify(mBackAnimationRunner).onAnimationStart(anyInt(), any(), any(), any(), any()); + + reset(mAnimatorCallback); + reset(mBackAnimationRunner); - reset(mIOnBackInvokedCallback); // Verify that we prevent animation from restarting if another gestures happens before // the previous transition is finished. doMotionEvent(MotionEvent.ACTION_DOWN, 0); - verifyNoMoreInteractions(mIOnBackInvokedCallback); + verifyNoMoreInteractions(mAnimatorCallback); + + // Finish back navigation. + simulateRemoteAnimationFinished(); + + // Verify that more events from a rejected swipe cannot start animation. + doMotionEvent(MotionEvent.ACTION_MOVE, 100); + doMotionEvent(MotionEvent.ACTION_UP, 0); + verifyNoMoreInteractions(mAnimatorCallback); // 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(); + + simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME); + verify(mAnimatorCallback).onBackStarted(any()); + verify(mBackAnimationRunner).onAnimationStart(anyInt(), any(), any(), any(), any()); } @Test public void acceptsGesture_transitionTimeout() throws RemoteException { - mController.setBackToLauncherCallback(mIOnBackInvokedCallback); - RemoteAnimationTarget animationTarget = createAnimationTarget(); - createNavigationInfo(animationTarget, null, null, - BackNavigationInfo.TYPE_RETURN_TO_HOME, null); + registerAnimation(BackNavigationInfo.TYPE_RETURN_TO_HOME); + createNavigationInfo(BackNavigationInfo.TYPE_RETURN_TO_HOME, true); + + // In case it is still running in animation. + doNothing().when(mAnimatorCallback).onBackInvoked(); triggerBackGesture(); - reset(mIOnBackInvokedCallback); + simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME); // Simulate transition timeout. mShellExecutor.flushAll(); + reset(mAnimatorCallback); + doMotionEvent(MotionEvent.ACTION_DOWN, 0); doMotionEvent(MotionEvent.ACTION_MOVE, 100); - verify(mIOnBackInvokedCallback).onBackStarted(); + simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME); + verify(mAnimatorCallback).onBackStarted(any()); + } + + @Test + public void cancelBackInvokeWhenLostFocus() throws RemoteException { + registerAnimation(BackNavigationInfo.TYPE_RETURN_TO_HOME); + + createNavigationInfo(BackNavigationInfo.TYPE_RETURN_TO_HOME, true); + + doMotionEvent(MotionEvent.ACTION_DOWN, 0); + // Check that back start and progress is dispatched when first move. + doMotionEvent(MotionEvent.ACTION_MOVE, 100); + + simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME); + verify(mAnimatorCallback).onBackStarted(any()); + verify(mBackAnimationRunner).onAnimationStart(anyInt(), any(), any(), any(), any()); + + // Check that back invocation is dispatched. + mController.setTriggerBack(true); // Fake trigger back + + // In case the focus has been changed. + mController.mNavigationObserver.sendResult(null); + mShellExecutor.flushAll(); + verify(mAnimatorCallback).onBackCancelled(); + + // No more back invoke. + doMotionEvent(MotionEvent.ACTION_UP, 0); + verify(mAnimatorCallback, never()).onBackInvoked(); + } + + @Test + public void animationNotDefined() throws RemoteException { + final int[] testTypes = new int[] { + BackNavigationInfo.TYPE_RETURN_TO_HOME, + BackNavigationInfo.TYPE_CROSS_TASK, + BackNavigationInfo.TYPE_CROSS_ACTIVITY, + BackNavigationInfo.TYPE_DIALOG_CLOSE}; + + for (int type: testTypes) { + unregisterAnimation(type); + } + + for (int type: testTypes) { + final ResultListener result = new ResultListener(); + createNavigationInfo(new BackNavigationInfo.Builder() + .setType(type) + .setOnBackInvokedCallback(mAppCallback) + .setPrepareRemoteAnimation(true) + .setOnBackNavigationDone(new RemoteCallback(result))); + triggerBackGesture(); + simulateRemoteAnimationStart(type); + mShellExecutor.flushAll(); + + assertTrue("Navigation Done callback not called for " + + BackNavigationInfo.typeToString(type), result.mBackNavigationDone); + assertTrue("TriggerBack should have been true", result.mTriggerBack); + } + + verify(mAppCallback, never()).onBackStarted(any()); + verify(mAppCallback, never()).onBackProgressed(any()); + verify(mAppCallback, times(testTypes.length)).onBackInvoked(); + + verify(mAnimatorCallback, never()).onBackStarted(any()); + verify(mAnimatorCallback, never()).onBackProgressed(any()); + verify(mAnimatorCallback, never()).onBackInvoked(); + } + + @Test + public void callbackShouldDeliverProgress() throws RemoteException { + registerAnimation(BackNavigationInfo.TYPE_RETURN_TO_HOME); + + final int type = BackNavigationInfo.TYPE_CALLBACK; + final ResultListener result = new ResultListener(); + createNavigationInfo(new BackNavigationInfo.Builder() + .setType(type) + .setOnBackInvokedCallback(mAppCallback) + .setOnBackNavigationDone(new RemoteCallback(result))); + triggerBackGesture(); + mShellExecutor.flushAll(); + + assertTrue("Navigation Done callback not called for " + + BackNavigationInfo.typeToString(type), result.mBackNavigationDone); + assertTrue("TriggerBack should have been true", result.mTriggerBack); + + verify(mAppCallback, times(1)).onBackStarted(any()); + verify(mAppCallback, times(1)).onBackProgressed(any()); + verify(mAppCallback, times(1)).onBackInvoked(); + + verify(mAnimatorCallback, never()).onBackStarted(any()); + verify(mAnimatorCallback, never()).onBackProgressed(any()); + verify(mAnimatorCallback, never()).onBackInvoked(); + } + + @Test + public void testBackToActivity() throws RemoteException { + final CrossActivityAnimation animation = new CrossActivityAnimation(mContext, + mAnimationBackground); + verifySystemBackBehavior( + BackNavigationInfo.TYPE_CROSS_ACTIVITY, animation.mBackAnimationRunner); + } + + @Test + public void testBackToTask() throws RemoteException { + final CrossTaskBackAnimation animation = new CrossTaskBackAnimation(mContext, + mAnimationBackground); + verifySystemBackBehavior( + BackNavigationInfo.TYPE_CROSS_TASK, animation.mBackAnimationRunner); + } + + private void verifySystemBackBehavior(int type, BackAnimationRunner animation) + throws RemoteException { + final BackAnimationRunner animationRunner = spy(animation); + final IRemoteAnimationRunner runner = spy(animationRunner.getRunner()); + final IOnBackInvokedCallback callback = spy(animationRunner.getCallback()); + + // Set up the monitoring objects. + doNothing().when(runner).onAnimationStart(anyInt(), any(), any(), any(), any()); + doReturn(runner).when(animationRunner).getRunner(); + doReturn(callback).when(animationRunner).getCallback(); + + mController.registerAnimation(type, animationRunner); + + createNavigationInfo(type, true); + + doMotionEvent(MotionEvent.ACTION_DOWN, 0); + + // Check that back start and progress is dispatched when first move. + doMotionEvent(MotionEvent.ACTION_MOVE, 100); + + simulateRemoteAnimationStart(type); + + verify(callback).onBackStarted(any(BackMotionEvent.class)); + verify(animationRunner).startAnimation(any(), any(), any(), any()); + + // Check that back invocation is dispatched. + mController.setTriggerBack(true); // Fake trigger back + doMotionEvent(MotionEvent.ACTION_UP, 0); + verify(callback).onBackInvoked(); } private void doMotionEvent(int actionDown, int coordinate) { @@ -301,6 +461,39 @@ public class BackAnimationControllerTest { coordinate, coordinate, actionDown, BackEvent.EDGE_LEFT); - mEventTime += 10; } + + private void simulateRemoteAnimationStart(int type) throws RemoteException { + RemoteAnimationTarget animationTarget = createAnimationTarget(); + RemoteAnimationTarget[] targets = new RemoteAnimationTarget[]{animationTarget}; + if (mController.mBackAnimationAdapter != null) { + mController.mBackAnimationAdapter.getRunner().onAnimationStart( + targets, null, null, mBackAnimationFinishedCallback); + mShellExecutor.flushAll(); + } + } + + private void simulateRemoteAnimationFinished() { + mController.onBackAnimationFinished(); + mController.finishBackNavigation(); + } + + private void registerAnimation(int type) { + mController.registerAnimation(type, + new BackAnimationRunner(mAnimatorCallback, mBackAnimationRunner)); + } + + private void unregisterAnimation(int type) { + mController.unregisterAnimation(type); + } + + private static class ResultListener implements RemoteCallback.OnResultListener { + boolean mBackNavigationDone = false; + boolean mTriggerBack = false; + @Override + public void onResult(@Nullable Bundle result) { + mBackNavigationDone = true; + mTriggerBack = result.getBoolean(KEY_TRIGGER_BACK); + } + }; } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackProgressAnimatorTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackProgressAnimatorTest.java new file mode 100644 index 000000000000..3608474bd90e --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackProgressAnimatorTest.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.back; + +import static android.window.BackEvent.EDGE_LEFT; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import android.os.Handler; +import android.os.Looper; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; +import android.window.BackEvent; +import android.window.BackMotionEvent; +import android.window.BackProgressAnimator; + +import androidx.test.filters.SmallTest; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +@SmallTest +@TestableLooper.RunWithLooper +@RunWith(AndroidTestingRunner.class) +public class BackProgressAnimatorTest { + private BackProgressAnimator mProgressAnimator; + private BackEvent mReceivedBackEvent; + private float mTargetProgress = 0.5f; + private CountDownLatch mTargetProgressCalled = new CountDownLatch(1); + private Handler mMainThreadHandler; + + @Before + public void setUp() throws Exception { + mMainThreadHandler = new Handler(Looper.getMainLooper()); + final BackMotionEvent backEvent = new BackMotionEvent( + 0, 0, + 0, EDGE_LEFT, null); + mMainThreadHandler.post( + () -> { + mProgressAnimator = new BackProgressAnimator(); + mProgressAnimator.onBackStarted(backEvent, this::onGestureProgress); + }); + } + + @Test + public void testBackProgressed() throws InterruptedException { + final BackMotionEvent backEvent = new BackMotionEvent( + 100, 0, + mTargetProgress, EDGE_LEFT, null); + mMainThreadHandler.post( + () -> mProgressAnimator.onBackProgressed(backEvent)); + + mTargetProgressCalled.await(1, TimeUnit.SECONDS); + + assertNotNull(mReceivedBackEvent); + assertEquals(mReceivedBackEvent.getProgress(), mTargetProgress, 0 /* delta */); + } + + @Test + public void testBackCancelled() throws InterruptedException { + // Give the animator some progress. + final BackMotionEvent backEvent = new BackMotionEvent( + 100, 0, + mTargetProgress, EDGE_LEFT, null); + mMainThreadHandler.post( + () -> mProgressAnimator.onBackProgressed(backEvent)); + mTargetProgressCalled.await(1, TimeUnit.SECONDS); + assertNotNull(mReceivedBackEvent); + + // Trigger animation cancel, the target progress should be 0. + mTargetProgress = 0; + mTargetProgressCalled = new CountDownLatch(1); + CountDownLatch cancelCallbackCalled = new CountDownLatch(1); + mMainThreadHandler.post( + () -> mProgressAnimator.onBackCancelled(() -> cancelCallbackCalled.countDown())); + cancelCallbackCalled.await(1, TimeUnit.SECONDS); + mTargetProgressCalled.await(1, TimeUnit.SECONDS); + assertNotNull(mReceivedBackEvent); + assertEquals(mReceivedBackEvent.getProgress(), mTargetProgress, 0 /* delta */); + } + + private void onGestureProgress(BackEvent backEvent) { + if (mTargetProgress == backEvent.getProgress()) { + mReceivedBackEvent = backEvent; + mTargetProgressCalled.countDown(); + } + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/CustomizeActivityAnimationTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/CustomizeActivityAnimationTest.java new file mode 100644 index 000000000000..e7d459893ce8 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/CustomizeActivityAnimationTest.java @@ -0,0 +1,232 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.back; + +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.times; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; +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.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +import android.app.WindowConfiguration; +import android.graphics.Color; +import android.graphics.Point; +import android.graphics.Rect; +import android.os.RemoteException; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; +import android.view.Choreographer; +import android.view.RemoteAnimationTarget; +import android.view.SurfaceControl; +import android.view.animation.Animation; +import android.window.BackNavigationInfo; + +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 org.mockito.Mock; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +@SmallTest +@TestableLooper.RunWithLooper +@RunWith(AndroidTestingRunner.class) +public class CustomizeActivityAnimationTest extends ShellTestCase { + private static final int BOUND_SIZE = 100; + @Mock + private BackAnimationBackground mBackAnimationBackground; + @Mock + private Animation mMockCloseAnimation; + @Mock + private Animation mMockOpenAnimation; + + private CustomizeActivityAnimation mCustomizeActivityAnimation; + + @Before + public void setUp() throws Exception { + mCustomizeActivityAnimation = new CustomizeActivityAnimation(mContext, + mBackAnimationBackground, mock(SurfaceControl.Transaction.class), + mock(Choreographer.class)); + spyOn(mCustomizeActivityAnimation); + spyOn(mCustomizeActivityAnimation.mCustomAnimationLoader.mTransitionAnimation); + } + + RemoteAnimationTarget createAnimationTarget(boolean open) { + SurfaceControl topWindowLeash = new SurfaceControl(); + return new RemoteAnimationTarget(1, + open ? RemoteAnimationTarget.MODE_OPENING : RemoteAnimationTarget.MODE_CLOSING, + topWindowLeash, false, new Rect(), new Rect(), -1, + new Point(0, 0), new Rect(0, 0, BOUND_SIZE, BOUND_SIZE), new Rect(), + new WindowConfiguration(), true, null, null, null, false, -1); + } + + @Test + public void receiveFinishAfterInvoke() throws InterruptedException { + spyOn(mCustomizeActivityAnimation.mCustomAnimationLoader); + doReturn(mMockCloseAnimation).when(mCustomizeActivityAnimation.mCustomAnimationLoader) + .loadAnimation(any(), eq(false)); + doReturn(mMockOpenAnimation).when(mCustomizeActivityAnimation.mCustomAnimationLoader) + .loadAnimation(any(), eq(true)); + + mCustomizeActivityAnimation.prepareNextAnimation( + new BackNavigationInfo.CustomAnimationInfo("TestPackage")); + final RemoteAnimationTarget close = createAnimationTarget(false); + final RemoteAnimationTarget open = createAnimationTarget(true); + // start animation with remote animation targets + final CountDownLatch finishCalled = new CountDownLatch(1); + final Runnable finishCallback = finishCalled::countDown; + mCustomizeActivityAnimation.mBackAnimationRunner.startAnimation( + new RemoteAnimationTarget[]{close, open}, null, null, finishCallback); + verify(mMockCloseAnimation).initialize(eq(BOUND_SIZE), eq(BOUND_SIZE), + eq(BOUND_SIZE), eq(BOUND_SIZE)); + verify(mMockOpenAnimation).initialize(eq(BOUND_SIZE), eq(BOUND_SIZE), + eq(BOUND_SIZE), eq(BOUND_SIZE)); + + try { + mCustomizeActivityAnimation.mBackAnimationRunner.getCallback().onBackInvoked(); + } catch (RemoteException r) { + fail("onBackInvoked throw remote exception"); + } + verify(mCustomizeActivityAnimation).onGestureCommitted(); + finishCalled.await(1, TimeUnit.SECONDS); + } + + @Test + public void receiveFinishAfterCancel() throws InterruptedException { + spyOn(mCustomizeActivityAnimation.mCustomAnimationLoader); + doReturn(mMockCloseAnimation).when(mCustomizeActivityAnimation.mCustomAnimationLoader) + .loadAnimation(any(), eq(false)); + doReturn(mMockOpenAnimation).when(mCustomizeActivityAnimation.mCustomAnimationLoader) + .loadAnimation(any(), eq(true)); + + mCustomizeActivityAnimation.prepareNextAnimation( + new BackNavigationInfo.CustomAnimationInfo("TestPackage")); + final RemoteAnimationTarget close = createAnimationTarget(false); + final RemoteAnimationTarget open = createAnimationTarget(true); + // start animation with remote animation targets + final CountDownLatch finishCalled = new CountDownLatch(1); + final Runnable finishCallback = finishCalled::countDown; + mCustomizeActivityAnimation.mBackAnimationRunner.startAnimation( + new RemoteAnimationTarget[]{close, open}, null, null, finishCallback); + verify(mMockCloseAnimation).initialize(eq(BOUND_SIZE), eq(BOUND_SIZE), + eq(BOUND_SIZE), eq(BOUND_SIZE)); + verify(mMockOpenAnimation).initialize(eq(BOUND_SIZE), eq(BOUND_SIZE), + eq(BOUND_SIZE), eq(BOUND_SIZE)); + + try { + mCustomizeActivityAnimation.mBackAnimationRunner.getCallback().onBackCancelled(); + } catch (RemoteException r) { + fail("onBackCancelled throw remote exception"); + } + finishCalled.await(1, TimeUnit.SECONDS); + } + + @Test + public void receiveFinishWithoutAnimationAfterInvoke() throws InterruptedException { + mCustomizeActivityAnimation.prepareNextAnimation( + new BackNavigationInfo.CustomAnimationInfo("TestPackage")); + // start animation without any remote animation targets + final CountDownLatch finishCalled = new CountDownLatch(1); + final Runnable finishCallback = finishCalled::countDown; + mCustomizeActivityAnimation.mBackAnimationRunner.startAnimation( + new RemoteAnimationTarget[]{}, null, null, finishCallback); + + try { + mCustomizeActivityAnimation.mBackAnimationRunner.getCallback().onBackInvoked(); + } catch (RemoteException r) { + fail("onBackInvoked throw remote exception"); + } + verify(mCustomizeActivityAnimation).onGestureCommitted(); + finishCalled.await(1, TimeUnit.SECONDS); + } + + @Test + public void testLoadCustomAnimation() { + testLoadCustomAnimation(10, 20, 0); + } + + @Test + public void testLoadCustomAnimationNoEnter() { + testLoadCustomAnimation(0, 10, 0); + } + + @Test + public void testLoadWindowAnimations() { + testLoadCustomAnimation(0, 0, 30); + } + + @Test + public void testCustomAnimationHigherThanWindowAnimations() { + testLoadCustomAnimation(10, 20, 30); + } + + private void testLoadCustomAnimation(int enterResId, int exitResId, int windowAnimations) { + final String testPackage = "TestPackage"; + BackNavigationInfo.Builder builder = new BackNavigationInfo.Builder() + .setCustomAnimation(testPackage, enterResId, exitResId, Color.GREEN) + .setWindowAnimations(testPackage, windowAnimations); + final BackNavigationInfo.CustomAnimationInfo info = builder.build() + .getCustomAnimationInfo(); + + doReturn(mMockOpenAnimation).when(mCustomizeActivityAnimation.mCustomAnimationLoader + .mTransitionAnimation) + .loadAppTransitionAnimation(eq(testPackage), eq(enterResId)); + doReturn(mMockCloseAnimation).when(mCustomizeActivityAnimation.mCustomAnimationLoader + .mTransitionAnimation) + .loadAppTransitionAnimation(eq(testPackage), eq(exitResId)); + doReturn(mMockCloseAnimation).when(mCustomizeActivityAnimation.mCustomAnimationLoader + .mTransitionAnimation) + .loadAnimationAttr(eq(testPackage), eq(windowAnimations), anyInt(), anyBoolean()); + doReturn(mMockOpenAnimation).when(mCustomizeActivityAnimation.mCustomAnimationLoader + .mTransitionAnimation).loadDefaultAnimationAttr(anyInt(), anyBoolean()); + + CustomizeActivityAnimation.AnimationLoadResult result = + mCustomizeActivityAnimation.mCustomAnimationLoader.loadAll(info); + + if (exitResId != 0) { + if (enterResId == 0) { + verify(mCustomizeActivityAnimation.mCustomAnimationLoader.mTransitionAnimation, + never()).loadAppTransitionAnimation(eq(testPackage), eq(enterResId)); + verify(mCustomizeActivityAnimation.mCustomAnimationLoader.mTransitionAnimation) + .loadDefaultAnimationAttr(anyInt(), anyBoolean()); + } else { + assertEquals(result.mEnterAnimation, mMockOpenAnimation); + } + assertEquals(result.mBackgroundColor, Color.GREEN); + assertEquals(result.mCloseAnimation, mMockCloseAnimation); + verify(mCustomizeActivityAnimation.mCustomAnimationLoader.mTransitionAnimation, never()) + .loadAnimationAttr(eq(testPackage), anyInt(), anyInt(), anyBoolean()); + } else if (windowAnimations != 0) { + verify(mCustomizeActivityAnimation.mCustomAnimationLoader.mTransitionAnimation, + times(2)).loadAnimationAttr(eq(testPackage), anyInt(), anyInt(), anyBoolean()); + assertEquals(result.mCloseAnimation, mMockCloseAnimation); + } + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/OWNERS b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/OWNERS new file mode 100644 index 000000000000..1e0f9bc6322f --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/OWNERS @@ -0,0 +1,5 @@ +# WM shell sub-module back navigation owners +# Bug component: 1152663 +shanh@google.com +arthurhung@google.com +wilsonshih@google.com diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/TouchTrackerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/TouchTrackerTest.java new file mode 100644 index 000000000000..ba9c159bad28 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/TouchTrackerTest.java @@ -0,0 +1,137 @@ +/* + * 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.back; + +import static org.junit.Assert.assertEquals; + +import android.window.BackEvent; +import android.window.BackMotionEvent; + +import org.junit.Before; +import org.junit.Test; + +public class TouchTrackerTest { + private static final float FAKE_THRESHOLD = 400; + private static final float INITIAL_X_LEFT_EDGE = 5; + private static final float INITIAL_X_RIGHT_EDGE = FAKE_THRESHOLD - INITIAL_X_LEFT_EDGE; + private TouchTracker mTouchTracker; + + @Before + public void setUp() throws Exception { + mTouchTracker = new TouchTracker(); + mTouchTracker.setProgressThreshold(FAKE_THRESHOLD); + } + + @Test + public void generatesProgress_onStart() { + mTouchTracker.setGestureStartLocation(INITIAL_X_LEFT_EDGE, 0, BackEvent.EDGE_LEFT); + BackMotionEvent event = mTouchTracker.createStartEvent(null); + assertEquals(event.getProgress(), 0f, 0f); + } + + @Test + public void generatesProgress_leftEdge() { + mTouchTracker.setGestureStartLocation(INITIAL_X_LEFT_EDGE, 0, BackEvent.EDGE_LEFT); + float touchX = 10; + + // Pre-commit + mTouchTracker.update(touchX, 0); + assertEquals(getProgress(), (touchX - INITIAL_X_LEFT_EDGE) / FAKE_THRESHOLD, 0f); + + // Post-commit + touchX += 100; + mTouchTracker.setTriggerBack(true); + mTouchTracker.update(touchX, 0); + assertEquals(getProgress(), (touchX - INITIAL_X_LEFT_EDGE) / FAKE_THRESHOLD, 0f); + + // Cancel + touchX -= 10; + mTouchTracker.setTriggerBack(false); + mTouchTracker.update(touchX, 0); + assertEquals(getProgress(), 0, 0f); + + // Cancel more + touchX -= 10; + mTouchTracker.update(touchX, 0); + assertEquals(getProgress(), 0, 0f); + + // Restart + touchX += 10; + mTouchTracker.update(touchX, 0); + assertEquals(getProgress(), 0, 0f); + + // Restarted, but pre-commit + float restartX = touchX; + touchX += 10; + mTouchTracker.update(touchX, 0); + assertEquals(getProgress(), (touchX - restartX) / FAKE_THRESHOLD, 0f); + + // Restarted, post-commit + touchX += 10; + mTouchTracker.setTriggerBack(true); + mTouchTracker.update(touchX, 0); + assertEquals(getProgress(), (touchX - INITIAL_X_LEFT_EDGE) / FAKE_THRESHOLD, 0f); + } + + @Test + public void generatesProgress_rightEdge() { + mTouchTracker.setGestureStartLocation(INITIAL_X_RIGHT_EDGE, 0, BackEvent.EDGE_RIGHT); + float touchX = INITIAL_X_RIGHT_EDGE - 10; // Fake right edge + + // Pre-commit + mTouchTracker.update(touchX, 0); + assertEquals(getProgress(), (INITIAL_X_RIGHT_EDGE - touchX) / FAKE_THRESHOLD, 0f); + + // Post-commit + touchX -= 100; + mTouchTracker.setTriggerBack(true); + mTouchTracker.update(touchX, 0); + assertEquals(getProgress(), (INITIAL_X_RIGHT_EDGE - touchX) / FAKE_THRESHOLD, 0f); + + // Cancel + touchX += 10; + mTouchTracker.setTriggerBack(false); + mTouchTracker.update(touchX, 0); + assertEquals(getProgress(), 0, 0f); + + // Cancel more + touchX += 10; + mTouchTracker.update(touchX, 0); + assertEquals(getProgress(), 0, 0f); + + // Restart + touchX -= 10; + mTouchTracker.update(touchX, 0); + assertEquals(getProgress(), 0, 0f); + + // Restarted, but pre-commit + float restartX = touchX; + touchX -= 10; + mTouchTracker.update(touchX, 0); + assertEquals(getProgress(), (restartX - touchX) / FAKE_THRESHOLD, 0f); + + // Restarted, post-commit + touchX -= 10; + mTouchTracker.setTriggerBack(true); + mTouchTracker.update(touchX, 0); + assertEquals(getProgress(), (INITIAL_X_RIGHT_EDGE - touchX) / FAKE_THRESHOLD, 0f); + } + + private float getProgress() { + return mTouchTracker.createProgressEvent().getProgress(); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java index e6711aca19c1..919bf0665b5e 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java @@ -16,6 +16,8 @@ package com.android.wm.shell.bubbles; +import static com.android.wm.shell.bubbles.Bubble.KEY_APP_BUBBLE; + import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; @@ -32,6 +34,7 @@ import static org.mockito.Mockito.when; import android.app.Notification; import android.app.PendingIntent; +import android.content.Intent; import android.content.LocusId; import android.graphics.drawable.Icon; import android.os.Bundle; @@ -94,6 +97,7 @@ public class BubbleDataTest extends ShellTestCase { private Bubble mBubbleInterruptive; private Bubble mBubbleDismissed; private Bubble mBubbleLocusId; + private Bubble mAppBubble; private BubbleData mBubbleData; private TestableBubblePositioner mPositioner; @@ -178,6 +182,12 @@ public class BubbleDataTest extends ShellTestCase { mBubbleMetadataFlagListener, mPendingIntentCanceledListener, mMainExecutor); + + Intent appBubbleIntent = new Intent(mContext, BubblesTestActivity.class); + appBubbleIntent.setPackage(mContext.getPackageName()); + mAppBubble = new Bubble(appBubbleIntent, new UserHandle(1), mock(Icon.class), + mMainExecutor); + mPositioner = new TestableBubblePositioner(mContext, mock(WindowManager.class)); mBubbleData = new BubbleData(getContext(), mBubbleLogger, mPositioner, @@ -1089,6 +1099,18 @@ public class BubbleDataTest extends ShellTestCase { assertOverflowChangedTo(ImmutableList.of()); } + @Test + public void test_removeAppBubble_skipsOverflow() { + mBubbleData.notificationEntryUpdated(mAppBubble, true /* suppressFlyout*/, + false /* showInShade */); + assertThat(mBubbleData.getBubbleInStackWithKey(KEY_APP_BUBBLE)).isEqualTo(mAppBubble); + + mBubbleData.dismissBubbleWithKey(KEY_APP_BUBBLE, Bubbles.DISMISS_USER_GESTURE); + + assertThat(mBubbleData.getOverflowBubbleWithKey(KEY_APP_BUBBLE)).isNull(); + assertThat(mBubbleData.getBubbleInStackWithKey(KEY_APP_BUBBLE)).isNull(); + } + private void verifyUpdateReceived() { verify(mListener).applyUpdate(mUpdateCaptor.capture()); reset(mListener); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTest.java index e8f3f69ca64e..de967bfa288b 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTest.java @@ -29,6 +29,8 @@ import static org.mockito.Mockito.when; import android.app.Notification; import android.app.PendingIntent; import android.content.Intent; +import android.content.pm.ShortcutInfo; +import android.content.res.Resources; import android.graphics.drawable.Icon; import android.os.Bundle; import android.service.notification.StatusBarNotification; @@ -162,4 +164,27 @@ public class BubbleTest extends ShellTestCase { verify(mBubbleMetadataFlagListener, never()).onBubbleMetadataFlagChanged(any()); } + + @Test + public void testBubbleIsConversation_hasConversationShortcut() { + Bubble bubble = createBubbleWithShortcut(); + assertThat(bubble.getShortcutInfo()).isNotNull(); + assertThat(bubble.isConversation()).isTrue(); + } + + @Test + public void testBubbleIsConversation_hasNoShortcut() { + Bubble bubble = new Bubble(mBubbleEntry, mBubbleMetadataFlagListener, null, mMainExecutor); + assertThat(bubble.getShortcutInfo()).isNull(); + assertThat(bubble.isConversation()).isFalse(); + } + + private Bubble createBubbleWithShortcut() { + ShortcutInfo shortcutInfo = new ShortcutInfo.Builder(mContext) + .setId("mockShortcutId") + .build(); + return new Bubble("mockKey", shortcutInfo, 10, Resources.ID_NULL, + "mockTitle", 0 /* taskId */, "mockLocus", true /* isDismissible */, + mMainExecutor, mBubbleMetadataFlagListener); + } } 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..0a31338a7c81 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 @@ -21,10 +21,10 @@ import android.testing.AndroidTestingRunner import android.util.SparseArray import androidx.test.filters.SmallTest import com.android.wm.shell.ShellTestCase -import com.android.wm.shell.bubbles.storage.BubbleXmlHelperTest.Companion.sparseArraysEqual 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 @@ -35,7 +35,8 @@ class BubblePersistentRepositoryTest : ShellTestCase() { // user, package, shortcut, notification key, height, res-height, title, taskId, locusId private val user0Bubbles = listOf( - BubbleEntity(0, "com.example.messenger", "shortcut-1", "0k1", 120, 0, null, 1, null), + BubbleEntity(0, "com.example.messenger", "shortcut-1", "0k1", 120, 0, null, 1, null, + true), BubbleEntity(10, "com.example.chat", "alice and bob", "0k2", 0, 16537428, "title", 2, null), BubbleEntity(0, "com.example.messenger", "shortcut-2", "0k3", 120, 0, null, @@ -43,7 +44,8 @@ class BubblePersistentRepositoryTest : ShellTestCase() { ) private val user1Bubbles = listOf( - BubbleEntity(1, "com.example.messenger", "shortcut-1", "1k1", 120, 0, null, 3, null), + BubbleEntity(1, "com.example.messenger", "shortcut-1", "1k1", 120, 0, null, 3, null, + true), BubbleEntity(12, "com.example.chat", "alice and bob", "1k2", 0, 16537428, "title", 4, null), BubbleEntity(1, "com.example.messenger", "shortcut-2", "1k3", 120, 0, null, @@ -61,6 +63,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 @@ -69,6 +77,6 @@ class BubblePersistentRepositoryTest : ShellTestCase() { assertEquals(actual.size(), 0) repository.persistsToDisk(bubbles) - assertTrue(sparseArraysEqual(bubbles, repository.readFromDisk())) + assertTrue(bubbles.contentEquals(repository.readFromDisk())) } -}
\ No newline at end of file +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/storage/BubbleXmlHelperTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/storage/BubbleXmlHelperTest.kt index 4ab9f87dbbf6..3bfbcd26a577 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/storage/BubbleXmlHelperTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/storage/BubbleXmlHelperTest.kt @@ -34,7 +34,8 @@ import java.io.ByteArrayOutputStream class BubbleXmlHelperTest : ShellTestCase() { private val user0Bubbles = listOf( - BubbleEntity(0, "com.example.messenger", "shortcut-1", "0k1", 120, 0, null, 1), + BubbleEntity(0, "com.example.messenger", "shortcut-1", "0k1", 120, 0, null, 1, + isDismissable = true), BubbleEntity(10, "com.example.chat", "alice and bob", "0k2", 0, 16537428, "title", 2, null), BubbleEntity(0, "com.example.messenger", "shortcut-2", "0k3", 120, 0, null, @@ -42,7 +43,8 @@ class BubbleXmlHelperTest : ShellTestCase() { ) private val user1Bubbles = listOf( - BubbleEntity(1, "com.example.messenger", "shortcut-1", "1k1", 120, 0, null, 3), + BubbleEntity(1, "com.example.messenger", "shortcut-1", "1k1", 120, 0, null, 3, + isDismissable = true), BubbleEntity(12, "com.example.chat", "alice and bob", "1k2", 0, 16537428, "title", 4, null), BubbleEntity(1, "com.example.messenger", "shortcut-2", "1k3", 120, 0, null, @@ -51,28 +53,6 @@ class BubbleXmlHelperTest : ShellTestCase() { private val bubbles = SparseArray<List<BubbleEntity>>() - // Checks that the contents of the two sparse arrays are the same. - companion object { - fun sparseArraysEqual( - one: SparseArray<List<BubbleEntity>>?, - two: SparseArray<List<BubbleEntity>>? - ): Boolean { - if (one == null && two == null) return true - if ((one == null) != (two == null)) return false - if (one!!.size() != two!!.size()) return false - for (i in 0 until one.size()) { - val k1 = one.keyAt(i) - val v1 = one.valueAt(i) - val k2 = two.keyAt(i) - val v2 = two.valueAt(i) - if (k1 != k2 && v1 != v2) { - return false - } - } - return true - } - } - @Before fun setup() { bubbles.put(0, user0Bubbles) @@ -83,14 +63,14 @@ class BubbleXmlHelperTest : ShellTestCase() { fun testWriteXml() { val expectedEntries = """ <bs uid="0"> -<bb uid="0" pkg="com.example.messenger" sid="shortcut-1" key="0k1" h="120" hid="0" tid="1" /> -<bb uid="10" pkg="com.example.chat" sid="alice and bob" key="0k2" h="0" hid="16537428" t="title" tid="2" /> -<bb uid="0" pkg="com.example.messenger" sid="shortcut-2" key="0k3" h="120" hid="0" tid="-1" l="l3" /> +<bb uid="0" pkg="com.example.messenger" sid="shortcut-1" key="0k1" h="120" hid="0" tid="1" d="true" /> +<bb uid="10" pkg="com.example.chat" sid="alice and bob" key="0k2" h="0" hid="16537428" t="title" tid="2" d="false" /> +<bb uid="0" pkg="com.example.messenger" sid="shortcut-2" key="0k3" h="120" hid="0" tid="-1" l="l3" d="false" /> </bs> <bs uid="1"> -<bb uid="1" pkg="com.example.messenger" sid="shortcut-1" key="1k1" h="120" hid="0" tid="3" /> -<bb uid="12" pkg="com.example.chat" sid="alice and bob" key="1k2" h="0" hid="16537428" t="title" tid="4" /> -<bb uid="1" pkg="com.example.messenger" sid="shortcut-2" key="1k3" h="120" hid="0" tid="-1" l="l4" /> +<bb uid="1" pkg="com.example.messenger" sid="shortcut-1" key="1k1" h="120" hid="0" tid="3" d="true" /> +<bb uid="12" pkg="com.example.chat" sid="alice and bob" key="1k2" h="0" hid="16537428" t="title" tid="4" d="false" /> +<bb uid="1" pkg="com.example.messenger" sid="shortcut-2" key="1k3" h="120" hid="0" tid="-1" l="l4" d="false" /> </bs> """.trimIndent() ByteArrayOutputStream().use { @@ -107,19 +87,19 @@ class BubbleXmlHelperTest : ShellTestCase() { <?xml version='1.0' encoding='utf-8' standalone='yes' ?> <bs v="2"> <bs uid="0"> -<bb uid="0" pkg="com.example.messenger" sid="shortcut-1" key="0k1" h="120" hid="0" tid="1" /> -<bb uid="10" pkg="com.example.chat" sid="alice and bob" key="0k2" h="0" hid="16537428" t="title" tid="2" /> -<bb uid="0" pkg="com.example.messenger" sid="shortcut-2" key="0k3" h="120" hid="0" tid="-1" l="l3" /> +<bb uid="0" pkg="com.example.messenger" sid="shortcut-1" key="0k1" h="120" hid="0" tid="1" d="true" /> +<bb uid="10" pkg="com.example.chat" sid="alice and bob" key="0k2" h="0" hid="16537428" t="title" tid="2" d="false" /> +<bb uid="0" pkg="com.example.messenger" sid="shortcut-2" key="0k3" h="120" hid="0" tid="-1" l="l3" d="false" /> </bs> <bs uid="1"> -<bb uid="1" pkg="com.example.messenger" sid="shortcut-1" key="1k1" h="120" hid="0" tid="3" /> -<bb uid="12" pkg="com.example.chat" sid="alice and bob" key="1k2" h="0" hid="16537428" t="title" tid="4" /> -<bb uid="1" pkg="com.example.messenger" sid="shortcut-2" key="1k3" h="120" hid="0" tid="-1" l="l4" /> +<bb uid="1" pkg="com.example.messenger" sid="shortcut-1" key="1k1" h="120" hid="0" tid="3" d="true" /> +<bb uid="12" pkg="com.example.chat" sid="alice and bob" key="1k2" h="0" hid="16537428" t="title" tid="4" d="false" /> +<bb uid="1" pkg="com.example.messenger" sid="shortcut-2" key="1k3" h="120" hid="0" tid="-1" l="l4" d="false" /> </bs> </bs> """.trimIndent() val actual = readXml(ByteArrayInputStream(src.toByteArray(Charsets.UTF_8))) - assertTrue("failed parsing bubbles from xml\n$src", sparseArraysEqual(bubbles, actual)) + assertTrue("failed parsing bubbles from xml\n$src", bubbles.contentEquals(actual)) } // V0 -> V1 happened prior to release / during dogfood so nothing is saved @@ -161,8 +141,7 @@ class BubbleXmlHelperTest : ShellTestCase() { </bs> """.trimIndent() val actual = readXml(ByteArrayInputStream(src.toByteArray(Charsets.UTF_8))) - assertTrue("failed parsing bubbles from xml\n$src", - sparseArraysEqual(expectedBubbles, actual)) + assertTrue("failed parsing bubbles from xml\n$src", expectedBubbles.contentEquals(actual)) } /** @@ -187,7 +166,7 @@ class BubbleXmlHelperTest : ShellTestCase() { """.trimIndent() val actual = readXml(ByteArrayInputStream(src.toByteArray(Charsets.UTF_8))) assertTrue("failed parsing bubbles from xml\n$src", - sparseArraysEqual(expectedBubbles, actual)) + expectedBubbles.contentEquals(actual)) } @Test @@ -210,6 +189,6 @@ class BubbleXmlHelperTest : ShellTestCase() { ) val actual = readXml(ByteArrayInputStream(src.toByteArray(Charsets.UTF_8))) assertTrue("failed parsing bubbles from xml\n$src", - sparseArraysEqual(expectedBubbles, actual)) + expectedBubbles.contentEquals(actual)) } }
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DevicePostureControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DevicePostureControllerTest.java new file mode 100644 index 000000000000..f8ee300e411c --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DevicePostureControllerTest.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.common; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; + +import android.content.Context; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import com.android.wm.shell.sysui.ShellInit; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class DevicePostureControllerTest { + @Mock + private Context mContext; + + @Mock + private ShellInit mShellInit; + + @Mock + private ShellExecutor mMainExecutor; + + @Captor + private ArgumentCaptor<Integer> mDevicePostureCaptor; + + @Mock + private DevicePostureController.OnDevicePostureChangedListener mOnDevicePostureChangedListener; + + private DevicePostureController mDevicePostureController; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mDevicePostureController = new DevicePostureController(mContext, mShellInit, mMainExecutor); + } + + @Test + public void instantiateController_addInitCallback() { + verify(mShellInit, times(1)).addInitCallback(any(), eq(mDevicePostureController)); + } + + @Test + public void registerOnDevicePostureChangedListener_callbackCurrentPosture() { + mDevicePostureController.registerOnDevicePostureChangedListener( + mOnDevicePostureChangedListener); + verify(mOnDevicePostureChangedListener, times(1)) + .onDevicePostureChanged(anyInt()); + } + + @Test + public void onDevicePostureChanged_differentPosture_callbackListener() { + mDevicePostureController.registerOnDevicePostureChangedListener( + mOnDevicePostureChangedListener); + verify(mOnDevicePostureChangedListener).onDevicePostureChanged( + mDevicePostureCaptor.capture()); + clearInvocations(mOnDevicePostureChangedListener); + + int differentDevicePosture = mDevicePostureCaptor.getValue() + 1; + mDevicePostureController.onDevicePostureChanged(differentDevicePosture); + + verify(mOnDevicePostureChangedListener, times(1)) + .onDevicePostureChanged(differentDevicePosture); + } + + @Test + public void onDevicePostureChanged_samePosture_doesNotCallbackListener() { + mDevicePostureController.registerOnDevicePostureChangedListener( + mOnDevicePostureChangedListener); + verify(mOnDevicePostureChangedListener).onDevicePostureChanged( + mDevicePostureCaptor.capture()); + clearInvocations(mOnDevicePostureChangedListener); + + int sameDevicePosture = mDevicePostureCaptor.getValue(); + mDevicePostureController.onDevicePostureChanged(sameDevicePosture); + + verifyZeroInteractions(mOnDevicePostureChangedListener); + } +} 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..01e2f988fbfc 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 @@ -17,15 +17,18 @@ package com.android.wm.shell.common; import static android.view.Display.DEFAULT_DISPLAY; -import static android.view.InsetsState.ITYPE_IME; +import static android.view.InsetsSource.ID_IME; import static android.view.Surface.ROTATION_0; import static android.view.WindowInsets.Type.ime; import static org.junit.Assert.assertFalse; +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.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; @@ -38,27 +41,31 @@ 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; + @Mock + private ShellInit mShellInit; private DisplayImeController.PerDisplay mPerDisplay; - private IInputMethodManager mMock; 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,17 +74,18 @@ public class DisplayImeControllerTest { @Override public void release(SurfaceControl.Transaction t) { } - }) { - @Override - public IInputMethodManager getImms() { - return mMock; - } + }, mExecutor) { @Override void removeImeSurface() { } }.new PerDisplay(DEFAULT_DISPLAY, ROTATION_0); } @Test + public void instantiateController_addInitCallback() { + verify(mShellInit, times(1)).addInitCallback(any(), any()); + } + + @Test public void insetsControlChanged_schedulesNoWorkOnExecutor() { mPerDisplay.insetsControlChanged(insetsStateWithIme(false), insetsSourceControl()); verifyZeroInteractions(mExecutor); @@ -91,13 +99,13 @@ public class DisplayImeControllerTest { @Test public void showInsets_schedulesNoWorkOnExecutor() { - mPerDisplay.showInsets(ime(), true); + mPerDisplay.showInsets(ime(), true /* fromIme */, null /* statsToken */); verifyZeroInteractions(mExecutor); } @Test public void hideInsets_schedulesNoWorkOnExecutor() { - mPerDisplay.hideInsets(ime(), true); + mPerDisplay.hideInsets(ime(), true /* fromIme */, null /* statsToken */); verifyZeroInteractions(mExecutor); } @@ -118,17 +126,27 @@ public class DisplayImeControllerTest { verify(mT).show(any()); } + @Test + public void insetsControlChanged_updateImeSourceControl() { + mPerDisplay.insetsControlChanged(insetsStateWithIme(false), insetsSourceControl()); + assertNotNull(mPerDisplay.mImeSourceControl); + + mPerDisplay.insetsControlChanged(new InsetsState(), new InsetsSourceControl[]{}); + assertNull(mPerDisplay.mImeSourceControl); + } + private InsetsSourceControl[] insetsSourceControl() { return new InsetsSourceControl[]{ new InsetsSourceControl( - ITYPE_IME, mock(SurfaceControl.class), new Point(0, 0), Insets.NONE) + ID_IME, ime(), mock(SurfaceControl.class), false, new Point(0, 0), + Insets.NONE) }; } private InsetsState insetsStateWithIme(boolean visible) { InsetsState state = new InsetsState(); - state.addSource(new InsetsSource(ITYPE_IME)); - state.setSourceVisible(ITYPE_IME, visible); + state.addSource(new InsetsSource(ID_IME, ime())); + state.setSourceVisible(ID_IME, visible); return state; } 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..956f1cd419c2 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,22 +19,28 @@ 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.annotation.Nullable; +import android.content.ComponentName; import android.os.RemoteException; import android.util.SparseArray; import android.view.IDisplayWindowInsetsController; import android.view.IWindowManager; import android.view.InsetsSourceControl; import android.view.InsetsState; -import android.view.InsetsVisibilities; +import android.view.WindowInsets; +import android.view.inputmethod.ImeTracker; 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 +51,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 +59,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 +75,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); @@ -97,11 +110,13 @@ public class DisplayInsetsControllerTest { mController.addInsetsChangedListener(SECOND_DISPLAY, secondListener); mInsetsControllersByDisplayId.get(DEFAULT_DISPLAY).topFocusedWindowChanged(null, - new InsetsVisibilities()); + WindowInsets.Type.defaultVisible()); mInsetsControllersByDisplayId.get(DEFAULT_DISPLAY).insetsChanged(null); mInsetsControllersByDisplayId.get(DEFAULT_DISPLAY).insetsControlChanged(null, null); - mInsetsControllersByDisplayId.get(DEFAULT_DISPLAY).showInsets(0, false); - mInsetsControllersByDisplayId.get(DEFAULT_DISPLAY).hideInsets(0, false); + mInsetsControllersByDisplayId.get(DEFAULT_DISPLAY).showInsets(0, false, + null /* statsToken */); + mInsetsControllersByDisplayId.get(DEFAULT_DISPLAY).hideInsets(0, false, + null /* statsToken */); mExecutor.flushAll(); assertTrue(defaultListener.topFocusedWindowChangedCount == 1); @@ -117,11 +132,13 @@ public class DisplayInsetsControllerTest { assertTrue(secondListener.hideInsetsCount == 0); mInsetsControllersByDisplayId.get(SECOND_DISPLAY).topFocusedWindowChanged(null, - new InsetsVisibilities()); + WindowInsets.Type.defaultVisible()); mInsetsControllersByDisplayId.get(SECOND_DISPLAY).insetsChanged(null); mInsetsControllersByDisplayId.get(SECOND_DISPLAY).insetsControlChanged(null, null); - mInsetsControllersByDisplayId.get(SECOND_DISPLAY).showInsets(0, false); - mInsetsControllersByDisplayId.get(SECOND_DISPLAY).hideInsets(0, false); + mInsetsControllersByDisplayId.get(SECOND_DISPLAY).showInsets(0, false, + null /* statsToken */); + mInsetsControllersByDisplayId.get(SECOND_DISPLAY).hideInsets(0, false, + null /* statsToken */); mExecutor.flushAll(); assertTrue(defaultListener.topFocusedWindowChangedCount == 1); @@ -164,8 +181,7 @@ public class DisplayInsetsControllerTest { int hideInsetsCount = 0; @Override - public void topFocusedWindowChanged(String packageName, - InsetsVisibilities requestedVisibilities) { + public void topFocusedWindowChanged(ComponentName component, int requestedVisibleTypes) { topFocusedWindowChangedCount++; } @@ -181,12 +197,12 @@ public class DisplayInsetsControllerTest { } @Override - public void showInsets(int types, boolean fromIme) { + public void showInsets(int types, boolean fromIme, @Nullable ImeTracker.Token statsToken) { showInsetsCount++; } @Override - public void hideInsets(int types, boolean fromIme) { + public void hideInsets(int types, boolean fromIme, @Nullable ImeTracker.Token statsToken) { hideInsetsCount++; } } 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/TabletopModeControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/TabletopModeControllerTest.java new file mode 100644 index 000000000000..96d202ce3a85 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/TabletopModeControllerTest.java @@ -0,0 +1,270 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.common; + +import static android.view.Display.DEFAULT_DISPLAY; + +import static com.android.wm.shell.common.DevicePostureController.DEVICE_POSTURE_CLOSED; +import static com.android.wm.shell.common.DevicePostureController.DEVICE_POSTURE_HALF_OPENED; +import static com.android.wm.shell.common.DevicePostureController.DEVICE_POSTURE_OPENED; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; +import android.view.Surface; + +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; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** + * Tests for {@link TabletopModeController}. + */ +@RunWith(AndroidTestingRunner.class) +@TestableLooper.RunWithLooper +@SmallTest +public class TabletopModeControllerTest extends ShellTestCase { + // It's considered tabletop mode if the display rotation angle matches what's in this array. + // It's defined as com.android.internal.R.array.config_deviceTabletopRotations on real devices. + private static final int[] TABLETOP_MODE_ROTATIONS = new int[] { + 90 /* Surface.ROTATION_90 */, + 270 /* Surface.ROTATION_270 */ + }; + + private TestShellExecutor mMainExecutor; + + private Configuration mConfiguration; + + private TabletopModeController mPipTabletopController; + + @Mock + private Context mContext; + + @Mock + private ShellInit mShellInit; + + @Mock + private Resources mResources; + + @Mock + private DevicePostureController mDevicePostureController; + + @Mock + private DisplayController mDisplayController; + + @Mock + private TabletopModeController.OnTabletopModeChangedListener mOnTabletopModeChangedListener; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + when(mResources.getIntArray(com.android.internal.R.array.config_deviceTabletopRotations)) + .thenReturn(TABLETOP_MODE_ROTATIONS); + when(mContext.getResources()).thenReturn(mResources); + mMainExecutor = new TestShellExecutor(); + mConfiguration = new Configuration(); + mPipTabletopController = new TabletopModeController(mContext, mShellInit, + mDevicePostureController, mDisplayController, mMainExecutor); + mPipTabletopController.onInit(); + } + + @Test + public void instantiateController_addInitCallback() { + verify(mShellInit, times(1)).addInitCallback(any(), eq(mPipTabletopController)); + } + + @Test + public void registerOnTabletopModeChangedListener_notInTabletopMode_callbackFalse() { + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_CLOSED); + mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_0); + mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); + + mPipTabletopController.registerOnTabletopModeChangedListener( + mOnTabletopModeChangedListener); + + verify(mOnTabletopModeChangedListener, times(1)) + .onTabletopModeChanged(false); + } + + @Test + public void registerOnTabletopModeChangedListener_inTabletopMode_callbackTrue() { + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_HALF_OPENED); + mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_90); + mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); + + mPipTabletopController.registerOnTabletopModeChangedListener( + mOnTabletopModeChangedListener); + + verify(mOnTabletopModeChangedListener, times(1)) + .onTabletopModeChanged(true); + } + + @Test + public void registerOnTabletopModeChangedListener_notInTabletopModeTwice_callbackOnce() { + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_CLOSED); + mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_90); + mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); + + mPipTabletopController.registerOnTabletopModeChangedListener( + mOnTabletopModeChangedListener); + clearInvocations(mOnTabletopModeChangedListener); + mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_0); + mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); + + verifyZeroInteractions(mOnTabletopModeChangedListener); + } + + // Test cases starting from folded state (DEVICE_POSTURE_CLOSED) + @Test + public void foldedRotation90_halfOpen_scheduleTabletopModeChange() { + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_CLOSED); + mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_90); + mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); + + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_HALF_OPENED); + + assertTrue(mMainExecutor.hasCallback(mPipTabletopController.mOnEnterTabletopModeCallback)); + } + + @Test + public void foldedRotation0_halfOpen_noScheduleTabletopModeChange() { + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_CLOSED); + mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_0); + mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); + + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_HALF_OPENED); + + assertFalse(mMainExecutor.hasCallback(mPipTabletopController.mOnEnterTabletopModeCallback)); + } + + @Test + public void foldedRotation90_halfOpenThenUnfold_cancelTabletopModeChange() { + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_CLOSED); + mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_90); + mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); + + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_HALF_OPENED); + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_OPENED); + + assertFalse(mMainExecutor.hasCallback(mPipTabletopController.mOnEnterTabletopModeCallback)); + } + + @Test + public void foldedRotation90_halfOpenThenFold_cancelTabletopModeChange() { + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_CLOSED); + mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_90); + mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); + + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_HALF_OPENED); + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_CLOSED); + + assertFalse(mMainExecutor.hasCallback(mPipTabletopController.mOnEnterTabletopModeCallback)); + } + + @Test + public void foldedRotation90_halfOpenThenRotate_cancelTabletopModeChange() { + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_CLOSED); + mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_90); + mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); + + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_HALF_OPENED); + mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_0); + mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); + + assertFalse(mMainExecutor.hasCallback(mPipTabletopController.mOnEnterTabletopModeCallback)); + } + + // Test cases starting from unfolded state (DEVICE_POSTURE_OPENED) + @Test + public void unfoldedRotation90_halfOpen_scheduleTabletopModeChange() { + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_OPENED); + mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_90); + mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); + + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_HALF_OPENED); + + assertTrue(mMainExecutor.hasCallback(mPipTabletopController.mOnEnterTabletopModeCallback)); + } + + @Test + public void unfoldedRotation0_halfOpen_noScheduleTabletopModeChange() { + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_OPENED); + mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_0); + mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); + + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_HALF_OPENED); + + assertFalse(mMainExecutor.hasCallback(mPipTabletopController.mOnEnterTabletopModeCallback)); + } + + @Test + public void unfoldedRotation90_halfOpenThenUnfold_cancelTabletopModeChange() { + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_OPENED); + mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_90); + mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); + + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_HALF_OPENED); + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_OPENED); + + assertFalse(mMainExecutor.hasCallback(mPipTabletopController.mOnEnterTabletopModeCallback)); + } + + @Test + public void unfoldedRotation90_halfOpenThenFold_cancelTabletopModeChange() { + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_OPENED); + mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_90); + mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); + + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_HALF_OPENED); + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_CLOSED); + + assertFalse(mMainExecutor.hasCallback(mPipTabletopController.mOnEnterTabletopModeCallback)); + } + + @Test + public void unfoldedRotation90_halfOpenThenRotate_cancelTabletopModeChange() { + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_OPENED); + mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_90); + mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); + + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_HALF_OPENED); + mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_0); + mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); + + assertFalse(mMainExecutor.hasCallback(mPipTabletopController.mOnEnterTabletopModeCallback)); + } +} 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..3d779481d361 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 @@ -16,6 +16,7 @@ package com.android.wm.shell.common.split; +import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; import static android.content.res.Configuration.ORIENTATION_PORTRAIT; import static com.google.common.truth.Truth.assertThat; @@ -91,12 +92,21 @@ public class SplitLayoutTests extends ShellTestCase { // Verify updateConfiguration returns true if the root bounds changed. config.windowConfiguration.setBounds(new Rect(0, 0, 2160, 1080)); assertThat(mSplitLayout.updateConfiguration(config)).isTrue(); + + // Verify updateConfiguration returns true if the orientation changed. + config.orientation = ORIENTATION_LANDSCAPE; + assertThat(mSplitLayout.updateConfiguration(config)).isTrue(); + + // Verify updateConfiguration returns true if the density changed. + config.densityDpi = 123; + assertThat(mSplitLayout.updateConfiguration(config)).isTrue(); } @Test public void testUpdateDivideBounds() { mSplitLayout.updateDivideBounds(anyInt()); - verify(mSplitLayoutHandler).onLayoutSizeChanging(any(SplitLayout.class)); + verify(mSplitLayoutHandler).onLayoutSizeChanging(any(SplitLayout.class), anyInt(), + anyInt()); } @Test @@ -131,9 +141,9 @@ public class SplitLayoutTests extends ShellTestCase { DividerSnapAlgorithm.SnapTarget snapTarget = getSnapTarget(0 /* position */, DividerSnapAlgorithm.SnapTarget.FLAG_DISMISS_START); - mSplitLayout.snapToTarget(0 /* currentPosition */, snapTarget); + mSplitLayout.snapToTarget(mSplitLayout.getDividePosition(), snapTarget); waitDividerFlingFinished(); - verify(mSplitLayoutHandler).onSnappedToDismiss(eq(false)); + verify(mSplitLayoutHandler).onSnappedToDismiss(eq(false), anyInt()); } @Test @@ -143,9 +153,9 @@ public class SplitLayoutTests extends ShellTestCase { DividerSnapAlgorithm.SnapTarget snapTarget = getSnapTarget(0 /* position */, DividerSnapAlgorithm.SnapTarget.FLAG_DISMISS_END); - mSplitLayout.snapToTarget(0 /* currentPosition */, snapTarget); + mSplitLayout.snapToTarget(mSplitLayout.getDividePosition(), snapTarget); waitDividerFlingFinished(); - verify(mSplitLayoutHandler).onSnappedToDismiss(eq(true)); + verify(mSplitLayoutHandler).onSnappedToDismiss(eq(true), anyInt()); } @Test @@ -159,7 +169,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..a6501f05475f 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 @@ -18,7 +18,7 @@ package com.android.wm.shell.compatui; import static android.app.TaskInfo.CAMERA_COMPAT_CONTROL_HIDDEN; import static android.app.TaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED; -import static android.view.InsetsState.ITYPE_EXTRA_NAVIGATION_BAR; +import static android.view.WindowInsets.Type.navigationBars; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; @@ -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; @@ -50,9 +51,11 @@ import com.android.wm.shell.common.DisplayImeController; import com.android.wm.shell.common.DisplayInsetsController; import com.android.wm.shell.common.DisplayInsetsController.OnInsetsChangedListener; import com.android.wm.shell.common.DisplayLayout; +import com.android.wm.shell.common.DockStateReader; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; -import com.android.wm.shell.compatui.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; @@ -88,6 +93,10 @@ public class CompatUIControllerTest extends ShellTestCase { private @Mock Lazy<Transitions> mMockTransitionsLazy; private @Mock CompatUIWindowManager mMockCompatLayout; private @Mock LetterboxEduWindowManager mMockLetterboxEduLayout; + private @Mock RestartDialogWindowManager mMockRestartDialogLayout; + private @Mock DockStateReader mDockStateReader; + private @Mock CompatUIConfiguration mCompatUIConfiguration; + private @Mock CompatUIShellCommandHandler mCompatUIShellCommandHandler; @Captor ArgumentCaptor<OnInsetsChangedListener> mOnInsetsChangedListenerCaptor; @@ -105,9 +114,17 @@ 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) { + + doReturn(DISPLAY_ID).when(mMockRestartDialogLayout).getDisplayId(); + doReturn(TASK_ID).when(mMockRestartDialogLayout).getTaskId(); + doReturn(true).when(mMockRestartDialogLayout).createLayout(anyBoolean()); + doReturn(true).when(mMockRestartDialogLayout).updateCompatInfo(any(), any(), anyBoolean()); + + mShellInit = spy(new ShellInit(mMockExecutor)); + mController = new CompatUIController(mContext, mShellInit, mMockShellController, + mMockDisplayController, mMockDisplayInsetsController, mMockImeController, + mMockSyncQueue, mMockExecutor, mMockTransitionsLazy, mDockStateReader, + mCompatUIConfiguration, mCompatUIShellCommandHandler) { @Override CompatUIWindowManager createCompatUiWindowManager(Context context, TaskInfo taskInfo, ShellTaskOrganizer.TaskListener taskListener) { @@ -119,11 +136,28 @@ public class CompatUIControllerTest extends ShellTestCase { TaskInfo taskInfo, ShellTaskOrganizer.TaskListener taskListener) { return mMockLetterboxEduLayout; } + + @Override + RestartDialogWindowManager createRestartDialogWindowManager(Context context, + TaskInfo taskInfo, ShellTaskOrganizer.TaskListener taskListener) { + return mMockRestartDialogLayout; + } }; + 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); @@ -140,6 +174,8 @@ public class CompatUIControllerTest extends ShellTestCase { verify(mController).createCompatUiWindowManager(any(), eq(taskInfo), eq(mMockTaskListener)); verify(mController).createLetterboxEduWindowManager(any(), eq(taskInfo), eq(mMockTaskListener)); + verify(mController).createRestartDialogWindowManager(any(), eq(taskInfo), + eq(mMockTaskListener)); // Verify that the compat controls and letterbox education are updated with new size compat // info. @@ -148,10 +184,12 @@ public class CompatUIControllerTest extends ShellTestCase { CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED); mController.onCompatInfoChanged(taskInfo, mMockTaskListener); - verify(mMockCompatLayout).updateCompatInfo(taskInfo, mMockTaskListener, /* canShow= */ - true); - verify(mMockLetterboxEduLayout).updateCompatInfo(taskInfo, mMockTaskListener, /* canShow= */ - true); + verify(mMockCompatLayout).updateCompatInfo(taskInfo, mMockTaskListener, + /* canShow= */ true); + verify(mMockLetterboxEduLayout).updateCompatInfo(taskInfo, mMockTaskListener, + /* canShow= */ true); + verify(mMockRestartDialogLayout).updateCompatInfo(taskInfo, mMockTaskListener, + /* canShow= */ true); // Verify that compat controls and letterbox education are removed with null task listener. clearInvocations(mMockCompatLayout, mMockLetterboxEduLayout, mController); @@ -161,12 +199,14 @@ public class CompatUIControllerTest extends ShellTestCase { verify(mMockCompatLayout).release(); verify(mMockLetterboxEduLayout).release(); + verify(mMockRestartDialogLayout).release(); } @Test public void testOnCompatInfoChanged_createLayoutReturnsFalse() { doReturn(false).when(mMockCompatLayout).createLayout(anyBoolean()); doReturn(false).when(mMockLetterboxEduLayout).createLayout(anyBoolean()); + doReturn(false).when(mMockRestartDialogLayout).createLayout(anyBoolean()); TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true, CAMERA_COMPAT_CONTROL_HIDDEN); @@ -175,6 +215,8 @@ public class CompatUIControllerTest extends ShellTestCase { verify(mController).createCompatUiWindowManager(any(), eq(taskInfo), eq(mMockTaskListener)); verify(mController).createLetterboxEduWindowManager(any(), eq(taskInfo), eq(mMockTaskListener)); + verify(mController).createRestartDialogWindowManager(any(), eq(taskInfo), + eq(mMockTaskListener)); // Verify that the layout is created again. clearInvocations(mMockCompatLayout, mMockLetterboxEduLayout, mController); @@ -182,15 +224,19 @@ public class CompatUIControllerTest extends ShellTestCase { verify(mMockCompatLayout, never()).updateCompatInfo(any(), any(), anyBoolean()); verify(mMockLetterboxEduLayout, never()).updateCompatInfo(any(), any(), anyBoolean()); + verify(mMockRestartDialogLayout, never()).updateCompatInfo(any(), any(), anyBoolean()); verify(mController).createCompatUiWindowManager(any(), eq(taskInfo), eq(mMockTaskListener)); verify(mController).createLetterboxEduWindowManager(any(), eq(taskInfo), eq(mMockTaskListener)); + verify(mController).createRestartDialogWindowManager(any(), eq(taskInfo), + eq(mMockTaskListener)); } @Test public void testOnCompatInfoChanged_updateCompatInfoReturnsFalse() { doReturn(false).when(mMockCompatLayout).updateCompatInfo(any(), any(), anyBoolean()); doReturn(false).when(mMockLetterboxEduLayout).updateCompatInfo(any(), any(), anyBoolean()); + doReturn(false).when(mMockRestartDialogLayout).updateCompatInfo(any(), any(), anyBoolean()); TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true, CAMERA_COMPAT_CONTROL_HIDDEN); @@ -199,24 +245,33 @@ public class CompatUIControllerTest extends ShellTestCase { verify(mController).createCompatUiWindowManager(any(), eq(taskInfo), eq(mMockTaskListener)); verify(mController).createLetterboxEduWindowManager(any(), eq(taskInfo), eq(mMockTaskListener)); + verify(mController).createRestartDialogWindowManager(any(), eq(taskInfo), + eq(mMockTaskListener)); - clearInvocations(mMockCompatLayout, mMockLetterboxEduLayout, mController); + clearInvocations(mMockCompatLayout, mMockLetterboxEduLayout, mMockRestartDialogLayout, + mController); mController.onCompatInfoChanged(taskInfo, mMockTaskListener); - verify(mMockCompatLayout).updateCompatInfo(taskInfo, mMockTaskListener, /* canShow= */ - true); - verify(mMockLetterboxEduLayout).updateCompatInfo(taskInfo, mMockTaskListener, /* canShow= */ - true); + verify(mMockCompatLayout).updateCompatInfo(taskInfo, mMockTaskListener, + /* canShow= */ true); + verify(mMockLetterboxEduLayout).updateCompatInfo(taskInfo, mMockTaskListener, + /* canShow= */ true); + verify(mMockRestartDialogLayout).updateCompatInfo(taskInfo, mMockTaskListener, + /* canShow= */ true); // Verify that the layout is created again. - clearInvocations(mMockCompatLayout, mMockLetterboxEduLayout, mController); + clearInvocations(mMockCompatLayout, mMockLetterboxEduLayout, mMockRestartDialogLayout, + mController); mController.onCompatInfoChanged(taskInfo, mMockTaskListener); verify(mMockCompatLayout, never()).updateCompatInfo(any(), any(), anyBoolean()); verify(mMockLetterboxEduLayout, never()).updateCompatInfo(any(), any(), anyBoolean()); + verify(mMockRestartDialogLayout, never()).updateCompatInfo(any(), any(), anyBoolean()); verify(mController).createCompatUiWindowManager(any(), eq(taskInfo), eq(mMockTaskListener)); verify(mController).createLetterboxEduWindowManager(any(), eq(taskInfo), eq(mMockTaskListener)); + verify(mController).createRestartDialogWindowManager(any(), eq(taskInfo), + eq(mMockTaskListener)); } @@ -240,6 +295,7 @@ public class CompatUIControllerTest extends ShellTestCase { verify(mMockCompatLayout, never()).release(); verify(mMockLetterboxEduLayout, never()).release(); + verify(mMockRestartDialogLayout, never()).release(); verify(mMockDisplayInsetsController, never()).removeInsetsChangedListener(eq(DISPLAY_ID), any()); @@ -248,6 +304,7 @@ public class CompatUIControllerTest extends ShellTestCase { verify(mMockDisplayInsetsController).removeInsetsChangedListener(eq(DISPLAY_ID), any()); verify(mMockCompatLayout).release(); verify(mMockLetterboxEduLayout).release(); + verify(mMockRestartDialogLayout).release(); } @Test @@ -259,11 +316,13 @@ public class CompatUIControllerTest extends ShellTestCase { verify(mMockCompatLayout, never()).updateDisplayLayout(any()); verify(mMockLetterboxEduLayout, never()).updateDisplayLayout(any()); + verify(mMockRestartDialogLayout, never()).updateDisplayLayout(any()); mController.onDisplayConfigurationChanged(DISPLAY_ID, new Configuration()); verify(mMockCompatLayout).updateDisplayLayout(mMockDisplayLayout); verify(mMockLetterboxEduLayout).updateDisplayLayout(mMockDisplayLayout); + verify(mMockRestartDialogLayout).updateDisplayLayout(mMockDisplayLayout); } @Test @@ -272,7 +331,8 @@ public class CompatUIControllerTest extends ShellTestCase { mController.onCompatInfoChanged(createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true, CAMERA_COMPAT_CONTROL_HIDDEN), mMockTaskListener); InsetsState insetsState = new InsetsState(); - InsetsSource insetsSource = new InsetsSource(ITYPE_EXTRA_NAVIGATION_BAR); + InsetsSource insetsSource = new InsetsSource( + InsetsSource.createId(null, 0, navigationBars()), navigationBars()); insetsSource.setFrame(0, 0, 1000, 1000); insetsState.addSource(insetsSource); @@ -282,12 +342,14 @@ public class CompatUIControllerTest extends ShellTestCase { verify(mMockCompatLayout).updateDisplayLayout(mMockDisplayLayout); verify(mMockLetterboxEduLayout).updateDisplayLayout(mMockDisplayLayout); + verify(mMockRestartDialogLayout).updateDisplayLayout(mMockDisplayLayout); // No update if the insets state is the same. - clearInvocations(mMockCompatLayout, mMockLetterboxEduLayout); + clearInvocations(mMockCompatLayout, mMockLetterboxEduLayout, mMockRestartDialogLayout); mOnInsetsChangedListenerCaptor.getValue().insetsChanged(new InsetsState(insetsState)); verify(mMockCompatLayout, never()).updateDisplayLayout(mMockDisplayLayout); verify(mMockLetterboxEduLayout, never()).updateDisplayLayout(mMockDisplayLayout); + verify(mMockRestartDialogLayout, never()).updateDisplayLayout(mMockDisplayLayout); } @Test @@ -300,22 +362,26 @@ public class CompatUIControllerTest extends ShellTestCase { verify(mMockCompatLayout).updateVisibility(false); verify(mMockLetterboxEduLayout).updateVisibility(false); + verify(mMockRestartDialogLayout).updateVisibility(false); // Verify button remains hidden while IME is showing. TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true, CAMERA_COMPAT_CONTROL_HIDDEN); mController.onCompatInfoChanged(taskInfo, mMockTaskListener); - verify(mMockCompatLayout).updateCompatInfo(taskInfo, mMockTaskListener, /* canShow= */ - false); - verify(mMockLetterboxEduLayout).updateCompatInfo(taskInfo, mMockTaskListener, /* canShow= */ - false); + verify(mMockCompatLayout).updateCompatInfo(taskInfo, mMockTaskListener, + /* canShow= */ false); + verify(mMockLetterboxEduLayout).updateCompatInfo(taskInfo, mMockTaskListener, + /* canShow= */ false); + verify(mMockRestartDialogLayout).updateCompatInfo(taskInfo, mMockTaskListener, + /* canShow= */ false); // Verify button is shown after IME is hidden. mController.onImeVisibilityChanged(DISPLAY_ID, /* isShowing= */ false); verify(mMockCompatLayout).updateVisibility(true); verify(mMockLetterboxEduLayout).updateVisibility(true); + verify(mMockRestartDialogLayout).updateVisibility(true); } @Test @@ -324,26 +390,30 @@ 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); + verify(mMockRestartDialogLayout).updateVisibility(false); // Verify button remains hidden while keyguard is showing. TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true, CAMERA_COMPAT_CONTROL_HIDDEN); mController.onCompatInfoChanged(taskInfo, mMockTaskListener); - verify(mMockCompatLayout).updateCompatInfo(taskInfo, mMockTaskListener, /* canShow= */ - false); - verify(mMockLetterboxEduLayout).updateCompatInfo(taskInfo, mMockTaskListener, /* canShow= */ - false); + verify(mMockCompatLayout).updateCompatInfo(taskInfo, mMockTaskListener, + /* canShow= */ false); + verify(mMockLetterboxEduLayout).updateCompatInfo(taskInfo, mMockTaskListener, + /* canShow= */ false); + verify(mMockRestartDialogLayout).updateCompatInfo(taskInfo, mMockTaskListener, + /* canShow= */ 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); + verify(mMockRestartDialogLayout).updateVisibility(true); } @Test @@ -352,24 +422,27 @@ 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); + verify(mMockRestartDialogLayout, times(2)).updateVisibility(false); - clearInvocations(mMockCompatLayout, mMockLetterboxEduLayout); + clearInvocations(mMockCompatLayout, mMockLetterboxEduLayout, mMockRestartDialogLayout); // 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); + verify(mMockRestartDialogLayout).updateVisibility(false); // Verify button is shown after IME is not showing. mController.onImeVisibilityChanged(DISPLAY_ID, /* isShowing= */ false); verify(mMockCompatLayout).updateVisibility(true); verify(mMockLetterboxEduLayout).updateVisibility(true); + verify(mMockRestartDialogLayout).updateVisibility(true); } @Test @@ -378,24 +451,57 @@ 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); + verify(mMockRestartDialogLayout, times(2)).updateVisibility(false); - clearInvocations(mMockCompatLayout, mMockLetterboxEduLayout); + clearInvocations(mMockCompatLayout, mMockLetterboxEduLayout, mMockRestartDialogLayout); // Verify button remains hidden after IME is hidden since keyguard is showing. mController.onImeVisibilityChanged(DISPLAY_ID, /* isShowing= */ false); verify(mMockCompatLayout).updateVisibility(false); verify(mMockLetterboxEduLayout).updateVisibility(false); + verify(mMockRestartDialogLayout).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); + verify(mMockRestartDialogLayout).updateVisibility(true); + } + + @Test + public void testRestartLayoutRecreatedIfNeeded() { + final TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, + /* hasSizeCompat= */ true, CAMERA_COMPAT_CONTROL_HIDDEN); + doReturn(true).when(mMockRestartDialogLayout) + .needsToBeRecreated(any(TaskInfo.class), + any(ShellTaskOrganizer.TaskListener.class)); + + mController.onCompatInfoChanged(taskInfo, mMockTaskListener); + mController.onCompatInfoChanged(taskInfo, mMockTaskListener); + + verify(mMockRestartDialogLayout, times(2)) + .createLayout(anyBoolean()); + } + + @Test + public void testRestartLayoutNotRecreatedIfNotNeeded() { + final TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, + /* hasSizeCompat= */ true, CAMERA_COMPAT_CONTROL_HIDDEN); + doReturn(false).when(mMockRestartDialogLayout) + .needsToBeRecreated(any(TaskInfo.class), + any(ShellTaskOrganizer.TaskListener.class)); + + mController.onCompatInfoChanged(taskInfo, mMockTaskListener); + mController.onCompatInfoChanged(taskInfo, mMockTaskListener); + + verify(mMockRestartDialogLayout, times(1)) + .createLayout(anyBoolean()); } private static TaskInfo createTaskInfo(int displayId, int taskId, boolean hasSizeCompat, diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java index 7d3e718313e6..5f294d53b662 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java @@ -31,6 +31,7 @@ import android.app.ActivityManager; import android.app.TaskInfo; import android.app.TaskInfo.CameraCompatControlState; import android.testing.AndroidTestingRunner; +import android.util.Pair; import android.view.LayoutInflater; import android.view.SurfaceControlViewHost; import android.widget.ImageButton; @@ -45,12 +46,17 @@ import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.compatui.CompatUIWindowManager.CompatUIHintsState; +import junit.framework.Assert; + import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import java.util.function.Consumer; + /** * Tests for {@link CompatUILayout}. * @@ -65,20 +71,22 @@ public class CompatUILayoutTest extends ShellTestCase { @Mock private SyncTransactionQueue mSyncTransactionQueue; @Mock private CompatUIController.CompatUICallback mCallback; + @Mock private Consumer<Pair<TaskInfo, ShellTaskOrganizer.TaskListener>> mOnRestartButtonClicked; @Mock private ShellTaskOrganizer.TaskListener mTaskListener; @Mock private SurfaceControlViewHost mViewHost; + @Mock private CompatUIConfiguration mCompatUIConfiguration; private CompatUIWindowManager mWindowManager; private CompatUILayout mLayout; + private TaskInfo mTaskInfo; @Before public void setUp() { MockitoAnnotations.initMocks(this); - - mWindowManager = new CompatUIWindowManager(mContext, - createTaskInfo(/* hasSizeCompat= */ false, CAMERA_COMPAT_CONTROL_HIDDEN), - mSyncTransactionQueue, mCallback, mTaskListener, - new DisplayLayout(), new CompatUIHintsState()); + mTaskInfo = createTaskInfo(/* hasSizeCompat= */ false, CAMERA_COMPAT_CONTROL_HIDDEN); + mWindowManager = new CompatUIWindowManager(mContext, mTaskInfo, mSyncTransactionQueue, + mCallback, mTaskListener, new DisplayLayout(), new CompatUIHintsState(), + mCompatUIConfiguration, mOnRestartButtonClicked); mLayout = (CompatUILayout) LayoutInflater.from(mContext).inflate(R.layout.compat_ui_layout, null); @@ -95,8 +103,15 @@ public class CompatUILayoutTest extends ShellTestCase { final ImageButton button = mLayout.findViewById(R.id.size_compat_restart_button); button.performClick(); + @SuppressWarnings("unchecked") + ArgumentCaptor<Pair<TaskInfo, ShellTaskOrganizer.TaskListener>> restartCaptor = + ArgumentCaptor.forClass(Pair.class); + verify(mWindowManager).onRestartButtonClicked(); - verify(mCallback).onSizeCompatRestartButtonClicked(TASK_ID); + verify(mOnRestartButtonClicked).accept(restartCaptor.capture()); + final Pair<TaskInfo, ShellTaskOrganizer.TaskListener> result = restartCaptor.getValue(); + Assert.assertEquals(mTaskInfo, result.first); + Assert.assertEquals(mTaskListener, result.second); } @Test diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java index e79b803b4304..4de529885565 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java @@ -20,7 +20,7 @@ import static android.app.TaskInfo.CAMERA_COMPAT_CONTROL_DISMISSED; import static android.app.TaskInfo.CAMERA_COMPAT_CONTROL_HIDDEN; import static android.app.TaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED; import static android.app.TaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED; -import static android.view.InsetsState.ITYPE_EXTRA_NAVIGATION_BAR; +import static android.view.WindowInsets.Type.navigationBars; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; @@ -28,6 +28,7 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; @@ -38,6 +39,7 @@ import android.app.ActivityManager; import android.app.TaskInfo; import android.graphics.Rect; import android.testing.AndroidTestingRunner; +import android.util.Pair; import android.view.DisplayInfo; import android.view.InsetsSource; import android.view.InsetsState; @@ -53,12 +55,17 @@ import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.compatui.CompatUIWindowManager.CompatUIHintsState; +import junit.framework.Assert; + import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import java.util.function.Consumer; + /** * Tests for {@link CompatUIWindowManager}. * @@ -73,20 +80,22 @@ public class CompatUIWindowManagerTest extends ShellTestCase { @Mock private SyncTransactionQueue mSyncTransactionQueue; @Mock private CompatUIController.CompatUICallback mCallback; + @Mock private Consumer<Pair<TaskInfo, ShellTaskOrganizer.TaskListener>> mOnRestartButtonClicked; @Mock private ShellTaskOrganizer.TaskListener mTaskListener; @Mock private CompatUILayout mLayout; @Mock private SurfaceControlViewHost mViewHost; + @Mock private CompatUIConfiguration mCompatUIConfiguration; private CompatUIWindowManager mWindowManager; + private TaskInfo mTaskInfo; @Before public void setUp() { MockitoAnnotations.initMocks(this); - - mWindowManager = new CompatUIWindowManager(mContext, - createTaskInfo(/* hasSizeCompat= */ false, CAMERA_COMPAT_CONTROL_HIDDEN), - mSyncTransactionQueue, mCallback, mTaskListener, - new DisplayLayout(), new CompatUIHintsState()); + mTaskInfo = createTaskInfo(/* hasSizeCompat= */ false, CAMERA_COMPAT_CONTROL_HIDDEN); + mWindowManager = new CompatUIWindowManager(mContext, mTaskInfo, mSyncTransactionQueue, + mCallback, mTaskListener, new DisplayLayout(), new CompatUIHintsState(), + mCompatUIConfiguration, mOnRestartButtonClicked); spyOn(mWindowManager); doReturn(mLayout).when(mWindowManager).inflateLayout(); @@ -328,8 +337,10 @@ public class CompatUIWindowManagerTest extends ShellTestCase { // Update if the insets change on the existing display layout clearInvocations(mWindowManager); InsetsState insetsState = new InsetsState(); - InsetsSource insetsSource = new InsetsSource(ITYPE_EXTRA_NAVIGATION_BAR); - insetsSource.setFrame(0, 0, 1000, 1000); + insetsState.setDisplayFrame(new Rect(0, 0, 1000, 2000)); + InsetsSource insetsSource = new InsetsSource( + InsetsSource.createId(null, 0, navigationBars()), navigationBars()); + insetsSource.setFrame(0, 1800, 1000, 2000); insetsState.addSource(insetsSource); displayLayout.setInsets(mContext.getResources(), insetsState); mWindowManager.updateDisplayLayout(displayLayout); @@ -351,14 +362,14 @@ public class CompatUIWindowManagerTest extends ShellTestCase { mWindowManager.updateVisibility(/* canShow= */ false); verify(mWindowManager, never()).createLayout(anyBoolean()); - verify(mLayout).setVisibility(View.GONE); + verify(mLayout, atLeastOnce()).setVisibility(View.GONE); // Show button. doReturn(View.GONE).when(mLayout).getVisibility(); mWindowManager.updateVisibility(/* canShow= */ true); verify(mWindowManager, never()).createLayout(anyBoolean()); - verify(mLayout).setVisibility(View.VISIBLE); + verify(mLayout, atLeastOnce()).setVisibility(View.VISIBLE); } @Test @@ -404,7 +415,14 @@ public class CompatUIWindowManagerTest extends ShellTestCase { public void testOnRestartButtonClicked() { mWindowManager.onRestartButtonClicked(); - verify(mCallback).onSizeCompatRestartButtonClicked(TASK_ID); + @SuppressWarnings("unchecked") + ArgumentCaptor<Pair<TaskInfo, ShellTaskOrganizer.TaskListener>> restartCaptor = + ArgumentCaptor.forClass(Pair.class); + + verify(mOnRestartButtonClicked).accept(restartCaptor.capture()); + final Pair<TaskInfo, ShellTaskOrganizer.TaskListener> result = restartCaptor.getValue(); + Assert.assertEquals(mTaskInfo, result.first); + Assert.assertEquals(mTaskListener, result.second); } @Test diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduDialogLayoutTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/LetterboxEduDialogLayoutTest.java index 1dee88c43806..172c263ab0f6 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduDialogLayoutTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/LetterboxEduDialogLayoutTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.compatui.letterboxedu; +package com.android.wm.shell.compatui; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -58,9 +58,8 @@ public class LetterboxEduDialogLayoutTest extends ShellTestCase { public void setUp() { MockitoAnnotations.initMocks(this); - mLayout = (LetterboxEduDialogLayout) - LayoutInflater.from(mContext).inflate(R.layout.letterbox_education_dialog_layout, - null); + mLayout = (LetterboxEduDialogLayout) LayoutInflater.from(mContext) + .inflate(R.layout.letterbox_education_dialog_layout, null); mDismissButton = mLayout.findViewById(R.id.letterbox_education_dialog_dismiss_button); mDialogContainer = mLayout.findViewById(R.id.letterbox_education_dialog_container); mLayout.setDismissOnClickListener(mDismissCallback); @@ -68,11 +67,11 @@ public class LetterboxEduDialogLayoutTest extends ShellTestCase { @Test public void testOnFinishInflate() { - assertEquals(mLayout.getDialogContainer(), + assertEquals(mLayout.getDialogContainerView(), mLayout.findViewById(R.id.letterbox_education_dialog_container)); assertEquals(mLayout.getDialogTitle(), mLayout.findViewById(R.id.letterbox_education_dialog_title)); - assertEquals(mLayout.getBackgroundDim(), mLayout.getBackground()); + assertEquals(mLayout.getBackgroundDimDrawable(), mLayout.getBackground()); assertEquals(mLayout.getBackground().getAlpha(), 0); } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduWindowManagerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/LetterboxEduWindowManagerTest.java index f3a8cf45b7f8..12ceb0a9a9ba 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduWindowManagerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/LetterboxEduWindowManagerTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.compatui.letterboxedu; +package com.android.wm.shell.compatui; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; @@ -31,14 +31,12 @@ import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; -import android.annotation.Nullable; import android.app.ActivityManager; import android.app.TaskInfo; -import android.content.Context; -import android.content.SharedPreferences; import android.graphics.Insets; import android.graphics.Rect; import android.testing.AndroidTestingRunner; +import android.util.Pair; import android.view.DisplayCutout; import android.view.DisplayInfo; import android.view.SurfaceControlViewHost; @@ -53,7 +51,9 @@ import androidx.test.filters.SmallTest; import com.android.wm.shell.R; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.ShellTestCase; +import com.android.wm.shell.TestShellExecutor; import com.android.wm.shell.common.DisplayLayout; +import com.android.wm.shell.common.DockStateReader; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.transition.Transitions; @@ -66,6 +66,10 @@ import org.mockito.Captor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import java.util.HashSet; +import java.util.Set; +import java.util.function.Consumer; + /** * Tests for {@link LetterboxEduWindowManager}. * @@ -79,8 +83,10 @@ public class LetterboxEduWindowManagerTest extends ShellTestCase { private static final int USER_ID_1 = 1; private static final int USER_ID_2 = 2; - private static final String PREF_KEY_1 = String.valueOf(USER_ID_1); - private static final String PREF_KEY_2 = String.valueOf(USER_ID_2); + private static final String TEST_COMPAT_UI_SHARED_PREFERENCES = "test_compat_ui_configuration"; + + private static final String TEST_HAS_SEEN_LETTERBOX_SHARED_PREFERENCES = + "test_has_seen_letterbox"; private static final int TASK_ID = 1; @@ -97,50 +103,51 @@ public class LetterboxEduWindowManagerTest extends ShellTestCase { @Captor private ArgumentCaptor<Runnable> mRunOnIdleCaptor; - @Mock private LetterboxEduAnimationController mAnimationController; + @Mock private DialogAnimationController<LetterboxEduDialogLayout> mAnimationController; @Mock private SyncTransactionQueue mSyncTransactionQueue; @Mock private ShellTaskOrganizer.TaskListener mTaskListener; @Mock private SurfaceControlViewHost mViewHost; @Mock private Transitions mTransitions; - @Mock private Runnable mOnDismissCallback; + @Mock private Consumer<Pair<TaskInfo, ShellTaskOrganizer.TaskListener>> mOnDismissCallback; + @Mock private DockStateReader mDockStateReader; - private SharedPreferences mSharedPreferences; - @Nullable - private Boolean mInitialPrefValue1 = null; - @Nullable - private Boolean mInitialPrefValue2 = null; + private CompatUIConfiguration mCompatUIConfiguration; + private TestShellExecutor mExecutor; @Before public void setUp() { MockitoAnnotations.initMocks(this); - - mSharedPreferences = mContext.getSharedPreferences( - LetterboxEduWindowManager.HAS_SEEN_LETTERBOX_EDUCATION_PREF_NAME, - Context.MODE_PRIVATE); - if (mSharedPreferences.contains(PREF_KEY_1)) { - mInitialPrefValue1 = mSharedPreferences.getBoolean(PREF_KEY_1, /* default= */ false); - mSharedPreferences.edit().remove(PREF_KEY_1).apply(); - } - if (mSharedPreferences.contains(PREF_KEY_2)) { - mInitialPrefValue2 = mSharedPreferences.getBoolean(PREF_KEY_2, /* default= */ false); - mSharedPreferences.edit().remove(PREF_KEY_2).apply(); - } + mExecutor = new TestShellExecutor(); + mCompatUIConfiguration = new CompatUIConfiguration(mContext, mExecutor) { + + final Set<Integer> mHasSeenSet = new HashSet<>(); + + @Override + boolean getHasSeenLetterboxEducation(int userId) { + return mHasSeenSet.contains(userId); + } + + @Override + void setSeenLetterboxEducation(int userId) { + mHasSeenSet.add(userId); + } + + @Override + protected String getCompatUISharedPreferenceName() { + return TEST_COMPAT_UI_SHARED_PREFERENCES; + } + + @Override + protected String getHasSeenLetterboxEducationSharedPreferencedName() { + return TEST_HAS_SEEN_LETTERBOX_SHARED_PREFERENCES; + } + }; } @After public void tearDown() { - SharedPreferences.Editor editor = mSharedPreferences.edit(); - if (mInitialPrefValue1 == null) { - editor.remove(PREF_KEY_1); - } else { - editor.putBoolean(PREF_KEY_1, mInitialPrefValue1); - } - if (mInitialPrefValue2 == null) { - editor.remove(PREF_KEY_2); - } else { - editor.putBoolean(PREF_KEY_2, mInitialPrefValue2); - } - editor.apply(); + mContext.deleteSharedPreferences(TEST_COMPAT_UI_SHARED_PREFERENCES); + mContext.deleteSharedPreferences(TEST_HAS_SEEN_LETTERBOX_SHARED_PREFERENCES); } @Test @@ -153,9 +160,19 @@ public class LetterboxEduWindowManagerTest extends ShellTestCase { } @Test - public void testCreateLayout_taskBarEducationIsShowing_doesNotCreateLayout() { + public void testCreateLayout_eligibleAndDocked_doesNotCreateLayout() { LetterboxEduWindowManager windowManager = createWindowManager(/* eligible= */ - true, USER_ID_1, /* isTaskbarEduShowing= */ true); + true, /* isDocked */ true); + + assertFalse(windowManager.createLayout(/* canShow= */ true)); + + assertNull(windowManager.mLayout); + } + + @Test + public void testCreateLayout_taskBarEducationIsShowing_doesNotCreateLayout() { + LetterboxEduWindowManager windowManager = createWindowManager(/* eligible= */ true, + USER_ID_1, /* isTaskbarEduShowing= */ true); assertFalse(windowManager.createLayout(/* canShow= */ true)); @@ -168,7 +185,7 @@ public class LetterboxEduWindowManagerTest extends ShellTestCase { assertTrue(windowManager.createLayout(/* canShow= */ false)); - assertFalse(mSharedPreferences.getBoolean(PREF_KEY_1, /* default= */ false)); + assertFalse(mCompatUIConfiguration.getHasSeenLetterboxEducation(USER_ID_1)); assertNull(windowManager.mLayout); } @@ -189,7 +206,7 @@ public class LetterboxEduWindowManagerTest extends ShellTestCase { spyOn(dialogTitle); // The education shouldn't be marked as seen until enter animation is done. - assertFalse(mSharedPreferences.getBoolean(PREF_KEY_1, /* default= */ false)); + assertFalse(mCompatUIConfiguration.getHasSeenLetterboxEducation(USER_ID_1)); // Clicking the layout does nothing until enter animation is done. layout.performClick(); verify(mAnimationController, never()).startExitAnimation(any(), any()); @@ -198,7 +215,7 @@ public class LetterboxEduWindowManagerTest extends ShellTestCase { verifyAndFinishEnterAnimation(layout); - assertTrue(mSharedPreferences.getBoolean(PREF_KEY_1, /* default= */ false)); + assertFalse(mCompatUIConfiguration.getHasSeenLetterboxEducation(USER_ID_1)); verify(dialogTitle).sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED); // Exit animation should start following a click on the layout. layout.performClick(); @@ -206,13 +223,16 @@ public class LetterboxEduWindowManagerTest extends ShellTestCase { // Window manager isn't released until exit animation is done. verify(windowManager, never()).release(); + // After dismissed the user has seen the dialog + assertTrue(mCompatUIConfiguration.getHasSeenLetterboxEducation(USER_ID_1)); + // Verify multiple clicks are ignored. layout.performClick(); verifyAndFinishExitAnimation(layout); verify(windowManager).release(); - verify(mOnDismissCallback).run(); + verify(mOnDismissCallback).accept(any()); } @Test @@ -224,7 +244,10 @@ public class LetterboxEduWindowManagerTest extends ShellTestCase { assertNotNull(windowManager.mLayout); verifyAndFinishEnterAnimation(windowManager.mLayout); - assertTrue(mSharedPreferences.getBoolean(PREF_KEY_1, /* default= */ false)); + + // We dismiss + windowManager.mLayout.findViewById(R.id.letterbox_education_dialog_dismiss_button) + .performClick(); windowManager.release(); windowManager = createWindowManager(/* eligible= */ true, @@ -242,7 +265,7 @@ public class LetterboxEduWindowManagerTest extends ShellTestCase { assertNotNull(windowManager.mLayout); verifyAndFinishEnterAnimation(windowManager.mLayout); - assertTrue(mSharedPreferences.getBoolean(PREF_KEY_1, /* default= */ false)); + assertTrue(mCompatUIConfiguration.getHasSeenLetterboxEducation(USER_ID_1)); } @Test @@ -259,7 +282,7 @@ public class LetterboxEduWindowManagerTest extends ShellTestCase { mRunOnIdleCaptor.getValue().run(); verify(mAnimationController, never()).startEnterAnimation(any(), any()); - assertFalse(mSharedPreferences.getBoolean(PREF_KEY_1, /* default= */ false)); + assertFalse(mCompatUIConfiguration.getHasSeenLetterboxEducation(USER_ID_1)); } @Test @@ -285,7 +308,7 @@ public class LetterboxEduWindowManagerTest extends ShellTestCase { mTaskListener, /* canShow= */ true)); verify(windowManager).release(); - verify(mOnDismissCallback, never()).run(); + verify(mOnDismissCallback, never()).accept(any()); verify(mAnimationController, never()).startExitAnimation(any(), any()); assertNull(windowManager.mLayout); } @@ -354,7 +377,7 @@ public class LetterboxEduWindowManagerTest extends ShellTestCase { assertThat(params.width).isEqualTo(expectedWidth); assertThat(params.height).isEqualTo(expectedHeight); MarginLayoutParams dialogParams = - (MarginLayoutParams) layout.getDialogContainer().getLayoutParams(); + (MarginLayoutParams) layout.getDialogContainerView().getLayoutParams(); int verticalMargin = (int) mContext.getResources().getDimension( R.dimen.letterbox_education_dialog_margin); assertThat(dialogParams.topMargin).isEqualTo(verticalMargin + expectedExtraTopMargin); @@ -382,17 +405,26 @@ public class LetterboxEduWindowManagerTest extends ShellTestCase { return createWindowManager(eligible, USER_ID_1, /* isTaskbarEduShowing= */ false); } - private LetterboxEduWindowManager createWindowManager(boolean eligible, - int userId, boolean isTaskbarEduShowing) { - LetterboxEduWindowManager windowManager = new LetterboxEduWindowManager(mContext, - createTaskInfo(eligible, userId), mSyncTransactionQueue, mTaskListener, - createDisplayLayout(), mTransitions, mOnDismissCallback, - mAnimationController); + private LetterboxEduWindowManager createWindowManager(boolean eligible, boolean isDocked) { + return createWindowManager(eligible, USER_ID_1, /* isTaskbarEduShowing= */ false, isDocked); + } + + private LetterboxEduWindowManager createWindowManager(boolean eligible, int userId, + boolean isTaskbarEduShowing) { + return createWindowManager(eligible, userId, isTaskbarEduShowing, /* isDocked */false); + } + private LetterboxEduWindowManager createWindowManager(boolean eligible, int userId, + boolean isTaskbarEduShowing, boolean isDocked) { + doReturn(isDocked).when(mDockStateReader).isDocked(); + LetterboxEduWindowManager + windowManager = new LetterboxEduWindowManager(mContext, + createTaskInfo(eligible, userId), mSyncTransactionQueue, mTaskListener, + createDisplayLayout(), mTransitions, mOnDismissCallback, mAnimationController, + mDockStateReader, mCompatUIConfiguration); spyOn(windowManager); doReturn(mViewHost).when(windowManager).createSurfaceViewHost(); doReturn(isTaskbarEduShowing).when(windowManager).isTaskbarEduShowing(); - return windowManager; } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/ReachabilityEduLayoutTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/ReachabilityEduLayoutTest.java new file mode 100644 index 000000000000..0be08ba74d86 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/ReachabilityEduLayoutTest.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.compatui; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertNotNull; + +import android.testing.AndroidTestingRunner; +import android.view.LayoutInflater; +import android.view.View; + +import androidx.test.filters.SmallTest; + +import com.android.wm.shell.R; +import com.android.wm.shell.ShellTestCase; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.MockitoAnnotations; + +/** + * Tests for {@link LetterboxEduDialogLayout}. + * + * Build/Install/Run: + * atest WMShellUnitTests:ReachabilityEduLayoutTest + */ +@RunWith(AndroidTestingRunner.class) +@SmallTest +public class ReachabilityEduLayoutTest extends ShellTestCase { + + private ReachabilityEduLayout mLayout; + private View mMoveUpButton; + private View mMoveDownButton; + private View mMoveLeftButton; + private View mMoveRightButton; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mLayout = (ReachabilityEduLayout) LayoutInflater.from(mContext) + .inflate(R.layout.reachability_ui_layout, null); + mMoveLeftButton = mLayout.findViewById(R.id.reachability_move_left_button); + mMoveRightButton = mLayout.findViewById(R.id.reachability_move_right_button); + mMoveUpButton = mLayout.findViewById(R.id.reachability_move_up_button); + mMoveDownButton = mLayout.findViewById(R.id.reachability_move_down_button); + } + + @Test + public void testOnFinishInflate() { + assertNotNull(mMoveUpButton); + assertNotNull(mMoveDownButton); + assertNotNull(mMoveLeftButton); + assertNotNull(mMoveRightButton); + } + + @Test + public void handleVisibility_activityNotLetterboxed_buttonsAreHidden() { + mLayout.handleVisibility(/* isActivityLetterboxed */ false, + /* letterboxVerticalPosition */ -1, /* letterboxHorizontalPosition */ -1, + /* availableWidth */ 0, /* availableHeight */ 0, /* fromDoubleTap */ false); + assertEquals(View.INVISIBLE, mMoveUpButton.getVisibility()); + assertEquals(View.INVISIBLE, mMoveDownButton.getVisibility()); + assertEquals(View.INVISIBLE, mMoveLeftButton.getVisibility()); + assertEquals(View.INVISIBLE, mMoveRightButton.getVisibility()); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/ReachabilityEduWindowManagerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/ReachabilityEduWindowManagerTest.java new file mode 100644 index 000000000000..359ef979a310 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/ReachabilityEduWindowManagerTest.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.compatui; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; + +import android.app.ActivityManager; +import android.app.TaskInfo; +import android.testing.AndroidTestingRunner; + +import androidx.test.filters.SmallTest; + +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.ShellTestCase; +import com.android.wm.shell.TestShellExecutor; +import com.android.wm.shell.common.DisplayLayout; +import com.android.wm.shell.common.SyncTransactionQueue; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** + * Tests for {@link ReachabilityEduWindowManager}. + * + * Build/Install/Run: + * atest WMShellUnitTests:ReachabilityEduWindowManagerTest + */ +@RunWith(AndroidTestingRunner.class) +@SmallTest +public class ReachabilityEduWindowManagerTest extends ShellTestCase { + + private static final int USER_ID = 1; + private static final int TASK_ID = 1; + + @Mock + private SyncTransactionQueue mSyncTransactionQueue; + @Mock + private ShellTaskOrganizer.TaskListener mTaskListener; + @Mock + private CompatUIController.CompatUICallback mCallback; + @Mock + private CompatUIConfiguration mCompatUIConfiguration; + @Mock + private DisplayLayout mDisplayLayout; + + private TestShellExecutor mExecutor; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mExecutor = new TestShellExecutor(); + } + + @Test + public void testCreateLayout_notEligible_doesNotCreateLayout() { + final ReachabilityEduWindowManager windowManager = createReachabilityEduWindowManager( + createTaskInfo(/* userId= */ USER_ID, /*isLetterboxDoubleTapEnabled */ false)); + + assertFalse(windowManager.createLayout(/* canShow= */ true)); + + assertNull(windowManager.mLayout); + } + + private ReachabilityEduWindowManager createReachabilityEduWindowManager(TaskInfo taskInfo) { + return new ReachabilityEduWindowManager(mContext, taskInfo, + mSyncTransactionQueue, mCallback, mTaskListener, mDisplayLayout, + mCompatUIConfiguration, mExecutor); + } + + private static TaskInfo createTaskInfo(int userId, boolean isLetterboxDoubleTapEnabled) { + return createTaskInfo(userId, /* isLetterboxDoubleTapEnabled */ isLetterboxDoubleTapEnabled, + /* topActivityLetterboxVerticalPosition */ -1, + /* topActivityLetterboxHorizontalPosition */ -1, + /* topActivityLetterboxWidth */ -1, + /* topActivityLetterboxHeight */ -1); + } + + private static TaskInfo createTaskInfo(int userId, boolean isLetterboxDoubleTapEnabled, + int topActivityLetterboxVerticalPosition, int topActivityLetterboxHorizontalPosition, + int topActivityLetterboxWidth, int topActivityLetterboxHeight) { + ActivityManager.RunningTaskInfo taskInfo = new ActivityManager.RunningTaskInfo(); + taskInfo.userId = userId; + taskInfo.taskId = TASK_ID; + taskInfo.isLetterboxDoubleTapEnabled = isLetterboxDoubleTapEnabled; + taskInfo.topActivityLetterboxVerticalPosition = topActivityLetterboxVerticalPosition; + taskInfo.topActivityLetterboxHorizontalPosition = topActivityLetterboxHorizontalPosition; + taskInfo.topActivityLetterboxWidth = topActivityLetterboxWidth; + taskInfo.topActivityLetterboxHeight = topActivityLetterboxHeight; + return taskInfo; + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/RestartDialogLayoutTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/RestartDialogLayoutTest.java new file mode 100644 index 000000000000..e2dcdb0e91b2 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/RestartDialogLayoutTest.java @@ -0,0 +1,148 @@ +/* + * 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.compatui; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +import android.testing.AndroidTestingRunner; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.CheckBox; + +import androidx.test.filters.SmallTest; + +import com.android.wm.shell.R; +import com.android.wm.shell.ShellTestCase; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.function.Consumer; + +/** + * Tests for {@link RestartDialogLayout}. + * + * Build/Install/Run: + * atest WMShellUnitTests:RestartDialogLayoutTest + */ +@RunWith(AndroidTestingRunner.class) +@SmallTest +public class RestartDialogLayoutTest extends ShellTestCase { + + @Mock private Runnable mDismissCallback; + @Mock private Consumer<Boolean> mRestartCallback; + + private RestartDialogLayout mLayout; + private View mDismissButton; + private View mRestartButton; + private View mDialogContainer; + private CheckBox mDontRepeatCheckBox; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + mLayout = (RestartDialogLayout) + LayoutInflater.from(mContext).inflate(R.layout.letterbox_restart_dialog_layout, + null); + mDismissButton = mLayout.findViewById(R.id.letterbox_restart_dialog_dismiss_button); + mRestartButton = mLayout.findViewById(R.id.letterbox_restart_dialog_restart_button); + mDialogContainer = mLayout.findViewById(R.id.letterbox_restart_dialog_container); + mDontRepeatCheckBox = mLayout.findViewById(R.id.letterbox_restart_dialog_checkbox); + mLayout.setDismissOnClickListener(mDismissCallback); + mLayout.setRestartOnClickListener(mRestartCallback); + } + + @Test + public void testOnFinishInflate() { + assertEquals(mLayout.getDialogContainerView(), + mLayout.findViewById(R.id.letterbox_restart_dialog_container)); + assertEquals(mLayout.getDialogTitle(), + mLayout.findViewById(R.id.letterbox_restart_dialog_title)); + assertEquals(mLayout.getBackgroundDimDrawable(), mLayout.getBackground()); + assertEquals(mLayout.getBackground().getAlpha(), 0); + } + + @Test + public void testOnDismissButtonClicked() { + assertTrue(mDismissButton.performClick()); + + verify(mDismissCallback).run(); + } + + @Test + public void testOnRestartButtonClickedWithoutCheckbox() { + mDontRepeatCheckBox.setChecked(false); + assertTrue(mRestartButton.performClick()); + + verify(mRestartCallback).accept(false); + } + + @Test + public void testOnRestartButtonClickedWithCheckbox() { + mDontRepeatCheckBox.setChecked(true); + assertTrue(mRestartButton.performClick()); + + verify(mRestartCallback).accept(true); + } + + @Test + public void testOnBackgroundClickedDoesntDismiss() { + assertFalse(mLayout.performClick()); + + verify(mDismissCallback, never()).run(); + } + + @Test + public void testOnDialogContainerClicked() { + assertTrue(mDialogContainer.performClick()); + + verify(mDismissCallback, never()).run(); + verify(mRestartCallback, never()).accept(anyBoolean()); + } + + @Test + public void testSetDismissOnClickListenerNull() { + mLayout.setDismissOnClickListener(null); + + assertFalse(mDismissButton.performClick()); + assertFalse(mLayout.performClick()); + assertTrue(mDialogContainer.performClick()); + + verify(mDismissCallback, never()).run(); + } + + @Test + public void testSetRestartOnClickListenerNull() { + mLayout.setRestartOnClickListener(null); + + assertFalse(mRestartButton.performClick()); + assertFalse(mLayout.performClick()); + assertTrue(mDialogContainer.performClick()); + + verify(mRestartCallback, never()).accept(anyBoolean()); + } + +} 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..63de74fa3b05 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java @@ -0,0 +1,474 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.desktopmode; + +import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; +import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; +import static android.app.WindowConfiguration.WINDOW_CONFIG_BOUNDS; +import static android.view.WindowManager.TRANSIT_CHANGE; +import static android.view.WindowManager.TRANSIT_CLOSE; +import static android.view.WindowManager.TRANSIT_NONE; +import static android.view.WindowManager.TRANSIT_OPEN; +import static android.view.WindowManager.TRANSIT_TO_FRONT; +import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REORDER; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; +import static com.android.wm.shell.desktopmode.DesktopTestHelpers.createFreeformTask; +import static com.android.wm.shell.desktopmode.DesktopTestHelpers.createFullscreenTask; +import static com.android.wm.shell.desktopmode.DesktopTestHelpers.createHomeTask; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; + +import android.app.ActivityManager.RunningTaskInfo; +import android.os.Binder; +import android.os.Handler; +import android.os.IBinder; +import android.testing.AndroidTestingRunner; +import android.window.DisplayAreaInfo; +import android.window.TransitionRequestInfo; +import android.window.WindowContainerTransaction; +import android.window.WindowContainerTransaction.Change; +import android.window.WindowContainerTransaction.HierarchyOp; + +import androidx.test.filters.SmallTest; + +import com.android.dx.mockito.inline.extended.StaticMockitoSession; +import com.android.wm.shell.RootTaskDisplayAreaOrganizer; +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.ShellTestCase; +import com.android.wm.shell.TestShellExecutor; +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.sysui.ShellController; +import com.android.wm.shell.sysui.ShellInit; +import com.android.wm.shell.transition.Transitions; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.Mockito; + +import java.util.ArrayList; +import java.util.Arrays; + +@SmallTest +@RunWith(AndroidTestingRunner.class) +public class DesktopModeControllerTest extends ShellTestCase { + + @Mock + private ShellController mShellController; + @Mock + private ShellTaskOrganizer mShellTaskOrganizer; + @Mock + private RootTaskDisplayAreaOrganizer mRootTaskDisplayAreaOrganizer; + @Mock + private ShellExecutor mTestExecutor; + @Mock + private Handler mMockHandler; + @Mock + private Transitions mTransitions; + private DesktopModeController mController; + private DesktopModeTaskRepository mDesktopModeTaskRepository; + private ShellInit mShellInit; + private StaticMockitoSession mMockitoSession; + + @Before + public void setUp() { + mMockitoSession = mockitoSession().mockStatic(DesktopModeStatus.class).startMocking(); + when(DesktopModeStatus.isProto1Enabled()).thenReturn(true); + when(DesktopModeStatus.isActive(any())).thenReturn(true); + + mShellInit = Mockito.spy(new ShellInit(mTestExecutor)); + + mDesktopModeTaskRepository = new DesktopModeTaskRepository(); + + mController = createController(); + + when(mShellTaskOrganizer.getRunningTasks(anyInt())).thenReturn(new ArrayList<>()); + + mShellInit.init(); + clearInvocations(mShellTaskOrganizer); + clearInvocations(mRootTaskDisplayAreaOrganizer); + clearInvocations(mTransitions); + } + + @After + public void tearDown() { + mMockitoSession.finishMocking(); + } + + @Test + public void instantiate_addInitCallback() { + verify(mShellInit).addInitCallback(any(), any()); + } + + @Test + public void instantiate_flagOff_doNotAddInitCallback() { + when(DesktopModeStatus.isProto1Enabled()).thenReturn(false); + clearInvocations(mShellInit); + + createController(); + + verify(mShellInit, never()).addInitCallback(any(), any()); + } + + @Test + public void testDesktopModeEnabled_rootTdaSetToFreeform() { + DisplayAreaInfo displayAreaInfo = createMockDisplayArea(); + + mController.updateDesktopModeActive(true); + WindowContainerTransaction wct = getDesktopModeSwitchTransaction(); + + // 1 change: Root TDA windowing mode + assertThat(wct.getChanges().size()).isEqualTo(1); + // Verify WCT has a change for setting windowing mode to freeform + Change change = wct.getChanges().get(displayAreaInfo.token.asBinder()); + assertThat(change).isNotNull(); + assertThat(change.getWindowingMode()).isEqualTo(WINDOWING_MODE_FREEFORM); + } + + @Test + public void testDesktopModeDisabled_rootTdaSetToFullscreen() { + DisplayAreaInfo displayAreaInfo = createMockDisplayArea(); + + mController.updateDesktopModeActive(false); + WindowContainerTransaction wct = getDesktopModeSwitchTransaction(); + + // 1 change: Root TDA windowing mode + assertThat(wct.getChanges().size()).isEqualTo(1); + // Verify WCT has a change for setting windowing mode to fullscreen + Change change = wct.getChanges().get(displayAreaInfo.token.asBinder()); + assertThat(change).isNotNull(); + assertThat(change.getWindowingMode()).isEqualTo(WINDOWING_MODE_FULLSCREEN); + } + + @Test + public void testDesktopModeEnabled_windowingModeCleared() { + createMockDisplayArea(); + RunningTaskInfo freeformTask = createFreeformTask(); + RunningTaskInfo fullscreenTask = createFullscreenTask(); + RunningTaskInfo homeTask = createHomeTask(); + when(mShellTaskOrganizer.getRunningTasks(anyInt())).thenReturn(new ArrayList<>( + Arrays.asList(freeformTask, fullscreenTask, homeTask))); + + mController.updateDesktopModeActive(true); + WindowContainerTransaction wct = getDesktopModeSwitchTransaction(); + + // 2 changes: Root TDA windowing mode and 1 task + assertThat(wct.getChanges().size()).isEqualTo(2); + // No changes for tasks that are not standard or freeform + assertThat(wct.getChanges().get(fullscreenTask.token.asBinder())).isNull(); + assertThat(wct.getChanges().get(homeTask.token.asBinder())).isNull(); + // Standard freeform task has windowing mode cleared + Change change = wct.getChanges().get(freeformTask.token.asBinder()); + assertThat(change).isNotNull(); + assertThat(change.getWindowingMode()).isEqualTo(WINDOWING_MODE_UNDEFINED); + } + + @Test + public void testDesktopModeDisabled_windowingModeAndBoundsCleared() { + createMockDisplayArea(); + RunningTaskInfo freeformTask = createFreeformTask(); + RunningTaskInfo fullscreenTask = createFullscreenTask(); + RunningTaskInfo homeTask = createHomeTask(); + when(mShellTaskOrganizer.getRunningTasks(anyInt())).thenReturn(new ArrayList<>( + Arrays.asList(freeformTask, fullscreenTask, homeTask))); + + mController.updateDesktopModeActive(false); + WindowContainerTransaction wct = getDesktopModeSwitchTransaction(); + + // 3 changes: Root TDA windowing mode and 2 tasks + assertThat(wct.getChanges().size()).isEqualTo(3); + // No changes to home task + assertThat(wct.getChanges().get(homeTask.token.asBinder())).isNull(); + // Standard tasks have bounds cleared + assertThatBoundsCleared(wct.getChanges().get(freeformTask.token.asBinder())); + assertThatBoundsCleared(wct.getChanges().get(fullscreenTask.token.asBinder())); + // Freeform standard tasks have windowing mode cleared + assertThat(wct.getChanges().get( + freeformTask.token.asBinder()).getWindowingMode()).isEqualTo( + WINDOWING_MODE_UNDEFINED); + } + + @Test + public void testDesktopModeEnabled_homeTaskBehindVisibleTask() { + createMockDisplayArea(); + RunningTaskInfo fullscreenTask1 = createFullscreenTask(); + fullscreenTask1.isVisible = true; + RunningTaskInfo fullscreenTask2 = createFullscreenTask(); + fullscreenTask2.isVisible = false; + RunningTaskInfo homeTask = createHomeTask(); + when(mShellTaskOrganizer.getRunningTasks(anyInt())).thenReturn(new ArrayList<>( + Arrays.asList(fullscreenTask1, fullscreenTask2, homeTask))); + + mController.updateDesktopModeActive(true); + WindowContainerTransaction wct = getDesktopModeSwitchTransaction(); + + // Check that there are hierarchy changes for home task and visible task + assertThat(wct.getHierarchyOps()).hasSize(2); + // First show home task + HierarchyOp op1 = wct.getHierarchyOps().get(0); + assertThat(op1.getType()).isEqualTo(HIERARCHY_OP_TYPE_REORDER); + assertThat(op1.getContainer()).isEqualTo(homeTask.token.asBinder()); + + // Then visible task on top of it + HierarchyOp op2 = wct.getHierarchyOps().get(1); + assertThat(op2.getType()).isEqualTo(HIERARCHY_OP_TYPE_REORDER); + assertThat(op2.getContainer()).isEqualTo(fullscreenTask1.token.asBinder()); + } + + @Test + public void testShowDesktopApps_allAppsInvisible_bringsToFront() { + // Set up two active tasks on desktop, task2 is on top of task1. + RunningTaskInfo freeformTask1 = createFreeformTask(); + mDesktopModeTaskRepository.addActiveTask(freeformTask1.taskId); + mDesktopModeTaskRepository.addOrMoveFreeformTaskToTop(freeformTask1.taskId); + mDesktopModeTaskRepository.updateVisibleFreeformTasks( + freeformTask1.taskId, false /* visible */); + RunningTaskInfo freeformTask2 = createFreeformTask(); + mDesktopModeTaskRepository.addActiveTask(freeformTask2.taskId); + mDesktopModeTaskRepository.addOrMoveFreeformTaskToTop(freeformTask2.taskId); + mDesktopModeTaskRepository.updateVisibleFreeformTasks( + freeformTask2.taskId, false /* visible */); + when(mShellTaskOrganizer.getRunningTaskInfo(freeformTask1.taskId)).thenReturn( + freeformTask1); + when(mShellTaskOrganizer.getRunningTaskInfo(freeformTask2.taskId)).thenReturn( + freeformTask2); + + // Run show desktop apps logic + mController.showDesktopApps(); + + final WindowContainerTransaction wct = getBringAppsToFrontTransaction(); + // Check wct has reorder calls + assertThat(wct.getHierarchyOps()).hasSize(2); + + // Task 1 appeared first, must be first reorder to top. + HierarchyOp op1 = wct.getHierarchyOps().get(0); + assertThat(op1.getType()).isEqualTo(HIERARCHY_OP_TYPE_REORDER); + assertThat(op1.getContainer()).isEqualTo(freeformTask1.token.asBinder()); + + // Task 2 appeared last, must be last reorder to top. + HierarchyOp op2 = wct.getHierarchyOps().get(1); + assertThat(op2.getType()).isEqualTo(HIERARCHY_OP_TYPE_REORDER); + assertThat(op2.getContainer()).isEqualTo(freeformTask2.token.asBinder()); + } + + @Test + public void testShowDesktopApps_appsAlreadyVisible_bringsToFront() { + final RunningTaskInfo task1 = createFreeformTask(); + mDesktopModeTaskRepository.addActiveTask(task1.taskId); + mDesktopModeTaskRepository.addOrMoveFreeformTaskToTop(task1.taskId); + mDesktopModeTaskRepository.updateVisibleFreeformTasks(task1.taskId, true /* visible */); + when(mShellTaskOrganizer.getRunningTaskInfo(task1.taskId)).thenReturn(task1); + final RunningTaskInfo task2 = createFreeformTask(); + mDesktopModeTaskRepository.addActiveTask(task2.taskId); + mDesktopModeTaskRepository.addOrMoveFreeformTaskToTop(task2.taskId); + mDesktopModeTaskRepository.updateVisibleFreeformTasks(task2.taskId, true /* visible */); + when(mShellTaskOrganizer.getRunningTaskInfo(task2.taskId)).thenReturn(task2); + + mController.showDesktopApps(); + + final WindowContainerTransaction wct = getBringAppsToFrontTransaction(); + // Check wct has reorder calls + assertThat(wct.getHierarchyOps()).hasSize(2); + // Task 1 appeared first, must be first reorder to top. + HierarchyOp op1 = wct.getHierarchyOps().get(0); + assertThat(op1.getType()).isEqualTo(HIERARCHY_OP_TYPE_REORDER); + assertThat(op1.getContainer()).isEqualTo(task1.token.asBinder()); + + // Task 2 appeared last, must be last reorder to top. + HierarchyOp op2 = wct.getHierarchyOps().get(1); + assertThat(op2.getType()).isEqualTo(HIERARCHY_OP_TYPE_REORDER); + assertThat(op2.getContainer()).isEqualTo(task2.token.asBinder()); + } + + @Test + public void testShowDesktopApps_someAppsInvisible_reordersAll() { + final RunningTaskInfo task1 = createFreeformTask(); + mDesktopModeTaskRepository.addActiveTask(task1.taskId); + mDesktopModeTaskRepository.addOrMoveFreeformTaskToTop(task1.taskId); + mDesktopModeTaskRepository.updateVisibleFreeformTasks(task1.taskId, false /* visible */); + when(mShellTaskOrganizer.getRunningTaskInfo(task1.taskId)).thenReturn(task1); + final RunningTaskInfo task2 = createFreeformTask(); + mDesktopModeTaskRepository.addActiveTask(task2.taskId); + mDesktopModeTaskRepository.addOrMoveFreeformTaskToTop(task2.taskId); + mDesktopModeTaskRepository.updateVisibleFreeformTasks(task2.taskId, true /* visible */); + when(mShellTaskOrganizer.getRunningTaskInfo(task2.taskId)).thenReturn(task2); + + mController.showDesktopApps(); + + final WindowContainerTransaction wct = getBringAppsToFrontTransaction(); + // Both tasks should be reordered to top, even if one was already visible. + assertThat(wct.getHierarchyOps()).hasSize(2); + final HierarchyOp op1 = wct.getHierarchyOps().get(0); + assertThat(op1.getType()).isEqualTo(HIERARCHY_OP_TYPE_REORDER); + assertThat(op1.getContainer()).isEqualTo(task1.token.asBinder()); + final HierarchyOp op2 = wct.getHierarchyOps().get(1); + assertThat(op2.getType()).isEqualTo(HIERARCHY_OP_TYPE_REORDER); + assertThat(op2.getContainer()).isEqualTo(task2.token.asBinder()); + } + + @Test + public void testGetVisibleTaskCount_noTasks_returnsZero() { + assertThat(mController.getVisibleTaskCount()).isEqualTo(0); + } + + @Test + public void testGetVisibleTaskCount_twoTasks_bothVisible_returnsTwo() { + RunningTaskInfo task1 = createFreeformTask(); + mDesktopModeTaskRepository.addActiveTask(task1.taskId); + mDesktopModeTaskRepository.addOrMoveFreeformTaskToTop(task1.taskId); + mDesktopModeTaskRepository.updateVisibleFreeformTasks(task1.taskId, true /* visible */); + + RunningTaskInfo task2 = createFreeformTask(); + mDesktopModeTaskRepository.addActiveTask(task2.taskId); + mDesktopModeTaskRepository.addOrMoveFreeformTaskToTop(task2.taskId); + mDesktopModeTaskRepository.updateVisibleFreeformTasks(task2.taskId, true /* visible */); + + assertThat(mController.getVisibleTaskCount()).isEqualTo(2); + } + + @Test + public void testGetVisibleTaskCount_twoTasks_oneVisible_returnsOne() { + RunningTaskInfo task1 = createFreeformTask(); + mDesktopModeTaskRepository.addActiveTask(task1.taskId); + mDesktopModeTaskRepository.addOrMoveFreeformTaskToTop(task1.taskId); + mDesktopModeTaskRepository.updateVisibleFreeformTasks(task1.taskId, true /* visible */); + + RunningTaskInfo task2 = createFreeformTask(); + mDesktopModeTaskRepository.addActiveTask(task2.taskId); + mDesktopModeTaskRepository.addOrMoveFreeformTaskToTop(task2.taskId); + mDesktopModeTaskRepository.updateVisibleFreeformTasks(task2.taskId, false /* visible */); + + assertThat(mController.getVisibleTaskCount()).isEqualTo(1); + } + + @Test + public void testHandleTransitionRequest_desktopModeNotActive_returnsNull() { + when(DesktopModeStatus.isActive(any())).thenReturn(false); + WindowContainerTransaction wct = mController.handleRequest( + new Binder(), + new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */, null /* remote */)); + assertThat(wct).isNull(); + } + + @Test + public void testHandleTransitionRequest_unsupportedTransit_returnsNull() { + WindowContainerTransaction wct = mController.handleRequest( + new Binder(), + new TransitionRequestInfo(TRANSIT_CLOSE, null /* trigger */, null /* remote */)); + assertThat(wct).isNull(); + } + + @Test + public void testHandleTransitionRequest_notFreeform_returnsNull() { + RunningTaskInfo trigger = new RunningTaskInfo(); + trigger.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN); + WindowContainerTransaction wct = mController.handleRequest( + new Binder(), + new TransitionRequestInfo(TRANSIT_TO_FRONT, trigger, null /* remote */)); + assertThat(wct).isNull(); + } + + @Test + public void testHandleTransitionRequest_taskOpen_returnsWct() { + RunningTaskInfo trigger = new RunningTaskInfo(); + trigger.token = new MockToken().token(); + trigger.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM); + WindowContainerTransaction wct = mController.handleRequest( + mock(IBinder.class), + new TransitionRequestInfo(TRANSIT_OPEN, trigger, null /* remote */)); + assertThat(wct).isNotNull(); + } + + @Test + public void testHandleTransitionRequest_taskToFront_returnsWct() { + RunningTaskInfo trigger = new RunningTaskInfo(); + trigger.token = new MockToken().token(); + trigger.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM); + WindowContainerTransaction wct = mController.handleRequest( + mock(IBinder.class), + new TransitionRequestInfo(TRANSIT_TO_FRONT, trigger, null /* remote */)); + assertThat(wct).isNotNull(); + } + + @Test + public void testHandleTransitionRequest_taskOpen_doesNotStartAnotherTransition() { + RunningTaskInfo trigger = new RunningTaskInfo(); + trigger.token = new MockToken().token(); + trigger.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM); + mController.handleRequest( + mock(IBinder.class), + new TransitionRequestInfo(TRANSIT_OPEN, trigger, null /* remote */)); + verifyZeroInteractions(mTransitions); + } + + private DesktopModeController createController() { + return new DesktopModeController(mContext, mShellInit, mShellController, + mShellTaskOrganizer, mRootTaskDisplayAreaOrganizer, mTransitions, + mDesktopModeTaskRepository, mMockHandler, new TestShellExecutor()); + } + + private DisplayAreaInfo createMockDisplayArea() { + DisplayAreaInfo displayAreaInfo = new DisplayAreaInfo(new MockToken().token(), + mContext.getDisplayId(), 0); + when(mRootTaskDisplayAreaOrganizer.getDisplayAreaInfo(mContext.getDisplayId())) + .thenReturn(displayAreaInfo); + return displayAreaInfo; + } + + private WindowContainerTransaction getDesktopModeSwitchTransaction() { + ArgumentCaptor<WindowContainerTransaction> arg = ArgumentCaptor.forClass( + WindowContainerTransaction.class); + if (Transitions.ENABLE_SHELL_TRANSITIONS) { + verify(mTransitions).startTransition(eq(TRANSIT_CHANGE), arg.capture(), any()); + } else { + verify(mRootTaskDisplayAreaOrganizer).applyTransaction(arg.capture()); + } + return arg.getValue(); + } + + private WindowContainerTransaction getBringAppsToFrontTransaction() { + final ArgumentCaptor<WindowContainerTransaction> arg = ArgumentCaptor.forClass( + WindowContainerTransaction.class); + if (Transitions.ENABLE_SHELL_TRANSITIONS) { + verify(mTransitions).startTransition(eq(TRANSIT_NONE), arg.capture(), any()); + } else { + verify(mShellTaskOrganizer).applyTransaction(arg.capture()); + } + return arg.getValue(); + } + + private void assertThatBoundsCleared(Change change) { + assertThat((change.getWindowSetMask() & WINDOW_CONFIG_BOUNDS) != 0).isTrue(); + assertThat(change.getConfiguration().windowConfiguration.getBounds().isEmpty()).isTrue(); + } + +} 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..45cb3a062cc5 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt @@ -0,0 +1,215 @@ +/* + * 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.android.wm.shell.TestShellExecutor +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.addActiveTaskListener(listener) + + repo.addActiveTask(1) + assertThat(listener.activeTaskChangedCalls).isEqualTo(1) + assertThat(repo.isActiveTask(1)).isTrue() + } + + @Test + fun addActiveTask_sameTaskDoesNotNotify() { + val listener = TestListener() + repo.addActiveTaskListener(listener) + + repo.addActiveTask(1) + repo.addActiveTask(1) + assertThat(listener.activeTaskChangedCalls).isEqualTo(1) + } + + @Test + fun addActiveTask_multipleTasksAddedNotifiesForEach() { + val listener = TestListener() + repo.addActiveTaskListener(listener) + + repo.addActiveTask(1) + repo.addActiveTask(2) + assertThat(listener.activeTaskChangedCalls).isEqualTo(2) + } + + @Test + fun removeActiveTask_listenerNotifiedAndTaskNotActive() { + val listener = TestListener() + repo.addActiveTaskListener(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.addActiveTaskListener(listener) + repo.removeActiveTask(99) + assertThat(listener.activeTaskChangedCalls).isEqualTo(0) + } + + @Test + fun isActiveTask_notExistingTaskReturnsFalse() { + assertThat(repo.isActiveTask(99)).isFalse() + } + + @Test + fun addListener_notifiesVisibleFreeformTask() { + repo.updateVisibleFreeformTasks(1, true) + val listener = TestVisibilityListener() + val executor = TestShellExecutor() + repo.addVisibleTasksListener(listener, executor) + executor.flushAll() + + assertThat(listener.hasVisibleFreeformTasks).isTrue() + assertThat(listener.visibleFreeformTaskChangedCalls).isEqualTo(1) + } + + @Test + fun updateVisibleFreeformTasks_addVisibleTasksNotifiesListener() { + val listener = TestVisibilityListener() + val executor = TestShellExecutor() + repo.addVisibleTasksListener(listener, executor) + repo.updateVisibleFreeformTasks(1, true) + repo.updateVisibleFreeformTasks(2, true) + executor.flushAll() + + assertThat(listener.hasVisibleFreeformTasks).isTrue() + // Equal to 2 because adding the listener notifies the current state + assertThat(listener.visibleFreeformTaskChangedCalls).isEqualTo(2) + } + + @Test + fun updateVisibleFreeformTasks_removeVisibleTasksNotifiesListener() { + val listener = TestVisibilityListener() + val executor = TestShellExecutor() + repo.addVisibleTasksListener(listener, executor) + repo.updateVisibleFreeformTasks(1, true) + repo.updateVisibleFreeformTasks(2, true) + executor.flushAll() + + assertThat(listener.hasVisibleFreeformTasks).isTrue() + repo.updateVisibleFreeformTasks(1, false) + executor.flushAll() + + // Equal to 2 because adding the listener notifies the current state + assertThat(listener.visibleFreeformTaskChangedCalls).isEqualTo(2) + + repo.updateVisibleFreeformTasks(2, false) + executor.flushAll() + + assertThat(listener.hasVisibleFreeformTasks).isFalse() + assertThat(listener.visibleFreeformTaskChangedCalls).isEqualTo(3) + } + + @Test + fun getVisibleTaskCount() { + // No tasks, count is 0 + assertThat(repo.getVisibleTaskCount()).isEqualTo(0) + + // New task increments count to 1 + repo.updateVisibleFreeformTasks(taskId = 1, visible = true) + assertThat(repo.getVisibleTaskCount()).isEqualTo(1) + + // Visibility update to same task does not increase count + repo.updateVisibleFreeformTasks(taskId = 1, visible = true) + assertThat(repo.getVisibleTaskCount()).isEqualTo(1) + + // Second task visible increments count + repo.updateVisibleFreeformTasks(taskId = 2, visible = true) + assertThat(repo.getVisibleTaskCount()).isEqualTo(2) + + // Hiding a task decrements count + repo.updateVisibleFreeformTasks(taskId = 1, visible = false) + assertThat(repo.getVisibleTaskCount()).isEqualTo(1) + + // Hiding all tasks leaves count at 0 + repo.updateVisibleFreeformTasks(taskId = 2, visible = false) + assertThat(repo.getVisibleTaskCount()).isEqualTo(0) + + // Hiding a not existing task, count remains at 0 + repo.updateVisibleFreeformTasks(taskId = 999, visible = false) + assertThat(repo.getVisibleTaskCount()).isEqualTo(0) + } + + @Test + fun addOrMoveFreeformTaskToTop_didNotExist_addsToTop() { + repo.addOrMoveFreeformTaskToTop(5) + repo.addOrMoveFreeformTaskToTop(6) + repo.addOrMoveFreeformTaskToTop(7) + + val tasks = repo.getFreeformTasksInZOrder() + assertThat(tasks.size).isEqualTo(3) + assertThat(tasks[0]).isEqualTo(7) + assertThat(tasks[1]).isEqualTo(6) + assertThat(tasks[2]).isEqualTo(5) + } + + @Test + fun addOrMoveFreeformTaskToTop_alreadyExists_movesToTop() { + repo.addOrMoveFreeformTaskToTop(5) + repo.addOrMoveFreeformTaskToTop(6) + repo.addOrMoveFreeformTaskToTop(7) + + repo.addOrMoveFreeformTaskToTop(6) + + val tasks = repo.getFreeformTasksInZOrder() + assertThat(tasks.size).isEqualTo(3) + assertThat(tasks.first()).isEqualTo(6) + } + + class TestListener : DesktopModeTaskRepository.ActiveTasksListener { + var activeTaskChangedCalls = 0 + override fun onActiveTasksChanged() { + activeTaskChangedCalls++ + } + } + + class TestVisibilityListener : DesktopModeTaskRepository.VisibleTasksListener { + var hasVisibleFreeformTasks = false + var visibleFreeformTaskChangedCalls = 0 + + override fun onVisibilityChanged(hasVisibleTasks: Boolean) { + hasVisibleFreeformTasks = hasVisibleTasks + visibleFreeformTaskChangedCalls++ + } + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt new file mode 100644 index 000000000000..c9bd695ffb33 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt @@ -0,0 +1,475 @@ +/* + * 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.app.ActivityManager.RunningTaskInfo +import android.app.WindowConfiguration.ACTIVITY_TYPE_HOME +import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD +import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM +import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN +import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW +import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED +import android.os.Binder +import android.testing.AndroidTestingRunner +import android.view.WindowManager +import android.view.WindowManager.TRANSIT_CHANGE +import android.view.WindowManager.TRANSIT_NONE +import android.view.WindowManager.TRANSIT_OPEN +import android.view.WindowManager.TRANSIT_TO_FRONT +import android.window.TransitionRequestInfo +import android.window.WindowContainerTransaction +import android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REORDER +import androidx.test.filters.SmallTest +import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession +import com.android.dx.mockito.inline.extended.ExtendedMockito.never +import com.android.dx.mockito.inline.extended.StaticMockitoSession +import com.android.wm.shell.RootTaskDisplayAreaOrganizer +import com.android.wm.shell.ShellTaskOrganizer +import com.android.wm.shell.ShellTestCase +import com.android.wm.shell.TestRunningTaskInfoBuilder +import com.android.wm.shell.TestShellExecutor +import com.android.wm.shell.common.DisplayController +import com.android.wm.shell.common.ShellExecutor +import com.android.wm.shell.common.SyncTransactionQueue +import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createFreeformTask +import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createFullscreenTask +import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createHomeTask +import com.android.wm.shell.sysui.ShellController +import com.android.wm.shell.sysui.ShellInit +import com.android.wm.shell.transition.Transitions +import com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS +import com.google.common.truth.Truth.assertThat +import com.google.common.truth.Truth.assertWithMessage +import org.junit.After +import org.junit.Assume.assumeTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.ArgumentMatchers.eq +import org.mockito.ArgumentMatchers.isNull +import org.mockito.Mock +import org.mockito.Mockito +import org.mockito.Mockito.any +import org.mockito.Mockito.anyInt +import org.mockito.Mockito.clearInvocations +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` as whenever + +@SmallTest +@RunWith(AndroidTestingRunner::class) +class DesktopTasksControllerTest : ShellTestCase() { + + @Mock lateinit var testExecutor: ShellExecutor + @Mock lateinit var shellController: ShellController + @Mock lateinit var displayController: DisplayController + @Mock lateinit var shellTaskOrganizer: ShellTaskOrganizer + @Mock lateinit var syncQueue: SyncTransactionQueue + @Mock lateinit var rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer + @Mock lateinit var transitions: Transitions + @Mock lateinit var exitDesktopTransitionHandler: ExitDesktopTaskTransitionHandler + @Mock lateinit var enterDesktopTransitionHandler: EnterDesktopTaskTransitionHandler + + lateinit var mockitoSession: StaticMockitoSession + lateinit var controller: DesktopTasksController + lateinit var shellInit: ShellInit + lateinit var desktopModeTaskRepository: DesktopModeTaskRepository + + // Mock running tasks are registered here so we can get the list from mock shell task organizer + private val runningTasks = mutableListOf<RunningTaskInfo>() + + @Before + fun setUp() { + mockitoSession = mockitoSession().mockStatic(DesktopModeStatus::class.java).startMocking() + whenever(DesktopModeStatus.isProto2Enabled()).thenReturn(true) + + shellInit = Mockito.spy(ShellInit(testExecutor)) + desktopModeTaskRepository = DesktopModeTaskRepository() + + whenever(shellTaskOrganizer.getRunningTasks(anyInt())).thenAnswer { runningTasks } + whenever(transitions.startTransition(anyInt(), any(), isNull())).thenAnswer { Binder() } + + controller = createController() + + shellInit.init() + } + + private fun createController(): DesktopTasksController { + return DesktopTasksController( + context, + shellInit, + shellController, + displayController, + shellTaskOrganizer, + syncQueue, + rootTaskDisplayAreaOrganizer, + transitions, + enterDesktopTransitionHandler, + exitDesktopTransitionHandler, + desktopModeTaskRepository, + TestShellExecutor() + ) + } + + @After + fun tearDown() { + mockitoSession.finishMocking() + + runningTasks.clear() + } + + @Test + fun instantiate_addInitCallback() { + verify(shellInit).addInitCallback(any(), any<DesktopTasksController>()) + } + + @Test + fun instantiate_flagOff_doNotAddInitCallback() { + whenever(DesktopModeStatus.isProto2Enabled()).thenReturn(false) + clearInvocations(shellInit) + + createController() + + verify(shellInit, never()).addInitCallback(any(), any<DesktopTasksController>()) + } + + @Test + fun showDesktopApps_allAppsInvisible_bringsToFront() { + val homeTask = setUpHomeTask() + val task1 = setUpFreeformTask() + val task2 = setUpFreeformTask() + markTaskHidden(task1) + markTaskHidden(task2) + + controller.showDesktopApps() + + val wct = getLatestWct(expectTransition = TRANSIT_NONE) + assertThat(wct.hierarchyOps).hasSize(3) + // Expect order to be from bottom: home, task1, task2 + wct.assertReorderAt(index = 0, homeTask) + wct.assertReorderAt(index = 1, task1) + wct.assertReorderAt(index = 2, task2) + } + + @Test + fun showDesktopApps_appsAlreadyVisible_bringsToFront() { + val homeTask = setUpHomeTask() + val task1 = setUpFreeformTask() + val task2 = setUpFreeformTask() + markTaskVisible(task1) + markTaskVisible(task2) + + controller.showDesktopApps() + + val wct = getLatestWct(expectTransition = TRANSIT_NONE) + assertThat(wct.hierarchyOps).hasSize(3) + // Expect order to be from bottom: home, task1, task2 + wct.assertReorderAt(index = 0, homeTask) + wct.assertReorderAt(index = 1, task1) + wct.assertReorderAt(index = 2, task2) + } + + @Test + fun showDesktopApps_someAppsInvisible_reordersAll() { + val homeTask = setUpHomeTask() + val task1 = setUpFreeformTask() + val task2 = setUpFreeformTask() + markTaskHidden(task1) + markTaskVisible(task2) + + controller.showDesktopApps() + + val wct = getLatestWct(expectTransition = TRANSIT_NONE) + assertThat(wct.hierarchyOps).hasSize(3) + // Expect order to be from bottom: home, task1, task2 + wct.assertReorderAt(index = 0, homeTask) + wct.assertReorderAt(index = 1, task1) + wct.assertReorderAt(index = 2, task2) + } + + @Test + fun showDesktopApps_noActiveTasks_reorderHomeToTop() { + val homeTask = setUpHomeTask() + + controller.showDesktopApps() + + val wct = getLatestWct(expectTransition = TRANSIT_NONE) + assertThat(wct.hierarchyOps).hasSize(1) + wct.assertReorderAt(index = 0, homeTask) + } + + @Test + fun getVisibleTaskCount_noTasks_returnsZero() { + assertThat(controller.getVisibleTaskCount()).isEqualTo(0) + } + + @Test + fun getVisibleTaskCount_twoTasks_bothVisible_returnsTwo() { + setUpHomeTask() + setUpFreeformTask().also(::markTaskVisible) + setUpFreeformTask().also(::markTaskVisible) + assertThat(controller.getVisibleTaskCount()).isEqualTo(2) + } + + @Test + fun getVisibleTaskCount_twoTasks_oneVisible_returnsOne() { + setUpHomeTask() + setUpFreeformTask().also(::markTaskVisible) + setUpFreeformTask().also(::markTaskHidden) + assertThat(controller.getVisibleTaskCount()).isEqualTo(1) + } + + @Test + fun moveToDesktop() { + val task = setUpFullscreenTask() + controller.moveToDesktop(task) + val wct = getLatestWct(expectTransition = TRANSIT_CHANGE) + assertThat(wct.changes[task.token.asBinder()]?.windowingMode) + .isEqualTo(WINDOWING_MODE_FREEFORM) + } + + @Test + fun moveToDesktop_nonExistentTask_doesNothing() { + controller.moveToDesktop(999) + verifyWCTNotExecuted() + } + + @Test + fun moveToDesktop_otherFreeformTasksBroughtToFront() { + val homeTask = setUpHomeTask() + val freeformTask = setUpFreeformTask() + val fullscreenTask = setUpFullscreenTask() + markTaskHidden(freeformTask) + + controller.moveToDesktop(fullscreenTask) + + with(getLatestWct(expectTransition = TRANSIT_CHANGE)) { + assertThat(hierarchyOps).hasSize(3) + assertReorderSequence(homeTask, freeformTask, fullscreenTask) + assertThat(changes[fullscreenTask.token.asBinder()]?.windowingMode) + .isEqualTo(WINDOWING_MODE_FREEFORM) + } + } + + @Test + fun moveToFullscreen() { + val task = setUpFreeformTask() + controller.moveToFullscreen(task) + val wct = getLatestWct(expectTransition = TRANSIT_CHANGE) + assertThat(wct.changes[task.token.asBinder()]?.windowingMode) + .isEqualTo(WINDOWING_MODE_FULLSCREEN) + } + + @Test + fun moveToFullscreen_nonExistentTask_doesNothing() { + controller.moveToFullscreen(999) + verifyWCTNotExecuted() + } + + @Test + fun getTaskWindowingMode() { + val fullscreenTask = setUpFullscreenTask() + val freeformTask = setUpFreeformTask() + + assertThat(controller.getTaskWindowingMode(fullscreenTask.taskId)) + .isEqualTo(WINDOWING_MODE_FULLSCREEN) + assertThat(controller.getTaskWindowingMode(freeformTask.taskId)) + .isEqualTo(WINDOWING_MODE_FREEFORM) + assertThat(controller.getTaskWindowingMode(999)).isEqualTo(WINDOWING_MODE_UNDEFINED) + } + + @Test + fun handleRequest_fullscreenTask_freeformVisible_returnSwitchToFreeformWCT() { + assumeTrue(ENABLE_SHELL_TRANSITIONS) + + val freeformTask = setUpFreeformTask() + markTaskVisible(freeformTask) + val fullscreenTask = createFullscreenTask() + + val result = controller.handleRequest(Binder(), createTransition(fullscreenTask)) + assertThat(result?.changes?.get(fullscreenTask.token.asBinder())?.windowingMode) + .isEqualTo(WINDOWING_MODE_FREEFORM) + } + + @Test + fun handleRequest_fullscreenTask_freeformNotVisible_returnNull() { + assumeTrue(ENABLE_SHELL_TRANSITIONS) + + val freeformTask = setUpFreeformTask() + markTaskHidden(freeformTask) + val fullscreenTask = createFullscreenTask() + assertThat(controller.handleRequest(Binder(), createTransition(fullscreenTask))).isNull() + } + + @Test + fun handleRequest_fullscreenTask_noOtherTasks_returnNull() { + assumeTrue(ENABLE_SHELL_TRANSITIONS) + + val fullscreenTask = createFullscreenTask() + assertThat(controller.handleRequest(Binder(), createTransition(fullscreenTask))).isNull() + } + + @Test + fun handleRequest_freeformTask_freeformVisible_returnNull() { + assumeTrue(ENABLE_SHELL_TRANSITIONS) + + val freeformTask1 = setUpFreeformTask() + markTaskVisible(freeformTask1) + + val freeformTask2 = createFreeformTask() + assertThat(controller.handleRequest(Binder(), createTransition(freeformTask2))).isNull() + } + + @Test + fun handleRequest_freeformTask_freeformNotVisible_returnSwitchToFullscreenWCT() { + assumeTrue(ENABLE_SHELL_TRANSITIONS) + + val freeformTask1 = setUpFreeformTask() + markTaskHidden(freeformTask1) + + val freeformTask2 = createFreeformTask() + val result = + controller.handleRequest( + Binder(), + createTransition(freeformTask2, type = TRANSIT_TO_FRONT) + ) + assertThat(result?.changes?.get(freeformTask2.token.asBinder())?.windowingMode) + .isEqualTo(WINDOWING_MODE_FULLSCREEN) + } + + @Test + fun handleRequest_freeformTask_noOtherTasks_returnSwitchToFullscreenWCT() { + assumeTrue(ENABLE_SHELL_TRANSITIONS) + + val task = createFreeformTask() + val result = controller.handleRequest(Binder(), createTransition(task)) + assertThat(result?.changes?.get(task.token.asBinder())?.windowingMode) + .isEqualTo(WINDOWING_MODE_FULLSCREEN) + } + + @Test + fun handleRequest_notOpenOrToFrontTransition_returnNull() { + assumeTrue(ENABLE_SHELL_TRANSITIONS) + + val task = + TestRunningTaskInfoBuilder() + .setActivityType(ACTIVITY_TYPE_STANDARD) + .setWindowingMode(WINDOWING_MODE_FULLSCREEN) + .build() + val transition = createTransition(task = task, type = WindowManager.TRANSIT_CLOSE) + val result = controller.handleRequest(Binder(), transition) + assertThat(result).isNull() + } + + @Test + fun handleRequest_noTriggerTask_returnNull() { + assumeTrue(ENABLE_SHELL_TRANSITIONS) + assertThat(controller.handleRequest(Binder(), createTransition(task = null))).isNull() + } + + @Test + fun handleRequest_triggerTaskNotStandard_returnNull() { + assumeTrue(ENABLE_SHELL_TRANSITIONS) + val task = TestRunningTaskInfoBuilder().setActivityType(ACTIVITY_TYPE_HOME).build() + assertThat(controller.handleRequest(Binder(), createTransition(task))).isNull() + } + + @Test + fun handleRequest_triggerTaskNotFullscreenOrFreeform_returnNull() { + assumeTrue(ENABLE_SHELL_TRANSITIONS) + + val task = + TestRunningTaskInfoBuilder() + .setActivityType(ACTIVITY_TYPE_STANDARD) + .setWindowingMode(WINDOWING_MODE_MULTI_WINDOW) + .build() + assertThat(controller.handleRequest(Binder(), createTransition(task))).isNull() + } + + private fun setUpFreeformTask(): RunningTaskInfo { + val task = createFreeformTask() + whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task) + desktopModeTaskRepository.addActiveTask(task.taskId) + desktopModeTaskRepository.addOrMoveFreeformTaskToTop(task.taskId) + runningTasks.add(task) + return task + } + + private fun setUpHomeTask(): RunningTaskInfo { + val task = createHomeTask() + whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task) + runningTasks.add(task) + return task + } + + private fun setUpFullscreenTask(): RunningTaskInfo { + val task = createFullscreenTask() + whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task) + runningTasks.add(task) + return task + } + + private fun markTaskVisible(task: RunningTaskInfo) { + desktopModeTaskRepository.updateVisibleFreeformTasks(task.taskId, visible = true) + } + + private fun markTaskHidden(task: RunningTaskInfo) { + desktopModeTaskRepository.updateVisibleFreeformTasks(task.taskId, visible = false) + } + + private fun getLatestWct( + @WindowManager.TransitionType expectTransition: Int = TRANSIT_OPEN + ): WindowContainerTransaction { + val arg = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + if (ENABLE_SHELL_TRANSITIONS) { + verify(transitions).startTransition(eq(expectTransition), arg.capture(), isNull()) + } else { + verify(shellTaskOrganizer).applyTransaction(arg.capture()) + } + return arg.value + } + + private fun verifyWCTNotExecuted() { + if (ENABLE_SHELL_TRANSITIONS) { + verify(transitions, never()).startTransition(anyInt(), any(), isNull()) + } else { + verify(shellTaskOrganizer, never()).applyTransaction(any()) + } + } + + private fun createTransition( + task: RunningTaskInfo?, + @WindowManager.TransitionType type: Int = TRANSIT_OPEN + ): TransitionRequestInfo { + return TransitionRequestInfo(type, task, null /* remoteTransition */) + } +} + +private fun WindowContainerTransaction.assertReorderAt(index: Int, task: RunningTaskInfo) { + assertWithMessage("WCT does not have a hierarchy operation at index $index") + .that(hierarchyOps.size) + .isGreaterThan(index) + val op = hierarchyOps[index] + assertThat(op.type).isEqualTo(HIERARCHY_OP_TYPE_REORDER) + assertThat(op.container).isEqualTo(task.token.asBinder()) +} + +private fun WindowContainerTransaction.assertReorderSequence(vararg tasks: RunningTaskInfo) { + for (i in tasks.indices) { + assertReorderAt(i, tasks[i]) + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTestHelpers.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTestHelpers.kt new file mode 100644 index 000000000000..dc91d756842e --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTestHelpers.kt @@ -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.desktopmode + +import android.app.ActivityManager.RunningTaskInfo +import android.app.WindowConfiguration.ACTIVITY_TYPE_HOME +import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD +import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM +import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN +import com.android.wm.shell.TestRunningTaskInfoBuilder + +class DesktopTestHelpers { + companion object { + /** Create a task that has windowing mode set to [WINDOWING_MODE_FREEFORM] */ + @JvmStatic + fun createFreeformTask(): RunningTaskInfo { + return TestRunningTaskInfoBuilder() + .setToken(MockToken().token()) + .setActivityType(ACTIVITY_TYPE_STANDARD) + .setWindowingMode(WINDOWING_MODE_FREEFORM) + .setLastActiveTime(100) + .build() + } + + /** Create a task that has windowing mode set to [WINDOWING_MODE_FULLSCREEN] */ + @JvmStatic + fun createFullscreenTask(): RunningTaskInfo { + return TestRunningTaskInfoBuilder() + .setToken(MockToken().token()) + .setActivityType(ACTIVITY_TYPE_STANDARD) + .setWindowingMode(WINDOWING_MODE_FULLSCREEN) + .setLastActiveTime(100) + .build() + } + + /** Create a new home task */ + @JvmStatic + fun createHomeTask(): RunningTaskInfo { + return TestRunningTaskInfoBuilder() + .setToken(MockToken().token()) + .setActivityType(ACTIVITY_TYPE_HOME) + .setWindowingMode(WINDOWING_MODE_FULLSCREEN) + .setLastActiveTime(100) + .build() + } + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandlerTest.java new file mode 100644 index 000000000000..6199e0b05059 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandlerTest.java @@ -0,0 +1,159 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.desktopmode; + +import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; + +import static androidx.test.internal.runner.junit4.statement.UiThreadStatement.runOnUiThread; + +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import android.annotation.NonNull; +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.IWindowContainerToken; +import android.window.TransitionInfo; +import android.window.WindowContainerToken; +import android.window.WindowContainerTransaction; + +import androidx.test.filters.SmallTest; + +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.transition.Transitions; + +import junit.framework.AssertionFailedError; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.function.Supplier; + +/** Tests of {@link com.android.wm.shell.desktopmode.EnterDesktopTaskTransitionHandler} */ +@SmallTest +public class EnterDesktopTaskTransitionHandlerTest { + + @Mock + private Transitions mTransitions; + @Mock + IBinder mToken; + @Mock + Supplier<SurfaceControl.Transaction> mTransactionFactory; + @Mock + SurfaceControl.Transaction mStartT; + @Mock + SurfaceControl.Transaction mFinishT; + @Mock + SurfaceControl.Transaction mAnimationT; + @Mock + Transitions.TransitionFinishCallback mTransitionFinishCallback; + @Mock + ShellExecutor mExecutor; + @Mock + SurfaceControl mSurfaceControl; + + private EnterDesktopTaskTransitionHandler mEnterDesktopTaskTransitionHandler; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + doReturn(mExecutor).when(mTransitions).getMainExecutor(); + doReturn(mAnimationT).when(mTransactionFactory).get(); + + mEnterDesktopTaskTransitionHandler = new EnterDesktopTaskTransitionHandler(mTransitions, + mTransactionFactory); + } + + @Test + public void testEnterFreeformAnimation() { + final int transitionType = Transitions.TRANSIT_ENTER_FREEFORM; + final int taskId = 1; + WindowContainerTransaction wct = new WindowContainerTransaction(); + doReturn(mToken).when(mTransitions) + .startTransition(transitionType, wct, mEnterDesktopTaskTransitionHandler); + mEnterDesktopTaskTransitionHandler.startTransition(transitionType, wct); + + TransitionInfo.Change change = + createChange(WindowManager.TRANSIT_CHANGE, taskId, WINDOWING_MODE_FREEFORM); + TransitionInfo info = createTransitionInfo(Transitions.TRANSIT_ENTER_FREEFORM, change); + + + assertTrue(mEnterDesktopTaskTransitionHandler + .startAnimation(mToken, info, mStartT, mFinishT, mTransitionFinishCallback)); + + verify(mStartT).setWindowCrop(mSurfaceControl, null); + verify(mStartT).apply(); + } + + @Test + public void testTransitEnterDesktopModeAnimation() throws Throwable { + final int transitionType = Transitions.TRANSIT_ENTER_DESKTOP_MODE; + final int taskId = 1; + WindowContainerTransaction wct = new WindowContainerTransaction(); + doReturn(mToken).when(mTransitions) + .startTransition(transitionType, wct, mEnterDesktopTaskTransitionHandler); + mEnterDesktopTaskTransitionHandler.startTransition(transitionType, wct); + + TransitionInfo.Change change = + createChange(WindowManager.TRANSIT_CHANGE, taskId, WINDOWING_MODE_FREEFORM); + change.setEndAbsBounds(new Rect(0, 0, 1, 1)); + TransitionInfo info = createTransitionInfo(Transitions.TRANSIT_ENTER_DESKTOP_MODE, change); + + runOnUiThread(() -> { + try { + assertTrue(mEnterDesktopTaskTransitionHandler + .startAnimation(mToken, info, mStartT, mFinishT, + mTransitionFinishCallback)); + } catch (Exception e) { + throw new AssertionFailedError(e.getMessage()); + } + }); + + verify(mStartT).setWindowCrop(mSurfaceControl, change.getEndAbsBounds().width(), + change.getEndAbsBounds().height()); + verify(mStartT).apply(); + } + + private TransitionInfo.Change createChange(@WindowManager.TransitionType int type, int taskId, + @WindowConfiguration.WindowingMode 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)), mSurfaceControl); + change.setMode(type); + change.setTaskInfo(taskInfo); + return change; + } + + private static TransitionInfo createTransitionInfo( + @WindowManager.TransitionType int type, @NonNull TransitionInfo.Change change) { + TransitionInfo info = new TransitionInfo(type, 0); + info.addChange(change); + return info; + } + +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandlerTest.java new file mode 100644 index 000000000000..2c5a5cd72c53 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandlerTest.java @@ -0,0 +1,151 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.desktopmode; + + +import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; + +import static androidx.test.internal.runner.junit4.statement.UiThreadStatement.runOnUiThread; + +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import android.annotation.NonNull; +import android.app.ActivityManager; +import android.app.WindowConfiguration; +import android.content.Context; +import android.content.res.Resources; +import android.os.IBinder; +import android.util.DisplayMetrics; +import android.view.SurfaceControl; +import android.view.WindowManager; +import android.window.IWindowContainerToken; +import android.window.TransitionInfo; +import android.window.WindowContainerToken; +import android.window.WindowContainerTransaction; + +import androidx.test.filters.SmallTest; + +import com.android.wm.shell.ShellTestCase; +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.transition.Transitions; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.ArrayList; +import java.util.function.Supplier; + +/** Tests of {@link com.android.wm.shell.desktopmode.ExitDesktopTaskTransitionHandler} */ +@SmallTest +public class ExitDesktopTaskTransitionHandlerTest extends ShellTestCase { + + @Mock + private Transitions mTransitions; + @Mock + IBinder mToken; + @Mock + Supplier<SurfaceControl.Transaction> mTransactionFactory; + @Mock + Context mContext; + @Mock + DisplayMetrics mDisplayMetrics; + @Mock + Resources mResources; + @Mock + SurfaceControl.Transaction mStartT; + @Mock + SurfaceControl.Transaction mFinishT; + @Mock + SurfaceControl.Transaction mAnimationT; + @Mock + Transitions.TransitionFinishCallback mTransitionFinishCallback; + @Mock + ShellExecutor mExecutor; + + private ExitDesktopTaskTransitionHandler mExitDesktopTaskTransitionHandler; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + doReturn(mExecutor).when(mTransitions).getMainExecutor(); + doReturn(mAnimationT).when(mTransactionFactory).get(); + doReturn(mResources).when(mContext).getResources(); + doReturn(mDisplayMetrics).when(mResources).getDisplayMetrics(); + when(mResources.getDisplayMetrics()) + .thenReturn(getContext().getResources().getDisplayMetrics()); + + mExitDesktopTaskTransitionHandler = new ExitDesktopTaskTransitionHandler(mTransitions, + mContext); + } + + @Test + public void testTransitExitDesktopModeAnimation() throws Throwable { + final int transitionType = Transitions.TRANSIT_EXIT_DESKTOP_MODE; + final int taskId = 1; + WindowContainerTransaction wct = new WindowContainerTransaction(); + doReturn(mToken).when(mTransitions) + .startTransition(transitionType, wct, mExitDesktopTaskTransitionHandler); + + mExitDesktopTaskTransitionHandler.startTransition(transitionType, wct); + + TransitionInfo.Change change = + createChange(WindowManager.TRANSIT_CHANGE, taskId, WINDOWING_MODE_FULLSCREEN); + TransitionInfo info = createTransitionInfo(Transitions.TRANSIT_EXIT_DESKTOP_MODE, change); + ArrayList<Exception> exceptions = new ArrayList<>(); + runOnUiThread(() -> { + try { + assertTrue(mExitDesktopTaskTransitionHandler + .startAnimation(mToken, info, mStartT, mFinishT, + mTransitionFinishCallback)); + } catch (Exception e) { + exceptions.add(e); + } + }); + if (!exceptions.isEmpty()) { + throw exceptions.get(0); + } + } + + private TransitionInfo.Change createChange(@WindowManager.TransitionType int type, int taskId, + @WindowConfiguration.WindowingMode int windowingMode) { + final ActivityManager.RunningTaskInfo taskInfo = new ActivityManager.RunningTaskInfo(); + taskInfo.taskId = taskId; + taskInfo.token = new WindowContainerToken(mock(IWindowContainerToken.class)); + taskInfo.configuration.windowConfiguration.setWindowingMode(windowingMode); + SurfaceControl.Builder b = new SurfaceControl.Builder() + .setName("test task"); + final TransitionInfo.Change change = new TransitionInfo.Change( + taskInfo.token, b.build()); + change.setMode(type); + change.setTaskInfo(taskInfo); + return change; + } + + private static TransitionInfo createTransitionInfo( + @WindowManager.TransitionType int type, @NonNull TransitionInfo.Change change) { + TransitionInfo info = new TransitionInfo(type, 0); + info.addChange(change); + return info; + } + +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/MockToken.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/MockToken.java new file mode 100644 index 000000000000..09d474d1f97c --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/MockToken.java @@ -0,0 +1,40 @@ +/* + * 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 org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import android.os.IBinder; +import android.window.WindowContainerToken; + +/** + * {@link WindowContainerToken} wrapper that supports a mock binder + */ +class MockToken { + private final WindowContainerToken mToken; + + MockToken() { + mToken = mock(WindowContainerToken.class); + IBinder binder = mock(IBinder.class); + when(mToken.asBinder()).thenReturn(binder); + } + + WindowContainerToken token() { + return mToken; + } +} 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..523cb6629d9a 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,11 @@ 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 +57,45 @@ 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 WindowManager mWindowManager; 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 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..69f664a3a89d --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserverTest.java @@ -0,0 +1,238 @@ +/* + * 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.view.WindowManager.TRANSIT_CLOSE; +import static android.view.WindowManager.TRANSIT_OPEN; + +import static org.mockito.Mockito.doReturn; +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.TransitionInfoBuilder; +import com.android.wm.shell.sysui.ShellInit; +import com.android.wm.shell.transition.Transitions; +import com.android.wm.shell.windowdecor.WindowDecorViewModel; + +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 WindowDecorViewModel mWindowDecorViewModel; + + 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, mWindowDecorViewModel); + 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 TransitionInfoBuilder(TRANSIT_OPEN, 0) + .addChange(change).build(); + + 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(mWindowDecorViewModel).onTaskOpening( + change.getTaskInfo(), change.getLeash(), startT, finishT); + } + + @Test + public void testPreparesWindowDecorOnCloseTransition_freeform() { + final TransitionInfo.Change change = + createChange(TRANSIT_CLOSE, 1, WINDOWING_MODE_FREEFORM); + final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_CLOSE, 0) + .addChange(change).build(); + + 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(mWindowDecorViewModel).onTaskClosing( + 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 TransitionInfoBuilder(TRANSIT_CLOSE, 0) + .addChange(change).build(); + + 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(mWindowDecorViewModel, never()).destroyWindowDecoration(change.getTaskInfo()); + } + + @Test + public void testClosesWindowDecorAfterCloseTransition() throws Exception { + final TransitionInfo.Change change = + createChange(TRANSIT_CLOSE, 1, WINDOWING_MODE_FREEFORM); + final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_CLOSE, 0) + .addChange(change).build(); + + final AutoCloseable windowDecor = mock(AutoCloseable.class); + + 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(mWindowDecorViewModel).destroyWindowDecoration(change.getTaskInfo()); + } + + @Test + public void testClosesMergedWindowDecorationAfterTransitionFinishes() throws Exception { + // The playing transition + final TransitionInfo.Change change1 = + createChange(TRANSIT_OPEN, 1, WINDOWING_MODE_FREEFORM); + final TransitionInfo info1 = new TransitionInfoBuilder(TRANSIT_OPEN, 0) + .addChange(change1).build(); + + 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 TransitionInfoBuilder(TRANSIT_CLOSE, 0) + .addChange(change2).build(); + + 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(mWindowDecorViewModel).destroyWindowDecoration(change2.getTaskInfo()); + } + + @Test + public void testClosesAllWindowDecorsOnTransitionMergeAfterCloseTransitions() throws Exception { + // The playing transition + final TransitionInfo.Change change1 = + createChange(TRANSIT_CLOSE, 1, WINDOWING_MODE_FREEFORM); + final TransitionInfo info1 = new TransitionInfoBuilder(TRANSIT_CLOSE, 0) + .addChange(change1).build(); + + 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 TransitionInfoBuilder(TRANSIT_CLOSE, 0) + .addChange(change2).build(); + + 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(mWindowDecorViewModel).destroyWindowDecoration(change1.getTaskInfo()); + verify(mWindowDecorViewModel).destroyWindowDecoration(change2.getTaskInfo()); + } + + 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..58e91cb50c7a 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 @@ -31,6 +31,7 @@ import static org.mockito.Mockito.verify; import android.app.ActivityManager; import android.content.Context; import android.content.pm.ParceledListSlice; +import android.content.res.Resources; import android.os.Handler; import android.os.IBinder; import android.os.RemoteException; @@ -44,11 +45,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 +64,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,8 +75,10 @@ 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; + @Mock private Resources mResources; KidsModeTaskOrganizer mOrganizer; @@ -86,15 +91,22 @@ 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); + doReturn(mResources).when(mContext).getResources(); + final KidsModeTaskOrganizer kidsModeTaskOrganizer = new KidsModeTaskOrganizer(mContext, + mShellInit, mShellCommandHandler, mTaskOrganizerController, mSyncTransactionQueue, + mDisplayController, mDisplayInsetsController, Optional.empty(), Optional.empty(), + mObserver, mTestExecutor, mHandler); + mOrganizer = spy(kidsModeTaskOrganizer); 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(); @@ -104,6 +116,8 @@ public class KidsModeTaskOrganizerTest { verify(mOrganizer, times(1)).registerOrganizer(); verify(mOrganizer, times(1)).createRootTask( eq(DEFAULT_DISPLAY), eq(WINDOWING_MODE_FULLSCREEN), eq(mOrganizer.mCookie)); + verify(mOrganizer, times(1)) + .setOrientationRequestPolicy(eq(true), any(), any()); final ActivityManager.RunningTaskInfo rootTask = createTaskInfo(12, WINDOWING_MODE_FULLSCREEN, mOrganizer.mCookie); @@ -124,10 +138,11 @@ public class KidsModeTaskOrganizerTest { doReturn(false).when(mObserver).isEnabled(); mOrganizer.updateKidsModeState(); - verify(mOrganizer, times(1)).disable(); verify(mOrganizer, times(1)).unregisterOrganizer(); verify(mOrganizer, times(1)).deleteRootTask(rootTask.token); + verify(mOrganizer, times(1)) + .setOrientationRequestPolicy(eq(false), any(), any()); assertThat(mOrganizer.mLaunchRootLeash).isNull(); assertThat(mOrganizer.mLaunchRootTask).isNull(); } 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..8ad3d2a72617 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,28 @@ 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 com.android.wm.shell.sysui.ShellSharedConstants; import org.junit.Before; import org.junit.Test; @@ -62,16 +63,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 +92,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 +106,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 +126,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 +143,43 @@ 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 + public void testControllerRegisteresExternalInterface() { + verify(mMockShellController, times(1)).addExternalInterface( + eq(ShellSharedConstants.KEY_EXTRA_SHELL_ONE_HANDED), any(), any()); } @Test @@ -304,9 +337,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 +349,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 +362,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 +378,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 +391,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 +404,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 +417,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..addc2338144f 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 @@ -32,6 +32,7 @@ import androidx.test.filters.SmallTest; import com.android.wm.shell.R; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.common.DisplayLayout; +import com.android.wm.shell.pip.phone.PipSizeSpecHandler; import org.junit.Before; import org.junit.Test; @@ -54,20 +55,28 @@ public class PipBoundsAlgorithmTest extends ShellTestCase { private static final float MAX_ASPECT_RATIO = 2f; private static final int DEFAULT_MIN_EDGE_SIZE = 100; + /** The minimum possible size of the override min size's width or height */ + private static final int OVERRIDABLE_MIN_SIZE = 40; + private PipBoundsAlgorithm mPipBoundsAlgorithm; private DisplayInfo mDefaultDisplayInfo; - private PipBoundsState mPipBoundsState; + private PipBoundsState mPipBoundsState; private PipSizeSpecHandler mPipSizeSpecHandler; + private PipDisplayLayoutState mPipDisplayLayoutState; @Before public void setUp() throws Exception { initializeMockResources(); - mPipBoundsState = new PipBoundsState(mContext); + mPipDisplayLayoutState = new PipDisplayLayoutState(mContext); + mPipSizeSpecHandler = new PipSizeSpecHandler(mContext, mPipDisplayLayoutState); + mPipBoundsState = new PipBoundsState(mContext, mPipSizeSpecHandler, mPipDisplayLayoutState); mPipBoundsAlgorithm = new PipBoundsAlgorithm(mContext, mPipBoundsState, - new PipSnapAlgorithm()); + new PipSnapAlgorithm(), new PipKeepClearAlgorithmInterface() {}, + mPipSizeSpecHandler); - mPipBoundsState.setDisplayLayout( - new DisplayLayout(mDefaultDisplayInfo, mContext.getResources(), true, true)); + DisplayLayout layout = + new DisplayLayout(mDefaultDisplayInfo, mContext.getResources(), true, true); + mPipDisplayLayoutState.setDisplayLayout(layout); } private void initializeMockResources() { @@ -82,6 +91,9 @@ public class PipBoundsAlgorithmTest extends ShellTestCase { R.dimen.default_minimal_size_pip_resizable_task, DEFAULT_MIN_EDGE_SIZE); res.addOverride( + R.dimen.overridable_minimal_size_pip_resizable_task, + OVERRIDABLE_MIN_SIZE); + res.addOverride( R.string.config_defaultPictureInPictureScreenEdgeInsets, "16x16"); res.addOverride( @@ -120,9 +132,7 @@ public class PipBoundsAlgorithmTest extends ShellTestCase { @Test public void getDefaultBounds_noOverrideMinSize_matchesDefaultSizeAndAspectRatio() { - final Size defaultSize = mPipBoundsAlgorithm.getSizeForAspectRatio(DEFAULT_ASPECT_RATIO, - DEFAULT_MIN_EDGE_SIZE, mDefaultDisplayInfo.logicalWidth, - mDefaultDisplayInfo.logicalHeight); + final Size defaultSize = mPipSizeSpecHandler.getDefaultSize(DEFAULT_ASPECT_RATIO); mPipBoundsState.setOverrideMinSize(null); final Rect defaultBounds = mPipBoundsAlgorithm.getDefaultBounds(); @@ -296,9 +306,9 @@ public class PipBoundsAlgorithmTest extends ShellTestCase { (MAX_ASPECT_RATIO + DEFAULT_ASPECT_RATIO) / 2 }; final Size[] minimalSizes = new Size[] { - new Size((int) (100 * aspectRatios[0]), 100), - new Size((int) (100 * aspectRatios[1]), 100), - new Size((int) (100 * aspectRatios[2]), 100) + new Size((int) (200 * aspectRatios[0]), 200), + new Size((int) (200 * aspectRatios[1]), 200), + new Size((int) (200 * aspectRatios[2]), 200) }; for (int i = 0; i < aspectRatios.length; i++) { final float aspectRatio = aspectRatios[i]; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsStateTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsStateTest.java index 8e30f65cee78..f32000445ca9 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsStateTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsStateTest.java @@ -27,12 +27,15 @@ import android.content.ComponentName; import android.graphics.Rect; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; +import android.testing.TestableResources; import android.util.Size; import androidx.test.filters.SmallTest; import com.android.internal.util.function.TriConsumer; +import com.android.wm.shell.R; import com.android.wm.shell.ShellTestCase; +import com.android.wm.shell.pip.phone.PipSizeSpecHandler; import org.junit.Before; import org.junit.Test; @@ -51,13 +54,23 @@ public class PipBoundsStateTest extends ShellTestCase { private static final Size DEFAULT_SIZE = new Size(10, 10); private static final float DEFAULT_SNAP_FRACTION = 1.0f; + /** The minimum possible size of the override min size's width or height */ + private static final int OVERRIDABLE_MIN_SIZE = 40; + private PipBoundsState mPipBoundsState; private ComponentName mTestComponentName1; private ComponentName mTestComponentName2; @Before public void setUp() { - mPipBoundsState = new PipBoundsState(mContext); + final TestableResources res = mContext.getOrCreateTestableResources(); + res.addOverride( + R.dimen.overridable_minimal_size_pip_resizable_task, + OVERRIDABLE_MIN_SIZE); + + PipDisplayLayoutState pipDisplayLayoutState = new PipDisplayLayoutState(mContext); + mPipBoundsState = new PipBoundsState(mContext, + new PipSizeSpecHandler(mContext, pipDisplayLayoutState), pipDisplayLayoutState); mTestComponentName1 = new ComponentName(mContext, "component1"); mTestComponentName2 = new ComponentName(mContext, "component2"); } @@ -161,10 +174,10 @@ public class PipBoundsStateTest extends ShellTestCase { @Test public void testSetOverrideMinSize_notChanged_callbackNotInvoked() { final Runnable callback = mock(Runnable.class); - mPipBoundsState.setOverrideMinSize(new Size(5, 5)); + mPipBoundsState.setOverrideMinSize(new Size(100, 150)); mPipBoundsState.setOnMinimalSizeChangeCallback(callback); - mPipBoundsState.setOverrideMinSize(new Size(5, 5)); + mPipBoundsState.setOverrideMinSize(new Size(100, 150)); verify(callback, never()).run(); } @@ -174,11 +187,11 @@ public class PipBoundsStateTest extends ShellTestCase { mPipBoundsState.setOverrideMinSize(null); assertEquals(0, mPipBoundsState.getOverrideMinEdgeSize()); - mPipBoundsState.setOverrideMinSize(new Size(5, 10)); - assertEquals(5, mPipBoundsState.getOverrideMinEdgeSize()); + mPipBoundsState.setOverrideMinSize(new Size(100, 110)); + assertEquals(100, mPipBoundsState.getOverrideMinEdgeSize()); - mPipBoundsState.setOverrideMinSize(new Size(15, 10)); - assertEquals(10, mPipBoundsState.getOverrideMinEdgeSize()); + mPipBoundsState.setOverrideMinSize(new Size(150, 200)); + assertEquals(150, mPipBoundsState.getOverrideMinEdgeSize()); } @Test 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..15bb10ed4f2b 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; @@ -51,6 +53,7 @@ import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.pip.phone.PhonePipMenuController; +import com.android.wm.shell.pip.phone.PipSizeSpecHandler; import com.android.wm.shell.splitscreen.SplitScreenController; import org.junit.Before; @@ -68,7 +71,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; @@ -84,6 +87,8 @@ public class PipTaskOrganizerTest extends ShellTestCase { private PipBoundsState mPipBoundsState; private PipTransitionState mPipTransitionState; private PipBoundsAlgorithm mPipBoundsAlgorithm; + private PipSizeSpecHandler mPipSizeSpecHandler; + private PipDisplayLayoutState mPipDisplayLayoutState; private ComponentName mComponent1; private ComponentName mComponent2; @@ -93,19 +98,23 @@ public class PipTaskOrganizerTest extends ShellTestCase { MockitoAnnotations.initMocks(this); mComponent1 = new ComponentName(mContext, "component1"); mComponent2 = new ComponentName(mContext, "component2"); - mPipBoundsState = new PipBoundsState(mContext); + mPipDisplayLayoutState = new PipDisplayLayoutState(mContext); + mPipSizeSpecHandler = new PipSizeSpecHandler(mContext, mPipDisplayLayoutState); + mPipBoundsState = new PipBoundsState(mContext, mPipSizeSpecHandler, mPipDisplayLayoutState); mPipTransitionState = new PipTransitionState(); mPipBoundsAlgorithm = new PipBoundsAlgorithm(mContext, mPipBoundsState, - new PipSnapAlgorithm()); + new PipSnapAlgorithm(), new PipKeepClearAlgorithmInterface() {}, + mPipSizeSpecHandler); mMainExecutor = new TestShellExecutor(); - mSpiedPipTaskOrganizer = spy(new PipTaskOrganizer(mContext, - mMockSyncTransactionQueue, mPipTransitionState, mPipBoundsState, + mPipTaskOrganizer = new PipTaskOrganizer(mContext, mMockSyncTransactionQueue, + mPipTransitionState, mPipBoundsState, mPipDisplayLayoutState, mPipBoundsAlgorithm, mMockPhonePipMenuController, mMockPipAnimationController, mMockPipSurfaceTransactionHelper, mMockPipTransitionController, mMockPipParamsChangedForwarder, mMockOptionalSplitScreen, mMockDisplayController, - mMockPipUiEventLogger, mMockShellTaskOrganizer, mMainExecutor)); + mMockPipUiEventLogger, mMockShellTaskOrganizer, mMainExecutor); mMainExecutor.flushAll(); preparePipTaskOrg(); + preparePipSurfaceTransactionHelper(); } @Test @@ -122,14 +131,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 +147,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 +157,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 +175,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 +186,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 +204,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 +217,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 +229,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 +242,43 @@ 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()); + DisplayLayout layout = new DisplayLayout(info, + mContext.getResources(), true, true); + mPipDisplayLayoutState.setDisplayLayout(layout); + 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..6995d10dd78d 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 @@ -18,20 +18,29 @@ package com.android.wm.shell.pip.phone; import static android.content.pm.PackageManager.FEATURE_PICTURE_IN_PICTURE; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; 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; +import static java.lang.Integer.MAX_VALUE; + import android.content.ComponentName; import android.content.Context; import android.content.pm.PackageManager; import android.content.res.Configuration; +import android.graphics.Point; import android.graphics.Rect; +import android.os.Bundle; import android.os.RemoteException; import android.test.suitebuilder.annotation.SmallTest; import android.testing.AndroidTestingRunner; @@ -41,23 +50,33 @@ 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.TabletopModeController; import com.android.wm.shell.common.TaskStackListenerImpl; import com.android.wm.shell.onehanded.OneHandedController; +import com.android.wm.shell.pip.PipAnimationController; import com.android.wm.shell.pip.PipAppOpsListener; import com.android.wm.shell.pip.PipBoundsAlgorithm; import com.android.wm.shell.pip.PipBoundsState; +import com.android.wm.shell.pip.PipDisplayLayoutState; 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.sysui.ShellCommandHandler; +import com.android.wm.shell.sysui.ShellController; +import com.android.wm.shell.sysui.ShellInit; +import com.android.wm.shell.sysui.ShellSharedConstants; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import java.util.Optional; @@ -71,23 +90,33 @@ import java.util.Set; @TestableLooper.RunWithLooper public class PipControllerTest extends ShellTestCase { private PipController mPipController; + private ShellInit mShellInit; + private ShellController mShellController; + @Mock private ShellCommandHandler mMockShellCommandHandler; @Mock private DisplayController mMockDisplayController; @Mock private PhonePipMenuController mMockPhonePipMenuController; + @Mock private PipAnimationController mMockPipAnimationController; @Mock private PipAppOpsListener mMockPipAppOpsListener; @Mock private PipBoundsAlgorithm mMockPipBoundsAlgorithm; + @Mock private PhonePipKeepClearAlgorithm mMockPipKeepClearAlgorithm; @Mock private PipSnapAlgorithm mMockPipSnapAlgorithm; @Mock private PipMediaController mMockPipMediaController; @Mock private PipTaskOrganizer mMockPipTaskOrganizer; + @Mock private PipTransitionState mMockPipTransitionState; @Mock private PipTransitionController mMockPipTransitionController; @Mock private PipTouchHandler mMockPipTouchHandler; @Mock private PipMotionHelper mMockPipMotionHelper; @Mock private WindowManagerShellWrapper mMockWindowManagerShellWrapper; @Mock private PipBoundsState mMockPipBoundsState; + @Mock private PipSizeSpecHandler mMockPipSizeSpecHandler; + @Mock private PipDisplayLayoutState mMockPipDisplayLayoutState; @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 TabletopModeController mMockTabletopModeController; @Mock private DisplayLayout mMockDisplayLayout1; @Mock private DisplayLayout mMockDisplayLayout2; @@ -99,18 +128,61 @@ 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, + mShellInit = spy(new ShellInit(mMockExecutor)); + mShellController = spy(new ShellController(mShellInit, mMockShellCommandHandler, + mMockExecutor)); + mPipController = new PipController(mContext, mShellInit, mMockShellCommandHandler, + mShellController, mMockDisplayController, mMockPipAnimationController, + mMockPipAppOpsListener, mMockPipBoundsAlgorithm, mMockPipKeepClearAlgorithm, + mMockPipBoundsState, mMockPipSizeSpecHandler, mMockPipDisplayLayoutState, + mMockPipMotionHelper, mMockPipMediaController, mMockPhonePipMenuController, + mMockPipTaskOrganizer, mMockPipTransitionState, mMockPipTouchHandler, mMockPipTransitionController, mMockWindowManagerShellWrapper, - mMockTaskStackListener, mPipParamsChangedForwarder, + mMockTaskStackListener, mMockPipParamsChangedForwarder, + mMockDisplayInsetsController, mMockTabletopModeController, 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(), eq(mPipController)); + } + + @Test + public void instantiateController_registerDumpCallback() { + verify(mMockShellCommandHandler, times(1)).addDumpCallback(any(), eq(mPipController)); + } + + @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_registerExternalInterface() { + verify(mShellController, times(1)).addExternalInterface( + eq(ShellSharedConstants.KEY_EXTRA_SHELL_PIP), any(), eq(mPipController)); + } + + @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()); } @@ -126,18 +198,40 @@ public class PipControllerTest extends ShellTestCase { } @Test + public void testInvalidateExternalInterface_unregistersListener() { + mPipController.setPinnedStackAnimationListener(new PipController.PipAnimationListener() { + @Override + public void onPipAnimationStarted() {} + @Override + public void onPipResourceDimensionsChanged(int cornerRadius, int shadowRadius) {} + @Override + public void onExpandPip() {} + }); + assertTrue(mPipController.hasPinnedStackAnimationListener()); + // Create initial interface + mShellController.createExternalInterfaces(new Bundle()); + // Recreate the interface to trigger invalidation of the previous instance + mShellController.createExternalInterfaces(new Bundle()); + assertFalse(mPipController.hasPinnedStackAnimationListener()); + } + + @Test public void createPip_notSupported_returnsNull() { Context spyContext = spy(mContext); PackageManager mockPackageManager = mock(PackageManager.class); 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, + ShellInit shellInit = new ShellInit(mMockExecutor); + assertNull(PipController.create(spyContext, shellInit, mMockShellCommandHandler, + mShellController, mMockDisplayController, mMockPipAnimationController, + mMockPipAppOpsListener, mMockPipBoundsAlgorithm, mMockPipKeepClearAlgorithm, + mMockPipBoundsState, mMockPipSizeSpecHandler, mMockPipDisplayLayoutState, + mMockPipMotionHelper, mMockPipMediaController, mMockPhonePipMenuController, + mMockPipTaskOrganizer, mMockPipTransitionState, mMockPipTouchHandler, mMockPipTransitionController, mMockWindowManagerShellWrapper, - mMockTaskStackListener, mPipParamsChangedForwarder, + mMockTaskStackListener, mMockPipParamsChangedForwarder, + mMockDisplayInsetsController, mMockTabletopModeController, mMockOneHandedController, mMockExecutor)); } @@ -191,15 +285,19 @@ public class PipControllerTest extends ShellTestCase { final int displayId = 1; final Rect bounds = new Rect(0, 0, 10, 10); when(mMockPipBoundsAlgorithm.getDefaultBounds()).thenReturn(bounds); - when(mMockPipBoundsState.getDisplayId()).thenReturn(displayId); - when(mMockPipBoundsState.getDisplayLayout()).thenReturn(mMockDisplayLayout1); + when(mMockPipBoundsState.getBounds()).thenReturn(bounds); + when(mMockPipBoundsState.getMinSize()).thenReturn(new Point(1, 1)); + when(mMockPipBoundsState.getMaxSize()).thenReturn(new Point(MAX_VALUE, MAX_VALUE)); + when(mMockPipBoundsState.getBounds()).thenReturn(bounds); + when(mMockPipDisplayLayoutState.getDisplayId()).thenReturn(displayId); + when(mMockPipDisplayLayoutState.getDisplayLayout()).thenReturn(mMockDisplayLayout1); when(mMockDisplayController.getDisplayLayout(displayId)).thenReturn(mMockDisplayLayout2); when(mMockPipTaskOrganizer.isInPip()).thenReturn(true); mPipController.mDisplaysChangedListener.onDisplayConfigurationChanged( displayId, new Configuration()); - verify(mMockPipMotionHelper).movePip(any(Rect.class)); + verify(mMockPipTaskOrganizer).scheduleFinishResizePip(any(Rect.class)); } @Test @@ -207,26 +305,47 @@ public class PipControllerTest extends ShellTestCase { final int displayId = 1; final Rect bounds = new Rect(0, 0, 10, 10); when(mMockPipBoundsAlgorithm.getDefaultBounds()).thenReturn(bounds); - when(mMockPipBoundsState.getDisplayId()).thenReturn(displayId); - when(mMockPipBoundsState.getDisplayLayout()).thenReturn(mMockDisplayLayout1); + when(mMockPipDisplayLayoutState.getDisplayId()).thenReturn(displayId); + when(mMockPipDisplayLayoutState.getDisplayLayout()).thenReturn(mMockDisplayLayout1); when(mMockDisplayController.getDisplayLayout(displayId)).thenReturn(mMockDisplayLayout2); when(mMockPipTaskOrganizer.isInPip()).thenReturn(false); mPipController.mDisplaysChangedListener.onDisplayConfigurationChanged( displayId, new Configuration()); - verify(mMockPipMotionHelper, never()).movePip(any(Rect.class)); + verify(mMockPipTaskOrganizer, never()).scheduleFinishResizePip(any(Rect.class)); + } + + @Test + public void onKeepClearAreasChanged_featureDisabled_pipBoundsStateDoesntChange() { + mPipController.setEnablePipKeepClearAlgorithm(false); + final int displayId = 1; + final Rect keepClearArea = new Rect(0, 0, 10, 10); + when(mMockPipDisplayLayoutState.getDisplayId()).thenReturn(displayId); + + mPipController.mDisplaysChangedListener.onKeepClearAreasChanged( + displayId, Set.of(keepClearArea), Set.of()); + + verify(mMockPipBoundsState, never()).setKeepClearAreas(Mockito.anySet(), Mockito.anySet()); } @Test - public void onKeepClearAreasChanged_updatesPipBoundsState() { + 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); + when(mMockPipDisplayLayoutState.getDisplayId()).thenReturn(displayId); mPipController.mDisplaysChangedListener.onKeepClearAreasChanged( displayId, Set.of(keepClearArea), Set.of()); 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..ada3455fae18 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 @@ -16,6 +16,7 @@ package com.android.wm.shell.pip.phone; +import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; @@ -36,6 +37,8 @@ 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.PipDisplayLayoutState; +import com.android.wm.shell.pip.PipKeepClearAlgorithmInterface; import com.android.wm.shell.pip.PipSnapAlgorithm; import com.android.wm.shell.pip.PipTaskOrganizer; import com.android.wm.shell.pip.PipTransitionController; @@ -54,6 +57,7 @@ import org.mockito.MockitoAnnotations; @SmallTest @TestableLooper.RunWithLooper(setAsMainLooper = true) public class PipResizeGestureHandlerTest extends ShellTestCase { + private static final float DEFAULT_SNAP_FRACTION = 2.0f; private static final int STEP_SIZE = 40; private final MotionEvent.PointerProperties[] mPp = new MotionEvent.PointerProperties[2]; @@ -82,13 +86,21 @@ public class PipResizeGestureHandlerTest extends ShellTestCase { private PipBoundsState mPipBoundsState; + private PipSizeSpecHandler mPipSizeSpecHandler; + + private PipDisplayLayoutState mPipDisplayLayoutState; + @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); - mPipBoundsState = new PipBoundsState(mContext); + mPipDisplayLayoutState = new PipDisplayLayoutState(mContext); + mPipSizeSpecHandler = new PipSizeSpecHandler(mContext, mPipDisplayLayoutState); + mPipBoundsState = new PipBoundsState(mContext, mPipSizeSpecHandler, mPipDisplayLayoutState); final PipSnapAlgorithm pipSnapAlgorithm = new PipSnapAlgorithm(); + final PipKeepClearAlgorithmInterface pipKeepClearAlgorithm = + new PipKeepClearAlgorithmInterface() {}; final PipBoundsAlgorithm pipBoundsAlgorithm = new PipBoundsAlgorithm(mContext, - mPipBoundsState, pipSnapAlgorithm); + mPipBoundsState, pipSnapAlgorithm, pipKeepClearAlgorithm, mPipSizeSpecHandler); final PipMotionHelper motionHelper = new PipMotionHelper(mContext, mPipBoundsState, mPipTaskOrganizer, mPhonePipMenuController, pipSnapAlgorithm, mMockPipTransitionController, mFloatingContentCoordinator); @@ -147,7 +159,7 @@ public class PipResizeGestureHandlerTest extends ShellTestCase { mPipResizeGestureHandler.onPinchResize(upEvent); verify(mPipTaskOrganizer, times(1)) - .scheduleAnimateResizePip(any(), any(), anyInt(), anyFloat(), any()); + .scheduleAnimateResizePip(any(), any(), anyInt(), anyFloat(), any(), any()); assertTrue("The new size should be bigger than the original PiP size.", mPipResizeGestureHandler.getLastResizeBounds().width() @@ -186,13 +198,58 @@ public class PipResizeGestureHandlerTest extends ShellTestCase { mPipResizeGestureHandler.onPinchResize(upEvent); verify(mPipTaskOrganizer, times(1)) - .scheduleAnimateResizePip(any(), any(), anyInt(), anyFloat(), any()); + .scheduleAnimateResizePip(any(), any(), anyInt(), anyFloat(), any(), any()); assertTrue("The new size should be smaller than the original PiP size.", mPipResizeGestureHandler.getLastResizeBounds().width() < mPipBoundsState.getBounds().width()); } + @Test + public void testUserResizeTo() { + // resizing the bounds to normal bounds at first + mPipResizeGestureHandler.userResizeTo(mPipBoundsState.getNormalBounds(), + DEFAULT_SNAP_FRACTION); + + assertPipBoundsUserResizedTo(mPipBoundsState.getNormalBounds()); + + verify(mPipTaskOrganizer, times(1)) + .scheduleUserResizePip(any(), any(), any()); + + verify(mPipTaskOrganizer, times(1)) + .scheduleFinishResizePip(any(), any()); + + // bounds with max size + final Rect maxBounds = new Rect(0, 0, mPipBoundsState.getMaxSize().x, + mPipBoundsState.getMaxSize().y); + + // resizing the bounds to maximum bounds the second time + mPipResizeGestureHandler.userResizeTo(maxBounds, DEFAULT_SNAP_FRACTION); + + assertPipBoundsUserResizedTo(maxBounds); + + // another call to scheduleUserResizePip() and scheduleFinishResizePip() makes + // the total number of invocations 2 for each method + verify(mPipTaskOrganizer, times(2)) + .scheduleUserResizePip(any(), any(), any()); + + verify(mPipTaskOrganizer, times(2)) + .scheduleFinishResizePip(any(), any()); + } + + private void assertPipBoundsUserResizedTo(Rect bounds) { + // check user-resized bounds + assertEquals(mPipResizeGestureHandler.getUserResizeBounds().width(), bounds.width()); + assertEquals(mPipResizeGestureHandler.getUserResizeBounds().height(), bounds.height()); + + // check if the bounds are the same + assertEquals(mPipBoundsState.getBounds().width(), bounds.width()); + assertEquals(mPipBoundsState.getBounds().height(), bounds.height()); + + // a flag should be set to indicate pip has been resized by the user + assertTrue(mPipBoundsState.hasUserResizedPip()); + } + private MotionEvent obtainMotionEvent(int action, int topLeft, int bottomRight) { final MotionEvent.PointerCoords[] pc = new MotionEvent.PointerCoords[2]; for (int i = 0; i < 2; i++) { diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipSizeSpecHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipSizeSpecHandlerTest.java new file mode 100644 index 000000000000..390c830069eb --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipSizeSpecHandlerTest.java @@ -0,0 +1,217 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.pip.phone; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; + +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.content.res.Resources; +import android.os.SystemProperties; +import android.testing.AndroidTestingRunner; +import android.util.Size; +import android.view.DisplayInfo; + +import com.android.dx.mockito.inline.extended.StaticMockitoSession; +import com.android.wm.shell.ShellTestCase; +import com.android.wm.shell.common.DisplayLayout; +import com.android.wm.shell.pip.PipDisplayLayoutState; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.exceptions.misusing.InvalidUseOfMatchersException; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; + +/** + * Unit test against {@link PipSizeSpecHandler} with feature flag on. + */ +@RunWith(AndroidTestingRunner.class) +public class PipSizeSpecHandlerTest extends ShellTestCase { + /** A sample overridden min edge size. */ + private static final int OVERRIDE_MIN_EDGE_SIZE = 40; + /** A sample default min edge size */ + private static final int DEFAULT_MIN_EDGE_SIZE = 40; + /** Display edge size */ + private static final int DISPLAY_EDGE_SIZE = 1000; + /** Default sizing percentage */ + private static final float DEFAULT_PERCENT = 0.6f; + /** Minimum sizing percentage */ + private static final float MIN_PERCENT = 0.5f; + /** Aspect ratio that the new PIP size spec logic optimizes for. */ + private static final float OPTIMIZED_ASPECT_RATIO = 9f / 16; + + /** A map of aspect ratios to be tested to expected sizes */ + private static Map<Float, Size> sExpectedMaxSizes; + private static Map<Float, Size> sExpectedDefaultSizes; + private static Map<Float, Size> sExpectedMinSizes; + /** A static mockito session object to mock {@link SystemProperties} */ + private static StaticMockitoSession sStaticMockitoSession; + + @Mock private Context mContext; + @Mock private Resources mResources; + + private PipDisplayLayoutState mPipDisplayLayoutState; + private PipSizeSpecHandler mPipSizeSpecHandler; + + /** + * Sets up static Mockito session for SystemProperties and mocks necessary static methods. + */ + private static void setUpStaticSystemPropertiesSession() { + sStaticMockitoSession = mockitoSession() + .mockStatic(SystemProperties.class).startMocking(); + // make sure the feature flag is on + when(SystemProperties.getBoolean(anyString(), anyBoolean())).thenReturn(true); + when(SystemProperties.get(anyString(), anyString())).thenAnswer(invocation -> { + String property = invocation.getArgument(0); + if (property.equals("com.android.wm.shell.pip.phone.def_percentage")) { + return Float.toString(DEFAULT_PERCENT); + } else if (property.equals("com.android.wm.shell.pip.phone.min_percentage")) { + return Float.toString(MIN_PERCENT); + } + + // throw an exception if illegal arguments are used for these tests + throw new InvalidUseOfMatchersException( + String.format("Argument %s does not match", property) + ); + }); + } + + /** + * Initializes the map with the aspect ratios to be tested and corresponding expected max sizes. + */ + private static void initExpectedSizes() { + sExpectedMaxSizes = new HashMap<>(); + sExpectedDefaultSizes = new HashMap<>(); + sExpectedMinSizes = new HashMap<>(); + + sExpectedMaxSizes.put(16f / 9, new Size(1000, 562)); + sExpectedDefaultSizes.put(16f / 9, new Size(600, 337)); + sExpectedMinSizes.put(16f / 9, new Size(499, 281)); + + sExpectedMaxSizes.put(4f / 3, new Size(892, 669)); + sExpectedDefaultSizes.put(4f / 3, new Size(535, 401)); + sExpectedMinSizes.put(4f / 3, new Size(445, 334)); + + sExpectedMaxSizes.put(3f / 4, new Size(669, 892)); + sExpectedDefaultSizes.put(3f / 4, new Size(401, 535)); + sExpectedMinSizes.put(3f / 4, new Size(334, 445)); + + sExpectedMaxSizes.put(9f / 16, new Size(562, 999)); + sExpectedDefaultSizes.put(9f / 16, new Size(337, 599)); + sExpectedMinSizes.put(9f / 16, new Size(281, 499)); + } + + private void forEveryTestCaseCheck(Map<Float, Size> expectedSizes, + Function<Float, Size> callback) { + for (Map.Entry<Float, Size> expectedSizesEntry : expectedSizes.entrySet()) { + float aspectRatio = expectedSizesEntry.getKey(); + Size expectedSize = expectedSizesEntry.getValue(); + + Assert.assertEquals(expectedSize, callback.apply(aspectRatio)); + } + } + + @Before + public void setUp() { + initExpectedSizes(); + + when(mResources.getDimensionPixelSize(anyInt())).thenReturn(DEFAULT_MIN_EDGE_SIZE); + when(mResources.getFloat(anyInt())).thenReturn(OPTIMIZED_ASPECT_RATIO); + when(mResources.getString(anyInt())).thenReturn("0x0"); + when(mResources.getDisplayMetrics()) + .thenReturn(getContext().getResources().getDisplayMetrics()); + + // set up the mock context for spec handler specifically + when(mContext.getResources()).thenReturn(mResources); + + DisplayInfo displayInfo = new DisplayInfo(); + displayInfo.logicalWidth = DISPLAY_EDGE_SIZE; + displayInfo.logicalHeight = DISPLAY_EDGE_SIZE; + + // use the parent context (not the mocked one) to obtain the display layout + // this is done to avoid unnecessary mocking while allowing for custom display dimensions + DisplayLayout displayLayout = new DisplayLayout(displayInfo, getContext().getResources(), + false, false); + mPipDisplayLayoutState = new PipDisplayLayoutState(mContext); + mPipDisplayLayoutState.setDisplayLayout(displayLayout); + + setUpStaticSystemPropertiesSession(); + mPipSizeSpecHandler = new PipSizeSpecHandler(mContext, mPipDisplayLayoutState); + + // no overridden min edge size by default + mPipSizeSpecHandler.setOverrideMinSize(null); + } + + @After + public void cleanUp() { + sStaticMockitoSession.finishMocking(); + } + + @Test + public void testGetMaxSize() { + forEveryTestCaseCheck(sExpectedMaxSizes, + (aspectRatio) -> mPipSizeSpecHandler.getMaxSize(aspectRatio)); + } + + @Test + public void testGetDefaultSize() { + forEveryTestCaseCheck(sExpectedDefaultSizes, + (aspectRatio) -> mPipSizeSpecHandler.getDefaultSize(aspectRatio)); + } + + @Test + public void testGetMinSize() { + forEveryTestCaseCheck(sExpectedMinSizes, + (aspectRatio) -> mPipSizeSpecHandler.getMinSize(aspectRatio)); + } + + @Test + public void testGetSizeForAspectRatio_noOverrideMinSize() { + // an initial size with 16:9 aspect ratio + Size initSize = new Size(600, 337); + + Size expectedSize = new Size(337, 599); + Size actualSize = mPipSizeSpecHandler.getSizeForAspectRatio(initSize, 9f / 16); + + Assert.assertEquals(expectedSize, actualSize); + } + + @Test + public void testGetSizeForAspectRatio_withOverrideMinSize() { + // an initial size with a 1:1 aspect ratio + mPipSizeSpecHandler.setOverrideMinSize(new Size(OVERRIDE_MIN_EDGE_SIZE, + OVERRIDE_MIN_EDGE_SIZE)); + // make sure initial size is same as override min size + Size initSize = mPipSizeSpecHandler.getOverrideMinSize(); + + Size expectedSize = new Size(40, 71); + Size actualSize = mPipSizeSpecHandler.getSizeForAspectRatio(initSize, 9f / 16); + + Assert.assertEquals(expectedSize, actualSize); + } +} 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..10b1ddf1b868 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 @@ -25,6 +25,7 @@ import static org.mockito.Mockito.verify; import android.graphics.Rect; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; +import android.util.Size; import androidx.test.filters.SmallTest; @@ -34,10 +35,13 @@ 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.PipDisplayLayoutState; +import com.android.wm.shell.pip.PipKeepClearAlgorithmInterface; 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 +82,9 @@ public class PipTouchHandlerTest extends ShellTestCase { private PipUiEventLogger mPipUiEventLogger; @Mock + private ShellInit mShellInit; + + @Mock private ShellExecutor mMainExecutor; private PipBoundsState mPipBoundsState; @@ -85,6 +92,8 @@ public class PipTouchHandlerTest extends ShellTestCase { private PipSnapAlgorithm mPipSnapAlgorithm; private PipMotionHelper mMotionHelper; private PipResizeGestureHandler mPipResizeGestureHandler; + private PipSizeSpecHandler mPipSizeSpecHandler; + private PipDisplayLayoutState mPipDisplayLayoutState; private DisplayLayout mDisplayLayout; private Rect mInsetBounds; @@ -98,24 +107,27 @@ public class PipTouchHandlerTest extends ShellTestCase { @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); - mPipBoundsState = new PipBoundsState(mContext); + mPipDisplayLayoutState = new PipDisplayLayoutState(mContext); + mPipSizeSpecHandler = new PipSizeSpecHandler(mContext, mPipDisplayLayoutState); + mPipBoundsState = new PipBoundsState(mContext, mPipSizeSpecHandler, mPipDisplayLayoutState); mPipSnapAlgorithm = new PipSnapAlgorithm(); - mPipBoundsAlgorithm = new PipBoundsAlgorithm(mContext, mPipBoundsState, mPipSnapAlgorithm); + mPipBoundsAlgorithm = new PipBoundsAlgorithm(mContext, mPipBoundsState, mPipSnapAlgorithm, + new PipKeepClearAlgorithmInterface() {}, mPipSizeSpecHandler); 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, mPipSizeSpecHandler, 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); mPipTouchHandler.setPipResizeGestureHandler(mPipResizeGestureHandler); mDisplayLayout = new DisplayLayout(mContext, mContext.getDisplay()); - mPipBoundsState.setDisplayLayout(mDisplayLayout); + mPipDisplayLayoutState.setDisplayLayout(mDisplayLayout); mInsetBounds = new Rect(mPipBoundsState.getDisplayBounds().left + INSET, mPipBoundsState.getDisplayBounds().top + INSET, mPipBoundsState.getDisplayBounds().right - INSET, @@ -133,6 +145,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()); @@ -143,17 +160,22 @@ public class PipTouchHandlerTest extends ShellTestCase { mPipTouchHandler.onMovementBoundsChanged(mInsetBounds, mPipBounds, mCurBounds, mFromImeAdjustment, mFromShelfAdjustment, mDisplayRotation); + // getting the expected min and max size + float aspectRatio = (float) mPipBounds.width() / mPipBounds.height(); + Size expectedMinSize = mPipSizeSpecHandler.getMinSize(aspectRatio); + Size expectedMaxSize = mPipSizeSpecHandler.getMaxSize(aspectRatio); + assertEquals(expectedMovementBounds, mPipBoundsState.getNormalMovementBounds()); verify(mPipResizeGestureHandler, times(1)) - .updateMinSize(mPipBounds.width(), mPipBounds.height()); + .updateMinSize(expectedMinSize.getWidth(), expectedMinSize.getHeight()); verify(mPipResizeGestureHandler, times(1)) - .updateMaxSize(shorterLength - 2 * mInsetBounds.left, - shorterLength - 2 * mInsetBounds.left); + .updateMaxSize(expectedMaxSize.getWidth(), expectedMaxSize.getHeight()); } @Test public void updateMovementBounds_withImeAdjustment_movesPip() { + mPipTouchHandler.setEnablePipKeepClearAlgorithm(false); mFromImeAdjustment = true; mPipTouchHandler.onImeVisibilityChanged(true /* imeVisible */, mImeHeight); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/OWNERS b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/OWNERS new file mode 100644 index 000000000000..736d4cff6ce8 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/OWNERS @@ -0,0 +1,3 @@ +# WM shell sub-module TV pip owners +galinap@google.com +bronger@google.com
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipActionProviderTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipActionProviderTest.java new file mode 100644 index 000000000000..e5b61ed2fd25 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipActionProviderTest.java @@ -0,0 +1,328 @@ +/* + * 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.tv; + +import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_CLOSE; +import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_CUSTOM; +import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_CUSTOM_CLOSE; +import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_EXPAND_COLLAPSE; +import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_FULLSCREEN; +import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_MOVE; + +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import android.app.PendingIntent; +import android.app.RemoteAction; +import android.graphics.drawable.Icon; +import android.test.suitebuilder.annotation.SmallTest; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; +import android.util.Log; + +import com.android.wm.shell.ShellTestCase; +import com.android.wm.shell.pip.PipMediaController; + +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; + +/** + * Unit tests for {@link TvPipActionsProvider} + */ +@SmallTest +@RunWith(AndroidTestingRunner.class) +@TestableLooper.RunWithLooper +public class TvPipActionProviderTest extends ShellTestCase { + private static final String TAG = TvPipActionProviderTest.class.getSimpleName(); + private TvPipActionsProvider mActionsProvider; + + @Mock + private PipMediaController mMockPipMediaController; + @Mock + private TvPipActionsProvider.Listener mMockListener; + @Mock + private TvPipAction.SystemActionsHandler mMockSystemActionsHandler; + @Mock + private Icon mMockIcon; + @Mock + private PendingIntent mMockPendingIntent; + + private RemoteAction createRemoteAction(int identifier) { + return new RemoteAction(mMockIcon, "" + identifier, "" + identifier, mMockPendingIntent); + } + + private List<RemoteAction> createRemoteActions(int numberOfActions) { + List<RemoteAction> actions = new ArrayList<>(); + for (int i = 0; i < numberOfActions; i++) { + actions.add(createRemoteAction(i)); + } + return actions; + } + + private boolean checkActionsMatch(List<TvPipAction> actions, int[] actionTypes) { + for (int i = 0; i < actions.size(); i++) { + int type = actions.get(i).getActionType(); + if (type != actionTypes[i]) { + Log.e(TAG, "Action at index " + i + ": found " + type + + ", expected " + actionTypes[i]); + return false; + } + } + return true; + } + + @Before + public void setUp() { + if (!isTelevision()) { + return; + } + MockitoAnnotations.initMocks(this); + mActionsProvider = new TvPipActionsProvider(mContext, mMockPipMediaController, + mMockSystemActionsHandler); + } + + @Test + public void defaultSystemActions_regularPip() { + assumeTelevision(); + mActionsProvider.updateExpansionEnabled(false); + assertTrue(checkActionsMatch(mActionsProvider.getActionsList(), + new int[]{ACTION_FULLSCREEN, ACTION_CLOSE, ACTION_MOVE})); + } + + @Test + public void defaultSystemActions_expandedPip() { + assumeTelevision(); + mActionsProvider.updateExpansionEnabled(true); + assertTrue(checkActionsMatch(mActionsProvider.getActionsList(), + new int[]{ACTION_FULLSCREEN, ACTION_CLOSE, ACTION_MOVE, ACTION_EXPAND_COLLAPSE})); + } + + @Test + public void expandedPip_enableExpansion_enable() { + assumeTelevision(); + // PiP has expanded PiP disabled. + mActionsProvider.updateExpansionEnabled(false); + + mActionsProvider.addListener(mMockListener); + mActionsProvider.updateExpansionEnabled(true); + + assertTrue(checkActionsMatch(mActionsProvider.getActionsList(), + new int[]{ACTION_FULLSCREEN, ACTION_CLOSE, ACTION_MOVE, ACTION_EXPAND_COLLAPSE})); + verify(mMockListener).onActionsChanged(/* added= */ 1, /* updated= */ 0, /* index= */ 3); + } + + @Test + public void expandedPip_enableExpansion_disable() { + assumeTelevision(); + mActionsProvider.updateExpansionEnabled(true); + + mActionsProvider.addListener(mMockListener); + mActionsProvider.updateExpansionEnabled(false); + + assertTrue(checkActionsMatch(mActionsProvider.getActionsList(), + new int[]{ACTION_FULLSCREEN, ACTION_CLOSE, ACTION_MOVE})); + verify(mMockListener).onActionsChanged(/* added= */ -1, /* updated= */ 0, /* index= */ 3); + } + + @Test + public void expandedPip_enableExpansion_AlreadyEnabled() { + assumeTelevision(); + mActionsProvider.updateExpansionEnabled(true); + + mActionsProvider.addListener(mMockListener); + mActionsProvider.updateExpansionEnabled(true); + + assertTrue(checkActionsMatch(mActionsProvider.getActionsList(), + new int[]{ACTION_FULLSCREEN, ACTION_CLOSE, ACTION_MOVE, ACTION_EXPAND_COLLAPSE})); + } + + @Test + public void expandedPip_toggleExpansion() { + assumeTelevision(); + // PiP has expanded PiP enabled, but is in a collapsed state + mActionsProvider.updateExpansionEnabled(true); + mActionsProvider.onPipExpansionToggled(/* expanded= */ false); + + mActionsProvider.addListener(mMockListener); + mActionsProvider.onPipExpansionToggled(/* expanded= */ true); + + assertTrue(checkActionsMatch(mActionsProvider.getActionsList(), + new int[]{ACTION_FULLSCREEN, ACTION_CLOSE, ACTION_MOVE, ACTION_EXPAND_COLLAPSE})); + verify(mMockListener).onActionsChanged(0, 1, 3); + } + + @Test + public void customActions_added() { + assumeTelevision(); + mActionsProvider.updateExpansionEnabled(false); + mActionsProvider.addListener(mMockListener); + + mActionsProvider.setAppActions(createRemoteActions(2), null); + + assertTrue(checkActionsMatch(mActionsProvider.getActionsList(), + new int[]{ACTION_FULLSCREEN, ACTION_CLOSE, ACTION_CUSTOM, ACTION_CUSTOM, + ACTION_MOVE})); + verify(mMockListener).onActionsChanged(/* added= */ 2, /* updated= */ 0, /* index= */ 2); + } + + @Test + public void customActions_replacedMore() { + assumeTelevision(); + mActionsProvider.updateExpansionEnabled(false); + mActionsProvider.setAppActions(createRemoteActions(2), null); + + mActionsProvider.addListener(mMockListener); + mActionsProvider.setAppActions(createRemoteActions(3), null); + + assertTrue(checkActionsMatch(mActionsProvider.getActionsList(), + new int[]{ACTION_FULLSCREEN, ACTION_CLOSE, ACTION_CUSTOM, ACTION_CUSTOM, + ACTION_CUSTOM, ACTION_MOVE})); + verify(mMockListener).onActionsChanged(/* added= */ 1, /* updated= */ 2, /* index= */ 2); + } + + @Test + public void customActions_replacedLess() { + assumeTelevision(); + mActionsProvider.updateExpansionEnabled(false); + mActionsProvider.setAppActions(createRemoteActions(2), null); + + mActionsProvider.addListener(mMockListener); + mActionsProvider.setAppActions(createRemoteActions(0), null); + + assertTrue(checkActionsMatch(mActionsProvider.getActionsList(), + new int[]{ACTION_FULLSCREEN, ACTION_CLOSE, ACTION_MOVE})); + verify(mMockListener).onActionsChanged(/* added= */ -2, /* updated= */ 0, /* index= */ 2); + } + + @Test + public void customCloseAdded() { + assumeTelevision(); + mActionsProvider.updateExpansionEnabled(false); + + List<RemoteAction> customActions = new ArrayList<>(); + mActionsProvider.setAppActions(customActions, null); + + mActionsProvider.addListener(mMockListener); + mActionsProvider.setAppActions(customActions, createRemoteAction(0)); + + assertTrue(checkActionsMatch(mActionsProvider.getActionsList(), + new int[]{ACTION_FULLSCREEN, ACTION_CUSTOM_CLOSE, ACTION_MOVE})); + verify(mMockListener).onActionsChanged(/* added= */ 0, /* updated= */ 1, /* index= */ 1); + } + + @Test + public void customClose_matchesOtherCustomAction() { + assumeTelevision(); + mActionsProvider.updateExpansionEnabled(false); + + List<RemoteAction> customActions = createRemoteActions(2); + RemoteAction customClose = createRemoteAction(/* id= */ 10); + customActions.add(customClose); + + mActionsProvider.addListener(mMockListener); + mActionsProvider.setAppActions(customActions, customClose); + + assertTrue(checkActionsMatch(mActionsProvider.getActionsList(), + new int[]{ACTION_FULLSCREEN, ACTION_CUSTOM_CLOSE, ACTION_CUSTOM, ACTION_CUSTOM, + ACTION_MOVE})); + verify(mMockListener).onActionsChanged(/* added= */ 0, /* updated= */ 1, /* index= */ 1); + verify(mMockListener).onActionsChanged(/* added= */ 2, /* updated= */ 0, /* index= */ 2); + } + + @Test + public void mediaActions_added_whileCustomActionsExist() { + assumeTelevision(); + mActionsProvider.updateExpansionEnabled(false); + mActionsProvider.setAppActions(createRemoteActions(2), null); + + mActionsProvider.addListener(mMockListener); + mActionsProvider.onMediaActionsChanged(createRemoteActions(3)); + + assertTrue(checkActionsMatch(mActionsProvider.getActionsList(), + new int[]{ACTION_FULLSCREEN, ACTION_CLOSE, ACTION_CUSTOM, ACTION_CUSTOM, + ACTION_MOVE})); + verify(mMockListener, times(0)).onActionsChanged(anyInt(), anyInt(), anyInt()); + } + + @Test + public void customActions_removed_whileMediaActionsExist() { + assumeTelevision(); + mActionsProvider.updateExpansionEnabled(false); + mActionsProvider.onMediaActionsChanged(createRemoteActions(2)); + mActionsProvider.setAppActions(createRemoteActions(3), null); + + mActionsProvider.addListener(mMockListener); + mActionsProvider.setAppActions(createRemoteActions(0), null); + + assertTrue(checkActionsMatch(mActionsProvider.getActionsList(), + new int[]{ACTION_FULLSCREEN, ACTION_CLOSE, ACTION_CUSTOM, ACTION_CUSTOM, + ACTION_MOVE})); + verify(mMockListener).onActionsChanged(/* added= */ -1, /* updated= */ 2, /* index= */ 2); + } + + @Test + public void customCloseOnly_mediaActionsShowing() { + assumeTelevision(); + mActionsProvider.updateExpansionEnabled(false); + mActionsProvider.onMediaActionsChanged(createRemoteActions(2)); + + mActionsProvider.addListener(mMockListener); + mActionsProvider.setAppActions(createRemoteActions(0), createRemoteAction(5)); + + assertTrue(checkActionsMatch(mActionsProvider.getActionsList(), + new int[]{ACTION_FULLSCREEN, ACTION_CUSTOM_CLOSE, ACTION_CUSTOM, ACTION_CUSTOM, + ACTION_MOVE})); + verify(mMockListener).onActionsChanged(/* added= */ 0, /* updated= */ 1, /* index= */ 1); + } + + @Test + public void customActions_showDisabledActions() { + assumeTelevision(); + mActionsProvider.updateExpansionEnabled(false); + + List<RemoteAction> customActions = createRemoteActions(2); + customActions.get(0).setEnabled(false); + mActionsProvider.setAppActions(customActions, null); + + assertTrue(checkActionsMatch(mActionsProvider.getActionsList(), + new int[]{ACTION_FULLSCREEN, ACTION_CLOSE, ACTION_CUSTOM, ACTION_CUSTOM, + ACTION_MOVE})); + } + + @Test + public void mediaActions_hideDisabledActions() { + assumeTelevision(); + mActionsProvider.updateExpansionEnabled(false); + + List<RemoteAction> customActions = createRemoteActions(2); + customActions.get(0).setEnabled(false); + mActionsProvider.onMediaActionsChanged(customActions); + + assertTrue(checkActionsMatch(mActionsProvider.getActionsList(), + new int[]{ACTION_FULLSCREEN, ACTION_CLOSE, ACTION_CUSTOM, ACTION_MOVE})); + } + +} + 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..7370ed71bbdd 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 @@ -24,6 +24,7 @@ import android.os.test.TestLooper import android.testing.AndroidTestingRunner import com.android.wm.shell.R +import com.android.wm.shell.ShellTestCase import com.android.wm.shell.pip.PipBoundsState.STASH_TYPE_RIGHT import com.android.wm.shell.pip.tv.TvPipBoundsController.POSITION_DEBOUNCE_TIMEOUT_MILLIS import com.android.wm.shell.pip.tv.TvPipKeepClearAlgorithm.Placement @@ -43,7 +44,7 @@ import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations @RunWith(AndroidTestingRunner::class) -class TvPipBoundsControllerTest { +class TvPipBoundsControllerTest : ShellTestCase() { val ANIMATION_DURATION = 100 val STASH_DURATION = 5000 val FAR_FUTURE = 60 * 60000L @@ -71,7 +72,7 @@ class TvPipBoundsControllerTest { var inMoveMode = false @Mock - lateinit var context: Context + lateinit var mockContext: Context @Mock lateinit var resources: Resources @Mock @@ -83,6 +84,9 @@ class TvPipBoundsControllerTest { @Before fun setUp() { + if (!isTelevision) { + return + } MockitoAnnotations.initMocks(this) time = 0L inMenu = false @@ -91,13 +95,13 @@ class TvPipBoundsControllerTest { testLooper = TestLooper { time } mainHandler = Handler(testLooper.getLooper()) - whenever(context.resources).thenReturn(resources) + whenever(mockContext.resources).thenReturn(resources) whenever(resources.getInteger(R.integer.config_pipStashDuration)).thenReturn(STASH_DURATION) whenever(tvPipBoundsAlgorithm.adjustBoundsForTemporaryDecor(any())) .then(returnsFirstArg<Rect>()) boundsController = TvPipBoundsController( - context, + mockContext, { time }, mainHandler, tvPipBoundsState, @@ -107,6 +111,7 @@ class TvPipBoundsControllerTest { @Test fun testPlacement_MovedAfterDebounceTimeout() { + assumeTelevision() triggerPlacement(MOVED_PLACEMENT) assertMovementAt(POSITION_DEBOUNCE_TIMEOUT_MILLIS, MOVED_BOUNDS) assertNoMovementUpTo(time + FAR_FUTURE) @@ -114,6 +119,7 @@ class TvPipBoundsControllerTest { @Test fun testStashedPlacement_MovedAfterDebounceTimeout_Unstashes() { + assumeTelevision() triggerPlacement(STASHED_PLACEMENT_RESTASH) assertMovementAt(POSITION_DEBOUNCE_TIMEOUT_MILLIS, STASHED_BOUNDS) assertMovementAt(POSITION_DEBOUNCE_TIMEOUT_MILLIS + STASH_DURATION, ANCHOR_BOUNDS) @@ -121,6 +127,7 @@ class TvPipBoundsControllerTest { @Test fun testDebounceSamePlacement_MovesDebounceTimeoutAfterFirstPlacement() { + assumeTelevision() triggerPlacement(MOVED_PLACEMENT) advanceTimeTo(POSITION_DEBOUNCE_TIMEOUT_MILLIS / 2) triggerPlacement(MOVED_PLACEMENT) @@ -130,6 +137,7 @@ class TvPipBoundsControllerTest { @Test fun testNoMovementUntilPlacementStabilizes() { + assumeTelevision() triggerPlacement(ANCHOR_PLACEMENT) advanceTimeTo(time + POSITION_DEBOUNCE_TIMEOUT_MILLIS / 10) triggerPlacement(MOVED_PLACEMENT) @@ -143,6 +151,7 @@ class TvPipBoundsControllerTest { @Test fun testUnstashIfStashNoLongerNecessary() { + assumeTelevision() triggerPlacement(STASHED_PLACEMENT_RESTASH) assertMovementAt(POSITION_DEBOUNCE_TIMEOUT_MILLIS, STASHED_BOUNDS) @@ -152,6 +161,7 @@ class TvPipBoundsControllerTest { @Test fun testRestashingPlacementDelaysUnstash() { + assumeTelevision() triggerPlacement(STASHED_PLACEMENT_RESTASH) assertMovementAt(POSITION_DEBOUNCE_TIMEOUT_MILLIS, STASHED_BOUNDS) @@ -163,6 +173,7 @@ class TvPipBoundsControllerTest { @Test fun testNonRestashingPlacementDoesNotDelayUnstash() { + assumeTelevision() triggerPlacement(STASHED_PLACEMENT_RESTASH) assertMovementAt(POSITION_DEBOUNCE_TIMEOUT_MILLIS, STASHED_BOUNDS) @@ -173,13 +184,26 @@ class TvPipBoundsControllerTest { @Test fun testImmediatePlacement() { + assumeTelevision() triggerImmediatePlacement(STASHED_PLACEMENT_RESTASH) assertMovement(STASHED_BOUNDS) assertMovementAt(time + STASH_DURATION, ANCHOR_BOUNDS) } @Test + fun testImmediatePlacement_DoNotStashIfAlreadyUnstashed() { + assumeTelevision() + triggerImmediatePlacement(STASHED_PLACEMENT_RESTASH) + assertMovement(STASHED_BOUNDS) + assertMovementAt(time + STASH_DURATION, ANCHOR_BOUNDS) + + triggerImmediatePlacement(STASHED_PLACEMENT) + assertNoMovementUpTo(time + FAR_FUTURE) + } + + @Test fun testInMoveMode_KeepAtAnchor() { + assumeTelevision() startMoveMode() triggerImmediatePlacement(STASHED_MOVED_PLACEMENT_RESTASH) assertMovement(ANCHOR_BOUNDS) @@ -188,6 +212,7 @@ class TvPipBoundsControllerTest { @Test fun testInMenu_Unstashed() { + assumeTelevision() openPipMenu() triggerImmediatePlacement(STASHED_MOVED_PLACEMENT_RESTASH) assertMovement(MOVED_BOUNDS) @@ -196,6 +221,7 @@ class TvPipBoundsControllerTest { @Test fun testCloseMenu_DoNotRestash() { + assumeTelevision() openPipMenu() triggerImmediatePlacement(STASHED_MOVED_PLACEMENT_RESTASH) assertMovement(MOVED_BOUNDS) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipGravityTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipGravityTest.java new file mode 100644 index 000000000000..f9b772345b14 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipGravityTest.java @@ -0,0 +1,348 @@ +/* + * 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.tv; + +import static android.view.KeyEvent.KEYCODE_DPAD_DOWN; +import static android.view.KeyEvent.KEYCODE_DPAD_LEFT; +import static android.view.KeyEvent.KEYCODE_DPAD_RIGHT; +import static android.view.KeyEvent.KEYCODE_DPAD_UP; + +import static org.junit.Assert.assertEquals; + +import android.view.Gravity; + +import com.android.wm.shell.ShellTestCase; +import com.android.wm.shell.pip.PipDisplayLayoutState; +import com.android.wm.shell.pip.PipSnapAlgorithm; +import com.android.wm.shell.pip.phone.PipSizeSpecHandler; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.Locale; + +public class TvPipGravityTest extends ShellTestCase { + + private static final float VERTICAL_EXPANDED_ASPECT_RATIO = 1f / 3; + private static final float HORIZONTAL_EXPANDED_ASPECT_RATIO = 3f; + + @Mock + private PipSnapAlgorithm mMockPipSnapAlgorithm; + + private TvPipBoundsState mTvPipBoundsState; + private TvPipBoundsAlgorithm mTvPipBoundsAlgorithm; + private PipSizeSpecHandler mPipSizeSpecHandler; + private PipDisplayLayoutState mPipDisplayLayoutState; + + @Before + public void setUp() { + if (!isTelevision()) { + return; + } + MockitoAnnotations.initMocks(this); + mPipDisplayLayoutState = new PipDisplayLayoutState(mContext); + mPipSizeSpecHandler = new PipSizeSpecHandler(mContext, mPipDisplayLayoutState); + mTvPipBoundsState = new TvPipBoundsState(mContext, mPipSizeSpecHandler, + mPipDisplayLayoutState); + mTvPipBoundsAlgorithm = new TvPipBoundsAlgorithm(mContext, mTvPipBoundsState, + mMockPipSnapAlgorithm, mPipSizeSpecHandler); + + setRTL(false); + } + + private void checkGravity(int gravityActual, int gravityExpected) { + assertEquals(gravityExpected, gravityActual); + } + + private void setRTL(boolean isRtl) { + mContext.getResources().getConfiguration().setLayoutDirection( + isRtl ? new Locale("ar") : Locale.ENGLISH); + mTvPipBoundsState.onConfigurationChanged(); + mTvPipBoundsAlgorithm.onConfigurationChanged(mContext); + } + + private void assertGravityAfterExpansion(int gravityFrom, int gravityTo) { + mTvPipBoundsState.setTvPipExpanded(false); + mTvPipBoundsState.setTvPipGravity(gravityFrom); + mTvPipBoundsAlgorithm.updateGravityOnExpansionToggled(true); + checkGravity(mTvPipBoundsState.getTvPipGravity(), gravityTo); + } + + private void assertGravityAfterCollapse(int gravityFrom, int gravityTo) { + mTvPipBoundsState.setTvPipExpanded(true); + mTvPipBoundsState.setTvPipGravity(gravityFrom); + mTvPipBoundsAlgorithm.updateGravityOnExpansionToggled(false); + checkGravity(mTvPipBoundsState.getTvPipGravity(), gravityTo); + } + + private void assertGravityAfterExpandAndCollapse(int gravityStartAndEnd) { + mTvPipBoundsState.setTvPipGravity(gravityStartAndEnd); + mTvPipBoundsAlgorithm.updateGravityOnExpansionToggled(true); + mTvPipBoundsAlgorithm.updateGravityOnExpansionToggled(false); + checkGravity(mTvPipBoundsState.getTvPipGravity(), gravityStartAndEnd); + } + + @Test + public void regularPip_defaultGravity() { + assumeTelevision(); + checkGravity(mTvPipBoundsState.getDefaultGravity(), Gravity.RIGHT | Gravity.BOTTOM); + } + + @Test + public void regularPip_defaultGravity_RTL() { + assumeTelevision(); + setRTL(true); + checkGravity(mTvPipBoundsState.getDefaultGravity(), Gravity.LEFT | Gravity.BOTTOM); + } + + @Test + public void updateGravity_expand_vertical() { + assumeTelevision(); + // Vertical expanded PiP. + mTvPipBoundsState.setDesiredTvExpandedAspectRatio(VERTICAL_EXPANDED_ASPECT_RATIO, true); + + assertGravityAfterExpansion(Gravity.BOTTOM | Gravity.RIGHT, + Gravity.CENTER_VERTICAL | Gravity.RIGHT); + assertGravityAfterExpansion(Gravity.TOP | Gravity.RIGHT, + Gravity.CENTER_VERTICAL | Gravity.RIGHT); + assertGravityAfterExpansion(Gravity.BOTTOM | Gravity.LEFT, + Gravity.CENTER_VERTICAL | Gravity.LEFT); + assertGravityAfterExpansion(Gravity.TOP | Gravity.LEFT, + Gravity.CENTER_VERTICAL | Gravity.LEFT); + } + + @Test + public void updateGravity_expand_horizontal() { + assumeTelevision(); + // Horizontal expanded PiP. + mTvPipBoundsState.setDesiredTvExpandedAspectRatio(HORIZONTAL_EXPANDED_ASPECT_RATIO, true); + + assertGravityAfterExpansion(Gravity.BOTTOM | Gravity.RIGHT, + Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL); + assertGravityAfterExpansion(Gravity.TOP | Gravity.RIGHT, + Gravity.TOP | Gravity.CENTER_HORIZONTAL); + assertGravityAfterExpansion(Gravity.BOTTOM | Gravity.LEFT, + Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL); + assertGravityAfterExpansion(Gravity.TOP | Gravity.LEFT, + Gravity.TOP | Gravity.CENTER_HORIZONTAL); + } + + @Test + public void updateGravity_collapse() { + assumeTelevision(); + // Vertical expansion + mTvPipBoundsState.setDesiredTvExpandedAspectRatio(VERTICAL_EXPANDED_ASPECT_RATIO, true); + assertGravityAfterCollapse(Gravity.CENTER_VERTICAL | Gravity.RIGHT, + Gravity.BOTTOM | Gravity.RIGHT); + assertGravityAfterCollapse(Gravity.CENTER_VERTICAL | Gravity.LEFT, + Gravity.BOTTOM | Gravity.LEFT); + + // Horizontal expansion + mTvPipBoundsState.setDesiredTvExpandedAspectRatio(HORIZONTAL_EXPANDED_ASPECT_RATIO, true); + assertGravityAfterCollapse(Gravity.TOP | Gravity.CENTER_HORIZONTAL, + Gravity.TOP | Gravity.RIGHT); + assertGravityAfterCollapse(Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL, + Gravity.BOTTOM | Gravity.RIGHT); + } + + @Test + public void updateGravity_collapse_RTL() { + assumeTelevision(); + setRTL(true); + + // Horizontal expansion + mTvPipBoundsState.setDesiredTvExpandedAspectRatio(HORIZONTAL_EXPANDED_ASPECT_RATIO, true); + assertGravityAfterCollapse(Gravity.TOP | Gravity.CENTER_HORIZONTAL, + Gravity.TOP | Gravity.LEFT); + assertGravityAfterCollapse(Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL, + Gravity.BOTTOM | Gravity.LEFT); + } + + @Test + public void updateGravity_expand_collapse() { + assumeTelevision(); + // Vertical expanded PiP. + mTvPipBoundsState.setDesiredTvExpandedAspectRatio(VERTICAL_EXPANDED_ASPECT_RATIO, true); + + assertGravityAfterExpandAndCollapse(Gravity.BOTTOM | Gravity.RIGHT); + assertGravityAfterExpandAndCollapse(Gravity.BOTTOM | Gravity.LEFT); + assertGravityAfterExpandAndCollapse(Gravity.TOP | Gravity.LEFT); + assertGravityAfterExpandAndCollapse(Gravity.TOP | Gravity.RIGHT); + + // Horizontal expanded PiP. + mTvPipBoundsState.setDesiredTvExpandedAspectRatio(HORIZONTAL_EXPANDED_ASPECT_RATIO, true); + + assertGravityAfterExpandAndCollapse(Gravity.BOTTOM | Gravity.RIGHT); + assertGravityAfterExpandAndCollapse(Gravity.BOTTOM | Gravity.LEFT); + assertGravityAfterExpandAndCollapse(Gravity.TOP | Gravity.LEFT); + assertGravityAfterExpandAndCollapse(Gravity.TOP | Gravity.RIGHT); + } + + @Test + public void updateGravity_expand_move_collapse() { + assumeTelevision(); + // Vertical expanded PiP. + mTvPipBoundsState.setDesiredTvExpandedAspectRatio(VERTICAL_EXPANDED_ASPECT_RATIO, true); + expandMoveCollapseCheck(Gravity.TOP | Gravity.RIGHT, KEYCODE_DPAD_LEFT, + Gravity.TOP | Gravity.LEFT); + + // Horizontal expanded PiP. + mTvPipBoundsState.setDesiredTvExpandedAspectRatio(HORIZONTAL_EXPANDED_ASPECT_RATIO, true); + expandMoveCollapseCheck(Gravity.BOTTOM | Gravity.LEFT, KEYCODE_DPAD_UP, + Gravity.TOP | Gravity.LEFT); + } + + private void expandMoveCollapseCheck(int gravityFrom, int keycode, int gravityTo) { + // Expand + mTvPipBoundsState.setTvPipExpanded(false); + mTvPipBoundsState.setTvPipGravity(gravityFrom); + mTvPipBoundsAlgorithm.updateGravityOnExpansionToggled(true); + // Move + mTvPipBoundsAlgorithm.updateGravity(keycode); + // Collapse + mTvPipBoundsState.setTvPipExpanded(true); + mTvPipBoundsAlgorithm.updateGravityOnExpansionToggled(false); + + checkGravity(mTvPipBoundsState.getTvPipGravity(), gravityTo); + } + + private void moveAndCheckGravity(int keycode, int gravityEnd, boolean expectChange) { + assertEquals(expectChange, mTvPipBoundsAlgorithm.updateGravity(keycode)); + checkGravity(mTvPipBoundsState.getTvPipGravity(), gravityEnd); + } + + @Test + public void updateGravity_move_regular_valid() { + assumeTelevision(); + mTvPipBoundsState.setTvPipGravity(Gravity.BOTTOM | Gravity.RIGHT); + // clockwise + moveAndCheckGravity(KEYCODE_DPAD_LEFT, Gravity.BOTTOM | Gravity.LEFT, true); + moveAndCheckGravity(KEYCODE_DPAD_UP, Gravity.TOP | Gravity.LEFT, true); + moveAndCheckGravity(KEYCODE_DPAD_RIGHT, Gravity.TOP | Gravity.RIGHT, true); + moveAndCheckGravity(KEYCODE_DPAD_DOWN, Gravity.BOTTOM | Gravity.RIGHT, true); + // anti-clockwise + moveAndCheckGravity(KEYCODE_DPAD_UP, Gravity.TOP | Gravity.RIGHT, true); + moveAndCheckGravity(KEYCODE_DPAD_LEFT, Gravity.TOP | Gravity.LEFT, true); + moveAndCheckGravity(KEYCODE_DPAD_DOWN, Gravity.BOTTOM | Gravity.LEFT, true); + moveAndCheckGravity(KEYCODE_DPAD_RIGHT, Gravity.BOTTOM | Gravity.RIGHT, true); + } + + @Test + public void updateGravity_move_expanded_valid() { + assumeTelevision(); + mTvPipBoundsState.setTvPipExpanded(true); + + // Vertical expanded PiP. + mTvPipBoundsState.setDesiredTvExpandedAspectRatio(VERTICAL_EXPANDED_ASPECT_RATIO, true); + mTvPipBoundsState.setTvPipGravity(Gravity.CENTER_VERTICAL | Gravity.RIGHT); + moveAndCheckGravity(KEYCODE_DPAD_LEFT, Gravity.CENTER_VERTICAL | Gravity.LEFT, true); + moveAndCheckGravity(KEYCODE_DPAD_RIGHT, Gravity.CENTER_VERTICAL | Gravity.RIGHT, true); + + // Horizontal expanded PiP. + mTvPipBoundsState.setDesiredTvExpandedAspectRatio(HORIZONTAL_EXPANDED_ASPECT_RATIO, true); + mTvPipBoundsState.setTvPipGravity(Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL); + moveAndCheckGravity(KEYCODE_DPAD_UP, Gravity.TOP | Gravity.CENTER_HORIZONTAL, true); + moveAndCheckGravity(KEYCODE_DPAD_DOWN, Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL, true); + } + + @Test + public void updateGravity_move_regular_invalid() { + assumeTelevision(); + int gravity = Gravity.BOTTOM | Gravity.RIGHT; + mTvPipBoundsState.setTvPipGravity(gravity); + moveAndCheckGravity(KEYCODE_DPAD_DOWN, gravity, false); + moveAndCheckGravity(KEYCODE_DPAD_RIGHT, gravity, false); + + gravity = Gravity.BOTTOM | Gravity.LEFT; + mTvPipBoundsState.setTvPipGravity(gravity); + moveAndCheckGravity(KEYCODE_DPAD_DOWN, gravity, false); + moveAndCheckGravity(KEYCODE_DPAD_LEFT, gravity, false); + + gravity = Gravity.TOP | Gravity.LEFT; + mTvPipBoundsState.setTvPipGravity(gravity); + moveAndCheckGravity(KEYCODE_DPAD_UP, gravity, false); + moveAndCheckGravity(KEYCODE_DPAD_LEFT, gravity, false); + + gravity = Gravity.TOP | Gravity.RIGHT; + mTvPipBoundsState.setTvPipGravity(gravity); + moveAndCheckGravity(KEYCODE_DPAD_UP, gravity, false); + moveAndCheckGravity(KEYCODE_DPAD_RIGHT, gravity, false); + } + + @Test + public void updateGravity_move_expanded_invalid() { + assumeTelevision(); + mTvPipBoundsState.setTvPipExpanded(true); + + // Vertical expanded PiP. + mTvPipBoundsState.setDesiredTvExpandedAspectRatio(VERTICAL_EXPANDED_ASPECT_RATIO, true); + mTvPipBoundsState.setTvPipGravity(Gravity.CENTER_VERTICAL | Gravity.RIGHT); + moveAndCheckGravity(KEYCODE_DPAD_RIGHT, Gravity.CENTER_VERTICAL | Gravity.RIGHT, false); + moveAndCheckGravity(KEYCODE_DPAD_UP, Gravity.CENTER_VERTICAL | Gravity.RIGHT, false); + moveAndCheckGravity(KEYCODE_DPAD_DOWN, Gravity.CENTER_VERTICAL | Gravity.RIGHT, false); + + mTvPipBoundsState.setTvPipGravity(Gravity.CENTER_VERTICAL | Gravity.LEFT); + moveAndCheckGravity(KEYCODE_DPAD_LEFT, Gravity.CENTER_VERTICAL | Gravity.LEFT, false); + moveAndCheckGravity(KEYCODE_DPAD_UP, Gravity.CENTER_VERTICAL | Gravity.LEFT, false); + moveAndCheckGravity(KEYCODE_DPAD_DOWN, Gravity.CENTER_VERTICAL | Gravity.LEFT, false); + + // Horizontal expanded PiP. + mTvPipBoundsState.setDesiredTvExpandedAspectRatio(HORIZONTAL_EXPANDED_ASPECT_RATIO, true); + mTvPipBoundsState.setTvPipGravity(Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL); + moveAndCheckGravity(KEYCODE_DPAD_DOWN, Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL, false); + moveAndCheckGravity(KEYCODE_DPAD_LEFT, Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL, false); + moveAndCheckGravity(KEYCODE_DPAD_RIGHT, Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL, false); + + mTvPipBoundsState.setTvPipGravity(Gravity.TOP | Gravity.CENTER_HORIZONTAL); + moveAndCheckGravity(KEYCODE_DPAD_UP, Gravity.TOP | Gravity.CENTER_HORIZONTAL, false); + moveAndCheckGravity(KEYCODE_DPAD_LEFT, Gravity.TOP | Gravity.CENTER_HORIZONTAL, false); + moveAndCheckGravity(KEYCODE_DPAD_RIGHT, Gravity.TOP | Gravity.CENTER_HORIZONTAL, false); + } + + @Test + public void previousCollapsedGravity_defaultValue() { + assumeTelevision(); + assertEquals(mTvPipBoundsState.getTvPipPreviousCollapsedGravity(), + mTvPipBoundsState.getDefaultGravity()); + setRTL(true); + assertEquals(mTvPipBoundsState.getTvPipPreviousCollapsedGravity(), + mTvPipBoundsState.getDefaultGravity()); + } + + @Test + public void previousCollapsedGravity_changes_on_RTL() { + assumeTelevision(); + mTvPipBoundsState.setTvPipPreviousCollapsedGravity(Gravity.TOP | Gravity.LEFT); + setRTL(true); + assertEquals(mTvPipBoundsState.getTvPipPreviousCollapsedGravity(), + Gravity.TOP | Gravity.RIGHT); + setRTL(false); + assertEquals(mTvPipBoundsState.getTvPipPreviousCollapsedGravity(), + Gravity.TOP | Gravity.LEFT); + + mTvPipBoundsState.setTvPipPreviousCollapsedGravity(Gravity.BOTTOM | Gravity.RIGHT); + setRTL(true); + assertEquals(mTvPipBoundsState.getTvPipPreviousCollapsedGravity(), + Gravity.BOTTOM | Gravity.LEFT); + setRTL(false); + assertEquals(mTvPipBoundsState.getTvPipPreviousCollapsedGravity(), + Gravity.BOTTOM | Gravity.RIGHT); + } + +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipKeepClearAlgorithmTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipKeepClearAlgorithmTest.kt index 0fcc5cf384c9..aedf65ddc269 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipKeepClearAlgorithmTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipKeepClearAlgorithmTest.kt @@ -21,23 +21,25 @@ import android.graphics.Rect import android.testing.AndroidTestingRunner import android.util.Size import android.view.Gravity -import org.junit.runner.RunWith -import com.android.wm.shell.pip.PipBoundsState.STASH_TYPE_NONE +import com.android.wm.shell.ShellTestCase import com.android.wm.shell.pip.PipBoundsState.STASH_TYPE_BOTTOM +import com.android.wm.shell.pip.PipBoundsState.STASH_TYPE_NONE import com.android.wm.shell.pip.PipBoundsState.STASH_TYPE_RIGHT import com.android.wm.shell.pip.PipBoundsState.STASH_TYPE_TOP import com.android.wm.shell.pip.tv.TvPipKeepClearAlgorithm.Placement -import org.junit.Before -import org.junit.Test import junit.framework.Assert.assertEquals import junit.framework.Assert.assertFalse import junit.framework.Assert.assertNull import junit.framework.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith @RunWith(AndroidTestingRunner::class) -class TvPipKeepClearAlgorithmTest { +class TvPipKeepClearAlgorithmTest : ShellTestCase() { private val DEFAULT_PIP_SIZE = Size(384, 216) - private val EXPANDED_WIDE_PIP_SIZE = Size(384*2, 216) + private val EXPANDED_WIDE_PIP_SIZE = Size(384 * 2, 216) + private val EXPANDED_TALL_PIP_SIZE = Size(384, 216 * 4) private val DASHBOARD_WIDTH = 484 private val BOTTOM_SHEET_HEIGHT = 524 private val STASH_OFFSET = 64 @@ -54,6 +56,9 @@ class TvPipKeepClearAlgorithmTest { @Before fun setup() { + if (!isTelevision) { + return + } movementBounds = Rect(0, 0, SCREEN_SIZE.width, SCREEN_SIZE.height) movementBounds.inset(SCREEN_EDGE_INSET, SCREEN_EDGE_INSET) @@ -73,72 +78,84 @@ class TvPipKeepClearAlgorithmTest { @Test fun testAnchorPosition_BottomRight() { + assumeTelevision() gravity = Gravity.BOTTOM or Gravity.RIGHT testAnchorPosition() } @Test fun testAnchorPosition_TopRight() { + assumeTelevision() gravity = Gravity.TOP or Gravity.RIGHT testAnchorPosition() } @Test fun testAnchorPosition_TopLeft() { + assumeTelevision() gravity = Gravity.TOP or Gravity.LEFT testAnchorPosition() } @Test fun testAnchorPosition_BottomLeft() { + assumeTelevision() gravity = Gravity.BOTTOM or Gravity.LEFT testAnchorPosition() } @Test fun testAnchorPosition_Right() { + assumeTelevision() gravity = Gravity.RIGHT testAnchorPosition() } @Test fun testAnchorPosition_Left() { + assumeTelevision() gravity = Gravity.LEFT testAnchorPosition() } @Test fun testAnchorPosition_Top() { + assumeTelevision() gravity = Gravity.TOP testAnchorPosition() } @Test fun testAnchorPosition_Bottom() { + assumeTelevision() gravity = Gravity.BOTTOM testAnchorPosition() } @Test fun testAnchorPosition_TopCenterHorizontal() { + assumeTelevision() gravity = Gravity.TOP or Gravity.CENTER_HORIZONTAL testAnchorPosition() } @Test fun testAnchorPosition_BottomCenterHorizontal() { + assumeTelevision() gravity = Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL testAnchorPosition() } @Test fun testAnchorPosition_RightCenterVertical() { + assumeTelevision() gravity = Gravity.RIGHT or Gravity.CENTER_VERTICAL testAnchorPosition() } @Test fun testAnchorPosition_LeftCenterVertical() { + assumeTelevision() gravity = Gravity.LEFT or Gravity.CENTER_VERTICAL testAnchorPosition() } @@ -152,6 +169,7 @@ class TvPipKeepClearAlgorithmTest { @Test fun test_AnchorBottomRight_KeepClearNotObstructing_StayAtAnchor() { + assumeTelevision() gravity = Gravity.BOTTOM or Gravity.RIGHT val sidebar = makeSideBar(DASHBOARD_WIDTH, Gravity.LEFT) @@ -166,6 +184,7 @@ class TvPipKeepClearAlgorithmTest { @Test fun test_AnchorBottomRight_UnrestrictedRightSidebar_PushedLeft() { + assumeTelevision() gravity = Gravity.BOTTOM or Gravity.RIGHT val sidebar = makeSideBar(DASHBOARD_WIDTH, Gravity.RIGHT) @@ -180,6 +199,7 @@ class TvPipKeepClearAlgorithmTest { @Test fun test_AnchorTopRight_UnrestrictedRightSidebar_PushedLeft() { + assumeTelevision() gravity = Gravity.TOP or Gravity.RIGHT val sidebar = makeSideBar(DASHBOARD_WIDTH, Gravity.RIGHT) @@ -194,6 +214,7 @@ class TvPipKeepClearAlgorithmTest { @Test fun test_AnchorBottomLeft_UnrestrictedRightSidebar_StayAtAnchor() { + assumeTelevision() gravity = Gravity.BOTTOM or Gravity.LEFT val sidebar = makeSideBar(DASHBOARD_WIDTH, Gravity.RIGHT) @@ -208,6 +229,7 @@ class TvPipKeepClearAlgorithmTest { @Test fun test_AnchorBottom_UnrestrictedRightSidebar_StayAtAnchor() { + assumeTelevision() gravity = Gravity.BOTTOM val sidebar = makeSideBar(DASHBOARD_WIDTH, Gravity.RIGHT) @@ -222,6 +244,7 @@ class TvPipKeepClearAlgorithmTest { @Test fun testExpanded_AnchorBottom_UnrestrictedRightSidebar_StayAtAnchor() { + assumeTelevision() pipSize = EXPANDED_WIDE_PIP_SIZE gravity = Gravity.BOTTOM @@ -237,6 +260,7 @@ class TvPipKeepClearAlgorithmTest { @Test fun test_AnchorBottomRight_RestrictedSmallBottomBar_PushedUp() { + assumeTelevision() gravity = Gravity.BOTTOM or Gravity.RIGHT val bottomBar = makeBottomBar(96) @@ -252,6 +276,7 @@ class TvPipKeepClearAlgorithmTest { @Test fun test_AnchorBottomRight_RestrictedBottomSheet_StashDownAtAnchor() { + assumeTelevision() gravity = Gravity.BOTTOM or Gravity.RIGHT val bottomBar = makeBottomBar(BOTTOM_SHEET_HEIGHT) @@ -269,6 +294,7 @@ class TvPipKeepClearAlgorithmTest { @Test fun test_AnchorBottomRight_UnrestrictedBottomSheet_PushUp() { + assumeTelevision() gravity = Gravity.BOTTOM or Gravity.RIGHT val bottomBar = makeBottomBar(BOTTOM_SHEET_HEIGHT) @@ -284,6 +310,7 @@ class TvPipKeepClearAlgorithmTest { @Test fun test_AnchorBottomRight_UnrestrictedBottomSheet_RestrictedSidebar_StashAboveBottomSheet() { + assumeTelevision() gravity = Gravity.BOTTOM or Gravity.RIGHT val bottomBar = makeBottomBar(BOTTOM_SHEET_HEIGHT) @@ -309,6 +336,7 @@ class TvPipKeepClearAlgorithmTest { @Test fun test_AnchorBottomRight_UnrestrictedBottomSheet_UnrestrictedSidebar_PushUpLeft() { + assumeTelevision() gravity = Gravity.BOTTOM or Gravity.RIGHT val bottomBar = makeBottomBar(BOTTOM_SHEET_HEIGHT) @@ -331,6 +359,7 @@ class TvPipKeepClearAlgorithmTest { @Test fun test_Stashed_UnstashBoundsBecomeUnobstructed_Unstashes() { + assumeTelevision() gravity = Gravity.BOTTOM or Gravity.RIGHT val bottomBar = makeBottomBar(BOTTOM_SHEET_HEIGHT) @@ -361,6 +390,7 @@ class TvPipKeepClearAlgorithmTest { @Test fun test_Stashed_UnstashBoundsStaysObstructed_DoesNotTriggerStash() { + assumeTelevision() gravity = Gravity.BOTTOM or Gravity.RIGHT val bottomBar = makeBottomBar(BOTTOM_SHEET_HEIGHT) @@ -392,6 +422,7 @@ class TvPipKeepClearAlgorithmTest { @Test fun test_Stashed_UnstashBoundsObstructionChanges_UnstashTimeExtended() { + assumeTelevision() gravity = Gravity.BOTTOM or Gravity.RIGHT val bottomBar = makeBottomBar(BOTTOM_SHEET_HEIGHT) @@ -431,6 +462,7 @@ class TvPipKeepClearAlgorithmTest { @Test fun test_ExpandedPiPHeightExceedsMovementBounds_AtAnchor() { + assumeTelevision() gravity = Gravity.RIGHT or Gravity.CENTER_VERTICAL pipSize = Size(DEFAULT_PIP_SIZE.width, SCREEN_SIZE.height) testAnchorPosition() @@ -438,6 +470,7 @@ class TvPipKeepClearAlgorithmTest { @Test fun test_ExpandedPiPHeightExceedsMovementBounds_BottomBar_StashedUp() { + assumeTelevision() gravity = Gravity.RIGHT or Gravity.CENTER_VERTICAL pipSize = Size(DEFAULT_PIP_SIZE.width, SCREEN_SIZE.height) val bottomBar = makeBottomBar(96) @@ -453,6 +486,7 @@ class TvPipKeepClearAlgorithmTest { @Test fun test_PipInsets() { + assumeTelevision() val insets = Insets.of(-1, -2, -3, -4) algorithm.setPipPermanentDecorInsets(insets) @@ -485,6 +519,41 @@ class TvPipKeepClearAlgorithmTest { testAnchorPositionWithInsets(insets) } + @Test + fun test_AnchorRightExpandedPiP_UnrestrictedRightSidebar_PushedLeft() { + assumeTelevision() + pipSize = EXPANDED_TALL_PIP_SIZE + gravity = Gravity.RIGHT + + val sidebar = makeSideBar(DASHBOARD_WIDTH, Gravity.RIGHT) + unrestrictedAreas.add(sidebar) + + val expectedBounds = anchorBoundsOffsetBy(SCREEN_EDGE_INSET - sidebar.width() - PADDING, 0) + + val placement = getActualPlacement() + assertEquals(expectedBounds, placement.bounds) + assertNotStashed(placement) + } + + @Test + fun test_AnchorRightExpandedPiP_RestrictedRightSidebar_StashedRight() { + assumeTelevision() + assumeTelevision() + pipSize = EXPANDED_TALL_PIP_SIZE + gravity = Gravity.RIGHT + + val sidebar = makeSideBar(DASHBOARD_WIDTH, Gravity.RIGHT) + restrictedAreas.add(sidebar) + + val expectedBounds = getExpectedAnchorBounds() + expectedBounds.offsetTo(SCREEN_SIZE.width - STASH_OFFSET, expectedBounds.top) + + val placement = getActualPlacement() + assertEquals(expectedBounds, placement.bounds) + assertEquals(STASH_TYPE_RIGHT, placement.stashType) + assertEquals(getExpectedAnchorBounds(), placement.unstashDestinationBounds) + } + private fun testAnchorPositionWithInsets(insets: Insets) { var pipRect = Rect(0, 0, pipSize.width, pipSize.height) pipRect.inset(insets) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipMenuControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipMenuControllerTest.java new file mode 100644 index 000000000000..3a08d32bc430 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipMenuControllerTest.java @@ -0,0 +1,336 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.pip.tv; + +import static android.view.KeyEvent.KEYCODE_DPAD_UP; + +import static com.android.wm.shell.pip.tv.TvPipMenuController.MODE_ALL_ACTIONS_MENU; +import static com.android.wm.shell.pip.tv.TvPipMenuController.MODE_MOVE_MENU; +import static com.android.wm.shell.pip.tv.TvPipMenuController.MODE_NO_MENU; + +import static org.junit.Assert.assertTrue; +import static org.junit.Assume.assumeTrue; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import android.os.Handler; +import android.view.SurfaceControl; + +import com.android.wm.shell.ShellTestCase; +import com.android.wm.shell.common.SystemWindows; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +public class TvPipMenuControllerTest extends ShellTestCase { + private static final int TEST_MOVE_KEYCODE = KEYCODE_DPAD_UP; + + @Mock + private TvPipMenuController.Delegate mMockDelegate; + @Mock + private TvPipBoundsState mMockTvPipBoundsState; + @Mock + private SystemWindows mMockSystemWindows; + @Mock + private SurfaceControl mMockPipLeash; + @Mock + private Handler mMockHandler; + @Mock + private TvPipActionsProvider mMockActionsProvider; + @Mock + private TvPipMenuView mMockTvPipMenuView; + @Mock + private TvPipBackgroundView mMockTvPipBackgroundView; + + private TvPipMenuController mTvPipMenuController; + + @Before + public void setUp() { + assumeTrue(isTelevision()); + + MockitoAnnotations.initMocks(this); + + mTvPipMenuController = new TestTvPipMenuController(); + mTvPipMenuController.setDelegate(mMockDelegate); + mTvPipMenuController.setTvPipActionsProvider(mMockActionsProvider); + mTvPipMenuController.attach(mMockPipLeash); + } + + @Test + public void testMenuNotOpenByDefault() { + assertMenuIsOpen(false); + } + + @Test + public void testSwitch_FromNoMenuMode_ToMoveMode() { + showAndAssertMoveMenu(); + } + + @Test + public void testSwitch_FromNoMenuMode_ToAllActionsMode() { + showAndAssertAllActionsMenu(); + } + + @Test + public void testSwitch_FromMoveMode_ToAllActionsMode() { + showAndAssertMoveMenu(); + showAndAssertAllActionsMenu(); + } + + @Test + public void testSwitch_FromAllActionsMode_ToMoveMode() { + showAndAssertAllActionsMenu(); + showAndAssertMoveMenu(); + } + + @Test + public void testCloseMenu_NoMenuMode() { + mTvPipMenuController.closeMenu(); + assertMenuIsOpen(false); + verify(mMockDelegate, never()).onMenuClosed(); + } + + @Test + public void testCloseMenu_MoveMode() { + showAndAssertMoveMenu(); + + closeMenuAndAssertMenuClosed(); + verify(mMockDelegate, times(2)).onInMoveModeChanged(); + } + + @Test + public void testCloseMenu_AllActionsMode() { + showAndAssertAllActionsMenu(); + + closeMenuAndAssertMenuClosed(); + } + + @Test + public void testCloseMenu_MoveModeFollowedByAllActionsMode() { + showAndAssertMoveMenu(); + showAndAssertAllActionsMenu(); + verify(mMockDelegate, times(2)).onInMoveModeChanged(); + + closeMenuAndAssertMenuClosed(); + } + + @Test + public void testCloseMenu_AllActionsModeFollowedByMoveMode() { + showAndAssertAllActionsMenu(); + showAndAssertMoveMenu(); + + closeMenuAndAssertMenuClosed(); + verify(mMockDelegate, times(2)).onInMoveModeChanged(); + } + + @Test + public void testExitMoveMode_NoMenuMode() { + mTvPipMenuController.onExitMoveMode(); + assertMenuIsOpen(false); + verify(mMockDelegate, never()).onMenuClosed(); + } + + @Test + public void testExitMoveMode_MoveMode() { + showAndAssertMoveMenu(); + + mTvPipMenuController.onExitMoveMode(); + assertMenuClosed(); + verify(mMockDelegate, times(2)).onInMoveModeChanged(); + } + + @Test + public void testExitMoveMode_AllActionsMode() { + showAndAssertAllActionsMenu(); + + mTvPipMenuController.onExitMoveMode(); + assertMenuIsInAllActionsMode(); + + } + + @Test + public void testExitMoveMode_AllActionsModeFollowedByMoveMode() { + showAndAssertAllActionsMenu(); + showAndAssertMoveMenu(); + + mTvPipMenuController.onExitMoveMode(); + assertMenuIsInAllActionsMode(); + verify(mMockDelegate, times(2)).onInMoveModeChanged(); + verify(mMockTvPipMenuView).transitionToMenuMode(eq(MODE_ALL_ACTIONS_MENU), eq(false)); + verify(mMockTvPipBackgroundView, times(2)).transitionToMenuMode(eq(MODE_ALL_ACTIONS_MENU)); + } + + @Test + public void testOnBackPress_NoMenuMode() { + mTvPipMenuController.onBackPress(); + assertMenuIsOpen(false); + verify(mMockDelegate, never()).onMenuClosed(); + } + + @Test + public void testOnBackPress_MoveMode() { + showAndAssertMoveMenu(); + + pressBackAndAssertMenuClosed(); + verify(mMockDelegate, times(2)).onInMoveModeChanged(); + } + + @Test + public void testOnBackPress_AllActionsMode() { + showAndAssertAllActionsMenu(); + + pressBackAndAssertMenuClosed(); + } + + @Test + public void testOnBackPress_MoveModeFollowedByAllActionsMode() { + showAndAssertMoveMenu(); + showAndAssertAllActionsMenu(); + verify(mMockDelegate, times(2)).onInMoveModeChanged(); + + pressBackAndAssertMenuClosed(); + } + + @Test + public void testOnBackPress_AllActionsModeFollowedByMoveMode() { + showAndAssertAllActionsMenu(); + showAndAssertMoveMenu(); + + mTvPipMenuController.onBackPress(); + assertMenuIsInAllActionsMode(); + verify(mMockDelegate, times(2)).onInMoveModeChanged(); + verify(mMockTvPipMenuView).transitionToMenuMode(eq(MODE_ALL_ACTIONS_MENU), eq(false)); + verify(mMockTvPipBackgroundView, times(2)).transitionToMenuMode(eq(MODE_ALL_ACTIONS_MENU)); + + pressBackAndAssertMenuClosed(); + } + + @Test + public void testOnPipMovement_NoMenuMode() { + assertPipMoveSuccessful(false, mTvPipMenuController.onPipMovement(TEST_MOVE_KEYCODE)); + } + + @Test + public void testOnPipMovement_MoveMode() { + showAndAssertMoveMenu(); + assertPipMoveSuccessful(true, mTvPipMenuController.onPipMovement(TEST_MOVE_KEYCODE)); + verify(mMockDelegate).movePip(eq(TEST_MOVE_KEYCODE)); + } + + @Test + public void testOnPipMovement_AllActionsMode() { + showAndAssertAllActionsMenu(); + assertPipMoveSuccessful(false, mTvPipMenuController.onPipMovement(TEST_MOVE_KEYCODE)); + } + + @Test + public void testOnPipWindowFocusChanged_NoMenuMode() { + mTvPipMenuController.onPipWindowFocusChanged(false); + assertMenuIsOpen(false); + } + + @Test + public void testOnPipWindowFocusChanged_MoveMode() { + showAndAssertMoveMenu(); + mTvPipMenuController.onPipWindowFocusChanged(false); + assertMenuClosed(); + } + + @Test + public void testOnPipWindowFocusChanged_AllActionsMode() { + showAndAssertAllActionsMenu(); + mTvPipMenuController.onPipWindowFocusChanged(false); + assertMenuClosed(); + } + + private void showAndAssertMoveMenu() { + mTvPipMenuController.showMovementMenu(); + assertMenuIsInMoveMode(); + verify(mMockDelegate).onInMoveModeChanged(); + verify(mMockTvPipMenuView).transitionToMenuMode(eq(MODE_MOVE_MENU), eq(false)); + verify(mMockTvPipBackgroundView).transitionToMenuMode(eq(MODE_MOVE_MENU)); + } + + private void showAndAssertAllActionsMenu() { + mTvPipMenuController.showMenu(); + assertMenuIsInAllActionsMode(); + verify(mMockTvPipMenuView).transitionToMenuMode(eq(MODE_ALL_ACTIONS_MENU), eq(true)); + verify(mMockTvPipBackgroundView).transitionToMenuMode(eq(MODE_ALL_ACTIONS_MENU)); + } + + private void closeMenuAndAssertMenuClosed() { + mTvPipMenuController.closeMenu(); + assertMenuClosed(); + } + + private void pressBackAndAssertMenuClosed() { + mTvPipMenuController.onBackPress(); + assertMenuClosed(); + } + + private void assertMenuClosed() { + assertMenuIsOpen(false); + verify(mMockDelegate).onMenuClosed(); + verify(mMockTvPipMenuView).transitionToMenuMode(eq(MODE_NO_MENU), eq(false)); + verify(mMockTvPipBackgroundView).transitionToMenuMode(eq(MODE_NO_MENU)); + } + + private void assertMenuIsOpen(boolean open) { + assertTrue("The TV PiP menu should " + (open ? "" : "not ") + "be open, but it" + + " is in mode " + mTvPipMenuController.getMenuModeString(), + mTvPipMenuController.isMenuOpen() == open); + } + + private void assertMenuIsInMoveMode() { + assertTrue("Expected MODE_MOVE_MENU, but got " + mTvPipMenuController.getMenuModeString(), + mTvPipMenuController.isInMoveMode()); + assertMenuIsOpen(true); + } + + private void assertMenuIsInAllActionsMode() { + assertTrue("Expected MODE_ALL_ACTIONS_MENU, but got " + + mTvPipMenuController.getMenuModeString(), + mTvPipMenuController.isInAllActionsMode()); + assertMenuIsOpen(true); + } + + private void assertPipMoveSuccessful(boolean expected, boolean actual) { + assertTrue("Should " + (expected ? "" : "not ") + "move PiP when the menu is in mode " + + mTvPipMenuController.getMenuModeString(), expected == actual); + } + + private class TestTvPipMenuController extends TvPipMenuController { + + TestTvPipMenuController() { + super(mContext, mMockTvPipBoundsState, mMockSystemWindows, mMockHandler); + } + + @Override + TvPipMenuView createTvPipMenuView() { + return mMockTvPipMenuView; + } + + @Override + TvPipBackgroundView createTvPipBackgroundView() { + return mMockTvPipBackgroundView; + } + } +} 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..b542fae060d1 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,51 @@ 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.assertFalse; 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.eq; +import static org.mockito.ArgumentMatchers.isA; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import static java.lang.Integer.MAX_VALUE; import android.app.ActivityManager; +import android.app.ActivityTaskManager; import android.content.Context; +import android.content.pm.PackageManager; import android.graphics.Rect; +import android.os.Bundle; import android.view.SurfaceControl; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; +import com.android.dx.mockito.inline.extended.StaticMockitoSession; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.TestShellExecutor; -import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.TaskStackListenerImpl; +import com.android.wm.shell.desktopmode.DesktopModeStatus; +import com.android.wm.shell.desktopmode.DesktopModeTaskRepository; +import com.android.wm.shell.sysui.ShellCommandHandler; +import com.android.wm.shell.sysui.ShellController; +import com.android.wm.shell.sysui.ShellInit; +import com.android.wm.shell.sysui.ShellSharedConstants; import com.android.wm.shell.util.GroupedRecentTaskInfo; -import com.android.wm.shell.util.StagedSplitBounds; +import com.android.wm.shell.util.SplitBounds; import org.junit.Before; import org.junit.Test; @@ -56,7 +73,9 @@ import org.mockito.Mock; import java.util.ArrayList; import java.util.Arrays; +import java.util.List; import java.util.Optional; +import java.util.function.Consumer; /** * Tests for {@link RecentTasksController}. @@ -69,18 +88,66 @@ public class RecentTasksControllerTest extends ShellTestCase { private Context mContext; @Mock private TaskStackListenerImpl mTaskStackListener; + @Mock + private ShellCommandHandler mShellCommandHandler; + @Mock + private DesktopModeTaskRepository mDesktopModeTaskRepository; + @Mock + private ActivityTaskManager mActivityTaskManager; private ShellTaskOrganizer mShellTaskOrganizer; private RecentTasksController mRecentTasksController; - private ShellExecutor mMainExecutor; + private RecentTasksController mRecentTasksControllerReal; + private ShellInit mShellInit; + private ShellController mShellController; + private TestShellExecutor 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)); + mShellController = spy(new ShellController(mShellInit, mShellCommandHandler, mMainExecutor)); - mShellTaskOrganizer = new ShellTaskOrganizer(mMainExecutor, mContext, - null /* sizeCompatUI */, Optional.of(mRecentTasksController)); + mRecentTasksControllerReal = new RecentTasksController(mContext, mShellInit, + mShellController, mShellCommandHandler, mTaskStackListener, mActivityTaskManager, + Optional.of(mDesktopModeTaskRepository), mMainExecutor); + mRecentTasksController = spy(mRecentTasksControllerReal); + 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 + public void instantiateController_addExternalInterface() { + verify(mShellController, times(1)).addExternalInterface( + eq(ShellSharedConstants.KEY_EXTRA_SHELL_RECENT_TASKS), any(), any()); + } + + @Test + public void testInvalidateExternalInterface_unregistersListener() { + // Note: We have to use the real instance of the controller here since that is the instance + // that is passed to ShellController internally, and the instance that the listener will be + // unregistered from + mRecentTasksControllerReal.registerRecentTasksListener(new IRecentTasksListener.Default()); + assertTrue(mRecentTasksControllerReal.hasRecentTasksListener()); + // Create initial interface + mShellController.createExternalInterfaces(new Bundle()); + // Recreate the interface to trigger invalidation of the previous instance + mShellController.createExternalInterfaces(new Bundle()); + assertFalse(mRecentTasksControllerReal.hasRecentTasksListener()); } @Test @@ -89,7 +156,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 +171,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 +206,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 +222,110 @@ public class RecentTasksControllerTest extends ShellTestCase { } @Test + public void testGetRecentTasks_ReturnsRecentTasksAsynchronously() { + @SuppressWarnings("unchecked") + final List<GroupedRecentTaskInfo>[] recentTasks = new List[1]; + Consumer<List<GroupedRecentTaskInfo>> consumer = argument -> recentTasks[0] = argument; + ActivityManager.RecentTaskInfo t1 = makeTaskInfo(1); + ActivityManager.RecentTaskInfo t2 = makeTaskInfo(2); + ActivityManager.RecentTaskInfo t3 = makeTaskInfo(3); + ActivityManager.RecentTaskInfo t4 = makeTaskInfo(4); + ActivityManager.RecentTaskInfo t5 = makeTaskInfo(5); + ActivityManager.RecentTaskInfo t6 = makeTaskInfo(6); + setRawList(t1, t2, t3, t4, t5, t6); + + // Mark a couple pairs [t2, t4], [t3, t5] + SplitBounds pair1Bounds = new SplitBounds(new Rect(), new Rect(), 2, 4); + SplitBounds pair2Bounds = new SplitBounds(new Rect(), new Rect(), 3, 5); + + mRecentTasksController.addSplitPair(t2.taskId, t4.taskId, pair1Bounds); + mRecentTasksController.addSplitPair(t3.taskId, t5.taskId, pair2Bounds); + + mRecentTasksController.asRecentTasks() + .getRecentTasks(MAX_VALUE, RECENT_IGNORE_UNAVAILABLE, 0, Runnable::run, consumer); + mMainExecutor.flushAll(); + + assertGroupedTasksListEquals(recentTasks[0], + t1.taskId, -1, + t2.taskId, t4.taskId, + t3.taskId, t5.taskId, + t6.taskId, -1); + } + + @Test + public void testGetRecentTasks_hasActiveDesktopTasks_proto2Enabled_groupFreeformTasks() { + StaticMockitoSession mockitoSession = mockitoSession().mockStatic( + DesktopModeStatus.class).startMocking(); + when(DesktopModeStatus.isProto2Enabled()).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 testGetRecentTasks_hasActiveDesktopTasks_proto2Disabled_doNotGroupFreeformTasks() { + StaticMockitoSession mockitoSession = mockitoSession().mockStatic( + DesktopModeStatus.class).startMocking(); + when(DesktopModeStatus.isProto2Enabled()).thenReturn(false); + + 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); + + // Expect no grouping of tasks + assertEquals(4, recentTasks.size()); + assertEquals(GroupedRecentTaskInfo.TYPE_SINGLE, recentTasks.get(0).getType()); + assertEquals(GroupedRecentTaskInfo.TYPE_SINGLE, recentTasks.get(1).getType()); + assertEquals(GroupedRecentTaskInfo.TYPE_SINGLE, recentTasks.get(2).getType()); + assertEquals(GroupedRecentTaskInfo.TYPE_SINGLE, recentTasks.get(3).getType()); + + assertEquals(t1, recentTasks.get(0).getTaskInfo1()); + assertEquals(t2, recentTasks.get(1).getTaskInfo1()); + assertEquals(t3, recentTasks.get(2).getTaskInfo1()); + assertEquals(t4, recentTasks.get(3).getTaskInfo1()); + + mockitoSession.finishMocking(); + } + + @Test public void testRemovedTaskRemovesSplit() { ActivityManager.RecentTaskInfo t1 = makeTaskInfo(1); ActivityManager.RecentTaskInfo t2 = makeTaskInfo(2); @@ -162,7 +333,7 @@ public class RecentTasksControllerTest extends ShellTestCase { setRawList(t1, t2, t3); // Add a pair - StagedSplitBounds pair1Bounds = new StagedSplitBounds(new Rect(), new Rect(), 2, 3); + SplitBounds pair1Bounds = new SplitBounds(new Rect(), new Rect(), 2, 3); mRecentTasksController.addSplitPair(t2.taskId, t3.taskId, pair1Bounds); reset(mRecentTasksController); @@ -223,37 +394,39 @@ public class RecentTasksControllerTest extends ShellTestCase { for (ActivityManager.RecentTaskInfo task : tasks) { rawList.add(task); } - doReturn(rawList).when(mRecentTasksController).getRawRecentTasks(anyInt(), anyInt(), + doReturn(rawList).when(mActivityTaskManager).getRecentTasks(anyInt(), anyInt(), anyInt()); return rawList; } /** * Asserts that the recent tasks matches the given task ids. + * * @param expectedTaskIds list of task ids that map to the flattened task ids of the tasks in * the grouped task list */ - private void assertGroupedTasksListEquals(ArrayList<GroupedRecentTaskInfo> recentTasks, + private void assertGroupedTasksListEquals(List<GroupedRecentTaskInfo> recentTasks, int... expectedTaskIds) { int[] flattenedTaskIds = new int[recentTasks.size() * 2]; for (int i = 0; i < recentTasks.size(); i++) { GroupedRecentTaskInfo pair = recentTasks.get(i); - int taskId1 = pair.mTaskInfo1.taskId; + int taskId1 = pair.getTaskInfo1().taskId; flattenedTaskIds[2 * i] = taskId1; - flattenedTaskIds[2 * i + 1] = pair.mTaskInfo2 != null - ? pair.mTaskInfo2.taskId + flattenedTaskIds[2 * i + 1] = pair.getTaskInfo2() != null + ? pair.getTaskInfo2().taskId : -1; - if (pair.mTaskInfo2 != null) { - assertNotNull(pair.mStagedSplitBounds); - int leftTopTaskId = pair.mStagedSplitBounds.leftTopTaskId; - int bottomRightTaskId = pair.mStagedSplitBounds.rightBottomTaskId; + if (pair.getTaskInfo2() != null) { + assertNotNull(pair.getSplitBounds()); + int leftTopTaskId = pair.getSplitBounds().leftTopTaskId; + int bottomRightTaskId = pair.getSplitBounds().rightBottomTaskId; // Unclear if pairs are ordered by split position, most likely not. - assertTrue(leftTopTaskId == taskId1 || leftTopTaskId == pair.mTaskInfo2.taskId); + assertTrue(leftTopTaskId == taskId1 + || leftTopTaskId == pair.getTaskInfo2().taskId); assertTrue(bottomRightTaskId == taskId1 - || bottomRightTaskId == pair.mTaskInfo2.taskId); + || bottomRightTaskId == pair.getTaskInfo2().taskId); } else { - assertNull(pair.mStagedSplitBounds); + assertNull(pair.getSplitBounds()); } } assertTrue("Expected: " + Arrays.toString(expectedTaskIds) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/StagedSplitBoundsTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/SplitBoundsTest.java index ad73c56950bd..50d02ae0dccd 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/StagedSplitBoundsTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/SplitBoundsTest.java @@ -9,7 +9,8 @@ import android.graphics.Rect; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; -import com.android.wm.shell.util.StagedSplitBounds; +import com.android.wm.shell.ShellTestCase; +import com.android.wm.shell.util.SplitBounds; import org.junit.Before; import org.junit.Test; @@ -17,7 +18,7 @@ import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) @SmallTest -public class StagedSplitBoundsTest { +public class SplitBoundsTest extends ShellTestCase { private static final int DEVICE_WIDTH = 100; private static final int DEVICE_LENGTH = 200; private static final int DIVIDER_SIZE = 20; @@ -42,21 +43,21 @@ public class StagedSplitBoundsTest { @Test public void testVerticalStacked() { - StagedSplitBounds ssb = new StagedSplitBounds(mTopRect, mBottomRect, + SplitBounds ssb = new SplitBounds(mTopRect, mBottomRect, TASK_ID_1, TASK_ID_2); assertTrue(ssb.appsStackedVertically); } @Test public void testHorizontalStacked() { - StagedSplitBounds ssb = new StagedSplitBounds(mLeftRect, mRightRect, + SplitBounds ssb = new SplitBounds(mLeftRect, mRightRect, TASK_ID_1, TASK_ID_2); assertFalse(ssb.appsStackedVertically); } @Test public void testHorizontalDividerBounds() { - StagedSplitBounds ssb = new StagedSplitBounds(mTopRect, mBottomRect, + SplitBounds ssb = new SplitBounds(mTopRect, mBottomRect, TASK_ID_1, TASK_ID_2); Rect dividerBounds = ssb.visualDividerBounds; assertEquals(0, dividerBounds.left); @@ -67,7 +68,7 @@ public class StagedSplitBoundsTest { @Test public void testVerticalDividerBounds() { - StagedSplitBounds ssb = new StagedSplitBounds(mLeftRect, mRightRect, + SplitBounds ssb = new SplitBounds(mLeftRect, mRightRect, TASK_ID_1, TASK_ID_2); Rect dividerBounds = ssb.visualDividerBounds; assertEquals(DEVICE_WIDTH / 2 - DIVIDER_SIZE / 2, dividerBounds.left); @@ -78,7 +79,7 @@ public class StagedSplitBoundsTest { @Test public void testEqualVerticalTaskPercent() { - StagedSplitBounds ssb = new StagedSplitBounds(mTopRect, mBottomRect, + SplitBounds ssb = new SplitBounds(mTopRect, mBottomRect, TASK_ID_1, TASK_ID_2); float topPercentSpaceTaken = (float) (DEVICE_LENGTH / 2 - DIVIDER_SIZE / 2) / DEVICE_LENGTH; assertEquals(topPercentSpaceTaken, ssb.topTaskPercent, 0.01); @@ -86,7 +87,7 @@ public class StagedSplitBoundsTest { @Test public void testEqualHorizontalTaskPercent() { - StagedSplitBounds ssb = new StagedSplitBounds(mLeftRect, mRightRect, + SplitBounds ssb = new SplitBounds(mLeftRect, mRightRect, TASK_ID_1, TASK_ID_2); float leftPercentSpaceTaken = (float) (DEVICE_WIDTH / 2 - DIVIDER_SIZE / 2) / DEVICE_WIDTH; assertEquals(leftPercentSpaceTaken, ssb.leftTaskPercent, 0.01); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/MainStageTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/MainStageTests.java index 0639ad5d0a62..68cb57c14d8c 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/MainStageTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/MainStageTests.java @@ -61,7 +61,7 @@ public class MainStageTests extends ShellTestCase { MockitoAnnotations.initMocks(this); mRootTaskInfo = new TestRunningTaskInfoBuilder().build(); mMainStage = new MainStage(mContext, mTaskOrganizer, DEFAULT_DISPLAY, mCallbacks, - mSyncQueue, mSurfaceSession, mIconProvider, null); + mSyncQueue, mSurfaceSession, mIconProvider); mMainStage.onTaskAppeared(mRootTaskInfo, mRootLeash); } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SideStageTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SideStageTests.java index a31aa58bdc26..3b42a48b5a40 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SideStageTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SideStageTests.java @@ -66,7 +66,7 @@ public class SideStageTests extends ShellTestCase { MockitoAnnotations.initMocks(this); mRootTask = new TestRunningTaskInfoBuilder().build(); mSideStage = new SideStage(mContext, mTaskOrganizer, DEFAULT_DISPLAY, mCallbacks, - mSyncQueue, mSurfaceSession, mIconProvider, null); + mSyncQueue, mSurfaceSession, mIconProvider); mSideStage.onTaskAppeared(mRootTask, mRootLeash); } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java new file mode 100644 index 000000000000..d0e26019f9bf --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java @@ -0,0 +1,293 @@ +/* + * 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.PendingIntent.FLAG_IMMUTABLE; +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.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK; +import static android.content.Intent.FLAG_ACTIVITY_NO_USER_ACTION; + +import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; +import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assume.assumeTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isA; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.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.app.ActivityTaskManager; +import android.app.PendingIntent; +import android.content.ComponentName; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.os.Bundle; + +import androidx.test.annotation.UiThreadTest; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import com.android.launcher3.icons.IconProvider; +import com.android.wm.shell.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.sysui.ShellSharedConstants; +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.Captor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** + * Tests for {@link SplitScreenController} + */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class SplitScreenControllerTests extends ShellTestCase { + + @Mock ShellInit mShellInit; + @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 StageCoordinator mStageCoordinator; + @Mock RecentTasksController mRecentTasks; + @Captor ArgumentCaptor<Intent> mIntentCaptor; + + private ShellController mShellController; + private SplitScreenController mSplitScreenController; + + @Before + public void setup() { + assumeTrue(ActivityTaskManager.supportsSplitScreenMultiWindow(mContext)); + MockitoAnnotations.initMocks(this); + mShellController = spy(new ShellController(mShellInit, mShellCommandHandler, + mMainExecutor)); + mSplitScreenController = spy(new SplitScreenController(mContext, mShellInit, + mShellCommandHandler, mShellController, mTaskOrganizer, mSyncQueue, + mRootTDAOrganizer, mDisplayController, mDisplayImeController, + mDisplayInsetsController, mDragAndDropController, mTransitions, mTransactionPool, + mIconProvider, mRecentTasks, mMainExecutor, mStageCoordinator)); + } + + @Test + public void instantiateController_addInitCallback() { + verify(mShellInit, times(1)).addInitCallback(any(), isA(SplitScreenController.class)); + } + + @Test + @UiThreadTest + 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 + @UiThreadTest + 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 + @UiThreadTest + public void testControllerRegistersKeyguardChangeListener() { + doReturn(mMainExecutor).when(mTaskOrganizer).getExecutor(); + when(mDisplayController.getDisplayLayout(anyInt())).thenReturn(new DisplayLayout()); + mSplitScreenController.onInit(); + verify(mShellController, times(1)).addKeyguardChangeListener(any()); + } + + @Test + @UiThreadTest + public void instantiateController_addExternalInterface() { + doReturn(mMainExecutor).when(mTaskOrganizer).getExecutor(); + when(mDisplayController.getDisplayLayout(anyInt())).thenReturn(new DisplayLayout()); + mSplitScreenController.onInit(); + verify(mShellController, times(1)).addExternalInterface( + eq(ShellSharedConstants.KEY_EXTRA_SHELL_SPLIT_SCREEN), any(), any()); + } + + @Test + public void testInvalidateExternalInterface_unregistersListener() { + mSplitScreenController.onInit(); + mSplitScreenController.registerSplitScreenListener( + new SplitScreen.SplitScreenListener() {}); + verify(mStageCoordinator).registerSplitScreenListener(any()); + // Create initial interface + mShellController.createExternalInterfaces(new Bundle()); + // Recreate the interface to trigger invalidation of the previous instance + mShellController.createExternalInterfaces(new Bundle()); + verify(mStageCoordinator).unregisterSplitScreenListener(any()); + } + + @Test + public void testStartIntent_appendsNoUserActionFlag() { + Intent startIntent = createStartIntent("startActivity"); + PendingIntent pendingIntent = + PendingIntent.getActivity(mContext, 0, startIntent, FLAG_IMMUTABLE); + + mSplitScreenController.startIntent(pendingIntent, null, SPLIT_POSITION_TOP_OR_LEFT, null); + + verify(mStageCoordinator).startIntent(eq(pendingIntent), mIntentCaptor.capture(), + eq(SPLIT_POSITION_TOP_OR_LEFT), isNull()); + assertEquals(FLAG_ACTIVITY_NO_USER_ACTION, + mIntentCaptor.getValue().getFlags() & FLAG_ACTIVITY_NO_USER_ACTION); + } + + @Test + public void startIntent_multiInstancesSupported_appendsMultipleTaskFag() { + doReturn(true).when(mSplitScreenController).supportMultiInstancesSplit(any()); + Intent startIntent = createStartIntent("startActivity"); + PendingIntent pendingIntent = + PendingIntent.getActivity(mContext, 0, startIntent, FLAG_IMMUTABLE); + // Put the same component to the top running task + ActivityManager.RunningTaskInfo topRunningTask = + createTaskInfo(WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, startIntent); + doReturn(topRunningTask).when(mRecentTasks).getTopRunningTask(); + + mSplitScreenController.startIntent(pendingIntent, null, SPLIT_POSITION_TOP_OR_LEFT, null); + + verify(mStageCoordinator).startIntent(eq(pendingIntent), mIntentCaptor.capture(), + eq(SPLIT_POSITION_TOP_OR_LEFT), isNull()); + assertEquals(FLAG_ACTIVITY_MULTIPLE_TASK, + mIntentCaptor.getValue().getFlags() & FLAG_ACTIVITY_MULTIPLE_TASK); + } + + @Test + public void startIntent_multiInstancesSupported_startTaskInBackgroundBeforeSplitActivated() { + doReturn(true).when(mSplitScreenController).supportMultiInstancesSplit(any()); + doNothing().when(mSplitScreenController).startTask(anyInt(), anyInt(), any()); + Intent startIntent = createStartIntent("startActivity"); + PendingIntent pendingIntent = + PendingIntent.getActivity(mContext, 0, startIntent, FLAG_IMMUTABLE); + // Put the same component to the top running task + ActivityManager.RunningTaskInfo topRunningTask = + createTaskInfo(WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, startIntent); + doReturn(topRunningTask).when(mRecentTasks).getTopRunningTask(); + // Put the same component into a task in the background + ActivityManager.RecentTaskInfo sameTaskInfo = new ActivityManager.RecentTaskInfo(); + doReturn(sameTaskInfo).when(mRecentTasks).findTaskInBackground(any()); + + mSplitScreenController.startIntent(pendingIntent, null, SPLIT_POSITION_TOP_OR_LEFT, null); + + verify(mSplitScreenController).startTask(anyInt(), eq(SPLIT_POSITION_TOP_OR_LEFT), + isNull()); + } + + @Test + public void startIntent_multiInstancesSupported_startTaskInBackgroundAfterSplitActivated() { + doReturn(true).when(mSplitScreenController).supportMultiInstancesSplit(any()); + doNothing().when(mSplitScreenController).startTask(anyInt(), anyInt(), any()); + Intent startIntent = createStartIntent("startActivity"); + PendingIntent pendingIntent = + PendingIntent.getActivity(mContext, 0, startIntent, FLAG_IMMUTABLE); + // Put the same component into another side of the split + doReturn(true).when(mSplitScreenController).isSplitScreenVisible(); + ActivityManager.RunningTaskInfo sameTaskInfo = + createTaskInfo(WINDOWING_MODE_MULTI_WINDOW, ACTIVITY_TYPE_STANDARD, startIntent); + doReturn(sameTaskInfo).when(mSplitScreenController).getTaskInfo( + SPLIT_POSITION_BOTTOM_OR_RIGHT); + // Put the same component into a task in the background + doReturn(new ActivityManager.RecentTaskInfo()).when(mRecentTasks) + .findTaskInBackground(any()); + + mSplitScreenController.startIntent(pendingIntent, null, SPLIT_POSITION_TOP_OR_LEFT, null); + + verify(mSplitScreenController).startTask(anyInt(), eq(SPLIT_POSITION_TOP_OR_LEFT), + isNull()); + } + + @Test + public void startIntent_multiInstancesNotSupported_switchesPositionAfterSplitActivated() { + doReturn(false).when(mSplitScreenController).supportMultiInstancesSplit(any()); + Intent startIntent = createStartIntent("startActivity"); + PendingIntent pendingIntent = + PendingIntent.getActivity(mContext, 0, startIntent, FLAG_IMMUTABLE); + // Put the same component into another side of the split + doReturn(true).when(mSplitScreenController).isSplitScreenVisible(); + ActivityManager.RunningTaskInfo sameTaskInfo = + createTaskInfo(WINDOWING_MODE_MULTI_WINDOW, ACTIVITY_TYPE_STANDARD, startIntent); + doReturn(sameTaskInfo).when(mSplitScreenController).getTaskInfo( + SPLIT_POSITION_BOTTOM_OR_RIGHT); + + mSplitScreenController.startIntent(pendingIntent, null, SPLIT_POSITION_TOP_OR_LEFT, null); + + verify(mStageCoordinator).switchSplitPosition(anyString()); + } + + 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..a9f311f9e9eb 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 @@ -35,6 +35,7 @@ import static com.android.wm.shell.transition.Transitions.TRANSIT_SPLIT_SCREEN_P import static org.junit.Assert.assertFalse; 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; @@ -64,6 +65,7 @@ import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.TestRunningTaskInfoBuilder; +import com.android.wm.shell.TransitionInfoBuilder; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayImeController; import com.android.wm.shell.common.DisplayInsetsController; @@ -95,7 +97,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 +119,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()); @@ -157,12 +158,10 @@ public class SplitTransitionTests extends ShellTestCase { assertTrue(containsSplitEnter(result)); // simulate the transition - TransitionInfo.Change openChange = createChange(TRANSIT_OPEN, newTask); - TransitionInfo.Change reparentChange = createChange(TRANSIT_CHANGE, reparentTask); - - TransitionInfo info = new TransitionInfo(TRANSIT_TO_FRONT, 0); - info.addChange(openChange); - info.addChange(reparentChange); + final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_TO_FRONT, 0) + .addChange(TRANSIT_OPEN, newTask) + .addChange(TRANSIT_CHANGE, reparentTask) + .build(); mSideStage.onTaskAppeared(newTask, createMockSurface()); mMainStage.onTaskAppeared(reparentTask, createMockSurface()); boolean accepted = mStageCoordinator.startAnimation(transition, info, @@ -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, "Test"), mStageCoordinator, null, null); mMainStage.onTaskAppeared(mMainChild, createMockSurface()); mSideStage.onTaskAppeared(mSideChild, createMockSurface()); boolean accepted = mStageCoordinator.startAnimation(transition, info, @@ -217,12 +216,10 @@ public class SplitTransitionTests extends ShellTestCase { assertFalse(containsSplitExit(result)); // simulate the transition - TransitionInfo.Change openChange = createChange(TRANSIT_TO_FRONT, newTask); - TransitionInfo.Change hideChange = createChange(TRANSIT_TO_BACK, mSideChild); - - TransitionInfo info = new TransitionInfo(TRANSIT_TO_FRONT, 0); - info.addChange(openChange); - info.addChange(hideChange); + TransitionInfo info = new TransitionInfoBuilder(TRANSIT_TO_FRONT, 0) + .addChange(TRANSIT_TO_FRONT, newTask) + .addChange(TRANSIT_TO_BACK, mSideChild) + .build(); mSideStage.onTaskAppeared(newTask, createMockSurface()); boolean accepted = mStageCoordinator.startAnimation(transition, info, mock(SurfaceControl.Transaction.class), @@ -238,12 +235,10 @@ public class SplitTransitionTests extends ShellTestCase { assertNotNull(result); assertFalse(containsSplitExit(result)); - TransitionInfo.Change showChange = createChange(TRANSIT_TO_FRONT, mSideChild); - TransitionInfo.Change closeChange = createChange(TRANSIT_CLOSE, newTask); - - info = new TransitionInfo(TRANSIT_CLOSE, 0); - info.addChange(showChange); - info.addChange(closeChange); + info = new TransitionInfoBuilder(TRANSIT_CLOSE, 0) + .addChange(TRANSIT_TO_FRONT, mSideChild) + .addChange(TRANSIT_CLOSE, newTask) + .build(); mSideStage.onTaskVanished(newTask); accepted = mStageCoordinator.startAnimation(transition, info, mock(SurfaceControl.Transaction.class), @@ -255,7 +250,7 @@ public class SplitTransitionTests extends ShellTestCase { @Test @UiThreadTest - public void testEnterRecents() { + public void testEnterRecentsAndCommit() { enterSplit(); ActivityManager.RunningTaskInfo homeTask = new TestRunningTaskInfoBuilder() @@ -264,69 +259,66 @@ public class SplitTransitionTests extends ShellTestCase { .build(); // Create a request to bring home forward - TransitionRequestInfo request = new TransitionRequestInfo(TRANSIT_TO_FRONT, homeTask, null); + TransitionRequestInfo request = new TransitionRequestInfo(TRANSIT_TO_FRONT, homeTask, + mock(RemoteTransition.class)); IBinder transition = mock(IBinder.class); WindowContainerTransaction result = mStageCoordinator.handleRequest(transition, request); - - assertTrue(result.isEmpty()); + // Don't handle recents opening + assertNull(result); // make sure we haven't made any local changes yet (need to wait until transition is ready) assertTrue(mStageCoordinator.isSplitScreenVisible()); - // simulate the transition - TransitionInfo.Change homeChange = createChange(TRANSIT_TO_FRONT, homeTask); - TransitionInfo.Change mainChange = createChange(TRANSIT_TO_BACK, mMainChild); - TransitionInfo.Change sideChange = createChange(TRANSIT_TO_BACK, mSideChild); - - TransitionInfo info = new TransitionInfo(TRANSIT_TO_FRONT, 0); - info.addChange(homeChange); - info.addChange(mainChange); - info.addChange(sideChange); + // simulate the start of recents transition mMainStage.onTaskVanished(mMainChild); mSideStage.onTaskVanished(mSideChild); - mStageCoordinator.startAnimation(transition, info, - mock(SurfaceControl.Transaction.class), - mock(SurfaceControl.Transaction.class), - mock(Transitions.TransitionFinishCallback.class)); + mStageCoordinator.onRecentsInSplitAnimationStart(mock(SurfaceControl.Transaction.class)); assertTrue(mStageCoordinator.isSplitScreenVisible()); + + // Make sure it cleans-up if recents doesn't restore + WindowContainerTransaction commitWCT = new WindowContainerTransaction(); + mStageCoordinator.onRecentsInSplitAnimationFinish(commitWCT, + mock(SurfaceControl.Transaction.class), mock(TransitionInfo.class)); + assertFalse(mStageCoordinator.isSplitScreenVisible()); } @Test @UiThreadTest - public void testDismissFromBeingOccluded() { + public void testEnterRecentsAndRestore() { enterSplit(); - ActivityManager.RunningTaskInfo normalTask = new TestRunningTaskInfoBuilder() + ActivityManager.RunningTaskInfo homeTask = new TestRunningTaskInfoBuilder() .setWindowingMode(WINDOWING_MODE_FULLSCREEN) + .setActivityType(ACTIVITY_TYPE_HOME) .build(); - // Create a request to bring a normal task forward - TransitionRequestInfo request = - new TransitionRequestInfo(TRANSIT_TO_FRONT, normalTask, null); + // Create a request to bring home forward + TransitionRequestInfo request = new TransitionRequestInfo(TRANSIT_TO_FRONT, homeTask, + mock(RemoteTransition.class)); IBinder transition = mock(IBinder.class); WindowContainerTransaction result = mStageCoordinator.handleRequest(transition, request); - - assertTrue(containsSplitExit(result)); + // Don't handle recents opening + assertNull(result); // make sure we haven't made any local changes yet (need to wait until transition is ready) assertTrue(mStageCoordinator.isSplitScreenVisible()); - // simulate the transition - TransitionInfo.Change normalChange = createChange(TRANSIT_TO_FRONT, normalTask); - TransitionInfo.Change mainChange = createChange(TRANSIT_TO_BACK, mMainChild); - TransitionInfo.Change sideChange = createChange(TRANSIT_TO_BACK, mSideChild); - - TransitionInfo info = new TransitionInfo(TRANSIT_TO_FRONT, 0); - info.addChange(normalChange); - info.addChange(mainChange); - info.addChange(sideChange); + // simulate the start of recents transition mMainStage.onTaskVanished(mMainChild); mSideStage.onTaskVanished(mSideChild); - mStageCoordinator.startAnimation(transition, info, - mock(SurfaceControl.Transaction.class), - mock(SurfaceControl.Transaction.class), - mock(Transitions.TransitionFinishCallback.class)); - assertFalse(mStageCoordinator.isSplitScreenVisible()); + mStageCoordinator.onRecentsInSplitAnimationStart(mock(SurfaceControl.Transaction.class)); + assertTrue(mStageCoordinator.isSplitScreenVisible()); + + // Make sure we remain in split after recents restores. + WindowContainerTransaction restoreWCT = new WindowContainerTransaction(); + restoreWCT.reorder(mMainChild.token, true /* toTop */); + restoreWCT.reorder(mSideChild.token, true /* toTop */); + // simulate the restoreWCT being applied: + mMainStage.onTaskAppeared(mMainChild, mock(SurfaceControl.class)); + mSideStage.onTaskAppeared(mSideChild, mock(SurfaceControl.class)); + mStageCoordinator.onRecentsInSplitAnimationFinish(restoreWCT, + mock(SurfaceControl.Transaction.class), mock(TransitionInfo.class)); + assertTrue(mStageCoordinator.isSplitScreenVisible()); } @Test @@ -335,12 +327,11 @@ public class SplitTransitionTests extends ShellTestCase { enterSplit(); // simulate the transition - TransitionInfo.Change mainChange = createChange(TRANSIT_TO_BACK, mMainChild); - TransitionInfo.Change sideChange = createChange(TRANSIT_TO_BACK, mSideChild); - TransitionInfo info = new TransitionInfo(TRANSIT_TO_BACK, 0); - info.addChange(mainChange); - info.addChange(sideChange); - IBinder transition = mSplitScreenTransitions.startDismissTransition(null, + TransitionInfo info = new TransitionInfoBuilder(TRANSIT_TO_BACK, 0) + .addChange(TRANSIT_TO_BACK, mMainChild) + .addChange(TRANSIT_TO_BACK, mSideChild) + .build(); + IBinder transition = mSplitScreenTransitions.startDismissTransition( new WindowContainerTransaction(), mStageCoordinator, EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW, STAGE_TYPE_SIDE); boolean accepted = mStageCoordinator.startAnimation(transition, info, @@ -357,13 +348,11 @@ public class SplitTransitionTests extends ShellTestCase { enterSplit(); // simulate the transition - TransitionInfo.Change mainChange = createChange(TRANSIT_TO_BACK, mMainChild); - TransitionInfo.Change sideChange = createChange(TRANSIT_CHANGE, mSideChild); - - TransitionInfo info = new TransitionInfo(TRANSIT_TO_BACK, 0); - info.addChange(mainChange); - info.addChange(sideChange); - IBinder transition = mSplitScreenTransitions.startDismissTransition(null, + TransitionInfo info = new TransitionInfoBuilder(TRANSIT_TO_BACK, 0) + .addChange(TRANSIT_TO_BACK, mMainChild) + .addChange(TRANSIT_CHANGE, mSideChild) + .build(); + IBinder transition = mSplitScreenTransitions.startDismissTransition( new WindowContainerTransaction(), mStageCoordinator, EXIT_REASON_DRAG_DIVIDER, STAGE_TYPE_SIDE); mMainStage.onTaskVanished(mMainChild); @@ -386,18 +375,17 @@ public class SplitTransitionTests extends ShellTestCase { IBinder transition = mock(IBinder.class); WindowContainerTransaction result = mStageCoordinator.handleRequest(transition, request); - assertTrue(containsSplitExit(result)); + // Don't reparent tasks until the animation is complete. + assertFalse(containsSplitExit(result)); // make sure we haven't made any local changes yet (need to wait until transition is ready) assertTrue(mStageCoordinator.isSplitScreenVisible()); // simulate the transition - TransitionInfo.Change mainChange = createChange(TRANSIT_CHANGE, mMainChild); - TransitionInfo.Change sideChange = createChange(TRANSIT_CLOSE, mSideChild); - - TransitionInfo info = new TransitionInfo(TRANSIT_CLOSE, 0); - info.addChange(mainChange); - info.addChange(sideChange); + TransitionInfo info = new TransitionInfoBuilder(TRANSIT_CLOSE, 0) + .addChange(TRANSIT_CHANGE, mMainChild) + .addChange(TRANSIT_CLOSE, mSideChild) + .build(); mMainStage.onTaskVanished(mMainChild); mSideStage.onTaskVanished(mSideChild); boolean accepted = mStageCoordinator.startAnimation(transition, info, @@ -409,20 +397,18 @@ public class SplitTransitionTests extends ShellTestCase { } private TransitionInfo createEnterPairInfo() { - TransitionInfo.Change mainChange = createChange(TRANSIT_OPEN, mMainChild); - TransitionInfo.Change sideChange = createChange(TRANSIT_OPEN, mSideChild); - - TransitionInfo info = new TransitionInfo(TRANSIT_SPLIT_SCREEN_PAIR_OPEN, 0); - info.addChange(mainChange); - info.addChange(sideChange); - return info; + return new TransitionInfoBuilder(TRANSIT_SPLIT_SCREEN_PAIR_OPEN, 0) + .addChange(TRANSIT_OPEN, mMainChild) + .addChange(TRANSIT_OPEN, mSideChild) + .build(); } private void enterSplit() { TransitionInfo enterInfo = createEnterPairInfo(); IBinder enterTransit = mSplitScreenTransitions.startEnterTransition( TRANSIT_SPLIT_SCREEN_PAIR_OPEN, new WindowContainerTransaction(), - new RemoteTransition(new TestRemoteTransition()), mStageCoordinator); + new RemoteTransition(new TestRemoteTransition(), "Test"), + mStageCoordinator, null, 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..eda6fdc4dbd4 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java @@ -16,7 +16,10 @@ package com.android.wm.shell.splitscreen; +import static android.app.ActivityOptions.KEY_LAUNCH_ROOT_TASK_TOKEN; import static android.app.ActivityTaskManager.INVALID_TASK_ID; +import static android.app.ComponentOptions.KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED; +import static android.app.ComponentOptions.KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED_BY_PERMISSION; import static android.view.Display.DEFAULT_DISPLAY; import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; @@ -26,14 +29,16 @@ import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_MAIN; import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_SIDE; import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_UNDEFINED; import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_RETURN_HOME; +import static com.android.wm.shell.transition.Transitions.TRANSIT_SPLIT_DISMISS; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.notNull; import static org.mockito.Mockito.clearInvocations; -import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -41,16 +46,22 @@ import static org.mockito.Mockito.when; import android.app.ActivityManager; import android.content.res.Configuration; import android.graphics.Rect; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; import android.view.SurfaceControl; import android.view.SurfaceSession; +import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; +import androidx.test.annotation.UiThreadTest; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.TestRunningTaskInfoBuilder; +import com.android.wm.shell.TestShellExecutor; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayImeController; import com.android.wm.shell.common.DisplayInsetsController; @@ -58,6 +69,9 @@ 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.sysui.ShellController; +import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.transition.Transitions; import org.junit.Before; @@ -68,8 +82,6 @@ import org.mockito.MockitoAnnotations; import java.util.Optional; -import javax.inject.Provider; - /** * Tests for {@link StageCoordinator} */ @@ -85,10 +97,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; @@ -97,81 +105,88 @@ public class StageCoordinatorTests extends ShellTestCase { @Mock private DisplayInsetsController mDisplayInsetsController; @Mock - private Transitions mTransitions; - @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; private ActivityManager.RunningTaskInfo mRootTask; private StageCoordinator mStageCoordinator; + private Transitions mTransitions; + private final TestShellExecutor mMainExecutor = new TestShellExecutor(); + private final ShellExecutor mAnimExecutor = new TestShellExecutor(); + private final Handler mMainHandler = new Handler(Looper.getMainLooper()); @Before + @UiThreadTest public void setup() { MockitoAnnotations.initMocks(this); + mTransitions = createTestTransitions(); mStageCoordinator = spy(new StageCoordinator(mContext, DEFAULT_DISPLAY, mSyncQueue, mTaskOrganizer, mMainStage, mSideStage, mDisplayController, mDisplayImeController, - mDisplayInsetsController, mSplitLayout, mTransitions, mTransactionPool, mLogger, - mMainExecutor, Optional.empty(), new UnfoldControllerProvider())); - doNothing().when(mStageCoordinator).updateActivityOptions(any(), anyInt()); + mDisplayInsetsController, mSplitLayout, mTransitions, mTransactionPool, + mMainExecutor, Optional.empty())); when(mSplitLayout.getBounds1()).thenReturn(mBounds1); when(mSplitLayout.getBounds2()).thenReturn(mBounds2); + when(mSplitLayout.getRootBounds()).thenReturn(mRootBounds); when(mSplitLayout.isLandscape()).thenReturn(false); + when(mSplitLayout.applyTaskChanges(any(), any(), any())).thenReturn(true); 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 - public void testMoveToStage() { - final ActivityManager.RunningTaskInfo task = new TestRunningTaskInfoBuilder().build(); + public void testMoveToStage_splitActiveBackground() { + when(mStageCoordinator.isSplitActive()).thenReturn(true); - mStageCoordinator.moveToStage(task, STAGE_TYPE_MAIN, SPLIT_POSITION_BOTTOM_OR_RIGHT, - new WindowContainerTransaction()); - verify(mMainStage).addTask(eq(task), any(WindowContainerTransaction.class)); - assertEquals(SPLIT_POSITION_BOTTOM_OR_RIGHT, mStageCoordinator.getMainStagePosition()); + final ActivityManager.RunningTaskInfo task = new TestRunningTaskInfoBuilder().build(); + final WindowContainerTransaction wct = new WindowContainerTransaction(); - mStageCoordinator.moveToStage(task, STAGE_TYPE_SIDE, SPLIT_POSITION_BOTTOM_OR_RIGHT, - new WindowContainerTransaction()); - verify(mSideStage).addTask(eq(task), any(WindowContainerTransaction.class)); + mStageCoordinator.moveToStage(task, SPLIT_POSITION_BOTTOM_OR_RIGHT, wct); + verify(mSideStage).addTask(eq(task), eq(wct)); assertEquals(SPLIT_POSITION_BOTTOM_OR_RIGHT, mStageCoordinator.getSideStagePosition()); + assertEquals(SPLIT_POSITION_TOP_OR_LEFT, mStageCoordinator.getMainStagePosition()); } @Test - public void testMoveToUndefinedStage() { + public void testMoveToStage_splitActiveForeground() { + when(mStageCoordinator.isSplitActive()).thenReturn(true); + when(mStageCoordinator.isSplitScreenVisible()).thenReturn(true); + // Assume current side stage is top or left. + mStageCoordinator.setSideStagePosition(SPLIT_POSITION_TOP_OR_LEFT, null); + final ActivityManager.RunningTaskInfo task = new TestRunningTaskInfoBuilder().build(); + final WindowContainerTransaction wct = new WindowContainerTransaction(); - // Verify move to undefined stage while split screen not activated moves task to side stage. - when(mMainStage.isActive()).thenReturn(false); - mStageCoordinator.setSideStagePosition(SPLIT_POSITION_TOP_OR_LEFT, null); - mStageCoordinator.moveToStage(task, STAGE_TYPE_UNDEFINED, SPLIT_POSITION_BOTTOM_OR_RIGHT, - new WindowContainerTransaction()); - verify(mSideStage).addTask(eq(task), any(WindowContainerTransaction.class)); - assertEquals(SPLIT_POSITION_BOTTOM_OR_RIGHT, mStageCoordinator.getSideStagePosition()); + mStageCoordinator.moveToStage(task, SPLIT_POSITION_BOTTOM_OR_RIGHT, wct); + verify(mMainStage).addTask(eq(task), eq(wct)); + assertEquals(SPLIT_POSITION_BOTTOM_OR_RIGHT, mStageCoordinator.getMainStagePosition()); + assertEquals(SPLIT_POSITION_TOP_OR_LEFT, mStageCoordinator.getSideStagePosition()); - // Verify move to undefined stage after split screen activated moves task based on position. - when(mMainStage.isActive()).thenReturn(true); - assertEquals(SPLIT_POSITION_TOP_OR_LEFT, mStageCoordinator.getMainStagePosition()); - mStageCoordinator.moveToStage(task, STAGE_TYPE_UNDEFINED, SPLIT_POSITION_TOP_OR_LEFT, - new WindowContainerTransaction()); - verify(mMainStage).addTask(eq(task), any(WindowContainerTransaction.class)); - assertEquals(SPLIT_POSITION_TOP_OR_LEFT, mStageCoordinator.getMainStagePosition()); + mStageCoordinator.moveToStage(task, SPLIT_POSITION_TOP_OR_LEFT, wct); + verify(mSideStage).addTask(eq(task), eq(wct)); + assertEquals(SPLIT_POSITION_TOP_OR_LEFT, mStageCoordinator.getSideStagePosition()); + assertEquals(SPLIT_POSITION_BOTTOM_OR_RIGHT, mStageCoordinator.getMainStagePosition()); } @Test - public void testRootTaskAppeared_initializesUnfoldControllers() { - verify(mMainUnfoldController).init(); - verify(mSideUnfoldController).init(); - verify(mStageCoordinator).onRootTaskAppeared(); + public void testMoveToStage_splitInctive() { + final ActivityManager.RunningTaskInfo task = new TestRunningTaskInfoBuilder().build(); + final WindowContainerTransaction wct = new WindowContainerTransaction(); + + mStageCoordinator.moveToStage(task, SPLIT_POSITION_BOTTOM_OR_RIGHT, wct); + verify(mStageCoordinator).prepareEnterSplitScreen(eq(wct), eq(task), + eq(SPLIT_POSITION_BOTTOM_OR_RIGHT)); + assertEquals(SPLIT_POSITION_BOTTOM_OR_RIGHT, mStageCoordinator.getSideStagePosition()); } @Test @@ -184,26 +199,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 +248,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 +260,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 @@ -268,7 +280,7 @@ public class StageCoordinatorTests extends ShellTestCase { @Test public void testResolveStartStage_afterSplitActivated_retrievesStagePosition() { - when(mMainStage.isActive()).thenReturn(true); + when(mStageCoordinator.isSplitScreenVisible()).thenReturn(true); mStageCoordinator.setSideStagePosition(SPLIT_POSITION_TOP_OR_LEFT, null /* wct */); mStageCoordinator.resolveStartStage(STAGE_TYPE_UNDEFINED, SPLIT_POSITION_TOP_OR_LEFT, @@ -315,19 +327,40 @@ public class StageCoordinatorTests extends ShellTestCase { verify(mSplitLayout).applySurfaceChanges(any(), any(), any(), any(), any(), eq(false)); } - private class UnfoldControllerProvider implements - Provider<Optional<StageTaskUnfoldController>> { + @Test + public void testAddActivityOptions_addsBackgroundActivitiesFlags() { + Bundle options = mStageCoordinator.resolveStartStage(STAGE_TYPE_MAIN, + SPLIT_POSITION_UNDEFINED, null /* options */, null /* wct */); + + assertEquals(options.getParcelable(KEY_LAUNCH_ROOT_TASK_TOKEN, WindowContainerToken.class), + mMainStage.mRootTaskInfo.token); + assertTrue(options.getBoolean(KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED)); + assertTrue(options.getBoolean( + KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED_BY_PERMISSION)); + } + + @Test + public void testExitSplitScreenAfterFolded() { + when(mMainStage.isActive()).thenReturn(true); + when(mMainStage.isFocused()).thenReturn(true); + when(mMainStage.getTopVisibleChildTaskId()).thenReturn(INVALID_TASK_ID); - private boolean isMain = true; + mStageCoordinator.onFoldedStateChanged(true); - @Override - public Optional<StageTaskUnfoldController> get() { - if (isMain) { - isMain = false; - return Optional.of(mMainUnfoldController); - } else { - return Optional.of(mSideUnfoldController); - } + if (Transitions.ENABLE_SHELL_TRANSITIONS) { + verify(mTaskOrganizer).startNewTransition(eq(TRANSIT_SPLIT_DISMISS), notNull()); + } else { + verify(mStageCoordinator).onSplitScreenExit(); + verify(mMainStage).deactivate(any(WindowContainerTransaction.class), eq(false)); } } + + private Transitions createTestTransitions() { + ShellInit shellInit = new ShellInit(mMainExecutor); + final Transitions t = new Transitions(mContext, shellInit, mock(ShellController.class), + mTaskOrganizer, mTransactionPool, mock(DisplayController.class), mMainExecutor, + mMainHandler, mAnimExecutor); + shellInit.init(); + return t; + } } 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..784ad9b006b6 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; @@ -62,7 +61,7 @@ import org.mockito.MockitoAnnotations; @RunWith(AndroidJUnit4.class) public final class StageTaskListenerTests extends ShellTestCase { private static final boolean ENABLE_SHELL_TRANSITIONS = - SystemProperties.getBoolean("persist.wm.debug.shell_transit", false); + SystemProperties.getBoolean("persist.wm.debug.shell_transit", true); @Mock private ShellTaskOrganizer mTaskOrganizer; @@ -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(); @@ -131,36 +127,6 @@ public final class StageTaskListenerTests extends ShellTestCase { } @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(); - mStageTaskListener.onTaskVanished(task); - } - - @Test public void testTaskVanished() { // With shell transitions, the transition manages status changes, so skip this test. assumeFalse(ENABLE_SHELL_TRANSITIONS); 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..bf62acfc47a1 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 @@ -24,8 +24,8 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spy; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; -import static com.android.wm.shell.startingsurface.StartingSurfaceDrawer.MAX_ANIMATION_DURATION; -import static com.android.wm.shell.startingsurface.StartingSurfaceDrawer.MINIMAL_ANIMATION_DURATION; +import static com.android.wm.shell.startingsurface.SplashscreenContentDrawer.MAX_ANIMATION_DURATION; +import static com.android.wm.shell.startingsurface.SplashscreenContentDrawer.MINIMAL_ANIMATION_DURATION; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; @@ -56,11 +56,9 @@ import android.os.IBinder; import android.os.Looper; import android.os.UserHandle; import android.testing.TestableContext; -import android.view.Display; import android.view.IWindowSession; import android.view.InsetsState; import android.view.Surface; -import android.view.View; import android.view.WindowManager; import android.view.WindowManagerGlobal; import android.view.WindowMetrics; @@ -73,6 +71,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 +90,7 @@ import java.util.function.IntSupplier; */ @SmallTest @RunWith(AndroidJUnit4.class) -public class StartingSurfaceDrawerTests { +public class StartingSurfaceDrawerTests extends ShellTestCase { @Mock private IBinder mBinder; @Mock @@ -105,36 +104,7 @@ public class StartingSurfaceDrawerTests { private ShellExecutor mTestExecutor; private final TestableContext mTestContext = new TestContext( InstrumentationRegistry.getInstrumentation().getTargetContext()); - TestStartingSurfaceDrawer mStartingSurfaceDrawer; - - static final class TestStartingSurfaceDrawer extends StartingSurfaceDrawer{ - int mAddWindowForTask = 0; - - TestStartingSurfaceDrawer(Context context, ShellExecutor splashScreenExecutor, - IconProvider iconProvider, TransactionPool pool) { - super(context, splashScreenExecutor, iconProvider, pool); - } - - @Override - protected boolean addWindow(int taskId, IBinder appToken, View view, Display display, - WindowManager.LayoutParams params, int suggestType) { - // listen for addView - mAddWindowForTask = taskId; - saveSplashScreenRecord(appToken, taskId, view, suggestType); - // Do not wait for background color - return false; - } - - @Override - protected void removeWindowSynced(StartingWindowRemovalInfo removalInfo, - boolean immediately) { - // listen for removeView - if (mAddWindowForTask == removalInfo.taskId) { - mAddWindowForTask = 0; - } - mStartingWindowRecords.remove(removalInfo.taskId); - } - } + StartingSurfaceDrawer mStartingSurfaceDrawer; private static class TestContext extends TestableContext { TestContext(Context context) { @@ -164,44 +134,51 @@ public class StartingSurfaceDrawerTests { doReturn(metrics).when(mMockWindowManager).getMaximumWindowMetrics(); doNothing().when(mMockWindowManager).addView(any(), any()); mTestExecutor = new HandlerExecutor(mTestHandler); + mStartingSurfaceDrawer = new StartingSurfaceDrawer(mTestContext, mTestExecutor, + mIconProvider, mTransactionPool); mStartingSurfaceDrawer = spy( - new TestStartingSurfaceDrawer(mTestContext, mTestExecutor, mIconProvider, + new StartingSurfaceDrawer(mTestContext, mTestExecutor, mIconProvider, mTransactionPool)); + spyOn(mStartingSurfaceDrawer.mSplashscreenWindowCreator); + spyOn(mStartingSurfaceDrawer.mWindowRecords); + spyOn(mStartingSurfaceDrawer.mWindowlessRecords); } @Test public void testAddSplashScreenSurface() { final int taskId = 1; final StartingWindowInfo windowInfo = - createWindowInfo(taskId, android.R.style.Theme); - mStartingSurfaceDrawer.addSplashScreenStartingWindow(windowInfo, mBinder, + createWindowInfo(taskId, android.R.style.Theme, mBinder); + mStartingSurfaceDrawer.addSplashScreenStartingWindow(windowInfo, STARTING_WINDOW_TYPE_SPLASH_SCREEN); waitHandlerIdle(mTestHandler); - verify(mStartingSurfaceDrawer).addWindow(eq(taskId), eq(mBinder), any(), any(), any(), + verify(mStartingSurfaceDrawer.mSplashscreenWindowCreator).addWindow( + eq(taskId), eq(mBinder), any(), any(), any(), eq(STARTING_WINDOW_TYPE_SPLASH_SCREEN)); - assertEquals(mStartingSurfaceDrawer.mAddWindowForTask, taskId); StartingWindowRemovalInfo removalInfo = new StartingWindowRemovalInfo(); removalInfo.taskId = windowInfo.taskInfo.taskId; mStartingSurfaceDrawer.removeStartingWindow(removalInfo); waitHandlerIdle(mTestHandler); - verify(mStartingSurfaceDrawer).removeWindowSynced(any(), eq(false)); - assertEquals(mStartingSurfaceDrawer.mAddWindowForTask, 0); + verify(mStartingSurfaceDrawer.mWindowRecords).removeWindow(any(), eq(false)); + assertEquals(mStartingSurfaceDrawer.mWindowRecords.recordSize(), 0); } @Test public void testFallbackDefaultTheme() { final int taskId = 1; final StartingWindowInfo windowInfo = - createWindowInfo(taskId, 0); + createWindowInfo(taskId, 0, mBinder); final int[] theme = new int[1]; doAnswer(invocation -> theme[0] = (Integer) invocation.callRealMethod()) - .when(mStartingSurfaceDrawer).getSplashScreenTheme(eq(0), any()); + .when(mStartingSurfaceDrawer.mSplashscreenWindowCreator) + .getSplashScreenTheme(eq(0), any()); - mStartingSurfaceDrawer.addSplashScreenStartingWindow(windowInfo, mBinder, + mStartingSurfaceDrawer.addSplashScreenStartingWindow(windowInfo, STARTING_WINDOW_TYPE_SPLASH_SCREEN); waitHandlerIdle(mTestHandler); - verify(mStartingSurfaceDrawer).getSplashScreenTheme(eq(0), any()); + verify(mStartingSurfaceDrawer.mSplashscreenWindowCreator) + .getSplashScreenTheme(eq(0), any()); assertNotEquals(theme[0], 0); } @@ -240,7 +217,7 @@ public class StartingSurfaceDrawerTests { public void testRemoveTaskSnapshotWithImeSurfaceWhenOnImeDrawn() throws Exception { final int taskId = 1; final StartingWindowInfo windowInfo = - createWindowInfo(taskId, android.R.style.Theme); + createWindowInfo(taskId, android.R.style.Theme, mBinder); TaskSnapshot snapshot = createTaskSnapshot(100, 100, new Point(100, 100), new Rect(0, 0, 0, 50), true /* hasImeSurface */); final IWindowSession session = WindowManagerGlobal.getWindowSession(); @@ -248,8 +225,9 @@ public class StartingSurfaceDrawerTests { doReturn(WindowManagerGlobal.ADD_OKAY).when(session).addToDisplay( any() /* window */, any() /* attrs */, anyInt() /* viewVisibility */, anyInt() /* displayId */, - any() /* requestedVisibility */, any() /* outInputChannel */, - any() /* outInsetsState */, any() /* outActiveControls */); + anyInt() /* requestedVisibleTypes */, any() /* outInputChannel */, + any() /* outInsetsState */, any() /* outActiveControls */, + any() /* outAttachedFrame */, any() /* outSizeCompatScale */); TaskSnapshotWindow mockSnapshotWindow = TaskSnapshotWindow.create(windowInfo, mBinder, snapshot, mTestExecutor, () -> { @@ -268,7 +246,7 @@ public class StartingSurfaceDrawerTests { when(TaskSnapshotWindow.create(eq(windowInfo), eq(mBinder), eq(snapshot), any(), any())).thenReturn(mockSnapshotWindow); // Simulate a task snapshot window created with IME snapshot shown. - mStartingSurfaceDrawer.makeTaskSnapshotWindow(windowInfo, mBinder, snapshot); + mStartingSurfaceDrawer.makeTaskSnapshotWindow(windowInfo, snapshot); waitHandlerIdle(mTestHandler); // Verify the task snapshot with IME snapshot will be removed when received the real IME @@ -276,27 +254,36 @@ public class StartingSurfaceDrawerTests { // makeTaskSnapshotWindow shall call removeWindowSynced before there add a new // StartingWindowRecord for the task. mStartingSurfaceDrawer.onImeDrawnOnTask(1); - verify(mStartingSurfaceDrawer, times(2)) - .removeWindowSynced(any(), eq(true)); + verify(mStartingSurfaceDrawer.mWindowRecords, times(2)) + .removeWindow(any(), eq(true)); } } @Test public void testClearAllWindows() { final int taskId = 1; - final StartingWindowInfo windowInfo = - createWindowInfo(taskId, android.R.style.Theme); - mStartingSurfaceDrawer.addSplashScreenStartingWindow(windowInfo, mBinder, - STARTING_WINDOW_TYPE_SPLASH_SCREEN); - waitHandlerIdle(mTestHandler); - verify(mStartingSurfaceDrawer).addWindow(eq(taskId), eq(mBinder), any(), any(), any(), - eq(STARTING_WINDOW_TYPE_SPLASH_SCREEN)); - assertEquals(mStartingSurfaceDrawer.mAddWindowForTask, taskId); + mStartingSurfaceDrawer.mWindowRecords.addRecord(taskId, + new StartingSurfaceDrawer.StartingWindowRecord() { + @Override + public void removeIfPossible(StartingWindowRemovalInfo info, + boolean immediately) { + + } + }); + mStartingSurfaceDrawer.mWindowlessRecords.addRecord(taskId, + new StartingSurfaceDrawer.StartingWindowRecord() { + @Override + public void removeIfPossible(StartingWindowRemovalInfo info, + boolean immediately) { + } + }); mStartingSurfaceDrawer.clearAllWindows(); waitHandlerIdle(mTestHandler); - verify(mStartingSurfaceDrawer).removeWindowSynced(any(), eq(true)); - assertEquals(mStartingSurfaceDrawer.mStartingWindowRecords.size(), 0); + verify(mStartingSurfaceDrawer.mWindowRecords).removeWindow(any(), eq(true)); + assertEquals(mStartingSurfaceDrawer.mWindowRecords.recordSize(), 0); + verify(mStartingSurfaceDrawer.mWindowlessRecords).removeWindow(any(), eq(true)); + assertEquals(mStartingSurfaceDrawer.mWindowlessRecords.recordSize(), 0); } @Test @@ -349,7 +336,7 @@ public class StartingSurfaceDrawerTests { longAppDuration, longAppDuration)); } - private StartingWindowInfo createWindowInfo(int taskId, int themeResId) { + private StartingWindowInfo createWindowInfo(int taskId, int themeResId, IBinder appToken) { StartingWindowInfo windowInfo = new StartingWindowInfo(); final ActivityInfo info = new ActivityInfo(); info.applicationInfo = new ApplicationInfo(); @@ -358,6 +345,7 @@ public class StartingSurfaceDrawerTests { final ActivityManager.RunningTaskInfo taskInfo = new ActivityManager.RunningTaskInfo(); taskInfo.topActivityInfo = info; taskInfo.taskId = taskId; + windowInfo.appToken = appToken; windowInfo.targetActivityInfo = info; windowInfo.taskInfo = taskInfo; windowInfo.topOpaqueWindowInsetsState = new InsetsState(); 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..10dec9ef12f9 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingWindowControllerTests.java @@ -0,0 +1,115 @@ +/* + * 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.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.ArgumentMatchers.isA; +import static org.mockito.Mockito.doReturn; +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 android.content.Context; +import android.hardware.display.DisplayManager; +import android.os.Bundle; +import android.view.Display; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import com.android.internal.util.function.TriConsumer; +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.ShellCommandHandler; +import com.android.wm.shell.sysui.ShellController; +import com.android.wm.shell.sysui.ShellInit; +import com.android.wm.shell.sysui.ShellSharedConstants; + +import org.junit.Before; +import org.junit.Test; +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 ShellCommandHandler mShellCommandHandler; + private @Mock ShellTaskOrganizer mTaskOrganizer; + private @Mock ShellExecutor mMainExecutor; + private @Mock StartingWindowTypeAlgorithm mTypeAlgorithm; + private @Mock IconProvider mIconProvider; + private @Mock TransactionPool mTransactionPool; + private StartingWindowController mController; + private ShellInit mShellInit; + private ShellController mShellController; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + doReturn(mock(Display.class)).when(mDisplayManager).getDisplay(anyInt()); + doReturn(mDisplayManager).when(mContext).getSystemService(eq(DisplayManager.class)); + mShellInit = spy(new ShellInit(mMainExecutor)); + mShellController = spy(new ShellController(mShellInit, mShellCommandHandler, + mMainExecutor)); + mController = new StartingWindowController(mContext, mShellInit, mShellController, + mTaskOrganizer, mMainExecutor, mTypeAlgorithm, mIconProvider, mTransactionPool); + mShellInit.init(); + } + + @Test + public void instantiateController_addInitCallback() { + verify(mShellInit, times(1)).addInitCallback(any(), isA(StartingWindowController.class)); + } + + @Test + public void instantiateController_addExternalInterface() { + verify(mShellController, times(1)).addExternalInterface( + eq(ShellSharedConstants.KEY_EXTRA_SHELL_STARTING_WINDOW), any(), any()); + } + + @Test + public void testInvalidateExternalInterface_unregistersListener() { + mController.setStartingWindowListener(new TriConsumer<Integer, Integer, Integer>() { + @Override + public void accept(Integer integer, Integer integer2, Integer integer3) {} + }); + assertTrue(mController.hasStartingWindowListener()); + // Create initial interface + mShellController.createExternalInterfaces(new Bundle()); + // Recreate the interface to trigger invalidation of the previous instance + mShellController.createExternalInterfaces(new Bundle()); + assertFalse(mController.hasStartingWindowListener()); + } +} 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 deleted file mode 100644 index 78e27c956807..000000000000 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/TaskSnapshotWindowTest.java +++ /dev/null @@ -1,293 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY 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 android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; -import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; -import static android.content.res.Configuration.ORIENTATION_PORTRAIT; -import static android.view.WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS; - -import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock; -import static com.android.dx.mockito.inline.extended.ExtendedMockito.never; -import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; -import static com.android.dx.mockito.inline.extended.ExtendedMockito.when; - -import static org.junit.Assert.assertEquals; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anyInt; -import static org.mockito.Matchers.eq; - -import android.app.ActivityManager.TaskDescription; -import android.content.ComponentName; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.ColorSpace; -import android.graphics.Point; -import android.graphics.Rect; -import android.hardware.HardwareBuffer; -import android.view.InsetsState; -import android.view.Surface; -import android.view.SurfaceControl; -import android.window.TaskSnapshot; - -import androidx.test.ext.junit.runners.AndroidJUnit4; -import androidx.test.filters.SmallTest; - -import com.android.wm.shell.TestShellExecutor; - -import org.junit.Test; -import org.junit.runner.RunWith; - -/** - * Test class for {@link TaskSnapshotWindow}. - * - */ -@SmallTest -@RunWith(AndroidJUnit4.class) -public class TaskSnapshotWindowTest { - - private TaskSnapshotWindow mWindow; - - private void setupSurface(int width, int height) { - setupSurface(width, height, new Rect(), 0, FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS, - new Rect(0, 0, width, height)); - } - - private void setupSurface(int width, int height, Rect contentInsets, int sysuiVis, - int windowFlags, Rect taskBounds) { - // Previously when constructing TaskSnapshots for this test, scale was 1.0f, so to mimic - // this behavior set the taskSize to be the same as the taskBounds width and height. The - // taskBounds passed here are assumed to be the same task bounds as when the snapshot was - // taken. We assume there is no aspect ratio mismatch between the screenshot and the - // taskBounds - assertEquals(width, taskBounds.width()); - assertEquals(height, taskBounds.height()); - Point taskSize = new Point(taskBounds.width(), taskBounds.height()); - - final TaskSnapshot snapshot = createTaskSnapshot(width, height, taskSize, contentInsets); - mWindow = new TaskSnapshotWindow(new SurfaceControl(), snapshot, "Test", - createTaskDescription(Color.WHITE, Color.RED, Color.BLUE), - 0 /* appearance */, windowFlags /* windowFlags */, 0 /* privateWindowFlags */, - taskBounds, ORIENTATION_PORTRAIT, ACTIVITY_TYPE_STANDARD, - new InsetsState(), null /* clearWindow */, new TestShellExecutor()); - } - - private TaskSnapshot createTaskSnapshot(int width, int height, Point taskSize, - Rect contentInsets) { - final HardwareBuffer buffer = HardwareBuffer.create(width, height, HardwareBuffer.RGBA_8888, - 1, HardwareBuffer.USAGE_CPU_READ_RARELY); - return new TaskSnapshot( - System.currentTimeMillis(), - new ComponentName("", ""), buffer, - ColorSpace.get(ColorSpace.Named.SRGB), ORIENTATION_PORTRAIT, - Surface.ROTATION_0, taskSize, contentInsets, new Rect() /* letterboxInsets */, - false, true /* isRealSnapshot */, WINDOWING_MODE_FULLSCREEN, - 0 /* systemUiVisibility */, false /* isTranslucent */, false /* hasImeSurface */); - } - - private static TaskDescription createTaskDescription(int background, int statusBar, - int navigationBar) { - final TaskDescription td = new TaskDescription(); - td.setBackgroundColor(background); - td.setStatusBarColor(statusBar); - td.setNavigationBarColor(navigationBar); - return td; - } - - @Test - public void fillEmptyBackground_fillHorizontally() { - setupSurface(200, 100); - final Canvas mockCanvas = mock(Canvas.class); - when(mockCanvas.getWidth()).thenReturn(200); - when(mockCanvas.getHeight()).thenReturn(100); - mWindow.drawBackgroundAndBars(mockCanvas, new Rect(0, 0, 100, 200)); - verify(mockCanvas).drawRect(eq(100.0f), eq(0.0f), eq(200.0f), eq(100.0f), any()); - } - - @Test - public void fillEmptyBackground_fillVertically() { - setupSurface(100, 200); - final Canvas mockCanvas = mock(Canvas.class); - when(mockCanvas.getWidth()).thenReturn(100); - when(mockCanvas.getHeight()).thenReturn(200); - mWindow.drawBackgroundAndBars(mockCanvas, new Rect(0, 0, 200, 100)); - verify(mockCanvas).drawRect(eq(0.0f), eq(100.0f), eq(100.0f), eq(200.0f), any()); - } - - @Test - public void fillEmptyBackground_fillBoth() { - setupSurface(200, 200); - final Canvas mockCanvas = mock(Canvas.class); - when(mockCanvas.getWidth()).thenReturn(200); - when(mockCanvas.getHeight()).thenReturn(200); - mWindow.drawBackgroundAndBars(mockCanvas, new Rect(0, 0, 100, 100)); - verify(mockCanvas).drawRect(eq(100.0f), eq(0.0f), eq(200.0f), eq(100.0f), any()); - verify(mockCanvas).drawRect(eq(0.0f), eq(100.0f), eq(200.0f), eq(200.0f), any()); - } - - @Test - public void fillEmptyBackground_dontFill_sameSize() { - setupSurface(100, 100); - final Canvas mockCanvas = mock(Canvas.class); - when(mockCanvas.getWidth()).thenReturn(100); - when(mockCanvas.getHeight()).thenReturn(100); - mWindow.drawBackgroundAndBars(mockCanvas, new Rect(0, 0, 100, 100)); - verify(mockCanvas, never()).drawRect(anyInt(), anyInt(), anyInt(), anyInt(), any()); - } - - @Test - public void fillEmptyBackground_dontFill_bitmapLarger() { - setupSurface(100, 100); - final Canvas mockCanvas = mock(Canvas.class); - when(mockCanvas.getWidth()).thenReturn(100); - when(mockCanvas.getHeight()).thenReturn(100); - mWindow.drawBackgroundAndBars(mockCanvas, new Rect(0, 0, 200, 200)); - verify(mockCanvas, never()).drawRect(anyInt(), anyInt(), anyInt(), anyInt(), any()); - } - - @Test - public void testCalculateSnapshotCrop() { - setupSurface(100, 100, new Rect(0, 10, 0, 10), 0, 0, new Rect(0, 0, 100, 100)); - assertEquals(new Rect(0, 0, 100, 90), mWindow.calculateSnapshotCrop()); - } - - @Test - public void testCalculateSnapshotCrop_taskNotOnTop() { - setupSurface(100, 100, new Rect(0, 10, 0, 10), 0, 0, new Rect(0, 50, 100, 150)); - assertEquals(new Rect(0, 10, 100, 90), mWindow.calculateSnapshotCrop()); - } - - @Test - public void testCalculateSnapshotCrop_navBarLeft() { - setupSurface(100, 100, new Rect(10, 10, 0, 0), 0, 0, new Rect(0, 0, 100, 100)); - assertEquals(new Rect(10, 0, 100, 100), mWindow.calculateSnapshotCrop()); - } - - @Test - public void testCalculateSnapshotCrop_navBarRight() { - setupSurface(100, 100, new Rect(0, 10, 10, 0), 0, 0, new Rect(0, 0, 100, 100)); - assertEquals(new Rect(0, 0, 90, 100), mWindow.calculateSnapshotCrop()); - } - - @Test - public void testCalculateSnapshotCrop_waterfall() { - setupSurface(100, 100, new Rect(5, 10, 5, 10), 0, 0, new Rect(0, 0, 100, 100)); - assertEquals(new Rect(5, 0, 95, 90), mWindow.calculateSnapshotCrop()); - } - - @Test - public void testCalculateSnapshotFrame() { - setupSurface(100, 100); - final Rect insets = new Rect(0, 10, 0, 10); - mWindow.setFrames(new Rect(0, 0, 100, 100), insets); - assertEquals(new Rect(0, 0, 100, 80), - mWindow.calculateSnapshotFrame(new Rect(0, 10, 100, 90))); - } - - @Test - public void testCalculateSnapshotFrame_navBarLeft() { - setupSurface(100, 100); - final Rect insets = new Rect(10, 10, 0, 0); - mWindow.setFrames(new Rect(0, 0, 100, 100), insets); - assertEquals(new Rect(10, 0, 100, 90), - mWindow.calculateSnapshotFrame(new Rect(10, 10, 100, 100))); - } - - @Test - public void testCalculateSnapshotFrame_waterfall() { - setupSurface(100, 100, new Rect(5, 10, 5, 10), 0, 0, new Rect(0, 0, 100, 100)); - final Rect insets = new Rect(0, 10, 0, 10); - mWindow.setFrames(new Rect(5, 0, 95, 100), insets); - assertEquals(new Rect(0, 0, 90, 90), - mWindow.calculateSnapshotFrame(new Rect(5, 0, 95, 90))); - } - - @Test - public void testDrawStatusBarBackground() { - setupSurface(100, 100); - final Rect insets = new Rect(0, 10, 10, 0); - mWindow.setFrames(new Rect(0, 0, 100, 100), insets); - final Canvas mockCanvas = mock(Canvas.class); - when(mockCanvas.getWidth()).thenReturn(100); - when(mockCanvas.getHeight()).thenReturn(100); - mWindow.drawStatusBarBackground(mockCanvas, new Rect(0, 0, 50, 100)); - verify(mockCanvas).drawRect(eq(50.0f), eq(0.0f), eq(90.0f), eq(10.0f), any()); - } - - @Test - public void testDrawStatusBarBackground_nullFrame() { - setupSurface(100, 100); - final Rect insets = new Rect(0, 10, 10, 0); - mWindow.setFrames(new Rect(0, 0, 100, 100), insets); - final Canvas mockCanvas = mock(Canvas.class); - when(mockCanvas.getWidth()).thenReturn(100); - when(mockCanvas.getHeight()).thenReturn(100); - mWindow.drawStatusBarBackground(mockCanvas, null); - verify(mockCanvas).drawRect(eq(0.0f), eq(0.0f), eq(90.0f), eq(10.0f), any()); - } - - @Test - public void testDrawStatusBarBackground_nope() { - setupSurface(100, 100); - final Rect insets = new Rect(0, 10, 10, 0); - mWindow.setFrames(new Rect(0, 0, 100, 100), insets); - final Canvas mockCanvas = mock(Canvas.class); - when(mockCanvas.getWidth()).thenReturn(100); - when(mockCanvas.getHeight()).thenReturn(100); - mWindow.drawStatusBarBackground(mockCanvas, new Rect(0, 0, 100, 100)); - verify(mockCanvas, never()).drawRect(anyInt(), anyInt(), anyInt(), anyInt(), any()); - } - - @Test - public void testDrawNavigationBarBackground() { - final Rect insets = new Rect(0, 10, 0, 10); - setupSurface(100, 100, insets, 0, FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS, - new Rect(0, 0, 100, 100)); - mWindow.setFrames(new Rect(0, 0, 100, 100), insets); - final Canvas mockCanvas = mock(Canvas.class); - when(mockCanvas.getWidth()).thenReturn(100); - when(mockCanvas.getHeight()).thenReturn(100); - mWindow.drawNavigationBarBackground(mockCanvas); - verify(mockCanvas).drawRect(eq(new Rect(0, 90, 100, 100)), any()); - } - - @Test - public void testDrawNavigationBarBackground_left() { - final Rect insets = new Rect(10, 10, 0, 0); - setupSurface(100, 100, insets, 0, FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS, - new Rect(0, 0, 100, 100)); - mWindow.setFrames(new Rect(0, 0, 100, 100), insets); - final Canvas mockCanvas = mock(Canvas.class); - when(mockCanvas.getWidth()).thenReturn(100); - when(mockCanvas.getHeight()).thenReturn(100); - mWindow.drawNavigationBarBackground(mockCanvas); - verify(mockCanvas).drawRect(eq(new Rect(0, 0, 10, 100)), any()); - } - - @Test - public void testDrawNavigationBarBackground_right() { - final Rect insets = new Rect(0, 10, 10, 0); - setupSurface(100, 100, insets, 0, FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS, - new Rect(0, 0, 100, 100)); - mWindow.setFrames(new Rect(0, 0, 100, 100), insets); - final Canvas mockCanvas = mock(Canvas.class); - when(mockCanvas.getWidth()).thenReturn(100); - when(mockCanvas.getHeight()).thenReturn(100); - mWindow.drawNavigationBarBackground(mockCanvas); - verify(mockCanvas).drawRect(eq(new Rect(90, 0, 100, 100)), any()); - } -} 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..8d92d0864338 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sysui/ShellControllerTest.java @@ -0,0 +1,460 @@ +/* + * 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.assertThrows; +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.os.Binder; +import android.os.Bundle; +import android.os.IBinder; +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.TestShellExecutor; +import com.android.wm.shell.common.ExternalInterfaceBinder; +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; + private static final String EXTRA_TEST_BINDER = "test_binder"; + + @Mock + private ShellInit mShellInit; + @Mock + private ShellCommandHandler mShellCommandHandler; + @Mock + private Context mTestUserContext; + + private TestShellExecutor mExecutor; + 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(); + mExecutor = new TestShellExecutor(); + mController = new ShellController(mShellInit, mShellCommandHandler, mExecutor); + mController.onConfigurationChanged(getConfigurationCopy()); + } + + @After + public void tearDown() { + // Do nothing + } + + @Test + public void testAddExternalInterface_ensureCallback() { + Binder callback = new Binder(); + ExternalInterfaceBinder wrapper = new ExternalInterfaceBinder() { + @Override + public void invalidate() { + // Do nothing + } + + @Override + public IBinder asBinder() { + return callback; + } + }; + mController.addExternalInterface(EXTRA_TEST_BINDER, () -> wrapper, this); + + Bundle b = new Bundle(); + mController.asShell().createExternalInterfaces(b); + mExecutor.flushAll(); + assertTrue(b.getIBinder(EXTRA_TEST_BINDER) == callback); + } + + @Test + public void testAddExternalInterface_disallowDuplicateKeys() { + Binder callback = new Binder(); + ExternalInterfaceBinder wrapper = new ExternalInterfaceBinder() { + @Override + public void invalidate() { + // Do nothing + } + + @Override + public IBinder asBinder() { + return callback; + } + }; + mController.addExternalInterface(EXTRA_TEST_BINDER, () -> wrapper, this); + assertThrows(IllegalArgumentException.class, () -> { + mController.addExternalInterface(EXTRA_TEST_BINDER, () -> wrapper, this); + }); + } + + @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/TaskViewTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTest.java index 32f1587752cb..b6d7ff3cd5cf 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/taskview/TaskViewTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell; +package com.android.wm.shell.taskview; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; @@ -28,9 +28,11 @@ import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doAnswer; +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.verify; import static org.mockito.Mockito.when; @@ -51,6 +53,8 @@ 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.common.HandlerExecutor; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.common.SyncTransactionQueue.TransactionRunnable; @@ -82,13 +86,15 @@ public class TaskViewTest extends ShellTestCase { @Mock SyncTransactionQueue mSyncQueue; @Mock - TaskViewTransitions mTaskViewTransitions; + Transitions mTransitions; SurfaceSession mSession; SurfaceControl mLeash; Context mContext; TaskView mTaskView; + TaskViewTransitions mTaskViewTransitions; + TaskViewTaskController mTaskViewTaskController; @Before public void setUp() { @@ -118,7 +124,13 @@ public class TaskViewTest extends ShellTestCase { return null; }).when(mSyncQueue).runInSync(any()); - mTaskView = new TaskView(mContext, mOrganizer, mTaskViewTransitions, mSyncQueue); + if (Transitions.ENABLE_SHELL_TRANSITIONS) { + doReturn(true).when(mTransitions).isRegistered(); + } + mTaskViewTransitions = spy(new TaskViewTransitions(mTransitions)); + mTaskViewTaskController = new TaskViewTaskController(mContext, mOrganizer, + mTaskViewTransitions, mSyncQueue); + mTaskView = new TaskView(mContext, mTaskViewTaskController); mTaskView.setListener(mExecutor, mViewListener); } @@ -131,7 +143,8 @@ public class TaskViewTest extends ShellTestCase { @Test public void testSetPendingListener_throwsException() { - TaskView taskView = new TaskView(mContext, mOrganizer, mTaskViewTransitions, mSyncQueue); + TaskView taskView = new TaskView(mContext, + new TaskViewTaskController(mContext, mOrganizer, mTaskViewTransitions, mSyncQueue)); taskView.setListener(mExecutor, mViewListener); try { taskView.setListener(mExecutor, mViewListener); @@ -145,16 +158,17 @@ public class TaskViewTest extends ShellTestCase { @Test public void testStartActivity() { ActivityOptions options = ActivityOptions.makeBasic(); - mTaskView.startActivity(mock(PendingIntent.class), null, options, new Rect(0, 0, 100, 100)); + mTaskView.startActivity(mock(PendingIntent.class), null, options, + new Rect(0, 0, 100, 100)); - verify(mOrganizer).setPendingLaunchCookieListener(any(), eq(mTaskView)); + verify(mOrganizer).setPendingLaunchCookieListener(any(), eq(mTaskViewTaskController)); assertThat(options.getLaunchWindowingMode()).isEqualTo(WINDOWING_MODE_MULTI_WINDOW); } @Test public void testOnTaskAppeared_noSurface_legacyTransitions() { assumeFalse(Transitions.ENABLE_SHELL_TRANSITIONS); - mTaskView.onTaskAppeared(mTaskInfo, mLeash); + mTaskViewTaskController.onTaskAppeared(mTaskInfo, mLeash); verify(mViewListener).onTaskCreated(eq(mTaskInfo.taskId), any()); verify(mViewListener, never()).onInitialized(); @@ -166,9 +180,10 @@ public class TaskViewTest extends ShellTestCase { public void testOnTaskAppeared_withSurface_legacyTransitions() { assumeFalse(Transitions.ENABLE_SHELL_TRANSITIONS); mTaskView.surfaceCreated(mock(SurfaceHolder.class)); - mTaskView.onTaskAppeared(mTaskInfo, mLeash); + mTaskViewTaskController.onTaskAppeared(mTaskInfo, mLeash); verify(mViewListener).onTaskCreated(eq(mTaskInfo.taskId), any()); + assertThat(mTaskView.isInitialized()).isTrue(); verify(mViewListener, never()).onTaskVisibilityChanged(anyInt(), anyBoolean()); } @@ -178,6 +193,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()); } @@ -185,10 +201,11 @@ public class TaskViewTest extends ShellTestCase { @Test public void testSurfaceCreated_withTask_legacyTransitions() { assumeFalse(Transitions.ENABLE_SHELL_TRANSITIONS); - mTaskView.onTaskAppeared(mTaskInfo, mLeash); + mTaskViewTaskController.onTaskAppeared(mTaskInfo, mLeash); mTaskView.surfaceCreated(mock(SurfaceHolder.class)); verify(mViewListener).onInitialized(); + assertThat(mTaskView.isInitialized()).isTrue(); verify(mViewListener).onTaskVisibilityChanged(eq(mTaskInfo.taskId), eq(true)); } @@ -206,7 +223,7 @@ public class TaskViewTest extends ShellTestCase { public void testSurfaceDestroyed_withTask_legacyTransitions() { assumeFalse(Transitions.ENABLE_SHELL_TRANSITIONS); SurfaceHolder sh = mock(SurfaceHolder.class); - mTaskView.onTaskAppeared(mTaskInfo, mLeash); + mTaskViewTaskController.onTaskAppeared(mTaskInfo, mLeash); mTaskView.surfaceCreated(sh); reset(mViewListener); mTaskView.surfaceDestroyed(sh); @@ -217,20 +234,21 @@ public class TaskViewTest extends ShellTestCase { @Test public void testOnReleased_legacyTransitions() { assumeFalse(Transitions.ENABLE_SHELL_TRANSITIONS); - mTaskView.onTaskAppeared(mTaskInfo, mLeash); + mTaskViewTaskController.onTaskAppeared(mTaskInfo, mLeash); mTaskView.surfaceCreated(mock(SurfaceHolder.class)); mTaskView.release(); - verify(mOrganizer).removeListener(eq(mTaskView)); + verify(mOrganizer).removeListener(eq(mTaskViewTaskController)); verify(mViewListener).onReleased(); + assertThat(mTaskView.isInitialized()).isFalse(); } @Test public void testOnTaskVanished_legacyTransitions() { assumeFalse(Transitions.ENABLE_SHELL_TRANSITIONS); - mTaskView.onTaskAppeared(mTaskInfo, mLeash); + mTaskViewTaskController.onTaskAppeared(mTaskInfo, mLeash); mTaskView.surfaceCreated(mock(SurfaceHolder.class)); - mTaskView.onTaskVanished(mTaskInfo); + mTaskViewTaskController.onTaskVanished(mTaskInfo); verify(mViewListener).onTaskRemovalStarted(eq(mTaskInfo.taskId)); } @@ -238,8 +256,8 @@ public class TaskViewTest extends ShellTestCase { @Test public void testOnBackPressedOnTaskRoot_legacyTransitions() { assumeFalse(Transitions.ENABLE_SHELL_TRANSITIONS); - mTaskView.onTaskAppeared(mTaskInfo, mLeash); - mTaskView.onBackPressedOnTaskRoot(mTaskInfo); + mTaskViewTaskController.onTaskAppeared(mTaskInfo, mLeash); + mTaskViewTaskController.onBackPressedOnTaskRoot(mTaskInfo); verify(mViewListener).onBackPressedOnTaskRoot(eq(mTaskInfo.taskId)); } @@ -247,17 +265,17 @@ public class TaskViewTest extends ShellTestCase { @Test public void testSetOnBackPressedOnTaskRoot_legacyTransitions() { assumeFalse(Transitions.ENABLE_SHELL_TRANSITIONS); - mTaskView.onTaskAppeared(mTaskInfo, mLeash); + mTaskViewTaskController.onTaskAppeared(mTaskInfo, mLeash); verify(mOrganizer).setInterceptBackPressedOnTaskRoot(eq(mTaskInfo.token), eq(true)); } @Test public void testUnsetOnBackPressedOnTaskRoot_legacyTransitions() { assumeFalse(Transitions.ENABLE_SHELL_TRANSITIONS); - mTaskView.onTaskAppeared(mTaskInfo, mLeash); + mTaskViewTaskController.onTaskAppeared(mTaskInfo, mLeash); verify(mOrganizer).setInterceptBackPressedOnTaskRoot(eq(mTaskInfo.token), eq(true)); - mTaskView.onTaskVanished(mTaskInfo); + mTaskViewTaskController.onTaskVanished(mTaskInfo); verify(mOrganizer).setInterceptBackPressedOnTaskRoot(eq(mTaskInfo.token), eq(false)); } @@ -265,11 +283,13 @@ public class TaskViewTest extends ShellTestCase { public void testOnNewTask_noSurface() { assumeTrue(Transitions.ENABLE_SHELL_TRANSITIONS); WindowContainerTransaction wct = new WindowContainerTransaction(); - mTaskView.prepareOpenAnimation(true /* newTask */, new SurfaceControl.Transaction(), - new SurfaceControl.Transaction(), mTaskInfo, mLeash, wct); + mTaskViewTaskController.prepareOpenAnimation(true /* newTask */, + new SurfaceControl.Transaction(), new SurfaceControl.Transaction(), mTaskInfo, + mLeash, wct); 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 +301,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()); } @@ -290,8 +311,9 @@ public class TaskViewTest extends ShellTestCase { assumeTrue(Transitions.ENABLE_SHELL_TRANSITIONS); mTaskView.surfaceCreated(mock(SurfaceHolder.class)); WindowContainerTransaction wct = new WindowContainerTransaction(); - mTaskView.prepareOpenAnimation(true /* newTask */, new SurfaceControl.Transaction(), - new SurfaceControl.Transaction(), mTaskInfo, mLeash, wct); + mTaskViewTaskController.prepareOpenAnimation(true /* newTask */, + new SurfaceControl.Transaction(), new SurfaceControl.Transaction(), mTaskInfo, + mLeash, wct); verify(mViewListener).onTaskCreated(eq(mTaskInfo.taskId), any()); verify(mViewListener, never()).onTaskVisibilityChanged(anyInt(), anyBoolean()); @@ -301,15 +323,17 @@ public class TaskViewTest extends ShellTestCase { public void testSurfaceCreated_withTask() { assumeTrue(Transitions.ENABLE_SHELL_TRANSITIONS); WindowContainerTransaction wct = new WindowContainerTransaction(); - mTaskView.prepareOpenAnimation(true /* newTask */, new SurfaceControl.Transaction(), - new SurfaceControl.Transaction(), mTaskInfo, mLeash, wct); + mTaskViewTaskController.prepareOpenAnimation(true /* newTask */, + new SurfaceControl.Transaction(), new SurfaceControl.Transaction(), mTaskInfo, + mLeash, wct); mTaskView.surfaceCreated(mock(SurfaceHolder.class)); verify(mViewListener).onInitialized(); - verify(mTaskViewTransitions).setTaskViewVisible(eq(mTaskView), eq(true)); + verify(mTaskViewTransitions).setTaskViewVisible(eq(mTaskViewTaskController), eq(true)); - mTaskView.prepareOpenAnimation(false /* newTask */, new SurfaceControl.Transaction(), - new SurfaceControl.Transaction(), mTaskInfo, mLeash, wct); + mTaskViewTaskController.prepareOpenAnimation(false /* newTask */, + new SurfaceControl.Transaction(), new SurfaceControl.Transaction(), mTaskInfo, + mLeash, wct); verify(mViewListener).onTaskVisibilityChanged(eq(mTaskInfo.taskId), eq(true)); } @@ -329,15 +353,16 @@ public class TaskViewTest extends ShellTestCase { assumeTrue(Transitions.ENABLE_SHELL_TRANSITIONS); SurfaceHolder sh = mock(SurfaceHolder.class); WindowContainerTransaction wct = new WindowContainerTransaction(); - mTaskView.prepareOpenAnimation(true /* newTask */, new SurfaceControl.Transaction(), - new SurfaceControl.Transaction(), mTaskInfo, mLeash, wct); + mTaskViewTaskController.prepareOpenAnimation(true /* newTask */, + new SurfaceControl.Transaction(), new SurfaceControl.Transaction(), mTaskInfo, + mLeash, wct); mTaskView.surfaceCreated(sh); reset(mViewListener); mTaskView.surfaceDestroyed(sh); - verify(mTaskViewTransitions).setTaskViewVisible(eq(mTaskView), eq(false)); + verify(mTaskViewTransitions).setTaskViewVisible(eq(mTaskViewTaskController), eq(false)); - mTaskView.prepareHideAnimation(new SurfaceControl.Transaction()); + mTaskViewTaskController.prepareHideAnimation(new SurfaceControl.Transaction()); verify(mViewListener).onTaskVisibilityChanged(eq(mTaskInfo.taskId), eq(false)); } @@ -346,24 +371,27 @@ public class TaskViewTest extends ShellTestCase { public void testOnReleased() { assumeTrue(Transitions.ENABLE_SHELL_TRANSITIONS); WindowContainerTransaction wct = new WindowContainerTransaction(); - mTaskView.prepareOpenAnimation(true /* newTask */, new SurfaceControl.Transaction(), - new SurfaceControl.Transaction(), mTaskInfo, mLeash, wct); + mTaskViewTaskController.prepareOpenAnimation(true /* newTask */, + new SurfaceControl.Transaction(), new SurfaceControl.Transaction(), mTaskInfo, + mLeash, wct); mTaskView.surfaceCreated(mock(SurfaceHolder.class)); mTaskView.release(); - verify(mOrganizer).removeListener(eq(mTaskView)); + verify(mOrganizer).removeListener(eq(mTaskViewTaskController)); verify(mViewListener).onReleased(); - verify(mTaskViewTransitions).removeTaskView(eq(mTaskView)); + assertThat(mTaskView.isInitialized()).isFalse(); + verify(mTaskViewTransitions).removeTaskView(eq(mTaskViewTaskController)); } @Test public void testOnTaskVanished() { assumeTrue(Transitions.ENABLE_SHELL_TRANSITIONS); WindowContainerTransaction wct = new WindowContainerTransaction(); - mTaskView.prepareOpenAnimation(true /* newTask */, new SurfaceControl.Transaction(), - new SurfaceControl.Transaction(), mTaskInfo, mLeash, wct); + mTaskViewTaskController.prepareOpenAnimation(true /* newTask */, + new SurfaceControl.Transaction(), new SurfaceControl.Transaction(), mTaskInfo, + mLeash, wct); mTaskView.surfaceCreated(mock(SurfaceHolder.class)); - mTaskView.prepareCloseAnimation(); + mTaskViewTaskController.prepareCloseAnimation(); verify(mViewListener).onTaskRemovalStarted(eq(mTaskInfo.taskId)); } @@ -372,9 +400,10 @@ public class TaskViewTest extends ShellTestCase { public void testOnBackPressedOnTaskRoot() { assumeTrue(Transitions.ENABLE_SHELL_TRANSITIONS); WindowContainerTransaction wct = new WindowContainerTransaction(); - mTaskView.prepareOpenAnimation(true /* newTask */, new SurfaceControl.Transaction(), - new SurfaceControl.Transaction(), mTaskInfo, mLeash, wct); - mTaskView.onBackPressedOnTaskRoot(mTaskInfo); + mTaskViewTaskController.prepareOpenAnimation(true /* newTask */, + new SurfaceControl.Transaction(), new SurfaceControl.Transaction(), mTaskInfo, + mLeash, wct); + mTaskViewTaskController.onBackPressedOnTaskRoot(mTaskInfo); verify(mViewListener).onBackPressedOnTaskRoot(eq(mTaskInfo.taskId)); } @@ -383,8 +412,9 @@ public class TaskViewTest extends ShellTestCase { public void testSetOnBackPressedOnTaskRoot() { assumeTrue(Transitions.ENABLE_SHELL_TRANSITIONS); WindowContainerTransaction wct = new WindowContainerTransaction(); - mTaskView.prepareOpenAnimation(true /* newTask */, new SurfaceControl.Transaction(), - new SurfaceControl.Transaction(), mTaskInfo, mLeash, wct); + mTaskViewTaskController.prepareOpenAnimation(true /* newTask */, + new SurfaceControl.Transaction(), new SurfaceControl.Transaction(), mTaskInfo, + mLeash, wct); verify(mOrganizer).setInterceptBackPressedOnTaskRoot(eq(mTaskInfo.token), eq(true)); } @@ -392,11 +422,12 @@ public class TaskViewTest extends ShellTestCase { public void testUnsetOnBackPressedOnTaskRoot() { assumeTrue(Transitions.ENABLE_SHELL_TRANSITIONS); WindowContainerTransaction wct = new WindowContainerTransaction(); - mTaskView.prepareOpenAnimation(true /* newTask */, new SurfaceControl.Transaction(), - new SurfaceControl.Transaction(), mTaskInfo, mLeash, wct); + mTaskViewTaskController.prepareOpenAnimation(true /* newTask */, + new SurfaceControl.Transaction(), new SurfaceControl.Transaction(), mTaskInfo, + mLeash, wct); verify(mOrganizer).setInterceptBackPressedOnTaskRoot(eq(mTaskInfo.token), eq(true)); - mTaskView.prepareCloseAnimation(); + mTaskViewTaskController.prepareCloseAnimation(); verify(mOrganizer).setInterceptBackPressedOnTaskRoot(eq(mTaskInfo.token), eq(false)); } 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..5cd548bfe5ab 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,17 +62,18 @@ 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.util.ArraySet; import android.view.Surface; import android.view.SurfaceControl; import android.view.WindowManager; import android.window.IRemoteTransition; import android.window.IRemoteTransitionFinishedCallback; +import android.window.IWindowContainerToken; import android.window.RemoteTransition; import android.window.TransitionFilter; import android.window.TransitionInfo; import android.window.TransitionRequestInfo; +import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; import android.window.WindowOrganizer; @@ -79,16 +83,24 @@ 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.TransitionInfoBuilder; 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.ShellController; +import com.android.wm.shell.sysui.ShellInit; +import com.android.wm.shell.sysui.ShellSharedConstants; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.InOrder; import java.util.ArrayList; +import java.util.function.Function; /** * Tests for the shell transitions. @@ -98,7 +110,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); @@ -111,8 +123,29 @@ public class ShellTransitionTests { @Before public void setUp() { - doAnswer(invocation -> invocation.getArguments()[1]) - .when(mOrganizer).startTransition(anyInt(), any(), any()); + doAnswer(invocation -> new Binder()) + .when(mOrganizer).startNewTransition(anyInt(), any()); + } + + @Test + public void instantiate_addInitCallback() { + ShellInit shellInit = mock(ShellInit.class); + final Transitions t = new Transitions(mContext, shellInit, mock(ShellController.class), + mOrganizer, mTransactionPool, createTestDisplayController(), mMainExecutor, + mMainHandler, mAnimExecutor); + verify(shellInit, times(1)).addInitCallback(any(), eq(t)); + } + + @Test + public void instantiateController_addExternalInterface() { + ShellInit shellInit = new ShellInit(mMainExecutor); + ShellController shellController = mock(ShellController.class); + final Transitions t = new Transitions(mContext, shellInit, shellController, + mOrganizer, mTransactionPool, createTestDisplayController(), mMainExecutor, + mMainHandler, mAnimExecutor); + shellInit.init(); + verify(shellController, times(1)).addExternalInterface( + eq(ShellSharedConstants.KEY_EXTRA_SHELL_SHELL_TRANSITIONS), any(), any()); } @Test @@ -123,7 +156,7 @@ public class ShellTransitionTests { IBinder transitToken = new Binder(); transitions.requestStartTransition(transitToken, new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */, null /* remote */)); - verify(mOrganizer, times(1)).startTransition(eq(TRANSIT_OPEN), eq(transitToken), any()); + verify(mOrganizer, times(1)).startTransition(eq(transitToken), any()); TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN) .addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build(); transitions.onTransitionReady(transitToken, info, mock(SurfaceControl.Transaction.class), @@ -175,7 +208,7 @@ public class ShellTransitionTests { // Make a request that will be rejected by the testhandler. transitions.requestStartTransition(transitToken, new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */, null /* remote */)); - verify(mOrganizer, times(1)).startTransition(eq(TRANSIT_OPEN), eq(transitToken), isNull()); + verify(mOrganizer, times(1)).startTransition(eq(transitToken), isNull()); transitions.onTransitionReady(transitToken, open, mock(SurfaceControl.Transaction.class), mock(SurfaceControl.Transaction.class)); assertEquals(1, mDefaultHandler.activeCount()); @@ -186,10 +219,12 @@ public class ShellTransitionTests { // Make a request that will be handled by testhandler but not animated by it. RunningTaskInfo mwTaskInfo = createTaskInfo(1, WINDOWING_MODE_MULTI_WINDOW, ACTIVITY_TYPE_STANDARD); + // Make the wct non-empty. + handlerWCT.setFocusable(new WindowContainerToken(mock(IWindowContainerToken.class)), true); transitions.requestStartTransition(transitToken, new TransitionRequestInfo(TRANSIT_OPEN, mwTaskInfo, null /* remote */)); verify(mOrganizer, times(1)).startTransition( - eq(TRANSIT_OPEN), eq(transitToken), eq(handlerWCT)); + eq(transitToken), eq(handlerWCT)); transitions.onTransitionReady(transitToken, open, mock(SurfaceControl.Transaction.class), mock(SurfaceControl.Transaction.class)); assertEquals(1, mDefaultHandler.activeCount()); @@ -204,8 +239,8 @@ public class ShellTransitionTests { transitions.addHandler(topHandler); transitions.requestStartTransition(transitToken, new TransitionRequestInfo(TRANSIT_CHANGE, mwTaskInfo, null /* remote */)); - verify(mOrganizer, times(1)).startTransition( - eq(TRANSIT_CHANGE), eq(transitToken), eq(handlerWCT)); + verify(mOrganizer, times(2)).startTransition( + eq(transitToken), eq(handlerWCT)); TransitionInfo change = new TransitionInfoBuilder(TRANSIT_CHANGE) .addChange(TRANSIT_CHANGE).build(); transitions.onTransitionReady(transitToken, change, mock(SurfaceControl.Transaction.class), @@ -242,8 +277,8 @@ public class ShellTransitionTests { IBinder transitToken = new Binder(); transitions.requestStartTransition(transitToken, new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */, - new RemoteTransition(testRemote))); - verify(mOrganizer, times(1)).startTransition(eq(TRANSIT_OPEN), eq(transitToken), any()); + new RemoteTransition(testRemote, "Test"))); + verify(mOrganizer, times(1)).startTransition(eq(transitToken), any()); TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN) .addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build(); transitions.onTransitionReady(transitToken, info, mock(SurfaceControl.Transaction.class), @@ -387,13 +422,13 @@ public class ShellTransitionTests { new TransitionFilter.Requirement[]{new TransitionFilter.Requirement()}; filter.mRequirements[0].mModes = new int[]{TRANSIT_OPEN, TRANSIT_TO_FRONT}; - transitions.registerRemote(filter, new RemoteTransition(testRemote)); + transitions.registerRemote(filter, new RemoteTransition(testRemote, "Test")); mMainExecutor.flushAll(); IBinder transitToken = new Binder(); transitions.requestStartTransition(transitToken, new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */, null /* remote */)); - verify(mOrganizer, times(1)).startTransition(eq(TRANSIT_OPEN), eq(transitToken), any()); + verify(mOrganizer, times(1)).startTransition(eq(transitToken), any()); TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN) .addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build(); transitions.onTransitionReady(transitToken, info, mock(SurfaceControl.Transaction.class), @@ -431,11 +466,12 @@ public class ShellTransitionTests { final int transitType = TRANSIT_FIRST_CUSTOM + 1; OneShotRemoteHandler oneShot = new OneShotRemoteHandler(mMainExecutor, - new RemoteTransition(testRemote)); + new RemoteTransition(testRemote, "Test")); // Verify that it responds to the remote but not other things. IBinder transitToken = new Binder(); assertNotNull(oneShot.handleRequest(transitToken, - new TransitionRequestInfo(transitType, null, new RemoteTransition(testRemote)))); + new TransitionRequestInfo(transitType, null, + new RemoteTransition(testRemote, "Test")))); assertNull(oneShot.handleRequest(transitToken, new TransitionRequestInfo(transitType, null, null))); @@ -530,6 +566,121 @@ public class ShellTransitionTests { assertEquals(0, mDefaultHandler.activeCount()); } + + @Test + public void testTransitionMergingOnFinish() { + final Transitions transitions = createTestTransitions(); + transitions.replaceDefaultHandlerForTest(mDefaultHandler); + + // The current transition. + final IBinder transitToken1 = new Binder(); + requestStartTransition(transitions, transitToken1); + onTransitionReady(transitions, transitToken1); + + // The next ready transition. + final IBinder transitToken2 = new Binder(); + requestStartTransition(transitions, transitToken2); + onTransitionReady(transitions, transitToken2); + + // The non-ready merge candidate. + final IBinder transitTokenNotReady = new Binder(); + requestStartTransition(transitions, transitTokenNotReady); + + mDefaultHandler.setSimulateMerge(true); + mDefaultHandler.mFinishes.get(0).onTransitionFinished(null /* wct */, null /* wctCB */); + + // Make sure that the non-ready transition is not merged. + assertEquals(0, mDefaultHandler.mergeCount()); + } + + @Test + public void testInterleavedMerging() { + Transitions transitions = createTestTransitions(); + transitions.replaceDefaultHandlerForTest(mDefaultHandler); + + Function<Boolean, IBinder> startATransition = (doMerge) -> { + IBinder token = new Binder(); + if (doMerge) { + mDefaultHandler.setShouldMerge(token); + } + transitions.requestStartTransition(token, + new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */, null /* remote */)); + TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN) + .addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build(); + transitions.onTransitionReady(token, info, mock(SurfaceControl.Transaction.class), + mock(SurfaceControl.Transaction.class)); + return token; + }; + + IBinder transitToken1 = startATransition.apply(false); + // merge first one + IBinder transitToken2 = startATransition.apply(true); + assertEquals(1, mDefaultHandler.activeCount()); + assertEquals(1, mDefaultHandler.mergeCount()); + + // don't merge next one + IBinder transitToken3 = startATransition.apply(false); + // make sure nothing happened (since it wasn't merged) + assertEquals(1, mDefaultHandler.activeCount()); + assertEquals(1, mDefaultHandler.mergeCount()); + + // make a mergable + IBinder transitToken4 = startATransition.apply(true); + // make sure nothing happened since there is a non-mergable pending. + assertEquals(1, mDefaultHandler.activeCount()); + assertEquals(1, mDefaultHandler.mergeCount()); + + // Queue up another mergable + IBinder transitToken5 = startATransition.apply(true); + + // Queue up a non-mergable + IBinder transitToken6 = startATransition.apply(false); + + // Our active now looks like: [playing, merged] + // and ready queue: [non-mergable, mergable, mergable, non-mergable] + // finish the playing one + mDefaultHandler.finishOne(); + mMainExecutor.flushAll(); + // Now we should have the non-mergable playing now with 2 merged: + // active: [playing, merged, merged] queue: [non-mergable] + assertEquals(1, mDefaultHandler.activeCount()); + assertEquals(2, mDefaultHandler.mergeCount()); + + mDefaultHandler.finishOne(); + mMainExecutor.flushAll(); + assertEquals(1, mDefaultHandler.activeCount()); + assertEquals(0, mDefaultHandler.mergeCount()); + + mDefaultHandler.finishOne(); + mMainExecutor.flushAll(); + } + + @Test + public void testTransitionOrderMatchesCore() { + Transitions transitions = createTestTransitions(); + transitions.replaceDefaultHandlerForTest(mDefaultHandler); + + IBinder transitToken = new Binder(); + IBinder shellInit = transitions.startTransition(TRANSIT_CLOSE, + new WindowContainerTransaction(), null /* handler */); + // make sure we are testing the "New" API. + verify(mOrganizer, times(1)).startNewTransition(eq(TRANSIT_CLOSE), any()); + // WMCore may not receive the new transition before requesting its own. + transitions.requestStartTransition(transitToken, + new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */, null /* remote */)); + verify(mOrganizer, times(1)).startTransition(eq(transitToken), any()); + + // At this point, WM is working on its transition (the shell-initialized one is still + // queued), so continue the transition lifecycle for that. + TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN) + .addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build(); + transitions.onTransitionReady(transitToken, info, mock(SurfaceControl.Transaction.class), + mock(SurfaceControl.Transaction.class)); + // At this point, if things are not working, we'd get an NPE due to attempting to merge + // into the shellInit transition which hasn't started yet. + assertEquals(1, mDefaultHandler.activeCount()); + } + @Test public void testShouldRotateSeamlessly() throws Exception { final RunningTaskInfo taskInfo = @@ -541,64 +692,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,48 +842,228 @@ public class ShellTransitionTests { verify(runnable4, times(1)).run(); } - class TransitionInfoBuilder { - final TransitionInfo mInfo; + @Test + public void testObserverLifecycle_basicTransitionFlow() { + Transitions transitions = createTestTransitions(); + Transitions.TransitionObserver observer = mock(Transitions.TransitionObserver.class); + transitions.registerObserver(observer); + transitions.replaceDefaultHandlerForTest(mDefaultHandler); - TransitionInfoBuilder(@WindowManager.TransitionType int type) { - this(type, 0 /* flags */); - } + 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); + } - TransitionInfoBuilder(@WindowManager.TransitionType int type, - @WindowManager.TransitionFlags int flags) { - mInfo = new TransitionInfo(type, flags); - mInfo.setRootLeash(createMockSurface(true /* valid */), 0, 0); - } + @Test + public void testObserverLifecycle_queueing() { + Transitions transitions = createTestTransitions(); + Transitions.TransitionObserver observer = mock(Transitions.TransitionObserver.class); + transitions.registerObserver(observer); + transitions.replaceDefaultHandlerForTest(mDefaultHandler); - TransitionInfoBuilder addChange(@WindowManager.TransitionType int mode, - RunningTaskInfo taskInfo) { - final TransitionInfo.Change change = - new TransitionInfo.Change(null /* token */, null /* leash */); - change.setMode(mode); - change.setTaskInfo(taskInfo); - mInfo.addChange(change); - return this; - } + 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); - TransitionInfoBuilder addChange(@WindowManager.TransitionType int mode) { - return addChange(mode, null /* taskInfo */); - } + 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()); - TransitionInfoBuilder addChange(TransitionInfo.Change change) { - mInfo.addChange(change); - return this; - } + 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()); - TransitionInfo build() { - return mInfo; - } + 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()); + } + + @Test + public void testEmptyTransitionStillReportsKeyguardGoingAway() { + Transitions transitions = createTestTransitions(); + transitions.replaceDefaultHandlerForTest(mDefaultHandler); + + IBinder transitToken = new Binder(); + transitions.requestStartTransition(transitToken, + new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */, null /* remote */)); + + // Make a no-op transition + TransitionInfo info = new TransitionInfoBuilder( + TRANSIT_OPEN, TRANSIT_FLAG_KEYGUARD_GOING_AWAY, true /* noOp */).build(); + transitions.onTransitionReady(transitToken, info, mock(SurfaceControl.Transaction.class), + mock(SurfaceControl.Transaction.class)); + + // If keyguard-going-away flag set, then it shouldn't be aborted. + assertEquals(1, mDefaultHandler.activeCount()); } class ChangeBuilder { final TransitionInfo.Change mChange; ChangeBuilder(@WindowManager.TransitionType int mode) { - mChange = new TransitionInfo.Change(null /* token */, null /* leash */); + mChange = new TransitionInfo.Change(null /* token */, createMockSurface(true)); mChange.setMode(mode); } @@ -756,6 +1100,7 @@ public class ShellTransitionTests { ArrayList<Transitions.TransitionFinishCallback> mFinishes = new ArrayList<>(); final ArrayList<IBinder> mMerged = new ArrayList<>(); boolean mSimulateMerge = false; + final ArraySet<IBinder> mShouldMerge = new ArraySet<>(); @Override public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, @@ -770,7 +1115,7 @@ public class ShellTransitionTests { public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget, @NonNull Transitions.TransitionFinishCallback finishCallback) { - if (!mSimulateMerge) return; + if (!(mSimulateMerge || mShouldMerge.contains(transition))) return; mMerged.add(transition); finishCallback.onTransitionFinished(null /* wct */, null /* wctCB */); } @@ -786,12 +1131,23 @@ public class ShellTransitionTests { mSimulateMerge = sim; } + void setShouldMerge(IBinder toMerge) { + mShouldMerge.add(toMerge); + } + void finishAll() { final ArrayList<Transitions.TransitionFinishCallback> finishes = mFinishes; mFinishes = new ArrayList<>(); for (int i = finishes.size() - 1; i >= 0; --i) { finishes.get(i).onTransitionFinished(null /* wct */, null /* wctCB */); } + mShouldMerge.clear(); + } + + void finishOne() { + Transitions.TransitionFinishCallback fin = mFinishes.remove(0); + mMerged.clear(); + fin.onTransitionFinished(null /* wct */, null /* wctCB */); } int activeCount() { @@ -803,6 +1159,21 @@ public class ShellTransitionTests { } } + private static void requestStartTransition(Transitions transitions, IBinder token) { + transitions.requestStartTransition(token, + new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */, null /* remote */)); + } + + private static void onTransitionReady(Transitions transitions, IBinder token) { + transitions.onTransitionReady(token, createTransitionInfo(), + mock(SurfaceControl.Transaction.class), mock(SurfaceControl.Transaction.class)); + } + + private static TransitionInfo createTransitionInfo() { + return new TransitionInfoBuilder(TRANSIT_OPEN) + .addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build(); + } + private static SurfaceControl createMockSurface(boolean valid) { SurfaceControl sc = mock(SurfaceControl.class); doReturn(valid).when(sc).isValid(); @@ -824,33 +1195,22 @@ 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); - } -// -// private class TestDisplayController extends DisplayController { -// private final DisplayLayout mTestDisplayLayout; -// TestDisplayController() { -// super(mContext, mock(IWindowManager.class), mMainExecutor); -// mTestDisplayLayout = new DisplayLayout(); -// mTestDisplayLayout. -// } -// -// @Override -// DisplayLayout -// } - + ShellInit shellInit = new ShellInit(mMainExecutor); + final Transitions t = new Transitions(mContext, shellInit, mock(ShellController.class), + mOrganizer, mTransactionPool, createTestDisplayController(), mMainExecutor, + mMainHandler, mAnimExecutor); + shellInit.init(); + return t; + } } 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..8196c5ab08e4 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/unfold/UnfoldAnimationControllerTest.java @@ -0,0 +1,371 @@ +/* + * 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.onStateChangeStarted(); + mUnfoldAnimationController.onTaskVanished(taskInfo); + mUnfoldAnimationController.onStateChangeFinished(); + + assertThat(mTaskAnimator1.mResetTasks).contains(taskInfo.taskId); + } + + @Test + public void testApplicableTaskDisappeared_noStateChange_doesNotResetSurface() { + 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).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.onStateChangeStarted(); + mUnfoldAnimationController.onTaskVanished(taskInfo); + mUnfoldAnimationController.onStateChangeFinished(); + + 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/DesktopModeWindowDecorViewModelTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.java new file mode 100644 index 000000000000..9a90996b786c --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.java @@ -0,0 +1,268 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.wm.shell.windowdecor; + +import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; +import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED; +import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; +import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; + +import static org.mockito.Mockito.any; +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 static org.mockito.Mockito.when; + +import android.app.ActivityManager; +import android.app.WindowConfiguration; +import android.hardware.display.DisplayManager; +import android.hardware.display.VirtualDisplay; +import android.hardware.input.InputManager; +import android.os.Handler; +import android.os.Looper; +import android.view.Choreographer; +import android.view.Display; +import android.view.InputChannel; +import android.view.InputMonitor; +import android.view.SurfaceControl; +import android.view.SurfaceView; + +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 com.android.wm.shell.desktopmode.DesktopModeController; +import com.android.wm.shell.desktopmode.DesktopTasksController; +import com.android.wm.shell.splitscreen.SplitScreenController; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; + +/** Tests of {@link DesktopModeWindowDecorViewModel} */ +@SmallTest +public class DesktopModeWindowDecorViewModelTests extends ShellTestCase { + + private static final String TAG = "DesktopModeWindowDecorViewModelTests"; + + @Mock private DesktopModeWindowDecoration mDesktopModeWindowDecoration; + @Mock private DesktopModeWindowDecoration.Factory mDesktopModeWindowDecorFactory; + + @Mock private Handler mMainHandler; + @Mock private Choreographer mMainChoreographer; + @Mock private ShellTaskOrganizer mTaskOrganizer; + @Mock private DisplayController mDisplayController; + @Mock private SplitScreenController mSplitScreenController; + @Mock private SyncTransactionQueue mSyncQueue; + @Mock private DesktopModeController mDesktopModeController; + @Mock private DesktopTasksController mDesktopTasksController; + @Mock private InputMonitor mInputMonitor; + @Mock private InputManager mInputManager; + @Mock private DesktopModeWindowDecorViewModel.InputMonitorFactory mMockInputMonitorFactory; + @Mock private Supplier<SurfaceControl.Transaction> mTransactionFactory; + @Mock private SurfaceControl.Transaction mTransaction; + private final List<InputManager> mMockInputManagers = new ArrayList<>(); + + private DesktopModeWindowDecorViewModel mDesktopModeWindowDecorViewModel; + + @Before + public void setUp() { + mMockInputManagers.add(mInputManager); + + mDesktopModeWindowDecorViewModel = + new DesktopModeWindowDecorViewModel( + mContext, + mMainHandler, + mMainChoreographer, + mTaskOrganizer, + mDisplayController, + mSyncQueue, + Optional.of(mDesktopModeController), + Optional.of(mDesktopTasksController), + Optional.of(mSplitScreenController), + mDesktopModeWindowDecorFactory, + mMockInputMonitorFactory, + mTransactionFactory + ); + + doReturn(mDesktopModeWindowDecoration) + .when(mDesktopModeWindowDecorFactory) + .create(any(), any(), any(), any(), any(), any(), any(), any()); + doReturn(mTransaction).when(mTransactionFactory).get(); + + when(mMockInputMonitorFactory.create(any(), any())).thenReturn(mInputMonitor); + // InputChannel cannot be mocked because it passes to InputEventReceiver. + final InputChannel[] inputChannels = InputChannel.openInputChannelPair(TAG); + inputChannels[0].dispose(); + when(mInputMonitor.getInputChannel()).thenReturn(inputChannels[1]); + } + + @Test + public void testDeleteCaptionOnChangeTransitionWhenNecessary() throws Exception { + final int taskId = 1; + final ActivityManager.RunningTaskInfo taskInfo = + createTaskInfo(taskId, Display.DEFAULT_DISPLAY, WINDOWING_MODE_FREEFORM); + SurfaceControl surfaceControl = mock(SurfaceControl.class); + runOnMainThread(() -> { + final SurfaceControl.Transaction startT = mock(SurfaceControl.Transaction.class); + final SurfaceControl.Transaction finishT = mock(SurfaceControl.Transaction.class); + + mDesktopModeWindowDecorViewModel.onTaskOpening( + taskInfo, surfaceControl, startT, finishT); + + taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_UNDEFINED); + taskInfo.configuration.windowConfiguration.setActivityType(ACTIVITY_TYPE_UNDEFINED); + mDesktopModeWindowDecorViewModel.onTaskChanging( + taskInfo, surfaceControl, startT, finishT); + }); + verify(mDesktopModeWindowDecorFactory) + .create( + mContext, + mDisplayController, + mTaskOrganizer, + taskInfo, + surfaceControl, + mMainHandler, + mMainChoreographer, + mSyncQueue); + verify(mDesktopModeWindowDecoration).close(); + } + + @Test + public void testCreateCaptionOnChangeTransitionWhenNecessary() throws Exception { + final int taskId = 1; + final ActivityManager.RunningTaskInfo taskInfo = + createTaskInfo(taskId, Display.DEFAULT_DISPLAY, WINDOWING_MODE_UNDEFINED); + SurfaceControl surfaceControl = mock(SurfaceControl.class); + runOnMainThread(() -> { + final SurfaceControl.Transaction startT = mock(SurfaceControl.Transaction.class); + final SurfaceControl.Transaction finishT = mock(SurfaceControl.Transaction.class); + taskInfo.configuration.windowConfiguration.setActivityType(ACTIVITY_TYPE_UNDEFINED); + + mDesktopModeWindowDecorViewModel.onTaskChanging( + taskInfo, surfaceControl, startT, finishT); + + taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM); + taskInfo.configuration.windowConfiguration.setActivityType(ACTIVITY_TYPE_STANDARD); + + mDesktopModeWindowDecorViewModel.onTaskChanging( + taskInfo, surfaceControl, startT, finishT); + }); + verify(mDesktopModeWindowDecorFactory, times(1)) + .create( + mContext, + mDisplayController, + mTaskOrganizer, + taskInfo, + surfaceControl, + mMainHandler, + mMainChoreographer, + mSyncQueue); + } + + @Test + public void testCreateAndDisposeEventReceiver() throws Exception { + final int taskId = 1; + final ActivityManager.RunningTaskInfo taskInfo = + createTaskInfo(taskId, Display.DEFAULT_DISPLAY, WINDOWING_MODE_FREEFORM); + taskInfo.configuration.windowConfiguration.setActivityType(ACTIVITY_TYPE_STANDARD); + runOnMainThread(() -> { + SurfaceControl surfaceControl = mock(SurfaceControl.class); + final SurfaceControl.Transaction startT = mock(SurfaceControl.Transaction.class); + final SurfaceControl.Transaction finishT = mock(SurfaceControl.Transaction.class); + + mDesktopModeWindowDecorViewModel.onTaskOpening( + taskInfo, surfaceControl, startT, finishT); + + mDesktopModeWindowDecorViewModel.destroyWindowDecoration(taskInfo); + }); + verify(mMockInputMonitorFactory).create(any(), any()); + verify(mInputMonitor).dispose(); + } + + @Test + public void testEventReceiversOnMultipleDisplays() throws Exception { + runOnMainThread(() -> { + SurfaceView surfaceView = new SurfaceView(mContext); + final DisplayManager mDm = mContext.getSystemService(DisplayManager.class); + final VirtualDisplay secondaryDisplay = mDm.createVirtualDisplay( + "testEventReceiversOnMultipleDisplays", /*width=*/ 400, /*height=*/ 400, + /*densityDpi=*/ 320, surfaceView.getHolder().getSurface(), + DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY); + try { + int secondaryDisplayId = secondaryDisplay.getDisplay().getDisplayId(); + + final int taskId = 1; + final ActivityManager.RunningTaskInfo taskInfo = + createTaskInfo(taskId, Display.DEFAULT_DISPLAY, WINDOWING_MODE_FREEFORM); + final ActivityManager.RunningTaskInfo secondTaskInfo = + createTaskInfo(taskId + 1, secondaryDisplayId, WINDOWING_MODE_FREEFORM); + final ActivityManager.RunningTaskInfo thirdTaskInfo = + createTaskInfo(taskId + 2, secondaryDisplayId, WINDOWING_MODE_FREEFORM); + + SurfaceControl surfaceControl = mock(SurfaceControl.class); + final SurfaceControl.Transaction startT = mock(SurfaceControl.Transaction.class); + final SurfaceControl.Transaction finishT = mock(SurfaceControl.Transaction.class); + + mDesktopModeWindowDecorViewModel.onTaskOpening(taskInfo, surfaceControl, startT, + finishT); + mDesktopModeWindowDecorViewModel.onTaskOpening(secondTaskInfo, surfaceControl, + startT, finishT); + mDesktopModeWindowDecorViewModel.onTaskOpening(thirdTaskInfo, surfaceControl, + startT, finishT); + mDesktopModeWindowDecorViewModel.destroyWindowDecoration(thirdTaskInfo); + mDesktopModeWindowDecorViewModel.destroyWindowDecoration(taskInfo); + } finally { + secondaryDisplay.release(); + } + }); + verify(mMockInputMonitorFactory, times(2)).create(any(), any()); + verify(mInputMonitor, times(1)).dispose(); + } + + private void runOnMainThread(Runnable r) throws Exception { + final Handler mainHandler = new Handler(Looper.getMainLooper()); + final CountDownLatch latch = new CountDownLatch(1); + mainHandler.post(() -> { + r.run(); + latch.countDown(); + }); + latch.await(1, TimeUnit.SECONDS); + } + + private static ActivityManager.RunningTaskInfo createTaskInfo(int taskId, + int displayId, @WindowConfiguration.WindowingMode int windowingMode) { + ActivityManager.RunningTaskInfo taskInfo = + new TestRunningTaskInfoBuilder() + .setDisplayId(displayId) + .setVisible(true) + .build(); + taskInfo.taskId = taskId; + taskInfo.configuration.windowConfiguration.setWindowingMode(windowingMode); + return taskInfo; + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragDetectorTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragDetectorTest.kt new file mode 100644 index 000000000000..8f84008e8d2d --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragDetectorTest.kt @@ -0,0 +1,210 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.windowdecor + +import android.os.SystemClock +import android.testing.AndroidTestingRunner +import android.view.MotionEvent +import android.view.InputDevice +import androidx.test.filters.SmallTest +import org.junit.After +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.MockitoAnnotations +import org.mockito.Mockito.`when` +import org.mockito.Mockito.any +import org.mockito.Mockito.argThat +import org.mockito.Mockito.never +import org.mockito.Mockito.verify + +/** + * Tests for [DragDetector]. + * + * Build/Install/Run: + * atest WMShellUnitTests:DragDetectorTest + */ +@SmallTest +@RunWith(AndroidTestingRunner::class) +class DragDetectorTest { + private val motionEvents = mutableListOf<MotionEvent>() + + @Mock + private lateinit var eventHandler: DragDetector.MotionEventHandler + + private lateinit var dragDetector: DragDetector + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + + `when`(eventHandler.handleMotionEvent(any())).thenReturn(true) + + dragDetector = DragDetector(eventHandler) + dragDetector.setTouchSlop(SLOP) + } + + @After + fun tearDown() { + motionEvents.forEach { + it.recycle() + } + motionEvents.clear() + } + + @Test + fun testNoMove_passesDownAndUp() { + assertTrue(dragDetector.onMotionEvent(createMotionEvent(MotionEvent.ACTION_DOWN))) + verify(eventHandler).handleMotionEvent(argThat { + return@argThat it.action == MotionEvent.ACTION_DOWN && it.x == X && it.y == Y && + it.source == InputDevice.SOURCE_TOUCHSCREEN + }) + + assertTrue(dragDetector.onMotionEvent(createMotionEvent(MotionEvent.ACTION_UP))) + verify(eventHandler).handleMotionEvent(argThat { + return@argThat it.action == MotionEvent.ACTION_UP && it.x == X && it.y == Y && + it.source == InputDevice.SOURCE_TOUCHSCREEN + }) + } + + @Test + fun testMoveInSlop_touch_passesDownAndUp() { + `when`(eventHandler.handleMotionEvent(argThat { + return@argThat it.action == MotionEvent.ACTION_DOWN + })).thenReturn(false) + + assertFalse(dragDetector.onMotionEvent(createMotionEvent(MotionEvent.ACTION_DOWN))) + verify(eventHandler).handleMotionEvent(argThat { + return@argThat it.action == MotionEvent.ACTION_DOWN && it.x == X && it.y == Y && + it.source == InputDevice.SOURCE_TOUCHSCREEN + }) + + val newX = X + SLOP - 1 + assertFalse( + dragDetector.onMotionEvent(createMotionEvent(MotionEvent.ACTION_MOVE, newX, Y))) + verify(eventHandler, never()).handleMotionEvent(argThat { + return@argThat it.action == MotionEvent.ACTION_MOVE + }) + + assertTrue(dragDetector.onMotionEvent(createMotionEvent(MotionEvent.ACTION_UP, newX, Y))) + verify(eventHandler).handleMotionEvent(argThat { + return@argThat it.action == MotionEvent.ACTION_UP && it.x == newX && it.y == Y && + it.source == InputDevice.SOURCE_TOUCHSCREEN + }) + } + + @Test + fun testMoveInSlop_mouse_passesDownMoveAndUp() { + `when`(eventHandler.handleMotionEvent(argThat { + it.action == MotionEvent.ACTION_DOWN + })).thenReturn(false) + + assertFalse(dragDetector.onMotionEvent( + createMotionEvent(MotionEvent.ACTION_DOWN, isTouch = false))) + verify(eventHandler).handleMotionEvent(argThat { + return@argThat it.action == MotionEvent.ACTION_DOWN && it.x == X && it.y == Y && + it.source == InputDevice.SOURCE_MOUSE + }) + + val newX = X + SLOP - 1 + assertTrue(dragDetector.onMotionEvent( + createMotionEvent(MotionEvent.ACTION_MOVE, newX, Y, isTouch = false))) + verify(eventHandler).handleMotionEvent(argThat { + return@argThat it.action == MotionEvent.ACTION_MOVE && it.x == newX && it.y == Y && + it.source == InputDevice.SOURCE_MOUSE + }) + + assertTrue(dragDetector.onMotionEvent( + createMotionEvent(MotionEvent.ACTION_UP, newX, Y, isTouch = false))) + verify(eventHandler).handleMotionEvent(argThat { + return@argThat it.action == MotionEvent.ACTION_UP && it.x == newX && it.y == Y && + it.source == InputDevice.SOURCE_MOUSE + }) + } + + @Test + fun testMoveBeyondSlop_passesDownMoveAndUp() { + `when`(eventHandler.handleMotionEvent(argThat { + it.action == MotionEvent.ACTION_DOWN + })).thenReturn(false) + + assertFalse(dragDetector.onMotionEvent(createMotionEvent(MotionEvent.ACTION_DOWN))) + verify(eventHandler).handleMotionEvent(argThat { + return@argThat it.action == MotionEvent.ACTION_DOWN && it.x == X && it.y == Y && + it.source == InputDevice.SOURCE_TOUCHSCREEN + }) + + val newX = X + SLOP + 1 + assertTrue(dragDetector.onMotionEvent(createMotionEvent(MotionEvent.ACTION_MOVE, newX, Y))) + verify(eventHandler).handleMotionEvent(argThat { + return@argThat it.action == MotionEvent.ACTION_MOVE && it.x == newX && it.y == Y && + it.source == InputDevice.SOURCE_TOUCHSCREEN + }) + + assertTrue(dragDetector.onMotionEvent(createMotionEvent(MotionEvent.ACTION_UP, newX, Y))) + verify(eventHandler).handleMotionEvent(argThat { + return@argThat it.action == MotionEvent.ACTION_UP && it.x == newX && it.y == Y && + it.source == InputDevice.SOURCE_TOUCHSCREEN + }) + } + + @Test + fun testPassesHoverEnter() { + `when`(eventHandler.handleMotionEvent(argThat { + it.action == MotionEvent.ACTION_HOVER_ENTER + })).thenReturn(false) + + assertFalse(dragDetector.onMotionEvent(createMotionEvent(MotionEvent.ACTION_HOVER_ENTER))) + verify(eventHandler).handleMotionEvent(argThat { + return@argThat it.action == MotionEvent.ACTION_HOVER_ENTER && it.x == X && it.y == Y + }) + } + + @Test + fun testPassesHoverMove() { + assertTrue(dragDetector.onMotionEvent(createMotionEvent(MotionEvent.ACTION_HOVER_MOVE))) + verify(eventHandler).handleMotionEvent(argThat { + return@argThat it.action == MotionEvent.ACTION_HOVER_MOVE && it.x == X && it.y == Y + }) + } + + @Test + fun testPassesHoverExit() { + assertTrue(dragDetector.onMotionEvent(createMotionEvent(MotionEvent.ACTION_HOVER_EXIT))) + verify(eventHandler).handleMotionEvent(argThat { + return@argThat it.action == MotionEvent.ACTION_HOVER_EXIT && it.x == X && it.y == Y + }) + } + + private fun createMotionEvent(action: Int, x: Float = X, y: Float = Y, isTouch: Boolean = true): + MotionEvent { + val time = SystemClock.uptimeMillis() + val ev = MotionEvent.obtain(time, time, action, x, y, 0) + ev.source = if (isTouch) InputDevice.SOURCE_TOUCHSCREEN else InputDevice.SOURCE_MOUSE + motionEvents.add(ev) + return ev + } + + companion object { + private const val SLOP = 10 + private const val X = 123f + private const val Y = 234f + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/TaskPositionerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/TaskPositionerTest.kt new file mode 100644 index 000000000000..94c064bda763 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/TaskPositionerTest.kt @@ -0,0 +1,553 @@ +package com.android.wm.shell.windowdecor + +import android.app.ActivityManager +import android.app.WindowConfiguration +import android.graphics.Rect +import android.os.IBinder +import android.testing.AndroidTestingRunner +import android.view.Display +import android.window.WindowContainerToken +import android.window.WindowContainerTransaction +import android.window.WindowContainerTransaction.Change.CHANGE_DRAG_RESIZING +import androidx.test.filters.SmallTest +import com.android.wm.shell.common.DisplayController +import com.android.wm.shell.common.DisplayLayout +import com.android.wm.shell.ShellTaskOrganizer +import com.android.wm.shell.ShellTestCase +import com.android.wm.shell.windowdecor.TaskPositioner.CTRL_TYPE_BOTTOM +import com.android.wm.shell.windowdecor.TaskPositioner.CTRL_TYPE_RIGHT +import com.android.wm.shell.windowdecor.TaskPositioner.CTRL_TYPE_TOP +import com.android.wm.shell.windowdecor.TaskPositioner.CTRL_TYPE_UNDEFINED +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.`when` +import org.mockito.Mockito.any +import org.mockito.Mockito.argThat +import org.mockito.Mockito.never +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +/** + * Tests for [TaskPositioner]. + * + * Build/Install/Run: + * atest WMShellUnitTests:TaskPositionerTest + */ +@SmallTest +@RunWith(AndroidTestingRunner::class) +class TaskPositionerTest : ShellTestCase() { + + @Mock + private lateinit var mockShellTaskOrganizer: ShellTaskOrganizer + @Mock + private lateinit var mockWindowDecoration: WindowDecoration<*> + @Mock + private lateinit var mockDragStartListener: TaskPositioner.DragStartListener + + @Mock + private lateinit var taskToken: WindowContainerToken + @Mock + private lateinit var taskBinder: IBinder + + @Mock + private lateinit var mockDisplayController: DisplayController + @Mock + private lateinit var mockDisplayLayout: DisplayLayout + @Mock + private lateinit var mockDisplay: Display + + private lateinit var taskPositioner: TaskPositioner + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + + taskPositioner = TaskPositioner( + mockShellTaskOrganizer, + mockWindowDecoration, + mockDisplayController, + mockDragStartListener + ) + + `when`(taskToken.asBinder()).thenReturn(taskBinder) + `when`(mockDisplayController.getDisplayLayout(DISPLAY_ID)).thenReturn(mockDisplayLayout) + `when`(mockDisplayLayout.densityDpi()).thenReturn(DENSITY_DPI) + `when`(mockDisplayLayout.getStableBounds(any())).thenAnswer { i -> + (i.arguments.first() as Rect).set(STABLE_BOUNDS) + } + + mockWindowDecoration.mTaskInfo = ActivityManager.RunningTaskInfo().apply { + taskId = TASK_ID + token = taskToken + minWidth = MIN_WIDTH + minHeight = MIN_HEIGHT + defaultMinSize = DEFAULT_MIN + displayId = DISPLAY_ID + configuration.windowConfiguration.bounds = STARTING_BOUNDS + } + mockWindowDecoration.mDisplay = mockDisplay + `when`(mockDisplay.displayId).thenAnswer { DISPLAY_ID } + } + + @Test + fun testDragResize_notMove_skipsTransactionOnEnd() { + taskPositioner.onDragPositioningStart( + CTRL_TYPE_TOP or CTRL_TYPE_RIGHT, + STARTING_BOUNDS.left.toFloat(), + STARTING_BOUNDS.top.toFloat() + ) + + taskPositioner.onDragPositioningEnd( + STARTING_BOUNDS.left.toFloat() + 10, + STARTING_BOUNDS.top.toFloat() + 10 + ) + + verify(mockShellTaskOrganizer, never()).applyTransaction(argThat { wct -> + return@argThat wct.changes.any { (token, change) -> + token == taskBinder && + ((change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != 0) + } + }) + } + + @Test + fun testDragResize_noEffectiveMove_skipsTransactionOnMoveAndEnd() { + taskPositioner.onDragPositioningStart( + CTRL_TYPE_TOP or CTRL_TYPE_RIGHT, + STARTING_BOUNDS.left.toFloat(), + STARTING_BOUNDS.top.toFloat() + ) + + taskPositioner.onDragPositioningMove( + STARTING_BOUNDS.left.toFloat(), + STARTING_BOUNDS.top.toFloat() + ) + + taskPositioner.onDragPositioningEnd( + STARTING_BOUNDS.left.toFloat() + 10, + STARTING_BOUNDS.top.toFloat() + 10 + ) + + verify(mockShellTaskOrganizer, never()).applyTransaction(argThat { wct -> + return@argThat wct.changes.any { (token, change) -> + token == taskBinder && + ((change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != 0) + } + }) + } + + @Test + fun testDragResize_hasEffectiveMove_issuesTransactionOnMoveAndEnd() { + taskPositioner.onDragPositioningStart( + CTRL_TYPE_TOP or CTRL_TYPE_RIGHT, + STARTING_BOUNDS.left.toFloat(), + STARTING_BOUNDS.top.toFloat() + ) + + taskPositioner.onDragPositioningMove( + STARTING_BOUNDS.left.toFloat() + 10, + STARTING_BOUNDS.top.toFloat() + ) + val rectAfterMove = Rect(STARTING_BOUNDS) + rectAfterMove.right += 10 + verify(mockShellTaskOrganizer).applyTransaction(argThat { wct -> + return@argThat wct.changes.any { (token, change) -> + token == taskBinder && + (change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != 0 && + change.configuration.windowConfiguration.bounds == rectAfterMove + } + }) + + taskPositioner.onDragPositioningEnd( + STARTING_BOUNDS.left.toFloat() + 10, + STARTING_BOUNDS.top.toFloat() + 10 + ) + val rectAfterEnd = Rect(rectAfterMove) + rectAfterEnd.top += 10 + verify(mockShellTaskOrganizer).applyTransaction(argThat { wct -> + return@argThat wct.changes.any { (token, change) -> + token == taskBinder && + (change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != 0 && + change.configuration.windowConfiguration.bounds == rectAfterEnd + } + }) + } + + @Test + fun testDragResize_move_skipsDragResizingFlag() { + taskPositioner.onDragPositioningStart( + CTRL_TYPE_UNDEFINED, // Move + STARTING_BOUNDS.left.toFloat(), + STARTING_BOUNDS.top.toFloat() + ) + + // Move the task 10px to the right. + val newX = STARTING_BOUNDS.left.toFloat() + 10 + val newY = STARTING_BOUNDS.top.toFloat() + taskPositioner.onDragPositioningMove( + newX, + newY + ) + + taskPositioner.onDragPositioningEnd(newX, newY) + + verify(mockShellTaskOrganizer, never()).applyTransaction(argThat { wct -> + return@argThat wct.changes.any { (token, change) -> + token == taskBinder && + ((change.changeMask and CHANGE_DRAG_RESIZING) != 0) && + change.dragResizing + } + }) + } + + @Test + fun testDragResize_resize_setsDragResizingFlag() { + taskPositioner.onDragPositioningStart( + CTRL_TYPE_RIGHT, // Resize right + STARTING_BOUNDS.left.toFloat(), + STARTING_BOUNDS.top.toFloat() + ) + + // Resize the task by 10px to the right. + val newX = STARTING_BOUNDS.right.toFloat() + 10 + val newY = STARTING_BOUNDS.top.toFloat() + taskPositioner.onDragPositioningMove( + newX, + newY + ) + + taskPositioner.onDragPositioningEnd(newX, newY) + + verify(mockShellTaskOrganizer).applyTransaction(argThat { wct -> + return@argThat wct.changes.any { (token, change) -> + token == taskBinder && + ((change.changeMask and CHANGE_DRAG_RESIZING) != 0) && + change.dragResizing + } + }) + verify(mockShellTaskOrganizer).applyTransaction(argThat { wct -> + return@argThat wct.changes.any { (token, change) -> + token == taskBinder && + ((change.changeMask and CHANGE_DRAG_RESIZING) != 0) && + !change.dragResizing + } + }) + } + + @Test + fun testDragResize_resize_setBoundsDoesNotChangeHeightWhenLessThanMin() { + taskPositioner.onDragPositioningStart( + CTRL_TYPE_RIGHT or CTRL_TYPE_TOP, // Resize right and top + STARTING_BOUNDS.right.toFloat(), + STARTING_BOUNDS.top.toFloat() + ) + + // Resize to width of 95px and height of 5px with min width of 10px + val newX = STARTING_BOUNDS.right.toFloat() - 5 + val newY = STARTING_BOUNDS.top.toFloat() + 95 + taskPositioner.onDragPositioningMove( + newX, + newY + ) + + taskPositioner.onDragPositioningEnd(newX, newY) + + verify(mockShellTaskOrganizer).applyTransaction(argThat { wct -> + return@argThat wct.changes.any { (token, change) -> + token == taskBinder && + ((change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) + != 0) && change.configuration.windowConfiguration.bounds.top == + STARTING_BOUNDS.top && + change.configuration.windowConfiguration.bounds.bottom == + STARTING_BOUNDS.bottom + } + }) + } + + @Test + fun testDragResize_resize_setBoundsDoesNotChangeWidthWhenLessThanMin() { + taskPositioner.onDragPositioningStart( + CTRL_TYPE_RIGHT or CTRL_TYPE_TOP, // Resize right and top + STARTING_BOUNDS.right.toFloat(), + STARTING_BOUNDS.top.toFloat() + ) + + // Resize to height of 95px and width of 5px with min width of 10px + val newX = STARTING_BOUNDS.right.toFloat() - 95 + val newY = STARTING_BOUNDS.top.toFloat() + 5 + taskPositioner.onDragPositioningMove( + newX, + newY + ) + + taskPositioner.onDragPositioningEnd(newX, newY) + + verify(mockShellTaskOrganizer).applyTransaction(argThat { wct -> + return@argThat wct.changes.any { (token, change) -> + token == taskBinder && + ((change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) + != 0) && change.configuration.windowConfiguration.bounds.right == + STARTING_BOUNDS.right && + change.configuration.windowConfiguration.bounds.left == + STARTING_BOUNDS.left + } + }) + } + + @Test + fun testDragResize_resize_setBoundsDoesNotChangeHeightWhenNegative() { + taskPositioner.onDragPositioningStart( + CTRL_TYPE_RIGHT or CTRL_TYPE_TOP, // Resize right and top + STARTING_BOUNDS.right.toFloat(), + STARTING_BOUNDS.top.toFloat() + ) + + // Resize to height of -5px and width of 95px + val newX = STARTING_BOUNDS.right.toFloat() - 5 + val newY = STARTING_BOUNDS.top.toFloat() + 105 + taskPositioner.onDragPositioningMove( + newX, + newY + ) + + taskPositioner.onDragPositioningEnd(newX, newY) + + verify(mockShellTaskOrganizer).applyTransaction(argThat { wct -> + return@argThat wct.changes.any { (token, change) -> + token == taskBinder && + ((change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) + != 0) && change.configuration.windowConfiguration.bounds.top == + STARTING_BOUNDS.top && + change.configuration.windowConfiguration.bounds.bottom == + STARTING_BOUNDS.bottom + } + }) + } + + @Test + fun testDragResize_resize_setBoundsDoesNotChangeWidthWhenNegative() { + taskPositioner.onDragPositioningStart( + CTRL_TYPE_RIGHT or CTRL_TYPE_TOP, // Resize right and top + STARTING_BOUNDS.right.toFloat(), + STARTING_BOUNDS.top.toFloat() + ) + + // Resize to width of -5px and height of 95px + val newX = STARTING_BOUNDS.right.toFloat() - 105 + val newY = STARTING_BOUNDS.top.toFloat() + 5 + taskPositioner.onDragPositioningMove( + newX, + newY + ) + + taskPositioner.onDragPositioningEnd(newX, newY) + + verify(mockShellTaskOrganizer).applyTransaction(argThat { wct -> + return@argThat wct.changes.any { (token, change) -> + token == taskBinder && + ((change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) + != 0) && change.configuration.windowConfiguration.bounds.right == + STARTING_BOUNDS.right && + change.configuration.windowConfiguration.bounds.left == + STARTING_BOUNDS.left + } + }) + } + + @Test + fun testDragResize_resize_setBoundsRunsWhenResizeBoundsValid() { + taskPositioner.onDragPositioningStart( + CTRL_TYPE_RIGHT or CTRL_TYPE_TOP, // Resize right and top + STARTING_BOUNDS.right.toFloat(), + STARTING_BOUNDS.top.toFloat() + ) + + // Shrink to height 20px and width 20px with both min height/width equal to 10px + val newX = STARTING_BOUNDS.right.toFloat() - 80 + val newY = STARTING_BOUNDS.top.toFloat() + 80 + taskPositioner.onDragPositioningMove( + newX, + newY + ) + + taskPositioner.onDragPositioningEnd(newX, newY) + + verify(mockShellTaskOrganizer).applyTransaction(argThat { wct -> + return@argThat wct.changes.any { (token, change) -> + token == taskBinder && + ((change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != 0) + } + }) + } + + @Test + fun testDragResize_resize_setBoundsDoesNotRunWithNegativeHeightAndWidth() { + taskPositioner.onDragPositioningStart( + CTRL_TYPE_RIGHT or CTRL_TYPE_TOP, // Resize right and top + STARTING_BOUNDS.right.toFloat(), + STARTING_BOUNDS.top.toFloat() + ) + + // Shrink to height 5px and width 5px with both min height/width equal to 10px + val newX = STARTING_BOUNDS.right.toFloat() - 95 + val newY = STARTING_BOUNDS.top.toFloat() + 95 + taskPositioner.onDragPositioningMove( + newX, + newY + ) + + taskPositioner.onDragPositioningEnd(newX, newY) + + verify(mockShellTaskOrganizer, never()).applyTransaction(argThat { wct -> + return@argThat wct.changes.any { (token, change) -> + token == taskBinder && + ((change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != 0) + } + }) + } + + @Test + fun testDragResize_resize_useDefaultMinWhenMinWidthInvalid() { + mockWindowDecoration.mTaskInfo.minWidth = -1 + + taskPositioner.onDragPositioningStart( + CTRL_TYPE_RIGHT or CTRL_TYPE_TOP, // Resize right and top + STARTING_BOUNDS.right.toFloat(), + STARTING_BOUNDS.top.toFloat() + ) + + // Shrink to width and height of 3px with invalid minWidth = -1 and defaultMinSize = 5px + val newX = STARTING_BOUNDS.right.toFloat() - 97 + val newY = STARTING_BOUNDS.top.toFloat() + 97 + taskPositioner.onDragPositioningMove( + newX, + newY + ) + + taskPositioner.onDragPositioningEnd(newX, newY) + + verify(mockShellTaskOrganizer, never()).applyTransaction(argThat { wct -> + return@argThat wct.changes.any { (token, change) -> + token == taskBinder && + ((change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != 0) + } + }) + } + + @Test + fun testDragResize_resize_useMinWidthWhenValid() { + taskPositioner.onDragPositioningStart( + CTRL_TYPE_RIGHT or CTRL_TYPE_TOP, // Resize right and top + STARTING_BOUNDS.right.toFloat(), + STARTING_BOUNDS.top.toFloat() + ) + + // Shrink to width and height of 7px with valid minWidth = 10px and defaultMinSize = 5px + val newX = STARTING_BOUNDS.right.toFloat() - 93 + val newY = STARTING_BOUNDS.top.toFloat() + 93 + taskPositioner.onDragPositioningMove( + newX, + newY + ) + + taskPositioner.onDragPositioningEnd(newX, newY) + + verify(mockShellTaskOrganizer, never()).applyTransaction(argThat { wct -> + return@argThat wct.changes.any { (token, change) -> + token == taskBinder && + ((change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != 0) + } + }) + } + + fun testDragResize_toDisallowedBounds_freezesAtLimit() { + taskPositioner.onDragPositioningStart( + CTRL_TYPE_RIGHT or CTRL_TYPE_BOTTOM, // Resize right-bottom corner + STARTING_BOUNDS.right.toFloat(), + STARTING_BOUNDS.bottom.toFloat() + ) + + // Resize the task by 10px to the right and bottom, a valid destination + val newBounds = Rect( + STARTING_BOUNDS.left, + STARTING_BOUNDS.top, + STARTING_BOUNDS.right + 10, + STARTING_BOUNDS.bottom + 10) + taskPositioner.onDragPositioningMove( + newBounds.right.toFloat(), + newBounds.bottom.toFloat() + ) + + // Resize the task by another 10px to the right (allowed) and to just in the disallowed + // area of the Y coordinate. + val newBounds2 = Rect( + newBounds.left, + newBounds.top, + newBounds.right + 10, + DISALLOWED_RESIZE_AREA.top + ) + taskPositioner.onDragPositioningMove( + newBounds2.right.toFloat(), + newBounds2.bottom.toFloat() + ) + + taskPositioner.onDragPositioningEnd(newBounds2.right.toFloat(), newBounds2.bottom.toFloat()) + + // The first resize falls in the allowed area, verify there's a change for it. + verify(mockShellTaskOrganizer).applyTransaction(argThat { wct -> + return@argThat wct.changes.any { (token, change) -> + token == taskBinder && change.ofBounds(newBounds) + } + }) + // The second resize falls in the disallowed area, verify there's no change for it. + verify(mockShellTaskOrganizer, never()).applyTransaction(argThat { wct -> + return@argThat wct.changes.any { (token, change) -> + token == taskBinder && change.ofBounds(newBounds2) + } + }) + // Instead, there should be a change for its allowed portion (the X movement) with the Y + // staying frozen in the last valid resize position. + verify(mockShellTaskOrganizer).applyTransaction(argThat { wct -> + return@argThat wct.changes.any { (token, change) -> + token == taskBinder && change.ofBounds( + Rect( + newBounds2.left, + newBounds2.top, + newBounds2.right, + newBounds.bottom // Stayed at the first resize destination. + ) + ) + } + }) + } + + private fun WindowContainerTransaction.Change.ofBounds(bounds: Rect): Boolean { + return ((windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != 0) && + bounds == configuration.windowConfiguration.bounds + } + + companion object { + private const val TASK_ID = 5 + private const val MIN_WIDTH = 10 + private const val MIN_HEIGHT = 10 + private const val DENSITY_DPI = 20 + private const val DEFAULT_MIN = 40 + private const val DISPLAY_ID = 1 + private const val NAVBAR_HEIGHT = 50 + private val DISPLAY_BOUNDS = Rect(0, 0, 2400, 1600) + private val STARTING_BOUNDS = Rect(0, 0, 100, 100) + private val DISALLOWED_RESIZE_AREA = Rect( + DISPLAY_BOUNDS.left, + DISPLAY_BOUNDS.bottom - NAVBAR_HEIGHT, + DISPLAY_BOUNDS.right, + DISPLAY_BOUNDS.bottom) + private val STABLE_BOUNDS = Rect( + DISPLAY_BOUNDS.left, + DISPLAY_BOUNDS.top, + DISPLAY_BOUNDS.right, + DISPLAY_BOUNDS.bottom - NAVBAR_HEIGHT + ) + } +} 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..dfa3c1010eed --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java @@ -0,0 +1,594 @@ +/* + * 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.ArgumentMatchers.anyInt; +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.content.res.Resources; +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.SurfaceControl; +import android.view.SurfaceControlViewHost; +import android.view.View; +import android.view.ViewRootImpl; +import android.view.WindowInsets; +import android.view.WindowManager.LayoutParams; +import android.window.TaskConstants; +import android.window.WindowContainerTransaction; + +import androidx.test.filters.SmallTest; +import androidx.test.platform.app.InstrumentationRegistry; + +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.tests.R; + +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 org.mockito.Mockito; + +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 Rect TASK_BOUNDS = new Rect(100, 300, 400, 400); + private static final Point TASK_POSITION_IN_PARENT = new Point(40, 60); + + 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; + private SurfaceControl.Transaction mMockSurfaceControlAddWindowT; + private WindowDecoration.RelayoutParams mRelayoutParams = new WindowDecoration.RelayoutParams(); + private int mCaptionMenuWidthId; + private int mCaptionMenuShadowRadiusId; + private int mCaptionMenuCornerRadiusId; + + @Before + public void setUp() { + mMockSurfaceControlStartT = createMockSurfaceControlTransaction(); + mMockSurfaceControlFinishT = createMockSurfaceControlTransaction(); + mMockSurfaceControlAddWindowT = createMockSurfaceControlTransaction(); + + mRelayoutParams.mLayoutResId = 0; + mRelayoutParams.mCaptionHeightId = R.dimen.test_freeform_decor_caption_height; + mCaptionMenuWidthId = R.dimen.test_freeform_decor_caption_menu_width; + mCaptionMenuShadowRadiusId = R.dimen.test_caption_menu_shadow_radius; + mCaptionMenuCornerRadiusId = R.dimen.test_caption_menu_corner_radius; + mRelayoutParams.mShadowRadiusId = R.dimen.test_window_decor_shadow_radius; + + 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; + mRelayoutParams.setOutsets( + R.dimen.test_window_decor_left_outset, + R.dimen.test_window_decor_top_outset, + R.dimen.test_window_decor_right_outset, + R.dimen.test_window_decor_bottom_outset); + + 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; + mRelayoutParams.setOutsets( + R.dimen.test_window_decor_left_outset, + R.dimen.test_window_decor_top_outset, + R.dimen.test_window_decor_right_outset, + R.dimen.test_window_decor_bottom_outset); + 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, + TaskConstants.TASK_CHILD_LAYER_TASK_BACKGROUND); + 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).addInsetsSource( + eq(taskInfo.token), + any(), + eq(0 /* index */), + eq(WindowInsets.Type.captionBar()), + eq(new Rect(100, 300, 400, 364))); + } + + 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); + } + + @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; + mRelayoutParams.setOutsets( + R.dimen.test_window_decor_left_outset, + R.dimen.test_window_decor_top_outset, + R.dimen.test_window_decor_right_outset, + R.dimen.test_window_decor_bottom_outset); + + 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()) + .removeInsetsSource(eq(taskInfo.token), any(), anyInt(), anyInt()); + + 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) + .removeInsetsSource(eq(taskInfo.token), any(), anyInt(), anyInt()); + } + + @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()); + } + + @Test + public void testAddWindow() { + 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; + taskInfo.configuration.densityDpi = DisplayMetrics.DENSITY_DEFAULT * 2; + mRelayoutParams.setOutsets( + R.dimen.test_window_decor_left_outset, + R.dimen.test_window_decor_top_outset, + R.dimen.test_window_decor_right_outset, + R.dimen.test_window_decor_bottom_outset); + final SurfaceControl taskSurface = mock(SurfaceControl.class); + final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo, taskSurface); + windowDecor.relayout(taskInfo); + + final SurfaceControl additionalWindowSurface = mock(SurfaceControl.class); + final SurfaceControl.Builder additionalWindowSurfaceBuilder = + createMockSurfaceControlBuilder(additionalWindowSurface); + mMockSurfaceControlBuilders.add(additionalWindowSurfaceBuilder); + + WindowDecoration.AdditionalWindow additionalWindow = windowDecor.addTestWindow(); + + verify(additionalWindowSurfaceBuilder).setContainerLayer(); + verify(additionalWindowSurfaceBuilder).setParent(decorContainerSurface); + verify(additionalWindowSurfaceBuilder).build(); + verify(mMockSurfaceControlAddWindowT).setPosition(additionalWindowSurface, 20, 40); + final int width = WindowDecoration.loadDimensionPixelSize( + mContext.getResources(), mCaptionMenuWidthId); + final int height = WindowDecoration.loadDimensionPixelSize( + mContext.getResources(), mRelayoutParams.mCaptionHeightId); + verify(mMockSurfaceControlAddWindowT).setWindowCrop(additionalWindowSurface, width, height); + final int shadowRadius = WindowDecoration.loadDimensionPixelSize(mContext.getResources(), + mCaptionMenuShadowRadiusId); + verify(mMockSurfaceControlAddWindowT) + .setShadowRadius(additionalWindowSurface, shadowRadius); + final int cornerRadius = WindowDecoration.loadDimensionPixelSize(mContext.getResources(), + mCaptionMenuCornerRadiusId); + verify(mMockSurfaceControlAddWindowT) + .setCornerRadius(additionalWindowSurface, cornerRadius); + verify(mMockSurfaceControlAddWindowT).show(additionalWindowSurface); + verify(mMockSurfaceControlViewHostFactory, Mockito.times(2)) + .create(any(), eq(defaultDisplay), any()); + assertThat(additionalWindow.mWindowViewHost).isNotNull(); + + additionalWindow.releaseView(); + + assertThat(additionalWindow.mWindowViewHost).isNull(); + assertThat(additionalWindow.mWindowSurface).isNull(); + } + + @Test + public void testLayoutResultCalculation_fullWidthCaption() { + 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; + taskInfo.configuration.densityDpi = DisplayMetrics.DENSITY_DEFAULT * 2; + mRelayoutParams.setOutsets( + R.dimen.test_window_decor_left_outset, + R.dimen.test_window_decor_top_outset, + R.dimen.test_window_decor_right_outset, + R.dimen.test_window_decor_bottom_outset); + final SurfaceControl taskSurface = mock(SurfaceControl.class); + final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo, taskSurface); + + windowDecor.relayout(taskInfo); + + verify(captionContainerSurfaceBuilder).setParent(decorContainerSurface); + verify(captionContainerSurfaceBuilder).setContainerLayer(); + verify(mMockSurfaceControlStartT).setPosition(captionContainerSurface, 20, 40); + // Width of the captionContainerSurface should match the width of TASK_BOUNDS + verify(mMockSurfaceControlStartT).setWindowCrop(captionContainerSurface, 300, 64); + verify(mMockSurfaceControlStartT).show(captionContainerSurface); + } + + private TestWindowDecoration createWindowDecoration( + ActivityManager.RunningTaskInfo taskInfo, SurfaceControl testSurface) { + return new TestWindowDecoration(InstrumentationRegistry.getInstrumentation().getContext(), + 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(mRelayoutParams, mMockSurfaceControlStartT, mMockSurfaceControlFinishT, + mMockWindowContainerTransaction, mMockView, mRelayoutResult); + } + + private WindowDecoration.AdditionalWindow addTestWindow() { + final Resources resources = mDecorWindowContext.getResources(); + int x = mRelayoutParams.mCaptionX; + int y = mRelayoutParams.mCaptionY; + int width = loadDimensionPixelSize(resources, mCaptionMenuWidthId); + int height = loadDimensionPixelSize(resources, mRelayoutParams.mCaptionHeightId); + int shadowRadius = loadDimensionPixelSize(resources, mCaptionMenuShadowRadiusId); + int cornerRadius = loadDimensionPixelSize(resources, mCaptionMenuCornerRadiusId); + String name = "Test Window"; + WindowDecoration.AdditionalWindow additionalWindow = + addWindow(R.layout.desktop_mode_window_decor_handle_menu_app_info_pill, name, + mMockSurfaceControlAddWindowT, + x - mRelayoutResult.mDecorContainerOffsetX, + y - mRelayoutResult.mDecorContainerOffsetY, + width, height, shadowRadius, cornerRadius); + return additionalWindow; + } + } +} diff --git a/libs/androidfw/Android.bp b/libs/androidfw/Android.bp index c80fb188e70f..28bda72bccdd 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", @@ -54,12 +55,14 @@ cc_library { host_supported: true, srcs: [ "ApkAssets.cpp", + "ApkParsing.cpp", "Asset.cpp", "AssetDir.cpp", "AssetManager.cpp", "AssetManager2.cpp", "AssetsProvider.cpp", "AttributeResolution.cpp", + "BigBuffer.cpp", "ChunkIterator.cpp", "ConfigDescription.cpp", "Idmap.cpp", @@ -69,9 +72,11 @@ cc_library { "misc.cpp", "ObbFile.cpp", "PosixUtils.cpp", + "ResourceTimer.cpp", "ResourceTypes.cpp", "ResourceUtils.cpp", "StreamingZipInflater.cpp", + "StringPool.cpp", "TypeWrappers.cpp", "Util.cpp", "ZipFileRO.cpp", @@ -156,11 +161,13 @@ cc_test { // Actual tests. "tests/ApkAssets_test.cpp", + "tests/ApkParsing_test.cpp", "tests/AppAsLib_test.cpp", "tests/Asset_test.cpp", "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", @@ -169,10 +176,12 @@ cc_test { "tests/Idmap_test.cpp", "tests/LoadedArsc_test.cpp", "tests/Locale_test.cpp", + "tests/ResourceTimer_test.cpp", "tests/ResourceUtils_test.cpp", "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", @@ -204,6 +213,8 @@ cc_test { "tests/data/**/*.apk", "tests/data/**/*.arsc", "tests/data/**/*.idmap", + ":FrameworkResourcesSparseTestApp", + ":FrameworkResourcesNotSparseTestApp", ], test_suites: ["device-tests"], } diff --git a/libs/androidfw/ApkAssets.cpp b/libs/androidfw/ApkAssets.cpp index 2beb33abe782..15aaae25f754 100755..100644 --- a/libs/androidfw/ApkAssets.cpp +++ b/libs/androidfw/ApkAssets.cpp @@ -18,6 +18,7 @@ #include "android-base/errors.h" #include "android-base/logging.h" +#include "android-base/utf8.h" namespace android { @@ -83,15 +84,16 @@ std::unique_ptr<ApkAssets> ApkAssets::LoadOverlay(const std::string& idmap_path, return {}; } + std::string overlay_path(loaded_idmap->OverlayApkPath()); + auto fd = unique_fd(base::utf8::open(overlay_path.c_str(), O_RDONLY | O_CLOEXEC)); std::unique_ptr<AssetsProvider> overlay_assets; - const std::string overlay_path(loaded_idmap->OverlayApkPath()); - if (IsFabricatedOverlay(overlay_path)) { + if (IsFabricatedOverlay(fd)) { // Fabricated overlays do not contain resource definitions. All of the overlay resource values // are defined inline in the idmap. - overlay_assets = EmptyAssetsProvider::Create(overlay_path); + overlay_assets = EmptyAssetsProvider::Create(std::move(overlay_path)); } else { // The overlay should be an APK. - overlay_assets = ZipAssetsProvider::Create(overlay_path, flags); + overlay_assets = ZipAssetsProvider::Create(std::move(overlay_path), flags, std::move(fd)); } if (overlay_assets == nullptr) { return {}; @@ -141,6 +143,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/ApkParsing.cpp b/libs/androidfw/ApkParsing.cpp new file mode 100644 index 000000000000..32d2c5b05acb --- /dev/null +++ b/libs/androidfw/ApkParsing.cpp @@ -0,0 +1,110 @@ +/* + * 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. + */ + +#include "androidfw/ApkParsing.h" +#include <algorithm> +#include <array> +#include <stdlib.h> +#include <string_view> +#include <sys/types.h> + +const std::string_view APK_LIB = "lib/"; +const size_t APK_LIB_LEN = APK_LIB.size(); + +const std::string_view LIB_PREFIX = "/lib"; +const size_t LIB_PREFIX_LEN = LIB_PREFIX.size(); + +const std::string_view LIB_SUFFIX = ".so"; +const size_t LIB_SUFFIX_LEN = LIB_SUFFIX.size(); + +static const std::array<std::string_view, 2> abis = {"arm64-v8a", "x86_64"}; + +namespace android::util { +const char* ValidLibraryPathLastSlash(const char* fileName, bool suppress64Bit, bool debuggable) { + // Make sure the filename is at least to the minimum library name size. + const size_t fileNameLen = strlen(fileName); + static const size_t minLength = APK_LIB_LEN + 2 + LIB_PREFIX_LEN + 1 + LIB_SUFFIX_LEN; + if (fileNameLen < minLength) { + return nullptr; + } + + const char* lastSlash = strrchr(fileName, '/'); + if (!lastSlash) { + return nullptr; + } + + // Skip directories. + if (*(lastSlash + 1) == 0) { + return nullptr; + } + + // Make sure the filename is safe. + if (!isFilenameSafe(lastSlash + 1)) { + return nullptr; + } + + // Make sure there aren't subdirectories by checking if the next / after lib/ is the last slash + if (memchr(fileName + APK_LIB_LEN, '/', fileNameLen - APK_LIB_LEN) != lastSlash) { + return nullptr; + } + + if (!debuggable) { + // Make sure the filename starts with lib and ends with ".so". + if (strncmp(fileName + fileNameLen - LIB_SUFFIX_LEN, LIB_SUFFIX.data(), LIB_SUFFIX_LEN) != 0 + || strncmp(lastSlash, LIB_PREFIX.data(), LIB_PREFIX_LEN) != 0) { + return nullptr; + } + } + + // Don't include 64 bit versions if they are suppressed + if (suppress64Bit && std::find(abis.begin(), abis.end(), std::string_view( + fileName + APK_LIB_LEN, lastSlash - fileName - APK_LIB_LEN)) != abis.end()) { + return nullptr; + } + + return lastSlash; +} + +bool isFilenameSafe(const char* filename) { + off_t offset = 0; + for (;;) { + switch (*(filename + offset)) { + case 0: + // Null. + // If we've reached the end, all the other characters are good. + return true; + + case 'A' ... 'Z': + case 'a' ... 'z': + case '0' ... '9': + case '+': + case ',': + case '-': + case '.': + case '/': + case '=': + case '_': + offset++; + break; + + default: + // We found something that is not good. + return false; + } + } + // Should not reach here. +} +}
\ No newline at end of file diff --git a/libs/androidfw/AssetManager2.cpp b/libs/androidfw/AssetManager2.cpp index 136fc6ca4e2a..68f5e4a88c7e 100644 --- a/libs/androidfw/AssetManager2.cpp +++ b/libs/androidfw/AssetManager2.cpp @@ -22,6 +22,7 @@ #include <iterator> #include <map> #include <set> +#include <span> #include "android-base/logging.h" #include "android-base/stringprintf.h" @@ -43,28 +44,19 @@ namespace { using EntryValue = std::variant<Res_value, incfs::verified_map_ptr<ResTable_map_entry>>; +/* NOTE: table_entry has been verified in LoadedPackage::GetEntryFromOffset(), + * and so access to ->value() and ->map_entry() are safe here + */ base::expected<EntryValue, IOError> GetEntryValue( incfs::verified_map_ptr<ResTable_entry> table_entry) { - const uint16_t entry_size = dtohs(table_entry->size); + const uint16_t entry_size = table_entry->size(); // Check if the entry represents a bag value. - if (entry_size >= sizeof(ResTable_map_entry) && - (dtohs(table_entry->flags) & ResTable_entry::FLAG_COMPLEX)) { - const auto map_entry = table_entry.convert<ResTable_map_entry>(); - if (!map_entry) { - return base::unexpected(IOError::PAGES_MISSING); - } - return map_entry.verified(); + if (entry_size >= sizeof(ResTable_map_entry) && table_entry->is_complex()) { + return table_entry.convert<ResTable_map_entry>().verified(); } - // The entry represents a non-bag value. - const auto entry_value = table_entry.offset(entry_size).convert<Res_value>(); - if (!entry_value) { - return base::unexpected(IOError::PAGES_MISSING); - } - Res_value value; - value.copyFrom_dtoh(entry_value.value()); - return value; + return table_entry->value(); } } // namespace @@ -120,7 +112,7 @@ void AssetManager2::BuildDynamicRefTable() { // A mapping from path of apk assets that could be target packages of overlays to the runtime // package id of its first loaded package. Overlays currently can only override resources in the // first package in the target resource table. - std::unordered_map<std::string, uint8_t> target_assets_package_ids; + std::unordered_map<std::string_view, uint8_t> target_assets_package_ids; // Overlay resources are not directly referenced by an application so their resource ids // can change throughout the application's lifetime. Assign overlay package ids last. @@ -143,7 +135,7 @@ void AssetManager2::BuildDynamicRefTable() { if (auto loaded_idmap = apk_assets->GetLoadedIdmap(); loaded_idmap != nullptr) { // The target package must precede the overlay package in the apk assets paths in order // to take effect. - auto iter = target_assets_package_ids.find(std::string(loaded_idmap->TargetApkPath())); + auto iter = target_assets_package_ids.find(loaded_idmap->TargetApkPath()); if (iter == target_assets_package_ids.end()) { LOG(INFO) << "failed to find target package for overlay " << loaded_idmap->OverlayApkPath(); @@ -188,7 +180,7 @@ void AssetManager2::BuildDynamicRefTable() { if (overlay_ref_table != nullptr) { // If this package is from an overlay, use a dynamic reference table that can rewrite // overlay resource ids to their corresponding target resource ids. - new_group.dynamic_ref_table = overlay_ref_table; + new_group.dynamic_ref_table = std::move(overlay_ref_table); } DynamicRefTable* ref_table = new_group.dynamic_ref_table.get(); @@ -196,9 +188,9 @@ void AssetManager2::BuildDynamicRefTable() { ref_table->mAppAsLib = package->IsDynamic() && package->GetPackageId() == 0x7f; } - // Add the package and to the set of packages with the same ID. + // Add the package to the set of packages with the same ID. PackageGroup* package_group = &package_groups_[idx]; - package_group->packages_.push_back(ConfiguredPackage{package.get(), {}}); + package_group->packages_.emplace_back().loaded_package_ = package.get(); package_group->cookies_.push_back(apk_assets_cookies[apk_assets]); // Add the package name -> build time ID mappings. @@ -210,30 +202,39 @@ void AssetManager2::BuildDynamicRefTable() { if (auto apk_assets_path = apk_assets->GetPath()) { // Overlay target ApkAssets must have been created using path based load apis. - target_assets_package_ids.insert(std::make_pair(std::string(*apk_assets_path), package_id)); + target_assets_package_ids.emplace(*apk_assets_path, package_id); } } } // Now assign the runtime IDs so that we have a build-time to runtime ID map. - const auto package_groups_end = package_groups_.end(); - for (auto iter = package_groups_.begin(); iter != package_groups_end; ++iter) { - const std::string& package_name = iter->packages_[0].loaded_package_->GetPackageName(); - for (auto iter2 = package_groups_.begin(); iter2 != package_groups_end; ++iter2) { - iter2->dynamic_ref_table->addMapping(String16(package_name.c_str(), package_name.size()), - iter->dynamic_ref_table->mAssignedPackageId); - - // Add the alias resources to the dynamic reference table of every package group. Since - // staging aliases can only be defined by the framework package (which is not a shared - // library), the compile-time package id of the framework is the same across all packages - // that compile against the framework. - for (const auto& package : iter->packages_) { - for (const auto& entry : package.loaded_package_->GetAliasResourceIdMap()) { - iter2->dynamic_ref_table->addAlias(entry.first, entry.second); - } - } + DynamicRefTable::AliasMap aliases; + for (const auto& group : package_groups_) { + const std::string& package_name = group.packages_[0].loaded_package_->GetPackageName(); + const auto name_16 = String16(package_name.c_str(), package_name.size()); + for (auto&& inner_group : package_groups_) { + inner_group.dynamic_ref_table->addMapping(name_16, + group.dynamic_ref_table->mAssignedPackageId); + } + + for (const auto& package : group.packages_) { + const auto& package_aliases = package.loaded_package_->GetAliasResourceIdMap(); + aliases.insert(aliases.end(), package_aliases.begin(), package_aliases.end()); } } + + if (!aliases.empty()) { + std::sort(aliases.begin(), aliases.end(), [](auto&& l, auto&& r) { return l.first < r.first; }); + + // Add the alias resources to the dynamic reference table of every package group. Since + // staging aliases can only be defined by the framework package (which is not a shared + // library), the compile-time package id of the framework is the same across all packages + // that compile against the framework. + for (auto& group : std::span(package_groups_.data(), package_groups_.size() - 1)) { + group.dynamic_ref_table->setAliases(aliases); + } + package_groups_.back().dynamic_ref_table->setAliases(std::move(aliases)); + } } void AssetManager2::DumpToLog() const { @@ -326,7 +327,7 @@ const std::unordered_map<std::string, std::string>* return &loaded_package->GetOverlayableMap(); } -bool AssetManager2::GetOverlayablesToString(const android::StringPiece& package_name, +bool AssetManager2::GetOverlayablesToString(android::StringPiece package_name, std::string* out) const { uint8_t package_id = 0U; for (const auto& apk_assets : apk_assets_) { @@ -373,7 +374,7 @@ bool AssetManager2::GetOverlayablesToString(const android::StringPiece& package_ const std::string name = ToFormattedResourceString(*res_name); output.append(base::StringPrintf( "resource='%s' overlayable='%s' actor='%s' policy='0x%08x'\n", - name.c_str(), info->name.c_str(), info->actor.c_str(), info->policy_flags)); + name.c_str(), info->name.data(), info->actor.data(), info->policy_flags)); } } } @@ -501,7 +502,7 @@ std::unique_ptr<AssetDir> AssetManager2::OpenDir(const std::string& dirname) con continue; } - auto func = [&](const StringPiece& name, FileType type) { + auto func = [&](StringPiece name, FileType type) { AssetDir::FileInfo info; info.setFileName(String8(name.data(), name.size())); info.setFileType(type); @@ -580,7 +581,7 @@ base::expected<FindEntryResult, NullOrIOError> AssetManager2::FindEntry( // Retrieve the package group from the package id of the resource id. if (UNLIKELY(!is_valid_resid(resid))) { - LOG(ERROR) << base::StringPrintf("Invalid ID 0x%08x.", resid); + LOG(ERROR) << base::StringPrintf("Invalid resource ID 0x%08x.", resid); return base::unexpected(std::nullopt); } @@ -589,7 +590,7 @@ base::expected<FindEntryResult, NullOrIOError> AssetManager2::FindEntry( const uint16_t entry_idx = get_entry_id(resid); uint8_t package_idx = package_ids_[package_id]; if (UNLIKELY(package_idx == 0xff)) { - ANDROID_LOG(ERROR) << base::StringPrintf("No package ID %02x found for ID 0x%08x.", + ANDROID_LOG(ERROR) << base::StringPrintf("No package ID %02x found for resource ID 0x%08x.", package_id, resid); return base::unexpected(std::nullopt); } @@ -611,7 +612,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; @@ -800,17 +815,12 @@ base::expected<FindEntryResult, NullOrIOError> AssetManager2::FindEntryInternal( return base::unexpected(std::nullopt); } - auto best_entry_result = LoadedPackage::GetEntryFromOffset(best_type, best_offset); - if (!best_entry_result.has_value()) { - return base::unexpected(best_entry_result.error()); - } - - const incfs::map_ptr<ResTable_entry> best_entry = *best_entry_result; - if (!best_entry) { - return base::unexpected(IOError::PAGES_MISSING); + auto best_entry_verified = LoadedPackage::GetEntryFromOffset(best_type, best_offset); + if (!best_entry_verified.has_value()) { + return base::unexpected(best_entry_verified.error()); } - const auto entry = GetEntryValue(best_entry.verified()); + const auto entry = GetEntryValue(*best_entry_verified); if (!entry.has_value()) { return base::unexpected(entry.error()); } @@ -823,7 +833,7 @@ base::expected<FindEntryResult, NullOrIOError> AssetManager2::FindEntryInternal( .package_name = &best_package->GetPackageName(), .type_string_ref = StringPoolRef(best_package->GetTypeStringPool(), best_type->id - 1), .entry_string_ref = StringPoolRef(best_package->GetKeyStringPool(), - best_entry->key.index), + (*best_entry_verified)->key()), .dynamic_ref_table = package_group.dynamic_ref_table.get(), }; } @@ -1054,7 +1064,7 @@ base::expected<const ResolvedBag*, NullOrIOError> AssetManager2::ResolveBag( base::expected<const ResolvedBag*, NullOrIOError> AssetManager2::GetBag(uint32_t resid) const { std::vector<uint32_t> found_resids; const auto bag = GetBag(resid, found_resids); - cached_bag_resid_stacks_.emplace(resid, found_resids); + cached_bag_resid_stacks_.emplace(resid, std::move(found_resids)); return bag; } @@ -1271,7 +1281,7 @@ base::expected<const ResolvedBag*, NullOrIOError> AssetManager2::GetBag( return result; } -static bool Utf8ToUtf16(const StringPiece& str, std::u16string* out) { +static bool Utf8ToUtf16(StringPiece str, std::u16string* out) { ssize_t len = utf8_to_utf16_length(reinterpret_cast<const uint8_t*>(str.data()), str.size(), false); if (len < 0) { @@ -1346,22 +1356,22 @@ base::expected<uint32_t, NullOrIOError> AssetManager2::GetResourceId( void AssetManager2::RebuildFilterList() { for (PackageGroup& group : package_groups_) { - for (ConfiguredPackage& impl : group.packages_) { - // Destroy it. - impl.filtered_configs_.~ByteBucketArray(); - - // Re-create it. - new (&impl.filtered_configs_) ByteBucketArray<FilteredConfigGroup>(); - + for (ConfiguredPackage& package : group.packages_) { + package.filtered_configs_.forEachItem([](auto, auto& fcg) { fcg.type_entries.clear(); }); // Create the filters here. - impl.loaded_package_->ForEachTypeSpec([&](const TypeSpec& type_spec, uint8_t type_id) { - FilteredConfigGroup& group = impl.filtered_configs_.editItemAt(type_id - 1); + package.loaded_package_->ForEachTypeSpec([&](const TypeSpec& type_spec, uint8_t type_id) { + FilteredConfigGroup* group = nullptr; for (const auto& type_entry : type_spec.type_entries) { if (type_entry.config.match(configuration_)) { - group.type_entries.push_back(&type_entry); + if (!group) { + group = &package.filtered_configs_.editItemAt(type_id - 1); + } + group->type_entries.push_back(&type_entry); } } }); + package.filtered_configs_.trimBuckets( + [](const auto& fcg) { return fcg.type_entries.empty(); }); } } } @@ -1402,30 +1412,34 @@ uint8_t AssetManager2::GetAssignedPackageId(const LoadedPackage* package) const std::unique_ptr<Theme> AssetManager2::NewTheme() { constexpr size_t kInitialReserveSize = 32; auto theme = std::unique_ptr<Theme>(new Theme(this)); + theme->keys_.reserve(kInitialReserveSize); theme->entries_.reserve(kInitialReserveSize); return theme; } +void AssetManager2::ForEachPackage(base::function_ref<bool(const std::string&, uint8_t)> func, + package_property_t excluded_property_flags) const { + for (const PackageGroup& package_group : package_groups_) { + const auto loaded_package = package_group.packages_.front().loaded_package_; + if ((loaded_package->GetPropertyFlags() & excluded_property_flags) == 0U + && !func(loaded_package->GetPackageName(), + package_group.dynamic_ref_table->mAssignedPackageId)) { + return; + } + } +} + Theme::Theme(AssetManager2* asset_manager) : asset_manager_(asset_manager) { } Theme::~Theme() = default; struct Theme::Entry { - uint32_t attr_res_id; ApkAssetsCookie cookie; uint32_t type_spec_flags; Res_value value; }; -namespace { -struct ThemeEntryKeyComparer { - bool operator() (const Theme::Entry& entry, uint32_t attr_res_id) const noexcept { - return entry.attr_res_id < attr_res_id; - } -}; -} // namespace - base::expected<std::monostate, NullOrIOError> Theme::ApplyStyle(uint32_t resid, bool force) { ATRACE_NAME("Theme::ApplyStyle"); @@ -1454,19 +1468,20 @@ base::expected<std::monostate, NullOrIOError> Theme::ApplyStyle(uint32_t resid, continue; } - Theme::Entry new_entry{attr_res_id, it->cookie, (*bag)->type_spec_flags, it->value}; - auto entry_it = std::lower_bound(entries_.begin(), entries_.end(), attr_res_id, - ThemeEntryKeyComparer{}); - if (entry_it != entries_.end() && entry_it->attr_res_id == attr_res_id) { + const auto key_it = std::lower_bound(keys_.begin(), keys_.end(), attr_res_id); + const auto entry_it = entries_.begin() + (key_it - keys_.begin()); + if (key_it != keys_.end() && *key_it == attr_res_id) { if (is_undefined) { // DATA_NULL_UNDEFINED clears the value of the attribute in the theme only when `force` is - /// true. + // true. + keys_.erase(key_it); entries_.erase(entry_it); } else if (force) { - *entry_it = new_entry; + *entry_it = Entry{it->cookie, (*bag)->type_spec_flags, it->value}; } } else { - entries_.insert(entry_it, new_entry); + keys_.insert(key_it, attr_res_id); + entries_.insert(entry_it, Entry{it->cookie, (*bag)->type_spec_flags, it->value}); } } return {}; @@ -1477,6 +1492,7 @@ void Theme::Rebase(AssetManager2* am, const uint32_t* style_ids, const uint8_t* ATRACE_NAME("Theme::Rebase"); // Reset the entries without changing the vector capacity to prevent reallocations during // ApplyStyle. + keys_.clear(); entries_.clear(); asset_manager_ = am; for (size_t i = 0; i < style_count; i++) { @@ -1485,16 +1501,14 @@ void Theme::Rebase(AssetManager2* am, const uint32_t* style_ids, const uint8_t* } std::optional<AssetManager2::SelectedValue> Theme::GetAttribute(uint32_t resid) const { - constexpr const uint32_t kMaxIterations = 20; uint32_t type_spec_flags = 0u; for (uint32_t i = 0; i <= kMaxIterations; i++) { - auto entry_it = std::lower_bound(entries_.begin(), entries_.end(), resid, - ThemeEntryKeyComparer{}); - if (entry_it == entries_.end() || entry_it->attr_res_id != resid) { + const auto key_it = std::lower_bound(keys_.begin(), keys_.end(), resid); + if (key_it == keys_.end() || *key_it != resid) { return std::nullopt; } - + const auto entry_it = entries_.begin() + (key_it - keys_.begin()); type_spec_flags |= entry_it->type_spec_flags; if (entry_it->value.dataType == Res_value::TYPE_ATTRIBUTE) { resid = entry_it->value.data; @@ -1528,6 +1542,7 @@ base::expected<std::monostate, NullOrIOError> Theme::ResolveAttributeReference( } void Theme::Clear() { + keys_.clear(); entries_.clear(); } @@ -1539,18 +1554,19 @@ base::expected<std::monostate, IOError> Theme::SetTo(const Theme& source) { type_spec_flags_ = source.type_spec_flags_; if (asset_manager_ == source.asset_manager_) { + keys_ = source.keys_; entries_ = source.entries_; } else { - std::map<ApkAssetsCookie, ApkAssetsCookie> src_to_dest_asset_cookies; - typedef std::map<int, int> SourceToDestinationRuntimePackageMap; - std::map<ApkAssetsCookie, SourceToDestinationRuntimePackageMap> src_asset_cookie_id_map; + std::unordered_map<ApkAssetsCookie, ApkAssetsCookie> src_to_dest_asset_cookies; + using SourceToDestinationRuntimePackageMap = std::unordered_map<int, int>; + std::unordered_map<ApkAssetsCookie, SourceToDestinationRuntimePackageMap> src_asset_cookie_id_map; // Determine which ApkAssets are loaded in both theme AssetManagers. - const auto src_assets = source.asset_manager_->GetApkAssets(); + const auto& src_assets = source.asset_manager_->GetApkAssets(); for (size_t i = 0; i < src_assets.size(); i++) { const ApkAssets* src_asset = src_assets[i]; - const auto dest_assets = asset_manager_->GetApkAssets(); + const auto& dest_assets = asset_manager_->GetApkAssets(); for (size_t j = 0; j < dest_assets.size(); j++) { const ApkAssets* dest_asset = dest_assets[j]; if (src_asset != dest_asset) { @@ -1571,15 +1587,17 @@ base::expected<std::monostate, IOError> Theme::SetTo(const Theme& source) { } src_to_dest_asset_cookies.insert(std::make_pair(i, j)); - src_asset_cookie_id_map.insert(std::make_pair(i, package_map)); + src_asset_cookie_id_map.insert(std::make_pair(i, std::move(package_map))); break; } } // Reset the data in the destination theme. + keys_.clear(); entries_.clear(); - for (const auto& entry : source.entries_) { + for (size_t i = 0, size = source.entries_.size(); i != size; ++i) { + const auto& entry = source.entries_[i]; bool is_reference = (entry.value.dataType == Res_value::TYPE_ATTRIBUTE || entry.value.dataType == Res_value::TYPE_REFERENCE || entry.value.dataType == Res_value::TYPE_DYNAMIC_ATTRIBUTE @@ -1619,13 +1637,15 @@ base::expected<std::monostate, IOError> Theme::SetTo(const Theme& source) { } } + const auto source_res_id = source.keys_[i]; + // The package id of the attribute needs to be rewritten to the package id of the // attribute in the destination. - int attribute_dest_package_id = get_package_id(entry.attr_res_id); + int attribute_dest_package_id = get_package_id(source_res_id); if (attribute_dest_package_id != 0x01) { // Find the cookie of the attribute resource id in the source AssetManager base::expected<FindEntryResult, NullOrIOError> attribute_entry_result = - source.asset_manager_->FindEntry(entry.attr_res_id, 0 /* density_override */ , + source.asset_manager_->FindEntry(source_res_id, 0 /* density_override */ , true /* stop_at_first_match */, true /* ignore_configuration */); if (UNLIKELY(IsIOError(attribute_entry_result))) { @@ -1649,16 +1669,15 @@ base::expected<std::monostate, IOError> Theme::SetTo(const Theme& source) { attribute_dest_package_id = attribute_dest_package->second; } - auto dest_attr_id = make_resid(attribute_dest_package_id, get_type_id(entry.attr_res_id), - get_entry_id(entry.attr_res_id)); - Theme::Entry new_entry{dest_attr_id, data_dest_cookie, entry.type_spec_flags, - Res_value{.dataType = entry.value.dataType, - .data = attribute_data}}; - + auto dest_attr_id = make_resid(attribute_dest_package_id, get_type_id(source_res_id), + get_entry_id(source_res_id)); + const auto key_it = std::lower_bound(keys_.begin(), keys_.end(), dest_attr_id); + const auto entry_it = entries_.begin() + (key_it - keys_.begin()); // Since the entries were cleared, the attribute resource id has yet been mapped to any value. - auto entry_it = std::lower_bound(entries_.begin(), entries_.end(), dest_attr_id, - ThemeEntryKeyComparer{}); - entries_.insert(entry_it, new_entry); + keys_.insert(key_it, dest_attr_id); + entries_.insert(entry_it, Entry{data_dest_cookie, entry.type_spec_flags, + Res_value{.dataType = entry.value.dataType, + .data = attribute_data}}); } } return {}; @@ -1666,9 +1685,11 @@ base::expected<std::monostate, IOError> Theme::SetTo(const Theme& source) { void Theme::Dump() const { LOG(INFO) << base::StringPrintf("Theme(this=%p, AssetManager2=%p)", this, asset_manager_); - for (auto& entry : entries_) { + for (size_t i = 0, size = keys_.size(); i != size; ++i) { + auto res_id = keys_[i]; + const auto& entry = entries_[i]; LOG(INFO) << base::StringPrintf(" entry(0x%08x)=(0x%08x) type=(0x%02x), cookie(%d)", - entry.attr_res_id, entry.value.data, entry.value.dataType, + res_id, entry.value.data, entry.value.dataType, entry.cookie); } } diff --git a/libs/androidfw/AssetsProvider.cpp b/libs/androidfw/AssetsProvider.cpp index bce34d37c90b..2d3c06506a1f 100644 --- a/libs/androidfw/AssetsProvider.cpp +++ b/libs/androidfw/AssetsProvider.cpp @@ -73,9 +73,6 @@ std::unique_ptr<Asset> AssetsProvider::CreateAssetFromFd(base::unique_fd fd, (path != nullptr) ? base::unique_fd(-1) : std::move(fd)); } -ZipAssetsProvider::PathOrDebugName::PathOrDebugName(std::string&& value, bool is_path) - : value_(std::forward<std::string>(value)), is_path_(is_path) {} - const std::string* ZipAssetsProvider::PathOrDebugName::GetPath() const { return is_path_ ? &value_ : nullptr; } @@ -84,34 +81,42 @@ const std::string& ZipAssetsProvider::PathOrDebugName::GetDebugName() const { return value_; } +void ZipAssetsProvider::ZipCloser::operator()(ZipArchive* a) const { + ::CloseArchive(a); +} + ZipAssetsProvider::ZipAssetsProvider(ZipArchiveHandle handle, PathOrDebugName&& path, package_property_t flags, time_t last_mod_time) - : zip_handle_(handle, ::CloseArchive), - name_(std::forward<PathOrDebugName>(path)), + : zip_handle_(handle), + name_(std::move(path)), flags_(flags), last_mod_time_(last_mod_time) {} std::unique_ptr<ZipAssetsProvider> ZipAssetsProvider::Create(std::string path, - package_property_t flags) { + package_property_t flags, + base::unique_fd fd) { + const auto released_fd = fd.ok() ? fd.release() : -1; ZipArchiveHandle handle; - if (int32_t result = OpenArchive(path.c_str(), &handle); result != 0) { + if (int32_t result = released_fd < 0 ? OpenArchive(path.c_str(), &handle) + : OpenArchiveFd(released_fd, path.c_str(), &handle)) { LOG(ERROR) << "Failed to open APK '" << path << "': " << ::ErrorCodeString(result); CloseArchive(handle); return {}; } struct stat sb{.st_mtime = -1}; - if (stat(path.c_str(), &sb) < 0) { - // Stat requires execute permissions on all directories path to the file. If the process does - // not have execute permissions on this file, allow the zip to be opened but IsUpToDate() will - // always have to return true. - LOG(WARNING) << "Failed to stat file '" << path << "': " - << base::SystemErrorCodeToString(errno); + // Skip all up-to-date checks if the file won't ever change. + if (!isReadonlyFilesystem(path.c_str())) { + if ((released_fd < 0 ? stat(path.c_str(), &sb) : fstat(released_fd, &sb)) < 0) { + // Stat requires execute permissions on all directories path to the file. If the process does + // not have execute permissions on this file, allow the zip to be opened but IsUpToDate() will + // always have to return true. + PLOG(WARNING) << "Failed to stat file '" << path << "'"; + } } return std::unique_ptr<ZipAssetsProvider>( - new ZipAssetsProvider(handle, PathOrDebugName{std::move(path), - true /* is_path */}, flags, sb.st_mtime)); + new ZipAssetsProvider(handle, PathOrDebugName::Path(std::move(path)), flags, sb.st_mtime)); } std::unique_ptr<ZipAssetsProvider> ZipAssetsProvider::Create(base::unique_fd fd, @@ -133,17 +138,19 @@ std::unique_ptr<ZipAssetsProvider> ZipAssetsProvider::Create(base::unique_fd fd, } struct stat sb{.st_mtime = -1}; - if (fstat(released_fd, &sb) < 0) { - // Stat requires execute permissions on all directories path to the file. If the process does - // not have execute permissions on this file, allow the zip to be opened but IsUpToDate() will - // always have to return true. - LOG(WARNING) << "Failed to fstat file '" << friendly_name << "': " - << base::SystemErrorCodeToString(errno); + // Skip all up-to-date checks if the file won't ever change. + if (!isReadonlyFilesystem(released_fd)) { + if (fstat(released_fd, &sb) < 0) { + // Stat requires execute permissions on all directories path to the file. If the process does + // not have execute permissions on this file, allow the zip to be opened but IsUpToDate() will + // always have to return true. + LOG(WARNING) << "Failed to fstat file '" << friendly_name + << "': " << base::SystemErrorCodeToString(errno); + } } - return std::unique_ptr<ZipAssetsProvider>( - new ZipAssetsProvider(handle, PathOrDebugName{std::move(friendly_name), - false /* is_path */}, flags, sb.st_mtime)); + return std::unique_ptr<ZipAssetsProvider>(new ZipAssetsProvider( + handle, PathOrDebugName::DebugName(std::move(friendly_name)), flags, sb.st_mtime)); } std::unique_ptr<Asset> ZipAssetsProvider::OpenInternal(const std::string& path, @@ -210,9 +217,9 @@ std::unique_ptr<Asset> ZipAssetsProvider::OpenInternal(const std::string& path, return asset; } -bool ZipAssetsProvider::ForEachFile(const std::string& root_path, - const std::function<void(const StringPiece&, FileType)>& f) - const { +bool ZipAssetsProvider::ForEachFile( + const std::string& root_path, + base::function_ref<void(StringPiece, FileType)> f) const { std::string root_path_full = root_path; if (root_path_full.back() != '/') { root_path_full += '/'; @@ -238,8 +245,7 @@ bool ZipAssetsProvider::ForEachFile(const std::string& root_path, if (!leaf_file_path.empty()) { auto iter = std::find(leaf_file_path.begin(), leaf_file_path.end(), '/'); if (iter != leaf_file_path.end()) { - std::string dir = - leaf_file_path.substr(0, std::distance(leaf_file_path.begin(), iter)).to_string(); + std::string dir(leaf_file_path.substr(0, std::distance(leaf_file_path.begin(), iter))); dirs.insert(std::move(dir)); } else { f(leaf_file_path, kFileTypeRegular); @@ -277,6 +283,9 @@ const std::string& ZipAssetsProvider::GetDebugName() const { } bool ZipAssetsProvider::IsUpToDate() const { + if (last_mod_time_ == -1) { + return true; + } struct stat sb{}; if (fstat(GetFileDescriptor(zip_handle_.get()), &sb) < 0) { // If fstat fails on the zip archive, return true so the zip archive the resource system does @@ -287,10 +296,10 @@ bool ZipAssetsProvider::IsUpToDate() const { } DirectoryAssetsProvider::DirectoryAssetsProvider(std::string&& path, time_t last_mod_time) - : dir_(std::forward<std::string>(path)), last_mod_time_(last_mod_time) {} + : dir_(std::move(path)), last_mod_time_(last_mod_time) {} std::unique_ptr<DirectoryAssetsProvider> DirectoryAssetsProvider::Create(std::string path) { - struct stat sb{}; + struct stat sb; const int result = stat(path.c_str(), &sb); if (result == -1) { LOG(ERROR) << "Failed to find directory '" << path << "'."; @@ -302,12 +311,13 @@ std::unique_ptr<DirectoryAssetsProvider> DirectoryAssetsProvider::Create(std::st return nullptr; } - if (path[path.size() - 1] != OS_PATH_SEPARATOR) { + if (path.back() != OS_PATH_SEPARATOR) { path += OS_PATH_SEPARATOR; } - return std::unique_ptr<DirectoryAssetsProvider>(new DirectoryAssetsProvider(std::move(path), - sb.st_mtime)); + const bool isReadonly = isReadonlyFilesystem(path.c_str()); + return std::unique_ptr<DirectoryAssetsProvider>( + new DirectoryAssetsProvider(std::move(path), isReadonly ? -1 : sb.st_mtime)); } std::unique_ptr<Asset> DirectoryAssetsProvider::OpenInternal(const std::string& path, @@ -324,8 +334,7 @@ std::unique_ptr<Asset> DirectoryAssetsProvider::OpenInternal(const std::string& bool DirectoryAssetsProvider::ForEachFile( const std::string& /* root_path */, - const std::function<void(const StringPiece&, FileType)>& /* f */) - const { + base::function_ref<void(StringPiece, FileType)> /* f */) const { return true; } @@ -338,7 +347,10 @@ const std::string& DirectoryAssetsProvider::GetDebugName() const { } bool DirectoryAssetsProvider::IsUpToDate() const { - struct stat sb{}; + if (last_mod_time_ == -1) { + return true; + } + struct stat sb; if (stat(dir_.c_str(), &sb) < 0) { // If stat fails on the zip archive, return true so the zip archive the resource system does // attempt to refresh the ApkAsset. @@ -349,8 +361,7 @@ bool DirectoryAssetsProvider::IsUpToDate() const { MultiAssetsProvider::MultiAssetsProvider(std::unique_ptr<AssetsProvider>&& primary, std::unique_ptr<AssetsProvider>&& secondary) - : primary_(std::forward<std::unique_ptr<AssetsProvider>>(primary)), - secondary_(std::forward<std::unique_ptr<AssetsProvider>>(secondary)) { + : primary_(std::move(primary)), secondary_(std::move(secondary)) { debug_name_ = primary_->GetDebugName() + " and " + secondary_->GetDebugName(); path_ = (primary_->GetDebugName() != kEmptyDebugString) ? primary_->GetPath() : secondary_->GetPath(); @@ -372,9 +383,9 @@ std::unique_ptr<Asset> MultiAssetsProvider::OpenInternal(const std::string& path return (asset) ? std::move(asset) : secondary_->Open(path, mode, file_exists); } -bool MultiAssetsProvider::ForEachFile(const std::string& root_path, - const std::function<void(const StringPiece&, FileType)>& f) - const { +bool MultiAssetsProvider::ForEachFile( + const std::string& root_path, + base::function_ref<void(StringPiece, FileType)> f) const { return primary_->ForEachFile(root_path, f) && secondary_->ForEachFile(root_path, f); } @@ -397,8 +408,8 @@ std::unique_ptr<AssetsProvider> EmptyAssetsProvider::Create() { return std::unique_ptr<EmptyAssetsProvider>(new EmptyAssetsProvider({})); } -std::unique_ptr<AssetsProvider> EmptyAssetsProvider::Create(const std::string& path) { - return std::unique_ptr<EmptyAssetsProvider>(new EmptyAssetsProvider(path)); +std::unique_ptr<AssetsProvider> EmptyAssetsProvider::Create(std::string path) { + return std::unique_ptr<EmptyAssetsProvider>(new EmptyAssetsProvider(std::move(path))); } std::unique_ptr<Asset> EmptyAssetsProvider::OpenInternal(const std::string& /* path */, @@ -412,7 +423,7 @@ std::unique_ptr<Asset> EmptyAssetsProvider::OpenInternal(const std::string& /* p bool EmptyAssetsProvider::ForEachFile( const std::string& /* root_path */, - const std::function<void(const StringPiece&, FileType)>& /* f */) const { + base::function_ref<void(StringPiece, FileType)> /* f */) const { return true; } @@ -435,4 +446,4 @@ bool EmptyAssetsProvider::IsUpToDate() const { return true; } -} // namespace android
\ No newline at end of file +} // namespace android 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/ConfigDescription.cpp b/libs/androidfw/ConfigDescription.cpp index 19ead9583eb2..cf2fd6f59b87 100644 --- a/libs/androidfw/ConfigDescription.cpp +++ b/libs/androidfw/ConfigDescription.cpp @@ -21,6 +21,7 @@ #include "androidfw/Util.h" #include <string> +#include <string_view> #include <vector> namespace android { @@ -38,11 +39,11 @@ static bool parseMcc(const char* name, ResTable_config* out) { return true; } const char* c = name; - if (tolower(*c) != 'm') return false; + if (*c != 'm') return false; c++; - if (tolower(*c) != 'c') return false; + if (*c != 'c') return false; c++; - if (tolower(*c) != 'c') return false; + if (*c != 'c') return false; c++; const char* val = c; @@ -68,11 +69,11 @@ static bool parseMnc(const char* name, ResTable_config* out) { return true; } const char* c = name; - if (tolower(*c) != 'm') return false; + if (*c != 'm') return false; c++; - if (tolower(*c) != 'n') return false; + if (*c != 'n') return false; c++; - if (tolower(*c) != 'c') return false; + if (*c != 'c') return false; c++; const char* val = c; @@ -93,6 +94,23 @@ static bool parseMnc(const char* name, ResTable_config* out) { return true; } +static bool parseGrammaticalInflection(const std::string& name, ResTable_config* out) { + using namespace std::literals; + if (name == "feminine"sv) { + if (out) out->grammaticalInflection = ResTable_config::GRAMMATICAL_GENDER_FEMININE; + return true; + } + if (name == "masculine"sv) { + if (out) out->grammaticalInflection = ResTable_config::GRAMMATICAL_GENDER_MASCULINE; + return true; + } + if (name == "neuter"sv) { + if (out) out->grammaticalInflection = ResTable_config::GRAMMATICAL_GENDER_NEUTER; + return true; + } + return false; +} + static bool parseLayoutDirection(const char* name, ResTable_config* out) { if (strcmp(name, kWildcardName) == 0) { if (out) @@ -637,7 +655,7 @@ static bool parseVersion(const char* name, ResTable_config* out) { return true; } -bool ConfigDescription::Parse(const StringPiece& str, ConfigDescription* out) { +bool ConfigDescription::Parse(StringPiece str, ConfigDescription* out) { std::vector<std::string> parts = util::SplitAndLowercase(str, '-'); ConfigDescription config; @@ -678,6 +696,13 @@ bool ConfigDescription::Parse(const StringPiece& str, ConfigDescription* out) { } } + if (parseGrammaticalInflection(*part_iter, &config)) { + ++part_iter; + if (part_iter == parts_end) { + goto success; + } + } + if (parseLayoutDirection(part_iter->c_str(), &config)) { ++part_iter; if (part_iter == parts_end) { @@ -832,11 +857,13 @@ success: void ConfigDescription::ApplyVersionForCompatibility( ConfigDescription* config) { uint16_t min_sdk = 0; - if ((config->uiMode & ResTable_config::MASK_UI_MODE_TYPE) + if (config->grammaticalInflection != 0) { + min_sdk = SDK_U; + } else if ((config->uiMode & ResTable_config::MASK_UI_MODE_TYPE) == ResTable_config::UI_MODE_TYPE_VR_HEADSET || config->colorMode & ResTable_config::MASK_WIDE_COLOR_GAMUT || config->colorMode & ResTable_config::MASK_HDR) { - min_sdk = SDK_O; + min_sdk = SDK_O; } else if (config->screenLayout2 & ResTable_config::MASK_SCREENROUND) { min_sdk = SDK_MARSHMALLOW; } else if (config->density == ResTable_config::DENSITY_ANY) { @@ -913,6 +940,7 @@ bool ConfigDescription::HasHigherPrecedenceThan( if (country[0] || o.country[0]) return (!o.country[0]); // Script and variant require either a language or country, both of which // have higher precedence. + if (grammaticalInflection || o.grammaticalInflection) return !o.grammaticalInflection; if ((screenLayout | o.screenLayout) & MASK_LAYOUTDIR) { return !(o.screenLayout & MASK_LAYOUTDIR); } @@ -971,6 +999,7 @@ bool ConfigDescription::ConflictsWith(const ConfigDescription& o) const { // The values here can be found in ResTable_config#match. Density and range // values can't lead to conflicts, and are ignored. return !pred(mcc, o.mcc) || !pred(mnc, o.mnc) || !pred(locale, o.locale) || + !pred(grammaticalInflection, o.grammaticalInflection) || !pred(screenLayout & MASK_LAYOUTDIR, o.screenLayout & MASK_LAYOUTDIR) || !pred(screenLayout & MASK_SCREENLONG, diff --git a/libs/androidfw/Idmap.cpp b/libs/androidfw/Idmap.cpp index efd1f6a25786..89835742c8ff 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(std::move(values_map)); } return {}; } @@ -232,28 +250,29 @@ std::optional<std::string_view> ReadString(const uint8_t** in_out_data_ptr, size } } // namespace -LoadedIdmap::LoadedIdmap(std::string&& idmap_path, - const Idmap_header* header, +LoadedIdmap::LoadedIdmap(std::string&& idmap_path, const Idmap_header* header, 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, - std::string_view target_apk_path) - : header_(header), - data_header_(data_header), - target_entries_(target_entries), - target_inline_entries_(target_inline_entries), - overlay_entries_(overlay_entries), - string_pool_(std::move(string_pool)), - idmap_path_(std::move(idmap_path)), - overlay_apk_path_(overlay_apk_path), - target_apk_path_(target_apk_path), - idmap_last_mod_time_(getFileModDate(idmap_path_.data())) {} - -std::unique_ptr<LoadedIdmap> LoadedIdmap::Load(const StringPiece& idmap_path, - const StringPiece& idmap_data) { + std::string_view overlay_apk_path, std::string_view target_apk_path) + : header_(header), + 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)), + overlay_apk_path_(overlay_apk_path), + target_apk_path_(target_apk_path), + idmap_last_mod_time_(getFileModDate(idmap_path_.data())) {} + +std::unique_ptr<LoadedIdmap> LoadedIdmap::Load(StringPiece idmap_path, StringPiece idmap_data) { ATRACE_CALL(); size_t data_size = idmap_data.size(); auto data_ptr = reinterpret_cast<const uint8_t*>(idmap_data.data()); @@ -303,6 +322,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) { @@ -328,9 +362,9 @@ 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)); + new LoadedIdmap(std::string(idmap_path), header, data_header, target_entries, + 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..c0fdfe25da21 100644 --- a/libs/androidfw/LoadedArsc.cpp +++ b/libs/androidfw/LoadedArsc.cpp @@ -88,7 +88,9 @@ static bool VerifyResTableType(incfs::map_ptr<ResTable_type> header) { // Make sure that there is enough room for the entry offsets. const size_t offsets_offset = dtohs(header->header.headerSize); const size_t entries_offset = dtohl(header->entriesStart); - const size_t offsets_length = sizeof(uint32_t) * entry_count; + const size_t offsets_length = header->flags & ResTable_type::FLAG_OFFSET16 + ? sizeof(uint16_t) * entry_count + : sizeof(uint32_t) * entry_count; if (offsets_offset > entries_offset || entries_offset - offsets_offset < offsets_length) { LOG(ERROR) << "RES_TABLE_TYPE_TYPE entry offsets overlap actual entry data."; @@ -107,8 +109,8 @@ static bool VerifyResTableType(incfs::map_ptr<ResTable_type> header) { return true; } -static base::expected<std::monostate, NullOrIOError> VerifyResTableEntry( - incfs::verified_map_ptr<ResTable_type> type, uint32_t entry_offset) { +static base::expected<incfs::verified_map_ptr<ResTable_entry>, NullOrIOError> +VerifyResTableEntry(incfs::verified_map_ptr<ResTable_type> type, uint32_t entry_offset) { // Check that the offset is aligned. if (UNLIKELY(entry_offset & 0x03U)) { LOG(ERROR) << "Entry at offset " << entry_offset << " is not 4-byte aligned."; @@ -136,7 +138,7 @@ static base::expected<std::monostate, NullOrIOError> VerifyResTableEntry( return base::unexpected(IOError::PAGES_MISSING); } - const size_t entry_size = dtohs(entry->size); + const size_t entry_size = entry->size(); if (UNLIKELY(entry_size < sizeof(entry.value()))) { LOG(ERROR) << "ResTable_entry size " << entry_size << " at offset " << entry_offset << " is too small."; @@ -149,6 +151,11 @@ static base::expected<std::monostate, NullOrIOError> VerifyResTableEntry( return base::unexpected(std::nullopt); } + // If entry is compact, value is already encoded, and a compact entry + // cannot be a map_entry, we are done verifying + if (entry->is_compact()) + return entry.verified(); + if (entry_size < sizeof(ResTable_map_entry)) { // There needs to be room for one Res_value struct. if (UNLIKELY(entry_offset + entry_size > chunk_size - sizeof(Res_value))) { @@ -192,7 +199,7 @@ static base::expected<std::monostate, NullOrIOError> VerifyResTableEntry( return base::unexpected(std::nullopt); } } - return {}; + return entry.verified(); } LoadedPackage::iterator::iterator(const LoadedPackage* lp, size_t ti, size_t ei) @@ -228,7 +235,7 @@ uint32_t LoadedPackage::iterator::operator*() const { entryIndex_); } -base::expected<incfs::map_ptr<ResTable_entry>, NullOrIOError> LoadedPackage::GetEntry( +base::expected<incfs::verified_map_ptr<ResTable_entry>, NullOrIOError> LoadedPackage::GetEntry( incfs::verified_map_ptr<ResTable_type> type_chunk, uint16_t entry_index) { base::expected<uint32_t, NullOrIOError> entry_offset = GetEntryOffset(type_chunk, entry_index); if (UNLIKELY(!entry_offset.has_value())) { @@ -242,14 +249,13 @@ base::expected<uint32_t, NullOrIOError> LoadedPackage::GetEntryOffset( // The configuration matches and is better than the previous selection. // Find the entry value if it exists for this configuration. const size_t entry_count = dtohl(type_chunk->entryCount); - const size_t offsets_offset = dtohs(type_chunk->header.headerSize); + const auto offsets = type_chunk.offset(dtohs(type_chunk->header.headerSize)); // Check if there is the desired entry in this type. if (type_chunk->flags & ResTable_type::FLAG_SPARSE) { // This is encoded as a sparse map, so perform a binary search. bool error = false; - auto sparse_indices = type_chunk.offset(offsets_offset) - .convert<ResTable_sparseTypeEntry>().iterator(); + auto sparse_indices = offsets.convert<ResTable_sparseTypeEntry>().iterator(); auto sparse_indices_end = sparse_indices + entry_count; auto result = std::lower_bound(sparse_indices, sparse_indices_end, entry_index, [&error](const incfs::map_ptr<ResTable_sparseTypeEntry>& entry, @@ -284,26 +290,36 @@ base::expected<uint32_t, NullOrIOError> LoadedPackage::GetEntryOffset( return base::unexpected(std::nullopt); } - const auto entry_offset_ptr = type_chunk.offset(offsets_offset).convert<uint32_t>() + entry_index; - if (UNLIKELY(!entry_offset_ptr)) { - return base::unexpected(IOError::PAGES_MISSING); + uint32_t result; + + if (type_chunk->flags & ResTable_type::FLAG_OFFSET16) { + const auto entry_offset_ptr = offsets.convert<uint16_t>() + entry_index; + if (UNLIKELY(!entry_offset_ptr)) { + return base::unexpected(IOError::PAGES_MISSING); + } + result = offset_from16(entry_offset_ptr.value()); + } else { + const auto entry_offset_ptr = offsets.convert<uint32_t>() + entry_index; + if (UNLIKELY(!entry_offset_ptr)) { + return base::unexpected(IOError::PAGES_MISSING); + } + result = dtohl(entry_offset_ptr.value()); } - const uint32_t value = dtohl(entry_offset_ptr.value()); - if (value == ResTable_type::NO_ENTRY) { + if (result == ResTable_type::NO_ENTRY) { return base::unexpected(std::nullopt); } - - return value; + return result; } -base::expected<incfs::map_ptr<ResTable_entry>, NullOrIOError> LoadedPackage::GetEntryFromOffset( - incfs::verified_map_ptr<ResTable_type> type_chunk, uint32_t offset) { +base::expected<incfs::verified_map_ptr<ResTable_entry>, NullOrIOError> +LoadedPackage::GetEntryFromOffset(incfs::verified_map_ptr<ResTable_type> type_chunk, + uint32_t offset) { auto valid = VerifyResTableEntry(type_chunk, offset); if (UNLIKELY(!valid.has_value())) { return base::unexpected(valid.error()); } - return type_chunk.offset(offset + dtohl(type_chunk->entriesStart)).convert<ResTable_entry>(); + return valid; } base::expected<std::monostate, IOError> LoadedPackage::CollectConfigurations( @@ -376,31 +392,42 @@ base::expected<uint32_t, NullOrIOError> LoadedPackage::FindEntryByName( for (const auto& type_entry : type_spec->type_entries) { const incfs::verified_map_ptr<ResTable_type>& type = type_entry.type; - size_t entry_count = dtohl(type->entryCount); - for (size_t entry_idx = 0; entry_idx < entry_count; entry_idx++) { - auto entry_offset_ptr = type.offset(dtohs(type->header.headerSize)).convert<uint32_t>() + - entry_idx; - if (!entry_offset_ptr) { - return base::unexpected(IOError::PAGES_MISSING); - } + const size_t entry_count = dtohl(type->entryCount); + const auto entry_offsets = type.offset(dtohs(type->header.headerSize)); + for (size_t entry_idx = 0; entry_idx < entry_count; entry_idx++) { uint32_t offset; uint16_t res_idx; if (type->flags & ResTable_type::FLAG_SPARSE) { - auto sparse_entry = entry_offset_ptr.convert<ResTable_sparseTypeEntry>(); + auto sparse_entry = entry_offsets.convert<ResTable_sparseTypeEntry>() + entry_idx; + if (!sparse_entry) { + return base::unexpected(IOError::PAGES_MISSING); + } offset = dtohs(sparse_entry->offset) * 4u; res_idx = dtohs(sparse_entry->idx); + } else if (type->flags & ResTable_type::FLAG_OFFSET16) { + auto entry = entry_offsets.convert<uint16_t>() + entry_idx; + if (!entry) { + return base::unexpected(IOError::PAGES_MISSING); + } + offset = offset_from16(entry.value()); + res_idx = entry_idx; } else { - offset = dtohl(entry_offset_ptr.value()); + auto entry = entry_offsets.convert<uint32_t>() + entry_idx; + if (!entry) { + return base::unexpected(IOError::PAGES_MISSING); + } + offset = dtohl(entry.value()); res_idx = entry_idx; } + if (offset != ResTable_type::NO_ENTRY) { auto entry = type.offset(dtohl(type->entriesStart) + offset).convert<ResTable_entry>(); if (!entry) { return base::unexpected(IOError::PAGES_MISSING); } - if (dtohl(entry->key.index) == static_cast<uint32_t>(*key_idx)) { + if (entry->key() == static_cast<uint32_t>(*key_idx)) { // The package ID will be overridden by the caller (due to runtime assignment of package // IDs for shared libraries). return make_resid(0x00, *type_idx + type_id_offset_ + 1, res_idx); @@ -618,16 +645,16 @@ std::unique_ptr<const LoadedPackage> LoadedPackage::Load(const Chunk& chunk, } std::string name; - util::ReadUtf16StringFromDevice(overlayable->name, arraysize(overlayable->name), &name); + util::ReadUtf16StringFromDevice(overlayable->name, std::size(overlayable->name), &name); std::string actor; - util::ReadUtf16StringFromDevice(overlayable->actor, arraysize(overlayable->actor), &actor); - - if (loaded_package->overlayable_map_.find(name) != - loaded_package->overlayable_map_.end()) { - LOG(ERROR) << "Multiple <overlayable> blocks with the same name '" << name << "'."; + util::ReadUtf16StringFromDevice(overlayable->actor, std::size(overlayable->actor), &actor); + auto [name_to_actor_it, inserted] = + loaded_package->overlayable_map_.emplace(std::move(name), std::move(actor)); + if (!inserted) { + LOG(ERROR) << "Multiple <overlayable> blocks with the same name '" + << name_to_actor_it->first << "'."; return {}; } - loaded_package->overlayable_map_.emplace(name, actor); // Iterate over the overlayable policy chunks contained within the overlayable chunk data ChunkIterator overlayable_iter(child_chunk.data_ptr(), child_chunk.data_size()); @@ -642,7 +669,6 @@ std::unique_ptr<const LoadedPackage> LoadedPackage::Load(const Chunk& chunk, LOG(ERROR) << "RES_TABLE_OVERLAYABLE_POLICY_TYPE too small."; return {}; } - if ((overlayable_child_chunk.data_size() / sizeof(ResTable_ref)) < dtohl(policy_header->entry_count)) { LOG(ERROR) << "RES_TABLE_OVERLAYABLE_POLICY_TYPE too small to hold entries."; @@ -664,8 +690,8 @@ std::unique_ptr<const LoadedPackage> LoadedPackage::Load(const Chunk& chunk, // Add the pairing of overlayable properties and resource ids to the package OverlayableInfo overlayable_info { - .name = name, - .actor = actor, + .name = name_to_actor_it->first, + .actor = name_to_actor_it->second, .policy_flags = policy_header->policy_flags }; loaded_package->overlayable_infos_.emplace_back(std::move(overlayable_info), std::move(ids)); @@ -709,6 +735,7 @@ std::unique_ptr<const LoadedPackage> LoadedPackage::Load(const Chunk& chunk, const auto entry_end = entry_begin + dtohl(lib_alias->count); std::unordered_set<uint32_t> finalized_ids; finalized_ids.reserve(entry_end - entry_begin); + loaded_package->alias_id_map_.reserve(entry_end - entry_begin); for (auto entry_iter = entry_begin; entry_iter != entry_end; ++entry_iter) { if (!entry_iter) { LOG(ERROR) << "NULL ResTable_staged_alias_entry record??"; @@ -722,13 +749,20 @@ std::unique_ptr<const LoadedPackage> LoadedPackage::Load(const Chunk& chunk, } auto staged_id = dtohl(entry_iter->stagedResId); - auto [_, success] = loaded_package->alias_id_map_.emplace(staged_id, finalized_id); - if (!success) { + loaded_package->alias_id_map_.emplace_back(staged_id, finalized_id); + } + + std::sort(loaded_package->alias_id_map_.begin(), loaded_package->alias_id_map_.end(), + [](auto&& l, auto&& r) { return l.first < r.first; }); + const auto duplicate_it = + std::adjacent_find(loaded_package->alias_id_map_.begin(), + loaded_package->alias_id_map_.end(), + [](auto&& l, auto&& r) { return l.first == r.first; }); + if (duplicate_it != loaded_package->alias_id_map_.end()) { LOG(ERROR) << StringPrintf("Repeated staged resource id '%08x' in staged aliases.", - staged_id); + duplicate_it->first); return {}; } - } } break; default: @@ -820,6 +854,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 +896,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/Locale.cpp b/libs/androidfw/Locale.cpp index d87a3ce72177..272a988ec55a 100644 --- a/libs/androidfw/Locale.cpp +++ b/libs/androidfw/Locale.cpp @@ -66,7 +66,7 @@ static inline bool is_number(const std::string& str) { return std::all_of(std::begin(str), std::end(str), ::isdigit); } -bool LocaleValue::InitFromFilterString(const StringPiece& str) { +bool LocaleValue::InitFromFilterString(StringPiece str) { // A locale (as specified in the filter) is an underscore separated name such // as "en_US", "en_Latn_US", or "en_US_POSIX". std::vector<std::string> parts = util::SplitAndLowercase(str, '_'); @@ -132,11 +132,11 @@ bool LocaleValue::InitFromFilterString(const StringPiece& str) { return true; } -bool LocaleValue::InitFromBcp47Tag(const StringPiece& bcp47tag) { +bool LocaleValue::InitFromBcp47Tag(StringPiece bcp47tag) { return InitFromBcp47TagImpl(bcp47tag, '-'); } -bool LocaleValue::InitFromBcp47TagImpl(const StringPiece& bcp47tag, const char separator) { +bool LocaleValue::InitFromBcp47TagImpl(StringPiece bcp47tag, const char separator) { std::vector<std::string> subtags = util::SplitAndLowercase(bcp47tag, separator); if (subtags.size() == 1) { set_language(subtags[0].c_str()); diff --git a/libs/androidfw/LocaleDataTables.cpp b/libs/androidfw/LocaleDataTables.cpp index 2c005fd81de5..b68143d82090 100644 --- a/libs/androidfw/LocaleDataTables.cpp +++ b/libs/androidfw/LocaleDataTables.cpp @@ -39,204 +39,206 @@ 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 + {0xCD200000u, 2u}, // ajt -> Arab + {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 +246,1253 @@ 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 + {0x6F634553u, 46u}, // oc-ES -> 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 + {0xC90F0000u, 46u}, // pis -> 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 + {0x8D920000u, 46u}, // smd -> 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 + {0x85B20000u, 46u}, // snb -> 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 + {0xA9D30000u, 46u}, // tok -> 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 +1509,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({ @@ -1517,6 +1533,7 @@ std::unordered_set<uint64_t> REPRESENTATIVE_LOCALES({ 0x61665A414C61746ELLU, // af_Latn_ZA 0xC0C0434D4C61746ELLU, // agq_Latn_CM 0xB8E0494E41686F6DLLU, // aho_Ahom_IN + 0xCD20544E41726162LLU, // ajt_Arab_TN 0x616B47484C61746ELLU, // ak_Latn_GH 0xA940495158737578LLU, // akk_Xsux_IQ 0xB560584B4C61746ELLU, // aln_Latn_XK @@ -1524,6 +1541,7 @@ std::unordered_set<uint64_t> REPRESENTATIVE_LOCALES({ 0x616D455445746869LLU, // am_Ethi_ET 0xB9804E474C61746ELLU, // amo_Latn_NG 0x616E45534C61746ELLU, // an_Latn_ES + 0xB5A04E474C61746ELLU, // ann_Latn_NG 0xE5C049444C61746ELLU, // aoz_Latn_ID 0x8DE0544741726162LLU, // apd_Arab_TG 0x6172454741726162LLU, // ar_Arab_EG @@ -1580,6 +1598,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 +1642,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 +1769,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 +1787,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 +1797,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 +1820,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 +1878,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 +1904,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 +1950,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 +2023,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 @@ -2020,6 +2047,7 @@ std::unordered_set<uint64_t> REPRESENTATIVE_LOCALES({ 0xB88F49525870656FLLU, // peo_Xpeo_IR 0xACAF44454C61746ELLU, // pfl_Latn_DE 0xB4EF4C4250686E78LLU, // phn_Phnx_LB + 0xC90F53424C61746ELLU, // pis_Latn_SB 0x814F494E42726168LLU, // pka_Brah_IN 0xB94F4B454C61746ELLU, // pko_Latn_KE 0x706C504C4C61746ELLU, // pl_Latn_PL @@ -2027,6 +2055,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 +2103,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 @@ -2100,11 +2128,13 @@ std::unordered_set<uint64_t> REPRESENTATIVE_LOCALES({ 0xE17249444C61746ELLU, // sly_Latn_ID 0x736D57534C61746ELLU, // sm_Latn_WS 0x819253454C61746ELLU, // sma_Latn_SE + 0x8D92414F4C61746ELLU, // smd_Latn_AO 0xA59253454C61746ELLU, // smj_Latn_SE 0xB59246494C61746ELLU, // smn_Latn_FI 0xBD92494C53616D72LLU, // smp_Samr_IL 0xC99246494C61746ELLU, // sms_Latn_FI 0x736E5A574C61746ELLU, // sn_Latn_ZW + 0x85B24D594C61746ELLU, // snb_Latn_MY 0xA9B24D4C4C61746ELLU, // snk_Latn_ML 0x736F534F4C61746ELLU, // so_Latn_SO 0x99D2555A536F6764LLU, // sog_Sogd_UZ @@ -2275,6 +2305,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 +2367,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 +2452,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/PosixUtils.cpp b/libs/androidfw/PosixUtils.cpp index 026912883a73..8ddc57240129 100644 --- a/libs/androidfw/PosixUtils.cpp +++ b/libs/androidfw/PosixUtils.cpp @@ -17,7 +17,7 @@ #ifdef _WIN32 // nothing to see here #else -#include <memory> +#include <optional> #include <string> #include <vector> @@ -29,45 +29,42 @@ #include "androidfw/PosixUtils.h" -namespace { - -std::unique_ptr<std::string> ReadFile(int fd) { - std::unique_ptr<std::string> str(new std::string()); +static std::optional<std::string> ReadFile(int fd) { + std::string str; char buf[1024]; ssize_t r; while ((r = read(fd, buf, sizeof(buf))) > 0) { - str->append(buf, r); + str.append(buf, r); } if (r != 0) { - return nullptr; + return std::nullopt; } - return str; -} - + return std::move(str); } namespace android { namespace util { -std::unique_ptr<ProcResult> ExecuteBinary(const std::vector<std::string>& argv) { - int stdout[2]; // stdout[0] read, stdout[1] write +ProcResult ExecuteBinary(const std::vector<std::string>& argv) { + int stdout[2]; // [0] read, [1] write if (pipe(stdout) != 0) { - PLOG(ERROR) << "pipe"; - return nullptr; + PLOG(ERROR) << "out pipe"; + return ProcResult{-1}; } - int stderr[2]; // stdout[0] read, stdout[1] write + int stderr[2]; // [0] read, [1] write if (pipe(stderr) != 0) { - PLOG(ERROR) << "pipe"; + PLOG(ERROR) << "err pipe"; close(stdout[0]); close(stdout[1]); - return nullptr; + return ProcResult{-1}; } auto gid = getgid(); auto uid = getuid(); - char const** argv0 = (char const**)malloc(sizeof(char*) * (argv.size() + 1)); + // better keep no C++ objects going into the child here + auto argv0 = (char const**)malloc(sizeof(char*) * (argv.size() + 1)); for (size_t i = 0; i < argv.size(); i++) { argv0[i] = argv[i].c_str(); } @@ -76,8 +73,12 @@ std::unique_ptr<ProcResult> ExecuteBinary(const std::vector<std::string>& argv) switch (pid) { case -1: // error free(argv0); + close(stdout[0]); + close(stdout[1]); + close(stderr[0]); + close(stderr[1]); PLOG(ERROR) << "fork"; - return nullptr; + return ProcResult{-1}; case 0: // child if (setgid(gid) != 0) { PLOG(ERROR) << "setgid"; @@ -109,17 +110,16 @@ std::unique_ptr<ProcResult> ExecuteBinary(const std::vector<std::string>& argv) if (!WIFEXITED(status)) { close(stdout[0]); close(stderr[0]); - return nullptr; + return ProcResult{-1}; } - std::unique_ptr<ProcResult> result(new ProcResult()); - result->status = status; - const auto out = ReadFile(stdout[0]); - result->stdout_str = out ? *out : ""; + ProcResult result(status); + auto out = ReadFile(stdout[0]); + result.stdout_str = out ? std::move(*out) : ""; close(stdout[0]); - const auto err = ReadFile(stderr[0]); - result->stderr_str = err ? *err : ""; + auto err = ReadFile(stderr[0]); + result.stderr_str = err ? std::move(*err) : ""; close(stderr[0]); - return result; + return std::move(result); } } diff --git a/libs/androidfw/ResourceTimer.cpp b/libs/androidfw/ResourceTimer.cpp new file mode 100644 index 000000000000..44128d9e4e3d --- /dev/null +++ b/libs/androidfw/ResourceTimer.cpp @@ -0,0 +1,271 @@ +/* + * 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. + */ + +#include <unistd.h> +#include <string.h> + +#include <map> +#include <atomic> + +#include <utils/Log.h> +#include <androidfw/ResourceTimer.h> + +// The following block allows compilation on windows, which does not have getuid(). +#ifdef _WIN32 +#ifdef ERROR +#undef ERROR +#endif +#define getuid() (getUidWindows_) +#endif + +namespace android { + +namespace { + +#ifdef _WIN32 +// A temporary to confuse lint into thinking that getuid() on windows might return something other +// than zero. +int getUidWindows_ = 0; +#endif + +// The number of nanoseconds in a microsecond. +static const unsigned int US = 1000; +// The number of nanoseconds in a second. +static const unsigned int S = 1000 * 1000 * 1000; + +// Return the difference between two timespec values. The difference is in nanoseconds. If the +// return value would exceed 2s (2^31 nanoseconds) then UINT_MAX is returned. +unsigned int diffInNs(timespec const &a, timespec const &b) { + timespec r = { 0, 0 }; + r.tv_nsec = a.tv_nsec - b.tv_nsec; + if (r.tv_nsec < 0) { + r.tv_sec = -1; + r.tv_nsec += S; + } + r.tv_sec = r.tv_sec + (a.tv_sec - b.tv_sec); + if (r.tv_sec > 2) return UINT_MAX; + unsigned int result = (r.tv_sec * S) + r.tv_nsec; + if (result > 2 * S) return UINT_MAX; + return result; +} + +} + +ResourceTimer::ResourceTimer(Counter api) + : active_(enabled_.load()), + api_(api) { + if (active_) { + clock_gettime(CLOCK_MONOTONIC, &start_); + } +} + +ResourceTimer::~ResourceTimer() { + record(); +} + +void ResourceTimer::enable() { + if (!enabled_.load()) counter_ = new GuardedTimer[ResourceTimer::counterSize]; + enabled_.store(true); +} + +void ResourceTimer::cancel() { + active_ = false; +} + +void ResourceTimer::record() { + if (!active_) return; + + struct timespec end; + clock_gettime(CLOCK_MONOTONIC, &end); + // Get the difference in microseconds. + const unsigned int ticks = diffInNs(end, start_); + ScopedTimer t(counter_[toIndex(api_)]); + t->record(ticks); + active_ = false; +} + +bool ResourceTimer::copy(int counter, Timer &dst, bool reset) { + ScopedTimer t(counter_[counter]); + if (t->count == 0) { + dst.reset(); + if (reset) t->reset(); + return false; + } + Timer::copy(dst, *t, reset); + return true; +} + +void ResourceTimer::reset() { + for (int i = 0; i < counterSize; i++) { + ScopedTimer t(counter_[i]); + t->reset(); + } +} + +ResourceTimer::Timer::Timer() { + // Ensure newly-created objects are zeroed. + memset(buckets, 0, sizeof(buckets)); + reset(); +} + +ResourceTimer::Timer::~Timer() { + for (int d = 0; d < MaxDimension; d++) { + delete[] buckets[d]; + } +} + +void ResourceTimer::Timer::freeBuckets() { + for (int d = 0; d < MaxDimension; d++) { + delete[] buckets[d]; + buckets[d] = 0; + } +} + +void ResourceTimer::Timer::reset() { + count = total = mintime = maxtime = 0; + memset(largest, 0, sizeof(largest)); + memset(&pvalues, 0, sizeof(pvalues)); + // Zero the histogram, keeping any allocated dimensions. + for (int d = 0; d < MaxDimension; d++) { + if (buckets[d] != 0) memset(buckets[d], 0, sizeof(int) * MaxBuckets); + } +} + +void ResourceTimer::Timer::copy(Timer &dst, Timer &src, bool reset) { + dst.freeBuckets(); + dst = src; + // Clean up the histograms. + if (reset) { + // Do NOT free the src buckets because they being used by dst. + memset(src.buckets, 0, sizeof(src.buckets)); + src.reset(); + } else { + for (int d = 0; d < MaxDimension; d++) { + if (src.buckets[d] != nullptr) { + dst.buckets[d] = new int[MaxBuckets]; + memcpy(dst.buckets[d], src.buckets[d], sizeof(int) * MaxBuckets); + } + } + } +} + +void ResourceTimer::Timer::record(int ticks) { + // Record that the event happened. + count++; + + total += ticks; + if (mintime == 0 || ticks < mintime) mintime = ticks; + if (ticks > maxtime) maxtime = ticks; + + // Do not add oversized events to the histogram. + if (ticks != UINT_MAX) { + for (int d = 0; d < MaxDimension; d++) { + if (ticks < range[d]) { + if (buckets[d] == 0) { + buckets[d] = new int[MaxBuckets]; + memset(buckets[d], 0, sizeof(int) * MaxBuckets); + } + if (ticks < width[d]) { + // Special case: never write to bucket 0 because it complicates the percentile logic. + // However, this is always the smallest possible value to it is very unlikely to ever + // affect any of the percentile results. + buckets[d][1]++; + } else { + buckets[d][ticks / width[d]]++; + } + break; + } + } + } + + // The list of largest times is sorted with the biggest value at index 0 and the smallest at + // index MaxLargest-1. The incoming tick count should be added to the array only if it is + // larger than the current value at MaxLargest-1. + if (ticks > largest[Timer::MaxLargest-1]) { + for (size_t i = 0; i < Timer::MaxLargest; i++) { + if (ticks > largest[i]) { + if (i < Timer::MaxLargest-1) { + for (size_t j = Timer::MaxLargest - 1; j > i; j--) { + largest[j] = largest[j-1]; + } + } + largest[i] = ticks; + break; + } + } + } +} + +void ResourceTimer::Timer::Percentile::compute( + int cumulative, int current, int count, int width, int time) { + nominal = time; + nominal_actual = (cumulative * 100) / count; + floor = nominal - width; + floor_actual = ((cumulative - current) * 100) / count; +} + +void ResourceTimer::Timer::compute() { + memset(&pvalues, 0, sizeof(pvalues)); + + float l50 = count / 2.0; + float l90 = (count * 9.0) / 10.0; + float l95 = (count * 95.0) / 100.0; + float l99 = (count * 99.0) / 100.0; + + int sum = 0; + for (int d = 0; d < MaxDimension; d++) { + if (buckets[d] == 0) continue; + for (int j = 0; j < MaxBuckets && sum < count; j++) { + // Empty buckets don't contribute to the answers. Skip them. + if (buckets[d][j] == 0) continue; + sum += buckets[d][j]; + // A word on indexing. j is never zero in the following lines. buckets[0][0] corresponds + // to a delay of 0us, which cannot happen. buckets[n][0], for n > 0 overlaps a value in + // buckets[n-1], and the code would have stopped there. + if (sum >= l50 && pvalues.p50.nominal == 0) { + pvalues.p50.compute(sum, buckets[d][j], count, width[d], j * width[d]); + } + if (sum >= l90 && pvalues.p90.nominal == 0) { + pvalues.p90.compute(sum, buckets[d][j], count, width[d], j * width[d]); + } + if (sum >= l95 && pvalues.p95.nominal == 0) { + pvalues.p95.compute(sum, buckets[d][j], count, width[d], j * width[d]); + } + if (sum >= l99 && pvalues.p99.nominal == 0) { + pvalues.p99.compute(sum, buckets[d][j], count, width[d], j * width[d]); + } + } + } +} + +char const *ResourceTimer::toString(ResourceTimer::Counter counter) { + switch (counter) { + case Counter::GetResourceValue: + return "GetResourceValue"; + case Counter::RetrieveAttributes: + return "RetrieveAttributes"; + }; + return "Unknown"; +} + +std::atomic<bool> ResourceTimer::enabled_(false); +std::atomic<ResourceTimer::GuardedTimer *> ResourceTimer::counter_(nullptr); + +const int ResourceTimer::Timer::range[] = { 100 * US, 1000 * US, 10*1000 * US, 100*1000 * US }; +const int ResourceTimer::Timer::width[] = { 1 * US, 10 * US, 100 * US, 1000 * US }; + + +} // namespace android diff --git a/libs/androidfw/ResourceTypes.cpp b/libs/androidfw/ResourceTypes.cpp index 5e8a623d4205..29d33da6b2f7 100644 --- a/libs/androidfw/ResourceTypes.cpp +++ b/libs/androidfw/ResourceTypes.cpp @@ -33,7 +33,9 @@ #include <type_traits> #include <vector> +#include <android-base/file.h> #include <android-base/macros.h> +#include <android-base/utf8.h> #include <androidfw/ByteBucketArray.h> #include <androidfw/ResourceTypes.h> #include <androidfw/TypeWrappers.h> @@ -236,12 +238,23 @@ void Res_png_9patch::serialize(const Res_png_9patch& patch, const int32_t* xDivs } bool IsFabricatedOverlay(const std::string& path) { - std::ifstream fin(path); + return IsFabricatedOverlay(path.c_str()); +} + +bool IsFabricatedOverlay(const char* path) { + auto fd = base::unique_fd(base::utf8::open(path, O_RDONLY|O_CLOEXEC)); + if (fd < 0) { + return false; + } + return IsFabricatedOverlay(fd); +} + +bool IsFabricatedOverlay(base::borrowed_fd fd) { uint32_t magic; - if (fin.read(reinterpret_cast<char*>(&magic), sizeof(uint32_t))) { - return magic == kFabricatedOverlayMagic; + if (!base::ReadFullyAtOffset(fd, &magic, sizeof(magic), 0)) { + return false; } - return false; + return magic == kFabricatedOverlayMagic; } static bool assertIdmapHeader(const void* idmap, size_t size) { @@ -2092,6 +2105,9 @@ int ResTable_config::compare(const ResTable_config& o) const { return 1; } + if (grammaticalInflection != o.grammaticalInflection) { + return grammaticalInflection < o.grammaticalInflection ? -1 : 1; + } if (screenType != o.screenType) { return (screenType > o.screenType) ? 1 : -1; } @@ -2140,7 +2156,9 @@ int ResTable_config::compareLogical(const ResTable_config& o) const { if (diff > 0) { return 1; } - + if (grammaticalInflection != o.grammaticalInflection) { + return grammaticalInflection < o.grammaticalInflection ? -1 : 1; + } if ((screenLayout & MASK_LAYOUTDIR) != (o.screenLayout & MASK_LAYOUTDIR)) { return (screenLayout & MASK_LAYOUTDIR) < (o.screenLayout & MASK_LAYOUTDIR) ? -1 : 1; } @@ -2210,6 +2228,7 @@ int ResTable_config::diff(const ResTable_config& o) const { if (uiMode != o.uiMode) diffs |= CONFIG_UI_MODE; if (smallestScreenWidthDp != o.smallestScreenWidthDp) diffs |= CONFIG_SMALLEST_SCREEN_SIZE; if (screenSizeDp != o.screenSizeDp) diffs |= CONFIG_SCREEN_SIZE; + if (grammaticalInflection != o.grammaticalInflection) diffs |= CONFIG_GRAMMATICAL_GENDER; const int diff = compareLocales(*this, o); if (diff) diffs |= CONFIG_LOCALE; @@ -2276,6 +2295,13 @@ bool ResTable_config::isMoreSpecificThan(const ResTable_config& o) const { } } + if (grammaticalInflection || o.grammaticalInflection) { + if (grammaticalInflection != o.grammaticalInflection) { + if (!grammaticalInflection) return false; + if (!o.grammaticalInflection) return true; + } + } + if (screenLayout || o.screenLayout) { if (((screenLayout^o.screenLayout) & MASK_LAYOUTDIR) != 0) { if (!(screenLayout & MASK_LAYOUTDIR)) return false; @@ -2542,6 +2568,13 @@ bool ResTable_config::isBetterThan(const ResTable_config& o, return true; } + if (grammaticalInflection || o.grammaticalInflection) { + if (grammaticalInflection != o.grammaticalInflection + && requested->grammaticalInflection) { + return !!grammaticalInflection; + } + } + if (screenLayout || o.screenLayout) { if (((screenLayout^o.screenLayout) & MASK_LAYOUTDIR) != 0 && (requested->screenLayout & MASK_LAYOUTDIR)) { @@ -2841,6 +2874,10 @@ bool ResTable_config::match(const ResTable_config& settings) const { } } + if (grammaticalInflection && grammaticalInflection != settings.grammaticalInflection) { + return false; + } + if (screenConfig != 0) { const int layoutDir = screenLayout&MASK_LAYOUTDIR; const int setLayoutDir = settings.screenLayout&MASK_LAYOUTDIR; @@ -3254,6 +3291,15 @@ String8 ResTable_config::toString() const { appendDirLocale(res); + if ((grammaticalInflection & GRAMMATICAL_INFLECTION_GENDER_MASK) != 0) { + if (res.size() > 0) res.append("-"); + switch (grammaticalInflection & GRAMMATICAL_INFLECTION_GENDER_MASK) { + case GRAMMATICAL_GENDER_NEUTER: res.append("neuter"); break; + case GRAMMATICAL_GENDER_FEMININE: res.append("feminine"); break; + case GRAMMATICAL_GENDER_MASCULINE: res.append("masculine"); break; + } + } + if ((screenLayout&MASK_LAYOUTDIR) != 0) { if (res.size() > 0) res.append("-"); switch (screenLayout&ResTable_config::MASK_LAYOUTDIR) { @@ -4487,20 +4533,14 @@ ssize_t ResTable::getResource(uint32_t resID, Res_value* outValue, bool mayBeBag return err; } - if ((dtohs(entry.entry->flags) & ResTable_entry::FLAG_COMPLEX) != 0) { + if (entry.entry->map_entry()) { if (!mayBeBag) { ALOGW("Requesting resource 0x%08x failed because it is complex\n", resID); } return BAD_VALUE; } - const Res_value* value = reinterpret_cast<const Res_value*>( - reinterpret_cast<const uint8_t*>(entry.entry) + entry.entry->size); - - outValue->size = dtohs(value->size); - outValue->res0 = value->res0; - outValue->dataType = value->dataType; - outValue->data = dtohl(value->data); + *outValue = entry.entry->value(); // The reference may be pointing to a resource in a shared library. These // references have build-time generated package IDs. These ids may not match @@ -4691,11 +4731,10 @@ ssize_t ResTable::getBagLocked(uint32_t resID, const bag_entry** outBag, return err; } - const uint16_t entrySize = dtohs(entry.entry->size); - const uint32_t parent = entrySize >= sizeof(ResTable_map_entry) - ? dtohl(((const ResTable_map_entry*)entry.entry)->parent.ident) : 0; - const uint32_t count = entrySize >= sizeof(ResTable_map_entry) - ? dtohl(((const ResTable_map_entry*)entry.entry)->count) : 0; + const uint16_t entrySize = entry.entry->size(); + const ResTable_map_entry* map_entry = entry.entry->map_entry(); + const uint32_t parent = map_entry ? dtohl(map_entry->parent.ident) : 0; + const uint32_t count = map_entry ? dtohl(map_entry->count) : 0; size_t N = count; @@ -4759,7 +4798,7 @@ ssize_t ResTable::getBagLocked(uint32_t resID, const bag_entry** outBag, // Now merge in the new attributes... size_t curOff = (reinterpret_cast<uintptr_t>(entry.entry) - reinterpret_cast<uintptr_t>(entry.type)) - + dtohs(entry.entry->size); + + entrySize; const ResTable_map* map; bag_entry* entries = (bag_entry*)(set+1); size_t curEntry = 0; @@ -5137,7 +5176,7 @@ uint32_t ResTable::findEntry(const PackageGroup* group, ssize_t typeIndex, const continue; } - if (dtohl(entry->key.index) == (size_t) *ei) { + if (entry->key() == (size_t) *ei) { uint32_t resId = Res_MAKEID(group->id - 1, typeIndex, iter.index()); if (outTypeSpecFlags) { Entry result; @@ -6600,8 +6639,12 @@ status_t ResTable::getEntry( // Entry does not exist. continue; } - - thisOffset = dtohl(eindex[realEntryIndex]); + if (thisType->flags & ResTable_type::FLAG_OFFSET16) { + auto eindex16 = reinterpret_cast<const uint16_t*>(eindex); + thisOffset = offset_from16(eindex16[realEntryIndex]); + } else { + thisOffset = dtohl(eindex[realEntryIndex]); + } } if (thisOffset == ResTable_type::NO_ENTRY) { @@ -6651,8 +6694,8 @@ status_t ResTable::getEntry( const ResTable_entry* const entry = reinterpret_cast<const ResTable_entry*>( reinterpret_cast<const uint8_t*>(bestType) + bestOffset); - if (dtohs(entry->size) < sizeof(*entry)) { - ALOGW("ResTable_entry size 0x%x is too small", dtohs(entry->size)); + if (entry->size() < sizeof(*entry)) { + ALOGW("ResTable_entry size 0x%zx is too small", entry->size()); return BAD_TYPE; } @@ -6663,7 +6706,7 @@ status_t ResTable::getEntry( outEntry->specFlags = specFlags; outEntry->package = bestPackage; outEntry->typeStr = StringPoolRef(&bestPackage->typeStrings, actualTypeIndex - bestPackage->typeIdOffset); - outEntry->keyStr = StringPoolRef(&bestPackage->keyStrings, dtohl(entry->key.index)); + outEntry->keyStr = StringPoolRef(&bestPackage->keyStrings, entry->key()); } return NO_ERROR; } @@ -6880,7 +6923,8 @@ status_t ResTable::parsePackage(const ResTable_package* const pkg, const uint32_t typeSize = dtohl(type->header.size); const size_t newEntryCount = dtohl(type->entryCount); - + const size_t entrySize = type->flags & ResTable_type::FLAG_OFFSET16 ? + sizeof(uint16_t) : sizeof(uint32_t); if (kDebugLoadTableNoisy) { printf("Type off %p: type=0x%x, headerSize=0x%x, size=%u\n", (void*)(base-(const uint8_t*)chunk), @@ -6888,9 +6932,9 @@ status_t ResTable::parsePackage(const ResTable_package* const pkg, dtohs(type->header.headerSize), typeSize); } - if (dtohs(type->header.headerSize)+(sizeof(uint32_t)*newEntryCount) > typeSize) { + if (dtohs(type->header.headerSize)+(entrySize*newEntryCount) > typeSize) { ALOGW("ResTable_type entry index to %p extends beyond chunk end 0x%x.", - (void*)(dtohs(type->header.headerSize) + (sizeof(uint32_t)*newEntryCount)), + (void*)(dtohs(type->header.headerSize) + (entrySize*newEntryCount)), typeSize); return (mError=BAD_TYPE); } @@ -6991,11 +7035,10 @@ status_t ResTable::parsePackage(const ResTable_package* const pkg, DynamicRefTable::DynamicRefTable() : DynamicRefTable(0, false) {} DynamicRefTable::DynamicRefTable(uint8_t packageId, bool appAsLib) - : mAssignedPackageId(packageId) + : mLookupTable() + , mAssignedPackageId(packageId) , mAppAsLib(appAsLib) { - memset(mLookupTable, 0, sizeof(mLookupTable)); - // Reserved package ids mLookupTable[APP_PACKAGE_ID] = APP_PACKAGE_ID; mLookupTable[SYS_PACKAGE_ID] = SYS_PACKAGE_ID; @@ -7076,10 +7119,6 @@ void DynamicRefTable::addMapping(uint8_t buildPackageId, uint8_t runtimePackageI mLookupTable[buildPackageId] = runtimePackageId; } -void DynamicRefTable::addAlias(uint32_t stagedId, uint32_t finalizedId) { - mAliasId[stagedId] = finalizedId; -} - status_t DynamicRefTable::lookupResourceId(uint32_t* resId) const { uint32_t res = *resId; size_t packageId = Res_GETPACKAGE(res) + 1; @@ -7089,11 +7128,12 @@ status_t DynamicRefTable::lookupResourceId(uint32_t* resId) const { return NO_ERROR; } - auto alias_id = mAliasId.find(res); - if (alias_id != mAliasId.end()) { + const auto alias_it = std::lower_bound(mAliasId.begin(), mAliasId.end(), res, + [](const AliasMap::value_type& pair, uint32_t val) { return pair.first < val; }); + if (alias_it != mAliasId.end() && alias_it->first == res) { // Rewrite the resource id to its alias resource id. Since the alias resource id is a // compile-time id, it still needs to be resolved further. - res = alias_id->second; + res = alias_it->second; } if (packageId == SYS_PACKAGE_ID || (packageId == APP_PACKAGE_ID && !mAppAsLib)) { @@ -7653,6 +7693,9 @@ void ResTable::print(bool inclValues) const if (type->flags & ResTable_type::FLAG_SPARSE) { printf(" [sparse]"); } + if (type->flags & ResTable_type::FLAG_OFFSET16) { + printf(" [offset16]"); + } } printf(":\n"); @@ -7684,7 +7727,13 @@ void ResTable::print(bool inclValues) const thisOffset = static_cast<uint32_t>(dtohs(entry->offset)) * 4u; } else { entryId = entryIndex; - thisOffset = dtohl(eindex[entryIndex]); + if (type->flags & ResTable_type::FLAG_OFFSET16) { + const auto eindex16 = + reinterpret_cast<const uint16_t*>(eindex); + thisOffset = offset_from16(eindex16[entryIndex]); + } else { + thisOffset = dtohl(eindex[entryIndex]); + } if (thisOffset == ResTable_type::NO_ENTRY) { continue; } @@ -7734,7 +7783,7 @@ void ResTable::print(bool inclValues) const continue; } - uintptr_t esize = dtohs(ent->size); + uintptr_t esize = ent->size(); if ((esize&0x3) != 0) { printf("NON-INTEGER ResTable_entry SIZE: %p\n", (void *)esize); continue; @@ -7746,30 +7795,27 @@ void ResTable::print(bool inclValues) const } const Res_value* valuePtr = NULL; - const ResTable_map_entry* bagPtr = NULL; + const ResTable_map_entry* bagPtr = ent->map_entry(); Res_value value; - if ((dtohs(ent->flags)&ResTable_entry::FLAG_COMPLEX) != 0) { + if (bagPtr) { printf("<bag>"); - bagPtr = (const ResTable_map_entry*)ent; } else { - valuePtr = (const Res_value*) - (((const uint8_t*)ent) + esize); - value.copyFrom_dtoh(*valuePtr); + value = ent->value(); printf("t=0x%02x d=0x%08x (s=0x%04x r=0x%02x)", (int)value.dataType, (int)value.data, (int)value.size, (int)value.res0); } - if ((dtohs(ent->flags)&ResTable_entry::FLAG_PUBLIC) != 0) { + if (ent->flags() & ResTable_entry::FLAG_PUBLIC) { printf(" (PUBLIC)"); } printf("\n"); if (inclValues) { - if (valuePtr != NULL) { + if (bagPtr == NULL) { printf(" "); print_value(typeConfigs->package, value); - } else if (bagPtr != NULL) { + } else { const int N = dtohl(bagPtr->count); const uint8_t* baseMapPtr = (const uint8_t*)ent; size_t mapOffset = esize; diff --git a/libs/androidfw/ResourceUtils.cpp b/libs/androidfw/ResourceUtils.cpp index 87fb2c038c9f..ccb61561578f 100644 --- a/libs/androidfw/ResourceUtils.cpp +++ b/libs/androidfw/ResourceUtils.cpp @@ -18,7 +18,7 @@ namespace android { -bool ExtractResourceName(const StringPiece& str, StringPiece* out_package, StringPiece* out_type, +bool ExtractResourceName(StringPiece str, StringPiece* out_package, StringPiece* out_type, StringPiece* out_entry) { *out_package = ""; *out_type = ""; @@ -33,16 +33,16 @@ bool ExtractResourceName(const StringPiece& str, StringPiece* out_package, Strin while (current != end) { if (out_type->size() == 0 && *current == '/') { has_type_separator = true; - out_type->assign(start, current - start); + *out_type = StringPiece(start, current - start); start = current + 1; } else if (out_package->size() == 0 && *current == ':') { has_package_separator = true; - out_package->assign(start, current - start); + *out_package = StringPiece(start, current - start); start = current + 1; } current++; } - out_entry->assign(start, end - start); + *out_entry = StringPiece(start, end - start); return !(has_package_separator && out_package->empty()) && !(has_type_separator && out_type->empty()); @@ -50,7 +50,7 @@ bool ExtractResourceName(const StringPiece& str, StringPiece* out_package, Strin base::expected<AssetManager2::ResourceName, NullOrIOError> ToResourceName( const StringPoolRef& type_string_ref, const StringPoolRef& entry_string_ref, - const StringPiece& package_name) { + StringPiece package_name) { AssetManager2::ResourceName name{ .package = package_name.data(), .package_len = package_name.size(), diff --git a/libs/androidfw/StringPool.cpp b/libs/androidfw/StringPool.cpp new file mode 100644 index 000000000000..1cb8df311c89 --- /dev/null +++ b/libs/androidfw/StringPool.cpp @@ -0,0 +1,507 @@ +/* + * 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(StringPiece str) { + return MakeRefImpl(str, Context{}, true); +} + +StringPool::Ref StringPool::MakeRef(StringPiece str, const Context& context) { + return MakeRefImpl(str, context, true); +} + +StringPool::Ref StringPool::MakeRefImpl(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 = std::string(str); + 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/TypeWrappers.cpp b/libs/androidfw/TypeWrappers.cpp index 647aa197a94d..70d14a11830e 100644 --- a/libs/androidfw/TypeWrappers.cpp +++ b/libs/androidfw/TypeWrappers.cpp @@ -59,7 +59,9 @@ const ResTable_entry* TypeVariant::iterator::operator*() const { + dtohl(type->header.size); const uint32_t* const entryIndices = reinterpret_cast<const uint32_t*>( reinterpret_cast<uintptr_t>(type) + dtohs(type->header.headerSize)); - if (reinterpret_cast<uintptr_t>(entryIndices) + (sizeof(uint32_t) * entryCount) > containerEnd) { + const size_t indexSize = type->flags & ResTable_type::FLAG_OFFSET16 ? + sizeof(uint16_t) : sizeof(uint32_t); + if (reinterpret_cast<uintptr_t>(entryIndices) + (indexSize * entryCount) > containerEnd) { ALOGE("Type's entry indices extend beyond its boundaries"); return NULL; } @@ -73,6 +75,9 @@ const ResTable_entry* TypeVariant::iterator::operator*() const { } entryOffset = static_cast<uint32_t>(dtohs(ResTable_sparseTypeEntry{*iter}.offset)) * 4u; + } else if (type->flags & ResTable_type::FLAG_OFFSET16) { + auto entryIndices16 = reinterpret_cast<const uint16_t*>(entryIndices); + entryOffset = offset_from16(entryIndices16[mIndex]); } else { entryOffset = dtohl(entryIndices[mIndex]); } @@ -91,11 +96,11 @@ const ResTable_entry* TypeVariant::iterator::operator*() const { if (reinterpret_cast<uintptr_t>(entry) > containerEnd - sizeof(*entry)) { ALOGE("Entry offset at index %u points outside the Type's boundaries", mIndex); return NULL; - } else if (reinterpret_cast<uintptr_t>(entry) + dtohs(entry->size) > containerEnd) { + } else if (reinterpret_cast<uintptr_t>(entry) + entry->size() > containerEnd) { ALOGE("Entry at index %u extends beyond Type's boundaries", mIndex); return NULL; - } else if (dtohs(entry->size) < sizeof(*entry)) { - ALOGE("Entry at index %u is too small (%u)", mIndex, dtohs(entry->size)); + } else if (entry->size() < sizeof(*entry)) { + ALOGE("Entry at index %u is too small (%zu)", mIndex, entry->size()); return NULL; } return entry; diff --git a/libs/androidfw/Util.cpp b/libs/androidfw/Util.cpp index 59c9d640bb91..be55fe8b4bb6 100644 --- a/libs/androidfw/Util.cpp +++ b/libs/androidfw/Util.cpp @@ -42,7 +42,7 @@ void ReadUtf16StringFromDevice(const uint16_t* src, size_t len, std::string* out } } -std::u16string Utf8ToUtf16(const StringPiece& utf8) { +std::u16string Utf8ToUtf16(StringPiece utf8) { ssize_t utf16_length = utf8_to_utf16_length(reinterpret_cast<const uint8_t*>(utf8.data()), utf8.length()); if (utf16_length <= 0) { @@ -56,7 +56,7 @@ std::u16string Utf8ToUtf16(const StringPiece& utf8) { return utf16; } -std::string Utf16ToUtf8(const StringPiece16& utf16) { +std::string Utf16ToUtf8(StringPiece16 utf16) { ssize_t utf8_length = utf16_to_utf8_length(utf16.data(), utf16.length()); if (utf8_length <= 0) { return {}; @@ -68,28 +68,151 @@ std::string Utf16ToUtf8(const StringPiece16& utf16) { return utf8; } -static std::vector<std::string> SplitAndTransform( - const StringPiece& str, char sep, const std::function<char(char)>& f) { +std::string Utf8ToModifiedUtf8(std::string_view 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 std::string(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(std::string_view 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; +} + +template <class Func> +static std::vector<std::string> SplitAndTransform(StringPiece str, char sep, Func&& f) { std::vector<std::string> parts; const StringPiece::const_iterator end = std::end(str); StringPiece::const_iterator start = std::begin(str); StringPiece::const_iterator current; do { current = std::find(start, end, sep); - parts.emplace_back(str.substr(start, current).to_string()); - if (f) { - std::string& part = parts.back(); - std::transform(part.begin(), part.end(), part.begin(), f); - } + parts.emplace_back(StringPiece(start, current - start)); + std::string& part = parts.back(); + std::transform(part.begin(), part.end(), part.begin(), f); start = current + 1; } while (current != end); return parts; } -std::vector<std::string> SplitAndLowercase(const StringPiece& str, char sep) { - return SplitAndTransform(str, sep, ::tolower); +std::vector<std::string> SplitAndLowercase(StringPiece str, char sep) { + return SplitAndTransform(str, sep, [](char c) { return ::tolower(c); }); } +std::unique_ptr<uint8_t[]> Copy(const BigBuffer& buffer) { + auto 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); + } + return Utf16ToUtf8(GetString16(pool, idx)); +} } // namespace util } // namespace android diff --git a/libs/androidfw/ZipUtils.cpp b/libs/androidfw/ZipUtils.cpp index 58fc5bbbab5e..a1385f2cf7b1 100644 --- a/libs/androidfw/ZipUtils.cpp +++ b/libs/androidfw/ZipUtils.cpp @@ -35,7 +35,7 @@ using namespace android; // TODO: This can go away once the only remaining usage in aapt goes away. -class FileReader : public zip_archive::Reader { +class FileReader final : public zip_archive::Reader { public: explicit FileReader(FILE* fp) : Reader(), mFp(fp), mCurrentOffset(0) { } @@ -66,7 +66,7 @@ class FileReader : public zip_archive::Reader { mutable off64_t mCurrentOffset; }; -class FdReader : public zip_archive::Reader { +class FdReader final : public zip_archive::Reader { public: explicit FdReader(int fd) : mFd(fd) { } @@ -79,7 +79,7 @@ class FdReader : public zip_archive::Reader { const int mFd; }; -class BufferReader : public zip_archive::Reader { +class BufferReader final : public zip_archive::Reader { public: BufferReader(incfs::map_ptr<void> input, size_t inputSize) : Reader(), mInput(input.convert<uint8_t>()), @@ -105,7 +105,7 @@ class BufferReader : public zip_archive::Reader { const size_t mInputSize; }; -class BufferWriter : public zip_archive::Writer { +class BufferWriter final : public zip_archive::Writer { public: BufferWriter(void* output, size_t outputSize) : Writer(), mOutput(reinterpret_cast<uint8_t*>(output)), mOutputSize(outputSize), mBytesWritten(0) { diff --git a/libs/androidfw/include/androidfw/ApkParsing.h b/libs/androidfw/include/androidfw/ApkParsing.h new file mode 100644 index 000000000000..194eaae8e12a --- /dev/null +++ b/libs/androidfw/include/androidfw/ApkParsing.h @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#pragma once + +#include <string_view> +#include <sys/types.h> + +extern const std::string_view APK_LIB; +extern const size_t APK_LIB_LEN; + +namespace android::util { +// Checks if filename is a valid library path and returns a pointer to the last slash in the path +// if it is, nullptr otherwise +const char* ValidLibraryPathLastSlash(const char* filename, bool suppress64Bit, bool debuggable); + +// Equivalent to android.os.FileUtils.isFilenameSafe +bool isFilenameSafe(const char* filename); +} diff --git a/libs/androidfw/include/androidfw/AssetManager2.h b/libs/androidfw/include/androidfw/AssetManager2.h index 1bde792da2ba..f10cb9bf480a 100644 --- a/libs/androidfw/include/androidfw/AssetManager2.h +++ b/libs/androidfw/include/androidfw/AssetManager2.h @@ -17,6 +17,7 @@ #ifndef ANDROIDFW_ASSETMANAGER2_H_ #define ANDROIDFW_ASSETMANAGER2_H_ +#include "android-base/function_ref.h" #include "android-base/macros.h" #include <array> @@ -104,7 +105,7 @@ class AssetManager2 { // new resource IDs. bool SetApkAssets(std::vector<const ApkAssets*> apk_assets, bool invalidate_caches = true); - inline const std::vector<const ApkAssets*> GetApkAssets() const { + inline const std::vector<const ApkAssets*>& GetApkAssets() const { return apk_assets_; } @@ -124,8 +125,7 @@ class AssetManager2 { uint8_t GetAssignedPackageId(const LoadedPackage* package) const; // Returns a string representation of the overlayable API of a package. - bool GetOverlayablesToString(const android::StringPiece& package_name, - std::string* out) const; + bool GetOverlayablesToString(android::StringPiece package_name, std::string* out) const; const std::unordered_map<std::string, std::string>* GetOverlayableMapForPackage( uint32_t package_id) const; @@ -321,17 +321,8 @@ class AssetManager2 { // Creates a new Theme from this AssetManager. std::unique_ptr<Theme> NewTheme(); - void ForEachPackage(const std::function<bool(const std::string&, uint8_t)> func, - package_property_t excluded_property_flags = 0U) const { - for (const PackageGroup& package_group : package_groups_) { - const auto loaded_package = package_group.packages_.front().loaded_package_; - if ((loaded_package->GetPropertyFlags() & excluded_property_flags) == 0U - && !func(loaded_package->GetPackageName(), - package_group.dynamic_ref_table->mAssignedPackageId)) { - return; - } - } - } + void ForEachPackage(base::function_ref<bool(const std::string&, uint8_t)> func, + package_property_t excluded_property_flags = 0U) const; void DumpToLog() const; @@ -572,6 +563,7 @@ class Theme { AssetManager2* asset_manager_ = nullptr; uint32_t type_spec_flags_ = 0u; + std::vector<uint32_t> keys_; std::vector<Entry> entries_; }; diff --git a/libs/androidfw/include/androidfw/AssetsProvider.h b/libs/androidfw/include/androidfw/AssetsProvider.h index 966ec74c1786..d33c325ff369 100644 --- a/libs/androidfw/include/androidfw/AssetsProvider.h +++ b/libs/androidfw/include/androidfw/AssetsProvider.h @@ -20,6 +20,7 @@ #include <memory> #include <string> +#include "android-base/function_ref.h" #include "android-base/macros.h" #include "android-base/unique_fd.h" @@ -46,7 +47,7 @@ struct AssetsProvider { // Iterate over all files and directories provided by the interface. The order of iteration is // stable. virtual bool ForEachFile(const std::string& path, - const std::function<void(const StringPiece&, FileType)>& f) const = 0; + base::function_ref<void(StringPiece, FileType)> f) const = 0; // Retrieves the path to the contents of the AssetsProvider on disk. The path could represent an // APk, a directory, or some other file type. @@ -80,8 +81,8 @@ struct AssetsProvider { // Supplies assets from a zip archive. struct ZipAssetsProvider : public AssetsProvider { - static std::unique_ptr<ZipAssetsProvider> Create(std::string path, - package_property_t flags); + static std::unique_ptr<ZipAssetsProvider> Create(std::string path, package_property_t flags, + base::unique_fd fd = {}); static std::unique_ptr<ZipAssetsProvider> Create(base::unique_fd fd, std::string friendly_name, @@ -90,7 +91,7 @@ struct ZipAssetsProvider : public AssetsProvider { off64_t len = kUnknownLength); bool ForEachFile(const std::string& root_path, - const std::function<void(const StringPiece&, FileType)>& f) const override; + base::function_ref<void(StringPiece, FileType)> f) const override; WARN_UNUSED std::optional<std::string_view> GetPath() const override; WARN_UNUSED const std::string& GetDebugName() const override; @@ -108,7 +109,12 @@ struct ZipAssetsProvider : public AssetsProvider { time_t last_mod_time); struct PathOrDebugName { - PathOrDebugName(std::string&& value, bool is_path); + static PathOrDebugName Path(std::string value) { + return {std::move(value), true}; + } + static PathOrDebugName DebugName(std::string value) { + return {std::move(value), false}; + } // Retrieves the path or null if this class represents a debug name. WARN_UNUSED const std::string* GetPath() const; @@ -117,11 +123,16 @@ struct ZipAssetsProvider : public AssetsProvider { WARN_UNUSED const std::string& GetDebugName() const; private: + PathOrDebugName(std::string value, bool is_path) : value_(std::move(value)), is_path_(is_path) { + } std::string value_; bool is_path_; }; - std::unique_ptr<ZipArchive, void (*)(ZipArchive*)> zip_handle_; + struct ZipCloser { + void operator()(ZipArchive* a) const; + }; + std::unique_ptr<ZipArchive, ZipCloser> zip_handle_; PathOrDebugName name_; package_property_t flags_; time_t last_mod_time_; @@ -132,7 +143,7 @@ struct DirectoryAssetsProvider : public AssetsProvider { static std::unique_ptr<DirectoryAssetsProvider> Create(std::string root_dir); bool ForEachFile(const std::string& path, - const std::function<void(const StringPiece&, FileType)>& f) const override; + base::function_ref<void(StringPiece, FileType)> f) const override; WARN_UNUSED std::optional<std::string_view> GetPath() const override; WARN_UNUSED const std::string& GetDebugName() const override; @@ -157,7 +168,7 @@ struct MultiAssetsProvider : public AssetsProvider { std::unique_ptr<AssetsProvider>&& secondary); bool ForEachFile(const std::string& root_path, - const std::function<void(const StringPiece&, FileType)>& f) const override; + base::function_ref<void(StringPiece, FileType)> f) const override; WARN_UNUSED std::optional<std::string_view> GetPath() const override; WARN_UNUSED const std::string& GetDebugName() const override; @@ -181,10 +192,10 @@ struct MultiAssetsProvider : public AssetsProvider { // Does not provide any assets. struct EmptyAssetsProvider : public AssetsProvider { static std::unique_ptr<AssetsProvider> Create(); - static std::unique_ptr<AssetsProvider> Create(const std::string& path); + static std::unique_ptr<AssetsProvider> Create(std::string path); bool ForEachFile(const std::string& path, - const std::function<void(const StringPiece&, FileType)>& f) const override; + base::function_ref<void(StringPiece, FileType)> f) const override; WARN_UNUSED std::optional<std::string_view> GetPath() const override; WARN_UNUSED const std::string& GetDebugName() const override; 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/ByteBucketArray.h b/libs/androidfw/include/androidfw/ByteBucketArray.h index 949c9445b3e8..ca0a9eda9caa 100644 --- a/libs/androidfw/include/androidfw/ByteBucketArray.h +++ b/libs/androidfw/include/androidfw/ByteBucketArray.h @@ -17,6 +17,7 @@ #ifndef __BYTE_BUCKET_ARRAY_H #define __BYTE_BUCKET_ARRAY_H +#include <algorithm> #include <cstdint> #include <cstring> @@ -31,14 +32,16 @@ namespace android { template <typename T> class ByteBucketArray { public: - ByteBucketArray() : default_() { memset(buckets_, 0, sizeof(buckets_)); } + ByteBucketArray() { + memset(buckets_, 0, sizeof(buckets_)); + } ~ByteBucketArray() { - for (size_t i = 0; i < kNumBuckets; i++) { - if (buckets_[i] != NULL) { - delete[] buckets_[i]; - } - } + deleteBuckets(); + } + + void clear() { + deleteBuckets(); memset(buckets_, 0, sizeof(buckets_)); } @@ -53,7 +56,7 @@ class ByteBucketArray { uint8_t bucket_index = static_cast<uint8_t>(index) >> 4; T* bucket = buckets_[bucket_index]; - if (bucket == NULL) { + if (bucket == nullptr) { return default_; } return bucket[0x0f & static_cast<uint8_t>(index)]; @@ -64,9 +67,9 @@ class ByteBucketArray { << ") with size=" << size(); uint8_t bucket_index = static_cast<uint8_t>(index) >> 4; - T* bucket = buckets_[bucket_index]; - if (bucket == NULL) { - bucket = buckets_[bucket_index] = new T[kBucketSize](); + T*& bucket = buckets_[bucket_index]; + if (bucket == nullptr) { + bucket = new T[kBucketSize](); } return bucket[0x0f & static_cast<uint8_t>(index)]; } @@ -80,11 +83,44 @@ class ByteBucketArray { return true; } + template <class Func> + void forEachItem(Func f) { + for (size_t i = 0; i < kNumBuckets; i++) { + const auto bucket = buckets_[i]; + if (bucket != nullptr) { + for (size_t j = 0; j < kBucketSize; j++) { + f((i << 4) | j, bucket[j]); + } + } + } + } + + template <class Func> + void trimBuckets(Func isEmptyFunc) { + for (size_t i = 0; i < kNumBuckets; i++) { + const auto bucket = buckets_[i]; + if (bucket != nullptr) { + if (std::all_of(bucket, bucket + kBucketSize, isEmptyFunc)) { + delete[] bucket; + buckets_[i] = nullptr; + } + } + } + } + private: enum { kNumBuckets = 16, kBucketSize = 16 }; + void deleteBuckets() { + for (size_t i = 0; i < kNumBuckets; i++) { + if (buckets_[i] != nullptr) { + delete[] buckets_[i]; + } + } + } + T* buckets_[kNumBuckets]; - T default_; + static inline const T default_ = {}; }; } // namespace android diff --git a/libs/androidfw/include/androidfw/ConfigDescription.h b/libs/androidfw/include/androidfw/ConfigDescription.h index 61d10cd4e55b..7fbd7c08ea69 100644 --- a/libs/androidfw/include/androidfw/ConfigDescription.h +++ b/libs/androidfw/include/androidfw/ConfigDescription.h @@ -53,6 +53,12 @@ enum : ApiVersion { SDK_O = 26, SDK_O_MR1 = 27, SDK_P = 28, + SDK_Q = 29, + SDK_R = 30, + SDK_S = 31, + SDK_S_V2 = 32, + SDK_TIRAMISU = 33, + SDK_U = 34, }; /* @@ -72,7 +78,7 @@ struct ConfigDescription : public ResTable_config { * The resulting configuration has the appropriate sdkVersion defined * for backwards compatibility. */ - static bool Parse(const android::StringPiece& str, ConfigDescription* out = nullptr); + static bool Parse(android::StringPiece str, ConfigDescription* out = nullptr); /** * If the configuration uses an axis that was added after diff --git a/libs/androidfw/include/androidfw/IDiagnostics.h b/libs/androidfw/include/androidfw/IDiagnostics.h new file mode 100644 index 000000000000..4d5844eaa069 --- /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(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_ << 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..60689128dffb 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(std::map<ConfigDescription, Res_value> value) : data_(std::move(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_; @@ -149,8 +157,7 @@ class IdmapResMap { class LoadedIdmap { public: // Loads an IDMAP from a chunk of memory. Returns nullptr if the IDMAP data was malformed. - static std::unique_ptr<LoadedIdmap> Load(const StringPiece& idmap_path, - const StringPiece& idmap_data); + static std::unique_ptr<LoadedIdmap> Load(StringPiece idmap_path, StringPiece idmap_data); // Returns the path to the IDMAP. std::string_view IdmapPath() const { @@ -170,8 +177,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 +198,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 +216,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..4d12885ad291 100644 --- a/libs/androidfw/include/androidfw/LoadedArsc.h +++ b/libs/androidfw/include/androidfw/LoadedArsc.h @@ -99,8 +99,8 @@ enum : package_property_t { }; struct OverlayableInfo { - std::string name; - std::string actor; + std::string_view name; + std::string_view actor; uint32_t policy_flags; }; @@ -166,14 +166,14 @@ class LoadedPackage { base::expected<uint32_t, NullOrIOError> FindEntryByName(const std::u16string& type_name, const std::u16string& entry_name) const; - static base::expected<incfs::map_ptr<ResTable_entry>, NullOrIOError> GetEntry( - incfs::verified_map_ptr<ResTable_type> type_chunk, uint16_t entry_index); + static base::expected<incfs::verified_map_ptr<ResTable_entry>, NullOrIOError> + GetEntry(incfs::verified_map_ptr<ResTable_type> type_chunk, uint16_t entry_index); static base::expected<uint32_t, NullOrIOError> GetEntryOffset( incfs::verified_map_ptr<ResTable_type> type_chunk, uint16_t entry_index); - static base::expected<incfs::map_ptr<ResTable_entry>, NullOrIOError> GetEntryFromOffset( - incfs::verified_map_ptr<ResTable_type> type_chunk, uint32_t offset); + static base::expected<incfs::verified_map_ptr<ResTable_entry>, NullOrIOError> + GetEntryFromOffset(incfs::verified_map_ptr<ResTable_type> type_chunk, uint32_t offset); // Returns the string pool where type names are stored. const ResStringPool* GetTypeStringPool() const { @@ -275,7 +275,7 @@ class LoadedPackage { return overlayable_map_; } - const std::map<uint32_t, uint32_t>& GetAliasResourceIdMap() const { + const std::vector<std::pair<uint32_t, uint32_t>>& GetAliasResourceIdMap() const { return alias_id_map_; } @@ -295,8 +295,8 @@ class LoadedPackage { std::unordered_map<uint8_t, TypeSpec> type_specs_; ByteBucketArray<uint32_t> resource_ids_; std::vector<DynamicPackageEntry> dynamic_package_map_; - std::vector<const std::pair<OverlayableInfo, std::unordered_set<uint32_t>>> overlayable_infos_; - std::map<uint32_t, uint32_t> alias_id_map_; + std::vector<std::pair<OverlayableInfo, std::unordered_set<uint32_t>>> overlayable_infos_; + std::vector<std::pair<uint32_t, uint32_t>> alias_id_map_; // A map of overlayable name to actor std::unordered_map<std::string, std::string> overlayable_map_; @@ -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/Locale.h b/libs/androidfw/include/androidfw/Locale.h index 484ed79a8efd..8934bed098fe 100644 --- a/libs/androidfw/include/androidfw/Locale.h +++ b/libs/androidfw/include/androidfw/Locale.h @@ -39,10 +39,10 @@ struct LocaleValue { /** * Initialize this LocaleValue from a config string. */ - bool InitFromFilterString(const android::StringPiece& config); + bool InitFromFilterString(android::StringPiece config); // Initializes this LocaleValue from a BCP-47 locale tag. - bool InitFromBcp47Tag(const android::StringPiece& bcp47tag); + bool InitFromBcp47Tag(android::StringPiece bcp47tag); /** * Initialize this LocaleValue from parts of a vector. @@ -70,7 +70,7 @@ struct LocaleValue { inline bool operator>(const LocaleValue& o) const; private: - bool InitFromBcp47TagImpl(const android::StringPiece& bcp47tag, const char separator); + bool InitFromBcp47TagImpl(android::StringPiece bcp47tag, const char separator); void set_language(const char* language); void set_region(const char* language); diff --git a/libs/androidfw/include/androidfw/PosixUtils.h b/libs/androidfw/include/androidfw/PosixUtils.h index bb2084740a44..c46e5e6b3fb5 100644 --- a/libs/androidfw/include/androidfw/PosixUtils.h +++ b/libs/androidfw/include/androidfw/PosixUtils.h @@ -25,12 +25,18 @@ struct ProcResult { int status; std::string stdout_str; std::string stderr_str; + + explicit ProcResult(int status) : status(status) {} + ProcResult(ProcResult&&) noexcept = default; + ProcResult& operator=(ProcResult&&) noexcept = default; + + explicit operator bool() const { return status >= 0; } }; -// Fork, exec and wait for an external process. Return nullptr if the process could not be launched, -// otherwise a ProcResult containing the external process' exit status and captured stdout and -// stderr. -std::unique_ptr<ProcResult> ExecuteBinary(const std::vector<std::string>& argv); +// Fork, exec and wait for an external process. Returns status < 0 if the process could not be +// launched, otherwise a ProcResult containing the external process' exit status and captured +// stdout and stderr. +ProcResult ExecuteBinary(const std::vector<std::string>& argv); } // namespace util } // namespace android diff --git a/libs/androidfw/include/androidfw/ResourceTimer.h b/libs/androidfw/include/androidfw/ResourceTimer.h new file mode 100644 index 000000000000..74613519a920 --- /dev/null +++ b/libs/androidfw/include/androidfw/ResourceTimer.h @@ -0,0 +1,221 @@ +/* + * 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. + */ + +#ifndef ANDROIDFW_RESOURCETIMER_H_ +#define ANDROIDFW_RESOURCETIMER_H_ + +#include <time.h> +#include <atomic> +#include <vector> + +#include <utils/Mutex.h> +#include <android-base/macros.h> +#include <androidfw/Util.h> + +namespace android { + +// ResourceTimer captures the duration of short functions. Durations are accumulated in registers +// and statistics are pulled back to the Java layer as needed. +// To monitor an API, first add it to the Counter enumeration. Then, inside the API, create an +// instance of ResourceTimer with the appropriate enumeral. The corresponding counter will be +// updated when the ResourceTimer destructor is called, normally at the end of the enclosing block. +class ResourceTimer { + public: + enum class Counter { + GetResourceValue, + RetrieveAttributes, + + LastCounter = RetrieveAttributes, + }; + static const int counterSize = static_cast<int>(Counter::LastCounter) + 1; + static char const *toString(Counter); + + // Start a timer for the specified counter. + ResourceTimer(Counter); + // The block is exiting. If the timer is active, record it. + ~ResourceTimer(); + // This records the elapsed time and disables further recording. Use this if the containing + // block includes extra processing that should not be included in the timer. The method is + // destructive in that the timer is no longer valid and further calls to record() will be + // ignored. + void record(); + // This cancels a timer. Elapsed time will neither be computed nor recorded. + void cancel(); + + // A single timer contains the count of events and the cumulative time spent handling the + // events. It also includes the smallest value seen and 10 largest values seen. Finally, it + // includes a histogram of values that approximates a semi-log. + + // The timer can compute percentiles of recorded events. For example, the p50 value is a time + // such that 50% of the readings are below the value and 50% are above the value. The + // granularity in the readings means that a percentile cannot always be computed. In this case, + // the percentile is reported as zero. (The simplest example is when there is a single + // reading.) Even if the value can be computed, it will not be exact. Therefore, a percentile + // is actually reported as two values: the lowest time at which it might be valid and the + // highest time at which it might be valid. + struct Timer { + static const size_t MaxLargest = 5; + + // The construct zeros all the fields. The destructor releases memory allocated to the + // buckets. + Timer(); + ~Timer(); + + // The following summary values are set to zero on a reset. All times are in ns. + + // The total number of events recorded. + int count; + // The total duration of events. + int64_t total; + // The smallest event duration seen. This is guaranteed to be non-zero if count is greater + // than 0. + int mintime; + // The largest event duration seen. + int maxtime; + + // The largest values seen. Element 0 is the largest value seen (and is the same as maxtime, + // above). Element 1 is the next largest, and so on. If count is less than MaxLargest, + // unused elements will be zero. + int largest[MaxLargest]; + + // The p50 value is a time such that 50% of the readings are below that time and 50% of the + // readings. + + // A single percentile is defined by the lowest value supported by the readings and the + // highest value supported by the readings. + struct Percentile { + // The nominal time (in ns) of the percentile. The true percentile is guaranteed to be less + // than or equal to this time. + int nominal; + // The actual percentile of the nominal time. + int nominal_actual; + // The time of the next lower bin. The true percentile is guaranteed to be greater than + // this time. + int floor; + // The actual percentile of the floor time. + int floor_actual; + + // Fill in a percentile given the cumulative to the bin, the count in the current bin, the + // total count, the width of the bin, and the time of the bin. + void compute(int cumulative, int current, int count, int width, int time); + }; + + // The structure that holds the percentiles. + struct { + Percentile p50; + Percentile p90; + Percentile p95; + Percentile p99; + } pvalues; + + // Set all counters to zero. + void reset(); + // Record an event. The input time is in ns. + void record(int); + // Compute the percentiles. Percentiles are computed on demand, as the computation is too + // expensive to be done inline. + void compute(); + + // Copy one timer to another. If reset is true then the src is reset immediately after the + // copy. The reset flag is exploited to make the copy faster. Any data in dst is lost. + static void copy(Timer &dst, Timer &src, bool reset); + + private: + // Free any buckets. + void freeBuckets(); + + // Readings are placed in bins, which are orgzanized into decades. The decade 0 covers + // [0,100) in steps of 1us. Decade 1 covers [0,1000) in steps of 10us. Decade 2 covers + // [0,10000) in steps of 100us. And so on. + + // An event is placed in the first bin that can hold it. This means that events in the range + // of [0,100) are placed in the first decade, events in the range of [0,1000) are placed in + // the second decade, and so on. This also means that the first 10% of the bins are unused + // in each decade after the first. + + // The design provides at least two significant digits across the range of [0,10000). + + static const size_t MaxDimension = 4; + static const size_t MaxBuckets = 100; + + // The range of each dimension. The lower value is always zero. + static const int range[MaxDimension]; + // The width of each bin, by dimension + static const int width[MaxDimension]; + + // A histogram of the values seen. Centuries are allocated as needed, to minimize the memory + // impact. + int *buckets[MaxDimension]; + }; + + // Fetch one Timer. The function has a short-circuit behavior: if the count is zero then + // destination count is set to zero and the function returns false. Otherwise, the destination + // is a copy of the source and the function returns true. This behavior lowers the cost of + // handling unused timers. + static bool copy(int src, Timer &dst, bool reset); + + // Enable the timers. Timers are initially disabled. Enabling timers allocates memory for the + // counters. Timers cannot be disabled. + static void enable(); + + private: + // An internal reset method. This does not take a lock. + static void reset(); + + // Helper method to convert a counter into an enum. Presumably, this will be inlined into zero + // actual cpu instructions. + static inline std::vector<unsigned int>::size_type toIndex(Counter c) { + return static_cast<std::vector<unsigned int>::size_type>(c); + } + + // Every counter has an associated lock. The lock has been factored into a separate class to + // keep the Timer class a POD. + struct GuardedTimer { + Mutex lock_; + Timer timer_; + }; + + // Scoped timer + struct ScopedTimer { + AutoMutex _l; + Timer &t; + ScopedTimer(GuardedTimer &g) : + _l(g.lock_), t(g.timer_) { + } + Timer *operator->() { + return &t; + } + Timer& operator*() { + return t; + } + }; + + // An individual timer is active (or not), is tracking a specific API, and has a start time. + // The api and the start time are undefined if the timer is not active. + bool active_; + Counter api_; + struct timespec start_; + + // The global enable flag. This is initially false and may be set true by the java runtime. + static std::atomic<bool> enabled_; + + // The global timers. The memory for the timers is not allocated until the timers are enabled. + static std::atomic<GuardedTimer *> counter_; +}; + +} // namespace android + +#endif /* ANDROIDFW_RESOURCETIMER_H_ */ diff --git a/libs/androidfw/include/androidfw/ResourceTypes.h b/libs/androidfw/include/androidfw/ResourceTypes.h index 3d66244646d5..631bda4f886c 100644 --- a/libs/androidfw/include/androidfw/ResourceTypes.h +++ b/libs/androidfw/include/androidfw/ResourceTypes.h @@ -21,11 +21,13 @@ #define _LIBS_UTILS_RESOURCE_TYPES_H #include <android-base/expected.h> +#include <android-base/unique_fd.h> #include <androidfw/Asset.h> #include <androidfw/Errors.h> #include <androidfw/LocaleData.h> #include <androidfw/StringPiece.h> +#include <utils/ByteOrder.h> #include <utils/Errors.h> #include <utils/String16.h> #include <utils/Vector.h> @@ -45,7 +47,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,10 +55,12 @@ 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 = 3; // Returns whether or not the path represents a fabricated overlay. bool IsFabricatedOverlay(const std::string& path); +bool IsFabricatedOverlay(const char* path); +bool IsFabricatedOverlay(android::base::borrowed_fd fd); /** * In C++11, char16_t is defined as *at least* 16 bits. We do a lot of @@ -1067,15 +1071,32 @@ struct ResTable_config NAVHIDDEN_NO = ACONFIGURATION_NAVHIDDEN_NO << SHIFT_NAVHIDDEN, NAVHIDDEN_YES = ACONFIGURATION_NAVHIDDEN_YES << SHIFT_NAVHIDDEN, }; - - union { - struct { - uint8_t keyboard; - uint8_t navigation; - uint8_t inputFlags; - uint8_t inputPad0; + + enum { + GRAMMATICAL_GENDER_ANY = ACONFIGURATION_GRAMMATICAL_GENDER_ANY, + GRAMMATICAL_GENDER_NEUTER = ACONFIGURATION_GRAMMATICAL_GENDER_NEUTER, + GRAMMATICAL_GENDER_FEMININE = ACONFIGURATION_GRAMMATICAL_GENDER_FEMININE, + GRAMMATICAL_GENDER_MASCULINE = ACONFIGURATION_GRAMMATICAL_GENDER_MASCULINE, + GRAMMATICAL_INFLECTION_GENDER_MASK = 0b11, + }; + + struct { + union { + struct { + uint8_t keyboard; + uint8_t navigation; + uint8_t inputFlags; + uint8_t inputFieldPad0; + }; + struct { + uint32_t input : 24; + uint32_t inputFullPad0 : 8; + }; + struct { + uint8_t grammaticalInflectionPad0[3]; + uint8_t grammaticalInflection; + }; }; - uint32_t input; }; enum { @@ -1098,7 +1119,7 @@ struct ResTable_config SDKVERSION_ANY = 0 }; - enum { + enum { MINORVERSION_ANY = 0 }; @@ -1259,6 +1280,7 @@ struct ResTable_config CONFIG_LAYOUTDIR = ACONFIGURATION_LAYOUTDIR, CONFIG_SCREEN_ROUND = ACONFIGURATION_SCREEN_ROUND, CONFIG_COLOR_MODE = ACONFIGURATION_COLOR_MODE, + CONFIG_GRAMMATICAL_GENDER = ACONFIGURATION_GRAMMATICAL_GENDER, }; // Compare two configuration, returning CONFIG_* flags set for each value @@ -1437,6 +1459,10 @@ struct ResTable_type // Mark any types that use this with a v26 qualifier to prevent runtime issues on older // platforms. FLAG_SPARSE = 0x01, + + // If set, the offsets to the entries are encoded in 16-bit, real_offset = offset * 4u + // An 16-bit offset of 0xffffu means a NO_ENTRY + FLAG_OFFSET16 = 0x02, }; uint8_t flags; @@ -1453,6 +1479,11 @@ struct ResTable_type ResTable_config config; }; +// Convert a 16-bit offset to 32-bit if FLAG_OFFSET16 is set +static inline uint32_t offset_from16(uint16_t off16) { + return dtohs(off16) == 0xffffu ? ResTable_type::NO_ENTRY : dtohs(off16) * 4u; +} + // The minimum size required to read any version of ResTable_type. constexpr size_t kResTableTypeMinSize = sizeof(ResTable_type) - sizeof(ResTable_config) + sizeof(ResTable_config::size); @@ -1480,6 +1511,8 @@ union ResTable_sparseTypeEntry { static_assert(sizeof(ResTable_sparseTypeEntry) == sizeof(uint32_t), "ResTable_sparseTypeEntry must be 4 bytes in size"); +struct ResTable_map_entry; + /** * This is the beginning of information about an entry in the resource * table. It holds the reference to the name of this entry, and is @@ -1487,12 +1520,11 @@ static_assert(sizeof(ResTable_sparseTypeEntry) == sizeof(uint32_t), * * A Res_value structure, if FLAG_COMPLEX is -not- set. * * An array of ResTable_map structures, if FLAG_COMPLEX is set. * These supply a set of name/value mappings of data. + * * If FLAG_COMPACT is set, this entry is a compact entry for + * simple values only */ -struct ResTable_entry +union ResTable_entry { - // Number of bytes in this structure. - uint16_t size; - enum { // If set, this is a complex entry, holding a set of name/value // mappings. It is followed by an array of ResTable_map structures. @@ -1504,18 +1536,91 @@ struct ResTable_entry // resources of the same name/type. This is only useful during // linking with other resource tables. FLAG_WEAK = 0x0004, + // If set, this is a compact entry with data type and value directly + // encoded in the this entry, see ResTable_entry::compact + FLAG_COMPACT = 0x0008, }; - uint16_t flags; - - // Reference into ResTable_package::keyStrings identifying this entry. - struct ResStringPool_ref key; + + struct Full { + // Number of bytes in this structure. + uint16_t size; + + uint16_t flags; + + // Reference into ResTable_package::keyStrings identifying this entry. + struct ResStringPool_ref key; + } full; + + /* A compact entry is indicated by FLAG_COMPACT, with flags at the same + * offset as a normal entry. This is only for simple data values where + * + * - size for entry or value can be inferred (both being 8 bytes). + * - key index is encoded in 16-bit + * - dataType is encoded as the higher 8-bit of flags + * - data is encoded directly in this entry + */ + struct Compact { + uint16_t key; + uint16_t flags; + uint32_t data; + } compact; + + uint16_t flags() const { return dtohs(full.flags); }; + bool is_compact() const { return flags() & FLAG_COMPACT; } + bool is_complex() const { return flags() & FLAG_COMPLEX; } + + size_t size() const { + return is_compact() ? sizeof(ResTable_entry) : dtohs(this->full.size); + } + + uint32_t key() const { + return is_compact() ? dtohs(this->compact.key) : dtohl(this->full.key.index); + } + + /* Always verify the memory associated with this entry and its value + * before calling value() or map_entry() + */ + Res_value value() const { + Res_value v; + if (is_compact()) { + v.size = sizeof(Res_value); + v.res0 = 0; + v.data = dtohl(this->compact.data); + v.dataType = dtohs(compact.flags) >> 8; + } else { + auto vaddr = reinterpret_cast<const uint8_t*>(this) + dtohs(this->full.size); + auto value = reinterpret_cast<const Res_value*>(vaddr); + v.size = dtohs(value->size); + v.res0 = value->res0; + v.data = dtohl(value->data); + v.dataType = value->dataType; + } + return v; + } + + const ResTable_map_entry* map_entry() const { + return is_complex() && !is_compact() ? + reinterpret_cast<const ResTable_map_entry*>(this) : nullptr; + } }; +/* Make sure size of ResTable_entry::Full and ResTable_entry::Compact + * be the same as ResTable_entry. This is to allow iteration of entries + * to work in either cases. + * + * The offset of flags must be at the same place for both structures, + * to ensure the correct reading to decide whether this is a full entry + * or a compact entry. + */ +static_assert(sizeof(ResTable_entry) == sizeof(ResTable_entry::Full)); +static_assert(sizeof(ResTable_entry) == sizeof(ResTable_entry::Compact)); +static_assert(offsetof(ResTable_entry, full.flags) == offsetof(ResTable_entry, compact.flags)); + /** * Extended form of a ResTable_entry for map entries, defining a parent map * resource from which to inherit values. */ -struct ResTable_map_entry : public ResTable_entry +struct ResTable_map_entry : public ResTable_entry::Full { // Resource identifier of the parent mapping, or 0 if there is none. // This is always treated as a TYPE_DYNAMIC_REFERENCE. @@ -1746,6 +1851,28 @@ inline ResTable_overlayable_policy_header::PolicyFlags& operator |=( return first; } +using ResourceId = uint32_t; // 0xpptteeee + +using DataType = uint8_t; // Res_value::dataType +using DataValue = uint32_t; // Res_value::data + +struct OverlayManifestInfo { + std::string package_name; // NOLINT(misc-non-private-member-variables-in-classes) + std::string name; // NOLINT(misc-non-private-member-variables-in-classes) + std::string target_package; // NOLINT(misc-non-private-member-variables-in-classes) + std::string target_name; // NOLINT(misc-non-private-member-variables-in-classes) + ResourceId resource_mapping; // NOLINT(misc-non-private-member-variables-in-classes) +}; + +struct FabricatedOverlayEntryParameters { + std::string resource_name; + DataType data_type; + DataValue data_value; + std::string data_string_value; + std::optional<android::base::borrowed_fd> data_binary_value; + std::string configuration; +}; + class AssetManager2; /** @@ -1776,7 +1903,10 @@ public: void addMapping(uint8_t buildPackageId, uint8_t runtimePackageId); - void addAlias(uint32_t stagedId, uint32_t finalizedId); + using AliasMap = std::vector<std::pair<uint32_t, uint32_t>>; + void setAliases(AliasMap aliases) { + mAliasId = std::move(aliases); + } // Returns whether or not the value must be looked up. bool requiresLookup(const Res_value* value) const; @@ -1790,12 +1920,12 @@ public: return mEntries; } -private: - uint8_t mAssignedPackageId; - uint8_t mLookupTable[256]; - KeyedVector<String16, uint8_t> mEntries; - bool mAppAsLib; - std::map<uint32_t, uint32_t> mAliasId; + private: + uint8_t mLookupTable[256]; + uint8_t mAssignedPackageId; + bool mAppAsLib; + KeyedVector<String16, uint8_t> mEntries; + AliasMap mAliasId; }; bool U16StringToInt(const char16_t* s, size_t len, Res_value* outValue); diff --git a/libs/androidfw/include/androidfw/ResourceUtils.h b/libs/androidfw/include/androidfw/ResourceUtils.h index bd1c44033b88..2d90a526dfbe 100644 --- a/libs/androidfw/include/androidfw/ResourceUtils.h +++ b/libs/androidfw/include/androidfw/ResourceUtils.h @@ -25,14 +25,14 @@ namespace android { // Extracts the package, type, and name from a string of the format: [[package:]type/]name // Validation must be performed on each extracted piece. // Returns false if there was a syntax error. -bool ExtractResourceName(const StringPiece& str, StringPiece* out_package, StringPiece* out_type, +bool ExtractResourceName(StringPiece str, StringPiece* out_package, StringPiece* out_type, StringPiece* out_entry); // Convert a type_string_ref, entry_string_ref, and package to AssetManager2::ResourceName. // Useful for getting resource name without re-running AssetManager2::FindEntry searches. base::expected<AssetManager2::ResourceName, NullOrIOError> ToResourceName( const StringPoolRef& type_string_ref, const StringPoolRef& entry_string_ref, - const StringPiece& package_name); + StringPiece package_name); // Formats a ResourceName to "package:type/entry_name". std::string ToFormattedResourceString(const AssetManager2::ResourceName& resource_name); diff --git a/libs/androidfw/include/androidfw/Source.h b/libs/androidfw/include/androidfw/Source.h new file mode 100644 index 000000000000..ddc9ba421101 --- /dev/null +++ b/libs/androidfw/include/androidfw/Source.h @@ -0,0 +1,90 @@ +/* + * 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(android::StringPiece path) : path(path) { // NOLINT(implicit) + } + + inline Source(android::StringPiece path, android::StringPiece archive) + : path(path), archive(archive) { + } + + inline Source(android::StringPiece path, size_t line) : path(path), 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..f6cc64edfb5a 100644 --- a/libs/androidfw/include/androidfw/StringPiece.h +++ b/libs/androidfw/include/androidfw/StringPiece.h @@ -19,307 +19,37 @@ #include <ostream> #include <string> +#include <string_view> -#include "utils/JenkinsHash.h" #include "utils/Unicode.h" namespace android { -// Read only wrapper around basic C strings. Prevents excessive copying. -// StringPiece does not own the data it is wrapping. The lifetime of the underlying -// data must outlive this StringPiece. -// -// WARNING: When creating from std::basic_string<>, moving the original -// std::basic_string<> will invalidate the data held in a BasicStringPiece<>. -// BasicStringPiece<> should only be used transitively. -// -// NOTE: When creating an std::pair<StringPiece, T> using std::make_pair(), -// passing an std::string will first copy the string, then create a StringPiece -// on the copy, which is then immediately destroyed. -// Instead, create a StringPiece explicitly: -// -// std::string my_string = "foo"; -// std::make_pair<StringPiece, T>(StringPiece(my_string), ...); -template <typename TChar> -class BasicStringPiece { - public: - using const_iterator = const TChar*; - using difference_type = size_t; - using size_type = size_t; - - // End of string marker. - constexpr static const size_t npos = static_cast<size_t>(-1); - - BasicStringPiece(); - BasicStringPiece(const BasicStringPiece<TChar>& str); - BasicStringPiece(const std::basic_string<TChar>& str); // NOLINT(google-explicit-constructor) - BasicStringPiece(const TChar* str); // NOLINT(google-explicit-constructor) - BasicStringPiece(const TChar* str, size_t len); - - BasicStringPiece<TChar>& operator=(const BasicStringPiece<TChar>& rhs); - BasicStringPiece<TChar>& assign(const TChar* str, size_t len); - - BasicStringPiece<TChar> substr(size_t start, size_t len = npos) const; - BasicStringPiece<TChar> substr(BasicStringPiece<TChar>::const_iterator begin, - BasicStringPiece<TChar>::const_iterator end) const; - - const TChar* data() const; - size_t length() const; - size_t size() const; - bool empty() const; - std::basic_string<TChar> to_string() const; - - bool contains(const BasicStringPiece<TChar>& rhs) const; - int compare(const BasicStringPiece<TChar>& rhs) const; - bool operator<(const BasicStringPiece<TChar>& rhs) const; - bool operator>(const BasicStringPiece<TChar>& rhs) const; - bool operator==(const BasicStringPiece<TChar>& rhs) const; - bool operator!=(const BasicStringPiece<TChar>& rhs) const; - - const_iterator begin() const; - const_iterator end() const; - - private: - const TChar* data_; - size_t length_; -}; +template <class T> +using BasicStringPiece = std::basic_string_view<T>; using StringPiece = BasicStringPiece<char>; using StringPiece16 = BasicStringPiece<char16_t>; -// -// BasicStringPiece implementation. -// - -template <typename TChar> -constexpr const size_t BasicStringPiece<TChar>::npos; - -template <typename TChar> -inline BasicStringPiece<TChar>::BasicStringPiece() : data_(nullptr), length_(0) {} - -template <typename TChar> -inline BasicStringPiece<TChar>::BasicStringPiece(const BasicStringPiece<TChar>& str) - : data_(str.data_), length_(str.length_) {} - -template <typename TChar> -inline BasicStringPiece<TChar>::BasicStringPiece(const std::basic_string<TChar>& str) - : data_(str.data()), length_(str.length()) {} - -template <> -inline BasicStringPiece<char>::BasicStringPiece(const char* str) - : data_(str), length_(str != nullptr ? strlen(str) : 0) {} - -template <> -inline BasicStringPiece<char16_t>::BasicStringPiece(const char16_t* str) - : data_(str), length_(str != nullptr ? strlen16(str) : 0) {} - -template <typename TChar> -inline BasicStringPiece<TChar>::BasicStringPiece(const TChar* str, size_t len) - : data_(str), length_(len) {} - -template <typename TChar> -inline BasicStringPiece<TChar>& BasicStringPiece<TChar>::operator=( - const BasicStringPiece<TChar>& rhs) { - data_ = rhs.data_; - length_ = rhs.length_; - return *this; -} - -template <typename TChar> -inline BasicStringPiece<TChar>& BasicStringPiece<TChar>::assign(const TChar* str, size_t len) { - data_ = str; - length_ = len; - return *this; -} - -template <typename TChar> -inline BasicStringPiece<TChar> BasicStringPiece<TChar>::substr(size_t start, size_t len) const { - if (len == npos) { - len = length_ - start; - } - - if (start > length_ || start + len > length_) { - return BasicStringPiece<TChar>(); - } - return BasicStringPiece<TChar>(data_ + start, len); -} - -template <typename TChar> -inline BasicStringPiece<TChar> BasicStringPiece<TChar>::substr( - BasicStringPiece<TChar>::const_iterator begin, - BasicStringPiece<TChar>::const_iterator end) const { - return BasicStringPiece<TChar>(begin, end - begin); -} - -template <typename TChar> -inline const TChar* BasicStringPiece<TChar>::data() const { - return data_; -} - -template <typename TChar> -inline size_t BasicStringPiece<TChar>::length() const { - return length_; -} - -template <typename TChar> -inline size_t BasicStringPiece<TChar>::size() const { - return length_; -} - -template <typename TChar> -inline bool BasicStringPiece<TChar>::empty() const { - return length_ == 0; -} - -template <typename TChar> -inline std::basic_string<TChar> BasicStringPiece<TChar>::to_string() const { - return std::basic_string<TChar>(data_, length_); -} - -template <> -inline bool BasicStringPiece<char>::contains(const BasicStringPiece<char>& rhs) const { - if (!data_ || !rhs.data_) { - return false; - } - if (rhs.length_ > length_) { - return false; - } - return strstr(data_, rhs.data_) != nullptr; -} - -template <> -inline int BasicStringPiece<char>::compare(const BasicStringPiece<char>& rhs) const { - const char nullStr = '\0'; - const char* b1 = data_ != nullptr ? data_ : &nullStr; - const char* e1 = b1 + length_; - const char* b2 = rhs.data_ != nullptr ? rhs.data_ : &nullStr; - const char* e2 = b2 + rhs.length_; - - while (b1 < e1 && b2 < e2) { - const int d = static_cast<int>(*b1++) - static_cast<int>(*b2++); - if (d) { - return d; - } - } - return static_cast<int>(length_ - rhs.length_); -} - -inline ::std::ostream& operator<<(::std::ostream& out, const BasicStringPiece<char16_t>& str) { - const ssize_t result_len = utf16_to_utf8_length(str.data(), str.size()); - if (result_len < 0) { - // Empty string. - return out; - } - - std::string result; - result.resize(static_cast<size_t>(result_len)); - utf16_to_utf8(str.data(), str.length(), &*result.begin(), static_cast<size_t>(result_len) + 1); - return out << result; -} - -template <> -inline bool BasicStringPiece<char16_t>::contains(const BasicStringPiece<char16_t>& rhs) const { - if (!data_ || !rhs.data_) { - return false; - } - if (rhs.length_ > length_) { - return false; - } - return strstr16(data_, rhs.data_) != nullptr; -} - -template <> -inline int BasicStringPiece<char16_t>::compare(const BasicStringPiece<char16_t>& rhs) const { - const char16_t nullStr = u'\0'; - const char16_t* b1 = data_ != nullptr ? data_ : &nullStr; - const char16_t* b2 = rhs.data_ != nullptr ? rhs.data_ : &nullStr; - return strzcmp16(b1, length_, b2, rhs.length_); -} - -template <typename TChar> -inline bool BasicStringPiece<TChar>::operator<(const BasicStringPiece<TChar>& rhs) const { - return compare(rhs) < 0; -} - -template <typename TChar> -inline bool BasicStringPiece<TChar>::operator>(const BasicStringPiece<TChar>& rhs) const { - return compare(rhs) > 0; -} - -template <typename TChar> -inline bool BasicStringPiece<TChar>::operator==(const BasicStringPiece<TChar>& rhs) const { - return compare(rhs) == 0; -} - -template <typename TChar> -inline bool BasicStringPiece<TChar>::operator!=(const BasicStringPiece<TChar>& rhs) const { - return compare(rhs) != 0; -} - -template <typename TChar> -inline typename BasicStringPiece<TChar>::const_iterator BasicStringPiece<TChar>::begin() const { - return data_; -} - -template <typename TChar> -inline typename BasicStringPiece<TChar>::const_iterator BasicStringPiece<TChar>::end() const { - return data_ + length_; -} - -template <typename TChar> -inline bool operator==(const TChar* lhs, const BasicStringPiece<TChar>& rhs) { - return BasicStringPiece<TChar>(lhs) == rhs; -} - -template <typename TChar> -inline bool operator!=(const TChar* lhs, const BasicStringPiece<TChar>& rhs) { - return BasicStringPiece<TChar>(lhs) != rhs; -} - -inline ::std::ostream& operator<<(::std::ostream& out, const BasicStringPiece<char>& str) { - return out.write(str.data(), str.size()); -} - -template <typename TChar> -inline ::std::basic_string<TChar>& operator+=(::std::basic_string<TChar>& lhs, - const BasicStringPiece<TChar>& rhs) { - return lhs.append(rhs.data(), rhs.size()); -} - -template <typename TChar> -inline bool operator==(const ::std::basic_string<TChar>& lhs, const BasicStringPiece<TChar>& rhs) { - return rhs == lhs; -} - -template <typename TChar> -inline bool operator!=(const ::std::basic_string<TChar>& lhs, const BasicStringPiece<TChar>& rhs) { - return rhs != lhs; -} - } // namespace android -inline ::std::ostream& operator<<(::std::ostream& out, const std::u16string& str) { +namespace std { + +inline ::std::ostream& operator<<(::std::ostream& out, ::std::u16string_view str) { ssize_t utf8_len = utf16_to_utf8_length(str.data(), str.size()); if (utf8_len < 0) { - return out << "???"; + return out; // empty } std::string utf8; utf8.resize(static_cast<size_t>(utf8_len)); - utf16_to_utf8(str.data(), str.size(), &*utf8.begin(), utf8_len + 1); + utf16_to_utf8(str.data(), str.size(), utf8.data(), utf8_len + 1); return out << utf8; } -namespace std { - -template <typename TChar> -struct hash<android::BasicStringPiece<TChar>> { - size_t operator()(const android::BasicStringPiece<TChar>& str) const { - uint32_t hashCode = android::JenkinsHashMixBytes( - 0, reinterpret_cast<const uint8_t*>(str.data()), sizeof(TChar) * str.size()); - return static_cast<size_t>(hashCode); - } -}; +inline ::std::ostream& operator<<(::std::ostream& out, const ::std::u16string& str) { + return out << std::u16string_view(str); +} } // namespace std diff --git a/libs/androidfw/include/androidfw/StringPool.h b/libs/androidfw/include/androidfw/StringPool.h new file mode 100644 index 000000000000..0190ab57bf23 --- /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(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(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(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..a188abb7ecb5 100644 --- a/libs/androidfw/include/androidfw/Util.h +++ b/libs/androidfw/include/androidfw/Util.h @@ -17,20 +17,29 @@ #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) #else -#define ANDROID_LOG(x) std::stringstream() +namespace android { +// No default logging for aapt2, as it's too noisy for a command line dev tool. +struct NullLogger { + template <class T> + friend const NullLogger& operator<<(const NullLogger& l, const T&) { return l; } +}; +} +#define ANDROID_LOG(x) (android::NullLogger{}) #endif namespace android { @@ -46,95 +55,67 @@ std::unique_ptr<T> make_unique(Args&&... args) { return std::unique_ptr<T>(new T{std::forward<Args>(args)...}); } -// Based on std::unique_ptr, but uses free() to release malloc'ed memory -// without incurring the size increase of holding on to a custom deleter. -template <typename T> -class unique_cptr { - public: - using pointer = typename std::add_pointer<T>::type; - - constexpr unique_cptr() : ptr_(nullptr) {} - constexpr explicit unique_cptr(std::nullptr_t) : ptr_(nullptr) {} - explicit unique_cptr(pointer ptr) : ptr_(ptr) {} - unique_cptr(unique_cptr&& o) noexcept : ptr_(o.ptr_) { o.ptr_ = nullptr; } - - ~unique_cptr() { std::free(reinterpret_cast<void*>(ptr_)); } - - inline unique_cptr& operator=(unique_cptr&& o) noexcept { - if (&o == this) { - return *this; - } - - std::free(reinterpret_cast<void*>(ptr_)); - ptr_ = o.ptr_; - o.ptr_ = nullptr; - return *this; - } - - inline unique_cptr& operator=(std::nullptr_t) { - std::free(reinterpret_cast<void*>(ptr_)); - ptr_ = nullptr; - return *this; - } - - pointer release() { - pointer result = ptr_; - ptr_ = nullptr; - return result; - } - - inline pointer get() const { return ptr_; } - - void reset(pointer ptr = pointer()) { - if (ptr == ptr_) { - return; - } - - pointer old_ptr = ptr_; - ptr_ = ptr; - std::free(reinterpret_cast<void*>(old_ptr)); +// Based on std::unique_ptr, but uses free() to release malloc'ed memory. +struct FreeDeleter { + void operator()(void* ptr) const { + ::free(ptr); } +}; +template <typename T> +using unique_cptr = std::unique_ptr<T, FreeDeleter>; - inline void swap(unique_cptr& o) { std::swap(ptr_, o.ptr_); } - - inline explicit operator bool() const { return ptr_ != nullptr; } - - inline typename std::add_lvalue_reference<T>::type operator*() const { return *ptr_; } - - inline pointer operator->() const { return ptr_; } +void ReadUtf16StringFromDevice(const uint16_t* src, size_t len, std::string* out); - inline bool operator==(const unique_cptr& o) const { return ptr_ == o.ptr_; } +// Converts a UTF-8 string to a UTF-16 string. +std::u16string Utf8ToUtf16(StringPiece utf8); - inline bool operator!=(const unique_cptr& o) const { return ptr_ != o.ptr_; } +// Converts a UTF-16 string to a UTF-8 string. +std::string Utf16ToUtf8(StringPiece16 utf16); - inline bool operator==(std::nullptr_t) const { return ptr_ == nullptr; } +// Converts a UTF8 string into Modified UTF8 +std::string Utf8ToModifiedUtf8(std::string_view utf8); - inline bool operator!=(std::nullptr_t) const { return ptr_ != nullptr; } +// Converts a Modified UTF8 string into a UTF8 string +std::string ModifiedUtf8ToUtf8(std::string_view modified_utf8); - private: - DISALLOW_COPY_AND_ASSIGN(unique_cptr); +inline uint16_t HostToDevice16(uint16_t value) { + return htods(value); +} - pointer ptr_; -}; +inline uint32_t HostToDevice32(uint32_t value) { + return htodl(value); +} -void ReadUtf16StringFromDevice(const uint16_t* src, size_t len, std::string* out); +inline uint16_t DeviceToHost16(uint16_t value) { + return dtohs(value); +} -// Converts a UTF-8 string to a UTF-16 string. -std::u16string Utf8ToUtf16(const StringPiece& utf8); +inline uint32_t DeviceToHost32(uint32_t value) { + return dtohl(value); +} -// Converts a UTF-16 string to a UTF-8 string. -std::string Utf16ToUtf8(const StringPiece16& utf16); +std::vector<std::string> SplitAndLowercase(android::StringPiece str, char sep); -std::vector<std::string> SplitAndLowercase(const android::StringPiece& str, char sep); +inline bool IsFourByteAligned(const void* data) { + return ((uintptr_t)data & 0x3U) == 0; +} template <typename T> inline bool IsFourByteAligned(const incfs::map_ptr<T>& data) { - return ((size_t)data.unsafe_ptr() & 0x3U) == 0; + return IsFourByteAligned(data.unsafe_ptr()); } -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/include/androidfw/misc.h b/libs/androidfw/include/androidfw/misc.h index 5a5a0e29125d..d40d24ede769 100644 --- a/libs/androidfw/include/androidfw/misc.h +++ b/libs/androidfw/include/androidfw/misc.h @@ -44,6 +44,10 @@ FileType getFileType(const char* fileName); /* get the file's modification date; returns -1 w/errno set on failure */ time_t getFileModDate(const char* fileName); +// Check if |path| or |fd| resides on a readonly filesystem. +bool isReadonlyFilesystem(const char* path); +bool isReadonlyFilesystem(int fd); + }; // namespace android #endif // _LIBS_ANDROID_FW_MISC_H diff --git a/libs/androidfw/misc.cpp b/libs/androidfw/misc.cpp index 52854205207c..d3949e9cf69f 100644 --- a/libs/androidfw/misc.cpp +++ b/libs/androidfw/misc.cpp @@ -21,12 +21,17 @@ // #include <androidfw/misc.h> -#include <sys/stat.h> +#include "android-base/logging.h" + +#ifdef __linux__ +#include <sys/statvfs.h> +#include <sys/vfs.h> +#endif // __linux__ + #include <cstring> -#include <errno.h> #include <cstdio> - -using namespace android; +#include <errno.h> +#include <sys/stat.h> namespace android { @@ -41,8 +46,7 @@ FileType getFileType(const char* fileName) if (errno == ENOENT || errno == ENOTDIR) return kFileTypeNonexistent; else { - fprintf(stderr, "getFileType got errno=%d on '%s'\n", - errno, fileName); + PLOG(ERROR) << "getFileType(): stat(" << fileName << ") failed"; return kFileTypeUnknown; } } else { @@ -82,4 +86,32 @@ time_t getFileModDate(const char* fileName) return sb.st_mtime; } +#ifndef __linux__ +// No need to implement these on the host, the functions only matter on a device. +bool isReadonlyFilesystem(const char*) { + return false; +} +bool isReadonlyFilesystem(int) { + return false; +} +#else // __linux__ +bool isReadonlyFilesystem(const char* path) { + struct statfs sfs; + if (::statfs(path, &sfs)) { + PLOG(ERROR) << "isReadonlyFilesystem(): statfs(" << path << ") failed"; + return false; + } + return (sfs.f_flags & ST_RDONLY) != 0; +} + +bool isReadonlyFilesystem(int fd) { + struct statfs sfs; + if (::fstatfs(fd, &sfs)) { + PLOG(ERROR) << "isReadonlyFilesystem(): fstatfs(" << fd << ") failed"; + return false; + } + return (sfs.f_flags & ST_RDONLY) != 0; +} +#endif // __linux__ + }; // namespace android diff --git a/libs/androidfw/tests/ApkParsing_test.cpp b/libs/androidfw/tests/ApkParsing_test.cpp new file mode 100644 index 000000000000..62e88c619e5c --- /dev/null +++ b/libs/androidfw/tests/ApkParsing_test.cpp @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "androidfw/ApkParsing.h" + +#include "android-base/test_utils.h" + +#include "TestHelpers.h" + +using ::testing::Eq; +using ::testing::IsNull; +using ::testing::NotNull; + +namespace android { +TEST(ApkParsingTest, ValidArm64Path) { + const char* path = "lib/arm64-v8a/library.so"; + auto lastSlash = util::ValidLibraryPathLastSlash(path, false, false); + ASSERT_THAT(lastSlash, NotNull()); + ASSERT_THAT(lastSlash, Eq(path + 13)); +} + +TEST(ApkParsingTest, ValidArm64PathButSuppressed) { + const char* path = "lib/arm64-v8a/library.so"; + auto lastSlash = util::ValidLibraryPathLastSlash(path, true, false); + ASSERT_THAT(lastSlash, IsNull()); +} + +TEST(ApkParsingTest, ValidArm32Path) { + const char* path = "lib/armeabi-v7a/library.so"; + auto lastSlash = util::ValidLibraryPathLastSlash(path, false, false); + ASSERT_THAT(lastSlash, NotNull()); + ASSERT_THAT(lastSlash, Eq(path + 15)); +} + +TEST(ApkParsingTest, InvalidMustStartWithLib) { + const char* path = "lib/arm64-v8a/random.so"; + auto lastSlash = util::ValidLibraryPathLastSlash(path, false, false); + ASSERT_THAT(lastSlash, IsNull()); +} + +TEST(ApkParsingTest, InvalidMustEndInSo) { + const char* path = "lib/arm64-v8a/library.txt"; + auto lastSlash = util::ValidLibraryPathLastSlash(path, false, false); + ASSERT_THAT(lastSlash, IsNull()); +} + +TEST(ApkParsingTest, InvalidCharacter) { + const char* path = "lib/arm64-v8a/lib#.so"; + auto lastSlash = util::ValidLibraryPathLastSlash(path, false, false); + ASSERT_THAT(lastSlash, IsNull()); +} + +TEST(ApkParsingTest, InvalidSubdirectories) { + const char* path = "lib/arm64-v8a/anything/library.so"; + auto lastSlash = util::ValidLibraryPathLastSlash(path, false, false); + ASSERT_THAT(lastSlash, IsNull()); +} + +TEST(ApkParsingTest, InvalidFileAtRoot) { + const char* path = "lib/library.so"; + auto lastSlash = util::ValidLibraryPathLastSlash(path, false, false); + ASSERT_THAT(lastSlash, IsNull()); +} +}
\ No newline at end of file diff --git a/libs/androidfw/tests/AttributeResolution_bench.cpp b/libs/androidfw/tests/AttributeResolution_bench.cpp index ddd8ab820cb1..1c89c61c8f78 100644 --- a/libs/androidfw/tests/AttributeResolution_bench.cpp +++ b/libs/androidfw/tests/AttributeResolution_bench.cpp @@ -120,8 +120,8 @@ static void BM_ApplyStyleFramework(benchmark::State& state) { return; } - std::unique_ptr<Asset> asset = assetmanager.OpenNonAsset(layout_path->to_string(), value->cookie, - Asset::ACCESS_BUFFER); + std::unique_ptr<Asset> asset = + assetmanager.OpenNonAsset(std::string(*layout_path), value->cookie, Asset::ACCESS_BUFFER); if (asset == nullptr) { state.SkipWithError("failed to load layout"); return; 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/ByteBucketArray_test.cpp b/libs/androidfw/tests/ByteBucketArray_test.cpp index 5d464c7dc0f7..9c36cfb212c5 100644 --- a/libs/androidfw/tests/ByteBucketArray_test.cpp +++ b/libs/androidfw/tests/ByteBucketArray_test.cpp @@ -52,4 +52,57 @@ TEST(ByteBucketArrayTest, TestSparseInsertion) { } } +TEST(ByteBucketArrayTest, TestForEach) { + ByteBucketArray<int> bba; + ASSERT_TRUE(bba.set(0, 1)); + ASSERT_TRUE(bba.set(10, 2)); + ASSERT_TRUE(bba.set(26, 3)); + ASSERT_TRUE(bba.set(129, 4)); + ASSERT_TRUE(bba.set(234, 5)); + + int count = 0; + bba.forEachItem([&count](auto i, auto val) { + ++count; + switch (i) { + case 0: + EXPECT_EQ(1, val); + break; + case 10: + EXPECT_EQ(2, val); + break; + case 26: + EXPECT_EQ(3, val); + break; + case 129: + EXPECT_EQ(4, val); + break; + case 234: + EXPECT_EQ(5, val); + break; + default: + EXPECT_EQ(0, val); + break; + } + }); + ASSERT_EQ(4 * 16, count); +} + +TEST(ByteBucketArrayTest, TestTrimBuckets) { + ByteBucketArray<int> bba; + ASSERT_TRUE(bba.set(0, 1)); + ASSERT_TRUE(bba.set(255, 2)); + { + bba.trimBuckets([](auto val) { return val < 2; }); + int count = 0; + bba.forEachItem([&count](auto, auto) { ++count; }); + ASSERT_EQ(1 * 16, count); + } + { + bba.trimBuckets([](auto val) { return val < 3; }); + int count = 0; + bba.forEachItem([&count](auto, auto) { ++count; }); + ASSERT_EQ(0, count); + } +} + } // namespace android diff --git a/libs/androidfw/tests/ConfigDescription_test.cpp b/libs/androidfw/tests/ConfigDescription_test.cpp index ce7f8054e2ca..f5c01e5d9b68 100644 --- a/libs/androidfw/tests/ConfigDescription_test.cpp +++ b/libs/androidfw/tests/ConfigDescription_test.cpp @@ -25,8 +25,8 @@ namespace android { -static ::testing::AssertionResult TestParse( - const StringPiece& input, ConfigDescription* config = nullptr) { +static ::testing::AssertionResult TestParse(StringPiece input, + ConfigDescription* config = nullptr) { if (ConfigDescription::Parse(input, config)) { return ::testing::AssertionSuccess() << input << " was successfully parsed"; } @@ -138,7 +138,7 @@ TEST(ConfigDescriptionTest, ParseVrAttribute) { EXPECT_EQ(std::string("vrheadset-v26"), config.toString().string()); } -static inline ConfigDescription ParseConfigOrDie(const android::StringPiece& str) { +static inline ConfigDescription ParseConfigOrDie(android::StringPiece str) { ConfigDescription config; CHECK(ConfigDescription::Parse(str, &config)) << "invalid configuration: " << str; return config; @@ -154,4 +154,22 @@ TEST(ConfigDescriptionTest, RangeQualifiersDoNotConflict) { EXPECT_FALSE(ParseConfigOrDie("600x400").ConflictsWith(ParseConfigOrDie("300x200"))); } +TEST(ConfigDescriptionTest, TestGrammaticalGenderQualifier) { + ConfigDescription config; + EXPECT_TRUE(TestParse("feminine", &config)); + EXPECT_EQ(android::ResTable_config::GRAMMATICAL_GENDER_FEMININE, config.grammaticalInflection); + EXPECT_EQ(SDK_U, config.sdkVersion); + EXPECT_EQ(std::string("feminine-v34"), config.toString().string()); + + EXPECT_TRUE(TestParse("masculine", &config)); + EXPECT_EQ(android::ResTable_config::GRAMMATICAL_GENDER_MASCULINE, config.grammaticalInflection); + EXPECT_EQ(SDK_U, config.sdkVersion); + EXPECT_EQ(std::string("masculine-v34"), config.toString().string()); + + EXPECT_TRUE(TestParse("neuter", &config)); + EXPECT_EQ(android::ResTable_config::GRAMMATICAL_GENDER_NEUTER, config.grammaticalInflection); + EXPECT_EQ(SDK_U, config.sdkVersion); + EXPECT_EQ(std::string("neuter-v34"), config.toString().string()); +} + } // namespace android diff --git a/libs/androidfw/tests/Config_test.cpp b/libs/androidfw/tests/Config_test.cpp index 698c36f09301..5477621ce9fd 100644 --- a/libs/androidfw/tests/Config_test.cpp +++ b/libs/androidfw/tests/Config_test.cpp @@ -205,4 +205,18 @@ TEST(ConfigTest, ScreenIsHdr) { EXPECT_EQ(defaultConfig.diff(hdrConfig), ResTable_config::CONFIG_COLOR_MODE); } +TEST(ConfigTest, GrammaticalGender) { + ResTable_config defaultConfig = {}; + ResTable_config masculine = {}; + masculine.grammaticalInflection = ResTable_config::GRAMMATICAL_GENDER_MASCULINE; + + EXPECT_EQ(defaultConfig.diff(masculine), ResTable_config::CONFIG_GRAMMATICAL_GENDER); + + ResTable_config feminine = {}; + feminine.grammaticalInflection = ResTable_config::GRAMMATICAL_GENDER_FEMININE; + + EXPECT_EQ(defaultConfig.diff(feminine), ResTable_config::CONFIG_GRAMMATICAL_GENDER); + EXPECT_EQ(masculine.diff(feminine), ResTable_config::CONFIG_GRAMMATICAL_GENDER); +} + } // namespace android. diff --git a/libs/androidfw/tests/CursorWindow_test.cpp b/libs/androidfw/tests/CursorWindow_test.cpp index 15be80c48192..d1cfd03276c2 100644 --- a/libs/androidfw/tests/CursorWindow_test.cpp +++ b/libs/androidfw/tests/CursorWindow_test.cpp @@ -14,6 +14,7 @@ * limitations under the License. */ +#include <memory> #include <utility> #include "androidfw/CursorWindow.h" @@ -184,7 +185,7 @@ TEST(CursorWindowTest, Inflate) { ASSERT_EQ(w->allocRow(), OK); // Scratch buffer that will fit before inflation - void* buf = malloc(kHalfInlineSize); + char buf[kHalfInlineSize]; // Store simple value ASSERT_EQ(w->putLong(0, 0, 0xcafe), OK); @@ -262,7 +263,7 @@ TEST(CursorWindowTest, ParcelSmall) { ASSERT_EQ(w->allocRow(), OK); // Scratch buffer that will fit before inflation - void* buf = malloc(kHalfInlineSize); + char buf[kHalfInlineSize]; // Store simple value ASSERT_EQ(w->putLong(0, 0, 0xcafe), OK); @@ -322,7 +323,8 @@ TEST(CursorWindowTest, ParcelLarge) { ASSERT_EQ(w->putLong(0, 0, 0xcafe), OK); // Store object that forces inflation - void* buf = malloc(kGiantSize); + std::unique_ptr<char> bufPtr(new char[kGiantSize]); + void* buf = bufPtr.get(); memset(buf, 42, kGiantSize); ASSERT_EQ(w->putBlob(0, 1, buf, kGiantSize), OK); diff --git a/libs/androidfw/tests/LoadedArsc_test.cpp b/libs/androidfw/tests/LoadedArsc_test.cpp index d214e2dfef7b..c90ec197b5ef 100644 --- a/libs/androidfw/tests/LoadedArsc_test.cpp +++ b/libs/androidfw/tests/LoadedArsc_test.cpp @@ -71,62 +71,6 @@ TEST(LoadedArscTest, LoadSinglePackageArsc) { ASSERT_TRUE(LoadedPackage::GetEntry(type.type, entry_index).has_value()); } -TEST(LoadedArscTest, LoadSparseEntryApp) { - std::string contents; - ASSERT_TRUE(ReadFileFromZipToString(GetTestDataPath() + "/sparse/sparse.apk", "resources.arsc", - &contents)); - - std::unique_ptr<const LoadedArsc> loaded_arsc = LoadedArsc::Load(contents.data(), - contents.length()); - ASSERT_THAT(loaded_arsc, NotNull()); - - const LoadedPackage* package = - loaded_arsc->GetPackageById(get_package_id(sparse::R::integer::foo_9)); - ASSERT_THAT(package, NotNull()); - - const uint8_t type_index = get_type_id(sparse::R::integer::foo_9) - 1; - const uint16_t entry_index = get_entry_id(sparse::R::integer::foo_9); - - const TypeSpec* type_spec = package->GetTypeSpecByTypeIndex(type_index); - ASSERT_THAT(type_spec, NotNull()); - ASSERT_THAT(type_spec->type_entries.size(), Ge(1u)); - - auto type = type_spec->type_entries[0]; - ASSERT_TRUE(LoadedPackage::GetEntry(type.type, entry_index).has_value()); -} - -TEST(LoadedArscTest, FindSparseEntryApp) { - std::string contents; - ASSERT_TRUE(ReadFileFromZipToString(GetTestDataPath() + "/sparse/sparse.apk", "resources.arsc", - &contents)); - - std::unique_ptr<const LoadedArsc> loaded_arsc = LoadedArsc::Load(contents.data(), - contents.length()); - ASSERT_THAT(loaded_arsc, NotNull()); - - const LoadedPackage* package = - loaded_arsc->GetPackageById(get_package_id(sparse::R::string::only_v26)); - ASSERT_THAT(package, NotNull()); - - const uint8_t type_index = get_type_id(sparse::R::string::only_v26) - 1; - const uint16_t entry_index = get_entry_id(sparse::R::string::only_v26); - - const TypeSpec* type_spec = package->GetTypeSpecByTypeIndex(type_index); - ASSERT_THAT(type_spec, NotNull()); - ASSERT_THAT(type_spec->type_entries.size(), Ge(1u)); - - // Ensure that AAPT2 sparsely encoded the v26 config as expected. - auto type_entry = std::find_if( - type_spec->type_entries.begin(), type_spec->type_entries.end(), - [](const TypeSpec::TypeEntry& x) { return x.config.sdkVersion == 26; }); - ASSERT_NE(type_entry, type_spec->type_entries.end()); - ASSERT_NE(type_entry->type->flags & ResTable_type::FLAG_SPARSE, 0); - - // Test fetching a resource with only sparsely encoded configs by name. - auto id = package->FindEntryByName(u"string", u"only_v26"); - ASSERT_EQ(id.value(), fix_package_id(sparse::R::string::only_v26, 0)); -} - TEST(LoadedArscTest, LoadSharedLibrary) { std::string contents; ASSERT_TRUE(ReadFileFromZipToString(GetTestDataPath() + "/lib_one/lib_one.apk", "resources.arsc", @@ -404,4 +348,84 @@ TEST(LoadedArscTest, LoadCustomLoader) { // sizeof(Res_value) might not be backwards compatible. // TEST(LoadedArscTest, LoadingShouldBeForwardsAndBackwardsCompatible) { ASSERT_TRUE(false); } +class LoadedArscParameterizedTest : + public testing::TestWithParam<std::string> { +}; + +TEST_P(LoadedArscParameterizedTest, LoadSparseEntryApp) { + std::string contents; + ASSERT_TRUE(ReadFileFromZipToString(GetParam(), "resources.arsc", &contents)); + + std::unique_ptr<const LoadedArsc> loaded_arsc = LoadedArsc::Load(contents.data(), + contents.length()); + ASSERT_THAT(loaded_arsc, NotNull()); + + const LoadedPackage* package = + loaded_arsc->GetPackageById(get_package_id(sparse::R::integer::foo_9)); + ASSERT_THAT(package, NotNull()); + + const uint8_t type_index = get_type_id(sparse::R::integer::foo_9) - 1; + const uint16_t entry_index = get_entry_id(sparse::R::integer::foo_9); + + const TypeSpec* type_spec = package->GetTypeSpecByTypeIndex(type_index); + ASSERT_THAT(type_spec, NotNull()); + ASSERT_THAT(type_spec->type_entries.size(), Ge(1u)); + + auto type = type_spec->type_entries[0]; + ASSERT_TRUE(LoadedPackage::GetEntry(type.type, entry_index).has_value()); +} + +TEST_P(LoadedArscParameterizedTest, FindSparseEntryApp) { + std::string contents; + ASSERT_TRUE(ReadFileFromZipToString(GetParam(), "resources.arsc", &contents)); + + std::unique_ptr<const LoadedArsc> loaded_arsc = LoadedArsc::Load(contents.data(), + contents.length()); + ASSERT_THAT(loaded_arsc, NotNull()); + + const LoadedPackage* package = + loaded_arsc->GetPackageById(get_package_id(sparse::R::string::only_land)); + ASSERT_THAT(package, NotNull()); + + const uint8_t type_index = get_type_id(sparse::R::string::only_land) - 1; + + const TypeSpec* type_spec = package->GetTypeSpecByTypeIndex(type_index); + ASSERT_THAT(type_spec, NotNull()); + ASSERT_THAT(type_spec->type_entries.size(), Ge(1u)); + + // Type Entry with default orientation is not sparse encoded because the ratio of + // populated entries to total entries is above threshold. + // Only find out default locale because Soong build system will introduce pseudo + // locales for the apk generated at runtime. + auto type_entry_default = std::find_if( + type_spec->type_entries.begin(), type_spec->type_entries.end(), + [] (const TypeSpec::TypeEntry& x) { return x.config.orientation == 0 && + x.config.locale == 0; }); + ASSERT_NE(type_entry_default, type_spec->type_entries.end()); + ASSERT_EQ(type_entry_default->type->flags & ResTable_type::FLAG_SPARSE, 0); + + // Type Entry with land orientation is sparse encoded as expected. + // Only find out default locale because Soong build system will introduce pseudo + // locales for the apk generated at runtime. + auto type_entry_land = std::find_if( + type_spec->type_entries.begin(), type_spec->type_entries.end(), + [](const TypeSpec::TypeEntry& x) { return x.config.orientation == + ResTable_config::ORIENTATION_LAND && + x.config.locale == 0; }); + ASSERT_NE(type_entry_land, type_spec->type_entries.end()); + ASSERT_NE(type_entry_land->type->flags & ResTable_type::FLAG_SPARSE, 0); + + // Test fetching a resource with only sparsely encoded configs by name. + auto id = package->FindEntryByName(u"string", u"only_land"); + ASSERT_EQ(id.value(), fix_package_id(sparse::R::string::only_land, 0)); +} + +INSTANTIATE_TEST_SUITE_P( + FrameWorkResourcesLoadedArscTests, + LoadedArscParameterizedTest, + ::testing::Values( + base::GetExecutableDirectory() + "/tests/data/sparse/sparse.apk", + base::GetExecutableDirectory() + "/FrameworkResourcesSparseTestApp.apk" + )); + } // namespace android diff --git a/libs/androidfw/tests/PosixUtils_test.cpp b/libs/androidfw/tests/PosixUtils_test.cpp index 8c49350796ec..097e6b0bba65 100644 --- a/libs/androidfw/tests/PosixUtils_test.cpp +++ b/libs/androidfw/tests/PosixUtils_test.cpp @@ -28,27 +28,27 @@ namespace util { TEST(PosixUtilsTest, AbsolutePathToBinary) { const auto result = ExecuteBinary({"/bin/date", "--help"}); - ASSERT_THAT(result, NotNull()); - ASSERT_EQ(result->status, 0); - ASSERT_GE(result->stdout_str.find("usage: date "), 0); + ASSERT_TRUE((bool)result); + ASSERT_EQ(result.status, 0); + ASSERT_GE(result.stdout_str.find("usage: date "), 0); } TEST(PosixUtilsTest, RelativePathToBinary) { const auto result = ExecuteBinary({"date", "--help"}); - ASSERT_THAT(result, NotNull()); - ASSERT_EQ(result->status, 0); - ASSERT_GE(result->stdout_str.find("usage: date "), 0); + ASSERT_TRUE((bool)result); + ASSERT_EQ(result.status, 0); + ASSERT_GE(result.stdout_str.find("usage: date "), 0); } TEST(PosixUtilsTest, BadParameters) { const auto result = ExecuteBinary({"/bin/date", "--this-parameter-is-not-supported"}); - ASSERT_THAT(result, NotNull()); - ASSERT_NE(result->status, 0); + ASSERT_TRUE((bool)result); + ASSERT_GT(result.status, 0); } TEST(PosixUtilsTest, NoSuchBinary) { const auto result = ExecuteBinary({"/this/binary/does/not/exist"}); - ASSERT_THAT(result, IsNull()); + ASSERT_FALSE((bool)result); } } // android diff --git a/libs/androidfw/tests/ResTable_test.cpp b/libs/androidfw/tests/ResTable_test.cpp index 9aeb00c47e63..fbf70981f2de 100644 --- a/libs/androidfw/tests/ResTable_test.cpp +++ b/libs/androidfw/tests/ResTable_test.cpp @@ -15,6 +15,7 @@ */ #include "androidfw/ResourceTypes.h" +#include "android-base/file.h" #include <codecvt> #include <locale> @@ -41,34 +42,6 @@ TEST(ResTableTest, ShouldLoadSuccessfully) { ASSERT_EQ(NO_ERROR, table.add(contents.data(), contents.size())); } -TEST(ResTableTest, ShouldLoadSparseEntriesSuccessfully) { - std::string contents; - ASSERT_TRUE(ReadFileFromZipToString(GetTestDataPath() + "/sparse/sparse.apk", "resources.arsc", - &contents)); - - ResTable table; - ASSERT_EQ(NO_ERROR, table.add(contents.data(), contents.size())); - - ResTable_config config; - memset(&config, 0, sizeof(config)); - config.sdkVersion = 26; - table.setParameters(&config); - - String16 name(u"com.android.sparse:integer/foo_9"); - uint32_t flags; - uint32_t resid = - table.identifierForName(name.string(), name.size(), nullptr, 0, nullptr, 0, &flags); - ASSERT_NE(0u, resid); - - Res_value val; - ResTable_config selected_config; - ASSERT_GE( - table.getResource(resid, &val, false /*mayBeBag*/, 0u /*density*/, &flags, &selected_config), - 0); - EXPECT_EQ(Res_value::TYPE_INT_DEC, val.dataType); - EXPECT_EQ(900u, val.data); -} - TEST(ResTableTest, SimpleTypeIsRetrievedCorrectly) { std::string contents; ASSERT_TRUE(ReadFileFromZipToString(GetTestDataPath() + "/basic/basic.apk", @@ -476,4 +449,43 @@ TEST(ResTableTest, TruncatedEncodeLength) { ASSERT_FALSE(invalid_pool->stringAt(invalid_val.data).has_value()); } +class ResTableParameterizedTest : + public testing::TestWithParam<std::string> { +}; + +TEST_P(ResTableParameterizedTest, ShouldLoadSparseEntriesSuccessfully) { + std::string contents; + ASSERT_TRUE(ReadFileFromZipToString(GetParam(), "resources.arsc", &contents)); + + ResTable table; + ASSERT_EQ(NO_ERROR, table.add(contents.data(), contents.size())); + + ResTable_config config; + memset(&config, 0, sizeof(config)); + config.orientation = ResTable_config::ORIENTATION_LAND; + table.setParameters(&config); + + String16 name(u"com.android.sparse:integer/foo_9"); + uint32_t flags; + uint32_t resid = + table.identifierForName(name.string(), name.size(), nullptr, 0, nullptr, 0, &flags); + ASSERT_NE(0u, resid); + + Res_value val; + ResTable_config selected_config; + ASSERT_GE( + table.getResource(resid, &val, false /*mayBeBag*/, 0u /*density*/, &flags, &selected_config), + 0); + EXPECT_EQ(Res_value::TYPE_INT_DEC, val.dataType); + EXPECT_EQ(900u, val.data); +} + +INSTANTIATE_TEST_SUITE_P( + FrameWorkResourcesResTableTests, + ResTableParameterizedTest, + ::testing::Values( + base::GetExecutableDirectory() + "/tests/data/sparse/sparse.apk", + base::GetExecutableDirectory() + "/FrameworkResourcesSparseTestApp.apk" + )); + } // namespace android diff --git a/libs/androidfw/tests/ResourceTimer_test.cpp b/libs/androidfw/tests/ResourceTimer_test.cpp new file mode 100644 index 000000000000..4a1e9735de7a --- /dev/null +++ b/libs/androidfw/tests/ResourceTimer_test.cpp @@ -0,0 +1,245 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +#include <android-base/file.h> +#include <android-base/test_utils.h> +#include <androidfw/Util.h> + +#include "TestHelpers.h" + +#include <androidfw/ResourceTimer.h> + +namespace android { + +namespace { + +// Create a reading in us. This is a convenience function to avoid multiplying by 1000 +// everywhere. +unsigned int US(int us) { + return us * 1000; +} + +} + +TEST(ResourceTimerTest, TimerBasic) { + ResourceTimer::Timer timer; + ASSERT_THAT(timer.count, 0); + ASSERT_THAT(timer.total, 0); + + for (int i = 1; i <= 100; i++) { + timer.record(US(i)); + } + ASSERT_THAT(timer.count, 100); + ASSERT_THAT(timer.total, US((101 * 100)/2)); + ASSERT_THAT(timer.mintime, US(1)); + ASSERT_THAT(timer.maxtime, US(100)); + ASSERT_THAT(timer.pvalues.p50.floor, 0); + ASSERT_THAT(timer.pvalues.p50.nominal, 0); + ASSERT_THAT(timer.largest[0], US(100)); + ASSERT_THAT(timer.largest[1], US(99)); + ASSERT_THAT(timer.largest[2], US(98)); + ASSERT_THAT(timer.largest[3], US(97)); + ASSERT_THAT(timer.largest[4], US(96)); + timer.compute(); + ASSERT_THAT(timer.pvalues.p50.floor, US(49)); + ASSERT_THAT(timer.pvalues.p50.nominal, US(50)); + ASSERT_THAT(timer.pvalues.p90.floor, US(89)); + ASSERT_THAT(timer.pvalues.p90.nominal, US(90)); + ASSERT_THAT(timer.pvalues.p95.floor, US(94)); + ASSERT_THAT(timer.pvalues.p95.nominal, US(95)); + ASSERT_THAT(timer.pvalues.p99.floor, US(98)); + ASSERT_THAT(timer.pvalues.p99.nominal, US(99)); + + // Test reset functionality. All values should be zero after the reset. Computing pvalues + // after the result should also yield zeros. + timer.reset(); + ASSERT_THAT(timer.count, 0); + ASSERT_THAT(timer.total, 0); + ASSERT_THAT(timer.mintime, US(0)); + ASSERT_THAT(timer.maxtime, US(0)); + ASSERT_THAT(timer.pvalues.p50.floor, US(0)); + ASSERT_THAT(timer.pvalues.p50.nominal, US(0)); + ASSERT_THAT(timer.largest[0], US(0)); + ASSERT_THAT(timer.largest[1], US(0)); + ASSERT_THAT(timer.largest[2], US(0)); + ASSERT_THAT(timer.largest[3], US(0)); + ASSERT_THAT(timer.largest[4], US(0)); + timer.compute(); + ASSERT_THAT(timer.pvalues.p50.floor, US(0)); + ASSERT_THAT(timer.pvalues.p50.nominal, US(0)); + ASSERT_THAT(timer.pvalues.p90.floor, US(0)); + ASSERT_THAT(timer.pvalues.p90.nominal, US(0)); + ASSERT_THAT(timer.pvalues.p95.floor, US(0)); + ASSERT_THAT(timer.pvalues.p95.nominal, US(0)); + ASSERT_THAT(timer.pvalues.p99.floor, US(0)); + ASSERT_THAT(timer.pvalues.p99.nominal, US(0)); + + // Test again, adding elements in reverse. + for (int i = 100; i >= 1; i--) { + timer.record(US(i)); + } + ASSERT_THAT(timer.count, 100); + ASSERT_THAT(timer.total, US((101 * 100)/2)); + ASSERT_THAT(timer.mintime, US(1)); + ASSERT_THAT(timer.maxtime, US(100)); + ASSERT_THAT(timer.pvalues.p50.floor, 0); + ASSERT_THAT(timer.pvalues.p50.nominal, 0); + timer.compute(); + ASSERT_THAT(timer.pvalues.p50.floor, US(49)); + ASSERT_THAT(timer.pvalues.p50.nominal, US(50)); + ASSERT_THAT(timer.pvalues.p90.floor, US(89)); + ASSERT_THAT(timer.pvalues.p90.nominal, US(90)); + ASSERT_THAT(timer.pvalues.p95.floor, US(94)); + ASSERT_THAT(timer.pvalues.p95.nominal, US(95)); + ASSERT_THAT(timer.pvalues.p99.floor, US(98)); + ASSERT_THAT(timer.pvalues.p99.nominal, US(99)); + ASSERT_THAT(timer.largest[0], US(100)); + ASSERT_THAT(timer.largest[1], US(99)); + ASSERT_THAT(timer.largest[2], US(98)); + ASSERT_THAT(timer.largest[3], US(97)); + ASSERT_THAT(timer.largest[4], US(96)); +} + +TEST(ResourceTimerTest, TimerLimit) { + ResourceTimer::Timer timer; + + // Event truncation means that a time of 1050us will be stored in the 1000us + // bucket. Since there is a single event, all p-values lie in the same range. + timer.record(US(1050)); + timer.compute(); + ASSERT_THAT(timer.pvalues.p50.floor, US(900)); + ASSERT_THAT(timer.pvalues.p50.nominal, US(1000)); + ASSERT_THAT(timer.pvalues.p90.floor, US(900)); + ASSERT_THAT(timer.pvalues.p90.nominal, US(1000)); + ASSERT_THAT(timer.pvalues.p95.floor, US(900)); + ASSERT_THAT(timer.pvalues.p95.nominal, US(1000)); + ASSERT_THAT(timer.pvalues.p99.floor, US(900)); + ASSERT_THAT(timer.pvalues.p99.nominal, US(1000)); +} + +TEST(ResourceTimerTest, TimerCopy) { + ResourceTimer::Timer source; + for (int i = 1; i <= 100; i++) { + source.record(US(i)); + } + ResourceTimer::Timer timer; + ResourceTimer::Timer::copy(timer, source, true); + ASSERT_THAT(source.count, 0); + ASSERT_THAT(source.total, 0); + // compute() is not normally be called on a reset timer, but it should work and it should return + // all zeros. + source.compute(); + ASSERT_THAT(source.pvalues.p50.floor, US(0)); + ASSERT_THAT(source.pvalues.p50.nominal, US(0)); + ASSERT_THAT(source.pvalues.p90.floor, US(0)); + ASSERT_THAT(source.pvalues.p90.nominal, US(0)); + ASSERT_THAT(source.pvalues.p95.floor, US(0)); + ASSERT_THAT(source.pvalues.p95.nominal, US(0)); + ASSERT_THAT(source.pvalues.p99.floor, US(0)); + ASSERT_THAT(source.pvalues.p99.nominal, US(0)); + ASSERT_THAT(source.largest[0], US(0)); + ASSERT_THAT(source.largest[1], US(0)); + ASSERT_THAT(source.largest[2], US(0)); + ASSERT_THAT(source.largest[3], US(0)); + ASSERT_THAT(source.largest[4], US(0)); + + timer.compute(); + ASSERT_THAT(timer.pvalues.p50.floor, US(49)); + ASSERT_THAT(timer.pvalues.p50.nominal, US(50)); + ASSERT_THAT(timer.pvalues.p90.floor, US(89)); + ASSERT_THAT(timer.pvalues.p90.nominal, US(90)); + ASSERT_THAT(timer.pvalues.p95.floor, US(94)); + ASSERT_THAT(timer.pvalues.p95.nominal, US(95)); + ASSERT_THAT(timer.pvalues.p99.floor, US(98)); + ASSERT_THAT(timer.pvalues.p99.nominal, US(99)); + ASSERT_THAT(timer.largest[0], US(100)); + ASSERT_THAT(timer.largest[1], US(99)); + ASSERT_THAT(timer.largest[2], US(98)); + ASSERT_THAT(timer.largest[3], US(97)); + ASSERT_THAT(timer.largest[4], US(96)); + + // Call compute a second time. The values must be the same. + timer.compute(); + ASSERT_THAT(timer.pvalues.p50.floor, US(49)); + ASSERT_THAT(timer.pvalues.p50.nominal, US(50)); + ASSERT_THAT(timer.pvalues.p90.floor, US(89)); + ASSERT_THAT(timer.pvalues.p90.nominal, US(90)); + ASSERT_THAT(timer.pvalues.p95.floor, US(94)); + ASSERT_THAT(timer.pvalues.p95.nominal, US(95)); + ASSERT_THAT(timer.pvalues.p99.floor, US(98)); + ASSERT_THAT(timer.pvalues.p99.nominal, US(99)); + ASSERT_THAT(timer.largest[0], US(100)); + ASSERT_THAT(timer.largest[1], US(99)); + ASSERT_THAT(timer.largest[2], US(98)); + ASSERT_THAT(timer.largest[3], US(97)); + ASSERT_THAT(timer.largest[4], US(96)); + + // Modify the source. If timer and source share histogram arrays, this will introduce an + // error. + for (int i = 1; i <= 100; i++) { + source.record(US(i)); + } + // Call compute a third time. The values must be the same. + timer.compute(); + ASSERT_THAT(timer.pvalues.p50.floor, US(49)); + ASSERT_THAT(timer.pvalues.p50.nominal, US(50)); + ASSERT_THAT(timer.pvalues.p90.floor, US(89)); + ASSERT_THAT(timer.pvalues.p90.nominal, US(90)); + ASSERT_THAT(timer.pvalues.p95.floor, US(94)); + ASSERT_THAT(timer.pvalues.p95.nominal, US(95)); + ASSERT_THAT(timer.pvalues.p99.floor, US(98)); + ASSERT_THAT(timer.pvalues.p99.nominal, US(99)); + ASSERT_THAT(timer.largest[0], US(100)); + ASSERT_THAT(timer.largest[1], US(99)); + ASSERT_THAT(timer.largest[2], US(98)); + ASSERT_THAT(timer.largest[3], US(97)); + ASSERT_THAT(timer.largest[4], US(96)); +} + +// Verify that if too many oversize entries are reported, the percentile values cannot be computed +// and are set to zero. +TEST(ResourceTimerTest, TimerOversize) { + static const int oversize = US(2 * 1000 * 1000); + + ResourceTimer::Timer timer; + for (int i = 1; i <= 100; i++) { + timer.record(US(i)); + } + + // Insert enough oversize values to invalidate the p90, p95, and p99 percentiles. The p50 is + // still computable. + for (int i = 1; i <= 50; i++) { + timer.record(oversize); + } + ASSERT_THAT(timer.largest[0], oversize); + ASSERT_THAT(timer.largest[1], oversize); + ASSERT_THAT(timer.largest[2], oversize); + ASSERT_THAT(timer.largest[3], oversize); + ASSERT_THAT(timer.largest[4], oversize); + timer.compute(); + ASSERT_THAT(timer.pvalues.p50.floor, US(74)); + ASSERT_THAT(timer.pvalues.p50.nominal, US(75)); + ASSERT_THAT(timer.pvalues.p90.floor, 0); + ASSERT_THAT(timer.pvalues.p90.nominal, 0); + ASSERT_THAT(timer.pvalues.p95.floor, 0); + ASSERT_THAT(timer.pvalues.p95.nominal, 0); + ASSERT_THAT(timer.pvalues.p99.floor, 0); + ASSERT_THAT(timer.pvalues.p99.nominal, 0); +} + + +} // namespace android diff --git a/libs/androidfw/tests/SparseEntry_bench.cpp b/libs/androidfw/tests/SparseEntry_bench.cpp index c9b4ad8af278..fffeeb802873 100644 --- a/libs/androidfw/tests/SparseEntry_bench.cpp +++ b/libs/androidfw/tests/SparseEntry_bench.cpp @@ -16,6 +16,7 @@ #include "androidfw/AssetManager.h" #include "androidfw/ResourceTypes.h" +#include "android-base/file.h" #include "BenchmarkHelpers.h" #include "data/sparse/R.h" @@ -24,40 +25,74 @@ namespace sparse = com::android::sparse; namespace android { +static void BM_SparseEntryGetResourceHelper(const std::vector<std::string>& paths, + uint32_t resid, benchmark::State& state, void (*GetResourceBenchmarkFunc)( + const std::vector<std::string>&, const ResTable_config*, + uint32_t, benchmark::State&)){ + ResTable_config config; + memset(&config, 0, sizeof(config)); + config.orientation = ResTable_config::ORIENTATION_LAND; + GetResourceBenchmarkFunc(paths, &config, resid, state); +} + static void BM_SparseEntryGetResourceOldSparse(benchmark::State& state, uint32_t resid) { - ResTable_config config; - memset(&config, 0, sizeof(config)); - config.sdkVersion = 26; - GetResourceBenchmarkOld({GetTestDataPath() + "/sparse/sparse.apk"}, &config, resid, state); + BM_SparseEntryGetResourceHelper({GetTestDataPath() + "/sparse/sparse.apk"}, resid, + state, &GetResourceBenchmarkOld); } BENCHMARK_CAPTURE(BM_SparseEntryGetResourceOldSparse, Small, sparse::R::integer::foo_9); BENCHMARK_CAPTURE(BM_SparseEntryGetResourceOldSparse, Large, sparse::R::string::foo_999); static void BM_SparseEntryGetResourceOldNotSparse(benchmark::State& state, uint32_t resid) { - ResTable_config config; - memset(&config, 0, sizeof(config)); - config.sdkVersion = 26; - GetResourceBenchmarkOld({GetTestDataPath() + "/sparse/not_sparse.apk"}, &config, resid, state); + BM_SparseEntryGetResourceHelper({GetTestDataPath() + "/sparse/not_sparse.apk"}, resid, + state, &GetResourceBenchmarkOld); } BENCHMARK_CAPTURE(BM_SparseEntryGetResourceOldNotSparse, Small, sparse::R::integer::foo_9); BENCHMARK_CAPTURE(BM_SparseEntryGetResourceOldNotSparse, Large, sparse::R::string::foo_999); static void BM_SparseEntryGetResourceSparse(benchmark::State& state, uint32_t resid) { - ResTable_config config; - memset(&config, 0, sizeof(config)); - config.sdkVersion = 26; - GetResourceBenchmark({GetTestDataPath() + "/sparse/sparse.apk"}, &config, resid, state); + BM_SparseEntryGetResourceHelper({GetTestDataPath() + "/sparse/sparse.apk"}, resid, + state, &GetResourceBenchmark); } BENCHMARK_CAPTURE(BM_SparseEntryGetResourceSparse, Small, sparse::R::integer::foo_9); BENCHMARK_CAPTURE(BM_SparseEntryGetResourceSparse, Large, sparse::R::string::foo_999); static void BM_SparseEntryGetResourceNotSparse(benchmark::State& state, uint32_t resid) { - ResTable_config config; - memset(&config, 0, sizeof(config)); - config.sdkVersion = 26; - GetResourceBenchmark({GetTestDataPath() + "/sparse/not_sparse.apk"}, &config, resid, state); + BM_SparseEntryGetResourceHelper({GetTestDataPath() + "/sparse/not_sparse.apk"}, resid, + state, &GetResourceBenchmark); } BENCHMARK_CAPTURE(BM_SparseEntryGetResourceNotSparse, Small, sparse::R::integer::foo_9); BENCHMARK_CAPTURE(BM_SparseEntryGetResourceNotSparse, Large, sparse::R::string::foo_999); +static void BM_SparseEntryGetResourceOldSparseRuntime(benchmark::State& state, uint32_t resid) { + BM_SparseEntryGetResourceHelper({base::GetExecutableDirectory() + + "/FrameworkResourcesSparseTestApp.apk"}, resid, state, + &GetResourceBenchmarkOld); +} +BENCHMARK_CAPTURE(BM_SparseEntryGetResourceOldSparseRuntime, Small, sparse::R::integer::foo_9); +BENCHMARK_CAPTURE(BM_SparseEntryGetResourceOldSparseRuntime, Large, sparse::R::string::foo_999); + +static void BM_SparseEntryGetResourceOldNotSparseRuntime(benchmark::State& state, uint32_t resid) { + BM_SparseEntryGetResourceHelper({base::GetExecutableDirectory() + + "/FrameworkResourcesNotSparseTestApp.apk"}, resid, state, + &GetResourceBenchmarkOld); +} +BENCHMARK_CAPTURE(BM_SparseEntryGetResourceOldNotSparseRuntime, Small, sparse::R::integer::foo_9); +BENCHMARK_CAPTURE(BM_SparseEntryGetResourceOldNotSparseRuntime, Large, sparse::R::string::foo_999); + +static void BM_SparseEntryGetResourceSparseRuntime(benchmark::State& state, uint32_t resid) { + BM_SparseEntryGetResourceHelper({base::GetExecutableDirectory() + + "/FrameworkResourcesSparseTestApp.apk"}, resid, state, + &GetResourceBenchmark); +} +BENCHMARK_CAPTURE(BM_SparseEntryGetResourceSparseRuntime, Small, sparse::R::integer::foo_9); +BENCHMARK_CAPTURE(BM_SparseEntryGetResourceSparseRuntime, Large, sparse::R::string::foo_999); + +static void BM_SparseEntryGetResourceNotSparseRuntime(benchmark::State& state, uint32_t resid) { + BM_SparseEntryGetResourceHelper({base::GetExecutableDirectory() + + "/FrameworkResourcesNotSparseTestApp.apk"}, resid, state, + &GetResourceBenchmark); +} +BENCHMARK_CAPTURE(BM_SparseEntryGetResourceNotSparseRuntime, Small, sparse::R::integer::foo_9); +BENCHMARK_CAPTURE(BM_SparseEntryGetResourceNotSparseRuntime, Large, sparse::R::string::foo_999); + } // namespace android diff --git a/libs/androidfw/tests/StringPiece_test.cpp b/libs/androidfw/tests/StringPiece_test.cpp index 316a5c1bf40e..822e527253df 100644 --- a/libs/androidfw/tests/StringPiece_test.cpp +++ b/libs/androidfw/tests/StringPiece_test.cpp @@ -60,36 +60,4 @@ TEST(StringPieceTest, PiecesHaveCorrectSortOrderUtf8) { EXPECT_TRUE(StringPiece(car) > banana); } -TEST(StringPieceTest, ContainsOtherStringPiece) { - StringPiece text("I am a leaf on the wind."); - StringPiece start_needle("I am"); - StringPiece end_needle("wind."); - StringPiece middle_needle("leaf"); - StringPiece empty_needle(""); - StringPiece missing_needle("soar"); - StringPiece long_needle("This string is longer than the text."); - - EXPECT_TRUE(text.contains(start_needle)); - EXPECT_TRUE(text.contains(end_needle)); - EXPECT_TRUE(text.contains(middle_needle)); - EXPECT_TRUE(text.contains(empty_needle)); - EXPECT_FALSE(text.contains(missing_needle)); - EXPECT_FALSE(text.contains(long_needle)); - - StringPiece16 text16(u"I am a leaf on the wind."); - StringPiece16 start_needle16(u"I am"); - StringPiece16 end_needle16(u"wind."); - StringPiece16 middle_needle16(u"leaf"); - StringPiece16 empty_needle16(u""); - StringPiece16 missing_needle16(u"soar"); - StringPiece16 long_needle16(u"This string is longer than the text."); - - EXPECT_TRUE(text16.contains(start_needle16)); - EXPECT_TRUE(text16.contains(end_needle16)); - EXPECT_TRUE(text16.contains(middle_needle16)); - EXPECT_TRUE(text16.contains(empty_needle16)); - EXPECT_FALSE(text16.contains(missing_needle16)); - EXPECT_FALSE(text16.contains(long_needle16)); -} - } // namespace android diff --git a/libs/androidfw/tests/StringPool_test.cpp b/libs/androidfw/tests/StringPool_test.cpp new file mode 100644 index 000000000000..0e0acae165d9 --- /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, Eq("\xED\xA0\x81\xED\xB0\x80")); + + str = test.string8At(1); + ASSERT_TRUE(str.has_value()); + EXPECT_THAT(*str, Eq("foo \xED\xA0\x81\xED\xB0\xB7 bar")); + + str = test.string8At(2); + ASSERT_TRUE(str.has_value()); + EXPECT_THAT(*str, 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/TypeWrappers_test.cpp b/libs/androidfw/tests/TypeWrappers_test.cpp index d69abe5d0f11..ed30904ec179 100644 --- a/libs/androidfw/tests/TypeWrappers_test.cpp +++ b/libs/androidfw/tests/TypeWrappers_test.cpp @@ -14,6 +14,7 @@ * limitations under the License. */ +#include <algorithm> #include <androidfw/ResourceTypes.h> #include <androidfw/TypeWrappers.h> #include <utils/String8.h> @@ -22,88 +23,123 @@ namespace android { -void* createTypeData() { - ResTable_type t; - memset(&t, 0, sizeof(t)); +// create a ResTable_type in memory with a vector of Res_value* +static ResTable_type* createTypeTable(std::vector<Res_value*>& values, + bool compact_entry = false, + bool short_offsets = false) +{ + ResTable_type t{}; t.header.type = RES_TABLE_TYPE_TYPE; t.header.headerSize = sizeof(t); + t.header.size = sizeof(t); t.id = 1; - t.entryCount = 3; - - uint32_t offsets[3]; - t.entriesStart = t.header.headerSize + sizeof(offsets); - t.header.size = t.entriesStart; - - offsets[0] = 0; - ResTable_entry e1; - memset(&e1, 0, sizeof(e1)); - e1.size = sizeof(e1); - e1.key.index = 0; - t.header.size += sizeof(e1); - - Res_value v1; - memset(&v1, 0, sizeof(v1)); - t.header.size += sizeof(v1); - - offsets[1] = ResTable_type::NO_ENTRY; - - offsets[2] = sizeof(e1) + sizeof(v1); - ResTable_entry e2; - memset(&e2, 0, sizeof(e2)); - e2.size = sizeof(e2); - e2.key.index = 1; - t.header.size += sizeof(e2); - - Res_value v2; - memset(&v2, 0, sizeof(v2)); - t.header.size += sizeof(v2); - - uint8_t* data = (uint8_t*)malloc(t.header.size); - uint8_t* p = data; - memcpy(p, &t, sizeof(t)); - p += sizeof(t); - memcpy(p, offsets, sizeof(offsets)); - p += sizeof(offsets); - memcpy(p, &e1, sizeof(e1)); - p += sizeof(e1); - memcpy(p, &v1, sizeof(v1)); - p += sizeof(v1); - memcpy(p, &e2, sizeof(e2)); - p += sizeof(e2); - memcpy(p, &v2, sizeof(v2)); - p += sizeof(v2); - return data; + t.flags = short_offsets ? ResTable_type::FLAG_OFFSET16 : 0; + + t.header.size += values.size() * (short_offsets ? sizeof(uint16_t) : sizeof(uint32_t)); + t.entriesStart = t.header.size; + t.entryCount = values.size(); + + size_t entry_size = compact_entry ? sizeof(ResTable_entry) + : sizeof(ResTable_entry) + sizeof(Res_value); + for (auto const v : values) { + t.header.size += v ? entry_size : 0; + } + + uint8_t* data = (uint8_t *)malloc(t.header.size); + uint8_t* p_header = data; + uint8_t* p_offsets = data + t.header.headerSize; + uint8_t* p_entries = data + t.entriesStart; + + memcpy(p_header, &t, sizeof(t)); + + size_t i = 0, entry_offset = 0; + uint32_t k = 0; + for (auto const& v : values) { + if (short_offsets) { + uint16_t *p = reinterpret_cast<uint16_t *>(p_offsets) + i; + *p = v ? (entry_offset >> 2) & 0xffffu : 0xffffu; + } else { + uint32_t *p = reinterpret_cast<uint32_t *>(p_offsets) + i; + *p = v ? entry_offset : ResTable_type::NO_ENTRY; + } + + if (v) { + ResTable_entry entry{}; + if (compact_entry) { + entry.compact.key = i; + entry.compact.flags = ResTable_entry::FLAG_COMPACT | (v->dataType << 8); + entry.compact.data = v->data; + memcpy(p_entries, &entry, sizeof(entry)); p_entries += sizeof(entry); + entry_offset += sizeof(entry); + } else { + Res_value value{}; + entry.full.size = sizeof(entry); + entry.full.key.index = i; + value = *v; + memcpy(p_entries, &entry, sizeof(entry)); p_entries += sizeof(entry); + memcpy(p_entries, &value, sizeof(value)); p_entries += sizeof(value); + entry_offset += sizeof(entry) + sizeof(value); + } + } + i++; + } + return reinterpret_cast<ResTable_type*>(data); } TEST(TypeVariantIteratorTest, shouldIterateOverTypeWithoutErrors) { - ResTable_type* data = (ResTable_type*) createTypeData(); + std::vector<Res_value *> values; - TypeVariant v(data); + Res_value *v1 = new Res_value{}; + values.push_back(v1); - TypeVariant::iterator iter = v.beginEntries(); - ASSERT_EQ(uint32_t(0), iter.index()); - ASSERT_TRUE(NULL != *iter); - ASSERT_EQ(uint32_t(0), iter->key.index); - ASSERT_NE(v.endEntries(), iter); + values.push_back(nullptr); - iter++; + Res_value *v2 = new Res_value{}; + values.push_back(v2); - ASSERT_EQ(uint32_t(1), iter.index()); - ASSERT_TRUE(NULL == *iter); - ASSERT_NE(v.endEntries(), iter); + Res_value *v3 = new Res_value{ sizeof(Res_value), 0, Res_value::TYPE_STRING, 0x12345678}; + values.push_back(v3); - iter++; + // test for combinations of compact_entry and short_offsets + for (size_t i = 0; i < 4; i++) { + bool compact_entry = i & 0x1, short_offsets = i & 0x2; + ResTable_type* data = createTypeTable(values, compact_entry, short_offsets); + TypeVariant v(data); - ASSERT_EQ(uint32_t(2), iter.index()); - ASSERT_TRUE(NULL != *iter); - ASSERT_EQ(uint32_t(1), iter->key.index); - ASSERT_NE(v.endEntries(), iter); + TypeVariant::iterator iter = v.beginEntries(); + ASSERT_EQ(uint32_t(0), iter.index()); + ASSERT_TRUE(NULL != *iter); + ASSERT_EQ(uint32_t(0), iter->key()); + ASSERT_NE(v.endEntries(), iter); - iter++; + iter++; - ASSERT_EQ(v.endEntries(), iter); + ASSERT_EQ(uint32_t(1), iter.index()); + ASSERT_TRUE(NULL == *iter); + ASSERT_NE(v.endEntries(), iter); - free(data); + iter++; + + ASSERT_EQ(uint32_t(2), iter.index()); + ASSERT_TRUE(NULL != *iter); + ASSERT_EQ(uint32_t(2), iter->key()); + ASSERT_NE(v.endEntries(), iter); + + iter++; + + ASSERT_EQ(uint32_t(3), iter.index()); + ASSERT_TRUE(NULL != *iter); + ASSERT_EQ(iter->is_compact(), compact_entry); + ASSERT_EQ(uint32_t(3), iter->key()); + ASSERT_EQ(uint32_t(0x12345678), iter->value().data); + ASSERT_EQ(Res_value::TYPE_STRING, iter->value().dataType); + + iter++; + + ASSERT_EQ(v.endEntries(), iter); + + free(data); + } } } // 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/androidfw/tests/data/sparse/Android.bp b/libs/androidfw/tests/data/sparse/Android.bp new file mode 100644 index 000000000000..b0da375c7971 --- /dev/null +++ b/libs/androidfw/tests/data/sparse/Android.bp @@ -0,0 +1,23 @@ +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_libs_androidfw_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_libs_androidfw_license"], +} + +android_test_helper_app { + name: "FrameworkResourcesSparseTestApp", + sdk_version: "current", + min_sdk_version: "32", + aaptflags: [ + "--enable-sparse-encoding", + ], +} + +android_test_helper_app { + name: "FrameworkResourcesNotSparseTestApp", + sdk_version: "current", + min_sdk_version: "32", +} diff --git a/libs/androidfw/tests/data/sparse/AndroidManifest.xml b/libs/androidfw/tests/data/sparse/AndroidManifest.xml index 27911b62447a..9c23a7227631 100644 --- a/libs/androidfw/tests/data/sparse/AndroidManifest.xml +++ b/libs/androidfw/tests/data/sparse/AndroidManifest.xml @@ -17,4 +17,5 @@ <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.android.sparse"> <application /> + <uses-sdk android:minSdkVersion="32" /> </manifest> diff --git a/libs/androidfw/tests/data/sparse/R.h b/libs/androidfw/tests/data/sparse/R.h index 2492dbf33f4a..a66e1af150c4 100644 --- a/libs/androidfw/tests/data/sparse/R.h +++ b/libs/androidfw/tests/data/sparse/R.h @@ -42,7 +42,7 @@ struct R { struct string { enum : uint32_t { foo_999 = 0x7f0203e7, - only_v26 = 0x7f0203e8 + only_land = 0x7f0203e8 }; }; }; diff --git a/libs/androidfw/tests/data/sparse/gen_strings.sh b/libs/androidfw/tests/data/sparse/gen_strings.sh index 4ea5468c7df9..114ecbb7d860 100755 --- a/libs/androidfw/tests/data/sparse/gen_strings.sh +++ b/libs/androidfw/tests/data/sparse/gen_strings.sh @@ -1,20 +1,20 @@ #!/bin/bash OUTPUT_default=res/values/strings.xml -OUTPUT_v26=res/values-v26/strings.xml +OUTPUT_land=res/values-land/strings.xml echo "<resources>" > $OUTPUT_default -echo "<resources>" > $OUTPUT_v26 +echo "<resources>" > $OUTPUT_land for i in {0..999} do echo " <string name=\"foo_$i\">$i</string>" >> $OUTPUT_default if [ "$(($i % 3))" -eq "0" ] then - echo " <string name=\"foo_$i\">$(($i * 10))</string>" >> $OUTPUT_v26 + echo " <string name=\"foo_$i\">$(($i * 10))</string>" >> $OUTPUT_land fi done echo "</resources>" >> $OUTPUT_default -echo " <string name=\"only_v26\">only v26</string>" >> $OUTPUT_v26 -echo "</resources>" >> $OUTPUT_v26 +echo " <string name=\"only_land\">only land</string>" >> $OUTPUT_land +echo "</resources>" >> $OUTPUT_land diff --git a/libs/androidfw/tests/data/sparse/not_sparse.apk b/libs/androidfw/tests/data/sparse/not_sparse.apk Binary files differindex b08a621195c0..4d4d4a849033 100644 --- a/libs/androidfw/tests/data/sparse/not_sparse.apk +++ b/libs/androidfw/tests/data/sparse/not_sparse.apk diff --git a/libs/androidfw/tests/data/sparse/res/values-v26/strings.xml b/libs/androidfw/tests/data/sparse/res/values-land/strings.xml index d116087ec3c0..66222c327416 100644 --- a/libs/androidfw/tests/data/sparse/res/values-v26/strings.xml +++ b/libs/androidfw/tests/data/sparse/res/values-land/strings.xml @@ -333,5 +333,5 @@ <string name="foo_993">9930</string> <string name="foo_996">9960</string> <string name="foo_999">9990</string> - <string name="only_v26">only v26</string> + <string name="only_land">only land</string> </resources> diff --git a/libs/androidfw/tests/data/sparse/res/values-v26/values.xml b/libs/androidfw/tests/data/sparse/res/values-land/values.xml index b396ad24aa8c..b396ad24aa8c 100644 --- a/libs/androidfw/tests/data/sparse/res/values-v26/values.xml +++ b/libs/androidfw/tests/data/sparse/res/values-land/values.xml diff --git a/libs/androidfw/tests/data/sparse/sparse.apk b/libs/androidfw/tests/data/sparse/sparse.apk Binary files differindex 9fd01fbf2941..0f2d75a62b96 100644 --- a/libs/androidfw/tests/data/sparse/sparse.apk +++ b/libs/androidfw/tests/data/sparse/sparse.apk diff --git a/libs/dream/OWNERS b/libs/dream/OWNERS new file mode 100644 index 000000000000..a4b0127d23b1 --- /dev/null +++ b/libs/dream/OWNERS @@ -0,0 +1 @@ +include /core/java/android/service/dreams/OWNERS
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/Android.bp b/libs/dream/lowlight/Android.bp index ea606df1536d..e4d2e022cd76 100644 --- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/Android.bp +++ b/libs/dream/lowlight/Android.bp @@ -1,4 +1,4 @@ -// 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. @@ -21,15 +21,33 @@ package { default_applicable_licenses: ["frameworks_base_license"], } -android_test { - name: "WMShellFlickerTestApp", - srcs: ["**/*.java"], - sdk_version: "current", - test_suites: ["device-tests"], +filegroup { + name: "low_light_dream_lib-sources", + srcs: [ + "src/**/*.java", + "src/**/*.kt", + ], + path: "src", } -java_library { - name: "wmshell-flicker-test-components", - srcs: ["src/**/Components.java"], - sdk_version: "test_current", +android_library { + name: "LowLightDreamLib", + srcs: [ + ":low_light_dream_lib-sources", + ], + resource_dirs: [ + "res", + ], + libs: [ + "kotlin-annotations", + ], + static_libs: [ + "androidx.arch.core_core-runtime", + "dagger2", + "jsr330", + "kotlinx-coroutines-android", + "kotlinx-coroutines-core", + ], + 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..78fefbf41141 --- /dev/null +++ b/libs/dream/lowlight/res/values/config.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. + --> +<resources> + <!-- The dream component used when the device is low light environment. --> + <string translatable="false" name="config_lowLightDreamComponent"/> + <!-- The max number of milliseconds to wait for the low light transition before setting + the system dream component --> + <integer name="config_lowLightTransitionTimeoutMs">2000</integer> +</resources> diff --git a/libs/dream/lowlight/src/com/android/dream/lowlight/LowLightDreamManager.kt b/libs/dream/lowlight/src/com/android/dream/lowlight/LowLightDreamManager.kt new file mode 100644 index 000000000000..96bfb78eff0d --- /dev/null +++ b/libs/dream/lowlight/src/com/android/dream/lowlight/LowLightDreamManager.kt @@ -0,0 +1,138 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.dream.lowlight + +import android.Manifest +import android.annotation.IntDef +import android.annotation.RequiresPermission +import android.app.DreamManager +import android.content.ComponentName +import android.util.Log +import com.android.dream.lowlight.dagger.LowLightDreamModule +import com.android.dream.lowlight.dagger.qualifiers.Application +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.TimeoutCancellationException +import kotlinx.coroutines.launch +import javax.inject.Inject +import javax.inject.Named +import kotlin.time.DurationUnit +import kotlin.time.toDuration + +/** + * 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 + */ +class LowLightDreamManager @Inject constructor( + @Application private val coroutineScope: CoroutineScope, + private val dreamManager: DreamManager, + private val lowLightTransitionCoordinator: LowLightTransitionCoordinator, + @param:Named(LowLightDreamModule.LOW_LIGHT_DREAM_COMPONENT) + private val lowLightDreamComponent: ComponentName?, + @param:Named(LowLightDreamModule.LOW_LIGHT_TRANSITION_TIMEOUT_MS) + private val lowLightTransitionTimeoutMs: Long +) { + /** + * @hide + */ + @Retention(AnnotationRetention.SOURCE) + @IntDef( + prefix = ["AMBIENT_LIGHT_MODE_"], + value = [ + AMBIENT_LIGHT_MODE_UNKNOWN, + AMBIENT_LIGHT_MODE_REGULAR, + AMBIENT_LIGHT_MODE_LOW_LIGHT + ] + ) + annotation class AmbientLightMode + + private var mTransitionJob: Job? = null + private var mAmbientLightMode = AMBIENT_LIGHT_MODE_UNKNOWN + private val mLowLightTransitionTimeout = + lowLightTransitionTimeoutMs.toDuration(DurationUnit.MILLISECONDS) + + /** + * Sets the current ambient light mode. + * + * @hide + */ + @RequiresPermission(Manifest.permission.WRITE_DREAM_STATE) + fun setAmbientLightMode(@AmbientLightMode ambientLightMode: Int) { + if (lowLightDreamComponent == 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 + val shouldEnterLowLight = mAmbientLightMode == AMBIENT_LIGHT_MODE_LOW_LIGHT + + // Cancel any previous transitions + mTransitionJob?.cancel() + mTransitionJob = coroutineScope.launch { + try { + lowLightTransitionCoordinator.waitForLowLightTransitionAnimation( + timeout = mLowLightTransitionTimeout, + entering = shouldEnterLowLight + ) + } catch (ex: TimeoutCancellationException) { + Log.e(TAG, "timed out while waiting for low light animation", ex) + } + dreamManager.setSystemDreamComponent( + if (shouldEnterLowLight) lowLightDreamComponent else null + ) + } + } + + companion object { + private const val TAG = "LowLightDreamManager" + private val DEBUG = Log.isLoggable(TAG, Log.DEBUG) + + /** + * Constant for ambient light mode being unknown. + * + * @hide + */ + const val AMBIENT_LIGHT_MODE_UNKNOWN = 0 + + /** + * Constant for ambient light mode being regular / bright. + * + * @hide + */ + const val AMBIENT_LIGHT_MODE_REGULAR = 1 + + /** + * Constant for ambient light mode being low light / dim. + * + * @hide + */ + const val AMBIENT_LIGHT_MODE_LOW_LIGHT = 2 + } +} diff --git a/libs/dream/lowlight/src/com/android/dream/lowlight/LowLightTransitionCoordinator.kt b/libs/dream/lowlight/src/com/android/dream/lowlight/LowLightTransitionCoordinator.kt new file mode 100644 index 000000000000..26efb55fa560 --- /dev/null +++ b/libs/dream/lowlight/src/com/android/dream/lowlight/LowLightTransitionCoordinator.kt @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.dream.lowlight + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import com.android.dream.lowlight.util.suspendCoroutineWithTimeout +import javax.inject.Inject +import javax.inject.Singleton +import kotlin.coroutines.resume +import kotlin.time.Duration + +/** + * Helper class that allows listening and running animations before entering or exiting low light. + */ +@Singleton +class LowLightTransitionCoordinator @Inject constructor() { + /** + * Listener that is notified before low light entry. + */ + interface LowLightEnterListener { + /** + * Callback that is notified before the device enters low light. + * + * @return an optional animator that will be waited upon before entering low light. + */ + fun onBeforeEnterLowLight(): Animator? + } + + /** + * Listener that is notified before low light exit. + */ + interface LowLightExitListener { + /** + * Callback that is notified before the device exits low light. + * + * @return an optional animator that will be waited upon before exiting low light. + */ + fun onBeforeExitLowLight(): Animator? + } + + private var mLowLightEnterListener: LowLightEnterListener? = null + private var mLowLightExitListener: LowLightExitListener? = null + + /** + * Sets the listener for the low light enter event. + * + * Only one listener can be set at a time. This method will overwrite any previously set + * listener. Null can be used to unset the listener. + */ + fun setLowLightEnterListener(lowLightEnterListener: LowLightEnterListener?) { + mLowLightEnterListener = lowLightEnterListener + } + + /** + * Sets the listener for the low light exit event. + * + * Only one listener can be set at a time. This method will overwrite any previously set + * listener. Null can be used to unset the listener. + */ + fun setLowLightExitListener(lowLightExitListener: LowLightExitListener?) { + mLowLightExitListener = lowLightExitListener + } + + /** + * Notifies listeners that the device is about to enter or exit low light, and waits for the + * animation to complete. If this function is cancelled, the animation is also cancelled. + * + * @param timeout the maximum duration to wait for the transition animation. If the animation + * does not complete within this time period, a + * @param entering true if listeners should be notified before entering low light, false if this + * is notifying before exiting. + */ + suspend fun waitForLowLightTransitionAnimation(timeout: Duration, entering: Boolean) = + suspendCoroutineWithTimeout(timeout) { continuation -> + var animator: Animator? = null + if (entering && mLowLightEnterListener != null) { + animator = mLowLightEnterListener!!.onBeforeEnterLowLight() + } else if (!entering && mLowLightExitListener != null) { + animator = mLowLightExitListener!!.onBeforeExitLowLight() + } + + if (animator == null) { + continuation.resume(Unit) + return@suspendCoroutineWithTimeout + } + + // If the listener returned an animator to indicate it was running an animation, run the + // callback after the animation completes, otherwise call the callback directly. + val listener = object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animator: Animator) { + continuation.resume(Unit) + } + + override fun onAnimationCancel(animation: Animator) { + continuation.cancel() + } + } + animator.addListener(listener) + continuation.invokeOnCancellation { + animator.removeListener(listener) + animator.cancel() + } + } +} diff --git a/libs/dream/lowlight/src/com/android/dream/lowlight/dagger/LowLightDreamModule.kt b/libs/dream/lowlight/src/com/android/dream/lowlight/dagger/LowLightDreamModule.kt new file mode 100644 index 000000000000..dd274bd9d509 --- /dev/null +++ b/libs/dream/lowlight/src/com/android/dream/lowlight/dagger/LowLightDreamModule.kt @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.dream.lowlight.dagger + +import android.app.DreamManager +import android.content.ComponentName +import android.content.Context +import com.android.dream.lowlight.R +import com.android.dream.lowlight.dagger.qualifiers.Application +import com.android.dream.lowlight.dagger.qualifiers.Main +import dagger.Module +import dagger.Provides +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import javax.inject.Named + +/** + * Dagger module for low light dream. + * + * @hide + */ +@Module +object LowLightDreamModule { + /** + * Provides dream manager. + */ + @Provides + fun providesDreamManager(context: Context): DreamManager { + return requireNotNull(context.getSystemService(DreamManager::class.java)) + } + + /** + * Provides the component name of the low light dream, or null if not configured. + */ + @Provides + @Named(LOW_LIGHT_DREAM_COMPONENT) + fun providesLowLightDreamComponent(context: Context): ComponentName? { + val lowLightDreamComponent = context.resources.getString( + R.string.config_lowLightDreamComponent + ) + return if (lowLightDreamComponent.isEmpty()) { + null + } else { + ComponentName.unflattenFromString(lowLightDreamComponent) + } + } + + @Provides + @Named(LOW_LIGHT_TRANSITION_TIMEOUT_MS) + fun providesLowLightTransitionTimeout(context: Context): Long { + return context.resources.getInteger(R.integer.config_lowLightTransitionTimeoutMs).toLong() + } + + @Provides + @Main + fun providesMainDispatcher(): CoroutineDispatcher { + return Dispatchers.Main.immediate + } + + @Provides + @Application + fun providesApplicationScope(@Main dispatcher: CoroutineDispatcher): CoroutineScope { + return CoroutineScope(dispatcher) + } + + const val LOW_LIGHT_DREAM_COMPONENT = "low_light_dream_component" + const val LOW_LIGHT_TRANSITION_TIMEOUT_MS = "low_light_transition_timeout" +} diff --git a/libs/dream/lowlight/src/com/android/dream/lowlight/dagger/qualifiers/Application.kt b/libs/dream/lowlight/src/com/android/dream/lowlight/dagger/qualifiers/Application.kt new file mode 100644 index 000000000000..541fe4017e6e --- /dev/null +++ b/libs/dream/lowlight/src/com/android/dream/lowlight/dagger/qualifiers/Application.kt @@ -0,0 +1,9 @@ +package com.android.dream.lowlight.dagger.qualifiers + +import android.content.Context +import javax.inject.Qualifier + +/** + * Used to qualify a context as [Context.getApplicationContext] + */ +@Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class Application diff --git a/libs/dream/lowlight/src/com/android/dream/lowlight/dagger/qualifiers/Main.kt b/libs/dream/lowlight/src/com/android/dream/lowlight/dagger/qualifiers/Main.kt new file mode 100644 index 000000000000..ccd0710bdc60 --- /dev/null +++ b/libs/dream/lowlight/src/com/android/dream/lowlight/dagger/qualifiers/Main.kt @@ -0,0 +1,8 @@ +package com.android.dream.lowlight.dagger.qualifiers + +import javax.inject.Qualifier + +/** + * Used to qualify code running on the main thread. + */ +@Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class Main diff --git a/libs/dream/lowlight/src/com/android/dream/lowlight/util/KotlinUtils.kt b/libs/dream/lowlight/src/com/android/dream/lowlight/util/KotlinUtils.kt new file mode 100644 index 000000000000..ff675ccfaffb --- /dev/null +++ b/libs/dream/lowlight/src/com/android/dream/lowlight/util/KotlinUtils.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dream.lowlight.util + +import kotlinx.coroutines.CancellableContinuation +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.withTimeout +import kotlin.time.Duration + +suspend inline fun <T> suspendCoroutineWithTimeout( + timeout: Duration, + crossinline block: (CancellableContinuation<T>) -> Unit +) = withTimeout(timeout) { + suspendCancellableCoroutine(block = block) +} diff --git a/libs/dream/lowlight/tests/Android.bp b/libs/dream/lowlight/tests/Android.bp new file mode 100644 index 000000000000..2d79090cd7d4 --- /dev/null +++ b/libs/dream/lowlight/tests/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 { + default_applicable_licenses: ["frameworks_base_license"], +} + +android_test { + name: "LowLightDreamTests", + srcs: [ + "**/*.java", + "**/*.kt", + ], + static_libs: [ + "LowLightDreamLib", + "androidx.test.runner", + "androidx.test.rules", + "androidx.test.ext.junit", + "frameworks-base-testutils", + "junit", + "kotlinx_coroutines_test", + "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.kt b/libs/dream/lowlight/tests/src/com.android.dream.lowlight/LowLightDreamManagerTest.kt new file mode 100644 index 000000000000..2a886bc31788 --- /dev/null +++ b/libs/dream/lowlight/tests/src/com.android.dream.lowlight/LowLightDreamManagerTest.kt @@ -0,0 +1,169 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.dream.lowlight + +import android.animation.Animator +import android.app.DreamManager +import android.content.ComponentName +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceTimeBy +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.clearInvocations +import org.mockito.Mockito.never +import org.mockito.Mockito.times +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations +import src.com.android.dream.lowlight.utils.any +import src.com.android.dream.lowlight.utils.withArgCaptor + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(AndroidTestingRunner::class) +class LowLightDreamManagerTest { + @Mock + private lateinit var mDreamManager: DreamManager + @Mock + private lateinit var mEnterAnimator: Animator + @Mock + private lateinit var mExitAnimator: Animator + + private lateinit var mTransitionCoordinator: LowLightTransitionCoordinator + private lateinit var mLowLightDreamManager: LowLightDreamManager + private lateinit var testScope: TestScope + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + testScope = TestScope(StandardTestDispatcher()) + + mTransitionCoordinator = LowLightTransitionCoordinator() + mTransitionCoordinator.setLowLightEnterListener( + object : LowLightTransitionCoordinator.LowLightEnterListener { + override fun onBeforeEnterLowLight() = mEnterAnimator + }) + mTransitionCoordinator.setLowLightExitListener( + object : LowLightTransitionCoordinator.LowLightExitListener { + override fun onBeforeExitLowLight() = mExitAnimator + }) + + mLowLightDreamManager = LowLightDreamManager( + coroutineScope = testScope, + dreamManager = mDreamManager, + lowLightTransitionCoordinator = mTransitionCoordinator, + lowLightDreamComponent = DREAM_COMPONENT, + lowLightTransitionTimeoutMs = LOW_LIGHT_TIMEOUT_MS + ) + } + + @Test + fun setAmbientLightMode_lowLight_setSystemDream() = testScope.runTest { + mLowLightDreamManager.setAmbientLightMode(LowLightDreamManager.AMBIENT_LIGHT_MODE_LOW_LIGHT) + runCurrent() + verify(mDreamManager, never()).setSystemDreamComponent(DREAM_COMPONENT) + completeEnterAnimations() + runCurrent() + verify(mDreamManager).setSystemDreamComponent(DREAM_COMPONENT) + } + + @Test + fun setAmbientLightMode_regularLight_clearSystemDream() = testScope.runTest { + mLowLightDreamManager.setAmbientLightMode(LowLightDreamManager.AMBIENT_LIGHT_MODE_REGULAR) + runCurrent() + verify(mDreamManager, never()).setSystemDreamComponent(null) + completeExitAnimations() + runCurrent() + verify(mDreamManager).setSystemDreamComponent(null) + } + + @Test + fun setAmbientLightMode_defaultUnknownMode_clearSystemDream() = testScope.runTest { + // Set to low light first. + mLowLightDreamManager.setAmbientLightMode(LowLightDreamManager.AMBIENT_LIGHT_MODE_LOW_LIGHT) + runCurrent() + completeEnterAnimations() + runCurrent() + verify(mDreamManager).setSystemDreamComponent(DREAM_COMPONENT) + clearInvocations(mDreamManager) + + // Return to default unknown mode. + mLowLightDreamManager.setAmbientLightMode(LowLightDreamManager.AMBIENT_LIGHT_MODE_UNKNOWN) + runCurrent() + completeExitAnimations() + runCurrent() + verify(mDreamManager).setSystemDreamComponent(null) + } + + @Test + fun setAmbientLightMode_dreamComponentNotSet_doNothing() = testScope.runTest { + val lowLightDreamManager = LowLightDreamManager( + coroutineScope = testScope, + dreamManager = mDreamManager, + lowLightTransitionCoordinator = mTransitionCoordinator, + lowLightDreamComponent = null, + lowLightTransitionTimeoutMs = LOW_LIGHT_TIMEOUT_MS + ) + lowLightDreamManager.setAmbientLightMode(LowLightDreamManager.AMBIENT_LIGHT_MODE_LOW_LIGHT) + runCurrent() + verify(mEnterAnimator, never()).addListener(any()) + verify(mDreamManager, never()).setSystemDreamComponent(any()) + } + + @Test + fun setAmbientLightMode_multipleTimesBeforeAnimationEnds_cancelsPrevious() = testScope.runTest { + mLowLightDreamManager.setAmbientLightMode(LowLightDreamManager.AMBIENT_LIGHT_MODE_LOW_LIGHT) + runCurrent() + // If we reset the light mode back to regular before the previous animation finishes, it + // should be ignored. + mLowLightDreamManager.setAmbientLightMode(LowLightDreamManager.AMBIENT_LIGHT_MODE_REGULAR) + runCurrent() + completeEnterAnimations() + completeExitAnimations() + runCurrent() + verify(mDreamManager, times(1)).setSystemDreamComponent(null) + } + + @Test + fun setAmbientLightMode_animatorNeverFinishes_timesOut() = testScope.runTest { + mLowLightDreamManager.setAmbientLightMode(LowLightDreamManager.AMBIENT_LIGHT_MODE_LOW_LIGHT) + advanceTimeBy(delayTimeMillis = LOW_LIGHT_TIMEOUT_MS + 1) + // Animation never finishes, but we should still set the system dream + verify(mDreamManager).setSystemDreamComponent(DREAM_COMPONENT) + } + + private fun completeEnterAnimations() { + val listener = withArgCaptor { verify(mEnterAnimator).addListener(capture()) } + listener.onAnimationEnd(mEnterAnimator) + } + + private fun completeExitAnimations() { + val listener = withArgCaptor { verify(mExitAnimator).addListener(capture()) } + listener.onAnimationEnd(mExitAnimator) + } + + companion object { + private val DREAM_COMPONENT = ComponentName("test_package", "test_dream") + private const val LOW_LIGHT_TIMEOUT_MS: Long = 1000 + } +}
\ No newline at end of file diff --git a/libs/dream/lowlight/tests/src/com.android.dream.lowlight/LowLightTransitionCoordinatorTest.kt b/libs/dream/lowlight/tests/src/com.android.dream.lowlight/LowLightTransitionCoordinatorTest.kt new file mode 100644 index 000000000000..4c526a6ac69d --- /dev/null +++ b/libs/dream/lowlight/tests/src/com.android.dream.lowlight/LowLightTransitionCoordinatorTest.kt @@ -0,0 +1,184 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.dream.lowlight + +import android.animation.Animator +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.android.dream.lowlight.LowLightTransitionCoordinator.LowLightEnterListener +import com.android.dream.lowlight.LowLightTransitionCoordinator.LowLightExitListener +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceTimeBy +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.Captor +import org.mockito.Mock +import org.mockito.Mockito.never +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations +import src.com.android.dream.lowlight.utils.whenever +import kotlin.time.DurationUnit +import kotlin.time.toDuration + +@SmallTest +@OptIn(ExperimentalCoroutinesApi::class) +@RunWith(AndroidTestingRunner::class) +class LowLightTransitionCoordinatorTest { + @Mock + private lateinit var mEnterListener: LowLightEnterListener + + @Mock + private lateinit var mExitListener: LowLightExitListener + + @Mock + private lateinit var mAnimator: Animator + + @Captor + private lateinit var mAnimatorListenerCaptor: ArgumentCaptor<Animator.AnimatorListener> + + private lateinit var testScope: TestScope + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + testScope = TestScope(StandardTestDispatcher()) + } + + @Test + fun onEnterCalledOnListeners() = testScope.runTest { + val coordinator = LowLightTransitionCoordinator() + coordinator.setLowLightEnterListener(mEnterListener) + val job = launch { + coordinator.waitForLowLightTransitionAnimation(timeout = TIMEOUT, entering = true) + } + runCurrent() + verify(mEnterListener).onBeforeEnterLowLight() + assertThat(job.isCompleted).isTrue() + } + + @Test + fun onExitCalledOnListeners() = testScope.runTest { + val coordinator = LowLightTransitionCoordinator() + coordinator.setLowLightExitListener(mExitListener) + val job = launch { + coordinator.waitForLowLightTransitionAnimation(timeout = TIMEOUT, entering = false) + } + runCurrent() + verify(mExitListener).onBeforeExitLowLight() + assertThat(job.isCompleted).isTrue() + } + + @Test + fun listenerNotCalledAfterRemoval() = testScope.runTest { + val coordinator = LowLightTransitionCoordinator() + coordinator.setLowLightEnterListener(mEnterListener) + coordinator.setLowLightEnterListener(null) + val job = launch { + coordinator.waitForLowLightTransitionAnimation(timeout = TIMEOUT, entering = true) + } + runCurrent() + verify(mEnterListener, never()).onBeforeEnterLowLight() + assertThat(job.isCompleted).isTrue() + } + + @Test + fun waitsForAnimationToEnd() = testScope.runTest { + whenever(mEnterListener.onBeforeEnterLowLight()).thenReturn(mAnimator) + val coordinator = LowLightTransitionCoordinator() + coordinator.setLowLightEnterListener(mEnterListener) + val job = launch { + coordinator.waitForLowLightTransitionAnimation(timeout = TIMEOUT, entering = true) + } + runCurrent() + // Animator listener is added and the runnable is not run yet. + verify(mAnimator).addListener(mAnimatorListenerCaptor.capture()) + assertThat(job.isCompleted).isFalse() + + // Runnable is run once the animation ends. + mAnimatorListenerCaptor.value.onAnimationEnd(mAnimator) + runCurrent() + assertThat(job.isCompleted).isTrue() + assertThat(job.isCancelled).isFalse() + } + + @Test + fun waitsForTimeoutIfAnimationNeverEnds() = testScope.runTest { + whenever(mEnterListener.onBeforeEnterLowLight()).thenReturn(mAnimator) + val coordinator = LowLightTransitionCoordinator() + coordinator.setLowLightEnterListener(mEnterListener) + val job = launch { + coordinator.waitForLowLightTransitionAnimation(timeout = TIMEOUT, entering = true) + } + runCurrent() + assertThat(job.isCancelled).isFalse() + advanceTimeBy(delayTimeMillis = TIMEOUT.inWholeMilliseconds + 1) + // If animator doesn't complete within the timeout, we should cancel ourselves. + assertThat(job.isCancelled).isTrue() + } + + @Test + fun shouldCancelIfAnimationIsCancelled() = testScope.runTest { + whenever(mEnterListener.onBeforeEnterLowLight()).thenReturn(mAnimator) + val coordinator = LowLightTransitionCoordinator() + coordinator.setLowLightEnterListener(mEnterListener) + val job = launch { + coordinator.waitForLowLightTransitionAnimation(timeout = TIMEOUT, entering = true) + } + runCurrent() + // Animator listener is added and the runnable is not run yet. + verify(mAnimator).addListener(mAnimatorListenerCaptor.capture()) + assertThat(job.isCompleted).isFalse() + assertThat(job.isCancelled).isFalse() + + // Runnable is run once the animation ends. + mAnimatorListenerCaptor.value.onAnimationCancel(mAnimator) + runCurrent() + assertThat(job.isCompleted).isTrue() + assertThat(job.isCancelled).isTrue() + } + + @Test + fun shouldCancelAnimatorWhenJobCancelled() = testScope.runTest { + whenever(mEnterListener.onBeforeEnterLowLight()).thenReturn(mAnimator) + val coordinator = LowLightTransitionCoordinator() + coordinator.setLowLightEnterListener(mEnterListener) + val job = launch { + coordinator.waitForLowLightTransitionAnimation(timeout = TIMEOUT, entering = true) + } + runCurrent() + // Animator listener is added and the runnable is not run yet. + verify(mAnimator).addListener(mAnimatorListenerCaptor.capture()) + verify(mAnimator, never()).cancel() + assertThat(job.isCompleted).isFalse() + + job.cancel() + // We should have removed the listener and cancelled the animator + verify(mAnimator).removeListener(mAnimatorListenerCaptor.value) + verify(mAnimator).cancel() + } + + companion object { + private val TIMEOUT = 1.toDuration(DurationUnit.SECONDS) + } +} diff --git a/libs/dream/lowlight/tests/src/com/android/dream/lowlight/utils/KotlinMockitoHelpers.kt b/libs/dream/lowlight/tests/src/com/android/dream/lowlight/utils/KotlinMockitoHelpers.kt new file mode 100644 index 000000000000..e5ec26ca4b41 --- /dev/null +++ b/libs/dream/lowlight/tests/src/com/android/dream/lowlight/utils/KotlinMockitoHelpers.kt @@ -0,0 +1,125 @@ +package src.com.android.dream.lowlight.utils + +import org.mockito.ArgumentCaptor +import org.mockito.ArgumentMatcher +import org.mockito.Mockito +import org.mockito.Mockito.`when` +import org.mockito.stubbing.OngoingStubbing +import org.mockito.stubbing.Stubber + +/** + * Returns Mockito.eq() as nullable type to avoid java.lang.IllegalStateException when + * null is returned. + * + * Generic T is nullable because implicitly bounded by Any?. + */ +fun <T> eq(obj: T): T = Mockito.eq<T>(obj) + +/** + * Returns Mockito.any() as nullable type to avoid java.lang.IllegalStateException when + * null is returned. + * + * Generic T is nullable because implicitly bounded by Any?. + */ +fun <T> any(type: Class<T>): T = Mockito.any<T>(type) +inline fun <reified T> any(): T = any(T::class.java) + +/** + * Returns Mockito.argThat() as nullable type to avoid java.lang.IllegalStateException when + * null is returned. + * + * Generic T is nullable because implicitly bounded by Any?. + */ +fun <T> argThat(matcher: ArgumentMatcher<T>): T = Mockito.argThat(matcher) + +/** + * Kotlin type-inferred version of Mockito.nullable() + */ +inline fun <reified T> nullable(): T? = Mockito.nullable(T::class.java) + +/** + * Returns ArgumentCaptor.capture() as nullable type to avoid java.lang.IllegalStateException + * when null is returned. + * + * Generic T is nullable because implicitly bounded by Any?. + */ +fun <T> capture(argumentCaptor: ArgumentCaptor<T>): T = argumentCaptor.capture() + +/** + * Helper function for creating an argumentCaptor in kotlin. + * + * Generic T is nullable because implicitly bounded by Any?. + */ +inline fun <reified T : Any> argumentCaptor(): ArgumentCaptor<T> = + ArgumentCaptor.forClass(T::class.java) + +/** + * Helper function for creating new mocks, without the need to pass in a [Class] instance. + * + * Generic T is nullable because implicitly bounded by Any?. + * + * @param apply builder function to simplify stub configuration by improving type inference. + */ +inline fun <reified T : Any> mock(apply: T.() -> Unit = {}): T = Mockito.mock(T::class.java) + .apply(apply) + +/** + * Helper function for stubbing methods without the need to use backticks. + * + * @see Mockito.when + */ +fun <T> whenever(methodCall: T): OngoingStubbing<T> = `when`(methodCall) +fun <T> Stubber.whenever(mock: T): T = `when`(mock) + +/** + * A kotlin implemented wrapper of [ArgumentCaptor] which prevents the following exception when + * kotlin tests are mocking kotlin objects and the methods take non-null parameters: + * + * java.lang.NullPointerException: capture() must not be null + */ +class KotlinArgumentCaptor<T> constructor(clazz: Class<T>) { + private val wrapped: ArgumentCaptor<T> = ArgumentCaptor.forClass(clazz) + fun capture(): T = wrapped.capture() + val value: T + get() = wrapped.value + val allValues: List<T> + get() = wrapped.allValues +} + +/** + * Helper function for creating an argumentCaptor in kotlin. + * + * Generic T is nullable because implicitly bounded by Any?. + */ +inline fun <reified T : Any> kotlinArgumentCaptor(): KotlinArgumentCaptor<T> = + KotlinArgumentCaptor(T::class.java) + +/** + * Helper function for creating and using a single-use ArgumentCaptor in kotlin. + * + * val captor = argumentCaptor<Foo>() + * verify(...).someMethod(captor.capture()) + * val captured = captor.value + * + * becomes: + * + * val captured = withArgCaptor<Foo> { verify(...).someMethod(capture()) } + * + * NOTE: this uses the KotlinArgumentCaptor to avoid the NullPointerException. + */ +inline fun <reified T : Any> withArgCaptor(block: KotlinArgumentCaptor<T>.() -> Unit): T = + kotlinArgumentCaptor<T>().apply { block() }.value + +/** + * Variant of [withArgCaptor] for capturing multiple arguments. + * + * val captor = argumentCaptor<Foo>() + * verify(...).someMethod(captor.capture()) + * val captured: List<Foo> = captor.allValues + * + * becomes: + * + * val capturedList = captureMany<Foo> { verify(...).someMethod(capture()) } + */ +inline fun <reified T : Any> captureMany(block: KotlinArgumentCaptor<T>.() -> Unit): List<T> = + kotlinArgumentCaptor<T>().apply{ block() }.allValues diff --git a/libs/hwui/Android.bp b/libs/hwui/Android.bp index ad9aa6cdd3d9..5d79104200d9 100644 --- a/libs/hwui/Android.bp +++ b/libs/hwui/Android.bp @@ -73,7 +73,6 @@ cc_defaults { target: { android: { include_dirs: [ - "external/skia/src/effects", "external/skia/src/image", "external/skia/src/utils", "external/skia/src/gpu", @@ -93,6 +92,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 +109,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", @@ -212,6 +213,15 @@ filegroup { path: "apex/java", } +java_api_contribution { + name: "framework-graphics-public-stubs", + api_surface: "public", + api_file: "api/current.txt", + visibility: [ + "//build/orchestrator/apis", + ], +} + // ------------------------ // APEX // ------------------------ @@ -322,11 +332,14 @@ cc_defaults { "jni/android_graphics_Matrix.cpp", "jni/android_graphics_Picture.cpp", "jni/android_graphics_DisplayListCanvas.cpp", + "jni/android_graphics_Mesh.cpp", "jni/android_graphics_RenderNode.cpp", "jni/android_nio_utils.cpp", "jni/android_util_PathParser.cpp", "jni/Bitmap.cpp", + "jni/BufferUtils.cpp", + "jni/HardwareBufferHelpers.cpp", "jni/BitmapFactory.cpp", "jni/ByteBufferStreamAdaptor.cpp", "jni/Camera.cpp", @@ -335,9 +348,11 @@ cc_defaults { "jni/CreateJavaOutputStreamAdaptor.cpp", "jni/FontFamily.cpp", "jni/FontUtils.cpp", + "jni/Gainmap.cpp", "jni/Graphics.cpp", "jni/ImageDecoder.cpp", "jni/Interpolator.cpp", + "jni/MeshSpecification.cpp", "jni/MaskFilter.cpp", "jni/NinePatch.cpp", "jni/NinePatchPeeker.cpp", @@ -345,9 +360,11 @@ cc_defaults { "jni/PaintFilter.cpp", "jni/Path.cpp", "jni/PathEffect.cpp", + "jni/PathIterator.cpp", "jni/PathMeasure.cpp", "jni/Picture.cpp", "jni/Region.cpp", + "jni/ScopedParcel.cpp", "jni/Shader.cpp", "jni/RenderEffect.cpp", "jni/Typeface.cpp", @@ -358,27 +375,30 @@ cc_defaults { "jni/text/LineBreaker.cpp", "jni/text/MeasuredText.cpp", "jni/text/TextShaper.cpp", + "jni/text/GraphemeBreak.cpp", ], - header_libs: ["android_graphics_jni_headers"], + header_libs: [ + "android_graphics_jni_headers", + "libnativewindow_headers", + ], include_dirs: [ "external/skia/include/private", "external/skia/src/codec", "external/skia/src/core", - "external/skia/src/effects", - "external/skia/src/image", - "external/skia/src/images", ], shared_libs: [ "libbase", "libcutils", "libharfbuzz_ng", + "libimage_io", + "libjpeg", + "libjpegrecoverymap", "liblog", "libminikin", "libz", - "libjpeg", ], static_libs: [ @@ -392,6 +412,7 @@ cc_defaults { "jni/AnimatedImageDrawable.cpp", "jni/android_graphics_TextureLayer.cpp", "jni/android_graphics_HardwareRenderer.cpp", + "jni/android_graphics_HardwareBufferRenderer.cpp", "jni/BitmapRegionDecoder.cpp", "jni/GIFMovie.cpp", "jni/GraphicsStatsService.cpp", @@ -487,6 +508,7 @@ cc_defaults { "canvas/CanvasOpBuffer.cpp", "canvas/CanvasOpRasterizer.cpp", "effects/StretchEffect.cpp", + "effects/GainmapRenderer.cpp", "pipeline/skia/HolePunch.cpp", "pipeline/skia/SkiaDisplayList.cpp", "pipeline/skia/SkiaRecordingCanvas.cpp", @@ -515,9 +537,12 @@ cc_defaults { "AnimatorManager.cpp", "CanvasTransform.cpp", "DamageAccumulator.cpp", + "Gainmap.cpp", "Interpolator.cpp", "LightingInfo.cpp", "Matrix.cpp", + "Mesh.cpp", + "MemoryPolicy.cpp", "PathParser.cpp", "Properties.cpp", "PropertyValuesAnimatorSet.cpp", @@ -528,6 +553,7 @@ cc_defaults { "RootRenderNode.cpp", "SkiaCanvas.cpp", "SkiaInterpolator.cpp", + "Tonemapper.cpp", "VectorDrawable.cpp", ], @@ -566,6 +592,7 @@ cc_defaults { "renderthread/VulkanSurface.cpp", "renderthread/RenderProxy.cpp", "renderthread/RenderThread.cpp", + "renderthread/HintSessionWrapper.cpp", "service/GraphicsStatsService.cpp", "thread/CommonPool.cpp", "utils/GLUtils.cpp", @@ -632,7 +659,7 @@ cc_library_static { cc_defaults { name: "hwui_test_defaults", defaults: ["hwui_defaults"], - test_suites: ["device-tests"], + test_suites: ["general-tests"], header_libs: ["libandroid_headers_private"], target: { android: { @@ -674,6 +701,7 @@ cc_test { srcs: [ "tests/unit/main.cpp", "tests/unit/ABitmapTests.cpp", + "tests/unit/AutoBackendTextureReleaseTests.cpp", "tests/unit/CacheManagerTests.cpp", "tests/unit/CanvasContextTests.cpp", "tests/unit/CanvasOpTests.cpp", @@ -710,6 +738,9 @@ cc_test { "tests/unit/VectorDrawableTests.cpp", "tests/unit/WebViewFunctorManagerTests.cpp", ], + data: [ + ":hwuimicro", + ], } // ------------------------ diff --git a/libs/hwui/AndroidTest.xml b/libs/hwui/AndroidTest.xml index 381fb9f6c7bf..75f61f5f7f9d 100644 --- a/libs/hwui/AndroidTest.xml +++ b/libs/hwui/AndroidTest.xml @@ -16,22 +16,23 @@ <configuration description="Config for hwuimicro"> <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer"> <option name="cleanup" value="true" /> - <option name="push" value="hwui_unit_tests->/data/nativetest/hwui_unit_tests" /> - <option name="push" value="hwuimicro->/data/benchmarktest/hwuimicro" /> - <option name="push" value="hwuimacro->/data/benchmarktest/hwuimacro" /> + <option name="push" value="hwui_unit_tests->/data/local/tmp/nativetest/hwui_unit_tests" /> + <option name="push" value="hwuimicro->/data/local/tmp/benchmarktest/hwuimicro" /> + <option name="push" value="hwuimacro->/data/local/tmp/benchmarktest/hwuimacro" /> </target_preparer> <option name="test-suite-tag" value="apct" /> + <option name="not-shardable" value="true" /> <test class="com.android.tradefed.testtype.GTest" > - <option name="native-test-device-path" value="/data/nativetest" /> + <option name="native-test-device-path" value="/data/local/tmp/nativetest" /> <option name="module-name" value="hwui_unit_tests" /> </test> <test class="com.android.tradefed.testtype.GoogleBenchmarkTest" > - <option name="native-benchmark-device-path" value="/data/benchmarktest" /> + <option name="native-benchmark-device-path" value="/data/local/tmp/benchmarktest" /> <option name="benchmark-module-name" value="hwuimicro" /> <option name="file-exclusion-filter-regex" value=".*\.config$" /> </test> <test class="com.android.tradefed.testtype.GoogleBenchmarkTest" > - <option name="native-benchmark-device-path" value="/data/benchmarktest" /> + <option name="native-benchmark-device-path" value="/data/local/tmp/benchmarktest" /> <option name="benchmark-module-name" value="hwuimacro" /> <option name="file-exclusion-filter-regex" value=".*\.config$" /> </test> diff --git a/libs/hwui/AutoBackendTextureRelease.cpp b/libs/hwui/AutoBackendTextureRelease.cpp index ef5eacbdb4ad..b656b6ac8204 100644 --- a/libs/hwui/AutoBackendTextureRelease.cpp +++ b/libs/hwui/AutoBackendTextureRelease.cpp @@ -32,9 +32,17 @@ AutoBackendTextureRelease::AutoBackendTextureRelease(GrDirectContext* context, bool createProtectedImage = 0 != (desc.usage & AHARDWAREBUFFER_USAGE_PROTECTED_CONTENT); GrBackendFormat backendFormat = GrAHardwareBufferUtils::GetBackendFormat(context, buffer, desc.format, false); + LOG_ALWAYS_FATAL_IF(!backendFormat.isValid(), + __FILE__ " Invalid GrBackendFormat. GrBackendApi==%" PRIu32 + ", AHardwareBuffer_Format==%" PRIu32 ".", + static_cast<int>(context->backend()), desc.format); mBackendTexture = GrAHardwareBufferUtils::MakeBackendTexture( context, buffer, desc.width, desc.height, &mDeleteProc, &mUpdateProc, &mImageCtx, createProtectedImage, backendFormat, false); + LOG_ALWAYS_FATAL_IF(!mBackendTexture.isValid(), + __FILE__ " Invalid GrBackendTexture. Width==%" PRIu32 ", height==%" PRIu32 + ", protected==%d", + desc.width, desc.height, createProtectedImage); } void AutoBackendTextureRelease::unref(bool releaseImage) { @@ -74,13 +82,13 @@ void AutoBackendTextureRelease::makeImage(AHardwareBuffer* buffer, AHardwareBuffer_Desc desc; AHardwareBuffer_describe(buffer, &desc); SkColorType colorType = GrAHardwareBufferUtils::GetSkColorTypeFromBufferFormat(desc.format); + // The following ref will be counteracted by Skia calling releaseProc, either during + // MakeFromTexture if there is a failure, or later when SkImage is discarded. It must + // be called before MakeFromTexture, otherwise Skia may remove HWUI's ref on failure. + ref(); mImage = SkImage::MakeFromTexture( context, mBackendTexture, kTopLeft_GrSurfaceOrigin, colorType, kPremul_SkAlphaType, uirenderer::DataSpaceToColorSpace(dataspace), releaseProc, this); - if (mImage.get()) { - // The following ref will be counteracted by releaseProc, when SkImage is discarded. - ref(); - } } void AutoBackendTextureRelease::newBufferContent(GrDirectContext* context) { diff --git a/libs/hwui/AutoBackendTextureRelease.h b/libs/hwui/AutoBackendTextureRelease.h index c9bb767a3185..f0eb2a8b6eab 100644 --- a/libs/hwui/AutoBackendTextureRelease.h +++ b/libs/hwui/AutoBackendTextureRelease.h @@ -25,6 +25,9 @@ namespace android { namespace uirenderer { +// Friend TestUtils serves as a proxy for any test cases that require access to private members. +class TestUtils; + /** * AutoBackendTextureRelease manages EglImage/VkImage lifetime. It is a ref-counted object * that keeps GPU resources alive until the last SkImage object using them is destroyed. @@ -66,6 +69,9 @@ private: // mImage is the SkImage created from mBackendTexture. sk_sp<SkImage> mImage; + + // Friend TestUtils serves as a proxy for any test cases that require access to private members. + friend class TestUtils; }; } /* namespace uirenderer */ diff --git a/libs/hwui/CanvasTransform.cpp b/libs/hwui/CanvasTransform.cpp index d0d24a8738f4..cd4fae86aa52 100644 --- a/libs/hwui/CanvasTransform.cpp +++ b/libs/hwui/CanvasTransform.cpp @@ -15,19 +15,21 @@ */ #include "CanvasTransform.h" -#include "Properties.h" -#include "utils/Color.h" +#include <SkAndroidFrameworkUtils.h> +#include <SkBlendMode.h> #include <SkColorFilter.h> #include <SkGradientShader.h> +#include <SkHighContrastFilter.h> #include <SkPaint.h> #include <SkShader.h> +#include <log/log.h> #include <algorithm> #include <cmath> -#include <log/log.h> -#include <SkHighContrastFilter.h> +#include "Properties.h" +#include "utils/Color.h" namespace android::uirenderer { @@ -82,27 +84,21 @@ static void applyColorTransform(ColorTransform transform, SkPaint& paint) { paint.setColor(newColor); if (paint.getShader()) { - SkShader::GradientInfo info; + SkAndroidFrameworkUtils::LinearGradientInfo info; std::array<SkColor, 10> _colorStorage; std::array<SkScalar, _colorStorage.size()> _offsetStorage; info.fColorCount = _colorStorage.size(); info.fColors = _colorStorage.data(); info.fColorOffsets = _offsetStorage.data(); - SkShader::GradientType type = paint.getShader()->asAGradient(&info); - - if (info.fColorCount <= 10) { - switch (type) { - case SkShader::kLinear_GradientType: - for (int i = 0; i < info.fColorCount; i++) { - info.fColors[i] = transformColor(transform, info.fColors[i]); - } - paint.setShader(SkGradientShader::MakeLinear(info.fPoint, info.fColors, - info.fColorOffsets, info.fColorCount, - info.fTileMode, info.fGradientFlags, nullptr)); - break; - default:break; - } + if (SkAndroidFrameworkUtils::ShaderAsALinearGradient(paint.getShader(), &info) && + info.fColorCount <= _colorStorage.size()) { + for (int i = 0; i < info.fColorCount; i++) { + info.fColors[i] = transformColor(transform, info.fColors[i]); + } + paint.setShader(SkGradientShader::MakeLinear( + info.fPoints, info.fColors, info.fColorOffsets, info.fColorCount, + info.fTileMode, info.fGradientFlags, nullptr)); } } diff --git a/libs/hwui/CanvasTransform.h b/libs/hwui/CanvasTransform.h index c46a2d369974..291f4cf7193b 100644 --- a/libs/hwui/CanvasTransform.h +++ b/libs/hwui/CanvasTransform.h @@ -45,4 +45,4 @@ bool transformPaint(ColorTransform transform, SkPaint* paint, BitmapPalette pale SkColor transformColor(ColorTransform transform, SkColor color); SkColor transformColorInverse(ColorTransform transform, SkColor color); -} // namespace android::uirenderer;
\ No newline at end of file +} // namespace android::uirenderer diff --git a/libs/hwui/ColorMode.h b/libs/hwui/ColorMode.h index 3df5c3c9caed..959cf742c8e4 100644 --- a/libs/hwui/ColorMode.h +++ b/libs/hwui/ColorMode.h @@ -25,9 +25,10 @@ enum class ColorMode { // WideColorGamut selects the most optimal colorspace & format for the device's display // Most commonly DisplayP3 + RGBA_8888 currently. WideColorGamut = 1, - // HDR Rec2020 + F16 + // Extended range Display P3 Hdr = 2, - // HDR Rec2020 + 1010102 + // Extended range Display P3 10-bit + // for test purposes only, not shippable due to insuffient alpha Hdr10 = 3, // Alpha 8 A8 = 4, 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/DeferredLayerUpdater.cpp b/libs/hwui/DeferredLayerUpdater.cpp index a5c0924579eb..b763a96e8e8a 100644 --- a/libs/hwui/DeferredLayerUpdater.cpp +++ b/libs/hwui/DeferredLayerUpdater.cpp @@ -169,6 +169,8 @@ void DeferredLayerUpdater::apply() { sk_sp<SkImage> layerImage = mImageSlots[slot].createIfNeeded( hardwareBuffer, dataspace, newContent, mRenderState.getRenderThread().getGrContext()); + AHardwareBuffer_Desc bufferDesc; + AHardwareBuffer_describe(hardwareBuffer, &bufferDesc); // unref to match the ref added by ASurfaceTexture_dequeueBuffer. eglCreateImageKHR // (invoked by createIfNeeded) will add a ref to the AHardwareBuffer. AHardwareBuffer_release(hardwareBuffer); @@ -189,6 +191,7 @@ void DeferredLayerUpdater::apply() { maxLuminanceNits = std::max(cta861_3.maxContentLightLevel, maxLuminanceNits); } + mLayer->setBufferFormat(bufferDesc.format); updateLayer(forceFilter, layerImage, outTransform, currentCropRect, maxLuminanceNits); } diff --git a/libs/hwui/DeferredLayerUpdater.h b/libs/hwui/DeferredLayerUpdater.h index 9a4c5505fa35..a7f8f6189a8e 100644 --- a/libs/hwui/DeferredLayerUpdater.h +++ b/libs/hwui/DeferredLayerUpdater.h @@ -16,6 +16,7 @@ #pragma once +#include <SkBlendMode.h> #include <SkColorFilter.h> #include <SkImage.h> #include <SkMatrix.h> diff --git a/libs/hwui/DeviceInfo.cpp b/libs/hwui/DeviceInfo.cpp index 07594715a84c..32bc122fdc58 100644 --- a/libs/hwui/DeviceInfo.cpp +++ b/libs/hwui/DeviceInfo.cpp @@ -93,15 +93,25 @@ void DeviceInfo::setWideColorDataspace(ADataSpace dataspace) { case ADATASPACE_SCRGB: get()->mWideColorSpace = SkColorSpace::MakeSRGB(); break; + default: + ALOGW("Unknown dataspace %d", dataspace); + // Treat unknown dataspaces as sRGB, so fall through + [[fallthrough]]; case ADATASPACE_SRGB: // when sRGB is returned, it means wide color gamut is not supported. get()->mWideColorSpace = SkColorSpace::MakeSRGB(); break; - default: - LOG_ALWAYS_FATAL("Unreachable: unsupported wide color space."); } } +void DeviceInfo::setSupportFp16ForHdr(bool supportFp16ForHdr) { + get()->mSupportFp16ForHdr = supportFp16ForHdr; +} + +void DeviceInfo::setSupportMixedColorSpaces(bool supportMixedColorSpaces) { + get()->mSupportMixedColorSpaces = supportMixedColorSpaces; +} + void DeviceInfo::onRefreshRateChanged(int64_t vsyncPeriod) { mVsyncPeriod = vsyncPeriod; } diff --git a/libs/hwui/DeviceInfo.h b/libs/hwui/DeviceInfo.h index d5fee3f667a9..d4af0872e31e 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> @@ -57,6 +59,12 @@ public: } static void setWideColorDataspace(ADataSpace dataspace); + static void setSupportFp16ForHdr(bool supportFp16ForHdr); + static bool isSupportFp16ForHdr() { return get()->mSupportFp16ForHdr; }; + + static void setSupportMixedColorSpaces(bool supportMixedColorSpaces); + static bool isSupportMixedColorSpaces() { return get()->mSupportMixedColorSpaces; }; + // this value is only valid after the GPU has been initialized and there is a valid graphics // context or if you are using the HWUI_NULL_GPU int maxTextureSize() const; @@ -86,6 +94,8 @@ private: int mMaxTextureSize; sk_sp<SkColorSpace> mWideColorSpace = SkColorSpace::MakeSRGB(); + bool mSupportFp16ForHdr = false; + bool mSupportMixedColorSpaces = false; SkColorType mWideColorType = SkColorType::kN32_SkColorType; int mDisplaysSize = 0; int mPhysicalDisplayIndex = -1; diff --git a/libs/hwui/DisplayListOps.in b/libs/hwui/DisplayListOps.in index 4ec782f6fec0..a18ba1c633b9 100644 --- a/libs/hwui/DisplayListOps.in +++ b/libs/hwui/DisplayListOps.in @@ -52,3 +52,5 @@ X(DrawShadowRec) X(DrawVectorDrawable) X(DrawRippleDrawable) X(DrawWebView) +X(DrawSkMesh) +X(DrawMesh)
\ No newline at end of file diff --git a/libs/hwui/FrameInfo.h b/libs/hwui/FrameInfo.h index 564ee4f53a54..b15b6cb9a9ec 100644 --- a/libs/hwui/FrameInfo.h +++ b/libs/hwui/FrameInfo.h @@ -104,6 +104,7 @@ public: set(FrameInfoIndex::AnimationStart) = vsyncTime; set(FrameInfoIndex::PerformTraversalsStart) = vsyncTime; set(FrameInfoIndex::DrawStart) = vsyncTime; + set(FrameInfoIndex::FrameStartTime) = vsyncTime; set(FrameInfoIndex::FrameDeadline) = frameDeadline; set(FrameInfoIndex::FrameInterval) = frameInterval; return *this; 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/Gainmap.cpp b/libs/hwui/Gainmap.cpp new file mode 100644 index 000000000000..30f401ef5f01 --- /dev/null +++ b/libs/hwui/Gainmap.cpp @@ -0,0 +1,32 @@ +/** + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include "Gainmap.h" + +namespace android::uirenderer { + +sp<Gainmap> Gainmap::allocateHardwareGainmap(const sp<Gainmap>& srcGainmap) { + auto gainmap = sp<Gainmap>::make(); + gainmap->info = srcGainmap->info; + const SkBitmap skSrcBitmap = srcGainmap->bitmap->getSkBitmap(); + sk_sp<Bitmap> skBitmap(Bitmap::allocateHardwareBitmap(skSrcBitmap)); + if (!skBitmap.get()) { + return nullptr; + } + gainmap->bitmap = std::move(skBitmap); + return gainmap; +} + +} // namespace android::uirenderer
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/SplitScreenSecondaryActivity.java b/libs/hwui/Gainmap.h index baa1e6fdd1e9..3bc183ab854f 100644 --- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/SplitScreenSecondaryActivity.java +++ b/libs/hwui/Gainmap.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 The Android Open Source Project + * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,15 +14,20 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.testapp; +#pragma once -import android.app.Activity; -import android.os.Bundle; +#include <SkGainmapInfo.h> +#include <SkImage.h> +#include <hwui/Bitmap.h> +#include <utils/LightRefBase.h> -public class SplitScreenSecondaryActivity extends Activity { - @Override - protected void onCreate(Bundle icicle) { - super.onCreate(icicle); - setContentView(R.layout.activity_splitscreen_secondary); - } -} +namespace android::uirenderer { + +class Gainmap : public LightRefBase<Gainmap> { +public: + SkGainmapInfo info; + sk_sp<Bitmap> bitmap; + static sp<Gainmap> allocateHardwareGainmap(const sp<Gainmap>& srcGainmap); +}; + +} // namespace android::uirenderer diff --git a/libs/hwui/HardwareBitmapUploader.cpp b/libs/hwui/HardwareBitmapUploader.cpp index c24cabb287de..b7e99994355c 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> @@ -39,6 +42,8 @@ namespace android::uirenderer { +static constexpr auto kThreadTimeout = 60000_ms; + class AHBUploader; // This helper uploader classes allows us to upload using either EGL or Vulkan using the same // interface. @@ -77,7 +82,7 @@ public: } void postIdleTimeoutCheck() { - mUploadThread->queue().postDelayed(5000_ms, [this](){ this->idleTimeoutCheck(); }); + mUploadThread->queue().postDelayed(kThreadTimeout, [this]() { this->idleTimeoutCheck(); }); } protected: @@ -94,7 +99,7 @@ private: bool shouldTimeOutLocked() { nsecs_t durationSince = systemTime() - mLastUpload; - return durationSince > 2000_ms; + return durationSince > kThreadTimeout; } void idleTimeoutCheck() { 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/Layer.cpp b/libs/hwui/Layer.cpp index 9053c1240957..fc3118ae32dd 100644 --- a/libs/hwui/Layer.cpp +++ b/libs/hwui/Layer.cpp @@ -20,6 +20,8 @@ #include "utils/Color.h" #include "utils/MathUtils.h" +#include <SkBlendMode.h> + #include <log/log.h> namespace android { diff --git a/libs/hwui/Layer.h b/libs/hwui/Layer.h index 47eb5d3bfb83..345749b6d920 100644 --- a/libs/hwui/Layer.h +++ b/libs/hwui/Layer.h @@ -102,6 +102,10 @@ public: inline float getMaxLuminanceNits() { return mMaxLuminanceNits; } + void setBufferFormat(uint32_t format) { mBufferFormat = format; } + + uint32_t getBufferFormat() const { return mBufferFormat; } + void draw(SkCanvas* canvas); protected: @@ -169,6 +173,8 @@ private: */ float mMaxLuminanceNits = -1; + uint32_t mBufferFormat = 0; + }; // struct Layer } // namespace uirenderer diff --git a/libs/hwui/MemoryPolicy.cpp b/libs/hwui/MemoryPolicy.cpp new file mode 100644 index 000000000000..ca1312e75f4c --- /dev/null +++ b/libs/hwui/MemoryPolicy.cpp @@ -0,0 +1,69 @@ +/* + * 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. + */ + +#include "MemoryPolicy.h" + +#include <android-base/properties.h> + +#include <optional> +#include <string_view> + +#include "Properties.h" + +namespace android::uirenderer { + +constexpr static MemoryPolicy sDefaultMemoryPolicy; +constexpr static MemoryPolicy sPersistentOrSystemPolicy{ + .contextTimeout = 10_s, + .useAlternativeUiHidden = true, +}; +constexpr static MemoryPolicy sLowRamPolicy{ + .useAlternativeUiHidden = true, + .purgeScratchOnly = false, +}; +constexpr static MemoryPolicy sExtremeLowRam{ + .initialMaxSurfaceAreaScale = 0.2f, + .surfaceSizeMultiplier = 5 * 4.0f, + .backgroundRetentionPercent = 0.2f, + .contextTimeout = 5_s, + .minimumResourceRetention = 1_s, + .useAlternativeUiHidden = true, + .purgeScratchOnly = false, + .releaseContextOnStoppedOnly = true, +}; + +const MemoryPolicy& loadMemoryPolicy() { + if (Properties::isSystemOrPersistent) { + return sPersistentOrSystemPolicy; + } + std::string memoryPolicy = base::GetProperty(PROPERTY_MEMORY_POLICY, ""); + if (memoryPolicy == "default") { + return sDefaultMemoryPolicy; + } + if (memoryPolicy == "lowram") { + return sLowRamPolicy; + } + if (memoryPolicy == "extremelowram") { + return sExtremeLowRam; + } + + if (Properties::isLowRam) { + return sLowRamPolicy; + } + return sDefaultMemoryPolicy; +} + +} // namespace android::uirenderer
\ No newline at end of file diff --git a/libs/hwui/MemoryPolicy.h b/libs/hwui/MemoryPolicy.h new file mode 100644 index 000000000000..139cdde5c0d2 --- /dev/null +++ b/libs/hwui/MemoryPolicy.h @@ -0,0 +1,63 @@ +/* + * 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 "utils/TimeUtils.h" + +namespace android::uirenderer { + +// Values mirror those from ComponentCallbacks2.java +enum class TrimLevel { + COMPLETE = 80, + MODERATE = 60, + BACKGROUND = 40, + UI_HIDDEN = 20, + RUNNING_CRITICAL = 15, + RUNNING_LOW = 10, + RUNNING_MODERATE = 5, +}; + +struct MemoryPolicy { + // The initial scale factor applied to the display resolution. The default is 1, but + // lower values may be used to start with a smaller initial cache size. The cache will + // be adjusted if larger frames are actually rendered + float initialMaxSurfaceAreaScale = 1.0f; + // The foreground cache size multiplier. The surface area of the screen will be multiplied + // by this + float surfaceSizeMultiplier = 12.0f * 4.0f; + // How much of the foreground cache size should be preserved when going into the background + float backgroundRetentionPercent = 0.5f; + // How long after the last renderer goes away before the GPU context is released. A value + // of 0 means only drop the context on background TRIM signals + nsecs_t contextTimeout = 10_s; + // The minimum amount of time to hold onto items in the resource cache + // The actual time used will be the max of this & when frames were actually rendered + nsecs_t minimumResourceRetention = 10_s; + // If false, use only TRIM_UI_HIDDEN to drive background cache limits; + // If true, use all signals (such as all contexts are stopped) to drive the limits + bool useAlternativeUiHidden = true; + // Whether or not to only purge scratch resources when triggering UI Hidden or background + // collection + bool purgeScratchOnly = true; + // EXPERIMENTAL: Whether or not to trigger releasing GPU context when all contexts are stopped + // WARNING: Enabling this option can lead to instability, see b/266626090 + bool releaseContextOnStoppedOnly = false; +}; + +const MemoryPolicy& loadMemoryPolicy(); + +} // namespace android::uirenderer
\ No newline at end of file diff --git a/libs/hwui/Mesh.cpp b/libs/hwui/Mesh.cpp new file mode 100644 index 000000000000..e59bc9565a59 --- /dev/null +++ b/libs/hwui/Mesh.cpp @@ -0,0 +1,102 @@ +/* + * 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. + */ + +#include "Mesh.h" + +#include <GLES/gl.h> +#include <SkMesh.h> + +#include "SafeMath.h" + +static size_t min_vcount_for_mode(SkMesh::Mode mode) { + switch (mode) { + case SkMesh::Mode::kTriangles: + return 3; + case SkMesh::Mode::kTriangleStrip: + return 3; + } +} + +// Re-implementation of SkMesh::validate to validate user side that their mesh is valid. +std::tuple<bool, SkString> Mesh::validate() { +#define FAIL_MESH_VALIDATE(...) return std::make_tuple(false, SkStringPrintf(__VA_ARGS__)) + if (!mMeshSpec) { + FAIL_MESH_VALIDATE("MeshSpecification is required."); + } + if (mVertexBufferData.empty()) { + FAIL_MESH_VALIDATE("VertexBuffer is required."); + } + + auto meshStride = mMeshSpec->stride(); + auto meshMode = SkMesh::Mode(mMode); + SafeMath sm; + size_t vsize = sm.mul(meshStride, mVertexCount); + if (sm.add(vsize, mVertexOffset) > mVertexBufferData.size()) { + FAIL_MESH_VALIDATE( + "The vertex buffer offset and vertex count reads beyond the end of the" + " vertex buffer."); + } + + if (mVertexOffset % meshStride != 0) { + FAIL_MESH_VALIDATE("The vertex offset (%zu) must be a multiple of the vertex stride (%zu).", + mVertexOffset, meshStride); + } + + if (size_t uniformSize = mMeshSpec->uniformSize()) { + if (!mBuilder->fUniforms || mBuilder->fUniforms->size() < uniformSize) { + FAIL_MESH_VALIDATE("The uniform data is %zu bytes but must be at least %zu.", + mBuilder->fUniforms->size(), uniformSize); + } + } + + auto modeToStr = [](SkMesh::Mode m) { + switch (m) { + case SkMesh::Mode::kTriangles: + return "triangles"; + case SkMesh::Mode::kTriangleStrip: + return "triangle-strip"; + } + }; + if (!mIndexBufferData.empty()) { + if (mIndexCount < min_vcount_for_mode(meshMode)) { + FAIL_MESH_VALIDATE("%s mode requires at least %zu indices but index count is %zu.", + modeToStr(meshMode), min_vcount_for_mode(meshMode), mIndexCount); + } + size_t isize = sm.mul(sizeof(uint16_t), mIndexCount); + if (sm.add(isize, mIndexOffset) > mIndexBufferData.size()) { + FAIL_MESH_VALIDATE( + "The index buffer offset and index count reads beyond the end of the" + " index buffer."); + } + // If we allow 32 bit indices then this should enforce 4 byte alignment in that case. + if (!SkIsAlign2(mIndexOffset)) { + FAIL_MESH_VALIDATE("The index offset must be a multiple of 2."); + } + } else { + if (mVertexCount < min_vcount_for_mode(meshMode)) { + FAIL_MESH_VALIDATE("%s mode requires at least %zu vertices but vertex count is %zu.", + modeToStr(meshMode), min_vcount_for_mode(meshMode), mVertexCount); + } + SkASSERT(!fICount); + SkASSERT(!fIOffset); + } + + if (!sm.ok()) { + FAIL_MESH_VALIDATE("Overflow"); + } +#undef FAIL_MESH_VALIDATE + return {true, {}}; +} diff --git a/libs/hwui/Mesh.h b/libs/hwui/Mesh.h new file mode 100644 index 000000000000..13e3c8e7bf77 --- /dev/null +++ b/libs/hwui/Mesh.h @@ -0,0 +1,199 @@ +/* + * 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. + */ + +#ifndef MESH_H_ +#define MESH_H_ + +#include <GrDirectContext.h> +#include <SkMesh.h> +#include <jni.h> +#include <log/log.h> + +#include <utility> + +class MeshUniformBuilder { +public: + struct MeshUniform { + template <typename T> + std::enable_if_t<std::is_trivially_copyable<T>::value, MeshUniform> operator=( + const T& val) { + if (!fVar) { + LOG_FATAL("Assigning to missing variable"); + } else if (sizeof(val) != fVar->sizeInBytes()) { + LOG_FATAL("Incorrect value size"); + } else { + void* dst = reinterpret_cast<void*>( + reinterpret_cast<uint8_t*>(fOwner->writableUniformData()) + fVar->offset); + memcpy(dst, &val, sizeof(val)); + } + } + + MeshUniform& operator=(const SkMatrix& val) { + if (!fVar) { + LOG_FATAL("Assigning to missing variable"); + } else if (fVar->sizeInBytes() != 9 * sizeof(float)) { + LOG_FATAL("Incorrect value size"); + } else { + float* data = reinterpret_cast<float*>( + reinterpret_cast<uint8_t*>(fOwner->writableUniformData()) + fVar->offset); + data[0] = val.get(0); + data[1] = val.get(3); + data[2] = val.get(6); + data[3] = val.get(1); + data[4] = val.get(4); + data[5] = val.get(7); + data[6] = val.get(2); + data[7] = val.get(5); + data[8] = val.get(8); + } + return *this; + } + + template <typename T> + bool set(const T val[], const int count) { + static_assert(std::is_trivially_copyable<T>::value, "Value must be trivial copyable"); + if (!fVar) { + LOG_FATAL("Assigning to missing variable"); + return false; + } else if (sizeof(T) * count != fVar->sizeInBytes()) { + LOG_FATAL("Incorrect value size"); + return false; + } else { + void* dst = reinterpret_cast<void*>( + reinterpret_cast<uint8_t*>(fOwner->writableUniformData()) + fVar->offset); + memcpy(dst, val, sizeof(T) * count); + } + return true; + } + + MeshUniformBuilder* fOwner; + const SkRuntimeEffect::Uniform* fVar; + }; + MeshUniform uniform(std::string_view name) { return {this, fMeshSpec->findUniform(name)}; } + + explicit MeshUniformBuilder(sk_sp<SkMeshSpecification> meshSpec) { + fMeshSpec = sk_sp(meshSpec); + fUniforms = (SkData::MakeZeroInitialized(meshSpec->uniformSize())); + } + + sk_sp<SkData> fUniforms; + +private: + void* writableUniformData() { + if (!fUniforms->unique()) { + fUniforms = SkData::MakeWithCopy(fUniforms->data(), fUniforms->size()); + } + return fUniforms->writable_data(); + } + + sk_sp<SkMeshSpecification> fMeshSpec; +}; + +class Mesh { +public: + Mesh(const sk_sp<SkMeshSpecification>& meshSpec, int mode, + std::vector<uint8_t>&& vertexBufferData, jint vertexCount, jint vertexOffset, + std::unique_ptr<MeshUniformBuilder> builder, const SkRect& bounds) + : mMeshSpec(meshSpec) + , mMode(mode) + , mVertexBufferData(std::move(vertexBufferData)) + , mVertexCount(vertexCount) + , mVertexOffset(vertexOffset) + , mBuilder(std::move(builder)) + , mBounds(bounds) {} + + Mesh(const sk_sp<SkMeshSpecification>& meshSpec, int mode, + std::vector<uint8_t>&& vertexBufferData, jint vertexCount, jint vertexOffset, + std::vector<uint8_t>&& indexBuffer, jint indexCount, jint indexOffset, + std::unique_ptr<MeshUniformBuilder> builder, const SkRect& bounds) + : mMeshSpec(meshSpec) + , mMode(mode) + , mVertexBufferData(std::move(vertexBufferData)) + , mVertexCount(vertexCount) + , mVertexOffset(vertexOffset) + , mIndexBufferData(std::move(indexBuffer)) + , mIndexCount(indexCount) + , mIndexOffset(indexOffset) + , mBuilder(std::move(builder)) + , mBounds(bounds) {} + + Mesh(Mesh&&) = default; + + Mesh& operator=(Mesh&&) = default; + + [[nodiscard]] std::tuple<bool, SkString> validate(); + + void updateSkMesh(GrDirectContext* context) const { + GrDirectContext::DirectContextID genId = GrDirectContext::DirectContextID(); + if (context) { + genId = context->directContextID(); + } + + if (mIsDirty || genId != mGenerationId) { + auto vb = SkMesh::MakeVertexBuffer( + context, reinterpret_cast<const void*>(mVertexBufferData.data()), + mVertexBufferData.size()); + auto meshMode = SkMesh::Mode(mMode); + if (!mIndexBufferData.empty()) { + auto ib = SkMesh::MakeIndexBuffer( + context, reinterpret_cast<const void*>(mIndexBufferData.data()), + mIndexBufferData.size()); + mMesh = SkMesh::MakeIndexed(mMeshSpec, meshMode, vb, mVertexCount, mVertexOffset, + ib, mIndexCount, mIndexOffset, mBuilder->fUniforms, + mBounds) + .mesh; + } else { + mMesh = SkMesh::Make(mMeshSpec, meshMode, vb, mVertexCount, mVertexOffset, + mBuilder->fUniforms, mBounds) + .mesh; + } + mIsDirty = false; + mGenerationId = genId; + } + } + + SkMesh& getSkMesh() const { + LOG_FATAL_IF(mIsDirty, + "Attempt to obtain SkMesh when Mesh is dirty, did you " + "forget to call updateSkMesh with a GrDirectContext? " + "Defensively creating a CPU mesh"); + return mMesh; + } + + void markDirty() { mIsDirty = true; } + + MeshUniformBuilder* uniformBuilder() { return mBuilder.get(); } + +private: + sk_sp<SkMeshSpecification> mMeshSpec; + int mMode = 0; + + std::vector<uint8_t> mVertexBufferData; + size_t mVertexCount = 0; + size_t mVertexOffset = 0; + + std::vector<uint8_t> mIndexBufferData; + size_t mIndexCount = 0; + size_t mIndexOffset = 0; + + std::unique_ptr<MeshUniformBuilder> mBuilder; + SkRect mBounds{}; + + mutable SkMesh mMesh{}; + mutable bool mIsDirty = true; + mutable GrDirectContext::DirectContextID mGenerationId = GrDirectContext::DirectContextID(); +}; +#endif // MESH_H_ diff --git a/libs/hwui/Properties.cpp b/libs/hwui/Properties.cpp index 5a67eb9935dd..7af6efb7da41 100644 --- a/libs/hwui/Properties.cpp +++ b/libs/hwui/Properties.cpp @@ -82,11 +82,17 @@ bool Properties::isolatedProcess = false; int Properties::contextPriority = 0; float Properties::defaultSdrWhitePoint = 200.f; -bool Properties::useHintManager = true; +bool Properties::useHintManager = false; int Properties::targetCpuTimePercentage = 70; bool Properties::enableWebViewOverlays = true; +bool Properties::isHighEndGfx = true; +bool Properties::isLowRam = false; +bool Properties::isSystemOrPersistent = false; + +float Properties::maxHdrHeadroomOn8bit = 5.f; // TODO: Refine this number + StretchEffectBehavior Properties::stretchEffectBehavior = StretchEffectBehavior::ShaderHWUI; DrawingEnabled Properties::drawingEnabled = DrawingEnabled::NotInitialized; @@ -98,7 +104,7 @@ bool Properties::load() { debugOverdraw = false; std::string debugOverdrawProperty = base::GetProperty(PROPERTY_DEBUG_OVERDRAW, ""); if (debugOverdrawProperty != "") { - INIT_LOGD(" Overdraw debug enabled: %s", debugOverdrawProperty); + INIT_LOGD(" Overdraw debug enabled: %s", debugOverdrawProperty.c_str()); if (debugOverdrawProperty == "show") { debugOverdraw = true; overdrawColorSet = OverdrawColorSet::Default; @@ -134,16 +140,23 @@ bool Properties::load() { skpCaptureEnabled = debuggingEnabled && base::GetBoolProperty(PROPERTY_CAPTURE_SKP_ENABLED, false); SkAndroidFrameworkTraceUtil::setEnableTracing( - base::GetBoolProperty(PROPERTY_SKIA_ATRACE_ENABLED, false)); + base::GetBoolProperty(PROPERTY_SKIA_TRACING_ENABLED, false)); + SkAndroidFrameworkTraceUtil::setUsePerfettoTrackEvents( + base::GetBoolProperty(PROPERTY_SKIA_USE_PERFETTO_TRACK_EVENTS, false)); runningInEmulator = base::GetBoolProperty(PROPERTY_IS_EMULATOR, false); - useHintManager = base::GetBoolProperty(PROPERTY_USE_HINT_MANAGER, true); + useHintManager = base::GetBoolProperty(PROPERTY_USE_HINT_MANAGER, false); targetCpuTimePercentage = base::GetIntProperty(PROPERTY_TARGET_CPU_TIME_PERCENTAGE, 70); if (targetCpuTimePercentage <= 0 || targetCpuTimePercentage > 100) targetCpuTimePercentage = 70; enableWebViewOverlays = base::GetBoolProperty(PROPERTY_WEBVIEW_OVERLAYS_ENABLED, true); + auto hdrHeadroom = (float)atof(base::GetProperty(PROPERTY_8BIT_HDR_HEADROOM, "").c_str()); + if (hdrHeadroom >= 1.f) { + maxHdrHeadroomOn8bit = std::min(hdrHeadroom, 100.f); + } + // call isDrawingEnabled to force loading of the property isDrawingEnabled(); diff --git a/libs/hwui/Properties.h b/libs/hwui/Properties.h index 2f8c67903a8b..24e206bbc3b1 100644 --- a/libs/hwui/Properties.h +++ b/libs/hwui/Properties.h @@ -143,9 +143,32 @@ enum DebugLevel { #define PROPERTY_CAPTURE_SKP_ENABLED "debug.hwui.capture_skp_enabled" /** - * Allows to record Skia drawing commands with systrace. + * Allows broad recording of Skia drawing commands. + * + * If disabled, a very minimal set of trace events *may* be recorded. + * If enabled, a much broader set of trace events *may* be recorded. + * + * In either case, trace events are only recorded if an appropriately configured tracing session is + * active. + * + * Use debug.hwui.skia_use_perfetto_track_events to determine if ATrace (default) or Perfetto is + * used as the tracing backend. + */ +#define PROPERTY_SKIA_TRACING_ENABLED "debug.hwui.skia_tracing_enabled" + +/** + * Switches Skia's tracing to use Perfetto's Track Event system instead of ATrace. + * + * If disabled, ATrace will be used by default, which will record trace events from any of Skia's + * tracing categories if overall system tracing is active and the "gfx" and "view" ATrace categories + * are enabled. + * + * If enabled, then Perfetto's Track Event system will be used instead, which will only record if an + * active Perfetto tracing session is targeting the correct apps and Skia tracing categories with + * the Track Event data source enabled. This approach may be used to selectively filter out + * undesired Skia tracing categories, and events will contain more data fields. */ -#define PROPERTY_SKIA_ATRACE_ENABLED "debug.hwui.skia_atrace_enabled" +#define PROPERTY_SKIA_USE_PERFETTO_TRACK_EVENTS "debug.hwui.skia_use_perfetto_track_events" /** * Defines how many frames in a sequence to capture. @@ -193,6 +216,10 @@ enum DebugLevel { */ #define PROPERTY_DRAWING_ENABLED "debug.hwui.drawing_enabled" +#define PROPERTY_MEMORY_POLICY "debug.hwui.app_memory_policy" + +#define PROPERTY_8BIT_HDR_HEADROOM "debug.hwui.8bit_hdr_headroom" + /////////////////////////////////////////////////////////////////////////////// // Misc /////////////////////////////////////////////////////////////////////////////// @@ -292,16 +319,29 @@ public: static bool enableWebViewOverlays; + static bool isHighEndGfx; + static bool isLowRam; + static bool isSystemOrPersistent; + + static float maxHdrHeadroomOn8bit; + static StretchEffectBehavior getStretchEffectBehavior() { return stretchEffectBehavior; } static void setIsHighEndGfx(bool isHighEndGfx) { + Properties::isHighEndGfx = isHighEndGfx; stretchEffectBehavior = isHighEndGfx ? StretchEffectBehavior::ShaderHWUI : StretchEffectBehavior::UniformScale; } + static void setIsLowRam(bool isLowRam) { Properties::isLowRam = isLowRam; } + + static void setIsSystemOrPersistent(bool isSystemOrPersistent) { + Properties::isSystemOrPersistent = isSystemOrPersistent; + } + /** * Used for testing. Typical configuration of stretch behavior is done * through setIsHighEndGfx diff --git a/libs/hwui/Readback.cpp b/libs/hwui/Readback.cpp index 4cce87ad1a2f..045de35c1d97 100644 --- a/libs/hwui/Readback.cpp +++ b/libs/hwui/Readback.cpp @@ -16,12 +16,28 @@ #include "Readback.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 "include/gpu/GpuTypes.h" // from Skia +#include <gui/TraceUtils.h> +#include <private/android/AHardwareBufferHelpers.h> +#include <shaders/shaders.h> #include <sync/sync.h> #include <system/window.h> -#include <gui/TraceUtils.h> #include "DeferredLayerUpdater.h" #include "Properties.h" +#include "Tonemapper.h" #include "hwui/Bitmap.h" #include "pipeline/skia/LayerDrawable.h" #include "renderthread/EglManager.h" @@ -37,8 +53,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,50 +66,88 @@ 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); + } + + { + 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); + } } - 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; + int32_t dataspace = ANativeWindow_getBuffersDataSpace(window); + + // If the application is not updating the Surface themselves, e.g., another + // process is producing buffers for the application to display, then + // ANativeWindow_getBuffersDataSpace will return an unknown answer, so grab + // the dataspace from buffer metadata instead, if it exists. + if (dataspace == 0) { + dataspace = AHardwareBuffer_getDataSpace(sourceBuffer.get()); } - sk_sp<SkColorSpace> colorSpace = DataSpaceToColorSpace( - static_cast<android_dataspace>(ANativeWindow_getBuffersDataSpace(window))); + sk_sp<SkColorSpace> colorSpace = + DataSpaceToColorSpace(static_cast<android_dataspace>(dataspace)); sk_sp<SkImage> image = 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,23 +164,26 @@ 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, + SkSurface::MakeRenderTarget(mRenderThread.getGrContext(), skgpu::Budgeted::kYes, bitmap->info(), 0, kTopLeft_GrSurfaceOrigin, nullptr); // if we can't generate a GPU surface that matches the destination bitmap (e.g. 565) then we // attempt to do the intermediate rendering step in 8888 if (!tmpSurface.get()) { SkImageInfo tmpInfo = bitmap->info().makeColorType(SkColorType::kN32_SkColorType); - tmpSurface = SkSurface::MakeRenderTarget(mRenderThread.getGrContext(), SkBudgeted::kYes, + tmpSurface = SkSurface::MakeRenderTarget(mRenderThread.getGrContext(), + skgpu::Budgeted::kYes, 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 +209,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) { @@ -186,6 +242,10 @@ CopyResult Readback::copySurfaceInto(ANativeWindow* window, const Rect& inSrcRec const bool hasBufferCrop = cropRect.left < cropRect.right && cropRect.top < cropRect.bottom; auto constraint = hasBufferCrop ? SkCanvas::kStrict_SrcRectConstraint : SkCanvas::kFast_SrcRectConstraint; + + static constexpr float kMaxLuminanceNits = 4000.f; + tonemapPaint(image->imageInfo(), canvas->imageInfo(), kMaxLuminanceNits, paint); + canvas->drawImageRect(image, imageSrcRect, imageDstRect, sampling, &paint, constraint); canvas->restore(); @@ -198,52 +258,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 +302,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()); @@ -326,14 +347,17 @@ bool Readback::copyLayerInto(Layer* layer, const SkRect* srcRect, const SkRect* * software buffer. */ sk_sp<SkSurface> tmpSurface = SkSurface::MakeRenderTarget(mRenderThread.getGrContext(), - SkBudgeted::kYes, bitmap->info(), 0, + skgpu::Budgeted::kYes, + bitmap->info(), + 0, kTopLeft_GrSurfaceOrigin, nullptr); // if we can't generate a GPU surface that matches the destination bitmap (e.g. 565) then we // attempt to do the intermediate rendering step in 8888 if (!tmpSurface.get()) { SkImageInfo tmpInfo = bitmap->info().makeColorType(SkColorType::kN32_SkColorType); - tmpSurface = SkSurface::MakeRenderTarget(mRenderThread.getGrContext(), SkBudgeted::kYes, + tmpSurface = SkSurface::MakeRenderTarget(mRenderThread.getGrContext(), + skgpu::Budgeted::kYes, tmpInfo, 0, kTopLeft_GrSurfaceOrigin, nullptr); if (!tmpSurface.get()) { ALOGW("Unable to generate GPU buffer in a format compatible with the provided bitmap"); 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..0b58406516e3 100644 --- a/libs/hwui/RecordingCanvas.cpp +++ b/libs/hwui/RecordingCanvas.cpp @@ -15,13 +15,18 @@ */ #include "RecordingCanvas.h" -#include <hwui/Paint.h> #include <GrRecordingContext.h> +#include <SkMesh.h> +#include <hwui/Paint.h> +#include <log/log.h> #include <experimental/type_traits> +#include <utility> +#include "Mesh.h" #include "SkAndroidFrameworkUtils.h" +#include "SkBlendMode.h" #include "SkCanvas.h" #include "SkCanvasPriv.h" #include "SkColor.h" @@ -29,14 +34,21 @@ #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" +#include "Tonemapper.h" #include "VectorDrawable.h" +#include "effects/GainmapRenderer.h" +#include "include/gpu/GpuTypes.h" // from Skia +#include "include/gpu/GrDirectContext.h" #include "pipeline/skia/AnimatedDrawables.h" #include "pipeline/skia/FunctorDrawable.h" @@ -58,16 +70,24 @@ static void copy_v(void* dst) {} template <typename S, typename... Rest> static void copy_v(void* dst, const S* src, int n, Rest&&... rest) { - SkASSERTF(((uintptr_t)dst & (alignof(S) - 1)) == 0, - "Expected %p to be aligned for at least %zu bytes.", dst, alignof(S)); - sk_careful_memcpy(dst, src, n * sizeof(S)); - copy_v(SkTAddOffset<void>(dst, n * sizeof(S)), std::forward<Rest>(rest)...); + LOG_FATAL_IF(((uintptr_t)dst & (alignof(S) - 1)) != 0, + "Expected %p to be aligned for at least %zu bytes.", + dst, alignof(S)); + // If n is 0, there is nothing to copy into dst from src. + if (n > 0) { + memcpy(dst, src, n * sizeof(S)); + dst = reinterpret_cast<void*>( + reinterpret_cast<uint8_t*>(dst) + n * sizeof(S)); + } + // Repeat for the next items, if any + copy_v(dst, std::forward<Rest>(rest)...); } // Helper for getting back at arrays which have been copy_v'd together after an Op. template <typename D, typename T> static const D* pod(const T* op, size_t offset = 0) { - return SkTAddOffset<const D>(op + 1, offset); + return reinterpret_cast<const D*>( + reinterpret_cast<const uint8_t*>(op + 1) + offset); } namespace { @@ -266,7 +286,6 @@ struct DrawDRRect final : Op { SkPaint paint; void draw(SkCanvas* c, const SkMatrix&) const { c->drawDRRect(outer, inner, paint); } }; - struct DrawAnnotation final : Op { static const auto kType = Type::DrawAnnotation; DrawAnnotation(const SkRect& rect, SkData* value) : rect(rect), value(sk_ref_sp(value)) {} @@ -315,9 +334,15 @@ struct DrawPicture final : Op { struct DrawImage final : Op { static const auto kType = Type::DrawImage; - DrawImage(sk_sp<const SkImage>&& image, SkScalar x, SkScalar y, - const SkSamplingOptions& sampling, const SkPaint* paint, BitmapPalette palette) - : image(std::move(image)), x(x), y(y), sampling(sampling), palette(palette) { + DrawImage(DrawImagePayload&& payload, SkScalar x, SkScalar y, const SkSamplingOptions& sampling, + const SkPaint* paint) + : image(std::move(payload.image)) + , x(x) + , y(y) + , sampling(sampling) + , palette(payload.palette) + , gainmap(std::move(payload.gainmapImage)) + , gainmapInfo(payload.gainmapInfo) { if (paint) { this->paint = *paint; } @@ -327,17 +352,34 @@ struct DrawImage final : Op { SkSamplingOptions sampling; SkPaint paint; BitmapPalette palette; + sk_sp<const SkImage> gainmap; + SkGainmapInfo gainmapInfo; + void draw(SkCanvas* c, const SkMatrix&) const { - c->drawImage(image.get(), x, y, sampling, &paint); + if (gainmap) { + SkRect src = SkRect::MakeWH(image->width(), image->height()); + SkRect dst = SkRect::MakeXYWH(x, y, src.width(), src.height()); + DrawGainmapBitmap(c, image, src, dst, sampling, &paint, + SkCanvas::kFast_SrcRectConstraint, gainmap, gainmapInfo); + } else { + SkPaint newPaint = paint; + tonemapPaint(image->imageInfo(), c->imageInfo(), -1, newPaint); + c->drawImage(image.get(), x, y, sampling, &newPaint); + } } }; struct DrawImageRect final : Op { static const auto kType = Type::DrawImageRect; - DrawImageRect(sk_sp<const SkImage>&& image, const SkRect* src, const SkRect& dst, + DrawImageRect(DrawImagePayload&& payload, const SkRect* src, const SkRect& dst, const SkSamplingOptions& sampling, const SkPaint* paint, - SkCanvas::SrcRectConstraint constraint, BitmapPalette palette) - : image(std::move(image)), dst(dst), sampling(sampling), constraint(constraint) - , palette(palette) { + SkCanvas::SrcRectConstraint constraint) + : image(std::move(payload.image)) + , dst(dst) + , sampling(sampling) + , constraint(constraint) + , palette(payload.palette) + , gainmap(std::move(payload.gainmapImage)) + , gainmapInfo(payload.gainmapInfo) { this->src = src ? *src : SkRect::MakeIWH(this->image->width(), this->image->height()); if (paint) { this->paint = *paint; @@ -349,23 +391,32 @@ struct DrawImageRect final : Op { SkPaint paint; SkCanvas::SrcRectConstraint constraint; BitmapPalette palette; + sk_sp<const SkImage> gainmap; + SkGainmapInfo gainmapInfo; + void draw(SkCanvas* c, const SkMatrix&) const { - c->drawImageRect(image.get(), src, dst, sampling, &paint, constraint); + if (gainmap) { + DrawGainmapBitmap(c, image, src, dst, sampling, &paint, constraint, gainmap, + gainmapInfo); + } else { + SkPaint newPaint = paint; + tonemapPaint(image->imageInfo(), c->imageInfo(), -1, newPaint); + c->drawImageRect(image.get(), src, dst, sampling, &newPaint, constraint); + } } }; struct DrawImageLattice final : Op { static const auto kType = Type::DrawImageLattice; - DrawImageLattice(sk_sp<const SkImage>&& image, int xs, int ys, int fs, const SkIRect& src, - const SkRect& dst, SkFilterMode filter, const SkPaint* paint, - BitmapPalette palette) - : image(std::move(image)) + DrawImageLattice(DrawImagePayload&& payload, int xs, int ys, int fs, const SkIRect& src, + const SkRect& dst, SkFilterMode filter, const SkPaint* paint) + : image(std::move(payload.image)) , xs(xs) , ys(ys) , fs(fs) , src(src) , dst(dst) , filter(filter) - , palette(palette) { + , palette(payload.palette) { if (paint) { this->paint = *paint; } @@ -378,13 +429,17 @@ struct DrawImageLattice final : Op { SkPaint paint; BitmapPalette palette; void draw(SkCanvas* c, const SkMatrix&) const { + // TODO: Support drawing a gainmap 9-patch? + auto xdivs = pod<int>(this, 0), ydivs = pod<int>(this, xs * sizeof(int)); auto colors = (0 == fs) ? nullptr : pod<SkColor>(this, (xs + ys) * sizeof(int)); auto flags = (0 == fs) ? nullptr : pod<SkCanvas::Lattice::RectType>( this, (xs + ys) * sizeof(int) + fs * sizeof(SkColor)); - c->drawImageLattice(image.get(), {xdivs, ydivs, flags, xs, ys, &src, colors}, dst, - filter, &paint); + SkPaint newPaint = paint; + tonemapPaint(image->imageInfo(), c->imageInfo(), -1, newPaint); + c->drawImageLattice(image.get(), {xdivs, ydivs, flags, xs, ys, &src, colors}, dst, filter, + &newPaint); } }; @@ -448,6 +503,60 @@ struct DrawVertices final : Op { c->drawVertices(vertices, mode, paint); } }; +struct DrawSkMesh final : Op { + static const auto kType = Type::DrawSkMesh; + DrawSkMesh(const SkMesh& mesh, sk_sp<SkBlender> blender, const SkPaint& paint) + : cpuMesh(mesh), blender(std::move(blender)), paint(paint) { + isGpuBased = false; + } + + const SkMesh& cpuMesh; + mutable SkMesh gpuMesh; + sk_sp<SkBlender> blender; + SkPaint paint; + mutable bool isGpuBased; + mutable GrDirectContext::DirectContextID contextId; + void draw(SkCanvas* c, const SkMatrix&) const { + GrDirectContext* directContext = c->recordingContext()->asDirectContext(); + + GrDirectContext::DirectContextID id = directContext->directContextID(); + if (!isGpuBased || contextId != id) { + sk_sp<SkMesh::VertexBuffer> vb = + SkMesh::CopyVertexBuffer(directContext, cpuMesh.refVertexBuffer()); + if (!cpuMesh.indexBuffer()) { + gpuMesh = SkMesh::Make(cpuMesh.refSpec(), cpuMesh.mode(), vb, cpuMesh.vertexCount(), + cpuMesh.vertexOffset(), cpuMesh.refUniforms(), + cpuMesh.bounds()) + .mesh; + } else { + sk_sp<SkMesh::IndexBuffer> ib = + SkMesh::CopyIndexBuffer(directContext, cpuMesh.refIndexBuffer()); + gpuMesh = SkMesh::MakeIndexed(cpuMesh.refSpec(), cpuMesh.mode(), vb, + cpuMesh.vertexCount(), cpuMesh.vertexOffset(), ib, + cpuMesh.indexCount(), cpuMesh.indexOffset(), + cpuMesh.refUniforms(), cpuMesh.bounds()) + .mesh; + } + + isGpuBased = true; + contextId = id; + } + + c->drawMesh(gpuMesh, blender, paint); + } +}; + +struct DrawMesh final : Op { + static const auto kType = Type::DrawMesh; + DrawMesh(const Mesh& mesh, sk_sp<SkBlender> blender, const SkPaint& paint) + : mesh(mesh), blender(std::move(blender)), paint(paint) {} + + const Mesh& mesh; + sk_sp<SkBlender> blender; + SkPaint paint; + + void draw(SkCanvas* c, const SkMatrix&) const { c->drawMesh(mesh.getSkMesh(), blender, paint); } +}; struct DrawAtlas final : Op { static const auto kType = Type::DrawAtlas; DrawAtlas(const SkImage* atlas, int count, SkBlendMode mode, const SkSamplingOptions& sampling, @@ -554,7 +663,7 @@ public: GrRecordingContext* directContext = c->recordingContext(); mLayerImageInfo = c->imageInfo().makeWH(deviceBounds.width(), deviceBounds.height()); - mLayerSurface = SkSurface::MakeRenderTarget(directContext, SkBudgeted::kYes, + mLayerSurface = SkSurface::MakeRenderTarget(directContext, skgpu::Budgeted::kYes, mLayerImageInfo, 0, kTopLeft_GrSurfaceOrigin, nullptr); } @@ -589,18 +698,23 @@ public: }; } +static constexpr inline bool is_power_of_two(int value) { + return (value & (value - 1)) == 0; +} + template <typename T, typename... Args> void* DisplayListData::push(size_t pod, Args&&... args) { size_t skip = SkAlignPtr(sizeof(T) + pod); - SkASSERT(skip < (1 << 24)); + LOG_FATAL_IF(skip >= (1 << 24)); if (fUsed + skip > fReserved) { - static_assert(SkIsPow2(SKLITEDL_PAGE), "This math needs updating for non-pow2."); + static_assert(is_power_of_two(SKLITEDL_PAGE), + "This math needs updating for non-pow2."); // Next greater multiple of SKLITEDL_PAGE. fReserved = (fUsed + skip + SKLITEDL_PAGE) & ~(SKLITEDL_PAGE - 1); fBytes.realloc(fReserved); LOG_ALWAYS_FATAL_IF(fBytes.get() == nullptr, "realloc(%zd) failed", fReserved); } - SkASSERT(fUsed + skip <= fReserved); + LOG_FATAL_IF((fUsed + skip) > fReserved); auto op = (T*)(fBytes.get() + fUsed); fUsed += skip; new (op) T{std::forward<Args>(args)...}; @@ -712,27 +826,25 @@ void DisplayListData::drawPicture(const SkPicture* picture, const SkMatrix* matr const SkPaint* paint) { this->push<DrawPicture>(0, picture, matrix, paint); } -void DisplayListData::drawImage(sk_sp<const SkImage> image, SkScalar x, SkScalar y, - const SkSamplingOptions& sampling, const SkPaint* paint, - BitmapPalette palette) { - this->push<DrawImage>(0, std::move(image), x, y, sampling, paint, palette); +void DisplayListData::drawImage(DrawImagePayload&& payload, SkScalar x, SkScalar y, + const SkSamplingOptions& sampling, const SkPaint* paint) { + this->push<DrawImage>(0, std::move(payload), x, y, sampling, paint); } -void DisplayListData::drawImageRect(sk_sp<const SkImage> image, const SkRect* src, +void DisplayListData::drawImageRect(DrawImagePayload&& payload, const SkRect* src, const SkRect& dst, const SkSamplingOptions& sampling, - const SkPaint* paint, SkCanvas::SrcRectConstraint constraint, - BitmapPalette palette) { - this->push<DrawImageRect>(0, std::move(image), src, dst, sampling, paint, constraint, palette); + const SkPaint* paint, SkCanvas::SrcRectConstraint constraint) { + this->push<DrawImageRect>(0, std::move(payload), src, dst, sampling, paint, constraint); } -void DisplayListData::drawImageLattice(sk_sp<const SkImage> image, const SkCanvas::Lattice& lattice, - const SkRect& dst, SkFilterMode filter, const SkPaint* paint, - BitmapPalette palette) { +void DisplayListData::drawImageLattice(DrawImagePayload&& payload, const SkCanvas::Lattice& lattice, + const SkRect& dst, SkFilterMode filter, + const SkPaint* paint) { int xs = lattice.fXCount, ys = lattice.fYCount; int fs = lattice.fRectTypes ? (xs + 1) * (ys + 1) : 0; size_t bytes = (xs + ys) * sizeof(int) + fs * sizeof(SkCanvas::Lattice::RectType) + fs * sizeof(SkColor); - SkASSERT(lattice.fBounds); - void* pod = this->push<DrawImageLattice>(bytes, std::move(image), xs, ys, fs, *lattice.fBounds, - dst, filter, paint, palette); + LOG_FATAL_IF(!lattice.fBounds); + void* pod = this->push<DrawImageLattice>(bytes, std::move(payload), xs, ys, fs, + *lattice.fBounds, dst, filter, paint); copy_v(pod, lattice.fXDivs, xs, lattice.fYDivs, ys, lattice.fColors, fs, lattice.fRectTypes, fs); } @@ -759,6 +871,14 @@ void DisplayListData::drawPoints(SkCanvas::PointMode mode, size_t count, const S void DisplayListData::drawVertices(const SkVertices* vert, SkBlendMode mode, const SkPaint& paint) { this->push<DrawVertices>(0, vert, mode, paint); } +void DisplayListData::drawMesh(const SkMesh& mesh, const sk_sp<SkBlender>& blender, + const SkPaint& paint) { + this->push<DrawSkMesh>(0, mesh, blender, paint); +} +void DisplayListData::drawMesh(const Mesh& mesh, const sk_sp<SkBlender>& blender, + const SkPaint& paint) { + this->push<DrawMesh>(0, mesh, blender, paint); +} void DisplayListData::drawAtlas(const SkImage* atlas, const SkRSXform xforms[], const SkRect texs[], const SkColor colors[], int count, SkBlendMode xfermode, const SkSamplingOptions& sampling, const SkRect* cull, @@ -1035,57 +1155,55 @@ void RecordingCanvas::drawRippleDrawable(const skiapipeline::RippleDrawableParam fDL->drawRippleDrawable(params); } -void RecordingCanvas::drawImage(const sk_sp<SkImage>& image, SkScalar x, SkScalar y, - const SkSamplingOptions& sampling, const SkPaint* paint, - BitmapPalette palette) { - fDL->drawImage(image, x, y, sampling, paint, palette); +void RecordingCanvas::drawImage(DrawImagePayload&& payload, SkScalar x, SkScalar y, + const SkSamplingOptions& sampling, const SkPaint* paint) { + fDL->drawImage(std::move(payload), x, y, sampling, paint); } -void RecordingCanvas::drawImageRect(const sk_sp<SkImage>& image, const SkRect& src, +void RecordingCanvas::drawImageRect(DrawImagePayload&& payload, const SkRect& src, const SkRect& dst, const SkSamplingOptions& sampling, - const SkPaint* paint, SrcRectConstraint constraint, - BitmapPalette palette) { - fDL->drawImageRect(image, &src, dst, sampling, paint, constraint, palette); + const SkPaint* paint, SrcRectConstraint constraint) { + fDL->drawImageRect(std::move(payload), &src, dst, sampling, paint, constraint); } -void RecordingCanvas::drawImageLattice(const sk_sp<SkImage>& image, const Lattice& lattice, - const SkRect& dst, SkFilterMode filter, const SkPaint* paint, - BitmapPalette palette) { - if (!image || dst.isEmpty()) { +void RecordingCanvas::drawImageLattice(DrawImagePayload&& payload, const Lattice& lattice, + const SkRect& dst, SkFilterMode filter, + const SkPaint* paint) { + if (!payload.image || dst.isEmpty()) { return; } SkIRect bounds; Lattice latticePlusBounds = lattice; if (!latticePlusBounds.fBounds) { - bounds = SkIRect::MakeWH(image->width(), image->height()); + bounds = SkIRect::MakeWH(payload.image->width(), payload.image->height()); latticePlusBounds.fBounds = &bounds; } - if (SkLatticeIter::Valid(image->width(), image->height(), latticePlusBounds)) { - fDL->drawImageLattice(image, latticePlusBounds, dst, filter, paint, palette); + if (SkLatticeIter::Valid(payload.image->width(), payload.image->height(), latticePlusBounds)) { + fDL->drawImageLattice(std::move(payload), latticePlusBounds, dst, filter, paint); } else { SkSamplingOptions sampling(filter, SkMipmapMode::kNone); - fDL->drawImageRect(image, nullptr, dst, sampling, paint, kFast_SrcRectConstraint, palette); + fDL->drawImageRect(std::move(payload), nullptr, dst, sampling, paint, + kFast_SrcRectConstraint); } } void RecordingCanvas::onDrawImage2(const SkImage* img, SkScalar x, SkScalar y, const SkSamplingOptions& sampling, const SkPaint* paint) { - fDL->drawImage(sk_ref_sp(img), x, y, sampling, paint, BitmapPalette::Unknown); + fDL->drawImage(DrawImagePayload(img), x, y, sampling, paint); } void RecordingCanvas::onDrawImageRect2(const SkImage* img, const SkRect& src, const SkRect& dst, const SkSamplingOptions& sampling, const SkPaint* paint, SrcRectConstraint constraint) { - fDL->drawImageRect(sk_ref_sp(img), &src, dst, sampling, paint, constraint, - BitmapPalette::Unknown); + fDL->drawImageRect(DrawImagePayload(img), &src, dst, sampling, paint, constraint); } void RecordingCanvas::onDrawImageLattice2(const SkImage* img, const SkCanvas::Lattice& lattice, const SkRect& dst, SkFilterMode filter, const SkPaint* paint) { - fDL->drawImageLattice(sk_ref_sp(img), lattice, dst, filter, paint, BitmapPalette::Unknown); + fDL->drawImageLattice(DrawImagePayload(img), lattice, dst, filter, paint); } void RecordingCanvas::onDrawPatch(const SkPoint cubics[12], const SkColor colors[4], @@ -1101,6 +1219,13 @@ void RecordingCanvas::onDrawVerticesObject(const SkVertices* vertices, SkBlendMode mode, const SkPaint& paint) { fDL->drawVertices(vertices, mode, paint); } +void RecordingCanvas::onDrawMesh(const SkMesh& mesh, sk_sp<SkBlender> blender, + const SkPaint& paint) { + fDL->drawMesh(mesh, blender, paint); +} +void RecordingCanvas::drawMesh(const Mesh& mesh, sk_sp<SkBlender> blender, const SkPaint& paint) { + fDL->drawMesh(mesh, blender, paint); +} void RecordingCanvas::onDrawAtlas2(const SkImage* atlas, const SkRSXform xforms[], const SkRect texs[], const SkColor colors[], int count, SkBlendMode bmode, const SkSamplingOptions& sampling, @@ -1119,5 +1244,14 @@ void RecordingCanvas::drawWebView(skiapipeline::FunctorDrawable* drawable) { fDL->drawWebView(drawable); } +[[nodiscard]] const SkMesh& DrawMeshPayload::getSkMesh() const { + LOG_FATAL_IF(!meshWrapper && !mesh, "One of Mesh or Mesh must be non-null"); + if (meshWrapper) { + return meshWrapper->getSkMesh(); + } else { + return *mesh; + } +} + } // namespace uirenderer } // namespace android diff --git a/libs/hwui/RecordingCanvas.h b/libs/hwui/RecordingCanvas.h index 212b4e72dcb2..1f4ba5d6d557 100644 --- a/libs/hwui/RecordingCanvas.h +++ b/libs/hwui/RecordingCanvas.h @@ -16,23 +16,32 @@ #pragma once +#include <SkCanvas.h> +#include <SkCanvasVirtualEnforcer.h> +#include <SkDrawable.h> +#include <SkGainmapInfo.h> +#include <SkNoDrawCanvas.h> +#include <SkPaint.h> +#include <SkPath.h> +#include <SkRect.h> +#include <SkRuntimeEffect.h> +#include <log/log.h> + +#include <cstdlib> +#include <utility> +#include <vector> + #include "CanvasTransform.h" +#include "Gainmap.h" #include "hwui/Bitmap.h" +#include "pipeline/skia/AnimatedDrawables.h" +#include "utils/AutoMalloc.h" #include "utils/Macros.h" #include "utils/TypeLogic.h" -#include "SkCanvas.h" -#include "SkCanvasVirtualEnforcer.h" -#include "SkDrawable.h" -#include "SkNoDrawCanvas.h" -#include "SkPaint.h" -#include "SkPath.h" -#include "SkRect.h" - -#include "pipeline/skia/AnimatedDrawables.h" - -#include <SkRuntimeEffect.h> -#include <vector> +enum class SkBlendMode; +class SkRRect; +class Mesh; namespace android { namespace uirenderer { @@ -59,6 +68,44 @@ struct DisplayListOp { static_assert(sizeof(DisplayListOp) == 4); +class DrawMeshPayload { +public: + explicit DrawMeshPayload(const SkMesh* mesh) : mesh(mesh) {} + explicit DrawMeshPayload(const Mesh* meshWrapper) : meshWrapper(meshWrapper) {} + + [[nodiscard]] const SkMesh& getSkMesh() const; + +private: + const SkMesh* mesh = nullptr; + const Mesh* meshWrapper = nullptr; +}; + +struct DrawImagePayload { + explicit DrawImagePayload(Bitmap& bitmap) + : image(bitmap.makeImage()), palette(bitmap.palette()) { + if (bitmap.hasGainmap()) { + auto gainmap = bitmap.gainmap(); + gainmapInfo = gainmap->info; + gainmapImage = gainmap->bitmap->makeImage(); + } + } + + explicit DrawImagePayload(const SkImage* image) + : image(sk_ref_sp(image)), palette(BitmapPalette::Unknown) {} + + DrawImagePayload(const DrawImagePayload&) = default; + DrawImagePayload(DrawImagePayload&&) = default; + DrawImagePayload& operator=(const DrawImagePayload&) = default; + DrawImagePayload& operator=(DrawImagePayload&&) = default; + ~DrawImagePayload() = default; + + sk_sp<SkImage> image; + BitmapPalette palette; + + sk_sp<SkImage> gainmapImage; + SkGainmapInfo gainmapInfo; +}; + class RecordingCanvas; class DisplayListData final { @@ -109,19 +156,21 @@ private: void drawRRect(const SkRRect&, const SkPaint&); void drawDRRect(const SkRRect&, const SkRRect&, const SkPaint&); + void drawMesh(const SkMesh&, const sk_sp<SkBlender>&, const SkPaint&); + void drawMesh(const Mesh&, const sk_sp<SkBlender>&, const SkPaint&); + void drawAnnotation(const SkRect&, const char*, SkData*); void drawDrawable(SkDrawable*, const SkMatrix*); void drawPicture(const SkPicture*, const SkMatrix*, const SkPaint*); void drawTextBlob(const SkTextBlob*, SkScalar, SkScalar, const SkPaint&); - void drawImage(sk_sp<const SkImage>, SkScalar, SkScalar, const SkSamplingOptions&, - const SkPaint*, BitmapPalette palette); - void drawImageNine(sk_sp<const SkImage>, const SkIRect&, const SkRect&, const SkPaint*); - void drawImageRect(sk_sp<const SkImage>, const SkRect*, const SkRect&, const SkSamplingOptions&, - const SkPaint*, SkCanvas::SrcRectConstraint, BitmapPalette palette); - void drawImageLattice(sk_sp<const SkImage>, const SkCanvas::Lattice&, const SkRect&, - SkFilterMode, const SkPaint*, BitmapPalette); + void drawImage(DrawImagePayload&&, SkScalar, SkScalar, const SkSamplingOptions&, + const SkPaint*); + void drawImageRect(DrawImagePayload&&, const SkRect*, const SkRect&, const SkSamplingOptions&, + const SkPaint*, SkCanvas::SrcRectConstraint); + void drawImageLattice(DrawImagePayload&&, const SkCanvas::Lattice&, const SkRect&, SkFilterMode, + const SkPaint*); void drawPatch(const SkPoint[12], const SkColor[4], const SkPoint[4], SkBlendMode, const SkPaint&); @@ -140,7 +189,7 @@ private: template <typename Fn, typename... Args> void map(const Fn[], Args...) const; - SkAutoTMalloc<uint8_t> fBytes; + AutoTMalloc<uint8_t> fBytes; size_t fUsed = 0; size_t fReserved = 0; @@ -188,14 +237,14 @@ public: void onDrawTextBlob(const SkTextBlob*, SkScalar, SkScalar, const SkPaint&) override; - void drawImage(const sk_sp<SkImage>&, SkScalar left, SkScalar top, const SkSamplingOptions&, - const SkPaint* paint, BitmapPalette pallete); void drawRippleDrawable(const skiapipeline::RippleDrawableParams& params); - void drawImageRect(const sk_sp<SkImage>& image, const SkRect& src, const SkRect& dst, - const SkSamplingOptions&, const SkPaint*, SrcRectConstraint, BitmapPalette); - void drawImageLattice(const sk_sp<SkImage>& image, const Lattice& lattice, const SkRect& dst, - SkFilterMode, const SkPaint* paint, BitmapPalette palette); + void drawImage(DrawImagePayload&&, SkScalar, SkScalar, const SkSamplingOptions&, + const SkPaint*); + void drawImageRect(DrawImagePayload&&, const SkRect&, const SkRect&, const SkSamplingOptions&, + const SkPaint*, SrcRectConstraint); + void drawImageLattice(DrawImagePayload&&, const Lattice& lattice, const SkRect&, SkFilterMode, + const SkPaint*); void onDrawImage2(const SkImage*, SkScalar, SkScalar, const SkSamplingOptions&, const SkPaint*) override; @@ -208,10 +257,12 @@ public: const SkPaint&) override; void onDrawPoints(PointMode, size_t count, const SkPoint pts[], const SkPaint&) override; void onDrawVerticesObject(const SkVertices*, SkBlendMode, const SkPaint&) override; + void onDrawMesh(const SkMesh&, sk_sp<SkBlender>, const SkPaint&) override; void onDrawAtlas2(const SkImage*, const SkRSXform[], const SkRect[], const SkColor[], int, SkBlendMode, const SkSamplingOptions&, const SkRect*, const SkPaint*) override; void onDrawShadowRec(const SkPath&, const SkDrawShadowRec&) override; + void drawMesh(const Mesh& mesh, sk_sp<SkBlender> blender, const SkPaint& paint); void drawVectorDrawable(VectorDrawableRoot* tree); void drawWebView(skiapipeline::FunctorDrawable*); diff --git a/libs/hwui/Rect.h b/libs/hwui/Rect.h index 24443c8c9836..7170226037f9 100644 --- a/libs/hwui/Rect.h +++ b/libs/hwui/Rect.h @@ -71,9 +71,14 @@ public: , right(rect.fRight) , bottom(rect.fBottom) {} - friend int operator==(const Rect& a, const Rect& b) { return !memcmp(&a, &b, sizeof(a)); } + friend int operator==(const Rect& a, const Rect& b) { + return a.left == b.left && + a.top == b.top && + a.right == b.right && + a.bottom == b.bottom; + } - friend int operator!=(const Rect& a, const Rect& b) { return memcmp(&a, &b, sizeof(a)); } + friend int operator!=(const Rect& a, const Rect& b) { return !(a == b); } inline void clear() { left = top = right = bottom = 0.0f; } 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/SafeMath.h b/libs/hwui/SafeMath.h new file mode 100644 index 000000000000..4d6adf55c0cb --- /dev/null +++ b/libs/hwui/SafeMath.h @@ -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. + */ + +#ifndef SkSafeMath_DEFINED +#define SkSafeMath_DEFINED + +#include <cstddef> +#include <cstdint> +#include <limits> + +// Copy of Skia's SafeMath API used to validate Mesh parameters to support +// deferred creation of SkMesh instances on RenderThread. +// SafeMath always check that a series of operations do not overflow. +// This must be correct for all platforms, because this is a check for safety at runtime. + +class SafeMath { +public: + SafeMath() = default; + + bool ok() const { return fOK; } + explicit operator bool() const { return fOK; } + + size_t mul(size_t x, size_t y) { + return sizeof(size_t) == sizeof(uint64_t) ? mul64(x, y) : mul32(x, y); + } + + size_t add(size_t x, size_t y) { + size_t result = x + y; + fOK &= result >= x; + return result; + } + + /** + * Return a + b, unless this result is an overflow/underflow. In those cases, fOK will + * be set to false, and it is undefined what this returns. + */ + int addInt(int a, int b) { + if (b < 0 && a < std::numeric_limits<int>::min() - b) { + fOK = false; + return a; + } else if (b > 0 && a > std::numeric_limits<int>::max() - b) { + fOK = false; + return a; + } + return a + b; + } + + // These saturate to their results + static size_t Add(size_t x, size_t y) { + SafeMath tmp; + size_t sum = tmp.add(x, y); + return tmp.ok() ? sum : SIZE_MAX; + } + + static size_t Mul(size_t x, size_t y) { + SafeMath tmp; + size_t prod = tmp.mul(x, y); + return tmp.ok() ? prod : SIZE_MAX; + } + +private: + uint32_t mul32(uint32_t x, uint32_t y) { + uint64_t bx = x; + uint64_t by = y; + uint64_t result = bx * by; + fOK &= result >> 32 == 0; + // Overflow information is capture in fOK. Return the result modulo 2^32. + return (uint32_t)result; + } + + uint64_t mul64(uint64_t x, uint64_t y) { + if (x <= std::numeric_limits<uint64_t>::max() >> 32 && + y <= std::numeric_limits<uint64_t>::max() >> 32) { + return x * y; + } else { + auto hi = [](uint64_t x) { return x >> 32; }; + auto lo = [](uint64_t x) { return x & 0xFFFFFFFF; }; + + uint64_t lx_ly = lo(x) * lo(y); + uint64_t hx_ly = hi(x) * lo(y); + uint64_t lx_hy = lo(x) * hi(y); + uint64_t hx_hy = hi(x) * hi(y); + uint64_t result = 0; + result = this->add(lx_ly, (hx_ly << 32)); + result = this->add(result, (lx_hy << 32)); + fOK &= (hx_hy + (hx_ly >> 32) + (lx_hy >> 32)) == 0; + + return result; + } + } + bool fOK = true; +}; + +#endif // SkSafeMath_DEFINED diff --git a/libs/hwui/SkiaCanvas.cpp b/libs/hwui/SkiaCanvas.cpp index 53c6db0cdf3a..7a1276982d0a 100644 --- a/libs/hwui/SkiaCanvas.cpp +++ b/libs/hwui/SkiaCanvas.cpp @@ -16,41 +16,93 @@ #include "SkiaCanvas.h" -#include "CanvasProperty.h" -#include "NinePatchUtils.h" -#include "VectorDrawable.h" -#include "hwui/Bitmap.h" -#include "hwui/MinikinUtils.h" -#include "hwui/PaintFilter.h" -#include "pipeline/skia/AnimatedDrawables.h" -#include "pipeline/skia/HolePunch.h" - #include <SkAndroidFrameworkUtils.h> #include <SkAnimatedImage.h> +#include <SkBitmap.h> +#include <SkBlendMode.h> +#include <SkCanvas.h> #include <SkCanvasPriv.h> #include <SkCanvasStateUtils.h> #include <SkColorFilter.h> -#include <SkDeque.h> #include <SkDrawable.h> #include <SkFont.h> #include <SkGraphics.h> #include <SkImage.h> #include <SkImagePriv.h> +#include <SkMatrix.h> +#include <SkPaint.h> #include <SkPicture.h> +#include <SkRRect.h> #include <SkRSXform.h> +#include <SkRect.h> +#include <SkRefCnt.h> #include <SkShader.h> -#include <SkTemplates.h> #include <SkTextBlob.h> #include <SkVertices.h> +#include <log/log.h> +#include <ui/FatVector.h> #include <memory> #include <optional> #include <utility> +#include "CanvasProperty.h" +#include "Mesh.h" +#include "NinePatchUtils.h" +#include "VectorDrawable.h" +#include "hwui/Bitmap.h" +#include "hwui/MinikinUtils.h" +#include "hwui/PaintFilter.h" +#include "pipeline/skia/AnimatedDrawables.h" +#include "pipeline/skia/HolePunch.h" + 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); } @@ -126,7 +178,7 @@ int SkiaCanvas::save(SaveFlags::Flags flags) { // operation. It does this by explicitly saving off the clip & matrix state // when requested and playing it back after the SkCanvas::restore. void SkiaCanvas::restore() { - const auto* rec = this->currentSaveRec(); + const SaveRec* rec = this->currentSaveRec(); if (!rec) { // Fast path - no record for this frame. mCanvas->restore(); @@ -194,61 +246,21 @@ 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; + const SaveRec* rec = (mSaveStack && !mSaveStack->empty()) + ? static_cast<const SaveRec*>(&mSaveStack->back()) + : nullptr; int currentSaveCount = mCanvas->getSaveCount(); - SkASSERT(!rec || currentSaveCount >= rec->saveCount); + LOG_FATAL_IF(!(!rec || currentSaveCount >= rec->saveCount)); 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); } @@ -269,13 +281,12 @@ void SkiaCanvas::recordPartialSave(SaveFlags::Flags flags) { } if (!mSaveStack) { - mSaveStack.reset(new SkDeque(sizeof(struct SaveRec), 8)); + mSaveStack.reset(new std::deque<SaveRec>()); } - SaveRec* rec = static_cast<SaveRec*>(mSaveStack->push_back()); - rec->saveCount = mCanvas->getSaveCount(); - rec->saveFlags = flags; - rec->clipIndex = mClipStack.size(); + mSaveStack->emplace_back(mCanvas->getSaveCount(), // saveCount + flags, // saveFlags + mClipStack.size()); // clipIndex } template <typename T> @@ -290,7 +301,7 @@ void SkiaCanvas::recordClip(const T& clip, SkClipOp op) { // Applies and optionally removes all clips >= index. void SkiaCanvas::applyPersistentClips(size_t clipStartIndex) { - SkASSERT(clipStartIndex <= mClipStack.size()); + LOG_FATAL_IF(clipStartIndex > mClipStack.size()); const auto begin = mClipStack.cbegin() + clipStartIndex; const auto end = mClipStack.cend(); @@ -306,7 +317,7 @@ void SkiaCanvas::applyPersistentClips(size_t clipStartIndex) { // If the current/post-restore save rec is also persisting clips, we // leave them on the stack to be reapplied part of the next restore(). // Otherwise we're done and just pop them. - const auto* rec = this->currentSaveRec(); + const SaveRec* rec = this->currentSaveRec(); if (!rec || (rec->saveFlags & SaveFlags::Clip)) { mClipStack.erase(begin, end); } @@ -562,6 +573,16 @@ void SkiaCanvas::drawVertices(const SkVertices* vertices, SkBlendMode mode, cons applyLooper(&paint, [&](const SkPaint& p) { mCanvas->drawVertices(vertices, mode, p); }); } +void SkiaCanvas::drawMesh(const Mesh& mesh, sk_sp<SkBlender> blender, const Paint& paint) { + GrDirectContext* context = nullptr; + auto recordingContext = mCanvas->recordingContext(); + if (recordingContext) { + context = recordingContext->asDirectContext(); + } + mesh.updateSkMesh(context); + mCanvas->drawMesh(mesh.getSkMesh(), blender, paint); +} + // ---------------------------------------------------------------------------- // Canvas draw operations: Bitmaps // ---------------------------------------------------------------------------- @@ -634,7 +655,7 @@ void SkiaCanvas::drawBitmapMesh(Bitmap& bitmap, int meshWidth, int meshHeight, texsPtr += 1; y += dy; } - SkASSERT(texsPtr - texs == ptCount); + LOG_FATAL_IF((texsPtr - texs) != ptCount); } // cons up indices @@ -657,14 +678,14 @@ void SkiaCanvas::drawBitmapMesh(Bitmap& bitmap, int meshWidth, int meshHeight, // bump to the next row index += 1; } - SkASSERT(indexPtr - indices == indexCount); + LOG_FATAL_IF((indexPtr - indices) != indexCount); } // double-check that we have legal indices -#ifdef SK_DEBUG +#if !defined(NDEBUG) { for (int i = 0; i < indexCount; i++) { - SkASSERT((unsigned)indices[i] < (unsigned)ptCount); + LOG_FATAL_IF((unsigned)indices[i] >= (unsigned)ptCount); } } #endif @@ -706,10 +727,12 @@ void SkiaCanvas::drawNinePatch(Bitmap& bitmap, const Res_png_9patch& chunk, floa numFlags = (lattice.fXCount + 1) * (lattice.fYCount + 1); } - SkAutoSTMalloc<25, SkCanvas::Lattice::RectType> flags(numFlags); - SkAutoSTMalloc<25, SkColor> colors(numFlags); + // Most times, we do not have very many flags/colors, so the stack allocated part of + // FatVector will save us a heap allocation. + FatVector<SkCanvas::Lattice::RectType, 25> flags(numFlags); + FatVector<SkColor, 25> colors(numFlags); if (numFlags > 0) { - NinePatchUtils::SetLatticeFlags(&lattice, flags.get(), numFlags, chunk, colors.get()); + NinePatchUtils::SetLatticeFlags(&lattice, flags.data(), numFlags, chunk, colors.data()); } lattice.fBounds = nullptr; diff --git a/libs/hwui/SkiaCanvas.h b/libs/hwui/SkiaCanvas.h index 715007cdcd3b..b785989f35cb 100644 --- a/libs/hwui/SkiaCanvas.h +++ b/libs/hwui/SkiaCanvas.h @@ -19,19 +19,21 @@ #ifdef __ANDROID__ // Layoutlib does not support hardware acceleration #include "DeferredLayerUpdater.h" #endif +#include <SkCanvas.h> + +#include <cassert> +#include <deque> +#include <optional> + #include "RenderNode.h" #include "VectorDrawable.h" +#include "hwui/BlurDrawLooper.h" #include "hwui/Canvas.h" #include "hwui/Paint.h" -#include "hwui/BlurDrawLooper.h" - -#include <SkCanvas.h> -#include <SkDeque.h> #include "pipeline/skia/AnimatedDrawables.h" -#include "src/core/SkArenaAlloc.h" -#include <cassert> -#include <optional> +enum class SkBlendMode; +class SkRRect; namespace android { @@ -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; @@ -117,8 +119,8 @@ public: virtual void drawRoundRect(float left, float top, float right, float bottom, float rx, float ry, const Paint& paint) override; - virtual void drawDoubleRoundRect(const SkRRect& outer, const SkRRect& inner, - const Paint& paint) override; + virtual void drawDoubleRoundRect(const SkRRect& outer, const SkRRect& inner, + const Paint& paint) override; virtual void drawCircle(float x, float y, float radius, const Paint& paint) override; virtual void drawOval(float left, float top, float right, float bottom, @@ -127,6 +129,7 @@ public: float sweepAngle, bool useCenter, const Paint& paint) override; virtual void drawPath(const SkPath& path, const Paint& paint) override; virtual void drawVertices(const SkVertices*, SkBlendMode, const Paint& paint) override; + virtual void drawMesh(const Mesh& mesh, sk_sp<SkBlender> blender, const Paint& paint) override; virtual void drawBitmap(Bitmap& bitmap, float left, float top, const Paint* paint) override; virtual void drawBitmap(Bitmap& bitmap, const SkMatrix& matrix, const Paint* paint) override; @@ -206,6 +209,9 @@ private: int saveCount; SaveFlags::Flags saveFlags; size_t clipIndex; + + SaveRec(int saveCount, SaveFlags::Flags saveFlags, size_t clipIndex) + : saveCount(saveCount), saveFlags(saveFlags), clipIndex(clipIndex) {} }; const SaveRec* currentSaveRec() const; @@ -219,11 +225,11 @@ private: class Clip; - std::unique_ptr<SkCanvas> mCanvasOwned; // might own a canvas we allocated - SkCanvas* mCanvas; // we do NOT own this canvas, it must survive us - // unless it is the same as mCanvasOwned.get() - std::unique_ptr<SkDeque> mSaveStack; // lazily allocated, tracks partial saves. - std::vector<Clip> mClipStack; // tracks persistent clips. + std::unique_ptr<SkCanvas> mCanvasOwned; // Might own a canvas we allocated. + SkCanvas* mCanvas; // We do NOT own this canvas, it must survive us + // unless it is the same as mCanvasOwned.get(). + std::unique_ptr<std::deque<SaveRec>> mSaveStack; // Lazily allocated, tracks partial saves. + std::vector<Clip> mClipStack; // Tracks persistent clips. sk_sp<PaintFilter> mPaintFilter; }; diff --git a/libs/hwui/SkiaInterpolator.cpp b/libs/hwui/SkiaInterpolator.cpp index 0695dd1ab218..b58f517834a3 100644 --- a/libs/hwui/SkiaInterpolator.cpp +++ b/libs/hwui/SkiaInterpolator.cpp @@ -16,12 +16,13 @@ #include "SkiaInterpolator.h" -#include "include/core/SkMath.h" +#include "include/core/SkScalar.h" +#include "include/core/SkTypes.h" #include "include/private/SkFixed.h" -#include "include/private/SkMalloc.h" -#include "include/private/SkTo.h" #include "src/core/SkTSearch.h" +#include <log/log.h> + typedef int Dot14; #define Dot14_ONE (1 << 14) #define Dot14_HALF (1 << 13) @@ -91,25 +92,23 @@ static float SkUnitCubicInterp(float value, float bx, float by, float cx, float SkiaInterpolatorBase::SkiaInterpolatorBase() { fStorage = nullptr; fTimes = nullptr; - SkDEBUGCODE(fTimesArray = nullptr;) } SkiaInterpolatorBase::~SkiaInterpolatorBase() { if (fStorage) { - sk_free(fStorage); + free(fStorage); } } void SkiaInterpolatorBase::reset(int elemCount, int frameCount) { fFlags = 0; - fElemCount = SkToU8(elemCount); - fFrameCount = SkToS16(frameCount); + fElemCount = static_cast<uint8_t>(elemCount); + fFrameCount = static_cast<int16_t>(frameCount); fRepeat = SK_Scalar1; if (fStorage) { - sk_free(fStorage); + free(fStorage); fStorage = nullptr; fTimes = nullptr; - SkDEBUGCODE(fTimesArray = nullptr); } } @@ -205,7 +204,6 @@ SkiaInterpolatorBase::Result SkiaInterpolatorBase::timeToT(SkMSec time, float* T SkiaInterpolator::SkiaInterpolator() { INHERITED::reset(0, 0); fValues = nullptr; - SkDEBUGCODE(fScalarsArray = nullptr;) } SkiaInterpolator::SkiaInterpolator(int elemCount, int frameCount) { @@ -215,13 +213,12 @@ SkiaInterpolator::SkiaInterpolator(int elemCount, int frameCount) { void SkiaInterpolator::reset(int elemCount, int frameCount) { INHERITED::reset(elemCount, frameCount); - fStorage = sk_malloc_throw((sizeof(float) * elemCount + sizeof(SkTimeCode)) * frameCount); + size_t numBytes = (sizeof(float) * elemCount + sizeof(SkTimeCode)) * frameCount; + fStorage = malloc(numBytes); + LOG_ALWAYS_FATAL_IF(!fStorage, "Failed to allocate %zu bytes in %s", + numBytes, __func__); fTimes = (SkTimeCode*)fStorage; fValues = (float*)((char*)fStorage + sizeof(SkTimeCode) * frameCount); -#ifdef SK_DEBUG - fTimesArray = (SkTimeCode(*)[10])fTimes; - fScalarsArray = (float(*)[10])fValues; -#endif } #define SK_Fixed1Third (SK_Fixed1 / 3) diff --git a/libs/hwui/SkiaInterpolator.h b/libs/hwui/SkiaInterpolator.h index c03f502528be..9422cb526a8f 100644 --- a/libs/hwui/SkiaInterpolator.h +++ b/libs/hwui/SkiaInterpolator.h @@ -17,7 +17,8 @@ #ifndef SkiaInterpolator_DEFINED #define SkiaInterpolator_DEFINED -#include "include/private/SkTo.h" +#include <cstddef> +#include <cstdint> class SkiaInterpolatorBase { public: @@ -46,7 +47,9 @@ public: @param mirror If true, the odd repeats interpolate from the last key frame and the first. */ - void setMirror(bool mirror) { fFlags = SkToU8((fFlags & ~kMirror) | (int)mirror); } + void setMirror(bool mirror) { + fFlags = static_cast<uint8_t>((fFlags & ~kMirror) | (int)mirror); + } /** Set the repeat count. The repeat count may be fractional. @param repeatCount Multiplies the total time by this scalar. @@ -57,7 +60,7 @@ public: @param reset If true, the odd repeats interpolate from the last key frame and the first. */ - void setReset(bool reset) { fFlags = SkToU8((fFlags & ~kReset) | (int)reset); } + void setReset(bool reset) { fFlags = static_cast<uint8_t>((fFlags & ~kReset) | (int)reset); } Result timeToT(uint32_t time, float* T, int* index, bool* exact) const; @@ -75,9 +78,6 @@ protected: }; SkTimeCode* fTimes; // pointer into fStorage void* fStorage; -#ifdef SK_DEBUG - SkTimeCode (*fTimesArray)[10]; -#endif }; class SkiaInterpolator : public SkiaInterpolatorBase { diff --git a/libs/hwui/TEST_MAPPING b/libs/hwui/TEST_MAPPING index b1719a979ce5..03682e82e28d 100644 --- a/libs/hwui/TEST_MAPPING +++ b/libs/hwui/TEST_MAPPING @@ -5,6 +5,9 @@ }, { "name": "CtsAccelerationTestCases" + }, + { + "name": "hwui_unit_tests" } ], "imports": [ diff --git a/libs/hwui/Tonemapper.cpp b/libs/hwui/Tonemapper.cpp new file mode 100644 index 000000000000..0d39f0e33298 --- /dev/null +++ b/libs/hwui/Tonemapper.cpp @@ -0,0 +1,118 @@ +/* + * Copyright 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. + */ + +#include "Tonemapper.h" + +#include <SkRuntimeEffect.h> +#include <log/log.h> +// libshaders only exists on Android devices +#ifdef __ANDROID__ +#include <shaders/shaders.h> +#endif + +#include "utils/Color.h" + +namespace android::uirenderer { + +namespace { + +// custom tonemapping only exists on Android devices +#ifdef __ANDROID__ +class ColorFilterRuntimeEffectBuilder : public SkRuntimeEffectBuilder { +public: + explicit ColorFilterRuntimeEffectBuilder(sk_sp<SkRuntimeEffect> effect) + : SkRuntimeEffectBuilder(std::move(effect)) {} + + sk_sp<SkColorFilter> makeColorFilter() { + return this->effect()->makeColorFilter(this->uniforms()); + } +}; + +static sk_sp<SkColorFilter> createLinearEffectColorFilter(const shaders::LinearEffect& linearEffect, + float maxDisplayLuminance, + float currentDisplayLuminanceNits, + float maxLuminance) { + auto shaderString = SkString(shaders::buildLinearEffectSkSL(linearEffect)); + auto [runtimeEffect, error] = SkRuntimeEffect::MakeForColorFilter(std::move(shaderString)); + if (!runtimeEffect) { + LOG_ALWAYS_FATAL("LinearColorFilter construction error: %s", error.c_str()); + } + + ColorFilterRuntimeEffectBuilder effectBuilder(std::move(runtimeEffect)); + + const auto uniforms = + shaders::buildLinearEffectUniforms(linearEffect, android::mat4(), maxDisplayLuminance, + currentDisplayLuminanceNits, maxLuminance); + + for (const auto& uniform : uniforms) { + effectBuilder.uniform(uniform.name.c_str()).set(uniform.value.data(), uniform.value.size()); + } + + return effectBuilder.makeColorFilter(); +} + +static ui::Dataspace extractTransfer(ui::Dataspace dataspace) { + return static_cast<ui::Dataspace>(dataspace & HAL_DATASPACE_TRANSFER_MASK); +} + +static bool isHdrDataspace(ui::Dataspace dataspace) { + const auto transfer = extractTransfer(dataspace); + + return transfer == ui::Dataspace::TRANSFER_ST2084 || transfer == ui::Dataspace::TRANSFER_HLG; +} + +static ui::Dataspace getDataspace(const SkImageInfo& image) { + return static_cast<ui::Dataspace>( + ColorSpaceToADataSpace(image.colorSpace(), image.colorType())); +} +#endif + +} // namespace + +// Given a source and destination image info, and the max content luminance, generate a tonemaping +// shader and tag it on the supplied paint. +void tonemapPaint(const SkImageInfo& source, const SkImageInfo& destination, float maxLuminanceNits, + SkPaint& paint) { +// custom tonemapping only exists on Android devices +#ifdef __ANDROID__ + const auto sourceDataspace = getDataspace(source); + const auto destinationDataspace = getDataspace(destination); + + if (extractTransfer(sourceDataspace) != extractTransfer(destinationDataspace) && + (isHdrDataspace(sourceDataspace) || isHdrDataspace(destinationDataspace))) { + const auto effect = shaders::LinearEffect{ + .inputDataspace = sourceDataspace, + .outputDataspace = destinationDataspace, + .undoPremultipliedAlpha = source.alphaType() == kPremul_SkAlphaType, + .fakeInputDataspace = destinationDataspace, + .type = shaders::LinearEffect::SkSLType::ColorFilter}; + constexpr float kMaxDisplayBrightnessNits = 1000.f; + constexpr float kCurrentDisplayBrightnessNits = 500.f; + sk_sp<SkColorFilter> colorFilter = createLinearEffectColorFilter( + effect, kMaxDisplayBrightnessNits, kCurrentDisplayBrightnessNits, maxLuminanceNits); + + if (paint.getColorFilter()) { + paint.setColorFilter(SkColorFilters::Compose(paint.refColorFilter(), colorFilter)); + } else { + paint.setColorFilter(colorFilter); + } + } +#else + return; +#endif +} + +} // namespace android::uirenderer diff --git a/libs/hwui/Tonemapper.h b/libs/hwui/Tonemapper.h new file mode 100644 index 000000000000..c0d5325fa9f8 --- /dev/null +++ b/libs/hwui/Tonemapper.h @@ -0,0 +1,28 @@ +/* + * Copyright 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 <SkCanvas.h> + +namespace android::uirenderer { + +// Given a source and destination image info, and the max content luminance, generate a tonemaping +// shader and tag it on the supplied paint. +void tonemapPaint(const SkImageInfo& source, const SkImageInfo& destination, float maxLuminanceNits, + SkPaint& paint); + +} // namespace android::uirenderer 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..770822a049b7 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); @@ -65,6 +66,7 @@ extern int register_android_graphics_fonts_FontFamily(JNIEnv* env); extern int register_android_graphics_text_LineBreaker(JNIEnv* env); extern int register_android_graphics_text_MeasuredText(JNIEnv* env); extern int register_android_graphics_text_TextShaper(JNIEnv* env); +extern int register_android_graphics_text_GraphemeBreak(JNIEnv* env); extern int register_android_util_PathParser(JNIEnv* env); extern int register_android_view_DisplayListCanvas(JNIEnv* env); @@ -100,6 +102,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)}, @@ -123,6 +126,8 @@ static const std::unordered_map<std::string, RegJNIRec> gRegJNIMap = { {"android.graphics.text.MeasuredText", REG_JNI(register_android_graphics_text_MeasuredText)}, {"android.graphics.text.TextRunShaper", REG_JNI(register_android_graphics_text_TextShaper)}, + {"android.graphics.text.GraphemeBreak", + REG_JNI(register_android_graphics_text_GraphemeBreak)}, {"android.util.PathParser", REG_JNI(register_android_util_PathParser)}, }; 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/android_paint.cpp b/libs/hwui/apex/android_paint.cpp index 70bd085343ce..cc79cba5e19c 100644 --- a/libs/hwui/apex/android_paint.cpp +++ b/libs/hwui/apex/android_paint.cpp @@ -19,6 +19,7 @@ #include "TypeCast.h" #include <hwui/Paint.h> +#include <SkBlendMode.h> using namespace android; diff --git a/libs/hwui/apex/jni_runtime.cpp b/libs/hwui/apex/jni_runtime.cpp index e1f5abd786bf..09ae7e78fe23 100644 --- a/libs/hwui/apex/jni_runtime.cpp +++ b/libs/hwui/apex/jni_runtime.cpp @@ -55,10 +55,12 @@ extern int register_android_graphics_ColorFilter(JNIEnv* env); extern int register_android_graphics_ColorSpace(JNIEnv* env); extern int register_android_graphics_DrawFilter(JNIEnv* env); extern int register_android_graphics_FontFamily(JNIEnv* env); +extern int register_android_graphics_Gainmap(JNIEnv* env); extern int register_android_graphics_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); @@ -75,11 +77,15 @@ extern int register_android_graphics_pdf_PdfRenderer(JNIEnv* env); extern int register_android_graphics_text_MeasuredText(JNIEnv* env); extern int register_android_graphics_text_LineBreaker(JNIEnv *env); extern int register_android_graphics_text_TextShaper(JNIEnv *env); +extern int register_android_graphics_text_GraphemeBreak(JNIEnv* env); +extern int register_android_graphics_MeshSpecification(JNIEnv* env); +extern int register_android_graphics_Mesh(JNIEnv* env); extern int register_android_util_PathParser(JNIEnv* env); extern int register_android_view_DisplayListCanvas(JNIEnv* env); extern int register_android_view_RenderNode(JNIEnv* env); extern int register_android_view_ThreadedRenderer(JNIEnv* env); +extern int register_android_graphics_HardwareBufferRenderer(JNIEnv* env); #ifdef NDEBUG #define REG_JNI(name) { name } @@ -94,59 +100,66 @@ 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_Gainmap), + 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_graphics_text_GraphemeBreak), + REG_JNI(register_android_graphics_MeshSpecification), + REG_JNI(register_android_graphics_Mesh), + + REG_JNI(register_android_util_PathParser), + REG_JNI(register_android_view_RenderNode), + REG_JNI(register_android_view_DisplayListCanvas), + REG_JNI(register_android_graphics_HardwareBufferRenderer), + + 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/effects/GainmapRenderer.cpp b/libs/hwui/effects/GainmapRenderer.cpp new file mode 100644 index 000000000000..bfe4eaf39e21 --- /dev/null +++ b/libs/hwui/effects/GainmapRenderer.cpp @@ -0,0 +1,304 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "GainmapRenderer.h" + +#include <SkGainmapShader.h> + +#include "Gainmap.h" +#include "Rect.h" +#include "utils/Trace.h" + +#ifdef __ANDROID__ +#include "include/core/SkColorSpace.h" +#include "include/core/SkImage.h" +#include "include/core/SkShader.h" +#include "include/effects/SkRuntimeEffect.h" +#include "include/private/SkGainmapInfo.h" +#include "renderthread/CanvasContext.h" +#include "src/core/SkColorFilterPriv.h" +#include "src/core/SkImageInfoPriv.h" +#include "src/core/SkRuntimeEffectPriv.h" +#endif + +namespace android::uirenderer { + +using namespace renderthread; + +static float getTargetHdrSdrRatio(const SkColorSpace* destColorspace) { + // We should always have a known destination colorspace. If we don't we must be in some + // legacy mode where we're lost and also definitely not going to HDR + if (destColorspace == nullptr) { + return 1.f; + } + + constexpr float GenericSdrWhiteNits = 203.f; + constexpr float maxPQLux = 10000.f; + constexpr float maxHLGLux = 1000.f; + skcms_TransferFunction destTF; + destColorspace->transferFn(&destTF); + if (skcms_TransferFunction_isPQish(&destTF)) { + return maxPQLux / GenericSdrWhiteNits; + } else if (skcms_TransferFunction_isHLGish(&destTF)) { + return maxHLGLux / GenericSdrWhiteNits; + } else { +#ifdef __ANDROID__ + CanvasContext* context = CanvasContext::getActiveContext(); + return context ? context->targetSdrHdrRatio() : 1.f; +#else + return 1.f; +#endif + } +} + +void DrawGainmapBitmap(SkCanvas* c, const sk_sp<const SkImage>& image, const SkRect& src, + const SkRect& dst, const SkSamplingOptions& sampling, const SkPaint* paint, + SkCanvas::SrcRectConstraint constraint, + const sk_sp<const SkImage>& gainmapImage, const SkGainmapInfo& gainmapInfo) { + ATRACE_CALL(); +#ifdef __ANDROID__ + auto destColorspace = c->imageInfo().refColorSpace(); + float targetSdrHdrRatio = getTargetHdrSdrRatio(destColorspace.get()); + if (targetSdrHdrRatio > 1.f && gainmapImage) { + SkPaint gainmapPaint = *paint; + float sX = gainmapImage->width() / (float)image->width(); + float sY = gainmapImage->height() / (float)image->height(); + SkRect gainmapSrc = src; + // TODO: Tweak rounding? + gainmapSrc.fLeft *= sX; + gainmapSrc.fRight *= sX; + gainmapSrc.fTop *= sY; + gainmapSrc.fBottom *= sY; + auto shader = + SkGainmapShader::Make(image, src, sampling, gainmapImage, gainmapSrc, sampling, + gainmapInfo, dst, targetSdrHdrRatio, destColorspace); + gainmapPaint.setShader(shader); + c->drawRect(dst, gainmapPaint); + } else +#endif + c->drawImageRect(image.get(), src, dst, sampling, paint, constraint); +} + +#ifdef __ANDROID__ + +static constexpr char gGainmapSKSL[] = R"SKSL( + uniform shader base; + uniform shader gainmap; + uniform colorFilter workingSpaceToLinearSrgb; + uniform half4 logRatioMin; + uniform half4 logRatioMax; + uniform half4 gainmapGamma; + uniform half4 epsilonSdr; + uniform half4 epsilonHdr; + uniform half W; + uniform int gainmapIsAlpha; + uniform int gainmapIsRed; + uniform int singleChannel; + uniform int noGamma; + + half4 toDest(half4 working) { + half4 ls = workingSpaceToLinearSrgb.eval(working); + vec3 dest = fromLinearSrgb(ls.rgb); + return half4(dest.r, dest.g, dest.b, ls.a); + } + + half4 main(float2 coord) { + half4 S = base.eval(coord); + half4 G = gainmap.eval(coord); + if (gainmapIsAlpha == 1) { + G = half4(G.a, G.a, G.a, 1.0); + } + if (gainmapIsRed == 1) { + G = half4(G.r, G.r, G.r, 1.0); + } + if (singleChannel == 1) { + half L; + if (noGamma == 1) { + L = mix(logRatioMin.r, logRatioMax.r, G.r); + } else { + L = mix(logRatioMin.r, logRatioMax.r, pow(G.r, gainmapGamma.r)); + } + half3 H = (S.rgb + epsilonSdr.rgb) * exp(L * W) - epsilonHdr.rgb; + return toDest(half4(H.r, H.g, H.b, S.a)); + } else { + half3 L; + if (noGamma == 1) { + L = mix(logRatioMin.rgb, logRatioMax.rgb, G.rgb); + } else { + L = mix(logRatioMin.rgb, logRatioMax.rgb, pow(G.rgb, gainmapGamma.rgb)); + } + half3 H = (S.rgb + epsilonSdr.rgb) * exp(L * W) - epsilonHdr.rgb; + return toDest(half4(H.r, H.g, H.b, S.a)); + } + } +)SKSL"; + +static sk_sp<SkRuntimeEffect> gainmap_apply_effect() { + static const SkRuntimeEffect* effect = []() -> SkRuntimeEffect* { + auto buildResult = SkRuntimeEffect::MakeForShader(SkString(gGainmapSKSL), {}); + if (buildResult.effect) { + return buildResult.effect.release(); + } else { + LOG_ALWAYS_FATAL("Failed to build gainmap shader: %s", buildResult.errorText.c_str()); + } + }(); + SkASSERT(effect); + return sk_ref_sp(effect); +} + +static bool all_channels_equal(const SkColor4f& c) { + return c.fR == c.fG && c.fR == c.fB; +} + +class DeferredGainmapShader { +private: + sk_sp<SkRuntimeEffect> mShader{gainmap_apply_effect()}; + SkRuntimeShaderBuilder mBuilder{mShader}; + SkGainmapInfo mGainmapInfo; + std::mutex mUniformGuard; + + void setupChildren(const sk_sp<const SkImage>& baseImage, + const sk_sp<const SkImage>& gainmapImage, SkTileMode tileModeX, + SkTileMode tileModeY, const SkSamplingOptions& samplingOptions) { + sk_sp<SkColorSpace> baseColorSpace = + baseImage->colorSpace() ? baseImage->refColorSpace() : SkColorSpace::MakeSRGB(); + + // Determine the color space in which the gainmap math is to be applied. + sk_sp<SkColorSpace> gainmapMathColorSpace = baseColorSpace->makeLinearGamma(); + + // Create a color filter to transform from the base image's color space to the color space + // in which the gainmap is to be applied. + auto colorXformSdrToGainmap = + SkColorFilterPriv::MakeColorSpaceXform(baseColorSpace, gainmapMathColorSpace); + + // The base image shader will convert into the color space in which the gainmap is applied. + auto baseImageShader = baseImage->makeRawShader(tileModeX, tileModeY, samplingOptions) + ->makeWithColorFilter(colorXformSdrToGainmap); + + // The gainmap image shader will ignore any color space that the gainmap has. + const SkMatrix gainmapRectToDstRect = + SkMatrix::RectToRect(SkRect::MakeWH(gainmapImage->width(), gainmapImage->height()), + SkRect::MakeWH(baseImage->width(), baseImage->height())); + auto gainmapImageShader = gainmapImage->makeRawShader(tileModeX, tileModeY, samplingOptions, + &gainmapRectToDstRect); + + // Create a color filter to transform from the color space in which the gainmap is applied + // to the intermediate destination color space. + auto colorXformGainmapToDst = SkColorFilterPriv::MakeColorSpaceXform( + gainmapMathColorSpace, SkColorSpace::MakeSRGBLinear()); + + mBuilder.child("base") = std::move(baseImageShader); + mBuilder.child("gainmap") = std::move(gainmapImageShader); + mBuilder.child("workingSpaceToLinearSrgb") = std::move(colorXformGainmapToDst); + } + + void setupGenericUniforms(const sk_sp<const SkImage>& gainmapImage, + const SkGainmapInfo& gainmapInfo) { + const SkColor4f logRatioMin({sk_float_log(gainmapInfo.fGainmapRatioMin.fR), + sk_float_log(gainmapInfo.fGainmapRatioMin.fG), + sk_float_log(gainmapInfo.fGainmapRatioMin.fB), 1.f}); + const SkColor4f logRatioMax({sk_float_log(gainmapInfo.fGainmapRatioMax.fR), + sk_float_log(gainmapInfo.fGainmapRatioMax.fG), + sk_float_log(gainmapInfo.fGainmapRatioMax.fB), 1.f}); + const int noGamma = gainmapInfo.fGainmapGamma.fR == 1.f && + gainmapInfo.fGainmapGamma.fG == 1.f && + gainmapInfo.fGainmapGamma.fB == 1.f; + const uint32_t colorTypeFlags = SkColorTypeChannelFlags(gainmapImage->colorType()); + const int gainmapIsAlpha = colorTypeFlags == kAlpha_SkColorChannelFlag; + const int gainmapIsRed = colorTypeFlags == kRed_SkColorChannelFlag; + const int singleChannel = all_channels_equal(gainmapInfo.fGainmapGamma) && + all_channels_equal(gainmapInfo.fGainmapRatioMin) && + all_channels_equal(gainmapInfo.fGainmapRatioMax) && + (colorTypeFlags == kGray_SkColorChannelFlag || + colorTypeFlags == kAlpha_SkColorChannelFlag || + colorTypeFlags == kRed_SkColorChannelFlag); + mBuilder.uniform("logRatioMin") = logRatioMin; + mBuilder.uniform("logRatioMax") = logRatioMax; + mBuilder.uniform("gainmapGamma") = gainmapInfo.fGainmapGamma; + mBuilder.uniform("epsilonSdr") = gainmapInfo.fEpsilonSdr; + mBuilder.uniform("epsilonHdr") = gainmapInfo.fEpsilonHdr; + mBuilder.uniform("noGamma") = noGamma; + mBuilder.uniform("singleChannel") = singleChannel; + mBuilder.uniform("gainmapIsAlpha") = gainmapIsAlpha; + mBuilder.uniform("gainmapIsRed") = gainmapIsRed; + } + + sk_sp<const SkData> build(float targetHdrSdrRatio) { + sk_sp<const SkData> uniforms; + { + // If we are called concurrently from multiple threads, we need to guard the call + // to writableUniforms() which mutates mUniform. This is otherwise safe because + // writeableUniforms() will make a copy if it's not unique before mutating + // This can happen if a BitmapShader is used on multiple canvas', such as a + // software + hardware canvas, which is otherwise valid as SkShader is "immutable" + std::lock_guard _lock(mUniformGuard); + const float Wunclamped = (sk_float_log(targetHdrSdrRatio) - + sk_float_log(mGainmapInfo.fDisplayRatioSdr)) / + (sk_float_log(mGainmapInfo.fDisplayRatioHdr) - + sk_float_log(mGainmapInfo.fDisplayRatioSdr)); + const float W = std::max(std::min(Wunclamped, 1.f), 0.f); + mBuilder.uniform("W") = W; + uniforms = mBuilder.uniforms(); + } + return uniforms; + } + +public: + explicit DeferredGainmapShader(const sk_sp<const SkImage>& image, + const sk_sp<const SkImage>& gainmapImage, + const SkGainmapInfo& gainmapInfo, SkTileMode tileModeX, + SkTileMode tileModeY, const SkSamplingOptions& sampling) { + mGainmapInfo = gainmapInfo; + setupChildren(image, gainmapImage, tileModeX, tileModeY, sampling); + setupGenericUniforms(gainmapImage, gainmapInfo); + } + + static sk_sp<SkShader> Make(const sk_sp<const SkImage>& image, + const sk_sp<const SkImage>& gainmapImage, + const SkGainmapInfo& gainmapInfo, SkTileMode tileModeX, + SkTileMode tileModeY, const SkSamplingOptions& sampling) { + auto deferredHandler = std::make_shared<DeferredGainmapShader>( + image, gainmapImage, gainmapInfo, tileModeX, tileModeY, sampling); + auto callback = + [deferredHandler](const SkRuntimeEffectPriv::UniformsCallbackContext& renderContext) + -> sk_sp<const SkData> { + return deferredHandler->build(getTargetHdrSdrRatio(renderContext.fDstColorSpace)); + }; + return SkRuntimeEffectPriv::MakeDeferredShader(deferredHandler->mShader.get(), callback, + deferredHandler->mBuilder.children()); + } +}; + +sk_sp<SkShader> MakeGainmapShader(const sk_sp<const SkImage>& image, + const sk_sp<const SkImage>& gainmapImage, + const SkGainmapInfo& gainmapInfo, SkTileMode tileModeX, + SkTileMode tileModeY, const SkSamplingOptions& sampling) { + return DeferredGainmapShader::Make(image, gainmapImage, gainmapInfo, tileModeX, tileModeY, + sampling); +} + +#else // __ANDROID__ + +sk_sp<SkShader> MakeGainmapShader(const sk_sp<const SkImage>& image, + const sk_sp<const SkImage>& gainmapImage, + const SkGainmapInfo& gainmapInfo, SkTileMode tileModeX, + SkTileMode tileModeY, const SkSamplingOptions& sampling) { + return nullptr; +} + +#endif // __ANDROID__ + +} // namespace android::uirenderer
\ No newline at end of file diff --git a/libs/hwui/effects/GainmapRenderer.h b/libs/hwui/effects/GainmapRenderer.h new file mode 100644 index 000000000000..4ed2445da17e --- /dev/null +++ b/libs/hwui/effects/GainmapRenderer.h @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <SkCanvas.h> +#include <SkGainmapInfo.h> +#include <SkImage.h> +#include <SkPaint.h> + +#include "hwui/Bitmap.h" + +namespace android::uirenderer { + +void DrawGainmapBitmap(SkCanvas* c, const sk_sp<const SkImage>& image, const SkRect& src, + const SkRect& dst, const SkSamplingOptions& sampling, const SkPaint* paint, + SkCanvas::SrcRectConstraint constraint, + const sk_sp<const SkImage>& gainmapImage, const SkGainmapInfo& gainmapInfo); + +sk_sp<SkShader> MakeGainmapShader(const sk_sp<const SkImage>& image, + const sk_sp<const SkImage>& gainmapImage, + const SkGainmapInfo& gainmapInfo, SkTileMode tileModeX, + SkTileMode tileModeY, const SkSamplingOptions& sampling); + +} // namespace android::uirenderer diff --git a/libs/hwui/hwui/Bitmap.cpp b/libs/hwui/hwui/Bitmap.cpp index 67f47580a70f..b3eaa0ce5979 100644 --- a/libs/hwui/hwui/Bitmap.cpp +++ b/libs/hwui/hwui/Bitmap.cpp @@ -34,10 +34,19 @@ #include <binder/IServiceManager.h> #endif +#include <Gainmap.h> #include <SkCanvas.h> +#include <SkColor.h> +#include <SkEncodedImageFormat.h> +#include <SkHighContrastFilter.h> +#include <SkImageEncoder.h> #include <SkImagePriv.h> +#include <SkJpegGainmapEncoder.h> +#include <SkPixmap.h> +#include <SkRect.h> +#include <SkStream.h> #include <SkWebpEncoder.h> -#include <SkHighContrastFilter.h> + #include <limits> namespace android { @@ -450,6 +459,23 @@ BitmapPalette Bitmap::computePalette(const SkImageInfo& info, const void* addr, } bool Bitmap::compress(JavaCompressFormat format, int32_t quality, SkWStream* stream) { +#ifdef __ANDROID__ // TODO: This isn't built for host for some reason? + if (hasGainmap() && format == JavaCompressFormat::Jpeg) { + SkBitmap baseBitmap = getSkBitmap(); + SkBitmap gainmapBitmap = gainmap()->bitmap->getSkBitmap(); + if (gainmapBitmap.colorType() == SkColorType::kAlpha_8_SkColorType) { + SkBitmap greyGainmap; + auto greyInfo = gainmapBitmap.info().makeColorType(SkColorType::kGray_8_SkColorType); + greyGainmap.setInfo(greyInfo, gainmapBitmap.rowBytes()); + greyGainmap.setPixelRef(sk_ref_sp(gainmapBitmap.pixelRef()), 0, 0); + gainmapBitmap = std::move(greyGainmap); + } + SkJpegEncoder::Options options{.fQuality = quality}; + return SkJpegGainmapEncoder::EncodeHDRGM(stream, baseBitmap.pixmap(), options, + gainmapBitmap.pixmap(), options, gainmap()->info); + } +#endif + SkBitmap skbitmap; getSkBitmap(&skbitmap); return compress(skbitmap, format, quality, stream); @@ -488,4 +514,14 @@ bool Bitmap::compress(const SkBitmap& bitmap, JavaCompressFormat format, return SkEncodeImage(stream, bitmap, fm, quality); } + +sp<uirenderer::Gainmap> Bitmap::gainmap() const { + LOG_ALWAYS_FATAL_IF(!hasGainmap(), "Bitmap doesn't have a gainmap"); + return mGainmap; +} + +void Bitmap::setGainmap(sp<uirenderer::Gainmap>&& gainmap) { + mGainmap = std::move(gainmap); +} + } // namespace android diff --git a/libs/hwui/hwui/Bitmap.h b/libs/hwui/hwui/Bitmap.h index 94a047c06ced..912d311c3678 100644 --- a/libs/hwui/hwui/Bitmap.h +++ b/libs/hwui/hwui/Bitmap.h @@ -19,10 +19,14 @@ #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> +#include <utils/StrongPointer.h> + +#include <optional> + #ifdef __ANDROID__ // Layoutlib does not support hardware acceleration #include <android/hardware_buffer.h> #endif @@ -47,6 +51,7 @@ enum class BitmapPalette { }; namespace uirenderer { +class Gainmap; namespace renderthread { class RenderThread; } @@ -119,6 +124,11 @@ public: void getBounds(SkRect* bounds) const; bool isHardware() const { return mPixelStorageType == PixelStorageType::Hardware; } + bool hasGainmap() const { return mGainmap.get() != nullptr; } + + sp<uirenderer::Gainmap> gainmap() const; + + void setGainmap(sp<uirenderer::Gainmap>&& gainmap); PixelStorageType pixelStorageType() const { return mPixelStorageType; } @@ -193,6 +203,8 @@ private: bool mHasHardwareMipMap = false; + sp<uirenderer::Gainmap> mGainmap; + union { struct { SkPixelRef* pixelRef; diff --git a/libs/hwui/hwui/BlurDrawLooper.cpp b/libs/hwui/hwui/BlurDrawLooper.cpp index 270d24af99fd..8b52551fc107 100644 --- a/libs/hwui/hwui/BlurDrawLooper.cpp +++ b/libs/hwui/hwui/BlurDrawLooper.cpp @@ -15,6 +15,8 @@ */ #include "BlurDrawLooper.h" +#include <SkBlurTypes.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..44ee31d34d23 100644 --- a/libs/hwui/hwui/Canvas.h +++ b/libs/hwui/hwui/Canvas.h @@ -16,23 +16,25 @@ #pragma once +#include <SaveFlags.h> +#include <SkBitmap.h> +#include <SkCanvas.h> +#include <SkMatrix.h> +#include <androidfw/ResourceTypes.h> #include <cutils/compiler.h> #include <utils/Functor.h> -#include <SaveFlags.h> -#include <androidfw/ResourceTypes.h> #include "Properties.h" #include "pipeline/skia/AnimatedDrawables.h" #include "utils/Macros.h" -#include <SkBitmap.h> -#include <SkCanvas.h> -#include <SkMatrix.h> - class SkAnimatedImage; +enum class SkBlendMode; class SkCanvasState; +class SkRRect; class SkRuntimeShaderBuilder; class SkVertices; +class Mesh; namespace minikin { class Font; @@ -151,7 +153,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 @@ -225,6 +227,7 @@ public: float sweepAngle, bool useCenter, const Paint& paint) = 0; virtual void drawPath(const SkPath& path, const Paint& paint) = 0; virtual void drawVertices(const SkVertices*, SkBlendMode, const Paint& paint) = 0; + virtual void drawMesh(const Mesh& mesh, sk_sp<SkBlender>, const Paint& paint) = 0; // Bitmap-based virtual void drawBitmap(Bitmap& bitmap, float left, float top, const Paint* paint) = 0; diff --git a/libs/hwui/hwui/ImageDecoder.cpp b/libs/hwui/hwui/ImageDecoder.cpp index dd68f825b61d..9a06be006dca 100644 --- a/libs/hwui/hwui/ImageDecoder.cpp +++ b/libs/hwui/hwui/ImageDecoder.cpp @@ -16,15 +16,32 @@ #include "ImageDecoder.h" -#include <hwui/Bitmap.h> -#include <log/log.h> - +#include <Gainmap.h> +#include <SkAlphaType.h> #include <SkAndroidCodec.h> #include <SkBitmap.h> #include <SkBlendMode.h> #include <SkCanvas.h> +#include <SkCodec.h> +#include <SkCodecAnimation.h> +#include <SkColorSpace.h> +#include <SkColorType.h> #include <SkEncodedOrigin.h> +#include <SkImageInfo.h> +#include <SkGainmapInfo.h> +#include <SkMatrix.h> #include <SkPaint.h> +#include <SkPngChunkReader.h> +#include <SkRect.h> +#include <SkRefCnt.h> +#include <SkSamplingOptions.h> +#include <SkSize.h> +#include <SkStream.h> +#include <hwui/Bitmap.h> +#include <log/log.h> +#include <utils/Trace.h> + +#include <memory> #undef LOG_TAG #define LOG_TAG "ImageDecoder" @@ -195,7 +212,7 @@ SkImageInfo ImageDecoder::getOutputInfo() const { } bool ImageDecoder::swapWidthHeight() const { - return SkEncodedOriginSwapsWidthHeight(mCodec->codec()->getOrigin()); + return SkEncodedOriginSwapsWidthHeight(getOrigin()); } int ImageDecoder::width() const { @@ -316,7 +333,7 @@ SkCodec::FrameInfo ImageDecoder::getCurrentFrameInfo() { info.fFrameRect = SkIRect::MakeSize(dims); } - if (auto origin = mCodec->codec()->getOrigin(); origin != kDefault_SkEncodedOrigin) { + if (auto origin = getOrigin(); origin != kDefault_SkEncodedOrigin) { if (SkEncodedOriginSwapsWidthHeight(origin)) { dims = swapped(dims); } @@ -400,7 +417,7 @@ SkCodec::Result ImageDecoder::decode(void* pixels, size_t rowBytes) { // FIXME: Use scanline decoding on only a couple lines to save memory. b/70709380. SkBitmap tmp; const bool scale = mDecodeSize != mTargetSize; - const auto origin = mCodec->codec()->getOrigin(); + const auto origin = getOrigin(); const bool handleOrigin = origin != kDefault_SkEncodedOrigin; SkMatrix outputMatrix; if (scale || handleOrigin || mCropRect) { @@ -455,12 +472,15 @@ SkCodec::Result ImageDecoder::decode(void* pixels, size_t rowBytes) { mOptions.fZeroInitialized = SkCodec::kYes_ZeroInitialized; } + ATRACE_BEGIN("getAndroidPixels"); auto result = mCodec->getAndroidPixels(decodeInfo, decodePixels, decodeRowBytes, &mOptions); + ATRACE_END(); // The next call to decode() may not provide zero initialized memory. mOptions.fZeroInitialized = SkCodec::kNo_ZeroInitialized; if (scale || handleOrigin || mCropRect) { + ATRACE_NAME("Handling scale/origin/crop"); SkBitmap scaledBm; if (!scaledBm.installPixels(outputInfo, pixels, rowBytes)) { return SkCodec::kInternalError; @@ -478,3 +498,82 @@ SkCodec::Result ImageDecoder::decode(void* pixels, size_t rowBytes) { return result; } +SkCodec::Result ImageDecoder::extractGainmap(Bitmap* destination, bool isShared) { + ATRACE_CALL(); + SkGainmapInfo gainmapInfo; + std::unique_ptr<SkStream> gainmapStream; + { + ATRACE_NAME("getAndroidGainmap"); + if (!mCodec->getAndroidGainmap(&gainmapInfo, &gainmapStream)) { + return SkCodec::kSuccess; + } + } + auto gainmapCodec = SkAndroidCodec::MakeFromStream(std::move(gainmapStream)); + if (!gainmapCodec) { + ALOGW("Failed to create codec for gainmap stream"); + return SkCodec::kInvalidInput; + } + ImageDecoder decoder{std::move(gainmapCodec)}; + // Gainmap inherits the origin of the containing image + decoder.mOverrideOrigin.emplace(getOrigin()); + // Update mDecodeSize / mTargetSize for the overridden origin + decoder.setTargetSize(decoder.width(), decoder.height()); + if (decoder.gray()) { + decoder.setOutColorType(kGray_8_SkColorType); + } + + const bool isScaled = width() != mTargetSize.width() || height() != mTargetSize.height(); + + if (isScaled) { + float scaleX = (float)mTargetSize.width() / width(); + float scaleY = (float)mTargetSize.height() / height(); + decoder.setTargetSize(decoder.width() * scaleX, decoder.height() * scaleY); + } + + if (mCropRect) { + float sX = decoder.mTargetSize.width() / (float)mTargetSize.width(); + float sY = decoder.mTargetSize.height() / (float)mTargetSize.height(); + SkIRect crop = *mCropRect; + // TODO: Tweak rounding? + crop.fLeft *= sX; + crop.fRight *= sX; + crop.fTop *= sY; + crop.fBottom *= sY; + decoder.setCropRect(&crop); + } + + SkImageInfo bitmapInfo = decoder.getOutputInfo(); + if (bitmapInfo.colorType() == kGray_8_SkColorType) { + bitmapInfo = bitmapInfo.makeColorType(kAlpha_8_SkColorType); + } + + SkBitmap bm; + if (!bm.setInfo(bitmapInfo)) { + ALOGE("Failed to setInfo properly"); + return SkCodec::kInternalError; + } + + sk_sp<Bitmap> nativeBitmap; + if (isShared) { + nativeBitmap = Bitmap::allocateAshmemBitmap(&bm); + } else { + nativeBitmap = Bitmap::allocateHeapBitmap(&bm); + } + if (!nativeBitmap) { + ALOGE("OOM allocating Bitmap with dimensions %i x %i", bitmapInfo.width(), + bitmapInfo.height()); + return SkCodec::kInternalError; + } + + SkCodec::Result result = decoder.decode(bm.getPixels(), bm.rowBytes()); + bm.setImmutable(); + + if (result == SkCodec::kSuccess) { + auto gainmap = sp<uirenderer::Gainmap>::make(); + gainmap->info = gainmapInfo; + gainmap->bitmap = std::move(nativeBitmap); + destination->setGainmap(std::move(gainmap)); + } + + return result; +} diff --git a/libs/hwui/hwui/ImageDecoder.h b/libs/hwui/hwui/ImageDecoder.h index cef2233fc371..b3781b52a418 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> @@ -77,6 +79,8 @@ public: // Set whether the ImageDecoder should handle RestorePrevious frames. void setHandleRestorePrevious(bool handle); + SkCodec::Result extractGainmap(Bitmap* destination, bool isShared); + private: // State machine for keeping track of how to handle RestorePrevious (RP) // frames in decode(). @@ -113,6 +117,7 @@ private: RestoreState mRestoreState; sk_sp<Bitmap> mRestoreFrame; std::optional<SkIRect> mCropRect; + std::optional<SkEncodedOrigin> mOverrideOrigin; ImageDecoder(const ImageDecoder&) = delete; ImageDecoder& operator=(const ImageDecoder&) = delete; @@ -122,6 +127,10 @@ private: bool swapWidthHeight() const; // Store/restore a frame if necessary. Returns false on error. bool handleRestorePrevious(const SkImageInfo&, void* pixels, size_t rowBytes); + + SkEncodedOrigin getOrigin() const { + return mOverrideOrigin.has_value() ? *mOverrideOrigin : mCodec->codec()->getOrigin(); + } }; } // namespace android 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..6ee7576651f2 100755..100644 --- a/libs/hwui/jni/Bitmap.cpp +++ b/libs/hwui/jni/Bitmap.cpp @@ -1,41 +1,40 @@ #undef LOG_TAG #define LOG_TAG "Bitmap" +// #define LOG_NDEBUG 0 #include "Bitmap.h" +#include <hwui/Bitmap.h> +#include <hwui/Paint.h> + +#include "CreateJavaOutputStreamAdaptor.h" +#include "Gainmap.h" +#include "GraphicsJNI.h" +#include "HardwareBufferHelpers.h" +#include "ScopedParcel.h" #include "SkBitmap.h" +#include "SkBlendMode.h" #include "SkCanvas.h" #include "SkColor.h" #include "SkColorSpace.h" -#include "SkPixelRef.h" -#include "SkImageEncoder.h" +#include "SkData.h" #include "SkImageInfo.h" -#include "GraphicsJNI.h" +#include "SkPaint.h" +#include "SkPixmap.h" +#include "SkPoint.h" +#include "SkRefCnt.h" #include "SkStream.h" -#include "SkWebpEncoder.h" - +#include "SkTypes.h" #include "android_nio_utils.h" -#include "CreateJavaOutputStreamAdaptor.h" -#include <hwui/Paint.h> -#include <hwui/Bitmap.h> -#include <utils/Color.h> #ifdef __ANDROID__ // Layoutlib does not support graphic buffer, parcel or render thread #include <android-base/unique_fd.h> -#include <android/binder_parcel.h> -#include <android/binder_parcel_jni.h> -#include <android/binder_parcel_platform.h> -#include <android/binder_parcel_utils.h> -#include <private/android/AHardwareBufferHelpers.h> -#include <cutils/ashmem.h> -#include <dlfcn.h> #include <renderthread/RenderProxy.h> -#include <sys/mman.h> #endif #include <inttypes.h> #include <string.h> + #include <memory> -#include <string> #define DEBUG_PARCEL 0 @@ -46,6 +45,8 @@ static jmethodID gBitmap_reinitMethodID; namespace android { +jobject Gainmap_extractFromBitmap(JNIEnv* env, const Bitmap& bitmap); + class BitmapWrapper { public: explicit BitmapWrapper(Bitmap* bitmap) @@ -368,15 +369,28 @@ static bool bitmapCopyTo(SkBitmap* dst, SkColorType dstCT, const SkBitmap& src, return srcPM.readPixels(dstPM); } -static jobject Bitmap_copy(JNIEnv* env, jobject, jlong srcHandle, - jint dstConfigHandle, jboolean isMutable) { +static jobject Bitmap_copy(JNIEnv* env, jobject, jlong srcHandle, jint dstConfigHandle, + jboolean isMutable) { + LocalScopedBitmap bitmapHolder(srcHandle); + if (!bitmapHolder.valid()) { + return NULL; + } + const Bitmap& original = bitmapHolder->bitmap(); + const bool hasGainmap = original.hasGainmap(); SkBitmap src; - reinterpret_cast<BitmapWrapper*>(srcHandle)->getSkBitmap(&src); + bitmapHolder->getSkBitmap(&src); + if (dstConfigHandle == GraphicsJNI::hardwareLegacyBitmapConfig()) { sk_sp<Bitmap> bitmap(Bitmap::allocateHardwareBitmap(src)); if (!bitmap.get()) { return NULL; } + if (hasGainmap) { + auto gm = uirenderer::Gainmap::allocateHardwareGainmap(original.gainmap()); + if (gm) { + bitmap->setGainmap(std::move(gm)); + } + } return createBitmap(env, bitmap.release(), getPremulBitmapCreateFlags(isMutable)); } @@ -388,6 +402,18 @@ static jobject Bitmap_copy(JNIEnv* env, jobject, jlong srcHandle, return NULL; } auto bitmap = allocator.getStorageObjAndReset(); + if (hasGainmap) { + auto gainmap = sp<uirenderer::Gainmap>::make(); + gainmap->info = original.gainmap()->info; + const SkBitmap skSrcBitmap = original.gainmap()->bitmap->getSkBitmap(); + SkBitmap skDestBitmap; + HeapAllocator destAllocator; + if (!bitmapCopyTo(&skDestBitmap, dstCT, skSrcBitmap, &destAllocator)) { + return NULL; + } + gainmap->bitmap = sk_sp<Bitmap>(destAllocator.getStorageObjAndReset()); + bitmap->setGainmap(std::move(gainmap)); + } return createBitmap(env, bitmap, getPremulBitmapCreateFlags(isMutable)); } @@ -421,6 +447,11 @@ static jobject Bitmap_copyAshmemConfig(JNIEnv* env, jobject, jlong srcHandle, ji return ret; } +static jint Bitmap_getAshmemFd(JNIEnv* env, jobject, jlong bitmapHandle) { + LocalScopedBitmap bitmap(bitmapHandle); + return (bitmap.valid()) ? bitmap->bitmap().getAshmemFd() : -1; +} + static void Bitmap_destruct(BitmapWrapper* bitmap) { delete bitmap; } @@ -576,91 +607,7 @@ static void Bitmap_setHasMipMap(JNIEnv* env, jobject, jlong bitmapHandle, /////////////////////////////////////////////////////////////////////////////// // TODO: Move somewhere else -#ifdef __ANDROID__ // Layoutlib does not support parcel - -class ScopedParcel { -public: - explicit ScopedParcel(JNIEnv* env, jobject parcel) { - mParcel = AParcel_fromJavaParcel(env, parcel); - } - - ~ScopedParcel() { AParcel_delete(mParcel); } - - int32_t readInt32() { - int32_t temp = 0; - // TODO: This behavior-matches what android::Parcel does - // but this should probably be better - if (AParcel_readInt32(mParcel, &temp) != STATUS_OK) { - temp = 0; - } - return temp; - } - - uint32_t readUint32() { - uint32_t temp = 0; - // TODO: This behavior-matches what android::Parcel does - // but this should probably be better - if (AParcel_readUint32(mParcel, &temp) != STATUS_OK) { - temp = 0; - } - return temp; - } - - void writeInt32(int32_t value) { AParcel_writeInt32(mParcel, value); } - - void writeUint32(uint32_t value) { AParcel_writeUint32(mParcel, value); } - - bool allowFds() const { return AParcel_getAllowFds(mParcel); } - - std::optional<sk_sp<SkData>> readData() { - struct Data { - void* ptr = nullptr; - size_t size = 0; - } data; - auto error = AParcel_readByteArray(mParcel, &data, - [](void* arrayData, int32_t length, - int8_t** outBuffer) -> bool { - Data* data = reinterpret_cast<Data*>(arrayData); - if (length > 0) { - data->ptr = sk_malloc_canfail(length); - if (!data->ptr) { - return false; - } - *outBuffer = - reinterpret_cast<int8_t*>(data->ptr); - data->size = length; - } - return true; - }); - if (error != STATUS_OK || data.size <= 0) { - sk_free(data.ptr); - return std::nullopt; - } else { - return SkData::MakeFromMalloc(data.ptr, data.size); - } - } - - void writeData(const std::optional<sk_sp<SkData>>& optData) { - if (optData) { - const auto& data = *optData; - AParcel_writeByteArray(mParcel, reinterpret_cast<const int8_t*>(data->data()), - data->size()); - } else { - AParcel_writeByteArray(mParcel, nullptr, -1); - } - } - - AParcel* get() { return mParcel; } - -private: - AParcel* mParcel; -}; - -enum class BlobType : int32_t { - IN_PLACE, - ASHMEM, -}; - +#ifdef __ANDROID__ // Layoutlib does not support parcel #define ON_ERROR_RETURN(X) \ if ((error = (X)) != STATUS_OK) return error @@ -1189,18 +1136,11 @@ static jobject Bitmap_copyPreserveInternalConfig(JNIEnv* env, jobject, jlong bit return createBitmap(env, bitmap.release(), getPremulBitmapCreateFlags(false)); } -#ifdef __ANDROID__ // Layoutlib does not support graphic buffer -typedef AHardwareBuffer* (*AHB_from_HB)(JNIEnv*, jobject); -AHB_from_HB AHardwareBuffer_fromHardwareBuffer; - -typedef jobject (*AHB_to_HB)(JNIEnv*, AHardwareBuffer*); -AHB_to_HB AHardwareBuffer_toHardwareBuffer; -#endif - static jobject Bitmap_wrapHardwareBufferBitmap(JNIEnv* env, jobject, jobject hardwareBuffer, jlong colorSpacePtr) { #ifdef __ANDROID__ // Layoutlib does not support graphic buffer - AHardwareBuffer* buffer = AHardwareBuffer_fromHardwareBuffer(env, hardwareBuffer); + AHardwareBuffer* buffer = uirenderer::HardwareBufferHelpers::AHardwareBuffer_fromHardwareBuffer( + env, hardwareBuffer); sk_sp<Bitmap> bitmap = Bitmap::createFrom(buffer, GraphicsJNI::getNativeColorSpace(colorSpacePtr)); if (!bitmap.get()) { @@ -1223,7 +1163,8 @@ static jobject Bitmap_getHardwareBuffer(JNIEnv* env, jobject, jlong bitmapPtr) { } Bitmap& bitmap = bitmapHandle->bitmap(); - return AHardwareBuffer_toHardwareBuffer(env, bitmap.hardwareBuffer()); + return uirenderer::HardwareBufferHelpers::AHardwareBuffer_toHardwareBuffer( + env, bitmap.hardwareBuffer()); #else return nullptr; #endif @@ -1251,67 +1192,85 @@ static void Bitmap_setImmutable(JNIEnv* env, jobject, jlong bitmapHandle) { return bitmapHolder->bitmap().setImmutable(); } +static jboolean Bitmap_hasGainmap(CRITICAL_JNI_PARAMS_COMMA jlong bitmapHandle) { + LocalScopedBitmap bitmapHolder(bitmapHandle); + if (!bitmapHolder.valid()) return false; + + return bitmapHolder->bitmap().hasGainmap(); +} + +static jobject Bitmap_extractGainmap(JNIEnv* env, jobject, jlong bitmapHandle) { + LocalScopedBitmap bitmapHolder(bitmapHandle); + if (!bitmapHolder.valid()) return nullptr; + if (!bitmapHolder->bitmap().hasGainmap()) return nullptr; + + return Gainmap_extractFromBitmap(env, bitmapHolder->bitmap()); +} + +static void Bitmap_setGainmap(JNIEnv*, jobject, jlong bitmapHandle, jlong gainmapPtr) { + LocalScopedBitmap bitmapHolder(bitmapHandle); + if (!bitmapHolder.valid()) return; + uirenderer::Gainmap* gainmap = reinterpret_cast<uirenderer::Gainmap*>(gainmapPtr); + bitmapHolder->bitmap().setGainmap(sp<uirenderer::Gainmap>::fromExisting(gainmap)); +} + /////////////////////////////////////////////////////////////////////////////// static const JNINativeMethod gBitmapMethods[] = { - { "nativeCreate", "([IIIIIIZJ)Landroid/graphics/Bitmap;", - (void*)Bitmap_creator }, - { "nativeCopy", "(JIZ)Landroid/graphics/Bitmap;", - (void*)Bitmap_copy }, - { "nativeCopyAshmem", "(J)Landroid/graphics/Bitmap;", - (void*)Bitmap_copyAshmem }, - { "nativeCopyAshmemConfig", "(JI)Landroid/graphics/Bitmap;", - (void*)Bitmap_copyAshmemConfig }, - { "nativeGetNativeFinalizer", "()J", (void*)Bitmap_getNativeFinalizer }, - { "nativeRecycle", "(J)V", (void*)Bitmap_recycle }, - { "nativeReconfigure", "(JIIIZ)V", (void*)Bitmap_reconfigure }, - { "nativeCompress", "(JIILjava/io/OutputStream;[B)Z", - (void*)Bitmap_compress }, - { "nativeErase", "(JI)V", (void*)Bitmap_erase }, - { "nativeErase", "(JJJ)V", (void*)Bitmap_eraseLong }, - { "nativeRowBytes", "(J)I", (void*)Bitmap_rowBytes }, - { "nativeConfig", "(J)I", (void*)Bitmap_config }, - { "nativeHasAlpha", "(J)Z", (void*)Bitmap_hasAlpha }, - { "nativeIsPremultiplied", "(J)Z", (void*)Bitmap_isPremultiplied}, - { "nativeSetHasAlpha", "(JZZ)V", (void*)Bitmap_setHasAlpha}, - { "nativeSetPremultiplied", "(JZ)V", (void*)Bitmap_setPremultiplied}, - { "nativeHasMipMap", "(J)Z", (void*)Bitmap_hasMipMap }, - { "nativeSetHasMipMap", "(JZ)V", (void*)Bitmap_setHasMipMap }, - { "nativeCreateFromParcel", - "(Landroid/os/Parcel;)Landroid/graphics/Bitmap;", - (void*)Bitmap_createFromParcel }, - { "nativeWriteToParcel", "(JILandroid/os/Parcel;)Z", - (void*)Bitmap_writeToParcel }, - { "nativeExtractAlpha", "(JJ[I)Landroid/graphics/Bitmap;", - (void*)Bitmap_extractAlpha }, - { "nativeGenerationId", "(J)I", (void*)Bitmap_getGenerationId }, - { "nativeGetPixel", "(JII)I", (void*)Bitmap_getPixel }, - { "nativeGetColor", "(JII)J", (void*)Bitmap_getColor }, - { "nativeGetPixels", "(J[IIIIIII)V", (void*)Bitmap_getPixels }, - { "nativeSetPixel", "(JIII)V", (void*)Bitmap_setPixel }, - { "nativeSetPixels", "(J[IIIIIII)V", (void*)Bitmap_setPixels }, - { "nativeCopyPixelsToBuffer", "(JLjava/nio/Buffer;)V", - (void*)Bitmap_copyPixelsToBuffer }, - { "nativeCopyPixelsFromBuffer", "(JLjava/nio/Buffer;)V", - (void*)Bitmap_copyPixelsFromBuffer }, - { "nativeSameAs", "(JJ)Z", (void*)Bitmap_sameAs }, - { "nativePrepareToDraw", "(J)V", (void*)Bitmap_prepareToDraw }, - { "nativeGetAllocationByteCount", "(J)I", (void*)Bitmap_getAllocationByteCount }, - { "nativeCopyPreserveInternalConfig", "(J)Landroid/graphics/Bitmap;", - (void*)Bitmap_copyPreserveInternalConfig }, - { "nativeWrapHardwareBufferBitmap", "(Landroid/hardware/HardwareBuffer;J)Landroid/graphics/Bitmap;", - (void*) Bitmap_wrapHardwareBufferBitmap }, - { "nativeGetHardwareBuffer", "(J)Landroid/hardware/HardwareBuffer;", - (void*) Bitmap_getHardwareBuffer }, - { "nativeComputeColorSpace", "(J)Landroid/graphics/ColorSpace;", (void*)Bitmap_computeColorSpace }, - { "nativeSetColorSpace", "(JJ)V", (void*)Bitmap_setColorSpace }, - { "nativeIsSRGB", "(J)Z", (void*)Bitmap_isSRGB }, - { "nativeIsSRGBLinear", "(J)Z", (void*)Bitmap_isSRGBLinear}, - { "nativeSetImmutable", "(J)V", (void*)Bitmap_setImmutable}, - - // ------------ @CriticalNative ---------------- - { "nativeIsImmutable", "(J)Z", (void*)Bitmap_isImmutable}, - { "nativeIsBackedByAshmem", "(J)Z", (void*)Bitmap_isBackedByAshmem} + {"nativeCreate", "([IIIIIIZJ)Landroid/graphics/Bitmap;", (void*)Bitmap_creator}, + {"nativeCopy", "(JIZ)Landroid/graphics/Bitmap;", (void*)Bitmap_copy}, + {"nativeCopyAshmem", "(J)Landroid/graphics/Bitmap;", (void*)Bitmap_copyAshmem}, + {"nativeCopyAshmemConfig", "(JI)Landroid/graphics/Bitmap;", (void*)Bitmap_copyAshmemConfig}, + {"nativeGetAshmemFD", "(J)I", (void*)Bitmap_getAshmemFd}, + {"nativeGetNativeFinalizer", "()J", (void*)Bitmap_getNativeFinalizer}, + {"nativeRecycle", "(J)V", (void*)Bitmap_recycle}, + {"nativeReconfigure", "(JIIIZ)V", (void*)Bitmap_reconfigure}, + {"nativeCompress", "(JIILjava/io/OutputStream;[B)Z", (void*)Bitmap_compress}, + {"nativeErase", "(JI)V", (void*)Bitmap_erase}, + {"nativeErase", "(JJJ)V", (void*)Bitmap_eraseLong}, + {"nativeRowBytes", "(J)I", (void*)Bitmap_rowBytes}, + {"nativeConfig", "(J)I", (void*)Bitmap_config}, + {"nativeHasAlpha", "(J)Z", (void*)Bitmap_hasAlpha}, + {"nativeIsPremultiplied", "(J)Z", (void*)Bitmap_isPremultiplied}, + {"nativeSetHasAlpha", "(JZZ)V", (void*)Bitmap_setHasAlpha}, + {"nativeSetPremultiplied", "(JZ)V", (void*)Bitmap_setPremultiplied}, + {"nativeHasMipMap", "(J)Z", (void*)Bitmap_hasMipMap}, + {"nativeSetHasMipMap", "(JZ)V", (void*)Bitmap_setHasMipMap}, + {"nativeCreateFromParcel", "(Landroid/os/Parcel;)Landroid/graphics/Bitmap;", + (void*)Bitmap_createFromParcel}, + {"nativeWriteToParcel", "(JILandroid/os/Parcel;)Z", (void*)Bitmap_writeToParcel}, + {"nativeExtractAlpha", "(JJ[I)Landroid/graphics/Bitmap;", (void*)Bitmap_extractAlpha}, + {"nativeGenerationId", "(J)I", (void*)Bitmap_getGenerationId}, + {"nativeGetPixel", "(JII)I", (void*)Bitmap_getPixel}, + {"nativeGetColor", "(JII)J", (void*)Bitmap_getColor}, + {"nativeGetPixels", "(J[IIIIIII)V", (void*)Bitmap_getPixels}, + {"nativeSetPixel", "(JIII)V", (void*)Bitmap_setPixel}, + {"nativeSetPixels", "(J[IIIIIII)V", (void*)Bitmap_setPixels}, + {"nativeCopyPixelsToBuffer", "(JLjava/nio/Buffer;)V", (void*)Bitmap_copyPixelsToBuffer}, + {"nativeCopyPixelsFromBuffer", "(JLjava/nio/Buffer;)V", (void*)Bitmap_copyPixelsFromBuffer}, + {"nativeSameAs", "(JJ)Z", (void*)Bitmap_sameAs}, + {"nativePrepareToDraw", "(J)V", (void*)Bitmap_prepareToDraw}, + {"nativeGetAllocationByteCount", "(J)I", (void*)Bitmap_getAllocationByteCount}, + {"nativeCopyPreserveInternalConfig", "(J)Landroid/graphics/Bitmap;", + (void*)Bitmap_copyPreserveInternalConfig}, + {"nativeWrapHardwareBufferBitmap", + "(Landroid/hardware/HardwareBuffer;J)Landroid/graphics/Bitmap;", + (void*)Bitmap_wrapHardwareBufferBitmap}, + {"nativeGetHardwareBuffer", "(J)Landroid/hardware/HardwareBuffer;", + (void*)Bitmap_getHardwareBuffer}, + {"nativeComputeColorSpace", "(J)Landroid/graphics/ColorSpace;", + (void*)Bitmap_computeColorSpace}, + {"nativeSetColorSpace", "(JJ)V", (void*)Bitmap_setColorSpace}, + {"nativeIsSRGB", "(J)Z", (void*)Bitmap_isSRGB}, + {"nativeIsSRGBLinear", "(J)Z", (void*)Bitmap_isSRGBLinear}, + {"nativeSetImmutable", "(J)V", (void*)Bitmap_setImmutable}, + {"nativeExtractGainmap", "(J)Landroid/graphics/Gainmap;", (void*)Bitmap_extractGainmap}, + {"nativeSetGainmap", "(JJ)V", (void*)Bitmap_setGainmap}, + + // ------------ @CriticalNative ---------------- + {"nativeIsImmutable", "(J)Z", (void*)Bitmap_isImmutable}, + {"nativeIsBackedByAshmem", "(J)Z", (void*)Bitmap_isBackedByAshmem}, + {"nativeHasGainmap", "(J)Z", (void*)Bitmap_hasGainmap}, }; @@ -1321,18 +1280,7 @@ int register_android_graphics_Bitmap(JNIEnv* env) gBitmap_nativePtr = GetFieldIDOrDie(env, gBitmap_class, "mNativePtr", "J"); gBitmap_constructorMethodID = GetMethodIDOrDie(env, gBitmap_class, "<init>", "(JIIIZ[BLandroid/graphics/NinePatch$InsetStruct;Z)V"); gBitmap_reinitMethodID = GetMethodIDOrDie(env, gBitmap_class, "reinit", "(IIZ)V"); - -#ifdef __ANDROID__ // Layoutlib does not support graphic buffer or parcel - void* handle_ = dlopen("libandroid.so", RTLD_NOW | RTLD_NODELETE); - AHardwareBuffer_fromHardwareBuffer = - (AHB_from_HB)dlsym(handle_, "AHardwareBuffer_fromHardwareBuffer"); - LOG_ALWAYS_FATAL_IF(AHardwareBuffer_fromHardwareBuffer == nullptr, - "Failed to find required symbol AHardwareBuffer_fromHardwareBuffer!"); - - AHardwareBuffer_toHardwareBuffer = (AHB_to_HB)dlsym(handle_, "AHardwareBuffer_toHardwareBuffer"); - LOG_ALWAYS_FATAL_IF(AHardwareBuffer_toHardwareBuffer == nullptr, - " Failed to find required symbol AHardwareBuffer_toHardwareBuffer!"); -#endif + uirenderer::HardwareBufferHelpers::init(); return android::RegisterMethodsOrDie(env, "android/graphics/Bitmap", gBitmapMethods, NELEM(gBitmapMethods)); } 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..c57e6f09347a 100644 --- a/libs/hwui/jni/BitmapFactory.cpp +++ b/libs/hwui/jni/BitmapFactory.cpp @@ -2,30 +2,43 @@ #define LOG_TAG "BitmapFactory" #include "BitmapFactory.h" + +#include <Gainmap.h> +#include <HardwareBitmapUploader.h> +#include <androidfw/Asset.h> +#include <androidfw/ResourceTypes.h> +#include <cutils/compiler.h> +#include <fcntl.h> +#include <nativehelper/JNIPlatformHelp.h> +#include <stdint.h> +#include <stdio.h> +#include <sys/stat.h> + +#include <memory> + #include "CreateJavaOutputStreamAdaptor.h" #include "FrontBufferedStream.h" #include "GraphicsJNI.h" #include "MimeType.h" #include "NinePatchPeeker.h" #include "SkAndroidCodec.h" +#include "SkBitmap.h" +#include "SkBlendMode.h" #include "SkCanvas.h" -#include "SkMath.h" +#include "SkColorSpace.h" +#include "SkEncodedImageFormat.h" +#include "SkGainmapInfo.h" +#include "SkImageInfo.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" #include "Utils.h" -#include <HardwareBitmapUploader.h> -#include <nativehelper/JNIPlatformHelp.h> -#include <androidfw/Asset.h> -#include <androidfw/ResourceTypes.h> -#include <cutils/compiler.h> -#include <fcntl.h> -#include <memory> -#include <stdio.h> -#include <sys/stat.h> - jfieldID gOptions_justBoundsFieldID; jfieldID gOptions_sampleSizeFieldID; jfieldID gOptions_configFieldID; @@ -132,7 +145,7 @@ public: } const size_t size = info.computeByteSize(bitmap->rowBytes()); - if (size > SK_MaxS32) { + if (size > INT32_MAX) { ALOGW("bitmap is too large"); return false; } @@ -173,6 +186,115 @@ static bool needsFineScale(const SkISize fullSize, const SkISize decodedSize, needsFineScale(fullSize.height(), decodedSize.height(), sampleSize); } +static bool decodeGainmap(std::unique_ptr<SkStream> gainmapStream, const SkGainmapInfo& gainmapInfo, + sp<uirenderer::Gainmap>* outGainmap, const int sampleSize, float scale) { + std::unique_ptr<SkAndroidCodec> codec; + codec = SkAndroidCodec::MakeFromStream(std::move(gainmapStream), nullptr); + if (!codec) { + ALOGE("Can not create a codec for Gainmap."); + return false; + } + SkColorType decodeColorType = codec->computeOutputColorType(kN32_SkColorType); + sk_sp<SkColorSpace> decodeColorSpace = codec->computeOutputColorSpace(decodeColorType, nullptr); + + SkISize size = codec->getSampledDimensions(sampleSize); + + int scaledWidth = size.width(); + int scaledHeight = size.height(); + bool willScale = false; + + // Apply a fine scaling step if necessary. + if (needsFineScale(codec->getInfo().dimensions(), size, sampleSize) || scale != 1.0f) { + willScale = true; + // The operation below may loose precision (integer division), but it is put this way to + // mimic main image scale calculation + scaledWidth = static_cast<int>((codec->getInfo().width() / sampleSize) * scale + 0.5f); + scaledHeight = static_cast<int>((codec->getInfo().height() / sampleSize) * scale + 0.5f); + } + + SkAlphaType alphaType = codec->computeOutputAlphaType(false); + + const SkImageInfo decodeInfo = SkImageInfo::Make(size.width(), size.height(), decodeColorType, + alphaType, decodeColorSpace); + + const SkImageInfo& bitmapInfo = decodeInfo; + SkBitmap decodeBitmap; + sk_sp<Bitmap> nativeBitmap = nullptr; + + if (!decodeBitmap.setInfo(bitmapInfo)) { + ALOGE("Failed to setInfo."); + return false; + } + + if (willScale) { + if (!decodeBitmap.tryAllocPixels(nullptr)) { + ALOGE("OOM allocating gainmap pixels."); + return false; + } + } else { + nativeBitmap = android::Bitmap::allocateHeapBitmap(&decodeBitmap); + if (!nativeBitmap) { + ALOGE("OOM allocating gainmap pixels."); + return false; + } + } + + // Use SkAndroidCodec to perform the decode. + SkAndroidCodec::AndroidOptions codecOptions; + codecOptions.fZeroInitialized = SkCodec::kYes_ZeroInitialized; + codecOptions.fSampleSize = sampleSize; + SkCodec::Result result = codec->getAndroidPixels(decodeInfo, decodeBitmap.getPixels(), + decodeBitmap.rowBytes(), &codecOptions); + switch (result) { + case SkCodec::kSuccess: + case SkCodec::kIncompleteInput: + break; + default: + ALOGE("Error decoding gainmap."); + return false; + } + + if (willScale) { + SkBitmap gainmapBitmap; + const float scaleX = scaledWidth / float(decodeBitmap.width()); + const float scaleY = scaledHeight / float(decodeBitmap.height()); + + SkColorType scaledColorType = decodeBitmap.colorType(); + gainmapBitmap.setInfo( + bitmapInfo.makeWH(scaledWidth, scaledHeight).makeColorType(scaledColorType)); + + nativeBitmap = android::Bitmap::allocateHeapBitmap(&gainmapBitmap); + if (!nativeBitmap) { + ALOGE("OOM allocating gainmap pixels."); + return false; + } + + SkPaint paint; + // kSrc_Mode instructs us to overwrite the uninitialized pixels in + // outputBitmap. Otherwise we would blend by default, which is not + // what we want. + paint.setBlendMode(SkBlendMode::kSrc); + + SkCanvas canvas(gainmapBitmap, SkCanvas::ColorBehavior::kLegacy); + canvas.scale(scaleX, scaleY); + decodeBitmap.setImmutable(); // so .asImage() doesn't make a copy + canvas.drawImage(decodeBitmap.asImage(), 0.0f, 0.0f, + SkSamplingOptions(SkFilterMode::kLinear), &paint); + } + + auto gainmap = sp<uirenderer::Gainmap>::make(); + if (!gainmap) { + ALOGE("OOM allocating Gainmap"); + return false; + } + + gainmap->info = gainmapInfo; + gainmap->bitmap = std::move(nativeBitmap); + *outGainmap = std::move(gainmap); + + return true; +} + static jobject doDecode(JNIEnv* env, std::unique_ptr<SkStreamRewindable> stream, jobject padding, jobject options, jlong inBitmapHandle, jlong colorSpaceHandle) { @@ -476,6 +598,19 @@ static jobject doDecode(JNIEnv* env, std::unique_ptr<SkStreamRewindable> stream, return nullObjectReturn("Got null SkPixelRef"); } + bool hasGainmap = false; + SkGainmapInfo gainmapInfo; + std::unique_ptr<SkStream> gainmapStream = nullptr; + sp<uirenderer::Gainmap> gainmap = nullptr; + if (result == SkCodec::kSuccess) { + hasGainmap = codec->getAndroidGainmap(&gainmapInfo, &gainmapStream); + } + + if (hasGainmap) { + hasGainmap = + decodeGainmap(std::move(gainmapStream), gainmapInfo, &gainmap, sampleSize, scale); + } + if (!isMutable && javaBitmap == NULL) { // promise we will never change our pixels (great for sharing and pictures) outputBitmap.setImmutable(); @@ -483,6 +618,9 @@ static jobject doDecode(JNIEnv* env, std::unique_ptr<SkStreamRewindable> stream, bool isPremultiplied = !requireUnpremultiplied; if (javaBitmap != nullptr) { + if (hasGainmap) { + reuseBitmap->setGainmap(std::move(gainmap)); + } bitmap::reinitBitmap(env, javaBitmap, outputBitmap.info(), isPremultiplied); outputBitmap.notifyPixelsChanged(); // If a java bitmap was passed in for reuse, pass it back @@ -498,13 +636,25 @@ static jobject doDecode(JNIEnv* env, std::unique_ptr<SkStreamRewindable> stream, if (!hardwareBitmap.get()) { return nullObjectReturn("Failed to allocate a hardware bitmap"); } + if (hasGainmap) { + auto gm = uirenderer::Gainmap::allocateHardwareGainmap(gainmap); + if (gm) { + hardwareBitmap->setGainmap(std::move(gm)); + } + } + return bitmap::createBitmap(env, hardwareBitmap.release(), bitmapCreateFlags, ninePatchChunk, ninePatchInsets, -1); } + Bitmap* heapBitmap = defaultAllocator.getStorageObjAndReset(); + if (hasGainmap && heapBitmap != nullptr) { + heapBitmap->setGainmap(std::move(gainmap)); + } + // now create the java bitmap - return bitmap::createBitmap(env, defaultAllocator.getStorageObjAndReset(), - bitmapCreateFlags, ninePatchChunk, ninePatchInsets, -1); + return bitmap::createBitmap(env, heapBitmap, bitmapCreateFlags, ninePatchChunk, ninePatchInsets, + -1); } static jobject nativeDecodeStream(JNIEnv* env, jobject clazz, jobject is, jbyteArray storage, diff --git a/libs/hwui/jni/BitmapRegionDecoder.cpp b/libs/hwui/jni/BitmapRegionDecoder.cpp index 1c20415dcc8f..aeaa17198be5 100644 --- a/libs/hwui/jni/BitmapRegionDecoder.cpp +++ b/libs/hwui/jni/BitmapRegionDecoder.cpp @@ -17,27 +17,139 @@ #undef LOG_TAG #define LOG_TAG "BitmapRegionDecoder" +#include "BitmapRegionDecoder.h" + +#include <HardwareBitmapUploader.h> +#include <androidfw/Asset.h> +#include <sys/stat.h> + +#include <memory> + #include "BitmapFactory.h" #include "CreateJavaOutputStreamAdaptor.h" +#include "Gainmap.h" #include "GraphicsJNI.h" -#include "Utils.h" - -#include "BitmapRegionDecoder.h" #include "SkBitmap.h" #include "SkCodec.h" +#include "SkColorSpace.h" #include "SkData.h" +#include "SkGainmapInfo.h" #include "SkStream.h" +#include "SkStreamPriv.h" +#include "Utils.h" -#include <HardwareBitmapUploader.h> -#include <androidfw/Asset.h> -#include <sys/stat.h> +using namespace android; -#include <memory> +namespace android { +class BitmapRegionDecoderWrapper { +public: + static std::unique_ptr<BitmapRegionDecoderWrapper> Make(sk_sp<SkData> data) { + std::unique_ptr<skia::BitmapRegionDecoder> mainImageBRD = + skia::BitmapRegionDecoder::Make(std::move(data)); + if (!mainImageBRD) { + return nullptr; + } -using namespace android; + SkGainmapInfo gainmapInfo; + std::unique_ptr<SkStream> gainmapStream; + std::unique_ptr<skia::BitmapRegionDecoder> gainmapBRD = nullptr; + if (mainImageBRD->getAndroidGainmap(&gainmapInfo, &gainmapStream)) { + sk_sp<SkData> data = nullptr; + if (gainmapStream->getMemoryBase()) { + // It is safe to make without copy because we'll hold onto the stream. + data = SkData::MakeWithoutCopy(gainmapStream->getMemoryBase(), + gainmapStream->getLength()); + } else { + data = SkCopyStreamToData(gainmapStream.get()); + // We don't need to hold the stream anymore + gainmapStream = nullptr; + } + gainmapBRD = skia::BitmapRegionDecoder::Make(std::move(data)); + } + + return std::unique_ptr<BitmapRegionDecoderWrapper>( + new BitmapRegionDecoderWrapper(std::move(mainImageBRD), std::move(gainmapBRD), + gainmapInfo, std::move(gainmapStream))); + } + + SkEncodedImageFormat getEncodedFormat() { return mMainImageBRD->getEncodedFormat(); } + + SkColorType computeOutputColorType(SkColorType requestedColorType) { + return mMainImageBRD->computeOutputColorType(requestedColorType); + } + + sk_sp<SkColorSpace> computeOutputColorSpace(SkColorType outputColorType, + sk_sp<SkColorSpace> prefColorSpace = nullptr) { + return mMainImageBRD->computeOutputColorSpace(outputColorType, prefColorSpace); + } + + bool decodeRegion(SkBitmap* bitmap, skia::BRDAllocator* allocator, const SkIRect& desiredSubset, + int sampleSize, SkColorType colorType, bool requireUnpremul, + sk_sp<SkColorSpace> prefColorSpace) { + return mMainImageBRD->decodeRegion(bitmap, allocator, desiredSubset, sampleSize, colorType, + requireUnpremul, prefColorSpace); + } + + bool decodeGainmapRegion(sp<uirenderer::Gainmap>* outGainmap, const SkIRect& desiredSubset, + int sampleSize, bool requireUnpremul) { + SkColorType decodeColorType = mGainmapBRD->computeOutputColorType(kN32_SkColorType); + sk_sp<SkColorSpace> decodeColorSpace = + mGainmapBRD->computeOutputColorSpace(decodeColorType, nullptr); + SkBitmap bm; + HeapAllocator heapAlloc; + if (!mGainmapBRD->decodeRegion(&bm, &heapAlloc, desiredSubset, sampleSize, decodeColorType, + requireUnpremul, decodeColorSpace)) { + ALOGE("Error decoding Gainmap region"); + return false; + } + sk_sp<Bitmap> nativeBitmap(heapAlloc.getStorageObjAndReset()); + if (!nativeBitmap) { + ALOGE("OOM allocating Bitmap for Gainmap"); + return false; + } + auto gainmap = sp<uirenderer::Gainmap>::make(); + if (!gainmap) { + ALOGE("OOM allocating Gainmap"); + return false; + } + gainmap->info = mGainmapInfo; + gainmap->bitmap = std::move(nativeBitmap); + *outGainmap = std::move(gainmap); + return true; + } + + SkIRect calculateGainmapRegion(const SkIRect& mainImageRegion) { + const float scaleX = ((float)mGainmapBRD->width()) / mMainImageBRD->width(); + const float scaleY = ((float)mGainmapBRD->height()) / mMainImageBRD->height(); + // TODO: Account for rounding error? + return SkIRect::MakeLTRB(mainImageRegion.left() * scaleX, mainImageRegion.top() * scaleY, + mainImageRegion.right() * scaleX, + mainImageRegion.bottom() * scaleY); + } + + bool hasGainmap() { return mGainmapBRD != nullptr; } + + int width() const { return mMainImageBRD->width(); } + int height() const { return mMainImageBRD->height(); } + +private: + BitmapRegionDecoderWrapper(std::unique_ptr<skia::BitmapRegionDecoder> mainImageBRD, + std::unique_ptr<skia::BitmapRegionDecoder> gainmapBRD, + SkGainmapInfo info, std::unique_ptr<SkStream> stream) + : mMainImageBRD(std::move(mainImageBRD)) + , mGainmapBRD(std::move(gainmapBRD)) + , mGainmapInfo(info) + , mGainmapStream(std::move(stream)) {} + + std::unique_ptr<skia::BitmapRegionDecoder> mMainImageBRD; + std::unique_ptr<skia::BitmapRegionDecoder> mGainmapBRD; + SkGainmapInfo mGainmapInfo; + std::unique_ptr<SkStream> mGainmapStream; +}; +} // namespace android static jobject createBitmapRegionDecoder(JNIEnv* env, sk_sp<SkData> data) { - auto brd = skia::BitmapRegionDecoder::Make(std::move(data)); + auto brd = android::BitmapRegionDecoderWrapper::Make(std::move(data)); if (!brd) { doThrowIOE(env, "Image format not supported"); return nullObjectReturn("CreateBitmapRegionDecoder returned null"); @@ -135,7 +247,7 @@ static jobject nativeDecodeRegion(JNIEnv* env, jobject, jlong brdHandle, jint in recycledBytes = recycledBitmap->getAllocationByteCount(); } - auto* brd = reinterpret_cast<skia::BitmapRegionDecoder*>(brdHandle); + auto* brd = reinterpret_cast<BitmapRegionDecoderWrapper*>(brdHandle); SkColorType decodeColorType = brd->computeOutputColorType(colorType); if (isHardware) { @@ -195,9 +307,22 @@ static jobject nativeDecodeRegion(JNIEnv* env, jobject, jlong brdHandle, jint in GraphicsJNI::getColorSpace(env, decodeColorSpace.get(), decodeColorType)); } + sp<uirenderer::Gainmap> gainmap; + bool hasGainmap = brd->hasGainmap(); + if (hasGainmap) { + SkIRect gainmapSubset = brd->calculateGainmapRegion(subset); + if (!brd->decodeGainmapRegion(&gainmap, gainmapSubset, sampleSize, requireUnpremul)) { + // If there is an error decoding Gainmap - we don't fail. We just don't provide Gainmap + hasGainmap = false; + } + } + // If we may have reused a bitmap, we need to indicate that the pixels have changed. if (javaBitmap) { recycleAlloc.copyIfNecessary(); + if (hasGainmap) { + recycledBitmap->setGainmap(std::move(gainmap)); + } bitmap::reinitBitmap(env, javaBitmap, recycledBitmap->info(), !requireUnpremul); return javaBitmap; } @@ -208,23 +333,33 @@ static jobject nativeDecodeRegion(JNIEnv* env, jobject, jlong brdHandle, jint in } if (isHardware) { sk_sp<Bitmap> hardwareBitmap = Bitmap::allocateHardwareBitmap(bitmap); + if (hasGainmap) { + auto gm = uirenderer::Gainmap::allocateHardwareGainmap(gainmap); + if (gm) { + hardwareBitmap->setGainmap(std::move(gm)); + } + } return bitmap::createBitmap(env, hardwareBitmap.release(), bitmapCreateFlags); } - return android::bitmap::createBitmap(env, heapAlloc.getStorageObjAndReset(), bitmapCreateFlags); + Bitmap* heapBitmap = heapAlloc.getStorageObjAndReset(); + if (hasGainmap && heapBitmap != nullptr) { + heapBitmap->setGainmap(std::move(gainmap)); + } + return android::bitmap::createBitmap(env, heapBitmap, bitmapCreateFlags); } static jint nativeGetHeight(JNIEnv* env, jobject, jlong brdHandle) { - auto* brd = reinterpret_cast<skia::BitmapRegionDecoder*>(brdHandle); + auto* brd = reinterpret_cast<BitmapRegionDecoderWrapper*>(brdHandle); return static_cast<jint>(brd->height()); } static jint nativeGetWidth(JNIEnv* env, jobject, jlong brdHandle) { - auto* brd = reinterpret_cast<skia::BitmapRegionDecoder*>(brdHandle); + auto* brd = reinterpret_cast<BitmapRegionDecoderWrapper*>(brdHandle); return static_cast<jint>(brd->width()); } static void nativeClean(JNIEnv* env, jobject, jlong brdHandle) { - auto* brd = reinterpret_cast<skia::BitmapRegionDecoder*>(brdHandle); + auto* brd = reinterpret_cast<BitmapRegionDecoderWrapper*>(brdHandle); delete brd; } diff --git a/libs/hwui/jni/BufferUtils.cpp b/libs/hwui/jni/BufferUtils.cpp new file mode 100644 index 000000000000..3eb08d7552da --- /dev/null +++ b/libs/hwui/jni/BufferUtils.cpp @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include "BufferUtils.h" + +#include "graphics_jni_helpers.h" + +static void copyToVector(std::vector<uint8_t>& dst, const void* src, size_t srcSize) { + if (src) { + dst.resize(srcSize); + memcpy(dst.data(), src, srcSize); + } +} + +/** + * This code is taken and modified from com_google_android_gles_jni_GLImpl.cpp to extract data + * from a java.nio.Buffer. + */ +static void* getDirectBufferPointer(JNIEnv* env, jobject buffer) { + if (buffer == nullptr) { + return nullptr; + } + + jint position; + jint limit; + jint elementSizeShift; + jlong pointer; + pointer = jniGetNioBufferFields(env, buffer, &position, &limit, &elementSizeShift); + if (pointer == 0) { + jniThrowException(env, "java/lang/IllegalArgumentException", + "Must use a native order direct Buffer"); + return nullptr; + } + pointer += position << elementSizeShift; + return reinterpret_cast<void*>(pointer); +} + +static void releasePointer(JNIEnv* env, jarray array, void* data, jboolean commit) { + env->ReleasePrimitiveArrayCritical(array, data, commit ? 0 : JNI_ABORT); +} + +static void* getPointer(JNIEnv* env, jobject buffer, jarray* array, jint* remaining, jint* offset) { + jint position; + jint limit; + jint elementSizeShift; + + jlong pointer; + pointer = jniGetNioBufferFields(env, buffer, &position, &limit, &elementSizeShift); + *remaining = (limit - position) << elementSizeShift; + if (pointer != 0L) { + *array = nullptr; + pointer += position << elementSizeShift; + return reinterpret_cast<void*>(pointer); + } + + *array = jniGetNioBufferBaseArray(env, buffer); + *offset = jniGetNioBufferBaseArrayOffset(env, buffer); + return nullptr; +} + +/** + * This is a copy of + * static void android_glBufferData__IILjava_nio_Buffer_2I + * from com_google_android_gles_jni_GLImpl.cpp + */ +static void setIndirectData(JNIEnv* env, size_t size, jobject data_buf, + std::vector<uint8_t>& result) { + jint exception = 0; + const char* exceptionType = nullptr; + const char* exceptionMessage = nullptr; + jarray array = nullptr; + jint bufferOffset = 0; + jint remaining; + void* data = 0; + char* dataBase = nullptr; + + if (data_buf) { + data = getPointer(env, data_buf, (jarray*)&array, &remaining, &bufferOffset); + if (remaining < size) { + exception = 1; + exceptionType = "java/lang/IllegalArgumentException"; + exceptionMessage = "remaining() < size < needed"; + goto exit; + } + } + if (data_buf && data == nullptr) { + dataBase = (char*)env->GetPrimitiveArrayCritical(array, (jboolean*)0); + data = (void*)(dataBase + bufferOffset); + } + + copyToVector(result, data, size); + +exit: + if (array) { + releasePointer(env, array, (void*)dataBase, JNI_FALSE); + } + if (exception) { + jniThrowException(env, exceptionType, exceptionMessage); + } +} + +std::vector<uint8_t> copyJavaNioBufferToVector(JNIEnv* env, jobject buffer, size_t size, + jboolean isDirect) { + std::vector<uint8_t> data; + if (buffer == nullptr) { + jniThrowNullPointerException(env); + } else { + if (isDirect) { + void* directBufferPtr = getDirectBufferPointer(env, buffer); + if (directBufferPtr) { + copyToVector(data, directBufferPtr, size); + } + } else { + setIndirectData(env, size, buffer, data); + } + } + return data; +} diff --git a/libs/hwui/jni/BufferUtils.h b/libs/hwui/jni/BufferUtils.h new file mode 100644 index 000000000000..b43c320b7771 --- /dev/null +++ b/libs/hwui/jni/BufferUtils.h @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#ifndef BUFFERUTILS_H_ +#define BUFFERUTILS_H_ + +#include <jni.h> + +#include <vector> + +/** + * Helper method to load a java.nio.Buffer instance into a vector. This handles + * both direct and indirect buffers and promptly releases any critical arrays that + * have been retrieved in order to avoid potential jni exceptions due to interleaved + * jni calls between get/release primitive method invocations. + */ +std::vector<uint8_t> copyJavaNioBufferToVector(JNIEnv* env, jobject buffer, size_t size, + jboolean isDirect); + +#endif // BUFFERUTILS_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/ColorFilter.cpp b/libs/hwui/jni/ColorFilter.cpp index cef21f91f3c1..4bd7ef47b871 100644 --- a/libs/hwui/jni/ColorFilter.cpp +++ b/libs/hwui/jni/ColorFilter.cpp @@ -17,6 +17,7 @@ #include "GraphicsJNI.h" +#include "SkBlendMode.h" #include "SkColorFilter.h" #include "SkColorMatrixFilter.h" diff --git a/libs/hwui/jni/FontFamily.cpp b/libs/hwui/jni/FontFamily.cpp index ce5ac382aeff..28e71d74e5b9 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>( + std::shared_ptr<minikin::FontFamily> family = minikin::FontFamily::create( builder->langId, builder->variant, std::move(builder->fonts), - true /* isCustomFallback */); + true /* isCustomFallback */, false /* isDefaultFallback */); 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/Gainmap.cpp b/libs/hwui/jni/Gainmap.cpp new file mode 100644 index 000000000000..0f8a85dd9e62 --- /dev/null +++ b/libs/hwui/jni/Gainmap.cpp @@ -0,0 +1,267 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <Gainmap.h> + +#ifdef __ANDROID__ +#include <binder/Parcel.h> +#endif + +#include "Bitmap.h" +#include "GraphicsJNI.h" +#include "ScopedParcel.h" +#include "graphics_jni_helpers.h" + +namespace android { + +static jclass gGainmap_class; +static jmethodID gGainmap_constructorMethodID; + +using namespace uirenderer; + +static Gainmap* fromJava(jlong gainmap) { + return reinterpret_cast<Gainmap*>(gainmap); +} + +static int getCreateFlags(const sk_sp<Bitmap>& bitmap) { + int flags = 0; + if (bitmap->info().alphaType() == kPremul_SkAlphaType) { + flags |= android::bitmap::kBitmapCreateFlag_Premultiplied; + } + if (!bitmap->isImmutable()) { + flags |= android::bitmap::kBitmapCreateFlag_Mutable; + } + return flags; +} + +jobject Gainmap_extractFromBitmap(JNIEnv* env, const Bitmap& bitmap) { + auto gainmap = bitmap.gainmap(); + jobject jGainmapImage; + + { + // Scope to guard the release of nativeBitmap + auto nativeBitmap = gainmap->bitmap; + const int createFlags = getCreateFlags(nativeBitmap); + jGainmapImage = bitmap::createBitmap(env, nativeBitmap.release(), createFlags); + } + + // Grab a ref for the jobject + gainmap->incStrong(0); + jobject obj = env->NewObject(gGainmap_class, gGainmap_constructorMethodID, jGainmapImage, + gainmap.get()); + + if (env->ExceptionCheck() != 0) { + // sadtrombone + gainmap->decStrong(0); + ALOGE("*** Uncaught exception returned from Java call!\n"); + env->ExceptionDescribe(); + } + return obj; +} + +static void Gainmap_destructor(Gainmap* gainmap) { + gainmap->decStrong(0); +} + +static jlong Gainmap_getNativeFinalizer(JNIEnv*, jobject) { + return static_cast<jlong>(reinterpret_cast<uintptr_t>(&Gainmap_destructor)); +} + +jlong Gainmap_createEmpty(JNIEnv*, jobject) { + Gainmap* gainmap = new Gainmap(); + gainmap->incStrong(0); + return static_cast<jlong>(reinterpret_cast<uintptr_t>(gainmap)); +} + +static void Gainmap_setBitmap(JNIEnv* env, jobject, jlong gainmapPtr, jobject jBitmap) { + android::Bitmap* bitmap = GraphicsJNI::getNativeBitmap(env, jBitmap); + fromJava(gainmapPtr)->bitmap = sk_ref_sp(bitmap); +} + +static void Gainmap_setRatioMin(JNIEnv*, jobject, jlong gainmapPtr, jfloat r, jfloat g, jfloat b) { + fromJava(gainmapPtr)->info.fGainmapRatioMin = {r, g, b, 1.f}; +} + +static void Gainmap_getRatioMin(JNIEnv* env, jobject, jlong gainmapPtr, jfloatArray components) { + const auto value = fromJava(gainmapPtr)->info.fGainmapRatioMin; + jfloat buf[3]{value.fR, value.fG, value.fB}; + env->SetFloatArrayRegion(components, 0, 3, buf); +} + +static void Gainmap_setRatioMax(JNIEnv*, jobject, jlong gainmapPtr, jfloat r, jfloat g, jfloat b) { + fromJava(gainmapPtr)->info.fGainmapRatioMax = {r, g, b, 1.f}; +} + +static void Gainmap_getRatioMax(JNIEnv* env, jobject, jlong gainmapPtr, jfloatArray components) { + const auto value = fromJava(gainmapPtr)->info.fGainmapRatioMax; + jfloat buf[3]{value.fR, value.fG, value.fB}; + env->SetFloatArrayRegion(components, 0, 3, buf); +} + +static void Gainmap_setGamma(JNIEnv*, jobject, jlong gainmapPtr, jfloat r, jfloat g, jfloat b) { + fromJava(gainmapPtr)->info.fGainmapGamma = {r, g, b, 1.f}; +} + +static void Gainmap_getGamma(JNIEnv* env, jobject, jlong gainmapPtr, jfloatArray components) { + const auto value = fromJava(gainmapPtr)->info.fGainmapGamma; + jfloat buf[3]{value.fR, value.fG, value.fB}; + env->SetFloatArrayRegion(components, 0, 3, buf); +} + +static void Gainmap_setEpsilonSdr(JNIEnv*, jobject, jlong gainmapPtr, jfloat r, jfloat g, + jfloat b) { + fromJava(gainmapPtr)->info.fEpsilonSdr = {r, g, b, 1.f}; +} + +static void Gainmap_getEpsilonSdr(JNIEnv* env, jobject, jlong gainmapPtr, jfloatArray components) { + const auto value = fromJava(gainmapPtr)->info.fEpsilonSdr; + jfloat buf[3]{value.fR, value.fG, value.fB}; + env->SetFloatArrayRegion(components, 0, 3, buf); +} + +static void Gainmap_setEpsilonHdr(JNIEnv*, jobject, jlong gainmapPtr, jfloat r, jfloat g, + jfloat b) { + fromJava(gainmapPtr)->info.fEpsilonHdr = {r, g, b, 1.f}; +} + +static void Gainmap_getEpsilonHdr(JNIEnv* env, jobject, jlong gainmapPtr, jfloatArray components) { + const auto value = fromJava(gainmapPtr)->info.fEpsilonHdr; + jfloat buf[3]{value.fR, value.fG, value.fB}; + env->SetFloatArrayRegion(components, 0, 3, buf); +} + +static void Gainmap_setDisplayRatioHdr(JNIEnv*, jobject, jlong gainmapPtr, jfloat max) { + fromJava(gainmapPtr)->info.fDisplayRatioHdr = max; +} + +static jfloat Gainmap_getDisplayRatioHdr(JNIEnv*, jobject, jlong gainmapPtr) { + return fromJava(gainmapPtr)->info.fDisplayRatioHdr; +} + +static void Gainmap_setDisplayRatioSdr(JNIEnv*, jobject, jlong gainmapPtr, jfloat min) { + fromJava(gainmapPtr)->info.fDisplayRatioSdr = min; +} + +static jfloat Gainmap_getDisplayRatioSdr(JNIEnv*, jobject, jlong gainmapPtr) { + return fromJava(gainmapPtr)->info.fDisplayRatioSdr; +} + +// ---------------------------------------------------------------------------- +// Serialization +// ---------------------------------------------------------------------------- + +static void Gainmap_writeToParcel(JNIEnv* env, jobject, jlong nativeObject, jobject parcel) { +#ifdef __ANDROID__ // Layoutlib does not support parcel + if (parcel == NULL) { + ALOGD("write null parcel\n"); + return; + } + ScopedParcel p(env, parcel); + SkGainmapInfo info = fromJava(nativeObject)->info; + // write gainmap to parcel + // ratio min + p.writeFloat(info.fGainmapRatioMin.fR); + p.writeFloat(info.fGainmapRatioMin.fG); + p.writeFloat(info.fGainmapRatioMin.fB); + // ratio max + p.writeFloat(info.fGainmapRatioMax.fR); + p.writeFloat(info.fGainmapRatioMax.fG); + p.writeFloat(info.fGainmapRatioMax.fB); + // gamma + p.writeFloat(info.fGainmapGamma.fR); + p.writeFloat(info.fGainmapGamma.fG); + p.writeFloat(info.fGainmapGamma.fB); + // epsilonsdr + p.writeFloat(info.fEpsilonSdr.fR); + p.writeFloat(info.fEpsilonSdr.fG); + p.writeFloat(info.fEpsilonSdr.fB); + // epsilonhdr + p.writeFloat(info.fEpsilonHdr.fR); + p.writeFloat(info.fEpsilonHdr.fG); + p.writeFloat(info.fEpsilonHdr.fB); + // display ratio sdr + p.writeFloat(info.fDisplayRatioSdr); + // display ratio hdr + p.writeFloat(info.fDisplayRatioHdr); + // base image type + p.writeInt32(static_cast<int32_t>(info.fBaseImageType)); + // type + p.writeInt32(static_cast<int32_t>(info.fType)); +#else + doThrowRE(env, "Cannot use parcels outside of Android!"); +#endif +} + +static void Gainmap_readFromParcel(JNIEnv* env, jobject, jlong nativeObject, jobject parcel) { +#ifdef __ANDROID__ // Layoutlib does not support parcel + if (parcel == NULL) { + jniThrowNullPointerException(env, "parcel cannot be null"); + return; + } + ScopedParcel p(env, parcel); + + SkGainmapInfo info; + info.fGainmapRatioMin = {p.readFloat(), p.readFloat(), p.readFloat(), 1.f}; + info.fGainmapRatioMax = {p.readFloat(), p.readFloat(), p.readFloat(), 1.f}; + info.fGainmapGamma = {p.readFloat(), p.readFloat(), p.readFloat(), 1.f}; + info.fEpsilonSdr = {p.readFloat(), p.readFloat(), p.readFloat(), 1.f}; + info.fEpsilonHdr = {p.readFloat(), p.readFloat(), p.readFloat(), 1.f}; + info.fDisplayRatioSdr = p.readFloat(); + info.fDisplayRatioHdr = p.readFloat(); + info.fBaseImageType = static_cast<SkGainmapInfo::BaseImageType>(p.readInt32()); + info.fType = static_cast<SkGainmapInfo::Type>(p.readInt32()); + + fromJava(nativeObject)->info = info; +#else + jniThrowRuntimeException(env, "Cannot use parcels outside of Android"); +#endif +} + +// ---------------------------------------------------------------------------- +// JNI Glue +// ---------------------------------------------------------------------------- + +static const JNINativeMethod gGainmapMethods[] = { + {"nGetFinalizer", "()J", (void*)Gainmap_getNativeFinalizer}, + {"nCreateEmpty", "()J", (void*)Gainmap_createEmpty}, + {"nSetBitmap", "(JLandroid/graphics/Bitmap;)V", (void*)Gainmap_setBitmap}, + {"nSetRatioMin", "(JFFF)V", (void*)Gainmap_setRatioMin}, + {"nGetRatioMin", "(J[F)V", (void*)Gainmap_getRatioMin}, + {"nSetRatioMax", "(JFFF)V", (void*)Gainmap_setRatioMax}, + {"nGetRatioMax", "(J[F)V", (void*)Gainmap_getRatioMax}, + {"nSetGamma", "(JFFF)V", (void*)Gainmap_setGamma}, + {"nGetGamma", "(J[F)V", (void*)Gainmap_getGamma}, + {"nSetEpsilonSdr", "(JFFF)V", (void*)Gainmap_setEpsilonSdr}, + {"nGetEpsilonSdr", "(J[F)V", (void*)Gainmap_getEpsilonSdr}, + {"nSetEpsilonHdr", "(JFFF)V", (void*)Gainmap_setEpsilonHdr}, + {"nGetEpsilonHdr", "(J[F)V", (void*)Gainmap_getEpsilonHdr}, + {"nSetDisplayRatioHdr", "(JF)V", (void*)Gainmap_setDisplayRatioHdr}, + {"nGetDisplayRatioHdr", "(J)F", (void*)Gainmap_getDisplayRatioHdr}, + {"nSetDisplayRatioSdr", "(JF)V", (void*)Gainmap_setDisplayRatioSdr}, + {"nGetDisplayRatioSdr", "(J)F", (void*)Gainmap_getDisplayRatioSdr}, + {"nWriteGainmapToParcel", "(JLandroid/os/Parcel;)V", (void*)Gainmap_writeToParcel}, + {"nReadGainmapFromParcel", "(JLandroid/os/Parcel;)V", (void*)Gainmap_readFromParcel}, +}; + +int register_android_graphics_Gainmap(JNIEnv* env) { + gGainmap_class = MakeGlobalRefOrDie(env, FindClassOrDie(env, "android/graphics/Gainmap")); + gGainmap_constructorMethodID = + GetMethodIDOrDie(env, gGainmap_class, "<init>", "(Landroid/graphics/Bitmap;J)V"); + return android::RegisterMethodsOrDie(env, "android/graphics/Gainmap", gGainmapMethods, + NELEM(gGainmapMethods)); +} + +} // namespace android diff --git a/libs/hwui/jni/Graphics.cpp b/libs/hwui/jni/Graphics.cpp index 33669ac0a34e..914266de2753 100644 --- a/libs/hwui/jni/Graphics.cpp +++ b/libs/hwui/jni/Graphics.cpp @@ -8,12 +8,19 @@ #include <nativehelper/JNIHelp.h> #include "GraphicsJNI.h" +#include "SkBitmap.h" #include "SkCanvas.h" +#include "SkColorSpace.h" #include "SkFontMetrics.h" -#include "SkMath.h" +#include "SkImageInfo.h" +#include "SkPixelRef.h" +#include "SkPoint.h" +#include "SkRect.h" #include "SkRegion.h" +#include "SkTypes.h" #include <cutils/ashmem.h> #include <hwui/Canvas.h> +#include <log/log.h> using namespace android; @@ -483,7 +490,7 @@ SkRegion* GraphicsJNI::getNativeRegion(JNIEnv* env, jobject region) void GraphicsJNI::set_metrics(JNIEnv* env, jobject metrics, const SkFontMetrics& skmetrics) { if (metrics == nullptr) return; - SkASSERT(env->IsInstanceOf(metrics, gFontMetrics_class)); + LOG_FATAL_IF(!env->IsInstanceOf(metrics, gFontMetrics_class)); env->SetFloatField(metrics, gFontMetrics_top, SkScalarToFloat(skmetrics.fTop)); env->SetFloatField(metrics, gFontMetrics_ascent, SkScalarToFloat(skmetrics.fAscent)); env->SetFloatField(metrics, gFontMetrics_descent, SkScalarToFloat(skmetrics.fDescent)); @@ -497,7 +504,7 @@ int GraphicsJNI::set_metrics_int(JNIEnv* env, jobject metrics, const SkFontMetri int leading = SkScalarRoundToInt(skmetrics.fLeading); if (metrics) { - SkASSERT(env->IsInstanceOf(metrics, gFontMetricsInt_class)); + LOG_FATAL_IF(!env->IsInstanceOf(metrics, gFontMetricsInt_class)); env->SetIntField(metrics, gFontMetricsInt_top, SkScalarFloorToInt(skmetrics.fTop)); env->SetIntField(metrics, gFontMetricsInt_ascent, ascent); env->SetIntField(metrics, gFontMetricsInt_descent, descent); @@ -509,8 +516,7 @@ int GraphicsJNI::set_metrics_int(JNIEnv* env, jobject metrics, const SkFontMetri /////////////////////////////////////////////////////////////////////////////////////////// -jobject GraphicsJNI::createBitmapRegionDecoder(JNIEnv* env, skia::BitmapRegionDecoder* bitmap) -{ +jobject GraphicsJNI::createBitmapRegionDecoder(JNIEnv* env, BitmapRegionDecoderWrapper* bitmap) { ALOG_ASSERT(bitmap != NULL); jobject obj = env->NewObject(gBitmapRegionDecoder_class, @@ -568,14 +574,14 @@ jobject GraphicsJNI::getColorSpace(JNIEnv* env, SkColorSpace* decodeColorSpace, LOG_ALWAYS_FATAL_IF(!decodeColorSpace->toXYZD50(&xyzMatrix)); skcms_TransferFunction transferParams; - // We can only handle numerical transfer functions at the moment - LOG_ALWAYS_FATAL_IF(!decodeColorSpace->isNumericalTransferFn(&transferParams)); + decodeColorSpace->transferFn(&transferParams); + auto res = skcms_TransferFunction_getType(&transferParams); + LOG_ALWAYS_FATAL_IF(res == skcms_TFType_HLGinvish || res == skcms_TFType_Invalid); - jobject params = env->NewObject(gTransferParameters_class, - gTransferParameters_constructorMethodID, - transferParams.a, transferParams.b, transferParams.c, - transferParams.d, transferParams.e, transferParams.f, - transferParams.g); + jobject params; + params = env->NewObject(gTransferParameters_class, gTransferParameters_constructorMethodID, + transferParams.a, transferParams.b, transferParams.c, transferParams.d, + transferParams.e, transferParams.f, transferParams.g); jfloatArray xyzArray = env->NewFloatArray(9); jfloat xyz[9] = { @@ -698,7 +704,9 @@ void RecyclingClippingPixelAllocator::copyIfNecessary() { mSkiaBitmap->info().height()); for (int y = 0; y < rowsToCopy; y++) { memcpy(dst, mSkiaBitmap->getAddr(0, y), bytesToCopy); - dst = SkTAddOffset<void>(dst, dstRowBytes); + // Cast to bytes in order to apply the dstRowBytes offset correctly. + dst = reinterpret_cast<void*>( + reinterpret_cast<uint8_t*>(dst) + dstRowBytes); } recycledPixels->notifyPixelsChanged(); recycledPixels->unref(); @@ -800,8 +808,8 @@ int register_android_graphics_Graphics(JNIEnv* env) gTransferParameters_class = MakeGlobalRefOrDie(env, FindClassOrDie(env, "android/graphics/ColorSpace$Rgb$TransferParameters")); - gTransferParameters_constructorMethodID = GetMethodIDOrDie(env, gTransferParameters_class, - "<init>", "(DDDDDDD)V"); + gTransferParameters_constructorMethodID = + GetMethodIDOrDie(env, gTransferParameters_class, "<init>", "(DDDDDDD)V"); gFontMetrics_class = FindClassOrDie(env, "android/graphics/Paint$FontMetrics"); gFontMetrics_class = MakeGlobalRefOrDie(env, gFontMetrics_class); diff --git a/libs/hwui/jni/GraphicsJNI.h b/libs/hwui/jni/GraphicsJNI.h index 085a905abaf8..24f9e82b5340 100644 --- a/libs/hwui/jni/GraphicsJNI.h +++ b/libs/hwui/jni/GraphicsJNI.h @@ -2,28 +2,25 @@ #define _ANDROID_GRAPHICS_GRAPHICS_JNI_H_ #include <cutils/compiler.h> +#include <hwui/Bitmap.h> +#include <hwui/Canvas.h> -#include "Bitmap.h" #include "BRDAllocator.h" +#include "Bitmap.h" #include "SkBitmap.h" #include "SkCodec.h" -#include "SkPixelRef.h" +#include "SkColorSpace.h" #include "SkMallocPixelRef.h" +#include "SkPixelRef.h" #include "SkPoint.h" #include "SkRect.h" -#include "SkColorSpace.h" -#include <hwui/Canvas.h> -#include <hwui/Bitmap.h> - #include "graphics_jni_helpers.h" class SkCanvas; struct SkFontMetrics; namespace android { -namespace skia { - class BitmapRegionDecoder; -} +class BitmapRegionDecoderWrapper; class Canvas; class Paint; struct Typeface; @@ -49,10 +46,16 @@ public: static void setJavaVM(JavaVM* javaVM); - /** returns a pointer to the JavaVM provided when we initialized the module */ + /** + * returns a pointer to the JavaVM provided when we initialized the module + * DEPRECATED: Objects should know the JavaVM that created them + */ static JavaVM* getJavaVM() { return mJavaVM; } - /** return a pointer to the JNIEnv for this thread */ + /** + * return a pointer to the JNIEnv for this thread + * DEPRECATED: Objects should know the JavaVM that created them + */ static JNIEnv* getJNIEnv(); /** create a JNIEnv* for this thread or assert if one already exists */ @@ -120,7 +123,7 @@ public: static jobject createRegion(JNIEnv* env, SkRegion* region); static jobject createBitmapRegionDecoder(JNIEnv* env, - android::skia::BitmapRegionDecoder* bitmap); + android::BitmapRegionDecoderWrapper* bitmap); /** * Given a bitmap we natively allocate a memory block to store the contents @@ -335,6 +338,34 @@ private: int fLen; }; +class JGlobalRefHolder { +public: + JGlobalRefHolder(JavaVM* vm, jobject object) : mVm(vm), mObject(object) {} + + virtual ~JGlobalRefHolder() { + env()->DeleteGlobalRef(mObject); + mObject = nullptr; + } + + jobject object() { return mObject; } + JavaVM* vm() { return mVm; } + + JNIEnv* env() { + JNIEnv* env; + if (mVm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) { + LOG_ALWAYS_FATAL("Failed to get JNIEnv for JavaVM: %p", mVm); + } + return env; + } + +private: + JGlobalRefHolder(const JGlobalRefHolder&) = delete; + void operator=(const JGlobalRefHolder&) = delete; + + JavaVM* mVm; + jobject mObject; +}; + void doThrowNPE(JNIEnv* env); void doThrowAIOOBE(JNIEnv* env); // Array Index Out Of Bounds Exception void doThrowIAE(JNIEnv* env, const char* msg = NULL); // Illegal Argument diff --git a/libs/hwui/jni/HardwareBufferHelpers.cpp b/libs/hwui/jni/HardwareBufferHelpers.cpp new file mode 100644 index 000000000000..7e3f771b6b3d --- /dev/null +++ b/libs/hwui/jni/HardwareBufferHelpers.cpp @@ -0,0 +1,68 @@ +/* + * 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. + */ + +#include "HardwareBufferHelpers.h" + +#include <dlfcn.h> +#include <log/log.h> + +#ifdef __ANDROID__ +typedef AHardwareBuffer* (*AHB_from_HB)(JNIEnv*, jobject); +typedef jobject (*AHB_to_HB)(JNIEnv*, AHardwareBuffer*); +static AHB_from_HB fromHardwareBuffer = nullptr; +static AHB_to_HB toHardwareBuffer = nullptr; +#endif + +void android::uirenderer::HardwareBufferHelpers::init() { +#ifdef __ANDROID__ // Layoutlib does not support graphic buffer or parcel + void* handle_ = dlopen("libandroid.so", RTLD_NOW | RTLD_NODELETE); + fromHardwareBuffer = (AHB_from_HB)dlsym(handle_, "AHardwareBuffer_fromHardwareBuffer"); + LOG_ALWAYS_FATAL_IF(fromHardwareBuffer == nullptr, + "Failed to find required symbol AHardwareBuffer_fromHardwareBuffer!"); + + toHardwareBuffer = (AHB_to_HB)dlsym(handle_, "AHardwareBuffer_toHardwareBuffer"); + LOG_ALWAYS_FATAL_IF(toHardwareBuffer == nullptr, + " Failed to find required symbol AHardwareBuffer_toHardwareBuffer!"); +#endif +} + +AHardwareBuffer* android::uirenderer::HardwareBufferHelpers::AHardwareBuffer_fromHardwareBuffer( + JNIEnv* env, jobject hardwarebuffer) { +#ifdef __ANDROID__ + LOG_ALWAYS_FATAL_IF(fromHardwareBuffer == nullptr, + "Failed to find symbol AHardwareBuffer_fromHardwareBuffer, did you forget " + "to call HardwareBufferHelpers::init?"); + return fromHardwareBuffer(env, hardwarebuffer); +#else + ALOGE("ERROR attempting to invoke AHardwareBuffer_fromHardwareBuffer on non Android " + "configuration"); + return nullptr; +#endif +} + +jobject android::uirenderer::HardwareBufferHelpers::AHardwareBuffer_toHardwareBuffer( + JNIEnv* env, AHardwareBuffer* ahardwarebuffer) { +#ifdef __ANDROID__ + LOG_ALWAYS_FATAL_IF(toHardwareBuffer == nullptr, + "Failed to find symbol AHardwareBuffer_toHardwareBuffer, did you forget to " + "call HardwareBufferHelpers::init?"); + return toHardwareBuffer(env, ahardwarebuffer); +#else + ALOGE("ERROR attempting to invoke AHardwareBuffer_toHardwareBuffer on non Android " + "configuration"); + return nullptr; +#endif +}
\ No newline at end of file diff --git a/libs/hwui/jni/HardwareBufferHelpers.h b/libs/hwui/jni/HardwareBufferHelpers.h new file mode 100644 index 000000000000..326babfb0b34 --- /dev/null +++ b/libs/hwui/jni/HardwareBufferHelpers.h @@ -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. + */ +#ifndef HARDWAREBUFFER_JNI_HELPERS_H +#define HARDWAREBUFFER_JNI_HELPERS_H + +#include <android/bitmap.h> +#include <jni.h> + +namespace android { +namespace uirenderer { + +class HardwareBufferHelpers { +public: + static void init(); + static AHardwareBuffer* AHardwareBuffer_fromHardwareBuffer(JNIEnv*, jobject); + static jobject AHardwareBuffer_toHardwareBuffer(JNIEnv*, AHardwareBuffer*); + +private: + HardwareBufferHelpers() = default; // not to be instantiated +}; + +} // namespace uirenderer +} // namespace android + +#endif // HARDWAREBUFFER_JNI_HELPERS_H
\ No newline at end of file diff --git a/libs/hwui/jni/ImageDecoder.cpp b/libs/hwui/jni/ImageDecoder.cpp index f7b8c014be6e..db1c188e425e 100644 --- a/libs/hwui/jni/ImageDecoder.cpp +++ b/libs/hwui/jni/ImageDecoder.cpp @@ -14,28 +14,38 @@ * limitations under the License. */ -#include "Bitmap.h" -#include "BitmapFactory.h" -#include "ByteBufferStreamAdaptor.h" -#include "CreateJavaOutputStreamAdaptor.h" -#include "GraphicsJNI.h" #include "ImageDecoder.h" -#include "NinePatchPeeker.h" -#include "Utils.h" - -#include <hwui/Bitmap.h> -#include <hwui/ImageDecoder.h> -#include <HardwareBitmapUploader.h> #include <FrontBufferedStream.h> +#include <HardwareBitmapUploader.h> +#include <SkAlphaType.h> #include <SkAndroidCodec.h> -#include <SkEncodedImageFormat.h> +#include <SkBitmap.h> +#include <SkCodec.h> +#include <SkCodecAnimation.h> +#include <SkColorSpace.h> +#include <SkColorType.h> +#include <SkImageInfo.h> +#include <SkRect.h> +#include <SkSize.h> #include <SkStream.h> - +#include <SkString.h> #include <androidfw/Asset.h> #include <fcntl.h> +#include <gui/TraceUtils.h> +#include <hwui/Bitmap.h> +#include <hwui/ImageDecoder.h> #include <sys/stat.h> +#include "Bitmap.h" +#include "BitmapFactory.h" +#include "ByteBufferStreamAdaptor.h" +#include "CreateJavaOutputStreamAdaptor.h" +#include "Gainmap.h" +#include "GraphicsJNI.h" +#include "NinePatchPeeker.h" +#include "Utils.h" + using namespace android; static jclass gImageDecoder_class; @@ -242,6 +252,7 @@ static jobject ImageDecoder_nDecodeBitmap(JNIEnv* env, jobject /*clazz*/, jlong jboolean requireUnpremul, jboolean preferRamOverQuality, jboolean asAlphaMask, jlong colorSpaceHandle, jboolean extended) { + ATRACE_CALL(); auto* decoder = reinterpret_cast<ImageDecoder*>(nativePtr); if (!decoder->setTargetSize(targetWidth, targetHeight)) { doThrowISE(env, "Could not scale to target size!"); @@ -332,10 +343,22 @@ static jobject ImageDecoder_nDecodeBitmap(JNIEnv* env, jobject /*clazz*/, jlong return nullptr; } + ATRACE_FORMAT("Decoding %dx%d bitmap", bitmapInfo.width(), bitmapInfo.height()); SkCodec::Result result = decoder->decode(bm.getPixels(), bm.rowBytes()); jthrowable jexception = get_and_clear_exception(env); - int onPartialImageError = jexception ? kSourceException - : 0; // No error. + int onPartialImageError = jexception ? kSourceException : 0; // No error. + + // Only attempt to extract the gainmap if we're not post-processing, as we can't automatically + // mimic that to the gainmap and expect it to be meaningful. And also don't extract the gainmap + // if we're prioritizing RAM over quality, since the gainmap improves quality at the + // cost of RAM + if (result == SkCodec::kSuccess && !jpostProcess && !preferRamOverQuality) { + // The gainmap costs RAM to improve quality, so skip this if we're prioritizing RAM instead + result = decoder->extractGainmap(nativeBitmap.get(), + allocator == kSharedMemory_Allocator ? true : false); + jexception = get_and_clear_exception(env); + } + switch (result) { case SkCodec::kSuccess: // Ignore the exception, since the decode was successful anyway. @@ -446,6 +469,12 @@ static jobject ImageDecoder_nDecodeBitmap(JNIEnv* env, jobject /*clazz*/, jlong sk_sp<Bitmap> hwBitmap = Bitmap::allocateHardwareBitmap(bm); if (hwBitmap) { hwBitmap->setImmutable(); + if (nativeBitmap->hasGainmap()) { + auto gm = uirenderer::Gainmap::allocateHardwareGainmap(nativeBitmap->gainmap()); + if (gm) { + hwBitmap->setGainmap(std::move(gm)); + } + } return bitmap::createBitmap(env, hwBitmap.release(), bitmapCreateFlags, ninePatchChunk, ninePatchInsets); } diff --git a/libs/hwui/jni/Interpolator.cpp b/libs/hwui/jni/Interpolator.cpp index fc3d70b87f5a..c71e3085caf5 100644 --- a/libs/hwui/jni/Interpolator.cpp +++ b/libs/hwui/jni/Interpolator.cpp @@ -24,12 +24,8 @@ static void Interpolator_setKeyFrame(JNIEnv* env, jobject clazz, jlong interpHan AutoJavaFloatArray autoValues(env, valueArray); AutoJavaFloatArray autoBlend(env, blendArray, 4); -#ifdef SK_SCALAR_IS_FLOAT SkScalar* scalars = autoValues.ptr(); SkScalar* blend = autoBlend.ptr(); -#else - #error Need to convert float array to SkScalar array before calling the following function. -#endif interp->setKeyFrame(index, msec, scalars, blend); } diff --git a/libs/hwui/jni/JvmErrorReporter.h b/libs/hwui/jni/JvmErrorReporter.h new file mode 100644 index 000000000000..3a3587572a1a --- /dev/null +++ b/libs/hwui/jni/JvmErrorReporter.h @@ -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. + */ +#ifndef JVMERRORREPORTER_H +#define JVMERRORREPORTER_H + +#include <TreeInfo.h> +#include <jni.h> +#include <nativehelper/JNIHelp.h> + +#include "GraphicsJNI.h" + +namespace android { +namespace uirenderer { + +class JvmErrorReporter : public android::uirenderer::ErrorHandler { +public: + JvmErrorReporter(JNIEnv* env) { env->GetJavaVM(&mVm); } + + virtual void onError(const std::string& message) override { + JNIEnv* env; + if (mVm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) { + LOG_ALWAYS_FATAL("Failed to get JNIEnv for JavaVM: %p", mVm); + } + jniThrowException(env, "java/lang/IllegalStateException", message.c_str()); + } + +private: + JavaVM* mVm; +}; + +} // namespace uirenderer +} // namespace android + +#endif // JVMERRORREPORTER_H diff --git a/libs/hwui/jni/MaskFilter.cpp b/libs/hwui/jni/MaskFilter.cpp index 5383032e0f77..048ce025ce27 100644 --- a/libs/hwui/jni/MaskFilter.cpp +++ b/libs/hwui/jni/MaskFilter.cpp @@ -2,6 +2,7 @@ #include "SkMaskFilter.h" #include "SkBlurMask.h" #include "SkBlurMaskFilter.h" +#include "SkBlurTypes.h" #include "SkTableMaskFilter.h" static void ThrowIAE_IfNull(JNIEnv* env, void* ptr) { diff --git a/libs/hwui/jni/MeshSpecification.cpp b/libs/hwui/jni/MeshSpecification.cpp new file mode 100644 index 000000000000..ae9792df3d82 --- /dev/null +++ b/libs/hwui/jni/MeshSpecification.cpp @@ -0,0 +1,164 @@ +/* + * 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. + */ + +#include <SkMesh.h> + +#include "GraphicsJNI.h" +#include "graphics_jni_helpers.h" + +namespace android { + +using Attribute = SkMeshSpecification::Attribute; +using Varying = SkMeshSpecification::Varying; + +static struct { + jclass clazz{}; + jfieldID type{}; + jfieldID offset{}; + jfieldID name{}; +} gAttributeInfo; + +static struct { + jclass clazz{}; + jfieldID type{}; + jfieldID name{}; +} gVaryingInfo; + +std::vector<Attribute> extractAttributes(JNIEnv* env, jobjectArray attributes) { + int size = env->GetArrayLength(attributes); + std::vector<Attribute> attVector; + attVector.reserve(size); + for (int i = 0; i < size; i++) { + jobject attribute = env->GetObjectArrayElement(attributes, i); + auto name = (jstring)env->GetObjectField(attribute, gAttributeInfo.name); + auto attName = ScopedUtfChars(env, name); + Attribute temp{Attribute::Type(env->GetIntField(attribute, gAttributeInfo.type)), + static_cast<size_t>(env->GetIntField(attribute, gAttributeInfo.offset)), + SkString(attName.c_str())}; + attVector.push_back(std::move(temp)); + } + return attVector; +} + +std::vector<Varying> extractVaryings(JNIEnv* env, jobjectArray varyings) { + int size = env->GetArrayLength(varyings); + std::vector<Varying> varyVector; + varyVector.reserve(size); + for (int i = 0; i < size; i++) { + jobject varying = env->GetObjectArrayElement(varyings, i); + auto name = (jstring)env->GetObjectField(varying, gVaryingInfo.name); + auto varyName = ScopedUtfChars(env, name); + Varying temp{Varying::Type(env->GetIntField(varying, gVaryingInfo.type)), + SkString(varyName.c_str())}; + varyVector.push_back(std::move(temp)); + } + + return varyVector; +} + +static jlong Make(JNIEnv* env, jobject thiz, jobjectArray attributeArray, jint vertexStride, + jobjectArray varyingArray, jstring vertexShader, jstring fragmentShader) { + auto attributes = extractAttributes(env, attributeArray); + auto varyings = extractVaryings(env, varyingArray); + auto skVertexShader = ScopedUtfChars(env, vertexShader); + auto skFragmentShader = ScopedUtfChars(env, fragmentShader); + auto meshSpecResult = SkMeshSpecification::Make(attributes, vertexStride, varyings, + SkString(skVertexShader.c_str()), + SkString(skFragmentShader.c_str())); + if (meshSpecResult.specification.get() == nullptr) { + jniThrowException(env, "java/lang/IllegalArgumentException", meshSpecResult.error.c_str()); + } + + return reinterpret_cast<jlong>(meshSpecResult.specification.release()); +} + +static jlong MakeWithCS(JNIEnv* env, jobject thiz, jobjectArray attributeArray, jint vertexStride, + jobjectArray varyingArray, jstring vertexShader, jstring fragmentShader, + jlong colorSpace) { + auto attributes = extractAttributes(env, attributeArray); + auto varyings = extractVaryings(env, varyingArray); + auto skVertexShader = ScopedUtfChars(env, vertexShader); + auto skFragmentShader = ScopedUtfChars(env, fragmentShader); + auto meshSpecResult = SkMeshSpecification::Make( + attributes, vertexStride, varyings, SkString(skVertexShader.c_str()), + SkString(skFragmentShader.c_str()), GraphicsJNI::getNativeColorSpace(colorSpace)); + + if (meshSpecResult.specification.get() == nullptr) { + jniThrowException(env, "java/lang/IllegalArgumentException", meshSpecResult.error.c_str()); + } + + return reinterpret_cast<jlong>(meshSpecResult.specification.release()); +} + +static jlong MakeWithAlpha(JNIEnv* env, jobject thiz, jobjectArray attributeArray, + jint vertexStride, jobjectArray varyingArray, jstring vertexShader, + jstring fragmentShader, jlong colorSpace, jint alphaType) { + auto attributes = extractAttributes(env, attributeArray); + auto varyings = extractVaryings(env, varyingArray); + auto skVertexShader = ScopedUtfChars(env, vertexShader); + auto skFragmentShader = ScopedUtfChars(env, fragmentShader); + auto meshSpecResult = SkMeshSpecification::Make( + attributes, vertexStride, varyings, SkString(skVertexShader.c_str()), + SkString(skFragmentShader.c_str()), GraphicsJNI::getNativeColorSpace(colorSpace), + SkAlphaType(alphaType)); + + if (meshSpecResult.specification.get() == nullptr) { + jniThrowException(env, "java/lang/IllegalArgumentException", meshSpecResult.error.c_str()); + } + + return reinterpret_cast<jlong>(meshSpecResult.specification.release()); +} + +static void MeshSpecification_safeUnref(SkMeshSpecification* meshSpec) { + SkSafeUnref(meshSpec); +} + +static jlong getMeshSpecificationFinalizer() { + return static_cast<jlong>(reinterpret_cast<uintptr_t>(&MeshSpecification_safeUnref)); +} + +static const JNINativeMethod gMeshSpecificationMethods[] = { + {"nativeGetFinalizer", "()J", (void*)getMeshSpecificationFinalizer}, + {"nativeMake", + "([Landroid/graphics/MeshSpecification$Attribute;I[Landroid/graphics/" + "MeshSpecification$Varying;" + "Ljava/lang/String;Ljava/lang/String;)J", + (void*)Make}, + {"nativeMakeWithCS", + "([Landroid/graphics/MeshSpecification$Attribute;I" + "[Landroid/graphics/MeshSpecification$Varying;Ljava/lang/String;Ljava/lang/String;J)J", + (void*)MakeWithCS}, + {"nativeMakeWithAlpha", + "([Landroid/graphics/MeshSpecification$Attribute;I" + "[Landroid/graphics/MeshSpecification$Varying;Ljava/lang/String;Ljava/lang/String;JI)J", + (void*)MakeWithAlpha}}; + +int register_android_graphics_MeshSpecification(JNIEnv* env) { + android::RegisterMethodsOrDie(env, "android/graphics/MeshSpecification", + gMeshSpecificationMethods, NELEM(gMeshSpecificationMethods)); + + gAttributeInfo.clazz = env->FindClass("android/graphics/MeshSpecification$Attribute"); + gAttributeInfo.type = env->GetFieldID(gAttributeInfo.clazz, "mType", "I"); + gAttributeInfo.offset = env->GetFieldID(gAttributeInfo.clazz, "mOffset", "I"); + gAttributeInfo.name = env->GetFieldID(gAttributeInfo.clazz, "mName", "Ljava/lang/String;"); + + gVaryingInfo.clazz = env->FindClass("android/graphics/MeshSpecification$Varying"); + gVaryingInfo.type = env->GetFieldID(gVaryingInfo.clazz, "mType", "I"); + gVaryingInfo.name = env->GetFieldID(gVaryingInfo.clazz, "mName", "Ljava/lang/String;"); + return 0; +} + +} // namespace android diff --git a/libs/hwui/jni/Movie.cpp b/libs/hwui/jni/Movie.cpp index bb8c99a73edf..7dfd8740b2d7 100644 --- a/libs/hwui/jni/Movie.cpp +++ b/libs/hwui/jni/Movie.cpp @@ -3,8 +3,8 @@ #include "GraphicsJNI.h" #include <nativehelper/ScopedLocalRef.h> #include "Movie.h" +#include "SkRefCnt.h" #include "SkStream.h" -#include "SkUtils.h" #include "Utils.h" #include <androidfw/Asset.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..13357fa25e8c 100644 --- a/libs/hwui/jni/Paint.cpp +++ b/libs/hwui/jni/Paint.cpp @@ -26,16 +26,17 @@ #include <nativehelper/ScopedPrimitiveArray.h> #include "SkColorFilter.h" +#include "SkColorSpace.h" #include "SkFont.h" #include "SkFontMetrics.h" #include "SkFontTypes.h" #include "SkMaskFilter.h" #include "SkPath.h" #include "SkPathEffect.h" +#include "SkPathUtils.h" #include "SkShader.h" #include "SkBlendMode.h" #include "unicode/uloc.h" -#include "unicode/ushape.h" #include "utils/Blur.h" #include <hwui/BlurDrawLooper.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; } @@ -779,7 +809,7 @@ namespace PaintGlue { Paint* obj = reinterpret_cast<Paint*>(objHandle); SkPath* src = reinterpret_cast<SkPath*>(srcHandle); SkPath* dst = reinterpret_cast<SkPath*>(dstHandle); - return obj->getFillPath(*src, dst) ? JNI_TRUE : JNI_FALSE; + return skpathutils::FillPathWithPaint(*src, *obj, dst) ? JNI_TRUE : JNI_FALSE; } static jlong setShader(CRITICAL_JNI_PARAMS_COMMA jlong objHandle, jlong shaderHandle) { @@ -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..a5e04763d885 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); @@ -170,11 +182,7 @@ public: SkPath* obj = reinterpret_cast<SkPath*>(objHandle); SkPathDirection dir = static_cast<SkPathDirection>(dirHandle); AutoJavaFloatArray afa(env, array, 8); -#ifdef SK_SCALAR_IS_FLOAT const float* src = afa.ptr(); -#else - #error Need to convert float array to SkScalar array before calling the following function. -#endif obj->addRoundRect(rect, src, dir); } @@ -209,6 +217,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 +489,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 +532,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/PathEffect.cpp b/libs/hwui/jni/PathEffect.cpp index f99bef7b7d58..3dbe1a67f52e 100644 --- a/libs/hwui/jni/PathEffect.cpp +++ b/libs/hwui/jni/PathEffect.cpp @@ -35,11 +35,7 @@ public: jfloatArray intervalArray, jfloat phase) { AutoJavaFloatArray autoInterval(env, intervalArray); int count = autoInterval.length() & ~1; // even number -#ifdef SK_SCALAR_IS_FLOAT - SkScalar* intervals = autoInterval.ptr(); -#else - #error Need to convert float array to SkScalar array before calling the following function. -#endif + SkScalar* intervals = autoInterval.ptr(); SkPathEffect* effect = SkDashPathEffect::Make(intervals, count, phase).release(); return reinterpret_cast<jlong>(effect); } 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/RenderEffect.cpp b/libs/hwui/jni/RenderEffect.cpp index 213f35a81b88..f3db1705e694 100644 --- a/libs/hwui/jni/RenderEffect.cpp +++ b/libs/hwui/jni/RenderEffect.cpp @@ -15,6 +15,7 @@ */ #include "Bitmap.h" #include "GraphicsJNI.h" +#include "SkBlendMode.h" #include "SkImageFilter.h" #include "SkImageFilters.h" #include "graphics_jni_helpers.h" diff --git a/libs/hwui/jni/ScopedParcel.cpp b/libs/hwui/jni/ScopedParcel.cpp new file mode 100644 index 000000000000..b0f5423813b7 --- /dev/null +++ b/libs/hwui/jni/ScopedParcel.cpp @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include "ScopedParcel.h" + +#ifdef __ANDROID__ // Layoutlib does not support parcel + +using namespace android; + +int32_t ScopedParcel::readInt32() { + int32_t temp = 0; + // TODO: This behavior-matches what android::Parcel does + // but this should probably be better + if (AParcel_readInt32(mParcel, &temp) != STATUS_OK) { + temp = 0; + } + return temp; +} + +uint32_t ScopedParcel::readUint32() { + uint32_t temp = 0; + // TODO: This behavior-matches what android::Parcel does + // but this should probably be better + if (AParcel_readUint32(mParcel, &temp) != STATUS_OK) { + temp = 0; + } + return temp; +} + +float ScopedParcel::readFloat() { + float temp = 0.; + if (AParcel_readFloat(mParcel, &temp) != STATUS_OK) { + temp = 0.; + } + return temp; +} + +std::optional<sk_sp<SkData>> ScopedParcel::readData() { + struct Data { + void* ptr = nullptr; + size_t size = 0; + } data; + auto error = AParcel_readByteArray( + mParcel, &data, [](void* arrayData, int32_t length, int8_t** outBuffer) -> bool { + Data* data = reinterpret_cast<Data*>(arrayData); + if (length > 0) { + data->ptr = sk_malloc_canfail(length); + if (!data->ptr) { + return false; + } + *outBuffer = reinterpret_cast<int8_t*>(data->ptr); + data->size = length; + } + return true; + }); + if (error != STATUS_OK || data.size <= 0) { + sk_free(data.ptr); + return std::nullopt; + } else { + return SkData::MakeFromMalloc(data.ptr, data.size); + } +} + +void ScopedParcel::writeData(const std::optional<sk_sp<SkData>>& optData) { + if (optData) { + const auto& data = *optData; + AParcel_writeByteArray(mParcel, reinterpret_cast<const int8_t*>(data->data()), + data->size()); + } else { + AParcel_writeByteArray(mParcel, nullptr, -1); + } +} +#endif // __ANDROID__ // Layoutlib does not support parcel diff --git a/libs/hwui/jni/ScopedParcel.h b/libs/hwui/jni/ScopedParcel.h new file mode 100644 index 000000000000..fd8d6a210f0f --- /dev/null +++ b/libs/hwui/jni/ScopedParcel.h @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include "SkData.h" + +#ifdef __ANDROID__ // Layoutlib does not support parcel +#include <android-base/unique_fd.h> +#include <android/binder_parcel.h> +#include <android/binder_parcel_jni.h> +#include <android/binder_parcel_platform.h> +#include <cutils/ashmem.h> +#include <renderthread/RenderProxy.h> + +class ScopedParcel { +public: + explicit ScopedParcel(JNIEnv* env, jobject parcel) { + mParcel = AParcel_fromJavaParcel(env, parcel); + } + + ~ScopedParcel() { AParcel_delete(mParcel); } + + int32_t readInt32(); + + uint32_t readUint32(); + + float readFloat(); + + void writeInt32(int32_t value) { AParcel_writeInt32(mParcel, value); } + + void writeUint32(uint32_t value) { AParcel_writeUint32(mParcel, value); } + + void writeFloat(float value) { AParcel_writeFloat(mParcel, value); } + + bool allowFds() const { return AParcel_getAllowFds(mParcel); } + + std::optional<sk_sp<SkData>> readData(); + + void writeData(const std::optional<sk_sp<SkData>>& optData); + + AParcel* get() { return mParcel; } + +private: + AParcel* mParcel; +}; + +enum class BlobType : int32_t { + IN_PLACE, + ASHMEM, +}; + +#endif // __ANDROID__ // Layoutlib does not support parcel
\ No newline at end of file diff --git a/libs/hwui/jni/Shader.cpp b/libs/hwui/jni/Shader.cpp index 0bbd8a8cf97c..7eb79be6f55b 100644 --- a/libs/hwui/jni/Shader.cpp +++ b/libs/hwui/jni/Shader.cpp @@ -1,22 +1,34 @@ #undef LOG_TAG #define LOG_TAG "ShaderJNI" +#include <vector> + +#include "Gainmap.h" #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 "effects/GainmapRenderer.h" #include "include/effects/SkRuntimeEffect.h" -#include <vector> - 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; @@ -42,12 +54,7 @@ static void Color_RGBToHSV(JNIEnv* env, jobject, jint red, jint green, jint blue static jint Color_HSVToColor(JNIEnv* env, jobject, jint alpha, jfloatArray hsvArray) { AutoJavaFloatArray autoHSV(env, hsvArray, 3); -#ifdef SK_SCALAR_IS_FLOAT - SkScalar* hsv = autoHSV.ptr(); -#else - #error Need to convert float array to SkScalar array before calling the following function. -#endif - + SkScalar* hsv = autoHSV.ptr(); return static_cast<jint>(SkHSVToColor(alpha, hsv)); } @@ -61,25 +68,35 @@ static jlong Shader_getNativeFinalizer(JNIEnv*, jobject) { return static_cast<jlong>(reinterpret_cast<uintptr_t>(&Shader_safeUnref)); } -/////////////////////////////////////////////////////////////////////////////////////////////// - -static jlong BitmapShader_constructor(JNIEnv* env, jobject o, jlong matrixPtr, jlong bitmapHandle, - jint tileModeX, jint tileModeY, bool filter, - bool isDirectSampled) { +static jlong createBitmapShaderHelper(JNIEnv* env, jobject o, jlong matrixPtr, jlong bitmapHandle, + jint tileModeX, jint tileModeY, bool isDirectSampled, + const SkSamplingOptions& sampling) { const SkMatrix* matrix = reinterpret_cast<const SkMatrix*>(matrixPtr); sk_sp<SkImage> image; if (bitmapHandle) { // Only pass a valid SkBitmap object to the constructor if the Bitmap exists. Otherwise, // we'll pass an empty SkBitmap to avoid crashing/excepting for compatibility. - image = android::bitmap::toBitmap(bitmapHandle).makeImage(); + auto& bitmap = android::bitmap::toBitmap(bitmapHandle); + image = bitmap.makeImage(); + + if (!isDirectSampled && bitmap.hasGainmap()) { + sk_sp<SkShader> gainmapShader = MakeGainmapShader( + image, bitmap.gainmap()->bitmap->makeImage(), bitmap.gainmap()->info, + (SkTileMode)tileModeX, (SkTileMode)tileModeY, sampling); + if (gainmapShader) { + if (matrix) { + gainmapShader = gainmapShader->makeWithLocalMatrix(*matrix); + } + return reinterpret_cast<jlong>(gainmapShader.release()); + } + } } if (!image.get()) { SkBitmap bitmap; image = SkMakeImageFromRasterBitmap(bitmap, kNever_SkCopyPixelsMode); } - SkSamplingOptions sampling(filter ? SkFilterMode::kLinear : SkFilterMode::kNearest, - SkMipmapMode::kNone); + sk_sp<SkShader> shader; if (isDirectSampled) { shader = image->makeRawShader((SkTileMode)tileModeX, (SkTileMode)tileModeY, sampling); @@ -97,6 +114,26 @@ static jlong BitmapShader_constructor(JNIEnv* env, jobject o, jlong matrixPtr, j /////////////////////////////////////////////////////////////////////////////////////////////// +static jlong BitmapShader_constructor(JNIEnv* env, jobject o, jlong matrixPtr, jlong bitmapHandle, + jint tileModeX, jint tileModeY, bool filter, + bool isDirectSampled) { + SkSamplingOptions sampling(filter ? SkFilterMode::kLinear : SkFilterMode::kNearest, + SkMipmapMode::kNone); + return createBitmapShaderHelper(env, o, matrixPtr, bitmapHandle, tileModeX, tileModeY, + isDirectSampled, sampling); +} + +static jlong BitmapShader_constructorWithMaxAniso(JNIEnv* env, jobject o, jlong matrixPtr, + jlong bitmapHandle, jint tileModeX, + jint tileModeY, jint maxAniso, + bool isDirectSampled) { + auto sampling = SkSamplingOptions::Aniso(static_cast<int>(maxAniso)); + return createBitmapShaderHelper(env, o, matrixPtr, bitmapHandle, tileModeX, tileModeY, + isDirectSampled, sampling); +} + +/////////////////////////////////////////////////////////////////////////////////////////////// + static std::vector<SkColor4f> convertColorLongs(JNIEnv* env, jlongArray colorArray) { const size_t count = env->GetArrayLength(colorArray); const jlong* colorValues = env->GetLongArrayElements(colorArray, nullptr); @@ -122,11 +159,7 @@ static jlong LinearGradient_create(JNIEnv* env, jobject, jlong matrixPtr, std::vector<SkColor4f> colors = convertColorLongs(env, colorArray); AutoJavaFloatArray autoPos(env, posArray, colors.size()); -#ifdef SK_SCALAR_IS_FLOAT SkScalar* pos = autoPos.ptr(); -#else - #error Need to convert float array to SkScalar array before calling the following function. -#endif sk_sp<SkShader> shader(SkGradientShader::MakeLinear(pts, &colors[0], GraphicsJNI::getNativeColorSpace(colorSpaceHandle), pos, colors.size(), @@ -166,11 +199,7 @@ static jlong RadialGradient_create(JNIEnv* env, std::vector<SkColor4f> colors = convertColorLongs(env, colorArray); AutoJavaFloatArray autoPos(env, posArray, colors.size()); -#ifdef SK_SCALAR_IS_FLOAT SkScalar* pos = autoPos.ptr(); -#else - #error Need to convert float array to SkScalar array before calling the following function. -#endif auto colorSpace = GraphicsJNI::getNativeColorSpace(colorSpaceHandle); auto skTileMode = static_cast<SkTileMode>(tileMode); @@ -198,11 +227,7 @@ static jlong SweepGradient_create(JNIEnv* env, jobject, jlong matrixPtr, jfloat std::vector<SkColor4f> colors = convertColorLongs(env, colorArray); AutoJavaFloatArray autoPos(env, jpositions, colors.size()); -#ifdef SK_SCALAR_IS_FLOAT SkScalar* pos = autoPos.ptr(); -#else - #error Need to convert float array to SkScalar array before calling the following function. -#endif sk_sp<SkShader> shader = SkGradientShader::MakeSweep(x, y, &colors[0], GraphicsJNI::getNativeColorSpace(colorSpaceHandle), pos, colors.size(), @@ -398,6 +423,8 @@ static const JNINativeMethod gShaderMethods[] = { static const JNINativeMethod gBitmapShaderMethods[] = { {"nativeCreate", "(JJIIZZ)J", (void*)BitmapShader_constructor}, + {"nativeCreateWithMaxAniso", "(JJIIIZ)J", (void*)BitmapShader_constructorWithMaxAniso}, + }; static const JNINativeMethod gLinearGradientMethods[] = { 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.cpp b/libs/hwui/jni/Utils.cpp index 106c6db57e18..9f5a2145792c 100644 --- a/libs/hwui/jni/Utils.cpp +++ b/libs/hwui/jni/Utils.cpp @@ -15,8 +15,9 @@ */ #include "Utils.h" -#include "SkUtils.h" #include "SkData.h" +#include "SkRefCnt.h" +#include "SkStream.h" #include <inttypes.h> #include <log/log.h> 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..8874ef1d2fe0 100644 --- a/libs/hwui/jni/YuvToJpegEncoder.cpp +++ b/libs/hwui/jni/YuvToJpegEncoder.cpp @@ -1,11 +1,25 @@ +#undef LOG_TAG +#define LOG_TAG "YuvToJpegEncoder" + #include "CreateJavaOutputStreamAdaptor.h" -#include "SkJPEGWriteUtility.h" +#include "SkStream.h" #include "YuvToJpegEncoder.h" #include <ui/PixelFormat.h> #include <hardware/hardware.h> #include "graphics_jni_helpers.h" +#include <csetjmp> + +extern "C" { + // We need to include stdio.h before jpeg because jpeg does not include it, but uses FILE + // See https://github.com/libjpeg-turbo/libjpeg-turbo/issues/17 + #include <stdio.h> + #include "jpeglib.h" + #include "jerror.h" + #include "jmorecfg.h" +} + YuvToJpegEncoder* YuvToJpegEncoder::create(int format, int* strides) { // Only ImageFormat.NV21 and ImageFormat.YUY2 are supported // for now. @@ -32,11 +46,64 @@ void error_exit(j_common_ptr cinfo) { longjmp(err->jmp, 1); } +/* + * Destination struct for directing decompressed pixels to a SkStream. + */ +static constexpr size_t kMgrBufferSize = 1024; +struct skstream_destination_mgr : jpeg_destination_mgr { + skstream_destination_mgr(SkWStream* stream); + + SkWStream* const fStream; + + uint8_t fBuffer[kMgrBufferSize]; +}; + +static void sk_init_destination(j_compress_ptr cinfo) { + skstream_destination_mgr* dest = (skstream_destination_mgr*)cinfo->dest; + + dest->next_output_byte = dest->fBuffer; + dest->free_in_buffer = kMgrBufferSize; +} + +static boolean sk_empty_output_buffer(j_compress_ptr cinfo) { + skstream_destination_mgr* dest = (skstream_destination_mgr*)cinfo->dest; + + if (!dest->fStream->write(dest->fBuffer, kMgrBufferSize)) { + ERREXIT(cinfo, JERR_FILE_WRITE); + return FALSE; + } + + dest->next_output_byte = dest->fBuffer; + dest->free_in_buffer = kMgrBufferSize; + return TRUE; +} + +static void sk_term_destination(j_compress_ptr cinfo) { + skstream_destination_mgr* dest = (skstream_destination_mgr*)cinfo->dest; + + size_t size = kMgrBufferSize - dest->free_in_buffer; + if (size > 0) { + if (!dest->fStream->write(dest->fBuffer, size)) { + ERREXIT(cinfo, JERR_FILE_WRITE); + return; + } + } + + dest->fStream->flush(); +} + +skstream_destination_mgr::skstream_destination_mgr(SkWStream* stream) + : fStream(stream) { + this->init_destination = sk_init_destination; + this->empty_output_buffer = sk_empty_output_buffer; + this->term_destination = sk_term_destination; +} + bool YuvToJpegEncoder::encode(SkWStream* stream, void* inYuv, int width, int height, int* offsets, int jpegQuality) { - jpeg_compress_struct cinfo; - ErrorMgr err; - skjpeg_destination_mgr sk_wstream(stream); + jpeg_compress_struct cinfo; + ErrorMgr err; + skstream_destination_mgr sk_wstream(stream); cinfo.err = jpeg_std_error(&err.pub); err.pub.error_exit = error_exit; @@ -231,6 +298,99 @@ void Yuv422IToJpegEncoder::configSamplingFactors(jpeg_compress_struct* cinfo) { } /////////////////////////////////////////////////////////////////////////////// +using namespace android::jpegrecoverymap; + +jpegr_color_gamut P010Yuv420ToJpegREncoder::findColorGamut(JNIEnv* env, int aDataSpace) { + switch (aDataSpace & ADataSpace::STANDARD_MASK) { + case ADataSpace::STANDARD_BT709: + return jpegr_color_gamut::JPEGR_COLORGAMUT_BT709; + case ADataSpace::STANDARD_DCI_P3: + return jpegr_color_gamut::JPEGR_COLORGAMUT_P3; + case ADataSpace::STANDARD_BT2020: + return jpegr_color_gamut::JPEGR_COLORGAMUT_BT2100; + default: + jclass IllegalArgumentException = env->FindClass("java/lang/IllegalArgumentException"); + env->ThrowNew(IllegalArgumentException, + "The requested color gamut is not supported by JPEG/R."); + } + + return jpegr_color_gamut::JPEGR_COLORGAMUT_UNSPECIFIED; +} + +jpegr_transfer_function P010Yuv420ToJpegREncoder::findHdrTransferFunction(JNIEnv* env, + int aDataSpace) { + switch (aDataSpace & ADataSpace::TRANSFER_MASK) { + case ADataSpace::TRANSFER_ST2084: + return jpegr_transfer_function::JPEGR_TF_PQ; + case ADataSpace::TRANSFER_HLG: + return jpegr_transfer_function::JPEGR_TF_HLG; + default: + jclass IllegalArgumentException = env->FindClass("java/lang/IllegalArgumentException"); + env->ThrowNew(IllegalArgumentException, + "The requested HDR transfer function is not supported by JPEG/R."); + } + + return jpegr_transfer_function::JPEGR_TF_UNSPECIFIED; +} + +bool P010Yuv420ToJpegREncoder::encode(JNIEnv* env, + SkWStream* stream, void* hdr, int hdrColorSpace, void* sdr, int sdrColorSpace, + int width, int height, int jpegQuality) { + // Check SDR color space. Now we only support SRGB transfer function + if ((sdrColorSpace & ADataSpace::TRANSFER_MASK) != ADataSpace::TRANSFER_SRGB) { + jclass IllegalArgumentException = env->FindClass("java/lang/IllegalArgumentException"); + env->ThrowNew(IllegalArgumentException, + "The requested SDR color space is not supported. Transfer function must be SRGB"); + return false; + } + + jpegr_color_gamut hdrColorGamut = findColorGamut(env, hdrColorSpace); + jpegr_color_gamut sdrColorGamut = findColorGamut(env, sdrColorSpace); + jpegr_transfer_function hdrTransferFunction = findHdrTransferFunction(env, hdrColorSpace); + + if (hdrColorGamut == jpegr_color_gamut::JPEGR_COLORGAMUT_UNSPECIFIED + || sdrColorGamut == jpegr_color_gamut::JPEGR_COLORGAMUT_UNSPECIFIED + || hdrTransferFunction == jpegr_transfer_function::JPEGR_TF_UNSPECIFIED) { + return false; + } + + JpegR jpegREncoder; + + jpegr_uncompressed_struct p010; + p010.data = hdr; + p010.width = width; + p010.height = height; + p010.colorGamut = hdrColorGamut; + + jpegr_uncompressed_struct yuv420; + yuv420.data = sdr; + yuv420.width = width; + yuv420.height = height; + yuv420.colorGamut = sdrColorGamut; + + jpegr_compressed_struct jpegR; + jpegR.maxLength = width * height * sizeof(uint8_t); + + std::unique_ptr<uint8_t[]> jpegr_data = std::make_unique<uint8_t[]>(jpegR.maxLength); + jpegR.data = jpegr_data.get(); + + if (int success = jpegREncoder.encodeJPEGR(&p010, &yuv420, + hdrTransferFunction, + &jpegR, jpegQuality, nullptr); success != android::OK) { + ALOGW("Encode JPEG/R failed, error code: %d.", success); + return false; + } + + if (!stream->write(jpegR.data, jpegR.length)) { + ALOGW("Writing JPEG/R to stream failed."); + return false; + } + + return true; +} + +/////////////////////////////////////////////////////////////////////////////// + static jboolean YuvImage_compressToJpeg(JNIEnv* env, jobject, jbyteArray inYuv, jint format, jint width, jint height, jintArray offsets, jintArray strides, jint jpegQuality, jobject jstream, @@ -254,11 +414,34 @@ static jboolean YuvImage_compressToJpeg(JNIEnv* env, jobject, jbyteArray inYuv, delete strm; return result; } + +static jboolean YuvImage_compressToJpegR(JNIEnv* env, jobject, jbyteArray inHdr, + jint hdrColorSpace, jbyteArray inSdr, jint sdrColorSpace, + jint width, jint height, jint quality, jobject jstream, + jbyteArray jstorage) { + jbyte* hdr = env->GetByteArrayElements(inHdr, NULL); + jbyte* sdr = env->GetByteArrayElements(inSdr, NULL); + SkWStream* strm = CreateJavaOutputStreamAdaptor(env, jstream, jstorage); + P010Yuv420ToJpegREncoder encoder; + + jboolean result = JNI_FALSE; + if (encoder.encode(env, strm, hdr, hdrColorSpace, sdr, sdrColorSpace, + width, height, quality)) { + result = JNI_TRUE; + } + + env->ReleaseByteArrayElements(inHdr, hdr, 0); + env->ReleaseByteArrayElements(inSdr, sdr, 0); + delete strm; + return result; +} /////////////////////////////////////////////////////////////////////////////// static const JNINativeMethod gYuvImageMethods[] = { { "nativeCompressToJpeg", "([BIII[I[IILjava/io/OutputStream;[B)Z", - (void*)YuvImage_compressToJpeg } + (void*)YuvImage_compressToJpeg }, + { "nativeCompressToJpegR", "([BI[BIIIILjava/io/OutputStream;[B)Z", + (void*)YuvImage_compressToJpegR } }; int register_android_graphics_YuvImage(JNIEnv* env) diff --git a/libs/hwui/jni/YuvToJpegEncoder.h b/libs/hwui/jni/YuvToJpegEncoder.h index 7e7b935df276..d22a26c83567 100644 --- a/libs/hwui/jni/YuvToJpegEncoder.h +++ b/libs/hwui/jni/YuvToJpegEncoder.h @@ -1,13 +1,16 @@ #ifndef _ANDROID_GRAPHICS_YUV_TO_JPEG_ENCODER_H_ #define _ANDROID_GRAPHICS_YUV_TO_JPEG_ENCODER_H_ -#include "SkTypes.h" -#include "SkStream.h" +#include <android/data_space.h> +#include <jpegrecoverymap/jpegr.h> + extern "C" { #include "jpeglib.h" #include "jerror.h" } +class SkWStream; + class YuvToJpegEncoder { public: /** Create an encoder based on the YUV format. @@ -24,7 +27,7 @@ public: * * @param stream The jpeg output stream. * @param inYuv The input yuv data. - * @param width Width of the the Yuv data in terms of pixels. + * @param width Width of the Yuv data in terms of pixels. * @param height Height of the Yuv data in terms of pixels. * @param offsets The offsets in each image plane with respect to inYuv. * @param jpegQuality Picture quality in [0, 100]. @@ -71,4 +74,46 @@ private: uint8_t* vRows, int rowIndex, int width, int height); }; +class P010Yuv420ToJpegREncoder { +public: + /** Encode YUV data to jpeg/r, which is output to a stream. + * This method will call JpegR::EncodeJPEGR() method. If encoding failed, + * Corresponding error code (defined in jpegrerrorcode.h) will be printed and this + * method will be terminated and return false. + * + * @param env JNI environment. + * @param stream The jpeg output stream. + * @param hdr The input yuv data (p010 format). + * @param hdrColorSpaceId color space id for the input hdr. + * @param sdr The input yuv data (yuv420p format). + * @param sdrColorSpaceId color space id for the input sdr. + * @param width Width of the Yuv data in terms of pixels. + * @param height Height of the Yuv data in terms of pixels. + * @param jpegQuality Picture quality in [0, 100]. + * @return true if successfully compressed the stream. + */ + bool encode(JNIEnv* env, + SkWStream* stream, void* hdr, int hdrColorSpace, void* sdr, int sdrColorSpace, + int width, int height, int jpegQuality); + + /** Map data space (defined in DataSpace.java and data_space.h) to the color gamut + * used in JPEG/R + * + * @param env JNI environment. + * @param aDataSpace data space defined in data_space.h. + * @return color gamut for JPEG/R. + */ + static android::jpegrecoverymap::jpegr_color_gamut findColorGamut(JNIEnv* env, int aDataSpace); + + /** Map data space (defined in DataSpace.java and data_space.h) to the transfer function + * used in JPEG/R + * + * @param env JNI environment. + * @param aDataSpace data space defined in data_space.h. + * @return color gamut for JPEG/R. + */ + static android::jpegrecoverymap::jpegr_transfer_function findHdrTransferFunction( + JNIEnv* env, int aDataSpace); +}; + #endif // _ANDROID_GRAPHICS_YUV_TO_JPEG_ENCODER_H_ diff --git a/libs/hwui/jni/android_graphics_Canvas.cpp b/libs/hwui/jni/android_graphics_Canvas.cpp index 0ef80ee10708..8ba750372d18 100644 --- a/libs/hwui/jni/android_graphics_Canvas.cpp +++ b/libs/hwui/jni/android_graphics_Canvas.cpp @@ -30,12 +30,24 @@ #include <nativehelper/ScopedPrimitiveArray.h> #include <nativehelper/ScopedStringChars.h> -#include "FontUtils.h" #include "Bitmap.h" +#include "FontUtils.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 "SkRRect.h" +#include "SkRect.h" +#include "SkRefCnt.h" #include "SkRegion.h" +#include "SkScalar.h" #include "SkVertices.h" -#include "SkRRect.h" namespace minikin { class MeasuredText; @@ -407,14 +419,36 @@ 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 drawMesh(JNIEnv* env, jobject, jlong canvasHandle, jlong meshHandle, jint modeHandle, + jlong paintHandle) { + const Mesh* mesh = reinterpret_cast<Mesh*>(meshHandle); + SkBlendMode blendMode = static_cast<SkBlendMode>(modeHandle); + Paint* paint = reinterpret_cast<Paint*>(paintHandle); + get_canvas(canvasHandle)->drawMesh(*mesh, SkBlender::Mode(blendMode), *paint); } static void drawNinePatch(JNIEnv* env, jobject, jlong canvasHandle, jlong bitmapHandle, @@ -687,9 +721,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 @@ -734,38 +769,38 @@ static const JNINativeMethod gMethods[] = { // If called from Canvas these are regular JNI // If called from DisplayListCanvas they are @FastNative static const JNINativeMethod gDrawMethods[] = { - {"nDrawColor","(JII)V", (void*) CanvasJNI::drawColor}, - {"nDrawColor","(JJJI)V", (void*) CanvasJNI::drawColorLong}, - {"nDrawPaint","(JJ)V", (void*) CanvasJNI::drawPaint}, - {"nDrawPoint", "(JFFJ)V", (void*) CanvasJNI::drawPoint}, - {"nDrawPoints", "(J[FIIJ)V", (void*) CanvasJNI::drawPoints}, - {"nDrawLine", "(JFFFFJ)V", (void*) CanvasJNI::drawLine}, - {"nDrawLines", "(J[FIIJ)V", (void*) CanvasJNI::drawLines}, - {"nDrawRect","(JFFFFJ)V", (void*) CanvasJNI::drawRect}, - {"nDrawRegion", "(JJJ)V", (void*) CanvasJNI::drawRegion }, - {"nDrawRoundRect","(JFFFFFFJ)V", (void*) CanvasJNI::drawRoundRect}, - {"nDrawDoubleRoundRect", "(JFFFFFFFFFFFFJ)V", (void*) CanvasJNI::drawDoubleRoundRectXY}, - {"nDrawDoubleRoundRect", "(JFFFF[FFFFF[FJ)V", (void*) CanvasJNI::drawDoubleRoundRectRadii}, - {"nDrawCircle","(JFFFJ)V", (void*) CanvasJNI::drawCircle}, - {"nDrawOval","(JFFFFJ)V", (void*) CanvasJNI::drawOval}, - {"nDrawArc","(JFFFFFFZJ)V", (void*) CanvasJNI::drawArc}, - {"nDrawPath","(JJJ)V", (void*) CanvasJNI::drawPath}, - {"nDrawVertices", "(JII[FI[FI[II[SIIJ)V", (void*)CanvasJNI::drawVertices}, - {"nDrawNinePatch", "(JJJFFFFJII)V", (void*)CanvasJNI::drawNinePatch}, - {"nDrawBitmapMatrix", "(JJJJ)V", (void*)CanvasJNI::drawBitmapMatrix}, - {"nDrawBitmapMesh", "(JJII[FI[IIJ)V", (void*)CanvasJNI::drawBitmapMesh}, - {"nDrawBitmap","(JJFFJIII)V", (void*) CanvasJNI::drawBitmap}, - {"nDrawBitmap","(JJFFFFFFFFJII)V", (void*) CanvasJNI::drawBitmapRect}, - {"nDrawBitmap", "(J[IIIFFIIZJ)V", (void*)CanvasJNI::drawBitmapArray}, - {"nDrawGlyphs", "(J[I[FIIIJJ)V", (void*)CanvasJNI::drawGlyphs}, - {"nDrawText","(J[CIIFFIJ)V", (void*) CanvasJNI::drawTextChars}, - {"nDrawText","(JLjava/lang/String;IIFFIJ)V", (void*) CanvasJNI::drawTextString}, - {"nDrawTextRun","(J[CIIIIFFZJJ)V", (void*) CanvasJNI::drawTextRunChars}, - {"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} -}; + {"nDrawColor", "(JII)V", (void*)CanvasJNI::drawColor}, + {"nDrawColor", "(JJJI)V", (void*)CanvasJNI::drawColorLong}, + {"nDrawPaint", "(JJ)V", (void*)CanvasJNI::drawPaint}, + {"nDrawPoint", "(JFFJ)V", (void*)CanvasJNI::drawPoint}, + {"nDrawPoints", "(J[FIIJ)V", (void*)CanvasJNI::drawPoints}, + {"nDrawLine", "(JFFFFJ)V", (void*)CanvasJNI::drawLine}, + {"nDrawLines", "(J[FIIJ)V", (void*)CanvasJNI::drawLines}, + {"nDrawRect", "(JFFFFJ)V", (void*)CanvasJNI::drawRect}, + {"nDrawRegion", "(JJJ)V", (void*)CanvasJNI::drawRegion}, + {"nDrawRoundRect", "(JFFFFFFJ)V", (void*)CanvasJNI::drawRoundRect}, + {"nDrawDoubleRoundRect", "(JFFFFFFFFFFFFJ)V", (void*)CanvasJNI::drawDoubleRoundRectXY}, + {"nDrawDoubleRoundRect", "(JFFFF[FFFFF[FJ)V", (void*)CanvasJNI::drawDoubleRoundRectRadii}, + {"nDrawCircle", "(JFFFJ)V", (void*)CanvasJNI::drawCircle}, + {"nDrawOval", "(JFFFFJ)V", (void*)CanvasJNI::drawOval}, + {"nDrawArc", "(JFFFFFFZJ)V", (void*)CanvasJNI::drawArc}, + {"nDrawPath", "(JJJ)V", (void*)CanvasJNI::drawPath}, + {"nDrawVertices", "(JII[FI[FI[II[SIIJ)V", (void*)CanvasJNI::drawVertices}, + {"nDrawMesh", "(JJIJ)V", (void*)CanvasJNI::drawMesh}, + {"nDrawNinePatch", "(JJJFFFFJII)V", (void*)CanvasJNI::drawNinePatch}, + {"nDrawBitmapMatrix", "(JJJJ)V", (void*)CanvasJNI::drawBitmapMatrix}, + {"nDrawBitmapMesh", "(JJII[FI[IIJ)V", (void*)CanvasJNI::drawBitmapMesh}, + {"nDrawBitmap", "(JJFFJIII)V", (void*)CanvasJNI::drawBitmap}, + {"nDrawBitmap", "(JJFFFFFFFFJII)V", (void*)CanvasJNI::drawBitmapRect}, + {"nDrawBitmap", "(J[IIIFFIIZJ)V", (void*)CanvasJNI::drawBitmapArray}, + {"nDrawGlyphs", "(J[I[FIIIJJ)V", (void*)CanvasJNI::drawGlyphs}, + {"nDrawText", "(J[CIIFFIJ)V", (void*)CanvasJNI::drawTextChars}, + {"nDrawText", "(JLjava/lang/String;IIFFIJ)V", (void*)CanvasJNI::drawTextString}, + {"nDrawTextRun", "(J[CIIIIFFZJJ)V", (void*)CanvasJNI::drawTextRunChars}, + {"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", "(JFFFFFFF)V", (void*)CanvasJNI::punchHole}}; int register_android_graphics_Canvas(JNIEnv* env) { int ret = 0; diff --git a/libs/hwui/jni/android_graphics_ColorSpace.cpp b/libs/hwui/jni/android_graphics_ColorSpace.cpp index 232fd71a12b4..63d3f83febd6 100644 --- a/libs/hwui/jni/android_graphics_ColorSpace.cpp +++ b/libs/hwui/jni/android_graphics_ColorSpace.cpp @@ -18,7 +18,6 @@ #include "SkColor.h" #include "SkColorSpace.h" -#include "SkHalf.h" using namespace android; @@ -40,15 +39,58 @@ static skcms_Matrix3x3 getNativeXYZMatrix(JNIEnv* env, jfloatArray xyzD50) { /////////////////////////////////////////////////////////////////////////////// +#if defined(__ANDROID__) // __fp16 is not defined on non-Android builds static float halfToFloat(uint16_t bits) { -#ifdef __ANDROID__ // __fp16 is not defined on non-Android builds __fp16 h; memcpy(&h, &bits, 2); return (float)h; +} #else - return SkHalfToFloat(bits); -#endif +// This is Skia's implementation of SkHalfToFloat, which is +// based on Fabien Giesen's half_to_float_fast2() +// see https://fgiesen.wordpress.com/2012/03/28/half-to-float-done-quic/ +static uint16_t halfMantissa(uint16_t h) { + return h & 0x03ff; +} + +static uint16_t halfExponent(uint16_t h) { + return (h >> 10) & 0x001f; +} + +static uint16_t halfSign(uint16_t h) { + return h >> 15; +} + +union FloatUIntUnion { + uint32_t mUInt; // this must come first for the initializations below to work + float mFloat; +}; + +static float halfToFloat(uint16_t bits) { + static const FloatUIntUnion magic = { 126 << 23 }; + FloatUIntUnion o; + + if (halfExponent(bits) == 0) { + // Zero / Denormal + o.mUInt = magic.mUInt + halfMantissa(bits); + o.mFloat -= magic.mFloat; + } else { + // Set mantissa + o.mUInt = halfMantissa(bits) << 13; + // Set exponent + if (halfExponent(bits) == 0x1f) { + // Inf/NaN + o.mUInt |= (255 << 23); + } else { + o.mUInt |= ((127 - 15 + halfExponent(bits)) << 23); + } + } + + // Set sign + o.mUInt |= (halfSign(bits) << 31); + return o.mFloat; } +#endif // defined(__ANDROID__) SkColor4f GraphicsJNI::convertColorLong(jlong color) { if ((color & 0x3f) == 0) { diff --git a/libs/hwui/jni/android_graphics_HardwareBufferRenderer.cpp b/libs/hwui/jni/android_graphics_HardwareBufferRenderer.cpp new file mode 100644 index 000000000000..706f18c3be80 --- /dev/null +++ b/libs/hwui/jni/android_graphics_HardwareBufferRenderer.cpp @@ -0,0 +1,176 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#undef LOG_TAG +#define LOG_TAG "HardwareBufferRenderer" +#define ATRACE_TAG ATRACE_TAG_VIEW + +#include <GraphicsJNI.h> +#include <RootRenderNode.h> +#include <TreeInfo.h> +#include <android-base/unique_fd.h> +#include <android/native_window.h> +#include <nativehelper/JNIPlatformHelp.h> +#include <renderthread/CanvasContext.h> +#include <renderthread/RenderProxy.h> +#include <renderthread/RenderThread.h> + +#include "HardwareBufferHelpers.h" +#include "JvmErrorReporter.h" + +namespace android { + +using namespace android::uirenderer; +using namespace android::uirenderer::renderthread; + +struct { + jclass clazz; + jmethodID invokeRenderCallback; +} gHardwareBufferRendererClassInfo; + +static RenderCallback createRenderCallback(JNIEnv* env, jobject releaseCallback) { + if (releaseCallback == nullptr) return nullptr; + + JavaVM* vm = nullptr; + LOG_ALWAYS_FATAL_IF(env->GetJavaVM(&vm) != JNI_OK, "Unable to get Java VM"); + auto globalCallbackRef = + std::make_shared<JGlobalRefHolder>(vm, env->NewGlobalRef(releaseCallback)); + return [globalCallbackRef](android::base::unique_fd&& fd, int status) { + globalCallbackRef->env()->CallStaticVoidMethod( + gHardwareBufferRendererClassInfo.clazz, + gHardwareBufferRendererClassInfo.invokeRenderCallback, globalCallbackRef->object(), + reinterpret_cast<jint>(fd.release()), reinterpret_cast<jint>(status)); + }; +} + +static long android_graphics_HardwareBufferRenderer_createRootNode(JNIEnv* env, jobject) { + auto* node = new RootRenderNode(std::make_unique<JvmErrorReporter>(env)); + node->incStrong(nullptr); + node->setName("RootRenderNode"); + return reinterpret_cast<jlong>(node); +} + +static void android_graphics_hardwareBufferRenderer_destroyRootNode(JNIEnv*, jobject, + jlong renderNodePtr) { + auto* node = reinterpret_cast<RootRenderNode*>(renderNodePtr); + node->destroy(); +} + +static long android_graphics_HardwareBufferRenderer_create(JNIEnv* env, jobject, jobject buffer, + jlong renderNodePtr) { + auto* hardwareBuffer = HardwareBufferHelpers::AHardwareBuffer_fromHardwareBuffer(env, buffer); + auto* rootRenderNode = reinterpret_cast<RootRenderNode*>(renderNodePtr); + ContextFactoryImpl factory(rootRenderNode); + auto* proxy = new RenderProxy(false, rootRenderNode, &factory); + proxy->setHardwareBuffer(hardwareBuffer); + return (jlong)proxy; +} + +static void HardwareBufferRenderer_destroy(jlong renderProxy) { + auto* proxy = reinterpret_cast<RenderProxy*>(renderProxy); + delete proxy; +} + +static SkMatrix createMatrixFromBufferTransform(SkScalar width, SkScalar height, int transform) { + switch (transform) { + case ANATIVEWINDOW_TRANSFORM_ROTATE_90: + return SkMatrix::MakeAll(0, -1, height, 1, 0, 0, 0, 0, 1); + case ANATIVEWINDOW_TRANSFORM_ROTATE_180: + return SkMatrix::MakeAll(-1, 0, width, 0, -1, height, 0, 0, 1); + case ANATIVEWINDOW_TRANSFORM_ROTATE_270: + return SkMatrix::MakeAll(0, 1, 0, -1, 0, width, 0, 0, 1); + default: + ALOGE("Invalid transform provided. Transform should be validated from" + "the java side. Leveraging identity transform as a fallback"); + [[fallthrough]]; + case ANATIVEWINDOW_TRANSFORM_IDENTITY: + return SkMatrix::I(); + } +} + +static int android_graphics_HardwareBufferRenderer_render(JNIEnv* env, jobject, jlong renderProxy, + jint transform, jint width, jint height, + jlong colorspacePtr, jobject consumer) { + auto* proxy = reinterpret_cast<RenderProxy*>(renderProxy); + auto skWidth = static_cast<SkScalar>(width); + auto skHeight = static_cast<SkScalar>(height); + auto matrix = createMatrixFromBufferTransform(skWidth, skHeight, transform); + auto colorSpace = GraphicsJNI::getNativeColorSpace(colorspacePtr); + proxy->setHardwareBufferRenderParams(HardwareBufferRenderParams( + width, height, matrix, colorSpace, createRenderCallback(env, consumer))); + nsecs_t vsync = systemTime(SYSTEM_TIME_MONOTONIC); + UiFrameInfoBuilder(proxy->frameInfo()) + .setVsync(vsync, vsync, UiFrameInfoBuilder::INVALID_VSYNC_ID, + UiFrameInfoBuilder::UNKNOWN_DEADLINE, + UiFrameInfoBuilder::UNKNOWN_FRAME_INTERVAL) + .addFlag(FrameInfoFlags::SurfaceCanvas); + return proxy->syncAndDrawFrame(); +} + +static void android_graphics_HardwareBufferRenderer_setLightGeometry(JNIEnv*, jobject, + jlong renderProxyPtr, + jfloat lightX, jfloat lightY, + jfloat lightZ, + jfloat lightRadius) { + auto* proxy = reinterpret_cast<RenderProxy*>(renderProxyPtr); + proxy->setLightGeometry((Vector3){lightX, lightY, lightZ}, lightRadius); +} + +static void android_graphics_HardwareBufferRenderer_setLightAlpha(JNIEnv* env, jobject, + jlong renderProxyPtr, + jfloat ambientShadowAlpha, + jfloat spotShadowAlpha) { + auto* proxy = reinterpret_cast<RenderProxy*>(renderProxyPtr); + proxy->setLightAlpha((uint8_t)(255 * ambientShadowAlpha), (uint8_t)(255 * spotShadowAlpha)); +} + +static jlong android_graphics_HardwareBufferRenderer_getFinalizer() { + return static_cast<jlong>(reinterpret_cast<uintptr_t>(&HardwareBufferRenderer_destroy)); +} + +// ---------------------------------------------------------------------------- +// JNI Glue +// ---------------------------------------------------------------------------- + +const char* const kClassPathName = "android/graphics/HardwareBufferRenderer"; + +static const JNINativeMethod gMethods[] = { + {"nCreateHardwareBufferRenderer", "(Landroid/hardware/HardwareBuffer;J)J", + (void*)android_graphics_HardwareBufferRenderer_create}, + {"nRender", "(JIIIJLjava/util/function/Consumer;)I", + (void*)android_graphics_HardwareBufferRenderer_render}, + {"nCreateRootRenderNode", "()J", + (void*)android_graphics_HardwareBufferRenderer_createRootNode}, + {"nSetLightGeometry", "(JFFFF)V", + (void*)android_graphics_HardwareBufferRenderer_setLightGeometry}, + {"nSetLightAlpha", "(JFF)V", (void*)android_graphics_HardwareBufferRenderer_setLightAlpha}, + {"nGetFinalizer", "()J", (void*)android_graphics_HardwareBufferRenderer_getFinalizer}, + {"nDestroyRootRenderNode", "(J)V", + (void*)android_graphics_hardwareBufferRenderer_destroyRootNode}}; + +int register_android_graphics_HardwareBufferRenderer(JNIEnv* env) { + jclass hardwareBufferRendererClazz = + FindClassOrDie(env, "android/graphics/HardwareBufferRenderer"); + gHardwareBufferRendererClassInfo.clazz = + reinterpret_cast<jclass>(env->NewGlobalRef(hardwareBufferRendererClazz)); + gHardwareBufferRendererClassInfo.invokeRenderCallback = + GetStaticMethodIDOrDie(env, hardwareBufferRendererClazz, "invokeRenderCallback", + "(Ljava/util/function/Consumer;II)V"); + HardwareBufferHelpers::init(); + return RegisterMethodsOrDie(env, kClassPathName, gMethods, NELEM(gMethods)); +} + +} // namespace android
\ No newline at end of file diff --git a/libs/hwui/jni/android_graphics_HardwareRenderer.cpp b/libs/hwui/jni/android_graphics_HardwareRenderer.cpp index c48448dffdd2..6a7411f5d859 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> @@ -48,6 +56,7 @@ #include <atomic> #include <vector> +#include "JvmErrorReporter.h" #include "android_graphics_HardwareRendererObserver.h" namespace android { @@ -80,6 +89,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) { @@ -91,20 +105,6 @@ static JNIEnv* getenv(JavaVM* vm) { typedef ANativeWindow* (*ANW_fromSurface)(JNIEnv* env, jobject surface); ANW_fromSurface fromSurface; -class JvmErrorReporter : public ErrorHandler { -public: - JvmErrorReporter(JNIEnv* env) { - env->GetJavaVM(&mVm); - } - - virtual void onError(const std::string& message) override { - JNIEnv* env = getenv(mVm); - jniThrowException(env, "java/lang/IllegalStateException", message.c_str()); - } -private: - JavaVM* mVm; -}; - class FrameCommitWrapper : public LightRefBase<FrameCommitWrapper> { public: explicit FrameCommitWrapper(JNIEnv* env, jobject jobject) { @@ -242,10 +242,16 @@ static void android_view_ThreadedRenderer_setOpaque(JNIEnv* env, jobject clazz, proxy->setOpaque(opaque); } -static void android_view_ThreadedRenderer_setColorMode(JNIEnv* env, jobject clazz, - jlong proxyPtr, jint colorMode) { +static jfloat android_view_ThreadedRenderer_setColorMode(JNIEnv* env, jobject clazz, jlong proxyPtr, + jint colorMode) { + RenderProxy* proxy = reinterpret_cast<RenderProxy*>(proxyPtr); + return proxy->setColorMode(static_cast<ColorMode>(colorMode)); +} + +static void android_view_ThreadedRenderer_setTargetSdrHdrRatio(JNIEnv* env, jobject clazz, + jlong proxyPtr, jfloat ratio) { RenderProxy* proxy = reinterpret_cast<RenderProxy*>(proxyPtr); - proxy->setColorMode(static_cast<ColorMode>(colorMode)); + return proxy->setRenderSdrHdrRatio(ratio); } static void android_view_ThreadedRenderer_setSdrWhitePoint(JNIEnv* env, jobject clazz, @@ -258,6 +264,16 @@ static void android_view_ThreadedRenderer_setIsHighEndGfx(JNIEnv* env, jobject c Properties::setIsHighEndGfx(jIsHighEndGfx); } +static void android_view_ThreadedRenderer_setIsLowRam(JNIEnv* env, jobject clazz, + jboolean isLowRam) { + Properties::setIsLowRam(isLowRam); +} + +static void android_view_ThreadedRenderer_setIsSystemOrPersistent(JNIEnv* env, jobject clazz, + jboolean isSystemOrPersistent) { + Properties::setIsSystemOrPersistent(isSystemOrPersistent); +} + static int android_view_ThreadedRenderer_syncAndDrawFrame(JNIEnv* env, jobject clazz, jlong proxyPtr, jlongArray frameInfo, jint frameInfoSize) { @@ -420,26 +436,6 @@ static void android_view_ThreadedRenderer_forceDrawNextFrame(JNIEnv* env, jobjec proxy->forceDrawNextFrame(); } -class JGlobalRefHolder { -public: - JGlobalRefHolder(JavaVM* vm, jobject object) : mVm(vm), mObject(object) {} - - virtual ~JGlobalRefHolder() { - getenv(mVm)->DeleteGlobalRef(mObject); - mObject = nullptr; - } - - jobject object() { return mObject; } - JavaVM* vm() { return mVm; } - -private: - JGlobalRefHolder(const JGlobalRefHolder&) = delete; - void operator=(const JGlobalRefHolder&) = delete; - - JavaVM* mVm; - jobject mObject; -}; - using TextureMap = std::unordered_map<uint32_t, sk_sp<SkImage>>; struct PictureCaptureState { @@ -451,7 +447,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) @@ -555,10 +551,9 @@ static void android_view_ThreadedRenderer_setPictureCapturedCallbackJNI(JNIEnv* auto pictureState = std::make_shared<PictureCaptureState>(); proxy->setPictureCapturedCallback([globalCallbackRef, pictureState](sk_sp<SkPicture>&& picture) { - JNIEnv* env = getenv(globalCallbackRef->vm()); Picture* wrapper = new PictureWrapper{std::move(picture), pictureState}; - env->CallStaticVoidMethod(gHardwareRenderer.clazz, - gHardwareRenderer.invokePictureCapturedCallback, + globalCallbackRef->env()->CallStaticVoidMethod( + gHardwareRenderer.clazz, gHardwareRenderer.invokePictureCapturedCallback, static_cast<jlong>(reinterpret_cast<intptr_t>(wrapper)), globalCallbackRef->object()); }); @@ -575,16 +570,14 @@ static void android_view_ThreadedRenderer_setASurfaceTransactionCallback( LOG_ALWAYS_FATAL_IF(env->GetJavaVM(&vm) != JNI_OK, "Unable to get Java VM"); auto globalCallbackRef = std::make_shared<JGlobalRefHolder>( vm, env->NewGlobalRef(aSurfaceTransactionCallback)); - proxy->setASurfaceTransactionCallback( - [globalCallbackRef](int64_t transObj, int64_t scObj, int64_t frameNr) -> bool { - JNIEnv* env = getenv(globalCallbackRef->vm()); - jboolean ret = env->CallBooleanMethod( - globalCallbackRef->object(), - gASurfaceTransactionCallback.onMergeTransaction, - static_cast<jlong>(transObj), static_cast<jlong>(scObj), - static_cast<jlong>(frameNr)); - return ret; - }); + proxy->setASurfaceTransactionCallback([globalCallbackRef](int64_t transObj, int64_t scObj, + int64_t frameNr) -> bool { + jboolean ret = globalCallbackRef->env()->CallBooleanMethod( + globalCallbackRef->object(), gASurfaceTransactionCallback.onMergeTransaction, + static_cast<jlong>(transObj), static_cast<jlong>(scObj), + static_cast<jlong>(frameNr)); + return ret; + }); } } @@ -599,9 +592,8 @@ static void android_view_ThreadedRenderer_setPrepareSurfaceControlForWebviewCall auto globalCallbackRef = std::make_shared<JGlobalRefHolder>(vm, env->NewGlobalRef(callback)); proxy->setPrepareSurfaceControlForWebviewCallback([globalCallbackRef]() { - JNIEnv* env = getenv(globalCallbackRef->vm()); - env->CallVoidMethod(globalCallbackRef->object(), - gPrepareSurfaceControlForWebviewCallback.prepare); + globalCallbackRef->env()->CallVoidMethod( + globalCallbackRef->object(), gPrepareSurfaceControlForWebviewCallback.prepare); }); } } @@ -618,7 +610,7 @@ static void android_view_ThreadedRenderer_setFrameCallback(JNIEnv* env, env->NewGlobalRef(frameCallback)); proxy->setFrameCallback([globalCallbackRef](int32_t syncResult, int64_t frameNr) -> std::function<void(bool)> { - JNIEnv* env = getenv(globalCallbackRef->vm()); + JNIEnv* env = globalCallbackRef->env(); ScopedLocalRef<jobject> frameCommitCallback( env, env->CallObjectMethod( globalCallbackRef->object(), gFrameDrawingCallback.onFrameDraw, @@ -657,22 +649,45 @@ static void android_view_ThreadedRenderer_setFrameCompleteCallback(JNIEnv* env, auto globalCallbackRef = std::make_shared<JGlobalRefHolder>(vm, env->NewGlobalRef(callback)); proxy->setFrameCompleteCallback([globalCallbackRef]() { - JNIEnv* env = getenv(globalCallbackRef->vm()); - env->CallVoidMethod(globalCallbackRef->object(), - gFrameCompleteCallback.onFrameComplete); + globalCallbackRef->env()->CallVoidMethod(globalCallbackRef->object(), + gFrameCompleteCallback.onFrameComplete); }); } } -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 { + jlong bitmapPtr = mRefHolder.env()->CallLongMethod( + mRefHolder.object(), gCopyRequest.getDestinationBitmap, srcWidth, srcHeight); + SkBitmap bitmap; + bitmap::toBitmap(bitmapPtr).getSkBitmap(&bitmap); + return bitmap; + } + + virtual void onCopyFinished(CopyResult result) override { + mRefHolder.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 { @@ -811,6 +826,16 @@ static void android_view_ThreadedRenderer_setRtAnimationsEnabled(JNIEnv* env, jo RenderProxy::setRtAnimationsEnabled(enabled); } +static void android_view_ThreadedRenderer_notifyCallbackPending(JNIEnv*, jclass, jlong proxyPtr) { + RenderProxy* proxy = reinterpret_cast<RenderProxy*>(proxyPtr); + proxy->notifyCallbackPending(); +} + +static void android_view_ThreadedRenderer_notifyExpensiveFrame(JNIEnv*, jclass, jlong proxyPtr) { + RenderProxy* proxy = reinterpret_cast<RenderProxy*>(proxyPtr); + proxy->notifyExpensiveFrame(); +} + // Plumbs the display density down to DeviceInfo. static void android_view_ThreadedRenderer_setDisplayDensityDpi(JNIEnv*, jclass, jint densityDpi) { // Convert from dpi to density-independent pixels. @@ -818,17 +843,18 @@ static void android_view_ThreadedRenderer_setDisplayDensityDpi(JNIEnv*, jclass, DeviceInfo::setDensity(density); } -static void android_view_ThreadedRenderer_initDisplayInfo(JNIEnv*, jclass, jint physicalWidth, - jint physicalHeight, jfloat refreshRate, - jint wideColorDataspace, - jlong appVsyncOffsetNanos, - jlong presentationDeadlineNanos) { +static void android_view_ThreadedRenderer_initDisplayInfo( + JNIEnv* env, jclass, jint physicalWidth, jint physicalHeight, jfloat refreshRate, + jint wideColorDataspace, jlong appVsyncOffsetNanos, jlong presentationDeadlineNanos, + jboolean supportFp16ForHdr, jboolean supportMixedColorSpaces) { DeviceInfo::setWidth(physicalWidth); DeviceInfo::setHeight(physicalHeight); DeviceInfo::setRefreshRate(refreshRate); DeviceInfo::setWideColorDataspace(static_cast<ADataSpace>(wideColorDataspace)); DeviceInfo::setAppVsyncOffsetNanos(appVsyncOffsetNanos); DeviceInfo::setPresentationDeadlineNanos(presentationDeadlineNanos); + DeviceInfo::setSupportFp16ForHdr(supportFp16ForHdr); + DeviceInfo::setSupportMixedColorSpaces(supportMixedColorSpaces); } static void android_view_ThreadedRenderer_setDrawingEnabled(JNIEnv*, jclass, jboolean enabled) { @@ -907,9 +933,14 @@ static const JNINativeMethod gMethods[] = { {"nSetLightAlpha", "(JFF)V", (void*)android_view_ThreadedRenderer_setLightAlpha}, {"nSetLightGeometry", "(JFFFF)V", (void*)android_view_ThreadedRenderer_setLightGeometry}, {"nSetOpaque", "(JZ)V", (void*)android_view_ThreadedRenderer_setOpaque}, - {"nSetColorMode", "(JI)V", (void*)android_view_ThreadedRenderer_setColorMode}, + {"nSetColorMode", "(JI)F", (void*)android_view_ThreadedRenderer_setColorMode}, + {"nSetTargetSdrHdrRatio", "(JF)V", + (void*)android_view_ThreadedRenderer_setTargetSdrHdrRatio}, {"nSetSdrWhitePoint", "(JF)V", (void*)android_view_ThreadedRenderer_setSdrWhitePoint}, {"nSetIsHighEndGfx", "(Z)V", (void*)android_view_ThreadedRenderer_setIsHighEndGfx}, + {"nSetIsLowRam", "(Z)V", (void*)android_view_ThreadedRenderer_setIsLowRam}, + {"nSetIsSystemOrPersistent", "(Z)V", + (void*)android_view_ThreadedRenderer_setIsSystemOrPersistent}, {"nSyncAndDrawFrame", "(J[JI)I", (void*)android_view_ThreadedRenderer_syncAndDrawFrame}, {"nDestroy", "(JJ)V", (void*)android_view_ThreadedRenderer_destroy}, {"nRegisterAnimatingRenderNode", "(JJ)V", @@ -961,7 +992,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}, @@ -974,7 +1006,7 @@ static const JNINativeMethod gMethods[] = { {"nSetForceDark", "(JZ)V", (void*)android_view_ThreadedRenderer_setForceDark}, {"nSetDisplayDensityDpi", "(I)V", (void*)android_view_ThreadedRenderer_setDisplayDensityDpi}, - {"nInitDisplayInfo", "(IIFIJJ)V", (void*)android_view_ThreadedRenderer_initDisplayInfo}, + {"nInitDisplayInfo", "(IIFIJJZZ)V", (void*)android_view_ThreadedRenderer_initDisplayInfo}, {"preload", "()V", (void*)android_view_ThreadedRenderer_preload}, {"isWebViewOverlaysEnabled", "()Z", (void*)android_view_ThreadedRenderer_isWebViewOverlaysEnabled}, @@ -982,6 +1014,10 @@ static const JNINativeMethod gMethods[] = { {"nIsDrawingEnabled", "()Z", (void*)android_view_ThreadedRenderer_isDrawingEnabled}, {"nSetRtAnimationsEnabled", "(Z)V", (void*)android_view_ThreadedRenderer_setRtAnimationsEnabled}, + {"nNotifyCallbackPending", "(J)V", + (void*)android_view_ThreadedRenderer_notifyCallbackPending}, + {"nNotifyExpensiveFrame", "(J)V", + (void*)android_view_ThreadedRenderer_notifyExpensiveFrame}, }; static JavaVM* mJvm = nullptr; @@ -1034,6 +1070,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/android_graphics_Matrix.cpp b/libs/hwui/jni/android_graphics_Matrix.cpp index cf6702e45fff..ca667b0d09bc 100644 --- a/libs/hwui/jni/android_graphics_Matrix.cpp +++ b/libs/hwui/jni/android_graphics_Matrix.cpp @@ -23,8 +23,6 @@ namespace android { static_assert(sizeof(SkMatrix) == 40, "Unexpected sizeof(SkMatrix), " "update size in Matrix.java#NATIVE_ALLOCATION_SIZE and here"); -static_assert(SK_SCALAR_IS_FLOAT, "SK_SCALAR_IS_FLOAT is false, " - "only float scalar is supported"); class SkMatrixGlue { public: diff --git a/libs/hwui/jni/android_graphics_Mesh.cpp b/libs/hwui/jni/android_graphics_Mesh.cpp new file mode 100644 index 000000000000..5cb43e54e499 --- /dev/null +++ b/libs/hwui/jni/android_graphics_Mesh.cpp @@ -0,0 +1,205 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <Mesh.h> +#include <SkMesh.h> +#include <jni.h> + +#include <utility> + +#include "BufferUtils.h" +#include "GraphicsJNI.h" +#include "graphics_jni_helpers.h" + +#define gIndexByteSize 2 + +namespace android { + +static jlong make(JNIEnv* env, jobject, jlong meshSpec, jint mode, jobject vertexBuffer, + jboolean isDirect, jint vertexCount, jint vertexOffset, jfloat left, jfloat top, + jfloat right, jfloat bottom) { + auto skMeshSpec = sk_ref_sp(reinterpret_cast<SkMeshSpecification*>(meshSpec)); + size_t bufferSize = vertexCount * skMeshSpec->stride(); + auto buffer = copyJavaNioBufferToVector(env, vertexBuffer, bufferSize, isDirect); + if (env->ExceptionCheck()) { + return 0; + } + auto skRect = SkRect::MakeLTRB(left, top, right, bottom); + auto meshPtr = new Mesh(skMeshSpec, mode, std::move(buffer), vertexCount, vertexOffset, + std::make_unique<MeshUniformBuilder>(skMeshSpec), skRect); + auto [valid, msg] = meshPtr->validate(); + if (!valid) { + jniThrowExceptionFmt(env, "java/lang/IllegalArgumentException", msg.c_str()); + } + return reinterpret_cast<jlong>(meshPtr); +} + +static jlong makeIndexed(JNIEnv* env, jobject, jlong meshSpec, jint mode, jobject vertexBuffer, + jboolean isVertexDirect, jint vertexCount, jint vertexOffset, + jobject indexBuffer, jboolean isIndexDirect, jint indexCount, + jint indexOffset, jfloat left, jfloat top, jfloat right, jfloat bottom) { + auto skMeshSpec = sk_ref_sp(reinterpret_cast<SkMeshSpecification*>(meshSpec)); + auto vertexBufferSize = vertexCount * skMeshSpec->stride(); + auto indexBufferSize = indexCount * gIndexByteSize; + auto vBuf = copyJavaNioBufferToVector(env, vertexBuffer, vertexBufferSize, isVertexDirect); + if (env->ExceptionCheck()) { + return 0; + } + auto iBuf = copyJavaNioBufferToVector(env, indexBuffer, indexBufferSize, isIndexDirect); + if (env->ExceptionCheck()) { + return 0; + } + auto skRect = SkRect::MakeLTRB(left, top, right, bottom); + auto meshPtr = new Mesh(skMeshSpec, mode, std::move(vBuf), vertexCount, vertexOffset, + std::move(iBuf), indexCount, indexOffset, + std::make_unique<MeshUniformBuilder>(skMeshSpec), skRect); + auto [valid, msg] = meshPtr->validate(); + if (!valid) { + jniThrowExceptionFmt(env, "java/lang/IllegalArgumentException", msg.c_str()); + } + + return reinterpret_cast<jlong>(meshPtr); +} + +static inline int ThrowIAEFmt(JNIEnv* env, const char* fmt, ...) { + va_list args; + va_start(args, fmt); + int ret = jniThrowExceptionFmt(env, "java/lang/IllegalArgumentException", fmt, args); + va_end(args); + return ret; +} + +static bool isIntUniformType(const SkRuntimeEffect::Uniform::Type& type) { + switch (type) { + case SkRuntimeEffect::Uniform::Type::kFloat: + case SkRuntimeEffect::Uniform::Type::kFloat2: + case SkRuntimeEffect::Uniform::Type::kFloat3: + case SkRuntimeEffect::Uniform::Type::kFloat4: + case SkRuntimeEffect::Uniform::Type::kFloat2x2: + case SkRuntimeEffect::Uniform::Type::kFloat3x3: + case SkRuntimeEffect::Uniform::Type::kFloat4x4: + return false; + case SkRuntimeEffect::Uniform::Type::kInt: + case SkRuntimeEffect::Uniform::Type::kInt2: + case SkRuntimeEffect::Uniform::Type::kInt3: + case SkRuntimeEffect::Uniform::Type::kInt4: + return true; + } +} + +static void nativeUpdateFloatUniforms(JNIEnv* env, MeshUniformBuilder* builder, + const char* uniformName, const float values[], int count, + bool isColor) { + MeshUniformBuilder::MeshUniform uniform = builder->uniform(uniformName); + if (uniform.fVar == nullptr) { + ThrowIAEFmt(env, "unable to find uniform named %s", uniformName); + } else if (isColor != ((uniform.fVar->flags & SkRuntimeEffect::Uniform::kColor_Flag) != 0)) { + if (isColor) { + jniThrowExceptionFmt( + env, "java/lang/IllegalArgumentException", + "attempting to set a color uniform using the non-color specific APIs: %s %x", + uniformName, uniform.fVar->flags); + } else { + ThrowIAEFmt(env, + "attempting to set a non-color uniform using the setColorUniform APIs: %s", + uniformName); + } + } else if (isIntUniformType(uniform.fVar->type)) { + ThrowIAEFmt(env, "attempting to set a int uniform using the setUniform APIs: %s", + uniformName); + } else if (!uniform.set<float>(values, count)) { + ThrowIAEFmt(env, "mismatch in byte size for uniform [expected: %zu actual: %zu]", + uniform.fVar->sizeInBytes(), sizeof(float) * count); + } +} + +static void updateFloatUniforms(JNIEnv* env, jobject, jlong meshWrapper, jstring uniformName, + jfloat value1, jfloat value2, jfloat value3, jfloat value4, + jint count) { + auto* wrapper = reinterpret_cast<Mesh*>(meshWrapper); + ScopedUtfChars name(env, uniformName); + const float values[4] = {value1, value2, value3, value4}; + nativeUpdateFloatUniforms(env, wrapper->uniformBuilder(), name.c_str(), values, count, false); + wrapper->markDirty(); +} + +static void updateFloatArrayUniforms(JNIEnv* env, jobject, jlong meshWrapper, jstring jUniformName, + jfloatArray jvalues, jboolean isColor) { + auto wrapper = reinterpret_cast<Mesh*>(meshWrapper); + ScopedUtfChars name(env, jUniformName); + AutoJavaFloatArray autoValues(env, jvalues, 0, kRO_JNIAccess); + nativeUpdateFloatUniforms(env, wrapper->uniformBuilder(), name.c_str(), autoValues.ptr(), + autoValues.length(), isColor); + wrapper->markDirty(); +} + +static void nativeUpdateIntUniforms(JNIEnv* env, MeshUniformBuilder* builder, + const char* uniformName, const int values[], int count) { + MeshUniformBuilder::MeshUniform uniform = builder->uniform(uniformName); + if (uniform.fVar == nullptr) { + ThrowIAEFmt(env, "unable to find uniform named %s", uniformName); + } else if (!isIntUniformType(uniform.fVar->type)) { + ThrowIAEFmt(env, "attempting to set a non-int uniform using the setIntUniform APIs: %s", + uniformName); + } else if (!uniform.set<int>(values, count)) { + ThrowIAEFmt(env, "mismatch in byte size for uniform [expected: %zu actual: %zu]", + uniform.fVar->sizeInBytes(), sizeof(float) * count); + } +} + +static void updateIntUniforms(JNIEnv* env, jobject, jlong meshWrapper, jstring uniformName, + jint value1, jint value2, jint value3, jint value4, jint count) { + auto wrapper = reinterpret_cast<Mesh*>(meshWrapper); + ScopedUtfChars name(env, uniformName); + const int values[4] = {value1, value2, value3, value4}; + nativeUpdateIntUniforms(env, wrapper->uniformBuilder(), name.c_str(), values, count); + wrapper->markDirty(); +} + +static void updateIntArrayUniforms(JNIEnv* env, jobject, jlong meshWrapper, jstring uniformName, + jintArray values) { + auto wrapper = reinterpret_cast<Mesh*>(meshWrapper); + ScopedUtfChars name(env, uniformName); + AutoJavaIntArray autoValues(env, values, 0); + nativeUpdateIntUniforms(env, wrapper->uniformBuilder(), name.c_str(), autoValues.ptr(), + autoValues.length()); + wrapper->markDirty(); +} + +static void MeshWrapper_destroy(Mesh* wrapper) { + delete wrapper; +} + +static jlong getMeshFinalizer(JNIEnv*, jobject) { + return static_cast<jlong>(reinterpret_cast<uintptr_t>(&MeshWrapper_destroy)); +} + +static const JNINativeMethod gMeshMethods[] = { + {"nativeGetFinalizer", "()J", (void*)getMeshFinalizer}, + {"nativeMake", "(JILjava/nio/Buffer;ZIIFFFF)J", (void*)make}, + {"nativeMakeIndexed", "(JILjava/nio/Buffer;ZIILjava/nio/ShortBuffer;ZIIFFFF)J", + (void*)makeIndexed}, + {"nativeUpdateUniforms", "(JLjava/lang/String;[FZ)V", (void*)updateFloatArrayUniforms}, + {"nativeUpdateUniforms", "(JLjava/lang/String;FFFFI)V", (void*)updateFloatUniforms}, + {"nativeUpdateUniforms", "(JLjava/lang/String;[I)V", (void*)updateIntArrayUniforms}, + {"nativeUpdateUniforms", "(JLjava/lang/String;IIIII)V", (void*)updateIntUniforms}}; + +int register_android_graphics_Mesh(JNIEnv* env) { + android::RegisterMethodsOrDie(env, "android/graphics/Mesh", gMeshMethods, NELEM(gMeshMethods)); + return 0; +} + +} // namespace android
\ No newline at end of file diff --git a/libs/hwui/jni/android_graphics_RenderNode.cpp b/libs/hwui/jni/android_graphics_RenderNode.cpp index db7639029187..ac1f92dee507 100644 --- a/libs/hwui/jni/android_graphics_RenderNode.cpp +++ b/libs/hwui/jni/android_graphics_RenderNode.cpp @@ -605,15 +605,25 @@ static void android_view_RenderNode_requestPositionUpdates(JNIEnv* env, jobject, } mPreviousPosition = bounds; + ATRACE_NAME("Update SurfaceView position"); + #ifdef __ANDROID__ // Layoutlib does not support CanvasContext - incStrong(0); - auto functor = std::bind( - std::mem_fn(&PositionListenerTrampoline::doUpdatePositionAsync), this, - (jlong) info.canvasContext.getFrameNumber(), - (jint) bounds.left, (jint) bounds.top, - (jint) bounds.right, (jint) bounds.bottom); - - info.canvasContext.enqueueFrameWork(std::move(functor)); + JNIEnv* env = jnienv(); + // Update the new position synchronously. We cannot defer this to + // a worker pool to process asynchronously because the UI thread + // may be unblocked by the time a worker thread can process this, + // In particular if the app removes a view from the view tree before + // this callback is dispatched, then we lose the position + // information for this frame. + jboolean keepListening = env->CallStaticBooleanMethod( + gPositionListener.clazz, gPositionListener.callPositionChanged, mListener, + static_cast<jlong>(info.canvasContext.getFrameNumber()), + static_cast<jint>(bounds.left), static_cast<jint>(bounds.top), + static_cast<jint>(bounds.right), static_cast<jint>(bounds.bottom)); + if (!keepListening) { + env->DeleteGlobalRef(mListener); + mListener = nullptr; + } #endif } @@ -628,7 +638,14 @@ static void android_view_RenderNode_requestPositionUpdates(JNIEnv* env, jobject, ATRACE_NAME("SurfaceView position lost"); JNIEnv* env = jnienv(); #ifdef __ANDROID__ // Layoutlib does not support CanvasContext - // TODO: Remember why this is synchronous and then make a comment + // Update the lost position synchronously. We cannot defer this to + // a worker pool to process asynchronously because the UI thread + // may be unblocked by the time a worker thread can process this, + // In particular if a view's rendernode is readded to the scene + // before this callback is dispatched, then we report that we lost + // position information on the wrong frame, which can be problematic + // for views like SurfaceView which rely on RenderNode callbacks + // for driving visibility. jboolean keepListening = env->CallStaticBooleanMethod( gPositionListener.clazz, gPositionListener.callPositionLost, mListener, info ? info->canvasContext.getFrameNumber() : 0); @@ -708,23 +725,6 @@ static void android_view_RenderNode_requestPositionUpdates(JNIEnv* env, jobject, } } - void doUpdatePositionAsync(jlong frameNumber, jint left, jint top, - jint right, jint bottom) { - ATRACE_NAME("Update SurfaceView position"); - - JNIEnv* env = jnienv(); - jboolean keepListening = env->CallStaticBooleanMethod( - gPositionListener.clazz, gPositionListener.callPositionChanged, mListener, - frameNumber, left, top, right, bottom); - if (!keepListening) { - env->DeleteGlobalRef(mListener); - mListener = nullptr; - } - - // We need to release ourselves here - decStrong(0); - } - JavaVM* mVm; jobject mListener; uirenderer::Rect mPreviousPosition; diff --git a/libs/hwui/jni/fonts/Font.cpp b/libs/hwui/jni/fonts/Font.cpp index 09be630dc741..1af60b2f5fae 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> @@ -105,8 +108,9 @@ static jlong Font_Builder_build(JNIEnv* env, jobject clazz, jlong builderPtr, jo std::move(data), std::string_view(fontPath.c_str(), fontPath.size()), fontPtr, fontSize, ttcIndex, builder->axes); if (minikinFont == nullptr) { - jniThrowException(env, "java/lang/IllegalArgumentException", - "Failed to create internal object. maybe invalid font data."); + jniThrowExceptionFmt(env, "java/lang/IllegalArgumentException", + "Failed to create internal object. maybe invalid font data. filePath %s", + fontPath.c_str()); return 0; } uint32_t localeListId = minikin::registerLocaleList(langTagStr.c_str()); @@ -225,7 +229,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 +271,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 +284,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 +299,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..897c4d71c0d5 100644 --- a/libs/hwui/jni/fonts/FontFamily.cpp +++ b/libs/hwui/jni/fonts/FontFamily.cpp @@ -57,7 +57,8 @@ static void FontFamily_Builder_addFont(CRITICAL_JNI_PARAMS_COMMA jlong builderPt // Regular JNI static jlong FontFamily_Builder_build(JNIEnv* env, jobject clazz, jlong builderPtr, - jstring langTags, jint variant, jboolean isCustomFallback) { + jstring langTags, jint variant, jboolean isCustomFallback, + jboolean isDefaultFallback) { std::unique_ptr<NativeFamilyBuilder> builder(toBuilder(builderPtr)); uint32_t localeId; if (langTags == nullptr) { @@ -66,9 +67,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>( + std::shared_ptr<minikin::FontFamily> family = minikin::FontFamily::create( localeId, static_cast<minikin::FamilyVariant>(variant), std::move(builder->fonts), - isCustomFallback); + isCustomFallback, isDefaultFallback); if (family->getCoverage().length() == 0) { // No coverage means minikin rejected given font for some reasons. jniThrowException(env, "java/lang/IllegalArgumentException", @@ -116,10 +117,10 @@ static jlong FontFamily_getFont(CRITICAL_JNI_PARAMS_COMMA jlong familyPtr, jint /////////////////////////////////////////////////////////////////////////////// static const JNINativeMethod gFontFamilyBuilderMethods[] = { - { "nInitBuilder", "()J", (void*) FontFamily_Builder_initBuilder }, - { "nAddFont", "(JJ)V", (void*) FontFamily_Builder_addFont }, - { "nBuild", "(JLjava/lang/String;IZ)J", (void*) FontFamily_Builder_build }, - { "nGetReleaseNativeFamily", "()J", (void*) FontFamily_Builder_GetReleaseFunc }, + {"nInitBuilder", "()J", (void*)FontFamily_Builder_initBuilder}, + {"nAddFont", "(JJ)V", (void*)FontFamily_Builder_addFont}, + {"nBuild", "(JLjava/lang/String;IZZ)J", (void*)FontFamily_Builder_build}, + {"nGetReleaseNativeFamily", "()J", (void*)FontFamily_Builder_GetReleaseFunc}, }; static const JNINativeMethod gFontFamilyMethods[] = { diff --git a/libs/hwui/jni/text/GraphemeBreak.cpp b/libs/hwui/jni/text/GraphemeBreak.cpp new file mode 100644 index 000000000000..55f03bd9f7b1 --- /dev/null +++ b/libs/hwui/jni/text/GraphemeBreak.cpp @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#undef LOG_TAG +#define LOG_TAG "GraphemeBreaker" + +#include <minikin/GraphemeBreak.h> +#include <nativehelper/ScopedPrimitiveArray.h> + +#include "GraphicsJNI.h" + +namespace android { + +static void nIsGraphemeBreak(JNIEnv* env, jclass, jfloatArray advances, jcharArray text, jint start, + jint end, jbooleanArray isGraphemeBreak) { + if (start > end || env->GetArrayLength(advances) < end || + env->GetArrayLength(isGraphemeBreak) < end - start) { + doThrowAIOOBE(env); + } + + if (start == end) { + return; + } + + ScopedFloatArrayRO advancesArray(env, advances); + ScopedCharArrayRO textArray(env, text); + ScopedBooleanArrayRW isGraphemeBreakArray(env, isGraphemeBreak); + + size_t count = end - start; + for (size_t offset = 0; offset < count; ++offset) { + bool isBreak = minikin::GraphemeBreak::isGraphemeBreak(advancesArray.get(), textArray.get(), + start, end, start + offset); + isGraphemeBreakArray[offset] = isBreak ? JNI_TRUE : JNI_FALSE; + } +} + +static const JNINativeMethod gMethods[] = { + {"nIsGraphemeBreak", + "(" + "[F" // advances + "[C" // text + "I" // start + "I" // end + "[Z" // isGraphemeBreak + ")V", + (void*)nIsGraphemeBreak}, +}; + +int register_android_graphics_text_GraphemeBreak(JNIEnv* env) { + return RegisterMethodsOrDie(env, "android/graphics/text/GraphemeBreak", gMethods, + NELEM(gMethods)); +} + +} // namespace android 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/GLFunctorDrawable.cpp b/libs/hwui/pipeline/skia/GLFunctorDrawable.cpp index dc72aead4873..a4960ea17c79 100644 --- a/libs/hwui/pipeline/skia/GLFunctorDrawable.cpp +++ b/libs/hwui/pipeline/skia/GLFunctorDrawable.cpp @@ -24,6 +24,7 @@ #include "SkClipStack.h" #include "SkRect.h" #include "SkM44.h" +#include "include/gpu/GpuTypes.h" // from Skia #include "utils/GLUtils.h" namespace android { @@ -92,7 +93,7 @@ void GLFunctorDrawable::onDraw(SkCanvas* canvas) { SkImageInfo surfaceInfo = canvas->imageInfo().makeWH(clipBounds.width(), clipBounds.height()); tmpSurface = - SkSurface::MakeRenderTarget(directContext, SkBudgeted::kYes, surfaceInfo); + SkSurface::MakeRenderTarget(directContext, skgpu::Budgeted::kYes, surfaceInfo); tmpSurface->getCanvas()->clear(SK_ColorTRANSPARENT); GrGLFramebufferInfo fboInfo; 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/LayerDrawable.cpp b/libs/hwui/pipeline/skia/LayerDrawable.cpp index 2fba13c3cfea..99f54c19d2e5 100644 --- a/libs/hwui/pipeline/skia/LayerDrawable.cpp +++ b/libs/hwui/pipeline/skia/LayerDrawable.cpp @@ -25,6 +25,7 @@ #include "SkColorFilter.h" #include "SkRuntimeEffect.h" #include "SkSurface.h" +#include "Tonemapper.h" #include "gl/GrGLTypes.h" #include "math/mat4.h" #include "system/graphics-base-v1.0.h" @@ -76,35 +77,30 @@ static bool shouldFilterRect(const SkMatrix& matrix, const SkRect& srcRect, cons isIntegerAligned(dstDevRect.y())); } -static sk_sp<SkShader> createLinearEffectShader(sk_sp<SkShader> shader, - const shaders::LinearEffect& linearEffect, - float maxDisplayLuminance, - float currentDisplayLuminanceNits, - float maxLuminance) { - auto shaderString = SkString(shaders::buildLinearEffectSkSL(linearEffect)); - auto [runtimeEffect, error] = SkRuntimeEffect::MakeForShader(std::move(shaderString)); - if (!runtimeEffect) { - LOG_ALWAYS_FATAL("LinearColorFilter construction error: %s", error.c_str()); +static void adjustCropForYUV(uint32_t format, int bufferWidth, int bufferHeight, SkRect* cropRect) { + // 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 (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; } - SkRuntimeShaderBuilder effectBuilder(std::move(runtimeEffect)); - - effectBuilder.child("child") = std::move(shader); - - const auto uniforms = shaders::buildLinearEffectUniforms( - linearEffect, mat4(), maxDisplayLuminance, currentDisplayLuminanceNits, maxLuminance); - - for (const auto& uniform : uniforms) { - effectBuilder.uniform(uniform.name.c_str()).set(uniform.value.data(), uniform.value.size()); + // Shrink the crop if it has more than 1-px and differs from the buffer size. + if (cropRect->width() > 1 && cropRect->width() < bufferWidth) { + cropRect->inset(shrinkAmount, 0); } - return effectBuilder.makeShader(); -} - -static bool isHdrDataspace(ui::Dataspace dataspace) { - const auto transfer = dataspace & HAL_DATASPACE_TRANSFER_MASK; - - return transfer == HAL_DATASPACE_TRANSFER_ST2084 || transfer == HAL_DATASPACE_TRANSFER_HLG; + if (cropRect->height() > 1 && cropRect->height() < bufferHeight) { + cropRect->inset(0, shrinkAmount); + } } // TODO: Context arg probably doesn't belong here – do debug check at callsite instead. @@ -142,6 +138,7 @@ bool LayerDrawable::DrawLayer(GrRecordingContext* context, SkRect skiaSrcRect; if (srcRect && !srcRect->isEmpty()) { skiaSrcRect = *srcRect; + adjustCropForYUV(layer->getBufferFormat(), imageWidth, imageHeight, &skiaSrcRect); } else { skiaSrcRect = SkRect::MakeIWH(imageWidth, imageHeight); } @@ -188,31 +185,10 @@ bool LayerDrawable::DrawLayer(GrRecordingContext* context, sampling = SkSamplingOptions(SkFilterMode::kLinear); } - const auto sourceDataspace = static_cast<ui::Dataspace>( - ColorSpaceToADataSpace(layerImage->colorSpace(), layerImage->colorType())); - const SkImageInfo& imageInfo = canvas->imageInfo(); - const auto destinationDataspace = static_cast<ui::Dataspace>( - ColorSpaceToADataSpace(imageInfo.colorSpace(), imageInfo.colorType())); - - if (isHdrDataspace(sourceDataspace) || isHdrDataspace(destinationDataspace)) { - const auto effect = shaders::LinearEffect{ - .inputDataspace = sourceDataspace, - .outputDataspace = destinationDataspace, - .undoPremultipliedAlpha = layerImage->alphaType() == kPremul_SkAlphaType, - .fakeInputDataspace = destinationDataspace}; - auto shader = layerImage->makeShader(sampling, - SkMatrix::RectToRect(skiaSrcRect, skiaDestRect)); - constexpr float kMaxDisplayBrightess = 1000.f; - constexpr float kCurrentDisplayBrightness = 500.f; - shader = createLinearEffectShader(std::move(shader), effect, kMaxDisplayBrightess, - kCurrentDisplayBrightness, - layer->getMaxLuminanceNits()); - paint.setShader(shader); - canvas->drawRect(skiaDestRect, paint); - } else { - canvas->drawImageRect(layerImage.get(), skiaSrcRect, skiaDestRect, sampling, &paint, - constraint); - } + tonemapPaint(layerImage->imageInfo(), canvas->imageInfo(), layer->getMaxLuminanceNits(), + paint); + canvas->drawImageRect(layerImage.get(), skiaSrcRect, skiaDestRect, sampling, &paint, + constraint); canvas->restore(); // restore the original matrix diff --git a/libs/hwui/pipeline/skia/RenderNodeDrawable.cpp b/libs/hwui/pipeline/skia/RenderNodeDrawable.cpp index 507d3dcdcde9..1a47db5c8ec2 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. @@ -224,10 +229,10 @@ void RenderNodeDrawable::drawContent(SkCanvas* canvas) const { // TODO should we let the bound of the drawable do this for us? const SkRect bounds = SkRect::MakeWH(properties.getWidth(), properties.getHeight()); bool quickRejected = properties.getClipToBounds() && canvas->quickReject(bounds); - auto clipBounds = canvas->getLocalClipBounds(); - SkIRect srcBounds = SkIRect::MakeWH(bounds.width(), bounds.height()); - SkIPoint offset = SkIPoint::Make(0.0f, 0.0f); if (!quickRejected) { + auto clipBounds = canvas->getLocalClipBounds(); + SkIRect srcBounds = SkIRect::MakeWH(bounds.width(), bounds.height()); + SkIPoint offset = SkIPoint::Make(0.0f, 0.0f); SkiaDisplayList* displayList = renderNode->getDisplayList().asSkiaDl(); const LayerProperties& layerProperties = properties.layerProperties(); // composing a hardware layer @@ -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/RenderNodeDrawable.h b/libs/hwui/pipeline/skia/RenderNodeDrawable.h index 6c390c3fce24..c7582e734009 100644 --- a/libs/hwui/pipeline/skia/RenderNodeDrawable.h +++ b/libs/hwui/pipeline/skia/RenderNodeDrawable.h @@ -18,6 +18,7 @@ #include "SkiaUtils.h" +#include <SkBlendMode.h> #include <SkCanvas.h> #include <SkDrawable.h> #include <SkMatrix.h> 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..00919dc3f22a 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> @@ -32,7 +33,8 @@ namespace skiapipeline { // Cache size limits. static const size_t maxKeySize = 1024; static const size_t maxValueSize = 2 * 1024 * 1024; -static const size_t maxTotalSize = 1024 * 1024; +static const size_t maxTotalSize = 4 * 1024 * 1024; +static_assert(maxKeySize + maxValueSize < maxTotalSize); ShaderCache::ShaderCache() { // There is an "incomplete FileBlobCache type" compilation error, if ctor is moved to header. @@ -174,14 +176,13 @@ void set(BlobCache* cache, const void* key, size_t keySize, const void* value, s void ShaderCache::saveToDiskLocked() { ATRACE_NAME("ShaderCache::saveToDiskLocked"); - if (mInitialized && mBlobCache && mSavePending) { + if (mInitialized && mBlobCache) { if (mIDHash.size()) { auto key = sIDKey; set(mBlobCache.get(), &key, sizeof(key), mIDHash.data(), mIDHash.size()); } mBlobCache->writeToFile(); } - mSavePending = false; } void ShaderCache::store(const SkData& key, const SkData& data, const SkString& /*description*/) { @@ -224,10 +225,10 @@ void ShaderCache::store(const SkData& key, const SkData& data, const SkString& / } set(bc, key.data(), keySize, value, valueSize); - if (!mSavePending && mDeferredSaveDelay > 0) { + if (!mSavePending && mDeferredSaveDelayMs > 0) { mSavePending = true; std::thread deferredSaveThread([this]() { - sleep(mDeferredSaveDelay); + usleep(mDeferredSaveDelayMs * 1000); // milliseconds to microseconds std::lock_guard<std::mutex> lock(mMutex); // Store file on disk if there a new shader or Vulkan pipeline cache size changed. if (mCacheDirty || mNewPipelineCacheSize != mOldPipelineCacheSize) { @@ -236,6 +237,7 @@ void ShaderCache::store(const SkData& key, const SkData& data, const SkString& / mTryToStorePipelineCache = false; mCacheDirty = false; } + mSavePending = false; }); deferredSaveThread.detach(); } diff --git a/libs/hwui/pipeline/skia/ShaderCache.h b/libs/hwui/pipeline/skia/ShaderCache.h index 3e0fd5164011..f5506d60f811 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 @@ -153,7 +156,8 @@ private: * pending. Each time a key/value pair is inserted into the cache via * load, a deferred save is initiated if one is not already pending. * This will wait some amount of time and then trigger a save of the cache - * contents to disk. + * contents to disk, unless mDeferredSaveDelayMs is 0 in which case saving + * is disabled. */ bool mSavePending = false; @@ -163,9 +167,11 @@ private: size_t mObservedBlobValueSize = 20 * 1024; /** - * The time in seconds to wait before saving newly inserted cache entries. + * The time in milliseconds to wait before saving newly inserted cache entries. + * + * WARNING: setting this to 0 will disable writing the cache to disk. */ - unsigned int mDeferredSaveDelay = 4; + unsigned int mDeferredSaveDelayMs = 4 * 1000; /** * "mMutex" is the mutex used to prevent concurrent access to the member diff --git a/libs/hwui/pipeline/skia/SkiaDisplayList.cpp b/libs/hwui/pipeline/skia/SkiaDisplayList.cpp index fcfc4f82abed..af2d3b34bac7 100644 --- a/libs/hwui/pipeline/skia/SkiaDisplayList.cpp +++ b/libs/hwui/pipeline/skia/SkiaDisplayList.cpp @@ -23,6 +23,7 @@ #else #include "DamageAccumulator.h" #endif +#include "TreeInfo.h" #include "VectorDrawable.h" #ifdef __ANDROID__ #include "renderthread/CanvasContext.h" @@ -102,6 +103,12 @@ bool SkiaDisplayList::prepareListAndChildren( info.prepareTextures = false; info.canvasContext.unpinImages(); } + + auto grContext = info.canvasContext.getGrContext(); + for (auto mesh : mMeshes) { + mesh->updateSkMesh(grContext); + } + #endif bool hasBackwardProjectedNodesHere = false; @@ -168,6 +175,7 @@ void SkiaDisplayList::reset() { mDisplayList.reset(); + mMeshes.clear(); mMutableImages.clear(); mVectorDrawables.clear(); mAnimatedImages.clear(); diff --git a/libs/hwui/pipeline/skia/SkiaDisplayList.h b/libs/hwui/pipeline/skia/SkiaDisplayList.h index 2a677344b7b2..7af31a4dc4c6 100644 --- a/libs/hwui/pipeline/skia/SkiaDisplayList.h +++ b/libs/hwui/pipeline/skia/SkiaDisplayList.h @@ -18,6 +18,7 @@ #include <deque> +#include "Mesh.h" #include "RecordingCanvas.h" #include "RenderNodeDrawable.h" #include "TreeInfo.h" @@ -167,6 +168,7 @@ public: std::deque<RenderNodeDrawable> mChildNodes; std::deque<FunctorDrawable*> mChildFunctors; std::vector<SkImage*> mMutableImages; + std::vector<const Mesh*> mMeshes; private: std::vector<Pair<VectorDrawableRoot*, SkMatrix>> mVectorDrawables; diff --git a/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp b/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp index 2aca41e41905..cc987bcd8f0e 100644 --- a/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp +++ b/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp @@ -53,8 +53,14 @@ 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 (mHardwareBuffer) { + mRenderThread.requireGlContext(); + } else if (!isSurfaceReady() && mNativeWindow) { + setSurface(mNativeWindow.get(), mSwapBehavior); + } + EGLint error = 0; if (!mEglManager.makeCurrent(mEglSurface, &error)) { return MakeCurrentResult::AlreadyCurrent; @@ -72,8 +78,9 @@ IRenderPipeline::DrawResult SkiaOpenGLPipeline::draw( const Frame& frame, const SkRect& screenDirty, const SkRect& dirty, const LightGeometry& lightGeometry, LayerUpdateQueue* layerUpdateQueue, const Rect& contentDrawBounds, bool opaque, const LightInfo& lightInfo, - const std::vector<sp<RenderNode>>& renderNodes, FrameInfoVisualizer* profiler) { - if (!isCapturingSkp()) { + const std::vector<sp<RenderNode>>& renderNodes, FrameInfoVisualizer* profiler, + const HardwareBufferRenderParams& bufferParams) { + if (!isCapturingSkp() && !mHardwareBuffer) { mEglManager.damageFrame(frame, dirty); } @@ -100,19 +107,31 @@ IRenderPipeline::DrawResult SkiaOpenGLPipeline::draw( SkSurfaceProps props(0, kUnknown_SkPixelGeometry); SkASSERT(mRenderThread.getGrContext() != nullptr); - sk_sp<SkSurface> surface(SkSurface::MakeFromBackendRenderTarget( - mRenderThread.getGrContext(), backendRT, this->getSurfaceOrigin(), colorType, - mSurfaceColorSpace, &props)); + sk_sp<SkSurface> surface; + SkMatrix preTransform; + if (mHardwareBuffer) { + surface = getBufferSkSurface(bufferParams); + preTransform = bufferParams.getTransform(); + } else { + surface = SkSurface::MakeFromBackendRenderTarget(mRenderThread.getGrContext(), backendRT, + getSurfaceOrigin(), colorType, + mSurfaceColorSpace, &props); + preTransform = SkMatrix::I(); + } - LightingInfo::updateLighting(lightGeometry, lightInfo); + SkPoint lightCenter = preTransform.mapXY(lightGeometry.center.x, lightGeometry.center.y); + LightGeometry localGeometry = lightGeometry; + localGeometry.center.x = lightCenter.fX; + localGeometry.center.y = lightCenter.fY; + LightingInfo::updateLighting(localGeometry, lightInfo); renderFrame(*layerUpdateQueue, dirty, renderNodes, opaque, contentDrawBounds, surface, - SkMatrix::I()); + preTransform); // Draw visual debugging features 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); } @@ -138,6 +157,10 @@ bool SkiaOpenGLPipeline::swapBuffers(const Frame& frame, bool drew, const SkRect // metrics the frame was swapped at this point currentFrameInfo->markSwapBuffers(); + if (mHardwareBuffer) { + return false; + } + *requireSwap = drew || mEglManager.damageRequiresSwap(); if (*requireSwap && (CC_UNLIKELY(!mEglManager.swapBuffers(frame, screenDirty)))) { @@ -166,6 +189,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,13 +208,34 @@ 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; } return false; } +[[nodiscard]] android::base::unique_fd SkiaOpenGLPipeline::flush() { + int fence = -1; + EGLSyncKHR sync = EGL_NO_SYNC_KHR; + mEglManager.createReleaseFence(true, &sync, &fence); + // If a sync object is returned here then the device does not support native + // fences, we block on the returned sync and return -1 as a file descriptor + if (sync != EGL_NO_SYNC_KHR) { + EGLDisplay display = mEglManager.eglDisplay(); + EGLint result = eglClientWaitSyncKHR(display, sync, 0, 1000000000); + if (result == EGL_FALSE) { + ALOGE("EglManager::createReleaseFence: error waiting for previous fence: %#x", + eglGetError()); + } else if (result == EGL_TIMEOUT_EXPIRED_KHR) { + ALOGE("EglManager::createReleaseFence: timeout waiting for previous fence"); + } + eglDestroySyncKHR(display, sync); + } + return android::base::unique_fd(fence); +} + bool SkiaOpenGLPipeline::isSurfaceReady() { return CC_UNLIKELY(mEglSurface != EGL_NO_SURFACE); } diff --git a/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.h b/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.h index 186998a01745..940d6bfdb83c 100644 --- a/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.h +++ b/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.h @@ -21,6 +21,7 @@ #include "SkiaPipeline.h" #include "renderstate/RenderState.h" +#include "renderthread/HardwareBufferRenderParams.h" namespace android { @@ -36,19 +37,18 @@ public: renderthread::MakeCurrentResult makeCurrent() override; renderthread::Frame getFrame() override; - renderthread::IRenderPipeline::DrawResult draw(const renderthread::Frame& frame, - const SkRect& screenDirty, const SkRect& dirty, - const LightGeometry& lightGeometry, - LayerUpdateQueue* layerUpdateQueue, - const Rect& contentDrawBounds, bool opaque, - const LightInfo& lightInfo, - const std::vector<sp<RenderNode> >& renderNodes, - FrameInfoVisualizer* profiler) override; + renderthread::IRenderPipeline::DrawResult draw( + const renderthread::Frame& frame, const SkRect& screenDirty, const SkRect& dirty, + const LightGeometry& lightGeometry, LayerUpdateQueue* layerUpdateQueue, + const Rect& contentDrawBounds, bool opaque, const LightInfo& lightInfo, + const std::vector<sp<RenderNode> >& renderNodes, FrameInfoVisualizer* profiler, + const renderthread::HardwareBufferRenderParams& bufferParams) override; GrSurfaceOrigin getSurfaceOrigin() override { return kBottomLeft_GrSurfaceOrigin; } bool swapBuffers(const renderthread::Frame& frame, bool drew, const SkRect& screenDirty, FrameInfo* currentFrameInfo, bool* requireSwap) override; DeferredLayerUpdater* createTextureLayer() override; bool setSurface(ANativeWindow* surface, renderthread::SwapBehavior swapBehavior) override; + [[nodiscard]] android::base::unique_fd flush() override; void onStop() override; bool isSurfaceReady() override; bool isContextReady() override; @@ -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..8ea71f11e2f0 100644 --- a/libs/hwui/pipeline/skia/SkiaPipeline.cpp +++ b/libs/hwui/pipeline/skia/SkiaPipeline.cpp @@ -16,16 +16,27 @@ #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 "include/gpu/GpuTypes.h" // from Skia #include <android-base/properties.h> #include <unistd.h> @@ -177,7 +188,7 @@ bool SkiaPipeline::createOrUpdateLayer(RenderNode* node, const DamageAccumulator SkSurfaceProps props(0, kUnknown_SkPixelGeometry); SkASSERT(mRenderThread.getGrContext() != nullptr); node->setLayerSurface(SkSurface::MakeRenderTarget(mRenderThread.getGrContext(), - SkBudgeted::kYes, info, 0, + skgpu::Budgeted::kYes, info, 0, this->getSurfaceOrigin(), &props)); if (node->getLayerSurface()) { // update the transform in window of the layer to reset its origin wrt light source @@ -488,8 +499,7 @@ void SkiaPipeline::renderFrameImpl(const SkRect& clip, } canvas->concat(preTransform); - // STOPSHIP: Revert, temporary workaround to clear always F16 frame buffer for b/74976293 - if (!opaque || getSurfaceColorType() == kRGBA_F16_SkColorType) { + if (!opaque) { canvas->clear(SK_ColorTRANSPARENT); } @@ -594,6 +604,31 @@ void SkiaPipeline::dumpResourceCacheUsage() const { ALOGD("%s", log.c_str()); } +void SkiaPipeline::setHardwareBuffer(AHardwareBuffer* buffer) { + if (mHardwareBuffer) { + AHardwareBuffer_release(mHardwareBuffer); + mHardwareBuffer = nullptr; + } + + if (buffer) { + AHardwareBuffer_acquire(buffer); + mHardwareBuffer = buffer; + } +} + +sk_sp<SkSurface> SkiaPipeline::getBufferSkSurface( + const renderthread::HardwareBufferRenderParams& bufferParams) { + auto bufferColorSpace = bufferParams.getColorSpace(); + if (mBufferSurface == nullptr || mBufferColorSpace == nullptr || + !SkColorSpace::Equals(mBufferColorSpace.get(), bufferColorSpace.get())) { + mBufferSurface = SkSurface::MakeFromAHardwareBuffer( + mRenderThread.getGrContext(), mHardwareBuffer, kTopLeft_GrSurfaceOrigin, + bufferColorSpace, nullptr, true); + mBufferColorSpace = bufferColorSpace; + } + return mBufferSurface; +} + void SkiaPipeline::setSurfaceColorProperties(ColorMode colorMode) { mColorMode = colorMode; switch (colorMode) { @@ -606,12 +641,14 @@ void SkiaPipeline::setSurfaceColorProperties(ColorMode colorMode) { mSurfaceColorSpace = DeviceInfo::get()->getWideColorSpace(); break; case ColorMode::Hdr: - mSurfaceColorType = SkColorType::kRGBA_F16_SkColorType; - mSurfaceColorSpace = SkColorSpace::MakeRGB(GetPQSkTransferFunction(), SkNamedGamut::kRec2020); + mSurfaceColorType = SkColorType::kN32_SkColorType; + mSurfaceColorSpace = SkColorSpace::MakeRGB( + GetExtendedTransferFunction(mTargetSdrHdrRatio), SkNamedGamut::kDisplayP3); break; case ColorMode::Hdr10: mSurfaceColorType = SkColorType::kRGBA_1010102_SkColorType; - mSurfaceColorSpace = SkColorSpace::MakeRGB(GetPQSkTransferFunction(), SkNamedGamut::kRec2020); + mSurfaceColorSpace = SkColorSpace::MakeRGB( + GetExtendedTransferFunction(mTargetSdrHdrRatio), SkNamedGamut::kDisplayP3); break; case ColorMode::A8: mSurfaceColorType = SkColorType::kAlpha_8_SkColorType; @@ -620,6 +657,16 @@ void SkiaPipeline::setSurfaceColorProperties(ColorMode colorMode) { } } +void SkiaPipeline::setTargetSdrHdrRatio(float ratio) { + if (mColorMode == ColorMode::Hdr || mColorMode == ColorMode::Hdr10) { + mTargetSdrHdrRatio = ratio; + mSurfaceColorSpace = SkColorSpace::MakeRGB(GetExtendedTransferFunction(mTargetSdrHdrRatio), + SkNamedGamut::kDisplayP3); + } else { + mTargetSdrHdrRatio = 1.f; + } +} + // Overdraw debugging // These colors should be kept in sync with Caches::getOverdrawColor() with a few differences. diff --git a/libs/hwui/pipeline/skia/SkiaPipeline.h b/libs/hwui/pipeline/skia/SkiaPipeline.h index bc8a5659dd83..befee8989383 100644 --- a/libs/hwui/pipeline/skia/SkiaPipeline.h +++ b/libs/hwui/pipeline/skia/SkiaPipeline.h @@ -16,14 +16,18 @@ #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/HardwareBufferRenderParams.h" #include "renderthread/IRenderPipeline.h" +class SkFILEWStream; class SkPictureRecorder; struct SkSharingSerialContext; @@ -71,14 +75,26 @@ public: mCaptureMode = callback ? CaptureMode::CallbackAPI : CaptureMode::None; } + virtual void setHardwareBuffer(AHardwareBuffer* buffer) override; + bool hasHardwareBuffer() override { return mHardwareBuffer != nullptr; } + + void setTargetSdrHdrRatio(float ratio) override; + protected: + sk_sp<SkSurface> getBufferSkSurface( + const renderthread::HardwareBufferRenderParams& bufferParams); void dumpResourceCacheUsage() const; renderthread::RenderThread& mRenderThread; + AHardwareBuffer* mHardwareBuffer = nullptr; + sk_sp<SkSurface> mBufferSurface = nullptr; + sk_sp<SkColorSpace> mBufferColorSpace = nullptr; + ColorMode mColorMode = ColorMode::Default; SkColorType mSurfaceColorType; sk_sp<SkColorSpace> mSurfaceColorSpace; + float mTargetSdrHdrRatio = 1.f; bool isCapturingSkp() const { return mCaptureMode != CaptureMode::None; } 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..3ca7eeb37a89 100644 --- a/libs/hwui/pipeline/skia/SkiaRecordingCanvas.cpp +++ b/libs/hwui/pipeline/skia/SkiaRecordingCanvas.cpp @@ -16,7 +16,19 @@ #include "SkiaRecordingCanvas.h" #include "hwui/Paint.h" +#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" @@ -30,6 +42,8 @@ #include "pipeline/skia/VkFunctorDrawable.h" #include "pipeline/skia/VkInteropFunctorDrawable.h" #endif +#include <log/log.h> +#include <ui/FatVector.h> namespace android { namespace uirenderer { @@ -42,7 +56,7 @@ namespace skiapipeline { void SkiaRecordingCanvas::initDisplayList(uirenderer::RenderNode* renderNode, int width, int height) { mCurrentBarrier = nullptr; - SkASSERT(mDisplayList.get() == nullptr); + LOG_FATAL_IF(mDisplayList.get() != nullptr); if (renderNode) { mDisplayList = renderNode->detachAvailableList(); @@ -56,20 +70,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); @@ -192,40 +208,52 @@ void SkiaRecordingCanvas::FilterForImage(SkPaint& paint) { } } +void SkiaRecordingCanvas::handleMutableImages(Bitmap& bitmap, DrawImagePayload& payload) { + // if image->unique() is true, then mRecorder.drawImage failed for some reason. It also means + // it is not safe to store a raw SkImage pointer, because the image object will be destroyed + // when this function ends. + if (!bitmap.isImmutable() && payload.image.get() && !payload.image->unique()) { + mDisplayList->mMutableImages.push_back(payload.image.get()); + } + + if (bitmap.hasGainmap()) { + auto gainmapBitmap = bitmap.gainmap()->bitmap; + // Not all DrawImagePayload receivers will store the gainmap (such as DrawImageLattice), + // so only store it in the mutable list if it was actually recorded + if (!gainmapBitmap->isImmutable() && payload.gainmapImage.get() && + !payload.gainmapImage->unique()) { + mDisplayList->mMutableImages.push_back(payload.gainmapImage.get()); + } + } +} + void SkiaRecordingCanvas::drawBitmap(Bitmap& bitmap, float left, float top, const Paint* paint) { - sk_sp<SkImage> image = bitmap.makeImage(); + auto payload = DrawImagePayload(bitmap); applyLooper( paint, [&](const Paint& p) { - mRecorder.drawImage(image, left, top, p.sampling(), &p, bitmap.palette()); + mRecorder.drawImage(DrawImagePayload(payload), left, top, p.sampling(), &p); }, FilterForImage); - // if image->unique() is true, then mRecorder.drawImage failed for some reason. It also means - // it is not safe to store a raw SkImage pointer, because the image object will be destroyed - // when this function ends. - if (!bitmap.isImmutable() && image.get() && !image->unique()) { - mDisplayList->mMutableImages.push_back(image.get()); - } + handleMutableImages(bitmap, payload); } void SkiaRecordingCanvas::drawBitmap(Bitmap& bitmap, const SkMatrix& matrix, const Paint* paint) { SkAutoCanvasRestore acr(&mRecorder, true); concat(matrix); - sk_sp<SkImage> image = bitmap.makeImage(); + auto payload = DrawImagePayload(bitmap); applyLooper( paint, [&](const Paint& p) { - mRecorder.drawImage(image, 0, 0, p.sampling(), &p, bitmap.palette()); + mRecorder.drawImage(DrawImagePayload(payload), 0, 0, p.sampling(), &p); }, FilterForImage); - if (!bitmap.isImmutable() && image.get() && !image->unique()) { - mDisplayList->mMutableImages.push_back(image.get()); - } + handleMutableImages(bitmap, payload); } void SkiaRecordingCanvas::drawBitmap(Bitmap& bitmap, float srcLeft, float srcTop, float srcRight, @@ -234,20 +262,17 @@ void SkiaRecordingCanvas::drawBitmap(Bitmap& bitmap, float srcLeft, float srcTop SkRect srcRect = SkRect::MakeLTRB(srcLeft, srcTop, srcRight, srcBottom); SkRect dstRect = SkRect::MakeLTRB(dstLeft, dstTop, dstRight, dstBottom); - sk_sp<SkImage> image = bitmap.makeImage(); + auto payload = DrawImagePayload(bitmap); applyLooper( paint, [&](const Paint& p) { - mRecorder.drawImageRect(image, srcRect, dstRect, p.sampling(), &p, - SkCanvas::kFast_SrcRectConstraint, bitmap.palette()); + mRecorder.drawImageRect(DrawImagePayload(payload), srcRect, dstRect, p.sampling(), + &p, SkCanvas::kFast_SrcRectConstraint); }, FilterForImage); - if (!bitmap.isImmutable() && image.get() && !image->unique() && !srcRect.isEmpty() && - !dstRect.isEmpty()) { - mDisplayList->mMutableImages.push_back(image.get()); - } + handleMutableImages(bitmap, payload); } void SkiaRecordingCanvas::drawNinePatch(Bitmap& bitmap, const Res_png_9patch& chunk, float dstLeft, @@ -265,15 +290,17 @@ void SkiaRecordingCanvas::drawNinePatch(Bitmap& bitmap, const Res_png_9patch& ch numFlags = (lattice.fXCount + 1) * (lattice.fYCount + 1); } - SkAutoSTMalloc<25, SkCanvas::Lattice::RectType> flags(numFlags); - SkAutoSTMalloc<25, SkColor> colors(numFlags); + // Most times, we do not have very many flags/colors, so the stack allocated part of + // FatVector will save us a heap allocation. + FatVector<SkCanvas::Lattice::RectType, 25> flags(numFlags); + FatVector<SkColor, 25> colors(numFlags); if (numFlags > 0) { - NinePatchUtils::SetLatticeFlags(&lattice, flags.get(), numFlags, chunk, colors.get()); + NinePatchUtils::SetLatticeFlags(&lattice, flags.data(), numFlags, chunk, colors.data()); } lattice.fBounds = nullptr; SkRect dst = SkRect::MakeLTRB(dstLeft, dstTop, dstRight, dstBottom); - sk_sp<SkImage> image = bitmap.makeImage(); + auto payload = DrawImagePayload(bitmap); // HWUI always draws 9-patches with linear filtering, regardless of the Paint. const SkFilterMode filter = SkFilterMode::kLinear; @@ -281,13 +308,11 @@ void SkiaRecordingCanvas::drawNinePatch(Bitmap& bitmap, const Res_png_9patch& ch applyLooper( paint, [&](const SkPaint& p) { - mRecorder.drawImageLattice(image, lattice, dst, filter, &p, bitmap.palette()); + mRecorder.drawImageLattice(DrawImagePayload(payload), lattice, dst, filter, &p); }, FilterForImage); - if (!bitmap.isImmutable() && image.get() && !image->unique() && !dst.isEmpty()) { - mDisplayList->mMutableImages.push_back(image.get()); - } + handleMutableImages(bitmap, payload); } double SkiaRecordingCanvas::drawAnimatedImage(AnimatedImageDrawable* animatedImage) { @@ -296,6 +321,11 @@ double SkiaRecordingCanvas::drawAnimatedImage(AnimatedImageDrawable* animatedIma return 0; } +void SkiaRecordingCanvas::drawMesh(const Mesh& mesh, sk_sp<SkBlender> blender, const Paint& paint) { + mDisplayList->mMeshes.push_back(&mesh); + mRecorder.drawMesh(mesh, blender, paint); +} + } // namespace skiapipeline } // namespace uirenderer } // namespace android diff --git a/libs/hwui/pipeline/skia/SkiaRecordingCanvas.h b/libs/hwui/pipeline/skia/SkiaRecordingCanvas.h index 1445a27e4248..a8e4580dc200 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(); @@ -76,6 +81,7 @@ public: virtual void drawVectorDrawable(VectorDrawableRoot* vectorDrawable) override; virtual void enableZ(bool enableZ) override; + virtual void drawMesh(const Mesh& mesh, sk_sp<SkBlender> blender, const Paint& paint) override; virtual void drawLayer(uirenderer::DeferredLayerUpdater* layerHandle) override; virtual void drawRenderNode(uirenderer::RenderNode* renderNode) override; @@ -97,6 +103,8 @@ private: */ void initDisplayList(uirenderer::RenderNode* renderNode, int width, int height); + void handleMutableImages(Bitmap& bitmap, DrawImagePayload& payload); + using INHERITED = SkiaCanvas; }; diff --git a/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp b/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp index 905d46e58014..c8f2e69ae0a4 100644 --- a/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp +++ b/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp @@ -55,7 +55,14 @@ 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 (mHardwareBuffer) { + mRenderThread.requireVkContext(); + } else if (!isSurfaceReady() && mNativeWindow) { + setSurface(mNativeWindow.get(), SwapBehavior::kSwap_default); + } + return isContextReady() ? MakeCurrentResult::AlreadyCurrent : MakeCurrentResult::Failed; } Frame SkiaVulkanPipeline::getFrame() { @@ -67,28 +74,39 @@ IRenderPipeline::DrawResult SkiaVulkanPipeline::draw( const Frame& frame, const SkRect& screenDirty, const SkRect& dirty, const LightGeometry& lightGeometry, LayerUpdateQueue* layerUpdateQueue, const Rect& contentDrawBounds, bool opaque, const LightInfo& lightInfo, - const std::vector<sp<RenderNode>>& renderNodes, FrameInfoVisualizer* profiler) { - sk_sp<SkSurface> backBuffer = mVkSurface->getCurrentSkSurface(); + const std::vector<sp<RenderNode>>& renderNodes, FrameInfoVisualizer* profiler, + const HardwareBufferRenderParams& bufferParams) { + sk_sp<SkSurface> backBuffer; + SkMatrix preTransform; + if (mHardwareBuffer) { + backBuffer = getBufferSkSurface(bufferParams); + preTransform = bufferParams.getTransform(); + } else { + backBuffer = mVkSurface->getCurrentSkSurface(); + preTransform = mVkSurface->getCurrentPreTransform(); + } + if (backBuffer.get() == nullptr) { return {false, -1}; } // update the coordinates of the global light position based on surface rotation - SkPoint lightCenter = mVkSurface->getCurrentPreTransform().mapXY(lightGeometry.center.x, - lightGeometry.center.y); + SkPoint lightCenter = preTransform.mapXY(lightGeometry.center.x, lightGeometry.center.y); LightGeometry localGeometry = lightGeometry; localGeometry.center.x = lightCenter.fX; localGeometry.center.y = lightCenter.fY; LightingInfo::updateLighting(localGeometry, lightInfo); renderFrame(*layerUpdateQueue, dirty, renderNodes, opaque, contentDrawBounds, backBuffer, - mVkSurface->getCurrentPreTransform()); + preTransform); // Draw visual debugging features 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); } @@ -109,12 +127,16 @@ IRenderPipeline::DrawResult SkiaVulkanPipeline::draw( bool SkiaVulkanPipeline::swapBuffers(const Frame& frame, bool drew, const SkRect& screenDirty, FrameInfo* currentFrameInfo, bool* requireSwap) { - *requireSwap = drew; - // Even if we decided to cancel the frame, from the perspective of jank // metrics the frame was swapped at this point currentFrameInfo->markSwapBuffers(); + if (mHardwareBuffer) { + return false; + } + + *requireSwap = drew; + if (*requireSwap) { vulkanManager().swapBuffers(mVkSurface, screenDirty); } @@ -130,7 +152,17 @@ DeferredLayerUpdater* SkiaVulkanPipeline::createTextureLayer() { void SkiaVulkanPipeline::onStop() {} -bool SkiaVulkanPipeline::setSurface(ANativeWindow* surface, SwapBehavior swapBehavior) { +[[nodiscard]] android::base::unique_fd SkiaVulkanPipeline::flush() { + int fence = -1; + vulkanManager().createReleaseFence(&fence, mRenderThread.getGrContext()); + return android::base::unique_fd(fence); +} + +// 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; @@ -146,6 +178,13 @@ bool SkiaVulkanPipeline::setSurface(ANativeWindow* surface, SwapBehavior swapBeh return mVkSurface != nullptr; } +void SkiaVulkanPipeline::setTargetSdrHdrRatio(float ratio) { + SkiaPipeline::setTargetSdrHdrRatio(ratio); + if (mVkSurface) { + mVkSurface->setColorSpace(mSurfaceColorSpace); + } +} + bool SkiaVulkanPipeline::isSurfaceReady() { return CC_UNLIKELY(mVkSurface != nullptr); } diff --git a/libs/hwui/pipeline/skia/SkiaVulkanPipeline.h b/libs/hwui/pipeline/skia/SkiaVulkanPipeline.h index ada6af67d4a0..d921ddb0d0fb 100644 --- a/libs/hwui/pipeline/skia/SkiaVulkanPipeline.h +++ b/libs/hwui/pipeline/skia/SkiaVulkanPipeline.h @@ -16,11 +16,15 @@ #pragma once +#include "SkRefCnt.h" #include "SkiaPipeline.h" +#include "renderstate/RenderState.h" +#include "renderthread/HardwareBufferRenderParams.h" #include "renderthread/VulkanManager.h" #include "renderthread/VulkanSurface.h" -#include "renderstate/RenderState.h" +class SkBitmap; +struct SkRect; namespace android { namespace uirenderer { @@ -33,22 +37,24 @@ public: renderthread::MakeCurrentResult makeCurrent() override; renderthread::Frame getFrame() override; - renderthread::IRenderPipeline::DrawResult draw(const renderthread::Frame& frame, - const SkRect& screenDirty, const SkRect& dirty, - const LightGeometry& lightGeometry, - LayerUpdateQueue* layerUpdateQueue, - const Rect& contentDrawBounds, bool opaque, - const LightInfo& lightInfo, - const std::vector<sp<RenderNode> >& renderNodes, - FrameInfoVisualizer* profiler) override; + renderthread::IRenderPipeline::DrawResult draw( + const renderthread::Frame& frame, const SkRect& screenDirty, const SkRect& dirty, + const LightGeometry& lightGeometry, LayerUpdateQueue* layerUpdateQueue, + const Rect& contentDrawBounds, bool opaque, const LightInfo& lightInfo, + const std::vector<sp<RenderNode> >& renderNodes, FrameInfoVisualizer* profiler, + const renderthread::HardwareBufferRenderParams& bufferParams) override; GrSurfaceOrigin getSurfaceOrigin() override { return kTopLeft_GrSurfaceOrigin; } bool swapBuffers(const renderthread::Frame& frame, bool drew, const SkRect& screenDirty, FrameInfo* currentFrameInfo, bool* requireSwap) override; DeferredLayerUpdater* createTextureLayer() override; + [[nodiscard]] android::base::unique_fd flush() override; + bool setSurface(ANativeWindow* surface, renderthread::SwapBehavior swapBehavior) override; void onStop() override; bool isSurfaceReady() override; bool isContextReady() override; + bool supportsExtendedRangeHdr() const override { return true; } + void setTargetSdrHdrRatio(float ratio) override; static void invokeFunctor(const renderthread::RenderThread& thread, Functor* functor); static sk_sp<Bitmap> allocateHardwareBitmap(renderthread::RenderThread& thread, @@ -59,8 +65,8 @@ protected: private: renderthread::VulkanManager& vulkanManager(); - renderthread::VulkanSurface* mVkSurface = nullptr; + sp<ANativeWindow> mNativeWindow; }; } /* namespace skiapipeline */ diff --git a/libs/hwui/pipeline/skia/StretchMask.cpp b/libs/hwui/pipeline/skia/StretchMask.cpp index 2dbeb3adfab3..cad3703d8d2b 100644 --- a/libs/hwui/pipeline/skia/StretchMask.cpp +++ b/libs/hwui/pipeline/skia/StretchMask.cpp @@ -14,8 +14,12 @@ * limitations under the License. */ #include "StretchMask.h" -#include "SkSurface.h" + +#include "SkBlendMode.h" #include "SkCanvas.h" +#include "SkSurface.h" +#include "include/gpu/GpuTypes.h" // from Skia + #include "TransformCanvas.h" #include "SkiaDisplayList.h" @@ -34,7 +38,7 @@ void StretchMask::draw(GrRecordingContext* context, // not match. mMaskSurface = SkSurface::MakeRenderTarget( context, - SkBudgeted::kYes, + skgpu::Budgeted::kYes, SkImageInfo::Make( width, height, 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/pipeline/skia/VkInteropFunctorDrawable.cpp b/libs/hwui/pipeline/skia/VkInteropFunctorDrawable.cpp index 3c7617d35c7c..e168a7b9459a 100644 --- a/libs/hwui/pipeline/skia/VkInteropFunctorDrawable.cpp +++ b/libs/hwui/pipeline/skia/VkInteropFunctorDrawable.cpp @@ -33,6 +33,8 @@ #include "thread/ThreadBase.h" #include "utils/TimeUtils.h" +#include <SkBlendMode.h> + namespace android { namespace uirenderer { namespace skiapipeline { diff --git a/libs/hwui/renderthread/CacheManager.cpp b/libs/hwui/renderthread/CacheManager.cpp index ded2b06fb3cf..23611efccd73 100644 --- a/libs/hwui/renderthread/CacheManager.cpp +++ b/libs/hwui/renderthread/CacheManager.cpp @@ -16,6 +16,15 @@ #include "CacheManager.h" +#include <GrContextOptions.h> +#include <SkExecutor.h> +#include <SkGraphics.h> +#include <math.h> +#include <utils/Trace.h> + +#include <set> + +#include "CanvasContext.h" #include "DeviceInfo.h" #include "Layer.h" #include "Properties.h" @@ -25,40 +34,44 @@ #include "pipeline/skia/SkiaMemoryTracer.h" #include "renderstate/RenderState.h" #include "thread/CommonPool.h" -#include <utils/Trace.h> - -#include <GrContextOptions.h> -#include <SkExecutor.h> -#include <SkGraphics.h> -#include <SkMathPriv.h> -#include <math.h> -#include <set> namespace android { namespace uirenderer { namespace renderthread { -// This multiplier was selected based on historical review of cache sizes relative -// to the screen resolution. This is meant to be a conservative default based on -// that analysis. The 4.0f is used because the default pixel format is assumed to -// be ARGB_8888. -#define SURFACE_SIZE_MULTIPLIER (12.0f * 4.0f) -#define BACKGROUND_RETENTION_PERCENTAGE (0.5f) - -CacheManager::CacheManager() - : mMaxSurfaceArea(DeviceInfo::getWidth() * DeviceInfo::getHeight()) - , mMaxResourceBytes(mMaxSurfaceArea * SURFACE_SIZE_MULTIPLIER) - , mBackgroundResourceBytes(mMaxResourceBytes * BACKGROUND_RETENTION_PERCENTAGE) - // This sets the maximum size for a single texture atlas in the GPU font cache. If - // necessary, the cache can allocate additional textures that are counted against the - // total cache limits provided to Skia. - , mMaxGpuFontAtlasBytes(GrNextSizePow2(mMaxSurfaceArea)) - // This sets the maximum size of the CPU font cache to be at least the same size as the - // total number of GPU font caches (i.e. 4 separate GPU atlases). - , mMaxCpuFontCacheBytes( - std::max(mMaxGpuFontAtlasBytes * 4, SkGraphics::GetFontCacheLimit())) - , mBackgroundCpuFontCacheBytes(mMaxCpuFontCacheBytes * BACKGROUND_RETENTION_PERCENTAGE) { +CacheManager::CacheManager(RenderThread& thread) + : mRenderThread(thread), mMemoryPolicy(loadMemoryPolicy()) { + mMaxSurfaceArea = static_cast<size_t>((DeviceInfo::getWidth() * DeviceInfo::getHeight()) * + mMemoryPolicy.initialMaxSurfaceAreaScale); + setupCacheLimits(); +} + +static inline int countLeadingZeros(uint32_t mask) { + // __builtin_clz(0) is undefined, so we have to detect that case. + return mask ? __builtin_clz(mask) : 32; +} + +// Return the smallest power-of-2 >= n. +static inline uint32_t nextPowerOfTwo(uint32_t n) { + return n ? (1 << (32 - countLeadingZeros(n - 1))) : 1; +} + +void CacheManager::setupCacheLimits() { + mMaxResourceBytes = mMaxSurfaceArea * mMemoryPolicy.surfaceSizeMultiplier; + mBackgroundResourceBytes = mMaxResourceBytes * mMemoryPolicy.backgroundRetentionPercent; + // This sets the maximum size for a single texture atlas in the GPU font cache. If + // necessary, the cache can allocate additional textures that are counted against the + // total cache limits provided to Skia. + mMaxGpuFontAtlasBytes = nextPowerOfTwo(mMaxSurfaceArea); + // This sets the maximum size of the CPU font cache to be at least the same size as the + // total number of GPU font caches (i.e. 4 separate GPU atlases). + mMaxCpuFontCacheBytes = std::max(mMaxGpuFontAtlasBytes * 4, SkGraphics::GetFontCacheLimit()); + mBackgroundCpuFontCacheBytes = mMaxCpuFontCacheBytes * mMemoryPolicy.backgroundRetentionPercent; + SkGraphics::SetFontCacheLimit(mMaxCpuFontCacheBytes); + if (mGrContext) { + mGrContext->setResourceCacheLimit(mMaxResourceBytes); + } } void CacheManager::reset(sk_sp<GrDirectContext> context) { @@ -69,6 +82,7 @@ void CacheManager::reset(sk_sp<GrDirectContext> context) { if (context) { mGrContext = std::move(context); mGrContext->setResourceCacheLimit(mMaxResourceBytes); + mLastDeferredCleanup = systemTime(CLOCK_MONOTONIC); } } @@ -93,10 +107,9 @@ void CacheManager::configureContext(GrContextOptions* contextOptions, const void auto& cache = skiapipeline::ShaderCache::get(); cache.initShaderDiskCache(identity, size); contextOptions->fPersistentCache = &cache; - contextOptions->fGpuPathRenderers &= ~GpuPathRenderers::kCoverageCounting; } -void CacheManager::trimMemory(TrimMemoryMode mode) { +void CacheManager::trimMemory(TrimLevel mode) { if (!mGrContext) { return; } @@ -104,21 +117,28 @@ void CacheManager::trimMemory(TrimMemoryMode mode) { // flush and submit all work to the gpu and wait for it to finish mGrContext->flushAndSubmit(/*syncCpu=*/true); + if (!Properties::isHighEndGfx && mode >= TrimLevel::MODERATE) { + mode = TrimLevel::COMPLETE; + } + switch (mode) { - case TrimMemoryMode::Complete: + case TrimLevel::COMPLETE: mGrContext->freeGpuResources(); SkGraphics::PurgeAllCaches(); + mRenderThread.destroyRenderingContext(); break; - case TrimMemoryMode::UiHidden: + case TrimLevel::UI_HIDDEN: // Here we purge all the unlocked scratch resources and then toggle the resources cache // limits between the background and max amounts. This causes the unlocked resources // that have persistent data to be purged in LRU order. - mGrContext->purgeUnlockedResources(true); mGrContext->setResourceCacheLimit(mBackgroundResourceBytes); - mGrContext->setResourceCacheLimit(mMaxResourceBytes); SkGraphics::SetFontCacheLimit(mBackgroundCpuFontCacheBytes); + mGrContext->purgeUnlockedResources(mMemoryPolicy.purgeScratchOnly); + mGrContext->setResourceCacheLimit(mMaxResourceBytes); SkGraphics::SetFontCacheLimit(mMaxCpuFontCacheBytes); break; + default: + break; } } @@ -147,11 +167,29 @@ void CacheManager::getMemoryUsage(size_t* cpuUsage, size_t* gpuUsage) { } void CacheManager::dumpMemoryUsage(String8& log, const RenderState* renderState) { + log.appendFormat(R"(Memory policy: + Max surface area: %zu + Max resource usage: %.2fMB (x%.0f) + Background retention: %.0f%% (altUiHidden = %s) +)", + mMaxSurfaceArea, mMaxResourceBytes / 1000000.f, + mMemoryPolicy.surfaceSizeMultiplier, + mMemoryPolicy.backgroundRetentionPercent * 100.0f, + mMemoryPolicy.useAlternativeUiHidden ? "true" : "false"); + if (Properties::isSystemOrPersistent) { + log.appendFormat(" IsSystemOrPersistent\n"); + } + log.appendFormat(" GPU Context timeout: %" PRIu64 "\n", ns2s(mMemoryPolicy.contextTimeout)); + size_t stoppedContexts = 0; + for (auto context : mCanvasContexts) { + if (context->isStopped()) stoppedContexts++; + } + log.appendFormat("Contexts: %zu (stopped = %zu)\n", mCanvasContexts.size(), stoppedContexts); + if (!mGrContext) { - log.appendFormat("No valid cache instance.\n"); + log.appendFormat("No GPU context.\n"); return; } - std::vector<skiapipeline::ResourcePair> cpuResourceMap = { {"skia/sk_resource_cache/bitmap_", "Bitmaps"}, {"skia/sk_resource_cache/rrect-blur_", "Masks"}, @@ -199,6 +237,8 @@ void CacheManager::dumpMemoryUsage(String8& log, const RenderState* renderState) } void CacheManager::onFrameCompleted() { + cancelDestroyContext(); + mFrameCompletions.next() = systemTime(CLOCK_MONOTONIC); if (ATRACE_ENABLED()) { static skiapipeline::ATraceMemoryDump tracer; tracer.startFrame(); @@ -210,11 +250,82 @@ void CacheManager::onFrameCompleted() { } } -void CacheManager::performDeferredCleanup(nsecs_t cleanupOlderThanMillis) { - if (mGrContext) { - mGrContext->performDeferredCleanup( - std::chrono::milliseconds(cleanupOlderThanMillis), - /* scratchResourcesOnly */true); +void CacheManager::onThreadIdle() { + if (!mGrContext || mFrameCompletions.size() == 0) return; + + const nsecs_t now = systemTime(CLOCK_MONOTONIC); + // Rate limiting + if ((now - mLastDeferredCleanup) < 25_ms) { + mLastDeferredCleanup = now; + const nsecs_t frameCompleteNanos = mFrameCompletions[0]; + const nsecs_t frameDiffNanos = now - frameCompleteNanos; + const nsecs_t cleanupMillis = + ns2ms(std::max(frameDiffNanos, mMemoryPolicy.minimumResourceRetention)); + mGrContext->performDeferredCleanup(std::chrono::milliseconds(cleanupMillis), + mMemoryPolicy.purgeScratchOnly); + } +} + +void CacheManager::scheduleDestroyContext() { + if (mMemoryPolicy.contextTimeout > 0) { + mRenderThread.queue().postDelayed(mMemoryPolicy.contextTimeout, + [this, genId = mGenerationId] { + if (mGenerationId != genId) return; + // GenID should have already stopped this, but just in + // case + if (!areAllContextsStopped()) return; + mRenderThread.destroyRenderingContext(); + }); + } +} + +void CacheManager::cancelDestroyContext() { + if (mIsDestructionPending) { + mIsDestructionPending = false; + mGenerationId++; + } +} + +bool CacheManager::areAllContextsStopped() { + for (auto context : mCanvasContexts) { + if (!context->isStopped()) return false; + } + return true; +} + +void CacheManager::checkUiHidden() { + if (!mGrContext) return; + + if (mMemoryPolicy.useAlternativeUiHidden && areAllContextsStopped()) { + trimMemory(TrimLevel::UI_HIDDEN); + } +} + +void CacheManager::registerCanvasContext(CanvasContext* context) { + mCanvasContexts.push_back(context); + cancelDestroyContext(); +} + +void CacheManager::unregisterCanvasContext(CanvasContext* context) { + std::erase(mCanvasContexts, context); + checkUiHidden(); + if (mCanvasContexts.empty()) { + scheduleDestroyContext(); + } +} + +void CacheManager::onContextStopped(CanvasContext* context) { + checkUiHidden(); + if (mMemoryPolicy.releaseContextOnStoppedOnly && areAllContextsStopped()) { + scheduleDestroyContext(); + } +} + +void CacheManager::notifyNextFrameSize(int width, int height) { + int frameArea = width * height; + if (frameArea > mMaxSurfaceArea) { + mMaxSurfaceArea = frameArea; + setupCacheLimits(); } } diff --git a/libs/hwui/renderthread/CacheManager.h b/libs/hwui/renderthread/CacheManager.h index af82672c6f23..d21ac9badc43 100644 --- a/libs/hwui/renderthread/CacheManager.h +++ b/libs/hwui/renderthread/CacheManager.h @@ -22,7 +22,11 @@ #endif #include <SkSurface.h> #include <utils/String8.h> + #include <vector> + +#include "MemoryPolicy.h" +#include "utils/RingBuffer.h" #include "utils/TimeUtils.h" namespace android { @@ -35,17 +39,15 @@ class RenderState; namespace renderthread { -class IRenderPipeline; class RenderThread; +class CanvasContext; class CacheManager { public: - enum class TrimMemoryMode { Complete, UiHidden }; - #ifdef __ANDROID__ // Layoutlib does not support hardware acceleration void configureContext(GrContextOptions* context, const void* identity, ssize_t size); #endif - void trimMemory(TrimMemoryMode mode); + void trimMemory(TrimLevel mode); void trimStaleResources(); void dumpMemoryUsage(String8& log, const RenderState* renderState = nullptr); void getMemoryUsage(size_t* cpuUsage, size_t* gpuUsage); @@ -53,30 +55,50 @@ public: size_t getCacheSize() const { return mMaxResourceBytes; } size_t getBackgroundCacheSize() const { return mBackgroundResourceBytes; } void onFrameCompleted(); + void notifyNextFrameSize(int width, int height); + + void onThreadIdle(); - void performDeferredCleanup(nsecs_t cleanupOlderThanMillis); + void registerCanvasContext(CanvasContext* context); + void unregisterCanvasContext(CanvasContext* context); + void onContextStopped(CanvasContext* context); private: friend class RenderThread; - explicit CacheManager(); + explicit CacheManager(RenderThread& thread); + void setupCacheLimits(); + bool areAllContextsStopped(); + void checkUiHidden(); + void scheduleDestroyContext(); + void cancelDestroyContext(); #ifdef __ANDROID__ // Layoutlib does not support hardware acceleration void reset(sk_sp<GrDirectContext> grContext); #endif void destroy(); - const size_t mMaxSurfaceArea; + RenderThread& mRenderThread; + const MemoryPolicy& mMemoryPolicy; #ifdef __ANDROID__ // Layoutlib does not support hardware acceleration sk_sp<GrDirectContext> mGrContext; #endif - const size_t mMaxResourceBytes; - const size_t mBackgroundResourceBytes; + size_t mMaxSurfaceArea = 0; + + size_t mMaxResourceBytes = 0; + size_t mBackgroundResourceBytes = 0; + + size_t mMaxGpuFontAtlasBytes = 0; + size_t mMaxCpuFontCacheBytes = 0; + size_t mBackgroundCpuFontCacheBytes = 0; + + std::vector<CanvasContext*> mCanvasContexts; + RingBuffer<uint64_t, 100> mFrameCompletions; - const size_t mMaxGpuFontAtlasBytes; - const size_t mMaxCpuFontCacheBytes; - const size_t mBackgroundCpuFontCacheBytes; + nsecs_t mLastDeferredCleanup = 0; + bool mIsDestructionPending = false; + uint32_t mGenerationId = 0; }; } /* namespace renderthread */ diff --git a/libs/hwui/renderthread/CanvasContext.cpp b/libs/hwui/renderthread/CanvasContext.cpp index 976117b9bbd4..6b2c99534a4c 100644 --- a/libs/hwui/renderthread/CanvasContext.cpp +++ b/libs/hwui/renderthread/CanvasContext.cpp @@ -42,9 +42,6 @@ #include "utils/GLUtils.h" #include "utils/TimeUtils.h" -#define TRIM_MEMORY_COMPLETE 80 -#define TRIM_MEMORY_UI_HIDDEN 20 - #define LOG_FRAMETIME_MMA 0 #if LOG_FRAMETIME_MMA @@ -74,16 +71,19 @@ CanvasContext* ScopedActiveContext::sActiveContext = nullptr; } /* namespace */ CanvasContext* CanvasContext::create(RenderThread& thread, bool translucent, - RenderNode* rootRenderNode, IContextFactory* contextFactory) { + RenderNode* rootRenderNode, IContextFactory* contextFactory, + int32_t uiThreadId, int32_t renderThreadId) { auto renderType = Properties::getRenderPipelineType(); switch (renderType) { case RenderPipelineType::SkiaGL: return new CanvasContext(thread, translucent, rootRenderNode, contextFactory, - std::make_unique<skiapipeline::SkiaOpenGLPipeline>(thread)); + std::make_unique<skiapipeline::SkiaOpenGLPipeline>(thread), + uiThreadId, renderThreadId); case RenderPipelineType::SkiaVulkan: return new CanvasContext(thread, translucent, rootRenderNode, contextFactory, - std::make_unique<skiapipeline::SkiaVulkanPipeline>(thread)); + std::make_unique<skiapipeline::SkiaVulkanPipeline>(thread), + uiThreadId, renderThreadId); default: LOG_ALWAYS_FATAL("canvas context type %d not supported", (int32_t)renderType); break; @@ -113,7 +113,8 @@ void CanvasContext::prepareToDraw(const RenderThread& thread, Bitmap* bitmap) { CanvasContext::CanvasContext(RenderThread& thread, bool translucent, RenderNode* rootRenderNode, IContextFactory* contextFactory, - std::unique_ptr<IRenderPipeline> renderPipeline) + std::unique_ptr<IRenderPipeline> renderPipeline, pid_t uiThreadId, + pid_t renderThreadId) : mRenderThread(thread) , mGenerationID(0) , mOpaque(!translucent) @@ -121,7 +122,9 @@ CanvasContext::CanvasContext(RenderThread& thread, bool translucent, RenderNode* , mJankTracker(&thread.globalProfileData()) , mProfiler(mJankTracker.frames(), thread.timeLord().frameIntervalNanos()) , mContentDrawBounds(0, 0, 0, 0) - , mRenderPipeline(std::move(renderPipeline)) { + , mRenderPipeline(std::move(renderPipeline)) + , mHintSessionWrapper(uiThreadId, renderThreadId) { + mRenderThread.cacheManager().registerCanvasContext(this); rootRenderNode->makeRoot(); mRenderNodes.emplace_back(rootRenderNode); mProfiler.setDensity(DeviceInfo::getDensity()); @@ -133,6 +136,7 @@ CanvasContext::~CanvasContext() { node->clearRoot(); } mRenderNodes.clear(); + mRenderThread.cacheManager().unregisterCanvasContext(this); } void CanvasContext::addRenderNode(RenderNode* node, bool placeFront) { @@ -149,11 +153,13 @@ void CanvasContext::removeRenderNode(RenderNode* node) { void CanvasContext::destroy() { stopDrawing(); + setHardwareBuffer(nullptr); setSurface(nullptr); setSurfaceControl(nullptr); freePrefetchedLayers(); destroyHardwareResources(); mAnimationContext->destroy(); + mRenderThread.cacheManager().onContextStopped(this); } static void setBufferCount(ANativeWindow* window) { @@ -171,6 +177,19 @@ static void setBufferCount(ANativeWindow* window) { native_window_set_buffer_count(window, bufferCount); } +void CanvasContext::setHardwareBuffer(AHardwareBuffer* buffer) { + if (mHardwareBuffer) { + AHardwareBuffer_release(mHardwareBuffer); + mHardwareBuffer = nullptr; + } + + if (buffer) { + AHardwareBuffer_acquire(buffer); + mHardwareBuffer = buffer; + } + mRenderPipeline->setHardwareBuffer(mHardwareBuffer); +} + void CanvasContext::setSurface(ANativeWindow* window, bool enableTimeout) { ATRACE_CALL(); @@ -229,6 +248,8 @@ void CanvasContext::setupPipelineSurface() { // Order is important when new and old surfaces are the same, because old surface has // its frame stats disabled automatically. native_window_enable_frame_timestamps(mNativeSurface->getNativeWindow(), true); + native_window_set_scaling_mode(mNativeSurface->getNativeWindow(), + NATIVE_WINDOW_SCALING_MODE_FREEZE); } else { mRenderThread.removeFrameCallback(this); mGenerationID++; @@ -251,7 +272,8 @@ void CanvasContext::setStopped(bool stopped) { mGenerationID++; mRenderThread.removeFrameCallback(this); mRenderPipeline->onStop(); - } else if (mIsDirty && hasSurface()) { + mRenderThread.cacheManager().onContextStopped(this); + } else if (mIsDirty && hasOutputTarget()) { mRenderThread.postFrameCallback(this); } } @@ -277,9 +299,43 @@ void CanvasContext::setOpaque(bool opaque) { mOpaque = opaque; } -void CanvasContext::setColorMode(ColorMode mode) { - mRenderPipeline->setSurfaceColorProperties(mode); - setupPipelineSurface(); +float CanvasContext::setColorMode(ColorMode mode) { + if (mode != mColorMode) { + const bool isHdr = mode == ColorMode::Hdr || mode == ColorMode::Hdr10; + if (isHdr && !mRenderPipeline->supportsExtendedRangeHdr()) { + mode = ColorMode::WideColorGamut; + } + mColorMode = mode; + mRenderPipeline->setSurfaceColorProperties(mode); + setupPipelineSurface(); + } + switch (mColorMode) { + case ColorMode::Hdr: + return Properties::maxHdrHeadroomOn8bit; + case ColorMode::Hdr10: + return 10.f; + default: + return 1.f; + } +} + +float CanvasContext::targetSdrHdrRatio() const { + if (mColorMode == ColorMode::Hdr || mColorMode == ColorMode::Hdr10) { + return mTargetSdrHdrRatio; + } else { + return 1.f; + } +} + +void CanvasContext::setTargetSdrHdrRatio(float ratio) { + if (mTargetSdrHdrRatio == ratio) return; + + mTargetSdrHdrRatio = ratio; + mRenderPipeline->setTargetSdrHdrRatio(ratio); + // We don't actually but we need to behave as if we do. Specifically we need to ensure + // all buffers in the swapchain are fully re-rendered as any partial updates to them will + // result in mixed target white points which looks really bad & flickery + mHaveNewSurface = true; } bool CanvasContext::makeCurrent() { @@ -384,7 +440,7 @@ void CanvasContext::prepareTree(TreeInfo& info, int64_t* uiFrameInfo, int64_t sy mIsDirty = true; - if (CC_UNLIKELY(!hasSurface())) { + if (CC_UNLIKELY(!hasOutputTarget())) { mCurrentFrameInfo->addFlag(FrameInfoFlags::SkippedFrame); info.out.canDrawThisFrame = false; return; @@ -461,7 +517,6 @@ void CanvasContext::prepareTree(TreeInfo& info, int64_t* uiFrameInfo, int64_t sy } void CanvasContext::stopDrawing() { - cleanupResources(); mRenderThread.removeFrameCallback(this); mAnimationContext->pauseAnimators(); mGenerationID++; @@ -470,18 +525,33 @@ void CanvasContext::stopDrawing() { void CanvasContext::notifyFramePending() { ATRACE_CALL(); mRenderThread.pushBackFrameCallback(this); + sendLoadResetHint(); +} + +Frame CanvasContext::getFrame() { + if (mHardwareBuffer != nullptr) { + return {mBufferParams.getLogicalWidth(), mBufferParams.getLogicalHeight(), 0}; + } else { + return mRenderPipeline->getFrame(); + } } -nsecs_t CanvasContext::draw() { +void CanvasContext::draw() { if (auto grContext = getGrContext()) { if (grContext->abandoned()) { LOG_ALWAYS_FATAL("GrContext is abandoned/device lost at start of CanvasContext::draw"); - return 0; + return; } } SkRect dirty; mDamageAccumulator.finish(&dirty); + // reset syncDelayDuration each time we draw + nsecs_t syncDelayDuration = mSyncDelayDuration; + nsecs_t idleDuration = mIdleDuration; + mSyncDelayDuration = 0; + mIdleDuration = 0; + if (!Properties::isDrawingEnabled() || (dirty.isEmpty() && Properties::skipEmptyFrames && !surfaceRequiresRedraw())) { mCurrentFrameInfo->addFlag(FrameInfoFlags::SkippedFrame); @@ -498,7 +568,7 @@ nsecs_t CanvasContext::draw() { std::invoke(func, false /* didProduceBuffer */); } mFrameCommitCallbacks.clear(); - return 0; + return; } ScopedActiveContext activeContext(this); @@ -507,14 +577,25 @@ nsecs_t CanvasContext::draw() { mCurrentFrameInfo->markIssueDrawCommandsStart(); - Frame frame = mRenderPipeline->getFrame(); + Frame frame = getFrame(); + SkRect windowDirty = computeDirtyRect(frame, &dirty); 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()), mBufferParams); + } uint64_t frameCompleteNr = getFrameNumber(); @@ -527,12 +608,14 @@ nsecs_t CanvasContext::draw() { const auto inputEventId = static_cast<int32_t>(mCurrentFrameInfo->get(FrameInfoIndex::InputEventId)); native_window_set_frame_timeline_info( - mNativeSurface->getNativeWindow(), vsyncId, inputEventId, + mNativeSurface->getNativeWindow(), frameCompleteNr, vsyncId, inputEventId, mCurrentFrameInfo->get(FrameInfoIndex::FrameStartTime)); } } bool requireSwap = false; + bool didDraw = false; + int error = OK; bool didSwap = mRenderPipeline->swapBuffers(frame, drawResult.success, windowDirty, mCurrentFrameInfo, &requireSwap); @@ -543,7 +626,7 @@ nsecs_t CanvasContext::draw() { mIsDirty = false; if (requireSwap) { - bool didDraw = true; + didDraw = true; // Handle any swapchain errors error = mNativeSurface->getAndClearError(); if (error == TIMED_OUT) { @@ -638,23 +721,25 @@ nsecs_t CanvasContext::draw() { } } - cleanupResources(); - mRenderThread.cacheManager().onFrameCompleted(); - return mCurrentFrameInfo->get(FrameInfoIndex::DequeueBufferDuration); -} + int64_t intendedVsync = mCurrentFrameInfo->get(FrameInfoIndex::IntendedVsync); + int64_t frameDeadline = mCurrentFrameInfo->get(FrameInfoIndex::FrameDeadline); + int64_t dequeueBufferDuration = mCurrentFrameInfo->get(FrameInfoIndex::DequeueBufferDuration); + + mHintSessionWrapper.updateTargetWorkDuration(frameDeadline - intendedVsync); -void CanvasContext::cleanupResources() { - auto& tracker = mJankTracker.frames(); - auto size = tracker.size(); - auto capacity = tracker.capacity(); - if (size == capacity) { - nsecs_t nowNanos = systemTime(SYSTEM_TIME_MONOTONIC); - nsecs_t frameCompleteNanos = - tracker[0].get(FrameInfoIndex::FrameCompleted); - nsecs_t frameDiffNanos = nowNanos - frameCompleteNanos; - nsecs_t cleanupMillis = ns2ms(std::max(frameDiffNanos, 10_s)); - mRenderThread.cacheManager().performDeferredCleanup(cleanupMillis); + if (didDraw) { + int64_t frameStartTime = mCurrentFrameInfo->get(FrameInfoIndex::FrameStartTime); + int64_t frameDuration = systemTime(SYSTEM_TIME_MONOTONIC) - frameStartTime; + int64_t actualDuration = frameDuration - + (std::min(syncDelayDuration, mLastDequeueBufferDuration)) - + dequeueBufferDuration - idleDuration; + mHintSessionWrapper.reportActualWorkDuration(actualDuration); } + + mLastDequeueBufferDuration = dequeueBufferDuration; + + mRenderThread.cacheManager().onFrameCompleted(); + return; } void CanvasContext::reportMetricsWithPresentTime() { @@ -754,11 +839,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); } @@ -767,6 +852,8 @@ void CanvasContext::onSurfaceStatsAvailable(void* context, int32_t surfaceContro // Called by choreographer to do an RT-driven animation void CanvasContext::doFrame() { if (!mRenderPipeline->isSurfaceReady()) return; + mIdleDuration = + systemTime(SYSTEM_TIME_MONOTONIC) - mRenderThread.timeLord().computeFrameTimeNanos(); prepareAndDraw(nullptr); } @@ -780,6 +867,7 @@ SkISize CanvasContext::getNextFrameSize() const { SkISize size; size.fWidth = ANativeWindow_getWidth(anw); size.fHeight = ANativeWindow_getHeight(anw); + mRenderThread.cacheManager().notifyNextFrameSize(size.fWidth, size.fHeight); return size; } @@ -858,18 +946,6 @@ void CanvasContext::destroyHardwareResources() { } } -void CanvasContext::trimMemory(RenderThread& thread, int level) { - ATRACE_CALL(); - if (!thread.getGrContext()) return; - ATRACE_CALL(); - if (level >= TRIM_MEMORY_COMPLETE) { - thread.cacheManager().trimMemory(CacheManager::TrimMemoryMode::Complete); - thread.destroyRenderingContext(); - } else if (level >= TRIM_MEMORY_UI_HIDDEN) { - thread.cacheManager().trimMemory(CacheManager::TrimMemoryMode::UiHidden); - } -} - DeferredLayerUpdater* CanvasContext::createTextureLayer() { return mRenderPipeline->createTextureLayer(); } @@ -986,6 +1062,22 @@ void CanvasContext::prepareSurfaceControlForWebview() { } } +void CanvasContext::sendLoadResetHint() { + mHintSessionWrapper.sendLoadResetHint(); +} + +void CanvasContext::sendLoadIncreaseHint() { + mHintSessionWrapper.sendLoadIncreaseHint(); +} + +void CanvasContext::setSyncDelayDuration(nsecs_t duration) { + mSyncDelayDuration = duration; +} + +void CanvasContext::startHintSession() { + mHintSessionWrapper.init(); +} + } /* namespace renderthread */ } /* namespace uirenderer */ } /* namespace android */ diff --git a/libs/hwui/renderthread/CanvasContext.h b/libs/hwui/renderthread/CanvasContext.h index 951ee216ce35..3f2533959c20 100644 --- a/libs/hwui/renderthread/CanvasContext.h +++ b/libs/hwui/renderthread/CanvasContext.h @@ -16,10 +16,26 @@ #pragma once +#include <SkBitmap.h> +#include <SkRect.h> +#include <SkSize.h> +#include <cutils/compiler.h> +#include <utils/Functor.h> +#include <utils/Mutex.h> + +#include <functional> +#include <future> +#include <set> +#include <string> +#include <utility> +#include <vector> + +#include "ColorMode.h" #include "DamageAccumulator.h" #include "FrameInfo.h" #include "FrameInfoVisualizer.h" #include "FrameMetricsReporter.h" +#include "HintSessionWrapper.h" #include "IContextFactory.h" #include "IRenderPipeline.h" #include "JankTracker.h" @@ -30,21 +46,6 @@ #include "renderthread/RenderTask.h" #include "renderthread/RenderThread.h" #include "utils/RingBuffer.h" -#include "ColorMode.h" - -#include <SkBitmap.h> -#include <SkRect.h> -#include <SkSize.h> -#include <cutils/compiler.h> -#include <utils/Functor.h> -#include <utils/Mutex.h> - -#include <functional> -#include <future> -#include <set> -#include <string> -#include <utility> -#include <vector> namespace android { namespace uirenderer { @@ -66,7 +67,8 @@ class Frame; class CanvasContext : public IFrameCallback { public: static CanvasContext* create(RenderThread& thread, bool translucent, RenderNode* rootRenderNode, - IContextFactory* contextFactory); + IContextFactory* contextFactory, pid_t uiThreadId, + pid_t renderThreadId); virtual ~CanvasContext(); /** @@ -123,21 +125,25 @@ public: // Won't take effect until next EGLSurface creation void setSwapBehavior(SwapBehavior swapBehavior); + void setHardwareBuffer(AHardwareBuffer* buffer); void setSurface(ANativeWindow* window, bool enableTimeout = true); void setSurfaceControl(ASurfaceControl* surfaceControl); bool pauseSurface(); void setStopped(bool stopped); - bool hasSurface() const { return mNativeSurface.get(); } + bool isStopped() { return mStopped || !hasOutputTarget(); } + bool hasOutputTarget() const { return mNativeSurface.get() || mHardwareBuffer; } void allocateBuffers(); void setLightAlpha(uint8_t ambientShadowAlpha, uint8_t spotShadowAlpha); void setLightGeometry(const Vector3& lightCenter, float lightRadius); void setOpaque(bool opaque); - void setColorMode(ColorMode mode); + float setColorMode(ColorMode mode); + float targetSdrHdrRatio() const; + void setTargetSdrHdrRatio(float ratio); bool makeCurrent(); void prepareTree(TreeInfo& info, int64_t* uiFrameInfo, int64_t syncQueued, RenderNode* target); // Returns the DequeueBufferDuration. - nsecs_t draw(); + void draw(); void destroy(); // IFrameCallback, Choreographer-driven frame callback entry point @@ -148,7 +154,6 @@ public: void markLayerInUse(RenderNode* node); void destroyHardwareResources(); - static void trimMemory(RenderThread& thread, int level); DeferredLayerUpdater* createTextureLayer(); @@ -204,6 +209,10 @@ public: mASurfaceTransactionCallback = callback; } + void setHardwareBufferRenderParams(const HardwareBufferRenderParams& params) { + mBufferParams = params; + } + bool mergeTransaction(ASurfaceTransaction* transaction, ASurfaceControl* control); void setPrepareSurfaceControlForWebviewCallback(const std::function<void()>& callback) { @@ -214,9 +223,18 @@ public: static CanvasContext* getActiveContext(); + void sendLoadResetHint(); + + void sendLoadIncreaseHint(); + + void setSyncDelayDuration(nsecs_t duration); + + void startHintSession(); + private: CanvasContext(RenderThread& thread, bool translucent, RenderNode* rootRenderNode, - IContextFactory* contextFactory, std::unique_ptr<IRenderPipeline> renderPipeline); + IContextFactory* contextFactory, std::unique_ptr<IRenderPipeline> renderPipeline, + pid_t uiThreadId, pid_t renderThreadId); friend class RegisterFrameCallbackTask; // TODO: Replace with something better for layer & other GL object @@ -246,11 +264,16 @@ private: FrameInfo* getFrameInfoFromLast4(uint64_t frameNumber, uint32_t surfaceControlId); + Frame getFrame(); + // The same type as Frame.mWidth and Frame.mHeight int32_t mLastFrameWidth = 0; int32_t mLastFrameHeight = 0; RenderThread& mRenderThread; + + AHardwareBuffer* mHardwareBuffer = nullptr; + HardwareBufferRenderParams mBufferParams; std::unique_ptr<ReliableSurface> mNativeSurface; // The SurfaceControl reference is passed from ViewRootImpl, can be set to // NULL to remove the reference @@ -331,7 +354,13 @@ private: std::function<bool(int64_t, int64_t, int64_t)> mASurfaceTransactionCallback; std::function<void()> mPrepareSurfaceControlForWebviewCallback; - void cleanupResources(); + HintSessionWrapper mHintSessionWrapper; + nsecs_t mLastDequeueBufferDuration = 0; + nsecs_t mSyncDelayDuration = 0; + nsecs_t mIdleDuration = 0; + + ColorMode mColorMode = ColorMode::Default; + float mTargetSdrHdrRatio = 1.f; }; } /* namespace renderthread */ diff --git a/libs/hwui/renderthread/DrawFrameTask.cpp b/libs/hwui/renderthread/DrawFrameTask.cpp index 59c914f0198c..fab2f46e91c3 100644 --- a/libs/hwui/renderthread/DrawFrameTask.cpp +++ b/libs/hwui/renderthread/DrawFrameTask.cpp @@ -16,9 +16,9 @@ #include "DrawFrameTask.h" -#include <dlfcn.h> #include <gui/TraceUtils.h> #include <utils/Log.h> + #include <algorithm> #include "../DeferredLayerUpdater.h" @@ -26,64 +26,13 @@ #include "../Properties.h" #include "../RenderNode.h" #include "CanvasContext.h" +#include "HardwareBufferRenderParams.h" #include "RenderThread.h" -#include "thread/CommonPool.h" namespace android { namespace uirenderer { namespace renderthread { -namespace { - -typedef APerformanceHintManager* (*APH_getManager)(); -typedef APerformanceHintSession* (*APH_createSession)(APerformanceHintManager*, const int32_t*, - size_t, int64_t); -typedef void (*APH_updateTargetWorkDuration)(APerformanceHintSession*, int64_t); -typedef void (*APH_reportActualWorkDuration)(APerformanceHintSession*, int64_t); -typedef void (*APH_closeSession)(APerformanceHintSession* session); - -bool gAPerformanceHintBindingInitialized = false; -APH_getManager gAPH_getManagerFn = nullptr; -APH_createSession gAPH_createSessionFn = nullptr; -APH_updateTargetWorkDuration gAPH_updateTargetWorkDurationFn = nullptr; -APH_reportActualWorkDuration gAPH_reportActualWorkDurationFn = nullptr; -APH_closeSession gAPH_closeSessionFn = nullptr; - -void ensureAPerformanceHintBindingInitialized() { - if (gAPerformanceHintBindingInitialized) return; - - void* handle_ = dlopen("libandroid.so", RTLD_NOW | RTLD_NODELETE); - LOG_ALWAYS_FATAL_IF(handle_ == nullptr, "Failed to dlopen libandroid.so!"); - - gAPH_getManagerFn = (APH_getManager)dlsym(handle_, "APerformanceHint_getManager"); - LOG_ALWAYS_FATAL_IF(gAPH_getManagerFn == nullptr, - "Failed to find required symbol APerformanceHint_getManager!"); - - gAPH_createSessionFn = (APH_createSession)dlsym(handle_, "APerformanceHint_createSession"); - LOG_ALWAYS_FATAL_IF(gAPH_createSessionFn == nullptr, - "Failed to find required symbol APerformanceHint_createSession!"); - - gAPH_updateTargetWorkDurationFn = (APH_updateTargetWorkDuration)dlsym( - handle_, "APerformanceHint_updateTargetWorkDuration"); - LOG_ALWAYS_FATAL_IF( - gAPH_updateTargetWorkDurationFn == nullptr, - "Failed to find required symbol APerformanceHint_updateTargetWorkDuration!"); - - gAPH_reportActualWorkDurationFn = (APH_reportActualWorkDuration)dlsym( - handle_, "APerformanceHint_reportActualWorkDuration"); - LOG_ALWAYS_FATAL_IF( - gAPH_reportActualWorkDurationFn == nullptr, - "Failed to find required symbol APerformanceHint_reportActualWorkDuration!"); - - gAPH_closeSessionFn = (APH_closeSession)dlsym(handle_, "APerformanceHint_closeSession"); - LOG_ALWAYS_FATAL_IF(gAPH_closeSessionFn == nullptr, - "Failed to find required symbol APerformanceHint_closeSession!"); - - gAPerformanceHintBindingInitialized = true; -} - -} // namespace - DrawFrameTask::DrawFrameTask() : mRenderThread(nullptr) , mContext(nullptr) @@ -92,13 +41,11 @@ DrawFrameTask::DrawFrameTask() DrawFrameTask::~DrawFrameTask() {} -void DrawFrameTask::setContext(RenderThread* thread, CanvasContext* context, RenderNode* targetNode, - int32_t uiThreadId, int32_t renderThreadId) { +void DrawFrameTask::setContext(RenderThread* thread, CanvasContext* context, + RenderNode* targetNode) { mRenderThread = thread; mContext = context; mTargetNode = targetNode; - mUiThreadId = uiThreadId; - mRenderThreadId = renderThreadId; } void DrawFrameTask::pushLayerUpdate(DeferredLayerUpdater* layer) { @@ -142,8 +89,13 @@ void DrawFrameTask::postAndWait() { void DrawFrameTask::run() { const int64_t vsyncId = mFrameInfo[static_cast<int>(FrameInfoIndex::FrameTimelineVsyncId)]; ATRACE_FORMAT("DrawFrames %" PRId64, vsyncId); - nsecs_t syncDelayDuration = systemTime(SYSTEM_TIME_MONOTONIC) - mSyncQueued; + mContext->setSyncDelayDuration(systemTime(SYSTEM_TIME_MONOTONIC) - mSyncQueued); + mContext->setTargetSdrHdrRatio(mRenderSdrHdrRatio); + + auto hardwareBufferParams = mHardwareBufferParams; + mContext->setHardwareBufferRenderParams(hardwareBufferParams); + IRenderPipeline* pipeline = mContext->getRenderPipeline(); bool canUnblockUiThread; bool canDrawThisFrame; { @@ -166,9 +118,6 @@ void DrawFrameTask::run() { std::function<void()> frameCompleteCallback = std::move(mFrameCompleteCallback); mFrameCallback = nullptr; mFrameCompleteCallback = nullptr; - int64_t intendedVsync = mFrameInfo[static_cast<int>(FrameInfoIndex::IntendedVsync)]; - int64_t frameDeadline = mFrameInfo[static_cast<int>(FrameInfoIndex::FrameDeadline)]; - int64_t frameStartTime = mFrameInfo[static_cast<int>(FrameInfoIndex::FrameStartTime)]; // From this point on anything in "this" is *UNSAFE TO ACCESS* if (canUnblockUiThread) { @@ -179,16 +128,15 @@ void DrawFrameTask::run() { if (CC_UNLIKELY(frameCallback)) { context->enqueueFrameWork([frameCallback, context, syncResult = mSyncResult, frameNr = context->getFrameNumber()]() { - auto frameCommitCallback = std::move(frameCallback(syncResult, frameNr)); + auto frameCommitCallback = frameCallback(syncResult, frameNr); if (frameCommitCallback) { context->addFrameCommitListener(std::move(frameCommitCallback)); } }); } - nsecs_t dequeueBufferDuration = 0; if (CC_LIKELY(canDrawThisFrame)) { - dequeueBufferDuration = context->draw(); + context->draw(); } else { // Do a flush in case syncFrameState performed any texture uploads. Since we skipped // the draw() call, those uploads (or deletes) will end up sitting in the queue. @@ -208,26 +156,10 @@ void DrawFrameTask::run() { unblockUiThread(); } - if (!mHintSessionWrapper) mHintSessionWrapper.emplace(mUiThreadId, mRenderThreadId); - constexpr int64_t kSanityCheckLowerBound = 100000; // 0.1ms - constexpr int64_t kSanityCheckUpperBound = 10000000000; // 10s - int64_t targetWorkDuration = frameDeadline - intendedVsync; - targetWorkDuration = targetWorkDuration * Properties::targetCpuTimePercentage / 100; - if (targetWorkDuration > kSanityCheckLowerBound && - targetWorkDuration < kSanityCheckUpperBound && - targetWorkDuration != mLastTargetWorkDuration) { - mLastTargetWorkDuration = targetWorkDuration; - mHintSessionWrapper->updateTargetWorkDuration(targetWorkDuration); + if (pipeline->hasHardwareBuffer()) { + auto fence = pipeline->flush(); + hardwareBufferParams.invokeRenderCallback(std::move(fence), 0); } - int64_t frameDuration = systemTime(SYSTEM_TIME_MONOTONIC) - frameStartTime; - int64_t actualDuration = frameDuration - - (std::min(syncDelayDuration, mLastDequeueBufferDuration)) - - dequeueBufferDuration; - if (actualDuration > kSanityCheckLowerBound && actualDuration < kSanityCheckUpperBound) { - mHintSessionWrapper->reportActualWorkDuration(actualDuration); - } - - mLastDequeueBufferDuration = dequeueBufferDuration; } bool DrawFrameTask::syncFrameState(TreeInfo& info) { @@ -243,7 +175,9 @@ bool DrawFrameTask::syncFrameState(TreeInfo& info) { mContext->unpinImages(); for (size_t i = 0; i < mLayers.size(); i++) { - mLayers[i]->apply(); + if (mLayers[i]) { + mLayers[i]->apply(); + } } mLayers.clear(); mContext->setContentDrawBounds(mContentDrawBounds); @@ -251,8 +185,9 @@ bool DrawFrameTask::syncFrameState(TreeInfo& info) { // This is after the prepareTree so that any pending operations // (RenderNode tree state, prefetched layers, etc...) will be flushed. - if (CC_UNLIKELY(!mContext->hasSurface() || !canDraw)) { - if (!mContext->hasSurface()) { + bool hasTarget = mContext->hasOutputTarget(); + if (CC_UNLIKELY(!hasTarget || !canDraw)) { + if (!hasTarget) { mSyncResult |= SyncResult::LostSurfaceRewardIfFound; } else { // If we have a surface but can't draw we must be stopped @@ -278,44 +213,6 @@ void DrawFrameTask::unblockUiThread() { mSignal.signal(); } -DrawFrameTask::HintSessionWrapper::HintSessionWrapper(int32_t uiThreadId, int32_t renderThreadId) { - if (!Properties::useHintManager) return; - if (uiThreadId < 0 || renderThreadId < 0) return; - - ensureAPerformanceHintBindingInitialized(); - - APerformanceHintManager* manager = gAPH_getManagerFn(); - if (!manager) return; - - std::vector<int32_t> tids = CommonPool::getThreadIds(); - tids.push_back(uiThreadId); - tids.push_back(renderThreadId); - - // DrawFrameTask code will always set a target duration before reporting actual durations. - // So this is just a placeholder value that's never used. - int64_t dummyTargetDurationNanos = 16666667; - mHintSession = - gAPH_createSessionFn(manager, tids.data(), tids.size(), dummyTargetDurationNanos); -} - -DrawFrameTask::HintSessionWrapper::~HintSessionWrapper() { - if (mHintSession) { - gAPH_closeSessionFn(mHintSession); - } -} - -void DrawFrameTask::HintSessionWrapper::updateTargetWorkDuration(long targetDurationNanos) { - if (mHintSession) { - gAPH_updateTargetWorkDurationFn(mHintSession, targetDurationNanos); - } -} - -void DrawFrameTask::HintSessionWrapper::reportActualWorkDuration(long actualDurationNanos) { - if (mHintSession) { - gAPH_reportActualWorkDurationFn(mHintSession, actualDurationNanos); - } -} - } /* namespace renderthread */ } /* namespace uirenderer */ } /* namespace android */ diff --git a/libs/hwui/renderthread/DrawFrameTask.h b/libs/hwui/renderthread/DrawFrameTask.h index d6fc292d5900..4130d4abe09e 100644 --- a/libs/hwui/renderthread/DrawFrameTask.h +++ b/libs/hwui/renderthread/DrawFrameTask.h @@ -16,7 +16,6 @@ #ifndef DRAWFRAMETASK_H #define DRAWFRAMETASK_H -#include <android/performance_hint.h> #include <utils/Condition.h> #include <utils/Mutex.h> #include <utils/StrongPointer.h> @@ -28,8 +27,16 @@ #include "../Rect.h" #include "../TreeInfo.h" #include "RenderTask.h" +#include "SkColorSpace.h" +#include "SwapBehavior.h" +#include "utils/TimeUtils.h" +#ifdef __ANDROID__ // Layoutlib does not support hardware acceleration +#include <android/hardware_buffer.h> +#endif +#include "HardwareBufferRenderParams.h" namespace android { + namespace uirenderer { class DeferredLayerUpdater; @@ -61,8 +68,7 @@ public: DrawFrameTask(); virtual ~DrawFrameTask(); - void setContext(RenderThread* thread, CanvasContext* context, RenderNode* targetNode, - int32_t uiThreadId, int32_t renderThreadId); + void setContext(RenderThread* thread, CanvasContext* context, RenderNode* targetNode); void setContentDrawBounds(int left, int top, int right, int bottom) { mContentDrawBounds.set(left, top, right, bottom); } @@ -90,19 +96,13 @@ public: void forceDrawNextFrame() { mForceDrawFrame = true; } -private: - class HintSessionWrapper { - public: - HintSessionWrapper(int32_t uiThreadId, int32_t renderThreadId); - ~HintSessionWrapper(); - - void updateTargetWorkDuration(long targetDurationNanos); - void reportActualWorkDuration(long actualDurationNanos); + void setHardwareBufferRenderParams(const HardwareBufferRenderParams& params) { + mHardwareBufferParams = params; + } - private: - APerformanceHintSession* mHintSession = nullptr; - }; + void setRenderSdrHdrRatio(float ratio) { mRenderSdrHdrRatio = ratio; } +private: void postAndWait(); bool syncFrameState(TreeInfo& info); void unblockUiThread(); @@ -113,9 +113,8 @@ private: RenderThread* mRenderThread; CanvasContext* mContext; RenderNode* mTargetNode = nullptr; - int32_t mUiThreadId = -1; - int32_t mRenderThreadId = -1; Rect mContentDrawBounds; + float mRenderSdrHdrRatio = 1.f; /********************************************* * Single frame data @@ -127,14 +126,11 @@ private: int64_t mFrameInfo[UI_THREAD_FRAME_INFO_SIZE]; + HardwareBufferRenderParams mHardwareBufferParams; std::function<std::function<void(bool)>(int32_t, int64_t)> mFrameCallback; std::function<void(bool)> mFrameCommitCallback; std::function<void()> mFrameCompleteCallback; - nsecs_t mLastDequeueBufferDuration = 0; - nsecs_t mLastTargetWorkDuration = 0; - std::optional<HintSessionWrapper> mHintSessionWrapper; - bool mForceDrawFrame = false; }; diff --git a/libs/hwui/renderthread/EglManager.cpp b/libs/hwui/renderthread/EglManager.cpp index 02257db9df6a..4fb114b71bf5 100644 --- a/libs/hwui/renderthread/EglManager.cpp +++ b/libs/hwui/renderthread/EglManager.cpp @@ -450,6 +450,12 @@ Result<EGLSurface, EGLint> EglManager::createSurface(EGLNativeWindowType window, case ColorMode::Default: attribs[1] = EGL_GL_COLORSPACE_LINEAR_KHR; break; + // Extended Range HDR requires being able to manipulate the dataspace in ways + // we cannot easily do while going through EGLSurface. Given this requires + // composer3 support, just treat HDR as equivalent to wide color gamut if + // the GLES path is still being hit + case ColorMode::Hdr: + case ColorMode::Hdr10: case ColorMode::WideColorGamut: { skcms_Matrix3x3 colorGamut; LOG_ALWAYS_FATAL_IF(!colorSpace->toXYZD50(&colorGamut), @@ -466,14 +472,6 @@ Result<EGLSurface, EGLint> EglManager::createSurface(EGLNativeWindowType window, } break; } - case ColorMode::Hdr: - config = mEglConfigF16; - attribs[1] = EGL_GL_COLORSPACE_BT2020_PQ_EXT; - break; - case ColorMode::Hdr10: - config = mEglConfig1010102; - attribs[1] = EGL_GL_COLORSPACE_BT2020_PQ_EXT; - break; case ColorMode::A8: LOG_ALWAYS_FATAL("Unreachable: A8 doesn't use a color space"); break; 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/HardwareBufferRenderParams.h b/libs/hwui/renderthread/HardwareBufferRenderParams.h new file mode 100644 index 000000000000..8c942d0fa102 --- /dev/null +++ b/libs/hwui/renderthread/HardwareBufferRenderParams.h @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 HARDWAREBUFFERRENDERER_H_ +#define HARDWAREBUFFERRENDERER_H_ + +#include <android-base/unique_fd.h> +#include <android/hardware_buffer.h> + +#include "SkColorSpace.h" +#include "SkMatrix.h" +#include "SkSurface.h" + +namespace android { +namespace uirenderer { +namespace renderthread { + +using namespace android::uirenderer::renderthread; + +using RenderCallback = std::function<void(android::base::unique_fd&&, int)>; + +class RenderProxy; + +class HardwareBufferRenderParams { +public: + HardwareBufferRenderParams() = default; + HardwareBufferRenderParams(int32_t logicalWidth, int32_t logicalHeight, + const SkMatrix& transform, const sk_sp<SkColorSpace>& colorSpace, + RenderCallback&& callback) + : mLogicalWidth(logicalWidth) + , mLogicalHeight(logicalHeight) + , mTransform(transform) + , mColorSpace(colorSpace) + , mRenderCallback(std::move(callback)) {} + const SkMatrix& getTransform() const { return mTransform; } + sk_sp<SkColorSpace> getColorSpace() const { return mColorSpace; } + + void invokeRenderCallback(android::base::unique_fd&& fenceFd, int status) { + if (mRenderCallback) { + std::invoke(mRenderCallback, std::move(fenceFd), status); + } + } + + int32_t getLogicalWidth() { return mLogicalWidth; } + int32_t getLogicalHeight() { return mLogicalHeight; } + +private: + int32_t mLogicalWidth; + int32_t mLogicalHeight; + SkMatrix mTransform = SkMatrix::I(); + sk_sp<SkColorSpace> mColorSpace = SkColorSpace::MakeSRGB(); + RenderCallback mRenderCallback = nullptr; +}; + +} // namespace renderthread +} // namespace uirenderer +} // namespace android +#endif // HARDWAREBUFFERRENDERER_H_ diff --git a/libs/hwui/renderthread/HintSessionWrapper.cpp b/libs/hwui/renderthread/HintSessionWrapper.cpp new file mode 100644 index 000000000000..8c9f65fac10c --- /dev/null +++ b/libs/hwui/renderthread/HintSessionWrapper.cpp @@ -0,0 +1,164 @@ +/* + * 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. + */ + +#include "HintSessionWrapper.h" + +#include <dlfcn.h> +#include <private/performance_hint_private.h> +#include <utils/Log.h> + +#include <vector> + +#include "../Properties.h" +#include "thread/CommonPool.h" + +namespace android { +namespace uirenderer { +namespace renderthread { + +namespace { + +typedef APerformanceHintManager* (*APH_getManager)(); +typedef APerformanceHintSession* (*APH_createSession)(APerformanceHintManager*, const int32_t*, + size_t, int64_t); +typedef void (*APH_closeSession)(APerformanceHintSession* session); +typedef void (*APH_updateTargetWorkDuration)(APerformanceHintSession*, int64_t); +typedef void (*APH_reportActualWorkDuration)(APerformanceHintSession*, int64_t); +typedef void (*APH_sendHint)(APerformanceHintSession* session, int32_t); + +bool gAPerformanceHintBindingInitialized = false; +APH_getManager gAPH_getManagerFn = nullptr; +APH_createSession gAPH_createSessionFn = nullptr; +APH_closeSession gAPH_closeSessionFn = nullptr; +APH_updateTargetWorkDuration gAPH_updateTargetWorkDurationFn = nullptr; +APH_reportActualWorkDuration gAPH_reportActualWorkDurationFn = nullptr; +APH_sendHint gAPH_sendHintFn = nullptr; + +void ensureAPerformanceHintBindingInitialized() { + if (gAPerformanceHintBindingInitialized) return; + + void* handle_ = dlopen("libandroid.so", RTLD_NOW | RTLD_NODELETE); + LOG_ALWAYS_FATAL_IF(handle_ == nullptr, "Failed to dlopen libandroid.so!"); + + gAPH_getManagerFn = (APH_getManager)dlsym(handle_, "APerformanceHint_getManager"); + LOG_ALWAYS_FATAL_IF(gAPH_getManagerFn == nullptr, + "Failed to find required symbol APerformanceHint_getManager!"); + + gAPH_createSessionFn = (APH_createSession)dlsym(handle_, "APerformanceHint_createSession"); + LOG_ALWAYS_FATAL_IF(gAPH_createSessionFn == nullptr, + "Failed to find required symbol APerformanceHint_createSession!"); + + gAPH_closeSessionFn = (APH_closeSession)dlsym(handle_, "APerformanceHint_closeSession"); + LOG_ALWAYS_FATAL_IF(gAPH_closeSessionFn == nullptr, + "Failed to find required symbol APerformanceHint_closeSession!"); + + gAPH_updateTargetWorkDurationFn = (APH_updateTargetWorkDuration)dlsym( + handle_, "APerformanceHint_updateTargetWorkDuration"); + LOG_ALWAYS_FATAL_IF( + gAPH_updateTargetWorkDurationFn == nullptr, + "Failed to find required symbol APerformanceHint_updateTargetWorkDuration!"); + + gAPH_reportActualWorkDurationFn = (APH_reportActualWorkDuration)dlsym( + handle_, "APerformanceHint_reportActualWorkDuration"); + LOG_ALWAYS_FATAL_IF( + gAPH_reportActualWorkDurationFn == nullptr, + "Failed to find required symbol APerformanceHint_reportActualWorkDuration!"); + + gAPH_sendHintFn = (APH_sendHint)dlsym(handle_, "APerformanceHint_sendHint"); + LOG_ALWAYS_FATAL_IF(gAPH_sendHintFn == nullptr, + "Failed to find required symbol APerformanceHint_sendHint!"); + + gAPerformanceHintBindingInitialized = true; +} + +} // namespace + +HintSessionWrapper::HintSessionWrapper(pid_t uiThreadId, pid_t renderThreadId) + : mUiThreadId(uiThreadId), mRenderThreadId(renderThreadId) {} + +HintSessionWrapper::~HintSessionWrapper() { + if (mHintSession) { + gAPH_closeSessionFn(mHintSession); + } +} + +bool HintSessionWrapper::init() { + // If it already exists, broke last time we tried this, shouldn't be running, or + // has bad argument values, don't even bother + if (mHintSession != nullptr || !mSessionValid || !Properties::useHintManager || + !Properties::isDrawingEnabled() || mUiThreadId < 0 || mRenderThreadId < 0) { + return false; + } + + // Assume that if we return before the end, it broke + mSessionValid = false; + + ensureAPerformanceHintBindingInitialized(); + + APerformanceHintManager* manager = gAPH_getManagerFn(); + if (!manager) return false; + + std::vector<pid_t> tids = CommonPool::getThreadIds(); + tids.push_back(mUiThreadId); + tids.push_back(mRenderThreadId); + + // Use a placeholder target value to initialize, + // this will always be replaced elsewhere before it gets used + int64_t defaultTargetDurationNanos = 16666667; + mHintSession = + gAPH_createSessionFn(manager, tids.data(), tids.size(), defaultTargetDurationNanos); + + mSessionValid = !!mHintSession; + return mSessionValid; +} + +void HintSessionWrapper::updateTargetWorkDuration(long targetWorkDurationNanos) { + if (mHintSession == nullptr) return; + targetWorkDurationNanos = targetWorkDurationNanos * Properties::targetCpuTimePercentage / 100; + if (targetWorkDurationNanos != mLastTargetWorkDuration && + targetWorkDurationNanos > kSanityCheckLowerBound && + targetWorkDurationNanos < kSanityCheckUpperBound) { + mLastTargetWorkDuration = targetWorkDurationNanos; + gAPH_updateTargetWorkDurationFn(mHintSession, targetWorkDurationNanos); + } + mLastFrameNotification = systemTime(); +} + +void HintSessionWrapper::reportActualWorkDuration(long actualDurationNanos) { + if (mHintSession == nullptr) return; + if (actualDurationNanos > kSanityCheckLowerBound && + actualDurationNanos < kSanityCheckUpperBound) { + gAPH_reportActualWorkDurationFn(mHintSession, actualDurationNanos); + } +} + +void HintSessionWrapper::sendLoadResetHint() { + if (mHintSession == nullptr) return; + nsecs_t now = systemTime(); + if (now - mLastFrameNotification > kResetHintTimeout) { + gAPH_sendHintFn(mHintSession, static_cast<int>(SessionHint::CPU_LOAD_RESET)); + } + mLastFrameNotification = now; +} + +void HintSessionWrapper::sendLoadIncreaseHint() { + if (mHintSession == nullptr) return; + gAPH_sendHintFn(mHintSession, static_cast<int>(SessionHint::CPU_LOAD_UP)); +} + +} /* namespace renderthread */ +} /* namespace uirenderer */ +} /* namespace android */ diff --git a/libs/hwui/renderthread/HintSessionWrapper.h b/libs/hwui/renderthread/HintSessionWrapper.h new file mode 100644 index 000000000000..f2f1298c1eec --- /dev/null +++ b/libs/hwui/renderthread/HintSessionWrapper.h @@ -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. + */ + +#pragma once + +#include <android/performance_hint.h> + +#include "utils/TimeUtils.h" + +namespace android { +namespace uirenderer { + +namespace renderthread { + +class HintSessionWrapper { +public: + HintSessionWrapper(pid_t uiThreadId, pid_t renderThreadId); + ~HintSessionWrapper(); + + void updateTargetWorkDuration(long targetDurationNanos); + void reportActualWorkDuration(long actualDurationNanos); + void sendLoadResetHint(); + void sendLoadIncreaseHint(); + bool init(); + +private: + APerformanceHintSession* mHintSession = nullptr; + + nsecs_t mLastFrameNotification = 0; + nsecs_t mLastTargetWorkDuration = 0; + + pid_t mUiThreadId; + pid_t mRenderThreadId; + + bool mSessionValid = true; + + static constexpr nsecs_t kResetHintTimeout = 100_ms; + static constexpr int64_t kSanityCheckLowerBound = 100_us; + static constexpr int64_t kSanityCheckUpperBound = 10_s; +}; + +} /* namespace renderthread */ +} /* namespace uirenderer */ +} /* namespace android */ diff --git a/libs/hwui/renderthread/IRenderPipeline.h b/libs/hwui/renderthread/IRenderPipeline.h index ef58bc553c23..c68fcdfc76f2 100644 --- a/libs/hwui/renderthread/IRenderPipeline.h +++ b/libs/hwui/renderthread/IRenderPipeline.h @@ -16,16 +16,19 @@ #pragma once +#include <SkColorSpace.h> +#include <SkRect.h> +#include <android-base/unique_fd.h> +#include <utils/RefBase.h> + +#include "ColorMode.h" #include "DamageAccumulator.h" #include "FrameInfoVisualizer.h" +#include "HardwareBufferRenderParams.h" #include "LayerUpdateQueue.h" #include "Lighting.h" #include "SwapBehavior.h" #include "hwui/Bitmap.h" -#include "ColorMode.h" - -#include <SkRect.h> -#include <utils/RefBase.h> class GrDirectContext; @@ -63,10 +66,14 @@ public: const LightGeometry& lightGeometry, LayerUpdateQueue* layerUpdateQueue, const Rect& contentDrawBounds, bool opaque, const LightInfo& lightInfo, const std::vector<sp<RenderNode>>& renderNodes, - FrameInfoVisualizer* profiler) = 0; + FrameInfoVisualizer* profiler, + const HardwareBufferRenderParams& bufferParams) = 0; virtual bool swapBuffers(const Frame& frame, bool drew, const SkRect& screenDirty, FrameInfo* currentFrameInfo, bool* requireSwap) = 0; virtual DeferredLayerUpdater* createTextureLayer() = 0; + [[nodiscard]] virtual android::base::unique_fd flush() = 0; + virtual void setHardwareBuffer(AHardwareBuffer* hardwareBuffer) = 0; + virtual bool hasHardwareBuffer() = 0; virtual bool setSurface(ANativeWindow* window, SwapBehavior swapBehavior) = 0; virtual void onStop() = 0; virtual bool isSurfaceReady() = 0; @@ -88,6 +95,9 @@ public: virtual void setPictureCapturedCallback( const std::function<void(sk_sp<SkPicture>&&)>& callback) = 0; + virtual bool supportsExtendedRangeHdr() const { return false; } + virtual void setTargetSdrHdrRatio(float ratio) = 0; + virtual ~IRenderPipeline() {} }; diff --git a/libs/hwui/renderthread/RenderProxy.cpp b/libs/hwui/renderthread/RenderProxy.cpp index a44b498c81c1..31b4b203c670 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 { @@ -38,11 +42,17 @@ namespace renderthread { RenderProxy::RenderProxy(bool translucent, RenderNode* rootRenderNode, IContextFactory* contextFactory) : mRenderThread(RenderThread::getInstance()), mContext(nullptr) { - mContext = mRenderThread.queue().runSync([&]() -> CanvasContext* { - return CanvasContext::create(mRenderThread, translucent, rootRenderNode, contextFactory); + pid_t uiThreadId = pthread_gettid_np(pthread_self()); + pid_t renderThreadId = getRenderThreadTid(); + mContext = mRenderThread.queue().runSync([=, this]() -> CanvasContext* { + CanvasContext* context = CanvasContext::create(mRenderThread, translucent, rootRenderNode, + contextFactory, uiThreadId, renderThreadId); + if (context != nullptr) { + mRenderThread.queue().post([=] { context->startHintSession(); }); + } + return context; }); - mDrawFrameTask.setContext(&mRenderThread, mContext, rootRenderNode, - pthread_gettid_np(pthread_self()), getRenderThreadTid()); + mDrawFrameTask.setContext(&mRenderThread, mContext, rootRenderNode); } RenderProxy::~RenderProxy() { @@ -51,7 +61,7 @@ RenderProxy::~RenderProxy() { void RenderProxy::destroyContext() { if (mContext) { - mDrawFrameTask.setContext(nullptr, nullptr, nullptr, -1, -1); + mDrawFrameTask.setContext(nullptr, nullptr, nullptr); // This is also a fence as we need to be certain that there are no // outstanding mDrawFrame tasks posted before it is destroyed mRenderThread.queue().runSync([this]() { delete mContext; }); @@ -79,6 +89,18 @@ void RenderProxy::setName(const char* name) { mRenderThread.queue().runSync([this, name]() { mContext->setName(std::string(name)); }); } +void RenderProxy::setHardwareBuffer(AHardwareBuffer* buffer) { + if (buffer) { + AHardwareBuffer_acquire(buffer); + } + mRenderThread.queue().post([this, hardwareBuffer = buffer]() mutable { + mContext->setHardwareBuffer(hardwareBuffer); + if (hardwareBuffer) { + AHardwareBuffer_release(hardwareBuffer); + } + }); +} + void RenderProxy::setSurface(ANativeWindow* window, bool enableTimeout) { if (window) { ANativeWindow_acquire(window); } mRenderThread.queue().post([this, win = window, enableTimeout]() mutable { @@ -125,8 +147,20 @@ void RenderProxy::setOpaque(bool opaque) { mRenderThread.queue().post([=]() { mContext->setOpaque(opaque); }); } -void RenderProxy::setColorMode(ColorMode mode) { - mRenderThread.queue().post([=]() { mContext->setColorMode(mode); }); +float RenderProxy::setColorMode(ColorMode mode) { + // We only need to figure out what the renderer supports for HDR, otherwise this can stay + // an async call since we already know the return value + if (mode == ColorMode::Hdr || mode == ColorMode::Hdr10) { + return mRenderThread.queue().runSync( + [=]() -> float { return mContext->setColorMode(mode); }); + } else { + mRenderThread.queue().post([=]() { mContext->setColorMode(mode); }); + return 1.f; + } +} + +void RenderProxy::setRenderSdrHdrRatio(float ratio) { + mDrawFrameTask.setRenderSdrHdrRatio(ratio); } int64_t* RenderProxy::frameInfo() { @@ -192,7 +226,8 @@ void RenderProxy::trimMemory(int level) { // Avoid creating a RenderThread to do a trimMemory. if (RenderThread::hasInstance()) { RenderThread& thread = RenderThread::getInstance(); - thread.queue().post([&thread, level]() { CanvasContext::trimMemory(thread, level); }); + const auto trimLevel = static_cast<TrimLevel>(level); + thread.queue().post([&thread, trimLevel]() { thread.trimMemory(trimLevel); }); } } @@ -201,7 +236,7 @@ void RenderProxy::purgeCaches() { RenderThread& thread = RenderThread::getInstance(); thread.queue().post([&thread]() { if (thread.getGrContext()) { - thread.cacheManager().trimMemory(CacheManager::TrimMemoryMode::Complete); + thread.cacheManager().trimMemory(TrimLevel::COMPLETE); } }); } @@ -231,6 +266,14 @@ void RenderProxy::notifyFramePending() { mRenderThread.queue().post([this]() { mContext->notifyFramePending(); }); } +void RenderProxy::notifyCallbackPending() { + mRenderThread.queue().post([this]() { mContext->sendLoadResetHint(); }); +} + +void RenderProxy::notifyExpensiveFrame() { + mRenderThread.queue().post([this]() { mContext->sendLoadIncreaseHint(); }); +} + void RenderProxy::dumpProfileInfo(int fd, int dumpFlags) { mRenderThread.queue().runSync([&]() { std::lock_guard lock(mRenderThread.getJankDataMutex()); @@ -313,6 +356,10 @@ void RenderProxy::setContentDrawBounds(int left, int top, int right, int bottom) mDrawFrameTask.setContentDrawBounds(left, top, right, bottom); } +void RenderProxy::setHardwareBufferRenderParams(const HardwareBufferRenderParams& params) { + mDrawFrameTask.setHardwareBufferRenderParams(params); +} + void RenderProxy::setPictureCapturedCallback( const std::function<void(sk_sp<SkPicture>&&)>& callback) { mRenderThread.queue().post( @@ -360,12 +407,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..82072a6e2499 100644 --- a/libs/hwui/renderthread/RenderProxy.h +++ b/libs/hwui/renderthread/RenderProxy.h @@ -17,19 +17,25 @@ #ifndef RENDERPROXY_H_ #define RENDERPROXY_H_ -#include <SkBitmap.h> +#include <SkRefCnt.h> +#include <android/hardware_buffer.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; @@ -71,7 +77,7 @@ public: void setSwapBehavior(SwapBehavior swapBehavior); bool loadSystemProperties(); void setName(const char* name); - + void setHardwareBuffer(AHardwareBuffer* buffer); void setSurface(ANativeWindow* window, bool enableTimeout = true); void setSurfaceControl(ASurfaceControl* surfaceControl); void allocateBuffers(); @@ -79,8 +85,10 @@ public: void setStopped(bool stopped); void setLightAlpha(uint8_t ambientShadowAlpha, uint8_t spotShadowAlpha); void setLightGeometry(const Vector3& lightCenter, float lightRadius); + void setHardwareBufferRenderParams(const HardwareBufferRenderParams& params); void setOpaque(bool opaque); - void setColorMode(ColorMode mode); + float setColorMode(ColorMode mode); + void setRenderSdrHdrRatio(float ratio); int64_t* frameInfo(); void forceDrawNextFrame(); int syncAndDrawFrame(); @@ -104,6 +112,8 @@ public: static int maxTextureSize(); void stopDrawing(); void notifyFramePending(); + void notifyCallbackPending(); + void notifyExpensiveFrame(); void dumpProfileInfo(int fd, int dumpFlags); // Not exported, only used for testing @@ -133,8 +143,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..0afd949cf5c9 100644 --- a/libs/hwui/renderthread/RenderThread.cpp +++ b/libs/hwui/renderthread/RenderThread.cpp @@ -135,7 +135,9 @@ void RenderThread::frameCallback(int64_t vsyncId, int64_t frameDeadline, int64_t !mFrameCallbackTaskPending) { ATRACE_NAME("queue mFrameCallbackTask"); mFrameCallbackTaskPending = true; - nsecs_t runAt = (frameTimeNanos + mDispatchFrameDelay); + + nsecs_t timeUntilDeadline = frameDeadline - frameTimeNanos; + nsecs_t runAt = (frameTimeNanos + (timeUntilDeadline * 0.25f)); queue().postAt(runAt, [=]() { dispatchFrameCallbacks(); }); } } @@ -251,13 +253,12 @@ void RenderThread::initThreadLocals() { mEglManager = new EglManager(); mRenderState = new RenderState(*this); mVkManager = VulkanManager::getInstance(); - mCacheManager = new CacheManager(); + mCacheManager = new CacheManager(*this); } void RenderThread::setupFrameInterval() { nsecs_t frameIntervalNanos = DeviceInfo::getVsyncPeriod(); mTimeLord.setFrameInterval(frameIntervalNanos); - mDispatchFrameDelay = static_cast<nsecs_t>(frameIntervalNanos * .25f); } void RenderThread::requireGlContext() { @@ -266,7 +267,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; @@ -453,6 +454,8 @@ bool RenderThread::threadLoop() { // next vsync (oops), so none of the callbacks are run. requestVsync(); } + + mCacheManager->onThreadIdle(); } return false; @@ -502,6 +505,11 @@ void RenderThread::preload() { HardwareBitmapUploader::initialize(); } +void RenderThread::trimMemory(TrimLevel level) { + ATRACE_CALL(); + cacheManager().trimMemory(level); +} + } /* namespace renderthread */ } /* namespace uirenderer */ } /* namespace android */ diff --git a/libs/hwui/renderthread/RenderThread.h b/libs/hwui/renderthread/RenderThread.h index c1f6790b25b2..c77cd4134d1e 100644 --- a/libs/hwui/renderthread/RenderThread.h +++ b/libs/hwui/renderthread/RenderThread.h @@ -17,11 +17,11 @@ #ifndef RENDERTHREAD_H_ #define RENDERTHREAD_H_ -#include <surface_control_private.h> #include <GrDirectContext.h> #include <SkBitmap.h> #include <cutils/compiler.h> #include <private/android/choreographer.h> +#include <surface_control_private.h> #include <thread/ThreadBase.h> #include <utils/Looper.h> #include <utils/Thread.h> @@ -31,6 +31,7 @@ #include <set> #include "CacheManager.h" +#include "MemoryPolicy.h" #include "ProfileDataContainer.h" #include "RenderTask.h" #include "TimeLord.h" @@ -172,6 +173,8 @@ public: return mASurfaceControlFunctions; } + void trimMemory(TrimLevel level); + /** * isCurrent provides a way to query, if the caller is running on * the render thread. @@ -232,7 +235,6 @@ private: bool mFrameCallbackTaskPending; TimeLord mTimeLord; - nsecs_t mDispatchFrameDelay = 4_ms; RenderState* mRenderState; EglManager* mEglManager; WebViewFunctorManager& mFunctorManager; 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.cpp b/libs/hwui/renderthread/VulkanSurface.cpp index 7dd3561cb220..21b6c44e997e 100644 --- a/libs/hwui/renderthread/VulkanSurface.cpp +++ b/libs/hwui/renderthread/VulkanSurface.cpp @@ -199,7 +199,14 @@ bool VulkanSurface::InitializeWindowInfoStruct(ANativeWindow* window, ColorMode outWindowInfo->bufferFormat = ColorTypeToBufferFormat(colorType); outWindowInfo->colorspace = colorSpace; - outWindowInfo->dataspace = ColorSpaceToADataSpace(colorSpace.get(), colorType); + outWindowInfo->colorMode = colorMode; + + if (colorMode == ColorMode::Hdr || colorMode == ColorMode::Hdr10) { + outWindowInfo->dataspace = + static_cast<android_dataspace>(STANDARD_DCI_P3 | TRANSFER_SRGB | RANGE_EXTENDED); + } else { + outWindowInfo->dataspace = ColorSpaceToADataSpace(colorSpace.get(), colorType); + } LOG_ALWAYS_FATAL_IF( outWindowInfo->dataspace == HAL_DATASPACE_UNKNOWN && colorType != kAlpha_8_SkColorType, "Unsupported colorspace"); @@ -496,6 +503,33 @@ int VulkanSurface::getCurrentBuffersAge() { return currentBuffer.hasValidContents ? (mPresentCount - currentBuffer.lastPresentedCount) : 0; } +void VulkanSurface::setColorSpace(sk_sp<SkColorSpace> colorSpace) { + mWindowInfo.colorspace = std::move(colorSpace); + for (int i = 0; i < kNumBufferSlots; i++) { + mNativeBuffers[i].skSurface.reset(); + } + + if (mWindowInfo.colorMode == ColorMode::Hdr || mWindowInfo.colorMode == ColorMode::Hdr10) { + mWindowInfo.dataspace = + static_cast<android_dataspace>(STANDARD_DCI_P3 | TRANSFER_SRGB | RANGE_EXTENDED); + } else { + mWindowInfo.dataspace = ColorSpaceToADataSpace( + mWindowInfo.colorspace.get(), BufferFormatToColorType(mWindowInfo.bufferFormat)); + } + LOG_ALWAYS_FATAL_IF(mWindowInfo.dataspace == HAL_DATASPACE_UNKNOWN && + mWindowInfo.bufferFormat != AHARDWAREBUFFER_FORMAT_R8_UNORM, + "Unsupported colorspace"); + + if (mNativeWindow) { + int err = ANativeWindow_setBuffersDataSpace(mNativeWindow.get(), mWindowInfo.dataspace); + if (err != 0) { + ALOGE("VulkanSurface::setColorSpace() native_window_set_buffers_data_space(%d) " + "failed: %s (%d)", + mWindowInfo.dataspace, strerror(-err), err); + } + } +} + } /* namespace renderthread */ } /* namespace uirenderer */ } /* namespace android */ diff --git a/libs/hwui/renderthread/VulkanSurface.h b/libs/hwui/renderthread/VulkanSurface.h index beb71b727f51..e2ddc6b07768 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> @@ -45,6 +46,8 @@ public: } const SkMatrix& getCurrentPreTransform() { return mWindowInfo.preTransform; } + void setColorSpace(sk_sp<SkColorSpace> colorSpace); + private: /* * All structs/methods in this private section are specifically for use by the VulkanManager @@ -93,6 +96,7 @@ private: uint32_t bufferFormat; android_dataspace dataspace; sk_sp<SkColorSpace> colorspace; + ColorMode colorMode; int transform; size_t bufferCount; uint64_t windowUsageFlags; diff --git a/libs/hwui/tests/common/BitmapAllocationTestUtils.h b/libs/hwui/tests/common/BitmapAllocationTestUtils.h index 312b60bbc067..bbdd98e0c0d1 100644 --- a/libs/hwui/tests/common/BitmapAllocationTestUtils.h +++ b/libs/hwui/tests/common/BitmapAllocationTestUtils.h @@ -56,10 +56,11 @@ public: template <class BaseScene> static bool registerBitmapAllocationScene(std::string name, std::string description) { - TestScene::registerScene({name + "GlTex", description + " (GlTex version).", + TestScene::registerScene({name + "Texture", description + " (Texture version).", createBitmapAllocationScene<BaseScene, &allocateHeapBitmap>}); - TestScene::registerScene({name + "EglImage", description + " (EglImage version).", + TestScene::registerScene({name + "HardwareBuffer", + description + " (HardwareBuffer version).", createBitmapAllocationScene<BaseScene, &allocateHardwareBitmap>}); return true; } diff --git a/libs/hwui/tests/common/CallCountingCanvas.h b/libs/hwui/tests/common/CallCountingCanvas.h index d3c41191eef1..dc36a2e01815 100644 --- a/libs/hwui/tests/common/CallCountingCanvas.h +++ b/libs/hwui/tests/common/CallCountingCanvas.h @@ -19,6 +19,8 @@ #include <SkCanvasVirtualEnforcer.h> #include <SkNoDrawCanvas.h> +enum class SkBlendMode; + namespace android { namespace uirenderer { namespace test { diff --git a/libs/hwui/tests/common/TestContext.cpp b/libs/hwui/tests/common/TestContext.cpp index 898c64bd4159..fd596d998dfd 100644 --- a/libs/hwui/tests/common/TestContext.cpp +++ b/libs/hwui/tests/common/TestContext.cpp @@ -28,10 +28,11 @@ const ui::StaticDisplayInfo& getDisplayInfo() { #if HWUI_NULL_GPU info.density = 2.f; #else - const sp<IBinder> token = SurfaceComposerClient::getInternalDisplayToken(); - LOG_ALWAYS_FATAL_IF(!token, "%s: No internal display", __FUNCTION__); + const std::vector<PhysicalDisplayId> ids = SurfaceComposerClient::getPhysicalDisplayIds(); + LOG_ALWAYS_FATAL_IF(ids.empty(), "%s: No displays", __FUNCTION__); - const status_t status = SurfaceComposerClient::getStaticDisplayInfo(token, &info); + const status_t status = + SurfaceComposerClient::getStaticDisplayInfo(ids.front().value, &info); LOG_ALWAYS_FATAL_IF(status, "%s: Failed to get display info", __FUNCTION__); #endif return info; @@ -48,7 +49,10 @@ const ui::DisplayMode& getActiveDisplayMode() { config.xDpi = config.yDpi = 320.f; config.refreshRate = 60.f; #else - const sp<IBinder> token = SurfaceComposerClient::getInternalDisplayToken(); + const std::vector<PhysicalDisplayId> ids = SurfaceComposerClient::getPhysicalDisplayIds(); + LOG_ALWAYS_FATAL_IF(ids.empty(), "%s: No displays", __FUNCTION__); + + const sp<IBinder> token = SurfaceComposerClient::getPhysicalDisplayToken(ids.front()); LOG_ALWAYS_FATAL_IF(!token, "%s: No internal display", __FUNCTION__); const status_t status = SurfaceComposerClient::getActiveDisplayMode(token, &config); diff --git a/libs/hwui/tests/common/TestListViewSceneBase.cpp b/libs/hwui/tests/common/TestListViewSceneBase.cpp index 43df4a0b1576..e70d44c9c60a 100644 --- a/libs/hwui/tests/common/TestListViewSceneBase.cpp +++ b/libs/hwui/tests/common/TestListViewSceneBase.cpp @@ -19,6 +19,8 @@ #include "TestContext.h" #include "TestUtils.h" +#include <SkBlendMode.h> + #include <utils/Color.h> namespace android { 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..9d5c13e5cd75 100644 --- a/libs/hwui/tests/common/TestUtils.h +++ b/libs/hwui/tests/common/TestUtils.h @@ -16,6 +16,7 @@ #pragma once +#include <AutoBackendTextureRelease.h> #include <DisplayList.h> #include <Matrix.h> #include <Properties.h> @@ -27,10 +28,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 { @@ -283,6 +294,11 @@ public: static SkRect getClipBounds(const SkCanvas* canvas); static SkRect getLocalClipBounds(const SkCanvas* canvas); + static int getUsageCount(const AutoBackendTextureRelease* textureRelease) { + EXPECT_NE(nullptr, textureRelease); + return textureRelease->mUsageCount; + } + struct CallCounts { int sync = 0; int contextDestroyed = 0; diff --git a/libs/hwui/tests/common/scenes/BitmapFillrate.cpp b/libs/hwui/tests/common/scenes/BitmapFillrate.cpp index 5af7d43d7f66..19e87f851827 100644 --- a/libs/hwui/tests/common/scenes/BitmapFillrate.cpp +++ b/libs/hwui/tests/common/scenes/BitmapFillrate.cpp @@ -19,6 +19,7 @@ #include "utils/Color.h" #include <SkBitmap.h> +#include <SkBlendMode.h> using namespace android; using namespace android::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/ClippingAnimation.cpp b/libs/hwui/tests/common/scenes/ClippingAnimation.cpp index 2a016ac1b5bc..3a1ea8c29963 100644 --- a/libs/hwui/tests/common/scenes/ClippingAnimation.cpp +++ b/libs/hwui/tests/common/scenes/ClippingAnimation.cpp @@ -16,6 +16,8 @@ #include "TestSceneBase.h" +#include <SkBlendMode.h> + class ClippingAnimation; static TestScene::Registrar _RectGrid(TestScene::Info{ diff --git a/libs/hwui/tests/common/scenes/GlyphStressAnimation.cpp b/libs/hwui/tests/common/scenes/GlyphStressAnimation.cpp index 4271d2f04b88..484289a8ef1d 100644 --- a/libs/hwui/tests/common/scenes/GlyphStressAnimation.cpp +++ b/libs/hwui/tests/common/scenes/GlyphStressAnimation.cpp @@ -20,6 +20,8 @@ #include <hwui/Paint.h> #include <minikin/Layout.h> +#include <SkBlendMode.h> + #include <cstdio> class GlyphStressAnimation; 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..dfdd0d8727b9 100644 --- a/libs/hwui/tests/common/scenes/HwBitmapInCompositeShader.cpp +++ b/libs/hwui/tests/common/scenes/HwBitmapInCompositeShader.cpp @@ -17,6 +17,8 @@ #include "TestSceneBase.h" #include "utils/Color.h" +#include <SkBlendMode.h> +#include <SkColorSpace.h> #include <SkGradientShader.h> #include <SkImagePriv.h> #include <ui/PixelFormat.h> diff --git a/libs/hwui/tests/common/scenes/HwLayerAnimation.cpp b/libs/hwui/tests/common/scenes/HwLayerAnimation.cpp index cac2fb3d8d5c..2955fb25ec2c 100644 --- a/libs/hwui/tests/common/scenes/HwLayerAnimation.cpp +++ b/libs/hwui/tests/common/scenes/HwLayerAnimation.cpp @@ -16,6 +16,8 @@ #include "TestSceneBase.h" +#include <SkBlendMode.h> + class HwLayerAnimation; static TestScene::Registrar _HwLayer(TestScene::Info{ diff --git a/libs/hwui/tests/common/scenes/HwLayerSizeAnimation.cpp b/libs/hwui/tests/common/scenes/HwLayerSizeAnimation.cpp index 77a59dfe6ba5..8c9a6147f47d 100644 --- a/libs/hwui/tests/common/scenes/HwLayerSizeAnimation.cpp +++ b/libs/hwui/tests/common/scenes/HwLayerSizeAnimation.cpp @@ -16,6 +16,8 @@ #include "TestSceneBase.h" +#include <SkBlendMode.h> + class HwLayerSizeAnimation; static TestScene::Registrar _HwLayerSize(TestScene::Info{ diff --git a/libs/hwui/tests/common/scenes/JankyScene.cpp b/libs/hwui/tests/common/scenes/JankyScene.cpp index f5e6b317529a..250b986e7e73 100644 --- a/libs/hwui/tests/common/scenes/JankyScene.cpp +++ b/libs/hwui/tests/common/scenes/JankyScene.cpp @@ -16,6 +16,8 @@ #include "TestSceneBase.h" +#include <SkBlendMode.h> + #include <unistd.h> class JankyScene; diff --git a/libs/hwui/tests/common/scenes/ListOfFadedTextAnimation.cpp b/libs/hwui/tests/common/scenes/ListOfFadedTextAnimation.cpp index 5eaf1853233a..f669dbc9323e 100644 --- a/libs/hwui/tests/common/scenes/ListOfFadedTextAnimation.cpp +++ b/libs/hwui/tests/common/scenes/ListOfFadedTextAnimation.cpp @@ -17,6 +17,7 @@ #include "TestSceneBase.h" #include "tests/common/TestListViewSceneBase.h" #include "hwui/Paint.h" +#include <SkBlendMode.h> #include <SkGradientShader.h> class ListOfFadedTextAnimation; 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/OvalAnimation.cpp b/libs/hwui/tests/common/scenes/OvalAnimation.cpp index 402c1ece2146..1a2af8382ad7 100644 --- a/libs/hwui/tests/common/scenes/OvalAnimation.cpp +++ b/libs/hwui/tests/common/scenes/OvalAnimation.cpp @@ -17,6 +17,8 @@ #include "TestSceneBase.h" #include "utils/Color.h" +#include <SkBlendMode.h> + class OvalAnimation; static TestScene::Registrar _Oval(TestScene::Info{"oval", "Draws 1 oval.", diff --git a/libs/hwui/tests/common/scenes/PartialDamageAnimation.cpp b/libs/hwui/tests/common/scenes/PartialDamageAnimation.cpp index fb1b000a995e..25cf4d61bf9d 100644 --- a/libs/hwui/tests/common/scenes/PartialDamageAnimation.cpp +++ b/libs/hwui/tests/common/scenes/PartialDamageAnimation.cpp @@ -16,6 +16,8 @@ #include "TestSceneBase.h" +#include <SkBlendMode.h> + class PartialDamageAnimation; static TestScene::Registrar _PartialDamage(TestScene::Info{ diff --git a/libs/hwui/tests/common/scenes/PathClippingAnimation.cpp b/libs/hwui/tests/common/scenes/PathClippingAnimation.cpp index 1e343c1dd283..969514c50d14 100644 --- a/libs/hwui/tests/common/scenes/PathClippingAnimation.cpp +++ b/libs/hwui/tests/common/scenes/PathClippingAnimation.cpp @@ -16,6 +16,8 @@ #include <vector> +#include <SkBlendMode.h> + #include "TestSceneBase.h" class PathClippingAnimation : public TestScene { 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/RectGridAnimation.cpp b/libs/hwui/tests/common/scenes/RectGridAnimation.cpp index f37bcbc3ee1b..99e785887b16 100644 --- a/libs/hwui/tests/common/scenes/RectGridAnimation.cpp +++ b/libs/hwui/tests/common/scenes/RectGridAnimation.cpp @@ -16,6 +16,8 @@ #include "TestSceneBase.h" +#include <SkBlendMode.h> + class RectGridAnimation; static TestScene::Registrar _RectGrid(TestScene::Info{ diff --git a/libs/hwui/tests/common/scenes/RoundRectClippingAnimation.cpp b/libs/hwui/tests/common/scenes/RoundRectClippingAnimation.cpp index e9f353d887f2..2c27969487d3 100644 --- a/libs/hwui/tests/common/scenes/RoundRectClippingAnimation.cpp +++ b/libs/hwui/tests/common/scenes/RoundRectClippingAnimation.cpp @@ -16,6 +16,8 @@ #include "TestSceneBase.h" +#include <SkBlendMode.h> + #include <vector> class RoundRectClippingAnimation : public TestScene { diff --git a/libs/hwui/tests/common/scenes/SaveLayer2Animation.cpp b/libs/hwui/tests/common/scenes/SaveLayer2Animation.cpp index 252f539ffca9..ee30c131efbd 100644 --- a/libs/hwui/tests/common/scenes/SaveLayer2Animation.cpp +++ b/libs/hwui/tests/common/scenes/SaveLayer2Animation.cpp @@ -16,6 +16,7 @@ #include <hwui/Paint.h> #include <minikin/Layout.h> +#include <SkBlendMode.h> #include <string> #include "TestSceneBase.h" diff --git a/libs/hwui/tests/common/scenes/SaveLayerAnimation.cpp b/libs/hwui/tests/common/scenes/SaveLayerAnimation.cpp index 31a8ae1d38cd..d5060c758f93 100644 --- a/libs/hwui/tests/common/scenes/SaveLayerAnimation.cpp +++ b/libs/hwui/tests/common/scenes/SaveLayerAnimation.cpp @@ -16,6 +16,8 @@ #include "TestSceneBase.h" +#include <SkBlendMode.h> + class SaveLayerAnimation; static TestScene::Registrar _SaveLayer(TestScene::Info{ diff --git a/libs/hwui/tests/common/scenes/ShadowGrid2Animation.cpp b/libs/hwui/tests/common/scenes/ShadowGrid2Animation.cpp index c13e80e8c204..827ddab118d9 100644 --- a/libs/hwui/tests/common/scenes/ShadowGrid2Animation.cpp +++ b/libs/hwui/tests/common/scenes/ShadowGrid2Animation.cpp @@ -16,6 +16,8 @@ #include "TestSceneBase.h" +#include <SkBlendMode.h> + class ShadowGrid2Animation; static TestScene::Registrar _ShadowGrid2(TestScene::Info{ diff --git a/libs/hwui/tests/common/scenes/ShadowGridAnimation.cpp b/libs/hwui/tests/common/scenes/ShadowGridAnimation.cpp index 772b98e32220..a4fb10c5081e 100644 --- a/libs/hwui/tests/common/scenes/ShadowGridAnimation.cpp +++ b/libs/hwui/tests/common/scenes/ShadowGridAnimation.cpp @@ -16,6 +16,8 @@ #include "TestSceneBase.h" +#include <SkBlendMode.h> + class ShadowGridAnimation; static TestScene::Registrar _ShadowGrid(TestScene::Info{ diff --git a/libs/hwui/tests/common/scenes/ShadowShaderAnimation.cpp b/libs/hwui/tests/common/scenes/ShadowShaderAnimation.cpp index 0019da5fd80b..58c03727bc29 100644 --- a/libs/hwui/tests/common/scenes/ShadowShaderAnimation.cpp +++ b/libs/hwui/tests/common/scenes/ShadowShaderAnimation.cpp @@ -16,6 +16,8 @@ #include "TestSceneBase.h" +#include <SkBlendMode.h> + class ShadowShaderAnimation; static TestScene::Registrar _ShadowShader(TestScene::Info{ diff --git a/libs/hwui/tests/common/scenes/ShapeAnimation.cpp b/libs/hwui/tests/common/scenes/ShapeAnimation.cpp index 70a1557dcf6a..c0c3dfd9a8c4 100644 --- a/libs/hwui/tests/common/scenes/ShapeAnimation.cpp +++ b/libs/hwui/tests/common/scenes/ShapeAnimation.cpp @@ -17,6 +17,8 @@ #include "TestSceneBase.h" #include "utils/Color.h" +#include <SkBlendMode.h> + #include <cstdio> class ShapeAnimation; diff --git a/libs/hwui/tests/common/scenes/SimpleColorMatrixAnimation.cpp b/libs/hwui/tests/common/scenes/SimpleColorMatrixAnimation.cpp index a0bc5aa245d5..40f2ed081626 100644 --- a/libs/hwui/tests/common/scenes/SimpleColorMatrixAnimation.cpp +++ b/libs/hwui/tests/common/scenes/SimpleColorMatrixAnimation.cpp @@ -16,7 +16,9 @@ #include "TestSceneBase.h" -#include <SkColorMatrixFilter.h> +#include <SkBlendMode.h> +#include <SkColorFilter.h> +#include <SkColorMatrix.h> #include <SkGradientShader.h> class SimpleColorMatrixAnimation; diff --git a/libs/hwui/tests/common/scenes/SimpleGradientAnimation.cpp b/libs/hwui/tests/common/scenes/SimpleGradientAnimation.cpp index 57a260c8d234..a9e7a34b5b3f 100644 --- a/libs/hwui/tests/common/scenes/SimpleGradientAnimation.cpp +++ b/libs/hwui/tests/common/scenes/SimpleGradientAnimation.cpp @@ -16,6 +16,7 @@ #include "TestSceneBase.h" +#include <SkBlendMode.h> #include <SkGradientShader.h> class SimpleGradientAnimation; diff --git a/libs/hwui/tests/common/scenes/StretchyListViewAnimation.cpp b/libs/hwui/tests/common/scenes/StretchyListViewAnimation.cpp index e677549b7894..bb95490c1d39 100644 --- a/libs/hwui/tests/common/scenes/StretchyListViewAnimation.cpp +++ b/libs/hwui/tests/common/scenes/StretchyListViewAnimation.cpp @@ -14,7 +14,16 @@ * limitations under the License. */ +#include <SkBitmap.h> +#include <SkBlendMode.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 +139,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 +236,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/TextAnimation.cpp b/libs/hwui/tests/common/scenes/TextAnimation.cpp index d30903679bce..78146b8cabf2 100644 --- a/libs/hwui/tests/common/scenes/TextAnimation.cpp +++ b/libs/hwui/tests/common/scenes/TextAnimation.cpp @@ -17,6 +17,8 @@ #include "TestSceneBase.h" #include "hwui/Paint.h" +#include <SkBlendMode.h> + class TextAnimation; static TestScene::Registrar _Text(TestScene::Info{"text", "Draws a bunch of text.", 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/microbench/DisplayListCanvasBench.cpp b/libs/hwui/tests/microbench/DisplayListCanvasBench.cpp index 9cd10759a834..a55b72534924 100644 --- a/libs/hwui/tests/microbench/DisplayListCanvasBench.cpp +++ b/libs/hwui/tests/microbench/DisplayListCanvasBench.cpp @@ -22,6 +22,8 @@ #include "pipeline/skia/SkiaDisplayList.h" #include "tests/common/TestUtils.h" +#include <SkBlendMode.h> + using namespace android; using namespace android::uirenderer; using namespace android::uirenderer::skiapipeline; diff --git a/libs/hwui/tests/microbench/RenderNodeBench.cpp b/libs/hwui/tests/microbench/RenderNodeBench.cpp index 6aed251481bf..72946c4abdf0 100644 --- a/libs/hwui/tests/microbench/RenderNodeBench.cpp +++ b/libs/hwui/tests/microbench/RenderNodeBench.cpp @@ -19,6 +19,8 @@ #include "hwui/Canvas.h" #include "RenderNode.h" +#include <SkBlendMode.h> + using namespace android; using namespace android::uirenderer; diff --git a/libs/hwui/tests/unit/AutoBackendTextureReleaseTests.cpp b/libs/hwui/tests/unit/AutoBackendTextureReleaseTests.cpp new file mode 100644 index 000000000000..138b3efd10ed --- /dev/null +++ b/libs/hwui/tests/unit/AutoBackendTextureReleaseTests.cpp @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <gtest/gtest.h> + +#include "AutoBackendTextureRelease.h" +#include "tests/common/TestUtils.h" + +using namespace android; +using namespace android::uirenderer; + +AHardwareBuffer* allocHardwareBuffer() { + AHardwareBuffer* buffer; + AHardwareBuffer_Desc desc = { + .width = 16, + .height = 16, + .layers = 1, + .format = AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM, + .usage = AHARDWAREBUFFER_USAGE_GPU_SAMPLED_IMAGE, + }; + constexpr int kSucceeded = 0; + int status = AHardwareBuffer_allocate(&desc, &buffer); + EXPECT_EQ(kSucceeded, status); + return buffer; +} + +// Expands to AutoBackendTextureRelease_makeImage_invalid_RenderThreadTest, +// set as friend in AutoBackendTextureRelease.h +RENDERTHREAD_TEST(AutoBackendTextureRelease, makeImage_invalid) { + AHardwareBuffer* buffer = allocHardwareBuffer(); + AutoBackendTextureRelease* textureRelease = + new AutoBackendTextureRelease(renderThread.getGrContext(), buffer); + + EXPECT_EQ(1, TestUtils::getUsageCount(textureRelease)); + + // SkImage::MakeFromTexture should fail if given null GrDirectContext. + textureRelease->makeImage(buffer, HAL_DATASPACE_UNKNOWN, /*context = */ nullptr); + + EXPECT_EQ(1, TestUtils::getUsageCount(textureRelease)); + + textureRelease->unref(true); + AHardwareBuffer_release(buffer); +} + +// Expands to AutoBackendTextureRelease_makeImage_valid_RenderThreadTest, +// set as friend in AutoBackendTextureRelease.h +RENDERTHREAD_TEST(AutoBackendTextureRelease, makeImage_valid) { + AHardwareBuffer* buffer = allocHardwareBuffer(); + AutoBackendTextureRelease* textureRelease = + new AutoBackendTextureRelease(renderThread.getGrContext(), buffer); + + EXPECT_EQ(1, TestUtils::getUsageCount(textureRelease)); + + textureRelease->makeImage(buffer, HAL_DATASPACE_UNKNOWN, renderThread.getGrContext()); + + EXPECT_EQ(2, TestUtils::getUsageCount(textureRelease)); + + textureRelease->unref(true); + AHardwareBuffer_release(buffer); +} diff --git a/libs/hwui/tests/unit/CacheManagerTests.cpp b/libs/hwui/tests/unit/CacheManagerTests.cpp index edd3e4e4f4d4..2b90bda87ecd 100644 --- a/libs/hwui/tests/unit/CacheManagerTests.cpp +++ b/libs/hwui/tests/unit/CacheManagerTests.cpp @@ -21,6 +21,7 @@ #include "tests/common/TestUtils.h" #include <SkImagePriv.h> +#include "include/gpu/GpuTypes.h" // from Skia using namespace android; using namespace android::uirenderer; @@ -32,7 +33,8 @@ static size_t getCacheUsage(GrDirectContext* grContext) { return cacheUsage; } -RENDERTHREAD_SKIA_PIPELINE_TEST(CacheManager, trimMemory) { +// TOOD(258700630): fix this test and re-enable +RENDERTHREAD_SKIA_PIPELINE_TEST(CacheManager, DISABLED_trimMemory) { int32_t width = DeviceInfo::get()->getWidth(); int32_t height = DeviceInfo::get()->getHeight(); GrDirectContext* grContext = renderThread.getGrContext(); @@ -44,7 +46,8 @@ RENDERTHREAD_SKIA_PIPELINE_TEST(CacheManager, trimMemory) { while (getCacheUsage(grContext) <= renderThread.cacheManager().getBackgroundCacheSize()) { SkImageInfo info = SkImageInfo::MakeA8(width, height); - sk_sp<SkSurface> surface = SkSurface::MakeRenderTarget(grContext, SkBudgeted::kYes, info); + sk_sp<SkSurface> surface = SkSurface::MakeRenderTarget(grContext, skgpu::Budgeted::kYes, + info); surface->getCanvas()->drawColor(SK_AlphaTRANSPARENT); grContext->flushAndSubmit(); @@ -58,7 +61,7 @@ RENDERTHREAD_SKIA_PIPELINE_TEST(CacheManager, trimMemory) { ASSERT_TRUE(SkImage_pinAsTexture(image.get(), grContext)); // attempt to trim all memory while we still hold strong refs - renderThread.cacheManager().trimMemory(CacheManager::TrimMemoryMode::Complete); + renderThread.cacheManager().trimMemory(TrimLevel::COMPLETE); ASSERT_TRUE(0 == grContext->getResourceCachePurgeableBytes()); // free the surfaces @@ -75,11 +78,11 @@ RENDERTHREAD_SKIA_PIPELINE_TEST(CacheManager, trimMemory) { ASSERT_TRUE(renderThread.cacheManager().getBackgroundCacheSize() < purgeableBytes); // UI hidden and make sure only some got purged (unique should remain) - renderThread.cacheManager().trimMemory(CacheManager::TrimMemoryMode::UiHidden); + renderThread.cacheManager().trimMemory(TrimLevel::UI_HIDDEN); ASSERT_TRUE(0 < grContext->getResourceCachePurgeableBytes()); ASSERT_TRUE(renderThread.cacheManager().getBackgroundCacheSize() > getCacheUsage(grContext)); // complete and make sure all get purged - renderThread.cacheManager().trimMemory(CacheManager::TrimMemoryMode::Complete); + renderThread.cacheManager().trimMemory(TrimLevel::COMPLETE); ASSERT_TRUE(0 == grContext->getResourceCachePurgeableBytes()); } diff --git a/libs/hwui/tests/unit/CanvasContextTests.cpp b/libs/hwui/tests/unit/CanvasContextTests.cpp index 1771c3590e10..9e376e32f8ea 100644 --- a/libs/hwui/tests/unit/CanvasContextTests.cpp +++ b/libs/hwui/tests/unit/CanvasContextTests.cpp @@ -36,9 +36,9 @@ RENDERTHREAD_TEST(CanvasContext, create) { auto rootNode = TestUtils::createNode(0, 0, 200, 400, nullptr); ContextFactory contextFactory; std::unique_ptr<CanvasContext> canvasContext( - CanvasContext::create(renderThread, false, rootNode.get(), &contextFactory)); + CanvasContext::create(renderThread, false, rootNode.get(), &contextFactory, 0, 0)); - ASSERT_FALSE(canvasContext->hasSurface()); + ASSERT_FALSE(canvasContext->hasOutputTarget()); canvasContext->destroy(); } diff --git a/libs/hwui/tests/unit/CanvasOpTests.cpp b/libs/hwui/tests/unit/CanvasOpTests.cpp index 2cf3456694b0..1f6edf36af25 100644 --- a/libs/hwui/tests/unit/CanvasOpTests.cpp +++ b/libs/hwui/tests/unit/CanvasOpTests.cpp @@ -23,9 +23,18 @@ #include <tests/common/CallCountingCanvas.h> -#include "SkPictureRecorder.h" +#include "SkBlendMode.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/GraphicsStatsServiceTests.cpp b/libs/hwui/tests/unit/GraphicsStatsServiceTests.cpp index 098b4ccea8cf..c2d23e6d1101 100644 --- a/libs/hwui/tests/unit/GraphicsStatsServiceTests.cpp +++ b/libs/hwui/tests/unit/GraphicsStatsServiceTests.cpp @@ -14,17 +14,18 @@ * limitations under the License. */ +#include <android-base/macros.h> +#include <gmock/gmock.h> #include <gtest/gtest.h> - -#include "protos/graphicsstats.pb.h" -#include "service/GraphicsStatsService.h" - #include <stdio.h> #include <stdlib.h> #include <sys/stat.h> #include <sys/types.h> #include <unistd.h> +#include "protos/graphicsstats.pb.h" +#include "service/GraphicsStatsService.h" + using namespace android; using namespace android::uirenderer; @@ -49,12 +50,14 @@ std::string findRootPath() { // No code left untested TEST(GraphicsStats, findRootPath) { -#ifdef __LP64__ - std::string expected = "/data/nativetest64/hwui_unit_tests"; -#else - std::string expected = "/data/nativetest/hwui_unit_tests"; -#endif - EXPECT_EQ(expected, findRootPath()); + // Different tools/infrastructure seem to push this to different locations. It shouldn't really + // matter where the binary is, so add new locations here as needed. This test still seems good + // as it's nice to understand the possibility space, and ensure findRootPath continues working + // as expected. + std::string acceptableLocations[] = {"/data/nativetest/hwui_unit_tests", + "/data/nativetest64/hwui_unit_tests", + "/data/local/tmp/nativetest/hwui_unit_tests/" ABI_STRING}; + EXPECT_THAT(acceptableLocations, ::testing::Contains(findRootPath())); } TEST(GraphicsStats, saveLoad) { 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/RenderNodeDrawableTests.cpp b/libs/hwui/tests/unit/RenderNodeDrawableTests.cpp index ec949b80ea55..596bd37e4cf5 100644 --- a/libs/hwui/tests/unit/RenderNodeDrawableTests.cpp +++ b/libs/hwui/tests/unit/RenderNodeDrawableTests.cpp @@ -17,6 +17,7 @@ #include <VectorDrawable.h> #include <gtest/gtest.h> +#include <SkBlendMode.h> #include <SkClipStack.h> #include <SkSurface_Base.h> #include <string.h> @@ -334,7 +335,7 @@ RENDERTHREAD_TEST(RenderNodeDrawable, projectionReorder) { "A"); ContextFactory contextFactory; std::unique_ptr<CanvasContext> canvasContext( - CanvasContext::create(renderThread, false, parent.get(), &contextFactory)); + CanvasContext::create(renderThread, false, parent.get(), &contextFactory, 0, 0)); TreeInfo info(TreeInfo::MODE_RT_ONLY, *canvasContext.get()); DamageAccumulator damageAccumulator; info.damageAccumulator = &damageAccumulator; @@ -398,7 +399,7 @@ RENDERTHREAD_SKIA_PIPELINE_TEST(RenderNodeDrawable, emptyReceiver) { "A"); ContextFactory contextFactory; std::unique_ptr<CanvasContext> canvasContext( - CanvasContext::create(renderThread, false, parent.get(), &contextFactory)); + CanvasContext::create(renderThread, false, parent.get(), &contextFactory, 0, 0)); TreeInfo info(TreeInfo::MODE_RT_ONLY, *canvasContext.get()); DamageAccumulator damageAccumulator; info.damageAccumulator = &damageAccumulator; @@ -518,7 +519,7 @@ RENDERTHREAD_SKIA_PIPELINE_TEST(RenderNodeDrawable, projectionHwLayer) { // prepareTree is required to find, which receivers have backward projected nodes ContextFactory contextFactory; std::unique_ptr<CanvasContext> canvasContext( - CanvasContext::create(renderThread, false, parent.get(), &contextFactory)); + CanvasContext::create(renderThread, false, parent.get(), &contextFactory, 0, 0)); TreeInfo info(TreeInfo::MODE_RT_ONLY, *canvasContext.get()); DamageAccumulator damageAccumulator; info.damageAccumulator = &damageAccumulator; @@ -618,7 +619,7 @@ RENDERTHREAD_TEST(RenderNodeDrawable, projectionChildScroll) { // prepareTree is required to find, which receivers have backward projected nodes ContextFactory contextFactory; std::unique_ptr<CanvasContext> canvasContext( - CanvasContext::create(renderThread, false, parent.get(), &contextFactory)); + CanvasContext::create(renderThread, false, parent.get(), &contextFactory, 0, 0)); TreeInfo info(TreeInfo::MODE_RT_ONLY, *canvasContext.get()); DamageAccumulator damageAccumulator; info.damageAccumulator = &damageAccumulator; @@ -634,7 +635,7 @@ namespace { static int drawNode(RenderThread& renderThread, const sp<RenderNode>& renderNode) { ContextFactory contextFactory; std::unique_ptr<CanvasContext> canvasContext( - CanvasContext::create(renderThread, false, renderNode.get(), &contextFactory)); + CanvasContext::create(renderThread, false, renderNode.get(), &contextFactory, 0, 0)); TreeInfo info(TreeInfo::MODE_RT_ONLY, *canvasContext.get()); DamageAccumulator damageAccumulator; info.damageAccumulator = &damageAccumulator; diff --git a/libs/hwui/tests/unit/RenderNodeTests.cpp b/libs/hwui/tests/unit/RenderNodeTests.cpp index 61bd646b0a76..80796f4a4111 100644 --- a/libs/hwui/tests/unit/RenderNodeTests.cpp +++ b/libs/hwui/tests/unit/RenderNodeTests.cpp @@ -274,7 +274,7 @@ RENDERTHREAD_TEST(RenderNode, prepareTree_nullableDisplayList) { auto rootNode = TestUtils::createNode(0, 0, 200, 400, nullptr); ContextFactory contextFactory; std::unique_ptr<CanvasContext> canvasContext( - CanvasContext::create(renderThread, false, rootNode.get(), &contextFactory)); + CanvasContext::create(renderThread, false, rootNode.get(), &contextFactory, 0, 0)); TreeInfo info(TreeInfo::MODE_RT_ONLY, *canvasContext.get()); DamageAccumulator damageAccumulator; info.damageAccumulator = &damageAccumulator; @@ -310,7 +310,7 @@ RENDERTHREAD_TEST(DISABLED_RenderNode, prepareTree_HwLayer_AVD_enqueueDamage) { }); ContextFactory contextFactory; std::unique_ptr<CanvasContext> canvasContext( - CanvasContext::create(renderThread, false, rootNode.get(), &contextFactory)); + CanvasContext::create(renderThread, false, rootNode.get(), &contextFactory, 0, 0)); canvasContext->setSurface(nullptr); TreeInfo info(TreeInfo::MODE_RT_ONLY, *canvasContext.get()); DamageAccumulator damageAccumulator; diff --git a/libs/hwui/tests/unit/ShaderCacheTests.cpp b/libs/hwui/tests/unit/ShaderCacheTests.cpp index 974d85a453db..7bcd45c6b643 100644 --- a/libs/hwui/tests/unit/ShaderCacheTests.cpp +++ b/libs/hwui/tests/unit/ShaderCacheTests.cpp @@ -14,6 +14,10 @@ * limitations under the License. */ +#include <GrDirectContext.h> +#include <Properties.h> +#include <SkData.h> +#include <SkRefCnt.h> #include <cutils/properties.h> #include <dirent.h> #include <errno.h> @@ -22,9 +26,12 @@ #include <stdlib.h> #include <sys/types.h> #include <utils/Log.h> + #include <cstdint> + #include "FileBlobCache.h" #include "pipeline/skia/ShaderCache.h" +#include "tests/common/TestUtils.h" using namespace android::uirenderer::skiapipeline; @@ -35,11 +42,38 @@ namespace skiapipeline { class ShaderCacheTestUtils { public: /** - * "setSaveDelay" sets the time in seconds to wait before saving newly inserted cache entries. - * If set to 0, then deferred save is disabled. + * Hack to reset all member variables of the given cache to their default / initial values. + * + * WARNING: this must be kept up to date manually, since ShaderCache's parent disables just + * reassigning a new instance. */ - static void setSaveDelay(ShaderCache& cache, unsigned int saveDelay) { - cache.mDeferredSaveDelay = saveDelay; + static void reinitializeAllFields(ShaderCache& cache) { + ShaderCache newCache = ShaderCache(); + std::lock_guard<std::mutex> lock(cache.mMutex); + // By order of declaration + cache.mInitialized = newCache.mInitialized; + cache.mBlobCache.reset(nullptr); + cache.mFilename = newCache.mFilename; + cache.mIDHash.clear(); + cache.mSavePending = newCache.mSavePending; + cache.mObservedBlobValueSize = newCache.mObservedBlobValueSize; + cache.mDeferredSaveDelayMs = newCache.mDeferredSaveDelayMs; + cache.mTryToStorePipelineCache = newCache.mTryToStorePipelineCache; + cache.mInStoreVkPipelineInProgress = newCache.mInStoreVkPipelineInProgress; + cache.mNewPipelineCacheSize = newCache.mNewPipelineCacheSize; + cache.mOldPipelineCacheSize = newCache.mOldPipelineCacheSize; + cache.mCacheDirty = newCache.mCacheDirty; + cache.mNumShadersCachedInRam = newCache.mNumShadersCachedInRam; + } + + /** + * "setSaveDelayMs" sets the time in milliseconds to wait before saving newly inserted cache + * entries. If set to 0, then deferred save is disabled, and "saveToDiskLocked" must be called + * manually, as seen in the "terminate" testing helper function. + */ + static void setSaveDelayMs(ShaderCache& cache, unsigned int saveDelayMs) { + std::lock_guard<std::mutex> lock(cache.mMutex); + cache.mDeferredSaveDelayMs = saveDelayMs; } /** @@ -48,8 +82,9 @@ public: */ static void terminate(ShaderCache& cache, bool saveContent) { std::lock_guard<std::mutex> lock(cache.mMutex); - cache.mSavePending = saveContent; - cache.saveToDiskLocked(); + if (saveContent) { + cache.saveToDiskLocked(); + } cache.mBlobCache = NULL; } @@ -60,6 +95,38 @@ public: static bool validateCache(ShaderCache& cache, std::vector<T> hash) { return cache.validateCache(hash.data(), hash.size() * sizeof(T)); } + + /** + * Waits until cache::mSavePending is false, checking every 0.1 ms *while the mutex is free*. + * + * Fails if there was no save pending, or if the cache was already being written to disk, or if + * timeoutMs is exceeded. + * + * Note: timeoutMs only guards against mSavePending getting stuck like in b/268205519, and + * cannot protect against mutex-based deadlock. Reaching timeoutMs implies something is broken, + * so setting it to a sufficiently large value will not delay execution in the happy state. + */ + static void waitForPendingSave(ShaderCache& cache, const int timeoutMs = 50) { + { + std::lock_guard<std::mutex> lock(cache.mMutex); + ASSERT_TRUE(cache.mSavePending); + } + bool saving = true; + float elapsedMilliseconds = 0; + while (saving) { + if (elapsedMilliseconds >= timeoutMs) { + FAIL() << "Timed out after waiting " << timeoutMs << " ms for a pending save"; + } + // This small (0.1 ms) delay is to avoid working too much while waiting for + // deferredSaveThread to take the mutex and start the disk write. + const int delayMicroseconds = 100; + usleep(delayMicroseconds); + elapsedMilliseconds += (float)delayMicroseconds / 1000; + + std::lock_guard<std::mutex> lock(cache.mMutex); + saving = cache.mSavePending; + } + } }; } /* namespace skiapipeline */ @@ -81,6 +148,18 @@ bool folderExist(const std::string& folderName) { return false; } +/** + * Attempts to delete the given file, and asserts that either: + * 1. Deletion was successful, OR + * 2. The file did not exist. + * + * Tip: wrap calls to this in ASSERT_NO_FATAL_FAILURE(x) if a test should exit early if this fails. + */ +void deleteFileAssertSuccess(const std::string& filePath) { + int deleteResult = remove(filePath.c_str()); + ASSERT_TRUE(0 == deleteResult || ENOENT == errno); +} + inline bool checkShader(const sk_sp<SkData>& shader1, const sk_sp<SkData>& shader2) { return nullptr != shader1 && nullptr != shader2 && shader1->size() == shader2->size() && 0 == memcmp(shader1->data(), shader2->data(), shader1->size()); @@ -91,6 +170,10 @@ inline bool checkShader(const sk_sp<SkData>& shader, const char* program) { return checkShader(shader, shader2); } +inline bool checkShader(const sk_sp<SkData>& shader, const std::string& program) { + return checkShader(shader, program.c_str()); +} + template <typename T> bool checkShader(const sk_sp<SkData>& shader, std::vector<T>& program) { sk_sp<SkData> shader2 = SkData::MakeWithCopy(program.data(), program.size() * sizeof(T)); @@ -101,6 +184,10 @@ void setShader(sk_sp<SkData>& shader, const char* program) { shader = SkData::MakeWithCString(program); } +void setShader(sk_sp<SkData>& shader, const std::string& program) { + setShader(shader, program.c_str()); +} + template <typename T> void setShader(sk_sp<SkData>& shader, std::vector<T>& buffer) { shader = SkData::MakeWithCopy(buffer.data(), buffer.size() * sizeof(T)); @@ -124,13 +211,13 @@ TEST(ShaderCacheTest, testWriteAndRead) { std::string cacheFile2 = getExternalStorageFolder() + "/shaderCacheTest2"; // remove any test files from previous test run - int deleteFile = remove(cacheFile1.c_str()); - ASSERT_TRUE(0 == deleteFile || ENOENT == errno); + ASSERT_NO_FATAL_FAILURE(deleteFileAssertSuccess(cacheFile1)); + ASSERT_NO_FATAL_FAILURE(deleteFileAssertSuccess(cacheFile2)); std::srand(0); // read the cache from a file that does not exist ShaderCache::get().setFilename(cacheFile1.c_str()); - ShaderCacheTestUtils::setSaveDelay(ShaderCache::get(), 0); // disable deferred save + ShaderCacheTestUtils::setSaveDelayMs(ShaderCache::get(), 0); // disable deferred save ShaderCache::get().initShaderDiskCache(); // read a key - should not be found since the cache is empty @@ -184,7 +271,8 @@ TEST(ShaderCacheTest, testWriteAndRead) { ASSERT_TRUE(checkShader(outVS2, dataBuffer)); ShaderCacheTestUtils::terminate(ShaderCache::get(), false); - remove(cacheFile1.c_str()); + ASSERT_NO_FATAL_FAILURE(deleteFileAssertSuccess(cacheFile1)); + ASSERT_NO_FATAL_FAILURE(deleteFileAssertSuccess(cacheFile2)); } TEST(ShaderCacheTest, testCacheValidation) { @@ -196,13 +284,13 @@ TEST(ShaderCacheTest, testCacheValidation) { std::string cacheFile2 = getExternalStorageFolder() + "/shaderCacheTest2"; // remove any test files from previous test run - int deleteFile = remove(cacheFile1.c_str()); - ASSERT_TRUE(0 == deleteFile || ENOENT == errno); + ASSERT_NO_FATAL_FAILURE(deleteFileAssertSuccess(cacheFile1)); + ASSERT_NO_FATAL_FAILURE(deleteFileAssertSuccess(cacheFile2)); std::srand(0); // generate identity and read the cache from a file that does not exist ShaderCache::get().setFilename(cacheFile1.c_str()); - ShaderCacheTestUtils::setSaveDelay(ShaderCache::get(), 0); // disable deferred save + ShaderCacheTestUtils::setSaveDelayMs(ShaderCache::get(), 0); // disable deferred save std::vector<uint8_t> identity(1024); genRandomData(identity); ShaderCache::get().initShaderDiskCache( @@ -276,7 +364,81 @@ TEST(ShaderCacheTest, testCacheValidation) { } ShaderCacheTestUtils::terminate(ShaderCache::get(), false); - remove(cacheFile1.c_str()); + ASSERT_NO_FATAL_FAILURE(deleteFileAssertSuccess(cacheFile1)); + ASSERT_NO_FATAL_FAILURE(deleteFileAssertSuccess(cacheFile2)); +} + +using namespace android::uirenderer; +RENDERTHREAD_SKIA_PIPELINE_TEST(ShaderCacheTest, testOnVkFrameFlushed) { + if (Properties::getRenderPipelineType() != RenderPipelineType::SkiaVulkan) { + // RENDERTHREAD_SKIA_PIPELINE_TEST declares both SkiaVK and SkiaGL variants. + GTEST_SKIP() << "This test is only applicable to RenderPipelineType::SkiaVulkan"; + } + if (!folderExist(getExternalStorageFolder())) { + // Don't run the test if external storage folder is not available + return; + } + std::string cacheFile = getExternalStorageFolder() + "/shaderCacheTest"; + GrDirectContext* grContext = renderThread.getGrContext(); + + // Remove any test files from previous test run + ASSERT_NO_FATAL_FAILURE(deleteFileAssertSuccess(cacheFile)); + + // The first iteration of this loop is to save an initial VkPipelineCache data blob to disk, + // which sets up the second iteration for a common scenario of comparing a "new" VkPipelineCache + // blob passed to "store" against the same blob that's already in the persistent cache from a + // previous launch. "reinitializeAllFields" is critical to emulate each iteration being as close + // to the state of a freshly launched app as possible, as the initial values of member variables + // like mInStoreVkPipelineInProgress and mOldPipelineCacheSize are critical to catch issues + // such as b/268205519 + for (int flushIteration = 1; flushIteration <= 2; flushIteration++) { + SCOPED_TRACE("Frame flush iteration " + std::to_string(flushIteration)); + // Reset *all* in-memory data and reload the cache from disk. + ShaderCacheTestUtils::reinitializeAllFields(ShaderCache::get()); + ShaderCacheTestUtils::setSaveDelayMs(ShaderCache::get(), 10); // Delay must be > 0 to save. + ShaderCache::get().setFilename(cacheFile.c_str()); + ShaderCache::get().initShaderDiskCache(); + + // 1st iteration: store pipeline data to be read back on a subsequent "boot" of the "app". + // 2nd iteration: ensure that an initial frame flush (without storing any shaders) given the + // same pipeline data that's already on disk doesn't break the cache. + ShaderCache::get().onVkFrameFlushed(grContext); + ASSERT_NO_FATAL_FAILURE(ShaderCacheTestUtils::waitForPendingSave(ShaderCache::get())); + } + + constexpr char shader1[] = "sassas"; + constexpr char shader2[] = "someVS"; + constexpr int numIterations = 3; + // Also do n iterations of separate "store some shaders then flush the frame" pairs to just + // double-check the cache also doesn't get stuck from that use case. + for (int saveIteration = 1; saveIteration <= numIterations; saveIteration++) { + SCOPED_TRACE("Shader save iteration " + std::to_string(saveIteration)); + // Write twice to the in-memory cache, which should start a deferred save with both queued. + sk_sp<SkData> inVS; + setShader(inVS, shader1 + std::to_string(saveIteration)); + ShaderCache::get().store(GrProgramDescTest(100), *inVS.get(), SkString()); + setShader(inVS, shader2 + std::to_string(saveIteration)); + ShaderCache::get().store(GrProgramDescTest(432), *inVS.get(), SkString()); + + // Simulate flush to also save latest pipeline info. + ShaderCache::get().onVkFrameFlushed(grContext); + ASSERT_NO_FATAL_FAILURE(ShaderCacheTestUtils::waitForPendingSave(ShaderCache::get())); + } + + // Reload from disk to ensure saving succeeded. + ShaderCacheTestUtils::terminate(ShaderCache::get(), false); + ShaderCache::get().initShaderDiskCache(); + + // Read twice, ensure equal to last store. + sk_sp<SkData> outVS; + ASSERT_NE((outVS = ShaderCache::get().load(GrProgramDescTest(100))), sk_sp<SkData>()); + ASSERT_TRUE(checkShader(outVS, shader1 + std::to_string(numIterations))); + ASSERT_NE((outVS = ShaderCache::get().load(GrProgramDescTest(432))), sk_sp<SkData>()); + ASSERT_TRUE(checkShader(outVS, shader2 + std::to_string(numIterations))); + + // Clean up. + ShaderCacheTestUtils::terminate(ShaderCache::get(), false); + ASSERT_NO_FATAL_FAILURE(deleteFileAssertSuccess(cacheFile)); } } // namespace 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..87c52161d68e 100644 --- a/libs/hwui/tests/unit/SkiaCanvasTests.cpp +++ b/libs/hwui/tests/unit/SkiaCanvasTests.cpp @@ -17,9 +17,19 @@ #include "tests/common/TestUtils.h" #include <hwui/Paint.h> +#include <SkAlphaType.h> +#include <SkBitmap.h> +#include <SkBlendMode.h> +#include <SkCanvas.h> #include <SkCanvasStateUtils.h> +#include <SkColor.h> +#include <SkColorSpace.h> +#include <SkColorType.h> +#include <SkImageInfo.h> #include <SkPicture.h> #include <SkPictureRecorder.h> +#include <SkRefCnt.h> +#include <SkSurface.h> #include <gtest/gtest.h> using namespace android; diff --git a/libs/hwui/tests/unit/SkiaDisplayListTests.cpp b/libs/hwui/tests/unit/SkiaDisplayListTests.cpp index 3d5aca4bf05a..f825d7c5d9cc 100644 --- a/libs/hwui/tests/unit/SkiaDisplayListTests.cpp +++ b/libs/hwui/tests/unit/SkiaDisplayListTests.cpp @@ -142,7 +142,7 @@ RENDERTHREAD_SKIA_PIPELINE_TEST(SkiaDisplayList, prepareListAndChildren) { auto rootNode = TestUtils::createNode(0, 0, 200, 400, nullptr); ContextFactory contextFactory; std::unique_ptr<CanvasContext> canvasContext( - CanvasContext::create(renderThread, false, rootNode.get(), &contextFactory)); + CanvasContext::create(renderThread, false, rootNode.get(), &contextFactory, 0, 0)); TreeInfo info(TreeInfo::MODE_FULL, *canvasContext.get()); DamageAccumulator damageAccumulator; info.damageAccumulator = &damageAccumulator; @@ -201,7 +201,7 @@ RENDERTHREAD_SKIA_PIPELINE_TEST(SkiaDisplayList, prepareListAndChildren_vdOffscr auto rootNode = TestUtils::createNode(0, 0, 200, 400, nullptr); ContextFactory contextFactory; std::unique_ptr<CanvasContext> canvasContext( - CanvasContext::create(renderThread, false, rootNode.get(), &contextFactory)); + CanvasContext::create(renderThread, false, rootNode.get(), &contextFactory, 0, 0)); // Set up a Surface so that we can position the VectorDrawable offscreen. test::TestContext testContext; diff --git a/libs/hwui/tests/unit/SkiaPipelineTests.cpp b/libs/hwui/tests/unit/SkiaPipelineTests.cpp index 60ae6044cd5b..4d0595e03da6 100644 --- a/libs/hwui/tests/unit/SkiaPipelineTests.cpp +++ b/libs/hwui/tests/unit/SkiaPipelineTests.cpp @@ -17,6 +17,7 @@ #include <VectorDrawable.h> #include <gtest/gtest.h> +#include <SkBlendMode.h> #include <SkClipStack.h> #include <SkSurface_Base.h> #include <string.h> @@ -404,7 +405,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/hwui/utils/AutoMalloc.h b/libs/hwui/utils/AutoMalloc.h new file mode 100644 index 000000000000..05f5e9f24133 --- /dev/null +++ b/libs/hwui/utils/AutoMalloc.h @@ -0,0 +1,94 @@ +/** + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <cstdlib> +#include <memory> +#include <type_traits> + +namespace android { +namespace uirenderer { + +/** Manages an array of T elements, freeing the array in the destructor. + * Does NOT call any constructors/destructors on T (T must be POD). + */ +template <typename T, + typename = std::enable_if_t<std::is_trivially_default_constructible<T>::value && + std::is_trivially_destructible<T>::value>> +class AutoTMalloc { +public: + /** Takes ownership of the ptr. The ptr must be a value which can be passed to std::free. */ + explicit AutoTMalloc(T* ptr = nullptr) : fPtr(ptr) {} + + /** Allocates space for 'count' Ts. */ + explicit AutoTMalloc(size_t count) : fPtr(mallocIfCountThrowOnFail(count)) {} + + AutoTMalloc(AutoTMalloc&&) = default; + AutoTMalloc& operator=(AutoTMalloc&&) = default; + + /** Resize the memory area pointed to by the current ptr preserving contents. */ + void realloc(size_t count) { fPtr.reset(reallocIfCountThrowOnFail(count)); } + + /** Resize the memory area pointed to by the current ptr without preserving contents. */ + T* reset(size_t count = 0) { + fPtr.reset(mallocIfCountThrowOnFail(count)); + return this->get(); + } + + T* get() const { return fPtr.get(); } + + operator T*() { return fPtr.get(); } + + operator const T*() const { return fPtr.get(); } + + T& operator[](int index) { return fPtr.get()[index]; } + + const T& operator[](int index) const { return fPtr.get()[index]; } + + /** + * Transfer ownership of the ptr to the caller, setting the internal + * pointer to NULL. Note that this differs from get(), which also returns + * the pointer, but it does not transfer ownership. + */ + T* release() { return fPtr.release(); } + +private: + struct FreeDeleter { + void operator()(uint8_t* p) { std::free(p); } + }; + std::unique_ptr<T, FreeDeleter> fPtr; + + T* mallocIfCountThrowOnFail(size_t count) { + T* newPtr = nullptr; + if (count) { + newPtr = (T*)std::malloc(count * sizeof(T)); + LOG_ALWAYS_FATAL_IF(!newPtr, "failed to malloc %zu bytes", count * sizeof(T)); + } + return newPtr; + } + T* reallocIfCountThrowOnFail(size_t count) { + T* newPtr = nullptr; + if (count) { + newPtr = (T*)std::realloc(fPtr.release(), count * sizeof(T)); + LOG_ALWAYS_FATAL_IF(!newPtr, "failed to realloc %zu bytes", count * sizeof(T)); + } + return newPtr; + } +}; + +} // namespace uirenderer +} // namespace android diff --git a/libs/hwui/utils/Color.cpp b/libs/hwui/utils/Color.cpp index 3afb419f9b8b..bffe137c2bd3 100644 --- a/libs/hwui/utils/Color.cpp +++ b/libs/hwui/utils/Color.cpp @@ -101,6 +101,26 @@ uint32_t ColorTypeToBufferFormat(SkColorType colorType) { return AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM; } } + +SkColorType BufferFormatToColorType(uint32_t format) { + switch (format) { + case AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM: + return kN32_SkColorType; + case AHARDWAREBUFFER_FORMAT_R8G8B8X8_UNORM: + return kN32_SkColorType; + case AHARDWAREBUFFER_FORMAT_R5G6B5_UNORM: + return kRGB_565_SkColorType; + case AHARDWAREBUFFER_FORMAT_R10G10B10A2_UNORM: + return kRGBA_1010102_SkColorType; + case AHARDWAREBUFFER_FORMAT_R16G16B16A16_FLOAT: + return kRGBA_F16_SkColorType; + case AHARDWAREBUFFER_FORMAT_R8_UNORM: + return kAlpha_8_SkColorType; + default: + ALOGV("Unsupported format: %d, return unknown by default", format); + return kUnknown_SkColorType; + } +} #endif namespace { @@ -155,23 +175,12 @@ android_dataspace ColorSpaceToADataSpace(SkColorSpace* colorSpace, SkColorType c skcms_TransferFunction fn; if (!colorSpace->isNumericalTransferFn(&fn)) { - // pq with the default white point - auto rec2020PQ = SkColorSpace::MakeRGB(GetPQSkTransferFunction(), SkNamedGamut::kRec2020); - if (SkColorSpace::Equals(colorSpace, rec2020PQ.get())) { - return HAL_DATASPACE_BT2020_PQ; - } - // standard PQ - rec2020PQ = SkColorSpace::MakeRGB(SkNamedTransferFn::kPQ, SkNamedGamut::kRec2020); - if (SkColorSpace::Equals(colorSpace, rec2020PQ.get())) { + auto res = skcms_TransferFunction_getType(&fn); + if (res == skcms_TFType_PQish) { return HAL_DATASPACE_BT2020_PQ; } - // HLG - const auto hlgFn = GetHLGScaleTransferFunction(); - if (hlgFn.has_value()) { - auto rec2020HLG = SkColorSpace::MakeRGB(hlgFn.value(), SkNamedGamut::kRec2020); - if (SkColorSpace::Equals(colorSpace, rec2020HLG.get())) { - return static_cast<android_dataspace>(HAL_DATASPACE_BT2020_HLG); - } + if (res == skcms_TFType_HLGish) { + return static_cast<android_dataspace>(HAL_DATASPACE_BT2020_HLG); } LOG_ALWAYS_FATAL("Only select non-numerical transfer functions are supported"); } @@ -396,6 +405,27 @@ skcms_TransferFunction GetPQSkTransferFunction(float sdr_white_level) { return fn; } +static skcms_TransferFunction trfn_apply_gain(const skcms_TransferFunction trfn, float gain) { + float pow_gain_ginv = sk_float_pow(gain, 1 / trfn.g); + skcms_TransferFunction result; + result.g = trfn.g; + result.a = trfn.a * pow_gain_ginv; + result.b = trfn.b * pow_gain_ginv; + result.c = trfn.c * gain; + result.d = trfn.d; + result.e = trfn.e * gain; + result.f = trfn.f * gain; + return result; +} + +skcms_TransferFunction GetExtendedTransferFunction(float sdrHdrRatio) { + if (sdrHdrRatio <= 1.f) { + return SkNamedTransferFn::kSRGB; + } + // Scale the transfer by the sdrHdrRatio + return trfn_apply_gain(SkNamedTransferFn::kSRGB, sdrHdrRatio); +} + // Skia skcms' default HLG maps encoded [0, 1] to linear [1, 12] in order to follow ARIB // but LinearEffect expects a decoded [0, 1] range instead to follow Rec 2100. std::optional<skcms_TransferFunction> GetHLGScaleTransferFunction() { diff --git a/libs/hwui/utils/Color.h b/libs/hwui/utils/Color.h index 00f910f45c38..0fd61c7b990b 100644 --- a/libs/hwui/utils/Color.h +++ b/libs/hwui/utils/Color.h @@ -100,6 +100,7 @@ SkImageInfo BufferDescriptionToImageInfo(const AHardwareBuffer_Desc& bufferDesc, sk_sp<SkColorSpace> colorSpace); uint32_t ColorTypeToBufferFormat(SkColorType colorType); +SkColorType BufferFormatToColorType(uint32_t bufferFormat); #endif ANDROID_API sk_sp<SkColorSpace> DataSpaceToColorSpace(android_dataspace dataspace); @@ -129,6 +130,7 @@ struct Lab { Lab sRGBToLab(SkColor color); SkColor LabToSRGB(const Lab& lab, SkAlpha alpha); skcms_TransferFunction GetPQSkTransferFunction(float sdr_white_level = 0.f); +skcms_TransferFunction GetExtendedTransferFunction(float sdrHdrRatio); std::optional<skcms_TransferFunction> GetHLGScaleTransferFunction(); } /* namespace uirenderer */ diff --git a/libs/hwui/utils/PaintUtils.h b/libs/hwui/utils/PaintUtils.h index 94bcb1110e05..f44f9d0fe2d4 100644 --- a/libs/hwui/utils/PaintUtils.h +++ b/libs/hwui/utils/PaintUtils.h @@ -19,6 +19,7 @@ #include <GLES2/gl2.h> #include <utils/Blur.h> +#include <SkBlendMode.h> #include <SkColorFilter.h> #include <SkPaint.h> #include <SkShader.h> diff --git a/libs/incident/Android.bp b/libs/incident/Android.bp index 547d7192090a..a996700eed2a 100644 --- a/libs/incident/Android.bp +++ b/libs/incident/Android.bp @@ -60,6 +60,7 @@ cc_defaults { ":libincident_aidl", "src/IncidentReportArgs.cpp", ], + min_sdk_version: "29", } cc_library_shared { @@ -133,4 +134,6 @@ cc_test { static_libs: [ "libgmock", ], + + host_required: ["compatibility-tradefed"], } 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..c3ad7670d473 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 @@ -43,6 +38,8 @@ MouseCursorController::MouseCursorController(PointerControllerContext& context) : mContext(context) { std::scoped_lock lock(mLock); + mLocked.stylusHoverMode = false; + mLocked.animationFrameIndex = 0; mLocked.lastFrameUpdatedTime = 0; @@ -52,11 +49,10 @@ MouseCursorController::MouseCursorController(PointerControllerContext& context) mLocked.pointerAlpha = 0.0f; // pointer is initially faded mLocked.pointerSprite = mContext.getSpriteController()->createSprite(); mLocked.updatePointerIcon = false; - mLocked.requestedPointerType = mContext.getPolicy()->getDefaultPointerIconId(); + mLocked.requestedPointerType = PointerIconStyle::TYPE_NOT_SPECIFIED; + mLocked.resolvedPointerType = PointerIconStyle::TYPE_NOT_SPECIFIED; mLocked.resourcesLoaded = false; - - mLocked.buttonState = 0; } MouseCursorController::~MouseCursorController() { @@ -65,24 +61,23 @@ MouseCursorController::~MouseCursorController() { mLocked.pointerSprite.clear(); } -bool MouseCursorController::getBounds(float* outMinX, float* outMinY, float* outMaxX, - float* outMaxY) const { +std::optional<FloatRect> MouseCursorController::getBounds() const { std::scoped_lock lock(mLock); - return getBoundsLocked(outMinX, outMinY, outMaxX, outMaxY); + return getBoundsLocked(); } -bool MouseCursorController::getBoundsLocked(float* outMinX, float* outMinY, float* outMaxX, - float* outMaxY) const REQUIRES(mLock) { +std::optional<FloatRect> MouseCursorController::getBoundsLocked() const REQUIRES(mLock) { if (!mLocked.viewport.isValid()) { - return false; + return {}; } - *outMinX = mLocked.viewport.logicalLeft; - *outMinY = mLocked.viewport.logicalTop; - *outMaxX = mLocked.viewport.logicalRight - 1; - *outMaxY = mLocked.viewport.logicalBottom - 1; - return true; + return FloatRect{ + static_cast<float>(mLocked.viewport.logicalLeft), + static_cast<float>(mLocked.viewport.logicalTop), + static_cast<float>(mLocked.viewport.logicalRight - 1), + static_cast<float>(mLocked.viewport.logicalBottom - 1), + }; } void MouseCursorController::move(float deltaX, float deltaY) { @@ -98,22 +93,6 @@ void MouseCursorController::move(float deltaX, float deltaY) { setPositionLocked(mLocked.pointerX + deltaX, mLocked.pointerY + deltaY); } -void MouseCursorController::setButtonState(int32_t buttonState) { -#if DEBUG_MOUSE_CURSOR_UPDATES - ALOGD("Set button state 0x%08x", buttonState); -#endif - std::scoped_lock lock(mLock); - - if (mLocked.buttonState != buttonState) { - mLocked.buttonState = buttonState; - } -} - -int32_t MouseCursorController::getButtonState() const { - std::scoped_lock lock(mLock); - return mLocked.buttonState; -} - void MouseCursorController::setPosition(float x, float y) { #if DEBUG_MOUSE_CURSOR_UPDATES ALOGD("Set pointer position to x=%0.3f, y=%0.3f", x, y); @@ -123,31 +102,19 @@ void MouseCursorController::setPosition(float x, float y) { } void MouseCursorController::setPositionLocked(float x, float y) REQUIRES(mLock) { - float minX, minY, maxX, maxY; - if (getBoundsLocked(&minX, &minY, &maxX, &maxY)) { - if (x <= minX) { - mLocked.pointerX = minX; - } else if (x >= maxX) { - mLocked.pointerX = maxX; - } else { - mLocked.pointerX = x; - } - if (y <= minY) { - mLocked.pointerY = minY; - } else if (y >= maxY) { - mLocked.pointerY = maxY; - } else { - mLocked.pointerY = y; - } - updatePointerLocked(); - } + const auto bounds = getBoundsLocked(); + if (!bounds) return; + + mLocked.pointerX = std::max(bounds->left, std::min(bounds->right, x)); + mLocked.pointerY = std::max(bounds->top, std::min(bounds->bottom, y)); + + updatePointerLocked(); } -void MouseCursorController::getPosition(float* outX, float* outY) const { +FloatPoint MouseCursorController::getPosition() const { std::scoped_lock lock(mLock); - *outX = mLocked.pointerX; - *outY = mLocked.pointerY; + return {mLocked.pointerX, mLocked.pointerY}; } int32_t MouseCursorController::getDisplayId() const { @@ -189,6 +156,15 @@ void MouseCursorController::unfade(PointerControllerInterface::Transition transi } } +void MouseCursorController::setStylusHoverMode(bool stylusHoverMode) { + std::scoped_lock lock(mLock); + + if (mLocked.stylusHoverMode != stylusHoverMode) { + mLocked.stylusHoverMode = stylusHoverMode; + mLocked.updatePointerIcon = true; + } +} + void MouseCursorController::reloadPointerResources(bool getAdditionalMouseResources) { std::scoped_lock lock(mLock); @@ -204,8 +180,7 @@ static void getNonRotatedSize(const DisplayViewport& viewport, int32_t& width, i width = viewport.deviceWidth; height = viewport.deviceHeight; - if (viewport.orientation == DISPLAY_ORIENTATION_90 || - viewport.orientation == DISPLAY_ORIENTATION_270) { + if (viewport.orientation == ui::ROTATION_90 || viewport.orientation == ui::ROTATION_270) { std::swap(width, height); } } @@ -229,10 +204,9 @@ void MouseCursorController::setDisplayViewport(const DisplayViewport& viewport, // Reset cursor position to center if size or display changed. if (oldViewport.displayId != viewport.displayId || oldDisplayWidth != newDisplayWidth || oldDisplayHeight != newDisplayHeight) { - float minX, minY, maxX, maxY; - if (getBoundsLocked(&minX, &minY, &maxX, &maxY)) { - mLocked.pointerX = (minX + maxX) * 0.5f; - mLocked.pointerY = (minY + maxY) * 0.5f; + if (const auto bounds = getBoundsLocked(); bounds) { + mLocked.pointerX = (bounds->left + bounds->right) * 0.5f; + mLocked.pointerY = (bounds->top + bounds->bottom) * 0.5f; // Reload icon resources for density may be changed. loadResourcesLocked(getAdditionalMouseResources); } else { @@ -249,38 +223,42 @@ void MouseCursorController::setDisplayViewport(const DisplayViewport& viewport, // Undo the previous rotation. switch (oldViewport.orientation) { - case DISPLAY_ORIENTATION_90: + case ui::ROTATION_90: temp = x; x = oldViewport.deviceHeight - y; y = temp; break; - case DISPLAY_ORIENTATION_180: + case ui::ROTATION_180: x = oldViewport.deviceWidth - x; y = oldViewport.deviceHeight - y; break; - case DISPLAY_ORIENTATION_270: + case ui::ROTATION_270: temp = x; x = y; y = oldViewport.deviceWidth - temp; break; + case ui::ROTATION_0: + break; } // Perform the new rotation. switch (viewport.orientation) { - case DISPLAY_ORIENTATION_90: + case ui::ROTATION_90: temp = x; x = y; y = viewport.deviceHeight - temp; break; - case DISPLAY_ORIENTATION_180: + case ui::ROTATION_180: x = viewport.deviceWidth - x; y = viewport.deviceHeight - y; break; - case DISPLAY_ORIENTATION_270: + case ui::ROTATION_270: temp = x; x = viewport.deviceWidth - y; y = temp; break; + case ui::ROTATION_0: + break; } // Apply offsets to convert from the pixel center to the pixel top-left corner position @@ -292,7 +270,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 +283,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,8 +318,8 @@ bool MouseCursorController::doFadingAnimationLocked(nsecs_t timestamp) REQUIRES( } bool MouseCursorController::doBitmapAnimationLocked(nsecs_t timestamp) REQUIRES(mLock) { - std::map<int32_t, PointerAnimation>::const_iterator iter = - mLocked.animationResources.find(mLocked.requestedPointerType); + std::map<PointerIconStyle, PointerAnimation>::const_iterator iter = + mLocked.animationResources.find(mLocked.resolvedPointerType); if (iter == mLocked.animationResources.end()) { return false; } @@ -383,14 +361,23 @@ void MouseCursorController::updatePointerLocked() REQUIRES(mLock) { } if (mLocked.updatePointerIcon) { - if (mLocked.requestedPointerType == mContext.getPolicy()->getDefaultPointerIconId()) { + mLocked.resolvedPointerType = mLocked.requestedPointerType; + const PointerIconStyle defaultPointerIconId = + mContext.getPolicy()->getDefaultPointerIconId(); + if (mLocked.resolvedPointerType == PointerIconStyle::TYPE_NOT_SPECIFIED) { + mLocked.resolvedPointerType = mLocked.stylusHoverMode + ? mContext.getPolicy()->getDefaultStylusIconId() + : defaultPointerIconId; + } + + if (mLocked.resolvedPointerType == defaultPointerIconId) { mLocked.pointerSprite->setIcon(mLocked.pointerIcon); } else { - std::map<int32_t, SpriteIcon>::const_iterator iter = - mLocked.additionalMouseResources.find(mLocked.requestedPointerType); + std::map<PointerIconStyle, SpriteIcon>::const_iterator iter = + mLocked.additionalMouseResources.find(mLocked.resolvedPointerType); if (iter != mLocked.additionalMouseResources.end()) { - std::map<int32_t, PointerAnimation>::const_iterator anim_iter = - mLocked.animationResources.find(mLocked.requestedPointerType); + std::map<PointerIconStyle, PointerAnimation>::const_iterator anim_iter = + mLocked.animationResources.find(mLocked.resolvedPointerType); if (anim_iter != mLocked.animationResources.end()) { mLocked.animationFrameIndex = 0; mLocked.lastFrameUpdatedTime = systemTime(SYSTEM_TIME_MONOTONIC); @@ -398,7 +385,7 @@ void MouseCursorController::updatePointerLocked() REQUIRES(mLock) { } mLocked.pointerSprite->setIcon(iter->second); } else { - ALOGW("Can't find the resource for icon id %d", mLocked.requestedPointerType); + ALOGW("Can't find the resource for icon id %d", mLocked.resolvedPointerType); mLocked.pointerSprite->setIcon(mLocked.pointerIcon); } } diff --git a/libs/input/MouseCursorController.h b/libs/input/MouseCursorController.h index c0ab58bd2e7e..00dc0854440e 100644 --- a/libs/input/MouseCursorController.h +++ b/libs/input/MouseCursorController.h @@ -43,18 +43,17 @@ public: MouseCursorController(PointerControllerContext& context); ~MouseCursorController(); - bool getBounds(float* outMinX, float* outMinY, float* outMaxX, float* outMaxY) const; + std::optional<FloatRect> getBounds() const; void move(float deltaX, float deltaY); - void setButtonState(int32_t buttonState); - int32_t getButtonState() const; void setPosition(float x, float y); - void getPosition(float* outX, float* outY) const; + FloatPoint getPosition() const; int32_t getDisplayId() const; void fade(PointerControllerInterface::Transition transition); void unfade(PointerControllerInterface::Transition transition); void setDisplayViewport(const DisplayViewport& viewport, bool getAdditionalMouseResources); + void setStylusHoverMode(bool stylusHoverMode); - void updatePointerIcon(int32_t iconId); + void updatePointerIcon(PointerIconStyle iconId); void setCustomPointerIcon(const SpriteIcon& icon); void reloadPointerResources(bool getAdditionalMouseResources); @@ -74,6 +73,7 @@ private: struct Locked { DisplayViewport viewport; + bool stylusHoverMode; size_t animationFrameIndex; nsecs_t lastFrameUpdatedTime; @@ -88,18 +88,17 @@ 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; - - int32_t buttonState; + PointerIconStyle requestedPointerType; + PointerIconStyle resolvedPointerType; bool animating{false}; } mLocked GUARDED_BY(mLock); - bool getBoundsLocked(float* outMinX, float* outMinY, float* outMaxX, float* outMaxY) const; + std::optional<FloatRect> getBoundsLocked() const; void setPositionLocked(float x, float y); void updatePointerLocked(); diff --git a/libs/input/PointerController.cpp b/libs/input/PointerController.cpp index 10ea6512c724..88e351963148 100644 --- a/libs/input/PointerController.cpp +++ b/libs/input/PointerController.cpp @@ -22,10 +22,18 @@ #include <SkBlendMode.h> #include <SkCanvas.h> #include <SkColor.h> +#include <android-base/stringprintf.h> #include <android-base/thread_annotations.h> +#include <ftl/enum.h> + +#include <mutex> #include "PointerControllerContext.h" +#define INDENT " " +#define INDENT2 " " +#define INDENT3 " " + namespace android { namespace { @@ -106,16 +114,15 @@ PointerController::PointerController(const sp<PointerControllerPolicyInterface>& PointerController::~PointerController() { mDisplayInfoListener->onPointerControllerDestroyed(); mUnregisterWindowInfosListener(mDisplayInfoListener); - mContext.getPolicy()->onPointerDisplayIdChanged(ADISPLAY_ID_NONE, 0, 0); + mContext.getPolicy()->onPointerDisplayIdChanged(ADISPLAY_ID_NONE, FloatPoint{0, 0}); } std::mutex& PointerController::getLock() const { return mDisplayInfoListener->mLock; } -bool PointerController::getBounds(float* outMinX, float* outMinY, float* outMaxX, - float* outMaxY) const { - return mCursorController.getBounds(outMinX, outMinY, outMaxX, outMaxY); +std::optional<FloatRect> PointerController::getBounds() const { + return mCursorController.getBounds(); } void PointerController::move(float deltaX, float deltaY) { @@ -129,14 +136,6 @@ void PointerController::move(float deltaX, float deltaY) { mCursorController.move(transformed.x, transformed.y); } -void PointerController::setButtonState(int32_t buttonState) { - mCursorController.setButtonState(buttonState); -} - -int32_t PointerController::getButtonState() const { - return mCursorController.getButtonState(); -} - void PointerController::setPosition(float x, float y) { const int32_t displayId = mCursorController.getDisplayId(); vec2 transformed; @@ -148,15 +147,13 @@ void PointerController::setPosition(float x, float y) { mCursorController.setPosition(transformed.x, transformed.y); } -void PointerController::getPosition(float* outX, float* outY) const { +FloatPoint PointerController::getPosition() const { const int32_t displayId = mCursorController.getDisplayId(); - mCursorController.getPosition(outX, outY); + const auto p = mCursorController.getPosition(); { std::scoped_lock lock(getLock()); const auto& transform = getTransformForDisplayLocked(displayId); - const auto xy = transform.inverse().transform(*outX, *outY); - *outX = xy.x; - *outY = xy.y; + return FloatPoint{transform.inverse().transform(p.x, p.y)}; } } @@ -187,7 +184,11 @@ void PointerController::setPresentation(Presentation presentation) { return; } - if (presentation == Presentation::POINTER) { + if (presentation == Presentation::POINTER || presentation == Presentation::STYLUS_HOVER) { + // For now, we support stylus hover using the mouse cursor implementation. + // TODO: Add proper support for stylus hover icons. + mCursorController.setStylusHoverMode(presentation == Presentation::STYLUS_HOVER); + mCursorController.getAdditionalMouseResources(); clearSpotsLocked(); } @@ -223,7 +224,7 @@ void PointerController::clearSpots() { } void PointerController::clearSpotsLocked() { - for (auto& [displayID, spotController] : mLocked.spotControllers) { + for (auto& [displayId, spotController] : mLocked.spotControllers) { spotController.clearSpots(); } } @@ -235,13 +236,14 @@ void PointerController::setInactivityTimeout(InactivityTimeout inactivityTimeout void PointerController::reloadPointerResources() { std::scoped_lock lock(getLock()); - for (auto& [displayID, spotController] : mLocked.spotControllers) { + for (auto& [displayId, spotController] : mLocked.spotControllers) { spotController.reloadSpotResources(); } if (mCursorController.resourcesLoaded()) { bool getAdditionalMouseResources = false; - if (mLocked.presentation == PointerController::Presentation::POINTER) { + if (mLocked.presentation == PointerController::Presentation::POINTER || + mLocked.presentation == PointerController::Presentation::STYLUS_HOVER) { getAdditionalMouseResources = true; } mCursorController.reloadPointerResources(getAdditionalMouseResources); @@ -249,22 +251,35 @@ void PointerController::reloadPointerResources() { } void PointerController::setDisplayViewport(const DisplayViewport& viewport) { - std::scoped_lock lock(getLock()); + struct PointerDisplayChangeArgs { + int32_t displayId; + FloatPoint cursorPosition; + }; + std::optional<PointerDisplayChangeArgs> pointerDisplayChanged; - bool getAdditionalMouseResources = false; - if (mLocked.presentation == PointerController::Presentation::POINTER) { - getAdditionalMouseResources = true; - } - mCursorController.setDisplayViewport(viewport, getAdditionalMouseResources); - if (viewport.displayId != mLocked.pointerDisplayId) { - float xPos, yPos; - mCursorController.getPosition(&xPos, &yPos); - mContext.getPolicy()->onPointerDisplayIdChanged(viewport.displayId, xPos, yPos); - mLocked.pointerDisplayId = viewport.displayId; + { // acquire lock + std::scoped_lock lock(getLock()); + + bool getAdditionalMouseResources = false; + if (mLocked.presentation == PointerController::Presentation::POINTER || + mLocked.presentation == PointerController::Presentation::STYLUS_HOVER) { + getAdditionalMouseResources = true; + } + mCursorController.setDisplayViewport(viewport, getAdditionalMouseResources); + if (viewport.displayId != mLocked.pointerDisplayId) { + mLocked.pointerDisplayId = viewport.displayId; + pointerDisplayChanged = {viewport.displayId, mCursorController.getPosition()}; + } + } // release lock + + if (pointerDisplayChanged) { + // Notify the policy without holding the pointer controller lock. + mContext.getPolicy()->onPointerDisplayIdChanged(pointerDisplayChanged->displayId, + pointerDisplayChanged->cursorPosition); } } -void PointerController::updatePointerIcon(int32_t iconId) { +void PointerController::updatePointerIcon(PointerIconStyle iconId) { std::scoped_lock lock(getLock()); mCursorController.updatePointerIcon(iconId); } @@ -286,13 +301,13 @@ void PointerController::onDisplayViewportsUpdated(std::vector<DisplayViewport>& std::scoped_lock lock(getLock()); for (auto it = mLocked.spotControllers.begin(); it != mLocked.spotControllers.end();) { - int32_t displayID = it->first; - if (!displayIdSet.count(displayID)) { + int32_t displayId = it->first; + if (!displayIdSet.count(displayId)) { /* * Ensures that an in-progress animation won't dereference * a null pointer to TouchSpotController. */ - mContext.removeAnimationCallback(displayID); + mContext.removeAnimationCallback(displayId); it = mLocked.spotControllers.erase(it); } else { ++it; @@ -313,4 +328,20 @@ const ui::Transform& PointerController::getTransformForDisplayLocked(int display return it != di.end() ? it->transform : kIdentityTransform; } +void PointerController::dump(std::string& dump) { + dump += INDENT "PointerController:\n"; + std::scoped_lock lock(getLock()); + dump += StringPrintf(INDENT2 "Presentation: %s\n", + ftl::enum_string(mLocked.presentation).c_str()); + dump += StringPrintf(INDENT2 "Pointer Display ID: %" PRIu32 "\n", mLocked.pointerDisplayId); + dump += StringPrintf(INDENT2 "Viewports:\n"); + for (const auto& info : mLocked.mDisplayInfos) { + info.dump(dump, INDENT3); + } + dump += INDENT2 "Spot Controllers:\n"; + for (const auto& [_, spotController] : mLocked.spotControllers) { + spotController.dump(dump, INDENT3); + } +} + } // namespace android diff --git a/libs/input/PointerController.h b/libs/input/PointerController.h index eab030f71e1a..ca14b6e9bfdc 100644 --- a/libs/input/PointerController.h +++ b/libs/input/PointerController.h @@ -27,6 +27,7 @@ #include <map> #include <memory> +#include <string> #include <vector> #include "MouseCursorController.h" @@ -49,23 +50,21 @@ public: ~PointerController() override; - virtual bool getBounds(float* outMinX, float* outMinY, float* outMaxX, float* outMaxY) const; - virtual void move(float deltaX, float deltaY); - virtual void setButtonState(int32_t buttonState); - virtual int32_t getButtonState() const; - virtual void setPosition(float x, float y); - virtual void getPosition(float* outX, float* outY) const; - virtual int32_t getDisplayId() const; - virtual void fade(Transition transition); - virtual void unfade(Transition transition); - virtual void setDisplayViewport(const DisplayViewport& viewport); - - virtual void setPresentation(Presentation presentation); - virtual void setSpots(const PointerCoords* spotCoords, const uint32_t* spotIdToIndex, - BitSet32 spotIdBits, int32_t displayId); - virtual void clearSpots(); - - void updatePointerIcon(int32_t iconId); + std::optional<FloatRect> getBounds() const override; + void move(float deltaX, float deltaY) override; + void setPosition(float x, float y) override; + FloatPoint getPosition() const override; + int32_t getDisplayId() const override; + void fade(Transition transition) override; + void unfade(Transition transition) override; + void setDisplayViewport(const DisplayViewport& viewport) override; + + void setPresentation(Presentation presentation) override; + void setSpots(const PointerCoords* spotCoords, const uint32_t* spotIdToIndex, + BitSet32 spotIdBits, int32_t displayId) override; + void clearSpots() override; + + void updatePointerIcon(PointerIconStyle iconId); void setCustomPointerIcon(const SpriteIcon& icon); void setInactivityTimeout(InactivityTimeout inactivityTimeout); void doInactivityTimeout(); @@ -75,6 +74,8 @@ public: void onDisplayInfosChangedLocked(const std::vector<gui::DisplayInfo>& displayInfos) REQUIRES(getLock()); + void dump(std::string& dump); + protected: using WindowListenerConsumer = std::function<void(const sp<android::gui::WindowInfosListener>&)>; diff --git a/libs/input/PointerControllerContext.h b/libs/input/PointerControllerContext.h index c2bc1e020279..f6f5d3bc51bd 100644 --- a/libs/input/PointerControllerContext.h +++ b/libs/input/PointerControllerContext.h @@ -75,11 +75,13 @@ 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; - virtual void onPointerDisplayIdChanged(int32_t displayId, float xPos, float yPos) = 0; + std::map<PointerIconStyle, SpriteIcon>* outResources, + std::map<PointerIconStyle, PointerAnimation>* outAnimationResources, + int32_t displayId) = 0; + virtual PointerIconStyle getDefaultPointerIconId() = 0; + virtual PointerIconStyle getDefaultStylusIconId() = 0; + virtual PointerIconStyle getCustomPointerIconId() = 0; + virtual void onPointerDisplayIdChanged(int32_t displayId, const FloatPoint& position) = 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..d9fe5996bcff 100644 --- a/libs/input/TouchSpotController.cpp +++ b/libs/input/TouchSpotController.cpp @@ -21,13 +21,14 @@ #include "TouchSpotController.h" +#include <android-base/stringprintf.h> +#include <input/PrintTools.h> #include <log/log.h> -#include <SkBitmap.h> -#include <SkBlendMode.h> -#include <SkCanvas.h> -#include <SkColor.h> -#include <SkPaint.h> +#include <mutex> + +#define INDENT " " +#define INDENT2 " " namespace { // Time to spend fading out the spot completely. @@ -59,6 +60,12 @@ void TouchSpotController::Spot::updateSprite(const SpriteIcon* icon, float x, fl } } +void TouchSpotController::Spot::dump(std::string& out, const char* prefix) const { + out += prefix; + base::StringAppendF(&out, "Spot{id=%" PRIx32 ", alpha=%f, scale=%f, pos=[%f, %f]}\n", id, alpha, + scale, x, y); +} + // --- TouchSpotController --- TouchSpotController::TouchSpotController(int32_t displayId, PointerControllerContext& context) @@ -261,4 +268,22 @@ void TouchSpotController::startAnimationLocked() REQUIRES(mLock) { mContext.addAnimationCallback(mDisplayId, func); } +void TouchSpotController::dump(std::string& out, const char* prefix) const { + using base::StringAppendF; + out += prefix; + out += "SpotController:\n"; + out += prefix; + StringAppendF(&out, INDENT "DisplayId: %" PRId32 "\n", mDisplayId); + std::scoped_lock lock(mLock); + out += prefix; + StringAppendF(&out, INDENT "Animating: %s\n", toString(mLocked.animating)); + out += prefix; + out += INDENT "Spots:\n"; + std::string spotPrefix = prefix; + spotPrefix += INDENT2; + for (const auto& spot : mLocked.displaySpots) { + spot->dump(out, spotPrefix.c_str()); + } +} + } // namespace android diff --git a/libs/input/TouchSpotController.h b/libs/input/TouchSpotController.h index 703de3603f48..5bbc75d9570b 100644 --- a/libs/input/TouchSpotController.h +++ b/libs/input/TouchSpotController.h @@ -38,6 +38,8 @@ public: void reloadSpotResources(); bool doAnimations(nsecs_t timestamp); + void dump(std::string& out, const char* prefix = "") const; + private: struct Spot { static const uint32_t INVALID_ID = 0xffffffff; @@ -58,6 +60,7 @@ private: mLastIcon(nullptr) {} void updateSprite(const SpriteIcon* icon, float x, float y, int32_t displayId); + void dump(std::string& out, const char* prefix = "") const; private: const SpriteIcon* mLastIcon; diff --git a/libs/input/tests/PointerController_test.cpp b/libs/input/tests/PointerController_test.cpp index f9752ed155df..2378d42793a1 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 { @@ -34,12 +35,12 @@ enum TestCursorType { CURSOR_TYPE_ANCHOR, CURSOR_TYPE_ADDITIONAL, CURSOR_TYPE_ADDITIONAL_ANIM, + CURSOR_TYPE_STYLUS, CURSOR_TYPE_CUSTOM = -1, }; using ::testing::AllOf; using ::testing::Field; -using ::testing::Mock; using ::testing::NiceMock; using ::testing::Return; using ::testing::Test; @@ -52,11 +53,14 @@ 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 onPointerDisplayIdChanged(int32_t displayId, float xPos, float yPos) override; + virtual void loadAdditionalMouseResources( + std::map<PointerIconStyle, SpriteIcon>* outResources, + std::map<PointerIconStyle, PointerAnimation>* outAnimationResources, + int32_t displayId) override; + virtual PointerIconStyle getDefaultPointerIconId() override; + virtual PointerIconStyle getDefaultStylusIconId() override; + virtual PointerIconStyle getCustomPointerIconId() override; + virtual void onPointerDisplayIdChanged(int32_t displayId, const FloatPoint& position) override; bool allResourcesAreLoaded(); bool noResourcesAreLoaded(); @@ -85,34 +89,42 @@ 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; + + // CURSOR_TYPE_STYLUS doesn't have animation resource. + cursorType = CURSOR_TYPE_STYLUS; + loadPointerIconForType(&icon, cursorType); + (*outResources)[static_cast<PointerIconStyle>(cursorType)] = icon; additionalMouseResourcesLoaded = true; } -int32_t MockPointerControllerPolicyInterface::getDefaultPointerIconId() { - return CURSOR_TYPE_DEFAULT; +PointerIconStyle MockPointerControllerPolicyInterface::getDefaultPointerIconId() { + return static_cast<PointerIconStyle>(CURSOR_TYPE_DEFAULT); +} + +PointerIconStyle MockPointerControllerPolicyInterface::getDefaultStylusIconId() { + return static_cast<PointerIconStyle>(CURSOR_TYPE_STYLUS); } -int32_t MockPointerControllerPolicyInterface::getCustomPointerIconId() { - return CURSOR_TYPE_CUSTOM; +PointerIconStyle MockPointerControllerPolicyInterface::getCustomPointerIconId() { + return static_cast<PointerIconStyle>(CURSOR_TYPE_CUSTOM); } bool MockPointerControllerPolicyInterface::allResourcesAreLoaded() { @@ -124,15 +136,15 @@ 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; } void MockPointerControllerPolicyInterface::onPointerDisplayIdChanged(int32_t displayId, - float /*xPos*/, - float /*yPos*/) { + const FloatPoint& /*position*/ +) { latestPointerDisplayId = displayId; } @@ -205,11 +217,26 @@ 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(); +} + +TEST_F(PointerControllerTest, useStylusTypeForStylusHover) { + ensureDisplayViewportIsSet(); + mPointerController->setPresentation(PointerController::Presentation::STYLUS_HOVER); + mPointerController->unfade(PointerController::Transition::IMMEDIATE); + std::pair<float, float> hotspot = getHotSpotCoordinatesForType(CURSOR_TYPE_STYLUS); + EXPECT_CALL(*mPointerSprite, setVisible(true)); + EXPECT_CALL(*mPointerSprite, setAlpha(1.0f)); + EXPECT_CALL(*mPointerSprite, + setIcon(AllOf(Field(&SpriteIcon::style, + static_cast<PointerIconStyle>(CURSOR_TYPE_STYLUS)), + Field(&SpriteIcon::hotSpotX, hotspot.first), + Field(&SpriteIcon::hotSpotY, hotspot.second)))); mPointerController->reloadPointerResources(); } @@ -222,12 +249,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 +265,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); } diff --git a/libs/securebox/Android.bp b/libs/securebox/Android.bp new file mode 100644 index 000000000000..a29c03cfdcca --- /dev/null +++ b/libs/securebox/Android.bp @@ -0,0 +1,8 @@ +package { + default_applicable_licenses: ["frameworks_base_license"], +} + +java_library { + name: "securebox", + srcs: ["src/**/*.java"], +} diff --git a/libs/securebox/OWNERS b/libs/securebox/OWNERS new file mode 100644 index 000000000000..e160799aa10d --- /dev/null +++ b/libs/securebox/OWNERS @@ -0,0 +1 @@ +include /services/core/java/com/android/server/locksettings/recoverablekeystore/OWNERS diff --git a/libs/securebox/src/com/android/security/SecureBox.java b/libs/securebox/src/com/android/security/SecureBox.java new file mode 100644 index 000000000000..0ebaff4ac8e5 --- /dev/null +++ b/libs/securebox/src/com/android/security/SecureBox.java @@ -0,0 +1,461 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.security; + +import android.annotation.Nullable; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.ArrayUtils; + +import java.math.BigInteger; +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.interfaces.ECPublicKey; +import java.security.spec.ECFieldFp; +import java.security.spec.ECGenParameterSpec; +import java.security.spec.ECParameterSpec; +import java.security.spec.ECPoint; +import java.security.spec.ECPublicKeySpec; +import java.security.spec.EllipticCurve; +import java.security.spec.InvalidKeySpecException; +import java.util.Arrays; + +import javax.crypto.AEADBadTagException; +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.KeyAgreement; +import javax.crypto.Mac; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.SecretKey; +import javax.crypto.spec.GCMParameterSpec; +import javax.crypto.spec.SecretKeySpec; + +/** + * Implementation of the SecureBox v2 crypto functions. + * + * <p>Securebox v2 provides a simple interface to perform encryptions by using any of the following + * credential types: + * + * <ul> + * <li>A public key owned by the recipient, + * <li>A secret shared between the sender and the recipient, or + * <li>Both a recipient's public key and a shared secret. + * </ul> + * + * @hide + */ +public class SecureBox { + + private static final byte[] VERSION = new byte[] {(byte) 0x02, 0}; // LITTLE_ENDIAN_TWO_BYTES(2) + private static final byte[] HKDF_SALT = + ArrayUtils.concat("SECUREBOX".getBytes(StandardCharsets.UTF_8), VERSION); + private static final byte[] HKDF_INFO_WITH_PUBLIC_KEY = + "P256 HKDF-SHA-256 AES-128-GCM".getBytes(StandardCharsets.UTF_8); + private static final byte[] HKDF_INFO_WITHOUT_PUBLIC_KEY = + "SHARED HKDF-SHA-256 AES-128-GCM".getBytes(StandardCharsets.UTF_8); + private static final byte[] CONSTANT_01 = {(byte) 0x01}; + private static final byte[] EMPTY_BYTE_ARRAY = new byte[0]; + private static final byte EC_PUBLIC_KEY_PREFIX = (byte) 0x04; + + private static final String CIPHER_ALG = "AES"; + private static final String EC_ALG = "EC"; + private static final String EC_P256_COMMON_NAME = "secp256r1"; + private static final String EC_P256_OPENSSL_NAME = "prime256v1"; + private static final String ENC_ALG = "AES/GCM/NoPadding"; + private static final String KA_ALG = "ECDH"; + private static final String MAC_ALG = "HmacSHA256"; + + private static final int EC_COORDINATE_LEN_BYTES = 32; + private static final int EC_PUBLIC_KEY_LEN_BYTES = 2 * EC_COORDINATE_LEN_BYTES + 1; + private static final int GCM_NONCE_LEN_BYTES = 12; + private static final int GCM_KEY_LEN_BYTES = 16; + private static final int GCM_TAG_LEN_BYTES = 16; + + private static final BigInteger BIG_INT_02 = BigInteger.valueOf(2); + + private enum AesGcmOperation { + ENCRYPT, + DECRYPT + } + + // Parameters for the NIST P-256 curve y^2 = x^3 + ax + b (mod p) + private static final BigInteger EC_PARAM_P = + new BigInteger("ffffffff00000001000000000000000000000000ffffffffffffffffffffffff", 16); + private static final BigInteger EC_PARAM_A = EC_PARAM_P.subtract(new BigInteger("3")); + private static final BigInteger EC_PARAM_B = + new BigInteger("5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f63bce3c3e27d2604b", 16); + + @VisibleForTesting static final ECParameterSpec EC_PARAM_SPEC; + + static { + EllipticCurve curveSpec = + new EllipticCurve(new ECFieldFp(EC_PARAM_P), EC_PARAM_A, EC_PARAM_B); + ECPoint generator = + new ECPoint( + new BigInteger( + "6b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296", + 16), + new BigInteger( + "4fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ececbb6406837bf51f5", + 16)); + BigInteger generatorOrder = + new BigInteger( + "ffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551", 16); + EC_PARAM_SPEC = new ECParameterSpec(curveSpec, generator, generatorOrder, /* cofactor */ 1); + } + + private SecureBox() {} + + /** + * Randomly generates a public-key pair that can be used for the functions {@link #encrypt} and + * {@link #decrypt}. + * + * @return the randomly generated public-key pair + * @throws NoSuchAlgorithmException if the underlying crypto algorithm is not supported + * @hide + */ + public static KeyPair genKeyPair() throws NoSuchAlgorithmException { + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(EC_ALG); + try { + // Try using the OpenSSL provider first + keyPairGenerator.initialize(new ECGenParameterSpec(EC_P256_OPENSSL_NAME)); + return keyPairGenerator.generateKeyPair(); + } catch (InvalidAlgorithmParameterException ex) { + // Try another name for NIST P-256 + } + try { + keyPairGenerator.initialize(new ECGenParameterSpec(EC_P256_COMMON_NAME)); + return keyPairGenerator.generateKeyPair(); + } catch (InvalidAlgorithmParameterException ex) { + throw new NoSuchAlgorithmException("Unable to find the NIST P-256 curve", ex); + } + } + + /** + * Encrypts {@code payload} by using {@code theirPublicKey} and/or {@code sharedSecret}. At + * least one of {@code theirPublicKey} and {@code sharedSecret} must be non-null, and an empty + * {@code sharedSecret} is equivalent to null. + * + * <p>Note that {@code header} will be authenticated (but not encrypted) together with {@code + * payload}, and the same {@code header} has to be provided for {@link #decrypt}. + * + * @param theirPublicKey the recipient's public key, or null if the payload is to be encrypted + * only with the shared secret + * @param sharedSecret the secret shared between the sender and the recipient, or null if the + * payload is to be encrypted only with the recipient's public key + * @param header the data that will be authenticated with {@code payload} but not encrypted, or + * null if the data is empty + * @param payload the data to be encrypted, or null if the data is empty + * @return the encrypted payload + * @throws NoSuchAlgorithmException if any underlying crypto algorithm is not supported + * @throws InvalidKeyException if the provided key is invalid for underlying crypto algorithms + * @hide + */ + public static byte[] encrypt( + @Nullable PublicKey theirPublicKey, + @Nullable byte[] sharedSecret, + @Nullable byte[] header, + @Nullable byte[] payload) + throws NoSuchAlgorithmException, InvalidKeyException { + sharedSecret = emptyByteArrayIfNull(sharedSecret); + if (theirPublicKey == null && sharedSecret.length == 0) { + throw new IllegalArgumentException("Both the public key and shared secret are empty"); + } + header = emptyByteArrayIfNull(header); + payload = emptyByteArrayIfNull(payload); + + KeyPair senderKeyPair; + byte[] dhSecret; + byte[] hkdfInfo; + if (theirPublicKey == null) { + senderKeyPair = null; + dhSecret = EMPTY_BYTE_ARRAY; + hkdfInfo = HKDF_INFO_WITHOUT_PUBLIC_KEY; + } else { + senderKeyPair = genKeyPair(); + dhSecret = dhComputeSecret(senderKeyPair.getPrivate(), theirPublicKey); + hkdfInfo = HKDF_INFO_WITH_PUBLIC_KEY; + } + + byte[] randNonce = genRandomNonce(); + byte[] keyingMaterial = ArrayUtils.concat(dhSecret, sharedSecret); + SecretKey encryptionKey = hkdfDeriveKey(keyingMaterial, HKDF_SALT, hkdfInfo); + byte[] ciphertext = aesGcmEncrypt(encryptionKey, randNonce, payload, header); + if (senderKeyPair == null) { + return ArrayUtils.concat(VERSION, randNonce, ciphertext); + } else { + return ArrayUtils.concat( + VERSION, encodePublicKey(senderKeyPair.getPublic()), randNonce, ciphertext); + } + } + + /** + * Decrypts {@code encryptedPayload} by using {@code ourPrivateKey} and/or {@code sharedSecret}. + * At least one of {@code ourPrivateKey} and {@code sharedSecret} must be non-null, and an empty + * {@code sharedSecret} is equivalent to null. + * + * <p>Note that {@code header} should be the same data used for {@link #encrypt}, which is + * authenticated (but not encrypted) together with {@code payload}; otherwise, an {@code + * AEADBadTagException} will be thrown. + * + * @param ourPrivateKey the recipient's private key, or null if the payload was encrypted only + * with the shared secret + * @param sharedSecret the secret shared between the sender and the recipient, or null if the + * payload was encrypted only with the recipient's public key + * @param header the data that was authenticated with the original payload but not encrypted, or + * null if the data is empty + * @param encryptedPayload the data to be decrypted + * @return the original payload that was encrypted + * @throws NoSuchAlgorithmException if any underlying crypto algorithm is not supported + * @throws InvalidKeyException if the provided key is invalid for underlying crypto algorithms + * @throws AEADBadTagException if the authentication tag contained in {@code encryptedPayload} + * cannot be validated, or if the payload is not a valid SecureBox V2 payload. + * @hide + */ + public static byte[] decrypt( + @Nullable PrivateKey ourPrivateKey, + @Nullable byte[] sharedSecret, + @Nullable byte[] header, + byte[] encryptedPayload) + throws NoSuchAlgorithmException, InvalidKeyException, AEADBadTagException { + sharedSecret = emptyByteArrayIfNull(sharedSecret); + if (ourPrivateKey == null && sharedSecret.length == 0) { + throw new IllegalArgumentException("Both the private key and shared secret are empty"); + } + header = emptyByteArrayIfNull(header); + if (encryptedPayload == null) { + throw new NullPointerException("Encrypted payload must not be null."); + } + + ByteBuffer ciphertextBuffer = ByteBuffer.wrap(encryptedPayload); + byte[] version = readEncryptedPayload(ciphertextBuffer, VERSION.length); + if (!Arrays.equals(version, VERSION)) { + throw new AEADBadTagException("The payload was not encrypted by SecureBox v2"); + } + + byte[] senderPublicKeyBytes; + byte[] dhSecret; + byte[] hkdfInfo; + if (ourPrivateKey == null) { + dhSecret = EMPTY_BYTE_ARRAY; + hkdfInfo = HKDF_INFO_WITHOUT_PUBLIC_KEY; + } else { + senderPublicKeyBytes = readEncryptedPayload(ciphertextBuffer, EC_PUBLIC_KEY_LEN_BYTES); + dhSecret = dhComputeSecret(ourPrivateKey, decodePublicKey(senderPublicKeyBytes)); + hkdfInfo = HKDF_INFO_WITH_PUBLIC_KEY; + } + + byte[] randNonce = readEncryptedPayload(ciphertextBuffer, GCM_NONCE_LEN_BYTES); + byte[] ciphertext = readEncryptedPayload(ciphertextBuffer, ciphertextBuffer.remaining()); + byte[] keyingMaterial = ArrayUtils.concat(dhSecret, sharedSecret); + SecretKey decryptionKey = hkdfDeriveKey(keyingMaterial, HKDF_SALT, hkdfInfo); + return aesGcmDecrypt(decryptionKey, randNonce, ciphertext, header); + } + + private static byte[] readEncryptedPayload(ByteBuffer buffer, int length) + throws AEADBadTagException { + byte[] output = new byte[length]; + try { + buffer.get(output); + } catch (BufferUnderflowException ex) { + throw new AEADBadTagException("The encrypted payload is too short"); + } + return output; + } + + private static byte[] dhComputeSecret(PrivateKey ourPrivateKey, PublicKey theirPublicKey) + throws NoSuchAlgorithmException, InvalidKeyException { + KeyAgreement agreement = KeyAgreement.getInstance(KA_ALG); + try { + agreement.init(ourPrivateKey); + } catch (RuntimeException ex) { + // Rethrow the RuntimeException as InvalidKeyException + throw new InvalidKeyException(ex); + } + agreement.doPhase(theirPublicKey, /*lastPhase=*/ true); + return agreement.generateSecret(); + } + + /** Derives a 128-bit AES key. */ + private static SecretKey hkdfDeriveKey(byte[] secret, byte[] salt, byte[] info) + throws NoSuchAlgorithmException { + Mac mac = Mac.getInstance(MAC_ALG); + try { + mac.init(new SecretKeySpec(salt, MAC_ALG)); + } catch (InvalidKeyException ex) { + // This should never happen + throw new RuntimeException(ex); + } + byte[] pseudorandomKey = mac.doFinal(secret); + + try { + mac.init(new SecretKeySpec(pseudorandomKey, MAC_ALG)); + } catch (InvalidKeyException ex) { + // This should never happen + throw new RuntimeException(ex); + } + mac.update(info); + // Hashing just one block will yield 256 bits, which is enough to construct the AES key + byte[] hkdfOutput = mac.doFinal(CONSTANT_01); + + return new SecretKeySpec(Arrays.copyOf(hkdfOutput, GCM_KEY_LEN_BYTES), CIPHER_ALG); + } + + private static byte[] aesGcmEncrypt(SecretKey key, byte[] nonce, byte[] plaintext, byte[] aad) + throws NoSuchAlgorithmException, InvalidKeyException { + try { + return aesGcmInternal(AesGcmOperation.ENCRYPT, key, nonce, plaintext, aad); + } catch (AEADBadTagException ex) { + // This should never happen + throw new RuntimeException(ex); + } + } + + private static byte[] aesGcmDecrypt(SecretKey key, byte[] nonce, byte[] ciphertext, byte[] aad) + throws NoSuchAlgorithmException, InvalidKeyException, AEADBadTagException { + return aesGcmInternal(AesGcmOperation.DECRYPT, key, nonce, ciphertext, aad); + } + + private static byte[] aesGcmInternal( + AesGcmOperation operation, SecretKey key, byte[] nonce, byte[] text, byte[] aad) + throws NoSuchAlgorithmException, InvalidKeyException, AEADBadTagException { + Cipher cipher; + try { + cipher = Cipher.getInstance(ENC_ALG); + } catch (NoSuchPaddingException ex) { + // This should never happen because AES-GCM doesn't use padding + throw new RuntimeException(ex); + } + GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LEN_BYTES * 8, nonce); + try { + if (operation == AesGcmOperation.DECRYPT) { + cipher.init(Cipher.DECRYPT_MODE, key, spec); + } else { + cipher.init(Cipher.ENCRYPT_MODE, key, spec); + } + } catch (InvalidAlgorithmParameterException ex) { + // This should never happen + throw new RuntimeException(ex); + } + try { + cipher.updateAAD(aad); + return cipher.doFinal(text); + } catch (AEADBadTagException ex) { + // Catch and rethrow AEADBadTagException first because it's a subclass of + // BadPaddingException + throw ex; + } catch (IllegalBlockSizeException | BadPaddingException ex) { + // This should never happen because AES-GCM can handle inputs of any length without + // padding + throw new RuntimeException(ex); + } + } + + /** + * Encodes public key in format expected by the secure hardware module. This is used as part + * of the vault params. + * + * @param publicKey The public key. + * @return The key packed into a 65-byte array. + */ + public static byte[] encodePublicKey(PublicKey publicKey) { + ECPoint point = ((ECPublicKey) publicKey).getW(); + byte[] x = point.getAffineX().toByteArray(); + byte[] y = point.getAffineY().toByteArray(); + + byte[] output = new byte[EC_PUBLIC_KEY_LEN_BYTES]; + // The order of arraycopy() is important, because the coordinates may have a one-byte + // leading 0 for the sign bit of two's complement form + System.arraycopy(y, 0, output, EC_PUBLIC_KEY_LEN_BYTES - y.length, y.length); + System.arraycopy(x, 0, output, 1 + EC_COORDINATE_LEN_BYTES - x.length, x.length); + output[0] = EC_PUBLIC_KEY_PREFIX; + return output; + } + + /** + * Decodes byte[] encoded public key. + * + * @param keyBytes encoded public key + * @return the public key + */ + public static PublicKey decodePublicKey(byte[] keyBytes) + throws NoSuchAlgorithmException, InvalidKeyException { + BigInteger x = + new BigInteger( + /*signum=*/ 1, + Arrays.copyOfRange(keyBytes, 1, 1 + EC_COORDINATE_LEN_BYTES)); + BigInteger y = + new BigInteger( + /*signum=*/ 1, + Arrays.copyOfRange( + keyBytes, 1 + EC_COORDINATE_LEN_BYTES, EC_PUBLIC_KEY_LEN_BYTES)); + + // Checks if the point is indeed on the P-256 curve for security considerations + validateEcPoint(x, y); + + KeyFactory keyFactory = KeyFactory.getInstance(EC_ALG); + try { + return keyFactory.generatePublic(new ECPublicKeySpec(new ECPoint(x, y), EC_PARAM_SPEC)); + } catch (InvalidKeySpecException ex) { + // This should never happen + throw new RuntimeException(ex); + } + } + + private static void validateEcPoint(BigInteger x, BigInteger y) throws InvalidKeyException { + if (x.compareTo(EC_PARAM_P) >= 0 + || y.compareTo(EC_PARAM_P) >= 0 + || x.signum() == -1 + || y.signum() == -1) { + throw new InvalidKeyException("Point lies outside of the expected curve"); + } + + // Points on the curve satisfy y^2 = x^3 + ax + b (mod p) + BigInteger lhs = y.modPow(BIG_INT_02, EC_PARAM_P); + BigInteger rhs = + x.modPow(BIG_INT_02, EC_PARAM_P) // x^2 + .add(EC_PARAM_A) // x^2 + a + .mod(EC_PARAM_P) // This will speed up the next multiplication + .multiply(x) // (x^2 + a) * x = x^3 + ax + .add(EC_PARAM_B) // x^3 + ax + b + .mod(EC_PARAM_P); + if (!lhs.equals(rhs)) { + throw new InvalidKeyException("Point lies outside of the expected curve"); + } + } + + private static byte[] genRandomNonce() throws NoSuchAlgorithmException { + byte[] nonce = new byte[GCM_NONCE_LEN_BYTES]; + new SecureRandom().nextBytes(nonce); + return nonce; + } + + private static byte[] emptyByteArrayIfNull(@Nullable byte[] input) { + return input == null ? EMPTY_BYTE_ARRAY : input; + } +} diff --git a/libs/securebox/tests/Android.bp b/libs/securebox/tests/Android.bp new file mode 100644 index 000000000000..7df546ae0ff6 --- /dev/null +++ b/libs/securebox/tests/Android.bp @@ -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 { + default_applicable_licenses: ["frameworks_base_license"], +} + +android_test { + name: "SecureBoxTests", + srcs: [ + "**/*.java", + ], + static_libs: [ + "securebox", + "androidx.test.runner", + "androidx.test.rules", + "androidx.test.ext.junit", + "frameworks-base-testutils", + "junit", + "mockito-target-extended-minus-junit4", + "platform-test-annotations", + "testables", + "testng", + "truth-prebuilt", + ], + libs: [ + "android.test.mock", + "android.test.base", + "android.test.runner", + ], + jni_libs: [ + "libdexmakerjvmtiagent", + "libstaticjvmtiagent", + ], +} diff --git a/libs/securebox/tests/AndroidManifest.xml b/libs/securebox/tests/AndroidManifest.xml new file mode 100644 index 000000000000..3dc956394362 --- /dev/null +++ b/libs/securebox/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.security.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 SecureBox" + android:targetPackage="com.android.security.tests"> + </instrumentation> + +</manifest> diff --git a/libs/securebox/tests/AndroidTest.xml b/libs/securebox/tests/AndroidTest.xml new file mode 100644 index 000000000000..54abd13515b4 --- /dev/null +++ b/libs/securebox/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 SecureBox"> + <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="SecureBoxTests.apk" /> + </target_preparer> + + <option name="test-suite-tag" value="apct" /> + <option name="test-suite-tag" value="framework-base-presubmit" /> + <option name="test-tag" value="SecureBoxTests" /> + <test class="com.android.tradefed.testtype.AndroidJUnitTest" > + <option name="package" value="com.android.security.tests" /> + <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" /> + <option name="hidden-api-checks" value="false"/> + </test> +</configuration> diff --git a/libs/securebox/tests/src/com/android/security/SecureBoxTest.java b/libs/securebox/tests/src/com/android/security/SecureBoxTest.java new file mode 100644 index 000000000000..b6e2365038dc --- /dev/null +++ b/libs/securebox/tests/src/com/android/security/SecureBoxTest.java @@ -0,0 +1,371 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.security; + +import static com.google.common.truth.Truth.assertThat; + +import static org.testng.Assert.assertThrows; +import static org.testng.Assert.expectThrows; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.internal.util.ArrayUtils; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.security.InvalidKeyException; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.spec.ECPrivateKeySpec; + +import javax.crypto.AEADBadTagException; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class SecureBoxTest { + + private static final int EC_PUBLIC_KEY_LEN_BYTES = 65; + private static final int NUM_TEST_ITERATIONS = 100; + private static final int VERSION_LEN_BYTES = 2; + + // The following fixtures were produced by the C implementation of SecureBox v2. We use these to + // cross-verify the two implementations. + private static final byte[] VAULT_PARAMS = + new byte[] { + (byte) 0x04, (byte) 0xb8, (byte) 0x00, (byte) 0x11, (byte) 0x18, (byte) 0x98, + (byte) 0x1d, (byte) 0xf0, (byte) 0x6e, (byte) 0xb4, (byte) 0x94, (byte) 0xfe, + (byte) 0x86, (byte) 0xda, (byte) 0x1c, (byte) 0x07, (byte) 0x8d, (byte) 0x01, + (byte) 0xb4, (byte) 0x3a, (byte) 0xf6, (byte) 0x8d, (byte) 0xdc, (byte) 0x61, + (byte) 0xd0, (byte) 0x46, (byte) 0x49, (byte) 0x95, (byte) 0x0f, (byte) 0x10, + (byte) 0x86, (byte) 0x93, (byte) 0x24, (byte) 0x66, (byte) 0xe0, (byte) 0x3f, + (byte) 0xd2, (byte) 0xdf, (byte) 0xf3, (byte) 0x79, (byte) 0x20, (byte) 0x1d, + (byte) 0x91, (byte) 0x55, (byte) 0xb0, (byte) 0xe5, (byte) 0xbd, (byte) 0x7a, + (byte) 0x8b, (byte) 0x32, (byte) 0x7d, (byte) 0x25, (byte) 0x53, (byte) 0xa2, + (byte) 0xfc, (byte) 0xa5, (byte) 0x65, (byte) 0xe1, (byte) 0xbd, (byte) 0x21, + (byte) 0x44, (byte) 0x7e, (byte) 0x78, (byte) 0x52, (byte) 0xfa, (byte) 0x31, + (byte) 0x32, (byte) 0x33, (byte) 0x34, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x78, (byte) 0x56, (byte) 0x34, (byte) 0x12, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x0a, (byte) 0x00, (byte) 0x00, + (byte) 0x00 + }; + private static final byte[] VAULT_CHALLENGE = getBytes("Not a real vault challenge"); + private static final byte[] THM_KF_HASH = getBytes("12345678901234567890123456789012"); + private static final byte[] ENCRYPTED_RECOVERY_KEY = + new byte[] { + (byte) 0x02, (byte) 0x00, (byte) 0x04, (byte) 0xe3, (byte) 0xa8, (byte) 0xd0, + (byte) 0x32, (byte) 0x3c, (byte) 0xc7, (byte) 0xe5, (byte) 0xe8, (byte) 0xc1, + (byte) 0x73, (byte) 0x4c, (byte) 0x75, (byte) 0x20, (byte) 0x2e, (byte) 0xb7, + (byte) 0xba, (byte) 0xef, (byte) 0x3e, (byte) 0x3e, (byte) 0xa6, (byte) 0x93, + (byte) 0xe9, (byte) 0xde, (byte) 0xa7, (byte) 0x00, (byte) 0x09, (byte) 0xba, + (byte) 0xa8, (byte) 0x9c, (byte) 0xac, (byte) 0x72, (byte) 0xff, (byte) 0xf6, + (byte) 0x84, (byte) 0x16, (byte) 0xb0, (byte) 0xff, (byte) 0x47, (byte) 0x98, + (byte) 0x53, (byte) 0xc4, (byte) 0xa3, (byte) 0x4a, (byte) 0x54, (byte) 0x21, + (byte) 0x8e, (byte) 0x00, (byte) 0x4b, (byte) 0xfa, (byte) 0xce, (byte) 0xe3, + (byte) 0x79, (byte) 0x8e, (byte) 0x20, (byte) 0x7c, (byte) 0x9b, (byte) 0xc4, + (byte) 0x7c, (byte) 0xd5, (byte) 0x33, (byte) 0x70, (byte) 0x96, (byte) 0xdc, + (byte) 0xa0, (byte) 0x1f, (byte) 0x6e, (byte) 0xbb, (byte) 0x5d, (byte) 0x0c, + (byte) 0x64, (byte) 0x5f, (byte) 0xed, (byte) 0xbf, (byte) 0x79, (byte) 0x8a, + (byte) 0x0e, (byte) 0xd6, (byte) 0x4b, (byte) 0x93, (byte) 0xc9, (byte) 0xcd, + (byte) 0x25, (byte) 0x06, (byte) 0x73, (byte) 0x5e, (byte) 0xdb, (byte) 0xac, + (byte) 0xa8, (byte) 0xeb, (byte) 0x6e, (byte) 0x26, (byte) 0x77, (byte) 0x56, + (byte) 0xd1, (byte) 0x23, (byte) 0x48, (byte) 0xb6, (byte) 0x6a, (byte) 0x15, + (byte) 0xd4, (byte) 0x3e, (byte) 0x38, (byte) 0x7d, (byte) 0x6f, (byte) 0x6f, + (byte) 0x7c, (byte) 0x0b, (byte) 0x93, (byte) 0x4e, (byte) 0xb3, (byte) 0x21, + (byte) 0x44, (byte) 0x86, (byte) 0xf3, (byte) 0x2e + }; + private static final byte[] KEY_CLAIMANT = getBytes("asdfasdfasdfasdf"); + private static final byte[] RECOVERY_CLAIM = + new byte[] { + (byte) 0x02, (byte) 0x00, (byte) 0x04, (byte) 0x16, (byte) 0x75, (byte) 0x5b, + (byte) 0xa2, (byte) 0xdc, (byte) 0x2b, (byte) 0x58, (byte) 0xb9, (byte) 0x66, + (byte) 0xcb, (byte) 0x6f, (byte) 0xb1, (byte) 0xc1, (byte) 0xb0, (byte) 0x1d, + (byte) 0x82, (byte) 0x29, (byte) 0x97, (byte) 0xec, (byte) 0x65, (byte) 0x5e, + (byte) 0xef, (byte) 0x14, (byte) 0xc7, (byte) 0xf0, (byte) 0xf1, (byte) 0x83, + (byte) 0x15, (byte) 0x0b, (byte) 0xcb, (byte) 0x33, (byte) 0x2d, (byte) 0x05, + (byte) 0x20, (byte) 0xdc, (byte) 0xc7, (byte) 0x0d, (byte) 0xc8, (byte) 0xc0, + (byte) 0xc9, (byte) 0xa8, (byte) 0x67, (byte) 0xc8, (byte) 0x16, (byte) 0xfe, + (byte) 0xfb, (byte) 0xb0, (byte) 0x28, (byte) 0x8e, (byte) 0x4f, (byte) 0xd5, + (byte) 0x31, (byte) 0xa7, (byte) 0x94, (byte) 0x33, (byte) 0x23, (byte) 0x15, + (byte) 0x04, (byte) 0xbf, (byte) 0x13, (byte) 0x6a, (byte) 0x28, (byte) 0x8f, + (byte) 0xa6, (byte) 0xfc, (byte) 0x01, (byte) 0xd5, (byte) 0x69, (byte) 0x3d, + (byte) 0x96, (byte) 0x0c, (byte) 0x37, (byte) 0xb4, (byte) 0x1e, (byte) 0x13, + (byte) 0x40, (byte) 0xcc, (byte) 0x44, (byte) 0x19, (byte) 0xf2, (byte) 0xdb, + (byte) 0x49, (byte) 0x80, (byte) 0x9f, (byte) 0xef, (byte) 0xee, (byte) 0x41, + (byte) 0xe6, (byte) 0x3f, (byte) 0xa8, (byte) 0xea, (byte) 0x89, (byte) 0xfe, + (byte) 0x56, (byte) 0x20, (byte) 0xba, (byte) 0x90, (byte) 0x9a, (byte) 0xba, + (byte) 0x0e, (byte) 0x30, (byte) 0xa7, (byte) 0x2b, (byte) 0x0a, (byte) 0x12, + (byte) 0x0b, (byte) 0x03, (byte) 0xd1, (byte) 0x0c, (byte) 0x8e, (byte) 0x82, + (byte) 0x03, (byte) 0xa1, (byte) 0x7f, (byte) 0xc8, (byte) 0xd0, (byte) 0xa9, + (byte) 0x86, (byte) 0x55, (byte) 0x63, (byte) 0xdc, (byte) 0x70, (byte) 0x34, + (byte) 0x21, (byte) 0x2a, (byte) 0x41, (byte) 0x3f, (byte) 0xbb, (byte) 0x82, + (byte) 0x82, (byte) 0xf9, (byte) 0x2b, (byte) 0xd2, (byte) 0x33, (byte) 0x03, + (byte) 0x50, (byte) 0xd2, (byte) 0x27, (byte) 0xeb, (byte) 0x1a + }; + + private static final byte[] TEST_SHARED_SECRET = getBytes("TEST_SHARED_SECRET"); + private static final byte[] TEST_HEADER = getBytes("TEST_HEADER"); + private static final byte[] TEST_PAYLOAD = getBytes("TEST_PAYLOAD"); + + private static final PublicKey THM_PUBLIC_KEY; + private static final PrivateKey THM_PRIVATE_KEY; + + static { + try { + THM_PUBLIC_KEY = + SecureBox.decodePublicKey( + new byte[] { + (byte) 0x04, (byte) 0xb8, (byte) 0x00, (byte) 0x11, (byte) 0x18, + (byte) 0x98, (byte) 0x1d, (byte) 0xf0, (byte) 0x6e, (byte) 0xb4, + (byte) 0x94, (byte) 0xfe, (byte) 0x86, (byte) 0xda, (byte) 0x1c, + (byte) 0x07, (byte) 0x8d, (byte) 0x01, (byte) 0xb4, (byte) 0x3a, + (byte) 0xf6, (byte) 0x8d, (byte) 0xdc, (byte) 0x61, (byte) 0xd0, + (byte) 0x46, (byte) 0x49, (byte) 0x95, (byte) 0x0f, (byte) 0x10, + (byte) 0x86, (byte) 0x93, (byte) 0x24, (byte) 0x66, (byte) 0xe0, + (byte) 0x3f, (byte) 0xd2, (byte) 0xdf, (byte) 0xf3, (byte) 0x79, + (byte) 0x20, (byte) 0x1d, (byte) 0x91, (byte) 0x55, (byte) 0xb0, + (byte) 0xe5, (byte) 0xbd, (byte) 0x7a, (byte) 0x8b, (byte) 0x32, + (byte) 0x7d, (byte) 0x25, (byte) 0x53, (byte) 0xa2, (byte) 0xfc, + (byte) 0xa5, (byte) 0x65, (byte) 0xe1, (byte) 0xbd, (byte) 0x21, + (byte) 0x44, (byte) 0x7e, (byte) 0x78, (byte) 0x52, (byte) 0xfa + }); + THM_PRIVATE_KEY = + decodePrivateKey( + new byte[] { + (byte) 0x70, (byte) 0x01, (byte) 0xc7, (byte) 0x87, (byte) 0x32, + (byte) 0x2f, (byte) 0x1c, (byte) 0x9a, (byte) 0x6e, (byte) 0xb1, + (byte) 0x91, (byte) 0xca, (byte) 0x4e, (byte) 0xb5, (byte) 0x44, + (byte) 0xba, (byte) 0xc8, (byte) 0x68, (byte) 0xc6, (byte) 0x0a, + (byte) 0x76, (byte) 0xcb, (byte) 0xd3, (byte) 0x63, (byte) 0x67, + (byte) 0x7c, (byte) 0xb0, (byte) 0x11, (byte) 0x82, (byte) 0x65, + (byte) 0x77, (byte) 0x01 + }); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } + + @Test + public void genKeyPair_alwaysReturnsANewKeyPair() throws Exception { + KeyPair keyPair1 = SecureBox.genKeyPair(); + KeyPair keyPair2 = SecureBox.genKeyPair(); + assertThat(keyPair1).isNotEqualTo(keyPair2); + } + + @Test + public void decryptRecoveryClaim() throws Exception { + byte[] claimContent = + SecureBox.decrypt( + THM_PRIVATE_KEY, + /*sharedSecret=*/ null, + ArrayUtils.concat(getBytes("V1 KF_claim"), VAULT_PARAMS, VAULT_CHALLENGE), + RECOVERY_CLAIM); + assertThat(claimContent).isEqualTo(ArrayUtils.concat(THM_KF_HASH, KEY_CLAIMANT)); + } + + @Test + public void decryptRecoveryKey_doesNotThrowForValidAuthenticationTag() throws Exception { + SecureBox.decrypt( + THM_PRIVATE_KEY, + THM_KF_HASH, + ArrayUtils.concat(getBytes("V1 THM_encrypted_recovery_key"), VAULT_PARAMS), + ENCRYPTED_RECOVERY_KEY); + } + + @Test + public void encryptThenDecrypt() throws Exception { + byte[] state = TEST_PAYLOAD; + // Iterate multiple times to amplify any errors + for (int i = 0; i < NUM_TEST_ITERATIONS; i++) { + state = SecureBox.encrypt(THM_PUBLIC_KEY, TEST_SHARED_SECRET, TEST_HEADER, state); + } + for (int i = 0; i < NUM_TEST_ITERATIONS; i++) { + state = SecureBox.decrypt(THM_PRIVATE_KEY, TEST_SHARED_SECRET, TEST_HEADER, state); + } + assertThat(state).isEqualTo(TEST_PAYLOAD); + } + + @Test + public void encryptThenDecrypt_nullPublicPrivateKeys() throws Exception { + byte[] encrypted = + SecureBox.encrypt( + /*theirPublicKey=*/ null, TEST_SHARED_SECRET, TEST_HEADER, TEST_PAYLOAD); + byte[] decrypted = + SecureBox.decrypt( + /*ourPrivateKey=*/ null, TEST_SHARED_SECRET, TEST_HEADER, encrypted); + assertThat(decrypted).isEqualTo(TEST_PAYLOAD); + } + + @Test + public void encryptThenDecrypt_nullSharedSecret() throws Exception { + byte[] encrypted = + SecureBox.encrypt( + THM_PUBLIC_KEY, /*sharedSecret=*/ null, TEST_HEADER, TEST_PAYLOAD); + byte[] decrypted = + SecureBox.decrypt(THM_PRIVATE_KEY, /*sharedSecret=*/ null, TEST_HEADER, encrypted); + assertThat(decrypted).isEqualTo(TEST_PAYLOAD); + } + + @Test + public void encryptThenDecrypt_nullHeader() throws Exception { + byte[] encrypted = + SecureBox.encrypt( + THM_PUBLIC_KEY, TEST_SHARED_SECRET, /*header=*/ null, TEST_PAYLOAD); + byte[] decrypted = + SecureBox.decrypt(THM_PRIVATE_KEY, TEST_SHARED_SECRET, /*header=*/ null, encrypted); + assertThat(decrypted).isEqualTo(TEST_PAYLOAD); + } + + @Test + public void encryptThenDecrypt_nullPayload() throws Exception { + byte[] encrypted = + SecureBox.encrypt( + THM_PUBLIC_KEY, TEST_SHARED_SECRET, TEST_HEADER, /*payload=*/ null); + byte[] decrypted = + SecureBox.decrypt( + THM_PRIVATE_KEY, + TEST_SHARED_SECRET, + TEST_HEADER, + /*encryptedPayload=*/ encrypted); + assertThat(decrypted.length).isEqualTo(0); + } + + @Test + public void encrypt_nullPublicKeyAndSharedSecret() throws Exception { + IllegalArgumentException expected = + expectThrows( + IllegalArgumentException.class, + () -> + SecureBox.encrypt( + /*theirPublicKey=*/ null, + /*sharedSecret=*/ null, + TEST_HEADER, + TEST_PAYLOAD)); + assertThat(expected.getMessage()).contains("public key and shared secret"); + } + + @Test + public void decrypt_nullPrivateKeyAndSharedSecret() throws Exception { + IllegalArgumentException expected = + expectThrows( + IllegalArgumentException.class, + () -> + SecureBox.decrypt( + /*ourPrivateKey=*/ null, + /*sharedSecret=*/ null, + TEST_HEADER, + TEST_PAYLOAD)); + assertThat(expected.getMessage()).contains("private key and shared secret"); + } + + @Test + public void decrypt_nullEncryptedPayload() throws Exception { + NullPointerException expected = + expectThrows( + NullPointerException.class, + () -> + SecureBox.decrypt( + THM_PRIVATE_KEY, + TEST_SHARED_SECRET, + TEST_HEADER, + /*encryptedPayload=*/ null)); + assertThat(expected.getMessage()).contains("payload"); + } + + @Test + public void decrypt_badAuthenticationTag() throws Exception { + byte[] encrypted = + SecureBox.encrypt(THM_PUBLIC_KEY, TEST_SHARED_SECRET, TEST_HEADER, TEST_PAYLOAD); + encrypted[encrypted.length - 1] ^= (byte) 1; + + assertThrows( + AEADBadTagException.class, + () -> + SecureBox.decrypt( + THM_PRIVATE_KEY, TEST_SHARED_SECRET, TEST_HEADER, encrypted)); + } + + @Test + public void encrypt_invalidPublicKey() throws Exception { + KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA"); + keyGen.initialize(2048); + PublicKey publicKey = keyGen.genKeyPair().getPublic(); + + assertThrows( + InvalidKeyException.class, + () -> SecureBox.encrypt(publicKey, TEST_SHARED_SECRET, TEST_HEADER, TEST_PAYLOAD)); + } + + @Test + public void decrypt_invalidPrivateKey() throws Exception { + byte[] encrypted = + SecureBox.encrypt(THM_PUBLIC_KEY, TEST_SHARED_SECRET, TEST_HEADER, TEST_PAYLOAD); + KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA"); + keyGen.initialize(2048); + PrivateKey privateKey = keyGen.genKeyPair().getPrivate(); + + assertThrows( + InvalidKeyException.class, + () -> SecureBox.decrypt(privateKey, TEST_SHARED_SECRET, TEST_HEADER, encrypted)); + } + + @Test + public void decrypt_publicKeyOutsideCurve() throws Exception { + byte[] encrypted = + SecureBox.encrypt(THM_PUBLIC_KEY, TEST_SHARED_SECRET, TEST_HEADER, TEST_PAYLOAD); + // Flip the least significant bit of the encoded public key + encrypted[VERSION_LEN_BYTES + EC_PUBLIC_KEY_LEN_BYTES - 1] ^= (byte) 1; + + InvalidKeyException expected = + expectThrows( + InvalidKeyException.class, + () -> + SecureBox.decrypt( + THM_PRIVATE_KEY, + TEST_SHARED_SECRET, + TEST_HEADER, + encrypted)); + assertThat(expected.getMessage()).contains("expected curve"); + } + + @Test + public void encodeThenDecodePublicKey() throws Exception { + for (int i = 0; i < NUM_TEST_ITERATIONS; i++) { + PublicKey originalKey = SecureBox.genKeyPair().getPublic(); + byte[] encodedKey = SecureBox.encodePublicKey(originalKey); + PublicKey decodedKey = SecureBox.decodePublicKey(encodedKey); + assertThat(originalKey).isEqualTo(decodedKey); + } + } + + private static byte[] getBytes(String str) { + return str.getBytes(StandardCharsets.UTF_8); + } + + private static PrivateKey decodePrivateKey(byte[] keyBytes) throws Exception { + assertThat(keyBytes.length).isEqualTo(32); + BigInteger priv = new BigInteger(/*signum=*/ 1, keyBytes); + KeyFactory keyFactory = KeyFactory.getInstance("EC"); + return keyFactory.generatePrivate(new ECPrivateKeySpec(priv, SecureBox.EC_PARAM_SPEC)); + } +} diff --git a/libs/usb/tests/AccessoryChat/src/com/android/accessorychat/AccessoryChat.java b/libs/usb/tests/AccessoryChat/src/com/android/accessorychat/AccessoryChat.java index 18cfce528205..c019a8ce0b44 100644 --- a/libs/usb/tests/AccessoryChat/src/com/android/accessorychat/AccessoryChat.java +++ b/libs/usb/tests/AccessoryChat/src/com/android/accessorychat/AccessoryChat.java @@ -83,7 +83,9 @@ public class AccessoryChat extends Activity implements Runnable, TextView.OnEdit super.onCreate(savedInstanceState); mUsbManager = (UsbManager) getSystemService(Context.USB_SERVICE); - mPermissionIntent = PendingIntent.getBroadcast(this, 0, new Intent(ACTION_USB_PERMISSION), PendingIntent.FLAG_MUTABLE_UNAUDITED); + mPermissionIntent = PendingIntent.getBroadcast(this, 0, + new Intent(ACTION_USB_PERMISSION).setPackage(this.getPackageName()), + PendingIntent.FLAG_MUTABLE); IntentFilter filter = new IntentFilter(ACTION_USB_PERMISSION); registerReceiver(mUsbReceiver, filter); |