diff options
Diffstat (limited to 'libs')
683 files changed, 30827 insertions, 8569 deletions
diff --git a/libs/WindowManager/Jetpack/src/TEST_MAPPING b/libs/WindowManager/Jetpack/src/TEST_MAPPING new file mode 100644 index 000000000000..f8f64001dd24 --- /dev/null +++ b/libs/WindowManager/Jetpack/src/TEST_MAPPING @@ -0,0 +1,37 @@ +{ + "presubmit": [ + { + "name": "WMJetpackUnitTests", + "options": [ + { + "include-annotation": "android.platform.test.annotations.Presubmit" + }, + { + "exclude-annotation": "androidx.test.filters.FlakyTest" + }, + { + "exclude-annotation": "org.junit.Ignore" + } + ] + }, + { + "name": "CtsWindowManagerJetpackTestCases", + "options": [ + { + "include-annotation": "android.platform.test.annotations.Presubmit" + }, + { + "exclude-annotation": "androidx.test.filters.FlakyTest" + }, + { + "exclude-annotation": "org.junit.Ignore" + } + ] + } + ], + "imports": [ + { + "path": "vendor/google_testing/integration/tests/scenarios/src/android/platform/test/scenario/sysui" + } + ] +} diff --git a/libs/WindowManager/Jetpack/src/androidx/window/common/CommonFoldingFeature.java b/libs/WindowManager/Jetpack/src/androidx/window/common/CommonFoldingFeature.java index 8733c152dca9..921552b6cfbb 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/common/CommonFoldingFeature.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/common/CommonFoldingFeature.java @@ -208,7 +208,7 @@ public final class CommonFoldingFeature { return mType; } - /** Returns the state of the feature, or {@code null} if the feature has no state. */ + /** Returns the state of the feature.*/ @State public int getState() { return mState; diff --git a/libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java b/libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java index 6987401525b4..fdcb7be597d5 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java @@ -31,11 +31,13 @@ import android.util.Log; import android.util.SparseIntArray; import androidx.window.util.BaseDataProducer; +import androidx.window.util.DataProducer; import com.android.internal.R; import java.util.List; import java.util.Optional; +import java.util.Set; /** * An implementation of {@link androidx.window.util.DataProducer} that returns the device's posture @@ -48,7 +50,6 @@ public final class DeviceStateManagerFoldingFeatureProducer extends DeviceStateManagerFoldingFeatureProducer.class.getSimpleName(); private static final boolean DEBUG = false; - private final Context mContext; private final SparseIntArray mDeviceStateToPostureMap = new SparseIntArray(); private int mCurrentDeviceState = INVALID_DEVICE_STATE; @@ -57,9 +58,12 @@ public final class DeviceStateManagerFoldingFeatureProducer extends mCurrentDeviceState = state; notifyDataChanged(); }; + @NonNull + private final DataProducer<String> mRawFoldSupplier; - public DeviceStateManagerFoldingFeatureProducer(@NonNull Context context) { - mContext = context; + public DeviceStateManagerFoldingFeatureProducer(@NonNull Context context, + @NonNull DataProducer<String> rawFoldSupplier) { + mRawFoldSupplier = rawFoldSupplier; String[] deviceStatePosturePairs = context.getResources() .getStringArray(R.array.config_device_state_postures); for (String deviceStatePosturePair : deviceStatePosturePairs) { @@ -97,12 +101,21 @@ public final class DeviceStateManagerFoldingFeatureProducer extends @Nullable public Optional<List<CommonFoldingFeature>> getData() { final int globalHingeState = globalHingeState(); - String displayFeaturesString = mContext.getResources().getString( - R.string.config_display_features); - if (TextUtils.isEmpty(displayFeaturesString)) { + Optional<String> displayFeaturesString = mRawFoldSupplier.getData(); + if (displayFeaturesString.isEmpty() || TextUtils.isEmpty(displayFeaturesString.get())) { return Optional.empty(); } - return Optional.of(parseListFromString(displayFeaturesString, globalHingeState)); + return Optional.of(parseListFromString(displayFeaturesString.get(), globalHingeState)); + } + + @Override + protected void onListenersChanged(Set<Runnable> callbacks) { + super.onListenersChanged(callbacks); + if (callbacks.isEmpty()) { + mRawFoldSupplier.removeDataChangedCallback(this::notifyDataChanged); + } else { + mRawFoldSupplier.addDataChangedCallback(this::notifyDataChanged); + } } private int globalHingeState() { diff --git a/libs/WindowManager/Jetpack/src/androidx/window/common/EmptyLifecycleCallbacksAdapter.java b/libs/WindowManager/Jetpack/src/androidx/window/common/EmptyLifecycleCallbacksAdapter.java index f2e403b4f792..d923a46c3b5d 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/common/EmptyLifecycleCallbacksAdapter.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/common/EmptyLifecycleCallbacksAdapter.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. diff --git a/libs/WindowManager/Jetpack/src/androidx/window/common/SettingsDisplayFeatureProducer.java b/libs/WindowManager/Jetpack/src/androidx/window/common/RawFoldingFeatureProducer.java index 0e696eb8efb7..69ad1badce60 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/common/SettingsDisplayFeatureProducer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/common/RawFoldingFeatureProducer.java @@ -16,11 +16,6 @@ package androidx.window.common; -import static androidx.window.common.CommonFoldingFeature.COMMON_STATE_FLAT; -import static androidx.window.common.CommonFoldingFeature.COMMON_STATE_HALF_OPENED; -import static androidx.window.common.CommonFoldingFeature.COMMON_STATE_UNKNOWN; -import static androidx.window.common.CommonFoldingFeature.parseListFromString; - import android.annotation.NonNull; import android.content.ContentResolver; import android.content.Context; @@ -33,75 +28,88 @@ import android.text.TextUtils; import androidx.window.util.BaseDataProducer; -import java.util.Collections; -import java.util.List; +import com.android.internal.R; + import java.util.Optional; +import java.util.Set; /** - * Implementation of {@link androidx.window.util.DataProducer} that produces - * {@link CommonFoldingFeature} parsed from a string stored in {@link Settings}. + * Implementation of {@link androidx.window.util.DataProducer} that produces a + * {@link String} that can be parsed to a {@link CommonFoldingFeature}. + * {@link RawFoldingFeatureProducer} searches for the value in two places. The first check is in + * settings where the {@link String} property is saved with the key + * {@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} listens to changes in the setting so that it can override + * the system {@link CommonFoldingFeature} data. */ -public final class SettingsDisplayFeatureProducer - extends BaseDataProducer<List<CommonFoldingFeature>> { +public final class RawFoldingFeatureProducer extends BaseDataProducer<String> { private static final String DISPLAY_FEATURES = "display_features"; - private static final String DEVICE_POSTURE = "device_posture"; - private final Uri mDevicePostureUri = - Settings.Global.getUriFor(DEVICE_POSTURE); private final Uri mDisplayFeaturesUri = Settings.Global.getUriFor(DISPLAY_FEATURES); private final ContentResolver mResolver; private final ContentObserver mObserver; + private final String mResourceFeature; private boolean mRegisteredObservers; - public SettingsDisplayFeatureProducer(@NonNull Context context) { + public RawFoldingFeatureProducer(@NonNull Context context) { mResolver = context.getContentResolver(); mObserver = new SettingsObserver(); - } - - private int getPosture() { - int posture = Settings.Global.getInt(mResolver, DEVICE_POSTURE, COMMON_STATE_UNKNOWN); - if (posture == COMMON_STATE_HALF_OPENED || posture == COMMON_STATE_FLAT) { - return posture; - } else { - return COMMON_STATE_UNKNOWN; - } + mResourceFeature = context.getResources().getString(R.string.config_display_features); } @Override @NonNull - public Optional<List<CommonFoldingFeature>> getData() { - String displayFeaturesString = Settings.Global.getString(mResolver, DISPLAY_FEATURES); + public Optional<String> getData() { + String displayFeaturesString = getFeatureString(); if (displayFeaturesString == null) { return Optional.empty(); } + return Optional.of(displayFeaturesString); + } + + /** + * Returns the {@link String} representation for a {@link CommonFoldingFeature} from settings if + * present and falls back to the resource value if empty or {@code null}. + */ + private String getFeatureString() { + String settingsFeature = Settings.Global.getString(mResolver, DISPLAY_FEATURES); + if (TextUtils.isEmpty(settingsFeature)) { + return mResourceFeature; + } + return settingsFeature; + } - if (TextUtils.isEmpty(displayFeaturesString)) { - return Optional.of(Collections.emptyList()); + @Override + protected void onListenersChanged(Set<Runnable> callbacks) { + if (callbacks.isEmpty()) { + unregisterObserversIfNeeded(); + } else { + registerObserversIfNeeded(); } - return Optional.of(parseListFromString(displayFeaturesString, getPosture())); } /** * Registers settings observers, if needed. When settings observers are registered for this * producer callbacks for changes in data will be triggered. */ - public void registerObserversIfNeeded() { + private void registerObserversIfNeeded() { if (mRegisteredObservers) { return; } mRegisteredObservers = true; mResolver.registerContentObserver(mDisplayFeaturesUri, false /* notifyForDescendants */, mObserver /* ContentObserver */); - mResolver.registerContentObserver(mDevicePostureUri, false, mObserver); } /** * Unregisters settings observers, if needed. When settings observers are unregistered for this * producer callbacks for changes in data will not be triggered. */ - public void unregisterObserversIfNeeded() { + private void unregisterObserversIfNeeded() { if (!mRegisteredObservers) { return; } @@ -116,7 +124,7 @@ public final class SettingsDisplayFeatureProducer @Override public void onChange(boolean selfChange, Uri uri) { - if (mDisplayFeaturesUri.equals(uri) || mDevicePostureUri.equals(uri)) { + if (mDisplayFeaturesUri.equals(uri)) { notifyDataChanged(); } } 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 180c77250fd1..3ff531573f1f 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java @@ -16,7 +16,6 @@ package androidx.window.extensions.embedding; -import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import android.app.Activity; @@ -35,6 +34,8 @@ import android.window.WindowContainerTransaction; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.android.internal.annotations.VisibleForTesting; + import java.util.Map; import java.util.concurrent.Executor; @@ -47,7 +48,8 @@ import java.util.concurrent.Executor; class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer { /** Mapping from the client assigned unique token to the {@link TaskFragmentInfo}. */ - private final Map<IBinder, TaskFragmentInfo> mFragmentInfos = new ArrayMap<>(); + @VisibleForTesting + final Map<IBinder, TaskFragmentInfo> mFragmentInfos = new ArrayMap<>(); /** * Mapping from the client assigned unique token to the TaskFragment parent @@ -56,7 +58,8 @@ class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer { final Map<IBinder, Configuration> mFragmentParentConfigs = new ArrayMap<>(); private final TaskFragmentCallback mCallback; - private TaskFragmentAnimationController mAnimationController; + @VisibleForTesting + TaskFragmentAnimationController mAnimationController; /** * Callback that notifies the controller about changes to task fragments. @@ -67,6 +70,8 @@ class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer { void onTaskFragmentVanished(@NonNull TaskFragmentInfo taskFragmentInfo); void onTaskFragmentParentInfoChanged(@NonNull IBinder fragmentToken, @NonNull Configuration parentConfig); + void onActivityReparentToTask(int taskId, @NonNull Intent activityIntent, + @NonNull IBinder activityToken); } /** @@ -80,21 +85,25 @@ class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer { @Override public void unregisterOrganizer() { - stopOverrideSplitAnimation(); - mAnimationController = null; + if (mAnimationController != null) { + mAnimationController.unregisterAllRemoteAnimations(); + mAnimationController = null; + } super.unregisterOrganizer(); } - void startOverrideSplitAnimation() { + /** Overrides the animation if the transition is on the given Task. */ + void startOverrideSplitAnimation(int taskId) { if (mAnimationController == null) { mAnimationController = new TaskFragmentAnimationController(this); } - mAnimationController.registerRemoteAnimations(); + mAnimationController.registerRemoteAnimations(taskId); } - void stopOverrideSplitAnimation() { + /** No longer overrides the animation if the transition is on the given Task. */ + void stopOverrideSplitAnimation(int taskId) { if (mAnimationController != null) { - mAnimationController.unregisterRemoteAnimations(); + mAnimationController.unregisterRemoteAnimations(taskId); } } @@ -111,25 +120,28 @@ class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer { * @param secondaryFragmentBounds the initial bounds for the secondary TaskFragment * @param activityIntent Intent to start the secondary Activity with. * @param activityOptions ActivityOptions to start the secondary Activity with. + * @param windowingMode the windowing mode to set for the TaskFragments. */ void startActivityToSide(@NonNull WindowContainerTransaction wct, @NonNull IBinder launchingFragmentToken, @NonNull Rect launchingFragmentBounds, @NonNull Activity launchingActivity, @NonNull IBinder secondaryFragmentToken, @NonNull Rect secondaryFragmentBounds, @NonNull Intent activityIntent, - @Nullable Bundle activityOptions, @NonNull SplitRule rule) { + @Nullable Bundle activityOptions, @NonNull SplitRule rule, + @WindowingMode int windowingMode) { final IBinder ownerToken = launchingActivity.getActivityToken(); // Create or resize the launching TaskFragment. if (mFragmentInfos.containsKey(launchingFragmentToken)) { resizeTaskFragment(wct, launchingFragmentToken, launchingFragmentBounds); + updateWindowingMode(wct, launchingFragmentToken, windowingMode); } else { createTaskFragmentAndReparentActivity(wct, launchingFragmentToken, ownerToken, - launchingFragmentBounds, WINDOWING_MODE_MULTI_WINDOW, launchingActivity); + launchingFragmentBounds, windowingMode, launchingActivity); } // Create a TaskFragment for the secondary activity. createTaskFragmentAndStartActivity(wct, secondaryFragmentToken, ownerToken, - secondaryFragmentBounds, WINDOWING_MODE_MULTI_WINDOW, activityIntent, + secondaryFragmentBounds, windowingMode, activityIntent, activityOptions); // Set adjacent to each other so that the containers below will be invisible. @@ -144,6 +156,7 @@ class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer { void expandTaskFragment(WindowContainerTransaction wct, IBinder fragmentToken) { resizeTaskFragment(wct, fragmentToken, new Rect()); setAdjacentTaskFragments(wct, fragmentToken, null /* secondary */, null /* splitRule */); + updateWindowingMode(wct, fragmentToken, WINDOWING_MODE_UNDEFINED); } /** @@ -246,6 +259,15 @@ class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer { wct.setBounds(mFragmentInfos.get(fragmentToken).getToken(), bounds); } + void updateWindowingMode(WindowContainerTransaction wct, IBinder fragmentToken, + @WindowingMode int windowingMode) { + if (!mFragmentInfos.containsKey(fragmentToken)) { + throw new IllegalArgumentException( + "Can't find an existing TaskFragment with fragmentToken=" + fragmentToken); + } + wct.setWindowingMode(mFragmentInfos.get(fragmentToken).getToken(), windowingMode); + } + void deleteTaskFragment(WindowContainerTransaction wct, IBinder fragmentToken) { if (!mFragmentInfos.containsKey(fragmentToken)) { throw new IllegalArgumentException( @@ -293,4 +315,12 @@ class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer { mCallback.onTaskFragmentParentInfoChanged(fragmentToken, parentConfig); } } + + @Override + public void onActivityReparentToTask(int taskId, @NonNull Intent activityIntent, + @NonNull IBinder activityToken) { + if (mCallback != null) { + mCallback.onActivityReparentToTask(taskId, activityIntent, activityToken); + } + } } 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 44af1a9fd780..f09a91018bf0 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java @@ -18,6 +18,8 @@ package androidx.window.extensions.embedding; import android.annotation.NonNull; import android.app.Activity; +import android.util.Pair; +import android.util.Size; /** * Client-side descriptor of a split that holds two containers. @@ -66,6 +68,13 @@ class SplitContainer { return mSplitRule; } + /** Returns the minimum dimension pair of primary container and secondary container. */ + @NonNull + Pair<Size, Size> getMinDimensionsPair() { + return new Pair<>(mPrimaryContainer.getMinDimensions(), + mSecondaryContainer.getMinDimensions()); + } + boolean isPlaceholderContainer() { return (mSplitRule instanceof SplitPlaceholderRule); } 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 fc955927f3ed..c9a0d7d99cc6 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java @@ -16,19 +16,25 @@ package androidx.window.extensions.embedding; +import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; +import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; + import static androidx.window.extensions.embedding.SplitContainer.getFinishPrimaryWithSecondaryBehavior; import static androidx.window.extensions.embedding.SplitContainer.getFinishSecondaryWithPrimaryBehavior; import static androidx.window.extensions.embedding.SplitContainer.isStickyPlaceholderRule; 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.getActivityIntentMinDimensionsPair; +import static androidx.window.extensions.embedding.SplitPresenter.getNonEmbeddedActivityBounds; +import static androidx.window.extensions.embedding.SplitPresenter.shouldShowSideBySide; -import android.annotation.NonNull; -import android.annotation.Nullable; import android.app.Activity; import android.app.ActivityClient; import android.app.ActivityOptions; import android.app.ActivityThread; import android.app.Instrumentation; +import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.res.Configuration; @@ -37,11 +43,21 @@ import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.Looper; +import android.util.ArraySet; +import android.util.Log; +import android.util.Pair; +import android.util.Size; +import android.util.SparseArray; import android.window.TaskFragmentInfo; import android.window.WindowContainerTransaction; +import androidx.annotation.GuardedBy; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.window.common.EmptyLifecycleCallbacksAdapter; +import com.android.internal.annotations.VisibleForTesting; + import java.util.ArrayList; import java.util.List; import java.util.Set; @@ -53,23 +69,36 @@ import java.util.function.Consumer; */ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmentCallback, ActivityEmbeddingComponent { + static final String TAG = "SplitController"; - private final SplitPresenter mPresenter; + @VisibleForTesting + @GuardedBy("mLock") + final SplitPresenter mPresenter; // Currently applied split configuration. + @GuardedBy("mLock") private final List<EmbeddingRule> mSplitRules = new ArrayList<>(); - private final List<TaskFragmentContainer> mContainers = new ArrayList<>(); - private final List<SplitContainer> mSplitContainers = new ArrayList<>(); + /** + * Map from Task id to {@link TaskContainer} which contains all TaskFragment and split pair info + * below it. + * When the app is host of multiple Tasks, there can be multiple splits controlled by the same + * organizer. + */ + @VisibleForTesting + @GuardedBy("mLock") + final SparseArray<TaskContainer> mTaskContainers = new SparseArray<>(); // Callback to Jetpack to notify about changes to split states. - private @NonNull Consumer<List<SplitInfo>> mEmbeddingCallback; + @NonNull + private Consumer<List<SplitInfo>> mEmbeddingCallback; private final List<SplitInfo> mLastReportedSplitStates = new ArrayList<>(); - - // We currently only support split activity embedding within the one root Task. - private final Rect mParentBounds = new Rect(); + private final Handler mHandler; + private final Object mLock = new Object(); public SplitController() { - mPresenter = new SplitPresenter(new MainThreadExecutor(), this); + final MainThreadExecutor executor = new MainThreadExecutor(); + mHandler = executor.mHandler; + mPresenter = new SplitPresenter(executor, this); ActivityThread activityThread = ActivityThread.currentActivityThread(); // Register a callback to be notified about activities being created. activityThread.getApplication().registerActivityLifecycleCallbacks( @@ -82,108 +111,219 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen /** Updates the embedding rules applied to future activity launches. */ @Override public void setEmbeddingRules(@NonNull Set<EmbeddingRule> rules) { - mSplitRules.clear(); - mSplitRules.addAll(rules); - updateAnimationOverride(); + synchronized (mLock) { + mSplitRules.clear(); + mSplitRules.addAll(rules); + for (int i = mTaskContainers.size() - 1; i >= 0; i--) { + updateAnimationOverride(mTaskContainers.valueAt(i)); + } + } } @NonNull - public List<EmbeddingRule> getSplitRules() { + List<EmbeddingRule> getSplitRules() { return mSplitRules; } /** - * Starts an activity to side of the launchingActivity with the provided split config. - */ - public void startActivityToSide(@NonNull Activity launchingActivity, @NonNull Intent intent, - @Nullable Bundle options, @NonNull SplitRule sideRule, - @Nullable Consumer<Exception> failureCallback) { - try { - mPresenter.startActivityToSide(launchingActivity, intent, options, sideRule); - } catch (Exception e) { - if (failureCallback != null) { - failureCallback.accept(e); - } - } - } - - /** * Registers the split organizer callback to notify about changes to active splits. */ @Override public void setSplitInfoCallback(@NonNull Consumer<List<SplitInfo>> callback) { - mEmbeddingCallback = callback; - updateCallbackIfNecessary(); + synchronized (mLock) { + mEmbeddingCallback = callback; + updateCallbackIfNecessary(); + } } @Override public void onTaskFragmentAppeared(@NonNull TaskFragmentInfo taskFragmentInfo) { - TaskFragmentContainer container = getContainer(taskFragmentInfo.getFragmentToken()); - if (container == null) { - return; - } + synchronized (mLock) { + TaskFragmentContainer container = getContainer(taskFragmentInfo.getFragmentToken()); + if (container == null) { + return; + } - container.setInfo(taskFragmentInfo); - if (container.isFinished()) { - mPresenter.cleanupContainer(container, false /* shouldFinishDependent */); + container.setInfo(taskFragmentInfo); + if (container.isFinished()) { + mPresenter.cleanupContainer(container, false /* shouldFinishDependent */); + } + updateCallbackIfNecessary(); } - updateCallbackIfNecessary(); } @Override public void onTaskFragmentInfoChanged(@NonNull TaskFragmentInfo taskFragmentInfo) { - TaskFragmentContainer container = getContainer(taskFragmentInfo.getFragmentToken()); - if (container == null) { - return; - } + synchronized (mLock) { + TaskFragmentContainer container = getContainer(taskFragmentInfo.getFragmentToken()); + if (container == null) { + return; + } - container.setInfo(taskFragmentInfo); - // Check if there are no running activities - consider the container empty if there are no - // non-finishing activities left. - if (!taskFragmentInfo.hasRunningActivity()) { - // Do not finish the dependents if this TaskFragment was cleared due to launching - // activity in the Task. - final boolean shouldFinishDependent = - !taskFragmentInfo.isTaskClearedForReuse(); - mPresenter.cleanupContainer(container, shouldFinishDependent); + final WindowContainerTransaction wct = new WindowContainerTransaction(); + final boolean wasInPip = isInPictureInPicture(container); + container.setInfo(taskFragmentInfo); + final boolean isInPip = isInPictureInPicture(container); + // Check if there are no running activities - consider the container empty if there are + // no non-finishing activities left. + if (!taskFragmentInfo.hasRunningActivity()) { + if (taskFragmentInfo.isTaskFragmentClearedForPip()) { + // Do not finish the dependents if the last activity is reparented to PiP. + // Instead, the original split should be cleanup, and the dependent may be + // expanded to fullscreen. + cleanupForEnterPip(wct, container); + mPresenter.cleanupContainer(container, false /* shouldFinishDependent */, wct); + } else if (taskFragmentInfo.isTaskClearedForReuse()) { + // Do not finish the dependents if this TaskFragment was cleared due to + // launching activity in the Task. + mPresenter.cleanupContainer(container, false /* shouldFinishDependent */, wct); + } else if (!container.isWaitingActivityAppear()) { + // Do not finish the container before the expected activity appear until + // timeout. + mPresenter.cleanupContainer(container, true /* shouldFinishDependent */, wct); + } + } else if (wasInPip && isInPip) { + // No update until exit PIP. + return; + } else if (isInPip) { + // Enter PIP. + // All overrides will be cleanup. + container.setLastRequestedBounds(null /* bounds */); + container.setLastRequestedWindowingMode(WINDOWING_MODE_UNDEFINED); + cleanupForEnterPip(wct, container); + } else if (wasInPip) { + // Exit PIP. + // Updates the presentation of the container. Expand or launch placeholder if + // needed. + updateContainer(wct, container); + } + mPresenter.applyTransaction(wct); + updateCallbackIfNecessary(); } - updateCallbackIfNecessary(); } @Override public void onTaskFragmentVanished(@NonNull TaskFragmentInfo taskFragmentInfo) { - TaskFragmentContainer container = getContainer(taskFragmentInfo.getFragmentToken()); - if (container == null) { - return; + synchronized (mLock) { + final TaskFragmentContainer container = getContainer( + taskFragmentInfo.getFragmentToken()); + if (container != null) { + // Cleanup if the TaskFragment vanished is not requested by the organizer. + removeContainer(container); + // Make sure the top container is updated. + final TaskFragmentContainer newTopContainer = getTopActiveContainer( + container.getTaskId()); + if (newTopContainer != null) { + final WindowContainerTransaction wct = new WindowContainerTransaction(); + updateContainer(wct, newTopContainer); + mPresenter.applyTransaction(wct); + } + updateCallbackIfNecessary(); + } + cleanupTaskFragment(taskFragmentInfo.getFragmentToken()); } - - mPresenter.cleanupContainer(container, true /* shouldFinishDependent */); - updateCallbackIfNecessary(); } @Override public void onTaskFragmentParentInfoChanged(@NonNull IBinder fragmentToken, @NonNull Configuration parentConfig) { - onParentBoundsMayChange(parentConfig.windowConfiguration.getBounds()); - TaskFragmentContainer container = getContainer(fragmentToken); - if (container != null) { - mPresenter.updateContainer(container); - updateCallbackIfNecessary(); + 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(); + } } } - private void onParentBoundsMayChange(Activity activity) { - if (activity.isFinishing()) { - return; + @Override + public void onActivityReparentToTask(int taskId, @NonNull Intent activityIntent, + @NonNull IBinder activityToken) { + synchronized (mLock) { + // If the activity belongs to the current app process, we treat it as a new activity + // launch. + final Activity activity = getActivity(activityToken); + if (activity != null) { + // We don't allow split as primary for new launch because we currently only support + // launching to top. We allow split as primary for activity reparent because the + // activity may be split as primary before it is reparented out. In that case, we + // want to show it as primary again when it is reparented back. + if (!resolveActivityToContainer(activity, true /* isOnReparent */)) { + // When there is no embedding rule matched, try to place it in the top container + // like a normal launch. + placeActivityInTopContainer(activity); + } + updateCallbackIfNecessary(); + return; + } + + final TaskContainer taskContainer = getTaskContainer(taskId); + if (taskContainer == null || taskContainer.isInPictureInPicture()) { + // We don't embed activity when it is in PIP. + return; + } + + // If the activity belongs to a different app process, we treat it as starting new + // intent, since both actions might result in a new activity that should appear in an + // organized TaskFragment. + final WindowContainerTransaction wct = new WindowContainerTransaction(); + TaskFragmentContainer targetContainer = resolveStartActivityIntent(wct, taskId, + activityIntent, null /* launchingActivity */); + if (targetContainer == null) { + // 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(). } + } - onParentBoundsMayChange(mPresenter.getParentContainerBounds(activity)); + /** 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()); + } + return; + } } - private void onParentBoundsMayChange(Rect parentBounds) { - if (!parentBounds.isEmpty() && !mParentBounds.equals(parentBounds)) { - mParentBounds.set(parentBounds); - updateAnimationOverride(); + private void onTaskConfigurationChanged(int taskId, @NonNull Configuration config) { + final TaskContainer taskContainer = mTaskContainers.get(taskId); + if (taskContainer == null) { + return; + } + final boolean wasInPip = taskContainer.isInPictureInPicture(); + final boolean isInPIp = isInPictureInPicture(config); + taskContainer.setWindowingMode(config.windowConfiguration.getWindowingMode()); + + // We need to check the animation override when enter/exit PIP or has bounds changed. + boolean shouldUpdateAnimationOverride = wasInPip != isInPIp; + if (taskContainer.setTaskBounds(config.windowConfiguration.getBounds()) + && !isInPIp) { + // We don't care the bounds change when it has already entered PIP. + shouldUpdateAnimationOverride = true; + } + if (shouldUpdateAnimationOverride) { + updateAnimationOverride(taskContainer); } } @@ -191,158 +331,563 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen * Updates if we should override transition animation. We only want to override if the Task * bounds is large enough for at least one split rule. */ - private void updateAnimationOverride() { - if (mParentBounds.isEmpty()) { - // We don't know about the parent bounds yet. + private void updateAnimationOverride(@NonNull TaskContainer taskContainer) { + if (!taskContainer.isTaskBoundsInitialized() + || !taskContainer.isWindowingModeInitialized()) { + // We don't know about the Task bounds/windowingMode yet. return; } + // We only want to override if it supports split. + if (supportSplit(taskContainer)) { + mPresenter.startOverrideSplitAnimation(taskContainer.getTaskId()); + } else { + mPresenter.stopOverrideSplitAnimation(taskContainer.getTaskId()); + } + } + + private boolean supportSplit(@NonNull TaskContainer taskContainer) { + // No split inside PIP. + if (taskContainer.isInPictureInPicture()) { + return false; + } // Check if the parent container bounds can support any split rule. - boolean supportSplit = false; for (EmbeddingRule rule : mSplitRules) { if (!(rule instanceof SplitRule)) { continue; } - if (mPresenter.shouldShowSideBySide(mParentBounds, (SplitRule) rule)) { - supportSplit = true; - break; + if (shouldShowSideBySide(taskContainer.getTaskBounds(), (SplitRule) rule)) { + return true; } } - - // We only want to override if it supports split. - if (supportSplit) { - mPresenter.startOverrideSplitAnimation(); - } else { - mPresenter.stopOverrideSplitAnimation(); - } + return false; } + @VisibleForTesting void onActivityCreated(@NonNull Activity launchedActivity) { - handleActivityCreated(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 */); updateCallbackIfNecessary(); } /** - * Checks if the activity start should be routed to a particular container. It can create a new - * container for the activity and a new split container if necessary. + * Checks if the new added activity should be routed to a particular container. It can create a + * new container for the activity and a new split container if necessary. + * @param activity the activity that is newly added to the Task. + * @param isOnReparent whether the activity is reparented to the Task instead of new launched. + * We only support to split as primary for reparented activity for now. + * @return {@code true} if the activity has been handled, such as placed in a TaskFragment, or + * in a state that the caller shouldn't handle. */ - // TODO(b/190433398): Break down into smaller functions. - void handleActivityCreated(@NonNull Activity launchedActivity) { - final List<EmbeddingRule> splitRules = getSplitRules(); - final TaskFragmentContainer currentContainer = getContainerWithActivity( - launchedActivity.getActivityToken()); + @VisibleForTesting + boolean resolveActivityToContainer(@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. + return true; + } - if (currentContainer == null) { - // Initial check before any TaskFragment is created. - onParentBoundsMayChange(launchedActivity); + if (!isOnReparent && getContainerWithActivity(activity) == null + && getInitialTaskFragmentToken(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. + // 2. the organizer has already requested to remove the TaskFragment. + // In either case, return true since we don't want any extra handling. + Log.d(TAG, "Activity is in a TaskFragment that is not recorded by the organizer. r=" + + activity); + return true; } - // Check if the activity is configured to always be expanded. - if (shouldExpand(launchedActivity, null, splitRules)) { - if (shouldContainerBeExpanded(currentContainer)) { - // Make sure that the existing container is expanded - mPresenter.expandTaskFragment(currentContainer.getTaskFragmentToken()); - } else { - // Put activity into a new expanded container - final TaskFragmentContainer newContainer = newContainer(launchedActivity); - mPresenter.expandActivity(newContainer.getTaskFragmentToken(), - launchedActivity); - } - return; + /* + * We will check the following to see if there is any embedding rule matched: + * 1. Whether the new launched activity should always expand. + * 2. Whether the new launched activity should launch a placeholder. + * 3. Whether the new launched activity has already been in a split with a rule matched + * (likely done in #onStartActivity). + * 4. Whether the activity below (if any) should be split with the new launched activity. + * 5. Whether the activity split with the activity below (if any) should be split with the + * new launched activity. + */ + + // 1. Whether the new launched activity should always expand. + if (shouldExpand(activity, null /* intent */)) { + expandActivity(activity); + return true; } - // Check if activity requires a placeholder - if (launchPlaceholderIfNecessary(launchedActivity)) { + // 2. Whether the new launched activity should launch a placeholder. + if (launchPlaceholderIfNecessary(activity, !isOnReparent)) { + return true; + } + + // 3. Whether the new launched activity has already been in a split with a rule matched. + if (isNewActivityInSplitWithRuleMatched(activity)) { + return true; + } + + // 4. Whether the activity below (if any) should be split with the new launched activity. + final Activity activityBelow = findActivityBelow(activity); + if (activityBelow == null) { + // Can't find any activity below. + return false; + } + if (putActivitiesIntoSplitIfNecessary(activityBelow, activity)) { + // Have split rule of [ activityBelow | launchedActivity ]. + return true; + } + if (isOnReparent && putActivitiesIntoSplitIfNecessary(activity, activityBelow)) { + // Have split rule of [ launchedActivity | activityBelow]. + return true; + } + + // 5. Whether the activity split with the activity below (if any) should be split with the + // new launched activity. + final TaskFragmentContainer activityBelowContainer = getContainerWithActivity( + activityBelow); + final SplitContainer topSplit = getActiveSplitForContainer(activityBelowContainer); + if (topSplit == null || !isTopMostSplit(topSplit)) { + // Skip if it is not the topmost split. + return false; + } + final TaskFragmentContainer otherTopContainer = + topSplit.getPrimaryContainer() == activityBelowContainer + ? topSplit.getSecondaryContainer() + : topSplit.getPrimaryContainer(); + final Activity otherTopActivity = otherTopContainer.getTopNonFinishingActivity(); + if (otherTopActivity == null || otherTopActivity == activity) { + // Can't find the top activity on the other split TaskFragment. + return false; + } + if (putActivitiesIntoSplitIfNecessary(otherTopActivity, activity)) { + // Have split rule of [ otherTopActivity | launchedActivity ]. + return true; + } + // Have split rule of [ launchedActivity | otherTopActivity]. + return isOnReparent && putActivitiesIntoSplitIfNecessary(activity, otherTopActivity); + } + + /** + * Places the given activity to the top most TaskFragment in the task if there is any. + */ + @VisibleForTesting + void placeActivityInTopContainer(@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. return; } + final int taskId = getTaskId(activity); + final TaskContainer taskContainer = getTaskContainer(taskId); + if (taskContainer == null) { + return; + } + final TaskFragmentContainer targetContainer = taskContainer.getTopTaskFragmentContainer(); + if (targetContainer == null) { + 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, + @Nullable Bundle options, @NonNull SplitRule sideRule, + @Nullable Consumer<Exception> failureCallback, boolean isPlaceholder) { + try { + mPresenter.startActivityToSide(launchingActivity, intent, options, sideRule, + isPlaceholder); + } catch (Exception e) { + if (failureCallback != null) { + failureCallback.accept(e); + } + } + } + + /** + * 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) { + final TaskFragmentContainer container = getContainerWithActivity(activity); + if (shouldContainerBeExpanded(container)) { + // Make sure that the existing container is expanded. + mPresenter.expandTaskFragment(container.getTaskFragmentToken()); + } else { + // Put activity into a new expanded container. + final TaskFragmentContainer newContainer = newContainer(activity, getTaskId(activity)); + mPresenter.expandActivity(newContainer.getTaskFragmentToken(), activity); + } + } - // TODO(b/190433398): Check if it is a placeholder and there is already another split - // created by the primary activity. This is necessary for the case when the primary activity - // launched another secondary in the split, but the placeholder was still launched by the - // logic above. We didn't prevent the placeholder launcher because we didn't know that - // another secondary activity is coming up. + /** Whether the given new launched activity is in a split with a rule matched. */ + private boolean isNewActivityInSplitWithRuleMatched(@NonNull Activity launchedActivity) { + final TaskFragmentContainer container = getContainerWithActivity(launchedActivity); + final SplitContainer splitContainer = getActiveSplitForContainer(container); + if (splitContainer == null) { + return false; + } - // Check if the activity should form a split with the activity below in the same task - // fragment. + if (container == splitContainer.getPrimaryContainer()) { + // The new launched can be in the primary container when it is starting a new activity + // onCreate. + final TaskFragmentContainer secondaryContainer = splitContainer.getSecondaryContainer(); + final Intent secondaryIntent = secondaryContainer.getPendingAppearedIntent(); + if (secondaryIntent != null) { + // Check with the pending Intent before it is started on the server side. + // This can happen if the launched Activity start a new Intent to secondary during + // #onCreated(). + return getSplitRule(launchedActivity, secondaryIntent) != null; + } + final Activity secondaryActivity = secondaryContainer.getTopNonFinishingActivity(); + return secondaryActivity != null + && getSplitRule(launchedActivity, secondaryActivity) != null; + } + + // Check if the new launched activity is a placeholder. + if (splitContainer.getSplitRule() instanceof SplitPlaceholderRule) { + final SplitPlaceholderRule placeholderRule = + (SplitPlaceholderRule) splitContainer.getSplitRule(); + final ComponentName placeholderName = placeholderRule.getPlaceholderIntent() + .getComponent(); + // TODO(b/232330767): Do we have a better way to check this? + return placeholderName == null + || placeholderName.equals(launchedActivity.getComponentName()) + || placeholderRule.getPlaceholderIntent().equals(launchedActivity.getIntent()); + } + + // Check if the new launched activity should be split with the primary top activity. + final Activity primaryActivity = splitContainer.getPrimaryContainer() + .getTopNonFinishingActivity(); + if (primaryActivity == null) { + return false; + } + /* TODO(b/231845476) we should always respect clearTop. + final SplitPairRule curSplitRule = (SplitPairRule) splitContainer.getSplitRule(); + final SplitPairRule splitRule = getSplitRule(primaryActivity, launchedActivity); + return splitRule != null && haveSamePresentation(splitRule, curSplitRule) + // If the new launched split rule should clear top and it is not the bottom most, + // it means we should create a new split pair and clear the existing secondary. + && (!splitRule.shouldClearTop() + || container.getBottomMostActivity() == launchedActivity); + */ + return getSplitRule(primaryActivity, launchedActivity) != null; + } + + /** Finds the activity below the given activity. */ + @VisibleForTesting + @Nullable + Activity findActivityBelow(@NonNull Activity activity) { Activity activityBelow = null; - if (currentContainer != null) { - final List<Activity> containerActivities = currentContainer.collectActivities(); - final int index = containerActivities.indexOf(launchedActivity); + final TaskFragmentContainer container = getContainerWithActivity(activity); + if (container != null) { + final List<Activity> containerActivities = container.collectNonFinishingActivities(); + final int index = containerActivities.indexOf(activity); if (index > 0) { activityBelow = containerActivities.get(index - 1); } } if (activityBelow == null) { - IBinder belowToken = ActivityClient.getInstance().getActivityTokenBelow( - launchedActivity.getActivityToken()); + final IBinder belowToken = ActivityClient.getInstance().getActivityTokenBelow( + activity.getActivityToken()); if (belowToken != null) { - activityBelow = ActivityThread.currentActivityThread().getActivity(belowToken); + activityBelow = getActivity(belowToken); } } - if (activityBelow == null) { - return; - } + return activityBelow; + } - // Check if the split is already set. - final TaskFragmentContainer activityBelowContainer = getContainerWithActivity( - activityBelow.getActivityToken()); - if (currentContainer != null && activityBelowContainer != null) { - final SplitContainer existingSplit = getActiveSplitForContainers(currentContainer, - activityBelowContainer); - if (existingSplit != null) { - // There is already an active split with the activity below. - return; + /** + * 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) { + final SplitPairRule splitRule = getSplitRule(primaryActivity, secondaryActivity); + if (splitRule == null) { + return false; + } + final TaskFragmentContainer primaryContainer = getContainerWithActivity( + primaryActivity); + final SplitContainer splitContainer = getActiveSplitForContainer(primaryContainer); + if (splitContainer != null && primaryContainer == splitContainer.getPrimaryContainer() + && canReuseContainer(splitRule, splitContainer.getSplitRule())) { + // Can launch in the existing secondary container if the rules share the same + // presentation. + final TaskFragmentContainer secondaryContainer = splitContainer.getSecondaryContainer(); + if (secondaryContainer == getContainerWithActivity(secondaryActivity)) { + // The activity is already in the target TaskFragment. + 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); + return true; + } - final SplitPairRule splitPairRule = getSplitRule(activityBelow, launchedActivity, - splitRules); - if (splitPairRule == null) { + private void onActivityConfigurationChanged(@NonNull Activity activity) { + if (activity.isFinishing()) { + // Do nothing if the activity is currently finishing. return; } - mPresenter.createNewSplitContainer(activityBelow, launchedActivity, - splitPairRule); - } - - private void onActivityConfigurationChanged(@NonNull Activity activity) { - final TaskFragmentContainer currentContainer = getContainerWithActivity( - activity.getActivityToken()); + if (isInPictureInPicture(activity)) { + // We don't embed activity when it is in PIP. + return; + } + final TaskFragmentContainer currentContainer = getContainerWithActivity(activity); if (currentContainer != null) { // Changes to activities in controllers are handled in // onTaskFragmentParentInfoChanged return; } - // The bounds of the container may have been changed. - onParentBoundsMayChange(activity); // Check if activity requires a placeholder - launchPlaceholderIfNecessary(activity); + launchPlaceholderIfNecessary(activity, false /* isOnCreated */); + } + + @VisibleForTesting + void onActivityDestroyed(@NonNull Activity activity) { + // Remove any pending appeared activity, as the server won't send finished activity to the + // organizer. + for (int i = mTaskContainers.size() - 1; i >= 0; i--) { + mTaskContainers.valueAt(i).cleanupPendingAppearedActivity(activity); + } + // We didn't trigger the callback if there were any pending appeared activities, so check + // again after the pending is removed. + updateCallbackIfNecessary(); } /** - * Returns a container that this activity is registered with. An activity can only belong to one - * container, or no container at all. + * Called when we have been waiting too long for the TaskFragment to become non-empty after + * creation. + */ + void onTaskFragmentAppearEmptyTimeout(@NonNull TaskFragmentContainer container) { + mPresenter.cleanupContainer(container, false /* shouldFinishDependent */); + } + + /** + * When we are trying to handle a new activity Intent, returns the {@link TaskFragmentContainer} + * that we should reparent the new activity to if there is any embedding rule matched. + * + * @param wct {@link WindowContainerTransaction} including all the window change + * requests. The caller is responsible to call + * {@link android.window.TaskFragmentOrganizer#applyTransaction}. + * @param taskId The Task to start the activity in. + * @param intent The {@link Intent} for starting the new launched activity. + * @param launchingActivity The {@link Activity} that starts the new activity. We will + * prioritize to split the new activity with it if it is not + * {@code null}. + * @return the {@link TaskFragmentContainer} to start the new activity in. {@code null} if there + * is no embedding rule matched. */ + @VisibleForTesting @Nullable - TaskFragmentContainer getContainerWithActivity(@NonNull IBinder activityToken) { - for (TaskFragmentContainer container : mContainers) { - if (container.hasActivity(activityToken)) { + TaskFragmentContainer resolveStartActivityIntent(@NonNull WindowContainerTransaction wct, + int taskId, @NonNull Intent intent, @Nullable Activity launchingActivity) { + /* + * We will check the following to see if there is any embedding rule matched: + * 1. Whether the new activity intent should always expand. + * 2. Whether the launching activity (if set) should be split with the new activity intent. + * 3. Whether the top activity (if any) should be split with the new activity intent. + * 4. Whether the top activity (if any) in other split should be split with the new + * activity intent. + */ + + // 1. Whether the new activity intent should always expand. + if (shouldExpand(null /* activity */, intent)) { + return createEmptyExpandedContainer(wct, intent, taskId, launchingActivity); + } + + // 2. Whether the launching activity (if set) should be split with the new activity intent. + if (launchingActivity != null) { + final TaskFragmentContainer container = getSecondaryContainerForSplitIfAny(wct, + launchingActivity, intent, true /* respectClearTop */); + if (container != null) { return container; } } + // 3. Whether the top activity (if any) should be split with the new activity intent. + final TaskContainer taskContainer = getTaskContainer(taskId); + if (taskContainer == null || taskContainer.getTopTaskFragmentContainer() == null) { + // There is no other activity in the Task to check split with. + return null; + } + final TaskFragmentContainer topContainer = taskContainer.getTopTaskFragmentContainer(); + final Activity topActivity = topContainer.getTopNonFinishingActivity(); + if (topActivity != null && topActivity != launchingActivity) { + final TaskFragmentContainer container = getSecondaryContainerForSplitIfAny(wct, + topActivity, intent, false /* respectClearTop */); + if (container != null) { + return container; + } + } + + // 4. Whether the top activity (if any) in other split should be split with the new + // activity intent. + final SplitContainer topSplit = getActiveSplitForContainer(topContainer); + if (topSplit == null) { + return null; + } + final TaskFragmentContainer otherTopContainer = + topSplit.getPrimaryContainer() == topContainer + ? topSplit.getSecondaryContainer() + : topSplit.getPrimaryContainer(); + final Activity otherTopActivity = otherTopContainer.getTopNonFinishingActivity(); + if (otherTopActivity != null && otherTopActivity != launchingActivity) { + return getSecondaryContainerForSplitIfAny(wct, otherTopActivity, intent, + false /* respectClearTop */); + } + return null; + } + + /** + * Returns an empty expanded {@link TaskFragmentContainer} that we can launch an activity into. + */ + @Nullable + private TaskFragmentContainer createEmptyExpandedContainer( + @NonNull WindowContainerTransaction wct, @NonNull Intent intent, int taskId, + @Nullable Activity launchingActivity) { + // We need an activity in the organizer process in the same Task to use as the owner + // activity, as well as to get the Task window info. + final Activity activityInTask; + if (launchingActivity != null) { + activityInTask = launchingActivity; + } else { + final TaskContainer taskContainer = getTaskContainer(taskId); + activityInTask = taskContainer != null + ? taskContainer.getTopNonFinishingActivity() + : null; + } + if (activityInTask == null) { + // Can't find any activity in the Task that we can use as the owner activity. + return null; + } + final TaskFragmentContainer expandedContainer = newContainer(intent, activityInTask, + taskId); + mPresenter.createTaskFragment(wct, expandedContainer.getTaskFragmentToken(), + activityInTask.getActivityToken(), new Rect(), WINDOWING_MODE_UNDEFINED); + return expandedContainer; + } + + /** + * Returns a container for the new activity intent to launch into as splitting with the primary + * activity. + */ + @Nullable + private TaskFragmentContainer getSecondaryContainerForSplitIfAny( + @NonNull WindowContainerTransaction wct, @NonNull Activity primaryActivity, + @NonNull Intent intent, boolean respectClearTop) { + final SplitPairRule splitRule = getSplitRule(primaryActivity, intent); + if (splitRule == null) { + return null; + } + final TaskFragmentContainer existingContainer = getContainerWithActivity(primaryActivity); + final SplitContainer splitContainer = getActiveSplitForContainer(existingContainer); + if (splitContainer != null && existingContainer == splitContainer.getPrimaryContainer() + && (canReuseContainer(splitRule, splitContainer.getSplitRule()) + // TODO(b/231845476) we should always respect clearTop. + || !respectClearTop) + && mPresenter.expandSplitContainerIfNeeded(wct, splitContainer, primaryActivity, + null /* secondaryActivity */, intent) != RESULT_EXPAND_FAILED_NO_TF_INFO) { + // Can launch in the existing secondary container if the rules share the same + // presentation. + return splitContainer.getSecondaryContainer(); + } + // Create a new TaskFragment to split with the primary activity for the new activity. + return mPresenter.createNewSplitWithEmptySideContainer(wct, primaryActivity, intent, + splitRule); + } + + /** + * Returns a container that this activity is registered with. An activity can only belong to one + * container, or no container at all. + */ + @Nullable + TaskFragmentContainer getContainerWithActivity(@NonNull Activity activity) { + final IBinder activityToken = activity.getActivityToken(); + 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)) { + return container; + } + } + } return null; } + TaskFragmentContainer newContainer(@NonNull Activity pendingAppearedActivity, int taskId) { + return newContainer(pendingAppearedActivity, pendingAppearedActivity, taskId); + } + + TaskFragmentContainer newContainer(@NonNull Activity pendingAppearedActivity, + @NonNull Activity activityInTask, int taskId) { + return newContainer(pendingAppearedActivity, null /* pendingAppearedIntent */, + activityInTask, taskId); + } + + TaskFragmentContainer newContainer(@NonNull Intent pendingAppearedIntent, + @NonNull Activity activityInTask, int taskId) { + return newContainer(null /* pendingAppearedActivity */, pendingAppearedIntent, + activityInTask, taskId); + } + /** * Creates and registers a new organized container with an optional activity that will be * re-parented to it in a WCT. + * + * @param pendingAppearedActivity the activity that will be reparented to the TaskFragment. + * @param pendingAppearedIntent the Intent that will be started in the TaskFragment. + * @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. */ - TaskFragmentContainer newContainer(@Nullable Activity activity) { - TaskFragmentContainer container = new TaskFragmentContainer(activity); - mContainers.add(container); + TaskFragmentContainer newContainer(@Nullable Activity pendingAppearedActivity, + @Nullable Intent pendingAppearedIntent, @NonNull Activity activityInTask, int taskId) { + if (activityInTask == null) { + throw new IllegalArgumentException("activityInTask must not be null,"); + } + if (!mTaskContainers.contains(taskId)) { + mTaskContainers.put(taskId, new TaskContainer(taskId)); + } + 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); return container; } @@ -354,13 +899,49 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen @NonNull TaskFragmentContainer primaryContainer, @NonNull Activity primaryActivity, @NonNull TaskFragmentContainer secondaryContainer, @NonNull SplitRule splitRule) { - SplitContainer splitContainer = new SplitContainer(primaryContainer, primaryActivity, + final SplitContainer splitContainer = new SplitContainer(primaryContainer, primaryActivity, secondaryContainer, splitRule); // Remove container later to prevent pinning escaping toast showing in lock task mode. if (splitRule instanceof SplitPairRule && ((SplitPairRule) splitRule).shouldClearTop()) { removeExistingSecondaryContainers(wct, primaryContainer); } - mSplitContainers.add(splitContainer); + primaryContainer.getTaskContainer().mSplitContainers.add(splitContainer); + } + + /** Cleanups all the dependencies when the TaskFragment is entering PIP. */ + private void cleanupForEnterPip(@NonNull WindowContainerTransaction wct, + @NonNull TaskFragmentContainer container) { + final TaskContainer taskContainer = container.getTaskContainer(); + if (taskContainer == null) { + return; + } + final List<SplitContainer> splitsToRemove = new ArrayList<>(); + final Set<TaskFragmentContainer> containersToUpdate = new ArraySet<>(); + for (SplitContainer splitContainer : taskContainer.mSplitContainers) { + if (splitContainer.getPrimaryContainer() != container + && splitContainer.getSecondaryContainer() != container) { + continue; + } + splitsToRemove.add(splitContainer); + final TaskFragmentContainer splitTf = splitContainer.getPrimaryContainer() == container + ? splitContainer.getSecondaryContainer() + : splitContainer.getPrimaryContainer(); + containersToUpdate.add(splitTf); + // We don't want the PIP TaskFragment to be removed as a result of any of its dependents + // being removed. + splitTf.removeContainerToFinishOnExit(container); + if (container.getTopNonFinishingActivity() != null) { + splitTf.removeActivityToFinishOnExit(container.getTopNonFinishingActivity()); + } + } + container.resetDependencies(); + taskContainer.mSplitContainers.removeAll(splitsToRemove); + // If there is any TaskFragment split with the PIP TaskFragment, update their presentations + // since the split is dismissed. + // We don't want to close any of them even if they are dependencies of the PIP TaskFragment. + for (TaskFragmentContainer containerToUpdate : containersToUpdate) { + updateContainer(wct, containerToUpdate); + } } /** @@ -368,15 +949,31 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen */ void removeContainer(@NonNull TaskFragmentContainer container) { // Remove all split containers that included this one - mContainers.remove(container); - List<SplitContainer> containersToRemove = new ArrayList<>(); - for (SplitContainer splitContainer : mSplitContainers) { + final TaskContainer taskContainer = container.getTaskContainer(); + if (taskContainer == null) { + return; + } + taskContainer.mContainers.remove(container); + // 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()); + + // Cleanup any split references. + final List<SplitContainer> containersToRemove = new ArrayList<>(); + for (SplitContainer splitContainer : taskContainer.mSplitContainers) { if (container.equals(splitContainer.getSecondaryContainer()) || container.equals(splitContainer.getPrimaryContainer())) { containersToRemove.add(splitContainer); } } - mSplitContainers.removeAll(containersToRemove); + taskContainer.mSplitContainers.removeAll(containersToRemove); + + // Cleanup any dependent references. + for (TaskFragmentContainer containerToUpdate : taskContainer.mContainers) { + containerToUpdate.removeContainerToFinishOnExit(container); + } } /** @@ -399,13 +996,22 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen } /** - * Returns the topmost not finished container. + * Returns the topmost not finished container in Task of given task id. */ @Nullable - TaskFragmentContainer getTopActiveContainer() { - for (int i = mContainers.size() - 1; i >= 0; i--) { - TaskFragmentContainer container = mContainers.get(i); - if (!container.isFinished() && container.getTopNonFinishingActivity() != null) { + TaskFragmentContainer getTopActiveContainer(int taskId) { + final TaskContainer taskContainer = mTaskContainers.get(taskId); + if (taskContainer == null) { + return null; + } + for (int i = taskContainer.mContainers.size() - 1; i >= 0; i--) { + final TaskFragmentContainer container = taskContainer.mContainers.get(i); + if (!container.isFinished() && (container.getRunningActivityCount() > 0 + // We may be waiting for the top TaskFragment to become non-empty after + // creation. In that case, we don't want to treat the TaskFragment below it as + // top active, otherwise it may incorrectly launch placeholder on top of the + // pending TaskFragment. + || container.isWaitingActivityAppear())) { return container; } } @@ -434,13 +1040,13 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen if (splitContainer == null) { return; } - if (splitContainer != mSplitContainers.get(mSplitContainers.size() - 1)) { + if (!isTopMostSplit(splitContainer)) { // Skip position update - it isn't the topmost split. return; } - if (splitContainer.getPrimaryContainer().isEmpty() - || splitContainer.getSecondaryContainer().isEmpty()) { - // Skip position update - one or both containers are empty. + if (splitContainer.getPrimaryContainer().isFinished() + || splitContainer.getSecondaryContainer().isFinished()) { + // Skip position update - one or both containers are finished. return; } if (dismissPlaceholderIfNecessary(splitContainer)) { @@ -450,13 +1056,27 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen mPresenter.updateSplitContainer(splitContainer, container, wct); } + /** Whether the given split is the topmost split in the Task. */ + private boolean isTopMostSplit(@NonNull SplitContainer splitContainer) { + final List<SplitContainer> splitContainers = splitContainer.getPrimaryContainer() + .getTaskContainer().mSplitContainers; + return splitContainer == splitContainers.get(splitContainers.size() - 1); + } + /** * Returns the top active split container that has the provided container, if available. */ @Nullable - private SplitContainer getActiveSplitForContainer(@NonNull TaskFragmentContainer container) { - for (int i = mSplitContainers.size() - 1; i >= 0; i--) { - SplitContainer splitContainer = mSplitContainers.get(i); + private SplitContainer getActiveSplitForContainer(@Nullable TaskFragmentContainer container) { + if (container == null) { + return null; + } + final List<SplitContainer> splitContainers = container.getTaskContainer().mSplitContainers; + if (splitContainers.isEmpty()) { + return null; + } + for (int i = splitContainers.size() - 1; i >= 0; i--) { + final SplitContainer splitContainer = splitContainers.get(i); if (container.equals(splitContainer.getSecondaryContainer()) || container.equals(splitContainer.getPrimaryContainer())) { return splitContainer; @@ -469,12 +1089,15 @@ 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 @Nullable - private SplitContainer getActiveSplitForContainers( + SplitContainer getActiveSplitForContainers( @NonNull TaskFragmentContainer firstContainer, @NonNull TaskFragmentContainer secondContainer) { - for (int i = mSplitContainers.size() - 1; i >= 0; i--) { - SplitContainer splitContainer = mSplitContainers.get(i); + final List<SplitContainer> splitContainers = firstContainer.getTaskContainer() + .mSplitContainers; + for (int i = splitContainers.size() - 1; i >= 0; i--) { + final SplitContainer splitContainer = splitContainers.get(i); final TaskFragmentContainer primary = splitContainer.getPrimaryContainer(); final TaskFragmentContainer secondary = splitContainer.getSecondaryContainer(); if ((firstContainer == secondary && secondContainer == primary) @@ -494,15 +1117,21 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen return false; } - return launchPlaceholderIfNecessary(topActivity); + return launchPlaceholderIfNecessary(topActivity, false /* isOnCreated */); } - boolean launchPlaceholderIfNecessary(@NonNull Activity activity) { - final TaskFragmentContainer container = getContainerWithActivity( - activity.getActivityToken()); + boolean launchPlaceholderIfNecessary(@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; + } - SplitContainer splitContainer = container != null ? getActiveSplitForContainer(container) - : null; + final SplitContainer splitContainer = getActiveSplitForContainer(container); if (splitContainer != null && container.equals(splitContainer.getPrimaryContainer())) { // Don't launch placeholder in primary split container return false; @@ -510,18 +1139,49 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen // Check if there is enough space for launch final SplitPlaceholderRule placeholderRule = getPlaceholderRule(activity); - if (placeholderRule == null || !mPresenter.shouldShowSideBySide( - mPresenter.getParentContainerBounds(activity), placeholderRule)) { + + if (placeholderRule == null) { + return false; + } + + final Pair<Size, Size> minDimensionsPair = getActivityIntentMinDimensionsPair(activity, + placeholderRule.getPlaceholderIntent()); + if (!shouldShowSideBySide( + mPresenter.getParentContainerBounds(activity), placeholderRule, + minDimensionsPair)) { return false; } // TODO(b/190433398): Handle failed request - startActivityToSide(activity, placeholderRule.getPlaceholderIntent(), null, - placeholderRule, null); + final Bundle options = getPlaceholderOptions(activity, isOnCreated); + startActivityToSide(activity, placeholderRule.getPlaceholderIntent(), options, + placeholderRule, null /* failureCallback */, true /* isPlaceholder */); return true; } - private boolean dismissPlaceholderIfNecessary(@NonNull SplitContainer splitContainer) { + /** + * Gets the activity options for starting the placeholder activity. In case the placeholder is + * launched when the Task is in the background, we don't want to bring the Task to the front. + * @param primaryActivity the primary activity to launch the placeholder from. + * @param isOnCreated whether this happens during the primary activity onCreated. + */ + @VisibleForTesting + @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 + // the Task is currently in background. + // Check if the primary is resumed or if this is called when the primary is onCreated + // (not resumed yet). + if (isOnCreated || primaryActivity.isResumed()) { + return null; + } + final ActivityOptions options = ActivityOptions.makeBasic(); + options.setAvoidMoveToFront(); + return options.toBundle(); + } + + @VisibleForTesting + boolean dismissPlaceholderIfNecessary(@NonNull SplitContainer splitContainer) { if (!splitContainer.isPlaceholderContainer()) { return false; } @@ -531,7 +1191,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen return false; } - if (mPresenter.shouldShowSideBySide(splitContainer)) { + if (shouldShowSideBySide(splitContainer)) { return false; } @@ -584,24 +1244,30 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen @Nullable private List<SplitInfo> getActiveSplitStates() { List<SplitInfo> splitStates = new ArrayList<>(); - for (SplitContainer container : mSplitContainers) { - 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; - } - ActivityStack primaryContainer = container.getPrimaryContainer().toActivityStack(); - ActivityStack secondaryContainer = container.getSecondaryContainer().toActivityStack(); - 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. - mPresenter.shouldShowSideBySide(container) - ? container.getSplitRule().getSplitRatio() - : 0.0f); - splitStates.add(splitState); + 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); + } } return splitStates; } @@ -611,11 +1277,12 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen * the client. */ private boolean allActivitiesCreated() { - for (TaskFragmentContainer container : mContainers) { - if (container.getInfo() == null - || container.getInfo().getActivities().size() - != container.collectActivities().size()) { - return false; + 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; + } } } return true; @@ -629,13 +1296,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen if (container == null) { return false; } - for (SplitContainer splitContainer : mSplitContainers) { - if (container.equals(splitContainer.getPrimaryContainer()) - || container.equals(splitContainer.getSecondaryContainer())) { - return false; - } - } - return true; + return getActiveSplitForContainer(container) == null; } /** @@ -643,9 +1304,9 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen * if available. */ @Nullable - private static SplitPairRule getSplitRule(@NonNull Activity primaryActivity, - @NonNull Intent secondaryActivityIntent, @NonNull List<EmbeddingRule> splitRules) { - for (EmbeddingRule rule : splitRules) { + private SplitPairRule getSplitRule(@NonNull Activity primaryActivity, + @NonNull Intent secondaryActivityIntent) { + for (EmbeddingRule rule : mSplitRules) { if (!(rule instanceof SplitPairRule)) { continue; } @@ -661,9 +1322,9 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen * Returns a split rule for the provided pair of primary and secondary activities if available. */ @Nullable - private static SplitPairRule getSplitRule(@NonNull Activity primaryActivity, - @NonNull Activity secondaryActivity, @NonNull List<EmbeddingRule> splitRules) { - for (EmbeddingRule rule : splitRules) { + private SplitPairRule getSplitRule(@NonNull Activity primaryActivity, + @NonNull Activity secondaryActivity) { + for (EmbeddingRule rule : mSplitRules) { if (!(rule instanceof SplitPairRule)) { continue; } @@ -680,24 +1341,56 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen @Nullable TaskFragmentContainer getContainer(@NonNull IBinder fragmentToken) { - for (TaskFragmentContainer container : mContainers) { - if (container.getTaskFragmentToken().equals(fragmentToken)) { - return container; + for (int i = mTaskContainers.size() - 1; i >= 0; i--) { + final List<TaskFragmentContainer> containers = mTaskContainers.valueAt(i).mContainers; + for (TaskFragmentContainer container : containers) { + if (container.getTaskFragmentToken().equals(fragmentToken)) { + return container; + } } } return null; } + @Nullable + TaskContainer getTaskContainer(int taskId) { + return mTaskContainers.get(taskId); + } + + Handler getHandler() { + return mHandler; + } + + int getTaskId(@NonNull Activity activity) { + // Prefer to get the taskId from TaskFragmentContainer because Activity.getTaskId() is an + // IPC call. + final TaskFragmentContainer container = getContainerWithActivity(activity); + return container != null ? container.getTaskId() : activity.getTaskId(); + } + + @Nullable + Activity getActivity(@NonNull IBinder activityToken) { + return ActivityThread.currentActivityThread().getActivity(activityToken); + } + + /** + * Gets the token of the initial TaskFragment that embedded this activity. Do not rely on it + * after creation because the activity could be reparented. + */ + @VisibleForTesting + @Nullable + IBinder getInitialTaskFragmentToken(@NonNull Activity activity) { + final ActivityThread.ActivityClientRecord record = ActivityThread.currentActivityThread() + .getActivityClient(activity.getActivityToken()); + return record != null ? record.mInitialTaskFragmentToken : null; + } + /** * Returns {@code true} if an Activity with the provided component name should always be * expanded to occupy full task bounds. Such activity must not be put in a split. */ - private static boolean shouldExpand(@Nullable Activity activity, @Nullable Intent intent, - List<EmbeddingRule> splitRules) { - if (splitRules == null) { - return false; - } - for (EmbeddingRule rule : splitRules) { + private boolean shouldExpand(@Nullable Activity activity, @Nullable Intent intent) { + for (EmbeddingRule rule : mSplitRules) { if (!(rule instanceof ActivityRule)) { continue; } @@ -739,7 +1432,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen } // Decide whether the associated container should be retained based on the current // presentation mode. - if (mPresenter.shouldShowSideBySide(splitContainer)) { + if (shouldShowSideBySide(splitContainer)) { return !shouldFinishAssociatedContainerWhenAdjacent(finishBehavior); } else { return !shouldFinishAssociatedContainerWhenStacked(finishBehavior); @@ -751,8 +1444,8 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen */ boolean shouldRetainAssociatedActivity(@NonNull TaskFragmentContainer finishingContainer, @NonNull Activity associatedActivity) { - TaskFragmentContainer associatedContainer = getContainerWithActivity( - associatedActivity.getActivityToken()); + final TaskFragmentContainer associatedContainer = getContainerWithActivity( + associatedActivity); if (associatedContainer == null) { return false; } @@ -763,17 +1456,57 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen private final class LifecycleCallbacks extends EmptyLifecycleCallbacksAdapter { @Override + public void onActivityPreCreated(Activity activity, Bundle savedInstanceState) { + synchronized (mLock) { + final IBinder activityToken = activity.getActivityToken(); + final IBinder initialTaskFragmentToken = getInitialTaskFragmentToken(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) { + return; + } + 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.hasActivity(activityToken) + && container.getTaskFragmentToken() + .equals(initialTaskFragmentToken)) { + // The onTaskFragmentInfoChanged callback containing this activity has + // not reached the client yet, so add the activity to the pending + // appeared activities. + container.addPendingAppearedActivity(activity); + return; + } + } + } + } + } + + @Override public void onActivityPostCreated(Activity activity, Bundle savedInstanceState) { // Calling after Activity#onCreate is complete to allow the app launch something // first. In case of a configured placeholder activity we want to make sure // that we don't launch it if an activity itself already requested something to be // launched to side. - SplitController.this.onActivityCreated(activity); + synchronized (mLock) { + SplitController.this.onActivityCreated(activity); + } } @Override public void onActivityConfigurationChanged(Activity activity) { - SplitController.this.onActivityConfigurationChanged(activity); + synchronized (mLock) { + SplitController.this.onActivityConfigurationChanged(activity); + } + } + + @Override + public void onActivityPostDestroyed(Activity activity) { + synchronized (mLock) { + SplitController.this.onActivityDestroyed(activity); + } } } @@ -803,130 +1536,26 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen return super.onStartActivity(who, intent, options); } final Activity launchingActivity = (Activity) who; - - if (shouldExpand(null, intent, getSplitRules())) { - setLaunchingInExpandedContainer(launchingActivity, options); - } else if (!splitWithLaunchingActivity(launchingActivity, intent, options)) { - setLaunchingInSameSideContainer(launchingActivity, intent, options); - } - - return super.onStartActivity(who, intent, options); - } - - private void setLaunchingInExpandedContainer(Activity launchingActivity, Bundle options) { - TaskFragmentContainer newContainer = mPresenter.createNewExpandedContainer( - launchingActivity); - - // Amend the request to let the WM know that the activity should be placed in the - // dedicated container. - options.putBinder(ActivityOptions.KEY_LAUNCH_TASK_FRAGMENT_TOKEN, - newContainer.getTaskFragmentToken()); - } - - /** - * Returns {@code true} if the activity that is going to be started via the - * {@code intent} should be paired with the {@code launchingActivity} and is set to be - * launched in the side container. - */ - private boolean splitWithLaunchingActivity(Activity launchingActivity, Intent intent, - Bundle options) { - final SplitPairRule splitPairRule = getSplitRule(launchingActivity, intent, - getSplitRules()); - if (splitPairRule == null) { - return false; - } - - // Check if there is any existing side container to launch into. - TaskFragmentContainer secondaryContainer = findSideContainerForNewLaunch( - launchingActivity, splitPairRule); - if (secondaryContainer == null) { - // Create a new split with an empty side container. - secondaryContainer = mPresenter - .createNewSplitWithEmptySideContainer(launchingActivity, splitPairRule); - } - - // Amend the request to let the WM know that the activity should be placed in the - // dedicated container. - options.putBinder(ActivityOptions.KEY_LAUNCH_TASK_FRAGMENT_TOKEN, - secondaryContainer.getTaskFragmentToken()); - return true; - } - - /** - * Finds if there is an existing split side {@link TaskFragmentContainer} that can be used - * for the new rule. - */ - @Nullable - private TaskFragmentContainer findSideContainerForNewLaunch(Activity launchingActivity, - SplitPairRule splitPairRule) { - final TaskFragmentContainer launchingContainer = getContainerWithActivity( - launchingActivity.getActivityToken()); - if (launchingContainer == null) { - return null; - } - - // We only check if the launching activity is the primary of the split. We will check - // if the launching activity is the secondary in #setLaunchingInSameSideContainer. - final SplitContainer splitContainer = getActiveSplitForContainer(launchingContainer); - if (splitContainer == null - || splitContainer.getPrimaryContainer() != launchingContainer) { - return null; - } - - if (canReuseContainer(splitPairRule, splitContainer.getSplitRule())) { - return splitContainer.getSecondaryContainer(); - } - return null; - } - - /** - * Checks if the activity that is going to be started via the {@code intent} should be - * paired with the existing top activity which is currently paired with the - * {@code launchingActivity}. If so, set the activity to be launched in the same side - * container of the {@code launchingActivity}. - */ - private void setLaunchingInSameSideContainer(Activity launchingActivity, Intent intent, - Bundle options) { - final TaskFragmentContainer launchingContainer = getContainerWithActivity( - launchingActivity.getActivityToken()); - if (launchingContainer == null) { - return; - } - - final SplitContainer splitContainer = getActiveSplitForContainer(launchingContainer); - if (splitContainer == null) { - return; - } - - if (splitContainer.getSecondaryContainer() != launchingContainer) { - return; - } - - // The launching activity is on the secondary container. Retrieve the primary - // activity from the other container. - Activity primaryActivity = - splitContainer.getPrimaryContainer().getTopNonFinishingActivity(); - if (primaryActivity == null) { - return; - } - - final SplitPairRule splitPairRule = getSplitRule(primaryActivity, intent, - getSplitRules()); - if (splitPairRule == null) { - return; + if (isInPictureInPicture(launchingActivity)) { + // We don't embed activity when it is in PIP. + return super.onStartActivity(who, intent, options); } - // Can only launch in the same container if the rules share the same presentation. - if (!canReuseContainer(splitPairRule, splitContainer.getSplitRule())) { - return; + synchronized (mLock) { + final int taskId = getTaskId(launchingActivity); + final WindowContainerTransaction wct = new WindowContainerTransaction(); + final TaskFragmentContainer launchedInTaskFragment = resolveStartActivityIntent(wct, + taskId, intent, launchingActivity); + if (launchedInTaskFragment != null) { + mPresenter.applyTransaction(wct); + // Amend the request to let the WM know that the activity should be placed in + // the dedicated container. + options.putBinder(ActivityOptions.KEY_LAUNCH_TASK_FRAGMENT_TOKEN, + launchedInTaskFragment.getTaskFragmentToken()); + } } - // Amend the request to let the WM know that the activity should be placed in the - // dedicated container. This is necessary for the case that the activity is started - // into a new Task, or new Task will be escaped from the current host Task and be - // displayed in fullscreen. - options.putBinder(ActivityOptions.KEY_LAUNCH_TASK_FRAGMENT_TOKEN, - launchingContainer.getTaskFragmentToken()); + return super.onStartActivity(who, intent, options); } } @@ -934,8 +1563,11 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen * Checks if an activity is embedded and its presentation is customized by a * {@link android.window.TaskFragmentOrganizer} to only occupy a portion of Task bounds. */ + @Override public boolean isActivityEmbedded(@NonNull Activity activity) { - return mPresenter.isActivityEmbedded(activity.getActivityToken()); + synchronized (mLock) { + return mPresenter.isActivityEmbedded(activity.getActivityToken()); + } } /** @@ -946,8 +1578,18 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen if (!isContainerReusableRule(rule1) || !isContainerReusableRule(rule2)) { return false; } + return haveSamePresentation((SplitPairRule) rule1, (SplitPairRule) rule2); + } + + /** Whether the two rules have the same presentation. */ + private static boolean haveSamePresentation(SplitPairRule rule1, SplitPairRule rule2) { + // TODO(b/231655482): add util method to do the comparison in SplitPairRule. return rule1.getSplitRatio() == rule2.getSplitRatio() - && rule1.getLayoutDirection() == rule2.getLayoutDirection(); + && rule1.getLayoutDirection() == rule2.getLayoutDirection() + && rule1.getFinishPrimaryWithSecondary() + == rule2.getFinishPrimaryWithSecondary() + && rule1.getFinishSecondaryWithPrimary() + == rule2.getFinishSecondaryWithPrimary(); } /** @@ -964,4 +1606,17 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen // Not reuse if it needs to destroy the existing. return !pairRule.shouldClearTop(); } + + private static boolean isInPictureInPicture(@NonNull Activity activity) { + return isInPictureInPicture(activity.getResources().getConfiguration()); + } + + private static boolean isInPictureInPicture(@NonNull TaskFragmentContainer tf) { + return isInPictureInPicture(tf.getInfo().getConfiguration()); + } + + private static boolean isInPictureInPicture(@Nullable Configuration configuration) { + return configuration != null + && configuration.windowConfiguration.getWindowingMode() == WINDOWING_MODE_PINNED; + } } 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 ade573132eef..a89847a30d20 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java @@ -16,26 +16,34 @@ package androidx.window.extensions.embedding; -import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; +import static android.content.pm.PackageManager.MATCH_ALL; 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.res.Configuration; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; import android.graphics.Rect; import android.os.Bundle; import android.os.IBinder; 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.TaskFragmentCreationParams; import android.window.WindowContainerTransaction; import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.android.internal.annotations.VisibleForTesting; + import java.util.concurrent.Executor; /** @@ -43,9 +51,12 @@ import java.util.concurrent.Executor; * {@link SplitController}. */ class SplitPresenter extends JetpackTaskFragmentOrganizer { - private static final int POSITION_START = 0; - private static final int POSITION_END = 1; - private static final int POSITION_FILL = 2; + @VisibleForTesting + static final int POSITION_START = 0; + @VisibleForTesting + static final int POSITION_END = 1; + @VisibleForTesting + static final int POSITION_FILL = 2; @IntDef(value = { POSITION_START, @@ -54,6 +65,41 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { }) private @interface Position {} + /** + * 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. + */ + static final int RESULT_NOT_EXPANDED = 0; + /** + * Result of {@link #expandSplitContainerIfNeeded(WindowContainerTransaction, SplitContainer, + * Activity, Activity, Intent)}. + * The splitContainer should be expanded. It is usually because minimum dimensions is not + * satisfied. + * @see #shouldShowSideBySide(Rect, SplitRule, Pair) + */ + static final int RESULT_EXPANDED = 1; + /** + * Result of {@link #expandSplitContainerIfNeeded(WindowContainerTransaction, SplitContainer, + * Activity, Activity, Intent)}. + * The splitContainer should be expanded, but the client side hasn't received + * {@link android.window.TaskFragmentInfo} yet. Fallback to create new expanded SplitContainer + * instead. + */ + static final int RESULT_EXPAND_FAILED_NO_TF_INFO = 2; + + /** + * Result of {@link #expandSplitContainerIfNeeded(WindowContainerTransaction, SplitContainer, + * Activity, Activity, Intent)} + */ + @IntDef(value = { + RESULT_NOT_EXPANDED, + RESULT_EXPANDED, + RESULT_EXPAND_FAILED_NO_TF_INFO, + }) + private @interface ResultCode {} + private final SplitController mController; SplitPresenter(@NonNull Executor executor, SplitController controller) { @@ -65,7 +111,7 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { /** * Updates the presentation of the provided container. */ - void updateContainer(TaskFragmentContainer container) { + void updateContainer(@NonNull TaskFragmentContainer container) { final WindowContainerTransaction wct = new WindowContainerTransaction(); mController.updateContainer(wct, container); applyTransaction(wct); @@ -77,47 +123,59 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { */ void cleanupContainer(@NonNull TaskFragmentContainer container, boolean shouldFinishDependent) { final WindowContainerTransaction wct = new WindowContainerTransaction(); + cleanupContainer(container, shouldFinishDependent, wct); + applyTransaction(wct); + } + /** + * Deletes the specified container and all other associated and dependent containers in the same + * transaction. + */ + void cleanupContainer(@NonNull TaskFragmentContainer container, boolean shouldFinishDependent, + @NonNull WindowContainerTransaction wct) { container.finish(shouldFinishDependent, this, wct, mController); - final TaskFragmentContainer newTopContainer = mController.getTopActiveContainer(); + final TaskFragmentContainer newTopContainer = mController.getTopActiveContainer( + container.getTaskId()); if (newTopContainer != null) { mController.updateContainer(wct, newTopContainer); } - - applyTransaction(wct); } /** * Creates a new split with the primary activity and an empty secondary container. * @return The newly created secondary container. */ - TaskFragmentContainer createNewSplitWithEmptySideContainer(@NonNull Activity primaryActivity, - @NonNull SplitPairRule rule) { - final WindowContainerTransaction wct = new WindowContainerTransaction(); - + @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, - isLtr(primaryActivity, rule)); + primaryActivity, minDimensionsPair); final TaskFragmentContainer primaryContainer = prepareContainerForActivity(wct, primaryActivity, primaryRectBounds, null); // Create new empty task fragment - final TaskFragmentContainer secondaryContainer = mController.newContainer(null); + final int taskId = primaryContainer.getTaskId(); + final TaskFragmentContainer secondaryContainer = mController.newContainer( + secondaryIntent, primaryActivity, taskId); final Rect secondaryRectBounds = getBoundsForPosition(POSITION_END, parentBounds, - rule, isLtr(primaryActivity, rule)); + rule, primaryActivity, minDimensionsPair); + final int windowingMode = mController.getTaskContainer(taskId) + .getWindowingModeForSplitTaskFragment(secondaryRectBounds); createTaskFragment(wct, secondaryContainer.getTaskFragmentToken(), primaryActivity.getActivityToken(), secondaryRectBounds, - WINDOWING_MODE_MULTI_WINDOW); - secondaryContainer.setLastRequestedBounds(secondaryRectBounds); + windowingMode); // Set adjacent to each other so that the containers below will be invisible. - setAdjacentTaskFragments(wct, primaryContainer, secondaryContainer, rule); + setAdjacentTaskFragments(wct, primaryContainer, secondaryContainer, rule, + minDimensionsPair); mController.registerSplit(wct, primaryContainer, primaryActivity, secondaryContainer, rule); - applyTransaction(wct); - return secondaryContainer; } @@ -137,18 +195,28 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { 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, - isLtr(primaryActivity, rule)); + primaryActivity, minDimensionsPair); final TaskFragmentContainer primaryContainer = prepareContainerForActivity(wct, primaryActivity, primaryRectBounds, null); final Rect secondaryRectBounds = getBoundsForPosition(POSITION_END, parentBounds, rule, - isLtr(primaryActivity, rule)); + primaryActivity, minDimensionsPair); + 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. + containerToAvoid = curSecondaryContainer; + } final TaskFragmentContainer secondaryContainer = prepareContainerForActivity(wct, - secondaryActivity, secondaryRectBounds, primaryContainer); + secondaryActivity, secondaryRectBounds, containerToAvoid); // Set adjacent to each other so that the containers below will be invisible. - setAdjacentTaskFragments(wct, primaryContainer, secondaryContainer, rule); + setAdjacentTaskFragments(wct, primaryContainer, secondaryContainer, rule, + minDimensionsPair); mController.registerSplit(wct, primaryContainer, primaryActivity, secondaryContainer, rule); @@ -156,20 +224,6 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { } /** - * Creates a new expanded container. - */ - TaskFragmentContainer createNewExpandedContainer(@NonNull Activity launchingActivity) { - final TaskFragmentContainer newContainer = mController.newContainer(null); - - final WindowContainerTransaction wct = new WindowContainerTransaction(); - createTaskFragment(wct, newContainer.getTaskFragmentToken(), - launchingActivity.getActivityToken(), new Rect(), WINDOWING_MODE_MULTI_WINDOW); - - applyTransaction(wct); - return newContainer; - } - - /** * Creates a new container or resizes an existing container for activity to the provided bounds. * @param activity The activity to be re-parented to the container if necessary. * @param containerToAvoid Re-parent from this container if an activity is already in it. @@ -177,25 +231,21 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { private TaskFragmentContainer prepareContainerForActivity( @NonNull WindowContainerTransaction wct, @NonNull Activity activity, @NonNull Rect bounds, @Nullable TaskFragmentContainer containerToAvoid) { - TaskFragmentContainer container = mController.getContainerWithActivity( - activity.getActivityToken()); + TaskFragmentContainer container = mController.getContainerWithActivity(activity); + final int taskId = container != null ? container.getTaskId() : activity.getTaskId(); if (container == null || container == containerToAvoid) { - container = mController.newContainer(activity); - - final TaskFragmentCreationParams fragmentOptions = - createFragmentOptions( - container.getTaskFragmentToken(), - activity.getActivityToken(), - bounds, - WINDOWING_MODE_MULTI_WINDOW); - wct.createTaskFragment(fragmentOptions); - + container = mController.newContainer(activity, taskId); + final int windowingMode = mController.getTaskContainer(taskId) + .getWindowingModeForSplitTaskFragment(bounds); + createTaskFragment(wct, container.getTaskFragmentToken(), activity.getActivityToken(), + bounds, windowingMode); wct.reparentActivityToTaskFragment(container.getTaskFragmentToken(), activity.getActivityToken()); - - container.setLastRequestedBounds(bounds); } else { resizeTaskFragmentIfRegistered(wct, container, bounds); + final int windowingMode = mController.getTaskContainer(taskId) + .getWindowingModeForSplitTaskFragment(bounds); + updateTaskFragmentWindowingModeIfRegistered(wct, container, windowingMode); } return container; @@ -207,35 +257,44 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { * @param launchingActivity An activity that should be in the primary container. If it is not * currently in an existing container, a new one will be created and * the activity will be re-parented to it. - * @param activityIntent The intent to start the new activity. - * @param activityOptions The options to apply to new activity start. - * @param rule The split rule to be applied to the container. + * @param activityIntent The intent to start the new activity. + * @param activityOptions The options to apply to new activity start. + * @param rule The split rule to be applied to the container. + * @param isPlaceholder Whether the launch is a placeholder. */ void startActivityToSide(@NonNull Activity launchingActivity, @NonNull Intent activityIntent, - @Nullable Bundle activityOptions, @NonNull SplitRule rule) { + @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, - isLtr(launchingActivity, rule)); + launchingActivity, minDimensionsPair); final Rect secondaryRectBounds = getBoundsForPosition(POSITION_END, parentBounds, rule, - isLtr(launchingActivity, rule)); + launchingActivity, minDimensionsPair); TaskFragmentContainer primaryContainer = mController.getContainerWithActivity( - launchingActivity.getActivityToken()); + launchingActivity); if (primaryContainer == null) { - primaryContainer = mController.newContainer(launchingActivity); + primaryContainer = mController.newContainer(launchingActivity, + launchingActivity.getTaskId()); } - TaskFragmentContainer secondaryContainer = mController.newContainer(null); + 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(); mController.registerSplit(wct, primaryContainer, launchingActivity, secondaryContainer, rule); startActivityToSide(wct, primaryContainer.getTaskFragmentToken(), primaryRectBounds, launchingActivity, secondaryContainer.getTaskFragmentToken(), secondaryRectBounds, - activityIntent, activityOptions, rule); + activityIntent, activityOptions, rule, windowingMode); + if (isPlaceholder) { + // When placeholder is launched in split, we should keep the focus on the primary. + wct.requestFocusOnTaskFragment(primaryContainer.getTaskFragmentToken()); + } applyTransaction(wct); - - primaryContainer.setLastRequestedBounds(primaryRectBounds); - secondaryContainer.setLastRequestedBounds(secondaryRectBounds); } /** @@ -255,28 +314,42 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { if (activity == null) { return; } - final boolean isLtr = isLtr(activity, rule); + final Pair<Size, Size> minDimensionsPair = splitContainer.getMinDimensionsPair(); final Rect primaryRectBounds = getBoundsForPosition(POSITION_START, parentBounds, rule, - isLtr); + activity, minDimensionsPair); final Rect secondaryRectBounds = getBoundsForPosition(POSITION_END, parentBounds, rule, - isLtr); + activity, minDimensionsPair); + 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(); // If the task fragments are not registered yet, the positions will be updated after they // are created again. resizeTaskFragmentIfRegistered(wct, primaryContainer, primaryRectBounds); - final TaskFragmentContainer secondaryContainer = splitContainer.getSecondaryContainer(); resizeTaskFragmentIfRegistered(wct, secondaryContainer, secondaryRectBounds); - - setAdjacentTaskFragments(wct, primaryContainer, secondaryContainer, rule); + setAdjacentTaskFragments(wct, primaryContainer, secondaryContainer, rule, + minDimensionsPair); + 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); + updateTaskFragmentWindowingModeIfRegistered(wct, primaryContainer, windowingMode); + updateTaskFragmentWindowingModeIfRegistered(wct, secondaryContainer, windowingMode); } private void setAdjacentTaskFragments(@NonNull WindowContainerTransaction wct, @NonNull TaskFragmentContainer primaryContainer, - @NonNull TaskFragmentContainer secondaryContainer, @NonNull SplitRule splitRule) { + @NonNull TaskFragmentContainer secondaryContainer, @NonNull SplitRule splitRule, + @NonNull Pair<Size, Size> minDimensionsPair) { final Rect parentBounds = getParentContainerBounds(primaryContainer); // Clear adjacent TaskFragments if the container is shown in fullscreen, or the // secondaryContainer could not be finished. - if (!shouldShowSideBySide(parentBounds, splitRule)) { + if (!shouldShowSideBySide(parentBounds, splitRule, minDimensionsPair)) { setAdjacentTaskFragments(wct, primaryContainer.getTaskFragmentToken(), null /* secondary */, null /* splitRule */); } else { @@ -299,6 +372,29 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { resizeTaskFragment(wct, container.getTaskFragmentToken(), bounds); } + private void updateTaskFragmentWindowingModeIfRegistered( + @NonNull WindowContainerTransaction wct, + @NonNull TaskFragmentContainer container, + @WindowingMode int windowingMode) { + if (container.getInfo() != null) { + updateWindowingMode(wct, container.getTaskFragmentToken(), windowingMode); + } + } + + @Override + void createTaskFragment(@NonNull WindowContainerTransaction wct, @NonNull IBinder fragmentToken, + @NonNull IBinder ownerToken, @NonNull Rect bounds, @WindowingMode int windowingMode) { + final TaskFragmentContainer container = mController.getContainer(fragmentToken); + if (container == null) { + throw new IllegalStateException( + "Creating a task fragment that is not registered with controller."); + } + + container.setLastRequestedBounds(bounds); + container.setLastRequestedWindowingMode(windowingMode); + super.createTaskFragment(wct, fragmentToken, ownerToken, bounds, windowingMode); + } + @Override void resizeTaskFragment(@NonNull WindowContainerTransaction wct, @NonNull IBinder fragmentToken, @Nullable Rect bounds) { @@ -317,41 +413,188 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { super.resizeTaskFragment(wct, fragmentToken, bounds); } - boolean shouldShowSideBySide(@NonNull SplitContainer splitContainer) { + @Override + void updateWindowingMode(@NonNull WindowContainerTransaction wct, + @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" + + " not registered with controller."); + } + + if (container.isLastRequestedWindowingModeEqual(windowingMode)) { + // Return early if the windowing mode were already requested + return; + } + + container.setLastRequestedWindowingMode(windowingMode); + super.updateWindowingMode(wct, fragmentToken, windowingMode); + } + + /** + * 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. + */ + @ResultCode + int expandSplitContainerIfNeeded(@NonNull WindowContainerTransaction wct, + @NonNull SplitContainer splitContainer, @NonNull Activity primaryActivity, + @Nullable Activity secondaryActivity, @Nullable Intent secondaryIntent) { + if (secondaryActivity == null && secondaryIntent == null) { + 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); + } else { + minDimensionsPair = getActivityIntentMinDimensionsPair(primaryActivity, + secondaryIntent); + } + // Expand the splitContainer if minimum dimensions are not satisfied. + if (!shouldShowSideBySide(taskBounds, splitContainer.getSplitRule(), minDimensionsPair)) { + // 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()); + return RESULT_EXPANDED; + } + return RESULT_NOT_EXPANDED; + } + + static boolean shouldShowSideBySide(@NonNull Rect parentBounds, @NonNull SplitRule rule) { + return shouldShowSideBySide(parentBounds, rule, null /* minimumDimensionPair */); + } + + static boolean shouldShowSideBySide(@NonNull SplitContainer splitContainer) { final Rect parentBounds = getParentContainerBounds(splitContainer.getPrimaryContainer()); - return shouldShowSideBySide(parentBounds, splitContainer.getSplitRule()); + + return shouldShowSideBySide(parentBounds, splitContainer.getSplitRule(), + splitContainer.getMinDimensionsPair()); } - boolean shouldShowSideBySide(@Nullable Rect parentBounds, @NonNull SplitRule rule) { + static boolean shouldShowSideBySide(@NonNull Rect parentBounds, @NonNull SplitRule rule, + @Nullable Pair<Size, Size> minDimensionsPair) { // TODO(b/190433398): Supply correct insets. final WindowMetrics parentMetrics = new WindowMetrics(parentBounds, new WindowInsets(new Rect())); - return rule.checkParentMetrics(parentMetrics); + // Don't show side by side if bounds is not qualified. + if (!rule.checkParentMetrics(parentMetrics)) { + return false; + } + 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 */); + + if (minDimensionsPair == null) { + return true; + } + return !boundsSmallerThanMinDimensions(primaryBounds, minDimensionsPair.first) + && !boundsSmallerThanMinDimensions(secondaryBounds, minDimensionsPair.second); } @NonNull - private Rect getBoundsForPosition(@Position int position, @NonNull Rect parentBounds, - @NonNull SplitRule rule, boolean isLtr) { - if (!shouldShowSideBySide(parentBounds, rule)) { - return new Rect(); + static Pair<Size, Size> getActivitiesMinDimensionsPair(Activity primaryActivity, + Activity secondaryActivity) { + return new Pair<>(getMinDimensions(primaryActivity), getMinDimensions(secondaryActivity)); + } + + @NonNull + static Pair<Size, Size> getActivityIntentMinDimensionsPair(Activity primaryActivity, + Intent secondaryIntent) { + return new Pair<>(getMinDimensions(primaryActivity), getMinDimensions(secondaryIntent)); + } + + @Nullable + static Size getMinDimensions(@Nullable Activity activity) { + if (activity == null) { + return null; + } + final ActivityInfo.WindowLayout windowLayout = activity.getActivityInfo().windowLayout; + if (windowLayout == null) { + return null; } + return new Size(windowLayout.minWidth, windowLayout.minHeight); + } + + // TODO(b/232871351): find a light-weight approach for this check. + @Nullable + static Size getMinDimensions(@Nullable Intent intent) { + if (intent == null) { + return null; + } + final PackageManager packageManager = ActivityThread.currentActivityThread() + .getApplication().getPackageManager(); + final ResolveInfo resolveInfo = packageManager.resolveActivity(intent, + PackageManager.ResolveInfoFlags.of(MATCH_ALL)); + if (resolveInfo == null) { + return null; + } + final ActivityInfo activityInfo = resolveInfo.activityInfo; + if (activityInfo == null) { + return null; + } + final ActivityInfo.WindowLayout windowLayout = activityInfo.windowLayout; + if (windowLayout == null) { + return null; + } + return new Size(windowLayout.minWidth, windowLayout.minHeight); + } + static boolean boundsSmallerThanMinDimensions(@NonNull Rect bounds, + @Nullable Size minDimensions) { + if (minDimensions == null) { + return false; + } + return bounds.width() < minDimensions.getWidth() + || bounds.height() < minDimensions.getHeight(); + } + + @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)) { + return new Rect(); + } + final boolean isLtr = isLtr(primaryActivity, rule); final float splitRatio = rule.getSplitRatio(); - final float rtlSplitRatio = 1 - splitRatio; + switch (position) { case POSITION_START: - return isLtr ? getLeftContainerBounds(parentBounds, splitRatio) - : getRightContainerBounds(parentBounds, rtlSplitRatio); + return getPrimaryBounds(parentBounds, splitRatio, isLtr); case POSITION_END: - return isLtr ? getRightContainerBounds(parentBounds, splitRatio) - : getLeftContainerBounds(parentBounds, rtlSplitRatio); + return getSecondaryBounds(parentBounds, splitRatio, isLtr); case POSITION_FILL: - return parentBounds; + default: + return new Rect(); } - return parentBounds; } - private Rect getLeftContainerBounds(@NonNull Rect parentBounds, float splitRatio) { + @NonNull + private static Rect getPrimaryBounds(@NonNull Rect parentBounds, float splitRatio, + boolean isLtr) { + return isLtr ? getLeftContainerBounds(parentBounds, splitRatio) + : getRightContainerBounds(parentBounds, 1 - splitRatio); + } + + @NonNull + private static Rect getSecondaryBounds(@NonNull Rect parentBounds, float splitRatio, + boolean isLtr) { + return isLtr ? getRightContainerBounds(parentBounds, splitRatio) + : getLeftContainerBounds(parentBounds, 1 - splitRatio); + } + + private static Rect getLeftContainerBounds(@NonNull Rect parentBounds, float splitRatio) { return new Rect( parentBounds.left, parentBounds.top, @@ -359,7 +602,7 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { parentBounds.bottom); } - private Rect getRightContainerBounds(@NonNull Rect parentBounds, float splitRatio) { + private static Rect getRightContainerBounds(@NonNull Rect parentBounds, float splitRatio) { return new Rect( (int) (parentBounds.left + parentBounds.width() * splitRatio), parentBounds.top, @@ -371,7 +614,7 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { * Checks if a split with the provided rule should be displays in left-to-right layout * direction, either always or with the current configuration. */ - private boolean isLtr(@NonNull Context context, @NonNull SplitRule rule) { + private static boolean isLtr(@NonNull Context context, @NonNull SplitRule rule) { switch (rule.getLayoutDirection()) { case LayoutDirection.LOCALE: return context.getResources().getConfiguration().getLayoutDirection() @@ -385,40 +628,35 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { } @NonNull - Rect getParentContainerBounds(@NonNull TaskFragmentContainer container) { - final Configuration parentConfig = mFragmentParentConfigs.get( - container.getTaskFragmentToken()); - if (parentConfig != null) { - return parentConfig.windowConfiguration.getBounds(); - } - - // If there is no parent yet - then assuming that activities are running in full task bounds - final Activity topActivity = container.getTopNonFinishingActivity(); - final Rect bounds = topActivity != null ? getParentContainerBounds(topActivity) : null; - - if (bounds == null) { - throw new IllegalStateException("Unknown parent bounds"); - } - return bounds; + static Rect getParentContainerBounds(@NonNull TaskFragmentContainer container) { + return container.getTaskContainer().getTaskBounds(); } @NonNull Rect getParentContainerBounds(@NonNull Activity activity) { - final TaskFragmentContainer container = mController.getContainerWithActivity( - activity.getActivityToken()); + final TaskFragmentContainer container = mController.getContainerWithActivity(activity); if (container != null) { - final Configuration parentConfig = mFragmentParentConfigs.get( - container.getTaskFragmentToken()); - if (parentConfig != null) { - return parentConfig.windowConfiguration.getBounds(); - } + return getParentContainerBounds(container); } + // Obtain bounds from Activity instead because the Activity hasn't been embedded yet. + return getNonEmbeddedActivityBounds(activity); + } - // TODO(b/190433398): Check if the client-side available info about parent bounds is enough. + /** + * Obtains the bounds from a non-embedded Activity. + * <p> + * Note that callers should use {@link #getParentContainerBounds(Activity)} instead for most + * cases unless we want to obtain task bounds before + * {@link TaskContainer#isTaskBoundsInitialized()}. + */ + @NonNull + 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 activity.getResources().getConfiguration().windowConfiguration.getMaxBounds(); + return windowConfiguration.getMaxBounds(); } - return activity.getResources().getConfiguration().windowConfiguration.getBounds(); + return windowConfiguration.getBounds(); } } diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java new file mode 100644 index 000000000000..0ea5603b1f3d --- /dev/null +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java @@ -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. + */ + +package androidx.window.extensions.embedding; + +import static android.app.ActivityTaskManager.INVALID_TASK_ID; +import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; +import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; +import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.Activity; +import android.app.WindowConfiguration; +import android.app.WindowConfiguration.WindowingMode; +import android.graphics.Rect; +import android.os.IBinder; +import android.util.ArraySet; +import android.window.TaskFragmentInfo; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +/** Represents TaskFragments and split pairs below a Task. */ +class TaskContainer { + + /** 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<>(); + + /** Active split pairs in this Task. */ + @NonNull + final List<SplitContainer> mSplitContainers = new ArrayList<>(); + + /** + * TaskFragments that the organizer has requested to be closed. They should be removed when + * the organizer receives {@link SplitController#onTaskFragmentVanished(TaskFragmentInfo)} event + * for them. + */ + final Set<IBinder> mFinishedContainer = new ArraySet<>(); + + TaskContainer(int taskId) { + if (taskId == INVALID_TASK_ID) { + throw new IllegalArgumentException("Invalid Task id"); + } + mTaskId = taskId; + } + + int getTaskId() { + return mTaskId; + } + + @NonNull + Rect getTaskBounds() { + return mTaskBounds; + } + + /** 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(); + } + + void setWindowingMode(int windowingMode) { + mWindowingMode = windowingMode; + } + + /** Whether the Task windowing mode has been initialized. */ + boolean isWindowingModeInitialized() { + return mWindowingMode != WINDOWING_MODE_UNDEFINED; + } + + /** + * Returns the windowing mode for the TaskFragments below this Task, which should be split with + * other TaskFragments. + * + * @param taskFragmentBounds Requested bounds for the TaskFragment. It will be empty when + * the pair of TaskFragments are stacked due to the limited space. + */ + @WindowingMode + int getWindowingModeForSplitTaskFragment(@Nullable Rect taskFragmentBounds) { + // Only set to multi-windowing mode if the pair are showing side-by-side. Otherwise, it + // will be set to UNDEFINED which will then inherit the Task windowing mode. + if (taskFragmentBounds == null || taskFragmentBounds.isEmpty() || isInPictureInPicture()) { + return WINDOWING_MODE_UNDEFINED; + } + // We use WINDOWING_MODE_MULTI_WINDOW when the Task is fullscreen. + // However, when the Task is in other multi windowing mode, such as Freeform, we need to + // have the activity windowing mode to match the Task, otherwise things like + // 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; + } + + boolean isInPictureInPicture() { + return mWindowingMode == WINDOWING_MODE_PINNED; + } + + /** Whether there is any {@link TaskFragmentContainer} below this Task. */ + boolean isEmpty() { + return mContainers.isEmpty() && mFinishedContainer.isEmpty(); + } + + /** Removes the pending appeared activity from all TaskFragments in this Task. */ + void cleanupPendingAppearedActivity(@NonNull Activity pendingAppearedActivity) { + for (TaskFragmentContainer container : mContainers) { + container.removePendingAppearedActivity(pendingAppearedActivity); + } + } + + @Nullable + TaskFragmentContainer getTopTaskFragmentContainer() { + if (mContainers.isEmpty()) { + return null; + } + return mContainers.get(mContainers.size() - 1); + } + + @Nullable + Activity getTopNonFinishingActivity() { + for (int i = mContainers.size() - 1; i >= 0; i--) { + final Activity activity = mContainers.get(i).getTopNonFinishingActivity(); + if (activity != null) { + return activity; + } + } + return null; + } +} 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 b3becad3dc5a..cdee9e386b33 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationAdapter.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationAdapter.java @@ -17,6 +17,8 @@ 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 android.graphics.Rect; import android.view.Choreographer; @@ -96,22 +98,20 @@ class TaskFragmentAnimationAdapter { mTarget.localBounds.left, mTarget.localBounds.top); t.setMatrix(mLeash, mTransformation.getMatrix(), mMatrix); t.setAlpha(mLeash, mTransformation.getAlpha()); - - // Open/close animation may scale up the surface. Apply an inverse scale to the window crop - // so that it will not be covering other windows. - 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 = mTarget.localBounds; - 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); - mRect.offsetTo(Math.round(mTarget.localBounds.width() * (1 - mVecs[0]) / 2.f), - Math.round(mTarget.localBounds.height() * (1 - mVecs[3]) / 2.f)); - t.setWindowCrop(mLeash, mRect); + // Get current animation position. + 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); + t.setCrop(mLeash, cropRect); } /** Called after animation finished. */ 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 a801dc8193fd..f721341a3647 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationController.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationController.java @@ -24,11 +24,14 @@ 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 com.android.internal.annotations.VisibleForTesting; + /** Controls the TaskFragment remote animations. */ class TaskFragmentAnimationController { @@ -37,8 +40,10 @@ class TaskFragmentAnimationController { private final TaskFragmentOrganizer mOrganizer; private final TaskFragmentAnimationRunner mRemoteRunner = new TaskFragmentAnimationRunner(); - private final RemoteAnimationDefinition mDefinition; - private boolean mIsRegister; + @VisibleForTesting + final RemoteAnimationDefinition mDefinition; + /** Task Ids that we have registered for remote animation. */ + private final ArraySet<Integer> mRegisterTasks = new ArraySet<>(); TaskFragmentAnimationController(TaskFragmentOrganizer organizer) { mOrganizer = organizer; @@ -54,25 +59,32 @@ class TaskFragmentAnimationController { mDefinition.addRemoteAnimation(TRANSIT_OLD_TASK_FRAGMENT_CHANGE, animationAdapter); } - void registerRemoteAnimations() { + void registerRemoteAnimations(int taskId) { if (DEBUG) { Log.v(TAG, "registerRemoteAnimations"); } - if (mIsRegister) { + if (mRegisterTasks.contains(taskId)) { return; } - mOrganizer.registerRemoteAnimations(mDefinition); - mIsRegister = true; + mOrganizer.registerRemoteAnimations(taskId, mDefinition); + mRegisterTasks.add(taskId); } - void unregisterRemoteAnimations() { + void unregisterRemoteAnimations(int taskId) { if (DEBUG) { Log.v(TAG, "unregisterRemoteAnimations"); } - if (!mIsRegister) { + if (!mRegisterTasks.contains(taskId)) { return; } - mOrganizer.unregisterRemoteAnimations(); - mIsRegister = false; + mOrganizer.unregisterRemoteAnimations(taskId); + mRegisterTasks.remove(taskId); + } + + void unregisterAllRemoteAnimations() { + final ArraySet<Integer> tasks = new ArraySet<>(mRegisterTasks); + for (int taskId : tasks) { + unregisterRemoteAnimations(taskId); + } } } 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 1ac33173668b..c4f37091a491 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationRunner.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationRunner.java @@ -83,9 +83,9 @@ class TaskFragmentAnimationRunner extends IRemoteAnimationRunner.Stub { } @Override - public void onAnimationCancelled() { + public void onAnimationCancelled(boolean isKeyguardOccluded) { if (TaskFragmentAnimationController.DEBUG) { - Log.v(TAG, "onAnimationCancelled"); + Log.v(TAG, "onAnimationCancelled: isKeyguardOccluded=" + isKeyguardOccluded); } mHandler.post(this::cancelAnimation); } 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 4d2d0551d828..abf32a26efa2 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java @@ -16,16 +16,22 @@ 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.TaskFragmentInfo; import android.window.WindowContainerTransaction; +import com.android.internal.annotations.VisibleForTesting; + import java.util.ArrayList; import java.util.Iterator; import java.util.List; @@ -35,22 +41,41 @@ import java.util.List; * on the server side. */ class TaskFragmentContainer { + private static final int APPEAR_EMPTY_TIMEOUT_MS = 3000; + + @NonNull + private final SplitController mController; + /** * Client-created token that uniquely identifies the task fragment container instance. */ @NonNull private final IBinder mToken; + /** Parent leaf Task. */ + @NonNull + private final TaskContainer mTaskContainer; + /** * Server-provided task fragment information. */ - private TaskFragmentInfo mInfo; + @VisibleForTesting + TaskFragmentInfo mInfo; /** * Activities that are being reparented or being started to this container, but haven't been * added to {@link #mInfo} yet. */ - private final ArrayList<Activity> mPendingAppearedActivities = new ArrayList<>(); + @VisibleForTesting + final ArrayList<Activity> mPendingAppearedActivities = new ArrayList<>(); + + /** + * When this container is created for an {@link Intent} to start within, we store that Intent + * until the container becomes non-empty on the server side, so that we can use it to check + * rules associated with this container. + */ + @Nullable + private Intent mPendingAppearedIntent; /** Containers that are dependent on this one and should be completely destroyed on exit. */ private final List<TaskFragmentContainer> mContainersToFinishOnExit = @@ -68,14 +93,39 @@ class TaskFragmentContainer { private final Rect mLastRequestedBounds = new Rect(); /** + * Windowing mode that was requested last via {@link android.window.WindowContainerTransaction}. + */ + @WindowingMode + private int mLastRequestedWindowingMode = WINDOWING_MODE_UNDEFINED; + + /** + * When the TaskFragment has appeared in server, but is empty, we should remove the TaskFragment + * if it is still empty after the timeout. + */ + @VisibleForTesting + @Nullable + Runnable mAppearEmptyTimeout; + + /** * Creates a container with an existing activity that will be re-parented to it in a window * container transaction. */ - TaskFragmentContainer(@Nullable Activity activity) { + TaskFragmentContainer(@Nullable Activity pendingAppearedActivity, + @Nullable Intent pendingAppearedIntent, @NonNull TaskContainer taskContainer, + @NonNull SplitController controller) { + if ((pendingAppearedActivity == null && pendingAppearedIntent == null) + || (pendingAppearedActivity != null && pendingAppearedIntent != null)) { + throw new IllegalArgumentException( + "One and only one of pending activity and intent must be non-null"); + } + mController = controller; mToken = new Binder("TaskFragmentContainer"); - if (activity != null) { - addPendingAppearedActivity(activity); + mTaskContainer = taskContainer; + taskContainer.mContainers.add(this); + if (pendingAppearedActivity != null) { + addPendingAppearedActivity(pendingAppearedActivity); } + mPendingAppearedIntent = pendingAppearedIntent; } /** @@ -86,38 +136,68 @@ class TaskFragmentContainer { return mToken; } - /** List of activities that belong to this container and live in this process. */ + /** List of non-finishing activities that belong to this container and live in this process. */ @NonNull - List<Activity> collectActivities() { + List<Activity> collectNonFinishingActivities() { + final List<Activity> allActivities = new ArrayList<>(); + if (mInfo != null) { + // Add activities reported from the server. + for (IBinder token : mInfo.getActivities()) { + final Activity activity = mController.getActivity(token); + if (activity != null && !activity.isFinishing()) { + allActivities.add(activity); + } + } + } + // Add the re-parenting activity, in case the server has not yet reported the task // fragment info update with it placed in this container. We still want to apply rules // in this intermediate state. - List<Activity> allActivities = new ArrayList<>(); - if (!mPendingAppearedActivities.isEmpty()) { - allActivities.addAll(mPendingAppearedActivities); - } - // Add activities reported from the server. - if (mInfo == null) { - return allActivities; - } - ActivityThread activityThread = ActivityThread.currentActivityThread(); - for (IBinder token : mInfo.getActivities()) { - Activity activity = activityThread.getActivity(token); - if (activity != null && !activity.isFinishing() && !allActivities.contains(activity)) { + // 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()) { 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() { + if (mInfo == null) { + return false; + } + return mPendingAppearedActivities.isEmpty() + && mInfo.getActivities().size() == collectNonFinishingActivities().size(); + } + ActivityStack toActivityStack() { - return new ActivityStack(collectActivities(), mInfo.getRunningActivityCount() == 0); + return new ActivityStack(collectNonFinishingActivities(), isEmpty()); } + /** Adds the activity that will be reparented to this container. */ void addPendingAppearedActivity(@NonNull Activity pendingAppearedActivity) { + if (hasActivity(pendingAppearedActivity.getActivityToken())) { + return; + } + // Remove the pending activity from other TaskFragments. + mTaskContainer.cleanupPendingAppearedActivity(pendingAppearedActivity); mPendingAppearedActivities.add(pendingAppearedActivity); } + void removePendingAppearedActivity(@NonNull Activity pendingAppearedActivity) { + mPendingAppearedActivities.remove(pendingAppearedActivity); + } + + @Nullable + Intent getPendingAppearedIntent() { + return mPendingAppearedIntent; + } + boolean hasActivity(@NonNull IBinder token) { if (mInfo != null && mInfo.getActivities().contains(token)) { return true; @@ -138,14 +218,37 @@ class TaskFragmentContainer { return count; } + /** Whether we are waiting for the TaskFragment to appear and become non-empty. */ + boolean isWaitingActivityAppear() { + return !mIsFinished && (mInfo == null || mAppearEmptyTimeout != null); + } + @Nullable TaskFragmentInfo getInfo() { return mInfo; } void setInfo(@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 = () -> { + mAppearEmptyTimeout = null; + mController.onTaskFragmentAppearEmptyTimeout(this); + }; + mController.getHandler().postDelayed(mAppearEmptyTimeout, APPEAR_EMPTY_TIMEOUT_MS); + } else if (mAppearEmptyTimeout != null && !info.isEmpty()) { + mController.getHandler().removeCallbacks(mAppearEmptyTimeout); + mAppearEmptyTimeout = null; + } + mInfo = info; - if (mInfo == null || mPendingAppearedActivities.isEmpty()) { + if (mInfo == null || mInfo.isEmpty()) { + return; + } + // Only track the pending Intent when the container is empty. + mPendingAppearedIntent = null; + if (mPendingAppearedActivities.isEmpty()) { return; } // Cleanup activities that were being re-parented @@ -160,15 +263,14 @@ class TaskFragmentContainer { @Nullable Activity getTopNonFinishingActivity() { - List<Activity> activities = collectActivities(); - if (activities.isEmpty()) { - return null; - } - int i = activities.size() - 1; - while (i >= 0 && activities.get(i).isFinishing()) { - i--; - } - return i >= 0 ? activities.get(i) : null; + final List<Activity> activities = collectNonFinishingActivities(); + return activities.isEmpty() ? null : activities.get(activities.size() - 1); + } + + @Nullable + Activity getBottomMostActivity() { + final List<Activity> activities = collectNonFinishingActivities(); + return activities.isEmpty() ? null : activities.get(0); } boolean isEmpty() { @@ -179,17 +281,52 @@ class TaskFragmentContainer { * Adds a container that should be finished when this container is finished. */ void addContainerToFinishOnExit(@NonNull TaskFragmentContainer containerToFinish) { + if (mIsFinished) { + return; + } mContainersToFinishOnExit.add(containerToFinish); } /** + * Removes a container that should be finished when this container is finished. + */ + void removeContainerToFinishOnExit(@NonNull TaskFragmentContainer containerToRemove) { + if (mIsFinished) { + return; + } + mContainersToFinishOnExit.remove(containerToRemove); + } + + /** * Adds an activity that should be finished when this container is finished. */ void addActivityToFinishOnExit(@NonNull Activity activityToFinish) { + if (mIsFinished) { + return; + } mActivitiesToFinishOnExit.add(activityToFinish); } /** + * Removes an activity that should be finished when this container is finished. + */ + void removeActivityToFinishOnExit(@NonNull Activity activityToRemove) { + if (mIsFinished) { + return; + } + mActivitiesToFinishOnExit.remove(activityToRemove); + } + + /** Removes all dependencies that should be finished when this container is finished. */ + void resetDependencies() { + if (mIsFinished) { + return; + } + mContainersToFinishOnExit.clear(); + mActivitiesToFinishOnExit.clear(); + } + + /** * Removes all activities that belong to this process and finishes other containers/activities * configured to finish together. */ @@ -197,6 +334,10 @@ class TaskFragmentContainer { @NonNull WindowContainerTransaction wct, @NonNull SplitController controller) { if (!mIsFinished) { mIsFinished = true; + if (mAppearEmptyTimeout != null) { + mController.getHandler().removeCallbacks(mAppearEmptyTimeout); + mAppearEmptyTimeout = null; + } finishActivities(shouldFinishDependent, presenter, wct, controller); } @@ -216,8 +357,11 @@ class TaskFragmentContainer { private void finishActivities(boolean shouldFinishDependent, @NonNull SplitPresenter presenter, @NonNull WindowContainerTransaction wct, @NonNull SplitController controller) { // Finish own activities - for (Activity activity : collectActivities()) { - if (!activity.isFinishing()) { + for (Activity activity : collectNonFinishingActivities()) { + if (!activity.isFinishing() + // 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(); } } @@ -228,7 +372,8 @@ class TaskFragmentContainer { // Finish dependent containers for (TaskFragmentContainer container : mContainersToFinishOnExit) { - if (controller.shouldRetainAssociatedContainer(this, container)) { + if (container.mIsFinished + || controller.shouldRetainAssociatedContainer(this, container)) { continue; } container.finish(true /* shouldFinishDependent */, presenter, @@ -238,18 +383,13 @@ class TaskFragmentContainer { // Finish associated activities for (Activity activity : mActivitiesToFinishOnExit) { - if (controller.shouldRetainAssociatedActivity(this, activity)) { + if (activity.isFinishing() + || controller.shouldRetainAssociatedActivity(this, activity)) { continue; } activity.finish(); } mActivitiesToFinishOnExit.clear(); - - // Finish activities that were being re-parented to this container. - for (Activity activity : mPendingAppearedActivities) { - activity.finish(); - } - mPendingAppearedActivities.clear(); } boolean isFinished() { @@ -275,6 +415,61 @@ class TaskFragmentContainer { } } + @NonNull + Rect getLastRequestedBounds() { + return mLastRequestedBounds; + } + + /** + * Checks if last requested windowing mode is equal to the provided value. + */ + boolean isLastRequestedWindowingModeEqual(@WindowingMode int windowingMode) { + return mLastRequestedWindowingMode == windowingMode; + } + + /** + * Updates the last requested windowing mode. + */ + void setLastRequestedWindowingMode(@WindowingMode int windowingModes) { + mLastRequestedWindowingMode = windowingModes; + } + + /** Gets the parent leaf Task id. */ + int getTaskId() { + return mTaskContainer.getTaskId(); + } + + /** Gets the parent Task. */ + @NonNull + TaskContainer getTaskContainer() { + return mTaskContainer; + } + + @Nullable + Size getMinDimensions() { + if (mInfo == null) { + return null; + } + int maxMinWidth = mInfo.getMinimumWidth(); + int maxMinHeight = mInfo.getMinimumHeight(); + for (Activity activity : mPendingAppearedActivities) { + final Size minDimensions = SplitPresenter.getMinDimensions(activity); + if (minDimensions == null) { + continue; + } + maxMinWidth = Math.max(maxMinWidth, minDimensions.getWidth()); + maxMinHeight = Math.max(maxMinHeight, minDimensions.getHeight()); + } + if (mPendingAppearedIntent != null) { + final Size minDimensions = SplitPresenter.getMinDimensions(mPendingAppearedIntent); + if (minDimensions != null) { + maxMinWidth = Math.max(maxMinWidth, minDimensions.getWidth()); + maxMinHeight = Math.max(maxMinHeight, minDimensions.getHeight()); + } + } + return new Size(maxMinWidth, maxMinHeight); + } + @Override public String toString() { return toString(true /* includeContainersToFinishOnExit */); @@ -288,15 +483,17 @@ class TaskFragmentContainer { */ private String toString(boolean includeContainersToFinishOnExit) { return "TaskFragmentContainer{" + + " parentTaskId=" + getTaskId() + " token=" + mToken - + " info=" + mInfo + " topNonFinishingActivity=" + getTopNonFinishingActivity() + + " runningActivityCount=" + getRunningActivityCount() + + " isFinished=" + mIsFinished + + " lastRequestedBounds=" + mLastRequestedBounds + " pendingAppearedActivities=" + mPendingAppearedActivities + (includeContainersToFinishOnExit ? " containersToFinishOnExit=" + containersToFinishOnExitToString() : "") + " activitiesToFinishOnExit=" + mActivitiesToFinishOnExit - + " isFinished=" + mIsFinished - + " lastRequestedBounds=" + mLastRequestedBounds + + " info=" + mInfo + "}"; } 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 ee8cb48e3c4c..c1d1c8e8d4e0 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java @@ -25,23 +25,25 @@ 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.Application; +import android.app.WindowConfiguration; import android.content.Context; import android.graphics.Rect; import android.os.Bundle; import android.os.IBinder; +import android.util.ArrayMap; 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.SettingsDisplayFeatureProducer; +import androidx.window.common.RawFoldingFeatureProducer; import androidx.window.util.DataProducer; -import androidx.window.util.PriorityDataProducer; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; @@ -60,19 +62,16 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent { private static final String TAG = "SampleExtension"; private final Map<Activity, Consumer<WindowLayoutInfo>> mWindowLayoutChangeListeners = - new HashMap<>(); + new ArrayMap<>(); - private final SettingsDisplayFeatureProducer mSettingsDisplayFeatureProducer; private final DataProducer<List<CommonFoldingFeature>> mFoldingFeatureProducer; public WindowLayoutComponentImpl(Context context) { ((Application) context.getApplicationContext()) .registerActivityLifecycleCallbacks(new NotifyOnConfigurationChanged()); - mSettingsDisplayFeatureProducer = new SettingsDisplayFeatureProducer(context); - mFoldingFeatureProducer = new PriorityDataProducer<>(List.of( - mSettingsDisplayFeatureProducer, - new DeviceStateManagerFoldingFeatureProducer(context) - )); + RawFoldingFeatureProducer foldingFeatureProducer = new RawFoldingFeatureProducer(context); + mFoldingFeatureProducer = new DeviceStateManagerFoldingFeatureProducer(context, + foldingFeatureProducer); mFoldingFeatureProducer.addDataChangedCallback(this::onDisplayFeaturesChanged); } @@ -85,7 +84,7 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent { public void addWindowLayoutInfoListener(@NonNull Activity activity, @NonNull Consumer<WindowLayoutInfo> consumer) { mWindowLayoutChangeListeners.put(activity, consumer); - updateRegistrations(); + onDisplayFeaturesChanged(); } /** @@ -96,7 +95,7 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent { public void removeWindowLayoutInfoListener( @NonNull Consumer<WindowLayoutInfo> consumer) { mWindowLayoutChangeListeners.values().remove(consumer); - updateRegistrations(); + onDisplayFeaturesChanged(); } void updateWindowLayout(@NonNull Activity activity, @@ -113,7 +112,7 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent { } @NonNull - private Boolean isListeningForLayoutChanges(IBinder token) { + private boolean isListeningForLayoutChanges(IBinder token) { for (Activity activity: getActivitiesListeningForLayoutChanges()) { if (token.equals(activity.getWindow().getAttributes().token)) { return true; @@ -170,8 +169,8 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent { * coordinate space and the state is calculated from {@link CommonFoldingFeature#getState()}. * The state from {@link #mFoldingFeatureProducer} may not be valid since * {@link #mFoldingFeatureProducer} is a general state controller. If the state is not valid, - * the {@link FoldingFeature} is omitted from the {@link List} of {@link DisplayFeature}. If - * the bounds are not valid, constructing a {@link FoldingFeature} will throw an + * the {@link FoldingFeature} is omitted from the {@link List} of {@link DisplayFeature}. If the + * 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 @@ -187,7 +186,7 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent { return features; } - if (activity.isInMultiWindowMode()) { + if (isTaskInMultiWindowMode(activity)) { // It is recommended not to report any display features in multi-window mode, since it // won't be possible to synchronize the display feature positions with window movement. return features; @@ -204,19 +203,47 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent { rotateRectToDisplayRotation(displayId, featureRect); transformToWindowSpaceRect(activity, featureRect); - features.add(new FoldingFeature(featureRect, baseFeature.getType(), state)); + if (!isRectZero(featureRect)) { + // TODO(b/228641877) Remove guarding if when fixed. + features.add(new FoldingFeature(featureRect, baseFeature.getType(), state)); + } } } return features; } - private void updateRegistrations() { - if (hasListeners()) { - mSettingsDisplayFeatureProducer.registerObserversIfNeeded(); - } else { - mSettingsDisplayFeatureProducer.unregisterObserversIfNeeded(); + /** + * Checks whether the task associated with the activity is in multi-window. If task info is not + * available it defaults to {@code true}. + */ + private boolean isTaskInMultiWindowMode(@NonNull Activity activity) { + final ActivityManager am = activity.getSystemService(ActivityManager.class); + if (am == null) { + return true; } - onDisplayFeaturesChanged(); + + 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; + } + } + if (task == null) { + // The task might be removed on the server already. + return true; + } + return WindowConfiguration.inMultiWindowMode(task.getTaskInfo().getWindowingMode()); + } + + /** + * Returns {@link true} if a {@link Rect} has zero width and zero height, + * {@code false} otherwise. + */ + private boolean isRectZero(Rect rect) { + return rect.width() == 0 && rect.height() == 0; } private final class NotifyOnConfigurationChanged extends EmptyLifecycleCallbacksAdapter { diff --git a/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SampleSidecarImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SampleSidecarImpl.java index c7b709347060..970f0a2af632 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SampleSidecarImpl.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SampleSidecarImpl.java @@ -34,9 +34,8 @@ import androidx.annotation.NonNull; import androidx.window.common.CommonFoldingFeature; import androidx.window.common.DeviceStateManagerFoldingFeatureProducer; import androidx.window.common.EmptyLifecycleCallbacksAdapter; -import androidx.window.common.SettingsDisplayFeatureProducer; +import androidx.window.common.RawFoldingFeatureProducer; import androidx.window.util.DataProducer; -import androidx.window.util.PriorityDataProducer; import java.util.ArrayList; import java.util.Collections; @@ -52,16 +51,13 @@ class SampleSidecarImpl extends StubSidecar { private final DataProducer<List<CommonFoldingFeature>> mFoldingFeatureProducer; - private final SettingsDisplayFeatureProducer mSettingsFoldingFeatureProducer; SampleSidecarImpl(Context context) { ((Application) context.getApplicationContext()) .registerActivityLifecycleCallbacks(new NotifyOnConfigurationChanged()); - mSettingsFoldingFeatureProducer = new SettingsDisplayFeatureProducer(context); - mFoldingFeatureProducer = new PriorityDataProducer<>(List.of( - mSettingsFoldingFeatureProducer, - new DeviceStateManagerFoldingFeatureProducer(context) - )); + DataProducer<String> settingsFeatureProducer = new RawFoldingFeatureProducer(context); + mFoldingFeatureProducer = new DeviceStateManagerFoldingFeatureProducer(context, + settingsFeatureProducer); mFoldingFeatureProducer.addDataChangedCallback(this::onDisplayFeaturesChanged); } @@ -142,10 +138,7 @@ class SampleSidecarImpl extends StubSidecar { @Override protected void onListenersChanged() { if (hasListeners()) { - mSettingsFoldingFeatureProducer.registerObserversIfNeeded(); onDisplayFeaturesChanged(); - } else { - mSettingsFoldingFeatureProducer.unregisterObserversIfNeeded(); } } diff --git a/libs/WindowManager/Jetpack/src/androidx/window/util/BaseDataProducer.java b/libs/WindowManager/Jetpack/src/androidx/window/util/BaseDataProducer.java index 0a46703451ab..930db3b701b7 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/util/BaseDataProducer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/util/BaseDataProducer.java @@ -33,13 +33,17 @@ public abstract class BaseDataProducer<T> implements DataProducer<T> { @Override public final void addDataChangedCallback(@NonNull Runnable callback) { mCallbacks.add(callback); + onListenersChanged(mCallbacks); } @Override public final void removeDataChangedCallback(@NonNull Runnable callback) { mCallbacks.remove(callback); + onListenersChanged(mCallbacks); } + protected void onListenersChanged(Set<Runnable> callbacks) {} + /** * Called to notify all registered callbacks that the data provided by {@link #getData()} has * changed. diff --git a/libs/WindowManager/Jetpack/src/androidx/window/util/PriorityDataProducer.java b/libs/WindowManager/Jetpack/src/androidx/window/util/PriorityDataProducer.java deleted file mode 100644 index 990ae20cc934..000000000000 --- a/libs/WindowManager/Jetpack/src/androidx/window/util/PriorityDataProducer.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright (C) 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package androidx.window.util; - -import android.annotation.Nullable; - -import java.util.List; -import java.util.Optional; - -/** - * Implementation of {@link DataProducer} that delegates calls to {@link #getData()} to the list of - * provided child producers. - * <p> - * The value returned is based on the precedence of the supplied children where the producer with - * index 0 has a higher precedence than producers that come later in the list. When a producer with - * a higher precedence has a non-empty value returned from {@link #getData()}, its value will be - * returned from an instance of this class, ignoring all other producers with lower precedence. - * - * @param <T> The type of data this producer returns through {@link #getData()}. - */ -public final class PriorityDataProducer<T> extends BaseDataProducer<T> { - private final List<DataProducer<T>> mChildProducers; - - public PriorityDataProducer(List<DataProducer<T>> childProducers) { - mChildProducers = childProducers; - for (DataProducer<T> childProducer : mChildProducers) { - childProducer.addDataChangedCallback(this::notifyDataChanged); - } - } - - @Nullable - @Override - public Optional<T> getData() { - for (DataProducer<T> childProducer : mChildProducers) { - final Optional<T> data = childProducer.getData(); - if (data.isPresent()) { - return data; - } - } - return Optional.empty(); - } -} diff --git a/libs/WindowManager/Jetpack/tests/OWNERS b/libs/WindowManager/Jetpack/tests/OWNERS new file mode 100644 index 000000000000..ac522b2dde10 --- /dev/null +++ b/libs/WindowManager/Jetpack/tests/OWNERS @@ -0,0 +1,4 @@ +# Bug component: 1157642 +# includes OWNERS from parent directories +charlesccchen@google.com +diegovela@google.com diff --git a/libs/WindowManager/Jetpack/tests/unittest/Android.bp b/libs/WindowManager/Jetpack/tests/unittest/Android.bp new file mode 100644 index 000000000000..b6e743a2b7e1 --- /dev/null +++ b/libs/WindowManager/Jetpack/tests/unittest/Android.bp @@ -0,0 +1,60 @@ +// Copyright (C) 2022 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_license"], +} + +android_test { + name: "WMJetpackUnitTests", + // To make the test run via TEST_MAPPING + test_suites: ["device-tests"], + + srcs: [ + "**/*.java", + ], + + static_libs: [ + "androidx.window.extensions", + "junit", + "androidx.test.runner", + "androidx.test.rules", + "androidx.test.ext.junit", + "mockito-target-extended-minus-junit4", + "truth-prebuilt", + "testables", + "platform-test-annotations", + ], + + libs: [ + "android.test.mock", + "android.test.base", + "android.test.runner", + ], + + // These are not normally accessible from apps so they must be explicitly included. + jni_libs: [ + "libdexmakerjvmtiagent", + "libstaticjvmtiagent", + ], + + optimize: { + enabled: false, + }, +} diff --git a/libs/WindowManager/Jetpack/tests/unittest/AndroidManifest.xml b/libs/WindowManager/Jetpack/tests/unittest/AndroidManifest.xml new file mode 100644 index 000000000000..c736e9ed971e --- /dev/null +++ b/libs/WindowManager/Jetpack/tests/unittest/AndroidManifest.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. +--> + +<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="androidx.window.tests"> + + <application android:debuggable="true" android:largeHeap="true"> + <uses-library android:name="android.test.mock" /> + <uses-library android:name="android.test.runner" /> + + <activity android:name="androidx.window.extensions.embedding.MinimumDimensionActivity"> + <layout android:minWidth="600px" + android:minHeight="1200px"/> + </activity> + </application> + + <instrumentation + android:name="androidx.test.runner.AndroidJUnitRunner" + android:label="Tests for WindowManager Jetpack library" + android:targetPackage="androidx.window.tests"> + </instrumentation> +</manifest> diff --git a/libs/WindowManager/Jetpack/tests/unittest/AndroidTest.xml b/libs/WindowManager/Jetpack/tests/unittest/AndroidTest.xml new file mode 100644 index 000000000000..56d8c33fdc09 --- /dev/null +++ b/libs/WindowManager/Jetpack/tests/unittest/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 WindowManager Jetpack library"> + <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="WMJetpackUnitTests.apk" /> + </target_preparer> + + <option name="test-suite-tag" value="apct" /> + <option name="test-suite-tag" value="framework-base-presubmit" /> + <option name="test-tag" value="WMJetpackUnitTests" /> + <test class="com.android.tradefed.testtype.AndroidJUnitTest" > + <option name="package" value="androidx.window.tests" /> + <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" /> + <option name="hidden-api-checks" value="false"/> + </test> +</configuration> 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 new file mode 100644 index 000000000000..13a2c78d463e --- /dev/null +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/WindowExtensionsTest.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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; + +import static com.google.common.truth.Truth.assertThat; + +import android.platform.test.annotations.Presubmit; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Test class for {@link WindowExtensionsTest}. + * + * Build/Install/Run: + * atest WMJetpackUnitTests:WindowExtensionsTest + */ +@Presubmit +@SmallTest +@RunWith(AndroidJUnit4.class) +public class WindowExtensionsTest { + private WindowExtensions mExtensions; + + @Before + public void setUp() { + mExtensions = WindowExtensionsProvider.getWindowExtensions(); + } + + @Test + public void testGetWindowLayoutComponent() { + assertThat(mExtensions.getWindowLayoutComponent()).isNotNull(); + } + + @Test + public void testGetActivityEmbeddingComponent() { + assertThat(mExtensions.getActivityEmbeddingComponent()).isNotNull(); + } +} 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 new file mode 100644 index 000000000000..effc1a3ef3ea --- /dev/null +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/EmbeddingTestUtils.java @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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 androidx.window.extensions.embedding.SplitRule.FINISH_ALWAYS; +import static androidx.window.extensions.embedding.SplitRule.FINISH_NEVER; + +import static org.mockito.Mockito.mock; + +import android.annotation.NonNull; +import android.app.Activity; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.res.Configuration; +import android.graphics.Point; +import android.graphics.Rect; +import android.util.Pair; +import android.window.TaskFragmentInfo; +import android.window.WindowContainerToken; + +import java.util.Collections; + +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; + /** 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 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 isPrimary + ? new Rect(TASK_BOUNDS.left, TASK_BOUNDS.top, TASK_BOUNDS.left + width, + TASK_BOUNDS.bottom) + : new Rect( + TASK_BOUNDS.left + width, TASK_BOUNDS.top, TASK_BOUNDS.right, + TASK_BOUNDS.bottom); + } + + /** Creates a rule to always split the given activity and the given intent. */ + static SplitRule createSplitRule(@NonNull Activity primaryActivity, + @NonNull Intent secondaryIntent) { + return createSplitRule(primaryActivity, secondaryIntent, true /* clearTop */); + } + + /** Creates a rule to always split the given activity and the given intent. */ + static SplitRule createSplitRule(@NonNull Activity primaryActivity, + @NonNull Intent secondaryIntent, boolean clearTop) { + final Pair<Activity, Intent> targetPair = new Pair<>(primaryActivity, secondaryIntent); + return new SplitPairRule.Builder( + activityPair -> false, + targetPair::equals, + w -> true) + .setSplitRatio(SPLIT_RATIO) + .setShouldClearTop(clearTop) + .setFinishPrimaryWithSecondary(DEFAULT_FINISH_PRIMARY_WITH_SECONDARY) + .setFinishSecondaryWithPrimary(DEFAULT_FINISH_SECONDARY_WITH_PRIMARY) + .build(); + } + + /** Creates a rule to always split the given activities. */ + static SplitRule createSplitRule(@NonNull Activity primaryActivity, + @NonNull Activity secondaryActivity) { + return createSplitRule(primaryActivity, secondaryActivity, + DEFAULT_FINISH_PRIMARY_WITH_SECONDARY, DEFAULT_FINISH_SECONDARY_WITH_PRIMARY, + true /* clearTop */); + } + + /** Creates a rule to always split the given activities. */ + static SplitRule createSplitRule(@NonNull Activity primaryActivity, + @NonNull Activity secondaryActivity, boolean clearTop) { + return createSplitRule(primaryActivity, secondaryActivity, + DEFAULT_FINISH_PRIMARY_WITH_SECONDARY, DEFAULT_FINISH_SECONDARY_WITH_PRIMARY, + clearTop); + } + + /** Creates a rule to always split the given activities with the given finish behaviors. */ + static SplitRule createSplitRule(@NonNull Activity primaryActivity, + @NonNull Activity secondaryActivity, int finishPrimaryWithSecondary, + int finishSecondaryWithPrimary, boolean clearTop) { + final Pair<Activity, Activity> targetPair = new Pair<>(primaryActivity, secondaryActivity); + return new SplitPairRule.Builder( + targetPair::equals, + activityIntentPair -> false, + w -> true) + .setSplitRatio(SPLIT_RATIO) + .setFinishPrimaryWithSecondary(finishPrimaryWithSecondary) + .setFinishSecondaryWithPrimary(finishSecondaryWithPrimary) + .setShouldClearTop(clearTop) + .build(); + } + + /** Creates a mock TaskFragmentInfo for the given TaskFragment. */ + static TaskFragmentInfo createMockTaskFragmentInfo(@NonNull TaskFragmentContainer container, + @NonNull Activity activity) { + return new TaskFragmentInfo(container.getTaskFragmentToken(), + mock(WindowContainerToken.class), + new Configuration(), + 1, + true /* isVisible */, + Collections.singletonList(activity.getActivityToken()), + new Point(), + false /* isTaskClearedForReuse */, + false /* isTaskFragmentClearedForPip */, + new Point()); + } + + static ActivityInfo createActivityInfoWithMinDimensions() { + ActivityInfo aInfo = new ActivityInfo(); + final Rect primaryBounds = getSplitBounds(true /* isPrimary */); + aInfo.windowLayout = new ActivityInfo.WindowLayout(0, 0, 0, 0, 0, + primaryBounds.width() + 1, primaryBounds.height() + 1); + return aInfo; + } +} 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 new file mode 100644 index 000000000000..4d2595275f20 --- /dev/null +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.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 androidx.window.extensions.embedding; + +import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; + +import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_ID; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; + +import android.content.Intent; +import android.content.res.Configuration; +import android.graphics.Point; +import android.os.Handler; +import android.platform.test.annotations.Presubmit; +import android.window.TaskFragmentInfo; +import android.window.WindowContainerToken; +import android.window.WindowContainerTransaction; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.ArrayList; + +/** + * Test class for {@link JetpackTaskFragmentOrganizer}. + * + * Build/Install/Run: + * atest WMJetpackUnitTests:JetpackTaskFragmentOrganizerTest + */ +@Presubmit +@SmallTest +@RunWith(AndroidJUnit4.class) +public class JetpackTaskFragmentOrganizerTest { + @Mock + private WindowContainerTransaction mTransaction; + @Mock + private JetpackTaskFragmentOrganizer.TaskFragmentCallback mCallback; + @Mock + private SplitController mSplitController; + @Mock + private Handler mHandler; + private JetpackTaskFragmentOrganizer mOrganizer; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + mOrganizer = new JetpackTaskFragmentOrganizer(Runnable::run, mCallback); + mOrganizer.registerOrganizer(); + spyOn(mOrganizer); + doReturn(mHandler).when(mSplitController).getHandler(); + } + + @Test + public void testUnregisterOrganizer() { + mOrganizer.startOverrideSplitAnimation(TASK_ID); + mOrganizer.startOverrideSplitAnimation(TASK_ID + 1); + mOrganizer.unregisterOrganizer(); + + verify(mOrganizer).unregisterRemoteAnimations(TASK_ID); + verify(mOrganizer).unregisterRemoteAnimations(TASK_ID + 1); + } + + @Test + public void testStartOverrideSplitAnimation() { + assertNull(mOrganizer.mAnimationController); + + mOrganizer.startOverrideSplitAnimation(TASK_ID); + + 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); + } + + @Test + public void testExpandTaskFragment() { + final TaskContainer taskContainer = new TaskContainer(TASK_ID); + final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */, + new Intent(), taskContainer, mSplitController); + final TaskFragmentInfo info = createMockInfo(container); + mOrganizer.mFragmentInfos.put(container.getTaskFragmentToken(), info); + container.setInfo(info); + + mOrganizer.expandTaskFragment(mTransaction, container.getTaskFragmentToken()); + + verify(mTransaction).setWindowingMode(container.getInfo().getToken(), + WINDOWING_MODE_UNDEFINED); + } + + private TaskFragmentInfo createMockInfo(TaskFragmentContainer container) { + return new TaskFragmentInfo(container.getTaskFragmentToken(), + mock(WindowContainerToken.class), new Configuration(), 0 /* runningActivityCount */, + false /* isVisible */, new ArrayList<>(), new Point(), + false /* isTaskClearedForReuse */, false /* isTaskFragmentClearedForPip */, + new Point()); + } +} diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/MinimumDimensionActivity.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/MinimumDimensionActivity.java new file mode 100644 index 000000000000..ffcaf3e6f546 --- /dev/null +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/MinimumDimensionActivity.java @@ -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. + */ + +package androidx.window.extensions.embedding; + +import android.app.Activity; + +/** + * Activity that declares minWidth and minHeight in + * {@link android.content.pm.ActivityInfo.WindowLayout} + */ +public class MinimumDimensionActivity extends Activity {} 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 new file mode 100644 index 000000000000..ad496a906a33 --- /dev/null +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java @@ -0,0 +1,1104 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.window.extensions.embedding; + +import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; +import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; + +import static androidx.window.extensions.embedding.EmbeddingTestUtils.SPLIT_RATIO; +import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_BOUNDS; +import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_ID; +import static androidx.window.extensions.embedding.EmbeddingTestUtils.createActivityInfoWithMinDimensions; +import static androidx.window.extensions.embedding.EmbeddingTestUtils.createMockTaskFragmentInfo; +import static androidx.window.extensions.embedding.EmbeddingTestUtils.createSplitRule; +import static androidx.window.extensions.embedding.EmbeddingTestUtils.getSplitBounds; +import static androidx.window.extensions.embedding.SplitRule.FINISH_ALWAYS; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; + +import static com.google.common.truth.Truth.assertWithMessage; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; +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.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; + +import android.annotation.NonNull; +import android.app.Activity; +import android.app.ActivityOptions; +import android.content.ComponentName; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.graphics.Rect; +import android.os.Binder; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.platform.test.annotations.Presubmit; +import android.window.TaskFragmentInfo; +import android.window.WindowContainerTransaction; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Test class for {@link SplitController}. + * + * Build/Install/Run: + * atest WMJetpackUnitTests:SplitControllerTest + */ +@Presubmit +@SmallTest +@RunWith(AndroidJUnit4.class) +public class SplitControllerTest { + private static final Intent PLACEHOLDER_INTENT = new Intent().setComponent( + new ComponentName("test", "placeholder")); + + private Activity mActivity; + @Mock + private Resources mActivityResources; + @Mock + private TaskFragmentInfo mInfo; + @Mock + private WindowContainerTransaction mTransaction; + @Mock + private Handler mHandler; + + private SplitController mSplitController; + private SplitPresenter mSplitPresenter; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mSplitController = new SplitController(); + mSplitPresenter = mSplitController.mPresenter; + spyOn(mSplitController); + spyOn(mSplitPresenter); + doNothing().when(mSplitPresenter).applyTransaction(any()); + final Configuration activityConfig = new Configuration(); + activityConfig.windowConfiguration.setBounds(TASK_BOUNDS); + activityConfig.windowConfiguration.setMaxBounds(TASK_BOUNDS); + doReturn(activityConfig).when(mActivityResources).getConfiguration(); + doReturn(mHandler).when(mSplitController).getHandler(); + mActivity = createMockActivity(); + } + + @Test + public void testGetTopActiveContainer() { + final TaskContainer taskContainer = new TaskContainer(TASK_ID); + // tf1 has no running activity so is not active. + final TaskFragmentContainer tf1 = new TaskFragmentContainer(null /* activity */, + new Intent(), taskContainer, mSplitController); + // tf2 has running activity so is active. + final TaskFragmentContainer tf2 = mock(TaskFragmentContainer.class); + doReturn(1).when(tf2).getRunningActivityCount(); + taskContainer.mContainers.add(tf2); + // tf3 is finished so is not active. + final TaskFragmentContainer tf3 = mock(TaskFragmentContainer.class); + doReturn(true).when(tf3).isFinished(); + doReturn(false).when(tf3).isWaitingActivityAppear(); + taskContainer.mContainers.add(tf3); + mSplitController.mTaskContainers.put(TASK_ID, taskContainer); + + assertWithMessage("Must return tf2 because tf3 is not active.") + .that(mSplitController.getTopActiveContainer(TASK_ID)).isEqualTo(tf2); + + taskContainer.mContainers.remove(tf3); + + assertWithMessage("Must return tf2 because tf2 has running activity.") + .that(mSplitController.getTopActiveContainer(TASK_ID)).isEqualTo(tf2); + + taskContainer.mContainers.remove(tf2); + + assertWithMessage("Must return tf because we are waiting for tf1 to appear.") + .that(mSplitController.getTopActiveContainer(TASK_ID)).isEqualTo(tf1); + + final TaskFragmentInfo info = mock(TaskFragmentInfo.class); + doReturn(new ArrayList<>()).when(info).getActivities(); + doReturn(true).when(info).isEmpty(); + tf1.setInfo(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); + + assertWithMessage("Must return null because tf1 becomes empty.") + .that(mSplitController.getTopActiveContainer(TASK_ID)).isNull(); + } + + @Test + public void testOnTaskFragmentVanished() { + final TaskFragmentContainer tf = mSplitController.newContainer(mActivity, TASK_ID); + doReturn(tf.getTaskFragmentToken()).when(mInfo).getFragmentToken(); + + // The TaskFragment has been removed in the server, we only need to cleanup the reference. + mSplitController.onTaskFragmentVanished(mInfo); + + verify(mSplitPresenter, never()).deleteTaskFragment(any(), any()); + verify(mSplitController).removeContainer(tf); + verify(mActivity, never()).finish(); + } + + @Test + public void testOnTaskFragmentAppearEmptyTimeout() { + final TaskFragmentContainer tf = mSplitController.newContainer(mActivity, TASK_ID); + mSplitController.onTaskFragmentAppearEmptyTimeout(tf); + + verify(mSplitPresenter).cleanupContainer(tf, false /* shouldFinishDependent */); + } + + @Test + public void testOnActivityDestroyed() { + doReturn(new Binder()).when(mActivity).getActivityToken(); + final TaskFragmentContainer tf = mSplitController.newContainer(mActivity, TASK_ID); + + assertTrue(tf.hasActivity(mActivity.getActivityToken())); + + mSplitController.onActivityDestroyed(mActivity); + + assertFalse(tf.hasActivity(mActivity.getActivityToken())); + } + + @Test + public void testNewContainer() { + // Must pass in a valid activity. + assertThrows(IllegalArgumentException.class, () -> + mSplitController.newContainer(null /* activity */, TASK_ID)); + assertThrows(IllegalArgumentException.class, () -> + mSplitController.newContainer(mActivity, null /* launchingActivity */, TASK_ID)); + + final TaskFragmentContainer tf = mSplitController.newContainer(mActivity, mActivity, + TASK_ID); + final TaskContainer taskContainer = mSplitController.getTaskContainer(TASK_ID); + + assertNotNull(tf); + assertNotNull(taskContainer); + assertEquals(TASK_BOUNDS, taskContainer.getTaskBounds()); + } + + @Test + public void testUpdateContainer() { + // Make SplitController#launchPlaceholderIfNecessary(TaskFragmentContainer) return true + // and verify if shouldContainerBeExpanded() not called. + final TaskFragmentContainer tf = mSplitController.newContainer(mActivity, TASK_ID); + 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()); + + mSplitController.updateContainer(mTransaction, tf); + + verify(mSplitController, never()).shouldContainerBeExpanded(any()); + + // Verify if tf should be expanded, getTopActiveContainer() won't be called + doReturn(null).when(tf).getTopNonFinishingActivity(); + doReturn(true).when(mSplitController).shouldContainerBeExpanded(tf); + + mSplitController.updateContainer(mTransaction, tf); + + verify(mSplitController, never()).getTopActiveContainer(TASK_ID); + + // Verify if tf is not in split, dismissPlaceholderIfNecessary won't be called. + doReturn(false).when(mSplitController).shouldContainerBeExpanded(tf); + + mSplitController.updateContainer(mTransaction, tf); + + verify(mSplitController, never()).dismissPlaceholderIfNecessary(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(); + final List<SplitContainer> splitContainers = + mSplitController.getTaskContainer(TASK_ID).mSplitContainers; + splitContainers.add(splitContainer); + // Add a mock SplitContainer on top of splitContainer + splitContainers.add(1, mock(SplitContainer.class)); + + mSplitController.updateContainer(mTransaction, tf); + + verify(mSplitController, never()).dismissPlaceholderIfNecessary(any()); + + // Verify if one or both containers in the top SplitContainer are finished, + // dismissPlaceholder() won't be called. + splitContainers.remove(1); + doReturn(true).when(tf).isFinished(); + + mSplitController.updateContainer(mTransaction, tf); + + verify(mSplitController, never()).dismissPlaceholderIfNecessary(any()); + + // Verify if placeholder should be dismissed, updateSplitContainer() won't be called. + doReturn(false).when(tf).isFinished(); + doReturn(true).when(mSplitController) + .dismissPlaceholderIfNecessary(splitContainer); + + mSplitController.updateContainer(mTransaction, tf); + + verify(mSplitPresenter, never()).updateSplitContainer(any(), any(), any()); + + // Verify if the top active split is updated if both of its containers are not finished. + doReturn(false).when(mSplitController) + .dismissPlaceholderIfNecessary(splitContainer); + + mSplitController.updateContainer(mTransaction, tf); + + verify(mSplitPresenter).updateSplitContainer(splitContainer, tf, mTransaction); + } + + @Test + public void testOnActivityCreated() { + mSplitController.onActivityCreated(mActivity); + + // Disallow to split as primary because we want the new launch to be always on top. + verify(mSplitController).resolveActivityToContainer(mActivity, false /* isOnReparent */); + } + + @Test + public void testOnActivityReparentToTask_sameProcess() { + mSplitController.onActivityReparentToTask(TASK_ID, new Intent(), + mActivity.getActivityToken()); + + // Treated as on activity created, but allow to split as primary. + verify(mSplitController).resolveActivityToContainer(mActivity, true /* isOnReparent */); + // Try to place the activity to the top TaskFragment when there is no matched rule. + verify(mSplitController).placeActivityInTopContainer(mActivity); + } + + @Test + public void testOnActivityReparentToTask_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); + + // Treated as starting new intent + verify(mSplitController, never()).resolveActivityToContainer(any(), anyBoolean()); + verify(mSplitController).resolveStartActivityIntent(any(), eq(TASK_ID), eq(intent), + isNull()); + } + + @Test + public void testResolveStartActivityIntent_withoutLaunchingActivity() { + final Intent intent = new Intent(); + final ActivityRule expandRule = new ActivityRule.Builder(r -> false, i -> i == intent) + .setShouldAlwaysExpand(true) + .build(); + mSplitController.setEmbeddingRules(Collections.singleton(expandRule)); + + // No other activity available in the Task. + TaskFragmentContainer container = mSplitController.resolveStartActivityIntent(mTransaction, + TASK_ID, intent, null /* launchingActivity */); + assertNull(container); + + // Task contains another activity that can be used as owner activity. + createMockTaskFragmentContainer(mActivity); + container = mSplitController.resolveStartActivityIntent(mTransaction, + TASK_ID, intent, null /* launchingActivity */); + assertNotNull(container); + } + + @Test + public void testResolveStartActivityIntent_shouldExpand() { + final Intent intent = new Intent(); + setupExpandRule(intent); + final TaskFragmentContainer container = mSplitController.resolveStartActivityIntent( + mTransaction, TASK_ID, intent, mActivity); + + assertNotNull(container); + assertTrue(container.areLastRequestedBoundsEqual(null)); + assertTrue(container.isLastRequestedWindowingModeEqual(WINDOWING_MODE_UNDEFINED)); + assertFalse(container.hasActivity(mActivity.getActivityToken())); + verify(mSplitPresenter).createTaskFragment(mTransaction, container.getTaskFragmentToken(), + mActivity.getActivityToken(), new Rect(), WINDOWING_MODE_UNDEFINED); + } + + @Test + public void testResolveStartActivityIntent_shouldSplitWithLaunchingActivity() { + final Intent intent = new Intent(); + setupSplitRule(mActivity, intent); + + final TaskFragmentContainer container = mSplitController.resolveStartActivityIntent( + mTransaction, TASK_ID, intent, mActivity); + final TaskFragmentContainer primaryContainer = mSplitController.getContainerWithActivity( + mActivity); + + assertSplitPair(primaryContainer, container); + } + + @Test + public void testResolveStartActivityIntent_shouldSplitWithTopExpandActivity() { + final Intent intent = new Intent(); + setupSplitRule(mActivity, intent); + createMockTaskFragmentContainer(mActivity); + + final TaskFragmentContainer container = mSplitController.resolveStartActivityIntent( + mTransaction, TASK_ID, intent, null /* launchingActivity */); + final TaskFragmentContainer primaryContainer = mSplitController.getContainerWithActivity( + mActivity); + + assertSplitPair(primaryContainer, container); + } + + @Test + public void testResolveStartActivityIntent_shouldSplitWithTopSecondaryActivity() { + final Intent intent = new Intent(); + setupSplitRule(mActivity, intent); + final Activity primaryActivity = createMockActivity(); + addSplitTaskFragments(primaryActivity, mActivity); + + final TaskFragmentContainer container = mSplitController.resolveStartActivityIntent( + mTransaction, TASK_ID, intent, null /* launchingActivity */); + final TaskFragmentContainer primaryContainer = mSplitController.getContainerWithActivity( + mActivity); + + assertSplitPair(primaryContainer, container); + } + + @Test + public void testResolveStartActivityIntent_shouldSplitWithTopPrimaryActivity() { + final Intent intent = new Intent(); + setupSplitRule(mActivity, intent); + final Activity secondaryActivity = createMockActivity(); + addSplitTaskFragments(mActivity, secondaryActivity); + + final TaskFragmentContainer container = mSplitController.resolveStartActivityIntent( + mTransaction, TASK_ID, intent, null /* launchingActivity */); + final TaskFragmentContainer primaryContainer = mSplitController.getContainerWithActivity( + mActivity); + + assertSplitPair(primaryContainer, container); + } + + @Test + public void testResolveStartActivityIntent_shouldLaunchInFullscreen() { + final Intent intent = new Intent().setComponent( + new ComponentName(ApplicationProvider.getApplicationContext(), + MinimumDimensionActivity.class)); + setupSplitRule(mActivity, intent); + final Activity primaryActivity = createMockActivity(); + addSplitTaskFragments(primaryActivity, mActivity); + + final TaskFragmentContainer container = mSplitController.resolveStartActivityIntent( + mTransaction, TASK_ID, intent, null /* launchingActivity */); + final TaskFragmentContainer primaryContainer = mSplitController.getContainerWithActivity( + mActivity); + + assertNotNull(mSplitController.getActiveSplitForContainers(primaryContainer, container)); + assertTrue(primaryContainer.areLastRequestedBoundsEqual(null)); + assertTrue(container.areLastRequestedBoundsEqual(null)); + } + + @Test + public void testResolveStartActivityIntent_shouldExpandSplitContainer() { + final Intent intent = new Intent().setComponent( + new ComponentName(ApplicationProvider.getApplicationContext(), + MinimumDimensionActivity.class)); + setupSplitRule(mActivity, intent, false /* clearTop */); + final Activity secondaryActivity = createMockActivity(); + addSplitTaskFragments(mActivity, secondaryActivity, false /* clearTop */); + + final TaskFragmentContainer container = mSplitController.resolveStartActivityIntent( + mTransaction, TASK_ID, intent, mActivity); + final TaskFragmentContainer primaryContainer = mSplitController.getContainerWithActivity( + mActivity); + + assertNotNull(mSplitController.getActiveSplitForContainers(primaryContainer, container)); + assertTrue(primaryContainer.areLastRequestedBoundsEqual(null)); + assertTrue(container.areLastRequestedBoundsEqual(null)); + assertEquals(container, mSplitController.getContainerWithActivity(secondaryActivity)); + } + + @Test + public void testResolveStartActivityIntent_noInfo_shouldCreateSplitContainer() { + final Intent intent = new Intent().setComponent( + new ComponentName(ApplicationProvider.getApplicationContext(), + MinimumDimensionActivity.class)); + setupSplitRule(mActivity, intent, false /* clearTop */); + final Activity secondaryActivity = createMockActivity(); + addSplitTaskFragments(mActivity, secondaryActivity, false /* clearTop */); + + final TaskFragmentContainer secondaryContainer = mSplitController + .getContainerWithActivity(secondaryActivity); + secondaryContainer.mInfo = null; + + final TaskFragmentContainer container = mSplitController.resolveStartActivityIntent( + mTransaction, TASK_ID, intent, mActivity); + final TaskFragmentContainer primaryContainer = mSplitController.getContainerWithActivity( + mActivity); + + assertNotNull(mSplitController.getActiveSplitForContainers(primaryContainer, container)); + assertTrue(primaryContainer.areLastRequestedBoundsEqual(null)); + assertTrue(container.areLastRequestedBoundsEqual(null)); + assertNotEquals(container, secondaryContainer); + } + + @Test + public void testPlaceActivityInTopContainer() { + mSplitController.placeActivityInTopContainer(mActivity); + + verify(mSplitPresenter, never()).applyTransaction(any()); + + mSplitController.newContainer(new Intent(), mActivity, TASK_ID); + mSplitController.placeActivityInTopContainer(mActivity); + + verify(mSplitPresenter).applyTransaction(any()); + + // Not reparent if activity is in a TaskFragment. + clearInvocations(mSplitPresenter); + mSplitController.newContainer(mActivity, TASK_ID); + mSplitController.placeActivityInTopContainer(mActivity); + + verify(mSplitPresenter, never()).applyTransaction(any()); + } + + @Test + public void testResolveActivityToContainer_noRuleMatched() { + final boolean result = mSplitController.resolveActivityToContainer(mActivity, + false /* isOnReparent */); + + assertFalse(result); + verify(mSplitController, never()).newContainer(any(), any(), any(), anyInt()); + } + + @Test + public void testResolveActivityToContainer_expandRule_notInTaskFragment() { + setupExpandRule(mActivity); + + // When the activity is not in any TaskFragment, create a new expanded TaskFragment for it. + final boolean result = mSplitController.resolveActivityToContainer(mActivity, + false /* isOnReparent */); + final TaskFragmentContainer container = mSplitController.getContainerWithActivity( + mActivity); + + assertTrue(result); + assertNotNull(container); + verify(mSplitController).newContainer(mActivity, TASK_ID); + verify(mSplitPresenter).expandActivity(container.getTaskFragmentToken(), mActivity); + } + + @Test + public void testResolveActivityToContainer_expandRule_inSingleTaskFragment() { + setupExpandRule(mActivity); + + // 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, + false /* isOnReparent */); + + assertTrue(result); + verify(mSplitPresenter).expandTaskFragment(container.getTaskFragmentToken()); + } + + @Test + public void testResolveActivityToContainer_expandRule_inSplitTaskFragment() { + setupExpandRule(mActivity); + + // 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, + false /* isOnReparent */); + final TaskFragmentContainer container = mSplitController.getContainerWithActivity( + mActivity); + + assertTrue(result); + assertNotNull(container); + verify(mSplitPresenter).expandActivity(container.getTaskFragmentToken(), mActivity); + } + + @Test + public void testResolveActivityToContainer_placeholderRule_notInTaskFragment() { + 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, + false /* isOnReparent */); + + assertTrue(result); + verify(mSplitPresenter).startActivityToSide(mActivity, PLACEHOLDER_INTENT, + mSplitController.getPlaceholderOptions(mActivity, true /* isOnCreated */), + placeholderRule, true /* isPlaceholder */); + } + + @Test + public void testResolveActivityToContainer_placeholderRule_inOccludedTaskFragment() { + setupPlaceholderRule(mActivity); + + // Don't launch placeholder if the activity is not in the topmost active TaskFragment. + final Activity activity = createMockActivity(); + mSplitController.newContainer(mActivity, TASK_ID); + mSplitController.newContainer(activity, TASK_ID); + final boolean result = mSplitController.resolveActivityToContainer(mActivity, + false /* isOnReparent */); + + assertFalse(result); + verify(mSplitPresenter, never()).startActivityToSide(any(), any(), any(), any(), + anyBoolean()); + } + + @Test + public void testResolveActivityToContainer_placeholderRule_inTopMostTaskFragment() { + 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, + false /* isOnReparent */); + + assertTrue(result); + verify(mSplitPresenter).startActivityToSide(mActivity, PLACEHOLDER_INTENT, + mSplitController.getPlaceholderOptions(mActivity, true /* isOnCreated */), + placeholderRule, true /* isPlaceholder */); + } + + @Test + public void testResolveActivityToContainer_placeholderRule_inPrimarySplit() { + setupPlaceholderRule(mActivity); + + // Don't launch placeholder if the activity is in primary split. + final Activity secondaryActivity = createMockActivity(); + addSplitTaskFragments(mActivity, secondaryActivity); + final boolean result = mSplitController.resolveActivityToContainer(mActivity, + false /* isOnReparent */); + + assertFalse(result); + verify(mSplitPresenter, never()).startActivityToSide(any(), any(), any(), any(), + anyBoolean()); + } + + @Test + public void testResolveActivityToContainer_placeholderRule_inSecondarySplit() { + setupPlaceholderRule(mActivity); + final SplitPlaceholderRule placeholderRule = + (SplitPlaceholderRule) mSplitController.getSplitRules().get(0); + + // Launch placeholder if the activity is in secondary split. + final Activity primaryActivity = createMockActivity(); + addSplitTaskFragments(primaryActivity, mActivity); + final boolean result = mSplitController.resolveActivityToContainer(mActivity, + false /* isOnReparent */); + + assertTrue(result); + verify(mSplitPresenter).startActivityToSide(mActivity, PLACEHOLDER_INTENT, + mSplitController.getPlaceholderOptions(mActivity, true /* isOnCreated */), + placeholderRule, true /* isPlaceholder */); + } + + @Test + public void testResolveActivityToContainer_splitRule_inPrimarySplitWithRuleMatched() { + final Intent secondaryIntent = new Intent(); + setupSplitRule(mActivity, secondaryIntent); + final SplitPairRule splitRule = (SplitPairRule) mSplitController.getSplitRules().get(0); + + // Activity is already in primary split, no need to create new split. + final TaskFragmentContainer primaryContainer = mSplitController.newContainer(mActivity, + TASK_ID); + final TaskFragmentContainer secondaryContainer = mSplitController.newContainer( + secondaryIntent, mActivity, TASK_ID); + mSplitController.registerSplit( + mTransaction, + primaryContainer, + mActivity, + secondaryContainer, + splitRule); + clearInvocations(mSplitController); + final boolean result = mSplitController.resolveActivityToContainer(mActivity, + false /* isOnReparent */); + + assertTrue(result); + verify(mSplitController, never()).newContainer(any(), any(), any(), anyInt()); + verify(mSplitController, never()).registerSplit(any(), any(), any(), any(), any()); + } + + @Test + public void testResolveActivityToContainer_splitRule_inPrimarySplitWithNoRuleMatched() { + final Intent secondaryIntent = new Intent(); + setupSplitRule(mActivity, secondaryIntent); + final SplitPairRule splitRule = (SplitPairRule) mSplitController.getSplitRules().get(0); + + // The new launched activity is in primary split, but there is no rule for it to split with + // the secondary, so return false. + final TaskFragmentContainer primaryContainer = mSplitController.newContainer(mActivity, + TASK_ID); + final TaskFragmentContainer secondaryContainer = mSplitController.newContainer( + secondaryIntent, mActivity, TASK_ID); + mSplitController.registerSplit( + mTransaction, + primaryContainer, + mActivity, + secondaryContainer, + splitRule); + final Activity launchedActivity = createMockActivity(); + primaryContainer.addPendingAppearedActivity(launchedActivity); + + assertFalse(mSplitController.resolveActivityToContainer(launchedActivity, + false /* isOnReparent */)); + } + + @Test + public void testResolveActivityToContainer_splitRule_inSecondarySplitWithRuleMatched() { + final Activity primaryActivity = createMockActivity(); + setupSplitRule(primaryActivity, mActivity); + + // Activity is already in secondary split, no need to create new split. + addSplitTaskFragments(primaryActivity, mActivity); + clearInvocations(mSplitController); + final boolean result = mSplitController.resolveActivityToContainer(mActivity, + false /* isOnReparent */); + + assertTrue(result); + verify(mSplitController, never()).newContainer(any(), any(), any(), anyInt()); + verify(mSplitController, never()).registerSplit(any(), any(), any(), any(), any()); + } + + @Test + public void testResolveActivityToContainer_splitRule_inSecondarySplitWithNoRuleMatched() { + final Activity primaryActivity = createMockActivity(); + final Activity secondaryActivity = createMockActivity(); + setupSplitRule(primaryActivity, secondaryActivity); + + // Activity is in secondary split, but there is no rule to split it with primary. + addSplitTaskFragments(primaryActivity, secondaryActivity); + mSplitController.getContainerWithActivity(secondaryActivity) + .addPendingAppearedActivity(mActivity); + final boolean result = mSplitController.resolveActivityToContainer(mActivity, + false /* isOnReparent */); + + assertFalse(result); + } + + @Test + public void testResolveActivityToContainer_placeholderRule_isPlaceholderWithRuleMatched() { + final Activity primaryActivity = createMockActivity(); + setupPlaceholderRule(primaryActivity); + final SplitPlaceholderRule placeholderRule = + (SplitPlaceholderRule) mSplitController.getSplitRules().get(0); + doReturn(PLACEHOLDER_INTENT).when(mActivity).getIntent(); + + // Activity is a placeholder. + final TaskFragmentContainer primaryContainer = mSplitController.newContainer( + primaryActivity, TASK_ID); + final TaskFragmentContainer secondaryContainer = mSplitController.newContainer(mActivity, + TASK_ID); + mSplitController.registerSplit( + mTransaction, + primaryContainer, + mActivity, + secondaryContainer, + placeholderRule); + final boolean result = mSplitController.resolveActivityToContainer(mActivity, + false /* isOnReparent */); + + assertTrue(result); + } + + @Test + public void testResolveActivityToContainer_splitRule_splitWithActivityBelowAsSecondary() { + final Activity activityBelow = createMockActivity(); + setupSplitRule(activityBelow, mActivity); + + final TaskFragmentContainer container = mSplitController.newContainer(activityBelow, + TASK_ID); + container.addPendingAppearedActivity(mActivity); + final boolean result = mSplitController.resolveActivityToContainer(mActivity, + false /* isOnReparent */); + + assertTrue(result); + assertSplitPair(activityBelow, mActivity); + } + + @Test + public void testResolveActivityToContainer_splitRule_splitWithActivityBelowAsPrimary() { + final Activity activityBelow = createMockActivity(); + setupSplitRule(mActivity, activityBelow); + + // Disallow to split as primary. + final TaskFragmentContainer container = mSplitController.newContainer(activityBelow, + TASK_ID); + container.addPendingAppearedActivity(mActivity); + boolean result = mSplitController.resolveActivityToContainer(mActivity, + false /* isOnReparent */); + + assertFalse(result); + assertEquals(container, mSplitController.getContainerWithActivity(mActivity)); + + // Allow to split as primary. + result = mSplitController.resolveActivityToContainer(mActivity, true /* isOnReparent */); + + assertTrue(result); + assertSplitPair(mActivity, activityBelow); + } + + @Test + public void testResolveActivityToContainer_splitRule_splitWithCurrentPrimaryAsSecondary() { + final Activity primaryActivity = createMockActivity(); + setupSplitRule(primaryActivity, mActivity); + + final Activity activityBelow = createMockActivity(); + addSplitTaskFragments(primaryActivity, activityBelow); + final TaskFragmentContainer primaryContainer = mSplitController.getContainerWithActivity( + primaryActivity); + final TaskFragmentContainer secondaryContainer = mSplitController.getContainerWithActivity( + activityBelow); + secondaryContainer.addPendingAppearedActivity(mActivity); + final boolean result = mSplitController.resolveActivityToContainer(mActivity, + false /* isOnReparent */); + final TaskFragmentContainer container = mSplitController.getContainerWithActivity( + mActivity); + + assertTrue(result); + // TODO(b/231845476) we should always respect clearTop. + // assertNotEquals(secondaryContainer, container); + assertSplitPair(primaryContainer, container); + } + + @Test + public void testResolveActivityToContainer_splitRule_splitWithCurrentPrimaryAsPrimary() { + final Activity primaryActivity = createMockActivity(); + setupSplitRule(mActivity, primaryActivity); + + final Activity activityBelow = createMockActivity(); + addSplitTaskFragments(primaryActivity, activityBelow); + final TaskFragmentContainer primaryContainer = mSplitController.getContainerWithActivity( + primaryActivity); + primaryContainer.addPendingAppearedActivity(mActivity); + boolean result = mSplitController.resolveActivityToContainer(mActivity, + false /* isOnReparent */); + + assertFalse(result); + assertEquals(primaryContainer, mSplitController.getContainerWithActivity(mActivity)); + + + result = mSplitController.resolveActivityToContainer(mActivity, true /* isOnReparent */); + + assertTrue(result); + assertSplitPair(mActivity, primaryActivity); + } + + @Test + public void testResolveActivityToContainer_primaryActivityMinDimensionsNotSatisfied() { + final Activity activityBelow = createMockActivity(); + setupSplitRule(mActivity, activityBelow); + + doReturn(createActivityInfoWithMinDimensions()).when(mActivity).getActivityInfo(); + + final TaskFragmentContainer container = mSplitController.newContainer(activityBelow, + TASK_ID); + container.addPendingAppearedActivity(mActivity); + + // Allow to split as primary. + boolean result = mSplitController.resolveActivityToContainer(mActivity, + true /* isOnReparent */); + + assertTrue(result); + assertSplitPair(mActivity, activityBelow, true /* matchParentBounds */); + } + + @Test + public void testResolveActivityToContainer_secondaryActivityMinDimensionsNotSatisfied() { + final Activity activityBelow = createMockActivity(); + setupSplitRule(activityBelow, mActivity); + + doReturn(createActivityInfoWithMinDimensions()).when(mActivity).getActivityInfo(); + + final TaskFragmentContainer container = mSplitController.newContainer(activityBelow, + TASK_ID); + container.addPendingAppearedActivity(mActivity); + + boolean result = mSplitController.resolveActivityToContainer(mActivity, + false /* isOnReparent */); + + assertTrue(result); + assertSplitPair(activityBelow, mActivity, true /* matchParentBounds */); + } + + @Test + public void testResolveActivityToContainer_minDimensions_shouldExpandSplitContainer() { + final Activity primaryActivity = createMockActivity(); + final Activity secondaryActivity = createMockActivity(); + addSplitTaskFragments(primaryActivity, secondaryActivity, false /* clearTop */); + + setupSplitRule(primaryActivity, mActivity, false /* clearTop */); + doReturn(createActivityInfoWithMinDimensions()).when(mActivity).getActivityInfo(); + doReturn(secondaryActivity).when(mSplitController).findActivityBelow(eq(mActivity)); + + clearInvocations(mSplitPresenter); + boolean result = mSplitController.resolveActivityToContainer(mActivity, + false /* isOnReparent */); + + assertTrue(result); + assertSplitPair(primaryActivity, mActivity, true /* matchParentBounds */); + assertEquals(mSplitController.getContainerWithActivity(secondaryActivity), + mSplitController.getContainerWithActivity(mActivity)); + verify(mSplitPresenter, never()).createNewSplitContainer(any(), any(), any()); + } + + @Test + public void testResolveActivityToContainer_inUnknownTaskFragment() { + doReturn(new Binder()).when(mSplitController).getInitialTaskFragmentToken(mActivity); + + // No need to handle when the new launched activity is in an unknown TaskFragment. + assertTrue(mSplitController.resolveActivityToContainer(mActivity, + false /* isOnReparent */)); + } + + @Test + public void testGetPlaceholderOptions() { + doReturn(true).when(mActivity).isResumed(); + + assertNull(mSplitController.getPlaceholderOptions(mActivity, false /* isOnCreated */)); + + doReturn(false).when(mActivity).isResumed(); + + assertNull(mSplitController.getPlaceholderOptions(mActivity, true /* isOnCreated */)); + + // Launch placeholder without moving the Task to front if the Task is now in background (not + // resumed or onCreated). + final Bundle options = mSplitController.getPlaceholderOptions(mActivity, + false /* isOnCreated */); + + assertNotNull(options); + final ActivityOptions activityOptions = new ActivityOptions(options); + assertTrue(activityOptions.getAvoidMoveToFront()); + } + + @Test + public void testFinishTwoSplitThatShouldFinishTogether() { + // Setup two split pairs that should finish each other when finishing one. + final Activity secondaryActivity0 = createMockActivity(); + final Activity secondaryActivity1 = createMockActivity(); + final TaskFragmentContainer primaryContainer = createMockTaskFragmentContainer(mActivity); + final TaskFragmentContainer secondaryContainer0 = createMockTaskFragmentContainer( + secondaryActivity0); + final TaskFragmentContainer secondaryContainer1 = createMockTaskFragmentContainer( + secondaryActivity1); + final TaskContainer taskContainer = mSplitController.getTaskContainer(TASK_ID); + final SplitRule rule0 = createSplitRule(mActivity, secondaryActivity0, FINISH_ALWAYS, + FINISH_ALWAYS, false /* clearTop */); + final SplitRule rule1 = createSplitRule(mActivity, secondaryActivity1, FINISH_ALWAYS, + FINISH_ALWAYS, false /* clearTop */); + registerSplitPair(primaryContainer, secondaryContainer0, rule0); + registerSplitPair(primaryContainer, secondaryContainer1, rule1); + + primaryContainer.finish(true /* shouldFinishDependent */, mSplitPresenter, + mTransaction, mSplitController); + + // All containers and activities should be finished based on the FINISH_ALWAYS behavior. + assertTrue(primaryContainer.isFinished()); + assertTrue(secondaryContainer0.isFinished()); + assertTrue(secondaryContainer1.isFinished()); + verify(mActivity).finish(); + verify(secondaryActivity0).finish(); + verify(secondaryActivity1).finish(); + assertTrue(taskContainer.mContainers.isEmpty()); + assertTrue(taskContainer.mSplitContainers.isEmpty()); + } + + /** Creates a mock activity in the organizer process. */ + private Activity createMockActivity() { + 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(new ActivityInfo()).when(activity).getActivityInfo(); + 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); + setupTaskFragmentInfo(container, activity); + return container; + } + + /** Setups the given TaskFragment as it has appeared in the server. */ + private void setupTaskFragmentInfo(@NonNull TaskFragmentContainer container, + @NonNull Activity activity) { + final TaskFragmentInfo info = createMockTaskFragmentInfo(container, activity); + container.setInfo(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) + .setShouldAlwaysExpand(true) + .build(); + mSplitController.setEmbeddingRules(Collections.singleton(expandRule)); + } + + /** 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) + .setShouldAlwaysExpand(true) + .build(); + mSplitController.setEmbeddingRules(Collections.singleton(expandRule)); + } + + /** Setups a rule to launch placeholder for the given activity. */ + private void setupPlaceholderRule(@NonNull Activity primaryActivity) { + final SplitRule placeholderRule = new SplitPlaceholderRule.Builder(PLACEHOLDER_INTENT, + primaryActivity::equals, i -> false, w -> true) + .setSplitRatio(SPLIT_RATIO) + .build(); + mSplitController.setEmbeddingRules(Collections.singleton(placeholderRule)); + } + + /** Setups a rule to always split the given activities. */ + private void setupSplitRule(@NonNull Activity primaryActivity, + @NonNull Intent secondaryIntent) { + setupSplitRule(primaryActivity, secondaryIntent, true /* clearTop */); + } + + /** Setups a rule to always split the given activities. */ + private void setupSplitRule(@NonNull Activity primaryActivity, + @NonNull Intent secondaryIntent, boolean clearTop) { + final SplitRule splitRule = createSplitRule(primaryActivity, secondaryIntent, clearTop); + mSplitController.setEmbeddingRules(Collections.singleton(splitRule)); + } + + /** Setups a rule to always split the given activities. */ + private void setupSplitRule(@NonNull Activity primaryActivity, + @NonNull Activity secondaryActivity) { + setupSplitRule(primaryActivity, secondaryActivity, true /* clearTop */); + } + + /** Setups a rule to always split the given activities. */ + private void setupSplitRule(@NonNull Activity primaryActivity, + @NonNull Activity secondaryActivity, boolean clearTop) { + final SplitRule splitRule = createSplitRule(primaryActivity, secondaryActivity, clearTop); + mSplitController.setEmbeddingRules(Collections.singleton(splitRule)); + } + + /** Adds a pair of TaskFragments as split for the given activities. */ + private void addSplitTaskFragments(@NonNull Activity primaryActivity, + @NonNull Activity secondaryActivity) { + addSplitTaskFragments(primaryActivity, secondaryActivity, true /* clearTop */); + } + + /** Adds a pair of TaskFragments as split for the given activities. */ + private void addSplitTaskFragments(@NonNull Activity primaryActivity, + @NonNull Activity secondaryActivity, boolean clearTop) { + registerSplitPair(createMockTaskFragmentContainer(primaryActivity), + createMockTaskFragmentContainer(secondaryActivity), + createSplitRule(primaryActivity, secondaryActivity, clearTop)); + } + + /** Registers the two given TaskFragments as split pair. */ + private void registerSplitPair(@NonNull TaskFragmentContainer primaryContainer, + @NonNull TaskFragmentContainer secondaryContainer, @NonNull SplitRule rule) { + mSplitController.registerSplit( + mock(WindowContainerTransaction.class), + primaryContainer, + primaryContainer.getTopNonFinishingActivity(), + secondaryContainer, + rule); + + // 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) + .getWindowingModeForSplitTaskFragment(TASK_BOUNDS); + primaryContainer.setLastRequestedWindowingMode(windowingMode); + secondaryContainer.setLastRequestedWindowingMode(windowingMode); + primaryContainer.setLastRequestedBounds(getSplitBounds(true /* isPrimary */)); + secondaryContainer.setLastRequestedBounds(getSplitBounds(false /* isPrimary */)); + } + + /** Asserts that the two given activities are in split. */ + private void assertSplitPair(@NonNull Activity primaryActivity, + @NonNull Activity secondaryActivity) { + assertSplitPair(primaryActivity, secondaryActivity, false /* matchParentBounds */); + } + + /** Asserts that the two given activities are in split. */ + private void assertSplitPair(@NonNull Activity primaryActivity, + @NonNull Activity secondaryActivity, boolean matchParentBounds) { + assertSplitPair(mSplitController.getContainerWithActivity(primaryActivity), + mSplitController.getContainerWithActivity(secondaryActivity), matchParentBounds); + } + + private void assertSplitPair(@NonNull TaskFragmentContainer primaryContainer, + @NonNull TaskFragmentContainer secondaryContainer) { + assertSplitPair(primaryContainer, secondaryContainer, false /* matchParentBounds*/); + } + + /** Asserts that the two given TaskFragments are in split. */ + private void assertSplitPair(@NonNull TaskFragmentContainer primaryContainer, + @NonNull TaskFragmentContainer secondaryContainer, boolean matchParentBounds) { + assertNotNull(primaryContainer); + assertNotNull(secondaryContainer); + assertNotNull(mSplitController.getActiveSplitForContainers(primaryContainer, + secondaryContainer)); + if (primaryContainer.mInfo != null) { + final Rect primaryBounds = matchParentBounds ? new Rect() + : getSplitBounds(true /* isPrimary */); + final int windowingMode = matchParentBounds ? WINDOWING_MODE_UNDEFINED + : WINDOWING_MODE_MULTI_WINDOW; + assertTrue(primaryContainer.areLastRequestedBoundsEqual(primaryBounds)); + assertTrue(primaryContainer.isLastRequestedWindowingModeEqual(windowingMode)); + } + if (secondaryContainer.mInfo != null) { + final Rect secondaryBounds = matchParentBounds ? new Rect() + : getSplitBounds(false /* isPrimary */); + final int windowingMode = matchParentBounds ? WINDOWING_MODE_UNDEFINED + : WINDOWING_MODE_MULTI_WINDOW; + assertTrue(secondaryContainer.areLastRequestedBoundsEqual(secondaryBounds)); + assertTrue(secondaryContainer.isLastRequestedWindowingModeEqual(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 new file mode 100644 index 000000000000..d79319666c01 --- /dev/null +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java @@ -0,0 +1,262 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.window.extensions.embedding; + +import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; + +import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_BOUNDS; +import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_ID; +import static androidx.window.extensions.embedding.EmbeddingTestUtils.createActivityInfoWithMinDimensions; +import static androidx.window.extensions.embedding.EmbeddingTestUtils.createMockTaskFragmentInfo; +import static androidx.window.extensions.embedding.EmbeddingTestUtils.createSplitRule; +import static androidx.window.extensions.embedding.EmbeddingTestUtils.getSplitBounds; +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.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; + +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.Rect; +import android.os.IBinder; +import android.platform.test.annotations.Presubmit; +import android.util.Pair; +import android.util.Size; +import android.window.TaskFragmentInfo; +import android.window.WindowContainerTransaction; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** + * Test class for {@link SplitPresenter}. + * + * Build/Install/Run: + * atest WMJetpackUnitTests:SplitPresenterTest + */ +@Presubmit +@SmallTest +@RunWith(AndroidJUnit4.class) +public class SplitPresenterTest { + + @Mock + private Activity mActivity; + @Mock + private Resources mActivityResources; + @Mock + private TaskFragmentInfo mTaskFragmentInfo; + @Mock + private WindowContainerTransaction mTransaction; + private SplitController mController; + private SplitPresenter mPresenter; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mController = new SplitController(); + mPresenter = mController.mPresenter; + spyOn(mController); + spyOn(mPresenter); + mActivity = createMockActivity(); + } + + @Test + public void testCreateTaskFragment() { + final TaskFragmentContainer container = mController.newContainer(mActivity, TASK_ID); + mPresenter.createTaskFragment(mTransaction, container.getTaskFragmentToken(), + mActivity.getActivityToken(), TASK_BOUNDS, WINDOWING_MODE_MULTI_WINDOW); + + assertTrue(container.areLastRequestedBoundsEqual(TASK_BOUNDS)); + assertTrue(container.isLastRequestedWindowingModeEqual(WINDOWING_MODE_MULTI_WINDOW)); + verify(mTransaction).createTaskFragment(any()); + } + + @Test + public void testResizeTaskFragment() { + final TaskFragmentContainer container = mController.newContainer(mActivity, TASK_ID); + mPresenter.mFragmentInfos.put(container.getTaskFragmentToken(), mTaskFragmentInfo); + mPresenter.resizeTaskFragment(mTransaction, container.getTaskFragmentToken(), TASK_BOUNDS); + + assertTrue(container.areLastRequestedBoundsEqual(TASK_BOUNDS)); + verify(mTransaction).setBounds(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()); + } + + @Test + public void testUpdateWindowingMode() { + final TaskFragmentContainer container = mController.newContainer(mActivity, TASK_ID); + mPresenter.mFragmentInfos.put(container.getTaskFragmentToken(), mTaskFragmentInfo); + mPresenter.updateWindowingMode(mTransaction, container.getTaskFragmentToken(), + WINDOWING_MODE_MULTI_WINDOW); + + assertTrue(container.isLastRequestedWindowingModeEqual(WINDOWING_MODE_MULTI_WINDOW)); + verify(mTransaction).setWindowingMode(any(), eq(WINDOWING_MODE_MULTI_WINDOW)); + + // No request to set the same windowing mode. + clearInvocations(mTransaction); + mPresenter.updateWindowingMode(mTransaction, container.getTaskFragmentToken(), + WINDOWING_MODE_MULTI_WINDOW); + + verify(mTransaction, never()).setWindowingMode(any(), anyInt()); + + } + + @Test + public void testGetMinDimensionsForIntent() { + final Intent intent = new Intent(ApplicationProvider.getApplicationContext(), + MinimumDimensionActivity.class); + assertEquals(new Size(600, 1200), getMinDimensions(intent)); + } + + @Test + public void testShouldShowSideBySide() { + Activity secondaryActivity = createMockActivity(); + final SplitRule splitRule = createSplitRule(mActivity, secondaryActivity); + + assertTrue(shouldShowSideBySide(TASK_BOUNDS, splitRule)); + + // Set minDimensions of primary container to larger than primary bounds. + final Rect primaryBounds = getSplitBounds(true /* isPrimary */); + Pair<Size, Size> minDimensionsPair = new Pair<>( + new Size(primaryBounds.width() + 1, primaryBounds.height() + 1), null); + + assertFalse(shouldShowSideBySide(TASK_BOUNDS, splitRule, minDimensionsPair)); + } + + @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 */); + + assertEquals("Primary bounds must be reported.", + primaryBounds, + getBoundsForPosition(POSITION_START, TASK_BOUNDS, splitRule, + mActivity, null /* miniDimensionsPair */)); + + assertEquals("Secondary bounds must be reported.", + secondaryBounds, + getBoundsForPosition(POSITION_END, TASK_BOUNDS, splitRule, + mActivity, null /* miniDimensionsPair */)); + assertEquals("Task bounds must be reported.", + new Rect(), + getBoundsForPosition(POSITION_FILL, TASK_BOUNDS, splitRule, + mActivity, null /* miniDimensionsPair */)); + + Pair<Size, Size> minDimensionsPair = new Pair<>( + new Size(primaryBounds.width() + 1, primaryBounds.height() + 1), null); + + assertEquals("Fullscreen bounds must be reported because of min dimensions.", + new Rect(), + getBoundsForPosition(POSITION_START, TASK_BOUNDS, + splitRule, mActivity, minDimensionsPair)); + } + + @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(); + + assertThrows(IllegalArgumentException.class, () -> + mPresenter.expandSplitContainerIfNeeded(mTransaction, splitContainer, mActivity, + null /* secondaryActivity */, null /* secondaryIntent */)); + + assertEquals(RESULT_NOT_EXPANDED, mPresenter.expandSplitContainerIfNeeded(mTransaction, + splitContainer, mActivity, secondaryActivity, null /* secondaryIntent */)); + verify(mPresenter, never()).expandTaskFragment(any(), any()); + + 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)); + + 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())); + + 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())); + } + + private Activity createMockActivity() { + final Activity activity = mock(Activity.class); + final Configuration activityConfig = new Configuration(); + activityConfig.windowConfiguration.setBounds(TASK_BOUNDS); + activityConfig.windowConfiguration.setMaxBounds(TASK_BOUNDS); + doReturn(mActivityResources).when(activity).getResources(); + doReturn(activityConfig).when(mActivityResources).getConfiguration(); + doReturn(new ActivityInfo()).when(activity).getActivityInfo(); + doReturn(mock(IBinder.class)).when(activity).getActivityToken(); + return activity; + } +} 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 new file mode 100644 index 000000000000..dd67e48ef353 --- /dev/null +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java @@ -0,0 +1,189 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.window.extensions.embedding; + +import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; +import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; +import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; +import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; + +import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_BOUNDS; +import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_ID; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; + +import android.app.Activity; +import android.content.Intent; +import android.graphics.Rect; +import android.platform.test.annotations.Presubmit; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** + * Test class for {@link TaskContainer}. + * + * Build/Install/Run: + * atest WMJetpackUnitTests:TaskContainerTest + */ +@Presubmit +@SmallTest +@RunWith(AndroidJUnit4.class) +public class TaskContainerTest { + @Mock + private SplitController mController; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + } + + @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 Rect splitBounds = new Rect(0, 0, 500, 1000); + + assertEquals(WINDOWING_MODE_MULTI_WINDOW, + taskContainer.getWindowingModeForSplitTaskFragment(splitBounds)); + + taskContainer.setWindowingMode(WINDOWING_MODE_FULLSCREEN); + + assertEquals(WINDOWING_MODE_MULTI_WINDOW, + taskContainer.getWindowingModeForSplitTaskFragment(splitBounds)); + + taskContainer.setWindowingMode(WINDOWING_MODE_FREEFORM); + + assertEquals(WINDOWING_MODE_FREEFORM, + taskContainer.getWindowingModeForSplitTaskFragment(splitBounds)); + + // Empty bounds means the split pair are stacked, so it should be UNDEFINED which will then + // inherit the Task windowing mode + assertEquals(WINDOWING_MODE_UNDEFINED, + taskContainer.getWindowingModeForSplitTaskFragment(new Rect())); + } + + @Test + public void testIsInPictureInPicture() { + final TaskContainer taskContainer = new TaskContainer(TASK_ID); + + assertFalse(taskContainer.isInPictureInPicture()); + + taskContainer.setWindowingMode(WINDOWING_MODE_FULLSCREEN); + + assertFalse(taskContainer.isInPictureInPicture()); + + taskContainer.setWindowingMode(WINDOWING_MODE_PINNED); + + assertTrue(taskContainer.isInPictureInPicture()); + } + + @Test + public void testIsEmpty() { + final TaskContainer taskContainer = new TaskContainer(TASK_ID); + + assertTrue(taskContainer.isEmpty()); + + final TaskFragmentContainer tf = new TaskFragmentContainer(null /* activity */, + new Intent(), taskContainer, mController); + + assertFalse(taskContainer.isEmpty()); + + taskContainer.mFinishedContainer.add(tf.getTaskFragmentToken()); + taskContainer.mContainers.clear(); + + assertFalse(taskContainer.isEmpty()); + } + + @Test + public void testGetTopTaskFragmentContainer() { + final TaskContainer taskContainer = new TaskContainer(TASK_ID); + assertNull(taskContainer.getTopTaskFragmentContainer()); + + final TaskFragmentContainer tf0 = new TaskFragmentContainer(null /* activity */, + new Intent(), taskContainer, mController); + assertEquals(tf0, taskContainer.getTopTaskFragmentContainer()); + + final TaskFragmentContainer tf1 = new TaskFragmentContainer(null /* activity */, + new Intent(), taskContainer, mController); + assertEquals(tf1, taskContainer.getTopTaskFragmentContainer()); + } + + @Test + public void testGetTopNonFinishingActivity() { + final TaskContainer taskContainer = new TaskContainer(TASK_ID); + assertNull(taskContainer.getTopNonFinishingActivity()); + + final TaskFragmentContainer tf0 = mock(TaskFragmentContainer.class); + taskContainer.mContainers.add(tf0); + final Activity activity0 = mock(Activity.class); + doReturn(activity0).when(tf0).getTopNonFinishingActivity(); + assertEquals(activity0, taskContainer.getTopNonFinishingActivity()); + + final TaskFragmentContainer tf1 = mock(TaskFragmentContainer.class); + taskContainer.mContainers.add(tf1); + assertEquals(activity0, taskContainer.getTopNonFinishingActivity()); + + final Activity activity1 = mock(Activity.class); + doReturn(activity1).when(tf1).getTopNonFinishingActivity(); + assertEquals(activity1, taskContainer.getTopNonFinishingActivity()); + } +} 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 new file mode 100644 index 000000000000..d31342bfb309 --- /dev/null +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentAnimationControllerTest.java @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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 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; +import android.window.TaskFragmentOrganizer; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** + * Test class for {@link TaskFragmentAnimationController}. + * + * Build/Install/Run: + * atest WMJetpackUnitTests:TaskFragmentAnimationControllerTest + */ +@Presubmit +@SmallTest +@RunWith(AndroidJUnit4.class) +public class TaskFragmentAnimationControllerTest { + @Mock + private TaskFragmentOrganizer mOrganizer; + private TaskFragmentAnimationController mAnimationController; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + mAnimationController = new TaskFragmentAnimationController(mOrganizer); + } + + @Test + public void testRegisterRemoteAnimations() { + mAnimationController.registerRemoteAnimations(TASK_ID); + + verify(mOrganizer).registerRemoteAnimations(TASK_ID, mAnimationController.mDefinition); + + mAnimationController.registerRemoteAnimations(TASK_ID); + + // No extra call if it has been registered. + verify(mOrganizer).registerRemoteAnimations(TASK_ID, mAnimationController.mDefinition); + } + + @Test + public void testUnregisterRemoteAnimations() { + mAnimationController.unregisterRemoteAnimations(TASK_ID); + + // No call if it is not registered. + verify(mOrganizer, never()).unregisterRemoteAnimations(anyInt()); + + mAnimationController.registerRemoteAnimations(TASK_ID); + mAnimationController.unregisterRemoteAnimations(TASK_ID); + + verify(mOrganizer).unregisterRemoteAnimations(TASK_ID); + + mAnimationController.unregisterRemoteAnimations(TASK_ID); + + // 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); + } +} 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 new file mode 100644 index 000000000000..28c2773e25cb --- /dev/null +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java @@ -0,0 +1,310 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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 androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_ID; +import static androidx.window.extensions.embedding.EmbeddingTestUtils.createMockTaskFragmentInfo; + +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.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; + +import 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; +import android.window.WindowContainerTransaction; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import com.google.android.collect.Lists; + +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; + +/** + * Test class for {@link TaskFragmentContainer}. + * + * Build/Install/Run: + * atest WMJetpackUnitTests:TaskFragmentContainerTest + */ +@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 Activity mActivity; + private Intent mIntent; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + doReturn(mHandler).when(mController).getHandler(); + mActivity = createMockActivity(); + mIntent = new Intent(); + } + + @Test + public void testNewContainer() { + final TaskContainer taskContainer = new TaskContainer(TASK_ID); + + // One of the activity and the intent must be non-null + assertThrows(IllegalArgumentException.class, + () -> new TaskFragmentContainer(null, null, taskContainer, mController)); + + // One of the activity and the intent must be null. + assertThrows(IllegalArgumentException.class, + () -> new TaskFragmentContainer(mActivity, mIntent, taskContainer, mController)); + } + + @Test + public void testFinish() { + final TaskContainer taskContainer = new TaskContainer(TASK_ID); + final TaskFragmentContainer container = new TaskFragmentContainer(mActivity, + null /* pendingAppearedIntent */, taskContainer, mController); + doReturn(container).when(mController).getContainerWithActivity(mActivity); + final WindowContainerTransaction wct = new WindowContainerTransaction(); + + // Only remove the activity, but not clear the reference until appeared. + container.finish(true /* shouldFinishDependent */, mPresenter, wct, mController); + + verify(mActivity).finish(); + 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); + + verify(mActivity, never()).finish(); + 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); + + verify(mActivity, never()).finish(); + verify(mPresenter).deleteTaskFragment(wct, container.getTaskFragmentToken()); + verify(mController).removeContainer(container); + } + + @Test + public void testFinish_notFinishActivityThatIsReparenting() { + final TaskContainer taskContainer = new TaskContainer(TASK_ID); + final TaskFragmentContainer container0 = new TaskFragmentContainer(mActivity, + null /* pendingAppearedIntent */, taskContainer, mController); + final TaskFragmentInfo info = createMockTaskFragmentInfo(container0, mActivity); + container0.setInfo(info); + // Request to reparent the activity to a new TaskFragment. + final TaskFragmentContainer container1 = new TaskFragmentContainer(mActivity, + null /* pendingAppearedIntent */, taskContainer, mController); + 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); + + verify(mActivity, never()).finish(); + verify(mPresenter).deleteTaskFragment(wct, container0.getTaskFragmentToken()); + verify(mController).removeContainer(container0); + } + + @Test + public void testSetInfo() { + final TaskContainer taskContainer = new TaskContainer(TASK_ID); + // Pending activity should be cleared when it has appeared on server side. + final TaskFragmentContainer pendingActivityContainer = new TaskFragmentContainer(mActivity, + null /* pendingAppearedIntent */, taskContainer, mController); + + assertTrue(pendingActivityContainer.mPendingAppearedActivities.contains(mActivity)); + + final TaskFragmentInfo info0 = createMockTaskFragmentInfo(pendingActivityContainer, + mActivity); + pendingActivityContainer.setInfo(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); + + assertEquals(mIntent, pendingIntentContainer.getPendingAppearedIntent()); + + final TaskFragmentInfo info1 = createMockTaskFragmentInfo(pendingIntentContainer, + mActivity); + pendingIntentContainer.setInfo(info1); + + assertNull(pendingIntentContainer.getPendingAppearedIntent()); + } + + @Test + public void testIsWaitingActivityAppear() { + final TaskContainer taskContainer = new TaskContainer(TASK_ID); + final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */, + mIntent, taskContainer, mController); + + assertTrue(container.isWaitingActivityAppear()); + + final TaskFragmentInfo info = mock(TaskFragmentInfo.class); + doReturn(new ArrayList<>()).when(info).getActivities(); + doReturn(true).when(info).isEmpty(); + container.setInfo(info); + + assertTrue(container.isWaitingActivityAppear()); + + doReturn(false).when(info).isEmpty(); + container.setInfo(info); + + assertFalse(container.isWaitingActivityAppear()); + } + + @Test + public void testAppearEmptyTimeout() { + final TaskContainer taskContainer = new TaskContainer(TASK_ID); + final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */, + mIntent, taskContainer, mController); + + assertNull(container.mAppearEmptyTimeout); + + // Not set if it is not appeared empty. + final TaskFragmentInfo info = mock(TaskFragmentInfo.class); + doReturn(new ArrayList<>()).when(info).getActivities(); + doReturn(false).when(info).isEmpty(); + container.setInfo(info); + + assertNull(container.mAppearEmptyTimeout); + + // Set timeout if the first info set is empty. + container.mInfo = null; + doReturn(true).when(info).isEmpty(); + container.setInfo(info); + + assertNotNull(container.mAppearEmptyTimeout); + + // Remove timeout after the container becomes non-empty. + doReturn(false).when(info).isEmpty(); + container.setInfo(info); + + assertNull(container.mAppearEmptyTimeout); + + // Running the timeout will call into SplitController.onTaskFragmentAppearEmptyTimeout. + container.mInfo = null; + doReturn(true).when(info).isEmpty(); + container.setInfo(info); + container.mAppearEmptyTimeout.run(); + + assertNull(container.mAppearEmptyTimeout); + verify(mController).onTaskFragmentAppearEmptyTimeout(container); + } + + @Test + public void testCollectNonFinishingActivities() { + final TaskContainer taskContainer = new TaskContainer(TASK_ID); + final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */, + mIntent, taskContainer, mController); + List<Activity> activities = container.collectNonFinishingActivities(); + + assertTrue(activities.isEmpty()); + + container.addPendingAppearedActivity(mActivity); + activities = container.collectNonFinishingActivities(); + + assertEquals(1, activities.size()); + + final Activity activity0 = createMockActivity(); + final Activity activity1 = createMockActivity(); + final List<IBinder> runningActivities = Lists.newArrayList(activity0.getActivityToken(), + activity1.getActivityToken()); + doReturn(runningActivities).when(mInfo).getActivities(); + container.setInfo(mInfo); + activities = container.collectNonFinishingActivities(); + + assertEquals(3, activities.size()); + assertEquals(activity0, activities.get(0)); + assertEquals(activity1, activities.get(1)); + assertEquals(mActivity, activities.get(2)); + } + + @Test + public void testAddPendingActivity() { + final TaskContainer taskContainer = new TaskContainer(TASK_ID); + final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */, + mIntent, taskContainer, mController); + container.addPendingAppearedActivity(mActivity); + + assertEquals(1, container.collectNonFinishingActivities().size()); + + container.addPendingAppearedActivity(mActivity); + + assertEquals(1, container.collectNonFinishingActivities().size()); + } + + @Test + public void testGetBottomMostActivity() { + final TaskContainer taskContainer = new TaskContainer(TASK_ID); + final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */, + mIntent, taskContainer, mController); + container.addPendingAppearedActivity(mActivity); + + assertEquals(mActivity, container.getBottomMostActivity()); + + final Activity activity = createMockActivity(); + final List<IBinder> runningActivities = Lists.newArrayList(activity.getActivityToken()); + doReturn(runningActivities).when(mInfo).getActivities(); + container.setInfo(mInfo); + + assertEquals(activity, container.getBottomMostActivity()); + } + + /** Creates a mock activity in the organizer process. */ + private Activity createMockActivity() { + final Activity activity = mock(Activity.class); + final IBinder activityToken = new Binder(); + doReturn(activityToken).when(activity).getActivityToken(); + doReturn(activity).when(mController).getActivity(activityToken); + return activity; + } +} diff --git a/libs/WindowManager/OWNERS b/libs/WindowManager/OWNERS index 2c61df96eb03..780e4c1632f7 100644 --- a/libs/WindowManager/OWNERS +++ b/libs/WindowManager/OWNERS @@ -1,3 +1,6 @@ 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 cdff5858c77d..7960dec5080b 100644 --- a/libs/WindowManager/Shell/Android.bp +++ b/libs/WindowManager/Shell/Android.bp @@ -43,7 +43,7 @@ filegroup { name: "wm_shell_util-sources", srcs: [ "src/com/android/wm/shell/util/**/*.java", - "src/com/android/wm/shell/common/split/SplitScreenConstants.java" + "src/com/android/wm/shell/common/split/SplitScreenConstants.java", ], path: "src", } @@ -74,13 +74,13 @@ genrule { ], tools: ["protologtool"], cmd: "$(location protologtool) transform-protolog-calls " + - "--protolog-class com.android.internal.protolog.common.ProtoLog " + - "--protolog-impl-class com.android.wm.shell.protolog.ShellProtoLogImpl " + - "--protolog-cache-class com.android.wm.shell.protolog.ShellProtoLogCache " + - "--loggroups-class com.android.wm.shell.protolog.ShellProtoLogGroup " + - "--loggroups-jar $(location :wm_shell_protolog-groups) " + - "--output-srcjar $(out) " + - "$(locations :wm_shell-sources)", + "--protolog-class com.android.internal.protolog.common.ProtoLog " + + "--protolog-impl-class com.android.wm.shell.protolog.ShellProtoLogImpl " + + "--protolog-cache-class com.android.wm.shell.protolog.ShellProtoLogCache " + + "--loggroups-class com.android.wm.shell.protolog.ShellProtoLogGroup " + + "--loggroups-jar $(location :wm_shell_protolog-groups) " + + "--output-srcjar $(out) " + + "$(locations :wm_shell-sources)", out: ["wm_shell_protolog.srcjar"], } @@ -92,13 +92,14 @@ genrule { ], tools: ["protologtool"], cmd: "$(location protologtool) generate-viewer-config " + - "--protolog-class com.android.internal.protolog.common.ProtoLog " + - "--loggroups-class com.android.wm.shell.protolog.ShellProtoLogGroup " + - "--loggroups-jar $(location :wm_shell_protolog-groups) " + - "--viewer-conf $(out) " + - "$(locations :wm_shell-sources)", + "--protolog-class com.android.internal.protolog.common.ProtoLog " + + "--loggroups-class com.android.wm.shell.protolog.ShellProtoLogGroup " + + "--loggroups-jar $(location :wm_shell_protolog-groups) " + + "--viewer-conf $(out) " + + "$(locations :wm_shell-sources)", out: ["wm_shell_protolog.json"], } + // End ProtoLog java_library { @@ -123,11 +124,12 @@ android_library { "res", ], java_resources: [ - ":generate-wm_shell_protolog.json" + ":generate-wm_shell_protolog.json", ], static_libs: [ "androidx.appcompat_appcompat", "androidx.arch.core_core-runtime", + "androidx-constraintlayout_constraintlayout", "androidx.dynamicanimation_dynamicanimation", "androidx.recyclerview_recyclerview", "kotlinx-coroutines-android", @@ -138,6 +140,11 @@ android_library { "dagger2", "jsr330", ], + libs: [ + // Soong fails to automatically add this dependency because all the + // *.kt sources are inside a filegroup. + "kotlin-annotations", + ], kotlincflags: ["-Xjvm-default=enable"], manifest: "AndroidManifest.xml", plugins: ["dagger2-compiler"], diff --git a/libs/WindowManager/Shell/res/anim/tv_pip_controls_focus_gain_animation.xml b/libs/WindowManager/Shell/res/anim/tv_pip_controls_focus_gain_animation.xml deleted file mode 100644 index 29d9b257cc59..000000000000 --- a/libs/WindowManager/Shell/res/anim/tv_pip_controls_focus_gain_animation.xml +++ /dev/null @@ -1,20 +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. ---> -<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android" - android:propertyName="alpha" - android:valueTo="1" - android:interpolator="@android:interpolator/fast_out_slow_in" - android:duration="100" /> diff --git a/libs/WindowManager/Shell/res/anim/tv_pip_controls_focus_loss_animation.xml b/libs/WindowManager/Shell/res/anim/tv_pip_controls_focus_loss_animation.xml deleted file mode 100644 index 70f553b89657..000000000000 --- a/libs/WindowManager/Shell/res/anim/tv_pip_controls_focus_loss_animation.xml +++ /dev/null @@ -1,20 +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. ---> -<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android" - android:propertyName="alpha" - android:valueTo="0" - android:interpolator="@android:interpolator/fast_out_slow_in" - android:duration="100" /> diff --git a/libs/WindowManager/Shell/res/anim/tv_pip_menu_fade_out_animation.xml b/libs/WindowManager/Shell/res/anim/tv_pip_menu_fade_out_animation.xml deleted file mode 100644 index 70f553b89657..000000000000 --- a/libs/WindowManager/Shell/res/anim/tv_pip_menu_fade_out_animation.xml +++ /dev/null @@ -1,20 +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. ---> -<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android" - android:propertyName="alpha" - android:valueTo="0" - android:interpolator="@android:interpolator/fast_out_slow_in" - android:duration="100" /> diff --git a/libs/WindowManager/Shell/res/animator/tv_pip_menu_action_button_animator.xml b/libs/WindowManager/Shell/res/animator/tv_pip_menu_action_button_animator.xml new file mode 100644 index 000000000000..7475abac4695 --- /dev/null +++ b/libs/WindowManager/Shell/res/animator/tv_pip_menu_action_button_animator.xml @@ -0,0 +1,53 @@ +<?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"> + + <item android:state_focused="true"> + <set> + <objectAnimator + android:duration="200" + android:propertyName="scaleX" + android:valueFrom="1.0" + android:valueTo="1.1" + android:valueType="floatType"/> + <objectAnimator + android:duration="200" + android:propertyName="scaleY" + android:valueFrom="1.0" + android:valueTo="1.1" + android:valueType="floatType"/> + </set> + </item> + <item android:state_focused="false"> + <set> + <objectAnimator + android:duration="200" + android:propertyName="scaleX" + android:valueFrom="1.1" + android:valueTo="1.0" + android:valueType="floatType"/> + <objectAnimator + android:duration="200" + android:propertyName="scaleY" + android:valueFrom="1.1" + android:valueTo="1.0" + android:valueType="floatType"/> + </set> + </item> +</selector>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/color/size_compat_background_ripple.xml b/libs/WindowManager/Shell/res/color/compat_background_ripple.xml index 329e5b9b31a0..329e5b9b31a0 100644 --- a/libs/WindowManager/Shell/res/color/size_compat_background_ripple.xml +++ b/libs/WindowManager/Shell/res/color/compat_background_ripple.xml diff --git a/libs/WindowManager/Shell/res/color/letterbox_education_dismiss_button_background_ripple.xml b/libs/WindowManager/Shell/res/color/letterbox_education_dismiss_button_background_ripple.xml new file mode 100644 index 000000000000..43cba1a37bc8 --- /dev/null +++ b/libs/WindowManager/Shell/res/color/letterbox_education_dismiss_button_background_ripple.xml @@ -0,0 +1,19 @@ +<?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"> + <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/split_divider_background.xml b/libs/WindowManager/Shell/res/color/split_divider_background.xml index 329e5b9b31a0..049980803ee3 100644 --- a/libs/WindowManager/Shell/res/color/split_divider_background.xml +++ b/libs/WindowManager/Shell/res/color/split_divider_background.xml @@ -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="35" /> + <item android:color="@android:color/system_neutral1_500" android:lStar="15" /> </selector>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/color/tv_pip_menu_close_icon.xml b/libs/WindowManager/Shell/res/color/tv_pip_menu_close_icon.xml new file mode 100644 index 000000000000..ce8640df0093 --- /dev/null +++ b/libs/WindowManager/Shell/res/color/tv_pip_menu_close_icon.xml @@ -0,0 +1,19 @@ +<?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"> + <item android:color="@color/tv_pip_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_pip_menu_close_icon_bg.xml new file mode 100644 index 000000000000..6cbf66f00df7 --- /dev/null +++ b/libs/WindowManager/Shell/res/color/tv_pip_menu_close_icon_bg.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2022 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<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" /> +</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_pip_menu_icon.xml new file mode 100644 index 000000000000..275870450493 --- /dev/null +++ b/libs/WindowManager/Shell/res/color/tv_pip_menu_icon.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2021 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:state_focused="true" + android:color="@color/tv_pip_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" /> +</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_pip_menu_icon_bg.xml new file mode 100644 index 000000000000..4f5e63dac5c0 --- /dev/null +++ b/libs/WindowManager/Shell/res/color/tv_pip_menu_icon_bg.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2021 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:state_focused="true" + android:color="@color/tv_pip_menu_icon_bg_focused" /> + <item android:color="@color/tv_pip_menu_icon_bg_unfocused" /> +</selector>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/drawable/camera_compat_dismiss_button.xml b/libs/WindowManager/Shell/res/drawable/camera_compat_dismiss_button.xml new file mode 100644 index 000000000000..1c8cb914af81 --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/camera_compat_dismiss_button.xml @@ -0,0 +1,33 @@ +<!-- + 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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="48dp" + android:height="43dp" + android:viewportWidth="48" + android:viewportHeight="43"> + <group> + <clip-path + android:pathData="M48,43l-48,-0l-0,-43l48,-0z"/> + <path + android:pathData="M24,43C37.2548,43 48,32.2548 48,19L48,0L0,-0L0,19C0,32.2548 10.7452,43 24,43Z" + android:fillColor="@color/compat_controls_background" + android:strokeAlpha="0.8" + android:fillAlpha="0.8"/> + <path + android:pathData="M31,12.41L29.59,11L24,16.59L18.41,11L17,12.41L22.59,18L17,23.59L18.41,25L24,19.41L29.59,25L31,23.59L25.41,18L31,12.41Z" + android:fillColor="@color/compat_controls_text"/> + </group> +</vector> diff --git a/libs/WindowManager/Shell/res/drawable/camera_compat_dismiss_ripple.xml b/libs/WindowManager/Shell/res/drawable/camera_compat_dismiss_ripple.xml new file mode 100644 index 000000000000..c81013966c35 --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/camera_compat_dismiss_ripple.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2021 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<ripple xmlns:android="http://schemas.android.com/apk/res/android" + android:color="@color/compat_background_ripple"> + <item android:drawable="@drawable/camera_compat_dismiss_button"/> +</ripple>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/drawable/camera_compat_treatment_applied_button.xml b/libs/WindowManager/Shell/res/drawable/camera_compat_treatment_applied_button.xml new file mode 100644 index 000000000000..c796b5967f98 --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/camera_compat_treatment_applied_button.xml @@ -0,0 +1,32 @@ +<!-- + 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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="48dp" + android:height="43dp" + android:viewportWidth="48" + android:viewportHeight="43"> + <path + android:pathData="M24,0C10.7452,0 0,10.7452 0,24V43H48V24C48,10.7452 37.2548,0 24,0Z" + android:fillColor="@color/compat_controls_background" + android:strokeAlpha="0.8" + android:fillAlpha="0.8"/> + <path + android:pathData="M32,17H28.83L27,15H21L19.17,17H16C14.9,17 14,17.9 14,19V31C14,32.1 14.9,33 16,33H32C33.1,33 34,32.1 34,31V19C34,17.9 33.1,17 32,17ZM32,31H16V19H32V31Z" + android:fillColor="@color/compat_controls_text"/> + <path + android:pathData="M24.6618,22C23.0436,22 21.578,22.6187 20.4483,23.625L18.25,21.375V27H23.7458L21.5353,24.7375C22.3841,24.0125 23.4649,23.5625 24.6618,23.5625C26.8235,23.5625 28.6616,25.0062 29.3028,27L30.75,26.5125C29.9012,23.8938 27.5013,22 24.6618,22Z" + android:fillColor="@color/compat_controls_text"/> +</vector> diff --git a/libs/WindowManager/Shell/res/drawable/camera_compat_treatment_applied_ripple.xml b/libs/WindowManager/Shell/res/drawable/camera_compat_treatment_applied_ripple.xml new file mode 100644 index 000000000000..3e9fe6dc3b99 --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/camera_compat_treatment_applied_ripple.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2021 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<ripple xmlns:android="http://schemas.android.com/apk/res/android" + android:color="@color/compat_background_ripple"> + <item android:drawable="@drawable/camera_compat_treatment_applied_button"/> +</ripple> diff --git a/libs/WindowManager/Shell/res/drawable/camera_compat_treatment_suggested_button.xml b/libs/WindowManager/Shell/res/drawable/camera_compat_treatment_suggested_button.xml new file mode 100644 index 000000000000..af505d1cb73c --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/camera_compat_treatment_suggested_button.xml @@ -0,0 +1,53 @@ +<!-- + 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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="48dp" + android:height="43dp" + android:viewportWidth="48" + android:viewportHeight="43"> + <path + android:pathData="M24,0C10.7452,0 0,10.7452 0,24V43H48V24C48,10.7452 37.2548,0 24,0Z" + android:fillColor="@color/compat_controls_background" + android:strokeAlpha="0.8" + android:fillAlpha="0.8"/> + <path + android:pathData="M32,17H28.83L27,15H21L19.17,17H16C14.9,17 14,17.9 14,19V31C14,32.1 14.9,33 16,33H32C33.1,33 34,32.1 34,31V19C34,17.9 33.1,17 32,17ZM32,31H16V19H32V31Z" + android:fillColor="@color/compat_controls_text"/> + <path + android:pathData="M18,29L18,25.5L19.5,25.5L19.5,29L18,29Z" + android:fillColor="@color/compat_controls_text"/> + <path + android:pathData="M30,29L30,25.5L28.5,25.5L28.5,29L30,29Z" + android:fillColor="@color/compat_controls_text"/> + <path + android:pathData="M30,21L30,24.5L28.5,24.5L28.5,21L30,21Z" + android:fillColor="@color/compat_controls_text"/> + <path + android:pathData="M18,21L18,24.5L19.5,24.5L19.5,21L18,21Z" + android:fillColor="@color/compat_controls_text"/> + <path + android:pathData="M18,27.5L21.5,27.5L21.5,29L18,29L18,27.5Z" + android:fillColor="@color/compat_controls_text"/> + <path + android:pathData="M30,27.5L26.5,27.5L26.5,29L30,29L30,27.5Z" + android:fillColor="@color/compat_controls_text"/> + <path + android:pathData="M30,22.5L26.5,22.5L26.5,21L30,21L30,22.5Z" + android:fillColor="@color/compat_controls_text"/> + <path + android:pathData="M18,22.5L21.5,22.5L21.5,21L18,21L18,22.5Z" + android:fillColor="@color/compat_controls_text"/> +</vector> diff --git a/libs/WindowManager/Shell/res/drawable/camera_compat_treatment_suggested_ripple.xml b/libs/WindowManager/Shell/res/drawable/camera_compat_treatment_suggested_ripple.xml new file mode 100644 index 000000000000..c0f1c89b0cbb --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/camera_compat_treatment_suggested_ripple.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2021 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<ripple xmlns:android="http://schemas.android.com/apk/res/android" + android:color="@color/compat_background_ripple"> + <item android:drawable="@drawable/camera_compat_treatment_suggested_button"/> +</ripple> diff --git a/libs/WindowManager/Shell/res/drawable/home_icon.xml b/libs/WindowManager/Shell/res/drawable/home_icon.xml new file mode 100644 index 000000000000..1669d0167e4b --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/home_icon.xml @@ -0,0 +1,45 @@ +<?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. +--> +<layer-list xmlns:android="http://schemas.android.com/apk/res/android" + android:gravity="center"> + <item android:gravity="center"> + <shape + android:shape="oval"> + <stroke + android:color="@color/tv_pip_edu_text_home_icon" + android:width="1sp" /> + <solid android:color="@android:color/transparent" /> + <size + android:width="@dimen/pip_menu_edu_text_home_icon_outline" + android:height="@dimen/pip_menu_edu_text_home_icon_outline"/> + </shape> + </item> + <item + android:width="@dimen/pip_menu_edu_text_home_icon" + android:height="@dimen/pip_menu_edu_text_home_icon" + android:gravity="center"> + <vector + android:width="24sp" + android:height="24sp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:fillColor="@color/tv_pip_edu_text_home_icon" + android:pathData="M12,3L4,9v12h5v-7h6v7h5V9z"/> + </vector> + </item> +</layer-list> diff --git a/libs/WindowManager/Shell/res/drawable/letterbox_education_dialog_background.xml b/libs/WindowManager/Shell/res/drawable/letterbox_education_dialog_background.xml new file mode 100644 index 000000000000..3e1a2bce2393 --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/letterbox_education_dialog_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 xmlns:android="http://schemas.android.com/apk/res/android" + android:shape="rectangle"> + <solid android:color="@color/compat_controls_background"/> + <corners android:radius="@dimen/letterbox_education_dialog_corner_radius"/> +</shape>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/drawable/letterbox_education_dismiss_button_background.xml b/libs/WindowManager/Shell/res/drawable/letterbox_education_dismiss_button_background.xml new file mode 100644 index 000000000000..0d8811357c05 --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/letterbox_education_dismiss_button_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 xmlns:android="http://schemas.android.com/apk/res/android" + android:shape="rectangle"> + <solid android:color="@color/letterbox_education_accent_primary"/> + <corners android:radius="12dp"/> +</shape>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/drawable/letterbox_education_dismiss_button_background_ripple.xml b/libs/WindowManager/Shell/res/drawable/letterbox_education_dismiss_button_background_ripple.xml new file mode 100644 index 000000000000..42572d64b96f --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/letterbox_education_dismiss_button_background_ripple.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2022 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<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 diff --git a/libs/WindowManager/Shell/res/drawable/letterbox_education_ic_letterboxed_app.xml b/libs/WindowManager/Shell/res/drawable/letterbox_education_ic_letterboxed_app.xml new file mode 100644 index 000000000000..6fcd1de892a3 --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/letterbox_education_ic_letterboxed_app.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. + --> +<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_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" /> +</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 new file mode 100644 index 000000000000..cbfcfd06e3b7 --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/letterbox_education_ic_reposition.xml @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2022 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="@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="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="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" /> +</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 new file mode 100644 index 000000000000..469eb1e14849 --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/letterbox_education_ic_screen_rotation.xml @@ -0,0 +1,26 @@ +<?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 new file mode 100644 index 000000000000..dcb8aed05c9c --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/letterbox_education_ic_split_screen.xml @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2022 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="@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="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" /> +</vector>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/anim/tv_pip_menu_fade_in_animation.xml b/libs/WindowManager/Shell/res/drawable/pip_custom_close_bg.xml index 29d9b257cc59..39c3fe6f6106 100644 --- a/libs/WindowManager/Shell/res/anim/tv_pip_menu_fade_in_animation.xml +++ b/libs/WindowManager/Shell/res/drawable/pip_custom_close_bg.xml @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="utf-8"?> -<!-- 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. @@ -13,8 +13,12 @@ See the License for the specific language governing permissions and limitations under the License. --> -<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android" - android:propertyName="alpha" - android:valueTo="1" - android:interpolator="@android:interpolator/fast_out_slow_in" - android:duration="100" /> +<shape + xmlns:android="http://schemas.android.com/apk/res/android" + android:shape="oval"> + <solid + android:color="@color/pip_custom_close_bg" /> + <size + android:width="@dimen/pip_custom_close_bg_size" + android:height="@dimen/pip_custom_close_bg_size" /> +</shape> diff --git a/libs/WindowManager/Shell/res/drawable/pip_ic_collapse.xml b/libs/WindowManager/Shell/res/drawable/pip_ic_collapse.xml new file mode 100644 index 000000000000..63e2a4035cbf --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/pip_ic_collapse.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"> + <path + android:fillColor="@color/tv_pip_menu_focus_border" + android:pathData="M12,12V4h2v4.6L20.6,2 22,3.4 15.4,10H20v2zM3.4,22L2,20.6 8.6,14H4v-2h8v8h-2v-4.6z"/> +</vector> diff --git a/libs/WindowManager/Shell/res/drawable/pip_ic_expand.xml b/libs/WindowManager/Shell/res/drawable/pip_ic_expand.xml new file mode 100644 index 000000000000..758b92c4f4da --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/pip_ic_expand.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"> + <path + android:fillColor="@color/tv_pip_menu_focus_border" + android:pathData="M3,21v-8h2v4.6L17.6,5H13V3h8v8h-2V6.4L6.4,19H11v2z"/> +</vector> diff --git a/libs/WindowManager/Shell/res/drawable/pip_ic_move_down.xml b/libs/WindowManager/Shell/res/drawable/pip_ic_move_down.xml new file mode 100644 index 000000000000..d8f356164358 --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/pip_ic_move_down.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2021 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:fillColor="@color/tv_pip_menu_focus_border" + android:pathData="M7,10l5,5 5,-5H7z"/> +</vector>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/drawable/pip_ic_move_left.xml b/libs/WindowManager/Shell/res/drawable/pip_ic_move_left.xml new file mode 100644 index 000000000000..3e0011c65942 --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/pip_ic_move_left.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2021 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:fillColor="@color/tv_pip_menu_focus_border" + android:pathData="M14,7l-5,5 5,5V7z"/> +</vector>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/drawable/pip_ic_move_right.xml b/libs/WindowManager/Shell/res/drawable/pip_ic_move_right.xml new file mode 100644 index 000000000000..f6b3c72e3cb5 --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/pip_ic_move_right.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2021 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:fillColor="@color/tv_pip_menu_focus_border" + android:pathData="M10,17l5,-5 -5,-5v10z"/> +</vector>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/drawable/pip_ic_move_up.xml b/libs/WindowManager/Shell/res/drawable/pip_ic_move_up.xml new file mode 100644 index 000000000000..1a3446249573 --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/pip_ic_move_up.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2021 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:fillColor="@color/tv_pip_menu_focus_border" + android:pathData="M7,14l5,-5 5,5H7z"/> +</vector>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/drawable/pip_ic_move_white.xml b/libs/WindowManager/Shell/res/drawable/pip_ic_move_white.xml new file mode 100644 index 000000000000..37f4c87006ba --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/pip_ic_move_white.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2021 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<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="M11,5.83L11,10h2L13,5.83l1.83,1.83 1.41,-1.42L12,2 7.76,6.24l1.41,1.42zM17.76,7.76l-1.42,1.41L18.17,11L14,11v2h4.17l-1.83,1.83 1.42,1.41L22,12zM13,18.17L13,14h-2v4.17l-1.83,-1.83 -1.41,1.42L12,22l4.24,-4.24 -1.41,-1.42zM10,13v-2L5.83,11l1.83,-1.83 -1.42,-1.41L2,12l4.24,4.24 1.42,-1.41L5.83,13z" + android:fillColor="#FFFFFF" /> + +</vector>
\ No newline at end of file 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 ab74e43472c3..e6ae28207970 100644 --- a/libs/WindowManager/Shell/res/drawable/size_compat_restart_button.xml +++ b/libs/WindowManager/Shell/res/drawable/size_compat_restart_button.xml @@ -21,7 +21,9 @@ android:viewportHeight="48"> <path android:fillColor="@color/compat_controls_background" - android:pathData="M0,24 a24,24 0 1,0 48,0 a24,24 0 1,0 -48,0" /> + android:strokeAlpha="0.8" + android:fillAlpha="0.8" + android:pathData="M0,24 a24,24 0 1,0 48,0 a24,24 0 1,0 -48,0"/> <group android:translateX="12" android:translateY="12"> diff --git a/libs/WindowManager/Shell/res/drawable/size_compat_restart_button_ripple.xml b/libs/WindowManager/Shell/res/drawable/size_compat_restart_button_ripple.xml index 95decff24ac4..6551edf6d0e6 100644 --- a/libs/WindowManager/Shell/res/drawable/size_compat_restart_button_ripple.xml +++ b/libs/WindowManager/Shell/res/drawable/size_compat_restart_button_ripple.xml @@ -15,6 +15,6 @@ ~ limitations under the License. --> <ripple xmlns:android="http://schemas.android.com/apk/res/android" - android:color="@color/size_compat_background_ripple"> + android:color="@color/compat_background_ripple"> <item android:drawable="@drawable/size_compat_restart_button"/> </ripple>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/drawable/tv_pip_button_focused.xml b/libs/WindowManager/Shell/res/drawable/tv_pip_button_bg.xml index cce13035dba7..1938f4562e97 100644 --- a/libs/WindowManager/Shell/res/drawable/tv_pip_button_focused.xml +++ b/libs/WindowManager/Shell/res/drawable/tv_pip_button_bg.xml @@ -14,5 +14,8 @@ 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="#9AFFFFFF" android:radius="17dp" /> +<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_background.xml b/libs/WindowManager/Shell/res/drawable/tv_pip_menu_background.xml new file mode 100644 index 000000000000..0c627921573c --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/tv_pip_menu_background.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. + --> +<shape xmlns:android="http://schemas.android.com/apk/res/android" + android:shape="rectangle"> + <corners android:radius="@dimen/pip_menu_background_corner_radius" /> + <solid android:color="@color/tv_pip_menu_background"/> + <stroke android:width="@dimen/pip_menu_border_width" + android:color="@color/tv_pip_menu_background"/> +</shape> diff --git a/libs/WindowManager/Shell/res/drawable/tv_pip_menu_border.xml b/libs/WindowManager/Shell/res/drawable/tv_pip_menu_border.xml new file mode 100644 index 000000000000..846fdb3e8a58 --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/tv_pip_menu_border.xml @@ -0,0 +1,33 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2021 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<selector xmlns:android="http://schemas.android.com/apk/res/android" + android:exitFadeDuration="@integer/pip_menu_fade_animation_duration"> + <item android:state_activated="true"> + <shape android:shape="rectangle"> + <corners android:radius="@dimen/pip_menu_border_corner_radius" /> + <stroke android:width="@dimen/pip_menu_border_width" + android:color="@color/tv_pip_menu_focus_border" /> + </shape> + </item> + <item> + <shape android:shape="rectangle"> + <corners android:radius="@dimen/pip_menu_border_corner_radius" /> + <stroke android:width="@dimen/pip_menu_border_width" + android:color="@color/tv_pip_menu_background"/> + </shape> + </item> +</selector> diff --git a/libs/WindowManager/Shell/res/layout/background_panel.xml b/libs/WindowManager/Shell/res/layout/background_panel.xml new file mode 100644 index 000000000000..c3569d80fa1e --- /dev/null +++ b/libs/WindowManager/Shell/res/layout/background_panel.xml @@ -0,0 +1,26 @@ +<?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 + --> +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/background_panel_layout" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + android:gravity="center_horizontal | center_vertical" + android:background="@android:color/transparent"> +</LinearLayout>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/layout/badged_image_view.xml b/libs/WindowManager/Shell/res/layout/badged_image_view.xml new file mode 100644 index 000000000000..5f07121ec7d3 --- /dev/null +++ b/libs/WindowManager/Shell/res/layout/badged_image_view.xml @@ -0,0 +1,55 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2021 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<merge xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto"> + + <ImageView + android:id="@+id/icon_view" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:contentDescription="@null" /> + + <!-- + Icon badge size is defined in Launcher3 BaseIconFactory as 0.444 of icon size. + Constraint guide starts from left, which means for a badge positioned on the right, + percent has to be 1 - 0.444 to have the same effect. + --> + <androidx.constraintlayout.widget.Guideline + android:id="@+id/app_icon_constraint_horizontal" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="horizontal" + app:layout_constraintGuide_percent="0.556" /> + + <androidx.constraintlayout.widget.Guideline + android:id="@+id/app_icon_constraint_vertical" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="vertical" + app:layout_constraintGuide_percent="0.556" /> + + <ImageView + android:id="@+id/app_icon_view" + android:layout_width="0dp" + android:layout_height="0dp" + android:contentDescription="@null" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintLeft_toLeftOf="@id/app_icon_constraint_vertical" + app:layout_constraintRight_toRightOf="parent" + app:layout_constraintTop_toTopOf="@id/app_icon_constraint_horizontal" /> + +</merge>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/layout/bubble_overflow_container.xml b/libs/WindowManager/Shell/res/layout/bubble_overflow_container.xml index 76fe3c9bb862..cb516cdbe49b 100644 --- a/libs/WindowManager/Shell/res/layout/bubble_overflow_container.xml +++ b/libs/WindowManager/Shell/res/layout/bubble_overflow_container.xml @@ -19,9 +19,8 @@ android:id="@+id/bubble_overflow_container" android:layout_width="match_parent" android:layout_height="match_parent" - android:paddingTop="@dimen/bubble_overflow_padding" - android:paddingLeft="@dimen/bubble_overflow_padding" - android:paddingRight="@dimen/bubble_overflow_padding" + android:paddingLeft="@dimen/bubble_overflow_container_padding_horizontal" + android:paddingRight="@dimen/bubble_overflow_container_padding_horizontal" android:orientation="vertical" android:layout_gravity="center_horizontal"> diff --git a/libs/WindowManager/Shell/res/layout/bubble_overflow_view.xml b/libs/WindowManager/Shell/res/layout/bubble_overflow_view.xml index 05b15060946d..78de76a5465b 100644 --- a/libs/WindowManager/Shell/res/layout/bubble_overflow_view.xml +++ b/libs/WindowManager/Shell/res/layout/bubble_overflow_view.xml @@ -22,7 +22,6 @@ android:orientation="vertical"> <com.android.wm.shell.bubbles.BadgedImageView - xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/bubble_view" android:layout_gravity="center" android:layout_width="@dimen/bubble_size" @@ -30,16 +29,14 @@ <TextView android:id="@+id/bubble_view_name" - android:textAppearance="@*android:style/TextAppearance.DeviceDefault.ListItem" - android:textSize="13sp" + android:textSize="14sp" android:layout_width="@dimen/bubble_name_width" android:layout_height="wrap_content" - android:maxLines="1" - android:lines="2" + android:lines="1" android:ellipsize="end" android:layout_gravity="center" android:paddingTop="@dimen/bubble_overflow_text_padding" android:paddingEnd="@dimen/bubble_overflow_text_padding" android:paddingStart="@dimen/bubble_overflow_text_padding" - android:gravity="center"/> + android:gravity="center_horizontal|top"/> </LinearLayout> diff --git a/libs/WindowManager/Shell/res/layout/bubble_stack_user_education.xml b/libs/WindowManager/Shell/res/layout/bubble_stack_user_education.xml index 87deb8b5a1fd..5c8c84cbb85b 100644 --- a/libs/WindowManager/Shell/res/layout/bubble_stack_user_education.xml +++ b/libs/WindowManager/Shell/res/layout/bubble_stack_user_education.xml @@ -21,8 +21,8 @@ android:layout_width="wrap_content" android:paddingTop="48dp" android:paddingBottom="48dp" - android:paddingEnd="16dp" - android:layout_marginEnd="24dp" + android:paddingEnd="@dimen/bubble_user_education_padding_end" + android:layout_marginEnd="@dimen/bubble_user_education_margin_end" android:orientation="vertical" android:background="@drawable/bubble_stack_user_education_bg" > diff --git a/libs/WindowManager/Shell/res/layout/bubbles_manage_button_education.xml b/libs/WindowManager/Shell/res/layout/bubbles_manage_button_education.xml index fafe40e924f5..b28f58f8356d 100644 --- a/libs/WindowManager/Shell/res/layout/bubbles_manage_button_education.xml +++ b/libs/WindowManager/Shell/res/layout/bubbles_manage_button_education.xml @@ -23,8 +23,8 @@ android:clickable="true" android:paddingTop="28dp" android:paddingBottom="16dp" - android:paddingEnd="48dp" - android:layout_marginEnd="24dp" + android:paddingEnd="@dimen/bubble_user_education_padding_end" + android:layout_marginEnd="@dimen/bubble_user_education_margin_end" android:orientation="vertical" android:background="@drawable/bubble_stack_user_education_bg" > diff --git a/libs/WindowManager/Shell/res/layout/compat_mode_hint.xml b/libs/WindowManager/Shell/res/layout/compat_mode_hint.xml index bb48bf7b8b2c..44b2f45052ba 100644 --- a/libs/WindowManager/Shell/res/layout/compat_mode_hint.xml +++ b/libs/WindowManager/Shell/res/layout/compat_mode_hint.xml @@ -16,7 +16,7 @@ --> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" - android:layout_width="wrap_content" + android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:clipToPadding="false" @@ -26,7 +26,7 @@ <TextView android:id="@+id/compat_mode_hint_text" - android:layout_width="188dp" + android:layout_width="match_parent" android:layout_height="wrap_content" android:lineSpacingExtra="4sp" android:background="@drawable/compat_hint_bubble" diff --git a/libs/WindowManager/Shell/res/layout/compat_ui_layout.xml b/libs/WindowManager/Shell/res/layout/compat_ui_layout.xml index dc1683475c48..dfaeeeb81c07 100644 --- a/libs/WindowManager/Shell/res/layout/compat_ui_layout.xml +++ b/libs/WindowManager/Shell/res/layout/compat_ui_layout.xml @@ -21,11 +21,47 @@ android:orientation="vertical" android:gravity="bottom|end"> + <include android:id="@+id/camera_compat_hint" + android:visibility="gone" + android:layout_width="@dimen/camera_compat_hint_width" + android:layout_height="wrap_content" + layout="@layout/compat_mode_hint"/> + + <LinearLayout + android:id="@+id/camera_compat_control" + android:visibility="gone" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:clipToPadding="false" + android:layout_marginEnd="@dimen/compat_button_margin" + android:layout_marginBottom="@dimen/compat_button_margin" + android:orientation="vertical"> + + <ImageButton + android:id="@+id/camera_compat_treatment_button" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:background="@android:color/transparent"/> + + <ImageButton + android:id="@+id/camera_compat_dismiss_button" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:src="@drawable/camera_compat_dismiss_ripple" + android:background="@android:color/transparent" + android:contentDescription="@string/camera_compat_dismiss_button_description"/> + + </LinearLayout> + <include android:id="@+id/size_compat_hint" - layout="@layout/compat_mode_hint"/> + android:visibility="gone" + android:layout_width="@dimen/size_compat_hint_width" + android:layout_height="wrap_content" + layout="@layout/compat_mode_hint"/> <ImageButton android:id="@+id/size_compat_restart_button" + android:visibility="gone" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginEnd="@dimen/compat_button_margin" 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 new file mode 100644 index 000000000000..cd1d99ae58b0 --- /dev/null +++ b/libs/WindowManager/Shell/res/layout/letterbox_education_dialog_action_layout.xml @@ -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. + --> + +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="@dimen/letterbox_education_dialog_action_width" + android:layout_height="wrap_content" + android:gravity="center_horizontal" + android:orientation="vertical"> + + <ImageView + android:id="@+id/letterbox_education_dialog_action_icon" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:layout_marginBottom="12dp"/> + + <TextView + android:id="@+id/letterbox_education_dialog_action_text" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:lineSpacingExtra="4sp" + android:textAlignment="center" + android:textColor="@color/compat_controls_text" + android:textSize="14sp"/> + +</LinearLayout>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/layout/letterbox_education_dialog_layout.xml b/libs/WindowManager/Shell/res/layout/letterbox_education_dialog_layout.xml new file mode 100644 index 000000000000..95923763d889 --- /dev/null +++ b/libs/WindowManager/Shell/res/layout/letterbox_education_dialog_layout.xml @@ -0,0 +1,122 @@ +<!-- + ~ Copyright (C) 2022 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT 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.letterboxedu.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"> + + <!-- 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_education_dialog_container" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginHorizontal="@dimen/letterbox_education_dialog_margin" + android:background="@drawable/letterbox_education_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_education_dialog_width" + app:layout_constrainedHeight="true"> + + <!-- 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:layout_width="match_parent" + android:layout_height="wrap_content" + android:gravity="center_horizontal" + android:orientation="vertical" + android:padding="24dp"> + + <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"/> + + <TextView + android:id="@+id/letterbox_education_dialog_title" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:lineSpacingExtra="4sp" + android:text="@string/letterbox_education_dialog_title" + android:textAlignment="center" + android:textColor="@color/compat_controls_text" + android:textSize="24sp"/> + + <TextView + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="8dp" + android:lineSpacingExtra="4sp" + android:text="@string/letterbox_education_dialog_subtext" + android:textAlignment="center" + android:textColor="@color/letterbox_education_text_secondary" + android:textSize="14sp"/> + + <LinearLayout + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:gravity="top" + android:orientation="horizontal" + android:paddingTop="48dp"> + + <com.android.wm.shell.compatui.letterboxedu.LetterboxEduDialogActionLayout + android:layout_width="wrap_content" + android:layout_height="wrap_content" + app:icon="@drawable/letterbox_education_ic_screen_rotation" + app:text="@string/letterbox_education_screen_rotation_text"/> + + <com.android.wm.shell.compatui.letterboxedu.LetterboxEduDialogActionLayout + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart= + "@dimen/letterbox_education_dialog_space_between_actions" + app:icon="@drawable/letterbox_education_ic_reposition" + app:text="@string/letterbox_education_reposition_text"/> + + </LinearLayout> + + <Button + android:id="@+id/letterbox_education_dialog_dismiss_button" + android:layout_width="match_parent" + android:layout_height="56dp" + android:layout_marginTop="48dp" + android:background= + "@drawable/letterbox_education_dismiss_button_background_ripple" + android:text="@string/letterbox_education_got_it" + android:textColor="@android:color/system_neutral1_900" + android:textAlignment="center" + android:contentDescription="@string/letterbox_education_got_it"/> + + </LinearLayout> + + </ScrollView> + + </FrameLayout> + +</com.android.wm.shell.compatui.letterboxedu.LetterboxEduDialogLayout> diff --git a/libs/WindowManager/Shell/res/layout/pip_menu_action.xml b/libs/WindowManager/Shell/res/layout/pip_menu_action.xml index a733b31d9fb0..b51dd6a00815 100644 --- a/libs/WindowManager/Shell/res/layout/pip_menu_action.xml +++ b/libs/WindowManager/Shell/res/layout/pip_menu_action.xml @@ -22,6 +22,14 @@ android:forceHasOverlappingRendering="false"> <ImageView + android:id="@+id/custom_close_bg" + android:layout_width="@dimen/pip_custom_close_bg_size" + android:layout_height="@dimen/pip_custom_close_bg_size" + android:layout_gravity="center" + android:src="@drawable/pip_custom_close_bg" + android:visibility="gone"/> + + <ImageView android:id="@+id/image" android:layout_width="@dimen/pip_action_inner_size" android:layout_height="@dimen/pip_action_inner_size" diff --git a/libs/WindowManager/Shell/res/layout/split_decor.xml b/libs/WindowManager/Shell/res/layout/split_decor.xml index 9ffa5e8aa179..443ecb2ed3f3 100644 --- a/libs/WindowManager/Shell/res/layout/split_decor.xml +++ b/libs/WindowManager/Shell/res/layout/split_decor.xml @@ -20,9 +20,10 @@ android:layout_width="match_parent"> <ImageView android:id="@+id/split_resizing_icon" - android:layout_height="wrap_content" - android:layout_width="wrap_content" + android:layout_height="@dimen/split_icon_size" + android:layout_width="@dimen/split_icon_size" android:layout_gravity="center" + android:scaleType="fitCenter" android:padding="0dp" android:visibility="gone" android:background="@null"/> diff --git a/libs/WindowManager/Shell/res/layout/tv_pip_menu.xml b/libs/WindowManager/Shell/res/layout/tv_pip_menu.xml index 49e2379589a4..7a3ee23d8cdc 100644 --- a/libs/WindowManager/Shell/res/layout/tv_pip_menu.xml +++ b/libs/WindowManager/Shell/res/layout/tv_pip_menu.xml @@ -15,38 +15,172 @@ limitations under the License. --> <!-- Layout for TvPipMenuView --> -<FrameLayout 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:background="#CC000000"> - - <LinearLayout - android:id="@+id/tv_pip_menu_action_buttons" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_gravity="center_horizontal" - android:layout_marginTop="350dp" - android:orientation="horizontal" - android:alpha="0"> - - <com.android.wm.shell.pip.tv.TvPipMenuActionButton - android:id="@+id/tv_pip_menu_fullscreen_button" - android:layout_width="@dimen/picture_in_picture_button_width" - android:layout_height="wrap_content" - android:src="@drawable/pip_ic_fullscreen_white" - android:text="@string/pip_fullscreen" /> +<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"> + + <!-- Matches the PiP app content --> + <View + android:id="@+id/tv_pip" + android:layout_width="0dp" + android:layout_height="0dp" + android:alpha="0" + android:background="@color/tv_pip_menu_background" + android:layout_marginTop="@dimen/pip_menu_outer_space" + android:layout_marginStart="@dimen/pip_menu_outer_space" + android:layout_marginEnd="@dimen/pip_menu_outer_space"/> + + <ScrollView + android:id="@+id/tv_pip_menu_scroll" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_alignTop="@+id/tv_pip" + android:layout_alignStart="@+id/tv_pip" + android:layout_alignEnd="@+id/tv_pip" + android:layout_alignBottom="@+id/tv_pip" + android:scrollbars="none" + android:visibility="gone"/> - <com.android.wm.shell.pip.tv.TvPipMenuActionButton - android:id="@+id/tv_pip_menu_close_button" - android:layout_width="@dimen/picture_in_picture_button_width" + <HorizontalScrollView + android:id="@+id/tv_pip_menu_horizontal_scroll" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_alignTop="@+id/tv_pip" + android:layout_alignStart="@+id/tv_pip" + android:layout_alignEnd="@+id/tv_pip" + android:layout_alignBottom="@+id/tv_pip" + android:scrollbars="none"> + + <LinearLayout + android:id="@+id/tv_pip_menu_action_buttons" + android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginStart="@dimen/picture_in_picture_button_start_margin" - android:src="@drawable/pip_ic_close_white" - android:text="@string/pip_close" /> + 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> + + <View + android:id="@+id/tv_pip_border" + android:layout_width="0dp" + android:layout_height="0dp" + android:layout_marginTop="@dimen/pip_menu_outer_space_frame" + android:layout_marginStart="@dimen/pip_menu_outer_space_frame" + android:layout_marginEnd="@dimen/pip_menu_outer_space_frame" + android:background="@drawable/tv_pip_menu_border"/> + + <FrameLayout + android:id="@+id/tv_pip_menu_edu_text_container" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_below="@+id/tv_pip" + android:layout_alignBottom="@+id/tv_pip_menu_frame" + 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> + + <View + android:id="@+id/tv_pip_menu_frame" + 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_border"/> + + <ImageView + android:id="@+id/tv_pip_menu_arrow_up" + android:layout_width="@dimen/pip_menu_arrow_size" + android:layout_height="@dimen/pip_menu_arrow_size" + android:layout_centerHorizontal="true" + android:layout_alignParentTop="true" + android:alpha="0" + android:elevation="@dimen/pip_menu_arrow_elevation" + android:src="@drawable/pip_ic_move_up" /> - <!-- More TvPipMenuActionButtons may be added here at runtime. --> + <ImageView + android:id="@+id/tv_pip_menu_arrow_right" + android:layout_width="@dimen/pip_menu_arrow_size" + android:layout_height="@dimen/pip_menu_arrow_size" + android:layout_centerVertical="true" + android:layout_alignParentRight="true" + android:alpha="0" + android:elevation="@dimen/pip_menu_arrow_elevation" + android:src="@drawable/pip_ic_move_right" /> - </LinearLayout> + <ImageView + android:id="@+id/tv_pip_menu_arrow_down" + android:layout_width="@dimen/pip_menu_arrow_size" + android:layout_height="@dimen/pip_menu_arrow_size" + android:layout_centerHorizontal="true" + android:layout_alignParentBottom="true" + android:alpha="0" + android:elevation="@dimen/pip_menu_arrow_elevation" + android:src="@drawable/pip_ic_move_down" /> -</FrameLayout> + <ImageView + android:id="@+id/tv_pip_menu_arrow_left" + android:layout_width="@dimen/pip_menu_arrow_size" + android:layout_height="@dimen/pip_menu_arrow_size" + android:layout_centerVertical="true" + android:layout_alignParentLeft="true" + android:alpha="0" + 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 index 5925008e0d08..db96d8de4094 100644 --- a/libs/WindowManager/Shell/res/layout/tv_pip_menu_action_button.xml +++ b/libs/WindowManager/Shell/res/layout/tv_pip_menu_action_button.xml @@ -15,36 +15,26 @@ limitations under the License. --> <!-- Layout for TvPipMenuActionButton --> -<merge xmlns:android="http://schemas.android.com/apk/res/android"> +<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"> - <ImageView android:id="@+id/button" - android:layout_width="34dp" - android:layout_height="34dp" - android:layout_alignParentTop="true" - android:layout_centerHorizontal="true" - android:focusable="true" - android:src="@drawable/tv_pip_button_focused" - android:importantForAccessibility="yes" /> + <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="34dp" - android:layout_height="34dp" - android:layout_alignParentTop="true" - android:layout_centerHorizontal="true" - android:padding="5dp" - android:importantForAccessibility="no" /> - - <TextView android:id="@+id/desc" - android:layout_width="100dp" - android:layout_height="wrap_content" - android:layout_below="@id/icon" - android:layout_centerHorizontal="true" - android:layout_marginTop="3dp" - android:gravity="center" - android:text="@string/pip_fullscreen" - android:alpha="0" - android:fontFamily="sans-serif" - android:textSize="12sp" - android:textColor="#EEEEEE" - android:importantForAccessibility="no" /> -</merge> + <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_additional_action_button.xml b/libs/WindowManager/Shell/res/layout/tv_pip_menu_background.xml index bf4eb2691ff0..5af40200d240 100644 --- a/libs/WindowManager/Shell/res/layout/tv_pip_menu_additional_action_button.xml +++ b/libs/WindowManager/Shell/res/layout/tv_pip_menu_background.xml @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="utf-8"?> <!-- - 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,8 +14,15 @@ See the License for the specific language governing permissions and limitations under the License. --> -<com.android.wm.shell.pip.tv.TvPipMenuActionButton - xmlns:android="http://schemas.android.com/apk/res/android" - android:layout_width="@dimen/picture_in_picture_button_width" - android:layout_height="wrap_content" - android:layout_marginStart="@dimen/picture_in_picture_button_start_margin" /> +<!-- Layout for the back surface of the PiP menu --> +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent"> + <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"/> +</FrameLayout> + diff --git a/libs/WindowManager/Shell/res/values-af/strings.xml b/libs/WindowManager/Shell/res/values-af/strings.xml index 107da8149e5b..2476f65c7e5b 100644 --- a/libs/WindowManager/Shell/res/values-af/strings.xml +++ b/libs/WindowManager/Shell/res/values-af/strings.xml @@ -73,4 +73,12 @@ <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="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> + <string name="letterbox_education_dialog_title" msgid="6688664582871779215">"Sommige programme werk beter in portret"</string> + <string name="letterbox_education_dialog_subtext" msgid="4853542518367719562">"Probeer een van hierdie opsies om jou spasie ten beste te benut"</string> + <string name="letterbox_education_screen_rotation_text" msgid="5085786687366339027">"Draai jou toestel om dit volskerm te maak"</string> + <string name="letterbox_education_reposition_text" msgid="1068293354123934727">"Dubbeltik langs ’n program om dit te herposisioneer"</string> + <string name="letterbox_education_got_it" msgid="4057634570866051177">"Het dit"</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 6ce588034f9e..6187ea46769c 100644 --- a/libs/WindowManager/Shell/res/values-af/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-af/strings_tv.xml @@ -19,6 +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="158770205886688553">"Skuif"</string> + <string name="pip_expand" msgid="1051966011679297308">"Vou uit"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Vou in"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Dubbeldruk "<annotation icon="home_icon">" TUIS "</annotation>" vir kontroles"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Prent-in-prent-kieslys"</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Skuif links"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Skuif regs"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Skuif op"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Skuif af"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Klaar"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-am/strings.xml b/libs/WindowManager/Shell/res/values-am/strings.xml index d724372c3da3..f0c391cd6b99 100644 --- a/libs/WindowManager/Shell/res/values-am/strings.xml +++ b/libs/WindowManager/Shell/res/values-am/strings.xml @@ -73,4 +73,12 @@ <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="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> + <string name="letterbox_education_dialog_title" msgid="6688664582871779215">"አንዳንድ መተግበሪያዎች በቁም ፎቶ ውስጥ በተሻለ ሁኔታ ይሰራሉ"</string> + <string name="letterbox_education_dialog_subtext" msgid="4853542518367719562">"ቦታዎን በአግባቡ ለመጠቀም ከእነዚህ አማራጮች ውስጥ አንዱን ይሞክሩ"</string> + <string name="letterbox_education_screen_rotation_text" msgid="5085786687366339027">"ወደ የሙሉ ገጽ ዕይታ ለመሄድ መሣሪያዎን ያሽከርክሩት"</string> + <string name="letterbox_education_reposition_text" msgid="1068293354123934727">"ቦታውን ለመቀየር ከመተግበሪያው ቀጥሎ ላይ ሁለቴ መታ ያድርጉ"</string> + <string name="letterbox_education_got_it" msgid="4057634570866051177">"ገባኝ"</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 fcb87c5682e3..74ce49ef078e 100644 --- a/libs/WindowManager/Shell/res/values-am/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-am/strings_tv.xml @@ -19,6 +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="158770205886688553">"ውሰድ"</string> + <string name="pip_expand" msgid="1051966011679297308">"ዘርጋ"</string> + <string name="pip_collapse" msgid="3903295106641385962">"ሰብስብ"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" ለመቆጣጠሪያዎች "<annotation icon="home_icon">"መነሻ"</annotation>"ን ሁለቴ ይጫኑ"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"የስዕል-ላይ-ስዕል ምናሌ።"</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"ወደ ግራ ውሰድ"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"ወደ ቀኝ ውሰድ"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"ወደ ላይ ውሰድ"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"ወደ ታች ውሰድ"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"ተጠናቅቋል"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ar/strings.xml b/libs/WindowManager/Shell/res/values-ar/strings.xml index 7dd1f8151a72..aa4b3b704110 100644 --- a/libs/WindowManager/Shell/res/values-ar/strings.xml +++ b/libs/WindowManager/Shell/res/values-ar/strings.xml @@ -73,4 +73,12 @@ <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="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> + <string name="letterbox_education_dialog_title" msgid="6688664582871779215">"تعمل بعض التطبيقات على أكمل وجه في الشاشات العمودية"</string> + <string name="letterbox_education_dialog_subtext" msgid="4853542518367719562">"جرِّب تنفيذ أحد هذه الخيارات للاستفادة من مساحتك إلى أقصى حد."</string> + <string name="letterbox_education_screen_rotation_text" msgid="5085786687366339027">"قم بتدوير الشاشة للانتقال إلى وضع ملء الشاشة."</string> + <string name="letterbox_education_reposition_text" msgid="1068293354123934727">"انقر مرتين بجانب التطبيق لتغيير موضعه."</string> + <string name="letterbox_education_got_it" msgid="4057634570866051177">"حسنًا"</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 4eef29e2ed12..9c195a7386a9 100644 --- a/libs/WindowManager/Shell/res/values-ar/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-ar/strings_tv.xml @@ -19,6 +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="158770205886688553">"نقل"</string> + <string name="pip_expand" msgid="1051966011679297308">"توسيع"</string> + <string name="pip_collapse" msgid="3903295106641385962">"تصغير"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" انقر مرتين على "<annotation icon="home_icon">" الصفحة الرئيسية "</annotation>" للوصول لعناصر التحكم."</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"قائمة نافذة ضمن النافذة"</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"نقل لليسار"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"نقل لليمين"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"نقل للأعلى"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"نقل للأسفل"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"تمّ"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-as/strings.xml b/libs/WindowManager/Shell/res/values-as/strings.xml index 190f7ca9b96c..985d3b9b96fd 100644 --- a/libs/WindowManager/Shell/res/values-as/strings.xml +++ b/libs/WindowManager/Shell/res/values-as/strings.xml @@ -73,4 +73,12 @@ <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="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> + <string name="letterbox_education_dialog_title" msgid="6688664582871779215">"কিছুমান এপে প’ৰ্ট্ৰেইট ম’ডত বেছি ভালকৈ কাম কৰে"</string> + <string name="letterbox_education_dialog_subtext" msgid="4853542518367719562">"আপোনাৰ spaceৰ পৰা পাৰ্যমানে উপকৃত হ’বলৈ ইয়াৰে এটা বিকল্প চেষ্টা কৰি চাওক"</string> + <string name="letterbox_education_screen_rotation_text" msgid="5085786687366339027">"পূৰ্ণ স্ক্ৰীনলৈ যাবলৈ আপোনাৰ ডিভাইচটো ঘূৰাওক"</string> + <string name="letterbox_education_reposition_text" msgid="1068293354123934727">"এপ্টোৰ স্থান সলনি কৰিবলৈ ইয়াৰ কাষত দুবাৰ টিপক"</string> + <string name="letterbox_education_got_it" msgid="4057634570866051177">"বুজি পালোঁ"</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 170b2dbd458c..816b5b1c79dc 100644 --- a/libs/WindowManager/Shell/res/values-as/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-as/strings_tv.xml @@ -19,6 +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="158770205886688553">"স্থানান্তৰ কৰক"</string> + <string name="pip_expand" msgid="1051966011679297308">"বিস্তাৰ কৰক"</string> + <string name="pip_collapse" msgid="3903295106641385962">"সংকোচন কৰক"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" নিয়ন্ত্ৰণৰ বাবে "<annotation icon="home_icon">" গৃহপৃষ্ঠা "</annotation>" বুটামত দুবাৰ হেঁচক"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"চিত্ৰৰ ভিতৰৰ চিত্ৰ মেনু।"</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"বাওঁফাললৈ নিয়ক"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"সোঁফাললৈ নিয়ক"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"ওপৰলৈ নিয়ক"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"তললৈ নিয়ক"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"হ’ল"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-az/strings.xml b/libs/WindowManager/Shell/res/values-az/strings.xml index e33a35f975ad..8cd9b7a635ab 100644 --- a/libs/WindowManager/Shell/res/values-az/strings.xml +++ b/libs/WindowManager/Shell/res/values-az/strings.xml @@ -73,4 +73,12 @@ <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="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> + <string name="letterbox_education_dialog_title" msgid="6688664582871779215">"Bəzi tətbiqlər portret rejimində daha yaxşı işləyir"</string> + <string name="letterbox_education_dialog_subtext" msgid="4853542518367719562">"Məkanınızdan maksimum yararlanmaq üçün bu seçimlərdən birini sınayın"</string> + <string name="letterbox_education_screen_rotation_text" msgid="5085786687366339027">"Tam ekrana keçmək üçün cihazınızı fırladın"</string> + <string name="letterbox_education_reposition_text" msgid="1068293354123934727">"Tətbiqin yerini dəyişmək üçün yanına iki dəfə toxunun"</string> + <string name="letterbox_education_got_it" msgid="4057634570866051177">"Anladım"</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 c9f1acbef31b..ccb7a7069ad8 100644 --- a/libs/WindowManager/Shell/res/values-az/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-az/strings_tv.xml @@ -19,6 +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="158770205886688553">"Köçürün"</string> + <string name="pip_expand" msgid="1051966011679297308">"Genişləndirin"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Yığcamlaşdırın"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Nizamlayıcılar üçün "<annotation icon="home_icon">" ƏSAS SƏHİFƏ "</annotation>" süçimini iki dəfə basın"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Şəkildə şəkil menyusu."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Sola köçürün"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Sağa köçürün"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Yuxarı köçürün"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Aşağı köçürün"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Hazırdır"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-b+sr+Latn/strings.xml b/libs/WindowManager/Shell/res/values-b+sr+Latn/strings.xml index f59e9320c645..49524c608543 100644 --- a/libs/WindowManager/Shell/res/values-b+sr+Latn/strings.xml +++ b/libs/WindowManager/Shell/res/values-b+sr+Latn/strings.xml @@ -73,4 +73,12 @@ <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="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> + <string name="letterbox_education_dialog_title" msgid="6688664582871779215">"Neke aplikacije najbolje funkcionišu u uspravnom režimu"</string> + <string name="letterbox_education_dialog_subtext" msgid="4853542518367719562">"Isprobajte jednu od ovih opcija da biste na najbolji način iskoristili prostor"</string> + <string name="letterbox_education_screen_rotation_text" msgid="5085786687366339027">"Rotirajte uređaj za prikaz preko celog ekrana"</string> + <string name="letterbox_education_reposition_text" msgid="1068293354123934727">"Dvaput dodirnite pored aplikacije da biste promenili njenu poziciju"</string> + <string name="letterbox_education_got_it" msgid="4057634570866051177">"Važi"</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 6fbc91bbec60..51a1262b1de7 100644 --- a/libs/WindowManager/Shell/res/values-b+sr+Latn/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-b+sr+Latn/strings_tv.xml @@ -19,6 +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="158770205886688553">"Premesti"</string> + <string name="pip_expand" msgid="1051966011679297308">"Proširi"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Skupi"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Dvaput pritisnite "<annotation icon="home_icon">" HOME "</annotation>" za kontrole"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Meni Slika u slici."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Pomerite nalevo"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Pomerite nadesno"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Pomerite nagore"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Pomerite nadole"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Gotovo"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-be/strings.xml b/libs/WindowManager/Shell/res/values-be/strings.xml index 3b478f2ab6cb..1767e0d66241 100644 --- a/libs/WindowManager/Shell/res/values-be/strings.xml +++ b/libs/WindowManager/Shell/res/values-be/strings.xml @@ -73,4 +73,12 @@ <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="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> + <string name="letterbox_education_dialog_title" msgid="6688664582871779215">"Некаторыя праграмы лепш за ўсё працуюць у кніжнай арыентацыі"</string> + <string name="letterbox_education_dialog_subtext" msgid="4853542518367719562">"Каб эфектыўна выкарыстоўваць прастору, паспрабуйце адзін з гэтых варыянтаў"</string> + <string name="letterbox_education_screen_rotation_text" msgid="5085786687366339027">"Каб перайсці ў поўнаэкранны рэжым, павярніце прыладу"</string> + <string name="letterbox_education_reposition_text" msgid="1068293354123934727">"Двойчы націсніце побач з праграмай, каб перамясціць яе"</string> + <string name="letterbox_education_got_it" msgid="4057634570866051177">"Зразумела"</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 d33bf99e2ebd..15a353c649d6 100644 --- a/libs/WindowManager/Shell/res/values-be/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-be/strings_tv.xml @@ -19,6 +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="158770205886688553">"Перамясціць"</string> + <string name="pip_expand" msgid="1051966011679297308">"Разгарнуць"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Згарнуць"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Двойчы націсніце "<annotation icon="home_icon">" ГАЛОЎНЫ ЭКРАН "</annotation>" для пераходу ў налады"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Меню рэжыму \"Відарыс у відарысе\"."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Перамясціць улева"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Перамясціць управа"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Перамясціць уверх"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Перамясціць уніз"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Гатова"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-bg/strings.xml b/libs/WindowManager/Shell/res/values-bg/strings.xml index 3a77a1c7be28..c22fb86a4d4d 100644 --- a/libs/WindowManager/Shell/res/values-bg/strings.xml +++ b/libs/WindowManager/Shell/res/values-bg/strings.xml @@ -73,4 +73,12 @@ <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="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> + <string name="letterbox_education_dialog_title" msgid="6688664582871779215">"Някои приложения работят най-добре във вертикален режим"</string> + <string name="letterbox_education_dialog_subtext" msgid="4853542518367719562">"Изпробвайте една от следните опции, за да се възползвате максимално от мястото на екрана"</string> + <string name="letterbox_education_screen_rotation_text" msgid="5085786687366339027">"Завъртете екрана си, за да преминете в режим на цял екран"</string> + <string name="letterbox_education_reposition_text" msgid="1068293354123934727">"Докоснете два пъти дадено приложение, за да промените позицията му"</string> + <string name="letterbox_education_got_it" msgid="4057634570866051177">"Разбрах"</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 f4fad601179f..2b27a6927077 100644 --- a/libs/WindowManager/Shell/res/values-bg/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-bg/strings_tv.xml @@ -19,6 +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="158770205886688553">"Преместване"</string> + <string name="pip_expand" msgid="1051966011679297308">"Разгъване"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Свиване"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" За достъп до контролите натиснете 2 пъти "<annotation icon="home_icon">"НАЧАЛО"</annotation></string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Меню за функцията „Картина в картината“."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Преместване наляво"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Преместване надясно"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Преместване нагоре"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Преместване надолу"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Готово"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-bn/strings.xml b/libs/WindowManager/Shell/res/values-bn/strings.xml index 8bfd775704dd..c0944e0584e6 100644 --- a/libs/WindowManager/Shell/res/values-bn/strings.xml +++ b/libs/WindowManager/Shell/res/values-bn/strings.xml @@ -73,4 +73,12 @@ <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="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> + <string name="letterbox_education_dialog_title" msgid="6688664582871779215">"কিছু অ্যাপ \'পোর্ট্রেট\' মোডে সবচেয়ে ভাল কাজ করে"</string> + <string name="letterbox_education_dialog_subtext" msgid="4853542518367719562">"আপনার স্পেস সবচেয়ে ভালভাবে কাজে লাগাতে এইসব বিকল্পের মধ্যে কোনও একটি ব্যবহার করে দেখুন"</string> + <string name="letterbox_education_screen_rotation_text" msgid="5085786687366339027">"\'ফুল স্ক্রিন\' মোডে যেতে ডিভাইস ঘোরান"</string> + <string name="letterbox_education_reposition_text" msgid="1068293354123934727">"কোনও অ্যাপের পাশে ডবল ট্যাপ করে সেটির জায়গা পরিবর্তন করুন"</string> + <string name="letterbox_education_got_it" msgid="4057634570866051177">"বুঝেছি"</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 0eb83a0276e6..23c8ffabeede 100644 --- a/libs/WindowManager/Shell/res/values-bn/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-bn/strings_tv.xml @@ -19,6 +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="158770205886688553">"সরান"</string> + <string name="pip_expand" msgid="1051966011679297308">"বড় করুন"</string> + <string name="pip_collapse" msgid="3903295106641385962">"আড়াল করুন"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" কন্ট্রোলের জন্য "<annotation icon="home_icon">" হোম "</annotation>" বোতামে ডবল প্রেস করুন"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"ছবির-মধ্যে-ছবি মেনু।"</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"বাঁদিকে সরান"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"ডানদিকে সরান"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"উপরে তুলুন"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"নিচে নামান"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"হয়ে গেছে"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-bs/strings.xml b/libs/WindowManager/Shell/res/values-bs/strings.xml index d23cc61b52f1..f10b62e65e97 100644 --- a/libs/WindowManager/Shell/res/values-bs/strings.xml +++ b/libs/WindowManager/Shell/res/values-bs/strings.xml @@ -35,7 +35,7 @@ <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Aplikacija ne podržava dijeljenje ekrana."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Aplikacija možda neće raditi na sekundarnom ekranu."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Aplikacija ne podržava pokretanje na sekundarnim ekranima."</string> - <string name="accessibility_divider" msgid="703810061635792791">"Razdjelnik ekrana"</string> + <string name="accessibility_divider" msgid="703810061635792791">"Razdjelnik podijeljenog ekrana"</string> <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Lijevo cijeli ekran"</string> <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Lijevo 70%"</string> <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Lijevo 50%"</string> @@ -73,4 +73,12 @@ <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="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> + <string name="letterbox_education_dialog_title" msgid="6688664582871779215">"Određene aplikacije najbolje funkcioniraju u uspravnom načinu rada"</string> + <string name="letterbox_education_dialog_subtext" msgid="4853542518367719562">"Isprobajte jednu od ovih opcija da maksimalno iskoristite prostor"</string> + <string name="letterbox_education_screen_rotation_text" msgid="5085786687366339027">"Zarotirajte uređaj da aktivirate prikaz preko cijelog ekrana"</string> + <string name="letterbox_education_reposition_text" msgid="1068293354123934727">"Dvaput dodirnite pored aplikacije da promijenite njen položaj"</string> + <string name="letterbox_education_got_it" msgid="4057634570866051177">"Razumijem"</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 9a655bb41066..443fd620fd65 100644 --- a/libs/WindowManager/Shell/res/values-bs/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-bs/strings_tv.xml @@ -19,6 +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="158770205886688553">"Premjesti"</string> + <string name="pip_expand" msgid="1051966011679297308">"Proširi"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Suzi"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Dvaput pritisnite "<annotation icon="home_icon">" POČETNI EKRAN "</annotation>" za kontrole"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Meni za način rada slika u slici."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Pomjeranje ulijevo"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Pomjeranje udesno"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Pomjeranje nagore"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Pomjeranje nadolje"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Gotovo"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ca/strings.xml b/libs/WindowManager/Shell/res/values-ca/strings.xml index f3bab494c076..8a522b3e6397 100644 --- a/libs/WindowManager/Shell/res/values-ca/strings.xml +++ b/libs/WindowManager/Shell/res/values-ca/strings.xml @@ -73,4 +73,12 @@ <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="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> + <string name="letterbox_education_dialog_title" msgid="6688664582871779215">"Algunes aplicacions funcionen millor en posició vertical"</string> + <string name="letterbox_education_dialog_subtext" msgid="4853542518367719562">"Prova una d\'aquestes opcions per treure el màxim profit de l\'espai"</string> + <string name="letterbox_education_screen_rotation_text" msgid="5085786687366339027">"Gira el dispositiu per passar a pantalla completa"</string> + <string name="letterbox_education_reposition_text" msgid="1068293354123934727">"Fes doble toc al costat d\'una aplicació per canviar-ne la posició"</string> + <string name="letterbox_education_got_it" msgid="4057634570866051177">"Entesos"</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 b80fc41402dd..94ba0db7e978 100644 --- a/libs/WindowManager/Shell/res/values-ca/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-ca/strings_tv.xml @@ -19,6 +19,16 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Pantalla en pantalla"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Programa sense títol)"</string> - <string name="pip_close" msgid="9135220303720555525">"Tanca PIP"</string> + <string name="pip_close" msgid="2955969519031223530">"Tanca"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Pantalla completa"</string> + <string name="pip_move" msgid="158770205886688553">"Mou"</string> + <string name="pip_expand" msgid="1051966011679297308">"Desplega"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Replega"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Prem dos cops "<annotation icon="home_icon">" INICI "</annotation>" per accedir als controls"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Menú de pantalla en pantalla."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Mou cap a l\'esquerra"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Mou cap a la dreta"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Mou cap amunt"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Mou cap avall"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Fet"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-cs/strings.xml b/libs/WindowManager/Shell/res/values-cs/strings.xml index 3530a7c8b835..d0cf80aef38c 100644 --- a/libs/WindowManager/Shell/res/values-cs/strings.xml +++ b/libs/WindowManager/Shell/res/values-cs/strings.xml @@ -73,4 +73,12 @@ <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="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> + <string name="letterbox_education_dialog_title" msgid="6688664582871779215">"Některé aplikace fungují nejlépe na výšku"</string> + <string name="letterbox_education_dialog_subtext" msgid="4853542518367719562">"Pokud chcete maximálně využít prostor, vyzkoušejte jednu z těchto možností"</string> + <string name="letterbox_education_screen_rotation_text" msgid="5085786687366339027">"Otočením zařízení přejděte do režimu celé obrazovky"</string> + <string name="letterbox_education_reposition_text" msgid="1068293354123934727">"Dvojitým klepnutím vedle aplikace změňte její umístění"</string> + <string name="letterbox_education_got_it" msgid="4057634570866051177">"OK"</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 56abcbe473fb..3ed85dce0433 100644 --- a/libs/WindowManager/Shell/res/values-cs/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-cs/strings_tv.xml @@ -19,6 +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="158770205886688553">"Přesunout"</string> + <string name="pip_expand" msgid="1051966011679297308">"Rozbalit"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Sbalit"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Ovládací prvky zobrazíte dvojitým stisknutím "<annotation icon="home_icon">"tlačítka plochy"</annotation></string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Nabídka režimu obrazu v obraze"</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Přesunout doleva"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Přesunout doprava"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Přesunout nahoru"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Přesunout dolů"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Hotovo"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-da/strings.xml b/libs/WindowManager/Shell/res/values-da/strings.xml index 89b66e5309e9..bb81c10c6e1b 100644 --- a/libs/WindowManager/Shell/res/values-da/strings.xml +++ b/libs/WindowManager/Shell/res/values-da/strings.xml @@ -73,4 +73,12 @@ <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="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> + <string name="letterbox_education_dialog_title" msgid="6688664582871779215">"Nogle apps fungerer bedst i stående format"</string> + <string name="letterbox_education_dialog_subtext" msgid="4853542518367719562">"Prøv én af disse muligheder for at få mest muligt ud af dit rum"</string> + <string name="letterbox_education_screen_rotation_text" msgid="5085786687366339027">"Drej din enhed for at gå til fuld skærm"</string> + <string name="letterbox_education_reposition_text" msgid="1068293354123934727">"Tryk to gange ud for en app for at ændre dens placering"</string> + <string name="letterbox_education_got_it" msgid="4057634570866051177">"OK"</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 fdb6b783399e..09024428a825 100644 --- a/libs/WindowManager/Shell/res/values-da/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-da/strings_tv.xml @@ -19,6 +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="158770205886688553">"Flyt"</string> + <string name="pip_expand" msgid="1051966011679297308">"Udvid"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Skjul"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Tryk to gange på "<annotation icon="home_icon">" HJEM "</annotation>" for at se indstillinger"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Menu for integreret billede."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Flyt til venstre"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Flyt til højre"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Flyt op"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Flyt ned"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Udfør"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-de/strings.xml b/libs/WindowManager/Shell/res/values-de/strings.xml index 7a502283908f..c5d945a982ef 100644 --- a/libs/WindowManager/Shell/res/values-de/strings.xml +++ b/libs/WindowManager/Shell/res/values-de/strings.xml @@ -73,4 +73,12 @@ <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="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> + <string name="letterbox_education_dialog_title" msgid="6688664582871779215">"Einige Apps funktionieren am besten im Hochformat"</string> + <string name="letterbox_education_dialog_subtext" msgid="4853542518367719562">"Mithilfe dieser Möglichkeiten kannst du dein Display optimal nutzen"</string> + <string name="letterbox_education_screen_rotation_text" msgid="5085786687366339027">"Gerät drehen, um zum Vollbildmodus zu wechseln"</string> + <string name="letterbox_education_reposition_text" msgid="1068293354123934727">"Neben einer App doppeltippen, um die Position zu ändern"</string> + <string name="letterbox_education_got_it" msgid="4057634570866051177">"Ok"</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 02cce9d73647..18535c9d9338 100644 --- a/libs/WindowManager/Shell/res/values-de/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-de/strings_tv.xml @@ -19,6 +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="158770205886688553">"Bewegen"</string> + <string name="pip_expand" msgid="1051966011679297308">"Maximieren"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Minimieren"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Für Steuerelemente zweimal "<annotation icon="home_icon">"STARTBILDSCHIRMTASTE"</annotation>" drücken"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Menü „Bild im Bild“."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Nach links bewegen"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Nach rechts bewegen"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Nach oben bewegen"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Nach unten bewegen"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Fertig"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-el/strings.xml b/libs/WindowManager/Shell/res/values-el/strings.xml index ed1d9133eb92..70f55058925c 100644 --- a/libs/WindowManager/Shell/res/values-el/strings.xml +++ b/libs/WindowManager/Shell/res/values-el/strings.xml @@ -73,4 +73,12 @@ <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="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> + <string name="letterbox_education_dialog_title" msgid="6688664582871779215">"Ορισμένες εφαρμογές λειτουργούν καλύτερα σε κατακόρυφο προσανατολισμό"</string> + <string name="letterbox_education_dialog_subtext" msgid="4853542518367719562">"Δοκιμάστε μία από αυτές τις επιλογές για να αξιοποιήσετε στο έπακρο τον χώρο σας."</string> + <string name="letterbox_education_screen_rotation_text" msgid="5085786687366339027">"Περιστρέψτε τη συσκευή σας για μετάβαση σε πλήρη οθόνη."</string> + <string name="letterbox_education_reposition_text" msgid="1068293354123934727">"Πατήστε δύο φορές δίπλα σε μια εφαρμογή για να αλλάξετε τη θέση της."</string> + <string name="letterbox_education_got_it" msgid="4057634570866051177">"Το κατάλαβα"</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 880ea37e6bf7..5f8a004b0a1f 100644 --- a/libs/WindowManager/Shell/res/values-el/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-el/strings_tv.xml @@ -19,6 +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="158770205886688553">"Μετακίνηση"</string> + <string name="pip_expand" msgid="1051966011679297308">"Ανάπτυξη"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Σύμπτυξη"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Πατήστε δύο φορές "<annotation icon="home_icon">" ΑΡΧΙΚΗ ΟΘΟΝΗ "</annotation>" για στοιχεία ελέγχου"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Μενού λειτουργίας Picture-in-Picture."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Μετακίνηση αριστερά"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Μετακίνηση δεξιά"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Μετακίνηση επάνω"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Μετακίνηση κάτω"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Τέλος"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-en-rAU/strings.xml b/libs/WindowManager/Shell/res/values-en-rAU/strings.xml index 067e998ff396..0b5aefa5c72e 100644 --- a/libs/WindowManager/Shell/res/values-en-rAU/strings.xml +++ b/libs/WindowManager/Shell/res/values-en-rAU/strings.xml @@ -73,4 +73,12 @@ <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="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_got_it" msgid="4057634570866051177">"Got it"</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 e3f08c8cc76f..839789b22a1c 100644 --- a/libs/WindowManager/Shell/res/values-en-rAU/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-en-rAU/strings_tv.xml @@ -19,6 +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="158770205886688553">"Move"</string> + <string name="pip_expand" msgid="1051966011679297308">"Expand"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Collapse"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Double-press "<annotation icon="home_icon">" HOME "</annotation>" for controls"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Picture-in-picture menu"</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Move left"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Move right"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Move up"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Move down"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Done"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-en-rCA/strings.xml b/libs/WindowManager/Shell/res/values-en-rCA/strings.xml index 067e998ff396..0b5aefa5c72e 100644 --- a/libs/WindowManager/Shell/res/values-en-rCA/strings.xml +++ b/libs/WindowManager/Shell/res/values-en-rCA/strings.xml @@ -73,4 +73,12 @@ <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="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_got_it" msgid="4057634570866051177">"Got it"</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 e3f08c8cc76f..839789b22a1c 100644 --- a/libs/WindowManager/Shell/res/values-en-rCA/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-en-rCA/strings_tv.xml @@ -19,6 +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="158770205886688553">"Move"</string> + <string name="pip_expand" msgid="1051966011679297308">"Expand"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Collapse"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Double-press "<annotation icon="home_icon">" HOME "</annotation>" for controls"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Picture-in-picture menu"</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Move left"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Move right"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Move up"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Move down"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Done"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-en-rGB/strings.xml b/libs/WindowManager/Shell/res/values-en-rGB/strings.xml index 067e998ff396..0b5aefa5c72e 100644 --- a/libs/WindowManager/Shell/res/values-en-rGB/strings.xml +++ b/libs/WindowManager/Shell/res/values-en-rGB/strings.xml @@ -73,4 +73,12 @@ <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="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_got_it" msgid="4057634570866051177">"Got it"</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 e3f08c8cc76f..839789b22a1c 100644 --- a/libs/WindowManager/Shell/res/values-en-rGB/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-en-rGB/strings_tv.xml @@ -19,6 +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="158770205886688553">"Move"</string> + <string name="pip_expand" msgid="1051966011679297308">"Expand"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Collapse"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Double-press "<annotation icon="home_icon">" HOME "</annotation>" for controls"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Picture-in-picture menu"</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Move left"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Move right"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Move up"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Move down"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Done"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-en-rIN/strings.xml b/libs/WindowManager/Shell/res/values-en-rIN/strings.xml index 067e998ff396..0b5aefa5c72e 100644 --- a/libs/WindowManager/Shell/res/values-en-rIN/strings.xml +++ b/libs/WindowManager/Shell/res/values-en-rIN/strings.xml @@ -73,4 +73,12 @@ <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="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_got_it" msgid="4057634570866051177">"Got it"</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 e3f08c8cc76f..839789b22a1c 100644 --- a/libs/WindowManager/Shell/res/values-en-rIN/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-en-rIN/strings_tv.xml @@ -19,6 +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="158770205886688553">"Move"</string> + <string name="pip_expand" msgid="1051966011679297308">"Expand"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Collapse"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Double-press "<annotation icon="home_icon">" HOME "</annotation>" for controls"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Picture-in-picture menu"</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Move left"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Move right"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Move up"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Move down"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Done"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-en-rXC/strings.xml b/libs/WindowManager/Shell/res/values-en-rXC/strings.xml index 95c0d0175413..5c3d0f65374a 100644 --- a/libs/WindowManager/Shell/res/values-en-rXC/strings.xml +++ b/libs/WindowManager/Shell/res/values-en-rXC/strings.xml @@ -73,4 +73,12 @@ <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="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_got_it" msgid="4057634570866051177">"Got it"</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 3f9ef0ea2816..507e066e3812 100644 --- a/libs/WindowManager/Shell/res/values-en-rXC/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-en-rXC/strings_tv.xml @@ -19,6 +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="158770205886688553">"Move"</string> + <string name="pip_expand" msgid="1051966011679297308">"Expand"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Collapse"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Double press "<annotation icon="home_icon">" HOME "</annotation>" for controls"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Picture-in-Picture menu."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Move left"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Move right"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Move up"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Move down"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Done"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-es-rUS/strings.xml b/libs/WindowManager/Shell/res/values-es-rUS/strings.xml index 6e5347d1102c..e523ae53b0cc 100644 --- a/libs/WindowManager/Shell/res/values-es-rUS/strings.xml +++ b/libs/WindowManager/Shell/res/values-es-rUS/strings.xml @@ -73,4 +73,12 @@ <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="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> + <string name="letterbox_education_dialog_title" msgid="6688664582871779215">"Algunas apps funcionan mejor en modo vertical"</string> + <string name="letterbox_education_dialog_subtext" msgid="4853542518367719562">"Prueba estas opciones para aprovechar al máximo tu espacio"</string> + <string name="letterbox_education_screen_rotation_text" msgid="5085786687366339027">"Rota el dispositivo para ver la pantalla completa"</string> + <string name="letterbox_education_reposition_text" msgid="1068293354123934727">"Presiona dos veces junto a una app para cambiar su posición"</string> + <string name="letterbox_education_got_it" msgid="4057634570866051177">"Entendido"</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 5d5954a19761..a2c27b79e04c 100644 --- a/libs/WindowManager/Shell/res/values-es-rUS/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-es-rUS/strings_tv.xml @@ -19,6 +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="158770205886688553">"Mover"</string> + <string name="pip_expand" msgid="1051966011679297308">"Expandir"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Contraer"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Presiona dos veces "<annotation icon="home_icon">"INICIO"</annotation>" para ver los controles"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Menú de pantalla en pantalla"</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Mover hacia la izquierda"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Mover hacia la derecha"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Mover hacia arriba"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Mover hacia abajo"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Listo"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-es/strings.xml b/libs/WindowManager/Shell/res/values-es/strings.xml index 4ddfc9710b72..39990dc8cb0c 100644 --- a/libs/WindowManager/Shell/res/values-es/strings.xml +++ b/libs/WindowManager/Shell/res/values-es/strings.xml @@ -73,4 +73,12 @@ <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="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> + <string name="letterbox_education_dialog_title" msgid="6688664582871779215">"Algunas aplicaciones funcionan mejor en vertical"</string> + <string name="letterbox_education_dialog_subtext" msgid="4853542518367719562">"Prueba una de estas opciones para sacar el máximo partido al espacio de tu pantalla"</string> + <string name="letterbox_education_screen_rotation_text" msgid="5085786687366339027">"Gira el dispositivo para ir al modo de pantalla completa"</string> + <string name="letterbox_education_reposition_text" msgid="1068293354123934727">"Toca dos veces junto a una aplicación para cambiar su posición"</string> + <string name="letterbox_education_got_it" msgid="4057634570866051177">"Entendido"</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 d31b9b45cae3..75db421ec405 100644 --- a/libs/WindowManager/Shell/res/values-es/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-es/strings_tv.xml @@ -19,6 +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="158770205886688553">"Mover"</string> + <string name="pip_expand" msgid="1051966011679297308">"Mostrar"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Contraer"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Pulsa dos veces "<annotation icon="home_icon">"INICIO"</annotation>" para ver los controles"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Menú de imagen en imagen."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Mover hacia la izquierda"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Mover hacia la derecha"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Mover hacia arriba"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Mover hacia abajo"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Hecho"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-et/strings.xml b/libs/WindowManager/Shell/res/values-et/strings.xml index 4c946948fd26..a5f82a6452c4 100644 --- a/libs/WindowManager/Shell/res/values-et/strings.xml +++ b/libs/WindowManager/Shell/res/values-et/strings.xml @@ -73,4 +73,12 @@ <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="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> + <string name="letterbox_education_dialog_title" msgid="6688664582871779215">"Mõni rakendus töötab kõige paremini vertikaalpaigutuses"</string> + <string name="letterbox_education_dialog_subtext" msgid="4853542518367719562">"Proovige ühte neist valikutest, et oma ruumi parimal moel kasutada"</string> + <string name="letterbox_education_screen_rotation_text" msgid="5085786687366339027">"Pöörake seadet, et aktiveerida täisekraanirežiim"</string> + <string name="letterbox_education_reposition_text" msgid="1068293354123934727">"Topeltpuudutage rakenduse kõrval, et selle asendit muuta"</string> + <string name="letterbox_education_got_it" msgid="4057634570866051177">"Selge"</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 bc7a6adafc03..e8fcb180c0c4 100644 --- a/libs/WindowManager/Shell/res/values-et/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-et/strings_tv.xml @@ -19,6 +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="158770205886688553">"Teisalda"</string> + <string name="pip_expand" msgid="1051966011679297308">"Laienda"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Ahenda"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Nuppude nägemiseks vajutage 2 korda nuppu "<annotation icon="home_icon">"AVAKUVA"</annotation></string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Menüü Pilt pildis."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Teisalda vasakule"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Teisalda paremale"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Teisalda üles"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Teisalda alla"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Valmis"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-eu/strings.xml b/libs/WindowManager/Shell/res/values-eu/strings.xml index dba649c1e59b..67b9a433dc03 100644 --- a/libs/WindowManager/Shell/res/values-eu/strings.xml +++ b/libs/WindowManager/Shell/res/values-eu/strings.xml @@ -73,4 +73,12 @@ <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="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> + <string name="letterbox_education_dialog_title" msgid="6688664582871779215">"Aplikazio batzuk orientazio bertikalean funtzionatzen dute hobekien"</string> + <string name="letterbox_education_dialog_subtext" msgid="4853542518367719562">"Pantailako eremuari ahalik eta etekinik handiena ateratzeko, probatu aukera hauetakoren bat"</string> + <string name="letterbox_education_screen_rotation_text" msgid="5085786687366339027">"Pantaila osoko modua erabiltzeko, biratu gailua"</string> + <string name="letterbox_education_reposition_text" msgid="1068293354123934727">"Aplikazioaren posizioa aldatzeko, sakatu birritan haren ondoko edozein toki"</string> + <string name="letterbox_education_got_it" msgid="4057634570866051177">"Ados"</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 cf5f98883082..07d75d2de9cd 100644 --- a/libs/WindowManager/Shell/res/values-eu/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-eu/strings_tv.xml @@ -19,6 +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="158770205886688553">"Mugitu"</string> + <string name="pip_expand" msgid="1051966011679297308">"Zabaldu"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Tolestu"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Kontrolatzeko aukerak atzitzeko, sakatu birritan "<annotation icon="home_icon">" HASIERA "</annotation></string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Pantaila txiki gainjarriaren menua."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Eraman ezkerrera"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Eraman eskuinera"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Eraman gora"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Eraman behera"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Eginda"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-fa/strings.xml b/libs/WindowManager/Shell/res/values-fa/strings.xml index 0c67a41eac1f..761fb9ddeb2f 100644 --- a/libs/WindowManager/Shell/res/values-fa/strings.xml +++ b/libs/WindowManager/Shell/res/values-fa/strings.xml @@ -73,4 +73,12 @@ <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="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> + <string name="letterbox_education_dialog_title" msgid="6688664582871779215">"برخیاز برنامهها در حالت عمودی عملکرد بهتری دارند"</string> + <string name="letterbox_education_dialog_subtext" msgid="4853542518367719562">"با امتحان کردن یکی از این گزینهها، بیشترین بهره را از فضایتان ببرید"</string> + <string name="letterbox_education_screen_rotation_text" msgid="5085786687366339027">"برای رفتن به حالت تمام صفحه، دستگاهتان را بچرخانید"</string> + <string name="letterbox_education_reposition_text" msgid="1068293354123934727">"در کنار برنامه دوضربه بزنید تا جابهجا شود"</string> + <string name="letterbox_education_got_it" msgid="4057634570866051177">"متوجهام"</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 5b815b4c7b86..03f51d01a3a8 100644 --- a/libs/WindowManager/Shell/res/values-fa/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-fa/strings_tv.xml @@ -19,6 +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="158770205886688553">"انتقال"</string> + <string name="pip_expand" msgid="1051966011679297308">"گسترده کردن"</string> + <string name="pip_collapse" msgid="3903295106641385962">"جمع کردن"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" برای کنترلها، دکمه "<annotation icon="home_icon">"صفحه اصلی"</annotation>" را دوبار فشار دهید"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"منوی تصویر در تصویر."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"انتقال بهچپ"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"انتقال بهراست"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"انتقال بهبالا"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"انتقال بهپایین"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"تمام"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-fi/strings.xml b/libs/WindowManager/Shell/res/values-fi/strings.xml index f6711055e4d9..c809b4879e71 100644 --- a/libs/WindowManager/Shell/res/values-fi/strings.xml +++ b/libs/WindowManager/Shell/res/values-fi/strings.xml @@ -73,4 +73,12 @@ <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="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> + <string name="letterbox_education_dialog_title" msgid="6688664582871779215">"Osa sovelluksista toimii parhaiten pystytilassa"</string> + <string name="letterbox_education_dialog_subtext" msgid="4853542518367719562">"Kokeile jotakin näistä vaihtoehdoista, jotta saat parhaan hyödyn näytön tilasta"</string> + <string name="letterbox_education_screen_rotation_text" msgid="5085786687366339027">"Käännä laitetta, niin se siirtyy koko näytön tilaan"</string> + <string name="letterbox_education_reposition_text" msgid="1068293354123934727">"Kaksoisnapauta sovellusta, jos haluat siirtää sitä"</string> + <string name="letterbox_education_got_it" msgid="4057634570866051177">"OK"</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 77ad6eef91e7..24ab7d99e180 100644 --- a/libs/WindowManager/Shell/res/values-fi/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-fi/strings_tv.xml @@ -19,6 +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="158770205886688553">"Siirrä"</string> + <string name="pip_expand" msgid="1051966011679297308">"Laajenna"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Tiivistä"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Asetukset: paina "<annotation icon="home_icon">"ALOITUSNÄYTTÖPAINIKETTA"</annotation>" kahdesti"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Kuva kuvassa ‑valikko."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Siirrä vasemmalle"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Siirrä oikealle"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Siirrä ylös"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Siirrä alas"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Valmis"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-fr-rCA/strings.xml b/libs/WindowManager/Shell/res/values-fr-rCA/strings.xml index 0d0b71868170..62b2bb65a603 100644 --- a/libs/WindowManager/Shell/res/values-fr-rCA/strings.xml +++ b/libs/WindowManager/Shell/res/values-fr-rCA/strings.xml @@ -73,4 +73,12 @@ <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="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> + <string name="letterbox_education_dialog_title" msgid="6688664582871779215">"Certaines applications fonctionnent mieux en mode portrait"</string> + <string name="letterbox_education_dialog_subtext" msgid="4853542518367719562">"Essayez l\'une de ces options pour tirer le meilleur parti de votre espace"</string> + <string name="letterbox_education_screen_rotation_text" msgid="5085786687366339027">"Faites pivoter votre appareil pour passer en plein écran"</string> + <string name="letterbox_education_reposition_text" msgid="1068293354123934727">"Touchez deux fois à côté d\'une application pour la repositionner"</string> + <string name="letterbox_education_got_it" msgid="4057634570866051177">"OK"</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 0ec7f40f0e9f..87651ec711d9 100644 --- a/libs/WindowManager/Shell/res/values-fr-rCA/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-fr-rCA/strings_tv.xml @@ -19,6 +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="158770205886688553">"Déplacer"</string> + <string name="pip_expand" msgid="1051966011679297308">"Développer"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Réduire"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Appuyez deux fois sur "<annotation icon="home_icon">" ACCUEIL "</annotation>" pour les commandes"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Menu d\'incrustation d\'image."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Déplacer vers la gauche"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Déplacer vers la droite"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Déplacer vers le haut"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Déplacer vers le bas"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"OK"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-fr/strings.xml b/libs/WindowManager/Shell/res/values-fr/strings.xml index d9d537796c10..07475055f03e 100644 --- a/libs/WindowManager/Shell/res/values-fr/strings.xml +++ b/libs/WindowManager/Shell/res/values-fr/strings.xml @@ -73,4 +73,12 @@ <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="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> + <string name="letterbox_education_dialog_title" msgid="6688664582871779215">"Certaines applis fonctionnent mieux en mode Portrait"</string> + <string name="letterbox_education_dialog_subtext" msgid="4853542518367719562">"Essayez l\'une de ces options pour exploiter pleinement l\'espace"</string> + <string name="letterbox_education_screen_rotation_text" msgid="5085786687366339027">"Faites pivoter l\'appareil pour passer en plein écran"</string> + <string name="letterbox_education_reposition_text" msgid="1068293354123934727">"Appuyez deux fois à côté d\'une appli pour la repositionner"</string> + <string name="letterbox_education_got_it" msgid="4057634570866051177">"OK"</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 27fd155535b7..37863fb82295 100644 --- a/libs/WindowManager/Shell/res/values-fr/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-fr/strings_tv.xml @@ -19,6 +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="158770205886688553">"Déplacer"</string> + <string name="pip_expand" msgid="1051966011679297308">"Développer"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Réduire"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Menu de commandes : appuyez deux fois sur "<annotation icon="home_icon">"ACCUEIL"</annotation></string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Menu \"Picture-in-picture\"."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Déplacer vers la gauche"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Déplacer vers la droite"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Déplacer vers le haut"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Déplacer vers le bas"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"OK"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-gl/strings.xml b/libs/WindowManager/Shell/res/values-gl/strings.xml index 81bd9167d0e6..b8e039602243 100644 --- a/libs/WindowManager/Shell/res/values-gl/strings.xml +++ b/libs/WindowManager/Shell/res/values-gl/strings.xml @@ -73,4 +73,12 @@ <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="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> + <string name="letterbox_education_dialog_title" msgid="6688664582871779215">"Algunhas aplicacións funcionan mellor en modo vertical"</string> + <string name="letterbox_education_dialog_subtext" msgid="4853542518367719562">"Proba unha destas opcións para sacar o máximo proveito do espazo da pantalla"</string> + <string name="letterbox_education_screen_rotation_text" msgid="5085786687366339027">"Xira o dispositivo para ver o contido en pantalla completa"</string> + <string name="letterbox_education_reposition_text" msgid="1068293354123934727">"Toca dúas veces a carón dunha aplicación para cambiala de posición"</string> + <string name="letterbox_education_got_it" msgid="4057634570866051177">"Entendido"</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 df96f6cb794d..5d6de76c4deb 100644 --- a/libs/WindowManager/Shell/res/values-gl/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-gl/strings_tv.xml @@ -19,6 +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="158770205886688553">"Mover"</string> + <string name="pip_expand" msgid="1051966011679297308">"Despregar"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Contraer"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Preme "<annotation icon="home_icon">"INICIO"</annotation>" dúas veces para acceder aos controis"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Menú de pantalla superposta."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Mover cara á esquerda"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Mover cara á dereita"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Mover cara arriba"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Mover cara abaixo"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Feito"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-gu/strings.xml b/libs/WindowManager/Shell/res/values-gu/strings.xml index 3d408cf2f698..deda2d755e20 100644 --- a/libs/WindowManager/Shell/res/values-gu/strings.xml +++ b/libs/WindowManager/Shell/res/values-gu/strings.xml @@ -73,4 +73,12 @@ <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="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> + <string name="letterbox_education_dialog_title" msgid="6688664582871779215">"અમુક ઍપ પોર્ટ્રેટ મોડમાં શ્રેષ્ઠ રીતે કાર્ય કરે છે"</string> + <string name="letterbox_education_dialog_subtext" msgid="4853542518367719562">"તમારી સ્પેસનો વધુને વધુ લાભ લેવા માટે, આ વિકલ્પોમાંથી કોઈ એક અજમાવો"</string> + <string name="letterbox_education_screen_rotation_text" msgid="5085786687366339027">"પૂર્ણ સ્ક્રીન મોડ લાગુ કરવા માટે, તમારા ડિવાઇસને ફેરવો"</string> + <string name="letterbox_education_reposition_text" msgid="1068293354123934727">"કોઈ ઍપની જગ્યા બદલવા માટે, તેની બાજુમાં બે વાર ટૅપ કરો"</string> + <string name="letterbox_education_got_it" msgid="4057634570866051177">"સમજાઈ ગયું"</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 3608f1d530c0..6c1b9db73582 100644 --- a/libs/WindowManager/Shell/res/values-gu/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-gu/strings_tv.xml @@ -19,6 +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="158770205886688553">"ખસેડો"</string> + <string name="pip_expand" msgid="1051966011679297308">"મોટું કરો"</string> + <string name="pip_collapse" msgid="3903295106641385962">"નાનું કરો"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" નિયંત્રણો માટે "<annotation icon="home_icon">" હોમ "</annotation>" બટન પર બે વાર દબાવો"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"ચિત્રમાં ચિત્ર મેનૂ."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"ડાબે ખસેડો"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"જમણે ખસેડો"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"ઉપર ખસેડો"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"નીચે ખસેડો"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"થઈ ગયું"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-hi/strings.xml b/libs/WindowManager/Shell/res/values-hi/strings.xml index 49f72c9d3c6a..a5fcb97d1418 100644 --- a/libs/WindowManager/Shell/res/values-hi/strings.xml +++ b/libs/WindowManager/Shell/res/values-hi/strings.xml @@ -73,4 +73,12 @@ <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="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> + <string name="letterbox_education_dialog_title" msgid="6688664582871779215">"कुछ ऐप्लिकेशन, पोर्ट्रेट मोड में सबसे अच्छी तरह काम करते हैं"</string> + <string name="letterbox_education_dialog_subtext" msgid="4853542518367719562">"जगह का पूरा इस्तेमाल करने के लिए, इनमें से किसी एक विकल्प को आज़माएं"</string> + <string name="letterbox_education_screen_rotation_text" msgid="5085786687366339027">"फ़ुल स्क्रीन मोड में जाने के लिए, डिवाइस को घुमाएं"</string> + <string name="letterbox_education_reposition_text" msgid="1068293354123934727">"किसी ऐप्लिकेशन की जगह बदलने के लिए, उसके बगल में दो बार टैप करें"</string> + <string name="letterbox_education_got_it" msgid="4057634570866051177">"ठीक है"</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 720bb6ca5e24..e0227253b2dc 100644 --- a/libs/WindowManager/Shell/res/values-hi/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-hi/strings_tv.xml @@ -19,6 +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="158770205886688553">"ले जाएं"</string> + <string name="pip_expand" msgid="1051966011679297308">"बड़ा करें"</string> + <string name="pip_collapse" msgid="3903295106641385962">"छोटा करें"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" कंट्रोल मेन्यू पर जाने के लिए, "<annotation icon="home_icon">" होम बटन"</annotation>" दो बार दबाएं"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"पिक्चर में पिक्चर मेन्यू."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"बाईं ओर ले जाएं"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"दाईं ओर ले जाएं"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"ऊपर ले जाएं"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"नीचे ले जाएं"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"हो गया"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-hr/strings.xml b/libs/WindowManager/Shell/res/values-hr/strings.xml index 1f8f982fca69..5ecc5585a6e9 100644 --- a/libs/WindowManager/Shell/res/values-hr/strings.xml +++ b/libs/WindowManager/Shell/res/values-hr/strings.xml @@ -73,4 +73,12 @@ <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="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> + <string name="letterbox_education_dialog_title" msgid="6688664582871779215">"Neke aplikacije najbolje funkcioniraju u portretnom usmjerenju"</string> + <string name="letterbox_education_dialog_subtext" msgid="4853542518367719562">"Isprobajte jednu od ovih opcija da biste maksimalno iskoristili prostor"</string> + <string name="letterbox_education_screen_rotation_text" msgid="5085786687366339027">"Zakrenite uređaj radi prikaza na cijelom zaslonu"</string> + <string name="letterbox_education_reposition_text" msgid="1068293354123934727">"Dvaput dodirnite pored aplikacije da biste joj promijenili položaj"</string> + <string name="letterbox_education_got_it" msgid="4057634570866051177">"Shvaćam"</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 21f8cb63f470..a09e6e805f63 100644 --- a/libs/WindowManager/Shell/res/values-hr/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-hr/strings_tv.xml @@ -19,6 +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="158770205886688553">"Premjesti"</string> + <string name="pip_expand" msgid="1051966011679297308">"Proširi"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Sažmi"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Dvaput pritisnite "<annotation icon="home_icon">"POČETNI ZASLON"</annotation>" za kontrole"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Izbornik slike u slici."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Pomaknite ulijevo"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Pomaknite udesno"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Pomaknite prema gore"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Pomaknite prema dolje"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Gotovo"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-hu/strings.xml b/libs/WindowManager/Shell/res/values-hu/strings.xml index ebd02e59a101..2295250e2853 100644 --- a/libs/WindowManager/Shell/res/values-hu/strings.xml +++ b/libs/WindowManager/Shell/res/values-hu/strings.xml @@ -73,4 +73,12 @@ <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="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> + <string name="letterbox_education_dialog_title" msgid="6688664582871779215">"Egyes alkalmazások álló tájolásban működnek a leghatékonyabban"</string> + <string name="letterbox_education_dialog_subtext" msgid="4853542518367719562">"Próbálja ki az alábbi beállítások egyikét, hogy a legjobban ki tudja használni képernyő területét"</string> + <string name="letterbox_education_screen_rotation_text" msgid="5085786687366339027">"A teljes képernyős mód elindításához forgassa el az eszközt"</string> + <string name="letterbox_education_reposition_text" msgid="1068293354123934727">"Koppintson duplán az alkalmazás mellett az áthelyezéséhez"</string> + <string name="letterbox_education_got_it" msgid="4057634570866051177">"Értem"</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 0010086bb0b5..5e065c2ad4e7 100644 --- a/libs/WindowManager/Shell/res/values-hu/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-hu/strings_tv.xml @@ -19,6 +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="158770205886688553">"Áthelyezés"</string> + <string name="pip_expand" msgid="1051966011679297308">"Kibontás"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Összecsukás"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Vezérlők: "<annotation icon="home_icon">" KEZDŐKÉPERNYŐ "</annotation>" gomb kétszer megnyomva"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Kép a képben menü."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Mozgatás balra"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Mozgatás jobbra"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Mozgatás felfelé"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Mozgatás lefelé"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Kész"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-hy/strings.xml b/libs/WindowManager/Shell/res/values-hy/strings.xml index 29b20521829e..208936539094 100644 --- a/libs/WindowManager/Shell/res/values-hy/strings.xml +++ b/libs/WindowManager/Shell/res/values-hy/strings.xml @@ -73,4 +73,12 @@ <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="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> + <string name="letterbox_education_dialog_title" msgid="6688664582871779215">"Որոշ հավելվածներ լավագույնս աշխատում են դիմանկարի ռեժիմում"</string> + <string name="letterbox_education_dialog_subtext" msgid="4853542518367719562">"Փորձեք այս տարբերակներից մեկը՝ տարածքը հնարավորինս արդյունավետ օգտագործելու համար"</string> + <string name="letterbox_education_screen_rotation_text" msgid="5085786687366339027">"Պտտեք սարքը՝ լիաէկրան ռեժիմին անցնելու համար"</string> + <string name="letterbox_education_reposition_text" msgid="1068293354123934727">"Կրկնակի հպեք հավելվածի կողքին՝ այն տեղափոխելու համար"</string> + <string name="letterbox_education_got_it" msgid="4057634570866051177">"Եղավ"</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 cb18762be48b..7963abf8972b 100644 --- a/libs/WindowManager/Shell/res/values-hy/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-hy/strings_tv.xml @@ -19,6 +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="158770205886688553">"Տեղափոխել"</string> + <string name="pip_expand" msgid="1051966011679297308">"Ծավալել"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Ծալել"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Կարգավորումների համար կրկնակի սեղմեք "<annotation icon="home_icon">"ԳԼԽԱՎՈՐ ԷԿՐԱՆ"</annotation></string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"«Նկար նկարի մեջ» ռեժիմի ընտրացանկ։"</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Տեղափոխել ձախ"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Տեղափոխել աջ"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Տեղափոխել վերև"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Տեղափոխել ներքև"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Պատրաստ է"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-in/strings.xml b/libs/WindowManager/Shell/res/values-in/strings.xml index 6432aeff4630..1b46b2fe2570 100644 --- a/libs/WindowManager/Shell/res/values-in/strings.xml +++ b/libs/WindowManager/Shell/res/values-in/strings.xml @@ -73,4 +73,12 @@ <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="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> + <string name="letterbox_education_dialog_title" msgid="6688664582871779215">"Beberapa aplikasi berfungsi paling baik dalam mode potret"</string> + <string name="letterbox_education_dialog_subtext" msgid="4853542518367719562">"Coba salah satu opsi berikut untuk mengoptimalkan area layar Anda"</string> + <string name="letterbox_education_screen_rotation_text" msgid="5085786687366339027">"Putar perangkat untuk tampilan layar penuh"</string> + <string name="letterbox_education_reposition_text" msgid="1068293354123934727">"Ketuk dua kali di samping aplikasi untuk mengubah posisinya"</string> + <string name="letterbox_education_got_it" msgid="4057634570866051177">"Oke"</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 8f3a28764b00..7d37154bb86c 100644 --- a/libs/WindowManager/Shell/res/values-in/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-in/strings_tv.xml @@ -19,6 +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="158770205886688553">"Pindahkan"</string> + <string name="pip_expand" msgid="1051966011679297308">"Luaskan"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Ciutkan"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Tekan dua kali "<annotation icon="home_icon">" HOME "</annotation>" untuk membuka kontrol"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Menu Picture-in-Picture."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Pindahkan ke kiri"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Pindahkan ke kanan"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Pindahkan ke atas"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Pindahkan ke bawah"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Selesai"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-is/strings.xml b/libs/WindowManager/Shell/res/values-is/strings.xml index 126d1f13ba03..a201c95137f3 100644 --- a/libs/WindowManager/Shell/res/values-is/strings.xml +++ b/libs/WindowManager/Shell/res/values-is/strings.xml @@ -73,4 +73,12 @@ <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="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> + <string name="letterbox_education_dialog_title" msgid="6688664582871779215">"Sum forrit virka best í skammsniði"</string> + <string name="letterbox_education_dialog_subtext" msgid="4853542518367719562">"Prófaðu einhvern af eftirfarandi valkostum til að nýta plássið sem best"</string> + <string name="letterbox_education_screen_rotation_text" msgid="5085786687366339027">"Snúðu tækinu til að nota allan skjáinn"</string> + <string name="letterbox_education_reposition_text" msgid="1068293354123934727">"Ýttu tvisvar við hlið forritsins til að færa það"</string> + <string name="letterbox_education_got_it" msgid="4057634570866051177">"Ég skil"</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 1f148d948a0e..1490cb98e034 100644 --- a/libs/WindowManager/Shell/res/values-is/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-is/strings_tv.xml @@ -19,6 +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="158770205886688553">"Færa"</string> + <string name="pip_expand" msgid="1051966011679297308">"Stækka"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Minnka"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Ýttu tvisvar á "<annotation icon="home_icon">" HEIM "</annotation>" til að opna stillingar"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Valmynd fyrir mynd í mynd."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Færa til vinstri"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Færa til hægri"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Færa upp"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Færa niður"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Lokið"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-it/strings.xml b/libs/WindowManager/Shell/res/values-it/strings.xml index e9a714485643..7157ed088d30 100644 --- a/libs/WindowManager/Shell/res/values-it/strings.xml +++ b/libs/WindowManager/Shell/res/values-it/strings.xml @@ -73,4 +73,12 @@ <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="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> + <string name="letterbox_education_dialog_title" msgid="6688664582871779215">"Alcune app funzionano in modo ottimale in verticale"</string> + <string name="letterbox_education_dialog_subtext" msgid="4853542518367719562">"Prova una di queste opzioni per ottimizzare lo spazio a tua disposizione"</string> + <string name="letterbox_education_screen_rotation_text" msgid="5085786687366339027">"Ruota il dispositivo per passare alla modalità a schermo intero"</string> + <string name="letterbox_education_reposition_text" msgid="1068293354123934727">"Tocca due volte accanto a un\'app per riposizionarla"</string> + <string name="letterbox_education_got_it" msgid="4057634570866051177">"OK"</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 127454cf28bf..a48516f2588e 100644 --- a/libs/WindowManager/Shell/res/values-it/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-it/strings_tv.xml @@ -19,6 +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="158770205886688553">"Sposta"</string> + <string name="pip_expand" msgid="1051966011679297308">"Espandi"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Comprimi"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Premi due volte "<annotation icon="home_icon">" HOME "</annotation>" per aprire i controlli"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Menu Picture in picture."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Sposta a sinistra"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Sposta a destra"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Sposta su"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Sposta giù"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Fine"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-iw/strings.xml b/libs/WindowManager/Shell/res/values-iw/strings.xml index b87d10c22965..52a6b0676222 100644 --- a/libs/WindowManager/Shell/res/values-iw/strings.xml +++ b/libs/WindowManager/Shell/res/values-iw/strings.xml @@ -73,4 +73,12 @@ <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="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> + <string name="letterbox_education_dialog_title" msgid="6688664582871779215">"חלק מהאפליקציות פועלות בצורה הטובה ביותר במצב תצוגה לאורך"</string> + <string name="letterbox_education_dialog_subtext" msgid="4853542518367719562">"כדי להפיק את המרב משטח המסך, ניתן לנסות את אחת מהאפשרויות האלה"</string> + <string name="letterbox_education_screen_rotation_text" msgid="5085786687366339027">"מסובבים את המכשיר כדי לעבור לתצוגה במסך מלא"</string> + <string name="letterbox_education_reposition_text" msgid="1068293354123934727">"מקישים הקשה כפולה ליד אפליקציה כדי למקם אותה מחדש"</string> + <string name="letterbox_education_got_it" msgid="4057634570866051177">"הבנתי"</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 ef98a9c41cf2..2af1896d3c67 100644 --- a/libs/WindowManager/Shell/res/values-iw/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-iw/strings_tv.xml @@ -19,6 +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="158770205886688553">"העברה"</string> + <string name="pip_expand" msgid="1051966011679297308">"הרחבה"</string> + <string name="pip_collapse" msgid="3903295106641385962">"כיווץ"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" לחיצה כפולה על "<annotation icon="home_icon">" הלחצן הראשי "</annotation>" תציג את אמצעי הבקרה"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"תפריט \'תמונה בתוך תמונה\'."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"הזזה שמאלה"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"הזזה ימינה"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"הזזה למעלה"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"הזזה למטה"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"סיום"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ja/strings.xml b/libs/WindowManager/Shell/res/values-ja/strings.xml index 51ffca6c0d2a..5a25c24ba034 100644 --- a/libs/WindowManager/Shell/res/values-ja/strings.xml +++ b/libs/WindowManager/Shell/res/values-ja/strings.xml @@ -73,4 +73,12 @@ <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="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> + <string name="letterbox_education_dialog_title" msgid="6688664582871779215">"アプリによっては縦向きにすると正常に動作します"</string> + <string name="letterbox_education_dialog_subtext" msgid="4853542518367719562">"スペースを最大限に活用するには、以下の方法のいずれかをお試しください"</string> + <string name="letterbox_education_screen_rotation_text" msgid="5085786687366339027">"全画面表示にするにはデバイスを回転させてください"</string> + <string name="letterbox_education_reposition_text" msgid="1068293354123934727">"位置を変えるにはアプリの横をダブルタップしてください"</string> + <string name="letterbox_education_got_it" msgid="4057634570866051177">"OK"</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 b7ab28c44fd2..bc7dcb7aa029 100644 --- a/libs/WindowManager/Shell/res/values-ja/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-ja/strings_tv.xml @@ -19,6 +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="158770205886688553">"移動"</string> + <string name="pip_expand" msgid="1051966011679297308">"開く"</string> + <string name="pip_collapse" msgid="3903295106641385962">"閉じる"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" コントロールにアクセス: "<annotation icon="home_icon">" ホーム "</annotation>" を 2 回押します"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"ピクチャー イン ピクチャーのメニューです。"</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"左に移動"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"右に移動"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"上に移動"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"下に移動"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"完了"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ka/strings.xml b/libs/WindowManager/Shell/res/values-ka/strings.xml index fc91d72179d3..bff86fa6ffe2 100644 --- a/libs/WindowManager/Shell/res/values-ka/strings.xml +++ b/libs/WindowManager/Shell/res/values-ka/strings.xml @@ -73,4 +73,12 @@ <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="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> + <string name="letterbox_education_dialog_title" msgid="6688664582871779215">"ზოგიერთი აპი უკეთ მუშაობს პორტრეტის რეჟიმში"</string> + <string name="letterbox_education_dialog_subtext" msgid="4853542518367719562">"გამოცადეთ ამ ვარიანტებიდან ერთ-ერთი, რათა მაქსიმალურად ისარგებლოთ თქვენი მეხსიერებით"</string> + <string name="letterbox_education_screen_rotation_text" msgid="5085786687366339027">"მოატრიალეთ თქვენი მოწყობილობა სრული ეკრანის გასაშლელად"</string> + <string name="letterbox_education_reposition_text" msgid="1068293354123934727">"ორმაგად შეეხეთ აპის გვერდითა სივრცეს, რათა ის სხვაგან გადაიტანოთ"</string> + <string name="letterbox_education_got_it" msgid="4057634570866051177">"გასაგებია"</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 1bf4b8ebdcda..898dac2aca88 100644 --- a/libs/WindowManager/Shell/res/values-ka/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-ka/strings_tv.xml @@ -19,6 +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="158770205886688553">"გადაადგილება"</string> + <string name="pip_expand" msgid="1051966011679297308">"გაშლა"</string> + <string name="pip_collapse" msgid="3903295106641385962">"ჩაკეცვა"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" მართვის საშუალებებზე წვდომისთვის ორმაგად დააჭირეთ "<annotation icon="home_icon">" მთავარ ღილაკს "</annotation></string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"მენიუ „ეკრანი ეკრანში“."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"მარცხნივ გადატანა"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"მარჯვნივ გადატანა"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"ზემოთ გადატანა"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"ქვემოთ გადატანა"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"მზადაა"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-kk/strings.xml b/libs/WindowManager/Shell/res/values-kk/strings.xml index 05a905dac69f..f57f3f581c85 100644 --- a/libs/WindowManager/Shell/res/values-kk/strings.xml +++ b/libs/WindowManager/Shell/res/values-kk/strings.xml @@ -73,4 +73,12 @@ <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="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> + <string name="letterbox_education_dialog_title" msgid="6688664582871779215">"Кейбір қолданба портреттік режимде жақсы жұмыс істейді"</string> + <string name="letterbox_education_dialog_subtext" msgid="4853542518367719562">"Экранды тиімді пайдалану үшін мына опциялардың бірін байқап көріңіз."</string> + <string name="letterbox_education_screen_rotation_text" msgid="5085786687366339027">"Толық экранға ауысу үшін құрылғыңызды бұрыңыз."</string> + <string name="letterbox_education_reposition_text" msgid="1068293354123934727">"Қолданбаның орнын ауыстыру үшін жанынан екі рет түртіңіз."</string> + <string name="letterbox_education_got_it" msgid="4057634570866051177">"Түсінікті"</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 8f1e725e79e2..cdf564fb4ca0 100644 --- a/libs/WindowManager/Shell/res/values-kk/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-kk/strings_tv.xml @@ -19,6 +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="158770205886688553">"Жылжыту"</string> + <string name="pip_expand" msgid="1051966011679297308">"Жаю"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Жию"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Басқару элементтері: "<annotation icon="home_icon">" НЕГІЗГІ ЭКРАН "</annotation>" түймесін екі рет басыңыз."</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"\"Сурет ішіндегі сурет\" мәзірі."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Солға жылжыту"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Оңға жылжыту"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Жоғары жылжыту"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Төмен жылжыту"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Дайын"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-km/strings.xml b/libs/WindowManager/Shell/res/values-km/strings.xml index 6a1cb2b2ca5d..5c04f881fe0e 100644 --- a/libs/WindowManager/Shell/res/values-km/strings.xml +++ b/libs/WindowManager/Shell/res/values-km/strings.xml @@ -73,4 +73,12 @@ <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="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> + <string name="letterbox_education_dialog_title" msgid="6688664582871779215">"កម្មវិធីមួយចំនួនដំណើរការបានប្រសើរបំផុតក្នុងទិសដៅបញ្ឈរ"</string> + <string name="letterbox_education_dialog_subtext" msgid="4853542518367719562">"សាកល្បងជម្រើសមួយក្នុងចំណោមទាំងនេះ ដើម្បីទទួលបានអត្ថប្រយោជន៍ច្រើនបំផុតពីកន្លែងទំនេររបស់អ្នក"</string> + <string name="letterbox_education_screen_rotation_text" msgid="5085786687366339027">"បង្វិលឧបករណ៍របស់អ្នក ដើម្បីចូលប្រើអេក្រង់ពេញ"</string> + <string name="letterbox_education_reposition_text" msgid="1068293354123934727">"ចុចពីរដងនៅជាប់កម្មវិធីណាមួយ ដើម្បីប្ដូរទីតាំងកម្មវិធីនោះ"</string> + <string name="letterbox_education_got_it" msgid="4057634570866051177">"យល់ហើយ"</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 b55997056e66..1a7ae813c1d3 100644 --- a/libs/WindowManager/Shell/res/values-km/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-km/strings_tv.xml @@ -19,6 +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="158770205886688553">"ផ្លាស់ទី"</string> + <string name="pip_expand" msgid="1051966011679297308">"ពង្រីក"</string> + <string name="pip_collapse" msgid="3903295106641385962">"បង្រួម"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" ចុចពីរដងលើ"<annotation icon="home_icon">"ប៊ូតុងដើម"</annotation>" ដើម្បីបើកផ្ទាំងគ្រប់គ្រង"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"ម៉ឺនុយរូបក្នុងរូប"</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"ផ្លាស់ទីទៅឆ្វេង"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"ផ្លាស់ទីទៅស្តាំ"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"ផ្លាស់ទីឡើងលើ"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"ផ្លាស់ទីចុះក្រោម"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"រួចរាល់"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-kn/strings.xml b/libs/WindowManager/Shell/res/values-kn/strings.xml index aecb54b96839..e91383caa009 100644 --- a/libs/WindowManager/Shell/res/values-kn/strings.xml +++ b/libs/WindowManager/Shell/res/values-kn/strings.xml @@ -73,4 +73,12 @@ <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="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> + <string name="letterbox_education_dialog_title" msgid="6688664582871779215">"ಕೆಲವು ಆ್ಯಪ್ಗಳು ಪೋರ್ಟ್ರೇಟ್ ಮೋಡ್ನಲ್ಲಿ ಅತ್ಯುತ್ತಮವಾಗಿ ಕಾರ್ಯನಿರ್ವಹಿಸುತ್ತವೆ"</string> + <string name="letterbox_education_dialog_subtext" msgid="4853542518367719562">"ನಿಮ್ಮ ಸ್ಥಳಾವಕಾಶದ ಅತಿಹೆಚ್ಚು ಪ್ರಯೋಜನ ಪಡೆಯಲು ಈ ಆಯ್ಕೆಗಳಲ್ಲಿ ಒಂದನ್ನು ಬಳಸಿ ನೋಡಿ"</string> + <string name="letterbox_education_screen_rotation_text" msgid="5085786687366339027">"ಪೂರ್ಣ ಸ್ಕ್ರೀನ್ಗೆ ಹೋಗಲು ನಿಮ್ಮ ಸಾಧನವನ್ನು ತಿರುಗಿಸಿ"</string> + <string name="letterbox_education_reposition_text" msgid="1068293354123934727">"ಆ್ಯಪ್ ಒಂದರ ಸ್ಥಾನವನ್ನು ಬದಲಾಯಿಸಲು ಅದರ ಪಕ್ಕದಲ್ಲಿ ಡಬಲ್-ಟ್ಯಾಪ್ ಮಾಡಿ"</string> + <string name="letterbox_education_got_it" msgid="4057634570866051177">"ಸರಿ"</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 9d3942fa4dd3..45de068c80a0 100644 --- a/libs/WindowManager/Shell/res/values-kn/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-kn/strings_tv.xml @@ -19,6 +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="158770205886688553">"ಸರಿಸಿ"</string> + <string name="pip_expand" msgid="1051966011679297308">"ವಿಸ್ತೃತಗೊಳಿಸಿ"</string> + <string name="pip_collapse" msgid="3903295106641385962">"ಕುಗ್ಗಿಸಿ"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" ಕಂಟ್ರೋಲ್ಗಳಿಗಾಗಿ "<annotation icon="home_icon">" ಹೋಮ್ "</annotation>" ಅನ್ನು ಎರಡು ಬಾರಿ ಒತ್ತಿ"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"ಚಿತ್ರದಲ್ಲಿ ಚಿತ್ರ ಮೆನು."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"ಎಡಕ್ಕೆ ಸರಿಸಿ"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"ಬಲಕ್ಕೆ ಸರಿಸಿ"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"ಮೇಲಕ್ಕೆ ಸರಿಸಿ"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"ಕೆಳಗೆ ಸರಿಸಿ"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"ಮುಗಿದಿದೆ"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ko/strings.xml b/libs/WindowManager/Shell/res/values-ko/strings.xml index 5af9ca2a0221..104ba3f22c96 100644 --- a/libs/WindowManager/Shell/res/values-ko/strings.xml +++ b/libs/WindowManager/Shell/res/values-ko/strings.xml @@ -73,4 +73,12 @@ <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="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> + <string name="letterbox_education_dialog_title" msgid="6688664582871779215">"일부 앱은 세로 모드에서 가장 잘 작동함"</string> + <string name="letterbox_education_dialog_subtext" msgid="4853542518367719562">"공간을 최대한 이용할 수 있도록 이 옵션 중 하나를 시도해 보세요."</string> + <string name="letterbox_education_screen_rotation_text" msgid="5085786687366339027">"전체 화면 모드로 전환하려면 기기를 회전하세요."</string> + <string name="letterbox_education_reposition_text" msgid="1068293354123934727">"앱 위치를 조정하려면 앱 옆을 두 번 탭하세요."</string> + <string name="letterbox_education_got_it" msgid="4057634570866051177">"확인"</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 46d6ad4e0b0f..9e8f1f1258a5 100644 --- a/libs/WindowManager/Shell/res/values-ko/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-ko/strings_tv.xml @@ -19,6 +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="158770205886688553">"이동"</string> + <string name="pip_expand" msgid="1051966011679297308">"펼치기"</string> + <string name="pip_collapse" msgid="3903295106641385962">"접기"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" 제어 메뉴에 액세스하려면 "<annotation icon="home_icon">" 홈 "</annotation>"을 두 번 누르세요."</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"PIP 모드 메뉴입니다."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"왼쪽으로 이동"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"오른쪽으로 이동"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"위로 이동"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"아래로 이동"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"완료"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ky/strings.xml b/libs/WindowManager/Shell/res/values-ky/strings.xml index 76f192ed0414..8203622a33fc 100644 --- a/libs/WindowManager/Shell/res/values-ky/strings.xml +++ b/libs/WindowManager/Shell/res/values-ky/strings.xml @@ -73,4 +73,12 @@ <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="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> + <string name="letterbox_education_dialog_title" msgid="6688664582871779215">"Айрым колдонмолорду тигинен иштетүү туура болот"</string> + <string name="letterbox_education_dialog_subtext" msgid="4853542518367719562">"Иш чөйрөсүнүн бардык мүмкүнчүлүктөрүн пайдалануу үчүн бул параметрлердин бирин колдонуп көрүңүз"</string> + <string name="letterbox_education_screen_rotation_text" msgid="5085786687366339027">"Толук экран режимине өтүү үчүн түзмөктү буруңуз"</string> + <string name="letterbox_education_reposition_text" msgid="1068293354123934727">"Колдонмонун ракурсун өзгөртүү үчүн анын тушуна эки жолу басыңыз"</string> + <string name="letterbox_education_got_it" msgid="4057634570866051177">"Түшүндүм"</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 d5d1d7ef914e..19fac5876bb0 100644 --- a/libs/WindowManager/Shell/res/values-ky/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-ky/strings_tv.xml @@ -19,6 +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="158770205886688553">"Жылдыруу"</string> + <string name="pip_expand" msgid="1051966011679297308">"Жайып көрсөтүү"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Жыйыштыруу"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Башкаруу элементтерин ачуу үчүн "<annotation icon="home_icon">" БАШКЫ БЕТ "</annotation>" баскычын эки жолу басыңыз"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Сүрөт ичиндеги сүрөт менюсу."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Солго жылдыруу"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Оңго жылдыруу"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Жогору жылдыруу"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Төмөн жылдыруу"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Бүттү"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-land/styles.xml b/libs/WindowManager/Shell/res/values-land/styles.xml index 0ed9368aa067..e89f65bef792 100644 --- a/libs/WindowManager/Shell/res/values-land/styles.xml +++ b/libs/WindowManager/Shell/res/values-land/styles.xml @@ -16,7 +16,7 @@ <resources xmlns:android="http://schemas.android.com/apk/res/android"> <style name="DockedDividerBackground"> - <item name="android:layout_width">10dp</item> + <item name="android:layout_width">@dimen/split_divider_bar_width</item> <item name="android:layout_height">match_parent</item> <item name="android:layout_gravity">center_horizontal</item> <item name="android:background">@color/split_divider_background</item> diff --git a/libs/WindowManager/Shell/res/values-lo/strings.xml b/libs/WindowManager/Shell/res/values-lo/strings.xml index 4ec6313f8c8c..24396786f9d8 100644 --- a/libs/WindowManager/Shell/res/values-lo/strings.xml +++ b/libs/WindowManager/Shell/res/values-lo/strings.xml @@ -73,4 +73,12 @@ <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="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> + <string name="letterbox_education_dialog_title" msgid="6688664582871779215">"ແອັບບາງຢ່າງເຮັດວຽກໄດ້ດີທີ່ສຸດໃນໂໝດລວງຕັ້ງ"</string> + <string name="letterbox_education_dialog_subtext" msgid="4853542518367719562">"ໃຫ້ລອງຕົວເລືອກໃດໜຶ່ງເຫຼົ່ານີ້ເພື່ອໃຊ້ປະໂຫຍດຈາກພື້ນທີ່ຂອງທ່ານໃຫ້ໄດ້ສູງສຸດ"</string> + <string name="letterbox_education_screen_rotation_text" msgid="5085786687366339027">"ໝຸນອຸປະກອນຂອງທ່ານເພື່ອໃຊ້ແບບເຕັມຈໍ"</string> + <string name="letterbox_education_reposition_text" msgid="1068293354123934727">"ແຕະສອງເທື່ອໃສ່ຖັດຈາກແອັບໃດໜຶ່ງເພື່ອຈັດຕຳແໜ່ງຂອງມັນຄືນໃໝ່"</string> + <string name="letterbox_education_got_it" msgid="4057634570866051177">"ເຂົ້າໃຈແລ້ວ"</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 f6362c120b9f..6cd0f37c516c 100644 --- a/libs/WindowManager/Shell/res/values-lo/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-lo/strings_tv.xml @@ -19,6 +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="158770205886688553">"ຍ້າຍ"</string> + <string name="pip_expand" msgid="1051966011679297308">"ຂະຫຍາຍ"</string> + <string name="pip_collapse" msgid="3903295106641385962">"ຫຍໍ້"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" ກົດ "<annotation icon="home_icon">" HOME "</annotation>" ສອງເທື່ອສຳລັບການຄວບຄຸມ"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"ເມນູການສະແດງຜົນຊ້ອນກັນ."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"ຍ້າຍໄປຊ້າຍ"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"ຍ້າຍໄປຂວາ"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"ຍ້າຍຂຶ້ນ"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"ຍ້າຍລົງ"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"ແລ້ວໆ"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-lt/strings.xml b/libs/WindowManager/Shell/res/values-lt/strings.xml index 8630e915aa09..e2ae643ad308 100644 --- a/libs/WindowManager/Shell/res/values-lt/strings.xml +++ b/libs/WindowManager/Shell/res/values-lt/strings.xml @@ -73,4 +73,12 @@ <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="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> + <string name="letterbox_education_dialog_title" msgid="6688664582871779215">"Kai kurios programos geriausiai veikia stačiuoju režimu"</string> + <string name="letterbox_education_dialog_subtext" msgid="4853542518367719562">"Pabandykite naudoti vieną iš šių parinkčių, kad išnaudotumėte visą vietą"</string> + <string name="letterbox_education_screen_rotation_text" msgid="5085786687366339027">"Pasukite įrenginį, kad įjungtumėte viso ekrano režimą"</string> + <string name="letterbox_education_reposition_text" msgid="1068293354123934727">"Dukart palieskite šalia programos, kad pakeistumėte jos poziciją"</string> + <string name="letterbox_education_got_it" msgid="4057634570866051177">"Supratau"</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 e4695a05f038..52017dca2b94 100644 --- a/libs/WindowManager/Shell/res/values-lt/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-lt/strings_tv.xml @@ -19,6 +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="158770205886688553">"Perkelti"</string> + <string name="pip_expand" msgid="1051966011679297308">"Išskleisti"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Sutraukti"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Jei reikia valdiklių, dukart paspauskite "<annotation icon="home_icon">"PAGRINDINIS"</annotation></string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Vaizdo vaizde meniu."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Perkelti kairėn"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Perkelti dešinėn"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Perkelti aukštyn"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Perkelti žemyn"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Atlikta"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-lv/strings.xml b/libs/WindowManager/Shell/res/values-lv/strings.xml index b095b88bfa0c..a77160bc262a 100644 --- a/libs/WindowManager/Shell/res/values-lv/strings.xml +++ b/libs/WindowManager/Shell/res/values-lv/strings.xml @@ -73,4 +73,12 @@ <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="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> + <string name="letterbox_education_dialog_title" msgid="6688664582871779215">"Dažas lietotnes vislabāk darbojas portreta režīmā"</string> + <string name="letterbox_education_dialog_subtext" msgid="4853542518367719562">"Izmēģiniet vienu no šīm iespējām, lai efektīvi izmantotu pieejamo vietu"</string> + <string name="letterbox_education_screen_rotation_text" msgid="5085786687366339027">"Pagrieziet ierīci, lai aktivizētu pilnekrāna režīmu"</string> + <string name="letterbox_education_reposition_text" msgid="1068293354123934727">"Veiciet dubultskārienu blakus lietotnei, lai manītu tās pozīciju"</string> + <string name="letterbox_education_got_it" msgid="4057634570866051177">"Labi"</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 f2b037fbeeee..11abac6f6197 100644 --- a/libs/WindowManager/Shell/res/values-lv/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-lv/strings_tv.xml @@ -19,6 +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="158770205886688553">"Pārvietot"</string> + <string name="pip_expand" msgid="1051966011679297308">"Izvērst"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Sakļaut"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Atvērt vadīklas: divreiz nospiediet pogu "<annotation icon="home_icon">"SĀKUMS"</annotation></string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Izvēlne attēlam attēlā."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Pārvietot pa kreisi"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Pārvietot pa labi"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Pārvietot augšup"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Pārvietot lejup"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Gatavs"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-mk/strings.xml b/libs/WindowManager/Shell/res/values-mk/strings.xml index 184fe9d52283..bac0c9eee4c2 100644 --- a/libs/WindowManager/Shell/res/values-mk/strings.xml +++ b/libs/WindowManager/Shell/res/values-mk/strings.xml @@ -73,4 +73,12 @@ <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="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> + <string name="letterbox_education_dialog_title" msgid="6688664582871779215">"Некои апликации најдобро работат во режим на портрет"</string> + <string name="letterbox_education_dialog_subtext" msgid="4853542518367719562">"Испробајте една од опцииве за да го извлечете максимумот од вашиот простор"</string> + <string name="letterbox_education_screen_rotation_text" msgid="5085786687366339027">"Ротирајте го уредот за да отворите на цел екран"</string> + <string name="letterbox_education_reposition_text" msgid="1068293354123934727">"Допрете двапати до некоја апликација за да ја преместите"</string> + <string name="letterbox_education_got_it" msgid="4057634570866051177">"Сфатив"</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 25dc764f4d5e..21293223b882 100644 --- a/libs/WindowManager/Shell/res/values-mk/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-mk/strings_tv.xml @@ -19,6 +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="158770205886688553">"Премести"</string> + <string name="pip_expand" msgid="1051966011679297308">"Прошири"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Собери"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Притиснете двапати на "<annotation icon="home_icon">" HOME "</annotation>" за контроли"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Мени за „Слика во слика“."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Премести налево"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Премести надесно"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Премести нагоре"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Премести надолу"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Готово"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ml/strings.xml b/libs/WindowManager/Shell/res/values-ml/strings.xml index f1bfe9aa055e..de0f837fcd3f 100644 --- a/libs/WindowManager/Shell/res/values-ml/strings.xml +++ b/libs/WindowManager/Shell/res/values-ml/strings.xml @@ -73,4 +73,12 @@ <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="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> + <string name="letterbox_education_dialog_title" msgid="6688664582871779215">"ചില ആപ്പുകൾ പോർട്രെയ്റ്റിൽ മികച്ച രീതിയിൽ പ്രവർത്തിക്കുന്നു"</string> + <string name="letterbox_education_dialog_subtext" msgid="4853542518367719562">"നിങ്ങളുടെ ഇടം പരമാവധി പ്രയോജനപ്പെടുത്താൻ ഈ ഓപ്ഷനുകളിലൊന്ന് പരീക്ഷിക്കുക"</string> + <string name="letterbox_education_screen_rotation_text" msgid="5085786687366339027">"പൂർണ്ണ സ്ക്രീനിലേക്ക് മാറാൻ ഈ ഉപകരണം റൊട്ടേറ്റ് ചെയ്യുക"</string> + <string name="letterbox_education_reposition_text" msgid="1068293354123934727">"ഒരു ആപ്പിന്റെ സ്ഥാനം മാറ്റാൻ, അതിന് തൊട്ടടുത്ത് ഡബിൾ ടാപ്പ് ചെയ്യുക"</string> + <string name="letterbox_education_got_it" msgid="4057634570866051177">"മനസ്സിലായി"</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 c74e0bbfaa5b..549e39b21101 100644 --- a/libs/WindowManager/Shell/res/values-ml/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-ml/strings_tv.xml @@ -19,6 +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="158770205886688553">"നീക്കുക"</string> + <string name="pip_expand" msgid="1051966011679297308">"വികസിപ്പിക്കുക"</string> + <string name="pip_collapse" msgid="3903295106641385962">"ചുരുക്കുക"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" നിയന്ത്രണങ്ങൾക്കായി "<annotation icon="home_icon">" ഹോം "</annotation>" രണ്ട് തവണ അമർത്തുക"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"ചിത്രത്തിനുള്ളിൽ ചിത്രം മെനു."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"ഇടത്തേക്ക് നീക്കുക"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"വലത്തേക്ക് നീക്കുക"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"മുകളിലേക്ക് നീക്കുക"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"താഴേക്ക് നീക്കുക"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"പൂർത്തിയായി"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-mn/strings.xml b/libs/WindowManager/Shell/res/values-mn/strings.xml index 8b8cb95d1e9b..1205306e0833 100644 --- a/libs/WindowManager/Shell/res/values-mn/strings.xml +++ b/libs/WindowManager/Shell/res/values-mn/strings.xml @@ -73,4 +73,12 @@ <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="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> + <string name="letterbox_education_dialog_title" msgid="6688664582871779215">"Зарим апп нь босоо чиглэлд хамгийн сайн ажилладаг"</string> + <string name="letterbox_education_dialog_subtext" msgid="4853542518367719562">"Орон зайгаа сайтар ашиглахын тулд эдгээр сонголтуудын аль нэгийг туршиж үзээрэй"</string> + <string name="letterbox_education_screen_rotation_text" msgid="5085786687366339027">"Төхөөрөмжөө бүтэн дэлгэцээр үзэхийн тулд эргүүлнэ"</string> + <string name="letterbox_education_reposition_text" msgid="1068293354123934727">"Аппыг дахин байрлуулахын тулд хажууд нь хоёр товшино"</string> + <string name="letterbox_education_got_it" msgid="4057634570866051177">"Ойлголоо"</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 55519d462b69..9a85d96ca602 100644 --- a/libs/WindowManager/Shell/res/values-mn/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-mn/strings_tv.xml @@ -19,6 +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="158770205886688553">"Зөөх"</string> + <string name="pip_expand" msgid="1051966011679297308">"Дэлгэх"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Хураах"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Хяналтад хандах бол "<annotation icon="home_icon">" HOME "</annotation>" дээр хоёр дарна уу"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Дэлгэцэн доторх дэлгэцийн цэс."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Зүүн тийш зөөх"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Баруун тийш зөөх"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Дээш зөөх"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Доош зөөх"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Болсон"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-mr/strings.xml b/libs/WindowManager/Shell/res/values-mr/strings.xml index c11af7bf9f2c..c91d06fdf3d5 100644 --- a/libs/WindowManager/Shell/res/values-mr/strings.xml +++ b/libs/WindowManager/Shell/res/values-mr/strings.xml @@ -73,4 +73,12 @@ <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="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> + <string name="letterbox_education_dialog_title" msgid="6688664582871779215">"काही ॲप्स पोर्ट्रेटमध्ये सर्वोत्तम काम करतात"</string> + <string name="letterbox_education_dialog_subtext" msgid="4853542518367719562">"तुमच्या स्पेसचा पुरेपूर वापर करण्यासाठी, यांपैकी एक पर्याय वापरून पहा"</string> + <string name="letterbox_education_screen_rotation_text" msgid="5085786687366339027">"फुल स्क्रीन करण्यासाठी, तुमचे डिव्हाइस फिरवा"</string> + <string name="letterbox_education_reposition_text" msgid="1068293354123934727">"ॲपची स्थिती पुन्हा बदलण्यासाठी, त्याच्या शेजारी दोनदा टॅप करा"</string> + <string name="letterbox_education_got_it" msgid="4057634570866051177">"समजले"</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 ad2cfc6035c2..a9779b3a3e89 100644 --- a/libs/WindowManager/Shell/res/values-mr/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-mr/strings_tv.xml @@ -19,6 +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="158770205886688553">"हलवा"</string> + <string name="pip_expand" msgid="1051966011679297308">"विस्तार करा"</string> + <string name="pip_collapse" msgid="3903295106641385962">"कोलॅप्स करा"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" नियंत्रणांसाठी "<annotation icon="home_icon">" होम "</annotation>" दोनदा दाबा"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"चित्रात-चित्र मेनू."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"डावीकडे हलवा"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"उजवीकडे हलवा"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"वर हलवा"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"खाली हलवा"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"पूर्ण झाले"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ms/strings.xml b/libs/WindowManager/Shell/res/values-ms/strings.xml index 5493ce5a4fab..652a9919d163 100644 --- a/libs/WindowManager/Shell/res/values-ms/strings.xml +++ b/libs/WindowManager/Shell/res/values-ms/strings.xml @@ -73,4 +73,12 @@ <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="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> + <string name="letterbox_education_dialog_title" msgid="6688664582871779215">"Sesetengah apl berfungsi paling baik dalam mod potret"</string> + <string name="letterbox_education_dialog_subtext" msgid="4853542518367719562">"Cuba salah satu daripada pilihan ini untuk memanfaatkan ruang anda sepenuhnya"</string> + <string name="letterbox_education_screen_rotation_text" msgid="5085786687366339027">"Putar peranti anda untuk beralih ke skrin penuh"</string> + <string name="letterbox_education_reposition_text" msgid="1068293354123934727">"Ketik dua kali bersebelahan apl untuk menempatkan semula apl"</string> + <string name="letterbox_education_got_it" msgid="4057634570866051177">"OK"</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 b2d7214381ef..8fe992d9f3b9 100644 --- a/libs/WindowManager/Shell/res/values-ms/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-ms/strings_tv.xml @@ -19,6 +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="158770205886688553">"Alih"</string> + <string name="pip_expand" msgid="1051966011679297308">"Kembangkan"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Kuncupkan"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Tekan dua kali "<annotation icon="home_icon">" LAMAN UTAMA "</annotation>" untuk mengakses kawalan"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Menu Gambar dalam Gambar."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Alih ke kiri"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Alih ke kanan"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Alih ke atas"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Alih ke bawah"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Selesai"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-my/strings.xml b/libs/WindowManager/Shell/res/values-my/strings.xml index e1d17f88fb9b..15d182c6451e 100644 --- a/libs/WindowManager/Shell/res/values-my/strings.xml +++ b/libs/WindowManager/Shell/res/values-my/strings.xml @@ -73,4 +73,12 @@ <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="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> + <string name="letterbox_education_dialog_title" msgid="6688664582871779215">"အချို့အက်ပ်များသည် ဒေါင်လိုက်တွင် အကောင်းဆုံးလုပ်ဆောင်သည်"</string> + <string name="letterbox_education_dialog_subtext" msgid="4853542518367719562">"သင့်နေရာကို အကောင်းဆုံးအသုံးပြုနိုင်ရန် ဤရွေးစရာများထဲမှ တစ်ခုကို စမ်းကြည့်ပါ"</string> + <string name="letterbox_education_screen_rotation_text" msgid="5085786687366339027">"ဖန်သားပြင်အပြည့်လုပ်ရန် သင့်စက်ကို လှည့်နိုင်သည်"</string> + <string name="letterbox_education_reposition_text" msgid="1068293354123934727">"အက်ပ်နေရာပြန်ချရန် ၎င်းဘေးတွင် နှစ်ချက်တို့နိုင်သည်"</string> + <string name="letterbox_education_got_it" msgid="4057634570866051177">"ရပြီ"</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 c18d53932163..105628d8149e 100644 --- a/libs/WindowManager/Shell/res/values-my/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-my/strings_tv.xml @@ -19,6 +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="158770205886688553">"ရွှေ့ရန်"</string> + <string name="pip_expand" msgid="1051966011679297308">"ချဲ့ရန်"</string> + <string name="pip_collapse" msgid="3903295106641385962">"လျှော့ပြရန်"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" ထိန်းချုပ်မှုအတွက် "<annotation icon="home_icon">" ပင်မခလုတ် "</annotation>" နှစ်ချက်နှိပ်ပါ"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"နှစ်ခုထပ်၍ ကြည့်ခြင်းမီနူး။"</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"ဘယ်သို့ရွှေ့ရန်"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"ညာသို့ရွှေ့ရန်"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"အပေါ်သို့ရွှေ့ရန်"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"အောက်သို့ရွှေ့ရန်"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"ပြီးပြီ"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-nb/strings.xml b/libs/WindowManager/Shell/res/values-nb/strings.xml index 3ee28500c115..2f2fea6eb833 100644 --- a/libs/WindowManager/Shell/res/values-nb/strings.xml +++ b/libs/WindowManager/Shell/res/values-nb/strings.xml @@ -73,4 +73,12 @@ <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="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> + <string name="letterbox_education_dialog_title" msgid="6688664582871779215">"Noen apper fungerer best i stående format"</string> + <string name="letterbox_education_dialog_subtext" msgid="4853542518367719562">"Prøv et av disse alternativene for å få mest mulig ut av plassen din"</string> + <string name="letterbox_education_screen_rotation_text" msgid="5085786687366339027">"Roter enheten for å starte fullskjerm"</string> + <string name="letterbox_education_reposition_text" msgid="1068293354123934727">"Dobbelttrykk ved siden av en app for å flytte den"</string> + <string name="letterbox_education_got_it" msgid="4057634570866051177">"Greit"</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 8a7f315606ad..ca63518df7a5 100644 --- a/libs/WindowManager/Shell/res/values-nb/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-nb/strings_tv.xml @@ -19,6 +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="158770205886688553">"Flytt"</string> + <string name="pip_expand" msgid="1051966011679297308">"Vis"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Skjul"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Dobbelttrykk på "<annotation icon="home_icon">"HJEM"</annotation>" for å åpne kontroller"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Bilde-i-bilde-meny."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Flytt til venstre"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Flytt til høyre"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Flytt opp"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Flytt ned"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Ferdig"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ne/strings.xml b/libs/WindowManager/Shell/res/values-ne/strings.xml index 12df158f0903..8dfec88cc29d 100644 --- a/libs/WindowManager/Shell/res/values-ne/strings.xml +++ b/libs/WindowManager/Shell/res/values-ne/strings.xml @@ -73,4 +73,12 @@ <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="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> + <string name="letterbox_education_dialog_title" msgid="6688664582871779215">"केही एपहरूले पोर्ट्रेटमा राम्रोसँग काम गर्छन्"</string> + <string name="letterbox_education_dialog_subtext" msgid="4853542518367719562">"तपाईं स्क्रिनको अधिकतम ठाउँ प्रयोग गर्न चाहनुहुन्छ भने यीमध्ये कुनै विकल्प प्रयोग गरी हेर्नुहोस्"</string> + <string name="letterbox_education_screen_rotation_text" msgid="5085786687366339027">"तपाईं फुल स्क्रिन मोड हेर्न चाहनुहुन्छ भने आफ्नो डिभाइस रोटेट गर्नुहोस्"</string> + <string name="letterbox_education_reposition_text" msgid="1068293354123934727">"तपाईं जुन एपको स्थिति मिलाउन चाहनुहुन्छ सोही एपको छेउमा डबल ट्याप गर्नुहोस्"</string> + <string name="letterbox_education_got_it" msgid="4057634570866051177">"बुझेँ"</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 87fa3279f05e..7cbf9e294e7b 100644 --- a/libs/WindowManager/Shell/res/values-ne/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-ne/strings_tv.xml @@ -19,6 +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="158770205886688553">"सार्नुहोस्"</string> + <string name="pip_expand" msgid="1051966011679297308">"एक्स्पान्ड गर्नुहोस्"</string> + <string name="pip_collapse" msgid="3903295106641385962">"कोल्याप्स गर्नुहोस्"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" कन्ट्रोल मेनु खोल्न "<annotation icon="home_icon">" होम "</annotation>" बटन दुई पटक थिच्नुहोस्"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"\"picture-in-picture\" मेनु।"</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"बायाँतिर सार्नुहोस्"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"दायाँतिर सार्नुहोस्"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"माथितिर सार्नुहोस्"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"तलतिर सार्नुहोस्"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"सम्पन्न भयो"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-nl/strings.xml b/libs/WindowManager/Shell/res/values-nl/strings.xml index f83ad22dfea7..8468b04c66da 100644 --- a/libs/WindowManager/Shell/res/values-nl/strings.xml +++ b/libs/WindowManager/Shell/res/values-nl/strings.xml @@ -73,4 +73,12 @@ <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="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> + <string name="letterbox_education_dialog_title" msgid="6688664582871779215">"Sommige apps werken het best in de staande stand"</string> + <string name="letterbox_education_dialog_subtext" msgid="4853542518367719562">"Probeer een van deze opties om optimaal gebruik te maken van je ruimte"</string> + <string name="letterbox_education_screen_rotation_text" msgid="5085786687366339027">"Draai je apparaat om naar volledig scherm te schakelen"</string> + <string name="letterbox_education_reposition_text" msgid="1068293354123934727">"Dubbeltik naast een app om deze opnieuw te positioneren"</string> + <string name="letterbox_education_got_it" msgid="4057634570866051177">"OK"</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 df3809e5d6c6..2deaeddc4080 100644 --- a/libs/WindowManager/Shell/res/values-nl/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-nl/strings_tv.xml @@ -19,6 +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="158770205886688553">"Verplaatsen"</string> + <string name="pip_expand" msgid="1051966011679297308">"Uitvouwen"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Samenvouwen"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Druk twee keer op "<annotation icon="home_icon">" HOME "</annotation>" voor bedieningselementen"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Scherm-in-scherm-menu."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Naar links verplaatsen"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Naar rechts verplaatsen"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Omhoog verplaatsen"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Omlaag verplaatsen"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Klaar"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-or/strings.xml b/libs/WindowManager/Shell/res/values-or/strings.xml index 14f92c8f1d1c..a8d8448edf99 100644 --- a/libs/WindowManager/Shell/res/values-or/strings.xml +++ b/libs/WindowManager/Shell/res/values-or/strings.xml @@ -73,4 +73,12 @@ <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="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> + <string name="letterbox_education_dialog_title" msgid="6688664582871779215">"କିଛି ଆପ ପୋର୍ଟ୍ରେଟରେ ସବୁଠାରୁ ଭଲ କାମ କରେ"</string> + <string name="letterbox_education_dialog_subtext" msgid="4853542518367719562">"ଆପଣଙ୍କ ସ୍ପେସରୁ ଅଧିକ ଲାଭ ପାଇବାକୁ ଏହି ବିକଳ୍ପଗୁଡ଼ିକ ମଧ୍ୟରୁ ଗୋଟିଏ ବ୍ୟବହାର କରି ଦେଖନ୍ତୁ"</string> + <string name="letterbox_education_screen_rotation_text" msgid="5085786687366339027">"ପୂର୍ଣ୍ଣ-ସ୍କ୍ରିନ ବ୍ୟବହାର କରିବାକୁ ଆପଣଙ୍କ ଡିଭାଇସକୁ ରୋଟେଟ କରନ୍ତୁ"</string> + <string name="letterbox_education_reposition_text" msgid="1068293354123934727">"ଏକ ଆପକୁ ରିପୋଜିସନ କରିବା ପାଇଁ ଏହା ପାଖରେ ଦୁଇଥର-ଟାପ କରନ୍ତୁ"</string> + <string name="letterbox_education_got_it" msgid="4057634570866051177">"ବୁଝିଗଲି"</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 295a5c4ee1ce..0c1d99e4ca71 100644 --- a/libs/WindowManager/Shell/res/values-or/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-or/strings_tv.xml @@ -19,6 +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="158770205886688553">"ମୁଭ କରନ୍ତୁ"</string> + <string name="pip_expand" msgid="1051966011679297308">"ବିସ୍ତାର କରନ୍ତୁ"</string> + <string name="pip_collapse" msgid="3903295106641385962">"ସଙ୍କୁଚିତ କରନ୍ତୁ"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" ନିୟନ୍ତ୍ରଣଗୁଡ଼ିକ ପାଇଁ "<annotation icon="home_icon">" ହୋମ ବଟନ "</annotation>"କୁ ଦୁଇଥର ଦବାନ୍ତୁ"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"ପିକଚର-ଇନ-ପିକଚର ମେନୁ।"</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"ବାମକୁ ମୁଭ କରନ୍ତୁ"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"ଡାହାଣକୁ ମୁଭ କରନ୍ତୁ"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"ଉପରକୁ ମୁଭ କରନ୍ତୁ"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"ତଳକୁ ମୁଭ କରନ୍ତୁ"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"ହୋଇଗଲା"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-pa/strings.xml b/libs/WindowManager/Shell/res/values-pa/strings.xml index e09f53adba43..f99176cb682d 100644 --- a/libs/WindowManager/Shell/res/values-pa/strings.xml +++ b/libs/WindowManager/Shell/res/values-pa/strings.xml @@ -73,4 +73,12 @@ <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="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> + <string name="letterbox_education_dialog_title" msgid="6688664582871779215">"ਕੁਝ ਐਪਾਂ ਪੋਰਟਰੇਟ ਵਿੱਚ ਬਿਹਤਰ ਕੰਮ ਕਰਦੀਆਂ ਹਨ"</string> + <string name="letterbox_education_dialog_subtext" msgid="4853542518367719562">"ਆਪਣੀ ਜਗ੍ਹਾ ਦਾ ਵੱਧ ਤੋਂ ਵੱਧ ਲਾਹਾ ਲੈਣ ਲਈ ਇਨ੍ਹਾਂ ਵਿਕਲਪਾਂ ਵਿੱਚੋਂ ਕੋਈ ਇੱਕ ਵਰਤ ਕੇ ਦੇਖੋ"</string> + <string name="letterbox_education_screen_rotation_text" msgid="5085786687366339027">"ਪੂਰੀ-ਸਕ੍ਰੀਨ ਮੋਡ \'ਤੇ ਜਾਣ ਲਈ ਆਪਣੇ ਡੀਵਾਈਸ ਨੂੰ ਘੁਮਾਓ"</string> + <string name="letterbox_education_reposition_text" msgid="1068293354123934727">"ਕਿਸੇ ਐਪ ਦੀ ਜਗ੍ਹਾ ਬਦਲਣ ਲਈ ਉਸ ਦੇ ਅੱਗੇ ਡਬਲ ਟੈਪ ਕਰੋ"</string> + <string name="letterbox_education_got_it" msgid="4057634570866051177">"ਸਮਝ ਲਿਆ"</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 e32895a9a239..a1edde738775 100644 --- a/libs/WindowManager/Shell/res/values-pa/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-pa/strings_tv.xml @@ -19,6 +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="158770205886688553">"ਲਿਜਾਓ"</string> + <string name="pip_expand" msgid="1051966011679297308">"ਵਿਸਤਾਰ ਕਰੋ"</string> + <string name="pip_collapse" msgid="3903295106641385962">"ਸਮੇਟੋ"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" ਕੰਟਰੋਲਾਂ ਲਈ "<annotation icon="home_icon">" ਹੋਮ ਬਟਨ "</annotation>" ਨੂੰ ਦੋ ਵਾਰ ਦਬਾਓ"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"ਤਸਵੀਰ-ਵਿੱਚ-ਤਸਵੀਰ ਮੀਨੂ।"</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"ਖੱਬੇ ਲਿਜਾਓ"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"ਸੱਜੇ ਲਿਜਾਓ"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"ਉੱਪਰ ਲਿਜਾਓ"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"ਹੇਠਾਂ ਲਿਜਾਓ"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"ਹੋ ਗਿਆ"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-pl/strings.xml b/libs/WindowManager/Shell/res/values-pl/strings.xml index a2ef9975e487..f2147c04d335 100644 --- a/libs/WindowManager/Shell/res/values-pl/strings.xml +++ b/libs/WindowManager/Shell/res/values-pl/strings.xml @@ -73,4 +73,12 @@ <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="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> + <string name="letterbox_education_dialog_title" msgid="6688664582871779215">"Niektóre aplikacje działają najlepiej w orientacji pionowej"</string> + <string name="letterbox_education_dialog_subtext" msgid="4853542518367719562">"Wypróbuj jedną z tych opcji, aby jak najlepiej wykorzystać miejsce"</string> + <string name="letterbox_education_screen_rotation_text" msgid="5085786687366339027">"Obróć urządzenie, aby przejść do pełnego ekranu"</string> + <string name="letterbox_education_reposition_text" msgid="1068293354123934727">"Kliknij dwukrotnie obok aplikacji, aby ją przenieść"</string> + <string name="letterbox_education_got_it" msgid="4057634570866051177">"OK"</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 286fd7b2ff0f..2bb90addc241 100644 --- a/libs/WindowManager/Shell/res/values-pl/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-pl/strings_tv.xml @@ -19,6 +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="158770205886688553">"Przenieś"</string> + <string name="pip_expand" msgid="1051966011679297308">"Rozwiń"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Zwiń"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Naciśnij dwukrotnie "<annotation icon="home_icon">"EKRAN GŁÓWNY"</annotation>", aby wyświetlić ustawienia"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Menu funkcji Obraz w obrazie."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Przenieś w lewo"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Przenieś w prawo"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Przenieś w górę"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Przenieś w dół"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Gotowe"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-pt-rBR/strings.xml b/libs/WindowManager/Shell/res/values-pt-rBR/strings.xml index 1300e530c0a6..2efc5543dd87 100644 --- a/libs/WindowManager/Shell/res/values-pt-rBR/strings.xml +++ b/libs/WindowManager/Shell/res/values-pt-rBR/strings.xml @@ -73,4 +73,12 @@ <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="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> + <string name="letterbox_education_dialog_title" msgid="6688664582871779215">"Alguns apps funcionam melhor em modo retrato"</string> + <string name="letterbox_education_dialog_subtext" msgid="4853542518367719562">"Tente uma destas opções para aproveitar seu espaço ao máximo"</string> + <string name="letterbox_education_screen_rotation_text" msgid="5085786687366339027">"Gire o dispositivo para entrar no modo de tela cheia"</string> + <string name="letterbox_education_reposition_text" msgid="1068293354123934727">"Toque duas vezes ao lado de um app para reposicionar"</string> + <string name="letterbox_education_got_it" msgid="4057634570866051177">"Entendi"</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 57edcdf74cf4..14d1c34fd3e8 100644 --- a/libs/WindowManager/Shell/res/values-pt-rBR/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-pt-rBR/strings_tv.xml @@ -19,6 +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="158770205886688553">"Mover"</string> + <string name="pip_expand" msgid="1051966011679297308">"Abrir"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Fechar"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Pressione o botão "<annotation icon="home_icon">"home"</annotation>" duas vezes para acessar os controles"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Menu do picture-in-picture"</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Mover para a esquerda"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Mover para a direita"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Mover para cima"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Mover para baixo"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Concluído"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-pt-rPT/strings.xml b/libs/WindowManager/Shell/res/values-pt-rPT/strings.xml index f3314f80cdfe..c68a6934dead 100644 --- a/libs/WindowManager/Shell/res/values-pt-rPT/strings.xml +++ b/libs/WindowManager/Shell/res/values-pt-rPT/strings.xml @@ -73,4 +73,12 @@ <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="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> + <string name="letterbox_education_dialog_title" msgid="6688664582871779215">"Algumas apps funcionam melhor no modo vertical"</string> + <string name="letterbox_education_dialog_subtext" msgid="4853542518367719562">"Experimente uma destas opções para aproveitar ao máximo o seu espaço"</string> + <string name="letterbox_education_screen_rotation_text" msgid="5085786687366339027">"Rode o dispositivo para ficar em ecrã inteiro"</string> + <string name="letterbox_education_reposition_text" msgid="1068293354123934727">"Toque duas vezes junto a uma app para a reposicionar"</string> + <string name="letterbox_education_got_it" msgid="4057634570866051177">"OK"</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 9372e0f637cb..1ada4508714a 100644 --- a/libs/WindowManager/Shell/res/values-pt-rPT/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-pt-rPT/strings_tv.xml @@ -19,6 +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="158770205886688553">"Mover"</string> + <string name="pip_expand" msgid="1051966011679297308">"Expandir"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Reduzir"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Prima duas vezes "<annotation icon="home_icon">" PÁGINA INICIAL "</annotation>" para controlos"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Menu de ecrã no ecrã."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Mover para a esquerda"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Mover para a direita"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Mover para cima"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Mover para baixo"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Concluído"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-pt/strings.xml b/libs/WindowManager/Shell/res/values-pt/strings.xml index 1300e530c0a6..2efc5543dd87 100644 --- a/libs/WindowManager/Shell/res/values-pt/strings.xml +++ b/libs/WindowManager/Shell/res/values-pt/strings.xml @@ -73,4 +73,12 @@ <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="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> + <string name="letterbox_education_dialog_title" msgid="6688664582871779215">"Alguns apps funcionam melhor em modo retrato"</string> + <string name="letterbox_education_dialog_subtext" msgid="4853542518367719562">"Tente uma destas opções para aproveitar seu espaço ao máximo"</string> + <string name="letterbox_education_screen_rotation_text" msgid="5085786687366339027">"Gire o dispositivo para entrar no modo de tela cheia"</string> + <string name="letterbox_education_reposition_text" msgid="1068293354123934727">"Toque duas vezes ao lado de um app para reposicionar"</string> + <string name="letterbox_education_got_it" msgid="4057634570866051177">"Entendi"</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 57edcdf74cf4..14d1c34fd3e8 100644 --- a/libs/WindowManager/Shell/res/values-pt/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-pt/strings_tv.xml @@ -19,6 +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="158770205886688553">"Mover"</string> + <string name="pip_expand" msgid="1051966011679297308">"Abrir"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Fechar"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Pressione o botão "<annotation icon="home_icon">"home"</annotation>" duas vezes para acessar os controles"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Menu do picture-in-picture"</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Mover para a esquerda"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Mover para a direita"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Mover para cima"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Mover para baixo"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Concluído"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ro/strings.xml b/libs/WindowManager/Shell/res/values-ro/strings.xml index 01f96c881b7e..05942d368647 100644 --- a/libs/WindowManager/Shell/res/values-ro/strings.xml +++ b/libs/WindowManager/Shell/res/values-ro/strings.xml @@ -17,20 +17,20 @@ <resources xmlns:android="http://schemas.android.com/apk/res/android" xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> - <string name="pip_phone_close" msgid="5783752637260411309">"Închideț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_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="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> <string name="dock_forced_resizable" msgid="1749750436092293116">"Este posibil ca aplicația să nu funcționeze cu ecranul împărțit."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Aplicația nu acceptă ecranul împărțit."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Este posibil ca aplicația să nu funcționeze pe un ecran secundar."</string> @@ -47,30 +47,38 @@ <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="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_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="restart_button_description" msgid="5887656107651190519">"Atinge ca să repornești aplicația și să treci în modul ecran complet."</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="6688664582871779215">"Unele aplicații funcționează cel mai bine în orientarea portret"</string> + <string name="letterbox_education_dialog_subtext" msgid="4853542518367719562">"Încearcă una dintre aceste opțiuni pentru a profita din plin de spațiu"</string> + <string name="letterbox_education_screen_rotation_text" msgid="5085786687366339027">"Rotește dispozitivul pentru a trece în modul ecran complet"</string> + <string name="letterbox_education_reposition_text" msgid="1068293354123934727">"Atinge de două ori lângă o aplicație pentru a o repoziționa"</string> + <string name="letterbox_education_got_it" msgid="4057634570866051177">"OK"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ro/strings_tv.xml b/libs/WindowManager/Shell/res/values-ro/strings_tv.xml index 9438e4955b68..b5245ffbf0bc 100644 --- a/libs/WindowManager/Shell/res/values-ro/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-ro/strings_tv.xml @@ -19,6 +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="158770205886688553">"Mută"</string> + <string name="pip_expand" msgid="1051966011679297308">"Extinde"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Restrânge"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Apasă de două ori "<annotation icon="home_icon">"butonul ecran de pornire"</annotation>" pentru comenzi"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Meniu picture-in-picture."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Mută 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 6a0e9c12fe7f..95bf1cf11435 100644 --- a/libs/WindowManager/Shell/res/values-ru/strings.xml +++ b/libs/WindowManager/Shell/res/values-ru/strings.xml @@ -73,4 +73,12 @@ <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="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> + <string name="letterbox_education_dialog_title" msgid="6688664582871779215">"Некоторые приложения лучше работают в вертикальном режиме"</string> + <string name="letterbox_education_dialog_subtext" msgid="4853542518367719562">"Чтобы эффективно использовать экранное пространство, выполните одно из следующих действий:"</string> + <string name="letterbox_education_screen_rotation_text" msgid="5085786687366339027">"Чтобы перейти в полноэкранный режим, поверните устройство."</string> + <string name="letterbox_education_reposition_text" msgid="1068293354123934727">"Чтобы переместить приложение, нажмите на него дважды."</string> + <string name="letterbox_education_got_it" msgid="4057634570866051177">"ОК"</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 24785aa7e184..e7f55ec1bc57 100644 --- a/libs/WindowManager/Shell/res/values-ru/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-ru/strings_tv.xml @@ -19,6 +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="158770205886688553">"Переместить"</string> + <string name="pip_expand" msgid="1051966011679297308">"Развернуть"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Свернуть"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Элементы управления: дважды нажмите "<annotation icon="home_icon">" кнопку главного экрана "</annotation></string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Меню \"Картинка в картинке\"."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Переместить влево"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Переместить вправо"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Переместить вверх"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Переместить вниз"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Готово"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-si/strings.xml b/libs/WindowManager/Shell/res/values-si/strings.xml index d7ed24606f08..23dd65ad7b31 100644 --- a/libs/WindowManager/Shell/res/values-si/strings.xml +++ b/libs/WindowManager/Shell/res/values-si/strings.xml @@ -73,4 +73,12 @@ <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="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> + <string name="letterbox_education_dialog_title" msgid="6688664582871779215">"සමහර යෙදුම් ප්රතිමූර්තිය තුළ හොඳින්ම ක්රියා කරයි"</string> + <string name="letterbox_education_dialog_subtext" msgid="4853542518367719562">"ඔබගේ ඉඩෙන් උපරිම ප්රයෝජන ගැනීමට මෙම විකල්පවලින් එකක් උත්සාහ කරන්න"</string> + <string name="letterbox_education_screen_rotation_text" msgid="5085786687366339027">"සම්පූර්ණ තිරයට යාමට ඔබගේ උපාංගය කරකවන්න"</string> + <string name="letterbox_education_reposition_text" msgid="1068293354123934727">"එය නැවත ස්ථානගත කිරීමට යෙදුමකට යාබදව දෙවරක් තට්ටු කරන්න"</string> + <string name="letterbox_education_got_it" msgid="4057634570866051177">"තේරුණා"</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 62ee6d4f44d2..5478ce5d3d40 100644 --- a/libs/WindowManager/Shell/res/values-si/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-si/strings_tv.xml @@ -19,6 +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="158770205886688553">"ගෙන යන්න"</string> + <string name="pip_expand" msgid="1051966011679297308">"දිග හරින්න"</string> + <string name="pip_collapse" msgid="3903295106641385962">"හකුළන්න"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" පාලන සඳහා "<annotation icon="home_icon">" මුල් පිටුව "</annotation>" දෙවරක් ඔබන්න"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"පින්තූරය තුළ පින්තූරය මෙනුව"</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"වමට ගෙන යන්න"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"දකුණට ගෙන යන්න"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"ඉහළට ගෙන යන්න"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"පහළට ගෙන යන්න"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"නිමයි"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-sk/strings.xml b/libs/WindowManager/Shell/res/values-sk/strings.xml index 13fd58f801ea..a231cacefb20 100644 --- a/libs/WindowManager/Shell/res/values-sk/strings.xml +++ b/libs/WindowManager/Shell/res/values-sk/strings.xml @@ -73,4 +73,12 @@ <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="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> + <string name="letterbox_education_dialog_title" msgid="6688664582871779215">"Niektoré aplikácie fungujú najlepšie v režime na výšku"</string> + <string name="letterbox_education_dialog_subtext" msgid="4853542518367719562">"Vyskúšajte jednu z týchto možností a využívajte svoj priestor naplno"</string> + <string name="letterbox_education_screen_rotation_text" msgid="5085786687366339027">"Otočením zariadenia prejdete do režimu celej obrazovky"</string> + <string name="letterbox_education_reposition_text" msgid="1068293354123934727">"Dvojitým klepnutím vedľa aplikácie zmeníte jej pozíciu"</string> + <string name="letterbox_education_got_it" msgid="4057634570866051177">"Dobre"</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 a7a515cdc61c..1df43afca2da 100644 --- a/libs/WindowManager/Shell/res/values-sk/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-sk/strings_tv.xml @@ -19,6 +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="158770205886688553">"Presunúť"</string> + <string name="pip_expand" msgid="1051966011679297308">"Rozbaliť"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Zbaliť"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Ovládanie zobraz. dvoj. stlač. "<annotation icon="home_icon">" TLAČIDLA PLOCHY "</annotation></string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Ponuka obrazu v obraze."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Posunúť doľava"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Posunúť doprava"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Posunúť nahor"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Posunúť nadol"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Hotovo"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-sl/strings.xml b/libs/WindowManager/Shell/res/values-sl/strings.xml index 6a6806921c18..adeaae978eaa 100644 --- a/libs/WindowManager/Shell/res/values-sl/strings.xml +++ b/libs/WindowManager/Shell/res/values-sl/strings.xml @@ -73,4 +73,12 @@ <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="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> + <string name="letterbox_education_dialog_title" msgid="6688664582871779215">"Nekatere aplikacije najbolje delujejo v navpični postavitvi"</string> + <string name="letterbox_education_dialog_subtext" msgid="4853542518367719562">"Poskusite eno od teh možnosti za čim boljši izkoristek prostora"</string> + <string name="letterbox_education_screen_rotation_text" msgid="5085786687366339027">"Če želite preklopiti v celozaslonski način, zasukajte napravo."</string> + <string name="letterbox_education_reposition_text" msgid="1068293354123934727">"Dvakrat se dotaknite ob aplikaciji, če jo želite prestaviti."</string> + <string name="letterbox_education_got_it" msgid="4057634570866051177">"V redu"</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 fe5c9ae5d2a8..88fc8325aa01 100644 --- a/libs/WindowManager/Shell/res/values-sl/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-sl/strings_tv.xml @@ -19,6 +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="158770205886688553">"Premakni"</string> + <string name="pip_expand" msgid="1051966011679297308">"Razširi"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Strni"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Za kontrolnike dvakrat pritisnite gumb za "<annotation icon="home_icon">" ZAČETNI ZASLON "</annotation></string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Meni za sliko v sliki"</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Premakni levo"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Premakni desno"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Premakni navzgor"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Premakni navzdol"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Končano"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-sq/strings.xml b/libs/WindowManager/Shell/res/values-sq/strings.xml index 7382a480fb1e..2839b4bae7e4 100644 --- a/libs/WindowManager/Shell/res/values-sq/strings.xml +++ b/libs/WindowManager/Shell/res/values-sq/strings.xml @@ -73,4 +73,12 @@ <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="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> + <string name="letterbox_education_dialog_title" msgid="6688664582871779215">"Disa aplikacione funksionojnë më mirë në modalitetin vertikal"</string> + <string name="letterbox_education_dialog_subtext" msgid="4853542518367719562">"Provo një nga këto opsione për ta shfrytëzuar sa më mirë hapësirën"</string> + <string name="letterbox_education_screen_rotation_text" msgid="5085786687366339027">"Rrotullo ekranin për të kaluar në ekran të plotë"</string> + <string name="letterbox_education_reposition_text" msgid="1068293354123934727">"Trokit dy herë pranë një aplikacioni për ta ripozicionuar"</string> + <string name="letterbox_education_got_it" msgid="4057634570866051177">"E kuptova"</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 1d5583b2c826..58687e5867fe 100644 --- a/libs/WindowManager/Shell/res/values-sq/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-sq/strings_tv.xml @@ -19,6 +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="158770205886688553">"Lëviz"</string> + <string name="pip_expand" msgid="1051966011679297308">"Zgjero"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Palos"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Trokit dy herë "<annotation icon="home_icon">" KREU "</annotation>" për kontrollet"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Menyja e \"Figurës brenda figurës\"."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Lëviz majtas"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Lëviz djathtas"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Lëviz lart"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Lëviz poshtë"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"U krye"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-sr/strings.xml b/libs/WindowManager/Shell/res/values-sr/strings.xml index c0c1e3f2849e..9db6b7c63610 100644 --- a/libs/WindowManager/Shell/res/values-sr/strings.xml +++ b/libs/WindowManager/Shell/res/values-sr/strings.xml @@ -73,4 +73,12 @@ <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="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> + <string name="letterbox_education_dialog_title" msgid="6688664582871779215">"Неке апликације најбоље функционишу у усправном режиму"</string> + <string name="letterbox_education_dialog_subtext" msgid="4853542518367719562">"Испробајте једну од ових опција да бисте на најбољи начин искористили простор"</string> + <string name="letterbox_education_screen_rotation_text" msgid="5085786687366339027">"Ротирајте уређај за приказ преко целог екрана"</string> + <string name="letterbox_education_reposition_text" msgid="1068293354123934727">"Двапут додирните поред апликације да бисте променили њену позицију"</string> + <string name="letterbox_education_got_it" msgid="4057634570866051177">"Важи"</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 62ad1e8f6e69..e850979174a3 100644 --- a/libs/WindowManager/Shell/res/values-sr/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-sr/strings_tv.xml @@ -19,6 +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="158770205886688553">"Премести"</string> + <string name="pip_expand" msgid="1051966011679297308">"Прошири"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Скупи"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Двапут притисните "<annotation icon="home_icon">" HOME "</annotation>" за контроле"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Мени Слика у слици."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Померите налево"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Померите надесно"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Померите нагоре"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Померите надоле"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Готово"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-sv/strings.xml b/libs/WindowManager/Shell/res/values-sv/strings.xml index 34254d90a93d..f6bd55423cdc 100644 --- a/libs/WindowManager/Shell/res/values-sv/strings.xml +++ b/libs/WindowManager/Shell/res/values-sv/strings.xml @@ -73,4 +73,12 @@ <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="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> + <string name="letterbox_education_dialog_title" msgid="6688664582871779215">"Vissa appar fungerar bäst i stående läge"</string> + <string name="letterbox_education_dialog_subtext" msgid="4853542518367719562">"Testa med ett av dessa alternativ för att få ut mest möjliga av ytan"</string> + <string name="letterbox_education_screen_rotation_text" msgid="5085786687366339027">"Rotera skärmen för att gå över till helskärmsläge"</string> + <string name="letterbox_education_reposition_text" msgid="1068293354123934727">"Tryck snabbt två gånger bredvid en app för att flytta den"</string> + <string name="letterbox_education_got_it" msgid="4057634570866051177">"OK"</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 74fb590c3e4d..d3a9c3de66db 100644 --- a/libs/WindowManager/Shell/res/values-sv/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-sv/strings_tv.xml @@ -19,6 +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="158770205886688553">"Flytta"</string> + <string name="pip_expand" msgid="1051966011679297308">"Utöka"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Komprimera"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Tryck snabbt två gånger på "<annotation icon="home_icon">" HEM "</annotation>" för kontroller"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Bild-i-bild-meny."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Flytta åt vänster"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Flytta åt höger"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Flytta uppåt"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Flytta nedåt"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Klar"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-sw/strings.xml b/libs/WindowManager/Shell/res/values-sw/strings.xml index 82a9f146c449..f6e558527ee5 100644 --- a/libs/WindowManager/Shell/res/values-sw/strings.xml +++ b/libs/WindowManager/Shell/res/values-sw/strings.xml @@ -73,4 +73,12 @@ <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="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> + <string name="letterbox_education_dialog_title" msgid="6688664582871779215">"Baadhi ya programu hufanya kazi vizuri zaidi zikiwa wima"</string> + <string name="letterbox_education_dialog_subtext" msgid="4853542518367719562">"Jaribu moja kati ya chaguo hizi ili utumie nafasi ya skrini yako kwa ufanisi"</string> + <string name="letterbox_education_screen_rotation_text" msgid="5085786687366339027">"Zungusha kifaa chako ili uende kwenye hali ya skrini nzima"</string> + <string name="letterbox_education_reposition_text" msgid="1068293354123934727">"Gusa mara mbili karibu na programu ili uihamishe"</string> + <string name="letterbox_education_got_it" msgid="4057634570866051177">"Nimeelewa"</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 cf0d8a9b3910..7b9a310ff0b6 100644 --- a/libs/WindowManager/Shell/res/values-sw/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-sw/strings_tv.xml @@ -19,6 +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="158770205886688553">"Hamisha"</string> + <string name="pip_expand" msgid="1051966011679297308">"Panua"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Kunja"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Bonyeza mara mbili kitufe cha "<annotation icon="home_icon">" UKURASA WA KWANZA "</annotation>" kupata vidhibiti"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Menyu ya kipengele cha kupachika picha ndani ya picha nyingine."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Sogeza kushoto"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Sogeza kulia"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Sogeza juu"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Sogeza chini"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Imemaliza"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ta/strings.xml b/libs/WindowManager/Shell/res/values-ta/strings.xml index 0ed778a273af..d8334adfe5ef 100644 --- a/libs/WindowManager/Shell/res/values-ta/strings.xml +++ b/libs/WindowManager/Shell/res/values-ta/strings.xml @@ -73,4 +73,12 @@ <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="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> + <string name="letterbox_education_dialog_title" msgid="6688664582871779215">"சில ஆப்ஸ் \'போர்ட்ரெய்ட்டில்\' சிறப்பாகச் செயல்படும்"</string> + <string name="letterbox_education_dialog_subtext" msgid="4853542518367719562">"ஸ்பேஸ்களிலிருந்து அதிகப் பலன்களைப் பெற இந்த விருப்பங்களில் ஒன்றைப் பயன்படுத்துங்கள்"</string> + <string name="letterbox_education_screen_rotation_text" msgid="5085786687366339027">"முழுத்திரைக்குச் செல்ல உங்கள் சாதனத்தைச் சுழற்றவும்"</string> + <string name="letterbox_education_reposition_text" msgid="1068293354123934727">"ஆப்ஸை இடம் மாற்ற, ஆப்ஸுக்கு அடுத்து இருமுறை தட்டவும்"</string> + <string name="letterbox_education_got_it" msgid="4057634570866051177">"சரி"</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 8bca46314e30..e201401e2e35 100644 --- a/libs/WindowManager/Shell/res/values-ta/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-ta/strings_tv.xml @@ -19,6 +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="158770205886688553">"நகர்த்து"</string> + <string name="pip_expand" msgid="1051966011679297308">"விரி"</string> + <string name="pip_collapse" msgid="3903295106641385962">"சுருக்கு"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" கட்டுப்பாடுகள்: "<annotation icon="home_icon">" முகப்பு "</annotation>" பட்டனை இருமுறை அழுத்துக"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"பிக்ச்சர்-இன்-பிக்ச்சர் மெனு."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"இடப்புறம் நகர்த்து"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"வலப்புறம் நகர்த்து"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"மேலே நகர்த்து"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"கீழே நகர்த்து"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"முடிந்தது"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-te/strings.xml b/libs/WindowManager/Shell/res/values-te/strings.xml index c3cd04655178..11da09d1e71e 100644 --- a/libs/WindowManager/Shell/res/values-te/strings.xml +++ b/libs/WindowManager/Shell/res/values-te/strings.xml @@ -28,7 +28,7 @@ <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> @@ -73,4 +73,12 @@ <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="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> + <string name="letterbox_education_dialog_title" msgid="6688664582871779215">"కొన్ని యాప్లు పోర్ట్రెయిట్లో ఉత్తమంగా పని చేస్తాయి"</string> + <string name="letterbox_education_dialog_subtext" msgid="4853542518367719562">"మీ ప్రదేశాన్ని ఎక్కువగా ఉపయోగించుకోవడానికి ఈ ఆప్షన్లలో ఒకదాన్ని ట్రై చేయండి"</string> + <string name="letterbox_education_screen_rotation_text" msgid="5085786687366339027">"ఫుల్ స్క్రీన్కు వెళ్లడానికి మీ పరికరాన్ని తిప్పండి"</string> + <string name="letterbox_education_reposition_text" msgid="1068293354123934727">"యాప్ స్థానాన్ని మార్చడానికి దాని పక్కన డబుల్-ట్యాప్ చేయండి"</string> + <string name="letterbox_education_got_it" msgid="4057634570866051177">"అర్థమైంది"</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 b9e8d762eda9..6284d90cb11f 100644 --- a/libs/WindowManager/Shell/res/values-te/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-te/strings_tv.xml @@ -19,6 +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="158770205886688553">"తరలించండి"</string> + <string name="pip_expand" msgid="1051966011679297308">"విస్తరించండి"</string> + <string name="pip_collapse" msgid="3903295106641385962">"కుదించండి"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" కంట్రోల్స్ కోసం "<annotation icon="home_icon">" HOME "</annotation>" బటన్ రెండుసార్లు నొక్కండి"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"పిక్చర్-ఇన్-పిక్చర్ మెనూ."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"ఎడమ వైపుగా జరపండి"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"కుడి వైపుగా జరపండి"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"పైకి జరపండి"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"కిందికి జరపండి"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"పూర్తయింది"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-television/config.xml b/libs/WindowManager/Shell/res/values-television/config.xml new file mode 100644 index 000000000000..cc0333efd82b --- /dev/null +++ b/libs/WindowManager/Shell/res/values-television/config.xml @@ -0,0 +1,55 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2022 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<!-- These resources are around just to allow their values to be customized + for TV products. Do not translate. --> +<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + + <!-- The percentage of the screen width to use for the default width or height of + picture-in-picture windows. Regardless of the percent set here, calculated size will never + be smaller than @dimen/default_minimal_size_pip_resizable_task. --> + <item name="config_pictureInPictureDefaultSizePercent" format="float" type="dimen">0.2</item> + + <!-- Default insets [LEFT/RIGHTxTOP/BOTTOM] from the screen edge for picture-in-picture windows. + These values are in DPs and will be converted to pixel sizes internally. --> + <string translatable="false" name="config_defaultPictureInPictureScreenEdgeInsets"> + 24x24 + </string> + + <!-- The default gravity for the picture-in-picture window. + Currently, this maps to Gravity.BOTTOM | Gravity.RIGHT --> + <integer name="config_defaultPictureInPictureGravity">0x55</integer> + + <!-- Fraction of screen width/height restricted keep clear areas can move the PiP. --> + <fraction name="config_pipMaxRestrictedMoveDistance">15%</fraction> + + <!-- Duration (in milliseconds) the PiP stays stashed before automatically unstashing. --> + <integer name="config_pipStashDuration">5000</integer> + + <!-- Time (duration in milliseconds) that the shell waits for an app to close the PiP by itself + if a custom action is present before closing it. --> + <integer name="config_pipForceCloseDelay">5000</integer> + + <!-- Animation duration when exit starting window: fade out icon --> + <integer name="starting_window_app_reveal_icon_fade_out_duration">0</integer> + + <!-- Animation duration when exit starting window: reveal app --> + <integer name="starting_window_app_reveal_anim_delay">0</integer> + + <!-- Animation duration when exit starting window: reveal app --> + <integer name="starting_window_app_reveal_anim_duration">0</integer> +</resources> diff --git a/libs/WindowManager/Shell/res/values-television/dimen.xml b/libs/WindowManager/Shell/res/values-television/dimen.xml new file mode 100644 index 000000000000..14e89f8b08df --- /dev/null +++ b/libs/WindowManager/Shell/res/values-television/dimen.xml @@ -0,0 +1,24 @@ +<?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. + --> + +<!-- These resources are around just to allow their values to be customized + for TV products. Do not translate. --> +<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + + <!-- Padding between PIP and keep clear areas that caused it to move. --> + <dimen name="pip_keep_clear_area_padding">16dp</dimen> +</resources> diff --git a/libs/WindowManager/Shell/res/values-th/strings.xml b/libs/WindowManager/Shell/res/values-th/strings.xml index 06b04f145772..cfee8ea3242e 100644 --- a/libs/WindowManager/Shell/res/values-th/strings.xml +++ b/libs/WindowManager/Shell/res/values-th/strings.xml @@ -73,4 +73,12 @@ <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="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> + <string name="letterbox_education_dialog_title" msgid="6688664582871779215">"บางแอปทำงานได้ดีที่สุดในแนวตั้ง"</string> + <string name="letterbox_education_dialog_subtext" msgid="4853542518367719562">"ลองใช้หนึ่งในตัวเลือกเหล่านี้เพื่อให้ได้ประโยชน์สูงสุดจากพื้นที่ว่าง"</string> + <string name="letterbox_education_screen_rotation_text" msgid="5085786687366339027">"หมุนอุปกรณ์ให้แสดงเต็มหน้าจอ"</string> + <string name="letterbox_education_reposition_text" msgid="1068293354123934727">"แตะสองครั้งข้างแอปเพื่อเปลี่ยนตำแหน่ง"</string> + <string name="letterbox_education_got_it" msgid="4057634570866051177">"รับทราบ"</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 d3797e7c3cde..27cf56c6e154 100644 --- a/libs/WindowManager/Shell/res/values-th/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-th/strings_tv.xml @@ -19,6 +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="158770205886688553">"ย้าย"</string> + <string name="pip_expand" msgid="1051966011679297308">"ขยาย"</string> + <string name="pip_collapse" msgid="3903295106641385962">"ยุบ"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" กดปุ่ม "<annotation icon="home_icon">" หน้าแรก "</annotation>" สองครั้งเพื่อเปิดการควบคุม"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"เมนูการแสดงภาพซ้อนภาพ"</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"ย้ายไปทางซ้าย"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"ย้ายไปทางขวา"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"ย้ายขึ้น"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"ย้ายลง"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"เสร็จ"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-tl/strings.xml b/libs/WindowManager/Shell/res/values-tl/strings.xml index 62642c18937e..eed624dd5069 100644 --- a/libs/WindowManager/Shell/res/values-tl/strings.xml +++ b/libs/WindowManager/Shell/res/values-tl/strings.xml @@ -73,4 +73,12 @@ <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="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> + <string name="letterbox_education_dialog_title" msgid="6688664582871779215">"May ilang app na pinakamainam gamitin nang naka-portrait"</string> + <string name="letterbox_education_dialog_subtext" msgid="4853542518367719562">"Subukan ang isa sa mga opsyong ito para masulit ang iyong space"</string> + <string name="letterbox_education_screen_rotation_text" msgid="5085786687366339027">"I-rotate ang iyong device para mag-full screen"</string> + <string name="letterbox_education_reposition_text" msgid="1068293354123934727">"Mag-double tap sa tabi ng isang app para iposisyon ito ulit"</string> + <string name="letterbox_education_got_it" msgid="4057634570866051177">"OK"</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 b01c1115cd34..4cc050bebe5b 100644 --- a/libs/WindowManager/Shell/res/values-tl/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-tl/strings_tv.xml @@ -19,6 +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="158770205886688553">"Ilipat"</string> + <string name="pip_expand" msgid="1051966011679297308">"I-expand"</string> + <string name="pip_collapse" msgid="3903295106641385962">"I-collapse"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" I-double press ang "<annotation icon="home_icon">" HOME "</annotation>" para sa mga kontrol"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Menu ng Picture-in-Picture."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Ilipat pakaliwa"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Ilipat pakanan"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Itaas"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Ibaba"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Tapos na"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-tr/strings.xml b/libs/WindowManager/Shell/res/values-tr/strings.xml index 971520a0f229..2b4a2d0550f0 100644 --- a/libs/WindowManager/Shell/res/values-tr/strings.xml +++ b/libs/WindowManager/Shell/res/values-tr/strings.xml @@ -73,4 +73,12 @@ <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="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> + <string name="letterbox_education_dialog_title" msgid="6688664582871779215">"Bazı uygulamalar dikey modda en iyi performansı gösterir"</string> + <string name="letterbox_education_dialog_subtext" msgid="4853542518367719562">"Alanınızı en verimli şekilde kullanmak için bu seçeneklerden birini deneyin"</string> + <string name="letterbox_education_screen_rotation_text" msgid="5085786687366339027">"Tam ekrana geçmek için cihazınızı döndürün"</string> + <string name="letterbox_education_reposition_text" msgid="1068293354123934727">"Yeniden konumlandırmak için uygulamanın yanına iki kez dokunun"</string> + <string name="letterbox_education_got_it" msgid="4057634570866051177">"Anladım"</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 c92c4d02f465..69bb608061e4 100644 --- a/libs/WindowManager/Shell/res/values-tr/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-tr/strings_tv.xml @@ -19,6 +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="158770205886688553">"Taşı"</string> + <string name="pip_expand" msgid="1051966011679297308">"Genişlet"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Daralt"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Kontroller için "<annotation icon="home_icon">" ANA SAYFA "</annotation>" düğmesine iki kez basın"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Pencere içinde pencere menüsü."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Sola taşı"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Sağa taşı"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Yukarı taşı"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Aşağı taşı"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Bitti"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-tvdpi/dimen.xml b/libs/WindowManager/Shell/res/values-tvdpi/dimen.xml index 7920fd237a08..02e726fbc3bf 100644 --- a/libs/WindowManager/Shell/res/values-tvdpi/dimen.xml +++ b/libs/WindowManager/Shell/res/values-tvdpi/dimen.xml @@ -16,7 +16,33 @@ --> <resources> <!-- The dimensions to user for picture-in-picture action buttons. --> - <dimen name="picture_in_picture_button_width">100dp</dimen> - <dimen name="picture_in_picture_button_start_margin">-50dp</dimen> + <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 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_outer_space">24dp</dimen> + + <!-- outer space minus border width --> + <dimen name="pip_menu_outer_space_frame">20dp</dimen> + + <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_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> </resources> diff --git a/libs/WindowManager/Shell/res/values-uk/strings.xml b/libs/WindowManager/Shell/res/values-uk/strings.xml index 30147353e3f6..c3411a837c78 100644 --- a/libs/WindowManager/Shell/res/values-uk/strings.xml +++ b/libs/WindowManager/Shell/res/values-uk/strings.xml @@ -73,4 +73,12 @@ <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="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> + <string name="letterbox_education_dialog_title" msgid="6688664582871779215">"Деякі додатки найкраще працюють у вертикальній орієнтації"</string> + <string name="letterbox_education_dialog_subtext" msgid="4853542518367719562">"Щоб максимально ефективно використовувати місце на екрані, спробуйте виконати одну з наведених нижче дій"</string> + <string name="letterbox_education_screen_rotation_text" msgid="5085786687366339027">"Щоб перейти в повноекранний режим, поверніть пристрій"</string> + <string name="letterbox_education_reposition_text" msgid="1068293354123934727">"Щоб перемістити додаток, двічі торкніться області поруч із ним"</string> + <string name="letterbox_education_got_it" msgid="4057634570866051177">"ОK"</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 74d4723d7850..81a8285c58cf 100644 --- a/libs/WindowManager/Shell/res/values-uk/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-uk/strings_tv.xml @@ -19,6 +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="158770205886688553">"Перемістити"</string> + <string name="pip_expand" msgid="1051966011679297308">"Розгорнути"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Згорнути"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Відкрити елементи керування: двічі натисніть "<annotation icon="home_icon">"HOME"</annotation></string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Меню \"картинка в картинці\""</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Перемістити ліворуч"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Перемістити праворуч"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Перемістити вгору"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Перемістити вниз"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Готово"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ur/strings.xml b/libs/WindowManager/Shell/res/values-ur/strings.xml index 07319efdc52c..a31c2be25643 100644 --- a/libs/WindowManager/Shell/res/values-ur/strings.xml +++ b/libs/WindowManager/Shell/res/values-ur/strings.xml @@ -73,4 +73,12 @@ <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="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> + <string name="letterbox_education_dialog_title" msgid="6688664582871779215">"کچھ ایپس پورٹریٹ میں بہترین کام کرتی ہیں"</string> + <string name="letterbox_education_dialog_subtext" msgid="4853542518367719562">"اپنی اسپیس کا زیادہ سے زیادہ فائدہ اٹھانے کے لیے ان اختیارات میں سے ایک کو آزمائیں"</string> + <string name="letterbox_education_screen_rotation_text" msgid="5085786687366339027">"پوری اسکرین پر جانے کیلئے اپنا آلہ گھمائیں"</string> + <string name="letterbox_education_reposition_text" msgid="1068293354123934727">"کسی ایپ کی پوزیشن تبدیل کرنے کے لیے اس کے آگے دو بار تھپتھپائیں"</string> + <string name="letterbox_education_got_it" msgid="4057634570866051177">"سمجھ آ گئی"</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 317953309947..e83885772f2d 100644 --- a/libs/WindowManager/Shell/res/values-ur/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-ur/strings_tv.xml @@ -19,6 +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="158770205886688553">"منتقل کریں"</string> + <string name="pip_expand" msgid="1051966011679297308">"پھیلائیں"</string> + <string name="pip_collapse" msgid="3903295106641385962">"سکیڑیں"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" کنٹرولز کے لیے "<annotation icon="home_icon">"ہوم "</annotation>" بٹن کو دو بار دبائیں"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"تصویر میں تصویر کا مینو۔"</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"دائیں منتقل کریں"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"بائیں منتقل کریں"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"اوپر منتقل کریں"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"نیچے منتقل کریں"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"ہو گیا"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-uz/strings.xml b/libs/WindowManager/Shell/res/values-uz/strings.xml index 4c79d64fb8e1..2e3222560dde 100644 --- a/libs/WindowManager/Shell/res/values-uz/strings.xml +++ b/libs/WindowManager/Shell/res/values-uz/strings.xml @@ -73,4 +73,12 @@ <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="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> + <string name="letterbox_education_dialog_title" msgid="6688664582871779215">"Ayrim ilovalar tik holatda ishlashga eng mos"</string> + <string name="letterbox_education_dialog_subtext" msgid="4853542518367719562">"Muhitdan yanada samarali foydalanish uchun quyidagilardan birini sinang"</string> + <string name="letterbox_education_screen_rotation_text" msgid="5085786687366339027">"Butun ekranda ochish uchun qurilmani buring"</string> + <string name="letterbox_education_reposition_text" msgid="1068293354123934727">"Qayta joylash uchun keyingi ilova ustiga ikki marta bosing"</string> + <string name="letterbox_education_got_it" msgid="4057634570866051177">"OK"</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 ae5a647301c8..da953356628c 100644 --- a/libs/WindowManager/Shell/res/values-uz/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-uz/strings_tv.xml @@ -19,6 +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="158770205886688553">"Boshqa joyga olish"</string> + <string name="pip_expand" msgid="1051966011679297308">"Yoyish"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Yopish"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Boshqaruv uchun "<annotation icon="home_icon">"ASOSIY"</annotation>" tugmani ikki marta bosing"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Tasvir ustida tasvir menyusi."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Chapga olish"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Oʻngga olish"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Tepaga olish"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Pastga olish"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Tayyor"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-vi/strings.xml b/libs/WindowManager/Shell/res/values-vi/strings.xml index b9f23cd4672d..8f3cffecc952 100644 --- a/libs/WindowManager/Shell/res/values-vi/strings.xml +++ b/libs/WindowManager/Shell/res/values-vi/strings.xml @@ -73,4 +73,12 @@ <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="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> + <string name="letterbox_education_dialog_title" msgid="6688664582871779215">"Một số ứng dụng hoạt động tốt nhất ở chế độ dọc"</string> + <string name="letterbox_education_dialog_subtext" msgid="4853542518367719562">"Hãy thử một trong các tuỳ chọn sau để tận dụng không gian"</string> + <string name="letterbox_education_screen_rotation_text" msgid="5085786687366339027">"Xoay thiết bị để chuyển sang chế độ toàn màn hình"</string> + <string name="letterbox_education_reposition_text" msgid="1068293354123934727">"Nhấn đúp vào bên cạnh ứng dụng để đặt lại vị trí"</string> + <string name="letterbox_education_got_it" msgid="4057634570866051177">"OK"</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 082d12596076..1f9260fdcff0 100644 --- a/libs/WindowManager/Shell/res/values-vi/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-vi/strings_tv.xml @@ -19,6 +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="158770205886688553">"Di chuyển"</string> + <string name="pip_expand" msgid="1051966011679297308">"Mở rộng"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Thu gọn"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Nhấn đúp vào nút "<annotation icon="home_icon">" MÀN HÌNH CHÍNH "</annotation>" để mở trình đơn điều khiển"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Trình đơn hình trong hình."</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Di chuyển sang trái"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Di chuyển sang phải"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Di chuyển lên"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Di chuyển xuống"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Xong"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-zh-rCN/strings.xml b/libs/WindowManager/Shell/res/values-zh-rCN/strings.xml index c0072582ec88..19a9d371e435 100644 --- a/libs/WindowManager/Shell/res/values-zh-rCN/strings.xml +++ b/libs/WindowManager/Shell/res/values-zh-rCN/strings.xml @@ -73,4 +73,12 @@ <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="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> + <string name="letterbox_education_dialog_title" msgid="6688664582871779215">"某些应用在纵向模式下才能发挥最佳效果"</string> + <string name="letterbox_education_dialog_subtext" msgid="4853542518367719562">"这些选项都有助于您最大限度地利用屏幕空间,不妨从中择一试试"</string> + <string name="letterbox_education_screen_rotation_text" msgid="5085786687366339027">"旋转设备即可进入全屏模式"</string> + <string name="letterbox_education_reposition_text" msgid="1068293354123934727">"在某个应用旁边连续点按两次,即可调整它的位置"</string> + <string name="letterbox_education_got_it" msgid="4057634570866051177">"知道了"</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 cb3fcf7c4c16..399d639fe70f 100644 --- a/libs/WindowManager/Shell/res/values-zh-rCN/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-zh-rCN/strings_tv.xml @@ -19,6 +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="158770205886688553">"移动"</string> + <string name="pip_expand" msgid="1051966011679297308">"展开"</string> + <string name="pip_collapse" msgid="3903295106641385962">"收起"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" 按两次"<annotation icon="home_icon">"主屏幕"</annotation>"按钮可查看相关控件"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"画中画菜单。"</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"左移"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"右移"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"上移"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"下移"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"完成"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-zh-rHK/strings.xml b/libs/WindowManager/Shell/res/values-zh-rHK/strings.xml index 5e336770e83a..0c40e963f2e4 100644 --- a/libs/WindowManager/Shell/res/values-zh-rHK/strings.xml +++ b/libs/WindowManager/Shell/res/values-zh-rHK/strings.xml @@ -73,4 +73,12 @@ <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="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> + <string name="letterbox_education_dialog_title" msgid="6688664582871779215">"部分應用程式需要使用直向模式才能發揮最佳效果"</string> + <string name="letterbox_education_dialog_subtext" msgid="4853542518367719562">"請嘗試以下選項,充分運用螢幕的畫面空間"</string> + <string name="letterbox_education_screen_rotation_text" msgid="5085786687366339027">"旋轉裝置方向即可進入全螢幕模式"</string> + <string name="letterbox_education_reposition_text" msgid="1068293354123934727">"在應用程式旁輕按兩下即可調整位置"</string> + <string name="letterbox_education_got_it" msgid="4057634570866051177">"知道了"</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 956243ed6e6d..acbc26d033cd 100644 --- a/libs/WindowManager/Shell/res/values-zh-rHK/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-zh-rHK/strings_tv.xml @@ -19,6 +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="158770205886688553">"移動"</string> + <string name="pip_expand" msgid="1051966011679297308">"展開"</string> + <string name="pip_collapse" msgid="3903295106641385962">"收合"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" 按兩下"<annotation icon="home_icon">" 主畫面按鈕"</annotation>"即可顯示控制項"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"畫中畫選單。"</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"向左移"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"向右移"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"向上移"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"向下移"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"完成"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-zh-rTW/strings.xml b/libs/WindowManager/Shell/res/values-zh-rTW/strings.xml index 2439a975daa8..8691352cf94a 100644 --- a/libs/WindowManager/Shell/res/values-zh-rTW/strings.xml +++ b/libs/WindowManager/Shell/res/values-zh-rTW/strings.xml @@ -73,4 +73,12 @@ <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="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> + <string name="letterbox_education_dialog_title" msgid="6688664582871779215">"某些應用程式在直向模式下才能發揮最佳效果"</string> + <string name="letterbox_education_dialog_subtext" msgid="4853542518367719562">"請試試這裡的任一方式,以充分運用螢幕畫面的空間"</string> + <string name="letterbox_education_screen_rotation_text" msgid="5085786687366339027">"旋轉裝置方向即可進入全螢幕模式"</string> + <string name="letterbox_education_reposition_text" msgid="1068293354123934727">"在應用程式旁輕觸兩下即可調整位置"</string> + <string name="letterbox_education_got_it" msgid="4057634570866051177">"我知道了"</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 08b2f4bbca89..f8c683ec3a60 100644 --- a/libs/WindowManager/Shell/res/values-zh-rTW/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-zh-rTW/strings_tv.xml @@ -19,6 +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="158770205886688553">"移動"</string> + <string name="pip_expand" msgid="1051966011679297308">"展開"</string> + <string name="pip_collapse" msgid="3903295106641385962">"收合"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" 按兩下"<annotation icon="home_icon">"主畫面按鈕"</annotation>"即可顯示控制選項"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"子母畫面選單。"</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"向左移動"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"向右移動"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"向上移動"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"向下移動"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"完成"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-zu/strings.xml b/libs/WindowManager/Shell/res/values-zu/strings.xml index 20128f602abf..44ffbc6afa45 100644 --- a/libs/WindowManager/Shell/res/values-zu/strings.xml +++ b/libs/WindowManager/Shell/res/values-zu/strings.xml @@ -73,4 +73,12 @@ <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="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> + <string name="letterbox_education_dialog_title" msgid="6688664582871779215">"Amanye ama-app asebenza ngcono uma eme ngobude"</string> + <string name="letterbox_education_dialog_subtext" msgid="4853542518367719562">"Zama enye yalezi zinketho ukuze usebenzise isikhala sakho ngokugcwele"</string> + <string name="letterbox_education_screen_rotation_text" msgid="5085786687366339027">"Zungezisa idivayisi yakho ukuze uye esikrinini esigcwele"</string> + <string name="letterbox_education_reposition_text" msgid="1068293354123934727">"Thepha kabili eduze kwe-app ukuze uyimise kabusha"</string> + <string name="letterbox_education_got_it" msgid="4057634570866051177">"Ngiyezwa"</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 89c7f498652d..20243a9dfc9c 100644 --- a/libs/WindowManager/Shell/res/values-zu/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-zu/strings_tv.xml @@ -19,6 +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="158770205886688553">"Hambisa"</string> + <string name="pip_expand" msgid="1051966011679297308">"Nweba"</string> + <string name="pip_collapse" msgid="3903295106641385962">"Goqa"</string> + <string name="pip_edu_text" msgid="3672999496647508701">" Chofoza kabili "<annotation icon="home_icon">" IKHAYA"</annotation>" mayelana nezilawuli"</string> + <string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Imenyu yesithombe-esithombeni"</string> + <string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Yisa kwesokunxele"</string> + <string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Yisa kwesokudla"</string> + <string name="a11y_action_pip_move_up" msgid="98502616918621959">"Khuphula"</string> + <string name="a11y_action_pip_move_down" msgid="3858802832725159740">"Yehlisa"</string> + <string name="a11y_action_pip_move_done" msgid="1486845365134416210">"Kwenziwe"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values/attrs.xml b/libs/WindowManager/Shell/res/values/attrs.xml new file mode 100644 index 000000000000..4aaeef8afcb0 --- /dev/null +++ b/libs/WindowManager/Shell/res/values/attrs.xml @@ -0,0 +1,22 @@ +<!-- + ~ Copyright (C) 2022 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT 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> + <declare-styleable name="LetterboxEduDialogActionLayout"> + <attr name="icon" format="reference" /> + <attr name="text" format="string" /> + </declare-styleable> +</resources> diff --git a/libs/WindowManager/Shell/res/values/colors.xml b/libs/WindowManager/Shell/res/values/colors.xml index cf596f7d15dc..6e750a3d5e34 100644 --- a/libs/WindowManager/Shell/res/values/colors.xml +++ b/libs/WindowManager/Shell/res/values/colors.xml @@ -30,10 +30,17 @@ <color name="bubbles_dark">@color/GM2_grey_800</color> <color name="bubbles_icon_tint">@color/GM2_grey_700</color> + <!-- PiP --> + <color name="pip_custom_close_bg">#D93025</color> + <!-- Compat controls UI --> <color name="compat_controls_background">@android:color/system_neutral1_800</color> <color name="compat_controls_text">@android:color/system_neutral1_50</color> + <!-- Letterbox Education --> + <color name="letterbox_education_accent_primary">@android:color/system_accent1_100</color> + <color name="letterbox_education_text_secondary">@android:color/system_neutral2_200</color> + <!-- GM2 colors --> <color name="GM2_grey_200">#E8EAED</color> <color name="GM2_grey_700">#5F6368</color> @@ -43,4 +50,4 @@ <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> -</resources>
\ No newline at end of file +</resources> diff --git a/libs/WindowManager/Shell/res/values/colors_tv.xml b/libs/WindowManager/Shell/res/values/colors_tv.xml new file mode 100644 index 000000000000..fa90fe36b545 --- /dev/null +++ b/libs/WindowManager/Shell/res/values/colors_tv.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2021 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<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_pip_menu_focus_border">#E8EAED</color> + <color name="tv_pip_menu_background">#1E232C</color> + + <color name="tv_pip_edu_text">#99D2E3FC</color> + <color name="tv_pip_edu_text_home_icon">#D2E3FC</color> +</resources> diff --git a/libs/WindowManager/Shell/res/values/config.xml b/libs/WindowManager/Shell/res/values/config.xml index 1b8032b7077b..f03b7f66cdc8 100644 --- a/libs/WindowManager/Shell/res/values/config.xml +++ b/libs/WindowManager/Shell/res/values/config.xml @@ -46,6 +46,10 @@ <!-- Show PiP enter split icon, which allows apps to directly enter splitscreen from PiP. --> <bool name="config_pipEnableEnterSplitButton">false</bool> + <!-- Time (duration in milliseconds) that the shell waits for an app to close the PiP by itself + if a custom action is present before closing it. --> + <integer name="config_pipForceCloseDelay">1000</integer> + <!-- Animation duration when using long press on recents to dock --> <integer name="long_press_dock_anim_duration">250</integer> @@ -70,4 +74,33 @@ <!-- Animation duration when exit starting window: reveal app --> <integer name="starting_window_app_reveal_anim_duration">266</integer> + + <!-- Default insets [LEFT/RIGHTxTOP/BOTTOM] from the screen edge for picture-in-picture windows. + These values are in DPs and will be converted to pixel sizes internally. --> + <string translatable="false" name="config_defaultPictureInPictureScreenEdgeInsets"> + 16x16 + </string> + + <!-- The percentage of the screen width to use for the default width or height of + picture-in-picture windows. Regardless of the percent set here, calculated size will never + be smaller than @dimen/default_minimal_size_pip_resizable_task. --> + <item name="config_pictureInPictureDefaultSizePercent" format="float" type="dimen">0.23</item> + + <!-- The default aspect ratio for picture-in-picture windows. --> + <item name="config_pictureInPictureDefaultAspectRatio" format="float" type="dimen"> + 1.777778 + </item> + + <!-- This is the limit for the max and min aspect ratio (1 / this value) at which the min size + will be used instead of an adaptive size based loosely on area. --> + <item name="config_pictureInPictureAspectRatioLimitForMinSize" format="float" type="dimen"> + 1.777778 + </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> </resources> diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml index af78293eb3ea..1dac9caba01e 100644 --- a/libs/WindowManager/Shell/res/values/dimen.xml +++ b/libs/WindowManager/Shell/res/values/dimen.xml @@ -18,8 +18,8 @@ <dimen name="dismiss_circle_size">96dp</dimen> <dimen name="dismiss_circle_small">60dp</dimen> - <!-- The height of the gradient indicating the dismiss edge when moving a PIP. --> - <dimen name="floating_dismiss_gradient_height">250dp</dimen> + <!-- The height of the gradient indicating the dismiss edge when moving a PIP or bubble. --> + <dimen name="floating_dismiss_gradient_height">548dp</dimen> <!-- The padding around a PiP actions. --> <dimen name="pip_action_padding">16dp</dimen> @@ -74,12 +74,21 @@ <!-- PIP stash offset size, which is the width of visible PIP region when stashed. --> <dimen name="pip_stash_offset">32dp</dimen> + <!-- PIP shadow radius, originally as + WindowConfiguration#PINNED_WINDOWING_MODE_ELEVATION_IN_DIP --> + <dimen name="pip_shadow_radius">5dp</dimen> + + <!-- The width and height of the background for custom action in PiP menu. --> + <dimen name="pip_custom_close_bg_size">32dp</dimen> + <dimen name="dismiss_target_x_size">24dp</dimen> <dimen name="floating_dismiss_bottom_margin">50dp</dimen> <!-- How high we lift the divider when touching --> <dimen name="docked_stack_divider_lift_elevation">4dp</dimen> + <!-- Icon size for split screen --> + <dimen name="split_icon_size">72dp</dimen> <!-- Divider handle size for legacy split screen --> <dimen name="docked_divider_handle_width">16dp</dimen> <dimen name="docked_divider_handle_height">2dp</dimen> @@ -129,6 +138,9 @@ <dimen name="bubble_dismiss_encircle_size">52dp</dimen> <!-- Padding around the view displayed when the bubble is expanded --> <dimen name="bubble_expanded_view_padding">16dp</dimen> + <!-- Padding for the edge of the expanded view that is closest to the edge of the screen used + when displaying in landscape on a large screen. --> + <dimen name="bubble_expanded_view_largescreen_landscape_padding">128dp</dimen> <!-- This should be at least the size of bubble_expanded_view_padding; it is used to include a slight touch slop around the expanded view. --> <dimen name="bubble_expanded_view_slop">8dp</dimen> @@ -136,16 +148,21 @@ If this value changes then R.dimen.bubble_expanded_view_min_height in CtsVerifier should also be updated. --> <dimen name="bubble_expanded_default_height">180dp</dimen> - <!-- On large screens the width of the expanded view is restricted to this size. --> - <dimen name="bubble_expanded_view_phone_landscape_overflow_width">412dp</dimen> + <!-- The width of the overflow view on large screens or in landscape on phone. --> + <dimen name="bubble_expanded_view_overflow_width">380dp</dimen> <!-- Inset to apply to the icon in the overflow button. --> <dimen name="bubble_overflow_icon_inset">30dp</dimen> <!-- Default (and minimum) height of bubble overflow --> <dimen name="bubble_overflow_height">480dp</dimen> <!-- Bubble overflow padding when there are no bubbles --> <dimen name="bubble_overflow_empty_state_padding">16dp</dimen> - <!-- Padding of container for overflow bubbles --> - <dimen name="bubble_overflow_padding">15dp</dimen> + <!-- Horizontal padding of the overflow container. Total desired padding is 16dp but the items + already have 5dp added to each side. --> + <dimen name="bubble_overflow_container_padding_horizontal">11dp</dimen> + <!-- Horizontal padding between items in the overflow view, half of the desired amount. --> + <dimen name="bubble_overflow_item_padding_horizontal">5dp</dimen> + <!-- Vertical padding between items in the overflow view, half the desired amount. --> + <dimen name="bubble_overflow_item_padding_vertical">16dp</dimen> <!-- Padding of label for bubble overflow view --> <dimen name="bubble_overflow_text_padding">7dp</dimen> <!-- Height of bubble overflow empty state illustration --> @@ -197,8 +214,15 @@ <dimen name="bubble_dismiss_target_padding_x">40dp</dimen> <dimen name="bubble_dismiss_target_padding_y">20dp</dimen> <dimen name="bubble_manage_menu_elevation">4dp</dimen> - <!-- Size of user education views on large screens (phone is just match parent). --> - <dimen name="bubbles_user_education_width_large_screen">400dp</dimen> + <!-- Size of manage user education views on large screens or in landscape. --> + <dimen name="bubbles_user_education_width">480dp</dimen> + <!-- Margin applied to the end of the user education views (really only matters for phone + since the width is match parent). --> + <dimen name="bubble_user_education_margin_end">24dp</dimen> + <!-- Padding applied to the end of the user education view. --> + <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> <!-- Bottom and end margin for compat buttons. --> <dimen name="compat_button_margin">16dp</dimen> @@ -213,6 +237,30 @@ + compat_button_margin - compat_hint_corner_radius - compat_hint_point_width / 2). --> <dimen name="compat_hint_padding_end">7dp</dimen> + <!-- The width of the size compat hint. --> + <dimen name="size_compat_hint_width">188dp</dimen> + + <!-- The width of the camera compat hint. --> + <dimen name="camera_compat_hint_width">143dp</dimen> + + <!-- The corner radius of the letterbox education dialog. --> + <dimen name="letterbox_education_dialog_corner_radius">28dp</dimen> + + <!-- The size of an icon in the letterbox education dialog. --> + <dimen name="letterbox_education_dialog_icon_size">48dp</dimen> + + <!-- The fixed width of the dialog if there is enough space in the parent. --> + <dimen name="letterbox_education_dialog_width">472dp</dimen> + + <!-- The margin between the dialog container and its parent. --> + <dimen name="letterbox_education_dialog_margin">16dp</dimen> + + <!-- The width of each action container in the letterbox education dialog --> + <dimen name="letterbox_education_dialog_action_width">140dp</dimen> + + <!-- The space between two actions in the letterbox education dialog --> + <dimen name="letterbox_education_dialog_space_between_actions">24dp</dimen> + <!-- The width of the brand image on staring surface. --> <dimen name="starting_surface_brand_image_width">200dp</dimen> @@ -227,4 +275,14 @@ <!-- The distance of the shift icon when early exit starting window. --> <dimen name="starting_surface_early_exit_icon_distance">32dp</dimen> + + <!-- The default minimal size of a PiP task, in both dimensions. --> + <dimen name="default_minimal_size_pip_resizable_task">108dp</dimen> + + <!-- + The overridable minimal size of a PiP task, in both dimensions. + Different from default_minimal_size_pip_resizable_task, this is to limit the dimension + when the pinned stack size is overridden by app via minWidth/minHeight. + --> + <dimen name="overridable_minimal_size_pip_resizable_task">48dp</dimen> </resources> diff --git a/libs/WindowManager/Shell/res/values/strings.xml b/libs/WindowManager/Shell/res/values/strings.xml index c88fc16e218e..a24311fb1f21 100644 --- a/libs/WindowManager/Shell/res/values/strings.xml +++ b/libs/WindowManager/Shell/res/values/strings.xml @@ -158,4 +158,32 @@ <!-- 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> + + <!-- Description of the camera compat button for applying stretched issues treatment in the hint for + compatibility control. [CHAR LIMIT=NONE] --> + <string name="camera_compat_treatment_suggested_button_description">Camera issues?\nTap to refit</string> + + <!-- Description of the camera compat button for reverting stretched issues treatment in the hint for + compatibility control. [CHAR LIMIT=NONE] --> + <string name="camera_compat_treatment_applied_button_description">Didn\u2019t fix it?\nTap to revert</string> + + <!-- Accessibillity description of the camera dismiss button for stretched issues in the hint for + compatibility control. [CHAR LIMIT=NONE] --> + <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> + + <!-- 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 reposition app action. [CHAR LIMIT=NONE] --> + <string name="letterbox_education_reposition_text">Double-tap next to 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> + </resources> diff --git a/libs/WindowManager/Shell/res/values/strings_tv.xml b/libs/WindowManager/Shell/res/values/strings_tv.xml index 2dfdcabaa931..2b7a13eac6ca 100644 --- a/libs/WindowManager/Shell/res/values/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values/strings_tv.xml @@ -26,9 +26,38 @@ <!-- Picture-in-Picture (PIP) menu --> <eat-comment /> <!-- Button to close picture-in-picture (PIP) in PIP menu [CHAR LIMIT=30] --> - <string name="pip_close">Close PIP</string> + <string name="pip_close">Close</string> <!-- Button to move picture-in-picture (PIP) screen to the fullscreen in PIP menu [CHAR LIMIT=30] --> <string name="pip_fullscreen">Full screen</string> + + <!-- Button to move picture-in-picture (PIP) via DPAD in the PIP menu [CHAR LIMIT=30] --> + <string name="pip_move">Move</string> + + <!-- Button to expand the picture-in-picture (PIP) window [CHAR LIMIT=30] --> + <string name="pip_expand">Expand</string> + + <!-- Button to collapse/shrink the picture-in-picture (PIP) window [CHAR LIMIT=30] --> + <string name="pip_collapse">Collapse</string> + + <!-- 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> + + <!-- Accessibility announcement when opening the PiP menu. [CHAR LIMIT=NONE] --> + <string name="a11y_pip_menu_entered">Picture-in-Picture menu.</string> + + <!-- Accessibility action: move the PiP window to the left [CHAR LIMIT=30] --> + <string name="a11y_action_pip_move_left">Move left</string> + <!-- Accessibility action: move the PiP window to the right [CHAR LIMIT=30] --> + <string name="a11y_action_pip_move_right">Move right</string> + <!-- Accessibility action: move the PiP window up [CHAR LIMIT=30] --> + <string name="a11y_action_pip_move_up">Move up</string> + <!-- Accessibility action: move the PiP window down [CHAR LIMIT=30] --> + <string name="a11y_action_pip_move_down">Move down</string> + <!-- Accessibility action: done with moving the PiP [CHAR LIMIT=30] --> + <string name="a11y_action_pip_move_done">Done</string> + </resources> diff --git a/libs/WindowManager/Shell/res/values/styles.xml b/libs/WindowManager/Shell/res/values/styles.xml index 7733201d2465..19f7c3ef4364 100644 --- a/libs/WindowManager/Shell/res/values/styles.xml +++ b/libs/WindowManager/Shell/res/values/styles.xml @@ -47,4 +47,13 @@ <item name="android:layout_width">96dp</item> <item name="android:layout_height">48dp</item> </style> + + <style name="TvPipEduText"> + <item name="android:fontFamily">@*android:string/config_headlineFontFamilyMedium</item> + <item name="android:textAllCaps">true</item> + <item name="android:textSize">10sp</item> + <item name="android:lineSpacingExtra">4sp</item> + <item name="android:lineHeight">16sp</item> + <item name="android:textColor">@color/tv_pip_edu_text</item> + </style> </resources> 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 bf074b0337ef..9230c22c5d95 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/RootTaskDisplayAreaOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/RootTaskDisplayAreaOrganizer.java @@ -145,6 +145,8 @@ public class RootTaskDisplayAreaOrganizer extends DisplayAreaOrganizer { } mDisplayAreasInfo.remove(displayId); + mLeashes.get(displayId).release(); + mLeashes.remove(displayId); ArrayList<RootTaskDisplayAreaListener> listeners = mListeners.get(displayId); if (listeners != null) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellCommandHandlerImpl.java b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellCommandHandlerImpl.java index 908a31dc3e4e..06f4367752fb 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellCommandHandlerImpl.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellCommandHandlerImpl.java @@ -21,6 +21,7 @@ import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSIT 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; @@ -46,11 +47,13 @@ public final class ShellCommandHandlerImpl { 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, @@ -60,6 +63,7 @@ public final class ShellCommandHandlerImpl { Optional<RecentTasksController> recentTasks, ShellExecutor mainExecutor) { mShellTaskOrganizer = shellTaskOrganizer; + mKidsModeTaskOrganizer = kidsModeTaskOrganizer; mRecentTasks = recentTasks; mLegacySplitScreenOptional = legacySplitScreenOptional; mSplitScreenOptional = splitScreenOptional; @@ -92,6 +96,9 @@ public final class ShellCommandHandlerImpl { pw.println(); pw.println(); mRecentTasks.ifPresent(recentTasks -> recentTasks.dump(pw, "")); + pw.println(); + pw.println(); + mKidsModeTaskOrganizer.dump(pw, ""); } @@ -112,8 +119,6 @@ public final class ShellCommandHandlerImpl { return runRemoveFromSideStage(args, pw); case "setSideStagePosition": return runSetSideStagePosition(args, pw); - case "setSideStageVisibility": - return runSetSideStageVisibility(args, pw); case "help": return runHelp(pw); default: @@ -179,18 +184,6 @@ public final class ShellCommandHandlerImpl { return true; } - private boolean runSetSideStageVisibility(String[] args, PrintWriter pw) { - if (args.length < 3) { - // First arguments are "WMShell" and command name. - pw.println("Error: side stage visibility should be provided as arguments"); - return false; - } - final Boolean visible = new Boolean(args[2]); - - mSplitScreenOptional.ifPresent(split -> split.setSideStageVisibility(visible)); - return true; - } - private boolean runHelp(PrintWriter pw) { pw.println("Window Manager Shell commands:"); pw.println(" help"); @@ -208,8 +201,6 @@ public final class ShellCommandHandlerImpl { pw.println(" Enable/Disable outline on the side-stage."); pw.println(" setSideStagePosition <SideStagePosition>"); pw.println(" Sets the position of the side-stage."); - pw.println(" setSideStageVisibility <true/false>"); - pw.println(" Show/hide side-stage."); return true; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellInitImpl.java b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellInitImpl.java index c3ce3627fb0b..62fb840d29d1 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellInitImpl.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellInitImpl.java @@ -29,11 +29,13 @@ 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; @@ -48,12 +50,14 @@ public class ShellInitImpl { 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; @@ -68,12 +72,14 @@ public class ShellInitImpl { 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, @@ -84,12 +90,14 @@ public class ShellInitImpl { 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; @@ -122,6 +130,7 @@ public class ShellInitImpl { 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 @@ -136,6 +145,9 @@ public class ShellInitImpl { mFullscreenUnfoldController.ifPresent(FullscreenUnfoldController::init); mRecentTasks.ifPresent(RecentTasksController::init); + + // Initialize kids mode task organizer + mKidsModeTaskOrganizer.initialize(mStartingWindow); } @ExternalThread diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java index 8b3a35688f11..31f0ef0192ae 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java @@ -98,16 +98,22 @@ public class ShellTaskOrganizer extends TaskOrganizer implements default void onTaskInfoChanged(RunningTaskInfo taskInfo) {} default void onTaskVanished(RunningTaskInfo taskInfo) {} default void onBackPressedOnTaskRoot(RunningTaskInfo taskInfo) {} - /** Whether this task listener supports compat UI. */ + /** Whether this task listener supports compat UI. */ default boolean supportCompatUI() { // All TaskListeners should support compat UI except PIP. return true; } - /** Attaches the a child window surface to the task surface. */ + /** Attaches a child window surface to the task surface. */ default void attachChildSurfaceToTask(int taskId, SurfaceControl.Builder b) { throw new IllegalStateException( "This task listener doesn't support child surface attachment."); } + /** Reparents a child window surface to the task surface. */ + default void reparentChildSurfaceToTask(int taskId, SurfaceControl sc, + SurfaceControl.Transaction t) { + throw new IllegalStateException( + "This task listener doesn't support child surface reparent."); + } default void dump(@NonNull PrintWriter pw, String prefix) {}; } @@ -159,8 +165,12 @@ public class ShellTaskOrganizer extends TaskOrganizer implements private StartingWindowController mStartingWindow; /** - * In charge of showing compat UI. Can be {@code null} if device doesn't support size - * compat. + * In charge of showing compat UI. Can be {@code null} if the device doesn't support size + * compat or if this isn't the main {@link ShellTaskOrganizer}. + * + * <p>NOTE: only the main {@link ShellTaskOrganizer} should have a {@link CompatUIController}, + * and register itself as a {@link CompatUIController.CompatUICallback}. Subclasses should be + * initialized with a {@code null} {@link CompatUIController}. */ @Nullable private final CompatUIController mCompatUI; @@ -190,8 +200,8 @@ public class ShellTaskOrganizer extends TaskOrganizer implements } @VisibleForTesting - ShellTaskOrganizer(ITaskOrganizerController taskOrganizerController, ShellExecutor mainExecutor, - Context context, @Nullable CompatUIController compatUI, + protected ShellTaskOrganizer(ITaskOrganizerController taskOrganizerController, + ShellExecutor mainExecutor, Context context, @Nullable CompatUIController compatUI, Optional<RecentTasksController> recentTasks) { super(taskOrganizerController, mainExecutor); mCompatUI = compatUI; @@ -458,7 +468,7 @@ public class ShellTaskOrganizer extends TaskOrganizer implements newListener.onTaskInfoChanged(taskInfo); } notifyLocusVisibilityIfNeeded(taskInfo); - if (updated || !taskInfo.equalsForSizeCompat(data.getTaskInfo())) { + if (updated || !taskInfo.equalsForCompatUi(data.getTaskInfo())) { // Notify the compat UI if the listener or task info changed. notifyCompatUI(taskInfo, newListener); } @@ -607,6 +617,36 @@ public class ShellTaskOrganizer extends TaskOrganizer implements restartTaskTopActivityProcessIfVisible(info.getTaskInfo().token); } + @Override + public void onCameraControlStateUpdated( + int taskId, @TaskInfo.CameraCompatControlState int state) { + final TaskAppearedInfo info; + synchronized (mLock) { + info = mTasks.get(taskId); + } + if (info == null) { + return; + } + updateCameraCompatControlState(info.getTaskInfo().token, state); + } + + /** Reparents a child window surface to the task surface. */ + public void reparentChildSurfaceToTask(int taskId, SurfaceControl sc, + SurfaceControl.Transaction t) { + final TaskListener taskListener; + synchronized (mLock) { + taskListener = mTasks.contains(taskId) + ? getTaskListener(mTasks.get(taskId).getTaskInfo()) + : null; + } + if (taskListener == null) { + ProtoLog.w(WM_SHELL_TASK_ORG, "Failed to find Task to reparent surface taskId=%d", + taskId); + return; + } + taskListener.reparentChildSurfaceToTask(taskId, sc, t); + } + private void logSizeCompatRestartButtonEventReported(@NonNull TaskAppearedInfo info, int event) { ActivityInfo topActivityInfo = info.getTaskInfo().topActivityInfo; @@ -633,14 +673,11 @@ public class ShellTaskOrganizer extends TaskOrganizer implements // The task is vanished or doesn't support compat UI, notify to remove compat UI // on this Task if there is any. if (taskListener == null || !taskListener.supportCompatUI() - || !taskInfo.topActivityInSizeCompat || !taskInfo.isVisible) { - mCompatUI.onCompatInfoChanged(taskInfo.displayId, taskInfo.taskId, - null /* taskConfig */, null /* taskListener */); + || !taskInfo.hasCompatUI() || !taskInfo.isVisible) { + mCompatUI.onCompatInfoChanged(taskInfo, null /* taskListener */); return; } - - mCompatUI.onCompatInfoChanged(taskInfo.displayId, taskInfo.taskId, - taskInfo.configuration, taskListener); + mCompatUI.onCompatInfoChanged(taskInfo, taskListener); } private TaskListener getTaskListener(RunningTaskInfo runningTaskInfo) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/TaskView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/TaskView.java index 2f3214d1d1ab..d28a68a42b2b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/TaskView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/TaskView.java @@ -41,6 +41,7 @@ import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; import com.android.wm.shell.common.SyncTransactionQueue; +import com.android.wm.shell.transition.Transitions; import java.io.PrintWriter; import java.util.concurrent.Executor; @@ -77,6 +78,7 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback, private final ShellTaskOrganizer mTaskOrganizer; private final Executor mShellExecutor; private final SyncTransactionQueue mSyncQueue; + private final TaskViewTransitions mTaskViewTransitions; private ActivityManager.RunningTaskInfo mTaskInfo; private WindowContainerToken mTaskToken; @@ -86,23 +88,33 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback, private boolean mIsInitialized; private Listener mListener; private Executor mListenerExecutor; - private Rect mObscuredTouchRect; + 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, SyncTransactionQueue syncQueue) { + public TaskView(Context context, ShellTaskOrganizer organizer, + TaskViewTransitions taskViewTransitions, SyncTransactionQueue syncQueue) { super(context, null, 0, 0, true /* disableBackgroundLayer */); mTaskOrganizer = organizer; mShellExecutor = organizer.getExecutor(); mSyncQueue = syncQueue; + mTaskViewTransitions = taskViewTransitions; + if (mTaskViewTransitions != null) { + mTaskViewTransitions.addTaskView(this); + } setUseAlpha(); getHolder().addCallback(this); mGuard.open("release"); } + /** Until all users are converted, we may have mixed-use (eg. Car). */ + private boolean isUsingShellTransitions() { + return mTaskViewTransitions != null && Transitions.ENABLE_SHELL_TRANSITIONS; + } + /** * Only one listener may be set on the view, throws an exception otherwise. */ @@ -129,6 +141,14 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback, @NonNull ActivityOptions options, @Nullable Rect launchBounds) { prepareActivityOptions(options, launchBounds); LauncherApps service = mContext.getSystemService(LauncherApps.class); + if (isUsingShellTransitions()) { + mShellExecutor.execute(() -> { + final WindowContainerTransaction wct = new WindowContainerTransaction(); + wct.startShortcut(mContext.getPackageName(), shortcut, options.toBundle()); + mTaskViewTransitions.startTaskView(wct, this); + }); + return; + } try { service.startShortcut(shortcut, null /* sourceBounds */, options.toBundle()); } catch (Exception e) { @@ -148,6 +168,14 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback, public void startActivity(@NonNull PendingIntent pendingIntent, @Nullable Intent fillInIntent, @NonNull ActivityOptions options, @Nullable Rect launchBounds) { prepareActivityOptions(options, launchBounds); + if (isUsingShellTransitions()) { + mShellExecutor.execute(() -> { + WindowContainerTransaction wct = new WindowContainerTransaction(); + wct.sendPendingIntent(pendingIntent, fillInIntent, options.toBundle()); + mTaskViewTransitions.startTaskView(wct, this); + }); + return; + } try { pendingIntent.send(mContext, 0 /* code */, fillInIntent, null /* onFinished */, null /* handler */, null /* requiredPermission */, @@ -174,25 +202,41 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback, * @param obscuredRect the obscured region of the view. */ public void setObscuredTouchRect(Rect obscuredRect) { - mObscuredTouchRect = obscuredRect; + mObscuredTouchRegion = obscuredRect != null ? new Region(obscuredRect) : null; } /** - * Call when view position or size has changed. Do not call when animating. + * Indicates a region of the view that is not touchable. + * + * @param obscuredRegion the obscured region of the view. */ - public void onLocationChanged() { - if (mTaskToken == null) { - return; - } + 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(); - wct.setBounds(mTaskToken, mTmpRect); + onLocationChanged(wct); mSyncQueue.queue(wct); } @@ -217,6 +261,9 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback, private void performRelease() { getHolder().removeCallback(this); + if (mTaskViewTransitions != null) { + mTaskViewTransitions.removeTaskView(this); + } mShellExecutor.execute(() -> { mTaskOrganizer.removeListener(this); resetTaskInfo(); @@ -254,6 +301,10 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback, @Override public void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash) { + if (isUsingShellTransitions()) { + // Everything else handled by enter transition. + return; + } mTaskInfo = taskInfo; mTaskToken = taskInfo.token; mTaskLeash = leash; @@ -288,6 +339,8 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback, @Override public void onTaskVanished(ActivityManager.RunningTaskInfo taskInfo) { + // Unlike Appeared, we can't yet guarantee that vanish will happen within a transition that + // we know about -- so leave clean-up here even if shell transitions are enabled. if (mTaskToken == null || !mTaskToken.equals(taskInfo.token)) return; if (mListener != null) { @@ -323,10 +376,20 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback, @Override public void attachChildSurfaceToTask(int taskId, SurfaceControl.Builder b) { - if (mTaskInfo.taskId != taskId) { + 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 (mTaskInfo == null || mTaskLeash == null || mTaskInfo.taskId != taskId) { throw new IllegalArgumentException("There is no surface for taskId=" + taskId); } - b.setParent(mTaskLeash); + return mTaskLeash; } @Override @@ -355,6 +418,10 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback, // Nothing to update, task is not yet available return; } + if (isUsingShellTransitions()) { + mTaskViewTransitions.setTaskViewVisible(this, true /* visible */); + return; + } // Reparent the task when this surface is created mTransaction.reparent(mTaskLeash, getSurfaceControl()) .show(mTaskLeash) @@ -380,6 +447,11 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback, return; } + if (isUsingShellTransitions()) { + mTaskViewTransitions.setTaskViewVisible(this, false /* visible */); + return; + } + // Unparent the task when this surface is destroyed mTransaction.reparent(mTaskLeash, null).apply(); updateTaskVisibility(); @@ -405,8 +477,8 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback, mTmpLocation[0] + getWidth(), mTmpLocation[1] + getHeight()); inoutInfo.touchableRegion.op(mTmpRect, Region.Op.DIFFERENCE); - if (mObscuredTouchRect != null) { - inoutInfo.touchableRegion.union(mObscuredTouchRect); + if (mObscuredTouchRegion != null) { + inoutInfo.touchableRegion.op(mObscuredTouchRegion, Region.Op.UNION); } } @@ -421,4 +493,91 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback, super.onDetachedFromWindow(); getViewTreeObserver().removeOnComputeInternalInsetsListener(this); } + + ActivityManager.RunningTaskInfo getTaskInfo() { + return mTaskInfo; + } + + void prepareHideAnimation(@NonNull SurfaceControl.Transaction finishTransaction) { + if (mTaskToken == null) { + // Nothing to update, task is not yet available + return; + } + + finishTransaction.reparent(mTaskLeash, null).apply(); + + if (mListener != null) { + final int taskId = mTaskInfo.taskId; + mListener.onTaskVisibilityChanged(taskId, mSurfaceCreated /* visible */); + } + } + + /** + * Called when the associated Task closes. If the TaskView is just being hidden, prepareHide + * is used instead. + */ + void prepareCloseAnimation() { + if (mTaskToken != null) { + if (mListener != null) { + final int taskId = mTaskInfo.taskId; + mListenerExecutor.execute(() -> { + mListener.onTaskRemovalStarted(taskId); + }); + } + mTaskOrganizer.setInterceptBackPressedOnTaskRoot(mTaskToken, false); + } + resetTaskInfo(); + } + + void prepareOpenAnimation(final boolean newTask, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, + ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash, + WindowContainerTransaction wct) { + mTaskInfo = taskInfo; + mTaskToken = mTaskInfo.token; + mTaskLeash = leash; + if (mSurfaceCreated) { + // Surface is ready, so just reparent the task to this surface control + startTransaction.reparent(mTaskLeash, getSurfaceControl()) + .show(mTaskLeash) + .apply(); + // Also reparent on finishTransaction since the finishTransaction will reparent back + // to its "original" parent by default. + finishTransaction.reparent(mTaskLeash, getSurfaceControl()) + .setPosition(mTaskLeash, 0, 0) + .apply(); + + // TODO: determine if this is really necessary or not + onLocationChanged(wct); + } else { + // The surface has already been destroyed before the task has appeared, + // so go ahead and hide the task entirely + wct.setHidden(mTaskToken, true /* hidden */); + // listener callback is below + } + if (newTask) { + mTaskOrganizer.setInterceptBackPressedOnTaskRoot(mTaskToken, true /* intercept */); + } + + if (mTaskInfo.taskDescription != null) { + int backgroundColor = mTaskInfo.taskDescription.getBackgroundColor(); + setResizeBackgroundColor(startTransaction, backgroundColor); + } + + if (mListener != null) { + final int taskId = mTaskInfo.taskId; + final ComponentName baseActivity = mTaskInfo.baseActivity; + + mListenerExecutor.execute(() -> { + if (newTask) { + mListener.onTaskCreated(taskId, baseActivity); + } + // Even if newTask, send a visibilityChange if the surface was destroyed. + if (!newTask || !mSurfaceCreated) { + mListener.onTaskVisibilityChanged(taskId, mSurfaceCreated /* visible */); + } + }); + } + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/TaskViewFactoryController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/TaskViewFactoryController.java index 8286d102791e..42844b57b92a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/TaskViewFactoryController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/TaskViewFactoryController.java @@ -31,13 +31,24 @@ public class TaskViewFactoryController { private final ShellTaskOrganizer mTaskOrganizer; private final ShellExecutor mShellExecutor; private final SyncTransactionQueue mSyncQueue; + private final TaskViewTransitions mTaskViewTransitions; private final TaskViewFactory mImpl = new TaskViewFactoryImpl(); public TaskViewFactoryController(ShellTaskOrganizer taskOrganizer, + ShellExecutor shellExecutor, SyncTransactionQueue syncQueue, + TaskViewTransitions taskViewTransitions) { + mTaskOrganizer = taskOrganizer; + mShellExecutor = shellExecutor; + mSyncQueue = syncQueue; + mTaskViewTransitions = taskViewTransitions; + } + + public TaskViewFactoryController(ShellTaskOrganizer taskOrganizer, ShellExecutor shellExecutor, SyncTransactionQueue syncQueue) { mTaskOrganizer = taskOrganizer; mShellExecutor = shellExecutor; mSyncQueue = syncQueue; + mTaskViewTransitions = null; } public TaskViewFactory asTaskViewFactory() { @@ -46,7 +57,7 @@ public class TaskViewFactoryController { /** Creates an {@link TaskView} */ public void create(@UiContext Context context, Executor executor, Consumer<TaskView> onCreate) { - TaskView taskView = new TaskView(context, mTaskOrganizer, mSyncQueue); + TaskView taskView = new TaskView(context, mTaskOrganizer, mTaskViewTransitions, mSyncQueue); executor.execute(() -> { onCreate.accept(taskView); }); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/TaskViewTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/TaskViewTransitions.java new file mode 100644 index 000000000000..83335ac24799 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/TaskViewTransitions.java @@ -0,0 +1,253 @@ +/* + * 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; + +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.annotation.NonNull; +import android.annotation.Nullable; +import android.app.ActivityManager; +import android.os.IBinder; +import android.util.Slog; +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.transition.Transitions; + +import java.util.ArrayList; + +/** + * Handles Shell Transitions that involve TaskView tasks. + */ +public class TaskViewTransitions implements Transitions.TransitionHandler { + private static final String TAG = "TaskViewTransitions"; + + private final ArrayList<TaskView> mTaskViews = new ArrayList<>(); + private final ArrayList<PendingTransition> mPending = new ArrayList<>(); + private final Transitions mTransitions; + private final boolean[] mRegistered = new boolean[]{ false }; + + /** + * TaskView makes heavy use of startTransition. Only one shell-initiated transition can be + * in-flight (collecting) at a time (because otherwise, the operations could get merged into + * a single transition). So, keep a queue here until we add a queue in server-side. + */ + private static class PendingTransition { + final @WindowManager.TransitionType int mType; + final WindowContainerTransaction mWct; + final @NonNull TaskView mTaskView; + IBinder mClaimed; + + PendingTransition(@WindowManager.TransitionType int type, + @Nullable WindowContainerTransaction wct, @NonNull TaskView taskView) { + mType = type; + mWct = wct; + mTaskView = taskView; + } + } + + public TaskViewTransitions(Transitions transitions) { + mTransitions = transitions; + // Defer registration until the first TaskView because we want this to be the "first" in + // priority when handling requests. + // TODO(210041388): register here once we have an explicit ordering mechanism. + } + + void addTaskView(TaskView tv) { + synchronized (mRegistered) { + if (!mRegistered[0]) { + mRegistered[0] = true; + mTransitions.addHandler(this); + } + } + mTaskViews.add(tv); + } + + void removeTaskView(TaskView tv) { + mTaskViews.remove(tv); + // Note: Don't unregister handler since this is a singleton with lifetime bound to Shell + } + + /** + * Looks through the pending transitions for one matching `taskView`. + * @param taskView the pending transition should be for this. + * @param closing When true, this only returns a pending transition of the close/hide type. + * Otherwise it selects open/show. + * @param latest When true, this will only check the most-recent pending transition for the + * specified taskView. If it doesn't match `closing`, this will return null even + * 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) { + for (int i = mPending.size() - 1; i >= 0; --i) { + if (mPending.get(i).mTaskView != taskView) continue; + if (Transitions.isClosingType(mPending.get(i).mType) == closing) { + return mPending.get(i); + } + if (latest) { + return null; + } + } + return null; + } + + private PendingTransition findPending(IBinder claimed) { + for (int i = 0; i < mPending.size(); ++i) { + if (mPending.get(i).mClaimed != claimed) continue; + return mPending.get(i); + } + return null; + } + + /** @return whether there are pending transitions on TaskViews. */ + public boolean hasPending() { + return !mPending.isEmpty(); + } + + @Override + public WindowContainerTransaction handleRequest(@NonNull IBinder transition, + @Nullable TransitionRequestInfo request) { + final ActivityManager.RunningTaskInfo triggerTask = request.getTriggerTask(); + if (triggerTask == null) { + return null; + } + final TaskView taskView = findTaskView(triggerTask); + if (taskView == null) return null; + // Opening types should all be initiated by shell + if (!Transitions.isClosingType(request.getType())) return null; + PendingTransition pending = findPending(taskView, true /* closing */, false /* latest */); + if (pending == null) { + pending = new PendingTransition(request.getType(), null, taskView); + } + if (pending.mClaimed != null) { + throw new IllegalStateException("Task is closing in 2 collecting transitions?" + + " This state doesn't make sense"); + } + pending.mClaimed = transition; + return new WindowContainerTransaction(); + } + + private TaskView 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)) { + return mTaskViews.get(i); + } + } + return null; + } + + void startTaskView(WindowContainerTransaction wct, TaskView taskView) { + mPending.add(new PendingTransition(TRANSIT_OPEN, wct, taskView)); + startNextTransition(); + } + + void setTaskViewVisible(TaskView 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. + return; + } + if (taskView.getTaskInfo() == null) { + // Nothing to update, task is not yet available + return; + } + final WindowContainerTransaction wct = new WindowContainerTransaction(); + wct.setHidden(taskView.getTaskInfo().token, !visible /* hidden */); + pending = new PendingTransition( + visible ? TRANSIT_TO_FRONT : TRANSIT_TO_BACK, wct, taskView); + mPending.add(pending); + startNextTransition(); + // visibility is reported in transition. + } + + private void startNextTransition() { + if (mPending.isEmpty()) return; + final PendingTransition pending = mPending.get(0); + if (pending.mClaimed != null) { + // Wait for this to start animating. + return; + } + pending.mClaimed = mTransitions.startTransition(pending.mType, pending.mWct, this); + } + + @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 (tasks.isEmpty()) { + Slog.e(TAG, "Got a TaskView transition with no task."); + return false; + } + WindowContainerTransaction wct = null; + for (int i = 0; i < tasks.size(); ++i) { + TransitionInfo.Change chg = tasks.get(i); + if (Transitions.isClosingType(chg.getMode())) { + final boolean isHide = chg.getMode() == TRANSIT_TO_BACK; + TaskView tv = findTaskView(chg.getTaskInfo()); + if (tv == null) { + throw new IllegalStateException("TaskView transition is closing a " + + "non-taskview task "); + } + if (isHide) { + tv.prepareHideAnimation(finishTransaction); + } else { + tv.prepareCloseAnimation(); + } + } else if (Transitions.isOpeningType(chg.getMode())) { + final boolean taskIsNew = chg.getMode() == TRANSIT_OPEN; + if (wct == null) wct = new WindowContainerTransaction(); + TaskView tv = taskView; + if (!taskIsNew) { + tv = findTaskView(chg.getTaskInfo()); + if (tv == null) { + throw new IllegalStateException("TaskView transition is showing a " + + "non-taskview task "); + } + } + 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()); + } + } + // No animation, just show it immediately. + startTransaction.apply(); + finishTransaction.apply(); + finishCallback.onTransitionFinished(wct, null /* wctCB */); + startNextTransition(); + return true; + } +} 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 255e4d2c0d44..b483fe03e80f 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 @@ -19,12 +19,13 @@ package com.android.wm.shell.animation import android.util.ArrayMap import android.util.Log import android.view.View -import androidx.dynamicanimation.animation.AnimationHandler 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 + import com.android.wm.shell.animation.PhysicsAnimator.Companion.getInstance import java.lang.ref.WeakReference import java.util.WeakHashMap @@ -124,10 +125,10 @@ class PhysicsAnimator<T> private constructor (target: T) { private var defaultFling: FlingConfig = globalDefaultFling /** - * AnimationHandler to use if it need custom AnimationHandler, if this is null, it will use - * the default AnimationHandler in the DynamicAnimation. + * FrameCallbackScheduler to use if it need custom FrameCallbackScheduler, if this is null, + * it will use the default FrameCallbackScheduler in the DynamicAnimation. */ - private var customAnimationHandler: AnimationHandler? = null + private var customScheduler: FrameCallbackScheduler? = null /** * Internal listeners that respond to DynamicAnimations updating and ending, and dispatch to @@ -454,11 +455,11 @@ class PhysicsAnimator<T> private constructor (target: T) { } /** - * Set the custom AnimationHandler for all aniatmion in this animator. Set this with null for - * restoring to default AnimationHandler. + * Set the custom FrameCallbackScheduler for all aniatmion in this animator. Set this with null for + * restoring to default FrameCallbackScheduler. */ - fun setCustomAnimationHandler(handler: AnimationHandler) { - this.customAnimationHandler = handler + fun setCustomScheduler(scheduler: FrameCallbackScheduler) { + this.customScheduler = scheduler } /** Starts the animations! */ @@ -510,10 +511,9 @@ class PhysicsAnimator<T> private constructor (target: T) { // springs) on this property before flinging. cancel(animatedProperty) - // Apply the custom animation handler if it not null + // Apply the custom animation scheduler if it not null val flingAnim = getFlingAnimation(animatedProperty, target) - flingAnim.animationHandler = - customAnimationHandler ?: flingAnim.animationHandler + flingAnim.scheduler = customScheduler ?: flingAnim.scheduler // Apply the configuration and start the animation. flingAnim.also { flingConfig.applyToAnimation(it) }.start() @@ -529,17 +529,16 @@ class PhysicsAnimator<T> private constructor (target: T) { // Apply the configuration and start the animation. val springAnim = getSpringAnimation(animatedProperty, target) - // If customAnimationHander is exist and has not been set to the animation, + // If customScheduler is exist and has not been set to the animation, // it should set here. - if (customAnimationHandler != null && - springAnim.animationHandler != customAnimationHandler) { + if (customScheduler != null && + springAnim.scheduler != customScheduler) { // Cancel the animation before set animation handler if (springAnim.isRunning) { cancel(animatedProperty) } - // Apply the custom animation handler if it not null - springAnim.animationHandler = - customAnimationHandler ?: springAnim.animationHandler + // Apply the custom scheduler handler if it not null + springAnim.scheduler = customScheduler ?: springAnim.scheduler } // Apply the configuration and start the animation. @@ -597,10 +596,9 @@ class PhysicsAnimator<T> private constructor (target: T) { } } - // Apply the custom animation handler if it not null + // Apply the custom animation scheduler if it not null val springAnim = getSpringAnimation(animatedProperty, target) - springAnim.animationHandler = - customAnimationHandler ?: springAnim.animationHandler + springAnim.scheduler = customScheduler ?: springAnim.scheduler // Apply the configuration and start the spring animation. springAnim.also { springConfig.applyToAnimation(it) }.start() diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPair.java b/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPair.java index 1f21937b5025..3f0b01bef0ce 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPair.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPair.java @@ -19,7 +19,6 @@ package com.android.wm.shell.apppairs; import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; -import static android.view.WindowManagerPolicyConstants.SPLIT_DIVIDER_LAYER; import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT; @@ -83,7 +82,7 @@ class AppPair implements ShellTaskOrganizer.TaskListener, SplitLayout.SplitLayou public void onLeashReady(SurfaceControl leash) { mSyncQueue.runInSync(t -> t .show(leash) - .setLayer(leash, SPLIT_DIVIDER_LAYER) + .setLayer(leash, Integer.MAX_VALUE) .setPosition(leash, mSplitLayout.getDividerBounds().left, mSplitLayout.getDividerBounds().top)); @@ -132,7 +131,7 @@ class AppPair implements ShellTaskOrganizer.TaskListener, SplitLayout.SplitLayou mDisplayController.getDisplayContext(mRootTaskInfo.displayId), mRootTaskInfo.configuration, this /* layoutChangeListener */, mParentContainerCallbacks, mDisplayImeController, mController.getTaskOrganizer(), - true /* applyDismissingParallax */); + SplitLayout.PARALLAX_DISMISSING); mDisplayInsetsController.addInsetsChangedListener(mRootTaskInfo.displayId, mSplitLayout); final WindowContainerToken token1 = task1.token; @@ -275,12 +274,22 @@ class AppPair implements ShellTaskOrganizer.TaskListener, SplitLayout.SplitLayou @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) { - b.setParent(mRootTaskLeash); + return mRootTaskLeash; } else if (getTaskId1() == taskId) { - b.setParent(mTaskLeash1); + return mTaskLeash1; } else if (getTaskId2() == taskId) { - b.setParent(mTaskLeash2); + return mTaskLeash2; } else { throw new IllegalArgumentException("There is no surface for taskId=" + taskId); } @@ -291,8 +300,10 @@ class AppPair implements ShellTaskOrganizer.TaskListener, SplitLayout.SplitLayou final String innerPrefix = prefix + " "; final String childPrefix = innerPrefix + " "; pw.println(prefix + this); - pw.println(innerPrefix + "Root taskId=" + getRootTaskId() - + " winMode=" + mRootTaskInfo.getWindowingMode()); + 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()); @@ -316,13 +327,15 @@ class AppPair implements ShellTaskOrganizer.TaskListener, SplitLayout.SplitLayou @Override public void onLayoutPositionChanging(SplitLayout layout) { mSyncQueue.runInSync(t -> - layout.applySurfaceChanges(t, mTaskLeash1, mTaskLeash2, mDimLayer1, mDimLayer2)); + 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)); + layout.applySurfaceChanges(t, mTaskLeash1, mTaskLeash2, mDimLayer1, mDimLayer2, + true /* applyResizingOffset */)); } @Override @@ -331,7 +344,8 @@ class AppPair implements ShellTaskOrganizer.TaskListener, SplitLayout.SplitLayou layout.applyTaskChanges(wct, mTaskInfo1, mTaskInfo2); mSyncQueue.queue(wct); mSyncQueue.runInSync(t -> - layout.applySurfaceChanges(t, mTaskLeash1, mTaskLeash2, mDimLayer1, mDimLayer2)); + layout.applySurfaceChanges(t, mTaskLeash1, mTaskLeash2, mDimLayer1, mDimLayer2, + false /* applyResizingOffset */)); } @Override 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 new file mode 100644 index 000000000000..8c0affb0a432 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimation.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.back; + +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.window.BackEvent; + +import com.android.wm.shell.common.annotations.ExternalThread; + +/** + * Interface for external process to get access to the Back animation related methods. + */ +@ExternalThread +public interface BackAnimation { + + /** + * Called when a {@link MotionEvent} is generated by a back gesture. + * + * @param touchX the X touch position of the {@link MotionEvent}. + * @param touchY the Y touch position of the {@link MotionEvent}. + * @param keyAction the original {@link KeyEvent#getAction()} when the event was dispatched to + * the process. This is forwarded separately because the input pipeline may mutate + * the {#event} action state later. + * @param swipeEdge the edge from which the swipe begins. + */ + void onBackMotion(float touchX, float touchY, int keyAction, + @BackEvent.SwipeEdge int swipeEdge); + + /** + * Sets whether the back gesture is past the trigger threshold or not. + */ + 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); +} 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 new file mode 100644 index 000000000000..0cf2b28921e1 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java @@ -0,0 +1,511 @@ +/* + * 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.back; + +import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission; +import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BACK_PREVIEW; + +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.net.Uri; +import android.os.Handler; +import android.os.RemoteException; +import android.os.SystemProperties; +import android.os.UserHandle; +import android.provider.Settings.Global; +import android.util.Log; +import android.view.MotionEvent; +import android.view.RemoteAnimationTarget; +import android.view.SurfaceControl; +import android.window.BackEvent; +import android.window.BackNavigationInfo; +import android.window.IOnBackInvokedCallback; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.protolog.common.ProtoLog; +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 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 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); + private final AtomicBoolean mEnableAnimations = new AtomicBoolean(false); + /** + * Max duration to wait for a transition to finish before accepting another gesture start + * request. + */ + private static final long MAX_TRANSITION_DURATION = 2000; + + /** + * Location of the initial touch event of the back gesture. + */ + private final PointF mInitTouchLocation = new PointF(); + + /** + * Raw delta between {@link #mInitTouchLocation} and the last touch location. + */ + private final Point mTouchEventDelta = new Point(); + private final ShellExecutor mShellExecutor; + + /** True when a back gesture is ongoing */ + private boolean mBackGestureStarted = false; + + /** Tracks if an uninterruptible transition is in progress */ + private boolean mTransitionInProgress = false; + /** @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; + }; + + public BackAnimationController( + @NonNull @ShellMainThread ShellExecutor shellExecutor, + @NonNull @ShellBackgroundThread Handler backgroundHandler, + Context context) { + this(shellExecutor, backgroundHandler, new SurfaceControl.Transaction(), + ActivityTaskManager.getService(), context, context.getContentResolver()); + } + + @VisibleForTesting + BackAnimationController(@NonNull @ShellMainThread ShellExecutor shellExecutor, + @NonNull @ShellBackgroundThread Handler handler, + @NonNull SurfaceControl.Transaction transaction, + @NonNull IActivityTaskManager activityTaskManager, + Context context, ContentResolver contentResolver) { + mShellExecutor = shellExecutor; + mTransaction = transaction; + mActivityTaskManager = activityTaskManager; + mContext = context; + setupAnimationDeveloperSettingsObserver(contentResolver, handler); + } + + private void setupAnimationDeveloperSettingsObserver( + @NonNull ContentResolver contentResolver, + @NonNull @ShellBackgroundThread final Handler backgroundHandler) { + ContentObserver settingsObserver = new ContentObserver(backgroundHandler) { + @Override + public void onChange(boolean selfChange, Uri uri) { + updateEnableAnimationFromSetting(); + } + }; + contentResolver.registerContentObserver( + Global.getUriFor(Global.ENABLE_BACK_ANIMATION), + false, settingsObserver, UserHandle.USER_SYSTEM + ); + updateEnableAnimationFromSetting(); + } + + @ShellBackgroundThread + private void updateEnableAnimationFromSetting() { + int settingValue = Global.getInt(mContext.getContentResolver(), + 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); + } + + public BackAnimation getBackAnimationImpl() { + return mBackAnimation; + } + + private final BackAnimation mBackAnimation = new BackAnimationImpl(); + + @Override + public Context getContext() { + return mContext; + } + + @Override + public ShellExecutor getRemoteCallExecutor() { + return mShellExecutor; + } + + 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) { + mShellExecutor.execute(() -> onMotionEvent(touchX, touchY, keyAction, swipeEdge)); + } + + @Override + public void setTriggerBack(boolean triggerBack) { + mShellExecutor.execute(() -> BackAnimationController.this.setTriggerBack(triggerBack)); + } + + @Override + public void setSwipeThresholds(float triggerThreshold, float progressThreshold) { + mShellExecutor.execute(() -> BackAnimationController.this.setSwipeThresholds( + triggerThreshold, progressThreshold)); + } + } + + private static class IBackAnimationImpl extends IBackAnimation.Stub { + private BackAnimationController mController; + + IBackAnimationImpl(BackAnimationController controller) { + mController = controller; + } + + @Override + public void setBackToLauncherCallback(IOnBackInvokedCallback callback) { + executeRemoteCallWithTaskPermission(mController, "setBackToLauncherCallback", + (controller) -> controller.setBackToLauncherCallback(callback)); + } + + @Override + public void clearBackToLauncherCallback() { + executeRemoteCallWithTaskPermission(mController, "clearBackToLauncherCallback", + (controller) -> controller.clearBackToLauncherCallback()); + } + + @Override + public void onBackToLauncherAnimationFinished() { + executeRemoteCallWithTaskPermission(mController, "onBackToLauncherAnimationFinished", + (controller) -> controller.onBackToLauncherAnimationFinished()); + } + + void invalidate() { + mController = null; + } + } + + @VisibleForTesting + void setBackToLauncherCallback(IOnBackInvokedCallback callback) { + mBackToLauncherCallback = callback; + } + + private void clearBackToLauncherCallback() { + mBackToLauncherCallback = null; + } + + @VisibleForTesting + void onBackToLauncherAnimationFinished() { + if (mBackNavigationInfo != null) { + IOnBackInvokedCallback callback = mBackNavigationInfo.getOnBackInvokedCallback(); + if (mTriggerBack) { + dispatchOnBackInvoked(callback); + } else { + dispatchOnBackCancelled(callback); + } + } + finishAnimation(); + } + + /** + * Called when a new motion event needs to be transferred to this + * {@link BackAnimationController} + */ + public void onMotionEvent(float touchX, float touchY, int keyAction, + @BackEvent.SwipeEdge int swipeEdge) { + if (mTransitionInProgress) { + return; + } + if (keyAction == MotionEvent.ACTION_MOVE) { + if (!mBackGestureStarted) { + // 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); + } + onMove(touchX, touchY, swipeEdge); + } else if (keyAction == MotionEvent.ACTION_UP || keyAction == MotionEvent.ACTION_CANCEL) { + ProtoLog.d(WM_SHELL_BACK_PREVIEW, + "Finishing gesture with event action: %d", keyAction); + onGestureFinished(); + } + } + + private void initAnimation(float touchX, float touchY) { + ProtoLog.d(WM_SHELL_BACK_PREVIEW, "initAnimation mMotionStarted=%b", mBackGestureStarted); + if (mBackGestureStarted || mBackNavigationInfo != null) { + Log.e(TAG, "Animation is being initialized but is already started."); + finishAnimation(); + } + + mInitTouchLocation.set(touchX, touchY); + mBackGestureStarted = true; + + try { + boolean requestAnimation = mEnableAnimations.get(); + mBackNavigationInfo = mActivityTaskManager.startBackNavigation(requestAnimation); + onBackNavigationInfoReceived(mBackNavigationInfo); + } catch (RemoteException remoteException) { + Log.e(TAG, "Failed to initAnimation", remoteException); + finishAnimation(); + } + } + + private void onBackNavigationInfoReceived(@Nullable BackNavigationInfo backNavigationInfo) { + 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()); + } + mTransaction.apply(); + } else if (shouldDispatchToLauncher(backType)) { + targetCallback = mBackToLauncherCallback; + } else if (backType == BackNavigationInfo.TYPE_CALLBACK) { + targetCallback = mBackNavigationInfo.getOnBackInvokedCallback(); + } + 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. "); + 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); + } + + 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 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 boolean shouldDispatchToLauncher(int backType) { + return backType == BackNavigationInfo.TYPE_RETURN_TO_HOME + && mBackToLauncherCallback != null + && mEnableAnimations.get(); + } + + private static void dispatchOnBackStarted(IOnBackInvokedCallback callback) { + if (callback == null) { + return; + } + try { + callback.onBackStarted(); + } catch (RemoteException e) { + Log.e(TAG, "dispatchOnBackStarted error: ", e); + } + } + + private static void dispatchOnBackInvoked(IOnBackInvokedCallback callback) { + if (callback == null) { + return; + } + try { + callback.onBackInvoked(); + } catch (RemoteException e) { + Log.e(TAG, "dispatchOnBackInvoked error: ", e); + } + } + + private static void dispatchOnBackCancelled(IOnBackInvokedCallback callback) { + if (callback == null) { + return; + } + try { + callback.onBackCancelled(); + } catch (RemoteException e) { + Log.e(TAG, "dispatchOnBackCancelled error: ", e); + } + } + + private static void dispatchOnBackProgressed(IOnBackInvokedCallback callback, + BackEvent backEvent) { + if (callback == null) { + return; + } + try { + callback.onBackProgressed(backEvent); + } catch (RemoteException e) { + Log.e(TAG, "dispatchOnBackProgressed error: ", e); + } + } + + /** + * Sets to true when the back gesture has passed the triggering threshold, false otherwise. + */ + public void setTriggerBack(boolean triggerBack) { + if (mTransitionInProgress) { + return; + } + mTriggerBack = 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) { + return; + } + RemoteAnimationTarget animationTarget = backNavigationInfo.getDepartingAnimationTarget(); + if (animationTarget != null) { + if (animationTarget.leash != null && animationTarget.leash.isValid()) { + mTransaction.remove(animationTarget.leash); + } + } + SurfaceControl screenshotSurface = backNavigationInfo.getScreenshotSurface(); + if (screenshotSurface != null && screenshotSurface.isValid()) { + mTransaction.remove(screenshotSurface); + } + mTransaction.apply(); + stopTransition(); + backNavigationInfo.onBackNavigationFinished(triggerBack); + } + + private void startTransition() { + if (mTransitionInProgress) { + return; + } + mTransitionInProgress = true; + mShellExecutor.executeDelayed(mResetTransitionRunnable, MAX_TRANSITION_DURATION); + } + + private void stopTransition() { + if (!mTransitionInProgress) { + return; + } + mShellExecutor.removeCallbacks(mResetTransitionRunnable); + mTransitionInProgress = false; + } +} 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 new file mode 100644 index 000000000000..6311f879fd45 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/IBackAnimation.aidl @@ -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.back; + +import android.window.IOnBackInvokedCallback; + +/** + * Interface for Launcher process to register back invocation callbacks. + */ +interface IBackAnimation { + + /** + * Sets a {@link IOnBackInvokedCallback} to be invoked when + * back navigation has type {@link BackNavigationInfo#TYPE_RETURN_TO_HOME}. + */ + void setBackToLauncherCallback(in IOnBackInvokedCallback callback); + + /** + * Clears the previously registered {@link IOnBackInvokedCallback}. + */ + 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. + */ + void onBackToLauncherAnimationFinished(); +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BadgedImageView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BadgedImageView.java index d92e2ccc77bd..f1ee8fa38485 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BadgedImageView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BadgedImageView.java @@ -15,27 +15,28 @@ */ package com.android.wm.shell.bubbles; -import static android.graphics.Paint.ANTI_ALIAS_FLAG; -import static android.graphics.Paint.DITHER_FLAG; -import static android.graphics.Paint.FILTER_BITMAP_FLAG; - +import android.annotation.DrawableRes; import android.annotation.Nullable; import android.content.Context; +import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Outline; -import android.graphics.Paint; -import android.graphics.PaintFlagsDrawFilter; import android.graphics.Path; import android.graphics.Rect; +import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.util.PathParser; +import android.view.LayoutInflater; import android.view.View; import android.view.ViewOutlineProvider; import android.widget.ImageView; +import androidx.constraintlayout.widget.ConstraintLayout; + import com.android.launcher3.icons.DotRenderer; import com.android.launcher3.icons.IconNormalizer; +import com.android.wm.shell.R; import com.android.wm.shell.animation.Interpolators; import java.util.EnumSet; @@ -47,14 +48,12 @@ import java.util.EnumSet; * Badge = the icon associated with the app that created this bubble, this will show work profile * badge if appropriate. */ -public class BadgedImageView extends ImageView { +public class BadgedImageView extends ConstraintLayout { /** Same value as Launcher3 dot code */ public static final float WHITE_SCRIM_ALPHA = 0.54f; /** Same as value in Launcher3 IconShape */ public static final int DEFAULT_PATH_SIZE = 100; - /** Same as value in Launcher3 BaseIconFactory */ - private static final float ICON_BADGE_SCALE = 0.444f; /** * Flags that suppress the visibility of the 'new' dot, for one reason or another. If any of @@ -74,6 +73,9 @@ public class BadgedImageView extends ImageView { private final EnumSet<SuppressionFlag> mDotSuppressionFlags = EnumSet.of(SuppressionFlag.FLYOUT_VISIBLE); + private final ImageView mBubbleIcon; + private final ImageView mAppIcon; + private float mDotScale = 0f; private float mAnimatingToDotScale = 0f; private boolean mDotIsAnimating = false; @@ -86,7 +88,6 @@ public class BadgedImageView extends ImageView { private DotRenderer.DrawParams mDrawParams; private int mDotColor; - private Paint mPaint = new Paint(ANTI_ALIAS_FLAG); private Rect mTempBounds = new Rect(); public BadgedImageView(Context context) { @@ -104,6 +105,19 @@ public class BadgedImageView extends ImageView { public BadgedImageView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); + // We manage positioning the badge ourselves + setLayoutDirection(LAYOUT_DIRECTION_LTR); + + LayoutInflater.from(context).inflate(R.layout.badged_image_view, this); + + mBubbleIcon = findViewById(R.id.icon_view); + mAppIcon = findViewById(R.id.app_icon_view); + + final TypedArray ta = mContext.obtainStyledAttributes(attrs, new int[]{android.R.attr.src}, + defStyleAttr, defStyleRes); + mBubbleIcon.setImageResource(ta.getResourceId(0, 0)); + ta.recycle(); + mDrawParams = new DotRenderer.DrawParams(); setFocusable(true); @@ -135,7 +149,6 @@ public class BadgedImageView extends ImageView { public void showDotAndBadge(boolean onLeft) { removeDotSuppressionFlag(BadgedImageView.SuppressionFlag.BEHIND_STACK); animateDotBadgePositions(onLeft); - } public void hideDotAndBadge(boolean onLeft) { @@ -149,6 +162,8 @@ public class BadgedImageView extends ImageView { */ public void setRenderedBubble(BubbleViewProvider bubble) { mBubble = bubble; + mBubbleIcon.setImageBitmap(bubble.getBubbleIcon()); + mAppIcon.setImageBitmap(bubble.getAppBadge()); if (mDotSuppressionFlags.contains(SuppressionFlag.BEHIND_STACK)) { hideBadge(); } else { @@ -159,8 +174,8 @@ public class BadgedImageView extends ImageView { } @Override - public void onDraw(Canvas canvas) { - super.onDraw(canvas); + public void dispatchDraw(Canvas canvas) { + super.dispatchDraw(canvas); if (!shouldDrawDot()) { return; @@ -168,7 +183,7 @@ public class BadgedImageView extends ImageView { getDrawingRect(mTempBounds); - mDrawParams.color = mDotColor; + mDrawParams.dotColor = mDotColor; mDrawParams.iconBounds = mTempBounds; mDrawParams.leftAlign = mOnLeft; mDrawParams.scale = mDotScale; @@ -176,6 +191,20 @@ public class BadgedImageView extends ImageView { mDotRenderer.draw(canvas, mDrawParams); } + /** + * Set drawable resource shown as the icon + */ + public void setIconImageResource(@DrawableRes int drawable) { + mBubbleIcon.setImageResource(drawable); + } + + /** + * Get icon drawable + */ + public Drawable getIconDrawable() { + return mBubbleIcon.getDrawable(); + } + /** Adds a dot suppression flag, updating dot visibility if needed. */ void addDotSuppressionFlag(SuppressionFlag flag) { if (mDotSuppressionFlags.add(flag)) { @@ -279,7 +308,6 @@ public class BadgedImageView extends ImageView { showBadge(); } - /** Whether to draw the dot in onDraw(). */ private boolean shouldDrawDot() { // Always render the dot if it's animating, since it could be animating out. Otherwise, show @@ -323,31 +351,29 @@ public class BadgedImageView extends ImageView { } void showBadge() { - Bitmap badge = mBubble.getAppBadge(); - if (badge == null) { - setImageBitmap(mBubble.getBubbleIcon()); + Bitmap appBadgeBitmap = mBubble.getAppBadge(); + if (appBadgeBitmap == null) { + mAppIcon.setVisibility(GONE); return; } - Canvas bubbleCanvas = new Canvas(); - Bitmap noBadgeBubble = mBubble.getBubbleIcon(); - Bitmap bubble = noBadgeBubble.copy(noBadgeBubble.getConfig(), /* isMutable */ true); - - bubbleCanvas.setDrawFilter(new PaintFlagsDrawFilter(DITHER_FLAG, FILTER_BITMAP_FLAG)); - bubbleCanvas.setBitmap(bubble); - final int bubbleSize = bubble.getWidth(); - final int badgeSize = (int) (ICON_BADGE_SCALE * bubbleSize); - Rect dest = new Rect(); + + int translationX; if (mOnLeft) { - dest.set(0, bubbleSize - badgeSize, badgeSize, bubbleSize); + translationX = -(mBubble.getBubbleIcon().getWidth() - appBadgeBitmap.getWidth()); } else { - dest.set(bubbleSize - badgeSize, bubbleSize - badgeSize, bubbleSize, bubbleSize); + translationX = 0; } - bubbleCanvas.drawBitmap(badge, null /* src */, dest, mPaint); - bubbleCanvas.setBitmap(null); - setImageBitmap(bubble); + + mAppIcon.setTranslationX(translationX); + mAppIcon.setVisibility(VISIBLE); } void hideBadge() { - setImageBitmap(mBubble.getBubbleIcon()); + mAppIcon.setVisibility(GONE); + } + + @Override + public String toString() { + return "BadgedImageView{" + mBubble + "}"; } } 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 8d43f1375a8c..31fc6a5be589 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 @@ -47,7 +47,6 @@ import android.util.Log; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.logging.InstanceId; -import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.List; import java.util.Objects; @@ -72,7 +71,7 @@ public class Bubble implements BubbleViewProvider { private long mLastAccessed; @Nullable - private Bubbles.SuppressionChangedListener mSuppressionListener; + private Bubbles.BubbleMetadataFlagListener mBubbleMetadataFlagListener; /** Whether the bubble should show a dot for the notification indicating updated content. */ private boolean mShowBubbleUpdateDot = true; @@ -108,6 +107,8 @@ public class Bubble implements BubbleViewProvider { private Bitmap mBubbleBitmap; // The app badge for the bubble private Bitmap mBadgeBitmap; + // App badge without any markings for important conversations + private Bitmap mRawBadgeBitmap; private int mDotColor; private Path mDotPath; private int mFlags; @@ -191,13 +192,13 @@ public class Bubble implements BubbleViewProvider { @VisibleForTesting(visibility = PRIVATE) public Bubble(@NonNull final BubbleEntry entry, - @Nullable final Bubbles.SuppressionChangedListener listener, + @Nullable final Bubbles.BubbleMetadataFlagListener listener, final Bubbles.PendingIntentCanceledListener intentCancelListener, Executor mainExecutor) { mKey = entry.getKey(); mGroupKey = entry.getGroupKey(); mLocusId = entry.getLocusId(); - mSuppressionListener = listener; + mBubbleMetadataFlagListener = listener; mIntentCancelListener = intent -> { if (mIntent != null) { mIntent.unregisterCancelListener(mIntentCancelListener); @@ -248,6 +249,11 @@ public class Bubble implements BubbleViewProvider { } @Override + public Bitmap getRawAppBadge() { + return mRawBadgeBitmap; + } + + @Override public int getDotColor() { return mDotColor; } @@ -357,13 +363,15 @@ public class Bubble implements BubbleViewProvider { * @param context the context for the bubble. * @param controller the bubble controller. * @param stackView the stackView the bubble is eventually added to. - * @param iconFactory the iconfactory use to create badged images for the bubble. + * @param iconFactory the icon factory use to create images for the bubble. + * @param badgeIconFactory the icon factory to create app badges for the bubble. */ void inflate(BubbleViewInfoTask.Callback callback, Context context, BubbleController controller, BubbleStackView stackView, BubbleIconFactory iconFactory, + BubbleBadgeIconFactory badgeIconFactory, boolean skipInflation) { if (isBubbleLoading()) { mInflationTask.cancel(true /* mayInterruptIfRunning */); @@ -373,6 +381,7 @@ public class Bubble implements BubbleViewProvider { controller, stackView, iconFactory, + badgeIconFactory, skipInflation, callback, mMainExecutor); @@ -409,6 +418,7 @@ public class Bubble implements BubbleViewProvider { mFlyoutMessage = info.flyoutMessage; mBadgeBitmap = info.badgeBitmap; + mRawBadgeBitmap = info.mRawBadgeBitmap; mBubbleBitmap = info.bubbleBitmap; mDotColor = info.dotColor; @@ -596,8 +606,8 @@ public class Bubble implements BubbleViewProvider { mFlags &= ~Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION; } - if (showInShade() != prevShowInShade && mSuppressionListener != null) { - mSuppressionListener.onBubbleNotificationSuppressionChange(this); + if (showInShade() != prevShowInShade && mBubbleMetadataFlagListener != null) { + mBubbleMetadataFlagListener.onBubbleMetadataFlagChanged(this); } } @@ -616,8 +626,8 @@ public class Bubble implements BubbleViewProvider { } else { mFlags &= ~Notification.BubbleMetadata.FLAG_SUPPRESS_BUBBLE; } - if (prevSuppressed != suppressBubble && mSuppressionListener != null) { - mSuppressionListener.onBubbleNotificationSuppressionChange(this); + if (prevSuppressed != suppressBubble && mBubbleMetadataFlagListener != null) { + mBubbleMetadataFlagListener.onBubbleMetadataFlagChanged(this); } } @@ -761,12 +771,17 @@ public class Bubble implements BubbleViewProvider { return isEnabled(Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE); } - void setShouldAutoExpand(boolean shouldAutoExpand) { + @VisibleForTesting + public void setShouldAutoExpand(boolean shouldAutoExpand) { + boolean prevAutoExpand = shouldAutoExpand(); if (shouldAutoExpand) { enable(Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE); } else { disable(Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE); } + if (prevAutoExpand != shouldAutoExpand && mBubbleMetadataFlagListener != null) { + mBubbleMetadataFlagListener.onBubbleMetadataFlagChanged(this); + } } public void setIsBubble(final boolean isBubble) { @@ -789,6 +804,10 @@ public class Bubble implements BubbleViewProvider { return (mFlags & option) != 0; } + public int getFlags() { + return mFlags; + } + @Override public String toString() { return "Bubble{" + mKey + '}'; @@ -797,8 +816,7 @@ public class Bubble implements BubbleViewProvider { /** * Description of current bubble state. */ - public void dump( - @NonNull FileDescriptor fd, @NonNull PrintWriter pw, @NonNull String[] args) { + public void dump(@NonNull PrintWriter pw, @NonNull String[] args) { pw.print("key: "); pw.println(mKey); pw.print(" showInShade: "); pw.println(showInShade()); pw.print(" showDot: "); pw.println(showDot()); @@ -808,7 +826,7 @@ public class Bubble implements BubbleViewProvider { pw.print(" suppressNotif: "); pw.println(shouldSuppressNotification()); pw.print(" autoExpand: "); pw.println(shouldAutoExpand()); if (mExpandedView != null) { - mExpandedView.dump(fd, pw, args); + mExpandedView.dump(pw, args); } } 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 new file mode 100644 index 000000000000..4eeb20769e09 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleBadgeIconFactory.java @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.bubbles; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Path; +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; + +/** + * Factory for creating app badge icons that are shown on bubbles. + */ +public class BubbleBadgeIconFactory extends BaseIconFactory { + + public BubbleBadgeIconFactory(Context context) { + super(context, context.getResources().getConfiguration().densityDpi, + context.getResources().getDimensionPixelSize(R.dimen.bubble_badge_size)); + } + + /** + * Returns a {@link BitmapInfo} for the app-badge that is shown on top of each bubble. This + * 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); + } + + if (isImportantConversation) { + final float ringStrokeWidth = mContext.getResources().getDimensionPixelSize( + com.android.internal.R.dimen.importance_ring_stroke_width); + final int importantConversationColor = 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); + } + } + + 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; + } +} 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 ec59fad3e95b..f427a2c4bc95 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 @@ -17,11 +17,14 @@ package com.android.wm.shell.bubbles; import static android.app.ActivityTaskManager.INVALID_TASK_ID; +import static android.service.notification.NotificationListenerService.NOTIFICATION_CHANNEL_OR_GROUP_DELETED; +import static android.service.notification.NotificationListenerService.NOTIFICATION_CHANNEL_OR_GROUP_UPDATED; import static android.service.notification.NotificationListenerService.REASON_CANCEL; 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.BubbleDebugConfig.DEBUG_BUBBLE_CONTROLLER; 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; @@ -42,8 +45,12 @@ import android.annotation.NonNull; import android.annotation.UserIdInt; import android.app.ActivityManager; import android.app.Notification; +import android.app.NotificationChannel; import android.app.PendingIntent; +import android.content.BroadcastReceiver; import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; import android.content.pm.ActivityInfo; import android.content.pm.LauncherApps; import android.content.pm.PackageManager; @@ -59,6 +66,7 @@ import android.os.Handler; import android.os.RemoteException; import android.os.ServiceManager; import android.os.UserHandle; +import android.os.UserManager; import android.service.notification.NotificationListenerService; import android.service.notification.NotificationListenerService.RankingMap; import android.util.ArraySet; @@ -80,6 +88,7 @@ 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; @@ -88,15 +97,20 @@ import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.common.TaskStackListenerCallback; import com.android.wm.shell.common.TaskStackListenerImpl; +import com.android.wm.shell.common.annotations.ShellBackgroundThread; +import com.android.wm.shell.common.annotations.ShellMainThread; +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 java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Objects; +import java.util.Optional; import java.util.concurrent.Executor; import java.util.function.Consumer; import java.util.function.IntConsumer; @@ -123,6 +137,10 @@ public class BubbleController { 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"; + private final Context mContext; private final BubblesImpl mImpl = new BubblesImpl(); private Bubbles.BubbleExpandListener mExpandListener; @@ -130,22 +148,27 @@ public class BubbleController { private final FloatingContentCoordinator mFloatingContentCoordinator; private final BubbleDataRepository mDataRepository; private final WindowManagerShellWrapper mWindowManagerShellWrapper; + private final UserManager mUserManager; private final LauncherApps mLauncherApps; private final IStatusBarService mBarService; private final WindowManager mWindowManager; private final TaskStackListenerImpl mTaskStackListener; private final ShellTaskOrganizer mTaskOrganizer; private final DisplayController mDisplayController; + private final TaskViewTransitions mTaskViewTransitions; private final SyncTransactionQueue mSyncQueue; // Used to post to main UI thread private final ShellExecutor mMainExecutor; private final Handler mMainHandler; + private final ShellExecutor mBackgroundExecutor; + private BubbleLogger mLogger; private BubbleData mBubbleData; @Nullable private BubbleStackView mStackView; private BubbleIconFactory mBubbleIconFactory; + private BubbleBadgeIconFactory mBubbleBadgeIconFactory; private BubblePositioner mBubblePositioner; private Bubbles.SysuiProxy mSysuiProxy; @@ -196,6 +219,11 @@ public class BubbleController { /** True when user is in status bar unlock shade. */ private boolean mIsStatusBarShade = true; + /** One handed mode controller to register transition listener. */ + private Optional<OneHandedController> mOneHandedOptional; + /** Drag and drop controller to register listener for onDragStarted. */ + private DragAndDropController mDragAndDropController; + /** * Creates an instance of the BubbleController. */ @@ -205,22 +233,28 @@ public class BubbleController { @Nullable IStatusBarService statusBarService, WindowManager windowManager, WindowManagerShellWrapper windowManagerShellWrapper, + UserManager userManager, LauncherApps launcherApps, TaskStackListenerImpl taskStackListener, UiEventLogger uiEventLogger, ShellTaskOrganizer organizer, DisplayController displayController, - ShellExecutor mainExecutor, - Handler mainHandler, + 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, launcherApps, - logger, taskStackListener, organizer, positioner, displayController, mainExecutor, - mainHandler, syncQueue); + statusBarService, windowManager, windowManagerShellWrapper, userManager, + launcherApps, logger, taskStackListener, organizer, positioner, displayController, + oneHandedOptional, dragAndDropController, mainExecutor, mainHandler, bgExecutor, + taskViewTransitions, syncQueue); } /** @@ -235,14 +269,19 @@ public class BubbleController { @Nullable IStatusBarService statusBarService, WindowManager windowManager, WindowManagerShellWrapper windowManagerShellWrapper, + UserManager userManager, LauncherApps launcherApps, BubbleLogger bubbleLogger, TaskStackListenerImpl taskStackListener, ShellTaskOrganizer organizer, BubblePositioner positioner, DisplayController displayController, - ShellExecutor mainExecutor, - Handler mainHandler, + Optional<OneHandedController> oneHandedOptional, + DragAndDropController dragAndDropController, + @ShellMainThread ShellExecutor mainExecutor, + @ShellMainThread Handler mainHandler, + @ShellBackgroundThread ShellExecutor bgExecutor, + TaskViewTransitions taskViewTransitions, SyncTransactionQueue syncQueue) { mContext = context; mLauncherApps = launcherApps; @@ -252,11 +291,13 @@ public class BubbleController { : statusBarService; mWindowManager = windowManager; mWindowManagerShellWrapper = windowManagerShellWrapper; + mUserManager = userManager; mFloatingContentCoordinator = floatingContentCoordinator; mDataRepository = dataRepository; mLogger = bubbleLogger; mMainExecutor = mainExecutor; mMainHandler = mainHandler; + mBackgroundExecutor = bgExecutor; mTaskStackListener = taskStackListener; mTaskOrganizer = organizer; mSurfaceSynchronizer = synchronizer; @@ -265,13 +306,36 @@ public class BubbleController { mBubbleData = data; mSavedBubbleKeysPerUser = new SparseSetArray<>(); mBubbleIconFactory = new BubbleIconFactory(context); + mBubbleBadgeIconFactory = new BubbleBadgeIconFactory(context); mDisplayController = displayController; + mTaskViewTransitions = taskViewTransitions; + mOneHandedOptional = oneHandedOptional; + mDragAndDropController = dragAndDropController; mSyncQueue = syncQueue; } + private void registerOneHandedState(OneHandedController oneHanded) { + oneHanded.registerTransitionCallback( + new OneHandedTransitionCallback() { + @Override + public void onStartFinished(Rect bounds) { + if (mStackView != null) { + mStackView.onVerticalOffsetChanged(bounds.top); + } + } + + @Override + public void onStopFinished(Rect bounds) { + if (mStackView != null) { + mStackView.onVerticalOffsetChanged(bounds.top); + } + } + }); + } + public void initialize() { mBubbleData.setListener(mBubbleDataListener); - mBubbleData.setSuppressionChangedListener(this::onBubbleNotificationSuppressionChanged); + mBubbleData.setSuppressionChangedListener(this::onBubbleMetadataFlagChanged); mBubbleData.setPendingIntentCancelledListener(bubble -> { if (bubble.getBubbleIntent() == null) { @@ -336,23 +400,17 @@ public class BubbleController { mTaskStackListener.addListener(new TaskStackListenerCallback() { @Override public void onTaskMovedToFront(int taskId) { - if (mSysuiProxy == null) { - return; - } - - mSysuiProxy.isNotificationShadeExpand((expand) -> { - mMainExecutor.execute(() -> { - int expandedId = INVALID_TASK_ID; - if (mStackView != null && mStackView.getExpandedBubble() != null - && isStackExpanded() && !mStackView.isExpansionAnimating() - && !expand) { - expandedId = mStackView.getExpandedBubble().getTaskId(); - } - - if (expandedId != INVALID_TASK_ID && expandedId != taskId) { - mBubbleData.setExpanded(false); - } - }); + mMainExecutor.execute(() -> { + int expandedId = INVALID_TASK_ID; + if (mStackView != null && mStackView.getExpandedBubble() != null + && isStackExpanded() + && !mStackView.isExpansionAnimating() + && !mStackView.isSwitchAnimating()) { + expandedId = mStackView.getExpandedBubble().getTaskId(); + } + if (expandedId != INVALID_TASK_ID && expandedId != taskId) { + mBubbleData.setExpanded(false); + } }); } @@ -383,7 +441,6 @@ public class BubbleController { WindowContainerTransaction t) { // This is triggered right before the rotation is applied if (fromRotation != toRotation) { - mBubblePositioner.setRotation(toRotation); if (mStackView != null) { // Layout listener set on stackView will update the positioner // once the rotation is applied @@ -392,6 +449,13 @@ public class BubbleController { } } }); + + mOneHandedOptional.ifPresent(this::registerOneHandedState); + mDragAndDropController.addListener(this::collapseStack); + + // Clear out any persisted bubbles on disk that no longer have a valid user. + List<UserInfo> users = mUserManager.getAliveUsers(); + mDataRepository.sanitizeBubbles(users); } @VisibleForTesting @@ -464,6 +528,7 @@ public class BubbleController { } mStackView.updateStackPosition(); mBubbleIconFactory = new BubbleIconFactory(mContext); + mBubbleBadgeIconFactory = new BubbleBadgeIconFactory(mContext); mStackView.onDisplaySizeChanged(); } if (b.getBoolean(EXTRA_BUBBLE_OVERFLOW_OPENED, false)) { @@ -489,7 +554,8 @@ public class BubbleController { } } - private void onStatusBarStateChanged(boolean isShade) { + @VisibleForTesting + public void onStatusBarStateChanged(boolean isShade) { mIsStatusBarShade = isShade; if (!mIsStatusBarShade) { collapseStack(); @@ -504,11 +570,10 @@ public class BubbleController { } @VisibleForTesting - public void onBubbleNotificationSuppressionChanged(Bubble bubble) { + public void onBubbleMetadataFlagChanged(Bubble bubble) { // Make sure NoMan knows suppression state so that anyone querying it can tell. try { - mBarService.onBubbleNotificationSuppressionChanged(bubble.getKey(), - !bubble.showInShade(), bubble.isSuppressed()); + mBarService.onBubbleMetadataFlagChanged(bubble.getKey(), bubble.getFlags()); } catch (RemoteException e) { // Bad things have happened } @@ -534,6 +599,17 @@ public class BubbleController { mCurrentProfiles = currentProfiles; } + /** Called when a user is removed from the device, including work profiles. */ + public void onUserRemoved(int removedUserId) { + UserInfo parent = mUserManager.getProfileParent(removedUserId); + int parentUserId = parent != null ? parent.getUserHandle().getIdentifier() : -1; + mBubbleData.removeBubblesForUser(removedUserId); + // Typically calls from BubbleData would remove bubbles from the DataRepository as well, + // however, this gets complicated when users are removed (mCurrentUserId won't necessarily + // be correct for this) so we update the repo directly. + mDataRepository.removeBubblesForUser(removedUserId, parentUserId); + } + /** Whether this userId belongs to the current user. */ private boolean isCurrentProfile(int userId) { return userId == UserHandle.USER_ALL @@ -570,8 +646,13 @@ public class BubbleController { return mSyncQueue; } - /** Contains information to help position things on the screen. */ - BubblePositioner getPositioner() { + TaskViewTransitions getTaskViewTransitions() { + return mTaskViewTransitions; + } + + /** Contains information to help position things on the screen. */ + @VisibleForTesting + public BubblePositioner getPositioner() { return mBubblePositioner; } @@ -613,8 +694,8 @@ public class BubbleController { ViewGroup.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE - | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL - | WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED, + | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL + | WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED, PixelFormat.TRANSLUCENT); mWmLayoutParams.setTrustedOverlay(); @@ -628,6 +709,7 @@ public class BubbleController { try { mAddedToWindowManager = true; + registerBroadcastReceiver(); mBubbleData.getOverflow().initialize(this); mWindowManager.addView(mStackView, mWmLayoutParams); mStackView.setOnApplyWindowInsetsListener((view, windowInsets) -> { @@ -670,6 +752,8 @@ public class BubbleController { 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(); @@ -683,11 +767,34 @@ public class BubbleController { } } + private void registerBroadcastReceiver() { + IntentFilter filter = new IntentFilter(); + filter.addAction(Intent.ACTION_CLOSE_SYSTEM_DIALOGS); + filter.addAction(Intent.ACTION_SCREEN_OFF); + mContext.registerReceiver(mBroadcastReceiver, filter); + } + + private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (!isStackExpanded()) return; // Nothing to do + + String action = intent.getAction(); + String reason = intent.getStringExtra(SYSTEM_DIALOG_REASON_KEY); + if ((Intent.ACTION_CLOSE_SYSTEM_DIALOGS.equals(action) + && SYSTEM_DIALOG_REASON_GESTURE_NAV.equals(reason)) + || Intent.ACTION_SCREEN_OFF.equals(action)) { + mMainExecutor.execute(() -> collapseStack()); + } + } + }; + /** * Called by the BubbleStackView and whenever all bubbles have animated out, and none have been * added in the meantime. */ - void onAllBubblesAnimatedOut() { + @VisibleForTesting + public void onAllBubblesAnimatedOut() { if (mStackView != null) { mStackView.setVisibility(INVISIBLE); removeFromWindowManagerMaybe(); @@ -704,7 +811,7 @@ public class BubbleController { // First clear any existing keys that might be stored. mSavedBubbleKeysPerUser.remove(userId); // Add in all active bubbles for the current user. - for (Bubble bubble: mBubbleData.getBubbles()) { + for (Bubble bubble : mBubbleData.getBubbles()) { mSavedBubbleKeysPerUser.add(userId, bubble.getKey()); } } @@ -738,13 +845,17 @@ public class BubbleController { mStackView.onThemeChanged(); } mBubbleIconFactory = new BubbleIconFactory(mContext); + mBubbleBadgeIconFactory = new BubbleBadgeIconFactory(mContext); + // Reload each bubble - for (Bubble b: mBubbleData.getBubbles()) { + for (Bubble b : mBubbleData.getBubbles()) { b.inflate(null /* callback */, mContext, this, mStackView, mBubbleIconFactory, + mBubbleBadgeIconFactory, false /* skipInflation */); } - for (Bubble b: mBubbleData.getOverflowBubbles()) { + for (Bubble b : mBubbleData.getOverflowBubbles()) { b.inflate(null /* callback */, mContext, this, mStackView, mBubbleIconFactory, + mBubbleBadgeIconFactory, false /* skipInflation */); } } @@ -760,6 +871,7 @@ public class BubbleController { mScreenBounds.set(newConfig.windowConfiguration.getBounds()); mBubbleData.onMaxBubblesChanged(); mBubbleIconFactory = new BubbleIconFactory(mContext); + mBubbleBadgeIconFactory = new BubbleBadgeIconFactory(mContext); mStackView.onDisplaySizeChanged(); } if (newConfig.fontScale != mFontScale) { @@ -806,7 +918,6 @@ public class BubbleController { return mBubbleData.isExpanded(); } - @VisibleForTesting public void collapseStack() { mBubbleData.setExpanded(false /* expanded */); } @@ -921,7 +1032,8 @@ public class BubbleController { } bubble.inflate( (b) -> mBubbleData.overflowBubble(Bubbles.DISMISS_RELOAD_FROM_DISK, bubble), - mContext, this, mStackView, mBubbleIconFactory, true /* skipInflation */); + mContext, this, mStackView, mBubbleIconFactory, mBubbleBadgeIconFactory, + true /* skipInflation */); }); return null; }); @@ -930,9 +1042,9 @@ public class BubbleController { /** * Adds or updates a bubble associated with the provided notification entry. * - * @param notif the notification associated with this bubble. + * @param notif the notification associated with this bubble. * @param suppressFlyout this bubble suppress flyout or not. - * @param showInShade this bubble show in shade or not. + * @param showInShade this bubble show in shade or not. */ @VisibleForTesting public void updateBubble(BubbleEntry notif, boolean suppressFlyout, boolean showInShade) { @@ -940,14 +1052,28 @@ public class BubbleController { mSysuiProxy.setNotificationInterruption(notif.getKey()); if (!notif.getRanking().isTextChanged() && (notif.getBubbleMetadata() != null - && !notif.getBubbleMetadata().getAutoExpandBubble()) + && !notif.getBubbleMetadata().getAutoExpandBubble()) && mBubbleData.hasOverflowBubbleWithKey(notif.getKey())) { // Update the bubble but don't promote it out of overflow Bubble b = mBubbleData.getOverflowBubbleWithKey(notif.getKey()); b.setEntry(notif); + } 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); + } } else { Bubble bubble = mBubbleData.getOrCreateBubble(notif, null /* persistedBubble */); - inflateAndAdd(bubble, suppressFlyout, showInShade); + if (notif.shouldSuppressNotificationList()) { + // If we're suppressing notifs for DND, we don't want the bubbles to randomly + // expand when DND turns off so flip the flag. + if (bubble.shouldAutoExpand()) { + bubble.setShouldAutoExpand(false); + } + } else { + inflateAndAdd(bubble, suppressFlyout, showInShade); + } } } @@ -956,7 +1082,8 @@ public class BubbleController { ensureStackViewCreated(); bubble.setInflateSynchronously(mInflateSynchronously); bubble.inflate(b -> mBubbleData.notificationEntryUpdated(b, suppressFlyout, showInShade), - mContext, this, mStackView, mBubbleIconFactory, false /* skipInflation */); + mContext, this, mStackView, mBubbleIconFactory, mBubbleBadgeIconFactory, + false /* skipInflation */); } /** @@ -978,7 +1105,8 @@ public class BubbleController { } } - private void onEntryUpdated(BubbleEntry entry, boolean shouldBubbleUp) { + @VisibleForTesting + public void onEntryUpdated(BubbleEntry entry, boolean shouldBubbleUp) { // shouldBubbleUp checks canBubble & for bubble metadata boolean shouldBubble = shouldBubbleUp && canLaunchInTaskView(mContext, entry); if (!shouldBubble && mBubbleData.hasAnyBubbleWithKey(entry.getKey())) { @@ -1004,7 +1132,8 @@ public class BubbleController { } } - private void onRankingUpdated(RankingMap rankingMap, + @VisibleForTesting + public void onRankingUpdated(RankingMap rankingMap, HashMap<String, Pair<BubbleEntry, Boolean>> entryDataByKey) { if (mTmpRanking == null) { mTmpRanking = new NotificationListenerService.Ranking(); @@ -1015,19 +1144,22 @@ public class BubbleController { Pair<BubbleEntry, Boolean> entryData = entryDataByKey.get(key); BubbleEntry entry = entryData.first; boolean shouldBubbleUp = entryData.second; - if (entry != null && !isCurrentProfile( entry.getStatusBarNotification().getUser().getIdentifier())) { return; } - + if (entry != null && (entry.shouldSuppressNotificationList() + || entry.getRanking().isSuspended())) { + shouldBubbleUp = false; + } rankingMap.getRanking(key, mTmpRanking); - boolean isActiveBubble = mBubbleData.hasAnyBubbleWithKey(key); - if (isActiveBubble && !mTmpRanking.canBubble()) { + boolean isActiveOrInOverflow = mBubbleData.hasAnyBubbleWithKey(key); + boolean isActive = mBubbleData.hasBubbleInStackWithKey(key); + if (isActiveOrInOverflow && !mTmpRanking.canBubble()) { // If this entry is no longer allowed to bubble, dismiss with the BLOCKED reason. // This means that the app or channel's ability to bubble has been revoked. mBubbleData.dismissBubbleWithKey(key, DISMISS_BLOCKED); - } else if (isActiveBubble && (!shouldBubbleUp || entry.getRanking().isSuspended())) { + } else if (isActiveOrInOverflow && !shouldBubbleUp) { // If this entry is allowed to bubble, but cannot currently bubble up or is // suspended, dismiss it. This happens when DND is enabled and configured to hide // bubbles, or focus mode is enabled and the app is designated as distracting. @@ -1035,9 +1167,27 @@ 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() && !isActiveBubble) { + } else if (entry != null && mTmpRanking.isBubble() && !isActive) { entry.setFlagBubble(true); - onEntryUpdated(entry, shouldBubbleUp && !entry.getRanking().isSuspended()); + onEntryUpdated(entry, shouldBubbleUp); + } + } + } + + @VisibleForTesting + public void onNotificationChannelModified(String pkg, UserHandle user, + NotificationChannel channel, int modificationType) { + // Only query overflow bubbles here because active bubbles will have an active notification + // and channel changes we care about would result in a ranking update. + List<Bubble> overflowBubbles = new ArrayList<>(mBubbleData.getOverflowBubbles()); + for (int i = 0; i < overflowBubbles.size(); i++) { + Bubble b = overflowBubbles.get(i); + if (Objects.equals(b.getShortcutId(), channel.getConversationId()) + && b.getPackageName().equals(pkg) + && b.getUser().getIdentifier() == user.getIdentifier()) { + if (!channel.canBubble() || channel.isDeleted()) { + mBubbleData.dismissBubbleWithKey(b.getKey(), DISMISS_NO_LONGER_BUBBLE); + } } } } @@ -1099,6 +1249,18 @@ public class BubbleController { @Override public void applyUpdate(BubbleData.Update update) { + if (DEBUG_BUBBLE_CONTROLLER) { + Log.d(TAG, "applyUpdate:" + " bubbleAdded=" + (update.addedBubble != null) + + " bubbleRemoved=" + + (update.removedBubbles != null && update.removedBubbles.size() > 0) + + " bubbleUpdated=" + (update.updatedBubble != null) + + " orderChanged=" + update.orderChanged + + " expandedChanged=" + update.expandedChanged + + " selectionChanged=" + update.selectionChanged + + " suppressed=" + (update.suppressedBubble != null) + + " unsuppressed=" + (update.unsuppressedBubble != null)); + } + ensureStackViewCreated(); // Lazy load overflow bubbles from disk @@ -1111,12 +1273,6 @@ public class BubbleController { mOverflowListener.applyUpdate(update); } - // Collapsing? Do this first before remaining steps. - if (update.expandedChanged && !update.expanded) { - mStackView.setExpanded(false); - mSysuiProxy.requestNotificationShadeTopUi(false, TAG); - } - // Do removals, if any. ArrayList<Pair<Bubble, Integer>> removedBubbles = new ArrayList<>(update.removedBubbles); @@ -1178,6 +1334,14 @@ public class BubbleController { mStackView.updateBubble(update.updatedBubble); } + if (update.suppressedBubble != null && mStackView != null) { + mStackView.setBubbleSuppressed(update.suppressedBubble, true); + } + + if (update.unsuppressedBubble != null && mStackView != null) { + mStackView.setBubbleSuppressed(update.unsuppressedBubble, false); + } + // 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) { @@ -1185,6 +1349,11 @@ public class BubbleController { mStackView.updateBubbleOrder(update.bubbles); } + if (update.expandedChanged && !update.expanded) { + mStackView.setExpanded(false); + mSysuiProxy.requestNotificationShadeTopUi(false, TAG); + } + if (update.selectionChanged && mStackView != null) { mStackView.setSelectedBubble(update.selectedBubble); if (update.selectedBubble != null) { @@ -1192,14 +1361,6 @@ public class BubbleController { } } - if (update.suppressedBubble != null && mStackView != null) { - mStackView.setBubbleVisibility(update.suppressedBubble, false); - } - - if (update.unsuppressedBubble != null && mStackView != null) { - mStackView.setBubbleVisibility(update.unsuppressedBubble, true); - } - // Expanding? Apply this last. if (update.expandedChanged && update.expanded) { if (mStackView != null) { @@ -1279,6 +1440,7 @@ public class BubbleController { * Updates the visibility of the bubbles based on current state. * Does not un-bubble, just hides or un-hides. * Updates stack description for TalkBack focus. + * Updates bubbles' icon views clickable states */ public void updateStack() { if (mStackView == null) { @@ -1296,6 +1458,8 @@ public class BubbleController { } mStackView.updateContentDescription(); + + mStackView.updateBubblesAcessibillityStates(); } @VisibleForTesting @@ -1306,12 +1470,12 @@ public class BubbleController { /** * Description of current bubble state. */ - private void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + private void dump(PrintWriter pw, String[] args) { pw.println("BubbleController state:"); - mBubbleData.dump(fd, pw, args); + mBubbleData.dump(pw, args); pw.println(); if (mStackView != null) { - mStackView.dump(fd, pw, args); + mStackView.dump(pw, args); } pw.println(); } @@ -1324,7 +1488,7 @@ public class BubbleController { * that should filter out any invalid bubbles, but should protect SysUI side just in case. * * @param context the context to use. - * @param entry the entry to bubble. + * @param entry the entry to bubble. */ static boolean canLaunchInTaskView(Context context, BubbleEntry entry) { PendingIntent intent = entry.getBubbleMetadata() != null @@ -1457,7 +1621,7 @@ public class BubbleController { String groupKey) { return mSuppressedBubbleKeys.contains(key) || (mSuppressedGroupToNotifKeys.containsKey(groupKey) - && key.equals(mSuppressedGroupToNotifKeys.get(groupKey))); + && key.equals(mSuppressedGroupToNotifKeys.get(groupKey))); } @Nullable @@ -1617,6 +1781,19 @@ public class BubbleController { } @Override + public void onNotificationChannelModified(String pkg, + UserHandle user, NotificationChannel channel, int modificationType) { + // Bubbles only cares about updates or deletions. + if (modificationType == NOTIFICATION_CHANNEL_OR_GROUP_UPDATED + || modificationType == NOTIFICATION_CHANNEL_OR_GROUP_DELETED) { + mMainExecutor.execute(() -> { + BubbleController.this.onNotificationChannelModified(pkg, user, channel, + modificationType); + }); + } + } + + @Override public void onStatusBarVisibilityChanged(boolean visible) { mMainExecutor.execute(() -> { BubbleController.this.onStatusBarVisibilityChanged(visible); @@ -1652,6 +1829,13 @@ public class BubbleController { } @Override + public void onUserRemoved(int removedUserId) { + mMainExecutor.execute(() -> { + BubbleController.this.onUserRemoved(removedUserId); + }); + } + + @Override public void onConfigChanged(Configuration newConfig) { mMainExecutor.execute(() -> { BubbleController.this.onConfigChanged(newConfig); @@ -1659,10 +1843,10 @@ public class BubbleController { } @Override - public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + public void dump(PrintWriter pw, String[] args) { try { mMainExecutor.executeBlocking(() -> { - BubbleController.this.dump(fd, pw, args); + BubbleController.this.dump(pw, args); mCachedState.dump(pw); }); } catch (InterruptedException e) { 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 cd635c10fd8e..fa86c8436647 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 @@ -40,7 +40,6 @@ import com.android.internal.util.FrameworkStatsLog; import com.android.wm.shell.R; import com.android.wm.shell.bubbles.Bubbles.DismissReason; -import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Collections; @@ -160,7 +159,7 @@ public class BubbleData { private Listener mListener; @Nullable - private Bubbles.SuppressionChangedListener mSuppressionListener; + private Bubbles.BubbleMetadataFlagListener mBubbleMetadataFlagListener; private Bubbles.PendingIntentCanceledListener mCancelledListener; /** @@ -191,9 +190,8 @@ public class BubbleData { mMaxOverflowBubbles = mContext.getResources().getInteger(R.integer.bubbles_max_overflow); } - public void setSuppressionChangedListener( - Bubbles.SuppressionChangedListener listener) { - mSuppressionListener = listener; + public void setSuppressionChangedListener(Bubbles.BubbleMetadataFlagListener listener) { + mBubbleMetadataFlagListener = listener; } public void setPendingIntentCancelledListener( @@ -224,7 +222,8 @@ public class BubbleData { } public boolean hasAnyBubbleWithKey(String key) { - return hasBubbleInStackWithKey(key) || hasOverflowBubbleWithKey(key); + return hasBubbleInStackWithKey(key) || hasOverflowBubbleWithKey(key) + || hasSuppressedBubbleWithKey(key); } public boolean hasBubbleInStackWithKey(String key) { @@ -235,6 +234,20 @@ public class BubbleData { return getOverflowBubbleWithKey(key) != null; } + /** + * Check if there are any bubbles suppressed with the given notification <code>key</code> + */ + public boolean hasSuppressedBubbleWithKey(String key) { + return mSuppressedBubbles.values().stream().anyMatch(b -> b.getKey().equals(key)); + } + + /** + * Check if there are any bubbles suppressed with the given <code>LocusId</code> + */ + public boolean isSuppressedWithLocusId(LocusId locusId) { + return mSuppressedBubbles.get(locusId) != null; + } + @Nullable public BubbleViewProvider getSelectedBubble() { return mSelectedBubble; @@ -297,7 +310,7 @@ public class BubbleData { bubbleToReturn = mPendingBubbles.get(key); } else if (entry != null) { // New bubble - bubbleToReturn = new Bubble(entry, mSuppressionListener, mCancelledListener, + bubbleToReturn = new Bubble(entry, mBubbleMetadataFlagListener, mCancelledListener, mMainExecutor); } else { // Persisted bubble being promoted @@ -356,11 +369,11 @@ public class BubbleData { boolean isSuppressed = mSuppressedBubbles.containsKey(locusId); if (isSuppressed && (!bubble.isSuppressed() || !bubble.isSuppressable())) { mSuppressedBubbles.remove(locusId); - mStateChange.unsuppressedBubble = bubble; + doUnsuppress(bubble); } else if (!isSuppressed && (bubble.isSuppressed() || bubble.isSuppressable() && mVisibleLocusIds.contains(locusId))) { mSuppressedBubbles.put(locusId, bubble); - mStateChange.suppressedBubble = bubble; + doSuppress(bubble); } } dispatchPendingChanges(); @@ -452,7 +465,7 @@ public class BubbleData { getOverflowBubbles(), invalidBubblesFromPackage, removeBubble); } - /** Dismisses all bubbles from the given package. */ + /** Removes all bubbles from the given package. */ public void removeBubblesWithPackageName(String packageName, int reason) { final Predicate<Bubble> bubbleMatchesPackage = bubble -> bubble.getPackageName().equals(packageName); @@ -464,6 +477,18 @@ public class BubbleData { performActionOnBubblesMatching(getOverflowBubbles(), bubbleMatchesPackage, removeBubble); } + /** Removes all bubbles for the given user. */ + public void removeBubblesForUser(int userId) { + List<Bubble> removedBubbles = filterAllBubbles(bubble -> + userId == bubble.getUser().getIdentifier()); + for (Bubble b : removedBubbles) { + doRemove(b.getKey(), Bubbles.DISMISS_USER_REMOVED); + } + if (!removedBubbles.isEmpty()) { + dispatchPendingChanges(); + } + } + private void doAdd(Bubble bubble) { if (DEBUG_BUBBLE_DATA) { Log.d(TAG, "doAdd: " + bubble); @@ -532,16 +557,20 @@ public class BubbleData { if (mPendingBubbles.containsKey(key)) { mPendingBubbles.remove(key); } + + boolean shouldRemoveHiddenBubble = reason == Bubbles.DISMISS_NOTIF_CANCEL + || reason == Bubbles.DISMISS_GROUP_CANCELLED + || reason == Bubbles.DISMISS_NO_LONGER_BUBBLE + || reason == Bubbles.DISMISS_BLOCKED + || reason == Bubbles.DISMISS_SHORTCUT_REMOVED + || reason == Bubbles.DISMISS_PACKAGE_REMOVED + || reason == Bubbles.DISMISS_USER_CHANGED + || reason == Bubbles.DISMISS_USER_REMOVED; + int indexToRemove = indexForKey(key); if (indexToRemove == -1) { if (hasOverflowBubbleWithKey(key) - && (reason == Bubbles.DISMISS_NOTIF_CANCEL - || reason == Bubbles.DISMISS_GROUP_CANCELLED - || reason == Bubbles.DISMISS_NO_LONGER_BUBBLE - || reason == Bubbles.DISMISS_BLOCKED - || reason == Bubbles.DISMISS_SHORTCUT_REMOVED - || reason == Bubbles.DISMISS_PACKAGE_REMOVED - || reason == Bubbles.DISMISS_USER_CHANGED)) { + && shouldRemoveHiddenBubble) { Bubble b = getOverflowBubbleWithKey(key); if (DEBUG_BUBBLE_DATA) { @@ -555,6 +584,17 @@ public class BubbleData { mStateChange.bubbleRemoved(b, reason); mStateChange.removedOverflowBubble = b; } + if (hasSuppressedBubbleWithKey(key) && shouldRemoveHiddenBubble) { + Bubble b = getSuppressedBubbleWithKey(key); + if (DEBUG_BUBBLE_DATA) { + Log.d(TAG, "Cancel suppressed bubble: " + b); + } + if (b != null) { + mSuppressedBubbles.remove(b.getLocusId()); + b.stopInflation(); + mStateChange.bubbleRemoved(b, reason); + } + } return; } Bubble bubbleToRemove = mBubbles.get(indexToRemove); @@ -562,17 +602,10 @@ public class BubbleData { overflowBubble(reason, bubbleToRemove); if (mBubbles.size() == 1) { - if (hasOverflowBubbles() && (mPositioner.showingInTaskbar() || isExpanded())) { - // No more active bubbles but we have stuff in the overflow -- select that view - // if we're already expanded or always showing. - setShowingOverflow(true); - setSelectedBubbleInternal(mOverflow); - } else { - setExpandedInternal(false); - // Don't use setSelectedBubbleInternal because we don't want to trigger an - // applyUpdate - mSelectedBubble = null; - } + setExpandedInternal(false); + // Don't use setSelectedBubbleInternal because we don't want to trigger an + // applyUpdate + mSelectedBubble = null; } if (indexToRemove < mBubbles.size() - 1) { // Removing anything but the last bubble means positions will change. @@ -586,19 +619,73 @@ public class BubbleData { // Note: If mBubbles.isEmpty(), then mSelectedBubble is now null. if (Objects.equals(mSelectedBubble, bubbleToRemove)) { - // Move selection to the new bubble at the same position. - int newIndex = Math.min(indexToRemove, mBubbles.size() - 1); - BubbleViewProvider newSelected = mBubbles.get(newIndex); - setSelectedBubbleInternal(newSelected); + setNewSelectedIndex(indexToRemove); } maybeSendDeleteIntent(reason, bubbleToRemove); } + private void setNewSelectedIndex(int indexOfSelected) { + if (mBubbles.isEmpty()) { + Log.w(TAG, "Bubbles list empty when attempting to select index: " + indexOfSelected); + return; + } + // Move selection to the new bubble at the same position. + int newIndex = Math.min(indexOfSelected, mBubbles.size() - 1); + if (DEBUG_BUBBLE_DATA) { + Log.d(TAG, "setNewSelectedIndex: " + indexOfSelected); + } + BubbleViewProvider newSelected = mBubbles.get(newIndex); + setSelectedBubbleInternal(newSelected); + } + + private void doSuppress(Bubble bubble) { + if (DEBUG_BUBBLE_DATA) { + Log.d(TAG, "doSuppressed: " + bubble); + } + mStateChange.suppressedBubble = bubble; + bubble.setSuppressBubble(true); + + int indexToRemove = mBubbles.indexOf(bubble); + // Order changes if we are not suppressing the last bubble + mStateChange.orderChanged = !(mBubbles.size() - 1 == indexToRemove); + mBubbles.remove(indexToRemove); + + // Update selection if we suppressed the selected bubble + if (Objects.equals(mSelectedBubble, bubble)) { + if (mBubbles.isEmpty()) { + // Don't use setSelectedBubbleInternal because we don't want to trigger an + // applyUpdate + mSelectedBubble = null; + } else { + // Mark new first bubble as selected + setNewSelectedIndex(0); + } + } + } + + private void doUnsuppress(Bubble bubble) { + if (DEBUG_BUBBLE_DATA) { + Log.d(TAG, "doUnsuppressed: " + bubble); + } + bubble.setSuppressBubble(false); + mStateChange.unsuppressedBubble = bubble; + mBubbles.add(bubble); + if (mBubbles.size() > 1) { + // See where the bubble actually lands + repackAll(); + mStateChange.orderChanged = true; + } + if (mBubbles.get(0) == bubble) { + // Unsuppressed bubble is sorted to first position. Mark it as the selected. + setNewSelectedIndex(0); + } + } + void overflowBubble(@DismissReason int reason, Bubble bubble) { if (bubble.getPendingIntentCanceled() || !(reason == Bubbles.DISMISS_AGED - || reason == Bubbles.DISMISS_USER_GESTURE - || reason == Bubbles.DISMISS_RELOAD_FROM_DISK)) { + || reason == Bubbles.DISMISS_USER_GESTURE + || reason == Bubbles.DISMISS_RELOAD_FROM_DISK)) { return; } if (DEBUG_BUBBLE_DATA) { @@ -626,7 +713,7 @@ public class BubbleData { if (DEBUG_BUBBLE_DATA) { Log.d(TAG, "dismissAll: reason=" + reason); } - if (mBubbles.isEmpty()) { + if (mBubbles.isEmpty() && mSuppressedBubbles.isEmpty()) { return; } setExpandedInternal(false); @@ -634,6 +721,10 @@ public class BubbleData { while (!mBubbles.isEmpty()) { doRemove(mBubbles.get(0).getKey(), reason); } + while (!mSuppressedBubbles.isEmpty()) { + Bubble bubble = mSuppressedBubbles.removeAt(0); + doRemove(bubble.getKey(), reason); + } dispatchPendingChanges(); } @@ -642,11 +733,15 @@ public class BubbleData { * and if there's a matching bubble for that locusId then the bubble may be hidden or shown * depending on the visibility of the locusId. * - * @param taskId the taskId associated with the locusId visibility change. + * @param taskId the taskId associated with the locusId visibility change. * @param locusId the locusId whose visibility has changed. * @param visible whether the task with the locusId is visible or not. */ public void onLocusVisibilityChanged(int taskId, LocusId locusId, boolean visible) { + if (DEBUG_BUBBLE_DATA) { + Log.d(TAG, "onLocusVisibilityChanged: " + locusId + " visible=" + visible); + } + Bubble matchingBubble = getBubbleInStackWithLocusId(locusId); // Don't add the locus if it's from a bubble'd activity, we only suppress for non-bubbled. if (visible && (matchingBubble == null || matchingBubble.getTaskId() != taskId)) { @@ -655,20 +750,22 @@ public class BubbleData { mVisibleLocusIds.remove(locusId); } if (matchingBubble == null) { - return; + // Check if there is a suppressed bubble for this LocusId + matchingBubble = mSuppressedBubbles.get(locusId); + if (matchingBubble == null) { + return; + } } boolean isAlreadySuppressed = mSuppressedBubbles.get(locusId) != null; if (visible && !isAlreadySuppressed && matchingBubble.isSuppressable() && taskId != matchingBubble.getTaskId()) { mSuppressedBubbles.put(locusId, matchingBubble); - matchingBubble.setSuppressBubble(true); - mStateChange.suppressedBubble = matchingBubble; + doSuppress(matchingBubble); dispatchPendingChanges(); } else if (!visible) { Bubble unsuppressedBubble = mSuppressedBubbles.remove(locusId); if (unsuppressedBubble != null) { - unsuppressedBubble.setSuppressBubble(false); - mStateChange.unsuppressedBubble = unsuppressedBubble; + doUnsuppress(unsuppressedBubble); } dispatchPendingChanges(); } @@ -727,14 +824,14 @@ public class BubbleData { /** * Logs the bubble UI event. * - * @param provider The bubble view provider that is being interacted on. Null value indicates - * that the user interaction is not specific to one bubble. - * @param action The user interaction enum + * @param provider The bubble view provider that is being interacted on. Null value indicates + * that the user interaction is not specific to one bubble. + * @param action The user interaction enum * @param packageName SystemUI package * @param bubbleCount Number of bubbles in the stack * @param bubbleIndex Index of bubble in the stack - * @param normalX Normalized x position of the stack - * @param normalY Normalized y position of the stack + * @param normalX Normalized x position of the stack + * @param normalY Normalized y position of the stack */ void logBubbleEvent(@Nullable BubbleViewProvider provider, int action, String packageName, int bubbleCount, int bubbleIndex, float normalX, float normalY) { @@ -876,6 +973,9 @@ public class BubbleData { if (b == null) { b = getOverflowBubbleWithKey(key); } + if (b == null) { + b = getSuppressedBubbleWithKey(key); + } return b; } @@ -953,6 +1053,68 @@ public class BubbleData { return null; } + /** + * Get a suppressed bubble with given notification <code>key</code> + * + * @param key notification key + * @return bubble that matches or null + */ + @Nullable + @VisibleForTesting(visibility = PRIVATE) + public Bubble getSuppressedBubbleWithKey(String key) { + for (Bubble b : mSuppressedBubbles.values()) { + if (b.getKey().equals(key)) { + return b; + } + } + return null; + } + + /** + * Get a pending bubble with given notification <code>key</code> + * + * @param key notification key + * @return bubble that matches or null + */ + @VisibleForTesting(visibility = PRIVATE) + public Bubble getPendingBubbleWithKey(String key) { + for (Bubble b : mPendingBubbles.values()) { + if (b.getKey().equals(key)) { + return b; + } + } + return null; + } + + /** + * Returns a list of bubbles that match the provided predicate. This checks all types of + * bubbles (i.e. pending, suppressed, active, and overflowed). + */ + private List<Bubble> filterAllBubbles(Predicate<Bubble> predicate) { + ArrayList<Bubble> matchingBubbles = new ArrayList<>(); + for (Bubble b : mPendingBubbles.values()) { + if (predicate.test(b)) { + matchingBubbles.add(b); + } + } + for (Bubble b : mSuppressedBubbles.values()) { + if (predicate.test(b)) { + matchingBubbles.add(b); + } + } + for (Bubble b : mBubbles) { + if (predicate.test(b)) { + matchingBubbles.add(b); + } + } + for (Bubble b : mOverflowBubbles) { + if (predicate.test(b)) { + matchingBubbles.add(b); + } + } + return matchingBubbles; + } + @VisibleForTesting(visibility = PRIVATE) void setTimeSource(TimeSource timeSource) { mTimeSource = timeSource; @@ -974,7 +1136,7 @@ public class BubbleData { /** * Description of current bubble data state. */ - public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + public void dump(PrintWriter pw, String[] args) { pw.print("selected: "); pw.println(mSelectedBubble != null ? mSelectedBubble.getKey() @@ -985,13 +1147,13 @@ public class BubbleData { pw.print("stack bubble count: "); pw.println(mBubbles.size()); for (Bubble bubble : mBubbles) { - bubble.dump(fd, pw, args); + bubble.dump(pw, args); } pw.print("overflow bubble count: "); pw.println(mOverflowBubbles.size()); for (Bubble bubble : mOverflowBubbles) { - bubble.dump(fd, pw, args); + bubble.dump(pw, args); } 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 9d9e442affd3..97560f44fb06 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 @@ -22,6 +22,7 @@ import android.content.pm.LauncherApps import android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_CACHED import android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_DYNAMIC import android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_PINNED_BY_ANY_LAUNCHER +import android.content.pm.UserInfo import android.os.UserHandle import android.util.Log import com.android.wm.shell.bubbles.storage.BubbleEntity @@ -73,6 +74,22 @@ internal class BubbleDataRepository( if (entities.isNotEmpty()) persistToDisk() } + /** + * Removes all the bubbles associated with the provided user from memory. Then persists the + * snapshot to disk asynchronously. + */ + fun removeBubblesForUser(@UserIdInt userId: Int, @UserIdInt parentId: Int) { + if (volatileRepository.removeBubblesForUser(userId, parentId)) persistToDisk() + } + + /** + * Remove any bubbles that don't have a user id from the provided list of users. + */ + fun sanitizeBubbles(users: List<UserInfo>) { + val userIds = users.map { u -> u.id } + if (volatileRepository.sanitizeBubbles(userIds)) persistToDisk() + } + private fun transform(bubbles: List<Bubble>): List<BubbleEntity> { return bubbles.mapNotNull { b -> BubbleEntity( 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 a87aad4261a6..b8bf1a8e497e 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 @@ -60,13 +60,13 @@ import android.widget.LinearLayout; 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 java.io.FileDescriptor; import java.io.PrintWriter; /** @@ -112,6 +112,7 @@ public class BubbleExpandedView extends LinearLayout { private ShapeDrawable mRightPointer; private float mCornerRadius = 0f; private int mBackgroundColorFloating; + private boolean mUsingMaxHeight; @Nullable private Bubble mBubble; private PendingIntent mPendingIntent; @@ -335,7 +336,7 @@ public class BubbleExpandedView extends LinearLayout { mManageButton.setVisibility(GONE); } else { mTaskView = new TaskView(mContext, mController.getTaskOrganizer(), - mController.getSyncTransactionQueue()); + mController.getTaskViewTransitions(), mController.getSyncTransactionQueue()); mTaskView.setListener(mController.getMainExecutor(), mTaskViewListener); mExpandedViewContainer.addView(mTaskView); bringChildToFront(mTaskView); @@ -386,13 +387,14 @@ public class BubbleExpandedView extends LinearLayout { final TypedArray ta = mContext.obtainStyledAttributes(new int[] { android.R.attr.dialogCornerRadius, android.R.attr.colorBackgroundFloating}); - mCornerRadius = ta.getDimensionPixelSize(0, 0); + boolean supportsRoundedCorners = ScreenDecorationsUtils.supportsRoundedCornersOnWindows( + mContext.getResources()); + mCornerRadius = supportsRoundedCorners ? ta.getDimensionPixelSize(0, 0) : 0; mBackgroundColorFloating = ta.getColor(1, Color.WHITE); mExpandedViewContainer.setBackgroundColor(mBackgroundColorFloating); ta.recycle(); - if (mTaskView != null && ScreenDecorationsUtils.supportsRoundedCornersOnWindows( - mContext.getResources())) { + if (mTaskView != null) { mTaskView.setCornerRadius(mCornerRadius); } updatePointerView(); @@ -417,8 +419,9 @@ public class BubbleExpandedView extends LinearLayout { mPointerView.setBackground(mCurrentPointer); } - private String getBubbleKey() { - return mBubble != null ? mBubble.getKey() : "null"; + @VisibleForTesting + public String getBubbleKey() { + return mBubble != null ? mBubble.getKey() : mIsOverflow ? BubbleOverflow.KEY : null; } /** @@ -451,8 +454,11 @@ public class BubbleExpandedView extends LinearLayout { p.beginRecording(mOverflowView.getWidth(), mOverflowView.getHeight())); p.endRecording(); Bitmap snapshot = Bitmap.createBitmap(p); - return new SurfaceControl.ScreenshotHardwareBuffer(snapshot.getHardwareBuffer(), - snapshot.getColorSpace(), false /* containsSecureLayers */); + return new SurfaceControl.ScreenshotHardwareBuffer( + snapshot.getHardwareBuffer(), + snapshot.getColorSpace(), + false /* containsSecureLayers */, + false /* containsHdrLayers */); } if (mTaskView == null || mTaskView.getSurfaceControl() == null) { return null; @@ -619,6 +625,13 @@ public class BubbleExpandedView extends LinearLayout { return prevWasIntentBased != newIsIntentBased; } + /** + * Whether the bubble is using all available height to display or not. + */ + public boolean isUsingMaxHeight() { + return mUsingMaxHeight; + } + void updateHeight() { if (mExpandedViewContainerLocation == null) { return; @@ -630,6 +643,7 @@ public class BubbleExpandedView extends LinearLayout { float height = desiredHeight == MAX_HEIGHT ? maxHeight : Math.min(desiredHeight, maxHeight); + mUsingMaxHeight = height == maxHeight; FrameLayout.LayoutParams lp = mIsOverflow ? (FrameLayout.LayoutParams) mOverflowView.getLayoutParams() : (FrameLayout.LayoutParams) mTaskView.getLayoutParams(); @@ -689,8 +703,11 @@ public class BubbleExpandedView extends LinearLayout { * @param bubblePosition the x position of the bubble if showing on top, the y position of * the bubble if showing vertically. * @param onLeft whether the stack was on the left side of the screen when expanded. + * @param animate whether the pointer should animate to this position. */ public void setPointerPosition(float bubblePosition, boolean onLeft, boolean animate) { + final boolean isRtl = mContext.getResources().getConfiguration().getLayoutDirection() + == LAYOUT_DIRECTION_RTL; // Pointer gets drawn in the padding final boolean showVertically = mPositioner.showBubblesVertically(); final float paddingLeft = (showVertically && onLeft) @@ -717,12 +734,22 @@ public class BubbleExpandedView extends LinearLayout { float pointerX; if (showVertically) { pointerY = bubbleCenter - (mPointerWidth / 2f); - pointerX = onLeft - ? -mPointerHeight + mPointerOverlap - : getWidth() - mPaddingRight - mPointerOverlap; + if (!isRtl) { + pointerX = onLeft + ? -mPointerHeight + mPointerOverlap + : getWidth() - mPaddingRight - mPointerOverlap; + } else { + pointerX = onLeft + ? -(getWidth() - mPaddingLeft - mPointerOverlap) + : mPointerHeight - mPointerOverlap; + } } else { pointerY = mPointerOverlap; - pointerX = bubbleCenter - (mPointerWidth / 2f); + if (!isRtl) { + pointerX = bubbleCenter - (mPointerWidth / 2f); + } else { + pointerX = -(getWidth() - mPaddingLeft - bubbleCenter) + (mPointerWidth / 2f); + } } if (animate) { mPointerView.animate().translationX(pointerX).translationY(pointerY).start(); @@ -772,8 +799,7 @@ public class BubbleExpandedView extends LinearLayout { /** * Description of current expanded view state. */ - public void dump( - @NonNull FileDescriptor fd, @NonNull PrintWriter pw, @NonNull String[] args) { + public void dump(@NonNull PrintWriter pw, @NonNull String[] args) { 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 b0e029fdc681..9d3bf34895d3 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,19 +21,12 @@ 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.Canvas; -import android.graphics.Paint; -import android.graphics.Path; -import android.graphics.drawable.AdaptiveIconDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.Icon; import androidx.annotation.VisibleForTesting; 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,12 +37,9 @@ import com.android.wm.shell.R; @VisibleForTesting public class BubbleIconFactory extends BaseIconFactory { - private int mBadgeSize; - public BubbleIconFactory(Context context) { super(context, context.getResources().getConfiguration().densityDpi, context.getResources().getDimensionPixelSize(R.dimen.bubble_size)); - mBadgeSize = mContext.getResources().getDimensionPixelSize(R.dimen.bubble_badge_size); } /** @@ -75,84 +65,4 @@ public class BubbleIconFactory extends BaseIconFactory { return null; } } - - /** - * Returns a {@link BitmapInfo} for the app-badge that is shown on top of each bubble. This - * will include the workprofile indicator on the badge if appropriate. - */ - BitmapInfo getBadgeBitmap(Drawable userBadgedAppIcon, boolean isImportantConversation) { - ShadowGenerator shadowGenerator = new ShadowGenerator(mBadgeSize); - Bitmap userBadgedBitmap = createIconBitmap(userBadgedAppIcon, 1f, mBadgeSize); - - if (userBadgedAppIcon instanceof AdaptiveIconDrawable) { - userBadgedBitmap = Bitmap.createScaledBitmap( - getCircleBitmap((AdaptiveIconDrawable) userBadgedAppIcon, /* size */ - userBadgedAppIcon.getIntrinsicWidth()), - mBadgeSize, mBadgeSize, /* filter */ true); - } - - if (isImportantConversation) { - final float ringStrokeWidth = mContext.getResources().getDimensionPixelSize( - com.android.internal.R.dimen.importance_ring_stroke_width); - final int importantConversationColor = 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); - } - } - - public 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; - } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflow.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflow.kt index 0c3a6b2dbd84..eb7929b8ca54 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflow.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflow.kt @@ -54,6 +54,7 @@ class BubbleOverflow( /** Call before use and again if cleanUpExpandedState was called. */ fun initialize(controller: BubbleController) { + createExpandedView() getExpandedView()?.initialize(controller, controller.stackView, true /* isOverflow */) } @@ -66,7 +67,7 @@ class BubbleOverflow( updateResources() getExpandedView()?.applyThemeAttrs() // Apply inset and new style to fresh icon drawable. - getIconView()?.setImageResource(R.drawable.bubble_ic_overflow_button) + getIconView()?.setIconImageResource(R.drawable.bubble_ic_overflow_button) updateBtnTheme() } @@ -89,20 +90,19 @@ class BubbleOverflow( dotColor = colorAccent val shapeColor = res.getColor(android.R.color.system_accent1_1000) - overflowBtn?.drawable?.setTint(shapeColor) + overflowBtn?.iconDrawable?.setTint(shapeColor) val iconFactory = BubbleIconFactory(context) // Update bitmap - val fg = InsetDrawable(overflowBtn?.drawable, overflowIconInset) - bitmap = iconFactory.createBadgedIconBitmap(AdaptiveIconDrawable( - ColorDrawable(colorAccent), fg), - null /* user */, true /* shrinkNonAdaptiveIcons */).icon + val fg = InsetDrawable(overflowBtn?.iconDrawable, overflowIconInset) + bitmap = iconFactory.createBadgedIconBitmap( + AdaptiveIconDrawable(ColorDrawable(colorAccent), fg)).icon // Update dot path dotPath = PathParser.createPathFromPathData( res.getString(com.android.internal.R.string.config_icon_mask)) - val scale = iconFactory.normalizer.getScale(iconView!!.drawable, + val scale = iconFactory.normalizer.getScale(iconView!!.iconDrawable, null /* outBounds */, null /* path */, null /* outMaskShape */) val radius = BadgedImageView.DEFAULT_PATH_SIZE / 2f val matrix = Matrix() @@ -124,13 +124,15 @@ class BubbleOverflow( overflowBtn?.updateDotVisibility(true /* animate */) } + fun createExpandedView(): BubbleExpandedView? { + expandedView = inflater.inflate(R.layout.bubble_expanded_view, + null /* root */, false /* attachToRoot */) as BubbleExpandedView + expandedView?.applyThemeAttrs() + updateResources() + return expandedView + } + override fun getExpandedView(): BubbleExpandedView? { - if (expandedView == null) { - expandedView = inflater.inflate(R.layout.bubble_expanded_view, - null /* root */, false /* attachToRoot */) as BubbleExpandedView - expandedView?.applyThemeAttrs() - updateResources() - } return expandedView } @@ -142,6 +144,10 @@ class BubbleOverflow( return null } + override fun getRawAppBadge(): Bitmap? { + return null + } + override fun getBubbleIcon(): Bitmap { return bitmap } 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 5e9d97f23c57..fcd0ed7308ef 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 @@ -20,11 +20,13 @@ import static com.android.wm.shell.bubbles.BubbleDebugConfig.DEBUG_OVERFLOW; import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_BUBBLES; import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME; +import android.annotation.NonNull; import android.content.Context; import android.content.res.Configuration; import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.Color; +import android.graphics.Rect; import android.util.AttributeSet; import android.util.Log; import android.util.TypedValue; @@ -58,6 +60,8 @@ public class BubbleOverflowContainerView extends LinearLayout { private TextView mEmptyStateTitle; private TextView mEmptyStateSubtitle; private ImageView mEmptyStateImage; + private int mHorizontalMargin; + private int mVerticalMargin; private BubbleController mController; private BubbleOverflowAdapter mAdapter; private RecyclerView mRecyclerView; @@ -77,12 +81,6 @@ public class BubbleOverflowContainerView extends LinearLayout { super(context, columns); } -// @Override -// public boolean canScrollVertically() { -// // TODO (b/162006693): this should be based on items in the list & available height -// return true; -// } - @Override public int getColumnCountForAccessibility(RecyclerView.Recycler recycler, RecyclerView.State state) { @@ -98,6 +96,17 @@ public class BubbleOverflowContainerView extends LinearLayout { } } + private class OverflowItemDecoration extends RecyclerView.ItemDecoration { + @Override + public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, + @NonNull RecyclerView parent, @NonNull RecyclerView.State state) { + outRect.left = mHorizontalMargin; + outRect.top = mVerticalMargin; + outRect.right = mHorizontalMargin; + outRect.bottom = mVerticalMargin; + } + } + public BubbleOverflowContainerView(Context context) { this(context, null); } @@ -161,6 +170,9 @@ public class BubbleOverflowContainerView extends LinearLayout { final int columns = res.getInteger(R.integer.bubbles_overflow_columns); mRecyclerView.setLayoutManager( new OverflowGridLayoutManager(getContext(), columns)); + if (mRecyclerView.getItemDecorationCount() == 0) { + mRecyclerView.addItemDecoration(new OverflowItemDecoration()); + } mAdapter = new BubbleOverflowAdapter(getContext(), mOverflowBubbles, mController::promoteBubbleFromOverflow, mController.getPositioner()); @@ -188,6 +200,13 @@ public class BubbleOverflowContainerView extends LinearLayout { final int mode = res.getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK; final boolean isNightMode = (mode == Configuration.UI_MODE_NIGHT_YES); + mHorizontalMargin = res.getDimensionPixelSize( + R.dimen.bubble_overflow_item_padding_horizontal); + mVerticalMargin = res.getDimensionPixelSize(R.dimen.bubble_overflow_item_padding_vertical); + if (mRecyclerView != null) { + mRecyclerView.invalidateItemDecorations(); + } + mEmptyStateImage.setImageDrawable(isNightMode ? res.getDrawable(R.drawable.bubble_ic_empty_overflow_dark) : res.getDrawable(R.drawable.bubble_ic_empty_overflow_light)); @@ -277,8 +296,7 @@ class BubbleOverflowAdapter extends RecyclerView.Adapter<BubbleOverflowAdapter.V } @Override - public BubbleOverflowAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, - int viewType) { + public BubbleOverflowAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { // Set layout for overflow bubble view. LinearLayout overflowView = (LinearLayout) LayoutInflater.from(parent.getContext()) 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 127d5a8a9966..e9729e45731b 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 @@ -20,6 +20,7 @@ import static java.lang.annotation.RetentionPolicy.SOURCE; import android.annotation.IntDef; import android.content.Context; +import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Insets; import android.graphics.PointF; @@ -66,7 +67,11 @@ public class BubblePositioner { /** The max percent of screen width to use for the flyout on phone. */ public static final float FLYOUT_MAX_WIDTH_PERCENT = 0.6f; /** The percent of screen width that should be used for the expanded view on a large screen. **/ - public static final float EXPANDED_VIEW_LARGE_SCREEN_WIDTH_PERCENT = 0.72f; + private 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. **/ + 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. **/ + private static final float EXPANDED_VIEW_SMALL_TABLET_WIDTH_PERCENT = 0.72f; private Context mContext; private WindowManager mWindowManager; @@ -76,16 +81,21 @@ public class BubblePositioner { private boolean mImeVisible; private int mImeHeight; private boolean mIsLargeScreen; + private boolean mIsSmallTablet; private Rect mPositionRect; private int mDefaultMaxBubbles; private int mMaxBubbles; private int mBubbleSize; private int mSpacingBetweenBubbles; + private int mBubblePaddingTop; + private int mBubbleOffscreenAmount; + private int mStackOffset; private int mExpandedViewMinHeight; private int mExpandedViewLargeScreenWidth; - private int mExpandedViewLargeScreenInset; + private int mExpandedViewLargeScreenInsetClosestEdge; + private int mExpandedViewLargeScreenInsetFurthestEdge; private int mOverflowWidth; private int mExpandedViewPadding; @@ -112,10 +122,6 @@ public class BubblePositioner { update(); } - public void setRotation(int rotation) { - mRotation = rotation; - } - /** * Available space and inset information. Call this when config changes * occur or when added to a window. @@ -130,17 +136,26 @@ public class BubblePositioner { | WindowInsets.Type.statusBars() | WindowInsets.Type.displayCutout()); - mIsLargeScreen = mContext.getResources().getConfiguration().smallestScreenWidthDp >= 600; + final Rect bounds = windowMetrics.getBounds(); + Configuration config = mContext.getResources().getConfiguration(); + mIsLargeScreen = config.smallestScreenWidthDp >= 600; + if (mIsLargeScreen) { + float largestEdgeDp = Math.max(config.screenWidthDp, config.screenHeightDp); + mIsSmallTablet = largestEdgeDp < 960; + } else { + mIsSmallTablet = false; + } if (BubbleDebugConfig.DEBUG_POSITIONER) { Log.w(TAG, "update positioner:" + " rotation: " + mRotation + " insets: " + insets + " isLargeScreen: " + mIsLargeScreen - + " bounds: " + windowMetrics.getBounds() + + " isSmallTablet: " + mIsSmallTablet + + " bounds: " + bounds + " showingInTaskbar: " + mShowingInTaskbar); } - updateInternal(mRotation, insets, windowMetrics.getBounds()); + updateInternal(mRotation, insets, bounds); } /** @@ -175,15 +190,36 @@ 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); - mExpandedViewLargeScreenWidth = (int) (bounds.width() - * EXPANDED_VIEW_LARGE_SCREEN_WIDTH_PERCENT); - mExpandedViewLargeScreenInset = mIsLargeScreen - ? (bounds.width() - mExpandedViewLargeScreenWidth) / 2 - : mExpandedViewPadding; - mOverflowWidth = mIsLargeScreen - ? mExpandedViewLargeScreenWidth - : res.getDimensionPixelSize( - R.dimen.bubble_expanded_view_phone_landscape_overflow_width); + mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top); + mBubbleOffscreenAmount = res.getDimensionPixelSize(R.dimen.bubble_stack_offscreen); + mStackOffset = res.getDimensionPixelSize(R.dimen.bubble_stack_offset); + + if (mIsSmallTablet) { + mExpandedViewLargeScreenWidth = (int) (bounds.width() + * EXPANDED_VIEW_SMALL_TABLET_WIDTH_PERCENT); + } else { + mExpandedViewLargeScreenWidth = isLandscape() + ? (int) (bounds.width() * EXPANDED_VIEW_LARGE_SCREEN_LANDSCAPE_WIDTH_PERCENT) + : (int) (bounds.width() * EXPANDED_VIEW_LARGE_SCREEN_PORTRAIT_WIDTH_PERCENT); + } + if (mIsLargeScreen) { + if (isLandscape() && !mIsSmallTablet) { + mExpandedViewLargeScreenInsetClosestEdge = res.getDimensionPixelSize( + R.dimen.bubble_expanded_view_largescreen_landscape_padding); + mExpandedViewLargeScreenInsetFurthestEdge = bounds.width() + - mExpandedViewLargeScreenInsetClosestEdge + - mExpandedViewLargeScreenWidth; + } else { + final int centeredInset = (bounds.width() - mExpandedViewLargeScreenWidth) / 2; + mExpandedViewLargeScreenInsetClosestEdge = centeredInset; + mExpandedViewLargeScreenInsetFurthestEdge = centeredInset; + } + } else { + mExpandedViewLargeScreenInsetClosestEdge = mExpandedViewPadding; + mExpandedViewLargeScreenInsetFurthestEdge = mExpandedViewPadding; + } + + mOverflowWidth = res.getDimensionPixelSize(R.dimen.bubble_expanded_view_overflow_width); mPointerWidth = res.getDimensionPixelSize(R.dimen.bubble_pointer_width); mPointerHeight = res.getDimensionPixelSize(R.dimen.bubble_pointer_height); mPointerMargin = res.getDimensionPixelSize(R.dimen.bubble_pointer_margin); @@ -273,7 +309,8 @@ public class BubblePositioner { /** @return whether the device is in landscape orientation. */ public boolean isLandscape() { - return mRotation == Surface.ROTATION_90 || mRotation == Surface.ROTATION_270; + return mContext.getResources().getConfiguration().orientation + == Configuration.ORIENTATION_LANDSCAPE; } /** @return whether the screen is considered large. */ @@ -299,6 +336,26 @@ public class BubblePositioner { : mBubbleSize; } + /** The amount of padding at the top of the screen that the bubbles avoid when being placed. */ + public int getBubblePaddingTop() { + return mBubblePaddingTop; + } + + /** The amount the stack hang off of the screen when collapsed. */ + public int getStackOffScreenAmount() { + return mBubbleOffscreenAmount; + } + + /** Offset of bubbles in the stack (i.e. how much they overlap). */ + public int getStackOffset() { + return mStackOffset; + } + + /** Size of the visible (non-overlapping) part of the pointer. */ + public int getPointerSize() { + return mPointerHeight - mPointerOverlap; + } + /** The maximum number of bubbles that can be displayed comfortably on screen. */ public int getMaxBubbles() { return mMaxBubbles; @@ -315,6 +372,15 @@ public class BubblePositioner { mImeHeight = height; } + private int getExpandedViewLargeScreenInsetFurthestEdge(boolean isOverflow) { + if (isOverflow && mIsLargeScreen) { + return mScreenRect.width() + - mExpandedViewLargeScreenInsetClosestEdge + - mOverflowWidth; + } + return mExpandedViewLargeScreenInsetFurthestEdge; + } + /** * Calculates the padding for the bubble expanded view. * @@ -328,17 +394,22 @@ public class BubblePositioner { * padding is added. */ public int[] getExpandedViewContainerPadding(boolean onLeft, boolean isOverflow) { - final int pointerTotalHeight = mPointerHeight - mPointerOverlap; + final int pointerTotalHeight = getPointerSize(); + final int expandedViewLargeScreenInsetFurthestEdge = + getExpandedViewLargeScreenInsetFurthestEdge(isOverflow); if (mIsLargeScreen) { + // Note: + // If we're in portrait OR if we're a small tablet, then the two insets values will + // be equal. If we're landscape and a large tablet, the two values will be different. // [left, top, right, bottom] mPaddings[0] = onLeft - ? mExpandedViewLargeScreenInset - pointerTotalHeight - : mExpandedViewLargeScreenInset; + ? mExpandedViewLargeScreenInsetClosestEdge - pointerTotalHeight + : expandedViewLargeScreenInsetFurthestEdge; mPaddings[1] = 0; mPaddings[2] = onLeft - ? mExpandedViewLargeScreenInset - : mExpandedViewLargeScreenInset - pointerTotalHeight; - // Overflow doesn't show manage button / get padding from it so add padding here for it + ? expandedViewLargeScreenInsetFurthestEdge + : mExpandedViewLargeScreenInsetClosestEdge - pointerTotalHeight; + // Overflow doesn't show manage button / get padding from it so add padding here mPaddings[3] = isOverflow ? mExpandedViewPadding : 0; return mPaddings; } else { @@ -496,12 +567,13 @@ public class BubblePositioner { float x; float y; if (showBubblesVertically()) { + int inset = mExpandedViewLargeScreenInsetClosestEdge; y = rowStart + positionInRow; int left = mIsLargeScreen - ? mExpandedViewLargeScreenInset - mExpandedViewPadding - mBubbleSize + ? inset - mExpandedViewPadding - mBubbleSize : mPositionRect.left; int right = mIsLargeScreen - ? mPositionRect.right - mExpandedViewLargeScreenInset + mExpandedViewPadding + ? mPositionRect.right - inset + mExpandedViewPadding : mPositionRect.right - mBubbleSize; x = state.onLeft ? left @@ -512,7 +584,7 @@ public class BubblePositioner { } if (showBubblesVertically() && mImeVisible) { - return new PointF(x, getExpandedBubbleYForIme(index, state.numberOfBubbles)); + return new PointF(x, getExpandedBubbleYForIme(index, state)); } return new PointF(x, y); } @@ -522,10 +594,10 @@ public class BubblePositioner { * is showing. * * @param index the index of the bubble in the stack. - * @param numberOfBubbles the total number of bubbles in the stack. + * @param state information about the stack state (# of bubbles, selected bubble). * @return y position of the bubble on-screen when the stack is expanded. */ - private float getExpandedBubbleYForIme(int index, int numberOfBubbles) { + private float getExpandedBubbleYForIme(int index, BubbleStackView.StackViewState state) { final float top = getAvailableRect().top + mExpandedViewPadding; if (!showBubblesVertically()) { // Showing horizontally: align to top @@ -533,12 +605,11 @@ public class BubblePositioner { } // Showing vertically: might need to translate the bubbles above the IME. - // Subtract spacing here to provide a margin between top of IME and bottom of bubble row. - final float bottomInset = getImeHeight() + mInsets.bottom - (mSpacingBetweenBubbles * 2); - final float expandedStackSize = getExpandedStackSize(numberOfBubbles); - final float centerPosition = showBubblesVertically() - ? mPositionRect.centerY() - : mPositionRect.centerX(); + // Add spacing here to provide a margin between top of IME and bottom of bubble row. + final float bottomHeight = getImeHeight() + mInsets.bottom + (mSpacingBetweenBubbles * 2); + final float bottomInset = mScreenRect.bottom - bottomHeight; + final float expandedStackSize = getExpandedStackSize(state.numberOfBubbles); + final float centerPosition = mPositionRect.centerY(); final float rowBottom = centerPosition + (expandedStackSize / 2f); final float rowTop = centerPosition - (expandedStackSize / 2f); float rowTopForIme = rowTop; @@ -549,7 +620,7 @@ public class BubblePositioner { if (rowTop - translationY < top) { // Even if we shift the bubbles, they will still overlap with the IME. // Hide the overflow for a lil more space: - final float expandedStackSizeNoO = getExpandedStackSize(numberOfBubbles - 1); + final float expandedStackSizeNoO = getExpandedStackSize(state.numberOfBubbles - 1); final float centerPositionNoO = showBubblesVertically() ? mPositionRect.centerY() : mPositionRect.centerX(); @@ -559,6 +630,13 @@ public class BubblePositioner { rowTopForIme = rowTopNoO - translationY; } } + // Check if the selected bubble is within the appropriate space + final float selectedPosition = rowTopForIme + + (state.selectedIndex * (mBubbleSize + mSpacingBetweenBubbles)); + if (selectedPosition < top) { + // We must always keep the selected bubble in view so we'll have to allow more overlap. + rowTopForIme = top; + } return rowTopForIme + (index * (mBubbleSize + mSpacingBetweenBubbles)); } @@ -622,7 +700,28 @@ public class BubblePositioner { return new BubbleStackView.RelativeStackPosition( startOnLeft, startingVerticalOffset / mPositionRect.height()) - .getAbsolutePositionInRegion(new RectF(mPositionRect)); + .getAbsolutePositionInRegion(getAllowableStackPositionRegion( + 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. + * While the stack position is not allowed to rest outside of these bounds, it can temporarily + * be animated or dragged beyond them. + */ + public RectF getAllowableStackPositionRegion(int bubbleCount) { + final RectF allowableRegion = new RectF(getAvailableRect()); + final int imeHeight = getImeHeight(); + final float bottomPadding = bubbleCount > 1 + ? mBubblePaddingTop + mStackOffset + : mBubblePaddingTop; + allowableRegion.left -= mBubbleOffscreenAmount; + allowableRegion.top += mBubblePaddingTop; + allowableRegion.right += mBubbleOffscreenAmount - mBubbleSize; + allowableRegion.bottom -= imeHeight + bottomPadding + mBubbleSize; + return allowableRegion; } /** 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 14433c233273..0a334140d616 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 @@ -70,6 +70,7 @@ import androidx.dynamicanimation.animation.SpringAnimation; import androidx.dynamicanimation.animation.SpringForce; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.policy.ScreenDecorationsUtils; import com.android.internal.util.FrameworkStatsLog; import com.android.wm.shell.R; import com.android.wm.shell.animation.Interpolators; @@ -82,7 +83,6 @@ import com.android.wm.shell.common.FloatingContentCoordinator; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.magnetictarget.MagnetizedObject; -import java.io.FileDescriptor; import java.io.PrintWriter; import java.math.BigDecimal; import java.math.RoundingMode; @@ -167,26 +167,27 @@ public class BubbleStackView extends FrameLayout private static final SurfaceSynchronizer DEFAULT_SURFACE_SYNCHRONIZER = new SurfaceSynchronizer() { - @Override - public void syncSurfaceAndRun(Runnable callback) { - Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() { - // Just wait 2 frames. There is no guarantee, but this is usually enough time that - // the requested change is reflected on the screen. - // TODO: Once SurfaceFlinger provide APIs to sync the state of {@code View} and - // surfaces, rewrite this logic with them. - private int mFrameWait = 2; - @Override - public void doFrame(long frameTimeNanos) { - if (--mFrameWait > 0) { - Choreographer.getInstance().postFrameCallback(this); - } else { - callback.run(); - } + public void syncSurfaceAndRun(Runnable callback) { + Choreographer.FrameCallback frameCallback = new Choreographer.FrameCallback() { + // Just wait 2 frames. There is no guarantee, but this is usually enough + // time that the requested change is reflected on the screen. + // TODO: Once SurfaceFlinger provide APIs to sync the state of + // {@code View} and surfaces, rewrite this logic with them. + private int mFrameWait = 2; + + @Override + public void doFrame(long frameTimeNanos) { + if (--mFrameWait > 0) { + Choreographer.getInstance().postFrameCallback(this); + } else { + callback.run(); + } + } + }; + Choreographer.getInstance().postFrameCallback(frameCallback); } - }); - } - }; + }; private final BubbleController mBubbleController; private final BubbleData mBubbleData; private StackViewState mStackViewState = new StackViewState(); @@ -276,7 +277,7 @@ public class BubbleStackView extends FrameLayout private int mPointerIndexDown = -1; /** Description of current animation controller state. */ - public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + public void dump(PrintWriter pw, String[] args) { pw.println("Stack view state:"); String bubblesOnScreen = BubbleDebugConfig.formatBubblesString( @@ -290,8 +291,8 @@ public class BubbleStackView extends FrameLayout pw.print(" expandedContainerMatrix: "); pw.println(mExpandedViewContainer.getAnimationMatrix()); - mStackAnimationController.dump(fd, pw, args); - mExpandedAnimationController.dump(fd, pw, args); + mStackAnimationController.dump(pw, args); + mExpandedAnimationController.dump(pw, args); if (mExpandedBubble != null) { pw.println("Expanded bubble state:"); @@ -780,12 +781,12 @@ public class BubbleStackView extends FrameLayout mPositioner = mBubbleController.getPositioner(); final TypedArray ta = mContext.obtainStyledAttributes( - new int[] {android.R.attr.dialogCornerRadius}); + new int[]{android.R.attr.dialogCornerRadius}); mCornerRadius = ta.getDimensionPixelSize(0, 0); ta.recycle(); final Runnable onBubbleAnimatedOut = () -> { - if (getBubbleCount() == 0 && !mBubbleData.isShowingOverflow()) { + if (getBubbleCount() == 0) { mBubbleController.onAllBubblesAnimatedOut(); } }; @@ -822,7 +823,9 @@ public class BubbleStackView extends FrameLayout mAnimatingOutSurfaceView = new SurfaceView(getContext()); mAnimatingOutSurfaceView.setUseAlpha(); mAnimatingOutSurfaceView.setZOrderOnTop(true); - mAnimatingOutSurfaceView.setCornerRadius(mCornerRadius); + boolean supportsRoundedCorners = ScreenDecorationsUtils.supportsRoundedCornersOnWindows( + mContext.getResources()); + mAnimatingOutSurfaceView.setCornerRadius(supportsRoundedCorners ? mCornerRadius : 0); mAnimatingOutSurfaceView.setLayoutParams(new ViewGroup.LayoutParams(0, 0)); mAnimatingOutSurfaceView.getHolder().addCallback(new SurfaceHolder.Callback() { @Override @@ -895,7 +898,7 @@ public class BubbleStackView extends FrameLayout mStackAnimationController.updateResources(); mBubbleOverflow.updateResources(); - if (mRelativeStackPositionBeforeRotation != null) { + if (!isStackEduShowing() && mRelativeStackPositionBeforeRotation != null) { mStackAnimationController.setStackPosition( mRelativeStackPositionBeforeRotation); mRelativeStackPositionBeforeRotation = null; @@ -910,8 +913,10 @@ public class BubbleStackView extends FrameLayout afterExpandedViewAnimation(); showManageMenu(mShowingManage); } /* after */); + PointF p = mPositioner.getExpandedBubbleXY(getBubbleIndex(mExpandedBubble), + getState()); final float translationY = mPositioner.getExpandedViewY(mExpandedBubble, - getBubbleIndex(mExpandedBubble)); + mPositioner.showBubblesVertically() ? p.y : p.x); mExpandedViewContainer.setTranslationX(0f); mExpandedViewContainer.setTranslationY(translationY); mExpandedViewContainer.setAlpha(1f); @@ -939,7 +944,7 @@ public class BubbleStackView extends FrameLayout }); // If the stack itself is clicked, it means none of its touchable views (bubbles, flyouts, - // TaskView, etc.) were touched. Collapse the stack if it's expanded. + // TaskView, etc.) were touched. Collapse the stack if it's expanded. setOnClickListener(view -> { if (mShowingManage) { showManageMenu(false /* show */); @@ -1045,10 +1050,17 @@ public class BubbleStackView extends FrameLayout private final Runnable mAnimateTemporarilyInvisibleImmediate = () -> { if (mTemporarilyInvisible && mFlyout.getVisibility() != View.VISIBLE) { + // To calculate a distance, bubble stack needs to be moved to become hidden, + // we need to take into account that the bubble stack is positioned on the edge + // of the available screen rect, which can be offset by system bars and cutouts. if (mStackAnimationController.isStackOnLeftSide()) { - animate().translationX(-mBubbleSize).start(); + int availableRectOffsetX = + mPositioner.getAvailableRect().left - mPositioner.getScreenRect().left; + animate().translationX(-(mBubbleSize + availableRectOffsetX)).start(); } else { - animate().translationX(mBubbleSize).start(); + int availableRectOffsetX = + mPositioner.getAvailableRect().right - mPositioner.getScreenRect().right; + animate().translationX(mBubbleSize - availableRectOffsetX).start(); } } else { animate().translationX(0).start(); @@ -1128,6 +1140,7 @@ public class BubbleStackView extends FrameLayout // The menu itself should respect locale direction so the icons are on the correct side. mManageMenu.setLayoutDirection(LAYOUT_DIRECTION_LOCALE); addView(mManageMenu); + updateManageButtonListener(); } /** @@ -1189,6 +1202,8 @@ public class BubbleStackView extends FrameLayout addView(mStackEduView); } mBubbleContainer.bringToFront(); + // Ensure the stack is in the correct spot + mStackAnimationController.setStackPosition(mPositioner.getDefaultStartPosition()); return mStackEduView.show(mPositioner.getDefaultStartPosition()); } @@ -1203,6 +1218,8 @@ public class BubbleStackView extends FrameLayout mStackEduView = new StackEducationView(mContext, mPositioner, mBubbleController); addView(mStackEduView); mBubbleContainer.bringToFront(); // Stack appears on top of the stack education + // Ensure the stack is in the correct spot + mStackAnimationController.setStackPosition(mPositioner.getDefaultStartPosition()); mStackEduView.show(mPositioner.getDefaultStartPosition()); } if (mManageEduView != null && mManageEduView.getVisibility() == VISIBLE) { @@ -1233,7 +1250,7 @@ public class BubbleStackView extends FrameLayout b.getExpandedView().updateFontSize(); } } - if (mBubbleOverflow != null) { + if (mBubbleOverflow != null && mBubbleOverflow.getExpandedView() != null) { mBubbleOverflow.getExpandedView().updateFontSize(); } } @@ -1279,7 +1296,7 @@ public class BubbleStackView extends FrameLayout public void onOrientationChanged() { mRelativeStackPositionBeforeRotation = new RelativeStackPosition( mPositioner.getRestingPosition(), - mStackAnimationController.getAllowableStackPositionRegion()); + mPositioner.getAllowableStackPositionRegion(getBubbleCount())); addOnLayoutChangeListener(mOrientationChangedListener); hideFlyoutImmediate(); } @@ -1300,7 +1317,6 @@ public class BubbleStackView extends FrameLayout /** Respond to the display size change by recalculating view size and location. */ public void onDisplaySizeChanged() { updateOverflow(); - setUpManageMenu(); setUpFlyout(); setUpDismissView(); updateUserEdu(); @@ -1320,13 +1336,16 @@ public class BubbleStackView extends FrameLayout mStackAnimationController.updateResources(); mDismissView.updateResources(); mMagneticTarget.setMagneticFieldRadiusPx(mBubbleSize * 2); - mStackAnimationController.setStackPosition( - new RelativeStackPosition( - mPositioner.getRestingPosition(), - mStackAnimationController.getAllowableStackPositionRegion())); + if (!isStackEduShowing()) { + mStackAnimationController.setStackPosition( + new RelativeStackPosition( + mPositioner.getRestingPosition(), + mPositioner.getAllowableStackPositionRegion(getBubbleCount()))); + } if (mIsExpanded) { updateExpandedView(); } + setUpManageMenu(); } @Override @@ -1421,7 +1440,7 @@ public class BubbleStackView extends FrameLayout if (super.performAccessibilityActionInternal(action, arguments)) { return true; } - final RectF stackBounds = mStackAnimationController.getAllowableStackPositionRegion(); + final RectF stackBounds = mPositioner.getAllowableStackPositionRegion(getBubbleCount()); // R constants are not final so we cannot use switch-case here. if (action == AccessibilityNodeInfo.ACTION_DISMISS) { @@ -1482,6 +1501,69 @@ public class BubbleStackView extends FrameLayout } } + /** + * Update bubbles' icon views accessibility states. + */ + public void updateBubblesAcessibillityStates() { + for (int i = 0; i < mBubbleData.getBubbles().size(); i++) { + Bubble prevBubble = i > 0 ? mBubbleData.getBubbles().get(i - 1) : null; + Bubble bubble = mBubbleData.getBubbles().get(i); + + View bubbleIconView = bubble.getIconView(); + if (bubbleIconView == null) { + continue; + } + + if (mIsExpanded) { + // when stack is expanded + // all bubbles are important for accessibility + bubbleIconView + .setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES); + + View prevBubbleIconView = prevBubble != null ? prevBubble.getIconView() : null; + + if (prevBubbleIconView != null) { + bubbleIconView.setAccessibilityDelegate(new View.AccessibilityDelegate() { + @Override + public void onInitializeAccessibilityNodeInfo(View v, + AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(v, info); + info.setTraversalAfter(prevBubbleIconView); + } + }); + } + } else { + // when stack is collapsed, only the top bubble is important for accessibility, + bubbleIconView.setImportantForAccessibility( + i == 0 ? View.IMPORTANT_FOR_ACCESSIBILITY_YES : + View.IMPORTANT_FOR_ACCESSIBILITY_NO); + } + } + + if (mIsExpanded) { + // make the overflow bubble last in the accessibility traversal order + + View bubbleOverflowIconView = + mBubbleOverflow != null ? mBubbleOverflow.getIconView() : null; + if (bubbleOverflowIconView != null && !mBubbleData.getBubbles().isEmpty()) { + Bubble lastBubble = + mBubbleData.getBubbles().get(mBubbleData.getBubbles().size() - 1); + View lastBubbleIconView = lastBubble.getIconView(); + if (lastBubbleIconView != null) { + bubbleOverflowIconView.setAccessibilityDelegate( + new View.AccessibilityDelegate() { + @Override + public void onInitializeAccessibilityNodeInfo(View v, + AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(v, info); + info.setTraversalAfter(lastBubbleIconView); + } + }); + } + } + } + } + private void updateSystemGestureExcludeRects() { // Exclude the region occupied by the first BubbleView in the stack Rect excludeZone = mSystemGestureExclusionRects.get(0); @@ -1526,6 +1608,13 @@ public class BubbleStackView extends FrameLayout } /** + * Whether the stack of bubbles is animating a switch between bubbles. + */ + public boolean isSwitchAnimating() { + return mIsBubbleSwitchAnimating; + } + + /** * The {@link Bubble} that is expanded, null if one does not exist. */ @VisibleForTesting @@ -1541,7 +1630,9 @@ public class BubbleStackView extends FrameLayout Log.d(TAG, "addBubble: " + bubble); } - if (getBubbleCount() == 0 && shouldShowStackEdu()) { + final boolean firstBubble = getBubbleCount() == 0; + + if (firstBubble && shouldShowStackEdu()) { // Override the default stack position if we're showing user education. mStackAnimationController.setStackPosition(mPositioner.getDefaultStartPosition()); } @@ -1554,7 +1645,7 @@ public class BubbleStackView extends FrameLayout new FrameLayout.LayoutParams(mPositioner.getBubbleSize(), mPositioner.getBubbleSize())); - if (getBubbleCount() == 0) { + if (firstBubble) { mStackOnLeftOrWillBe = mStackAnimationController.isStackOnLeftSide(); } // Set the dot position to the opposite of the side the stack is resting on, since the stack @@ -1584,13 +1675,21 @@ public class BubbleStackView extends FrameLayout } else { bubble.cleanupViews(); } - updatePointerPosition(false /* forIme */); updateExpandedView(); + if (getBubbleCount() == 0 && !isExpanded()) { + // This is the last bubble and the stack is collapsed + updateStackPosition(); + } logBubbleEvent(bubble, FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__DISMISSED); return; } } - Log.d(TAG, "was asked to remove Bubble, but didn't find the view! " + bubble); + // If a bubble is suppressed, it is not attached to the container. Clean it up. + if (bubble.isSuppressed()) { + bubble.cleanupViews(); + } else { + Log.d(TAG, "was asked to remove Bubble, but didn't find the view! " + bubble); + } } private void updateOverflowVisibility() { @@ -1776,11 +1875,30 @@ public class BubbleStackView extends FrameLayout } } - void setBubbleVisibility(Bubble b, boolean visible) { - if (b.getIconView() != null) { - b.getIconView().setVisibility(visible ? VISIBLE : GONE); + void setBubbleSuppressed(Bubble bubble, boolean suppressed) { + if (DEBUG_BUBBLE_STACK_VIEW) { + Log.d(TAG, "setBubbleSuppressed: suppressed=" + suppressed + " bubble=" + bubble); + } + if (suppressed) { + int index = getBubbleIndex(bubble); + mBubbleContainer.removeViewAt(index); + updateExpandedView(); + } else { + if (bubble.getIconView() == null) { + return; + } + if (bubble.getIconView().getParent() != null) { + Log.e(TAG, "Bubble is already added to parent. Can't unsuppress: " + bubble); + return; + } + int index = mBubbleData.getBubbles().indexOf(bubble); + // Add the view back to the correct position + mBubbleContainer.addView(bubble.getIconView(), index, + new LayoutParams(mPositioner.getBubbleSize(), + mPositioner.getBubbleSize())); + updateBubbleShadows(false /* showForAllBubbles */); + requestUpdate(); } - // TODO(b/181166384): Animate in / out & handle adjusting how the bubbles overlap } /** @@ -2102,11 +2220,10 @@ public class BubbleStackView extends FrameLayout private void animateSwitchBubbles() { // If we're no longer expanded, this is meaningless. if (!mIsExpanded) { + mIsBubbleSwitchAnimating = false; return; } - mIsBubbleSwitchAnimating = true; - // The surface contains a screenshot of the animating out bubble, so we just need to animate // it out (and then release the GraphicBuffer). PhysicsAnimator.getInstance(mAnimatingOutSurfaceContainer).cancel(); @@ -2125,7 +2242,7 @@ public class BubbleStackView extends FrameLayout PhysicsAnimator.getInstance(mAnimatingOutSurfaceContainer) .spring(DynamicAnimation.TRANSLATION_Y, mAnimatingOutSurfaceContainer.getTranslationY() - mBubbleSize, - mTranslateSpringConfig) + mTranslateSpringConfig) .start(); } @@ -2246,7 +2363,14 @@ public class BubbleStackView extends FrameLayout } } 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); @@ -2349,6 +2473,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 + mIsBubbleSwitchAnimating = true; + } mBubbleData.dismissBubbleWithKey(bubble.getKey(), Bubbles.DISMISS_USER_GESTURE); } } @@ -2614,11 +2742,13 @@ public class BubbleStackView extends FrameLayout // If available, update the manage menu's settings option with the expanded bubble's app // name and icon. - if (show && mBubbleData.hasBubbleInStackWithKey(mExpandedBubble.getKey())) { + if (show) { final Bubble bubble = mBubbleData.getBubbleInStackWithKey(mExpandedBubble.getKey()); - mManageSettingsIcon.setImageBitmap(bubble.getAppBadge()); - mManageSettingsText.setText(getResources().getString( - R.string.bubbles_app_settings, bubble.getAppName())); + if (bubble != null) { + mManageSettingsIcon.setImageBitmap(bubble.getRawAppBadge()); + mManageSettingsText.setText(getResources().getString( + R.string.bubbles_app_settings, bubble.getAppName())); + } } if (mExpandedBubble.getExpandedView().getTaskView() != null) { @@ -2697,9 +2827,17 @@ public class BubbleStackView extends FrameLayout mExpandedViewContainer.setVisibility(View.INVISIBLE); mExpandedViewContainer.setAlpha(0f); mExpandedViewContainer.addView(bev); - bev.setManageClickListener((view) -> showManageMenu(!mShowingManage)); + + postDelayed(() -> { + // Set the Manage button click handler from postDelayed. This appears to resolve + // a race condition with adding the BubbleExpandedView view to the expanded view + // container. Due to the race condition the click handler sometimes is not set up + // correctly and is never called. + updateManageButtonListener(); + }, 0); if (!mIsExpansionAnimating) { + mIsBubbleSwitchAnimating = true; mSurfaceSynchronizer.syncSurfaceAndRun(() -> { post(this::animateSwitchBubbles); }); @@ -2707,6 +2845,16 @@ public class BubbleStackView extends FrameLayout } } + private void updateManageButtonListener() { + if (mIsExpanded && mExpandedBubble != null + && mExpandedBubble.getExpandedView() != null) { + BubbleExpandedView bev = mExpandedBubble.getExpandedView(); + bev.setManageClickListener((view) -> { + showManageMenu(true /* show */); + }); + } + } + /** * Requests a snapshot from the currently expanded bubble's TaskView and displays it in a * SurfaceView. This allows us to load a newly expanded bubble's Activity into the TaskView, @@ -2750,7 +2898,10 @@ public class BubbleStackView extends FrameLayout PhysicsAnimator.getInstance(mAnimatingOutSurfaceContainer).cancel(); mAnimatingOutSurfaceContainer.setScaleX(1f); mAnimatingOutSurfaceContainer.setScaleY(1f); - mAnimatingOutSurfaceContainer.setTranslationX(mExpandedViewContainer.getPaddingLeft()); + final float translationX = mPositioner.showBubblesVertically() && mStackOnLeftOrWillBe + ? mExpandedViewContainer.getPaddingLeft() + mPositioner.getPointerSize() + : mExpandedViewContainer.getPaddingLeft(); + mAnimatingOutSurfaceContainer.setTranslationX(translationX); mAnimatingOutSurfaceContainer.setTranslationY(0); final int[] taskViewLocation = @@ -2972,14 +3123,14 @@ public class BubbleStackView extends FrameLayout * Logs the bubble UI event. * * @param provider the bubble view provider that is being interacted on. Null value indicates - * that the user interaction is not specific to one bubble. - * @param action the user interaction enum. + * that the user interaction is not specific to one bubble. + * @param action the user interaction enum. */ private void logBubbleEvent(@Nullable BubbleViewProvider provider, int action) { final String packageName = (provider != null && provider instanceof Bubble) - ? ((Bubble) provider).getPackageName() - : "null"; + ? ((Bubble) provider).getPackageName() + : "null"; mBubbleData.logBubbleEvent(provider, action, packageName, @@ -3012,6 +3163,16 @@ public class BubbleStackView extends FrameLayout } /** + * Handles vertical offset changes, e.g. when one handed mode is switched on/off. + * + * @param offset new vertical offset. + */ + void onVerticalOffsetChanged(int offset) { + // adjust dismiss view vertical position, so that it is still visible to the user + mDismissView.setPadding(/* left = */ 0, /* top = */ 0, /* right = */ 0, offset); + } + + /** * Holds some commonly queried information about the stack. */ public static class StackViewState { 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 932f879caef8..69762c9bc06a 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 @@ -71,6 +71,7 @@ public class BubbleViewInfoTask extends AsyncTask<Void, Void, BubbleViewInfoTask private WeakReference<BubbleController> mController; private WeakReference<BubbleStackView> mStackView; private BubbleIconFactory mIconFactory; + private BubbleBadgeIconFactory mBadgeIconFactory; private boolean mSkipInflation; private Callback mCallback; private Executor mMainExecutor; @@ -84,6 +85,7 @@ public class BubbleViewInfoTask extends AsyncTask<Void, Void, BubbleViewInfoTask BubbleController controller, BubbleStackView stackView, BubbleIconFactory factory, + BubbleBadgeIconFactory badgeFactory, boolean skipInflation, Callback c, Executor mainExecutor) { @@ -92,6 +94,7 @@ public class BubbleViewInfoTask extends AsyncTask<Void, Void, BubbleViewInfoTask mController = new WeakReference<>(controller); mStackView = new WeakReference<>(stackView); mIconFactory = factory; + mBadgeIconFactory = badgeFactory; mSkipInflation = skipInflation; mCallback = c; mMainExecutor = mainExecutor; @@ -100,7 +103,7 @@ public class BubbleViewInfoTask extends AsyncTask<Void, Void, BubbleViewInfoTask @Override protected BubbleViewInfo doInBackground(Void... voids) { return BubbleViewInfo.populate(mContext.get(), mController.get(), mStackView.get(), - mIconFactory, mBubble, mSkipInflation); + mIconFactory, mBadgeIconFactory, mBubble, mSkipInflation); } @Override @@ -127,6 +130,7 @@ public class BubbleViewInfoTask extends AsyncTask<Void, Void, BubbleViewInfoTask String appName; Bitmap bubbleBitmap; Bitmap badgeBitmap; + Bitmap mRawBadgeBitmap; int dotColor; Path dotPath; Bubble.FlyoutMessage flyoutMessage; @@ -134,7 +138,8 @@ public class BubbleViewInfoTask extends AsyncTask<Void, Void, BubbleViewInfoTask @VisibleForTesting @Nullable public static BubbleViewInfo populate(Context c, BubbleController controller, - BubbleStackView stackView, BubbleIconFactory iconFactory, Bubble b, + BubbleStackView stackView, BubbleIconFactory iconFactory, + BubbleBadgeIconFactory badgeIconFactory, Bubble b, boolean skipInflation) { BubbleViewInfo info = new BubbleViewInfo(); @@ -186,12 +191,12 @@ public class BubbleViewInfoTask extends AsyncTask<Void, Void, BubbleViewInfoTask bubbleDrawable = appIcon; } - BitmapInfo badgeBitmapInfo = iconFactory.getBadgeBitmap(badgedIcon, + BitmapInfo badgeBitmapInfo = badgeIconFactory.getBadgeBitmap(badgedIcon, b.isImportantConversation()); info.badgeBitmap = badgeBitmapInfo.icon; - info.bubbleBitmap = iconFactory.createBadgedIconBitmap(bubbleDrawable, - null /* user */, - true /* shrinkNonAdaptiveIcons */).icon; + // Raw badge bitmap never includes the important conversation ring + info.mRawBadgeBitmap = badgeIconFactory.getBadgeBitmap(badgedIcon, false).icon; + info.bubbleBitmap = iconFactory.createBadgedIconBitmap(bubbleDrawable).icon; // Dot color & placement Path iconPath = PathParser.createPathFromPathData( diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewProvider.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewProvider.java index 7e552826e94a..3f6d41bb2b68 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewProvider.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewProvider.java @@ -43,6 +43,9 @@ public interface BubbleViewProvider { /** App badge drawable to draw above bubble icon. */ @Nullable Bitmap getAppBadge(); + /** Base app badge drawable without any markings. */ + @Nullable Bitmap getRawAppBadge(); + /** Path of normalized bubble icon to draw dot on. */ Path getDotPath(); 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 c82249b8a369..8a0db0a12711 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 @@ -21,9 +21,12 @@ 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.pm.UserInfo; import android.content.res.Configuration; import android.os.Bundle; +import android.os.UserHandle; +import android.service.notification.NotificationListenerService; import android.service.notification.NotificationListenerService.RankingMap; import android.util.ArraySet; import android.util.Pair; @@ -34,7 +37,6 @@ import androidx.annotation.Nullable; import com.android.wm.shell.common.annotations.ExternalThread; -import java.io.FileDescriptor; import java.io.PrintWriter; import java.lang.annotation.Retention; import java.lang.annotation.Target; @@ -55,7 +57,7 @@ public interface Bubbles { DISMISS_NOTIF_CANCEL, DISMISS_ACCESSIBILITY_ACTION, DISMISS_NO_LONGER_BUBBLE, DISMISS_USER_CHANGED, DISMISS_GROUP_CANCELLED, DISMISS_INVALID_INTENT, DISMISS_OVERFLOW_MAX_REACHED, DISMISS_SHORTCUT_REMOVED, DISMISS_PACKAGE_REMOVED, - DISMISS_NO_BUBBLE_UP, DISMISS_RELOAD_FROM_DISK}) + DISMISS_NO_BUBBLE_UP, DISMISS_RELOAD_FROM_DISK, DISMISS_USER_REMOVED}) @Target({FIELD, LOCAL_VARIABLE, PARAMETER}) @interface DismissReason {} @@ -74,6 +76,7 @@ public interface Bubbles { int DISMISS_PACKAGE_REMOVED = 13; int DISMISS_NO_BUBBLE_UP = 14; int DISMISS_RELOAD_FROM_DISK = 15; + int DISMISS_USER_REMOVED = 16; /** * @return {@code true} if there is a bubble associated with the provided key and if its @@ -191,10 +194,26 @@ public interface Bubbles { * @param entryDataByKey a map of ranking key to bubble entry and whether the entry should * bubble up */ - void onRankingUpdated(RankingMap rankingMap, + void onRankingUpdated( + RankingMap rankingMap, HashMap<String, Pair<BubbleEntry, Boolean>> entryDataByKey); /** + * Called when a notification channel is modified, in response to + * {@link NotificationListenerService#onNotificationChannelModified}. + * + * @param pkg the package the notification channel belongs to. + * @param user the user the notification channel belongs to. + * @param channel the channel being modified. + * @param modificationType the type of modification that occurred to the channel. + */ + void onNotificationChannelModified( + String pkg, + UserHandle user, + NotificationChannel channel, + int modificationType); + + /** * Called when the status bar has become visible or invisible (either permanently or * temporarily). */ @@ -225,6 +244,13 @@ public interface Bubbles { void onCurrentProfilesChanged(SparseArray<UserInfo> currentProfiles); /** + * Called when a user is removed. + * + * @param removedUserId the id of the removed user. + */ + void onUserRemoved(int removedUserId); + + /** * Called when config changed. * * @param newConfig the new config. @@ -232,7 +258,7 @@ public interface Bubbles { void onConfigChanged(Configuration newConfig); /** Description of current bubble state. */ - void dump(FileDescriptor fd, PrintWriter pw, String[] args); + void dump(PrintWriter pw, String[] args); /** Listener to find out about stack expansion / collapse events. */ interface BubbleExpandListener { @@ -245,10 +271,10 @@ public interface Bubbles { void onBubbleExpandChanged(boolean isExpanding, String key); } - /** Listener to be notified when the flags for notification or bubble suppression changes.*/ - interface SuppressionChangedListener { - /** Called when the notification suppression state of a bubble changes. */ - void onBubbleNotificationSuppressionChange(Bubble bubble); + /** Listener to be notified when the flags on BubbleMetadata have changed. */ + interface BubbleMetadataFlagListener { + /** Called when the flags on BubbleMetadata have changed for the provided bubble. */ + void onBubbleMetadataFlagChanged(Bubble bubble); } /** Listener to be notified when a pending intent has been canceled for a bubble. */ 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 74672a336161..063dac3d4109 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 @@ -16,11 +16,16 @@ package com.android.wm.shell.bubbles +import android.animation.ObjectAnimator import android.content.Context -import android.graphics.drawable.TransitionDrawable +import android.graphics.Color +import android.graphics.drawable.GradientDrawable +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.widget.FrameLayout import androidx.dynamicanimation.animation.DynamicAnimation import androidx.dynamicanimation.animation.SpringForce.DAMPING_RATIO_LOW_BOUNCY @@ -28,8 +33,6 @@ import androidx.dynamicanimation.animation.SpringForce.STIFFNESS_LOW import com.android.wm.shell.R import com.android.wm.shell.animation.PhysicsAnimator import com.android.wm.shell.common.DismissCircleView -import android.view.WindowInsets -import android.view.WindowManager /* * View that handles interactions between DismissCircleView and BubbleStackView. @@ -41,9 +44,21 @@ class DismissView(context: Context) : FrameLayout(context) { private val animator = PhysicsAnimator.getInstance(circle) private val spring = PhysicsAnimator.SpringConfig(STIFFNESS_LOW, DAMPING_RATIO_LOW_BOUNCY) - private val DISMISS_SCRIM_FADE_MS = 200 + private val DISMISS_SCRIM_FADE_MS = 200L private var wm: WindowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager + private var gradientDrawable = createGradient() + + private val GRADIENT_ALPHA: IntProperty<GradientDrawable> = + object : IntProperty<GradientDrawable>("alpha") { + override fun setValue(d: GradientDrawable, percent: Int) { + d.alpha = percent + } + override fun get(d: GradientDrawable): Int { + return d.alpha + } + } + init { setLayoutParams(LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, @@ -53,8 +68,7 @@ class DismissView(context: Context) : FrameLayout(context) { setClipToPadding(false) setClipChildren(false) setVisibility(View.INVISIBLE) - setBackgroundResource( - R.drawable.floating_dismiss_gradient_transition) + setBackgroundDrawable(gradientDrawable) val targetSize: Int = resources.getDimensionPixelSize(R.dimen.dismiss_circle_size) addView(circle, LayoutParams(targetSize, targetSize, @@ -71,7 +85,11 @@ class DismissView(context: Context) : FrameLayout(context) { if (isShowing) return isShowing = true setVisibility(View.VISIBLE) - (getBackground() as TransitionDrawable).startTransition(DISMISS_SCRIM_FADE_MS) + val alphaAnim = ObjectAnimator.ofInt(gradientDrawable, GRADIENT_ALPHA, + gradientDrawable.alpha, 255) + alphaAnim.setDuration(DISMISS_SCRIM_FADE_MS) + alphaAnim.start() + animator.cancel() animator .spring(DynamicAnimation.TRANSLATION_Y, 0f, spring) @@ -85,7 +103,10 @@ class DismissView(context: Context) : FrameLayout(context) { fun hide() { if (!isShowing) return isShowing = false - (getBackground() as TransitionDrawable).reverseTransition(DISMISS_SCRIM_FADE_MS) + val alphaAnim = ObjectAnimator.ofInt(gradientDrawable, GRADIENT_ALPHA, + gradientDrawable.alpha, 0) + alphaAnim.setDuration(DISMISS_SCRIM_FADE_MS) + alphaAnim.start() animator .spring(DynamicAnimation.TRANSLATION_Y, height.toFloat(), spring) @@ -93,6 +114,13 @@ class DismissView(context: Context) : FrameLayout(context) { .start() } + /** + * Cancels the animator for the dismiss target. + */ + fun cancelAnimators() { + animator.cancel() + } + fun updateResources() { updatePadding() layoutParams.height = resources.getDimensionPixelSize( @@ -104,6 +132,20 @@ class DismissView(context: Context) : FrameLayout(context) { circle.requestLayout() } + private fun createGradient(): GradientDrawable { + val gradientColor = context.resources.getColor(android.R.color.system_neutral1_900) + val alpha = 0.7f * 255 + val gradientColorWithAlpha = Color.argb(alpha.toInt(), + Color.red(gradientColor), + Color.green(gradientColor), + Color.blue(gradientColor)) + val gd = GradientDrawable( + GradientDrawable.Orientation.BOTTOM_TOP, + intArrayOf(gradientColorWithAlpha, Color.TRANSPARENT)) + gd.setAlpha(0) + return gd + } + private fun updatePadding() { val insets: WindowInsets = wm.getCurrentWindowMetrics().getWindowInsets() val navInset = insets.getInsetsIgnoringVisibility( diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/ManageEducationView.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/ManageEducationView.kt index eb4737ac6c63..e95e8e5cdaea 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/ManageEducationView.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/ManageEducationView.kt @@ -101,16 +101,23 @@ class ManageEducationView constructor(context: Context, positioner: BubblePositi bubbleExpandedView = expandedView expandedView.taskView?.setObscuredTouchRect(Rect(positioner.screenRect)) - layoutParams.width = if (positioner.isLargeScreen) - context.resources.getDimensionPixelSize( - R.dimen.bubbles_user_education_width_large_screen) + layoutParams.width = if (positioner.isLargeScreen || positioner.isLandscape) + context.resources.getDimensionPixelSize(R.dimen.bubbles_user_education_width) else ViewGroup.LayoutParams.MATCH_PARENT alpha = 0f visibility = View.VISIBLE expandedView.getManageButtonBoundsOnScreen(realManageButtonRect) - manageView.setPadding(realManageButtonRect.left - expandedView.manageButtonMargin, - manageView.paddingTop, manageView.paddingRight, manageView.paddingBottom) + val isRTL = mContext.resources.configuration.layoutDirection == LAYOUT_DIRECTION_RTL + if (isRTL) { + val rightPadding = positioner.screenRect.right - realManageButtonRect.right - + expandedView.manageButtonMargin + manageView.setPadding(manageView.paddingLeft, manageView.paddingTop, + rightPadding, manageView.paddingBottom) + } else { + manageView.setPadding(realManageButtonRect.left - expandedView.manageButtonMargin, + manageView.paddingTop, manageView.paddingRight, manageView.paddingBottom) + } post { manageButton .setOnClickListener { @@ -123,7 +130,11 @@ class ManageEducationView constructor(context: Context, positioner: BubblePositi val offsetViewBounds = Rect() manageButton.getDrawingRect(offsetViewBounds) manageView.offsetDescendantRectToMyCoords(manageButton, offsetViewBounds) - translationX = 0f + if (isRTL && (positioner.isLargeScreen || positioner.isLandscape)) { + translationX = (positioner.screenRect.right - width).toFloat() + } else { + translationX = 0f + } translationY = (realManageButtonRect.top - offsetViewBounds.top).toFloat() bringToFront() animate() diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/StackEducationView.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/StackEducationView.kt index 3846de73842d..627273f093f3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/StackEducationView.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/StackEducationView.kt @@ -122,29 +122,36 @@ class StackEducationView constructor( * If necessary, shows the user education view for the bubble stack. This appears the first * time a user taps on a bubble. * - * @return true if user education was shown, false otherwise. + * @return true if user education was shown and wasn't showing before, false otherwise. */ fun show(stackPosition: PointF): Boolean { isHiding = false if (visibility == VISIBLE) return false controller.updateWindowFlagsForBackpress(true /* interceptBack */) - layoutParams.width = if (positioner.isLargeScreen) - context.resources.getDimensionPixelSize( - R.dimen.bubbles_user_education_width_large_screen) + layoutParams.width = if (positioner.isLargeScreen || positioner.isLandscape) + context.resources.getDimensionPixelSize(R.dimen.bubbles_user_education_width) else ViewGroup.LayoutParams.MATCH_PARENT + val stackPadding = context.resources.getDimensionPixelSize( + R.dimen.bubble_user_education_stack_padding) setAlpha(0f) setVisibility(View.VISIBLE) post { requestFocus() with(view) { if (resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_LTR) { - setPadding(positioner.bubbleSize + paddingRight, paddingTop, paddingRight, + setPadding(positioner.bubbleSize + stackPadding, paddingTop, paddingRight, paddingBottom) } else { - setPadding(paddingLeft, paddingTop, positioner.bubbleSize + paddingLeft, + setPadding(paddingLeft, paddingTop, positioner.bubbleSize + stackPadding, paddingBottom) + if (positioner.isLargeScreen || positioner.isLandscape) { + translationX = (positioner.screenRect.right - width - stackPadding) + .toFloat() + } else { + translationX = 0f + } } translationY = stackPosition.y + positioner.bubbleSize / 2 - getHeight() / 2 } 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 19d513f81cab..573f42468512 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 @@ -37,7 +37,6 @@ import com.android.wm.shell.common.magnetictarget.MagnetizedObject; import com.google.android.collect.Sets; -import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.Set; @@ -432,7 +431,7 @@ public class ExpandedAnimationController } /** Description of current animation controller state. */ - public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + public void dump(PrintWriter pw, String[] args) { 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/PhysicsAnimationLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/PhysicsAnimationLayout.java index 4ec2c8d4d362..55052e614458 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/PhysicsAnimationLayout.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/PhysicsAnimationLayout.java @@ -364,6 +364,11 @@ public class PhysicsAnimationLayout extends FrameLayout { final int oldIndex = indexOfChild(view); super.removeView(view); + if (view.getParent() != null) { + // View still has a parent. This could have been added as a transient view. + // Remove it from transient views. + super.removeTransientView(view); + } addViewInternal(view, index, view.getLayoutParams(), true /* isReorder */); if (mController != null) { 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 60b64333114e..0a1b4d70fb2b 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 @@ -46,7 +46,6 @@ import com.android.wm.shell.common.magnetictarget.MagnetizedObject; import com.google.android.collect.Sets; -import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.HashMap; import java.util.List; @@ -186,8 +185,6 @@ public class StackAnimationController extends * stack goes offscreen intentionally. */ private int mBubblePaddingTop; - /** How far offscreen the stack rests. */ - private int mBubbleOffscreen; /** Contains display size, orientation, and inset information. */ private BubblePositioner mPositioner; @@ -213,7 +210,8 @@ public class StackAnimationController extends public Rect getAllowedFloatingBoundsRegion() { final Rect floatingBounds = getFloatingBoundsOnScreen(); final Rect allowableStackArea = new Rect(); - getAllowableStackPositionRegion().roundOut(allowableStackArea); + mPositioner.getAllowableStackPositionRegion(getBubbleCount()) + .roundOut(allowableStackArea); allowableStackArea.right += floatingBounds.width(); allowableStackArea.bottom += floatingBounds.height(); return allowableStackArea; @@ -350,7 +348,7 @@ public class StackAnimationController extends ? velX < ESCAPE_VELOCITY : velX < -ESCAPE_VELOCITY; - final RectF stackBounds = getAllowableStackPositionRegion(); + final RectF stackBounds = mPositioner.getAllowableStackPositionRegion(getBubbleCount()); // Target X translation (either the left or right side of the screen). final float destinationRelativeX = stackShouldFlingLeft @@ -426,14 +424,14 @@ public class StackAnimationController extends } final PointF stackPos = getStackPosition(); final boolean onLeft = mLayout.isFirstChildXLeftOfCenter(stackPos.x); - final RectF bounds = getAllowableStackPositionRegion(); + final RectF bounds = mPositioner.getAllowableStackPositionRegion(getBubbleCount()); stackPos.x = onLeft ? bounds.left : bounds.right; return stackPos; } /** Description of current animation controller state. */ - public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + public void dump(PrintWriter pw, String[] args) { pw.println("StackAnimationController state:"); pw.print(" isActive: "); pw.println(isActiveController()); pw.print(" restingStackPos: "); @@ -465,7 +463,7 @@ public class StackAnimationController extends StackPositionProperty firstBubbleProperty = new StackPositionProperty(property); final float currentValue = firstBubbleProperty.getValue(this); - final RectF bounds = getAllowableStackPositionRegion(); + final RectF bounds = mPositioner.getAllowableStackPositionRegion(getBubbleCount()); final float min = property.equals(DynamicAnimation.TRANSLATION_X) ? bounds.left @@ -526,7 +524,8 @@ public class StackAnimationController extends * of the stack if it's not moving). */ public float animateForImeVisibility(boolean imeVisible) { - final float maxBubbleY = getAllowableStackPositionRegion().bottom; + final float maxBubbleY = mPositioner.getAllowableStackPositionRegion( + getBubbleCount()).bottom; float destinationY = UNSET; if (imeVisible) { @@ -568,25 +567,6 @@ public class StackAnimationController extends mFloatingContentCoordinator.onContentMoved(mStackFloatingContent); } - /** - * 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. - * While the stack position is not allowed to rest outside of these bounds, it can temporarily - * be animated or dragged beyond them. - */ - public RectF getAllowableStackPositionRegion() { - final RectF allowableRegion = new RectF(mPositioner.getAvailableRect()); - final int imeHeight = mPositioner.getImeHeight(); - final float bottomPadding = getBubbleCount() > 1 - ? mBubblePaddingTop + mStackOffset - : mBubblePaddingTop; - allowableRegion.left -= mBubbleOffscreen; - allowableRegion.top += mBubblePaddingTop; - allowableRegion.right += mBubbleOffscreen - mBubbleSize; - allowableRegion.bottom -= imeHeight + bottomPadding + mBubbleSize; - return allowableRegion; - } - /** Moves the stack in response to a touch event. */ public void moveStackFromTouch(float x, float y) { // Begin the spring-to-touch catch up animation if needed. @@ -750,6 +730,12 @@ public class StackAnimationController extends // Otherwise, animate the bubble in if it's the newest bubble. If we're adding a bubble // to the back of the stack, it'll be largely invisible so don't bother animating it in. animateInBubble(child, index); + } else { + // We are not animating the bubble in. Make sure it has the right alpha and scale values + // in case this view was previously removed and is being re-added. + child.setAlpha(1f); + child.setScaleX(1f); + child.setScaleY(1f); } } @@ -785,23 +771,24 @@ public class StackAnimationController extends } }; + boolean swapped = false; for (int newIndex = 0; newIndex < bubbleViews.size(); newIndex++) { View view = bubbleViews.get(newIndex); final int oldIndex = mLayout.indexOfChild(view); - animateSwap(view, oldIndex, newIndex, updateAllIcons, after); + swapped |= animateSwap(view, oldIndex, newIndex, updateAllIcons, after); + } + if (!swapped) { + // All bubbles were at the right position. Make sure badges and z order is correct. + updateAllIcons.run(); } } - private void animateSwap(View view, int oldIndex, int newIndex, + private boolean animateSwap(View view, int oldIndex, int newIndex, Runnable updateAllIcons, Runnable finishReorder) { if (newIndex == oldIndex) { - // Add new bubble to index 0; move existing bubbles down - updateBadgesAndZOrder(view, newIndex); - if (newIndex == 0) { - animateInBubble(view, newIndex); - } else { - moveToFinalIndex(view, newIndex, finishReorder); - } + // View order did not change. Make sure position is correct. + moveToFinalIndex(view, newIndex, finishReorder); + return false; } else { // Reorder existing bubbles if (newIndex == 0) { @@ -809,6 +796,7 @@ public class StackAnimationController extends } else { moveToFinalIndex(view, newIndex, finishReorder); } + return true; } } @@ -854,13 +842,12 @@ public class StackAnimationController extends @Override void onActiveControllerForLayout(PhysicsAnimationLayout layout) { Resources res = layout.getResources(); - mStackOffset = res.getDimensionPixelSize(R.dimen.bubble_stack_offset); + mStackOffset = mPositioner.getStackOffset(); mSwapAnimationOffset = res.getDimensionPixelSize(R.dimen.bubble_swap_animation_offset); mMaxBubbles = res.getInteger(R.integer.bubbles_max_rendered); mElevation = res.getDimensionPixelSize(R.dimen.bubble_elevation); mBubbleSize = mPositioner.getBubbleSize(); - mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top); - mBubbleOffscreen = res.getDimensionPixelSize(R.dimen.bubble_stack_offscreen); + mBubblePaddingTop = mPositioner.getBubblePaddingTop(); } /** @@ -951,7 +938,8 @@ public class StackAnimationController extends } public void setStackPosition(BubbleStackView.RelativeStackPosition position) { - setStackPosition(position.getAbsolutePositionInRegion(getAllowableStackPositionRegion())); + setStackPosition(position.getAbsolutePositionInRegion( + mPositioner.getAllowableStackPositionRegion(getBubbleCount()))); } private boolean isStackPositionSet() { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/storage/BubbleVolatileRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/storage/BubbleVolatileRepository.kt index a5267d8be9fe..1eee0291cb26 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/storage/BubbleVolatileRepository.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/storage/BubbleVolatileRepository.kt @@ -15,6 +15,7 @@ */ package com.android.wm.shell.bubbles.storage +import android.annotation.UserIdInt import android.content.pm.LauncherApps import android.os.UserHandle import android.util.SparseArray @@ -95,10 +96,68 @@ class BubbleVolatileRepository(private val launcherApps: LauncherApps) { } @Synchronized - fun removeBubbles(userId: Int, bubbles: List<BubbleEntity>) = + fun removeBubbles(@UserIdInt userId: Int, bubbles: List<BubbleEntity>) = uncache(bubbles.filter { b: BubbleEntity -> getEntities(userId).removeIf { e: BubbleEntity -> b.key == e.key } }) + /** + * Removes all the bubbles associated with the provided userId. + * @return whether bubbles were removed or not. + */ + @Synchronized + fun removeBubblesForUser(@UserIdInt userId: Int, @UserIdInt parentUserId: Int): Boolean { + if (parentUserId != -1) { + return removeBubblesForUserWithParent(userId, parentUserId) + } else { + val entities = entitiesByUser.get(userId) + entitiesByUser.remove(userId) + return entities != null + } + } + + /** + * Removes all the bubbles associated with the provided userId when that userId is part of + * a profile (e.g. managed account). + * + * @return whether bubbles were removed or not. + */ + @Synchronized + private fun removeBubblesForUserWithParent( + @UserIdInt userId: Int, + @UserIdInt parentUserId: Int + ): Boolean { + if (entitiesByUser.get(parentUserId) != null) { + return entitiesByUser.get(parentUserId).removeIf { + b: BubbleEntity -> b.userId == userId } + } + return false + } + + /** + * Goes through all the persisted bubbles and removes them if the user is not in the active + * list of users. + * + * @return whether the list of bubbles changed or not (i.e. was a removal made). + */ + @Synchronized + fun sanitizeBubbles(activeUsers: List<Int>): Boolean { + for (i in 0 until entitiesByUser.size()) { + // First check if the user is a parent / top-level user + val parentUserId = entitiesByUser.keyAt(i) + if (!activeUsers.contains(parentUserId)) { + entitiesByUser.remove(parentUserId) + return true + } else if (entitiesByUser.get(parentUserId) != null) { + // Then check if each of the bubbles in the top-level user, still has a valid user + // as it could belong to a profile and have a different id from the parent. + return entitiesByUser.get(parentUserId).removeIf { b: BubbleEntity -> + !activeUsers.contains(b.userId) + } + } + } + return false + } + private fun cache(bubbles: List<BubbleEntity>) { bubbles.groupBy { ShortcutKey(it.userId, it.packageName) }.forEach { (key, bubbles) -> launcherApps.cacheShortcuts(key.pkg, bubbles.map { it.shortcutId }, 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 ffda1f92ec90..c32733d4f73c 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 @@ -27,7 +27,6 @@ import androidx.annotation.BinderThread; import com.android.wm.shell.common.annotations.ShellMainThread; -import java.util.ArrayList; import java.util.concurrent.CopyOnWriteArrayList; /** @@ -71,12 +70,18 @@ public class DisplayChangeController { mRotationListener.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); + } + } + private void onRotateDisplay(int displayId, final int fromRotation, final int toRotation, IDisplayWindowRotationCallback callback) { WindowContainerTransaction t = new WindowContainerTransaction(); - for (OnDisplayChangingListener c : mRotationListener) { - c.onRotateDisplay(displayId, fromRotation, toRotation, t); - } + dispatchOnRotateDisplay(t, displayId, fromRotation, toRotation); try { callback.continueRotateDisplay(toRotation, t); } catch (RemoteException e) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java index a1fb658ccb9d..4ba32e93fb3d 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 @@ -19,8 +19,10 @@ package com.android.wm.shell.common; import android.annotation.Nullable; import android.content.Context; import android.content.res.Configuration; +import android.graphics.Rect; import android.hardware.display.DisplayManager; import android.os.RemoteException; +import android.util.ArraySet; import android.util.Slog; import android.util.SparseArray; import android.view.Display; @@ -34,6 +36,8 @@ import com.android.wm.shell.common.DisplayChangeController.OnDisplayChangingList import com.android.wm.shell.common.annotations.ShellMainThread; import java.util.ArrayList; +import java.util.List; +import java.util.Set; /** * This module deals with display rotations coming from WM. When WM starts a rotation: after it has @@ -76,6 +80,11 @@ public class DisplayController { } } + /** Get the DisplayChangeController. */ + public DisplayChangeController getChangeController() { + return mChangeController; + } + /** * Gets a display by id from DisplayManager. */ @@ -101,6 +110,14 @@ public class DisplayController { } /** + * Get the InsetsState of a display. + */ + public InsetsState getInsetsState(int displayId) { + final DisplayRecord r = mDisplays.get(displayId); + return r != null ? r.mInsetsState : null; + } + + /** * Updates the insets for a given display. */ public void updateDisplayInsets(int displayId, InsetsState state) { @@ -238,6 +255,21 @@ public class DisplayController { } } + private void onKeepClearAreasChanged(int displayId, Set<Rect> restricted, + Set<Rect> unrestricted) { + synchronized (mDisplays) { + if (mDisplays.get(displayId) == null || getDisplay(displayId) == null) { + Slog.w(TAG, "Skipping onKeepClearAreasChanged on unknown" + + " display, displayId=" + displayId); + return; + } + for (int i = mDisplayChangedListeners.size() - 1; i >= 0; --i) { + mDisplayChangedListeners.get(i) + .onKeepClearAreasChanged(displayId, restricted, unrestricted); + } + } + } + private static class DisplayRecord { private int mDisplayId; private Context mContext; @@ -296,6 +328,15 @@ public class DisplayController { DisplayController.this.onFixedRotationFinished(displayId); }); } + + @Override + public void onKeepClearAreasChanged(int displayId, List<Rect> restricted, + List<Rect> unrestricted) { + mMainExecutor.execute(() -> { + DisplayController.this.onKeepClearAreasChanged(displayId, + new ArraySet<>(restricted), new ArraySet<>(unrestricted)); + }); + } } /** @@ -330,5 +371,11 @@ public class DisplayController { * Called when fixed rotation on a display is finished. */ default void onFixedRotationFinished(int displayId) {} + + /** + * Called when keep-clear areas on a display have changed. + */ + default void onKeepClearAreasChanged(int displayId, Set<Rect> restricted, + Set<Rect> unrestricted) {} } } 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 a7052bc49699..6a2acf438302 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 @@ -40,7 +40,6 @@ import android.view.WindowInsets; import android.view.animation.Interpolator; import android.view.animation.PathInterpolator; -import androidx.annotation.BinderThread; import androidx.annotation.VisibleForTesting; import com.android.internal.view.IInputMethodManager; @@ -325,7 +324,8 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged } @Override - public void topFocusedWindowChanged(String packageName) { + public void topFocusedWindowChanged(String packageName, + InsetsVisibilities requestedVisibilities) { // Do nothing } @@ -498,6 +498,11 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged dispatchVisibilityChanged(mDisplayId, isShowing); } } + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + public InsetsSourceControl getImeSourceControl() { + return mImeSourceControl; + } } void removeImeSurface() { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayInsetsController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayInsetsController.java index 565f1481233c..b6705446674a 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 @@ -23,6 +23,7 @@ import android.view.IDisplayWindowInsetsController; import android.view.IWindowManager; import android.view.InsetsSourceControl; import android.view.InsetsState; +import android.view.InsetsVisibilities; import androidx.annotation.BinderThread; @@ -170,13 +171,14 @@ public class DisplayInsetsController implements DisplayController.OnDisplaysChan } } - private void topFocusedWindowChanged(String packageName) { + private void topFocusedWindowChanged(String packageName, + InsetsVisibilities requestedVisibilities) { CopyOnWriteArrayList<OnInsetsChangedListener> listeners = mListeners.get(mDisplayId); if (listeners == null) { return; } for (OnInsetsChangedListener listener : listeners) { - listener.topFocusedWindowChanged(packageName); + listener.topFocusedWindowChanged(packageName, requestedVisibilities); } } @@ -184,9 +186,10 @@ public class DisplayInsetsController implements DisplayController.OnDisplaysChan private class DisplayWindowInsetsControllerImpl extends IDisplayWindowInsetsController.Stub { @Override - public void topFocusedWindowChanged(String packageName) throws RemoteException { + public void topFocusedWindowChanged(String packageName, + InsetsVisibilities requestedVisibilities) throws RemoteException { mMainExecutor.execute(() -> { - PerDisplay.this.topFocusedWindowChanged(packageName); + PerDisplay.this.topFocusedWindowChanged(packageName, requestedVisibilities); }); } @@ -231,9 +234,11 @@ 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: Passes the top package name + * @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. */ - default void topFocusedWindowChanged(String packageName) {} + default void topFocusedWindowChanged(String packageName, + InsetsVisibilities requestedVisibilities) {} /** * Called when the window insets configuration has changed. 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 6f4e22fa8a04..47f1e2e18255 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 @@ -32,6 +32,7 @@ import static android.view.Surface.ROTATION_90; import android.annotation.IntDef; import android.annotation.NonNull; +import android.annotation.Nullable; import android.content.ContentResolver; import android.content.Context; import android.content.res.Resources; @@ -336,6 +337,12 @@ public class DisplayLayout { return navigationBarPosition(res, mWidth, mHeight, mRotation); } + /** @return {@link DisplayCutout} instance. */ + @Nullable + public DisplayCutout getDisplayCutout() { + return mCutout; + } + /** * Calculates the stable insets if we already have the non-decor insets. */ @@ -416,8 +423,9 @@ public class DisplayLayout { } final DisplayCutout.CutoutPathParserInfo info = cutout.getCutoutPathParserInfo(); final DisplayCutout.CutoutPathParserInfo newInfo = new DisplayCutout.CutoutPathParserInfo( - info.getDisplayWidth(), info.getDisplayHeight(), info.getDensity(), - info.getCutoutSpec(), rotation, info.getScale()); + info.getDisplayWidth(), info.getDisplayHeight(), info.getPhysicalDisplayWidth(), + info.getPhysicalDisplayHeight(), info.getDensity(), info.getCutoutSpec(), rotation, + info.getScale(), info.getPhysicalPixelDisplaySizeRatio()); return computeSafeInsets( DisplayCutout.constructDisplayCutout(newBounds, waterfallInsets, newInfo), rotated ? displayHeight : displayWidth, 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 new file mode 100644 index 000000000000..fd3aa05cfc06 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/InteractionJankMonitorUtils.java @@ -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. + */ + +package com.android.wm.shell.common; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.text.TextUtils; +import android.view.View; + +import com.android.internal.jank.InteractionJankMonitor; + +/** Utils class for simplfy InteractionJank trancing call */ +public class InteractionJankMonitorUtils { + + /** + * Begin a trace session. + * + * @param cujType the specific {@link InteractionJankMonitor.CujType}. + * @param view the view to trace + * @param tag the tag to distinguish different flow of same type CUJ. + */ + public static void beginTracing(@InteractionJankMonitor.CujType int cujType, + @NonNull View view, @Nullable String tag) { + final InteractionJankMonitor.Configuration.Builder builder = + InteractionJankMonitor.Configuration.Builder.withView(cujType, view); + if (!TextUtils.isEmpty(tag)) { + builder.setTag(tag); + } + InteractionJankMonitor.getInstance().begin(builder); + } + + /** + * End a trace session. + * + * @param cujType the specific {@link InteractionJankMonitor.CujType}. + */ + public static void endTracing(@InteractionJankMonitor.CujType int cujType) { + InteractionJankMonitor.getInstance().end(cujType); + } + + /** + * Cancel the trace session. + * + * @param cujType the specific {@link InteractionJankMonitor.CujType}. + */ + public static void cancelTracing(@InteractionJankMonitor.CujType int cujType) { + InteractionJankMonitor.getInstance().cancel(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 eea6e3cb35db..c4bd73ba1b4a 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 @@ -16,7 +16,6 @@ package com.android.wm.shell.common; -import android.graphics.GraphicBuffer; import android.graphics.PixelFormat; import android.graphics.Rect; import android.view.SurfaceControl; @@ -63,8 +62,6 @@ public class ScreenshotUtils { if (buffer == null || buffer.getHardwareBuffer() == null) { return; } - final GraphicBuffer graphicBuffer = GraphicBuffer.createFromHardwareBuffer( - buffer.getHardwareBuffer()); mScreenshot = new SurfaceControl.Builder() .setName("ScreenshotUtils screenshot") .setFormat(PixelFormat.TRANSLUCENT) @@ -73,7 +70,7 @@ public class ScreenshotUtils { .setBLASTLayer() .build(); - mTransaction.setBuffer(mScreenshot, graphicBuffer); + mTransaction.setBuffer(mScreenshot, buffer.getHardwareBuffer()); mTransaction.setColorSpace(mScreenshot, buffer.getColorSpace()); mTransaction.reparent(mScreenshot, mSurfaceControl); mTransaction.setLayer(mScreenshot, mLayer); 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 97c89d042be0..d5875c03ccd2 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 @@ -21,7 +21,6 @@ import static android.view.WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED; import android.annotation.NonNull; import android.content.Context; import android.content.res.Configuration; -import android.graphics.Point; import android.graphics.Region; import android.os.Bundle; import android.os.IBinder; @@ -192,6 +191,19 @@ public class SystemWindows { return null; } + /** + * Gets a token associated with the view that can be used to grant the view focus. + */ + public IBinder getFocusGrantToken(View view) { + SurfaceControlViewHost root = mViewRoots.get(view); + if (root == null) { + Slog.e(TAG, "Couldn't get focus grant token since view does not exist in " + + "SystemWindow:" + view); + return null; + } + return root.getFocusGrantToken(); + } + private class PerDisplay { final int mDisplayId; private final SparseArray<SysUiWindowManager> mWwms = new SparseArray<>(); @@ -331,18 +343,13 @@ public class SystemWindows { @Override public void resized(ClientWindowFrames frames, boolean reportDraw, - MergedConfiguration newMergedConfiguration, boolean forceLayout, - boolean alwaysConsumeSystemBars, int displayId) {} - - @Override - public void locationInParentDisplayChanged(Point offset) {} - - @Override - public void insetsChanged(InsetsState insetsState, boolean willMove, boolean willResize) {} + MergedConfiguration newMergedConfiguration, InsetsState insetsState, + boolean forceLayout, boolean alwaysConsumeSystemBars, int displayId, int syncSeqId, + int resizeMode) {} @Override public void insetsControlChanged(InsetsState insetsState, - InsetsSourceControl[] activeControls, boolean willMove, boolean willResize) {} + InsetsSourceControl[] activeControls) {} @Override public void showInsets(int types, boolean fromIme) {} @@ -360,9 +367,6 @@ public class SystemWindows { public void dispatchGetNewSurface() {} @Override - public void windowFocusChanged(boolean hasFocus, boolean inTouchMode) {} - - @Override public void executeCommand(String command, String parameters, ParcelFileDescriptor out) {} @Override diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/TaskStackListenerCallback.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/TaskStackListenerCallback.java index 59374a6069c8..0f9260c9deaa 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/TaskStackListenerCallback.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/TaskStackListenerCallback.java @@ -38,7 +38,7 @@ public interface TaskStackListenerCallback { default void onTaskStackChanged() { } - default void onTaskProfileLocked(int taskId, int userId) { } + default void onTaskProfileLocked(RunningTaskInfo taskInfo) { } default void onTaskDisplayChanged(int taskId, int newDisplayId) { } @@ -54,7 +54,13 @@ public interface TaskStackListenerCallback { default void onTaskDescriptionChanged(RunningTaskInfo taskInfo) { } - default void onTaskSnapshotChanged(int taskId, TaskSnapshot snapshot) { } + /** + * @return whether the snapshot is consumed and the lifecycle of the snapshot extends beyond + * the lifecycle of this callback. + */ + default boolean onTaskSnapshotChanged(int taskId, TaskSnapshot snapshot) { + return false; + } default void onBackPressedOnTaskRoot(RunningTaskInfo taskInfo) { } 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 3b670057cb1a..9e0a48b13413 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 @@ -150,8 +150,8 @@ public class TaskStackListenerImpl extends TaskStackListener implements Handler. } @Override - public void onTaskProfileLocked(int taskId, int userId) { - mMainHandler.obtainMessage(ON_TASK_PROFILE_LOCKED, taskId, userId).sendToTarget(); + public void onTaskProfileLocked(ActivityManager.RunningTaskInfo taskInfo) { + mMainHandler.obtainMessage(ON_TASK_PROFILE_LOCKED, taskInfo).sendToTarget(); } @Override @@ -275,9 +275,15 @@ public class TaskStackListenerImpl extends TaskStackListener implements Handler. } case ON_TASK_SNAPSHOT_CHANGED: { Trace.beginSection("onTaskSnapshotChanged"); + final TaskSnapshot snapshot = (TaskSnapshot) msg.obj; + boolean snapshotConsumed = false; for (int i = mTaskStackListeners.size() - 1; i >= 0; i--) { - mTaskStackListeners.get(i).onTaskSnapshotChanged(msg.arg1, - (TaskSnapshot) msg.obj); + boolean consumed = mTaskStackListeners.get(i).onTaskSnapshotChanged( + msg.arg1, snapshot); + snapshotConsumed |= consumed; + } + if (!snapshotConsumed && snapshot.getHardwareBuffer() != null) { + snapshot.getHardwareBuffer().close(); } Trace.endSection(); break; @@ -341,8 +347,10 @@ public class TaskStackListenerImpl extends TaskStackListener implements Handler. break; } case ON_TASK_PROFILE_LOCKED: { + final ActivityManager.RunningTaskInfo + info = (ActivityManager.RunningTaskInfo) msg.obj; for (int i = mTaskStackListeners.size() - 1; i >= 0; i--) { - mTaskStackListeners.get(i).onTaskProfileLocked(msg.arg1, msg.arg2); + mTaskStackListeners.get(i).onTaskProfileLocked(info); } break; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ShellBackgroundThread.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ShellBackgroundThread.java new file mode 100644 index 000000000000..4cd3c903f2f8 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ShellBackgroundThread.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.common.annotations; + + +import java.lang.annotation.Documented; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import javax.inject.Qualifier; + +/** Annotates a method or qualifies a provider that runs on the shared background thread */ +@Documented +@Inherited +@Qualifier +@Retention(RetentionPolicy.RUNTIME) +public @interface ShellBackgroundThread { +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/magnetictarget/MagnetizedObject.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/magnetictarget/MagnetizedObject.kt index 9e012598554b..aac1d0626d30 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/magnetictarget/MagnetizedObject.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/magnetictarget/MagnetizedObject.kt @@ -17,13 +17,10 @@ package com.android.wm.shell.common.magnetictarget import android.annotation.SuppressLint import android.content.Context -import android.database.ContentObserver import android.graphics.PointF -import android.os.Handler -import android.os.UserHandle +import android.os.VibrationAttributes import android.os.VibrationEffect import android.os.Vibrator -import android.provider.Settings import android.view.MotionEvent import android.view.VelocityTracker import android.view.View @@ -147,6 +144,8 @@ abstract class MagnetizedObject<T : Any>( private val velocityTracker: VelocityTracker = VelocityTracker.obtain() private val vibrator: Vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator + private val vibrationAttributes: VibrationAttributes = VibrationAttributes.createForUsage( + VibrationAttributes.USAGE_TOUCH) private var touchDown = PointF() private var touchSlop = 0 @@ -268,10 +267,6 @@ abstract class MagnetizedObject<T : Any>( */ var flungIntoTargetSpringConfig = springConfig - init { - initHapticSettingObserver(context) - } - /** * Adds the provided MagneticTarget to this object. The object will now be attracted to the * target if it strays within its magnetic field or is flung towards it. @@ -468,8 +463,8 @@ abstract class MagnetizedObject<T : Any>( /** Plays the given vibration effect if haptics are enabled. */ @SuppressLint("MissingPermission") private fun vibrateIfEnabled(effectId: Int) { - if (hapticsEnabled && systemHapticsEnabled) { - vibrator.vibrate(VibrationEffect.createPredefined(effectId)) + if (hapticsEnabled) { + vibrator.vibrate(VibrationEffect.createPredefined(effectId), vibrationAttributes) } } @@ -622,44 +617,6 @@ abstract class MagnetizedObject<T : Any>( } companion object { - - /** - * Whether the HAPTIC_FEEDBACK_ENABLED setting is true. - * - * We put it in the companion object because we need to register a settings observer and - * [MagnetizedObject] doesn't have an obvious lifecycle so we don't have a good time to - * remove that observer. Since this settings is shared among all instances we just let all - * instances read from this value. - */ - private var systemHapticsEnabled = false - private var hapticSettingObserverInitialized = false - - private fun initHapticSettingObserver(context: Context) { - if (hapticSettingObserverInitialized) { - return - } - - val hapticSettingObserver = - object : ContentObserver(Handler.getMain()) { - override fun onChange(selfChange: Boolean) { - systemHapticsEnabled = - Settings.System.getIntForUser( - context.contentResolver, - Settings.System.HAPTIC_FEEDBACK_ENABLED, - 0, - UserHandle.USER_CURRENT) != 0 - } - } - - context.contentResolver.registerContentObserver( - Settings.System.getUriFor(Settings.System.HAPTIC_FEEDBACK_ENABLED), - true /* notifyForDescendants */, hapticSettingObserver) - - // Trigger the observer once to initialize systemHapticsEnabled. - hapticSettingObserver.onChange(false /* selfChange */) - hapticSettingObserverInitialized = true - } - /** * Magnetizes the given view. Magnetized views are attracted to one or more magnetic * targets. Magnetic targets attract objects that are dragged near them, and hold them there 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 4b125b118ceb..6305959bb6ac 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 @@ -24,6 +24,7 @@ import android.animation.AnimatorListenerAdapter; import android.animation.ObjectAnimator; import android.content.Context; import android.graphics.Rect; +import android.os.Bundle; import android.util.AttributeSet; import android.util.Property; import android.view.GestureDetector; @@ -37,6 +38,8 @@ import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; import android.view.WindowManager; +import android.view.accessibility.AccessibilityNodeInfo; +import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; import android.widget.FrameLayout; import androidx.annotation.NonNull; @@ -80,7 +83,6 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { private final Rect mTempRect = new Rect(); private FrameLayout mDividerBar; - static final Property<DividerView, Integer> DIVIDER_HEIGHT_PROPERTY = new Property<DividerView, Integer>(Integer.class, "height") { @Override @@ -109,6 +111,74 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { } }; + private final AccessibilityDelegate mHandleDelegate = new AccessibilityDelegate() { + @Override + public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(host, info); + final DividerSnapAlgorithm snapAlgorithm = mSplitLayout.mDividerSnapAlgorithm; + if (isLandscape()) { + 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))); + } else { + 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))); + } + } + + @Override + public boolean performAccessibilityAction(@NonNull View host, int action, + @Nullable Bundle args) { + DividerSnapAlgorithm.SnapTarget nextTarget = null; + DividerSnapAlgorithm snapAlgorithm = mSplitLayout.mDividerSnapAlgorithm; + 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) { + mSplitLayout.snapToTarget(mSplitLayout.getDividePosition(), nextTarget); + return true; + } + return super.performAccessibilityAction(host, action, args); + } + }; + public DividerView(@NonNull Context context) { super(context); } @@ -179,6 +249,7 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { mDoubleTapDetector = new GestureDetector(getContext(), new DoubleTapListener()); mInteractive = true; setOnTouchListener(this); + mHandle.setAccessibilityDelegate(mHandleDelegate); } @Override 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 36e55bae18c3..484294ab295b 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 @@ -21,8 +21,10 @@ 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 static android.view.WindowManagerPolicyConstants.SPLIT_DIVIDER_LAYER; +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; import android.app.ActivityManager; import android.content.Context; import android.content.res.Configuration; @@ -42,6 +44,8 @@ import android.view.WindowlessWindowManager; import android.widget.FrameLayout; import android.widget.ImageView; +import androidx.annotation.NonNull; + import com.android.launcher3.icons.IconProvider; import com.android.wm.shell.R; import com.android.wm.shell.common.SurfaceUtils; @@ -52,6 +56,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 long FADE_DURATION = 133; private final IconProvider mIconProvider; private final SurfaceSession mSurfaceSession; @@ -63,6 +68,13 @@ public class SplitDecorManager extends WindowlessWindowManager { private SurfaceControl mIconLeash; private SurfaceControl mBackgroundLeash; + private boolean mShown; + private boolean mIsResizing; + private Rect mBounds = new Rect(); + private ValueAnimator mFadeAnimator; + + private int mIconSize; + public SplitDecorManager(Configuration configuration, IconProvider iconProvider, SurfaceSession surfaceSession) { super(configuration, null /* rootSurface */, null /* hostInputToken */); @@ -94,6 +106,7 @@ public class SplitDecorManager extends WindowlessWindowManager { mHostLeash = rootLeash; mViewHost = new SurfaceControlViewHost(context, context.getDisplay(), this); + mIconSize = context.getResources().getDimensionPixelSize(R.dimen.split_icon_size); final FrameLayout rootLayout = (FrameLayout) LayoutInflater.from(context) .inflate(R.layout.split_decor, null); mResizingIconView = rootLayout.findViewById(R.id.split_resizing_icon); @@ -113,6 +126,9 @@ 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 (mViewHost != null) { mViewHost.release(); mViewHost = null; @@ -128,6 +144,8 @@ public class SplitDecorManager extends WindowlessWindowManager { mHostLeash = null; mIcon = null; mResizingIconView = null; + mIsResizing = false; + mShown = false; } /** Showing resizing hint. */ @@ -137,39 +155,126 @@ public class SplitDecorManager extends WindowlessWindowManager { return; } + if (!mIsResizing) { + mIsResizing = true; + mBounds.set(newBounds); + } + + final boolean show = + newBounds.width() > mBounds.width() || newBounds.height() > mBounds.height(); + final boolean animate = show != mShown; + if (animate && 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(); + } + if (mBackgroundLeash == null) { mBackgroundLeash = SurfaceUtils.makeColorLayer(mHostLeash, RESIZING_BACKGROUND_SURFACE_NAME, mSurfaceSession); t.setColor(mBackgroundLeash, getResizingBackgroundColor(resizingTask)) - .setLayer(mBackgroundLeash, SPLIT_DIVIDER_LAYER - 1) - .show(mBackgroundLeash); + .setLayer(mBackgroundLeash, Integer.MAX_VALUE - 1); } if (mIcon == null && resizingTask.topActivityInfo != null) { - // TODO: add fade-in animation. mIcon = mIconProvider.getIcon(resizingTask.topActivityInfo); mResizingIconView.setImageDrawable(mIcon); mResizingIconView.setVisibility(View.VISIBLE); WindowManager.LayoutParams lp = (WindowManager.LayoutParams) mViewHost.getView().getLayoutParams(); - lp.width = mIcon.getIntrinsicWidth(); - lp.height = mIcon.getIntrinsicHeight(); + lp.width = mIconSize; + lp.height = mIconSize; mViewHost.relayout(lp); - t.show(mIconLeash).setLayer(mIconLeash, SPLIT_DIVIDER_LAYER); + t.setLayer(mIconLeash, Integer.MAX_VALUE); } - t.setPosition(mIconLeash, - newBounds.width() / 2 - mIcon.getIntrinsicWidth() / 2, - newBounds.height() / 2 - mIcon.getIntrinsicWidth() / 2); + newBounds.width() / 2 - mIconSize / 2, + newBounds.height() / 2 - mIconSize / 2); + + if (animate) { + startFadeAnimation(show, false /* isResized */); + mShown = show; + } } /** Stops showing resizing hint. */ - public void onResized(Rect newBounds, SurfaceControl.Transaction t) { + public void onResized(SurfaceControl.Transaction t) { if (mResizingIconView == null) { return; } + mIsResizing = false; + if (mFadeAnimator != null && mFadeAnimator.isRunning()) { + if (!mShown) { + // If fade-out animation is running, just add release callback to it. + SurfaceControl.Transaction finishT = new SurfaceControl.Transaction(); + mFadeAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + releaseDecor(finishT); + finishT.apply(); + finishT.close(); + } + }); + return; + } + + // If fade-in animation is running, cancel it and re-run fade-out one. + mFadeAnimator.cancel(); + } + if (mShown) { + startFadeAnimation(false /* show */, true /* isResized */); + } else { + // Decor surface is hidden so release it directly. + releaseDecor(t); + } + } + + private void startFadeAnimation(boolean show, boolean isResized) { + final SurfaceControl.Transaction animT = new SurfaceControl.Transaction(); + mFadeAnimator = ValueAnimator.ofFloat(0f, 1f); + mFadeAnimator.setDuration(FADE_DURATION); + mFadeAnimator.addUpdateListener(valueAnimator-> { + final float progress = (float) valueAnimator.getAnimatedValue(); + if (mBackgroundLeash != null) { + animT.setAlpha(mBackgroundLeash, show ? progress : 1 - progress); + } + if (mIconLeash != null) { + animT.setAlpha(mIconLeash, show ? progress : 1 - progress); + } + animT.apply(); + }); + mFadeAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(@NonNull Animator animation) { + if (show) { + animT.show(mBackgroundLeash).show(mIconLeash).apply(); + } + } + + @Override + public void onAnimationEnd(@NonNull Animator animation) { + if (!show) { + if (mBackgroundLeash != null) { + animT.hide(mBackgroundLeash); + } + if (mIconLeash != null) { + animT.hide(mIconLeash); + } + } + if (isResized) { + releaseDecor(animT); + } + animT.apply(); + animT.close(); + } + }); + mFadeAnimator.start(); + } + + /** Release or hide decor hint. */ + private void releaseDecor(SurfaceControl.Transaction t) { if (mBackgroundLeash != null) { t.remove(mBackgroundLeash); mBackgroundLeash = 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 ba343cb12085..c94455d9151a 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 @@ -23,7 +23,6 @@ import static android.view.WindowManager.DOCKED_INVALID; import static android.view.WindowManager.DOCKED_LEFT; import static android.view.WindowManager.DOCKED_RIGHT; import static android.view.WindowManager.DOCKED_TOP; -import static android.view.WindowManagerPolicyConstants.SPLIT_DIVIDER_LAYER; import static com.android.internal.policy.DividerSnapAlgorithm.SnapTarget.FLAG_DISMISS_END; import static com.android.internal.policy.DividerSnapAlgorithm.SnapTarget.FLAG_DISMISS_START; @@ -56,6 +55,7 @@ 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; @@ -63,6 +63,7 @@ import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.animation.Interpolators; import com.android.wm.shell.common.DisplayImeController; import com.android.wm.shell.common.DisplayInsetsController; +import com.android.wm.shell.common.InteractionJankMonitorUtils; import com.android.wm.shell.common.split.SplitScreenConstants.SplitPosition; import java.io.PrintWriter; @@ -73,6 +74,10 @@ import java.io.PrintWriter; */ public final class SplitLayout implements DisplayInsetsController.OnInsetsChangedListener { + public static final int PARALLAX_NONE = 0; + 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; @@ -88,24 +93,27 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange private final SplitWindowManager mSplitWindowManager; private final DisplayImeController mDisplayImeController; private final ImePositionProcessor mImePositionProcessor; - private final DismissingEffectPolicy mDismissingEffectPolicy; + private final ResizingEffectPolicy mSurfaceEffectPolicy; private final ShellTaskOrganizer mTaskOrganizer; private final InsetsState mInsetsState = new InsetsState(); private Context mContext; - private DividerSnapAlgorithm mDividerSnapAlgorithm; + @VisibleForTesting DividerSnapAlgorithm mDividerSnapAlgorithm; private WindowContainerToken mWinToken1; private WindowContainerToken mWinToken2; private int mDividePosition; private boolean mInitialized = false; + private boolean mFreezeDividerWindow = false; private int mOrientation; private int mRotation; + private final boolean mDimNonImeSide; + public SplitLayout(String windowName, Context context, Configuration configuration, SplitLayoutHandler splitLayoutHandler, SplitWindowManager.ParentContainerCallbacks parentContainerCallbacks, DisplayImeController displayImeController, ShellTaskOrganizer taskOrganizer, - boolean applyDismissingParallax) { + int parallaxType) { mContext = context.createConfigurationContext(configuration); mOrientation = configuration.orientation; mRotation = configuration.windowConfiguration.getRotation(); @@ -115,7 +123,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange parentContainerCallbacks); mTaskOrganizer = taskOrganizer; mImePositionProcessor = new ImePositionProcessor(mContext.getDisplayId()); - mDismissingEffectPolicy = new DismissingEffectPolicy(applyDismissingParallax); + mSurfaceEffectPolicy = new ResizingEffectPolicy(parallaxType); final Resources resources = context.getResources(); mDividerSize = resources.getDimensionPixelSize(R.dimen.split_divider_bar_width); @@ -123,8 +131,10 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange mDividerWindowWidth = mDividerSize + 2 * mDividerInsets; mRootBounds.set(configuration.windowConfiguration.getBounds()); - mDividerSnapAlgorithm = getSnapAlgorithm(mContext, mRootBounds); + mDividerSnapAlgorithm = getSnapAlgorithm(mContext, mRootBounds, null); resetDividerPosition(); + + mDimNonImeSide = resources.getBoolean(R.bool.config_dimNonImeAttachedSide); } private int getDividerInsets(Resources resources, Display display) { @@ -144,21 +154,42 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange return Math.max(dividerInset, radius); } - /** Gets bounds of the primary split. */ + /** Gets bounds of the primary split with screen based coordinate. */ public Rect getBounds1() { return new Rect(mBounds1); } - /** Gets bounds of the secondary split. */ + /** Gets bounds of the primary split with parent based coordinate. */ + public Rect getRefBounds1() { + Rect outBounds = getBounds1(); + outBounds.offset(-mRootBounds.left, -mRootBounds.top); + return outBounds; + } + + /** Gets bounds of the secondary split with screen based coordinate. */ public Rect getBounds2() { return new Rect(mBounds2); } - /** Gets bounds of divider window. */ + /** Gets bounds of the secondary split with parent based coordinate. */ + public Rect getRefBounds2() { + final Rect outBounds = getBounds2(); + outBounds.offset(-mRootBounds.left, -mRootBounds.top); + return outBounds; + } + + /** Gets bounds of divider window with screen based coordinate. */ public Rect getDividerBounds() { return new Rect(mDividerBounds); } + /** Gets bounds of divider window with parent based coordinate. */ + public Rect getRefDividerBounds() { + final Rect outBounds = getDividerBounds(); + outBounds.offset(-mRootBounds.left, -mRootBounds.top); + return outBounds; + } + /** Returns leash of the current divider bar. */ @Nullable public SurfaceControl getDividerLeash() { @@ -180,38 +211,50 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange /** Applies new configuration, returns {@code false} if there's no effect to the layout. */ public boolean updateConfiguration(Configuration configuration) { - boolean affectsLayout = false; + // 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(); - final int orientation = configuration.orientation; - - if (mOrientation == orientation - && rotation == mRotation - && mRootBounds.equals(rootBounds)) { + if (mRotation == rotation && mRootBounds.equals(rootBounds)) { return false; } - mContext = mContext.createConfigurationContext(configuration); - mSplitWindowManager.setConfiguration(configuration); - mOrientation = orientation; mTempRect.set(mRootBounds); mRootBounds.set(rootBounds); mRotation = rotation; - mDividerSnapAlgorithm = getSnapAlgorithm(mContext, mRootBounds); + mDividerSnapAlgorithm = getSnapAlgorithm(mContext, mRootBounds, null); initDividerPosition(mTempRect); - if (mInitialized) { - release(); - init(); + return true; + } + + /** Rotate the layout to specific rotation and calculate new bounds. The stable insets value + * should be calculated by display layout. */ + public void rotateTo(int newRotation, Rect stableInsets) { + final int rotationDelta = (newRotation - mRotation + 4) % 4; + final boolean changeOrient = (rotationDelta % 2) != 0; + + mRotation = newRotation; + Rect tmpRect = new Rect(mRootBounds); + if (changeOrient) { + tmpRect.set(mRootBounds.top, mRootBounds.left, mRootBounds.bottom, mRootBounds.right); } - return true; + // We only need new bounds here, other configuration should be update later. + mTempRect.set(mRootBounds); + mRootBounds.set(tmpRect); + mDividerSnapAlgorithm = getSnapAlgorithm(mContext, mRootBounds, stableInsets); + initDividerPosition(mTempRect); } private void initDividerPosition(Rect oldBounds) { @@ -248,7 +291,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange } DockedDividerUtils.sanitizeStackBounds(mBounds1, true /** topLeft */); DockedDividerUtils.sanitizeStackBounds(mBounds2, false /** topLeft */); - mDismissingEffectPolicy.applyDividerPosition(position, isLandscape); + mSurfaceEffectPolicy.applyDividerPosition(position, isLandscape); } /** Inflates {@link DividerView} on the root surface. */ @@ -260,20 +303,37 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange } /** Releases the surface holding the current {@link DividerView}. */ - public void release() { + public void release(SurfaceControl.Transaction t) { if (!mInitialized) return; mInitialized = false; - mSplitWindowManager.release(); + mSplitWindowManager.release(t); mDisplayImeController.removePositionProcessor(mImePositionProcessor); mImePositionProcessor.reset(); } + public void release() { + release(null /* t */); + } + + /** Releases and re-inflates {@link DividerView} on the root surface. */ + public void update(SurfaceControl.Transaction t) { + if (!mInitialized) return; + mSplitWindowManager.release(t); + mImePositionProcessor.reset(); + mSplitWindowManager.init(this, mInsetsState); + } + @Override public void insetsChanged(InsetsState insetsState) { mInsetsState.set(insetsState); if (!mInitialized) { return; } + if (mFreezeDividerWindow) { + // DO NOT change its layout before transition actually run because it might cause + // flicker. + return; + } mSplitWindowManager.onInsetsChanged(insetsState); } @@ -285,6 +345,10 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange } } + public void setFreezeDividerWindow(boolean freezeDividerWindow) { + mFreezeDividerWindow = freezeDividerWindow; + } + /** * Updates bounds with the passing position. Usually used to update recording bounds while * performing animation or dragging divider bar to resize the splits. @@ -294,20 +358,22 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange mSplitLayoutHandler.onLayoutSizeChanging(this); } - void setDividePosition(int position) { + void setDividePosition(int position, boolean applyLayoutChange) { mDividePosition = position; updateBounds(mDividePosition); - mSplitLayoutHandler.onLayoutSizeChanged(this); + if (applyLayoutChange) { + mSplitLayoutHandler.onLayoutSizeChanged(this); + } } - /** Sets divide position base on the ratio within root bounds. */ + /** Updates divide position and split bounds base on the ratio within root bounds. */ public void setDivideRatio(float ratio) { final int position = isLandscape() ? mRootBounds.left + (int) (mRootBounds.width() * ratio) : mRootBounds.top + (int) (mRootBounds.height() * ratio); - DividerSnapAlgorithm.SnapTarget snapTarget = + final DividerSnapAlgorithm.SnapTarget snapTarget = mDividerSnapAlgorithm.calculateNonDismissingSnapTarget(position); - setDividePosition(snapTarget.position); + setDividePosition(snapTarget.position, false /* applyLayoutChange */); } /** Resets divider position. */ @@ -335,7 +401,8 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange () -> mSplitLayoutHandler.onSnappedToDismiss(true /* bottomOrRight */)); break; default: - flingDividePosition(currentPosition, snapTarget.position, null); + flingDividePosition(currentPosition, snapTarget.position, + () -> setDividePosition(snapTarget.position, true /* applyLayoutChange */)); break; } } @@ -353,7 +420,8 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange return mDividerSnapAlgorithm.calculateSnapTarget(position, velocity, hardDismiss); } - private DividerSnapAlgorithm getSnapAlgorithm(Context context, Rect rootBounds) { + private DividerSnapAlgorithm getSnapAlgorithm(Context context, Rect rootBounds, + @Nullable Rect stableInsets) { final boolean isLandscape = isLandscape(rootBounds); return new DividerSnapAlgorithm( context.getResources(), @@ -361,7 +429,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange rootBounds.height(), mDividerSize, !isLandscape, - getDisplayInsets(context), + stableInsets != null ? stableInsets : getDisplayInsets(context), isLandscape ? DOCKED_LEFT : DOCKED_TOP /* dockSide */); } @@ -372,6 +440,8 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange mSplitLayoutHandler.onLayoutSizeChanged(this); return; } + InteractionJankMonitorUtils.beginTracing(InteractionJankMonitor.CUJ_SPLIT_SCREEN_RESIZE, + mSplitWindowManager.getDividerView(), "Divider fling"); ValueAnimator animator = ValueAnimator .ofInt(from, to) .setDuration(250); @@ -381,15 +451,16 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange animator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { - setDividePosition(to); if (flingFinishedCallback != null) { flingFinishedCallback.run(); } + InteractionJankMonitorUtils.endTracing( + InteractionJankMonitor.CUJ_SPLIT_SCREEN_RESIZE); } @Override public void onAnimationCancel(Animator animation) { - setDividePosition(to); + setDividePosition(to, true /* applyLayoutChange */); } }); animator.start(); @@ -429,45 +500,58 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange /** Apply recorded surface layout to the {@link SurfaceControl.Transaction}. */ public void applySurfaceChanges(SurfaceControl.Transaction t, SurfaceControl leash1, - SurfaceControl leash2, SurfaceControl dimLayer1, SurfaceControl dimLayer2) { + SurfaceControl leash2, SurfaceControl dimLayer1, SurfaceControl dimLayer2, + boolean applyResizingOffset) { final SurfaceControl dividerLeash = getDividerLeash(); if (dividerLeash != null) { - t.setPosition(dividerLeash, mDividerBounds.left, mDividerBounds.top); + mTempRect.set(getRefDividerBounds()); + t.setPosition(dividerLeash, mTempRect.left, mTempRect.top); // Resets layer of divider bar to make sure it is always on top. - t.setLayer(dividerLeash, SPLIT_DIVIDER_LAYER); + t.setLayer(dividerLeash, Integer.MAX_VALUE); } - t.setPosition(leash1, mBounds1.left, mBounds1.top) - .setWindowCrop(leash1, mBounds1.width(), mBounds1.height()); - t.setPosition(leash2, mBounds2.left, mBounds2.top) - .setWindowCrop(leash2, mBounds2.width(), mBounds2.height()); + mTempRect.set(getRefBounds1()); + t.setPosition(leash1, mTempRect.left, mTempRect.top) + .setWindowCrop(leash1, mTempRect.width(), mTempRect.height()); + mTempRect.set(getRefBounds2()); + t.setPosition(leash2, mTempRect.left, mTempRect.top) + .setWindowCrop(leash2, mTempRect.width(), mTempRect.height()); if (mImePositionProcessor.adjustSurfaceLayoutForIme( t, dividerLeash, leash1, leash2, dimLayer1, dimLayer2)) { return; } - mDismissingEffectPolicy.adjustDismissingSurface(t, leash1, leash2, dimLayer1, dimLayer2); + mSurfaceEffectPolicy.adjustDimSurface(t, dimLayer1, dimLayer2); + if (applyResizingOffset) { + mSurfaceEffectPolicy.adjustRootSurface(t, leash1, leash2); + } } /** Apply recorded task layout to the {@link WindowContainerTransaction}. */ public void applyTaskChanges(WindowContainerTransaction wct, ActivityManager.RunningTaskInfo task1, ActivityManager.RunningTaskInfo task2) { - if (mImePositionProcessor.applyTaskLayoutForIme(wct, task1.token, task2.token)) { - return; - } - 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; } 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; } } + private int getSmallestWidthDp(Rect bounds) { + mTempRect.set(bounds); + mTempRect.inset(getDisplayInsets(mContext)); + final int minWidth = Math.min(mTempRect.width(), mTempRect.height()); + final float density = mContext.getResources().getDisplayMetrics().density; + return (int) (minWidth / density); + } + /** * Shift configuration bounds to prevent client apps get configuration changed or relaunch. And * restore shifted configuration bounds if it's no longer shifted. @@ -524,7 +608,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange * Calls when resizing the split bounds. * * @see #applySurfaceChanges(SurfaceControl.Transaction, SurfaceControl, SurfaceControl, - * SurfaceControl, SurfaceControl) + * SurfaceControl, SurfaceControl, boolean) */ void onLayoutSizeChanging(SplitLayout layout); @@ -534,7 +618,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange * @see #applyTaskChanges(WindowContainerTransaction, ActivityManager.RunningTaskInfo, * ActivityManager.RunningTaskInfo) * @see #applySurfaceChanges(SurfaceControl.Transaction, SurfaceControl, SurfaceControl, - * SurfaceControl, SurfaceControl) + * SurfaceControl, SurfaceControl, boolean) */ void onLayoutSizeChanged(SplitLayout layout); @@ -543,7 +627,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange * panel. * * @see #applySurfaceChanges(SurfaceControl.Transaction, SurfaceControl, SurfaceControl, - * SurfaceControl, SurfaceControl) + * SurfaceControl, SurfaceControl, boolean) */ void onLayoutPositionChanging(SplitLayout layout); @@ -571,21 +655,25 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange * Calculates and applies proper dismissing parallax offset and dimming value to hint users * dismissing gesture. */ - private class DismissingEffectPolicy { + private class ResizingEffectPolicy { /** Indicates whether to offset splitting bounds to hint dismissing progress or not. */ - private final boolean mApplyParallax; + private final int mParallaxType; + + int mShrinkSide = DOCKED_INVALID; // The current dismissing side. int mDismissingSide = DOCKED_INVALID; // The parallax offset to hint the dismissing side and progress. - final Point mDismissingParallaxOffset = new Point(); + final Point mParallaxOffset = new Point(); // The dimming value to hint the dismissing side and progress. float mDismissingDimValue = 0.0f; + final Rect mContentBounds = new Rect(); + final Rect mSurfaceBounds = new Rect(); - DismissingEffectPolicy(boolean applyDismissingParallax) { - mApplyParallax = applyDismissingParallax; + ResizingEffectPolicy(int parallaxType) { + mParallaxType = parallaxType; } /** @@ -596,7 +684,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange */ void applyDividerPosition(int position, boolean isLandscape) { mDismissingSide = DOCKED_INVALID; - mDismissingParallaxOffset.set(0, 0); + mParallaxOffset.set(0, 0); mDismissingDimValue = 0; int totalDismissingDistance = 0; @@ -610,15 +698,39 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange - mDividerSnapAlgorithm.getDismissEndTarget().position; } + final boolean topLeftShrink = isLandscape + ? position < mWinBounds1.right : position < mWinBounds1.bottom; + if (topLeftShrink) { + mShrinkSide = isLandscape ? DOCKED_LEFT : DOCKED_TOP; + mContentBounds.set(mWinBounds1); + mSurfaceBounds.set(mBounds1); + } else { + mShrinkSide = isLandscape ? DOCKED_RIGHT : DOCKED_BOTTOM; + mContentBounds.set(mWinBounds2); + mSurfaceBounds.set(mBounds2); + } + if (mDismissingSide != DOCKED_INVALID) { float fraction = Math.max(0, Math.min(mDividerSnapAlgorithm.calculateDismissingFraction(position), 1f)); mDismissingDimValue = DIM_INTERPOLATOR.getInterpolation(fraction); - fraction = calculateParallaxDismissingFraction(fraction, mDismissingSide); + if (mParallaxType == PARALLAX_DISMISSING) { + fraction = calculateParallaxDismissingFraction(fraction, mDismissingSide); + if (isLandscape) { + mParallaxOffset.x = (int) (fraction * totalDismissingDistance); + } else { + mParallaxOffset.y = (int) (fraction * totalDismissingDistance); + } + } + } + + if (mParallaxType == PARALLAX_ALIGN_CENTER) { if (isLandscape) { - mDismissingParallaxOffset.x = (int) (fraction * totalDismissingDistance); + mParallaxOffset.x = + (mSurfaceBounds.width() - mContentBounds.width()) / 2; } else { - mDismissingParallaxOffset.y = (int) (fraction * totalDismissingDistance); + mParallaxOffset.y = + (mSurfaceBounds.height() - mContentBounds.height()) / 2; } } } @@ -638,41 +750,66 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange } /** Applies parallax offset and dimming value to the root surface at the dismissing side. */ - boolean adjustDismissingSurface(SurfaceControl.Transaction t, - SurfaceControl leash1, SurfaceControl leash2, + void adjustRootSurface(SurfaceControl.Transaction t, + SurfaceControl leash1, SurfaceControl leash2) { + SurfaceControl targetLeash = null; + + if (mParallaxType == PARALLAX_DISMISSING) { + switch (mDismissingSide) { + case DOCKED_TOP: + case DOCKED_LEFT: + targetLeash = leash1; + mTempRect.set(mBounds1); + break; + case DOCKED_BOTTOM: + case DOCKED_RIGHT: + targetLeash = leash2; + mTempRect.set(mBounds2); + break; + } + } else if (mParallaxType == PARALLAX_ALIGN_CENTER) { + switch (mShrinkSide) { + case DOCKED_TOP: + case DOCKED_LEFT: + targetLeash = leash1; + mTempRect.set(mBounds1); + break; + case DOCKED_BOTTOM: + case DOCKED_RIGHT: + targetLeash = leash2; + mTempRect.set(mBounds2); + break; + } + } + if (mParallaxType != PARALLAX_NONE && targetLeash != null) { + t.setPosition(targetLeash, + mTempRect.left + mParallaxOffset.x, mTempRect.top + mParallaxOffset.y); + // Transform the screen-based split bounds to surface-based crop bounds. + mTempRect.offsetTo(-mParallaxOffset.x, -mParallaxOffset.y); + t.setWindowCrop(targetLeash, mTempRect); + } + } + + void adjustDimSurface(SurfaceControl.Transaction t, SurfaceControl dimLayer1, SurfaceControl dimLayer2) { - SurfaceControl targetLeash, targetDimLayer; + SurfaceControl targetDimLayer; switch (mDismissingSide) { case DOCKED_TOP: case DOCKED_LEFT: - targetLeash = leash1; targetDimLayer = dimLayer1; - mTempRect.set(mBounds1); break; case DOCKED_BOTTOM: case DOCKED_RIGHT: - targetLeash = leash2; targetDimLayer = dimLayer2; - mTempRect.set(mBounds2); break; case DOCKED_INVALID: default: t.setAlpha(dimLayer1, 0).hide(dimLayer1); t.setAlpha(dimLayer2, 0).hide(dimLayer2); - return false; - } - - if (mApplyParallax) { - t.setPosition(targetLeash, - mTempRect.left + mDismissingParallaxOffset.x, - mTempRect.top + mDismissingParallaxOffset.y); - // Transform the screen-based split bounds to surface-based crop bounds. - mTempRect.offsetTo(-mDismissingParallaxOffset.x, -mDismissingParallaxOffset.y); - t.setWindowCrop(targetLeash, mTempRect); + return; } t.setAlpha(targetDimLayer, mDismissingDimValue) .setVisibility(targetDimLayer, mDismissingDimValue > 0.001f); - return true; } } @@ -687,6 +824,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange private final int mDisplayId; + private boolean mHasImeFocus; private boolean mImeShown; private int mYOffsetForIme; private float mDimValue1; @@ -709,25 +847,32 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange @Override public int onImeStartPositioning(int displayId, int hiddenTop, int shownTop, boolean showing, boolean isFloating, SurfaceControl.Transaction t) { - if (displayId != mDisplayId) return 0; + if (displayId != mDisplayId || !mInitialized) { + return 0; + } + final int imeTargetPosition = getImeTargetPosition(); - if (!mInitialized || imeTargetPosition == SPLIT_POSITION_UNDEFINED) return 0; + mHasImeFocus = imeTargetPosition != SPLIT_POSITION_UNDEFINED; + if (!mHasImeFocus) { + return 0; + } + mStartImeTop = showing ? hiddenTop : shownTop; mEndImeTop = showing ? shownTop : hiddenTop; mImeShown = showing; // Update target dim values mLastDim1 = mDimValue1; - mTargetDim1 = imeTargetPosition == SPLIT_POSITION_BOTTOM_OR_RIGHT && showing - ? ADJUSTED_NONFOCUS_DIM : 0.0f; + mTargetDim1 = imeTargetPosition == SPLIT_POSITION_BOTTOM_OR_RIGHT && mImeShown + && mDimNonImeSide ? ADJUSTED_NONFOCUS_DIM : 0.0f; mLastDim2 = mDimValue2; - mTargetDim2 = imeTargetPosition == SPLIT_POSITION_TOP_OR_LEFT && showing - ? ADJUSTED_NONFOCUS_DIM : 0.0f; + mTargetDim2 = imeTargetPosition == SPLIT_POSITION_TOP_OR_LEFT && mImeShown + && mDimNonImeSide ? ADJUSTED_NONFOCUS_DIM : 0.0f; // Calculate target bounds offset for IME mLastYOffset = mYOffsetForIme; final boolean needOffset = imeTargetPosition == SPLIT_POSITION_BOTTOM_OR_RIGHT - && !isFloating && !isLandscape(mRootBounds) && showing; + && !isFloating && !isLandscape(mRootBounds) && mImeShown; mTargetYOffset = needOffset ? getTargetYOffset() : 0; if (mTargetYOffset != mLastYOffset) { @@ -746,15 +891,14 @@ 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( - !showing || imeTargetPosition == SPLIT_POSITION_UNDEFINED); + mSplitWindowManager.setInteractive(!mImeShown || !mHasImeFocus); return needOffset ? IME_ANIMATION_NO_ALPHA : 0; } @Override public void onImePositionChanged(int displayId, int imeTop, SurfaceControl.Transaction t) { - if (displayId != mDisplayId) return; + if (displayId != mDisplayId || !mHasImeFocus) return; onProgress(getProgress(imeTop)); mSplitLayoutHandler.onLayoutPositionChanging(SplitLayout.this); } @@ -762,7 +906,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange @Override public void onImeEndPositioning(int displayId, boolean cancel, SurfaceControl.Transaction t) { - if (displayId != mDisplayId || cancel) return; + if (displayId != mDisplayId || !mHasImeFocus || cancel) return; onProgress(1.0f); mSplitLayoutHandler.onLayoutPositionChanging(SplitLayout.this); } @@ -774,6 +918,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange if (!controlling && mImeShown) { reset(); mSplitWindowManager.setInteractive(true); + mSplitLayoutHandler.setLayoutOffsetTarget(0, 0, SplitLayout.this); mSplitLayoutHandler.onLayoutPositionChanging(SplitLayout.this); } } @@ -807,6 +952,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange } void reset() { + mHasImeFocus = false; mImeShown = false; mYOffsetForIme = mLastYOffset = mTargetYOffset = 0; mDimValue1 = mLastDim1 = mTargetDim1 = 0.0f; @@ -814,26 +960,6 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange } /** - * Applies adjusted task layout for showing IME. - * - * @return {@code false} if there's no need to adjust, otherwise {@code true} - */ - boolean applyTaskLayoutForIme(WindowContainerTransaction wct, - WindowContainerToken token1, WindowContainerToken token2) { - if (mYOffsetForIme == 0) return false; - - mTempRect.set(mBounds1); - mTempRect.offset(0, mYOffsetForIme); - wct.setBounds(token1, mTempRect); - - mTempRect.set(mBounds2); - mTempRect.offset(0, mYOffsetForIme); - wct.setBounds(token2, mTempRect); - - return true; - } - - /** * Adjusts surface layout while showing IME. * * @return {@code false} if there's no need to adjust, otherwise {@code true} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitWindowManager.java index 4903f9d46dc7..864b9a7528b0 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 @@ -37,6 +37,7 @@ import android.view.LayoutInflater; import android.view.SurfaceControl; import android.view.SurfaceControlViewHost; import android.view.SurfaceSession; +import android.view.View; import android.view.WindowManager; import android.view.WindowlessWindowManager; @@ -58,6 +59,9 @@ public final class SplitWindowManager extends WindowlessWindowManager { private SurfaceControl mLeash; private DividerView mDividerView; + // Used to "pass" a transaction to WWM.remove so that view removal can be synchronized. + private SurfaceControl.Transaction mSyncTransaction = null; + public interface ParentContainerCallbacks { void attachToParentSurface(SurfaceControl.Builder b); void onLeashReady(SurfaceControl leash); @@ -130,27 +134,47 @@ public final class SplitWindowManager extends WindowlessWindowManager { * Releases the surface control of the current {@link DividerView} and tear down the view * hierarchy. */ - void release() { + void release(@Nullable SurfaceControl.Transaction t) { if (mDividerView != null) { mDividerView = null; } if (mViewHost != null){ + mSyncTransaction = t; mViewHost.release(); + mSyncTransaction = null; mViewHost = null; } if (mLeash != null) { - new SurfaceControl.Transaction().remove(mLeash).apply(); + if (t == null) { + new SurfaceControl.Transaction().remove(mLeash).apply(); + } else { + t.remove(mLeash); + } mLeash = null; } } + @Override + protected void removeSurface(SurfaceControl sc) { + // This gets called via SurfaceControlViewHost.release() + if (mSyncTransaction != null) { + mSyncTransaction.remove(sc); + } else { + super.removeSurface(sc); + } + } + void setInteractive(boolean interactive) { if (mDividerView == null) return; mDividerView.setInteractive(interactive); } + View getDividerView() { + return mDividerView; + } + /** * Gets {@link SurfaceControl} of the surface holding divider view. @return {@code null} if not * feasible. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUI.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUI.java index 99dbfe01964c..b87cf47dd93f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUI.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUI.java @@ -24,9 +24,12 @@ import com.android.wm.shell.common.annotations.ExternalThread; @ExternalThread public interface CompatUI { /** - * Called when the keyguard occluded state changes. Removes all compat UIs if the - * keyguard is now occluded. - * @param occluded indicates if the keyguard is now occluded. + * Called when the keyguard showing state changes. Removes all compat UIs if the + * keyguard is now showing. + * + * <p>Note that if the keyguard is occluded it will also be considered showing. + * + * @param showing indicates if the keyguard is now showing. */ - void onKeyguardOccludedChanged(boolean occluded); + void onKeyguardShowingChanged(boolean showing); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java index e0b23873a980..99b32a677abe 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 @@ -17,6 +17,8 @@ package com.android.wm.shell.compatui; import android.annotation.Nullable; +import android.app.TaskInfo; +import android.app.TaskInfo.CameraCompatControlState; import android.content.Context; import android.content.res.Configuration; import android.hardware.display.DisplayManager; @@ -38,6 +40,9 @@ import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.common.annotations.ExternalThread; +import com.android.wm.shell.compatui.CompatUIWindowManager.CompatUIHintsState; +import com.android.wm.shell.compatui.letterboxedu.LetterboxEduWindowManager; +import com.android.wm.shell.transition.Transitions; import java.lang.ref.WeakReference; import java.util.ArrayList; @@ -46,19 +51,23 @@ import java.util.Set; import java.util.function.Consumer; import java.util.function.Predicate; +import dagger.Lazy; + /** - * Controls to show/update restart-activity buttons on Tasks based on whether the foreground + * Controller to show/update compat UI components on Tasks based on whether the foreground * activities are in compatibility mode. */ public class CompatUIController implements OnDisplaysChangedListener, DisplayImeController.ImePositionProcessor { - /** Callback for size compat UI interaction. */ + /** Callback for compat UI interaction. */ public interface CompatUICallback { /** Called when the size compat restart button appears. */ void onSizeCompatRestartButtonAppeared(int taskId); /** Called when the size compat restart button is clicked. */ void onSizeCompatRestartButtonClicked(int taskId); + /** Called when the camera compat control state is updated. */ + void onCameraControlStateUpdated(int taskId, @CameraCompatControlState int state); } private static final String TAG = "CompatUIController"; @@ -70,8 +79,22 @@ public class CompatUIController implements OnDisplaysChangedListener, private final SparseArray<PerDisplayOnInsetsChangedListener> mOnInsetsChangedListeners = new SparseArray<>(0); - /** The showing UIs by task id. */ - private final SparseArray<CompatUIWindowManager> mActiveLayouts = new SparseArray<>(0); + /** + * The active Compat Control UI layouts by task id. + * + * <p>An active layout is a layout that is eligible to be shown for the associated task but + * isn't necessarily shown at a given time. + */ + private final SparseArray<CompatUIWindowManager> mActiveCompatLayouts = new SparseArray<>(0); + + /** + * 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 + * isn't necessarily shown at a given time. + */ + @Nullable + private LetterboxEduWindowManager mActiveLetterboxEduLayout; /** Avoid creating display context frequently for non-default display. */ private final SparseArray<WeakReference<Context>> mDisplayContextCache = new SparseArray<>(0); @@ -82,30 +105,35 @@ public class CompatUIController implements OnDisplaysChangedListener, 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; - /** Only show once automatically in the process life. */ - private boolean mHasShownHint; - /** Indicates if the keyguard is currently occluded, in which case compat UIs shouldn't - * be shown. */ - private boolean mKeyguardOccluded; + // Only show each hint once automatically in the process life. + private final CompatUIHintsState mCompatUIHintsState; + + // Indicates if the keyguard is currently showing, in which case compat UIs shouldn't + // be shown. + private boolean mKeyguardShowing; public CompatUIController(Context context, DisplayController displayController, DisplayInsetsController displayInsetsController, DisplayImeController imeController, SyncTransactionQueue syncQueue, - ShellExecutor mainExecutor) { + ShellExecutor mainExecutor, + Lazy<Transitions> transitionsLazy) { mContext = context; mDisplayController = displayController; mDisplayInsetsController = displayInsetsController; mImeController = imeController; mSyncQueue = syncQueue; mMainExecutor = mainExecutor; + mTransitionsLazy = transitionsLazy; mDisplayController.addDisplayWindowListener(this); mImeController.addPositionProcessor(this); + mCompatUIHintsState = new CompatUIHintsState(); } /** Returns implementation of {@link CompatUI}. */ @@ -122,24 +150,19 @@ public class CompatUIController implements OnDisplaysChangedListener, * Called when the Task info changed. Creates and updates the compat UI if there is an * activity in size compat, or removes the UI if there is no size compat activity. * - * @param displayId display the task and activity are in. - * @param taskId task the activity is in. - * @param taskConfig task config to place the compat UI with. + * @param taskInfo {@link TaskInfo} task the activity is in. * @param taskListener listener to handle the Task Surface placement. */ - public void onCompatInfoChanged(int displayId, int taskId, - @Nullable Configuration taskConfig, + public void onCompatInfoChanged(TaskInfo taskInfo, @Nullable ShellTaskOrganizer.TaskListener taskListener) { - if (taskConfig == null || taskListener == null) { + if (taskInfo.configuration == null || taskListener == null) { // Null token means the current foreground activity is not in compatibility mode. - removeLayout(taskId); - } else if (mActiveLayouts.contains(taskId)) { - // UI already exists, update the UI layout. - updateLayout(taskId, taskConfig, taskListener); - } else { - // Create a new compat UI. - createLayout(displayId, taskId, taskConfig, taskListener); + removeLayouts(taskInfo.taskId); + return; } + + createOrUpdateCompatLayout(taskInfo, taskListener); + createOrUpdateLetterboxEduLayout(taskInfo, taskListener); } @Override @@ -156,7 +179,7 @@ public class CompatUIController implements OnDisplaysChangedListener, final List<Integer> toRemoveTaskIds = new ArrayList<>(); forAllLayoutsOnDisplay(displayId, layout -> toRemoveTaskIds.add(layout.getTaskId())); for (int i = toRemoveTaskIds.size() - 1; i >= 0; i--) { - removeLayout(toRemoveTaskIds.get(i)); + removeLayouts(toRemoveTaskIds.get(i)); } } @@ -201,59 +224,108 @@ public class CompatUIController implements OnDisplaysChangedListener, } @VisibleForTesting - void onKeyguardOccludedChanged(boolean occluded) { - mKeyguardOccluded = occluded; - // Hide the compat UIs when keyguard is occluded. + void onKeyguardShowingChanged(boolean showing) { + mKeyguardShowing = showing; + // Hide the compat UIs when keyguard is showing. forAllLayouts(layout -> layout.updateVisibility(showOnDisplay(layout.getDisplayId()))); } private boolean showOnDisplay(int displayId) { - return !mKeyguardOccluded && !isImeShowingOnDisplay(displayId); + return !mKeyguardShowing && !isImeShowingOnDisplay(displayId); } private boolean isImeShowingOnDisplay(int displayId) { return mDisplaysWithIme.contains(displayId); } - private void createLayout(int displayId, int taskId, Configuration taskConfig, + private void createOrUpdateCompatLayout(TaskInfo taskInfo, ShellTaskOrganizer.TaskListener taskListener) { - final Context context = getOrCreateDisplayContext(displayId); - if (context == null) { - Log.e(TAG, "Cannot get context for display " + displayId); + CompatUIWindowManager layout = mActiveCompatLayouts.get(taskInfo.taskId); + if (layout != null) { + // 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. + mActiveCompatLayouts.remove(taskInfo.taskId); + } return; } - final CompatUIWindowManager compatUIWindowManager = - createLayout(context, displayId, taskId, taskConfig, taskListener); - mActiveLayouts.put(taskId, compatUIWindowManager); - compatUIWindowManager.createLayout(showOnDisplay(displayId)); + // Create a new UI layout. + final Context context = getOrCreateDisplayContext(taskInfo.displayId); + if (context == null) { + return; + } + layout = createCompatUiWindowManager(context, taskInfo, taskListener); + if (layout.createLayout(showOnDisplay(taskInfo.displayId))) { + // The new layout is eligible to be shown, add it the active layouts. + mActiveCompatLayouts.put(taskInfo.taskId, layout); + } } @VisibleForTesting - CompatUIWindowManager createLayout(Context context, int displayId, int taskId, - Configuration taskConfig, ShellTaskOrganizer.TaskListener taskListener) { - final CompatUIWindowManager compatUIWindowManager = new CompatUIWindowManager(context, - taskConfig, mSyncQueue, mCallback, taskId, taskListener, - mDisplayController.getDisplayLayout(displayId), mHasShownHint); - // Only show hint for the first time. - mHasShownHint = true; - return compatUIWindowManager; + CompatUIWindowManager createCompatUiWindowManager(Context context, TaskInfo taskInfo, + ShellTaskOrganizer.TaskListener taskListener) { + return new CompatUIWindowManager(context, + taskInfo, mSyncQueue, mCallback, taskListener, + mDisplayController.getDisplayLayout(taskInfo.displayId), mCompatUIHintsState); } - private void updateLayout(int taskId, Configuration taskConfig, + private void createOrUpdateLetterboxEduLayout(TaskInfo taskInfo, ShellTaskOrganizer.TaskListener taskListener) { - final CompatUIWindowManager layout = mActiveLayouts.get(taskId); - if (layout == null) { + if (mActiveLetterboxEduLayout != null + && mActiveLetterboxEduLayout.getTaskId() == taskInfo.taskId) { + // UI already exists, update the UI layout. + if (!mActiveLetterboxEduLayout.updateCompatInfo(taskInfo, taskListener, + showOnDisplay(mActiveLetterboxEduLayout.getDisplayId()))) { + // The layout is no longer eligible to be shown, clear active layout. + mActiveLetterboxEduLayout = null; + } + return; + } + + // Create a new UI layout. + final Context context = getOrCreateDisplayContext(taskInfo.displayId); + if (context == null) { return; } - layout.updateCompatInfo(taskConfig, taskListener, showOnDisplay(layout.getDisplayId())); + LetterboxEduWindowManager newLayout = createLetterboxEduWindowManager(context, taskInfo, + taskListener); + if (newLayout.createLayout(showOnDisplay(taskInfo.displayId))) { + // The new layout is eligible to be shown, make it the active layout. + if (mActiveLetterboxEduLayout != null) { + // Release the previous layout since at most one can be active. + // Since letterbox education is only shown once to the user, releasing the previous + // layout is only a precaution. + mActiveLetterboxEduLayout.release(); + } + mActiveLetterboxEduLayout = newLayout; + } } - private void removeLayout(int taskId) { - final CompatUIWindowManager layout = mActiveLayouts.get(taskId); + @VisibleForTesting + LetterboxEduWindowManager createLetterboxEduWindowManager(Context context, TaskInfo taskInfo, + ShellTaskOrganizer.TaskListener taskListener) { + return new LetterboxEduWindowManager(context, taskInfo, + mSyncQueue, taskListener, mDisplayController.getDisplayLayout(taskInfo.displayId), + mTransitionsLazy.get(), + this::onLetterboxEduDismissed); + } + + private void onLetterboxEduDismissed() { + mActiveLetterboxEduLayout = null; + } + + private void removeLayouts(int taskId) { + final CompatUIWindowManager layout = mActiveCompatLayouts.get(taskId); if (layout != null) { layout.release(); - mActiveLayouts.remove(taskId); + mActiveCompatLayouts.remove(taskId); + } + + if (mActiveLetterboxEduLayout != null && mActiveLetterboxEduLayout.getTaskId() == taskId) { + mActiveLetterboxEduLayout.release(); + mActiveLetterboxEduLayout = null; } } @@ -271,28 +343,34 @@ public class CompatUIController implements OnDisplaysChangedListener, if (display != null) { context = mContext.createDisplayContext(display); mDisplayContextCache.put(displayId, new WeakReference<>(context)); + } else { + Log.e(TAG, "Cannot get context for display " + displayId); } } return context; } - private void forAllLayoutsOnDisplay(int displayId, Consumer<CompatUIWindowManager> callback) { + private void forAllLayoutsOnDisplay(int displayId, + Consumer<CompatUIWindowManagerAbstract> callback) { forAllLayouts(layout -> layout.getDisplayId() == displayId, callback); } - private void forAllLayouts(Consumer<CompatUIWindowManager> callback) { + private void forAllLayouts(Consumer<CompatUIWindowManagerAbstract> callback) { forAllLayouts(layout -> true, callback); } - private void forAllLayouts(Predicate<CompatUIWindowManager> condition, - Consumer<CompatUIWindowManager> callback) { - for (int i = 0; i < mActiveLayouts.size(); i++) { - final int taskId = mActiveLayouts.keyAt(i); - final CompatUIWindowManager layout = mActiveLayouts.get(taskId); + private void forAllLayouts(Predicate<CompatUIWindowManagerAbstract> condition, + Consumer<CompatUIWindowManagerAbstract> callback) { + for (int i = 0; i < mActiveCompatLayouts.size(); i++) { + final int taskId = mActiveCompatLayouts.keyAt(i); + final CompatUIWindowManager layout = mActiveCompatLayouts.get(taskId); if (layout != null && condition.test(layout)) { callback.accept(layout); } } + if (mActiveLetterboxEduLayout != null && condition.test(mActiveLetterboxEduLayout)) { + callback.accept(mActiveLetterboxEduLayout); + } } /** @@ -301,9 +379,9 @@ public class CompatUIController implements OnDisplaysChangedListener, @ExternalThread private class CompatUIImpl implements CompatUI { @Override - public void onKeyguardOccludedChanged(boolean occluded) { + public void onKeyguardShowingChanged(boolean showing) { mMainExecutor.execute(() -> { - CompatUIController.this.onKeyguardOccludedChanged(occluded); + CompatUIController.this.onKeyguardShowingChanged(showing); }); } } 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 ea4f20968438..d44b4d8f63b6 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUILayout.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUILayout.java @@ -16,6 +16,9 @@ package com.android.wm.shell.compatui; +import android.annotation.IdRes; +import android.app.TaskInfo; +import android.app.TaskInfo.CameraCompatControlState; import android.content.Context; import android.util.AttributeSet; import android.view.View; @@ -28,7 +31,7 @@ import com.android.wm.shell.R; /** * Container for compat UI controls. */ -public class CompatUILayout extends LinearLayout { +class CompatUILayout extends LinearLayout { private CompatUIWindowManager mWindowManager; @@ -53,21 +56,59 @@ public class CompatUILayout extends LinearLayout { mWindowManager = windowManager; } - @Override - protected void onLayout(boolean changed, int left, int top, int right, int bottom) { - super.onLayout(changed, left, top, right, bottom); - // Need to relayout after changes like hiding / showing a hint since they affect size. - // Doing this directly in setSizeCompatHintVisibility can result in flaky animation. - mWindowManager.relayout(); + void updateCameraTreatmentButton(@CameraCompatControlState int newState) { + int buttonBkgId = newState == TaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED + ? R.drawable.camera_compat_treatment_suggested_ripple + : R.drawable.camera_compat_treatment_applied_ripple; + int hintStringId = newState == TaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED + ? R.string.camera_compat_treatment_suggested_button_description + : R.string.camera_compat_treatment_applied_button_description; + final ImageButton button = findViewById(R.id.camera_compat_treatment_button); + button.setImageResource(buttonBkgId); + button.setContentDescription(getResources().getString(hintStringId)); + final LinearLayout hint = findViewById(R.id.camera_compat_hint); + ((TextView) hint.findViewById(R.id.compat_mode_hint_text)).setText(hintStringId); } void setSizeCompatHintVisibility(boolean show) { - final LinearLayout sizeCompatHint = findViewById(R.id.size_compat_hint); + setViewVisibility(R.id.size_compat_hint, show); + } + + void setCameraCompatHintVisibility(boolean show) { + setViewVisibility(R.id.camera_compat_hint, show); + } + + void setRestartButtonVisibility(boolean show) { + setViewVisibility(R.id.size_compat_restart_button, show); + // Hint should never be visible without button. + if (!show) { + setSizeCompatHintVisibility(/* show= */ false); + } + } + + void setCameraControlVisibility(boolean show) { + setViewVisibility(R.id.camera_compat_control, show); + // Hint should never be visible without button. + if (!show) { + setCameraCompatHintVisibility(/* show= */ false); + } + } + + private void setViewVisibility(@IdRes int resId, boolean show) { + final View view = findViewById(resId); int visibility = show ? View.VISIBLE : View.GONE; - if (sizeCompatHint.getVisibility() == visibility) { + if (view.getVisibility() == visibility) { return; } - sizeCompatHint.setVisibility(visibility); + view.setVisibility(visibility); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + // Need to relayout after changes like hiding / showing a hint since they affect size. + // Doing this directly in setSizeCompatHintVisibility can result in flaky animation. + mWindowManager.relayout(); } @Override @@ -85,5 +126,26 @@ public class CompatUILayout extends LinearLayout { ((TextView) sizeCompatHint.findViewById(R.id.compat_mode_hint_text)) .setText(R.string.restart_button_description); sizeCompatHint.setOnClickListener(view -> setSizeCompatHintVisibility(/* show= */ false)); + + final ImageButton cameraTreatmentButton = + findViewById(R.id.camera_compat_treatment_button); + cameraTreatmentButton.setOnClickListener( + view -> mWindowManager.onCameraTreatmentButtonClicked()); + cameraTreatmentButton.setOnLongClickListener(view -> { + mWindowManager.onCameraButtonLongClicked(); + return true; + }); + + final ImageButton cameraDismissButton = findViewById(R.id.camera_compat_dismiss_button); + cameraDismissButton.setOnClickListener( + view -> mWindowManager.onCameraDismissButtonClicked()); + cameraDismissButton.setOnLongClickListener(view -> { + mWindowManager.onCameraButtonLongClicked(); + return true; + }); + + final LinearLayout cameraCompatHint = findViewById(R.id.camera_compat_hint); + cameraCompatHint.setOnClickListener( + view -> setCameraCompatHintVisibility(/* show= */ false)); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java index 997ad04e3b57..bce3ec4128e8 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java @@ -16,178 +16,124 @@ package com.android.wm.shell.compatui; -import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; -import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL; -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 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 android.annotation.Nullable; +import android.app.TaskInfo; +import android.app.TaskInfo.CameraCompatControlState; import android.content.Context; -import android.content.res.Configuration; -import android.graphics.PixelFormat; import android.graphics.Rect; -import android.os.Binder; import android.util.Log; -import android.view.IWindow; import android.view.LayoutInflater; -import android.view.SurfaceControl; -import android.view.SurfaceControlViewHost; -import android.view.SurfaceSession; import android.view.View; -import android.view.WindowManager; -import android.view.WindowlessWindowManager; 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.compatui.CompatUIController.CompatUICallback; +import com.android.wm.shell.compatui.letterboxedu.LetterboxEduWindowManager; /** - * Holds view hierarchy of a root surface and helps to inflate and manage layout for compat - * controls. + * Window manager for the Size Compat restart button and Camera Compat control. */ -class CompatUIWindowManager extends WindowlessWindowManager { +class CompatUIWindowManager extends CompatUIWindowManagerAbstract { - private static final String TAG = "CompatUIWindowManager"; + /** + * The Compat UI should be below the Letterbox Education. + */ + private static final int Z_ORDER = LetterboxEduWindowManager.Z_ORDER - 1; - private final SyncTransactionQueue mSyncQueue; - private final CompatUIController.CompatUICallback mCallback; - private final int mDisplayId; - private final int mTaskId; - private final Rect mStableBounds; + private final CompatUICallback mCallback; - private Context mContext; - private Configuration mTaskConfig; - private ShellTaskOrganizer.TaskListener mTaskListener; - private DisplayLayout mDisplayLayout; + // Remember the last reported states in case visibility changes due to keyguard or IME updates. + @VisibleForTesting + boolean mHasSizeCompat; @VisibleForTesting - boolean mShouldShowHint; + @CameraCompatControlState + int mCameraCompatControlState = CAMERA_COMPAT_CONTROL_HIDDEN; - @Nullable @VisibleForTesting - CompatUILayout mCompatUILayout; + CompatUIHintsState mCompatUIHintsState; @Nullable - private SurfaceControlViewHost mViewHost; - @Nullable - private SurfaceControl mLeash; - - CompatUIWindowManager(Context context, Configuration taskConfig, - SyncTransactionQueue syncQueue, CompatUIController.CompatUICallback callback, - int taskId, ShellTaskOrganizer.TaskListener taskListener, DisplayLayout displayLayout, - boolean hasShownHint) { - super(taskConfig, null /* rootSurface */, null /* hostInputToken */); - mContext = context; - mSyncQueue = syncQueue; + @VisibleForTesting + CompatUILayout mLayout; + + CompatUIWindowManager(Context context, TaskInfo taskInfo, + SyncTransactionQueue syncQueue, CompatUICallback callback, + ShellTaskOrganizer.TaskListener taskListener, DisplayLayout displayLayout, + CompatUIHintsState compatUIHintsState) { + super(context, taskInfo, syncQueue, taskListener, displayLayout); mCallback = callback; - mTaskConfig = taskConfig; - mDisplayId = mContext.getDisplayId(); - mTaskId = taskId; - mTaskListener = taskListener; - mDisplayLayout = displayLayout; - mShouldShowHint = !hasShownHint; - mStableBounds = new Rect(); - mDisplayLayout.getStableBounds(mStableBounds); + mHasSizeCompat = taskInfo.topActivityInSizeCompat; + mCameraCompatControlState = taskInfo.cameraCompatControlState; + mCompatUIHintsState = compatUIHintsState; } @Override - public void setConfiguration(Configuration configuration) { - super.setConfiguration(configuration); - mContext = mContext.createConfigurationContext(configuration); + protected int getZOrder() { + return Z_ORDER; } @Override - protected void attachToParentSurface(IWindow window, SurfaceControl.Builder b) { - // 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() - .setName("CompatUILeash") - .setHidden(false) - .setCallsite("CompatUIWindowManager#attachToParentSurface"); - attachToParentSurface(builder); - mLeash = builder.build(); - b.setParent(mLeash); + protected @Nullable View getLayout() { + return mLayout; } - /** Creates the layout for compat controls. */ - void createLayout(boolean show) { - if (!show || mCompatUILayout != null) { - // Wait until compat controls should be visible. - return; - } - - initCompatUi(); - updateSurfacePosition(); + @Override + protected void removeLayout() { + mLayout = null; + } - mCallback.onSizeCompatRestartButtonAppeared(mTaskId); + @Override + protected boolean eligibleToShowLayout() { + return mHasSizeCompat || shouldShowCameraControl(); } - /** Called when compat info changed. */ - void updateCompatInfo(Configuration taskConfig, - ShellTaskOrganizer.TaskListener taskListener, boolean show) { - final Configuration prevTaskConfig = mTaskConfig; - final ShellTaskOrganizer.TaskListener prevTaskListener = mTaskListener; - mTaskConfig = taskConfig; - mTaskListener = taskListener; - - // Update configuration. - mContext = mContext.createConfigurationContext(taskConfig); - setConfiguration(taskConfig); - - if (mCompatUILayout == null || prevTaskListener != taskListener) { - // TaskListener changed, recreate the layout for new surface parent. - release(); - createLayout(show); - return; - } + @Override + protected View createLayout() { + mLayout = inflateLayout(); + mLayout.inject(this); - if (!taskConfig.windowConfiguration.getBounds() - .equals(prevTaskConfig.windowConfiguration.getBounds())) { - // Reposition the UI surfaces. - updateSurfacePosition(); - } + updateVisibilityOfViews(); - if (taskConfig.getLayoutDirection() != prevTaskConfig.getLayoutDirection()) { - // Update layout for RTL. - mCompatUILayout.setLayoutDirection(taskConfig.getLayoutDirection()); - updateSurfacePosition(); + if (mHasSizeCompat) { + mCallback.onSizeCompatRestartButtonAppeared(mTaskId); } + + return mLayout; } - /** Called when the visibility of the UI should change. */ - void updateVisibility(boolean show) { - if (mCompatUILayout == null) { - // Layout may not have been created because it was hidden previously. - createLayout(show); - return; - } + @VisibleForTesting + CompatUILayout inflateLayout() { + return (CompatUILayout) LayoutInflater.from(mContext).inflate(R.layout.compat_ui_layout, + null); + } - // Hide compat UIs when IME is showing. - final int newVisibility = show ? View.VISIBLE : View.GONE; - if (mCompatUILayout.getVisibility() != newVisibility) { - mCompatUILayout.setVisibility(newVisibility); + @Override + public boolean updateCompatInfo(TaskInfo taskInfo, ShellTaskOrganizer.TaskListener taskListener, + boolean canShow) { + final boolean prevHasSizeCompat = mHasSizeCompat; + final int prevCameraCompatControlState = mCameraCompatControlState; + mHasSizeCompat = taskInfo.topActivityInSizeCompat; + mCameraCompatControlState = taskInfo.cameraCompatControlState; + + if (!super.updateCompatInfo(taskInfo, taskListener, canShow)) { + return false; } - } - /** Called when display layout changed. */ - void updateDisplayLayout(DisplayLayout displayLayout) { - final Rect prevStableBounds = mStableBounds; - final Rect curStableBounds = new Rect(); - displayLayout.getStableBounds(curStableBounds); - mDisplayLayout = displayLayout; - if (!prevStableBounds.equals(curStableBounds)) { - // Stable bounds changed, update UI surface positions. - updateSurfacePosition(); - mStableBounds.set(curStableBounds); + if (prevHasSizeCompat != mHasSizeCompat + || prevCameraCompatControlState != mCameraCompatControlState) { + updateVisibilityOfViews(); } - } - /** Called when it is ready to be placed compat UI surface. */ - void attachToParentSurface(SurfaceControl.Builder b) { - mTaskListener.attachChildSurfaceToTask(mTaskId, b); + return true; } /** Called when the restart button is clicked. */ @@ -195,127 +141,105 @@ class CompatUIWindowManager extends WindowlessWindowManager { mCallback.onSizeCompatRestartButtonClicked(mTaskId); } - /** Called when the restart button is long clicked. */ - void onRestartButtonLongClicked() { - if (mCompatUILayout == null) { + /** Called when the camera treatment button is clicked. */ + void onCameraTreatmentButtonClicked() { + if (!shouldShowCameraControl()) { + Log.w(getTag(), "Camera compat shouldn't receive clicks in the hidden state."); return; } - mCompatUILayout.setSizeCompatHintVisibility(/* show= */ true); - } - - int getDisplayId() { - return mDisplayId; - } - - int getTaskId() { - return mTaskId; - } - - /** Releases the surface control and tears down the view hierarchy. */ - void release() { - mCompatUILayout = null; - - if (mViewHost != null) { - mViewHost.release(); - mViewHost = null; + // When a camera control is shown, only two states are allowed: "treament applied" and + // "treatment suggested". Clicks on the conrol's treatment button toggle between these + // two states. + mCameraCompatControlState = + mCameraCompatControlState == CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED + ? CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED + : CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED; + mCallback.onCameraControlStateUpdated(mTaskId, mCameraCompatControlState); + mLayout.updateCameraTreatmentButton(mCameraCompatControlState); + } + + /** Called when the camera dismiss button is clicked. */ + void onCameraDismissButtonClicked() { + if (!shouldShowCameraControl()) { + Log.w(getTag(), "Camera compat shouldn't receive clicks in the hidden state."); + return; } + mCameraCompatControlState = CAMERA_COMPAT_CONTROL_DISMISSED; + mCallback.onCameraControlStateUpdated(mTaskId, CAMERA_COMPAT_CONTROL_DISMISSED); + mLayout.setCameraControlVisibility(/* show= */ false); + } - if (mLeash != null) { - final SurfaceControl leash = mLeash; - mSyncQueue.runInSync(t -> t.remove(leash)); - mLeash = null; + /** Called when the restart button is long clicked. */ + void onRestartButtonLongClicked() { + if (mLayout == null) { + return; } + mLayout.setSizeCompatHintVisibility(/* show= */ true); } - void relayout() { - mViewHost.relayout(getWindowLayoutParams()); - updateSurfacePosition(); + /** Called when either dismiss or treatment camera buttons is long clicked. */ + void onCameraButtonLongClicked() { + if (mLayout == null) { + return; + } + mLayout.setCameraCompatHintVisibility(/* show= */ true); } + @Override @VisibleForTesting - void updateSurfacePosition() { - if (mCompatUILayout == null || mLeash == null) { + public void updateSurfacePosition() { + if (mLayout == null) { return; } - - // Use stable bounds to prevent controls from overlapping with system bars. - final Rect taskBounds = mTaskConfig.windowConfiguration.getBounds(); - final Rect stableBounds = new Rect(); - mDisplayLayout.getStableBounds(stableBounds); - stableBounds.intersect(taskBounds); - // Position of the button in the container coordinate. + final Rect taskBounds = getTaskBounds(); + final Rect taskStableBounds = getTaskStableBounds(); final int positionX = getLayoutDirection() == View.LAYOUT_DIRECTION_RTL - ? stableBounds.left - taskBounds.left - : stableBounds.right - taskBounds.left - mCompatUILayout.getMeasuredWidth(); - final int positionY = stableBounds.bottom - taskBounds.top - - mCompatUILayout.getMeasuredHeight(); + ? taskStableBounds.left - taskBounds.left + : taskStableBounds.right - taskBounds.left - mLayout.getMeasuredWidth(); + final int positionY = taskStableBounds.bottom - taskBounds.top + - mLayout.getMeasuredHeight(); updateSurfacePosition(positionX, positionY); } - private int getLayoutDirection() { - return mContext.getResources().getConfiguration().getLayoutDirection(); - } - - private void updateSurfacePosition(int positionX, int positionY) { - mSyncQueue.runInSync(t -> { - if (mLeash == null || !mLeash.isValid()) { - Log.w(TAG, "The leash has been released."); - return; - } - t.setPosition(mLeash, positionX, positionY); - // The compat UI should be the topmost child of the Task in case there can be more - // than one children. - t.setLayer(mLeash, Integer.MAX_VALUE); - }); - } - - /** Inflates {@link CompatUILayout} on to the root surface. */ - private void initCompatUi() { - if (mViewHost != null) { - throw new IllegalStateException( - "A UI has already been created with this window manager."); + private void updateVisibilityOfViews() { + if (mLayout == null) { + return; } - - // Construction extracted into the separate methods to allow injection for tests. - mViewHost = createSurfaceViewHost(); - mCompatUILayout = inflateCompatUILayout(); - mCompatUILayout.inject(this); - - mCompatUILayout.setSizeCompatHintVisibility(mShouldShowHint); - - mViewHost.setView(mCompatUILayout, getWindowLayoutParams()); - + // Size Compat mode restart button. + mLayout.setRestartButtonVisibility(mHasSizeCompat); // Only show by default for the first time. - mShouldShowHint = false; - } + if (mHasSizeCompat && !mCompatUIHintsState.mHasShownSizeCompatHint) { + mLayout.setSizeCompatHintVisibility(/* show= */ true); + mCompatUIHintsState.mHasShownSizeCompatHint = true; + } - @VisibleForTesting - CompatUILayout inflateCompatUILayout() { - return (CompatUILayout) LayoutInflater.from(mContext) - .inflate(R.layout.compat_ui_layout, null); + // Camera control for stretched issues. + mLayout.setCameraControlVisibility(shouldShowCameraControl()); + // Only show by default for the first time. + if (shouldShowCameraControl() && !mCompatUIHintsState.mHasShownCameraCompatHint) { + mLayout.setCameraCompatHintVisibility(/* show= */ true); + mCompatUIHintsState.mHasShownCameraCompatHint = true; + } + if (shouldShowCameraControl()) { + mLayout.updateCameraTreatmentButton(mCameraCompatControlState); + } } - @VisibleForTesting - SurfaceControlViewHost createSurfaceViewHost() { - return new SurfaceControlViewHost(mContext, mContext.getDisplay(), this); + private boolean shouldShowCameraControl() { + return mCameraCompatControlState != CAMERA_COMPAT_CONTROL_HIDDEN + && mCameraCompatControlState != CAMERA_COMPAT_CONTROL_DISMISSED; } - /** Gets the layout params. */ - private WindowManager.LayoutParams getWindowLayoutParams() { - // Measure how big the hint is since its size depends on the text size. - mCompatUILayout.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED); - final WindowManager.LayoutParams winParams = new WindowManager.LayoutParams( - // Cannot be wrap_content as this determines the actual window size - mCompatUILayout.getMeasuredWidth(), mCompatUILayout.getMeasuredHeight(), - TYPE_APPLICATION_OVERLAY, - FLAG_NOT_FOCUSABLE | FLAG_NOT_TOUCH_MODAL, - PixelFormat.TRANSLUCENT); - winParams.token = new Binder(); - winParams.setTitle(CompatUILayout.class.getSimpleName() + mTaskId); - winParams.privateFlags |= PRIVATE_FLAG_NO_MOVE_ANIMATION | PRIVATE_FLAG_TRUSTED_OVERLAY; - return winParams; + /** + * A class holding the state of the compat UI hints, which is shared between all compat UI + * window managers. + */ + static class CompatUIHintsState { + @VisibleForTesting + boolean mHasShownSizeCompatHint; + @VisibleForTesting + boolean mHasShownCameraCompatHint; } - } 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 new file mode 100644 index 000000000000..face24340a4e --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManagerAbstract.java @@ -0,0 +1,393 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY 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_TOUCH_MODAL; +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 static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE; +import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE; +import static com.android.internal.annotations.VisibleForTesting.Visibility.PROTECTED; + +import android.annotation.Nullable; +import android.app.TaskInfo; +import android.content.Context; +import android.content.res.Configuration; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.os.Binder; +import android.util.Log; +import android.view.IWindow; +import android.view.SurfaceControl; +import android.view.SurfaceControlViewHost; +import android.view.SurfaceSession; +import android.view.View; +import android.view.WindowManager; +import android.view.WindowlessWindowManager; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.common.DisplayLayout; +import com.android.wm.shell.common.SyncTransactionQueue; + +/** + * A superclass for all Compat UI {@link WindowlessWindowManager}s that holds shared logic and + * exposes general API for {@link CompatUIController}. + * + * <p>Holds view hierarchy of a root surface and helps to inflate and manage layout. + */ +public abstract class CompatUIWindowManagerAbstract extends WindowlessWindowManager { + + protected final int mTaskId; + protected Context mContext; + + private final SyncTransactionQueue mSyncQueue; + private final int mDisplayId; + private Configuration mTaskConfig; + private ShellTaskOrganizer.TaskListener mTaskListener; + private DisplayLayout mDisplayLayout; + private final Rect mStableBounds; + + /** + * Utility class for adding and releasing a View hierarchy for this {@link + * WindowlessWindowManager} to {@code mLeash}. + */ + @Nullable + protected SurfaceControlViewHost mViewHost; + + /** + * A surface leash to position the layout relative to the task, since we can't set position for + * the {@code mViewHost} directly. + */ + @Nullable + protected SurfaceControl mLeash; + + protected CompatUIWindowManagerAbstract(Context context, TaskInfo taskInfo, + SyncTransactionQueue syncQueue, ShellTaskOrganizer.TaskListener taskListener, + DisplayLayout displayLayout) { + super(taskInfo.configuration, null /* rootSurface */, null /* hostInputToken */); + mContext = context; + mSyncQueue = syncQueue; + mTaskConfig = taskInfo.configuration; + mDisplayId = mContext.getDisplayId(); + mTaskId = taskInfo.taskId; + mTaskListener = taskListener; + mDisplayLayout = displayLayout; + mStableBounds = new Rect(); + mDisplayLayout.getStableBounds(mStableBounds); + } + + /** + * Returns the z-order of this window which will be passed to the {@link SurfaceControl} once + * {@link #attachToParentSurface} is called. + * + * <p>See {@link SurfaceControl.Transaction#setLayer}. + */ + protected abstract int getZOrder(); + + /** Returns the layout of this window manager. */ + protected abstract @Nullable View getLayout(); + + /** + * Inflates and inits the layout of this window manager on to the root surface if both {@code + * canShow} and {@link #eligibleToShowLayout} are true. + * + * <p>Doesn't do anything if layout is not eligible to be shown. + * + * @param canShow whether the layout is allowed to be shown by the parent controller. + * @return whether the layout is eligible to be shown. + */ + @VisibleForTesting(visibility = PROTECTED) + public boolean createLayout(boolean canShow) { + if (!eligibleToShowLayout()) { + return false; + } + if (!canShow || getLayout() != null) { + // Wait until layout should be visible, or layout was already created. + return true; + } + + if (mViewHost != null) { + throw new IllegalStateException( + "A UI has already been created with this window manager."); + } + + // Construction extracted into separate methods to allow injection for tests. + mViewHost = createSurfaceViewHost(); + mViewHost.setView(createLayout(), getWindowLayoutParams()); + + updateSurfacePosition(); + + return true; + } + + /** Inflates and inits the layout of this window manager. */ + protected abstract View createLayout(); + + protected abstract void removeLayout(); + + /** + * Whether the layout is eligible to be shown according to the internal state of the subclass. + */ + protected abstract boolean eligibleToShowLayout(); + + @Override + public void setConfiguration(Configuration configuration) { + super.setConfiguration(configuration); + mContext = mContext.createConfigurationContext(configuration); + } + + @Override + protected void attachToParentSurface(IWindow window, SurfaceControl.Builder b) { + String className = getClass().getSimpleName(); + final SurfaceControl.Builder builder = new SurfaceControl.Builder(new SurfaceSession()) + .setContainerLayer() + .setName(className + "Leash") + .setHidden(false) + .setCallsite(className + "#attachToParentSurface"); + attachToParentSurface(builder); + mLeash = builder.build(); + b.setParent(mLeash); + + initSurface(mLeash); + } + + /** Inits the z-order of the surface. */ + private void initSurface(SurfaceControl leash) { + final int z = getZOrder(); + mSyncQueue.runInSync(t -> { + if (leash == null || !leash.isValid()) { + Log.w(getTag(), "The leash has been released."); + return; + } + t.setLayer(leash, z); + }); + } + + /** + * Called when compat info changed. + * + * <p>The window manager is released if the layout is no longer eligible to be shown. + * + * @param canShow whether the layout is allowed to be shown by the parent controller. + * @return whether the layout is eligible to be shown. + */ + @VisibleForTesting(visibility = PROTECTED) + public boolean updateCompatInfo(TaskInfo taskInfo, + ShellTaskOrganizer.TaskListener taskListener, boolean canShow) { + final Configuration prevTaskConfig = mTaskConfig; + final ShellTaskOrganizer.TaskListener prevTaskListener = mTaskListener; + mTaskConfig = taskInfo.configuration; + mTaskListener = taskListener; + + // Update configuration. + setConfiguration(mTaskConfig); + + if (!eligibleToShowLayout()) { + release(); + return false; + } + + View layout = getLayout(); + if (layout == null || prevTaskListener != taskListener) { + // Layout wasn't created yet or TaskListener changed, recreate the layout for new + // surface parent. + release(); + return createLayout(canShow); + } + + boolean boundsUpdated = !mTaskConfig.windowConfiguration.getBounds().equals( + prevTaskConfig.windowConfiguration.getBounds()); + boolean layoutDirectionUpdated = + mTaskConfig.getLayoutDirection() != prevTaskConfig.getLayoutDirection(); + if (boundsUpdated || layoutDirectionUpdated) { + onParentBoundsChanged(); + } + + if (layout != null && layoutDirectionUpdated) { + // Update layout for RTL. + layout.setLayoutDirection(mTaskConfig.getLayoutDirection()); + } + + return true; + } + + /** + * Updates the visibility of the layout. + * + * @param canShow whether the layout is allowed to be shown by the parent controller. + */ + @VisibleForTesting(visibility = PACKAGE) + public void updateVisibility(boolean canShow) { + View layout = getLayout(); + if (layout == null) { + // Layout may not have been created because it was hidden previously. + createLayout(canShow); + return; + } + + final int newVisibility = canShow && eligibleToShowLayout() ? View.VISIBLE : View.GONE; + if (layout.getVisibility() != newVisibility) { + layout.setVisibility(newVisibility); + } + } + + /** Called when display layout changed. */ + @VisibleForTesting(visibility = PACKAGE) + public void updateDisplayLayout(DisplayLayout displayLayout) { + final Rect prevStableBounds = mStableBounds; + final Rect curStableBounds = new Rect(); + displayLayout.getStableBounds(curStableBounds); + mDisplayLayout = displayLayout; + if (!prevStableBounds.equals(curStableBounds)) { + // mStableBounds should be updated before we call onParentBoundsChanged. + mStableBounds.set(curStableBounds); + onParentBoundsChanged(); + } + } + + /** Called when the surface is ready to be placed under the task surface. */ + @VisibleForTesting(visibility = PRIVATE) + void attachToParentSurface(SurfaceControl.Builder b) { + mTaskListener.attachChildSurfaceToTask(mTaskId, b); + } + + public int getDisplayId() { + return mDisplayId; + } + + public int getTaskId() { + return mTaskId; + } + + /** Releases the surface control and tears down the view hierarchy. */ + public void release() { + // Hiding before releasing to avoid flickering when transitioning to the Home screen. + View layout = getLayout(); + if (layout != null) { + layout.setVisibility(View.GONE); + } + removeLayout(); + + if (mViewHost != null) { + mViewHost.release(); + mViewHost = null; + } + + if (mLeash != null) { + final SurfaceControl leash = mLeash; + mSyncQueue.runInSync(t -> t.remove(leash)); + mLeash = null; + } + } + + /** Re-layouts the view host and updates the surface position. */ + void relayout() { + relayout(getWindowLayoutParams()); + } + + protected void relayout(WindowManager.LayoutParams windowLayoutParams) { + if (mViewHost == null) { + return; + } + mViewHost.relayout(windowLayoutParams); + updateSurfacePosition(); + } + + /** + * Called following a change in the task bounds, display layout stable bounds, or the layout + * direction. + */ + protected void onParentBoundsChanged() { + updateSurfacePosition(); + } + + /** + * Updates the position of the surface with respect to the parent bounds. + */ + protected abstract void updateSurfacePosition(); + + /** + * Updates the position of the surface with respect to the given {@code positionX} and {@code + * positionY}. + */ + protected void updateSurfacePosition(int positionX, int positionY) { + if (mLeash == null) { + return; + } + mSyncQueue.runInSync(t -> { + if (mLeash == null || !mLeash.isValid()) { + Log.w(getTag(), "The leash has been released."); + return; + } + t.setPosition(mLeash, positionX, positionY); + }); + } + + protected int getLayoutDirection() { + return mContext.getResources().getConfiguration().getLayoutDirection(); + } + + protected Rect getTaskBounds() { + return mTaskConfig.windowConfiguration.getBounds(); + } + + /** Returns the intersection between the task bounds and the display layout stable bounds. */ + protected Rect getTaskStableBounds() { + final Rect result = new Rect(mStableBounds); + result.intersect(getTaskBounds()); + return result; + } + + /** Creates a {@link SurfaceControlViewHost} for this window manager. */ + @VisibleForTesting(visibility = PRIVATE) + public SurfaceControlViewHost createSurfaceViewHost() { + return new SurfaceControlViewHost(mContext, mContext.getDisplay(), this); + } + + /** 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. + layout.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED); + return getWindowLayoutParams(layout.getMeasuredWidth(), layout.getMeasuredHeight()); + } + + /** Gets the layout params given the width and height of the layout. */ + protected WindowManager.LayoutParams getWindowLayoutParams(int width, int height) { + final WindowManager.LayoutParams winParams = new WindowManager.LayoutParams( + // Cannot be wrap_content as this determines the actual window size + width, height, + TYPE_APPLICATION_OVERLAY, + FLAG_NOT_FOCUSABLE | FLAG_NOT_TOUCH_MODAL, + PixelFormat.TRANSLUCENT); + winParams.token = new Binder(); + winParams.setTitle(getClass().getSimpleName() + mTaskId); + winParams.privateFlags |= PRIVATE_FLAG_NO_MOVE_ANIMATION | PRIVATE_FLAG_TRUSTED_OVERLAY; + return winParams; + } + + 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/letterboxedu/LetterboxEduAnimationController.java new file mode 100644 index 000000000000..3061eab17d24 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduAnimationController.java @@ -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.compatui.letterboxedu; + +import static com.android.internal.R.styleable.WindowAnimation_windowEnterAnimation; +import static com.android.internal.R.styleable.WindowAnimation_windowExitAnimation; +import static com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ObjectAnimator; +import android.annotation.AnyRes; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.util.IntProperty; +import android.util.Log; +import android.util.Property; +import android.view.ContextThemeWrapper; +import android.view.View; +import android.view.animation.Animation; + +import com.android.internal.policy.TransitionAnimation; + +/** + * Controls the enter/exit animations of the letterbox education. + */ +class LetterboxEduAnimationController { + private static final String TAG = "LetterboxEduAnimation"; + + // If shell transitions are enabled, startEnterAnimation will be called after all transitions + // have finished, and therefore the start delay should be shorter. + private static final int ENTER_ANIM_START_DELAY_MILLIS = ENABLE_SHELL_TRANSITIONS ? 300 : 500; + + private final TransitionAnimation mTransitionAnimation; + private final String mPackageName; + @AnyRes + private final int mAnimStyleResId; + + @Nullable + private Animation mDialogAnimation; + @Nullable + private Animator mBackgroundDimAnimator; + + LetterboxEduAnimationController(Context context) { + 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(); + } + + /** + * Starts both background dim fade-in animation and the dialog enter animation. + */ + void startEnterAnimation(@NonNull LetterboxEduDialogLayout layout, Runnable endCallback) { + // Cancel any previous animation if it's still running. + cancelAnimation(); + + final View dialogContainer = layout.getDialogContainer(); + mDialogAnimation = loadAnimation(WindowAnimation_windowEnterAnimation); + if (mDialogAnimation == null) { + endCallback.run(); + return; + } + mDialogAnimation.setAnimationListener(getAnimationListener( + /* startCallback= */ () -> dialogContainer.setAlpha(1), + /* endCallback= */ () -> { + mDialogAnimation = null; + endCallback.run(); + })); + + mBackgroundDimAnimator = getAlphaAnimator(layout.getBackgroundDim(), + /* endAlpha= */ LetterboxEduDialogLayout.BACKGROUND_DIM_ALPHA, + mDialogAnimation.getDuration()); + mBackgroundDimAnimator.addListener(getDimAnimatorListener()); + + mDialogAnimation.setStartOffset(ENTER_ANIM_START_DELAY_MILLIS); + mBackgroundDimAnimator.setStartDelay(ENTER_ANIM_START_DELAY_MILLIS); + + dialogContainer.startAnimation(mDialogAnimation); + mBackgroundDimAnimator.start(); + } + + /** + * Starts both the background dim fade-out animation and the dialog exit animation. + */ + void startExitAnimation(@NonNull LetterboxEduDialogLayout layout, Runnable endCallback) { + // Cancel any previous animation if it's still running. + cancelAnimation(); + + final View dialogContainer = layout.getDialogContainer(); + mDialogAnimation = loadAnimation(WindowAnimation_windowExitAnimation); + if (mDialogAnimation == null) { + endCallback.run(); + return; + } + mDialogAnimation.setAnimationListener(getAnimationListener( + /* startCallback= */ () -> {}, + /* endCallback= */ () -> { + dialogContainer.setAlpha(0); + mDialogAnimation = null; + endCallback.run(); + })); + + mBackgroundDimAnimator = getAlphaAnimator(layout.getBackgroundDim(), /* endAlpha= */ 0, + mDialogAnimation.getDuration()); + mBackgroundDimAnimator.addListener(getDimAnimatorListener()); + + dialogContainer.startAnimation(mDialogAnimation); + mBackgroundDimAnimator.start(); + } + + /** + * Cancels all animations and resets the state of the controller. + */ + void cancelAnimation() { + if (mDialogAnimation != null) { + mDialogAnimation.cancel(); + mDialogAnimation = null; + } + if (mBackgroundDimAnimator != null) { + mBackgroundDimAnimator.cancel(); + mBackgroundDimAnimator = null; + } + } + + private Animation loadAnimation(int animAttr) { + Animation animation = mTransitionAnimation.loadAnimationAttr(mPackageName, mAnimStyleResId, + animAttr, /* translucent= */ false); + if (animation == null) { + Log.e(TAG, "Failed to load animation " + animAttr); + } + return animation; + } + + private Animation.AnimationListener getAnimationListener(Runnable startCallback, + Runnable endCallback) { + return new Animation.AnimationListener() { + @Override + public void onAnimationStart(Animation animation) { + startCallback.run(); + } + + @Override + public void onAnimationEnd(Animation animation) { + endCallback.run(); + } + + @Override + public void onAnimationRepeat(Animation animation) {} + }; + } + + private AnimatorListenerAdapter getDimAnimatorListener() { + return new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mBackgroundDimAnimator = null; + } + }; + } + + private static Animator getAlphaAnimator( + Drawable drawable, int endAlpha, long duration) { + Animator animator = ObjectAnimator.ofInt(drawable, DRAWABLE_ALPHA, endAlpha); + animator.setDuration(duration); + return animator; + } + + private static final Property<Drawable, Integer> DRAWABLE_ALPHA = new IntProperty<Drawable>( + "alpha") { + @Override + public void setValue(Drawable object, int value) { + object.setAlpha(value); + } + + @Override + public Integer get(Drawable object) { + return object.getAlpha(); + } + }; +} 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/letterboxedu/LetterboxEduDialogActionLayout.java new file mode 100644 index 000000000000..02197f644a39 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduDialogActionLayout.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.letterboxedu; + +import android.content.Context; +import android.content.res.TypedArray; +import android.util.AttributeSet; +import android.view.View; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.TextView; + +import com.android.wm.shell.R; + +/** + * Custom layout for Letterbox Education dialog action. + */ +class LetterboxEduDialogActionLayout extends FrameLayout { + + public LetterboxEduDialogActionLayout(Context context) { + this(context, null); + } + + public LetterboxEduDialogActionLayout(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public LetterboxEduDialogActionLayout(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public LetterboxEduDialogActionLayout(Context context, AttributeSet attrs, int defStyleAttr, + int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + + TypedArray styledAttributes = + context.getTheme().obtainStyledAttributes( + attrs, R.styleable.LetterboxEduDialogActionLayout, defStyleAttr, + defStyleRes); + int iconId = styledAttributes.getResourceId( + R.styleable.LetterboxEduDialogActionLayout_icon, 0); + String text = styledAttributes.getString( + R.styleable.LetterboxEduDialogActionLayout_text); + styledAttributes.recycle(); + + View rootView = inflate(getContext(), R.layout.letterbox_education_dialog_action_layout, + this); + ((ImageView) rootView.findViewById( + R.id.letterbox_education_dialog_action_icon)).setImageResource(iconId); + ((TextView) rootView.findViewById(R.id.letterbox_education_dialog_action_text)).setText( + text); + } +} 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/letterboxedu/LetterboxEduDialogLayout.java new file mode 100644 index 000000000000..2e0b09e9d230 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduDialogLayout.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.compatui.letterboxedu; + +import android.annotation.Nullable; +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.view.View; +import android.widget.TextView; + +import androidx.constraintlayout.widget.ConstraintLayout; + +import com.android.wm.shell.R; + +/** + * Container for Letterbox Education Dialog and background dim. + * + * <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; + + private View mDialogContainer; + private TextView mDialogTitle; + private Drawable mBackgroundDim; + + public LetterboxEduDialogLayout(Context context) { + this(context, null); + } + + public LetterboxEduDialogLayout(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public LetterboxEduDialogLayout(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public LetterboxEduDialogLayout(Context context, AttributeSet attrs, int defStyleAttr, + int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + View getDialogContainer() { + return mDialogContainer; + } + + TextView getDialogTitle() { + return mDialogTitle; + } + + Drawable getBackgroundDim() { + 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_education_dialog_dismiss_button).setOnClickListener(listener); + // Clicks on the background dim should also dismiss the dialog. + setOnClickListener(listener); + // 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(callback == null ? null : view -> {}); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mDialogContainer = findViewById(R.id.letterbox_education_dialog_container); + mDialogTitle = findViewById(R.id.letterbox_education_dialog_title); + mBackgroundDim = getBackground().mutate(); + // Set the alpha of the background dim to 0 for enter animation. + mBackgroundDim.setAlpha(0); + } +} 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/letterboxedu/LetterboxEduWindowManager.java new file mode 100644 index 000000000000..35f1038a6853 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduWindowManager.java @@ -0,0 +1,260 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.compatui.letterboxedu; + +import static android.provider.Settings.Secure.LAUNCHER_TASKBAR_EDUCATION_SHOWING; + +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.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup.MarginLayoutParams; +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.compatui.CompatUIWindowManagerAbstract; +import com.android.wm.shell.transition.Transitions; + +/** + * Window manager for the Letterbox Education. + */ +public class LetterboxEduWindowManager extends CompatUIWindowManagerAbstract { + + /** + * The Letterbox Education should be the topmost child of the Task in case there can be more + * than one child. + */ + 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 Transitions mTransitions; + + /** + * The id of the current user, to associate with a boolean in {@link + * #HAS_SEEN_LETTERBOX_EDUCATION_PREF_NAME}, indicating whether that user has already seen the + * Letterbox Education dialog. + */ + private final int mUserId; + + // Remember the last reported state in case visibility changes due to keyguard or IME updates. + private boolean mEligibleForLetterboxEducation; + + @Nullable + @VisibleForTesting + LetterboxEduDialogLayout mLayout; + + private final Runnable mOnDismissCallback; + + /** + * The vertical margin between the dialog container and the task stable bounds (excluding + * insets). + */ + private final int mDialogVerticalMargin; + + public LetterboxEduWindowManager(Context context, TaskInfo taskInfo, + SyncTransactionQueue syncQueue, ShellTaskOrganizer.TaskListener taskListener, + DisplayLayout displayLayout, Transitions transitions, + Runnable onDismissCallback) { + this(context, taskInfo, syncQueue, taskListener, displayLayout, transitions, + onDismissCallback, new LetterboxEduAnimationController(context)); + } + + @VisibleForTesting + LetterboxEduWindowManager(Context context, TaskInfo taskInfo, + SyncTransactionQueue syncQueue, ShellTaskOrganizer.TaskListener taskListener, + DisplayLayout displayLayout, Transitions transitions, Runnable onDismissCallback, + LetterboxEduAnimationController animationController) { + super(context, taskInfo, syncQueue, taskListener, displayLayout); + 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); + } + + @Override + protected int getZOrder() { + return Z_ORDER; + } + + @Override + protected @Nullable View getLayout() { + return mLayout; + } + + @Override + protected void removeLayout() { + mLayout = null; + } + + @Override + protected boolean eligibleToShowLayout() { + // - 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()); + } + + @Override + protected View createLayout() { + mLayout = inflateLayout(); + updateDialogMargins(); + + // startEnterAnimation will be called immediately if shell-transitions are disabled. + mTransitions.runOnIdle(this::startEnterAnimation); + + return mLayout; + } + + private void updateDialogMargins() { + if (mLayout == null) { + return; + } + final View dialogContainer = mLayout.getDialogContainer(); + MarginLayoutParams marginParams = (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 LetterboxEduDialogLayout inflateLayout() { + return (LetterboxEduDialogLayout) LayoutInflater.from(mContext).inflate( + R.layout.letterbox_education_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; + } + setSeenLetterboxEducation(); + mLayout.setDismissOnClickListener(this::onDismiss); + // 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.run(); + }); + } + + @Override + public void release() { + mAnimationController.cancelAnimation(); + super.release(); + } + + @Override + public boolean updateCompatInfo(TaskInfo taskInfo, ShellTaskOrganizer.TaskListener taskListener, + boolean canShow) { + mEligibleForLetterboxEducation = taskInfo.topActivityEligibleForLetterboxEducation; + + return super.updateCompatInfo(taskInfo, taskListener, canShow); + } + + @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()); + } + + 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(), + LAUNCHER_TASKBAR_EDUCATION_SHOWING, /* def= */ 0) == 1; + } +} 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 711a0ac76702..1ea5e21a2c1e 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 @@ -18,6 +18,7 @@ package com.android.wm.shell.dagger; import android.content.Context; import android.os.Handler; +import android.os.SystemClock; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.WindowManagerShellWrapper; @@ -27,21 +28,24 @@ import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.common.SystemWindows; import com.android.wm.shell.common.TaskStackListenerImpl; import com.android.wm.shell.common.annotations.ShellMainThread; -import com.android.wm.shell.legacysplitscreen.LegacySplitScreenController; import com.android.wm.shell.pip.Pip; import com.android.wm.shell.pip.PipAnimationController; -import com.android.wm.shell.pip.PipBoundsAlgorithm; -import com.android.wm.shell.pip.PipBoundsState; +import com.android.wm.shell.pip.PipAppOpsListener; 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.PipSurfaceTransactionHelper; 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.tv.TvPipBoundsAlgorithm; +import com.android.wm.shell.pip.tv.TvPipBoundsController; +import com.android.wm.shell.pip.tv.TvPipBoundsState; import com.android.wm.shell.pip.tv.TvPipController; import com.android.wm.shell.pip.tv.TvPipMenuController; 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.transition.Transitions; @@ -60,48 +64,71 @@ public abstract class TvPipModule { @Provides static Optional<Pip> providePip( Context context, - PipBoundsState pipBoundsState, - PipBoundsAlgorithm pipBoundsAlgorithm, + TvPipBoundsState tvPipBoundsState, + TvPipBoundsAlgorithm tvPipBoundsAlgorithm, + TvPipBoundsController tvPipBoundsController, + PipAppOpsListener pipAppOpsListener, PipTaskOrganizer pipTaskOrganizer, TvPipMenuController tvPipMenuController, PipMediaController pipMediaController, PipTransitionController pipTransitionController, TvPipNotificationController tvPipNotificationController, TaskStackListenerImpl taskStackListener, + PipParamsChangedForwarder pipParamsChangedForwarder, + DisplayController displayController, WindowManagerShellWrapper windowManagerShellWrapper, @ShellMainThread ShellExecutor mainExecutor) { return Optional.of( TvPipController.create( context, - pipBoundsState, - pipBoundsAlgorithm, + tvPipBoundsState, + tvPipBoundsAlgorithm, + tvPipBoundsController, + pipAppOpsListener, pipTaskOrganizer, pipTransitionController, tvPipMenuController, pipMediaController, tvPipNotificationController, taskStackListener, + pipParamsChangedForwarder, + displayController, windowManagerShellWrapper, mainExecutor)); } @WMSingleton @Provides + static TvPipBoundsController provideTvPipBoundsController( + Context context, + @ShellMainThread Handler mainHandler, + TvPipBoundsState tvPipBoundsState, + TvPipBoundsAlgorithm tvPipBoundsAlgorithm) { + return new TvPipBoundsController( + context, + SystemClock::uptimeMillis, + mainHandler, + tvPipBoundsState, + tvPipBoundsAlgorithm); + } + + @WMSingleton + @Provides static PipSnapAlgorithm providePipSnapAlgorithm() { return new PipSnapAlgorithm(); } @WMSingleton @Provides - static PipBoundsAlgorithm providePipBoundsAlgorithm(Context context, - PipBoundsState pipBoundsState, PipSnapAlgorithm pipSnapAlgorithm) { - return new PipBoundsAlgorithm(context, pipBoundsState, pipSnapAlgorithm); + static TvPipBoundsAlgorithm provideTvPipBoundsAlgorithm(Context context, + TvPipBoundsState tvPipBoundsState, PipSnapAlgorithm pipSnapAlgorithm) { + return new TvPipBoundsAlgorithm(context, tvPipBoundsState, pipSnapAlgorithm); } @WMSingleton @Provides - static PipBoundsState providePipBoundsState(Context context) { - return new PipBoundsState(context); + static TvPipBoundsState provideTvPipBoundsState(Context context) { + return new TvPipBoundsState(context); } // Handler needed for loadDrawableAsync() in PipControlsViewController @@ -109,21 +136,22 @@ public abstract class TvPipModule { @Provides static PipTransitionController provideTvPipTransition( Transitions transitions, ShellTaskOrganizer shellTaskOrganizer, - PipAnimationController pipAnimationController, PipBoundsAlgorithm pipBoundsAlgorithm, - PipBoundsState pipBoundsState, TvPipMenuController pipMenuController) { - return new TvPipTransition(pipBoundsState, pipMenuController, - pipBoundsAlgorithm, pipAnimationController, transitions, shellTaskOrganizer); + PipAnimationController pipAnimationController, + TvPipBoundsAlgorithm tvPipBoundsAlgorithm, + TvPipBoundsState tvPipBoundsState, TvPipMenuController pipMenuController) { + return new TvPipTransition(tvPipBoundsState, pipMenuController, + tvPipBoundsAlgorithm, pipAnimationController, transitions, shellTaskOrganizer); } @WMSingleton @Provides static TvPipMenuController providesTvPipMenuController( Context context, - PipBoundsState pipBoundsState, + TvPipBoundsState tvPipBoundsState, SystemWindows systemWindows, PipMediaController pipMediaController, @ShellMainThread Handler mainHandler) { - return new TvPipMenuController(context, pipBoundsState, systemWindows, pipMediaController, + return new TvPipMenuController(context, tvPipBoundsState, systemWindows, pipMediaController, mainHandler); } @@ -132,8 +160,11 @@ public abstract class TvPipModule { @Provides static TvPipNotificationController provideTvPipNotificationController(Context context, PipMediaController pipMediaController, + PipParamsChangedForwarder pipParamsChangedForwarder, + TvPipBoundsState tvPipBoundsState, @ShellMainThread Handler mainHandler) { - return new TvPipNotificationController(context, pipMediaController, mainHandler); + return new TvPipNotificationController(context, pipMediaController, + pipParamsChangedForwarder, tvPipBoundsState, mainHandler); } @WMSingleton @@ -154,21 +185,35 @@ public abstract class TvPipModule { static PipTaskOrganizer providePipTaskOrganizer(Context context, TvPipMenuController tvPipMenuController, SyncTransactionQueue syncTransactionQueue, - PipBoundsState pipBoundsState, + TvPipBoundsState tvPipBoundsState, PipTransitionState pipTransitionState, - PipBoundsAlgorithm pipBoundsAlgorithm, + TvPipBoundsAlgorithm tvPipBoundsAlgorithm, PipAnimationController pipAnimationController, PipTransitionController pipTransitionController, + PipParamsChangedForwarder pipParamsChangedForwarder, PipSurfaceTransactionHelper pipSurfaceTransactionHelper, - Optional<LegacySplitScreenController> splitScreenOptional, - Optional<SplitScreenController> newSplitScreenOptional, + Optional<SplitScreenController> splitScreenControllerOptional, DisplayController displayController, PipUiEventLogger pipUiEventLogger, ShellTaskOrganizer shellTaskOrganizer, @ShellMainThread ShellExecutor mainExecutor) { - return new PipTaskOrganizer(context, - syncTransactionQueue, pipTransitionState, pipBoundsState, pipBoundsAlgorithm, + return new TvPipTaskOrganizer(context, + syncTransactionQueue, pipTransitionState, tvPipBoundsState, tvPipBoundsAlgorithm, tvPipMenuController, pipAnimationController, pipSurfaceTransactionHelper, - pipTransitionController, splitScreenOptional, newSplitScreenOptional, + pipTransitionController, pipParamsChangedForwarder, splitScreenControllerOptional, displayController, pipUiEventLogger, shellTaskOrganizer, mainExecutor); } + + @WMSingleton + @Provides + static PipParamsChangedForwarder providePipParamsChangedForwarder() { + return new PipParamsChangedForwarder(); + } + + @WMSingleton + @Provides + static PipAppOpsListener providePipAppOpsListener(Context context, + PipTaskOrganizer pipTaskOrganizer, + @ShellMainThread ShellExecutor mainExecutor) { + return new PipAppOpsListener(context, pipTaskOrganizer::removePip, mainExecutor); + } } 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 6d4b2fa60b55..db6131a17114 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 @@ -36,9 +36,12 @@ 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.back.BackAnimation; +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.DisplayController; @@ -52,6 +55,7 @@ import com.android.wm.shell.common.SystemWindows; import com.android.wm.shell.common.TaskStackListenerImpl; import com.android.wm.shell.common.TransactionPool; import com.android.wm.shell.common.annotations.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; @@ -65,6 +69,7 @@ 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; @@ -73,7 +78,6 @@ import com.android.wm.shell.pip.Pip; import com.android.wm.shell.pip.PipMediaController; import com.android.wm.shell.pip.PipSurfaceTransactionHelper; import com.android.wm.shell.pip.PipUiEventLogger; -import com.android.wm.shell.pip.phone.PipAppOpsListener; import com.android.wm.shell.pip.phone.PipTouchHandler; import com.android.wm.shell.recents.RecentTasks; import com.android.wm.shell.recents.RecentTasksController; @@ -88,10 +92,12 @@ import com.android.wm.shell.tasksurfacehelper.TaskSurfaceHelperController; 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.UnfoldTransitionHandler; import java.util.Optional; import dagger.BindsOptionalOf; +import dagger.Lazy; import dagger.Module; import dagger.Provides; @@ -165,8 +171,8 @@ public abstract class WMShellBaseModule { @WMSingleton @Provides - static DragAndDrop provideDragAndDrop(DragAndDropController dragAndDropController) { - return dragAndDropController.asDragAndDrop(); + static Optional<DragAndDrop> provideDragAndDrop(DragAndDropController dragAndDropController) { + return Optional.of(dragAndDropController.asDragAndDrop()); } @WMSingleton @@ -181,8 +187,22 @@ public abstract class WMShellBaseModule { @WMSingleton @Provides - static CompatUI provideCompatUI(CompatUIController compatUIController) { - return compatUIController.asCompatUI(); + static KidsModeTaskOrganizer provideKidsModeTaskOrganizer( + @ShellMainThread ShellExecutor mainExecutor, + @ShellMainThread Handler mainHandler, + Context context, + SyncTransactionQueue syncTransactionQueue, + DisplayController displayController, + DisplayInsetsController displayInsetsController, + Optional<RecentTasksController> recentTasksOptional + ) { + return new KidsModeTaskOrganizer(mainExecutor, mainHandler, context, syncTransactionQueue, + displayController, displayInsetsController, recentTasksOptional); + } + + @WMSingleton + @Provides static Optional<CompatUI> provideCompatUI(CompatUIController compatUIController) { + return Optional.of(compatUIController.asCompatUI()); } @WMSingleton @@ -190,9 +210,9 @@ public abstract class WMShellBaseModule { static CompatUIController provideCompatUIController(Context context, DisplayController displayController, DisplayInsetsController displayInsetsController, DisplayImeController imeController, SyncTransactionQueue syncQueue, - @ShellMainThread ShellExecutor mainExecutor) { + @ShellMainThread ShellExecutor mainExecutor, Lazy<Transitions> transitionsLazy) { return new CompatUIController(context, displayController, displayInsetsController, - imeController, syncQueue, mainExecutor); + imeController, syncQueue, mainExecutor, transitionsLazy); } @WMSingleton @@ -237,6 +257,17 @@ public abstract class WMShellBaseModule { } // + // Back animation + // + + @WMSingleton + @Provides + static Optional<BackAnimation> provideBackAnimation( + Optional<BackAnimationController> backAnimationController) { + return backAnimationController.map(BackAnimationController::getBackAnimationImpl); + } + + // // Bubbles (optional feature) // @@ -253,15 +284,24 @@ public abstract class WMShellBaseModule { // Fullscreen // + // Workaround for dynamic overriding with a default implementation, see {@link DynamicOverride} + @BindsOptionalOf + @DynamicOverride + abstract FullscreenTaskListener optionalFullscreenTaskListener(); + @WMSingleton @Provides static FullscreenTaskListener provideFullscreenTaskListener( + @DynamicOverride Optional<FullscreenTaskListener> fullscreenTaskListener, SyncTransactionQueue syncQueue, Optional<FullscreenUnfoldController> optionalFullscreenUnfoldController, - Optional<RecentTasksController> recentTasksOptional - ) { - return new FullscreenTaskListener(syncQueue, optionalFullscreenUnfoldController, - recentTasksOptional); + Optional<RecentTasksController> recentTasksOptional) { + if (fullscreenTaskListener.isPresent()) { + return fullscreenTaskListener.get(); + } else { + return new FullscreenTaskListener(syncQueue, optionalFullscreenUnfoldController, + recentTasksOptional); + } } // @@ -288,6 +328,21 @@ public abstract class WMShellBaseModule { return Optional.empty(); } + @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)); + } + return Optional.empty(); + } + // // Freeform (optional feature) // @@ -352,7 +407,6 @@ public abstract class WMShellBaseModule { return Optional.empty(); } - // // Task to Surface communication // @@ -380,14 +434,6 @@ public abstract class WMShellBaseModule { return new FloatingContentCoordinator(); } - @WMSingleton - @Provides - static PipAppOpsListener providePipAppOpsListener(Context context, - PipTouchHandler pipTouchHandler, - @ShellMainThread ShellExecutor mainExecutor) { - return new PipAppOpsListener(context, pipTouchHandler.getMotionHelper(), mainExecutor); - } - // Needs handler for registering broadcast receivers @WMSingleton @Provides @@ -449,9 +495,16 @@ public abstract class WMShellBaseModule { static Transitions provideTransitions(ShellTaskOrganizer organizer, TransactionPool pool, DisplayController displayController, Context context, @ShellMainThread ShellExecutor mainExecutor, + @ShellMainThread Handler mainHandler, @ShellAnimationThread ShellExecutor animExecutor) { return new Transitions(organizer, pool, displayController, context, mainExecutor, - animExecutor); + mainHandler, animExecutor); + } + + @WMSingleton + @Provides + static TaskViewTransitions provideTaskViewTransitions(Transitions transitions) { + return new TaskViewTransitions(transitions); } // @@ -585,8 +638,10 @@ public abstract class WMShellBaseModule { static TaskViewFactoryController provideTaskViewFactoryController( ShellTaskOrganizer shellTaskOrganizer, @ShellMainThread ShellExecutor mainExecutor, - SyncTransactionQueue syncQueue) { - return new TaskViewFactoryController(shellTaskOrganizer, mainExecutor, syncQueue); + SyncTransactionQueue syncQueue, + TaskViewTransitions taskViewTransitions) { + return new TaskViewFactoryController(shellTaskOrganizer, mainExecutor, syncQueue, + taskViewTransitions); } // @@ -606,12 +661,14 @@ public abstract class WMShellBaseModule { DisplayInsetsController displayInsetsController, DragAndDropController dragAndDropController, ShellTaskOrganizer shellTaskOrganizer, + KidsModeTaskOrganizer kidsModeTaskOrganizer, Optional<BubbleController> bubblesOptional, Optional<SplitScreenController> splitScreenOptional, Optional<AppPairsController> appPairsOptional, Optional<PipTouchHandler> pipTouchHandlerOptional, FullscreenTaskListener fullscreenTaskListener, Optional<FullscreenUnfoldController> appUnfoldTransitionController, + Optional<UnfoldTransitionHandler> unfoldTransitionHandler, Optional<FreeformTaskListener> freeformTaskListener, Optional<RecentTasksController> recentTasksOptional, Transitions transitions, @@ -622,12 +679,14 @@ public abstract class WMShellBaseModule { displayInsetsController, dragAndDropController, shellTaskOrganizer, + kidsModeTaskOrganizer, bubblesOptional, splitScreenOptional, appPairsOptional, pipTouchHandlerOptional, fullscreenTaskListener, appUnfoldTransitionController, + unfoldTransitionHandler, freeformTaskListener, recentTasksOptional, transitions, @@ -649,6 +708,7 @@ public abstract class WMShellBaseModule { @Provides static ShellCommandHandlerImpl provideShellCommandHandlerImpl( ShellTaskOrganizer shellTaskOrganizer, + KidsModeTaskOrganizer kidsModeTaskOrganizer, Optional<LegacySplitScreenController> legacySplitScreenOptional, Optional<SplitScreenController> splitScreenOptional, Optional<Pip> pipOptional, @@ -657,8 +717,22 @@ public abstract class WMShellBaseModule { Optional<AppPairsController> appPairsOptional, Optional<RecentTasksController> recentTasksOptional, @ShellMainThread ShellExecutor mainExecutor) { - return new ShellCommandHandlerImpl(shellTaskOrganizer, + return new ShellCommandHandlerImpl(shellTaskOrganizer, kidsModeTaskOrganizer, legacySplitScreenOptional, splitScreenOptional, pipOptional, oneHandedOptional, hideDisplayCutout, appPairsOptional, recentTasksOptional, mainExecutor); } + + @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(); + } } 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 5c205f97beb7..cc741d3896a2 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 @@ -16,6 +16,7 @@ package com.android.wm.shell.dagger; +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; @@ -27,15 +28,18 @@ import android.os.HandlerThread; import android.os.Looper; import android.os.Trace; +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; import com.android.wm.shell.common.annotations.ShellMainThread; import com.android.wm.shell.common.annotations.ShellSplashscreenThread; -import com.android.wm.shell.R; import dagger.Module; import dagger.Provides; @@ -53,7 +57,7 @@ public abstract class WMShellConcurrencyModule { /** * Returns whether to enable a separate shell thread for the shell features. */ - private static boolean enableShellMainThread(Context context) { + public static boolean enableShellMainThread(Context context) { return context.getResources().getBoolean(R.bool.config_enableShellMainThread); } @@ -85,23 +89,41 @@ public abstract class WMShellConcurrencyModule { } /** + * Creates a shell main thread to be injected into the shell components. This does not provide + * the {@param HandleThread}, but is used to create the thread prior to initializing the + * WM component, and is explicitly bound. + * + * See {@link com.android.systemui.SystemUIFactory#init(Context, boolean)}. + */ + public static HandlerThread createShellMainThread() { + HandlerThread mainThread = new HandlerThread("wmshell.main", THREAD_PRIORITY_DISPLAY); + return mainThread; + } + + /** * Shell main-thread Handler, don't use this unless really necessary (ie. need to dedupe * multiple types of messages, etc.) + * + * @param mainThread If non-null, this thread is expected to be started already */ @WMSingleton @Provides @ShellMainThread public static Handler provideShellMainHandler(Context context, + @Nullable @ShellMainThread HandlerThread mainThread, @ExternalMainThread Handler sysuiMainHandler) { if (enableShellMainThread(context)) { - HandlerThread mainThread = new HandlerThread("wmshell.main", THREAD_PRIORITY_DISPLAY); - mainThread.start(); - if (Build.IS_DEBUGGABLE) { - mainThread.getLooper().setTraceTag(Trace.TRACE_TAG_WINDOW_MANAGER); - mainThread.getLooper().setSlowLogThresholdMs(MSGQ_SLOW_DISPATCH_THRESHOLD_MS, - MSGQ_SLOW_DELIVERY_THRESHOLD_MS); - } - return Handler.createAsync(mainThread.getLooper()); + if (mainThread == null) { + // If this thread wasn't pre-emptively started, then create and start it + mainThread = createShellMainThread(); + mainThread.start(); + } + if (Build.IS_DEBUGGABLE) { + mainThread.getLooper().setTraceTag(Trace.TRACE_TAG_WINDOW_MANAGER); + mainThread.getLooper().setSlowLogThresholdMs(MSGQ_SLOW_DISPATCH_THRESHOLD_MS, + MSGQ_SLOW_DELIVERY_THRESHOLD_MS); + } + return Handler.createAsync(mainThread.getLooper()); } return sysuiMainHandler; } @@ -175,4 +197,28 @@ public abstract class WMShellConcurrencyModule { throw new RuntimeException("Failed to initialize SfVsync animation handler in 1s", e); } } + + /** + * Provides a Shell background thread Handler for low priority background tasks. + */ + @WMSingleton + @Provides + @ShellBackgroundThread + public static Handler provideSharedBackgroundHandler() { + HandlerThread shellBackgroundThread = new HandlerThread("wmshell.background", + THREAD_PRIORITY_BACKGROUND); + shellBackgroundThread.start(); + return shellBackgroundThread.getThreadHandler(); + } + + /** + * Provides a Shell background thread Executor for low priority background tasks. + */ + @WMSingleton + @Provides + @ShellBackgroundThread + public static ShellExecutor provideSharedBackgroundExecutor( + @ShellBackgroundThread Handler handler) { + return new HandlerExecutor(handler); + } } 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 f562fd9cf1af..b3799e2cf8d9 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 @@ -20,13 +20,16 @@ import android.animation.AnimationHandler; import android.content.Context; import android.content.pm.LauncherApps; import android.os.Handler; +import android.os.UserManager; import android.view.WindowManager; +import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.logging.UiEventLogger; import com.android.internal.statusbar.IStatusBarService; import com.android.launcher3.icons.IconProvider; import com.android.wm.shell.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; @@ -41,16 +44,20 @@ import com.android.wm.shell.common.SystemWindows; import com.android.wm.shell.common.TaskStackListenerImpl; import com.android.wm.shell.common.TransactionPool; import com.android.wm.shell.common.annotations.ChoreographerSfVsync; +import com.android.wm.shell.common.annotations.ShellBackgroundThread; import com.android.wm.shell.common.annotations.ShellMainThread; +import com.android.wm.shell.draganddrop.DragAndDropController; 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.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.PipMediaController; +import com.android.wm.shell.pip.PipParamsChangedForwarder; import com.android.wm.shell.pip.PipSnapAlgorithm; import com.android.wm.shell.pip.PipSurfaceTransactionHelper; import com.android.wm.shell.pip.PipTaskOrganizer; @@ -59,7 +66,6 @@ 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.PhonePipMenuController; -import com.android.wm.shell.pip.phone.PipAppOpsListener; import com.android.wm.shell.pip.phone.PipController; import com.android.wm.shell.pip.phone.PipMotionHelper; import com.android.wm.shell.pip.phone.PipTouchHandler; @@ -101,18 +107,25 @@ public class WMShellModule { IStatusBarService statusBarService, WindowManager windowManager, WindowManagerShellWrapper windowManagerShellWrapper, + UserManager userManager, LauncherApps launcherApps, TaskStackListenerImpl taskStackListener, UiEventLogger uiEventLogger, ShellTaskOrganizer organizer, DisplayController displayController, + @DynamicOverride Optional<OneHandedController> oneHandedOptional, + DragAndDropController dragAndDropController, @ShellMainThread ShellExecutor mainExecutor, @ShellMainThread Handler mainHandler, + @ShellBackgroundThread ShellExecutor bgExecutor, + TaskViewTransitions taskViewTransitions, SyncTransactionQueue syncQueue) { return BubbleController.create(context, null /* synchronizer */, floatingContentCoordinator, statusBarService, windowManager, - windowManagerShellWrapper, launcherApps, taskStackListener, - uiEventLogger, organizer, displayController, mainExecutor, mainHandler, syncQueue); + windowManagerShellWrapper, userManager, launcherApps, taskStackListener, + uiEventLogger, organizer, displayController, oneHandedOptional, + dragAndDropController, mainExecutor, mainHandler, bgExecutor, + taskViewTransitions, syncQueue); } // @@ -139,12 +152,10 @@ public class WMShellModule { static OneHandedController provideOneHandedController(Context context, WindowManager windowManager, DisplayController displayController, DisplayLayout displayLayout, TaskStackListenerImpl taskStackListener, - UiEventLogger uiEventLogger, - @ShellMainThread ShellExecutor mainExecutor, - @ShellMainThread Handler mainHandler) { - return OneHandedController.create(context, windowManager, - displayController, displayLayout, taskStackListener, uiEventLogger, mainExecutor, - mainHandler); + UiEventLogger uiEventLogger, InteractionJankMonitor jankMonitor, + @ShellMainThread ShellExecutor mainExecutor, @ShellMainThread Handler mainHandler) { + return OneHandedController.create(context, windowManager, displayController, displayLayout, + taskStackListener, jankMonitor, uiEventLogger, mainExecutor, mainHandler); } // @@ -159,13 +170,14 @@ public class WMShellModule { SyncTransactionQueue syncQueue, Context context, RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, @ShellMainThread ShellExecutor mainExecutor, + DisplayController displayController, DisplayImeController displayImeController, DisplayInsetsController displayInsetsController, Transitions transitions, TransactionPool transactionPool, IconProvider iconProvider, Optional<RecentTasksController> recentTasks, Provider<Optional<StageTaskUnfoldController>> stageTaskUnfoldControllerProvider) { return new SplitScreenController(shellTaskOrganizer, syncQueue, context, - rootTaskDisplayAreaOrganizer, mainExecutor, displayImeController, + rootTaskDisplayAreaOrganizer, mainExecutor, displayController, displayImeController, displayInsetsController, transitions, transactionPool, iconProvider, recentTasks, stageTaskUnfoldControllerProvider); } @@ -208,12 +220,14 @@ public class WMShellModule { PipTouchHandler pipTouchHandler, PipTransitionController pipTransitionController, WindowManagerShellWrapper windowManagerShellWrapper, TaskStackListenerImpl taskStackListener, + PipParamsChangedForwarder pipParamsChangedForwarder, Optional<OneHandedController> oneHandedController, @ShellMainThread ShellExecutor mainExecutor) { return Optional.ofNullable(PipController.create(context, displayController, - pipAppOpsListener, pipBoundsAlgorithm, pipBoundsState, pipMediaController, - phonePipMenuController, pipTaskOrganizer, pipTouchHandler, pipTransitionController, - windowManagerShellWrapper, taskStackListener, oneHandedController, mainExecutor)); + pipAppOpsListener, pipBoundsAlgorithm, pipBoundsState, + pipMediaController, phonePipMenuController, pipTaskOrganizer, + pipTouchHandler, pipTransitionController, windowManagerShellWrapper, + taskStackListener, pipParamsChangedForwarder, oneHandedController, mainExecutor)); } @WMSingleton @@ -242,10 +256,11 @@ public class WMShellModule { PipBoundsState pipBoundsState, PipMediaController pipMediaController, SystemWindows systemWindows, Optional<SplitScreenController> splitScreenOptional, + PipUiEventLogger pipUiEventLogger, @ShellMainThread ShellExecutor mainExecutor, @ShellMainThread Handler mainHandler) { return new PhonePipMenuController(context, pipBoundsState, pipMediaController, - systemWindows, splitScreenOptional, mainExecutor, mainHandler); + systemWindows, splitScreenOptional, pipUiEventLogger, mainExecutor, mainHandler); } @WMSingleton @@ -280,15 +295,15 @@ public class WMShellModule { PipAnimationController pipAnimationController, PipSurfaceTransactionHelper pipSurfaceTransactionHelper, PipTransitionController pipTransitionController, - Optional<LegacySplitScreenController> splitScreenOptional, - Optional<SplitScreenController> newSplitScreenOptional, + PipParamsChangedForwarder pipParamsChangedForwarder, + Optional<SplitScreenController> splitScreenControllerOptional, DisplayController displayController, PipUiEventLogger pipUiEventLogger, ShellTaskOrganizer shellTaskOrganizer, @ShellMainThread ShellExecutor mainExecutor) { return new PipTaskOrganizer(context, syncTransactionQueue, pipTransitionState, pipBoundsState, pipBoundsAlgorithm, menuPhoneController, pipAnimationController, pipSurfaceTransactionHelper, - pipTransitionController, splitScreenOptional, newSplitScreenOptional, + pipTransitionController, pipParamsChangedForwarder, splitScreenControllerOptional, displayController, pipUiEventLogger, shellTaskOrganizer, mainExecutor); } @@ -305,9 +320,20 @@ public class WMShellModule { Transitions transitions, ShellTaskOrganizer shellTaskOrganizer, PipAnimationController pipAnimationController, PipBoundsAlgorithm pipBoundsAlgorithm, PipBoundsState pipBoundsState, PipTransitionState pipTransitionState, - PhonePipMenuController pipMenuController) { + PhonePipMenuController pipMenuController, + PipSurfaceTransactionHelper pipSurfaceTransactionHelper, + Optional<SplitScreenController> splitScreenOptional) { return new PipTransition(context, pipBoundsState, pipTransitionState, pipMenuController, - pipBoundsAlgorithm, pipAnimationController, transitions, shellTaskOrganizer); + pipBoundsAlgorithm, pipAnimationController, transitions, shellTaskOrganizer, + pipSurfaceTransactionHelper, splitScreenOptional); + } + + @WMSingleton + @Provides + static PipAppOpsListener providePipAppOpsListener(Context context, + PipTouchHandler pipTouchHandler, + @ShellMainThread ShellExecutor mainExecutor) { + return new PipAppOpsListener(context, pipTouchHandler.getMotionHelper(), mainExecutor); } @WMSingleton @@ -372,4 +398,10 @@ public class WMShellModule { rootTaskDisplayAreaOrganizer ); } + + @WMSingleton + @Provides + static PipParamsChangedForwarder providePipParamsChangedForwarder() { + return new PipParamsChangedForwarder(); + } } 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 101295d246bc..95de2dc61a43 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 @@ -49,6 +49,8 @@ import android.view.ViewGroup; import android.view.WindowManager; import android.widget.FrameLayout; +import androidx.annotation.VisibleForTesting; + import com.android.internal.logging.InstanceId; import com.android.internal.logging.UiEventLogger; import com.android.internal.protolog.common.ProtoLog; @@ -59,6 +61,7 @@ import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.splitscreen.SplitScreenController; +import java.util.ArrayList; import java.util.Optional; /** @@ -76,10 +79,19 @@ public class DragAndDropController implements DisplayController.OnDisplaysChange 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. + */ + public interface DragAndDropListener { + /** Called when a drag has started. */ + void onDragStarted(); + } + public DragAndDropController(Context context, DisplayController displayController, UiEventLogger uiEventLogger, IconProvider iconProvider, ShellExecutor mainExecutor) { mContext = context; @@ -99,6 +111,22 @@ public class DragAndDropController implements DisplayController.OnDisplaysChange mDisplayController.addDisplayWindowListener(this); } + /** Adds a listener to be notified of drag and drop events. */ + public void addListener(DragAndDropListener listener) { + mListeners.add(listener); + } + + /** Removes a drag and drop listener. */ + public void removeListener(DragAndDropListener listener) { + mListeners.remove(listener); + } + + private void notifyListeners() { + for (int i = 0; i < mListeners.size(); i++) { + mListeners.get(i).onDragStarted(); + } + } + @Override public void onDisplayAdded(int displayId) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, "Display added: %d", displayId); @@ -133,13 +161,19 @@ public class DragAndDropController implements DisplayController.OnDisplaysChange new FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)); try { wm.addView(rootView, layoutParams); - mDisplayDropTargets.put(displayId, - new PerDisplay(displayId, context, wm, rootView, dragLayout)); + addDisplayDropTarget(displayId, context, wm, rootView, dragLayout); } catch (WindowManager.InvalidDisplayException e) { Slog.w(TAG, "Unable to add view for display id: " + displayId); } } + @VisibleForTesting + void addDisplayDropTarget(int displayId, Context context, WindowManager wm, + FrameLayout rootView, DragLayout dragLayout) { + mDisplayDropTargets.put(displayId, + new PerDisplay(displayId, context, wm, rootView, dragLayout)); + } + @Override public void onDisplayConfigurationChanged(int displayId, Configuration newConfig) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, "Display changed: %d", displayId); @@ -202,9 +236,11 @@ public class DragAndDropController implements DisplayController.OnDisplaysChange pd.dragLayout.prepare(mDisplayController.getDisplayLayout(displayId), event.getClipData(), loggerSessionId); setDropTargetWindowVisibility(pd, View.VISIBLE); + notifyListeners(); break; case ACTION_DRAG_ENTERED: pd.dragLayout.show(); + pd.dragLayout.update(event); break; case ACTION_DRAG_LOCATION: pd.dragLayout.update(event); @@ -250,10 +286,6 @@ public class DragAndDropController implements DisplayController.OnDisplaysChange // Hide the window if another drag hasn't been started while animating the drop setDropTargetWindowVisibility(pd, View.INVISIBLE); } - - // Clean up the drag surface - mTransaction.reparent(dragSurface, null); - mTransaction.apply(); }); } 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 8e6c05d83906..756831007c35 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 @@ -45,10 +45,12 @@ import android.app.WindowConfiguration; import android.content.ActivityNotFoundException; import android.content.ClipData; import android.content.ClipDescription; +import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.pm.ActivityInfo; import android.content.pm.LauncherApps; +import android.content.pm.ResolveInfo; import android.graphics.Insets; import android.graphics.Rect; import android.os.Bundle; @@ -62,8 +64,11 @@ 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; @@ -105,12 +110,26 @@ public class DragAndDropPolicy { */ void start(DisplayLayout displayLayout, ClipData data, InstanceId loggerSessionId) { mLoggerSessionId = loggerSessionId; - mSession = new DragSession(mContext, mActivityTaskManager, displayLayout, data); + mSession = new DragSession(mActivityTaskManager, displayLayout, data); // TODO(b/169894807): Also update the session data with task stack changes mSession.update(); } /** + * Returns the last running task. + */ + ActivityManager.RunningTaskInfo getLatestRunningTask() { + return mSession.runningTaskInfo; + } + + /** + * Returns the number of targets. + */ + int getNumTargets() { + return mTargets.size(); + } + + /** * Returns the target's regions based on the current state of the device and display. */ @NonNull @@ -132,6 +151,8 @@ public class DragAndDropPolicy { final Rect fullscreenHitRegion = new Rect(displayRegion); final boolean inLandscape = mSession.displayLayout.isLandscape(); final boolean inSplitScreen = mSplitScreen != null && mSplitScreen.isSplitScreenVisible(); + final float dividerWidth = mContext.getResources().getDimensionPixelSize( + R.dimen.split_divider_bar_width); // We allow splitting if we are already in split-screen or the running task is a standard // task in fullscreen mode. final boolean allowSplit = inSplitScreen @@ -147,37 +168,45 @@ public class DragAndDropPolicy { if (inLandscape) { final Rect leftHitRegion = new Rect(); - final Rect leftDrawRegion = topOrLeftBounds; final Rect rightHitRegion = new Rect(); - final Rect rightDrawRegion = bottomOrRightBounds; // If we have existing split regions use those bounds, otherwise split it 50/50 if (inSplitScreen) { - leftHitRegion.set(topOrLeftBounds); - rightHitRegion.set(bottomOrRightBounds); + // The bounds of the existing split will have a divider bar, the hit region + // should include that space. Find the center of the divider bar: + float centerX = topOrLeftBounds.right + (dividerWidth / 2); + // Now set the hit regions using that center. + leftHitRegion.set(displayRegion); + leftHitRegion.right = (int) centerX; + rightHitRegion.set(displayRegion); + rightHitRegion.left = (int) centerX; } else { displayRegion.splitVertically(leftHitRegion, rightHitRegion); } - mTargets.add(new Target(TYPE_SPLIT_LEFT, leftHitRegion, leftDrawRegion)); - mTargets.add(new Target(TYPE_SPLIT_RIGHT, rightHitRegion, rightDrawRegion)); + mTargets.add(new Target(TYPE_SPLIT_LEFT, leftHitRegion, topOrLeftBounds)); + mTargets.add(new Target(TYPE_SPLIT_RIGHT, rightHitRegion, bottomOrRightBounds)); } else { final Rect topHitRegion = new Rect(); - final Rect topDrawRegion = topOrLeftBounds; final Rect bottomHitRegion = new Rect(); - final Rect bottomDrawRegion = bottomOrRightBounds; // If we have existing split regions use those bounds, otherwise split it 50/50 if (inSplitScreen) { - topHitRegion.set(topOrLeftBounds); - bottomHitRegion.set(bottomOrRightBounds); + // The bounds of the existing split will have a divider bar, the hit region + // should include that space. Find the center of the divider bar: + float centerX = topOrLeftBounds.bottom + (dividerWidth / 2); + // Now set the hit regions using that center. + topHitRegion.set(displayRegion); + topHitRegion.bottom = (int) centerX; + bottomHitRegion.set(displayRegion); + bottomHitRegion.top = (int) centerX; } else { displayRegion.splitHorizontally(topHitRegion, bottomHitRegion); } - mTargets.add(new Target(TYPE_SPLIT_TOP, topHitRegion, topDrawRegion)); - mTargets.add(new Target(TYPE_SPLIT_BOTTOM, bottomHitRegion, bottomDrawRegion)); + mTargets.add(new Target(TYPE_SPLIT_TOP, topHitRegion, topOrLeftBounds)); + mTargets.add(new Target(TYPE_SPLIT_BOTTOM, bottomHitRegion, bottomOrRightBounds)); } } else { // Split-screen not allowed, so only show the fullscreen target @@ -237,32 +266,68 @@ public class DragAndDropPolicy { final UserHandle user = intent.getParcelableExtra(EXTRA_USER); mStarter.startShortcut(packageName, id, position, opts, user); } else { - mStarter.startIntent(intent.getParcelableExtra(EXTRA_PENDING_INTENT), - null, position, opts); + final PendingIntent launchIntent = intent.getParcelableExtra(EXTRA_PENDING_INTENT); + mStarter.startIntent(launchIntent, getStartIntentFillInIntent(launchIntent, position), + 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 { - private final Context mContext; private final ActivityTaskManager mActivityTaskManager; private final ClipData mInitialDragData; final DisplayLayout displayLayout; Intent dragData; - int runningTaskId; + ActivityManager.RunningTaskInfo runningTaskInfo; @WindowConfiguration.WindowingMode int runningTaskWinMode = WINDOWING_MODE_UNDEFINED; @WindowConfiguration.ActivityType int runningTaskActType = ACTIVITY_TYPE_STANDARD; - boolean runningTaskIsResizeable; boolean dragItemSupportsSplitscreen; - DragSession(Context context, ActivityTaskManager activityTaskManager, + DragSession(ActivityTaskManager activityTaskManager, DisplayLayout dispLayout, ClipData data) { - mContext = context; mActivityTaskManager = activityTaskManager; mInitialDragData = data; displayLayout = dispLayout; @@ -276,10 +341,9 @@ public class DragAndDropPolicy { mActivityTaskManager.getTasks(1, false /* filterOnlyVisibleRecents */); if (!tasks.isEmpty()) { final ActivityManager.RunningTaskInfo task = tasks.get(0); + runningTaskInfo = task; runningTaskWinMode = task.getWindowingMode(); runningTaskActType = task.getActivityType(); - runningTaskId = task.taskId; - runningTaskIsResizeable = task.isResizeable; } final ActivityInfo info = mInitialDragData.getItemAt(0).getActivityInfo(); 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 f98849260511..ff3c0834cf62 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 @@ -17,6 +17,7 @@ package com.android.wm.shell.draganddrop; import static android.app.StatusBarManager.DISABLE_NONE; +import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; @@ -24,10 +25,9 @@ import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSIT import android.animation.Animator; import android.animation.AnimatorListenerAdapter; -import android.animation.ObjectAnimator; +import android.animation.ValueAnimator; import android.annotation.SuppressLint; import android.app.ActivityManager; -import android.app.ActivityTaskManager; import android.app.StatusBarManager; import android.content.ClipData; import android.content.Context; @@ -36,7 +36,6 @@ import android.graphics.Color; import android.graphics.Insets; import android.graphics.Rect; import android.graphics.drawable.Drawable; -import android.os.RemoteException; import android.view.DragEvent; import android.view.SurfaceControl; import android.view.WindowInsets; @@ -47,12 +46,12 @@ 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.animation.Interpolators; import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.splitscreen.SplitScreenController; import java.util.ArrayList; -import java.util.List; /** * Coordinates the visible drop targets for the current drag. @@ -95,6 +94,9 @@ public class DragLayout extends LinearLayout { mDividerSize = context.getResources().getDimensionPixelSize( R.dimen.split_divider_bar_width); + // Always use LTR because we assume dropZoneView1 is on the left and 2 is on the right when + // showing the highlight. + setLayoutDirection(LAYOUT_DIRECTION_LTR); mDropZoneView1 = new DropZoneView(context); mDropZoneView2 = new DropZoneView(context); addView(mDropZoneView1, new LinearLayout.LayoutParams(MATCH_PARENT, @@ -139,6 +141,12 @@ public class DragLayout extends LinearLayout { } } + private void updateContainerMarginsForSingleTask() { + mDropZoneView1.setContainerMargin( + mDisplayMargin, mDisplayMargin, mDisplayMargin, mDisplayMargin); + mDropZoneView2.setContainerMargin(0, 0, 0, 0); + } + private void updateContainerMargins(int orientation) { final float halfMargin = mDisplayMargin / 2f; if (orientation == Configuration.ORIENTATION_LANDSCAPE) { @@ -154,10 +162,6 @@ public class DragLayout extends LinearLayout { } } - public boolean hasDropTarget() { - return mCurrentTarget != null; - } - public boolean hasDropped() { return mHasDropped; } @@ -171,22 +175,22 @@ public class DragLayout extends LinearLayout { boolean alreadyInSplit = mSplitScreenController != null && mSplitScreenController.isSplitScreenVisible(); if (!alreadyInSplit) { - List<ActivityManager.RunningTaskInfo> tasks = null; - // Figure out the splashscreen info for the existing task. - try { - tasks = ActivityTaskManager.getService().getTasks(1, - false /* filterOnlyVisibleRecents */, - false /* keepIntentExtra */); - } catch (RemoteException e) { - // don't show an icon / will just use the defaults - } - if (tasks != null && !tasks.isEmpty()) { - ActivityManager.RunningTaskInfo taskInfo1 = tasks.get(0); - Drawable icon1 = mIconProvider.getIcon(taskInfo1.topActivityInfo); - int bgColor1 = getResizingBackgroundColor(taskInfo1); - mDropZoneView1.setAppInfo(bgColor1, icon1); - mDropZoneView2.setAppInfo(bgColor1, icon1); - updateDropZoneSizes(null, null); // passing null splits the views evenly + ActivityManager.RunningTaskInfo taskInfo1 = mPolicy.getLatestRunningTask(); + if (taskInfo1 != null) { + final int activityType = taskInfo1.getActivityType(); + if (activityType == ACTIVITY_TYPE_STANDARD) { + Drawable icon1 = mIconProvider.getIcon(taskInfo1.topActivityInfo); + int bgColor1 = getResizingBackgroundColor(taskInfo1); + mDropZoneView1.setAppInfo(bgColor1, icon1); + mDropZoneView2.setAppInfo(bgColor1, icon1); + updateDropZoneSizes(null, null); // passing null splits the views evenly + } else { + // We use the first drop zone to show the fullscreen highlight, and don't need + // to set additional info + mDropZoneView1.setForceIgnoreBottomMargin(true); + updateDropZoneSizesForSingleTask(); + updateContainerMarginsForSingleTask(); + } } } else { // We're already in split so get taskInfo from the controller to populate icon / color. @@ -212,6 +216,21 @@ public class DragLayout extends LinearLayout { } } + private void updateDropZoneSizesForSingleTask() { + final LinearLayout.LayoutParams dropZoneView1 = + (LayoutParams) mDropZoneView1.getLayoutParams(); + final LinearLayout.LayoutParams dropZoneView2 = + (LayoutParams) mDropZoneView2.getLayoutParams(); + dropZoneView1.width = MATCH_PARENT; + dropZoneView1.height = MATCH_PARENT; + dropZoneView2.width = 0; + dropZoneView2.height = 0; + dropZoneView1.weight = 1; + dropZoneView2.weight = 0; + mDropZoneView1.setLayoutParams(dropZoneView1); + mDropZoneView2.setLayoutParams(dropZoneView2); + } + /** * Sets the size of the two drop zones based on the provided bounds. The divider sits between * the views and its size is included in the calculations. @@ -269,6 +288,9 @@ public class DragLayout extends LinearLayout { * Updates the visible drop target as the user drags. */ public void update(DragEvent event) { + if (mHasDropped) { + return; + } // Find containing region, if the same as mCurrentRegion, then skip, otherwise, animate the // visibility of the current region DragAndDropPolicy.Target target = mPolicy.getTargetAtLocation( @@ -279,12 +301,16 @@ public class DragLayout extends LinearLayout { // Animating to no target animateSplitContainers(false, null /* animCompleteCallback */); } else if (mCurrentTarget == null) { - // Animating to first target - animateSplitContainers(true, null /* animCompleteCallback */); - animateHighlight(target); + if (mPolicy.getNumTargets() == 1) { + animateFullscreenContainer(true); + } else { + animateSplitContainers(true, null /* animCompleteCallback */); + animateHighlight(target); + } } else { // Switching between targets - animateHighlight(target); + mDropZoneView1.animateSwitch(); + mDropZoneView2.animateSwitch(); } mCurrentTarget = target; } @@ -296,6 +322,10 @@ public class DragLayout extends LinearLayout { public void hide(DragEvent event, Runnable hideCompleteCallback) { mIsShowing = false; animateSplitContainers(false, hideCompleteCallback); + // Reset the state if we previously force-ignore the bottom margin + mDropZoneView1.setForceIgnoreBottomMargin(false); + mDropZoneView2.setForceIgnoreBottomMargin(false); + updateContainerMargins(getResources().getConfiguration().orientation); mCurrentTarget = null; } @@ -310,18 +340,70 @@ public class DragLayout extends LinearLayout { // Process the drop mPolicy.handleDrop(mCurrentTarget, event.getClipData()); - // TODO(b/169894807): Coordinate with dragSurface + // Start animating the drop UI out with the drag surface hide(event, dropCompleteCallback); + hideDragSurface(dragSurface); return handledDrop; } + private void hideDragSurface(SurfaceControl dragSurface) { + final SurfaceControl.Transaction tx = new SurfaceControl.Transaction(); + final ValueAnimator dragSurfaceAnimator = ValueAnimator.ofFloat(0f, 1f); + // Currently the splash icon animation runs with the default ValueAnimator duration of + // 300ms + dragSurfaceAnimator.setDuration(300); + dragSurfaceAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN); + dragSurfaceAnimator.addUpdateListener(animation -> { + float t = animation.getAnimatedFraction(); + float alpha = 1f - t; + // TODO: Scale the drag surface as well once we make all the source surfaces + // consistent + tx.setAlpha(dragSurface, alpha); + tx.apply(); + }); + dragSurfaceAnimator.addListener(new AnimatorListenerAdapter() { + private boolean mCanceled = false; + + @Override + public void onAnimationCancel(Animator animation) { + cleanUpSurface(); + mCanceled = true; + } + + @Override + public void onAnimationEnd(Animator animation) { + if (mCanceled) { + // Already handled above + return; + } + cleanUpSurface(); + } + + private void cleanUpSurface() { + // Clean up the drag surface + tx.remove(dragSurface); + tx.apply(); + } + }); + dragSurfaceAnimator.start(); + } + + private void animateFullscreenContainer(boolean visible) { + mStatusBarManager.disable(visible + ? HIDE_STATUS_BAR_FLAGS + : DISABLE_NONE); + // We're only using the first drop zone if there is one fullscreen target + mDropZoneView1.setShowingMargin(visible); + mDropZoneView1.setShowingHighlight(visible); + } + private void animateSplitContainers(boolean visible, Runnable animCompleteCallback) { mStatusBarManager.disable(visible ? HIDE_STATUS_BAR_FLAGS : DISABLE_NONE); mDropZoneView1.setShowingMargin(visible); mDropZoneView2.setShowingMargin(visible); - ObjectAnimator animator = mDropZoneView1.getAnimator(); + Animator animator = mDropZoneView1.getAnimator(); if (animCompleteCallback != null) { if (animator != null) { animator.addListener(new AnimatorListenerAdapter() { @@ -341,17 +423,11 @@ public class DragLayout extends LinearLayout { if (target.type == DragAndDropPolicy.Target.TYPE_SPLIT_LEFT || target.type == DragAndDropPolicy.Target.TYPE_SPLIT_TOP) { mDropZoneView1.setShowingHighlight(true); - mDropZoneView1.setShowingSplash(false); - mDropZoneView2.setShowingHighlight(false); - mDropZoneView2.setShowingSplash(true); } else if (target.type == DragAndDropPolicy.Target.TYPE_SPLIT_RIGHT || target.type == DragAndDropPolicy.Target.TYPE_SPLIT_BOTTOM) { mDropZoneView1.setShowingHighlight(false); - mDropZoneView1.setShowingSplash(true); - mDropZoneView2.setShowingHighlight(true); - mDropZoneView2.setShowingSplash(false); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DropZoneView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DropZoneView.java index 2f47af57d496..28f59b53b5b6 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DropZoneView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DropZoneView.java @@ -18,6 +18,7 @@ package com.android.wm.shell.draganddrop; import static com.android.wm.shell.animation.Interpolators.FAST_OUT_SLOW_IN; +import android.animation.Animator; import android.animation.ObjectAnimator; import android.content.Context; import android.graphics.Canvas; @@ -27,7 +28,7 @@ import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.util.FloatProperty; -import android.util.IntProperty; +import android.view.Gravity; import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; @@ -43,8 +44,8 @@ import com.android.wm.shell.R; */ public class DropZoneView extends FrameLayout { - private static final int SPLASHSCREEN_ALPHA_INT = (int) (255 * 0.90f); - private static final int HIGHLIGHT_ALPHA_INT = 255; + private static final float SPLASHSCREEN_ALPHA = 0.90f; + private static final float HIGHLIGHT_ALPHA = 1f; private static final int MARGIN_ANIMATION_ENTER_DURATION = 400; private static final int MARGIN_ANIMATION_EXIT_DURATION = 250; @@ -61,54 +62,28 @@ public class DropZoneView extends FrameLayout { } }; - private static final IntProperty<ColorDrawable> SPLASHSCREEN_ALPHA = - new IntProperty<ColorDrawable>("splashscreen") { - @Override - public void setValue(ColorDrawable d, int alpha) { - d.setAlpha(alpha); - } - - @Override - public Integer get(ColorDrawable d) { - return d.getAlpha(); - } - }; - - private static final IntProperty<ColorDrawable> HIGHLIGHT_ALPHA = - new IntProperty<ColorDrawable>("highlight") { - @Override - public void setValue(ColorDrawable d, int alpha) { - d.setAlpha(alpha); - } - - @Override - public Integer get(ColorDrawable d) { - return d.getAlpha(); - } - }; - private final Path mPath = new Path(); private final float[] mContainerMargin = new float[4]; private float mCornerRadius; private float mBottomInset; + private boolean mIgnoreBottomMargin; private int mMarginColor; // i.e. color used for negative space like the container insets - private int mHighlightColor; private boolean mShowingHighlight; private boolean mShowingSplash; private boolean mShowingMargin; - // TODO: might be more seamless to animate between splash/highlight color instead of 2 separate - private ObjectAnimator mSplashAnimator; - private ObjectAnimator mHighlightAnimator; + private int mSplashScreenColor; + private int mHighlightColor; + + private ObjectAnimator mBackgroundAnimator; private ObjectAnimator mMarginAnimator; private float mMarginPercent; // Renders a highlight or neutral transparent color - private ColorDrawable mDropZoneDrawable; + private ColorDrawable mColorDrawable; // Renders the translucent splashscreen with the app icon in the middle private ImageView mSplashScreenView; - private ColorDrawable mSplashBackgroundDrawable; // Renders the margin / insets around the dropzone container private MarginView mMarginView; @@ -130,21 +105,17 @@ public class DropZoneView extends FrameLayout { mCornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context); mMarginColor = getResources().getColor(R.color.taskbar_background); - mHighlightColor = getResources().getColor(android.R.color.system_accent1_500); - - mDropZoneDrawable = new ColorDrawable(); - mDropZoneDrawable.setColor(mHighlightColor); - mDropZoneDrawable.setAlpha(0); - setBackgroundDrawable(mDropZoneDrawable); + int c = getResources().getColor(android.R.color.system_accent1_500); + mHighlightColor = Color.argb(HIGHLIGHT_ALPHA, Color.red(c), Color.green(c), Color.blue(c)); + mSplashScreenColor = Color.argb(SPLASHSCREEN_ALPHA, 0, 0, 0); + mColorDrawable = new ColorDrawable(); + setBackgroundDrawable(mColorDrawable); + final int iconSize = context.getResources().getDimensionPixelSize(R.dimen.split_icon_size); mSplashScreenView = new ImageView(context); - mSplashScreenView.setScaleType(ImageView.ScaleType.CENTER); - mSplashBackgroundDrawable = new ColorDrawable(); - mSplashBackgroundDrawable.setColor(Color.WHITE); - mSplashBackgroundDrawable.setAlpha(SPLASHSCREEN_ALPHA_INT); - mSplashScreenView.setBackgroundDrawable(mSplashBackgroundDrawable); - addView(mSplashScreenView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT)); + mSplashScreenView.setScaleType(ImageView.ScaleType.FIT_CENTER); + addView(mSplashScreenView, + new FrameLayout.LayoutParams(iconSize, iconSize, Gravity.CENTER)); mSplashScreenView.setAlpha(0f); mMarginView = new MarginView(context); @@ -157,10 +128,6 @@ public class DropZoneView extends FrameLayout { mMarginColor = getResources().getColor(R.color.taskbar_background); mHighlightColor = getResources().getColor(android.R.color.system_accent1_500); - final int alpha = mDropZoneDrawable.getAlpha(); - mDropZoneDrawable.setColor(mHighlightColor); - mDropZoneDrawable.setAlpha(alpha); - if (mMarginPercent > 0) { mMarginView.invalidate(); } @@ -177,6 +144,14 @@ public class DropZoneView extends FrameLayout { } } + /** Ignores the bottom margin provided by the insets. */ + public void setForceIgnoreBottomMargin(boolean ignoreBottomMargin) { + mIgnoreBottomMargin = ignoreBottomMargin; + if (mMarginPercent > 0) { + mMarginView.invalidate(); + } + } + /** Sets the bottom inset so the drop zones are above bottom navigation. */ public void setBottomInset(float bottom) { mBottomInset = bottom; @@ -187,38 +162,39 @@ public class DropZoneView extends FrameLayout { } /** Sets the color and icon to use for the splashscreen when shown. */ - public void setAppInfo(int splashScreenColor, Drawable appIcon) { - mSplashBackgroundDrawable.setColor(splashScreenColor); + public void setAppInfo(int color, Drawable appIcon) { + Color c = Color.valueOf(color); + mSplashScreenColor = Color.argb(SPLASHSCREEN_ALPHA, c.red(), c.green(), c.blue()); mSplashScreenView.setImageDrawable(appIcon); } /** @return an active animator for this view if one exists. */ @Nullable - public ObjectAnimator getAnimator() { + public Animator getAnimator() { if (mMarginAnimator != null && mMarginAnimator.isRunning()) { return mMarginAnimator; - } else if (mHighlightAnimator != null && mHighlightAnimator.isRunning()) { - return mHighlightAnimator; - } else if (mSplashAnimator != null && mSplashAnimator.isRunning()) { - return mSplashAnimator; + } else if (mBackgroundAnimator != null && mBackgroundAnimator.isRunning()) { + return mBackgroundAnimator; } return null; } - /** Animates the splashscreen to show or hide. */ - public void setShowingSplash(boolean showingSplash) { - if (mShowingSplash != showingSplash) { - mShowingSplash = showingSplash; - animateSplashToState(); - } + /** Animates between highlight and splashscreen depending on current state. */ + public void animateSwitch() { + mShowingHighlight = !mShowingHighlight; + mShowingSplash = !mShowingHighlight; + final int newColor = mShowingHighlight ? mHighlightColor : mSplashScreenColor; + animateBackground(mColorDrawable.getColor(), newColor); + animateSplashScreenIcon(); } /** Animates the highlight indicating the zone is hovered on or not. */ public void setShowingHighlight(boolean showingHighlight) { - if (mShowingHighlight != showingHighlight) { - mShowingHighlight = showingHighlight; - animateHighlightToState(); - } + mShowingHighlight = showingHighlight; + mShowingSplash = !mShowingHighlight; + final int newColor = mShowingHighlight ? mHighlightColor : mSplashScreenColor; + animateBackground(Color.TRANSPARENT, newColor); + animateSplashScreenIcon(); } /** Animates the margins around the drop zone to show or hide. */ @@ -228,38 +204,29 @@ public class DropZoneView extends FrameLayout { animateMarginToState(); } if (!mShowingMargin) { - setShowingHighlight(false); - setShowingSplash(false); + mShowingHighlight = false; + mShowingSplash = false; + animateBackground(mColorDrawable.getColor(), Color.TRANSPARENT); + animateSplashScreenIcon(); } } - private void animateSplashToState() { - if (mSplashAnimator != null) { - mSplashAnimator.cancel(); + private void animateBackground(int startColor, int endColor) { + if (mBackgroundAnimator != null) { + mBackgroundAnimator.cancel(); } - mSplashAnimator = ObjectAnimator.ofInt(mSplashBackgroundDrawable, - SPLASHSCREEN_ALPHA, - mSplashBackgroundDrawable.getAlpha(), - mShowingSplash ? SPLASHSCREEN_ALPHA_INT : 0); - if (!mShowingSplash) { - mSplashAnimator.setInterpolator(FAST_OUT_SLOW_IN); + mBackgroundAnimator = ObjectAnimator.ofArgb(mColorDrawable, + "color", + startColor, + endColor); + if (!mShowingSplash && !mShowingHighlight) { + mBackgroundAnimator.setInterpolator(FAST_OUT_SLOW_IN); } - mSplashAnimator.start(); - mSplashScreenView.animate().alpha(mShowingSplash ? 1f : 0f).start(); + mBackgroundAnimator.start(); } - private void animateHighlightToState() { - if (mHighlightAnimator != null) { - mHighlightAnimator.cancel(); - } - mHighlightAnimator = ObjectAnimator.ofInt(mDropZoneDrawable, - HIGHLIGHT_ALPHA, - mDropZoneDrawable.getAlpha(), - mShowingHighlight ? HIGHLIGHT_ALPHA_INT : 0); - if (!mShowingHighlight) { - mHighlightAnimator.setInterpolator(FAST_OUT_SLOW_IN); - } - mHighlightAnimator.start(); + private void animateSplashScreenIcon() { + mSplashScreenView.animate().alpha(mShowingSplash ? 1f : 0f).start(); } private void animateMarginToState() { @@ -301,7 +268,8 @@ public class DropZoneView extends FrameLayout { mPath.addRoundRect(mContainerMargin[0] * mMarginPercent, mContainerMargin[1] * mMarginPercent, getWidth() - (mContainerMargin[2] * mMarginPercent), - getHeight() - (mContainerMargin[3] * mMarginPercent) - mBottomInset, + getHeight() - (mContainerMargin[3] * mMarginPercent) + - (mIgnoreBottomMargin ? 0 : mBottomInset), mCornerRadius * mMarginPercent, mCornerRadius * mMarginPercent, Path.Direction.CW); 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 52ff21bc3172..fef9be36a35f 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 @@ -110,6 +110,24 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener { } @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 (!mTasks.contains(taskId)) { + throw new IllegalArgumentException("There is no surface for taskId=" + taskId); + } + return mTasks.get(taskId).mLeash; + } + + @Override public void dump(PrintWriter pw, String prefix) { final String innerPrefix = prefix + " "; pw.println(prefix + this); 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 6e38e421d4b6..73e6cba43ec0 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 @@ -133,10 +133,20 @@ public class FullscreenTaskListener implements ShellTaskOrganizer.TaskListener { @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 (!mDataByTaskId.contains(taskId)) { throw new IllegalArgumentException("There is no surface for taskId=" + taskId); } - b.setParent(mDataByTaskId.get(taskId).surface); + return mDataByTaskId.get(taskId).surface; } @Override diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/kidsmode/KidsModeSettingsObserver.java b/libs/WindowManager/Shell/src/com/android/wm/shell/kidsmode/KidsModeSettingsObserver.java new file mode 100644 index 000000000000..65cb7ac1e5f7 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/kidsmode/KidsModeSettingsObserver.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.kidsmode; + +import android.annotation.NonNull; +import android.app.ActivityManager; +import android.content.ContentResolver; +import android.content.Context; +import android.database.ContentObserver; +import android.net.Uri; +import android.os.Handler; +import android.os.UserHandle; +import android.provider.Settings; + +import java.util.Collection; + +/** + * A ContentObserver for listening kids mode relative setting keys: + * - {@link Settings.Secure#NAVIGATION_MODE} + * - {@link Settings.Secure#NAV_BAR_KIDS_MODE} + * + * @hide + */ +public class KidsModeSettingsObserver extends ContentObserver { + private Context mContext; + private Runnable mOnChangeRunnable; + + public KidsModeSettingsObserver(Handler handler, Context context) { + super(handler); + mContext = context; + } + + public void setOnChangeRunnable(Runnable r) { + mOnChangeRunnable = r; + } + + /** + * Registers the observer. + */ + public void register() { + final ContentResolver r = mContext.getContentResolver(); + r.registerContentObserver( + Settings.Secure.getUriFor(Settings.Secure.NAVIGATION_MODE), + false, this, UserHandle.USER_ALL); + r.registerContentObserver( + Settings.Secure.getUriFor(Settings.Secure.NAV_BAR_KIDS_MODE), + false, this, UserHandle.USER_ALL); + } + + /** + * Unregisters the observer. + */ + public void unregister() { + mContext.getContentResolver().unregisterContentObserver(this); + } + + @Override + public void onChange(boolean selfChange, @NonNull Collection<Uri> uris, int flags, int userId) { + if (userId != ActivityManager.getCurrentUser()) { + return; + } + + if (mOnChangeRunnable != null) { + mOnChangeRunnable.run(); + } + } + + /** + * Returns true only when it's in three button nav mode and the kid nav bar mode is enabled. + * Otherwise, return false. + */ + public boolean isEnabled() { + return Settings.Secure.getIntForUser(mContext.getContentResolver(), + Settings.Secure.NAVIGATION_MODE, 0, UserHandle.USER_CURRENT) == 0 + && Settings.Secure.getIntForUser(mContext.getContentResolver(), + Settings.Secure.NAV_BAR_KIDS_MODE, 0, UserHandle.USER_CURRENT) == 1; + } +} 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 new file mode 100644 index 000000000000..b4c87b6cbf95 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/kidsmode/KidsModeTaskOrganizer.java @@ -0,0 +1,354 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.kidsmode; + +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.view.Display.DEFAULT_DISPLAY; + +import android.app.ActivityManager; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.res.Configuration; +import android.graphics.Rect; +import android.os.Binder; +import android.os.Handler; +import android.os.IBinder; +import android.view.InsetsSource; +import android.view.InsetsState; +import android.view.SurfaceControl; +import android.window.ITaskOrganizerController; +import android.window.TaskAppearedInfo; +import android.window.WindowContainerToken; +import android.window.WindowContainerTransaction; + +import androidx.annotation.NonNull; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.wm.shell.ShellTaskOrganizer; +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.SyncTransactionQueue; +import com.android.wm.shell.recents.RecentTasksController; +import com.android.wm.shell.startingsurface.StartingWindowController; + +import java.io.PrintWriter; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +/** + * A dedicated task organizer when kids mode is enabled. + * - Creates a root task with bounds that exclude the navigation bar area + * - Launch all task into the root task except for Launcher + */ +public class KidsModeTaskOrganizer extends ShellTaskOrganizer { + private static final String TAG = "KidsModeTaskOrganizer"; + + private static final int[] CONTROLLED_ACTIVITY_TYPES = + {ACTIVITY_TYPE_UNDEFINED, ACTIVITY_TYPE_STANDARD}; + private static final int[] CONTROLLED_WINDOWING_MODES = + {WINDOWING_MODE_FULLSCREEN, WINDOWING_MODE_UNDEFINED}; + + private final Handler mMainHandler; + private final Context mContext; + private final SyncTransactionQueue mSyncQueue; + private final DisplayController mDisplayController; + private final DisplayInsetsController mDisplayInsetsController; + + @VisibleForTesting + ActivityManager.RunningTaskInfo mLaunchRootTask; + @VisibleForTesting + SurfaceControl mLaunchRootLeash; + @VisibleForTesting + final IBinder mCookie = new Binder(); + + private final InsetsState mInsetsState = new InsetsState(); + private int mDisplayWidth; + private int mDisplayHeight; + + private KidsModeSettingsObserver mKidsModeSettingsObserver; + private boolean mEnabled; + + private final BroadcastReceiver mUserSwitchIntentReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + updateKidsModeState(); + } + }; + + DisplayController.OnDisplaysChangedListener mOnDisplaysChangedListener = + new DisplayController.OnDisplaysChangedListener() { + @Override + public void onDisplayConfigurationChanged(int displayId, Configuration newConfig) { + if (displayId != DEFAULT_DISPLAY) { + return; + } + final DisplayLayout displayLayout = + mDisplayController.getDisplayLayout(DEFAULT_DISPLAY); + if (displayLayout == null) { + return; + } + final int displayWidth = displayLayout.width(); + final int displayHeight = displayLayout.height(); + if (displayWidth == mDisplayWidth || displayHeight == mDisplayHeight) { + return; + } + mDisplayWidth = displayWidth; + mDisplayHeight = displayHeight; + updateBounds(); + } + }; + + DisplayInsetsController.OnInsetsChangedListener mOnInsetsChangedListener = + 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))) { + return; + } + mInsetsState.set(insetsState); + updateBounds(); + } + }; + + @VisibleForTesting + KidsModeTaskOrganizer( + ITaskOrganizerController taskOrganizerController, + ShellExecutor mainExecutor, + Handler mainHandler, + Context context, + SyncTransactionQueue syncTransactionQueue, + DisplayController displayController, + DisplayInsetsController displayInsetsController, + Optional<RecentTasksController> recentTasks, + KidsModeSettingsObserver kidsModeSettingsObserver) { + super(taskOrganizerController, mainExecutor, context, /* compatUI= */ null, recentTasks); + mContext = context; + mMainHandler = mainHandler; + mSyncQueue = syncTransactionQueue; + mDisplayController = displayController; + mDisplayInsetsController = displayInsetsController; + mKidsModeSettingsObserver = kidsModeSettingsObserver; + } + + public KidsModeTaskOrganizer( + ShellExecutor mainExecutor, + Handler mainHandler, + Context context, + SyncTransactionQueue syncTransactionQueue, + DisplayController displayController, + DisplayInsetsController displayInsetsController, + Optional<RecentTasksController> recentTasks) { + super(mainExecutor, context, /* compatUI= */ null, recentTasks); + mContext = context; + mMainHandler = mainHandler; + mSyncQueue = syncTransactionQueue; + mDisplayController = displayController; + mDisplayInsetsController = displayInsetsController; + } + + /** + * Initializes kids mode status. + */ + public void initialize(StartingWindowController startingWindowController) { + initStartingWindow(startingWindowController); + if (mKidsModeSettingsObserver == null) { + mKidsModeSettingsObserver = new KidsModeSettingsObserver(mMainHandler, mContext); + } + mKidsModeSettingsObserver.setOnChangeRunnable(() -> updateKidsModeState()); + updateKidsModeState(); + mKidsModeSettingsObserver.register(); + + final IntentFilter filter = new IntentFilter(); + filter.addAction(Intent.ACTION_USER_SWITCHED); + mContext.registerReceiverForAllUsers(mUserSwitchIntentReceiver, filter, null, mMainHandler); + } + + @Override + public void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash) { + if (mEnabled && mLaunchRootTask == null && taskInfo.launchCookies != null + && taskInfo.launchCookies.contains(mCookie)) { + mLaunchRootTask = taskInfo; + mLaunchRootLeash = leash; + updateTask(); + } + super.onTaskAppeared(taskInfo, leash); + + mSyncQueue.runInSync(t -> { + // Reset several properties back to fullscreen (PiP, for example, leaves all these + // properties in a bad state). + t.setCrop(leash, null); + t.setPosition(leash, 0, 0); + t.setAlpha(leash, 1f); + t.setMatrix(leash, 1, 0, 0, 1); + t.show(leash); + }); + } + + @Override + public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) { + if (mLaunchRootTask != null && mLaunchRootTask.taskId == taskInfo.taskId + && !taskInfo.equals(mLaunchRootTask)) { + mLaunchRootTask = taskInfo; + } + + super.onTaskInfoChanged(taskInfo); + } + + @VisibleForTesting + void updateKidsModeState() { + final boolean enabled = mKidsModeSettingsObserver.isEnabled(); + if (mEnabled == enabled) { + return; + } + mEnabled = enabled; + if (mEnabled) { + enable(); + } else { + disable(); + } + } + + @VisibleForTesting + void enable() { + // 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); + final DisplayLayout displayLayout = mDisplayController.getDisplayLayout(DEFAULT_DISPLAY); + if (displayLayout != null) { + mDisplayWidth = displayLayout.width(); + mDisplayHeight = displayLayout.height(); + } + mInsetsState.set(mDisplayController.getInsetsState(DEFAULT_DISPLAY)); + mDisplayInsetsController.addInsetsChangedListener(DEFAULT_DISPLAY, + mOnInsetsChangedListener); + mDisplayController.addDisplayWindowListener(mOnDisplaysChangedListener); + List<TaskAppearedInfo> taskAppearedInfos = registerOrganizer(); + for (int i = 0; i < taskAppearedInfos.size(); i++) { + final TaskAppearedInfo info = taskAppearedInfos.get(i); + onTaskAppeared(info.getTaskInfo(), info.getLeash()); + } + createRootTask(DEFAULT_DISPLAY, WINDOWING_MODE_FULLSCREEN, mCookie); + updateTask(); + } + + @VisibleForTesting + void disable() { + setIsIgnoreOrientationRequestDisabled(false); + mDisplayInsetsController.removeInsetsChangedListener(DEFAULT_DISPLAY, + mOnInsetsChangedListener); + mDisplayController.removeDisplayWindowListener(mOnDisplaysChangedListener); + updateTask(); + final WindowContainerToken token = mLaunchRootTask.token; + if (token != null) { + deleteRootTask(token); + } + mLaunchRootTask = null; + mLaunchRootLeash = null; + unregisterOrganizer(); + } + + private void updateTask() { + updateTask(getWindowContainerTransaction()); + } + + private void updateTask(WindowContainerTransaction wct) { + if (mLaunchRootTask == null || mLaunchRootLeash == null) { + return; + } + final Rect taskBounds = calculateBounds(); + final WindowContainerToken rootToken = mLaunchRootTask.token; + wct.setBounds(rootToken, mEnabled ? taskBounds : null); + wct.setLaunchRoot(rootToken, + mEnabled ? CONTROLLED_WINDOWING_MODES : null, + mEnabled ? CONTROLLED_ACTIVITY_TYPES : null); + wct.reparentTasks( + mEnabled ? null : rootToken /* currentParent */, + mEnabled ? rootToken : null /* newParent */, + CONTROLLED_WINDOWING_MODES, + CONTROLLED_ACTIVITY_TYPES, + 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()); + }); + } + + 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(); + } + return bounds; + } + + private void updateBounds() { + if (mLaunchRootTask == null) { + return; + } + final WindowContainerTransaction wct = getWindowContainerTransaction(); + final Rect taskBounds = calculateBounds(); + wct.setBounds(mLaunchRootTask.token, taskBounds); + mSyncQueue.queue(wct); + final SurfaceControl finalLeash = mLaunchRootLeash; + mSyncQueue.runInSync(t -> { + t.setPosition(finalLeash, taskBounds.left, taskBounds.top); + t.setWindowCrop(finalLeash, taskBounds.width(), taskBounds.height()); + }); + } + + @VisibleForTesting + WindowContainerTransaction getWindowContainerTransaction() { + return new WindowContainerTransaction(); + } + + @Override + public void dump(@NonNull PrintWriter pw, String prefix) { + final String innerPrefix = prefix + " "; + pw.println(prefix + TAG); + pw.println(innerPrefix + " mEnabled=" + mEnabled); + pw.println(innerPrefix + " mLaunchRootTask=" + mLaunchRootTask); + pw.println(innerPrefix + " mLaunchRootLeash=" + mLaunchRootLeash); + pw.println(innerPrefix + " mDisplayWidth=" + mDisplayWidth); + pw.println(innerPrefix + " mDisplayHeight=" + mDisplayHeight); + pw.println(innerPrefix + " mInsetsState=" + mInsetsState); + super.dump(pw, innerPrefix); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/DividerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/DividerView.java index 067f80800ed5..73be2835d2cd 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/DividerView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/DividerView.java @@ -38,7 +38,6 @@ import android.graphics.Region; import android.graphics.Region.Op; import android.hardware.display.DisplayManager; import android.os.Bundle; -import android.os.RemoteException; import android.util.AttributeSet; import android.util.Slog; import android.view.Choreographer; @@ -243,22 +242,6 @@ public class DividerView extends FrameLayout implements OnTouchListener, } }; - private Runnable mUpdateEmbeddedMatrix = () -> { - if (getViewRootImpl() == null) { - return; - } - if (isHorizontalDivision()) { - mTmpMatrix.setTranslate(0, mDividerPositionY - mDividerInsets); - } else { - mTmpMatrix.setTranslate(mDividerPositionX - mDividerInsets, 0); - } - mTmpMatrix.getValues(mTmpValues); - try { - getViewRootImpl().getAccessibilityEmbeddedConnection().setScreenMatrix(mTmpValues); - } catch (RemoteException e) { - } - }; - public DividerView(Context context) { this(context, null); } @@ -1052,10 +1035,6 @@ public class DividerView extends FrameLayout implements OnTouchListener, t.setPosition(dividerCtrl, mDividerPositionX - mDividerInsets, 0); } } - if (getViewRootImpl() != null) { - getHandler().removeCallbacks(mUpdateEmbeddedMatrix); - getHandler().post(mUpdateEmbeddedMatrix); - } } void setResizeDimLayer(Transaction t, boolean primary, float alpha) { 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 index 86bf3ff1cea9..d2f42c39acd5 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitScreenTaskListener.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitScreenTaskListener.java @@ -343,10 +343,20 @@ class LegacySplitScreenTaskListener implements ShellTaskOrganizer.TaskListener { @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); } - b.setParent(mLeashByTaskId.get(taskId)); + return mLeashByTaskId.get(taskId); } @Override 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 new file mode 100644 index 000000000000..b310ee2095bf --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/BackgroundWindowManager.java @@ -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.onehanded; + +import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; +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_WATCH_OUTSIDE_TOUCH; +import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION; +import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY; + +import static com.android.wm.shell.onehanded.OneHandedState.STATE_ACTIVE; +import static com.android.wm.shell.onehanded.OneHandedState.STATE_ENTERING; + +import android.content.Context; +import android.content.res.Configuration; +import android.graphics.Color; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.os.Binder; +import android.util.Slog; +import android.view.ContextThemeWrapper; +import android.view.IWindow; +import android.view.LayoutInflater; +import android.view.SurfaceControl; +import android.view.SurfaceControlViewHost; +import android.view.SurfaceSession; +import android.view.View; +import android.view.WindowManager; +import android.view.WindowlessWindowManager; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.wm.shell.R; +import com.android.wm.shell.common.DisplayLayout; + +import java.io.PrintWriter; + +/** + * Holds view hierarchy of a root surface and helps inflate a themeable view for background. + */ +public final class BackgroundWindowManager extends WindowlessWindowManager { + private static final String TAG = BackgroundWindowManager.class.getSimpleName(); + private static final int THEME_COLOR_OFFSET = 10; + + private final OneHandedSurfaceTransactionHelper.SurfaceControlTransactionFactory + mTransactionFactory; + + private Context mContext; + private Rect mDisplayBounds; + private SurfaceControlViewHost mViewHost; + private SurfaceControl mLeash; + private View mBackgroundView; + private @OneHandedState.State int mCurrentState; + + public BackgroundWindowManager(Context context) { + super(context.getResources().getConfiguration(), null /* rootSurface */, + null /* hostInputToken */); + mContext = context; + mTransactionFactory = SurfaceControl.Transaction::new; + } + + @Override + public SurfaceControl getSurfaceControl(IWindow window) { + return super.getSurfaceControl(window); + } + + @Override + public void setConfiguration(Configuration configuration) { + super.setConfiguration(configuration); + mContext = mContext.createConfigurationContext(configuration); + } + + /** + * onConfigurationChanged events for updating background theme color. + */ + public void onConfigurationChanged() { + if (mCurrentState == STATE_ENTERING || mCurrentState == STATE_ACTIVE) { + updateThemeOnly(); + } + } + + /** + * One-handed mode state changed callback + * @param newState of One-handed mode representing by {@link OneHandedState} + */ + public void onStateChanged(int newState) { + mCurrentState = newState; + } + + @Override + protected void attachToParentSurface(IWindow window, SurfaceControl.Builder b) { + final SurfaceControl.Builder builder = new SurfaceControl.Builder(new SurfaceSession()) + .setColorLayer() + .setBufferSize(mDisplayBounds.width(), mDisplayBounds.height()) + .setFormat(PixelFormat.RGB_888) + .setOpaque(true) + .setName(TAG) + .setCallsite("BackgroundWindowManager#attachToParentSurface"); + mLeash = builder.build(); + b.setParent(mLeash); + } + + /** Inflates background view on to the root surface. */ + boolean initView() { + if (mBackgroundView != null || mViewHost != null) { + return false; + } + + mViewHost = new SurfaceControlViewHost(mContext, mContext.getDisplay(), this); + mBackgroundView = (View) LayoutInflater.from(mContext) + .inflate(R.layout.background_panel, null /* root */); + WindowManager.LayoutParams lp = new WindowManager.LayoutParams( + mDisplayBounds.width(), mDisplayBounds.height(), 0 /* TYPE NONE */, + FLAG_NOT_FOCUSABLE | FLAG_NOT_TOUCH_MODAL | FLAG_WATCH_OUTSIDE_TOUCH + | FLAG_SLIPPERY, PixelFormat.TRANSLUCENT); + lp.token = new Binder(); + lp.setTitle("background-panel"); + lp.privateFlags |= PRIVATE_FLAG_NO_MOVE_ANIMATION | PRIVATE_FLAG_TRUSTED_OVERLAY; + mBackgroundView.setBackgroundColor(getThemeColorForBackground()); + mViewHost.setView(mBackgroundView, lp); + return true; + } + + /** + * Called when onDisplayAdded() or onDisplayRemoved() callback. + * @param displayLayout The latest {@link DisplayLayout} for display bounds. + */ + public void onDisplayChanged(DisplayLayout displayLayout) { + mDisplayBounds = new Rect(0, 0, displayLayout.width(), displayLayout.height()); + } + + private void updateThemeOnly() { + if (mBackgroundView == null || mViewHost == null || mLeash == null) { + Slog.w(TAG, "Background view or SurfaceControl does not exist when trying to " + + "update theme only!"); + return; + } + + WindowManager.LayoutParams lp = (WindowManager.LayoutParams) + mBackgroundView.getLayoutParams(); + mBackgroundView.setBackgroundColor(getThemeColorForBackground()); + mViewHost.setView(mBackgroundView, lp); + } + + /** + * Shows the background layer when One-handed mode triggered. + */ + public void showBackgroundLayer() { + if (!initView()) { + updateThemeOnly(); + return; + } + if (mLeash == null) { + Slog.w(TAG, "SurfaceControl mLeash is null, can't show One-handed mode " + + "background panel!"); + return; + } + + mTransactionFactory.getTransaction() + .setAlpha(mLeash, 1.0f) + .setLayer(mLeash, -1 /* at bottom-most layer */) + .show(mLeash) + .apply(); + } + + /** + * Remove the leash of background layer after stop One-handed mode. + */ + public void removeBackgroundLayer() { + if (mBackgroundView != null) { + mBackgroundView = null; + } + + if (mViewHost != null) { + mViewHost.release(); + mViewHost = null; + } + + if (mLeash != null) { + mTransactionFactory.getTransaction().remove(mLeash).apply(); + mLeash = null; + } + } + + /** + * Gets {@link SurfaceControl} of the background layer. + * @return {@code null} if not exist. + */ + @Nullable + SurfaceControl getSurfaceControl() { + return mLeash; + } + + private int getThemeColor() { + final Context themedContext = new ContextThemeWrapper(mContext, + com.android.internal.R.style.Theme_DeviceDefault_DayNight); + return themedContext.getColor(R.color.one_handed_tutorial_background_color); + } + + int getThemeColorForBackground() { + final int origThemeColor = getThemeColor(); + return android.graphics.Color.argb(Color.alpha(origThemeColor), + Color.red(origThemeColor) - THEME_COLOR_OFFSET, + Color.green(origThemeColor) - THEME_COLOR_OFFSET, + Color.blue(origThemeColor) - THEME_COLOR_OFFSET); + } + + private float adjustColor(int origColor) { + return Math.max(origColor - THEME_COLOR_OFFSET, 0) / 255.0f; + } + + void dump(@NonNull PrintWriter pw) { + final String innerPrefix = " "; + pw.println(TAG); + pw.print(innerPrefix + "mDisplayBounds="); + pw.println(mDisplayBounds); + pw.print(innerPrefix + "mViewHost="); + pw.println(mViewHost); + pw.print(innerPrefix + "mLeash="); + pw.println(mLeash); + pw.print(innerPrefix + "mBackgroundView="); + pw.println(mBackgroundView); + } + +} 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 3253bb06c835..b00182f36cc8 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 @@ -17,6 +17,7 @@ package com.android.wm.shell.onehanded; import android.content.res.Configuration; +import android.os.SystemProperties; import com.android.wm.shell.common.annotations.ExternalThread; @@ -26,6 +27,9 @@ import com.android.wm.shell.common.annotations.ExternalThread; @ExternalThread public interface OneHanded { + boolean sIsSupportOneHandedMode = SystemProperties.getBoolean( + OneHandedController.SUPPORT_ONE_HANDED_MODE, false); + /** * Returns a binder that can be passed to an external process to manipulate OneHanded. */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedBackgroundPanelOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedBackgroundPanelOrganizer.java deleted file mode 100644 index 9e1c61aac868..000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedBackgroundPanelOrganizer.java +++ /dev/null @@ -1,272 +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.onehanded; - -import static com.android.wm.shell.onehanded.OneHandedState.STATE_ACTIVE; - -import android.animation.ValueAnimator; -import android.content.Context; -import android.graphics.Color; -import android.graphics.PixelFormat; -import android.graphics.Rect; -import android.view.ContextThemeWrapper; -import android.view.SurfaceControl; -import android.view.SurfaceSession; -import android.view.animation.LinearInterpolator; -import android.window.DisplayAreaAppearedInfo; -import android.window.DisplayAreaInfo; -import android.window.DisplayAreaOrganizer; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; - -import com.android.wm.shell.R; -import com.android.wm.shell.common.DisplayLayout; - -import java.io.PrintWriter; -import java.util.List; -import java.util.concurrent.Executor; - -/** - * Manages OneHanded color background layer areas. - * To avoid when turning the Dark theme on, users can not clearly identify - * the screen has entered one handed mode. - */ -public class OneHandedBackgroundPanelOrganizer extends DisplayAreaOrganizer - implements OneHandedAnimationCallback, OneHandedState.OnStateChangedListener { - private static final String TAG = "OneHandedBackgroundPanelOrganizer"; - private static final int THEME_COLOR_OFFSET = 10; - private static final int ALPHA_ANIMATION_DURATION = 200; - - private final Context mContext; - private final SurfaceSession mSurfaceSession = new SurfaceSession(); - private final OneHandedSurfaceTransactionHelper.SurfaceControlTransactionFactory - mTransactionFactory; - - private @OneHandedState.State int mCurrentState; - private ValueAnimator mAlphaAnimator; - - private float mTranslationFraction; - private float[] mThemeColor; - - /** - * The background to distinguish the boundary of translated windows and empty region when - * one handed mode triggered. - */ - private Rect mBkgBounds; - private Rect mStableInsets; - - @Nullable - @VisibleForTesting - SurfaceControl mBackgroundSurface; - @Nullable - private SurfaceControl mParentLeash; - - public OneHandedBackgroundPanelOrganizer(Context context, DisplayLayout displayLayout, - OneHandedSettingsUtil settingsUtil, Executor executor) { - super(executor); - mContext = context; - mTranslationFraction = settingsUtil.getTranslationFraction(context); - mTransactionFactory = SurfaceControl.Transaction::new; - updateThemeColors(); - } - - @Override - public void onDisplayAreaAppeared(@NonNull DisplayAreaInfo displayAreaInfo, - @NonNull SurfaceControl leash) { - mParentLeash = leash; - } - - @Override - public List<DisplayAreaAppearedInfo> registerOrganizer(int displayAreaFeature) { - final List<DisplayAreaAppearedInfo> displayAreaInfos; - displayAreaInfos = super.registerOrganizer(displayAreaFeature); - for (int i = 0; i < displayAreaInfos.size(); i++) { - final DisplayAreaAppearedInfo info = displayAreaInfos.get(i); - onDisplayAreaAppeared(info.getDisplayAreaInfo(), info.getLeash()); - } - return displayAreaInfos; - } - - @Override - public void unregisterOrganizer() { - super.unregisterOrganizer(); - removeBackgroundPanelLayer(); - mParentLeash = null; - } - - @Override - public void onAnimationUpdate(SurfaceControl.Transaction tx, float xPos, float yPos) { - final int yTopPos = (mStableInsets.top - mBkgBounds.height()) + Math.round(yPos); - tx.setPosition(mBackgroundSurface, 0, yTopPos); - } - - @Nullable - @VisibleForTesting - boolean isRegistered() { - return mParentLeash != null; - } - - void createBackgroundSurface() { - mBackgroundSurface = new SurfaceControl.Builder(mSurfaceSession) - .setBufferSize(mBkgBounds.width(), mBkgBounds.height()) - .setColorLayer() - .setFormat(PixelFormat.RGB_888) - .setOpaque(true) - .setName("one-handed-background-panel") - .setCallsite("OneHandedBackgroundPanelOrganizer") - .build(); - - // TODO(185890335) Avoid Dimming for mid-range luminance wallpapers flash. - mAlphaAnimator = ValueAnimator.ofFloat(1.0f, 0.0f); - mAlphaAnimator.setInterpolator(new LinearInterpolator()); - mAlphaAnimator.setDuration(ALPHA_ANIMATION_DURATION); - mAlphaAnimator.addUpdateListener( - animator -> detachBackgroundFromParent(animator)); - } - - void detachBackgroundFromParent(ValueAnimator animator) { - if (mBackgroundSurface == null || mParentLeash == null) { - return; - } - // TODO(185890335) Avoid Dimming for mid-range luminance wallpapers flash. - final float currentValue = (float) animator.getAnimatedValue(); - final SurfaceControl.Transaction tx = mTransactionFactory.getTransaction(); - if (currentValue == 0.0f) { - tx.reparent(mBackgroundSurface, null).apply(); - } else { - tx.setAlpha(mBackgroundSurface, (float) animator.getAnimatedValue()).apply(); - } - } - - /** - * Called when onDisplayAdded() or onDisplayRemoved() callback. - * - * @param displayLayout The latest {@link DisplayLayout} representing current displayId - */ - public void onDisplayChanged(DisplayLayout displayLayout) { - mStableInsets = displayLayout.stableInsets(); - // Ensure the mBkgBounds is portrait, due to OHM only support on portrait - if (displayLayout.height() > displayLayout.width()) { - mBkgBounds = new Rect(0, 0, displayLayout.width(), - Math.round(displayLayout.height() * mTranslationFraction) + mStableInsets.top); - } else { - mBkgBounds = new Rect(0, 0, displayLayout.height(), - Math.round(displayLayout.width() * mTranslationFraction) + mStableInsets.top); - } - } - - @VisibleForTesting - void onStart() { - if (mBackgroundSurface == null) { - createBackgroundSurface(); - } - showBackgroundPanelLayer(); - } - - /** - * Called when transition finished. - */ - public void onStopFinished() { - if (mAlphaAnimator == null) { - return; - } - mAlphaAnimator.start(); - } - - @VisibleForTesting - void showBackgroundPanelLayer() { - if (mParentLeash == null) { - return; - } - - if (mBackgroundSurface == null) { - createBackgroundSurface(); - } - - // TODO(185890335) Avoid Dimming for mid-range luminance wallpapers flash. - if (mAlphaAnimator.isRunning()) { - mAlphaAnimator.end(); - } - - mTransactionFactory.getTransaction() - .reparent(mBackgroundSurface, mParentLeash) - .setAlpha(mBackgroundSurface, 1.0f) - .setLayer(mBackgroundSurface, -1 /* at bottom-most layer */) - .setColor(mBackgroundSurface, mThemeColor) - .show(mBackgroundSurface) - .apply(); - } - - @VisibleForTesting - void removeBackgroundPanelLayer() { - if (mBackgroundSurface == null) { - return; - } - - mTransactionFactory.getTransaction() - .remove(mBackgroundSurface) - .apply(); - mBackgroundSurface = null; - } - - /** - * onConfigurationChanged events for updating tutorial text. - */ - public void onConfigurationChanged() { - updateThemeColors(); - - if (mCurrentState != STATE_ACTIVE) { - return; - } - showBackgroundPanelLayer(); - } - - private void updateThemeColors() { - final Context themedContext = new ContextThemeWrapper(mContext, - com.android.internal.R.style.Theme_DeviceDefault_DayNight); - final int themeColor = themedContext.getColor( - R.color.one_handed_tutorial_background_color); - mThemeColor = new float[]{ - adjustColor(Color.red(themeColor)), - adjustColor(Color.green(themeColor)), - adjustColor(Color.blue(themeColor))}; - } - - private float adjustColor(int origColor) { - return Math.max(origColor - THEME_COLOR_OFFSET, 0) / 255.0f; - } - - @Override - public void onStateChanged(int newState) { - mCurrentState = newState; - } - - void dump(@NonNull PrintWriter pw) { - final String innerPrefix = " "; - pw.println(TAG); - pw.print(innerPrefix + "mBackgroundSurface="); - pw.println(mBackgroundSurface); - pw.print(innerPrefix + "mBkgBounds="); - pw.println(mBkgBounds); - pw.print(innerPrefix + "mThemeColor="); - pw.println(mThemeColor); - pw.print(innerPrefix + "mTranslationFraction="); - pw.println(mTranslationFraction); - } -} 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 e0686146e821..179b725ab210 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 @@ -16,7 +16,6 @@ package com.android.wm.shell.onehanded; -import static android.os.UserHandle.USER_CURRENT; import static android.os.UserHandle.myUserId; import static android.view.Display.DEFAULT_DISPLAY; @@ -30,17 +29,14 @@ import android.annotation.BinderThread; import android.content.ComponentName; import android.content.Context; import android.content.om.IOverlayManager; -import android.content.om.OverlayInfo; import android.content.res.Configuration; import android.database.ContentObserver; import android.graphics.Rect; import android.os.Handler; -import android.os.RemoteException; import android.os.ServiceManager; import android.os.SystemProperties; import android.provider.Settings; import android.util.Slog; -import android.view.Surface; import android.view.WindowManager; import android.view.accessibility.AccessibilityManager; import android.window.WindowContainerTransaction; @@ -48,6 +44,7 @@ import android.window.WindowContainerTransaction; import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; +import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.logging.UiEventLogger; import com.android.wm.shell.R; import com.android.wm.shell.common.DisplayChangeController; @@ -70,9 +67,6 @@ public class OneHandedController implements RemoteCallable<OneHandedController>, private static final String ONE_HANDED_MODE_OFFSET_PERCENTAGE = "persist.debug.one_handed_offset_percentage"; - private static final String ONE_HANDED_MODE_GESTURAL_OVERLAY = - "com.android.internal.systemui.onehanded.gestural"; - private static final int OVERLAY_ENABLED_DELAY_MS = 250; private static final int DISPLAY_AREA_READY_RETRY_MS = 10; public static final String SUPPORT_ONE_HANDED_MODE = "ro.support_one_handed_mode"; @@ -104,7 +98,6 @@ public class OneHandedController implements RemoteCallable<OneHandedController>, private OneHandedEventCallback mEventCallback; private OneHandedDisplayAreaOrganizer mDisplayAreaOrganizer; - private OneHandedBackgroundPanelOrganizer mBackgroundPanelOrganizer; private OneHandedUiEventLogger mOneHandedUiEventLogger; private final DisplayController.OnDisplaysChangedListener mDisplaysChangedListener = @@ -168,7 +161,6 @@ public class OneHandedController implements RemoteCallable<OneHandedController>, public void onStopFinished(Rect bounds) { mState.setState(STATE_NONE); notifyShortcutStateChanged(STATE_NONE); - mBackgroundPanelOrganizer.onStopFinished(); } }; @@ -200,37 +192,34 @@ public class OneHandedController implements RemoteCallable<OneHandedController>, public static OneHandedController create( Context context, WindowManager windowManager, DisplayController displayController, DisplayLayout displayLayout, TaskStackListenerImpl taskStackListener, - UiEventLogger uiEventLogger, ShellExecutor mainExecutor, Handler mainHandler) { + InteractionJankMonitor jankMonitor, UiEventLogger uiEventLogger, + ShellExecutor mainExecutor, Handler mainHandler) { OneHandedSettingsUtil settingsUtil = new OneHandedSettingsUtil(); OneHandedAccessibilityUtil accessibilityUtil = new OneHandedAccessibilityUtil(context); OneHandedTimeoutHandler timeoutHandler = new OneHandedTimeoutHandler(mainExecutor); - OneHandedState transitionState = new OneHandedState(); + OneHandedState oneHandedState = new OneHandedState(); + BackgroundWindowManager backgroundWindowManager = new BackgroundWindowManager(context); OneHandedTutorialHandler tutorialHandler = new OneHandedTutorialHandler(context, - settingsUtil, windowManager); + settingsUtil, windowManager, backgroundWindowManager); OneHandedAnimationController animationController = new OneHandedAnimationController(context); OneHandedTouchHandler touchHandler = new OneHandedTouchHandler(timeoutHandler, mainExecutor); - OneHandedBackgroundPanelOrganizer oneHandedBackgroundPanelOrganizer = - new OneHandedBackgroundPanelOrganizer(context, displayLayout, settingsUtil, - mainExecutor); OneHandedDisplayAreaOrganizer organizer = new OneHandedDisplayAreaOrganizer( context, displayLayout, settingsUtil, animationController, tutorialHandler, - oneHandedBackgroundPanelOrganizer, mainExecutor); + jankMonitor, mainExecutor); OneHandedUiEventLogger oneHandedUiEventsLogger = new OneHandedUiEventLogger(uiEventLogger); IOverlayManager overlayManager = IOverlayManager.Stub.asInterface( ServiceManager.getService(Context.OVERLAY_SERVICE)); - return new OneHandedController(context, displayController, - oneHandedBackgroundPanelOrganizer, organizer, touchHandler, tutorialHandler, - settingsUtil, accessibilityUtil, timeoutHandler, transitionState, - oneHandedUiEventsLogger, overlayManager, taskStackListener, mainExecutor, - mainHandler); + return new OneHandedController(context, displayController, organizer, touchHandler, + tutorialHandler, settingsUtil, accessibilityUtil, timeoutHandler, oneHandedState, + jankMonitor, oneHandedUiEventsLogger, overlayManager, taskStackListener, + mainExecutor, mainHandler); } @VisibleForTesting OneHandedController(Context context, DisplayController displayController, - OneHandedBackgroundPanelOrganizer backgroundPanelOrganizer, OneHandedDisplayAreaOrganizer displayAreaOrganizer, OneHandedTouchHandler touchHandler, OneHandedTutorialHandler tutorialHandler, @@ -238,6 +227,7 @@ public class OneHandedController implements RemoteCallable<OneHandedController>, OneHandedAccessibilityUtil oneHandedAccessibilityUtil, OneHandedTimeoutHandler timeoutHandler, OneHandedState state, + InteractionJankMonitor jankMonitor, OneHandedUiEventLogger uiEventsLogger, IOverlayManager overlayManager, TaskStackListenerImpl taskStackListener, @@ -246,7 +236,6 @@ public class OneHandedController implements RemoteCallable<OneHandedController>, mContext = context; mOneHandedSettingsUtil = settingsUtil; mOneHandedAccessibilityUtil = oneHandedAccessibilityUtil; - mBackgroundPanelOrganizer = backgroundPanelOrganizer; mDisplayAreaOrganizer = displayAreaOrganizer; mDisplayController = displayController; mTouchHandler = touchHandler; @@ -282,14 +271,13 @@ public class OneHandedController implements RemoteCallable<OneHandedController>, setupCallback(); registerSettingObservers(mUserId); setupTimeoutListener(); - setupGesturalOverlay(); updateSettings(); + updateDisplayLayout(mContext.getDisplayId()); mAccessibilityManager = AccessibilityManager.getInstance(context); mAccessibilityManager.addAccessibilityStateChangeListener( mAccessibilityStateChangeListener); - mState.addSListeners(mBackgroundPanelOrganizer); mState.addSListeners(mTutorialHandler); } @@ -360,8 +348,7 @@ public class OneHandedController implements RemoteCallable<OneHandedController>, return; } - final int currentRotation = mDisplayAreaOrganizer.getDisplayLayout().rotation(); - if (currentRotation != Surface.ROTATION_0 && currentRotation != Surface.ROTATION_180) { + if (mDisplayAreaOrganizer.getDisplayLayout().isLandscape()) { Slog.w(TAG, "One handed mode only support portrait mode"); return; } @@ -371,7 +358,6 @@ public class OneHandedController implements RemoteCallable<OneHandedController>, mDisplayAreaOrganizer.getDisplayLayout().height() * mOffSetFraction); mOneHandedAccessibilityUtil.announcementForScreenReader( mOneHandedAccessibilityUtil.getOneHandedStartDescription()); - mBackgroundPanelOrganizer.onStart(); mDisplayAreaOrganizer.scheduleOffset(0, yOffSet); mTimeoutHandler.resetTimer(); mOneHandedUiEventLogger.writeEvent( @@ -399,8 +385,10 @@ public class OneHandedController implements RemoteCallable<OneHandedController>, mEventCallback = callback; } - @VisibleForTesting - void registerTransitionCallback(OneHandedTransitionCallback callback) { + /** + * Registers {@link OneHandedTransitionCallback} to monitor the transition status + */ + public void registerTransitionCallback(OneHandedTransitionCallback callback) { mDisplayAreaOrganizer.registerTransitionCallback(callback); } @@ -453,11 +441,15 @@ public class OneHandedController implements RemoteCallable<OneHandedController>, onShortcutEnabledChanged(); } - private void updateDisplayLayout(int displayId) { + @VisibleForTesting + void updateDisplayLayout(int displayId) { final DisplayLayout newDisplayLayout = mDisplayController.getDisplayLayout(displayId); + if (newDisplayLayout == null) { + Slog.w(TAG, "Failed to get new DisplayLayout."); + return; + } mDisplayAreaOrganizer.setDisplayLayout(newDisplayLayout); mTutorialHandler.onDisplayChanged(newDisplayLayout); - mBackgroundPanelOrganizer.onDisplayChanged(newDisplayLayout); } private ContentObserver getObserver(Runnable onChangeRunnable) { @@ -517,11 +509,6 @@ public class OneHandedController implements RemoteCallable<OneHandedController>, : OneHandedUiEventLogger.EVENT_ONE_HANDED_SETTINGS_ENABLED_OFF); setOneHandedEnabled(enabled); - - // Also checks swipe to notification settings since they all need gesture overlay. - setEnabledGesturalOverlay( - enabled || mOneHandedSettingsUtil.getSettingsSwipeToNotificationEnabled( - mContext.getContentResolver(), mUserId), true /* DelayExecute */); } @VisibleForTesting @@ -586,7 +573,6 @@ public class OneHandedController implements RemoteCallable<OneHandedController>, if (!mIsOneHandedEnabled) { mDisplayAreaOrganizer.unregisterOrganizer(); - mBackgroundPanelOrganizer.unregisterOrganizer(); // Do NOT register + unRegister DA in the same call return; } @@ -595,45 +581,6 @@ public class OneHandedController implements RemoteCallable<OneHandedController>, mDisplayAreaOrganizer.registerOrganizer( OneHandedDisplayAreaOrganizer.FEATURE_ONE_HANDED); } - - if (!mBackgroundPanelOrganizer.isRegistered()) { - mBackgroundPanelOrganizer.registerOrganizer( - OneHandedBackgroundPanelOrganizer.FEATURE_ONE_HANDED_BACKGROUND_PANEL); - } - } - - private void setupGesturalOverlay() { - if (!mOneHandedSettingsUtil.getSettingsOneHandedModeEnabled( - mContext.getContentResolver(), mUserId)) { - return; - } - - OverlayInfo info = null; - try { - mOverlayManager.setHighestPriority(ONE_HANDED_MODE_GESTURAL_OVERLAY, USER_CURRENT); - info = mOverlayManager.getOverlayInfo(ONE_HANDED_MODE_GESTURAL_OVERLAY, USER_CURRENT); - } catch (RemoteException e) { /* Do nothing */ } - - if (info != null && !info.isEnabled()) { - // Enable the default gestural one handed overlay. - setEnabledGesturalOverlay(true /* enabled */, false /* delayExecute */); - } - } - - @VisibleForTesting - private void setEnabledGesturalOverlay(boolean enabled, boolean delayExecute) { - if (mState.isTransitioning() || delayExecute) { - // Enabled overlay package may affect the current animation(e.g:Settings switch), - // so we delay 250ms to enabled overlay after switch animation finish, only delay once. - mMainExecutor.executeDelayed(() -> setEnabledGesturalOverlay(enabled, false), - OVERLAY_ENABLED_DELAY_MS); - return; - } - try { - mOverlayManager.setEnabled(ONE_HANDED_MODE_GESTURAL_OVERLAY, enabled, USER_CURRENT); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } } @VisibleForTesting @@ -648,13 +595,12 @@ public class OneHandedController implements RemoteCallable<OneHandedController>, } private void onConfigChanged(Configuration newConfig) { - if (mTutorialHandler == null || mBackgroundPanelOrganizer == null) { + if (mTutorialHandler == null) { return; } if (!mIsOneHandedEnabled || newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) { return; } - mBackgroundPanelOrganizer.onConfigurationChanged(); mTutorialHandler.onConfigurationChanged(); } @@ -685,10 +631,6 @@ public class OneHandedController implements RemoteCallable<OneHandedController>, pw.print(innerPrefix + "mIsSwipeToNotificationEnabled="); pw.println(mIsSwipeToNotificationEnabled); - if (mBackgroundPanelOrganizer != null) { - mBackgroundPanelOrganizer.dump(pw); - } - if (mDisplayAreaOrganizer != null) { mDisplayAreaOrganizer.dump(pw); } @@ -714,19 +656,6 @@ public class OneHandedController implements RemoteCallable<OneHandedController>, } mOneHandedSettingsUtil.dump(pw, innerPrefix, mContext.getContentResolver(), mUserId); - - if (mOverlayManager != null) { - OverlayInfo info = null; - try { - info = mOverlayManager.getOverlayInfo(ONE_HANDED_MODE_GESTURAL_OVERLAY, - USER_CURRENT); - } catch (RemoteException e) { /* Do nothing */ } - - if (info != null && !info.isEnabled()) { - pw.print(innerPrefix + "OverlayInfo="); - pw.println(info); - } - } } /** 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 1b2f4768110b..f61d1b95bd85 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 @@ -16,12 +16,15 @@ package com.android.wm.shell.onehanded; +import static com.android.internal.jank.InteractionJankMonitor.CUJ_ONE_HANDED_ENTER_TRANSITION; +import static com.android.internal.jank.InteractionJankMonitor.CUJ_ONE_HANDED_EXIT_TRANSITION; import static com.android.wm.shell.onehanded.OneHandedAnimationController.TRANSITION_DIRECTION_EXIT; import static com.android.wm.shell.onehanded.OneHandedAnimationController.TRANSITION_DIRECTION_TRIGGER; import android.content.Context; import android.graphics.Rect; import android.os.SystemProperties; +import android.text.TextUtils; import android.util.ArrayMap; import android.view.SurfaceControl; import android.window.DisplayAreaAppearedInfo; @@ -34,6 +37,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; +import com.android.internal.jank.InteractionJankMonitor; import com.android.wm.shell.R; import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.ShellExecutor; @@ -41,6 +45,7 @@ import com.android.wm.shell.common.ShellExecutor; import java.io.PrintWriter; import java.util.ArrayList; import java.util.List; +import java.util.Map; /** * Manages OneHanded display areas such as offset. @@ -62,6 +67,8 @@ public class OneHandedDisplayAreaOrganizer extends DisplayAreaOrganizer { private final Rect mLastVisualDisplayBounds = new Rect(); private final Rect mDefaultDisplayBounds = new Rect(); private final OneHandedSettingsUtil mOneHandedSettingsUtil; + private final InteractionJankMonitor mJankMonitor; + private final Context mContext; private boolean mIsReady; private float mLastVisualOffset = 0; @@ -73,7 +80,6 @@ public class OneHandedDisplayAreaOrganizer extends DisplayAreaOrganizer { mSurfaceControlTransactionFactory; private OneHandedTutorialHandler mTutorialHandler; private List<OneHandedTransitionCallback> mTransitionCallbacks = new ArrayList<>(); - private OneHandedBackgroundPanelOrganizer mBackgroundPanelOrganizer; @VisibleForTesting OneHandedAnimationCallback mOneHandedAnimationCallback = @@ -95,7 +101,11 @@ public class OneHandedDisplayAreaOrganizer extends DisplayAreaOrganizer { public void onOneHandedAnimationEnd(SurfaceControl.Transaction tx, OneHandedAnimationController.OneHandedTransitionAnimator animator) { mAnimationController.removeAnimator(animator.getToken()); + final boolean isEntering = animator.getTransitionDirection() + == TRANSITION_DIRECTION_TRIGGER; if (mAnimationController.isAnimatorsConsumed()) { + endCUJTracing(isEntering ? CUJ_ONE_HANDED_ENTER_TRANSITION + : CUJ_ONE_HANDED_EXIT_TRANSITION); finishOffset((int) animator.getDestinationOffset(), animator.getTransitionDirection()); } @@ -105,7 +115,11 @@ public class OneHandedDisplayAreaOrganizer extends DisplayAreaOrganizer { public void onOneHandedAnimationCancel( OneHandedAnimationController.OneHandedTransitionAnimator animator) { mAnimationController.removeAnimator(animator.getToken()); + final boolean isEntering = animator.getTransitionDirection() + == TRANSITION_DIRECTION_TRIGGER; if (mAnimationController.isAnimatorsConsumed()) { + cancelCUJTracing(isEntering ? CUJ_ONE_HANDED_ENTER_TRANSITION + : CUJ_ONE_HANDED_EXIT_TRANSITION); finishOffset((int) animator.getDestinationOffset(), animator.getTransitionDirection()); } @@ -120,20 +134,20 @@ public class OneHandedDisplayAreaOrganizer extends DisplayAreaOrganizer { OneHandedSettingsUtil oneHandedSettingsUtil, OneHandedAnimationController animationController, OneHandedTutorialHandler tutorialHandler, - OneHandedBackgroundPanelOrganizer oneHandedBackgroundGradientOrganizer, + InteractionJankMonitor jankMonitor, ShellExecutor mainExecutor) { super(mainExecutor); - mDisplayLayout.set(displayLayout); + mContext = context; + setDisplayLayout(displayLayout); mOneHandedSettingsUtil = oneHandedSettingsUtil; - updateDisplayBounds(); mAnimationController = animationController; + mJankMonitor = jankMonitor; final int animationDurationConfig = context.getResources().getInteger( R.integer.config_one_handed_translate_animation_duration); mEnterExitAnimationDurationMs = SystemProperties.getInt(ONE_HANDED_MODE_TRANSLATE_ANIMATION_DURATION, animationDurationConfig); mSurfaceControlTransactionFactory = SurfaceControl.Transaction::new; - mBackgroundPanelOrganizer = oneHandedBackgroundGradientOrganizer; mTutorialHandler = tutorialHandler; } @@ -198,6 +212,11 @@ public class OneHandedDisplayAreaOrganizer extends DisplayAreaOrganizer { final int direction = yOffset > 0 ? TRANSITION_DIRECTION_TRIGGER : TRANSITION_DIRECTION_EXIT; + if (direction == TRANSITION_DIRECTION_TRIGGER) { + beginCUJTracing(CUJ_ONE_HANDED_ENTER_TRANSITION, "enterOneHanded"); + } else { + beginCUJTracing(CUJ_ONE_HANDED_EXIT_TRANSITION, "stopOneHanded"); + } mDisplayAreaTokenMap.forEach( (token, leash) -> { animateWindows(token, leash, fromPos, yOffset, direction, @@ -236,7 +255,6 @@ public class OneHandedDisplayAreaOrganizer extends DisplayAreaOrganizer { animator.setTransitionDirection(direction) .addOneHandedAnimationCallback(mOneHandedAnimationCallback) .addOneHandedAnimationCallback(mTutorialHandler) - .addOneHandedAnimationCallback(mBackgroundPanelOrganizer) .setDuration(durationMs) .start(); } @@ -282,6 +300,7 @@ public class OneHandedDisplayAreaOrganizer extends DisplayAreaOrganizer { @VisibleForTesting void setDisplayLayout(@NonNull DisplayLayout displayLayout) { mDisplayLayout.set(displayLayout); + updateDisplayBounds(); } @VisibleForTesting @@ -289,6 +308,7 @@ public class OneHandedDisplayAreaOrganizer extends DisplayAreaOrganizer { return mDisplayAreaTokenMap; } + @VisibleForTesting void updateDisplayBounds() { mDefaultDisplayBounds.set(0, 0, mDisplayLayout.width(), mDisplayLayout.height()); mLastVisualDisplayBounds.set(mDefaultDisplayBounds); @@ -301,6 +321,26 @@ public class OneHandedDisplayAreaOrganizer extends DisplayAreaOrganizer { mTransitionCallbacks.add(callback); } + void beginCUJTracing(@InteractionJankMonitor.CujType int cujType, @Nullable String tag) { + final Map.Entry<WindowContainerToken, SurfaceControl> firstEntry = + getDisplayAreaTokenMap().entrySet().iterator().next(); + final InteractionJankMonitor.Configuration.Builder builder = + InteractionJankMonitor.Configuration.Builder.withSurface( + cujType, mContext, firstEntry.getValue()); + if (!TextUtils.isEmpty(tag)) { + builder.setTag(tag); + } + mJankMonitor.begin(builder); + } + + void endCUJTracing(@InteractionJankMonitor.CujType int cujType) { + mJankMonitor.end(cujType); + } + + void cancelCUJTracing(@InteractionJankMonitor.CujType int cujType) { + mJankMonitor.cancel(cujType); + } + void dump(@NonNull PrintWriter pw) { final String innerPrefix = " "; pw.println(TAG); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedTutorialHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedTutorialHandler.java index 81dd60d715e9..fe997b93616b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedTutorialHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedTutorialHandler.java @@ -64,6 +64,7 @@ public class OneHandedTutorialHandler implements OneHandedTransitionCallback, private final float mTutorialHeightRatio; private final WindowManager mWindowManager; + private final BackgroundWindowManager mBackgroundWindowManager; private @OneHandedState.State int mCurrentState; private int mTutorialAreaHeight; @@ -78,9 +79,10 @@ public class OneHandedTutorialHandler implements OneHandedTransitionCallback, private int mAlphaAnimationDurationMs; public OneHandedTutorialHandler(Context context, OneHandedSettingsUtil settingsUtil, - WindowManager windowManager) { + WindowManager windowManager, BackgroundWindowManager backgroundWindowManager) { mContext = context; mWindowManager = windowManager; + mBackgroundWindowManager = backgroundWindowManager; mTutorialHeightRatio = settingsUtil.getTranslationFraction(context); mAlphaAnimationDurationMs = settingsUtil.getTransitionDuration(context); } @@ -109,8 +111,19 @@ public class OneHandedTutorialHandler implements OneHandedTransitionCallback, } @Override + public void onStartFinished(Rect bounds) { + fillBackgroundColor(); + } + + @Override + public void onStopFinished(Rect bounds) { + removeBackgroundSurface(); + } + + @Override public void onStateChanged(int newState) { mCurrentState = newState; + mBackgroundWindowManager.onStateChanged(newState); switch (newState) { case STATE_ENTERING: createViewAndAttachToWindow(mContext); @@ -125,7 +138,6 @@ public class OneHandedTutorialHandler implements OneHandedTransitionCallback, case STATE_NONE: checkTransitionEnd(); removeTutorialFromWindowManager(); - break; default: break; } @@ -137,14 +149,10 @@ public class OneHandedTutorialHandler implements OneHandedTransitionCallback, * @param displayLayout The latest {@link DisplayLayout} representing current displayId */ public void onDisplayChanged(DisplayLayout displayLayout) { - // Ensure the mDisplayBounds is portrait, due to OHM only support on portrait - if (displayLayout.height() > displayLayout.width()) { - mDisplayBounds = new Rect(0, 0, displayLayout.width(), displayLayout.height()); - } else { - mDisplayBounds = new Rect(0, 0, displayLayout.height(), displayLayout.width()); - } + mDisplayBounds = new Rect(0, 0, displayLayout.width(), displayLayout.height()); mTutorialAreaHeight = Math.round(mDisplayBounds.height() * mTutorialHeightRatio); mAlphaTransitionStart = mTutorialAreaHeight * START_TRANSITION_FRACTION; + mBackgroundWindowManager.onDisplayChanged(displayLayout); } @VisibleForTesting @@ -168,6 +176,7 @@ public class OneHandedTutorialHandler implements OneHandedTransitionCallback, private void attachTargetToWindow() { try { mWindowManager.addView(mTargetViewContainer, getTutorialTargetLayoutParams()); + mBackgroundWindowManager.showBackgroundLayer(); } catch (IllegalStateException e) { // This shouldn't happen, but if the target is already added, just update its // layout params. @@ -185,6 +194,11 @@ public class OneHandedTutorialHandler implements OneHandedTransitionCallback, mTargetViewContainer = null; } + @VisibleForTesting + void removeBackgroundSurface() { + mBackgroundWindowManager.removeBackgroundLayer(); + } + /** * Returns layout params for the dismiss target, using the latest display metrics. */ @@ -212,9 +226,12 @@ public class OneHandedTutorialHandler implements OneHandedTransitionCallback, * onConfigurationChanged events for updating tutorial text. */ public void onConfigurationChanged() { + mBackgroundWindowManager.onConfigurationChanged(); + removeTutorialFromWindowManager(); if (mCurrentState == STATE_ENTERING || mCurrentState == STATE_ACTIVE) { createViewAndAttachToWindow(mContext); + fillBackgroundColor(); updateThemeColor(); checkTransitionEnd(); } @@ -246,6 +263,14 @@ public class OneHandedTutorialHandler implements OneHandedTransitionCallback, tutorialDesc.setTextColor(themedTextColorSecondary); } + private void fillBackgroundColor() { + if (mTargetViewContainer == null || mBackgroundWindowManager == null) { + return; + } + mTargetViewContainer.setBackgroundColor( + mBackgroundWindowManager.getThemeColorForBackground()); + } + private void setupAlphaTransition(boolean isEntering) { final float start = isEntering ? 0.0f : 1.0f; final float end = isEntering ? 1.0f : 0.0f; @@ -281,5 +306,9 @@ public class OneHandedTutorialHandler implements OneHandedTransitionCallback, pw.println(mAlphaTransitionStart); pw.print(innerPrefix + "mAlphaAnimationDurationMs="); pw.println(mAlphaAnimationDurationMs); + + if (mBackgroundWindowManager != null) { + mBackgroundWindowManager.dump(pw); + } } } 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 ddc85f758916..e03421dd58ac 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 @@ -47,12 +47,13 @@ interface IPip { /** * Notifies the swiping Activity to PiP onto home transition is finished * + * @param taskId the Task id that the Activity and overlay are currently in. * @param componentName ComponentName represents the Activity * @param destinationBounds the destination bounds the PiP window lands into * @param overlay an optional overlay to fade out after entering PiP */ - oneway void stopSwipePipToHome(in ComponentName componentName, in Rect destinationBounds, - in SurfaceControl overlay) = 2; + oneway void stopSwipePipToHome(int taskId, in ComponentName componentName, + in Rect destinationBounds, in SurfaceControl overlay) = 2; /** * Sets listener to get pinned stack animation callbacks. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/IPipAnimationListener.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/IPipAnimationListener.aidl index b4c745fc4892..062e3ba26356 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/IPipAnimationListener.aidl +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/IPipAnimationListener.aidl @@ -26,10 +26,16 @@ oneway interface IPipAnimationListener { void onPipAnimationStarted(); /** - * Notifies the listener about PiP round corner radius changes. + * Notifies the listener about PiP resource dimensions changed. * Listener can expect an immediate callback the first time they attach. * * @param cornerRadius the pixel value of the corner radius, zero means it's disabled. + * @param shadowRadius the pixel value of the shadow radius, zero means it's disabled. */ - void onPipCornerRadiusChanged(int cornerRadius); + void onPipResourceDimensionsChanged(int cornerRadius, int shadowRadius); + + /** + * Notifies the listener that user leaves PiP by tapping on the expand button. + */ + void onExpandPip(); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PinnedStackListenerForwarder.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PinnedStackListenerForwarder.java index b3b1ba7cd1c1..ce98458c0575 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PinnedStackListenerForwarder.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PinnedStackListenerForwarder.java @@ -16,9 +16,7 @@ package com.android.wm.shell.pip; -import android.app.RemoteAction; import android.content.ComponentName; -import android.content.pm.ParceledListSlice; import android.os.RemoteException; import android.view.IPinnedTaskListener; import android.view.WindowManagerGlobal; @@ -72,24 +70,12 @@ public class PinnedStackListenerForwarder { } } - private void onActionsChanged(ParceledListSlice<RemoteAction> actions) { - for (PinnedTaskListener listener : mListeners) { - listener.onActionsChanged(actions); - } - } - private void onActivityHidden(ComponentName componentName) { for (PinnedTaskListener listener : mListeners) { listener.onActivityHidden(componentName); } } - private void onAspectRatioChanged(float aspectRatio) { - for (PinnedTaskListener listener : mListeners) { - listener.onAspectRatioChanged(aspectRatio); - } - } - @BinderThread private class PinnedTaskListenerImpl extends IPinnedTaskListener.Stub { @Override @@ -107,25 +93,11 @@ public class PinnedStackListenerForwarder { } @Override - public void onActionsChanged(ParceledListSlice<RemoteAction> actions) { - mMainExecutor.execute(() -> { - PinnedStackListenerForwarder.this.onActionsChanged(actions); - }); - } - - @Override public void onActivityHidden(ComponentName componentName) { mMainExecutor.execute(() -> { PinnedStackListenerForwarder.this.onActivityHidden(componentName); }); } - - @Override - public void onAspectRatioChanged(float aspectRatio) { - mMainExecutor.execute(() -> { - PinnedStackListenerForwarder.this.onAspectRatioChanged(aspectRatio); - }); - } } /** @@ -137,10 +109,6 @@ public class PinnedStackListenerForwarder { public void onImeVisibilityChanged(boolean imeVisible, int imeHeight) {} - public void onActionsChanged(ParceledListSlice<RemoteAction> actions) {} - public void onActivityHidden(ComponentName componentName) {} - - public void onAspectRatioChanged(float aspectRatio) {} } } 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 c0734e95ecb7..3b3091a9caf3 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 @@ -44,11 +44,6 @@ public interface Pip { } /** - * Hides the PIP menu. - */ - default void hidePipMenu(Runnable onStartCallback, Runnable onEndCallback) {} - - /** * Called when configuration is changed. */ default void onConfigurationChanged(Configuration newConfig) { @@ -125,6 +120,23 @@ public interface Pip { 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. 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 9575b0a720bc..4eba1697b595 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 @@ -25,15 +25,14 @@ import android.animation.Animator; import android.animation.RectEvaluator; import android.animation.ValueAnimator; import android.annotation.IntDef; +import android.annotation.NonNull; import android.app.TaskInfo; import android.content.Context; -import android.content.res.TypedArray; -import android.graphics.Color; import android.graphics.Rect; import android.view.Choreographer; import android.view.Surface; import android.view.SurfaceControl; -import android.view.SurfaceSession; +import android.window.TaskSnapshot; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.graphics.SfVsyncFrameCallbackProvider; @@ -197,6 +196,15 @@ public class PipAnimationController { } /** + * Quietly cancel the animator by removing the listeners first. + */ + static void quietCancel(@NonNull ValueAnimator animator) { + animator.removeAllUpdateListeners(); + animator.removeAllListeners(); + animator.cancel(); + } + + /** * Additional callback interface for PiP animation */ public static class PipAnimationCallback { @@ -251,18 +259,17 @@ public class PipAnimationController { protected T mCurrentValue; protected T mStartValue; private T mEndValue; - private float mStartingAngle; private PipAnimationCallback mPipAnimationCallback; private PipTransactionHandler mPipTransactionHandler; private PipSurfaceTransactionHelper.SurfaceControlTransactionFactory mSurfaceControlTransactionFactory; private PipSurfaceTransactionHelper mSurfaceTransactionHelper; private @TransitionDirection int mTransitionDirection; - protected SurfaceControl mContentOverlay; + protected PipContentOverlay mContentOverlay; private PipTransitionAnimator(TaskInfo taskInfo, SurfaceControl leash, - @AnimationType int animationType, Rect destinationBounds, T baseValue, T startValue, - T endValue, float startingAngle) { + @AnimationType int animationType, + Rect destinationBounds, T baseValue, T startValue, T endValue) { mTaskInfo = taskInfo; mLeash = leash; mAnimationType = animationType; @@ -270,7 +277,6 @@ public class PipAnimationController { mBaseValue = baseValue; mStartValue = startValue; mEndValue = endValue; - mStartingAngle = startingAngle; addListener(this); addUpdateListener(this); mSurfaceControlTransactionFactory = SurfaceControl.Transaction::new; @@ -337,43 +343,26 @@ public class PipAnimationController { return false; } - SurfaceControl getContentOverlay() { - return mContentOverlay; + SurfaceControl getContentOverlayLeash() { + return mContentOverlay == null ? null : mContentOverlay.mLeash; } - PipTransitionAnimator<T> setUseContentOverlay(Context context) { + void setColorContentOverlay(Context context) { final SurfaceControl.Transaction tx = newSurfaceControlTransaction(); if (mContentOverlay != null) { - // remove existing content overlay if there is any. - tx.remove(mContentOverlay); - tx.apply(); + mContentOverlay.detach(tx); } - mContentOverlay = new SurfaceControl.Builder(new SurfaceSession()) - .setCallsite("PipAnimation") - .setName("PipContentOverlay") - .setColorLayer() - .build(); - tx.show(mContentOverlay); - tx.setLayer(mContentOverlay, Integer.MAX_VALUE); - tx.setColor(mContentOverlay, getContentOverlayColor(context)); - tx.setAlpha(mContentOverlay, 0f); - tx.reparent(mContentOverlay, mLeash); - tx.apply(); - return this; + mContentOverlay = new PipContentOverlay.PipColorOverlay(context); + mContentOverlay.attach(tx, mLeash); } - private float[] getContentOverlayColor(Context context) { - final TypedArray ta = context.obtainStyledAttributes(new int[] { - android.R.attr.colorBackground }); - try { - int colorAccent = ta.getColor(0, 0); - return new float[] { - Color.red(colorAccent) / 255f, - Color.green(colorAccent) / 255f, - Color.blue(colorAccent) / 255f }; - } finally { - ta.recycle(); + void setSnapshotContentOverlay(TaskSnapshot snapshot, Rect sourceRectHint) { + final SurfaceControl.Transaction tx = newSurfaceControlTransaction(); + if (mContentOverlay != null) { + mContentOverlay.detach(tx); } + mContentOverlay = new PipContentOverlay.PipSnapshotOverlay(snapshot, sourceRectHint); + mContentOverlay.attach(tx, mLeash); } /** @@ -429,6 +418,11 @@ public class PipAnimationController { return !isOutPipDirection(mTransitionDirection); } + boolean shouldApplyShadowRadius() { + return !isOutPipDirection(mTransitionDirection) + && !isRemovePipDirection(mTransitionDirection); + } + boolean inScaleTransition() { if (mAnimationType != ANIM_TYPE_BOUNDS) return false; final int direction = getTransitionDirection(); @@ -482,14 +476,15 @@ public class PipAnimationController { static PipTransitionAnimator<Float> ofAlpha(TaskInfo taskInfo, SurfaceControl leash, Rect destinationBounds, float startValue, float endValue) { return new PipTransitionAnimator<Float>(taskInfo, leash, ANIM_TYPE_ALPHA, - destinationBounds, startValue, startValue, endValue, 0) { + destinationBounds, startValue, startValue, endValue) { @Override void applySurfaceControlTransaction(SurfaceControl leash, SurfaceControl.Transaction tx, float fraction) { final float alpha = getStartValue() * (1 - fraction) + getEndValue() * fraction; setCurrentValue(alpha); getSurfaceTransactionHelper().alpha(tx, leash, alpha) - .round(tx, leash, shouldApplyCornerRadius()); + .round(tx, leash, shouldApplyCornerRadius()) + .shadow(tx, leash, shouldApplyShadowRadius()); tx.apply(); } @@ -502,7 +497,8 @@ public class PipAnimationController { getSurfaceTransactionHelper() .resetScale(tx, leash, getDestinationBounds()) .crop(tx, leash, getDestinationBounds()) - .round(tx, leash, shouldApplyCornerRadius()); + .round(tx, leash, shouldApplyCornerRadius()) + .shadow(tx, leash, shouldApplyShadowRadius()); tx.show(leash); tx.apply(); } @@ -520,7 +516,7 @@ public class PipAnimationController { @PipAnimationController.TransitionDirection int direction, float startingAngle, @Surface.Rotation int rotationDelta) { final boolean isOutPipDirection = isOutPipDirection(direction); - + final boolean isInPipDirection = isInPipDirection(direction); // Just for simplicity we'll interpolate between the source rect hint insets and empty // insets to calculate the window crop final Rect initialSourceValue; @@ -559,8 +555,7 @@ public class PipAnimationController { // construct new Rect instances in case they are recycled return new PipTransitionAnimator<Rect>(taskInfo, leash, ANIM_TYPE_BOUNDS, - endValue, new Rect(baseValue), new Rect(startValue), new Rect(endValue), - startingAngle) { + endValue, new Rect(baseValue), new Rect(startValue), new Rect(endValue)) { private final RectEvaluator mRectEvaluator = new RectEvaluator(new Rect()); private final RectEvaluator mInsetsEvaluator = new RectEvaluator(new Rect()); @@ -571,7 +566,7 @@ public class PipAnimationController { final Rect start = getStartValue(); final Rect end = getEndValue(); if (mContentOverlay != null) { - tx.setAlpha(mContentOverlay, fraction < 0.5f ? 0 : (fraction - 0.5f) * 2); + mContentOverlay.onAnimationUpdate(tx, fraction); } if (rotatedEndRect != null) { // Animate the bounds in a different orientation. It only happens when @@ -584,20 +579,25 @@ public class PipAnimationController { setCurrentValue(bounds); if (inScaleTransition() || sourceHintRect == null) { if (isOutPipDirection) { - getSurfaceTransactionHelper().scale(tx, leash, end, bounds); + getSurfaceTransactionHelper().crop(tx, leash, end) + .scale(tx, leash, end, bounds); } else { - getSurfaceTransactionHelper().scale(tx, leash, base, bounds, angle) - .round(tx, leash, base, bounds); + getSurfaceTransactionHelper().crop(tx, leash, base) + .scale(tx, leash, base, bounds, angle) + .round(tx, leash, base, bounds) + .shadow(tx, leash, shouldApplyShadowRadius()); } } else { final Rect insets = computeInsets(fraction); getSurfaceTransactionHelper().scaleAndCrop(tx, leash, - initialSourceValue, bounds, insets); + sourceHintRect, initialSourceValue, bounds, insets, + isInPipDirection); if (shouldApplyCornerRadius()) { final Rect sourceBounds = new Rect(initialContainerRect); sourceBounds.inset(insets); - getSurfaceTransactionHelper().round(tx, leash, - sourceBounds, bounds); + getSurfaceTransactionHelper() + .round(tx, leash, sourceBounds, bounds) + .shadow(tx, leash, shouldApplyShadowRadius()); } } if (!handlePipTransaction(leash, tx, bounds)) { @@ -618,17 +618,17 @@ public class PipAnimationController { setCurrentValue(bounds); final Rect insets = computeInsets(fraction); final float degree, x, y; - if (Transitions.ENABLE_SHELL_TRANSITIONS) { + if (Transitions.SHELL_TRANSITIONS_ROTATION) { if (rotationDelta == ROTATION_90) { degree = 90 * (1 - fraction); x = fraction * (end.left - start.left) - + start.left + start.right * (1 - fraction); + + start.left + start.width() * (1 - fraction); y = fraction * (end.top - start.top) + start.top; } else { degree = -90 * (1 - fraction); x = fraction * (end.left - start.left) + start.left; y = fraction * (end.top - start.top) - + start.top + start.bottom * (1 - fraction); + + start.top + start.height() * (1 - fraction); } } else { if (rotationDelta == ROTATION_90) { @@ -646,8 +646,12 @@ public class PipAnimationController { getSurfaceTransactionHelper() .rotateAndScaleWithCrop(tx, leash, initialContainerRect, bounds, insets, degree, x, y, isOutPipDirection, - rotationDelta == ROTATION_270 /* clockwise */) - .round(tx, leash, sourceBounds, bounds); + rotationDelta == ROTATION_270 /* clockwise */); + if (shouldApplyCornerRadius()) { + getSurfaceTransactionHelper() + .round(tx, leash, sourceBounds, bounds) + .shadow(tx, leash, shouldApplyShadowRadius()); + } tx.apply(); } @@ -664,9 +668,10 @@ public class PipAnimationController { void onStartTransaction(SurfaceControl leash, SurfaceControl.Transaction tx) { getSurfaceTransactionHelper() .alpha(tx, leash, 1f) - .round(tx, leash, shouldApplyCornerRadius()); + .round(tx, leash, shouldApplyCornerRadius()) + .shadow(tx, leash, shouldApplyShadowRadius()); // TODO(b/178632364): this is a work around for the black background when - // entering PiP in buttion navigation mode. + // entering PiP in button navigation mode. if (isInPipDirection(direction)) { tx.setWindowCrop(leash, getStartValue()); } @@ -690,6 +695,9 @@ public class PipAnimationController { } else { getSurfaceTransactionHelper().crop(tx, leash, destBounds); } + if (mContentOverlay != null) { + mContentOverlay.onAnimationEnd(tx, destBounds); + } } @Override diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipAppOpsListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAppOpsListener.java index d97d2d6ebb4f..48a3fc2460a2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipAppOpsListener.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAppOpsListener.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.pip.phone; +package com.android.wm.shell.pip; import static android.app.AppOpsManager.MODE_ALLOWED; import static android.app.AppOpsManager.OP_PICTURE_IN_PICTURE; @@ -28,7 +28,6 @@ import android.content.pm.PackageManager.NameNotFoundException; import android.util.Pair; import com.android.wm.shell.common.ShellExecutor; -import com.android.wm.shell.pip.PipUtils; public class PipAppOpsListener { private static final String TAG = PipAppOpsListener.class.getSimpleName(); 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 a4b866aa3f5e..7397e5273753 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 @@ -32,6 +32,7 @@ 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 java.io.PrintWriter; @@ -56,7 +57,7 @@ public class PipBoundsAlgorithm { private int mDefaultStackGravity; private int mDefaultMinSize; private int mOverridableMinSize; - private Point mScreenEdgeInsets; + protected Point mScreenEdgeInsets; public PipBoundsAlgorithm(Context context, @NonNull PipBoundsState pipBoundsState, @NonNull PipSnapAlgorithm pipSnapAlgorithm) { @@ -76,15 +77,15 @@ public class PipBoundsAlgorithm { private void reloadResources(Context context) { final Resources res = context.getResources(); mDefaultAspectRatio = res.getFloat( - com.android.internal.R.dimen.config_pictureInPictureDefaultAspectRatio); + R.dimen.config_pictureInPictureDefaultAspectRatio); mDefaultStackGravity = res.getInteger( - com.android.internal.R.integer.config_defaultPictureInPictureGravity); + R.integer.config_defaultPictureInPictureGravity); mDefaultMinSize = res.getDimensionPixelSize( - com.android.internal.R.dimen.default_minimal_size_pip_resizable_task); + R.dimen.default_minimal_size_pip_resizable_task); mOverridableMinSize = res.getDimensionPixelSize( - com.android.internal.R.dimen.overridable_minimal_size_pip_resizable_task); + R.dimen.overridable_minimal_size_pip_resizable_task); final String screenEdgeInsetsDpString = res.getString( - com.android.internal.R.string.config_defaultPictureInPictureScreenEdgeInsets); + R.string.config_defaultPictureInPictureScreenEdgeInsets); final Size screenEdgeInsetsDp = !screenEdgeInsetsDpString.isEmpty() ? Size.parseSize(screenEdgeInsetsDpString) : null; @@ -96,9 +97,9 @@ public class PipBoundsAlgorithm { mMaxAspectRatio = res.getFloat( com.android.internal.R.dimen.config_pictureInPictureMaxAspectRatio); mDefaultSizePercent = res.getFloat( - com.android.internal.R.dimen.config_pictureInPictureDefaultSizePercent); + R.dimen.config_pictureInPictureDefaultSizePercent); mMaxAspectRatioForMinSize = res.getFloat( - com.android.internal.R.dimen.config_pictureInPictureAspectRatioLimitForMinSize); + R.dimen.config_pictureInPictureAspectRatioLimitForMinSize); mMinAspectRatioForMinSize = 1f / mMaxAspectRatioForMinSize; } @@ -193,7 +194,7 @@ public class PipBoundsAlgorithm { public float getAspectRatioOrDefault( @android.annotation.Nullable PictureInPictureParams params) { return params != null && params.hasSetAspectRatio() - ? params.getAspectRatio() + ? params.getAspectRatioFloat() : getDefaultAspectRatio(); } 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 b3558ad4b91e..17d7f5d0d567 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 @@ -20,20 +20,24 @@ import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.ActivityTaskManager; +import android.app.PictureInPictureParams; import android.app.PictureInPictureUiState; import android.content.ComponentName; import android.content.Context; +import android.content.pm.ActivityInfo; import android.graphics.Point; import android.graphics.Rect; import android.os.RemoteException; -import android.util.Log; +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.protolog.ShellProtoLogGroup; import java.io.PrintWriter; import java.lang.annotation.Retention; @@ -41,20 +45,25 @@ import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.List; import java.util.Objects; +import java.util.Set; import java.util.function.Consumer; /** * Singleton source of truth for the current state of PIP bounds. */ -public final class PipBoundsState { +public class PipBoundsState { public static final int STASH_TYPE_NONE = 0; public static final int STASH_TYPE_LEFT = 1; public static final int STASH_TYPE_RIGHT = 2; + public static final int STASH_TYPE_BOTTOM = 3; + public static final int STASH_TYPE_TOP = 4; @IntDef(prefix = { "STASH_TYPE_" }, value = { STASH_TYPE_NONE, STASH_TYPE_LEFT, - STASH_TYPE_RIGHT + STASH_TYPE_RIGHT, + STASH_TYPE_BOTTOM, + STASH_TYPE_TOP }) @Retention(RetentionPolicy.SOURCE) public @interface StashType {} @@ -88,6 +97,24 @@ public final class PipBoundsState { private int mShelfHeight; /** Whether the user has resized the PIP manually. */ private boolean mHasUserResizedPip; + /** + * 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. + * The system will try to respect these areas, but when not possible will ignore them. + * + * @see android.view.View#setPreferKeepClearRects + */ + private final Set<Rect> mRestrictedKeepClearAreas = new ArraySet<>(); + /** + * Areas defined by currently visible apps holding + * {@link android.Manifest.permission#SET_UNRESTRICTED_KEEP_CLEAR_AREAS} that they prefer to + * keep clear from overlays such as the PiP. + * Unrestricted areas can move the PiP farther than restricted areas, and the system will try + * harder to respect these areas. + * + * @see android.view.View#setPreferKeepClearRects + */ + private final Set<Rect> mUnrestrictedKeepClearAreas = new ArraySet<>(); private @Nullable Runnable mOnMinimalSizeChangeCallback; private @Nullable TriConsumer<Boolean, Integer, Boolean> mOnShelfVisibilityChangeCallback; @@ -201,7 +228,8 @@ public final class PipBoundsState { new PictureInPictureUiState(stashedState != STASH_TYPE_NONE /* isStashed */) ); } catch (RemoteException e) { - Log.e(TAG, "Unable to set alert PiP state change."); + ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: Unable to set alert PiP state change.", TAG); } } @@ -365,14 +393,33 @@ public final class PipBoundsState { } } + /** Set the keep clear areas onscreen. The PiP should ideally not cover them. */ + public void setKeepClearAreas(@NonNull Set<Rect> restrictedAreas, + @NonNull Set<Rect> unrestrictedAreas) { + mRestrictedKeepClearAreas.clear(); + mRestrictedKeepClearAreas.addAll(restrictedAreas); + mUnrestrictedKeepClearAreas.clear(); + mUnrestrictedKeepClearAreas.addAll(unrestrictedAreas); + } + + @NonNull + public Set<Rect> getRestrictedKeepClearAreas() { + return mRestrictedKeepClearAreas; + } + + @NonNull + public Set<Rect> getUnrestrictedKeepClearAreas() { + return mUnrestrictedKeepClearAreas; + } + /** * Initialize states when first entering PiP. */ - public void setBoundsStateForEntry(ComponentName componentName, float aspectRatio, - Size overrideMinSize) { + public void setBoundsStateForEntry(ComponentName componentName, ActivityInfo activityInfo, + PictureInPictureParams params, PipBoundsAlgorithm pipBoundsAlgorithm) { setLastPipComponentName(componentName); - setAspectRatio(aspectRatio); - setOverrideMinSize(overrideMinSize); + setAspectRatio(pipBoundsAlgorithm.getAspectRatioOrDefault(params)); + setOverrideMinSize(pipBoundsAlgorithm.getMinimalSize(activityInfo)); } /** Returns whether the shelf is currently showing. */ 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 new file mode 100644 index 000000000000..0e32663955d3 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipContentOverlay.java @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.content.res.TypedArray; +import android.graphics.Color; +import android.graphics.Rect; +import android.view.SurfaceControl; +import android.view.SurfaceSession; +import android.window.TaskSnapshot; + +/** + * Represents the content overlay used during the entering PiP animation. + */ +public abstract class PipContentOverlay { + protected SurfaceControl mLeash; + + /** Attaches the internal {@link #mLeash} to the given parent leash. */ + public abstract void attach(SurfaceControl.Transaction tx, SurfaceControl parentLeash); + + /** Detaches the internal {@link #mLeash} from its parent by removing itself. */ + public void detach(SurfaceControl.Transaction tx) { + if (mLeash != null && mLeash.isValid()) { + tx.remove(mLeash); + tx.apply(); + } + } + + /** + * 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 fraction progress of the animation ranged from 0f to 1f. + */ + public abstract void onAnimationUpdate(SurfaceControl.Transaction atomicTx, float fraction); + + /** + * Callback when reaches the end of animation on the internal {@link #mLeash}. + * @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 destinationBounds {@link Rect} of the final bounds. + */ + public abstract void onAnimationEnd(SurfaceControl.Transaction atomicTx, + Rect destinationBounds); + + /** A {@link PipContentOverlay} uses solid color. */ + public static final class PipColorOverlay extends PipContentOverlay { + private final Context mContext; + + public PipColorOverlay(Context context) { + mContext = context; + mLeash = new SurfaceControl.Builder(new SurfaceSession()) + .setCallsite("PipAnimation") + .setName(PipColorOverlay.class.getSimpleName()) + .setColorLayer() + .build(); + } + + @Override + public void attach(SurfaceControl.Transaction tx, SurfaceControl parentLeash) { + tx.show(mLeash); + tx.setLayer(mLeash, Integer.MAX_VALUE); + tx.setColor(mLeash, getContentOverlayColor(mContext)); + tx.setAlpha(mLeash, 0f); + tx.reparent(mLeash, parentLeash); + tx.apply(); + } + + @Override + public void onAnimationUpdate(SurfaceControl.Transaction atomicTx, float fraction) { + atomicTx.setAlpha(mLeash, fraction < 0.5f ? 0 : (fraction - 0.5f) * 2); + } + + @Override + public void onAnimationEnd(SurfaceControl.Transaction atomicTx, Rect destinationBounds) { + // Do nothing. Color overlay should be fully opaque by now. + } + + private float[] getContentOverlayColor(Context context) { + final TypedArray ta = context.obtainStyledAttributes(new int[] { + android.R.attr.colorBackground }); + try { + int colorAccent = ta.getColor(0, 0); + return new float[] { + Color.red(colorAccent) / 255f, + Color.green(colorAccent) / 255f, + Color.blue(colorAccent) / 255f }; + } finally { + ta.recycle(); + } + } + } + + /** A {@link PipContentOverlay} uses {@link TaskSnapshot}. */ + public static final class PipSnapshotOverlay extends PipContentOverlay { + private final TaskSnapshot mSnapshot; + private final Rect mSourceRectHint; + + private float mTaskSnapshotScaleX; + private float mTaskSnapshotScaleY; + + public PipSnapshotOverlay(TaskSnapshot snapshot, Rect sourceRectHint) { + mSnapshot = snapshot; + mSourceRectHint = new Rect(sourceRectHint); + mLeash = new SurfaceControl.Builder(new SurfaceSession()) + .setCallsite("PipAnimation") + .setName(PipSnapshotOverlay.class.getSimpleName()) + .build(); + } + + @Override + public void attach(SurfaceControl.Transaction tx, SurfaceControl parentLeash) { + mTaskSnapshotScaleX = (float) mSnapshot.getTaskSize().x + / mSnapshot.getHardwareBuffer().getWidth(); + mTaskSnapshotScaleY = (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.reparent(mLeash, parentLeash); + tx.apply(); + } + + @Override + public void onAnimationUpdate(SurfaceControl.Transaction atomicTx, 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); + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipMediaController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipMediaController.java index 97139626a3d2..65a12d629c5a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipMediaController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipMediaController.java @@ -32,6 +32,7 @@ import android.content.IntentFilter; import android.graphics.drawable.Icon; import android.media.MediaMetadata; import android.media.session.MediaController; +import android.media.session.MediaSession; import android.media.session.MediaSessionManager; import android.media.session.PlaybackState; import android.os.Handler; @@ -64,7 +65,7 @@ public class PipMediaController { */ public interface ActionListener { /** - * Called when the media actions changes. + * Called when the media actions changed. */ void onMediaActionsChanged(List<RemoteAction> actions); } @@ -74,11 +75,21 @@ public class PipMediaController { */ public interface MetadataListener { /** - * Called when the media metadata changes. + * Called when the media metadata changed. */ void onMediaMetadataChanged(MediaMetadata metadata); } + /** + * A listener interface to receive notification on changes to the media session token. + */ + public interface TokenListener { + /** + * Called when the media session token changed. + */ + void onMediaSessionTokenChanged(MediaSession.Token token); + } + private final Context mContext; private final Handler mMainHandler; private final HandlerExecutor mHandlerExecutor; @@ -133,6 +144,7 @@ public class PipMediaController { private final ArrayList<ActionListener> mActionListeners = new ArrayList<>(); private final ArrayList<MetadataListener> mMetadataListeners = new ArrayList<>(); + private final ArrayList<TokenListener> mTokenListeners = new ArrayList<>(); public PipMediaController(Context context, Handler mainHandler) { mContext = context; @@ -144,7 +156,7 @@ public class PipMediaController { mediaControlFilter.addAction(ACTION_NEXT); mediaControlFilter.addAction(ACTION_PREV); mContext.registerReceiverForAllUsers(mMediaActionReceiver, mediaControlFilter, - SYSTEMUI_PERMISSION, mainHandler); + SYSTEMUI_PERMISSION, mainHandler, Context.RECEIVER_EXPORTED); // Creates the standard media buttons that we may show. mPauseAction = getDefaultRemoteAction(R.string.pip_pause, @@ -204,6 +216,31 @@ public class PipMediaController { mMetadataListeners.remove(listener); } + /** + * Adds a new token listener. + */ + public void addTokenListener(TokenListener listener) { + if (!mTokenListeners.contains(listener)) { + mTokenListeners.add(listener); + listener.onMediaSessionTokenChanged(getToken()); + } + } + + /** + * Removes a token listener. + */ + public void removeTokenListener(TokenListener listener) { + listener.onMediaSessionTokenChanged(null); + mTokenListeners.remove(listener); + } + + private MediaSession.Token getToken() { + if (mMediaController == null) { + return null; + } + return mMediaController.getSessionToken(); + } + private MediaMetadata getMediaMetadata() { return mMediaController != null ? mMediaController.getMetadata() : null; } @@ -294,6 +331,7 @@ public class PipMediaController { } notifyActionsChanged(); notifyMetadataChanged(getMediaMetadata()); + notifyTokenChanged(getToken()); // TODO(winsonc): Consider if we want to close the PIP after a timeout (like on TV) } @@ -317,4 +355,10 @@ public class PipMediaController { mMetadataListeners.forEach(l -> l.onMediaMetadataChanged(metadata)); } } + + private void notifyTokenChanged(MediaSession.Token token) { + if (!mTokenListeners.isEmpty()) { + mTokenListeners.forEach(l -> l.onMediaSessionTokenChanged(token)); + } + } } 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 caa1f017082b..16f1d1c2944c 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,12 +26,13 @@ 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.pm.ParceledListSlice; import android.graphics.PixelFormat; import android.graphics.Rect; import android.view.SurfaceControl; import android.view.WindowManager; +import java.util.List; + /** * Interface to allow {@link com.android.wm.shell.pip.PipTaskOrganizer} to call into * PiP menu when certain events happen (task appear/vanish, PiP move, etc.) @@ -66,7 +67,7 @@ public interface PipMenuController { /** * Given a set of actions, update the menu. */ - void setAppActions(ParceledListSlice<RemoteAction> appActions); + void setAppActions(List<RemoteAction> appActions, RemoteAction closeAction); /** * Resize the PiP menu with the given bounds. The PiP SurfaceControl is given if there is a diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipParamsChangedForwarder.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipParamsChangedForwarder.java new file mode 100644 index 000000000000..21ba85459c48 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipParamsChangedForwarder.java @@ -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. + */ + +package com.android.wm.shell.pip; + +import android.app.RemoteAction; + +import java.util.ArrayList; +import java.util.List; + +/** + * Forwards changes to the Picture-in-Picture params to all listeners. + */ +public class PipParamsChangedForwarder { + + private final List<PipParamsChangedCallback> + mPipParamsChangedListeners = new ArrayList<>(); + + /** + * Add a listener that implements at least one of the callbacks. + */ + public void addListener(PipParamsChangedCallback listener) { + if (mPipParamsChangedListeners.contains(listener)) { + return; + } + mPipParamsChangedListeners.add(listener); + } + + /** + * Call to notify all listeners of the changed aspect ratio. + */ + public void notifyAspectRatioChanged(float aspectRatio) { + for (PipParamsChangedCallback listener : mPipParamsChangedListeners) { + listener.onAspectRatioChanged(aspectRatio); + } + } + + /** + * Call to notify all listeners of the changed expanded aspect ratio. + */ + public void notifyExpandedAspectRatioChanged(float aspectRatio) { + for (PipParamsChangedCallback listener : mPipParamsChangedListeners) { + listener.onExpandedAspectRatioChanged(aspectRatio); + } + } + + /** + * Call to notify all listeners of the changed title. + */ + public void notifyTitleChanged(CharSequence title) { + String value = title == null ? null : title.toString(); + for (PipParamsChangedCallback listener : mPipParamsChangedListeners) { + listener.onTitleChanged(value); + } + } + + /** + * Call to notify all listeners of the changed subtitle. + */ + public void notifySubtitleChanged(CharSequence subtitle) { + String value = subtitle == null ? null : subtitle.toString(); + for (PipParamsChangedCallback listener : mPipParamsChangedListeners) { + listener.onSubtitleChanged(value); + } + } + + /** + * Call to notify all listeners of the changed app actions or close action. + */ + public void notifyActionsChanged(List<RemoteAction> actions, RemoteAction closeAction) { + for (PipParamsChangedCallback listener : mPipParamsChangedListeners) { + listener.onActionsChanged(actions, closeAction); + } + } + + /** + * Contains callbacks for PiP params changes. Subclasses can choose which changes they want to + * listen to by only overriding those selectively. + */ + public interface PipParamsChangedCallback { + + /** + * Called if aspect ratio changed. + */ + default void onAspectRatioChanged(float aspectRatio) { + } + + /** + * Called if expanded aspect ratio changed. + */ + default void onExpandedAspectRatioChanged(float aspectRatio) { + } + + /** + * Called if either the actions or the close action changed. + */ + default void onActionsChanged(List<RemoteAction> actions, RemoteAction closeAction) { + } + + /** + * Called if the title changed. + */ + default void onTitleChanged(String title) { + } + + /** + * Called if the subtitle changed. + */ + default void onSubtitleChanged(String subtitle) { + } + } +} 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 180e3fb48c9d..a017a2674359 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 @@ -37,6 +37,7 @@ public class PipSurfaceTransactionHelper { private final Rect mTmpDestinationRect = new Rect(); private int mCornerRadius; + private int mShadowRadius; /** * Called when display size or font size of settings changed @@ -45,6 +46,7 @@ public class PipSurfaceTransactionHelper { */ public void onDensityOrFontScaleChanged(Context context) { mCornerRadius = context.getResources().getDimensionPixelSize(R.dimen.pip_corner_radius); + mShadowRadius = context.getResources().getDimensionPixelSize(R.dimen.pip_shadow_radius); } /** @@ -100,21 +102,33 @@ public class PipSurfaceTransactionHelper { * @return same {@link PipSurfaceTransactionHelper} instance for method chaining */ public PipSurfaceTransactionHelper scaleAndCrop(SurfaceControl.Transaction tx, - SurfaceControl leash, - Rect sourceBounds, Rect destinationBounds, Rect insets) { - mTmpSourceRectF.set(sourceBounds); + SurfaceControl leash, Rect sourceRectHint, + Rect sourceBounds, Rect destinationBounds, Rect insets, + boolean isInPipDirection) { 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 - final float scale = sourceBounds.width() <= sourceBounds.height() - ? (float) destinationBounds.width() / sourceBounds.width() - : (float) destinationBounds.height() / sourceBounds.height(); + 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() + ? (float) destinationBounds.width() / sourceRectHint.width() + : (float) destinationBounds.height() / sourceRectHint.height(); + } else { + scale = sourceBounds.width() <= sourceBounds.height() + ? (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; mTmpTransform.setScale(scale, scale); tx.setMatrix(leash, mTmpTransform, mTmpFloat9) - .setWindowCrop(leash, mTmpDestinationRect) + .setCrop(leash, mTmpDestinationRect) .setPosition(leash, left, top); return this; } @@ -138,8 +152,8 @@ public class PipSurfaceTransactionHelper { // destination are different. final float scale = srcW <= srcH ? (float) destW / srcW : (float) destH / srcH; final Rect crop = mTmpDestinationRect; - crop.set(0, 0, Transitions.ENABLE_SHELL_TRANSITIONS ? destH - : destW, Transitions.ENABLE_SHELL_TRANSITIONS ? destW : destH); + crop.set(0, 0, Transitions.SHELL_TRANSITIONS_ROTATION ? destH + : destW, Transitions.SHELL_TRANSITIONS_ROTATION ? destW : destH); // Inverse scale for crop to fit in screen coordinates. crop.scale(1 / scale); crop.offset(insets.left, insets.top); @@ -160,7 +174,7 @@ public class PipSurfaceTransactionHelper { mTmpTransform.setScale(scale, scale); mTmpTransform.postRotate(degrees); mTmpTransform.postTranslate(positionX, positionY); - tx.setMatrix(leash, mTmpTransform, mTmpFloat9).setWindowCrop(leash, crop); + tx.setMatrix(leash, mTmpTransform, mTmpFloat9).setCrop(leash, crop); return this; } @@ -200,14 +214,12 @@ public class PipSurfaceTransactionHelper { } /** - * Re-parents the snapshot to the parent's surface control and shows it. + * Operates the shadow radius on a given transaction and leash + * @return same {@link PipSurfaceTransactionHelper} instance for method chaining */ - public PipSurfaceTransactionHelper reparentAndShowSurfaceSnapshot( - SurfaceControl.Transaction t, SurfaceControl parent, SurfaceControl snapshot) { - t.reparent(snapshot, parent); - t.setLayer(snapshot, Integer.MAX_VALUE); - t.show(snapshot); - t.apply(); + public PipSurfaceTransactionHelper shadow(SurfaceControl.Transaction tx, SurfaceControl leash, + boolean applyShadowRadius) { + tx.setShadowRadius(leash, applyShadowRadius ? mShadowRadius : 0); return this; } 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 a201616db208..e624de661737 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 @@ -18,13 +18,14 @@ package com.android.wm.shell.pip; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; -import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_SECONDARY; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static android.util.RotationUtils.deltaRotation; import static android.util.RotationUtils.rotateBounds; import static com.android.wm.shell.ShellTaskOrganizer.TASK_LISTENER_TYPE_PIP; import static com.android.wm.shell.ShellTaskOrganizer.taskListenerTypeToString; +import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; +import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT; import static com.android.wm.shell.pip.PipAnimationController.ANIM_TYPE_ALPHA; import static com.android.wm.shell.pip.PipAnimationController.ANIM_TYPE_BOUNDS; import static com.android.wm.shell.pip.PipAnimationController.FRACTION_START; @@ -40,6 +41,10 @@ import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTI import static com.android.wm.shell.pip.PipAnimationController.isInPipDirection; import static com.android.wm.shell.pip.PipAnimationController.isOutPipDirection; import static com.android.wm.shell.pip.PipAnimationController.isRemovePipDirection; +import static com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS; +import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP; +import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP_TO_SPLIT; +import static com.android.wm.shell.transition.Transitions.TRANSIT_REMOVE_PIP; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; @@ -57,16 +62,16 @@ import android.content.res.Configuration; import android.graphics.Rect; import android.os.RemoteException; import android.os.SystemClock; -import android.util.Log; -import android.util.Rational; import android.view.Display; import android.view.Surface; import android.view.SurfaceControl; import android.window.TaskOrganizer; +import android.window.TaskSnapshot; import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.protolog.common.ProtoLog; import com.android.wm.shell.R; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.animation.Interpolators; @@ -75,8 +80,8 @@ 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.annotations.ShellMainThread; -import com.android.wm.shell.legacysplitscreen.LegacySplitScreenController; import com.android.wm.shell.pip.phone.PipMotionHelper; +import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.splitscreen.SplitScreenController; import com.android.wm.shell.transition.Transitions; @@ -122,12 +127,12 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, private final @NonNull PipMenuController mPipMenuController; private final PipAnimationController mPipAnimationController; private final PipTransitionController mPipTransitionController; + protected final PipParamsChangedForwarder mPipParamsChangedForwarder; private final PipUiEventLogger mPipUiEventLoggerLogger; private final int mEnterAnimationDuration; private final int mExitAnimationDuration; private final int mCrossFadeAnimationDuration; private final PipSurfaceTransactionHelper mSurfaceTransactionHelper; - private final Optional<LegacySplitScreenController> mLegacySplitScreenOptional; private final Optional<SplitScreenController> mSplitScreenOptional; protected final ShellTaskOrganizer mTaskOrganizer; protected final ShellExecutor mMainExecutor; @@ -148,8 +153,8 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, final int direction = animator.getTransitionDirection(); final int animationType = animator.getAnimationType(); final Rect destinationBounds = animator.getDestinationBounds(); - if (isInPipDirection(direction) && animator.getContentOverlay() != null) { - fadeOutAndRemoveOverlay(animator.getContentOverlay(), + if (isInPipDirection(direction) && animator.getContentOverlayLeash() != null) { + fadeOutAndRemoveOverlay(animator.getContentOverlayLeash(), animator::clearContentOverlay, true /* withStartDelay*/); } if (mWaitForFixedRotation && animationType == ANIM_TYPE_BOUNDS @@ -182,8 +187,8 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, public void onPipAnimationCancel(TaskInfo taskInfo, PipAnimationController.PipTransitionAnimator animator) { final int direction = animator.getTransitionDirection(); - if (isInPipDirection(direction) && animator.getContentOverlay() != null) { - fadeOutAndRemoveOverlay(animator.getContentOverlay(), + if (isInPipDirection(direction) && animator.getContentOverlayLeash() != null) { + fadeOutAndRemoveOverlay(animator.getContentOverlayLeash(), animator::clearContentOverlay, true /* withStartDelay */); } sendOnPipTransitionCancelled(direction); @@ -215,7 +220,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, private long mLastOneShotAlphaAnimationTime; private PipSurfaceTransactionHelper.SurfaceControlTransactionFactory mSurfaceControlTransactionFactory; - private PictureInPictureParams mPictureInPictureParams; + protected PictureInPictureParams mPictureInPictureParams; private IntConsumer mOnDisplayIdChangeCallback; /** * The end transaction of PiP animation for switching between PiP and fullscreen with @@ -243,7 +248,8 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, * An optional overlay used to mask content changing between an app in/out of PiP, only set if * {@link PipTransitionState#getInSwipePipToHomeTransition()} is true. */ - private SurfaceControl mSwipePipToHomeOverlay; + @Nullable + SurfaceControl mSwipePipToHomeOverlay; public PipTaskOrganizer(Context context, @NonNull SyncTransactionQueue syncTransactionQueue, @@ -254,7 +260,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, @NonNull PipAnimationController pipAnimationController, @NonNull PipSurfaceTransactionHelper surfaceTransactionHelper, @NonNull PipTransitionController pipTransitionController, - Optional<LegacySplitScreenController> legacySplitScreenOptional, + @NonNull PipParamsChangedForwarder pipParamsChangedForwarder, Optional<SplitScreenController> splitScreenOptional, @NonNull DisplayController displayController, @NonNull PipUiEventLogger pipUiEventLogger, @@ -267,6 +273,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, mPipBoundsAlgorithm = boundsHandler; mPipMenuController = pipMenuController; mPipTransitionController = pipTransitionController; + mPipParamsChangedForwarder = pipParamsChangedForwarder; mEnterAnimationDuration = context.getResources() .getInteger(R.integer.config_pipEnterAnimationDuration); mExitAnimationDuration = context.getResources() @@ -277,7 +284,6 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, mPipAnimationController = pipAnimationController; mPipUiEventLoggerLogger = pipUiEventLogger; mSurfaceControlTransactionFactory = SurfaceControl.Transaction::new; - mLegacySplitScreenOptional = legacySplitScreenOptional; mSplitScreenOptional = splitScreenOptional; mTaskOrganizer = shellTaskOrganizer; mMainExecutor = mainExecutor; @@ -304,6 +310,10 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, return mPipTransitionState.isInPip(); } + private boolean isLaunchIntoPipTask() { + return mPictureInPictureParams != null && mPictureInPictureParams.isLaunchIntoPip(); + } + /** * Returns whether the entry animation is waiting to be started. */ @@ -346,12 +356,24 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, * Callback when launcher finishes swipe-pip-to-home operation. * Expect {@link #onTaskAppeared(ActivityManager.RunningTaskInfo, SurfaceControl)} afterwards. */ - public void stopSwipePipToHome(ComponentName componentName, Rect destinationBounds, + public void stopSwipePipToHome(int taskId, ComponentName componentName, Rect destinationBounds, SurfaceControl overlay) { // do nothing if there is no startSwipePipToHome being called before - if (mPipTransitionState.getInSwipePipToHomeTransition()) { - mPipBoundsState.setBounds(destinationBounds); - mSwipePipToHomeOverlay = overlay; + if (!mPipTransitionState.getInSwipePipToHomeTransition()) { + return; + } + mPipBoundsState.setBounds(destinationBounds); + mSwipePipToHomeOverlay = overlay; + if (ENABLE_SHELL_TRANSITIONS && overlay != null) { + // With Shell transition, the overlay was attached to the remote transition leash, which + // will be removed when the current transition is finished, so we need to reparent it + // to the actual Task surface now. + // PipTransition is responsible to fade it out and cleanup when finishing the enter PIP + // transition. + final SurfaceControl.Transaction t = new SurfaceControl.Transaction(); + mTaskOrganizer.reparentChildSurfaceToTask(taskId, overlay, t); + t.setLayer(overlay, Integer.MAX_VALUE); + t.apply(); } } @@ -363,11 +385,10 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, return mLeash; } - private void setBoundsStateForEntry(ComponentName componentName, PictureInPictureParams params, - ActivityInfo activityInfo) { - mPipBoundsState.setBoundsStateForEntry(componentName, - mPipBoundsAlgorithm.getAspectRatioOrDefault(params), - mPipBoundsAlgorithm.getMinimalSize(activityInfo)); + private void setBoundsStateForEntry(ComponentName componentName, + PictureInPictureParams params, ActivityInfo activityInfo) { + mPipBoundsState.setBoundsStateForEntry(componentName, activityInfo, params, + mPipBoundsAlgorithm); } /** @@ -386,35 +407,65 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, if (!mPipTransitionState.isInPip() || mPipTransitionState.getTransitionState() == PipTransitionState.EXITING_PIP || mToken == null) { - Log.wtf(TAG, "Not allowed to exitPip in current state" - + " mState=" + mPipTransitionState.getTransitionState() + " mToken=" + mToken); + ProtoLog.wtf(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: Not allowed to exitPip in current state" + + " mState=%d mToken=%s", TAG, mPipTransitionState.getTransitionState(), + mToken); return; } - mPipUiEventLoggerLogger.log( - PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_EXPAND_TO_FULLSCREEN); final WindowContainerTransaction wct = new WindowContainerTransaction(); - final Rect destinationBounds = mPipBoundsState.getDisplayBounds(); + if (isLaunchIntoPipTask()) { + exitLaunchIntoPipTask(wct); + return; + } + + if (ENABLE_SHELL_TRANSITIONS) { + if (requestEnterSplit && mSplitScreenOptional.isPresent()) { + mSplitScreenOptional.get().prepareEnterSplitScreen(wct, mTaskInfo, + isPipTopLeft() + ? SPLIT_POSITION_TOP_OR_LEFT : SPLIT_POSITION_BOTTOM_OR_RIGHT); + mPipTransitionController.startExitTransition( + TRANSIT_EXIT_PIP_TO_SPLIT, wct, null /* destinationBounds */); + return; + } + } + + final Rect destinationBounds = getExitDestinationBounds(); final int direction = syncWithSplitScreenBounds(destinationBounds, requestEnterSplit) ? TRANSITION_DIRECTION_LEAVE_PIP_TO_SPLIT_SCREEN : TRANSITION_DIRECTION_LEAVE_PIP; - final SurfaceControl.Transaction tx = mSurfaceControlTransactionFactory.getTransaction(); - mSurfaceTransactionHelper.scale(tx, mLeash, destinationBounds, mPipBoundsState.getBounds()); - tx.setWindowCrop(mLeash, destinationBounds.width(), destinationBounds.height()); - // We set to fullscreen here for now, but later it will be set to UNDEFINED for - // the proper windowing mode to take place. See #applyWindowingModeChangeOnExit. - wct.setActivityWindowingMode(mToken, - direction == TRANSITION_DIRECTION_LEAVE_PIP_TO_SPLIT_SCREEN && !requestEnterSplit - ? WINDOWING_MODE_SPLIT_SCREEN_SECONDARY - : WINDOWING_MODE_FULLSCREEN); - wct.setBounds(mToken, destinationBounds); - wct.setBoundsChangeTransaction(mToken, tx); + + if (Transitions.ENABLE_SHELL_TRANSITIONS && direction == TRANSITION_DIRECTION_LEAVE_PIP) { + // 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); + // 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); + } else { + final SurfaceControl.Transaction tx = + mSurfaceControlTransactionFactory.getTransaction(); + mSurfaceTransactionHelper.scale(tx, mLeash, destinationBounds, + mPipBoundsState.getBounds()); + tx.setWindowCrop(mLeash, destinationBounds.width(), destinationBounds.height()); + // We set to fullscreen here for now, but later it will be set to UNDEFINED for + // the proper windowing mode to take place. See #applyWindowingModeChangeOnExit. + wct.setActivityWindowingMode(mToken, WINDOWING_MODE_FULLSCREEN); + wct.setBounds(mToken, destinationBounds); + wct.setBoundsChangeTransaction(mToken, tx); + } + + // Cancel the existing animator if there is any. + 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. mPipTransitionState.setTransitionState(PipTransitionState.EXITING_PIP); if (Transitions.ENABLE_SHELL_TRANSITIONS) { - mPipTransitionController.startTransition(destinationBounds, wct); + mPipTransitionController.startExitTransition(TRANSIT_EXIT_PIP, wct, destinationBounds); return; } mSyncTransactionQueue.queue(wct); @@ -438,25 +489,35 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, }); } + /** Returns the bounds to restore to when exiting PIP mode. */ + public Rect getExitDestinationBounds() { + return mPipBoundsState.getDisplayBounds(); + } + + private void exitLaunchIntoPipTask(WindowContainerTransaction wct) { + wct.startTask(mTaskInfo.launchIntoPipHostTaskId, null /* ActivityOptions */); + mTaskOrganizer.applyTransaction(wct); + + // Remove the PiP with fade-out animation right after the host Task is brought to front. + removePip(); + } + private void applyWindowingModeChangeOnExit(WindowContainerTransaction wct, int direction) { // Reset the final windowing mode. wct.setWindowingMode(mToken, getOutPipWindowingMode()); // Simply reset the activity mode set prior to the animation running. wct.setActivityWindowingMode(mToken, WINDOWING_MODE_UNDEFINED); - mLegacySplitScreenOptional.ifPresent(splitScreen -> { - if (direction == TRANSITION_DIRECTION_LEAVE_PIP_TO_SPLIT_SCREEN) { - wct.reparent(mToken, splitScreen.getSecondaryRoot(), true /* onTop */); - } - }); } /** * Removes PiP immediately. */ public void removePip() { - if (!mPipTransitionState.isInPip() || mToken == null) { - Log.wtf(TAG, "Not allowed to removePip in current state" - + " mState=" + mPipTransitionState.getTransitionState() + " mToken=" + mToken); + if (!mPipTransitionState.isInPip() || mToken == null) { + ProtoLog.wtf(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: Not allowed to removePip in current state" + + " mState=%d mToken=%s", TAG, mPipTransitionState.getTransitionState(), + mToken); return; } @@ -479,7 +540,8 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, wct.setBounds(mToken, null); wct.setWindowingMode(mToken, WINDOWING_MODE_UNDEFINED); wct.reorder(mToken, false); - mPipTransitionController.startTransition(null, wct); + mPipTransitionController.startExitTransition(TRANSIT_REMOVE_PIP, wct, + null /* destinationBounds */); return; } @@ -492,7 +554,9 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, ActivityTaskManager.getService().removeRootTasksInWindowingModes( new int[]{ WINDOWING_MODE_PINNED }); } catch (RemoteException e) { - Log.e(TAG, "Failed to remove PiP", e); + ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: Failed to remove PiP, %s", + TAG, e); } } @@ -506,6 +570,14 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, mPictureInPictureParams = mTaskInfo.pictureInPictureParams; setBoundsStateForEntry(mTaskInfo.topActivity, mPictureInPictureParams, mTaskInfo.topActivityInfo); + if (mPictureInPictureParams != null) { + mPipParamsChangedForwarder.notifyActionsChanged(mPictureInPictureParams.getActions(), + mPictureInPictureParams.getCloseAction()); + mPipParamsChangedForwarder.notifyTitleChanged( + mPictureInPictureParams.getTitle()); + mPipParamsChangedForwarder.notifySubtitleChanged( + mPictureInPictureParams.getSubtitle()); + } mPipUiEventLoggerLogger.setTaskInfo(mTaskInfo); mPipUiEventLoggerLogger.log(PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_ENTER); @@ -521,7 +593,9 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, if (!mWaitForFixedRotation) { onEndOfSwipePipToHomeTransition(); } else { - Log.d(TAG, "Defer onTaskAppeared-SwipePipToHome until end of fixed rotation."); + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: Defer onTaskAppeared-SwipePipToHome until end of fixed rotation.", + TAG); } return; } @@ -529,9 +603,17 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, if (mOneShotAnimationType == ANIM_TYPE_ALPHA && SystemClock.uptimeMillis() - mLastOneShotAlphaAnimationTime > ONE_SHOT_ALPHA_ANIMATION_TIMEOUT_MS) { - Log.d(TAG, "Alpha animation is expired. Use bounds animation."); + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: Alpha animation is expired. Use bounds animation.", TAG); mOneShotAnimationType = ANIM_TYPE_BOUNDS; } + + if (Transitions.ENABLE_SHELL_TRANSITIONS) { + // For Shell transition, we will animate the window in PipTransition#startAnimation + // instead of #onTaskAppeared. + return; + } + if (mWaitForFixedRotation) { onTaskAppearedWithFixedRotation(); return; @@ -541,15 +623,6 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, Objects.requireNonNull(destinationBounds, "Missing destination bounds"); final Rect currentBounds = mTaskInfo.configuration.windowConfiguration.getBounds(); - if (Transitions.ENABLE_SHELL_TRANSITIONS) { - if (mOneShotAnimationType == ANIM_TYPE_BOUNDS) { - mPipMenuController.attach(mLeash); - } else if (mOneShotAnimationType == ANIM_TYPE_ALPHA) { - mOneShotAnimationType = ANIM_TYPE_BOUNDS; - } - return; - } - if (mOneShotAnimationType == ANIM_TYPE_BOUNDS) { mPipMenuController.attach(mLeash); final Rect sourceHintRect = PipBoundsAlgorithm.getValidSourceHintRect( @@ -568,8 +641,9 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, private void onTaskAppearedWithFixedRotation() { if (mOneShotAnimationType == ANIM_TYPE_ALPHA) { - Log.d(TAG, "Defer entering PiP alpha animation, fixed rotation is ongoing"); - // If deferred, hide the surface till fixed rotation is completed. + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: Defer entering PiP alpha animation, fixed rotation is ongoing", TAG); + // If deferred, hside the surface till fixed rotation is completed. final SurfaceControl.Transaction tx = mSurfaceControlTransactionFactory.getTransaction(); tx.setAlpha(mLeash, 0f); @@ -609,6 +683,15 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, mSurfaceControlTransactionFactory.getTransaction(); tx.setAlpha(mLeash, 0f); tx.apply(); + + // When entering PiP this transaction will be applied within WindowContainerTransaction and + // ensure that the PiP has rounded corners. + final SurfaceControl.Transaction boundsChangeTx = + mSurfaceControlTransactionFactory.getTransaction(); + mSurfaceTransactionHelper + .crop(boundsChangeTx, mLeash, destinationBounds) + .round(boundsChangeTx, mLeash, true /* applyCornerRadius */); + mPipTransitionState.setTransitionState(PipTransitionState.ENTRY_SCHEDULED); applyEnterPipSyncTransaction(destinationBounds, () -> { mPipAnimationController @@ -621,12 +704,11 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, // mState is set right after the animation is kicked off to block any resize // requests such as offsetPip that may have been called prior to the transition. mPipTransitionState.setTransitionState(PipTransitionState.ENTERING_PIP); - }, null /* boundsChangeTransaction */); + }, boundsChangeTx); } private void onEndOfSwipePipToHomeTransition() { if (Transitions.ENABLE_SHELL_TRANSITIONS) { - mSwipePipToHomeOverlay = null; return; } @@ -698,7 +780,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, } /** - * Note that dismissing PiP is now originated from SystemUI, see {@link #exitPip(int)}. + * Note that dismissing PiP is now originated from SystemUI, see {@link #exitPip(int, boolean)}. * Meanwhile this callback is invoked whenever the task is removed. For instance: * - as a result of removeRootTasksInWindowingModes from WM * - activity itself is died @@ -710,38 +792,25 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, if (mPipTransitionState.getTransitionState() == PipTransitionState.UNDEFINED) { return; } + if (Transitions.ENABLE_SHELL_TRANSITIONS + && mPipTransitionState.getTransitionState() == PipTransitionState.EXITING_PIP) { + // With Shell transition, we do the cleanup in PipTransition after exiting PIP. + return; + } final WindowContainerToken token = info.token; Objects.requireNonNull(token, "Requires valid WindowContainerToken"); if (token.asBinder() != mToken.asBinder()) { - Log.wtf(TAG, "Unrecognized token: " + token); + ProtoLog.wtf(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: Unrecognized token: %s", TAG, token); return; } - clearWaitForFixedRotation(); - mPipTransitionState.setInSwipePipToHomeTransition(false); - mPictureInPictureParams = null; - mPipTransitionState.setTransitionState(PipTransitionState.UNDEFINED); - // Re-set the PIP bounds to none. - mPipBoundsState.setBounds(new Rect()); - mPipUiEventLoggerLogger.setTaskInfo(null); - mPipMenuController.detach(); - if (info.displayId != Display.DEFAULT_DISPLAY && mOnDisplayIdChangeCallback != null) { - mOnDisplayIdChangeCallback.accept(Display.DEFAULT_DISPLAY); - } + cancelCurrentAnimator(); + onExitPipFinished(info); if (Transitions.ENABLE_SHELL_TRANSITIONS) { mPipTransitionController.forceFinishTransition(); } - final PipAnimationController.PipTransitionAnimator<?> animator = - mPipAnimationController.getCurrentAnimator(); - if (animator != null) { - if (animator.getContentOverlay() != null) { - removeContentOverlay(animator.getContentOverlay(), animator::clearContentOverlay); - } - animator.removeAllUpdateListeners(); - animator.removeAllListeners(); - animator.cancel(); - } } @Override @@ -749,8 +818,9 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, Objects.requireNonNull(mToken, "onTaskInfoChanged requires valid existing mToken"); if (mPipTransitionState.getTransitionState() != PipTransitionState.ENTERED_PIP && mPipTransitionState.getTransitionState() != PipTransitionState.EXITING_PIP) { - Log.d(TAG, "Defer onTaskInfoChange in current state: " - + mPipTransitionState.getTransitionState()); + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: Defer onTaskInfoChange in current state: %d", TAG, + mPipTransitionState.getTransitionState()); // Defer applying PiP parameters if the task is entering PiP to avoid disturbing // the animation. mDeferredTaskInfo = info; @@ -760,16 +830,13 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, mPipBoundsState.setOverrideMinSize( mPipBoundsAlgorithm.getMinimalSize(info.topActivityInfo)); final PictureInPictureParams newParams = info.pictureInPictureParams; - if (newParams == null || !applyPictureInPictureParams(newParams)) { - Log.d(TAG, "Ignored onTaskInfoChanged with PiP param: " + newParams); + + // mPictureInPictureParams is only null if there is no PiP + if (newParams == null || mPictureInPictureParams == null) { return; } - // Aspect ratio changed, re-calculate bounds if valid. - final Rect destinationBounds = mPipBoundsAlgorithm.getAdjustedDestinationBounds( - mPipBoundsState.getBounds(), mPipBoundsState.getAspectRatio()); - Objects.requireNonNull(destinationBounds, "Missing destination bounds"); - scheduleAnimateResizePip(destinationBounds, mEnterAnimationDuration, - null /* updateBoundsCallback */); + applyNewPictureInPictureParams(newParams); + mPictureInPictureParams = newParams; } @Override @@ -784,10 +851,38 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, } @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 (mTaskInfo == null || mLeash == null || mTaskInfo.taskId != taskId) { + throw new IllegalArgumentException("There is no surface for taskId=" + taskId); + } + return mLeash; + } + + @Override public void onFixedRotationStarted(int displayId, int newRotation) { mNextRotation = newRotation; mWaitForFixedRotation = true; + if (Transitions.ENABLE_SHELL_TRANSITIONS) { + // The fixed rotation will also be included in the transition info. However, if it is + // not a PIP transition (such as open another app to different orientation), + // PIP transition handler may not be aware of the fixed rotation start. + // Notify the PIP transition handler so that it can fade out the PIP window early for + // fixed transition of other windows. + mPipTransitionController.onFixedRotationStarted(); + return; + } + if (mPipTransitionState.isInPip()) { // Fade out the existing PiP to avoid jump cut during seamless rotation. fadeExistingPip(false /* show */); @@ -799,6 +894,10 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, if (!mWaitForFixedRotation) { return; } + if (Transitions.ENABLE_SHELL_TRANSITIONS) { + clearWaitForFixedRotation(); + return; + } if (mPipTransitionState.getTransitionState() == PipTransitionState.TASK_APPEARED) { if (mPipTransitionState.getInSwipePipToHomeTransition()) { onEndOfSwipePipToHomeTransition(); @@ -824,9 +923,31 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, clearWaitForFixedRotation(); } + /** Called when exiting PIP transition is finished to do the state cleanup. */ + void onExitPipFinished(TaskInfo info) { + clearWaitForFixedRotation(); + if (mSwipePipToHomeOverlay != null) { + removeContentOverlay(mSwipePipToHomeOverlay, null /* callback */); + mSwipePipToHomeOverlay = null; + } + resetShadowRadius(); + mPipTransitionState.setInSwipePipToHomeTransition(false); + mPictureInPictureParams = null; + mPipTransitionState.setTransitionState(PipTransitionState.UNDEFINED); + // Re-set the PIP bounds to none. + mPipBoundsState.setBounds(new Rect()); + mPipUiEventLoggerLogger.setTaskInfo(null); + mPipMenuController.detach(); + + if (info.displayId != Display.DEFAULT_DISPLAY && mOnDisplayIdChangeCallback != null) { + mOnDisplayIdChangeCallback.accept(Display.DEFAULT_DISPLAY); + } + } + private void fadeExistingPip(boolean show) { if (mLeash == null || !mLeash.isValid()) { - Log.w(TAG, "Invalid leash on fadeExistingPip: " + mLeash); + ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: Invalid leash on fadeExistingPip: %s", TAG, mLeash); return; } final float alphaStart = show ? 0 : 1; @@ -845,6 +966,22 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, mDeferredAnimEndTransaction = null; } + /** Explicitly set the visibility of PiP window. */ + public void setPipVisibility(boolean visible) { + if (!isInPip()) { + return; + } + if (mLeash == null || !mLeash.isValid()) { + ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: Invalid leash on setPipVisibility: %s", TAG, mLeash); + return; + } + final SurfaceControl.Transaction tx = + mSurfaceControlTransactionFactory.getTransaction(); + mSurfaceTransactionHelper.alpha(tx, mLeash, visible ? 1f : 0f); + tx.apply(); + } + @Override public void onDisplayConfigurationChanged(int displayId, Configuration newConfig) { mCurrentRotation = newConfig.windowConfiguration.getRotation(); @@ -873,11 +1010,13 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, if ((mPipTransitionState.getInSwipePipToHomeTransition() || waitForFixedRotationOnEnteringPip) && fromRotation) { if (DEBUG) { - Log.d(TAG, "Skip onMovementBoundsChanged on rotation change" - + " InSwipePipToHomeTransition=" - + mPipTransitionState.getInSwipePipToHomeTransition() - + " mWaitForFixedRotation=" + mWaitForFixedRotation - + " getTransitionState=" + mPipTransitionState.getTransitionState()); + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: Skip onMovementBoundsChanged on rotation change" + + " InSwipePipToHomeTransition=%b" + + " mWaitForFixedRotation=%b" + + " getTransitionState=%d", TAG, + mPipTransitionState.getInSwipePipToHomeTransition(), mWaitForFixedRotation, + mPipTransitionState.getTransitionState()); } return; } @@ -886,7 +1025,10 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, if (animator == null || !animator.isRunning() || animator.getTransitionDirection() != TRANSITION_DIRECTION_TO_PIP) { final boolean rotatingPip = mPipTransitionState.isInPip() && fromRotation; - if (rotatingPip && mWaitForFixedRotation && mHasFadeOut) { + if (rotatingPip && Transitions.ENABLE_SHELL_TRANSITIONS) { + // The animation and surface update will be handled by the shell transition handler. + mPipBoundsState.setBounds(destinationBoundsOut); + } else if (rotatingPip && mWaitForFixedRotation && mHasFadeOut) { // The position will be used by fade-in animation when the fixed rotation is done. mPipBoundsState.setBounds(destinationBoundsOut); } else if (rotatingPip) { @@ -900,9 +1042,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, int direction = TRANSITION_DIRECTION_NONE; if (animator != null) { direction = animator.getTransitionDirection(); - animator.removeAllUpdateListeners(); - animator.removeAllListeners(); - animator.cancel(); + PipAnimationController.quietCancel(animator); // Do notify the listeners that this was canceled sendOnPipTransitionCancelled(direction); sendOnPipTransitionFinished(direction); @@ -957,20 +1097,21 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, } /** - * @return {@code true} if the aspect ratio is changed since no other parameters within - * {@link PictureInPictureParams} would affect the bounds. + * Handles all changes to the PictureInPictureParams. */ - private boolean applyPictureInPictureParams(@NonNull PictureInPictureParams params) { - final Rational currentAspectRatio = - mPictureInPictureParams != null ? mPictureInPictureParams.getAspectRatioRational() - : null; - final boolean aspectRatioChanged = !Objects.equals(currentAspectRatio, - params.getAspectRatioRational()); - mPictureInPictureParams = params; - if (aspectRatioChanged) { - mPipBoundsState.setAspectRatio(params.getAspectRatio()); + protected void applyNewPictureInPictureParams(@NonNull PictureInPictureParams params) { + if (mDeferredTaskInfo != null || PipUtils.aspectRatioChanged(params.getAspectRatioFloat(), + mPictureInPictureParams.getAspectRatioFloat())) { + mPipParamsChangedForwarder.notifyAspectRatioChanged(params.getAspectRatioFloat()); + } + if (mDeferredTaskInfo != null + || PipUtils.remoteActionsChanged(params.getActions(), + mPictureInPictureParams.getActions()) + || !PipUtils.remoteActionsMatch(params.getCloseAction(), + mPictureInPictureParams.getCloseAction())) { + mPipParamsChangedForwarder.notifyActionsChanged(params.getActions(), + params.getCloseAction()); } - return aspectRatioChanged; } /** @@ -989,7 +1130,8 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, @PipAnimationController.TransitionDirection int direction, Consumer<Rect> updateBoundsCallback) { if (mWaitForFixedRotation) { - Log.d(TAG, "skip scheduleAnimateResizePip, entering pip deferred"); + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: skip scheduleAnimateResizePip, entering pip deferred", TAG); return; } scheduleAnimateResizePip(mPipBoundsState.getBounds(), toBounds, 0 /* startingAngle */, @@ -1003,7 +1145,8 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, public void scheduleAnimateResizePip(Rect fromBounds, Rect toBounds, int duration, float startingAngle, Consumer<Rect> updateBoundsCallback) { if (mWaitForFixedRotation) { - Log.d(TAG, "skip scheduleAnimateResizePip, entering pip deferred"); + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: skip scheduleAnimateResizePip, entering pip deferred", TAG); return; } scheduleAnimateResizePip(fromBounds, toBounds, startingAngle, null /* sourceHintRect */, @@ -1041,7 +1184,8 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, public void scheduleResizePip(Rect toBounds, Consumer<Rect> updateBoundsCallback) { // Could happen when exitPip if (mToken == null || mLeash == null) { - Log.w(TAG, "Abort animation, invalid leash"); + ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: Abort animation, invalid leash", TAG); return; } mPipBoundsState.setBounds(toBounds); @@ -1076,12 +1220,14 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, Consumer<Rect> updateBoundsCallback) { // Could happen when exitPip if (mToken == null || mLeash == null) { - Log.w(TAG, "Abort animation, invalid leash"); + ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: Abort animation, invalid leash", TAG); return; } if (startBounds.isEmpty() || toBounds.isEmpty()) { - Log.w(TAG, "Attempted to user resize PIP to or from empty bounds, aborting."); + ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: Attempted to user resize PIP to or from empty bounds, aborting.", TAG); return; } @@ -1152,11 +1298,13 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, */ public void scheduleOffsetPip(Rect originalBounds, int offset, int duration, Consumer<Rect> updateBoundsCallback) { - if (mPipTransitionState.shouldBlockResizeRequest()) { + if (mPipTransitionState.shouldBlockResizeRequest() + || mPipTransitionState.getInSwipePipToHomeTransition()) { return; } if (mWaitForFixedRotation) { - Log.d(TAG, "skip scheduleOffsetPip, entering pip deferred"); + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: skip scheduleOffsetPip, entering pip deferred", TAG); return; } offsetPip(originalBounds, 0 /* xOffset */, offset, duration); @@ -1169,7 +1317,8 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, private void offsetPip(Rect originalBounds, int xOffset, int yOffset, int durationMs) { if (mTaskInfo == null) { - Log.w(TAG, "mTaskInfo is not set"); + ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: mTaskInfo is not set", + TAG); return; } final Rect destinationBounds = new Rect(originalBounds); @@ -1281,7 +1430,8 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, public void applyFinishBoundsResize(@NonNull WindowContainerTransaction wct, @PipAnimationController.TransitionDirection int direction, boolean wasPipTopLeft) { if (direction == TRANSITION_DIRECTION_LEAVE_PIP_TO_SPLIT_SCREEN) { - mSplitScreenOptional.get().enterSplitScreen(mTaskInfo.taskId, wasPipTopLeft, wct); + mSplitScreenOptional.ifPresent(splitScreenController -> + splitScreenController.enterSplitScreen(mTaskInfo.taskId, wasPipTopLeft, wct)); } else { mTaskOrganizer.applyTransaction(wct); } @@ -1313,7 +1463,8 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, float startingAngle) { // Could happen when exitPip if (mToken == null || mLeash == null) { - Log.w(TAG, "Abort animation, invalid leash"); + ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: Abort animation, invalid leash", TAG); return null; } final int rotationDelta = mWaitForFixedRotation @@ -1339,7 +1490,17 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, if (isInPipDirection(direction)) { // 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.setUseContentOverlay(mContext); + if (sourceHintRect == null) { + animator.setColorContentOverlay(mContext); + } else { + final TaskSnapshot snapshot = PipUtils.getTaskSnapshot( + mTaskInfo.launchIntoPipHostTaskId, false /* isLowResolution */); + if (snapshot != null) { + // use the task snapshot during the animation, this is for + // launch-into-pip aka. content-pip use case. + animator.setSnapshotContentOverlay(snapshot, sourceHintRect); + } + } // The destination bounds are used for the end rect of animation and the final bounds // after animation finishes. So after the animation is started, the destination bounds // can be updated to new rotation (computeRotatedBounds has changed the DisplayLayout @@ -1380,45 +1541,30 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, } /** - * Sync with {@link LegacySplitScreenController} or {@link SplitScreenController} on destination - * bounds if PiP is going to split screen. + * Sync with {@link SplitScreenController} on destination bounds if PiP is going to + * split screen. * * @param destinationBoundsOut contain the updated destination bounds if applicable * @return {@code true} if destinationBounds is altered for split screen */ private boolean syncWithSplitScreenBounds(Rect destinationBoundsOut, boolean enterSplit) { - if (enterSplit && mSplitScreenOptional.isPresent()) { - final Rect topLeft = new Rect(); - final Rect bottomRight = new Rect(); - mSplitScreenOptional.get().getStageBounds(topLeft, bottomRight); - final boolean isPipTopLeft = isPipTopLeft(); - destinationBoundsOut.set(isPipTopLeft ? topLeft : bottomRight); - return true; - } - - if (!mLegacySplitScreenOptional.isPresent()) { + if (!enterSplit || !mSplitScreenOptional.isPresent()) { return false; } - - LegacySplitScreenController legacySplitScreen = mLegacySplitScreenOptional.get(); - if (!legacySplitScreen.isDividerVisible()) { - // fail early if system is not in split screen mode - return false; - } - - // PiP window will go to split-secondary mode instead of fullscreen, populates the - // split screen bounds here. - destinationBoundsOut.set(legacySplitScreen.getDividerView() - .getNonMinimizedSplitScreenSecondaryBounds()); + final Rect topLeft = new Rect(); + final Rect bottomRight = new Rect(); + mSplitScreenOptional.get().getStageBounds(topLeft, bottomRight); + final boolean isPipTopLeft = isPipTopLeft(); + destinationBoundsOut.set(isPipTopLeft ? topLeft : bottomRight); return true; } /** * Fades out and removes an overlay surface. */ - private void fadeOutAndRemoveOverlay(SurfaceControl surface, Runnable callback, + void fadeOutAndRemoveOverlay(SurfaceControl surface, Runnable callback, boolean withStartDelay) { - if (surface == null) { + if (surface == null || !surface.isValid()) { return; } @@ -1428,11 +1574,10 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, if (mPipTransitionState.getTransitionState() == PipTransitionState.UNDEFINED) { // Could happen if onTaskVanished happens during the animation since we may have // set a start delay on this animation. - Log.d(TAG, "Task vanished, skip fadeOutAndRemoveOverlay"); - animation.removeAllListeners(); - animation.removeAllUpdateListeners(); - animation.cancel(); - } else { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: Task vanished, skip fadeOutAndRemoveOverlay", TAG); + PipAnimationController.quietCancel(animation); + } else if (surface.isValid()) { final float alpha = (float) animation.getAnimatedValue(); final SurfaceControl.Transaction transaction = mSurfaceControlTransactionFactory.getTransaction(); @@ -1455,13 +1600,45 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, // Avoid double removal, which is fatal. return; } - final SurfaceControl.Transaction tx = - mSurfaceControlTransactionFactory.getTransaction(); + if (surface == null || !surface.isValid()) { + ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: trying to remove invalid content overlay (%s)", TAG, surface); + return; + } + final SurfaceControl.Transaction tx = mSurfaceControlTransactionFactory.getTransaction(); tx.remove(surface); tx.apply(); if (callback != null) callback.run(); } + private void resetShadowRadius() { + if (mPipTransitionState.getTransitionState() == PipTransitionState.UNDEFINED) { + // mLeash is undefined when in PipTransitionState.UNDEFINED + return; + } + final SurfaceControl.Transaction tx = mSurfaceControlTransactionFactory.getTransaction(); + tx.setShadowRadius(mLeash, 0f); + tx.apply(); + } + + private void cancelCurrentAnimator() { + final PipAnimationController.PipTransitionAnimator<?> animator = + mPipAnimationController.getCurrentAnimator(); + if (animator != null) { + if (animator.getContentOverlayLeash() != null) { + removeContentOverlay(animator.getContentOverlayLeash(), + animator::clearContentOverlay); + } + PipAnimationController.quietCancel(animator); + } + } + + @VisibleForTesting + public void setSurfaceControlTransactionFactory( + PipSurfaceTransactionHelper.SurfaceControlTransactionFactory factory) { + mSurfaceControlTransactionFactory = factory; + } + /** * 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 b31e6e0750ce..36e712459863 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 @@ -16,42 +16,60 @@ package com.android.wm.shell.pip; +import static android.app.WindowConfiguration.ROTATION_UNDEFINED; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static android.util.RotationUtils.deltaRotation; +import static android.util.RotationUtils.rotateBounds; +import static android.view.Surface.ROTATION_270; +import static android.view.Surface.ROTATION_90; +import static android.view.WindowManager.TRANSIT_CHANGE; 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; import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_LEAVE_PIP; +import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_SAME; import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_TO_PIP; import static com.android.wm.shell.pip.PipAnimationController.isInPipDirection; import static com.android.wm.shell.pip.PipAnimationController.isOutPipDirection; +import static com.android.wm.shell.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.app.ActivityManager; import android.app.TaskInfo; import android.content.Context; import android.graphics.Matrix; +import android.graphics.Point; import android.graphics.Rect; import android.os.IBinder; -import android.util.Log; import android.view.Surface; import android.view.SurfaceControl; import android.window.TransitionInfo; import android.window.TransitionRequestInfo; +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.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.transition.CounterRotatorHelper; import com.android.wm.shell.transition.Transitions; +import java.util.Optional; + /** * Implementation of transitions for PiP on phone. Responsible for enter (alpha, bounds) and * exit animation. @@ -60,12 +78,32 @@ public class PipTransition extends PipTransitionController { private static final String TAG = PipTransition.class.getSimpleName(); + private final Context mContext; private final PipTransitionState mPipTransitionState; private final int mEnterExitAnimationDuration; + private final PipSurfaceTransactionHelper mSurfaceTransactionHelper; + private final Optional<SplitScreenController> mSplitScreenOptional; private @PipAnimationController.AnimationType int mOneShotAnimationType = ANIM_TYPE_BOUNDS; private Transitions.TransitionFinishCallback mFinishCallback; - private Rect mExitDestinationBounds = new Rect(); - private IBinder mExitTransition = null; + private SurfaceControl.Transaction mFinishTransaction; + private final Rect mExitDestinationBounds = new Rect(); + @Nullable + private IBinder mExitTransition; + private IBinder mRequestedEnterTransition; + private WindowContainerToken mRequestedEnterTask; + /** The Task window that is currently in PIP windowing mode. */ + @Nullable + private WindowContainerToken mCurrentPipTaskToken; + /** Whether display is in fixed rotation. */ + private boolean mInFixedRotation; + /** + * The rotation that the display will apply after expanding PiP to fullscreen. This is only + * meaningful if {@link #mInFixedRotation} is true. + */ + @Surface.Rotation + private int mEndFixedRotation; + /** Whether the PIP window has fade out for fixed rotation. */ + private boolean mHasFadeOut; public PipTransition(Context context, PipBoundsState pipBoundsState, @@ -74,12 +112,17 @@ public class PipTransition extends PipTransitionController { PipBoundsAlgorithm pipBoundsAlgorithm, PipAnimationController pipAnimationController, Transitions transitions, - @NonNull ShellTaskOrganizer shellTaskOrganizer) { + @NonNull ShellTaskOrganizer shellTaskOrganizer, + PipSurfaceTransactionHelper pipSurfaceTransactionHelper, + Optional<SplitScreenController> splitScreenOptional) { super(pipBoundsState, pipMenuController, pipBoundsAlgorithm, pipAnimationController, transitions, shellTaskOrganizer); + mContext = context; mPipTransitionState = pipTransitionState; mEnterExitAnimationDuration = context.getResources() .getInteger(R.integer.config_pipResizeAnimationDuration); + mSurfaceTransactionHelper = pipSurfaceTransactionHelper; + mSplitScreenOptional = splitScreenOptional; } @Override @@ -97,97 +140,105 @@ public class PipTransition extends PipTransitionController { } @Override - public void startTransition(Rect destinationBounds, WindowContainerTransaction out) { + public void startExitTransition(int type, WindowContainerTransaction out, + @Nullable Rect destinationBounds) { if (destinationBounds != null) { mExitDestinationBounds.set(destinationBounds); - mExitTransition = mTransitions.startTransition(TRANSIT_EXIT_PIP, out, this); - } else { - mTransitions.startTransition(TRANSIT_REMOVE_PIP, out, this); } + mExitTransition = mTransitions.startTransition(type, out, this); } @Override - public boolean startAnimation(@android.annotation.NonNull IBinder transition, - @android.annotation.NonNull TransitionInfo info, - @android.annotation.NonNull SurfaceControl.Transaction startTransaction, - @android.annotation.NonNull SurfaceControl.Transaction finishTransaction, - @android.annotation.NonNull Transitions.TransitionFinishCallback finishCallback) { - - if (mExitTransition == transition || info.getType() == TRANSIT_EXIT_PIP) { + public boolean startAnimation(@NonNull IBinder transition, + @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, + @NonNull Transitions.TransitionFinishCallback finishCallback) { + final TransitionInfo.Change currentPipTaskChange = findCurrentPipTaskChange(info); + final TransitionInfo.Change fixedRotationChange = findFixedRotationChange(info); + mInFixedRotation = fixedRotationChange != null; + mEndFixedRotation = mInFixedRotation + ? fixedRotationChange.getEndFixedRotation() + : ROTATION_UNDEFINED; + + // Exiting PIP. + final int type = info.getType(); + if (transition.equals(mExitTransition)) { + mExitDestinationBounds.setEmpty(); mExitTransition = null; - if (info.getChanges().size() == 1) { - if (mFinishCallback != null) { - mFinishCallback.onTransitionFinished(null, null); - mFinishCallback = null; - throw new RuntimeException("Previous callback not called, aborting exit PIP."); - } - - final TransitionInfo.Change change = info.getChanges().get(0); - mFinishCallback = finishCallback; - startTransaction.apply(); - boolean success = startExpandAnimation(change.getTaskInfo(), change.getLeash(), - new Rect(mExitDestinationBounds)); - mExitDestinationBounds.setEmpty(); - return success; - } else { - Log.e(TAG, "Got an exit-pip transition with unexpected change-list"); + mHasFadeOut = false; + if (mFinishCallback != null) { + callFinishCallback(null /* wct */); + mFinishTransaction = null; + throw new RuntimeException("Previous callback not called, aborting exit PIP."); } - } - if (info.getType() == TRANSIT_REMOVE_PIP) { - if (mFinishCallback != null) { - mFinishCallback.onTransitionFinished(null /* wct */, null /* callback */); - mFinishCallback = null; - throw new RuntimeException("Previous callback not called, aborting remove PIP."); + // PipTaskChange can be null if the PIP task has been detached, for example, when the + // task contains multiple activities, the PIP will be moved to a new PIP task when + // entering, and be moved back when exiting. In that case, the PIP task will be removed + // immediately. + final TaskInfo pipTaskInfo = currentPipTaskChange != null + ? currentPipTaskChange.getTaskInfo() + : mPipOrganizer.getTaskInfo(); + if (pipTaskInfo == null) { + throw new RuntimeException("Cannot find the pip task for exit-pip transition."); } - startTransaction.apply(); - finishTransaction.setWindowCrop(info.getChanges().get(0).getLeash(), - mPipBoundsState.getDisplayBounds()); - finishCallback.onTransitionFinished(null, null); + switch (type) { + case TRANSIT_EXIT_PIP: + startExitAnimation(info, startTransaction, finishTransaction, finishCallback, + pipTaskInfo, currentPipTaskChange); + break; + case TRANSIT_EXIT_PIP_TO_SPLIT: + startExitToSplitAnimation(info, startTransaction, finishTransaction, + finishCallback, pipTaskInfo); + break; + case TRANSIT_REMOVE_PIP: + removePipImmediately(info, startTransaction, finishTransaction, finishCallback, + pipTaskInfo); + break; + default: + throw new IllegalStateException("mExitTransition with unexpected transit type=" + + transitTypeToString(type)); + } + mCurrentPipTaskToken = null; return true; + } else if (transition == mRequestedEnterTransition) { + mRequestedEnterTransition = null; + mRequestedEnterTask = null; } - // We only support TRANSIT_PIP type (from RootWindowContainer) or TRANSIT_OPEN (from apps - // that enter PiP instantly on opening, mostly from CTS/Flicker tests) - if (info.getType() != TRANSIT_PIP && info.getType() != TRANSIT_OPEN) { - return false; + // The previous PIP Task is no longer in PIP, but this is not an exit transition (This can + // happen when a new activity requests enter PIP). In this case, we just show this Task in + // its end state, and play other animation as normal. + if (currentPipTaskChange != null + && currentPipTaskChange.getTaskInfo().getWindowingMode() != WINDOWING_MODE_PINNED) { + resetPrevPip(currentPipTaskChange, startTransaction); } - // Search for an Enter PiP transition (along with a show wallpaper one) - TransitionInfo.Change enterPip = null; - TransitionInfo.Change wallpaper = null; - for (int i = info.getChanges().size() - 1; i >= 0; --i) { - final TransitionInfo.Change change = info.getChanges().get(i); - if (change.getTaskInfo() != null - && change.getTaskInfo().configuration.windowConfiguration.getWindowingMode() - == WINDOWING_MODE_PINNED) { - enterPip = change; - } else if ((change.getFlags() & FLAG_IS_WALLPAPER) != 0) { - wallpaper = change; - } - } - if (enterPip == null) { - return false; + // Entering PIP. + if (isEnteringPip(info, mCurrentPipTaskToken)) { + return startEnterAnimation(info, startTransaction, finishTransaction, finishCallback); } - if (mFinishCallback != null) { - mFinishCallback.onTransitionFinished(null /* wct */, null /* callback */); - mFinishCallback = null; - throw new RuntimeException("Previous callback not called, aborting entering PIP."); + // 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) { + updatePipForUnhandledTransition(currentPipTaskChange, startTransaction, + finishTransaction); } - // Show the wallpaper if there is a wallpaper change. - if (wallpaper != null) { - startTransaction.show(wallpaper.getLeash()); - startTransaction.setAlpha(wallpaper.getLeash(), 1.f); + // Fade in the fadeout PIP when the fixed rotation is finished. + if (mPipTransitionState.isInPip() && !mInFixedRotation && mHasFadeOut) { + fadeExistingPip(true /* show */); } - mPipTransitionState.setTransitionState(PipTransitionState.ENTERING_PIP); - mFinishCallback = finishCallback; - return startEnterAnimation(enterPip.getTaskInfo(), enterPip.getLeash(), - startTransaction, finishTransaction, enterPip.getStartRotation(), - enterPip.getEndRotation()); + return false; + } + + /** Helper to identify whether this handler is currently the one playing an animation */ + private boolean isAnimatingLocally() { + return mFinishTransaction != null; } @Nullable @@ -196,8 +247,9 @@ public class PipTransition extends PipTransitionController { @NonNull TransitionRequestInfo request) { if (request.getType() == TRANSIT_PIP) { WindowContainerTransaction wct = new WindowContainerTransaction(); - mPipTransitionState.setTransitionState(PipTransitionState.ENTRY_SCHEDULED); if (mOneShotAnimationType == ANIM_TYPE_ALPHA) { + mRequestedEnterTransition = transition; + mRequestedEnterTask = request.getTriggerTask().token; wct.setActivityWindowingMode(request.getTriggerTask().token, WINDOWING_MODE_UNDEFINED); final Rect destinationBounds = mPipBoundsAlgorithm.getEntryDestinationBounds(); @@ -210,6 +262,23 @@ public class PipTransition extends PipTransitionController { } @Override + public boolean handleRotateDisplay(int startRotation, int endRotation, + WindowContainerTransaction wct) { + if (mRequestedEnterTransition != null && mOneShotAnimationType == ANIM_TYPE_ALPHA) { + // A fade-in was requested but not-yet started. In this case, just recalculate the + // initial state under the new rotation. + int rotationDelta = deltaRotation(startRotation, endRotation); + if (rotationDelta != Surface.ROTATION_0) { + mPipBoundsState.getDisplayLayout().rotateTo(mContext.getResources(), endRotation); + final Rect destinationBounds = mPipBoundsAlgorithm.getEntryDestinationBounds(); + wct.setBounds(mRequestedEnterTask, destinationBounds); + return true; + } + } + return false; + } + + @Override public void onTransitionMerged(@NonNull IBinder transition) { if (transition != mExitTransition) { return; @@ -227,54 +296,349 @@ public class PipTransition extends PipTransitionController { final ActivityManager.RunningTaskInfo taskInfo = mPipOrganizer.getTaskInfo(); if (taskInfo != null) { startExpandAnimation(taskInfo, mPipOrganizer.getSurfaceControl(), - new Rect(mExitDestinationBounds)); + new Rect(mExitDestinationBounds), Surface.ROTATION_0); } mExitDestinationBounds.setEmpty(); + mCurrentPipTaskToken = null; } @Override public void onFinishResize(TaskInfo taskInfo, Rect destinationBounds, @PipAnimationController.TransitionDirection int direction, @Nullable SurfaceControl.Transaction tx) { - - if (isInPipDirection(direction)) { - mPipTransitionState.setTransitionState(PipTransitionState.ENTERED_PIP); + final boolean enteringPip = isInPipDirection(direction); + if (enteringPip) { + mPipTransitionState.setTransitionState(ENTERED_PIP); } - // If there is an expected exit transition, then the exit will be "merged" into this - // transition so don't fire the finish-callback in that case. - if (mExitTransition == null && mFinishCallback != null) { + // If we have an exit transition, but aren't playing a transition locally, it + // means we're expecting the exit transition will be "merged" into another transition + // (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); } - mFinishCallback.onTransitionFinished(wct, null /* callback */); - mFinishCallback = null; + final SurfaceControl leash = mPipOrganizer.getSurfaceControl(); + final int displayRotation = taskInfo.getConfiguration().windowConfiguration + .getDisplayRotation(); + if (enteringPip && mInFixedRotation && mEndFixedRotation != displayRotation + && leash != null && leash.isValid()) { + // 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 finishBounds = new Rect(destinationBounds); + rotateBounds(finishBounds, displayBounds, mEndFixedRotation, displayRotation); + mSurfaceTransactionHelper.crop(mFinishTransaction, leash, finishBounds); + } + mFinishTransaction = null; + callFinishCallback(wct); } finishResizeForMenu(destinationBounds); } + private void callFinishCallback(WindowContainerTransaction wct) { + // Need to unset mFinishCallback first because onTransitionFinished can re-enter this + // handler if there is a pending PiP animation. + final Transitions.TransitionFinishCallback finishCallback = mFinishCallback; + mFinishCallback = null; + finishCallback.onTransitionFinished(wct, null /* callback */); + } + @Override public void forceFinishTransition() { if (mFinishCallback == null) return; mFinishCallback.onTransitionFinished(null /* wct */, null /* callback */); mFinishCallback = null; + mFinishTransaction = null; + } + + @Override + public void onFixedRotationStarted() { + // The transition with this fixed rotation may be handled by other handler before reaching + // PipTransition, so we cannot do this in #startAnimation. + if (mPipTransitionState.getTransitionState() == ENTERED_PIP && !mHasFadeOut) { + // Fade out the existing PiP to avoid jump cut during seamless rotation. + fadeExistingPip(false /* show */); + } + } + + @Nullable + private TransitionInfo.Change findCurrentPipTaskChange(@NonNull TransitionInfo info) { + if (mCurrentPipTaskToken == null) { + return null; + } + for (int i = info.getChanges().size() - 1; i >= 0; --i) { + final TransitionInfo.Change change = info.getChanges().get(i); + if (mCurrentPipTaskToken.equals(change.getContainer())) { + return change; + } + } + return null; + } + + @Nullable + private TransitionInfo.Change findFixedRotationChange(@NonNull TransitionInfo info) { + for (int i = info.getChanges().size() - 1; i >= 0; --i) { + final TransitionInfo.Change change = info.getChanges().get(i); + if (change.getEndFixedRotation() != ROTATION_UNDEFINED) { + return change; + } + } + return null; + } + + private void startExitAnimation(@NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, + @NonNull Transitions.TransitionFinishCallback finishCallback, + @NonNull TaskInfo taskInfo, @Nullable TransitionInfo.Change pipTaskChange) { + TransitionInfo.Change pipChange = pipTaskChange; + 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. + 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)) { + pipChange = change; + break; + } + } + } + if (pipChange == null) { + ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: No window of exiting PIP is found. Can't play expand animation", TAG); + removePipImmediately(info, startTransaction, finishTransaction, finishCallback, + taskInfo); + return; + } + mFinishCallback = (wct, wctCB) -> { + mPipOrganizer.onExitPipFinished(taskInfo); + finishCallback.onTransitionFinished(wct, wctCB); + }; + mFinishTransaction = finishTransaction; + + // Check if it is Shell rotation. + if (Transitions.SHELL_TRANSITIONS_ROTATION) { + TransitionInfo.Change displayRotationChange = null; + for (int i = info.getChanges().size() - 1; i >= 0; --i) { + final TransitionInfo.Change change = info.getChanges().get(i); + if (change.getMode() == TRANSIT_CHANGE + && (change.getFlags() & FLAG_IS_DISPLAY) != 0 + && change.getStartRotation() != change.getEndRotation()) { + displayRotationChange = change; + break; + } + } + if (displayRotationChange != null) { + // Exiting PIP to fullscreen with orientation change. + startExpandAndRotationAnimation(info, startTransaction, finishTransaction, + displayRotationChange, taskInfo, pipChange); + 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.apply(); + + // Check if it is fixed rotation. + final int rotationDelta; + if (mInFixedRotation) { + final int startRotation = pipChange.getStartRotation(); + final int endRotation = mEndFixedRotation; + rotationDelta = deltaRotation(startRotation, endRotation); + final Rect endBounds = new Rect(destinationBounds); + + // Set the end frame since the display won't rotate until fixed rotation is finished + // in the next display change transition. + rotateBounds(endBounds, destinationBounds, rotationDelta); + final int degree, x, y; + if (rotationDelta == ROTATION_90) { + degree = 90; + x = destinationBounds.right; + y = destinationBounds.top; + } else { + degree = -90; + x = destinationBounds.left; + y = destinationBounds.bottom; + } + mSurfaceTransactionHelper.rotateAndScaleWithCrop(finishTransaction, + pipChange.getLeash(), endBounds, endBounds, new Rect(), degree, x, y, + true /* isExpanding */, rotationDelta == ROTATION_270 /* clockwise */); + } else { + rotationDelta = Surface.ROTATION_0; + } + startExpandAnimation(taskInfo, pipChange.getLeash(), 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) { + final int rotateDelta = deltaRotation(displayRotationChange.getStartRotation(), + displayRotationChange.getEndRotation()); + + // Counter-rotate all "going-away" things since they are still in the old orientation. + final CounterRotatorHelper rotator = new CounterRotatorHelper(); + rotator.handleClosingChanges(info, startTransaction, displayRotationChange); + + // Get the start bounds in new orientation. + 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); + + // Reverse the rotation direction for expansion. + final int pipRotateDelta = deltaRotation(rotateDelta, 0); + + // Set the start frame. + final int degree, x, y; + if (pipRotateDelta == ROTATION_90) { + degree = 90; + x = startBounds.right; + y = startBounds.top; + } else { + degree = -90; + x = startBounds.left; + y = startBounds.bottom; + } + mSurfaceTransactionHelper.rotateAndScaleWithCrop(startTransaction, pipChange.getLeash(), + endBounds, startBounds, new Rect(), degree, x, y, true /* isExpanding */, + pipRotateDelta == ROTATION_270 /* clockwise */); + startTransaction.apply(); + rotator.cleanUp(finishTransaction); + + // Expand and rotate the pip window to fullscreen. + final PipAnimationController.PipTransitionAnimator animator = + mPipAnimationController.getAnimator(taskInfo, pipChange.getLeash(), + startBounds, startBounds, endBounds, null, TRANSITION_DIRECTION_LEAVE_PIP, + 0 /* startingAngle */, pipRotateDelta); + animator.setTransitionDirection(TRANSITION_DIRECTION_LEAVE_PIP) + .setPipAnimationCallback(mPipAnimationCallback) + .setDuration(mEnterExitAnimationDuration) + .start(); } - private boolean startExpandAnimation(final TaskInfo taskInfo, final SurfaceControl leash, - final Rect destinationBounds) { - PipAnimationController.PipTransitionAnimator animator = + private void startExpandAnimation(final TaskInfo taskInfo, final SurfaceControl leash, + final Rect destinationBounds, final int rotationDelta) { + final PipAnimationController.PipTransitionAnimator animator = mPipAnimationController.getAnimator(taskInfo, leash, mPipBoundsState.getBounds(), mPipBoundsState.getBounds(), destinationBounds, null, - TRANSITION_DIRECTION_LEAVE_PIP, 0 /* startingAngle */, Surface.ROTATION_0); - + TRANSITION_DIRECTION_LEAVE_PIP, 0 /* startingAngle */, rotationDelta); animator.setTransitionDirection(TRANSITION_DIRECTION_LEAVE_PIP) .setPipAnimationCallback(mPipAnimationCallback) .setDuration(mEnterExitAnimationDuration) .start(); + } - return true; + /** For {@link Transitions#TRANSIT_REMOVE_PIP}, we just immediately remove the PIP Task. */ + private void removePipImmediately(@NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, + @NonNull Transitions.TransitionFinishCallback finishCallback, + @NonNull TaskInfo taskInfo) { + startTransaction.apply(); + finishTransaction.setWindowCrop(info.getChanges().get(0).getLeash(), + mPipBoundsState.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) { + 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; + } + + // Please file a bug to handle the unexpected transition type. + throw new IllegalStateException("Entering PIP with unexpected transition type=" + + transitTypeToString(info.getType())); + } + } + return false; + } + + private boolean 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) + 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; + } + // 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())) { + final SurfaceControl leash = change.getLeash(); + startTransaction.show(leash).setAlpha(leash, 1.f); + } + } + + 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, @@ -285,48 +649,70 @@ public class PipTransition extends PipTransitionController { taskInfo.topActivityInfo); final Rect destinationBounds = mPipBoundsAlgorithm.getEntryDestinationBounds(); final Rect currentBounds = taskInfo.configuration.windowConfiguration.getBounds(); + int rotationDelta = deltaRotation(startRotation, endRotation); + Rect sourceHintRect = PipBoundsAlgorithm.getValidSourceHintRect( + taskInfo.pictureInPictureParams, currentBounds); + if (rotationDelta != Surface.ROTATION_0 && mInFixedRotation) { + // Need to get the bounds of new rotation in old rotation for fixed rotation, + computeEnterPipRotatedBounds(rotationDelta, startRotation, endRotation, taskInfo, + destinationBounds, sourceHintRect); + } PipAnimationController.PipTransitionAnimator animator; - finishTransaction.setPosition(leash, destinationBounds.left, destinationBounds.top); + // Set corner radius for entering pip. + mSurfaceTransactionHelper + .crop(finishTransaction, leash, destinationBounds) + .round(finishTransaction, leash, true /* applyCornerRadius */); + mPipMenuController.attach(leash); + if (taskInfo.pictureInPictureParams != null && taskInfo.pictureInPictureParams.isAutoEnterEnabled() && mPipTransitionState.getInSwipePipToHomeTransition()) { mOneShotAnimationType = ANIM_TYPE_BOUNDS; - - // PiP menu is attached late in the process here to avoid any artifacts on the leash - // caused by addShellRoot when in gesture navigation mode. - mPipMenuController.attach(leash); - SurfaceControl.Transaction tx = new SurfaceControl.Transaction(); - tx.setMatrix(leash, Matrix.IDENTITY_MATRIX, new float[9]) + final SurfaceControl swipePipToHomeOverlay = mPipOrganizer.mSwipePipToHomeOverlay; + startTransaction.setMatrix(leash, Matrix.IDENTITY_MATRIX, new float[9]) .setPosition(leash, destinationBounds.left, destinationBounds.top) .setWindowCrop(leash, destinationBounds.width(), destinationBounds.height()); - startTransaction.merge(tx); + if (swipePipToHomeOverlay != null) { + // Launcher fade in the overlay on top of the fullscreen Task. It is possible we + // reparent the PIP activity to a new PIP task (in case there are other activities + // in the original Task), so we should also reparent the overlay to the PIP task. + startTransaction.reparent(swipePipToHomeOverlay, leash) + .setLayer(swipePipToHomeOverlay, Integer.MAX_VALUE); + mPipOrganizer.mSwipePipToHomeOverlay = null; + } startTransaction.apply(); + if (rotationDelta != Surface.ROTATION_0 && mInFixedRotation) { + // For fixed rotation, set the destination bounds to the new rotation coordinates + // at the end. + destinationBounds.set(mPipBoundsAlgorithm.getEntryDestinationBounds()); + } mPipBoundsState.setBounds(destinationBounds); onFinishResize(taskInfo, destinationBounds, TRANSITION_DIRECTION_TO_PIP, null /* tx */); sendOnPipTransitionFinished(TRANSITION_DIRECTION_TO_PIP); + if (swipePipToHomeOverlay != null) { + mPipOrganizer.fadeOutAndRemoveOverlay(swipePipToHomeOverlay, + null /* callback */, false /* withStartDelay */); + } mPipTransitionState.setInSwipePipToHomeTransition(false); return true; } - int rotationDelta = deltaRotation(endRotation, startRotation); if (rotationDelta != Surface.ROTATION_0) { Matrix tmpTransform = new Matrix(); - tmpTransform.postRotate(rotationDelta == Surface.ROTATION_90 - ? Surface.ROTATION_270 : Surface.ROTATION_90); + tmpTransform.postRotate(rotationDelta); startTransaction.setMatrix(leash, tmpTransform, new float[9]); } if (mOneShotAnimationType == ANIM_TYPE_BOUNDS) { - final Rect sourceHintRect = - PipBoundsAlgorithm.getValidSourceHintRect( - taskInfo.pictureInPictureParams, currentBounds); animator = mPipAnimationController.getAnimator(taskInfo, leash, currentBounds, currentBounds, destinationBounds, sourceHintRect, TRANSITION_DIRECTION_TO_PIP, 0 /* startingAngle */, rotationDelta); + if (sourceHintRect == null) { + // We use content overlay when there is no source rect hint to enter PiP use bounds + // animation. + animator.setColorContentOverlay(mContext); + } } else if (mOneShotAnimationType == ANIM_TYPE_ALPHA) { startTransaction.setAlpha(leash, 0f); - // PiP menu is attached late in the process here to avoid any artifacts on the leash - // caused by addShellRoot when in gesture navigation mode. - mPipMenuController.attach(leash); animator = mPipAnimationController.getAnimator(taskInfo, leash, destinationBounds, 0f, 1f); mOneShotAnimationType = ANIM_TYPE_BOUNDS; @@ -337,12 +723,131 @@ public class PipTransition extends PipTransitionController { startTransaction.apply(); animator.setTransitionDirection(TRANSITION_DIRECTION_TO_PIP) .setPipAnimationCallback(mPipAnimationCallback) - .setDuration(mEnterExitAnimationDuration) - .start(); + .setDuration(mEnterExitAnimationDuration); + if (rotationDelta != Surface.ROTATION_0 && mInFixedRotation) { + // For fixed rotation, the animation destination bounds is in old rotation coordinates. + // Set the destination bounds to new coordinates after the animation is finished. + // ComputeRotatedBounds has changed the DisplayLayout without affecting the animation. + 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(); + outDestinationBounds.set(mPipBoundsAlgorithm.getEntryDestinationBounds()); + // Transform the destination bounds to current display coordinates. + rotateBounds(outDestinationBounds, displayBounds, endRotation, startRotation); + // When entering PiP (from button navigation mode), adjust the source rect hint by + // display cutout if applicable. + if (outSourceHintRect != null && taskInfo.displayCutoutInsets != null) { + if (rotationDelta == Surface.ROTATION_270) { + outSourceHintRect.offset(taskInfo.displayCutoutInsets.left, + taskInfo.displayCutoutInsets.top); + } + } + } + + private void startExitToSplitAnimation(@NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, + @NonNull Transitions.TransitionFinishCallback finishCallback, + @NonNull TaskInfo taskInfo) { + final int changeSize = info.getChanges().size(); + if (changeSize < 4) { + throw new RuntimeException( + "Got an exit-pip-to-split transition with unexpected change-list"); + } + for (int i = changeSize - 1; i >= 0; i--) { + final TransitionInfo.Change change = info.getChanges().get(i); + final int mode = change.getMode(); + + if (mode == TRANSIT_CHANGE && change.getParent() != null) { + // TODO: perform resize/expand animation for reparented child task. + continue; + } + + if (isOpeningType(mode) && change.getParent() == null) { + final SurfaceControl leash = change.getLeash(); + final Rect endBounds = change.getEndAbsBounds(); + startTransaction + .show(leash) + .setAlpha(leash, 1f) + .setPosition(leash, endBounds.left, endBounds.top) + .setWindowCrop(leash, endBounds.width(), endBounds.height()); + } + } + mSplitScreenOptional.get().finishEnterSplitScreen(startTransaction); + startTransaction.apply(); + + mPipOrganizer.onExitPipFinished(taskInfo); + finishCallback.onTransitionFinished(null, null); + } + + private void resetPrevPip(@NonNull TransitionInfo.Change prevPipTaskChange, + @NonNull SurfaceControl.Transaction startTransaction) { + final SurfaceControl leash = prevPipTaskChange.getLeash(); + final Rect bounds = prevPipTaskChange.getEndAbsBounds(); + final Point offset = prevPipTaskChange.getEndRelOffset(); + bounds.offset(-offset.x, -offset.y); + + startTransaction.setWindowCrop(leash, null); + startTransaction.setMatrix(leash, 1, 0, 0, 1); + startTransaction.setCornerRadius(leash, 0); + startTransaction.setPosition(leash, bounds.left, bounds.top); + + if (mHasFadeOut && prevPipTaskChange.getTaskInfo().isVisible()) { + if (mPipAnimationController.getCurrentAnimator() != null) { + mPipAnimationController.getCurrentAnimator().cancel(); + } + startTransaction.setAlpha(leash, 1); + } + mHasFadeOut = false; + mCurrentPipTaskToken = null; + mPipOrganizer.onExitPipFinished(prevPipTaskChange.getTaskInfo()); + } + + private void updatePipForUnhandledTransition(@NonNull TransitionInfo.Change pipChange, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction) { + // When the PIP window is visible and being a part of the transition, such as display + // rotation, we need to update its bounds and rounded corner. + final SurfaceControl leash = pipChange.getLeash(); + final Rect destBounds = mPipBoundsState.getBounds(); + final boolean isInPip = mPipTransitionState.isInPip(); + mSurfaceTransactionHelper + .crop(startTransaction, leash, destBounds) + .round(startTransaction, leash, isInPip); + mSurfaceTransactionHelper + .crop(finishTransaction, leash, destBounds) + .round(finishTransaction, leash, isInPip); + } + + /** Hides and shows the existing PIP during fixed rotation transition of other activities. */ + private void fadeExistingPip(boolean show) { + final SurfaceControl leash = mPipOrganizer.getSurfaceControl(); + final TaskInfo taskInfo = mPipOrganizer.getTaskInfo(); + if (leash == null || !leash.isValid() || taskInfo == null) { + ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: Invalid leash on fadeExistingPip: %s", TAG, leash); + return; + } + final float alphaStart = show ? 0 : 1; + final float alphaEnd = show ? 1 : 0; + mPipAnimationController + .getAnimator(taskInfo, leash, mPipBoundsState.getBounds(), alphaStart, alphaEnd) + .setTransitionDirection(TRANSITION_DIRECTION_SAME) + .setPipAnimationCallback(mPipAnimationCallback) + .setDuration(mEnterExitAnimationDuration) + .start(); + mHasFadeOut = !show; + } + private void finishResizeForMenu(Rect destinationBounds) { mPipMenuController.movePipMenu(null, null, destinationBounds); mPipMenuController.updateMenuBounds(destinationBounds); 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 376f3298a83c..54f46e0c9938 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java @@ -19,7 +19,9 @@ package com.android.wm.shell.pip; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_REMOVE_STACK; +import static com.android.wm.shell.pip.PipAnimationController.isInPipDirection; +import android.annotation.Nullable; import android.app.PictureInPictureParams; import android.app.TaskInfo; import android.content.ComponentName; @@ -68,6 +70,10 @@ public abstract class PipTransitionController implements Transitions.TransitionH if (direction == TRANSITION_DIRECTION_REMOVE_STACK) { return; } + if (isInPipDirection(direction) && animator.getContentOverlayLeash() != null) { + mPipOrganizer.fadeOutAndRemoveOverlay(animator.getContentOverlayLeash(), + animator::clearContentOverlay, true /* withStartDelay*/); + } onFinishResize(taskInfo, animator.getDestinationBounds(), direction, tx); sendOnPipTransitionFinished(direction); } @@ -75,6 +81,11 @@ public abstract class PipTransitionController implements Transitions.TransitionH @Override public void onPipAnimationCancel(TaskInfo taskInfo, PipAnimationController.PipTransitionAnimator animator) { + final int direction = animator.getTransitionDirection(); + if (isInPipDirection(direction) && animator.getContentOverlayLeash() != null) { + mPipOrganizer.fadeOutAndRemoveOverlay(animator.getContentOverlayLeash(), + animator::clearContentOverlay, true /* withStartDelay */); + } sendOnPipTransitionCancelled(animator.getTransitionDirection()); } }; @@ -98,9 +109,10 @@ public abstract class PipTransitionController implements Transitions.TransitionH } /** - * Called when the Shell wants to starts a transition/animation. + * Called when the Shell wants to start an exit Pip transition/animation. */ - public void startTransition(Rect destinationBounds, WindowContainerTransaction out) { + public void startExitTransition(int type, WindowContainerTransaction out, + @Nullable Rect destinationBounds) { // Default implementation does nothing. } @@ -111,6 +123,10 @@ public abstract class PipTransitionController implements Transitions.TransitionH public void forceFinishTransition() { } + /** Called when the fixed rotation started. */ + public void onFixedRotationStarted() { + } + public PipTransitionController(PipBoundsState pipBoundsState, PipMenuController pipMenuController, PipBoundsAlgorithm pipBoundsAlgorithm, PipAnimationController pipAnimationController, Transitions transitions, @@ -175,9 +191,19 @@ public abstract class PipTransitionController implements Transitions.TransitionH protected void setBoundsStateForEntry(ComponentName componentName, PictureInPictureParams params, ActivityInfo activityInfo) { - mPipBoundsState.setBoundsStateForEntry(componentName, - mPipBoundsAlgorithm.getAspectRatioOrDefault(params), - mPipBoundsAlgorithm.getMinimalSize(activityInfo)); + mPipBoundsState.setBoundsStateForEntry(componentName, activityInfo, params, + mPipBoundsAlgorithm); + } + + /** + * Called when the display is going to rotate. + * + * @return {@code true} if it was handled, otherwise the existing pip logic + * will deal with rotation. + */ + public boolean handleRotateDisplay(int startRotation, int endRotation, + WindowContainerTransaction wct) { + return false; } /** 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 a0a76d801cf4..513ebba59258 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 @@ -107,7 +107,13 @@ public class PipUiEventLogger { PICTURE_IN_PICTURE_STASH_LEFT(710), @UiEvent(doc = "User stashed picture-in-picture to the right side") - PICTURE_IN_PICTURE_STASH_RIGHT(711); + PICTURE_IN_PICTURE_STASH_RIGHT(711), + + @UiEvent(doc = "User taps on the settings button in PiP menu") + PICTURE_IN_PICTURE_SHOW_SETTINGS(933), + + @UiEvent(doc = "Closes PiP with app-provided close action") + PICTURE_IN_PICTURE_CUSTOM_CLOSE(1058); private final int mId; 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 da6d9804b29d..dc60bcf742ce 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 @@ -19,18 +19,30 @@ package com.android.wm.shell.pip; import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; +import android.annotation.Nullable; import android.app.ActivityTaskManager; import android.app.ActivityTaskManager.RootTaskInfo; +import android.app.RemoteAction; import android.content.ComponentName; import android.content.Context; import android.os.RemoteException; import android.util.Log; import android.util.Pair; +import android.window.TaskSnapshot; + +import com.android.internal.protolog.common.ProtoLog; +import com.android.wm.shell.protolog.ShellProtoLogGroup; + +import java.util.List; +import java.util.Objects; /** A class that includes convenience methods. */ public class PipUtils { private static final String TAG = "PipUtils"; + // Minimum difference between two floats (e.g. aspect ratios) to consider them not equal. + private static final double EPSILON = 1e-7; + /** * @return the ComponentName and user id of the top non-SystemUI activity in the pinned stack. * The component name may be null if no such activity exists. @@ -51,8 +63,63 @@ public class PipUtils { } } } catch (RemoteException e) { - Log.w(TAG, "Unable to get pinned stack."); + ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: Unable to get pinned stack.", TAG); } return new Pair<>(null, 0); } + + /** + * @return true if the aspect ratios differ + */ + public static boolean aspectRatioChanged(float aspectRatio1, float aspectRatio2) { + return Math.abs(aspectRatio1 - aspectRatio2) > EPSILON; + } + + /** + * Checks whether title, description and intent match. + * Comparing icons would be good, but using equals causes false negatives + */ + 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()) + && Objects.equals(action1.getContentDescription(), action2.getContentDescription()) + && Objects.equals(action1.getActionIntent(), action2.getActionIntent()); + } + + /** + * Returns true if the actions in the lists match each other according to {@link + * PipUtils#remoteActionsMatch(RemoteAction, RemoteAction)}, including their position. + */ + public static boolean remoteActionsChanged(List<RemoteAction> list1, List<RemoteAction> list2) { + if (list1 == null && list2 == null) { + return false; + } + if (list1 == null || list2 == null) { + return true; + } + if (list1.size() != list2.size()) { + return true; + } + for (int i = 0; i < list1.size(); i++) { + if (!remoteActionsMatch(list1.get(i), list2.get(i))) { + return true; + } + } + return false; + } + + /** @return {@link TaskSnapshot} for a given task id. */ + @Nullable + public static TaskSnapshot getTaskSnapshot(int taskId, boolean isLowResolution) { + if (taskId <= 0) return null; + try { + return ActivityTaskManager.getService().getTaskSnapshot( + taskId, isLowResolution); + } 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/PhonePipMenuController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java index 101a55d8d367..4942987742a0 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 @@ -22,15 +22,12 @@ import android.annotation.Nullable; import android.app.ActivityManager; import android.app.RemoteAction; import android.content.Context; -import android.content.pm.ParceledListSlice; import android.graphics.Matrix; import android.graphics.Rect; import android.graphics.RectF; import android.os.Debug; import android.os.Handler; -import android.os.IBinder; import android.os.RemoteException; -import android.util.Log; import android.util.Size; import android.view.MotionEvent; import android.view.SurfaceControl; @@ -38,12 +35,15 @@ import android.view.SyncRtSurfaceTransactionApplier; import android.view.SyncRtSurfaceTransactionApplier.SurfaceParams; import android.view.WindowManagerGlobal; +import com.android.internal.protolog.common.ProtoLog; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SystemWindows; 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.PipUiEventLogger; +import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.splitscreen.SplitScreenController; import java.io.PrintWriter; @@ -118,38 +118,29 @@ public class PhonePipMenuController implements PipMenuController { private final ArrayList<Listener> mListeners = new ArrayList<>(); private final SystemWindows mSystemWindows; private final Optional<SplitScreenController> mSplitScreenController; - private ParceledListSlice<RemoteAction> mAppActions; - private ParceledListSlice<RemoteAction> mMediaActions; + private final PipUiEventLogger mPipUiEventLogger; + + private List<RemoteAction> mAppActions; + private RemoteAction mCloseAction; + private List<RemoteAction> mMediaActions; + private SyncRtSurfaceTransactionApplier mApplier; private int mMenuState; private PipMenuView mPipMenuView; - private IBinder mPipMenuInputToken; private ActionListener mMediaActionListener = new ActionListener() { @Override public void onMediaActionsChanged(List<RemoteAction> mediaActions) { - mMediaActions = new ParceledListSlice<>(mediaActions); + mMediaActions = new ArrayList<>(mediaActions); updateMenuActions(); } }; - private final float[] mTmpValues = new float[9]; - private final Runnable mUpdateEmbeddedMatrix = () -> { - if (mPipMenuView == null || mPipMenuView.getViewRootImpl() == null) { - return; - } - mMoveTransform.getValues(mTmpValues); - try { - mPipMenuView.getViewRootImpl().getAccessibilityEmbeddedConnection() - .setScreenMatrix(mTmpValues); - } catch (RemoteException e) { - } - }; - public PhonePipMenuController(Context context, PipBoundsState pipBoundsState, PipMediaController mediaController, SystemWindows systemWindows, Optional<SplitScreenController> splitScreenOptional, + PipUiEventLogger pipUiEventLogger, ShellExecutor mainExecutor, Handler mainHandler) { mContext = context; mPipBoundsState = pipBoundsState; @@ -158,6 +149,7 @@ public class PhonePipMenuController implements PipMenuController { mMainExecutor = mainExecutor; mMainHandler = mainHandler; mSplitScreenController = splitScreenOptional; + mPipUiEventLogger = pipUiEventLogger; } public boolean isMenuVisible() { @@ -181,17 +173,20 @@ public class PhonePipMenuController implements PipMenuController { detachPipMenuView(); } - private void attachPipMenuView() { + void attachPipMenuView() { // In case detach was not called (e.g. PIP unexpectedly closed) if (mPipMenuView != null) { detachPipMenuView(); } mPipMenuView = new PipMenuView(mContext, this, mMainExecutor, mMainHandler, - mSplitScreenController); + mSplitScreenController, mPipUiEventLogger); mSystemWindows.addView(mPipMenuView, getPipMenuLayoutParams(MENU_WINDOW_TITLE, 0 /* width */, 0 /* height */), 0, SHELL_ROOT_LAYER_PIP); setShellRootAccessibilityWindow(); + + // Make sure the initial actions are set + updateMenuActions(); } private void detachPipMenuView() { @@ -202,7 +197,6 @@ public class PhonePipMenuController implements PipMenuController { mApplier = null; mSystemWindows.removeView(mPipMenuView); mPipMenuView = null; - mPipMenuInputToken = null; } /** @@ -284,13 +278,15 @@ public class PhonePipMenuController implements PipMenuController { private void showMenuInternal(int menuState, Rect stackBounds, boolean allowMenuTimeout, boolean willResizeMenu, boolean withDelay, boolean showResizeHandle) { if (DEBUG) { - Log.d(TAG, "showMenu() state=" + menuState - + " isMenuVisible=" + isMenuVisible() - + " allowMenuTimeout=" + allowMenuTimeout - + " willResizeMenu=" + willResizeMenu - + " withDelay=" + withDelay - + " showResizeHandle=" + showResizeHandle - + " callers=\n" + Debug.getCallers(5, " ")); + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: showMenu() state=%s" + + " isMenuVisible=%s" + + " allowMenuTimeout=%s" + + " willResizeMenu=%s" + + " withDelay=%s" + + " showResizeHandle=%s" + + " callers=\n%s", TAG, menuState, isMenuVisible(), allowMenuTimeout, + willResizeMenu, withDelay, showResizeHandle, Debug.getCallers(5, " ")); } if (!maybeCreateSyncApplier()) { @@ -344,11 +340,6 @@ public class PhonePipMenuController implements PipMenuController { } else { mApplier.scheduleApply(params); } - - if (mPipMenuView.getViewRootImpl() != null) { - mPipMenuView.getHandler().removeCallbacks(mUpdateEmbeddedMatrix); - mPipMenuView.getHandler().post(mUpdateEmbeddedMatrix); - } } /** @@ -382,13 +373,13 @@ public class PhonePipMenuController implements PipMenuController { private boolean maybeCreateSyncApplier() { if (mPipMenuView == null || mPipMenuView.getViewRootImpl() == null) { - Log.v(TAG, "Not going to move PiP, either menu or its parent is not created."); + 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); - mPipMenuInputToken = mPipMenuView.getViewRootImpl().getInputToken(); } return mApplier != null; @@ -400,7 +391,8 @@ public class PhonePipMenuController implements PipMenuController { public void pokeMenu() { final boolean isMenuVisible = isMenuVisible(); if (DEBUG) { - Log.d(TAG, "pokeMenu() isMenuVisible=" + isMenuVisible); + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: pokeMenu() isMenuVisible=%b", TAG, isMenuVisible); } if (isMenuVisible) { mPipMenuView.pokeMenu(); @@ -410,7 +402,8 @@ public class PhonePipMenuController implements PipMenuController { private void fadeOutMenu() { final boolean isMenuVisible = isMenuVisible(); if (DEBUG) { - Log.d(TAG, "fadeOutMenu() isMenuVisible=" + isMenuVisible); + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: fadeOutMenu() isMenuVisible=%b", TAG, isMenuVisible); } if (isMenuVisible) { mPipMenuView.fadeOutMenu(); @@ -436,11 +429,14 @@ public class PhonePipMenuController implements PipMenuController { public void hideMenu(@PipMenuView.AnimationType int animationType, boolean resize) { final boolean isMenuVisible = isMenuVisible(); if (DEBUG) { - Log.d(TAG, "hideMenu() state=" + mMenuState - + " isMenuVisible=" + isMenuVisible - + " animationType=" + animationType - + " resize=" + resize - + " callers=\n" + Debug.getCallers(5, " ")); + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: hideMenu() state=%s" + + " isMenuVisible=%s" + + " animationType=%s" + + " resize=%s" + + " callers=\n%s", TAG, mMenuState, isMenuVisible, + animationType, resize, + Debug.getCallers(5, " ")); } if (isMenuVisible) { mPipMenuView.hideMenu(resize, animationType); @@ -465,8 +461,10 @@ public class PhonePipMenuController implements PipMenuController { * Sets the menu actions to the actions provided by the current PiP menu. */ @Override - public void setAppActions(ParceledListSlice<RemoteAction> appActions) { + public void setAppActions(List<RemoteAction> appActions, + RemoteAction closeAction) { mAppActions = appActions; + mCloseAction = closeAction; updateMenuActions(); } @@ -485,7 +483,7 @@ public class PhonePipMenuController implements PipMenuController { /** * @return the best set of actions to show in the PiP menu. */ - private ParceledListSlice<RemoteAction> resolveMenuActions() { + private List<RemoteAction> resolveMenuActions() { if (isValidActions(mAppActions)) { return mAppActions; } @@ -497,18 +495,16 @@ public class PhonePipMenuController implements PipMenuController { */ private void updateMenuActions() { if (mPipMenuView != null) { - final ParceledListSlice<RemoteAction> menuActions = resolveMenuActions(); - if (menuActions != null) { - mPipMenuView.setActions(mPipBoundsState.getBounds(), menuActions.getList()); - } + mPipMenuView.setActions(mPipBoundsState.getBounds(), + resolveMenuActions(), mCloseAction); } } /** * Returns whether the set of actions are valid. */ - private static boolean isValidActions(ParceledListSlice<?> actions) { - return actions != null && actions.getList().size() > 0; + private static boolean isValidActions(List<?> actions) { + return actions != null && actions.size() > 0; } /** @@ -516,9 +512,11 @@ public class PhonePipMenuController implements PipMenuController { */ void onMenuStateChangeStart(int menuState, boolean resize, Runnable callback) { if (DEBUG) { - Log.d(TAG, "onMenuStateChangeStart() mMenuState=" + mMenuState - + " menuState=" + menuState + " resize=" + resize - + " callers=\n" + Debug.getCallers(5, " ")); + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: onMenuStateChangeStart() mMenuState=%s" + + " menuState=%s resize=%s" + + " callers=\n%s", TAG, mMenuState, menuState, resize, + Debug.getCallers(5, " ")); } if (menuState != mMenuState) { @@ -535,9 +533,11 @@ public class PhonePipMenuController implements PipMenuController { try { WindowManagerGlobal.getWindowSession().grantEmbeddedWindowFocus(null /* window */, - mPipMenuInputToken, menuState != MENU_STATE_NONE /* grantFocus */); + mSystemWindows.getFocusGrantToken(mPipMenuView), + menuState != MENU_STATE_NONE /* grantFocus */); } catch (RemoteException e) { - Log.e(TAG, "Unable to update focus as menu appears/disappears", e); + ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: Unable to update focus as menu appears/disappears, %s", TAG, e); } } } @@ -583,9 +583,11 @@ public class PhonePipMenuController implements PipMenuController { public void updateMenuLayout(Rect bounds) { final boolean isMenuVisible = isMenuVisible(); if (DEBUG) { - Log.d(TAG, "updateMenuLayout() state=" + mMenuState - + " isMenuVisible=" + isMenuVisible - + " callers=\n" + Debug.getCallers(5, " ")); + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: updateMenuLayout() state=%s" + + " isMenuVisible=%s" + + " callers=\n%s", TAG, mMenuState, isMenuVisible, + Debug.getCallers(5, " ")); } if (isMenuVisible) { mPipMenuView.updateMenuLayout(bounds); 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 69ae45d12795..7365b9525919 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 @@ -285,7 +285,7 @@ public class PipAccessibilityInteractionConnection { Region bounds, int interactionId, IAccessibilityInteractionConnectionCallback callback, int flags, int interrogatingPid, long interrogatingTid, MagnificationSpec spec, - Bundle arguments) throws RemoteException { + float[] matrixValues, Bundle arguments) throws RemoteException { mMainExcutor.execute(() -> { PipAccessibilityInteractionConnection.this .findAccessibilityNodeInfoByAccessibilityId(accessibilityNodeId, bounds, @@ -298,7 +298,8 @@ public class PipAccessibilityInteractionConnection { public void findAccessibilityNodeInfosByViewId(long accessibilityNodeId, String viewId, Region bounds, int interactionId, IAccessibilityInteractionConnectionCallback callback, int flags, - int interrogatingPid, long interrogatingTid, MagnificationSpec spec) + int interrogatingPid, long interrogatingTid, MagnificationSpec spec, + float[] matrixValues) throws RemoteException { mMainExcutor.execute(() -> { PipAccessibilityInteractionConnection.this.findAccessibilityNodeInfosByViewId( @@ -311,7 +312,8 @@ public class PipAccessibilityInteractionConnection { public void findAccessibilityNodeInfosByText(long accessibilityNodeId, String text, Region bounds, int interactionId, IAccessibilityInteractionConnectionCallback callback, int flags, - int interrogatingPid, long interrogatingTid, MagnificationSpec spec) + int interrogatingPid, long interrogatingTid, MagnificationSpec spec, + float[] matrixValues) throws RemoteException { mMainExcutor.execute(() -> { PipAccessibilityInteractionConnection.this.findAccessibilityNodeInfosByText( @@ -323,7 +325,8 @@ public class PipAccessibilityInteractionConnection { @Override public void findFocus(long accessibilityNodeId, int focusType, Region bounds, int interactionId, IAccessibilityInteractionConnectionCallback callback, int flags, - int interrogatingPid, long interrogatingTid, MagnificationSpec spec) + int interrogatingPid, long interrogatingTid, MagnificationSpec spec, + float[] matrixValues) throws RemoteException { mMainExcutor.execute(() -> { PipAccessibilityInteractionConnection.this.findFocus(accessibilityNodeId, focusType, @@ -335,7 +338,8 @@ public class PipAccessibilityInteractionConnection { @Override public void focusSearch(long accessibilityNodeId, int direction, Region bounds, int interactionId, IAccessibilityInteractionConnectionCallback callback, int flags, - int interrogatingPid, long interrogatingTid, MagnificationSpec spec) + int interrogatingPid, long interrogatingTid, MagnificationSpec spec, + float[] matrixValues) throws RemoteException { mMainExcutor.execute(() -> { PipAccessibilityInteractionConnection.this.focusSearch(accessibilityNodeId, 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 a41fd8429e35..dad261ad9580 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 @@ -40,16 +40,13 @@ import android.app.RemoteAction; import android.content.ComponentName; import android.content.Context; import android.content.pm.ActivityInfo; -import android.content.pm.ParceledListSlice; import android.content.res.Configuration; import android.graphics.Rect; import android.os.RemoteException; import android.os.UserHandle; import android.os.UserManager; -import android.util.Log; import android.util.Pair; import android.util.Size; -import android.util.Slog; import android.view.DisplayInfo; import android.view.SurfaceControl; import android.view.WindowManagerGlobal; @@ -61,6 +58,7 @@ import androidx.annotation.Nullable; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.jank.InteractionJankMonitor; +import com.android.internal.protolog.common.ProtoLog; import com.android.wm.shell.R; import com.android.wm.shell.WindowManagerShellWrapper; import com.android.wm.shell.common.DisplayChangeController; @@ -78,17 +76,23 @@ import com.android.wm.shell.pip.IPipAnimationListener; import com.android.wm.shell.pip.PinnedStackListenerForwarder; import com.android.wm.shell.pip.Pip; import com.android.wm.shell.pip.PipAnimationController; +import com.android.wm.shell.pip.PipAppOpsListener; import com.android.wm.shell.pip.PipBoundsAlgorithm; import com.android.wm.shell.pip.PipBoundsState; 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.PipUtils; +import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.transition.Transitions; import java.io.PrintWriter; +import java.util.List; +import java.util.Objects; import java.util.Optional; +import java.util.Set; import java.util.function.Consumer; /** @@ -110,10 +114,12 @@ public class PipController implements PipTransitionController.PipTransitionCallb private PipTouchHandler mTouchHandler; private PipTransitionController mPipTransitionController; private TaskStackListenerImpl mTaskStackListener; + private PipParamsChangedForwarder mPipParamsChangedForwarder; private Optional<OneHandedController> mOneHandedController; protected final PipImpl mImpl; private final Rect mTmpInsetBounds = new Rect(); + private final int mEnterAnimationDuration; private boolean mIsInFixedRotation; private PipAnimationListener mPinnedStackAnimationRecentsCallback; @@ -123,6 +129,8 @@ public class PipController implements PipTransitionController.PipTransitionCallb protected PinnedStackListenerForwarder.PinnedTaskListener mPinnedTaskListener = new PipControllerPinnedTaskListener(); + private boolean mIsKeyguardShowingOrAnimating; + private interface PipAnimationListener { /** * Notifies the listener that the Pip animation is started. @@ -130,12 +138,18 @@ public class PipController implements PipTransitionController.PipTransitionCallb void onPipAnimationStarted(); /** - * Notifies the listener about PiP round corner radius changes. + * Notifies the listener about PiP resource dimensions changed. * Listener can expect an immediate callback the first time they attach. * * @param cornerRadius the pixel value of the corner radius, zero means it's disabled. + * @param shadowRadius the pixel value of the shadow radius, zero means it's disabled. + */ + void onPipResourceDimensionsChanged(int cornerRadius, int shadowRadius); + + /** + * Notifies the listener that user leaves PiP by tapping on the expand button. */ - void onPipCornerRadiusChanged(int cornerRadius); + void onExpandPip(); } /** @@ -143,6 +157,9 @@ public class PipController implements PipTransitionController.PipTransitionCallb */ private final DisplayChangeController.OnDisplayChangingListener mRotationController = ( int displayId, int fromRotation, int toRotation, WindowContainerTransaction t) -> { + if (mPipTransitionController.handleRotateDisplay(fromRotation, toRotation, t)) { + return; + } if (mPipBoundsState.getDisplayLayout().rotation() == toRotation) { // The same rotation may have been set by auto PiP-able or fixed rotation. So notify // the change with fromRotation=false to apply the rotated destination bounds from @@ -225,6 +242,14 @@ public class PipController implements PipTransitionController.PipTransitionCallb onDisplayChanged(mDisplayController.getDisplayLayout(displayId), true /* saveRestoreSnapFraction */); } + + @Override + public void onKeepClearAreasChanged(int displayId, Set<Rect> restricted, + Set<Rect> unrestricted) { + if (mPipBoundsState.getDisplayId() == displayId) { + mPipBoundsState.setKeepClearAreas(restricted, unrestricted); + } + } }; /** @@ -246,11 +271,6 @@ public class PipController implements PipTransitionController.PipTransitionCallb } @Override - public void onActionsChanged(ParceledListSlice<RemoteAction> actions) { - mMenuController.setAppActions(actions); - } - - @Override public void onActivityHidden(ComponentName componentName) { if (componentName.equals(mPipBoundsState.getLastPipComponentName())) { // The activity was removed, we don't want to restore to the reentry state @@ -258,14 +278,6 @@ public class PipController implements PipTransitionController.PipTransitionCallb mPipBoundsState.setLastPipComponentName(null); } } - - @Override - public void onAspectRatioChanged(float aspectRatio) { - // TODO(b/169373982): Remove this callback as it is redundant with PipTaskOrg params - // change. - mPipBoundsState.setAspectRatio(aspectRatio); - mTouchHandler.onAspectRatioChanged(); - } } /** @@ -279,17 +291,20 @@ public class PipController implements PipTransitionController.PipTransitionCallb PipTouchHandler pipTouchHandler, PipTransitionController pipTransitionController, WindowManagerShellWrapper windowManagerShellWrapper, TaskStackListenerImpl taskStackListener, + PipParamsChangedForwarder pipParamsChangedForwarder, Optional<OneHandedController> oneHandedController, ShellExecutor mainExecutor) { if (!context.getPackageManager().hasSystemFeature(FEATURE_PICTURE_IN_PICTURE)) { - Slog.w(TAG, "Device doesn't support Pip feature"); + ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: Device doesn't support Pip feature", TAG); return null; } return new PipController(context, displayController, pipAppOpsListener, pipBoundsAlgorithm, - pipBoundsState, pipMediaController, phonePipMenuController, pipTaskOrganizer, - pipTouchHandler, pipTransitionController, windowManagerShellWrapper, - taskStackListener, oneHandedController, mainExecutor) + pipBoundsState, pipMediaController, + phonePipMenuController, pipTaskOrganizer, pipTouchHandler, pipTransitionController, + windowManagerShellWrapper, taskStackListener, pipParamsChangedForwarder, + oneHandedController, mainExecutor) .mImpl; } @@ -305,11 +320,12 @@ public class PipController implements PipTransitionController.PipTransitionCallb PipTransitionController pipTransitionController, WindowManagerShellWrapper windowManagerShellWrapper, TaskStackListenerImpl taskStackListener, + PipParamsChangedForwarder pipParamsChangedForwarder, Optional<OneHandedController> oneHandedController, ShellExecutor mainExecutor ) { // Ensure that we are the primary user's SystemUI. - final int processUser = UserManager.get(context).getUserHandle(); + final int processUser = UserManager.get(context).getProcessUserId(); if (processUser != UserHandle.USER_SYSTEM) { throw new IllegalStateException("Non-primary Pip component not currently supported."); } @@ -329,6 +345,11 @@ public class PipController implements PipTransitionController.PipTransitionCallb mOneHandedController = oneHandedController; mPipTransitionController = pipTransitionController; mTaskStackListener = taskStackListener; + + mEnterAnimationDuration = mContext.getResources() + .getInteger(R.integer.config_pipEnterAnimationDuration); + mPipParamsChangedForwarder = pipParamsChangedForwarder; + //TODO: move this to ShellInit when PipController can be injected mMainExecutor.execute(this::init); } @@ -375,7 +396,8 @@ public class PipController implements PipTransitionController.PipTransitionCallb try { mWindowManagerShellWrapper.addPinnedStackListener(mPinnedTaskListener); } catch (RemoteException e) { - Slog.e(TAG, "Failed to register pinned stack listener", e); + ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: Failed to register pinned stack listener, %s", TAG, e); } try { @@ -387,7 +409,8 @@ public class PipController implements PipTransitionController.PipTransitionCallb mPipInputConsumer.registerInputConsumer(); } } catch (RemoteException | UnsupportedOperationException e) { - Log.e(TAG, "Failed to register pinned stack listener", e); + ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: Failed to register pinned stack listener, %s", TAG, e); e.printStackTrace(); } @@ -424,6 +447,34 @@ public class PipController implements PipTransitionController.PipTransitionCallb } }); + mPipParamsChangedForwarder.addListener( + new PipParamsChangedForwarder.PipParamsChangedCallback() { + @Override + public void onAspectRatioChanged(float ratio) { + mPipBoundsState.setAspectRatio(ratio); + + final Rect destinationBounds = + mPipBoundsAlgorithm.getAdjustedDestinationBounds( + 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 */); + } + + @Override + public void onActionsChanged(List<RemoteAction> actions, + RemoteAction closeAction) { + mMenuController.setAppActions(actions, closeAction); + } + }); + mOneHandedController.ifPresent(controller -> { controller.asOneHanded().registerTransitionCallback( new OneHandedTransitionCallback() { @@ -458,7 +509,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb private void onDensityOrFontScaleChanged() { mPipTaskOrganizer.onDensityOrFontScaleChanged(mContext); - onPipCornerRadiusChanged(); + onPipResourceDimensionsChanged(); } private void onOverlayChanged() { @@ -488,6 +539,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb }; if (mPipTaskOrganizer.isInPip() && saveRestoreSnapFraction) { + 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()); @@ -543,6 +595,33 @@ public class PipController implements PipTransitionController.PipTransitionCallb } /** + * If {@param keyguardShowing} is {@code false} and {@param animating} is {@code true}, + * we would wait till the dismissing animation of keyguard and surfaces behind to be + * finished first to reset the visibility of PiP window. + * See also {@link #onKeyguardDismissAnimationFinished()} + */ + private void onKeyguardVisibilityChanged(boolean keyguardShowing, boolean animating) { + if (!mPipTaskOrganizer.isInPip()) { + return; + } + if (keyguardShowing) { + mIsKeyguardShowingOrAnimating = true; + hidePipMenu(null /* onStartCallback */, null /* onEndCallback */); + mPipTaskOrganizer.setPipVisibility(false); + } else if (!animating) { + mIsKeyguardShowingOrAnimating = false; + mPipTaskOrganizer.setPipVisibility(true); + } + } + + private void onKeyguardDismissAnimationFinished() { + if (mPipTaskOrganizer.isInPip()) { + mIsKeyguardShowingOrAnimating = false; + mPipTaskOrganizer.setPipVisibility(true); + } + } + + /** * Sets a customized touch gesture that replaces the default one. */ public void setTouchGesture(PipTouchGesture gesture) { @@ -553,7 +632,9 @@ public class PipController implements PipTransitionController.PipTransitionCallb * Sets both shelf visibility and its height. */ private void setShelfHeight(boolean visible, int height) { - setShelfHeightLocked(visible, height); + if (!mIsKeyguardShowingOrAnimating) { + setShelfHeightLocked(visible, height); + } } private void setShelfHeightLocked(boolean visible, int height) { @@ -569,14 +650,14 @@ public class PipController implements PipTransitionController.PipTransitionCallb private void setPinnedStackAnimationListener(PipAnimationListener callback) { mPinnedStackAnimationRecentsCallback = callback; - onPipCornerRadiusChanged(); + onPipResourceDimensionsChanged(); } - private void onPipCornerRadiusChanged() { + private void onPipResourceDimensionsChanged() { if (mPinnedStackAnimationRecentsCallback != null) { - final int cornerRadius = - mContext.getResources().getDimensionPixelSize(R.dimen.pip_corner_radius); - mPinnedStackAnimationRecentsCallback.onPipCornerRadiusChanged(cornerRadius); + mPinnedStackAnimationRecentsCallback.onPipResourceDimensionsChanged( + mContext.getResources().getDimensionPixelSize(R.dimen.pip_corner_radius), + mContext.getResources().getDimensionPixelSize(R.dimen.pip_shadow_radius)); } } @@ -592,9 +673,9 @@ public class PipController implements PipTransitionController.PipTransitionCallb return entryBounds; } - private void stopSwipePipToHome(ComponentName componentName, Rect destinationBounds, + private void stopSwipePipToHome(int taskId, ComponentName componentName, Rect destinationBounds, SurfaceControl overlay) { - mPipTaskOrganizer.stopSwipePipToHome(componentName, destinationBounds, overlay); + mPipTaskOrganizer.stopSwipePipToHome(taskId, componentName, destinationBounds, overlay); } private String getTransitionTag(int direction) { @@ -636,6 +717,9 @@ public class PipController implements PipTransitionController.PipTransitionCallb mTouchHandler.setTouchEnabled(false); if (mPinnedStackAnimationRecentsCallback != null) { mPinnedStackAnimationRecentsCallback.onPipAnimationStarted(); + if (direction == TRANSITION_DIRECTION_LEAVE_PIP) { + mPinnedStackAnimationRecentsCallback.onExpandPip(); + } } } @@ -724,7 +808,8 @@ public class PipController implements PipTransitionController.PipTransitionCallb .getRootTaskInfo(WINDOWING_MODE_PINNED, ACTIVITY_TYPE_UNDEFINED); if (pinnedTaskInfo == null) return false; } catch (RemoteException e) { - Log.e(TAG, "Failed to get RootTaskInfo for pinned task", e); + ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: Failed to get RootTaskInfo for pinned task, %s", TAG, e); return false; } final PipSnapAlgorithm pipSnapAlgorithm = mPipBoundsAlgorithm.getSnapAlgorithm(); @@ -780,13 +865,6 @@ public class PipController implements PipTransitionController.PipTransitionCallb } @Override - public void hidePipMenu(Runnable onStartCallback, Runnable onEndCallback) { - mMainExecutor.execute(() -> { - PipController.this.hidePipMenu(onStartCallback, onEndCallback); - }); - } - - @Override public void expandPip() { mMainExecutor.execute(() -> { PipController.this.expandPip(); @@ -864,13 +942,26 @@ public class PipController implements PipTransitionController.PipTransitionCallb } @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) { - Slog.e(TAG, "Failed to dump PipController in 2s"); + ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: Failed to dump PipController in 2s", TAG); } } } @@ -890,8 +981,13 @@ public class PipController implements PipTransitionController.PipTransitionCallb } @Override - public void onPipCornerRadiusChanged(int cornerRadius) { - mListener.call(l -> l.onPipCornerRadiusChanged(cornerRadius)); + public void onPipResourceDimensionsChanged(int cornerRadius, int shadowRadius) { + mListener.call(l -> l.onPipResourceDimensionsChanged(cornerRadius, shadowRadius)); + } + + @Override + public void onExpandPip() { + mListener.call(l -> l.onExpandPip()); } }; @@ -923,11 +1019,12 @@ public class PipController implements PipTransitionController.PipTransitionCallb } @Override - public void stopSwipePipToHome(ComponentName componentName, Rect destinationBounds, - SurfaceControl overlay) { + public void stopSwipePipToHome(int taskId, ComponentName componentName, + Rect destinationBounds, SurfaceControl overlay) { executeRemoteCallWithTaskPermission(mController, "stopSwipePipToHome", (controller) -> { - controller.stopSwipePipToHome(componentName, destinationBounds, overlay); + controller.stopSwipePipToHome(taskId, componentName, destinationBounds, + overlay); }); } 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 915c5939c34b..a0e22011b5d0 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 @@ -20,27 +20,20 @@ import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_M import android.content.Context; import android.content.res.Resources; -import android.graphics.Insets; import android.graphics.PixelFormat; import android.graphics.Point; import android.graphics.Rect; -import android.graphics.drawable.TransitionDrawable; -import android.view.Gravity; import android.view.MotionEvent; import android.view.SurfaceControl; import android.view.View; -import android.view.ViewGroup; import android.view.ViewTreeObserver; import android.view.WindowInsets; import android.view.WindowManager; -import android.widget.FrameLayout; import androidx.annotation.NonNull; -import androidx.dynamicanimation.animation.DynamicAnimation; -import androidx.dynamicanimation.animation.SpringForce; import com.android.wm.shell.R; -import com.android.wm.shell.animation.PhysicsAnimator; +import com.android.wm.shell.bubbles.DismissView; import com.android.wm.shell.common.DismissCircleView; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.magnetictarget.MagnetizedObject; @@ -56,9 +49,6 @@ public class PipDismissTargetHandler implements ViewTreeObserver.OnPreDrawListen /* The multiplier to apply scale the target size by when applying the magnetic field radius */ private static final float MAGNETIC_FIELD_RADIUS_MULTIPLIER = 1.25f; - /** Duration of the dismiss scrim fading in/out. */ - private static final int DISMISS_TRANSITION_DURATION_MS = 200; - /** * MagnetizedObject wrapper for PIP. This allows the magnetic target library to locate and move * PIP. @@ -69,7 +59,7 @@ public class PipDismissTargetHandler implements ViewTreeObserver.OnPreDrawListen * Container for the dismiss circle, so that it can be animated within the container via * translation rather than within the WindowManager via slow layout animations. */ - private ViewGroup mTargetViewContainer; + private DismissView mTargetViewContainer; /** Circle view used to render the dismiss target. */ private DismissCircleView mTargetView; @@ -79,16 +69,6 @@ public class PipDismissTargetHandler implements ViewTreeObserver.OnPreDrawListen */ private MagnetizedObject.MagneticTarget mMagneticTarget; - /** - * PhysicsAnimator instance for animating the dismiss target in/out. - */ - private PhysicsAnimator<View> mMagneticTargetAnimator; - - /** Default configuration to use for springing the dismiss target in/out. */ - private final PhysicsAnimator.SpringConfig mTargetSpringConfig = - new PhysicsAnimator.SpringConfig( - SpringForce.STIFFNESS_LOW, SpringForce.DAMPING_RATIO_LOW_BOUNCY); - // Allow dragging the PIP to a location to close it private boolean mEnableDismissDragToEdge; @@ -125,12 +105,8 @@ public class PipDismissTargetHandler implements ViewTreeObserver.OnPreDrawListen cleanUpDismissTarget(); } - mTargetView = new DismissCircleView(mContext); - mTargetViewContainer = new FrameLayout(mContext); - mTargetViewContainer.setBackgroundDrawable( - mContext.getDrawable(R.drawable.floating_dismiss_gradient_transition)); - mTargetViewContainer.setClipChildren(false); - mTargetViewContainer.addView(mTargetView); + mTargetViewContainer = new DismissView(mContext); + mTargetView = mTargetViewContainer.getCircle(); mTargetViewContainer.setOnApplyWindowInsetsListener((view, windowInsets) -> { if (!windowInsets.equals(mWindowInsets)) { mWindowInsets = windowInsets; @@ -187,7 +163,6 @@ public class PipDismissTargetHandler implements ViewTreeObserver.OnPreDrawListen } }); - mMagneticTargetAnimator = PhysicsAnimator.getInstance(mTargetView); } @Override @@ -213,19 +188,13 @@ public class PipDismissTargetHandler implements ViewTreeObserver.OnPreDrawListen if (mTargetView == null) { return; } + if (mTargetViewContainer != null) { + mTargetViewContainer.updateResources(); + } final Resources res = mContext.getResources(); mTargetSize = res.getDimensionPixelSize(R.dimen.dismiss_circle_size); mDismissAreaHeight = res.getDimensionPixelSize(R.dimen.floating_dismiss_gradient_height); - final WindowInsets insets = mWindowManager.getCurrentWindowMetrics().getWindowInsets(); - final Insets navInset = insets.getInsetsIgnoringVisibility( - WindowInsets.Type.navigationBars()); - final FrameLayout.LayoutParams newParams = - new FrameLayout.LayoutParams(mTargetSize, mTargetSize); - newParams.gravity = Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM; - newParams.bottomMargin = navInset.bottom + mContext.getResources().getDimensionPixelSize( - R.dimen.floating_dismiss_bottom_margin); - mTargetView.setLayoutParams(newParams); // Set the magnetic field radius equal to the target size from the center of the target setMagneticFieldRadiusPercent(mMagneticFieldRadiusPercent); @@ -251,17 +220,23 @@ public class PipDismissTargetHandler implements ViewTreeObserver.OnPreDrawListen return; } + final SurfaceControl targetViewLeash = + mTargetViewContainer.getViewRootImpl().getSurfaceControl(); + if (!targetViewLeash.isValid()) { + // The surface of mTargetViewContainer is somehow not ready, bail early + return; + } + // Put the dismiss target behind the task SurfaceControl.Transaction t = new SurfaceControl.Transaction(); - t.setRelativeLayer(mTargetViewContainer.getViewRootImpl().getSurfaceControl(), - mTaskLeash, -1); + t.setRelativeLayer(targetViewLeash, mTaskLeash, -1); t.apply(); } /** Adds the magnetic target view to the WindowManager so it's ready to be animated in. */ public void createOrUpdateDismissTarget() { if (!mTargetViewContainer.isAttachedToWindow()) { - mMagneticTargetAnimator.cancel(); + mTargetViewContainer.cancelAnimators(); mTargetViewContainer.setVisibility(View.INVISIBLE); mTargetViewContainer.getViewTreeObserver().removeOnPreDrawListener(this); @@ -284,11 +259,11 @@ public class PipDismissTargetHandler implements ViewTreeObserver.OnPreDrawListen private WindowManager.LayoutParams getDismissTargetLayoutParams() { final Point windowSize = new Point(); mWindowManager.getDefaultDisplay().getRealSize(windowSize); - + int height = Math.min(windowSize.y, mDismissAreaHeight); final WindowManager.LayoutParams lp = new WindowManager.LayoutParams( WindowManager.LayoutParams.MATCH_PARENT, - mDismissAreaHeight, - 0, windowSize.y - mDismissAreaHeight, + height, + 0, windowSize.y - height, WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL, WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE @@ -312,18 +287,8 @@ public class PipDismissTargetHandler implements ViewTreeObserver.OnPreDrawListen createOrUpdateDismissTarget(); if (mTargetViewContainer.getVisibility() != View.VISIBLE) { - mTargetView.setTranslationY(mTargetViewContainer.getHeight()); - mTargetViewContainer.setVisibility(View.VISIBLE); mTargetViewContainer.getViewTreeObserver().addOnPreDrawListener(this); - - // Cancel in case we were in the middle of animating it out. - mMagneticTargetAnimator.cancel(); - mMagneticTargetAnimator - .spring(DynamicAnimation.TRANSLATION_Y, 0f, mTargetSpringConfig) - .start(); - - ((TransitionDrawable) mTargetViewContainer.getBackground()).startTransition( - DISMISS_TRANSITION_DURATION_MS); + mTargetViewContainer.show(); } } @@ -332,16 +297,7 @@ public class PipDismissTargetHandler implements ViewTreeObserver.OnPreDrawListen if (!mEnableDismissDragToEdge) { return; } - - mMagneticTargetAnimator - .spring(DynamicAnimation.TRANSLATION_Y, - mTargetViewContainer.getHeight(), - mTargetSpringConfig) - .withEndActions(() -> mTargetViewContainer.setVisibility(View.GONE)) - .start(); - - ((TransitionDrawable) mTargetViewContainer.getBackground()).reverseTransition( - DISMISS_TRANSITION_DURATION_MS); + mTargetViewContainer.hide(); } /** 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 6e3a20d5f2b2..0f3ff36601fb 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 @@ -22,14 +22,15 @@ import android.os.Binder; import android.os.IBinder; import android.os.Looper; import android.os.RemoteException; -import android.util.Log; import android.view.BatchedInputEventReceiver; import android.view.Choreographer; import android.view.IWindowManager; import android.view.InputChannel; import android.view.InputEvent; +import com.android.internal.protolog.common.ProtoLog; import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.protolog.ShellProtoLogGroup; import java.io.PrintWriter; @@ -141,11 +142,13 @@ public class PipInputConsumer { mWindowManager.destroyInputConsumer(mName, DEFAULT_DISPLAY); mWindowManager.createInputConsumer(mToken, mName, DEFAULT_DISPLAY, inputChannel); } catch (RemoteException e) { - Log.e(TAG, "Failed to create input consumer", e); + ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%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()); if (mRegistrationListener != null) { @@ -165,7 +168,8 @@ public class PipInputConsumer { // TODO(b/113087003): Support Picture-in-picture in multi-display. mWindowManager.destroyInputConsumer(mName, DEFAULT_DISPLAY); } catch (RemoteException e) { - Log.e(TAG, "Failed to destroy input consumer", e); + ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: Failed to destroy input consumer, %s", TAG, e); } mInputEventReceiver.dispose(); mInputEventReceiver = null; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuActionView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuActionView.java index f11ae422e837..7f84500e8406 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuActionView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuActionView.java @@ -19,6 +19,7 @@ package com.android.wm.shell.pip.phone; import android.content.Context; import android.graphics.drawable.Drawable; import android.util.AttributeSet; +import android.view.View; import android.widget.FrameLayout; import android.widget.ImageView; @@ -30,6 +31,7 @@ import com.android.wm.shell.R; */ public class PipMenuActionView extends FrameLayout { private ImageView mImageView; + private View mCustomCloseBackground; public PipMenuActionView(Context context, AttributeSet attrs) { super(context, attrs); @@ -39,10 +41,16 @@ public class PipMenuActionView extends FrameLayout { protected void onFinishInflate() { super.onFinishInflate(); mImageView = findViewById(R.id.image); + mCustomCloseBackground = findViewById(R.id.custom_close_bg); } /** pass through to internal {@link #mImageView} */ public void setImageDrawable(Drawable drawable) { mImageView.setImageDrawable(drawable); } + + /** pass through to internal {@link #mCustomCloseBackground} */ + public void setCustomCloseBackgroundVisibility(@View.Visibility int visibility) { + mCustomCloseBackground.setVisibility(visibility); + } } 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 e1475efcdb57..6390c8984dac 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 @@ -33,8 +33,10 @@ import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.animation.ValueAnimator; import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; import android.app.ActivityManager; -import android.app.PendingIntent.CanceledException; +import android.app.PendingIntent; import android.app.RemoteAction; import android.app.WindowConfiguration; import android.content.ComponentName; @@ -47,7 +49,6 @@ import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.UserHandle; -import android.util.Log; import android.util.Pair; import android.util.Size; import android.view.KeyEvent; @@ -60,16 +61,20 @@ import android.view.accessibility.AccessibilityNodeInfo; import android.widget.FrameLayout; import android.widget.LinearLayout; +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.common.ShellExecutor; +import com.android.wm.shell.pip.PipUiEventLogger; import com.android.wm.shell.pip.PipUtils; +import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.splitscreen.SplitScreenController; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.List; +import java.util.Objects; import java.util.Optional; /** @@ -111,6 +116,7 @@ public class PipMenuView extends FrameLayout { private boolean mFocusedTaskAllowSplitScreen; private final List<RemoteAction> mActions = new ArrayList<>(); + private RemoteAction mCloseAction; private AccessibilityManager mAccessibilityManager; private Drawable mBackgroundDrawable; @@ -119,8 +125,9 @@ public class PipMenuView extends FrameLayout { private int mBetweenActionPaddingLand; private AnimatorSet mMenuContainerAnimator; - private PhonePipMenuController mController; - private Optional<SplitScreenController> mSplitScreenControllerOptional; + private final PhonePipMenuController mController; + private final Optional<SplitScreenController> mSplitScreenControllerOptional; + private final PipUiEventLogger mPipUiEventLogger; private ValueAnimator.AnimatorUpdateListener mMenuBgUpdateListener = new ValueAnimator.AnimatorUpdateListener() { @@ -148,19 +155,27 @@ public class PipMenuView extends FrameLayout { protected View mTopEndContainer; protected PipMenuIconsAlgorithm mPipMenuIconsAlgorithm; + // How long the shell will wait for the app to close the PiP if a custom action is set. + private final int mPipForceCloseDelay; + public PipMenuView(Context context, PhonePipMenuController controller, ShellExecutor mainExecutor, Handler mainHandler, - Optional<SplitScreenController> splitScreenController) { + Optional<SplitScreenController> splitScreenController, + PipUiEventLogger pipUiEventLogger) { super(context, null, 0); mContext = context; mController = controller; mMainExecutor = mainExecutor; mMainHandler = mainHandler; mSplitScreenControllerOptional = splitScreenController; + mPipUiEventLogger = pipUiEventLogger; mAccessibilityManager = context.getSystemService(AccessibilityManager.class); inflate(context, R.layout.pip_menu, this); + mPipForceCloseDelay = context.getResources().getInteger( + R.integer.config_pipForceCloseDelay); + mBackgroundDrawable = mContext.getDrawable(R.drawable.pip_menu_background); mBackgroundDrawable.setAlpha(0); mViewRoot = findViewById(R.id.background); @@ -419,7 +434,7 @@ public class PipMenuView extends FrameLayout { /** * @return Estimated minimum {@link Size} to hold the actions. - * See also {@link #updateActionViews(Rect)} + * See also {@link #updateActionViews(Rect)} */ Size getEstimatedMinMenuSize() { final int pipActionSize = getResources().getDimensionPixelSize(R.dimen.pip_action_size); @@ -432,9 +447,13 @@ public class PipMenuView extends FrameLayout { return new Size(width, height); } - void setActions(Rect stackBounds, List<RemoteAction> actions) { + void setActions(Rect stackBounds, @Nullable List<RemoteAction> actions, + @Nullable RemoteAction closeAction) { mActions.clear(); - mActions.addAll(actions); + if (actions != null && !actions.isEmpty()) { + mActions.addAll(actions); + } + mCloseAction = closeAction; if (mMenuState == MENU_STATE_FULL) { updateActionViews(mMenuState, stackBounds); } @@ -487,6 +506,8 @@ public class PipMenuView extends FrameLayout { final RemoteAction action = mActions.get(i); final PipMenuActionView actionView = (PipMenuActionView) mActionsGroup.getChildAt(i); + final boolean isCloseAction = mCloseAction != null && Objects.equals( + mCloseAction.getActionIntent(), action.getActionIntent()); // TODO: Check if the action drawable has changed before we reload it action.getIcon().loadDrawableAsync(mContext, d -> { @@ -495,15 +516,12 @@ public class PipMenuView extends FrameLayout { actionView.setImageDrawable(d); } }, mMainHandler); + actionView.setCustomCloseBackgroundVisibility( + isCloseAction ? View.VISIBLE : View.GONE); actionView.setContentDescription(action.getContentDescription()); if (action.isEnabled()) { - actionView.setOnClickListener(v -> { - try { - action.getActionIntent().send(); - } catch (CanceledException e) { - Log.w(TAG, "Failed to send action", e); - } - }); + actionView.setOnClickListener( + v -> onActionViewClicked(action.getActionIntent(), isCloseAction)); } actionView.setEnabled(action.isEnabled()); actionView.setAlpha(action.isEnabled() ? 1f : DISABLED_ACTION_ALPHA); @@ -539,6 +557,8 @@ public class PipMenuView extends FrameLayout { // handles the message hideMenu(mController::onPipExpand, false /* notifyMenuVisibility */, true /* resize */, ANIM_TYPE_HIDE); + mPipUiEventLogger.log( + PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_EXPAND_TO_FULLSCREEN); } private void dismissPip() { @@ -547,6 +567,33 @@ public class PipMenuView extends FrameLayout { // any other dismissal that will update the touch state and fade out the PIP task // and the menu view at the same time. mController.onPipDismiss(); + mPipUiEventLogger.log(PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_TAP_TO_REMOVE); + } + } + + /** + * Execute the {@link PendingIntent} attached to the {@link PipMenuActionView}. + * If the given {@link PendingIntent} matches {@link #mCloseAction}, we need to make sure + * the PiP is removed after a certain timeout in case the app does not respond in a + * timely manner. + */ + private void onActionViewClicked(@NonNull PendingIntent intent, boolean isCloseAction) { + try { + intent.send(); + } catch (PendingIntent.CanceledException e) { + ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: Failed to send action, %s", TAG, e); + } + if (isCloseAction) { + mPipUiEventLogger.log(PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_CUSTOM_CLOSE); + mAllowTouches = false; + mMainExecutor.executeDelayed(() -> { + hideMenu(); + // TODO: it's unsafe to call onPipDismiss with a delay here since + // we may have a different PiP by the time this runnable is executed. + mController.onPipDismiss(); + mAllowTouches = true; + }, mPipForceCloseDelay); } } @@ -566,6 +613,7 @@ public class PipMenuView extends FrameLayout { Uri.fromParts("package", topPipActivityInfo.first.getPackageName(), null)); settingsIntent.setFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TASK); mContext.startActivityAsUser(settingsIntent, UserHandle.of(topPipActivityInfo.second)); + mPipUiEventLogger.log(PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_SHOW_SETTINGS); } } 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 96fd59f0c911..5a21e0734277 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java @@ -34,21 +34,22 @@ import android.graphics.PointF; import android.graphics.Rect; import android.os.Debug; import android.os.Looper; -import android.util.Log; import android.view.Choreographer; -import androidx.dynamicanimation.animation.AnimationHandler; -import androidx.dynamicanimation.animation.AnimationHandler.FrameCallbackScheduler; +import androidx.dynamicanimation.animation.FrameCallbackScheduler; +import com.android.internal.protolog.common.ProtoLog; import com.android.wm.shell.R; import com.android.wm.shell.animation.FloatProperties; import com.android.wm.shell.animation.PhysicsAnimator; import com.android.wm.shell.common.FloatingContentCoordinator; import com.android.wm.shell.common.magnetictarget.MagnetizedObject; +import com.android.wm.shell.pip.PipAppOpsListener; import com.android.wm.shell.pip.PipBoundsState; 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.protolog.ShellProtoLogGroup; import java.util.function.Consumer; @@ -88,12 +89,14 @@ public class PipMotionHelper implements PipAppOpsListener.Callback, /** Coordinator instance for resolving conflicts with other floating content. */ private FloatingContentCoordinator mFloatingContentCoordinator; - private ThreadLocal<AnimationHandler> mSfAnimationHandlerThreadLocal = + 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()); } @@ -102,8 +105,7 @@ public class PipMotionHelper implements PipAppOpsListener.Callback, return Looper.myLooper() == initialLooper; } }; - AnimationHandler handler = new AnimationHandler(scheduler); - return handler; + return scheduler; }); /** @@ -211,8 +213,7 @@ public class PipMotionHelper implements PipAppOpsListener.Callback, // Note: Needs to get the shell main thread sf vsync animation handler mTemporaryBoundsPhysicsAnimator = PhysicsAnimator.getInstance( mPipBoundsState.getMotionBoundsState().getBoundsInMotion()); - mTemporaryBoundsPhysicsAnimator.setCustomAnimationHandler( - mSfAnimationHandlerThreadLocal.get()); + mTemporaryBoundsPhysicsAnimator.setCustomScheduler(mSfSchedulerThreadLocal.get()); } @NonNull @@ -354,8 +355,9 @@ public class PipMotionHelper implements PipAppOpsListener.Callback, */ private void expandLeavePip(boolean skipAnimation, boolean enterSplit) { if (DEBUG) { - Log.d(TAG, "exitPip: skipAnimation=" + skipAnimation - + " callers=\n" + Debug.getCallers(5, " ")); + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: exitPip: skipAnimation=%s" + + " callers=\n%s", TAG, skipAnimation, Debug.getCallers(5, " ")); } cancelPhysicsAnimation(); mMenuController.hideMenu(ANIM_TYPE_NONE, false /* resize */); @@ -368,7 +370,8 @@ public class PipMotionHelper implements PipAppOpsListener.Callback, @Override public void dismissPip() { if (DEBUG) { - Log.d(TAG, "removePip: callers=\n" + Debug.getCallers(5, " ")); + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: removePip: callers=\n%s", TAG, Debug.getCallers(5, " ")); } cancelPhysicsAnimation(); mMenuController.hideMenu(ANIM_TYPE_DISMISS, false /* resize */); @@ -552,8 +555,10 @@ public class PipMotionHelper implements PipAppOpsListener.Callback, */ void animateToOffset(Rect originalBounds, int offset) { if (DEBUG) { - Log.d(TAG, "animateToOffset: originalBounds=" + originalBounds + " offset=" + offset - + " callers=\n" + Debug.getCallers(5, " ")); + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: animateToOffset: originalBounds=%s offset=%s" + + " callers=\n%s", TAG, originalBounds, offset, + Debug.getCallers(5, " ")); } cancelPhysicsAnimation(); mPipTaskOrganizer.scheduleOffsetPip(originalBounds, offset, SHIFT_DURATION, @@ -671,8 +676,9 @@ public class PipMotionHelper implements PipAppOpsListener.Callback, */ private void resizePipUnchecked(Rect toBounds) { if (DEBUG) { - Log.d(TAG, "resizePipUnchecked: toBounds=" + toBounds - + " callers=\n" + Debug.getCallers(5, " ")); + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: resizePipUnchecked: toBounds=%s" + + " callers=\n%s", TAG, toBounds, Debug.getCallers(5, " ")); } if (!toBounds.equals(getBounds())) { mPipTaskOrganizer.scheduleResizePip(toBounds, mUpdateBoundsCallback); @@ -684,8 +690,10 @@ public class PipMotionHelper implements PipAppOpsListener.Callback, */ private void resizeAndAnimatePipUnchecked(Rect toBounds, int duration) { if (DEBUG) { - Log.d(TAG, "resizeAndAnimatePipUnchecked: toBounds=" + toBounds - + " duration=" + duration + " callers=\n" + Debug.getCallers(5, " ")); + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: resizeAndAnimatePipUnchecked: toBounds=%s" + + " duration=%s callers=\n%s", TAG, toBounds, duration, + Debug.getCallers(5, " ")); } // Intentionally resize here even if the current bounds match the destination bounds. 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 c816f18c2fc2..abf1a9500e6d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipResizeGestureHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipResizeGestureHandler.java @@ -625,6 +625,7 @@ public class PipResizeGestureHandler { class PipResizeInputEventReceiver extends BatchedInputEventReceiver { PipResizeInputEventReceiver(InputChannel channel, Looper looper) { + // TODO(b/222697646): remove getSfInstance usage and use vsyncId for transactions super(channel, looper, Choreographer.getSfInstance()); } 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 3ace5f405d36..ac7b9033b2b9 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 @@ -35,8 +35,8 @@ import android.graphics.Point; import android.graphics.PointF; import android.graphics.Rect; import android.provider.DeviceConfig; -import android.util.Log; import android.util.Size; +import android.view.DisplayCutout; import android.view.InputEvent; import android.view.MotionEvent; import android.view.ViewConfiguration; @@ -46,6 +46,7 @@ import android.view.accessibility.AccessibilityNodeInfo; import android.view.accessibility.AccessibilityWindowInfo; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.protolog.common.ProtoLog; import com.android.wm.shell.R; import com.android.wm.shell.common.FloatingContentCoordinator; import com.android.wm.shell.common.ShellExecutor; @@ -54,6 +55,7 @@ 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.PipUiEventLogger; +import com.android.wm.shell.protolog.ShellProtoLogGroup; import java.io.PrintWriter; @@ -149,7 +151,6 @@ public class PipTouchHandler { @Override public void onPipDismiss() { - mPipUiEventLogger.log(PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_TAP_TO_REMOVE); mTouchState.removeDoubleTapTimeoutCallback(); mMotionHelper.dismissPip(); } @@ -959,21 +960,38 @@ public class PipTouchHandler { } private boolean shouldStash(PointF vel, Rect motionBounds) { + final boolean flingToLeft = vel.x < -mStashVelocityThreshold; + final boolean flingToRight = vel.x > mStashVelocityThreshold; + final int offset = motionBounds.width() / 2; + final boolean droppingOnLeft = + motionBounds.left < mPipBoundsState.getDisplayBounds().left - offset; + final boolean droppingOnRight = + motionBounds.right > mPipBoundsState.getDisplayBounds().right + offset; + + // Do not allow stash if the destination edge contains display cutout. We only + // compare the left and right edges since we do not allow stash on top / bottom. + final DisplayCutout displayCutout = + mPipBoundsState.getDisplayLayout().getDisplayCutout(); + if (displayCutout != null) { + if ((flingToLeft || droppingOnLeft) + && !displayCutout.getBoundingRectLeft().isEmpty()) { + return false; + } else if ((flingToRight || droppingOnRight) + && !displayCutout.getBoundingRectRight().isEmpty()) { + return false; + } + } + // If user flings the PIP window above the minimum velocity, stash PIP. // Only allow stashing to the edge if PIP wasn't previously stashed on the opposite // edge. - final boolean stashFromFlingToEdge = ((vel.x < -mStashVelocityThreshold - && mPipBoundsState.getStashedState() != STASH_TYPE_RIGHT) - || (vel.x > mStashVelocityThreshold - && mPipBoundsState.getStashedState() != STASH_TYPE_LEFT)); + final boolean stashFromFlingToEdge = + (flingToLeft && mPipBoundsState.getStashedState() != STASH_TYPE_RIGHT) + || (flingToRight && mPipBoundsState.getStashedState() != STASH_TYPE_LEFT); // If User releases the PIP window while it's out of the display bounds, put // PIP into stashed mode. - final int offset = motionBounds.width() / 2; - final boolean stashFromDroppingOnEdge = - (motionBounds.right > mPipBoundsState.getDisplayBounds().right + offset - || motionBounds.left - < mPipBoundsState.getDisplayBounds().left - offset); + final boolean stashFromDroppingOnEdge = droppingOnLeft || droppingOnRight; return stashFromFlingToEdge || stashFromDroppingOnEdge; } @@ -1011,7 +1029,8 @@ public class PipTouchHandler { } final Size estimatedMinMenuSize = mMenuController.getEstimatedMinMenuSize(); if (estimatedMinMenuSize == null) { - Log.wtf(TAG, "Failed to get estimated menu size"); + ProtoLog.wtf(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: Failed to get estimated menu size", TAG); return false; } final Rect currentBounds = mPipBoundsState.getBounds(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchState.java index 53303ff2b679..d7d69f27f9f8 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchState.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchState.java @@ -17,15 +17,15 @@ package com.android.wm.shell.pip.phone; import android.graphics.PointF; -import android.os.Handler; -import android.util.Log; import android.view.Display; import android.view.MotionEvent; import android.view.VelocityTracker; import android.view.ViewConfiguration; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.protolog.common.ProtoLog; import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.protolog.ShellProtoLogGroup; import java.io.PrintWriter; @@ -104,7 +104,8 @@ public class PipTouchState { mActivePointerId = ev.getPointerId(0); if (DEBUG) { - Log.e(TAG, "Setting active pointer id on DOWN: " + mActivePointerId); + ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: Setting active pointer id on DOWN: %d", TAG, mActivePointerId); } mLastTouch.set(ev.getRawX(), ev.getRawY()); mDownTouch.set(mLastTouch); @@ -131,7 +132,8 @@ public class PipTouchState { addMovementToVelocityTracker(ev); int pointerIndex = ev.findPointerIndex(mActivePointerId); if (pointerIndex == -1) { - Log.e(TAG, "Invalid active pointer id on MOVE: " + mActivePointerId); + ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: Invalid active pointer id on MOVE: %d", TAG, mActivePointerId); break; } @@ -168,8 +170,9 @@ public class PipTouchState { final int newPointerIndex = (pointerIndex == 0) ? 1 : 0; mActivePointerId = ev.getPointerId(newPointerIndex); if (DEBUG) { - Log.e(TAG, - "Relinquish active pointer id on POINTER_UP: " + mActivePointerId); + ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: Relinquish active pointer id on POINTER_UP: %d", + TAG, mActivePointerId); } mLastTouch.set(ev.getRawX(newPointerIndex), ev.getRawY(newPointerIndex)); } @@ -189,7 +192,8 @@ public class PipTouchState { int pointerIndex = ev.findPointerIndex(mActivePointerId); if (pointerIndex == -1) { - Log.e(TAG, "Invalid active pointer id on UP: " + mActivePointerId); + ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: Invalid active pointer id on UP: %d", TAG, mActivePointerId); break; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/CenteredImageSpan.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/CenteredImageSpan.java new file mode 100644 index 000000000000..6efdd57bdc48 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/CenteredImageSpan.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.text.style.ImageSpan; + +/** An ImageSpan for a Drawable that is centered vertically in the line. */ +public class CenteredImageSpan extends ImageSpan { + + private Drawable mDrawable; + + public CenteredImageSpan(Drawable drawable) { + super(drawable); + } + + @Override + public int getSize( + Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fontMetrics) { + final Drawable drawable = getCachedDrawable(); + final Rect rect = drawable.getBounds(); + + if (fontMetrics != null) { + Paint.FontMetricsInt paintFontMetrics = paint.getFontMetricsInt(); + fontMetrics.ascent = paintFontMetrics.ascent; + fontMetrics.descent = paintFontMetrics.descent; + fontMetrics.top = paintFontMetrics.top; + fontMetrics.bottom = paintFontMetrics.bottom; + } + + return rect.right; + } + + @Override + public void draw( + Canvas canvas, + CharSequence text, + int start, + int end, + float x, + int top, + int y, + int bottom, + Paint paint) { + final Drawable drawable = getCachedDrawable(); + canvas.save(); + final int transY = (bottom - drawable.getBounds().bottom) / 2; + canvas.translate(x, transY); + drawable.draw(canvas); + canvas.restore(); + } + + private Drawable getCachedDrawable() { + if (mDrawable == null) { + mDrawable = getDrawable(); + } + return mDrawable; + } +} 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 new file mode 100644 index 000000000000..85441af9a870 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/OWNERS @@ -0,0 +1,2 @@ +# WM shell sub-module TV pip owner +galinap@google.com diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsAlgorithm.java new file mode 100644 index 000000000000..a2eadcdf6210 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsAlgorithm.java @@ -0,0 +1,420 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.pip.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 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; + +import androidx.annotation.NonNull; + +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.PipSnapAlgorithm; +import com.android.wm.shell.pip.tv.TvPipKeepClearAlgorithm.Placement; +import com.android.wm.shell.protolog.ShellProtoLogGroup; + +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; + + private int mFixedExpandedHeightInPx; + private int mFixedExpandedWidthInPx; + + private final TvPipKeepClearAlgorithm mKeepClearAlgorithm; + + public TvPipBoundsAlgorithm(Context context, + @NonNull TvPipBoundsState tvPipBoundsState, + @NonNull PipSnapAlgorithm pipSnapAlgorithm) { + super(context, tvPipBoundsState, pipSnapAlgorithm); + this.mTvPipBoundsState = tvPipBoundsState; + this.mKeepClearAlgorithm = new TvPipKeepClearAlgorithm(); + reloadResources(context); + } + + private void reloadResources(Context context) { + final Resources res = context.getResources(); + mFixedExpandedHeightInPx = res.getDimensionPixelSize( + com.android.internal.R.dimen.config_pictureInPictureExpandedHorizontalHeight); + mFixedExpandedWidthInPx = res.getDimensionPixelSize( + com.android.internal.R.dimen.config_pictureInPictureExpandedVerticalWidth); + mKeepClearAlgorithm.setPipAreaPadding( + res.getDimensionPixelSize(R.dimen.pip_keep_clear_area_padding)); + mKeepClearAlgorithm.setMaxRestrictedDistanceFraction( + res.getFraction(R.fraction.config_pipMaxRestrictedMoveDistance, 1, 1)); + } + + @Override + public void onConfigurationChanged(Context context) { + super.onConfigurationChanged(context); + reloadResources(context); + } + + /** 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); + } + updateExpandedPipSize(); + final boolean isPipExpanded = mTvPipBoundsState.isTvExpandedPipSupported() + && mTvPipBoundsState.getDesiredTvExpandedAspectRatio() != 0 + && !mTvPipBoundsState.isTvPipManuallyCollapsed(); + if (isPipExpanded) { + updateGravityOnExpandToggled(Gravity.NO_GRAVITY, true); + } + mTvPipBoundsState.setTvPipExpanded(isPipExpanded); + return adjustBoundsForTemporaryDecor(getTvPipPlacement().getBounds()); + } + + /** 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); + } + return adjustBoundsForTemporaryDecor(getTvPipPlacement().getBounds()); + } + + Rect adjustBoundsForTemporaryDecor(Rect bounds) { + Rect boundsWithDecor = new Rect(bounds); + Insets decorInset = mTvPipBoundsState.getPipMenuTemporaryDecorInsets(); + Insets pipDecorReverseInsets = Insets.subtract(Insets.NONE, decorInset); + boundsWithDecor.inset(decorInset); + Gravity.apply(mTvPipBoundsState.getTvPipGravity(), + boundsWithDecor.width(), boundsWithDecor.height(), bounds, boundsWithDecor); + + // remove temporary decoration again + boundsWithDecor.inset(pipDecorReverseInsets); + return boundsWithDecor; + } + + /** + * Calculates the PiP bounds. + */ + @NonNull + public Placement getTvPipPlacement() { + final Size pipSize = getPipSize(); + final Rect displayBounds = mTvPipBoundsState.getDisplayBounds(); + final Size screenSize = new Size(displayBounds.width(), displayBounds.height()); + 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); + } + + mKeepClearAlgorithm.setGravity(mTvPipBoundsState.getTvPipGravity()); + mKeepClearAlgorithm.setScreenSize(screenSize); + mKeepClearAlgorithm.setMovementBounds(insetBounds); + mKeepClearAlgorithm.setStashOffset(mTvPipBoundsState.getStashOffset()); + mKeepClearAlgorithm.setPipPermanentDecorInsets( + mTvPipBoundsState.getPipMenuPermanentDecorInsets()); + + final Placement placement = mKeepClearAlgorithm.calculatePipPosition( + pipSize, + 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); + } + + 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; + } + + 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 gravityToSave = Gravity.NO_GRAVITY; + int currentGravity = mTvPipBoundsState.getTvPipGravity(); + int updatedGravity; + + if (expanding) { + // save collapsed gravity + gravityToSave = mTvPipBoundsState.getTvPipGravity(); + + if (mTvPipBoundsState.getTvFixedPipOrientation() == ORIENTATION_HORIZONTAL) { + updatedGravity = + Gravity.CENTER_HORIZONTAL | (currentGravity + & Gravity.VERTICAL_GRAVITY_MASK); + } else { + updatedGravity = + Gravity.CENTER_VERTICAL | (currentGravity + & Gravity.HORIZONTAL_GRAVITY_MASK); + } + } else { + if (previousGravity != Gravity.NO_GRAVITY) { + // The pip hasn't been moved since expanding, + // go back to previous collapsed position. + updatedGravity = previousGravity; + } else { + if (mTvPipBoundsState.getTvFixedPipOrientation() == ORIENTATION_HORIZONTAL) { + updatedGravity = + Gravity.RIGHT | (currentGravity & Gravity.VERTICAL_GRAVITY_MASK); + } else { + updatedGravity = + Gravity.BOTTOM | (currentGravity & Gravity.HORIZONTAL_GRAVITY_MASK); + } + } + } + mTvPipBoundsState.setTvPipGravity(updatedGravity); + if (DEBUG) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: new gravity: %s", TAG, Gravity.toString(updatedGravity)); + } + + return gravityToSave; + } + + /** + * @return true if gravity changed + */ + boolean updateGravity(int keycode) { + if (DEBUG) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: updateGravity, keycode: %d", TAG, keycode); + } + + // Check if position change is valid + if (mTvPipBoundsState.isTvPipExpanded()) { + int mOrientation = mTvPipBoundsState.getTvFixedPipOrientation(); + if (mOrientation == ORIENTATION_VERTICAL + && (keycode == KEYCODE_DPAD_UP || keycode == KEYCODE_DPAD_DOWN) + || mOrientation == ORIENTATION_HORIZONTAL + && (keycode == KEYCODE_DPAD_RIGHT || keycode == KEYCODE_DPAD_LEFT)) { + return false; + } + } + + int currentGravity = mTvPipBoundsState.getTvPipGravity(); + int updatedGravity; + // First axis + switch (keycode) { + case KEYCODE_DPAD_UP: + updatedGravity = Gravity.TOP; + break; + case KEYCODE_DPAD_DOWN: + updatedGravity = Gravity.BOTTOM; + break; + case KEYCODE_DPAD_LEFT: + updatedGravity = Gravity.LEFT; + break; + case KEYCODE_DPAD_RIGHT: + updatedGravity = Gravity.RIGHT; + break; + default: + updatedGravity = currentGravity; + } + + // 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; + } + + if (updatedGravity != currentGravity) { + mTvPipBoundsState.setTvPipGravity(updatedGravity); + if (DEBUG) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: new gravity: %s", TAG, Gravity.toString(updatedGravity)); + } + return true; + } + return false; + } + + private Size getPipSize() { + final boolean isExpanded = + mTvPipBoundsState.isTvExpandedPipSupported() && mTvPipBoundsState.isTvPipExpanded() + && mTvPipBoundsState.getDesiredTvExpandedAspectRatio() != 0; + if (isExpanded) { + return mTvPipBoundsState.getTvExpandedSize(); + } else { + final Rect normalBounds = getNormalBounds(); + return new Size(normalBounds.width(), normalBounds.height()); + } + } + + /** + * Updates {@link TvPipBoundsState#getTvExpandedSize()} based on + * {@link TvPipBoundsState#getDesiredTvExpandedAspectRatio()}, the screen size. + */ + void updateExpandedPipSize() { + final DisplayLayout displayLayout = mTvPipBoundsState.getDisplayLayout(); + final float expandedRatio = + mTvPipBoundsState.getDesiredTvExpandedAspectRatio(); // width / height + final Insets pipDecorations = mTvPipBoundsState.getPipMenuPermanentDecorInsets(); + + 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); + return; + } else if (expandedRatio < 1) { + // vertical + if (mTvPipBoundsState.getTvFixedPipOrientation() == ORIENTATION_HORIZONTAL) { + expandedSize = mTvPipBoundsState.getTvExpandedSize(); + } else { + int maxHeight = displayLayout.height() - (2 * mScreenEdgeInsets.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); + } + 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); + } + expandedSize = new Size(mFixedExpandedWidthInPx, maxHeight); + } + } + } else { + // horizontal + if (mTvPipBoundsState.getTvFixedPipOrientation() == ORIENTATION_VERTICAL) { + expandedSize = mTvPipBoundsState.getTvExpandedSize(); + } else { + int maxWidth = displayLayout.width() - (2 * mScreenEdgeInsets.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); + } + 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); + } + 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()); + } + } +} 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 new file mode 100644 index 000000000000..3a6ce81821ec --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsController.java @@ -0,0 +1,253 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.PipBoundsState.STASH_TYPE_NONE; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Rect; +import android.os.Handler; + +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.tv.TvPipKeepClearAlgorithm.Placement; +import com.android.wm.shell.protolog.ShellProtoLogGroup; + +import java.util.Objects; +import java.util.function.Supplier; + +/** + * Controller managing the PiP's position. + * Manages debouncing of PiP movements and scheduling of unstashing. + */ +public class TvPipBoundsController { + private static final boolean DEBUG = false; + private static final String TAG = "TvPipBoundsController"; + + /** + * Time the calculated PiP position needs to be stable before PiP is moved there, + * to avoid erratic movement. + * Some changes will cause the PiP to be repositioned immediately, such as changes to + * unrestricted keep clear areas. + */ + @VisibleForTesting + static final long POSITION_DEBOUNCE_TIMEOUT_MILLIS = 300L; + + private final Context mContext; + private final Supplier<Long> mClock; + private final Handler mMainHandler; + private final TvPipBoundsState mTvPipBoundsState; + private final TvPipBoundsAlgorithm mTvPipBoundsAlgorithm; + + @Nullable + private PipBoundsListener mListener; + + private int mResizeAnimationDuration; + private int mStashDurationMs; + private Rect mCurrentPlacementBounds; + private Rect mPipTargetBounds; + + private final Runnable mApplyPendingPlacementRunnable = this::applyPendingPlacement; + private boolean mPendingStash; + private Placement mPendingPlacement; + private int mPendingPlacementAnimationDuration; + private Runnable mUnstashRunnable; + + public TvPipBoundsController( + Context context, + Supplier<Long> clock, + Handler mainHandler, + TvPipBoundsState tvPipBoundsState, + TvPipBoundsAlgorithm tvPipBoundsAlgorithm) { + mContext = context; + mClock = clock; + mMainHandler = mainHandler; + mTvPipBoundsState = tvPipBoundsState; + mTvPipBoundsAlgorithm = tvPipBoundsAlgorithm; + + loadConfigurations(); + } + + private void loadConfigurations() { + final Resources res = mContext.getResources(); + mResizeAnimationDuration = res.getInteger(R.integer.config_pipResizeAnimationDuration); + mStashDurationMs = res.getInteger(R.integer.config_pipStashDuration); + } + + void setListener(PipBoundsListener listener) { + mListener = listener; + } + + /** + * Update the PiP bounds based on the state of the PiP, decors, and keep clear areas. + * Unless {@code immediate} is {@code true}, the PiP does not move immediately to avoid + * keep clear areas, but waits for a new position to stay uncontested for + * {@link #POSITION_DEBOUNCE_TIMEOUT_MILLIS} before moving to it. + * Temporary decor changes are applied immediately. + * + * @param stayAtAnchorPosition If true, PiP will be placed at the anchor position + * @param disallowStashing If true, PiP will not be placed off-screen in a stashed position + * @param animationDuration Duration of the animation to the new position + * @param immediate If true, PiP will move immediately to avoid keep clear areas + */ + @VisibleForTesting + void recalculatePipBounds(boolean stayAtAnchorPosition, boolean disallowStashing, + int animationDuration, boolean immediate) { + final Placement placement = mTvPipBoundsAlgorithm.getTvPipPlacement(); + + final int stashType = disallowStashing ? STASH_TYPE_NONE : placement.getStashType(); + mTvPipBoundsState.setStashed(stashType); + if (stayAtAnchorPosition) { + cancelScheduledPlacement(); + applyPlacementBounds(placement.getAnchorBounds(), animationDuration); + } else if (disallowStashing) { + cancelScheduledPlacement(); + applyPlacementBounds(placement.getUnstashedBounds(), animationDuration); + } else if (immediate) { + cancelScheduledPlacement(); + applyPlacementBounds(placement.getBounds(), animationDuration); + scheduleUnstashIfNeeded(placement); + } else { + 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()); + } + + if (mPendingPlacement != null && Objects.equals(mPendingPlacement.getBounds(), + placement.getBounds())) { + mPendingStash = mPendingStash || placement.getTriggerStash(); + return; + } + + mPendingStash = placement.getStashType() != STASH_TYPE_NONE + && (mPendingStash || placement.getTriggerStash()); + + mMainHandler.removeCallbacks(mApplyPendingPlacementRunnable); + mPendingPlacement = placement; + mPendingPlacementAnimationDuration = animationDuration; + mMainHandler.postAtTime(mApplyPendingPlacementRunnable, + mClock.get() + POSITION_DEBOUNCE_TIMEOUT_MILLIS); + } + + private void scheduleUnstashIfNeeded(final Placement placement) { + if (mUnstashRunnable != null) { + mMainHandler.removeCallbacks(mUnstashRunnable); + mUnstashRunnable = null; + } + if (placement.getUnstashDestinationBounds() != null) { + mUnstashRunnable = () -> { + applyPlacementBounds(placement.getUnstashDestinationBounds(), + mResizeAnimationDuration); + mUnstashRunnable = null; + }; + mMainHandler.postAtTime(mUnstashRunnable, mClock.get() + mStashDurationMs); + } + } + + private void applyPendingPlacement() { + if (DEBUG) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: applyPendingPlacement()", TAG); + } + if (mPendingPlacement != null) { + if (mPendingStash) { + mPendingStash = false; + scheduleUnstashIfNeeded(mPendingPlacement); + } + + if (mUnstashRunnable != null) { + // currently stashed, use stashed pos + applyPlacementBounds(mPendingPlacement.getBounds(), + mPendingPlacementAnimationDuration); + } else { + applyPlacementBounds(mPendingPlacement.getUnstashedBounds(), + mPendingPlacementAnimationDuration); + } + } + + mPendingPlacement = null; + } + + void onPipDismissed() { + mCurrentPlacementBounds = null; + mPipTargetBounds = null; + cancelScheduledPlacement(); + } + + private void cancelScheduledPlacement() { + mMainHandler.removeCallbacks(mApplyPendingPlacementRunnable); + mPendingPlacement = null; + + if (mUnstashRunnable != null) { + mMainHandler.removeCallbacks(mUnstashRunnable); + mUnstashRunnable = null; + } + } + + private void applyPlacementBounds(Rect bounds, int animationDuration) { + if (bounds == null) { + return; + } + + mCurrentPlacementBounds = bounds; + Rect adjustedBounds = mTvPipBoundsAlgorithm.adjustBoundsForTemporaryDecor(bounds); + movePipTo(adjustedBounds, animationDuration); + } + + /** Animates the PiP to the given bounds with the given animation duration. */ + private void movePipTo(Rect bounds, int animationDuration) { + if (Objects.equals(mPipTargetBounds, bounds)) { + return; + } + + mPipTargetBounds = bounds; + if (DEBUG) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: movePipTo() - new pip bounds: %s", TAG, bounds.toShortString()); + } + + if (mListener != null) { + mListener.onPipTargetBoundsChange(bounds, animationDuration); + } + } + + /** + * Interface being notified of changes to the PiP bounds as calculated by + * @link TvPipBoundsController}. + */ + public interface PipBoundsListener { + /** + * Called when the calculated PiP bounds are changing. + * + * @param newTargetBounds The new bounds of the PiP. + * @param animationDuration The animation duration for the PiP movement. + */ + void onPipTargetBoundsChange(Rect newTargetBounds, int 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 new file mode 100644 index 000000000000..ca22882187d8 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsState.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.pip.tv; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.PictureInPictureParams; +import android.content.ComponentName; +import android.content.Context; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageManager; +import android.graphics.Insets; +import android.util.Size; +import android.view.Gravity; + +import com.android.wm.shell.pip.PipBoundsAlgorithm; +import com.android.wm.shell.pip.PipBoundsState; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * TV specific values of the current state of the PiP bounds. + */ +public class TvPipBoundsState extends PipBoundsState { + + public static final int ORIENTATION_UNDETERMINED = 0; + public static final int ORIENTATION_VERTICAL = 1; + public static final int ORIENTATION_HORIZONTAL = 2; + + @Retention(RetentionPolicy.SOURCE) + @IntDef(prefix = {"ORIENTATION_"}, value = { + ORIENTATION_UNDETERMINED, + ORIENTATION_VERTICAL, + ORIENTATION_HORIZONTAL + }) + public @interface Orientation { + } + + public static final int DEFAULT_TV_GRAVITY = Gravity.BOTTOM | Gravity.RIGHT; + + 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); + mIsTvExpandedPipSupported = context.getPackageManager().hasSystemFeature( + PackageManager.FEATURE_EXPANDED_PICTURE_IN_PICTURE); + } + + /** + * Initialize states when first entering PiP. + */ + @Override + public void setBoundsStateForEntry(ComponentName componentName, ActivityInfo activityInfo, + PictureInPictureParams params, PipBoundsAlgorithm pipBoundsAlgorithm) { + super.setBoundsStateForEntry(componentName, activityInfo, params, pipBoundsAlgorithm); + if (params == null) { + return; + } + setDesiredTvExpandedAspectRatio(params.getExpandedAspectRatioFloat(), true); + } + + /** Resets the TV PiP state for a new activity. */ + public void resetTvPipState() { + mTvFixedPipOrientation = ORIENTATION_UNDETERMINED; + mTvPipGravity = DEFAULT_TV_GRAVITY; + } + + /** Set the tv expanded bounds of PiP */ + public void setTvExpandedSize(@Nullable Size size) { + mTvExpandedSize = size; + } + + /** Get the expanded size of the PiP. */ + @Nullable + public Size getTvExpandedSize() { + return mTvExpandedSize; + } + + /** Set the PiP aspect ratio for the expanded PiP (TV) that is desired by the app. */ + public void setDesiredTvExpandedAspectRatio(float aspectRatio, boolean override) { + if (override || mTvFixedPipOrientation == ORIENTATION_UNDETERMINED) { + mDesiredTvExpandedAspectRatio = aspectRatio; + resetTvPipState(); + return; + } + if ((aspectRatio > 1 && mTvFixedPipOrientation == ORIENTATION_HORIZONTAL) + || (aspectRatio <= 1 && mTvFixedPipOrientation == ORIENTATION_VERTICAL) + || aspectRatio == 0) { + mDesiredTvExpandedAspectRatio = aspectRatio; + } + } + + /** + * Get the aspect ratio for the expanded PiP (TV) that is desired, or {@code 0} if it is not + * enabled by the app. + */ + public float getDesiredTvExpandedAspectRatio() { + 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() { + return mTvFixedPipOrientation; + } + + /** Sets the current gravity of the TV PiP. */ + public void setTvPipGravity(int gravity) { + mTvPipGravity = gravity; + } + + /** Returns the current gravity of the TV PiP. */ + public int getTvPipGravity() { + return mTvPipGravity; + } + + /** Sets whether the TV PiP is currently expanded. */ + public void setTvPipExpanded(boolean expanded) { + mIsTvPipExpanded = expanded; + } + + /** Returns whether the TV PiP is currently expanded. */ + public boolean isTvPipExpanded() { + return mIsTvPipExpanded; + } + + /** Sets whether the user has manually collapsed the TV PiP. */ + public void setTvPipManuallyCollapsed(boolean collapsed) { + mTvPipManuallyCollapsed = collapsed; + } + + /** Returns whether the user has manually collapsed the TV PiP. */ + public boolean isTvPipManuallyCollapsed() { + return mTvPipManuallyCollapsed; + } + + /** Returns whether expanded PiP is supported by the device. */ + public boolean isTvExpandedPipSupported() { + return mIsTvExpandedPipSupported; + } + + public void setPipMenuPermanentDecorInsets(@NonNull Insets permanentInsets) { + mPipMenuPermanentDecorInsets = permanentInsets; + } + + public @NonNull Insets getPipMenuPermanentDecorInsets() { + return mPipMenuPermanentDecorInsets; + } + + public void setPipMenuTemporaryDecorInsets(@NonNull Insets temporaryDecorInsets) { + mPipMenuTemporaryDecorInsets = temporaryDecorInsets; + } + + public @NonNull 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 00083d986dbe..fa48def9c7d7 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 @@ -22,50 +22,56 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; 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.ComponentName; import android.content.Context; -import android.content.pm.ParceledListSlice; import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Rect; import android.os.RemoteException; -import android.util.Log; -import android.view.DisplayInfo; +import android.view.Gravity; +import com.android.internal.protolog.common.ProtoLog; import com.android.wm.shell.R; import com.android.wm.shell.WindowManagerShellWrapper; +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.TaskStackListenerCallback; import com.android.wm.shell.common.TaskStackListenerImpl; import com.android.wm.shell.pip.PinnedStackListenerForwarder; import com.android.wm.shell.pip.Pip; -import com.android.wm.shell.pip.PipBoundsAlgorithm; -import com.android.wm.shell.pip.PipBoundsState; +import com.android.wm.shell.pip.PipAnimationController; +import com.android.wm.shell.pip.PipAppOpsListener; 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 java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.List; +import java.util.Objects; +import java.util.Set; /** * Manages the picture-in-picture (PIP) UI and states. */ public class TvPipController implements PipTransitionController.PipTransitionCallback, - TvPipMenuController.Delegate, TvPipNotificationController.Delegate { + TvPipBoundsController.PipBoundsListener, TvPipMenuController.Delegate, + TvPipNotificationController.Delegate, DisplayController.OnDisplaysChangedListener { private static final String TAG = "TvPipController"; - static final boolean DEBUG = true; + static final boolean DEBUG = false; private static final int NONEXISTENT_TASK_ID = -1; @Retention(RetentionPolicy.SOURCE) - @IntDef(prefix = { "STATE_" }, value = { + @IntDef(prefix = {"STATE_"}, value = { STATE_NO_PIP, STATE_PIP, - STATE_PIP_MENU + STATE_PIP_MENU, }) public @interface State {} @@ -87,8 +93,10 @@ public class TvPipController implements PipTransitionController.PipTransitionCal private final Context mContext; - private final PipBoundsState mPipBoundsState; - private final PipBoundsAlgorithm mPipBoundsAlgorithm; + private final TvPipBoundsState mTvPipBoundsState; + private final TvPipBoundsAlgorithm mTvPipBoundsAlgorithm; + private final TvPipBoundsController mTvPipBoundsController; + private final PipAppOpsListener mAppOpsListener; private final PipTaskOrganizer mPipTaskOrganizer; private final PipMediaController mPipMediaController; private final TvPipNotificationController mPipNotificationController; @@ -97,55 +105,75 @@ public class TvPipController implements PipTransitionController.PipTransitionCal private final TvPipImpl mImpl = new TvPipImpl(); private @State int mState = STATE_NO_PIP; + private int mPreviousGravity = TvPipBoundsState.DEFAULT_TV_GRAVITY; 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; public static Pip create( Context context, - PipBoundsState pipBoundsState, - PipBoundsAlgorithm pipBoundsAlgorithm, + TvPipBoundsState tvPipBoundsState, + TvPipBoundsAlgorithm tvPipBoundsAlgorithm, + TvPipBoundsController tvPipBoundsController, + PipAppOpsListener pipAppOpsListener, PipTaskOrganizer pipTaskOrganizer, PipTransitionController pipTransitionController, TvPipMenuController tvPipMenuController, PipMediaController pipMediaController, TvPipNotificationController pipNotificationController, TaskStackListenerImpl taskStackListener, + PipParamsChangedForwarder pipParamsChangedForwarder, + DisplayController displayController, WindowManagerShellWrapper wmShell, ShellExecutor mainExecutor) { return new TvPipController( context, - pipBoundsState, - pipBoundsAlgorithm, + tvPipBoundsState, + tvPipBoundsAlgorithm, + tvPipBoundsController, + pipAppOpsListener, pipTaskOrganizer, pipTransitionController, tvPipMenuController, pipMediaController, pipNotificationController, taskStackListener, + pipParamsChangedForwarder, + displayController, wmShell, mainExecutor).mImpl; } private TvPipController( Context context, - PipBoundsState pipBoundsState, - PipBoundsAlgorithm pipBoundsAlgorithm, + TvPipBoundsState tvPipBoundsState, + TvPipBoundsAlgorithm tvPipBoundsAlgorithm, + TvPipBoundsController tvPipBoundsController, + PipAppOpsListener pipAppOpsListener, PipTaskOrganizer pipTaskOrganizer, PipTransitionController pipTransitionController, TvPipMenuController tvPipMenuController, PipMediaController pipMediaController, TvPipNotificationController pipNotificationController, TaskStackListenerImpl taskStackListener, + PipParamsChangedForwarder pipParamsChangedForwarder, + DisplayController displayController, WindowManagerShellWrapper wmShell, ShellExecutor mainExecutor) { mContext = context; mMainExecutor = mainExecutor; - mPipBoundsState = pipBoundsState; - mPipBoundsState.setDisplayId(context.getDisplayId()); - mPipBoundsState.setDisplayLayout(new DisplayLayout(context, context.getDisplay())); - mPipBoundsAlgorithm = pipBoundsAlgorithm; + mTvPipBoundsState = tvPipBoundsState; + mTvPipBoundsState.setDisplayId(context.getDisplayId()); + mTvPipBoundsState.setDisplayLayout(new DisplayLayout(context, context.getDisplay())); + mTvPipBoundsAlgorithm = tvPipBoundsAlgorithm; + mTvPipBoundsController = tvPipBoundsController; + mTvPipBoundsController.setListener(this); mPipMediaController = pipMediaController; @@ -155,25 +183,35 @@ public class TvPipController implements PipTransitionController.PipTransitionCal mTvPipMenuController = tvPipMenuController; mTvPipMenuController.setDelegate(this); + mAppOpsListener = pipAppOpsListener; mPipTaskOrganizer = pipTaskOrganizer; pipTransitionController.registerPipTransitionCallback(this); loadConfigurations(); + registerPipParamsChangedListener(pipParamsChangedForwarder); registerTaskStackListenerCallback(taskStackListener); registerWmShellPinnedStackListener(wmShell); + displayController.addDisplayWindowListener(this); } private void onConfigurationChanged(Configuration newConfig) { - if (DEBUG) Log.d(TAG, "onConfigurationChanged(), state=" + stateToName(mState)); + if (DEBUG) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: onConfigurationChanged(), state=%s", TAG, stateToName(mState)); + } if (isPipShown()) { - if (DEBUG) Log.d(TAG, " > closing Pip."); + if (DEBUG) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: > closing Pip.", TAG); + } closePip(); } loadConfigurations(); mPipNotificationController.onConfigurationChanged(mContext); + mTvPipBoundsAlgorithm.onConfigurationChanged(mContext); } /** @@ -186,30 +224,41 @@ 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(ComponentName, int)}. + * task/window is properly positioned in {@link #onPipTransitionFinished(int)}. */ @Override public void showPictureInPictureMenu() { - if (DEBUG) Log.d(TAG, "showPictureInPictureMenu(), state=" + stateToName(mState)); + if (DEBUG) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: showPictureInPictureMenu(), state=%s", TAG, stateToName(mState)); + } - if (mState != STATE_PIP) { - if (DEBUG) Log.d(TAG, " > cannot open Menu from the current state."); + if (mState == STATE_NO_PIP) { + if (DEBUG) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: > cannot open Menu from the current state.", TAG); + } return; } setState(STATE_PIP_MENU); - resizePinnedStack(STATE_PIP_MENU); + mTvPipMenuController.showMenu(); + updatePinnedStackBounds(); } - /** - * Moves Pip window to its "normal" position. - */ @Override - public void movePipToNormalPosition() { - if (DEBUG) Log.d(TAG, "movePipToNormalPosition(), state=" + stateToName(mState)); - + public void onMenuClosed() { + if (DEBUG) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: closeMenu(), state before=%s", TAG, stateToName(mState)); + } setState(STATE_PIP); - resizePinnedStack(STATE_PIP); + updatePinnedStackBounds(); + } + + @Override + public void onInMoveModeChanged() { + updatePinnedStackBounds(); } /** @@ -217,59 +266,135 @@ public class TvPipController implements PipTransitionController.PipTransitionCal */ @Override public void movePipToFullscreen() { - if (DEBUG) Log.d(TAG, "movePipToFullscreen(), state=" + stateToName(mState)); + if (DEBUG) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: movePipToFullscreen(), state=%s", TAG, stateToName(mState)); + } mPipTaskOrganizer.exitPip(mResizeAnimationDuration, false /* requestEnterSplit */); onPipDisappeared(); } - /** - * Closes Pip window. - */ @Override - public void closePip() { - if (DEBUG) Log.d(TAG, "closePip(), state=" + stateToName(mState)); + public void togglePipExpansion() { + if (DEBUG) { + 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; + } + mTvPipBoundsState.setTvPipManuallyCollapsed(!expanding); + mTvPipBoundsState.setTvPipExpanded(expanding); + mPipNotificationController.updateExpansionState(); - removeTask(mPinnedTaskId); - onPipDisappeared(); + 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); + } + } + } + + @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) { + boolean unrestrictedAreasChanged = !Objects.equals(unrestricted, + mTvPipBoundsState.getUnrestrictedKeepClearAreas()); + mTvPipBoundsState.setKeepClearAreas(restricted, unrestricted); + updatePinnedStackBounds(mResizeAnimationDuration, unrestrictedAreasChanged); + } + } + + private void updatePinnedStackBounds() { + updatePinnedStackBounds(mResizeAnimationDuration, true); } /** - * Resizes the Pip task/window to the appropriate size for the given state. - * This is a legacy API. Now we expect that the state argument passed to it should always match - * the current state of the Controller. If it does not match an {@link IllegalArgumentException} - * will be thrown. However, if the passed state does match - we'll determine the right bounds - * to the state and will move Pip task/window there. - * - * @param state the to determine the Pip bounds. IMPORTANT: should always match the current - * state of the Controller. + * Update the PiP bounds based on the state of the PiP and keep clear areas. */ - private void resizePinnedStack(@State int state) { - if (state != mState) { - throw new IllegalArgumentException("The passed state should match the current state!"); + private void updatePinnedStackBounds(int animationDuration, boolean immediate) { + if (mState == STATE_NO_PIP) { + return; } - if (DEBUG) Log.d(TAG, "resizePinnedStack() state=" + stateToName(mState)); + final boolean stayAtAnchorPosition = mTvPipMenuController.isInMoveMode(); + final boolean disallowStashing = mState == STATE_PIP_MENU || stayAtAnchorPosition; + mTvPipBoundsController.recalculatePipBounds(stayAtAnchorPosition, disallowStashing, + animationDuration, immediate); + } - final Rect newBounds; - switch (mState) { - case STATE_PIP_MENU: - newBounds = mPipBoundsState.getExpandedBounds(); - break; + @Override + public void onPipTargetBoundsChange(Rect newTargetBounds, int animationDuration) { + mPipTaskOrganizer.scheduleAnimateResizePip(newTargetBounds, + animationDuration, rect -> mTvPipMenuController.updateExpansionState()); + mTvPipMenuController.onPipTransitionStarted(newTargetBounds); + } - case STATE_PIP: - // Let PipBoundsAlgorithm figure out what the correct bounds are at the moment. - // Internally, it will get the "default" bounds from PipBoundsState and adjust them - // as needed to account for things like IME state (will query PipBoundsState for - // this information as well, so it's important to keep PipBoundsState up to date). - newBounds = mPipBoundsAlgorithm.getNormalBounds(); - break; + /** + * 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); + } - case STATE_NO_PIP: - default: - return; + 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); } + } + + private void closeCurrentPiP(int pinnedTaskId) { + if (mPinnedTaskId != pinnedTaskId) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: PiP has already been closed by custom close action", TAG); + return; + } + removeTask(mPinnedTaskId); + onPipDisappeared(); + } - mPipTaskOrganizer.scheduleAnimateResizePip(newBounds, mResizeAnimationDuration, null); + @Override + public void closeEduText() { + updatePinnedStackBounds(mEduTextWindowExitAnimationDurationMs, false); } private void registerSessionListenerForCurrentUser() { @@ -278,57 +403,79 @@ public class TvPipController implements PipTransitionController.PipTransitionCal private void checkIfPinnedTaskAppeared() { final TaskInfo pinnedTask = getPinnedTaskInfo(); - if (DEBUG) Log.d(TAG, "checkIfPinnedTaskAppeared(), task=" + pinnedTask); + 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; - setState(STATE_PIP); mPipMediaController.onActivityPinned(); mPipNotificationController.show(pinnedTask.topActivity.getPackageName()); } private void checkIfPinnedTaskIsGone() { - if (DEBUG) Log.d(TAG, "onTaskStackChanged()"); + if (DEBUG) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: onTaskStackChanged()", TAG); + } if (isPipShown() && getPinnedTaskInfo() == null) { - Log.w(TAG, "Pinned task is gone."); + ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: Pinned task is gone.", TAG); onPipDisappeared(); } } private void onPipDisappeared() { - if (DEBUG) Log.d(TAG, "onPipDisappeared() state=" + stateToName(mState)); + if (DEBUG) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: onPipDisappeared() state=%s", TAG, stateToName(mState)); + } mPipNotificationController.dismiss(); - mTvPipMenuController.hideMenu(); + mTvPipMenuController.closeMenu(); + mTvPipBoundsState.resetTvPipState(); + mTvPipBoundsController.onPipDismissed(); setState(STATE_NO_PIP); mPinnedTaskId = NONEXISTENT_TASK_ID; } @Override public void onPipTransitionStarted(int direction, Rect pipBounds) { - if (DEBUG) Log.d(TAG, "onPipTransition_Started(), state=" + stateToName(mState)); + if (DEBUG) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: onPipTransition_Started(), state=%s", TAG, stateToName(mState)); + } + mTvPipMenuController.notifyPipAnimating(true); } @Override public void onPipTransitionCanceled(int direction) { - if (DEBUG) Log.d(TAG, "onPipTransition_Canceled(), state=" + stateToName(mState)); + if (DEBUG) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: onPipTransition_Canceled(), state=%s", TAG, stateToName(mState)); + } + mTvPipMenuController.notifyPipAnimating(false); } @Override public void onPipTransitionFinished(int direction) { - if (DEBUG) Log.d(TAG, "onPipTransition_Finished(), state=" + stateToName(mState)); - - if (mState == STATE_PIP_MENU) { - if (DEBUG) Log.d(TAG, " > show menu"); - mTvPipMenuController.showMenu(); + if (PipAnimationController.isInPipDirection(direction) && mState == STATE_NO_PIP) { + setState(STATE_PIP); } + if (DEBUG) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: onPipTransition_Finished(), state=%s", TAG, stateToName(mState)); + } + mTvPipMenuController.notifyPipAnimating(false); } private void setState(@State int state) { if (DEBUG) { - Log.d(TAG, "setState(), state=" + stateToName(state) + ", prev=" - + stateToName(mState)); + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: setState(), state=%s, prev=%s", + TAG, stateToName(state), stateToName(mState)); } mState = state; } @@ -336,17 +483,9 @@ public class TvPipController implements PipTransitionController.PipTransitionCal private void loadConfigurations() { final Resources res = mContext.getResources(); mResizeAnimationDuration = res.getInteger(R.integer.config_pipResizeAnimationDuration); - // "Cache" bounds for the Pip menu as "expanded" bounds in PipBoundsState. We'll refer back - // to this value in resizePinnedStack(), when we are adjusting Pip task/window position for - // the menu. - mPipBoundsState.setExpandedBounds( - Rect.unflattenFromString(res.getString(R.string.pip_menu_bounds))); - } - - private DisplayInfo getDisplayInfo() { - final DisplayInfo displayInfo = new DisplayInfo(); - mContext.getDisplay().getDisplayInfo(displayInfo); - return displayInfo; + mPipForceCloseDelay = res.getInteger(R.integer.config_pipForceCloseDelay); + mEduTextWindowExitAnimationDurationMs = + res.getInteger(R.integer.pip_edu_text_window_exit_animation_duration_ms); } private void registerTaskStackListenerCallback(TaskStackListenerImpl taskStackListener) { @@ -354,6 +493,12 @@ public class TvPipController implements PipTransitionController.PipTransitionCal @Override public void onActivityPinned(String packageName, int userId, int taskId, int stackId) { checkIfPinnedTaskAppeared(); + mAppOpsListener.onActivityPinned(packageName); + } + + @Override + public void onActivityUnpinned() { + mAppOpsListener.onActivityUnpinned(); } @Override @@ -365,7 +510,10 @@ 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) Log.d(TAG, "onPinnedActivityRestartAttempt()"); + if (DEBUG) { + 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. @@ -375,64 +523,133 @@ public class TvPipController implements PipTransitionController.PipTransitionCal }); } + private void registerPipParamsChangedListener(PipParamsChangedForwarder provider) { + provider.addListener(new PipParamsChangedForwarder.PipParamsChangedCallback() { + @Override + public void onActionsChanged(List<RemoteAction> actions, + RemoteAction closeAction) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: onActionsChanged()", TAG); + + mTvPipMenuController.setAppActions(actions, closeAction); + mCloseAction = closeAction; + } + + @Override + public void onAspectRatioChanged(float ratio) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: onAspectRatioChanged: %f", TAG, ratio); + + mTvPipBoundsState.setAspectRatio(ratio); + if (!mTvPipBoundsState.isTvPipExpanded()) { + updatePinnedStackBounds(); + } + } + + @Override + public void onExpandedAspectRatioChanged(float ratio) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: onExpandedAspectRatioChanged: %f", TAG, ratio); + + mTvPipBoundsState.setDesiredTvExpandedAspectRatio(ratio, false); + mTvPipMenuController.updateExpansionState(); + + // 1) PiP is expanded and only aspect ratio changed, but wasn't disabled + // --> update bounds, but don't toggle + if (mTvPipBoundsState.isTvPipExpanded() && ratio != 0) { + mTvPipBoundsAlgorithm.updateExpandedPipSize(); + updatePinnedStackBounds(); + } + + // 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; + } + mTvPipBoundsState.setTvPipExpanded(false); + updatePinnedStackBounds(); + } + + // 3) PiP not expanded and not manually collapsed and expand was enabled + // --> expand to new ratio + if (!mTvPipBoundsState.isTvPipExpanded() && ratio != 0 + && !mTvPipBoundsState.isTvPipManuallyCollapsed()) { + mTvPipBoundsAlgorithm.updateExpandedPipSize(); + int saveGravity = mTvPipBoundsAlgorithm + .updateGravityOnExpandToggled(mPreviousGravity, true); + if (saveGravity != Gravity.NO_GRAVITY) { + mPreviousGravity = saveGravity; + } + mTvPipBoundsState.setTvPipExpanded(true); + updatePinnedStackBounds(); + } + } + }); + } + private void registerWmShellPinnedStackListener(WindowManagerShellWrapper wmShell) { try { wmShell.addPinnedStackListener(new PinnedStackListenerForwarder.PinnedTaskListener() { @Override public void onImeVisibilityChanged(boolean imeVisible, int imeHeight) { if (DEBUG) { - Log.d(TAG, "onImeVisibilityChanged(), visible=" + imeVisible - + ", height=" + imeHeight); + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: onImeVisibilityChanged(), visible=%b, height=%d", + TAG, imeVisible, imeHeight); } - if (imeVisible == mPipBoundsState.isImeShowing() - && (!imeVisible || imeHeight == mPipBoundsState.getImeHeight())) { + if (imeVisible == mTvPipBoundsState.isImeShowing() + && (!imeVisible || imeHeight == mTvPipBoundsState.getImeHeight())) { // Nothing changed: either IME has been and remains invisible, or remains // visible with the same height. return; } - mPipBoundsState.setImeVisibility(imeVisible, imeHeight); - // "Normal" Pip bounds may have changed, so if we are in the "normal" state, - // let's update the bounds. - if (mState == STATE_PIP) { - resizePinnedStack(STATE_PIP); - } - } - - @Override - public void onMovementBoundsChanged(boolean fromImeAdjustment) {} + mTvPipBoundsState.setImeVisibility(imeVisible, imeHeight); - @Override - public void onActionsChanged(ParceledListSlice<RemoteAction> actions) { - if (DEBUG) Log.d(TAG, "onActionsChanged()"); - - mTvPipMenuController.setAppActions(actions); + if (mState != STATE_NO_PIP) { + updatePinnedStackBounds(); + } } }); } catch (RemoteException e) { - Log.e(TAG, "Failed to register pinned stack listener", e); + ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: Failed to register pinned stack listener, %s", TAG, e); } } private static TaskInfo getPinnedTaskInfo() { - if (DEBUG) Log.d(TAG, "getPinnedTaskInfo()"); + if (DEBUG) { + 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) Log.d(TAG, " > taskInfo=" + taskInfo); + if (DEBUG) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: taskInfo=%s", TAG, taskInfo); + } return taskInfo; } catch (RemoteException e) { - Log.e(TAG, "getRootTaskInfo() failed", e); + ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: getRootTaskInfo() failed, %s", TAG, e); return null; } } private static void removeTask(int taskId) { - if (DEBUG) Log.d(TAG, "removeTask(), taskId=" + 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) { - Log.e(TAG, "Atm.removeTask() failed", e); + ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: Atm.removeTask() failed, %s", TAG, e); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipInterpolators.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipInterpolators.java new file mode 100644 index 000000000000..927c1ec2a888 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipInterpolators.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.pip.tv; + +import android.view.animation.Interpolator; +import android.view.animation.PathInterpolator; + +/** + * All interpolators needed for TV specific Pip animations + */ +public class TvPipInterpolators { + + /** + * A standard ease-in-out curve reserved for moments of interaction (button and card states). + */ + public static final Interpolator STANDARD = new PathInterpolator(0.2f, 0.1f, 0f, 1f); + + /** + * A sharp ease-out-expo curve created for snappy but fluid browsing between cards and clusters. + */ + public static final Interpolator BROWSE = new PathInterpolator(0.18f, 1f, 0.22f, 1f); + + /** + * A smooth ease-out-expo curve created for incoming elements (forward, back, overlay). + */ + public static final Interpolator ENTER = new PathInterpolator(0.12f, 1f, 0.4f, 1f); + + /** + * A smooth ease-in-out-expo curve created for outgoing elements (forward, back, overlay). + */ + public static final Interpolator EXIT = new PathInterpolator(0.4f, 1f, 0.12f, 1f); + +} 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 new file mode 100644 index 000000000000..1e54436ebce9 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipKeepClearAlgorithm.kt @@ -0,0 +1,771 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.graphics.Insets +import android.graphics.Point +import android.graphics.Rect +import android.util.Size +import android.view.Gravity +import com.android.wm.shell.pip.PipBoundsState +import com.android.wm.shell.pip.PipBoundsState.STASH_TYPE_BOTTOM +import com.android.wm.shell.pip.PipBoundsState.STASH_TYPE_LEFT +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 kotlin.math.abs +import kotlin.math.max +import kotlin.math.min +import kotlin.math.roundToInt + +private const val DEFAULT_PIP_MARGINS = 48 +private const val RELAX_DEPTH = 1 +private const val DEFAULT_MAX_RESTRICTED_DISTANCE_FRACTION = 0.15 + +/** + * This class calculates an appropriate position for a Picture-In-Picture (PiP) window, taking + * into account app defined keep clear areas. + */ +class TvPipKeepClearAlgorithm() { + /** + * Result of the positioning algorithm. + * + * @param bounds The bounds the PiP should be placed at + * @param anchorBounds The bounds of the PiP anchor position + * (where the PiP would be placed if there were no keep clear areas) + * @param stashType Where the PiP has been stashed, if at all + * @param unstashDestinationBounds If stashed, the PiP should move to this position when + * unstashing. + * @param triggerStash Whether this placement should trigger the PiP to stash, or extend + * the unstash timeout if already stashed. + */ + data class Placement( + val bounds: Rect, + val anchorBounds: Rect, + @PipBoundsState.StashType val stashType: Int = STASH_TYPE_NONE, + val unstashDestinationBounds: Rect? = null, + val triggerStash: Boolean = false + ) { + /** Bounds to use if the PiP should not be stashed. */ + fun getUnstashedBounds() = unstashDestinationBounds ?: bounds + } + + /** The size of the screen */ + private var screenSize = Size(0, 0) + + /** The bounds the PiP is allowed to move in */ + private var movementBounds = Rect() + + /** Padding to add between a keep clear area that caused the PiP to move and the PiP */ + var pipAreaPadding = DEFAULT_PIP_MARGINS + + /** The distance the PiP peeks into the screen when stashed */ + var stashOffset = DEFAULT_PIP_MARGINS + + /** The fraction of screen width/height restricted keep clear areas can move the PiP */ + var maxRestrictedDistanceFraction = DEFAULT_MAX_RESTRICTED_DISTANCE_FRACTION + + private var pipGravity = Gravity.BOTTOM or Gravity.RIGHT + private var transformedScreenBounds = Rect() + private var transformedMovementBounds = Rect() + + private var lastAreasOverlappingUnstashPosition: Set<Rect> = emptySet() + + /** Spaces around the PiP that we should leave space for when placing the PiP. Permanent PiP + * decorations are relevant for calculating intersecting keep clear areas */ + private var pipPermanentDecorInsets = Insets.NONE + + /** + * Calculates the position the PiP should be placed at, taking into consideration the + * given keep clear areas. + * + * Restricted keep clear areas can move the PiP only by a limited amount, and may be ignored + * if there is no space for the PiP to move to. + * Apps holding the permission [android.Manifest.permission.USE_UNRESTRICTED_KEEP_CLEAR_AREAS] + * can declare unrestricted keep clear areas, which can move the PiP farther and placement will + * always try to respect these areas. + * + * If no free space the PiP is allowed to move to can be found, a stashed position is returned + * as [Placement.bounds], along with a position to move to when the PiP unstashes + * as [Placement.unstashDestinationBounds]. + * + * @param pipSize The size of the PiP window + * @param restrictedAreas The restricted keep clear areas + * @param unrestrictedAreas The unrestricted keep clear areas + * + */ + fun calculatePipPosition( + pipSize: Size, + restrictedAreas: Set<Rect>, + unrestrictedAreas: Set<Rect> + ): Placement { + val transformedRestrictedAreas = transformAndFilterAreas(restrictedAreas) + val transformedUnrestrictedAreas = transformAndFilterAreas(unrestrictedAreas) + + val pipSizeWithAllDecors = addDecors(pipSize) + val pipAnchorBoundsWithDecors = + getNormalPipAnchorBounds(pipSizeWithAllDecors, transformedMovementBounds) + + val result = calculatePipPositionTransformed( + pipAnchorBoundsWithDecors, + transformedRestrictedAreas, + transformedUnrestrictedAreas + ) + + val pipBounds = removePermanentDecors(fromTransformedSpace(result.bounds)) + val anchorBounds = removePermanentDecors(fromTransformedSpace(result.anchorBounds)) + val unstashedDestBounds = result.unstashDestinationBounds?.let { + removePermanentDecors(fromTransformedSpace(it)) + } + + return Placement( + pipBounds, + anchorBounds, + getStashType(pipBounds, unstashedDestBounds), + unstashedDestBounds, + result.triggerStash + ) + } + + /** + * Filters out areas that encompass the entire movement bounds and returns them mapped to + * the base case space. + * + * Areas encompassing the entire movement bounds can occur when a full-screen View gets focused, + * but we don't want this to cause the PiP to get stashed. + */ + private fun transformAndFilterAreas(areas: Set<Rect>): Set<Rect> { + return areas.mapNotNullTo(mutableSetOf()) { + when { + it.contains(movementBounds) -> null + else -> toTransformedSpace(it) + } + } + } + + /** + * Calculates the position the PiP should be placed at, taking into consideration the + * given keep clear areas. + * All parameters are transformed from screen space to the base case space, where the PiP + * anchor is in the bottom right corner / on the right side. + * + * @see [calculatePipPosition] + */ + private fun calculatePipPositionTransformed( + pipAnchorBounds: Rect, + restrictedAreas: Set<Rect>, + unrestrictedAreas: Set<Rect> + ): Placement { + // If PiP is not covered by any keep clear areas, we can leave it at the anchor bounds + val keepClearAreas = restrictedAreas + unrestrictedAreas + if (keepClearAreas.none { it.intersects(pipAnchorBounds) }) { + lastAreasOverlappingUnstashPosition = emptySet() + return Placement(pipAnchorBounds, pipAnchorBounds) + } + + // First try to find a free position to move to + val freeMovePos = findFreeMovePosition(pipAnchorBounds, restrictedAreas, unrestrictedAreas) + if (freeMovePos != null) { + lastAreasOverlappingUnstashPosition = emptySet() + return Placement(freeMovePos, pipAnchorBounds) + } + + // If no free position is found, we have to stash the PiP. + // Find the position the PiP should return to once it unstashes by doing a relaxed + // search, or ignoring restricted areas, or returning to the anchor position + val unstashBounds = + findRelaxedMovePosition(pipAnchorBounds, restrictedAreas, unrestrictedAreas) + ?: findFreeMovePosition(pipAnchorBounds, emptySet(), unrestrictedAreas) + ?: pipAnchorBounds + + val areasOverlappingUnstashPosition = + keepClearAreas.filterTo(mutableSetOf()) { it.intersects(unstashBounds) } + val areasOverlappingUnstashPositionChanged = + !lastAreasOverlappingUnstashPosition.containsAll(areasOverlappingUnstashPosition) + lastAreasOverlappingUnstashPosition = areasOverlappingUnstashPosition + + val stashedBounds = getNearbyStashedPosition(unstashBounds, keepClearAreas) + return Placement( + stashedBounds, + pipAnchorBounds, + getStashType(stashedBounds, unstashBounds), + unstashBounds, + areasOverlappingUnstashPositionChanged + ) + } + + @PipBoundsState.StashType + private fun getStashType(stashedBounds: Rect, unstashedDestBounds: Rect?): Int { + if (unstashedDestBounds == null) { + return STASH_TYPE_NONE + } + return when { + stashedBounds.left < unstashedDestBounds.left -> STASH_TYPE_LEFT + stashedBounds.right > unstashedDestBounds.right -> STASH_TYPE_RIGHT + stashedBounds.top < unstashedDestBounds.top -> STASH_TYPE_TOP + stashedBounds.bottom > unstashedDestBounds.bottom -> STASH_TYPE_BOTTOM + else -> STASH_TYPE_NONE + } + } + + private fun findRelaxedMovePosition( + pipAnchorBounds: Rect, + restrictedAreas: Set<Rect>, + unrestrictedAreas: Set<Rect> + ): Rect? { + if (RELAX_DEPTH <= 0) { + // relaxed search disabled + return null + } + + return findRelaxedMovePosition( + RELAX_DEPTH, + pipAnchorBounds, + restrictedAreas.toMutableSet(), + unrestrictedAreas + ) + } + + private fun findRelaxedMovePosition( + depth: Int, + pipAnchorBounds: Rect, + restrictedAreas: MutableSet<Rect>, + unrestrictedAreas: Set<Rect> + ): Rect? { + if (depth == 0) { + return findFreeMovePosition(pipAnchorBounds, restrictedAreas, unrestrictedAreas) + } + + val candidates = mutableListOf<Rect>() + val areasToExclude = restrictedAreas.toList() + for (area in areasToExclude) { + restrictedAreas.remove(area) + val candidate = findRelaxedMovePosition( + depth - 1, + pipAnchorBounds, + restrictedAreas, + unrestrictedAreas + ) + restrictedAreas.add(area) + + if (candidate != null) { + candidates.add(candidate) + } + } + return candidates.minByOrNull { candidateCost(it, pipAnchorBounds) } + } + + /** Cost function to evaluate candidate bounds */ + private fun candidateCost(candidateBounds: Rect, pipAnchorBounds: Rect): Int { + // squared euclidean distance of corresponding rect corners + val dx = candidateBounds.left - pipAnchorBounds.left + val dy = candidateBounds.top - pipAnchorBounds.top + return dx * dx + dy * dy + } + + private fun findFreeMovePosition( + pipAnchorBounds: Rect, + restrictedAreas: Set<Rect>, + unrestrictedAreas: Set<Rect> + ): Rect? { + val movementBounds = transformedMovementBounds + val candidateEdgeRects = mutableListOf<Rect>() + val minRestrictedLeft = + pipAnchorBounds.right - screenSize.width * maxRestrictedDistanceFraction + + candidateEdgeRects.add( + movementBounds.offsetCopy(movementBounds.width() + pipAreaPadding, 0) + ) + candidateEdgeRects.addAll(unrestrictedAreas) + candidateEdgeRects.addAll(restrictedAreas.filter { it.left >= minRestrictedLeft }) + + // 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() + + val candidateBounds = mutableListOf<Rect>() + for (edgeRect in candidateEdgeRects) { + val edge = edgeRect.left - pipAreaPadding + val dx = (edge - pipAnchorBounds.width()) - pipAnchorBounds.left + val candidatePipBounds = pipAnchorBounds.offsetCopy(dx, 0) + val searchUp = true + val searchDown = !isPipAnchoredToCorner() + + if (searchUp) { + val event = findMinMoveUp(candidatePipBounds, restrictedAreas, unrestrictedAreas) + val padding = if (event.start) 0 else pipAreaPadding + val dy = event.pos - pipAnchorBounds.bottom - padding + val maxDY = if (event.unrestricted) movementBounds.height() else maxRestrictedDY + val candidate = pipAnchorBounds.offsetCopy(dx, dy) + val isOnScreen = candidate.top > movementBounds.top + val hangingMidAir = !candidate.intersectsY(edgeRect) + if (isOnScreen && abs(dy) <= maxDY && !hangingMidAir) { + candidateBounds.add(candidate) + } + } + + if (searchDown) { + val event = findMinMoveDown(candidatePipBounds, restrictedAreas, unrestrictedAreas) + val padding = if (event.start) 0 else pipAreaPadding + val dy = event.pos - pipAnchorBounds.top + padding + val maxDY = if (event.unrestricted) movementBounds.height() else maxRestrictedDY + val candidate = pipAnchorBounds.offsetCopy(dx, dy) + val isOnScreen = candidate.bottom < movementBounds.bottom + val hangingMidAir = !candidate.intersectsY(edgeRect) + if (isOnScreen && abs(dy) <= maxDY && !hangingMidAir) { + candidateBounds.add(candidate) + } + } + } + + candidateBounds.sortBy { candidateCost(it, pipAnchorBounds) } + return candidateBounds.firstOrNull() + } + + private fun getNearbyStashedPosition(bounds: Rect, keepClearAreas: Set<Rect>): Rect { + val screenBounds = transformedScreenBounds + val stashCandidates = mutableListOf<Rect>() + val areasOverlappingPipX = keepClearAreas.filter { it.intersectsX(bounds) } + val areasOverlappingPipY = keepClearAreas.filter { it.intersectsY(bounds) } + + if (areasOverlappingPipX.isNotEmpty()) { + if (screenBounds.bottom - bounds.bottom <= bounds.top - screenBounds.top) { + val fullStashTop = screenBounds.bottom - stashOffset + + val maxBottom = areasOverlappingPipX.maxByOrNull { it.bottom }!!.bottom + val partialStashTop = maxBottom + pipAreaPadding + + val newTop = min(fullStashTop, partialStashTop) + if (newTop > bounds.top) { + val downPosition = Rect(bounds) + downPosition.offsetTo(bounds.left, newTop) + stashCandidates += downPosition + } + } + if (screenBounds.bottom - bounds.bottom >= bounds.top - screenBounds.top) { + val fullStashBottom = screenBounds.top - bounds.height() + stashOffset + + val minTop = areasOverlappingPipX.minByOrNull { it.top }!!.top + val partialStashBottom = minTop - bounds.height() - pipAreaPadding + + val newTop = max(fullStashBottom, partialStashBottom) + if (newTop < bounds.top) { + val upPosition = Rect(bounds) + upPosition.offsetTo(bounds.left, newTop) + stashCandidates += upPosition + } + } + } + + if (areasOverlappingPipY.isNotEmpty()) { + if (screenBounds.right - bounds.right <= bounds.left - screenBounds.left) { + val fullStashRight = screenBounds.right - stashOffset + + val maxRight = areasOverlappingPipY.maxByOrNull { it.right }!!.right + val partialStashRight = maxRight + pipAreaPadding + + val newLeft = min(fullStashRight, partialStashRight) + if (newLeft > bounds.left) { + val rightPosition = Rect(bounds) + rightPosition.offsetTo(newLeft, bounds.top) + stashCandidates += rightPosition + } + } + if (screenBounds.right - bounds.right >= bounds.left - screenBounds.left) { + val fullStashLeft = screenBounds.left - bounds.width() + stashOffset + + val minLeft = areasOverlappingPipY.minByOrNull { it.left }!!.left + val partialStashLeft = minLeft - bounds.width() - pipAreaPadding + + val newLeft = max(fullStashLeft, partialStashLeft) + if (newLeft < bounds.left) { + val leftPosition = Rect(bounds) + leftPosition.offsetTo(newLeft, bounds.top) + stashCandidates += leftPosition + } + } + } + + return stashCandidates.minByOrNull { + val dx = abs(it.left - bounds.left) + val dy = abs(it.top - bounds.top) + return@minByOrNull dx + dy + } ?: bounds + } + + /** + * Updates the size of the screen. + * + * @param size The new size of the screen + */ + fun setScreenSize(size: Size) { + if (screenSize == size) { + return + } + + screenSize = size + transformedScreenBounds = + toTransformedSpace(Rect(0, 0, screenSize.width, screenSize.height)) + transformedMovementBounds = toTransformedSpace(transformedMovementBounds) + } + + /** + * Updates the bounds within which the PiP is allowed to move. + * + * @param bounds The new movement bounds + */ + fun setMovementBounds(bounds: Rect) { + if (movementBounds == bounds) { + return + } + + movementBounds.set(bounds) + transformedMovementBounds = toTransformedSpace(movementBounds) + } + + /** + * Sets the corner/side of the PiP's home position. + */ + fun setGravity(gravity: Int) { + if (pipGravity == gravity) return + + pipGravity = gravity + transformedScreenBounds = + toTransformedSpace(Rect(0, 0, screenSize.width, screenSize.height)) + transformedMovementBounds = toTransformedSpace(movementBounds) + } + + fun setPipPermanentDecorInsets(insets: Insets) { + pipPermanentDecorInsets = insets + } + + /** + * @param open Whether this event marks the opening of an occupied segment + * @param pos The coordinate of this event + * @param unrestricted Whether this event was generated by an unrestricted keep clear area + * @param start Marks the special start event. Earlier events are skipped when sweeping + */ + data class SweepLineEvent( + val open: Boolean, + val pos: Int, + val unrestricted: Boolean, + val start: Boolean = false + ) + + /** + * Returns a [SweepLineEvent] representing the minimal move up from [pipBounds] that clears + * the given keep clear areas. + */ + private fun findMinMoveUp( + pipBounds: Rect, + restrictedAreas: Set<Rect>, + unrestrictedAreas: Set<Rect> + ): SweepLineEvent { + val events = mutableListOf<SweepLineEvent>() + val generateEvents: (Boolean) -> (Rect) -> Unit = { unrestricted -> + { area -> + if (pipBounds.intersectsX(area)) { + events.add(SweepLineEvent(true, area.bottom, unrestricted)) + events.add(SweepLineEvent(false, area.top, unrestricted)) + } + } + } + + restrictedAreas.forEach(generateEvents(false)) + unrestrictedAreas.forEach(generateEvents(true)) + + return sweepLineFindEarliestGap( + events, + pipBounds.height() + pipAreaPadding, + pipBounds.bottom, + pipBounds.height() + ) + } + + /** + * Returns a [SweepLineEvent] representing the minimal move down from [pipBounds] that clears + * the given keep clear areas. + */ + private fun findMinMoveDown( + pipBounds: Rect, + restrictedAreas: Set<Rect>, + unrestrictedAreas: Set<Rect> + ): SweepLineEvent { + val events = mutableListOf<SweepLineEvent>() + val generateEvents: (Boolean) -> (Rect) -> Unit = { unrestricted -> + { area -> + if (pipBounds.intersectsX(area)) { + events.add(SweepLineEvent(true, -area.top, unrestricted)) + events.add(SweepLineEvent(false, -area.bottom, unrestricted)) + } + } + } + + restrictedAreas.forEach(generateEvents(false)) + unrestrictedAreas.forEach(generateEvents(true)) + + val earliestEvent = sweepLineFindEarliestGap( + events, + pipBounds.height() + pipAreaPadding, + -pipBounds.top, + pipBounds.height() + ) + + return earliestEvent.copy(pos = -earliestEvent.pos) + } + + /** + * Takes a list of events representing the starts & ends of occupied segments, and + * returns the earliest event whose position is unoccupied and has [gapSize] distance to the + * next event. + * + * @param events List of [SweepLineEvent] representing occupied segments + * @param gapSize Size of the gap to search for + * @param startPos The position to start the search on. + * Inserts a special event marked with [SweepLineEvent.start]. + * @param startGapSize Used instead of [gapSize] for the start event + */ + private fun sweepLineFindEarliestGap( + events: MutableList<SweepLineEvent>, + gapSize: Int, + startPos: Int, + startGapSize: Int + ): SweepLineEvent { + events.add( + SweepLineEvent( + open = false, + pos = startPos, + unrestricted = true, + start = true + ) + ) + events.sortBy { -it.pos } + + // sweep + var openCount = 0 + var i = 0 + while (i < events.size) { + val event = events[i] + if (!event.start) { + if (event.open) { + openCount++ + } else { + openCount-- + } + } + + if (openCount == 0) { + // check if placement is possible + val candidate = event.pos + if (candidate > startPos) { + i++ + continue + } + + val eventGapSize = if (event.start) startGapSize else gapSize + val nextEvent = events.getOrNull(i + 1) + if (nextEvent == null || nextEvent.pos < candidate - eventGapSize) { + return event + } + } + i++ + } + + return events.last() + } + + private fun shouldTransformFlipX(): Boolean { + return when (pipGravity) { + (Gravity.TOP), (Gravity.TOP or Gravity.CENTER_HORIZONTAL) -> true + (Gravity.TOP or Gravity.LEFT) -> true + (Gravity.LEFT), (Gravity.LEFT or Gravity.CENTER_VERTICAL) -> true + (Gravity.BOTTOM or Gravity.LEFT) -> true + else -> false + } + } + + private fun shouldTransformFlipY(): Boolean { + return when (pipGravity) { + (Gravity.TOP or Gravity.LEFT) -> true + (Gravity.TOP or Gravity.RIGHT) -> true + else -> false + } + } + + private fun shouldTransformRotate(): Boolean { + val horizontalGravity = pipGravity and Gravity.HORIZONTAL_GRAVITY_MASK + val leftOrRight = horizontalGravity == Gravity.LEFT || horizontalGravity == Gravity.RIGHT + + if (leftOrRight) return false + return when (pipGravity and Gravity.VERTICAL_GRAVITY_MASK) { + (Gravity.TOP) -> true + (Gravity.BOTTOM) -> true + else -> false + } + } + + /** + * Transforms the given rect from screen space into the base case space, where the PiP + * anchor is positioned in the bottom right corner or on the right side (for expanded PiP). + * + * @see [fromTransformedSpace] + */ + private fun toTransformedSpace(r: Rect): Rect { + var screenWidth = screenSize.width + var screenHeight = screenSize.height + + val tl = Point(r.left, r.top) + val tr = Point(r.right, r.top) + val br = Point(r.right, r.bottom) + val bl = Point(r.left, r.bottom) + val corners = arrayOf(tl, tr, br, bl) + + // rotate first (CW) + if (shouldTransformRotate()) { + corners.forEach { p -> + val px = p.x + val py = p.y + p.x = py + p.y = -px + p.y += screenWidth // shift back screen into positive quadrant + } + screenWidth = screenSize.height + screenHeight = screenSize.width + } + + // flip second + corners.forEach { + if (shouldTransformFlipX()) it.x = screenWidth - it.x + if (shouldTransformFlipY()) it.y = screenHeight - it.y + } + + val top = corners.minByOrNull { it.y }!!.y + val right = corners.maxByOrNull { it.x }!!.x + val bottom = corners.maxByOrNull { it.y }!!.y + val left = corners.minByOrNull { it.x }!!.x + + return Rect(left, top, right, bottom) + } + + /** + * Transforms the given rect from the base case space, where the PiP anchor is positioned in + * the bottom right corner or on the right side, back into screen space. + * + * @see [toTransformedSpace] + */ + private fun fromTransformedSpace(r: Rect): Rect { + val rotate = shouldTransformRotate() + val transformedScreenWidth = if (rotate) screenSize.height else screenSize.width + val transformedScreenHeight = if (rotate) screenSize.width else screenSize.height + + val tl = Point(r.left, r.top) + val tr = Point(r.right, r.top) + val br = Point(r.right, r.bottom) + val bl = Point(r.left, r.bottom) + val corners = arrayOf(tl, tr, br, bl) + + // flip first + corners.forEach { + if (shouldTransformFlipX()) it.x = transformedScreenWidth - it.x + if (shouldTransformFlipY()) it.y = transformedScreenHeight - it.y + } + + // rotate second (CCW) + if (rotate) { + corners.forEach { p -> + p.y -= screenSize.width // undo shift back screen into positive quadrant + val px = p.x + val py = p.y + p.x = -py + p.y = px + } + } + + val top = corners.minByOrNull { it.y }!!.y + val right = corners.maxByOrNull { it.x }!!.x + val bottom = corners.maxByOrNull { it.y }!!.y + val left = corners.minByOrNull { it.x }!!.x + + return Rect(left, top, right, bottom) + } + + /** PiP anchor bounds in base case for given gravity */ + private fun getNormalPipAnchorBounds(pipSize: Size, movementBounds: Rect): Rect { + var size = pipSize + val rotateCW = shouldTransformRotate() + if (rotateCW) { + size = Size(pipSize.height, pipSize.width) + } + + val pipBounds = Rect() + if (isPipAnchoredToCorner()) { + // bottom right + Gravity.apply( + Gravity.BOTTOM or Gravity.RIGHT, + size.width, + size.height, + movementBounds, + pipBounds + ) + return pipBounds + } else { + // expanded, right side + Gravity.apply(Gravity.RIGHT, size.width, size.height, movementBounds, pipBounds) + return pipBounds + } + } + + private fun isPipAnchoredToCorner(): Boolean { + val left = (pipGravity and Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.LEFT + val right = (pipGravity and Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.RIGHT + val top = (pipGravity and Gravity.VERTICAL_GRAVITY_MASK) == Gravity.TOP + val bottom = (pipGravity and Gravity.VERTICAL_GRAVITY_MASK) == Gravity.BOTTOM + + val horizontal = left || right + val vertical = top || bottom + + return horizontal && vertical + } + + /** + * Adds space around [size] to leave space for decorations that will be drawn around the PiP + */ + private fun addDecors(size: Size): Size { + val bounds = Rect(0, 0, size.width, size.height) + bounds.inset(pipPermanentDecorInsets) + + return Size(bounds.width(), bounds.height()) + } + + /** + * Removes the space that was reserved for permanent decorations around the PiP + * @param bounds the bounds (in screen space) to remove the insets from + */ + private fun removePermanentDecors(bounds: Rect): Rect { + val pipDecorReverseInsets = Insets.subtract(Insets.NONE, pipPermanentDecorInsets) + bounds.inset(pipDecorReverseInsets) + return bounds + } + + private fun Rect.offsetCopy(dx: Int, dy: Int) = Rect(this).apply { offset(dx, dy) } + private fun Rect.intersectsX(other: Rect) = right >= other.left && left <= other.right + private fun Rect.intersectsY(other: Rect) = bottom >= other.top && top <= other.bottom + private fun Rect.intersects(other: Rect) = intersectsX(other) && intersectsY(other) +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuActionButton.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuActionButton.java index 6f7cd82f8da0..a09aab666a31 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuActionButton.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuActionButton.java @@ -16,8 +16,6 @@ package com.android.wm.shell.pip.tv; -import android.animation.Animator; -import android.animation.AnimatorInflater; import android.content.Context; import android.content.res.TypedArray; import android.graphics.drawable.Drawable; @@ -26,7 +24,6 @@ import android.view.LayoutInflater; import android.view.View; import android.widget.ImageView; import android.widget.RelativeLayout; -import android.widget.TextView; import com.android.wm.shell.R; @@ -36,12 +33,8 @@ import com.android.wm.shell.R; */ public class TvPipMenuActionButton extends RelativeLayout implements View.OnClickListener { private final ImageView mIconImageView; - private final ImageView mButtonImageView; - private final TextView mDescriptionTextView; - private Animator mTextFocusGainAnimator; - private Animator mButtonFocusGainAnimator; - private Animator mTextFocusLossAnimator; - private Animator mButtonFocusLossAnimator; + private final View mButtonBackgroundView; + private final View mButtonView; private OnClickListener mOnClickListener; public TvPipMenuActionButton(Context context) { @@ -64,8 +57,8 @@ public class TvPipMenuActionButton extends RelativeLayout implements View.OnClic inflater.inflate(R.layout.tv_pip_menu_action_button, this); mIconImageView = findViewById(R.id.icon); - mButtonImageView = findViewById(R.id.button); - mDescriptionTextView = findViewById(R.id.desc); + mButtonView = findViewById(R.id.button); + mButtonBackgroundView = findViewById(R.id.background); final int[] values = new int[]{android.R.attr.src, android.R.attr.text}; final TypedArray typedArray = context.obtainStyledAttributes(attrs, values, defStyleAttr, @@ -74,45 +67,18 @@ public class TvPipMenuActionButton extends RelativeLayout implements View.OnClic setImageResource(typedArray.getResourceId(0, 0)); final int textResId = typedArray.getResourceId(1, 0); if (textResId != 0) { - setTextAndDescription(getContext().getString(textResId)); + setTextAndDescription(textResId); } - typedArray.recycle(); } @Override - public void onFinishInflate() { - super.onFinishInflate(); - mButtonImageView.setOnFocusChangeListener((v, hasFocus) -> { - if (hasFocus) { - startFocusGainAnimation(); - } else { - startFocusLossAnimation(); - } - }); - - mTextFocusGainAnimator = AnimatorInflater.loadAnimator(getContext(), - R.anim.tv_pip_controls_focus_gain_animation); - mTextFocusGainAnimator.setTarget(mDescriptionTextView); - mButtonFocusGainAnimator = AnimatorInflater.loadAnimator(getContext(), - R.anim.tv_pip_controls_focus_gain_animation); - mButtonFocusGainAnimator.setTarget(mButtonImageView); - - mTextFocusLossAnimator = AnimatorInflater.loadAnimator(getContext(), - R.anim.tv_pip_controls_focus_loss_animation); - mTextFocusLossAnimator.setTarget(mDescriptionTextView); - mButtonFocusLossAnimator = AnimatorInflater.loadAnimator(getContext(), - R.anim.tv_pip_controls_focus_loss_animation); - mButtonFocusLossAnimator.setTarget(mButtonImageView); - } - - @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; - mButtonImageView.setOnClickListener(listener != null ? this : null); + mButtonView.setOnClickListener(listener != null ? this : null); } @Override @@ -143,55 +109,42 @@ public class TvPipMenuActionButton extends RelativeLayout implements View.OnClic * Sets the text for description the with the given string. */ public void setTextAndDescription(CharSequence text) { - mButtonImageView.setContentDescription(text); - mDescriptionTextView.setText(text); - } - - private static void cancelAnimator(Animator animator) { - if (animator.isStarted()) { - animator.cancel(); - } + mButtonView.setContentDescription(text); } /** - * Starts the focus gain animation. + * Sets the text and description with the given string resource id. */ - public void startFocusGainAnimation() { - cancelAnimator(mButtonFocusLossAnimator); - cancelAnimator(mTextFocusLossAnimator); - mTextFocusGainAnimator.start(); - if (mButtonImageView.getAlpha() < 1f) { - // If we had faded out the ripple drawable, run our manual focus change animation. - // See the comment at {@link #startFocusLossAnimation()} for the reason of manual - // animator. - mButtonFocusGainAnimator.start(); - } + public void setTextAndDescription(int resId) { + setTextAndDescription(getContext().getString(resId)); } - /** - * Starts the focus loss animation. - */ - public void startFocusLossAnimation() { - cancelAnimator(mButtonFocusGainAnimator); - cancelAnimator(mTextFocusGainAnimator); - mTextFocusLossAnimator.start(); - if (mButtonImageView.hasFocus()) { - // Button uses ripple that has the default animation for the focus changes. - // However, it doesn't expose the API to fade out while it is focused, so we should - // manually run the fade out animation when PIP controls row loses focus. - mButtonFocusLossAnimator.start(); - } + @Override + public void setEnabled(boolean enabled) { + mButtonView.setEnabled(enabled); } - /** - * Resets to initial state. - */ - public void reset() { - cancelAnimator(mButtonFocusGainAnimator); - cancelAnimator(mTextFocusGainAnimator); - cancelAnimator(mButtonFocusLossAnimator); - cancelAnimator(mTextFocusLossAnimator); - mButtonImageView.setAlpha(1f); - mDescriptionTextView.setAlpha(mButtonImageView.hasFocus() ? 1f : 0f); + @Override + public boolean isEnabled() { + return mButtonView.isEnabled(); + } + + void setIsCustomCloseAction(boolean isCustomCloseAction) { + mIconImageView.setImageTintList( + getResources().getColorStateList( + isCustomCloseAction ? R.color.tv_pip_menu_close_icon + : R.color.tv_pip_menu_icon)); + mButtonBackgroundView.setBackgroundTintList(getResources() + .getColorStateList(isCustomCloseAction ? R.color.tv_pip_menu_close_icon_bg + : R.color.tv_pip_menu_icon_bg)); } + + @Override + public String toString() { + if (mButtonView.getContentDescription() == null) { + return TvPipMenuActionButton.class.getSimpleName(); + } + return mButtonView.getContentDescription().toString(); + } + } 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 ee41b41a743d..4ce45e142c64 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,25 +18,36 @@ package com.android.wm.shell.pip.tv; import static android.view.WindowManager.SHELL_ROOT_LAYER_PIP; +import android.app.ActivityManager; import android.app.RemoteAction; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; -import android.content.pm.ParceledListSlice; +import android.graphics.Insets; +import android.graphics.Matrix; +import android.graphics.Rect; +import android.graphics.RectF; import android.os.Handler; -import android.util.Log; +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 androidx.annotation.Nullable; +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.PipBoundsState; 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. @@ -44,24 +55,44 @@ import java.util.List; 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 PipBoundsState mPipBoundsState; + private final TvPipBoundsState mTvPipBoundsState; private final Handler mMainHandler; + private final int mPipMenuBorderWidth; + private final int mPipEduTextShowDurationMs; + private final int mPipEduTextHeight; private Delegate mDelegate; private SurfaceControl mLeash; - private TvPipMenuView mMenuView; + private TvPipMenuView mPipMenuView; + private View mPipBackgroundView; + + // 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; private final List<RemoteAction> mMediaActions = new ArrayList<>(); private final List<RemoteAction> mAppActions = new ArrayList<>(); + private RemoteAction mCloseAction; + + private SyncRtSurfaceTransactionApplier mApplier; + private SyncRtSurfaceTransactionApplier mBackgroundApplier; + RectF mTmpSourceRectF = new RectF(); + RectF mTmpDestinationRectF = new RectF(); + Matrix mMoveTransform = new Matrix(); + + private final Runnable mCloseEduTextRunnable = this::closeEduText; - public TvPipMenuController(Context context, PipBoundsState pipBoundsState, + public TvPipMenuController(Context context, TvPipBoundsState tvPipBoundsState, SystemWindows systemWindows, PipMediaController pipMediaController, Handler mainHandler) { mContext = context; - mPipBoundsState = pipBoundsState; + mTvPipBoundsState = tvPipBoundsState; mSystemWindows = systemWindows; mMainHandler = mainHandler; @@ -70,18 +101,28 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis final BroadcastReceiver closeSystemDialogsBroadcastReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { - hideMenu(); + closeMenu(); } }; context.registerReceiverForAllUsers(closeSystemDialogsBroadcastReceiver, new IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS), null /* permission */, - mainHandler); + 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) Log.d(TAG, "setDelegate(), delegate=" + delegate); + if (DEBUG) { + 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."); @@ -100,98 +141,256 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis } mLeash = leash; + attachPipMenu(); + } + + private void attachPipMenu() { + if (DEBUG) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: attachPipMenu()", TAG); + } + + if (mPipMenuView != null) { + detachPipMenu(); + } + + attachPipBackgroundView(); attachPipMenuView(); + + mTvPipBoundsState.setPipMenuPermanentDecorInsets(Insets.of(-mPipMenuBorderWidth, + -mPipMenuBorderWidth, -mPipMenuBorderWidth, -mPipMenuBorderWidth)); + mTvPipBoundsState.setPipMenuTemporaryDecorInsets(Insets.of(0, 0, 0, -mPipEduTextHeight)); + mMainHandler.postDelayed(mCloseEduTextRunnable, mPipEduTextShowDurationMs); } private void attachPipMenuView() { - if (DEBUG) Log.d(TAG, "attachPipMenuView()"); + mPipMenuView = new TvPipMenuView(mContext); + mPipMenuView.setListener(this); + setUpViewSurfaceZOrder(mPipMenuView, 1); + addPipMenuViewToSystemWindows(mPipMenuView, MENU_WINDOW_TITLE); + maybeUpdateMenuViewActions(); + } + + private void attachPipBackgroundView() { + mPipBackgroundView = LayoutInflater.from(mContext) + .inflate(R.layout.tv_pip_menu_background, null); + setUpViewSurfaceZOrder(mPipBackgroundView, -1); + addPipMenuViewToSystemWindows(mPipBackgroundView, BACKGROUND_WINDOW_TITLE); + } + + 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) { + } + }); + } + + private void addPipMenuViewToSystemWindows(View v, String title) { + mSystemWindows.addView(v, getPipMenuLayoutParams(title, 0 /* width */, 0 /* height */), + 0 /* displayId */, SHELL_ROOT_LAYER_PIP); + } - if (mMenuView != null) { - detachPipMenuView(); + void notifyPipAnimating(boolean animating) { + mPipMenuView.setEduTextActive(!animating); + if (!animating) { + mPipMenuView.onPipTransitionFinished(); } + } - mMenuView = new TvPipMenuView(mContext); - mMenuView.setListener(this); - mSystemWindows.addView(mMenuView, - getPipMenuLayoutParams(MENU_WINDOW_TITLE, 0 /* width */, 0 /* height */), - 0, SHELL_ROOT_LAYER_PIP); + void showMovementMenuOnly() { + if (DEBUG) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: showMovementMenuOnly()", TAG); + } + setInMoveMode(true); + mCloseAfterExitMoveMenu = true; + showMenuInternal(); } @Override public void showMenu() { - if (DEBUG) Log.d(TAG, "showMenu()"); - - if (mMenuView != null) { - mSystemWindows.updateViewLayout(mMenuView, getPipMenuLayoutParams(MENU_WINDOW_TITLE, - mPipBoundsState.getDisplayBounds().width(), - mPipBoundsState.getDisplayBounds().height())); - maybeUpdateMenuViewActions(); - mMenuView.show(); - - // By default, SystemWindows views are above everything else. - // Set the relative z-order so the menu is below PiP. - if (mMenuView.getWindowSurfaceControl() != null && mLeash != null) { - SurfaceControl.Transaction t = new SurfaceControl.Transaction(); - t.setRelativeLayer(mMenuView.getWindowSurfaceControl(), mLeash, -1); - t.apply(); - } + 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()); + } + + void onPipTransitionStarted(Rect finishBounds) { + if (mPipMenuView != null) { + mPipMenuView.onPipTransitionStarted(finishBounds); + } + } + + private void maybeCloseEduText() { + if (mMainHandler.hasCallbacks(mCloseEduTextRunnable)) { + mMainHandler.removeCallbacks(mCloseEduTextRunnable); + mCloseEduTextRunnable.run(); + } + } + + 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()); } - void hideMenu() { - hideMenu(true); + private Rect calculateMenuSurfaceBounds(Rect pipBounds) { + return mPipMenuView.getPipMenuContainerBounds(pipBounds); } - void hideMenu(boolean movePipWindow) { - if (DEBUG) Log.d(TAG, "hideMenu(), movePipWindow=" + movePipWindow); + void closeMenu() { + if (DEBUG) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: closeMenu()", TAG); + } - if (!isMenuVisible()) { + if (mPipMenuView == null) { return; } - mMenuView.hide(); - if (movePipWindow) { - mDelegate.movePipToNormalPosition(); + 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 detach() { - hideMenu(); - detachPipMenuView(); - mLeash = null; + 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()); } - private void detachPipMenuView() { - if (DEBUG) Log.d(TAG, "detachPipMenuView()"); + @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; + } - if (mMenuView == null) { - return; + @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; + } - mSystemWindows.removeView(mMenuView); - mMenuView = null; + @Override + public void detach() { + closeMenu(); + mMainHandler.removeCallbacks(mCloseEduTextRunnable); + detachPipMenu(); + mLeash = null; } @Override - public void setAppActions(ParceledListSlice<RemoteAction> actions) { - if (DEBUG) Log.d(TAG, "setAppActions()"); - updateAdditionalActionsList(mAppActions, actions.getList()); + 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) Log.d(TAG, "onMediaActionsChanged()"); - updateAdditionalActionsList(mMediaActions, 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) { + 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()) { + if (number == 0 && destination.isEmpty() && Objects.equals(closeAction, mCloseAction)) { // Nothing changed. return; } + mCloseAction = closeAction; + destination.clear(); if (number > 0) { destination.addAll(source); @@ -200,24 +399,201 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis } private void maybeUpdateMenuViewActions() { - if (mMenuView == null) { + if (mPipMenuView == null) { return; } if (!mAppActions.isEmpty()) { - mMenuView.setAdditionalActions(mAppActions, mMainHandler); + mPipMenuView.setAdditionalActions(mAppActions, mCloseAction, mMainHandler); } else { - mMenuView.setAdditionalActions(mMediaActions, mMainHandler); + mPipMenuView.setAdditionalActions(mMediaActions, mCloseAction, mMainHandler); } } @Override public boolean isMenuVisible() { - return mMenuView != null && mMenuView.isVisible(); + return true; + } + + /** + * Does an immediate window crop of the PiP menu. + */ + @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()) { + return; + } + + if (!maybeCreateSyncApplier()) { + 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); + } + } + + private SurfaceControl getSurfaceControl(View v) { + return mSystemWindows.getViewSurface(v); + } + + @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()); + } + + if (pipDestBounds.isEmpty()) { + if (transaction == null && DEBUG) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: no transaction given", TAG); + } + return; + } + if (!maybeCreateSyncApplier()) { + 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) { + 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; + } + + 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()); + } + mSystemWindows.updateViewLayout(mPipBackgroundView, + getPipMenuLayoutParams(BACKGROUND_WINDOW_TITLE, menuBounds.width(), + menuBounds.height())); + mSystemWindows.updateViewLayout(mPipMenuView, + getPipMenuLayoutParams(MENU_WINDOW_TITLE, menuBounds.width(), + menuBounds.height())); + + if (mPipMenuView != null) { + mPipMenuView.updateBounds(destinationBounds); + } + } + + @Override + public void onFocusTaskChanged(ActivityManager.RunningTaskInfo taskInfo) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: onFocusTaskChanged", TAG); } @Override public void onBackPress() { - hideMenu(); + if (!onExitMoveMode()) { + closeMenu(); + } } @Override @@ -230,9 +606,67 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis mDelegate.movePipToFullscreen(); } + @Override + public void onToggleExpandedMode() { + mDelegate.togglePipExpansion(); + } + interface Delegate { - void movePipToNormalPosition(); 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); + } + + try { + WindowManagerGlobal.getWindowSession().grantEmbeddedWindowFocus(null /* window */, + mSystemWindows.getFocusGrantToken(mPipMenuView), grantFocus); + } catch (Exception e) { + ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: Unable to update focus, %s", TAG, e); + } + } + + private class PipMenuSurfaceChangedCallback implements ViewRootImpl.SurfaceChangedCallback { + private final View mView; + private final int mZOrder; + + PipMenuSurfaceChangedCallback(View v, int zOrder) { + mView = v; + mZOrder = zOrder; + } + + @Override + public void surfaceCreated(SurfaceControl.Transaction t) { + final SurfaceControl sc = getSurfaceControl(mView); + if (sc != null) { + t.setRelativeLayer(sc, mLeash, mZOrder); + } + } + + @Override + public void surfaceReplaced(SurfaceControl.Transaction t) { + } + + @Override + public void surfaceDestroyed() { + } + } } 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 d6cd9ea13ca1..320c05c4a415 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 @@ -16,53 +16,99 @@ package com.android.wm.shell.pip.tv; -import static android.animation.AnimatorInflater.loadAnimator; import static android.view.KeyEvent.ACTION_UP; import static android.view.KeyEvent.KEYCODE_BACK; - -import android.animation.Animator; +import static android.view.KeyEvent.KEYCODE_DPAD_CENTER; +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 android.view.KeyEvent.KEYCODE_ENTER; + +import android.animation.ValueAnimator; import android.app.PendingIntent; import android.app.RemoteAction; import android.content.Context; -import android.graphics.Color; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; import android.os.Handler; -import android.os.Looper; +import android.text.Annotation; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.SpannedString; import android.util.AttributeSet; -import android.util.Log; +import android.view.Gravity; import android.view.KeyEvent; -import android.view.LayoutInflater; import android.view.SurfaceControl; import android.view.View; +import android.view.ViewGroup; import android.view.ViewRootImpl; -import android.view.WindowManagerGlobal; 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.wm.shell.R; +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 2 ever-present Pip Menu - * actions: Fullscreen 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 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. */ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { private static final String TAG = "TvPipMenuView"; private static final boolean DEBUG = TvPipController.DEBUG; - private static final float DISABLED_ACTION_ALPHA = 0.54f; + private static final int FIRST_CUSTOM_ACTION_POSITION = 3; - private final Animator mFadeInAnimation; - private final Animator mFadeOutAnimation; - @Nullable private Listener mListener; + @Nullable + private Listener mListener; private final LinearLayout mActionButtonsContainer; + private final View mMenuFrameView; private final List<TvPipMenuActionButton> mAdditionalButtons = new ArrayList<>(); + private final View mPipFrameView; + private final View mPipView; + private final TextView mEduTextView; + private final View mEduTextContainerView; + private final int mPipMenuOuterSpace; + private final int mPipMenuBorderWidth; + private final int mEduTextFadeExitAnimationDurationMs; + private final int mEduTextSlideExitAnimationDurationMs; + private int mEduTextHeight; + + private final ImageView mArrowUp; + private final ImageView mArrowRight; + private final ImageView mArrowDown; + private final ImageView mArrowLeft; + + 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 boolean mSwitchingOrientation; + + private final int mPipMenuFadeAnimationDuration; + private final int mResizeAnimationDuration; public TvPipMenuView(@NonNull Context context) { this(context, null); @@ -85,67 +131,433 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { mActionButtonsContainer = findViewById(R.id.tv_pip_menu_action_buttons); mActionButtonsContainer.findViewById(R.id.tv_pip_menu_fullscreen_button) .setOnClickListener(this); - mActionButtonsContainer.findViewById(R.id.tv_pip_menu_close_button) + + mCloseButton = mActionButtonsContainer.findViewById(R.id.tv_pip_menu_close_button); + mCloseButton.setOnClickListener(this); + mCloseButton.setIsCustomCloseAction(true); + + mActionButtonsContainer.findViewById(R.id.tv_pip_menu_move_button) .setOnClickListener(this); + mExpandButton = findViewById(R.id.tv_pip_menu_expand_button); + mExpandButton.setOnClickListener(this); + + mScrollView = findViewById(R.id.tv_pip_menu_scroll); + mHorizontalScrollView = findViewById(R.id.tv_pip_menu_horizontal_scroll); + + mMenuFrameView = findViewById(R.id.tv_pip_menu_frame); + mPipFrameView = findViewById(R.id.tv_pip_border); + mPipView = findViewById(R.id.tv_pip); + + 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); + + mResizeAnimationDuration = context.getResources().getInteger( + R.integer.config_pipResizeAnimationDuration); + mPipMenuFadeAnimationDuration = context.getResources() + .getInteger(R.integer.pip_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); + } + + 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 onPipTransitionStarted(Rect finishBounds) { + // Fade out content by fading in view on top. + if (mCurrentPipBounds != null && finishBounds != null) { + boolean ratioChanged = PipUtils.aspectRatioChanged( + mCurrentPipBounds.width() / (float) mCurrentPipBounds.height(), + finishBounds.width() / (float) finishBounds.height()); + if (ratioChanged) { + mPipView.animate() + .alpha(1f) + .setInterpolator(TvPipInterpolators.EXIT) + .setDuration(mResizeAnimationDuration / 2) + .start(); + } + } + + // Update buttons. + final boolean vertical = finishBounds.height() > finishBounds.width(); + final boolean orientationChanged = + vertical != (mActionButtonsContainer.getOrientation() == LinearLayout.VERTICAL); + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: onPipTransitionStarted(), orientation changed %b", TAG, orientationChanged); + if (!orientationChanged) { + return; + } + + if (mButtonMenuIsVisible) { + mSwitchingOrientation = true; + mActionButtonsContainer.animate() + .alpha(0) + .setInterpolator(TvPipInterpolators.EXIT) + .setDuration(mResizeAnimationDuration / 2) + .withEndAction(() -> { + changeButtonScrollOrientation(finishBounds); + updateButtonGravity(finishBounds); + // Only make buttons visible again in onPipTransitionFinished to keep in + // sync with PiP content alpha animation. + }); + } else { + changeButtonScrollOrientation(finishBounds); + updateButtonGravity(finishBounds); + } + } + + void onPipTransitionFinished() { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: onPipTransitionFinished()", TAG); + + // Fade in content by fading out view on top. + mPipView.animate() + .alpha(0f) + .setDuration(mResizeAnimationDuration / 2) + .setInterpolator(TvPipInterpolators.ENTER) + .start(); + + // Update buttons. + if (mSwitchingOrientation) { + mActionButtonsContainer.animate() + .alpha(1) + .setInterpolator(TvPipInterpolators.ENTER) + .setDuration(mResizeAnimationDuration / 2); + } else { + refocusPreviousButton(); + } + mSwitchingOrientation = false; + } + + /** + * Also updates the button gravity. + */ + void updateBounds(Rect updatedBounds) { + 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); + } + + 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 + * the pip window (e.g. edu text). + * TvPipMenuView needs to account for this so that it can draw a white border around the whole + * pip menu when it gains focus. + */ + private void updatePipFrameBounds() { + final ViewGroup.LayoutParams pipFrameParams = mPipFrameView.getLayoutParams(); + if (pipFrameParams != null) { + pipFrameParams.width = mCurrentPipBounds.width() + 2 * mPipMenuBorderWidth; + pipFrameParams.height = mCurrentPipBounds.height() + 2 * mPipMenuBorderWidth; + mPipFrameView.setLayoutParams(pipFrameParams); + } + + final ViewGroup.LayoutParams pipViewParams = mPipView.getLayoutParams(); + if (pipViewParams != null) { + pipViewParams.width = mCurrentPipBounds.width(); + pipViewParams.height = mCurrentPipBounds.height(); + mPipView.setLayoutParams(pipViewParams); + } - mFadeInAnimation = loadAnimator(mContext, R.anim.tv_pip_menu_fade_in_animation); - mFadeInAnimation.setTarget(mActionButtonsContainer); - mFadeOutAnimation = loadAnimator(mContext, R.anim.tv_pip_menu_fade_out_animation); - mFadeOutAnimation.setTarget(mActionButtonsContainer); } void setListener(@Nullable Listener listener) { mListener = listener; } - void show() { - if (DEBUG) Log.d(TAG, "show()"); + void setExpandedModeEnabled(boolean enabled) { + mExpandButton.setVisibility(enabled ? VISIBLE : GONE); + } + + void setIsExpanded(boolean expanded) { + if (DEBUG) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: setIsExpanded, expanded: %b", TAG, expanded); + } + mExpandButton.setImageResource( + expanded ? R.drawable.pip_ic_collapse : R.drawable.pip_ic_expand); + mExpandButton.setTextAndDescription( + expanded ? R.string.pip_collapse : R.string.pip_expand); + } - mFadeInAnimation.start(); - setAlpha(1.0f); - grantWindowFocus(true); + /** + * @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); + setFrameHighlighted(true); } - void hide() { - if (DEBUG) Log.d(TAG, "hide()"); + void showButtonsMenu() { + if (DEBUG) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: showButtonsMenu()", TAG); + } - mFadeOutAnimation.start(); - setAlpha(0.0f); - grantWindowFocus(false); + mButtonMenuIsVisible = true; + mMoveMenuIsVisible = false; + showButtonsMenu(true); + hideMovementHints(); + setFrameHighlighted(true); + + // Always focus on the first button when opening the menu, except directly after moving. + if (mFocusedButton == null) { + // Focus on first button (there is a Space at position 0) + mFocusedButton = mActionButtonsContainer.getChildAt(1); + // Reset scroll position. + mScrollView.scrollTo(0, 0); + mHorizontalScrollView.scrollTo( + isLayoutRtl() ? mActionButtonsContainer.getWidth() : 0, 0); + } + refocusPreviousButton(); } - boolean isVisible() { - return getAlpha() == 1.0f; + /** + * Hides all menu views, including the menu frame. + */ + void hideAllUserControls() { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: hideAllUserControls()", TAG); + mFocusedButton = null; + mButtonMenuIsVisible = false; + mMoveMenuIsVisible = false; + showButtonsMenu(false); + hideMovementHints(); + setFrameHighlighted(false); } - private void grantWindowFocus(boolean grantFocus) { - if (DEBUG) Log.d(TAG, "grantWindowFocus(" + grantFocus + ")"); + @Override + public void onWindowFocusChanged(boolean hasWindowFocus) { + super.onWindowFocusChanged(hasWindowFocus); + if (!hasWindowFocus) { + hideAllUserControls(); + } + } - try { - WindowManagerGlobal.getWindowSession().grantEmbeddedWindowFocus(null /* window */, - getViewRootImpl().getInputToken(), grantFocus); - } catch (Exception e) { - Log.e(TAG, "Unable to update focus", e); + private void animateAlphaTo(float alpha, View view) { + if (view.getAlpha() == alpha) { + return; } + view.animate() + .alpha(alpha) + .setInterpolator(alpha == 0f ? TvPipInterpolators.EXIT : TvPipInterpolators.ENTER) + .setDuration(mPipMenuFadeAnimationDuration) + .withStartAction(() -> { + if (alpha != 0) { + view.setVisibility(VISIBLE); + } + }) + .withEndAction(() -> { + if (alpha == 0) { + view.setVisibility(GONE); + } + }); } - void setAdditionalActions(List<RemoteAction> actions, Handler mainHandler) { - if (DEBUG) Log.d(TAG, "setAdditionalActions()"); + /** + * 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) { - final LayoutInflater layoutInflater = LayoutInflater.from(mContext); - // Add buttons until we have enough to display all of the actions. + // Add buttons until we have enough to display all the actions. while (actionsNumber > buttonsNumber) { - final TvPipMenuActionButton button = (TvPipMenuActionButton) layoutInflater.inflate( - R.layout.tv_pip_menu_additional_action_button, mActionButtonsContainer, - false); + TvPipMenuActionButton button = new TvPipMenuActionButton(mContext); button.setOnClickListener(this); - mActionButtonsContainer.addView(button); + mActionButtonsContainer.addView(button, + FIRST_CUSTOM_ACTION_POSITION + buttonsNumber); mAdditionalButtons.add(button); buttonsNumber++; @@ -165,17 +577,32 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { for (int index = 0; index < actionsNumber; index++) { final RemoteAction action = actions.get(index); final TvPipMenuActionButton button = mAdditionalButtons.get(index); - button.setVisibility(View.VISIBLE); // Ensure the button is visible. - button.setTextAndDescription(action.getContentDescription()); - button.setEnabled(action.isEnabled()); - button.setAlpha(action.isEnabled() ? 1f : DISABLED_ACTION_ALPHA); - button.setTag(action); - action.getIcon().loadDrawableAsync(mContext, drawable -> { - drawable.setTint(Color.WHITE); - button.setImageDrawable(drawable); - }, mainHandler); + // 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 @@ -198,8 +625,12 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { 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(); @@ -207,27 +638,118 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { try { action.getActionIntent().send(); } catch (PendingIntent.CanceledException e) { - Log.w(TAG, "Failed to send action", e); + ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: Failed to send action, %s", TAG, e); } } else { - Log.w(TAG, "RemoteAction is null"); + ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: RemoteAction is null", TAG); } } } @Override public boolean dispatchKeyEvent(KeyEvent event) { - if (event.getAction() == ACTION_UP && event.getKeyCode() == KEYCODE_BACK - && mListener != null) { - mListener.onBackPress(); - return true; + if (mListener != null && event.getAction() == ACTION_UP) { + if (!mMoveMenuIsVisible) { + mFocusedButton = mActionButtonsContainer.getFocusedChild(); + } + + switch (event.getKeyCode()) { + case KEYCODE_BACK: + mListener.onBackPress(); + return true; + case KEYCODE_DPAD_UP: + case KEYCODE_DPAD_DOWN: + case KEYCODE_DPAD_LEFT: + case KEYCODE_DPAD_RIGHT: + return mListener.onPipMovement(event.getKeyCode()) || super.dispatchKeyEvent( + event); + case KEYCODE_ENTER: + case KEYCODE_DPAD_CENTER: + return mListener.onExitMoveMode() || super.dispatchKeyEvent(event); + default: + break; + } } return super.dispatchKeyEvent(event); } + /** + * 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)); + } + + 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 boolean checkGravity(int gravity, int feature) { + return (gravity & feature) == feature; + } + + /** + * 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); + } + animateAlphaTo(0, mArrowUp); + animateAlphaTo(0, mArrowRight); + animateAlphaTo(0, mArrowDown); + animateAlphaTo(0, mArrowLeft); + } + + /** + * 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 setFrameHighlighted(boolean highlighted) { + mMenuFrameView.setActivated(highlighted); + } + interface Listener { + void onBackPress(); + + void onEnterMoveMode(); + + /** + * Called when a button for exiting move mode was pressed. + * + * @return true if the event was handled or false if the key event should be handled by the + * next receiver. + */ + boolean onExitMoveMode(); + + /** + * @return whether pip movement was handled. + */ + boolean onPipMovement(int keycode); + void onCloseButtonClick(); + void onFullscreenButtonClick(); + + void onToggleExpandedMode(); } } 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 dd7e29451ffc..61a609d9755e 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,36 +16,47 @@ 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.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; -import android.media.MediaMetadata; +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.os.UserHandle; import android.text.TextUtils; -import android.util.Log; import com.android.internal.messages.nano.SystemMessageProto.SystemMessage; +import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.util.ImageUtils; import com.android.wm.shell.R; import com.android.wm.shell.pip.PipMediaController; +import com.android.wm.shell.pip.PipParamsChangedForwarder; +import com.android.wm.shell.pip.PipUtils; +import com.android.wm.shell.protolog.ShellProtoLogGroup; -import java.util.Objects; +import java.util.ArrayList; +import java.util.List; /** - * A notification that informs users that PIP is running and also provides PIP controls. - * <p>Once it's created, it will manage the PIP notification UI by itself except for handling - * configuration changes. + * A notification that informs users that PiP is running and also provides PiP controls. + * <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"; - private static final boolean DEBUG = TvPipController.DEBUG; // Referenced in com.android.systemui.util.NotificationChannels. public static final String NOTIFICATION_CHANNEL = "TVPIP"; @@ -56,6 +67,12 @@ public class TvPipNotificationController { "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; @@ -64,41 +81,88 @@ public class TvPipNotificationController { 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 MediaSession.Token mMediaSessionToken; + /** Package name for the application that owns PiP window. */ private String mPackageName; - private boolean mNotified; - private String mMediaTitle; - private Bitmap mArt; + + private boolean mIsNotificationShown; + private String mPipTitle; + private String mPipSubtitle; + + private Bitmap mActivityIcon; public TvPipNotificationController(Context context, PipMediaController pipMediaController, + PipParamsChangedForwarder pipParamsChangedForwarder, TvPipBoundsState tvPipBoundsState, Handler mainHandler) { mContext = context; mPackageManager = context.getPackageManager(); mNotificationManager = context.getSystemService(NotificationManager.class); mMainHandler = mainHandler; + mTvPipBoundsState = tvPipBoundsState; mNotificationBuilder = new Notification.Builder(context, NOTIFICATION_CHANNEL) .setLocalOnly(true) - .setOngoing(false) + .setOngoing(true) .setCategory(Notification.CATEGORY_SYSTEM) .setShowWhen(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(); - pipMediaController.addMetadataListener(this::onMediaMetadataChanged); + 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(); + } + + @Override + public void onSubtitleChanged(String subtitle) { + mPipSubtitle = subtitle; + updateNotificationContent(); + } + }); onConfigurationChanged(context); } void setDelegate(Delegate delegate) { - if (DEBUG) Log.d(TAG, "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."); @@ -111,90 +175,181 @@ public class TvPipNotificationController { } 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."); } + mIsNotificationShown = true; mPackageName = packageName; - update(); + mActivityIcon = getActivityIcon(); mActionBroadcastReceiver.register(); + + updateNotificationContent(); } void dismiss() { - mNotificationManager.cancel(NOTIFICATION_TAG, SystemMessage.NOTE_TV_PIP); - mNotified = false; + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: dismiss()", TAG); + + mIsNotificationShown = false; mPackageName = null; mActionBroadcastReceiver.unregister(); + + mNotificationManager.cancel(NOTIFICATION_TAG, SystemMessage.NOTE_TV_PIP); } - private void onMediaMetadataChanged(MediaMetadata metadata) { - if (updateMediaControllerMetadata(metadata) && mNotified) { - // update notification - update(); + 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); } } - /** - * Called by {@link PipController} when the configuration is changed. - */ - void onConfigurationChanged(Context context) { - mDefaultTitle = context.getResources().getString(R.string.pip_notification_unknown_title); - if (mNotified) { - // Update the notification. - update(); + 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 update() { - mNotified = true; - mNotificationBuilder - .setWhen(System.currentTimeMillis()) - .setContentTitle(getNotificationTitle()); - if (mArt != null) { - mNotificationBuilder.setStyle(new Notification.BigPictureStyle() - .bigPicture(mArt)); - } else { - mNotificationBuilder.setStyle(null); + 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); } - mNotificationManager.notify(NOTIFICATION_TAG, SystemMessage.NOTE_TV_PIP, - mNotificationBuilder.build()); + builder.setSemanticAction(semanticAction); + builder.setContextual(true); + return builder.build(); } - private boolean updateMediaControllerMetadata(MediaMetadata metadata) { - String title = null; - Bitmap art = null; - if (metadata != null) { - title = metadata.getString(MediaMetadata.METADATA_KEY_DISPLAY_TITLE); - if (TextUtils.isEmpty(title)) { - title = metadata.getString(MediaMetadata.METADATA_KEY_TITLE); - } - art = metadata.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART); - if (art == null) { - art = metadata.getBitmap(MediaMetadata.METADATA_KEY_ART); + 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]); + } - if (TextUtils.equals(title, mMediaTitle) && Objects.equals(art, mArt)) { - return false; + 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); + } - mMediaTitle = title; - mArt = art; + private Notification.Action getMoveAction() { + return createSystemAction(R.drawable.pip_ic_move_white, R.string.pip_move, + ACTION_MOVE_PIP); + } - return true; + /** + * 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 String getNotificationTitle() { - if (!TextUtils.isEmpty(mMediaTitle)) { - return mMediaTitle; + 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()); + } + + mNotificationBuilder + .setWhen(System.currentTimeMillis()) + .setContentTitle(getNotificationTitle()) + .setContentText(mPipSubtitle) + .setSubText(getApplicationLabel(mPackageName)) + .setActions(actions); + setPipIcon(); + + Bundle extras = new Bundle(); + extras.putParcelable(Notification.EXTRA_MEDIA_SESSION, mMediaSessionToken); + mNotificationBuilder.setExtras(extras); + + // 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))); + mNotificationManager.notify(NOTIFICATION_TAG, SystemMessage.NOTE_TV_PIP, + mNotificationBuilder.build()); + } + + private String getNotificationTitle() { + if (!TextUtils.isEmpty(mPipTitle)) { + return mPipTitle; + } final String applicationTitle = getApplicationLabel(mPackageName); if (!TextUtils.isEmpty(applicationTitle)) { return applicationTitle; } - return mDefaultTitle; } @@ -207,10 +362,37 @@ public class TvPipNotificationController { } } + private void setPipIcon() { + if (mActivityIcon != null) { + mNotificationBuilder.setLargeIcon(mActivityIcon); + return; + } + // Fallback: Picture-in-Picture icon + mNotificationBuilder.setLargeIcon(Icon.createWithResource(mContext, R.drawable.pip_icon)); + } + + private Bitmap getActivityIcon() { + if (mContext == null) return null; + ComponentName componentName = PipUtils.getTopPipActivity(mContext).first; + if (componentName == null) return null; + + Drawable drawable; + try { + drawable = mPackageManager.getActivityIcon(componentName); + } catch (PackageManager.NameNotFoundException e) { + return null; + } + int width = mContext.getResources().getDimensionPixelSize( + android.R.dimen.notification_large_icon_width); + int height = mContext.getResources().getDimensionPixelSize( + android.R.dimen.notification_large_icon_height); + return ImageUtils.buildScaledBitmap(drawable, width, height, /* allowUpscaling */ true); + } + private static PendingIntent createPendingIntent(Context context, String action) { return PendingIntent.getBroadcast(context, 0, new Intent(action).setPackage(context.getPackageName()), - PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE); + PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); } private class ActionBroadcastReceiver extends BroadcastReceiver { @@ -219,6 +401,9 @@ public class TvPipNotificationController { 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; @@ -240,18 +425,32 @@ public class TvPipNotificationController { @Override public void onReceive(Context context, Intent intent) { final String action = intent.getAction(); - if (DEBUG) Log.d(TAG, "on(Broadcast)Receive(), action=" + action); + 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(); } } } 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/TvPipTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTaskOrganizer.java new file mode 100644 index 000000000000..42fd1aab44f8 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTaskOrganizer.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.app.PictureInPictureParams; +import android.content.Context; + +import androidx.annotation.NonNull; + +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.common.DisplayController; +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.SyncTransactionQueue; +import com.android.wm.shell.pip.PipAnimationController; +import com.android.wm.shell.pip.PipBoundsAlgorithm; +import com.android.wm.shell.pip.PipBoundsState; +import com.android.wm.shell.pip.PipMenuController; +import com.android.wm.shell.pip.PipParamsChangedForwarder; +import com.android.wm.shell.pip.PipSurfaceTransactionHelper; +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.PipUtils; +import com.android.wm.shell.splitscreen.SplitScreenController; + +import java.util.Objects; +import java.util.Optional; + +/** + * TV specific changes to the PipTaskOrganizer. + */ +public class TvPipTaskOrganizer extends PipTaskOrganizer { + + public TvPipTaskOrganizer(Context context, + @NonNull SyncTransactionQueue syncTransactionQueue, + @NonNull PipTransitionState pipTransitionState, + @NonNull PipBoundsState pipBoundsState, + @NonNull PipBoundsAlgorithm boundsHandler, + @NonNull PipMenuController pipMenuController, + @NonNull PipAnimationController pipAnimationController, + @NonNull PipSurfaceTransactionHelper surfaceTransactionHelper, + @NonNull PipTransitionController pipTransitionController, + @NonNull PipParamsChangedForwarder pipParamsChangedForwarder, + Optional<SplitScreenController> splitScreenOptional, + @NonNull DisplayController displayController, + @NonNull PipUiEventLogger pipUiEventLogger, + @NonNull ShellTaskOrganizer shellTaskOrganizer, + ShellExecutor mainExecutor) { + super(context, syncTransactionQueue, pipTransitionState, pipBoundsState, boundsHandler, + pipMenuController, pipAnimationController, surfaceTransactionHelper, + pipTransitionController, pipParamsChangedForwarder, splitScreenOptional, + displayController, pipUiEventLogger, shellTaskOrganizer, mainExecutor); + } + + @Override + protected void applyNewPictureInPictureParams(@NonNull PictureInPictureParams params) { + super.applyNewPictureInPictureParams(params); + if (PipUtils.aspectRatioChanged(params.getExpandedAspectRatioFloat(), + mPictureInPictureParams.getExpandedAspectRatioFloat())) { + mPipParamsChangedForwarder.notifyExpandedAspectRatioChanged( + params.getExpandedAspectRatioFloat()); + } + if (!Objects.equals(params.getTitle(), mPictureInPictureParams.getTitle())) { + mPipParamsChangedForwarder.notifyTitleChanged(params.getTitle()); + } + if (!Objects.equals(params.getSubtitle(), mPictureInPictureParams.getSubtitle())) { + mPipParamsChangedForwarder.notifySubtitleChanged(params.getSubtitle()); + } + } +} 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 551476dc9d54..5062cc436461 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 @@ -29,7 +29,6 @@ import androidx.annotation.Nullable; import com.android.wm.shell.ShellTaskOrganizer; 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.PipMenuController; import com.android.wm.shell.pip.PipTransitionController; @@ -42,11 +41,11 @@ import com.android.wm.shell.transition.Transitions; public class TvPipTransition extends PipTransitionController { public TvPipTransition(PipBoundsState pipBoundsState, PipMenuController pipMenuController, - PipBoundsAlgorithm pipBoundsAlgorithm, + TvPipBoundsAlgorithm tvPipBoundsAlgorithm, PipAnimationController pipAnimationController, Transitions transitions, @NonNull ShellTaskOrganizer shellTaskOrganizer) { - super(pipBoundsState, pipMenuController, pipBoundsAlgorithm, pipAnimationController, + super(pipBoundsState, pipMenuController, tvPipBoundsAlgorithm, pipAnimationController, transitions, shellTaskOrganizer); } 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 963a3dc70262..d04c34916256 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 @@ -32,6 +32,16 @@ public enum ShellProtoLogGroup implements IProtoLogGroup { Consts.TAG_WM_SHELL), WM_SHELL_DRAG_AND_DROP(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false, Consts.TAG_WM_SHELL), + WM_SHELL_STARTING_WINDOW(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false, + Consts.TAG_WM_STARTING_WINDOW), + WM_SHELL_BACK_PREVIEW(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, true, + "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, + Consts.TAG_WM_SHELL), TEST_GROUP(true, true, false, "WindowManagerShellProtoLogTest"); private final boolean mEnabled; @@ -91,6 +101,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 boolean ENABLE_DEBUG = true; private static final boolean ENABLE_LOG_TO_PROTO_DEBUG = true; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java index 338c944f7eec..c166178e9bbd 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 @@ -34,6 +34,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; +import com.android.internal.protolog.common.ProtoLog; import com.android.wm.shell.common.RemoteCallable; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SingleInstanceRemoteListener; @@ -41,6 +42,7 @@ 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.protolog.ShellProtoLogGroup; import com.android.wm.shell.util.GroupedRecentTaskInfo; import com.android.wm.shell.util.StagedSplitBounds; @@ -128,6 +130,8 @@ public class RecentTasksController implements TaskStackListenerCallback, mTaskSplitBoundsMap.put(taskId1, splitBounds); mTaskSplitBoundsMap.put(taskId2, splitBounds); notifyRecentTasksChanged(); + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENT_TASKS, "Add split pair: %d, %d, %s", + taskId1, taskId2, splitBounds); } /** @@ -141,6 +145,8 @@ public class RecentTasksController implements TaskStackListenerCallback, mTaskSplitBoundsMap.remove(taskId); mTaskSplitBoundsMap.remove(pairedTaskId); notifyRecentTasksChanged(); + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENT_TASKS, "Remove split pair: %d, %d", + taskId, pairedTaskId); } } @@ -182,6 +188,7 @@ public class RecentTasksController implements TaskStackListenerCallback, @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(); } 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 3cfa541c1c86..51921e747f1a 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 @@ -42,11 +42,6 @@ interface ISplitScreen { 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; @@ -89,16 +84,28 @@ interface ISplitScreen { /** * 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 mainTaskId, in Bundle mainOptions, + int sideTaskId, in Bundle sideOptions, int sidePosition, + float splitRatio, in RemoteAnimationAdapter adapter) = 11; + + /** + * Start 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; /** * 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; + RemoteAnimationTarget[] onGoingToRecentsLegacy(in RemoteAnimationTarget[] appTargets) = 13; + + /** + * Blocking call that notifies and gets additional split-screen targets when entering + * recents (for example: the dividerBar). Different than the method above in that this one + * does not expect split to currently be running. + */ + RemoteAnimationTarget[] onStartingSplitLegacy(in RemoteAnimationTarget[] appTargets) = 14; } 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 082fe9205be8..ae5e075c4d3f 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 @@ -18,7 +18,6 @@ package com.android.wm.shell.splitscreen; import android.annotation.Nullable; import android.content.Context; -import android.graphics.Rect; import android.view.SurfaceSession; import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; @@ -49,14 +48,10 @@ class MainStage extends StageTaskListener { return mIsActive; } - void activate(Rect rootBounds, WindowContainerTransaction wct, boolean includingTopTask) { + void activate(WindowContainerTransaction wct, boolean includingTopTask) { if (mIsActive) return; final WindowContainerToken rootToken = mRootTaskInfo.token; - wct.setBounds(rootToken, rootBounds) - // 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 */); if (includingTopTask) { wct.reparentTasks( null /* currentParent */, @@ -85,9 +80,6 @@ class MainStage extends StageTaskListener { 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 */); + 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 122fc9f5f780..d55619f5e5ed 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 @@ -45,9 +45,6 @@ class SideStage extends StageTaskListener { } 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, 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 a91dfe1c13e2..448773ae9ea2 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 @@ -76,12 +76,6 @@ public interface SplitScreen { } /** - * 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. */ @@ -90,9 +84,6 @@ public interface SplitScreen { /** Called when device waking up finished. */ void onFinishedWakingUp(); - /** Called when device going to sleep finished. */ - void onFinishedGoingToSleep(); - /** 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 4c77f6a7e00d..31b510c38457 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,19 +18,23 @@ package com.android.wm.shell.splitscreen; import static android.app.ActivityManager.START_SUCCESS; import static android.app.ActivityManager.START_TASK_TO_FRONT; +import static android.content.Intent.FLAG_ACTIVITY_NO_USER_ACTION; 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.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED; 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.transition.Transitions.ENABLE_SHELL_TRANSITIONS; import android.app.ActivityManager; import android.app.ActivityTaskManager; import android.app.PendingIntent; import android.content.ActivityNotFoundException; +import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.pm.LauncherApps; @@ -58,6 +62,7 @@ import com.android.internal.logging.InstanceId; 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.RemoteCallable; @@ -66,6 +71,7 @@ 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.draganddrop.DragAndDropPolicy; import com.android.wm.shell.recents.RecentTasksController; @@ -124,6 +130,7 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, private final RootTaskDisplayAreaOrganizer mRootTDAOrganizer; private final ShellExecutor mMainExecutor; private final SplitScreenImpl mImpl = new SplitScreenImpl(); + private final DisplayController mDisplayController; private final DisplayImeController mDisplayImeController; private final DisplayInsetsController mDisplayInsetsController; private final Transitions mTransitions; @@ -141,7 +148,8 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, public SplitScreenController(ShellTaskOrganizer shellTaskOrganizer, SyncTransactionQueue syncQueue, Context context, RootTaskDisplayAreaOrganizer rootTDAOrganizer, - ShellExecutor mainExecutor, DisplayImeController displayImeController, + ShellExecutor mainExecutor, DisplayController displayController, + DisplayImeController displayImeController, DisplayInsetsController displayInsetsController, Transitions transitions, TransactionPool transactionPool, IconProvider iconProvider, Optional<RecentTasksController> recentTasks, @@ -151,6 +159,7 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, mContext = context; mRootTDAOrganizer = rootTDAOrganizer; mMainExecutor = mainExecutor; + mDisplayController = displayController; mDisplayImeController = displayImeController; mDisplayInsetsController = displayInsetsController; mTransitions = transitions; @@ -179,9 +188,9 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, if (mStageCoordinator == null) { // TODO: Multi-display mStageCoordinator = new StageCoordinator(mContext, DEFAULT_DISPLAY, mSyncQueue, - mRootTDAOrganizer, mTaskOrganizer, mDisplayImeController, + mTaskOrganizer, mDisplayController, mDisplayImeController, mDisplayInsetsController, mTransitions, mTransactionPool, mLogger, - mIconProvider, mRecentTasksOptional, mUnfoldControllerProvider); + mIconProvider, mMainExecutor, mRecentTasksOptional, mUnfoldControllerProvider); } } @@ -191,11 +200,12 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, @Nullable public ActivityManager.RunningTaskInfo getTaskInfo(@SplitPosition int splitPosition) { - if (isSplitScreenVisible()) { - int taskId = mStageCoordinator.getTaskId(splitPosition); - return mTaskOrganizer.getRunningTaskInfo(taskId); + if (!isSplitScreenVisible() || splitPosition == SPLIT_POSITION_UNDEFINED) { + return null; } - return null; + + final int taskId = mStageCoordinator.getTaskId(splitPosition); + return mTaskOrganizer.getRunningTaskInfo(taskId); } public boolean isTaskInSplitScreen(int taskId) { @@ -229,14 +239,19 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, mStageCoordinator.setSideStagePosition(sideStagePosition, null /* wct */); } - public void setSideStageVisibility(boolean visible) { - mStageCoordinator.setSideStageVisibility(visible); - } - public void enterSplitScreen(int taskId, boolean leftOrTop) { enterSplitScreen(taskId, leftOrTop, new WindowContainerTransaction()); } + public void prepareEnterSplitScreen(WindowContainerTransaction wct, + ActivityManager.RunningTaskInfo taskInfo, int startPosition) { + mStageCoordinator.prepareEnterSplitScreen(wct, taskInfo, startPosition); + } + + public void finishEnterSplitScreen(SurfaceControl.Transaction t) { + mStageCoordinator.finishEnterSplitScreen(t); + } + public void enterSplitScreen(int taskId, boolean leftOrTop, WindowContainerTransaction wct) { final int stageType = isSplitScreenVisible() ? STAGE_TYPE_UNDEFINED : STAGE_TYPE_SIDE; final int stagePosition = @@ -248,10 +263,6 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, mStageCoordinator.exitSplitScreen(toTopTaskId, exitReason); } - public void onKeyguardOccludedChanged(boolean occluded) { - mStageCoordinator.onKeyguardOccludedChanged(occluded); - } - public void onKeyguardVisibilityChanged(boolean showing) { mStageCoordinator.onKeyguardVisibilityChanged(showing); } @@ -260,10 +271,6 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, mStageCoordinator.onFinishedWakingUp(); } - public void onFinishedGoingToSleep() { - mStageCoordinator.onFinishedGoingToSleep(); - } - public void exitSplitScreenOnHide(boolean exitSplitScreenOnHide) { mStageCoordinator.exitSplitScreenOnHide(exitSplitScreenOnHide); } @@ -315,17 +322,33 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, } } - public void startIntent(PendingIntent intent, Intent fillInIntent, @SplitPosition int position, - @Nullable Bundle options) { - if (!Transitions.ENABLE_SHELL_TRANSITIONS) { + public void startIntent(PendingIntent intent, @Nullable Intent fillInIntent, + @SplitPosition int position, @Nullable Bundle options) { + if (!ENABLE_SHELL_TRANSITIONS) { startIntentLegacy(intent, fillInIntent, position, options); return; } - mStageCoordinator.startIntent(intent, fillInIntent, STAGE_TYPE_UNDEFINED, position, options, - null /* remote */); + + try { + options = mStageCoordinator.resolveStartStage(STAGE_TYPE_UNDEFINED, position, options, + null /* wct */); + + // Flag this as a no-user-action launch to prevent sending user leaving event to the + // current top activity since it's going to be put into another side of the split. This + // prevents the current top activity from going into pip mode due to user leaving event. + if (fillInIntent == null) { + fillInIntent = new Intent(); + } + fillInIntent.addFlags(FLAG_ACTIVITY_NO_USER_ACTION); + + 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 startIntentLegacy(PendingIntent intent, Intent fillInIntent, + private void startIntentLegacy(PendingIntent intent, @Nullable Intent fillInIntent, @SplitPosition int position, @Nullable Bundle options) { final WindowContainerTransaction evictWct = new WindowContainerTransaction(); mStageCoordinator.prepareEvictChildTasks(position, evictWct); @@ -336,17 +359,33 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, RemoteAnimationTarget[] wallpapers, RemoteAnimationTarget[] nonApps, IRemoteAnimationFinishedCallback finishedCallback, SurfaceControl.Transaction t) { - mStageCoordinator.updateSurfaceBounds(null /* layout */, t); - - if (apps != null) { - for (int i = 0; i < apps.length; ++i) { - if (apps[i].mode == MODE_OPENING) { - t.show(apps[i].leash); - } + if (apps == null || apps.length == 0) { + final ActivityManager.RunningTaskInfo pairedTaskInfo = + getTaskInfo(SplitLayout.reversePosition(position)); + final ComponentName pairedActivity = + pairedTaskInfo != null ? pairedTaskInfo.baseActivity : null; + final ComponentName intentActivity = + intent.getIntent() != null ? intent.getIntent().getComponent() : null; + if (pairedActivity != null && pairedActivity.equals(intentActivity)) { + // Switch split position if dragging the same activity to another side. + setSideStagePosition(SplitLayout.reversePosition( + mStageCoordinator.getSideStagePosition())); } + + // Do nothing when the animation was cancelled. + t.apply(); + return; } + mStageCoordinator.updateSurfaceBounds(null /* layout */, t, + false /* applyResizingOffset */); + for (int i = 0; i < apps.length; ++i) { + if (apps[i].mode == MODE_OPENING) { + t.show(apps[i].leash); + } + } t.apply(); + if (finishedCallback != null) { try { finishedCallback.onAnimationFinished(); @@ -361,12 +400,42 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, final WindowContainerTransaction wct = new WindowContainerTransaction(); options = mStageCoordinator.resolveStartStage(STAGE_TYPE_UNDEFINED, position, options, wct); + + // Flag this as a no-user-action launch to prevent sending user leaving event to the current + // top activity since it's going to be put into another side of the split. This prevents the + // current top activity from going into pip mode due to user leaving event. + if (fillInIntent == null) { + fillInIntent = new Intent(); + } + fillInIntent.addFlags(FLAG_ACTIVITY_NO_USER_ACTION); + wct.sendPendingIntent(intent, fillInIntent, options); mSyncQueue.queue(transition, WindowManager.TRANSIT_OPEN, wct); } - RemoteAnimationTarget[] onGoingToRecentsLegacy(boolean cancel, RemoteAnimationTarget[] apps) { - if (apps.length < 2) return null; + RemoteAnimationTarget[] onGoingToRecentsLegacy(RemoteAnimationTarget[] apps) { + 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); + } + return reparentSplitTasksForAnimation(apps, true /*splitExpectedToBeVisible*/); + } + + RemoteAnimationTarget[] onStartingSplitLegacy(RemoteAnimationTarget[] apps) { + return reparentSplitTasksForAnimation(apps, false /*splitExpectedToBeVisible*/); + } + + private RemoteAnimationTarget[] reparentSplitTasksForAnimation(RemoteAnimationTarget[] apps, + boolean splitExpectedToBeVisible) { + 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 @@ -393,7 +462,6 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, transaction.close(); return new RemoteAnimationTarget[]{mStageCoordinator.getDividerBarLegacyTarget()}; } - /** * Sets drag info to be logged when splitscreen is entered. */ @@ -487,13 +555,6 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, } @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; @@ -534,13 +595,6 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, SplitScreenController.this.onFinishedWakingUp(); }); } - - @Override - public void onFinishedGoingToSleep() { - mMainExecutor.execute(() -> { - SplitScreenController.this.onFinishedGoingToSleep(); - }); - } } /** @@ -607,14 +661,6 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, } @Override - public void setSideStageVisibility(boolean visible) { - executeRemoteCallWithTaskPermission(mController, "setSideStageVisibility", - (controller) -> { - controller.setSideStageVisibility(visible); - }); - } - - @Override public void removeFromSideStage(int taskId) { executeRemoteCallWithTaskPermission(mController, "removeFromSideStage", (controller) -> { @@ -641,6 +687,17 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, } @Override + public void startIntentAndTaskWithLegacyTransition(PendingIntent pendingIntent, + Intent fillInIntent, int taskId, Bundle mainOptions, Bundle sideOptions, + int sidePosition, float splitRatio, RemoteAnimationAdapter adapter) { + executeRemoteCallWithTaskPermission(mController, + "startIntentAndTaskWithLegacyTransition", (controller) -> + controller.mStageCoordinator.startIntentAndTaskWithLegacyTransition( + pendingIntent, fillInIntent, taskId, mainOptions, sideOptions, + sidePosition, splitRatio, adapter)); + } + + @Override public void startTasks(int mainTaskId, @Nullable Bundle mainOptions, int sideTaskId, @Nullable Bundle sideOptions, @SplitPosition int sidePosition, float splitRatio, @@ -669,11 +726,19 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, } @Override - public RemoteAnimationTarget[] onGoingToRecentsLegacy(boolean cancel, - RemoteAnimationTarget[] apps) { + public RemoteAnimationTarget[] onGoingToRecentsLegacy(RemoteAnimationTarget[] apps) { final RemoteAnimationTarget[][] out = new RemoteAnimationTarget[][]{null}; executeRemoteCallWithTaskPermission(mController, "onGoingToRecentsLegacy", - (controller) -> out[0] = controller.onGoingToRecentsLegacy(cancel, apps), + (controller) -> out[0] = controller.onGoingToRecentsLegacy(apps), + true /* blocking */); + return out[0]; + } + + @Override + public RemoteAnimationTarget[] onStartingSplitLegacy(RemoteAnimationTarget[] apps) { + final RemoteAnimationTarget[][] out = new RemoteAnimationTarget[][]{null}; + executeRemoteCallWithTaskPermission(mController, "onStartingSplitLegacy", + (controller) -> out[0] = controller.onStartingSplitLegacy(apps), true /* blocking */); return out[0]; } 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 86e7b0e4cb7f..cd121ed41fdd 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 @@ -23,8 +23,13 @@ import static android.view.WindowManager.TRANSIT_TO_BACK; import static android.view.WindowManager.TRANSIT_TO_FRONT; import static android.window.TransitionInfo.FLAG_FIRST_CUSTOM; +import static com.android.wm.shell.splitscreen.SplitScreen.stageTypeToString; +import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_DRAG_DIVIDER; +import static com.android.wm.shell.splitscreen.SplitScreenController.exitReasonToString; +import static com.android.wm.shell.transition.Transitions.TRANSIT_SPLIT_DISMISS; import static com.android.wm.shell.transition.Transitions.TRANSIT_SPLIT_DISMISS_SNAP; -import static com.android.wm.shell.transition.Transitions.isOpeningType; +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 android.animation.Animator; import android.animation.AnimatorListenerAdapter; @@ -39,8 +44,11 @@ import android.window.RemoteTransition; import android.window.TransitionInfo; import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; +import android.window.WindowContainerTransactionCallback; +import com.android.internal.protolog.common.ProtoLog; import com.android.wm.shell.common.TransactionPool; +import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.transition.OneShotRemoteHandler; import com.android.wm.shell.transition.Transitions; @@ -57,30 +65,29 @@ class SplitScreenTransitions { private final Transitions mTransitions; private final Runnable mOnFinish; - IBinder mPendingDismiss = null; + DismissTransition mPendingDismiss = null; IBinder mPendingEnter = null; + IBinder mPendingRecent = null; private IBinder mAnimatingTransition = null; - private OneShotRemoteHandler mRemoteHandler = null; + private OneShotRemoteHandler mPendingRemoteHandler = null; + private OneShotRemoteHandler mActiveRemoteHandler = null; - private Transitions.TransitionFinishCallback mRemoteFinishCB = (wct, wctCB) -> { - if (wct != null || wctCB != null) { - throw new UnsupportedOperationException("finish transactions not supported yet."); - } - onFinish(); - }; + private final Transitions.TransitionFinishCallback mRemoteFinishCB = this::onFinish; /** Keeps track of currently running animations */ private final ArrayList<Animator> mAnimations = new ArrayList<>(); + private final StageCoordinator mStageCoordinator; private Transitions.TransitionFinishCallback mFinishCallback = null; private SurfaceControl.Transaction mFinishTransaction; SplitScreenTransitions(@NonNull TransactionPool pool, @NonNull Transitions transitions, - @NonNull Runnable onFinishCallback) { + @NonNull Runnable onFinishCallback, StageCoordinator stageCoordinator) { mTransactionPool = pool; mTransitions = transitions; mOnFinish = onFinishCallback; + mStageCoordinator = stageCoordinator; } void playAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, @@ -90,10 +97,11 @@ class SplitScreenTransitions { @NonNull WindowContainerToken mainRoot, @NonNull WindowContainerToken sideRoot) { mFinishCallback = finishCallback; mAnimatingTransition = transition; - if (mRemoteHandler != null) { - mRemoteHandler.startAnimation(transition, info, startTransaction, finishTransaction, - mRemoteFinishCB); - mRemoteHandler = null; + if (mPendingRemoteHandler != null) { + mPendingRemoteHandler.startAnimation(transition, info, startTransaction, + finishTransaction, mRemoteFinishCB); + mActiveRemoteHandler = mPendingRemoteHandler; + mPendingRemoteHandler = null; return; } playInternalAnimation(transition, info, startTransaction, mainRoot, sideRoot); @@ -127,12 +135,6 @@ class SplitScreenTransitions { } // TODO(shell-transitions): screenshot here final Rect startBounds = new Rect(change.getStartAbsBounds()); - if (info.getType() == TRANSIT_SPLIT_DISMISS_SNAP) { - // Dismissing split via snap which means the still-visible task has been - // dragged to its end position at animation start so reflect that here. - startBounds.offsetTo(change.getEndAbsBounds().left, - change.getEndAbsBounds().top); - } final Rect endBounds = new Rect(change.getEndAbsBounds()); startBounds.offset(-info.getRootOffset().x, -info.getRootOffset().y); endBounds.offset(-info.getRootOffset().x, -info.getRootOffset().y); @@ -144,10 +146,11 @@ class SplitScreenTransitions { if (transition == mPendingEnter && (mainRoot.equals(change.getContainer()) || sideRoot.equals(change.getContainer()))) { - t.setWindowCrop(leash, change.getStartAbsBounds().width(), - change.getStartAbsBounds().height()); + t.setPosition(leash, change.getEndAbsBounds().left, change.getEndAbsBounds().top); + t.setWindowCrop(leash, change.getEndAbsBounds().width(), + change.getEndAbsBounds().height()); } - boolean isOpening = isOpeningType(info.getType()); + boolean isOpening = isOpeningTransition(info); if (isOpening && (mode == TRANSIT_OPEN || mode == TRANSIT_TO_FRONT)) { // fade in startExampleAnimation(leash, true /* show */); @@ -164,52 +167,100 @@ class SplitScreenTransitions { } } t.apply(); - onFinish(); + 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; + if (remoteTransition != null) { // Wrapping it for ease-of-use (OneShot handles all the binder linking/death stuff) - mRemoteHandler = new OneShotRemoteHandler( + mPendingRemoteHandler = new OneShotRemoteHandler( mTransitions.getMainExecutor(), remoteTransition); + mPendingRemoteHandler.setTransition(transition); } - final IBinder transition = mTransitions.startTransition(transitType, wct, handler); - mPendingEnter = transition; - if (mRemoteHandler != null) { - mRemoteHandler.setTransition(transition); + return transition; + } + + /** Starts a transition to dismiss split. */ + IBinder startDismissTransition(@Nullable IBinder transition, WindowContainerTransaction wct, + Transitions.TransitionHandler handler, @SplitScreen.StageType int dismissTop, + @SplitScreenController.ExitReason int reason) { + final int type = reason == EXIT_REASON_DRAG_DIVIDER + ? TRANSIT_SPLIT_DISMISS_SNAP : TRANSIT_SPLIT_DISMISS; + if (transition == null) { + transition = mTransitions.startTransition(type, wct, handler); } + mPendingDismiss = new DismissTransition(transition, reason, dismissTop); + + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " splitTransition " + + " deduced Dismiss due to %s. toTop=%s", + exitReasonToString(reason), stageTypeToString(dismissTop)); return transition; } - /** 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; + IBinder startRecentTransition(@Nullable IBinder transition, WindowContainerTransaction wct, + Transitions.TransitionHandler handler, @Nullable RemoteTransition remoteTransition) { + if (transition == null) { + transition = mTransitions.startTransition(TRANSIT_OPEN, wct, handler); + } + mPendingRecent = transition; + + 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); + } + + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " splitTransition " + + " deduced Enter recent panel"); return transition; } - void onFinish() { + void mergeAnimation(IBinder transition, TransitionInfo info, SurfaceControl.Transaction t, + IBinder mergeTarget, Transitions.TransitionFinishCallback finishCallback) { + if (mergeTarget == mAnimatingTransition && mActiveRemoteHandler != null) { + mActiveRemoteHandler.mergeAnimation(transition, info, t, mergeTarget, finishCallback); + } + } + + void onFinish(WindowContainerTransaction wct, WindowContainerTransactionCallback wctCB) { if (!mAnimations.isEmpty()) return; + if (mAnimatingTransition == mPendingEnter) { + mPendingEnter = null; + } + if (mPendingDismiss != null && mPendingDismiss.mTransition == mAnimatingTransition) { + mPendingDismiss = null; + } + if (mAnimatingTransition == mPendingRecent) { + // If the clean-up wct is null when finishing recent transition, it indicates it's + // returning to home and thus no need to reorder tasks. + final boolean returnToHome = wct == null; + if (returnToHome) { + wct = new WindowContainerTransaction(); + } + mStageCoordinator.onRecentTransitionFinished(returnToHome, wct, mFinishTransaction); + mPendingRecent = null; + } + mPendingRemoteHandler = null; + mActiveRemoteHandler = null; + mAnimatingTransition = null; + 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; + if (mFinishCallback != null) { + mFinishCallback.onTransitionFinished(wct /* wct */, wctCB /* wctCB */); + mFinishCallback = null; } - mAnimatingTransition = null; } // TODO(shell-transitions): real animations @@ -230,7 +281,7 @@ class SplitScreenTransitions { mTransactionPool.release(transaction); mTransitions.getMainExecutor().execute(() -> { mAnimations.remove(va); - onFinish(); + onFinish(null /* wct */, null /* wctCB */); }); }; va.addListener(new Animator.AnimatorListener() { @@ -278,7 +329,7 @@ class SplitScreenTransitions { mTransactionPool.release(transaction); mTransitions.getMainExecutor().execute(() -> { mAnimations.remove(va); - onFinish(); + onFinish(null /* wct */, null /* wctCB */); }); }; va.addListener(new AnimatorListenerAdapter() { @@ -295,4 +346,26 @@ class SplitScreenTransitions { mAnimations.add(va); mTransitions.getAnimExecutor().execute(va::start); } + + private boolean isOpeningTransition(TransitionInfo info) { + return Transitions.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; + + int mReason; + + @SplitScreen.StageType + int mDismissTop; + + DismissTransition(IBinder transition, int reason, int dismissTop) { + this.mTransition = transition; + this.mReason = reason; + this.mDismissTop = dismissTop; + } + } } 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 38c1aff0a62c..30f316efb2b3 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,37 +18,46 @@ 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.WindowConfiguration.ACTIVITY_TYPE_HOME; +import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS; +import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static android.view.Display.DEFAULT_DISPLAY; import static android.view.WindowManager.LayoutParams.TYPE_DOCK_DIVIDER; -import static android.view.WindowManager.TRANSIT_OPEN; +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.view.WindowManagerPolicyConstants.SPLIT_DIVIDER_LAYER; +import static android.window.TransitionInfo.FLAG_IS_DISPLAY; +import static com.android.wm.shell.common.split.SplitLayout.PARALLAX_ALIGN_CENTER; import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT; import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED; import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_MAIN; import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_SIDE; import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_UNDEFINED; -import static com.android.wm.shell.splitscreen.SplitScreen.stageTypeToString; 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_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_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.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; +import android.annotation.CallSuper; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.ActivityManager; @@ -58,13 +67,16 @@ import android.app.PendingIntent; import android.app.WindowConfiguration; import android.content.Context; import android.content.Intent; +import android.content.res.Configuration; import android.graphics.Rect; import android.hardware.devicestate.DeviceStateManager; import android.os.Bundle; +import android.os.Debug; import android.os.IBinder; import android.os.RemoteException; import android.util.Log; import android.util.Slog; +import android.view.Choreographer; import android.view.IRemoteAnimationFinishedCallback; import android.view.IRemoteAnimationRunner; import android.view.RemoteAnimationAdapter; @@ -72,7 +84,6 @@ 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; @@ -83,10 +94,12 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.logging.InstanceId; import com.android.internal.protolog.common.ProtoLog; import com.android.launcher3.icons.IconProvider; -import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.ShellTaskOrganizer; +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.common.split.SplitLayout; @@ -115,18 +128,16 @@ import javax.inject.Provider; * - 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. + * - Both stages are put under a single-top root task. * This rules are mostly implemented in {@link #onStageVisibilityChanged(StageListenerImpl)} and * {@link #onStageHasChildrenChanged(StageListenerImpl).} */ class StageCoordinator implements SplitLayout.SplitLayoutHandler, - RootTaskDisplayAreaOrganizer.RootTaskDisplayAreaListener, Transitions.TransitionHandler { + DisplayController.OnDisplaysChangedListener, Transitions.TransitionHandler, + ShellTaskOrganizer.TaskListener { 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; @@ -135,54 +146,52 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, 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; private final int mDisplayId; private SplitLayout mSplitLayout; + private ValueAnimator mDividerFadeInAnimator; private boolean mDividerVisible; + private boolean mKeyguardShowing; 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 DisplayController mDisplayController; private final DisplayImeController mDisplayImeController; private final DisplayInsetsController mDisplayInsetsController; + private final TransactionPool mTransactionPool; private final SplitScreenTransitions mSplitTransitions; private final SplitscreenEventLogger mLogger; + private final ShellExecutor mMainExecutor; private final Optional<RecentTasksController> mRecentTasks; + + /** + * A single-top root task which the split divider attached to. + */ + @VisibleForTesting + ActivityManager.RunningTaskInfo mRootTaskInfo; + + private SurfaceControl mRootTaskLeash; + // Tracks whether we should update the recent tasks. Only allow this to happen in between enter // and exit, since exit itself can trigger a number of changes that update the stages. private boolean mShouldUpdateRecents; private boolean mExitSplitScreenOnHide; - private boolean mKeyguardOccluded; - private boolean mDeviceSleep; private boolean mIsDividerRemoteAnimating; - - @StageType - private int mDismissTop = NO_DISMISS; + private boolean mResizingSplits; /** The target stage to dismiss to when unlock after folded. */ @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); + b.setParent(mRootTaskLeash); } @Override @@ -192,22 +201,23 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, }; StageCoordinator(Context context, int displayId, SyncTransactionQueue syncQueue, - RootTaskDisplayAreaOrganizer rootTDAOrganizer, ShellTaskOrganizer taskOrganizer, + ShellTaskOrganizer taskOrganizer, DisplayController displayController, DisplayImeController displayImeController, DisplayInsetsController displayInsetsController, Transitions transitions, TransactionPool transactionPool, SplitscreenEventLogger logger, - IconProvider iconProvider, + IconProvider iconProvider, ShellExecutor mainExecutor, Optional<RecentTasksController> recentTasks, Provider<Optional<StageTaskUnfoldController>> unfoldControllerProvider) { mContext = context; mDisplayId = displayId; mSyncQueue = syncQueue; - mRootTDAOrganizer = rootTDAOrganizer; mTaskOrganizer = taskOrganizer; mLogger = logger; + 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( mContext, @@ -227,44 +237,50 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, mSurfaceSession, iconProvider, mSideUnfoldController); + mDisplayController = displayController; mDisplayImeController = displayImeController; mDisplayInsetsController = displayInsetsController; - mRootTDAOrganizer.registerListener(displayId, this); + mTransactionPool = transactionPool; final DeviceStateManager deviceStateManager = mContext.getSystemService(DeviceStateManager.class); deviceStateManager.registerCallback(taskOrganizer.getExecutor(), new DeviceStateManager.FoldStateListener(mContext, this::onFoldedStateChanged)); mSplitTransitions = new SplitScreenTransitions(transactionPool, transitions, - mOnTransitionAnimationComplete); + this::onTransitionAnimationComplete, this); + mDisplayController.addDisplayWindowListener(this); + mDisplayLayout = new DisplayLayout(displayController.getDisplayLayout(displayId)); transitions.addHandler(this); } @VisibleForTesting StageCoordinator(Context context, int displayId, SyncTransactionQueue syncQueue, - RootTaskDisplayAreaOrganizer rootTDAOrganizer, ShellTaskOrganizer taskOrganizer, - MainStage mainStage, SideStage sideStage, DisplayImeController displayImeController, + ShellTaskOrganizer taskOrganizer, MainStage mainStage, SideStage sideStage, + DisplayController displayController, DisplayImeController displayImeController, DisplayInsetsController displayInsetsController, SplitLayout splitLayout, Transitions transitions, TransactionPool transactionPool, - SplitscreenEventLogger logger, + SplitscreenEventLogger logger, ShellExecutor mainExecutor, Optional<RecentTasksController> recentTasks, Provider<Optional<StageTaskUnfoldController>> unfoldControllerProvider) { mContext = context; mDisplayId = displayId; mSyncQueue = syncQueue; - mRootTDAOrganizer = rootTDAOrganizer; mTaskOrganizer = taskOrganizer; mMainStage = mainStage; mSideStage = sideStage; + mDisplayController = displayController; mDisplayImeController = displayImeController; mDisplayInsetsController = displayInsetsController; - mRootTDAOrganizer.registerListener(displayId, this); + mTransactionPool = transactionPool; mSplitLayout = splitLayout; mSplitTransitions = new SplitScreenTransitions(transactionPool, transitions, - mOnTransitionAnimationComplete); + this::onTransitionAnimationComplete, this); mMainUnfoldController = unfoldControllerProvider.get().orElse(null); mSideUnfoldController = unfoldControllerProvider.get().orElse(null); mLogger = logger; + mMainExecutor = mainExecutor; mRecentTasks = recentTasks; + mDisplayController.addDisplayWindowListener(this); + mDisplayLayout = new DisplayLayout(); transitions.addHandler(this); } @@ -316,7 +332,14 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, if (!evictWct.isEmpty()) { wct.merge(evictWct, true /* transfer */); } - mTaskOrganizer.applyTransaction(wct); + + if (ENABLE_SHELL_TRANSITIONS) { + prepareEnterSplitScreen(wct); + mSplitTransitions.startEnterTransition(TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE, + wct, null, this); + } else { + mTaskOrganizer.applyTransaction(wct); + } return true; } @@ -343,12 +366,13 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, sideOptions = sideOptions != null ? sideOptions : new Bundle(); setSideStagePosition(sidePosition, wct); + 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(getMainStageBounds(), wct, false /* reparent */); - mSideStage.setBounds(getSideStageBounds(), wct); + mMainStage.activate(wct, false /* reparent */); + updateWindowBounds(mSplitLayout, wct); + wct.reorder(mRootTaskInfo.token, true); - mSplitLayout.setDivideRatio(splitRatio); // Make sure the launch options will put tasks in the corresponding split roots addActivityOptions(mainOptions, mMainStage); addActivityOptions(sideOptions, mSideStage); @@ -365,12 +389,29 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, void startTasksWithLegacyTransition(int mainTaskId, @Nullable Bundle mainOptions, int sideTaskId, @Nullable Bundle sideOptions, @SplitPosition int sidePosition, float splitRatio, RemoteAnimationAdapter adapter) { - // Ensure divider is invisible before transition. - setDividerVisibility(false /* visible */); + startWithLegacyTransition(mainTaskId, sideTaskId, null /* pendingIntent */, + null /* fillInIntent */, mainOptions, sideOptions, sidePosition, splitRatio, + adapter); + } + + /** 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); + } + + 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; // Init divider first to make divider leash for remote animation target. mSplitLayout.init(); // Set false to avoid record new bounds with old task still on top; mShouldUpdateRecents = false; + mIsDividerRemoteAnimating = true; final WindowContainerTransaction wct = new WindowContainerTransaction(); final WindowContainerTransaction evictWct = new WindowContainerTransaction(); prepareEvictChildTasks(SPLIT_POSITION_TOP_OR_LEFT, evictWct); @@ -384,7 +425,6 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, RemoteAnimationTarget[] wallpapers, RemoteAnimationTarget[] nonApps, final IRemoteAnimationFinishedCallback finishedCallback) { - mIsDividerRemoteAnimating = true; RemoteAnimationTarget[] augmentedNonApps = new RemoteAnimationTarget[nonApps.length + 1]; for (int i = 0; i < nonApps.length; ++i) { @@ -396,11 +436,7 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, new IRemoteAnimationFinishedCallback.Stub() { @Override public void onAnimationFinished() throws RemoteException { - mIsDividerRemoteAnimating = false; - mShouldUpdateRecents = true; - setDividerVisibility(true /* visible */); - mSyncQueue.queue(evictWct); - mSyncQueue.runInSync(t -> applyDividerVisibility(t)); + onRemoteAnimationFinishedOrCancelled(evictWct); finishedCallback.onAnimationFinished(); } }; @@ -420,14 +456,10 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, } @Override - public void onAnimationCancelled() { - mIsDividerRemoteAnimating = false; - mShouldUpdateRecents = true; - setDividerVisibility(true /* visible */); - mSyncQueue.queue(evictWct); - mSyncQueue.runInSync(t -> applyDividerVisibility(t)); + public void onAnimationCancelled(boolean isKeyguardOccluded) { + onRemoteAnimationFinishedOrCancelled(evictWct); try { - adapter.getRunner().onAnimationCancelled(); + adapter.getRunner().onAnimationCancelled(isKeyguardOccluded); } catch (RemoteException e) { Slog.e(TAG, "Error starting remote animation", e); } @@ -448,14 +480,13 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, setSideStagePosition(sidePosition, wct); mSplitLayout.setDivideRatio(splitRatio); - if (mMainStage.isActive()) { - mMainStage.moveToTop(getMainStageBounds(), wct); - } else { + 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(getMainStageBounds(), wct, false /* reparent */); + mMainStage.activate(wct, false /* reparent */); } - mSideStage.moveToTop(getSideStageBounds(), wct); + updateWindowBounds(mSplitLayout, wct); + wct.reorder(mRootTaskInfo.token, true); // Make sure the launch options will put tasks in the corresponding split roots addActivityOptions(mainOptions, mMainStage); @@ -463,21 +494,32 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, // Add task launch requests wct.startTask(mainTaskId, mainOptions); - wct.startTask(sideTaskId, sideOptions); - + if (withIntent) { + wct.sendPendingIntent(pendingIntent, fillInIntent, sideOptions); + } else { + wct.startTask(sideTaskId, sideOptions); + } // Using legacy transitions, so we can't use blast sync since it conflicts. mTaskOrganizer.applyTransaction(wct); + mSyncQueue.runInSync(t -> { + setDividerVisibility(true, t); + updateSurfaceBounds(mSplitLayout, t, false /* applyResizingOffset */); + }); } - public void startIntent(PendingIntent intent, Intent fillInIntent, - @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); + private void onRemoteAnimationFinishedOrCancelled(WindowContainerTransaction evictWct) { + mIsDividerRemoteAnimating = false; + mShouldUpdateRecents = true; + // If any stage has no child after animation finished, it means that split will display + // nothing, such status will happen if task and intent is same app but not support + // multi-instagce, we should exit split and expand that app as full screen. + if (mMainStage.getChildCount() == 0 || mSideStage.getChildCount() == 0) { + mMainExecutor.execute(() -> + exitSplitScreen(mMainStage.getChildCount() == 0 + ? mSideStage : mMainStage, EXIT_REASON_UNKNOWN)); + } else { + mSyncQueue.queue(evictWct); + } } /** @@ -492,6 +534,11 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, } } + 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) { @@ -508,8 +555,8 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, options = resolveStartStage(STAGE_TYPE_SIDE, position, options, wct); } } else { - // Exit split-screen and launch fullscreen since stage wasn't specified. - prepareExitSplitScreen(STAGE_TYPE_UNDEFINED, wct); + Slog.w(TAG, + "No stage type nor split position specified to resolve start stage"); } break; } @@ -585,37 +632,53 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, } } - 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, - EXIT_REASON_DEVICE_FOLDED); + mKeyguardShowing = showing; + if (!mMainStage.isActive()) { + 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); } void onFinishedWakingUp() { - if (mMainStage.isActive()) { - exitSplitScreenIfKeyguardOccluded(); + if (!mMainStage.isActive()) { + return; } - mDeviceSleep = false; - } - void onFinishedGoingToSleep() { - mDeviceSleep = true; + // Check if there's only one stage visible while keyguard occluded. + final boolean mainStageVisible = mMainStage.mRootTaskInfo.isVisible; + final boolean oneStageVisible = + mMainStage.mRootTaskInfo.isVisible != mSideStage.mRootTaskInfo.isVisible; + if (oneStageVisible) { + // 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. + if (!ENABLE_SHELL_TRANSITIONS) { + final StageTaskListener toTop = mainStageVisible ? mMainStage : mSideStage; + exitSplitScreen(toTop, EXIT_REASON_SCREEN_LOCKED_SHOW_ON_TOP); + } else { + 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); + } + } } void exitSplitScreenOnHide(boolean exitSplitScreenOnHide) { @@ -639,28 +702,18 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, applyExitSplitScreen(childrenToTop, wct, exitReason); } - private void exitSplitScreen(StageTaskListener childrenToTop, @ExitReason int exitReason) { + private void exitSplitScreen(@Nullable StageTaskListener childrenToTop, + @ExitReason int exitReason) { if (!mMainStage.isActive()) return; final WindowContainerTransaction wct = new WindowContainerTransaction(); applyExitSplitScreen(childrenToTop, wct, exitReason); } - private void exitSplitScreenIfKeyguardOccluded() { - final boolean mainStageVisible = mMainStageListener.mVisible; - final boolean oneStageVisible = mainStageVisible ^ mSideStageListener.mVisible; - if (mDeviceSleep && mKeyguardOccluded && oneStageVisible) { - // Only the stages include show-when-locked activity 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 : mSideStage; - exitSplitScreen(toTop, EXIT_REASON_SCREEN_LOCKED_SHOW_ON_TOP); - } - } - - private void applyExitSplitScreen(StageTaskListener childrenToTop, + private void applyExitSplitScreen(@Nullable StageTaskListener childrenToTop, WindowContainerTransaction wct, @ExitReason int exitReason) { + if (!mMainStage.isActive()) return; + 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 @@ -675,16 +728,20 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, // 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 && childrenToTop == mSideStage); - mMainStage.deactivate(wct, !fromEnteringPip && childrenToTop == mMainStage); + mSideStage.removeAllTasks(wct, !fromEnteringPip && mSideStage == childrenToTop); + mMainStage.deactivate(wct, !fromEnteringPip && mMainStage == childrenToTop); + wct.reorder(mRootTaskInfo.token, false /* onTop */); mTaskOrganizer.applyTransaction(wct); - mSyncQueue.runInSync(t -> t - .setWindowCrop(mMainStage.mRootLeash, null) - .setWindowCrop(mSideStage.mRootLeash, null)); + mSyncQueue.runInSync(t -> { + setResizingSplits(false /* resizing */); + t.setWindowCrop(mMainStage.mRootLeash, null) + .setWindowCrop(mSideStage.mRootLeash, null); + setDividerVisibility(false, t); + }); // Hide divider and reset its position. - setDividerVisibility(false); mSplitLayout.resetDividerPosition(); + mSplitLayout.release(); mTopStageAfterFoldDismiss = STAGE_TYPE_UNDEFINED; Slog.i(TAG, "applyExitSplitScreen, reason = " + exitReasonToString(exitReason)); // Log the exit @@ -708,6 +765,10 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, case EXIT_REASON_APP_FINISHED: // One of the children enters PiP case EXIT_REASON_CHILD_TASK_ENTER_PIP: + // One of the apps occludes lock screen. + case EXIT_REASON_SCREEN_LOCKED_SHOW_ON_TOP: + // User has unlocked the device after folded + case EXIT_REASON_DEVICE_FOLDED: return true; default: return false; @@ -719,12 +780,49 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, * 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(@StageType int stageToTop, + 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); } + private void prepareEnterSplitScreen(WindowContainerTransaction wct) { + prepareEnterSplitScreen(wct, null /* taskInfo */, SPLIT_POSITION_UNDEFINED); + } + + /** + * Prepare transaction to active split screen. If there's a task indicated, the task will be put + * into side stage. + */ + void prepareEnterSplitScreen(WindowContainerTransaction wct, + @Nullable ActivityManager.RunningTaskInfo taskInfo, @SplitPosition int startPosition) { + if (mMainStage.isActive()) return; + + if (taskInfo != null) { + setSideStagePosition(startPosition, wct); + mSideStage.addTask(taskInfo, wct); + } + mMainStage.activate(wct, true /* includingTopTask */); + updateWindowBounds(mSplitLayout, wct); + wct.reorder(mRootTaskInfo.token, true); + } + + void finishEnterSplitScreen(SurfaceControl.Transaction t) { + mSplitLayout.init(); + setDividerVisibility(true, t); + updateSurfaceBounds(mSplitLayout, t, false /* applyResizingOffset */); + setSplitsVisible(true); + mShouldUpdateRecents = true; + updateRecentTasksSplitPair(); + if (!mLogger.hasStartedSession()) { + mLogger.logEnter(mSplitLayout.getDividerPositionAsFraction(), + getMainStagePosition(), mMainStage.getTopChildTaskUid(), + getSideStagePosition(), mSideStage.getTopChildTaskUid(), + mSplitLayout.isLandscape()); + } + } + void getStageBounds(Rect outTopOrLeftBounds, Rect outBottomOrRightBounds) { outTopOrLeftBounds.set(mSplitLayout.getBounds1()); outBottomOrRightBounds.set(mSplitLayout.getBounds2()); @@ -841,91 +939,220 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, if (mMainUnfoldController != null && mSideUnfoldController != null) { mMainUnfoldController.onSplitVisibilityChanged(mDividerVisible); mSideUnfoldController.onSplitVisibilityChanged(mDividerVisible); + updateUnfoldBounds(); } } - private void onStageRootTaskAppeared(StageListenerImpl stageListener) { - if (mMainStageListener.mHasRootTask && mSideStageListener.mHasRootTask) { - 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 */); - wct.setLaunchAdjacentFlagRoot(mSideStage.mRootTaskInfo.token); - mTaskOrganizer.applyTransaction(wct); + @Override + @CallSuper + public void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash) { + if (mRootTaskInfo != null || taskInfo.hasParentTask()) { + throw new IllegalArgumentException(this + "\n Unknown task appeared: " + taskInfo); } + + mRootTaskInfo = taskInfo; + mRootTaskLeash = leash; + + if (mSplitLayout == null) { + mSplitLayout = new SplitLayout(TAG + "SplitDivider", mContext, + mRootTaskInfo.configuration, this, mParentContainerCallbacks, + mDisplayImeController, mTaskOrganizer, + PARALLAX_ALIGN_CENTER /* parallaxType */); + mDisplayInsetsController.addInsetsChangedListener(mDisplayId, mSplitLayout); + } + + if (mMainUnfoldController != null && mSideUnfoldController != null) { + mMainUnfoldController.init(); + mSideUnfoldController.init(); + } + + onRootTaskAppeared(); } - private void onStageRootTaskVanished(StageListenerImpl stageListener) { - if (stageListener == mMainStageListener || stageListener == mSideStageListener) { - final WindowContainerTransaction wct = new WindowContainerTransaction(); - wct.clearLaunchAdjacentFlagRoot(mSideStage.mRootTaskInfo.token); - // Deactivate the main stage if it no longer has a root task. - mMainStage.deactivate(wct); - mTaskOrganizer.applyTransaction(wct); + @Override + @CallSuper + public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) { + if (mRootTaskInfo == null || mRootTaskInfo.taskId != taskInfo.taskId) { + throw new IllegalArgumentException(this + "\n Unknown task info changed: " + taskInfo); + } + + 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; + } + // Clear the divider remote animating flag as the divider will be re-rendered to apply + // the new rotation config. + mIsDividerRemoteAnimating = false; + mSplitLayout.update(null /* t */); + onLayoutSizeChanged(mSplitLayout); } } - private void setDividerVisibility(boolean visible) { - if (mIsDividerRemoteAnimating || mDividerVisible == visible) return; - mDividerVisible = visible; - if (visible) { - mSplitLayout.init(); - updateUnfoldBounds(); - } else { + @Override + @CallSuper + public void onTaskVanished(ActivityManager.RunningTaskInfo taskInfo) { + if (mRootTaskInfo == null) { + throw new IllegalArgumentException(this + "\n Unknown task vanished: " + taskInfo); + } + + onRootTaskVanished(); + + if (mSplitLayout != null) { mSplitLayout.release(); + mSplitLayout = null; } - sendSplitVisibilityChanged(); + + mRootTaskInfo = null; + } + + + @VisibleForTesting + void onRootTaskAppeared() { + // Wait unit all root tasks appeared. + if (mRootTaskInfo == null + || !mMainStageListener.mHasRootTask + || !mSideStageListener.mHasRootTask) { + return; + } + + final WindowContainerTransaction wct = new WindowContainerTransaction(); + 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.setLaunchAdjacentFlagRoot(mSideStage.mRootTaskInfo.token); + mTaskOrganizer.applyTransaction(wct); + } + + private void onRootTaskVanished() { + final WindowContainerTransaction wct = new WindowContainerTransaction(); + if (mRootTaskInfo != null) { + wct.clearLaunchAdjacentFlagRoot(mRootTaskInfo.token); + } + applyExitSplitScreen(null /* childrenToTop */, wct, EXIT_REASON_ROOT_TASK_VANISHED); + mDisplayInsetsController.removeInsetsChangedListener(mDisplayId, mSplitLayout); } 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); + + // Wait for both stages having the same visibility to prevent causing flicker. + if (mainStageVisible != sideStageVisible) { + return; } - if (bothStageInvisible) { + if (!mainStageVisible) { + // Both stages are not visible, check if it needs to dismiss split screen. 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. + // Don't dismiss split screen when both stages are not visible due to sleeping + // display. || (!mMainStage.mRootTaskInfo.isSleeping && !mSideStage.mRootTaskInfo.isSleeping)) { - // Don't dismiss staged split when both stages are not visible due to sleeping display, - // like the cases keyguard showing or screen off. exitSplitScreen(null /* childrenToTop */, EXIT_REASON_RETURN_HOME); } } - exitSplitScreenIfKeyguardOccluded(); 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); - } + t.setVisibility(mSideStage.mRootLeash, sideStageVisible) + .setVisibility(mMainStage.mRootLeash, mainStageVisible); + setDividerVisibility(mainStageVisible, t); }); } - private void applyDividerVisibility(SurfaceControl.Transaction t) { - if (mIsDividerRemoteAnimating) return; + private void setDividerVisibility(boolean visible, @Nullable SurfaceControl.Transaction t) { + if (visible == mDividerVisible) { + return; + } + + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, + "%s: Request to %s divider bar from %s.", TAG, + (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); + return; + } + + mDividerVisible = visible; + sendSplitVisibilityChanged(); + if (mIsDividerRemoteAnimating) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, + "%s: Skip animating divider bar due to it's remote animating.", TAG); + return; + } + + if (t != null) { + applyDividerVisibility(t); + } else { + mSyncQueue.runInSync(transaction -> applyDividerVisibility(transaction)); + } + } + + private void applyDividerVisibility(SurfaceControl.Transaction t) { final SurfaceControl dividerLeash = mSplitLayout.getDividerLeash(); - if (dividerLeash == null) return; + if (dividerLeash == null) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, + "%s: Skip animating divider bar due to divider leash not ready.", TAG); + return; + } + if (mIsDividerRemoteAnimating) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, + "%s: Skip animating divider bar due to it's remote animating.", TAG); + return; + } + + if (mDividerFadeInAnimator != null && mDividerFadeInAnimator.isRunning()) { + mDividerFadeInAnimator.cancel(); + } if (mDividerVisible) { - t.show(dividerLeash) - .setAlpha(dividerLeash, 1) - .setLayer(dividerLeash, SPLIT_DIVIDER_LAYER) - .setPosition(dividerLeash, - mSplitLayout.getDividerBounds().left, - mSplitLayout.getDividerBounds().top); + final SurfaceControl.Transaction transaction = mTransactionPool.acquire(); + mDividerFadeInAnimator = ValueAnimator.ofFloat(0f, 1f); + mDividerFadeInAnimator.addUpdateListener(animation -> { + if (dividerLeash == null || !dividerLeash.isValid()) { + mDividerFadeInAnimator.cancel(); + return; + } + transaction.setFrameTimelineVsync(Choreographer.getInstance().getVsyncId()); + transaction.setAlpha(dividerLeash, (float) animation.getAnimatedValue()); + transaction.apply(); + }); + mDividerFadeInAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + if (dividerLeash == null || !dividerLeash.isValid()) { + mDividerFadeInAnimator.cancel(); + return; + } + transaction.show(dividerLeash); + transaction.setAlpha(dividerLeash, 0); + transaction.setLayer(dividerLeash, Integer.MAX_VALUE); + transaction.setPosition(dividerLeash, + mSplitLayout.getRefDividerBounds().left, + mSplitLayout.getRefDividerBounds().top); + transaction.apply(); + } + + @Override + public void onAnimationEnd(Animator animation) { + mTransactionPool.release(transaction); + mDividerFadeInAnimator = null; + } + }); + + mDividerFadeInAnimator.start(); } else { t.hide(dividerLeash); } @@ -942,13 +1169,13 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, // Exit to side stage if main stage no longer has children. exitSplitScreen(mSideStage, EXIT_REASON_APP_FINISHED); } - } else if (isSideStage) { + } else if (isSideStage && !mMainStage.isActive()) { final WindowContainerTransaction wct = new WindowContainerTransaction(); - // Make sure the main stage is active. - mMainStage.activate(getMainStageBounds(), wct, true /* reparent */); - mSideStage.moveToTop(getSideStageBounds(), wct); + mSplitLayout.init(); + prepareEnterSplitScreen(wct); mSyncQueue.queue(wct); - mSyncQueue.runInSync(t -> updateSurfaceBounds(mSplitLayout, t)); + mSyncQueue.runInSync(t -> + updateSurfaceBounds(mSplitLayout, t, false /* applyResizingOffset */)); } if (mMainStageListener.mHasChildren && mSideStageListener.mHasChildren) { mShouldUpdateRecents = true; @@ -963,23 +1190,22 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, } } - @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); + if (!ENABLE_SHELL_TRANSITIONS) { + exitSplitScreen(mainStageToTop ? mMainStage : mSideStage, EXIT_REASON_DRAG_DIVIDER); return; } - exitSplitScreen(mainStageToTop ? mMainStage : mSideStage, EXIT_REASON_DRAG_DIVIDER); + + 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); } @Override @@ -992,16 +1218,23 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, @Override public void onLayoutPositionChanging(SplitLayout layout) { - mSyncQueue.runInSync(t -> updateSurfaceBounds(layout, t)); + final SurfaceControl.Transaction t = mTransactionPool.acquire(); + t.setFrameTimelineVsync(Choreographer.getInstance().getVsyncId()); + updateSurfaceBounds(layout, t, false /* applyResizingOffset */); + t.apply(); + mTransactionPool.release(t); } @Override public void onLayoutSizeChanging(SplitLayout layout) { - mSyncQueue.runInSync(t -> { - updateSurfaceBounds(layout, t); - mMainStage.onResizing(getMainStageBounds(), t); - mSideStage.onResizing(getSideStageBounds(), t); - }); + 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); + t.apply(); + mTransactionPool.release(t); } @Override @@ -1011,20 +1244,27 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, updateUnfoldBounds(); mSyncQueue.queue(wct); mSyncQueue.runInSync(t -> { - updateSurfaceBounds(layout, t); - mMainStage.onResized(getMainStageBounds(), t); - mSideStage.onResized(getSideStageBounds(), t); + setResizingSplits(false /* resizing */); + updateSurfaceBounds(layout, t, false /* applyResizingOffset */); + mMainStage.onResized(t); + mSideStage.onResized(t); }); mLogger.logResize(mSplitLayout.getDividerPositionAsFraction()); } private void updateUnfoldBounds() { if (mMainUnfoldController != null && mSideUnfoldController != null) { - mMainUnfoldController.onLayoutChanged(getMainStageBounds()); - mSideUnfoldController.onLayoutChanged(getSideStageBounds()); + mMainUnfoldController.onLayoutChanged(getMainStageBounds(), getMainStagePosition(), + isLandscape()); + mSideUnfoldController.onLayoutChanged(getSideStageBounds(), getSideStagePosition(), + isLandscape()); } } + private boolean isLandscape() { + return mSplitLayout.isLandscape(); + } + /** * 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 @@ -1037,13 +1277,25 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, layout.applyTaskChanges(wct, topLeftStage.mRootTaskInfo, bottomRightStage.mRootTaskInfo); } - void updateSurfaceBounds(@Nullable SplitLayout layout, @NonNull SurfaceControl.Transaction t) { + 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); + bottomRightStage.mRootLeash, topLeftStage.mDimLayer, bottomRightStage.mDimLayer, + 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 @@ -1052,9 +1304,9 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, return SPLIT_POSITION_UNDEFINED; } - if (token.equals(mMainStage.mRootTaskInfo.getToken())) { + if (mMainStage.containsToken(token)) { return getMainStagePosition(); - } else if (token.equals(mSideStage.mRootTaskInfo.getToken())) { + } else if (mSideStage.containsToken(token)) { return getSideStagePosition(); } @@ -1073,35 +1325,31 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, 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, false /* applyDismissingParallax */); - mDisplayInsetsController.addInsetsChangedListener(mDisplayId, mSplitLayout); - - if (mMainUnfoldController != null && mSideUnfoldController != null) { - mMainUnfoldController.init(); - mSideUnfoldController.init(); - } + public void onDisplayAdded(int displayId) { + if (displayId != DEFAULT_DISPLAY) { + return; } + mDisplayController.addDisplayChangingController(this::onRotateDisplay); } @Override - public void onDisplayAreaVanished(DisplayAreaInfo displayAreaInfo) { - throw new IllegalStateException("Well that was unexpected..."); + public void onDisplayConfigurationChanged(int displayId, Configuration newConfig) { + if (displayId != DEFAULT_DISPLAY) { + return; + } + mDisplayLayout.set(mDisplayController.getDisplayLayout(displayId)); } - @Override - public void onDisplayAreaInfoChanged(DisplayAreaInfo displayAreaInfo) { - mDisplayAreaInfo = displayAreaInfo; - if (mSplitLayout != null - && mSplitLayout.updateConfiguration(mDisplayAreaInfo.configuration) - && mMainStage.isActive()) { - onLayoutSizeChanged(mSplitLayout); - } + private void onRotateDisplay(int displayId, int fromRotation, int toRotation, + 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()); + updateWindowBounds(mSplitLayout, wct); + updateUnfoldBounds(); } private void onFoldedStateChanged(boolean folded) { @@ -1152,14 +1400,35 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, @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; + if (mMainStage.isActive()) { + final TransitionRequestInfo.DisplayChange displayChange = + request.getDisplayChange(); + if (request.getType() == TRANSIT_CHANGE && displayChange != null + && displayChange.getStartRotation() != displayChange.getEndRotation()) { + mSplitLayout.setFreezeDividerWindow(true); + } + // Still want to monitor everything while in split-screen, so return non-null. + return new WindowContainerTransaction(); + } else { + return null; + } + } else if (triggerTask.displayId != mDisplayId) { + // Skip handling task on the other display. + return 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. + final boolean isOpening = isOpeningType(type); + final boolean inFullscreen = triggerTask.getWindowingMode() == WINDOWING_MODE_FULLSCREEN; + + if (isOpening && inFullscreen) { + // One task is opening into fullscreen mode, remove the corresponding split record. + mRecentTasks.ifPresent(recentTasks -> recentTasks.removeSplitPair(triggerTask.taskId)); + } + + if (mMainStage.isActive()) { + // 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), @@ -1167,53 +1436,85 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, 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 + // 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; + int dismissTop = getStageType(stage) == STAGE_TYPE_MAIN ? STAGE_TYPE_SIDE + : STAGE_TYPE_MAIN; + prepareExitSplitScreen(dismissTop, out); + mSplitTransitions.startDismissTransition(transition, out, this, dismissTop, + EXIT_REASON_APP_FINISHED); } - } else { - if (triggerTask.getActivityType() == ACTIVITY_TYPE_HOME && isOpeningType(type)) { - // Going home so dismiss both. - mDismissTop = STAGE_TYPE_UNDEFINED; + } 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 (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."); - } + if (isOpening && getStageOfTask(triggerTask) != null) { + // One task is appearing into split, prepare to enter split screen. + out = new WindowContainerTransaction(); + prepareEnterSplitScreen(out); + mSplitTransitions.mPendingEnter = transition; } } return out; } @Override + public void mergeAnimation(IBinder transition, TransitionInfo info, + SurfaceControl.Transaction t, IBinder mergeTarget, + Transitions.TransitionFinishCallback finishCallback) { + mSplitTransitions.mergeAnimation(transition, info, t, mergeTarget, finishCallback); + } + + @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); + } + } + + @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) { + if (transition != mSplitTransitions.mPendingEnter + && transition != mSplitTransitions.mPendingRecent + && (mSplitTransitions.mPendingDismiss == null + || mSplitTransitions.mPendingDismiss.mTransition != 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. - if (!isSplitScreenVisible()) return false; + if (!mMainStage.isActive()) return false; + mSplitLayout.setFreezeDividerWindow(false); for (int iC = 0; iC < info.getChanges().size(); ++iC) { final TransitionInfo.Change change = info.getChanges().get(iC); + if (change.getMode() == TRANSIT_CHANGE + && (change.getFlags() & FLAG_IS_DISPLAY) != 0) { + mSplitLayout.update(startTransaction); + } + final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo(); if (taskInfo == null || !taskInfo.hasParentTask()) continue; final StageTaskListener stage = getStageOfTask(taskInfo); @@ -1249,8 +1550,12 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, boolean shouldAnimate = true; if (mSplitTransitions.mPendingEnter == transition) { shouldAnimate = startPendingEnterAnimation(transition, info, startTransaction); - } else if (mSplitTransitions.mPendingDismiss == transition) { - shouldAnimate = startPendingDismissAnimation(transition, info, startTransaction); + } else if (mSplitTransitions.mPendingRecent == transition) { + shouldAnimate = startPendingRecentAnimation(transition, info, startTransaction); + } else if (mSplitTransitions.mPendingDismiss != null + && mSplitTransitions.mPendingDismiss.mTransition == transition) { + shouldAnimate = startPendingDismissAnimation( + mSplitTransitions.mPendingDismiss, info, startTransaction, finishTransaction); } if (!shouldAnimate) return false; @@ -1259,63 +1564,74 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, return true; } + void onTransitionAnimationComplete() { + // If still playing, let it finish. + if (!mMainStage.isActive()) { + // 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) { - 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 @StageType int stageType = getStageType(getStageOfTask(taskInfo)); - if (stageType == STAGE_TYPE_MAIN) { - mainChild = change; - } else if (stageType == STAGE_TYPE_SIDE) { - sideChild = change; - } + // First, verify that we actually have opened apps in both splits. + 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 @StageType int stageType = getStageType(getStageOfTask(taskInfo)); + if (stageType == STAGE_TYPE_MAIN) { + mainChild = change; + } else if (stageType == STAGE_TYPE_SIDE) { + sideChild = change; } + } + + // 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."); + } + } else { 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"); + // 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 (mainChild != null && !mMainStage.containsTask(mainChild.getTaskInfo().taskId)) { + Log.w(TAG, "Expected onTaskAppeared on " + mMainStage + + " to have been called with " + mainChild.getTaskInfo().taskId + + " before startAnimation()."); } + if (sideChild != null && !mSideStage.containsTask(sideChild.getTaskInfo().taskId)) { + Log.w(TAG, "Expected onTaskAppeared on " + mSideStage + + " to have been called with " + sideChild.getTaskInfo().taskId + + " before startAnimation()."); + } + + finishEnterSplitScreen(t); + addDividerBarToTransition(info, t, true /* show */); + return true; } - private boolean startPendingDismissAnimation(@NonNull IBinder transition, - @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction t) { + private boolean startPendingDismissAnimation( + @NonNull SplitScreenTransitions.DismissTransition dismissTransition, + @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 // transitions locally, but remotes (like Launcher) may get confused if they were // depending on listener callbacks. This can happen because task-organizer callbacks @@ -1343,34 +1659,81 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, + "] 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); + } + } + } + }); + mShouldUpdateRecents = false; + // 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) + // Reset crops so they don't interfere with subsequent launches + t.setWindowCrop(mMainStage.mRootLeash, null); + t.setWindowCrop(mSideStage.mRootLeash, null); + if (dismissTransition.mDismissTop == STAGE_TYPE_UNDEFINED) { + logExit(dismissTransition.mReason); // TODO: Have a proper remote for this. Until then, though, reset state and use the // normal animation stuff (which falls back to the normal launcher remote). - t.hide(mSplitLayout.getDividerLeash()); - setDividerVisibility(false); + mSplitLayout.release(t); mSplitTransitions.mPendingDismiss = null; return false; + } else { + logExitToStage(dismissTransition.mReason, + dismissTransition.mDismissTop == STAGE_TYPE_MAIN); } addDividerBarToTransition(info, t, false /* show */); // We're dismissing split by moving the other one to fullscreen. // Since we don't have any animations for this yet, just use the internal example // animations. + + // Hide divider and dim layer on transition finished. + setDividerVisibility(false, finishT); + finishT.hide(mMainStage.mDimLayer); + finishT.hide(mSideStage.mDimLayer); return true; } + private boolean startPendingRecentAnimation(@NonNull IBinder transition, + @NonNull TransitionInfo info, @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; + } + + if (returnToHome) { + // When returning to home from recent apps, the splitting tasks are already hidden, so + // append the reset of dismissing operations into the clean-up wct. + prepareExitSplitScreen(STAGE_TYPE_UNDEFINED, wct); + setSplitsVisible(false); + logExit(EXIT_REASON_RETURN_HOME); + } else { + setDividerVisibility(true, finishT); + } + } + private void addDividerBarToTransition(@NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction t, boolean show) { final SurfaceControl leash = mSplitLayout.getDividerLeash(); @@ -1386,7 +1749,7 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, // Be default, make it visible. The remote animator can adjust alpha if it plans to animate. if (show) { t.setAlpha(leash, 1.f); - t.setLayer(leash, SPLIT_DIVIDER_LAYER); + t.setLayer(leash, Integer.MAX_VALUE); t.setPosition(leash, bounds.left, bounds.top); t.show(leash); } @@ -1469,7 +1832,7 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, @Override public void onRootTaskAppeared() { mHasRootTask = true; - StageCoordinator.this.onStageRootTaskAppeared(this); + StageCoordinator.this.onRootTaskAppeared(); } @Override @@ -1499,13 +1862,24 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, @Override public void onRootTaskVanished() { reset(); - StageCoordinator.this.onStageRootTaskVanished(this); + StageCoordinator.this.onRootTaskVanished(); } @Override public void onNoLongerSupportMultiWindow() { if (mMainStage.isActive()) { - StageCoordinator.this.exitSplitScreen(null /* childrenToTop */, + final boolean isMainStage = mMainStageListener == this; + if (!ENABLE_SHELL_TRANSITIONS) { + StageCoordinator.this.exitSplitScreen(isMainStage ? mMainStage : mSideStage, + EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW); + 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, EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java index 04e20db369ef..949bf5f55808 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 @@ -39,7 +39,6 @@ import android.window.WindowContainerTransaction; import androidx.annotation.NonNull; -import com.android.internal.R; import com.android.launcher3.icons.IconProvider; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.SurfaceUtils; @@ -53,8 +52,8 @@ 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. + * {@link #addTask(ActivityManager.RunningTaskInfo, WindowContainerTransaction)} for the centralized + * {@link StageCoordinator} to perform hierarchy operations in-sync with other containers. * * @see StageCoordinator */ @@ -108,12 +107,7 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener { mSurfaceSession = surfaceSession; mIconProvider = iconProvider; mStageTaskUnfoldController = stageTaskUnfoldController; - - // No need to create root task if the device is using legacy split screen. - // TODO(b/199236198): Remove this check after totally deprecated legacy split. - if (!context.getResources().getBoolean(R.bool.config_useLegacySplit)) { - taskOrganizer.createRootTask(displayId, WINDOWING_MODE_MULTI_WINDOW, this); - } + taskOrganizer.createRootTask(displayId, WINDOWING_MODE_MULTI_WINDOW, this); } int getChildCount() { @@ -124,6 +118,20 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener { return mChildrenTaskInfo.contains(taskId); } + 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 false; + } + /** * Returns the top visible child task's id. */ @@ -173,7 +181,7 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener { @Override @CallSuper public void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash) { - if (mRootTaskInfo == null && !taskInfo.hasParentTask()) { + if (mRootTaskInfo == null) { mRootLeash = leash; mRootTaskInfo = taskInfo; mSplitDecorManager = new SplitDecorManager( @@ -216,14 +224,12 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener { if (mRootTaskInfo.taskId == taskInfo.taskId) { // Inflates split decor view only when the root task is visible. if (mRootTaskInfo.isVisible != taskInfo.isVisible) { - mSyncQueue.runInSync(t -> { - if (taskInfo.isVisible) { - mSplitDecorManager.inflate(mContext, mRootLeash, - taskInfo.configuration.windowConfiguration.getBounds()); - } else { - mSplitDecorManager.release(t); - } - }); + if (taskInfo.isVisible) { + mSplitDecorManager.inflate(mContext, mRootLeash, + taskInfo.configuration.windowConfiguration.getBounds()); + } else { + mSyncQueue.runInSync(t -> mSplitDecorManager.release(t)); + } } mRootTaskInfo = taskInfo; } else if (taskInfo.parentTaskId == mRootTaskInfo.taskId) { @@ -280,10 +286,20 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener { @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) { - b.setParent(mRootLeash); + return mRootLeash; } else if (mChildrenLeashes.contains(taskId)) { - b.setParent(mChildrenLeashes.get(taskId)); + return mChildrenLeashes.get(taskId); } else { throw new IllegalArgumentException("There is no surface for taskId=" + taskId); } @@ -295,23 +311,19 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener { } } - void onResized(Rect newBounds, SurfaceControl.Transaction t) { + void onResized(SurfaceControl.Transaction t) { if (mSplitDecorManager != null) { - mSplitDecorManager.onResized(newBounds, t); + mSplitDecorManager.onResized(t); } } void addTask(ActivityManager.RunningTaskInfo task, WindowContainerTransaction wct) { - wct.reparent(task.token, mRootTaskInfo.token, true /* onTop*/); - } + // Clear overridden bounds and windowing mode to make sure the child task can inherit + // windowing mode and bounds from split root. + wct.setWindowingMode(task.token, WINDOWING_MODE_UNDEFINED) + .setBounds(task.token, null); - void moveToTop(Rect rootBounds, WindowContainerTransaction wct) { - final WindowContainerToken rootToken = mRootTaskInfo.token; - wct.setBounds(rootToken, rootBounds).reorder(rootToken, true /* onTop */); - } - - void setBounds(Rect bounds, WindowContainerTransaction wct) { - wct.setBounds(mRootTaskInfo.token, bounds); + wct.reparent(task.token, mRootTaskInfo.token, true /* onTop*/); } void reorderChild(int taskId, boolean onTop, WindowContainerTransaction wct) { @@ -329,8 +341,13 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener { } } - void setVisibility(boolean visible, WindowContainerTransaction wct) { - wct.reorder(mRootTaskInfo.token, visible /* onTop */); + void evictInvisibleChildren(WindowContainerTransaction wct) { + for (int i = mChildrenTaskInfo.size() - 1; i >= 0; i--) { + final ActivityManager.RunningTaskInfo taskInfo = mChildrenTaskInfo.valueAt(i); + if (!taskInfo.isVisible) { + wct.reparent(taskInfo.token, null /* parent */, false /* onTop */); + } + } } void onSplitScreenListenerRegistered(SplitScreen.SplitScreenListener listener, 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 index 4849163e96fd..59eecb5db136 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskUnfoldController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskUnfoldController.java @@ -18,11 +18,15 @@ 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; @@ -33,6 +37,7 @@ 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; @@ -166,12 +171,13 @@ public class StageTaskUnfoldController implements UnfoldListener, OnInsetsChange * Called when split screen stage bounds changed * @param bounds new bounds for this stage */ - public void onLayoutChanged(Rect bounds) { + 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(); + context.update(splitPosition, isLandscape); } } @@ -200,20 +206,27 @@ public class StageTaskUnfoldController implements UnfoldListener, OnInsetsChange 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); - if (mTaskbarInsetsSource != null) { + boolean taskbarExpanded = isTaskbarExpanded(); + if (taskbarExpanded) { // Only insets the cropping window with taskbar when taskbar is expanded - if (mTaskbarInsetsSource.getFrame().height() >= mExpandedTaskBarHeight) { - mStartCropRect.inset(mTaskbarInsetsSource - .calculateVisibleInsets(mStartCropRect)); - } + mStartCropRect.inset(mTaskbarInsetsSource.calculateVisibleInsets(mStartCropRect)); } // Offset to surface coordinates as layout bounds are in screen coordinates @@ -223,7 +236,46 @@ public class StageTaskUnfoldController implements UnfoldListener, OnInsetsChange int maxSize = Math.max(mEndCropRect.width(), mEndCropRect.height()); int margin = (int) (maxSize * CROPPING_START_MARGIN_FRACTION); - mStartCropRect.inset(margin, margin, margin, margin); + + // 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/stagesplit/SplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SplitScreenController.java index f1520edf53b1..07174051a344 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SplitScreenController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SplitScreenController.java @@ -253,7 +253,8 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, RemoteAnimationTarget[] wallpapers, RemoteAnimationTarget[] nonApps, IRemoteAnimationFinishedCallback finishedCallback, SurfaceControl.Transaction t) { - mStageCoordinator.updateSurfaceBounds(null /* layout */, t); + mStageCoordinator.updateSurfaceBounds(null /* layout */, t, + false /* applyResizingOffset */); if (apps != null) { for (int i = 0; i < apps.length; ++i) { 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 index af9a5aa501e8..018365420177 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SplitScreenTransitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SplitScreenTransitions.java @@ -127,12 +127,6 @@ class SplitScreenTransitions { } // TODO(shell-transitions): screenshot here final Rect startBounds = new Rect(change.getStartAbsBounds()); - if (info.getType() == TRANSIT_SPLIT_DISMISS_SNAP) { - // Dismissing split via snap which means the still-visible task has been - // dragged to its end position at animation start so reflect that here. - startBounds.offsetTo(change.getEndAbsBounds().left, - change.getEndAbsBounds().top); - } final Rect endBounds = new Rect(change.getEndAbsBounds()); startBounds.offset(-info.getRootOffset().x, -info.getRootOffset().y); endBounds.offset(-info.getRootOffset().x, -info.getRootOffset().y); 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 index a17942ff7cff..de0feeecad4b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/StageCoordinator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/StageCoordinator.java @@ -23,7 +23,6 @@ import static android.view.WindowManager.TRANSIT_OPEN; import static android.view.WindowManager.TRANSIT_TO_BACK; import static android.view.WindowManager.TRANSIT_TO_FRONT; import static android.view.WindowManager.transitTypeToString; -import static android.view.WindowManagerPolicyConstants.SPLIT_DIVIDER_LAYER; import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__APP_DOES_NOT_SUPPORT_MULTIWINDOW; import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__APP_FINISHED; @@ -266,7 +265,8 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, mMainStage.activate(getMainStageBounds(), wct); mSideStage.addTask(task, getSideStageBounds(), wct); mSyncQueue.queue(wct); - mSyncQueue.runInSync(t -> updateSurfaceBounds(null /* layout */, t)); + mSyncQueue.runInSync( + t -> updateSurfaceBounds(null /* layout */, t, false /* applyResizingOffset */)); return true; } @@ -345,9 +345,9 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, } @Override - public void onAnimationCancelled() { + public void onAnimationCancelled(boolean isKeyguardOccluded) { try { - adapter.getRunner().onAnimationCancelled(); + adapter.getRunner().onAnimationCancelled(isKeyguardOccluded); } catch (RemoteException e) { Slog.e(TAG, "Error starting remote animation", e); } @@ -722,7 +722,7 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, if (mDividerVisible) { t.show(dividerLeash) - .setLayer(dividerLeash, SPLIT_DIVIDER_LAYER) + .setLayer(dividerLeash, Integer.MAX_VALUE) .setPosition(dividerLeash, mSplitLayout.getDividerBounds().left, mSplitLayout.getDividerBounds().top); @@ -738,7 +738,7 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, } if (mDividerVisible) { - t.show(outlineLeash).setLayer(outlineLeash, SPLIT_DIVIDER_LAYER); + t.show(outlineLeash).setLayer(outlineLeash, Integer.MAX_VALUE); } else { t.hide(outlineLeash); } @@ -802,12 +802,12 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, @Override public void onLayoutPositionChanging(SplitLayout layout) { - mSyncQueue.runInSync(t -> updateSurfaceBounds(layout, t)); + mSyncQueue.runInSync(t -> updateSurfaceBounds(layout, t, true /* applyResizingOffset */)); } @Override public void onLayoutSizeChanging(SplitLayout layout) { - mSyncQueue.runInSync(t -> updateSurfaceBounds(layout, t)); + mSyncQueue.runInSync(t -> updateSurfaceBounds(layout, t, true /* applyResizingOffset */)); mSideStage.setOutlineVisibility(false); } @@ -817,7 +817,7 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, updateWindowBounds(layout, wct); updateUnfoldBounds(); mSyncQueue.queue(wct); - mSyncQueue.runInSync(t -> updateSurfaceBounds(layout, t)); + mSyncQueue.runInSync(t -> updateSurfaceBounds(layout, t, false /* applyResizingOffset */)); mSideStage.setOutlineVisibility(true); mLogger.logResize(mSplitLayout.getDividerPositionAsFraction()); } @@ -841,13 +841,15 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, layout.applyTaskChanges(wct, topLeftStage.mRootTaskInfo, bottomRightStage.mRootTaskInfo); } - void updateSurfaceBounds(@Nullable SplitLayout layout, @NonNull SurfaceControl.Transaction t) { + 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); + bottomRightStage.mRootLeash, topLeftStage.mDimLayer, bottomRightStage.mDimLayer, + applyResizingOffset); } @Override @@ -883,7 +885,7 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, if (mSplitLayout == null) { mSplitLayout = new SplitLayout(TAG + "SplitDivider", mContext, mDisplayAreaInfo.configuration, this, mParentContainerCallbacks, - mDisplayImeController, mTaskOrganizer, true /* applyDismissingParallax */); + mDisplayImeController, mTaskOrganizer, SplitLayout.PARALLAX_DISMISSING); mDisplayInsetsController.addInsetsChangedListener(mDisplayId, mSplitLayout); if (mMainUnfoldController != null && mSideUnfoldController != null) { @@ -1190,7 +1192,7 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, // Be default, make it visible. The remote animator can adjust alpha if it plans to animate. if (show) { t.setAlpha(leash, 1.f); - t.setLayer(leash, SPLIT_DIVIDER_LAYER); + t.setLayer(leash, Integer.MAX_VALUE); t.setPosition(leash, bounds.left, bounds.top); t.show(leash); } 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 index 8b36c9406b15..7b679580fa87 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/StageTaskListener.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/StageTaskListener.java @@ -227,10 +227,20 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener { @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) { - b.setParent(mRootLeash); + return mRootLeash; } else if (mChildrenLeashes.contains(taskId)) { - b.setParent(mChildrenLeashes.get(taskId)); + return mChildrenLeashes.get(taskId); } else { throw new IllegalArgumentException("There is no surface for taskId=" + taskId); } 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 e7b5744dd21b..014f02bcf8b7 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 @@ -56,7 +56,7 @@ 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 = StartingSurfaceDrawer.TAG; + 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 = 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 b191cabcf6aa..8cee4f1dc8fb 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 @@ -18,10 +18,13 @@ package com.android.wm.shell.startingsurface; import static android.os.Process.THREAD_PRIORITY_TOP_APP_BOOST; import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER; -import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_EMPTY_SPLASH_SCREEN; 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; @@ -32,6 +35,8 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.ActivityInfo; +import android.content.pm.PackageManager; +import android.content.res.Configuration; import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.Bitmap; @@ -46,14 +51,16 @@ import android.graphics.drawable.LayerDrawable; import android.net.Uri; import android.os.Handler; import android.os.HandlerThread; +import android.os.SystemClock; import android.os.Trace; import android.os.UserHandle; import android.util.ArrayMap; +import android.util.DisplayMetrics; import android.util.Slog; import android.view.ContextThemeWrapper; import android.view.SurfaceControl; -import android.view.View; import android.window.SplashScreenView; +import android.window.StartingWindowInfo; import android.window.StartingWindowInfo.StartingWindowType; import com.android.internal.R; @@ -61,9 +68,11 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.graphics.palette.Palette; import com.android.internal.graphics.palette.Quantizer; import com.android.internal.graphics.palette.VariationalKMeansQuantizer; +import com.android.internal.protolog.common.ProtoLog; import com.android.launcher3.icons.BaseIconFactory; import com.android.launcher3.icons.IconProvider; import com.android.wm.shell.common.TransactionPool; +import com.android.wm.shell.protolog.ShellProtoLogGroup; import java.util.List; import java.util.function.Consumer; @@ -78,8 +87,7 @@ import java.util.function.UnaryOperator; * @hide */ public class SplashscreenContentDrawer { - private static final String TAG = StartingSurfaceDrawer.TAG; - private static final boolean DEBUG = StartingSurfaceDrawer.DEBUG_SPLASH_SCREEN; + private static final String TAG = StartingWindowController.TAG; // 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 @@ -96,7 +104,7 @@ public class SplashscreenContentDrawer { */ private static final float NO_BACKGROUND_SCALE = 192f / 160; private final Context mContext; - private final IconProvider mIconProvider; + private final HighResIconProvider mHighResIconProvider; private int mIconSize; private int mDefaultIconSize; @@ -112,7 +120,7 @@ public class SplashscreenContentDrawer { SplashscreenContentDrawer(Context context, IconProvider iconProvider, TransactionPool pool) { mContext = context; - mIconProvider = iconProvider; + mHighResIconProvider = new HighResIconProvider(mContext, iconProvider); mTransactionPool = pool; // Initialize Splashscreen worker thread @@ -137,8 +145,8 @@ public class SplashscreenContentDrawer { * executed on splash screen thread. Note that the view can be * null if failed. */ - void createContentView(Context context, @StartingWindowType int suggestType, ActivityInfo info, - int taskId, Consumer<SplashScreenView> splashScreenViewConsumer, + void createContentView(Context context, @StartingWindowType int suggestType, + StartingWindowInfo info, Consumer<SplashScreenView> splashScreenViewConsumer, Consumer<Runnable> uiThreadInitConsumer) { mSplashscreenWorkerHandler.post(() -> { SplashScreenView contentView; @@ -149,7 +157,7 @@ public class SplashscreenContentDrawer { Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); } catch (RuntimeException e) { Slog.w(TAG, "failed creating starting window content at taskId: " - + taskId, e); + + info.taskInfo.taskId, e); contentView = null; } splashScreenViewConsumer.accept(contentView); @@ -240,7 +248,7 @@ public class SplashscreenContentDrawer { return null; } - private SplashScreenView makeSplashScreenContentView(Context context, ActivityInfo ai, + private SplashScreenView makeSplashScreenContentView(Context context, StartingWindowInfo info, @StartingWindowType int suggestType, Consumer<Runnable> uiThreadInitConsumer) { updateDensity(); @@ -249,6 +257,9 @@ public class SplashscreenContentDrawer { final Drawable legacyDrawable = suggestType == STARTING_WINDOW_TYPE_LEGACY_SPLASH_SCREEN ? peekLegacySplashscreenContent(context, mTmpAttrs) : null; + final ActivityInfo ai = info.targetActivityInfo != null + ? info.targetActivityInfo + : info.taskInfo.topActivityInfo; final int themeBGColor = legacyDrawable != null ? getBGColorFromCache(ai, () -> estimateWindowBGColor(legacyDrawable)) : getBGColorFromCache(ai, () -> peekWindowBGColor(context, mTmpAttrs)); @@ -257,6 +268,7 @@ public class SplashscreenContentDrawer { .overlayDrawable(legacyDrawable) .chooseStyle(suggestType) .setUiThreadInitConsumer(uiThreadInitConsumer) + .setAllowHandleSolidColor(info.allowHandleSolidColorSplashScreen()) .build(); } @@ -287,20 +299,15 @@ public class SplashscreenContentDrawer { Color.TRANSPARENT); attrs.mSplashScreenIcon = safeReturnAttrDefault((def) -> typedArray.getDrawable( R.styleable.Window_windowSplashScreenAnimatedIcon), null); - attrs.mAnimationDuration = safeReturnAttrDefault((def) -> typedArray.getInt( - R.styleable.Window_windowSplashScreenAnimationDuration, def), 0); attrs.mBrandingImage = safeReturnAttrDefault((def) -> typedArray.getDrawable( R.styleable.Window_windowSplashScreenBrandingImage), null); attrs.mIconBgColor = safeReturnAttrDefault((def) -> typedArray.getColor( R.styleable.Window_windowSplashScreenIconBackgroundColor, def), Color.TRANSPARENT); typedArray.recycle(); - if (DEBUG) { - Slog.d(TAG, "window attributes color: " - + Integer.toHexString(attrs.mWindowBgColor) - + " icon " + attrs.mSplashScreenIcon + " duration " + attrs.mAnimationDuration - + " brandImage " + attrs.mBrandingImage); - } + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW, + "getWindowAttrs: window attributes color: %s, replace icon: %b", + Integer.toHexString(attrs.mWindowBgColor), attrs.mSplashScreenIcon != null); } /** Creates the wrapper with system theme to avoid unexpected styles from app. */ @@ -315,7 +322,33 @@ public class SplashscreenContentDrawer { private Drawable mSplashScreenIcon = null; private Drawable mBrandingImage = null; private int mIconBgColor = Color.TRANSPARENT; - private int mAnimationDuration = 0; + } + + /** + * Get an optimal animation duration to keep the splash screen from showing. + * + * @param animationDuration The animation duration defined from app. + * @param appReadyDuration The real duration from the starting the app to the first app window + * drawn. + */ + @VisibleForTesting + static long getShowingDuration(long animationDuration, long appReadyDuration) { + if (animationDuration <= appReadyDuration) { + // app window ready took longer time than animation, it can be removed ASAP. + return appReadyDuration; + } + if (appReadyDuration < MAX_ANIMATION_DURATION) { + if (animationDuration > MAX_ANIMATION_DURATION + || appReadyDuration < MINIMAL_ANIMATION_DURATION) { + // animation is too long or too short, cut off with minimal duration + return MINIMAL_ANIMATION_DURATION; + } + // animation is longer than dOpt but shorter than max, allow it to play till finish + return MAX_ANIMATION_DURATION; + } + // the shortest duration is longer than dMax, cut off no matter how long the animation + // will be. + return appReadyDuration; } private class StartingWindowViewBuilder { @@ -328,6 +361,8 @@ public class SplashscreenContentDrawer { private Drawable[] mFinalIconDrawables; private int mFinalIconSize = mIconSize; private Consumer<Runnable> mUiThreadInitTask; + /** @see #setAllowHandleSolidColor(boolean) **/ + private boolean mAllowHandleSolidColor; StartingWindowViewBuilder(@NonNull Context context, @NonNull ActivityInfo aInfo) { mContext = context; @@ -354,54 +389,57 @@ public class SplashscreenContentDrawer { return this; } + /** + * If true, the application will receive a the + * {@link + * android.window.SplashScreen.OnExitAnimationListener#onSplashScreenExit(SplashScreenView)} + * callback, effectively copying the {@link SplashScreenView} into the client process. + */ + StartingWindowViewBuilder setAllowHandleSolidColor(boolean allowHandleSolidColor) { + mAllowHandleSolidColor = allowHandleSolidColor; + return this; + } + SplashScreenView build() { Drawable iconDrawable; - final int animationDuration; - if (mSuggestType == STARTING_WINDOW_TYPE_EMPTY_SPLASH_SCREEN + if (mSuggestType == STARTING_WINDOW_TYPE_SOLID_COLOR_SPLASH_SCREEN || mSuggestType == STARTING_WINDOW_TYPE_LEGACY_SPLASH_SCREEN) { // empty or legacy splash screen case - animationDuration = 0; mFinalIconSize = 0; } else if (mTmpAttrs.mSplashScreenIcon != null) { // Using the windowSplashScreenAnimatedIcon attribute iconDrawable = mTmpAttrs.mSplashScreenIcon; - animationDuration = mTmpAttrs.mAnimationDuration; // There is no background below the icon, so scale the icon up if (mTmpAttrs.mIconBgColor == Color.TRANSPARENT || mTmpAttrs.mIconBgColor == mThemeColor) { mFinalIconSize *= NO_BACKGROUND_SCALE; } - createIconDrawable(iconDrawable, false); + createIconDrawable(iconDrawable, false /* legacy */, false /* loadInDetail */); } else { final float iconScale = (float) mIconSize / (float) mDefaultIconSize; final int densityDpi = mContext.getResources().getConfiguration().densityDpi; final int scaledIconDpi = (int) (0.5f + iconScale * densityDpi * NO_BACKGROUND_SCALE); Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "getIcon"); - iconDrawable = mIconProvider.getIcon(mActivityInfo, scaledIconDpi); + iconDrawable = mHighResIconProvider.getIcon( + mActivityInfo, densityDpi, scaledIconDpi); Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); - if (iconDrawable == null) { - iconDrawable = mContext.getPackageManager().getDefaultActivityIcon(); - } if (!processAdaptiveIcon(iconDrawable)) { - if (DEBUG) { - Slog.d(TAG, "The icon is not an AdaptiveIconDrawable"); - } + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW, + "The icon is not an AdaptiveIconDrawable"); Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "legacy_icon_factory"); final ShapeIconFactory factory = new ShapeIconFactory( SplashscreenContentDrawer.this.mContext, scaledIconDpi, mFinalIconSize); - final Bitmap bitmap = factory.createScaledBitmapWithoutShadow( - iconDrawable, true /* shrinkNonAdaptiveIcons */); + final Bitmap bitmap = factory.createScaledBitmapWithoutShadow(iconDrawable); Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); - createIconDrawable(new BitmapDrawable(bitmap), true); + createIconDrawable(new BitmapDrawable(bitmap), true, + mHighResIconProvider.mLoadInDetail); } - animationDuration = 0; } - return fillViewWithIcon(mFinalIconSize, mFinalIconDrawables, animationDuration, - mUiThreadInitTask); + return fillViewWithIcon(mFinalIconSize, mFinalIconDrawables, mUiThreadInitTask); } private class ShapeIconFactory extends BaseIconFactory { @@ -410,14 +448,16 @@ public class SplashscreenContentDrawer { } } - private void createIconDrawable(Drawable iconDrawable, boolean legacy) { + private void createIconDrawable(Drawable iconDrawable, boolean legacy, + boolean loadInDetail) { if (legacy) { mFinalIconDrawables = SplashscreenIconDrawableFactory.makeLegacyIconDrawable( - iconDrawable, mDefaultIconSize, mFinalIconSize, mSplashscreenWorkerHandler); + iconDrawable, mDefaultIconSize, mFinalIconSize, loadInDetail, + mSplashscreenWorkerHandler); } else { mFinalIconDrawables = SplashscreenIconDrawableFactory.makeIconDrawable( - mTmpAttrs.mIconBgColor, mThemeColor, - iconDrawable, mDefaultIconSize, mFinalIconSize, mSplashscreenWorkerHandler); + mTmpAttrs.mIconBgColor, mThemeColor, iconDrawable, mDefaultIconSize, + mFinalIconSize, loadInDetail, mSplashscreenWorkerHandler); } } @@ -435,14 +475,14 @@ public class SplashscreenContentDrawer { () -> new DrawableColorTester(iconForeground, DrawableColorTester.TRANSLUCENT_FILTER /* filterType */), () -> new DrawableColorTester(adaptiveIconDrawable.getBackground())); - - if (DEBUG) { - Slog.d(TAG, "FgMainColor=" + Integer.toHexString(iconColor.mFgColor) - + " BgMainColor=" + Integer.toHexString(iconColor.mBgColor) - + " IsBgComplex=" + iconColor.mIsBgComplex - + " FromCache=" + (iconColor.mReuseCount > 0) - + " ThemeColor=" + Integer.toHexString(mThemeColor)); - } + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW, + "processAdaptiveIcon: FgMainColor=%s, BgMainColor=%s, " + + "IsBgComplex=%b, FromCache=%b, ThemeColor=%s", + Integer.toHexString(iconColor.mFgColor), + Integer.toHexString(iconColor.mBgColor), + iconColor.mIsBgComplex, + iconColor.mReuseCount > 0, + Integer.toHexString(mThemeColor)); // Only draw the foreground of AdaptiveIcon to the splash screen if below condition // meet: @@ -456,9 +496,8 @@ public class SplashscreenContentDrawer { && (isRgbSimilarInHsv(mThemeColor, iconColor.mBgColor) || (iconColor.mIsBgGrayscale && !isRgbSimilarInHsv(mThemeColor, iconColor.mFgColor)))) { - if (DEBUG) { - Slog.d(TAG, "makeSplashScreenContentView: choose fg icon"); - } + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW, + "processAdaptiveIcon: choose fg icon"); // Reference AdaptiveIcon description, outer is 108 and inner is 72, so we // scale by 192/160 if we only draw adaptiveIcon's foreground. final float noBgScale = @@ -467,19 +506,18 @@ public class SplashscreenContentDrawer { // Using AdaptiveIconDrawable here can help keep the shape consistent with the // current settings. mFinalIconSize = (int) (0.5f + mIconSize * noBgScale); - createIconDrawable(iconForeground, false); + createIconDrawable(iconForeground, false, mHighResIconProvider.mLoadInDetail); } else { - if (DEBUG) { - Slog.d(TAG, "makeSplashScreenContentView: draw whole icon"); - } - createIconDrawable(iconDrawable, false); + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW, + "processAdaptiveIcon: draw whole icon"); + createIconDrawable(iconDrawable, false, mHighResIconProvider.mLoadInDetail); } Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); return true; } private SplashScreenView fillViewWithIcon(int iconSize, @Nullable Drawable[] iconDrawable, - int animationDuration, Consumer<Runnable> uiThreadInitTask) { + Consumer<Runnable> uiThreadInitTask) { Drawable foreground = null; Drawable background = null; if (iconDrawable != null) { @@ -495,8 +533,8 @@ public class SplashscreenContentDrawer { .setIconSize(iconSize) .setIconBackground(background) .setCenterViewDrawable(foreground) - .setAnimationDurationMillis(animationDuration) - .setUiThreadInitConsumer(uiThreadInitTask); + .setUiThreadInitConsumer(uiThreadInitTask) + .setAllowHandleSolidColor(mAllowHandleSolidColor); if (mSuggestType == STARTING_WINDOW_TYPE_SPLASH_SCREEN && mTmpAttrs.mBrandingImage != null) { @@ -504,25 +542,6 @@ public class SplashscreenContentDrawer { mBrandingImageHeight); } final SplashScreenView splashScreenView = builder.build(); - if (DEBUG) { - Slog.d(TAG, "fillViewWithIcon surfaceWindowView " + splashScreenView); - } - if (mSuggestType != STARTING_WINDOW_TYPE_LEGACY_SPLASH_SCREEN) { - splashScreenView.addOnAttachStateChangeListener( - new View.OnAttachStateChangeListener() { - @Override - public void onViewAttachedToWindow(View v) { - SplashScreenView.applySystemBarsContrastColor( - v.getWindowInsetsController(), - splashScreenView.getInitBackgroundColor()); - } - - @Override - public void onViewDetachedFromWindow(View v) { - } - }); - } - Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); return splashScreenView; } @@ -536,10 +555,9 @@ public class SplashscreenContentDrawer { final float lumB = Color.luminance(b); final float contrastRatio = lumA > lumB ? (lumA + 0.05f) / (lumB + 0.05f) : (lumB + 0.05f) / (lumA + 0.05f); - if (DEBUG) { - Slog.d(TAG, "isRgbSimilarInHsv a: " + Integer.toHexString(a) - + " b " + Integer.toHexString(b) + " contrast ratio: " + contrastRatio); - } + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW, + "isRgbSimilarInHsv a:%s, b:%s, contrast ratio:%f", + Integer.toHexString(a), Integer.toHexString(b), contrastRatio); if (contrastRatio < 2) { return true; } @@ -560,14 +578,11 @@ public class SplashscreenContentDrawer { final double square = squareH + squareS + squareV; final double mean = square / 3; final double root = Math.sqrt(mean); - if (DEBUG) { - Slog.d(TAG, "hsvDiff " + minAngle - + " ah " + aHsv[0] + " bh " + bHsv[0] - + " as " + aHsv[1] + " bs " + bHsv[1] - + " av " + aHsv[2] + " bv " + bHsv[2] - + " sqH " + squareH + " sqS " + squareS + " sqV " + squareV - + " root " + root); - } + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW, + "isRgbSimilarInHsv hsvDiff: %d, ah: %f, bh: %f, as: %f, bs: %f, av: %f, bv: %f, " + + "sqH: %f, sqS: %f, sqV: %f, rsm: %f", + minAngle, aHsv[0], bHsv[0], aHsv[1], bHsv[1], aHsv[2], bHsv[2], + squareH, squareS, squareV, root); return root < 0.1; } @@ -598,9 +613,8 @@ public class SplashscreenContentDrawer { if (drawable instanceof LayerDrawable) { LayerDrawable layerDrawable = (LayerDrawable) drawable; if (layerDrawable.getNumberOfLayers() > 0) { - if (DEBUG) { - Slog.d(TAG, "replace drawable with bottom layer drawable"); - } + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW, + "DrawableColorTester: replace drawable with bottom layer drawable"); drawable = layerDrawable.getDrawable(0); } } @@ -805,9 +819,8 @@ public class SplashscreenContentDrawer { } } if (realSize == 0) { - if (DEBUG) { - Slog.d(TAG, "quantize: this is pure transparent image"); - } + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW, + "DrawableTester quantize: pure transparent image"); mInnerQuantizer.quantize(pixels, maxColors); return; } @@ -979,9 +992,100 @@ public class SplashscreenContentDrawer { * Create and play the default exit animation for splash screen view. */ void applyExitAnimation(SplashScreenView view, SurfaceControl leash, - Rect frame, Runnable finishCallback) { - final SplashScreenExitAnimation animation = new SplashScreenExitAnimation(mContext, view, - leash, frame, mMainWindowShiftLength, mTransactionPool, finishCallback); - animation.startAnimations(); + Rect frame, Runnable finishCallback, long createTime) { + final Runnable playAnimation = () -> { + final SplashScreenExitAnimation animation = new SplashScreenExitAnimation(mContext, + view, leash, frame, mMainWindowShiftLength, mTransactionPool, finishCallback); + animation.startAnimations(); + }; + if (view.getIconView() == null) { + playAnimation.run(); + return; + } + final long appReadyDuration = SystemClock.uptimeMillis() - createTime; + final long animDuration = view.getIconAnimationDuration() != null + ? view.getIconAnimationDuration().toMillis() : 0; + final long minimumShowingDuration = getShowingDuration(animDuration, appReadyDuration); + final long delayed = minimumShowingDuration - appReadyDuration; + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW, + "applyExitAnimation delayed: %s", delayed); + if (delayed > 0) { + view.postDelayed(playAnimation, delayed); + } else { + playAnimation.run(); + } + } + + /** + * When loading a BitmapDrawable object with specific density, there will decode the image based + * on the density from display metrics, so even when load with higher override density, the + * final intrinsic size of a BitmapDrawable can still not big enough to draw on expect size. + * + * So here we use a standalone IconProvider object to load the Drawable object for higher + * density, and the resources object won't affect the entire system. + * + */ + private static class HighResIconProvider { + private final Context mSharedContext; + private final IconProvider mSharedIconProvider; + private boolean mLoadInDetail; + + // only create standalone icon provider when the density dpi is low. + private Context mStandaloneContext; + private IconProvider mStandaloneIconProvider; + + HighResIconProvider(Context context, IconProvider sharedIconProvider) { + mSharedContext = context; + mSharedIconProvider = sharedIconProvider; + } + + Drawable getIcon(ActivityInfo activityInfo, int currentDpi, int iconDpi) { + mLoadInDetail = false; + Drawable drawable; + if (currentDpi < iconDpi && currentDpi < DisplayMetrics.DENSITY_XHIGH) { + drawable = loadFromStandalone(activityInfo, currentDpi, iconDpi); + } else { + drawable = mSharedIconProvider.getIcon(activityInfo, iconDpi); + } + + if (drawable == null) { + drawable = mSharedContext.getPackageManager().getDefaultActivityIcon(); + } + return drawable; + } + + private Drawable loadFromStandalone(ActivityInfo activityInfo, int currentDpi, + int iconDpi) { + if (mStandaloneContext == null) { + final Configuration defConfig = mSharedContext.getResources().getConfiguration(); + mStandaloneContext = mSharedContext.createConfigurationContext(defConfig); + mStandaloneIconProvider = new IconProvider(mStandaloneContext); + } + Resources resources; + try { + resources = mStandaloneContext.getPackageManager() + .getResourcesForApplication(activityInfo.applicationInfo); + } catch (PackageManager.NameNotFoundException | Resources.NotFoundException exc) { + resources = null; + } + if (resources != null) { + updateResourcesDpi(resources, iconDpi); + } + final Drawable drawable = mStandaloneIconProvider.getIcon(activityInfo, iconDpi); + mLoadInDetail = true; + // reset density dpi + if (resources != null) { + updateResourcesDpi(resources, currentDpi); + } + return drawable; + } + + private void updateResourcesDpi(Resources resources, int densityDpi) { + final Configuration config = resources.getConfiguration(); + final DisplayMetrics displayMetrics = resources.getDisplayMetrics(); + config.densityDpi = densityDpi; + displayMetrics.densityDpi = densityDpi; + resources.updateConfiguration(config, displayMetrics); + } } } 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 709e2219a64e..7f6bfd23f72b 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 @@ -18,9 +18,7 @@ package com.android.wm.shell.startingsurface; import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER; -import android.animation.Animator; import android.animation.AnimatorListenerAdapter; -import android.animation.ValueAnimator; import android.annotation.ColorInt; import android.annotation.NonNull; import android.annotation.Nullable; @@ -36,6 +34,8 @@ import android.graphics.PixelFormat; import android.graphics.Rect; import android.graphics.drawable.AdaptiveIconDrawable; import android.graphics.drawable.Animatable; +import android.graphics.drawable.AnimatedVectorDrawable; +import android.graphics.drawable.AnimationDrawable; import android.graphics.drawable.Drawable; import android.os.Handler; import android.os.Trace; @@ -45,6 +45,8 @@ import android.window.SplashScreenView; import com.android.internal.R; +import java.util.function.LongConsumer; + /** * Creating a lightweight Drawable object used for splash screen. * @@ -52,7 +54,7 @@ import com.android.internal.R; */ public class SplashscreenIconDrawableFactory { - private static final String TAG = "SplashscreenIconDrawableFactory"; + private static final String TAG = StartingWindowController.TAG; /** * @return An array containing the foreground drawable at index 0 and if needed a background @@ -60,7 +62,7 @@ public class SplashscreenIconDrawableFactory { */ static Drawable[] makeIconDrawable(@ColorInt int backgroundColor, @ColorInt int themeColor, @NonNull Drawable foregroundDrawable, int srcIconSize, int iconSize, - Handler splashscreenWorkerHandler) { + boolean loadInDetail, Handler splashscreenWorkerHandler) { Drawable foreground; Drawable background = null; boolean drawBackground = @@ -72,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, splashscreenWorkerHandler); + srcIconSize, iconSize, loadInDetail, splashscreenWorkerHandler); } 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, splashscreenWorkerHandler); + srcIconSize, iconSize, loadInDetail, splashscreenWorkerHandler); } if (drawBackground) { @@ -89,9 +91,9 @@ public class SplashscreenIconDrawableFactory { } static Drawable[] makeLegacyIconDrawable(@NonNull Drawable iconDrawable, int srcIconSize, - int iconSize, Handler splashscreenWorkerHandler) { + int iconSize, boolean loadInDetail, Handler splashscreenWorkerHandler) { return new Drawable[]{new ImmobileIconDrawable(iconDrawable, srcIconSize, iconSize, - splashscreenWorkerHandler)}; + loadInDetail, splashscreenWorkerHandler)}; } /** @@ -104,11 +106,16 @@ public class SplashscreenIconDrawableFactory { private final Matrix mMatrix = new Matrix(); private Bitmap mIconBitmap; - ImmobileIconDrawable(Drawable drawable, int srcIconSize, int iconSize, + ImmobileIconDrawable(Drawable drawable, int srcIconSize, int iconSize, boolean loadInDetail, Handler splashscreenWorkerHandler) { - final float scale = (float) iconSize / srcIconSize; - mMatrix.setScale(scale, scale); - splashscreenWorkerHandler.post(() -> preDrawIcon(drawable, srcIconSize)); + // This icon has lower density, don't scale it. + if (loadInDetail) { + splashscreenWorkerHandler.post(() -> preDrawIcon(drawable, iconSize)); + } else { + final float scale = (float) iconSize / srcIconSize; + mMatrix.setScale(scale, scale); + splashscreenWorkerHandler.post(() -> preDrawIcon(drawable, srcIconSize)); + } } private void preDrawIcon(Drawable drawable, int size) { @@ -266,99 +273,100 @@ public class SplashscreenIconDrawableFactory { */ public static class AnimatableIconAnimateListener extends AdaptiveForegroundDrawable implements SplashScreenView.IconAnimateListener { - private Animatable mAnimatableIcon; - private Animator mIconAnimator; + private final Animatable mAnimatableIcon; private boolean mAnimationTriggered; private AnimatorListenerAdapter mJankMonitoringListener; + private boolean mRunning; + private LongConsumer mStartListener; AnimatableIconAnimateListener(@NonNull Drawable foregroundDrawable) { super(foregroundDrawable); - mForegroundDrawable.setCallback(mCallback); - } - - @Override - public void setAnimationJankMonitoring(AnimatorListenerAdapter listener) { - mJankMonitoringListener = listener; - } - - @Override - public boolean prepareAnimate(long duration, Runnable startListener) { - mAnimatableIcon = (Animatable) mForegroundDrawable; - mIconAnimator = ValueAnimator.ofInt(0, 1); - mIconAnimator.setDuration(duration); - mIconAnimator.addListener(new Animator.AnimatorListener() { + Callback callback = new Callback() { @Override - public void onAnimationStart(Animator animation) { - if (startListener != null) { - startListener.run(); - } - try { - if (mJankMonitoringListener != null) { - mJankMonitoringListener.onAnimationStart(animation); - } - mAnimatableIcon.start(); - } catch (Exception ex) { - Log.e(TAG, "Error while running the splash screen animated icon", ex); - animation.cancel(); - } + public void invalidateDrawable(@NonNull Drawable who) { + invalidateSelf(); } @Override - public void onAnimationEnd(Animator animation) { - mAnimatableIcon.stop(); - if (mJankMonitoringListener != null) { - mJankMonitoringListener.onAnimationEnd(animation); - } + public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, + long when) { + scheduleSelf(what, when); } @Override - public void onAnimationCancel(Animator animation) { - mAnimatableIcon.stop(); - if (mJankMonitoringListener != null) { - mJankMonitoringListener.onAnimationCancel(animation); - } + public void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what) { + unscheduleSelf(what); } + }; + mForegroundDrawable.setCallback(callback); + mAnimatableIcon = (Animatable) mForegroundDrawable; + } - @Override - public void onAnimationRepeat(Animator animation) { - // do not repeat - mAnimatableIcon.stop(); - } - }); - return true; + @Override + public void setAnimationJankMonitoring(AnimatorListenerAdapter listener) { + mJankMonitoringListener = listener; } @Override - public void stopAnimation() { - if (mIconAnimator != null && mIconAnimator.isRunning()) { - mIconAnimator.end(); - mJankMonitoringListener = null; - } + public void prepareAnimate(LongConsumer startListener) { + stopAnimation(); + mStartListener = startListener; } - private final Callback mCallback = new Callback() { - @Override - public void invalidateDrawable(@NonNull Drawable who) { - invalidateSelf(); + private void startAnimation() { + if (mJankMonitoringListener != null) { + mJankMonitoringListener.onAnimationStart(null); + } + try { + mAnimatableIcon.start(); + } catch (Exception ex) { + Log.e(TAG, "Error while running the splash screen animated icon", ex); + mRunning = false; + if (mJankMonitoringListener != null) { + mJankMonitoringListener.onAnimationCancel(null); + } + if (mStartListener != null) { + mStartListener.accept(0); + } + return; } + long animDuration = 0; + if (mAnimatableIcon instanceof AnimatedVectorDrawable + && ((AnimatedVectorDrawable) mAnimatableIcon).getTotalDuration() > 0) { + animDuration = ((AnimatedVectorDrawable) mAnimatableIcon).getTotalDuration(); + } else if (mAnimatableIcon instanceof AnimationDrawable + && ((AnimationDrawable) mAnimatableIcon).getTotalDuration() > 0) { + animDuration = ((AnimationDrawable) mAnimatableIcon).getTotalDuration(); + } + mRunning = true; + if (mStartListener != null) { + mStartListener.accept(animDuration); + } + } - @Override - public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when) { - scheduleSelf(what, when); + private void onAnimationEnd() { + mAnimatableIcon.stop(); + if (mJankMonitoringListener != null) { + mJankMonitoringListener.onAnimationEnd(null); } + mStartListener = null; + mRunning = false; + } - @Override - public void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what) { - unscheduleSelf(what); + @Override + public void stopAnimation() { + if (mRunning) { + onAnimationEnd(); + mJankMonitoringListener = null; } - }; + } private void ensureAnimationStarted() { if (mAnimationTriggered) { return; } - if (mIconAnimator != null && !mIconAnimator.isRunning()) { - mIconAnimator.start(); + if (!mRunning) { + startAnimation(); } mAnimationTriggered = true; } 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 270107c01335..54d62edf2570 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 @@ -41,6 +41,7 @@ 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; @@ -49,6 +50,7 @@ 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; @@ -61,10 +63,13 @@ 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; @@ -106,9 +111,7 @@ import java.util.function.Supplier; */ @ShellSplashscreenThread public class StartingSurfaceDrawer { - static final String TAG = StartingSurfaceDrawer.class.getSimpleName(); - static final boolean DEBUG_SPLASH_SCREEN = StartingWindowController.DEBUG_SPLASH_SCREEN; - static final boolean DEBUG_TASK_SNAPSHOT = StartingWindowController.DEBUG_TASK_SNAPSHOT; + private static final String TAG = StartingWindowController.TAG; private final Context mContext; private final DisplayManager mDisplayManager; @@ -120,6 +123,28 @@ public class StartingSurfaceDrawer { 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; + /** * @param splashScreenExecutor The thread used to control add and remove starting window. */ @@ -179,11 +204,9 @@ public class StartingSurfaceDrawer { // replace with the default theme if the application didn't set final int theme = getSplashScreenTheme(windowInfo.splashScreenThemeResId, activityInfo); - if (DEBUG_SPLASH_SCREEN) { - Slog.d(TAG, "addSplashScreen " + activityInfo.packageName - + " theme=" + Integer.toHexString(theme) + " task=" + taskInfo.taskId - + " suggestType=" + suggestType); - } + 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. @@ -208,10 +231,9 @@ public class StartingSurfaceDrawer { final Configuration taskConfig = taskInfo.getConfiguration(); if (taskConfig.diffPublicOnly(context.getResources().getConfiguration()) != 0) { - if (DEBUG_SPLASH_SCREEN) { - Slog.d(TAG, "addSplashScreen: creating context based" - + " on task Configuration " + taskConfig + " for splash screen"); - } + 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( @@ -222,10 +244,9 @@ public class StartingSurfaceDrawer { // 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. - if (DEBUG_SPLASH_SCREEN) { - Slog.d(TAG, "addSplashScreen: apply overrideConfig" - + taskConfig + " to starting window resId=" + resId); - } + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW, + "addSplashScreen: apply overrideConfig %s", + taskConfig); context = overrideContext; } } catch (Resources.NotFoundException e) { @@ -273,6 +294,8 @@ public class StartingSurfaceDrawer { // 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; @@ -280,9 +303,6 @@ public class StartingSurfaceDrawer { params.token = appToken; params.packageName = activityInfo.packageName; params.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS; - // Setting as trusted overlay to let touches pass through. This is safe because this - // window is controlled by the system. - params.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY; if (!context.getResources().getCompatibilityInfo().supportsScreen()) { params.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_COMPATIBLE_WINDOW; @@ -333,7 +353,7 @@ public class StartingSurfaceDrawer { if (mSysuiProxy != null) { mSysuiProxy.requestTopUi(true, TAG); } - mSplashscreenContentDrawer.createContentView(context, suggestType, activityInfo, taskId, + mSplashscreenContentDrawer.createContentView(context, suggestType, windowInfo, viewSupplier::setView, viewSupplier::setUiThreadInitTask); try { if (addWindow(taskId, appToken, rootLayout, display, params, suggestType)) { @@ -345,10 +365,34 @@ public class StartingSurfaceDrawer { // the window before first round relayoutWindow, which will happen after insets // animation. mChoreographer.postCallback(CALLBACK_INSETS_ANIMATION, setViewSynchronized, null); - // Block until we get the background color. 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 @@ -461,10 +505,9 @@ public class StartingSurfaceDrawer { * Called when the content of a task is ready to show, starting window can be removed. */ public void removeStartingWindow(StartingWindowRemovalInfo removalInfo) { - if (DEBUG_SPLASH_SCREEN || DEBUG_TASK_SNAPSHOT) { - Slog.d(TAG, "Task start finish, remove starting surface for task " - + removalInfo.taskId); - } + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW, + "Task start finish, remove starting surface for task: %d", + removalInfo.taskId); removeWindowSynced(removalInfo, false /* immediately */); } @@ -472,9 +515,8 @@ public class StartingSurfaceDrawer { * Clear all starting windows immediately. */ public void clearAllWindows() { - if (DEBUG_SPLASH_SCREEN || DEBUG_TASK_SNAPSHOT) { - Slog.d(TAG, "Clear all starting windows immediately"); - } + 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) { @@ -502,10 +544,9 @@ public class StartingSurfaceDrawer { } else { parcelable = null; } - if (DEBUG_SPLASH_SCREEN) { - Slog.v(TAG, "Copying splash screen window view for task: " + taskId - + " parcelable: " + parcelable); - } + 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); } @@ -531,11 +572,9 @@ public class StartingSurfaceDrawer { return; } mAnimatedSplashScreenSurfaceHosts.remove(taskId); - if (DEBUG_SPLASH_SCREEN) { - String reason = fromServer ? "Server cleaned up" : "App removed"; - Slog.v(TAG, reason + "the splash screen. Releasing SurfaceControlViewHost for task:" - + 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); } @@ -593,10 +632,10 @@ public class StartingSurfaceDrawer { final StartingWindowRecord record = mStartingWindowRecords.get(taskId); if (record != null) { if (record.mDecorView != null) { - if (DEBUG_SPLASH_SCREEN) { - Slog.v(TAG, "Removing splash screen window for task: " + taskId); - } + 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); @@ -604,7 +643,8 @@ public class StartingSurfaceDrawer { if (removalInfo.playRevealAnimation) { mSplashscreenContentDrawer.applyExitAnimation(record.mContentView, removalInfo.windowAnimationLeash, removalInfo.mainFrame, - () -> removeWindowInner(record.mDecorView, true)); + () -> removeWindowInner(record.mDecorView, true), + record.mCreateTime); } else { // the SplashScreenView has been copied to client, hide the view to skip // default exit animation @@ -616,19 +656,18 @@ public class StartingSurfaceDrawer { Slog.e(TAG, "Found empty splash screen, remove!"); removeWindowInner(record.mDecorView, false); } - mStartingWindowRecords.remove(taskId); + } if (record.mTaskSnapshotWindow != null) { - if (DEBUG_TASK_SNAPSHOT) { - Slog.v(TAG, "Removing task snapshot window for " + taskId); - } + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW, + "Removing task snapshot window for %d", taskId); if (immediately) { record.mTaskSnapshotWindow.removeImmediately(); } else { - record.mTaskSnapshotWindow.scheduleRemove(() -> - mStartingWindowRecords.remove(taskId), removalInfo.deferRemoveForIme); + record.mTaskSnapshotWindow.scheduleRemove(removalInfo.deferRemoveForIme); } } + mStartingWindowRecords.remove(taskId); } } @@ -653,6 +692,9 @@ public class StartingSurfaceDrawer { 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) { @@ -663,6 +705,7 @@ public class StartingSurfaceDrawer { mBGColor = mTaskSnapshotWindow.getBackgroundColor(); } mSuggestType = suggestType; + mCreateTime = SystemClock.uptimeMillis(); } private void setSplashScreenView(SplashScreenView splashScreenView) { @@ -672,5 +715,37 @@ public class StartingSurfaceDrawer { mContentView = splashScreenView; mSetSplashScreen = true; } + + 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; + } + 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); + } } } 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 b0a66059a466..fbc992378e50 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 @@ -16,10 +16,10 @@ package com.android.wm.shell.startingsurface; import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER; -import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_EMPTY_SPLASH_SCREEN; import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_LEGACY_SPLASH_SCREEN; 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 com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission; @@ -64,10 +64,7 @@ import com.android.wm.shell.common.TransactionPool; * @hide */ public class StartingWindowController implements RemoteCallable<StartingWindowController> { - private static final String TAG = StartingWindowController.class.getSimpleName(); - - public static final boolean DEBUG_SPLASH_SCREEN = false; - public static final boolean DEBUG_TASK_SNAPSHOT = false; + public static final String TAG = "ShellStartingWindow"; private static final long TASK_BG_COLOR_RETAIN_TIME_MS = 5000; @@ -158,7 +155,7 @@ public class StartingWindowController implements RemoteCallable<StartingWindowCo private static boolean isSplashScreenType(@StartingWindowType int suggestionType) { return suggestionType == STARTING_WINDOW_TYPE_SPLASH_SCREEN - || suggestionType == STARTING_WINDOW_TYPE_EMPTY_SPLASH_SCREEN + || suggestionType == STARTING_WINDOW_TYPE_SOLID_COLOR_SPLASH_SCREEN || suggestionType == STARTING_WINDOW_TYPE_LEGACY_SPLASH_SCREEN; } 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 3e88c464d359..95bc579a4a51 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java @@ -20,8 +20,10 @@ 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; @@ -51,6 +53,7 @@ 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; @@ -62,6 +65,7 @@ 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; import android.os.Trace; @@ -76,6 +80,7 @@ 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; @@ -85,8 +90,12 @@ 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; +import com.android.wm.shell.protolog.ShellProtoLogGroup; + +import java.lang.ref.WeakReference; /** * This class represents a starting window that shows a snapshot. @@ -113,8 +122,7 @@ public class TaskSnapshotWindow { | FLAG_SCALED | FLAG_SECURE; - private static final String TAG = StartingSurfaceDrawer.TAG; - private static final boolean DEBUG = StartingSurfaceDrawer.DEBUG_TASK_SNAPSHOT; + 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; @@ -125,9 +133,6 @@ public class TaskSnapshotWindow { */ private static final long MAX_DELAY_REMOVAL_TIME_IME_VISIBLE = 600; - //tmp vars for unused relayout params - private static final Point TMP_SURFACE_SIZE = new Point(); - private final Window mWindow; private final Runnable mClearWindowHandler; private final ShellExecutor mSplashScreenExecutor; @@ -150,7 +155,7 @@ public class TaskSnapshotWindow { private final SurfaceControl.Transaction mTransaction; private final Matrix mSnapshotMatrix = new Matrix(); private final float[] mTmpFloat9 = new float[9]; - private Runnable mScheduledRunnable; + private final Runnable mScheduledRunnable = this::removeImmediately; private final boolean mHasImeSurface; static TaskSnapshotWindow create(StartingWindowInfo info, IBinder appToken, @@ -158,9 +163,8 @@ public class TaskSnapshotWindow { @NonNull Runnable clearWindowHandler) { final ActivityManager.RunningTaskInfo runningTaskInfo = info.taskInfo; final int taskId = runningTaskInfo.taskId; - if (DEBUG) { - Slog.d(TAG, "create taskSnapshot surface for task: " + taskId); - } + 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; @@ -208,6 +212,8 @@ public class TaskSnapshotWindow { final IWindowSession session = WindowManagerGlobal.getWindowSession(); final SurfaceControl surfaceControl = new SurfaceControl(); final ClientWindowFrames tmpFrames = new ClientWindowFrames(); + final WindowLayout windowLayout = new WindowLayout(); + final Rect displayCutoutSafe = new Rect(); final InsetsSourceControl[] tmpControls = new InsetsSourceControl[0]; final MergedConfiguration tmpMergedConfiguration = new MergedConfiguration(); @@ -244,9 +250,25 @@ public class TaskSnapshotWindow { window.setOuter(snapshotSurface); try { Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "TaskSnapshot#relayout"); - session.relayout(window, layoutParams, -1, -1, View.VISIBLE, 0, -1, - tmpFrames, tmpMergedConfiguration, surfaceControl, tmpInsetsState, - tmpControls, TMP_SURFACE_SIZE); + 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()); + } Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); } catch (RemoteException e) { snapshotSurface.clearWindowSynced(); @@ -308,36 +330,26 @@ public class TaskSnapshotWindow { mSystemBarBackgroundPainter.drawNavigationBarBackground(c); } - void scheduleRemove(Runnable onRemove, boolean deferRemoveForIme) { + void scheduleRemove(boolean deferRemoveForIme) { // Show the latest content as soon as possible for unlocking to home. if (mActivityType == ACTIVITY_TYPE_HOME) { removeImmediately(); - onRemove.run(); return; } - if (mScheduledRunnable != null) { - mSplashScreenExecutor.removeCallbacks(mScheduledRunnable); - mScheduledRunnable = null; - } - mScheduledRunnable = () -> { - TaskSnapshotWindow.this.removeImmediately(); - onRemove.run(); - }; + mSplashScreenExecutor.removeCallbacks(mScheduledRunnable); final long delayRemovalTime = mHasImeSurface && deferRemoveForIme ? MAX_DELAY_REMOVAL_TIME_IME_VISIBLE : DELAY_REMOVAL_TIME_GENERAL; mSplashScreenExecutor.executeDelayed(mScheduledRunnable, delayRemovalTime); - if (DEBUG) { - Slog.d(TAG, "Defer removing snapshot surface in " + delayRemovalTime); - } + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW, + "Defer removing snapshot surface in %d", delayRemovalTime); } void removeImmediately() { mSplashScreenExecutor.removeCallbacks(mScheduledRunnable); try { - if (DEBUG) { - Slog.d(TAG, "Removing taskSnapshot surface, mHasDrawn: " + mHasDrawn); - } + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW, + "Removing taskSnapshot surface, mHasDrawn=%b", mHasDrawn); mSession.remove(mWindow); } catch (RemoteException e) { // nothing @@ -363,9 +375,8 @@ public class TaskSnapshotWindow { } private void drawSnapshot() { - if (DEBUG) { - Slog.d(TAG, "Drawing snapshot surface sizeMismatch= " + mSizeMismatch); - } + 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 @@ -378,14 +389,15 @@ public class TaskSnapshotWindow { 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() { - GraphicBuffer graphicBuffer = GraphicBuffer.createFromHardwareBuffer( - mSnapshot.getHardwareBuffer()); - mTransaction.setBuffer(mSurfaceControl, graphicBuffer) + mTransaction.setBuffer(mSurfaceControl, mSnapshot.getHardwareBuffer()) .setColorSpace(mSurfaceControl, mSnapshot.getColorSpace()) .apply(); } @@ -431,20 +443,20 @@ public class TaskSnapshotWindow { // Scale the mismatch dimensions to fill the task bounds mSnapshotMatrix.setRectToRect(mTmpSnapshotSize, mTmpDstFrame, Matrix.ScaleToFit.FILL); mTransaction.setMatrix(childSurfaceControl, mSnapshotMatrix, mTmpFloat9); - GraphicBuffer graphicBuffer = GraphicBuffer.createFromHardwareBuffer( - mSnapshot.getHardwareBuffer()); mTransaction.setColorSpace(childSurfaceControl, mSnapshot.getColorSpace()); - mTransaction.setBuffer(childSurfaceControl, graphicBuffer); + 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, background); + mTransaction.setBuffer(mSurfaceControl, + HardwareBuffer.createFromGraphicBuffer(background)); } mTransaction.apply(); childSurfaceControl.release(); @@ -525,40 +537,43 @@ public class TaskSnapshotWindow { private void reportDrawn() { try { - mSession.finishDrawing(mWindow, null /* postDrawTransaction */); + mSession.finishDrawing(mWindow, null /* postDrawTransaction */, Integer.MAX_VALUE); } catch (RemoteException e) { clearWindowSynced(); } } - @BinderThread static class Window extends BaseIWindow { - private TaskSnapshotWindow mOuter; + private WeakReference<TaskSnapshotWindow> mOuter; public void setOuter(TaskSnapshotWindow outer) { - mOuter = outer; + mOuter = new WeakReference<>(outer); } + @BinderThread @Override public void resized(ClientWindowFrames frames, boolean reportDraw, - MergedConfiguration mergedConfiguration, boolean forceLayout, - boolean alwaysConsumeSystemBars, int displayId) { - if (mOuter != null) { - mOuter.mSplashScreenExecutor.execute(() -> { - if (mergedConfiguration != null - && mOuter.mOrientationOnCreation - != mergedConfiguration.getMergedConfiguration().orientation) { - // The orientation of the screen is changing. We better remove the snapshot - // ASAP as we are going to wait on the new window in any case to unfreeze - // the screen, and the starting window is not needed anymore. - mOuter.clearWindowSynced(); - } else if (reportDraw) { - if (mOuter.mHasDrawn) { - mOuter.reportDrawn(); - } - } - }); + MergedConfiguration mergedConfiguration, InsetsState insetsState, + boolean forceLayout, boolean alwaysConsumeSystemBars, int displayId, int seqId, + int resizeMode) { + final TaskSnapshotWindow snapshot = mOuter.get(); + if (snapshot == null) { + return; } + snapshot.mSplashScreenExecutor.execute(() -> { + if (mergedConfiguration != null + && snapshot.mOrientationOnCreation + != mergedConfiguration.getMergedConfiguration().orientation) { + // The orientation of the screen is changing. We better remove the snapshot + // ASAP as we are going to wait on the new window in any case to unfreeze + // the screen, and the starting window is not needed anymore. + snapshot.clearWindowSynced(); + } else if (reportDraw) { + if (snapshot.mHasDrawn) { + snapshot.reportDrawn(); + } + } + }); } } 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 bde2b5ff4d60..bb43d7c1a090 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 @@ -17,35 +17,31 @@ package com.android.wm.shell.startingsurface.phone; import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; -import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_EMPTY_SPLASH_SCREEN; import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_LEGACY_SPLASH_SCREEN; 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.TYPE_PARAMETER_ACTIVITY_CREATED; +import static android.window.StartingWindowInfo.TYPE_PARAMETER_ACTIVITY_DRAWN; import static android.window.StartingWindowInfo.TYPE_PARAMETER_ALLOW_TASK_SNAPSHOT; import static android.window.StartingWindowInfo.TYPE_PARAMETER_LEGACY_SPLASH_SCREEN; 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_EMPTY_SPLASH_SCREEN; +import static android.window.StartingWindowInfo.TYPE_PARAMETER_USE_SOLID_COLOR_SPLASH_SCREEN; -import static com.android.wm.shell.startingsurface.StartingWindowController.DEBUG_SPLASH_SCREEN; -import static com.android.wm.shell.startingsurface.StartingWindowController.DEBUG_TASK_SNAPSHOT; - -import android.util.Slog; import android.window.StartingWindowInfo; -import android.window.TaskSnapshot; +import com.android.internal.protolog.common.ProtoLog; +import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.startingsurface.StartingWindowTypeAlgorithm; /** * Algorithm for determining the type of a new starting window on handheld devices. - * At the moment also used on Android Auto. + * At the moment also used on Android Auto and Wear OS. */ public class PhoneStartingWindowTypeAlgorithm implements StartingWindowTypeAlgorithm { - private static final String TAG = PhoneStartingWindowTypeAlgorithm.class.getSimpleName(); - @Override public int getSuggestedWindowType(StartingWindowInfo windowInfo) { final int parameter = windowInfo.startingWindowTypeParameter; @@ -54,71 +50,55 @@ public class PhoneStartingWindowTypeAlgorithm implements StartingWindowTypeAlgor final boolean processRunning = (parameter & TYPE_PARAMETER_PROCESS_RUNNING) != 0; final boolean allowTaskSnapshot = (parameter & TYPE_PARAMETER_ALLOW_TASK_SNAPSHOT) != 0; final boolean activityCreated = (parameter & TYPE_PARAMETER_ACTIVITY_CREATED) != 0; - final boolean useEmptySplashScreen = - (parameter & TYPE_PARAMETER_USE_EMPTY_SPLASH_SCREEN) != 0; + final boolean isSolidColorSplashScreen = + (parameter & TYPE_PARAMETER_USE_SOLID_COLOR_SPLASH_SCREEN) != 0; final boolean legacySplashScreen = ((parameter & TYPE_PARAMETER_LEGACY_SPLASH_SCREEN) != 0); + final boolean activityDrawn = (parameter & TYPE_PARAMETER_ACTIVITY_DRAWN) != 0; final boolean topIsHome = windowInfo.taskInfo.topActivityType == ACTIVITY_TYPE_HOME; - if (DEBUG_SPLASH_SCREEN || DEBUG_TASK_SNAPSHOT) { - Slog.d(TAG, "preferredStartingWindowType newTask:" + newTask - + " taskSwitch:" + taskSwitch - + " processRunning:" + processRunning - + " allowTaskSnapshot:" + allowTaskSnapshot - + " activityCreated:" + activityCreated - + " useEmptySplashScreen:" + useEmptySplashScreen - + " legacySplashScreen:" + legacySplashScreen - + " topIsHome:" + topIsHome); - } + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW, + "preferredStartingWindowType " + + "newTask=%b, " + + "taskSwitch=%b, " + + "processRunning=%b, " + + "allowTaskSnapshot=%b, " + + "activityCreated=%b, " + + "isSolidColorSplashScreen=%b, " + + "legacySplashScreen=%b, " + + "activityDrawn=%b, " + + "topIsHome=%b", + newTask, taskSwitch, processRunning, allowTaskSnapshot, activityCreated, + isSolidColorSplashScreen, legacySplashScreen, activityDrawn, topIsHome); if (!topIsHome) { if (!processRunning || newTask || (taskSwitch && !activityCreated)) { - return useEmptySplashScreen - ? STARTING_WINDOW_TYPE_EMPTY_SPLASH_SCREEN - : legacySplashScreen - ? STARTING_WINDOW_TYPE_LEGACY_SPLASH_SCREEN - : STARTING_WINDOW_TYPE_SPLASH_SCREEN; + return getSplashscreenType(isSolidColorSplashScreen, legacySplashScreen); } } - if (taskSwitch && allowTaskSnapshot) { - if (isSnapshotCompatible(windowInfo)) { - return STARTING_WINDOW_TYPE_SNAPSHOT; + + if (taskSwitch) { + if (allowTaskSnapshot) { + if (windowInfo.taskSnapshot != null) { + return STARTING_WINDOW_TYPE_SNAPSHOT; + } + if (!topIsHome) { + return STARTING_WINDOW_TYPE_SOLID_COLOR_SPLASH_SCREEN; + } } - if (!topIsHome) { - return STARTING_WINDOW_TYPE_EMPTY_SPLASH_SCREEN; + if (!activityDrawn && !topIsHome) { + return getSplashscreenType(isSolidColorSplashScreen, legacySplashScreen); } } return STARTING_WINDOW_TYPE_NONE; } - - /** - * Returns {@code true} if the task snapshot is compatible with this activity (at least the - * rotation must be the same). - */ - private boolean isSnapshotCompatible(StartingWindowInfo windowInfo) { - final TaskSnapshot snapshot = windowInfo.taskSnapshot; - if (snapshot == null) { - if (DEBUG_SPLASH_SCREEN || DEBUG_TASK_SNAPSHOT) { - Slog.d(TAG, "isSnapshotCompatible no snapshot " + windowInfo.taskInfo.taskId); - } - return false; - } - if (!snapshot.getTopActivityComponent().equals(windowInfo.taskInfo.topActivity)) { - if (DEBUG_SPLASH_SCREEN || DEBUG_TASK_SNAPSHOT) { - Slog.d(TAG, "isSnapshotCompatible obsoleted snapshot " - + windowInfo.taskInfo.topActivity); - } - return false; - } - - final int taskRotation = windowInfo.taskInfo.configuration - .windowConfiguration.getRotation(); - final int snapshotRotation = snapshot.getRotation(); - if (DEBUG_SPLASH_SCREEN || DEBUG_TASK_SNAPSHOT) { - Slog.d(TAG, "isSnapshotCompatible rotation " + taskRotation - + " snapshot " + snapshotRotation); - } - return taskRotation == snapshotRotation; + private static int getSplashscreenType(boolean solidColorSplashScreen, + boolean legacySplashScreen) { + return solidColorSplashScreen + ? STARTING_WINDOW_TYPE_SOLID_COLOR_SPLASH_SCREEN + : legacySplashScreen + ? STARTING_WINDOW_TYPE_LEGACY_SPLASH_SCREEN + : STARTING_WINDOW_TYPE_SPLASH_SCREEN; } } 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 6e7dec590308..74fe8fbbd5e0 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_EMPTY_SPLASH_SCREEN; +import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_SOLID_COLOR_SPLASH_SCREEN; import android.window.StartingWindowInfo; @@ -30,6 +30,6 @@ public class TvStartingWindowTypeAlgorithm implements StartingWindowTypeAlgorith @Override public int getSuggestedWindowType(StartingWindowInfo windowInfo) { // For now we want to always show empty splash screens on TV. - return STARTING_WINDOW_TYPE_EMPTY_SPLASH_SCREEN; + return STARTING_WINDOW_TYPE_SOLID_COLOR_SPLASH_SCREEN; } } 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 new file mode 100644 index 000000000000..19133e29de4b --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/CounterRotatorHelper.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.transition; + +import static android.window.TransitionInfo.FLAG_IS_WALLPAPER; + +import android.graphics.Rect; +import android.util.ArrayMap; +import android.util.RotationUtils; +import android.view.SurfaceControl; +import android.window.TransitionInfo; +import android.window.WindowContainerToken; + +import androidx.annotation.NonNull; + +import com.android.wm.shell.util.CounterRotator; + +import java.util.List; + +/** + * The helper class that performs counter-rotate for all "going-away" window containers if they are + * still in the old rotation in a transition. + */ +public class CounterRotatorHelper { + private final ArrayMap<WindowContainerToken, CounterRotator> mRotatorMap = new ArrayMap<>(); + private final Rect mLastDisplayBounds = new Rect(); + private int mLastRotationDelta; + + /** Puts the surface controls of closing changes to counter-rotated surfaces. */ + public void handleClosingChanges(@NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull TransitionInfo.Change displayRotationChange) { + final int rotationDelta = RotationUtils.deltaRotation( + displayRotationChange.getStartRotation(), displayRotationChange.getEndRotation()); + final Rect displayBounds = displayRotationChange.getEndAbsBounds(); + final int displayW = displayBounds.width(); + final int displayH = displayBounds.height(); + mLastRotationDelta = rotationDelta; + mLastDisplayBounds.set(displayBounds); + + final List<TransitionInfo.Change> changes = info.getChanges(); + final int numChanges = changes.size(); + 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()) + || !TransitionInfo.isIndependent(change, info) || parent == null) { + continue; + } + + CounterRotator crot = mRotatorMap.get(parent); + if (crot == null) { + crot = new CounterRotator(); + crot.setup(startTransaction, info.getChange(parent).getLeash(), rotationDelta, + displayW, displayH); + final SurfaceControl rotatorSc = crot.getSurface(); + if (rotatorSc != null) { + // Wallpaper should be placed at the bottom. + final int layer = (change.getFlags() & FLAG_IS_WALLPAPER) == 0 + ? numChanges - i + : -1; + startTransaction.setLayer(rotatorSc, layer); + } + mRotatorMap.put(parent, crot); + } + crot.addChild(startTransaction, change.getLeash()); + } + } + + /** + * Returns the rotated end bounds if the change is put in previous rotation. Otherwise the + * original end bounds are returned. + */ + @NonNull + public Rect getEndBoundsInStartRotation(@NonNull TransitionInfo.Change change) { + if (mLastRotationDelta == 0) return change.getEndAbsBounds(); + final Rect rotatedBounds = new Rect(change.getEndAbsBounds()); + RotationUtils.rotateBounds(rotatedBounds, mLastDisplayBounds, mLastRotationDelta); + return rotatedBounds; + } + + /** + * Removes the counter rotation surface in the finish transaction. No need to reparent the + * children as the finish transaction should have already taken care of that. + * + * This can only be called after startTransaction for {@link #handleClosingChanges} is applied. + */ + public void cleanUp(@NonNull SurfaceControl.Transaction finishTransaction) { + for (int i = mRotatorMap.size() - 1; i >= 0; --i) { + mRotatorMap.valueAt(i).cleanUp(finishTransaction); + } + mRotatorMap.clear(); + mLastRotationDelta = 0; + } +} 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 7abda994bb5e..9154226b7b22 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,11 +18,19 @@ 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; import static android.app.ActivityOptions.ANIM_THUMBNAIL_SCALE_DOWN; import static android.app.ActivityOptions.ANIM_THUMBNAIL_SCALE_UP; +import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; +import static android.app.admin.DevicePolicyManager.ACTION_DEVICE_POLICY_RESOURCE_UPDATED; +import static android.app.admin.DevicePolicyManager.EXTRA_RESOURCE_TYPE; +import static android.app.admin.DevicePolicyManager.EXTRA_RESOURCE_TYPE_DRAWABLE; +import static android.app.admin.DevicePolicyResources.Drawables.Source.PROFILE_SWITCH_ANIMATION; +import static android.app.admin.DevicePolicyResources.Drawables.Style.OUTLINE; +import static android.app.admin.DevicePolicyResources.Drawables.WORK_PROFILE_ICON; import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_JUMPCUT; import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_ROTATE; import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_SEAMLESS; @@ -41,7 +49,6 @@ import static android.window.TransitionInfo.FLAG_IS_WALLPAPER; import static android.window.TransitionInfo.FLAG_SHOW_WALLPAPER; import static android.window.TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT; import static android.window.TransitionInfo.FLAG_TRANSLUCENT; -import static android.window.TransitionInfo.isIndependent; import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_CLOSE; import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_INTRA_CLOSE; @@ -52,32 +59,47 @@ import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITI import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; +import android.annotation.ColorInt; import android.annotation.NonNull; import android.annotation.Nullable; +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; +import android.view.WindowManager.TransitionType; import android.view.animation.AlphaAnimation; import android.view.animation.Animation; import android.view.animation.Transformation; import android.window.TransitionInfo; import android.window.TransitionMetrics; 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.policy.AttributeCache; +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.DisplayController; @@ -85,9 +107,10 @@ 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.util.CounterRotator; import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; /** The default handler that handles anything not already handled. */ public class DefaultTransitionHandler implements Transitions.TransitionHandler { @@ -114,12 +137,14 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { private final ShellExecutor mMainExecutor; private final ShellExecutor mAnimExecutor; private final TransitionAnimation mTransitionAnimation; + private final DevicePolicyManager mDevicePolicyManager; private final SurfaceSession mSurfaceSession = new SurfaceSession(); /** Keeps track of the currently-running animations associated with each transition. */ private final ArrayMap<IBinder, ArrayList<Animator>> mAnimations = new ArrayMap<>(); + private final CounterRotatorHelper mRotator = new CounterRotatorHelper(); private final Rect mInsets = new Rect(0, 0, 0, 0); private float mTransitionAnimationScaleSetting = 1.0f; @@ -127,9 +152,23 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { private ScreenRotationAnimation mRotationAnimation; + private Drawable mEnterpriseThumbnailDrawable; + + private BroadcastReceiver mEnterpriseResourceUpdatedReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (intent.getIntExtra(EXTRA_RESOURCE_TYPE, /* default= */ -1) + != EXTRA_RESOURCE_TYPE_DRAWABLE) { + return; + } + updateEnterpriseThumbnailDrawable(); + } + }; + DefaultTransitionHandler(@NonNull DisplayController displayController, @NonNull TransactionPool transactionPool, Context context, - @NonNull ShellExecutor mainExecutor, @NonNull ShellExecutor animExecutor) { + @NonNull ShellExecutor mainExecutor, @NonNull Handler mainHandler, + @NonNull ShellExecutor animExecutor) { mDisplayController = displayController; mTransactionPool = transactionPool; mContext = context; @@ -138,9 +177,23 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { mTransitionAnimation = new TransitionAnimation(context, false /* debug */, Transitions.TAG); mCurrentUserId = UserHandle.myUserId(); + mDevicePolicyManager = context.getSystemService(DevicePolicyManager.class); + updateEnterpriseThumbnailDrawable(); + mContext.registerReceiver( + mEnterpriseResourceUpdatedReceiver, + new IntentFilter(ACTION_DEVICE_POLICY_RESOURCE_UPDATED), + /* broadcastPermission = */ null, + mainHandler); + AttributeCache.init(context); } + private void updateEnterpriseThumbnailDrawable() { + mEnterpriseThumbnailDrawable = mDevicePolicyManager.getResources().getDrawable( + WORK_PROFILE_ICON, OUTLINE, PROFILE_SWITCH_ANIMATION, + () -> mContext.getDrawable(R.drawable.ic_corp_badge)); + } + @VisibleForTesting static boolean isRotationSeamless(@NonNull TransitionInfo info, DisplayController displayController) { @@ -148,6 +201,7 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { "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) { final TransitionInfo.Change change = info.getChanges().get(i); @@ -156,7 +210,6 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { // This container isn't rotating, so we can ignore it. if (change.getEndRotation() == change.getStartRotation()) continue; - if ((change.getFlags() & FLAG_IS_DISPLAY) != 0) { // In the presence of System Alert windows we can not seamlessly rotate. if ((change.getFlags() & FLAG_DISPLAY_HAS_ALERT_WINDOWS) != 0) { @@ -164,6 +217,8 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { " display has system alert windows, so not seamless."); return false; } + 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, @@ -215,8 +270,8 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { } } - // ROTATION_ANIMATION_SEAMLESS can only be requested by task. - if (hasTask) { + // 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; } @@ -273,16 +328,9 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { final ArrayList<Animator> animations = new ArrayList<>(); mAnimations.put(transition, animations); - final ArrayMap<WindowContainerToken, CounterRotator> counterRotators = new ArrayMap<>(); - final Runnable onAnimFinish = () -> { if (!animations.isEmpty()) return; - for (int i = 0; i < counterRotators.size(); ++i) { - counterRotators.valueAt(i).cleanUp(info.getRootLeash()); - } - counterRotators.clear(); - if (mRotationAnimation != null) { mRotationAnimation.kill(); mRotationAnimation = null; @@ -292,57 +340,65 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { finishCallback.onTransitionFinished(null /* wct */, null /* wctCB */); }; + final List<Consumer<SurfaceControl.Transaction>> postStartTransactionCallbacks = + new ArrayList<>(); + + @ColorInt int backgroundColorForTransition = 0; final int wallpaperTransit = getWallpaperTransitType(info); for (int i = info.getChanges().size() - 1; i >= 0; --i) { final TransitionInfo.Change change = info.getChanges().get(i); + final boolean isTask = change.getTaskInfo() != null; + boolean isSeamlessDisplayChange = false; if (change.getMode() == TRANSIT_CHANGE && (change.getFlags() & FLAG_IS_DISPLAY) != 0) { - int rotateDelta = change.getEndRotation() - change.getStartRotation(); - int displayW = change.getEndAbsBounds().width(); - int displayH = change.getEndAbsBounds().height(); if (info.getType() == TRANSIT_CHANGE) { - boolean isSeamless = isRotationSeamless(info, mDisplayController); + isSeamlessDisplayChange = isRotationSeamless(info, mDisplayController); final int anim = getRotationAnimation(info); - if (!(isSeamless || anim == ROTATION_ANIMATION_JUMPCUT)) { + if (!(isSeamlessDisplayChange || anim == ROTATION_ANIMATION_JUMPCUT)) { mRotationAnimation = new ScreenRotationAnimation(mContext, mSurfaceSession, - mTransactionPool, startTransaction, change, info.getRootLeash()); + mTransactionPool, startTransaction, change, info.getRootLeash(), + anim); mRotationAnimation.startAnimation(animations, onAnimFinish, mTransitionAnimationScaleSetting, mMainExecutor, mAnimExecutor); continue; } } else { - // opening/closing an app into a new orientation. Counter-rotate all - // "going-away" things since they are still in the old orientation. - for (int j = info.getChanges().size() - 1; j >= 0; --j) { - final TransitionInfo.Change innerChange = info.getChanges().get(j); - if (!Transitions.isClosingType(innerChange.getMode()) - || !isIndependent(innerChange, info) - || innerChange.getParent() == null) { - continue; - } - CounterRotator crot = counterRotators.get(innerChange.getParent()); - if (crot == null) { - crot = new CounterRotator(); - crot.setup(startTransaction, - info.getChange(innerChange.getParent()).getLeash(), - rotateDelta, displayW, displayH); - if (crot.getSurface() != null) { - int layer = info.getChanges().size() - j; - startTransaction.setLayer(crot.getSurface(), layer); - } - counterRotators.put(innerChange.getParent(), crot); - } - crot.addChild(startTransaction, innerChange.getLeash()); - } + // Opening/closing an app into a new orientation. + mRotator.handleClosingChanges(info, startTransaction, change); } } if (change.getMode() == TRANSIT_CHANGE) { + // If task is child task, only set position in parent and update crop when needed. + if (isTask && change.getParent() != null + && info.getChange(change.getParent()).getTaskInfo() != null) { + final Point positionInParent = change.getTaskInfo().positionInParent; + startTransaction.setPosition(change.getLeash(), + positionInParent.x, positionInParent.y); + + if (!change.getEndAbsBounds().equals( + info.getChange(change.getParent()).getEndAbsBounds())) { + startTransaction.setWindowCrop(change.getLeash(), + change.getEndAbsBounds().width(), + change.getEndAbsBounds().height()); + } + + continue; + } + + // There is no default animation for Pip window in rotation transition, and the + // PipTransition will update the surface of its own window at start/finish. + if (isTask && change.getTaskInfo().configuration.windowConfiguration + .getWindowingMode() == WINDOWING_MODE_PINNED) { + continue; + } // No default animation for this, so just update bounds/position. startTransaction.setPosition(change.getLeash(), - change.getEndAbsBounds().left - change.getEndRelOffset().x, - change.getEndAbsBounds().top - change.getEndRelOffset().y); - if (change.getTaskInfo() != null) { + change.getEndAbsBounds().left - info.getRootOffset().x, + change.getEndAbsBounds().top - info.getRootOffset().y); + // Seamless display transition doesn't need to animate. + if (isSeamlessDisplayChange) continue; + if (isTask) { // Skip non-tasks since those usually have null bounds. startTransaction.setWindowCrop(change.getLeash(), change.getEndAbsBounds().width(), change.getEndAbsBounds().height()); @@ -354,21 +410,250 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { Animation a = loadAnimation(info, change, wallpaperTransit); if (a != null) { - startSurfaceAnimation(animations, a, change.getLeash(), onAnimFinish, - mTransactionPool, mMainExecutor, mAnimExecutor, null /* position */); + if (isTask) { + final @TransitionType int type = info.getType(); + final boolean isOpenOrCloseTransition = type == TRANSIT_OPEN + || type == TRANSIT_CLOSE + || type == TRANSIT_TO_FRONT + || type == TRANSIT_TO_BACK; + final boolean isTranslucent = (change.getFlags() & FLAG_TRANSLUCENT) != 0; + if (isOpenOrCloseTransition && !isTranslucent + && wallpaperTransit == WALLPAPER_TRANSITION_NONE) { + // Use the overview background as the background for the animation + final Context uiContext = ActivityThread.currentActivityThread() + .getSystemUiContext(); + backgroundColorForTransition = + uiContext.getColor(R.color.overview_background); + } + } + + final float cornerRadius; + if (a.hasRoundedCorners() && isTask) { + // hasRoundedCorners is currently only enabled for tasks + final Context displayContext = + mDisplayController.getDisplayContext(change.getTaskInfo().displayId); + cornerRadius = displayContext == null ? 0 + : ScreenDecorationsUtils.getWindowCornerRadius(displayContext); + } else { + 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(); + } + } + + boolean delayedEdgeExtension = false; + if (!isTask && a.hasExtension()) { + if (!Transitions.isOpeningType(change.getMode())) { + // Can screenshot now (before startTransaction is applied) + edgeExtendWindow(change, a, startTransaction, finishTransaction); + } else { + // Need to screenshot after startTransaction is applied otherwise activity + // may not be visible or ready yet. + postStartTransactionCallbacks + .add(t -> edgeExtendWindow(change, a, t, finishTransaction)); + 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); + } if (info.getAnimationOptions() != null) { - attachThumbnail(animations, onAnimFinish, change, info.getAnimationOptions()); + attachThumbnail(animations, onAnimFinish, change, info.getAnimationOptions(), + cornerRadius); } } } - startTransaction.apply(); + + if (backgroundColorForTransition != 0) { + addBackgroundToTransition(info.getRootLeash(), 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); + } + + mRotator.cleanUp(finishTransaction); TransitionMetrics.getInstance().reportAnimationStart(transition); // run finish now in-case there are no animations onAnimFinish.run(); 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); + } + } + + 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; + } + + 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 @Override public WindowContainerTransaction handleRequest(@NonNull IBinder transition, @@ -396,6 +681,9 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { 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) + ? mRotator.getEndBoundsInStartRotation(change) + : change.getEndAbsBounds(); if (info.isKeyguardGoingAway()) { a = mTransitionAnimation.loadKeyguardExitAnimation(flags, @@ -413,8 +701,7 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { a = new AlphaAnimation(1.f, 1.f); a.setDuration(TransitionAnimation.DEFAULT_APP_TRANSITION_DURATION); } else if (type == TRANSIT_RELAUNCH) { - a = mTransitionAnimation.createRelaunchAnimation( - change.getEndAbsBounds(), mInsets, change.getEndAbsBounds()); + a = mTransitionAnimation.createRelaunchAnimation(endBounds, mInsets, endBounds); } else if (overrideType == ANIM_CUSTOM && (canCustomContainer || options.getOverrideTaskTransition())) { a = mTransitionAnimation.loadAnimationRes(options.getPackageName(), enter @@ -423,80 +710,93 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { a = mTransitionAnimation.loadCrossProfileAppEnterAnimation(); } else if (overrideType == ANIM_CLIP_REVEAL) { a = mTransitionAnimation.createClipRevealAnimationLocked(type, wallpaperTransit, enter, - change.getEndAbsBounds(), change.getEndAbsBounds(), - options.getTransitionBounds()); + endBounds, endBounds, options.getTransitionBounds()); } else if (overrideType == ANIM_SCALE_UP) { a = mTransitionAnimation.createScaleUpAnimationLocked(type, wallpaperTransit, enter, - change.getEndAbsBounds(), options.getTransitionBounds()); + endBounds, options.getTransitionBounds()); } else if (overrideType == ANIM_THUMBNAIL_SCALE_UP || overrideType == ANIM_THUMBNAIL_SCALE_DOWN) { final boolean scaleUp = overrideType == ANIM_THUMBNAIL_SCALE_UP; a = mTransitionAnimation.createThumbnailEnterExitAnimationLocked(enter, scaleUp, - change.getEndAbsBounds(), type, wallpaperTransit, options.getThumbnail(), + endBounds, type, wallpaperTransit, options.getThumbnail(), options.getTransitionBounds()); } else if ((changeFlags & FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT) != 0 && isOpeningType) { // This received a transferred starting window, so don't animate return null; - } else if (wallpaperTransit == WALLPAPER_TRANSITION_INTRA_OPEN) { - a = mTransitionAnimation.loadDefaultAnimationAttr(enter - ? R.styleable.WindowAnimation_wallpaperIntraOpenEnterAnimation - : R.styleable.WindowAnimation_wallpaperIntraOpenExitAnimation); - } else if (wallpaperTransit == WALLPAPER_TRANSITION_INTRA_CLOSE) { - a = mTransitionAnimation.loadDefaultAnimationAttr(enter - ? R.styleable.WindowAnimation_wallpaperIntraCloseEnterAnimation - : R.styleable.WindowAnimation_wallpaperIntraCloseExitAnimation); - } else if (wallpaperTransit == WALLPAPER_TRANSITION_OPEN) { - a = mTransitionAnimation.loadDefaultAnimationAttr(enter - ? R.styleable.WindowAnimation_wallpaperOpenEnterAnimation - : R.styleable.WindowAnimation_wallpaperOpenExitAnimation); - } else if (wallpaperTransit == WALLPAPER_TRANSITION_CLOSE) { - a = mTransitionAnimation.loadDefaultAnimationAttr(enter - ? R.styleable.WindowAnimation_wallpaperCloseEnterAnimation - : R.styleable.WindowAnimation_wallpaperCloseExitAnimation); - } else if (type == TRANSIT_OPEN) { - if (isTask) { - a = mTransitionAnimation.loadDefaultAnimationAttr(enter - ? R.styleable.WindowAnimation_taskOpenEnterAnimation - : R.styleable.WindowAnimation_taskOpenExitAnimation); - } else { + } 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) { - a = mTransitionAnimation.loadDefaultAnimationRes( - R.anim.activity_translucent_open_enter); + translucent = true; + } + if (isTask && !translucent) { + animAttr = enter + ? R.styleable.WindowAnimation_taskOpenEnterAnimation + : R.styleable.WindowAnimation_taskOpenExitAnimation; } else { - a = mTransitionAnimation.loadDefaultAnimationAttr(enter + animAttr = enter ? R.styleable.WindowAnimation_activityOpenEnterAnimation - : R.styleable.WindowAnimation_activityOpenExitAnimation); + : R.styleable.WindowAnimation_activityOpenExitAnimation; } - } - } else if (type == TRANSIT_TO_FRONT) { - a = mTransitionAnimation.loadDefaultAnimationAttr(enter - ? R.styleable.WindowAnimation_taskToFrontEnterAnimation - : R.styleable.WindowAnimation_taskToFrontExitAnimation); - } else if (type == TRANSIT_CLOSE) { - if (isTask) { - a = mTransitionAnimation.loadDefaultAnimationAttr(enter - ? R.styleable.WindowAnimation_taskCloseEnterAnimation - : R.styleable.WindowAnimation_taskCloseExitAnimation); - } else { - if ((changeFlags & FLAG_TRANSLUCENT) != 0 && !enter) { - a = mTransitionAnimation.loadDefaultAnimationRes( - R.anim.activity_translucent_close_exit); + } else 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 { - a = mTransitionAnimation.loadDefaultAnimationAttr(enter + if ((changeFlags & FLAG_TRANSLUCENT) != 0 && !enter) { + translucent = true; + } + animAttr = enter ? R.styleable.WindowAnimation_activityCloseEnterAnimation - : R.styleable.WindowAnimation_activityCloseExitAnimation); + : 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); } } - } else if (type == TRANSIT_TO_BACK) { - a = mTransitionAnimation.loadDefaultAnimationAttr(enter - ? R.styleable.WindowAnimation_taskToBackEnterAnimation - : R.styleable.WindowAnimation_taskToBackExitAnimation); } if (a != null) { if (!a.isInitialized()) { - Rect end = change.getEndAbsBounds(); - a.initialize(end.width(), end.height(), end.width(), end.height()); + final int width = endBounds.width(); + final int height = endBounds.height(); + a.initialize(width, height, width, height); } a.restrictDuration(MAX_ANIMATION_DURATION); a.scaleCurrentDuration(mTransitionAnimationScaleSetting); @@ -508,7 +808,7 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { @NonNull Animation anim, @NonNull SurfaceControl leash, @NonNull Runnable finishCallback, @NonNull TransactionPool pool, @NonNull ShellExecutor mainExecutor, @NonNull ShellExecutor animExecutor, - @Nullable Point position) { + @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(); @@ -520,12 +820,12 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { final long currentPlayTime = Math.min(va.getDuration(), va.getCurrentPlayTime()); applyTransformation(currentPlayTime, transaction, leash, anim, transformation, matrix, - position); + position, cornerRadius, clipRect); }); final Runnable finisher = () -> { applyTransformation(va.getDuration(), transaction, leash, anim, transformation, matrix, - position); + position, cornerRadius, clipRect); pool.release(transaction); mainExecutor.execute(() -> { @@ -550,28 +850,30 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { private void attachThumbnail(@NonNull ArrayList<Animator> animations, @NonNull Runnable finishCallback, TransitionInfo.Change change, - TransitionInfo.AnimationOptions options) { + TransitionInfo.AnimationOptions options, float cornerRadius) { final boolean isTask = change.getTaskInfo() != null; final boolean isOpen = Transitions.isOpeningType(change.getMode()); final boolean isClose = Transitions.isClosingType(change.getMode()); if (isOpen) { if (options.getType() == ANIM_OPEN_CROSS_PROFILE_APPS && isTask) { - attachCrossProfileThunmbnailAnimation(animations, finishCallback, change); + attachCrossProfileThumbnailAnimation(animations, finishCallback, change, + cornerRadius); } else if (options.getType() == ANIM_THUMBNAIL_SCALE_UP) { - attachThumbnailAnimation(animations, finishCallback, change, options); + attachThumbnailAnimation(animations, finishCallback, change, options, cornerRadius); } } else if (isClose && options.getType() == ANIM_THUMBNAIL_SCALE_DOWN) { - attachThumbnailAnimation(animations, finishCallback, change, options); + attachThumbnailAnimation(animations, finishCallback, change, options, cornerRadius); } } - private void attachCrossProfileThunmbnailAnimation(@NonNull ArrayList<Animator> animations, - @NonNull Runnable finishCallback, TransitionInfo.Change change) { - final int thumbnailDrawableRes = change.getTaskInfo().userId == mCurrentUserId - ? R.drawable.ic_account_circle : R.drawable.ic_corp_badge; + private void attachCrossProfileThumbnailAnimation(@NonNull ArrayList<Animator> animations, + @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 HardwareBuffer thumbnail = mTransitionAnimation.createCrossProfileAppsThumbnail( - thumbnailDrawableRes, bounds); + thumbnailDrawable, bounds); if (thumbnail == null) { return; } @@ -594,12 +896,13 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { a.restrictDuration(MAX_ANIMATION_DURATION); a.scaleCurrentDuration(mTransitionAnimationScaleSetting); startSurfaceAnimation(animations, a, wt.getSurface(), finisher, mTransactionPool, - mMainExecutor, mAnimExecutor, new Point(bounds.left, bounds.top)); + mMainExecutor, mAnimExecutor, new Point(bounds.left, bounds.top), + cornerRadius, change.getEndAbsBounds()); } private void attachThumbnailAnimation(@NonNull ArrayList<Animator> animations, @NonNull Runnable finishCallback, TransitionInfo.Change change, - TransitionInfo.AnimationOptions options) { + TransitionInfo.AnimationOptions options, float cornerRadius) { final SurfaceControl.Transaction transaction = mTransactionPool.acquire(); final WindowThumbnail wt = WindowThumbnail.createAndAttach(mSurfaceSession, change.getLeash(), options.getThumbnail(), transaction); @@ -618,7 +921,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 */); + mMainExecutor, mAnimExecutor, null /* position */, + cornerRadius, change.getEndAbsBounds()); } private static int getWallpaperTransitType(TransitionInfo info) { @@ -650,13 +954,27 @@ 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) { + Point position, float cornerRadius, @Nullable Rect clipRect) { anim.getTransformation(time, transformation); if (position != null) { transformation.getMatrix().postTranslate(position.x, position.y); } t.setMatrix(leash, transformation.getMatrix(), matrix); t.setAlpha(leash, transformation.getAlpha()); + + Insets extensionInsets = Insets.min(transformation.getInsets(), Insets.NONE); + if (!extensionInsets.equals(Insets.NONE) && clipRect != null && !clipRect.isEmpty()) { + // Clip out any overflowing edge extension + clipRect.inset(extensionInsets); + t.setCrop(leash, clipRect); + } + + if (anim.hasRoundedCorners() && cornerRadius > 0 && clipRect != null) { + // We can only apply rounded corner if a crop is set + t.setCrop(leash, clipRect); + t.setCornerRadius(leash, cornerRadius); + } + t.setFrameTimelineVsync(Choreographer.getInstance().getVsyncId()); t.apply(); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/LegacyTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/LegacyTransitions.java index 61e11e877b90..61e92f355dc2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/LegacyTransitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/LegacyTransitions.java @@ -107,7 +107,7 @@ public class LegacyTransitions { } @Override - public void onAnimationCancelled() throws RemoteException { + public void onAnimationCancelled(boolean isKeyguardOccluded) throws RemoteException { mCancelled = true; mApps = mWallpapers = mNonApps = null; checkApply(); 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 13c670a1ab1e..46f73fda37a1 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 @@ -19,6 +19,8 @@ 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; @@ -59,7 +61,7 @@ import java.util.Arrays; * This class handles the rotation animation when the device is rotated. * * <p> - * The screen rotation animation is composed of 4 different part: + * The screen rotation animation is composed of 3 different part: * <ul> * <li> The screenshot: <p> * A screenshot of the whole screen prior the change of orientation is taken to hide the @@ -75,10 +77,6 @@ import java.util.Arrays; * To have the animation seem more seamless, we add a color transitioning background behind the * exiting and entering layouts. We compute the brightness of the start and end * layouts and transition from the two brightness values as grayscale underneath the animation - * - * <li> The entering Blackframe: <p> - * The enter Blackframe is similar to the exit Blackframe but is only used when a custom - * rotation animation is used and matches the new content size instead of the screenshot. * </ul> */ class ScreenRotationAnimation { @@ -94,6 +92,7 @@ class ScreenRotationAnimation { private final Rect mStartBounds = new Rect(); private final Rect mEndBounds = new Rect(); + private final int mAnimHint; private final int mStartWidth; private final int mStartHeight; private final int mEndWidth; @@ -117,6 +116,7 @@ class ScreenRotationAnimation { // rotations. private Animation mRotateExitAnimation; private Animation mRotateEnterAnimation; + private Animation mRotateAlphaAnimation; /** Intensity of light/whiteness of the layout before rotation occurs. */ private float mStartLuma; @@ -124,9 +124,10 @@ class ScreenRotationAnimation { private float mEndLuma; ScreenRotationAnimation(Context context, SurfaceSession session, TransactionPool pool, - Transaction t, TransitionInfo.Change change, SurfaceControl rootLeash) { + Transaction t, TransitionInfo.Change change, SurfaceControl rootLeash, int animHint) { mContext = context; mTransactionPool = pool; + mAnimHint = animHint; mSurfaceControl = change.getLeash(); mStartWidth = change.getStartAbsBounds().width(); @@ -160,13 +161,6 @@ class ScreenRotationAnimation { return; } - mBackColorSurface = new SurfaceControl.Builder(session) - .setParent(rootLeash) - .setColorLayer() - .setCallsite("ShellRotationAnimation") - .setName("BackColorSurface") - .build(); - mScreenshotLayer = new SurfaceControl.Builder(session) .setParent(mAnimLeash) .setBLASTLayer() @@ -175,17 +169,9 @@ class ScreenRotationAnimation { .setName("RotationLayer") .build(); - HardwareBuffer hardwareBuffer = screenshotBuffer.getHardwareBuffer(); - mStartLuma = getMedianBorderLuma(hardwareBuffer, screenshotBuffer.getColorSpace()); - GraphicBuffer buffer = GraphicBuffer.createFromHardwareBuffer( screenshotBuffer.getHardwareBuffer()); - t.setLayer(mBackColorSurface, -1); - t.setColor(mBackColorSurface, new float[]{mStartLuma, mStartLuma, mStartLuma}); - t.setAlpha(mBackColorSurface, 1); - t.show(mBackColorSurface); - t.setLayer(mAnimLeash, SCREEN_FREEZE_LAYER_BASE); t.setPosition(mAnimLeash, 0, 0); t.setAlpha(mAnimLeash, 1); @@ -195,6 +181,23 @@ class ScreenRotationAnimation { t.setColorSpace(mScreenshotLayer, screenshotBuffer.getColorSpace()); t.show(mScreenshotLayer); + if (!isCustomRotate()) { + mBackColorSurface = new SurfaceControl.Builder(session) + .setParent(rootLeash) + .setColorLayer() + .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); + } + } catch (Surface.OutOfResourcesException e) { Slog.w(TAG, "Unable to allocate freeze surface", e); } @@ -203,6 +206,10 @@ class ScreenRotationAnimation { t.apply(); } + private boolean isCustomRotate() { + 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 @@ -244,33 +251,44 @@ class ScreenRotationAnimation { // color frame animation. //mEndLuma = getLumaOfSurfaceControl(mEndBounds, mSurfaceControl); - // Figure out how the screen has moved from the original rotation. - int delta = deltaRotation(mEndRotation, mStartRotation); - switch (delta) { /* Counter-Clockwise Rotations */ - case Surface.ROTATION_0: - mRotateExitAnimation = AnimationUtils.loadAnimation(mContext, - R.anim.screen_rotate_0_exit); - mRotateEnterAnimation = AnimationUtils.loadAnimation(mContext, - R.anim.rotation_animation_enter); - break; - case Surface.ROTATION_90: - mRotateExitAnimation = AnimationUtils.loadAnimation(mContext, - R.anim.screen_rotate_plus_90_exit); - mRotateEnterAnimation = AnimationUtils.loadAnimation(mContext, - R.anim.screen_rotate_plus_90_enter); - break; - case Surface.ROTATION_180: - mRotateExitAnimation = AnimationUtils.loadAnimation(mContext, - R.anim.screen_rotate_180_exit); - mRotateEnterAnimation = AnimationUtils.loadAnimation(mContext, - R.anim.screen_rotate_180_enter); - break; - case Surface.ROTATION_270: - mRotateExitAnimation = AnimationUtils.loadAnimation(mContext, - R.anim.screen_rotate_minus_90_exit); - mRotateEnterAnimation = AnimationUtils.loadAnimation(mContext, - R.anim.screen_rotate_minus_90_enter); - break; + final boolean customRotate = isCustomRotate(); + if (customRotate) { + mRotateExitAnimation = AnimationUtils.loadAnimation(mContext, + mAnimHint == ROTATION_ANIMATION_JUMPCUT ? R.anim.rotation_animation_jump_exit + : R.anim.rotation_animation_xfade_exit); + mRotateEnterAnimation = AnimationUtils.loadAnimation(mContext, + R.anim.rotation_animation_enter); + mRotateAlphaAnimation = AnimationUtils.loadAnimation(mContext, + R.anim.screen_rotate_alpha); + } else { + // Figure out how the screen has moved from the original rotation. + int delta = deltaRotation(mEndRotation, mStartRotation); + switch (delta) { /* Counter-Clockwise Rotations */ + case Surface.ROTATION_0: + mRotateExitAnimation = AnimationUtils.loadAnimation(mContext, + R.anim.screen_rotate_0_exit); + mRotateEnterAnimation = AnimationUtils.loadAnimation(mContext, + R.anim.rotation_animation_enter); + break; + case Surface.ROTATION_90: + mRotateExitAnimation = AnimationUtils.loadAnimation(mContext, + R.anim.screen_rotate_plus_90_exit); + mRotateEnterAnimation = AnimationUtils.loadAnimation(mContext, + R.anim.screen_rotate_plus_90_enter); + break; + case Surface.ROTATION_180: + mRotateExitAnimation = AnimationUtils.loadAnimation(mContext, + R.anim.screen_rotate_180_exit); + mRotateEnterAnimation = AnimationUtils.loadAnimation(mContext, + R.anim.screen_rotate_180_enter); + break; + case Surface.ROTATION_270: + mRotateExitAnimation = AnimationUtils.loadAnimation(mContext, + R.anim.screen_rotate_minus_90_exit); + mRotateEnterAnimation = AnimationUtils.loadAnimation(mContext, + R.anim.screen_rotate_minus_90_enter); + break; + } } mRotateExitAnimation.initialize(mEndWidth, mEndHeight, mStartWidth, mStartHeight); @@ -281,9 +299,20 @@ class ScreenRotationAnimation { mRotateEnterAnimation.scaleCurrentDuration(animationScale); mTransaction = mTransactionPool.acquire(); - startDisplayRotation(animations, finishCallback, mainExecutor, animExecutor); - startScreenshotRotationAnimation(animations, finishCallback, mainExecutor, animExecutor); - //startColorAnimation(mTransaction, animationScale); + 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); + } else { + startDisplayRotation(animations, finishCallback, mainExecutor, animExecutor); + startScreenshotRotationAnimation(animations, finishCallback, mainExecutor, + animExecutor); + //startColorAnimation(mTransaction, animationScale); + } return true; } @@ -292,14 +321,24 @@ class ScreenRotationAnimation { @NonNull Runnable finishCallback, @NonNull ShellExecutor mainExecutor, @NonNull ShellExecutor animExecutor) { startSurfaceAnimation(animations, mRotateEnterAnimation, mSurfaceControl, finishCallback, - mTransactionPool, mainExecutor, animExecutor, null /* position */); + mTransactionPool, mainExecutor, animExecutor, 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 */); + mTransactionPool, mainExecutor, animExecutor, 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 startColorAnimation(float animationScale, @NonNull ShellExecutor animExecutor) { @@ -349,13 +388,12 @@ class ScreenRotationAnimation { t.remove(mScreenshotLayer); } mScreenshotLayer = null; - - if (mBackColorSurface != null) { - if (mBackColorSurface.isValid()) { - t.remove(mBackColorSurface); - } - mBackColorSurface = null; + } + if (mBackColorSurface != null) { + if (mBackColorSurface.isValid()) { + t.remove(mBackColorSurface); } + mBackColorSurface = null; } t.apply(); mTransactionPool.release(t); 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 804e449decf8..435d67087f34 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 @@ -33,6 +33,7 @@ import android.annotation.Nullable; import android.content.ContentResolver; import android.content.Context; import android.database.ContentObserver; +import android.os.Handler; import android.os.IBinder; import android.os.RemoteException; import android.os.SystemProperties; @@ -72,34 +73,45 @@ public class Transitions implements RemoteCallable<Transitions> { /** Set to {@code true} to enable shell transitions. */ public static final boolean ENABLE_SHELL_TRANSITIONS = - SystemProperties.getBoolean("persist.debug.shell_transit", false); - - /** Transition type for dismissing split-screen via dragging the divider off the screen. */ - public static final int TRANSIT_SPLIT_DISMISS_SNAP = TRANSIT_FIRST_CUSTOM + 1; - - /** Transition type for launching 2 tasks simultaneously. */ - public static final int TRANSIT_SPLIT_SCREEN_PAIR_OPEN = TRANSIT_FIRST_CUSTOM + 2; + SystemProperties.getBoolean("persist.wm.debug.shell_transit", false); + public static final boolean SHELL_TRANSITIONS_ROTATION = ENABLE_SHELL_TRANSITIONS + && SystemProperties.getBoolean("persist.wm.debug.shell_transit_rotate", false); /** Transition type for exiting PIP via the Shell, via pressing the expand button. */ - public static final int TRANSIT_EXIT_PIP = TRANSIT_FIRST_CUSTOM + 3; + public static final int TRANSIT_EXIT_PIP = TRANSIT_FIRST_CUSTOM + 1; + + public static final int TRANSIT_EXIT_PIP_TO_SPLIT = TRANSIT_FIRST_CUSTOM + 2; /** Transition type for removing PIP via the Shell, either via Dismiss bubble or Close. */ - public static final int TRANSIT_REMOVE_PIP = TRANSIT_FIRST_CUSTOM + 4; + public static final int TRANSIT_REMOVE_PIP = TRANSIT_FIRST_CUSTOM + 3; + + /** Transition type for launching 2 tasks simultaneously. */ + public static final int TRANSIT_SPLIT_SCREEN_PAIR_OPEN = TRANSIT_FIRST_CUSTOM + 4; /** Transition type for entering split by opening an app into side-stage. */ public static final int TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE = TRANSIT_FIRST_CUSTOM + 5; + /** Transition type for dismissing split-screen via dragging the divider off the screen. */ + public static final int TRANSIT_SPLIT_DISMISS_SNAP = TRANSIT_FIRST_CUSTOM + 6; + + /** Transition type for dismissing split-screen. */ + public static final int TRANSIT_SPLIT_DISMISS = TRANSIT_FIRST_CUSTOM + 7; + private final WindowOrganizer mOrganizer; private final Context mContext; private final ShellExecutor mMainExecutor; private final ShellExecutor mAnimExecutor; private final TransitionPlayerImpl mPlayerImpl; private final RemoteTransitionHandler mRemoteTransitionHandler; + private final DisplayController mDisplayController; private final ShellTransitionImpl mImpl = new ShellTransitionImpl(); /** List of possible handlers. Ordered by specificity (eg. tapped back to front). */ private final ArrayList<TransitionHandler> mHandlers = 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; private static final class ActiveTransition { @@ -117,15 +129,17 @@ public class Transitions implements RemoteCallable<Transitions> { public Transitions(@NonNull WindowOrganizer organizer, @NonNull TransactionPool pool, @NonNull DisplayController displayController, @NonNull Context context, - @NonNull ShellExecutor mainExecutor, @NonNull ShellExecutor animExecutor) { + @NonNull ShellExecutor mainExecutor, @NonNull Handler mainHandler, + @NonNull ShellExecutor animExecutor) { mOrganizer = organizer; mContext = context; mMainExecutor = mainExecutor; mAnimExecutor = animExecutor; + mDisplayController = displayController; mPlayerImpl = new TransitionPlayerImpl(); // The very last handler (0 in the list) should be the default one. mHandlers.add(new DefaultTransitionHandler(displayController, pool, context, mainExecutor, - animExecutor)); + mainHandler, animExecutor)); // Next lowest priority is remote transitions. mRemoteTransitionHandler = new RemoteTransitionHandler(mainExecutor); mHandlers.add(mRemoteTransitionHandler); @@ -147,6 +161,7 @@ public class Transitions implements RemoteCallable<Transitions> { mContext = null; mMainExecutor = null; mAnimExecutor = null; + mDisplayController = null; mPlayerImpl = null; mRemoteTransitionHandler = null; } @@ -212,6 +227,21 @@ public class Transitions implements RemoteCallable<Transitions> { mRemoteTransitionHandler.removeFiltered(remoteTransition); } + /** + * Runs the given {@code runnable} when the last active transition has finished, or immediately + * if there are currently no active transitions. + * + * <p>This method should be called on the Shell main-thread, where the given {@code runnable} + * will be executed when the last active transition is finished. + */ + public void runOnIdle(Runnable runnable) { + if (mActiveTransitions.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 @@ -351,6 +381,32 @@ public class Transitions implements RemoteCallable<Transitions> { 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; + } + } + final ActiveTransition active = mActiveTransitions.get(activeIdx); active.mInfo = info; active.mStartT = t; @@ -482,6 +538,11 @@ public class Transitions implements RemoteCallable<Transitions> { 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(); + } + mRunWhenIdleQueue.clear(); return; } // Start animating the next active transition @@ -547,6 +608,17 @@ public class Transitions implements RemoteCallable<Transitions> { 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(); + } + mDisplayController.getChangeController().dispatchOnRotateDisplay(wct, + change.getDisplayId(), change.getStartRotation(), change.getEndRotation()); + } + } active.mToken = mOrganizer.startTransition( request.getType(), transitionToken, wct); mActiveTransitions.add(active); 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 new file mode 100644 index 000000000000..639603941c18 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldTransitionHandler.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.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; +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.wm.shell.common.TransactionPool; +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 java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Executor; + +public class UnfoldTransitionHandler implements TransitionHandler, UnfoldListener { + + private final ShellUnfoldProgressProvider mUnfoldProgressProvider; + private final Transitions mTransitions; + private final Executor mExecutor; + private final TransactionPool mTransactionPool; + + @Nullable + private TransitionFinishCallback mFinishCallback; + @Nullable + private IBinder mTransition; + + private final List<TransitionInfo.Change> mAnimatedFullscreenTasks = new ArrayList<>(); + + public UnfoldTransitionHandler(ShellUnfoldProgressProvider unfoldProgressProvider, + TransactionPool transactionPool, Executor executor, Transitions transitions) { + mUnfoldProgressProvider = unfoldProgressProvider; + mTransactionPool = transactionPool; + mExecutor = executor; + mTransitions = transitions; + } + + public void init() { + mTransitions.addHandler(this); + mUnfoldProgressProvider.addListener(mExecutor, this); + } + + @Override + public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, + @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); + } + }); + + mFinishCallback = finishCallback; + mTransition = null; + return true; + } + + @Override + public void onStateChangeProgress(float progress) { + mAnimatedFullscreenTasks.forEach(change -> { + final SurfaceControl.Transaction transaction = mTransactionPool.acquire(); + + // 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); + + transaction.apply(); + mTransactionPool.release(transaction); + }); + } + + @Override + public void onStateChangeFinished() { + if (mFinishCallback != null) { + mFinishCallback.onTransitionFinished(null, null); + mFinishCallback = null; + mAnimatedFullscreenTasks.clear(); + } + } + + @Nullable + @Override + public WindowContainerTransaction handleRequest(@NonNull IBinder transition, + @NonNull TransitionRequestInfo request) { + if (request.getType() == TRANSIT_CHANGE && request.getDisplayChange() != null + && request.getDisplayChange().isPhysicalDisplayChanged()) { + mTransition = transition; + return new WindowContainerTransaction(); + } + return null; + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/util/CounterRotator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/util/CounterRotator.java index b9b671635010..7e95814c06c2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/util/CounterRotator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/util/CounterRotator.java @@ -16,16 +16,17 @@ package com.android.wm.shell.util; +import android.graphics.Point; +import android.util.RotationUtils; import android.view.SurfaceControl; -import java.util.ArrayList; - /** - * Utility class that takes care of counter-rotating surfaces during a transition animation. + * Utility class that takes care of rotating unchanging child-surfaces to match the parent rotation + * during a transition animation. This gives the illusion that the child surfaces haven't rotated + * relative to the screen. */ public class CounterRotator { - SurfaceControl mSurface = null; - ArrayList<SurfaceControl> mRotateChildren = null; + private SurfaceControl mSurface = null; /** Gets the surface with the counter-rotation. */ public SurfaceControl getSurface() { @@ -36,52 +37,47 @@ public class CounterRotator { * Sets up this rotator. * * @param rotateDelta is the forward rotation change (the rotation the display is making). - * @param displayW (and H) Is the size of the rotating display. + * @param parentW (and H) Is the size of the rotating parent after the rotation. */ public void setup(SurfaceControl.Transaction t, SurfaceControl parent, int rotateDelta, - float displayW, float displayH) { + float parentW, float parentH) { if (rotateDelta == 0) return; - mRotateChildren = new ArrayList<>(); - // We want to counter-rotate, so subtract from 4 - rotateDelta = 4 - (rotateDelta + 4) % 4; mSurface = new SurfaceControl.Builder() .setName("Transition Unrotate") .setContainerLayer() .setParent(parent) .build(); - // column-major - if (rotateDelta == 1) { - t.setMatrix(mSurface, 0, 1, -1, 0); - t.setPosition(mSurface, displayW, 0); - } else if (rotateDelta == 2) { - t.setMatrix(mSurface, -1, 0, 0, -1); - t.setPosition(mSurface, displayW, displayH); - } else if (rotateDelta == 3) { - t.setMatrix(mSurface, 0, -1, 1, 0); - t.setPosition(mSurface, 0, displayH); + // Rotate forward to match the new rotation (rotateDelta is the forward rotation the parent + // already took). Child surfaces will be in the old rotation relative to the new parent + // rotation, so we need to forward-rotate the child surfaces to match. + RotationUtils.rotateSurface(t, mSurface, rotateDelta); + final Point tmpPt = new Point(0, 0); + // parentW/H are the size in the END rotation, the rotation utilities expect the starting + // size. So swap them if necessary + if ((rotateDelta % 2) != 0) { + final float w = parentW; + parentW = parentH; + parentH = w; } + RotationUtils.rotatePoint(tmpPt, rotateDelta, (int) parentW, (int) parentH); + t.setPosition(mSurface, tmpPt.x, tmpPt.y); t.show(mSurface); } /** - * Add a surface that needs to be counter-rotate. + * Adds a surface that needs to be counter-rotate. */ public void addChild(SurfaceControl.Transaction t, SurfaceControl child) { if (mSurface == null) return; t.reparent(child, mSurface); - mRotateChildren.add(child); } /** - * Clean-up. This undoes any reparenting and effectively stops the counter-rotation. + * Clean-up. Since finishTransaction should reset all change leashes, we only need to remove the + * counter rotation surface. */ - public void cleanUp(SurfaceControl rootLeash) { + public void cleanUp(SurfaceControl.Transaction finishTransaction) { if (mSurface == null) return; - SurfaceControl.Transaction t = new SurfaceControl.Transaction(); - for (int i = mRotateChildren.size() - 1; i >= 0; --i) { - t.reparent(mRotateChildren.get(i), rootLeash); - } - t.remove(mSurface); - t.apply(); + finishTransaction.remove(mSurface); } } diff --git a/libs/WindowManager/Shell/tests/OWNERS b/libs/WindowManager/Shell/tests/OWNERS index d80699de8a2d..f4efc374ecc2 100644 --- a/libs/WindowManager/Shell/tests/OWNERS +++ b/libs/WindowManager/Shell/tests/OWNERS @@ -1,3 +1,8 @@ -# Bug component: 909476 +# Bug component: 1157642 # includes OWNERS from parent directories natanieljr@google.com +pablogamito@google.com + +lbill@google.com +madym@google.com +hwwang@google.com diff --git a/libs/WindowManager/Shell/tests/flicker/AndroidTest.xml b/libs/WindowManager/Shell/tests/flicker/AndroidTest.xml index ad4ccc0288ad..574a9f4da627 100644 --- a/libs/WindowManager/Shell/tests/flicker/AndroidTest.xml +++ b/libs/WindowManager/Shell/tests/flicker/AndroidTest.xml @@ -16,10 +16,6 @@ <!-- restart launcher to activate TAPL --> <option name="run-command" value="setprop ro.test_harness 1 ; am force-stop com.google.android.apps.nexuslauncher" /> </target_preparer> - <target_preparer class="com.android.tradefed.targetprep.DeviceCleaner"> - <!-- reboot the device to teardown any crashed tests --> - <option name="cleanup-action" value="REBOOT" /> - </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"/> 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 c4be785cff19..cb478c84c2b7 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 @@ -17,11 +17,11 @@ @file:JvmName("CommonAssertions") package com.android.wm.shell.flicker -import android.graphics.Region import android.view.Surface import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.helpers.WindowUtils import com.android.server.wm.traces.common.FlickerComponentName +import com.android.server.wm.traces.common.region.Region fun FlickerTestParameter.appPairsDividerIsVisibleAtEnd() { assertLayersEnd { @@ -118,10 +118,10 @@ fun FlickerTestParameter.dockedStackSecondaryBoundsIsVisibleAtEnd( fun getPrimaryRegion(dividerRegion: Region, rotation: Int): Region { val displayBounds = WindowUtils.getDisplayBounds(rotation) return if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180) { - Region(0, 0, displayBounds.bounds.right, + Region.from(0, 0, displayBounds.bounds.right, dividerRegion.bounds.top + WindowUtils.dockedStackDividerInset) } else { - Region(0, 0, dividerRegion.bounds.left + WindowUtils.dockedStackDividerInset, + Region.from(0, 0, dividerRegion.bounds.left + WindowUtils.dockedStackDividerInset, displayBounds.bounds.bottom) } } @@ -129,10 +129,10 @@ fun getPrimaryRegion(dividerRegion: Region, rotation: Int): Region { fun getSecondaryRegion(dividerRegion: Region, rotation: Int): Region { val displayBounds = WindowUtils.getDisplayBounds(rotation) return if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180) { - Region(0, dividerRegion.bounds.bottom - WindowUtils.dockedStackDividerInset, + Region.from(0, dividerRegion.bounds.bottom - WindowUtils.dockedStackDividerInset, displayBounds.bounds.right, displayBounds.bounds.bottom) } else { - Region(dividerRegion.bounds.right - WindowUtils.dockedStackDividerInset, 0, + Region.from(dividerRegion.bounds.right - WindowUtils.dockedStackDividerInset, 0, displayBounds.bounds.right, displayBounds.bounds.bottom) } }
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/WaitUtils.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/WaitUtils.kt index b63d9fffdb61..4d87ec9e872f 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 @@ -43,4 +43,4 @@ fun <R> waitForResult( } while (SystemClock.uptimeMillis() - startTime < timeout) return (false to null) -}
\ No newline at end of file +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestCannotPairNonResizeableApps.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestCannotPairNonResizeableApps.kt index 038be9c190c2..c9cab39b7d8b 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestCannotPairNonResizeableApps.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestCannotPairNonResizeableApps.kt @@ -16,8 +16,6 @@ package com.android.wm.shell.flicker.apppairs -import android.platform.test.annotations.Presubmit -import androidx.test.filters.FlakyTest import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.FlickerParametersRunnerFactory import com.android.server.wm.flicker.FlickerTestParameter @@ -31,6 +29,7 @@ import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.setSuppo 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 @@ -52,9 +51,9 @@ class AppPairsTestCannotPairNonResizeableApps( testSpec: FlickerTestParameter ) : AppPairsTransition(testSpec) { - override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit + override val transition: FlickerBuilder.() -> Unit get() = { - super.transition(this, it) + super.transition(this) transitions { nonResizeableApp?.launchViaIntent(wmHelper) // TODO pair apps through normal UX flow @@ -76,23 +75,23 @@ class AppPairsTestCannotPairNonResizeableApps( resetMultiWindowConfig(instrumentation) } - @FlakyTest + @Ignore @Test override fun navBarLayerRotatesAndScales() = super.navBarLayerRotatesAndScales() - @FlakyTest + @Ignore @Test override fun statusBarLayerRotatesScales() = super.statusBarLayerRotatesScales() - @Presubmit + @Ignore @Test override fun navBarLayerIsVisible() = super.navBarLayerIsVisible() - @Presubmit + @Ignore @Test fun appPairsDividerIsInvisibleAtEnd() = testSpec.appPairsDividerIsInvisibleAtEnd() - @Presubmit + @Ignore @Test fun onlyResizeableAppWindowVisible() { val nonResizeableApp = nonResizeableApp diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestPairPrimaryAndSecondaryApps.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestPairPrimaryAndSecondaryApps.kt index bbc6b2dbece8..60c32c99d1ff 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestPairPrimaryAndSecondaryApps.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestPairPrimaryAndSecondaryApps.kt @@ -16,8 +16,6 @@ package com.android.wm.shell.flicker.apppairs -import android.platform.test.annotations.Presubmit -import androidx.test.filters.FlakyTest import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.FlickerParametersRunnerFactory import com.android.server.wm.flicker.FlickerTestParameter @@ -29,6 +27,7 @@ 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 @@ -46,9 +45,9 @@ import org.junit.runners.Parameterized class AppPairsTestPairPrimaryAndSecondaryApps( testSpec: FlickerTestParameter ) : AppPairsTransition(testSpec) { - override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit + override val transition: FlickerBuilder.() -> Unit get() = { - super.transition(this, it) + super.transition(this) transitions { // TODO pair apps through normal UX flow executeShellCommand( @@ -57,23 +56,23 @@ class AppPairsTestPairPrimaryAndSecondaryApps( } } - @Presubmit + @Ignore @Test override fun navBarLayerIsVisible() = super.navBarLayerIsVisible() - @FlakyTest + @Ignore @Test override fun navBarLayerRotatesAndScales() = super.navBarLayerRotatesAndScales() - @FlakyTest + @Ignore @Test override fun statusBarLayerRotatesScales() = super.statusBarLayerRotatesScales() - @Presubmit + @Ignore @Test fun appPairsDividerIsVisibleAtEnd() = testSpec.appPairsDividerIsVisibleAtEnd() - @Presubmit + @Ignore @Test fun bothAppWindowsVisible() { testSpec.assertWmEnd { @@ -82,7 +81,7 @@ class AppPairsTestPairPrimaryAndSecondaryApps( } } - @FlakyTest + @Ignore @Test fun appsEndingBounds() { testSpec.assertLayersEnd { diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestSupportPairNonResizeableApps.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestSupportPairNonResizeableApps.kt index bb784a809b7e..24869a802167 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestSupportPairNonResizeableApps.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestSupportPairNonResizeableApps.kt @@ -16,14 +16,14 @@ package com.android.wm.shell.flicker.apppairs -import android.platform.test.annotations.Presubmit -import androidx.test.filters.FlakyTest +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 @@ -31,6 +31,7 @@ import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.setSuppo 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 @@ -52,15 +53,26 @@ class AppPairsTestSupportPairNonResizeableApps( testSpec: FlickerTestParameter ) : AppPairsTransition(testSpec) { - override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit + override val transition: FlickerBuilder.() -> Unit get() = { - super.transition(this, it) + 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) } + 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()) } } @@ -76,23 +88,23 @@ class AppPairsTestSupportPairNonResizeableApps( resetMultiWindowConfig(instrumentation) } - @Presubmit + @Ignore @Test override fun navBarLayerIsVisible() = super.navBarLayerIsVisible() - @FlakyTest + @Ignore @Test override fun navBarLayerRotatesAndScales() = super.navBarLayerRotatesAndScales() - @FlakyTest + @Ignore @Test override fun statusBarLayerRotatesScales() = super.statusBarLayerRotatesScales() - @Presubmit + @Ignore @Test fun appPairsDividerIsVisibleAtEnd() = testSpec.appPairsDividerIsVisibleAtEnd() - @Presubmit + @Ignore @Test fun bothAppWindowVisible() { val nonResizeableApp = nonResizeableApp diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestUnpairPrimaryAndSecondaryApps.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestUnpairPrimaryAndSecondaryApps.kt index a1a4db112dfd..007415d19860 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestUnpairPrimaryAndSecondaryApps.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestUnpairPrimaryAndSecondaryApps.kt @@ -17,8 +17,6 @@ package com.android.wm.shell.flicker.apppairs import android.os.SystemClock -import android.platform.test.annotations.Presubmit -import androidx.test.filters.FlakyTest import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.FlickerParametersRunnerFactory import com.android.server.wm.flicker.FlickerTestParameter @@ -30,6 +28,7 @@ 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 @@ -47,9 +46,9 @@ import org.junit.runners.Parameterized class AppPairsTestUnpairPrimaryAndSecondaryApps( testSpec: FlickerTestParameter ) : AppPairsTransition(testSpec) { - override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit + override val transition: FlickerBuilder.() -> Unit get() = { - super.transition(this, it) + super.transition(this) setup { eachRun { executeShellCommand( @@ -65,19 +64,19 @@ class AppPairsTestUnpairPrimaryAndSecondaryApps( } } - @FlakyTest + @Ignore @Test override fun navBarLayerRotatesAndScales() = super.navBarLayerRotatesAndScales() - @FlakyTest + @Ignore @Test override fun statusBarLayerRotatesScales() = super.statusBarLayerRotatesScales() - @Presubmit + @Ignore @Test fun appPairsDividerIsInvisibleAtEnd() = testSpec.appPairsDividerIsInvisibleAtEnd() - @Presubmit + @Ignore @Test fun bothAppWindowsInvisible() { testSpec.assertWmEnd { @@ -86,7 +85,7 @@ class AppPairsTestUnpairPrimaryAndSecondaryApps( } } - @FlakyTest + @Ignore @Test fun appsStartingBounds() { testSpec.assertLayersStart { @@ -98,7 +97,7 @@ class AppPairsTestUnpairPrimaryAndSecondaryApps( } } - @FlakyTest + @Ignore @Test fun appsEndingBounds() { testSpec.assertLayersEnd { @@ -107,7 +106,7 @@ class AppPairsTestUnpairPrimaryAndSecondaryApps( } } - @Presubmit + @Ignore @Test override fun navBarLayerIsVisible() = super.navBarLayerIsVisible() diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTransition.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTransition.kt index 9e20bbbc1a1b..3e17948b4a84 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTransition.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTransition.kt @@ -18,21 +18,16 @@ package com.android.wm.shell.flicker.apppairs import android.app.Instrumentation import android.content.Context -import android.platform.test.annotations.Presubmit import android.system.helpers.ActivityHelper -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.isRotated 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.repetitions -import com.android.server.wm.flicker.startRotation import com.android.server.wm.flicker.statusBarLayerIsVisible import com.android.server.wm.flicker.statusBarLayerRotatesScales import com.android.server.wm.flicker.statusBarWindowIsVisible @@ -45,12 +40,12 @@ 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 isRotated = testSpec.config.startRotation.isRotated() protected val activityHelper = ActivityHelper.getInstance() protected val appPairsHelper = AppPairsHelper(instrumentation, Components.SplitScreenActivity.LABEL, @@ -82,20 +77,18 @@ abstract class AppPairsTransition(protected val testSpec: FlickerTestParameter) @FlickerBuilderProvider fun buildFlicker(): FlickerBuilder { return FlickerBuilder(instrumentation).apply { - withTestName { testSpec.name } - repeat { testSpec.config.repetitions } - transition(this, testSpec.config) + transition(this) } } - internal open val transition: FlickerBuilder.(Map<String, Any?>) -> Unit - get() = { configuration -> + internal open val transition: FlickerBuilder.() -> Unit + get() = { setup { test { device.wakeUpAndGoToHomeScreen() } eachRun { - this.setRotation(configuration.startRotation) + this.setRotation(testSpec.startRotation) primaryApp.launchViaIntent(wmHelper) secondaryApp.launchViaIntent(wmHelper) nonResizeableApp?.launchViaIntent(wmHelper) @@ -151,35 +144,35 @@ abstract class AppPairsTransition(protected val testSpec: FlickerTestParameter) append("$primaryApp $secondaryApp") } - @FlakyTest(bugId = 186510496) + @Ignore @Test open fun navBarLayerIsVisible() { testSpec.navBarLayerIsVisible() } - @Presubmit + @Ignore @Test open fun statusBarLayerIsVisible() { testSpec.statusBarLayerIsVisible() } - @Presubmit + @Ignore @Test open fun navBarWindowIsVisible() { testSpec.navBarWindowIsVisible() } - @Presubmit + @Ignore @Test open fun statusBarWindowIsVisible() { testSpec.statusBarWindowIsVisible() } - @Presubmit + @Ignore @Test open fun navBarLayerRotatesAndScales() = testSpec.navBarLayerRotatesAndScales() - @Presubmit + @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 new file mode 100644 index 000000000000..8446b37dbf06 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/OWNERS @@ -0,0 +1,2 @@ +# 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 index 56a2531a3fe1..b0c3ba20d948 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/RotateTwoLaunchedAppsInAppPairsMode.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/RotateTwoLaunchedAppsInAppPairsMode.kt @@ -16,16 +16,13 @@ package com.android.wm.shell.flicker.apppairs -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.Group1 import com.android.server.wm.flicker.dsl.FlickerBuilder -import com.android.server.wm.flicker.endRotation import com.android.server.wm.flicker.helpers.setRotation import com.android.wm.shell.flicker.appPairsDividerIsVisibleAtEnd import com.android.wm.shell.flicker.appPairsPrimaryBoundsIsVisibleAtEnd @@ -33,6 +30,7 @@ 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 @@ -50,26 +48,26 @@ import org.junit.runners.Parameterized class RotateTwoLaunchedAppsInAppPairsMode( testSpec: FlickerTestParameter ) : RotateTwoLaunchedAppsTransition(testSpec) { - override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit + override val transition: FlickerBuilder.() -> Unit get() = { - super.transition(this, it) + super.transition(this) transitions { executeShellCommand(composePairsCommand( primaryTaskId, secondaryTaskId, true /* pair */)) waitAppsShown(primaryApp, secondaryApp) - setRotation(testSpec.config.endRotation) + setRotation(testSpec.endRotation) } } - @Presubmit + @Ignore @Test override fun navBarLayerIsVisible() = super.navBarLayerIsVisible() - @Presubmit + @Ignore @Test override fun statusBarLayerIsVisible() = super.statusBarLayerIsVisible() - @Presubmit + @Ignore @Test fun bothAppWindowsVisible() { testSpec.assertWmEnd { @@ -78,22 +76,26 @@ class RotateTwoLaunchedAppsInAppPairsMode( } } - @Presubmit + @Ignore @Test fun appPairsDividerIsVisibleAtEnd() = testSpec.appPairsDividerIsVisibleAtEnd() - @Presubmit + @Ignore @Test fun appPairsPrimaryBoundsIsVisibleAtEnd() = - testSpec.appPairsPrimaryBoundsIsVisibleAtEnd(testSpec.config.endRotation, + testSpec.appPairsPrimaryBoundsIsVisibleAtEnd(testSpec.endRotation, primaryApp.component) - @FlakyTest + @Ignore @Test fun appPairsSecondaryBoundsIsVisibleAtEnd() = - testSpec.appPairsSecondaryBoundsIsVisibleAtEnd(testSpec.config.endRotation, + testSpec.appPairsSecondaryBoundsIsVisibleAtEnd(testSpec.endRotation, secondaryApp.component) + @Ignore + @Test + override fun statusBarLayerRotatesScales() = super.statusBarLayerRotatesScales() + companion object { @Parameterized.Parameters(name = "{0}") @JvmStatic diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/RotateTwoLaunchedAppsRotateAndEnterAppPairsMode.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/RotateTwoLaunchedAppsRotateAndEnterAppPairsMode.kt index 0699a4fd0512..ae56c7732a4d 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/RotateTwoLaunchedAppsRotateAndEnterAppPairsMode.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/RotateTwoLaunchedAppsRotateAndEnterAppPairsMode.kt @@ -16,16 +16,13 @@ package com.android.wm.shell.flicker.apppairs -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.Group1 import com.android.server.wm.flicker.dsl.FlickerBuilder -import com.android.server.wm.flicker.endRotation import com.android.server.wm.flicker.helpers.setRotation import com.android.wm.shell.flicker.appPairsDividerIsVisibleAtEnd import com.android.wm.shell.flicker.appPairsPrimaryBoundsIsVisibleAtEnd @@ -33,6 +30,7 @@ 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 @@ -50,38 +48,42 @@ import org.junit.runners.Parameterized class RotateTwoLaunchedAppsRotateAndEnterAppPairsMode( testSpec: FlickerTestParameter ) : RotateTwoLaunchedAppsTransition(testSpec) { - override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit + override val transition: FlickerBuilder.() -> Unit get() = { - super.transition(this, it) + super.transition(this) transitions { - this.setRotation(testSpec.config.endRotation) + this.setRotation(testSpec.endRotation) executeShellCommand( composePairsCommand(primaryTaskId, secondaryTaskId, pair = true)) waitAppsShown(primaryApp, secondaryApp) } } - @Presubmit + @Ignore @Test fun appPairsDividerIsVisibleAtEnd() = testSpec.appPairsDividerIsVisibleAtEnd() - @Presubmit + @Ignore @Test override fun navBarWindowIsVisible() = super.navBarWindowIsVisible() - @Presubmit + @Ignore @Test override fun navBarLayerIsVisible() = super.navBarLayerIsVisible() - @Presubmit + @Ignore @Test override fun statusBarWindowIsVisible() = super.statusBarWindowIsVisible() - @Presubmit + @Ignore @Test override fun statusBarLayerIsVisible() = super.statusBarLayerIsVisible() - @Presubmit + @Ignore + @Test + override fun statusBarLayerRotatesScales() = super.statusBarLayerRotatesScales() + + @Ignore @Test fun bothAppWindowsVisible() { testSpec.assertWmEnd { @@ -90,16 +92,16 @@ class RotateTwoLaunchedAppsRotateAndEnterAppPairsMode( } } - @FlakyTest(bugId = 172776659) + @Ignore @Test fun appPairsPrimaryBoundsIsVisibleAtEnd() = - testSpec.appPairsPrimaryBoundsIsVisibleAtEnd(testSpec.config.endRotation, + testSpec.appPairsPrimaryBoundsIsVisibleAtEnd(testSpec.endRotation, primaryApp.component) - @FlakyTest(bugId = 172776659) + @Ignore @Test fun appPairsSecondaryBoundsIsVisibleAtEnd() = - testSpec.appPairsSecondaryBoundsIsVisibleAtEnd(testSpec.config.endRotation, + testSpec.appPairsSecondaryBoundsIsVisibleAtEnd(testSpec.endRotation, secondaryApp.component) companion object { diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/RotateTwoLaunchedAppsTransition.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/RotateTwoLaunchedAppsTransition.kt index b95193a17265..b1f1c9e539df 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/RotateTwoLaunchedAppsTransition.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/RotateTwoLaunchedAppsTransition.kt @@ -17,7 +17,6 @@ package com.android.wm.shell.flicker.apppairs import android.view.Surface -import androidx.test.filters.FlakyTest import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.dsl.FlickerBuilder import com.android.server.wm.flicker.helpers.setRotation @@ -26,6 +25,7 @@ import com.android.wm.shell.flicker.helpers.BaseAppHelper.Companion.isShellTrans 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( @@ -34,7 +34,7 @@ abstract class RotateTwoLaunchedAppsTransition( override val nonResizeableApp: SplitScreenHelper? get() = null - override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit + override val transition: FlickerBuilder.() -> Unit get() = { setup { test { @@ -62,13 +62,13 @@ abstract class RotateTwoLaunchedAppsTransition( super.setup() } - @FlakyTest + @Ignore @Test override fun navBarLayerIsVisible() { super.navBarLayerIsVisible() } - @FlakyTest + @Ignore @Test override fun navBarLayerRotatesAndScales() { super.navBarLayerRotatesAndScales() 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 322d8b5e4dac..278ba9b0f4db 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 @@ -22,19 +22,17 @@ import android.app.NotificationManager import android.content.Context import android.os.ServiceManager import android.view.Surface -import androidx.test.filters.FlakyTest import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.By import androidx.test.uiautomator.UiObject2 import androidx.test.uiautomator.Until +import com.android.server.wm.flicker.Flicker import com.android.server.wm.flicker.FlickerBuilderProvider import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.FlickerTestParameterFactory import com.android.server.wm.flicker.dsl.FlickerBuilder import com.android.server.wm.flicker.helpers.SYSTEMUI_PACKAGE -import com.android.server.wm.flicker.repetitions import com.android.wm.shell.flicker.helpers.LaunchBubbleHelper -import org.junit.Test import org.junit.runners.Parameterized /** @@ -49,56 +47,47 @@ abstract class BaseBubbleScreen(protected val testSpec: FlickerTestParameter) { protected val notifyManager = INotificationManager.Stub.asInterface( ServiceManager.getService(Context.NOTIFICATION_SERVICE)) - protected val packageManager = context.getPackageManager() - protected val uid = packageManager.getApplicationInfo( + protected val uid = context.packageManager.getApplicationInfo( testApp.component.packageName, 0).uid - protected lateinit var addBubbleBtn: UiObject2 - protected lateinit var cancelAllBtn: UiObject2 - - protected abstract val transition: FlickerBuilder.(Map<String, Any?>) -> Unit + protected abstract val transition: FlickerBuilder.() -> Unit @JvmOverloads protected open fun buildTransition( - extraSpec: FlickerBuilder.(Map<String, Any?>) -> Unit = {} - ): FlickerBuilder.(Map<String, Any?>) -> Unit { - return { configuration -> - + extraSpec: FlickerBuilder.() -> Unit = {} + ): FlickerBuilder.() -> Unit { + return { setup { test { notifyManager.setBubblesAllowed(testApp.component.packageName, uid, NotificationManager.BUBBLE_PREFERENCE_ALL) testApp.launchViaIntent(wmHelper) - addBubbleBtn = device.wait(Until.findObject( - By.text("Add Bubble")), FIND_OBJECT_TIMEOUT) - cancelAllBtn = device.wait(Until.findObject( - By.text("Cancel All Bubble")), FIND_OBJECT_TIMEOUT) + waitAndGetAddBubbleBtn() + waitAndGetCancelAllBtn() } } teardown { - notifyManager.setBubblesAllowed(testApp.component.packageName, + test { + notifyManager.setBubblesAllowed(testApp.component.packageName, uid, NotificationManager.BUBBLE_PREFERENCE_NONE) - testApp.exit() + testApp.exit() + } } - extraSpec(this, configuration) + extraSpec(this) } } - @FlakyTest - @Test - fun testAppIsAlwaysVisible() { - testSpec.assertLayers { - this.isVisible(testApp.component) - } - } + 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 { - repeat { testSpec.config.repetitions } - transition(this, testSpec.config) + transition(this) } } @@ -108,7 +97,7 @@ abstract class BaseBubbleScreen(protected val testSpec: FlickerTestParameter) { fun getParams(): List<FlickerTestParameter> { return FlickerTestParameterFactory.getInstance() .getConfigNonRotationTests(supportedRotations = listOf(Surface.ROTATION_0), - repetitions = 5) + repetitions = 3) } const val FIND_OBJECT_TIMEOUT = 2000L diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/DismissBubbleScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/DismissBubbleScreen.kt index bfdcb363a818..b137e92881a5 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/DismissBubbleScreen.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/DismissBubbleScreen.kt @@ -18,6 +18,7 @@ package com.android.wm.shell.flicker.bubble import android.content.Context import android.graphics.Point +import android.platform.test.annotations.Presubmit import android.util.DisplayMetrics import android.view.WindowManager import androidx.test.filters.RequiresDevice @@ -28,6 +29,7 @@ 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.runners.Parameterized /** @@ -42,24 +44,33 @@ import org.junit.runners.Parameterized @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @Group4 -class DismissBubbleScreen(testSpec: FlickerTestParameter) : BaseBubbleScreen(testSpec) { +open class DismissBubbleScreen(testSpec: FlickerTestParameter) : BaseBubbleScreen(testSpec) { - val wm = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager - val displaySize = DisplayMetrics() + private val wm = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager + private val displaySize = DisplayMetrics() - override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit - get() = buildTransition() { + override val transition: FlickerBuilder.() -> Unit + get() = buildTransition { setup { eachRun { - addBubbleBtn?.run { addBubbleBtn.click() } ?: error("Add Bubble not found") + val addBubbleBtn = waitAndGetAddBubbleBtn() + addBubbleBtn?.click() ?: error("Add Bubble not found") } } transitions { - wm?.run { wm.getDefaultDisplay().getMetrics(displaySize) } ?: error("WM not found") + wm.run { wm.getDefaultDisplay().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) showBubble?.run { drag(dist, 1000) } ?: error("Show bubble not found") } } + + @Presubmit + @Test + open fun testAppIsAlwaysVisible() { + testSpec.assertLayers { + this.isVisible(testApp.component) + } + } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/ExpandBubbleScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/ExpandBubbleScreen.kt index 42eeadf3ddd9..f288b0a24d9d 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/ExpandBubbleScreen.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/ExpandBubbleScreen.kt @@ -16,6 +16,7 @@ package com.android.wm.shell.flicker.bubble +import android.platform.test.annotations.Presubmit import androidx.test.filters.RequiresDevice import androidx.test.uiautomator.By import androidx.test.uiautomator.Until @@ -24,6 +25,7 @@ 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.runners.Parameterized /** @@ -40,20 +42,28 @@ import org.junit.runners.Parameterized @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @Group4 -class ExpandBubbleScreen(testSpec: FlickerTestParameter) : BaseBubbleScreen(testSpec) { +open class ExpandBubbleScreen(testSpec: FlickerTestParameter) : BaseBubbleScreen(testSpec) { - override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit - get() = buildTransition() { + override val transition: FlickerBuilder.() -> Unit + get() = buildTransition { setup { test { - addBubbleBtn?.run { addBubbleBtn.click() } ?: error("Bubble widget 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) showBubble?.run { showBubble.click() } ?: error("Bubble notify not found") - device.pressBack() } } + + @Presubmit + @Test + open fun testAppIsAlwaysVisible() { + testSpec.assertLayers { + this.isVisible(testApp.component) + } + } } 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 new file mode 100644 index 000000000000..684e5cad0e67 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/LaunchBubbleFromLockScreen.kt @@ -0,0 +1,104 @@ +/* + * 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/LaunchBubbleScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/LaunchBubbleScreen.kt index 47e8c0c047a8..0bb4d398bff4 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/LaunchBubbleScreen.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/LaunchBubbleScreen.kt @@ -16,11 +16,13 @@ package com.android.wm.shell.flicker.bubble -import androidx.test.filters.RequiresDevice +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 org.junit.Test import org.junit.runner.RunWith import org.junit.runners.Parameterized @@ -37,12 +39,21 @@ import org.junit.runners.Parameterized @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @Group4 -class LaunchBubbleScreen(testSpec: FlickerTestParameter) : BaseBubbleScreen(testSpec) { +open class LaunchBubbleScreen(testSpec: FlickerTestParameter) : BaseBubbleScreen(testSpec) { - override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit - get() = buildTransition() { + override val transition: FlickerBuilder.() -> Unit + get() = buildTransition { transitions { - addBubbleBtn?.run { addBubbleBtn.click() } ?: error("Bubble widget not found") + val addBubbleBtn = waitAndGetAddBubbleBtn() + addBubbleBtn?.click() ?: error("Bubble widget not found") } } + + @Presubmit + @Test + open fun testAppIsAlwaysVisible() { + testSpec.assertLayers { + this.isVisible(testApp.component) + } + } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/MultiBubblesScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/MultiBubblesScreen.kt index 194e28fd6e8a..8d1e315e2d5e 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/MultiBubblesScreen.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/MultiBubblesScreen.kt @@ -17,6 +17,7 @@ package com.android.wm.shell.flicker.bubble import android.os.SystemClock +import android.platform.test.annotations.Presubmit import androidx.test.filters.RequiresDevice import androidx.test.uiautomator.By import androidx.test.uiautomator.Until @@ -24,7 +25,11 @@ 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.runners.Parameterized /** @@ -39,13 +44,19 @@ import org.junit.runners.Parameterized @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @Group4 -class MultiBubblesScreen(testSpec: FlickerTestParameter) : BaseBubbleScreen(testSpec) { +open class MultiBubblesScreen(testSpec: FlickerTestParameter) : BaseBubbleScreen(testSpec) { - override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit - get() = buildTransition() { + @Before + open fun before() { + Assume.assumeFalse(isShellTransitionsEnabled) + } + + 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( @@ -63,4 +74,12 @@ class MultiBubblesScreen(testSpec: FlickerTestParameter) : BaseBubbleScreen(test } } } + + @Presubmit + @Test + open fun testAppIsAlwaysVisible() { + testSpec.assertLayers { + this.isVisible(testApp.component) + } + } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/MultiBubblesScreenShellTransit.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/MultiBubblesScreenShellTransit.kt new file mode 100644 index 000000000000..ddebb6fed636 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/MultiBubblesScreenShellTransit.kt @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package 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 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 diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/OWNERS b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/OWNERS new file mode 100644 index 000000000000..566acc87e42d --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/OWNERS @@ -0,0 +1,2 @@ +# window manager > wm shell > Bubbles +# Bug component: 555586 diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/AppPairsHelper.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/AppPairsHelper.kt index 623055f659b9..41cd31aabf05 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/AppPairsHelper.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/AppPairsHelper.kt @@ -17,27 +17,27 @@ package com.android.wm.shell.flicker.helpers import android.app.Instrumentation -import android.graphics.Region 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): android.graphics.Region { - val primaryAppBounds = Region(0, 0, dividerBounds.bounds.right, + 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): android.graphics.Region { + fun getSecondaryBounds(dividerBounds: Region): Region { val displayBounds = WindowUtils.displayBounds - val secondaryAppBounds = Region(0, + val secondaryAppBounds = Region.from(0, dividerBounds.bounds.bottom - WindowUtils.dockedStackDividerInset, - displayBounds.right, displayBounds.bottom - WindowUtils.navigationBarHeight) + displayBounds.right, displayBounds.bottom - WindowUtils.navigationBarFrameHeight) return secondaryAppBounds } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/BaseAppHelper.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/BaseAppHelper.kt index 57bc0d580d72..3dd9e0572947 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/BaseAppHelper.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/BaseAppHelper.kt @@ -61,7 +61,7 @@ abstract class BaseAppHelper( private const val APP_CLOSE_WAIT_TIME_MS = 3_000L fun isShellTransitionsEnabled() = - SystemProperties.getBoolean("persist.debug.shell_transit", false) + SystemProperties.getBoolean("persist.wm.debug.shell_transit", false) fun executeShellCommand(instrumentation: Instrumentation, cmd: String) { try { diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/ImeAppHelper.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/ImeAppHelper.kt index 0f00edea136f..cc5b9f9eb26d 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/ImeAppHelper.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/ImeAppHelper.kt @@ -62,7 +62,7 @@ open class ImeAppHelper(instrumentation: Instrumentation) : BaseAppHelper( if (wmHelper == null) { device.waitForIdle() } else { - require(wmHelper.waitImeShown()) { "IME did not appear" } + wmHelper.waitImeShown() } } @@ -79,7 +79,7 @@ open class ImeAppHelper(instrumentation: Instrumentation) : BaseAppHelper( if (wmHelper == null) { uiDevice.waitForIdle() } else { - require(wmHelper.waitImeGone()) { "IME did did not close" } + wmHelper.waitImeGone() } } else { // While pressing the back button should close the IME on TV as well, it may also lead diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/PipAppHelper.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/PipAppHelper.kt index 2357b0debb33..e9d438a569d5 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/PipAppHelper.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/PipAppHelper.kt @@ -17,7 +17,6 @@ package com.android.wm.shell.flicker.helpers import android.app.Instrumentation -import android.graphics.Rect import android.media.session.MediaController import android.media.session.MediaSessionManager import android.os.SystemClock @@ -26,6 +25,7 @@ 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 @@ -58,17 +58,27 @@ class PipAppHelper(instrumentation: Instrumentation) : BaseAppHelper( } } - /** {@inheritDoc} */ - override fun launchViaIntent( + /** + * 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?, + expectedWindowName: String = "", + action: String? = null, stringExtras: Map<String, String> ) { - super.launchViaIntent(wmHelper, expectedWindowName, action, stringExtras) - wmHelper.waitFor("hasPipWindow") { it.wmState.hasPipWindow() } + 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" @@ -88,7 +98,7 @@ class PipAppHelper(instrumentation: Instrumentation) : BaseAppHelper( clickObject(ENTER_PIP_BUTTON_ID) // Wait on WMHelper or simply wait for 3 seconds - wmHelper?.waitFor("hasPipWindow") { it.wmState.hasPipWindow() } ?: SystemClock.sleep(3_000) + 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 @@ -148,7 +158,7 @@ class PipAppHelper(instrumentation: Instrumentation) : BaseAppHelper( } // Wait for animation to complete. - wmHelper.waitFor("!hasPipWindow") { !it.wmState.hasPipWindow() } + wmHelper.waitPipGone() wmHelper.waitForHomeActivityVisible() } @@ -165,7 +175,7 @@ class PipAppHelper(instrumentation: Instrumentation) : BaseAppHelper( ?: error("PIP window expand button not found") val expandButtonBounds = expandPipObject.visibleBounds uiDevice.click(expandButtonBounds.centerX(), expandButtonBounds.centerY()) - wmHelper.waitFor("!hasPipWindow") { !it.wmState.hasPipWindow() } + wmHelper.waitPipGone() wmHelper.waitForAppTransitionIdle() } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenDockActivity.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenDockActivity.kt index bd44d082a1aa..c86a1229d8d8 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenDockActivity.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenDockActivity.kt @@ -28,7 +28,6 @@ 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.startRotation import com.android.server.wm.flicker.statusBarWindowIsVisible import com.android.server.wm.traces.common.FlickerComponentName import com.android.wm.shell.flicker.dockedStackDividerBecomesVisible @@ -53,9 +52,9 @@ import org.junit.runners.Parameterized class EnterSplitScreenDockActivity( testSpec: FlickerTestParameter ) : LegacySplitScreenTransition(testSpec) { - override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit - get() = { configuration -> - super.transition(this, configuration) + override val transition: FlickerBuilder.() -> Unit + get() = { + super.transition(this) transitions { device.launchSplitScreen(wmHelper) } @@ -69,7 +68,7 @@ class EnterSplitScreenDockActivity( @Presubmit @Test fun dockedStackPrimaryBoundsIsVisibleAtEnd() = - testSpec.dockedStackPrimaryBoundsIsVisibleAtEnd(testSpec.config.startRotation, + testSpec.dockedStackPrimaryBoundsIsVisibleAtEnd(testSpec.startRotation, splitScreenApp.component) @Presubmit diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenFromDetachedRecentTask.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenFromDetachedRecentTask.kt index 625d48b8ab5a..2f9244be9c18 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenFromDetachedRecentTask.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenFromDetachedRecentTask.kt @@ -48,9 +48,9 @@ class EnterSplitScreenFromDetachedRecentTask( testSpec: FlickerTestParameter ) : LegacySplitScreenTransition(testSpec) { - override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit - get() = { configuration -> - cleanSetup(this, configuration) + override val transition: FlickerBuilder.() -> Unit + get() = { + cleanSetup(this) setup { eachRun { splitScreenApp.launchViaIntent(wmHelper) diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenLaunchToSide.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenLaunchToSide.kt index 2ed2806af528..1740c3ec24ca 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenLaunchToSide.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenLaunchToSide.kt @@ -27,7 +27,6 @@ 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.startRotation import com.android.server.wm.flicker.statusBarWindowIsVisible import com.android.server.wm.traces.common.FlickerComponentName import com.android.wm.shell.flicker.dockedStackDividerBecomesVisible @@ -52,9 +51,9 @@ import org.junit.runners.Parameterized class EnterSplitScreenLaunchToSide( testSpec: FlickerTestParameter ) : LegacySplitScreenTransition(testSpec) { - override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit - get() = { configuration -> - super.transition(this, configuration) + override val transition: FlickerBuilder.() -> Unit + get() = { + super.transition(this) transitions { device.launchSplitScreen(wmHelper) device.reopenAppFromOverview(wmHelper) @@ -69,13 +68,13 @@ class EnterSplitScreenLaunchToSide( @Presubmit @Test fun dockedStackPrimaryBoundsIsVisibleAtEnd() = - testSpec.dockedStackPrimaryBoundsIsVisibleAtEnd(testSpec.config.startRotation, + testSpec.dockedStackPrimaryBoundsIsVisibleAtEnd(testSpec.startRotation, splitScreenApp.component) @Presubmit @Test fun dockedStackSecondaryBoundsIsVisibleAtEnd() = - testSpec.dockedStackSecondaryBoundsIsVisibleAtEnd(testSpec.config.startRotation, + testSpec.dockedStackSecondaryBoundsIsVisibleAtEnd(testSpec.startRotation, secondaryApp.component) @Presubmit diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenNotSupportNonResizable.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenNotSupportNonResizable.kt index ee6cf341c9ff..4c063b918e96 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenNotSupportNonResizable.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenNotSupportNonResizable.kt @@ -55,9 +55,9 @@ class EnterSplitScreenNotSupportNonResizable( testSpec: FlickerTestParameter ) : LegacySplitScreenTransition(testSpec) { - override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit - get() = { configuration -> - cleanSetup(this, configuration) + override val transition: FlickerBuilder.() -> Unit + get() = { + cleanSetup(this) setup { eachRun { nonResizeableApp.launchViaIntent(wmHelper) diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenSupportNonResizable.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenSupportNonResizable.kt index 163b6ffda6e2..f75dee619564 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenSupportNonResizable.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenSupportNonResizable.kt @@ -54,9 +54,9 @@ class EnterSplitScreenSupportNonResizable( testSpec: FlickerTestParameter ) : LegacySplitScreenTransition(testSpec) { - override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit - get() = { configuration -> - cleanSetup(this, configuration) + override val transition: FlickerBuilder.() -> Unit + get() = { + cleanSetup(this) setup { eachRun { nonResizeableApp.launchViaIntent(wmHelper) diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ExitLegacySplitScreenFromBottom.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ExitLegacySplitScreenFromBottom.kt index 2b629b0a7eb5..ef7d65e8a732 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ExitLegacySplitScreenFromBottom.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ExitLegacySplitScreenFromBottom.kt @@ -16,7 +16,6 @@ package com.android.wm.shell.flicker.legacysplitscreen -import android.platform.test.annotations.Postsubmit import android.view.Surface import androidx.test.filters.FlakyTest import androidx.test.filters.RequiresDevice @@ -50,9 +49,9 @@ import org.junit.runners.Parameterized class ExitLegacySplitScreenFromBottom( testSpec: FlickerTestParameter ) : LegacySplitScreenTransition(testSpec) { - override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit - get() = { configuration -> - super.transition(this, configuration) + override val transition: FlickerBuilder.() -> Unit + get() = { + super.transition(this) setup { eachRun { splitScreenApp.launchViaIntent(wmHelper) @@ -74,7 +73,7 @@ class ExitLegacySplitScreenFromBottom( splitScreenApp.component, secondaryApp.component, FlickerComponentName.SNAPSHOT) - @Postsubmit + @FlakyTest @Test fun layerBecomesInvisible() { testSpec.assertLayers { @@ -94,11 +93,11 @@ class ExitLegacySplitScreenFromBottom( } } - @Postsubmit + @FlakyTest @Test fun navBarWindowIsVisible() = testSpec.navBarWindowIsVisible() - @Postsubmit + @FlakyTest @Test fun statusBarWindowIsVisible() = testSpec.statusBarWindowIsVisible() diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ExitPrimarySplitScreenShowSecondaryFullscreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ExitPrimarySplitScreenShowSecondaryFullscreen.kt index 95fe3bef4852..d913a6d85d3d 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ExitPrimarySplitScreenShowSecondaryFullscreen.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ExitPrimarySplitScreenShowSecondaryFullscreen.kt @@ -51,9 +51,9 @@ import org.junit.runners.Parameterized class ExitPrimarySplitScreenShowSecondaryFullscreen( testSpec: FlickerTestParameter ) : LegacySplitScreenTransition(testSpec) { - override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit - get() = { configuration -> - super.transition(this, configuration) + override val transition: FlickerBuilder.() -> Unit + get() = { + super.transition(this) teardown { eachRun { secondaryApp.exit(wmHelper) diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenFromIntentNotSupportNonResizable.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenFromIntentNotSupportNonResizable.kt index f7d628d48769..f3ff7b156aaf 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenFromIntentNotSupportNonResizable.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenFromIntentNotSupportNonResizable.kt @@ -53,9 +53,9 @@ class LegacySplitScreenFromIntentNotSupportNonResizable( testSpec: FlickerTestParameter ) : LegacySplitScreenTransition(testSpec) { - override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit - get() = { configuration -> - cleanSetup(this, configuration) + override val transition: FlickerBuilder.() -> Unit + get() = { + cleanSetup(this) setup { eachRun { splitScreenApp.launchViaIntent(wmHelper) diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenFromIntentSupportNonResizable.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenFromIntentSupportNonResizable.kt index a5c6571f68de..42e707ab0850 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenFromIntentSupportNonResizable.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenFromIntentSupportNonResizable.kt @@ -53,9 +53,9 @@ class LegacySplitScreenFromIntentSupportNonResizable( testSpec: FlickerTestParameter ) : LegacySplitScreenTransition(testSpec) { - override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit - get() = { configuration -> - cleanSetup(this, configuration) + override val transition: FlickerBuilder.() -> Unit + get() = { + cleanSetup(this) setup { eachRun { splitScreenApp.launchViaIntent(wmHelper) diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenFromRecentNotSupportNonResizable.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenFromRecentNotSupportNonResizable.kt index 6f486b0ddfea..c1fba7d1530c 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenFromRecentNotSupportNonResizable.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenFromRecentNotSupportNonResizable.kt @@ -16,9 +16,9 @@ package com.android.wm.shell.flicker.legacysplitscreen -import android.platform.test.annotations.Postsubmit import android.platform.test.annotations.Presubmit import android.view.Surface +import androidx.test.filters.FlakyTest import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.FlickerParametersRunnerFactory import com.android.server.wm.flicker.FlickerTestParameter @@ -55,9 +55,9 @@ class LegacySplitScreenFromRecentNotSupportNonResizable( testSpec: FlickerTestParameter ) : LegacySplitScreenTransition(testSpec) { - override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit - get() = { configuration -> - cleanSetup(this, configuration) + override val transition: FlickerBuilder.() -> Unit + get() = { + cleanSetup(this) setup { eachRun { nonResizeableApp.launchViaIntent(wmHelper) @@ -124,7 +124,7 @@ class LegacySplitScreenFromRecentNotSupportNonResizable( } } - @Postsubmit + @FlakyTest @Test fun nonResizableAppWindowBecomesVisible() { testSpec.assertWm { diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenFromRecentSupportNonResizable.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenFromRecentSupportNonResizable.kt index f03c927b8d58..6ac8683ac054 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenFromRecentSupportNonResizable.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenFromRecentSupportNonResizable.kt @@ -54,9 +54,9 @@ class LegacySplitScreenFromRecentSupportNonResizable( testSpec: FlickerTestParameter ) : LegacySplitScreenTransition(testSpec) { - override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit - get() = { configuration -> - cleanSetup(this, configuration) + override val transition: FlickerBuilder.() -> Unit + get() = { + cleanSetup(this) setup { eachRun { nonResizeableApp.launchViaIntent(wmHelper) 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 index 1e89a25c06df..b01f41c9e2ec 100644 --- 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 @@ -26,7 +26,7 @@ import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen abstract class LegacySplitScreenRotateTransition( testSpec: FlickerTestParameter ) : LegacySplitScreenTransition(testSpec) { - override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit + override val transition: FlickerBuilder.() -> Unit get() = { setup { eachRun { diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenToLauncher.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenToLauncher.kt index 2ccd03bf1d6a..fb1004bda0cb 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenToLauncher.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenToLauncher.kt @@ -16,16 +16,15 @@ package com.android.wm.shell.flicker.legacysplitscreen -import android.platform.test.annotations.Postsubmit import android.platform.test.annotations.Presubmit import android.view.Surface +import androidx.test.filters.FlakyTest import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.FlickerParametersRunnerFactory import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.FlickerTestParameterFactory import com.android.server.wm.flicker.annotation.Group2 import com.android.server.wm.flicker.dsl.FlickerBuilder -import com.android.server.wm.flicker.endRotation import com.android.server.wm.flicker.entireScreenCovered import com.android.server.wm.flicker.helpers.exitSplitScreen import com.android.server.wm.flicker.helpers.launchSplitScreen @@ -61,8 +60,8 @@ class LegacySplitScreenToLauncher( ) : LegacySplitScreenTransition(testSpec) { private val testApp = SimpleAppHelper(instrumentation) - override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit - get() = { configuration -> + override val transition: FlickerBuilder.() -> Unit + get() = { setup { test { device.wakeUpAndGoToHomeScreen() @@ -70,7 +69,7 @@ class LegacySplitScreenToLauncher( } eachRun { testApp.launchViaIntent(wmHelper) - this.setRotation(configuration.endRotation) + this.setRotation(testSpec.endRotation) device.launchSplitScreen(wmHelper) device.waitForIdle() } @@ -109,7 +108,7 @@ class LegacySplitScreenToLauncher( @Test fun navBarLayerRotatesAndScales() = testSpec.navBarLayerRotatesAndScales() - @Presubmit + @FlakyTest(bugId = 206753786) @Test fun statusBarLayerRotatesScales() = testSpec.statusBarLayerRotatesScales() @@ -117,11 +116,11 @@ class LegacySplitScreenToLauncher( @Test fun statusBarLayerIsVisible() = testSpec.statusBarLayerIsVisible() - @Postsubmit + @FlakyTest @Test fun dockedStackDividerBecomesInvisible() = testSpec.dockedStackDividerBecomesInvisible() - @Postsubmit + @FlakyTest @Test fun layerBecomesInvisible() { testSpec.assertLayers { @@ -131,7 +130,7 @@ class LegacySplitScreenToLauncher( } } - @Postsubmit + @FlakyTest @Test fun focusDoesNotChange() { testSpec.assertEventLog { diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenTransition.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenTransition.kt index 661c8b69068e..a4a1f617e497 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenTransition.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenTransition.kt @@ -25,12 +25,9 @@ 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.isRotated 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.repetitions -import com.android.server.wm.flicker.startRotation 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 @@ -45,7 +42,6 @@ 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 isRotated = testSpec.config.startRotation.isRotated() protected val splitScreenApp = SplitScreenHelper.getPrimary(instrumentation) protected val secondaryApp = SplitScreenHelper.getSecondary(instrumentation) protected val nonResizeableApp = SplitScreenHelper.getNonResizeable(instrumentation) @@ -82,15 +78,15 @@ abstract class LegacySplitScreenTransition(protected val testSpec: FlickerTestPa FlickerComponentName.SPLASH_SCREEN, FlickerComponentName.SNAPSHOT) - protected open val transition: FlickerBuilder.(Map<String, Any?>) -> Unit - get() = { configuration -> + protected open val transition: FlickerBuilder.() -> Unit + get() = { setup { eachRun { device.wakeUpAndGoToHomeScreen() device.openQuickStepAndClearRecentAppsFromOverview(wmHelper) secondaryApp.launchViaIntent(wmHelper) splitScreenApp.launchViaIntent(wmHelper) - this.setRotation(configuration.startRotation) + this.setRotation(testSpec.startRotation) } } teardown { @@ -105,19 +101,17 @@ abstract class LegacySplitScreenTransition(protected val testSpec: FlickerTestPa @FlickerBuilderProvider fun buildFlicker(): FlickerBuilder { return FlickerBuilder(instrumentation).apply { - withTestName { testSpec.name } - repeat { testSpec.config.repetitions } - transition(this, testSpec.config) + transition(this) } } - internal open val cleanSetup: FlickerBuilder.(Map<String, Any?>) -> Unit - get() = { configuration -> + internal open val cleanSetup: FlickerBuilder.() -> Unit + get() = { setup { eachRun { device.wakeUpAndGoToHomeScreen() device.openQuickStepAndClearRecentAppsFromOverview(wmHelper) - this.setRotation(configuration.startRotation) + this.setRotation(testSpec.startRotation) } } teardown { 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 new file mode 100644 index 000000000000..8446b37dbf06 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/OWNERS @@ -0,0 +1,2 @@ +# 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 index 34eff80a04bc..087b21c544c5 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/OpenAppToLegacySplitScreen.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/OpenAppToLegacySplitScreen.kt @@ -49,9 +49,9 @@ import org.junit.runners.Parameterized class OpenAppToLegacySplitScreen( testSpec: FlickerTestParameter ) : LegacySplitScreenTransition(testSpec) { - override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit - get() = { configuration -> - super.transition(this, configuration) + override val transition: FlickerBuilder.() -> Unit + get() = { + super.transition(this) transitions { device.launchSplitScreen(wmHelper) wmHelper.waitForAppTransitionIdle() diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ResizeLegacySplitScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ResizeLegacySplitScreen.kt index 58e1def6f37a..e2da1a4565c0 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ResizeLegacySplitScreen.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ResizeLegacySplitScreen.kt @@ -16,7 +16,6 @@ package com.android.wm.shell.flicker.legacysplitscreen -import android.graphics.Region import android.util.Rational import android.view.Surface import androidx.test.filters.FlakyTest @@ -37,10 +36,10 @@ 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.startRotation 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 @@ -69,12 +68,12 @@ class ResizeLegacySplitScreen( private val testAppTop = SimpleAppHelper(instrumentation) private val testAppBottom = ImeAppHelper(instrumentation) - override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit - get() = { configuration -> + override val transition: FlickerBuilder.() -> Unit + get() = { setup { eachRun { device.wakeUpAndGoToHomeScreen() - this.setRotation(configuration.startRotation) + this.setRotation(testSpec.startRotation) this.launcherStrategy.clearRecentAppsFromOverview() testAppBottom.launchViaIntent(wmHelper) device.pressHome() @@ -134,6 +133,7 @@ class ResizeLegacySplitScreen( @Test fun navBarLayerRotatesAndScales() = testSpec.navBarLayerRotatesAndScales() + @FlakyTest(bugId = 206753786) @Test fun statusBarLayerRotatesScales() = testSpec.statusBarLayerRotatesScales() @@ -166,12 +166,12 @@ class ResizeLegacySplitScreen( val dividerBounds = layer(DOCKED_STACK_DIVIDER_COMPONENT).visibleRegion.region.bounds - val topAppBounds = Region(0, 0, dividerBounds.right, + val topAppBounds = Region.from(0, 0, dividerBounds.right, dividerBounds.top + WindowUtils.dockedStackDividerInset) - val bottomAppBounds = Region(0, + val bottomAppBounds = Region.from(0, dividerBounds.bottom - WindowUtils.dockedStackDividerInset, displayBounds.right, - displayBounds.bottom - WindowUtils.navigationBarHeight) + displayBounds.bottom - WindowUtils.navigationBarFrameHeight) visibleRegion(Components.SimpleActivity.COMPONENT.toFlickerComponent()) .coversExactly(topAppBounds) visibleRegion(Components.ImeActivity.COMPONENT.toFlickerComponent()) @@ -187,12 +187,12 @@ class ResizeLegacySplitScreen( val dividerBounds = layer(DOCKED_STACK_DIVIDER_COMPONENT).visibleRegion.region.bounds - val topAppBounds = Region(0, 0, dividerBounds.right, + val topAppBounds = Region.from(0, 0, dividerBounds.right, dividerBounds.top + WindowUtils.dockedStackDividerInset) - val bottomAppBounds = Region(0, + val bottomAppBounds = Region.from(0, dividerBounds.bottom - WindowUtils.dockedStackDividerInset, displayBounds.right, - displayBounds.bottom - WindowUtils.navigationBarHeight) + displayBounds.bottom - WindowUtils.navigationBarFrameHeight) visibleRegion(Components.SimpleActivity.COMPONENT.toFlickerComponent()) .coversExactly(topAppBounds) @@ -220,8 +220,8 @@ class ResizeLegacySplitScreen( .map { val description = (startRatio.toString().replace("/", "-") + "_to_" + stopRatio.toString().replace("/", "-")) - val newName = "${FlickerTestParameter.defaultName(it.config)}_$description" - FlickerTestParameter(it.config, name = newName) + 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 index 8a50bc0b20cf..d703ea082c87 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateOneLaunchedAppAndEnterSplitScreen.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateOneLaunchedAppAndEnterSplitScreen.kt @@ -29,7 +29,6 @@ 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.startRotation import com.android.server.wm.flicker.statusBarLayerRotatesScales import com.android.server.wm.flicker.statusBarWindowIsVisible import com.android.wm.shell.flicker.dockedStackDividerIsVisibleAtEnd @@ -53,12 +52,12 @@ import org.junit.runners.Parameterized class RotateOneLaunchedAppAndEnterSplitScreen( testSpec: FlickerTestParameter ) : LegacySplitScreenRotateTransition(testSpec) { - override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit - get() = { configuration -> - super.transition(this, configuration) + override val transition: FlickerBuilder.() -> Unit + get() = { + super.transition(this) transitions { device.launchSplitScreen(wmHelper) - this.setRotation(testSpec.config.startRotation) + this.setRotation(testSpec.startRotation) } } @@ -69,14 +68,14 @@ class RotateOneLaunchedAppAndEnterSplitScreen( @Presubmit @Test fun dockedStackPrimaryBoundsIsVisibleAtEnd() = - testSpec.dockedStackPrimaryBoundsIsVisibleAtEnd(testSpec.config.startRotation, + testSpec.dockedStackPrimaryBoundsIsVisibleAtEnd(testSpec.startRotation, splitScreenApp.component) @Presubmit @Test fun navBarLayerRotatesAndScales() = testSpec.navBarLayerRotatesAndScales() - @Presubmit + @FlakyTest(bugId = 206753786) @Test fun statusBarLayerRotatesScales() = testSpec.statusBarLayerRotatesScales() diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateOneLaunchedAppInSplitScreenMode.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateOneLaunchedAppInSplitScreenMode.kt index 84676a9186be..6b1883914e59 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateOneLaunchedAppInSplitScreenMode.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateOneLaunchedAppInSplitScreenMode.kt @@ -29,7 +29,6 @@ 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.startRotation import com.android.server.wm.flicker.statusBarLayerRotatesScales import com.android.server.wm.flicker.statusBarWindowIsVisible import com.android.wm.shell.flicker.dockedStackDividerIsVisibleAtEnd @@ -53,11 +52,11 @@ import org.junit.runners.Parameterized class RotateOneLaunchedAppInSplitScreenMode( testSpec: FlickerTestParameter ) : LegacySplitScreenRotateTransition(testSpec) { - override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit - get() = { configuration -> - super.transition(this, configuration) + override val transition: FlickerBuilder.() -> Unit + get() = { + super.transition(this) transitions { - this.setRotation(testSpec.config.startRotation) + this.setRotation(testSpec.startRotation) device.launchSplitScreen(wmHelper) } } @@ -69,13 +68,13 @@ class RotateOneLaunchedAppInSplitScreenMode( @Presubmit @Test fun dockedStackPrimaryBoundsIsVisibleAtEnd() = testSpec.dockedStackPrimaryBoundsIsVisibleAtEnd( - testSpec.config.startRotation, splitScreenApp.component) + testSpec.startRotation, splitScreenApp.component) @Presubmit @Test fun navBarLayerRotatesAndScales() = testSpec.navBarLayerRotatesAndScales() - @Presubmit + @FlakyTest(bugId = 206753786) @Test fun statusBarLayerRotatesScales() = testSpec.statusBarLayerRotatesScales() diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateTwoLaunchedAppAndEnterSplitScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateTwoLaunchedAppAndEnterSplitScreen.kt index 2abdca9216f9..acd658b5ba56 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateTwoLaunchedAppAndEnterSplitScreen.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateTwoLaunchedAppAndEnterSplitScreen.kt @@ -18,6 +18,7 @@ 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 @@ -29,7 +30,6 @@ 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.startRotation import com.android.server.wm.flicker.statusBarLayerRotatesScales import com.android.server.wm.flicker.statusBarWindowIsVisible import com.android.wm.shell.flicker.dockedStackDividerIsVisibleAtEnd @@ -54,11 +54,11 @@ import org.junit.runners.Parameterized class RotateTwoLaunchedAppAndEnterSplitScreen( testSpec: FlickerTestParameter ) : LegacySplitScreenRotateTransition(testSpec) { - override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit - get() = { configuration -> - super.transition(this, configuration) + override val transition: FlickerBuilder.() -> Unit + get() = { + super.transition(this) transitions { - this.setRotation(testSpec.config.startRotation) + this.setRotation(testSpec.startRotation) device.launchSplitScreen(wmHelper) device.reopenAppFromOverview(wmHelper) } @@ -71,20 +71,20 @@ class RotateTwoLaunchedAppAndEnterSplitScreen( @Presubmit @Test fun dockedStackPrimaryBoundsIsVisibleAtEnd() = - testSpec.dockedStackPrimaryBoundsIsVisibleAtEnd(testSpec.config.startRotation, + testSpec.dockedStackPrimaryBoundsIsVisibleAtEnd(testSpec.startRotation, splitScreenApp.component) @Presubmit @Test fun dockedStackSecondaryBoundsIsVisibleAtEnd() = - testSpec.dockedStackSecondaryBoundsIsVisibleAtEnd(testSpec.config.startRotation, + testSpec.dockedStackSecondaryBoundsIsVisibleAtEnd(testSpec.startRotation, secondaryApp.component) @Presubmit @Test fun navBarLayerRotatesAndScales() = testSpec.navBarLayerRotatesAndScales() - @Presubmit + @FlakyTest(bugId = 206753786) @Test fun statusBarLayerRotatesScales() = testSpec.statusBarLayerRotatesScales() diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateTwoLaunchedAppInSplitScreenMode.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateTwoLaunchedAppInSplitScreenMode.kt index fe9b9f514015..b40be8b5f401 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateTwoLaunchedAppInSplitScreenMode.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateTwoLaunchedAppInSplitScreenMode.kt @@ -30,7 +30,6 @@ 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.startRotation import com.android.server.wm.flicker.statusBarLayerRotatesScales import com.android.server.wm.flicker.statusBarWindowIsVisible import com.android.wm.shell.flicker.dockedStackDividerIsVisibleAtEnd @@ -55,18 +54,18 @@ import org.junit.runners.Parameterized class RotateTwoLaunchedAppInSplitScreenMode( testSpec: FlickerTestParameter ) : LegacySplitScreenRotateTransition(testSpec) { - override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit - get() = { configuration -> - super.transition(this, configuration) + override val transition: FlickerBuilder.() -> Unit + get() = { + super.transition(this) setup { eachRun { device.launchSplitScreen(wmHelper) device.reopenAppFromOverview(wmHelper) - this.setRotation(testSpec.config.startRotation) + this.setRotation(testSpec.startRotation) } } transitions { - this.setRotation(testSpec.config.startRotation) + this.setRotation(testSpec.startRotation) } } @@ -77,20 +76,20 @@ class RotateTwoLaunchedAppInSplitScreenMode( @Presubmit @Test fun dockedStackPrimaryBoundsIsVisibleAtEnd() = - testSpec.dockedStackPrimaryBoundsIsVisibleAtEnd(testSpec.config.startRotation, + testSpec.dockedStackPrimaryBoundsIsVisibleAtEnd(testSpec.startRotation, splitScreenApp.component) @Presubmit @Test fun dockedStackSecondaryBoundsIsVisibleAtEnd() = - testSpec.dockedStackSecondaryBoundsIsVisibleAtEnd(testSpec.config.startRotation, + testSpec.dockedStackSecondaryBoundsIsVisibleAtEnd(testSpec.startRotation, secondaryApp.component) @Presubmit @Test fun navBarLayerRotatesAndScales() = testSpec.navBarLayerRotatesAndScales() - @Presubmit + @FlakyTest(bugId = 206753786) @Test fun statusBarLayerRotatesScales() = testSpec.statusBarLayerRotatesScales() diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipTest.kt index 52a744f3897d..0640ac526bd0 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipTest.kt @@ -55,16 +55,33 @@ import org.junit.runners.Parameterized @FixMethodOrder(MethodSorters.NAME_ASCENDING) @Group3 class EnterPipTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) { + /** * Defines the transition used to run the test */ - override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit - get() = buildTransition(eachRun = true, stringExtras = emptyMap()) { + 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 */ @@ -94,8 +111,8 @@ class EnterPipTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) { @Presubmit @Test fun pipWindowRemainInsideVisibleBounds() { - testSpec.assertWm { - coversAtMost(displayBounds, pipApp.component) + testSpec.assertWmVisibleRegion(pipApp.component) { + coversAtMost(displayBounds) } } @@ -106,8 +123,8 @@ class EnterPipTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) { @Presubmit @Test fun pipLayerRemainInsideVisibleBounds() { - testSpec.assertLayers { - coversAtMost(displayBounds, pipApp.component) + testSpec.assertLayersVisibleRegion(pipApp.component) { + coversAtMost(displayBounds) } } @@ -153,13 +170,14 @@ class EnterPipTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) { } /** - * Checks the focus doesn't change during the animation + * Checks that the focus changes between the [pipApp] window and the launcher when + * closing the pip window */ - @FlakyTest + @Presubmit @Test - fun focusDoesNotChange() { + fun focusChanges() { testSpec.assertEventLog { - this.focusDoesNotChange() + this.focusChanges(pipApp.`package`, "NexusLauncherActivity") } } @@ -175,7 +193,7 @@ class EnterPipTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) { fun getParams(): List<FlickerTestParameter> { return FlickerTestParameterFactory.getInstance() .getConfigNonRotationTests(supportedRotations = listOf(Surface.ROTATION_0), - repetitions = 5) + repetitions = 3) } } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientationTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientationTest.kt index c8c3f4d64294..accb524d3de1 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientationTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientationTest.kt @@ -74,9 +74,9 @@ class EnterPipToOtherOrientationTest( /** * Defines the transition used to run the test */ - override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit - get() = { configuration -> - setupAndTeardown(this, configuration) + override val transition: FlickerBuilder.() -> Unit + get() = { + setupAndTeardown(this) setup { eachRun { @@ -98,7 +98,7 @@ class EnterPipToOtherOrientationTest( // Enter PiP, and assert that the PiP is within bounds now that the device is back // in portrait broadcastActionTrigger.doAction(ACTION_ENTER_PIP) - wmHelper.waitFor { it.wmState.hasPipWindow() } + wmHelper.waitPipShown() wmHelper.waitForAppTransitionIdle() // during rotation the status bar becomes invisible and reappears at the end wmHelper.waitForNavBarStatusBarVisible() @@ -117,7 +117,7 @@ class EnterPipToOtherOrientationTest( * Checks that the [FlickerComponentName.STATUS_BAR] has the correct position at * the start and end of the transition */ - @Presubmit + @FlakyTest(bugId = 206753786) @Test override fun statusBarLayerRotatesScales() = testSpec.statusBarLayerRotatesScales() @@ -224,7 +224,7 @@ class EnterPipToOtherOrientationTest( fun getParams(): Collection<FlickerTestParameter> { return FlickerTestParameterFactory.getInstance() .getConfigNonRotationTests(supportedRotations = listOf(Surface.ROTATION_0), - repetitions = 5) + repetitions = 3) } } } 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 64b7eb53bd6f..990872f58dc1 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipToAppTransition.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipToAppTransition.kt @@ -34,8 +34,8 @@ abstract class ExitPipToAppTransition(testSpec: FlickerTestParameter) : PipTrans @Presubmit @Test open fun pipAppWindowRemainInsideVisibleBounds() { - testSpec.assertWm { - coversAtMost(displayBounds, pipApp.component) + testSpec.assertWmVisibleRegion(pipApp.component) { + coversAtMost(displayBounds) } } @@ -46,8 +46,8 @@ abstract class ExitPipToAppTransition(testSpec: FlickerTestParameter) : PipTrans @Presubmit @Test open fun pipAppLayerRemainInsideVisibleBounds() { - testSpec.assertLayers { - coversAtMost(displayBounds, pipApp.component) + testSpec.assertLayersVisibleRegion(pipApp.component) { + coversAtMost(displayBounds) } } @@ -102,7 +102,7 @@ abstract class ExitPipToAppTransition(testSpec: FlickerTestParameter) : PipTrans } /** - * Checks that the visible region of [pipApp] covers the full display area at the end of + * Checks that the visible region oft [pipApp] covers the full display area at the end of * the transition */ @Presubmit diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipTransition.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipTransition.kt index 5207fed59208..0b4bc761838d 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipTransition.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipTransition.kt @@ -18,23 +18,22 @@ package com.android.wm.shell.flicker.pip import android.platform.test.annotations.Presubmit import android.view.Surface -import androidx.test.filters.FlakyTest import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.LAUNCHER_COMPONENT import com.android.server.wm.flicker.dsl.FlickerBuilder +import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled import com.android.server.wm.flicker.helpers.setRotation -import com.android.server.wm.flicker.startRotation 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.(Map<String, Any?>) -> Unit - get() = buildTransition(eachRun = true) { configuration -> + override val transition: FlickerBuilder.() -> Unit + get() = buildTransition(eachRun = true) { setup { eachRun { - this.setRotation(configuration.startRotation) + this.setRotation(testSpec.startRotation) } } teardown { @@ -52,11 +51,28 @@ abstract class ExitPipTransition(testSpec: FlickerTestParameter) : PipTransition @Presubmit @Test open fun pipWindowBecomesInvisible() { - testSpec.assertWm { - this.invoke("hasPipWindow") { - it.isPinned(pipApp.component).isAppWindowVisible(pipApp.component) - }.then().invoke("!hasPipWindow") { - it.isNotPinned(pipApp.component).isAppWindowInvisible(pipApp.component) + 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) + } } } } @@ -77,16 +93,4 @@ abstract class ExitPipTransition(testSpec: FlickerTestParameter) : PipTransition .isVisible(LAUNCHER_COMPONENT) } } - - /** - * Checks that the focus changes between the [pipApp] window and the launcher when - * closing the pip window - */ - @FlakyTest(bugId = 151179149) - @Test - open fun focusChanges() { - testSpec.assertEventLog { - this.focusChanges(pipApp.launcherName, "NexusLauncherActivity") - } - } -}
\ No newline at end of file +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaExpandButtonClickTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaExpandButtonClickTest.kt index b53342d6f2f7..a3ed79bf0409 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaExpandButtonClickTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaExpandButtonClickTest.kt @@ -17,6 +17,7 @@ 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 @@ -24,6 +25,7 @@ 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 @@ -52,6 +54,7 @@ import org.junit.runners.Parameterized @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) @Group3 +@FlakyTest(bugId = 219750830) class ExitPipViaExpandButtonClickTest( testSpec: FlickerTestParameter ) : ExitPipToAppTransition(testSpec) { @@ -59,7 +62,7 @@ class ExitPipViaExpandButtonClickTest( /** * Defines the transition used to run the test */ - override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit + override val transition: FlickerBuilder.() -> Unit get() = buildTransition(eachRun = true) { setup { eachRun { @@ -71,10 +74,20 @@ class ExitPipViaExpandButtonClickTest( // This will bring PipApp to fullscreen pipApp.expandPipWindowToApp(wmHelper) // Wait until the other app is no longer visible - wmHelper.waitForSurfaceAppeared(testApp.component.toWindowName()) + wmHelper.waitForWindowSurfaceDisappeared(testApp.component) } } + /** {@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. @@ -86,7 +99,7 @@ class ExitPipViaExpandButtonClickTest( @JvmStatic fun getParams(): List<FlickerTestParameter> { return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests( - supportedRotations = listOf(Surface.ROTATION_0), repetitions = 5) + supportedRotations = listOf(Surface.ROTATION_0), repetitions = 3) } } -}
\ No newline at end of file +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaIntentTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaIntentTest.kt index 1fec3cf85214..37e9344348d9 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaIntentTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaIntentTest.kt @@ -16,14 +16,19 @@ 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 @@ -56,7 +61,7 @@ class ExitPipViaIntentTest(testSpec: FlickerTestParameter) : ExitPipToAppTransit /** * Defines the transition used to run the test */ - override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit + override val transition: FlickerBuilder.() -> Unit get() = buildTransition(eachRun = true) { setup { eachRun { @@ -66,12 +71,32 @@ class ExitPipViaIntentTest(testSpec: FlickerTestParameter) : ExitPipToAppTransit } transitions { // This will bring PipApp to fullscreen - pipApp.launchViaIntent(wmHelper) + pipApp.exitPipToFullScreenViaIntent(wmHelper) // Wait until the other app is no longer visible - wmHelper.waitForSurfaceAppeared(testApp.component.toWindowName()) + 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. @@ -83,7 +108,7 @@ class ExitPipViaIntentTest(testSpec: FlickerTestParameter) : ExitPipToAppTransit @JvmStatic fun getParams(): List<FlickerTestParameter> { return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests( - supportedRotations = listOf(Surface.ROTATION_0), repetitions = 5) + supportedRotations = listOf(Surface.ROTATION_0), repetitions = 3) } } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipWithDismissButtonTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipWithDismissButtonTest.kt index 73626c23065a..437ad893f1d9 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipWithDismissButtonTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipWithDismissButtonTest.kt @@ -16,7 +16,9 @@ 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 @@ -24,6 +26,7 @@ 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 @@ -52,14 +55,32 @@ import org.junit.runners.Parameterized @FixMethodOrder(MethodSorters.NAME_ASCENDING) @Group3 class ExitPipWithDismissButtonTest(testSpec: FlickerTestParameter) : ExitPipTransition(testSpec) { - override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit + + override val transition: FlickerBuilder.() -> Unit get() = { - super.transition(this, it) + super.transition(this) 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. @@ -72,7 +93,7 @@ class ExitPipWithDismissButtonTest(testSpec: FlickerTestParameter) : ExitPipTran fun getParams(): List<FlickerTestParameter> { return FlickerTestParameterFactory.getInstance() .getConfigNonRotationTests(supportedRotations = listOf(Surface.ROTATION_0), - repetitions = 5) + repetitions = 3) } } -}
\ No newline at end of file +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipWithSwipeDownTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipWithSwipeDownTest.kt index 9e43deef8d99..ab07ede5bb32 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipWithSwipeDownTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipWithSwipeDownTest.kt @@ -55,37 +55,21 @@ import org.junit.runners.Parameterized @FixMethodOrder(MethodSorters.NAME_ASCENDING) @Group3 class ExitPipWithSwipeDownTest(testSpec: FlickerTestParameter) : ExitPipTransition(testSpec) { - override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit - get() = { args -> - super.transition(this, args) + override val transition: FlickerBuilder.() -> Unit + get() = { + super.transition(this) transitions { val pipRegion = wmHelper.getWindowRegion(pipApp.component).bounds val pipCenterX = pipRegion.centerX() val pipCenterY = pipRegion.centerY() val displayCenterX = device.displayWidth / 2 device.swipe(pipCenterX, pipCenterY, displayCenterX, device.displayHeight, 10) - wmHelper.waitFor("!hasPipWindow") { !it.wmState.hasPipWindow() } + wmHelper.waitPipGone() wmHelper.waitForWindowSurfaceDisappeared(pipApp.component) wmHelper.waitForAppTransitionIdle() } } - @Presubmit - @Test - override fun navBarLayerIsVisible() = super.navBarLayerIsVisible() - - @Presubmit - @Test - override fun statusBarLayerIsVisible() = super.statusBarLayerIsVisible() - - @Presubmit - @Test - override fun navBarWindowIsVisible() = super.navBarWindowIsVisible() - - @Presubmit - @Test - override fun statusBarWindowIsVisible() = super.statusBarWindowIsVisible() - @FlakyTest @Test override fun pipWindowBecomesInvisible() = super.pipWindowBecomesInvisible() @@ -94,17 +78,20 @@ class ExitPipWithSwipeDownTest(testSpec: FlickerTestParameter) : ExitPipTransiti @Test override fun pipLayerBecomesInvisible() = super.pipLayerBecomesInvisible() - @Presubmit + @FlakyTest(bugId = 206753786) @Test override fun statusBarLayerRotatesScales() = testSpec.statusBarLayerRotatesScales() + /** + * Checks that the focus doesn't change between windows during the transition + */ @Presubmit @Test - override fun entireScreenCovered() = super.entireScreenCovered() - - @Presubmit - @Test - override fun navBarLayerRotatesAndScales() = super.navBarLayerRotatesAndScales() + fun focusDoesNotChange() { + testSpec.assertEventLog { + this.focusDoesNotChange() + } + } companion object { /** @@ -118,7 +105,7 @@ class ExitPipWithSwipeDownTest(testSpec: FlickerTestParameter) : ExitPipTransiti fun getParams(): List<FlickerTestParameter> { return FlickerTestParameterFactory.getInstance() .getConfigNonRotationTests(supportedRotations = listOf(Surface.ROTATION_0), - repetitions = 20) + repetitions = 3) } } }
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnDoubleClickTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnDoubleClickTest.kt index d0fee9a82093..28b7fc9bd29e 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,9 +16,9 @@ package com.android.wm.shell.flicker.pip +import androidx.test.filters.FlakyTest import android.platform.test.annotations.Presubmit import android.view.Surface -import androidx.test.filters.FlakyTest import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.FlickerParametersRunnerFactory import com.android.server.wm.flicker.FlickerTestParameter @@ -55,7 +55,7 @@ import org.junit.runners.Parameterized @FixMethodOrder(MethodSorters.NAME_ASCENDING) @Group3 class ExpandPipOnDoubleClickTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) { - override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit + override val transition: FlickerBuilder.() -> Unit get() = buildTransition(eachRun = true) { transitions { pipApp.doubleClickPipWindow(wmHelper) @@ -69,8 +69,8 @@ class ExpandPipOnDoubleClickTest(testSpec: FlickerTestParameter) : PipTransition @Presubmit @Test fun pipWindowRemainInsideVisibleBounds() { - testSpec.assertWm { - coversAtMost(displayBounds, pipApp.component) + testSpec.assertWmVisibleRegion(pipApp.component) { + coversAtMost(displayBounds) } } @@ -81,8 +81,8 @@ class ExpandPipOnDoubleClickTest(testSpec: FlickerTestParameter) : PipTransition @Presubmit @Test fun pipLayerRemainInsideVisibleBounds() { - testSpec.assertLayers { - coversAtMost(displayBounds, pipApp.component) + testSpec.assertLayersVisibleRegion(pipApp.component) { + coversAtMost(displayBounds) } } @@ -111,7 +111,7 @@ class ExpandPipOnDoubleClickTest(testSpec: FlickerTestParameter) : PipTransition /** * Checks that the visible region of [pipApp] always expands during the animation */ - @Presubmit + @FlakyTest(bugId = 228012337) @Test fun pipLayerExpands() { val layerName = pipApp.component.toLayerName() @@ -123,6 +123,18 @@ 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 } + pipLayerList.zipWithNext { previous, current -> + current.visibleRegion.isSameAspectRatio(previous.visibleRegion) + } + } + } + /** * Checks [pipApp] window remains pinned throughout the animation */ @@ -148,7 +160,7 @@ class ExpandPipOnDoubleClickTest(testSpec: FlickerTestParameter) : PipTransition /** * Checks that the focus doesn't change between windows during the transition */ - @FlakyTest + @FlakyTest(bugId = 216306753) @Test fun focusDoesNotChange() { testSpec.assertEventLog { @@ -156,6 +168,10 @@ class ExpandPipOnDoubleClickTest(testSpec: FlickerTestParameter) : PipTransition } } + @FlakyTest(bugId = 206753786) + @Test + override fun statusBarLayerRotatesScales() = super.statusBarLayerRotatesScales() + companion object { /** * Creates the test configurations. @@ -168,7 +184,7 @@ class ExpandPipOnDoubleClickTest(testSpec: FlickerTestParameter) : PipTransition fun getParams(): List<FlickerTestParameter> { return FlickerTestParameterFactory.getInstance() .getConfigNonRotationTests(supportedRotations = listOf(Surface.ROTATION_0), - repetitions = 5) + repetitions = 3) } } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipDownShelfHeightChangeTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipDownShelfHeightChangeTest.kt index 0ab857d755ee..8729bb6776f0 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipDownShelfHeightChangeTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipDownShelfHeightChangeTest.kt @@ -17,14 +17,16 @@ 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.RegionSubject +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 @@ -53,13 +55,13 @@ import org.junit.runners.Parameterized @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) @Group3 -class MovePipDownShelfHeightChangeTest( +open class MovePipDownShelfHeightChangeTest( testSpec: FlickerTestParameter ) : MovePipShelfHeightTransition(testSpec) { /** * Defines the transition used to run the test */ - override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit + override val transition: FlickerBuilder.() -> Unit get() = buildTransition(eachRun = false) { teardown { eachRun { @@ -78,6 +80,11 @@ class MovePipDownShelfHeightChangeTest( current.isHigherOrEqual(previous.region) } + /** {@inheritDoc} */ + @FlakyTest(bugId = 206753786) + @Test + override fun statusBarLayerRotatesScales() = super.statusBarLayerRotatesScales() + companion object { /** * Creates the test configurations. @@ -89,7 +96,7 @@ class MovePipDownShelfHeightChangeTest( @JvmStatic fun getParams(): List<FlickerTestParameter> { return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests( - supportedRotations = listOf(Surface.ROTATION_0), repetitions = 5) + supportedRotations = listOf(Surface.ROTATION_0), repetitions = 3) } } } 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 6e0324c17272..0499e7de9a0a 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 @@ -19,7 +19,7 @@ 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.RegionSubject +import com.android.server.wm.flicker.traces.region.RegionSubject import com.android.wm.shell.flicker.helpers.FixedAppHelper import org.junit.Test @@ -66,8 +66,8 @@ abstract class MovePipShelfHeightTransition( @Presubmit @Test open fun pipWindowRemainInsideVisibleBounds() { - testSpec.assertWm { - coversAtMost(displayBounds, pipApp.component) + testSpec.assertWmVisibleRegion(pipApp.component) { + coversAtMost(displayBounds) } } @@ -78,8 +78,8 @@ abstract class MovePipShelfHeightTransition( @Presubmit @Test open fun pipLayerRemainInsideVisibleBounds() { - testSpec.assertLayers { - coversAtMost(displayBounds, pipApp.component) + testSpec.assertLayersVisibleRegion(pipApp.component) { + coversAtMost(displayBounds) } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipUpShelfHeightChangeTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipUpShelfHeightChangeTest.kt index e507edfda48c..388b5e0b5e47 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipUpShelfHeightChangeTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipUpShelfHeightChangeTest.kt @@ -16,15 +16,20 @@ package com.android.wm.shell.flicker.pip +import androidx.test.filters.FlakyTest +import android.platform.test.annotations.RequiresDevice import android.view.Surface -import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.FlickerParametersRunnerFactory import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.FlickerTestParameterFactory import com.android.server.wm.flicker.annotation.Group3 import com.android.server.wm.flicker.dsl.FlickerBuilder -import com.android.server.wm.flicker.traces.RegionSubject +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 @@ -56,10 +61,15 @@ import org.junit.runners.Parameterized class MovePipUpShelfHeightChangeTest( testSpec: FlickerTestParameter ) : MovePipShelfHeightTransition(testSpec) { + @Before + fun before() { + Assume.assumeFalse(isShellTransitionsEnabled) + } + /** * Defines the transition used to run the test */ - override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit + override val transition: FlickerBuilder.() -> Unit get() = buildTransition(eachRun = false) { teardown { eachRun { @@ -78,6 +88,11 @@ class MovePipUpShelfHeightChangeTest( current.isLowerOrEqual(previous.region) } + /** {@inheritDoc} */ + @FlakyTest(bugId = 206753786) + @Test + override fun statusBarLayerRotatesScales() = super.statusBarLayerRotatesScales() + companion object { /** * Creates the test configurations. @@ -89,7 +104,7 @@ class MovePipUpShelfHeightChangeTest( @JvmStatic fun getParams(): List<FlickerTestParameter> { return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests( - supportedRotations = listOf(Surface.ROTATION_0), repetitions = 5) + supportedRotations = listOf(Surface.ROTATION_0), repetitions = 3) } } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/OWNERS b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/OWNERS new file mode 100644 index 000000000000..172e24bf4574 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/OWNERS @@ -0,0 +1,2 @@ +# window manager > wm shell > Picture-In-Picture +# Bug component: 316251 diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipKeyboardTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipKeyboardTest.kt index aba8aced298f..1e30f6b83874 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipKeyboardTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipKeyboardTest.kt @@ -18,6 +18,7 @@ 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 @@ -25,10 +26,12 @@ 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.flicker.startRotation 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 @@ -44,15 +47,21 @@ import org.junit.runners.Parameterized @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) @Group4 -class PipKeyboardTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) { +@FlakyTest(bugId = 218604389) +open class PipKeyboardTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) { private val imeApp = ImeAppHelper(instrumentation) - override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit - get() = buildTransition(eachRun = false) { configuration -> + @Before + open fun before() { + assumeFalse(isShellTransitionsEnabled) + } + + override val transition: FlickerBuilder.() -> Unit + get() = buildTransition(eachRun = false) { setup { test { imeApp.launchViaIntent(wmHelper) - setRotation(configuration.startRotation) + setRotation(testSpec.startRotation) } } teardown { @@ -71,15 +80,20 @@ class PipKeyboardTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) } } + /** {@inheritDoc} */ + @FlakyTest(bugId = 206753786) + @Test + override fun statusBarLayerRotatesScales() = super.statusBarLayerRotatesScales() + /** * Ensure the pip window remains visible throughout any keyboard interactions */ @Presubmit @Test - fun pipInVisibleBounds() { - testSpec.assertWm { - val displayBounds = WindowUtils.getDisplayBounds(testSpec.config.startRotation) - coversAtMost(displayBounds, pipApp.component) + open fun pipInVisibleBounds() { + testSpec.assertWmVisibleRegion(pipApp.component) { + val displayBounds = WindowUtils.getDisplayBounds(testSpec.startRotation) + coversAtMost(displayBounds) } } @@ -88,7 +102,7 @@ class PipKeyboardTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) */ @Presubmit @Test - fun pipIsAboveAppWindow() { + open fun pipIsAboveAppWindow() { testSpec.assertWmTag(TAG_IME_VISIBLE) { isAboveWindow(FlickerComponentName.IME, pipApp.component) } @@ -102,7 +116,7 @@ class PipKeyboardTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) fun getParams(): Collection<FlickerTestParameter> { return FlickerTestParameterFactory.getInstance() .getConfigNonRotationTests(supportedRotations = listOf(Surface.ROTATION_0), - repetitions = 5) + repetitions = 3) } } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipKeyboardTestShellTransit.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipKeyboardTestShellTransit.kt new file mode 100644 index 000000000000..1a21d32f568c --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipKeyboardTestShellTransit.kt @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY 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 androidx.test.filters.RequiresDevice +import com.android.server.wm.flicker.FlickerParametersRunnerFactory +import com.android.server.wm.flicker.FlickerTestParameter +import com.android.server.wm.flicker.annotation.Group4 +import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled +import org.junit.Assume +import org.junit.Before +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +@RequiresDevice +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +@Group4 +@FlakyTest(bugId = 217777115) +class PipKeyboardTestShellTransit(testSpec: FlickerTestParameter) : PipKeyboardTest(testSpec) { + + @Before + override fun before() { + Assume.assumeTrue(isShellTransitionsEnabled) + } + + @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/PipLegacySplitScreenTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipLegacySplitScreenTest.kt index 9bea5c03dadb..21175a0767a5 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipLegacySplitScreenTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipLegacySplitScreenTest.kt @@ -27,7 +27,6 @@ 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.repetitions import com.android.server.wm.flicker.rules.RemoveAllTasksButHomeRule.Companion.removeAllTasksButHome import com.android.wm.shell.flicker.helpers.BaseAppHelper.Companion.isShellTransitionsEnabled import com.android.wm.shell.flicker.helpers.FixedAppHelper @@ -64,10 +63,8 @@ class PipLegacySplitScreenTest(testSpec: FlickerTestParameter) : PipTransition(t assumeFalse(isShellTransitionsEnabled()) } - override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit + override val transition: FlickerBuilder.() -> Unit get() = { - withTestName { testSpec.name } - repeat { testSpec.config.repetitions } setup { test { removeAllTasksButHome() @@ -92,11 +89,16 @@ class PipLegacySplitScreenTest(testSpec: FlickerTestParameter) : PipTransition(t } } + /** {@inheritDoc} */ + @FlakyTest(bugId = 206753786) + @Test + override fun statusBarLayerRotatesScales() = super.statusBarLayerRotatesScales() + @FlakyTest(bugId = 161435597) @Test fun pipWindowInsideDisplayBounds() { - testSpec.assertWm { - coversAtMost(displayBounds, pipApp.component) + testSpec.assertWmVisibleRegion(pipApp.component) { + coversAtMost(displayBounds) } } @@ -113,8 +115,8 @@ class PipLegacySplitScreenTest(testSpec: FlickerTestParameter) : PipTransition(t @FlakyTest(bugId = 161435597) @Test fun pipLayerInsideDisplayBounds() { - testSpec.assertLayers { - coversAtMost(displayBounds, pipApp.component) + testSpec.assertLayersVisibleRegion(pipApp.component) { + coversAtMost(displayBounds) } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipRotationTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipRotationTest.kt index 669f37ad1e72..c1ee1a7cbb35 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipRotationTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipRotationTest.kt @@ -25,14 +25,14 @@ 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.endRotation 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.startRotation 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 @@ -47,7 +47,7 @@ import org.junit.runners.Parameterized * Actions: * Launch a [pipApp] in pip mode * Launch another app [fixedApp] (appears below pip) - * Rotate the screen from [testSpec.config.startRotation] to [testSpec.config.endRotation] + * Rotate the screen from [testSpec.startRotation] to [testSpec.endRotation] * (usually, 0->90 and 90->0) * * Notes: @@ -63,23 +63,23 @@ import org.junit.runners.Parameterized @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) @Group4 -class PipRotationTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) { +open class PipRotationTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) { private val fixedApp = FixedAppHelper(instrumentation) - private val screenBoundsStart = WindowUtils.getDisplayBounds(testSpec.config.startRotation) - private val screenBoundsEnd = WindowUtils.getDisplayBounds(testSpec.config.endRotation) + private val screenBoundsStart = WindowUtils.getDisplayBounds(testSpec.startRotation) + private val screenBoundsEnd = WindowUtils.getDisplayBounds(testSpec.endRotation) - override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit - get() = buildTransition(eachRun = false) { configuration -> + override val transition: FlickerBuilder.() -> Unit + get() = buildTransition(eachRun = false) { setup { test { fixedApp.launchViaIntent(wmHelper) } eachRun { - setRotation(configuration.startRotation) + setRotation(testSpec.startRotation) } } transitions { - setRotation(configuration.endRotation) + setRotation(testSpec.endRotation) } } @@ -100,7 +100,7 @@ class PipRotationTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) /** * Checks the position of the status bar at the start and end of the transition */ - @Presubmit + @FlakyTest(bugId = 206753786) @Test override fun statusBarLayerRotatesScales() = testSpec.statusBarLayerRotatesScales() @@ -129,15 +129,30 @@ class PipRotationTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) /** * Checks that [pipApp] layer is within [screenBoundsStart] at the start of the transition */ - @Presubmit - @Test - fun pipLayerRotates_StartingBounds() { + 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 @@ -184,7 +199,7 @@ class PipRotationTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) fun getParams(): Collection<FlickerTestParameter> { return FlickerTestParameterFactory.getInstance().getConfigRotationTests( supportedRotations = listOf(Surface.ROTATION_0, Surface.ROTATION_90), - repetitions = 5) + repetitions = 3) } } } 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 e8a61e8a1dae..8d542c8ec9e6 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 @@ -26,15 +26,12 @@ import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.dsl.FlickerBuilder import com.android.server.wm.flicker.entireScreenCovered import com.android.server.wm.flicker.helpers.WindowUtils -import com.android.server.wm.flicker.helpers.isRotated import com.android.server.wm.flicker.helpers.setRotation import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen import com.android.server.wm.flicker.navBarLayerIsVisible import com.android.server.wm.flicker.navBarLayerRotatesAndScales import com.android.server.wm.flicker.navBarWindowIsVisible -import com.android.server.wm.flicker.repetitions import com.android.server.wm.flicker.rules.RemoveAllTasksButHomeRule.Companion.removeAllTasksButHome -import com.android.server.wm.flicker.startRotation import com.android.server.wm.flicker.statusBarLayerIsVisible import com.android.server.wm.flicker.statusBarLayerRotatesScales import com.android.server.wm.flicker.statusBarWindowIsVisible @@ -44,11 +41,10 @@ import org.junit.Test abstract class PipTransition(protected val testSpec: FlickerTestParameter) { protected val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation() - protected val isRotated = testSpec.config.startRotation.isRotated() protected val pipApp = PipAppHelper(instrumentation) - protected val displayBounds = WindowUtils.getDisplayBounds(testSpec.config.startRotation) + protected val displayBounds = WindowUtils.getDisplayBounds(testSpec.startRotation) protected val broadcastActionTrigger = BroadcastActionTrigger(instrumentation) - protected abstract val transition: FlickerBuilder.(Map<String, Any?>) -> Unit + 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 { @@ -60,13 +56,6 @@ abstract class PipTransition(protected val testSpec: FlickerTestParameter) { .sendBroadcast(createIntentWithAction(broadcastAction)) } - fun requestOrientationForPip(orientation: Int) { - instrumentation.context.sendBroadcast( - createIntentWithAction(Components.PipActivity.ACTION_SET_REQUESTED_ORIENTATION) - .putExtra(Components.PipActivity.EXTRA_PIP_ORIENTATION, orientation.toString()) - ) - } - companion object { // Corresponds to ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE @JvmStatic @@ -81,16 +70,14 @@ abstract class PipTransition(protected val testSpec: FlickerTestParameter) { @FlickerBuilderProvider fun buildFlicker(): FlickerBuilder { return FlickerBuilder(instrumentation).apply { - withTestName { testSpec.name } - repeat { testSpec.config.repetitions } - transition(this, testSpec.config) + transition(this) } } /** * Gets a configuration that handles basic setup and teardown of pip tests */ - protected val setupAndTeardown: FlickerBuilder.(Map<String, Any?>) -> Unit + protected val setupAndTeardown: FlickerBuilder.() -> Unit get() = { setup { test { @@ -121,23 +108,22 @@ abstract class PipTransition(protected val testSpec: FlickerTestParameter) { protected open fun buildTransition( eachRun: Boolean, stringExtras: Map<String, String> = mapOf(Components.PipActivity.EXTRA_ENTER_PIP to "true"), - extraSpec: FlickerBuilder.(Map<String, Any?>) -> Unit = {} - ): FlickerBuilder.(Map<String, Any?>) -> Unit { - return { configuration -> - setupAndTeardown(this, configuration) + extraSpec: FlickerBuilder.() -> Unit = {} + ): FlickerBuilder.() -> Unit { + return { + setupAndTeardown(this) setup { test { - removeAllTasksButHome() if (!eachRun) { - pipApp.launchViaIntent(wmHelper, stringExtras = stringExtras) - wmHelper.waitFor { it.wmState.hasPipWindow() } + pipApp.launchViaIntentAndWaitForPip(wmHelper, stringExtras = stringExtras) + wmHelper.waitPipShown() } } eachRun { if (eachRun) { - pipApp.launchViaIntent(wmHelper, stringExtras = stringExtras) - wmHelper.waitFor { it.wmState.hasPipWindow() } + pipApp.launchViaIntentAndWaitForPip(wmHelper, stringExtras = stringExtras) + wmHelper.waitPipShown() } } } @@ -151,11 +137,10 @@ abstract class PipTransition(protected val testSpec: FlickerTestParameter) { if (!eachRun) { pipApp.exit(wmHelper) } - removeAllTasksButHome() } } - extraSpec(this, configuration) + extraSpec(this) } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/SetRequestedOrientationWhilePinnedTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/SetRequestedOrientationWhilePinnedTest.kt index d6dbc366aec0..e40f2bc1ed5a 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/SetRequestedOrientationWhilePinnedTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/SetRequestedOrientationWhilePinnedTest.kt @@ -16,6 +16,7 @@ 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 @@ -24,11 +25,13 @@ 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 com.android.wm.shell.flicker.testapp.Components.PipActivity.EXTRA_ENTER_PIP -import org.junit.Assert.assertEquals import org.junit.FixMethodOrder import org.junit.Test import org.junit.runner.RunWith @@ -36,75 +39,85 @@ import org.junit.runners.MethodSorters import org.junit.runners.Parameterized /** - * Test Pip with orientation changes. - * To run this test: `atest WMShellFlickerTests:PipOrientationTest` + * 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 -class SetRequestedOrientationWhilePinnedTest( +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.(Map<String, Any?>) -> Unit - get() = { configuration -> - setupAndTeardown(this, configuration) - + override val transition: FlickerBuilder.() -> Unit + get() = { setup { + test { + removeAllTasksButHome() + device.wakeUpAndGoToHomeScreen() + } eachRun { - // Launch the PiP activity fixed as landscape + // Launch the PiP activity fixed as landscape. pipApp.launchViaIntent(wmHelper, stringExtras = mapOf( - EXTRA_FIXED_ORIENTATION to ORIENTATION_LANDSCAPE.toString(), - EXTRA_ENTER_PIP to "true")) + 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 { - // Request that the orientation is set to landscape - broadcastActionTrigger.requestOrientationForPip(ORIENTATION_LANDSCAPE) - - // Launch the activity back into fullscreen and - // ensure that it is now in landscape + // 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) - assertEquals(Surface.ROTATION_90, device.displayRotation) + wmHelper.waitForAppTransitionIdle() + // System bar may fade out during fixed rotation. + wmHelper.waitForNavBarStatusBarVisible() } } - @FlakyTest + @Presubmit @Test - override fun navBarLayerIsVisible() = super.navBarLayerIsVisible() + fun displayEndsAt90Degrees() { + testSpec.assertWmEnd { + hasRotation(Surface.ROTATION_90) + } + } - @FlakyTest + @Presubmit @Test - override fun navBarWindowIsVisible() = super.navBarWindowIsVisible() + override fun navBarLayerIsVisible() = super.navBarLayerIsVisible() - @FlakyTest + @Presubmit @Test override fun statusBarLayerIsVisible() = super.statusBarLayerIsVisible() @FlakyTest @Test - override fun statusBarWindowIsVisible() = super.statusBarWindowIsVisible() - - @FlakyTest - @Test override fun navBarLayerRotatesAndScales() = super.navBarLayerRotatesAndScales() - @FlakyTest + @FlakyTest(bugId = 206753786) @Test override fun statusBarLayerRotatesScales() = super.statusBarLayerRotatesScales() - @FlakyTest + @Presubmit @Test fun pipWindowInsideDisplay() { testSpec.assertWmStart { @@ -112,7 +125,7 @@ class SetRequestedOrientationWhilePinnedTest( } } - @FlakyTest + @Presubmit @Test fun pipAppShowsOnTop() { testSpec.assertWmEnd { @@ -120,7 +133,7 @@ class SetRequestedOrientationWhilePinnedTest( } } - @FlakyTest + @Presubmit @Test fun pipLayerInsideDisplay() { testSpec.assertLayersStart { @@ -128,13 +141,15 @@ class SetRequestedOrientationWhilePinnedTest( } } - @FlakyTest + @Presubmit @Test - fun pipAlwaysVisible() = testSpec.assertWm { - this.isAppWindowVisible(pipApp.component) + fun pipAlwaysVisible() { + testSpec.assertWm { + this.isAppWindowVisible(pipApp.component) + } } - @FlakyTest + @Presubmit @Test fun pipAppLayerCoversFullScreen() { testSpec.assertLayersEnd { @@ -142,10 +157,6 @@ class SetRequestedOrientationWhilePinnedTest( } } - @FlakyTest - @Test - override fun entireScreenCovered() = super.entireScreenCovered() - companion object { @Parameterized.Parameters(name = "{0}") @JvmStatic 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 31e9167c79b2..9c3b0fa183b6 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 @@ -27,7 +27,7 @@ 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 +import org.junit.Assume.assumeTrue import org.junit.Before abstract class TvPipTestBase : PipTestBase(rotationToString(ROTATION_0), ROTATION_0) { @@ -37,7 +37,7 @@ abstract class TvPipTestBase : PipTestBase(rotationToString(ROTATION_0), ROTATIO @Before final override fun televisionSetUp() { // Should run only on TVs. - Assume.assumeTrue(isTelevision) + assumeTrue(isTelevision) systemUiProcessObserver.start() diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/AndroidManifest.xml b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/AndroidManifest.xml index 2cdbffa7589c..bd98585b67ec 100644 --- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/AndroidManifest.xml +++ b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/AndroidManifest.xml @@ -25,6 +25,7 @@ android:resizeableActivity="true" android:supportsPictureInPicture="true" android:launchMode="singleTop" + android:theme="@style/CutoutShortEdges" android:label="FixedApp" android:exported="true"> <intent-filter> @@ -37,6 +38,7 @@ 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"> @@ -52,6 +54,7 @@ <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"> @@ -68,6 +71,7 @@ <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> @@ -79,6 +83,7 @@ <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> @@ -90,6 +95,7 @@ <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> @@ -100,6 +106,7 @@ <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> @@ -111,16 +118,19 @@ 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/values/styles.xml b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/values/styles.xml new file mode 100644 index 000000000000..23b51cc06f04 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/values/styles.xml @@ -0,0 +1,34 @@ +<?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/BubbleHelper.java b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/BubbleHelper.java index d743dffd3c9e..6cd93eff2803 100644 --- 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 @@ -22,7 +22,6 @@ import android.app.NotificationChannel; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Person; -import android.app.RemoteInput; import android.content.Context; import android.content.Intent; import android.graphics.Point; @@ -116,24 +115,20 @@ public class BubbleHelper { private Notification.Builder getNotificationBuilder(int id) { Person chatBot = new Person.Builder() .setBot(true) - .setName("BubbleBot") + .setName("BubbleChat") .setImportant(true) .build(); - - RemoteInput remoteInput = new RemoteInput.Builder("key") - .setLabel("Reply") - .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("Bubble Chat") - .addMessage("Hello? This is bubble: " + id, + .setConversationTitle("BubbleChat") + .addMessage("BubbleChat", SystemClock.currentThreadTimeMillis() - 300000, chatBot) .addMessage("Is it me, " + id + ", you're looking for?", SystemClock.currentThreadTimeMillis(), chatBot) diff --git a/libs/WindowManager/Shell/tests/unittest/Android.bp b/libs/WindowManager/Shell/tests/unittest/Android.bp index fb53e5355c4a..ea10be564351 100644 --- a/libs/WindowManager/Shell/tests/unittest/Android.bp +++ b/libs/WindowManager/Shell/tests/unittest/Android.bp @@ -37,12 +37,14 @@ android_test { "androidx.test.ext.junit", "androidx.dynamicanimation_dynamicanimation", "dagger2", + "frameworks-base-testutils", "kotlinx-coroutines-android", "kotlinx-coroutines-core", "mockito-target-extended-minus-junit4", "truth-prebuilt", "testables", "platform-test-annotations", + "frameworks-base-testutils", ], libs: [ diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/BubbleOverflowTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/BubbleOverflowTest.java new file mode 100644 index 000000000000..8278c67a9b4f --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/BubbleOverflowTest.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; +import android.view.WindowManager; + +import androidx.test.filters.SmallTest; + +import com.android.wm.shell.bubbles.BubbleController; +import com.android.wm.shell.bubbles.BubbleOverflow; +import com.android.wm.shell.bubbles.BubbleStackView; +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; + +/** + * Unit tests for {@link com.android.wm.shell.bubbles.BubbleOverflow}. + */ +@SmallTest +@RunWith(AndroidTestingRunner.class) +@TestableLooper.RunWithLooper(setAsMainLooper = true) +public class BubbleOverflowTest extends ShellTestCase { + + private TestableBubblePositioner mPositioner; + private BubbleOverflow mOverflow; + + @Mock + private BubbleController mBubbleController; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + + mPositioner = new TestableBubblePositioner(mContext, mock(WindowManager.class)); + when(mBubbleController.getPositioner()).thenReturn(mPositioner); + when(mBubbleController.getStackView()).thenReturn(mock(BubbleStackView.class)); + + mOverflow = new BubbleOverflow(mContext, mPositioner); + } + + @Test + public void test_initialize() { + assertThat(mOverflow.getExpandedView()).isNull(); + + mOverflow.initialize(mBubbleController); + + assertThat(mOverflow.getExpandedView()).isNotNull(); + assertThat(mOverflow.getExpandedView().getBubbleKey()).isEqualTo(BubbleOverflow.KEY); + } + + @Test + public void test_cleanUpExpandedState() { + mOverflow.createExpandedView(); + assertThat(mOverflow.getExpandedView()).isNotNull(); + + mOverflow.cleanUpExpandedState(); + assertThat(mOverflow.getExpandedView()).isNull(); + } + +} 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 a3b98a8fc880..a6caefe6d3e7 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 @@ -37,6 +37,7 @@ import static org.mockito.Mockito.mock; 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; @@ -334,8 +335,7 @@ public class ShellTaskOrganizerTests { mOrganizer.onTaskAppeared(taskInfo1, null); // sizeCompatActivity is null if top activity is not in size compat. - verify(mCompatUI).onCompatInfoChanged(taskInfo1.displayId, taskInfo1.taskId, - null /* taskConfig */, null /* taskListener */); + verify(mCompatUI).onCompatInfoChanged(taskInfo1, null /* taskListener */); // sizeCompatActivity is non-null if top activity is in size compat. clearInvocations(mCompatUI); @@ -345,8 +345,7 @@ public class ShellTaskOrganizerTests { taskInfo2.topActivityInSizeCompat = true; taskInfo2.isVisible = true; mOrganizer.onTaskInfoChanged(taskInfo2); - verify(mCompatUI).onCompatInfoChanged(taskInfo1.displayId, taskInfo1.taskId, - taskInfo1.configuration, taskListener); + verify(mCompatUI).onCompatInfoChanged(taskInfo2, taskListener); // Not show size compat UI if task is not visible. clearInvocations(mCompatUI); @@ -356,13 +355,121 @@ public class ShellTaskOrganizerTests { taskInfo3.topActivityInSizeCompat = true; taskInfo3.isVisible = false; mOrganizer.onTaskInfoChanged(taskInfo3); - verify(mCompatUI).onCompatInfoChanged(taskInfo1.displayId, taskInfo1.taskId, - null /* taskConfig */, null /* taskListener */); + verify(mCompatUI).onCompatInfoChanged(taskInfo3, null /* taskListener */); clearInvocations(mCompatUI); mOrganizer.onTaskVanished(taskInfo1); - verify(mCompatUI).onCompatInfoChanged(taskInfo1.displayId, taskInfo1.taskId, - null /* taskConfig */, null /* taskListener */); + verify(mCompatUI).onCompatInfoChanged(taskInfo1, null /* taskListener */); + } + + @Test + public void testOnEligibleForLetterboxEducationActivityChanged() { + final RunningTaskInfo taskInfo1 = createTaskInfo(12, WINDOWING_MODE_FULLSCREEN); + taskInfo1.displayId = DEFAULT_DISPLAY; + taskInfo1.topActivityEligibleForLetterboxEducation = false; + final TrackingTaskListener taskListener = new TrackingTaskListener(); + mOrganizer.addListenerForType(taskListener, TASK_LISTENER_TYPE_FULLSCREEN); + mOrganizer.onTaskAppeared(taskInfo1, null); + + // Task listener sent to compat UI is null if top activity isn't eligible for letterbox + // education. + verify(mCompatUI).onCompatInfoChanged(taskInfo1, null /* taskListener */); + + // Task listener is non-null if top activity is eligible for letterbox education and task + // is visible. + clearInvocations(mCompatUI); + final RunningTaskInfo taskInfo2 = + createTaskInfo(taskInfo1.taskId, WINDOWING_MODE_FULLSCREEN); + taskInfo2.displayId = taskInfo1.displayId; + taskInfo2.topActivityEligibleForLetterboxEducation = true; + taskInfo2.isVisible = true; + mOrganizer.onTaskInfoChanged(taskInfo2); + verify(mCompatUI).onCompatInfoChanged(taskInfo2, taskListener); + + // Task listener is null if task is invisible. + clearInvocations(mCompatUI); + final RunningTaskInfo taskInfo3 = + createTaskInfo(taskInfo1.taskId, WINDOWING_MODE_FULLSCREEN); + taskInfo3.displayId = taskInfo1.displayId; + taskInfo3.topActivityEligibleForLetterboxEducation = true; + taskInfo3.isVisible = false; + mOrganizer.onTaskInfoChanged(taskInfo3); + verify(mCompatUI).onCompatInfoChanged(taskInfo3, null /* taskListener */); + + clearInvocations(mCompatUI); + mOrganizer.onTaskVanished(taskInfo1); + verify(mCompatUI).onCompatInfoChanged(taskInfo1, null /* taskListener */); + } + + @Test + public void testOnCameraCompatActivityChanged() { + final RunningTaskInfo taskInfo1 = createTaskInfo(1, WINDOWING_MODE_FULLSCREEN); + taskInfo1.displayId = DEFAULT_DISPLAY; + taskInfo1.cameraCompatControlState = TaskInfo.CAMERA_COMPAT_CONTROL_HIDDEN; + final TrackingTaskListener taskListener = new TrackingTaskListener(); + mOrganizer.addListenerForType(taskListener, TASK_LISTENER_TYPE_FULLSCREEN); + mOrganizer.onTaskAppeared(taskInfo1, null); + + // Task listener sent to compat UI is null if top activity doesn't request a camera + // compat control. + verify(mCompatUI).onCompatInfoChanged(taskInfo1, null /* taskListener */); + + // Task listener is non-null when request a camera compat control for a visible task. + clearInvocations(mCompatUI); + final RunningTaskInfo taskInfo2 = + createTaskInfo(taskInfo1.taskId, taskInfo1.getWindowingMode()); + taskInfo2.displayId = taskInfo1.displayId; + taskInfo2.cameraCompatControlState = TaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED; + taskInfo2.isVisible = true; + mOrganizer.onTaskInfoChanged(taskInfo2); + verify(mCompatUI).onCompatInfoChanged(taskInfo2, taskListener); + + // CompatUIController#onCompatInfoChanged is called when requested state for a camera + // compat control changes for a visible task. + clearInvocations(mCompatUI); + final RunningTaskInfo taskInfo3 = + createTaskInfo(taskInfo1.taskId, taskInfo1.getWindowingMode()); + taskInfo3.displayId = taskInfo1.displayId; + taskInfo3.cameraCompatControlState = TaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED; + taskInfo3.isVisible = true; + mOrganizer.onTaskInfoChanged(taskInfo3); + verify(mCompatUI).onCompatInfoChanged(taskInfo3, taskListener); + + // CompatUIController#onCompatInfoChanged is called when a top activity goes in size compat + // mode for a visible task that has a compat control. + clearInvocations(mCompatUI); + final RunningTaskInfo taskInfo4 = + createTaskInfo(taskInfo1.taskId, taskInfo1.getWindowingMode()); + taskInfo4.displayId = taskInfo1.displayId; + taskInfo4.topActivityInSizeCompat = true; + taskInfo4.cameraCompatControlState = TaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED; + taskInfo4.isVisible = true; + mOrganizer.onTaskInfoChanged(taskInfo4); + verify(mCompatUI).onCompatInfoChanged(taskInfo4, taskListener); + + // Task linster is null when a camera compat control is dimissed for a visible task. + clearInvocations(mCompatUI); + final RunningTaskInfo taskInfo5 = + createTaskInfo(taskInfo1.taskId, taskInfo1.getWindowingMode()); + taskInfo5.displayId = taskInfo1.displayId; + taskInfo5.cameraCompatControlState = TaskInfo.CAMERA_COMPAT_CONTROL_DISMISSED; + taskInfo5.isVisible = true; + mOrganizer.onTaskInfoChanged(taskInfo5); + verify(mCompatUI).onCompatInfoChanged(taskInfo5, null /* taskListener */); + + // Task linster is null when request a camera compat control for a invisible task. + clearInvocations(mCompatUI); + final RunningTaskInfo taskInfo6 = + createTaskInfo(taskInfo1.taskId, taskInfo1.getWindowingMode()); + taskInfo6.displayId = taskInfo1.displayId; + taskInfo6.cameraCompatControlState = TaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED; + taskInfo6.isVisible = false; + mOrganizer.onTaskInfoChanged(taskInfo6); + verify(mCompatUI).onCompatInfoChanged(taskInfo6, null /* taskListener */); + + clearInvocations(mCompatUI); + mOrganizer.onTaskVanished(taskInfo1); + verify(mCompatUI).onCompatInfoChanged(taskInfo1, null /* taskListener */); } @Test 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 6080f3ae78e8..403dbf9d9554 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 @@ -22,7 +22,7 @@ import android.content.Context; import android.hardware.display.DisplayManager; import android.testing.TestableContext; -import androidx.test.InstrumentationRegistry; +import androidx.test.platform.app.InstrumentationRegistry; import org.junit.After; import org.junit.Before; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TaskViewTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TaskViewTest.java index 1cbad155ba7b..32f1587752cb 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TaskViewTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TaskViewTest.java @@ -21,6 +21,8 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; +import static org.junit.Assume.assumeFalse; +import static org.junit.Assume.assumeTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; @@ -37,18 +39,22 @@ import android.app.ActivityOptions; import android.app.PendingIntent; import android.content.Context; import android.graphics.Rect; +import android.graphics.Region; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.view.SurfaceControl; import android.view.SurfaceHolder; import android.view.SurfaceSession; +import android.view.ViewTreeObserver; import android.window.WindowContainerToken; +import android.window.WindowContainerTransaction; import androidx.test.filters.SmallTest; import com.android.wm.shell.common.HandlerExecutor; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.common.SyncTransactionQueue.TransactionRunnable; +import com.android.wm.shell.transition.Transitions; import org.junit.After; import org.junit.Before; @@ -75,6 +81,8 @@ public class TaskViewTest extends ShellTestCase { HandlerExecutor mExecutor; @Mock SyncTransactionQueue mSyncQueue; + @Mock + TaskViewTransitions mTaskViewTransitions; SurfaceSession mSession; SurfaceControl mLeash; @@ -110,7 +118,7 @@ public class TaskViewTest extends ShellTestCase { return null; }).when(mSyncQueue).runInSync(any()); - mTaskView = new TaskView(mContext, mOrganizer, mSyncQueue); + mTaskView = new TaskView(mContext, mOrganizer, mTaskViewTransitions, mSyncQueue); mTaskView.setListener(mExecutor, mViewListener); } @@ -123,7 +131,7 @@ public class TaskViewTest extends ShellTestCase { @Test public void testSetPendingListener_throwsException() { - TaskView taskView = new TaskView(mContext, mOrganizer, mSyncQueue); + TaskView taskView = new TaskView(mContext, mOrganizer, mTaskViewTransitions, mSyncQueue); taskView.setListener(mExecutor, mViewListener); try { taskView.setListener(mExecutor, mViewListener); @@ -144,7 +152,8 @@ public class TaskViewTest extends ShellTestCase { } @Test - public void testOnTaskAppeared_noSurface() { + public void testOnTaskAppeared_noSurface_legacyTransitions() { + assumeFalse(Transitions.ENABLE_SHELL_TRANSITIONS); mTaskView.onTaskAppeared(mTaskInfo, mLeash); verify(mViewListener).onTaskCreated(eq(mTaskInfo.taskId), any()); @@ -154,7 +163,8 @@ public class TaskViewTest extends ShellTestCase { } @Test - public void testOnTaskAppeared_withSurface() { + public void testOnTaskAppeared_withSurface_legacyTransitions() { + assumeFalse(Transitions.ENABLE_SHELL_TRANSITIONS); mTaskView.surfaceCreated(mock(SurfaceHolder.class)); mTaskView.onTaskAppeared(mTaskInfo, mLeash); @@ -163,7 +173,8 @@ public class TaskViewTest extends ShellTestCase { } @Test - public void testSurfaceCreated_noTask() { + public void testSurfaceCreated_noTask_legacyTransitions() { + assumeFalse(Transitions.ENABLE_SHELL_TRANSITIONS); mTaskView.surfaceCreated(mock(SurfaceHolder.class)); verify(mViewListener).onInitialized(); @@ -172,7 +183,8 @@ public class TaskViewTest extends ShellTestCase { } @Test - public void testSurfaceCreated_withTask() { + public void testSurfaceCreated_withTask_legacyTransitions() { + assumeFalse(Transitions.ENABLE_SHELL_TRANSITIONS); mTaskView.onTaskAppeared(mTaskInfo, mLeash); mTaskView.surfaceCreated(mock(SurfaceHolder.class)); @@ -181,7 +193,8 @@ public class TaskViewTest extends ShellTestCase { } @Test - public void testSurfaceDestroyed_noTask() { + public void testSurfaceDestroyed_noTask_legacyTransitions() { + assumeFalse(Transitions.ENABLE_SHELL_TRANSITIONS); SurfaceHolder sh = mock(SurfaceHolder.class); mTaskView.surfaceCreated(sh); mTaskView.surfaceDestroyed(sh); @@ -190,7 +203,8 @@ public class TaskViewTest extends ShellTestCase { } @Test - public void testSurfaceDestroyed_withTask() { + public void testSurfaceDestroyed_withTask_legacyTransitions() { + assumeFalse(Transitions.ENABLE_SHELL_TRANSITIONS); SurfaceHolder sh = mock(SurfaceHolder.class); mTaskView.onTaskAppeared(mTaskInfo, mLeash); mTaskView.surfaceCreated(sh); @@ -201,7 +215,8 @@ public class TaskViewTest extends ShellTestCase { } @Test - public void testOnReleased() { + public void testOnReleased_legacyTransitions() { + assumeFalse(Transitions.ENABLE_SHELL_TRANSITIONS); mTaskView.onTaskAppeared(mTaskInfo, mLeash); mTaskView.surfaceCreated(mock(SurfaceHolder.class)); mTaskView.release(); @@ -211,7 +226,8 @@ public class TaskViewTest extends ShellTestCase { } @Test - public void testOnTaskVanished() { + public void testOnTaskVanished_legacyTransitions() { + assumeFalse(Transitions.ENABLE_SHELL_TRANSITIONS); mTaskView.onTaskAppeared(mTaskInfo, mLeash); mTaskView.surfaceCreated(mock(SurfaceHolder.class)); mTaskView.onTaskVanished(mTaskInfo); @@ -220,7 +236,8 @@ public class TaskViewTest extends ShellTestCase { } @Test - public void testOnBackPressedOnTaskRoot() { + public void testOnBackPressedOnTaskRoot_legacyTransitions() { + assumeFalse(Transitions.ENABLE_SHELL_TRANSITIONS); mTaskView.onTaskAppeared(mTaskInfo, mLeash); mTaskView.onBackPressedOnTaskRoot(mTaskInfo); @@ -228,17 +245,199 @@ public class TaskViewTest extends ShellTestCase { } @Test - public void testSetOnBackPressedOnTaskRoot() { + public void testSetOnBackPressedOnTaskRoot_legacyTransitions() { + assumeFalse(Transitions.ENABLE_SHELL_TRANSITIONS); mTaskView.onTaskAppeared(mTaskInfo, mLeash); verify(mOrganizer).setInterceptBackPressedOnTaskRoot(eq(mTaskInfo.token), eq(true)); } @Test - public void testUnsetOnBackPressedOnTaskRoot() { + public void testUnsetOnBackPressedOnTaskRoot_legacyTransitions() { + assumeFalse(Transitions.ENABLE_SHELL_TRANSITIONS); mTaskView.onTaskAppeared(mTaskInfo, mLeash); verify(mOrganizer).setInterceptBackPressedOnTaskRoot(eq(mTaskInfo.token), eq(true)); mTaskView.onTaskVanished(mTaskInfo); verify(mOrganizer).setInterceptBackPressedOnTaskRoot(eq(mTaskInfo.token), eq(false)); } + + @Test + 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); + + verify(mViewListener).onTaskCreated(eq(mTaskInfo.taskId), any()); + verify(mViewListener, never()).onInitialized(); + // If there's no surface the task should be made invisible + verify(mViewListener).onTaskVisibilityChanged(eq(mTaskInfo.taskId), eq(false)); + } + + @Test + public void testSurfaceCreated_noTask() { + assumeTrue(Transitions.ENABLE_SHELL_TRANSITIONS); + mTaskView.surfaceCreated(mock(SurfaceHolder.class)); + verify(mTaskViewTransitions, never()).setTaskViewVisible(any(), anyBoolean()); + + verify(mViewListener).onInitialized(); + // No task, no visibility change + verify(mViewListener, never()).onTaskVisibilityChanged(anyInt(), anyBoolean()); + } + + @Test + public void testOnNewTask_withSurface() { + 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); + + verify(mViewListener).onTaskCreated(eq(mTaskInfo.taskId), any()); + verify(mViewListener, never()).onTaskVisibilityChanged(anyInt(), anyBoolean()); + } + + @Test + 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); + mTaskView.surfaceCreated(mock(SurfaceHolder.class)); + + verify(mViewListener).onInitialized(); + verify(mTaskViewTransitions).setTaskViewVisible(eq(mTaskView), eq(true)); + + mTaskView.prepareOpenAnimation(false /* newTask */, new SurfaceControl.Transaction(), + new SurfaceControl.Transaction(), mTaskInfo, mLeash, wct); + + verify(mViewListener).onTaskVisibilityChanged(eq(mTaskInfo.taskId), eq(true)); + } + + @Test + public void testSurfaceDestroyed_noTask() { + assumeTrue(Transitions.ENABLE_SHELL_TRANSITIONS); + SurfaceHolder sh = mock(SurfaceHolder.class); + mTaskView.surfaceCreated(sh); + mTaskView.surfaceDestroyed(sh); + + verify(mViewListener, never()).onTaskVisibilityChanged(anyInt(), anyBoolean()); + } + + @Test + public void testSurfaceDestroyed_withTask() { + 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); + mTaskView.surfaceCreated(sh); + reset(mViewListener); + mTaskView.surfaceDestroyed(sh); + + verify(mTaskViewTransitions).setTaskViewVisible(eq(mTaskView), eq(false)); + + mTaskView.prepareHideAnimation(new SurfaceControl.Transaction()); + + verify(mViewListener).onTaskVisibilityChanged(eq(mTaskInfo.taskId), eq(false)); + } + + @Test + 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); + mTaskView.surfaceCreated(mock(SurfaceHolder.class)); + mTaskView.release(); + + verify(mOrganizer).removeListener(eq(mTaskView)); + verify(mViewListener).onReleased(); + verify(mTaskViewTransitions).removeTaskView(eq(mTaskView)); + } + + @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); + mTaskView.surfaceCreated(mock(SurfaceHolder.class)); + mTaskView.prepareCloseAnimation(); + + verify(mViewListener).onTaskRemovalStarted(eq(mTaskInfo.taskId)); + } + + @Test + 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); + + verify(mViewListener).onBackPressedOnTaskRoot(eq(mTaskInfo.taskId)); + } + + @Test + 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); + verify(mOrganizer).setInterceptBackPressedOnTaskRoot(eq(mTaskInfo.token), eq(true)); + } + + @Test + 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); + verify(mOrganizer).setInterceptBackPressedOnTaskRoot(eq(mTaskInfo.token), eq(true)); + + mTaskView.prepareCloseAnimation(); + verify(mOrganizer).setInterceptBackPressedOnTaskRoot(eq(mTaskInfo.token), eq(false)); + } + + @Test + public void testSetObscuredTouchRect() { + mTaskView.setObscuredTouchRect( + new Rect(/* left= */ 0, /* top= */ 10, /* right= */ 100, /* bottom= */ 120)); + ViewTreeObserver.InternalInsetsInfo insetsInfo = new ViewTreeObserver.InternalInsetsInfo(); + mTaskView.onComputeInternalInsets(insetsInfo); + + assertThat(insetsInfo.touchableRegion.contains(0, 10)).isTrue(); + // Region doesn't contain the right/bottom edge. + assertThat(insetsInfo.touchableRegion.contains(100 - 1, 120 - 1)).isTrue(); + + mTaskView.setObscuredTouchRect(null); + insetsInfo.touchableRegion.setEmpty(); + mTaskView.onComputeInternalInsets(insetsInfo); + + assertThat(insetsInfo.touchableRegion.contains(0, 10)).isFalse(); + assertThat(insetsInfo.touchableRegion.contains(100 - 1, 120 - 1)).isFalse(); + } + + @Test + public void testSetObscuredTouchRegion() { + Region obscuredRegion = new Region(10, 10, 19, 19); + obscuredRegion.union(new Rect(30, 30, 39, 39)); + + mTaskView.setObscuredTouchRegion(obscuredRegion); + ViewTreeObserver.InternalInsetsInfo insetsInfo = new ViewTreeObserver.InternalInsetsInfo(); + mTaskView.onComputeInternalInsets(insetsInfo); + + assertThat(insetsInfo.touchableRegion.contains(10, 10)).isTrue(); + assertThat(insetsInfo.touchableRegion.contains(20, 20)).isFalse(); + assertThat(insetsInfo.touchableRegion.contains(30, 30)).isTrue(); + + mTaskView.setObscuredTouchRegion(null); + insetsInfo.touchableRegion.setEmpty(); + mTaskView.onComputeInternalInsets(insetsInfo); + + assertThat(insetsInfo.touchableRegion.contains(10, 10)).isFalse(); + assertThat(insetsInfo.touchableRegion.contains(20, 20)).isFalse(); + assertThat(insetsInfo.touchableRegion.contains(30, 30)).isFalse(); + } } 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 2b5cd601b200..51eec27cfc0e 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 @@ -18,6 +18,7 @@ package com.android.wm.shell; import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; +import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; @@ -36,6 +37,7 @@ public final class TestRunningTaskInfoBuilder { private WindowContainerToken mToken = createMockWCToken(); private int mParentTaskId = INVALID_TASK_ID; private @WindowConfiguration.ActivityType int mActivityType = ACTIVITY_TYPE_STANDARD; + private @WindowConfiguration.WindowingMode int mWindowingMode = WINDOWING_MODE_UNDEFINED; public static WindowContainerToken createMockWCToken() { final IWindowContainerToken itoken = mock(IWindowContainerToken.class); @@ -60,6 +62,12 @@ public final class TestRunningTaskInfoBuilder { return this; } + public TestRunningTaskInfoBuilder setWindowingMode( + @WindowConfiguration.WindowingMode int windowingMode) { + mWindowingMode = windowingMode; + return this; + } + public ActivityManager.RunningTaskInfo build() { final ActivityManager.RunningTaskInfo info = new ActivityManager.RunningTaskInfo(); info.parentTaskId = INVALID_TASK_ID; @@ -67,6 +75,7 @@ public final class TestRunningTaskInfoBuilder { info.parentTaskId = mParentTaskId; info.configuration.windowConfiguration.setBounds(mBounds); info.configuration.windowConfiguration.setActivityType(mActivityType); + info.configuration.windowConfiguration.setWindowingMode(mWindowingMode); info.token = mToken; info.isResizeable = true; info.supportsMultiWindow = true; 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 new file mode 100644 index 000000000000..e7c5cb2183db --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java @@ -0,0 +1,306 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.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.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.atLeastOnce; +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.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +import android.app.IActivityTaskManager; +import android.app.WindowConfiguration; +import android.content.pm.ApplicationInfo; +import android.graphics.Point; +import android.graphics.Rect; +import android.hardware.HardwareBuffer; +import android.os.Handler; +import android.os.RemoteCallback; +import android.os.RemoteException; +import android.provider.Settings; +import android.testing.AndroidTestingRunner; +import android.testing.TestableContentResolver; +import android.testing.TestableContext; +import android.testing.TestableLooper; +import android.view.MotionEvent; +import android.view.RemoteAnimationTarget; +import android.view.SurfaceControl; +import android.window.BackEvent; +import android.window.BackNavigationInfo; +import android.window.IOnBackInvokedCallback; + +import androidx.test.filters.SmallTest; +import androidx.test.platform.app.InstrumentationRegistry; + +import com.android.internal.util.test.FakeSettingsProvider; +import com.android.wm.shell.TestShellExecutor; + +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** + * atest WMShellUnitTests:BackAnimationControllerTest + */ +@TestableLooper.RunWithLooper +@SmallTest +@RunWith(AndroidTestingRunner.class) +public class BackAnimationControllerTest { + + private static final String ANIMATION_ENABLED = "1"; + private final TestShellExecutor mShellExecutor = new TestShellExecutor(); + + @Rule + public TestableContext mContext = + new TestableContext(InstrumentationRegistry.getInstrumentation().getContext()); + + @Mock + private SurfaceControl.Transaction mTransaction; + + @Mock + private IActivityTaskManager mActivityTaskManager; + + @Mock + private IOnBackInvokedCallback mIOnBackInvokedCallback; + + private BackAnimationController mController; + + private int mEventTime = 0; + private TestableContentResolver mContentResolver; + private TestableLooper mTestableLooper; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + mContext.getApplicationInfo().privateFlags |= ApplicationInfo.PRIVATE_FLAG_PRIVILEGED; + mContentResolver = new TestableContentResolver(mContext); + mContentResolver.addProvider(Settings.AUTHORITY, new FakeSettingsProvider()); + Settings.Global.putString(mContentResolver, Settings.Global.ENABLE_BACK_ANIMATION, + ANIMATION_ENABLED); + mTestableLooper = TestableLooper.get(this); + mController = new BackAnimationController( + mShellExecutor, new Handler(mTestableLooper.getLooper()), mTransaction, + mActivityTaskManager, mContext, + mContentResolver); + mEventTime = 0; + 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(BackNavigationInfo.Builder builder) { + try { + doReturn(builder.build()).when(mActivityTaskManager).startBackNavigation(anyBoolean()); + } catch (RemoteException ex) { + ex.rethrowFromSystemServer(); + } + } + + RemoteAnimationTarget createAnimationTarget() { + SurfaceControl topWindowLeash = new SurfaceControl(); + return new RemoteAnimationTarget(-1, RemoteAnimationTarget.MODE_CLOSING, topWindowLeash, + false, new Rect(), new Rect(), -1, + new Point(0, 0), new Rect(), new Rect(), new WindowConfiguration(), + true, null, null, null, false, -1); + } + + private void triggerBackGesture() { + doMotionEvent(MotionEvent.ACTION_DOWN, 0); + doMotionEvent(MotionEvent.ACTION_MOVE, 0); + mController.setTriggerBack(true); + doMotionEvent(MotionEvent.ACTION_UP, 0); + } + + @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(); + } + + @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(); + } + + @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]); + } + + @Test + public void backToHome_dispatchesEvents() throws RemoteException { + mController.setBackToLauncherCallback(mIOnBackInvokedCallback); + RemoteAnimationTarget animationTarget = createAnimationTarget(); + createNavigationInfo(animationTarget, null, null, + BackNavigationInfo.TYPE_RETURN_TO_HOME, null); + + doMotionEvent(MotionEvent.ACTION_DOWN, 0); + + // Check that back start and progress is dispatched when first move. + doMotionEvent(MotionEvent.ACTION_MOVE, 100); + verify(mIOnBackInvokedCallback).onBackStarted(); + ArgumentCaptor<BackEvent> backEventCaptor = ArgumentCaptor.forClass(BackEvent.class); + verify(mIOnBackInvokedCallback).onBackProgressed(backEventCaptor.capture()); + assertEquals(animationTarget, backEventCaptor.getValue().getDepartingAnimationTarget()); + + // Check that back invocation is dispatched. + mController.setTriggerBack(true); // Fake trigger back + doMotionEvent(MotionEvent.ACTION_UP, 0); + verify(mIOnBackInvokedCallback).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, + mActivityTaskManager, mContext, + mContentResolver); + mController.setBackToLauncherCallback(mIOnBackInvokedCallback); + + RemoteAnimationTarget animationTarget = createAnimationTarget(); + IOnBackInvokedCallback appCallback = mock(IOnBackInvokedCallback.class); + ArgumentCaptor<BackEvent> backEventCaptor = ArgumentCaptor.forClass(BackEvent.class); + createNavigationInfo(animationTarget, null, null, + BackNavigationInfo.TYPE_RETURN_TO_HOME, appCallback); + + triggerBackGesture(); + + verify(appCallback, never()).onBackStarted(); + verify(appCallback, never()).onBackProgressed(backEventCaptor.capture()); + verify(appCallback, times(1)).onBackInvoked(); + + verify(mIOnBackInvokedCallback, never()).onBackStarted(); + verify(mIOnBackInvokedCallback, never()).onBackProgressed(backEventCaptor.capture()); + verify(mIOnBackInvokedCallback, never()).onBackInvoked(); + } + + @Test + public void ignoresGesture_transitionInProgress() throws RemoteException { + mController.setBackToLauncherCallback(mIOnBackInvokedCallback); + RemoteAnimationTarget animationTarget = createAnimationTarget(); + createNavigationInfo(animationTarget, null, null, + BackNavigationInfo.TYPE_RETURN_TO_HOME, null); + + triggerBackGesture(); + // Check that back invocation is dispatched. + verify(mIOnBackInvokedCallback).onBackInvoked(); + + 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); + + // 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(); + } + + @Test + public void acceptsGesture_transitionTimeout() throws RemoteException { + mController.setBackToLauncherCallback(mIOnBackInvokedCallback); + RemoteAnimationTarget animationTarget = createAnimationTarget(); + createNavigationInfo(animationTarget, null, null, + BackNavigationInfo.TYPE_RETURN_TO_HOME, null); + + triggerBackGesture(); + reset(mIOnBackInvokedCallback); + + // Simulate transition timeout. + mShellExecutor.flushAll(); + doMotionEvent(MotionEvent.ACTION_DOWN, 0); + doMotionEvent(MotionEvent.ACTION_MOVE, 100); + verify(mIOnBackInvokedCallback).onBackStarted(); + } + + private void doMotionEvent(int actionDown, int coordinate) { + mController.onMotionEvent( + coordinate, coordinate, + actionDown, + BackEvent.EDGE_LEFT); + mEventTime += 10; + } +} 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 8bc1223cfd64..e6711aca19c1 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 @@ -32,6 +32,7 @@ import static org.mockito.Mockito.when; import android.app.Notification; import android.app.PendingIntent; +import android.content.LocusId; import android.graphics.drawable.Icon; import android.os.Bundle; import android.os.UserHandle; @@ -39,7 +40,6 @@ import android.service.notification.NotificationListenerService; import android.service.notification.StatusBarNotification; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; -import android.util.Log; import android.util.Pair; import android.view.WindowManager; @@ -82,6 +82,7 @@ public class BubbleDataTest extends ShellTestCase { private BubbleEntry mEntryC1; private BubbleEntry mEntryInterruptive; private BubbleEntry mEntryDismissed; + private BubbleEntry mEntryLocusId; private Bubble mBubbleA1; private Bubble mBubbleA2; @@ -92,6 +93,7 @@ public class BubbleDataTest extends ShellTestCase { private Bubble mBubbleC1; private Bubble mBubbleInterruptive; private Bubble mBubbleDismissed; + private Bubble mBubbleLocusId; private BubbleData mBubbleData; private TestableBubblePositioner mPositioner; @@ -113,7 +115,7 @@ public class BubbleDataTest extends ShellTestCase { private ArgumentCaptor<BubbleData.Update> mUpdateCaptor; @Mock - private Bubbles.SuppressionChangedListener mSuppressionListener; + private Bubbles.BubbleMetadataFlagListener mBubbleMetadataFlagListener; @Mock private Bubbles.PendingIntentCanceledListener mPendingIntentCanceledListener; @@ -127,33 +129,54 @@ public class BubbleDataTest extends ShellTestCase { mEntryA3 = createBubbleEntry(1, "a3", "package.a", null); mEntryB1 = createBubbleEntry(1, "b1", "package.b", null); mEntryB2 = createBubbleEntry(1, "b2", "package.b", null); - mEntryB3 = createBubbleEntry(1, "b3", "package.b", null); - mEntryC1 = createBubbleEntry(1, "c1", "package.c", null); + mEntryB3 = createBubbleEntry(11, "b3", "package.b", null); + mEntryC1 = createBubbleEntry(11, "c1", "package.c", null); NotificationListenerService.Ranking ranking = mock(NotificationListenerService.Ranking.class); when(ranking.isTextChanged()).thenReturn(true); mEntryInterruptive = createBubbleEntry(1, "interruptive", "package.d", ranking); - mBubbleInterruptive = new Bubble(mEntryInterruptive, mSuppressionListener, null, + mBubbleInterruptive = new Bubble(mEntryInterruptive, mBubbleMetadataFlagListener, null, mMainExecutor); mEntryDismissed = createBubbleEntry(1, "dismissed", "package.d", null); - mBubbleDismissed = new Bubble(mEntryDismissed, mSuppressionListener, null, + mBubbleDismissed = new Bubble(mEntryDismissed, mBubbleMetadataFlagListener, null, mMainExecutor); - mBubbleA1 = new Bubble(mEntryA1, mSuppressionListener, mPendingIntentCanceledListener, + mEntryLocusId = createBubbleEntry(1, "keyLocus", "package.e", null, + new LocusId("locusId1")); + mBubbleLocusId = new Bubble(mEntryLocusId, + mBubbleMetadataFlagListener, + null /* pendingIntentCanceledListener */, mMainExecutor); - mBubbleA2 = new Bubble(mEntryA2, mSuppressionListener, mPendingIntentCanceledListener, + + mBubbleA1 = new Bubble(mEntryA1, + mBubbleMetadataFlagListener, + mPendingIntentCanceledListener, + mMainExecutor); + mBubbleA2 = new Bubble(mEntryA2, + mBubbleMetadataFlagListener, + mPendingIntentCanceledListener, mMainExecutor); - mBubbleA3 = new Bubble(mEntryA3, mSuppressionListener, mPendingIntentCanceledListener, + mBubbleA3 = new Bubble(mEntryA3, + mBubbleMetadataFlagListener, + mPendingIntentCanceledListener, mMainExecutor); - mBubbleB1 = new Bubble(mEntryB1, mSuppressionListener, mPendingIntentCanceledListener, + mBubbleB1 = new Bubble(mEntryB1, + mBubbleMetadataFlagListener, + mPendingIntentCanceledListener, mMainExecutor); - mBubbleB2 = new Bubble(mEntryB2, mSuppressionListener, mPendingIntentCanceledListener, + mBubbleB2 = new Bubble(mEntryB2, + mBubbleMetadataFlagListener, + mPendingIntentCanceledListener, mMainExecutor); - mBubbleB3 = new Bubble(mEntryB3, mSuppressionListener, mPendingIntentCanceledListener, + mBubbleB3 = new Bubble(mEntryB3, + mBubbleMetadataFlagListener, + mPendingIntentCanceledListener, mMainExecutor); - mBubbleC1 = new Bubble(mEntryC1, mSuppressionListener, mPendingIntentCanceledListener, + mBubbleC1 = new Bubble(mEntryC1, + mBubbleMetadataFlagListener, + mPendingIntentCanceledListener, mMainExecutor); mPositioner = new TestableBubblePositioner(mContext, mock(WindowManager.class)); @@ -794,7 +817,7 @@ public class BubbleDataTest extends ShellTestCase { } @Test - public void test_expanded_removeLastBubble_showsOverflowIfNotEmpty() { + public void test_expanded_removeLastBubble_collapsesIfOverflowNotEmpty() { // Setup sendUpdatedEntryAtTime(mEntryA1, 1000); changeExpandedStateAtTime(true, 2000); @@ -804,7 +827,7 @@ public class BubbleDataTest extends ShellTestCase { mBubbleData.dismissBubbleWithKey(mEntryA1.getKey(), Bubbles.DISMISS_USER_GESTURE); verifyUpdateReceived(); assertThat(mBubbleData.getOverflowBubbles().size()).isGreaterThan(0); - assertSelectionChangedTo(mBubbleData.getOverflow()); + assertExpandedChangedTo(false); } @Test @@ -939,6 +962,133 @@ public class BubbleDataTest extends ShellTestCase { assertOrderChangedTo(mBubbleB3, mBubbleB2, mBubbleB1, mBubbleA3, mBubbleA2); } + /** + * There is one bubble in the stack. If a task matching the locusId becomes visible, suppress + * the bubble. If it is hidden, unsuppress the bubble. + */ + @Test + public void test_onLocusVisibilityChanged_singleBubble() { + sendUpdatedEntryAtTime(mEntryLocusId, 1000); + mBubbleData.setListener(mListener); + + // Suppress the bubble + mBubbleData.onLocusVisibilityChanged(100, mEntryLocusId.getLocusId(), true /* visible */); + verifyUpdateReceived(); + assertBubbleSuppressed(mBubbleLocusId); + assertOrderNotChanged(); + assertBubbleListContains(/* empty list */); + + // Unsuppress the bubble + mBubbleData.onLocusVisibilityChanged(100, mEntryLocusId.getLocusId(), false /* visible */); + verifyUpdateReceived(); + assertBubbleUnsuppressed(mBubbleLocusId); + assertOrderNotChanged(); + assertBubbleListContains(mBubbleLocusId); + } + + /** + * Bubble stack has multiple bubbles. Suppress bubble based on matching locusId. Suppressed + * bubble is at the top. + * + * When suppressed: + * - hide bubble + * - update order + * - update selection + * + * When unsuppressed: + * - show bubble + * - update order + * - update selection + */ + @Test + public void test_onLocusVisibilityChanged_multipleBubbles_suppressTopBubble() { + sendUpdatedEntryAtTime(mEntryA1, 1000); + sendUpdatedEntryAtTime(mEntryA2, 2000); + sendUpdatedEntryAtTime(mEntryLocusId, 3000); + mBubbleData.setListener(mListener); + + // Suppress bubble + mBubbleData.onLocusVisibilityChanged(100, mEntryLocusId.getLocusId(), true /* visible */); + verifyUpdateReceived(); + assertBubbleSuppressed(mBubbleLocusId); + assertSelectionChangedTo(mBubbleA2); + assertOrderChangedTo(mBubbleA2, mBubbleA1); + + // Unsuppress bubble + mBubbleData.onLocusVisibilityChanged(100, mEntryLocusId.getLocusId(), false /* visible */); + verifyUpdateReceived(); + assertBubbleUnsuppressed(mBubbleLocusId); + assertSelectionChangedTo(mBubbleLocusId); + assertOrderChangedTo(mBubbleLocusId, mBubbleA2, mBubbleA1); + } + + /** + * Bubble stack has multiple bubbles. Suppress bubble based on matching locusId. Suppressed + * bubble is not at the top. + * + * When suppressed: + * - hide suppressed bubble + * - do not update order + * - do not update selection + * + * When unsuppressed: + * - show bubble + * - do not update order + * - do not update selection + */ + @Test + public void test_onLocusVisibilityChanged_multipleBubbles_suppressStackedBubble() { + sendUpdatedEntryAtTime(mEntryLocusId, 1000); + sendUpdatedEntryAtTime(mEntryA1, 2000); + sendUpdatedEntryAtTime(mEntryA2, 3000); + mBubbleData.setListener(mListener); + + // Suppress bubble + mBubbleData.onLocusVisibilityChanged(100, mEntryLocusId.getLocusId(), true /* visible */); + verifyUpdateReceived(); + assertBubbleSuppressed(mBubbleLocusId); + assertSelectionNotChanged(); + assertBubbleListContains(mBubbleA2, mBubbleA1); + + // Unsuppress bubble + mBubbleData.onLocusVisibilityChanged(100, mEntryLocusId.getLocusId(), false /* visible */); + verifyUpdateReceived(); + assertBubbleUnsuppressed(mBubbleLocusId); + assertSelectionNotChanged(); + assertBubbleListContains(mBubbleA2, mBubbleA1, mBubbleLocusId); + } + + @Test + public void test_removeBubblesForUser() { + // A is user 1 + sendUpdatedEntryAtTime(mEntryA1, 2000); + sendUpdatedEntryAtTime(mEntryA2, 3000); + // B & C belong to user 11 + sendUpdatedEntryAtTime(mEntryB3, 4000); + sendUpdatedEntryAtTime(mEntryC1, 5000); + mBubbleData.setListener(mListener); + + mBubbleData.dismissBubbleWithKey(mEntryA1.getKey(), Bubbles.DISMISS_USER_GESTURE); + verifyUpdateReceived(); + assertOverflowChangedTo(ImmutableList.of(mBubbleA1)); + assertBubbleListContains(mBubbleC1, mBubbleB3, mBubbleA2); + + // Remove all the A bubbles + mBubbleData.removeBubblesForUser(1); + verifyUpdateReceived(); + + // Verify the update has the removals. + BubbleData.Update update = mUpdateCaptor.getValue(); + assertThat(update.removedBubbles.get(0)).isEqualTo( + Pair.create(mBubbleA2, Bubbles.DISMISS_USER_REMOVED)); + assertThat(update.removedBubbles.get(1)).isEqualTo( + Pair.create(mBubbleA1, Bubbles.DISMISS_USER_REMOVED)); + + // Verify no A bubbles in active or overflow. + assertBubbleListContains(mBubbleC1, mBubbleB3); + assertOverflowChangedTo(ImmutableList.of()); + } + private void verifyUpdateReceived() { verify(mListener).applyUpdate(mUpdateCaptor.capture()); reset(mListener); @@ -995,9 +1145,29 @@ public class BubbleDataTest extends ShellTestCase { assertThat(update.overflowBubbles).isEqualTo(bubbles); } + private void assertBubbleListContains(Bubble... bubbles) { + BubbleData.Update update = mUpdateCaptor.getValue(); + assertWithMessage("bubbleList").that(update.bubbles).containsExactlyElementsIn(bubbles); + } + + private void assertBubbleSuppressed(Bubble expected) { + BubbleData.Update update = mUpdateCaptor.getValue(); + assertWithMessage("suppressedBubble").that(update.suppressedBubble).isEqualTo(expected); + } + + private void assertBubbleUnsuppressed(Bubble expected) { + BubbleData.Update update = mUpdateCaptor.getValue(); + assertWithMessage("unsuppressedBubble").that(update.unsuppressedBubble).isEqualTo(expected); + } + private BubbleEntry createBubbleEntry(int userId, String notifKey, String packageName, NotificationListenerService.Ranking ranking) { - return createBubbleEntry(userId, notifKey, packageName, ranking, 1000); + return createBubbleEntry(userId, notifKey, packageName, ranking, 1000, null); + } + + private BubbleEntry createBubbleEntry(int userId, String notifKey, String packageName, + NotificationListenerService.Ranking ranking, LocusId locusId) { + return createBubbleEntry(userId, notifKey, packageName, ranking, 1000, locusId); } private void setPostTime(BubbleEntry entry, long postTime) { @@ -1010,15 +1180,18 @@ public class BubbleDataTest extends ShellTestCase { * as a convenience to create a Notification w/BubbleMetadata. */ private BubbleEntry createBubbleEntry(int userId, String notifKey, String packageName, - NotificationListenerService.Ranking ranking, long postTime) { + NotificationListenerService.Ranking ranking, long postTime, + LocusId locusId) { // BubbleMetadata Notification.BubbleMetadata bubbleMetadata = new Notification.BubbleMetadata.Builder( mExpandIntent, Icon.createWithResource("", 0)) .setDeleteIntent(mDeleteIntent) + .setSuppressableBubble(true) .build(); // Notification -> BubbleMetadata Notification notification = mock(Notification.class); - notification.setBubbleMetadata(bubbleMetadata); + when(notification.getBubbleMetadata()).thenReturn(bubbleMetadata); + when(notification.getLocusId()).thenReturn(locusId); // Notification -> extras notification.extras = new Bundle(); 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 819a984b4a77..e8f3f69ca64e 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 @@ -63,7 +63,7 @@ public class BubbleTest extends ShellTestCase { private Bubble mBubble; @Mock - private Bubbles.SuppressionChangedListener mSuppressionListener; + private Bubbles.BubbleMetadataFlagListener mBubbleMetadataFlagListener; @Before public void setUp() { @@ -81,7 +81,7 @@ public class BubbleTest extends ShellTestCase { when(mNotif.getBubbleMetadata()).thenReturn(metadata); when(mSbn.getKey()).thenReturn("mock"); mBubbleEntry = new BubbleEntry(mSbn, null, true, false, false, false); - mBubble = new Bubble(mBubbleEntry, mSuppressionListener, null, mMainExecutor); + mBubble = new Bubble(mBubbleEntry, mBubbleMetadataFlagListener, null, mMainExecutor); } @Test @@ -144,22 +144,22 @@ public class BubbleTest extends ShellTestCase { } @Test - public void testSuppressionListener_change_notified() { + public void testBubbleMetadataFlagListener_change_notified() { assertThat(mBubble.showInShade()).isTrue(); mBubble.setSuppressNotification(true); assertThat(mBubble.showInShade()).isFalse(); - verify(mSuppressionListener).onBubbleNotificationSuppressionChange(mBubble); + verify(mBubbleMetadataFlagListener).onBubbleMetadataFlagChanged(mBubble); } @Test - public void testSuppressionListener_noChange_doesntNotify() { + public void testBubbleMetadataFlagListener_noChange_doesntNotify() { assertThat(mBubble.showInShade()).isTrue(); mBubble.setSuppressNotification(false); - verify(mSuppressionListener, never()).onBubbleNotificationSuppressionChange(any()); + verify(mBubbleMetadataFlagListener, never()).onBubbleMetadataFlagChanged(any()); } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/storage/BubbleVolatileRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/storage/BubbleVolatileRepositoryTest.kt index bfdf5208bbf0..9f0d89bc3128 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/storage/BubbleVolatileRepositoryTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/storage/BubbleVolatileRepositoryTest.kt @@ -23,14 +23,19 @@ import android.testing.AndroidTestingRunner import androidx.test.filters.SmallTest import org.junit.Test import com.android.wm.shell.ShellTestCase +import com.google.common.truth.Truth.assertThat import junit.framework.Assert.assertEquals import org.junit.Before import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.any +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.ArgumentMatchers.anyString import org.mockito.ArgumentMatchers.eq import org.mockito.Mockito import org.mockito.Mockito.mock -import org.mockito.Mockito.verify +import org.mockito.Mockito.never import org.mockito.Mockito.reset +import org.mockito.Mockito.verify @SmallTest @RunWith(AndroidTestingRunner::class) @@ -41,17 +46,17 @@ class BubbleVolatileRepositoryTest : ShellTestCase() { private val user11 = UserHandle.of(11) // user, package, shortcut, notification key, height, res-height, title, taskId, locusId - private val bubble1 = BubbleEntity(0, "com.example.messenger", "shortcut-1", - "0key-1", 120, 0, null, 1, null) - private val bubble2 = BubbleEntity(10, "com.example.chat", "alice and bob", - "10key-2", 0, 16537428, "title", 2, null) - private val bubble3 = BubbleEntity(0, "com.example.messenger", "shortcut-2", - "0key-3", 120, 0, null, INVALID_TASK_ID, null) - - private val bubble11 = BubbleEntity(11, "com.example.messenger", - "shortcut-1", "01key-1", 120, 0, null, 3) - private val bubble12 = BubbleEntity(11, "com.example.chat", "alice and bob", - "11key-2", 0, 16537428, "title", INVALID_TASK_ID) + private val bubble1 = BubbleEntity(user0.identifier, + "com.example.messenger", "shortcut-1", "0key-1", 120, 0, null, 1, null) + private val bubble2 = BubbleEntity(user10_managed.identifier, + "com.example.chat", "alice and bob", "10key-2", 0, 16537428, "title", 2, null) + private val bubble3 = BubbleEntity(user0.identifier, + "com.example.messenger", "shortcut-2", "0key-3", 120, 0, null, INVALID_TASK_ID, null) + + private val bubble11 = BubbleEntity(user11.identifier, + "com.example.messenger", "shortcut-1", "01key-1", 120, 0, null, 3) + private val bubble12 = BubbleEntity(user11.identifier, + "com.example.chat", "alice and bob", "11key-2", 0, 16537428, "title", INVALID_TASK_ID) private val user0bubbles = listOf(bubble1, bubble2, bubble3) private val user11bubbles = listOf(bubble11, bubble12) @@ -151,6 +156,125 @@ class BubbleVolatileRepositoryTest : ShellTestCase() { repository.addBubbles(user0.identifier, listOf(bubbleModified)) assertEquals(bubbleModified, repository.getEntities(user0.identifier).get(0)) } + + @Test + fun testRemoveBubblesForUser() { + repository.addBubbles(user0.identifier, user0bubbles) + assertThat(repository.getEntities(user0.identifier).toList()) + .isEqualTo(listOf(bubble1, bubble2, bubble3)) + + val ret = repository.removeBubblesForUser(user0.identifier, -1) + assertThat(ret).isTrue() // bubbles were removed + + assertThat(repository.getEntities(user0.identifier).toList()).isEmpty() + verify(launcherApps, never()).uncacheShortcuts(anyString(), + any(), + any(UserHandle::class.java), anyInt()) + } + + @Test + fun testRemoveBubblesForUser_parentUserRemoved() { + repository.addBubbles(user0.identifier, user0bubbles) + // bubble2 is the work profile bubble + assertThat(repository.getEntities(user0.identifier).toList()) + .isEqualTo(listOf(bubble1, bubble2, bubble3)) + + val ret = repository.removeBubblesForUser(user10_managed.identifier, user0.identifier) + assertThat(ret).isTrue() // bubbles were removed + + assertThat(repository.getEntities(user0.identifier).toList()) + .isEqualTo(listOf(bubble1, bubble3)) + verify(launcherApps, never()).uncacheShortcuts(anyString(), + any(), + any(UserHandle::class.java), anyInt()) + } + + @Test + fun testRemoveBubblesForUser_withoutBubbles() { + repository.addBubbles(user0.identifier, user0bubbles) + assertThat(repository.getEntities(user0.identifier).toList()) + .isEqualTo(listOf(bubble1, bubble2, bubble3)) + + val ret = repository.removeBubblesForUser(user11.identifier, -1) + assertThat(ret).isFalse() // bubbles were NOT removed + + assertThat(repository.getEntities(user0.identifier).toList()) + .isEqualTo(listOf(bubble1, bubble2, bubble3)) + verify(launcherApps, never()).uncacheShortcuts(anyString(), + any(), + any(UserHandle::class.java), anyInt()) + } + + @Test + fun testSanitizeBubbles_noChanges() { + repository.addBubbles(user0.identifier, user0bubbles) + assertThat(repository.getEntities(user0.identifier).toList()) + .isEqualTo(listOf(bubble1, bubble2, bubble3)) + repository.addBubbles(user11.identifier, user11bubbles) + assertThat(repository.getEntities(user11.identifier).toList()) + .isEqualTo(listOf(bubble11, bubble12)) + + val ret = repository.sanitizeBubbles(listOf(user0.identifier, + user10_managed.identifier, + user11.identifier)) + assertThat(ret).isFalse() // bubbles were NOT removed + + verify(launcherApps, never()).uncacheShortcuts(anyString(), + any(), + any(UserHandle::class.java), anyInt()) + } + + @Test + fun testSanitizeBubbles_userRemoved() { + repository.addBubbles(user0.identifier, user0bubbles) + assertThat(repository.getEntities(user0.identifier).toList()) + .isEqualTo(listOf(bubble1, bubble2, bubble3)) + repository.addBubbles(user11.identifier, user11bubbles) + assertThat(repository.getEntities(user11.identifier).toList()) + .isEqualTo(listOf(bubble11, bubble12)) + + val ret = repository.sanitizeBubbles(listOf(user11.identifier)) + assertThat(ret).isTrue() // bubbles were removed + + assertThat(repository.getEntities(user0.identifier).toList()).isEmpty() + verify(launcherApps, never()).uncacheShortcuts(anyString(), + any(), + any(UserHandle::class.java), anyInt()) + + // User 11 bubbles should still be here + assertThat(repository.getEntities(user11.identifier).toList()) + .isEqualTo(listOf(bubble11, bubble12)) + } + + @Test + fun testSanitizeBubbles_userParentRemoved() { + repository.addBubbles(user0.identifier, user0bubbles) + assertThat(repository.getEntities(user0.identifier).toList()) + .isEqualTo(listOf(bubble1, bubble2, bubble3)) + + repository.addBubbles(user11.identifier, user11bubbles) + assertThat(repository.getEntities(user11.identifier).toList()) + .isEqualTo(listOf(bubble11, bubble12)) + + val ret = repository.sanitizeBubbles(listOf(user0.identifier, user11.identifier)) + assertThat(ret).isTrue() // bubbles were removed + // bubble2 is the work profile bubble and should be removed + assertThat(repository.getEntities(user0.identifier).toList()) + .isEqualTo(listOf(bubble1, bubble3)) + verify(launcherApps, never()).uncacheShortcuts(anyString(), + any(), + any(UserHandle::class.java), anyInt()) + + // User 11 bubbles should still be here + assertThat(repository.getEntities(user11.identifier).toList()) + .isEqualTo(listOf(bubble11, bubble12)) + } + + @Test + fun testRemoveBubbleForUser_invalidInputDoesntCrash() { + repository.removeBubblesForUser(-1, 0) + repository.removeBubblesForUser(-1, -1) + } } private const val PKG_MESSENGER = "com.example.messenger" 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 b66c2b4aee9b..3bf06cc0ede3 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayInsetsControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayInsetsControllerTest.java @@ -19,11 +19,8 @@ package com.android.wm.shell.common; import static android.view.Display.DEFAULT_DISPLAY; import static org.junit.Assert.assertTrue; -import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.notNull; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -33,6 +30,7 @@ import android.view.IDisplayWindowInsetsController; import android.view.IWindowManager; import android.view.InsetsSourceControl; import android.view.InsetsState; +import android.view.InsetsVisibilities; import androidx.test.filters.SmallTest; @@ -42,7 +40,6 @@ import org.junit.Before; import org.junit.Test; import org.mockito.ArgumentCaptor; import org.mockito.Mock; -import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import java.util.List; @@ -99,7 +96,8 @@ public class DisplayInsetsControllerTest { mController.addInsetsChangedListener(DEFAULT_DISPLAY, defaultListener); mController.addInsetsChangedListener(SECOND_DISPLAY, secondListener); - mInsetsControllersByDisplayId.get(DEFAULT_DISPLAY).topFocusedWindowChanged(null); + mInsetsControllersByDisplayId.get(DEFAULT_DISPLAY).topFocusedWindowChanged(null, + new InsetsVisibilities()); mInsetsControllersByDisplayId.get(DEFAULT_DISPLAY).insetsChanged(null); mInsetsControllersByDisplayId.get(DEFAULT_DISPLAY).insetsControlChanged(null, null); mInsetsControllersByDisplayId.get(DEFAULT_DISPLAY).showInsets(0, false); @@ -118,7 +116,8 @@ public class DisplayInsetsControllerTest { assertTrue(secondListener.showInsetsCount == 0); assertTrue(secondListener.hideInsetsCount == 0); - mInsetsControllersByDisplayId.get(SECOND_DISPLAY).topFocusedWindowChanged(null); + mInsetsControllersByDisplayId.get(SECOND_DISPLAY).topFocusedWindowChanged(null, + new InsetsVisibilities()); mInsetsControllersByDisplayId.get(SECOND_DISPLAY).insetsChanged(null); mInsetsControllersByDisplayId.get(SECOND_DISPLAY).insetsControlChanged(null, null); mInsetsControllersByDisplayId.get(SECOND_DISPLAY).showInsets(0, false); @@ -165,7 +164,8 @@ public class DisplayInsetsControllerTest { int hideInsetsCount = 0; @Override - public void topFocusedWindowChanged(String packageName) { + public void topFocusedWindowChanged(String packageName, + InsetsVisibilities requestedVisibilities) { topFocusedWindowChangedCount++; } 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 d8aebc284bf1..96938ebc27df 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 @@ -109,9 +109,10 @@ public class TaskStackListenerImplTest { @Test public void testOnTaskProfileLocked() { - mImpl.onTaskProfileLocked(1, 2); - verify(mCallback).onTaskProfileLocked(eq(1), eq(2)); - verify(mOtherCallback).onTaskProfileLocked(eq(1), eq(2)); + ActivityManager.RunningTaskInfo info = mock(ActivityManager.RunningTaskInfo.class); + mImpl.onTaskProfileLocked(info); + verify(mCallback).onTaskProfileLocked(eq(info)); + verify(mOtherCallback).onTaskProfileLocked(eq(info)); } @Test diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java index 453050fcfab4..f1e602fcf778 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,7 +16,6 @@ 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; @@ -24,11 +23,14 @@ 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.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; +import android.app.ActivityManager; import android.content.res.Configuration; import android.graphics.Rect; +import android.window.WindowContainerTransaction; import androidx.test.annotation.UiThreadTest; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -37,6 +39,7 @@ import androidx.test.filters.SmallTest; import com.android.internal.policy.DividerSnapAlgorithm; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.ShellTestCase; +import com.android.wm.shell.TestRunningTaskInfoBuilder; import com.android.wm.shell.common.DisplayImeController; import org.junit.Before; @@ -55,6 +58,7 @@ public class SplitLayoutTests extends ShellTestCase { @Mock SplitWindowManager.ParentContainerCallbacks mCallbacks; @Mock DisplayImeController mDisplayImeController; @Mock ShellTaskOrganizer mTaskOrganizer; + @Mock WindowContainerTransaction mWct; @Captor ArgumentCaptor<Runnable> mRunnableCaptor; private SplitLayout mSplitLayout; @@ -69,7 +73,7 @@ public class SplitLayoutTests extends ShellTestCase { mCallbacks, mDisplayImeController, mTaskOrganizer, - false /* applyDismissingParallax */)); + SplitLayout.PARALLAX_NONE)); } @Test @@ -80,10 +84,6 @@ public class SplitLayoutTests extends ShellTestCase { // Verify it returns true if new config won't affect split layout. assertThat(mSplitLayout.updateConfiguration(config)).isFalse(); - // Verify updateConfiguration returns true if the orientation changed. - config.orientation = ORIENTATION_LANDSCAPE; - assertThat(mSplitLayout.updateConfiguration(config)).isTrue(); - // Verify updateConfiguration returns true if it rotated. config.windowConfiguration.setRotation(1); assertThat(mSplitLayout.updateConfiguration(config)).isTrue(); @@ -101,14 +101,21 @@ public class SplitLayoutTests extends ShellTestCase { @Test public void testSetDividePosition() { - mSplitLayout.setDividePosition(anyInt()); + mSplitLayout.setDividePosition(100, false /* applyLayoutChange */); + assertThat(mSplitLayout.getDividePosition()).isEqualTo(100); + verify(mSplitLayoutHandler, never()).onLayoutSizeChanged(any(SplitLayout.class)); + + mSplitLayout.setDividePosition(200, true /* applyLayoutChange */); + assertThat(mSplitLayout.getDividePosition()).isEqualTo(200); verify(mSplitLayoutHandler).onLayoutSizeChanged(any(SplitLayout.class)); } @Test public void testSetDivideRatio() { + mSplitLayout.setDividePosition(200, false /* applyLayoutChange */); mSplitLayout.setDivideRatio(0.5f); - verify(mSplitLayoutHandler).onLayoutSizeChanged(any(SplitLayout.class)); + assertThat(mSplitLayout.getDividePosition()).isEqualTo( + mSplitLayout.mDividerSnapAlgorithm.getMiddleTarget().position); } @Test @@ -141,6 +148,16 @@ public class SplitLayoutTests extends ShellTestCase { verify(mSplitLayoutHandler).onSnappedToDismiss(eq(true)); } + @Test + public void testApplyTaskChanges_updatesSmallestScreenWidthDp() { + final ActivityManager.RunningTaskInfo task1 = new TestRunningTaskInfoBuilder().build(); + final ActivityManager.RunningTaskInfo task2 = new TestRunningTaskInfoBuilder().build(); + mSplitLayout.applyTaskChanges(mWct, task1, task2); + + verify(mWct).setSmallestScreenWidthDp(eq(task1.token), anyInt()); + verify(mWct).setSmallestScreenWidthDp(eq(task2.token), anyInt()); + } + private void waitDividerFlingFinished() { verify(mSplitLayout).flingDividePosition(anyInt(), anyInt(), mRunnableCaptor.capture()); mRunnableCaptor.getValue().run(); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitWindowManagerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitWindowManagerTests.java index 9bb54a18063f..2e5078d86a8b 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitWindowManagerTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitWindowManagerTests.java @@ -61,7 +61,7 @@ public class SplitWindowManagerTests extends ShellTestCase { public void testInitRelease() { mSplitWindowManager.init(mSplitLayout, new InsetsState()); assertThat(mSplitWindowManager.getSurfaceControl()).isNotNull(); - mSplitWindowManager.release(); + mSplitWindowManager.release(null /* t */); assertThat(mSplitWindowManager.getSurfaceControl()).isNull(); } } 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 f622edb7f134..596100dcdead 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java @@ -16,11 +16,14 @@ 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 com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; 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.clearInvocations; @@ -29,6 +32,9 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import android.app.ActivityManager.RunningTaskInfo; +import android.app.TaskInfo; +import android.app.TaskInfo.CameraCompatControlState; import android.content.Context; import android.content.res.Configuration; import android.testing.AndroidTestingRunner; @@ -46,6 +52,8 @@ import com.android.wm.shell.common.DisplayInsetsController.OnInsetsChangedListen import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; +import com.android.wm.shell.compatui.letterboxedu.LetterboxEduWindowManager; +import com.android.wm.shell.transition.Transitions; import org.junit.Before; import org.junit.Test; @@ -55,6 +63,8 @@ import org.mockito.Captor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import dagger.Lazy; + /** * Tests for {@link CompatUIController}. * @@ -75,7 +85,9 @@ public class CompatUIControllerTest extends ShellTestCase { private @Mock ShellTaskOrganizer.TaskListener mMockTaskListener; private @Mock SyncTransactionQueue mMockSyncQueue; private @Mock ShellExecutor mMockExecutor; - private @Mock CompatUIWindowManager mMockLayout; + private @Mock Lazy<Transitions> mMockTransitionsLazy; + private @Mock CompatUIWindowManager mMockCompatLayout; + private @Mock LetterboxEduWindowManager mMockLetterboxEduLayout; @Captor ArgumentCaptor<OnInsetsChangedListener> mOnInsetsChangedListenerCaptor; @@ -85,14 +97,27 @@ public class CompatUIControllerTest extends ShellTestCase { MockitoAnnotations.initMocks(this); doReturn(mMockDisplayLayout).when(mMockDisplayController).getDisplayLayout(anyInt()); - doReturn(DISPLAY_ID).when(mMockLayout).getDisplayId(); - doReturn(TASK_ID).when(mMockLayout).getTaskId(); + doReturn(DISPLAY_ID).when(mMockCompatLayout).getDisplayId(); + doReturn(TASK_ID).when(mMockCompatLayout).getTaskId(); + doReturn(true).when(mMockCompatLayout).createLayout(anyBoolean()); + doReturn(true).when(mMockCompatLayout).updateCompatInfo(any(), any(), anyBoolean()); + doReturn(DISPLAY_ID).when(mMockLetterboxEduLayout).getDisplayId(); + 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) { + mMockDisplayInsetsController, mMockImeController, mMockSyncQueue, mMockExecutor, + mMockTransitionsLazy) { + @Override + CompatUIWindowManager createCompatUiWindowManager(Context context, TaskInfo taskInfo, + ShellTaskOrganizer.TaskListener taskListener) { + return mMockCompatLayout; + } + @Override - CompatUIWindowManager createLayout(Context context, int displayId, int taskId, - Configuration taskConfig, ShellTaskOrganizer.TaskListener taskListener) { - return mMockLayout; + LetterboxEduWindowManager createLetterboxEduWindowManager(Context context, + TaskInfo taskInfo, ShellTaskOrganizer.TaskListener taskListener) { + return mMockLetterboxEduLayout; } }; spyOn(mController); @@ -106,27 +131,95 @@ public class CompatUIControllerTest extends ShellTestCase { @Test public void testOnCompatInfoChanged() { - final Configuration taskConfig = new Configuration(); + TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true, + CAMERA_COMPAT_CONTROL_HIDDEN); + + // Verify that the compat controls are added with non-null task listener. + mController.onCompatInfoChanged(taskInfo, mMockTaskListener); + + verify(mController).createCompatUiWindowManager(any(), eq(taskInfo), eq(mMockTaskListener)); + verify(mController).createLetterboxEduWindowManager(any(), eq(taskInfo), + eq(mMockTaskListener)); + + // Verify that the compat controls and letterbox education are updated with new size compat + // info. + clearInvocations(mMockCompatLayout, mMockLetterboxEduLayout, mController); + taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true, + CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED); + mController.onCompatInfoChanged(taskInfo, mMockTaskListener); + + verify(mMockCompatLayout).updateCompatInfo(taskInfo, mMockTaskListener, /* canShow= */ + true); + verify(mMockLetterboxEduLayout).updateCompatInfo(taskInfo, mMockTaskListener, /* canShow= */ + true); + + // Verify that compat controls and letterbox education are removed with null task listener. + clearInvocations(mMockCompatLayout, mMockLetterboxEduLayout, mController); + mController.onCompatInfoChanged(createTaskInfo(DISPLAY_ID, TASK_ID, + /* hasSizeCompat= */ true, CAMERA_COMPAT_CONTROL_HIDDEN), + /* taskListener= */ null); + + verify(mMockCompatLayout).release(); + verify(mMockLetterboxEduLayout).release(); + } + + @Test + public void testOnCompatInfoChanged_createLayoutReturnsFalse() { + doReturn(false).when(mMockCompatLayout).createLayout(anyBoolean()); + doReturn(false).when(mMockLetterboxEduLayout).createLayout(anyBoolean()); + + TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true, + CAMERA_COMPAT_CONTROL_HIDDEN); + mController.onCompatInfoChanged(taskInfo, mMockTaskListener); + + verify(mController).createCompatUiWindowManager(any(), eq(taskInfo), eq(mMockTaskListener)); + verify(mController).createLetterboxEduWindowManager(any(), eq(taskInfo), + eq(mMockTaskListener)); + + // Verify that the layout is created again. + clearInvocations(mMockCompatLayout, mMockLetterboxEduLayout, mController); + mController.onCompatInfoChanged(taskInfo, mMockTaskListener); + + verify(mMockCompatLayout, never()).updateCompatInfo(any(), any(), anyBoolean()); + verify(mMockLetterboxEduLayout, never()).updateCompatInfo(any(), any(), anyBoolean()); + verify(mController).createCompatUiWindowManager(any(), eq(taskInfo), eq(mMockTaskListener)); + verify(mController).createLetterboxEduWindowManager(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()); - // Verify that the restart button is added with non-null size compat info. - mController.onCompatInfoChanged(DISPLAY_ID, TASK_ID, taskConfig, mMockTaskListener); + TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true, + CAMERA_COMPAT_CONTROL_HIDDEN); + mController.onCompatInfoChanged(taskInfo, mMockTaskListener); - verify(mController).createLayout(any(), eq(DISPLAY_ID), eq(TASK_ID), eq(taskConfig), + verify(mController).createCompatUiWindowManager(any(), eq(taskInfo), eq(mMockTaskListener)); + verify(mController).createLetterboxEduWindowManager(any(), eq(taskInfo), eq(mMockTaskListener)); - // Verify that the restart button is updated with non-null new size compat info. - final Configuration newTaskConfig = new Configuration(); - mController.onCompatInfoChanged(DISPLAY_ID, TASK_ID, newTaskConfig, mMockTaskListener); + clearInvocations(mMockCompatLayout, mMockLetterboxEduLayout, mController); + mController.onCompatInfoChanged(taskInfo, mMockTaskListener); - verify(mMockLayout).updateCompatInfo(taskConfig, mMockTaskListener, - true /* show */); + verify(mMockCompatLayout).updateCompatInfo(taskInfo, mMockTaskListener, /* canShow= */ + true); + verify(mMockLetterboxEduLayout).updateCompatInfo(taskInfo, mMockTaskListener, /* canShow= */ + true); - // Verify that the restart button is removed with null size compat info. - mController.onCompatInfoChanged(DISPLAY_ID, TASK_ID, null, mMockTaskListener); + // Verify that the layout is created again. + clearInvocations(mMockCompatLayout, mMockLetterboxEduLayout, mController); + mController.onCompatInfoChanged(taskInfo, mMockTaskListener); - verify(mMockLayout).release(); + verify(mMockCompatLayout, never()).updateCompatInfo(any(), any(), anyBoolean()); + verify(mMockLetterboxEduLayout, never()).updateCompatInfo(any(), any(), anyBoolean()); + verify(mController).createCompatUiWindowManager(any(), eq(taskInfo), eq(mMockTaskListener)); + verify(mController).createLetterboxEduWindowManager(any(), eq(taskInfo), + eq(mMockTaskListener)); } + @Test public void testOnDisplayAdded() { mController.onDisplayAdded(DISPLAY_ID); @@ -139,44 +232,45 @@ public class CompatUIControllerTest extends ShellTestCase { @Test public void testOnDisplayRemoved() { mController.onDisplayAdded(DISPLAY_ID); - final Configuration taskConfig = new Configuration(); - mController.onCompatInfoChanged(DISPLAY_ID, TASK_ID, taskConfig, + mController.onCompatInfoChanged(createTaskInfo(DISPLAY_ID, TASK_ID, + /* hasSizeCompat= */ true, CAMERA_COMPAT_CONTROL_HIDDEN), mMockTaskListener); mController.onDisplayRemoved(DISPLAY_ID + 1); - verify(mMockLayout, never()).release(); + verify(mMockCompatLayout, never()).release(); + verify(mMockLetterboxEduLayout, never()).release(); verify(mMockDisplayInsetsController, never()).removeInsetsChangedListener(eq(DISPLAY_ID), any()); mController.onDisplayRemoved(DISPLAY_ID); verify(mMockDisplayInsetsController).removeInsetsChangedListener(eq(DISPLAY_ID), any()); - verify(mMockLayout).release(); + verify(mMockCompatLayout).release(); + verify(mMockLetterboxEduLayout).release(); } @Test public void testOnDisplayConfigurationChanged() { - final Configuration taskConfig = new Configuration(); - mController.onCompatInfoChanged(DISPLAY_ID, TASK_ID, taskConfig, - mMockTaskListener); + mController.onCompatInfoChanged(createTaskInfo(DISPLAY_ID, TASK_ID, + /* hasSizeCompat= */ true, CAMERA_COMPAT_CONTROL_HIDDEN), mMockTaskListener); - final Configuration newTaskConfig = new Configuration(); - mController.onDisplayConfigurationChanged(DISPLAY_ID + 1, newTaskConfig); + mController.onDisplayConfigurationChanged(DISPLAY_ID + 1, new Configuration()); - verify(mMockLayout, never()).updateDisplayLayout(any()); + verify(mMockCompatLayout, never()).updateDisplayLayout(any()); + verify(mMockLetterboxEduLayout, never()).updateDisplayLayout(any()); - mController.onDisplayConfigurationChanged(DISPLAY_ID, newTaskConfig); + mController.onDisplayConfigurationChanged(DISPLAY_ID, new Configuration()); - verify(mMockLayout).updateDisplayLayout(mMockDisplayLayout); + verify(mMockCompatLayout).updateDisplayLayout(mMockDisplayLayout); + verify(mMockLetterboxEduLayout).updateDisplayLayout(mMockDisplayLayout); } @Test public void testInsetsChanged() { mController.onDisplayAdded(DISPLAY_ID); - final Configuration taskConfig = new Configuration(); - mController.onCompatInfoChanged(DISPLAY_ID, TASK_ID, taskConfig, - mMockTaskListener); + 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.setFrame(0, 0, 1000, 1000); @@ -186,101 +280,131 @@ public class CompatUIControllerTest extends ShellTestCase { mOnInsetsChangedListenerCaptor.capture()); mOnInsetsChangedListenerCaptor.getValue().insetsChanged(insetsState); - verify(mMockLayout).updateDisplayLayout(mMockDisplayLayout); + verify(mMockCompatLayout).updateDisplayLayout(mMockDisplayLayout); + verify(mMockLetterboxEduLayout).updateDisplayLayout(mMockDisplayLayout); // No update if the insets state is the same. - clearInvocations(mMockLayout); + clearInvocations(mMockCompatLayout, mMockLetterboxEduLayout); mOnInsetsChangedListenerCaptor.getValue().insetsChanged(new InsetsState(insetsState)); - verify(mMockLayout, never()).updateDisplayLayout(mMockDisplayLayout); + verify(mMockCompatLayout, never()).updateDisplayLayout(mMockDisplayLayout); + verify(mMockLetterboxEduLayout, never()).updateDisplayLayout(mMockDisplayLayout); } @Test - public void testChangeButtonVisibilityOnImeShowHide() { - final Configuration taskConfig = new Configuration(); - mController.onCompatInfoChanged(DISPLAY_ID, TASK_ID, taskConfig, mMockTaskListener); + public void testChangeLayoutsVisibilityOnImeShowHide() { + mController.onCompatInfoChanged(createTaskInfo(DISPLAY_ID, TASK_ID, + /* hasSizeCompat= */ true, CAMERA_COMPAT_CONTROL_HIDDEN), mMockTaskListener); // Verify that the restart button is hidden after IME is showing. - mController.onImeVisibilityChanged(DISPLAY_ID, true /* isShowing */); + mController.onImeVisibilityChanged(DISPLAY_ID, /* isShowing= */ true); - verify(mMockLayout).updateVisibility(false); + verify(mMockCompatLayout).updateVisibility(false); + verify(mMockLetterboxEduLayout).updateVisibility(false); // Verify button remains hidden while IME is showing. - mController.onCompatInfoChanged(DISPLAY_ID, TASK_ID, taskConfig, mMockTaskListener); + TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true, + CAMERA_COMPAT_CONTROL_HIDDEN); + mController.onCompatInfoChanged(taskInfo, mMockTaskListener); - verify(mMockLayout).updateCompatInfo(taskConfig, mMockTaskListener, - false /* show */); + verify(mMockCompatLayout).updateCompatInfo(taskInfo, mMockTaskListener, /* canShow= */ + false); + verify(mMockLetterboxEduLayout).updateCompatInfo(taskInfo, mMockTaskListener, /* canShow= */ + false); // Verify button is shown after IME is hidden. - mController.onImeVisibilityChanged(DISPLAY_ID, false /* isShowing */); + mController.onImeVisibilityChanged(DISPLAY_ID, /* isShowing= */ false); - verify(mMockLayout).updateVisibility(true); + verify(mMockCompatLayout).updateVisibility(true); + verify(mMockLetterboxEduLayout).updateVisibility(true); } @Test - public void testChangeButtonVisibilityOnKeyguardOccludedChanged() { - final Configuration taskConfig = new Configuration(); - mController.onCompatInfoChanged(DISPLAY_ID, TASK_ID, taskConfig, mMockTaskListener); + public void testChangeLayoutsVisibilityOnKeyguardShowingChanged() { + mController.onCompatInfoChanged(createTaskInfo(DISPLAY_ID, TASK_ID, + /* hasSizeCompat= */ true, CAMERA_COMPAT_CONTROL_HIDDEN), mMockTaskListener); - // Verify that the restart button is hidden after keyguard becomes occluded. - mController.onKeyguardOccludedChanged(true); + // Verify that the restart button is hidden after keyguard becomes showing. + mController.onKeyguardShowingChanged(true); - verify(mMockLayout).updateVisibility(false); + verify(mMockCompatLayout).updateVisibility(false); + verify(mMockLetterboxEduLayout).updateVisibility(false); - // Verify button remains hidden while keyguard is occluded. - mController.onCompatInfoChanged(DISPLAY_ID, TASK_ID, taskConfig, mMockTaskListener); + // 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(mMockLayout).updateCompatInfo(taskConfig, mMockTaskListener, - false /* show */); + verify(mMockCompatLayout).updateCompatInfo(taskInfo, mMockTaskListener, /* canShow= */ + false); + verify(mMockLetterboxEduLayout).updateCompatInfo(taskInfo, mMockTaskListener, /* canShow= */ + false); - // Verify button is shown after keyguard becomes not occluded. - mController.onKeyguardOccludedChanged(false); + // Verify button is shown after keyguard becomes not showing. + mController.onKeyguardShowingChanged(false); - verify(mMockLayout).updateVisibility(true); + verify(mMockCompatLayout).updateVisibility(true); + verify(mMockLetterboxEduLayout).updateVisibility(true); } @Test - public void testButtonRemainsHiddenOnKeyguardOccludedFalseWhenImeIsShowing() { - final Configuration taskConfig = new Configuration(); - mController.onCompatInfoChanged(DISPLAY_ID, TASK_ID, taskConfig, mMockTaskListener); + public void testLayoutsRemainHiddenOnKeyguardShowingFalseWhenImeIsShowing() { + mController.onCompatInfoChanged(createTaskInfo(DISPLAY_ID, TASK_ID, + /* hasSizeCompat= */ true, CAMERA_COMPAT_CONTROL_HIDDEN), mMockTaskListener); - mController.onImeVisibilityChanged(DISPLAY_ID, true /* isShowing */); - mController.onKeyguardOccludedChanged(true); + mController.onImeVisibilityChanged(DISPLAY_ID, /* isShowing= */ true); + mController.onKeyguardShowingChanged(true); - verify(mMockLayout, times(2)).updateVisibility(false); + verify(mMockCompatLayout, times(2)).updateVisibility(false); + verify(mMockLetterboxEduLayout, times(2)).updateVisibility(false); - clearInvocations(mMockLayout); + clearInvocations(mMockCompatLayout, mMockLetterboxEduLayout); - // Verify button remains hidden after keyguard becomes not occluded since IME is showing. - mController.onKeyguardOccludedChanged(false); + // Verify button remains hidden after keyguard becomes not showing since IME is showing. + mController.onKeyguardShowingChanged(false); - verify(mMockLayout).updateVisibility(false); + verify(mMockCompatLayout).updateVisibility(false); + verify(mMockLetterboxEduLayout).updateVisibility(false); // Verify button is shown after IME is not showing. - mController.onImeVisibilityChanged(DISPLAY_ID, false /* isShowing */); + mController.onImeVisibilityChanged(DISPLAY_ID, /* isShowing= */ false); - verify(mMockLayout).updateVisibility(true); + verify(mMockCompatLayout).updateVisibility(true); + verify(mMockLetterboxEduLayout).updateVisibility(true); } @Test - public void testButtonRemainsHiddenOnImeHideWhenKeyguardIsOccluded() { - final Configuration taskConfig = new Configuration(); - mController.onCompatInfoChanged(DISPLAY_ID, TASK_ID, taskConfig, mMockTaskListener); + public void testLayoutsRemainHiddenOnImeHideWhenKeyguardIsShowing() { + mController.onCompatInfoChanged(createTaskInfo(DISPLAY_ID, TASK_ID, + /* hasSizeCompat= */ true, CAMERA_COMPAT_CONTROL_HIDDEN), mMockTaskListener); + + mController.onImeVisibilityChanged(DISPLAY_ID, /* isShowing= */ true); + mController.onKeyguardShowingChanged(true); - mController.onImeVisibilityChanged(DISPLAY_ID, true /* isShowing */); - mController.onKeyguardOccludedChanged(true); + verify(mMockCompatLayout, times(2)).updateVisibility(false); + verify(mMockLetterboxEduLayout, times(2)).updateVisibility(false); - verify(mMockLayout, times(2)).updateVisibility(false); + clearInvocations(mMockCompatLayout, mMockLetterboxEduLayout); - clearInvocations(mMockLayout); + // Verify button remains hidden after IME is hidden since keyguard is showing. + mController.onImeVisibilityChanged(DISPLAY_ID, /* isShowing= */ false); - // Verify button remains hidden after IME is hidden since keyguard is occluded. - mController.onImeVisibilityChanged(DISPLAY_ID, false /* isShowing */); + verify(mMockCompatLayout).updateVisibility(false); + verify(mMockLetterboxEduLayout).updateVisibility(false); - verify(mMockLayout).updateVisibility(false); + // Verify button is shown after keyguard becomes not showing. + mController.onKeyguardShowingChanged(false); - // Verify button is shown after keyguard becomes not occluded. - mController.onKeyguardOccludedChanged(false); + verify(mMockCompatLayout).updateVisibility(true); + verify(mMockLetterboxEduLayout).updateVisibility(true); + } - verify(mMockLayout).updateVisibility(true); + private static TaskInfo createTaskInfo(int displayId, int taskId, boolean hasSizeCompat, + @CameraCompatControlState int cameraCompatControlState) { + RunningTaskInfo taskInfo = new RunningTaskInfo(); + taskInfo.taskId = taskId; + taskInfo.displayId = displayId; + taskInfo.topActivityInSizeCompat = hasSizeCompat; + taskInfo.cameraCompatControlState = cameraCompatControlState; + return taskInfo; } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java index 2c3987bc358d..7d3e718313e6 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java @@ -16,13 +16,20 @@ package com.android.wm.shell.compatui; +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 com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.verify; -import android.content.res.Configuration; +import android.app.ActivityManager; +import android.app.TaskInfo; +import android.app.TaskInfo.CameraCompatControlState; import android.testing.AndroidTestingRunner; import android.view.LayoutInflater; import android.view.SurfaceControlViewHost; @@ -36,6 +43,7 @@ import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.SyncTransactionQueue; +import com.android.wm.shell.compatui.CompatUIWindowManager.CompatUIHintsState; import org.junit.Before; import org.junit.Test; @@ -61,32 +69,33 @@ public class CompatUILayoutTest extends ShellTestCase { @Mock private SurfaceControlViewHost mViewHost; private CompatUIWindowManager mWindowManager; - private CompatUILayout mCompatUILayout; + private CompatUILayout mLayout; @Before public void setUp() { MockitoAnnotations.initMocks(this); - mWindowManager = new CompatUIWindowManager(mContext, new Configuration(), - mSyncTransactionQueue, mCallback, TASK_ID, mTaskListener, new DisplayLayout(), - false /* hasShownHint */); + mWindowManager = new CompatUIWindowManager(mContext, + createTaskInfo(/* hasSizeCompat= */ false, CAMERA_COMPAT_CONTROL_HIDDEN), + mSyncTransactionQueue, mCallback, mTaskListener, + new DisplayLayout(), new CompatUIHintsState()); - mCompatUILayout = (CompatUILayout) + mLayout = (CompatUILayout) LayoutInflater.from(mContext).inflate(R.layout.compat_ui_layout, null); - mCompatUILayout.inject(mWindowManager); + mLayout.inject(mWindowManager); spyOn(mWindowManager); - spyOn(mCompatUILayout); + spyOn(mLayout); doReturn(mViewHost).when(mWindowManager).createSurfaceViewHost(); + doReturn(mLayout).when(mWindowManager).inflateLayout(); } @Test public void testOnClickForRestartButton() { - final ImageButton button = mCompatUILayout.findViewById(R.id.size_compat_restart_button); + final ImageButton button = mLayout.findViewById(R.id.size_compat_restart_button); button.performClick(); verify(mWindowManager).onRestartButtonClicked(); - doReturn(mCompatUILayout).when(mWindowManager).inflateCompatUILayout(); verify(mCallback).onSizeCompatRestartButtonClicked(TASK_ID); } @@ -94,7 +103,7 @@ public class CompatUILayoutTest extends ShellTestCase { public void testOnLongClickForRestartButton() { doNothing().when(mWindowManager).onRestartButtonLongClicked(); - final ImageButton button = mCompatUILayout.findViewById(R.id.size_compat_restart_button); + final ImageButton button = mLayout.findViewById(R.id.size_compat_restart_button); button.performLongClick(); verify(mWindowManager).onRestartButtonLongClicked(); @@ -102,10 +111,101 @@ public class CompatUILayoutTest extends ShellTestCase { @Test public void testOnClickForSizeCompatHint() { - mWindowManager.createLayout(true /* show */); - final LinearLayout sizeCompatHint = mCompatUILayout.findViewById(R.id.size_compat_hint); + mWindowManager.mHasSizeCompat = true; + mWindowManager.createLayout(/* canShow= */ true); + final LinearLayout sizeCompatHint = mLayout.findViewById(R.id.size_compat_hint); sizeCompatHint.performClick(); - verify(mCompatUILayout).setSizeCompatHintVisibility(/* show= */ false); + verify(mLayout).setSizeCompatHintVisibility(/* show= */ false); + } + + @Test + public void testUpdateCameraTreatmentButton_treatmentAppliedByDefault() { + mWindowManager.mCameraCompatControlState = CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED; + mWindowManager.createLayout(/* canShow= */ true); + final ImageButton button = + mLayout.findViewById(R.id.camera_compat_treatment_button); + button.performClick(); + + verify(mWindowManager).onCameraTreatmentButtonClicked(); + verify(mCallback).onCameraControlStateUpdated( + TASK_ID, CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED); + + button.performClick(); + + verify(mCallback).onCameraControlStateUpdated( + TASK_ID, CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED); + } + + @Test + public void testUpdateCameraTreatmentButton_treatmentSuggestedByDefault() { + mWindowManager.mCameraCompatControlState = CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED; + mWindowManager.createLayout(/* canShow= */ true); + final ImageButton button = + mLayout.findViewById(R.id.camera_compat_treatment_button); + button.performClick(); + + verify(mWindowManager).onCameraTreatmentButtonClicked(); + verify(mCallback).onCameraControlStateUpdated( + TASK_ID, CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED); + + button.performClick(); + + verify(mCallback).onCameraControlStateUpdated( + TASK_ID, CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED); + } + + @Test + public void testOnCameraDismissButtonClicked() { + mWindowManager.mCameraCompatControlState = CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED; + mWindowManager.createLayout(/* canShow= */ true); + final ImageButton button = + mLayout.findViewById(R.id.camera_compat_dismiss_button); + button.performClick(); + + verify(mWindowManager).onCameraDismissButtonClicked(); + verify(mCallback).onCameraControlStateUpdated( + TASK_ID, CAMERA_COMPAT_CONTROL_DISMISSED); + verify(mLayout).setCameraControlVisibility(/* show */ false); + } + + @Test + public void testOnLongClickForCameraTreatmentButton() { + doNothing().when(mWindowManager).onCameraButtonLongClicked(); + + final ImageButton button = + mLayout.findViewById(R.id.camera_compat_treatment_button); + button.performLongClick(); + + verify(mWindowManager).onCameraButtonLongClicked(); + } + + @Test + public void testOnLongClickForCameraDismissButton() { + doNothing().when(mWindowManager).onCameraButtonLongClicked(); + + final ImageButton button = mLayout.findViewById(R.id.camera_compat_dismiss_button); + button.performLongClick(); + + verify(mWindowManager).onCameraButtonLongClicked(); + } + + @Test + public void testOnClickForCameraCompatHint() { + mWindowManager.mCameraCompatControlState = CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED; + mWindowManager.createLayout(/* canShow= */ true); + final LinearLayout hint = mLayout.findViewById(R.id.camera_compat_hint); + hint.performClick(); + + verify(mLayout).setCameraCompatHintVisibility(/* show= */ false); + } + + private static TaskInfo createTaskInfo(boolean hasSizeCompat, + @CameraCompatControlState int cameraCompatControlState) { + ActivityManager.RunningTaskInfo taskInfo = new ActivityManager.RunningTaskInfo(); + taskInfo.taskId = TASK_ID; + taskInfo.topActivityInSizeCompat = hasSizeCompat; + taskInfo.cameraCompatControlState = cameraCompatControlState; + return taskInfo; } } 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 d5dcf2e11a46..e79b803b4304 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java @@ -16,21 +16,26 @@ package com.android.wm.shell.compatui; +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 com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; 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.clearInvocations; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -import android.content.res.Configuration; +import android.app.ActivityManager; +import android.app.TaskInfo; import android.graphics.Rect; import android.testing.AndroidTestingRunner; import android.view.DisplayInfo; @@ -46,6 +51,7 @@ import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.SyncTransactionQueue; +import com.android.wm.shell.compatui.CompatUIWindowManager.CompatUIHintsState; import org.junit.Before; import org.junit.Test; @@ -68,56 +74,107 @@ public class CompatUIWindowManagerTest extends ShellTestCase { @Mock private SyncTransactionQueue mSyncTransactionQueue; @Mock private CompatUIController.CompatUICallback mCallback; @Mock private ShellTaskOrganizer.TaskListener mTaskListener; - @Mock private CompatUILayout mCompatUILayout; + @Mock private CompatUILayout mLayout; @Mock private SurfaceControlViewHost mViewHost; - private Configuration mTaskConfig; private CompatUIWindowManager mWindowManager; @Before public void setUp() { MockitoAnnotations.initMocks(this); - mTaskConfig = new Configuration(); - mWindowManager = new CompatUIWindowManager(mContext, new Configuration(), - mSyncTransactionQueue, mCallback, TASK_ID, mTaskListener, new DisplayLayout(), - false /* hasShownHint */); + mWindowManager = new CompatUIWindowManager(mContext, + createTaskInfo(/* hasSizeCompat= */ false, CAMERA_COMPAT_CONTROL_HIDDEN), + mSyncTransactionQueue, mCallback, mTaskListener, + new DisplayLayout(), new CompatUIHintsState()); spyOn(mWindowManager); - doReturn(mCompatUILayout).when(mWindowManager).inflateCompatUILayout(); + doReturn(mLayout).when(mWindowManager).inflateLayout(); doReturn(mViewHost).when(mWindowManager).createSurfaceViewHost(); } @Test public void testCreateSizeCompatButton() { - // Not create layout if show is false. - mWindowManager.createLayout(false /* show */); + // Doesn't create layout if show is false. + mWindowManager.mHasSizeCompat = true; + assertTrue(mWindowManager.createLayout(/* canShow= */ false)); - verify(mWindowManager, never()).inflateCompatUILayout(); + verify(mWindowManager, never()).inflateLayout(); - // Not create hint popup. - mWindowManager.mShouldShowHint = false; - mWindowManager.createLayout(true /* show */); + // Doesn't create hint popup. + mWindowManager.mCompatUIHintsState.mHasShownSizeCompatHint = true; + assertTrue(mWindowManager.createLayout(/* canShow= */ true)); - verify(mWindowManager).inflateCompatUILayout(); - verify(mCompatUILayout).setSizeCompatHintVisibility(false /* show */); + verify(mWindowManager).inflateLayout(); + verify(mLayout).setRestartButtonVisibility(/* show= */ true); + verify(mLayout, never()).setSizeCompatHintVisibility(/* show= */ true); - // Create hint popup. + // Creates hint popup. + clearInvocations(mWindowManager); + clearInvocations(mLayout); + mWindowManager.release(); + mWindowManager.mCompatUIHintsState.mHasShownSizeCompatHint = false; + assertTrue(mWindowManager.createLayout(/* canShow= */ true)); + + verify(mWindowManager).inflateLayout(); + assertNotNull(mLayout); + verify(mLayout).setRestartButtonVisibility(/* show= */ true); + verify(mLayout).setSizeCompatHintVisibility(/* show= */ true); + assertTrue(mWindowManager.mCompatUIHintsState.mHasShownSizeCompatHint); + + // Returns false and doesn't create layout if has Size Compat is false. + clearInvocations(mWindowManager); mWindowManager.release(); - mWindowManager.mShouldShowHint = true; - mWindowManager.createLayout(true /* show */); + mWindowManager.mHasSizeCompat = false; + assertFalse(mWindowManager.createLayout(/* canShow= */ true)); - verify(mWindowManager, times(2)).inflateCompatUILayout(); - assertNotNull(mCompatUILayout); - verify(mCompatUILayout).setSizeCompatHintVisibility(true /* show */); - assertFalse(mWindowManager.mShouldShowHint); + verify(mWindowManager, never()).inflateLayout(); + } + + @Test + public void testCreateCameraCompatControl() { + // Doesn't create layout if show is false. + mWindowManager.mCameraCompatControlState = CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED; + assertTrue(mWindowManager.createLayout(/* canShow= */ false)); + + verify(mWindowManager, never()).inflateLayout(); + + // Doesn't create hint popup. + mWindowManager.mCompatUIHintsState.mHasShownCameraCompatHint = true; + assertTrue(mWindowManager.createLayout(/* canShow= */ true)); + + verify(mWindowManager).inflateLayout(); + verify(mLayout).setCameraControlVisibility(/* show= */ true); + verify(mLayout, never()).setCameraCompatHintVisibility(/* show= */ true); + + // Creates hint popup. + clearInvocations(mWindowManager); + clearInvocations(mLayout); + mWindowManager.release(); + mWindowManager.mCompatUIHintsState.mHasShownCameraCompatHint = false; + assertTrue(mWindowManager.createLayout(/* canShow= */ true)); + + verify(mWindowManager).inflateLayout(); + assertNotNull(mLayout); + verify(mLayout).setCameraControlVisibility(/* show= */ true); + verify(mLayout).setCameraCompatHintVisibility(/* show= */ true); + assertTrue(mWindowManager.mCompatUIHintsState.mHasShownCameraCompatHint); + + // Returns false and doesn't create layout if Camera Compat state is hidden + clearInvocations(mWindowManager); + mWindowManager.release(); + mWindowManager.mCameraCompatControlState = CAMERA_COMPAT_CONTROL_HIDDEN; + assertFalse(mWindowManager.createLayout(/* canShow= */ true)); + + verify(mWindowManager, never()).inflateLayout(); } @Test public void testRelease() { - mWindowManager.createLayout(true /* show */); + mWindowManager.mHasSizeCompat = true; + mWindowManager.createLayout(/* canShow= */ true); - verify(mWindowManager).inflateCompatUILayout(); + verify(mWindowManager).inflateLayout(); mWindowManager.release(); @@ -126,11 +183,13 @@ public class CompatUIWindowManagerTest extends ShellTestCase { @Test public void testUpdateCompatInfo() { - mWindowManager.createLayout(true /* show */); + mWindowManager.mHasSizeCompat = true; + mWindowManager.createLayout(/* canShow= */ true); // No diff clearInvocations(mWindowManager); - mWindowManager.updateCompatInfo(mTaskConfig, mTaskListener, true /* show */); + TaskInfo taskInfo = createTaskInfo(/* hasSizeCompat= */ true, CAMERA_COMPAT_CONTROL_HIDDEN); + assertTrue(mWindowManager.updateCompatInfo(taskInfo, mTaskListener, /* canShow= */ true)); verify(mWindowManager, never()).updateSurfacePosition(); verify(mWindowManager, never()).release(); @@ -140,20 +199,100 @@ public class CompatUIWindowManagerTest extends ShellTestCase { clearInvocations(mWindowManager); final ShellTaskOrganizer.TaskListener newTaskListener = mock( ShellTaskOrganizer.TaskListener.class); - mWindowManager.updateCompatInfo(mTaskConfig, newTaskListener, - true /* show */); + assertTrue(mWindowManager.updateCompatInfo(taskInfo, newTaskListener, /* canShow= */ true)); verify(mWindowManager).release(); - verify(mWindowManager).createLayout(anyBoolean()); + verify(mWindowManager).createLayout(/* canShow= */ true); + + // Change Camera Compat state, show a control. + clearInvocations(mWindowManager); + clearInvocations(mLayout); + taskInfo = createTaskInfo(/* hasSizeCompat= */ true, + CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED); + assertTrue(mWindowManager.updateCompatInfo(taskInfo, newTaskListener, /* canShow= */ true)); + + verify(mLayout).setCameraControlVisibility(/* show= */ true); + verify(mLayout).updateCameraTreatmentButton( + CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED); + + // Change Camera Compat state, update a control. + clearInvocations(mWindowManager); + clearInvocations(mLayout); + taskInfo = createTaskInfo(/* hasSizeCompat= */ true, + CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED); + assertTrue(mWindowManager.updateCompatInfo(taskInfo, newTaskListener, /* canShow= */ true)); + + verify(mLayout).setCameraControlVisibility(/* show= */ true); + verify(mLayout).updateCameraTreatmentButton( + CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED); + + // Change has Size Compat to false, hides restart button. + clearInvocations(mWindowManager); + clearInvocations(mLayout); + taskInfo = createTaskInfo(/* hasSizeCompat= */ false, + CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED); + assertTrue(mWindowManager.updateCompatInfo(taskInfo, newTaskListener, /* canShow= */ true)); + + verify(mLayout).setRestartButtonVisibility(/* show= */ false); + + // Change has Size Compat to true, shows restart button. + clearInvocations(mWindowManager); + clearInvocations(mLayout); + taskInfo = createTaskInfo(/* hasSizeCompat= */ true, + CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED); + assertTrue(mWindowManager.updateCompatInfo(taskInfo, newTaskListener, /* canShow= */ true)); + + verify(mLayout).setRestartButtonVisibility(/* show= */ true); + + // Change Camera Compat state to dismissed, hide a control. + clearInvocations(mWindowManager); + clearInvocations(mLayout); + taskInfo = createTaskInfo(/* hasSizeCompat= */ true, CAMERA_COMPAT_CONTROL_DISMISSED); + assertTrue(mWindowManager.updateCompatInfo(taskInfo, newTaskListener, /* canShow= */ true)); + + verify(mLayout).setCameraControlVisibility(/* show= */ false); // Change task bounds, update position. clearInvocations(mWindowManager); - final Configuration newTaskConfiguration = new Configuration(); - newTaskConfiguration.windowConfiguration.setBounds(new Rect(0, 1000, 0, 2000)); - mWindowManager.updateCompatInfo(newTaskConfiguration, newTaskListener, - true /* show */); + clearInvocations(mLayout); + taskInfo = createTaskInfo(/* hasSizeCompat= */ true, CAMERA_COMPAT_CONTROL_HIDDEN); + taskInfo.configuration.windowConfiguration.setBounds(new Rect(0, 1000, 0, 2000)); + assertTrue(mWindowManager.updateCompatInfo(taskInfo, newTaskListener, /* canShow= */ true)); verify(mWindowManager).updateSurfacePosition(); + + // Change has Size Compat to false, release layout. + clearInvocations(mWindowManager); + clearInvocations(mLayout); + taskInfo = createTaskInfo(/* hasSizeCompat= */ false, CAMERA_COMPAT_CONTROL_HIDDEN); + assertFalse( + mWindowManager.updateCompatInfo(taskInfo, newTaskListener, /* canShow= */ true)); + + verify(mWindowManager).release(); + } + + @Test + public void testUpdateCompatInfoLayoutNotInflatedYet() { + mWindowManager.mHasSizeCompat = true; + mWindowManager.createLayout(/* canShow= */ false); + + verify(mWindowManager, never()).inflateLayout(); + + // Change topActivityInSizeCompat to false and pass canShow true, layout shouldn't be + // inflated + clearInvocations(mWindowManager); + TaskInfo taskInfo = createTaskInfo(/* hasSizeCompat= */ false, + CAMERA_COMPAT_CONTROL_HIDDEN); + mWindowManager.updateCompatInfo(taskInfo, mTaskListener, /* canShow= */ true); + + verify(mWindowManager, never()).inflateLayout(); + + // Change topActivityInSizeCompat to true and pass canShow true, layout should be inflated. + clearInvocations(mWindowManager); + taskInfo = createTaskInfo(/* hasSizeCompat= */ true, CAMERA_COMPAT_CONTROL_HIDDEN); + mWindowManager.updateCompatInfo(taskInfo, mTaskListener, /* canShow= */ true); + + verify(mWindowManager).inflateLayout(); } @Test @@ -200,25 +339,26 @@ public class CompatUIWindowManagerTest extends ShellTestCase { @Test public void testUpdateVisibility() { // Create button if it is not created. - mWindowManager.mCompatUILayout = null; - mWindowManager.updateVisibility(true /* show */); + mWindowManager.mLayout = null; + mWindowManager.mHasSizeCompat = true; + mWindowManager.updateVisibility(/* canShow= */ true); - verify(mWindowManager).createLayout(true /* show */); + verify(mWindowManager).createLayout(/* canShow= */ true); // Hide button. clearInvocations(mWindowManager); - doReturn(View.VISIBLE).when(mCompatUILayout).getVisibility(); - mWindowManager.updateVisibility(false /* show */); + doReturn(View.VISIBLE).when(mLayout).getVisibility(); + mWindowManager.updateVisibility(/* canShow= */ false); verify(mWindowManager, never()).createLayout(anyBoolean()); - verify(mCompatUILayout).setVisibility(View.GONE); + verify(mLayout).setVisibility(View.GONE); // Show button. - doReturn(View.GONE).when(mCompatUILayout).getVisibility(); - mWindowManager.updateVisibility(true /* show */); + doReturn(View.GONE).when(mLayout).getVisibility(); + mWindowManager.updateVisibility(/* canShow= */ true); verify(mWindowManager, never()).createLayout(anyBoolean()); - verify(mCompatUILayout).setVisibility(View.VISIBLE); + verify(mLayout).setVisibility(View.VISIBLE); } @Test @@ -230,6 +370,37 @@ public class CompatUIWindowManagerTest extends ShellTestCase { } @Test + public void testOnCameraDismissButtonClicked() { + mWindowManager.mCameraCompatControlState = CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED; + mWindowManager.createLayout(/* canShow= */ true); + clearInvocations(mLayout); + mWindowManager.onCameraDismissButtonClicked(); + + verify(mCallback).onCameraControlStateUpdated(TASK_ID, CAMERA_COMPAT_CONTROL_DISMISSED); + verify(mLayout).setCameraControlVisibility(/* show= */ false); + } + + @Test + public void testOnCameraTreatmentButtonClicked() { + mWindowManager.mCameraCompatControlState = CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED; + mWindowManager.createLayout(/* canShow= */ true); + clearInvocations(mLayout); + mWindowManager.onCameraTreatmentButtonClicked(); + + verify(mCallback).onCameraControlStateUpdated( + TASK_ID, CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED); + verify(mLayout).updateCameraTreatmentButton( + CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED); + + mWindowManager.onCameraTreatmentButtonClicked(); + + verify(mCallback).onCameraControlStateUpdated( + TASK_ID, CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED); + verify(mLayout).updateCameraTreatmentButton( + CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED); + } + + @Test public void testOnRestartButtonClicked() { mWindowManager.onRestartButtonClicked(); @@ -239,15 +410,39 @@ public class CompatUIWindowManagerTest extends ShellTestCase { @Test public void testOnRestartButtonLongClicked_showHint() { // Not create hint popup. - mWindowManager.mShouldShowHint = false; - mWindowManager.createLayout(true /* show */); + mWindowManager.mHasSizeCompat = true; + mWindowManager.mCompatUIHintsState.mHasShownSizeCompatHint = true; + mWindowManager.createLayout(/* canShow= */ true); - verify(mWindowManager).inflateCompatUILayout(); - verify(mCompatUILayout).setSizeCompatHintVisibility(false /* show */); + verify(mWindowManager).inflateLayout(); + verify(mLayout, never()).setSizeCompatHintVisibility(/* show= */ true); mWindowManager.onRestartButtonLongClicked(); - verify(mCompatUILayout).setSizeCompatHintVisibility(true /* show */); + verify(mLayout).setSizeCompatHintVisibility(/* show= */ true); } + @Test + public void testOnCameraControlLongClicked_showHint() { + // Not create hint popup. + mWindowManager.mCameraCompatControlState = CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED; + mWindowManager.mCompatUIHintsState.mHasShownCameraCompatHint = true; + mWindowManager.createLayout(/* canShow= */ true); + + verify(mWindowManager).inflateLayout(); + verify(mLayout, never()).setCameraCompatHintVisibility(/* show= */ true); + + mWindowManager.onCameraButtonLongClicked(); + + verify(mLayout).setCameraCompatHintVisibility(/* show= */ true); + } + + private static TaskInfo createTaskInfo(boolean hasSizeCompat, + @TaskInfo.CameraCompatControlState int cameraCompatControlState) { + ActivityManager.RunningTaskInfo taskInfo = new ActivityManager.RunningTaskInfo(); + taskInfo.taskId = TASK_ID; + taskInfo.topActivityInSizeCompat = hasSizeCompat; + taskInfo.cameraCompatControlState = cameraCompatControlState; + return taskInfo; + } } 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/letterboxedu/LetterboxEduDialogLayoutTest.java new file mode 100644 index 000000000000..1dee88c43806 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduDialogLayoutTest.java @@ -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. + */ + +package com.android.wm.shell.compatui.letterboxedu; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +import android.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.Mock; +import org.mockito.MockitoAnnotations; + +/** + * Tests for {@link LetterboxEduDialogLayout}. + * + * Build/Install/Run: + * atest WMShellUnitTests:LetterboxEduDialogLayoutTest + */ +@RunWith(AndroidTestingRunner.class) +@SmallTest +public class LetterboxEduDialogLayoutTest extends ShellTestCase { + + @Mock + private Runnable mDismissCallback; + + private LetterboxEduDialogLayout mLayout; + private View mDismissButton; + private View mDialogContainer; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + 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); + } + + @Test + public void testOnFinishInflate() { + assertEquals(mLayout.getDialogContainer(), + 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.getBackground().getAlpha(), 0); + } + + @Test + public void testOnDismissButtonClicked() { + assertTrue(mDismissButton.performClick()); + + verify(mDismissCallback).run(); + } + + @Test + public void testOnBackgroundClicked() { + assertTrue(mLayout.performClick()); + + verify(mDismissCallback).run(); + } + + @Test + public void testOnDialogContainerClicked() { + assertTrue(mDialogContainer.performClick()); + + verify(mDismissCallback, never()).run(); + } + + @Test + public void testSetDismissOnClickListenerNull() { + mLayout.setDismissOnClickListener(null); + + assertFalse(mDismissButton.performClick()); + assertFalse(mLayout.performClick()); + assertFalse(mDialogContainer.performClick()); + + verify(mDismissCallback, never()).run(); + } +} 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/letterboxedu/LetterboxEduWindowManagerTest.java new file mode 100644 index 000000000000..f3a8cf45b7f8 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduWindowManagerTest.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.compatui.letterboxedu; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; + +import static com.google.common.truth.Truth.assertThat; + +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.eq; +import static org.mockito.Mockito.clearInvocations; +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.view.DisplayCutout; +import android.view.DisplayInfo; +import android.view.SurfaceControlViewHost; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewGroup.MarginLayoutParams; +import android.view.WindowManager; +import android.view.accessibility.AccessibilityEvent; + +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.common.DisplayLayout; +import com.android.wm.shell.common.SyncTransactionQueue; +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.Captor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** + * Tests for {@link LetterboxEduWindowManager}. + * + * Build/Install/Run: + * atest WMShellUnitTests:LetterboxEduWindowManagerTest + */ +@RunWith(AndroidTestingRunner.class) +@SmallTest +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 int TASK_ID = 1; + + private static final int TASK_WIDTH = 200; + private static final int TASK_HEIGHT = 100; + private static final int DISPLAY_CUTOUT_TOP = 5; + private static final int DISPLAY_CUTOUT_BOTTOM = 10; + private static final int DISPLAY_CUTOUT_HORIZONTAL = 20; + + @Captor + private ArgumentCaptor<WindowManager.LayoutParams> mWindowAttrsCaptor; + @Captor + private ArgumentCaptor<Runnable> mEndCallbackCaptor; + @Captor + private ArgumentCaptor<Runnable> mRunOnIdleCaptor; + + @Mock private LetterboxEduAnimationController mAnimationController; + @Mock private SyncTransactionQueue mSyncTransactionQueue; + @Mock private ShellTaskOrganizer.TaskListener mTaskListener; + @Mock private SurfaceControlViewHost mViewHost; + @Mock private Transitions mTransitions; + @Mock private Runnable mOnDismissCallback; + + private SharedPreferences mSharedPreferences; + @Nullable + private Boolean mInitialPrefValue1 = null; + @Nullable + private Boolean mInitialPrefValue2 = null; + + @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(); + } + } + + @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(); + } + + @Test + public void testCreateLayout_notEligible_doesNotCreateLayout() { + LetterboxEduWindowManager windowManager = createWindowManager(/* eligible= */ false); + + 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)); + + assertNull(windowManager.mLayout); + } + + @Test + public void testCreateLayout_canShowFalse_returnsTrueButDoesNotCreateLayout() { + LetterboxEduWindowManager windowManager = createWindowManager(/* eligible= */ true); + + assertTrue(windowManager.createLayout(/* canShow= */ false)); + + assertFalse(mSharedPreferences.getBoolean(PREF_KEY_1, /* default= */ false)); + assertNull(windowManager.mLayout); + } + + @Test + public void testCreateLayout_canShowTrue_createsLayoutCorrectly() { + LetterboxEduWindowManager windowManager = createWindowManager(/* eligible= */ true); + + assertTrue(windowManager.createLayout(/* canShow= */ true)); + + LetterboxEduDialogLayout layout = windowManager.mLayout; + assertNotNull(layout); + verify(mViewHost).setView(eq(layout), mWindowAttrsCaptor.capture()); + verifyLayout(layout, mWindowAttrsCaptor.getValue(), /* expectedWidth= */ TASK_WIDTH, + /* expectedHeight= */ TASK_HEIGHT, /* expectedExtraTopMargin= */ DISPLAY_CUTOUT_TOP, + /* expectedExtraBottomMargin= */ DISPLAY_CUTOUT_BOTTOM); + View dialogTitle = layout.getDialogTitle(); + assertNotNull(dialogTitle); + spyOn(dialogTitle); + + // The education shouldn't be marked as seen until enter animation is done. + assertFalse(mSharedPreferences.getBoolean(PREF_KEY_1, /* default= */ false)); + // Clicking the layout does nothing until enter animation is done. + layout.performClick(); + verify(mAnimationController, never()).startExitAnimation(any(), any()); + // The dialog title shouldn't be focused for Accessibility until enter animation is done. + verify(dialogTitle, never()).sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED); + + verifyAndFinishEnterAnimation(layout); + + assertTrue(mSharedPreferences.getBoolean(PREF_KEY_1, /* default= */ false)); + verify(dialogTitle).sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED); + // Exit animation should start following a click on the layout. + layout.performClick(); + + // Window manager isn't released until exit animation is done. + verify(windowManager, never()).release(); + + // Verify multiple clicks are ignored. + layout.performClick(); + + verifyAndFinishExitAnimation(layout); + + verify(windowManager).release(); + verify(mOnDismissCallback).run(); + } + + @Test + public void testCreateLayout_alreadyShownToUser_createsLayoutForOtherUserOnly() { + LetterboxEduWindowManager windowManager = createWindowManager(/* eligible= */ true, + USER_ID_1, /* isTaskbarEduShowing= */ false); + + assertTrue(windowManager.createLayout(/* canShow= */ true)); + + assertNotNull(windowManager.mLayout); + verifyAndFinishEnterAnimation(windowManager.mLayout); + assertTrue(mSharedPreferences.getBoolean(PREF_KEY_1, /* default= */ false)); + + windowManager.release(); + windowManager = createWindowManager(/* eligible= */ true, + USER_ID_1, /* isTaskbarEduShowing= */ false); + + assertFalse(windowManager.createLayout(/* canShow= */ true)); + assertNull(windowManager.mLayout); + + clearInvocations(mTransitions, mAnimationController); + + windowManager = createWindowManager(/* eligible= */ true, + USER_ID_2, /* isTaskbarEduShowing= */ false); + + assertTrue(windowManager.createLayout(/* canShow= */ true)); + + assertNotNull(windowManager.mLayout); + verifyAndFinishEnterAnimation(windowManager.mLayout); + assertTrue(mSharedPreferences.getBoolean(PREF_KEY_1, /* default= */ false)); + } + + @Test + public void testCreateLayout_windowManagerReleasedBeforeTransitionsIsIdle_doesNotStartAnim() { + LetterboxEduWindowManager windowManager = createWindowManager(/* eligible= */ true); + + assertTrue(windowManager.createLayout(/* canShow= */ true)); + assertNotNull(windowManager.mLayout); + + verify(mTransitions).runOnIdle(mRunOnIdleCaptor.capture()); + + windowManager.release(); + + mRunOnIdleCaptor.getValue().run(); + + verify(mAnimationController, never()).startEnterAnimation(any(), any()); + assertFalse(mSharedPreferences.getBoolean(PREF_KEY_1, /* default= */ false)); + } + + @Test + public void testUpdateCompatInfo_updatesLayoutCorrectly() { + LetterboxEduWindowManager windowManager = createWindowManager(/* eligible= */ true); + + assertTrue(windowManager.createLayout(/* canShow= */ true)); + LetterboxEduDialogLayout layout = windowManager.mLayout; + assertNotNull(layout); + + assertTrue(windowManager.updateCompatInfo( + createTaskInfo(/* eligible= */ true, USER_ID_1, new Rect(50, 25, 150, 75)), + mTaskListener, /* canShow= */ true)); + + verifyLayout(layout, layout.getLayoutParams(), /* expectedWidth= */ 100, + /* expectedHeight= */ 50, /* expectedExtraTopMargin= */ 0, + /* expectedExtraBottomMargin= */ 0); + verify(mViewHost).relayout(mWindowAttrsCaptor.capture()); + assertThat(mWindowAttrsCaptor.getValue()).isEqualTo(layout.getLayoutParams()); + + // Window manager should be released (without animation) when eligible becomes false. + assertFalse(windowManager.updateCompatInfo(createTaskInfo(/* eligible= */ false), + mTaskListener, /* canShow= */ true)); + + verify(windowManager).release(); + verify(mOnDismissCallback, never()).run(); + verify(mAnimationController, never()).startExitAnimation(any(), any()); + assertNull(windowManager.mLayout); + } + + @Test + public void testUpdateCompatInfo_notEligibleUntilUpdate_createsLayoutAfterUpdate() { + LetterboxEduWindowManager windowManager = createWindowManager(/* eligible= */ false); + + assertFalse(windowManager.createLayout(/* canShow= */ true)); + assertNull(windowManager.mLayout); + + assertTrue(windowManager.updateCompatInfo(createTaskInfo(/* eligible= */ true), + mTaskListener, /* canShow= */ true)); + + assertNotNull(windowManager.mLayout); + } + + @Test + public void testUpdateCompatInfo_canShowFalse_doesNothing() { + LetterboxEduWindowManager windowManager = createWindowManager(/* eligible= */ true); + + assertTrue(windowManager.createLayout(/* canShow= */ false)); + assertNull(windowManager.mLayout); + + assertTrue(windowManager.updateCompatInfo(createTaskInfo(/* eligible= */ true), + mTaskListener, /* canShow= */ false)); + + assertNull(windowManager.mLayout); + verify(mViewHost, never()).relayout(any()); + } + + @Test + public void testUpdateDisplayLayout_updatesLayoutCorrectly() { + LetterboxEduWindowManager windowManager = createWindowManager(/* eligible= */ true); + + assertTrue(windowManager.createLayout(/* canShow= */ true)); + LetterboxEduDialogLayout layout = windowManager.mLayout; + assertNotNull(layout); + + int newDisplayCutoutTop = DISPLAY_CUTOUT_TOP + 7; + int newDisplayCutoutBottom = DISPLAY_CUTOUT_BOTTOM + 9; + windowManager.updateDisplayLayout(createDisplayLayout( + Insets.of(DISPLAY_CUTOUT_HORIZONTAL, newDisplayCutoutTop, + DISPLAY_CUTOUT_HORIZONTAL, newDisplayCutoutBottom))); + + verifyLayout(layout, layout.getLayoutParams(), /* expectedWidth= */ TASK_WIDTH, + /* expectedHeight= */ TASK_HEIGHT, /* expectedExtraTopMargin= */ + newDisplayCutoutTop, /* expectedExtraBottomMargin= */ newDisplayCutoutBottom); + verify(mViewHost).relayout(mWindowAttrsCaptor.capture()); + assertThat(mWindowAttrsCaptor.getValue()).isEqualTo(layout.getLayoutParams()); + } + + @Test + public void testRelease_animationIsCancelled() { + LetterboxEduWindowManager windowManager = createWindowManager(/* eligible= */ true); + + assertTrue(windowManager.createLayout(/* canShow= */ true)); + windowManager.release(); + + verify(mAnimationController).cancelAnimation(); + } + + private void verifyLayout(LetterboxEduDialogLayout layout, ViewGroup.LayoutParams params, + int expectedWidth, int expectedHeight, int expectedExtraTopMargin, + int expectedExtraBottomMargin) { + assertThat(params.width).isEqualTo(expectedWidth); + assertThat(params.height).isEqualTo(expectedHeight); + MarginLayoutParams dialogParams = + (MarginLayoutParams) layout.getDialogContainer().getLayoutParams(); + int verticalMargin = (int) mContext.getResources().getDimension( + R.dimen.letterbox_education_dialog_margin); + assertThat(dialogParams.topMargin).isEqualTo(verticalMargin + expectedExtraTopMargin); + assertThat(dialogParams.bottomMargin).isEqualTo(verticalMargin + expectedExtraBottomMargin); + } + + private void verifyAndFinishEnterAnimation(LetterboxEduDialogLayout layout) { + verify(mTransitions).runOnIdle(mRunOnIdleCaptor.capture()); + + // startEnterAnimation isn't called until run-on-idle runnable is called. + verify(mAnimationController, never()).startEnterAnimation(any(), any()); + + mRunOnIdleCaptor.getValue().run(); + + verify(mAnimationController).startEnterAnimation(eq(layout), mEndCallbackCaptor.capture()); + mEndCallbackCaptor.getValue().run(); + } + + private void verifyAndFinishExitAnimation(LetterboxEduDialogLayout layout) { + verify(mAnimationController).startExitAnimation(eq(layout), mEndCallbackCaptor.capture()); + mEndCallbackCaptor.getValue().run(); + } + + private LetterboxEduWindowManager createWindowManager(boolean eligible) { + 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); + + spyOn(windowManager); + doReturn(mViewHost).when(windowManager).createSurfaceViewHost(); + doReturn(isTaskbarEduShowing).when(windowManager).isTaskbarEduShowing(); + + return windowManager; + } + + private DisplayLayout createDisplayLayout() { + return createDisplayLayout( + Insets.of(DISPLAY_CUTOUT_HORIZONTAL, DISPLAY_CUTOUT_TOP, DISPLAY_CUTOUT_HORIZONTAL, + DISPLAY_CUTOUT_BOTTOM)); + } + + private DisplayLayout createDisplayLayout(Insets insets) { + DisplayInfo displayInfo = new DisplayInfo(); + displayInfo.logicalWidth = TASK_WIDTH; + displayInfo.logicalHeight = TASK_HEIGHT; + displayInfo.displayCutout = new DisplayCutout( + insets, null, null, null, null); + return new DisplayLayout(displayInfo, + mContext.getResources(), /* hasNavigationBar= */ false, /* hasStatusBar= */ false); + } + + private static TaskInfo createTaskInfo(boolean eligible) { + return createTaskInfo(eligible, USER_ID_1); + } + + private static TaskInfo createTaskInfo(boolean eligible, int userId) { + return createTaskInfo(eligible, userId, new Rect(0, 0, TASK_WIDTH, TASK_HEIGHT)); + } + + private static TaskInfo createTaskInfo(boolean eligible, int userId, Rect bounds) { + ActivityManager.RunningTaskInfo taskInfo = new ActivityManager.RunningTaskInfo(); + taskInfo.userId = userId; + taskInfo.taskId = TASK_ID; + taskInfo.topActivityEligibleForLetterboxEducation = eligible; + taskInfo.configuration.windowConfiguration.setBounds(bounds); + return taskInfo; + } +} 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 9f745208d3ed..aaeebef03d0f 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 @@ -16,15 +16,28 @@ package com.android.wm.shell.draganddrop; +import static android.content.ClipDescription.MIMETYPE_APPLICATION_SHORTCUT; +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.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.times; +import static org.mockito.Mockito.verify; +import android.content.ClipData; +import android.content.ClipDescription; import android.content.Context; +import android.content.Intent; import android.os.RemoteException; import android.view.Display; import android.view.DragEvent; import android.view.View; +import android.view.WindowManager; +import android.widget.FrameLayout; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; @@ -33,6 +46,7 @@ import com.android.internal.logging.UiEventLogger; import com.android.launcher3.icons.IconProvider; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.splitscreen.SplitScreenController; import org.junit.Before; import org.junit.Test; @@ -40,6 +54,8 @@ import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import java.util.Optional; + /** * Tests for the drag and drop controller. */ @@ -56,6 +72,9 @@ public class DragAndDropControllerTest { @Mock private UiEventLogger mUiEventLogger; + @Mock + private DragAndDropController.DragAndDropListener mDragAndDropListener; + private DragAndDropController mController; @Before @@ -63,6 +82,7 @@ public class DragAndDropControllerTest { MockitoAnnotations.initMocks(this); mController = new DragAndDropController(mContext, mDisplayController, mUiEventLogger, mock(IconProvider.class), mock(ShellExecutor.class)); + mController.initialize(Optional.of(mock(SplitScreenController.class))); } @Test @@ -77,4 +97,45 @@ public class DragAndDropControllerTest { mController.onDisplayAdded(nonDefaultDisplayId); assertFalse(mController.onDrag(dragLayout, mock(DragEvent.class))); } + + @Test + public void testListenerOnDragStarted() { + final View dragLayout = mock(View.class); + final Display display = mock(Display.class); + doReturn(display).when(dragLayout).getDisplay(); + doReturn(DEFAULT_DISPLAY).when(display).getDisplayId(); + + final ClipData clipData = createClipData(); + final DragEvent event = mock(DragEvent.class); + doReturn(ACTION_DRAG_STARTED).when(event).getAction(); + doReturn(clipData).when(event).getClipData(); + doReturn(clipData.getDescription()).when(event).getClipDescription(); + + mController.addListener(mDragAndDropListener); + + // Ensure there's a target so that onDrag will execute + mController.addDisplayDropTarget(0, mContext, mock(WindowManager.class), + mock(FrameLayout.class), mock(DragLayout.class)); + + // Verify the listener is called on a valid drag action. + mController.onDrag(dragLayout, event); + verify(mDragAndDropListener, times(1)).onDragStarted(); + + // Verify the listener isn't called after removal. + reset(mDragAndDropListener); + mController.removeListener(mDragAndDropListener); + mController.onDrag(dragLayout, event); + verify(mDragAndDropListener, never()).onDragStarted(); + } + + private ClipData createClipData() { + ClipDescription clipDescription = new ClipDescription(MIMETYPE_APPLICATION_SHORTCUT, + new String[] { MIMETYPE_APPLICATION_SHORTCUT }); + Intent i = new Intent(); + i.putExtra(Intent.EXTRA_PACKAGE_NAME, "pkg"); + i.putExtra(Intent.EXTRA_SHORTCUT_ID, "shortcutId"); + i.putExtra(Intent.EXTRA_USER, android.os.Process.myUserHandle()); + ClipData.Item item = new ClipData.Item(i); + return new ClipData(clipDescription, item); + } } 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 fe66e225ad4a..bb6026c36c97 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 @@ -19,11 +19,12 @@ package com.android.wm.shell.draganddrop; import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; -import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY; import static android.content.ClipDescription.MIMETYPE_APPLICATION_ACTIVITY; import static android.content.ClipDescription.MIMETYPE_APPLICATION_SHORTCUT; import static android.content.ClipDescription.MIMETYPE_APPLICATION_TASK; +import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; + 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; @@ -33,6 +34,7 @@ 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; @@ -51,9 +53,11 @@ import android.app.ActivityTaskManager; import android.app.PendingIntent; 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.ResolveInfo; import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Insets; @@ -111,7 +115,6 @@ public class DragAndDropPolicyTest { private ActivityManager.RunningTaskInfo mHomeTask; private ActivityManager.RunningTaskInfo mFullscreenAppTask; private ActivityManager.RunningTaskInfo mNonResizeableFullscreenAppTask; - private ActivityManager.RunningTaskInfo mSplitPrimaryAppTask; @Before public void setUp() throws RemoteException { @@ -144,8 +147,6 @@ public class DragAndDropPolicyTest { mNonResizeableFullscreenAppTask = createTaskInfo(WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD); mNonResizeableFullscreenAppTask.isResizeable = false; - mSplitPrimaryAppTask = createTaskInfo(WINDOWING_MODE_SPLIT_SCREEN_PRIMARY, - ACTIVITY_TYPE_STANDARD); setRunningTask(mFullscreenAppTask); } @@ -181,6 +182,12 @@ public class DragAndDropPolicyTest { info.configuration.windowConfiguration.setActivityType(actType); info.configuration.windowConfiguration.setWindowingMode(winMode); info.isResizeable = true; + info.baseActivity = new ComponentName(getInstrumentation().getContext().getPackageName(), + ".ActivityWithMode" + winMode); + ActivityInfo activityInfo = new ActivityInfo(); + activityInfo.packageName = info.baseActivity.getPackageName(); + activityInfo.name = info.baseActivity.getClassName(); + info.topActivityInfo = activityInfo; return info; } @@ -256,6 +263,62 @@ 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/fullscreen/FullscreenTaskListenerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/fullscreen/FullscreenTaskListenerTest.java index 9cbdf1e2dbb6..4523e2c9cba5 100644 --- 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 @@ -18,6 +18,7 @@ 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; @@ -30,6 +31,7 @@ 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; @@ -47,6 +49,8 @@ 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; @@ -71,6 +75,7 @@ public class FullscreenTaskListenerTest { @Test public void testAnimatableTaskAppeared_notifiesUnfoldController() { + assumeFalse(ENABLE_SHELL_TRANSITIONS); RunningTaskInfo info = createTaskInfo(/* visible */ true, /* taskId */ 0); mListener.onTaskAppeared(info, mSurfaceControl); @@ -80,6 +85,7 @@ public class FullscreenTaskListenerTest { @Test public void testMultipleAnimatableTasksAppeared_notifiesUnfoldController() { + assumeFalse(ENABLE_SHELL_TRANSITIONS); RunningTaskInfo animatable1 = createTaskInfo(/* visible */ true, /* taskId */ 0); RunningTaskInfo animatable2 = createTaskInfo(/* visible */ true, /* taskId */ 1); @@ -93,6 +99,7 @@ public class FullscreenTaskListenerTest { @Test public void testNonAnimatableTaskAppeared_doesNotNotifyUnfoldController() { + assumeFalse(ENABLE_SHELL_TRANSITIONS); RunningTaskInfo info = createTaskInfo(/* visible */ false, /* taskId */ 0); mListener.onTaskAppeared(info, mSurfaceControl); @@ -102,6 +109,7 @@ public class FullscreenTaskListenerTest { @Test public void testNonAnimatableTaskChanged_doesNotNotifyUnfoldController() { + assumeFalse(ENABLE_SHELL_TRANSITIONS); RunningTaskInfo info = createTaskInfo(/* visible */ false, /* taskId */ 0); mListener.onTaskAppeared(info, mSurfaceControl); @@ -112,6 +120,7 @@ public class FullscreenTaskListenerTest { @Test public void testNonAnimatableTaskVanished_doesNotNotifyUnfoldController() { + assumeFalse(ENABLE_SHELL_TRANSITIONS); RunningTaskInfo info = createTaskInfo(/* visible */ false, /* taskId */ 0); mListener.onTaskAppeared(info, mSurfaceControl); @@ -122,6 +131,7 @@ public class FullscreenTaskListenerTest { @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); @@ -133,6 +143,7 @@ public class FullscreenTaskListenerTest { @Test public void testAnimatableTaskVanished_notifiesUnfoldController() { + assumeFalse(ENABLE_SHELL_TRANSITIONS); RunningTaskInfo taskInfo = createTaskInfo(/* visible */ true, /* taskId */ 0); mListener.onTaskAppeared(taskInfo, mSurfaceControl); 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 f10dc16fae5c..b976c1287aca 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 @@ -24,8 +24,8 @@ import android.testing.AndroidTestingRunner; import android.testing.TestableContext; import android.testing.TestableLooper; -import androidx.test.InstrumentationRegistry; import androidx.test.filters.SmallTest; +import androidx.test.platform.app.InstrumentationRegistry; import com.android.wm.shell.common.ShellExecutor; 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 078e2b6cf574..16e92395c85e 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 @@ -45,8 +45,8 @@ import android.window.DisplayAreaOrganizer; import android.window.IWindowContainerToken; import android.window.WindowContainerToken; -import androidx.test.InstrumentationRegistry; import androidx.test.filters.SmallTest; +import androidx.test.platform.app.InstrumentationRegistry; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayLayout; 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 new file mode 100644 index 000000000000..440a6f8fb59a --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/kidsmode/KidsModeTaskOrganizerTest.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.kidsmode; + +import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static android.view.Display.DEFAULT_DISPLAY; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import android.app.ActivityManager; +import android.content.Context; +import android.content.pm.ParceledListSlice; +import android.os.Handler; +import android.os.IBinder; +import android.os.RemoteException; +import android.view.InsetsState; +import android.view.SurfaceControl; +import android.window.ITaskOrganizerController; +import android.window.TaskAppearedInfo; +import android.window.WindowContainerToken; +import android.window.WindowContainerTransaction; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +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 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.Optional; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class KidsModeTaskOrganizerTest { + @Mock private ITaskOrganizerController mTaskOrganizerController; + @Mock private Context mContext; + @Mock private Handler mHandler; + @Mock private SyncTransactionQueue mSyncTransactionQueue; + @Mock private ShellExecutor mTestExecutor; + @Mock private DisplayController mDisplayController; + @Mock private SurfaceControl mLeash; + @Mock private WindowContainerToken mToken; + @Mock private WindowContainerTransaction mTransaction; + @Mock private KidsModeSettingsObserver mObserver; + @Mock private StartingWindowController mStartingWindowController; + @Mock private DisplayInsetsController mDisplayInsetsController; + + KidsModeTaskOrganizer mOrganizer; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + try { + doReturn(ParceledListSlice.<TaskAppearedInfo>emptyList()) + .when(mTaskOrganizerController).registerTaskOrganizer(any()); + } 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(mTransaction).when(mOrganizer).getWindowContainerTransaction(); + doReturn(new InsetsState()).when(mDisplayController).getInsetsState(DEFAULT_DISPLAY); + } + + @Test + public void testKidsModeOn() { + doReturn(true).when(mObserver).isEnabled(); + + mOrganizer.updateKidsModeState(); + + verify(mOrganizer, times(1)).enable(); + verify(mOrganizer, times(1)).registerOrganizer(); + verify(mOrganizer, times(1)).createRootTask( + eq(DEFAULT_DISPLAY), eq(WINDOWING_MODE_FULLSCREEN), eq(mOrganizer.mCookie)); + + final ActivityManager.RunningTaskInfo rootTask = createTaskInfo(12, + WINDOWING_MODE_FULLSCREEN, mOrganizer.mCookie); + mOrganizer.onTaskAppeared(rootTask, mLeash); + + assertThat(mOrganizer.mLaunchRootLeash).isEqualTo(mLeash); + assertThat(mOrganizer.mLaunchRootTask).isEqualTo(rootTask); + } + + @Test + public void testKidsModeOff() { + doReturn(true).when(mObserver).isEnabled(); + mOrganizer.updateKidsModeState(); + final ActivityManager.RunningTaskInfo rootTask = createTaskInfo(12, + WINDOWING_MODE_FULLSCREEN, mOrganizer.mCookie); + mOrganizer.onTaskAppeared(rootTask, mLeash); + + doReturn(false).when(mObserver).isEnabled(); + mOrganizer.updateKidsModeState(); + + + verify(mOrganizer, times(1)).disable(); + verify(mOrganizer, times(1)).unregisterOrganizer(); + verify(mOrganizer, times(1)).deleteRootTask(rootTask.token); + assertThat(mOrganizer.mLaunchRootLeash).isNull(); + assertThat(mOrganizer.mLaunchRootTask).isNull(); + } + + private ActivityManager.RunningTaskInfo createTaskInfo( + int taskId, int windowingMode, IBinder cookies) { + ActivityManager.RunningTaskInfo taskInfo = new ActivityManager.RunningTaskInfo(); + taskInfo.taskId = taskId; + taskInfo.token = mToken; + taskInfo.configuration.windowConfiguration.setWindowingMode(windowingMode); + final ArrayList<IBinder> launchCookies = new ArrayList<>(); + if (cookies != null) { + launchCookies.add(cookies); + } + taskInfo.launchCookies = launchCookies; + return taskInfo; + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/BackgroundWindowManagerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/BackgroundWindowManagerTest.java new file mode 100644 index 000000000000..f3f70673b332 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/BackgroundWindowManagerTest.java @@ -0,0 +1,61 @@ +/** + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.onehanded; + +import static com.google.common.truth.Truth.assertThat; + +import android.testing.TestableLooper; + +import androidx.test.annotation.UiThreadTest; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import com.android.wm.shell.ShellTestCase; +import com.android.wm.shell.common.DisplayLayout; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** Tests for {@link BackgroundWindowManager} */ +@SmallTest +@TestableLooper.RunWithLooper(setAsMainLooper = true) +@RunWith(AndroidJUnit4.class) +public class BackgroundWindowManagerTest extends ShellTestCase { + private BackgroundWindowManager mBackgroundWindowManager; + @Mock + private DisplayLayout mMockDisplayLayout; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + mBackgroundWindowManager = new BackgroundWindowManager(mContext); + mBackgroundWindowManager.onDisplayChanged(mMockDisplayLayout); + } + + @Test + @UiThreadTest + public void testInitRelease() { + mBackgroundWindowManager.initView(); + assertThat(mBackgroundWindowManager.getSurfaceControl()).isNotNull(); + + mBackgroundWindowManager.removeBackgroundLayer(); + assertThat(mBackgroundWindowManager.getSurfaceControl()).isNull(); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedBackgroundPanelOrganizerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedBackgroundPanelOrganizerTest.java deleted file mode 100644 index 7b9553c5ef9b..000000000000 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedBackgroundPanelOrganizerTest.java +++ /dev/null @@ -1,131 +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.onehanded; - -import static android.view.Display.DEFAULT_DISPLAY; -import static android.window.DisplayAreaOrganizer.FEATURE_ONE_HANDED_BACKGROUND_PANEL; - -import static com.android.wm.shell.onehanded.OneHandedState.STATE_ACTIVE; -import static com.android.wm.shell.onehanded.OneHandedState.STATE_NONE; - -import static com.google.common.truth.Truth.assertThat; - -import static org.mockito.ArgumentMatchers.anyInt; -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; - -import android.testing.AndroidTestingRunner; -import android.testing.TestableLooper; -import android.view.Display; -import android.view.SurfaceControl; -import android.window.DisplayAreaInfo; -import android.window.IWindowContainerToken; -import android.window.WindowContainerToken; - -import androidx.test.filters.SmallTest; - -import com.android.wm.shell.common.DisplayController; -import com.android.wm.shell.common.DisplayLayout; - -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 -public class OneHandedBackgroundPanelOrganizerTest extends OneHandedTestCase { - private DisplayAreaInfo mDisplayAreaInfo; - private Display mDisplay; - private DisplayLayout mDisplayLayout; - private OneHandedBackgroundPanelOrganizer mSpiedBackgroundPanelOrganizer; - private WindowContainerToken mToken; - private SurfaceControl mLeash; - - @Mock - IWindowContainerToken mMockRealToken; - @Mock - DisplayController mMockDisplayController; - @Mock - OneHandedSettingsUtil mMockSettingsUtil; - - @Before - public void setUp() { - MockitoAnnotations.initMocks(this); - mToken = new WindowContainerToken(mMockRealToken); - mLeash = new SurfaceControl(); - mDisplay = mContext.getDisplay(); - mDisplayLayout = new DisplayLayout(mContext, mDisplay); - when(mMockDisplayController.getDisplay(anyInt())).thenReturn(mDisplay); - mDisplayAreaInfo = new DisplayAreaInfo(mToken, DEFAULT_DISPLAY, - FEATURE_ONE_HANDED_BACKGROUND_PANEL); - - mSpiedBackgroundPanelOrganizer = spy( - new OneHandedBackgroundPanelOrganizer(mContext, mDisplayLayout, mMockSettingsUtil, - Runnable::run)); - mSpiedBackgroundPanelOrganizer.onDisplayChanged(mDisplayLayout); - } - - @Test - public void testOnDisplayAreaAppeared() { - mSpiedBackgroundPanelOrganizer.onDisplayAreaAppeared(mDisplayAreaInfo, mLeash); - - assertThat(mSpiedBackgroundPanelOrganizer.isRegistered()).isTrue(); - verify(mSpiedBackgroundPanelOrganizer, never()).showBackgroundPanelLayer(); - } - - @Test - public void testShowBackgroundLayer() { - mSpiedBackgroundPanelOrganizer.onDisplayAreaAppeared(mDisplayAreaInfo, null); - mSpiedBackgroundPanelOrganizer.onStart(); - - verify(mSpiedBackgroundPanelOrganizer).showBackgroundPanelLayer(); - } - - @Test - public void testRemoveBackgroundLayer() { - mSpiedBackgroundPanelOrganizer.onDisplayAreaAppeared(mDisplayAreaInfo, mLeash); - - assertThat(mSpiedBackgroundPanelOrganizer.isRegistered()).isNotNull(); - - reset(mSpiedBackgroundPanelOrganizer); - mSpiedBackgroundPanelOrganizer.removeBackgroundPanelLayer(); - - assertThat(mSpiedBackgroundPanelOrganizer.mBackgroundSurface).isNull(); - } - - @Test - public void testStateNone_onConfigurationChanged() { - mSpiedBackgroundPanelOrganizer.onStateChanged(STATE_NONE); - mSpiedBackgroundPanelOrganizer.onConfigurationChanged(); - - verify(mSpiedBackgroundPanelOrganizer, never()).showBackgroundPanelLayer(); - } - - @Test - public void testStateActivate_onConfigurationChanged() { - mSpiedBackgroundPanelOrganizer.onStateChanged(STATE_ACTIVE); - mSpiedBackgroundPanelOrganizer.onConfigurationChanged(); - - verify(mSpiedBackgroundPanelOrganizer).showBackgroundPanelLayer(); - } -} 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 0a3a84923053..ecf1c5d41864 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 @@ -46,6 +46,7 @@ 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; @@ -55,6 +56,7 @@ 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; @SmallTest @@ -63,16 +65,15 @@ public class OneHandedControllerTest extends OneHandedTestCase { private int mCurrentUser = UserHandle.myUserId(); Display mDisplay; - DisplayLayout mDisplayLayout; OneHandedAccessibilityUtil mOneHandedAccessibilityUtil; OneHandedController mSpiedOneHandedController; OneHandedTimeoutHandler mSpiedTimeoutHandler; OneHandedState mSpiedTransitionState; @Mock - DisplayController mMockDisplayController; + DisplayLayout mDisplayLayout; @Mock - OneHandedBackgroundPanelOrganizer mMockBackgroundOrganizer; + DisplayController mMockDisplayController; @Mock OneHandedDisplayAreaOrganizer mMockDisplayAreaOrganizer; @Mock @@ -86,6 +87,8 @@ public class OneHandedControllerTest extends OneHandedTestCase { @Mock OneHandedUiEventLogger mMockUiEventLogger; @Mock + InteractionJankMonitor mMockJankMonitor; + @Mock IOverlayManager mMockOverlayManager; @Mock TaskStackListenerImpl mMockTaskStackListener; @@ -104,14 +107,14 @@ public class OneHandedControllerTest extends OneHandedTestCase { public void setUp() { MockitoAnnotations.initMocks(this); mDisplay = mContext.getDisplay(); - mDisplayLayout = new DisplayLayout(mContext, mDisplay); + mDisplayLayout = Mockito.mock(DisplayLayout.class); mSpiedTimeoutHandler = spy(new OneHandedTimeoutHandler(mMockShellMainExecutor)); mSpiedTransitionState = spy(new OneHandedState()); when(mMockDisplayController.getDisplay(anyInt())).thenReturn(mDisplay); + when(mMockDisplayController.getDisplayLayout(anyInt())).thenReturn(null); when(mMockDisplayAreaOrganizer.getDisplayAreaTokenMap()).thenReturn(new ArrayMap<>()); when(mMockDisplayAreaOrganizer.isReady()).thenReturn(true); - when(mMockBackgroundOrganizer.isRegistered()).thenReturn(true); when(mMockSettingsUitl.getSettingsOneHandedModeEnabled(any(), anyInt())).thenReturn( mDefaultEnabled); when(mMockSettingsUitl.getSettingsOneHandedModeTimeout(any(), anyInt())).thenReturn( @@ -123,14 +126,13 @@ public class OneHandedControllerTest extends OneHandedTestCase { when(mMockSettingsUitl.getShortcutEnabled(any(), anyInt())).thenReturn(false); when(mMockDisplayAreaOrganizer.getLastDisplayBounds()).thenReturn( - new Rect(0, 0, mDisplayLayout.width(), mDisplayLayout.height())); + new Rect(0, 0, 1080, 2400)); when(mMockDisplayAreaOrganizer.getDisplayLayout()).thenReturn(mDisplayLayout); mOneHandedAccessibilityUtil = new OneHandedAccessibilityUtil(mContext); mSpiedOneHandedController = spy(new OneHandedController( mContext, mMockDisplayController, - mMockBackgroundOrganizer, mMockDisplayAreaOrganizer, mMockTouchHandler, mMockTutorialHandler, @@ -138,6 +140,7 @@ public class OneHandedControllerTest extends OneHandedTestCase { mOneHandedAccessibilityUtil, mSpiedTimeoutHandler, mSpiedTransitionState, + mMockJankMonitor, mMockUiEventLogger, mMockOverlayManager, mMockTaskStackListener, @@ -153,6 +156,13 @@ public class OneHandedControllerTest extends OneHandedTestCase { } @Test + public void testNullDisplayLayout() { + mSpiedOneHandedController.updateDisplayLayout(0); + + verify(mMockDisplayAreaOrganizer, never()).setDisplayLayout(any()); + } + + @Test public void testStartOneHandedShouldTriggerScheduleOffset() { mSpiedTransitionState.setState(STATE_NONE); mSpiedOneHandedController.setOneHandedEnabled(true); @@ -294,10 +304,9 @@ public class OneHandedControllerTest extends OneHandedTestCase { @Test public void testRotation90CanNotStartOneHanded() { - final DisplayLayout landscapeDisplayLayout = new DisplayLayout(mDisplayLayout); - landscapeDisplayLayout.rotateTo(mContext.getResources(), Surface.ROTATION_90); + mDisplayLayout.rotateTo(mContext.getResources(), Surface.ROTATION_90); mSpiedTransitionState.setState(STATE_NONE); - when(mMockDisplayAreaOrganizer.getDisplayLayout()).thenReturn(landscapeDisplayLayout); + when(mDisplayLayout.isLandscape()).thenReturn(true); mSpiedOneHandedController.setOneHandedEnabled(true); mSpiedOneHandedController.setLockedDisabled(false /* locked */, false /* enabled */); mSpiedOneHandedController.startOneHanded(); @@ -307,11 +316,10 @@ public class OneHandedControllerTest extends OneHandedTestCase { @Test public void testRotation180CanStartOneHanded() { - final DisplayLayout testDisplayLayout = new DisplayLayout(mDisplayLayout); - testDisplayLayout.rotateTo(mContext.getResources(), Surface.ROTATION_180); + mDisplayLayout.rotateTo(mContext.getResources(), Surface.ROTATION_180); mSpiedTransitionState.setState(STATE_NONE); when(mMockDisplayAreaOrganizer.isReady()).thenReturn(true); - when(mMockDisplayAreaOrganizer.getDisplayLayout()).thenReturn(testDisplayLayout); + when(mDisplayLayout.isLandscape()).thenReturn(false); mSpiedOneHandedController.setOneHandedEnabled(true); mSpiedOneHandedController.setLockedDisabled(false /* locked */, false /* enabled */); mSpiedOneHandedController.startOneHanded(); @@ -321,10 +329,9 @@ public class OneHandedControllerTest extends OneHandedTestCase { @Test public void testRotation270CanNotStartOneHanded() { - final DisplayLayout testDisplayLayout = new DisplayLayout(mDisplayLayout); - testDisplayLayout.rotateTo(mContext.getResources(), Surface.ROTATION_270); + mDisplayLayout.rotateTo(mContext.getResources(), Surface.ROTATION_270); mSpiedTransitionState.setState(STATE_NONE); - when(mMockDisplayAreaOrganizer.getDisplayLayout()).thenReturn(testDisplayLayout); + when(mDisplayLayout.isLandscape()).thenReturn(true); mSpiedOneHandedController.setOneHandedEnabled(true); mSpiedOneHandedController.setLockedDisabled(false /* locked */, false /* enabled */); mSpiedOneHandedController.startOneHanded(); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedDisplayAreaOrganizerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedDisplayAreaOrganizerTest.java index ef16fd391235..9c7f7237871a 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedDisplayAreaOrganizerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedDisplayAreaOrganizerTest.java @@ -50,6 +50,7 @@ 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; @@ -94,11 +95,11 @@ public class OneHandedDisplayAreaOrganizerTest extends OneHandedTestCase { @Mock WindowContainerTransaction mMockWindowContainerTransaction; @Mock - OneHandedBackgroundPanelOrganizer mMockBackgroundOrganizer; - @Mock ShellExecutor mMockShellMainExecutor; @Mock OneHandedSettingsUtil mMockSettingsUitl; + @Mock + InteractionJankMonitor mJankMonitor; List<DisplayAreaAppearedInfo> mDisplayAreaAppearedInfoList = new ArrayList<>(); @@ -140,7 +141,7 @@ public class OneHandedDisplayAreaOrganizerTest extends OneHandedTestCase { mMockSettingsUitl, mMockAnimationController, mTutorialHandler, - mMockBackgroundOrganizer, + mJankMonitor, mMockShellMainExecutor)); for (int i = 0; i < DISPLAYAREA_INFO_COUNT; i++) { @@ -427,9 +428,16 @@ public class OneHandedDisplayAreaOrganizerTest extends OneHandedTestCase { mMockSettingsUitl, mMockAnimationController, mTutorialHandler, - mMockBackgroundOrganizer, + mJankMonitor, mMockShellMainExecutor)); assertThat(testSpiedDisplayAreaOrganizer.isReady()).isFalse(); } + + @Test + public void testDisplayArea_setDisplayLayout_should_updateDisplayBounds() { + mSpiedDisplayAreaOrganizer.setDisplayLayout(mDisplayLayout); + + verify(mSpiedDisplayAreaOrganizer).updateDisplayBounds(); + } } 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 bea69c5d80ef..dba1b8b86261 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 @@ -40,6 +40,7 @@ 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; @@ -66,8 +67,6 @@ public class OneHandedStateTest extends OneHandedTestCase { @Mock DisplayController mMockDisplayController; @Mock - OneHandedBackgroundPanelOrganizer mMockBackgroundOrganizer; - @Mock OneHandedDisplayAreaOrganizer mMockDisplayAreaOrganizer; @Mock OneHandedTouchHandler mMockTouchHandler; @@ -78,6 +77,8 @@ public class OneHandedStateTest extends OneHandedTestCase { @Mock OneHandedUiEventLogger mMockUiEventLogger; @Mock + InteractionJankMonitor mMockJankMonitor; + @Mock IOverlayManager mMockOverlayManager; @Mock TaskStackListenerImpl mMockTaskStackListener; @@ -102,7 +103,6 @@ public class OneHandedStateTest extends OneHandedTestCase { when(mMockDisplayController.getDisplay(anyInt())).thenReturn(mDisplay); when(mMockDisplayAreaOrganizer.getDisplayAreaTokenMap()).thenReturn(new ArrayMap<>()); - when(mMockBackgroundOrganizer.isRegistered()).thenReturn(true); when(mMockSettingsUitl.getSettingsOneHandedModeEnabled(any(), anyInt())).thenReturn( mDefaultEnabled); when(mMockSettingsUitl.getSettingsOneHandedModeTimeout(any(), anyInt())).thenReturn( @@ -120,7 +120,6 @@ public class OneHandedStateTest extends OneHandedTestCase { mSpiedOneHandedController = spy(new OneHandedController( mContext, mMockDisplayController, - mMockBackgroundOrganizer, mMockDisplayAreaOrganizer, mMockTouchHandler, mMockTutorialHandler, @@ -128,6 +127,7 @@ public class OneHandedStateTest extends OneHandedTestCase { mOneHandedAccessibilityUtil, mSpiedTimeoutHandler, mSpiedState, + mMockJankMonitor, mMockUiEventLogger, mMockOverlayManager, mMockTaskStackListener, diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedTutorialHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedTutorialHandlerTest.java index b1434ca325b7..63d8bfd1e7ef 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedTutorialHandlerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedTutorialHandlerTest.java @@ -56,6 +56,8 @@ public class OneHandedTutorialHandlerTest extends OneHandedTestCase { OneHandedSettingsUtil mMockSettingsUtil; @Mock WindowManager mMockWindowManager; + @Mock + BackgroundWindowManager mMockBackgroundWindowManager; @Before public void setUp() { @@ -63,10 +65,11 @@ public class OneHandedTutorialHandlerTest extends OneHandedTestCase { when(mMockSettingsUtil.getTutorialShownCounts(any(), anyInt())).thenReturn(0); mDisplay = mContext.getDisplay(); - mDisplayLayout = new DisplayLayout(mContext, mDisplay); + mDisplayLayout = new DisplayLayout(getTestContext().getApplicationContext(), mDisplay); mSpiedTransitionState = spy(new OneHandedState()); mSpiedTutorialHandler = spy( - new OneHandedTutorialHandler(mContext, mMockSettingsUtil, mMockWindowManager)); + new OneHandedTutorialHandler(mContext, mMockSettingsUtil, mMockWindowManager, + mMockBackgroundWindowManager)); mTimeoutHandler = new OneHandedTimeoutHandler(mMockShellMainExecutor); } 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 8ef1df606b43..c685fdc1f09c 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 @@ -30,7 +30,6 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.verify; import android.app.TaskInfo; -import android.graphics.Matrix; import android.graphics.Rect; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; @@ -104,7 +103,7 @@ 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(DummySurfaceControlTx::new); + oldAnimator.setSurfaceControlTransactionFactory(PipDummySurfaceControlTx::new); oldAnimator.start(); final PipAnimationController.PipTransitionAnimator newAnimator = mPipAnimationController @@ -134,7 +133,7 @@ public class PipAnimationControllerTest extends ShellTestCase { @Test public void pipTransitionAnimator_rotatedEndValue() { - final DummySurfaceControlTx tx = new DummySurfaceControlTx(); + final PipDummySurfaceControlTx tx = new PipDummySurfaceControlTx(); final Rect startBounds = new Rect(200, 700, 400, 800); final Rect endBounds = new Rect(0, 0, 500, 1000); // Fullscreen to PiP. @@ -184,7 +183,7 @@ 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(DummySurfaceControlTx::new); + animator.setSurfaceControlTransactionFactory(PipDummySurfaceControlTx::new); animator.setPipAnimationCallback(mPipAnimationCallback); @@ -201,44 +200,4 @@ public class PipAnimationControllerTest extends ShellTestCase { verify(mPipAnimationCallback).onPipAnimationEnd(eq(mTaskInfo), any(SurfaceControl.Transaction.class), eq(animator)); } - - /** - * A dummy {@link SurfaceControl.Transaction} class. - * This is created as {@link Mock} does not support method chaining. - */ - public static class DummySurfaceControlTx 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 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/PipBoundsAlgorithmTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsAlgorithmTest.java index 90f898aa09da..0059846c6055 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 @@ -29,6 +29,7 @@ import android.view.Gravity; import androidx.test.filters.SmallTest; +import com.android.wm.shell.R; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.common.DisplayLayout; @@ -72,16 +73,16 @@ public class PipBoundsAlgorithmTest extends ShellTestCase { private void initializeMockResources() { final TestableResources res = mContext.getOrCreateTestableResources(); res.addOverride( - com.android.internal.R.dimen.config_pictureInPictureDefaultAspectRatio, + R.dimen.config_pictureInPictureDefaultAspectRatio, DEFAULT_ASPECT_RATIO); res.addOverride( - com.android.internal.R.integer.config_defaultPictureInPictureGravity, + R.integer.config_defaultPictureInPictureGravity, Gravity.END | Gravity.BOTTOM); res.addOverride( - com.android.internal.R.dimen.default_minimal_size_pip_resizable_task, + R.dimen.default_minimal_size_pip_resizable_task, DEFAULT_MIN_EDGE_SIZE); res.addOverride( - com.android.internal.R.string.config_defaultPictureInPictureScreenEdgeInsets, + R.string.config_defaultPictureInPictureScreenEdgeInsets, "16x16"); res.addOverride( com.android.internal.R.dimen.config_pictureInPictureMinAspectRatio, @@ -107,7 +108,7 @@ public class PipBoundsAlgorithmTest extends ShellTestCase { public void onConfigurationChanged_reloadResources() { final float newDefaultAspectRatio = (DEFAULT_ASPECT_RATIO + MAX_ASPECT_RATIO) / 2; final TestableResources res = mContext.getOrCreateTestableResources(); - res.addOverride(com.android.internal.R.dimen.config_pictureInPictureDefaultAspectRatio, + res.addOverride(R.dimen.config_pictureInPictureDefaultAspectRatio, newDefaultAspectRatio); mPipBoundsAlgorithm.onConfigurationChanged(mContext); @@ -463,7 +464,7 @@ public class PipBoundsAlgorithmTest extends ShellTestCase { private void overrideDefaultAspectRatio(float aspectRatio) { final TestableResources res = mContext.getOrCreateTestableResources(); res.addOverride( - com.android.internal.R.dimen.config_pictureInPictureDefaultAspectRatio, + R.dimen.config_pictureInPictureDefaultAspectRatio, aspectRatio); mPipBoundsAlgorithm.onConfigurationChanged(mContext); } @@ -471,7 +472,7 @@ public class PipBoundsAlgorithmTest extends ShellTestCase { private void overrideDefaultStackGravity(int stackGravity) { final TestableResources res = mContext.getOrCreateTestableResources(); res.addOverride( - com.android.internal.R.integer.config_defaultPictureInPictureGravity, + R.integer.config_defaultPictureInPictureGravity, stackGravity); mPipBoundsAlgorithm.onConfigurationChanged(mContext); } 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 new file mode 100644 index 000000000000..ccf8f6e03844 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipDummySurfaceControlTx.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY 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 0172cf324eea..e8e6254697c2 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,10 +21,12 @@ 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.anyFloat; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; @@ -48,7 +50,6 @@ import com.android.wm.shell.TestShellExecutor; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.SyncTransactionQueue; -import com.android.wm.shell.legacysplitscreen.LegacySplitScreenController; import com.android.wm.shell.pip.phone.PhonePipMenuController; import com.android.wm.shell.splitscreen.SplitScreenController; @@ -76,9 +77,9 @@ public class PipTaskOrganizerTest extends ShellTestCase { @Mock private PipTransitionController mMockPipTransitionController; @Mock private PipSurfaceTransactionHelper mMockPipSurfaceTransactionHelper; @Mock private PipUiEventLogger mMockPipUiEventLogger; - @Mock private Optional<LegacySplitScreenController> mMockOptionalLegacySplitScreen; @Mock private Optional<SplitScreenController> mMockOptionalSplitScreen; @Mock private ShellTaskOrganizer mMockShellTaskOrganizer; + @Mock private PipParamsChangedForwarder mMockPipParamsChangedForwarder; private TestShellExecutor mMainExecutor; private PipBoundsState mPipBoundsState; private PipTransitionState mPipTransitionState; @@ -99,11 +100,10 @@ public class PipTaskOrganizerTest extends ShellTestCase { mMainExecutor = new TestShellExecutor(); mSpiedPipTaskOrganizer = spy(new PipTaskOrganizer(mContext, mMockSyncTransactionQueue, mPipTransitionState, mPipBoundsState, - mPipBoundsAlgorithm, mMockPhonePipMenuController, - mMockPipAnimationController, mMockPipSurfaceTransactionHelper, - mMockPipTransitionController, mMockOptionalLegacySplitScreen, - mMockOptionalSplitScreen, mMockDisplayController, mMockPipUiEventLogger, - mMockShellTaskOrganizer, mMainExecutor)); + mPipBoundsAlgorithm, mMockPhonePipMenuController, mMockPipAnimationController, + mMockPipSurfaceTransactionHelper, mMockPipTransitionController, + mMockPipParamsChangedForwarder, mMockOptionalSplitScreen, mMockDisplayController, + mMockPipUiEventLogger, mMockShellTaskOrganizer, mMainExecutor)); mMainExecutor.flushAll(); preparePipTaskOrg(); } @@ -183,11 +183,12 @@ public class PipTaskOrganizerTest extends ShellTestCase { // It is in entering transition, should defer onTaskInfoChanged callback in this case. mSpiedPipTaskOrganizer.onTaskInfoChanged(createTaskInfo(mComponent1, createPipParams(newAspectRatio))); - assertEquals(startAspectRatio.floatValue(), mPipBoundsState.getAspectRatio(), 0.01f); + 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); - assertEquals(newAspectRatio.floatValue(), mPipBoundsState.getAspectRatio(), 0.01f); + verify(mMockPipParamsChangedForwarder) + .notifyAspectRatioChanged(newAspectRatio.floatValue()); } @Test @@ -201,7 +202,8 @@ public class PipTaskOrganizerTest extends ShellTestCase { mSpiedPipTaskOrganizer.onTaskInfoChanged(createTaskInfo(mComponent1, createPipParams(newAspectRatio))); - assertEquals(newAspectRatio.floatValue(), mPipBoundsState.getAspectRatio(), 0.01f); + verify(mMockPipParamsChangedForwarder) + .notifyAspectRatioChanged(newAspectRatio.floatValue()); } @Test @@ -244,6 +246,7 @@ public class PipTaskOrganizerTest extends ShellTestCase { 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()); } 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 935f6695538d..df18133adcfb 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 @@ -45,9 +45,11 @@ import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.TaskStackListenerImpl; import com.android.wm.shell.onehanded.OneHandedController; +import com.android.wm.shell.pip.PipAppOpsListener; import com.android.wm.shell.pip.PipBoundsAlgorithm; import com.android.wm.shell.pip.PipBoundsState; 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; @@ -59,6 +61,7 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import java.util.Optional; +import java.util.Set; /** * Unit tests for {@link PipController} @@ -84,6 +87,7 @@ public class PipControllerTest extends ShellTestCase { @Mock private TaskStackListenerImpl mMockTaskStackListener; @Mock private ShellExecutor mMockExecutor; @Mock private Optional<OneHandedController> mMockOneHandedController; + @Mock private PipParamsChangedForwarder mPipParamsChangedForwarder; @Mock private DisplayLayout mMockDisplayLayout1; @Mock private DisplayLayout mMockDisplayLayout2; @@ -96,10 +100,12 @@ public class PipControllerTest extends ShellTestCase { return null; }).when(mMockExecutor).execute(any()); mPipController = new PipController(mContext, mMockDisplayController, - mMockPipAppOpsListener, mMockPipBoundsAlgorithm, mMockPipBoundsState, - mMockPipMediaController, mMockPhonePipMenuController, mMockPipTaskOrganizer, - mMockPipTouchHandler, mMockPipTransitionController, mMockWindowManagerShellWrapper, - mMockTaskStackListener, mMockOneHandedController, mMockExecutor); + mMockPipAppOpsListener, mMockPipBoundsAlgorithm, + mMockPipBoundsState, mMockPipMediaController, + mMockPhonePipMenuController, mMockPipTaskOrganizer, mMockPipTouchHandler, + mMockPipTransitionController, mMockWindowManagerShellWrapper, + mMockTaskStackListener, mPipParamsChangedForwarder, + mMockOneHandedController, mMockExecutor); when(mMockPipBoundsAlgorithm.getSnapAlgorithm()).thenReturn(mMockPipSnapAlgorithm); when(mMockPipTouchHandler.getMotionHelper()).thenReturn(mMockPipMotionHelper); } @@ -127,10 +133,12 @@ public class PipControllerTest extends ShellTestCase { when(spyContext.getPackageManager()).thenReturn(mockPackageManager); assertNull(PipController.create(spyContext, mMockDisplayController, - mMockPipAppOpsListener, mMockPipBoundsAlgorithm, mMockPipBoundsState, - mMockPipMediaController, mMockPhonePipMenuController, mMockPipTaskOrganizer, - mMockPipTouchHandler, mMockPipTransitionController, mMockWindowManagerShellWrapper, - mMockTaskStackListener, mMockOneHandedController, mMockExecutor)); + mMockPipAppOpsListener, mMockPipBoundsAlgorithm, + mMockPipBoundsState, mMockPipMediaController, + mMockPhonePipMenuController, mMockPipTaskOrganizer, mMockPipTouchHandler, + mMockPipTransitionController, mMockWindowManagerShellWrapper, + mMockTaskStackListener, mPipParamsChangedForwarder, + mMockOneHandedController, mMockExecutor)); } @Test @@ -209,4 +217,16 @@ public class PipControllerTest extends ShellTestCase { verify(mMockPipMotionHelper, never()).movePip(any(Rect.class)); } + + @Test + public void onKeepClearAreasChanged_updatesPipBoundsState() { + final int displayId = 1; + final Rect keepClearArea = new Rect(0, 0, 10, 10); + when(mMockPipBoundsState.getDisplayId()).thenReturn(displayId); + + mPipController.mDisplaysChangedListener.onKeepClearAreasChanged( + displayId, Set.of(keepClearArea), Set.of()); + + verify(mMockPipBoundsState).setKeepClearAreas(Set.of(keepClearArea), Set.of()); + } } 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 new file mode 100644 index 000000000000..05e472245b4a --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipBoundsControllerTest.kt @@ -0,0 +1,255 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.pip.tv + +import android.content.Context +import android.content.res.Resources +import android.graphics.Rect +import android.os.Handler +import android.os.test.TestLooper +import android.testing.AndroidTestingRunner + +import com.android.wm.shell.R +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 + +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.AdditionalAnswers.returnsFirstArg +import org.mockito.Mock +import org.mockito.Mockito.`when` as whenever +import org.mockito.Mockito.any +import org.mockito.Mockito.anyInt +import org.mockito.Mockito.eq +import org.mockito.Mockito.never +import org.mockito.Mockito.reset +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +@RunWith(AndroidTestingRunner::class) +class TvPipBoundsControllerTest { + val ANIMATION_DURATION = 100 + val STASH_DURATION = 5000 + val FAR_FUTURE = 60 * 60000L + val ANCHOR_BOUNDS = Rect(90, 90, 100, 100) + val STASHED_BOUNDS = Rect(99, 90, 109, 100) + val MOVED_BOUNDS = Rect(90, 80, 100, 90) + val STASHED_MOVED_BOUNDS = Rect(99, 80, 109, 90) + val ANCHOR_PLACEMENT = Placement(ANCHOR_BOUNDS, ANCHOR_BOUNDS) + val STASHED_PLACEMENT = Placement(STASHED_BOUNDS, ANCHOR_BOUNDS, + STASH_TYPE_RIGHT, ANCHOR_BOUNDS, false) + val STASHED_PLACEMENT_RESTASH = Placement(STASHED_BOUNDS, ANCHOR_BOUNDS, + STASH_TYPE_RIGHT, ANCHOR_BOUNDS, true) + val MOVED_PLACEMENT = Placement(MOVED_BOUNDS, ANCHOR_BOUNDS) + val STASHED_MOVED_PLACEMENT = Placement(STASHED_MOVED_BOUNDS, ANCHOR_BOUNDS, + STASH_TYPE_RIGHT, MOVED_BOUNDS, false) + val STASHED_MOVED_PLACEMENT_RESTASH = Placement(STASHED_MOVED_BOUNDS, ANCHOR_BOUNDS, + STASH_TYPE_RIGHT, MOVED_BOUNDS, true) + + lateinit var boundsController: TvPipBoundsController + var time = 0L + lateinit var testLooper: TestLooper + lateinit var mainHandler: Handler + + var inMenu = false + var inMoveMode = false + + @Mock + lateinit var context: Context + @Mock + lateinit var resources: Resources + @Mock + lateinit var tvPipBoundsState: TvPipBoundsState + @Mock + lateinit var tvPipBoundsAlgorithm: TvPipBoundsAlgorithm + @Mock + lateinit var listener: TvPipBoundsController.PipBoundsListener + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + time = 0L + inMenu = false + inMoveMode = false + + testLooper = TestLooper { time } + mainHandler = Handler(testLooper.getLooper()) + + whenever(context.resources).thenReturn(resources) + whenever(resources.getInteger(R.integer.config_pipStashDuration)).thenReturn(STASH_DURATION) + whenever(tvPipBoundsAlgorithm.adjustBoundsForTemporaryDecor(any())) + .then(returnsFirstArg<Rect>()) + + boundsController = TvPipBoundsController( + context, + { time }, + mainHandler, + tvPipBoundsState, + tvPipBoundsAlgorithm) + boundsController.setListener(listener) + } + + @Test + fun testPlacement_MovedAfterDebounceTimeout() { + triggerPlacement(MOVED_PLACEMENT) + assertMovementAt(POSITION_DEBOUNCE_TIMEOUT_MILLIS, MOVED_BOUNDS) + assertNoMovementUpTo(time + FAR_FUTURE) + } + + @Test + fun testStashedPlacement_MovedAfterDebounceTimeout_Unstashes() { + triggerPlacement(STASHED_PLACEMENT_RESTASH) + assertMovementAt(POSITION_DEBOUNCE_TIMEOUT_MILLIS, STASHED_BOUNDS) + assertMovementAt(POSITION_DEBOUNCE_TIMEOUT_MILLIS + STASH_DURATION, ANCHOR_BOUNDS) + } + + @Test + fun testDebounceSamePlacement_MovesDebounceTimeoutAfterFirstPlacement() { + triggerPlacement(MOVED_PLACEMENT) + advanceTimeTo(POSITION_DEBOUNCE_TIMEOUT_MILLIS / 2) + triggerPlacement(MOVED_PLACEMENT) + + assertMovementAt(POSITION_DEBOUNCE_TIMEOUT_MILLIS, MOVED_BOUNDS) + } + + @Test + fun testNoMovementUntilPlacementStabilizes() { + triggerPlacement(ANCHOR_PLACEMENT) + advanceTimeTo(time + POSITION_DEBOUNCE_TIMEOUT_MILLIS / 10) + triggerPlacement(MOVED_PLACEMENT) + advanceTimeTo(time + POSITION_DEBOUNCE_TIMEOUT_MILLIS / 10) + triggerPlacement(ANCHOR_PLACEMENT) + advanceTimeTo(time + POSITION_DEBOUNCE_TIMEOUT_MILLIS / 10) + triggerPlacement(MOVED_PLACEMENT) + + assertMovementAt(time + POSITION_DEBOUNCE_TIMEOUT_MILLIS, MOVED_BOUNDS) + } + + @Test + fun testUnstashIfStashNoLongerNecessary() { + triggerPlacement(STASHED_PLACEMENT_RESTASH) + assertMovementAt(POSITION_DEBOUNCE_TIMEOUT_MILLIS, STASHED_BOUNDS) + + triggerPlacement(ANCHOR_PLACEMENT) + assertMovementAt(time + POSITION_DEBOUNCE_TIMEOUT_MILLIS, ANCHOR_BOUNDS) + } + + @Test + fun testRestashingPlacementDelaysUnstash() { + triggerPlacement(STASHED_PLACEMENT_RESTASH) + assertMovementAt(POSITION_DEBOUNCE_TIMEOUT_MILLIS, STASHED_BOUNDS) + + assertNoMovementUpTo(time + STASH_DURATION / 2) + triggerPlacement(STASHED_PLACEMENT_RESTASH) + assertNoMovementUpTo(time + POSITION_DEBOUNCE_TIMEOUT_MILLIS) + assertMovementAt(time + STASH_DURATION, ANCHOR_BOUNDS) + } + + @Test + fun testNonRestashingPlacementDoesNotDelayUnstash() { + triggerPlacement(STASHED_PLACEMENT_RESTASH) + assertMovementAt(POSITION_DEBOUNCE_TIMEOUT_MILLIS, STASHED_BOUNDS) + + assertNoMovementUpTo(time + STASH_DURATION / 2) + triggerPlacement(STASHED_PLACEMENT) + assertMovementAt(POSITION_DEBOUNCE_TIMEOUT_MILLIS + STASH_DURATION, ANCHOR_BOUNDS) + } + + @Test + fun testImmediatePlacement() { + triggerImmediatePlacement(STASHED_PLACEMENT_RESTASH) + assertMovement(STASHED_BOUNDS) + assertMovementAt(time + STASH_DURATION, ANCHOR_BOUNDS) + } + + @Test + fun testInMoveMode_KeepAtAnchor() { + startMoveMode() + triggerImmediatePlacement(STASHED_MOVED_PLACEMENT_RESTASH) + assertMovement(ANCHOR_BOUNDS) + assertNoMovementUpTo(time + FAR_FUTURE) + } + + @Test + fun testInMenu_Unstashed() { + openPipMenu() + triggerImmediatePlacement(STASHED_MOVED_PLACEMENT_RESTASH) + assertMovement(MOVED_BOUNDS) + assertNoMovementUpTo(time + FAR_FUTURE) + } + + @Test + fun testCloseMenu_DoNotRestash() { + openPipMenu() + triggerImmediatePlacement(STASHED_MOVED_PLACEMENT_RESTASH) + assertMovement(MOVED_BOUNDS) + + closePipMenu() + triggerPlacement(STASHED_MOVED_PLACEMENT) + assertNoMovementUpTo(time + FAR_FUTURE) + } + + fun assertMovement(bounds: Rect) { + verify(listener).onPipTargetBoundsChange(eq(bounds), anyInt()) + reset(listener) + } + + fun assertMovementAt(timeMs: Long, bounds: Rect) { + assertNoMovementUpTo(timeMs - 1) + advanceTimeTo(timeMs) + assertMovement(bounds) + } + + fun assertNoMovementUpTo(timeMs: Long) { + advanceTimeTo(timeMs) + verify(listener, never()).onPipTargetBoundsChange(any(), anyInt()) + } + + fun triggerPlacement(placement: Placement, immediate: Boolean = false) { + whenever(tvPipBoundsAlgorithm.getTvPipPlacement()).thenReturn(placement) + val stayAtAnchorPosition = inMoveMode + val disallowStashing = inMenu || stayAtAnchorPosition + boundsController.recalculatePipBounds(stayAtAnchorPosition, disallowStashing, + ANIMATION_DURATION, immediate) + } + + fun triggerImmediatePlacement(placement: Placement) { + triggerPlacement(placement, true) + } + + fun openPipMenu() { + inMenu = true + inMoveMode = false + } + + fun closePipMenu() { + inMenu = false + inMoveMode = false + } + + fun startMoveMode() { + inMenu = true + inMoveMode = true + } + + fun advanceTimeTo(ms: Long) { + time = ms + testLooper.dispatchAll() + } +} 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 new file mode 100644 index 000000000000..0fcc5cf384c9 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipKeepClearAlgorithmTest.kt @@ -0,0 +1,534 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.graphics.Insets +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.pip.PipBoundsState.STASH_TYPE_BOTTOM +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 + +@RunWith(AndroidTestingRunner::class) +class TvPipKeepClearAlgorithmTest { + private val DEFAULT_PIP_SIZE = Size(384, 216) + private val EXPANDED_WIDE_PIP_SIZE = Size(384*2, 216) + private val DASHBOARD_WIDTH = 484 + private val BOTTOM_SHEET_HEIGHT = 524 + private val STASH_OFFSET = 64 + private val PADDING = 16 + private val SCREEN_SIZE = Size(1920, 1080) + private val SCREEN_EDGE_INSET = 50 + + private lateinit var pipSize: Size + private lateinit var movementBounds: Rect + private lateinit var algorithm: TvPipKeepClearAlgorithm + private var restrictedAreas = mutableSetOf<Rect>() + private var unrestrictedAreas = mutableSetOf<Rect>() + private var gravity: Int = 0 + + @Before + fun setup() { + movementBounds = Rect(0, 0, SCREEN_SIZE.width, SCREEN_SIZE.height) + movementBounds.inset(SCREEN_EDGE_INSET, SCREEN_EDGE_INSET) + + restrictedAreas.clear() + unrestrictedAreas.clear() + pipSize = DEFAULT_PIP_SIZE + gravity = Gravity.BOTTOM or Gravity.RIGHT + + algorithm = TvPipKeepClearAlgorithm() + algorithm.setScreenSize(SCREEN_SIZE) + algorithm.setMovementBounds(movementBounds) + algorithm.pipAreaPadding = PADDING + algorithm.stashOffset = STASH_OFFSET + algorithm.setGravity(gravity) + algorithm.maxRestrictedDistanceFraction = 0.3 + } + + @Test + fun testAnchorPosition_BottomRight() { + gravity = Gravity.BOTTOM or Gravity.RIGHT + testAnchorPosition() + } + + @Test + fun testAnchorPosition_TopRight() { + gravity = Gravity.TOP or Gravity.RIGHT + testAnchorPosition() + } + + @Test + fun testAnchorPosition_TopLeft() { + gravity = Gravity.TOP or Gravity.LEFT + testAnchorPosition() + } + + @Test + fun testAnchorPosition_BottomLeft() { + gravity = Gravity.BOTTOM or Gravity.LEFT + testAnchorPosition() + } + + @Test + fun testAnchorPosition_Right() { + gravity = Gravity.RIGHT + testAnchorPosition() + } + + @Test + fun testAnchorPosition_Left() { + gravity = Gravity.LEFT + testAnchorPosition() + } + + @Test + fun testAnchorPosition_Top() { + gravity = Gravity.TOP + testAnchorPosition() + } + + @Test + fun testAnchorPosition_Bottom() { + gravity = Gravity.BOTTOM + testAnchorPosition() + } + + @Test + fun testAnchorPosition_TopCenterHorizontal() { + gravity = Gravity.TOP or Gravity.CENTER_HORIZONTAL + testAnchorPosition() + } + + @Test + fun testAnchorPosition_BottomCenterHorizontal() { + gravity = Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL + testAnchorPosition() + } + + @Test + fun testAnchorPosition_RightCenterVertical() { + gravity = Gravity.RIGHT or Gravity.CENTER_VERTICAL + testAnchorPosition() + } + + @Test + fun testAnchorPosition_LeftCenterVertical() { + gravity = Gravity.LEFT or Gravity.CENTER_VERTICAL + testAnchorPosition() + } + + fun testAnchorPosition() { + val placement = getActualPlacement() + + assertEquals(getExpectedAnchorBounds(), placement.bounds) + assertNotStashed(placement) + } + + @Test + fun test_AnchorBottomRight_KeepClearNotObstructing_StayAtAnchor() { + gravity = Gravity.BOTTOM or Gravity.RIGHT + + val sidebar = makeSideBar(DASHBOARD_WIDTH, Gravity.LEFT) + unrestrictedAreas.add(sidebar) + + val expectedBounds = getExpectedAnchorBounds() + + val placement = getActualPlacement() + assertEquals(expectedBounds, placement.bounds) + assertNotStashed(placement) + } + + @Test + fun test_AnchorBottomRight_UnrestrictedRightSidebar_PushedLeft() { + gravity = Gravity.BOTTOM or 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_AnchorTopRight_UnrestrictedRightSidebar_PushedLeft() { + gravity = Gravity.TOP or 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_AnchorBottomLeft_UnrestrictedRightSidebar_StayAtAnchor() { + gravity = Gravity.BOTTOM or Gravity.LEFT + + val sidebar = makeSideBar(DASHBOARD_WIDTH, Gravity.RIGHT) + unrestrictedAreas.add(sidebar) + + val expectedBounds = anchorBoundsOffsetBy(0, 0) + + val placement = getActualPlacement() + assertEquals(expectedBounds, placement.bounds) + assertNotStashed(placement) + } + + @Test + fun test_AnchorBottom_UnrestrictedRightSidebar_StayAtAnchor() { + gravity = Gravity.BOTTOM + + val sidebar = makeSideBar(DASHBOARD_WIDTH, Gravity.RIGHT) + unrestrictedAreas.add(sidebar) + + val expectedBounds = anchorBoundsOffsetBy(0, 0) + + val placement = getActualPlacement() + assertEquals(expectedBounds, placement.bounds) + assertNotStashed(placement) + } + + @Test + fun testExpanded_AnchorBottom_UnrestrictedRightSidebar_StayAtAnchor() { + pipSize = EXPANDED_WIDE_PIP_SIZE + gravity = Gravity.BOTTOM + + val sidebar = makeSideBar(DASHBOARD_WIDTH, Gravity.RIGHT) + unrestrictedAreas.add(sidebar) + + val expectedBounds = anchorBoundsOffsetBy(0, 0) + + val placement = getActualPlacement() + assertEquals(expectedBounds, placement.bounds) + assertNotStashed(placement) + } + + @Test + fun test_AnchorBottomRight_RestrictedSmallBottomBar_PushedUp() { + gravity = Gravity.BOTTOM or Gravity.RIGHT + + val bottomBar = makeBottomBar(96) + restrictedAreas.add(bottomBar) + + val expectedBounds = anchorBoundsOffsetBy(0, + SCREEN_EDGE_INSET - bottomBar.height() - PADDING) + + val placement = getActualPlacement() + assertEquals(expectedBounds, placement.bounds) + assertNotStashed(placement) + } + + @Test + fun test_AnchorBottomRight_RestrictedBottomSheet_StashDownAtAnchor() { + gravity = Gravity.BOTTOM or Gravity.RIGHT + + val bottomBar = makeBottomBar(BOTTOM_SHEET_HEIGHT) + restrictedAreas.add(bottomBar) + + val expectedBounds = getExpectedAnchorBounds() + expectedBounds.offsetTo(expectedBounds.left, SCREEN_SIZE.height - STASH_OFFSET) + + val placement = getActualPlacement() + assertEquals(expectedBounds, placement.bounds) + assertEquals(STASH_TYPE_BOTTOM, placement.stashType) + assertEquals(getExpectedAnchorBounds(), placement.unstashDestinationBounds) + assertTrue(placement.triggerStash) + } + + @Test + fun test_AnchorBottomRight_UnrestrictedBottomSheet_PushUp() { + gravity = Gravity.BOTTOM or Gravity.RIGHT + + val bottomBar = makeBottomBar(BOTTOM_SHEET_HEIGHT) + unrestrictedAreas.add(bottomBar) + + val expectedBounds = anchorBoundsOffsetBy(0, + SCREEN_EDGE_INSET - bottomBar.height() - PADDING) + + val placement = getActualPlacement() + assertEquals(expectedBounds, placement.bounds) + assertNotStashed(placement) + } + + @Test + fun test_AnchorBottomRight_UnrestrictedBottomSheet_RestrictedSidebar_StashAboveBottomSheet() { + gravity = Gravity.BOTTOM or Gravity.RIGHT + + val bottomBar = makeBottomBar(BOTTOM_SHEET_HEIGHT) + unrestrictedAreas.add(bottomBar) + + val maxRestrictedHorizontalPush = + (algorithm.maxRestrictedDistanceFraction * SCREEN_SIZE.width).toInt() + val sideBar = makeSideBar(maxRestrictedHorizontalPush + 100, Gravity.RIGHT) + restrictedAreas.add(sideBar) + + val expectedUnstashBounds = + anchorBoundsOffsetBy(0, SCREEN_EDGE_INSET - bottomBar.height() - PADDING) + + val expectedBounds = Rect(expectedUnstashBounds) + expectedBounds.offsetTo(SCREEN_SIZE.width - STASH_OFFSET, expectedBounds.top) + + val placement = getActualPlacement() + assertEquals(expectedBounds, placement.bounds) + assertEquals(STASH_TYPE_RIGHT, placement.stashType) + assertEquals(expectedUnstashBounds, placement.unstashDestinationBounds) + assertTrue(placement.triggerStash) + } + + @Test + fun test_AnchorBottomRight_UnrestrictedBottomSheet_UnrestrictedSidebar_PushUpLeft() { + gravity = Gravity.BOTTOM or Gravity.RIGHT + + val bottomBar = makeBottomBar(BOTTOM_SHEET_HEIGHT) + unrestrictedAreas.add(bottomBar) + + val maxRestrictedHorizontalPush = + (algorithm.maxRestrictedDistanceFraction * SCREEN_SIZE.width).toInt() + val sideBar = makeSideBar(maxRestrictedHorizontalPush + 100, Gravity.RIGHT) + unrestrictedAreas.add(sideBar) + + val expectedBounds = anchorBoundsOffsetBy( + SCREEN_EDGE_INSET - sideBar.width() - PADDING, + SCREEN_EDGE_INSET - bottomBar.height() - PADDING + ) + + val placement = getActualPlacement() + assertEquals(expectedBounds, placement.bounds) + assertNotStashed(placement) + } + + @Test + fun test_Stashed_UnstashBoundsBecomeUnobstructed_Unstashes() { + gravity = Gravity.BOTTOM or Gravity.RIGHT + + val bottomBar = makeBottomBar(BOTTOM_SHEET_HEIGHT) + unrestrictedAreas.add(bottomBar) + + val maxRestrictedHorizontalPush = + (algorithm.maxRestrictedDistanceFraction * SCREEN_SIZE.width).toInt() + val sideBar = makeSideBar(maxRestrictedHorizontalPush + 100, Gravity.RIGHT) + restrictedAreas.add(sideBar) + + val expectedUnstashBounds = + anchorBoundsOffsetBy(0, SCREEN_EDGE_INSET - bottomBar.height() - PADDING) + + val expectedBounds = Rect(expectedUnstashBounds) + expectedBounds.offsetTo(SCREEN_SIZE.width - STASH_OFFSET, expectedBounds.top) + + var placement = getActualPlacement() + assertEquals(expectedBounds, placement.bounds) + assertEquals(STASH_TYPE_RIGHT, placement.stashType) + assertEquals(expectedUnstashBounds, placement.unstashDestinationBounds) + assertTrue(placement.triggerStash) + + restrictedAreas.remove(sideBar) + placement = getActualPlacement() + assertEquals(expectedUnstashBounds, placement.bounds) + assertNotStashed(placement) + } + + @Test + fun test_Stashed_UnstashBoundsStaysObstructed_DoesNotTriggerStash() { + gravity = Gravity.BOTTOM or Gravity.RIGHT + + val bottomBar = makeBottomBar(BOTTOM_SHEET_HEIGHT) + unrestrictedAreas.add(bottomBar) + + val maxRestrictedHorizontalPush = + (algorithm.maxRestrictedDistanceFraction * SCREEN_SIZE.width).toInt() + val sideBar = makeSideBar(maxRestrictedHorizontalPush + 100, Gravity.RIGHT) + restrictedAreas.add(sideBar) + + val expectedUnstashBounds = + anchorBoundsOffsetBy(0, SCREEN_EDGE_INSET - bottomBar.height() - PADDING) + + val expectedBounds = Rect(expectedUnstashBounds) + expectedBounds.offsetTo(SCREEN_SIZE.width - STASH_OFFSET, expectedBounds.top) + + var placement = getActualPlacement() + assertEquals(expectedBounds, placement.bounds) + assertEquals(STASH_TYPE_RIGHT, placement.stashType) + assertEquals(expectedUnstashBounds, placement.unstashDestinationBounds) + assertTrue(placement.triggerStash) + + placement = getActualPlacement() + assertEquals(expectedBounds, placement.bounds) + assertEquals(STASH_TYPE_RIGHT, placement.stashType) + assertEquals(expectedUnstashBounds, placement.unstashDestinationBounds) + assertFalse(placement.triggerStash) + } + + @Test + fun test_Stashed_UnstashBoundsObstructionChanges_UnstashTimeExtended() { + gravity = Gravity.BOTTOM or Gravity.RIGHT + + val bottomBar = makeBottomBar(BOTTOM_SHEET_HEIGHT) + unrestrictedAreas.add(bottomBar) + + val maxRestrictedHorizontalPush = + (algorithm.maxRestrictedDistanceFraction * SCREEN_SIZE.width).toInt() + val sideBar = makeSideBar(maxRestrictedHorizontalPush + 100, Gravity.RIGHT) + restrictedAreas.add(sideBar) + + val expectedUnstashBounds = + anchorBoundsOffsetBy(0, SCREEN_EDGE_INSET - bottomBar.height() - PADDING) + + val expectedBounds = Rect(expectedUnstashBounds) + expectedBounds.offsetTo(SCREEN_SIZE.width - STASH_OFFSET, expectedBounds.top) + + var placement = getActualPlacement() + assertEquals(expectedBounds, placement.bounds) + assertEquals(STASH_TYPE_RIGHT, placement.stashType) + assertEquals(expectedUnstashBounds, placement.unstashDestinationBounds) + assertTrue(placement.triggerStash) + + val newObstruction = Rect( + 0, + expectedUnstashBounds.top, + expectedUnstashBounds.right, + expectedUnstashBounds.bottom + ) + restrictedAreas.add(newObstruction) + + placement = getActualPlacement() + assertEquals(expectedBounds, placement.bounds) + assertEquals(STASH_TYPE_RIGHT, placement.stashType) + assertEquals(expectedUnstashBounds, placement.unstashDestinationBounds) + assertTrue(placement.triggerStash) + } + + @Test + fun test_ExpandedPiPHeightExceedsMovementBounds_AtAnchor() { + gravity = Gravity.RIGHT or Gravity.CENTER_VERTICAL + pipSize = Size(DEFAULT_PIP_SIZE.width, SCREEN_SIZE.height) + testAnchorPosition() + } + + @Test + fun test_ExpandedPiPHeightExceedsMovementBounds_BottomBar_StashedUp() { + gravity = Gravity.RIGHT or Gravity.CENTER_VERTICAL + pipSize = Size(DEFAULT_PIP_SIZE.width, SCREEN_SIZE.height) + val bottomBar = makeBottomBar(96) + unrestrictedAreas.add(bottomBar) + + val expectedBounds = getExpectedAnchorBounds() + expectedBounds.offset(0, -bottomBar.height() - PADDING) + val placement = getActualPlacement() + assertEquals(expectedBounds, placement.bounds) + assertEquals(STASH_TYPE_TOP, placement.stashType) + assertEquals(getExpectedAnchorBounds(), placement.unstashDestinationBounds) + } + + @Test + fun test_PipInsets() { + val insets = Insets.of(-1, -2, -3, -4) + algorithm.setPipPermanentDecorInsets(insets) + + gravity = Gravity.BOTTOM or Gravity.RIGHT + testAnchorPositionWithInsets(insets) + + gravity = Gravity.BOTTOM or Gravity.LEFT + testAnchorPositionWithInsets(insets) + + gravity = Gravity.TOP or Gravity.LEFT + testAnchorPositionWithInsets(insets) + + gravity = Gravity.TOP or Gravity.RIGHT + testAnchorPositionWithInsets(insets) + + pipSize = EXPANDED_WIDE_PIP_SIZE + + gravity = Gravity.BOTTOM + testAnchorPositionWithInsets(insets) + + gravity = Gravity.TOP + testAnchorPositionWithInsets(insets) + + pipSize = Size(pipSize.height, pipSize.width) + + gravity = Gravity.LEFT + testAnchorPositionWithInsets(insets) + + gravity = Gravity.RIGHT + testAnchorPositionWithInsets(insets) + } + + private fun testAnchorPositionWithInsets(insets: Insets) { + var pipRect = Rect(0, 0, pipSize.width, pipSize.height) + pipRect.inset(insets) + var expectedBounds = Rect() + Gravity.apply(gravity, pipRect.width(), pipRect.height(), movementBounds, expectedBounds) + val reverseInsets = Insets.subtract(Insets.NONE, insets) + expectedBounds.inset(reverseInsets) + + var placement = getActualPlacement() + assertEquals(expectedBounds, placement.bounds) + } + + private fun makeSideBar(width: Int, @Gravity.GravityFlags side: Int): Rect { + val sidebar = Rect(0, 0, width, SCREEN_SIZE.height) + if (side == Gravity.RIGHT) { + sidebar.offsetTo(SCREEN_SIZE.width - width, 0) + } + return sidebar + } + + private fun makeBottomBar(height: Int): Rect { + return Rect(0, SCREEN_SIZE.height - height, SCREEN_SIZE.width, SCREEN_SIZE.height) + } + + private fun getExpectedAnchorBounds(): Rect { + val expectedBounds = Rect() + Gravity.apply(gravity, pipSize.width, pipSize.height, movementBounds, expectedBounds) + return expectedBounds + } + + private fun anchorBoundsOffsetBy(dx: Int, dy: Int): Rect { + val bounds = getExpectedAnchorBounds() + bounds.offset(dx, dy) + return bounds + } + + private fun getActualPlacement(): Placement { + algorithm.setGravity(gravity) + return algorithm.calculatePipPosition(pipSize, restrictedAreas, unrestrictedAreas) + } + + private fun assertNotStashed(actual: Placement) { + assertEquals(STASH_TYPE_NONE, actual.stashType) + assertNull(actual.unstashDestinationBounds) + assertFalse(actual.triggerStash) + } +} 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 c9720671f49c..0639ad5d0a62 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 @@ -67,8 +67,7 @@ public class MainStageTests extends ShellTestCase { @Test public void testActiveDeactivate() { - mMainStage.activate(mRootTaskInfo.configuration.windowConfiguration.getBounds(), mWct, - true /* reparent */); + mMainStage.activate(mWct, true /* reparent */); assertThat(mMainStage.isActive()).isTrue(); mMainStage.deactivate(mWct); 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 aab1e3a99c98..eb9d3a11d285 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 @@ -16,23 +16,22 @@ package com.android.wm.shell.splitscreen; -import static android.view.Display.DEFAULT_DISPLAY; -import static android.window.DisplayAreaOrganizer.FEATURE_DEFAULT_TASK_CONTAINER; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; +import android.app.ActivityManager; import android.content.Context; import android.graphics.Rect; import android.view.SurfaceControl; -import android.window.DisplayAreaInfo; -import android.window.IWindowContainerToken; -import android.window.WindowContainerToken; +import android.view.SurfaceSession; import com.android.dx.mockito.inline.extended.ExtendedMockito; -import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.TestRunningTaskInfoBuilder; +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.TransactionPool; import com.android.wm.shell.common.split.SplitLayout; @@ -50,6 +49,7 @@ public class SplitTestUtils { final SurfaceControl leash = createMockSurface(); SplitLayout out = mock(SplitLayout.class); doReturn(dividerBounds).when(out).getDividerBounds(); + doReturn(dividerBounds).when(out).getRefDividerBounds(); doReturn(leash).when(out).getDividerLeash(); return out; } @@ -65,26 +65,26 @@ public class SplitTestUtils { } static class TestStageCoordinator extends StageCoordinator { - final DisplayAreaInfo mDisplayAreaInfo; + final ActivityManager.RunningTaskInfo mRootTask; + final SurfaceControl mRootLeash; TestStageCoordinator(Context context, int displayId, SyncTransactionQueue syncQueue, - RootTaskDisplayAreaOrganizer rootTDAOrganizer, ShellTaskOrganizer taskOrganizer, - MainStage mainStage, SideStage sideStage, DisplayImeController imeController, + ShellTaskOrganizer taskOrganizer, MainStage mainStage, SideStage sideStage, + DisplayController displayController, DisplayImeController imeController, DisplayInsetsController insetsController, SplitLayout splitLayout, Transitions transitions, TransactionPool transactionPool, - SplitscreenEventLogger logger, + SplitscreenEventLogger logger, ShellExecutor mainExecutor, Optional<RecentTasksController> recentTasks, Provider<Optional<StageTaskUnfoldController>> unfoldController) { - super(context, displayId, syncQueue, rootTDAOrganizer, taskOrganizer, mainStage, - sideStage, imeController, insetsController, splitLayout, transitions, - transactionPool, logger, recentTasks, unfoldController); + super(context, displayId, syncQueue, taskOrganizer, mainStage, + sideStage, displayController, imeController, insetsController, splitLayout, + transitions, transactionPool, logger, mainExecutor, recentTasks, + unfoldController); - // Prepare default TaskDisplayArea for testing. - mDisplayAreaInfo = new DisplayAreaInfo( - new WindowContainerToken(new IWindowContainerToken.Default()), - DEFAULT_DISPLAY, - FEATURE_DEFAULT_TASK_CONTAINER); - this.onDisplayAreaAppeared(mDisplayAreaInfo); + // Prepare root task for testing. + mRootTask = new TestRunningTaskInfoBuilder().build(); + mRootLeash = new SurfaceControl.Builder(new SurfaceSession()).setName("test").build(); + onTaskAppeared(mRootTask, mRootLeash); } } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java index 1eae625233a0..ffaab652aa99 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 @@ -17,6 +17,7 @@ package com.android.wm.shell.splitscreen; import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; +import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.view.Display.DEFAULT_DISPLAY; import static android.view.WindowManager.TRANSIT_CHANGE; import static android.view.WindowManager.TRANSIT_CLOSE; @@ -24,7 +25,11 @@ 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.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_CHILDREN_TASKS_REPARENT; +import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REORDER; +import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_SIDE; +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_DRAG_DIVIDER; import static com.android.wm.shell.splitscreen.SplitTestUtils.createMockSurface; import static com.android.wm.shell.transition.Transitions.TRANSIT_SPLIT_SCREEN_PAIR_OPEN; @@ -39,7 +44,6 @@ import static org.mockito.Mockito.mock; import android.annotation.NonNull; import android.app.ActivityManager; -import android.graphics.Rect; import android.os.IBinder; import android.os.RemoteException; import android.view.SurfaceControl; @@ -60,13 +64,13 @@ 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.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.TransactionPool; import com.android.wm.shell.common.split.SplitLayout; -import com.android.wm.shell.recents.RecentTasksController; import com.android.wm.shell.transition.Transitions; import org.junit.Before; @@ -85,6 +89,7 @@ public class SplitTransitionTests extends ShellTestCase { @Mock private ShellTaskOrganizer mTaskOrganizer; @Mock private SyncTransactionQueue mSyncQueue; @Mock private RootTaskDisplayAreaOrganizer mRootTDAOrganizer; + @Mock private DisplayController mDisplayController; @Mock private DisplayImeController mDisplayImeController; @Mock private DisplayInsetsController mDisplayInsetsController; @Mock private TransactionPool mTransactionPool; @@ -92,6 +97,7 @@ public class SplitTransitionTests extends ShellTestCase { @Mock private SurfaceSession mSurfaceSession; @Mock private SplitscreenEventLogger mLogger; @Mock private IconProvider mIconProvider; + @Mock private ShellExecutor mMainExecutor; private SplitLayout mSplitLayout; private MainStage mMainStage; private SideStage mSideStage; @@ -119,9 +125,9 @@ public class SplitTransitionTests extends ShellTestCase { mIconProvider, null); mSideStage.onTaskAppeared(new TestRunningTaskInfoBuilder().build(), createMockSurface()); mStageCoordinator = new SplitTestUtils.TestStageCoordinator(mContext, DEFAULT_DISPLAY, - mSyncQueue, mRootTDAOrganizer, mTaskOrganizer, mMainStage, mSideStage, + mSyncQueue, mTaskOrganizer, mMainStage, mSideStage, mDisplayController, mDisplayImeController, mDisplayInsetsController, mSplitLayout, mTransitions, - mTransactionPool, mLogger, Optional.empty(), Optional::empty); + mTransactionPool, mLogger, mMainExecutor, Optional.empty(), Optional::empty); mSplitScreenTransitions = mStageCoordinator.getSplitTransitions(); doAnswer((Answer<IBinder>) invocation -> mock(IBinder.class)) .when(mTransitions).startTransition(anyInt(), any(), any()); @@ -133,6 +139,42 @@ public class SplitTransitionTests extends ShellTestCase { } @Test + @UiThreadTest + public void testLaunchToSide() { + ActivityManager.RunningTaskInfo newTask = new TestRunningTaskInfoBuilder() + .setParentTaskId(mSideStage.mRootTaskInfo.taskId).build(); + ActivityManager.RunningTaskInfo reparentTask = new TestRunningTaskInfoBuilder() + .setParentTaskId(mMainStage.mRootTaskInfo.taskId).build(); + + // Create a request to start a new task in side stage + TransitionRequestInfo request = new TransitionRequestInfo(TRANSIT_TO_FRONT, newTask, null); + IBinder transition = mock(IBinder.class); + WindowContainerTransaction result = + mStageCoordinator.handleRequest(transition, request); + + // it should handle the transition to enter split screen. + assertNotNull(result); + 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); + mSideStage.onTaskAppeared(newTask, createMockSurface()); + mMainStage.onTaskAppeared(reparentTask, createMockSurface()); + boolean accepted = mStageCoordinator.startAnimation(transition, info, + mock(SurfaceControl.Transaction.class), + mock(SurfaceControl.Transaction.class), + mock(Transitions.TransitionFinishCallback.class)); + assertTrue(accepted); + assertTrue(mStageCoordinator.isSplitScreenVisible()); + } + + @Test + @UiThreadTest public void testLaunchPair() { TransitionInfo info = createEnterPairInfo(); @@ -155,6 +197,7 @@ public class SplitTransitionTests extends ShellTestCase { } @Test + @UiThreadTest public void testMonitorInSplit() { enterSplit(); @@ -211,18 +254,21 @@ public class SplitTransitionTests extends ShellTestCase { } @Test - public void testDismissToHome() { + @UiThreadTest + public void testEnterRecents() { enterSplit(); ActivityManager.RunningTaskInfo homeTask = new TestRunningTaskInfoBuilder() - .setActivityType(ACTIVITY_TYPE_HOME).build(); + .setWindowingMode(WINDOWING_MODE_FULLSCREEN) + .setActivityType(ACTIVITY_TYPE_HOME) + .build(); // Create a request to bring home forward TransitionRequestInfo request = new TransitionRequestInfo(TRANSIT_TO_FRONT, homeTask, null); IBinder transition = mock(IBinder.class); WindowContainerTransaction result = mStageCoordinator.handleRequest(transition, request); - assertTrue(containsSplitExit(result)); + assertTrue(result.isEmpty()); // make sure we haven't made any local changes yet (need to wait until transition is ready) assertTrue(mStageCoordinator.isSplitScreenVisible()); @@ -242,10 +288,71 @@ public class SplitTransitionTests extends ShellTestCase { mock(SurfaceControl.Transaction.class), mock(SurfaceControl.Transaction.class), mock(Transitions.TransitionFinishCallback.class)); + assertTrue(mStageCoordinator.isSplitScreenVisible()); + } + + @Test + @UiThreadTest + public void testDismissFromBeingOccluded() { + enterSplit(); + + ActivityManager.RunningTaskInfo normalTask = new TestRunningTaskInfoBuilder() + .setWindowingMode(WINDOWING_MODE_FULLSCREEN) + .build(); + + // Create a request to bring a normal task forward + TransitionRequestInfo request = + new TransitionRequestInfo(TRANSIT_TO_FRONT, normalTask, null); + IBinder transition = mock(IBinder.class); + WindowContainerTransaction result = mStageCoordinator.handleRequest(transition, request); + + assertTrue(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 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); + 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()); } @Test + @UiThreadTest + public void testDismissFromMultiWindowSupport() { + 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, + new WindowContainerTransaction(), mStageCoordinator, + EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW, STAGE_TYPE_SIDE); + boolean accepted = mStageCoordinator.startAnimation(transition, info, + mock(SurfaceControl.Transaction.class), + mock(SurfaceControl.Transaction.class), + mock(Transitions.TransitionFinishCallback.class)); + assertTrue(accepted); + assertFalse(mStageCoordinator.isSplitScreenVisible()); + } + + @Test + @UiThreadTest public void testDismissSnap() { enterSplit(); @@ -256,8 +363,9 @@ public class SplitTransitionTests extends ShellTestCase { TransitionInfo info = new TransitionInfo(TRANSIT_TO_BACK, 0); info.addChange(mainChange); info.addChange(sideChange); - IBinder transition = mStageCoordinator.onSnappedToDismissTransition( - false /* mainStageToTop */); + IBinder transition = mSplitScreenTransitions.startDismissTransition(null, + new WindowContainerTransaction(), mStageCoordinator, EXIT_REASON_DRAG_DIVIDER, + STAGE_TYPE_SIDE); mMainStage.onTaskVanished(mMainChild); mSideStage.onTaskVanished(mSideChild); boolean accepted = mStageCoordinator.startAnimation(transition, info, @@ -269,6 +377,7 @@ public class SplitTransitionTests extends ShellTestCase { } @Test + @UiThreadTest public void testDismissFromAppFinish() { enterSplit(); @@ -320,8 +429,18 @@ public class SplitTransitionTests extends ShellTestCase { mock(SurfaceControl.Transaction.class), mock(SurfaceControl.Transaction.class), mock(Transitions.TransitionFinishCallback.class)); - mMainStage.activate(new Rect(0, 0, 100, 100), new WindowContainerTransaction(), - true /* includingTopTask */); + mMainStage.activate(new WindowContainerTransaction(), true /* includingTopTask */); + } + + private boolean containsSplitEnter(@NonNull WindowContainerTransaction wct) { + for (int i = 0; i < wct.getHierarchyOps().size(); ++i) { + WindowContainerTransaction.HierarchyOp op = wct.getHierarchyOps().get(i); + if (op.getType() == HIERARCHY_OP_TYPE_REORDER + && op.getContainer() == mStageCoordinator.mRootTaskInfo.token.asBinder()) { + return true; + } + } + return false; } private boolean containsSplitExit(@NonNull WindowContainerTransaction wct) { 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 85f6789c3435..42d998f6b0ee 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java @@ -34,25 +34,27 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.app.ActivityManager; +import android.content.res.Configuration; import android.graphics.Rect; -import android.window.DisplayAreaInfo; +import android.view.SurfaceControl; +import android.view.SurfaceSession; import android.window.WindowContainerTransaction; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -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.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.TransactionPool; import com.android.wm.shell.common.split.SplitLayout; @@ -79,8 +81,6 @@ public class StageCoordinatorTests extends ShellTestCase { @Mock private SyncTransactionQueue mSyncQueue; @Mock - private RootTaskDisplayAreaOrganizer mRootTDAOrganizer; - @Mock private MainStage mMainStage; @Mock private SideStage mSideStage; @@ -91,6 +91,8 @@ public class StageCoordinatorTests extends ShellTestCase { @Mock private SplitLayout mSplitLayout; @Mock + private DisplayController mDisplayController; + @Mock private DisplayImeController mDisplayImeController; @Mock private DisplayInsetsController mDisplayInsetsController; @@ -100,20 +102,33 @@ public class StageCoordinatorTests extends ShellTestCase { 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 SurfaceSession mSurfaceSession = new SurfaceSession(); + private SurfaceControl mRootLeash; + private ActivityManager.RunningTaskInfo mRootTask; private StageCoordinator mStageCoordinator; @Before public void setup() { MockitoAnnotations.initMocks(this); - mStageCoordinator = spy(createStageCoordinator(/* splitLayout */ null)); + 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()); when(mSplitLayout.getBounds1()).thenReturn(mBounds1); when(mSplitLayout.getBounds2()).thenReturn(mBounds2); + when(mSplitLayout.isLandscape()).thenReturn(false); + + mRootTask = new TestRunningTaskInfoBuilder().build(); + mRootLeash = new SurfaceControl.Builder(mSurfaceSession).setName("test").build(); + mStageCoordinator.onTaskAppeared(mRootTask, mRootLeash); } @Test @@ -153,35 +168,42 @@ public class StageCoordinatorTests extends ShellTestCase { } @Test - public void testDisplayAreaAppeared_initializesUnfoldControllers() { - mStageCoordinator.onDisplayAreaAppeared(mock(DisplayAreaInfo.class)); - + public void testRootTaskAppeared_initializesUnfoldControllers() { verify(mMainUnfoldController).init(); verify(mSideUnfoldController).init(); + verify(mStageCoordinator).onRootTaskAppeared(); + } + + @Test + public void testRootTaskInfoChanged_updatesSplitLayout() { + mStageCoordinator.onTaskInfoChanged(mRootTask); + + verify(mSplitLayout).updateConfiguration(any(Configuration.class)); } @Test public void testLayoutChanged_topLeftSplitPosition_updatesUnfoldStageBounds() { - mStageCoordinator = createStageCoordinator(mSplitLayout); mStageCoordinator.setSideStagePosition(SPLIT_POSITION_TOP_OR_LEFT, null); clearInvocations(mMainUnfoldController, mSideUnfoldController); mStageCoordinator.onLayoutSizeChanged(mSplitLayout); - verify(mMainUnfoldController).onLayoutChanged(mBounds2); - verify(mSideUnfoldController).onLayoutChanged(mBounds1); + verify(mMainUnfoldController).onLayoutChanged(mBounds2, SPLIT_POSITION_BOTTOM_OR_RIGHT, + false); + verify(mSideUnfoldController).onLayoutChanged(mBounds1, SPLIT_POSITION_TOP_OR_LEFT, false); } @Test public void testLayoutChanged_bottomRightSplitPosition_updatesUnfoldStageBounds() { - mStageCoordinator = createStageCoordinator(mSplitLayout); mStageCoordinator.setSideStagePosition(SPLIT_POSITION_BOTTOM_OR_RIGHT, null); clearInvocations(mMainUnfoldController, mSideUnfoldController); mStageCoordinator.onLayoutSizeChanged(mSplitLayout); - verify(mMainUnfoldController).onLayoutChanged(mBounds1); - verify(mSideUnfoldController).onLayoutChanged(mBounds2); + verify(mMainUnfoldController).onLayoutChanged(mBounds1, SPLIT_POSITION_TOP_OR_LEFT, + false); + verify(mSideUnfoldController).onLayoutChanged(mBounds2, SPLIT_POSITION_BOTTOM_OR_RIGHT, + false); } @Test @@ -286,12 +308,11 @@ public class StageCoordinatorTests extends ShellTestCase { assertEquals(mStageCoordinator.getMainStagePosition(), SPLIT_POSITION_BOTTOM_OR_RIGHT); } - private StageCoordinator createStageCoordinator(SplitLayout splitLayout) { - return new SplitTestUtils.TestStageCoordinator(mContext, DEFAULT_DISPLAY, - mSyncQueue, mRootTDAOrganizer, mTaskOrganizer, mMainStage, mSideStage, - mDisplayImeController, mDisplayInsetsController, splitLayout, - mTransitions, mTransactionPool, mLogger, Optional.empty(), - new UnfoldControllerProvider()); + @Test + public void testFinishEnterSplitScreen_applySurfaceLayout() { + mStageCoordinator.finishEnterSplitScreen(new SurfaceControl.Transaction()); + + verify(mSplitLayout).applySurfaceChanges(any(), any(), any(), any(), any(), eq(false)); } private class UnfoldControllerProvider implements 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 53d5076f5835..157c30bcb6c7 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 @@ -62,7 +62,7 @@ import org.mockito.MockitoAnnotations; @RunWith(AndroidJUnit4.class) public final class StageTaskListenerTests extends ShellTestCase { private static final boolean ENABLE_SHELL_TRANSITIONS = - SystemProperties.getBoolean("persist.debug.shell_transit", false); + SystemProperties.getBoolean("persist.wm.debug.shell_transit", false); @Mock private ShellTaskOrganizer mTaskOrganizer; @@ -132,6 +132,7 @@ 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(); @@ -142,6 +143,7 @@ public final class StageTaskListenerTests extends ShellTestCase { @Test public void testTaskVanished_notifiesUnfoldListener() { + assumeFalse(ENABLE_SHELL_TRANSITIONS); final ActivityManager.RunningTaskInfo task = new TestRunningTaskInfoBuilder().setParentTaskId(mRootTask.taskId).build(); mStageTaskListener.onTaskAppeared(task, mSurfaceControl); 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 d92b12e60780..630d0d2c827c 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,6 +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 org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; @@ -297,6 +299,56 @@ public class StartingSurfaceDrawerTests { assertEquals(mStartingSurfaceDrawer.mStartingWindowRecords.size(), 0); } + @Test + public void testMinimumAnimationDuration() { + final long maxDuration = MAX_ANIMATION_DURATION; + final long minDuration = MINIMAL_ANIMATION_DURATION; + + final long shortDuration = minDuration - 1; + final long medianShortDuration = minDuration + 1; + final long medianLongDuration = maxDuration - 1; + final long longAppDuration = maxDuration + 1; + + // static icon + assertEquals(shortDuration, SplashscreenContentDrawer.getShowingDuration( + 0, shortDuration)); + // median launch + static icon + assertEquals(medianShortDuration, SplashscreenContentDrawer.getShowingDuration( + 0, medianShortDuration)); + // long launch + static icon + assertEquals(longAppDuration, SplashscreenContentDrawer.getShowingDuration( + 0, longAppDuration)); + + // fast launch + animatable icon + assertEquals(shortDuration, SplashscreenContentDrawer.getShowingDuration( + shortDuration, shortDuration)); + assertEquals(minDuration, SplashscreenContentDrawer.getShowingDuration( + medianShortDuration, shortDuration)); + assertEquals(minDuration, SplashscreenContentDrawer.getShowingDuration( + longAppDuration, shortDuration)); + + // median launch + animatable icon + assertEquals(medianShortDuration, SplashscreenContentDrawer.getShowingDuration( + shortDuration, medianShortDuration)); + assertEquals(medianShortDuration, SplashscreenContentDrawer.getShowingDuration( + medianShortDuration, medianShortDuration)); + assertEquals(minDuration, SplashscreenContentDrawer.getShowingDuration( + longAppDuration, medianShortDuration)); + // between min < max launch + animatable icon + assertEquals(medianLongDuration, SplashscreenContentDrawer.getShowingDuration( + medianShortDuration, medianLongDuration)); + assertEquals(maxDuration, SplashscreenContentDrawer.getShowingDuration( + medianLongDuration, medianShortDuration)); + + // long launch + animatable icon + assertEquals(longAppDuration, SplashscreenContentDrawer.getShowingDuration( + shortDuration, longAppDuration)); + assertEquals(longAppDuration, SplashscreenContentDrawer.getShowingDuration( + medianShortDuration, longAppDuration)); + assertEquals(longAppDuration, SplashscreenContentDrawer.getShowingDuration( + longAppDuration, longAppDuration)); + } + private StartingWindowInfo createWindowInfo(int taskId, int themeResId) { StartingWindowInfo windowInfo = new StartingWindowInfo(); final ActivityInfo info = new ActivityInfo(); 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 e39171343bb9..a0b12976b467 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 @@ -46,6 +46,7 @@ import static org.mockito.ArgumentMatchers.any; 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.mock; import static org.mockito.Mockito.times; @@ -54,7 +55,9 @@ import static org.mockito.Mockito.verify; import android.app.ActivityManager.RunningTaskInfo; import android.content.Context; import android.os.Binder; +import android.os.Handler; import android.os.IBinder; +import android.os.Looper; import android.os.RemoteException; import android.view.IDisplayWindowListener; import android.view.IWindowManager; @@ -84,8 +87,6 @@ import com.android.wm.shell.common.TransactionPool; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.stubbing.Answer; import java.util.ArrayList; @@ -93,7 +94,7 @@ import java.util.ArrayList; * Tests for the shell transitions. * * Build/Install/Run: - * atest WMShellUnitTests:ShellTransitionTests + * atest WMShellUnitTests:ShellTransitionTests */ @SmallTest @RunWith(AndroidJUnit4.class) @@ -106,6 +107,7 @@ public class ShellTransitionTests { private final TestShellExecutor mMainExecutor = new TestShellExecutor(); private final ShellExecutor mAnimExecutor = new TestShellExecutor(); private final TestTransitionHandler mDefaultHandler = new TestTransitionHandler(); + private final Handler mMainHandler = new Handler(Looper.getMainLooper()); @Before public void setUp() { @@ -590,6 +592,90 @@ public class ShellTransitionTests { .setRotate().build()) .build(); assertFalse(DefaultTransitionHandler.isRotationSeamless(noTask, displays)); + + // Seamless if display is explicitly seamless. + final TransitionInfo seamlessDisplay = new TransitionInfoBuilder(TRANSIT_CHANGE) + .addChange(new ChangeBuilder(TRANSIT_CHANGE).setFlags(FLAG_IS_DISPLAY) + .setRotate(ROTATION_ANIMATION_SEAMLESS).build()) + .build(); + assertTrue(DefaultTransitionHandler.isRotationSeamless(seamlessDisplay, displays)); + } + + @Test + public void testRunWhenIdle() { + Transitions transitions = createTestTransitions(); + transitions.replaceDefaultHandlerForTest(mDefaultHandler); + + Runnable runnable1 = mock(Runnable.class); + Runnable runnable2 = mock(Runnable.class); + Runnable runnable3 = mock(Runnable.class); + Runnable runnable4 = mock(Runnable.class); + + transitions.runOnIdle(runnable1); + + // runnable1 is executed immediately because there are no active transitions. + verify(runnable1, times(1)).run(); + + clearInvocations(runnable1); + + 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(); + transitions.onTransitionReady(transitToken1, info1, mock(SurfaceControl.Transaction.class), + mock(SurfaceControl.Transaction.class)); + assertEquals(1, mDefaultHandler.activeCount()); + + transitions.runOnIdle(runnable2); + transitions.runOnIdle(runnable3); + + // runnable2 and runnable3 aren't executed immediately because there is an active + // transaction. + + 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(); + transitions.onTransitionReady(transitToken2, info2, mock(SurfaceControl.Transaction.class), + mock(SurfaceControl.Transaction.class)); + assertEquals(1, mDefaultHandler.activeCount()); + + mDefaultHandler.finishAll(); + mMainExecutor.flushAll(); + // first transition finished + verify(mOrganizer, times(1)).finishTransition(eq(transitToken1), any(), any()); + verify(mOrganizer, times(0)).finishTransition(eq(transitToken2), any(), any()); + // But now the "queued" transition is running + assertEquals(1, mDefaultHandler.activeCount()); + + // runnable2 and runnable3 are still not executed because the second transition is still + // active. + verify(runnable2, times(0)).run(); + verify(runnable3, times(0)).run(); + + mDefaultHandler.finishAll(); + mMainExecutor.flushAll(); + verify(mOrganizer, times(1)).finishTransition(eq(transitToken2), any(), any()); + + // runnable2 and runnable3 are executed after the second transition finishes because there + // are no other active transitions, runnable1 isn't executed again. + verify(runnable1, times(0)).run(); + verify(runnable2, times(1)).run(); + verify(runnable3, times(1)).run(); + + clearInvocations(runnable2); + clearInvocations(runnable3); + + transitions.runOnIdle(runnable4); + + // runnable4 is executed immediately because there are no active transitions, all other + // runnables aren't executed again. + verify(runnable1, times(0)).run(); + verify(runnable2, times(0)).run(); + verify(runnable3, times(0)).run(); + verify(runnable4, times(1)).run(); } class TransitionInfoBuilder { @@ -741,7 +827,7 @@ public class ShellTransitionTests { IWindowManager mockWM = mock(IWindowManager.class); final IDisplayWindowListener[] displayListener = new IDisplayWindowListener[1]; try { - doReturn(new int[] {DEFAULT_DISPLAY}).when(mockWM).registerDisplayWindowListener(any()); + doReturn(new int[]{DEFAULT_DISPLAY}).when(mockWM).registerDisplayWindowListener(any()); } catch (RemoteException e) { // No remote stuff happening, so this can't be hit } @@ -752,7 +838,7 @@ public class ShellTransitionTests { private Transitions createTestTransitions() { return new Transitions(mOrganizer, mTransactionPool, createTestDisplayController(), - mContext, mMainExecutor, mAnimExecutor); + mContext, mMainExecutor, mMainHandler, mAnimExecutor); } // // private class TestDisplayController extends DisplayController { diff --git a/libs/androidfw/Android.bp b/libs/androidfw/Android.bp index 63b831de5da1..c80fb188e70f 100644 --- a/libs/androidfw/Android.bp +++ b/libs/androidfw/Android.bp @@ -118,7 +118,7 @@ cc_library { "libz", ], }, - linux_glibc: { + host_linux: { srcs: [ "CursorWindow.cpp", ], diff --git a/libs/androidfw/AssetManager2.cpp b/libs/androidfw/AssetManager2.cpp index 0cde3d1242c8..136fc6ca4e2a 100644 --- a/libs/androidfw/AssetManager2.cpp +++ b/libs/androidfw/AssetManager2.cpp @@ -25,6 +25,7 @@ #include "android-base/logging.h" #include "android-base/stringprintf.h" +#include "androidfw/ResourceTypes.h" #include "androidfw/ResourceUtils.h" #include "androidfw/Util.h" #include "utils/ByteOrder.h" @@ -600,6 +601,7 @@ base::expected<FindEntryResult, NullOrIOError> AssetManager2::FindEntry( return base::unexpected(result.error()); } + bool overlaid = false; if (!stop_at_first_match && !ignore_configuration && !apk_assets_[result->cookie]->IsLoader()) { for (const auto& id_map : package_group.overlays_) { auto overlay_entry = id_map.overlay_res_maps_.Lookup(resid); @@ -616,6 +618,27 @@ base::expected<FindEntryResult, NullOrIOError> AssetManager2::FindEntry( if (UNLIKELY(logging_enabled)) { last_resolution_.steps.push_back( Resolution::Step{Resolution::Step::Type::OVERLAID_INLINE, String8(), result->cookie}); + if (auto path = apk_assets_[result->cookie]->GetPath()) { + const std::string overlay_path = path->data(); + if (IsFabricatedOverlay(overlay_path)) { + // FRRO don't have package name so we use the creating package here. + String8 frro_name = String8("FRRO"); + // Get the first part of it since the expected one should be like + // {overlayPackageName}-{overlayName}-{4 alphanumeric chars}.frro + // under /data/resource-cache/. + const std::string name = overlay_path.substr(overlay_path.rfind('/') + 1); + const size_t end = name.find('-'); + if (frro_name.size() != overlay_path.size() && end != std::string::npos) { + frro_name.append(base::StringPrintf(" created by %s", + name.substr(0 /* pos */, + end).c_str()).c_str()); + } + last_resolution_.best_package_name = frro_name; + } else { + last_resolution_.best_package_name = result->package_name->c_str(); + } + } + overlaid = true; } continue; } @@ -646,6 +669,9 @@ base::expected<FindEntryResult, NullOrIOError> AssetManager2::FindEntry( last_resolution_.steps.push_back( Resolution::Step{Resolution::Step::Type::OVERLAID, overlay_result->config.toString(), overlay_result->cookie}); + last_resolution_.best_package_name = + overlay_result->package_name->c_str(); + overlaid = true; } } } @@ -654,6 +680,10 @@ base::expected<FindEntryResult, NullOrIOError> AssetManager2::FindEntry( last_resolution_.cookie = result->cookie; last_resolution_.type_string_ref = result->type_string_ref; last_resolution_.entry_string_ref = result->entry_string_ref; + last_resolution_.best_config_name = result->config.toString(); + if (!overlaid) { + last_resolution_.best_package_name = result->package_name->c_str(); + } } return result; @@ -671,8 +701,6 @@ base::expected<FindEntryResult, NullOrIOError> AssetManager2::FindEntryInternal( uint32_t best_offset = 0U; uint32_t type_flags = 0U; - std::vector<Resolution::Step> resolution_steps; - // If `desired_config` is not the same as the set configuration or the caller will accept a value // from any configuration, then we cannot use our filtered list of types since it only it contains // types matched to the set configuration. @@ -725,7 +753,7 @@ base::expected<FindEntryResult, NullOrIOError> AssetManager2::FindEntryInternal( resolution_type = Resolution::Step::Type::OVERLAID; } else { if (UNLIKELY(logging_enabled)) { - resolution_steps.push_back(Resolution::Step{Resolution::Step::Type::SKIPPED, + last_resolution_.steps.push_back(Resolution::Step{Resolution::Step::Type::SKIPPED, this_config.toString(), cookie}); } @@ -742,7 +770,7 @@ base::expected<FindEntryResult, NullOrIOError> AssetManager2::FindEntryInternal( if (!offset.has_value()) { if (UNLIKELY(logging_enabled)) { - resolution_steps.push_back(Resolution::Step{Resolution::Step::Type::NO_ENTRY, + last_resolution_.steps.push_back(Resolution::Step{Resolution::Step::Type::NO_ENTRY, this_config.toString(), cookie}); } @@ -806,6 +834,8 @@ void AssetManager2::ResetResourceResolution() const { last_resolution_.steps.clear(); last_resolution_.type_string_ref = StringPoolRef(); last_resolution_.entry_string_ref = StringPoolRef(); + last_resolution_.best_config_name.clear(); + last_resolution_.best_package_name.clear(); } void AssetManager2::SetResourceResolutionLoggingEnabled(bool enabled) { @@ -865,9 +895,34 @@ std::string AssetManager2::GetLastResourceResolution() const { } } + log_stream << "\nBest matching is from " + << (last_resolution_.best_config_name.isEmpty() ? "default" + : last_resolution_.best_config_name) + << " configuration of " << last_resolution_.best_package_name; return log_stream.str(); } +base::expected<uint32_t, NullOrIOError> AssetManager2::GetParentThemeResourceId(uint32_t resid) +const { + auto entry = FindEntry(resid, 0u /* density_override */, + false /* stop_at_first_match */, + false /* ignore_configuration */); + if (!entry.has_value()) { + return base::unexpected(entry.error()); + } + + auto entry_map = std::get_if<incfs::verified_map_ptr<ResTable_map_entry>>(&entry->entry); + if (entry_map == nullptr) { + // Not a bag, nothing to do. + return base::unexpected(std::nullopt); + } + + auto map = *entry_map; + const uint32_t parent_resid = dtohl(map->parent.ident); + + return parent_resid; +} + base::expected<AssetManager2::ResourceName, NullOrIOError> AssetManager2::GetResourceName( uint32_t resid) const { auto result = FindEntry(resid, 0u /* density_override */, true /* stop_at_first_match */, diff --git a/libs/androidfw/LoadedArsc.cpp b/libs/androidfw/LoadedArsc.cpp index 13aff3812767..35b6170fae5b 100644 --- a/libs/androidfw/LoadedArsc.cpp +++ b/libs/androidfw/LoadedArsc.cpp @@ -650,22 +650,25 @@ std::unique_ptr<const LoadedPackage> LoadedPackage::Load(const Chunk& chunk, } // Retrieve all the resource ids belonging to this policy chunk - std::unordered_set<uint32_t> ids; const auto ids_begin = overlayable_child_chunk.data_ptr().convert<ResTable_ref>(); const auto ids_end = ids_begin + dtohl(policy_header->entry_count); + std::unordered_set<uint32_t> ids; + ids.reserve(ids_end - ids_begin); for (auto id_iter = ids_begin; id_iter != ids_end; ++id_iter) { if (!id_iter) { + LOG(ERROR) << "NULL ResTable_ref record??"; return {}; } ids.insert(dtohl(id_iter->ident)); } // Add the pairing of overlayable properties and resource ids to the package - OverlayableInfo overlayable_info{}; - overlayable_info.name = name; - overlayable_info.actor = actor; - overlayable_info.policy_flags = policy_header->policy_flags; - loaded_package->overlayable_infos_.emplace_back(overlayable_info, ids); + OverlayableInfo overlayable_info { + .name = name, + .actor = actor, + .policy_flags = policy_header->policy_flags + }; + loaded_package->overlayable_infos_.emplace_back(std::move(overlayable_info), std::move(ids)); loaded_package->defines_overlayable_ = true; break; } @@ -692,7 +695,6 @@ std::unique_ptr<const LoadedPackage> LoadedPackage::Load(const Chunk& chunk, break; } - std::unordered_set<uint32_t> finalized_ids; const auto lib_alias = child_chunk.header<ResTable_staged_alias_header>(); if (!lib_alias) { LOG(ERROR) << "RES_TABLE_STAGED_ALIAS_TYPE is too small."; @@ -705,8 +707,11 @@ std::unique_ptr<const LoadedPackage> LoadedPackage::Load(const Chunk& chunk, } const auto entry_begin = child_chunk.data_ptr().convert<ResTable_staged_alias_entry>(); const auto entry_end = entry_begin + dtohl(lib_alias->count); + std::unordered_set<uint32_t> finalized_ids; + finalized_ids.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??"; return {}; } auto finalized_id = dtohl(entry_iter->finalizedResId); @@ -717,8 +722,7 @@ std::unique_ptr<const LoadedPackage> LoadedPackage::Load(const Chunk& chunk, } auto staged_id = dtohl(entry_iter->stagedResId); - auto [_, success] = loaded_package->alias_id_map_.insert(std::make_pair(staged_id, - finalized_id)); + auto [_, success] = loaded_package->alias_id_map_.emplace(staged_id, finalized_id); if (!success) { LOG(ERROR) << StringPrintf("Repeated staged resource id '%08x' in staged aliases.", staged_id); diff --git a/libs/androidfw/Locale.cpp b/libs/androidfw/Locale.cpp index 3eedda88fdce..d87a3ce72177 100644 --- a/libs/androidfw/Locale.cpp +++ b/libs/androidfw/Locale.cpp @@ -29,40 +29,33 @@ using ::android::StringPiece; namespace android { -void LocaleValue::set_language(const char* language_chars) { +template <size_t N, class Transformer> +static void safe_transform_copy(const char* source, char (&dest)[N], Transformer t) { size_t i = 0; - while ((*language_chars) != '\0') { - language[i++] = ::tolower(*language_chars); - language_chars++; + while (i < N && (*source) != '\0') { + dest[i++] = t(i, *source); + source++; + } + while (i < N) { + dest[i++] = '\0'; } } +void LocaleValue::set_language(const char* language_chars) { + safe_transform_copy(language_chars, language, [](size_t, char c) { return ::tolower(c); }); +} + void LocaleValue::set_region(const char* region_chars) { - size_t i = 0; - while ((*region_chars) != '\0') { - region[i++] = ::toupper(*region_chars); - region_chars++; - } + safe_transform_copy(region_chars, region, [](size_t, char c) { return ::toupper(c); }); } void LocaleValue::set_script(const char* script_chars) { - size_t i = 0; - while ((*script_chars) != '\0') { - if (i == 0) { - script[i++] = ::toupper(*script_chars); - } else { - script[i++] = ::tolower(*script_chars); - } - script_chars++; - } + safe_transform_copy(script_chars, script, + [](size_t i, char c) { return i ? ::tolower(c) : ::toupper(c); }); } void LocaleValue::set_variant(const char* variant_chars) { - size_t i = 0; - while ((*variant_chars) != '\0') { - variant[i++] = *variant_chars; - variant_chars++; - } + safe_transform_copy(variant_chars, variant, [](size_t, char c) { return c; }); } static inline bool is_alpha(const std::string& str) { @@ -234,6 +227,10 @@ ssize_t LocaleValue::InitFromParts(std::vector<std::string>::iterator iter, return static_cast<ssize_t>(iter - start_iter); } +// Make sure the following memcpy's are properly sized. +static_assert(sizeof(ResTable_config::localeScript) == sizeof(LocaleValue::script)); +static_assert(sizeof(ResTable_config::localeVariant) == sizeof(LocaleValue::variant)); + void LocaleValue::InitFromResTable(const ResTable_config& config) { config.unpackLanguage(language); config.unpackRegion(region); diff --git a/libs/androidfw/LocaleDataTables.cpp b/libs/androidfw/LocaleDataTables.cpp index 8a10599b498f..2c005fd81de5 100644 --- a/libs/androidfw/LocaleDataTables.cpp +++ b/libs/androidfw/LocaleDataTables.cpp @@ -1,60 +1,60 @@ // Auto-generated by ./tools/localedata/extract_icu_data.py const char SCRIPT_CODES[][4] = { - /* 0 */ {'A', 'h', 'o', 'm'}, - /* 1 */ {'A', 'r', 'a', 'b'}, - /* 2 */ {'A', 'r', 'm', 'i'}, - /* 3 */ {'A', 'r', 'm', 'n'}, - /* 4 */ {'A', 'v', 's', 't'}, - /* 5 */ {'B', 'a', 'm', 'u'}, - /* 6 */ {'B', 'a', 's', 's'}, - /* 7 */ {'B', 'e', 'n', 'g'}, - /* 8 */ {'B', 'r', 'a', 'h'}, - /* 9 */ {'C', 'a', 'k', 'm'}, - /* 10 */ {'C', 'a', 'n', 's'}, - /* 11 */ {'C', 'a', 'r', 'i'}, - /* 12 */ {'C', 'h', 'a', 'm'}, - /* 13 */ {'C', 'h', 'e', 'r'}, - /* 14 */ {'C', 'h', 'r', 's'}, - /* 15 */ {'C', 'o', 'p', 't'}, - /* 16 */ {'C', 'p', 'r', 't'}, - /* 17 */ {'C', 'y', 'r', 'l'}, - /* 18 */ {'D', 'e', 'v', 'a'}, - /* 19 */ {'E', 'g', 'y', 'p'}, - /* 20 */ {'E', 't', 'h', 'i'}, - /* 21 */ {'G', 'e', 'o', 'r'}, - /* 22 */ {'G', 'o', 'n', 'g'}, - /* 23 */ {'G', 'o', 'n', 'm'}, - /* 24 */ {'G', 'o', 't', 'h'}, - /* 25 */ {'G', 'r', 'e', 'k'}, - /* 26 */ {'G', 'u', 'j', 'r'}, - /* 27 */ {'G', 'u', 'r', 'u'}, - /* 28 */ {'H', 'a', 'n', 's'}, - /* 29 */ {'H', 'a', 'n', 't'}, - /* 30 */ {'H', 'a', 't', 'r'}, + /* 0 */ {'A', 'g', 'h', 'b'}, + /* 1 */ {'A', 'h', 'o', 'm'}, + /* 2 */ {'A', 'r', 'a', 'b'}, + /* 3 */ {'A', 'r', 'm', 'i'}, + /* 4 */ {'A', 'r', 'm', 'n'}, + /* 5 */ {'A', 'v', 's', 't'}, + /* 6 */ {'B', 'a', 'm', 'u'}, + /* 7 */ {'B', 'a', 's', 's'}, + /* 8 */ {'B', 'e', 'n', 'g'}, + /* 9 */ {'B', 'r', 'a', 'h'}, + /* 10 */ {'C', 'a', 'k', 'm'}, + /* 11 */ {'C', 'a', 'n', 's'}, + /* 12 */ {'C', 'a', 'r', 'i'}, + /* 13 */ {'C', 'h', 'a', 'm'}, + /* 14 */ {'C', 'h', 'e', 'r'}, + /* 15 */ {'C', 'h', 'r', 's'}, + /* 16 */ {'C', 'o', 'p', 't'}, + /* 17 */ {'C', 'p', 'r', 't'}, + /* 18 */ {'C', 'y', 'r', 'l'}, + /* 19 */ {'D', 'e', 'v', 'a'}, + /* 20 */ {'E', 'g', 'y', 'p'}, + /* 21 */ {'E', 't', 'h', 'i'}, + /* 22 */ {'G', 'e', 'o', 'r'}, + /* 23 */ {'G', 'o', 'n', 'g'}, + /* 24 */ {'G', 'o', 'n', 'm'}, + /* 25 */ {'G', 'o', 't', 'h'}, + /* 26 */ {'G', 'r', 'e', 'k'}, + /* 27 */ {'G', 'u', 'j', 'r'}, + /* 28 */ {'G', 'u', 'r', 'u'}, + /* 29 */ {'H', 'a', 'n', 's'}, + /* 30 */ {'H', 'a', 'n', 't'}, /* 31 */ {'H', 'e', 'b', 'r'}, /* 32 */ {'H', 'l', 'u', 'w'}, - /* 33 */ {'H', 'm', 'n', 'g'}, - /* 34 */ {'H', 'm', 'n', 'p'}, - /* 35 */ {'I', 't', 'a', 'l'}, - /* 36 */ {'J', 'p', 'a', 'n'}, - /* 37 */ {'K', 'a', 'l', 'i'}, - /* 38 */ {'K', 'a', 'n', 'a'}, - /* 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'}, + /* 33 */ {'H', 'm', 'n', 'p'}, + /* 34 */ {'I', 't', 'a', 'l'}, + /* 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'}, @@ -68,1430 +68,1439 @@ const char SCRIPT_CODES[][4] = { /* 64 */ {'O', 'r', 'k', 'h'}, /* 65 */ {'O', 'r', 'y', 'a'}, /* 66 */ {'O', 's', 'g', 'e'}, - /* 67 */ {'P', 'a', 'u', 'c'}, - /* 68 */ {'P', 'h', 'l', 'i'}, - /* 69 */ {'P', 'h', 'n', 'x'}, - /* 70 */ {'P', 'l', 'r', 'd'}, - /* 71 */ {'P', 'r', 't', 'i'}, - /* 72 */ {'R', 'u', 'n', 'r'}, - /* 73 */ {'S', 'a', 'm', 'r'}, - /* 74 */ {'S', 'a', 'r', 'b'}, - /* 75 */ {'S', 'a', 'u', 'r'}, - /* 76 */ {'S', 'g', 'n', 'w'}, - /* 77 */ {'S', 'i', 'n', 'h'}, - /* 78 */ {'S', 'o', 'g', 'd'}, - /* 79 */ {'S', 'o', 'r', 'a'}, - /* 80 */ {'S', 'o', 'y', 'o'}, - /* 81 */ {'S', 'y', 'r', 'c'}, - /* 82 */ {'T', 'a', 'l', 'e'}, - /* 83 */ {'T', 'a', 'l', 'u'}, - /* 84 */ {'T', 'a', 'm', 'l'}, - /* 85 */ {'T', 'a', 'n', 'g'}, - /* 86 */ {'T', 'a', 'v', 't'}, - /* 87 */ {'T', 'e', 'l', 'u'}, - /* 88 */ {'T', 'f', 'n', 'g'}, - /* 89 */ {'T', 'h', 'a', 'a'}, - /* 90 */ {'T', 'h', 'a', 'i'}, - /* 91 */ {'T', 'i', 'b', 't'}, - /* 92 */ {'U', 'g', 'a', 'r'}, - /* 93 */ {'V', 'a', 'i', 'i'}, - /* 94 */ {'W', 'c', 'h', 'o'}, - /* 95 */ {'X', 'p', 'e', 'o'}, - /* 96 */ {'X', 's', 'u', 'x'}, - /* 97 */ {'Y', 'i', 'i', 'i'}, - /* 98 */ {'~', '~', '~', 'A'}, - /* 99 */ {'~', '~', '~', 'B'}, + /* 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'}, }; const std::unordered_map<uint32_t, uint8_t> LIKELY_SCRIPTS({ - {0x61610000u, 46u}, // aa -> Latn - {0xA0000000u, 46u}, // aai -> Latn - {0xA8000000u, 46u}, // aak -> Latn - {0xD0000000u, 46u}, // aau -> Latn - {0x61620000u, 17u}, // ab -> Cyrl - {0xA0200000u, 46u}, // abi -> Latn - {0xC0200000u, 17u}, // abq -> Cyrl - {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, 91u}, // adp -> Tibt - {0xE0600000u, 17u}, // ady -> Cyrl - {0xE4600000u, 46u}, // adz -> Latn - {0x61650000u, 4u}, // ae -> Avst - {0x84800000u, 1u}, // aeb -> Arab - {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, 0u}, // aho -> Ahom - {0x99200000u, 46u}, // ajg -> Latn - {0x616B0000u, 46u}, // ak -> Latn - {0xA9400000u, 96u}, // akk -> Xsux - {0x81600000u, 46u}, // ala -> Latn - {0xA1600000u, 46u}, // ali -> Latn - {0xB5600000u, 46u}, // aln -> Latn - {0xCD600000u, 17u}, // alt -> Cyrl - {0x616D0000u, 20u}, // am -> Ethi - {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, 1u}, // apc -> Arab - {0x8DE00000u, 1u}, // apd -> Arab - {0x91E00000u, 46u}, // ape -> Latn - {0xC5E00000u, 46u}, // apr -> Latn - {0xC9E00000u, 46u}, // aps -> Latn - {0xE5E00000u, 46u}, // apz -> Latn - {0x61720000u, 1u}, // ar -> Arab - {0x61725842u, 99u}, // ar-XB -> ~~~B - {0x8A200000u, 2u}, // arc -> Armi - {0x9E200000u, 46u}, // arh -> Latn - {0xB6200000u, 46u}, // arn -> Latn - {0xBA200000u, 46u}, // aro -> Latn - {0xC2200000u, 1u}, // arq -> Arab - {0xCA200000u, 1u}, // ars -> Arab - {0xE2200000u, 1u}, // ary -> Arab - {0xE6200000u, 1u}, // arz -> Arab - {0x61730000u, 7u}, // as -> Beng - {0x82400000u, 46u}, // asa -> Latn - {0x92400000u, 76u}, // 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, 17u}, // av -> Cyrl - {0xAEA00000u, 1u}, // avl -> Arab - {0xB6A00000u, 46u}, // avn -> Latn - {0xCEA00000u, 46u}, // avt -> Latn - {0xD2A00000u, 46u}, // avu -> Latn - {0x82C00000u, 18u}, // awa -> Deva - {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, 1u}, // az-IQ -> Arab - {0x617A4952u, 1u}, // az-IR -> Arab - {0x617A5255u, 17u}, // az-RU -> Cyrl - {0x62610000u, 17u}, // ba -> Cyrl - {0xAC010000u, 1u}, // bal -> Arab - {0xB4010000u, 46u}, // ban -> Latn - {0xBC010000u, 18u}, // bap -> Deva - {0xC4010000u, 46u}, // bar -> Latn - {0xC8010000u, 46u}, // bas -> Latn - {0xD4010000u, 46u}, // bav -> Latn - {0xDC010000u, 5u}, // bax -> Bamu - {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, 20u}, // bcq -> Ethi - {0xD0410000u, 46u}, // bcu -> Latn - {0x8C610000u, 46u}, // bdd -> Latn - {0x62650000u, 17u}, // be -> Cyrl - {0x94810000u, 46u}, // bef -> Latn - {0x9C810000u, 46u}, // beh -> Latn - {0xA4810000u, 1u}, // bej -> Arab - {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, 84u}, // bfq -> Taml - {0xCCA10000u, 1u}, // bft -> Arab - {0xE0A10000u, 18u}, // bfy -> Deva - {0x62670000u, 17u}, // bg -> Cyrl - {0x88C10000u, 18u}, // bgc -> Deva - {0xB4C10000u, 1u}, // bgn -> Arab - {0xDCC10000u, 25u}, // bgx -> Grek - {0x84E10000u, 18u}, // bhb -> Deva - {0x98E10000u, 46u}, // bhg -> Latn - {0xA0E10000u, 18u}, // bhi -> Deva - {0xACE10000u, 46u}, // bhl -> Latn - {0xB8E10000u, 18u}, // bho -> Deva - {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, 20u}, // bji -> Ethi - {0xA5210000u, 18u}, // bjj -> Deva - {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 - {0xCD610000u, 86u}, // blt -> Tavt - {0x626D0000u, 46u}, // bm -> Latn - {0x9D810000u, 46u}, // bmh -> Latn - {0xA9810000u, 46u}, // bmk -> Latn - {0xC1810000u, 46u}, // bmq -> Latn - {0xD1810000u, 46u}, // bmu -> Latn - {0x626E0000u, 7u}, // bn -> Beng - {0x99A10000u, 46u}, // bng -> Latn - {0xB1A10000u, 46u}, // bnm -> Latn - {0xBDA10000u, 46u}, // bnp -> Latn - {0x626F0000u, 91u}, // bo -> Tibt - {0xA5C10000u, 46u}, // boj -> Latn - {0xB1C10000u, 46u}, // bom -> Latn - {0xB5C10000u, 46u}, // bon -> Latn - {0xE1E10000u, 7u}, // bpy -> Beng - {0x8A010000u, 46u}, // bqc -> Latn - {0xA2010000u, 1u}, // bqi -> Arab - {0xBE010000u, 46u}, // bqp -> Latn - {0xD6010000u, 46u}, // bqv -> Latn - {0x62720000u, 46u}, // br -> Latn - {0x82210000u, 18u}, // bra -> Deva - {0x9E210000u, 1u}, // brh -> Arab - {0xDE210000u, 18u}, // brx -> Deva - {0xE6210000u, 46u}, // brz -> Latn - {0x62730000u, 46u}, // bs -> Latn - {0xA6410000u, 46u}, // bsj -> Latn - {0xC2410000u, 6u}, // bsq -> Bass - {0xCA410000u, 46u}, // bss -> Latn - {0xCE410000u, 20u}, // bst -> Ethi - {0xBA610000u, 46u}, // bto -> Latn - {0xCE610000u, 46u}, // btt -> Latn - {0xD6610000u, 18u}, // btv -> Deva - {0x82810000u, 17u}, // bua -> Cyrl - {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, 20u}, // byn -> Ethi - {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, 9u}, // ccp -> Cakm - {0x63650000u, 17u}, // ce -> Cyrl - {0x84820000u, 46u}, // ceb -> Latn - {0x80A20000u, 46u}, // cfa -> Latn - {0x98C20000u, 46u}, // cgg -> Latn - {0x63680000u, 46u}, // ch -> Latn - {0xA8E20000u, 46u}, // chk -> Latn - {0xB0E20000u, 17u}, // chm -> Cyrl - {0xB8E20000u, 46u}, // cho -> Latn - {0xBCE20000u, 46u}, // chp -> Latn - {0xC4E20000u, 13u}, // chr -> Cher - {0x89020000u, 46u}, // cic -> Latn - {0x81220000u, 1u}, // cja -> Arab - {0xB1220000u, 12u}, // cjm -> Cham - {0xD5220000u, 46u}, // cjv -> Latn - {0x85420000u, 1u}, // ckb -> Arab - {0xAD420000u, 46u}, // ckl -> Latn - {0xB9420000u, 46u}, // cko -> Latn - {0xE1420000u, 46u}, // cky -> Latn - {0x81620000u, 46u}, // cla -> Latn - {0x91820000u, 46u}, // cme -> Latn - {0x99820000u, 80u}, // cmg -> Soyo - {0x636F0000u, 46u}, // co -> Latn - {0xBDC20000u, 15u}, // cop -> Copt - {0xC9E20000u, 46u}, // cps -> Latn - {0x63720000u, 10u}, // cr -> Cans - {0x9E220000u, 17u}, // crh -> Cyrl - {0xA6220000u, 10u}, // crj -> Cans - {0xAA220000u, 10u}, // crk -> Cans - {0xAE220000u, 10u}, // crl -> Cans - {0xB2220000u, 10u}, // crm -> Cans - {0xCA220000u, 46u}, // crs -> Latn - {0x63730000u, 46u}, // cs -> Latn - {0x86420000u, 46u}, // csb -> Latn - {0xDA420000u, 10u}, // csw -> Cans - {0x8E620000u, 67u}, // ctd -> Pauc - {0x63750000u, 17u}, // cu -> Cyrl - {0x63760000u, 17u}, // cv -> Cyrl - {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, 17u}, // dar -> Cyrl - {0xD4030000u, 46u}, // dav -> Latn - {0x8C230000u, 46u}, // dbd -> Latn - {0xC0230000u, 46u}, // dbq -> Latn - {0x88430000u, 1u}, // dcc -> Arab - {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, 1u}, // dgl -> Arab - {0xC4C30000u, 46u}, // dgr -> Latn - {0xE4C30000u, 46u}, // dgz -> Latn - {0x81030000u, 46u}, // dia -> Latn - {0x91230000u, 46u}, // dje -> Latn - {0xA5A30000u, 46u}, // dnj -> Latn - {0x85C30000u, 46u}, // dob -> Latn - {0xA1C30000u, 18u}, // doi -> Deva - {0xBDC30000u, 46u}, // dop -> Latn - {0xD9C30000u, 46u}, // dow -> Latn + {0x61610000u, 45u}, // aa -> Latn + {0xA0000000u, 45u}, // aai -> Latn + {0xA8000000u, 45u}, // aak -> Latn + {0xD0000000u, 45u}, // aau -> Latn + {0x61620000u, 18u}, // ab -> Cyrl + {0xA0200000u, 45u}, // 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 + {0xE0600000u, 18u}, // ady -> Cyrl + {0xE4600000u, 45u}, // 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 + {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 + {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 + {0x89E00000u, 2u}, // apc -> Arab + {0x8DE00000u, 2u}, // apd -> Arab + {0x91E00000u, 45u}, // ape -> Latn + {0xC5E00000u, 45u}, // apr -> Latn + {0xC9E00000u, 45u}, // aps -> Latn + {0xE5E00000u, 45u}, // apz -> Latn + {0x61720000u, 2u}, // ar -> Arab + {0x61725842u, 103u}, // ar-XB -> ~~~B + {0x8A200000u, 3u}, // arc -> Armi + {0x9E200000u, 45u}, // arh -> Latn + {0xB6200000u, 45u}, // arn -> Latn + {0xBA200000u, 45u}, // 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 + {0x61760000u, 18u}, // av -> Cyrl + {0xAEA00000u, 2u}, // avl -> Arab + {0xB6A00000u, 45u}, // avn -> Latn + {0xCEA00000u, 45u}, // avt -> Latn + {0xD2A00000u, 45u}, // 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 + {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 + {0xBC010000u, 19u}, // bap -> Deva + {0xC4010000u, 45u}, // bar -> Latn + {0xC8010000u, 45u}, // bas -> Latn + {0xD4010000u, 45u}, // 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 + {0xC0410000u, 21u}, // bcq -> Ethi + {0xD0410000u, 45u}, // bcu -> Latn + {0x8C610000u, 45u}, // bdd -> Latn + {0x62650000u, 18u}, // be -> Cyrl + {0x94810000u, 45u}, // bef -> Latn + {0x9C810000u, 45u}, // 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 + {0xCCA10000u, 2u}, // bft -> Arab + {0xE0A10000u, 19u}, // bfy -> Deva + {0x62670000u, 18u}, // bg -> Cyrl + {0x88C10000u, 19u}, // bgc -> Deva + {0xB4C10000u, 2u}, // bgn -> Arab + {0xDCC10000u, 26u}, // bgx -> Grek + {0x84E10000u, 19u}, // bhb -> Deva + {0x98E10000u, 45u}, // bhg -> Latn + {0xA0E10000u, 19u}, // bhi -> Deva + {0xACE10000u, 45u}, // 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 + {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 + {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 + {0xE1E10000u, 8u}, // bpy -> Beng + {0x8A010000u, 45u}, // bqc -> Latn + {0xA2010000u, 2u}, // bqi -> Arab + {0xBE010000u, 45u}, // bqp -> Latn + {0xD6010000u, 45u}, // bqv -> Latn + {0x62720000u, 45u}, // 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 + {0xC2410000u, 7u}, // bsq -> Bass + {0xCA410000u, 45u}, // bss -> Latn + {0xCE410000u, 21u}, // bst -> Ethi + {0xBA610000u, 45u}, // bto -> Latn + {0xCE610000u, 45u}, // 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 + {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 + {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 + {0xB0E20000u, 18u}, // chm -> Cyrl + {0xB8E20000u, 45u}, // cho -> Latn + {0xBCE20000u, 45u}, // chp -> Latn + {0xC4E20000u, 14u}, // chr -> Cher + {0x89020000u, 45u}, // cic -> Latn + {0x81220000u, 2u}, // cja -> Arab + {0xB1220000u, 13u}, // cjm -> Cham + {0xD5220000u, 45u}, // 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 + {0xBDC20000u, 16u}, // cop -> Copt + {0xC9E20000u, 45u}, // cps -> Latn + {0x63720000u, 11u}, // cr -> Cans + {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 + {0xDA420000u, 11u}, // csw -> Cans + {0x8E620000u, 68u}, // 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 + {0xC4030000u, 18u}, // dar -> Cyrl + {0xD4030000u, 45u}, // dav -> Latn + {0x8C230000u, 45u}, // dbd -> Latn + {0xC0230000u, 45u}, // 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 + {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 + {0xA1C30000u, 19u}, // doi -> Deva + {0xBDC30000u, 45u}, // dop -> Latn + {0xD9C30000u, 45u}, // dow -> Latn {0x9E230000u, 56u}, // drh -> Mong - {0xA2230000u, 46u}, // dri -> Latn - {0xCA230000u, 20u}, // drs -> Ethi - {0x86430000u, 46u}, // dsb -> Latn - {0xB2630000u, 46u}, // dtm -> Latn - {0xBE630000u, 46u}, // dtp -> Latn - {0xCA630000u, 46u}, // dts -> Latn - {0xE2630000u, 18u}, // dty -> Deva - {0x82830000u, 46u}, // dua -> Latn - {0x8A830000u, 46u}, // duc -> Latn - {0x8E830000u, 46u}, // dud -> Latn - {0x9A830000u, 46u}, // dug -> Latn - {0x64760000u, 89u}, // dv -> Thaa - {0x82A30000u, 46u}, // dva -> Latn - {0xDAC30000u, 46u}, // dww -> Latn - {0xBB030000u, 46u}, // dyo -> Latn - {0xD3030000u, 46u}, // dyu -> Latn - {0x647A0000u, 91u}, // dz -> Tibt - {0x9B230000u, 46u}, // dzg -> Latn - {0xD0240000u, 46u}, // ebu -> Latn - {0x65650000u, 46u}, // ee -> Latn - {0xA0A40000u, 46u}, // efi -> Latn - {0xACC40000u, 46u}, // egl -> Latn - {0xE0C40000u, 19u}, // egy -> Egyp - {0x81440000u, 46u}, // eka -> Latn - {0xE1440000u, 37u}, // eky -> Kali - {0x656C0000u, 25u}, // el -> Grek - {0x81840000u, 46u}, // ema -> Latn - {0xA1840000u, 46u}, // emi -> Latn - {0x656E0000u, 46u}, // en -> Latn - {0x656E5841u, 98u}, // en-XA -> ~~~A - {0xB5A40000u, 46u}, // enn -> Latn - {0xC1A40000u, 46u}, // enq -> Latn - {0x656F0000u, 46u}, // eo -> Latn - {0xA2240000u, 46u}, // eri -> Latn - {0x65730000u, 46u}, // es -> Latn - {0x9A440000u, 23u}, // esg -> Gonm - {0xD2440000u, 46u}, // esu -> Latn - {0x65740000u, 46u}, // et -> Latn - {0xC6640000u, 46u}, // etr -> Latn - {0xCE640000u, 35u}, // ett -> Ital - {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, 1u}, // fa -> Arab - {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, 1u}, // fia -> Arab - {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, 1u}, // fub -> Arab - {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, 28u}, // gan -> Hans - {0xD8060000u, 46u}, // gaw -> Latn - {0xE0060000u, 46u}, // gay -> Latn - {0x80260000u, 46u}, // gba -> Latn - {0x94260000u, 46u}, // gbf -> Latn - {0xB0260000u, 18u}, // gbm -> Deva - {0xE0260000u, 46u}, // gby -> Latn - {0xE4260000u, 1u}, // gbz -> Arab - {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, 20u}, // gez -> Ethi - {0xA8A60000u, 46u}, // gfk -> Latn - {0xB4C60000u, 18u}, // ggn -> Deva - {0xC8E60000u, 46u}, // ghs -> Latn - {0xAD060000u, 46u}, // gil -> Latn - {0xB1060000u, 46u}, // gim -> Latn - {0xA9260000u, 1u}, // gjk -> Arab - {0xB5260000u, 46u}, // gjn -> Latn - {0xD1260000u, 1u}, // gju -> Arab - {0xB5460000u, 46u}, // gkn -> Latn - {0xBD460000u, 46u}, // gkp -> Latn - {0x676C0000u, 46u}, // gl -> Latn - {0xA9660000u, 1u}, // glk -> Arab - {0xB1860000u, 46u}, // gmm -> Latn - {0xD5860000u, 20u}, // gmv -> Ethi - {0x676E0000u, 46u}, // gn -> Latn - {0x8DA60000u, 46u}, // gnd -> Latn - {0x99A60000u, 46u}, // gng -> Latn - {0x8DC60000u, 46u}, // god -> Latn - {0x95C60000u, 20u}, // gof -> Ethi - {0xA1C60000u, 46u}, // goi -> Latn - {0xB1C60000u, 18u}, // gom -> Deva - {0xB5C60000u, 87u}, // gon -> Telu - {0xC5C60000u, 46u}, // gor -> Latn - {0xC9C60000u, 46u}, // gos -> Latn - {0xCDC60000u, 24u}, // got -> Goth - {0x86260000u, 46u}, // grb -> Latn - {0x8A260000u, 16u}, // grc -> Cprt - {0xCE260000u, 7u}, // grt -> Beng - {0xDA260000u, 46u}, // grw -> Latn - {0xDA460000u, 46u}, // gsw -> Latn - {0x67750000u, 26u}, // gu -> Gujr - {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, 18u}, // gvr -> Deva - {0xCAA60000u, 46u}, // gvs -> Latn - {0x8AC60000u, 1u}, // gwc -> Arab - {0xA2C60000u, 46u}, // gwi -> Latn - {0xCEC60000u, 1u}, // gwt -> Arab - {0xA3060000u, 46u}, // gyi -> Latn - {0x68610000u, 46u}, // ha -> Latn - {0x6861434Du, 1u}, // ha-CM -> Arab - {0x68615344u, 1u}, // ha-SD -> Arab - {0x98070000u, 46u}, // hag -> Latn - {0xA8070000u, 28u}, // hak -> Hans - {0xB0070000u, 46u}, // ham -> Latn - {0xD8070000u, 46u}, // haw -> Latn - {0xE4070000u, 1u}, // haz -> Arab - {0x84270000u, 46u}, // hbb -> Latn - {0xE0670000u, 20u}, // hdy -> Ethi + {0xA2230000u, 45u}, // dri -> Latn + {0xCA230000u, 21u}, // drs -> Ethi + {0x86430000u, 45u}, // dsb -> Latn + {0xB2630000u, 45u}, // dtm -> Latn + {0xBE630000u, 45u}, // dtp -> Latn + {0xCA630000u, 45u}, // 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 + {0xE0C40000u, 20u}, // egy -> Egyp + {0x81440000u, 45u}, // 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 + {0x9A440000u, 24u}, // esg -> Gonm + {0xD2440000u, 45u}, // esu -> Latn + {0x65740000u, 45u}, // et -> Latn + {0xC6640000u, 45u}, // 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 + {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 + {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 + {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 + {0xB4060000u, 29u}, // gan -> Hans + {0xD8060000u, 45u}, // gaw -> Latn + {0xE0060000u, 45u}, // gay -> Latn + {0x80260000u, 45u}, // gba -> Latn + {0x94260000u, 45u}, // gbf -> Latn + {0xB0260000u, 19u}, // gbm -> Deva + {0xE0260000u, 45u}, // 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 + {0xE4860000u, 21u}, // gez -> Ethi + {0xA8A60000u, 45u}, // gfk -> Latn + {0xB4C60000u, 19u}, // ggn -> Deva + {0xC8E60000u, 45u}, // ghs -> Latn + {0xAD060000u, 45u}, // gil -> Latn + {0xB1060000u, 45u}, // gim -> Latn + {0xA9260000u, 2u}, // gjk -> Arab + {0xB5260000u, 45u}, // gjn -> Latn + {0xD1260000u, 2u}, // gju -> Arab + {0xB5460000u, 45u}, // gkn -> Latn + {0xBD460000u, 45u}, // gkp -> Latn + {0x676C0000u, 45u}, // gl -> Latn + {0xA9660000u, 2u}, // glk -> Arab + {0xB1860000u, 45u}, // gmm -> Latn + {0xD5860000u, 21u}, // gmv -> Ethi + {0x676E0000u, 45u}, // gn -> Latn + {0x8DA60000u, 45u}, // gnd -> Latn + {0x99A60000u, 45u}, // gng -> Latn + {0x8DC60000u, 45u}, // god -> Latn + {0x95C60000u, 21u}, // gof -> Ethi + {0xA1C60000u, 45u}, // goi -> Latn + {0xB1C60000u, 19u}, // gom -> Deva + {0xB5C60000u, 89u}, // gon -> Telu + {0xC5C60000u, 45u}, // gor -> Latn + {0xC9C60000u, 45u}, // gos -> Latn + {0xCDC60000u, 25u}, // got -> Goth + {0x86260000u, 45u}, // grb -> Latn + {0x8A260000u, 17u}, // grc -> Cprt + {0xCE260000u, 8u}, // grt -> Beng + {0xDA260000u, 45u}, // grw -> Latn + {0xDA460000u, 45u}, // 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 + {0xC6A60000u, 19u}, // gvr -> Deva + {0xCAA60000u, 45u}, // gvs -> Latn + {0x8AC60000u, 2u}, // gwc -> Arab + {0xA2C60000u, 45u}, // gwi -> Latn + {0xCEC60000u, 2u}, // gwt -> Arab + {0xA3060000u, 45u}, // gyi -> Latn + {0x68610000u, 45u}, // ha -> Latn + {0x6861434Du, 2u}, // ha-CM -> Arab + {0x68615344u, 2u}, // ha-SD -> Arab + {0x98070000u, 45u}, // hag -> Latn + {0xA8070000u, 29u}, // hak -> Hans + {0xB0070000u, 45u}, // ham -> Latn + {0xD8070000u, 45u}, // haw -> Latn + {0xE4070000u, 2u}, // haz -> Arab + {0x84270000u, 45u}, // hbb -> Latn + {0xE0670000u, 21u}, // hdy -> Ethi {0x68650000u, 31u}, // he -> Hebr - {0xE0E70000u, 46u}, // hhy -> Latn - {0x68690000u, 18u}, // hi -> Deva - {0x81070000u, 46u}, // hia -> Latn - {0x95070000u, 46u}, // hif -> Latn - {0x99070000u, 46u}, // hig -> Latn - {0x9D070000u, 46u}, // hih -> Latn - {0xAD070000u, 46u}, // hil -> Latn - {0x81670000u, 46u}, // hla -> Latn + {0xE0E70000u, 45u}, // 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 {0xD1670000u, 32u}, // hlu -> Hluw - {0x8D870000u, 70u}, // hmd -> Plrd - {0xCD870000u, 46u}, // hmt -> Latn - {0x8DA70000u, 1u}, // hnd -> Arab - {0x91A70000u, 18u}, // hne -> Deva - {0xA5A70000u, 33u}, // hnj -> Hmng - {0xB5A70000u, 46u}, // hnn -> Latn - {0xB9A70000u, 1u}, // hno -> Arab - {0x686F0000u, 46u}, // ho -> Latn - {0x89C70000u, 18u}, // hoc -> Deva - {0xA5C70000u, 18u}, // hoj -> Deva - {0xCDC70000u, 46u}, // hot -> Latn - {0x68720000u, 46u}, // hr -> Latn - {0x86470000u, 46u}, // hsb -> Latn - {0xB6470000u, 28u}, // hsn -> Hans - {0x68740000u, 46u}, // ht -> Latn - {0x68750000u, 46u}, // hu -> Latn - {0xA2870000u, 46u}, // hui -> Latn - {0x68790000u, 3u}, // hy -> Armn - {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, 97u}, // ii -> Yiii - {0xA5280000u, 46u}, // ijj -> Latn - {0x696B0000u, 46u}, // ik -> Latn - {0xA9480000u, 46u}, // ikk -> Latn - {0xCD480000u, 46u}, // ikt -> Latn - {0xD9480000u, 46u}, // ikw -> Latn - {0xDD480000u, 46u}, // ikx -> Latn - {0xB9680000u, 46u}, // ilo -> Latn - {0xB9880000u, 46u}, // imo -> Latn - {0x696E0000u, 46u}, // in -> Latn - {0x9DA80000u, 17u}, // inh -> Cyrl - {0x696F0000u, 46u}, // io -> Latn - {0xD1C80000u, 46u}, // iou -> Latn - {0xA2280000u, 46u}, // iri -> Latn - {0x69730000u, 46u}, // is -> Latn - {0x69740000u, 46u}, // it -> Latn - {0x69750000u, 10u}, // iu -> Cans + {0x8D870000u, 71u}, // hmd -> Plrd + {0xCD870000u, 45u}, // hmt -> Latn + {0x8DA70000u, 2u}, // hnd -> Arab + {0x91A70000u, 19u}, // hne -> Deva + {0xA5A70000u, 33u}, // hnj -> Hmnp + {0xB5A70000u, 45u}, // hnn -> Latn + {0xB9A70000u, 2u}, // hno -> Arab + {0x686F0000u, 45u}, // ho -> Latn + {0x89C70000u, 19u}, // hoc -> Deva + {0xA5C70000u, 19u}, // hoj -> Deva + {0xCDC70000u, 45u}, // hot -> Latn + {0x68720000u, 45u}, // hr -> Latn + {0x86470000u, 45u}, // hsb -> Latn + {0xB6470000u, 29u}, // hsn -> Hans + {0x68740000u, 45u}, // ht -> Latn + {0x68750000u, 45u}, // hu -> Latn + {0xA2870000u, 45u}, // hui -> 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 + {0x9DA80000u, 18u}, // inh -> Cyrl + {0x696F0000u, 45u}, // io -> Latn + {0xD1C80000u, 45u}, // iou -> Latn + {0xA2280000u, 45u}, // iri -> Latn + {0x69730000u, 45u}, // is -> Latn + {0x69740000u, 45u}, // it -> Latn + {0x69750000u, 11u}, // iu -> Cans {0x69770000u, 31u}, // iw -> Hebr - {0xB2C80000u, 46u}, // iwm -> Latn - {0xCAC80000u, 46u}, // iws -> Latn - {0x9F280000u, 46u}, // izh -> Latn - {0xA3280000u, 46u}, // izi -> Latn - {0x6A610000u, 36u}, // ja -> Jpan - {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 + {0xB2C80000u, 45u}, // iwm -> Latn + {0xCAC80000u, 45u}, // iws -> Latn + {0x9F280000u, 45u}, // izh -> Latn + {0xA3280000u, 45u}, // 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 {0x6A690000u, 31u}, // ji -> Hebr - {0x85090000u, 46u}, // jib -> Latn - {0x89890000u, 46u}, // jmc -> Latn - {0xAD890000u, 18u}, // jml -> Deva - {0x82290000u, 46u}, // jra -> Latn - {0xCE890000u, 46u}, // jut -> Latn - {0x6A760000u, 46u}, // jv -> Latn - {0x6A770000u, 46u}, // jw -> Latn - {0x6B610000u, 21u}, // ka -> Geor - {0x800A0000u, 17u}, // kaa -> Cyrl - {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 - {0x8C2A0000u, 17u}, // kbd -> Cyrl - {0xB02A0000u, 46u}, // kbm -> Latn - {0xBC2A0000u, 46u}, // kbp -> Latn - {0xC02A0000u, 46u}, // kbq -> Latn - {0xDC2A0000u, 46u}, // kbx -> Latn - {0xE02A0000u, 1u}, // kby -> Arab - {0x984A0000u, 46u}, // kcg -> Latn - {0xA84A0000u, 46u}, // kck -> Latn - {0xAC4A0000u, 46u}, // kcl -> Latn - {0xCC4A0000u, 46u}, // kct -> Latn - {0x906A0000u, 46u}, // kde -> Latn - {0x9C6A0000u, 1u}, // kdh -> Arab - {0xAC6A0000u, 46u}, // kdl -> Latn - {0xCC6A0000u, 90u}, // kdt -> Thai - {0x808A0000u, 46u}, // kea -> Latn - {0xB48A0000u, 46u}, // ken -> Latn - {0xE48A0000u, 46u}, // kez -> Latn - {0xB8AA0000u, 46u}, // kfo -> Latn - {0xC4AA0000u, 18u}, // kfr -> Deva - {0xE0AA0000u, 18u}, // kfy -> Deva - {0x6B670000u, 46u}, // kg -> Latn - {0x90CA0000u, 46u}, // kge -> Latn - {0x94CA0000u, 46u}, // kgf -> Latn - {0xBCCA0000u, 46u}, // kgp -> Latn - {0x80EA0000u, 46u}, // kha -> Latn - {0x84EA0000u, 83u}, // khb -> Talu - {0xB4EA0000u, 18u}, // khn -> Deva - {0xC0EA0000u, 46u}, // khq -> Latn - {0xC8EA0000u, 46u}, // khs -> Latn + {0x85090000u, 45u}, // jib -> Latn + {0x89890000u, 45u}, // jmc -> Latn + {0xAD890000u, 19u}, // jml -> Deva + {0x82290000u, 45u}, // jra -> Latn + {0xCE890000u, 45u}, // jut -> Latn + {0x6A760000u, 45u}, // jv -> Latn + {0x6A770000u, 45u}, // 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 + {0x8C2A0000u, 18u}, // kbd -> Cyrl + {0xB02A0000u, 45u}, // kbm -> Latn + {0xBC2A0000u, 45u}, // kbp -> Latn + {0xC02A0000u, 45u}, // kbq -> Latn + {0xDC2A0000u, 45u}, // 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 + {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 + {0xB4EA0000u, 19u}, // khn -> Deva + {0xC0EA0000u, 45u}, // khq -> Latn + {0xC8EA0000u, 45u}, // khs -> Latn {0xCCEA0000u, 58u}, // kht -> Mymr - {0xD8EA0000u, 1u}, // khw -> Arab - {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, 17u}, // kk -> Cyrl - {0x6B6B4146u, 1u}, // kk-AF -> Arab - {0x6B6B434Eu, 1u}, // kk-CN -> Arab - {0x6B6B4952u, 1u}, // kk-IR -> Arab - {0x6B6B4D4Eu, 1u}, // kk-MN -> Arab - {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, 17u}, // koi -> Cyrl - {0xA9CA0000u, 18u}, // kok -> Deva - {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, 20u}, // kqy -> Ethi - {0x6B720000u, 46u}, // kr -> Latn - {0x8A2A0000u, 17u}, // krc -> Cyrl - {0xA22A0000u, 46u}, // kri -> Latn - {0xA62A0000u, 46u}, // krj -> Latn - {0xAE2A0000u, 46u}, // krl -> Latn - {0xCA2A0000u, 46u}, // krs -> Latn - {0xD22A0000u, 18u}, // kru -> Deva - {0x6B730000u, 1u}, // ks -> Arab - {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, 20u}, // ktb -> Ethi - {0xB26A0000u, 46u}, // ktm -> Latn - {0xBA6A0000u, 46u}, // kto -> Latn - {0xC66A0000u, 46u}, // ktr -> Latn - {0x6B750000u, 46u}, // ku -> Latn - {0x6B754952u, 1u}, // ku-IR -> Arab - {0x6B754C42u, 1u}, // ku-LB -> Arab - {0x868A0000u, 46u}, // kub -> Latn - {0x8E8A0000u, 46u}, // kud -> Latn - {0x928A0000u, 46u}, // kue -> Latn - {0xA68A0000u, 46u}, // kuj -> Latn - {0xB28A0000u, 17u}, // kum -> Cyrl - {0xB68A0000u, 46u}, // kun -> Latn - {0xBE8A0000u, 46u}, // kup -> Latn - {0xCA8A0000u, 46u}, // kus -> Latn - {0x6B760000u, 17u}, // kv -> Cyrl - {0x9AAA0000u, 46u}, // kvg -> Latn - {0xC6AA0000u, 46u}, // kvr -> Latn - {0xDEAA0000u, 1u}, // kvx -> Arab - {0x6B770000u, 46u}, // kw -> Latn - {0xA6CA0000u, 46u}, // kwj -> Latn - {0xBACA0000u, 46u}, // kwo -> Latn - {0xC2CA0000u, 46u}, // kwq -> Latn - {0x82EA0000u, 46u}, // kxa -> Latn - {0x8AEA0000u, 20u}, // kxc -> Ethi - {0x92EA0000u, 46u}, // kxe -> Latn - {0xAEEA0000u, 18u}, // kxl -> Deva - {0xB2EA0000u, 90u}, // kxm -> Thai - {0xBEEA0000u, 1u}, // kxp -> Arab - {0xDAEA0000u, 46u}, // kxw -> Latn - {0xE6EA0000u, 46u}, // kxz -> Latn - {0x6B790000u, 17u}, // ky -> Cyrl - {0x6B79434Eu, 1u}, // ky-CN -> Arab - {0x6B795452u, 46u}, // ky-TR -> Latn - {0x930A0000u, 46u}, // kye -> Latn - {0xDF0A0000u, 46u}, // kyx -> Latn - {0x9F2A0000u, 1u}, // kzh -> Arab - {0xA72A0000u, 46u}, // kzj -> Latn - {0xC72A0000u, 46u}, // kzr -> Latn - {0xCF2A0000u, 46u}, // kzt -> Latn - {0x6C610000u, 46u}, // la -> Latn - {0x840B0000u, 48u}, // lab -> Lina + {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 + {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 + {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 + {0xE20A0000u, 21u}, // kqy -> Ethi + {0x6B720000u, 45u}, // kr -> Latn + {0x8A2A0000u, 18u}, // krc -> Cyrl + {0xA22A0000u, 45u}, // kri -> Latn + {0xA62A0000u, 45u}, // krj -> Latn + {0xAE2A0000u, 45u}, // krl -> Latn + {0xCA2A0000u, 45u}, // 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 + {0x866A0000u, 21u}, // ktb -> Ethi + {0xB26A0000u, 45u}, // ktm -> Latn + {0xBA6A0000u, 45u}, // kto -> Latn + {0xC66A0000u, 45u}, // ktr -> Latn + {0x6B750000u, 45u}, // 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 + {0xB28A0000u, 18u}, // kum -> Cyrl + {0xB68A0000u, 45u}, // kun -> Latn + {0xBE8A0000u, 45u}, // kup -> Latn + {0xCA8A0000u, 45u}, // kus -> Latn + {0x6B760000u, 18u}, // kv -> Cyrl + {0x9AAA0000u, 45u}, // kvg -> Latn + {0xC6AA0000u, 45u}, // 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 + {0x8AEA0000u, 21u}, // kxc -> Ethi + {0x92EA0000u, 45u}, // kxe -> Latn + {0xAEEA0000u, 19u}, // kxl -> Deva + {0xB2EA0000u, 92u}, // kxm -> Thai + {0xBEEA0000u, 2u}, // kxp -> Arab + {0xDAEA0000u, 45u}, // kxw -> Latn + {0xE6EA0000u, 45u}, // kxz -> Latn + {0x6B790000u, 18u}, // ky -> Cyrl + {0x6B79434Eu, 2u}, // ky-CN -> Arab + {0x6B795452u, 45u}, // ky-TR -> Latn + {0x930A0000u, 45u}, // kye -> Latn + {0xDF0A0000u, 45u}, // 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 {0x8C0B0000u, 31u}, // lad -> Hebr - {0x980B0000u, 46u}, // lag -> Latn - {0x9C0B0000u, 1u}, // lah -> Arab - {0xA40B0000u, 46u}, // laj -> Latn - {0xC80B0000u, 46u}, // las -> Latn - {0x6C620000u, 46u}, // lb -> Latn - {0x902B0000u, 17u}, // lbe -> Cyrl - {0xD02B0000u, 46u}, // lbu -> Latn - {0xD82B0000u, 46u}, // lbw -> Latn - {0xB04B0000u, 46u}, // lcm -> Latn - {0xBC4B0000u, 90u}, // 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, 17u}, // lez -> Cyrl - {0x6C670000u, 46u}, // lg -> Latn - {0x98CB0000u, 46u}, // lgg -> Latn - {0x6C690000u, 46u}, // li -> Latn - {0x810B0000u, 46u}, // lia -> Latn - {0x8D0B0000u, 46u}, // lid -> Latn - {0x950B0000u, 18u}, // lif -> Deva - {0x990B0000u, 46u}, // lig -> Latn - {0x9D0B0000u, 46u}, // lih -> Latn - {0xA50B0000u, 46u}, // lij -> Latn - {0xC90B0000u, 49u}, // lis -> Lisu - {0xBD2B0000u, 46u}, // ljp -> Latn - {0xA14B0000u, 1u}, // lki -> Arab - {0xCD4B0000u, 46u}, // lkt -> Latn - {0x916B0000u, 46u}, // lle -> Latn - {0xB56B0000u, 46u}, // lln -> Latn - {0xB58B0000u, 87u}, // 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, 1u}, // lrc -> Arab - {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, 1u}, // luz -> Arab - {0x6C760000u, 46u}, // lv -> Latn - {0xAECB0000u, 90u}, // lwl -> Thai - {0x9F2B0000u, 28u}, // lzh -> Hans - {0xE72B0000u, 46u}, // lzz -> Latn - {0x8C0C0000u, 46u}, // mad -> Latn - {0x940C0000u, 46u}, // maf -> Latn - {0x980C0000u, 18u}, // mag -> Deva - {0xA00C0000u, 18u}, // mai -> Deva - {0xA80C0000u, 46u}, // mak -> Latn - {0xB40C0000u, 46u}, // man -> Latn + {0x980B0000u, 45u}, // lag -> Latn + {0x9C0B0000u, 2u}, // lah -> Arab + {0xA40B0000u, 45u}, // laj -> Latn + {0xC80B0000u, 45u}, // las -> Latn + {0x6C620000u, 45u}, // 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 + {0xE48B0000u, 18u}, // lez -> Cyrl + {0x6C670000u, 45u}, // lg -> Latn + {0x98CB0000u, 45u}, // lgg -> Latn + {0x6C690000u, 45u}, // li -> Latn + {0x810B0000u, 45u}, // lia -> Latn + {0x8D0B0000u, 45u}, // 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 + {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 + {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 + {0xE68B0000u, 2u}, // luz -> Arab + {0x6C760000u, 45u}, // lv -> Latn + {0xAECB0000u, 92u}, // lwl -> Thai + {0x9F2B0000u, 29u}, // lzh -> Hans + {0xE72B0000u, 45u}, // lzz -> Latn + {0x8C0C0000u, 45u}, // mad -> Latn + {0x940C0000u, 45u}, // maf -> Latn + {0x980C0000u, 19u}, // mag -> Deva + {0xA00C0000u, 19u}, // mai -> Deva + {0xA80C0000u, 45u}, // mak -> Latn + {0xB40C0000u, 45u}, // man -> Latn {0xB40C474Eu, 60u}, // 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, 1u}, // mde -> Arab - {0x946C0000u, 17u}, // mdf -> Cyrl - {0x9C6C0000u, 46u}, // mdh -> Latn - {0xA46C0000u, 46u}, // mdj -> Latn - {0xC46C0000u, 46u}, // mdr -> Latn - {0xDC6C0000u, 20u}, // mdx -> Ethi - {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, 1u}, // mfa -> Arab - {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, 18u}, // mgp -> Deva - {0xE0CC0000u, 46u}, // mgy -> Latn - {0x6D680000u, 46u}, // mh -> Latn - {0xA0EC0000u, 46u}, // mhi -> Latn - {0xACEC0000u, 46u}, // mhl -> Latn - {0x6D690000u, 46u}, // mi -> Latn - {0x950C0000u, 46u}, // mif -> Latn - {0xB50C0000u, 46u}, // min -> Latn - {0xC90C0000u, 30u}, // mis -> Hatr - {0xD90C0000u, 46u}, // miw -> Latn - {0x6D6B0000u, 17u}, // mk -> Cyrl - {0xA14C0000u, 1u}, // mki -> Arab - {0xAD4C0000u, 46u}, // mkl -> Latn - {0xBD4C0000u, 46u}, // mkp -> Latn - {0xD94C0000u, 46u}, // mkw -> Latn + {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 + {0x906C0000u, 2u}, // mde -> Arab + {0x946C0000u, 18u}, // mdf -> Cyrl + {0x9C6C0000u, 45u}, // mdh -> Latn + {0xA46C0000u, 45u}, // mdj -> Latn + {0xC46C0000u, 45u}, // 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 + {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 + {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 + {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, 46u}, // mle -> Latn - {0xBD6C0000u, 46u}, // mlp -> Latn - {0xC96C0000u, 46u}, // mls -> Latn - {0xB98C0000u, 46u}, // mmo -> Latn - {0xD18C0000u, 46u}, // mmu -> Latn - {0xDD8C0000u, 46u}, // mmx -> Latn - {0x6D6E0000u, 17u}, // mn -> Cyrl + {0x916C0000u, 45u}, // mle -> Latn + {0xBD6C0000u, 45u}, // mlp -> Latn + {0xC96C0000u, 45u}, // mls -> Latn + {0xB98C0000u, 45u}, // mmo -> Latn + {0xD18C0000u, 45u}, // mmu -> Latn + {0xDD8C0000u, 45u}, // mmx -> Latn + {0x6D6E0000u, 18u}, // mn -> Cyrl {0x6D6E434Eu, 56u}, // mn-CN -> Mong - {0x81AC0000u, 46u}, // mna -> Latn - {0x95AC0000u, 46u}, // mnf -> Latn - {0xA1AC0000u, 7u}, // mni -> Beng + {0x81AC0000u, 45u}, // mna -> Latn + {0x95AC0000u, 45u}, // mnf -> Latn + {0xA1AC0000u, 8u}, // mni -> Beng {0xD9AC0000u, 58u}, // 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, 18u}, // mr -> Deva - {0x8E2C0000u, 18u}, // mrd -> Deva - {0xA62C0000u, 17u}, // mrj -> Cyrl + {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 + {0x6D720000u, 19u}, // mr -> Deva + {0x8E2C0000u, 19u}, // mrd -> Deva + {0xA62C0000u, 18u}, // mrj -> Cyrl {0xBA2C0000u, 57u}, // mro -> Mroo - {0x6D730000u, 46u}, // ms -> Latn - {0x6D734343u, 1u}, // ms-CC -> Arab - {0x6D740000u, 46u}, // mt -> Latn - {0x8A6C0000u, 46u}, // mtc -> Latn - {0x966C0000u, 46u}, // mtf -> Latn - {0xA26C0000u, 46u}, // mti -> Latn - {0xC66C0000u, 18u}, // mtr -> Deva - {0x828C0000u, 46u}, // mua -> Latn - {0xC68C0000u, 46u}, // mur -> Latn - {0xCA8C0000u, 46u}, // mus -> Latn - {0x82AC0000u, 46u}, // mva -> Latn - {0xB6AC0000u, 46u}, // mvn -> Latn - {0xE2AC0000u, 1u}, // mvy -> Arab - {0xAACC0000u, 46u}, // mwk -> Latn - {0xC6CC0000u, 18u}, // mwr -> Deva - {0xD6CC0000u, 46u}, // mwv -> Latn - {0xDACC0000u, 34u}, // mww -> Hmnp - {0x8AEC0000u, 46u}, // mxc -> Latn - {0xB2EC0000u, 46u}, // mxm -> Latn + {0x6D730000u, 45u}, // ms -> Latn + {0x6D734343u, 2u}, // ms-CC -> Arab + {0x6D740000u, 45u}, // mt -> Latn + {0x8A6C0000u, 45u}, // mtc -> Latn + {0x966C0000u, 45u}, // mtf -> Latn + {0xA26C0000u, 45u}, // 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 + {0xE2AC0000u, 2u}, // mvy -> Arab + {0xAACC0000u, 45u}, // mwk -> Latn + {0xC6CC0000u, 19u}, // mwr -> Deva + {0xD6CC0000u, 45u}, // mwv -> Latn + {0xDACC0000u, 33u}, // mww -> Hmnp + {0x8AEC0000u, 45u}, // mxc -> Latn + {0xB2EC0000u, 45u}, // mxm -> Latn {0x6D790000u, 58u}, // my -> Mymr - {0xAB0C0000u, 46u}, // myk -> Latn - {0xB30C0000u, 20u}, // mym -> Ethi - {0xD70C0000u, 17u}, // myv -> Cyrl - {0xDB0C0000u, 46u}, // myw -> Latn - {0xDF0C0000u, 46u}, // myx -> Latn - {0xE70C0000u, 52u}, // myz -> Mand - {0xAB2C0000u, 46u}, // mzk -> Latn - {0xB32C0000u, 46u}, // mzm -> Latn - {0xB72C0000u, 1u}, // mzn -> Arab - {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, 28u}, // nan -> Hans - {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, 18u}, // ne -> Deva - {0x848D0000u, 46u}, // neb -> Latn - {0xD88D0000u, 18u}, // new -> Deva - {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, 94u}, // nnp -> Wcho - {0x6E6F0000u, 46u}, // no -> Latn - {0x8DCD0000u, 44u}, // nod -> Lana - {0x91CD0000u, 18u}, // noe -> Deva - {0xB5CD0000u, 72u}, // non -> Runr - {0xBDCD0000u, 46u}, // nop -> Latn - {0xD1CD0000u, 46u}, // nou -> Latn + {0xAB0C0000u, 45u}, // 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 + {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 + {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 + {0x6E650000u, 19u}, // ne -> Deva + {0x848D0000u, 45u}, // 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 + {0x91CD0000u, 19u}, // noe -> Deva + {0xB5CD0000u, 74u}, // non -> Runr + {0xBDCD0000u, 45u}, // nop -> Latn + {0xD1CD0000u, 45u}, // nou -> Latn {0xBA0D0000u, 60u}, // nqo -> Nkoo - {0x6E720000u, 46u}, // nr -> Latn - {0x862D0000u, 46u}, // nrb -> Latn - {0xAA4D0000u, 10u}, // nsk -> Cans - {0xB64D0000u, 46u}, // nsn -> Latn - {0xBA4D0000u, 46u}, // nso -> Latn - {0xCA4D0000u, 46u}, // nss -> Latn - {0xB26D0000u, 46u}, // ntm -> Latn - {0xC66D0000u, 46u}, // ntr -> Latn - {0xA28D0000u, 46u}, // nui -> Latn - {0xBE8D0000u, 46u}, // nup -> Latn - {0xCA8D0000u, 46u}, // nus -> Latn - {0xD68D0000u, 46u}, // nuv -> Latn - {0xDE8D0000u, 46u}, // nux -> Latn - {0x6E760000u, 46u}, // nv -> Latn - {0x86CD0000u, 46u}, // nwb -> Latn - {0xC2ED0000u, 46u}, // nxq -> Latn - {0xC6ED0000u, 46u}, // nxr -> Latn - {0x6E790000u, 46u}, // ny -> Latn - {0xB30D0000u, 46u}, // nym -> Latn - {0xB70D0000u, 46u}, // nyn -> Latn - {0xA32D0000u, 46u}, // nzi -> Latn - {0x6F630000u, 46u}, // oc -> Latn - {0x88CE0000u, 46u}, // ogc -> Latn - {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 + {0x6E720000u, 45u}, // nr -> Latn + {0x862D0000u, 45u}, // 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, 46u}, // oro -> Latn - {0xD22E0000u, 1u}, // oru -> Arab - {0x6F730000u, 17u}, // os -> Cyrl + {0xBA2E0000u, 45u}, // oro -> Latn + {0xD22E0000u, 2u}, // oru -> Arab + {0x6F730000u, 18u}, // os -> Cyrl {0x824E0000u, 66u}, // osa -> Osge - {0x826E0000u, 1u}, // ota -> Arab + {0x826E0000u, 2u}, // ota -> Arab {0xAA6E0000u, 64u}, // otk -> Orkh - {0xB32E0000u, 46u}, // ozm -> Latn - {0x70610000u, 27u}, // pa -> Guru - {0x7061504Bu, 1u}, // pa-PK -> Arab - {0x980F0000u, 46u}, // pag -> Latn - {0xAC0F0000u, 68u}, // 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, 95u}, // peo -> Xpeo - {0xDC8F0000u, 46u}, // pex -> Latn - {0xACAF0000u, 46u}, // pfl -> Latn - {0xACEF0000u, 1u}, // phl -> Arab - {0xB4EF0000u, 69u}, // phn -> Phnx - {0xAD0F0000u, 46u}, // pil -> Latn - {0xBD0F0000u, 46u}, // pip -> Latn - {0x814F0000u, 8u}, // pka -> Brah - {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, 25u}, // pnt -> Grek - {0xB5CF0000u, 46u}, // pon -> Latn - {0x81EF0000u, 18u}, // ppa -> Deva - {0xB9EF0000u, 46u}, // ppo -> Latn - {0x822F0000u, 39u}, // pra -> Khar - {0x8E2F0000u, 1u}, // prd -> Arab - {0x9A2F0000u, 46u}, // prg -> Latn - {0x70730000u, 1u}, // ps -> Arab - {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, 18u}, // raj -> Deva - {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, 1u}, // rhg -> Arab - {0x81110000u, 46u}, // ria -> Latn - {0x95110000u, 88u}, // rif -> Tfng - {0x95114E4Cu, 46u}, // rif-NL -> Latn - {0xC9310000u, 18u}, // rjs -> Deva - {0xCD510000u, 7u}, // rkt -> Beng - {0x726D0000u, 46u}, // rm -> Latn - {0x95910000u, 46u}, // rmf -> Latn - {0xB9910000u, 46u}, // rmo -> Latn - {0xCD910000u, 1u}, // rmt -> Arab - {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, 17u}, // ru -> Cyrl - {0x92910000u, 17u}, // rue -> Cyrl - {0x9A910000u, 46u}, // rug -> Latn - {0x72770000u, 46u}, // rw -> Latn - {0xAAD10000u, 46u}, // rwk -> Latn - {0xBAD10000u, 46u}, // rwo -> Latn - {0xD3110000u, 38u}, // ryu -> Kana - {0x73610000u, 18u}, // sa -> Deva - {0x94120000u, 46u}, // saf -> Latn - {0x9C120000u, 17u}, // sah -> Cyrl - {0xC0120000u, 46u}, // saq -> Latn - {0xC8120000u, 46u}, // sas -> Latn + {0xA28E0000u, 67u}, // oui -> Ougr + {0xB32E0000u, 45u}, // 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 + {0xACEF0000u, 2u}, // phl -> Arab + {0xB4EF0000u, 70u}, // phn -> Phnx + {0xAD0F0000u, 45u}, // pil -> Latn + {0xBD0F0000u, 45u}, // pip -> Latn + {0x814F0000u, 9u}, // pka -> Brah + {0xB94F0000u, 45u}, // pko -> Latn + {0x706C0000u, 45u}, // pl -> Latn + {0x816F0000u, 45u}, // pla -> Latn + {0xC98F0000u, 45u}, // pms -> Latn + {0x99AF0000u, 45u}, // png -> Latn + {0xB5AF0000u, 45u}, // pnn -> Latn + {0xCDAF0000u, 26u}, // pnt -> Grek + {0xB5CF0000u, 45u}, // pon -> Latn + {0x81EF0000u, 19u}, // ppa -> Deva + {0xB9EF0000u, 45u}, // ppo -> Latn + {0x822F0000u, 38u}, // pra -> Khar + {0x8E2F0000u, 2u}, // prd -> Arab + {0x9A2F0000u, 45u}, // 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 + {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 + {0xC9310000u, 19u}, // rjs -> Deva + {0xCD510000u, 8u}, // rkt -> Beng + {0x726D0000u, 45u}, // rm -> Latn + {0x95910000u, 45u}, // rmf -> Latn + {0xB9910000u, 45u}, // 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 + {0x72750000u, 18u}, // ru -> Cyrl + {0x92910000u, 18u}, // rue -> Cyrl + {0x9A910000u, 45u}, // rug -> Latn + {0x72770000u, 45u}, // rw -> Latn + {0xAAD10000u, 45u}, // rwk -> Latn + {0xBAD10000u, 45u}, // rwo -> Latn + {0xD3110000u, 37u}, // ryu -> Kana + {0x73610000u, 19u}, // sa -> Deva + {0x94120000u, 45u}, // saf -> Latn + {0x9C120000u, 18u}, // sah -> Cyrl + {0xC0120000u, 45u}, // saq -> Latn + {0xC8120000u, 45u}, // sas -> Latn {0xCC120000u, 63u}, // sat -> Olck - {0xD4120000u, 46u}, // sav -> Latn - {0xE4120000u, 75u}, // saz -> Saur - {0x80320000u, 46u}, // sba -> Latn - {0x90320000u, 46u}, // sbe -> Latn - {0xBC320000u, 46u}, // sbp -> Latn - {0x73630000u, 46u}, // sc -> Latn - {0xA8520000u, 18u}, // sck -> Deva - {0xAC520000u, 1u}, // scl -> Arab - {0xB4520000u, 46u}, // scn -> Latn - {0xB8520000u, 46u}, // sco -> Latn - {0xC8520000u, 46u}, // scs -> Latn - {0x73640000u, 1u}, // sd -> Arab - {0x88720000u, 46u}, // sdc -> Latn - {0x9C720000u, 1u}, // sdh -> Arab - {0x73650000u, 46u}, // se -> Latn - {0x94920000u, 46u}, // sef -> Latn - {0x9C920000u, 46u}, // seh -> Latn - {0xA0920000u, 46u}, // sei -> Latn - {0xC8920000u, 46u}, // ses -> Latn - {0x73670000u, 46u}, // sg -> Latn + {0xD4120000u, 45u}, // sav -> Latn + {0xE4120000u, 77u}, // saz -> Saur + {0x80320000u, 45u}, // sba -> Latn + {0x90320000u, 45u}, // sbe -> Latn + {0xBC320000u, 45u}, // sbp -> Latn + {0x73630000u, 45u}, // sc -> Latn + {0xA8520000u, 19u}, // sck -> Deva + {0xAC520000u, 2u}, // scl -> Arab + {0xB4520000u, 45u}, // scn -> Latn + {0xB8520000u, 45u}, // sco -> Latn + {0xC8520000u, 45u}, // scs -> Latn + {0x73640000u, 2u}, // sd -> Arab + {0x88720000u, 45u}, // 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, 46u}, // sgs -> Latn - {0xD8D20000u, 20u}, // sgw -> Ethi - {0xE4D20000u, 46u}, // sgz -> Latn - {0x73680000u, 46u}, // sh -> Latn - {0xA0F20000u, 88u}, // shi -> Tfng - {0xA8F20000u, 46u}, // shk -> Latn + {0xC8D20000u, 45u}, // 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 - {0xD0F20000u, 1u}, // shu -> Arab - {0x73690000u, 77u}, // 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, 1u}, // skr -> Arab - {0xC9520000u, 46u}, // sks -> Latn - {0x736C0000u, 46u}, // sl -> Latn - {0x8D720000u, 46u}, // sld -> Latn - {0xA1720000u, 46u}, // sli -> Latn - {0xAD720000u, 46u}, // sll -> Latn - {0xE1720000u, 46u}, // sly -> Latn - {0x736D0000u, 46u}, // sm -> Latn - {0x81920000u, 46u}, // sma -> Latn - {0xA5920000u, 46u}, // smj -> Latn - {0xB5920000u, 46u}, // smn -> Latn - {0xBD920000u, 73u}, // smp -> Samr - {0xC1920000u, 46u}, // smq -> Latn - {0xC9920000u, 46u}, // sms -> Latn - {0x736E0000u, 46u}, // sn -> Latn - {0x89B20000u, 46u}, // snc -> Latn - {0xA9B20000u, 46u}, // snk -> Latn - {0xBDB20000u, 46u}, // snp -> Latn - {0xDDB20000u, 46u}, // snx -> Latn - {0xE1B20000u, 46u}, // sny -> Latn - {0x736F0000u, 46u}, // so -> Latn - {0x99D20000u, 78u}, // sog -> Sogd - {0xA9D20000u, 46u}, // sok -> Latn - {0xC1D20000u, 46u}, // soq -> Latn - {0xD1D20000u, 90u}, // sou -> Thai - {0xE1D20000u, 46u}, // soy -> Latn - {0x8DF20000u, 46u}, // spd -> Latn - {0xADF20000u, 46u}, // spl -> Latn - {0xC9F20000u, 46u}, // sps -> Latn - {0x73710000u, 46u}, // sq -> Latn - {0x73720000u, 17u}, // sr -> Cyrl - {0x73724D45u, 46u}, // sr-ME -> Latn - {0x7372524Fu, 46u}, // sr-RO -> Latn - {0x73725255u, 46u}, // sr-RU -> Latn - {0x73725452u, 46u}, // sr-TR -> Latn - {0x86320000u, 79u}, // srb -> Sora - {0xB6320000u, 46u}, // srn -> Latn - {0xC6320000u, 46u}, // srr -> Latn - {0xDE320000u, 18u}, // srx -> Deva - {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, 1u}, // swb -> Arab - {0x8AD20000u, 46u}, // swc -> Latn - {0x9AD20000u, 46u}, // swg -> Latn - {0xBED20000u, 46u}, // swp -> Latn - {0xD6D20000u, 18u}, // swv -> Deva - {0xB6F20000u, 46u}, // sxn -> Latn - {0xDAF20000u, 46u}, // sxw -> Latn - {0xAF120000u, 7u}, // syl -> Beng - {0xC7120000u, 81u}, // syr -> Syrc - {0xAF320000u, 46u}, // szl -> Latn - {0x74610000u, 84u}, // ta -> Taml - {0xA4130000u, 18u}, // taj -> Deva - {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, 82u}, // tdd -> Tale - {0x98730000u, 18u}, // tdg -> Deva - {0x9C730000u, 18u}, // tdh -> Deva - {0xD0730000u, 46u}, // tdu -> Latn - {0x74650000u, 87u}, // te -> Telu - {0x8C930000u, 46u}, // ted -> Latn - {0xB0930000u, 46u}, // tem -> Latn - {0xB8930000u, 46u}, // teo -> Latn - {0xCC930000u, 46u}, // tet -> Latn - {0xA0B30000u, 46u}, // tfi -> Latn - {0x74670000u, 17u}, // tg -> Cyrl - {0x7467504Bu, 1u}, // tg-PK -> Arab - {0x88D30000u, 46u}, // tgc -> Latn - {0xB8D30000u, 46u}, // tgo -> Latn - {0xD0D30000u, 46u}, // tgu -> Latn - {0x74680000u, 90u}, // th -> Thai - {0xACF30000u, 18u}, // thl -> Deva - {0xC0F30000u, 18u}, // thq -> Deva - {0xC4F30000u, 18u}, // thr -> Deva - {0x74690000u, 20u}, // ti -> Ethi - {0x95130000u, 46u}, // tif -> Latn - {0x99130000u, 20u}, // tig -> Ethi - {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, 18u}, // tkt -> Deva - {0x746C0000u, 46u}, // tl -> Latn - {0x95730000u, 46u}, // tlf -> Latn - {0xDD730000u, 46u}, // tlx -> Latn - {0xE1730000u, 46u}, // tly -> Latn - {0x9D930000u, 46u}, // tmh -> Latn - {0xE1930000u, 46u}, // tmy -> Latn - {0x746E0000u, 46u}, // tn -> Latn - {0x9DB30000u, 46u}, // tnh -> Latn - {0x746F0000u, 46u}, // to -> Latn - {0x95D30000u, 46u}, // tof -> Latn - {0x99D30000u, 46u}, // tog -> Latn - {0xC1D30000u, 46u}, // toq -> Latn - {0xA1F30000u, 46u}, // tpi -> Latn - {0xB1F30000u, 46u}, // tpm -> Latn - {0xE5F30000u, 46u}, // tpz -> Latn - {0xBA130000u, 46u}, // tqo -> Latn - {0x74720000u, 46u}, // tr -> Latn - {0xD2330000u, 46u}, // tru -> Latn - {0xD6330000u, 46u}, // trv -> Latn - {0xDA330000u, 1u}, // trw -> Arab - {0x74730000u, 46u}, // ts -> Latn - {0x8E530000u, 25u}, // tsd -> Grek - {0x96530000u, 18u}, // tsf -> Deva - {0x9A530000u, 46u}, // tsg -> Latn - {0xA6530000u, 91u}, // tsj -> Tibt - {0xDA530000u, 46u}, // tsw -> Latn - {0x74740000u, 17u}, // tt -> Cyrl - {0x8E730000u, 46u}, // ttd -> Latn - {0x92730000u, 46u}, // tte -> Latn - {0xA6730000u, 46u}, // ttj -> Latn - {0xC6730000u, 46u}, // ttr -> Latn - {0xCA730000u, 90u}, // 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, 85u}, // txg -> Tang - {0x74790000u, 46u}, // ty -> Latn - {0x83130000u, 46u}, // tya -> Latn - {0xD7130000u, 17u}, // tyv -> Cyrl - {0xB3330000u, 46u}, // tzm -> Latn - {0xD0340000u, 46u}, // ubu -> Latn - {0xB0740000u, 17u}, // udm -> Cyrl - {0x75670000u, 1u}, // ug -> Arab - {0x75674B5Au, 17u}, // ug-KZ -> Cyrl - {0x75674D4Eu, 17u}, // ug-MN -> Cyrl - {0x80D40000u, 92u}, // uga -> Ugar - {0x756B0000u, 17u}, // uk -> Cyrl - {0xA1740000u, 46u}, // uli -> Latn - {0x85940000u, 46u}, // umb -> Latn - {0xC5B40000u, 7u}, // unr -> Beng - {0xC5B44E50u, 18u}, // unr-NP -> Deva - {0xDDB40000u, 7u}, // unx -> Beng - {0xA9D40000u, 46u}, // uok -> Latn - {0x75720000u, 1u}, // ur -> Arab - {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, 1u}, // uz-AF -> Arab - {0x757A434Eu, 17u}, // uz-CN -> Cyrl - {0x98150000u, 46u}, // vag -> Latn - {0xA0150000u, 93u}, // 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, 20u}, // wal -> Ethi - {0xB4160000u, 46u}, // wan -> Latn - {0xC4160000u, 46u}, // war -> Latn - {0xBC360000u, 46u}, // wbp -> Latn - {0xC0360000u, 87u}, // wbq -> Telu - {0xC4360000u, 18u}, // wbr -> Deva - {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, 1u}, // wni -> Arab - {0xD1B60000u, 46u}, // wnu -> Latn - {0x776F0000u, 46u}, // wo -> Latn - {0x85D60000u, 46u}, // wob -> Latn - {0xC9D60000u, 46u}, // wos -> Latn - {0xCA360000u, 46u}, // wrs -> Latn - {0x9A560000u, 22u}, // wsg -> Gong - {0xAA560000u, 46u}, // wsk -> Latn - {0xB2760000u, 18u}, // wtm -> Deva - {0xD2960000u, 28u}, // wuu -> Hans - {0xD6960000u, 46u}, // wuv -> Latn - {0x82D60000u, 46u}, // wwa -> Latn - {0xD4170000u, 46u}, // xav -> Latn - {0xA0370000u, 46u}, // xbi -> Latn - {0xB8570000u, 14u}, // xco -> Chrs - {0xC4570000u, 11u}, // xcr -> Cari - {0xC8970000u, 46u}, // xes -> Latn - {0x78680000u, 46u}, // xh -> Latn - {0x81770000u, 46u}, // xla -> Latn - {0x89770000u, 50u}, // xlc -> Lyci - {0x8D770000u, 51u}, // xld -> Lydi - {0x95970000u, 21u}, // xmf -> Geor - {0xB5970000u, 53u}, // xmn -> Mani + {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 + {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 + {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 + {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 + {0x86D20000u, 2u}, // swb -> Arab + {0x8AD20000u, 45u}, // swc -> Latn + {0x9AD20000u, 45u}, // swg -> Latn + {0xBED20000u, 45u}, // swp -> Latn + {0xD6D20000u, 19u}, // swv -> Deva + {0xB6F20000u, 45u}, // sxn -> Latn + {0xDAF20000u, 45u}, // sxw -> Latn + {0xAF120000u, 8u}, // syl -> Beng + {0xC7120000u, 83u}, // syr -> Syrc + {0xAF320000u, 45u}, // szl -> Latn + {0x74610000u, 86u}, // 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 + {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 + {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 + {0xACF30000u, 19u}, // thl -> Deva + {0xC0F30000u, 19u}, // thq -> Deva + {0xC4F30000u, 19u}, // thr -> Deva + {0x74690000u, 21u}, // ti -> Ethi + {0x95130000u, 45u}, // 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 + {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 + {0xDA330000u, 2u}, // trw -> Arab + {0x74730000u, 45u}, // ts -> Latn + {0x8E530000u, 26u}, // tsd -> Grek + {0x96530000u, 19u}, // tsf -> Deva + {0x9A530000u, 45u}, // tsg -> Latn + {0xA6530000u, 93u}, // tsj -> Tibt + {0xDA530000u, 45u}, // 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 + {0xD7130000u, 18u}, // tyv -> Cyrl + {0xB3330000u, 45u}, // tzm -> Latn + {0xD0340000u, 45u}, // 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 + {0x756B0000u, 18u}, // uk -> Cyrl + {0xA1740000u, 45u}, // uli -> Latn + {0x85940000u, 45u}, // umb -> Latn + {0xC5B40000u, 8u}, // unr -> Beng + {0xC5B44E50u, 19u}, // unr-NP -> Deva + {0xDDB40000u, 8u}, // unx -> Beng + {0xA9D40000u, 45u}, // 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 + {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 + {0xAC160000u, 21u}, // wal -> Ethi + {0xB4160000u, 45u}, // wan -> Latn + {0xC4160000u, 45u}, // war -> Latn + {0xBC360000u, 45u}, // wbp -> Latn + {0xC0360000u, 89u}, // 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 + {0xA1B60000u, 2u}, // wni -> Arab + {0xD1B60000u, 45u}, // wnu -> Latn + {0x776F0000u, 45u}, // wo -> Latn + {0x85D60000u, 45u}, // wob -> Latn + {0xC9D60000u, 45u}, // wos -> Latn + {0xCA360000u, 45u}, // wrs -> Latn + {0x9A560000u, 23u}, // wsg -> Gong + {0xAA560000u, 45u}, // 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 + {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 + {0x95970000u, 22u}, // xmf -> Geor + {0xB5970000u, 52u}, // xmn -> Mani {0xC5970000u, 54u}, // xmr -> Merc {0x81B70000u, 59u}, // xna -> Narb - {0xC5B70000u, 18u}, // xnr -> Deva - {0x99D70000u, 46u}, // xog -> Latn - {0xB5D70000u, 46u}, // xon -> Latn - {0xC5F70000u, 71u}, // xpr -> Prti - {0x86370000u, 46u}, // xrb -> Latn - {0x82570000u, 74u}, // xsa -> Sarb - {0xA2570000u, 46u}, // xsi -> Latn - {0xB2570000u, 46u}, // xsm -> Latn - {0xC6570000u, 18u}, // xsr -> Deva - {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 + {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 + {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 {0x79690000u, 31u}, // yi -> Hebr - {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, 29u}, // yue -> Hant - {0x9298434Eu, 28u}, // yue-CN -> Hans - {0xA6980000u, 46u}, // yuj -> Latn - {0xCE980000u, 46u}, // yut -> Latn - {0xDA980000u, 46u}, // yuw -> Latn - {0x7A610000u, 46u}, // za -> Latn - {0x98190000u, 46u}, // zag -> Latn - {0xA4790000u, 1u}, // zdj -> Arab - {0x80990000u, 46u}, // zea -> Latn - {0x9CD90000u, 88u}, // zgh -> Tfng - {0x7A680000u, 28u}, // zh -> Hans - {0x7A684155u, 29u}, // zh-AU -> Hant - {0x7A68424Eu, 29u}, // zh-BN -> Hant - {0x7A684742u, 29u}, // zh-GB -> Hant - {0x7A684746u, 29u}, // zh-GF -> Hant - {0x7A68484Bu, 29u}, // zh-HK -> Hant - {0x7A684944u, 29u}, // zh-ID -> Hant - {0x7A684D4Fu, 29u}, // zh-MO -> Hant - {0x7A685041u, 29u}, // zh-PA -> Hant - {0x7A685046u, 29u}, // zh-PF -> Hant - {0x7A685048u, 29u}, // zh-PH -> Hant - {0x7A685352u, 29u}, // zh-SR -> Hant - {0x7A685448u, 29u}, // zh-TH -> Hant - {0x7A685457u, 29u}, // zh-TW -> Hant - {0x7A685553u, 29u}, // zh-US -> Hant - {0x7A68564Eu, 29u}, // zh-VN -> Hant + {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 + {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 + {0xA4790000u, 2u}, // zdj -> Arab + {0x80990000u, 45u}, // zea -> Latn + {0x9CD90000u, 90u}, // zgh -> Tfng + {0x7A680000u, 29u}, // zh -> Hans + {0x7A684155u, 30u}, // zh-AU -> Hant + {0x7A68424Eu, 30u}, // zh-BN -> Hant + {0x7A684742u, 30u}, // zh-GB -> Hant + {0x7A684746u, 30u}, // zh-GF -> Hant + {0x7A68484Bu, 30u}, // zh-HK -> Hant + {0x7A684944u, 30u}, // zh-ID -> Hant + {0x7A684D4Fu, 30u}, // zh-MO -> Hant + {0x7A685041u, 30u}, // zh-PA -> Hant + {0x7A685046u, 30u}, // zh-PF -> Hant + {0x7A685048u, 30u}, // zh-PH -> Hant + {0x7A685352u, 30u}, // zh-SR -> Hant + {0x7A685448u, 30u}, // zh-TH -> Hant + {0x7A685457u, 30u}, // zh-TW -> Hant + {0x7A685553u, 30u}, // zh-US -> Hant + {0x7A68564Eu, 30u}, // zh-VN -> Hant {0xDCF90000u, 61u}, // 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 + {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 }); std::unordered_set<uint64_t> REPRESENTATIVE_LOCALES({ @@ -1571,6 +1580,7 @@ std::unordered_set<uint64_t> REPRESENTATIVE_LOCALES({ 0xCD21534E4C61746ELLU, // bjt_Latn_SN 0xB141434D4C61746ELLU, // bkm_Latn_CM 0xD14150484C61746ELLU, // bku_Latn_PH + 0x99614D594C61746ELLU, // blg_Latn_MY 0xCD61564E54617674LLU, // blt_Tavt_VN 0x626D4D4C4C61746ELLU, // bm_Latn_ML 0xC1814D4C4C61746ELLU, // bmq_Latn_ML @@ -1642,6 +1652,7 @@ std::unordered_set<uint64_t> REPRESENTATIVE_LOCALES({ 0xB48343414C61746ELLU, // den_Latn_CA 0xC4C343414C61746ELLU, // dgr_Latn_CA 0x91234E454C61746ELLU, // dje_Latn_NE + 0x95834E474D656466LLU, // dmf_Medf_NG 0xA5A343494C61746ELLU, // dnj_Latn_CI 0xA1C3494E44657661LLU, // doi_Deva_IN 0x9E23434E4D6F6E67LLU, // drh_Mong_CN @@ -1745,7 +1756,7 @@ std::unordered_set<uint64_t> REPRESENTATIVE_LOCALES({ 0x8D87434E506C7264LLU, // hmd_Plrd_CN 0x8DA7504B41726162LLU, // hnd_Arab_PK 0x91A7494E44657661LLU, // hne_Deva_IN - 0xA5A74C41486D6E67LLU, // hnj_Hmng_LA + 0xA5A75553486D6E70LLU, // hnj_Hmnp_US 0xB5A750484C61746ELLU, // hnn_Latn_PH 0xB9A7504B41726162LLU, // hno_Arab_PK 0x686F50474C61746ELLU, // ho_Latn_PG @@ -1794,7 +1805,7 @@ std::unordered_set<uint64_t> REPRESENTATIVE_LOCALES({ 0x984A4E474C61746ELLU, // kcg_Latn_NG 0xA84A5A574C61746ELLU, // kck_Latn_ZW 0x906A545A4C61746ELLU, // kde_Latn_TZ - 0x9C6A544741726162LLU, // kdh_Arab_TG + 0x9C6A54474C61746ELLU, // kdh_Latn_TG 0xCC6A544854686169LLU, // kdt_Thai_TH 0x808A43564C61746ELLU, // kea_Latn_CV 0xB48A434D4C61746ELLU, // ken_Latn_CM @@ -1917,8 +1928,6 @@ std::unordered_set<uint64_t> REPRESENTATIVE_LOCALES({ 0x6D684D484C61746ELLU, // mh_Latn_MH 0x6D694E5A4C61746ELLU, // mi_Latn_NZ 0xB50C49444C61746ELLU, // min_Latn_ID - 0xC90C495148617472LLU, // mis_Hatr_IQ - 0xC90C4E474D656466LLU, // mis_Medf_NG 0x6D6B4D4B4379726CLLU, // mk_Cyrl_MK 0x6D6C494E4D6C796DLLU, // ml_Mlym_IN 0xC96C53444C61746ELLU, // mls_Latn_SD @@ -1981,6 +1990,7 @@ std::unordered_set<uint64_t> REPRESENTATIVE_LOCALES({ 0x6E725A414C61746ELLU, // nr_Latn_ZA 0xAA4D434143616E73LLU, // nsk_Cans_CA 0xBA4D5A414C61746ELLU, // nso_Latn_ZA + 0xCE4D494E546E7361LLU, // nst_Tnsa_IN 0xCA8D53534C61746ELLU, // nus_Latn_SS 0x6E7655534C61746ELLU, // nv_Latn_US 0xC2ED434E4C61746ELLU, // nxq_Latn_CN @@ -1994,6 +2004,7 @@ std::unordered_set<uint64_t> REPRESENTATIVE_LOCALES({ 0x6F7347454379726CLLU, // os_Cyrl_GE 0x824E55534F736765LLU, // osa_Osge_US 0xAA6E4D4E4F726B68LLU, // otk_Orkh_MN + 0xA28E8C814F756772LLU, // oui_Ougr_143 0x7061504B41726162LLU, // pa_Arab_PK 0x7061494E47757275LLU, // pa_Guru_IN 0x980F50484C61746ELLU, // pag_Latn_PH @@ -2028,7 +2039,7 @@ std::unordered_set<uint64_t> REPRESENTATIVE_LOCALES({ 0x945152454C61746ELLU, // rcf_Latn_RE 0xA49149444C61746ELLU, // rej_Latn_ID 0xB4D149544C61746ELLU, // rgn_Latn_IT - 0x98F14D4D41726162LLU, // rhg_Arab_MM + 0x98F14D4D526F6867LLU, // rhg_Rohg_MM 0x8111494E4C61746ELLU, // ria_Latn_IN 0x95114D4154666E67LLU, // rif_Tfng_MA 0xC9314E5044657661LLU, // rjs_Deva_NP @@ -2171,9 +2182,11 @@ std::unordered_set<uint64_t> REPRESENTATIVE_LOCALES({ 0xAEB354564C61746ELLU, // tvl_Latn_TV 0xC2D34E454C61746ELLU, // twq_Latn_NE 0x9AF3434E54616E67LLU, // txg_Tang_CN + 0xBAF3494E546F746FLLU, // txo_Toto_IN 0x747950464C61746ELLU, // ty_Latn_PF 0xD71352554379726CLLU, // tyv_Cyrl_RU 0xB3334D414C61746ELLU, // tzm_Latn_MA + 0xA074525541676862LLU, // udi_Aghb_RU 0xB07452554379726CLLU, // udm_Cyrl_RU 0x7567434E41726162LLU, // ug_Arab_CN 0x75674B5A4379726CLLU, // ug_Cyrl_KZ @@ -2254,6 +2267,7 @@ std::unordered_set<uint64_t> REPRESENTATIVE_LOCALES({ }); const std::unordered_map<uint32_t, uint32_t> ARAB_PARENTS({ + {0x61724145u, 0x61729420u}, // ar-AE -> ar-015 {0x6172445Au, 0x61729420u}, // ar-DZ -> ar-015 {0x61724548u, 0x61729420u}, // ar-EH -> ar-015 {0x61724C59u, 0x61729420u}, // ar-LY -> ar-015 @@ -2277,7 +2291,6 @@ const std::unordered_map<uint32_t, uint32_t> LATN_PARENTS({ {0x656E4253u, 0x656E8400u}, // en-BS -> en-001 {0x656E4257u, 0x656E8400u}, // en-BW -> en-001 {0x656E425Au, 0x656E8400u}, // en-BZ -> en-001 - {0x656E4341u, 0x656E8400u}, // en-CA -> en-001 {0x656E4343u, 0x656E8400u}, // en-CC -> en-001 {0x656E4348u, 0x656E80A1u}, // en-CH -> en-150 {0x656E434Bu, 0x656E8400u}, // en-CK -> en-001 @@ -2330,7 +2343,6 @@ const std::unordered_map<uint32_t, uint32_t> LATN_PARENTS({ {0x656E4E55u, 0x656E8400u}, // en-NU -> en-001 {0x656E4E5Au, 0x656E8400u}, // en-NZ -> en-001 {0x656E5047u, 0x656E8400u}, // en-PG -> en-001 - {0x656E5048u, 0x656E8400u}, // en-PH -> en-001 {0x656E504Bu, 0x656E8400u}, // en-PK -> en-001 {0x656E504Eu, 0x656E8400u}, // en-PN -> en-001 {0x656E5057u, 0x656E8400u}, // en-PW -> en-001 @@ -2382,6 +2394,8 @@ const std::unordered_map<uint32_t, uint32_t> LATN_PARENTS({ {0x65735553u, 0x6573A424u}, // es-US -> es-419 {0x65735559u, 0x6573A424u}, // es-UY -> es-419 {0x65735645u, 0x6573A424u}, // es-VE -> es-419 + {0x6E620000u, 0x6E6F0000u}, // nb -> no + {0x6E6E0000u, 0x6E6F0000u}, // nn -> no {0x7074414Fu, 0x70745054u}, // pt-AO -> pt-PT {0x70744348u, 0x70745054u}, // pt-CH -> pt-PT {0x70744356u, 0x70745054u}, // pt-CV -> pt-PT diff --git a/libs/androidfw/OWNERS b/libs/androidfw/OWNERS index 610fd80fe73c..17f5164cf417 100644 --- a/libs/androidfw/OWNERS +++ b/libs/androidfw/OWNERS @@ -1,6 +1,6 @@ set noparent toddke@google.com -rtmitchell@google.com +zyy@google.com patb@google.com per-file CursorWindow.cpp=omakoto@google.com diff --git a/libs/androidfw/PosixUtils.cpp b/libs/androidfw/PosixUtils.cpp index 4ec525a01da5..026912883a73 100644 --- a/libs/androidfw/PosixUtils.cpp +++ b/libs/androidfw/PosixUtils.cpp @@ -114,10 +114,10 @@ std::unique_ptr<ProcResult> ExecuteBinary(const std::vector<std::string>& argv) std::unique_ptr<ProcResult> result(new ProcResult()); result->status = status; const auto out = ReadFile(stdout[0]); - result->stdout = out ? *out : ""; + result->stdout_str = out ? *out : ""; close(stdout[0]); const auto err = ReadFile(stderr[0]); - result->stderr = err ? *err : ""; + result->stderr_str = err ? *err : ""; close(stderr[0]); return result; } diff --git a/libs/androidfw/ResourceTypes.cpp b/libs/androidfw/ResourceTypes.cpp index cae2d0bc16b3..5e8a623d4205 100644 --- a/libs/androidfw/ResourceTypes.cpp +++ b/libs/androidfw/ResourceTypes.cpp @@ -2677,30 +2677,27 @@ bool ResTable_config::isBetterThan(const ResTable_config& o, // DENSITY_ANY is now dealt with. We should look to // pick a density bucket and potentially scale it. // Any density is potentially useful - // because the system will scale it. Scaling down - // is generally better than scaling up. + // because the system will scale it. Always prefer + // scaling down. int h = thisDensity; int l = otherDensity; bool bImBigger = true; if (l > h) { - int t = h; - h = l; - l = t; + std::swap(l, h); bImBigger = false; } - if (requestedDensity >= h) { - // requested value higher than both l and h, give h + if (h == requestedDensity) { + // This handles the case where l == h == requestedDensity. + // In that case, this and o are equally good so both + // true and false are valid. This preserves previous + // behavior. return bImBigger; - } - if (l >= requestedDensity) { + } else if (l >= requestedDensity) { // requested value lower than both l and h, give l return !bImBigger; - } - // saying that scaling down is 2x better than up - if (((2 * l) - requestedDensity) * h > requestedDensity * requestedDensity) { - return !bImBigger; } else { + // otherwise give h return bImBigger; } } diff --git a/libs/androidfw/TEST_MAPPING b/libs/androidfw/TEST_MAPPING index 9ebc9969a730..8abe79d01642 100644 --- a/libs/androidfw/TEST_MAPPING +++ b/libs/androidfw/TEST_MAPPING @@ -2,6 +2,9 @@ "presubmit": [ { "name": "CtsResourcesLoaderTests" + }, + { + "name": "libandroidfw_tests" } ] } diff --git a/libs/androidfw/include/androidfw/AssetManager2.h b/libs/androidfw/include/androidfw/AssetManager2.h index 7d01395bbbbc..1bde792da2ba 100644 --- a/libs/androidfw/include/androidfw/AssetManager2.h +++ b/libs/androidfw/include/androidfw/AssetManager2.h @@ -192,6 +192,12 @@ class AssetManager2 { std::unique_ptr<Asset> OpenNonAsset(const std::string& filename, ApkAssetsCookie cookie, Asset::AccessMode mode) const; + // Returns the resource id of parent style of the specified theme. + // + // Returns a null error if the name is missing/corrupt, or an I/O error if reading resource data + // failed. + base::expected<uint32_t, NullOrIOError> GetParentThemeResourceId(uint32_t resid) const; + // Returns the resource name of the specified resource ID. // // Utf8 strings are preferred, and only if they are unavailable are the Utf16 variants populated. @@ -486,6 +492,12 @@ class AssetManager2 { // Steps taken to resolve last resource. std::vector<Step> steps; + + // The configuration name of the best resource found. + String8 best_config_name; + + // The package name of the best resource found. + String8 best_package_name; }; // Record of the last resolved resource's resolution path. diff --git a/libs/androidfw/include/androidfw/PosixUtils.h b/libs/androidfw/include/androidfw/PosixUtils.h index 8fc3ee2733c7..bb2084740a44 100644 --- a/libs/androidfw/include/androidfw/PosixUtils.h +++ b/libs/androidfw/include/androidfw/PosixUtils.h @@ -23,8 +23,8 @@ namespace util { struct ProcResult { int status; - std::string stdout; - std::string stderr; + std::string stdout_str; + std::string stderr_str; }; // Fork, exec and wait for an external process. Return nullptr if the process could not be launched, diff --git a/libs/androidfw/tests/AssetManager2_test.cpp b/libs/androidfw/tests/AssetManager2_test.cpp index 3c4ee4e63a76..4394740e44ba 100644 --- a/libs/androidfw/tests/AssetManager2_test.cpp +++ b/libs/androidfw/tests/AssetManager2_test.cpp @@ -766,7 +766,9 @@ TEST_F(AssetManager2Test, GetLastPathWithSingleApkAssets) { auto result = assetmanager.GetLastResourceResolution(); EXPECT_EQ("Resolution for 0x7f030000 com.android.basic:string/test1\n" "\tFor config - de\n" - "\tFound initial: basic/basic.apk", result); + "\tFound initial: basic/basic.apk\n" + "Best matching is from default configuration of com.android.basic", + result); } TEST_F(AssetManager2Test, GetLastPathWithMultipleApkAssets) { @@ -787,7 +789,9 @@ TEST_F(AssetManager2Test, GetLastPathWithMultipleApkAssets) { EXPECT_EQ("Resolution for 0x7f030000 com.android.basic:string/test1\n" "\tFor config - de\n" "\tFound initial: basic/basic.apk\n" - "\tFound better: basic/basic_de_fr.apk - de", result); + "\tFound better: basic/basic_de_fr.apk - de\n" + "Best matching is from de configuration of com.android.basic", + result); } TEST_F(AssetManager2Test, GetLastPathAfterDisablingReturnsEmpty) { diff --git a/libs/androidfw/tests/BackupHelpers_test.cpp b/libs/androidfw/tests/BackupHelpers_test.cpp index 86b7fb361228..c2fcb6990f90 100644 --- a/libs/androidfw/tests/BackupHelpers_test.cpp +++ b/libs/androidfw/tests/BackupHelpers_test.cpp @@ -50,7 +50,7 @@ TEST_F(BackupHelpersTest, WriteTarFileWithSizeLessThan2GB) { TEST_F(BackupHelpersTest, WriteTarFileWithSizeGreaterThan2GB) { TemporaryFile tf; // Allocate a 2 GB file. - off64_t fileSize = 2ll * 1024ll * 1024ll * 1024ll + 512ll; + off64_t fileSize = 2LL * 1024LL * 1024LL * 1024LL + 512LL; ASSERT_EQ(0, posix_fallocate64(tf.fd, 0, fileSize)); off64_t tarSize = 0; int err = write_tarfile(/* packageName */ String8("test-pkg"), /* domain */ String8(""), /* rootpath */ String8(""), /* filePath */ String8(tf.path), /* outSize */ &tarSize, /* writer */ NULL); diff --git a/libs/androidfw/tests/Config_test.cpp b/libs/androidfw/tests/Config_test.cpp index b54915f03c29..698c36f09301 100644 --- a/libs/androidfw/tests/Config_test.cpp +++ b/libs/androidfw/tests/Config_test.cpp @@ -75,6 +75,9 @@ TEST(ConfigTest, shouldSelectBestDensity) { configs.add(buildDensityConfig(int(ResTable_config::DENSITY_HIGH) + 20)); ASSERT_EQ(expectedBest, selectBest(deviceConfig, configs)); + configs.add(buildDensityConfig(int(ResTable_config::DENSITY_XHIGH) - 1)); + ASSERT_EQ(expectedBest, selectBest(deviceConfig, configs)); + expectedBest = buildDensityConfig(ResTable_config::DENSITY_XHIGH); configs.add(expectedBest); ASSERT_EQ(expectedBest, selectBest(deviceConfig, configs)); diff --git a/libs/androidfw/tests/PosixUtils_test.cpp b/libs/androidfw/tests/PosixUtils_test.cpp index cf97f87a4163..8c49350796ec 100644 --- a/libs/androidfw/tests/PosixUtils_test.cpp +++ b/libs/androidfw/tests/PosixUtils_test.cpp @@ -30,14 +30,14 @@ TEST(PosixUtilsTest, AbsolutePathToBinary) { const auto result = ExecuteBinary({"/bin/date", "--help"}); ASSERT_THAT(result, NotNull()); ASSERT_EQ(result->status, 0); - ASSERT_EQ(result->stdout.find("usage: date "), 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_EQ(result->stdout.find("usage: date "), 0); + ASSERT_GE(result->stdout_str.find("usage: date "), 0); } TEST(PosixUtilsTest, BadParameters) { diff --git a/libs/hwui/Android.bp b/libs/hwui/Android.bp index 2b31bcf78890..ad9aa6cdd3d9 100644 --- a/libs/hwui/Android.bp +++ b/libs/hwui/Android.bp @@ -35,7 +35,6 @@ cc_defaults { "skia_deps", //"hwui_bugreport_font_cache_usage", //"hwui_compile_for_perf", - "hwui_pgo", "hwui_lto", ], @@ -107,6 +106,9 @@ 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", @@ -126,9 +128,11 @@ cc_defaults { static_libs: [ "libEGL_blobCache", "libprotoutil", + "libshaders", "libstatslog_hwui", "libstatspull_lazy", "libstatssocket_lazy", + "libtonemap", ], }, host: { @@ -155,22 +159,6 @@ cc_defaults { ], } -// Build libhwui with PGO by default. -// Location of PGO profile data is defined in build/soong/cc/pgo.go -// and is separate from hwui. -// To turn it off, set ANDROID_PGO_NO_PROFILE_USE environment variable -// or set enable_profile_use property to false. -cc_defaults { - name: "hwui_pgo", - - pgo: { - instrumentation: true, - profile_file: "hwui/hwui.profdata", - benchmarks: ["hwui"], - enable_profile_use: true, - }, -} - // Build hwui library with ThinLTO by default. cc_defaults { name: "hwui_lto", @@ -262,6 +250,7 @@ cc_defaults { "apex/android_matrix.cpp", "apex/android_paint.cpp", "apex/android_region.cpp", + "apex/properties.cpp", ], header_libs: ["android_graphics_apex_headers"], @@ -272,7 +261,6 @@ cc_defaults { "apex/android_bitmap.cpp", "apex/android_canvas.cpp", "apex/jni_runtime.cpp", - "apex/renderthread.cpp", ], }, host: { @@ -359,6 +347,7 @@ cc_defaults { "jni/PathEffect.cpp", "jni/PathMeasure.cpp", "jni/Picture.cpp", + "jni/Region.cpp", "jni/Shader.cpp", "jni/RenderEffect.cpp", "jni/Typeface.cpp", @@ -408,7 +397,6 @@ cc_defaults { "jni/GraphicsStatsService.cpp", "jni/Movie.cpp", "jni/MovieImpl.cpp", - "jni/Region.cpp", // requires libbinder_ndk "jni/pdf/PdfDocument.cpp", "jni/pdf/PdfEditor.cpp", "jni/pdf/PdfRenderer.cpp", @@ -549,7 +537,10 @@ cc_defaults { target: { android: { - header_libs: ["libandroid_headers_private"], + header_libs: [ + "libandroid_headers_private", + "libtonemap_headers", + ], srcs: [ "hwui/AnimatedImageThread.cpp", @@ -588,6 +579,7 @@ cc_defaults { "HardwareBitmapUploader.cpp", "HWUIProperties.sysprop", "JankTracker.cpp", + "FrameMetricsReporter.cpp", "Layer.cpp", "LayerUpdateQueue.cpp", "ProfileData.cpp", @@ -627,6 +619,7 @@ cc_library { version_script: "libhwui.map.txt", }, }, + afdo: true, } cc_library_static { @@ -692,6 +685,7 @@ cc_test { "tests/unit/FatVectorTests.cpp", "tests/unit/GraphicsStatsServiceTests.cpp", "tests/unit/JankTrackerTests.cpp", + "tests/unit/FrameMetricsReporterTests.cpp", "tests/unit/LayerUpdateQueueTests.cpp", "tests/unit/LinearAllocatorTests.cpp", "tests/unit/MatrixTests.cpp", @@ -759,15 +753,3 @@ cc_benchmark { "tests/microbench/RenderNodeBench.cpp", ], } - -// ---------------------------------------- -// Phony target to build benchmarks for PGO -// ---------------------------------------- - -phony { - name: "pgo-targets-hwui", - required: [ - "hwuimicro", - "hwuimacro", - ], -} diff --git a/libs/hwui/AnimatorManager.cpp b/libs/hwui/AnimatorManager.cpp index 4826d5a0c8da..078041411a21 100644 --- a/libs/hwui/AnimatorManager.cpp +++ b/libs/hwui/AnimatorManager.cpp @@ -31,7 +31,8 @@ static void detach(sp<BaseRenderNodeAnimator>& animator) { animator->detach(); } -AnimatorManager::AnimatorManager(RenderNode& parent) : mParent(parent), mAnimationHandle(nullptr) {} +AnimatorManager::AnimatorManager(RenderNode& parent) + : mParent(parent), mAnimationHandle(nullptr), mCancelAllAnimators(false) {} AnimatorManager::~AnimatorManager() { for_each(mNewAnimators.begin(), mNewAnimators.end(), detach); @@ -82,8 +83,16 @@ void AnimatorManager::pushStaging() { } mNewAnimators.clear(); } - for (auto& animator : mAnimators) { - animator->pushStaging(mAnimationHandle->context()); + + if (mCancelAllAnimators) { + for (auto& animator : mAnimators) { + animator->forceEndNow(mAnimationHandle->context()); + } + mCancelAllAnimators = false; + } else { + for (auto& animator : mAnimators) { + animator->pushStaging(mAnimationHandle->context()); + } } } @@ -184,5 +193,9 @@ void AnimatorManager::endAllActiveAnimators() { mAnimationHandle->release(); } +void AnimatorManager::forceEndAnimators() { + mCancelAllAnimators = true; +} + } /* namespace uirenderer */ } /* namespace android */ diff --git a/libs/hwui/AnimatorManager.h b/libs/hwui/AnimatorManager.h index a0df01d5962c..6002661dc82a 100644 --- a/libs/hwui/AnimatorManager.h +++ b/libs/hwui/AnimatorManager.h @@ -16,11 +16,11 @@ #ifndef ANIMATORMANAGER_H #define ANIMATORMANAGER_H -#include <vector> - #include <cutils/compiler.h> #include <utils/StrongPointer.h> +#include <vector> + #include "utils/Macros.h" namespace android { @@ -56,6 +56,8 @@ public: // Hard-ends all animators. May only be called on the UI thread. void endAllStagingAnimators(); + void forceEndAnimators(); + // Hard-ends all animators that have been pushed. Used for cleanup if // the ActivityContext is being destroyed void endAllActiveAnimators(); @@ -71,6 +73,8 @@ private: // To improve the efficiency of resizing & removing from the vector std::vector<sp<BaseRenderNodeAnimator> > mNewAnimators; std::vector<sp<BaseRenderNodeAnimator> > mAnimators; + + bool mCancelAllAnimators; }; } /* namespace uirenderer */ diff --git a/libs/hwui/ColorMode.h b/libs/hwui/ColorMode.h index 6d387f9ef43d..3df5c3c9caed 100644 --- a/libs/hwui/ColorMode.h +++ b/libs/hwui/ColorMode.h @@ -29,6 +29,8 @@ enum class ColorMode { Hdr = 2, // HDR Rec2020 + 1010102 Hdr10 = 3, + // Alpha 8 + A8 = 4, }; } // namespace android::uirenderer diff --git a/libs/hwui/DeferredLayerUpdater.cpp b/libs/hwui/DeferredLayerUpdater.cpp index 8d112d1c64bf..a5c0924579eb 100644 --- a/libs/hwui/DeferredLayerUpdater.cpp +++ b/libs/hwui/DeferredLayerUpdater.cpp @@ -20,9 +20,11 @@ // TODO: Use public SurfaceTexture APIs once available and include public NDK header file instead. #include <surfacetexture/surface_texture_platform.h> + #include "AutoBackendTextureRelease.h" #include "Matrix.h" #include "Properties.h" +#include "android/hdr_metadata.h" #include "renderstate/RenderState.h" #include "renderthread/EglManager.h" #include "renderthread/RenderThread.h" @@ -147,14 +149,20 @@ void DeferredLayerUpdater::apply() { mUpdateTexImage = false; float transformMatrix[16]; android_dataspace dataspace; + AHdrMetadataType hdrMetadataType; + android_cta861_3_metadata cta861_3; + android_smpte2086_metadata smpte2086; int slot; bool newContent = false; + ARect currentCrop; + uint32_t outTransform; // Note: ASurfaceTexture_dequeueBuffer discards all but the last frame. This // is necessary if the SurfaceTexture queue is in synchronous mode, and we // cannot tell which mode it is in. AHardwareBuffer* hardwareBuffer = ASurfaceTexture_dequeueBuffer( - mSurfaceTexture.get(), &slot, &dataspace, transformMatrix, &newContent, - createReleaseFence, fenceWait, this); + mSurfaceTexture.get(), &slot, &dataspace, &hdrMetadataType, &cta861_3, + &smpte2086, transformMatrix, &outTransform, &newContent, createReleaseFence, + fenceWait, this, ¤tCrop); if (hardwareBuffer) { mCurrentSlot = slot; @@ -165,12 +173,24 @@ void DeferredLayerUpdater::apply() { // (invoked by createIfNeeded) will add a ref to the AHardwareBuffer. AHardwareBuffer_release(hardwareBuffer); if (layerImage.get()) { - SkMatrix textureTransform; - mat4(transformMatrix).copyTo(textureTransform); // force filtration if buffer size != layer size bool forceFilter = mWidth != layerImage->width() || mHeight != layerImage->height(); - updateLayer(forceFilter, textureTransform, layerImage); + SkRect currentCropRect = + SkRect::MakeLTRB(currentCrop.left, currentCrop.top, currentCrop.right, + currentCrop.bottom); + + float maxLuminanceNits = -1.f; + if (hdrMetadataType & HDR10_SMPTE2086) { + maxLuminanceNits = std::max(smpte2086.maxLuminance, maxLuminanceNits); + } + + if (hdrMetadataType & HDR10_CTA861_3) { + maxLuminanceNits = + std::max(cta861_3.maxContentLightLevel, maxLuminanceNits); + } + updateLayer(forceFilter, layerImage, outTransform, currentCropRect, + maxLuminanceNits); } } } @@ -182,13 +202,16 @@ void DeferredLayerUpdater::apply() { } } -void DeferredLayerUpdater::updateLayer(bool forceFilter, const SkMatrix& textureTransform, - const sk_sp<SkImage>& layerImage) { +void DeferredLayerUpdater::updateLayer(bool forceFilter, const sk_sp<SkImage>& layerImage, + const uint32_t transform, SkRect currentCrop, + float maxLuminanceNits) { mLayer->setBlend(mBlend); mLayer->setForceFilter(forceFilter); mLayer->setSize(mWidth, mHeight); - mLayer->getTexTransform() = textureTransform; + mLayer->setCurrentCropRect(currentCrop); + mLayer->setWindowTransform(transform); mLayer->setImage(layerImage); + mLayer->setMaxLuminanceNits(maxLuminanceNits); } void DeferredLayerUpdater::detachSurfaceTexture() { diff --git a/libs/hwui/DeferredLayerUpdater.h b/libs/hwui/DeferredLayerUpdater.h index 8f79c4ec97b8..9a4c5505fa35 100644 --- a/libs/hwui/DeferredLayerUpdater.h +++ b/libs/hwui/DeferredLayerUpdater.h @@ -90,8 +90,8 @@ public: void detachSurfaceTexture(); - void updateLayer(bool forceFilter, const SkMatrix& textureTransform, - const sk_sp<SkImage>& layerImage); + void updateLayer(bool forceFilter, const sk_sp<SkImage>& layerImage, const uint32_t transform, + SkRect currentCrop, float maxLuminanceNits = -1.f); void destroyLayer(); diff --git a/libs/hwui/DisplayListOps.in b/libs/hwui/DisplayListOps.in index fb3e21fc1571..4ec782f6fec0 100644 --- a/libs/hwui/DisplayListOps.in +++ b/libs/hwui/DisplayListOps.in @@ -27,6 +27,7 @@ X(ClipPath) X(ClipRect) X(ClipRRect) X(ClipRegion) +X(ResetClip) X(DrawPaint) X(DrawBehind) X(DrawPath) diff --git a/libs/hwui/FrameInfo.cpp b/libs/hwui/FrameInfo.cpp index fecf26906c04..8191f5e6a83a 100644 --- a/libs/hwui/FrameInfo.cpp +++ b/libs/hwui/FrameInfo.cpp @@ -20,19 +20,33 @@ namespace android { namespace uirenderer { -const std::array FrameInfoNames{ - "Flags", "FrameTimelineVsyncId", "IntendedVsync", - "Vsync", "InputEventId", "HandleInputStart", - "AnimationStart", "PerformTraversalsStart", "DrawStart", - "FrameDeadline", "FrameInterval", "FrameStartTime", - "SyncQueued", "SyncStart", "IssueDrawCommandsStart", - "SwapBuffers", "FrameCompleted", "DequeueBufferDuration", - "QueueBufferDuration", "GpuCompleted", "SwapBuffersCompleted", - "DisplayPresentTime", +const std::array FrameInfoNames{"Flags", + "FrameTimelineVsyncId", + "IntendedVsync", + "Vsync", + "InputEventId", + "HandleInputStart", + "AnimationStart", + "PerformTraversalsStart", + "DrawStart", + "FrameDeadline", + "FrameInterval", + "FrameStartTime", + "SyncQueued", + "SyncStart", + "IssueDrawCommandsStart", + "SwapBuffers", + "FrameCompleted", + "DequeueBufferDuration", + "QueueBufferDuration", + "GpuCompleted", + "SwapBuffersCompleted", + "DisplayPresentTime", + "CommandSubmissionCompleted" }; -static_assert(static_cast<int>(FrameInfoIndex::NumIndexes) == 22, +static_assert(static_cast<int>(FrameInfoIndex::NumIndexes) == 23, "Must update value in FrameMetrics.java#FRAME_STATS_COUNT (and here)"); void FrameInfo::importUiThreadInfo(int64_t* info) { diff --git a/libs/hwui/FrameInfo.h b/libs/hwui/FrameInfo.h index 540a88b16dc9..564ee4f53a54 100644 --- a/libs/hwui/FrameInfo.h +++ b/libs/hwui/FrameInfo.h @@ -58,6 +58,7 @@ enum class FrameInfoIndex { GpuCompleted, SwapBuffersCompleted, DisplayPresentTime, + CommandSubmissionCompleted, // Must be the last value! // Also must be kept in sync with FrameMetrics.java#FRAME_STATS_COUNT diff --git a/libs/hwui/FrameMetricsObserver.h b/libs/hwui/FrameMetricsObserver.h index ef1f5aabcbd8..3ea49518eecd 100644 --- a/libs/hwui/FrameMetricsObserver.h +++ b/libs/hwui/FrameMetricsObserver.h @@ -26,6 +26,13 @@ public: virtual void notify(const int64_t* buffer) = 0; bool waitForPresentTime() const { return mWaitForPresentTime; }; + void reportMetricsFrom(uint64_t frameNumber, int32_t surfaceControlId) { + mAttachedFrameNumber = frameNumber; + mSurfaceControlId = surfaceControlId; + }; + uint64_t attachedFrameNumber() const { return mAttachedFrameNumber; }; + int32_t attachedSurfaceControlId() const { return mSurfaceControlId; }; + /** * Create a new metrics observer. An observer that watches present time gets notified at a * different time than the observer that doesn't. @@ -38,10 +45,29 @@ public: * WARNING! This observer may not receive metrics for the last several frames that the app * produces. */ - FrameMetricsObserver(bool waitForPresentTime) : mWaitForPresentTime(waitForPresentTime) {} + FrameMetricsObserver(bool waitForPresentTime) + : mWaitForPresentTime(waitForPresentTime) + , mSurfaceControlId(INT32_MAX) + , mAttachedFrameNumber(UINT64_MAX) {} private: const bool mWaitForPresentTime; + + // The id of the surface control (mSurfaceControlGenerationId in CanvasContext) + // for which the mAttachedFrameNumber applies to. We rely on this value being + // an increasing counter. We will report metrics: + // - for all frames if the frame comes from a surface with a surfaceControlId + // that is strictly greater than mSurfaceControlId. + // - for all frames with a frame number greater than or equal to mAttachedFrameNumber + // if the frame comes from a surface with a surfaceControlId that is equal to the + // mSurfaceControlId. + // We will never report metrics if the frame comes from a surface with a surfaceControlId + // that is strictly smaller than mSurfaceControlId. + int32_t mSurfaceControlId; + + // The frame number the metrics observer was attached on. Metrics will be sent from this frame + // number (inclusive) onwards in the case that the surface id is equal to mSurfaceControlId. + uint64_t mAttachedFrameNumber; }; } // namespace uirenderer diff --git a/libs/hwui/FrameMetricsReporter.cpp b/libs/hwui/FrameMetricsReporter.cpp new file mode 100644 index 000000000000..ee32ea17bfaf --- /dev/null +++ b/libs/hwui/FrameMetricsReporter.cpp @@ -0,0 +1,56 @@ +/* + * 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. + */ + +#include "FrameMetricsReporter.h" + +namespace android { +namespace uirenderer { + +void FrameMetricsReporter::reportFrameMetrics(const int64_t* stats, bool hasPresentTime, + uint64_t frameNumber, int32_t surfaceControlId) { + FatVector<sp<FrameMetricsObserver>, 10> copy; + { + std::lock_guard lock(mObserversLock); + copy.reserve(mObservers.size()); + for (size_t i = 0; i < mObservers.size(); i++) { + auto observer = mObservers[i]; + + if (CC_UNLIKELY(surfaceControlId < observer->attachedSurfaceControlId())) { + // Don't notify if the metrics are from a frame that was run on an old + // surface (one from before the observer was attached). + ALOGV("skipped reporting metrics from old surface %d", surfaceControlId); + continue; + } else if (CC_UNLIKELY(surfaceControlId == observer->attachedSurfaceControlId() && + frameNumber < observer->attachedFrameNumber())) { + // Don't notify if the metrics are from a frame that was queued by the + // BufferQueueProducer on the render thread before the observer was attached. + ALOGV("skipped reporting metrics from old frame %ld", (long)frameNumber); + continue; + } + + const bool wantsPresentTime = observer->waitForPresentTime(); + if (hasPresentTime == wantsPresentTime) { + copy.push_back(observer); + } + } + } + for (size_t i = 0; i < copy.size(); i++) { + copy[i]->notify(stats); + } +} + +} // namespace uirenderer +} // namespace android diff --git a/libs/hwui/FrameMetricsReporter.h b/libs/hwui/FrameMetricsReporter.h index 0ac025fb01db..7e51df7ce6fc 100644 --- a/libs/hwui/FrameMetricsReporter.h +++ b/libs/hwui/FrameMetricsReporter.h @@ -63,23 +63,14 @@ public: * If an observer does not want present time, only notify when 'hasPresentTime' is false. * Never notify both types of observers from the same callback, because the callback with * 'hasPresentTime' is sent at a different time than the one without. + * + * The 'frameNumber' and 'surfaceControlId' associated to the frame whose's stats are being + * reported are used to determine whether or not the stats should be reported. We won't report + * stats of frames that are from "old" surfaces (i.e. with surfaceControlIds older than the one + * the observer was attached on) nor those that are from "old" frame numbers. */ - void reportFrameMetrics(const int64_t* stats, bool hasPresentTime) { - FatVector<sp<FrameMetricsObserver>, 10> copy; - { - std::lock_guard lock(mObserversLock); - copy.reserve(mObservers.size()); - for (size_t i = 0; i < mObservers.size(); i++) { - const bool wantsPresentTime = mObservers[i]->waitForPresentTime(); - if (hasPresentTime == wantsPresentTime) { - copy.push_back(mObservers[i]); - } - } - } - for (size_t i = 0; i < copy.size(); i++) { - copy[i]->notify(stats); - } - } + void reportFrameMetrics(const int64_t* stats, bool hasPresentTime, uint64_t frameNumber, + int32_t surfaceControlId); private: FatVector<sp<FrameMetricsObserver>, 10> mObservers GUARDED_BY(mObserversLock); diff --git a/libs/hwui/HardwareBitmapUploader.cpp b/libs/hwui/HardwareBitmapUploader.cpp index db3a1081e32c..c24cabb287de 100644 --- a/libs/hwui/HardwareBitmapUploader.cpp +++ b/libs/hwui/HardwareBitmapUploader.cpp @@ -287,29 +287,34 @@ private: std::mutex mVkLock; }; -bool HardwareBitmapUploader::hasFP16Support() { - static std::once_flag sOnce; - static bool hasFP16Support = false; - - // Gralloc shouldn't let us create a USAGE_HW_TEXTURE if GLES is unable to consume it, so - // we don't need to double-check the GLES version/extension. - std::call_once(sOnce, []() { - AHardwareBuffer_Desc desc = { - .width = 1, - .height = 1, - .layers = 1, - .format = AHARDWAREBUFFER_FORMAT_R16G16B16A16_FLOAT, - .usage = AHARDWAREBUFFER_USAGE_CPU_READ_NEVER | - AHARDWAREBUFFER_USAGE_CPU_WRITE_NEVER | - AHARDWAREBUFFER_USAGE_GPU_SAMPLED_IMAGE, - }; - UniqueAHardwareBuffer buffer = allocateAHardwareBuffer(desc); - hasFP16Support = buffer != nullptr; - }); +static bool checkSupport(AHardwareBuffer_Format format) { + AHardwareBuffer_Desc desc = { + .width = 1, + .height = 1, + .layers = 1, + .format = format, + .usage = AHARDWAREBUFFER_USAGE_CPU_READ_NEVER | AHARDWAREBUFFER_USAGE_CPU_WRITE_NEVER | + AHARDWAREBUFFER_USAGE_GPU_SAMPLED_IMAGE, + }; + UniqueAHardwareBuffer buffer = allocateAHardwareBuffer(desc); + return buffer != nullptr; +} +bool HardwareBitmapUploader::hasFP16Support() { + static bool hasFP16Support = checkSupport(AHARDWAREBUFFER_FORMAT_R16G16B16A16_FLOAT); return hasFP16Support; } +bool HardwareBitmapUploader::has1010102Support() { + static bool has101012Support = checkSupport(AHARDWAREBUFFER_FORMAT_R10G10B10A2_UNORM); + return has101012Support; +} + +bool HardwareBitmapUploader::hasAlpha8Support() { + static bool hasAlpha8Support = checkSupport(AHARDWAREBUFFER_FORMAT_R8_UNORM); + return hasAlpha8Support; +} + static FormatInfo determineFormat(const SkBitmap& skBitmap, bool usingGL) { FormatInfo formatInfo; switch (skBitmap.info().colorType()) { @@ -350,6 +355,26 @@ static FormatInfo determineFormat(const SkBitmap& skBitmap, bool usingGL) { formatInfo.type = GL_UNSIGNED_BYTE; formatInfo.vkFormat = VK_FORMAT_R8G8B8A8_UNORM; break; + case kRGBA_1010102_SkColorType: + formatInfo.isSupported = HardwareBitmapUploader::has1010102Support(); + if (formatInfo.isSupported) { + formatInfo.type = GL_UNSIGNED_INT_2_10_10_10_REV; + formatInfo.bufferFormat = AHARDWAREBUFFER_FORMAT_R10G10B10A2_UNORM; + formatInfo.vkFormat = VK_FORMAT_A2B10G10R10_UNORM_PACK32; + } else { + formatInfo.type = GL_UNSIGNED_BYTE; + formatInfo.bufferFormat = AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM; + formatInfo.vkFormat = VK_FORMAT_R8G8B8A8_UNORM; + } + formatInfo.format = GL_RGBA; + break; + case kAlpha_8_SkColorType: + formatInfo.isSupported = HardwareBitmapUploader::hasAlpha8Support(); + formatInfo.bufferFormat = AHARDWAREBUFFER_FORMAT_R8_UNORM; + formatInfo.format = GL_R8; + formatInfo.type = GL_UNSIGNED_BYTE; + formatInfo.vkFormat = VK_FORMAT_R8_UNORM; + break; default: ALOGW("unable to create hardware bitmap of colortype: %d", skBitmap.info().colorType()); formatInfo.valid = false; diff --git a/libs/hwui/HardwareBitmapUploader.h b/libs/hwui/HardwareBitmapUploader.h index ad7a95a4fa03..81057a24c29c 100644 --- a/libs/hwui/HardwareBitmapUploader.h +++ b/libs/hwui/HardwareBitmapUploader.h @@ -29,10 +29,14 @@ public: #ifdef __ANDROID__ static bool hasFP16Support(); + static bool has1010102Support(); + static bool hasAlpha8Support(); #else static bool hasFP16Support() { return true; } + static bool has1010102Support() { return true; } + static bool hasAlpha8Support() { return true; } #endif }; diff --git a/libs/hwui/JankTracker.cpp b/libs/hwui/JankTracker.cpp index 34e5577066f9..1e5be6c3eed7 100644 --- a/libs/hwui/JankTracker.cpp +++ b/libs/hwui/JankTracker.cpp @@ -111,19 +111,22 @@ void JankTracker::calculateLegacyJank(FrameInfo& frame) REQUIRES(mDataMutex) { // the actual time spent blocked. nsecs_t forgiveAmount = std::min(expectedDequeueDuration, frame[FrameInfoIndex::DequeueBufferDuration]); - LOG_ALWAYS_FATAL_IF(forgiveAmount >= totalDuration, - "Impossible dequeue duration! dequeue duration reported %" PRId64 - ", total duration %" PRId64, - forgiveAmount, totalDuration); + if (forgiveAmount >= totalDuration) { + ALOGV("Impossible dequeue duration! dequeue duration reported %" PRId64 + ", total duration %" PRId64, + forgiveAmount, totalDuration); + return; + } totalDuration -= forgiveAmount; } } - LOG_ALWAYS_FATAL_IF(totalDuration <= 0, "Impossible totalDuration %" PRId64 " start=%" PRIi64 - " gpuComplete=%" PRIi64, totalDuration, - frame[FrameInfoIndex::IntendedVsync], - frame[FrameInfoIndex::GpuCompleted]); - + if (totalDuration <= 0) { + ALOGV("Impossible totalDuration %" PRId64 " start=%" PRIi64 " gpuComplete=%" PRIi64, + totalDuration, frame[FrameInfoIndex::IntendedVsync], + frame[FrameInfoIndex::GpuCompleted]); + return; + } // Only things like Surface.lockHardwareCanvas() are exempt from tracking if (CC_UNLIKELY(frame[FrameInfoIndex::Flags] & EXEMPT_FRAMES_FLAGS)) { @@ -164,7 +167,8 @@ void JankTracker::calculateLegacyJank(FrameInfo& frame) REQUIRES(mDataMutex) { - lastFrameOffset + mFrameIntervalLegacy; } -void JankTracker::finishFrame(FrameInfo& frame, std::unique_ptr<FrameMetricsReporter>& reporter) { +void JankTracker::finishFrame(FrameInfo& frame, std::unique_ptr<FrameMetricsReporter>& reporter, + int64_t frameNumber, int32_t surfaceControlId) { std::lock_guard lock(mDataMutex); calculateLegacyJank(frame); @@ -173,7 +177,10 @@ void JankTracker::finishFrame(FrameInfo& frame, std::unique_ptr<FrameMetricsRepo int64_t totalDuration = frame.duration(FrameInfoIndex::IntendedVsync, FrameInfoIndex::FrameCompleted); - LOG_ALWAYS_FATAL_IF(totalDuration <= 0, "Impossible totalDuration %" PRId64, totalDuration); + if (totalDuration <= 0) { + ALOGV("Impossible totalDuration %" PRId64, totalDuration); + return; + } mData->reportFrame(totalDuration); (*mGlobalData)->reportFrame(totalDuration); @@ -253,7 +260,8 @@ void JankTracker::finishFrame(FrameInfo& frame, std::unique_ptr<FrameMetricsRepo } if (CC_UNLIKELY(reporter.get() != nullptr)) { - reporter->reportFrameMetrics(frame.data(), false /* hasPresentTime */); + reporter->reportFrameMetrics(frame.data(), false /* hasPresentTime */, frameNumber, + surfaceControlId); } } diff --git a/libs/hwui/JankTracker.h b/libs/hwui/JankTracker.h index bdb784dc8747..bcd031efa78d 100644 --- a/libs/hwui/JankTracker.h +++ b/libs/hwui/JankTracker.h @@ -57,7 +57,8 @@ public: } FrameInfo* startFrame() { return &mFrames.next(); } - void finishFrame(FrameInfo& frame, std::unique_ptr<FrameMetricsReporter>& reporter); + void finishFrame(FrameInfo& frame, std::unique_ptr<FrameMetricsReporter>& reporter, + int64_t frameNumber, int32_t surfaceId); // Calculates the 'legacy' jank information, i.e. with outdated refresh rate information and // without GPU completion or deadlined information. diff --git a/libs/hwui/Layer.cpp b/libs/hwui/Layer.cpp index b14ade97ca5f..9053c1240957 100644 --- a/libs/hwui/Layer.cpp +++ b/libs/hwui/Layer.cpp @@ -20,6 +20,8 @@ #include "utils/Color.h" #include "utils/MathUtils.h" +#include <log/log.h> + namespace android { namespace uirenderer { @@ -33,7 +35,6 @@ Layer::Layer(RenderState& renderState, sk_sp<SkColorFilter> colorFilter, int alp // preserves the old inc/dec ref locations. This should be changed... incStrong(nullptr); renderState.registerLayer(this); - texTransform.setIdentity(); transform.setIdentity(); } @@ -90,7 +91,7 @@ static bool shouldFilterRect(const SkMatrix& matrix, const SkRect& srcRect, cons void Layer::draw(SkCanvas* canvas) { GrRecordingContext* context = canvas->recordingContext(); if (context == nullptr) { - SkDEBUGF(("Attempting to draw LayerDrawable into an unsupported surface")); + ALOGD("Attempting to draw LayerDrawable into an unsupported surface"); return; } SkMatrix layerTransform = getTransform(); @@ -99,7 +100,6 @@ void Layer::draw(SkCanvas* canvas) { const int layerHeight = getHeight(); if (layerImage) { SkMatrix textureMatrixInv; - textureMatrixInv = getTexTransform(); // TODO: after skia bug https://bugs.chromium.org/p/skia/issues/detail?id=7075 is fixed // use bottom left origin and remove flipV and invert transformations. SkMatrix flipV; diff --git a/libs/hwui/Layer.h b/libs/hwui/Layer.h index e99e76299317..47eb5d3bfb83 100644 --- a/libs/hwui/Layer.h +++ b/libs/hwui/Layer.h @@ -74,10 +74,18 @@ public: void setColorFilter(sk_sp<SkColorFilter> filter) { mColorFilter = filter; }; - inline SkMatrix& getTexTransform() { return texTransform; } - inline SkMatrix& getTransform() { return transform; } + inline SkRect getCurrentCropRect() { return mCurrentCropRect; } + + inline void setCurrentCropRect(const SkRect currentCropRect) { + mCurrentCropRect = currentCropRect; + } + + inline void setWindowTransform(uint32_t windowTransform) { mWindowTransform = windowTransform; } + + inline uint32_t getWindowTransform() { return mWindowTransform; } + /** * Posts a decStrong call to the appropriate thread. * Thread-safe. @@ -88,6 +96,12 @@ public: inline sk_sp<SkImage> getImage() const { return this->layerImage; } + inline void setMaxLuminanceNits(float maxLuminanceNits) { + mMaxLuminanceNits = maxLuminanceNits; + } + + inline float getMaxLuminanceNits() { return mMaxLuminanceNits; } + void draw(SkCanvas* canvas); protected: @@ -116,14 +130,19 @@ private: SkBlendMode mode; /** - * Optional texture coordinates transform. + * Optional transform. */ - SkMatrix texTransform; + SkMatrix transform; /** - * Optional transform. + * Optional crop */ - SkMatrix transform; + SkRect mCurrentCropRect; + + /** + * Optional transform + */ + uint32_t mWindowTransform; /** * An image backing the layer. @@ -145,6 +164,11 @@ private: */ bool mBlend = false; + /** + * Max input luminance if the layer is HDR + */ + float mMaxLuminanceNits = -1; + }; // struct Layer } // namespace uirenderer diff --git a/libs/hwui/Outline.h b/libs/hwui/Outline.h index 2eb2c7c7e299..e16fd8c38c75 100644 --- a/libs/hwui/Outline.h +++ b/libs/hwui/Outline.h @@ -88,14 +88,10 @@ public: bool getShouldClip() const { return mShouldClip; } - bool willClip() const { - // only round rect outlines can be used for clipping - return mShouldClip && (mType == Type::RoundRect); - } + bool willClip() const { return mShouldClip; } - bool willRoundRectClip() const { - // only round rect outlines can be used for clipping - return willClip() && MathUtils::isPositive(mRadius); + bool willComplexClip() const { + return mShouldClip && (mType != Type::RoundRect || MathUtils::isPositive(mRadius)); } bool getAsRoundRect(Rect* outRect, float* outRadius) const { diff --git a/libs/hwui/ProfileData.cpp b/libs/hwui/ProfileData.cpp index dd8439647fd3..3d0ca0a10851 100644 --- a/libs/hwui/ProfileData.cpp +++ b/libs/hwui/ProfileData.cpp @@ -137,6 +137,7 @@ void ProfileData::dump(int fd) const { histogramGPUForEach([fd](HistogramEntry entry) { dprintf(fd, " %ums=%u", entry.renderTimeMs, entry.frameCount); }); + dprintf(fd, "\n"); } uint32_t ProfileData::findPercentile(int percentile) const { diff --git a/libs/hwui/Properties.cpp b/libs/hwui/Properties.cpp index 475fd700ccc9..5a67eb9935dd 100644 --- a/libs/hwui/Properties.cpp +++ b/libs/hwui/Properties.cpp @@ -69,7 +69,6 @@ RenderPipelineType Properties::sRenderPipelineType = RenderPipelineType::NotInit bool Properties::enableHighContrastText = false; bool Properties::waitForGpuCompletion = false; -bool Properties::forceDrawFrame = false; bool Properties::filterOutTestOverhead = false; bool Properties::disableVsync = false; @@ -90,6 +89,8 @@ bool Properties::enableWebViewOverlays = true; StretchEffectBehavior Properties::stretchEffectBehavior = StretchEffectBehavior::ShaderHWUI; +DrawingEnabled Properties::drawingEnabled = DrawingEnabled::NotInitialized; + bool Properties::load() { bool prevDebugLayersUpdates = debugLayersUpdates; bool prevDebugOverdraw = debugOverdraw; @@ -143,6 +144,9 @@ bool Properties::load() { enableWebViewOverlays = base::GetBoolProperty(PROPERTY_WEBVIEW_OVERLAYS_ENABLED, true); + // call isDrawingEnabled to force loading of the property + isDrawingEnabled(); + return (prevDebugLayersUpdates != debugLayersUpdates) || (prevDebugOverdraw != debugOverdraw); } @@ -212,5 +216,19 @@ void Properties::overrideRenderPipelineType(RenderPipelineType type, bool inUnit sRenderPipelineType = type; } +void Properties::setDrawingEnabled(bool newDrawingEnabled) { + drawingEnabled = newDrawingEnabled ? DrawingEnabled::On : DrawingEnabled::Off; + enableRTAnimations = newDrawingEnabled; +} + +bool Properties::isDrawingEnabled() { + if (drawingEnabled == DrawingEnabled::NotInitialized) { + bool drawingEnabledProp = base::GetBoolProperty(PROPERTY_DRAWING_ENABLED, true); + drawingEnabled = drawingEnabledProp ? DrawingEnabled::On : DrawingEnabled::Off; + enableRTAnimations = drawingEnabledProp; + } + return drawingEnabled == DrawingEnabled::On; +} + } // namespace uirenderer } // namespace android diff --git a/libs/hwui/Properties.h b/libs/hwui/Properties.h index d224a547ab4d..2f8c67903a8b 100644 --- a/libs/hwui/Properties.h +++ b/libs/hwui/Properties.h @@ -187,6 +187,12 @@ enum DebugLevel { */ #define PROPERTY_WEBVIEW_OVERLAYS_ENABLED "debug.hwui.webview_overlays_enabled" +/** + * Property for globally GL drawing state. Can be overridden per process with + * setDrawingEnabled. + */ +#define PROPERTY_DRAWING_ENABLED "debug.hwui.drawing_enabled" + /////////////////////////////////////////////////////////////////////////////// // Misc /////////////////////////////////////////////////////////////////////////////// @@ -208,6 +214,8 @@ enum class StretchEffectBehavior { UniformScale // Uniform scale stretch everywhere }; +enum class DrawingEnabled { NotInitialized, On, Off }; + /** * Renderthread-only singleton which manages several static rendering properties. Most of these * are driven by system properties which are queried once at initialization, and again if init() @@ -302,6 +310,11 @@ public: stretchEffectBehavior = behavior; } + // Represents if drawing is enabled. Should only be Off in headless testing environments + static DrawingEnabled drawingEnabled; + static bool isDrawingEnabled(); + static void setDrawingEnabled(bool enable); + private: static StretchEffectBehavior stretchEffectBehavior; static ProfileType sProfileType; diff --git a/libs/hwui/Readback.cpp b/libs/hwui/Readback.cpp index a743d30939d0..4cce87ad1a2f 100644 --- a/libs/hwui/Readback.cpp +++ b/libs/hwui/Readback.cpp @@ -243,18 +243,14 @@ CopyResult Readback::copySurfaceIntoLegacy(ANativeWindow* window, const Rect& sr static_cast<android_dataspace>(ANativeWindow_getBuffersDataSpace(window))); sk_sp<SkImage> image = SkImage::MakeFromAHardwareBuffer(sourceBuffer.get(), kPremul_SkAlphaType, colorSpace); - return copyImageInto(image, texTransform, srcRect, bitmap); + return copyImageInto(image, srcRect, bitmap); } CopyResult Readback::copyHWBitmapInto(Bitmap* hwBitmap, SkBitmap* bitmap) { LOG_ALWAYS_FATAL_IF(!hwBitmap->isHardware()); Rect srcRect; - Matrix4 transform; - transform.loadScale(1, -1, 1); - transform.translate(0, -1); - - return copyImageInto(hwBitmap->makeImage(), transform, srcRect, bitmap); + return copyImageInto(hwBitmap->makeImage(), srcRect, bitmap); } CopyResult Readback::copyLayerInto(DeferredLayerUpdater* deferredLayer, SkBitmap* bitmap) { @@ -279,14 +275,11 @@ CopyResult Readback::copyLayerInto(DeferredLayerUpdater* deferredLayer, SkBitmap CopyResult Readback::copyImageInto(const sk_sp<SkImage>& image, SkBitmap* bitmap) { Rect srcRect; - Matrix4 transform; - transform.loadScale(1, -1, 1); - transform.translate(0, -1); - return copyImageInto(image, transform, srcRect, bitmap); + return copyImageInto(image, srcRect, bitmap); } -CopyResult Readback::copyImageInto(const sk_sp<SkImage>& image, Matrix4& texTransform, - const Rect& srcRect, SkBitmap* bitmap) { +CopyResult Readback::copyImageInto(const sk_sp<SkImage>& image, const Rect& srcRect, + SkBitmap* bitmap) { ATRACE_CALL(); if (Properties::getRenderPipelineType() == RenderPipelineType::SkiaGL) { mRenderThread.requireGlContext(); @@ -303,11 +296,6 @@ CopyResult Readback::copyImageInto(const sk_sp<SkImage>& image, Matrix4& texTran CopyResult copyResult = CopyResult::UnknownError; int displayedWidth = imgWidth, displayedHeight = imgHeight; - // If this is a 90 or 270 degree rotation we need to swap width/height to get the device - // size. - if (texTransform[Matrix4::kSkewX] >= 0.5f || texTransform[Matrix4::kSkewX] <= -0.5f) { - std::swap(displayedWidth, displayedHeight); - } SkRect skiaDestRect = SkRect::MakeWH(bitmap->width(), bitmap->height()); SkRect skiaSrcRect = srcRect.toSkRect(); if (skiaSrcRect.isEmpty()) { @@ -320,7 +308,6 @@ CopyResult Readback::copyImageInto(const sk_sp<SkImage>& image, Matrix4& texTran Layer layer(mRenderThread.renderState(), nullptr, 255, SkBlendMode::kSrc); layer.setSize(displayedWidth, displayedHeight); - texTransform.copyTo(layer.getTexTransform()); layer.setImage(image); // Scaling filter is not explicitly set here, because it is done inside copyLayerInfo // after checking the necessity based on the src/dest rect size and the transformation. diff --git a/libs/hwui/Readback.h b/libs/hwui/Readback.h index da252695dd3b..d0d748ff5c16 100644 --- a/libs/hwui/Readback.h +++ b/libs/hwui/Readback.h @@ -56,8 +56,7 @@ public: private: CopyResult copySurfaceIntoLegacy(ANativeWindow* window, const Rect& srcRect, SkBitmap* bitmap); - CopyResult copyImageInto(const sk_sp<SkImage>& image, Matrix4& texTransform, - 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, SkBitmap* bitmap); diff --git a/libs/hwui/RecordingCanvas.cpp b/libs/hwui/RecordingCanvas.cpp index 442ae0fb2707..a285462eef74 100644 --- a/libs/hwui/RecordingCanvas.cpp +++ b/libs/hwui/RecordingCanvas.cpp @@ -15,6 +15,7 @@ */ #include "RecordingCanvas.h" +#include <hwui/Paint.h> #include <GrRecordingContext.h> @@ -186,6 +187,11 @@ struct ClipRegion final : Op { SkClipOp op; void draw(SkCanvas* c, const SkMatrix&) const { c->clipRegion(region, op); } }; +struct ResetClip final : Op { + static const auto kType = Type::ResetClip; + ResetClip() {} + void draw(SkCanvas* c, const SkMatrix&) const { SkAndroidFrameworkUtils::ResetClip(c); } +}; struct DrawPaint final : Op { static const auto kType = Type::DrawPaint; @@ -495,7 +501,7 @@ struct DrawVectorDrawable final : Op { sp<VectorDrawableRoot> mRoot; SkRect mBounds; - SkPaint paint; + Paint paint; BitmapPalette palette; }; @@ -661,6 +667,9 @@ void DisplayListData::clipRRect(const SkRRect& rrect, SkClipOp op, bool aa) { void DisplayListData::clipRegion(const SkRegion& region, SkClipOp op) { this->push<ClipRegion>(0, region, op); } +void DisplayListData::resetClip() { + this->push<ResetClip>(0); +} void DisplayListData::drawPaint(const SkPaint& paint) { this->push<DrawPaint>(0, paint); @@ -833,7 +842,8 @@ constexpr color_transform_fn colorTransformForOp() { // TODO: We should be const. Or not. Or just use a different map // Unclear, but this is the quick fix const T* op = reinterpret_cast<const T*>(opRaw); - transformPaint(transform, const_cast<SkPaint*>(&(op->paint)), op->palette); + const SkPaint* paint = &op->paint; + transformPaint(transform, const_cast<SkPaint*>(paint), op->palette); }; } else if @@ -842,7 +852,8 @@ constexpr color_transform_fn colorTransformForOp() { // TODO: We should be const. Or not. Or just use a different map // Unclear, but this is the quick fix const T* op = reinterpret_cast<const T*>(opRaw); - transformPaint(transform, const_cast<SkPaint*>(&(op->paint))); + const SkPaint* paint = &op->paint; + transformPaint(transform, const_cast<SkPaint*>(paint)); }; } else { @@ -966,6 +977,14 @@ void RecordingCanvas::onClipRegion(const SkRegion& region, SkClipOp op) { fDL->clipRegion(region, op); this->INHERITED::onClipRegion(region, op); } +void RecordingCanvas::onResetClip() { + // This is part of "replace op" emulation, but rely on the following intersection + // clip to potentially mark the clip as complex. If we are already complex, we do + // not reset the complexity so that we don't break the contract that no higher + // save point has a complex clip when "not complex". + fDL->resetClip(); + this->INHERITED::onResetClip(); +} void RecordingCanvas::onDrawPaint(const SkPaint& paint) { fDL->drawPaint(paint); diff --git a/libs/hwui/RecordingCanvas.h b/libs/hwui/RecordingCanvas.h index 4fae6a13a25a..212b4e72dcb2 100644 --- a/libs/hwui/RecordingCanvas.h +++ b/libs/hwui/RecordingCanvas.h @@ -97,6 +97,7 @@ private: void clipRect(const SkRect&, SkClipOp, bool aa); void clipRRect(const SkRRect&, SkClipOp, bool aa); void clipRegion(const SkRegion&, SkClipOp); + void resetClip(); void drawPaint(const SkPaint&); void drawBehind(const SkPaint&); @@ -169,6 +170,7 @@ public: void onClipRRect(const SkRRect&, SkClipOp, ClipEdgeStyle) override; void onClipPath(const SkPath&, SkClipOp, ClipEdgeStyle) override; void onClipRegion(const SkRegion&, SkClipOp) override; + void onResetClip() override; void onDrawPaint(const SkPaint&) override; void onDrawBehind(const SkPaint&) override; diff --git a/libs/hwui/RenderProperties.h b/libs/hwui/RenderProperties.h index cd622eba37b6..064ba7aee107 100644 --- a/libs/hwui/RenderProperties.h +++ b/libs/hwui/RenderProperties.h @@ -165,11 +165,11 @@ public: bool prepareForFunctorPresence(bool willHaveFunctor, bool ancestorDictatesFunctorsNeedLayer) { // parent may have already dictated that a descendant layer is needed bool functorsNeedLayer = - ancestorDictatesFunctorsNeedLayer - || CC_UNLIKELY(isClipMayBeComplex()) + ancestorDictatesFunctorsNeedLayer || + CC_UNLIKELY(isClipMayBeComplex()) // Round rect clipping forces layer for functors - || CC_UNLIKELY(getOutline().willRoundRectClip()) || + || CC_UNLIKELY(getOutline().willComplexClip()) || CC_UNLIKELY(getRevealClip().willClip()) // Complex matrices forces layer, due to stencil clipping diff --git a/libs/hwui/SkiaCanvas.cpp b/libs/hwui/SkiaCanvas.cpp index d032e2b00649..53c6db0cdf3a 100644 --- a/libs/hwui/SkiaCanvas.cpp +++ b/libs/hwui/SkiaCanvas.cpp @@ -182,7 +182,7 @@ int SkiaCanvas::saveUnclippedLayer(int left, int top, int right, int bottom) { return SkAndroidFrameworkUtils::SaveBehind(mCanvas, &bounds); } -void SkiaCanvas::restoreUnclippedLayer(int restoreCount, const SkPaint& paint) { +void SkiaCanvas::restoreUnclippedLayer(int restoreCount, const Paint& paint) { while (mCanvas->getSaveCount() > restoreCount + 1) { this->restore(); @@ -396,6 +396,22 @@ bool SkiaCanvas::clipPath(const SkPath* path, SkClipOp op) { return !mCanvas->isClipEmpty(); } +bool SkiaCanvas::replaceClipRect_deprecated(float left, float top, float right, float bottom) { + SkRect rect = SkRect::MakeLTRB(left, top, right, bottom); + + // Emulated clip rects are not recorded for partial saves, since + // partial saves have been removed from the public API. + SkAndroidFrameworkUtils::ResetClip(mCanvas); + mCanvas->clipRect(rect, SkClipOp::kIntersect); + return !mCanvas->isClipEmpty(); +} + +bool SkiaCanvas::replaceClipPath_deprecated(const SkPath* path) { + SkAndroidFrameworkUtils::ResetClip(mCanvas); + mCanvas->clipPath(*path, SkClipOp::kIntersect, true); + return !mCanvas->isClipEmpty(); +} + // ---------------------------------------------------------------------------- // Canvas state operations: Filters // ---------------------------------------------------------------------------- @@ -439,13 +455,13 @@ void SkiaCanvas::drawColor(int color, SkBlendMode mode) { mCanvas->drawColor(color, mode); } -void SkiaCanvas::onFilterPaint(SkPaint& paint) { +void SkiaCanvas::onFilterPaint(Paint& paint) { if (mPaintFilter) { - mPaintFilter->filter(&paint); + mPaintFilter->filterFullPaint(&paint); } } -void SkiaCanvas::drawPaint(const SkPaint& paint) { +void SkiaCanvas::drawPaint(const Paint& paint) { mCanvas->drawPaint(filterPaint(paint)); } @@ -552,9 +568,8 @@ void SkiaCanvas::drawVertices(const SkVertices* vertices, SkBlendMode mode, cons void SkiaCanvas::drawBitmap(Bitmap& bitmap, float left, float top, const Paint* paint) { auto image = bitmap.makeImage(); - applyLooper(paint, [&](const SkPaint& p) { - auto sampling = SkSamplingOptions(p.getFilterQuality()); - mCanvas->drawImage(image, left, top, sampling, &p); + applyLooper(paint, [&](const Paint& p) { + mCanvas->drawImage(image, left, top, p.sampling(), &p); }); } @@ -562,9 +577,8 @@ void SkiaCanvas::drawBitmap(Bitmap& bitmap, const SkMatrix& matrix, const Paint* auto image = bitmap.makeImage(); SkAutoCanvasRestore acr(mCanvas, true); mCanvas->concat(matrix); - applyLooper(paint, [&](const SkPaint& p) { - auto sampling = SkSamplingOptions(p.getFilterQuality()); - mCanvas->drawImage(image, 0, 0, sampling, &p); + applyLooper(paint, [&](const Paint& p) { + mCanvas->drawImage(image, 0, 0, p.sampling(), &p); }); } @@ -575,18 +589,12 @@ void SkiaCanvas::drawBitmap(Bitmap& bitmap, float srcLeft, float srcTop, float s SkRect srcRect = SkRect::MakeLTRB(srcLeft, srcTop, srcRight, srcBottom); SkRect dstRect = SkRect::MakeLTRB(dstLeft, dstTop, dstRight, dstBottom); - applyLooper(paint, [&](const SkPaint& p) { - auto sampling = SkSamplingOptions(p.getFilterQuality()); - mCanvas->drawImageRect(image, srcRect, dstRect, sampling, &p, + applyLooper(paint, [&](const Paint& p) { + mCanvas->drawImageRect(image, srcRect, dstRect, p.sampling(), &p, SkCanvas::kFast_SrcRectConstraint); }); } -static SkFilterMode paintToFilter(const Paint* paint) { - return paint && paint->isFilterBitmap() ? SkFilterMode::kLinear - : SkFilterMode::kNearest; -} - void SkiaCanvas::drawBitmapMesh(Bitmap& bitmap, int meshWidth, int meshHeight, const float* vertices, const int* colors, const Paint* paint) { const int ptCount = (meshWidth + 1) * (meshHeight + 1); @@ -668,13 +676,13 @@ void SkiaCanvas::drawBitmapMesh(Bitmap& bitmap, int meshWidth, int meshHeight, if (paint) { pnt = *paint; } - SkSamplingOptions sampling(paintToFilter(&pnt)); + SkSamplingOptions sampling = pnt.sampling(); pnt.setShader(image->makeShader(sampling)); auto v = builder.detach(); - applyLooper(&pnt, [&](const SkPaint& p) { + applyLooper(&pnt, [&](const Paint& p) { SkPaint copy(p); - auto s = SkSamplingOptions(p.getFilterQuality()); + auto s = p.sampling(); if (s != sampling) { // applyLooper changed the quality? copy.setShader(image->makeShader(s)); @@ -707,9 +715,8 @@ void SkiaCanvas::drawNinePatch(Bitmap& bitmap, const Res_png_9patch& chunk, floa lattice.fBounds = nullptr; SkRect dst = SkRect::MakeLTRB(dstLeft, dstTop, dstRight, dstBottom); auto image = bitmap.makeImage(); - applyLooper(paint, [&](const SkPaint& p) { - auto filter = SkSamplingOptions(p.getFilterQuality()).filter; - mCanvas->drawImageLattice(image.get(), lattice, dst, filter, &p); + applyLooper(paint, [&](const Paint& p) { + mCanvas->drawImageLattice(image.get(), lattice, dst, p.filterMode(), &p); }); } diff --git a/libs/hwui/SkiaCanvas.h b/libs/hwui/SkiaCanvas.h index 438a40cb4c81..715007cdcd3b 100644 --- a/libs/hwui/SkiaCanvas.h +++ b/libs/hwui/SkiaCanvas.h @@ -23,8 +23,10 @@ #include "VectorDrawable.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" @@ -73,7 +75,7 @@ public: virtual int save(SaveFlags::Flags flags) override; virtual void restore() override; virtual void restoreToCount(int saveCount) override; - virtual void restoreUnclippedLayer(int saveCount, const SkPaint& paint) override; + virtual void restoreUnclippedLayer(int saveCount, const Paint& paint) override; virtual int saveLayer(float left, float top, float right, float bottom, const SkPaint* paint) override; virtual int saveLayerAlpha(float left, float top, float right, float bottom, int alpha) override; @@ -92,6 +94,9 @@ public: virtual bool quickRejectPath(const SkPath& path) const override; virtual bool clipRect(float left, float top, float right, float bottom, SkClipOp op) override; virtual bool clipPath(const SkPath* path, SkClipOp op) override; + virtual bool replaceClipRect_deprecated(float left, float top, float right, + float bottom) override; + virtual bool replaceClipPath_deprecated(const SkPath* path) override; virtual PaintFilter* getPaintFilter() override; virtual void setPaintFilter(sk_sp<PaintFilter> paintFilter) override; @@ -99,7 +104,7 @@ public: virtual SkCanvasState* captureCanvasState() const override; virtual void drawColor(int color, SkBlendMode mode) override; - virtual void drawPaint(const SkPaint& paint) override; + virtual void drawPaint(const Paint& paint) override; virtual void drawPoint(float x, float y, const Paint& paint) override; virtual void drawPoints(const float* points, int count, const Paint& paint) override; @@ -167,10 +172,10 @@ protected: const Paint& paint, const SkPath& path, size_t start, size_t end) override; - void onFilterPaint(SkPaint& paint); + void onFilterPaint(Paint& paint); - SkPaint filterPaint(const SkPaint& src) { - SkPaint dst(src); + Paint filterPaint(const Paint& src) { + Paint dst(src); this->onFilterPaint(dst); return dst; } @@ -179,21 +184,20 @@ protected: template <typename Proc> void applyLooper(const Paint* paint, Proc proc, void (*preFilter)(SkPaint&) = nullptr) { BlurDrawLooper* looper = paint ? paint->getLooper() : nullptr; - const SkPaint* skpPtr = paint; - SkPaint skp = skpPtr ? *skpPtr : SkPaint(); + Paint pnt = paint ? *paint : Paint(); if (preFilter) { - preFilter(skp); + preFilter(pnt); } - this->onFilterPaint(skp); + this->onFilterPaint(pnt); if (looper) { - looper->apply(skp, [&](SkPoint offset, const SkPaint& modifiedPaint) { + looper->apply(pnt, [&](SkPoint offset, const Paint& modifiedPaint) { mCanvas->save(); mCanvas->translate(offset.fX, offset.fY); proc(modifiedPaint); mCanvas->restore(); }); } else { - proc(skp); + proc(pnt); } } diff --git a/libs/hwui/TreeInfo.h b/libs/hwui/TreeInfo.h index cc9094c8afe9..6b8f43946a74 100644 --- a/libs/hwui/TreeInfo.h +++ b/libs/hwui/TreeInfo.h @@ -100,6 +100,8 @@ public: int stretchEffectCount = 0; + bool forceDrawFrame = false; + struct Out { bool hasFunctors = false; // This is only updated if evaluateAnimations is true diff --git a/libs/hwui/VectorDrawable.cpp b/libs/hwui/VectorDrawable.cpp index 55f434f49bbd..983c7766273a 100644 --- a/libs/hwui/VectorDrawable.cpp +++ b/libs/hwui/VectorDrawable.cpp @@ -269,7 +269,7 @@ void FullPath::FullPathProperties::setPropertyValue(int propertyId, float value) void ClipPath::draw(SkCanvas* outCanvas, bool useStagingData) { SkPath tempStagingPath; - outCanvas->clipPath(getUpdatedPath(useStagingData, &tempStagingPath)); + outCanvas->clipPath(getUpdatedPath(useStagingData, &tempStagingPath), true); } Group::Group(const Group& group) : Node(group) { @@ -463,10 +463,10 @@ void Tree::drawStaging(Canvas* outCanvas) { mStagingCache.dirty = false; } - SkPaint skp; + Paint skp; getPaintFor(&skp, mStagingProperties); Paint paint; - paint.setFilterQuality(skp.getFilterQuality()); + paint.setFilterBitmap(skp.isFilterBitmap()); paint.setColorFilter(skp.refColorFilter()); paint.setAlpha(skp.getAlpha()); outCanvas->drawBitmap(*mStagingCache.bitmap, 0, 0, mStagingCache.bitmap->width(), @@ -476,9 +476,9 @@ void Tree::drawStaging(Canvas* outCanvas) { mStagingProperties.getBounds().bottom(), &paint); } -void Tree::getPaintFor(SkPaint* outPaint, const TreeProperties& prop) const { +void Tree::getPaintFor(Paint* outPaint, const TreeProperties& prop) const { // HWUI always draws VD with bilinear filtering. - outPaint->setFilterQuality(kLow_SkFilterQuality); + outPaint->setFilterBitmap(true); if (prop.getColorFilter() != nullptr) { outPaint->setColorFilter(sk_ref_sp(prop.getColorFilter())); } diff --git a/libs/hwui/VectorDrawable.h b/libs/hwui/VectorDrawable.h index ac7d41e0d600..30bb04ae8361 100644 --- a/libs/hwui/VectorDrawable.h +++ b/libs/hwui/VectorDrawable.h @@ -648,7 +648,7 @@ public: */ void draw(SkCanvas* canvas, const SkRect& bounds, const SkPaint& paint); - void getPaintFor(SkPaint* outPaint, const TreeProperties &props) const; + void getPaintFor(Paint* outPaint, const TreeProperties &props) const; BitmapPalette computePalette(); void setAntiAlias(bool aa) { mRootNode->setAntiAlias(aa); } diff --git a/libs/hwui/WebViewFunctorManager.cpp b/libs/hwui/WebViewFunctorManager.cpp index 5aad821ad59f..6fc251dc815c 100644 --- a/libs/hwui/WebViewFunctorManager.cpp +++ b/libs/hwui/WebViewFunctorManager.cpp @@ -118,6 +118,24 @@ void WebViewFunctor::onRemovedFromTree() { } } +bool WebViewFunctor::prepareRootSurfaceControl() { + if (!Properties::enableWebViewOverlays) return false; + + renderthread::CanvasContext* activeContext = renderthread::CanvasContext::getActiveContext(); + if (!activeContext) return false; + + ASurfaceControl* rootSurfaceControl = activeContext->getSurfaceControl(); + if (!rootSurfaceControl) return false; + + int32_t rgid = activeContext->getSurfaceControlGenerationId(); + if (mParentSurfaceControlGenerationId != rgid) { + reparentSurfaceControl(rootSurfaceControl); + mParentSurfaceControlGenerationId = rgid; + } + + return true; +} + void WebViewFunctor::drawGl(const DrawGlInfo& drawInfo) { ATRACE_NAME("WebViewFunctor::drawGl"); if (!mHasContext) { @@ -131,20 +149,8 @@ void WebViewFunctor::drawGl(const DrawGlInfo& drawInfo) { .mergeTransaction = currentFunctor.mergeTransaction, }; - if (Properties::enableWebViewOverlays && !drawInfo.isLayer) { - renderthread::CanvasContext* activeContext = - renderthread::CanvasContext::getActiveContext(); - if (activeContext != nullptr) { - ASurfaceControl* rootSurfaceControl = activeContext->getSurfaceControl(); - if (rootSurfaceControl) { - overlayParams.overlaysMode = OverlaysMode::Enabled; - int32_t rgid = activeContext->getSurfaceControlGenerationId(); - if (mParentSurfaceControlGenerationId != rgid) { - reparentSurfaceControl(rootSurfaceControl); - mParentSurfaceControlGenerationId = rgid; - } - } - } + if (!drawInfo.isLayer && prepareRootSurfaceControl()) { + overlayParams.overlaysMode = OverlaysMode::Enabled; } mCallbacks.gles.draw(mFunctor, mData, drawInfo, overlayParams); @@ -170,7 +176,10 @@ void WebViewFunctor::drawVk(const VkFunctorDrawParams& params) { .mergeTransaction = currentFunctor.mergeTransaction, }; - // TODO, enable surface control once offscreen mode figured out + if (!params.is_layer && prepareRootSurfaceControl()) { + overlayParams.overlaysMode = OverlaysMode::Enabled; + } + mCallbacks.vk.draw(mFunctor, mData, params, overlayParams); } diff --git a/libs/hwui/WebViewFunctorManager.h b/libs/hwui/WebViewFunctorManager.h index f28f310993ec..0a02f2d4b720 100644 --- a/libs/hwui/WebViewFunctorManager.h +++ b/libs/hwui/WebViewFunctorManager.h @@ -88,6 +88,7 @@ public: } private: + bool prepareRootSurfaceControl(); void reparentSurfaceControl(ASurfaceControl* parent); private: diff --git a/libs/hwui/apex/LayoutlibLoader.cpp b/libs/hwui/apex/LayoutlibLoader.cpp index dca10e29cbb8..942c0506321c 100644 --- a/libs/hwui/apex/LayoutlibLoader.cpp +++ b/libs/hwui/apex/LayoutlibLoader.cpp @@ -14,31 +14,22 @@ * limitations under the License. */ -#include "graphics_jni_helpers.h" - #include <GraphicsJNI.h> #include <SkGraphics.h> -#include <sstream> -#include <iostream> -#include <unicode/putil.h> #include <unordered_map> #include <vector> -using namespace std; - -/* - * This is responsible for setting up the JNI environment for communication between - * the Java and native parts of layoutlib, including registering native methods. - * This is mostly achieved by copying the way it is done in the platform - * (see AndroidRuntime.cpp). - */ +#include "Properties.h" +#include "android/graphics/jni_runtime.h" +#include "graphics_jni_helpers.h" -static JavaVM* javaVM; +using namespace std; extern int register_android_graphics_Bitmap(JNIEnv*); extern int register_android_graphics_BitmapFactory(JNIEnv*); extern int register_android_graphics_ByteBufferStreamAdaptor(JNIEnv* env); +extern int register_android_graphics_Camera(JNIEnv* env); extern int register_android_graphics_CreateJavaOutputStreamAdaptor(JNIEnv* env); extern int register_android_graphics_Graphics(JNIEnv* env); extern int register_android_graphics_ImageDecoder(JNIEnv*); @@ -49,10 +40,12 @@ extern int register_android_graphics_PathEffect(JNIEnv* env); extern int register_android_graphics_Shader(JNIEnv* env); extern int register_android_graphics_RenderEffect(JNIEnv* env); extern int register_android_graphics_Typeface(JNIEnv* env); +extern int register_android_graphics_YuvImage(JNIEnv* env); namespace android { extern int register_android_graphics_Canvas(JNIEnv* env); +extern int register_android_graphics_CanvasProperty(JNIEnv* env); extern int register_android_graphics_ColorFilter(JNIEnv* env); extern int register_android_graphics_ColorSpace(JNIEnv* env); extern int register_android_graphics_DrawFilter(JNIEnv* env); @@ -62,7 +55,7 @@ extern int register_android_graphics_Paint(JNIEnv* env); extern int register_android_graphics_Path(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); +extern int register_android_graphics_Region(JNIEnv* env); extern int register_android_graphics_animation_NativeInterpolatorFactory(JNIEnv* env); extern int register_android_graphics_animation_RenderNodeAnimator(JNIEnv* env); extern int register_android_graphics_drawable_AnimatedVectorDrawable(JNIEnv* env); @@ -71,9 +64,11 @@ extern int register_android_graphics_fonts_Font(JNIEnv* env); 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_util_PathParser(JNIEnv* env); -extern int register_android_view_RenderNode(JNIEnv* env); extern int register_android_view_DisplayListCanvas(JNIEnv* env); +extern int register_android_view_RenderNode(JNIEnv* env); #define REG_JNI(name) { name } struct RegJNIRec { @@ -87,8 +82,9 @@ static const std::unordered_map<std::string, RegJNIRec> gRegJNIMap = { {"android.graphics.BitmapFactory", REG_JNI(register_android_graphics_BitmapFactory)}, {"android.graphics.ByteBufferStreamAdaptor", REG_JNI(register_android_graphics_ByteBufferStreamAdaptor)}, + {"android.graphics.Camera", REG_JNI(register_android_graphics_Camera)}, {"android.graphics.Canvas", REG_JNI(register_android_graphics_Canvas)}, - {"android.graphics.RenderNode", REG_JNI(register_android_view_RenderNode)}, + {"android.graphics.CanvasProperty", REG_JNI(register_android_graphics_CanvasProperty)}, {"android.graphics.ColorFilter", REG_JNI(register_android_graphics_ColorFilter)}, {"android.graphics.ColorSpace", REG_JNI(register_android_graphics_ColorSpace)}, {"android.graphics.CreateJavaOutputStreamAdaptor", @@ -107,10 +103,12 @@ static const std::unordered_map<std::string, RegJNIRec> gRegJNIMap = { {"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)}, -// {"android.graphics.Region", REG_JNI(register_android_graphics_Region)}, + {"android.graphics.Region", REG_JNI(register_android_graphics_Region)}, + {"android.graphics.RenderNode", REG_JNI(register_android_view_RenderNode)}, {"android.graphics.Shader", REG_JNI(register_android_graphics_Shader)}, {"android.graphics.RenderEffect", REG_JNI(register_android_graphics_RenderEffect)}, {"android.graphics.Typeface", REG_JNI(register_android_graphics_Typeface)}, + {"android.graphics.YuvImage", REG_JNI(register_android_graphics_YuvImage)}, {"android.graphics.animation.NativeInterpolatorFactory", REG_JNI(register_android_graphics_animation_NativeInterpolatorFactory)}, {"android.graphics.animation.RenderNodeAnimator", @@ -124,6 +122,7 @@ static const std::unordered_map<std::string, RegJNIRec> gRegJNIMap = { {"android.graphics.text.LineBreaker", REG_JNI(register_android_graphics_text_LineBreaker)}, {"android.graphics.text.MeasuredText", REG_JNI(register_android_graphics_text_MeasuredText)}, + {"android.graphics.text.TextRunShaper", REG_JNI(register_android_graphics_text_TextShaper)}, {"android.util.PathParser", REG_JNI(register_android_util_PathParser)}, }; @@ -177,10 +176,9 @@ int register_android_graphics_classes(JNIEnv *env) { "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;"); // Get the names of classes that need to register their native methods - auto nativesClassesJString = - (jstring) env->CallStaticObjectMethod(system, - getPropertyMethod, env->NewStringUTF("native_classes"), - env->NewStringUTF("")); + auto nativesClassesJString = (jstring)env->CallStaticObjectMethod( + system, getPropertyMethod, env->NewStringUTF("graphics_native_classes"), + env->NewStringUTF("")); vector<string> classesToRegister = parseCsv(env, nativesClassesJString); if (register_jni_procs(gRegJNIMap, classesToRegister, env) < 0) { diff --git a/libs/hwui/apex/android_bitmap.cpp b/libs/hwui/apex/android_bitmap.cpp index 3780ba072308..bc6bc456ba5a 100644 --- a/libs/hwui/apex/android_bitmap.cpp +++ b/libs/hwui/apex/android_bitmap.cpp @@ -57,6 +57,8 @@ static AndroidBitmapFormat getFormat(const SkImageInfo& info) { return ANDROID_BITMAP_FORMAT_A_8; case kRGBA_F16_SkColorType: return ANDROID_BITMAP_FORMAT_RGBA_F16; + case kRGBA_1010102_SkColorType: + return ANDROID_BITMAP_FORMAT_RGBA_1010102; default: return ANDROID_BITMAP_FORMAT_NONE; } @@ -74,6 +76,8 @@ static SkColorType getColorType(AndroidBitmapFormat format) { return kAlpha_8_SkColorType; case ANDROID_BITMAP_FORMAT_RGBA_F16: return kRGBA_F16_SkColorType; + case ANDROID_BITMAP_FORMAT_RGBA_1010102: + return kRGBA_1010102_SkColorType; default: return kUnknown_SkColorType; } @@ -249,6 +253,9 @@ int ABitmap_compress(const AndroidBitmapInfo* info, ADataSpace dataSpace, const case ANDROID_BITMAP_FORMAT_RGBA_F16: colorType = kRGBA_F16_SkColorType; break; + case ANDROID_BITMAP_FORMAT_RGBA_1010102: + colorType = kRGBA_1010102_SkColorType; + break; default: return ANDROID_BITMAP_RESULT_BAD_PARAMETER; } diff --git a/libs/hwui/apex/include/android/graphics/renderthread.h b/libs/hwui/apex/include/android/graphics/properties.h index 50280a6dd1fb..f24f840710f9 100644 --- a/libs/hwui/apex/include/android/graphics/renderthread.h +++ b/libs/hwui/apex/include/android/graphics/properties.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 The Android Open Source Project + * 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. @@ -13,8 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -#ifndef ANDROID_GRAPHICS_RENDERTHREAD_H -#define ANDROID_GRAPHICS_RENDERTHREAD_H + +#ifndef ANDROID_GRAPHICS_PROPERTIES_H +#define ANDROID_GRAPHICS_PROPERTIES_H #include <cutils/compiler.h> #include <sys/cdefs.h> @@ -22,13 +23,10 @@ __BEGIN_DECLS /** - * Dumps a textual representation of the graphics stats for this process. - * @param fd The file descriptor that the available graphics stats will be appended to. The - * function requires a valid fd, but does not persist or assume ownership of the fd - * outside the scope of this function. + * Returns true if libhwui is using the vulkan backend. */ -ANDROID_API void ARenderThread_dumpGraphicsMemory(int fd); +ANDROID_API bool hwui_uses_vulkan(); __END_DECLS -#endif // ANDROID_GRAPHICS_RENDERTHREAD_H +#endif // ANDROID_GRAPHICS_PROPERTIES_H diff --git a/libs/hwui/apex/renderthread.cpp b/libs/hwui/apex/properties.cpp index 5d26afe7a2ab..abb333be159a 100644 --- a/libs/hwui/apex/renderthread.cpp +++ b/libs/hwui/apex/properties.cpp @@ -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,12 +14,11 @@ * limitations under the License. */ -#include "android/graphics/renderthread.h" +#include "android/graphics/properties.h" -#include <renderthread/RenderProxy.h> +#include <Properties.h> -using namespace android; - -void ARenderThread_dumpGraphicsMemory(int fd) { - uirenderer::renderthread::RenderProxy::dumpGraphicsMemory(fd); +bool hwui_uses_vulkan() { + return android::uirenderer::Properties::peekRenderPipelineType() == + android::uirenderer::RenderPipelineType::SkiaVulkan; } diff --git a/libs/hwui/canvas/CanvasFrontend.cpp b/libs/hwui/canvas/CanvasFrontend.cpp index 8f261c83b8d3..112ac124ffa1 100644 --- a/libs/hwui/canvas/CanvasFrontend.cpp +++ b/libs/hwui/canvas/CanvasFrontend.cpp @@ -30,44 +30,61 @@ void CanvasStateHelper::resetState(int width, int height) { mClipStack.clear(); mTransformStack.clear(); mSaveStack.emplace_back(); - mClipStack.emplace_back().setRect(mInitialBounds); + mClipStack.emplace_back(); mTransformStack.emplace_back(); - mCurrentClipIndex = 0; - mCurrentTransformIndex = 0; + + clip().bounds = mInitialBounds; } bool CanvasStateHelper::internalSave(SaveEntry saveEntry) { mSaveStack.push_back(saveEntry); if (saveEntry.matrix) { - // We need to push before accessing transform() to ensure the reference doesn't move - // across vector resizes - mTransformStack.emplace_back() = transform(); - mCurrentTransformIndex += 1; + pushEntry(&mTransformStack); } if (saveEntry.clip) { - // We need to push before accessing clip() to ensure the reference doesn't move - // across vector resizes - mClipStack.emplace_back() = clip(); - mCurrentClipIndex += 1; + pushEntry(&mClipStack); return true; } return false; } -// Assert that the cast from SkClipOp to SkRegion::Op is valid -static_assert(static_cast<int>(SkClipOp::kDifference) == SkRegion::Op::kDifference_Op); -static_assert(static_cast<int>(SkClipOp::kIntersect) == SkRegion::Op::kIntersect_Op); -static_assert(static_cast<int>(SkClipOp::kUnion_deprecated) == SkRegion::Op::kUnion_Op); -static_assert(static_cast<int>(SkClipOp::kXOR_deprecated) == SkRegion::Op::kXOR_Op); -static_assert(static_cast<int>(SkClipOp::kReverseDifference_deprecated) == SkRegion::Op::kReverseDifference_Op); -static_assert(static_cast<int>(SkClipOp::kReplace_deprecated) == SkRegion::Op::kReplace_Op); +void CanvasStateHelper::ConservativeClip::apply(SkClipOp op, const SkMatrix& matrix, + const SkRect& bounds, bool aa, bool fillsBounds) { + this->aa |= aa; + + if (op == SkClipOp::kIntersect) { + SkRect devBounds; + bool rect = matrix.mapRect(&devBounds, bounds) && fillsBounds; + if (!this->bounds.intersect(aa ? devBounds.roundOut() : devBounds.round())) { + this->bounds.setEmpty(); + } + this->rect &= rect; + } else { + // Difference operations subtracts a region from the clip, so conservatively + // the bounds remain unchanged and the shape is unlikely to remain a rect. + this->rect = false; + } +} void CanvasStateHelper::internalClipRect(const SkRect& rect, SkClipOp op) { - clip().opRect(rect, transform(), mInitialBounds, (SkRegion::Op)op, false); + clip().apply(op, transform(), rect, /*aa=*/false, /*fillsBounds=*/true); } void CanvasStateHelper::internalClipPath(const SkPath& path, SkClipOp op) { - clip().opPath(path, transform(), mInitialBounds, (SkRegion::Op)op, true); + SkRect bounds = path.getBounds(); + if (path.isInverseFillType()) { + // Toggle op type if the path is inverse filled + op = (op == SkClipOp::kIntersect ? SkClipOp::kDifference : SkClipOp::kIntersect); + } + clip().apply(op, transform(), bounds, /*aa=*/true, /*fillsBounds=*/false); +} + +CanvasStateHelper::ConservativeClip& CanvasStateHelper::clip() { + return writableEntry(&mClipStack); +} + +SkMatrix& CanvasStateHelper::transform() { + return writableEntry(&mTransformStack); } bool CanvasStateHelper::internalRestore() { @@ -80,45 +97,47 @@ bool CanvasStateHelper::internalRestore() { mSaveStack.pop_back(); bool needsRestorePropagation = entry.layer; if (entry.matrix) { - mTransformStack.pop_back(); - mCurrentTransformIndex -= 1; + popEntry(&mTransformStack); } if (entry.clip) { - // We need to push before accessing clip() to ensure the reference doesn't move - // across vector resizes - mClipStack.pop_back(); - mCurrentClipIndex -= 1; + popEntry(&mClipStack); needsRestorePropagation = true; } return needsRestorePropagation; } SkRect CanvasStateHelper::getClipBounds() const { - SkIRect ibounds = clip().getBounds(); - - if (ibounds.isEmpty()) { - return SkRect::MakeEmpty(); - } + SkIRect bounds = clip().bounds; SkMatrix inverse; // if we can't invert the CTM, we can't return local clip bounds - if (!transform().invert(&inverse)) { + if (bounds.isEmpty() || !transform().invert(&inverse)) { return SkRect::MakeEmpty(); } - SkRect ret = SkRect::MakeEmpty(); - inverse.mapRect(&ret, SkRect::Make(ibounds)); - return ret; + return inverse.mapRect(SkRect::Make(bounds)); +} + +bool CanvasStateHelper::ConservativeClip::quickReject(const SkMatrix& matrix, + const SkRect& bounds) const { + SkRect devRect = matrix.mapRect(bounds); + return devRect.isFinite() && + SkIRect::Intersects(this->bounds, aa ? devRect.roundOut() : devRect.round()); } bool CanvasStateHelper::quickRejectRect(float left, float top, float right, float bottom) const { - // TODO: Implement - return false; + return clip().quickReject(transform(), SkRect::MakeLTRB(left, top, right, bottom)); } bool CanvasStateHelper::quickRejectPath(const SkPath& path) const { - // TODO: Implement - return false; + if (this->isClipEmpty()) { + // reject everything (prioritized above path inverse fill type). + return true; + } else { + // Don't reject inverse-filled paths, since even if they are "empty" of points/verbs, + // they fill out the entire clip. + return !path.isInverseFillType() && clip().quickReject(transform(), path.getBounds()); + } } } // namespace android::uirenderer diff --git a/libs/hwui/canvas/CanvasFrontend.h b/libs/hwui/canvas/CanvasFrontend.h index f9a610129d3a..9f22b900e4ab 100644 --- a/libs/hwui/canvas/CanvasFrontend.h +++ b/libs/hwui/canvas/CanvasFrontend.h @@ -21,7 +21,6 @@ #include "CanvasOpBuffer.h" #include <SaveFlags.h> -#include <SkRasterClip.h> #include <ui/FatVector.h> #include <optional> @@ -40,6 +39,26 @@ protected: bool layer : 1 = false; }; + template <typename T> + struct DeferredEntry { + T entry; + int deferredSaveCount = 0; + + DeferredEntry() = default; + DeferredEntry(const T& t) : entry(t) {} + }; + + struct ConservativeClip { + SkIRect bounds = SkIRect::MakeEmpty(); + bool rect = true; + bool aa = false; + + bool quickReject(const SkMatrix& matrix, const SkRect& bounds) const; + + void apply(SkClipOp op, const SkMatrix& matrix, const SkRect& bounds, bool aa, + bool fillsBounds); + }; + constexpr SaveEntry saveEntryForLayer() { return { .clip = true, @@ -72,23 +91,47 @@ protected: void internalClipRect(const SkRect& rect, SkClipOp op); void internalClipPath(const SkPath& path, SkClipOp op); + // The canvas' clip will never expand beyond these bounds since intersect + // and difference operations only subtract pixels. SkIRect mInitialBounds; + // Every save() gets a SaveEntry to track what needs to be restored. FatVector<SaveEntry, 6> mSaveStack; - FatVector<SkMatrix, 6> mTransformStack; - FatVector<SkConservativeClip, 6> mClipStack; + // Transform and clip entries record a deferred save count and do not + // make a new entry until that particular state is modified. + FatVector<DeferredEntry<SkMatrix>, 6> mTransformStack; + FatVector<DeferredEntry<ConservativeClip>, 6> mClipStack; - size_t mCurrentTransformIndex; - size_t mCurrentClipIndex; + const ConservativeClip& clip() const { return mClipStack.back().entry; } - const SkConservativeClip& clip() const { - return mClipStack[mCurrentClipIndex]; + ConservativeClip& clip(); + + void resetState(int width, int height); + + // Stack manipulation for transform and clip stacks + template <typename T, size_t N> + void pushEntry(FatVector<DeferredEntry<T>, N>* stack) { + stack->back().deferredSaveCount += 1; } - SkConservativeClip& clip() { - return mClipStack[mCurrentClipIndex]; + template <typename T, size_t N> + void popEntry(FatVector<DeferredEntry<T>, N>* stack) { + if (!(stack->back().deferredSaveCount--)) { + stack->pop_back(); + } } - void resetState(int width, int height); + template <typename T, size_t N> + T& writableEntry(FatVector<DeferredEntry<T>, N>* stack) { + DeferredEntry<T>& back = stack->back(); + if (back.deferredSaveCount == 0) { + return back.entry; + } else { + back.deferredSaveCount -= 1; + // saved in case references move when re-allocating vector storage + T state = back.entry; + return stack->emplace_back(state).entry; + } + } public: int saveCount() const { return mSaveStack.size(); } @@ -97,13 +140,14 @@ public: bool quickRejectRect(float left, float top, float right, float bottom) const; bool quickRejectPath(const SkPath& path) const; - const SkMatrix& transform() const { - return mTransformStack[mCurrentTransformIndex]; - } + bool isClipAA() const { return clip().aa; } + bool isClipEmpty() const { return clip().bounds.isEmpty(); } + bool isClipRect() const { return clip().rect; } + bool isClipComplex() const { return !isClipEmpty() && (isClipAA() || !isClipRect()); } - SkMatrix& transform() { - return mTransformStack[mCurrentTransformIndex]; - } + const SkMatrix& transform() const { return mTransformStack.back().entry; } + + SkMatrix& transform(); // For compat with existing HWUI Canvas interface void getMatrix(SkMatrix* outMatrix) const { diff --git a/libs/hwui/effects/StretchEffect.cpp b/libs/hwui/effects/StretchEffect.cpp index 17cd3ceb577c..2757c3952dbb 100644 --- a/libs/hwui/effects/StretchEffect.cpp +++ b/libs/hwui/effects/StretchEffect.cpp @@ -181,7 +181,7 @@ static const SkString stretchShader = SkString(R"( ); coord.x = outU; coord.y = outV; - return sample(uContentTexture, coord); + return uContentTexture.eval(coord); })"); static const float ZERO = 0.f; @@ -227,7 +227,7 @@ sk_sp<SkShader> StretchEffect::getShader(float width, float height, mBuilder->uniform("viewportWidth").set(&width, 1); mBuilder->uniform("viewportHeight").set(&height, 1); - auto result = mBuilder->makeShader(nullptr, false); + auto result = mBuilder->makeShader(); mBuilder->child(CONTENT_TEXTURE) = nullptr; return result; } diff --git a/libs/hwui/hwui/AnimatedImageDrawable.cpp b/libs/hwui/hwui/AnimatedImageDrawable.cpp index 876f5c895c60..d08bc5c583c2 100644 --- a/libs/hwui/hwui/AnimatedImageDrawable.cpp +++ b/libs/hwui/hwui/AnimatedImageDrawable.cpp @@ -157,7 +157,6 @@ void AnimatedImageDrawable::onDraw(SkCanvas* canvas) { lazyPaint.emplace(); lazyPaint->setAlpha(mProperties.mAlpha); lazyPaint->setColorFilter(mProperties.mColorFilter); - lazyPaint->setFilterQuality(kLow_SkFilterQuality); } canvas->concat(matrix); diff --git a/libs/hwui/hwui/Bitmap.cpp b/libs/hwui/hwui/Bitmap.cpp index 1a89cfd5d0ad..67f47580a70f 100644 --- a/libs/hwui/hwui/Bitmap.cpp +++ b/libs/hwui/hwui/Bitmap.cpp @@ -104,6 +104,10 @@ sk_sp<Bitmap> Bitmap::allocateAshmemBitmap(size_t size, const SkImageInfo& info, sk_sp<Bitmap> Bitmap::allocateHardwareBitmap(const SkBitmap& bitmap) { #ifdef __ANDROID__ // Layoutlib does not support hardware acceleration + if (bitmap.colorType() == kAlpha_8_SkColorType && + !uirenderer::HardwareBitmapUploader::hasAlpha8Support()) { + return nullptr; + } return uirenderer::HardwareBitmapUploader::allocateHardwareBitmap(bitmap); #else return Bitmap::allocateHeapBitmap(bitmap.info()); diff --git a/libs/hwui/hwui/BlurDrawLooper.cpp b/libs/hwui/hwui/BlurDrawLooper.cpp index 27a038d4598e..270d24af99fd 100644 --- a/libs/hwui/hwui/BlurDrawLooper.cpp +++ b/libs/hwui/hwui/BlurDrawLooper.cpp @@ -24,7 +24,7 @@ BlurDrawLooper::BlurDrawLooper(SkColor4f color, float blurSigma, SkPoint offset) BlurDrawLooper::~BlurDrawLooper() = default; -SkPoint BlurDrawLooper::apply(SkPaint* paint) const { +SkPoint BlurDrawLooper::apply(Paint* paint) const { paint->setColor(mColor); if (mBlurSigma > 0) { paint->setMaskFilter(SkMaskFilter::MakeBlur(kNormal_SkBlurStyle, mBlurSigma, true)); diff --git a/libs/hwui/hwui/BlurDrawLooper.h b/libs/hwui/hwui/BlurDrawLooper.h index 7e6786f7dfbc..09a4e0f849b0 100644 --- a/libs/hwui/hwui/BlurDrawLooper.h +++ b/libs/hwui/hwui/BlurDrawLooper.h @@ -17,7 +17,7 @@ #ifndef ANDROID_GRAPHICS_BLURDRAWLOOPER_H_ #define ANDROID_GRAPHICS_BLURDRAWLOOPER_H_ -#include <SkPaint.h> +#include <hwui/Paint.h> #include <SkRefCnt.h> class SkColorSpace; @@ -30,10 +30,10 @@ public: ~BlurDrawLooper() override; - // proc(SkPoint offset, const SkPaint& modifiedPaint) + // proc(SkPoint offset, const Paint& modifiedPaint) template <typename DrawProc> - void apply(const SkPaint& paint, DrawProc proc) const { - SkPaint p(paint); + void apply(const Paint& paint, DrawProc proc) const { + Paint p(paint); proc(this->apply(&p), p); // draw the shadow proc({0, 0}, paint); // draw the original (on top) } @@ -43,7 +43,7 @@ private: const float mBlurSigma; const SkPoint mOffset; - SkPoint apply(SkPaint* paint) const; + SkPoint apply(Paint* paint) const; BlurDrawLooper(SkColor4f, float, SkPoint); }; diff --git a/libs/hwui/hwui/Canvas.h b/libs/hwui/hwui/Canvas.h index 9023613478fc..82777646f3a2 100644 --- a/libs/hwui/hwui/Canvas.h +++ b/libs/hwui/hwui/Canvas.h @@ -162,7 +162,7 @@ public: virtual int save(SaveFlags::Flags flags) = 0; virtual void restore() = 0; virtual void restoreToCount(int saveCount) = 0; - virtual void restoreUnclippedLayer(int saveCount, const SkPaint& paint) = 0; + virtual void restoreUnclippedLayer(int saveCount, const Paint& paint) = 0; virtual int saveLayer(float left, float top, float right, float bottom, const SkPaint* paint) = 0; virtual int saveLayerAlpha(float left, float top, float right, float bottom, int alpha) = 0; @@ -185,6 +185,12 @@ public: virtual bool clipRect(float left, float top, float right, float bottom, SkClipOp op) = 0; virtual bool clipPath(const SkPath* path, SkClipOp op) = 0; + // Resets clip to wide open, used to emulate the now-removed SkClipOp::kReplace on + // apps with compatibility < P. Canvases for version P and later are restricted to + // intersect and difference at the Java level, matching SkClipOp's definition. + // NOTE: These functions are deprecated and will be removed in a future release + virtual bool replaceClipRect_deprecated(float left, float top, float right, float bottom) = 0; + virtual bool replaceClipPath_deprecated(const SkPath* path) = 0; // filters virtual PaintFilter* getPaintFilter() = 0; @@ -197,7 +203,7 @@ public: // Canvas draw operations // ---------------------------------------------------------------------------- virtual void drawColor(int color, SkBlendMode mode) = 0; - virtual void drawPaint(const SkPaint& paint) = 0; + virtual void drawPaint(const Paint& paint) = 0; // Geometry virtual void drawPoint(float x, float y, const Paint& paint) = 0; diff --git a/libs/hwui/hwui/ImageDecoder.cpp b/libs/hwui/hwui/ImageDecoder.cpp index 5d9fad5b676e..dd68f825b61d 100644 --- a/libs/hwui/hwui/ImageDecoder.cpp +++ b/libs/hwui/hwui/ImageDecoder.cpp @@ -24,7 +24,6 @@ #include <SkBlendMode.h> #include <SkCanvas.h> #include <SkEncodedOrigin.h> -#include <SkFilterQuality.h> #include <SkPaint.h> #undef LOG_TAG @@ -160,6 +159,8 @@ bool ImageDecoder::setOutColorType(SkColorType colorType) { break; case kRGBA_F16_SkColorType: break; + case kRGBA_1010102_SkColorType: + break; default: return false; } diff --git a/libs/hwui/hwui/MinikinUtils.cpp b/libs/hwui/hwui/MinikinUtils.cpp index b8029087cb4f..e359145feef7 100644 --- a/libs/hwui/hwui/MinikinUtils.cpp +++ b/libs/hwui/hwui/MinikinUtils.cpp @@ -95,6 +95,16 @@ float MinikinUtils::measureText(const Paint* paint, minikin::Bidi bidiFlags, endHyphen, advances); } +minikin::MinikinExtent MinikinUtils::getFontExtent(const Paint* paint, minikin::Bidi bidiFlags, + const Typeface* typeface, const uint16_t* buf, + size_t start, size_t count, size_t bufSize) { + minikin::MinikinPaint minikinPaint = prepareMinikinPaint(paint, typeface); + const minikin::U16StringPiece textBuf(buf, bufSize); + const minikin::Range range(start, start + count); + + return minikin::getFontExtent(textBuf, range, bidiFlags, minikinPaint); +} + bool MinikinUtils::hasVariationSelector(const Typeface* typeface, uint32_t codepoint, uint32_t vs) { const Typeface* resolvedFace = Typeface::resolveDefault(typeface); return resolvedFace->fFontCollection->hasVariationSelector(codepoint, vs); diff --git a/libs/hwui/hwui/MinikinUtils.h b/libs/hwui/hwui/MinikinUtils.h index a15803ad2dca..009b84b140ea 100644 --- a/libs/hwui/hwui/MinikinUtils.h +++ b/libs/hwui/hwui/MinikinUtils.h @@ -56,6 +56,10 @@ public: size_t start, size_t count, size_t bufSize, float* advances); + static minikin::MinikinExtent getFontExtent(const Paint* paint, minikin::Bidi bidiFlags, + const Typeface* typeface, const uint16_t* buf, + size_t start, size_t count, size_t bufSize); + static bool hasVariationSelector(const Typeface* typeface, uint32_t codepoint, uint32_t vs); diff --git a/libs/hwui/hwui/Paint.h b/libs/hwui/hwui/Paint.h index d9c9eeed03e9..4a8f3e10fc26 100644 --- a/libs/hwui/hwui/Paint.h +++ b/libs/hwui/hwui/Paint.h @@ -17,13 +17,13 @@ #ifndef ANDROID_GRAPHICS_PAINT_H_ #define ANDROID_GRAPHICS_PAINT_H_ -#include "BlurDrawLooper.h" #include "Typeface.h" #include <cutils/compiler.h> #include <SkFont.h> #include <SkPaint.h> +#include <SkSamplingOptions.h> #include <string> #include <minikin/FontFamily.h> @@ -32,6 +32,8 @@ namespace android { +class BlurDrawLooper; + class Paint : public SkPaint { public: // Default values for underlined and strikethrough text, @@ -60,7 +62,7 @@ public: const SkFont& getSkFont() const { return mFont; } BlurDrawLooper* getLooper() const { return mLooper.get(); } - void setLooper(sk_sp<BlurDrawLooper> looper) { mLooper = std::move(looper); } + void setLooper(sk_sp<BlurDrawLooper> looper); // These shadow the methods on SkPaint, but we need to so we can keep related // attributes in-sync. @@ -138,7 +140,15 @@ public: void setDevKern(bool d) { mDevKern = d; } // Deprecated -- bitmapshaders will be taking this flag explicitly - bool isFilterBitmap() const { return this->getFilterQuality() != kNone_SkFilterQuality; } + bool isFilterBitmap() const { return mFilterBitmap; } + void setFilterBitmap(bool filter) { mFilterBitmap = filter; } + + SkFilterMode filterMode() const { + return mFilterBitmap ? SkFilterMode::kLinear : SkFilterMode::kNearest; + } + SkSamplingOptions sampling() const { + return SkSamplingOptions(this->filterMode()); + } // The Java flags (Paint.java) no longer fit into the native apis directly. // These methods handle converting to and from them and the native representations @@ -169,6 +179,7 @@ private: // nullptr is valid: it means the default typeface. const Typeface* mTypeface = nullptr; Align mAlign = kLeft_Align; + bool mFilterBitmap = false; bool mStrikeThru = false; bool mUnderline = false; bool mDevKern = false; diff --git a/libs/hwui/hwui/PaintFilter.h b/libs/hwui/hwui/PaintFilter.h index 0e7b61977000..4996aa445316 100644 --- a/libs/hwui/hwui/PaintFilter.h +++ b/libs/hwui/hwui/PaintFilter.h @@ -1,17 +1,18 @@ #ifndef ANDROID_GRAPHICS_PAINT_FILTER_H_ #define ANDROID_GRAPHICS_PAINT_FILTER_H_ -class SkPaint; +#include <SkRefCnt.h> namespace android { +class Paint; + class PaintFilter : public SkRefCnt { public: /** * Called with the paint that will be used to draw. * The implementation may modify the paint as they wish. */ - virtual void filter(SkPaint*) = 0; virtual void filterFullPaint(Paint*) = 0; }; diff --git a/libs/hwui/hwui/PaintImpl.cpp b/libs/hwui/hwui/PaintImpl.cpp index fa2674fc2f5e..aac928f85924 100644 --- a/libs/hwui/hwui/PaintImpl.cpp +++ b/libs/hwui/hwui/PaintImpl.cpp @@ -15,6 +15,7 @@ */ #include "Paint.h" +#include "BlurDrawLooper.h" namespace android { @@ -43,6 +44,7 @@ Paint::Paint(const Paint& paint) , mHyphenEdit(paint.mHyphenEdit) , mTypeface(paint.mTypeface) , mAlign(paint.mAlign) + , mFilterBitmap(paint.mFilterBitmap) , mStrikeThru(paint.mStrikeThru) , mUnderline(paint.mUnderline) , mDevKern(paint.mDevKern) {} @@ -62,6 +64,7 @@ Paint& Paint::operator=(const Paint& other) { mHyphenEdit = other.mHyphenEdit; mTypeface = other.mTypeface; mAlign = other.mAlign; + mFilterBitmap = other.mFilterBitmap; mStrikeThru = other.mStrikeThru; mUnderline = other.mUnderline; mDevKern = other.mDevKern; @@ -77,6 +80,7 @@ bool operator==(const Paint& a, const Paint& b) { a.mMinikinLocaleListId == b.mMinikinLocaleListId && a.mFamilyVariant == b.mFamilyVariant && a.mHyphenEdit == b.mHyphenEdit && a.mTypeface == b.mTypeface && a.mAlign == b.mAlign && + a.mFilterBitmap == b.mFilterBitmap && a.mStrikeThru == b.mStrikeThru && a.mUnderline == b.mUnderline && a.mDevKern == b.mDevKern; } @@ -88,11 +92,16 @@ void Paint::reset() { mFont.setEdging(SkFont::Edging::kAlias); mLooper.reset(); + mFilterBitmap = false; mStrikeThru = false; mUnderline = false; mDevKern = false; } +void Paint::setLooper(sk_sp<BlurDrawLooper> looper) { + mLooper = std::move(looper); +} + void Paint::setAntiAlias(bool aa) { // Java does not support/understand subpixel(lcd) antialiasing SkASSERT(mFont.getEdging() != SkFont::Edging::kSubpixelAntiAlias); @@ -131,9 +140,6 @@ static uint32_t paintToLegacyFlags(const SkPaint& paint) { uint32_t flags = 0; flags |= -(int)paint.isAntiAlias() & sAntiAliasFlag; flags |= -(int)paint.isDither() & sDitherFlag; - if (paint.getFilterQuality() != kNone_SkFilterQuality) { - flags |= sFilterBitmapFlag; - } return flags; } @@ -150,12 +156,6 @@ static uint32_t fontToLegacyFlags(const SkFont& font) { static void applyLegacyFlagsToPaint(uint32_t flags, SkPaint* paint) { paint->setAntiAlias((flags & sAntiAliasFlag) != 0); paint->setDither ((flags & sDitherFlag) != 0); - - if (flags & sFilterBitmapFlag) { - paint->setFilterQuality(kLow_SkFilterQuality); - } else { - paint->setFilterQuality(kNone_SkFilterQuality); - } } static void applyLegacyFlagsToFont(uint32_t flags, SkFont* font) { @@ -182,18 +182,20 @@ void Paint::SetSkPaintJavaFlags(SkPaint* paint, uint32_t flags) { uint32_t Paint::getJavaFlags() const { uint32_t flags = paintToLegacyFlags(*this) | fontToLegacyFlags(mFont); - flags |= -(int)mStrikeThru & sStrikeThruFlag; - flags |= -(int)mUnderline & sUnderlineFlag; - flags |= -(int)mDevKern & sDevKernFlag; + flags |= -(int)mStrikeThru & sStrikeThruFlag; + flags |= -(int)mUnderline & sUnderlineFlag; + flags |= -(int)mDevKern & sDevKernFlag; + flags |= -(int)mFilterBitmap & sFilterBitmapFlag; return flags; } void Paint::setJavaFlags(uint32_t flags) { applyLegacyFlagsToPaint(flags, this); applyLegacyFlagsToFont(flags, &mFont); - mStrikeThru = (flags & sStrikeThruFlag) != 0; - mUnderline = (flags & sUnderlineFlag) != 0; - mDevKern = (flags & sDevKernFlag) != 0; + mStrikeThru = (flags & sStrikeThruFlag) != 0; + mUnderline = (flags & sUnderlineFlag) != 0; + mDevKern = (flags & sDevKernFlag) != 0; + mFilterBitmap = (flags & sFilterBitmapFlag) != 0; } } // namespace android diff --git a/libs/hwui/jni/AnimatedImageDrawable.cpp b/libs/hwui/jni/AnimatedImageDrawable.cpp index c9433ec8a9da..c40b858268be 100644 --- a/libs/hwui/jni/AnimatedImageDrawable.cpp +++ b/libs/hwui/jni/AnimatedImageDrawable.cpp @@ -30,7 +30,8 @@ using namespace android; -static jmethodID gAnimatedImageDrawable_onAnimationEndMethodID; +static jclass gAnimatedImageDrawableClass; +static jmethodID gAnimatedImageDrawable_callOnAnimationEndMethodID; // Note: jpostProcess holds a handle to the ImageDecoder. static jlong AnimatedImageDrawable_nCreate(JNIEnv* env, jobject /*clazz*/, @@ -178,26 +179,23 @@ class InvokeListener : public MessageHandler { public: InvokeListener(JNIEnv* env, jobject javaObject) { LOG_ALWAYS_FATAL_IF(env->GetJavaVM(&mJvm) != JNI_OK); - // Hold a weak reference to break a cycle that would prevent GC. - mWeakRef = env->NewWeakGlobalRef(javaObject); + mCallbackRef = env->NewGlobalRef(javaObject); } ~InvokeListener() override { auto* env = requireEnv(mJvm); - env->DeleteWeakGlobalRef(mWeakRef); + env->DeleteGlobalRef(mCallbackRef); } virtual void handleMessage(const Message&) override { auto* env = get_env_or_die(mJvm); - jobject localRef = env->NewLocalRef(mWeakRef); - if (localRef) { - env->CallVoidMethod(localRef, gAnimatedImageDrawable_onAnimationEndMethodID); - } + env->CallStaticVoidMethod(gAnimatedImageDrawableClass, + gAnimatedImageDrawable_callOnAnimationEndMethodID, mCallbackRef); } private: JavaVM* mJvm; - jweak mWeakRef; + jobject mCallbackRef; }; class JniAnimationEndListener : public OnAnimationEndListener { @@ -253,26 +251,31 @@ static void AnimatedImageDrawable_nSetBounds(JNIEnv* env, jobject /*clazz*/, jlo } static const JNINativeMethod gAnimatedImageDrawableMethods[] = { - { "nCreate", "(JLandroid/graphics/ImageDecoder;IIJZLandroid/graphics/Rect;)J",(void*) AnimatedImageDrawable_nCreate }, - { "nGetNativeFinalizer", "()J", (void*) AnimatedImageDrawable_nGetNativeFinalizer }, - { "nDraw", "(JJ)J", (void*) AnimatedImageDrawable_nDraw }, - { "nSetAlpha", "(JI)V", (void*) AnimatedImageDrawable_nSetAlpha }, - { "nGetAlpha", "(J)I", (void*) AnimatedImageDrawable_nGetAlpha }, - { "nSetColorFilter", "(JJ)V", (void*) AnimatedImageDrawable_nSetColorFilter }, - { "nIsRunning", "(J)Z", (void*) AnimatedImageDrawable_nIsRunning }, - { "nStart", "(J)Z", (void*) AnimatedImageDrawable_nStart }, - { "nStop", "(J)Z", (void*) AnimatedImageDrawable_nStop }, - { "nGetRepeatCount", "(J)I", (void*) AnimatedImageDrawable_nGetRepeatCount }, - { "nSetRepeatCount", "(JI)V", (void*) AnimatedImageDrawable_nSetRepeatCount }, - { "nSetOnAnimationEndListener", "(JLandroid/graphics/drawable/AnimatedImageDrawable;)V", (void*) AnimatedImageDrawable_nSetOnAnimationEndListener }, - { "nNativeByteSize", "(J)J", (void*) AnimatedImageDrawable_nNativeByteSize }, - { "nSetMirrored", "(JZ)V", (void*) AnimatedImageDrawable_nSetMirrored }, - { "nSetBounds", "(JLandroid/graphics/Rect;)V", (void*) AnimatedImageDrawable_nSetBounds }, + {"nCreate", "(JLandroid/graphics/ImageDecoder;IIJZLandroid/graphics/Rect;)J", + (void*)AnimatedImageDrawable_nCreate}, + {"nGetNativeFinalizer", "()J", (void*)AnimatedImageDrawable_nGetNativeFinalizer}, + {"nDraw", "(JJ)J", (void*)AnimatedImageDrawable_nDraw}, + {"nSetAlpha", "(JI)V", (void*)AnimatedImageDrawable_nSetAlpha}, + {"nGetAlpha", "(J)I", (void*)AnimatedImageDrawable_nGetAlpha}, + {"nSetColorFilter", "(JJ)V", (void*)AnimatedImageDrawable_nSetColorFilter}, + {"nIsRunning", "(J)Z", (void*)AnimatedImageDrawable_nIsRunning}, + {"nStart", "(J)Z", (void*)AnimatedImageDrawable_nStart}, + {"nStop", "(J)Z", (void*)AnimatedImageDrawable_nStop}, + {"nGetRepeatCount", "(J)I", (void*)AnimatedImageDrawable_nGetRepeatCount}, + {"nSetRepeatCount", "(JI)V", (void*)AnimatedImageDrawable_nSetRepeatCount}, + {"nSetOnAnimationEndListener", "(JLjava/lang/ref/WeakReference;)V", + (void*)AnimatedImageDrawable_nSetOnAnimationEndListener}, + {"nNativeByteSize", "(J)J", (void*)AnimatedImageDrawable_nNativeByteSize}, + {"nSetMirrored", "(JZ)V", (void*)AnimatedImageDrawable_nSetMirrored}, + {"nSetBounds", "(JLandroid/graphics/Rect;)V", (void*)AnimatedImageDrawable_nSetBounds}, }; int register_android_graphics_drawable_AnimatedImageDrawable(JNIEnv* env) { - jclass animatedImageDrawable_class = FindClassOrDie(env, "android/graphics/drawable/AnimatedImageDrawable"); - gAnimatedImageDrawable_onAnimationEndMethodID = GetMethodIDOrDie(env, animatedImageDrawable_class, "onAnimationEnd", "()V"); + gAnimatedImageDrawableClass = reinterpret_cast<jclass>(env->NewGlobalRef( + FindClassOrDie(env, "android/graphics/drawable/AnimatedImageDrawable"))); + gAnimatedImageDrawable_callOnAnimationEndMethodID = + GetStaticMethodIDOrDie(env, gAnimatedImageDrawableClass, "callOnAnimationEnd", + "(Ljava/lang/ref/WeakReference;)V"); return android::RegisterMethodsOrDie(env, "android/graphics/drawable/AnimatedImageDrawable", gAnimatedImageDrawableMethods, NELEM(gAnimatedImageDrawableMethods)); diff --git a/libs/hwui/jni/Bitmap.cpp b/libs/hwui/jni/Bitmap.cpp index 4003f0b65fb5..5db0783cf83e 100755 --- a/libs/hwui/jni/Bitmap.cpp +++ b/libs/hwui/jni/Bitmap.cpp @@ -884,7 +884,7 @@ static jboolean Bitmap_writeToParcel(JNIEnv* env, jobject, jlong bitmapHandle, jint density, jobject parcel) { #ifdef __ANDROID__ // Layoutlib does not support parcel if (parcel == NULL) { - SkDebugf("------- writeToParcel null parcel\n"); + ALOGD("------- writeToParcel null parcel\n"); return JNI_FALSE; } diff --git a/libs/hwui/jni/BitmapRegionDecoder.cpp b/libs/hwui/jni/BitmapRegionDecoder.cpp index 4cc05ef6f13b..1c20415dcc8f 100644 --- a/libs/hwui/jni/BitmapRegionDecoder.cpp +++ b/libs/hwui/jni/BitmapRegionDecoder.cpp @@ -137,9 +137,16 @@ static jobject nativeDecodeRegion(JNIEnv* env, jobject, jlong brdHandle, jint in auto* brd = reinterpret_cast<skia::BitmapRegionDecoder*>(brdHandle); SkColorType decodeColorType = brd->computeOutputColorType(colorType); - if (decodeColorType == kRGBA_F16_SkColorType && isHardware && + + if (isHardware) { + if (decodeColorType == kRGBA_F16_SkColorType && !uirenderer::HardwareBitmapUploader::hasFP16Support()) { - decodeColorType = kN32_SkColorType; + decodeColorType = kN32_SkColorType; + } + if (decodeColorType == kRGBA_1010102_SkColorType && + !uirenderer::HardwareBitmapUploader::has1010102Support()) { + decodeColorType = kN32_SkColorType; + } } // Set up the pixel allocator diff --git a/libs/hwui/jni/CreateJavaOutputStreamAdaptor.cpp b/libs/hwui/jni/CreateJavaOutputStreamAdaptor.cpp index 785a5dc995ab..15e529e169fc 100644 --- a/libs/hwui/jni/CreateJavaOutputStreamAdaptor.cpp +++ b/libs/hwui/jni/CreateJavaOutputStreamAdaptor.cpp @@ -107,7 +107,7 @@ private: jint n = env->CallIntMethod(fJavaInputStream, gInputStream_readMethodID, fJavaByteArray, 0, requested); if (checkException(env)) { - SkDebugf("---- read threw an exception\n"); + ALOGD("---- read threw an exception\n"); return bytesRead; } @@ -119,7 +119,7 @@ private: env->GetByteArrayRegion(fJavaByteArray, 0, n, reinterpret_cast<jbyte*>(buffer)); if (checkException(env)) { - SkDebugf("---- read:GetByteArrayRegion threw an exception\n"); + ALOGD("---- read:GetByteArrayRegion threw an exception\n"); return bytesRead; } @@ -136,7 +136,7 @@ private: jlong skipped = env->CallLongMethod(fJavaInputStream, gInputStream_skipMethodID, (jlong)size); if (checkException(env)) { - SkDebugf("------- skip threw an exception\n"); + ALOGD("------- skip threw an exception\n"); return 0; } if (skipped < 0) { @@ -236,7 +236,7 @@ public: if (env->ExceptionCheck()) { env->ExceptionDescribe(); env->ExceptionClear(); - SkDebugf("--- write:SetByteArrayElements threw an exception\n"); + ALOGD("--- write:SetByteArrayElements threw an exception\n"); return false; } @@ -245,7 +245,7 @@ public: if (env->ExceptionCheck()) { env->ExceptionDescribe(); env->ExceptionClear(); - SkDebugf("------- write threw an exception\n"); + ALOGD("------- write threw an exception\n"); return false; } diff --git a/libs/hwui/jni/GIFMovie.cpp b/libs/hwui/jni/GIFMovie.cpp index f84a4bd09073..fef51b8d2f79 100644 --- a/libs/hwui/jni/GIFMovie.cpp +++ b/libs/hwui/jni/GIFMovie.cpp @@ -10,11 +10,13 @@ #include "SkColor.h" #include "SkColorPriv.h" #include "SkStream.h" -#include "SkTemplates.h" -#include "SkUtils.h" #include "gif_lib.h" +#include <log/log.h> + +#include <string.h> + #if GIFLIB_MAJOR < 5 || (GIFLIB_MAJOR == 5 && GIFLIB_MINOR == 0) #define DGifCloseFile(a, b) DGifCloseFile(a) #endif @@ -217,8 +219,9 @@ static void fillRect(SkBitmap* bm, GifWord left, GifWord top, GifWord width, Gif copyHeight = bmHeight - top; } + size_t bytes = copyWidth * SkColorTypeBytesPerPixel(bm->colorType()); for (; copyHeight > 0; copyHeight--) { - sk_memset32(dst, col, copyWidth); + memset(dst, col, bytes); dst += bmWidth; } } @@ -244,7 +247,7 @@ static void drawFrame(SkBitmap* bm, const SavedImage* frame, const ColorMapObjec } if (cmap == nullptr || cmap->ColorCount != (1 << cmap->BitsPerPixel)) { - SkDEBUGFAIL("bad colortable setup"); + ALOGD("bad colortable setup"); return; } diff --git a/libs/hwui/jni/Graphics.cpp b/libs/hwui/jni/Graphics.cpp index 77f46beb2100..33669ac0a34e 100644 --- a/libs/hwui/jni/Graphics.cpp +++ b/libs/hwui/jni/Graphics.cpp @@ -365,6 +365,8 @@ jint GraphicsJNI::colorTypeToLegacyBitmapConfig(SkColorType colorType) { return kRGB_565_LegacyBitmapConfig; case kAlpha_8_SkColorType: return kA8_LegacyBitmapConfig; + case kRGBA_1010102_SkColorType: + return kRGBA_1010102_LegacyBitmapConfig; case kUnknown_SkColorType: default: break; @@ -374,14 +376,10 @@ jint GraphicsJNI::colorTypeToLegacyBitmapConfig(SkColorType colorType) { SkColorType GraphicsJNI::legacyBitmapConfigToColorType(jint legacyConfig) { const uint8_t gConfig2ColorType[] = { - kUnknown_SkColorType, - kAlpha_8_SkColorType, - kUnknown_SkColorType, // Previously kIndex_8_SkColorType, - kRGB_565_SkColorType, - kARGB_4444_SkColorType, - kN32_SkColorType, - kRGBA_F16_SkColorType, - kN32_SkColorType + kUnknown_SkColorType, kAlpha_8_SkColorType, + kUnknown_SkColorType, // Previously kIndex_8_SkColorType, + kRGB_565_SkColorType, kARGB_4444_SkColorType, kN32_SkColorType, + kRGBA_F16_SkColorType, kN32_SkColorType, kRGBA_1010102_SkColorType, }; if (legacyConfig < 0 || legacyConfig > kLastEnum_LegacyBitmapConfig) { @@ -399,15 +397,12 @@ AndroidBitmapFormat GraphicsJNI::getFormatFromConfig(JNIEnv* env, jobject jconfi jint javaConfigId = env->GetIntField(jconfig, gBitmapConfig_nativeInstanceID); const AndroidBitmapFormat config2BitmapFormat[] = { - ANDROID_BITMAP_FORMAT_NONE, - ANDROID_BITMAP_FORMAT_A_8, - ANDROID_BITMAP_FORMAT_NONE, // Previously Config.Index_8 - ANDROID_BITMAP_FORMAT_RGB_565, - ANDROID_BITMAP_FORMAT_RGBA_4444, - ANDROID_BITMAP_FORMAT_RGBA_8888, - ANDROID_BITMAP_FORMAT_RGBA_F16, - ANDROID_BITMAP_FORMAT_NONE // Congfig.HARDWARE - }; + ANDROID_BITMAP_FORMAT_NONE, ANDROID_BITMAP_FORMAT_A_8, + ANDROID_BITMAP_FORMAT_NONE, // Previously Config.Index_8 + ANDROID_BITMAP_FORMAT_RGB_565, ANDROID_BITMAP_FORMAT_RGBA_4444, + ANDROID_BITMAP_FORMAT_RGBA_8888, ANDROID_BITMAP_FORMAT_RGBA_F16, + ANDROID_BITMAP_FORMAT_NONE, // Congfig.HARDWARE + ANDROID_BITMAP_FORMAT_RGBA_1010102}; return config2BitmapFormat[javaConfigId]; } @@ -430,6 +425,9 @@ jobject GraphicsJNI::getConfigFromFormat(JNIEnv* env, AndroidBitmapFormat format case ANDROID_BITMAP_FORMAT_RGBA_F16: configId = kRGBA_16F_LegacyBitmapConfig; break; + case ANDROID_BITMAP_FORMAT_RGBA_1010102: + configId = kRGBA_1010102_LegacyBitmapConfig; + break; default: break; } diff --git a/libs/hwui/jni/GraphicsJNI.h b/libs/hwui/jni/GraphicsJNI.h index ba407f2164de..085a905abaf8 100644 --- a/libs/hwui/jni/GraphicsJNI.h +++ b/libs/hwui/jni/GraphicsJNI.h @@ -34,16 +34,17 @@ public: // This enum must keep these int values, to match the int values // in the java Bitmap.Config enum. enum LegacyBitmapConfig { - kNo_LegacyBitmapConfig = 0, - kA8_LegacyBitmapConfig = 1, - kIndex8_LegacyBitmapConfig = 2, - kRGB_565_LegacyBitmapConfig = 3, - kARGB_4444_LegacyBitmapConfig = 4, - kARGB_8888_LegacyBitmapConfig = 5, - kRGBA_16F_LegacyBitmapConfig = 6, - kHardware_LegacyBitmapConfig = 7, - - kLastEnum_LegacyBitmapConfig = kHardware_LegacyBitmapConfig + kNo_LegacyBitmapConfig = 0, + kA8_LegacyBitmapConfig = 1, + kIndex8_LegacyBitmapConfig = 2, + kRGB_565_LegacyBitmapConfig = 3, + kARGB_4444_LegacyBitmapConfig = 4, + kARGB_8888_LegacyBitmapConfig = 5, + kRGBA_16F_LegacyBitmapConfig = 6, + kHardware_LegacyBitmapConfig = 7, + kRGBA_1010102_LegacyBitmapConfig = 8, + + kLastEnum_LegacyBitmapConfig = kRGBA_1010102_LegacyBitmapConfig }; static void setJavaVM(JavaVM* javaVM); diff --git a/libs/hwui/jni/NinePatch.cpp b/libs/hwui/jni/NinePatch.cpp index 6942017d5f27..08fc80fbdafd 100644 --- a/libs/hwui/jni/NinePatch.cpp +++ b/libs/hwui/jni/NinePatch.cpp @@ -64,10 +64,10 @@ public: } static jlong validateNinePatchChunk(JNIEnv* env, jobject, jbyteArray obj) { - size_t chunkSize = env->GetArrayLength(obj); + size_t chunkSize = obj != NULL ? env->GetArrayLength(obj) : 0; if (chunkSize < (int) (sizeof(Res_png_9patch))) { jniThrowRuntimeException(env, "Array too small for chunk."); - return NULL; + return 0; } int8_t* storage = new int8_t[chunkSize]; diff --git a/libs/hwui/jni/Paint.cpp b/libs/hwui/jni/Paint.cpp index bcec0fa8a1cc..f76863255153 100644 --- a/libs/hwui/jni/Paint.cpp +++ b/libs/hwui/jni/Paint.cpp @@ -541,26 +541,6 @@ namespace PaintGlue { return result; } - // ------------------ @FastNative --------------------------- - - static jint setTextLocales(JNIEnv* env, jobject clazz, jlong objHandle, jstring locales) { - Paint* obj = reinterpret_cast<Paint*>(objHandle); - ScopedUtfChars localesChars(env, locales); - jint minikinLocaleListId = minikin::registerLocaleList(localesChars.c_str()); - obj->setMinikinLocaleListId(minikinLocaleListId); - return minikinLocaleListId; - } - - static void setFontFeatureSettings(JNIEnv* env, jobject clazz, jlong paintHandle, jstring settings) { - Paint* paint = reinterpret_cast<Paint*>(paintHandle); - if (!settings) { - paint->setFontFeatureSettings(std::string()); - } else { - ScopedUtfChars settingsChars(env, settings); - paint->setFontFeatureSettings(std::string(settingsChars.c_str(), settingsChars.size())); - } - } - static SkScalar getMetricsInternal(jlong paintHandle, SkFontMetrics *metrics) { const int kElegantTop = 2500; const int kElegantBottom = -1000; @@ -593,6 +573,67 @@ namespace PaintGlue { return spacing; } + static void doFontExtent(JNIEnv* env, jlong paintHandle, const jchar buf[], jint start, + jint count, jint bufSize, jboolean isRtl, jobject fmi) { + const Paint* paint = reinterpret_cast<Paint*>(paintHandle); + const Typeface* typeface = paint->getAndroidTypeface(); + minikin::Bidi bidiFlags = isRtl ? minikin::Bidi::FORCE_RTL : minikin::Bidi::FORCE_LTR; + minikin::MinikinExtent extent = + MinikinUtils::getFontExtent(paint, bidiFlags, typeface, buf, start, count, bufSize); + + SkFontMetrics metrics; + getMetricsInternal(paintHandle, &metrics); + + metrics.fAscent = extent.ascent; + metrics.fDescent = extent.descent; + + // If top/bottom is narrower than ascent/descent, adjust top/bottom to ascent/descent. + metrics.fTop = std::min(metrics.fAscent, metrics.fTop); + metrics.fBottom = std::max(metrics.fDescent, metrics.fBottom); + + GraphicsJNI::set_metrics_int(env, fmi, metrics); + } + + static void getFontMetricsIntForText___C(JNIEnv* env, jclass, jlong paintHandle, + jcharArray text, jint start, jint count, jint ctxStart, + jint ctxCount, jboolean isRtl, jobject fmi) { + ScopedCharArrayRO textArray(env, text); + + doFontExtent(env, paintHandle, textArray.get() + ctxStart, start - ctxStart, count, + ctxCount, isRtl, fmi); + } + + static void getFontMetricsIntForText___String(JNIEnv* env, jclass, jlong paintHandle, + jstring text, jint start, jint count, + jint ctxStart, jint ctxCount, jboolean isRtl, + jobject fmi) { + ScopedStringChars textChars(env, text); + + doFontExtent(env, paintHandle, textChars.get() + ctxStart, start - ctxStart, count, + ctxCount, isRtl, fmi); + } + + // ------------------ @FastNative --------------------------- + + static jint setTextLocales(JNIEnv* env, jobject clazz, jlong objHandle, jstring locales) { + Paint* obj = reinterpret_cast<Paint*>(objHandle); + ScopedUtfChars localesChars(env, locales); + jint minikinLocaleListId = minikin::registerLocaleList(localesChars.c_str()); + obj->setMinikinLocaleListId(minikinLocaleListId); + return minikinLocaleListId; + } + + static void setFontFeatureSettings(JNIEnv* env, jobject clazz, jlong paintHandle, + jstring settings) { + Paint* paint = reinterpret_cast<Paint*>(paintHandle); + if (!settings) { + paint->setFontFeatureSettings(std::string()); + } else { + ScopedUtfChars settingsChars(env, settings); + paint->setFontFeatureSettings(std::string(settingsChars.c_str(), settingsChars.size())); + } + } + static jfloat getFontMetrics(JNIEnv* env, jobject, jlong paintHandle, jobject metricsObj) { SkFontMetrics metrics; SkScalar spacing = getMetricsInternal(paintHandle, &metrics); @@ -663,8 +704,7 @@ namespace PaintGlue { } static void setFilterBitmap(CRITICAL_JNI_PARAMS_COMMA jlong paintHandle, jboolean filterBitmap) { - reinterpret_cast<Paint*>(paintHandle)->setFilterQuality( - filterBitmap ? kLow_SkFilterQuality : kNone_SkFilterQuality); + reinterpret_cast<Paint*>(paintHandle)->setFilterBitmap(filterBitmap); } static void setDither(CRITICAL_JNI_PARAMS_COMMA jlong paintHandle, jboolean dither) { @@ -1016,6 +1056,11 @@ static const JNINativeMethod methods[] = { {"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 ---------------------- @@ -1094,6 +1139,7 @@ static const JNINativeMethod methods[] = { {"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/PaintFilter.cpp b/libs/hwui/jni/PaintFilter.cpp index ec115b4e141c..6b5310107c00 100644 --- a/libs/hwui/jni/PaintFilter.cpp +++ b/libs/hwui/jni/PaintFilter.cpp @@ -29,10 +29,6 @@ public: fClearFlags = static_cast<uint16_t>(clearFlags); fSetFlags = static_cast<uint16_t>(setFlags); } - void filter(SkPaint* paint) override { - uint32_t flags = Paint::GetSkPaintJavaFlags(*paint); - Paint::SetSkPaintJavaFlags(paint, (flags & ~fClearFlags) | fSetFlags); - } void filterFullPaint(Paint* paint) override { paint->setJavaFlags((paint->getJavaFlags() & ~fClearFlags) | fSetFlags); } @@ -74,7 +70,7 @@ int register_android_graphics_DrawFilter(JNIEnv* env) { result |= RegisterMethodsOrDie(env, "android/graphics/PaintFlagsDrawFilter", paintflags_methods, NELEM(paintflags_methods)); - return 0; + return result; } } diff --git a/libs/hwui/jni/RenderEffect.cpp b/libs/hwui/jni/RenderEffect.cpp index a48d7f734e29..213f35a81b88 100644 --- a/libs/hwui/jni/RenderEffect.cpp +++ b/libs/hwui/jni/RenderEffect.cpp @@ -127,6 +127,32 @@ static jlong createShaderEffect( return reinterpret_cast<jlong>(shaderFilter.release()); } +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 jlong createRuntimeShaderEffect(JNIEnv* env, jobject, jlong shaderBuilderHandle, + jstring inputShaderName) { + SkRuntimeShaderBuilder* builder = + reinterpret_cast<SkRuntimeShaderBuilder*>(shaderBuilderHandle); + ScopedUtfChars name(env, inputShaderName); + + if (builder->child(name.c_str()).fChild == nullptr) { + ThrowIAEFmt(env, + "unable to find a uniform with the name '%s' of the correct " + "type defined by the provided RuntimeShader", + name.c_str()); + return 0; + } + + sk_sp<SkImageFilter> filter = SkImageFilters::RuntimeShader(*builder, name.c_str(), nullptr); + return reinterpret_cast<jlong>(filter.release()); +} + static void RenderEffect_safeUnref(SkImageFilter* filter) { SkSafeUnref(filter); } @@ -136,15 +162,16 @@ static jlong getRenderEffectFinalizer(JNIEnv*, jobject) { } static const JNINativeMethod gRenderEffectMethods[] = { - {"nativeGetFinalizer", "()J", (void*)getRenderEffectFinalizer}, - {"nativeCreateOffsetEffect", "(FFJ)J", (void*)createOffsetEffect}, - {"nativeCreateBlurEffect", "(FFJI)J", (void*)createBlurEffect}, - {"nativeCreateBitmapEffect", "(JFFFFFFFF)J", (void*)createBitmapEffect}, - {"nativeCreateColorFilterEffect", "(JJ)J", (void*)createColorFilterEffect}, - {"nativeCreateBlendModeEffect", "(JJI)J", (void*)createBlendModeEffect}, - {"nativeCreateChainEffect", "(JJ)J", (void*)createChainEffect}, - {"nativeCreateShaderEffect", "(J)J", (void*)createShaderEffect} -}; + {"nativeGetFinalizer", "()J", (void*)getRenderEffectFinalizer}, + {"nativeCreateOffsetEffect", "(FFJ)J", (void*)createOffsetEffect}, + {"nativeCreateBlurEffect", "(FFJI)J", (void*)createBlurEffect}, + {"nativeCreateBitmapEffect", "(JFFFFFFFF)J", (void*)createBitmapEffect}, + {"nativeCreateColorFilterEffect", "(JJ)J", (void*)createColorFilterEffect}, + {"nativeCreateBlendModeEffect", "(JJI)J", (void*)createBlendModeEffect}, + {"nativeCreateChainEffect", "(JJ)J", (void*)createChainEffect}, + {"nativeCreateShaderEffect", "(J)J", (void*)createShaderEffect}, + {"nativeCreateRuntimeShaderEffect", "(JLjava/lang/String;)J", + (void*)createRuntimeShaderEffect}}; int register_android_graphics_RenderEffect(JNIEnv* env) { android::RegisterMethodsOrDie(env, "android/graphics/RenderEffect", diff --git a/libs/hwui/jni/Shader.cpp b/libs/hwui/jni/Shader.cpp index 90184432e8a4..0bbd8a8cf97c 100644 --- a/libs/hwui/jni/Shader.cpp +++ b/libs/hwui/jni/Shader.cpp @@ -64,7 +64,8 @@ static jlong Shader_getNativeFinalizer(JNIEnv*, jobject) { /////////////////////////////////////////////////////////////////////////////////////////////// static jlong BitmapShader_constructor(JNIEnv* env, jobject o, jlong matrixPtr, jlong bitmapHandle, - jint tileModeX, jint tileModeY, bool filter) { + jint tileModeX, jint tileModeY, bool filter, + bool isDirectSampled) { const SkMatrix* matrix = reinterpret_cast<const SkMatrix*>(matrixPtr); sk_sp<SkImage> image; if (bitmapHandle) { @@ -79,8 +80,12 @@ static jlong BitmapShader_constructor(JNIEnv* env, jobject o, jlong matrixPtr, j } SkSamplingOptions sampling(filter ? SkFilterMode::kLinear : SkFilterMode::kNearest, SkMipmapMode::kNone); - sk_sp<SkShader> shader = image->makeShader( - (SkTileMode)tileModeX, (SkTileMode)tileModeY, sampling); + sk_sp<SkShader> shader; + if (isDirectSampled) { + shader = image->makeRawShader((SkTileMode)tileModeX, (SkTileMode)tileModeY, sampling); + } else { + shader = image->makeShader((SkTileMode)tileModeX, (SkTileMode)tileModeY, sampling); + } ThrowIAE_IfNull(env, shader.get()); if (matrix) { @@ -256,11 +261,10 @@ static jlong RuntimeShader_getNativeFinalizer(JNIEnv*, jobject) { return static_cast<jlong>(reinterpret_cast<uintptr_t>(&SkRuntimeShaderBuilder_delete)); } -static jlong RuntimeShader_create(JNIEnv* env, jobject, jlong shaderBuilder, jlong matrixPtr, - jboolean isOpaque) { +static jlong RuntimeShader_create(JNIEnv* env, jobject, jlong shaderBuilder, jlong matrixPtr) { SkRuntimeShaderBuilder* builder = reinterpret_cast<SkRuntimeShaderBuilder*>(shaderBuilder); const SkMatrix* matrix = reinterpret_cast<const SkMatrix*>(matrixPtr); - sk_sp<SkShader> shader = builder->makeShader(matrix, isOpaque == JNI_TRUE); + sk_sp<SkShader> shader = builder->makeShader(matrix); ThrowIAE_IfNull(env, shader); return reinterpret_cast<jlong>(shader.release()); } @@ -273,21 +277,99 @@ static inline int ThrowIAEFmt(JNIEnv* env, const char* fmt, ...) { return ret; } -static void RuntimeShader_updateUniforms(JNIEnv* env, jobject, jlong shaderBuilder, - jstring jUniformName, jfloatArray jvalues) { +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 UpdateFloatUniforms(JNIEnv* env, SkRuntimeShaderBuilder* builder, + const char* uniformName, const float values[], int count, + bool isColor) { + SkRuntimeShaderBuilder::BuilderUniform 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 RuntimeShader_updateFloatUniforms(JNIEnv* env, jobject, jlong shaderBuilder, + jstring jUniformName, jfloat value1, jfloat value2, + jfloat value3, jfloat value4, jint count) { + SkRuntimeShaderBuilder* builder = reinterpret_cast<SkRuntimeShaderBuilder*>(shaderBuilder); + ScopedUtfChars name(env, jUniformName); + const float values[4] = {value1, value2, value3, value4}; + UpdateFloatUniforms(env, builder, name.c_str(), values, count, false); +} + +static void RuntimeShader_updateFloatArrayUniforms(JNIEnv* env, jobject, jlong shaderBuilder, + jstring jUniformName, jfloatArray jvalues, + jboolean isColor) { SkRuntimeShaderBuilder* builder = reinterpret_cast<SkRuntimeShaderBuilder*>(shaderBuilder); ScopedUtfChars name(env, jUniformName); AutoJavaFloatArray autoValues(env, jvalues, 0, kRO_JNIAccess); + UpdateFloatUniforms(env, builder, name.c_str(), autoValues.ptr(), autoValues.length(), isColor); +} - SkRuntimeShaderBuilder::BuilderUniform uniform = builder->uniform(name.c_str()); +static void UpdateIntUniforms(JNIEnv* env, SkRuntimeShaderBuilder* builder, const char* uniformName, + const int values[], int count) { + SkRuntimeShaderBuilder::BuilderUniform uniform = builder->uniform(uniformName); if (uniform.fVar == nullptr) { - ThrowIAEFmt(env, "unable to find uniform named %s", name.c_str()); - } else if (!uniform.set<float>(autoValues.ptr(), autoValues.length())) { + 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) * autoValues.length()); + uniform.fVar->sizeInBytes(), sizeof(float) * count); } } +static void RuntimeShader_updateIntUniforms(JNIEnv* env, jobject, jlong shaderBuilder, + jstring jUniformName, jint value1, jint value2, + jint value3, jint value4, jint count) { + SkRuntimeShaderBuilder* builder = reinterpret_cast<SkRuntimeShaderBuilder*>(shaderBuilder); + ScopedUtfChars name(env, jUniformName); + const int values[4] = {value1, value2, value3, value4}; + UpdateIntUniforms(env, builder, name.c_str(), values, count); +} + +static void RuntimeShader_updateIntArrayUniforms(JNIEnv* env, jobject, jlong shaderBuilder, + jstring jUniformName, jintArray jvalues) { + SkRuntimeShaderBuilder* builder = reinterpret_cast<SkRuntimeShaderBuilder*>(shaderBuilder); + ScopedUtfChars name(env, jUniformName); + AutoJavaIntArray autoValues(env, jvalues, 0); + UpdateIntUniforms(env, builder, name.c_str(), autoValues.ptr(), autoValues.length()); +} + static void RuntimeShader_updateShader(JNIEnv* env, jobject, jlong shaderBuilder, jstring jUniformName, jlong shaderHandle) { SkRuntimeShaderBuilder* builder = reinterpret_cast<SkRuntimeShaderBuilder*>(shaderBuilder); @@ -295,7 +377,7 @@ static void RuntimeShader_updateShader(JNIEnv* env, jobject, jlong shaderBuilder SkShader* shader = reinterpret_cast<SkShader*>(shaderHandle); SkRuntimeShaderBuilder::BuilderChild child = builder->child(name.c_str()); - if (child.fIndex == -1) { + if (child.fChild == nullptr) { ThrowIAEFmt(env, "unable to find shader named %s", name.c_str()); return; } @@ -315,7 +397,7 @@ static const JNINativeMethod gShaderMethods[] = { }; static const JNINativeMethod gBitmapShaderMethods[] = { - { "nativeCreate", "(JJIIZ)J", (void*)BitmapShader_constructor }, + {"nativeCreate", "(JJIIZZ)J", (void*)BitmapShader_constructor}, }; static const JNINativeMethod gLinearGradientMethods[] = { @@ -336,9 +418,16 @@ static const JNINativeMethod gComposeShaderMethods[] = { static const JNINativeMethod gRuntimeShaderMethods[] = { {"nativeGetFinalizer", "()J", (void*)RuntimeShader_getNativeFinalizer}, - {"nativeCreateShader", "(JJZ)J", (void*)RuntimeShader_create}, + {"nativeCreateShader", "(JJ)J", (void*)RuntimeShader_create}, {"nativeCreateBuilder", "(Ljava/lang/String;)J", (void*)RuntimeShader_createShaderBuilder}, - {"nativeUpdateUniforms", "(JLjava/lang/String;[F)V", (void*)RuntimeShader_updateUniforms}, + {"nativeUpdateUniforms", "(JLjava/lang/String;[FZ)V", + (void*)RuntimeShader_updateFloatArrayUniforms}, + {"nativeUpdateUniforms", "(JLjava/lang/String;FFFFI)V", + (void*)RuntimeShader_updateFloatUniforms}, + {"nativeUpdateUniforms", "(JLjava/lang/String;[I)V", + (void*)RuntimeShader_updateIntArrayUniforms}, + {"nativeUpdateUniforms", "(JLjava/lang/String;IIIII)V", + (void*)RuntimeShader_updateIntUniforms}, {"nativeUpdateShader", "(JLjava/lang/String;J)V", (void*)RuntimeShader_updateShader}, }; diff --git a/libs/hwui/jni/Utils.cpp b/libs/hwui/jni/Utils.cpp index ac2f5b77d23a..106c6db57e18 100644 --- a/libs/hwui/jni/Utils.cpp +++ b/libs/hwui/jni/Utils.cpp @@ -18,6 +18,7 @@ #include "SkUtils.h" #include "SkData.h" +#include <inttypes.h> #include <log/log.h> using namespace android; @@ -30,7 +31,7 @@ AssetStreamAdaptor::AssetStreamAdaptor(Asset* asset) bool AssetStreamAdaptor::rewind() { off64_t pos = fAsset->seek(0, SEEK_SET); if (pos == (off64_t)-1) { - SkDebugf("----- fAsset->seek(rewind) failed\n"); + ALOGD("----- fAsset->seek(rewind) failed\n"); return false; } return true; @@ -58,7 +59,7 @@ bool AssetStreamAdaptor::hasPosition() const { size_t AssetStreamAdaptor::getPosition() const { const off64_t offset = fAsset->seek(0, SEEK_CUR); if (offset == -1) { - SkDebugf("---- fAsset->seek(0, SEEK_CUR) failed\n"); + ALOGD("---- fAsset->seek(0, SEEK_CUR) failed\n"); return 0; } @@ -67,7 +68,7 @@ size_t AssetStreamAdaptor::getPosition() const { bool AssetStreamAdaptor::seek(size_t position) { if (fAsset->seek(position, SEEK_SET) == -1) { - SkDebugf("---- fAsset->seek(0, SEEK_SET) failed\n"); + ALOGD("---- fAsset->seek(0, SEEK_SET) failed\n"); return false; } @@ -76,7 +77,7 @@ bool AssetStreamAdaptor::seek(size_t position) { bool AssetStreamAdaptor::move(long offset) { if (fAsset->seek(offset, SEEK_CUR) == -1) { - SkDebugf("---- fAsset->seek(%i, SEEK_CUR) failed\n", offset); + ALOGD("---- fAsset->seek(%li, SEEK_CUR) failed\n", offset); return false; } @@ -95,12 +96,12 @@ size_t AssetStreamAdaptor::read(void* buffer, size_t size) { off64_t oldOffset = fAsset->seek(0, SEEK_CUR); if (-1 == oldOffset) { - SkDebugf("---- fAsset->seek(oldOffset) failed\n"); + ALOGD("---- fAsset->seek(oldOffset) failed\n"); return 0; } off64_t newOffset = fAsset->seek(size, SEEK_CUR); if (-1 == newOffset) { - SkDebugf("---- fAsset->seek(%d) failed\n", size); + ALOGD("---- fAsset->seek(%zu) failed\n", size); return 0; } amount = newOffset - oldOffset; @@ -121,20 +122,20 @@ sk_sp<SkData> android::CopyAssetToData(Asset* asset) { const off64_t seekReturnVal = asset->seek(0, SEEK_SET); if ((off64_t)-1 == seekReturnVal) { - SkDebugf("---- copyAsset: asset rewind failed\n"); + ALOGD("---- copyAsset: asset rewind failed\n"); return NULL; } const off64_t size = asset->getLength(); if (size <= 0) { - SkDebugf("---- copyAsset: asset->getLength() returned %d\n", size); + ALOGD("---- copyAsset: asset->getLength() returned %" PRId64 "\n", size); return NULL; } sk_sp<SkData> data(SkData::MakeUninitialized(size)); const off64_t len = asset->read(data->writable_data(), size); if (len != size) { - SkDebugf("---- copyAsset: asset->read(%d) returned %d\n", size, len); + ALOGD("---- copyAsset: asset->read(%" PRId64 ") returned %" PRId64 "\n", size, len); return NULL; } @@ -143,7 +144,7 @@ sk_sp<SkData> android::CopyAssetToData(Asset* asset) { jobject android::nullObjectReturn(const char msg[]) { if (msg) { - SkDebugf("--- %s\n", msg); + ALOGD("--- %s\n", msg); } return NULL; } diff --git a/libs/hwui/jni/YuvToJpegEncoder.cpp b/libs/hwui/jni/YuvToJpegEncoder.cpp index 689cf0bea741..77f42ae70268 100644 --- a/libs/hwui/jni/YuvToJpegEncoder.cpp +++ b/libs/hwui/jni/YuvToJpegEncoder.cpp @@ -85,7 +85,7 @@ Yuv420SpToJpegEncoder::Yuv420SpToJpegEncoder(int* strides) : void Yuv420SpToJpegEncoder::compress(jpeg_compress_struct* cinfo, uint8_t* yuv, int* offsets) { - SkDebugf("onFlyCompress"); + ALOGD("onFlyCompress"); JSAMPROW y[16]; JSAMPROW cb[8]; JSAMPROW cr[8]; @@ -161,7 +161,7 @@ Yuv422IToJpegEncoder::Yuv422IToJpegEncoder(int* strides) : void Yuv422IToJpegEncoder::compress(jpeg_compress_struct* cinfo, uint8_t* yuv, int* offsets) { - SkDebugf("onFlyCompress_422"); + ALOGD("onFlyCompress_422"); JSAMPROW y[16]; JSAMPROW cb[16]; JSAMPROW cr[16]; diff --git a/libs/hwui/jni/android_graphics_Canvas.cpp b/libs/hwui/jni/android_graphics_Canvas.cpp index a611f7ce2d14..0ef80ee10708 100644 --- a/libs/hwui/jni/android_graphics_Canvas.cpp +++ b/libs/hwui/jni/android_graphics_Canvas.cpp @@ -188,39 +188,57 @@ static jboolean quickRejectPath(CRITICAL_JNI_PARAMS_COMMA jlong canvasHandle, jl return result ? JNI_TRUE : JNI_FALSE; } -// SkRegion::Op and SkClipOp are numerically identical, so we can freely cast -// from one to the other (though SkClipOp is destined to become a strict subset) +// SkClipOp is a strict subset of SkRegion::Op and is castable back and forth for their +// shared operations (intersect and difference). static_assert(SkRegion::kDifference_Op == static_cast<SkRegion::Op>(SkClipOp::kDifference), ""); static_assert(SkRegion::kIntersect_Op == static_cast<SkRegion::Op>(SkClipOp::kIntersect), ""); -static_assert(SkRegion::kUnion_Op == static_cast<SkRegion::Op>(SkClipOp::kUnion_deprecated), ""); -static_assert(SkRegion::kXOR_Op == static_cast<SkRegion::Op>(SkClipOp::kXOR_deprecated), ""); -static_assert(SkRegion::kReverseDifference_Op == static_cast<SkRegion::Op>(SkClipOp::kReverseDifference_deprecated), ""); -static_assert(SkRegion::kReplace_Op == static_cast<SkRegion::Op>(SkClipOp::kReplace_deprecated), ""); - -static SkClipOp opHandleToClipOp(jint opHandle) { - // The opHandle is defined in Canvas.java to be Region::Op - SkRegion::Op rgnOp = static_cast<SkRegion::Op>(opHandle); - - // In the future, when we no longer support the wide range of ops (e.g. Union, Xor) - // this function can perform a range check and throw an unsupported-exception. - // e.g. if (rgnOp != kIntersect && rgnOp != kDifference) throw... - - // Skia now takes a different type, SkClipOp, as the parameter to clipping calls - // This type is binary compatible with SkRegion::Op, so a static_cast<> is safe. - return static_cast<SkClipOp>(rgnOp); -} static jboolean clipRect(CRITICAL_JNI_PARAMS_COMMA jlong canvasHandle, jfloat l, jfloat t, jfloat r, jfloat b, jint opHandle) { - bool nonEmptyClip = get_canvas(canvasHandle)->clipRect(l, t, r, b, - opHandleToClipOp(opHandle)); + // The opHandle is defined in Canvas.java to be Region::Op + SkRegion::Op rgnOp = static_cast<SkRegion::Op>(opHandle); + bool nonEmptyClip; + switch (rgnOp) { + case SkRegion::Op::kIntersect_Op: + case SkRegion::Op::kDifference_Op: + // Intersect and difference are supported clip operations + nonEmptyClip = + get_canvas(canvasHandle)->clipRect(l, t, r, b, static_cast<SkClipOp>(rgnOp)); + break; + case SkRegion::Op::kReplace_Op: + // Replace is emulated to support legacy apps older than P + nonEmptyClip = get_canvas(canvasHandle)->replaceClipRect_deprecated(l, t, r, b); + break; + default: + // All other operations would expand the clip and are no longer supported, + // so log and skip (to avoid breaking legacy apps). + ALOGW("Ignoring unsupported clip operation %d", opHandle); + SkRect clipBounds; // ignored + nonEmptyClip = get_canvas(canvasHandle)->getClipBounds(&clipBounds); + break; + } return nonEmptyClip ? JNI_TRUE : JNI_FALSE; } static jboolean clipPath(CRITICAL_JNI_PARAMS_COMMA jlong canvasHandle, jlong pathHandle, jint opHandle) { + SkRegion::Op rgnOp = static_cast<SkRegion::Op>(opHandle); SkPath* path = reinterpret_cast<SkPath*>(pathHandle); - bool nonEmptyClip = get_canvas(canvasHandle)->clipPath(path, opHandleToClipOp(opHandle)); + bool nonEmptyClip; + switch (rgnOp) { + case SkRegion::Op::kIntersect_Op: + case SkRegion::Op::kDifference_Op: + nonEmptyClip = get_canvas(canvasHandle)->clipPath(path, static_cast<SkClipOp>(rgnOp)); + break; + case SkRegion::Op::kReplace_Op: + nonEmptyClip = get_canvas(canvasHandle)->replaceClipPath_deprecated(path); + break; + default: + ALOGW("Ignoring unsupported clip operation %d", opHandle); + SkRect clipBounds; // ignored + nonEmptyClip = get_canvas(canvasHandle)->getClipBounds(&clipBounds); + break; + } return nonEmptyClip ? JNI_TRUE : JNI_FALSE; } @@ -233,7 +251,7 @@ static void drawColorLong(JNIEnv* env, jobject, jlong canvasHandle, jlong colorS jlong colorLong, jint modeHandle) { SkColor4f color = GraphicsJNI::convertColorLong(colorLong); sk_sp<SkColorSpace> cs = GraphicsJNI::getNativeColorSpace(colorSpaceHandle); - SkPaint p; + Paint p; p.setColor4f(color, cs.get()); SkBlendMode mode = static_cast<SkBlendMode>(modeHandle); @@ -421,7 +439,7 @@ static void drawNinePatch(JNIEnv* env, jobject, jlong canvasHandle, jlong bitmap if (paint) { filteredPaint = *paint; } - filteredPaint.setFilterQuality(kLow_SkFilterQuality); + filteredPaint.setFilterBitmap(true); canvas->drawNinePatch(bitmap, *chunk, 0, 0, (right-left)/scale, (bottom-top)/scale, &filteredPaint); @@ -443,7 +461,7 @@ static void drawBitmap(JNIEnv* env, jobject, jlong canvasHandle, jlong bitmapHan if (paint) { filteredPaint = *paint; } - filteredPaint.setFilterQuality(kLow_SkFilterQuality); + filteredPaint.setFilterBitmap(true); canvas->drawBitmap(bitmap, left, top, &filteredPaint); } else { canvas->drawBitmap(bitmap, left, top, paint); @@ -458,7 +476,7 @@ static void drawBitmap(JNIEnv* env, jobject, jlong canvasHandle, jlong bitmapHan if (paint) { filteredPaint = *paint; } - filteredPaint.setFilterQuality(kLow_SkFilterQuality); + filteredPaint.setFilterBitmap(true); canvas->drawBitmap(bitmap, 0, 0, &filteredPaint); canvas->restore(); @@ -486,7 +504,7 @@ static void drawBitmapRect(JNIEnv* env, jobject, jlong canvasHandle, jlong bitma if (paint) { filteredPaint = *paint; } - filteredPaint.setFilterQuality(kLow_SkFilterQuality); + filteredPaint.setFilterBitmap(true); canvas->drawBitmap(bitmap, srcLeft, srcTop, srcRight, srcBottom, dstLeft, dstTop, dstRight, dstBottom, &filteredPaint); } else { diff --git a/libs/hwui/jni/android_graphics_HardwareRenderer.cpp b/libs/hwui/jni/android_graphics_HardwareRenderer.cpp index b5536ad4830d..c48448dffdd2 100644 --- a/libs/hwui/jni/android_graphics_HardwareRenderer.cpp +++ b/libs/hwui/jni/android_graphics_HardwareRenderer.cpp @@ -259,7 +259,8 @@ static void android_view_ThreadedRenderer_setIsHighEndGfx(JNIEnv* env, jobject c } static int android_view_ThreadedRenderer_syncAndDrawFrame(JNIEnv* env, jobject clazz, - jlong proxyPtr, jlongArray frameInfo, jint frameInfoSize) { + jlong proxyPtr, jlongArray frameInfo, + jint frameInfoSize) { LOG_ALWAYS_FATAL_IF(frameInfoSize != UI_THREAD_FRAME_INFO_SIZE, "Mismatched size expectations, given %d expected %zu", frameInfoSize, UI_THREAD_FRAME_INFO_SIZE); @@ -379,6 +380,13 @@ static void android_view_ThreadedRenderer_dumpProfileInfo(JNIEnv* env, jobject c proxy->dumpProfileInfo(fd, dumpFlags); } +static void android_view_ThreadedRenderer_dumpGlobalProfileInfo(JNIEnv* env, jobject clazz, + jobject javaFileDescriptor, + jint dumpFlags) { + int fd = jniGetFDFromFileDescriptor(env, javaFileDescriptor); + RenderProxy::dumpGraphicsMemory(fd, true, dumpFlags & DumpFlags::Reset); +} + static void android_view_ThreadedRenderer_addRenderNode(JNIEnv* env, jobject clazz, jlong proxyPtr, jlong renderNodePtr, jboolean placeFront) { RenderProxy* proxy = reinterpret_cast<RenderProxy*>(proxyPtr); @@ -406,6 +414,12 @@ static void android_view_ThreadedRenderer_setContentDrawBounds(JNIEnv* env, proxy->setContentDrawBounds(left, top, right, bottom); } +static void android_view_ThreadedRenderer_forceDrawNextFrame(JNIEnv* env, jobject clazz, + jlong proxyPtr) { + RenderProxy* proxy = reinterpret_cast<RenderProxy*>(proxyPtr); + proxy->forceDrawNextFrame(); +} + class JGlobalRefHolder { public: JGlobalRefHolder(JavaVM* vm, jobject object) : mVm(vm), mObject(object) {} @@ -426,28 +440,6 @@ private: jobject mObject; }; -class JWeakGlobalRefHolder { -public: - JWeakGlobalRefHolder(JavaVM* vm, jobject object) : mVm(vm) { - mWeakRef = getenv(vm)->NewWeakGlobalRef(object); - } - - virtual ~JWeakGlobalRefHolder() { - if (mWeakRef != nullptr) getenv(mVm)->DeleteWeakGlobalRef(mWeakRef); - mWeakRef = nullptr; - } - - jobject ref() { return mWeakRef; } - JavaVM* vm() { return mVm; } - -private: - JWeakGlobalRefHolder(const JWeakGlobalRefHolder&) = delete; - void operator=(const JWeakGlobalRefHolder&) = delete; - - JavaVM* mVm; - jobject mWeakRef; -}; - using TextureMap = std::unordered_map<uint32_t, sk_sp<SkImage>>; struct PictureCaptureState { @@ -581,20 +573,16 @@ static void android_view_ThreadedRenderer_setASurfaceTransactionCallback( } else { JavaVM* vm = nullptr; LOG_ALWAYS_FATAL_IF(env->GetJavaVM(&vm) != JNI_OK, "Unable to get Java VM"); - auto globalCallbackRef = - std::make_shared<JWeakGlobalRefHolder>(vm, aSurfaceTransactionCallback); + 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()); - jobject localref = env->NewLocalRef(globalCallbackRef->ref()); - if (CC_UNLIKELY(!localref)) { - return false; - } jboolean ret = env->CallBooleanMethod( - localref, gASurfaceTransactionCallback.onMergeTransaction, + globalCallbackRef->object(), + gASurfaceTransactionCallback.onMergeTransaction, static_cast<jlong>(transObj), static_cast<jlong>(scObj), static_cast<jlong>(frameNr)); - env->DeleteLocalRef(localref); return ret; }); } @@ -609,15 +597,11 @@ static void android_view_ThreadedRenderer_setPrepareSurfaceControlForWebviewCall JavaVM* vm = nullptr; LOG_ALWAYS_FATAL_IF(env->GetJavaVM(&vm) != JNI_OK, "Unable to get Java VM"); auto globalCallbackRef = - std::make_shared<JWeakGlobalRefHolder>(vm, callback); + std::make_shared<JGlobalRefHolder>(vm, env->NewGlobalRef(callback)); proxy->setPrepareSurfaceControlForWebviewCallback([globalCallbackRef]() { JNIEnv* env = getenv(globalCallbackRef->vm()); - jobject localref = env->NewLocalRef(globalCallbackRef->ref()); - if (CC_UNLIKELY(!localref)) { - return; - } - env->CallVoidMethod(localref, gPrepareSurfaceControlForWebviewCallback.prepare); - env->DeleteLocalRef(localref); + env->CallVoidMethod(globalCallbackRef->object(), + gPrepareSurfaceControlForWebviewCallback.prepare); }); } } @@ -632,10 +616,19 @@ static void android_view_ThreadedRenderer_setFrameCallback(JNIEnv* env, LOG_ALWAYS_FATAL_IF(env->GetJavaVM(&vm) != JNI_OK, "Unable to get Java VM"); auto globalCallbackRef = std::make_shared<JGlobalRefHolder>(vm, env->NewGlobalRef(frameCallback)); - proxy->setFrameCallback([globalCallbackRef](int64_t frameNr) { + proxy->setFrameCallback([globalCallbackRef](int32_t syncResult, + int64_t frameNr) -> std::function<void(bool)> { JNIEnv* env = getenv(globalCallbackRef->vm()); - env->CallVoidMethod(globalCallbackRef->object(), gFrameDrawingCallback.onFrameDraw, - static_cast<jlong>(frameNr)); + ScopedLocalRef<jobject> frameCommitCallback( + env, env->CallObjectMethod( + globalCallbackRef->object(), gFrameDrawingCallback.onFrameDraw, + static_cast<jint>(syncResult), static_cast<jlong>(frameNr))); + if (frameCommitCallback == nullptr) { + return nullptr; + } + sp<FrameCommitWrapper> wrapper = + sp<FrameCommitWrapper>::make(env, frameCommitCallback.get()); + return [wrapper](bool didProduceBuffer) { wrapper->onFrameCommit(didProduceBuffer); }; }); } } @@ -646,7 +639,7 @@ static void android_view_ThreadedRenderer_setFrameCommitCallback(JNIEnv* env, jo if (!callback) { proxy->setFrameCommitCallback(nullptr); } else { - sp<FrameCommitWrapper> wrapper = new FrameCommitWrapper{env, callback}; + sp<FrameCommitWrapper> wrapper = sp<FrameCommitWrapper>::make(env, callback); proxy->setFrameCommitCallback( [wrapper](bool didProduceBuffer) { wrapper->onFrameCommit(didProduceBuffer); }); } @@ -784,11 +777,6 @@ static void android_view_ThreadedRenderer_setHighContrastText(JNIEnv*, jclass, j Properties::enableHighContrastText = enable; } -static void android_view_ThreadedRenderer_hackySetRTAnimationsEnabled(JNIEnv*, jclass, - jboolean enable) { - Properties::enableRTAnimations = enable; -} - static void android_view_ThreadedRenderer_setDebuggingEnabled(JNIEnv*, jclass, jboolean enable) { Properties::debuggingEnabled = enable; } @@ -818,6 +806,11 @@ static void android_view_ThreadedRenderer_preload(JNIEnv*, jclass) { RenderProxy::preload(); } +static void android_view_ThreadedRenderer_setRtAnimationsEnabled(JNIEnv* env, jobject clazz, + jboolean enabled) { + RenderProxy::setRtAnimationsEnabled(enabled); +} + // Plumbs the display density down to DeviceInfo. static void android_view_ThreadedRenderer_setDisplayDensityDpi(JNIEnv*, jclass, jint densityDpi) { // Convert from dpi to density-independent pixels. @@ -838,6 +831,14 @@ static void android_view_ThreadedRenderer_initDisplayInfo(JNIEnv*, jclass, jint DeviceInfo::setPresentationDeadlineNanos(presentationDeadlineNanos); } +static void android_view_ThreadedRenderer_setDrawingEnabled(JNIEnv*, jclass, jboolean enabled) { + Properties::setDrawingEnabled(enabled); +} + +static jboolean android_view_ThreadedRenderer_isDrawingEnabled(JNIEnv*, jclass) { + return Properties::isDrawingEnabled(); +} + // ---------------------------------------------------------------------------- // HardwareRendererObserver // ---------------------------------------------------------------------------- @@ -932,6 +933,8 @@ static const JNINativeMethod gMethods[] = { {"nNotifyFramePending", "(J)V", (void*)android_view_ThreadedRenderer_notifyFramePending}, {"nDumpProfileInfo", "(JLjava/io/FileDescriptor;I)V", (void*)android_view_ThreadedRenderer_dumpProfileInfo}, + {"nDumpGlobalProfileInfo", "(Ljava/io/FileDescriptor;I)V", + (void*)android_view_ThreadedRenderer_dumpGlobalProfileInfo}, {"setupShadersDiskCache", "(Ljava/lang/String;Ljava/lang/String;)V", (void*)android_view_ThreadedRenderer_setupShadersDiskCache}, {"nAddRenderNode", "(JJZ)V", (void*)android_view_ThreadedRenderer_addRenderNode}, @@ -939,6 +942,7 @@ static const JNINativeMethod gMethods[] = { {"nDrawRenderNode", "(JJ)V", (void*)android_view_ThreadedRendererd_drawRenderNode}, {"nSetContentDrawBounds", "(JIIII)V", (void*)android_view_ThreadedRenderer_setContentDrawBounds}, + {"nForceDrawNextFrame", "(J)V", (void*)android_view_ThreadedRenderer_forceDrawNextFrame}, {"nSetPictureCaptureCallback", "(JLandroid/graphics/HardwareRenderer$PictureCapturedCallback;)V", (void*)android_view_ThreadedRenderer_setPictureCapturedCallbackJNI}, @@ -963,8 +967,6 @@ static const JNINativeMethod gMethods[] = { (void*)android_view_ThreadedRenderer_createHardwareBitmapFromRenderNode}, {"disableVsync", "()V", (void*)android_view_ThreadedRenderer_disableVsync}, {"nSetHighContrastText", "(Z)V", (void*)android_view_ThreadedRenderer_setHighContrastText}, - {"nHackySetRTAnimationsEnabled", "(Z)V", - (void*)android_view_ThreadedRenderer_hackySetRTAnimationsEnabled}, {"nSetDebuggingEnabled", "(Z)V", (void*)android_view_ThreadedRenderer_setDebuggingEnabled}, {"nSetIsolatedProcess", "(Z)V", (void*)android_view_ThreadedRenderer_setIsolatedProcess}, {"nSetContextPriority", "(I)V", (void*)android_view_ThreadedRenderer_setContextPriority}, @@ -976,6 +978,10 @@ static const JNINativeMethod gMethods[] = { {"preload", "()V", (void*)android_view_ThreadedRenderer_preload}, {"isWebViewOverlaysEnabled", "()Z", (void*)android_view_ThreadedRenderer_isWebViewOverlaysEnabled}, + {"nSetDrawingEnabled", "(Z)V", (void*)android_view_ThreadedRenderer_setDrawingEnabled}, + {"nIsDrawingEnabled", "()Z", (void*)android_view_ThreadedRenderer_isDrawingEnabled}, + {"nSetRtAnimationsEnabled", "(Z)V", + (void*)android_view_ThreadedRenderer_setRtAnimationsEnabled}, }; static JavaVM* mJvm = nullptr; @@ -1014,8 +1020,9 @@ int register_android_view_ThreadedRenderer(JNIEnv* env) { jclass frameCallbackClass = FindClassOrDie(env, "android/graphics/HardwareRenderer$FrameDrawingCallback"); - gFrameDrawingCallback.onFrameDraw = GetMethodIDOrDie(env, frameCallbackClass, - "onFrameDraw", "(J)V"); + gFrameDrawingCallback.onFrameDraw = + GetMethodIDOrDie(env, frameCallbackClass, "onFrameDraw", + "(IJ)Landroid/graphics/HardwareRenderer$FrameCommitCallback;"); jclass frameCommitClass = FindClassOrDie(env, "android/graphics/HardwareRenderer$FrameCommitCallback"); diff --git a/libs/hwui/jni/android_graphics_HardwareRendererObserver.cpp b/libs/hwui/jni/android_graphics_HardwareRendererObserver.cpp index e5d5e75d0f3b..6cae5ffa397f 100644 --- a/libs/hwui/jni/android_graphics_HardwareRendererObserver.cpp +++ b/libs/hwui/jni/android_graphics_HardwareRendererObserver.cpp @@ -24,6 +24,7 @@ namespace android { struct { + jclass clazz; jmethodID callback; } gHardwareRendererObserverClassInfo; @@ -38,14 +39,13 @@ static JNIEnv* getenv(JavaVM* vm) { HardwareRendererObserver::HardwareRendererObserver(JavaVM* vm, jobject observer, bool waitForPresentTime) : uirenderer::FrameMetricsObserver(waitForPresentTime), mVm(vm) { - mObserverWeak = getenv(mVm)->NewWeakGlobalRef(observer); - LOG_ALWAYS_FATAL_IF(mObserverWeak == nullptr, - "unable to create frame stats observer reference"); + mObserver = getenv(mVm)->NewGlobalRef(observer); + LOG_ALWAYS_FATAL_IF(mObserver == nullptr, "unable to create frame stats observer reference"); } HardwareRendererObserver::~HardwareRendererObserver() { JNIEnv* env = getenv(mVm); - env->DeleteWeakGlobalRef(mObserverWeak); + env->DeleteGlobalRef(mObserver); } bool HardwareRendererObserver::getNextBuffer(JNIEnv* env, jlongArray metrics, int* dropCount) { @@ -66,6 +66,8 @@ bool HardwareRendererObserver::getNextBuffer(JNIEnv* env, jlongArray metrics, in } void HardwareRendererObserver::notify(const int64_t* stats) { + if (!mKeepListening) return; + FrameMetricsNotification& elem = mRingBuffer[mNextFree]; if (!elem.hasData.load()) { @@ -77,18 +79,17 @@ void HardwareRendererObserver::notify(const int64_t* stats) { elem.hasData = true; JNIEnv* env = getenv(mVm); - jobject target = env->NewLocalRef(mObserverWeak); - if (target != nullptr) { - env->CallVoidMethod(target, gHardwareRendererObserverClassInfo.callback); - env->DeleteLocalRef(target); - } + mKeepListening = env->CallStaticBooleanMethod(gHardwareRendererObserverClassInfo.clazz, + gHardwareRendererObserverClassInfo.callback, + mObserver); } else { mDroppedReports++; } } static jlong android_graphics_HardwareRendererObserver_createObserver(JNIEnv* env, - jobject observerObj, + jobject /*clazz*/, + jobject weakRefThis, jboolean waitForPresentTime) { JavaVM* vm = nullptr; if (env->GetJavaVM(&vm) != JNI_OK) { @@ -97,7 +98,7 @@ static jlong android_graphics_HardwareRendererObserver_createObserver(JNIEnv* en } HardwareRendererObserver* observer = - new HardwareRendererObserver(vm, observerObj, waitForPresentTime); + new HardwareRendererObserver(vm, weakRefThis, waitForPresentTime); return reinterpret_cast<jlong>(observer); } @@ -114,7 +115,7 @@ static jint android_graphics_HardwareRendererObserver_getNextBuffer(JNIEnv* env, } static const std::array gMethods = { - MAKE_JNI_NATIVE_METHOD("nCreateObserver", "(Z)J", + MAKE_JNI_NATIVE_METHOD("nCreateObserver", "(Ljava/lang/ref/WeakReference;Z)J", android_graphics_HardwareRendererObserver_createObserver), MAKE_JNI_NATIVE_METHOD("nGetNextBuffer", "(J[J)I", android_graphics_HardwareRendererObserver_getNextBuffer), @@ -123,8 +124,10 @@ static const std::array gMethods = { int register_android_graphics_HardwareRendererObserver(JNIEnv* env) { jclass observerClass = FindClassOrDie(env, "android/graphics/HardwareRendererObserver"); - gHardwareRendererObserverClassInfo.callback = GetMethodIDOrDie(env, observerClass, - "notifyDataAvailable", "()V"); + gHardwareRendererObserverClassInfo.clazz = + reinterpret_cast<jclass>(env->NewGlobalRef(observerClass)); + gHardwareRendererObserverClassInfo.callback = GetStaticMethodIDOrDie( + env, observerClass, "invokeDataAvailable", "(Ljava/lang/ref/WeakReference;)Z"); return RegisterMethodsOrDie(env, "android/graphics/HardwareRendererObserver", gMethods.data(), gMethods.size()); diff --git a/libs/hwui/jni/android_graphics_HardwareRendererObserver.h b/libs/hwui/jni/android_graphics_HardwareRendererObserver.h index d3076140541b..5ee3e1669502 100644 --- a/libs/hwui/jni/android_graphics_HardwareRendererObserver.h +++ b/libs/hwui/jni/android_graphics_HardwareRendererObserver.h @@ -63,7 +63,8 @@ private: }; JavaVM* const mVm; - jweak mObserverWeak; + jobject mObserver; + bool mKeepListening = true; int mNextFree = 0; int mNextInQueue = 0; diff --git a/libs/hwui/jni/android_graphics_RenderNode.cpp b/libs/hwui/jni/android_graphics_RenderNode.cpp index e1da1690518a..db7639029187 100644 --- a/libs/hwui/jni/android_graphics_RenderNode.cpp +++ b/libs/hwui/jni/android_graphics_RenderNode.cpp @@ -543,13 +543,22 @@ static void android_view_RenderNode_endAllAnimators(JNIEnv* env, jobject clazz, renderNode->animators().endAllStagingAnimators(); } +static void android_view_RenderNode_forceEndAnimators(JNIEnv* env, jobject clazz, + jlong renderNodePtr) { + RenderNode* renderNode = reinterpret_cast<RenderNode*>(renderNodePtr); + renderNode->animators().forceEndAnimators(); +} + // ---------------------------------------------------------------------------- // SurfaceView position callback // ---------------------------------------------------------------------------- -jmethodID gPositionListener_PositionChangedMethod; -jmethodID gPositionListener_ApplyStretchMethod; -jmethodID gPositionListener_PositionLostMethod; +struct { + jclass clazz; + jmethodID callPositionChanged; + jmethodID callApplyStretch; + jmethodID callPositionLost; +} gPositionListener; static void android_view_RenderNode_requestPositionUpdates(JNIEnv* env, jobject, jlong renderNodePtr, jobject listener) { @@ -557,16 +566,16 @@ static void android_view_RenderNode_requestPositionUpdates(JNIEnv* env, jobject, public: PositionListenerTrampoline(JNIEnv* env, jobject listener) { env->GetJavaVM(&mVm); - mWeakRef = env->NewWeakGlobalRef(listener); + mListener = env->NewGlobalRef(listener); } virtual ~PositionListenerTrampoline() { - jnienv()->DeleteWeakGlobalRef(mWeakRef); - mWeakRef = nullptr; + jnienv()->DeleteGlobalRef(mListener); + mListener = nullptr; } virtual void onPositionUpdated(RenderNode& node, const TreeInfo& info) override { - if (CC_UNLIKELY(!mWeakRef || !info.updateWindowPositions)) return; + if (CC_UNLIKELY(!mListener || !info.updateWindowPositions)) return; Matrix4 transform; info.damageAccumulator->computeCurrentTransform(&transform); @@ -609,7 +618,7 @@ static void android_view_RenderNode_requestPositionUpdates(JNIEnv* env, jobject, } virtual void onPositionLost(RenderNode& node, const TreeInfo* info) override { - if (CC_UNLIKELY(!mWeakRef || (info && !info->updateWindowPositions))) return; + if (CC_UNLIKELY(!mListener || (info && !info->updateWindowPositions))) return; if (mPreviousPosition.isEmpty()) { return; @@ -618,18 +627,16 @@ static void android_view_RenderNode_requestPositionUpdates(JNIEnv* env, jobject, ATRACE_NAME("SurfaceView position lost"); JNIEnv* env = jnienv(); - jobject localref = env->NewLocalRef(mWeakRef); - if (CC_UNLIKELY(!localref)) { - env->DeleteWeakGlobalRef(mWeakRef); - mWeakRef = nullptr; - return; - } #ifdef __ANDROID__ // Layoutlib does not support CanvasContext // TODO: Remember why this is synchronous and then make a comment - env->CallVoidMethod(localref, gPositionListener_PositionLostMethod, + jboolean keepListening = env->CallStaticBooleanMethod( + gPositionListener.clazz, gPositionListener.callPositionLost, mListener, info ? info->canvasContext.getFrameNumber() : 0); + if (!keepListening) { + env->DeleteGlobalRef(mListener); + mListener = nullptr; + } #endif - env->DeleteLocalRef(localref); } private: @@ -684,28 +691,20 @@ static void android_view_RenderNode_requestPositionUpdates(JNIEnv* env, jobject, StretchEffectBehavior::Shader) { JNIEnv* env = jnienv(); - jobject localref = env->NewLocalRef(mWeakRef); - if (CC_UNLIKELY(!localref)) { - env->DeleteWeakGlobalRef(mWeakRef); - mWeakRef = nullptr; - return; - } #ifdef __ANDROID__ // Layoutlib does not support CanvasContext SkVector stretchDirection = effect->getStretchDirection(); - env->CallVoidMethod(localref, gPositionListener_ApplyStretchMethod, - info.canvasContext.getFrameNumber(), - result.width, - result.height, - stretchDirection.fX, - stretchDirection.fY, - effect->maxStretchAmountX, - effect->maxStretchAmountY, - childRelativeBounds.left(), - childRelativeBounds.top(), - childRelativeBounds.right(), - childRelativeBounds.bottom()); + jboolean keepListening = env->CallStaticBooleanMethod( + gPositionListener.clazz, gPositionListener.callApplyStretch, mListener, + info.canvasContext.getFrameNumber(), result.width, result.height, + stretchDirection.fX, stretchDirection.fY, effect->maxStretchAmountX, + effect->maxStretchAmountY, childRelativeBounds.left(), + childRelativeBounds.top(), childRelativeBounds.right(), + childRelativeBounds.bottom()); + if (!keepListening) { + env->DeleteGlobalRef(mListener); + mListener = nullptr; + } #endif - env->DeleteLocalRef(localref); } } @@ -714,14 +713,12 @@ static void android_view_RenderNode_requestPositionUpdates(JNIEnv* env, jobject, ATRACE_NAME("Update SurfaceView position"); JNIEnv* env = jnienv(); - jobject localref = env->NewLocalRef(mWeakRef); - if (CC_UNLIKELY(!localref)) { - env->DeleteWeakGlobalRef(mWeakRef); - mWeakRef = nullptr; - } else { - env->CallVoidMethod(localref, gPositionListener_PositionChangedMethod, - frameNumber, left, top, right, bottom); - env->DeleteLocalRef(localref); + 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 @@ -729,7 +726,7 @@ static void android_view_RenderNode_requestPositionUpdates(JNIEnv* env, jobject, } JavaVM* mVm; - jobject mWeakRef; + jobject mListener; uirenderer::Rect mPreviousPosition; }; @@ -754,7 +751,8 @@ static const JNINativeMethod gMethods[] = { {"nGetAllocatedSize", "(J)I", (void*)android_view_RenderNode_getAllocatedSize}, {"nAddAnimator", "(JJ)V", (void*)android_view_RenderNode_addAnimator}, {"nEndAllAnimators", "(J)V", (void*)android_view_RenderNode_endAllAnimators}, - {"nRequestPositionUpdates", "(JLandroid/graphics/RenderNode$PositionUpdateListener;)V", + {"nForceEndAnimators", "(J)V", (void*)android_view_RenderNode_forceEndAnimators}, + {"nRequestPositionUpdates", "(JLjava/lang/ref/WeakReference;)V", (void*)android_view_RenderNode_requestPositionUpdates}, // ---------------------------------------------------------------------------- @@ -852,12 +850,13 @@ static const JNINativeMethod gMethods[] = { int register_android_view_RenderNode(JNIEnv* env) { jclass clazz = FindClassOrDie(env, "android/graphics/RenderNode$PositionUpdateListener"); - gPositionListener_PositionChangedMethod = GetMethodIDOrDie(env, clazz, - "positionChanged", "(JIIII)V"); - gPositionListener_ApplyStretchMethod = - GetMethodIDOrDie(env, clazz, "applyStretch", "(JFFFFFFFFFF)V"); - gPositionListener_PositionLostMethod = GetMethodIDOrDie(env, clazz, - "positionLost", "(J)V"); + gPositionListener.clazz = MakeGlobalRefOrDie(env, clazz); + gPositionListener.callPositionChanged = GetStaticMethodIDOrDie( + env, clazz, "callPositionChanged", "(Ljava/lang/ref/WeakReference;JIIII)Z"); + gPositionListener.callApplyStretch = GetStaticMethodIDOrDie( + env, clazz, "callApplyStretch", "(Ljava/lang/ref/WeakReference;JFFFFFFFFFF)Z"); + gPositionListener.callPositionLost = GetStaticMethodIDOrDie( + env, clazz, "callPositionLost", "(Ljava/lang/ref/WeakReference;J)Z"); return RegisterMethodsOrDie(env, kClassPathName, gMethods, NELEM(gMethods)); } diff --git a/libs/hwui/jni/android_util_PathParser.cpp b/libs/hwui/jni/android_util_PathParser.cpp index 72995efb1c21..8cbb70ed2c86 100644 --- a/libs/hwui/jni/android_util_PathParser.cpp +++ b/libs/hwui/jni/android_util_PathParser.cpp @@ -61,7 +61,7 @@ static jlong createPathDataFromStringPath(JNIEnv* env, jobject, jstring inputStr } else { delete pathData; doThrowIAE(env, result.failureMessage.c_str()); - return NULL; + return 0; } } diff --git a/libs/hwui/jni/text/MeasuredText.cpp b/libs/hwui/jni/text/MeasuredText.cpp index 7793746ee285..c13c800651ef 100644 --- a/libs/hwui/jni/text/MeasuredText.cpp +++ b/libs/hwui/jni/text/MeasuredText.cpp @@ -65,11 +65,13 @@ static jlong nInitBuilder(CRITICAL_JNI_PARAMS) { // Regular JNI static void nAddStyleRun(JNIEnv* /* unused */, jclass /* unused */, jlong builderPtr, - jlong paintPtr, jint start, jint end, jboolean isRtl) { + jlong paintPtr, jint lbStyle, jint lbWordStyle, jint start, jint end, + jboolean isRtl) { Paint* paint = toPaint(paintPtr); const Typeface* typeface = Typeface::resolveDefault(paint->getAndroidTypeface()); minikin::MinikinPaint minikinPaint = MinikinUtils::prepareMinikinPaint(paint, typeface); - toBuilder(builderPtr)->addStyleRun(start, end, std::move(minikinPaint), isRtl); + toBuilder(builderPtr) + ->addStyleRun(start, end, std::move(minikinPaint), lbStyle, lbWordStyle, isRtl); } // Regular JNI @@ -80,15 +82,17 @@ static void nAddReplacementRun(JNIEnv* /* unused */, jclass /* unused */, jlong } // Regular JNI -static jlong nBuildMeasuredText(JNIEnv* env, jclass /* unused */, jlong builderPtr, - jlong hintPtr, jcharArray javaText, jboolean computeHyphenation, - jboolean computeLayout) { +static jlong nBuildMeasuredText(JNIEnv* env, jclass /* unused */, jlong builderPtr, jlong hintPtr, + jcharArray javaText, jboolean computeHyphenation, + jboolean computeLayout, jboolean fastHyphenationMode) { ScopedCharArrayRO text(env, javaText); const minikin::U16StringPiece textBuffer(text.get(), text.size()); // Pass the ownership to Java. - return toJLong(toBuilder(builderPtr)->build(textBuffer, computeHyphenation, computeLayout, - toMeasuredParagraph(hintPtr)).release()); + return toJLong(toBuilder(builderPtr) + ->build(textBuffer, computeHyphenation, computeLayout, + fastHyphenationMode, toMeasuredParagraph(hintPtr)) + .release()); } // Regular JNI @@ -130,6 +134,21 @@ static void nGetBounds(JNIEnv* env, jobject, jlong ptr, jcharArray javaText, jin GraphicsJNI::irect_to_jrect(ir, env, bounds); } +// Regular JNI +static jlong nGetExtent(JNIEnv* env, jobject, jlong ptr, jcharArray javaText, jint start, + jint end) { + ScopedCharArrayRO text(env, javaText); + const minikin::U16StringPiece textBuffer(text.get(), text.size()); + const minikin::Range range(start, end); + + minikin::MinikinExtent extent = toMeasuredParagraph(ptr)->getExtent(textBuffer, range); + + int32_t ascent = SkScalarRoundToInt(extent.ascent); + int32_t descent = SkScalarRoundToInt(extent.descent); + + return (((jlong)(ascent)) << 32) | ((jlong)descent); +} + // CriticalNative static jlong nGetReleaseFunc(CRITICAL_JNI_PARAMS) { return toJLong(&releaseMeasuredParagraph); @@ -140,21 +159,22 @@ static jint nGetMemoryUsage(CRITICAL_JNI_PARAMS_COMMA jlong ptr) { } static const JNINativeMethod gMTBuilderMethods[] = { - // MeasuredParagraphBuilder native functions. - {"nInitBuilder", "()J", (void*) nInitBuilder}, - {"nAddStyleRun", "(JJIIZ)V", (void*) nAddStyleRun}, - {"nAddReplacementRun", "(JJIIF)V", (void*) nAddReplacementRun}, - {"nBuildMeasuredText", "(JJ[CZZ)J", (void*) nBuildMeasuredText}, - {"nFreeBuilder", "(J)V", (void*) nFreeBuilder}, + // MeasuredParagraphBuilder native functions. + {"nInitBuilder", "()J", (void*)nInitBuilder}, + {"nAddStyleRun", "(JJIIIIZ)V", (void*)nAddStyleRun}, + {"nAddReplacementRun", "(JJIIF)V", (void*)nAddReplacementRun}, + {"nBuildMeasuredText", "(JJ[CZZZ)J", (void*)nBuildMeasuredText}, + {"nFreeBuilder", "(J)V", (void*)nFreeBuilder}, }; static const JNINativeMethod gMTMethods[] = { - // MeasuredParagraph native functions. - {"nGetWidth", "(JII)F", (void*) nGetWidth}, // Critical Natives - {"nGetBounds", "(J[CIILandroid/graphics/Rect;)V", (void*) nGetBounds}, // Regular JNI - {"nGetReleaseFunc", "()J", (void*) nGetReleaseFunc}, // Critical Natives - {"nGetMemoryUsage", "(J)I", (void*) nGetMemoryUsage}, // Critical Native - {"nGetCharWidthAt", "(JI)F", (void*) nGetCharWidthAt}, // Critical Native + // MeasuredParagraph native functions. + {"nGetWidth", "(JII)F", (void*)nGetWidth}, // Critical Natives + {"nGetBounds", "(J[CIILandroid/graphics/Rect;)V", (void*)nGetBounds}, // Regular JNI + {"nGetExtent", "(J[CII)J", (void*)nGetExtent}, // Regular JNI + {"nGetReleaseFunc", "()J", (void*)nGetReleaseFunc}, // Critical Natives + {"nGetMemoryUsage", "(J)I", (void*)nGetMemoryUsage}, // Critical Native + {"nGetCharWidthAt", "(JI)F", (void*)nGetCharWidthAt}, // Critical Native }; int register_android_graphics_text_MeasuredText(JNIEnv* env) { diff --git a/libs/hwui/jni/text/TextShaper.cpp b/libs/hwui/jni/text/TextShaper.cpp index a6fb95832c03..8e4dd53069f4 100644 --- a/libs/hwui/jni/text/TextShaper.cpp +++ b/libs/hwui/jni/text/TextShaper.cpp @@ -160,7 +160,6 @@ static jlong TextShaper_Result_nReleaseFunc(CRITICAL_JNI_PARAMS) { } static const JNINativeMethod gMethods[] = { - // Fast Natives {"nativeShapeTextRun", "(" "[C" // text "I" // start diff --git a/libs/hwui/libhwui.map.txt b/libs/hwui/libhwui.map.txt index 77b8a44d85a1..fdb237387098 100644 --- a/libs/hwui/libhwui.map.txt +++ b/libs/hwui/libhwui.map.txt @@ -1,4 +1,4 @@ -LIBHWUI { +LIBHWUI { # platform-only /* HWUI isn't current a module, so all of these are still platform-only */ global: /* listing of all C APIs to be exposed by libhwui to consumers outside of the module */ ABitmap_getInfoFromJava; @@ -39,7 +39,7 @@ LIBHWUI { ARegionIterator_next; ARegionIterator_getRect; ARegionIterator_getTotalBounds; - ARenderThread_dumpGraphicsMemory; + hwui_uses_vulkan; local: *; }; diff --git a/libs/hwui/pipeline/skia/AnimatedDrawables.h b/libs/hwui/pipeline/skia/AnimatedDrawables.h index d173782fd880..9cf93e66cfbe 100644 --- a/libs/hwui/pipeline/skia/AnimatedDrawables.h +++ b/libs/hwui/pipeline/skia/AnimatedDrawables.h @@ -110,7 +110,7 @@ public: const float rotation3 = turbulencePhase * PI_ROTATE_RIGHT + 2.75 * PI; setUniform2f(effectBuilder, "in_tRotation3", cos(rotation3), sin(rotation3)); - params.paint->value.setShader(effectBuilder.makeShader(nullptr, false)); + params.paint->value.setShader(effectBuilder.makeShader()); canvas->drawCircle(params.x->value, params.y->value, params.radius->value, params.paint->value); } diff --git a/libs/hwui/pipeline/skia/DumpOpsCanvas.h b/libs/hwui/pipeline/skia/DumpOpsCanvas.h index 3580bed45a1f..3f89c0712407 100644 --- a/libs/hwui/pipeline/skia/DumpOpsCanvas.h +++ b/libs/hwui/pipeline/skia/DumpOpsCanvas.h @@ -52,6 +52,8 @@ protected: mOutput << mIdent << "clipRegion" << std::endl; } + void onResetClip() override { mOutput << mIdent << "resetClip" << std::endl; } + void onDrawPaint(const SkPaint&) override { mOutput << mIdent << "drawPaint" << std::endl; } void onDrawPath(const SkPath&, const SkPaint&) override { diff --git a/libs/hwui/pipeline/skia/GLFunctorDrawable.cpp b/libs/hwui/pipeline/skia/GLFunctorDrawable.cpp index ab00dd5a487c..dc72aead4873 100644 --- a/libs/hwui/pipeline/skia/GLFunctorDrawable.cpp +++ b/libs/hwui/pipeline/skia/GLFunctorDrawable.cpp @@ -61,6 +61,17 @@ void GLFunctorDrawable::onDraw(SkCanvas* canvas) { return; } + // canvas may be an AlphaFilterCanvas, which is intended to draw with a + // modified alpha. We do not have a way to do this without drawing into an + // extra layer, which would have a performance cost. Draw directly into the + // underlying gpu canvas. This matches prior behavior and the behavior in + // Vulkan. + { + auto* gpuCanvas = SkAndroidFrameworkUtils::getBaseWrappedCanvas(canvas); + LOG_ALWAYS_FATAL_IF(!gpuCanvas, "GLFunctorDrawable::onDraw is using an invalid canvas!"); + canvas = gpuCanvas; + } + // flush will create a GrRenderTarget if not already present. canvas->flush(); diff --git a/libs/hwui/pipeline/skia/LayerDrawable.cpp b/libs/hwui/pipeline/skia/LayerDrawable.cpp index 471a7f7af3b1..2fba13c3cfea 100644 --- a/libs/hwui/pipeline/skia/LayerDrawable.cpp +++ b/libs/hwui/pipeline/skia/LayerDrawable.cpp @@ -15,12 +15,20 @@ */ #include "LayerDrawable.h" + +#include <shaders/shaders.h> +#include <utils/Color.h> #include <utils/MathUtils.h> +#include "DeviceInfo.h" #include "GrBackendSurface.h" #include "SkColorFilter.h" +#include "SkRuntimeEffect.h" #include "SkSurface.h" #include "gl/GrGLTypes.h" +#include "math/mat4.h" +#include "system/graphics-base-v1.0.h" +#include "system/window.h" namespace android { namespace uirenderer { @@ -29,7 +37,8 @@ namespace skiapipeline { void LayerDrawable::onDraw(SkCanvas* canvas) { Layer* layer = mLayerUpdater->backingLayer(); if (layer) { - DrawLayer(canvas->recordingContext(), canvas, layer, nullptr, nullptr, true); + SkRect srcRect = layer->getCurrentCropRect(); + DrawLayer(canvas->recordingContext(), canvas, layer, &srcRect, nullptr, true); } } @@ -67,6 +76,37 @@ 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()); + } + + 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()); + } + + 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; +} + // TODO: Context arg probably doesn't belong here – do debug check at callsite instead. bool LayerDrawable::DrawLayer(GrRecordingContext* context, SkCanvas* canvas, @@ -75,89 +115,108 @@ bool LayerDrawable::DrawLayer(GrRecordingContext* context, const SkRect* dstRect, bool useLayerTransform) { if (context == nullptr) { - SkDEBUGF(("Attempting to draw LayerDrawable into an unsupported surface")); + ALOGD("Attempting to draw LayerDrawable into an unsupported surface"); return false; } // transform the matrix based on the layer - SkMatrix layerTransform = layer->getTransform(); + // SkMatrix layerTransform = layer->getTransform(); + const uint32_t windowTransform = layer->getWindowTransform(); sk_sp<SkImage> layerImage = layer->getImage(); const int layerWidth = layer->getWidth(); const int layerHeight = layer->getHeight(); if (layerImage) { - SkMatrix textureMatrixInv; - textureMatrixInv = layer->getTexTransform(); - // TODO: after skia bug https://bugs.chromium.org/p/skia/issues/detail?id=7075 is fixed - // use bottom left origin and remove flipV and invert transformations. - SkMatrix flipV; - flipV.setAll(1, 0, 0, 0, -1, 1, 0, 0, 1); - textureMatrixInv.preConcat(flipV); - textureMatrixInv.preScale(1.0f / layerWidth, 1.0f / layerHeight); - textureMatrixInv.postScale(layerImage->width(), layerImage->height()); - SkMatrix textureMatrix; - if (!textureMatrixInv.invert(&textureMatrix)) { - textureMatrix = textureMatrixInv; - } + const int imageWidth = layerImage->width(); + const int imageHeight = layerImage->height(); - SkMatrix matrix; if (useLayerTransform) { - matrix = SkMatrix::Concat(layerTransform, textureMatrix); - } else { - matrix = textureMatrix; + canvas->save(); + canvas->concat(layer->getTransform()); } SkPaint paint; paint.setAlpha(layer->getAlpha()); paint.setBlendMode(layer->getMode()); paint.setColorFilter(layer->getColorFilter()); - const bool nonIdentityMatrix = !matrix.isIdentity(); - if (nonIdentityMatrix) { - canvas->save(); - canvas->concat(matrix); - } const SkMatrix& totalMatrix = canvas->getTotalMatrix(); - if (dstRect || srcRect) { - SkMatrix matrixInv; - if (!matrix.invert(&matrixInv)) { - matrixInv = matrix; - } - SkRect skiaSrcRect; - if (srcRect) { - skiaSrcRect = *srcRect; - } else { - skiaSrcRect = SkRect::MakeIWH(layerWidth, layerHeight); - } - matrixInv.mapRect(&skiaSrcRect); - SkRect skiaDestRect; - if (dstRect) { - skiaDestRect = *dstRect; - } else { - skiaDestRect = SkRect::MakeIWH(layerWidth, layerHeight); - } - matrixInv.mapRect(&skiaDestRect); - // If (matrix is a rect-to-rect transform) - // and (src/dst buffers size match in screen coordinates) - // and (src/dst corners align fractionally), - // then use nearest neighbor, otherwise use bilerp sampling. - // Skia TextureOp has the above logic build-in, but not NonAAFillRectOp. TextureOp works - // only for SrcOver blending and without color filter (readback uses Src blending). - SkSamplingOptions sampling(SkFilterMode::kNearest); - if (layer->getForceFilter() || - shouldFilterRect(totalMatrix, skiaSrcRect, skiaDestRect)) { - sampling = SkSamplingOptions(SkFilterMode::kLinear); - } - canvas->drawImageRect(layerImage.get(), skiaSrcRect, skiaDestRect, sampling, &paint, - SkCanvas::kFast_SrcRectConstraint); + SkRect skiaSrcRect; + if (srcRect && !srcRect->isEmpty()) { + skiaSrcRect = *srcRect; + } else { + skiaSrcRect = SkRect::MakeIWH(imageWidth, imageHeight); + } + SkRect skiaDestRect; + if (dstRect && !dstRect->isEmpty()) { + skiaDestRect = (windowTransform & NATIVE_WINDOW_TRANSFORM_ROT_90) + ? SkRect::MakeIWH(dstRect->height(), dstRect->width()) + : SkRect::MakeIWH(dstRect->width(), dstRect->height()); + } else { + skiaDestRect = (windowTransform & NATIVE_WINDOW_TRANSFORM_ROT_90) + ? SkRect::MakeIWH(layerHeight, layerWidth) + : SkRect::MakeIWH(layerWidth, layerHeight); + } + + const float px = skiaDestRect.centerX(); + const float py = skiaDestRect.centerY(); + SkMatrix m; + if (windowTransform & NATIVE_WINDOW_TRANSFORM_FLIP_H) { + m.postScale(-1.f, 1.f, px, py); + } + if (windowTransform & NATIVE_WINDOW_TRANSFORM_FLIP_V) { + m.postScale(1.f, -1.f, px, py); + } + if (windowTransform & NATIVE_WINDOW_TRANSFORM_ROT_90) { + m.postRotate(90, 0, 0); + m.postTranslate(skiaDestRect.height(), 0); + } + auto constraint = SkCanvas::kFast_SrcRectConstraint; + if (srcRect && !srcRect->isEmpty()) { + constraint = SkCanvas::kStrict_SrcRectConstraint; + } + + canvas->save(); + canvas->concat(m); + + // If (matrix is a rect-to-rect transform) + // and (src/dst buffers size match in screen coordinates) + // and (src/dst corners align fractionally), + // then use nearest neighbor, otherwise use bilerp sampling. + // Skia TextureOp has the above logic build-in, but not NonAAFillRectOp. TextureOp works + // only for SrcOver blending and without color filter (readback uses Src blending). + SkSamplingOptions sampling(SkFilterMode::kNearest); + if (layer->getForceFilter() || shouldFilterRect(totalMatrix, skiaSrcRect, skiaDestRect)) { + 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 { - SkRect imageRect = SkRect::MakeIWH(layerImage->width(), layerImage->height()); - SkSamplingOptions sampling(SkFilterMode::kNearest); - if (layer->getForceFilter() || shouldFilterRect(totalMatrix, imageRect, imageRect)) { - sampling = SkSamplingOptions(SkFilterMode::kLinear); - } - canvas->drawImage(layerImage.get(), 0, 0, sampling, &paint); + canvas->drawImageRect(layerImage.get(), skiaSrcRect, skiaDestRect, sampling, &paint, + constraint); } + + canvas->restore(); // restore the original matrix - if (nonIdentityMatrix) { + if (useLayerTransform) { canvas->restore(); } } diff --git a/libs/hwui/pipeline/skia/RenderNodeDrawable.cpp b/libs/hwui/pipeline/skia/RenderNodeDrawable.cpp index 48145d2331ee..507d3dcdcde9 100644 --- a/libs/hwui/pipeline/skia/RenderNodeDrawable.cpp +++ b/libs/hwui/pipeline/skia/RenderNodeDrawable.cpp @@ -88,6 +88,10 @@ static void clipOutline(const Outline& outline, SkCanvas* canvas, const SkRect* if (pendingClip) { canvas->clipRect(*pendingClip); } + const SkPath* path = outline.getPath(); + if (path) { + canvas->clipPath(*path, SkClipOp::kIntersect, true); + } return; } diff --git a/libs/hwui/pipeline/skia/ShaderCache.cpp b/libs/hwui/pipeline/skia/ShaderCache.cpp index e7432ac5f216..90c4440c8339 100644 --- a/libs/hwui/pipeline/skia/ShaderCache.cpp +++ b/libs/hwui/pipeline/skia/ShaderCache.cpp @@ -136,24 +136,59 @@ sk_sp<SkData> ShaderCache::load(const SkData& key) { free(valueBuffer); return nullptr; } + mNumShadersCachedInRam++; + ATRACE_FORMAT("HWUI RAM cache: %d shaders", mNumShadersCachedInRam); return SkData::MakeFromMalloc(valueBuffer, valueSize); } +namespace { +// Helper for BlobCache::set to trace the result. +void set(BlobCache* cache, const void* key, size_t keySize, const void* value, size_t valueSize) { + switch (cache->set(key, keySize, value, valueSize)) { + case BlobCache::InsertResult::kInserted: + // This is what we expect/hope. It means the cache is large enough. + return; + case BlobCache::InsertResult::kDidClean: { + ATRACE_FORMAT("ShaderCache: evicted an entry to fit {key: %lu value %lu}!", keySize, + valueSize); + return; + } + case BlobCache::InsertResult::kNotEnoughSpace: { + ATRACE_FORMAT("ShaderCache: could not fit {key: %lu value %lu}!", keySize, valueSize); + return; + } + case BlobCache::InsertResult::kInvalidValueSize: + case BlobCache::InsertResult::kInvalidKeySize: { + ATRACE_FORMAT("ShaderCache: invalid size {key: %lu value %lu}!", keySize, valueSize); + return; + } + case BlobCache::InsertResult::kKeyTooBig: + case BlobCache::InsertResult::kValueTooBig: + case BlobCache::InsertResult::kCombinedTooBig: { + ATRACE_FORMAT("ShaderCache: entry too big: {key: %lu value %lu}!", keySize, valueSize); + return; + } + } +} +} // namespace + void ShaderCache::saveToDiskLocked() { ATRACE_NAME("ShaderCache::saveToDiskLocked"); if (mInitialized && mBlobCache && mSavePending) { if (mIDHash.size()) { auto key = sIDKey; - mBlobCache->set(&key, sizeof(key), mIDHash.data(), mIDHash.size()); + set(mBlobCache.get(), &key, sizeof(key), mIDHash.data(), mIDHash.size()); } mBlobCache->writeToFile(); } mSavePending = false; } -void ShaderCache::store(const SkData& key, const SkData& data) { +void ShaderCache::store(const SkData& key, const SkData& data, const SkString& /*description*/) { ATRACE_NAME("ShaderCache::store"); std::lock_guard<std::mutex> lock(mMutex); + mNumShadersCachedInRam++; + ATRACE_FORMAT("HWUI RAM cache: %d shaders", mNumShadersCachedInRam); if (!mInitialized) { return; @@ -187,7 +222,7 @@ void ShaderCache::store(const SkData& key, const SkData& data) { mNewPipelineCacheSize = -1; mTryToStorePipelineCache = true; } - bc->set(key.data(), keySize, value, valueSize); + set(bc, key.data(), keySize, value, valueSize); if (!mSavePending && mDeferredSaveDelay > 0) { mSavePending = true; diff --git a/libs/hwui/pipeline/skia/ShaderCache.h b/libs/hwui/pipeline/skia/ShaderCache.h index 4dcc9fb49802..3e0fd5164011 100644 --- a/libs/hwui/pipeline/skia/ShaderCache.h +++ b/libs/hwui/pipeline/skia/ShaderCache.h @@ -73,7 +73,7 @@ public: * "store" attempts to insert a new key/value blob pair into the cache. * This will be called by Skia after it compiled a new SKSL shader */ - void store(const SkData& key, const SkData& data) override; + void store(const SkData& key, const SkData& data, const SkString& description) override; /** * "onVkFrameFlushed" tries to store Vulkan pipeline cache state. @@ -210,6 +210,13 @@ private: */ static constexpr uint8_t sIDKey = 0; + /** + * Most of this class concerns persistent storage for shaders, but it's also + * interesting to keep track of how many shaders are stored in RAM. This + * class provides a convenient entry point for that. + */ + int mNumShadersCachedInRam = 0; + friend class ShaderCacheTestUtils; // used for unit testing }; diff --git a/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp b/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp index 9bca4df577c9..2aca41e41905 100644 --- a/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp +++ b/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp @@ -16,8 +16,15 @@ #include "SkiaOpenGLPipeline.h" +#include <GrBackendSurface.h> +#include <SkBlendMode.h> +#include <SkImageInfo.h> +#include <cutils/properties.h> #include <gui/TraceUtils.h> +#include <strings.h> + #include "DeferredLayerUpdater.h" +#include "FrameInfo.h" #include "LayerDrawable.h" #include "LightingInfo.h" #include "SkiaPipeline.h" @@ -27,17 +34,9 @@ #include "renderstate/RenderState.h" #include "renderthread/EglManager.h" #include "renderthread/Frame.h" +#include "renderthread/IRenderPipeline.h" #include "utils/GLUtils.h" -#include <GLES3/gl3.h> - -#include <GrBackendSurface.h> -#include <SkBlendMode.h> -#include <SkImageInfo.h> - -#include <cutils/properties.h> -#include <strings.h> - using namespace android::uirenderer::renderthread; namespace android { @@ -69,12 +68,11 @@ Frame SkiaOpenGLPipeline::getFrame() { return mEglManager.beginFrame(mEglSurface); } -bool 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) { +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()) { mEglManager.damageFrame(frame, dirty); } @@ -91,6 +89,8 @@ bool SkiaOpenGLPipeline::draw(const Frame& frame, const SkRect& screenDirty, con fboInfo.fFormat = GL_RGBA8; } else if (colorType == kRGBA_1010102_SkColorType) { fboInfo.fFormat = GL_RGB10_A2; + } else if (colorType == kAlpha_8_SkColorType) { + fboInfo.fFormat = GL_R8; } else { LOG_ALWAYS_FATAL("Unsupported color type."); } @@ -127,7 +127,7 @@ bool SkiaOpenGLPipeline::draw(const Frame& frame, const SkRect& screenDirty, con dumpResourceCacheUsage(); } - return true; + return {true, IRenderPipeline::DrawResult::kUnknownTime}; } bool SkiaOpenGLPipeline::swapBuffers(const Frame& frame, bool drew, const SkRect& screenDirty, diff --git a/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.h b/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.h index fddd97f1c5b3..186998a01745 100644 --- a/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.h +++ b/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.h @@ -36,11 +36,14 @@ public: renderthread::MakeCurrentResult makeCurrent() override; renderthread::Frame getFrame() override; - bool 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) override; GrSurfaceOrigin getSurfaceOrigin() override { return kBottomLeft_GrSurfaceOrigin; } bool swapBuffers(const renderthread::Frame& frame, bool drew, const SkRect& screenDirty, FrameInfo* currentFrameInfo, bool* requireSwap) override; diff --git a/libs/hwui/pipeline/skia/SkiaPipeline.cpp b/libs/hwui/pipeline/skia/SkiaPipeline.cpp index 4e7471d5d888..bc386feb2d6f 100644 --- a/libs/hwui/pipeline/skia/SkiaPipeline.cpp +++ b/libs/hwui/pipeline/skia/SkiaPipeline.cpp @@ -613,6 +613,10 @@ void SkiaPipeline::setSurfaceColorProperties(ColorMode colorMode) { mSurfaceColorType = SkColorType::kRGBA_1010102_SkColorType; mSurfaceColorSpace = SkColorSpace::MakeRGB(GetPQSkTransferFunction(), SkNamedGamut::kRec2020); break; + case ColorMode::A8: + mSurfaceColorType = SkColorType::kAlpha_8_SkColorType; + mSurfaceColorSpace = nullptr; + break; } } diff --git a/libs/hwui/pipeline/skia/SkiaRecordingCanvas.cpp b/libs/hwui/pipeline/skia/SkiaRecordingCanvas.cpp index 76c4a03d3a91..9c51e628e04a 100644 --- a/libs/hwui/pipeline/skia/SkiaRecordingCanvas.cpp +++ b/libs/hwui/pipeline/skia/SkiaRecordingCanvas.cpp @@ -187,28 +187,18 @@ void SkiaRecordingCanvas::drawVectorDrawable(VectorDrawableRoot* tree) { void SkiaRecordingCanvas::FilterForImage(SkPaint& paint) { // kClear blend mode is drawn as kDstOut on HW for compatibility with Android O and // older. - if (sApiLevel <= 27 && paint.getBlendMode() == SkBlendMode::kClear) { + if (sApiLevel <= 27 && paint.asBlendMode() == SkBlendMode::kClear) { paint.setBlendMode(SkBlendMode::kDstOut); } } -static SkFilterMode Paint_to_filter(const SkPaint& paint) { - return paint.getFilterQuality() != kNone_SkFilterQuality ? SkFilterMode::kLinear - : SkFilterMode::kNearest; -} - -static SkSamplingOptions Paint_to_sampling(const SkPaint& paint) { - // Android only has 1-bit for "filter", so we don't try to cons-up mipmaps or cubics - return SkSamplingOptions(Paint_to_filter(paint), SkMipmapMode::kNone); -} - void SkiaRecordingCanvas::drawBitmap(Bitmap& bitmap, float left, float top, const Paint* paint) { sk_sp<SkImage> image = bitmap.makeImage(); applyLooper( paint, - [&](const SkPaint& p) { - mRecorder.drawImage(image, left, top, Paint_to_sampling(p), &p, bitmap.palette()); + [&](const Paint& p) { + mRecorder.drawImage(image, left, top, p.sampling(), &p, bitmap.palette()); }, FilterForImage); @@ -228,8 +218,8 @@ void SkiaRecordingCanvas::drawBitmap(Bitmap& bitmap, const SkMatrix& matrix, con applyLooper( paint, - [&](const SkPaint& p) { - mRecorder.drawImage(image, 0, 0, Paint_to_sampling(p), &p, bitmap.palette()); + [&](const Paint& p) { + mRecorder.drawImage(image, 0, 0, p.sampling(), &p, bitmap.palette()); }, FilterForImage); @@ -248,8 +238,8 @@ void SkiaRecordingCanvas::drawBitmap(Bitmap& bitmap, float srcLeft, float srcTop applyLooper( paint, - [&](const SkPaint& p) { - mRecorder.drawImageRect(image, srcRect, dstRect, Paint_to_sampling(p), &p, + [&](const Paint& p) { + mRecorder.drawImageRect(image, srcRect, dstRect, p.sampling(), &p, SkCanvas::kFast_SrcRectConstraint, bitmap.palette()); }, FilterForImage); diff --git a/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp b/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp index 99fd463b0660..905d46e58014 100644 --- a/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp +++ b/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp @@ -16,7 +16,15 @@ #include "SkiaVulkanPipeline.h" +#include <GrDirectContext.h> +#include <GrTypes.h> +#include <SkSurface.h> +#include <SkTypes.h> +#include <cutils/properties.h> #include <gui/TraceUtils.h> +#include <strings.h> +#include <vk/GrVkTypes.h> + #include "DeferredLayerUpdater.h" #include "LightingInfo.h" #include "Readback.h" @@ -26,16 +34,7 @@ #include "VkInteropFunctorDrawable.h" #include "renderstate/RenderState.h" #include "renderthread/Frame.h" - -#include <SkSurface.h> -#include <SkTypes.h> - -#include <GrDirectContext.h> -#include <GrTypes.h> -#include <vk/GrVkTypes.h> - -#include <cutils/properties.h> -#include <strings.h> +#include "renderthread/IRenderPipeline.h" using namespace android::uirenderer::renderthread; @@ -64,15 +63,14 @@ Frame SkiaVulkanPipeline::getFrame() { return vulkanManager().dequeueNextBuffer(mVkSurface); } -bool 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) { +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(); if (backBuffer.get() == nullptr) { - return false; + return {false, -1}; } // update the coordinates of the global light position based on surface rotation @@ -94,9 +92,10 @@ bool SkiaVulkanPipeline::draw(const Frame& frame, const SkRect& screenDirty, con profiler->draw(profileRenderer); } + nsecs_t submissionTime = IRenderPipeline::DrawResult::kUnknownTime; { ATRACE_NAME("flush commands"); - vulkanManager().finishFrame(backBuffer.get()); + submissionTime = vulkanManager().finishFrame(backBuffer.get()); } layerUpdateQueue->clear(); @@ -105,7 +104,7 @@ bool SkiaVulkanPipeline::draw(const Frame& frame, const SkRect& screenDirty, con dumpResourceCacheUsage(); } - return true; + return {true, submissionTime}; } bool SkiaVulkanPipeline::swapBuffers(const Frame& frame, bool drew, const SkRect& screenDirty, diff --git a/libs/hwui/pipeline/skia/SkiaVulkanPipeline.h b/libs/hwui/pipeline/skia/SkiaVulkanPipeline.h index 56d42e013f31..ada6af67d4a0 100644 --- a/libs/hwui/pipeline/skia/SkiaVulkanPipeline.h +++ b/libs/hwui/pipeline/skia/SkiaVulkanPipeline.h @@ -33,11 +33,14 @@ public: renderthread::MakeCurrentResult makeCurrent() override; renderthread::Frame getFrame() override; - bool 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) override; GrSurfaceOrigin getSurfaceOrigin() override { return kTopLeft_GrSurfaceOrigin; } bool swapBuffers(const renderthread::Frame& frame, bool drew, const SkRect& screenDirty, FrameInfo* currentFrameInfo, bool* requireSwap) override; diff --git a/libs/hwui/pipeline/skia/VkFunctorDrawable.cpp b/libs/hwui/pipeline/skia/VkFunctorDrawable.cpp index 8abf4534a04c..e6ef95b9cf91 100644 --- a/libs/hwui/pipeline/skia/VkFunctorDrawable.cpp +++ b/libs/hwui/pipeline/skia/VkFunctorDrawable.cpp @@ -72,6 +72,7 @@ void VkFunctorDrawHandler::draw(const GrBackendDrawableInfo& info) { .clip_top = mClip.fTop, .clip_right = mClip.fRight, .clip_bottom = mClip.fBottom, + .is_layer = !vulkan_info.fFromSwapchainOrAndroidWindow, }; mat4.getColMajor(¶ms.transform[0]); params.secondary_command_buffer = vulkan_info.fSecondaryCommandBuffer; diff --git a/libs/hwui/pipeline/skia/VkInteropFunctorDrawable.cpp b/libs/hwui/pipeline/skia/VkInteropFunctorDrawable.cpp index ddfb66f84f28..3c7617d35c7c 100644 --- a/libs/hwui/pipeline/skia/VkInteropFunctorDrawable.cpp +++ b/libs/hwui/pipeline/skia/VkInteropFunctorDrawable.cpp @@ -68,7 +68,7 @@ void VkInteropFunctorDrawable::onDraw(SkCanvas* canvas) { ATRACE_CALL(); if (canvas->recordingContext() == nullptr) { - SkDEBUGF(("Attempting to draw VkInteropFunctor into an unsupported surface")); + ALOGD("Attempting to draw VkInteropFunctor into an unsupported surface"); return; } diff --git a/libs/hwui/private/hwui/DrawVkInfo.h b/libs/hwui/private/hwui/DrawVkInfo.h index 4ae0f5a0a2e5..5c596576df4e 100644 --- a/libs/hwui/private/hwui/DrawVkInfo.h +++ b/libs/hwui/private/hwui/DrawVkInfo.h @@ -68,6 +68,9 @@ struct VkFunctorDrawParams { int clip_top; int clip_right; int clip_bottom; + + // Input: Whether destination surface is offscreen surface. + bool is_layer; }; } // namespace uirenderer diff --git a/libs/hwui/renderthread/CanvasContext.cpp b/libs/hwui/renderthread/CanvasContext.cpp index a066e6f7c693..976117b9bbd4 100644 --- a/libs/hwui/renderthread/CanvasContext.cpp +++ b/libs/hwui/renderthread/CanvasContext.cpp @@ -18,15 +18,16 @@ #include <apex/window.h> #include <fcntl.h> +#include <gui/TraceUtils.h> #include <strings.h> #include <sys/stat.h> +#include <ui/Fence.h> #include <algorithm> #include <cstdint> #include <cstdlib> #include <functional> -#include <gui/TraceUtils.h> #include "../Properties.h" #include "AnimationContext.h" #include "Frame.h" @@ -203,9 +204,10 @@ void CanvasContext::setSurfaceControl(ASurfaceControl* surfaceControl) { mSurfaceControl = surfaceControl; mSurfaceControlGenerationId++; mExpectSurfaceStats = surfaceControl != nullptr; - if (mSurfaceControl != nullptr) { + if (mExpectSurfaceStats) { funcs.acquireFunc(mSurfaceControl); - funcs.registerListenerFunc(surfaceControl, this, &onSurfaceStatsAvailable); + funcs.registerListenerFunc(surfaceControl, mSurfaceControlGenerationId, this, + &onSurfaceStatsAvailable); } } @@ -218,7 +220,7 @@ void CanvasContext::setupPipelineSurface() { } - mFrameNumber = -1; + mFrameNumber = 0; if (mNativeSurface != nullptr && hasSurface) { mHaveNewSurface = true; @@ -256,7 +258,7 @@ void CanvasContext::setStopped(bool stopped) { } void CanvasContext::allocateBuffers() { - if (mNativeSurface) { + if (mNativeSurface && Properties::isDrawingEnabled()) { ANativeWindow_tryAllocateBuffers(mNativeSurface->getNativeWindow()); } } @@ -388,7 +390,7 @@ void CanvasContext::prepareTree(TreeInfo& info, int64_t* uiFrameInfo, int64_t sy return; } - if (CC_LIKELY(mSwapHistory.size() && !Properties::forceDrawFrame)) { + if (CC_LIKELY(mSwapHistory.size() && !info.forceDrawFrame)) { nsecs_t latestVsync = mRenderThread.timeLord().latestVsync(); SwapHistory& lastSwap = mSwapHistory.back(); nsecs_t vsyncDelta = std::abs(lastSwap.vsyncTime - latestVsync); @@ -480,7 +482,8 @@ nsecs_t CanvasContext::draw() { SkRect dirty; mDamageAccumulator.finish(&dirty); - if (dirty.isEmpty() && Properties::skipEmptyFrames && !surfaceRequiresRedraw()) { + if (!Properties::isDrawingEnabled() || + (dirty.isEmpty() && Properties::skipEmptyFrames && !surfaceRequiresRedraw())) { mCurrentFrameInfo->addFlag(FrameInfoFlags::SkippedFrame); if (auto grContext = getGrContext()) { // Submit to ensure that any texture uploads complete and Skia can @@ -507,11 +510,13 @@ nsecs_t CanvasContext::draw() { Frame frame = mRenderPipeline->getFrame(); SkRect windowDirty = computeDirtyRect(frame, &dirty); - bool drew = mRenderPipeline->draw(frame, windowDirty, dirty, mLightGeometry, &mLayerUpdateQueue, - mContentDrawBounds, mOpaque, mLightInfo, mRenderNodes, - &(profiler())); + ATRACE_FORMAT("Drawing " RECT_STRING, SK_RECT_ARGS(dirty)); + + const auto drawResult = mRenderPipeline->draw(frame, windowDirty, dirty, mLightGeometry, + &mLayerUpdateQueue, mContentDrawBounds, mOpaque, + mLightInfo, mRenderNodes, &(profiler())); - int64_t frameCompleteNr = getFrameNumber(); + uint64_t frameCompleteNr = getFrameNumber(); waitOnFences(); @@ -521,15 +526,19 @@ nsecs_t CanvasContext::draw() { if (vsyncId != UiFrameInfoBuilder::INVALID_VSYNC_ID) { const auto inputEventId = static_cast<int32_t>(mCurrentFrameInfo->get(FrameInfoIndex::InputEventId)); - native_window_set_frame_timeline_info(mNativeSurface->getNativeWindow(), vsyncId, - inputEventId); + native_window_set_frame_timeline_info( + mNativeSurface->getNativeWindow(), vsyncId, inputEventId, + mCurrentFrameInfo->get(FrameInfoIndex::FrameStartTime)); } } bool requireSwap = false; int error = OK; - bool didSwap = - mRenderPipeline->swapBuffers(frame, drew, windowDirty, mCurrentFrameInfo, &requireSwap); + bool didSwap = mRenderPipeline->swapBuffers(frame, drawResult.success, windowDirty, + mCurrentFrameInfo, &requireSwap); + + mCurrentFrameInfo->set(FrameInfoIndex::CommandSubmissionCompleted) = std::max( + drawResult.commandSubmissionTime, mCurrentFrameInfo->get(FrameInfoIndex::SwapBuffers)); mIsDirty = false; @@ -579,7 +588,7 @@ nsecs_t CanvasContext::draw() { mCurrentFrameInfo->set(FrameInfoIndex::DequeueBufferDuration) = swap.dequeueDuration; mCurrentFrameInfo->set(FrameInfoIndex::QueueBufferDuration) = swap.queueDuration; mHaveNewSurface = false; - mFrameNumber = -1; + mFrameNumber = 0; } else { mCurrentFrameInfo->set(FrameInfoIndex::DequeueBufferDuration) = 0; mCurrentFrameInfo->set(FrameInfoIndex::QueueBufferDuration) = 0; @@ -612,16 +621,20 @@ nsecs_t CanvasContext::draw() { if (requireSwap) { if (mExpectSurfaceStats) { reportMetricsWithPresentTime(); - std::lock_guard lock(mLast4FrameInfosMutex); - std::pair<FrameInfo*, int64_t>& next = mLast4FrameInfos.next(); - next.first = mCurrentFrameInfo; - next.second = frameCompleteNr; + { // acquire lock + std::lock_guard lock(mLast4FrameMetricsInfosMutex); + FrameMetricsInfo& next = mLast4FrameMetricsInfos.next(); + next.frameInfo = mCurrentFrameInfo; + next.frameNumber = frameCompleteNr; + next.surfaceId = mSurfaceControlGenerationId; + } // release lock } else { mCurrentFrameInfo->markFrameCompleted(); mCurrentFrameInfo->set(FrameInfoIndex::GpuCompleted) = mCurrentFrameInfo->get(FrameInfoIndex::FrameCompleted); std::scoped_lock lock(mFrameMetricsReporterMutex); - mJankTracker.finishFrame(*mCurrentFrameInfo, mFrameMetricsReporter); + mJankTracker.finishFrame(*mCurrentFrameInfo, mFrameMetricsReporter, frameCompleteNr, + mSurfaceControlGenerationId); } } @@ -657,14 +670,18 @@ void CanvasContext::reportMetricsWithPresentTime() { ATRACE_CALL(); FrameInfo* forthBehind; int64_t frameNumber; + int32_t surfaceControlId; + { // acquire lock - std::scoped_lock lock(mLast4FrameInfosMutex); - if (mLast4FrameInfos.size() != mLast4FrameInfos.capacity()) { + std::scoped_lock lock(mLast4FrameMetricsInfosMutex); + if (mLast4FrameMetricsInfos.size() != mLast4FrameMetricsInfos.capacity()) { // Not enough frames yet return; } - // Surface object keeps stats for the last 8 frames. - std::tie(forthBehind, frameNumber) = mLast4FrameInfos.front(); + auto frameMetricsInfo = mLast4FrameMetricsInfos.front(); + forthBehind = frameMetricsInfo.frameInfo; + frameNumber = frameMetricsInfo.frameNumber; + surfaceControlId = frameMetricsInfo.surfaceId; } // release lock nsecs_t presentTime = 0; @@ -679,40 +696,71 @@ void CanvasContext::reportMetricsWithPresentTime() { { // acquire lock std::scoped_lock lock(mFrameMetricsReporterMutex); if (mFrameMetricsReporter != nullptr) { - mFrameMetricsReporter->reportFrameMetrics(forthBehind->data(), true /*hasPresentTime*/); + mFrameMetricsReporter->reportFrameMetrics(forthBehind->data(), true /*hasPresentTime*/, + frameNumber, surfaceControlId); } } // release lock } -FrameInfo* CanvasContext::getFrameInfoFromLast4(uint64_t frameNumber) { - std::scoped_lock lock(mLast4FrameInfosMutex); - for (size_t i = 0; i < mLast4FrameInfos.size(); i++) { - if (mLast4FrameInfos[i].second == frameNumber) { - return mLast4FrameInfos[i].first; +void CanvasContext::addFrameMetricsObserver(FrameMetricsObserver* observer) { + std::scoped_lock lock(mFrameMetricsReporterMutex); + if (mFrameMetricsReporter.get() == nullptr) { + mFrameMetricsReporter.reset(new FrameMetricsReporter()); + } + + // We want to make sure we aren't reporting frames that have already been queued by the + // BufferQueueProducer on the rendner thread but are still pending the callback to report their + // their frame metrics. + uint64_t nextFrameNumber = getFrameNumber(); + observer->reportMetricsFrom(nextFrameNumber, mSurfaceControlGenerationId); + mFrameMetricsReporter->addObserver(observer); +} + +void CanvasContext::removeFrameMetricsObserver(FrameMetricsObserver* observer) { + std::scoped_lock lock(mFrameMetricsReporterMutex); + if (mFrameMetricsReporter.get() != nullptr) { + mFrameMetricsReporter->removeObserver(observer); + if (!mFrameMetricsReporter->hasObservers()) { + mFrameMetricsReporter.reset(nullptr); } } - return nullptr; } -void CanvasContext::onSurfaceStatsAvailable(void* context, ASurfaceControl* control, - ASurfaceControlStats* stats) { +FrameInfo* CanvasContext::getFrameInfoFromLast4(uint64_t frameNumber, uint32_t surfaceControlId) { + std::scoped_lock lock(mLast4FrameMetricsInfosMutex); + for (size_t i = 0; i < mLast4FrameMetricsInfos.size(); i++) { + if (mLast4FrameMetricsInfos[i].frameNumber == frameNumber && + mLast4FrameMetricsInfos[i].surfaceId == surfaceControlId) { + return mLast4FrameMetricsInfos[i].frameInfo; + } + } - CanvasContext* instance = static_cast<CanvasContext*>(context); + return nullptr; +} + +void CanvasContext::onSurfaceStatsAvailable(void* context, int32_t surfaceControlId, + ASurfaceControlStats* stats) { + auto* instance = static_cast<CanvasContext*>(context); const ASurfaceControlFunctions& functions = instance->mRenderThread.getASurfaceControlFunctions(); nsecs_t gpuCompleteTime = functions.getAcquireTimeFunc(stats); + if (gpuCompleteTime == Fence::SIGNAL_TIME_PENDING) { + gpuCompleteTime = -1; + } uint64_t frameNumber = functions.getFrameNumberFunc(stats); - FrameInfo* frameInfo = instance->getFrameInfoFromLast4(frameNumber); + FrameInfo* frameInfo = instance->getFrameInfoFromLast4(frameNumber, surfaceControlId); if (frameInfo != nullptr) { frameInfo->set(FrameInfoIndex::FrameCompleted) = std::max(gpuCompleteTime, frameInfo->get(FrameInfoIndex::SwapBuffersCompleted)); - frameInfo->set(FrameInfoIndex::GpuCompleted) = gpuCompleteTime; + frameInfo->set(FrameInfoIndex::GpuCompleted) = std::max( + gpuCompleteTime, frameInfo->get(FrameInfoIndex::CommandSubmissionCompleted)); std::scoped_lock lock(instance->mFrameMetricsReporterMutex); - instance->mJankTracker.finishFrame(*frameInfo, instance->mFrameMetricsReporter); + instance->mJankTracker.finishFrame(*frameInfo, instance->mFrameMetricsReporter, frameNumber, + surfaceControlId); } } @@ -853,9 +901,9 @@ void CanvasContext::enqueueFrameWork(std::function<void()>&& func) { mFrameFences.push_back(CommonPool::async(std::move(func))); } -int64_t CanvasContext::getFrameNumber() { - // mFrameNumber is reset to -1 when the surface changes or we swap buffers - if (mFrameNumber == -1 && mNativeSurface.get()) { +uint64_t CanvasContext::getFrameNumber() { + // mFrameNumber is reset to 0 when the surface changes or we swap buffers + if (mFrameNumber == 0 && mNativeSurface.get()) { mFrameNumber = ANativeWindow_getNextFrameId(mNativeSurface->getNativeWindow()); } return mFrameNumber; diff --git a/libs/hwui/renderthread/CanvasContext.h b/libs/hwui/renderthread/CanvasContext.h index 9df429badd5e..951ee216ce35 100644 --- a/libs/hwui/renderthread/CanvasContext.h +++ b/libs/hwui/renderthread/CanvasContext.h @@ -90,9 +90,17 @@ public: * and false otherwise (e.g. cache limits have been exceeded). */ bool pinImages(std::vector<SkImage*>& mutableImages) { + if (!Properties::isDrawingEnabled()) { + return true; + } return mRenderPipeline->pinImages(mutableImages); } - bool pinImages(LsaVector<sk_sp<Bitmap>>& images) { return mRenderPipeline->pinImages(images); } + bool pinImages(LsaVector<sk_sp<Bitmap>>& images) { + if (!Properties::isDrawingEnabled()) { + return true; + } + return mRenderPipeline->pinImages(images); + } /** * Unpin any image that had be previously pinned to the GPU cache @@ -159,29 +167,13 @@ public: void setContentDrawBounds(const Rect& bounds) { mContentDrawBounds = bounds; } - void addFrameMetricsObserver(FrameMetricsObserver* observer) { - std::scoped_lock lock(mFrameMetricsReporterMutex); - if (mFrameMetricsReporter.get() == nullptr) { - mFrameMetricsReporter.reset(new FrameMetricsReporter()); - } - - mFrameMetricsReporter->addObserver(observer); - } - - void removeFrameMetricsObserver(FrameMetricsObserver* observer) { - std::scoped_lock lock(mFrameMetricsReporterMutex); - if (mFrameMetricsReporter.get() != nullptr) { - mFrameMetricsReporter->removeObserver(observer); - if (!mFrameMetricsReporter->hasObservers()) { - mFrameMetricsReporter.reset(nullptr); - } - } - } + void addFrameMetricsObserver(FrameMetricsObserver* observer); + void removeFrameMetricsObserver(FrameMetricsObserver* observer); // Used to queue up work that needs to be completed before this frame completes void enqueueFrameWork(std::function<void()>&& func); - int64_t getFrameNumber(); + uint64_t getFrameNumber(); void waitOnFences(); @@ -204,8 +196,8 @@ public: SkISize getNextFrameSize() const; // Called when SurfaceStats are available. - static void onSurfaceStatsAvailable(void* context, ASurfaceControl* control, - ASurfaceControlStats* stats); + static void onSurfaceStatsAvailable(void* context, int32_t surfaceControlId, + ASurfaceControlStats* stats); void setASurfaceTransactionCallback( const std::function<bool(int64_t, int64_t, int64_t)>& callback) { @@ -246,7 +238,13 @@ private: */ void reportMetricsWithPresentTime(); - FrameInfo* getFrameInfoFromLast4(uint64_t frameNumber); + struct FrameMetricsInfo { + FrameInfo* frameInfo; + int64_t frameNumber; + int32_t surfaceId; + }; + + FrameInfo* getFrameInfoFromLast4(uint64_t frameNumber, uint32_t surfaceControlId); // The same type as Frame.mWidth and Frame.mHeight int32_t mLastFrameWidth = 0; @@ -258,7 +256,10 @@ private: // NULL to remove the reference ASurfaceControl* mSurfaceControl = nullptr; // id to track surface control changes and WebViewFunctor uses it to determine - // whether reparenting is needed + // whether reparenting is needed also used by FrameMetricsReporter to determine + // if a frame is from an "old" surface (i.e. one that existed before the + // observer was attched) and therefore shouldn't be reported. + // NOTE: It is important that this is an increasing counter. int32_t mSurfaceControlGenerationId = 0; // stopped indicates the CanvasContext will reject actual redraw operations, // and defer repaint until it is un-stopped @@ -278,9 +279,10 @@ private: nsecs_t queueDuration; }; - // Need at least 4 because we do quad buffer. Add a 5th for good measure. - RingBuffer<SwapHistory, 5> mSwapHistory; - int64_t mFrameNumber = -1; + // Need at least 4 because we do quad buffer. Add a few more for good measure. + RingBuffer<SwapHistory, 7> mSwapHistory; + // Frame numbers start at 1, 0 means uninitialized + uint64_t mFrameNumber = 0; int64_t mDamageId = 0; // last vsync for a dropped frame due to stuffed queue @@ -300,10 +302,11 @@ private: FrameInfo* mCurrentFrameInfo = nullptr; - // List of frames that are awaiting GPU completion reporting - RingBuffer<std::pair<FrameInfo*, int64_t>, 4> mLast4FrameInfos - GUARDED_BY(mLast4FrameInfosMutex); - std::mutex mLast4FrameInfosMutex; + // List of data of frames that are awaiting GPU completion reporting. Used to compute frame + // metrics and determine whether or not to report the metrics. + RingBuffer<FrameMetricsInfo, 4> mLast4FrameMetricsInfos + GUARDED_BY(mLast4FrameMetricsInfosMutex); + std::mutex mLast4FrameMetricsInfosMutex; std::string mName; JankTracker mJankTracker; diff --git a/libs/hwui/renderthread/DrawFrameTask.cpp b/libs/hwui/renderthread/DrawFrameTask.cpp index 94aedd0f43be..59c914f0198c 100644 --- a/libs/hwui/renderthread/DrawFrameTask.cpp +++ b/libs/hwui/renderthread/DrawFrameTask.cpp @@ -133,6 +133,7 @@ int DrawFrameTask::drawFrame() { } void DrawFrameTask::postAndWait() { + ATRACE_CALL(); AutoMutex _lock(mLock); mRenderThread->queue().post([this]() { run(); }); mSignal.wait(mLock); @@ -147,6 +148,8 @@ void DrawFrameTask::run() { bool canDrawThisFrame; { TreeInfo info(TreeInfo::MODE_FULL, *mContext); + info.forceDrawFrame = mForceDrawFrame; + mForceDrawFrame = false; canUnblockUiThread = syncFrameState(info); canDrawThisFrame = info.out.canDrawThisFrame; @@ -158,7 +161,8 @@ void DrawFrameTask::run() { // Grab a copy of everything we need CanvasContext* context = mContext; - std::function<void(int64_t)> frameCallback = std::move(mFrameCallback); + std::function<std::function<void(bool)>(int32_t, int64_t)> frameCallback = + std::move(mFrameCallback); std::function<void()> frameCompleteCallback = std::move(mFrameCompleteCallback); mFrameCallback = nullptr; mFrameCompleteCallback = nullptr; @@ -173,8 +177,13 @@ void DrawFrameTask::run() { // Even if we aren't drawing this vsync pulse the next frame number will still be accurate if (CC_UNLIKELY(frameCallback)) { - context->enqueueFrameWork( - [frameCallback, frameNr = context->getFrameNumber()]() { frameCallback(frameNr); }); + context->enqueueFrameWork([frameCallback, context, syncResult = mSyncResult, + frameNr = context->getFrameNumber()]() { + auto frameCommitCallback = std::move(frameCallback(syncResult, frameNr)); + if (frameCommitCallback) { + context->addFrameCommitListener(std::move(frameCommitCallback)); + } + }); } nsecs_t dequeueBufferDuration = 0; diff --git a/libs/hwui/renderthread/DrawFrameTask.h b/libs/hwui/renderthread/DrawFrameTask.h index e3ea802b07b9..d6fc292d5900 100644 --- a/libs/hwui/renderthread/DrawFrameTask.h +++ b/libs/hwui/renderthread/DrawFrameTask.h @@ -16,19 +16,18 @@ #ifndef DRAWFRAMETASK_H #define DRAWFRAMETASK_H -#include <optional> -#include <vector> - -#include <performance_hint_private.h> +#include <android/performance_hint.h> #include <utils/Condition.h> #include <utils/Mutex.h> #include <utils/StrongPointer.h> -#include "RenderTask.h" +#include <optional> +#include <vector> #include "../FrameInfo.h" #include "../Rect.h" #include "../TreeInfo.h" +#include "RenderTask.h" namespace android { namespace uirenderer { @@ -77,7 +76,7 @@ public: void run(); - void setFrameCallback(std::function<void(int64_t)>&& callback) { + void setFrameCallback(std::function<std::function<void(bool)>(int32_t, int64_t)>&& callback) { mFrameCallback = std::move(callback); } @@ -89,6 +88,8 @@ public: mFrameCompleteCallback = std::move(callback); } + void forceDrawNextFrame() { mForceDrawFrame = true; } + private: class HintSessionWrapper { public: @@ -126,13 +127,15 @@ private: int64_t mFrameInfo[UI_THREAD_FRAME_INFO_SIZE]; - std::function<void(int64_t)> mFrameCallback; + 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; }; } /* namespace renderthread */ diff --git a/libs/hwui/renderthread/EglManager.cpp b/libs/hwui/renderthread/EglManager.cpp index c7d7a17a23eb..02257db9df6a 100644 --- a/libs/hwui/renderthread/EglManager.cpp +++ b/libs/hwui/renderthread/EglManager.cpp @@ -90,6 +90,7 @@ EglManager::EglManager() , mEglConfig(nullptr) , mEglConfigF16(nullptr) , mEglConfig1010102(nullptr) + , mEglConfigA8(nullptr) , mEglContext(EGL_NO_CONTEXT) , mPBufferSurface(EGL_NO_SURFACE) , mCurrentSurface(EGL_NO_SURFACE) @@ -246,6 +247,50 @@ EGLConfig EglManager::loadFP16Config(EGLDisplay display, SwapBehavior swapBehavi return config; } +EGLConfig EglManager::loadA8Config(EGLDisplay display, EglManager::SwapBehavior swapBehavior) { + EGLint eglSwapBehavior = + (swapBehavior == SwapBehavior::Preserved) ? EGL_SWAP_BEHAVIOR_PRESERVED_BIT : 0; + EGLint attribs[] = {EGL_RENDERABLE_TYPE, + EGL_OPENGL_ES2_BIT, + EGL_RED_SIZE, + 8, + EGL_GREEN_SIZE, + 0, + EGL_BLUE_SIZE, + 0, + EGL_ALPHA_SIZE, + 0, + EGL_DEPTH_SIZE, + 0, + EGL_SURFACE_TYPE, + EGL_WINDOW_BIT | eglSwapBehavior, + EGL_NONE}; + EGLint numConfigs = 1; + if (!eglChooseConfig(display, attribs, nullptr, numConfigs, &numConfigs)) { + return EGL_NO_CONFIG_KHR; + } + + std::vector<EGLConfig> configs(numConfigs, EGL_NO_CONFIG_KHR); + if (!eglChooseConfig(display, attribs, configs.data(), numConfigs, &numConfigs)) { + return EGL_NO_CONFIG_KHR; + } + + // The component sizes passed to eglChooseConfig are minimums, so configs + // contains entries that exceed them. Choose one that matches the sizes + // exactly. + for (EGLConfig config : configs) { + EGLint r{0}, g{0}, b{0}, a{0}; + eglGetConfigAttrib(display, config, EGL_RED_SIZE, &r); + eglGetConfigAttrib(display, config, EGL_GREEN_SIZE, &g); + eglGetConfigAttrib(display, config, EGL_BLUE_SIZE, &b); + eglGetConfigAttrib(display, config, EGL_ALPHA_SIZE, &a); + if (8 == r && 0 == g && 0 == b && 0 == a) { + return config; + } + } + return EGL_NO_CONFIG_KHR; +} + void EglManager::initExtensions() { auto extensions = StringUtils::split(eglQueryString(mEglDisplay, EGL_EXTENSIONS)); @@ -345,10 +390,14 @@ Result<EGLSurface, EGLint> EglManager::createSurface(EGLNativeWindowType window, sk_sp<SkColorSpace> colorSpace) { LOG_ALWAYS_FATAL_IF(!hasEglContext(), "Not initialized"); - if (!mHasWideColorGamutSupport || !EglExtensions.noConfigContext) { + if (!EglExtensions.noConfigContext) { + // The caller shouldn't use A8 if we cannot switch modes. + LOG_ALWAYS_FATAL_IF(colorMode == ColorMode::A8, + "Cannot use A8 without EGL_KHR_no_config_context!"); + + // Cannot switch modes without EGL_KHR_no_config_context. colorMode = ColorMode::Default; } - // The color space we want to use depends on whether linear blending is turned // on and whether the app has requested wide color gamut rendering. When wide // color gamut rendering is off, the app simply renders in the display's native @@ -374,42 +423,61 @@ Result<EGLSurface, EGLint> EglManager::createSurface(EGLNativeWindowType window, EGLint attribs[] = {EGL_NONE, EGL_NONE, EGL_NONE}; EGLConfig config = mEglConfig; - if (DeviceInfo::get()->getWideColorType() == kRGBA_F16_SkColorType) { - if (mEglConfigF16 == EGL_NO_CONFIG_KHR) { + if (colorMode == ColorMode::A8) { + // A8 doesn't use a color space + if (!mEglConfigA8) { + mEglConfigA8 = loadA8Config(mEglDisplay, mSwapBehavior); + LOG_ALWAYS_FATAL_IF(!mEglConfigA8, + "Requested ColorMode::A8, but EGL lacks support! error = %s", + eglErrorString()); + } + config = mEglConfigA8; + } else { + if (!mHasWideColorGamutSupport) { colorMode = ColorMode::Default; - } else { - config = mEglConfigF16; } - } - if (EglExtensions.glColorSpace) { - attribs[0] = EGL_GL_COLORSPACE_KHR; - switch (colorMode) { - case ColorMode::Default: - attribs[1] = EGL_GL_COLORSPACE_LINEAR_KHR; - break; - case ColorMode::WideColorGamut: { - skcms_Matrix3x3 colorGamut; - LOG_ALWAYS_FATAL_IF(!colorSpace->toXYZD50(&colorGamut), - "Could not get gamut matrix from color space"); - if (memcmp(&colorGamut, &SkNamedGamut::kDisplayP3, sizeof(colorGamut)) == 0) { - attribs[1] = EGL_GL_COLORSPACE_DISPLAY_P3_PASSTHROUGH_EXT; - } else if (memcmp(&colorGamut, &SkNamedGamut::kSRGB, sizeof(colorGamut)) == 0) { - attribs[1] = EGL_GL_COLORSPACE_SCRGB_EXT; - } else if (memcmp(&colorGamut, &SkNamedGamut::kRec2020, sizeof(colorGamut)) == 0) { - attribs[1] = EGL_GL_COLORSPACE_BT2020_PQ_EXT; - } else { - LOG_ALWAYS_FATAL("Unreachable: unsupported wide color space."); + + if (DeviceInfo::get()->getWideColorType() == kRGBA_F16_SkColorType) { + if (mEglConfigF16 == EGL_NO_CONFIG_KHR) { + colorMode = ColorMode::Default; + } else { + config = mEglConfigF16; + } + } + if (EglExtensions.glColorSpace) { + attribs[0] = EGL_GL_COLORSPACE_KHR; + switch (colorMode) { + case ColorMode::Default: + attribs[1] = EGL_GL_COLORSPACE_LINEAR_KHR; + break; + case ColorMode::WideColorGamut: { + skcms_Matrix3x3 colorGamut; + LOG_ALWAYS_FATAL_IF(!colorSpace->toXYZD50(&colorGamut), + "Could not get gamut matrix from color space"); + if (memcmp(&colorGamut, &SkNamedGamut::kDisplayP3, sizeof(colorGamut)) == 0) { + attribs[1] = EGL_GL_COLORSPACE_DISPLAY_P3_PASSTHROUGH_EXT; + } else if (memcmp(&colorGamut, &SkNamedGamut::kSRGB, sizeof(colorGamut)) == 0) { + attribs[1] = EGL_GL_COLORSPACE_SCRGB_EXT; + } else if (memcmp(&colorGamut, &SkNamedGamut::kRec2020, sizeof(colorGamut)) == + 0) { + attribs[1] = EGL_GL_COLORSPACE_BT2020_PQ_EXT; + } else { + LOG_ALWAYS_FATAL("Unreachable: unsupported wide color space."); + } + break; } - 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; } - 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; } } diff --git a/libs/hwui/renderthread/EglManager.h b/libs/hwui/renderthread/EglManager.h index 69f3ed014c53..fc6b28d2e1ad 100644 --- a/libs/hwui/renderthread/EglManager.h +++ b/libs/hwui/renderthread/EglManager.h @@ -89,6 +89,7 @@ private: static EGLConfig load8BitsConfig(EGLDisplay display, SwapBehavior swapBehavior); static EGLConfig loadFP16Config(EGLDisplay display, SwapBehavior swapBehavior); static EGLConfig load1010102Config(EGLDisplay display, SwapBehavior swapBehavior); + static EGLConfig loadA8Config(EGLDisplay display, SwapBehavior swapBehavior); void initExtensions(); void createPBufferSurface(); @@ -100,6 +101,7 @@ private: EGLConfig mEglConfig; EGLConfig mEglConfigF16; EGLConfig mEglConfig1010102; + EGLConfig mEglConfigA8; EGLContext mEglContext; EGLSurface mPBufferSurface; EGLSurface mCurrentSurface; diff --git a/libs/hwui/renderthread/IRenderPipeline.h b/libs/hwui/renderthread/IRenderPipeline.h index aceb5a528fc8..ef58bc553c23 100644 --- a/libs/hwui/renderthread/IRenderPipeline.h +++ b/libs/hwui/renderthread/IRenderPipeline.h @@ -49,11 +49,21 @@ class IRenderPipeline { public: virtual MakeCurrentResult makeCurrent() = 0; virtual Frame getFrame() = 0; - virtual bool 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) = 0; + + // Result of IRenderPipeline::draw + struct DrawResult { + // True if draw() succeeded, false otherwise + bool success = false; + // If drawing was successful, reports the time at which command + // submission occurred. -1 if this time is unknown. + static constexpr nsecs_t kUnknownTime = -1; + nsecs_t commandSubmissionTime = kUnknownTime; + }; + virtual DrawResult 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) = 0; virtual bool swapBuffers(const Frame& frame, bool drew, const SkRect& screenDirty, FrameInfo* currentFrameInfo, bool* requireSwap) = 0; virtual DeferredLayerUpdater* createTextureLayer() = 0; diff --git a/libs/hwui/renderthread/RenderProxy.cpp b/libs/hwui/renderthread/RenderProxy.cpp index 72d4ac5081e6..a44b498c81c1 100644 --- a/libs/hwui/renderthread/RenderProxy.cpp +++ b/libs/hwui/renderthread/RenderProxy.cpp @@ -133,6 +133,10 @@ int64_t* RenderProxy::frameInfo() { return mDrawFrameTask.frameInfo(); } +void RenderProxy::forceDrawNextFrame() { + mDrawFrameTask.forceDrawNextFrame(); +} + int RenderProxy::syncAndDrawFrame() { return mDrawFrameTask.drawFrame(); } @@ -257,10 +261,15 @@ uint32_t RenderProxy::frameTimePercentile(int percentile) { }); } -void RenderProxy::dumpGraphicsMemory(int fd, bool includeProfileData) { +void RenderProxy::dumpGraphicsMemory(int fd, bool includeProfileData, bool resetProfile) { if (RenderThread::hasInstance()) { auto& thread = RenderThread::getInstance(); - thread.queue().runSync([&]() { thread.dumpGraphicsMemory(fd, includeProfileData); }); + thread.queue().runSync([&]() { + thread.dumpGraphicsMemory(fd, includeProfileData); + if (resetProfile) { + thread.globalProfileData()->reset(); + } + }); } } @@ -322,7 +331,8 @@ void RenderProxy::setPrepareSurfaceControlForWebviewCallback( [this, cb = callback]() { mContext->setPrepareSurfaceControlForWebviewCallback(cb); }); } -void RenderProxy::setFrameCallback(std::function<void(int64_t)>&& callback) { +void RenderProxy::setFrameCallback( + std::function<std::function<void(bool)>(int32_t, int64_t)>&& callback) { mDrawFrameTask.setFrameCallback(std::move(callback)); } @@ -418,6 +428,15 @@ void RenderProxy::preload() { thread.queue().post([&thread]() { thread.preload(); }); } +void RenderProxy::setRtAnimationsEnabled(bool enabled) { + if (RenderThread::hasInstance()) { + RenderThread::getInstance().queue().post( + [enabled]() { Properties::enableRTAnimations = enabled; }); + } else { + Properties::enableRTAnimations = enabled; + } +} + } /* namespace renderthread */ } /* namespace uirenderer */ } /* namespace android */ diff --git a/libs/hwui/renderthread/RenderProxy.h b/libs/hwui/renderthread/RenderProxy.h index 6417b38df064..ee9efd46e307 100644 --- a/libs/hwui/renderthread/RenderProxy.h +++ b/libs/hwui/renderthread/RenderProxy.h @@ -82,6 +82,7 @@ public: void setOpaque(bool opaque); void setColorMode(ColorMode mode); int64_t* frameInfo(); + void forceDrawNextFrame(); int syncAndDrawFrame(); void destroy(); @@ -108,7 +109,8 @@ public: // Not exported, only used for testing void resetProfileInfo(); uint32_t frameTimePercentile(int p); - static void dumpGraphicsMemory(int fd, bool includeProfileData = true); + static void dumpGraphicsMemory(int fd, bool includeProfileData = true, + bool resetProfile = false); static void getMemoryUsage(size_t* cpuUsage, size_t* gpuUsage); static void rotateProcessStatsBuffer(); @@ -123,7 +125,7 @@ public: void setASurfaceTransactionCallback( const std::function<bool(int64_t, int64_t, int64_t)>& callback); void setPrepareSurfaceControlForWebviewCallback(const std::function<void()>& callback); - void setFrameCallback(std::function<void(int64_t)>&& callback); + void setFrameCallback(std::function<std::function<void(bool)>(int32_t, int64_t)>&& callback); void setFrameCommitCallback(std::function<void(bool)>&& callback); void setFrameCompleteCallback(std::function<void()>&& callback); @@ -142,6 +144,8 @@ public: static void preload(); + static void setRtAnimationsEnabled(bool enabled); + private: RenderThread& mRenderThread; CanvasContext* mContext; diff --git a/libs/hwui/renderthread/RenderThread.cpp b/libs/hwui/renderthread/RenderThread.cpp index f83c0a4926f9..01b956cb3dd5 100644 --- a/libs/hwui/renderthread/RenderThread.cpp +++ b/libs/hwui/renderthread/RenderThread.cpp @@ -16,8 +16,21 @@ #include "RenderThread.h" +#include <GrContextOptions.h> +#include <android-base/properties.h> +#include <dlfcn.h> +#include <gl/GrGLInterface.h> #include <gui/TraceUtils.h> +#include <sys/resource.h> +#include <ui/FatVector.h> +#include <utils/Condition.h> +#include <utils/Log.h> +#include <utils/Mutex.h> + +#include <thread> + #include "../HardwareBitmapUploader.h" +#include "CacheManager.h" #include "CanvasContext.h" #include "DeviceInfo.h" #include "EglManager.h" @@ -31,19 +44,6 @@ #include "renderstate/RenderState.h" #include "utils/TimeUtils.h" -#include <GrContextOptions.h> -#include <gl/GrGLInterface.h> - -#include <dlfcn.h> -#include <sys/resource.h> -#include <utils/Condition.h> -#include <utils/Log.h> -#include <utils/Mutex.h> -#include <thread> - -#include <android-base/properties.h> -#include <ui/FatVector.h> - namespace android { namespace uirenderer { namespace renderthread { @@ -112,18 +112,31 @@ ASurfaceControlFunctions::ASurfaceControlFunctions() { "Failed to find required symbol ASurfaceTransaction_setZOrder!"); } -void RenderThread::frameCallback(int64_t frameTimeNanos, void* data) { +void RenderThread::extendedFrameCallback(const AChoreographerFrameCallbackData* cbData, + void* data) { RenderThread* rt = reinterpret_cast<RenderThread*>(data); - int64_t vsyncId = AChoreographer_getVsyncId(rt->mChoreographer); - int64_t frameDeadline = AChoreographer_getFrameDeadline(rt->mChoreographer); + size_t preferredFrameTimelineIndex = + AChoreographerFrameCallbackData_getPreferredFrameTimelineIndex(cbData); + AVsyncId vsyncId = AChoreographerFrameCallbackData_getFrameTimelineVsyncId( + cbData, preferredFrameTimelineIndex); + int64_t frameDeadline = AChoreographerFrameCallbackData_getFrameTimelineDeadlineNanos( + cbData, preferredFrameTimelineIndex); + int64_t frameTimeNanos = AChoreographerFrameCallbackData_getFrameTimeNanos(cbData); + // TODO(b/193273294): Remove when shared memory in use w/ expected present time always current. int64_t frameInterval = AChoreographer_getFrameInterval(rt->mChoreographer); - rt->mVsyncRequested = false; - if (rt->timeLord().vsyncReceived(frameTimeNanos, frameTimeNanos, vsyncId, frameDeadline, - frameInterval) && !rt->mFrameCallbackTaskPending) { + rt->frameCallback(vsyncId, frameDeadline, frameTimeNanos, frameInterval); +} + +void RenderThread::frameCallback(int64_t vsyncId, int64_t frameDeadline, int64_t frameTimeNanos, + int64_t frameInterval) { + mVsyncRequested = false; + if (timeLord().vsyncReceived(frameTimeNanos, frameTimeNanos, vsyncId, frameDeadline, + frameInterval) && + !mFrameCallbackTaskPending) { ATRACE_NAME("queue mFrameCallbackTask"); - rt->mFrameCallbackTaskPending = true; - nsecs_t runAt = (frameTimeNanos + rt->mDispatchFrameDelay); - rt->queue().postAt(runAt, [=]() { rt->dispatchFrameCallbacks(); }); + mFrameCallbackTaskPending = true; + nsecs_t runAt = (frameTimeNanos + mDispatchFrameDelay); + queue().postAt(runAt, [=]() { dispatchFrameCallbacks(); }); } } @@ -139,8 +152,8 @@ public: ChoreographerSource(RenderThread* renderThread) : mRenderThread(renderThread) {} virtual void requestNextVsync() override { - AChoreographer_postFrameCallback64(mRenderThread->mChoreographer, - RenderThread::frameCallback, mRenderThread); + AChoreographer_postVsyncCallback(mRenderThread->mChoreographer, + RenderThread::extendedFrameCallback, mRenderThread); } virtual void drainPendingEvents() override { @@ -157,12 +170,16 @@ public: virtual void requestNextVsync() override { mRenderThread->queue().postDelayed(16_ms, [this]() { - RenderThread::frameCallback(systemTime(SYSTEM_TIME_MONOTONIC), mRenderThread); + mRenderThread->frameCallback(UiFrameInfoBuilder::INVALID_VSYNC_ID, + std::numeric_limits<int64_t>::max(), + systemTime(SYSTEM_TIME_MONOTONIC), 16_ms); }); } virtual void drainPendingEvents() override { - RenderThread::frameCallback(systemTime(SYSTEM_TIME_MONOTONIC), mRenderThread); + mRenderThread->frameCallback(UiFrameInfoBuilder::INVALID_VSYNC_ID, + std::numeric_limits<int64_t>::max(), + systemTime(SYSTEM_TIME_MONOTONIC), 16_ms); } private: diff --git a/libs/hwui/renderthread/RenderThread.h b/libs/hwui/renderthread/RenderThread.h index 05d225b856db..c1f6790b25b2 100644 --- a/libs/hwui/renderthread/RenderThread.h +++ b/libs/hwui/renderthread/RenderThread.h @@ -83,8 +83,9 @@ typedef ASurfaceControl* (*ASC_create)(ASurfaceControl* parent, const char* debu typedef void (*ASC_acquire)(ASurfaceControl* control); typedef void (*ASC_release)(ASurfaceControl* control); -typedef void (*ASC_registerSurfaceStatsListener)(ASurfaceControl* control, void* context, - ASurfaceControl_SurfaceStatsListener func); +typedef void (*ASC_registerSurfaceStatsListener)(ASurfaceControl* control, int32_t id, + void* context, + ASurfaceControl_SurfaceStatsListener func); typedef void (*ASC_unregisterSurfaceStatsListener)(void* context, ASurfaceControl_SurfaceStatsListener func); @@ -210,7 +211,9 @@ private: // corresponding callbacks for each display event type static int choreographerCallback(int fd, int events, void* data); // Callback that will be run on vsync ticks. - static void frameCallback(int64_t frameTimeNanos, void* data); + static void extendedFrameCallback(const AChoreographerFrameCallbackData* cbData, void* data); + void frameCallback(int64_t vsyncId, int64_t frameDeadline, int64_t frameTimeNanos, + int64_t frameInterval); // Callback that will be run whenver there is a refresh rate change. static void refreshRateCallback(int64_t vsyncPeriod, void* data); void drainDisplayEventQueue(); diff --git a/libs/hwui/renderthread/VulkanManager.cpp b/libs/hwui/renderthread/VulkanManager.cpp index 9e8a1e141fe1..718d4a16d5c8 100644 --- a/libs/hwui/renderthread/VulkanManager.cpp +++ b/libs/hwui/renderthread/VulkanManager.cpp @@ -35,6 +35,9 @@ #include "pipeline/skia/ShaderCache.h" #include "renderstate/RenderState.h" +#undef LOG_TAG +#define LOG_TAG "VulkanManager" + namespace android { namespace uirenderer { namespace renderthread { @@ -491,7 +494,7 @@ static void destroy_semaphore(void* context) { } } -void VulkanManager::finishFrame(SkSurface* surface) { +nsecs_t VulkanManager::finishFrame(SkSurface* surface) { ATRACE_NAME("Vulkan finish frame"); ALOGE_IF(mSwapSemaphore != VK_NULL_HANDLE || mDestroySemaphoreContext != nullptr, "finishFrame already has an outstanding semaphore"); @@ -527,6 +530,7 @@ void VulkanManager::finishFrame(SkSurface* surface) { GrDirectContext* context = GrAsDirectContext(surface->recordingContext()); ALOGE_IF(!context, "Surface is not backed by gpu"); context->submit(); + const nsecs_t submissionTime = systemTime(); if (semaphore != VK_NULL_HANDLE) { if (submitted == GrSemaphoresSubmitted::kYes) { mSwapSemaphore = semaphore; @@ -555,6 +559,8 @@ void VulkanManager::finishFrame(SkSurface* surface) { } } skiapipeline::ShaderCache::get().onVkFrameFlushed(context); + + return submissionTime; } void VulkanManager::swapBuffers(VulkanSurface* surface, const SkRect& dirtyRect) { diff --git a/libs/hwui/renderthread/VulkanManager.h b/libs/hwui/renderthread/VulkanManager.h index b816649edf6e..b8c2bdf112f8 100644 --- a/libs/hwui/renderthread/VulkanManager.h +++ b/libs/hwui/renderthread/VulkanManager.h @@ -84,7 +84,9 @@ public: void destroySurface(VulkanSurface* surface); Frame dequeueNextBuffer(VulkanSurface* surface); - void finishFrame(SkSurface* surface); + // Finishes the frame and submits work to the GPU + // Returns the estimated start time for intiating GPU work, -1 otherwise. + nsecs_t finishFrame(SkSurface* surface); void swapBuffers(VulkanSurface* surface, const SkRect& dirtyRect); // Inserts a wait on fence command into the Vulkan command buffer. diff --git a/libs/hwui/renderthread/VulkanSurface.cpp b/libs/hwui/renderthread/VulkanSurface.cpp index fe9a30a59870..7dd3561cb220 100644 --- a/libs/hwui/renderthread/VulkanSurface.cpp +++ b/libs/hwui/renderthread/VulkanSurface.cpp @@ -24,6 +24,9 @@ #include "VulkanManager.h" #include "utils/Color.h" +#undef LOG_TAG +#define LOG_TAG "VulkanSurface" + namespace android { namespace uirenderer { namespace renderthread { @@ -197,8 +200,9 @@ bool VulkanSurface::InitializeWindowInfoStruct(ANativeWindow* window, ColorMode outWindowInfo->bufferFormat = ColorTypeToBufferFormat(colorType); outWindowInfo->colorspace = colorSpace; outWindowInfo->dataspace = ColorSpaceToADataSpace(colorSpace.get(), colorType); - LOG_ALWAYS_FATAL_IF(outWindowInfo->dataspace == HAL_DATASPACE_UNKNOWN, - "Unsupported colorspace"); + LOG_ALWAYS_FATAL_IF( + outWindowInfo->dataspace == HAL_DATASPACE_UNKNOWN && colorType != kAlpha_8_SkColorType, + "Unsupported colorspace"); VkFormat vkPixelFormat; switch (colorType) { @@ -211,6 +215,9 @@ bool VulkanSurface::InitializeWindowInfoStruct(ANativeWindow* window, ColorMode case kRGBA_1010102_SkColorType: vkPixelFormat = VK_FORMAT_A2B10G10R10_UNORM_PACK32; break; + case kAlpha_8_SkColorType: + vkPixelFormat = VK_FORMAT_R8_UNORM; + break; default: LOG_ALWAYS_FATAL("Unsupported colorType: %d", (int)colorType); } @@ -426,7 +433,7 @@ VulkanSurface::NativeBufferInfo* VulkanSurface::dequeueNativeBuffer() { if (bufferInfo->skSurface.get() == nullptr) { bufferInfo->skSurface = SkSurface::MakeFromAHardwareBuffer( mGrContext, ANativeWindowBuffer_getHardwareBuffer(bufferInfo->buffer.get()), - kTopLeft_GrSurfaceOrigin, mWindowInfo.colorspace, nullptr); + kTopLeft_GrSurfaceOrigin, mWindowInfo.colorspace, nullptr, /*from_window=*/true); if (bufferInfo->skSurface.get() == nullptr) { ALOGE("SkSurface::MakeFromAHardwareBuffer failed"); mNativeWindow->cancelBuffer(mNativeWindow.get(), buffer, diff --git a/libs/hwui/tests/common/TestUtils.cpp b/libs/hwui/tests/common/TestUtils.cpp index e8ba15fe92af..491af4336f97 100644 --- a/libs/hwui/tests/common/TestUtils.cpp +++ b/libs/hwui/tests/common/TestUtils.cpp @@ -74,7 +74,7 @@ sp<DeferredLayerUpdater> TestUtils::createTextureLayerUpdater( layerUpdater->setTransform(&transform); // updateLayer so it's ready to draw - layerUpdater->updateLayer(true, SkMatrix::I(), nullptr); + layerUpdater->updateLayer(true, nullptr, 0, SkRect::MakeEmpty()); return layerUpdater; } diff --git a/libs/hwui/tests/common/scenes/PathClippingAnimation.cpp b/libs/hwui/tests/common/scenes/PathClippingAnimation.cpp new file mode 100644 index 000000000000..1e343c1dd283 --- /dev/null +++ b/libs/hwui/tests/common/scenes/PathClippingAnimation.cpp @@ -0,0 +1,153 @@ +/* + * 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. + */ + +#include <vector> + +#include "TestSceneBase.h" + +class PathClippingAnimation : public TestScene { +public: + int mSpacing, mSize; + bool mClip, mAnimateClip; + int mMaxCards; + std::vector<sp<RenderNode> > cards; + + PathClippingAnimation(int spacing, int size, bool clip, bool animateClip, int maxCards) + : mSpacing(spacing) + , mSize(size) + , mClip(clip) + , mAnimateClip(animateClip) + , mMaxCards(maxCards) {} + + PathClippingAnimation(int spacing, int size, bool clip, bool animateClip) + : PathClippingAnimation(spacing, size, clip, animateClip, INT_MAX) {} + + void createContent(int width, int height, Canvas& canvas) override { + canvas.drawColor(0xFFFFFFFF, SkBlendMode::kSrcOver); + canvas.enableZ(true); + int ci = 0; + int numCards = 0; + + for (int x = 0; x < width; x += mSpacing) { + for (int y = 0; y < height; y += mSpacing) { + auto color = BrightColors[ci++ % BrightColorsCount]; + auto card = TestUtils::createNode( + x, y, x + mSize, y + mSize, [&](RenderProperties& props, Canvas& canvas) { + canvas.drawColor(color, SkBlendMode::kSrcOver); + if (mClip) { + // Create circular path that rounds around the inside of all + // four corners of the given square defined by mSize*mSize + SkPath path = setPath(mSize); + props.mutableOutline().setPath(&path, 1); + props.mutableOutline().setShouldClip(true); + } + }); + canvas.drawRenderNode(card.get()); + cards.push_back(card); + ++numCards; + if (numCards >= mMaxCards) { + break; + } + } + if (numCards >= mMaxCards) { + break; + } + } + + canvas.enableZ(false); + } + + SkPath setPath(int size) { + SkPath path; + path.moveTo(0, size / 2); + path.cubicTo(0, size * .75, size * .25, size, size / 2, size); + path.cubicTo(size * .75, size, size, size * .75, size, size / 2); + path.cubicTo(size, size * .25, size * .75, 0, size / 2, 0); + path.cubicTo(size / 4, 0, 0, size / 4, 0, size / 2); + return path; + } + + void doFrame(int frameNr) override { + int curFrame = frameNr % 50; + if (curFrame > 25) curFrame = 50 - curFrame; + for (auto& card : cards) { + if (mAnimateClip) { + SkPath path = setPath(mSize - curFrame); + card->mutateStagingProperties().mutableOutline().setPath(&path, 1); + } + card->mutateStagingProperties().setTranslationX(curFrame); + card->mutateStagingProperties().setTranslationY(curFrame); + card->setPropertyFieldsDirty(RenderNode::X | RenderNode::Y | RenderNode::DISPLAY_LIST); + } + } +}; + +static TestScene::Registrar _PathClippingUnclipped(TestScene::Info{ + "pathClipping-unclipped", "Multiple RenderNodes, unclipped.", + [](const TestScene::Options&) -> test::TestScene* { + return new PathClippingAnimation(dp(100), dp(80), false, false); + }}); + +static TestScene::Registrar _PathClippingUnclippedSingle(TestScene::Info{ + "pathClipping-unclippedsingle", "A single RenderNode, unclipped.", + [](const TestScene::Options&) -> test::TestScene* { + return new PathClippingAnimation(dp(100), dp(80), false, false, 1); + }}); + +static TestScene::Registrar _PathClippingUnclippedSingleLarge(TestScene::Info{ + "pathClipping-unclippedsinglelarge", "A single large RenderNode, unclipped.", + [](const TestScene::Options&) -> test::TestScene* { + return new PathClippingAnimation(dp(100), dp(350), false, false, 1); + }}); + +static TestScene::Registrar _PathClippingClipped80(TestScene::Info{ + "pathClipping-clipped80", "Multiple RenderNodes, clipped by paths.", + [](const TestScene::Options&) -> test::TestScene* { + return new PathClippingAnimation(dp(100), dp(80), true, false); + }}); + +static TestScene::Registrar _PathClippingClippedSingle(TestScene::Info{ + "pathClipping-clippedsingle", "A single RenderNode, clipped by a path.", + [](const TestScene::Options&) -> test::TestScene* { + return new PathClippingAnimation(dp(100), dp(80), true, false, 1); + }}); + +static TestScene::Registrar _PathClippingClippedSingleLarge(TestScene::Info{ + "pathClipping-clippedsinglelarge", "A single large RenderNode, clipped by a path.", + [](const TestScene::Options&) -> test::TestScene* { + return new PathClippingAnimation(dp(100), dp(350), true, false, 1); + }}); + +static TestScene::Registrar _PathClippingAnimated(TestScene::Info{ + "pathClipping-animated", + "Multiple RenderNodes, clipped by paths which are being altered every frame.", + [](const TestScene::Options&) -> test::TestScene* { + return new PathClippingAnimation(dp(100), dp(80), true, true); + }}); + +static TestScene::Registrar _PathClippingAnimatedSingle(TestScene::Info{ + "pathClipping-animatedsingle", + "A single RenderNode, clipped by a path which is being altered every frame.", + [](const TestScene::Options&) -> test::TestScene* { + return new PathClippingAnimation(dp(100), dp(80), true, true, 1); + }}); + +static TestScene::Registrar _PathClippingAnimatedSingleLarge(TestScene::Info{ + "pathClipping-animatedsinglelarge", + "A single large RenderNode, clipped by a path which is being altered every frame.", + [](const TestScene::Options&) -> test::TestScene* { + return new PathClippingAnimation(dp(100), dp(350), true, true, 1); + }}); diff --git a/libs/hwui/tests/common/scenes/RoundRectClippingAnimation.cpp b/libs/hwui/tests/common/scenes/RoundRectClippingAnimation.cpp index 163745b04ed2..e9f353d887f2 100644 --- a/libs/hwui/tests/common/scenes/RoundRectClippingAnimation.cpp +++ b/libs/hwui/tests/common/scenes/RoundRectClippingAnimation.cpp @@ -21,14 +21,17 @@ class RoundRectClippingAnimation : public TestScene { public: int mSpacing, mSize; + int mMaxCards; - RoundRectClippingAnimation(int spacing, int size) : mSpacing(spacing), mSize(size) {} + RoundRectClippingAnimation(int spacing, int size, int maxCards = INT_MAX) + : mSpacing(spacing), mSize(size), mMaxCards(maxCards) {} std::vector<sp<RenderNode> > cards; void createContent(int width, int height, Canvas& canvas) override { canvas.drawColor(0xFFFFFFFF, SkBlendMode::kSrcOver); canvas.enableZ(true); int ci = 0; + int numCards = 0; for (int x = 0; x < width; x += mSpacing) { for (int y = 0; y < height; y += mSpacing) { @@ -42,6 +45,13 @@ public: }); canvas.drawRenderNode(card.get()); cards.push_back(card); + ++numCards; + if (numCards >= mMaxCards) { + break; + } + } + if (numCards >= mMaxCards) { + break; } } @@ -71,3 +81,22 @@ static TestScene::Registrar _RoundRectClippingCpu(TestScene::Info{ [](const TestScene::Options&) -> test::TestScene* { return new RoundRectClippingAnimation(dp(20), dp(20)); }}); + +static TestScene::Registrar _RoundRectClippingGrid(TestScene::Info{ + "roundRectClipping-grid", "A grid of RenderNodes with round rect clipping outlines.", + [](const TestScene::Options&) -> test::TestScene* { + return new RoundRectClippingAnimation(dp(100), dp(80)); + }}); + +static TestScene::Registrar _RoundRectClippingSingle(TestScene::Info{ + "roundRectClipping-single", "A single RenderNodes with round rect clipping outline.", + [](const TestScene::Options&) -> test::TestScene* { + return new RoundRectClippingAnimation(dp(100), dp(80), 1); + }}); + +static TestScene::Registrar _RoundRectClippingSingleLarge(TestScene::Info{ + "roundRectClipping-singlelarge", + "A single large RenderNodes with round rect clipping outline.", + [](const TestScene::Options&) -> test::TestScene* { + return new RoundRectClippingAnimation(dp(100), dp(350), 1); + }}); diff --git a/libs/hwui/tests/common/scenes/SaveLayerAnimation.cpp b/libs/hwui/tests/common/scenes/SaveLayerAnimation.cpp index 10ba07905c45..31a8ae1d38cd 100644 --- a/libs/hwui/tests/common/scenes/SaveLayerAnimation.cpp +++ b/libs/hwui/tests/common/scenes/SaveLayerAnimation.cpp @@ -49,7 +49,7 @@ public: paint.setAntiAlias(true); paint.setColor(Color::Green_700); canvas.drawCircle(200, 200, 200, paint); - SkPaint alphaPaint; + Paint alphaPaint; alphaPaint.setAlpha(128); canvas.restoreUnclippedLayer(unclippedSaveLayer, alphaPaint); canvas.restore(); diff --git a/libs/hwui/tests/macrobench/TestSceneRunner.cpp b/libs/hwui/tests/macrobench/TestSceneRunner.cpp index de2c6214088d..613a6aee3a5b 100644 --- a/libs/hwui/tests/macrobench/TestSceneRunner.cpp +++ b/libs/hwui/tests/macrobench/TestSceneRunner.cpp @@ -104,7 +104,6 @@ static void doRun(const TestScene::Info& info, const TestScene::Options& opts, i // If we're reporting GPU memory usage we need to first start with a clean slate RenderProxy::purgeCaches(); } - Properties::forceDrawFrame = true; TestContext testContext; testContext.setRenderOffscreen(opts.renderOffscreen); @@ -144,6 +143,7 @@ static void doRun(const TestScene::Info& info, const TestScene::Options& opts, i .setVsync(vsync, vsync, UiFrameInfoBuilder::INVALID_VSYNC_ID, UiFrameInfoBuilder::UNKNOWN_DEADLINE, UiFrameInfoBuilder::UNKNOWN_FRAME_INTERVAL); + proxy->forceDrawNextFrame(); proxy->syncAndDrawFrame(); } @@ -163,6 +163,7 @@ static void doRun(const TestScene::Info& info, const TestScene::Options& opts, i UiFrameInfoBuilder::UNKNOWN_DEADLINE, UiFrameInfoBuilder::UNKNOWN_FRAME_INTERVAL); scene->doFrame(i); + proxy->forceDrawNextFrame(); proxy->syncAndDrawFrame(); } if (opts.reportFrametimeWeight) { diff --git a/libs/hwui/tests/unit/DeferredLayerUpdaterTests.cpp b/libs/hwui/tests/unit/DeferredLayerUpdaterTests.cpp index 955a5e7d8b3a..0c389bfe8b71 100644 --- a/libs/hwui/tests/unit/DeferredLayerUpdaterTests.cpp +++ b/libs/hwui/tests/unit/DeferredLayerUpdaterTests.cpp @@ -36,19 +36,16 @@ RENDERTHREAD_TEST(DeferredLayerUpdater, updateLayer) { EXPECT_EQ(0u, layerUpdater->backingLayer()->getHeight()); EXPECT_FALSE(layerUpdater->backingLayer()->getForceFilter()); EXPECT_FALSE(layerUpdater->backingLayer()->isBlend()); - EXPECT_EQ(Matrix4::identity(), layerUpdater->backingLayer()->getTexTransform()); // push the deferred updates to the layer - SkMatrix scaledMatrix = SkMatrix::Scale(0.5, 0.5); SkBitmap bitmap; bitmap.allocN32Pixels(16, 16); sk_sp<SkImage> layerImage = SkImage::MakeFromBitmap(bitmap); - layerUpdater->updateLayer(true, scaledMatrix, layerImage); + layerUpdater->updateLayer(true, layerImage, 0, SkRect::MakeEmpty()); // the backing layer should now have all the properties applied. EXPECT_EQ(100u, layerUpdater->backingLayer()->getWidth()); EXPECT_EQ(100u, layerUpdater->backingLayer()->getHeight()); EXPECT_TRUE(layerUpdater->backingLayer()->getForceFilter()); EXPECT_TRUE(layerUpdater->backingLayer()->isBlend()); - EXPECT_EQ(scaledMatrix, layerUpdater->backingLayer()->getTexTransform()); } diff --git a/libs/hwui/tests/unit/FrameMetricsReporterTests.cpp b/libs/hwui/tests/unit/FrameMetricsReporterTests.cpp new file mode 100644 index 000000000000..571a26707c93 --- /dev/null +++ b/libs/hwui/tests/unit/FrameMetricsReporterTests.cpp @@ -0,0 +1,211 @@ +/* + * 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. + */ + +#include <gmock/gmock.h> +#include <gtest/gtest.h> + +#include <FrameMetricsObserver.h> +#include <FrameMetricsReporter.h> +#include <utils/TimeUtils.h> + +using namespace android; +using namespace android::uirenderer; + +using ::testing::NotNull; + +class TestFrameMetricsObserver : public FrameMetricsObserver { +public: + explicit TestFrameMetricsObserver(bool waitForPresentTime) + : FrameMetricsObserver(waitForPresentTime){}; + + MOCK_METHOD(void, notify, (const int64_t* buffer), (override)); +}; + +// To make sure it is clear that something went wrong if no from frame is set (to make it easier +// to catch bugs were we forget to set the fromFrame). +TEST(FrameMetricsReporter, doesNotReportAnyFrameIfNoFromFrameIsSpecified) { + auto reporter = std::make_shared<FrameMetricsReporter>(); + + auto observer = sp<TestFrameMetricsObserver>::make(false /*waitForPresentTime*/); + EXPECT_CALL(*observer, notify).Times(0); + + reporter->addObserver(observer.get()); + + const int64_t* stats; + bool hasPresentTime = false; + uint64_t frameNumber = 1; + int32_t surfaceControlId = 0; + reporter->reportFrameMetrics(stats, hasPresentTime, frameNumber, surfaceControlId); + + frameNumber = 10; + surfaceControlId = 0; + reporter->reportFrameMetrics(stats, hasPresentTime, frameNumber, surfaceControlId); + + frameNumber = 0; + surfaceControlId = 2; + reporter->reportFrameMetrics(stats, hasPresentTime, frameNumber, surfaceControlId); + + frameNumber = 10; + surfaceControlId = 2; + reporter->reportFrameMetrics(stats, hasPresentTime, frameNumber, surfaceControlId); +} + +TEST(FrameMetricsReporter, respectsWaitForPresentTimeUnset) { + const int64_t* stats; + bool hasPresentTime = false; + uint64_t frameNumber = 3; + int32_t surfaceControlId = 0; + + auto reporter = std::make_shared<FrameMetricsReporter>(); + + auto observer = sp<TestFrameMetricsObserver>::make(hasPresentTime); + observer->reportMetricsFrom(frameNumber, surfaceControlId); + reporter->addObserver(observer.get()); + + EXPECT_CALL(*observer, notify).Times(1); + hasPresentTime = false; + reporter->reportFrameMetrics(stats, hasPresentTime, frameNumber, surfaceControlId); + + EXPECT_CALL(*observer, notify).Times(0); + hasPresentTime = true; + reporter->reportFrameMetrics(stats, hasPresentTime, frameNumber, surfaceControlId); +} + +TEST(FrameMetricsReporter, respectsWaitForPresentTimeSet) { + const int64_t* stats; + bool hasPresentTime = true; + uint64_t frameNumber = 3; + int32_t surfaceControlId = 0; + + auto reporter = std::make_shared<FrameMetricsReporter>(); + + auto observer = sp<TestFrameMetricsObserver>::make(hasPresentTime); + observer->reportMetricsFrom(frameNumber, surfaceControlId); + reporter->addObserver(observer.get()); + + EXPECT_CALL(*observer, notify).Times(0); + hasPresentTime = false; + reporter->reportFrameMetrics(stats, hasPresentTime, frameNumber, surfaceControlId); + + EXPECT_CALL(*observer, notify).Times(1); + hasPresentTime = true; + reporter->reportFrameMetrics(stats, hasPresentTime, frameNumber, surfaceControlId); +} + +TEST(FrameMetricsReporter, reportsAllFramesAfterSpecifiedFromFrame) { + const int64_t* stats; + bool hasPresentTime = false; + + std::vector<uint64_t> frameNumbers{0, 1, 10}; + std::vector<int32_t> surfaceControlIds{0, 1, 10}; + for (uint64_t frameNumber : frameNumbers) { + for (int32_t surfaceControlId : surfaceControlIds) { + auto reporter = std::make_shared<FrameMetricsReporter>(); + + auto observer = + sp<TestFrameMetricsObserver>::make(hasPresentTime /*waitForPresentTime*/); + observer->reportMetricsFrom(frameNumber, surfaceControlId); + reporter->addObserver(observer.get()); + + EXPECT_CALL(*observer, notify).Times(8); + reporter->reportFrameMetrics(stats, hasPresentTime, frameNumber, surfaceControlId); + reporter->reportFrameMetrics(stats, hasPresentTime, frameNumber + 1, surfaceControlId); + reporter->reportFrameMetrics(stats, hasPresentTime, frameNumber + 10, surfaceControlId); + reporter->reportFrameMetrics(stats, hasPresentTime, frameNumber, surfaceControlId + 1); + reporter->reportFrameMetrics(stats, hasPresentTime, frameNumber - 1, + surfaceControlId + 1); + reporter->reportFrameMetrics(stats, hasPresentTime, frameNumber + 1, + surfaceControlId + 1); + reporter->reportFrameMetrics(stats, hasPresentTime, frameNumber + 10, + surfaceControlId + 1); + reporter->reportFrameMetrics(stats, hasPresentTime, frameNumber + 10, + surfaceControlId + 10); + } + } +} + +TEST(FrameMetricsReporter, doesNotReportsFramesBeforeSpecifiedFromFrame) { + const int64_t* stats; + bool hasPresentTime = false; + + std::vector<uint64_t> frameNumbers{1, 10}; + std::vector<int32_t> surfaceControlIds{0, 1, 10}; + for (uint64_t frameNumber : frameNumbers) { + for (int32_t surfaceControlId : surfaceControlIds) { + auto reporter = std::make_shared<FrameMetricsReporter>(); + + auto observer = + sp<TestFrameMetricsObserver>::make(hasPresentTime /*waitForPresentTime*/); + observer->reportMetricsFrom(frameNumber, surfaceControlId); + reporter->addObserver(observer.get()); + + EXPECT_CALL(*observer, notify).Times(0); + reporter->reportFrameMetrics(stats, hasPresentTime, frameNumber - 1, surfaceControlId); + if (surfaceControlId > 0) { + reporter->reportFrameMetrics(stats, hasPresentTime, frameNumber, + surfaceControlId - 1); + reporter->reportFrameMetrics(stats, hasPresentTime, frameNumber - 1, + surfaceControlId - 1); + } + } + } +} + +TEST(FrameMetricsReporter, canRemoveObservers) { + const int64_t* stats; + bool hasPresentTime = false; + uint64_t frameNumber = 3; + int32_t surfaceControlId = 0; + + auto reporter = std::make_shared<FrameMetricsReporter>(); + + auto observer = sp<TestFrameMetricsObserver>::make(hasPresentTime /*waitForPresentTime*/); + + observer->reportMetricsFrom(frameNumber, surfaceControlId); + reporter->addObserver(observer.get()); + + EXPECT_CALL(*observer, notify).Times(1); + reporter->reportFrameMetrics(stats, hasPresentTime, frameNumber, surfaceControlId); + + ASSERT_TRUE(reporter->removeObserver(observer.get())); + + EXPECT_CALL(*observer, notify).Times(0); + reporter->reportFrameMetrics(stats, hasPresentTime, frameNumber, surfaceControlId); +} + +TEST(FrameMetricsReporter, canSupportMultipleObservers) { + const int64_t* stats; + bool hasPresentTime = false; + uint64_t frameNumber = 3; + int32_t surfaceControlId = 0; + + auto reporter = std::make_shared<FrameMetricsReporter>(); + + auto observer1 = sp<TestFrameMetricsObserver>::make(hasPresentTime /*waitForPresentTime*/); + auto observer2 = sp<TestFrameMetricsObserver>::make(hasPresentTime /*waitForPresentTime*/); + observer1->reportMetricsFrom(frameNumber, surfaceControlId); + observer2->reportMetricsFrom(frameNumber + 10, surfaceControlId + 1); + reporter->addObserver(observer1.get()); + reporter->addObserver(observer2.get()); + + EXPECT_CALL(*observer1, notify).Times(1); + EXPECT_CALL(*observer2, notify).Times(0); + reporter->reportFrameMetrics(stats, hasPresentTime, frameNumber, surfaceControlId); + + EXPECT_CALL(*observer1, notify).Times(1); + EXPECT_CALL(*observer2, notify).Times(1); + reporter->reportFrameMetrics(stats, hasPresentTime, frameNumber + 10, surfaceControlId + 1); +} diff --git a/libs/hwui/tests/unit/JankTrackerTests.cpp b/libs/hwui/tests/unit/JankTrackerTests.cpp index f467ebf5d888..5b397de36a86 100644 --- a/libs/hwui/tests/unit/JankTrackerTests.cpp +++ b/libs/hwui/tests/unit/JankTrackerTests.cpp @@ -34,6 +34,9 @@ TEST(JankTracker, noJank) { JankTracker jankTracker(&container); std::unique_ptr<FrameMetricsReporter> reporter = std::make_unique<FrameMetricsReporter>(); + uint64_t frameNumber = 0; + uint32_t surfaceId = 0; + FrameInfo* info = jankTracker.startFrame(); info->set(FrameInfoIndex::IntendedVsync) = 100_ms; info->set(FrameInfoIndex::Vsync) = 101_ms; @@ -42,7 +45,7 @@ TEST(JankTracker, noJank) { info->set(FrameInfoIndex::FrameCompleted) = 115_ms; info->set(FrameInfoIndex::FrameInterval) = 16_ms; info->set(FrameInfoIndex::FrameDeadline) = 120_ms; - jankTracker.finishFrame(*info, reporter); + jankTracker.finishFrame(*info, reporter, frameNumber, surfaceId); info = jankTracker.startFrame(); info->set(FrameInfoIndex::IntendedVsync) = 116_ms; @@ -52,7 +55,7 @@ TEST(JankTracker, noJank) { info->set(FrameInfoIndex::FrameCompleted) = 131_ms; info->set(FrameInfoIndex::FrameInterval) = 16_ms; info->set(FrameInfoIndex::FrameDeadline) = 136_ms; - jankTracker.finishFrame(*info, reporter); + jankTracker.finishFrame(*info, reporter, frameNumber, surfaceId); ASSERT_EQ(2, container.get()->totalFrameCount()); ASSERT_EQ(0, container.get()->jankFrameCount()); @@ -65,6 +68,9 @@ TEST(JankTracker, jank) { JankTracker jankTracker(&container); std::unique_ptr<FrameMetricsReporter> reporter = std::make_unique<FrameMetricsReporter>(); + uint64_t frameNumber = 0; + uint32_t surfaceId = 0; + FrameInfo* info = jankTracker.startFrame(); info->set(FrameInfoIndex::IntendedVsync) = 100_ms; info->set(FrameInfoIndex::Vsync) = 101_ms; @@ -73,7 +79,7 @@ TEST(JankTracker, jank) { info->set(FrameInfoIndex::FrameCompleted) = 121_ms; info->set(FrameInfoIndex::FrameInterval) = 16_ms; info->set(FrameInfoIndex::FrameDeadline) = 120_ms; - jankTracker.finishFrame(*info, reporter); + jankTracker.finishFrame(*info, reporter, frameNumber, surfaceId); ASSERT_EQ(1, container.get()->totalFrameCount()); ASSERT_EQ(1, container.get()->jankFrameCount()); @@ -85,6 +91,9 @@ TEST(JankTracker, legacyJankButNoRealJank) { JankTracker jankTracker(&container); std::unique_ptr<FrameMetricsReporter> reporter = std::make_unique<FrameMetricsReporter>(); + uint64_t frameNumber = 0; + uint32_t surfaceId = 0; + FrameInfo* info = jankTracker.startFrame(); info->set(FrameInfoIndex::IntendedVsync) = 100_ms; info->set(FrameInfoIndex::Vsync) = 101_ms; @@ -93,7 +102,7 @@ TEST(JankTracker, legacyJankButNoRealJank) { info->set(FrameInfoIndex::FrameCompleted) = 118_ms; info->set(FrameInfoIndex::FrameInterval) = 16_ms; info->set(FrameInfoIndex::FrameDeadline) = 120_ms; - jankTracker.finishFrame(*info, reporter); + jankTracker.finishFrame(*info, reporter, frameNumber, surfaceId); ASSERT_EQ(1, container.get()->totalFrameCount()); ASSERT_EQ(0, container.get()->jankFrameCount()); @@ -106,6 +115,9 @@ TEST(JankTracker, doubleStuffed) { 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; @@ -115,7 +127,7 @@ TEST(JankTracker, doubleStuffed) { info->set(FrameInfoIndex::FrameCompleted) = 121_ms; info->set(FrameInfoIndex::FrameInterval) = 16_ms; info->set(FrameInfoIndex::FrameDeadline) = 120_ms; - jankTracker.finishFrame(*info, reporter); + jankTracker.finishFrame(*info, reporter, frameNumber, surfaceId); ASSERT_EQ(1, container.get()->jankFrameCount()); @@ -128,7 +140,7 @@ TEST(JankTracker, doubleStuffed) { info->set(FrameInfoIndex::FrameCompleted) = 137_ms; info->set(FrameInfoIndex::FrameInterval) = 16_ms; info->set(FrameInfoIndex::FrameDeadline) = 136_ms; - jankTracker.finishFrame(*info, reporter); + jankTracker.finishFrame(*info, reporter, frameNumber, surfaceId); ASSERT_EQ(2, container.get()->totalFrameCount()); ASSERT_EQ(1, container.get()->jankFrameCount()); @@ -140,6 +152,9 @@ TEST(JankTracker, doubleStuffedThenPauseThenJank) { 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; @@ -149,7 +164,7 @@ TEST(JankTracker, doubleStuffedThenPauseThenJank) { info->set(FrameInfoIndex::FrameCompleted) = 121_ms; info->set(FrameInfoIndex::FrameInterval) = 16_ms; info->set(FrameInfoIndex::FrameDeadline) = 120_ms; - jankTracker.finishFrame(*info, reporter); + jankTracker.finishFrame(*info, reporter, frameNumber, surfaceId); ASSERT_EQ(1, container.get()->jankFrameCount()); @@ -162,7 +177,7 @@ TEST(JankTracker, doubleStuffedThenPauseThenJank) { info->set(FrameInfoIndex::FrameCompleted) = 137_ms; info->set(FrameInfoIndex::FrameInterval) = 16_ms; info->set(FrameInfoIndex::FrameDeadline) = 136_ms; - jankTracker.finishFrame(*info, reporter); + jankTracker.finishFrame(*info, reporter, frameNumber, surfaceId); ASSERT_EQ(1, container.get()->jankFrameCount()); @@ -175,8 +190,8 @@ TEST(JankTracker, doubleStuffedThenPauseThenJank) { info->set(FrameInfoIndex::FrameCompleted) = 169_ms; info->set(FrameInfoIndex::FrameInterval) = 16_ms; info->set(FrameInfoIndex::FrameDeadline) = 168_ms; - jankTracker.finishFrame(*info, reporter); + jankTracker.finishFrame(*info, reporter, frameNumber, surfaceId); ASSERT_EQ(3, container.get()->totalFrameCount()); ASSERT_EQ(2, container.get()->jankFrameCount()); -}
\ No newline at end of file +} diff --git a/libs/hwui/tests/unit/RenderNodeDrawableTests.cpp b/libs/hwui/tests/unit/RenderNodeDrawableTests.cpp index 423400eb8ff1..ec949b80ea55 100644 --- a/libs/hwui/tests/unit/RenderNodeDrawableTests.cpp +++ b/libs/hwui/tests/unit/RenderNodeDrawableTests.cpp @@ -469,7 +469,7 @@ RENDERTHREAD_SKIA_PIPELINE_TEST(RenderNodeDrawable, projectionHwLayer) { } SkCanvas* onNewCanvas() override { return new ProjectionTestCanvas(mDrawCounter); } sk_sp<SkSurface> onNewSurface(const SkImageInfo&) override { return nullptr; } - void onCopyOnWrite(ContentChangeMode) override {} + bool onCopyOnWrite(ContentChangeMode) override { return true; } int* mDrawCounter; void onWritePixels(const SkPixmap&, int x, int y) {} }; diff --git a/libs/hwui/tests/unit/ShaderCacheTests.cpp b/libs/hwui/tests/unit/ShaderCacheTests.cpp index 87981f115763..974d85a453db 100644 --- a/libs/hwui/tests/unit/ShaderCacheTests.cpp +++ b/libs/hwui/tests/unit/ShaderCacheTests.cpp @@ -140,9 +140,9 @@ TEST(ShaderCacheTest, testWriteAndRead) { // write to the in-memory cache without storing on disk and verify we read the same values sk_sp<SkData> inVS; setShader(inVS, "sassas"); - ShaderCache::get().store(GrProgramDescTest(100), *inVS.get()); + ShaderCache::get().store(GrProgramDescTest(100), *inVS.get(), SkString()); setShader(inVS, "someVS"); - ShaderCache::get().store(GrProgramDescTest(432), *inVS.get()); + ShaderCache::get().store(GrProgramDescTest(432), *inVS.get(), SkString()); ASSERT_NE((outVS = ShaderCache::get().load(GrProgramDescTest(100))), sk_sp<SkData>()); ASSERT_TRUE(checkShader(outVS, "sassas")); ASSERT_NE((outVS = ShaderCache::get().load(GrProgramDescTest(432))), sk_sp<SkData>()); @@ -166,7 +166,7 @@ TEST(ShaderCacheTest, testWriteAndRead) { // change data, store to disk, read back again and verify data has been changed setShader(inVS, "ewData1"); - ShaderCache::get().store(GrProgramDescTest(432), *inVS.get()); + ShaderCache::get().store(GrProgramDescTest(432), *inVS.get(), SkString()); ShaderCacheTestUtils::terminate(ShaderCache::get(), true); ShaderCache::get().initShaderDiskCache(); ASSERT_NE((outVS2 = ShaderCache::get().load(GrProgramDescTest(432))), sk_sp<SkData>()); @@ -177,7 +177,7 @@ TEST(ShaderCacheTest, testWriteAndRead) { std::vector<uint8_t> dataBuffer(dataSize); genRandomData(dataBuffer); setShader(inVS, dataBuffer); - ShaderCache::get().store(GrProgramDescTest(432), *inVS.get()); + ShaderCache::get().store(GrProgramDescTest(432), *inVS.get(), SkString()); ShaderCacheTestUtils::terminate(ShaderCache::get(), true); ShaderCache::get().initShaderDiskCache(); ASSERT_NE((outVS2 = ShaderCache::get().load(GrProgramDescTest(432))), sk_sp<SkData>()); @@ -225,7 +225,7 @@ TEST(ShaderCacheTest, testCacheValidation) { setShader(data, dataBuffer); blob = std::make_pair(key, data); - ShaderCache::get().store(*key.get(), *data.get()); + ShaderCache::get().store(*key.get(), *data.get(), SkString()); } ShaderCacheTestUtils::terminate(ShaderCache::get(), true); diff --git a/libs/hwui/tests/unit/SkiaBehaviorTests.cpp b/libs/hwui/tests/unit/SkiaBehaviorTests.cpp index a1ba70a22581..dc1b2e668dd0 100644 --- a/libs/hwui/tests/unit/SkiaBehaviorTests.cpp +++ b/libs/hwui/tests/unit/SkiaBehaviorTests.cpp @@ -61,11 +61,11 @@ TEST(SkiaBehavior, lightingColorFilter_simplify) { TEST(SkiaBehavior, porterDuffCreateIsCached) { SkPaint paint; paint.setBlendMode(SkBlendMode::kOverlay); - auto expected = paint.getBlendMode(); + auto expected = paint.asBlendMode(); paint.setBlendMode(SkBlendMode::kClear); - ASSERT_NE(expected, paint.getBlendMode()); + ASSERT_NE(expected, paint.asBlendMode()); paint.setBlendMode(SkBlendMode::kOverlay); - ASSERT_EQ(expected, paint.getBlendMode()); + ASSERT_EQ(expected, paint.asBlendMode()); } TEST(SkiaBehavior, pathIntersection) { diff --git a/libs/hwui/tests/unit/SkiaPipelineTests.cpp b/libs/hwui/tests/unit/SkiaPipelineTests.cpp index 8c999c41bf7b..60ae6044cd5b 100644 --- a/libs/hwui/tests/unit/SkiaPipelineTests.cpp +++ b/libs/hwui/tests/unit/SkiaPipelineTests.cpp @@ -221,7 +221,7 @@ public: sk_sp<SkSurface> onNewSurface(const SkImageInfo&) override { return nullptr; } sk_sp<SkImage> onNewImageSnapshot(const SkIRect* bounds) override { return nullptr; } T* canvas() { return static_cast<T*>(getCanvas()); } - void onCopyOnWrite(ContentChangeMode) override {} + bool onCopyOnWrite(ContentChangeMode) override { return true; } void onWritePixels(const SkPixmap&, int x, int y) override {} }; } @@ -382,7 +382,7 @@ RENDERTHREAD_SKIA_PIPELINE_TEST(SkiaPipeline, clip_replace) { std::vector<sp<RenderNode>> nodes; nodes.push_back(TestUtils::createSkiaNode( 20, 20, 30, 30, [](RenderProperties& props, SkiaRecordingCanvas& canvas) { - canvas.clipRect(0, -20, 10, 30, SkClipOp::kReplace_deprecated); + canvas.replaceClipRect_deprecated(0, -20, 10, 30); canvas.drawColor(SK_ColorWHITE, SkBlendMode::kSrcOver); })); diff --git a/libs/hwui/utils/Color.cpp b/libs/hwui/utils/Color.cpp index 5d9f2297c15a..3afb419f9b8b 100644 --- a/libs/hwui/utils/Color.cpp +++ b/libs/hwui/utils/Color.cpp @@ -57,6 +57,10 @@ static inline SkImageInfo createImageInfo(int32_t width, int32_t height, int32_t colorType = kRGBA_F16_SkColorType; alphaType = kPremul_SkAlphaType; break; + case AHARDWAREBUFFER_FORMAT_R8_UNORM: + colorType = kAlpha_8_SkColorType; + alphaType = kPremul_SkAlphaType; + break; default: ALOGV("Unsupported format: %d, return unknown by default", format); break; @@ -90,6 +94,8 @@ uint32_t ColorTypeToBufferFormat(SkColorType colorType) { // Hardcoding the value from android::PixelFormat static constexpr uint64_t kRGBA4444 = 7; return kRGBA4444; + case kAlpha_8_SkColorType: + return AHARDWAREBUFFER_FORMAT_R8_UNORM; default: ALOGV("Unsupported colorType: %d, return RGBA_8888 by default", (int)colorType); return AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM; @@ -159,6 +165,14 @@ android_dataspace ColorSpaceToADataSpace(SkColorSpace* colorSpace, SkColorType c if (SkColorSpace::Equals(colorSpace, rec2020PQ.get())) { 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); + } + } LOG_ALWAYS_FATAL("Only select non-numerical transfer functions are supported"); } @@ -219,6 +233,7 @@ sk_sp<SkColorSpace> DataSpaceToColorSpace(android_dataspace dataspace) { gamut = SkNamedGamut::kSRGB; break; case HAL_DATASPACE_STANDARD_BT2020: + case HAL_DATASPACE_STANDARD_BT2020_CONSTANT_LUMINANCE: gamut = SkNamedGamut::kRec2020; break; case HAL_DATASPACE_STANDARD_DCI_P3: @@ -233,7 +248,6 @@ sk_sp<SkColorSpace> DataSpaceToColorSpace(android_dataspace dataspace) { case HAL_DATASPACE_STANDARD_BT601_625_UNADJUSTED: case HAL_DATASPACE_STANDARD_BT601_525: case HAL_DATASPACE_STANDARD_BT601_525_UNADJUSTED: - case HAL_DATASPACE_STANDARD_BT2020_CONSTANT_LUMINANCE: case HAL_DATASPACE_STANDARD_BT470M: case HAL_DATASPACE_STANDARD_FILM: default: @@ -241,6 +255,14 @@ sk_sp<SkColorSpace> DataSpaceToColorSpace(android_dataspace dataspace) { return nullptr; } + // HLG + if ((dataspace & HAL_DATASPACE_TRANSFER_MASK) == HAL_DATASPACE_TRANSFER_HLG) { + const auto hlgFn = GetHLGScaleTransferFunction(); + if (hlgFn.has_value()) { + return SkColorSpace::MakeRGB(hlgFn.value(), gamut); + } + } + switch (dataspace & HAL_DATASPACE_TRANSFER_MASK) { case HAL_DATASPACE_TRANSFER_LINEAR: return SkColorSpace::MakeRGB(SkNamedTransferFn::kLinear, gamut); @@ -258,7 +280,6 @@ sk_sp<SkColorSpace> DataSpaceToColorSpace(android_dataspace dataspace) { return SkColorSpace::MakeRGB(SkNamedTransferFn::kRec2020, gamut); case HAL_DATASPACE_TRANSFER_UNSPECIFIED: return nullptr; - case HAL_DATASPACE_TRANSFER_HLG: default: ALOGV("Unsupported Gamma: %d", dataspace); return nullptr; @@ -375,5 +396,16 @@ skcms_TransferFunction GetPQSkTransferFunction(float sdr_white_level) { return fn; } +// 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() { + skcms_TransferFunction hlgFn; + if (skcms_TransferFunction_makeScaledHLGish(&hlgFn, 1.f / 12.f, 2.f, 2.f, 1.f / 0.17883277f, + 0.28466892f, 0.55991073f)) { + return std::make_optional<skcms_TransferFunction>(hlgFn); + } + return {}; +} + } // namespace uirenderer } // namespace android diff --git a/libs/hwui/utils/Color.h b/libs/hwui/utils/Color.h index 1654072fd264..00f910f45c38 100644 --- a/libs/hwui/utils/Color.h +++ b/libs/hwui/utils/Color.h @@ -23,6 +23,8 @@ #include <math.h> #include <system/graphics.h> +#include <optional> + struct ANativeWindow_Buffer; struct AHardwareBuffer_Desc; @@ -127,6 +129,7 @@ struct Lab { Lab sRGBToLab(SkColor color); SkColor LabToSRGB(const Lab& lab, SkAlpha alpha); skcms_TransferFunction GetPQSkTransferFunction(float sdr_white_level = 0.f); +std::optional<skcms_TransferFunction> GetHLGScaleTransferFunction(); } /* namespace uirenderer */ } /* namespace android */ diff --git a/libs/hwui/utils/PaintUtils.h b/libs/hwui/utils/PaintUtils.h index a8f2d9a28d67..94bcb1110e05 100644 --- a/libs/hwui/utils/PaintUtils.h +++ b/libs/hwui/utils/PaintUtils.h @@ -32,13 +32,6 @@ namespace uirenderer { */ class PaintUtils { public: - static inline GLenum getFilter(const SkPaint* paint) { - if (!paint || paint->getFilterQuality() != kNone_SkFilterQuality) { - return GL_LINEAR; - } - return GL_NEAREST; - } - static bool isOpaquePaint(const SkPaint* paint) { if (!paint) return true; // default (paintless) behavior is SrcOver, black @@ -48,7 +41,7 @@ public: } // Only let simple srcOver / src blending modes declare opaque, since behavior is clear. - SkBlendMode mode = paint->getBlendMode(); + const auto mode = paint->asBlendMode(); return mode == SkBlendMode::kSrcOver || mode == SkBlendMode::kSrc; } @@ -59,7 +52,7 @@ public: } static inline SkBlendMode getBlendModeDirect(const SkPaint* paint) { - return paint ? paint->getBlendMode() : SkBlendMode::kSrcOver; + return paint ? paint->getBlendMode_or(SkBlendMode::kSrcOver) : SkBlendMode::kSrcOver; } static inline int getAlphaDirect(const SkPaint* paint) { diff --git a/libs/input/Android.bp b/libs/input/Android.bp index 55f932dffff1..6c0fd5f65359 100644 --- a/libs/input/Android.bp +++ b/libs/input/Android.bp @@ -55,6 +55,7 @@ cc_library_shared { "-Wall", "-Wextra", "-Werror", + "-Wthread-safety", ], } diff --git a/libs/input/PointerController.cpp b/libs/input/PointerController.cpp index 8f04cfb70469..10ea6512c724 100644 --- a/libs/input/PointerController.cpp +++ b/libs/input/PointerController.cpp @@ -17,24 +17,41 @@ #define LOG_TAG "PointerController" //#define LOG_NDEBUG 0 -// Log debug messages about pointer updates -#define DEBUG_POINTER_UPDATES 0 - #include "PointerController.h" -#include "MouseCursorController.h" -#include "PointerControllerContext.h" -#include "TouchSpotController.h" - -#include <log/log.h> -#include <SkBitmap.h> #include <SkBlendMode.h> #include <SkCanvas.h> #include <SkColor.h> -#include <SkPaint.h> +#include <android-base/thread_annotations.h> + +#include "PointerControllerContext.h" namespace android { +namespace { + +const ui::Transform kIdentityTransform; + +} // namespace + +// --- PointerController::DisplayInfoListener --- + +void PointerController::DisplayInfoListener::onWindowInfosChanged( + const std::vector<android::gui::WindowInfo>&, + const std::vector<android::gui::DisplayInfo>& displayInfos) { + std::scoped_lock lock(mLock); + if (mPointerController == nullptr) return; + + // PointerController uses DisplayInfoListener's lock. + base::ScopedLockAssertion assumeLocked(mPointerController->getLock()); + mPointerController->onDisplayInfosChangedLocked(displayInfos); +} + +void PointerController::DisplayInfoListener::onPointerControllerDestroyed() { + std::scoped_lock lock(mLock); + mPointerController = nullptr; +} + // --- PointerController --- std::shared_ptr<PointerController> PointerController::create( @@ -63,9 +80,37 @@ std::shared_ptr<PointerController> PointerController::create( PointerController::PointerController(const sp<PointerControllerPolicyInterface>& policy, const sp<Looper>& looper, const sp<SpriteController>& spriteController) - : mContext(policy, looper, spriteController, *this), mCursorController(mContext) { - std::scoped_lock lock(mLock); + : PointerController( + policy, looper, spriteController, + [](const sp<android::gui::WindowInfosListener>& listener) { + SurfaceComposerClient::getDefault()->addWindowInfosListener(listener); + }, + [](const sp<android::gui::WindowInfosListener>& listener) { + SurfaceComposerClient::getDefault()->removeWindowInfosListener(listener); + }) {} + +PointerController::PointerController(const sp<PointerControllerPolicyInterface>& policy, + const sp<Looper>& looper, + const sp<SpriteController>& spriteController, + WindowListenerConsumer registerListener, + WindowListenerConsumer unregisterListener) + : mContext(policy, looper, spriteController, *this), + mCursorController(mContext), + mDisplayInfoListener(new DisplayInfoListener(this)), + mUnregisterWindowInfosListener(std::move(unregisterListener)) { + std::scoped_lock lock(getLock()); mLocked.presentation = Presentation::SPOT; + registerListener(mDisplayInfoListener); +} + +PointerController::~PointerController() { + mDisplayInfoListener->onPointerControllerDestroyed(); + mUnregisterWindowInfosListener(mDisplayInfoListener); + mContext.getPolicy()->onPointerDisplayIdChanged(ADISPLAY_ID_NONE, 0, 0); +} + +std::mutex& PointerController::getLock() const { + return mDisplayInfoListener->mLock; } bool PointerController::getBounds(float* outMinX, float* outMinY, float* outMaxX, @@ -74,7 +119,14 @@ bool PointerController::getBounds(float* outMinX, float* outMinY, float* outMaxX } void PointerController::move(float deltaX, float deltaY) { - mCursorController.move(deltaX, deltaY); + const int32_t displayId = mCursorController.getDisplayId(); + vec2 transformed; + { + std::scoped_lock lock(getLock()); + const auto& transform = getTransformForDisplayLocked(displayId); + transformed = transformWithoutTranslation(transform, {deltaX, deltaY}); + } + mCursorController.move(transformed.x, transformed.y); } void PointerController::setButtonState(int32_t buttonState) { @@ -86,12 +138,26 @@ int32_t PointerController::getButtonState() const { } void PointerController::setPosition(float x, float y) { - std::scoped_lock lock(mLock); - mCursorController.setPosition(x, y); + const int32_t displayId = mCursorController.getDisplayId(); + vec2 transformed; + { + std::scoped_lock lock(getLock()); + const auto& transform = getTransformForDisplayLocked(displayId); + transformed = transform.transform(x, y); + } + mCursorController.setPosition(transformed.x, transformed.y); } void PointerController::getPosition(float* outX, float* outY) const { + const int32_t displayId = mCursorController.getDisplayId(); mCursorController.getPosition(outX, outY); + { + std::scoped_lock lock(getLock()); + const auto& transform = getTransformForDisplayLocked(displayId); + const auto xy = transform.inverse().transform(*outX, *outY); + *outX = xy.x; + *outY = xy.y; + } } int32_t PointerController::getDisplayId() const { @@ -99,17 +165,17 @@ int32_t PointerController::getDisplayId() const { } void PointerController::fade(Transition transition) { - std::scoped_lock lock(mLock); + std::scoped_lock lock(getLock()); mCursorController.fade(transition); } void PointerController::unfade(Transition transition) { - std::scoped_lock lock(mLock); + std::scoped_lock lock(getLock()); mCursorController.unfade(transition); } void PointerController::setPresentation(Presentation presentation) { - std::scoped_lock lock(mLock); + std::scoped_lock lock(getLock()); if (mLocked.presentation == presentation) { return; @@ -129,20 +195,34 @@ void PointerController::setPresentation(Presentation presentation) { void PointerController::setSpots(const PointerCoords* spotCoords, const uint32_t* spotIdToIndex, BitSet32 spotIdBits, int32_t displayId) { - std::scoped_lock lock(mLock); + std::scoped_lock lock(getLock()); + std::array<PointerCoords, MAX_POINTERS> outSpotCoords{}; + const ui::Transform& transform = getTransformForDisplayLocked(displayId); + + for (BitSet32 idBits(spotIdBits); !idBits.isEmpty();) { + const uint32_t index = spotIdToIndex[idBits.clearFirstMarkedBit()]; + + const vec2 xy = transform.transform(spotCoords[index].getXYValue()); + outSpotCoords[index].setAxisValue(AMOTION_EVENT_AXIS_X, xy.x); + outSpotCoords[index].setAxisValue(AMOTION_EVENT_AXIS_Y, xy.y); + + float pressure = spotCoords[index].getAxisValue(AMOTION_EVENT_AXIS_PRESSURE); + outSpotCoords[index].setAxisValue(AMOTION_EVENT_AXIS_PRESSURE, pressure); + } + auto it = mLocked.spotControllers.find(displayId); if (it == mLocked.spotControllers.end()) { mLocked.spotControllers.try_emplace(displayId, displayId, mContext); } - mLocked.spotControllers.at(displayId).setSpots(spotCoords, spotIdToIndex, spotIdBits); + mLocked.spotControllers.at(displayId).setSpots(outSpotCoords.data(), spotIdToIndex, spotIdBits); } void PointerController::clearSpots() { - std::scoped_lock lock(mLock); + std::scoped_lock lock(getLock()); clearSpotsLocked(); } -void PointerController::clearSpotsLocked() REQUIRES(mLock) { +void PointerController::clearSpotsLocked() { for (auto& [displayID, spotController] : mLocked.spotControllers) { spotController.clearSpots(); } @@ -153,7 +233,7 @@ void PointerController::setInactivityTimeout(InactivityTimeout inactivityTimeout } void PointerController::reloadPointerResources() { - std::scoped_lock lock(mLock); + std::scoped_lock lock(getLock()); for (auto& [displayID, spotController] : mLocked.spotControllers) { spotController.reloadSpotResources(); @@ -169,22 +249,28 @@ void PointerController::reloadPointerResources() { } void PointerController::setDisplayViewport(const DisplayViewport& viewport) { - std::scoped_lock lock(mLock); + std::scoped_lock lock(getLock()); 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; + } } void PointerController::updatePointerIcon(int32_t iconId) { - std::scoped_lock lock(mLock); + std::scoped_lock lock(getLock()); mCursorController.updatePointerIcon(iconId); } void PointerController::setCustomPointerIcon(const SpriteIcon& icon) { - std::scoped_lock lock(mLock); + std::scoped_lock lock(getLock()); mCursorController.setCustomPointerIcon(icon); } @@ -194,11 +280,11 @@ void PointerController::doInactivityTimeout() { void PointerController::onDisplayViewportsUpdated(std::vector<DisplayViewport>& viewports) { std::unordered_set<int32_t> displayIdSet; - for (DisplayViewport viewport : viewports) { + for (const DisplayViewport& viewport : viewports) { displayIdSet.insert(viewport.displayId); } - std::scoped_lock lock(mLock); + std::scoped_lock lock(getLock()); for (auto it = mLocked.spotControllers.begin(); it != mLocked.spotControllers.end();) { int32_t displayID = it->first; if (!displayIdSet.count(displayID)) { @@ -214,4 +300,17 @@ void PointerController::onDisplayViewportsUpdated(std::vector<DisplayViewport>& } } +void PointerController::onDisplayInfosChangedLocked( + const std::vector<gui::DisplayInfo>& displayInfo) { + mLocked.mDisplayInfos = displayInfo; +} + +const ui::Transform& PointerController::getTransformForDisplayLocked(int displayId) const { + const auto& di = mLocked.mDisplayInfos; + auto it = std::find_if(di.begin(), di.end(), [displayId](const gui::DisplayInfo& info) { + return info.displayId == displayId; + }); + return it != di.end() ? it->transform : kIdentityTransform; +} + } // namespace android diff --git a/libs/input/PointerController.h b/libs/input/PointerController.h index 97567bab202b..eab030f71e1a 100644 --- a/libs/input/PointerController.h +++ b/libs/input/PointerController.h @@ -47,7 +47,7 @@ public: const sp<PointerControllerPolicyInterface>& policy, const sp<Looper>& looper, const sp<SpriteController>& spriteController); - virtual ~PointerController() = default; + ~PointerController() override; virtual bool getBounds(float* outMinX, float* outMinY, float* outMaxX, float* outMaxY) const; virtual void move(float deltaX, float deltaY); @@ -72,11 +72,31 @@ public: void reloadPointerResources(); void onDisplayViewportsUpdated(std::vector<DisplayViewport>& viewports); + void onDisplayInfosChangedLocked(const std::vector<gui::DisplayInfo>& displayInfos) + REQUIRES(getLock()); + +protected: + using WindowListenerConsumer = + std::function<void(const sp<android::gui::WindowInfosListener>&)>; + + // Constructor used to test WindowInfosListener registration. + PointerController(const sp<PointerControllerPolicyInterface>& policy, const sp<Looper>& looper, + const sp<SpriteController>& spriteController, + WindowListenerConsumer registerListener, + WindowListenerConsumer unregisterListener); + private: + PointerController(const sp<PointerControllerPolicyInterface>& policy, const sp<Looper>& looper, + const sp<SpriteController>& spriteController); + friend PointerControllerContext::LooperCallback; friend PointerControllerContext::MessageHandler; - mutable std::mutex mLock; + // PointerController's DisplayInfoListener can outlive the PointerController because when the + // listener is registered, a strong pointer to the listener (which can extend its lifecycle) + // is given away. To avoid the small overhead of using two separate locks in these two objects, + // we use the DisplayInfoListener's lock in PointerController. + std::mutex& getLock() const; PointerControllerContext mContext; @@ -84,13 +104,32 @@ private: struct Locked { Presentation presentation; + int32_t pointerDisplayId = ADISPLAY_ID_NONE; + std::vector<gui::DisplayInfo> mDisplayInfos; std::unordered_map<int32_t /* displayId */, TouchSpotController> spotControllers; - } mLocked GUARDED_BY(mLock); + } mLocked GUARDED_BY(getLock()); - PointerController(const sp<PointerControllerPolicyInterface>& policy, const sp<Looper>& looper, - const sp<SpriteController>& spriteController); - void clearSpotsLocked(); + class DisplayInfoListener : public gui::WindowInfosListener { + public: + explicit DisplayInfoListener(PointerController* pc) : mPointerController(pc){}; + void onWindowInfosChanged(const std::vector<android::gui::WindowInfo>&, + const std::vector<android::gui::DisplayInfo>&) override; + void onPointerControllerDestroyed(); + + // This lock is also used by PointerController. See PointerController::getLock(). + std::mutex mLock; + + private: + PointerController* mPointerController GUARDED_BY(mLock); + }; + + sp<DisplayInfoListener> mDisplayInfoListener; + const WindowListenerConsumer mUnregisterWindowInfosListener; + + const ui::Transform& getTransformForDisplayLocked(int displayId) const REQUIRES(getLock()); + + void clearSpotsLocked() REQUIRES(getLock()); }; } // namespace android diff --git a/libs/input/PointerControllerContext.h b/libs/input/PointerControllerContext.h index 26a65a47471d..c2bc1e020279 100644 --- a/libs/input/PointerControllerContext.h +++ b/libs/input/PointerControllerContext.h @@ -79,6 +79,7 @@ public: 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; }; /* diff --git a/libs/input/SpriteController.cpp b/libs/input/SpriteController.cpp index d10e68816d28..2b809eab4ae4 100644 --- a/libs/input/SpriteController.cpp +++ b/libs/input/SpriteController.cpp @@ -27,10 +27,12 @@ namespace android { // --- SpriteController --- -SpriteController::SpriteController(const sp<Looper>& looper, int32_t overlayLayer) : - mLooper(looper), mOverlayLayer(overlayLayer) { +SpriteController::SpriteController(const sp<Looper>& looper, int32_t overlayLayer, + ParentSurfaceProvider parentSurfaceProvider) + : mLooper(looper), + mOverlayLayer(overlayLayer), + mParentSurfaceProvider(std::move(parentSurfaceProvider)) { mHandler = new WeakMessageHandler(this); - mLocked.transactionNestingCount = 0; mLocked.deferredSpriteUpdate = false; } @@ -68,8 +70,8 @@ void SpriteController::closeTransaction() { } void SpriteController::invalidateSpriteLocked(const sp<SpriteImpl>& sprite) { - bool wasEmpty = mLocked.invalidatedSprites.isEmpty(); - mLocked.invalidatedSprites.push(sprite); + bool wasEmpty = mLocked.invalidatedSprites.empty(); + mLocked.invalidatedSprites.push_back(sprite); if (wasEmpty) { if (mLocked.transactionNestingCount != 0) { mLocked.deferredSpriteUpdate = true; @@ -80,8 +82,8 @@ void SpriteController::invalidateSpriteLocked(const sp<SpriteImpl>& sprite) { } void SpriteController::disposeSurfaceLocked(const sp<SurfaceControl>& surfaceControl) { - bool wasEmpty = mLocked.disposedSurfaces.isEmpty(); - mLocked.disposedSurfaces.push(surfaceControl); + bool wasEmpty = mLocked.disposedSurfaces.empty(); + mLocked.disposedSurfaces.push_back(surfaceControl); if (wasEmpty) { mLooper->sendMessage(mHandler, Message(MSG_DISPOSE_SURFACES)); } @@ -111,7 +113,7 @@ void SpriteController::doUpdateSprites() { numSprites = mLocked.invalidatedSprites.size(); for (size_t i = 0; i < numSprites; i++) { - const sp<SpriteImpl>& sprite = mLocked.invalidatedSprites.itemAt(i); + const sp<SpriteImpl>& sprite = mLocked.invalidatedSprites[i]; updates.push(SpriteUpdate(sprite, sprite->getStateLocked())); sprite->resetDirtyLocked(); @@ -168,7 +170,7 @@ 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) { - t.setLayerStack(update.state.surfaceControl, update.state.displayId); + t.reparent(update.state.surfaceControl, mParentSurfaceProvider(update.state.displayId)); needApplyTransaction = true; } } @@ -303,7 +305,7 @@ void SpriteController::doUpdateSprites() { void SpriteController::doDisposeSurfaces() { // Collect disposed surfaces. - Vector<sp<SurfaceControl> > disposedSurfaces; + std::vector<sp<SurfaceControl>> disposedSurfaces; { // acquire lock AutoMutex _l(mLock); @@ -311,6 +313,13 @@ void SpriteController::doDisposeSurfaces() { mLocked.disposedSurfaces.clear(); } // release lock + // Remove the parent from all surfaces. + SurfaceComposerClient::Transaction t; + for (const sp<SurfaceControl>& sc : disposedSurfaces) { + t.reparent(sc, nullptr); + } + t.apply(); + // Release the last reference to each surface outside of the lock. // We don't want the surfaces to be deleted while we are holding our lock. disposedSurfaces.clear(); diff --git a/libs/input/SpriteController.h b/libs/input/SpriteController.h index 137b5646feae..2e9cb9685c46 100644 --- a/libs/input/SpriteController.h +++ b/libs/input/SpriteController.h @@ -114,7 +114,8 @@ protected: virtual ~SpriteController(); public: - SpriteController(const sp<Looper>& looper, int32_t overlayLayer); + using ParentSurfaceProvider = std::function<sp<SurfaceControl>(int /*displayId*/)>; + SpriteController(const sp<Looper>& looper, int32_t overlayLayer, ParentSurfaceProvider parent); /* Creates a new sprite, initially invisible. */ virtual sp<Sprite> createSprite(); @@ -245,12 +246,13 @@ private: sp<Looper> mLooper; const int32_t mOverlayLayer; sp<WeakMessageHandler> mHandler; + ParentSurfaceProvider mParentSurfaceProvider; sp<SurfaceComposerClient> mSurfaceComposerClient; struct Locked { - Vector<sp<SpriteImpl> > invalidatedSprites; - Vector<sp<SurfaceControl> > disposedSurfaces; + std::vector<sp<SpriteImpl>> invalidatedSprites; + std::vector<sp<SurfaceControl>> disposedSurfaces; uint32_t transactionNestingCount; bool deferredSpriteUpdate; } mLocked; // guarded by mLock diff --git a/libs/input/TEST_MAPPING b/libs/input/TEST_MAPPING index fe74c62d4ec1..9626d8dac787 100644 --- a/libs/input/TEST_MAPPING +++ b/libs/input/TEST_MAPPING @@ -1,7 +1,7 @@ { - "presubmit": [ - { - "name": "libinputservice_test" - } - ] + "imports": [ + { + "path": "frameworks/native/services/inputflinger" + } + ] } diff --git a/libs/input/tests/PointerController_test.cpp b/libs/input/tests/PointerController_test.cpp index b67088a389b6..f9752ed155df 100644 --- a/libs/input/tests/PointerController_test.cpp +++ b/libs/input/tests/PointerController_test.cpp @@ -56,9 +56,11 @@ public: 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; bool allResourcesAreLoaded(); bool noResourcesAreLoaded(); + std::optional<int32_t> getLastReportedPointerDisplayId() { return latestPointerDisplayId; } private: void loadPointerIconForType(SpriteIcon* icon, int32_t cursorType); @@ -66,6 +68,7 @@ private: bool pointerIconLoaded{false}; bool pointerResourcesLoaded{false}; bool additionalMouseResourcesLoaded{false}; + std::optional<int32_t /*displayId*/> latestPointerDisplayId; }; void MockPointerControllerPolicyInterface::loadPointerIcon(SpriteIcon* icon, int32_t) { @@ -126,12 +129,19 @@ void MockPointerControllerPolicyInterface::loadPointerIconForType(SpriteIcon* ic icon->hotSpotX = hotSpot.first; icon->hotSpotY = hotSpot.second; } + +void MockPointerControllerPolicyInterface::onPointerDisplayIdChanged(int32_t displayId, + float /*xPos*/, + float /*yPos*/) { + latestPointerDisplayId = displayId; +} + class PointerControllerTest : public Test { protected: PointerControllerTest(); ~PointerControllerTest(); - void ensureDisplayViewportIsSet(); + void ensureDisplayViewportIsSet(int32_t displayId = ADISPLAY_ID_DEFAULT); sp<MockSprite> mPointerSprite; sp<MockPointerControllerPolicyInterface> mPolicy; @@ -168,9 +178,9 @@ PointerControllerTest::~PointerControllerTest() { mThread.join(); } -void PointerControllerTest::ensureDisplayViewportIsSet() { +void PointerControllerTest::ensureDisplayViewportIsSet(int32_t displayId) { DisplayViewport viewport; - viewport.displayId = ADISPLAY_ID_DEFAULT; + viewport.displayId = displayId; viewport.logicalRight = 1600; viewport.logicalBottom = 1200; viewport.physicalRight = 800; @@ -255,4 +265,60 @@ TEST_F(PointerControllerTest, doesNotGetResourcesBeforeSettingViewport) { ensureDisplayViewportIsSet(); } +TEST_F(PointerControllerTest, notifiesPolicyWhenPointerDisplayChanges) { + EXPECT_FALSE(mPolicy->getLastReportedPointerDisplayId()) + << "A pointer display change does not occur when PointerController is created."; + + ensureDisplayViewportIsSet(ADISPLAY_ID_DEFAULT); + + const auto lastReportedPointerDisplayId = mPolicy->getLastReportedPointerDisplayId(); + ASSERT_TRUE(lastReportedPointerDisplayId) + << "The policy is notified of a pointer display change when the viewport is first set."; + EXPECT_EQ(ADISPLAY_ID_DEFAULT, *lastReportedPointerDisplayId) + << "Incorrect pointer display notified."; + + ensureDisplayViewportIsSet(42); + + EXPECT_EQ(42, *mPolicy->getLastReportedPointerDisplayId()) + << "The policy is notified when the pointer display changes."; + + // Release the PointerController. + mPointerController = nullptr; + + EXPECT_EQ(ADISPLAY_ID_NONE, *mPolicy->getLastReportedPointerDisplayId()) + << "The pointer display changes to invalid when PointerController is destroyed."; +} + +class PointerControllerWindowInfoListenerTest : public Test {}; + +class TestPointerController : public PointerController { +public: + TestPointerController(sp<android::gui::WindowInfosListener>& registeredListener, + const sp<Looper>& looper) + : PointerController( + new MockPointerControllerPolicyInterface(), looper, + new NiceMock<MockSpriteController>(looper), + [®isteredListener](const sp<android::gui::WindowInfosListener>& listener) { + // Register listener + registeredListener = listener; + }, + [®isteredListener](const sp<android::gui::WindowInfosListener>& listener) { + // Unregister listener + if (registeredListener == listener) registeredListener = nullptr; + }) {} +}; + +TEST_F(PointerControllerWindowInfoListenerTest, + doesNotCrashIfListenerCalledAfterPointerControllerDestroyed) { + sp<android::gui::WindowInfosListener> registeredListener; + sp<android::gui::WindowInfosListener> localListenerCopy; + { + TestPointerController pointerController(registeredListener, new Looper(false)); + ASSERT_NE(nullptr, registeredListener) << "WindowInfosListener was not registered"; + localListenerCopy = registeredListener; + } + EXPECT_EQ(nullptr, registeredListener) << "WindowInfosListener was not unregistered"; + localListenerCopy->onWindowInfosChanged({}, {}); +} + } // namespace android diff --git a/libs/input/tests/mocks/MockSpriteController.h b/libs/input/tests/mocks/MockSpriteController.h index a034f66c9abf..62f1d65e77a5 100644 --- a/libs/input/tests/mocks/MockSpriteController.h +++ b/libs/input/tests/mocks/MockSpriteController.h @@ -26,7 +26,8 @@ namespace android { class MockSpriteController : public SpriteController { public: - MockSpriteController(sp<Looper> looper) : SpriteController(looper, 0) {} + MockSpriteController(sp<Looper> looper) + : SpriteController(looper, 0, [](int) { return nullptr; }) {} ~MockSpriteController() {} MOCK_METHOD(sp<Sprite>, createSprite, (), (override)); diff --git a/libs/services/Android.bp b/libs/services/Android.bp index bf2e764aae6a..f656ebfc3b77 100644 --- a/libs/services/Android.bp +++ b/libs/services/Android.bp @@ -27,6 +27,7 @@ cc_library_shared { name: "libservices", srcs: [ ":IDropBoxManagerService.aidl", + ":ILogcatManagerService_aidl", "src/content/ComponentName.cpp", "src/os/DropBoxManager.cpp", ], diff --git a/libs/storage/IMountService.cpp b/libs/storage/IMountService.cpp index fd6e6e932ebc..99508a2943ff 100644 --- a/libs/storage/IMountService.cpp +++ b/libs/storage/IMountService.cpp @@ -48,8 +48,6 @@ enum { TRANSACTION_isObbMounted, TRANSACTION_getMountedObbPath, TRANSACTION_isExternalStorageEmulated, - TRANSACTION_decryptStorage, - TRANSACTION_encryptStorage, }; class BpMountService: public BpInterface<IMountService> @@ -442,14 +440,13 @@ public: reply.readExceptionCode(); } - void mountObb(const String16& rawPath, const String16& canonicalPath, const String16& key, + void mountObb(const String16& rawPath, const String16& canonicalPath, const sp<IObbActionListener>& token, int32_t nonce, const sp<ObbInfo>& obbInfo) { Parcel data, reply; data.writeInterfaceToken(IMountService::getInterfaceDescriptor()); data.writeString16(rawPath); data.writeString16(canonicalPath); - data.writeString16(key); data.writeStrongBinder(IInterface::asBinder(token)); data.writeInt32(nonce); obbInfo->writeToParcel(&data); @@ -518,40 +515,6 @@ public: path = reply.readString16(); return true; } - - int32_t decryptStorage(const String16& password) - { - Parcel data, reply; - data.writeInterfaceToken(IMountService::getInterfaceDescriptor()); - data.writeString16(password); - if (remote()->transact(TRANSACTION_decryptStorage, data, &reply) != NO_ERROR) { - ALOGD("decryptStorage could not contact remote\n"); - return -1; - } - int32_t err = reply.readExceptionCode(); - if (err < 0) { - ALOGD("decryptStorage caught exception %d\n", err); - return err; - } - return reply.readInt32(); - } - - int32_t encryptStorage(const String16& password) - { - Parcel data, reply; - data.writeInterfaceToken(IMountService::getInterfaceDescriptor()); - data.writeString16(password); - if (remote()->transact(TRANSACTION_encryptStorage, data, &reply) != NO_ERROR) { - ALOGD("encryptStorage could not contact remote\n"); - return -1; - } - int32_t err = reply.readExceptionCode(); - if (err < 0) { - ALOGD("encryptStorage caught exception %d\n", err); - return err; - } - return reply.readInt32(); - } }; IMPLEMENT_META_INTERFACE(MountService, "android.os.storage.IStorageManager") diff --git a/libs/storage/include/storage/IMountService.h b/libs/storage/include/storage/IMountService.h index 2463e023efc1..5a9c39bc021b 100644 --- a/libs/storage/include/storage/IMountService.h +++ b/libs/storage/include/storage/IMountService.h @@ -64,14 +64,12 @@ public: virtual void shutdown(const sp<IMountShutdownObserver>& observer) = 0; virtual void finishMediaUpdate() = 0; virtual void mountObb(const String16& rawPath, const String16& canonicalPath, - const String16& key, const sp<IObbActionListener>& token, - const int32_t nonce, const sp<ObbInfo>& obbInfo) = 0; + const sp<IObbActionListener>& token, const int32_t nonce, + const sp<ObbInfo>& obbInfo) = 0; virtual void unmountObb(const String16& filename, const bool force, const sp<IObbActionListener>& token, const int32_t nonce) = 0; virtual bool isObbMounted(const String16& filename) = 0; virtual bool getMountedObbPath(const String16& filename, String16& path) = 0; - virtual int32_t decryptStorage(const String16& password) = 0; - virtual int32_t encryptStorage(const String16& password) = 0; }; // ---------------------------------------------------------------------------- diff --git a/libs/tracingproxy/Android.bp b/libs/tracingproxy/Android.bp index 7126bfac773d..23d107b56340 100644 --- a/libs/tracingproxy/Android.bp +++ b/libs/tracingproxy/Android.bp @@ -37,6 +37,7 @@ cc_library_shared { srcs: [ ":ITracingServiceProxy.aidl", + ":TraceReportParams.aidl", ], shared_libs: [ diff --git a/libs/usb/tests/accessorytest/f_accessory.h b/libs/usb/tests/accessorytest/f_accessory.h index 312f4ba6eed3..75e017c16674 100644 --- a/libs/usb/tests/accessorytest/f_accessory.h +++ b/libs/usb/tests/accessorytest/f_accessory.h @@ -1,148 +1,53 @@ -/* - * Gadget Function Driver for Android USB accessories - * - * Copyright (C) 2011 Google, Inc. - * Author: Mike Lockwood <lockwood@android.com> - * - * This software is licensed under the terms of the GNU General Public - * License version 2, as published by the Free Software Foundation, and - * may be copied, distributed, and modified under those terms. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - */ - -#ifndef __LINUX_USB_F_ACCESSORY_H -#define __LINUX_USB_F_ACCESSORY_H - -/* Use Google Vendor ID when in accessory mode */ +/**************************************************************************** + **************************************************************************** + *** + *** This header was automatically generated from a Linux kernel header + *** of the same name, to make information necessary for userspace to + *** call into the kernel available to libc. It contains only constants, + *** structures, and macros generated from the original header, and thus, + *** contains no copyrightable information. + *** + *** To edit the content of this header, modify the corresponding + *** source file (e.g. under external/kernel-headers/original/) then + *** run bionic/libc/kernel/tools/update_all.py + *** + *** Any manual change here will be lost the next time this script will + *** be run. You've been warned! + *** + **************************************************************************** + ****************************************************************************/ +#ifndef _UAPI_LINUX_USB_F_ACCESSORY_H +#define _UAPI_LINUX_USB_F_ACCESSORY_H #define USB_ACCESSORY_VENDOR_ID 0x18D1 - - -/* Product ID to use when in accessory mode */ #define USB_ACCESSORY_PRODUCT_ID 0x2D00 - -/* Product ID to use when in accessory mode and adb is enabled */ +/* WARNING: DO NOT EDIT, AUTO-GENERATED CODE - SEE TOP FOR INSTRUCTIONS */ #define USB_ACCESSORY_ADB_PRODUCT_ID 0x2D01 - -/* Indexes for strings sent by the host via ACCESSORY_SEND_STRING */ -#define ACCESSORY_STRING_MANUFACTURER 0 -#define ACCESSORY_STRING_MODEL 1 -#define ACCESSORY_STRING_DESCRIPTION 2 -#define ACCESSORY_STRING_VERSION 3 -#define ACCESSORY_STRING_URI 4 -#define ACCESSORY_STRING_SERIAL 5 - -/* Control request for retrieving device's protocol version - * - * requestType: USB_DIR_IN | USB_TYPE_VENDOR - * request: ACCESSORY_GET_PROTOCOL - * value: 0 - * index: 0 - * data version number (16 bits little endian) - * 1 for original accessory support - * 2 adds audio and HID support - */ -#define ACCESSORY_GET_PROTOCOL 51 - -/* Control request for host to send a string to the device - * - * requestType: USB_DIR_OUT | USB_TYPE_VENDOR - * request: ACCESSORY_SEND_STRING - * value: 0 - * index: string ID - * data zero terminated UTF8 string - * - * The device can later retrieve these strings via the - * ACCESSORY_GET_STRING_* ioctls - */ -#define ACCESSORY_SEND_STRING 52 - -/* Control request for starting device in accessory mode. - * The host sends this after setting all its strings to the device. - * - * requestType: USB_DIR_OUT | USB_TYPE_VENDOR - * request: ACCESSORY_START - * value: 0 - * index: 0 - * data none - */ -#define ACCESSORY_START 53 - -/* Control request for registering a HID device. - * Upon registering, a unique ID is sent by the accessory in the - * value parameter. This ID will be used for future commands for - * the device - * - * requestType: USB_DIR_OUT | USB_TYPE_VENDOR - * request: ACCESSORY_REGISTER_HID_DEVICE - * value: Accessory assigned ID for the HID device - * index: total length of the HID report descriptor - * data none - */ -#define ACCESSORY_REGISTER_HID 54 - -/* Control request for unregistering a HID device. - * - * requestType: USB_DIR_OUT | USB_TYPE_VENDOR - * request: ACCESSORY_REGISTER_HID - * value: Accessory assigned ID for the HID device - * index: 0 - * data none - */ -#define ACCESSORY_UNREGISTER_HID 55 - -/* Control request for sending the HID report descriptor. - * If the HID descriptor is longer than the endpoint zero max packet size, - * the descriptor will be sent in multiple ACCESSORY_SET_HID_REPORT_DESC - * commands. The data for the descriptor must be sent sequentially - * if multiple packets are needed. - * - * requestType: USB_DIR_OUT | USB_TYPE_VENDOR - * request: ACCESSORY_SET_HID_REPORT_DESC - * value: Accessory assigned ID for the HID device - * index: offset of data in descriptor - * (needed when HID descriptor is too big for one packet) - * data the HID report descriptor - */ -#define ACCESSORY_SET_HID_REPORT_DESC 56 - -/* Control request for sending HID events. - * - * requestType: USB_DIR_OUT | USB_TYPE_VENDOR - * request: ACCESSORY_SEND_HID_EVENT - * value: Accessory assigned ID for the HID device - * index: 0 - * data the HID report for the event - */ -#define ACCESSORY_SEND_HID_EVENT 57 - -/* Control request for setting the audio mode. - * - * requestType: USB_DIR_OUT | USB_TYPE_VENDOR - * request: ACCESSORY_SET_AUDIO_MODE - * value: 0 - no audio - * 1 - device to host, 44100 16-bit stereo PCM - * index: 0 - * data the HID report for the event - */ -#define ACCESSORY_SET_AUDIO_MODE 58 - - - -/* ioctls for retrieving strings set by the host */ -#define ACCESSORY_GET_STRING_MANUFACTURER _IOW('M', 1, char[256]) -#define ACCESSORY_GET_STRING_MODEL _IOW('M', 2, char[256]) -#define ACCESSORY_GET_STRING_DESCRIPTION _IOW('M', 3, char[256]) -#define ACCESSORY_GET_STRING_VERSION _IOW('M', 4, char[256]) -#define ACCESSORY_GET_STRING_URI _IOW('M', 5, char[256]) -#define ACCESSORY_GET_STRING_SERIAL _IOW('M', 6, char[256]) -/* returns 1 if there is a start request pending */ -#define ACCESSORY_IS_START_REQUESTED _IO('M', 7) -/* returns audio mode (set via the ACCESSORY_SET_AUDIO_MODE control request) */ -#define ACCESSORY_GET_AUDIO_MODE _IO('M', 8) - -#endif /* __LINUX_USB_F_ACCESSORY_H */ +#define ACCESSORY_STRING_MANUFACTURER 0 +#define ACCESSORY_STRING_MODEL 1 +#define ACCESSORY_STRING_DESCRIPTION 2 +/* WARNING: DO NOT EDIT, AUTO-GENERATED CODE - SEE TOP FOR INSTRUCTIONS */ +#define ACCESSORY_STRING_VERSION 3 +#define ACCESSORY_STRING_URI 4 +#define ACCESSORY_STRING_SERIAL 5 +#define ACCESSORY_GET_PROTOCOL 51 +/* WARNING: DO NOT EDIT, AUTO-GENERATED CODE - SEE TOP FOR INSTRUCTIONS */ +#define ACCESSORY_SEND_STRING 52 +#define ACCESSORY_START 53 +#define ACCESSORY_REGISTER_HID 54 +#define ACCESSORY_UNREGISTER_HID 55 +/* WARNING: DO NOT EDIT, AUTO-GENERATED CODE - SEE TOP FOR INSTRUCTIONS */ +#define ACCESSORY_SET_HID_REPORT_DESC 56 +#define ACCESSORY_SEND_HID_EVENT 57 +#define ACCESSORY_SET_AUDIO_MODE 58 +#define ACCESSORY_GET_STRING_MANUFACTURER _IOW('M', 1, char[256]) +/* WARNING: DO NOT EDIT, AUTO-GENERATED CODE - SEE TOP FOR INSTRUCTIONS */ +#define ACCESSORY_GET_STRING_MODEL _IOW('M', 2, char[256]) +#define ACCESSORY_GET_STRING_DESCRIPTION _IOW('M', 3, char[256]) +#define ACCESSORY_GET_STRING_VERSION _IOW('M', 4, char[256]) +#define ACCESSORY_GET_STRING_URI _IOW('M', 5, char[256]) +/* WARNING: DO NOT EDIT, AUTO-GENERATED CODE - SEE TOP FOR INSTRUCTIONS */ +#define ACCESSORY_GET_STRING_SERIAL _IOW('M', 6, char[256]) +#define ACCESSORY_IS_START_REQUESTED _IO('M', 7) +#define ACCESSORY_GET_AUDIO_MODE _IO('M', 8) +#endif +/* WARNING: DO NOT EDIT, AUTO-GENERATED CODE - SEE TOP FOR INSTRUCTIONS */ |